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 | 安全 | Medium | XSRF-TOKEN Cookie 非 HttpOnly,可被 XSS 读取 | routes/auth.routes.js:38-45,124-129 |
| B2 | 业务/筛选 | Medium | 按年级筛选越界 over-inclusion | services/arrange/queries.js:71-79,106-150 |
| S3 | 安全 | Low–Medium | 3 处导出文件名未 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 | 业务/学期 | Low | duration_years=null 班级被静默排除,与 status=active 展示口径不一致 | services/semester.service.js:217-228 vs class.controller.js:108-116 |
| B5 | 业务/课时统计 | Low | weeks_count 与 weeks_per_semester 同时缺失时 totalHours=NaN | services/arrange/queries.js:128,147 |
| B6 | 业务/数据联动 | Low/Info | deleteCourse 注释称 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 | 前端/死接口 | Info | POST /auth/download-token 孤儿端点,前端从未调用 | routes/auth.routes.js:257 |
| F3 | 前端/类型 | Info | PaginatedResponse 声明 items 但审计实际返回 logs | client/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 + roleMiddleware;user.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:33app.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)、全局
/api120 次/min、导入/导出限流全部失效 → 削弱抗爆破/抗爬/抗 DoS。usernameLimiter(按用户名计数)部分缓解,但全局与导出限流被绕过。 - 修复方案:
- 运维侧(必须):Nginx 用
proxy_set_header X-Forwarded-For $remote_addr;(覆盖而非追加),从源头清洗。 - 代码侧(纵深防御):为
apiLimiter等配置keyGenerator,仅信任来自可信代理的 IP(如req.ip在单跳代理下已准确,可加validate:false关掉 IPv6 警告)。若多跳代理,应取X-Forwarded-For中倒数第二跳而非最左。
- 运维侧(必须):Nginx 用
- 可能影响的其他调用:仅改变限流计数归因;将同一代理后多用户共享计数器(轻微)。无功能破坏。
S2【Medium】XSRF-TOKEN Cookie 非 HttpOnly
- 位置:
routes/auth.routes.js:38-45(setAuthCookies)、:124-129(csrf-token 路由)设置XSRF-TOKEN时未带httpOnly;middleware/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”前提下的附加风险。 - 修复方案(按性价比排序):
- 根因治理(推荐):确保 Bearer Token 不存 localStorage(改内存 + refresh 续期),并给所有返回前端的字符串字段做输出转义(见 S7),先消灭 XSS 这一前置条件。
- 方案升级(中长期):将 CSRF 双提交改为服务端会话绑定——登录时把 CSRF 密钥存入服务端会话/Redis,校验时不仅比头与 Cookie,还比服务端期望值;或彻底移除 CSRF 依赖,仅靠
SameSite=strictCookie + 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\n抛Invalid character in header content→ 该导出 500(受限 DoS);② 含空格/中文/特殊符号时部分浏览器下载文件名乱码。 - 修复方案:统一改为js三处同理(
const rawName = `教材使用_${textbook.title}.xlsx`; res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(rawName)}`);:767的semester为YYYY-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:256await tx.token_blacklist.deleteMany(); - 现象:
resetSystem(仅 super_admin)执行后,此前因登出/改密/重置入黑名单、本应 fail-close 失效的 access/refresh token 重新可用,直到 JWT 自然过期。 - 修复方案:重置时保留尚未过期的黑名单条目(只删
expires_at < now的),或重置后强制全量会话失效(版本号/全局踢下线)。 - 可能影响的其他调用:改变“重置后旧会话是否仍有效”的语义——属预期增强,无功能破坏。
S6【Low】自动排课同步 CPU 密集,缺规模/速率保护
- 位置:
services/arrange/tabu-search.js、auto-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.js的parseSemester/calcClassSemester/getActiveClassFilter为全局唯一副本(grep 确认无内联(grade-1)*2+semesterIndex残留)。calcClassSemester对缺duration_years/越界正确返回null不抛错。 - 在读判定下界正确:
getActiveClassFilter(semester.service.js:235gte: startYear-d+1)数学正确,最后学期(grade==duration)计入、毕业后排除(P1-5 修复正确)。 - 方案矩阵窗口无 off-by-one:全仓用
start <= current <= end双闭区间,与建记录区间一致,current===start/end均包含。 - 合班无环/无死循环:实际模型是扁平
combination_id分组外键(非关系图),不存在“A 合 B、B 合 A”有向环;dedupeTeachingUnits(teaching-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-196(getTeachersForCourse中workloadMap用groupBy(teacher_id, _sum:{weekly_hours})直接对全部teaching_assignments行求和,未去重)。 - 现象:合班后每个成员班在
teaching_assignments各存一行且weekly_hours相同。该groupBy把同一节合班课计 N 次 → 教师totalWeeklyHours虚高 N 倍。 - 影响(三处消费者均被波及):
- 排课 UI 教师卡片展示的“已排课时”虚高;
teaching-arrange.controller.js:294-305的超限告警误报(教师实际未满却被判已满);- 进入自动排课容量判断(
effectiveTotal),导致合理排课被错误拒绝。
- 矛盾点(强佐证需修):
dashboard、teaching-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.js的effectiveTotal)须复用同一去重口径。 - 可能影响的其他调用:
- 正向:排课 UI 教师卡片、超限告警、自动排课容量将与 dashboard/导出统计一致(修复了数值打架)。
- 行为变化:合班场景下教师“已排课时”下降,超限告警阈值更宽松,自动排课可排更多——属预期修正。
- 风险点:须确认
auto-arrange.js的effectiveTotal确实读取此处返回的totalWeeklyHours,避免两端口径仍不一致;建议补一条“合班教师课时去重”回归测试。
B2【Medium】按年级筛选越界 over-inclusion
- 位置:
services/arrange/queries.js:71-79(grade 过滤保留o.enrollment_year?.gte <= enrollmentYear分支)+ 循环:106-150(仅calcClassSemester后if(!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_semester删plan_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-228(durationValues已filter(d=>d!=null),故getActiveClassFilter的 OR 不含 null 分支)+class.controller.js:108-116(calculateClassStatus在缺字段时兜底返回active)。 - 现象:同一班级在“全部班级”列表 status 显示
active,却不会出现在“按在读筛选/排课矩阵”中。 - 修复方案:API 层强制
duration_years必填(创建已有校验;更新validateClassUpdate建议加nullable:false或做历史数据回填),使getActiveClassFilter不会遇到 null 分支。 - 可能影响的其他调用:仅影响历史脏数据;新数据不会再有 null。
B5【Low】weeks_count 双缺失 → totalHours=NaN
- 位置:
services/arrange/queries.js:128weeksCount = semRecord.weeks_count ?? pc.weeks_per_semester;+:147totalHours: weeklyHours * weeksCount。 - 现象:
weeklyHours>0且weeksCount=null时weeklyHours*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。
已确认正确的关键项
- CSRF 登录闭环:
client/src/utils/request.js:18-30(拦截器 POST/PUT/PATCH/DELETE 从 Cookie 读XSRF-TOKEN写入X-CSRF-Token头)+client/src/api/auth.js:8(fetchCsrfToken)+client/src/stores/auth.js:61-63(登录前先取 CSRF)+ 后端GET /auth/csrf-token(auth.routes.js:122-131,GET 豁免 CSRF)。首次登录不会 403。 - 端点映射全一致:逐项核对 auth/dashboard/class/plan/teaching-arrange/teacher/CRUD/import/export/settings/audit/query 的 method+path+body,无 404/405/字段不匹配(导入 4 处
el-upload均带X-CSRF-Token头)。 - 命名一致性:前端 camelCase → 中间件转 snake;后端响应 snake → 驼峰。settings(
currentSemester/tabuSearchEnabled/allowHistoricalEdit)、teaching-arrange、plan-matrix、用户状态、批量排序的字段名均对齐。 - 响应/错误处理:统一
{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-693(updates.major_id/college_id/training_level_id/enrollment_year/duration_years/is_left_school)。 - 现象:违反“前端全程 camelCase 由中间件转换”约定;但因这些键无大写,中间件不会二次破坏,后端按 snake 解析 → 功能正确。
- 修复方案:改为
majorId/collegeId/trainingLevelId/enrollmentYear/durationYears/isLeftSchool,与api/types.ts的ClassInput一致。 - 可能影响的其他调用:无功能变化;仅维护性提升。
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 类型准确性,不影响运行。
四、修复方案汇总与影响分析(按优先级)
建议修复顺序
- S3 导出文件名编码(极低风险、改动极小、防 500/下载异常)——立即做。
- B5
weeks_count兜底(极低风险,消 NaN)——立即做。 - B2 年级筛选精确复核(低风险,正确性)——立即做 + 回归测试。
- B1 合班课时去重(High,需配套测试)——重点做,注意联动。
- S1 限流 XFF 绕过(运维+代码纵深)——部署前做。
- S5 重置保留黑名单 / B3 悬空安排提示(低风险增强)——迭代做。
- S2 XSRF Cookie + S7 输出清洗(需方案改造,配合前端)——规划做。
- 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-TOKEN加httpOnly会直接破坏登录(前端读不到 Cookie 无法设头)。必须与 CSRF 方案改造(S2 方案②)或前端 Token 存储改造(S7 根因)一起进行。 - B3 默认勿静默删数据:方案窗口变更影响的
teaching_assignments可能是用户期望保留的历史安排,推荐“提示 + 可选清理”,避免数据意外丢失引发二次工单。
五、附录:证据索引
- 安全基线:
app.js、middleware/{auth,csrf,xss,validation,error}.js、config/auth.config.js、services/auth.service.js - 业务核心:
services/semester.service.js、services/plan.service.js、services/teaching-arrange.service.js、services/arrange/{queries,tabu-search,auto-arrange,lock}.js、services/teaching-statistics.service.js、services/class.service.js、controllers/plan/plan-matrix.controller.js - 联调:
client/src/utils/request.js、client/src/api/auth.js、client/src/stores/auth.js、client/src/views/class/ClassList.vue、client/src/api/types.js