跳转到内容

KEC 管理器 — 安全漏洞与业务/数据逻辑/前后端联调审查报告

审查日期:2026-07-13 审查范围:server/src/(Node/Express 5 + Prisma + SQLite)、client/(Vue 3 + Vite + Pinia + Element Plus + axios) 审查方式:静态只读审计(3 路并行深度扫描 + 关键结论逐条源码复核) 结论速览:未发现 Critical / High 级阻断性安全漏洞;代码整体安全水位较高(helmet、CORS 白名单、限流、XSS 输入清洗、CSRF 双提交、JWT jti 黑名单 fail-close、RBAC 一致、无 SQL 注入、无危险函数)。业务逻辑发现 1 项 High + 若干 Medium/Low;前后端联调基本正确(仅 1 项 Low 命名约定违反 + 2 项 Info 清理建议)。


零、总览清单(按严重度)

#严重度问题位置
B1业务/课时统计High合班班级导致教师周课时被重复计数(容量/告警/展示虚高)services/arrange/queries.js:173-196
S1安全Medium限流器被伪造 X-Forwarded-For 绕过app.js:33 + express-rate-limit
S2安全MediumXSRF-TOKEN Cookie 非 HttpOnly,可被 XSS 读取routes/auth.routes.js:38-45,124-129
B2业务/筛选Medium按年级筛选越界 over-inclusionservices/arrange/queries.js:71-79,106-150
S3安全Low–Medium3 处导出文件名未 encodeURIComponent(响应头注入/下载异常)controllers/export/data-export.controller.js:474,767,896
B3业务/数据联动Medium→Low方案窗口收缩/删除后 teaching_assignments 成孤儿(前后清单不一致)controllers/plan/plan-matrix.controller.js:178-185,319-352
S4安全Low下载令牌以 URL 参数传递、非单次使用、可作全路由鉴权middleware/auth.middleware.js:60-83
S5安全Low系统重置清空 token 黑名单,已吊销令牌复效controllers/settings.controller.js:256
B4业务/学期Lowduration_years=null 班级被静默排除,与 status=active 展示口径不一致services/semester.service.js:217-228 vs class.controller.js:108-116
B5业务/课时统计Lowweeks_countweeks_per_semester 同时缺失时 totalHours=NaNservices/arrange/queries.js:128,147
B6业务/数据联动Low/InfodeleteCourse 注释称 onDelete:Cascade 与 schema(Restrict)不符course.controller.js:130 vs schema.prisma:322
S6安全Low自动排课为同步 CPU 密集,缺输入规模/速率保护(DoS 隐患)services/arrange/tabu-search.js
S7安全Info/Low响应未做输出清洗,存储型 XSS 依赖前端middleware/xss.js(仅输入清洗)
F1前端/约定Low班级批量更新 updates 误用 snake_case 键client/src/views/class/ClassList.vue:679-693
F2前端/死接口InfoPOST /auth/download-token 孤儿端点,前端从未调用routes/auth.routes.js:257
F3前端/类型InfoPaginatedResponse 声明 items 但审计实际返回 logsclient/src/api/types.js:23-26

一、高危安全漏洞检测

总体结论

未发现 Critical / High 级别安全漏洞。 以下为已复核确认安全的控制项(避免重复告警):

  • 无 SQL 注入:全仓仅 lib/prisma.js:10-13$queryRawUnsafe 用于硬编码 PRAGMA(无用户输入);lock.js$executeRaw 用 Prisma 模板标签参数化;app.js:135 为无插值 SELECT 1
  • 无命令执行/危险函数:全仓未使用 eval / new Function / child_process / exec / spawn
  • RBAC 一致:所有变更型路由均经 authMiddleware + roleMiddlewareuser.controller.js 限制 admin 只能建 viewer(:63)、自改角色被禁(:130)、禁止改 super_admin(:140)、仅 super_admin 可改 role(:152)。
  • 错误信息不泄露middleware/error.js 生产环境恒定不回显堆栈/SQL。
  • 密钥管理.env 已 gitignore;auth.config.js 对 JWT 密钥做强度/熵校验,生产拒绝弱密钥与占位符,refresh/download 密钥独立或 HKDF 派生。
  • CSRF 登录闭环完整:存在 GET /api/auth/csrf-token(GET 豁免 CSRF)专门下发 Cookie,前端登录前先取(见第三部分)。

