Переглянути джерело

Merge pull request #676 from XingHeYuZhuan/pending

星河欲转 1 тиждень тому
батько
коміт
4adaa6b9be
3 змінених файлів з 257 додано та 0 видалено
  1. 5 0
      index/root_index.yaml
  2. 8 0
      resources/XAUAT/adapters.yaml
  3. 244 0
      resources/XAUAT/xauat.js

+ 5 - 0
index/root_index.yaml

@@ -1022,3 +1022,8 @@ schools:
     name: "广州华立学院"
     initial: "G"
     resource_folder: "HUALIXY"
+
+  - id: "XAUAT"
+    name: "西安建筑科技大学"
+    initial: "X"
+    resource_folder: "XAUAT"

+ 8 - 0
resources/XAUAT/adapters.yaml

@@ -0,0 +1,8 @@
+adapters:
+  - adapter_id: "XAUAT_01"
+    adapter_name: "西安建筑科技大学"
+    category: "BACHELOR_AND_ASSOCIATE"
+    asset_js_path: "xauat.js"
+    import_url: "https://swjw.xauat.edu.cn/student/login/"
+    maintainer: "Alph4rc"
+    description: "西安建筑科技大学教务系统适配,请先登录统一身份认证,手动点击进入教务系统后执行脚本自动获取课程信息"

+ 244 - 0
resources/XAUAT/xauat.js

