|
@@ -0,0 +1,442 @@
|
|
|
|
|
+// 福建信息职业技术学院(福信智慧教务)课表导入适配脚本
|
|
|
|
|
+// 前端 jw.fjpit.com · 接口 jw-api.fjpit.com/api · 鉴权头 ba-token + server: 1
|
|
|
|
|
+// 取数直接请求接口,不解析页面 HTML;教师/教室按原文原样保留(空即空)
|
|
|
|
|
+
|
|
|
|
|
+const FJPIT_API = 'https://jw-api.fjpit.com/api';
|
|
|
|
|
+const FJPIT_HOST_KEY = 'fjpit.com';
|
|
|
|
|
+const FJPIT_LOGIN_WAIT_MS = 5 * 60 * 1000;
|
|
|
|
|
+const FJPIT_BREAK_RE = /中午|午休|午间|晚休|休息/; // 作息表里的非上课行,需剔除
|
|
|
|
|
+
|
|
|
|
|
+// ---------- 通用工具 ----------
|
|
|
|
|
+
|
|
|
|
|
+function fjpitDelay(ms) {
|
|
|
|
|
+ return new Promise(function (r) { setTimeout(r, ms); });
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function fjpitUuid() {
|
|
|
|
|
+ try { return crypto.randomUUID(); } catch (e) { return 'p-' + Date.now() + '-' + Math.random().toString(36).slice(2); }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** 把日期对齐到所在周的周一(教务给的 startDate 不保证是周一);用 UTC 运算避免时区挪日 */
|
|
|
|
|
+function fjpitAlignToMonday(dateStr) {
|
|
|
|
|
+ const m = /^(\d{4})-(\d{1,2})-(\d{1,2})/.exec(String(dateStr == null ? '' : dateStr));
|
|
|
|
|
+ if (!m) return '';
|
|
|
|
|
+ const d = new Date(Date.UTC(+m[1], +m[2] - 1, +m[3]));
|
|
|
|
|
+ if (isNaN(d.getTime())) return '';
|
|
|
|
|
+ const dow = d.getUTCDay(); // 0 = 周日
|
|
|
|
|
+ d.setUTCDate(d.getUTCDate() + (dow === 0 ? -6 : 1 - dow));
|
|
|
|
|
+ const pad = function (n) { return n < 10 ? '0' + n : '' + n; };
|
|
|
|
|
+ return d.getUTCFullYear() + '-' + pad(d.getUTCMonth() + 1) + '-' + pad(d.getUTCDate());
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function fjpitSafeToast(msg) {
|
|
|
|
|
+ try { window.shiguangBridge.showToast(msg); } catch (e) { console.log('JS[toast]: ' + msg); }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ---------- 一、鉴权 ----------
|
|
|
|
|
+
|
|
|
|
|
+/** 从 Pinia store 取 accessToken;取不到返回 null,由调用方走「请登录」引导 */
|
|
|
|
|
+function fjpitGetAccessToken() {
|
|
|
|
|
+ const root = document.querySelector('#app') || document.body.firstElementChild;
|
|
|
|
|
+ const app = root && root.__vue_app__;
|
|
|
|
|
+ const gp = app && app.config && app.config.globalProperties;
|
|
|
|
|
+ const pinia = gp && gp.$pinia;
|
|
|
|
|
+ if (!pinia || !(pinia._s instanceof Map)) return null;
|
|
|
|
|
+ let fallback = null;
|
|
|
|
|
+ for (const entry of pinia._s) {
|
|
|
|
|
+ const store = entry[1];
|
|
|
|
|
+ if (store && typeof store.accessToken === 'string' && store.accessToken) {
|
|
|
|
|
+ if (String(entry[0]).indexOf('access') >= 0) return store.accessToken;
|
|
|
|
|
+ if (!fallback) fallback = store.accessToken;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return fallback;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** 结构化接口用的头 —— 与移动端 m.fjpit.com 一致 */
|
|
|
|
|
+function fjpitApiHeaders(token) {
|
|
|
|
|
+ const h = {
|
|
|
|
|
+ 'Accept': 'application/json, text/plain, */*',
|
|
|
|
|
+ 'Content-Type': 'application/json;charset=UTF-8',
|
|
|
|
|
+ 'server': '1',
|
|
|
|
|
+ 'unique-request-id': fjpitUuid(),
|
|
|
|
|
+ 'Accept-Language': 'zh-CN,zh;q=0.9'
|
|
|
|
|
+ };
|
|
|
|
|
+ if (token) h['ba-token'] = token;
|
|
|
|
|
+ return h;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ---------- 二、取数(结构化 API) ----------
|
|
|
|
|
+
|
|
|
|
|
+// 教务 WAF 有短时速率限制(连续快速请求会被成片拒绝),故加请求间隔 + 失败退避重试
|
|
|
|
|
+// 一旦出现失败就把全局间隔翻倍(上限 800ms)自适应降速,宁可慢也要把数据取全
|
|
|
|
|
+const FJPIT_REQ_GAP_MS = 90; // 相邻请求最小间隔(起始值)
|
|
|
|
|
+const FJPIT_GAP_MAX_MS = 800; // 自适应间隔上限
|
|
|
|
|
+const FJPIT_MAX_ATTEMPT = 3; // 单个请求最多尝试次数
|
|
|
|
|
+const FJPIT_RETRY_BASE_MS = 400; // 退避基数(第 n 次重试等 n × 此值)
|
|
|
|
|
+const FJPIT_WEEK_CONCURRENCY = 5; // 逐周抓取的并发度
|
|
|
|
|
+
|
|
|
|
|
+let FJPIT_CUR_GAP_MS = FJPIT_REQ_GAP_MS;
|
|
|
|
|
+let FJPIT_LAST_REQ_AT = 0;
|
|
|
|
|
+
|
|
|
|
|
+/** 请求节流:保证相邻请求之间有最小间隔(间隔随失败自适应放宽) */
|
|
|
|
|
+async function fjpitPace() {
|
|
|
|
|
+ const wait = FJPIT_CUR_GAP_MS - (Date.now() - FJPIT_LAST_REQ_AT);
|
|
|
|
|
+ if (wait > 0) await fjpitDelay(wait);
|
|
|
|
|
+ FJPIT_LAST_REQ_AT = Date.now();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** 统一请求入口:节流 + 失败退避 */
|
|
|
|
|
+async function fjpitApiRequest(method, path, body, token) {
|
|
|
|
|
+ let lastErr = null;
|
|
|
|
|
+ for (let attempt = 1; attempt <= FJPIT_MAX_ATTEMPT; attempt++) {
|
|
|
|
|
+ await fjpitPace();
|
|
|
|
|
+ try {
|
|
|
|
|
+ const init = {
|
|
|
|
|
+ method: method,
|
|
|
|
|
+ headers: fjpitApiHeaders(token),
|
|
|
|
|
+ credentials: 'omit',
|
|
|
|
|
+ mode: 'cors'
|
|
|
|
|
+ };
|
|
|
|
|
+ if (body !== undefined) init.body = JSON.stringify(body);
|
|
|
|
|
+ const resp = await fetch(FJPIT_API + path, init);
|
|
|
|
|
+ if (!resp.ok) throw new Error(path + ' HTTP ' + resp.status);
|
|
|
|
|
+ const json = await resp.json();
|
|
|
|
|
+ if (json.code !== 1) throw new Error(path + ' code=' + json.code + ' ' + (json.msg || ''));
|
|
|
|
|
+ return json.data;
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ lastErr = e;
|
|
|
|
|
+ // 出现失败说明很可能正在撞限流 → 全局间隔翻倍
|
|
|
|
|
+ FJPIT_CUR_GAP_MS = Math.min(FJPIT_GAP_MAX_MS, FJPIT_CUR_GAP_MS * 2);
|
|
|
|
|
+ if (attempt < FJPIT_MAX_ATTEMPT) {
|
|
|
|
|
+ const back = FJPIT_RETRY_BASE_MS * attempt;
|
|
|
|
|
+ console.warn('JS: ' + path + ' 第 ' + attempt + ' 次失败(' + e.message
|
|
|
|
|
+ + '),' + back + 'ms 后重试');
|
|
|
|
|
+ await fjpitDelay(back);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ throw lastErr;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function fjpitApiGet(path, token) {
|
|
|
|
|
+ return fjpitApiRequest('GET', path, undefined, token);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function fjpitApiPost(path, body, token) {
|
|
|
|
|
+ return fjpitApiRequest('POST', path, body, token);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** 限并发遍历:最多同时执行 limit 个 worker,结果按原顺序返回 */
|
|
|
|
|
+async function fjpitForEachLimit(items, limit, worker) {
|
|
|
|
|
+ const results = new Array(items.length);
|
|
|
|
|
+ const n = Math.max(1, Math.min(limit | 0, items.length));
|
|
|
|
|
+ let next = 0;
|
|
|
|
|
+ async function runner() {
|
|
|
|
|
+ for (;;) {
|
|
|
|
|
+ const i = next++;
|
|
|
|
|
+ if (i >= items.length) return;
|
|
|
|
|
+ results[i] = await worker(items[i], i);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ const runners = [];
|
|
|
|
|
+ for (let k = 0; k < n; k++) runners.push(runner());
|
|
|
|
|
+ await Promise.all(runners);
|
|
|
|
|
+ return results;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** 作息 → {slots, nodeMap}。午休/晚休不是正式节次,须剔除并把课程节次重编号 */
|
|
|
|
|
+function fjpitParseTimeSlots(raw) {
|
|
|
|
|
+ const arr = Array.isArray(raw) ? raw : Object.values(raw || {});
|
|
|
|
|
+ const ok = /^\d{1,2}:\d{2}$/;
|
|
|
|
|
+ const all = arr.map(function (e) {
|
|
|
|
|
+ return {
|
|
|
|
|
+ node: Number(e && e.jcdm),
|
|
|
|
|
+ name: String((e && e.jcmc) || ''),
|
|
|
|
|
+ startTime: String((e && e.jcskkssj) || '').slice(0, 5),
|
|
|
|
|
+ endTime: String((e && e.jcskjssj) || '').slice(0, 5)
|
|
|
|
|
+ };
|
|
|
|
|
+ }).filter(function (t) {
|
|
|
|
|
+ return t.node >= 1 && ok.test(t.startTime) && ok.test(t.endTime);
|
|
|
|
|
+ }).sort(function (a, b) { return a.node - b.node; });
|
|
|
|
|
+
|
|
|
|
|
+ // 教务按「含休息行」的顺序给课程节点编号,故剔除休息行后必须同步重编号
|
|
|
|
|
+ let kept = all.filter(function (t) { return !FJPIT_BREAK_RE.test(t.name); });
|
|
|
|
|
+ if (!kept.length) kept = all; // 名称识别不出节次时保持原样,避免误伤
|
|
|
|
|
+
|
|
|
|
|
+ const nodeMap = {};
|
|
|
|
|
+ const slots = kept.map(function (t, i) {
|
|
|
|
|
+ nodeMap[t.node] = i + 1;
|
|
|
|
|
+ return { number: i + 1, startTime: t.startTime, endTime: t.endTime };
|
|
|
|
|
+ });
|
|
|
|
|
+ return { slots: slots, nodeMap: nodeMap, dropped: all.length - kept.length };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** 节次编号折算到重排后的编号;无对应项时原样返回 */
|
|
|
|
|
+function fjpitMapNode(node, nodeMap) {
|
|
|
|
|
+ const n = Number(node);
|
|
|
|
|
+ return (nodeMap && nodeMap[n]) ? nodeMap[n] : n;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** 单条课表记录 → entry;课程名/星期/节次非法的返回 null */
|
|
|
|
|
+function fjpitParseEntry(rec, week, nodeMap) {
|
|
|
|
|
+ if (!rec) return null;
|
|
|
|
|
+ const name = String(rec.course == null ? '' : rec.course).trim();
|
|
|
|
|
+ const day = Number(rec.DayIndex);
|
|
|
|
|
+ const start = fjpitMapNode(rec.startNode, nodeMap);
|
|
|
|
|
+ const end = fjpitMapNode(rec.endNode, nodeMap);
|
|
|
|
|
+ if (!name || !(day >= 1 && day <= 7) || !(start >= 1) || !(end >= start)) return null;
|
|
|
|
|
+ return {
|
|
|
|
|
+ week: week, day: day, date: '',
|
|
|
|
|
+ start: start, end: end, name: name,
|
|
|
|
|
+ teacher: rec.teacherName == null ? '' : String(rec.teacherName).trim(),
|
|
|
|
|
+ room: rec.spaceName == null ? '' : String(rec.spaceName).trim()
|
|
|
|
|
+ };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** 走教务接口取全部数据,任一步失败直接抛错。
|
|
|
|
|
+ * 并发编排:/semesters 串行 → 配置 + 作息并发 → 逐周限并发 FJPIT_WEEK_CONCURRENCY */
|
|
|
|
|
+async function fjpitCollectData(token) {
|
|
|
|
|
+ const t0 = Date.now();
|
|
|
|
|
+
|
|
|
|
|
+ // 1) 学期列表(必须最先,后续依赖它)
|
|
|
|
|
+ const semData = await fjpitApiGet('/semesters', token);
|
|
|
|
|
+ const semList = (semData && semData.semesters) || [];
|
|
|
|
|
+ if (!semList.length) throw new Error('/semesters 无学期数据');
|
|
|
|
|
+ const cur = semList.filter(function (s) { return s && s.isCurrent; })[0] || semList[0];
|
|
|
|
|
+ const semester = cur.value;
|
|
|
|
|
+ const showXxq = (cur.isCurrent && cur.isXxq) ? 1 : 0;
|
|
|
|
|
+
|
|
|
|
|
+ // 2) 学期配置(关键,失败即中断)与作息(非关键)并发
|
|
|
|
|
+ const slotPromise = fjpitApiPost('/scheduleTime', { dqz: 1 }, token).catch(function (e) {
|
|
|
|
|
+ console.warn('JS: 作息读取失败(不影响课表): ' + e.message);
|
|
|
|
|
+ return null;
|
|
|
|
|
+ });
|
|
|
|
|
+ const cfgData = await fjpitApiPost('/semesterConfig', { semester: semester }, token);
|
|
|
|
|
+ const sc = (cfgData && cfgData.semestersConfig) || cfgData || {};
|
|
|
|
|
+ const totalWeeks = Number(sc.totalWeeks) || 0;
|
|
|
|
|
+ if (!(totalWeeks >= 1)) throw new Error('/semesterConfig 未返回有效 totalWeeks');
|
|
|
|
|
+ const startDate = fjpitAlignToMonday(sc.startDate); // 教务给的不一定是周一
|
|
|
|
|
+ const ts = fjpitParseTimeSlots(await slotPromise);
|
|
|
|
|
+ console.log('JS: 作息 ' + ts.slots.length + ' 节(剔除休息行 ' + ts.dropped + ')');
|
|
|
|
|
+
|
|
|
|
|
+ // 3) 逐周课表(限并发;单周失败只记账不中断)
|
|
|
|
|
+ const weeks = [];
|
|
|
|
|
+ for (let w = 1; w <= totalWeeks; w++) weeks.push(w);
|
|
|
|
|
+
|
|
|
|
|
+ const results = await fjpitForEachLimit(weeks, FJPIT_WEEK_CONCURRENCY, async function (w) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const d = await fjpitApiPost('/schedule',
|
|
|
|
|
+ { semester: semester, week: w, showxxq: showXxq }, token);
|
|
|
|
|
+ return { week: w, list: Array.isArray(d && d.list) ? d.list : [], ok: true };
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.warn('JS: 第 ' + w + ' 周抓取失败: ' + e.message);
|
|
|
|
|
+ return { week: w, list: [], ok: false };
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const entries = [];
|
|
|
|
|
+ const failedWeeks = [];
|
|
|
|
|
+ results.forEach(function (r) {
|
|
|
|
|
+ if (!r.ok) { failedWeeks.push(r.week); return; }
|
|
|
|
|
+ r.list.forEach(function (rec) {
|
|
|
|
|
+ const e = fjpitParseEntry(rec, r.week, ts.nodeMap);
|
|
|
|
|
+ if (e) entries.push(e);
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (failedWeeks.length > Math.floor(totalWeeks / 2)) {
|
|
|
|
|
+ throw new Error('过半周次抓取失败(' + failedWeeks.length + '/' + totalWeeks + '):'
|
|
|
|
|
+ + failedWeeks.join(','));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const elapsedMs = Date.now() - t0;
|
|
|
|
|
+ console.log('JS: 取数完成 ' + elapsedMs + 'ms;失败周次 ' + failedWeeks.length
|
|
|
|
|
+ + ';请求间隔 ' + FJPIT_CUR_GAP_MS + 'ms');
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ entries: entries, timeSlots: ts.slots,
|
|
|
|
|
+ semesterStartDate: startDate, totalWeeks: totalWeeks,
|
|
|
|
|
+ failedWeeks: failedWeeks, elapsedMs: elapsedMs
|
|
|
|
|
+ };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ---------- 三、聚合(教师/教室原样透传) ----------
|
|
|
|
|
+
|
|
|
|
|
+function fjpitAggregate(entries) {
|
|
|
|
|
+ const SEP = '\u0001';
|
|
|
|
|
+ const map = new Map();
|
|
|
|
|
+ entries.forEach(function (e) {
|
|
|
|
|
+ const key = [e.name, e.teacher, e.room, e.day, e.start, e.end].join(SEP);
|
|
|
|
|
+ if (!map.has(key)) {
|
|
|
|
|
+ map.set(key, {
|
|
|
|
|
+ name: e.name,
|
|
|
|
|
+ teacher: e.teacher, // 可能为空字符串,原样保留
|
|
|
|
|
+ position: e.room, // 可能为空字符串,原样保留
|
|
|
|
|
+ day: e.day,
|
|
|
|
|
+ startSection: e.start,
|
|
|
|
|
+ endSection: e.end,
|
|
|
|
|
+ weeks: []
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ const course = map.get(key);
|
|
|
|
|
+ if (course.weeks.indexOf(e.week) < 0) course.weeks.push(e.week);
|
|
|
|
|
+ });
|
|
|
|
|
+ const list = Array.from(map.values());
|
|
|
|
|
+ list.forEach(function (c) { c.weeks.sort(function (a, b) { return a - b; }); });
|
|
|
|
|
+ list.sort(function (a, b) {
|
|
|
|
|
+ return a.day - b.day || a.startSection - b.startSection || a.name.localeCompare(b.name);
|
|
|
|
|
+ });
|
|
|
|
|
+ return list;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ---------- 四、保存 ----------
|
|
|
|
|
+
|
|
|
|
|
+async function fjpitSaveCourses(courses) {
|
|
|
|
|
+ await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses, null, 2));
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** 作息要求编号从 1 起连续,否则不导入(返回 false) */
|
|
|
|
|
+async function fjpitSaveTimeSlots(timeSlots) {
|
|
|
|
|
+ if (!timeSlots.length) return false;
|
|
|
|
|
+ for (let i = 0; i < timeSlots.length; i++) {
|
|
|
|
|
+ if (timeSlots[i].number !== i + 1) return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
|
|
|
|
|
+ return true;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function fjpitSaveConfig(semesterStartDate, totalWeeks) {
|
|
|
|
|
+ const config = { semesterTotalWeeks: totalWeeks };
|
|
|
|
|
+ if (semesterStartDate) config.semesterStartDate = semesterStartDate;
|
|
|
|
|
+ await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(config));
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ---------- 五、等待登录 ----------
|
|
|
|
|
+
|
|
|
|
|
+/** 轮询等待用户登录;页面被整页重载则提前返回 null */
|
|
|
|
|
+async function fjpitWaitForLogin(deadlineTs) {
|
|
|
|
|
+ const bodyRef = document.body;
|
|
|
|
|
+ let lastTip = 0;
|
|
|
|
|
+ while (Date.now() < deadlineTs) {
|
|
|
|
|
+ if (document.body !== bodyRef) return null;
|
|
|
|
|
+ const token = fjpitGetAccessToken();
|
|
|
|
|
+ if (token) return token;
|
|
|
|
|
+ if (Date.now() - lastTip > 60000) { // 每分钟用原生 toast 提醒一次
|
|
|
|
|
+ lastTip = Date.now();
|
|
|
|
|
+ fjpitSafeToast('请在页面中登录教务账号,登录后会自动继续导入');
|
|
|
|
|
+ }
|
|
|
|
|
+ await fjpitDelay(700);
|
|
|
|
|
+ }
|
|
|
|
|
+ return null;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ---------- 六、主流程(编排) ----------
|
|
|
|
|
+// 按官方推荐的编排模式:只做顺序编排;任一步失败立即 return;
|
|
|
|
|
+// notifyTaskCompletion() 只在完全成功后调用。
|
|
|
|
|
+
|
|
|
|
|
+/** 确保已登录;未登录则用原生 toast 提示并等待 */
|
|
|
|
|
+async function fjpitEnsureLogin() {
|
|
|
|
|
+ let token = fjpitGetAccessToken();
|
|
|
|
|
+ if (token) return token;
|
|
|
|
|
+
|
|
|
|
|
+ fjpitSafeToast('未检测到登录状态,请在页面中登录教务账号');
|
|
|
|
|
+ token = await fjpitWaitForLogin(Date.now() + FJPIT_LOGIN_WAIT_MS);
|
|
|
|
|
+ if (!token) token = fjpitGetAccessToken(); // 兜底:前端可能刚把 token 写进 store
|
|
|
|
|
+ return token;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** 聚合 + 保存。课程保存失败会抛出(必须中断);作息与配置尽力而为 */
|
|
|
|
|
+async function fjpitSaveAll(data) {
|
|
|
|
|
+ const courses = fjpitAggregate(data.entries);
|
|
|
|
|
+ console.log('JS: 原始条目 ' + data.entries.length + ',聚合为 ' + courses.length
|
|
|
|
|
+ + ' 条课程;开学日期 ' + data.semesterStartDate);
|
|
|
|
|
+
|
|
|
|
|
+ await fjpitSaveCourses(courses);
|
|
|
|
|
+
|
|
|
|
|
+ let timeSlotSaved = false;
|
|
|
|
|
+ try {
|
|
|
|
|
+ timeSlotSaved = await fjpitSaveTimeSlots(data.timeSlots || []);
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.warn('JS: 作息时间导入失败: ' + e.message);
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ await fjpitSaveConfig(data.semesterStartDate, data.totalWeeks);
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.warn('JS: 课表配置保存失败: ' + e.message);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return { courses: courses, timeSlotSaved: timeSlotSaved };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** 汇总公告 —— 本脚本唯一的弹窗 */
|
|
|
|
|
+async function fjpitReport(data, saved) {
|
|
|
|
|
+ const courses = saved.courses;
|
|
|
|
|
+
|
|
|
|
|
+ const nameSet = {};
|
|
|
|
|
+ courses.forEach(function (c) { nameSet[c.name] = 1; });
|
|
|
|
|
+
|
|
|
|
|
+ const summary = [
|
|
|
|
|
+ '导入完成',
|
|
|
|
|
+ '课程行数:' + courses.length + '(' + Object.keys(nameSet).length + ' 门课)',
|
|
|
|
|
+ '学期周数:' + data.totalWeeks,
|
|
|
|
|
+ '开学日期:' + (data.semesterStartDate || '未取到'),
|
|
|
|
|
+ '作息时间:' + (saved.timeSlotSaved ? (data.timeSlots || []).length + ' 节' : '未导入'),
|
|
|
|
|
+ '取数耗时:' + (data.elapsedMs ? ((data.elapsedMs / 1000).toFixed(1) + 's') : '未知'),
|
|
|
|
|
+ (data.failedWeeks && data.failedWeeks.length)
|
|
|
|
|
+ ? '失败周次:' + data.failedWeeks.join(',') : '全部周次抓取成功'
|
|
|
|
|
+ ];
|
|
|
|
|
+ console.log('JS: ' + summary.join(' | '));
|
|
|
|
|
+
|
|
|
|
|
+ await window.shiguangBridgePromise.showAlert('导入完成', summary.join('\n'), '好的');
|
|
|
|
|
+ fjpitSafeToast('导入成功,共 ' + courses.length + ' 条课程');
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** 编排入口;任一步失败或用户取消即 return */
|
|
|
|
|
+async function runImportFlow() {
|
|
|
|
|
+ console.log('JS: 福信智慧教务课表导入开始(v21)');
|
|
|
|
|
+
|
|
|
|
|
+ // 0. 环境准备
|
|
|
|
|
+ if (location.host.indexOf(FJPIT_HOST_KEY) < 0) {
|
|
|
|
|
+ fjpitSafeToast('请先在福信智慧教务页面登录后再执行导入。');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ const token = await fjpitEnsureLogin();
|
|
|
|
|
+ if (!token) {
|
|
|
|
|
+ fjpitSafeToast('未等到登录状态,请登录后重新点「执行导入」');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 取数(全部走教务接口,不解析页面 HTML)
|
|
|
|
|
+ let data;
|
|
|
|
|
+ try {
|
|
|
|
|
+ data = await fjpitCollectData(token);
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ fjpitSafeToast('取数失败,请确认已登录教务系统后重试:' + e.message);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!data.entries.length) {
|
|
|
|
|
+ fjpitSafeToast('未取到任何课程,请确认本学期是否有排课。');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 聚合 + 保存
|
|
|
|
|
+ let saved;
|
|
|
|
|
+ try {
|
|
|
|
|
+ saved = await fjpitSaveAll(data);
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ fjpitSafeToast('课程保存失败:' + e.message);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 4. 汇总
|
|
|
|
|
+ await fjpitReport(data, saved);
|
|
|
|
|
+
|
|
|
|
|
+ // 5. 完全成功,才发结束信号
|
|
|
|
|
+ window.shiguangBridge.notifyTaskCompletion();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+runImportFlow().catch(function (e) {
|
|
|
|
|
+ console.error('JS: 导入流程异常', e);
|
|
|
|
|
+ try { window.shiguangBridge.showToast('导入失败: ' + (e && e.message)); } catch (x) { }
|
|
|
|
|
+});
|