Просмотр исходного кода

新加了对铜仁学院旧版的教务系统的支持,使用v2 API

懒狗学校,还以为没开学就不允许查课表呢,结果是因为课表没排完查不了
DaoGuan 3 недель назад
Родитель
Сommit
ce582bbffb
2 измененных файлов с 261 добавлено и 2 удалено
  1. 9 2
      resources/GZTRC/adapters.yaml
  2. 252 0
      resources/GZTRC/gztrc_old.js

+ 9 - 2
resources/GZTRC/adapters.yaml

@@ -1,9 +1,16 @@
 # resources/GZTRC/adapters.yaml
 adapters:
   - adapter_id: "GZTRC"
-    adapter_name: "铜仁学院联奕教务"
+    adapter_name: "铜仁学院联奕教务(新版教务)"
     category: "BACHELOR_AND_ASSOCIATE"
     asset_js_path: "gztrc.js"
     import_url: "https://gztrc.edu.cn:17232/"
     maintainer: "Daoguan-king"
-    description: "由于教务系统未适配手机端,须在右上角选择\"切换到电脑模式\",然后点击\"刷新\",正常登录后点击下方\"执行导入\"按钮进行导入。"
+    description: "由于教务系统未适配手机端,须在右上角选择\"切换到电脑模式\",然后点击\"刷新\",正常登录后点击下方\"执行导入\"按钮进行导入。"
+  - adapter_id: "GZTRC_OLD"
+    adapter_name: "铜仁学院移动教务(旧版教务)"
+    category: "BACHELOR_AND_ASSOCIATE"
+    asset_js_path: "gztrc_old.js"
+    import_url: "https://gztrc.edu.cn:17230/yjw/pages/login/login.html"
+    maintainer: "Daoguan-king"
+    description: "此教务系统适配手机端,请在教务系统中正常登录并进入主页后,点击教务系统下方\"快捷查询->课程表\"页面后再电击\"执行导入\"按钮进行导入。由于学校太懒,校历数据未配置,导入后需在软件中手动设置开学日期。"

+ 252 - 0
resources/GZTRC/gztrc_old.js

