فهرست منبع

Merge pull request #633 from XingHeYuZhuan/pending

星河欲转 2 هفته پیش
والد
کامیت
9bb84d3693
3فایلهای تغییر یافته به همراه299 افزوده شده و 0 حذف شده
  1. 5 0
      index/root_index.yaml
  2. 8 0
      resources/BBGU/adapters.yaml
  3. 286 0
      resources/BBGU/bbgu.js

+ 5 - 0
index/root_index.yaml

@@ -28,6 +28,11 @@ schools:
     initial: "U"
     resource_folder: "urp_jiaowu"
 
+  - id: "BBGU"
+    name: "北部湾大学"
+    initial: "B"
+    resource_folder: "BBGU"
+
   - id: "BUPT"
     name: "北京邮电大学"
     initial: "B"

+ 8 - 0
resources/BBGU/adapters.yaml

@@ -0,0 +1,8 @@
+adapters:
+  - adapter_id: "BBGU_01"
+    adapter_name: "北部湾大学教务系统"
+    category: "BACHELOR_AND_ASSOCIATE"
+    asset_js_path: "bbgu.js"
+    import_url: "https://zhjw.bbgu.edu.cn/workspace/home"
+    maintainer: "Haooz"
+    description: "北部湾大学智慧教务系统课表导入,登录统一身份认证平台后直接点击导入即可"

+ 286 - 0
resources/BBGU/bbgu.js

