Răsfoiți Sursa

add: 大连理工大学本科教务系统适配(#727)

dffzcqs78694 2 zile în urmă
părinte
comite
0de4191d49
3 a modificat fișierele cu 445 adăugiri și 0 ștergeri
  1. 6 0
      index/root_index.yaml
  2. 9 0
      resources/DLUT/adapters.yaml
  3. 430 0
      resources/DLUT/dlut_01.js

+ 6 - 0
index/root_index.yaml

@@ -1087,3 +1087,9 @@ schools:
     name: "南开大学"
     initial: "N"
     resource_folder: "NANKAI"
+    
+  - id: "DLUT"
+    name: "大连理工大学"
+    initial: "D"
+    resource_folder: "DLUT"    
+    

+ 9 - 0
resources/DLUT/adapters.yaml

@@ -0,0 +1,9 @@
+# resources/DLUT/adapters.yaml
+adapters:
+  - adapter_id: "DLUT_01"
+    adapter_name: "大连理工大学本科教务系统"
+    category: "BACHELOR_AND_ASSOCIATE"
+    asset_js_path: "dlut_01.js"
+    import_url: "http://jxgl.dlut.edu.cn/student/for-std/course-table"
+    maintainer: "dffzcqs78694"
+    description: "登录统一身份认证后自动跳转到课表页面。\n支持自动选择学期、导入课程与作息时间、自动配置开学日期。"

+ 430 - 0
resources/DLUT/dlut_01.js

@@ -0,0 +1,430 @@
+// 大连理工大学本科教务系统适配脚本
+// 适配人: dffzcqs78694
+//
+// 接口链路(与页面实际请求一致):
+// 1. GET  /student/for-std/course-table                  → 解析学期下拉框 + 提取 stdPersonId
+// 2. GET  /student/ws/schedule-table/get-data?bizTypeId=2&semesterId={id} → lessonIds / timeTableLayoutId / weekIndices
+// 3. POST /student/ws/schedule-table/datum               → lessonList(课程) + scheduleList(排课明细)
+// 4. POST /student/ws/schedule-table/timetable-layout    → 节次时间定义
+// 5. GET  /student/ws/semester/get/{semesterId}          → 学期起止日期(用于推算开学日期, 失败则回退)
+
+// ---- 节次时间表兜底数据(接口失败时使用, 与大工 timetable-layout 一致) ----
+const FALLBACK_TIME_SLOTS = [
+    { "number": 1, "startTime": "08:00", "endTime": "08:45" },
+    { "number": 2, "startTime": "08:50", "endTime": "09:35" },
+    { "number": 3, "startTime": "10:05", "endTime": "10:50" },
+    { "number": 4, "startTime": "10:55", "endTime": "11:40" },
+    { "number": 5, "startTime": "13:30", "endTime": "14:15" },
+    { "number": 6, "startTime": "14:20", "endTime": "15:05" },
+    { "number": 7, "startTime": "15:35", "endTime": "16:20" },
+    { "number": 8, "startTime": "16:25", "endTime": "17:10" },
+    { "number": 9, "startTime": "18:00", "endTime": "18:45" },
+    { "number": 10, "startTime": "18:50", "endTime": "19:35" },
+    { "number": 11, "startTime": "19:40", "endTime": "20:25" },
+    { "number": 12, "startTime": "20:30", "endTime": "21:15" }
+];
+
+// 时间数字格式化: 1800 -> "18:00"
+function formatTimeNum(timeNum) {
+    if (timeNum === null || timeNum === undefined) return "";
+    const str = String(timeNum).padStart(4, '0');
+    return `${str.slice(0, 2)}:${str.slice(2, 4)}`;
+}
+
+// 判断时间数字是否在 [start, end] 区间内(用于节次映射)
+function timeInRange(timeNum, startNum, endNum) {
+    return timeNum >= startNum && timeNum <= endNum;
+}
+
+// 构建节次列表(数字格式): 优先用接口返回的 courseUnitList, 失败时用内置兜底作息表
+function buildCourseUnits(layout) {
+    if (layout && Array.isArray(layout.courseUnitList) && layout.courseUnitList.length > 0) {
+        return layout.courseUnitList;
+    }
+    // 兜底: 内置节次表(与大工 timetable-layout 实际数据一致)
+    return FALLBACK_TIME_SLOTS.map(slot => ({
+        indexNo: slot.number,
+        startTime: parseInt(slot.startTime.replace(':', ''), 10),
+        endTime: parseInt(slot.endTime.replace(':', ''), 10)
+    }));
+}
+
+// ---- 纯函数: 根据课程原始排课明细解析为拾光课程格式 ----
+// printData: datum 响应中的 result 对象
+// layout: timetable-layout 响应中的 result 对象
+// 返回课程数组
+function parseCourses(printData, layout) {
+    const lessonList = printData.lessonList || [];
+    const scheduleList = printData.scheduleList || [];
+    const lessonById = {};
+    lessonList.forEach(lesson => { lessonById[lesson.id] = lesson; });
+
+    // 从 layout 中构建节次列表 (用于 startTime/endTime -> 节次号)
+    const units = buildCourseUnits(layout);
+
+    // 按 (lessonId, weekday, startTime, endTime) 分组, 收集周次
+    const groups = {};
+    scheduleList.forEach(schedule => {
+        const key = `${schedule.lessonId}|${schedule.weekday}|${schedule.startTime}|${schedule.endTime}`;
+        if (!groups[key]) {
+            groups[key] = { schedule: schedule, weeks: [] };
+        }
+        if (schedule.weekIndex !== null && schedule.weekIndex !== undefined) {
+            groups[key].weeks.push(schedule.weekIndex);
+        }
+    });
+
+    const courses = [];
+    Object.keys(groups).forEach(key => {
+        const group = groups[key];
+        const schedule = group.schedule;
+        const lesson = lessonById[schedule.lessonId] || {};
+
+        // 星期几
+        const day = schedule.weekday;
+
+        // 节次映射: 找到所有 startTime>=调度开始 && endTime<=调度结束 的节次
+        let startSection = null;
+        let endSection = null;
+        if (units.length > 0 && schedule.startTime !== null && schedule.endTime !== null) {
+            const matched = units.filter(unit =>
+                timeInRange(unit.startTime, schedule.startTime, schedule.endTime) &&
+                timeInRange(unit.endTime, schedule.startTime, schedule.endTime)
+            );
+            if (matched.length > 0) {
+                startSection = matched[0].indexNo;
+                endSection = matched[matched.length - 1].indexNo;
+            }
+        }
+
+        // 教师: 优先取排课明细中的教师, 否则取课程下的教师列表
+        let teacher = schedule.personName || "";
+        if (!teacher && lesson.teacherAssignmentList) {
+            teacher = lesson.teacherAssignmentList.map(t => t.name).filter(Boolean).join(" ");
+        }
+
+        // 教室
+        let position = "";
+        if (schedule.room && schedule.room.nameZh) {
+            position = schedule.room.nameZh;
+        } else if (schedule.customPlace) {
+            position = schedule.customPlace;
+        }
+
+        // 周次去重排序
+        const weeks = Array.from(new Set(group.weeks)).sort((a, b) => a - b);
+
+        const course = {
+            name: lesson.courseName || "",
+            teacher: teacher,
+            position: position,
+            day: day,
+            startSection: startSection,
+            endSection: endSection,
+            weeks: weeks
+        };
+        courses.push(course);
+    });
+
+    return courses;
+}
+
+// ---- 纯函数: 生成预设时间段 ----
+// layout: timetable-layout 响应中的 result 对象
+function buildTimeSlots(layout) {
+    if (layout && Array.isArray(layout.courseUnitList) && layout.courseUnitList.length > 0) {
+        return layout.courseUnitList.map(unit => ({
+            number: unit.indexNo,
+            startTime: formatTimeNum(unit.startTime),
+            endTime: formatTimeNum(unit.endTime)
+        }));
+    }
+    return FALLBACK_TIME_SLOTS;
+}
+
+// ---- 纯函数: 推算学期开始日期(第1周周一) ----
+// 排课明细的 date + weekIndex 已知, 用最小的 date 算出其所在周周一,
+// 再按 weekIndex 差值回退到第 1 周周一。
+// 例: 第3周周一=2026-09-14, 则第1周周一 = 09-14 - 2周 = 2026-08-31
+function calcSemesterStartDate(printData) {
+    let minDate = null;
+    let minWeekIndex = null;
+
+    (printData.scheduleList || []).forEach(s => {
+        if (s.date && s.date.length >= 10 && s.weekIndex !== null && s.weekIndex !== undefined) {
+            if (!minDate || s.date < minDate) {
+                minDate = s.date;
+                minWeekIndex = s.weekIndex;
+            }
+        }
+    });
+
+    if (!minDate || !minWeekIndex) return null;
+
+    // 该日期所在周的周一
+    const dt = new Date(minDate);
+    const day = dt.getDay(); // 0=周日
+    const diff = (day === 0 ? -6 : 1 - day);
+    dt.setDate(dt.getDate() + diff);
+
+    // 回退到第1周
+    dt.setDate(dt.getDate() - (minWeekIndex - 1) * 7);
+    return dt.toISOString().split('T')[0];
+}
+
+// ---- 桥接封装 ----
+async function bridgeSaveCourses(courses) {
+    const result = await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses));
+    if (result === true) {
+        window.shiguangBridge.showToast(`成功导入 ${courses.length} 门课程!`);
+        return true;
+    }
+    window.shiguangBridge.showToast("课程导入未成功,请查看日志。");
+    return false;
+}
+
+async function bridgeSaveTimeSlots(timeSlots) {
+    const result = await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
+    if (result === true) {
+        window.shiguangBridge.showToast(`成功导入 ${timeSlots.length} 个时间段!`);
+        return true;
+    }
+    window.shiguangBridge.showToast("时间段导入失败,请查看日志。");
+    return false;
+}
+
+async function bridgeSaveConfig(config) {
+    const result = await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(config));
+    if (result === true) {
+        window.shiguangBridge.showToast("课表配置导入成功!");
+        return true;
+    }
+    window.shiguangBridge.showToast("课表配置导入失败,请查看日志。");
+    return false;
+}
+
+// ---- 1. 公告 ----
+async function promptUserToStart() {
+    try {
+        const confirmed = await window.shiguangBridgePromise.showAlert(
+            "导入提醒",
+            "请确保您已登录教务系统,导入前请确认当前学期正确。",
+            "好的,开始导入"
+        );
+        return confirmed === true;
+    } catch (e) {
+        console.error("公告弹窗出错:", e);
+        window.shiguangBridge.showToast("显示公告出错: " + e.message);
+        return false;
+    }
+}
+
+// ---- 2. 获取学期列表 ----
+async function getSemesterOptions() {
+    try {
+        const response = await fetch(`/student/for-std/course-table`);
+        const htmlString = await response.text();
+        const parser = new DOMParser();
+        const dom = parser.parseFromString(htmlString, 'text/html');
+        const selectElement = dom.getElementById('semesters') || dom.getElementById('allSemesters');
+        if (!selectElement) throw new Error("页面中未找到学期选择框");
+
+        const options = Array.from(selectElement.options);
+        const validOptions = options.filter(opt => opt.value && opt.value !== "all");
+        if (validOptions.length === 0) throw new Error("未解析到有效的学期列表");
+
+        const semesterTexts = validOptions.map(opt => opt.text.trim());
+        const semesterValues = validOptions.map(opt => opt.value);
+        return { semesterTexts, semesterValues };
+    } catch (e) {
+        console.error("获取学期列表失败:", e);
+        window.shiguangBridge.showToast("获取学期列表失败: " + e.message);
+        return null;
+    }
+}
+
+// ---- 3. 获取 stdPersonId (从页面 JS 变量中提取) ----
+async function fetchStdPersonId() {
+    try {
+        const response = await fetch(`/student/for-std/course-table`);
+        const htmlString = await response.text();
+        // 常见模式: stdPersonId = 3284653 或 stdPersonId: 3284653 或 stdPersonId":"3284653"
+        const patterns = [
+            /stdPersonId\s*[:=]\s*["']?(\d+)["']?/,
+            /std_person_id\s*[:=]\s*["']?(\d+)["']?/,
+            /stdPersonId["']?\s*[:=]\s*["']?(\d+)["']?/
+        ];
+        for (const pattern of patterns) {
+            const match = htmlString.match(pattern);
+            if (match) return match[1];
+        }
+        return null;
+    } catch (e) {
+        console.error("提取 stdPersonId 失败:", e);
+        return null;
+    }
+}
+
+// ---- 4. 获取课程基础数据 (get-data) ----
+async function fetchGetData(semesterId) {
+    const url = `/student/for-std/course-table/get-data?bizTypeId=2&semesterId=${semesterId}`;
+    const response = await fetch(url);
+    if (!response.ok) throw new Error(`get-data 请求失败, 状态码: ${response.status}`);
+    return await response.json();
+}
+
+// ---- 5. 获取排课明细 (datum) ----
+async function fetchDatum(lessonIds, stdPersonId) {
+    const body = {
+        lessonIds: lessonIds,
+        studentId: null,
+        stdPersonId: stdPersonId,
+        weekIndex: null
+    };
+    const response = await fetch(`/student/ws/schedule-table/datum`, {
+        method: "POST",
+        headers: { "Content-Type": "application/json" },
+        body: JSON.stringify(body)
+    });
+    if (!response.ok) throw new Error(`datum 请求失败, 状态码: ${response.status}`);
+    return await response.json();
+}
+
+// ---- 6. 获取节次时间表 (timetable-layout) ----
+async function fetchTimeTableLayout(timeTableLayoutId) {
+    try {
+        const response = await fetch(`/student/ws/schedule-table/timetable-layout`, {
+            method: "POST",
+            headers: { "Content-Type": "application/json" },
+            body: JSON.stringify({ id: timeTableLayoutId })
+        });
+        if (!response.ok) throw new Error(`timetable-layout 请求失败, 状态码: ${response.status}`);
+        const json = await response.json();
+        if (!json || !json.result || !json.result.courseUnitList) throw new Error("timetable-layout 响应结构异常");
+        return json.result;
+    } catch (e) {
+        console.warn("获取节次时间表失败, 使用内置兜底数据:", e);
+        return { courseUnitList: null };
+    }
+}
+
+// ---- 7. 获取学期日期信息 (用于推算开学日期, 可选) ----
+async function fetchSemesterInfo(semesterId) {
+    try {
+        const response = await fetch(`/student/ws/semester/get/${semesterId}`);
+        if (!response.ok) return null;
+        return await response.json();
+    } catch (e) {
+        return null;
+    }
+}
+
+// ---- 主流程 ----
+async function runImportFlow() {
+    window.shiguangBridge.showToast("课程导入流程即将开始...");
+
+    // 1. 公告确认
+    const alertConfirmed = await promptUserToStart();
+    if (!alertConfirmed) {
+        return;
+    }
+
+    // 2. 选择学期
+    const semesterOptions = await getSemesterOptions();
+    if (!semesterOptions) return;
+    const selectedIndex = await window.shiguangBridgePromise.showSingleSelection(
+        "选择学期",
+        JSON.stringify(semesterOptions.semesterTexts),
+        0
+    );
+    if (selectedIndex === null || selectedIndex < 0) {
+        window.shiguangBridge.showToast("导入已取消。");
+        return;
+    }
+    const semesterId = semesterOptions.semesterValues[selectedIndex];
+    window.shiguangBridge.showToast(`已选择: ${semesterOptions.semesterTexts[selectedIndex]}`);
+
+    try {
+        // 3. 获取课程基础数据
+        window.shiguangBridge.showToast("正在获取课程数据...");
+        const getData = await fetchGetData(semesterId);
+        const lessonIds = getData.lessonIds || [];
+        const timeTableLayoutId = getData.timeTableLayoutId;
+
+        // 4. 获取 stdPersonId
+        let stdPersonId = await fetchStdPersonId();
+
+        // 5. 获取排课明细
+        window.shiguangBridge.showToast("正在获取排课明细...");
+        const datum = await fetchDatum(lessonIds, stdPersonId);
+        const result = datum.result;
+        if (!result || !result.lessonList || !result.scheduleList) {
+            throw new Error("datum 响应结构异常, 请检查是否已登录");
+        }
+
+        // 6. 获取节次时间表
+        const layout = await fetchTimeTableLayout(timeTableLayoutId);
+
+        // 7. 解析课程
+        const courses = parseCourses(result, layout);
+        if (courses.length === 0) {
+            window.shiguangBridge.showToast("未解析到课程数据。");
+            return;
+        }
+
+        // 8. 生成时间段
+        const timeSlots = buildTimeSlots(layout);
+
+        // 9. 生成课表配置
+        const config = {
+            semesterStartDate: null,
+            semesterTotalWeeks: 20,
+            defaultClassDuration: 45,
+            defaultBreakDuration: 5,
+            firstDayOfWeek: 1
+        };
+
+        // 优先尝试从学期接口获取开学日期与周数
+        const semesterInfo = await fetchSemesterInfo(semesterId);
+        if (semesterInfo && semesterInfo.startDate) {
+            config.semesterStartDate = semesterInfo.startDate;
+            if (semesterInfo.endDate) {
+                const start = new Date(semesterInfo.startDate);
+                const end = new Date(semesterInfo.endDate);
+                const diffDays = Math.ceil((end - start) / (1000 * 60 * 60 * 24));
+                const weeks = Math.ceil(diffDays / 7);
+                if (weeks > 0) config.semesterTotalWeeks = weeks;
+            }
+        } else {
+            // 回退: 从排课明细推算开学日期
+            const startDate = calcSemesterStartDate(result);
+            if (startDate) config.semesterStartDate = startDate;
+            // 回退: 从课程实际最大周次推算总周数(避免出现大量空白周)
+            if (courses.length > 0) {
+                let maxWeek = 0;
+                courses.forEach(c => c.weeks.forEach(w => { if (w > maxWeek) maxWeek = w; }));
+                if (maxWeek > 0) config.semesterTotalWeeks = maxWeek;
+            }
+        }
+
+        // 10. 依次保存
+        const timeSlotOk = await bridgeSaveTimeSlots(timeSlots);
+        if (!timeSlotOk) return;
+
+        const configOk = await bridgeSaveConfig(config);
+        if (!configOk) return;
+
+        const coursesOk = await bridgeSaveCourses(courses);
+        if (!coursesOk) return;
+
+        // 11. 完成
+        window.shiguangBridge.showToast("课程导入完成!");
+        window.shiguangBridge.notifyTaskCompletion();
+    } catch (e) {
+        console.error("导入流程发生错误:", e);
+        window.shiguangBridge.showToast("导入失败: " + e.message);
+    }
+}
+
+// 启动
+runImportFlow();