安全发现明细

S1【Medium】限流器可被伪造 X-Forwarded-For 绕过

  • 位置app.js:33 app.set('trust proxy', 1) + 各 express-rate-limit 默认用 req.ip
  • 现象trust proxy=1 时 Express 把 req.ip 取为 X-Forwarded-For 最左值。若前置 Nginx 仅追加而非覆盖客户端传入的 XFF,攻击者可自造 X-Forwarded-For 任意变换 req.ip
  • 影响:login 限流(10 次/15min)、全局 /api 120 次/min、导入/导出限流全部失效 → 削弱抗爆破/抗爬/抗 DoS。usernameLimiter(按用户名计数)部分缓解,但全局与导出限流被绕过。
  • 修复方案
    1. 运维侧(必须):Nginx 用 proxy_set_header X-Forwarded-For $remote_addr;(覆盖而非追加),从源头清洗。
    2. 代码侧(纵深防御):为 apiLimiter 等配置 keyGenerator,仅信任来自可信代理的 IP(如 req.ip 在单跳代理下已准确,可加 validate:false 关掉 IPv6 警告)。若多跳代理,应取 X-Forwarded-For倒数第二跳而非最左。
  • 可能影响的其他调用:仅改变限流计数归因;将同一代理后多用户共享计数器(轻微)。无功能破坏。
  • 位置routes/auth.routes.js:38-45(setAuthCookies)、:124-129(csrf-token 路由)设置 XSRF-TOKEN未带 httpOnlymiddleware/csrf.js:5 注释明示“非 HttpOnly,JS 可读”。
  • 现象:当前采用 Double-Submit Cookie 方案,要求前端把 Cookie 值读入 X-CSRF-Token 请求头。该方案本身要求 Cookie 可被 JS 读取,故直接加 httpOnly 会破坏登录(前端读不到、无法设头)。
  • 影响(真实但有限):若前端存在存储型 XSS(见 S7),脚本可 document.cookie 读取 XSRF-TOKEN,配合同样可被 XSS 取得的 Bearer Token,伪造带 CSRF 头的状态变更请求。注意:本项目鉴权走 Authorization: Bearer 头(非 cookie 自动携带),因此跨站 CSRF 风险已被 sameSite:strict 大幅削弱;XSRF Cookie 非 HttpOnly 是在“已发生 XSS”前提下的附加风险。
  • 修复方案(按性价比排序)
    1. 根因治理(推荐):确保 Bearer Token 不存 localStorage(改内存 + refresh 续期),并给所有返回前端的字符串字段做输出转义(见 S7),先消灭 XSS 这一前置条件。
    2. 方案升级(中长期):将 CSRF 双提交改为服务端会话绑定——登录时把 CSRF 密钥存入服务端会话/Redis,校验时不仅比头与 Cookie,还比服务端期望值;或彻底移除 CSRF 依赖,仅靠 SameSite=strict Cookie + Bearer 头(Bearer 不会跨站自动携带,CSRF 天然缓解)。
  • 可能影响的其他调用:若直接加 httpOnly立即破坏登录(前端无法读 Cookie 设头)——切勿只改这一处。必须配合 CSRF 方案改造一起做。