@@ -0,0 +1,252 @@
+// 铜仁学院移动教务 (gztrc.edu.cn:17230/yjw) 拾光课程表适配脚本
+// 使用个人课表接口(courseSchedule)查询
+// 登录后需要从 localStorage 中读取用户信息,无需在脚本内登录
+
+const API = {
+    semesters: "/api/baseInfo/mobile/common/querySemester",
+    weeks: "/api/baseInfo/mobile/common/queryCurrentSemesterWeekList",
+    times: "/api/baseInfo/mobile/common/timeNameDetail",
+    schedule: "/api/arrange/mobile/courseSchedule/courseSchedule"
+};
+
+async function apiGet(path) {
+    const res = await fetch(path, { credentials: "include" });
+    if (!res.ok) throw new Error("请求失败: " + path);
+    return res.json();
+}
+
+async function apiPost(path, data, token) {
+    const res = await fetch(path, {
+        method: "POST",
+        headers: {
+            "Content-Type": "application/json",
+            "accessToken": token
+        },
+        credentials: "include",
+        body: JSON.stringify(data)
+    });
+    if (!res.ok) throw new Error("请求失败: " + path);
+    return res.json();
+}
+
+function parseUserFromStorage() {
+    try {
+        const raw = localStorage.getItem("user_info");
+        if (!raw) return null;
+        const data = JSON.parse(raw);
+        if (!data || !data.accessToken || !data.userInfo || !data.userInfo.userId) return null;
+        return {
+            token: data.accessToken,
+            userId: data.userInfo.userId,
+            userType: data.userInfo.userType || "0",
+            name: data.userInfo.name || ""
+        };
+    } catch (e) {
+        return null;
+    }
+}
+
+/**
+ * 解析单条课表记录(接口按"天 x 节次"逐格返回,每格一条记录)
+ * week 为空时使用记录自带的 weeks 字段
+ */
+function parseCourseItem(c, week) {
+    if (!c || !c.courseName) return null;
+    const section = parseInt(c.time);
+    if (isNaN(section) || section < 1) return null;
+
+    let w = week;
+    if (w === undefined || w === null) {
+        w = parseInt(c.weeks);
+        if (isNaN(w) || w < 1) return null;
+    }
+
+    // 教务dayOfWeek: 1=周日,2=周一,3=周二,4=周三,5=周四,6=周五,7=周六
+    // 规范day: 1=周一,2=周二,3=周三,4=周四,5=周五,6=周六,7=周日
+    const dayMap = { "1": 7, "2": 1, "3": 2, "4": 3, "5": 4, "6": 5, "7": 6 };
+    const day = dayMap[String(c.dayOfWeek)] || parseInt(c.dayOfWeek);
+
+    let position = c.classroomName;
+    if (!position || position === "," || position.trim() === "") {
+        position = "未知地点";
+    }
+
+    return {
+        name: c.courseName,
+        teacher: c.teacherName || "",
+        position: position,
+        day: day,
+        section: section,
+        week: w
+    };
+}
+
+function sameWeekSet(a, b) {
+    if (a.length !== b.length) return false;
+    for (let i = 0; i < a.length; i++) {
+        if (a[i] !== b[i]) return false;
+    }
+    return true;
+}
+
+function pushWeek(list, w) {
+    if (list.indexOf(w) === -1) list.push(w);
+}
+
+/**
+ * 将逐格记录合并为课程:
+ * 先按(天,课程,教师,地点,节次)聚合周次,再合并连续节次(且周次集合相同)
+ */
+function mergeToCourses(records) {
+    const groups = new Map();
+    for (let r of records) {
+        const key = [r.day, r.name, r.teacher, r.position].join("|");
+        let g = groups.get(key);
+        if (!g) {
+            g = { day: r.day, name: r.name, teacher: r.teacher, position: r.position, secMap: new Map() };
+            groups.set(key, g);
+        }
+        if (!g.secMap.has(r.section)) g.secMap.set(r.section, []);
+        pushWeek(g.secMap.get(r.section), r.week);
+    }
+
+    const courses = [];
+    for (let g of groups.values()) {
+        const sections = Array.from(g.secMap.keys()).sort((a, b) => a - b);
+        let cur = null;
+        for (let sec of sections) {
+            const weeks = g.secMap.get(sec).sort((a, b) => a - b);
+            if (cur && sec === cur.endSection + 1 && sameWeekSet(cur.weeks, weeks)) {
+                cur.endSection = sec;
+            } else {
+                cur = {
+                    name: g.name,
+                    teacher: g.teacher,
+                    position: g.position,
+                    day: g.day,
+                    startSection: sec,
+                    endSection: sec,
+                    weeks: weeks.slice()
+                };
+                courses.push(cur);
+            }
+        }
+    }
+    return courses;
+}
+
+async function querySchedule(user, semesterId, weeks) {
+    const body = {
+        academicYearSemester: semesterId,
+        userId: user.userId,
+        userType: user.userType,
+        weeks: weeks
+    };
+    const json = await apiPost(API.schedule, body, user.token);
+    if (json.code !== 200) {
+        if (json.code === 53000505) {
+            throw new Error("登录已失效,请重新登录教务系统后重试。");
+        }
+        throw new Error(json.message || "获取课表失败");
+    }
+    if (!json.data || !json.data.course) throw new Error("课表数据为空");
+    return json.data.course;
+}
+
+async function fetchAllCourses(user, semesterId, totalWeeks) {
+    // 注意:一次请求多周时,接口会把同一格子的多周数据以逗号拼接返回
+    // (如 courseName:"课A,课B"、teacherName:"师A,师A,..."),无法可靠拆分,
+    // 因此必须逐周查询,单周返回的数据是干净的
+    const records = [];
+    for (let w = 1; w <= totalWeeks; w++) {
+        const list = await querySchedule(user, semesterId, [w]);
+        records.push(...list.map(c => parseCourseItem(c, w)).filter(c => c !== null));
+    }
+    return mergeToCourses(records);
+}
+
+async function fetchTimeSlots() {
+    const json = await apiGet(API.times);
+    if (json.code !== 200 || !json.data) throw new Error("获取作息时间失败");
+    return json.data.map(t => ({
+        number: parseInt(t.timeCode),
+        startTime: t.startTime,
+        endTime: t.endTime
+    }));
+}
+
+async function runImportFlow() {
+    try {
+        window.shiguangBridge.showToast("开始导入课表...");
+
+        // 1. 读取登录信息
+        const user = parseUserFromStorage();
+        if (!user) {
+            await window.shiguangBridgePromise.showAlert(
+                "请先登录",
+                "请在教务系统(移动教务)中完成登录,进入主页后再次点击执行导入。",
+                "知道了"
+            );
+            return;
+        }
+
+        // 2. 获取学期列表
+        window.shiguangBridge.showToast("正在获取学期列表...");
+        const semesterJson = await apiGet(API.semesters);
+        if (semesterJson.code !== 200) throw new Error("获取学期列表失败");
+        const semesters = (semesterJson.data || []).filter(s => s && s.semesterId && /^\d{4}-\d{4}-\d+$/.test(s.semesterId));
+        if (semesters.length === 0) throw new Error("未能获取学期列表");
+
+        // 3. 选择学期
+        let defaultIndex = 0;
+        const currentIdx = semesters.findIndex(s => s.isCurrentSemester === "1");
+        if (currentIdx !== -1) defaultIndex = currentIdx;
+        const semesterIndex = await window.shiguangBridgePromise.showSingleSelection(
+            "选择学期",
+            JSON.stringify(semesters.map(s => s.semesterName)),
+            defaultIndex
+        );
+        if (semesterIndex === null) {
+            window.shiguangBridge.showToast("导入已取消。");
+            return;
+        }
+        const semester = semesters[semesterIndex];
+
+        // 4. 获取本学期周数
+        let totalWeeks = 20;
+        try {
+            const weekJson = await apiGet(API.weeks);
+            if (weekJson.code === 200 && weekJson.data && weekJson.data.length > 0) {
+                totalWeeks = weekJson.data.length;
+            }
+        } catch (e) {
+            // 使用默认周数
+        }
+
+        // 5. 获取课表数据
+        window.shiguangBridge.showToast("正在获取课表数据,共 " + totalWeeks + " 周...");
+        const courses = await fetchAllCourses(user, semester.semesterId, totalWeeks);
+        if (courses.length === 0) throw new Error("未解析到课程数据,可能该学期暂无课表。");
+
+        // 6. 保存预设时间段
+        try {
+            const timeSlots = await fetchTimeSlots();
+            if (timeSlots.length > 0) {
+                await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
+            }
+        } catch (e) {
+            window.shiguangBridge.showToast("时间段导入失败,但课程将继续导入。");
+        }
+
+        // 7. 保存课程数据
+        await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses));
+        window.shiguangBridge.showToast("成功导入 " + courses.length + " 条课程记录!");
+        window.shiguangBridge.notifyTaskCompletion();
+
+    } catch (e) {
+        console.error("[适配脚本错误] " + e.message);
+        window.shiguangBridge.showToast("导入失败: " + e.message);
+    }
+}
+
+runImportFlow();