@@ -0,0 +1,286 @@
+// 北部湾大学智慧教务系统(zhjw.bbgu.edu.cn)拾光课表导入适配脚本
+// 新一代金智 resourceapi 接口:session/list 取学期、timePattern/get-large-period 取作息、
+// timetable/class/timetable/student/my-table-detail 取课表
+
+// 解析周次字符串:"1-4,6-9" → [1,2,3,4,6,7,8,9]
+function parseWeeks(weekStr) {
+    const weeks = [];
+    String(weekStr || "").split(",").forEach(part => {
+        part = part.trim();
+        const range = part.match(/^(\d+)-(\d+)$/);
+        if (range) {
+            const start = Number(range[1]);
+            const end = Number(range[2]);
+            for (let i = start; i <= end; i++) weeks.push(i);
+        } else if (/^\d+$/.test(part)) {
+            weeks.push(Number(part));
+        }
+    });
+    return [...new Set(weeks)].sort((a, b) => a - b);
+}
+
+// 解析节次字符串:"10-11" → { startSection: 10, endSection: 11 };单值 "12" → 第12节
+function parsePeriods(periodStr) {
+    const str = String(periodStr || "").trim();
+    const m = str.match(/^(\d+)-(\d+)$/);
+    if (m) {
+        const start = Number(m[1]);
+        const end = Number(m[2]);
+        if (!start || !end || start > end) return null;
+        return { startSection: start, endSection: end };
+    }
+    if (/^\d+$/.test(str)) {
+        const n = Number(str);
+        if (!n) return null;
+        return { startSection: n, endSection: n };
+    }
+    return null;
+}
+
+// 清洗教师名:"周文红[主讲];" → "周文红";"王柏玲[主讲];谢娟[主讲];" → "王柏玲、谢娟"
+function cleanTeacher(name) {
+    const cleaned = String(name || "")
+        .replace(/\[[^\]]*\]/g, "")
+        .replace(/[;;]+/g, "、")
+        .replace(/、+$/, "")
+        .trim();
+    return cleaned || "未知";
+}
+
+function parseCourse(row) {
+    const day = Number(row.weekDay);
+    const periods = parsePeriods(row.periodFormat);
+    const weeks = parseWeeks(row.teachingWeekFormat);
+    if (!row.courseName || !day || day < 1 || day > 7 || !periods || weeks.length === 0) return null;
+
+    const campus = String(row.campusName || "").trim();
+    const room = String(row.roomName || "").trim();
+    let position = room;
+    if (campus && room) position = `${room}(${campus})`;
+    if (!position) position = campus || "待定";
+
+    return {
+        name: String(row.courseName).trim(),
+        teacher: cleanTeacher(row.instructorName),
+        position,
+        day,
+        startSection: periods.startSection,
+        endSection: periods.endSection,
+        weeks
+    };
+}
+
+function getErrorMessage(error) {
+    if (error && typeof error.message === "string" && error.message.trim()) return error.message;
+    if (typeof error === "string" && error.trim()) return error;
+    try {
+        const serialized = JSON.stringify(error);
+        if (serialized && serialized !== "{}") return serialized;
+    } catch (_) {
+        // Ignore serialization failures and use the generic fallback below.
+    }
+    return "未知错误";
+}
+
+// 从页面存储中提取 JWT 鉴权 token(金智系统常见 key + 全量扫描兜底)
+function getAuthToken() {
+    const keys = ["Authorization", "access_token", "token", "X-Access-Token", "x-token", "auth_token", "jwt", "user_token"];
+    for (const k of keys) {
+        try {
+            const v = window.localStorage.getItem(k) || window.sessionStorage.getItem(k);
+            if (v && v.includes("eyJ")) return v;
+        } catch (_) {
+            // Ignore storage access errors.
+        }
+    }
+    // 全量扫描:找直接以 eyJ 开头(JWT)或 JSON 包装中含 eyJ 的字符串
+    const stores = [window.localStorage, window.sessionStorage];
+    for (const store of stores) {
+        try {
+            for (let i = 0; i < store.length; i++) {
+                const raw = store.getItem(store.key(i));
+                if (!raw) continue;
+                if (raw.startsWith("eyJ")) return raw;
+                const m = raw.match(/"([^"]*eyJ[^"]*)"/);
+                if (m) return m[1];
+            }
+        } catch (_) {
+            // Ignore storage access errors.
+        }
+    }
+    return null;
+}
+
+async function requestJson(url, options) {
+    const token = getAuthToken();
+    const headers = { "X-Requested-With": "XMLHttpRequest" };
+    if (token) headers["Authorization"] = token.startsWith("Bearer ") ? token : `Bearer ${token}`;
+    const response = await fetch(url, Object.assign({
+        credentials: "include",
+        headers
+    }, options));
+    if (!response.ok) throw new Error(`接口请求失败(HTTP ${response.status})`);
+    return response.json();
+}
+
+// 获取学期列表,返回 { id, year, term, beginDate, endDate, active }
+async function fetchSemesters() {
+    const payload = await requestJson("https://zhjw.bbgu.edu.cn/api/resourceapi/session/list");
+    const list = payload && Array.isArray(payload.sessionVOList) ? payload.sessionVOList : [];
+    return list.filter(s => s && s.id && s.year && s.term);
+}
+
+// 获取作息时间(第1-N节)
+async function fetchTimeSlots(sessionId) {
+    const payload = await requestJson(
+        `https://zhjw.bbgu.edu.cn/api/resourceapi/timePattern/get-large-period?sessionId=${encodeURIComponent(sessionId)}`
+    );
+    const patterns = payload && Array.isArray(payload.data) ? payload.data : [];
+    const slots = [];
+    for (const group of patterns) {
+        const list = Array.isArray(group.periodList) ? group.periodList : [];
+        for (const p of list) {
+            const number = Number(p.smallPeriod);
+            if (!number || !p.startTime || !p.endTime) continue;
+            slots.push({ number, startTime: p.startTime, endTime: p.endTime });
+        }
+    }
+    return slots.sort((a, b) => a.number - b.number);
+}
+
+// 获取课表并解析
+async function fetchCourses(sessionId) {
+    window.shiguangBridge.showToast("正在请求课表数据...");
+    const payload = await requestJson(
+        `https://zhjw.bbgu.edu.cn/api/timetable/class/timetable/student/my-table-detail?sessionId=${encodeURIComponent(sessionId)}`,
+        { method: "POST", credentials: "include" }
+    );
+    const rows = payload && Array.isArray(payload.classTimetableVOList)
+        ? payload.classTimetableVOList
+        : (payload && payload.data && Array.isArray(payload.data.classTimetableVOList) ? payload.data.classTimetableVOList : []);
+    if (rows.length === 0) throw new Error("教务系统返回数据格式异常。");
+
+    const rawCourses = rows.map(parseCourse).filter(Boolean);
+
+    // 同一门课多处时段会分多条,按组合键去重
+    const seen = new Set();
+    const courses = [];
+    for (const c of rawCourses) {
+        const key = `${c.name}|${c.day}|${c.startSection}|${c.endSection}|${c.weeks.join(',')}|${c.teacher}|${c.position}`;
+        if (seen.has(key)) continue;
+        seen.add(key);
+        courses.push(c);
+    }
+    if (courses.length === 0) throw new Error("未找到包含有效时间的课程。");
+    courses.sort((a, b) => a.day - b.day || a.startSection - b.startSection || a.name.localeCompare(b.name));
+    return courses;
+}
+
+// 判断是否处于登录页
+function isLoginPage() {
+    const url = window.location.href;
+    return url.includes("authserver") && url.includes("login");
+}
+
+// 选择学期,默认定位到当前激活学期
+async function selectSemester() {
+    const list = await fetchSemesters();
+    if (list.length === 0) throw new Error("未获取到学期列表。");
+
+    const defaultIndex = Math.max(0, list.findIndex(s => s.active === "Y"));
+    const names = list.map(s => `${s.year}${s.term === "春" ? "春季" : "秋季"}学期`);
+    const selectedIndex = await window.shiguangBridgePromise.showSingleSelection(
+        "请选择学期",
+        JSON.stringify(names),
+        defaultIndex
+    );
+    if (selectedIndex === null || selectedIndex === undefined) return null;
+    return list[selectedIndex];
+}
+
+// 保存预设作息时间(失败仅告警,不阻断课程导入)
+async function saveTimeSlots(timeSlots) {
+    if (!timeSlots || timeSlots.length === 0) return;
+    try {
+        await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
+        console.log("JS: 作息时间保存成功");
+    } catch (error) {
+        console.error("JS: 作息时间保存失败:", error);
+    }
+}
+
+// 获取当前学期最大周次(失败返回 null)
+async function fetchMaxWeek(sessionId) {
+    try {
+        const payload = await requestJson(
+            `https://zhjw.bbgu.edu.cn/api/timetable/course/maxWeek/${encodeURIComponent(sessionId)}`
+        );
+        const weeks = Number(payload && payload.data);
+        return weeks > 0 ? weeks : null;
+    } catch (error) {
+        console.error("JS: 获取最大周次失败:", error);
+        return null;
+    }
+}
+
+// 保存课表配置(开学日期、总周数;失败仅告警,不阻断课程导入)
+async function saveCourseConfig(semester, courses) {
+    try {
+        const dateDiffDays = (new Date(semester.endDate) - new Date(semester.beginDate)) / 86400000;
+        const weeksByDate = Math.round(dateDiffDays / 7);
+        const maxCourseWeek = Math.max(0, ...courses.map(c => Math.max(...c.weeks)));
+        const maxWeek = await fetchMaxWeek(semester.id);
+        const config = {
+            semesterStartDate: semester.beginDate,
+            semesterTotalWeeks: maxWeek || Math.max(maxCourseWeek, weeksByDate || 20)
+        };
+        await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(config));
+        console.log(`JS: 课表配置保存成功(开学 ${config.semesterStartDate},共 ${config.semesterTotalWeeks} 周)`);
+    } catch (error) {
+        console.error("JS: 课表配置保存失败:", error);
+    }
+}
+
+async function runImportFlow() {
+    if (isLoginPage()) {
+        window.shiguangBridge.showToast("导入失败:请先登录教务系统!");
+        return;
+    }
+
+    const alertConfirmed = await window.shiguangBridgePromise.showAlert(
+        "教务系统课表导入",
+        "导入前请确保您已登录教务系统,建议在课表页面进行导入",
+        "好的,开始导入"
+    );
+    if (!alertConfirmed) {
+        window.shiguangBridge.showToast("用户取消了导入。");
+        return;
+    }
+
+    window.shiguangBridge.showToast("正在获取学期信息...");
+    try {
+        const semester = await selectSemester();
+        if (!semester) {
+            window.shiguangBridge.showToast("用户取消了导入。");
+            return;
+        }
+        console.log(`JS: 选择学期: ${semester.year}${semester.term} (${semester.id})`);
+
+        const courses = await fetchCourses(semester.id);
+        window.shiguangBridge.showToast(`正在保存 ${courses.length} 门课程...`);
+        await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses, null, 2));
+
+        await saveCourseConfig(semester, courses);
+
+        const timeSlots = await fetchTimeSlots(semester.id);
+        await saveTimeSlots(timeSlots);
+
+        window.shiguangBridge.showToast(`课程导入成功,共导入 ${courses.length} 门课程!`);
+        window.shiguangBridge.notifyTaskCompletion();
+    } catch (error) {
+        window.shiguangBridge.showToast(`导入失败:${getErrorMessage(error)}`);
+        console.error("JS: Import Error:", error);
+    }
+}
+
+runImportFlow();