S3【Low–Medium】3 处导出文件名未 encodeURIComponent

  • 位置data-export.controller.js:474教材使用_${textbook.title}.xlsx)、:767课时统计_${semester}.xlsx)、:896教学安排_${course.name}_${semester}.xlsx)。其余导出均用了 encodeURIComponent
  • 现象textbook.title / course.name 由管理员经接口写入 DB,可控。直接拼接写入 Content-Disposition
  • 影响:① 现代 Node res.setHeader\r\nInvalid character in header content → 该导出 500(受限 DoS);② 含空格/中文/特殊符号时部分浏览器下载文件名乱码。
  • 修复方案:统一改为
    js
    const rawName = `教材使用_${textbook.title}.xlsx`;
    res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(rawName)}`);
    三处同理(:767semesterYYYY-YYYY-N 安全,但仍统一处理更稳)。
  • 可能影响的其他调用:无。纯响应头修正,不改变业务数据。

S4【Low】下载令牌 URL 传递、非单次使用、可作全路由鉴权

  • 位置middleware/auth.middleware.js:60-83(任意路由带 download_token 即通过校验并 next())。
  • 现象:download token 有效期 30s、密钥独立,但经 ?download_token= 传递 → 易落浏览器历史/Referer/代理日志;非单次使用(无 jti 黑名单);且 authMiddleware所有路由都接受它作身份凭证。
  • 影响:令牌泄露后 30s 窗口内获得该用户完整角色权限。
  • 修复方案:下载改为“与具体资源 ID 绑定的短时效签名 URL”,使用后失效;避免把身份令牌放进 URL;或仅在校验导出路由时接受该令牌(而非全局)。
  • 可能影响的其他调用:前端导出目前走 Authorization 头 + 该查询令牌双路(auth.middleware.js:60-61 也支持),改造需同步前端导出逻辑(见 F2)。

S5【Low】系统重置清空 token 黑名单,已吊销令牌复效

  • 位置settings.controller.js:256 await tx.token_blacklist.deleteMany();
  • 现象resetSystem(仅 super_admin)执行后,此前因登出/改密/重置入黑名单、本应 fail-close 失效的 access/refresh token 重新可用,直到 JWT 自然过期。
  • 修复方案:重置时保留尚未过期的黑名单条目(只删 expires_at < now 的),或重置后强制全量会话失效(版本号/全局踢下线)。
  • 可能影响的其他调用:改变“重置后旧会话是否仍有效”的语义——属预期增强,无功能破坏。

S6【Low】自动排课同步 CPU 密集,缺规模/速率保护

  • 位置services/arrange/tabu-search.jsauto-arrange.js(需登录鉴权;TABU_SEARCH.ENABLED=false 默认关;单课程超时 15s)。
  • 现象:贪心 + 禁忌搜索为同步阻塞,单轮迭代不释放事件循环;无限流/规模上限。已登录用户对多 courseId 并发触发可长期占用事件循环。
  • 修复方案:对 /auto-arrange/batch-auto-arrange每用户速率限制;对中大规模排课改为可中断的异步任务(Worker/队列)。
  • 可能影响的其他调用:仅新增限流,正常用户单课程排课不受影响。

S7【Info/Low】响应未做输出清洗(存储型 XSS 依赖前端)

  • 位置middleware/xss.js 仅清洗 req.body/req.query(输入);所有 controller 把 DB 字符串原样返回 JSON。
  • 影响:若前端用 v-html/innerHTML 渲染教材标题、教师姓名、班级名等字段,存储的恶意脚本会在其他管理员页执行(并触发 S2)。
  • 修复方案:后端对返回前端的“展示型”字段统一做输出转义,或明确约束前端只用文本节点渲染(禁止 v-html)。
  • 可能影响的其他调用:若前端确有 v-html,加后端转义可能双重转义——需先盘点前端渲染方式,前后端协同。

二、业务功能与数据逻辑审查

重点结论:核心学期计算与合班去重主体正确

  • 学期计算唯一实现semester.service.jsparseSemester / calcClassSemester / getActiveClassFilter 为全局唯一副本(grep 确认无内联 (grade-1)*2+semesterIndex 残留)。calcClassSemester 对缺 duration_years/越界正确返回 null 不抛错。
  • 在读判定下界正确getActiveClassFiltersemester.service.js:235 gte: startYear-d+1)数学正确,最后学期(grade==duration)计入、毕业后排除(P1-5 修复正确)。
  • 方案矩阵窗口无 off-by-one:全仓用 start <= current <= end 双闭区间,与建记录区间一致,current===start/end 均包含。
  • 合班无环/无死循环:实际模型是扁平 combination_id 分组外键(非关系图),不存在“A 合 B、B 合 A”有向环;dedupeTeachingUnitsteaching-statistics.service.js:27-51)按 (combination_id??class_id, course_id, teacher_id) 去重,同一合班课只计 1 次——其本身逻辑正确
  • 删除级联安全deleteClass/deleteTeacher/deleteCourse 均先 count 软拦截(有安排则拒),schema 对 teaching_assignments 的 teacher/class/course 均为 onDelete: Restrict 兜底 → 无孤儿安排记录。
  • 开课不重不漏teaching_assignments 唯一约束 [class_id, course_id, semester] → 同班同课同学期不重复。

业务发现明细

B1【High】合班班级导致教师周课时被重复计数

  • 位置services/arrange/queries.js:173-196getTeachersForCourseworkloadMapgroupBy(teacher_id, _sum:{weekly_hours}) 直接对全部 teaching_assignments 行求和,未去重)。
  • 现象:合班后每个成员班在 teaching_assignments 各存一行且 weekly_hours 相同。该 groupBy 把同一节合班课计 N 次 → 教师 totalWeeklyHours 虚高 N 倍。
  • 影响(三处消费者均被波及)
    1. 排课 UI 教师卡片展示的“已排课时”虚高;
    2. teaching-arrange.controller.js:294-305 的超限告警误报(教师实际未满却被判已满);
    3. 进入自动排课容量判断(effectiveTotal),导致合理排课被错误拒绝。
  • 矛盾点(强佐证需修)dashboardteaching-arrange 列表、data-export 的课时统计dedupeTeachingUnits 去重,唯独 getTeachersForCourse 未用 → 排课界面与统计/导出的数字互相打架
  • 修复方案
    js
    // 在 getTeachersForCourse 中,用“先取行再按合班去重后聚合”替代裸 groupBy
    const rawAssignments = await prisma.teaching_assignments.findMany({
      where: { semester: semesterStr },
      select: { teacher_id: true, course_id: true, weekly_hours: true,
                class: { select: { combination_id: true } } },
    });
    const deduped = dedupeTeachingUnits(rawAssignments); // 返回逻辑单元
    const workloadMap = new Map();
    for (const u of deduped) {
      workloadMap.set(u.representative.teacher_id,
        (workloadMap.get(u.representative.teacher_id) || 0) + u.weeklyHours);
    }
    同理修正 courseAssignmentMap(按 combination_id??class_id 去重后再对当前课程求和)。自动排课容量计算处(auto-arrange.js / tabu-search.jseffectiveTotal)须复用同一去重口径。
  • 可能影响的其他调用
    • 正向:排课 UI 教师卡片、超限告警、自动排课容量将与 dashboard/导出统计一致(修复了数值打架)。
    • 行为变化:合班场景下教师“已排课时”下降,超限告警阈值更宽松,自动排课可排更多——属预期修正
    • 风险点:须确认 auto-arrange.jseffectiveTotal 确实读取此处返回的 totalWeeklyHours,避免两端口径仍不一致;建议补一条“合班教师课时去重”回归测试。

B2【Medium】按年级筛选越界 over-inclusion

  • 位置services/arrange/queries.js:71-79(grade 过滤保留 o.enrollment_year?.gte <= enrollmentYear 分支)+ 循环 :106-150(仅 calcClassSemesterif(!calc) continue未复核 calc.grade)。
  • 现象:过滤只保留“学制足够大”的分支,丢失上界;循环也未按 calc.grade === gradeNum 复核。例:当前学期 2025、筛 grade=2;学制 3、入学年 2025(实际 grade=1)的班,其 enrollment_year=2025 落在学制3 分支 [2023,2025] 内被保留 → 错误纳入 grade=2。
  • 影响:按年级筛选的排课清单、以及 data-export.controller.js:825 传入 grade 的导出,会多包含非目标年级的班级。
  • 修复方案(最小改动、最稳):在循环内补精确判定:
    js
    const calc = calcClassSemester(cls, semesterInfo);
    if (!calc) continue;
    if (filters.grade && calc.grade !== Number(filters.grade)) continue; // 新增
    (查询层 :71-79 的范围过滤可保留作 DB 层裁剪,也可改为精确 gte === enrollmentYear;但循环复核是正确性兜底,二者不冲突。)
  • 可能影响的其他调用:仅影响“按年级筛选”结果集(排课查询 + 含 grade 的导出),结果变少且变对;不影响无 grade 筛选的其它路径。需确认 queries.test.js 未断言错误行为(当前仅测不崩溃)。

B3【Medium→Low】方案窗口收缩/删除后 teaching_assignments 成孤儿

  • 位置controllers/plan/plan-matrix.controller.js:178-185(收缩 end_semesterplan_course_semesters)、:319-352(删 plan_courses 级联);teaching_assignments 仅引用 class_id+course_id+semester不引用 plan_courses
  • 现象:方案课程窗口被收缩/删除后,已落库的 teaching_assignments(同 class+course+semester)仍在。getClassesWithCourse 因找不到 semRecord 不再纳入该班,但排课列表/导出仍读出这些“方案已不再开课”的安排 → 两清单不一致。
  • 修复方案(推荐“提示+可选清理”,勿静默硬删)
    • updatePlanCourse/deletePlanCourse 中,计算“不再落在任一匹配方案 [start,end] 内的 class+course+semester”,在响应里返回受影响的安排清单并提示;
    • 提供独立“清理悬空安排”接口(或带 cleanup:true 参数),由用户确认后删除,避免意外数据丢失。
  • 可能影响的其他调用:仅当启用清理参数才改数据;默认仅增加响应提示字段,向后兼容。

B4【Low】duration_years=null 班级被静默排除,与 status=active 口径不一致

  • 位置semester.service.js:217-228durationValuesfilter(d=>d!=null),故 getActiveClassFilter 的 OR 不含 null 分支)+ class.controller.js:108-116calculateClassStatus 在缺字段时兜底返回 active)。
  • 现象:同一班级在“全部班级”列表 status 显示 active,却不会出现在“按在读筛选/排课矩阵”中。
  • 修复方案:API 层强制 duration_years 必填(创建已有校验;更新 validateClassUpdate 建议加 nullable:false 或做历史数据回填),使 getActiveClassFilter 不会遇到 null 分支。
  • 可能影响的其他调用:仅影响历史脏数据;新数据不会再有 null。

B5【Low】weeks_count 双缺失 → totalHours=NaN

  • 位置services/arrange/queries.js:128 weeksCount = semRecord.weeks_count ?? pc.weeks_per_semester; + :147 totalHours: weeklyHours * weeksCount
  • 现象weeklyHours>0weeksCount=nullweeklyHours*null=NaN。DB 有默认 18 且写入有兜底,触发概率低但缺显式兜底。
  • 修复方案const weeksCount = semRecord.weeks_count ?? pc.weeks_per_semester ?? 18;
  • 可能影响的其他调用:无;消除潜在 NaN。

B6【Low/Info】deleteCourse 注释与 schema 不符(功能安全)

  • 位置course.controller.js:130 注释写 onDelete:Cascade,但 schema.prisma:322 实为 onDelete:Restrict
  • 现象:功能上无孤儿风险(:138 已 count 软拦截 + schema Restrict 兜底),仅注释误导。
  • 修复方案:更正注释为 Restrict。无功能变化。

三、前后端逻辑联系与调用检查

总体结论

未发现 Critical/High 级阻断性联调缺陷。 核心链路(认证、CSRF、命名转换、端点映射、响应处理)均已正确对接。前端为 Vue 3 + Vite(非 React),axios 实例 baseURL:'/api'withCredentials:true

已确认正确的关键项

  1. CSRF 登录闭环client/src/utils/request.js:18-30(拦截器 POST/PUT/PATCH/DELETE 从 Cookie 读 XSRF-TOKEN 写入 X-CSRF-Token 头)+ client/src/api/auth.js:8fetchCsrfToken)+ client/src/stores/auth.js:61-63(登录前先取 CSRF)+ 后端 GET /auth/csrf-tokenauth.routes.js:122-131,GET 豁免 CSRF)。首次登录不会 403。
  2. 端点映射全一致:逐项核对 auth/dashboard/class/plan/teaching-arrange/teacher/CRUD/import/export/settings/audit/query 的 method+path+body,无 404/405/字段不匹配(导入 4 处 el-upload 均带 X-CSRF-Token 头)。
  3. 命名一致性:前端 camelCase → 中间件转 snake;后端响应 snake → 驼峰。settings(currentSemester/tabuSearchEnabled/allowHistoricalEdit)、teaching-arrange、plan-matrix、用户状态、批量排序的字段名均对齐。
  4. 响应/错误处理:统一 {success,data,message};401 自动刷新+队列重放;403/404/422/429 分别处理;班级/教师 items/total、审计 logs/total/page/pageSize 读取均一致。

联调发现明细

F1【Low】班级批量更新 updates 误用 snake_case 键

  • 位置client/src/views/class/ClassList.vue:679-693updates.major_id/college_id/training_level_id/enrollment_year/duration_years/is_left_school)。
  • 现象:违反“前端全程 camelCase 由中间件转换”约定;但因这些键无大写,中间件不会二次破坏,后端按 snake 解析 → 功能正确
  • 修复方案:改为 majorId/collegeId/trainingLevelId/enrollmentYear/durationYears/isLeftSchool,与 api/types.tsClassInput 一致。
  • 可能影响的其他调用:无功能变化;仅维护性提升。

F2【Info】POST /auth/download-token 孤儿端点

  • 位置routes/auth.routes.js:257;前端 client/src 无任何调用(grep 确认)。
  • 修复方案:确认导出流程后删除该端点,或补前端调用(若采用 S4 的“资源绑定签名 URL”方案则直接替换)。
  • 可能影响的其他调用:删除仅减少攻击面;不影响现有导出(现有导出走 Authorization 头 + 查询令牌双路)。

F3【Info】PaginatedResponse 类型与审计响应不符

  • 位置client/src/api/types.js:23-26 声明 {items,total};审计实际返回 {logs,total,page,pageSize}(前端按 logs 读取,运行无误)。
  • 修复方案:为审计单独定义 AuditLogResponse { logs, total, page, pageSize }
  • 可能影响的其他调用:仅 TS 类型准确性,不影响运行。

四、修复方案汇总与影响分析(按优先级)

建议修复顺序

  1. S3 导出文件名编码(极低风险、改动极小、防 500/下载异常)——立即做。
  2. B5 weeks_count 兜底(极低风险,消 NaN)——立即做。
  3. B2 年级筛选精确复核(低风险,正确性)——立即做 + 回归测试。
  4. B1 合班课时去重(High,需配套测试)——重点做,注意联动。
  5. S1 限流 XFF 绕过(运维+代码纵深)——部署前做。
  6. S5 重置保留黑名单 / B3 悬空安排提示(低风险增强)——迭代做。
  7. S2 XSRF Cookie + S7 输出清洗(需方案改造,配合前端)——规划做。
  8. F1/F2/F3 清理类(低优先)——日常迭代。

各修复的“可能影响的其他调用”一览

修复行为变化影响的调用方风险
S3 文件名编码仅响应头更规范所有导出下载
B5 weeks 兜底消除 NaN课时统计/导出
B2 年级复核筛选结果变少且正确排课按年级查询、含 grade 的导出低(须改测试)
B1 课时去重合班教师“已排课时”下降、告警/容量更准、与 dashboard/导出对齐getTeachersForCourse 消费者:teaching-arrange 教师卡片、超限告警(:294)、自动排课 effectiveTotal(须确认 auto-arrange 复用同口径 + 回归测试)
S1 限流 XFF限流归因更准全局/登录/导出/导入限流低(同一代理后共享计数)
S5 重置保留黑名单重置后旧会话更快失效登出/改密后令牌语义无(增强)
B3 悬空安排默认仅加提示;清理需显式参数方案窗口变更、排课列表/导出低(默认兼容)
S2 XSRF + S7 输出清洗消灭 XSS 前置;CSRF 方案可能改造登录链路、所有写接口、前端渲染(须前后端协同,勿只加 httpOnly)
F1 命名约定仅维护性班级批量更新
F2/F3 清理删死接口/修类型导出鉴权、审计类型

跨修复联动提醒

  • B1 与 dashboard/导出统计:dashboard 与导出已经dedupeTeachingUnits 去重;B1 是让排课界面向它们看齐。修复后三者一致,但务必确认自动排课容量(effectiveTotal)读取的是 B1 修正后的值,否则会出现“界面数字一致、但排课决策仍用旧口径”的新分裂。
  • S2 切勿孤立修改:只给 XSRF-TOKENhttpOnly 会直接破坏登录(前端读不到 Cookie 无法设头)。必须与 CSRF 方案改造(S2 方案②)或前端 Token 存储改造(S7 根因)一起进行。
  • B3 默认勿静默删数据:方案窗口变更影响的 teaching_assignments 可能是用户期望保留的历史安排,推荐“提示 + 可选清理”,避免数据意外丢失引发二次工单。

五、附录:证据索引

  • 安全基线:app.jsmiddleware/{auth,csrf,xss,validation,error}.jsconfig/auth.config.jsservices/auth.service.js
  • 业务核心:services/semester.service.jsservices/plan.service.jsservices/teaching-arrange.service.jsservices/arrange/{queries,tabu-search,auto-arrange,lock}.jsservices/teaching-statistics.service.jsservices/class.service.jscontrollers/plan/plan-matrix.controller.js
  • 联调:client/src/utils/request.jsclient/src/api/auth.jsclient/src/stores/auth.jsclient/src/views/class/ClassList.vueclient/src/api/types.js