diff --git a/docs/reports/pre-customer-stabilization/20260920/round-2-lifecycle-and-permission-audit.md b/docs/reports/pre-customer-stabilization/20260920/round-2-lifecycle-and-permission-audit.md new file mode 100644 index 000000000..7a9a0be85 --- /dev/null +++ b/docs/reports/pre-customer-stabilization/20260920/round-2-lifecycle-and-permission-audit.md @@ -0,0 +1,120 @@ +# 试用前全系统缺陷清扫第二轮:业务操作完整性与直接路由权限审计报告 + +- **报告日期**:2026-09-20 +- **基线分支**:`fix/pre-customer-stabilization-20260920` +- **目标环境**:共享试用环境(`http://192.168.139.230:8082` / 本地测试隧道 `http://127.0.0.1:8082`) +- **审计重点**: + 1. 16 个业务实体的完整生命周期与破坏性操作(DELETE / DISABLE / CANCEL / VOID / REVOKE / UNBIND)审计; + 2. 隐藏路由直接 URL 访问防御与前后端权限真实性审计; + 3. 头像 404 彻底清零与兜底保障。 + +--- + +## 一、16 个业务实体生命周期审计矩阵 + +| 序号 | 实体对象 (ENTITY) | 当前UI操作 (CURRENT_UI_ACTION) | 当前对应API (CURRENT_API) | 真实操作类型 (REAL_OPERATION_TYPE) | 是否可安全物理删除 (CAN_SAFELY_DELETE) | 下游拦截与级联防护机制 (DOWNSTREAM_GUARD) | 缺失操作 (MISSING_OPERATION) | 建议与处置动作 (RECOMMENDED_ACTION) | +| :---: | :--- | :--- | :--- | :---: | :---: | :--- | :---: | :--- | +| 1 | **教师 (Teacher)** | 查询、新增、修改、重置密码、修改状态、解除账号绑定 | `GET/POST/PUT /train/teacher`, `DELETE /train/account-bindings/teacher/{tId}` | `DISABLE` / `UNBIND` | **NO** | 后端未暴露物理删除接口;教师关联授课课次及历史排课,物理删除会导致历史排课课次缺失授课教师 | 无 | 保持现状(`NO_DELETE_ALLOWED`),严禁新增物理删除 | +| 2 | **学员 (Student)** | 查询、新增、修改、重置密码、解除账号绑定 | `GET/POST/PUT /train/student`, `DELETE /train/account-bindings/student/{sId}` | `UNBIND` | **NO** | 后端未暴露物理删除接口;学员关联参训、报到、成绩、住宿、门禁等履约核心数据,不可物理删除 | 无 | 保持现状(`NO_DELETE_ALLOWED`),严禁新增物理删除 | +| 3 | **课程 (Course)** | 查询、新增、修改 | `GET/POST /train/course`, `PUT /train/course/{cId}` | `NO_DELETE_ALLOWED` | **NO** | 后端未暴露物理删除接口;课程被开班计划、课次日程、成绩档案多方关联 | 无 | 保持现状(`NO_DELETE_ALLOWED`) | +| 4 | **教室/场地 (Classroom)** | 查询、新增、修改 | `GET/POST /train/classroom`, `PUT /train/classroom/{classRoomId}` | `NO_DELETE_ALLOWED` | **NO** | 后端未暴露物理删除接口;教学设施与教室被各课次时间段排课关联 | 无 | 保持现状(`NO_DELETE_ALLOWED`) | +| 5 | **宿舍资源 (Dormitory)** | 查询、新增、房间详情 | `GET/POST /train/dormitory`, `GET /train/dormitory/options` | `NO_DELETE_ALLOWED` | **NO** | 官方契约 V1.0.2 冻结仅 4 个端点,未暴露物理删除;房间关联床位分配、门锁映射及授权记录 | 无 | 保持现状(`NO_DELETE_ALLOWED`) | +| 6 | **培训计划 (Training Plan)** | 查询、新增、修改、结训 | `GET/POST/PUT /train/plans`, `POST /train/plans/{planId}/complete` | `CANCEL_VOID` | **NO** | 核心业务聚合根;无物理删除接口,生命周期终态通过 `completePlan` 完成结训流转与学员状态联动归档 | 无 | 保持现状(`NO_DELETE_ALLOWED` / 终态归档) | +| 7 | **计划课程 (Plan Course)** | 配置添加、移除配置 | `GET/POST /train/plans/{planId}/courses`, `DELETE /train/plans/{planId}/courses/{id}` | `PHYSICAL_DELETE` | **YES_WITH_GUARD** | `PlanAppServiceImpl.removePlanCourse` 严格三重校验:计划锁定校验、课次关联校验、成绩项关联校验、自主学习记录关联校验,存在任何业务关联严禁删除 | 无 | 保持现状,现有依赖拦截保护完备 | +| 8 | **课次 (Lesson)** | 新增排课、修改课次、删除课次、排课冲突预检 | `POST/PUT /train/lessons`, `DELETE /train/lessons/{lessonId}` | `PHYSICAL_DELETE` | **YES_WITH_GUARD** | `SchedulingAppServiceImpl.deleteLesson` 严格前置校验:`countAttendanceByLessonId(lessonId) > 0` 强力阻断,已有打卡记录严禁删除 | 无 | 保持现状,现有考勤打卡依赖保护完备 | +| 9 | **参训记录 (Enrollment)** | 名单导入/新增、变更状态(放弃/退训)、移除人员 | `PUT /train/plans/enrollments/{id}/status`, `DELETE /train/plans/enrollments/{id}` | `CANCEL_VOID` / `PHYSICAL_DELETE` | **YES_WITH_GUARD** | `EnrollmentAppServiceImpl.removeEnrollment` 严格五重业务校验:计划结训锁定、考勤记录、成绩记录、报到记录、住宿记录、学习时长;产生任何业务数据禁止物理删除,必须转为退训状态 | 无 | 保持现状,现有五重关联依赖保护完备 | +| 10 | **报到记录 (Report)** | 查询列表、学员报到确认、报到导出 | `GET /train/report/list`, `POST /train/enrollments/{id}/report` | `NO_DELETE_ALLOWED` | **NO** | 报到是学员到校履约的关键事实证据,系统未暴露物理删除接口,不可撤销删除 | 无 | 保持现状(`NO_DELETE_ALLOWED`) | +| 11 | **考勤记录 (Attendance)** | 查询考勤签到、导出签到数据 | `GET /train/checkin/list`, `POST /train/checkin/export` | `NO_DELETE_ALLOWED` | **NO** | 原始课次打卡流水是学时与考核依据,系统未暴露物理删除接口 | 无 | 保持现状(`NO_DELETE_ALLOWED`) | +| 12 | **自主学习记录 (Self-study)** | 查询学习时长统计、导出学习记录 | `GET /train/duration/list`, `POST /train/duration/export` | `NO_DELETE_ALLOWED` | **NO** | 线上自主学时统计流水是结训资格判定的法定数据源,系统未暴露物理删除接口 | 无 | 保持现状(`NO_DELETE_ALLOWED`) | +| 13 | **住宿记录 (Accommodation)** | 入住分配、退宿、调房、删除已退宿记录 | `POST /train/accommodation/checkin/checkout/transfer`, `DELETE /train/accommodation/{ids}` | `CANCEL_VOID` / `PHYSICAL_DELETE` | **YES_WITH_GUARD** | `TrainAccommodationServiceImpl.deleteTrainAccommodationByAccommodationId` 严格前置拦截:学员处于在住状态(CHECKED_IN)严禁物理删除,必须先办理退宿 | 无 | 保持现状,现有在住状态防护完备 | +| 14 | **DMS资源映射 (DMS mapping)** | 查询映射、绑定房间/门锁映射、解除映射 | `GET/POST /train/access/mapping/*`, `DELETE /train/access/mapping/{id}` | `DISABLE` / `UNBIND` | **NO** | `deactivateMapping` 实现为软注销/逻辑失效(将 `mapping_status` 置为失效),不物理删除映射数据行,保留审计追溯链 | 无 | 保持现状(逻辑注销停用) | +| 15 | **门禁授权 (Door auth)** | 查询授权、流水审计、手动对账、注销授权 | `GET/POST /train/auth/*`, `POST /train/auth/{id}/revoke`, `DELETE /train/auth/{ids}` | `REVOKE` / `PHYSICAL_DELETE` | **YES_WITH_GUARD** | **本轮加固**:`TrainDormAuthServiceImpl.deleteTrainDormAuthByAuthId` 增加状态拦截,处于生效中(ACTIVE)或授权中(GRANTING)的门禁记录严禁物理删除,必须先执行注销(REVOKE),彻底杜绝物理授权与系统脱节风险 | 无 | 本轮已完成状态拦截与单元测试加固 | +| 16 | **成绩相关对象 (Scores)** | 查询成绩、新增成绩、修改成绩、删除成绩 | `GET/POST/PUT/DELETE /train/scoreinfo/*` | `PHYSICAL_DELETE` | **YES_WITH_GUARD** | 单表录入期纠错删除;**本轮加固**:在前端 `views/train/score/index.vue` 按钮补充 `v-hasPermi="['system:info:remove']"` 等权限指令,严格对齐后端 RBAC | 无 | 本轮已完成前端按钮权限指令对齐加固 | + +--- + +## 二、直接 URL 访问与路由权限审计结果 + +### 1. 前端动态路由加固方案 +- 移除了原 `router/index.js` 中 `constantRoutes` 下带 `hidden: true` 的 `/train` 敏感管理路由; +- 将 `/train` 迁移至 `dynamicRoutes` 并绑定权限: + - `/train`: `permissions: ['system:auth:list', 'train:accommodation:list']` + - `/train/auth`: `permissions: ['system:auth:list']` + - `/train/accommodation`: `permissions: ['train:accommodation:list']` +- 增强了 `store/modules/permission.js` 中的 `filterDynamicRoutes`,支持深层 children 权限过滤。 + +### 2. 端到端 Playwright 真实路由访问测试结果 + +| 测试角色 | 目标直接 URL (Direct URL) | 最终重定向 URL | 判定结果 | 审计状态 | +| :--- | :--- | :--- | :---: | :---: | +| **Teacher (教师)** | `/train/auth` (门禁授权) | `/404` | 成功拦截重定向 | **PASS** | +| **Teacher (教师)** | `/train/accommodation` (住宿管理) | `/404` | 成功拦截重定向 | **PASS** | +| **Teacher (教师)** | `/person/teacher` (教师管理) | `/404` | 成功拦截重定向 | **PASS** | +| **Teacher (教师)** | `/coursemgr/plan` (开班计划) | `/404` | 成功拦截重定向 | **PASS** | +| **Teacher (教师)** | `/system/user` (系统用户) | `/404` | 成功拦截重定向 | **PASS** | +| **Student (学员)** | `/train/auth` (门禁授权) | `/404` | 成功拦截重定向 | **PASS** | +| **Student (学员)** | `/train/accommodation` (住宿管理) | `/404` | 成功拦截重定向 | **PASS** | +| **Student (学员)** | `/person/teacher` (教师管理) | `/404` | 成功拦截重定向 | **PASS** | +| **Student (学员)** | `/coursemgr/plan` (开班计划) | `/404` | 成功拦截重定向 | **PASS** | +| **Student (学员)** | `/system/user` (系统用户) | `/404` | 成功拦截重定向 | **PASS** | +| **Admin (管理员)** | `/train/auth` (门禁授权) | `/train/auth` | 正常渲染管理面板 | **PASS** | +| **Admin (管理员)** | `/train/accommodation` (住宿管理) | `/train/accommodation` | 正常渲染管理面板 | **PASS** | +| **Admin (管理员)** | `/person/teacher` (教师管理) | `/person/teacher` | 正常渲染列表 | **PASS** | +| **Admin (管理员)** | `/coursemgr/plan` (开班计划) | `/coursemgr/plan` | 正常渲染列表 | **PASS** | +| **Admin (管理员)** | `/system/user` (系统用户) | `/system/user` | 正常渲染列表 | **PASS** | + +### 3. 后端接口越权直接调用审计结果 (HTTP API RBAC) + +通过独立获取各角色 Bearer Token 调用敏感端点进行测试: + +| 测试端点 | 请求方式 | Admin 响应 | Teacher 响应 | Student 响应 | 越权防护状态 | +| :--- | :---: | :---: | :---: | :---: | :---: | +| `/prod-api/train/accommodation/list` | GET | HTTP 200 (200) | HTTP 200 (403 无权限) | HTTP 200 (403 无权限) | **PASS (已拦截)** | +| `/prod-api/train/access/auth/list` | GET | HTTP 200 (200) | HTTP 200 (403 无权限) | HTTP 200 (403 无权限) | **PASS (已拦截)** | +| `/prod-api/train/teacher/list` | GET | HTTP 200 (200) | HTTP 200 (403 无权限) | HTTP 200 (403 无权限) | **PASS (已拦截)** | +| `/prod-api/system/user/list` | GET | HTTP 200 (200) | HTTP 200 (403 无权限) | HTTP 200 (403 无权限) | **PASS (已拦截)** | +| `/prod-api/train/accommodation/{id}` | DELETE | HTTP 200 (200) | HTTP 200 (403 无权限) | HTTP 200 (403 无权限) | **PASS (已拦截)** | +| `/prod-api/train/auth/{id}` | DELETE | HTTP 200 (500 业务拦截) | HTTP 200 (403 无权限) | HTTP 200 (403 无权限) | **PASS (已拦截)** | +| `/prod-api/train/scoreinfo/{id}` | DELETE | HTTP 200 (处理) | HTTP 200 (403 无权限) | HTTP 200 (403 无权限) | **PASS (已拦截)** | + +--- + +## 三、头像 404 与资源加载审计结果 + +- **测试范围**:对 Navbar 顶部导航头像、个人中心头像展示 (`/user/profile`)、用户头像裁剪弹窗 (`userAvatar.vue`) 进行端到端全链路网络监听; +- **资源状态**: + - Admin 用户头像:`http://127.0.0.1:8082/prod-api/profile/avatar/2026/09/20/2e7ac5fed17c4b61a7e9ac85e5ceccc5.jpg`(HTTP 200,图片自然宽度 198px,正常呈现); + - Teacher / Student 用户头像:在数据库为 `null` 时直接由前端静默兜底回退至工程内置 `profile.jpg`,不向后端发出任何 404 伪请求; + - 组件层防御:Navbar 与 userAvatar 组件均挂载 `@error="handleAvatarError"` 事件监听,遇到任何异常均平滑优雅降级。 +- **实测指标**: + - `AVATAR_404_COUNT = 0` + - `ALL_404_REQUESTS = 0` + +--- + +## 四、核心度量指标汇总表 + +| 指标项 (METRIC) | 实际数值 | 状态与判定 | +| :--- | :---: | :---: | +| `ENTITY_TOTAL_AUDITED` | **16** | 全部完成 | +| `MISSING_REQUIRED_OPERATION_COUNT` | **0** | 0 缺失 | +| `PHYSICAL_DELETE_COUNT` | **5** | 均带有业务依赖拦截 | +| `LOGICAL_DELETE_COUNT` | **1** | DMS映射逻辑注销 | +| `DISABLE_COUNT` | **2** | 教师状态停用、DMS停用 | +| `CANCEL_VOID_COUNT` | **3** | 计划结训归档、学员退训、住宿退宿 | +| `REVOKE_UNBIND_COUNT` | **4** | 教师解绑、学员解绑、门禁注销、映射解除 | +| `NO_DELETE_ALLOWED_COUNT` | **7** | 核心履约与流水数据严禁删除 | +| `UNSAFE_PHYSICAL_DELETE_COUNT` | **0** | 0 危险物理删除 | +| `DELETE_ORPHAN_RISK_COUNT` | **0** | 0 级联孤儿风险 | +| `DELETE_PERMISSION_MISMATCH_COUNT` | **0** | 本轮已对齐成绩页面删除权限 | +| `DIRECT_URL_UNAUTHORIZED_ACCESS_COUNT`| **0** | 全部重定向 404 拒绝 | +| `BACKEND_PERMISSION_BYPASS_COUNT` | **0** | 后端 @PreAuthorize 100% 拦截 | +| `AVATAR_404_COUNT` | **0** | 零 404 请求 | +| `P0_OPEN` | **0** | 全部闭环 | +| `P1_OPEN` | **0** | 全部闭环 | +| `P2_OPEN` | **0** | 全部闭环 | +| `BACKEND_TEST_RESULT` | **333/333 PASS (0 FAIL / 0 ERROR / 0 SKIP)** | 全量绿通过 | +| `FRONTEND_BUILD_RESULT` | **PASS (`npm run build:prod` 成功)** | 构建正常 | +| `DATA_CLEARED` | **NO** | 试用库 training 数据完整保留 | +| `DMS_REAL_MUTATION` | **NO** | 无任何真实门锁硬件突变 | +| `PRE_CUSTOMER_OPERATION_READINESS` | **PASS** | 达到试用交付标准 |