@@ -0,0 +1,244 @@
+// 西安建筑科技大学(XAUAT)拾光课程表适配脚本 · 树维教务平台 · 本科
+"use strict";
+(function () {
+  // bizTypeId=2:本科课表(接口约定,勿改)
+  const BIZ_TYPE_ID = 2;
+  // datum 接口要求 studentId 传字符串 "null"(接口约定,勿改)
+  const DATUM_STUDENT_ID = "null";
+
+  const LIMITS = { timeout: 15000, minWeeks: 16, maxWeeks: 25, fallbackWeeks: 20 };
+  const MS_PER_DAY = 86400000;
+  const DAYS_PER_WEEK = 7;
+  const ENABLE_LOG = false; // release:关闭普通日志,仅保留 error
+
+  const API = {
+    courseTablePage: "/student/for-std/course-table",
+    lessonIds: (s) => "/student/for-std/course-table/get-data?bizTypeId=" + BIZ_TYPE_ID + "&semesterId=" + encodeURIComponent(s) + "&dataId=",
+    datum: "/student/ws/schedule-table/datum",
+    semester: (s) => "/student/ws/semester/get/" + encodeURIComponent(s),
+  };
+
+  const log = {
+    info: (m) => ENABLE_LOG && console.log("[XAUAT] " + m),
+    warn: (m) => ENABLE_LOG && console.warn("[XAUAT] " + m),
+    error: (m) => console.error("[XAUAT] " + m),
+  };
+
+  // 桥接调用统一封装,其余代码不直接访问 window.shiguangBridge
+  const ui = {
+    toast: (m) => window.shiguangBridge.showToast(m),
+    alert: (t, m, b) => window.shiguangBridgePromise.showAlert(t, m, b),
+    select: (t, items, d) => window.shiguangBridgePromise.showSingleSelection(t, JSON.stringify(items), d),
+    saveConfig: (j) => window.shiguangBridgePromise.saveCourseConfig(j),
+    saveCourses: (j) => window.shiguangBridgePromise.saveImportedCourses(j),
+    done: () => window.shiguangBridge.notifyTaskCompletion(),
+  };
+
+  async function requestText(url, options) {
+    const controller = typeof AbortController !== "undefined" ? new AbortController() : null;
+    const timer = controller ? setTimeout(() => controller.abort(), LIMITS.timeout) : null;
+    try {
+      const res = await fetch(url, Object.assign(
+        { credentials: "include" },
+        options,
+        controller ? { signal: controller.signal } : {}
+      ));
+      if (!res.ok) {
+        if (res.status === 401 || res.status === 403) throw new Error("登录状态已失效,请重新登录教务系统后重试");
+        throw new Error("网络请求失败,请稍后重试");
+      }
+      return await res.text();
+    } catch (e) {
+      if (e.name === "AbortError") throw new Error("请求超时,请检查网络后重试");
+      throw e;
+    } finally {
+      if (timer) clearTimeout(timer);
+    }
+  }
+
+  const requestJson = async (url, options) => JSON.parse(await requestText(url, options));
+
+  const pad2 = (n) => String(n).padStart(2, "0");
+  const formatTime = (hhmm) => pad2(Math.floor(hhmm / 100)) + ":" + pad2(hhmm % 100);
+  const toDateString = (d) => d.getFullYear() + "-" + pad2(d.getMonth() + 1) + "-" + pad2(d.getDate());
+  const weekdayOf = (dateStr) => { const d = new Date(dateStr).getDay(); return d === 0 ? 7 : d; };
+  const clampWeeks = (w) => Math.min(Math.max(w, LIMITS.minWeeks), LIMITS.maxWeeks);
+
+  // 周次去重升序:值域有界,用桶排序
+  function uniqueSortedWeeks(weeks) {
+    const seen = new Array(LIMITS.maxWeeks + 1).fill(false);
+    for (const w of weeks) if (w >= 1 && w <= LIMITS.maxWeeks) seen[w] = true;
+    const out = [];
+    for (let w = 1; w <= LIMITS.maxWeeks; w++) if (seen[w]) out.push(w);
+    return out;
+  }
+
+  function calculateTotalWeeks(start, end) {
+    if (!start || !end) return LIMITS.fallbackWeeks;
+    return Math.ceil(Math.ceil((new Date(end) - new Date(start)) / MS_PER_DAY) / DAYS_PER_WEEK);
+  }
+
+  // 由最早上课日期反推开学日(所在周周一)。局限:假设最早上课日在第一周。
+  function deriveSemesterStartDate(scheduleList) {
+    let minDate = null;
+    for (const it of scheduleList) if (it.date && (minDate === null || it.date < minDate)) minDate = it.date;
+    if (!minDate) return null;
+    const d = new Date(minDate);
+    d.setDate(d.getDate() - ((d.getDay() + 6) % 7));
+    return toDateString(d);
+  }
+
+  function getMaxCourseWeek(courses) {
+    let max = 0;
+    for (const c of courses) for (const w of c.weeks || []) if (w > max) max = w;
+    return max;
+  }
+
+  function readPosition(room) {
+    if (!room) return "未知地点";
+    return (typeof room === "object" ? room.nameZh : room) || "未知地点";
+  }
+
+  function convertCourses(lessonList, scheduleList, semesterStartDate) {
+    const names = new Map(lessonList.map((l) => [l.id, l.courseName]));
+    const startMs = new Date(semesterStartDate).getTime(); // 预计算,避免循环内重复解析
+    const groups = new Map();
+    for (const it of scheduleList) {
+      if (!it.date || typeof it.startTime !== "number" || typeof it.endTime !== "number") continue;
+      const diffDays = Math.floor((new Date(it.date).getTime() - startMs) / MS_PER_DAY);
+      if (diffDays < 0) continue;
+      const week = Math.floor(diffDays / DAYS_PER_WEEK) + 1;
+      const day = weekdayOf(it.date);
+      const key = it.lessonId + "|" + day + "|" + it.startTime + "|" + it.endTime;
+      if (!groups.has(key)) {
+        groups.set(key, {
+          name: names.get(it.lessonId) || "未知课程",
+          teacher: it.personName || "未知教师",
+          position: readPosition(it.room),
+          day, startTime: it.startTime, endTime: it.endTime, weeks: [],
+        });
+      }
+      groups.get(key).weeks.push(week);
+    }
+    const courses = [];
+    for (const g of groups.values()) {
+      courses.push({
+        name: g.name, teacher: g.teacher, position: g.position, day: g.day,
+        weeks: uniqueSortedWeeks(g.weeks), isCustomTime: true,
+        customStartTime: formatTime(g.startTime), customEndTime: formatTime(g.endTime),
+      });
+    }
+    log.info("转换完成,共 " + courses.length + " 条课程");
+    return courses;
+  }
+
+  async function getSemesters() {
+    log.info("获取学期列表...");
+    const select = new DOMParser().parseFromString(await requestText(API.courseTablePage), "text/html")
+      .querySelector("#allSemesters, #semesters");
+    if (!select) throw new Error("未找到学期选择框,请确认已登录教务系统");
+    const semesters = [];
+    for (const opt of select.querySelectorAll("option")) {
+      const id = opt.getAttribute("value");
+      const name = opt.textContent.trim();
+      if (id && id !== "all" && name) semesters.push({ id, name });
+    }
+    if (!semesters.length) throw new Error("学期列表为空");
+    return semesters;
+  }
+
+  async function fetchLessonIds(semesterId) {
+    const json = await requestJson(API.lessonIds(semesterId));
+    if (!json || !Array.isArray(json.lessonIds)) throw new Error("课程数据异常,请稍后重试");
+    return json.lessonIds;
+  }
+
+  async function fetchScheduleDatum(lessonIds) {
+    if (!lessonIds || !lessonIds.length) throw new Error("课程列表为空");
+    const json = await requestJson(API.datum, {
+      method: "POST",
+      headers: { "Content-Type": "application/json" },
+      body: JSON.stringify({ studentId: DATUM_STUDENT_ID, lessonIds }),
+    });
+    if (!json || !json.result) throw new Error("课程详情数据异常,请稍后重试");
+    return { lessonList: json.result.lessonList || [], scheduleList: json.result.scheduleList || [] };
+  }
+
+  // 学期日期仅作辅助,失败降级为空,不中断主流程
+  async function fetchSemesterInfo(semesterId) {
+    try {
+      const json = await requestJson(API.semester(semesterId));
+      return { startDate: json.startDate || null, endDate: json.endDate || null };
+    } catch (e) {
+      log.warn("学期信息获取失败: " + e.message);
+      return { startDate: null, endDate: null };
+    }
+  }
+
+  async function saveConfig(config) {
+    try { await ui.saveConfig(JSON.stringify(config)); }
+    catch (e) { throw new Error("保存配置失败,请重试"); }
+    ui.toast("课表配置导入成功");
+  }
+
+  async function saveCourses(courses) {
+    try { await ui.saveCourses(JSON.stringify(courses)); }
+    catch (e) { throw new Error("保存课程失败,请重试"); }
+    ui.toast("成功导入 " + courses.length + " 条课程");
+  }
+
+  async function chooseSemester(semesters) {
+    const names = semesters.map((s) => s.name);
+    for (;;) {
+      const index = await ui.select("选择学期", names, 0);
+      if (index === null || index < 0 || index >= semesters.length) return null;
+      const semester = semesters[index];
+      let lessonIds = [];
+      try { lessonIds = await fetchLessonIds(semester.id); }
+      catch (e) { log.warn("获取 " + semester.name + " 课程失败: " + e.message); }
+      if (lessonIds.length) return { semester, lessonIds };
+      if (!(await ui.alert("无课程数据", "「" + semester.name + "」没有课程数据,请选择其他学期。", "重新选择"))) return null;
+    }
+  }
+
+  async function runImportFlow() {
+    try {
+      if (!(await ui.alert("西安建筑科技大学课表导入", "请确保已登录教务系统(swjw.xauat.edu.cn)。\n本适配将自动获取学期与课程数据。", "开始导入"))) {
+        ui.toast("导入已取消"); return;
+      }
+      ui.toast("正在获取学期列表...");
+      const chosen = await chooseSemester(await getSemesters());
+      if (!chosen) { ui.toast("导入已取消"); return; }
+
+      // 课程详情与学期信息无依赖,并行请求
+      ui.toast("正在获取课程数据...");
+      const [datum, semesterInfo] = await Promise.all([
+        fetchScheduleDatum(chosen.lessonIds),
+        fetchSemesterInfo(chosen.semester.id),
+      ]);
+
+      const startDate = deriveSemesterStartDate(datum.scheduleList) || semesterInfo.startDate;
+      if (!startDate) throw new Error("无法确定开学日期,请重试");
+      log.info("推算开学日期: " + startDate);
+
+      const courses = convertCourses(datum.lessonList, datum.scheduleList, startDate);
+      if (!courses.length) {
+        await ui.alert("无课程数据", "未能转换出有效课程。", "确定");
+        return;
+      }
+
+      // 优先用课程实际最大周次,回退到日期差计算
+      const totalWeeks = clampWeeks(getMaxCourseWeek(courses) || calculateTotalWeeks(startDate, semesterInfo.endDate));
+
+      await saveConfig({ semesterStartDate: startDate, semesterTotalWeeks: totalWeeks });
+      await saveCourses(courses);
+      ui.toast("课表导入完成!");
+      ui.done();
+    } catch (e) {
+      log.error("导入异常: " + (e.stack || e.message));
+      await ui.alert("导入失败", e.message || "未知错误,请重试", "确定");
+    }
+  }
+
+  runImportFlow();
+})();