aepu.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. // 安徽电气工程职业技术学院教务系统(zt.aepu.com.cn)拾光课表导入适配脚本
  2. // 金智老版通用教务(jwapp/sys/wdkb 模块):
  3. // cxxszhxqkb.do 综合课表(主) + xsdkkc.do 已选课(补充)
  4. // 周次用 SKZC 位图("000000000000000000100" 第19位为1表示第19周)
  5. // 通过 webvpn 访问,接口前缀取自当前页面 URL。
  6. // 预设作息时间(该校实际 10 节)
  7. const AEPU_TIME_SLOTS = [
  8. { number: 1, startTime: "08:00", endTime: "08:45" },
  9. { number: 2, startTime: "08:50", endTime: "09:35" },
  10. { number: 3, startTime: "09:55", endTime: "10:40" },
  11. { number: 4, startTime: "10:45", endTime: "11:30" },
  12. { number: 5, startTime: "14:00", endTime: "14:45" },
  13. { number: 6, startTime: "14:50", endTime: "15:35" },
  14. { number: 7, startTime: "15:45", endTime: "16:30" },
  15. { number: 8, startTime: "16:35", endTime: "17:20" },
  16. { number: 9, startTime: "19:00", endTime: "19:45" },
  17. { number: 10, startTime: "19:55", endTime: "20:40" }
  18. ];
  19. function getErrorMessage(error) {
  20. if (error && typeof error.message === "string" && error.message.trim()) return error.message;
  21. if (typeof error === "string" && error.trim()) return error;
  22. try {
  23. const serialized = JSON.stringify(error);
  24. if (serialized && serialized !== "{}") return serialized;
  25. } catch (_) {
  26. // Ignore serialization failures and use the generic fallback below.
  27. }
  28. return "未知错误";
  29. }
  30. // 从当前页面 URL 提取 jwapp 之前的前缀(含 webvpn 哈希),用于拼接各接口地址
  31. function getApiBase() {
  32. const m = window.location.href.match(/^(https?:\/\/[^/]+(?:[^?#]*)?)\/jwapp/);
  33. return m ? m[1] : window.location.origin;
  34. }
  35. const API_BASE = getApiBase();
  36. const MODULES = {
  37. table: `${API_BASE}/jwapp/sys/wdkb/modules/xskcb/cxxszhxqkb.do?enlink-vpn`,
  38. selected: `${API_BASE}/jwapp/sys/wdkb/modules/xskcb/xsdkkc.do?enlink-vpn`,
  39. semester: `${API_BASE}/jwapp/sys/wdkb/modules/jshkcb/cxjcs.do?enlink-vpn`
  40. };
  41. // 从页面存储中提取 JWT 鉴权 token(金智常见 key + 全量扫描兜底)
  42. function getAuthToken() {
  43. const keys = ["Authorization", "access_token", "token", "X-Access-Token", "x-token", "auth_token", "jwt", "user_token"];
  44. for (const k of keys) {
  45. try {
  46. const v = window.localStorage.getItem(k) || window.sessionStorage.getItem(k);
  47. if (v && v.includes("eyJ")) return v;
  48. } catch (_) {
  49. // Ignore storage access errors.
  50. }
  51. }
  52. const stores = [window.localStorage, window.sessionStorage];
  53. for (const store of stores) {
  54. try {
  55. for (let i = 0; i < store.length; i++) {
  56. const raw = store.getItem(store.key(i));
  57. if (!raw) continue;
  58. if (raw.startsWith("eyJ")) return raw;
  59. const m = raw.match(/"([^"]*eyJ[^"]*)"/);
  60. if (m) return m[1];
  61. }
  62. } catch (_) {
  63. // Ignore storage access errors.
  64. }
  65. }
  66. return null;
  67. }
  68. async function postForm(url, params) {
  69. const token = getAuthToken();
  70. const headers = { "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8" };
  71. if (token) headers["Authorization"] = token.startsWith("Bearer ") ? token : `Bearer ${token}`;
  72. const response = await fetch(url, {
  73. method: "POST",
  74. credentials: "include",
  75. headers,
  76. body: new URLSearchParams(params).toString()
  77. });
  78. if (!response.ok) throw new Error(`接口请求失败(HTTP ${response.status})`);
  79. return response.json();
  80. }
  81. // 解析 SKZC 位图:"000000000000000000100" → [19](第 i 位为 '1' 表示第 i+1 周)
  82. function parseWeekBitmap(bitmap) {
  83. const weeks = [];
  84. const s = String(bitmap || "");
  85. for (let i = 0; i < s.length; i++) {
  86. if (s.charAt(i) === "1") weeks.push(i + 1);
  87. }
  88. return [...new Set(weeks)].sort((a, b) => a - b);
  89. }
  90. // 清洗教师名:"周锐\/2009021059" → "周锐";"王磊,易庆" → "王磊、易庆"
  91. function cleanTeacher(name) {
  92. const cleaned = String(name || "")
  93. .replace(/\/\d+$/i, "")
  94. .split(/[,,]/)
  95. .map(s => s.trim())
  96. .filter(Boolean)
  97. .join("、");
  98. return cleaned || "未知";
  99. }
  100. // 生成去重键(忽略位置,同一门课的多次调课合并到周次集合)
  101. function courseKey(c, detailKey) {
  102. return `${detailKey}|1`;
  103. }
  104. // 从单个课表行构造课程(合并同名、同星期、同节次、同教师下的周次与教室)
  105. // each() 收集原始行,随后按 (name|teacher|day|start|end) 合并周次
  106. function feedRow(map, row) {
  107. const name = String(row.KCM || "").trim();
  108. if (!name) return;
  109. const teacher = cleanTeacher(row.SKJS);
  110. const day = Number(row.SKXQ);
  111. const startSection = Number(row.KSJC);
  112. const endSection = Number(row.JSJC);
  113. if (!day || day < 1 || day > 7) return;
  114. if (!startSection || !endSection || startSection > endSection) return;
  115. const weeks = parseWeekBitmap(row.SKZC);
  116. if (weeks.length === 0) return;
  117. const position = String(row.JASMC || row.JXLDM_DISPLAY || "待定").trim();
  118. // 去重键含位置,避免同一门课不同教室(不同周次)被错误合并
  119. const key = `${name}|${teacher}|${day}|${startSection}|${endSection}|${position}`;
  120. const holder = map.get(key);
  121. if (holder) {
  122. holder.weeks = Array.from(new Set([...holder.weeks, ...weeks])).sort((a, b) => a - b);
  123. if (position && position !== "待定" && !holder.position.includes(position)) {
  124. holder.position = position;
  125. }
  126. } else {
  127. map.set(key, {
  128. name,
  129. teacher,
  130. position: position || "待定",
  131. day,
  132. startSection,
  133. endSection,
  134. weeks
  135. });
  136. }
  137. }
  138. // 处理调课记录(xsdkkc):按新时间生效——
  139. // 从主课表移除"原时间"周次,再把"新时间"合并进对应条目(无则新建)
  140. // 记录字段:原时间 SKZC/SKXQ/KSJC/JSJC(教师 SKJS 为 null),新时间 XSKZC/XSKXQ/XKSJC/XJSJC(教师 XSKJS,教室 XJASMC)
  141. function feedAdjustment(map, row) {
  142. const name = String(row.KCM || "").trim();
  143. if (!name) return;
  144. const teacher = cleanTeacher(row.XSKJS || row.SKJS);
  145. const origDay = Number(row.SKXQ);
  146. const origStart = Number(row.KSJC);
  147. const origEnd = Number(row.JSJC);
  148. const origWeeks = parseWeekBitmap(row.SKZC);
  149. // 1) 从主课表移除原时间周次
  150. if (origDay >= 1 && origDay <= 7 && origStart && origEnd && origWeeks.length > 0) {
  151. const origPos = String(row.JASMC || "").trim();
  152. const removed = new Set(origWeeks);
  153. // 若存在"课名+星期+节次+原教室"精确条目则只删它,否则退化为删同课同时段所有条目
  154. let precise = false;
  155. for (const c of map.values()) {
  156. if (c.name === name && c.day === origDay && c.startSection === origStart &&
  157. c.endSection === origEnd && origPos && c.position === origPos) {
  158. precise = true;
  159. break;
  160. }
  161. }
  162. for (const c of map.values()) {
  163. if (c.name !== name || c.day !== origDay || c.startSection !== origStart || c.endSection !== origEnd) continue;
  164. if (!precise || (origPos && c.position === origPos)) {
  165. c.weeks = c.weeks.filter(w => !removed.has(w));
  166. }
  167. }
  168. }
  169. // 2) 合并新时间到课表
  170. const newDay = Number(row.XSKXQ);
  171. const newStart = Number(row.XKSJC);
  172. const newEnd = Number(row.XJSJC);
  173. const newWeeks = parseWeekBitmap(row.XSKZC);
  174. if (!(newDay >= 1 && newDay <= 7) || !newStart || !newEnd || newWeeks.length === 0) return;
  175. const newPosition = String(row.XJASMC || row.JASMC || "待定").trim();
  176. const key = `${name}|${teacher}|${newDay}|${newStart}|${newEnd}|${newPosition}`;
  177. const holder = map.get(key);
  178. if (holder) {
  179. holder.weeks = Array.from(new Set([...holder.weeks, ...newWeeks])).sort((a, b) => a - b);
  180. } else {
  181. map.set(key, {
  182. name,
  183. teacher: teacher || "未知",
  184. position: newPosition || "待定",
  185. day: newDay,
  186. startSection: newStart,
  187. endSection: newEnd,
  188. weeks: newWeeks
  189. });
  190. }
  191. }
  192. // 解析金智标准响应结构 datas.<key>.rows
  193. function resolveRows(payload, key) {
  194. const ds = payload && payload.datas && payload.datas[key];
  195. return ds && Array.isArray(ds.rows) ? ds.rows : [];
  196. }
  197. // 从学期行提取开学配置(开学日期、总周数)
  198. function semesterConfigOf(s) {
  199. const beginDate = String((s && s.XQKSRQ) || "").split(" ")[0];
  200. const totalWeeks = Number(s && s.ZZC) || Number(s && s.ZJXZC) || 0;
  201. return { beginDate, totalWeeks };
  202. }
  203. async function fetchCourses(xnxqdm) {
  204. // 理论课:cxxszhxqkb.do 不带 KCJBDM;实训课:带 KCJBDM=2;调课:xsdkkc.do
  205. const merged = new Map();
  206. window.shiguangBridge.showToast("正在请求课表数据...");
  207. const [theoryPayload, practicalPayload, adjustPayload] = await Promise.all([
  208. postForm(MODULES.table, { XNXQDM: xnxqdm }),
  209. postForm(MODULES.table, { XNXQDM: xnxqdm, KCJBDM: "2" }),
  210. postForm(MODULES.selected, { XNXQDM: xnxqdm, "*order": "-SQSJ" })
  211. ]);
  212. resolveRows(theoryPayload, "cxxszhxqkb").forEach(r => feedRow(merged, r));
  213. resolveRows(practicalPayload, "cxxszhxqkb").forEach(r => feedRow(merged, r));
  214. // 调课记录最后处理:按新时间生效,覆盖/合并主课表
  215. resolveRows(adjustPayload, "xsdkkc").forEach(r => feedAdjustment(merged, r));
  216. const courses = Array.from(merged.values());
  217. courses.sort((a, b) => a.day - b.day || a.startSection - b.startSection || a.name.localeCompare(b.name));
  218. return courses;
  219. }
  220. async function saveCourseConfig(config) {
  221. try {
  222. await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(config));
  223. console.log(`JS: 课表配置保存成功(开学 ${config.semesterStartDate},共 ${config.semesterTotalWeeks} 周)`);
  224. } catch (error) {
  225. console.error("JS: 课表配置保存失败:", error);
  226. }
  227. }
  228. // 保存预设作息时间(失败仅告警,不阻断课程导入)
  229. async function saveTimeSlots(timeSlots) {
  230. if (!timeSlots || timeSlots.length === 0) return;
  231. try {
  232. await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
  233. console.log("JS: 作息时间保存成功");
  234. } catch (error) {
  235. console.error("JS: 作息时间保存失败:", error);
  236. }
  237. }
  238. async function runImportFlow() {
  239. const alertConfirmed = await window.shiguangBridgePromise.showAlert(
  240. "教务系统课表导入",
  241. "导入前请确保您已登录教务系统,建议在课表页面进行导入",
  242. "好的,开始导入"
  243. );
  244. if (!alertConfirmed) {
  245. window.shiguangBridge.showToast("用户取消了导入。");
  246. return;
  247. }
  248. try {
  249. let xnxqdm = "";
  250. window.shiguangBridge.showToast("正在获取学期信息...");
  251. const sessions = await postForm(MODULES.semester, {});
  252. // 学期列表按开学日期降序(最新学期排最前),弹出选择器默认选中第一个
  253. const rows = (resolveRows(sessions, "cxjcs") || [])
  254. .filter(s => s && s.XN && s.XQ !== undefined && s.XQ !== null)
  255. .sort((a, b) => {
  256. const da = new Date(String(a.XQKSRQ || "").split(" ")[0]).getTime();
  257. const db = new Date(String(b.XQKSRQ || "").split(" ")[0]).getTime();
  258. return (db || 0) - (da || 0);
  259. });
  260. if (rows.length === 0) throw new Error("未能获取到学期列表。");
  261. const names = rows.map(s => `${s.XN}年第${s.XQ}学期`);
  262. const selectedIndex = await window.shiguangBridgePromise.showSingleSelection(
  263. "请选择学期",
  264. JSON.stringify(names),
  265. 0
  266. );
  267. if (selectedIndex === null || selectedIndex === undefined) {
  268. window.shiguangBridge.showToast("用户取消了导入。");
  269. return;
  270. }
  271. xnxqdm = `${rows[selectedIndex].XN}-${rows[selectedIndex].XQ}`;
  272. console.log(`JS: 使用学期 ${xnxqdm}`);
  273. const courses = await fetchCourses(xnxqdm);
  274. if (courses.length === 0) throw new Error("未获取到有效课程数据。");
  275. window.shiguangBridge.showToast(`正在保存 ${courses.length} 门课程...`);
  276. await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses, null, 2));
  277. // 开学日期/总周数直接取自用户选择的学期行(XQKSRQ / ZZC)
  278. const { beginDate, totalWeeks } = semesterConfigOf(rows[selectedIndex]);
  279. if (beginDate && totalWeeks) {
  280. await saveCourseConfig({ semesterStartDate: beginDate, semesterTotalWeeks: totalWeeks });
  281. }
  282. await saveTimeSlots(AEPU_TIME_SLOTS);
  283. window.shiguangBridge.showToast(`课程导入成功,共导入 ${courses.length} 门课程!`);
  284. window.shiguangBridge.notifyTaskCompletion();
  285. } catch (error) {
  286. window.shiguangBridge.showToast(`导入失败:${getErrorMessage(error)}`);
  287. console.error("JS: Import Error:", error);
  288. }
  289. }
  290. runImportFlow();