bbgu.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. // 北部湾大学智慧教务系统(zhjw.bbgu.edu.cn)拾光课表导入适配脚本
  2. // 新一代金智 resourceapi 接口:session/list 取学期、timePattern/get-large-period 取作息、
  3. // timetable/class/timetable/student/my-table-detail 取课表
  4. // 解析周次字符串:"1-4,6-9" → [1,2,3,4,6,7,8,9]
  5. function parseWeeks(weekStr) {
  6. const weeks = [];
  7. String(weekStr || "").split(",").forEach(part => {
  8. part = part.trim();
  9. const range = part.match(/^(\d+)-(\d+)$/);
  10. if (range) {
  11. const start = Number(range[1]);
  12. const end = Number(range[2]);
  13. for (let i = start; i <= end; i++) weeks.push(i);
  14. } else if (/^\d+$/.test(part)) {
  15. weeks.push(Number(part));
  16. }
  17. });
  18. return [...new Set(weeks)].sort((a, b) => a - b);
  19. }
  20. // 解析节次字符串:"10-11" → { startSection: 10, endSection: 11 };单值 "12" → 第12节
  21. function parsePeriods(periodStr) {
  22. const str = String(periodStr || "").trim();
  23. const m = str.match(/^(\d+)-(\d+)$/);
  24. if (m) {
  25. const start = Number(m[1]);
  26. const end = Number(m[2]);
  27. if (!start || !end || start > end) return null;
  28. return { startSection: start, endSection: end };
  29. }
  30. if (/^\d+$/.test(str)) {
  31. const n = Number(str);
  32. if (!n) return null;
  33. return { startSection: n, endSection: n };
  34. }
  35. return null;
  36. }
  37. // 清洗教师名:"周文红[主讲];" → "周文红";"王柏玲[主讲];谢娟[主讲];" → "王柏玲、谢娟"
  38. function cleanTeacher(name) {
  39. const cleaned = String(name || "")
  40. .replace(/\[[^\]]*\]/g, "")
  41. .replace(/[;;]+/g, "、")
  42. .replace(/、+$/, "")
  43. .trim();
  44. return cleaned || "未知";
  45. }
  46. function parseCourse(row) {
  47. const day = Number(row.weekDay);
  48. const periods = parsePeriods(row.periodFormat);
  49. const weeks = parseWeeks(row.teachingWeekFormat);
  50. if (!row.courseName || !day || day < 1 || day > 7 || !periods || weeks.length === 0) return null;
  51. const campus = String(row.campusName || "").trim();
  52. const room = String(row.roomName || "").trim();
  53. let position = room;
  54. if (campus && room) position = `${room}(${campus})`;
  55. if (!position) position = campus || "待定";
  56. return {
  57. name: String(row.courseName).trim(),
  58. teacher: cleanTeacher(row.instructorName),
  59. position,
  60. day,
  61. startSection: periods.startSection,
  62. endSection: periods.endSection,
  63. weeks
  64. };
  65. }
  66. function getErrorMessage(error) {
  67. if (error && typeof error.message === "string" && error.message.trim()) return error.message;
  68. if (typeof error === "string" && error.trim()) return error;
  69. try {
  70. const serialized = JSON.stringify(error);
  71. if (serialized && serialized !== "{}") return serialized;
  72. } catch (_) {
  73. // Ignore serialization failures and use the generic fallback below.
  74. }
  75. return "未知错误";
  76. }
  77. // 从页面存储中提取 JWT 鉴权 token(金智系统常见 key + 全量扫描兜底)
  78. function getAuthToken() {
  79. const keys = ["Authorization", "access_token", "token", "X-Access-Token", "x-token", "auth_token", "jwt", "user_token"];
  80. for (const k of keys) {
  81. try {
  82. const v = window.localStorage.getItem(k) || window.sessionStorage.getItem(k);
  83. if (v && v.includes("eyJ")) return v;
  84. } catch (_) {
  85. // Ignore storage access errors.
  86. }
  87. }
  88. // 全量扫描:找直接以 eyJ 开头(JWT)或 JSON 包装中含 eyJ 的字符串
  89. const stores = [window.localStorage, window.sessionStorage];
  90. for (const store of stores) {
  91. try {
  92. for (let i = 0; i < store.length; i++) {
  93. const raw = store.getItem(store.key(i));
  94. if (!raw) continue;
  95. if (raw.startsWith("eyJ")) return raw;
  96. const m = raw.match(/"([^"]*eyJ[^"]*)"/);
  97. if (m) return m[1];
  98. }
  99. } catch (_) {
  100. // Ignore storage access errors.
  101. }
  102. }
  103. return null;
  104. }
  105. async function requestJson(url, options) {
  106. const token = getAuthToken();
  107. const headers = { "X-Requested-With": "XMLHttpRequest" };
  108. if (token) headers["Authorization"] = token.startsWith("Bearer ") ? token : `Bearer ${token}`;
  109. const response = await fetch(url, Object.assign({
  110. credentials: "include",
  111. headers
  112. }, options));
  113. if (!response.ok) throw new Error(`接口请求失败(HTTP ${response.status})`);
  114. return response.json();
  115. }
  116. // 获取学期列表,返回 { id, year, term, beginDate, endDate, active }
  117. async function fetchSemesters() {
  118. const payload = await requestJson("https://zhjw.bbgu.edu.cn/api/resourceapi/session/list");
  119. const list = payload && Array.isArray(payload.sessionVOList) ? payload.sessionVOList : [];
  120. return list.filter(s => s && s.id && s.year && s.term);
  121. }
  122. // 获取作息时间(第1-N节)
  123. async function fetchTimeSlots(sessionId) {
  124. const payload = await requestJson(
  125. `https://zhjw.bbgu.edu.cn/api/resourceapi/timePattern/get-large-period?sessionId=${encodeURIComponent(sessionId)}`
  126. );
  127. const patterns = payload && Array.isArray(payload.data) ? payload.data : [];
  128. const slots = [];
  129. for (const group of patterns) {
  130. const list = Array.isArray(group.periodList) ? group.periodList : [];
  131. for (const p of list) {
  132. const number = Number(p.smallPeriod);
  133. if (!number || !p.startTime || !p.endTime) continue;
  134. slots.push({ number, startTime: p.startTime, endTime: p.endTime });
  135. }
  136. }
  137. return slots.sort((a, b) => a.number - b.number);
  138. }
  139. // 获取课表并解析
  140. async function fetchCourses(sessionId) {
  141. window.shiguangBridge.showToast("正在请求课表数据...");
  142. const payload = await requestJson(
  143. `https://zhjw.bbgu.edu.cn/api/timetable/class/timetable/student/my-table-detail?sessionId=${encodeURIComponent(sessionId)}`,
  144. { method: "POST", credentials: "include" }
  145. );
  146. const rows = payload && Array.isArray(payload.classTimetableVOList)
  147. ? payload.classTimetableVOList
  148. : (payload && payload.data && Array.isArray(payload.data.classTimetableVOList) ? payload.data.classTimetableVOList : []);
  149. if (rows.length === 0) throw new Error("教务系统返回数据格式异常。");
  150. const rawCourses = rows.map(parseCourse).filter(Boolean);
  151. // 同一门课多处时段会分多条,按组合键去重
  152. const seen = new Set();
  153. const courses = [];
  154. for (const c of rawCourses) {
  155. const key = `${c.name}|${c.day}|${c.startSection}|${c.endSection}|${c.weeks.join(',')}|${c.teacher}|${c.position}`;
  156. if (seen.has(key)) continue;
  157. seen.add(key);
  158. courses.push(c);
  159. }
  160. if (courses.length === 0) throw new Error("未找到包含有效时间的课程。");
  161. courses.sort((a, b) => a.day - b.day || a.startSection - b.startSection || a.name.localeCompare(b.name));
  162. return courses;
  163. }
  164. // 判断是否处于登录页
  165. function isLoginPage() {
  166. const url = window.location.href;
  167. return url.includes("authserver") && url.includes("login");
  168. }
  169. // 选择学期,默认定位到当前激活学期
  170. async function selectSemester() {
  171. const list = await fetchSemesters();
  172. if (list.length === 0) throw new Error("未获取到学期列表。");
  173. const defaultIndex = Math.max(0, list.findIndex(s => s.active === "Y"));
  174. const names = list.map(s => `${s.year}${s.term === "春" ? "春季" : "秋季"}学期`);
  175. const selectedIndex = await window.shiguangBridgePromise.showSingleSelection(
  176. "请选择学期",
  177. JSON.stringify(names),
  178. defaultIndex
  179. );
  180. if (selectedIndex === null || selectedIndex === undefined) return null;
  181. return list[selectedIndex];
  182. }
  183. // 保存预设作息时间(失败仅告警,不阻断课程导入)
  184. async function saveTimeSlots(timeSlots) {
  185. if (!timeSlots || timeSlots.length === 0) return;
  186. try {
  187. await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
  188. console.log("JS: 作息时间保存成功");
  189. } catch (error) {
  190. console.error("JS: 作息时间保存失败:", error);
  191. }
  192. }
  193. // 获取当前学期最大周次(失败返回 null)
  194. async function fetchMaxWeek(sessionId) {
  195. try {
  196. const payload = await requestJson(
  197. `https://zhjw.bbgu.edu.cn/api/timetable/course/maxWeek/${encodeURIComponent(sessionId)}`
  198. );
  199. const weeks = Number(payload && payload.data);
  200. return weeks > 0 ? weeks : null;
  201. } catch (error) {
  202. console.error("JS: 获取最大周次失败:", error);
  203. return null;
  204. }
  205. }
  206. // 保存课表配置(开学日期、总周数;失败仅告警,不阻断课程导入)
  207. async function saveCourseConfig(semester, courses) {
  208. try {
  209. const dateDiffDays = (new Date(semester.endDate) - new Date(semester.beginDate)) / 86400000;
  210. const weeksByDate = Math.round(dateDiffDays / 7);
  211. const maxCourseWeek = Math.max(0, ...courses.map(c => Math.max(...c.weeks)));
  212. const maxWeek = await fetchMaxWeek(semester.id);
  213. const config = {
  214. semesterStartDate: semester.beginDate,
  215. semesterTotalWeeks: maxWeek || Math.max(maxCourseWeek, weeksByDate || 20)
  216. };
  217. await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(config));
  218. console.log(`JS: 课表配置保存成功(开学 ${config.semesterStartDate},共 ${config.semesterTotalWeeks} 周)`);
  219. } catch (error) {
  220. console.error("JS: 课表配置保存失败:", error);
  221. }
  222. }
  223. async function runImportFlow() {
  224. if (isLoginPage()) {
  225. window.shiguangBridge.showToast("导入失败:请先登录教务系统!");
  226. return;
  227. }
  228. const alertConfirmed = await window.shiguangBridgePromise.showAlert(
  229. "教务系统课表导入",
  230. "导入前请确保您已登录教务系统,建议在课表页面进行导入",
  231. "好的,开始导入"
  232. );
  233. if (!alertConfirmed) {
  234. window.shiguangBridge.showToast("用户取消了导入。");
  235. return;
  236. }
  237. window.shiguangBridge.showToast("正在获取学期信息...");
  238. try {
  239. const semester = await selectSemester();
  240. if (!semester) {
  241. window.shiguangBridge.showToast("用户取消了导入。");
  242. return;
  243. }
  244. console.log(`JS: 选择学期: ${semester.year}${semester.term} (${semester.id})`);
  245. const courses = await fetchCourses(semester.id);
  246. window.shiguangBridge.showToast(`正在保存 ${courses.length} 门课程...`);
  247. await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses, null, 2));
  248. await saveCourseConfig(semester, courses);
  249. const timeSlots = await fetchTimeSlots(semester.id);
  250. await saveTimeSlots(timeSlots);
  251. window.shiguangBridge.showToast(`课程导入成功,共导入 ${courses.length} 门课程!`);
  252. window.shiguangBridge.notifyTaskCompletion();
  253. } catch (error) {
  254. window.shiguangBridge.showToast(`导入失败:${getErrorMessage(error)}`);
  255. console.error("JS: Import Error:", error);
  256. }
  257. }
  258. runImportFlow();