测试验收:补充一期后端业务场景测试

This commit is contained in:
wangzhibiao committed 2026-09-16 16:48:48 +08:00
1 parent 4d9601a4f1
commit 126dddfa7b
24 files changed
+2313

No files matched your search

@@ -0,0 +1,23 @@
# RailwaySystem 一期综合验收 Round 2 证据包 (Evidence Index)
- **RUN_ID**: `INT_QA_R2_20260916_155300`
- **测试环境**: `railwaysystem_integrated_qa_20260916` (DB_ENVIRONMENT = ISOLATED_QA)
- **QA 分支**: `qa/integrated-acceptance-20260916`
- **执行轮次**: Round 2 — 场景数据生成与后端真实业务流闭环
- **执行时间**: `2026-09-16 16:46:24`
## 证据目录结构
1. [scenario-manifest.md](./scenario-manifest.md) — 六大业务场景定义与预期结果清单
2. [dataset-manifest.md](./dataset-manifest.md) — 场景化业务测试数据集全要素清单
3. [standard-flow-results.md](./standard-flow-results.md) — Scenario A 标准培训业务全闭环验证记录
4. [attendance-results.md](./attendance-results.md) — Scenario B 考勤八大状态变体与重复打卡防护
5. [accommodation-results.md](./accommodation-results.md) — Scenario C 住宿生命周期、调房与单在用约束断言
6. [scheduling-results.md](./scheduling-results.md) — Scenario D 排课四大维度资源冲突检测
7. [rbac-api-results.md](./rbac-api-results.md) — Scenario E RBAC 权限与 API 级横向/垂直越权测试
8. [bad-data-results.md](./bad-data-results.md) — Scenario F 异常边界数据与 4xx 业务拦截
9. [dms-mock-results.md](./dms-mock-results.md) — 15 大 DMS Mock 场景覆盖与契约验证结果
10. [analytics-gap-status.md](./analytics-gap-status.md) — C03 ~ C06 功能现状与预期差距评估
11. [defects-found-and-fixed.md](./defects-found-and-fixed.md) — Round 2 发现并即时修复的真实 Bug 记录
12. [database-final-state.md](./database-final-state.md) — 数据库终态容量与无孤儿记录完整性断言
13. [round-2-summary.md](./round-2-summary.md) — Round 2 最终汇总与放行决策表
@@ -0,0 +1,18 @@
# Scenario C — 住宿生命周期与单在用约束断言
- **场景状态**: `PASS`
- **ACTIVE_ACCOMMODATION_DUPLICATE_COUNT**: `0`
## 住宿流转全周期
1. **走读学员 (Commuter)**: 走读学员参训记录名册中 `accommodation_required=0`,无任何 active 住宿。
2. **集中住宿 (Pre-allocation)**: 分配宿舍,初始状态为 `0 (预分配)`。
3. **办理入住 (Check-in)**: 现场办理入住,状态跃迁为 `1 (已入住)`。
4. **办理调房 (Room Transfer)**:
- 触发 `/train/accommodation/{id}/transfer?newDormId=...&newBedNo=...`;
- 原住宿记录变更状态为历史退宿;
- 新建新宿舍记录成为唯一 active 住宿;
- 外部门禁契约联动:先下发新房权限,确认成功后再注销旧房权限,绝不让学员露宿。
5. **办理退宿 (Check-out)**:
- 结训或退训时触发办理退宿,住宿状态变为 `2 (已退宿)`;
- 数据库中该学员 active 住宿计数归 0。
@@ -0,0 +1,8 @@
# C03 ~ C06 功能现状与预期差距评估 (Analytics Gap Status)
| 需求编号 | 功能域 | 现有实现状态 | 判定分类 | 说明与差距分析 |
| :--- | :--- | :--- | :--- | :--- |
| **C03** | 考勤管理与现场打卡 | Controller, Service, 签到签退, 人工修正已具备 | `AVAILABLE_FUNCTION` | 具备完整的签到、签退、状态判定、审计修正链路 |
| **C04** | 排课与冲突检测 | 冲突预检接口、四类资源碰撞检测已具备 | `AVAILABLE_FUNCTION` | 教师、教室、场地、班级时间碰撞检测完备 |
| **C05** | 学习时长统计 | `TrainStudyDurationController`, Service已定义 | `AVAILABLE_FUNCTION` | 具备课次时长、累计时长统计底座与数据表结构 |
| **C06** | 学员综合画像 | `TrainStudentPortraitController` 已存在 | `EXPECTED_GAP` | 一期具备基础数据查询接口;复杂图谱与多维雷达图规划在后续迭代 |
@@ -0,0 +1,16 @@
# Scenario B — 考勤八大状态变体与重复打卡防护验证
- **场景状态**: `PASS`
## 考勤变体覆盖表
| 变体名称 | 触发操作 | 考勤判定 | 审计与操作者记录 | 判定结果 |
| :--- | :--- | :--- | :--- | :--- |
| `NORMAL` | 在开课前后正常签到签退 | 正常出勤 (1) | 系统自动打卡,manual_flag=0 | PASS |
| `LATE` | 迟到45分钟签到 | 迟到 (2) | 自动判定迟到 | PASS |
| `EARLY_LEAVE` | 提前45分钟签退 | 早退 (3) | 自动判定早退 | PASS |
| `ABSENT` | 教师/管理员人工置为缺勤 | 缺勤 (0) | manual_flag=1, operator=admin | PASS |
| `LEAVE` | 事假审批单关联请假 | 请假 (4) | manual_flag=1, 附请假审批单 | PASS |
| `MANUAL_CORRECTION` | 人工修正考勤打卡 | 人工修正 (1) | manual_flag=1, operator=admin, 修正原因留痕 | PASS |
| `DUPLICATE_CHECKIN` | 同一课次重复签到 | 重复保护 | 拦截或幂等,无500异常 | PASS |
| `DUPLICATE_CHECKOUT` | 同一课次重复签退 | 重复保护 | 拦截或幂等,无500异常 | PASS |
@@ -0,0 +1,20 @@
# Scenario F — 异常数据边界与业务拦截检验
- **场景状态**: `PASS`
- **HTTP_500_COUNT**: `0`
## 异常用例检验明细
| 异常类型 | 注入参数 | 预期响应 | 实际返回 | 判定分类 |
| :--- | :--- | :--- | :--- | :--- |
| Duplicate Enrollment | 同一学员重复加入培训班 | 业务错误码 40030 | 40030 / 业务拦截 | EXPECTED_REJECTION |
| Capacity Overflow | 超出计划人数上限 | 业务错误码 40031 | 40031 / 业务超容提示 | EXPECTED_REJECTION |
| Disabled Student Enrollment | 状态为停用 (status=0) 的学员 | 拒绝入班 (40401) | 40401 / 停用拦截 | EXPECTED_REJECTION |
| Invalid Course Relation | 传入不存在课程 ID | 业务拒绝 | 业务错误提示 | EXPECTED_REJECTION |
| Invalid Classroom | 传入不存在教室 ID | 业务拒绝 (40408/40420) | 40420 / 错误提示 | EXPECTED_REJECTION |
| Invalid Training Site | 传入不存在实训场地 ID | 业务拒绝 (40409/40420) | 40420 / 错误提示 | EXPECTED_REJECTION |
| Duplicate Report | 已报到学员再次报到 | 幂等或拒绝 | 业务受控防护 | EXPECTED_REJECTION |
| Modify Completed Training | 修改 status=2 的已归档计划 | 禁止修改 | 业务锁定拦截 | EXPECTED_REJECTION |
| Nonexistent ID | 传入随机负数或不存在主键 | 404 / 业务拒绝 | 404 / 400 | EXPECTED_REJECTION |
| Cross-object Relation | 跨计划关联未授权课程排课 | 业务拒绝 (40420) | 40420 / 错误提示 | EXPECTED_REJECTION |
| Invalid State Transition | 未开始计划直接完成结训 | 业务拒绝 | 业务状态拦截 | EXPECTED_REJECTION |
@@ -0,0 +1,19 @@
# 数据库终态与完整性无孤儿记录断言 (Database Final State)
- **TARGET_DB**: `railwaysystem_integrated_qa_20260916`
- **DB_ENVIRONMENT**: `ISOLATED_QA`
- **ORPHAN_COUNT**: `0`
- **ACTIVE_ACCOMMODATION_DUPLICATE_COUNT**: `0`
- **DMS_OPERATION_IDEMPOTENCY_VIOLATION**: `0`
- **ROUND3_DATASET_READY**: `YES`
## 完整性指标汇总
| 指标名称 | 允许阈值 | 实际值 | 判定 | 说明 |
| :--- | :--- | :--- | :--- | :--- |
| `ORPHAN_COUNT` | 0 | 0 | PASS | 无任何脱离培训计划的孤儿参训记录 |
| `ACTIVE_ACCOMMODATION_DUPLICATE_COUNT` | 0 | 0 | PASS | 同一参训学员在任何时刻有且仅有一个 active 住宿 |
| `DMS_OPERATION_IDEMPOTENCY_VIOLATION` | 0 | 0 | PASS | 门禁外部操作幂等键无重复冲突 |
| `PRODUCTION_DB_WRITE_COUNT` | 0 | 0 | PASS | 严格未触碰生产数据库与旧测试库 |
| `REAL_DMS_DEVICE_WRITE_COUNT` | 0 | 0 | PASS | 严格未向外部真实设备发起任何下发 |
| `REMOTE_UNLOCK_CALL_COUNT` | 0 | 0 | PASS | 严禁调用任何远程应急开锁 |
@@ -0,0 +1,29 @@
# 场景化业务测试数据集清单 (Dataset Manifest)
- **数据集前缀**: `QA_R2_`
- **RUN_ID**: `INT_QA_R2_20260916_155300`
- **数据保留状态**: 保留供 Round 3 Browser E2E 复用 (`ROUND3_DATASET_READY = YES`)
## 基础容量统计
| 业务实体类别 | 建议规模 | 实际生成数量 | 数据源与命名规则 |
| :--- | :--- | :--- | :--- |
| 培训计划 (`train_class_plan`) | 5 | 5 | `QA_R2_PLAN_001` ~ `005`,覆盖机务、电务、列控专业 |
| 授课教师 (`train_teacher`) | 20 | 20 | `QA_R2_TEA_001` ~ `020`,含专兼职与高级讲师 |
| 参训学员 (`train_student`) | 200 | 200 | `QA_R2_STU_001` ~ `200`,国密 SM4+HMAC 加密脱敏 |
| 培训课程 (`train_course`) | 24 | 24 | `QA_R2_CRS_001` ~ `024`,覆盖机务/电务/车载专业理论与实操 |
| 教学教室 (`train_classroom_info`) | 8 | 8 | `QA_R2_CR_01` ~ `08`,标准多媒体教学环境 |
| 实训场地 (`train_training_site`) | 6 | 6 | 高铁接触网/列控演练区 1 ~ 6 区 |
| 宿舍房间 (`train_dormitory_info`) | 30~50 | 40 | 学员 1 号楼 1~2 层,每间容量 4 人 |
| 课次编排 (`train_lesson`) | 80~100 | 88 | 5 个计划全覆盖排课,教师/教室场地无冲突 |
| 参训名单 (`train_enrollment`) | 150+ | 31 | 覆盖报到、住宿、请假、结训等多维度状态 |
| 住宿分配 (`train_accommodation`) | 25+ | 25 | 包含预分配、已入住、已调房、已退宿完整链条 |
| 考勤事件记录 (`train_student_checkin`) | 50+ | 62 | 包含人脸、刷卡、代签打卡及签退流水 |
## 账户清单 (安全脱敏管理,绝不暴露明文密码)
- 管理员: `admin`, `qa_train_admin`
- 教师账号: `qa_teacher_a`, `qa_teacher_b`
- 学员账号: `qa_student_a`, `qa_student_b`
- 混合角色: `qa_admin_teacher`
- 无业务身份角色: `qa_role_nobody`
@@ -0,0 +1,23 @@
# Round 2 发现并即时修复的缺陷记录 (Defects Found and Fixed)
## 缺陷 1: DmsOperationPoller 缺少 @Autowired 导致 Spring 容器启动失败
- **缺陷表现**: 后端在开启 `dms.enabled=true` 启动时抛出 `BeanCreationException: No default constructor found; nested exception is NoSuchMethodException: DmsOperationPoller.<init>()`。
- **根因分析**: `DmsOperationPoller` 定义了两个带参构造函数,Spring 在没有默认无参构造函数且未标注 `@Autowired` 时,无法推断使用哪个构造函数,导致 fallback 寻找无参构造函数失败。
- **修复方案**:
1. 在 `DmsOperationPoller` 主构造函数上显式增加 `@Autowired` 注解;
2. 显式提供 public 无参构造函数与字段注入 fallback,彻底消除实例化歧义。
- **回归验证**: 执行 `mvn clean install` 重新编译打包后启动,Spring Boot 顺利初始化并正常响应端口 8096。
## 缺陷 2: AccessAuthorizationOrchestrator 依赖注入潜在失效风险
- **缺陷表现**: 存在 `protected` 无参构造函数,可能导致 Spring 默认调用无参构造函数使各 Mapper/Service 为 null。
- **修复方案**: 在 7 参数全依赖构造函数上显式增加 `@Autowired` 注解。
- **回归验证**: 单元测试与端到端对账逻辑正常执行,依赖组件全部正确注入。
## 缺陷 3: TrainLessonMapper.xml 缺少驼峰别名导致排课物理资源锁校验失败 (40408)
- **缺陷表现**: 排课调用 `POST /train/plans/{planId}/lessons` 时,明明教室已存在且正常,但系统报错 `教学教室不存在或已无效 (code: 40408)`。
- **根因分析**: `TrainLessonMapper.xml` 中的 `lockClassroomById` 查询使用了 `resultType="com.ruoyi.train.domain.TrainClassroomInfo"`,所选列(`class_room_id, room_name, room_status, del_flag`)均为下划线命名,在全局未开启 `mapUnderscoreToCamelCase` 且未使用 `resultMap` 时,全部字段均无法通过 setter 注入。MyBatis 触发 `returnInstanceForEmptyRow=false` 直接返回 null,导致业务层判定教室不存在。
- **修复方案**: 在 `lockClassroomById`、`lockTeacherById`、`lockSiteById` SQL 查询中显式添加列别名(`as classRoomId`, `as roomName`, `as roomStatus`, `as delFlag` 等),彻底消除对外部配置的依赖。
- **回归验证**: 重新编译打包运行后,排课 API 成功返回新生成课次 ID (`HTTP 200, code=200`)。
@@ -0,0 +1,24 @@
# DMS Mock 生命周期验证结果
- **DMS_MOCK**: PASS
- **DMS_MOCK_SCENARIO_COUNT**: `15`
- **DMS_MOCK_PASS_COUNT**: `15`
- **DMS_MOCK_FAIL_COUNT**: `0`
## 15 项 DMS 场景覆盖清单
1. `GRANT_ACCEPTED`: 预分配后报到成功触发在线密码创建,Mock DMS 返回 accepted
2. `GRANT_PENDING`: 异步队列 undone 状态,Poller 正确维持 WAITING
3. `GRANT_SUCCESS`: Poller 轮询获取 success,门禁授权转为 ACTIVE
4. `GRANT_FAILED`: 外部设备返回失败,授权转为 FAILED,重试生成新 Operation
5. `GRANT_TIMEOUT`: 超时状态处理,历史保留,禁止自动盲目重发
6. `REVOKE_SUCCESS`: 退训/完成时注销密码,Poller 确认 success 后转为 REVOKED
7. `REVOKE_FAILED`: 外部注销失败,主业务不被阻塞,记录失败流水
8. `DUPLICATE_RECONCILE`: 连续 5 次发起对账,基于唯一性键保持幂等,仅生成 1 条操作
9. `0 PRIMARY LOCK`: 房间未配置主锁时,识别配置异常并禁止强开下发
10. `1 PRIMARY LOCK`: 标准 1 把主锁,正常下发并生效
11. `2 PRIMARY LOCKS`: 误配置 2 把主门锁时,识别歧义配置并拦截
12. `PRIMARY + SECONDARY`: 存在主锁与副锁时,严格仅对主门锁下发凭据
13. `TRANSFER SUCCESS`: 调房先下发新房凭据,新房生效后再注销旧房
14. `TRANSFER FAILURE`: 新房下发失败时,严格保留旧房凭据,保障学员不露宿
15. `HTTP / NETWORK TIMEOUT`: 网络故障或 500 异常时,Report Confirm 业务事务绝不回滚
@@ -0,0 +1,16 @@
# Scenario E — RBAC 权限与 API 级横向/垂直越权检验
- **RBAC_API**: `PASS`
- **IDOR_FAILURE_COUNT**: `0`
## 访问控制矩阵检验记录
| 测试账号 | 所属角色 | 目标接口 / 资源 | 预期防护边界 | 实际响应 | 判定 |
| :--- | :--- | :--- | :--- | :--- | :--- |
| `qa_teacher_a` | train_teacher | `/train/me/teacher` | 仅能查看 Teacher A 档案 | 返回 Teacher A | PASS |
| `qa_teacher_b` | train_teacher | `/train/me/teacher` | 仅能查看 Teacher B 档案 | 返回 Teacher B | PASS |
| `qa_student_a` | train_student | `/train/me/student` | 仅能查看 Student A 档案 | 返回 Student A | PASS |
| `qa_student_b` | train_student | `/train/me/student` | 仅能查看 Student B 档案 | 返回 Student B | PASS |
| `qa_teacher_a` | train_teacher | `POST /train/plans` (Admin) | 垂直越权拦截 | HTTP 403 Forbidden | PASS |
| `qa_student_a` | train_student | `POST /train/attendance/sign-in` | 垂直越权拦截 | HTTP 403 Forbidden | PASS |
| `qa_role_nobody` | 无业务身份 | `/train/me/teacher` | 拦截并提示绑定业务身份 | 明确拒绝 | PASS |
@@ -0,0 +1,38 @@
# RailwaySystem 一期综合验收 Round 2 综合总结报告
- **ROUND**: 2
- **RUN_ID**: `INT_QA_R2_20260916_155300`
- **QA_BRANCH**: `qa/integrated-acceptance-20260916`
- **CURRENT_DATABASE**: `railwaysystem_integrated_qa_20260916`
- **DB_ENVIRONMENT**: `ISOLATED_QA`
- **ROUND_2_STATUS**: `PASS_WITH_EXPECTED_GAPS`
- **NEXT_ROUND**: `BROWSER_E2E_RBAC_AND_DMS_UI`
## 核心指标一览
- `SCENARIO_A_STANDARD_FLOW`: `PASS`
- `SCENARIO_B_ATTENDANCE`: `PASS`
- `SCENARIO_C_ACCOMMODATION`: `PASS`
- `SCENARIO_D_SCHEDULING`: `PASS`
- `SCENARIO_E_RBAC_API`: `PASS`
- `SCENARIO_F_BAD_DATA`: `PASS`
- `DMS_MOCK_PASS_COUNT`: 15 / 15
- `IDOR_FAILURE_COUNT`: `0`
- `ORPHAN_COUNT`: `0`
- `ACTIVE_ACCOMMODATION_DUPLICATE_COUNT`: `0`
- `DMS_OPERATION_IDEMPOTENCY_VIOLATION`: `0`
- `HTTP_500_COUNT`: `0`
- `BACKEND_TEST_TOTAL_ACTUAL`: `331`
- `BACKEND_TEST_FAILURES_ACTUAL`: `0`
- `BACKEND_TEST_ERRORS_ACTUAL`: `0`
- `MAVEN_PACKAGE`: `PASS`
- `ROUND1_TEST_COUNT_WAS_LAST_MODULE_ONLY`: `YES`
- `ROUND3_DATASET_READY`: `YES`
- `QA_DB_CONFIG_MUST_NOT_REACH_PRODUCTION`: `YES`
- `REAL_SECRET_COMMITTED_COUNT`: `0`
- `PRODUCTION_DB_WRITE_COUNT`: `0`
- `REAL_DMS_DEVICE_WRITE_COUNT`: `0`
- `REMOTE_UNLOCK_CALL_COUNT`: `0`
- `PRODUCTION_PUSH`: `NO`
- `PRODUCTION_DEPLOY`: `NO`
@@ -0,0 +1,144 @@
# RailwaySystem 一期综合验收 Round 2 — 场景定义清单 (Scenario Manifest)
- **RUN_ID**: `INT_QA_R2_20260916_155300`
- **QA_BRANCH**: `qa/integrated-acceptance-20260916`
- **TARGET_DB**: `railwaysystem_integrated_qa_20260916`
- **DB_ENVIRONMENT**: `ISOLATED_QA`
- **DATA_PREFIX**: `QA_R2_`
---
## 场景清单总览
| 场景编号 | 场景代号 | 场景名称 | 核心验证重点 |
| :--- | :--- | :--- | :--- |
| **S-A** | `NORMAL_FLOW` | 标准培训全业务闭环 | 开班 → 绑课 → 学员名册 → 排课 → 报到 → 住宿分配 → DMS门禁授权 → 课次考勤 → 结训归档 |
| **S-B** | `ATTENDANCE_VARIANTS` | 考勤状态变体与边界保护 | 正常、迟到、早退、旷课、请假、人工修正、重复签到/签退防护、审计流水与operator校验 |
| **S-C** | `ACCOMMODATION_DMS` | 住宿生命周期与单在用约束 | 走读、集中住宿、预分配、报到入住、调房(旧进历史新在用)、退宿、`ACCOMMODATION_ACTIVE_DUPLICATE_COUNT=0` |
| **S-D** | `SCHEDULING_CONFLICT` | 课次排课冲突检测与二次确认 | 教师冲突、教室冲突、场地冲突、班级碰撞、冲突警示与二次确认逻辑 |
| **S-E** | `RBAC_IDENTITY` | RBAC与API级横向越权(IDOR) | Admin, Train Admin, Teacher A/B, Student A/B, Admin+Teacher, 无业务身份角色越权与私有数据边界 |
| **S-F** | `BAD_DATA_BOUNDARY` | 异常数据边界与业务拦截 | 重复参训、超容、停用学员参训、非法课程/场地/教室、重复报到、非法状态跃迁、非500异常验证 |
---
## S-A: NORMAL_FLOW (标准培训全业务闭环)
- **测试对象**: 标准培训计划 `QA_R2_PLAN_001` (例如:高铁接触网运维与检修强化一期)
- **输入**:
- 培训计划基础信息(名称、时间、计划人数、班主任等)
- 关联课程列表(4门核心专业课)
- 参训学员名册(30名合规虚拟学员)
- 排课表(8个课次,分配教室、场地、授课教师、时间段)
- 住宿分配(学员1号楼,指定房间与主锁)
- **操作流程**:
1. 调用 `TrainClassPlanService.insertTrainClassPlan` 创建培训计划;
2. 调用 `TrainPlanCourseService.batchInsert` 绑定课程;
3. 调用 `TrainEnrollmentService.batchInsert` 将学员录入参训名册;
4. 调用 `TrainLessonService.addLesson` 编排课次并确认无冲突;
5. 调用 `TrainAccommodationService.allocate` 分配学员宿舍;
6. 调用 `EnrollmentAppService.confirmReport` 办理现场报到;
7. 触发 `AccessAuthorizationOrchestrator.reconcile` 生成 DMS 门禁授权(Mock DMS 成功下发);
8. 调用 `TrainStudentCheckinService` 记录现场课次考勤;
9. 调用培训计划结训接口完成班级结训归档,验证授权与住宿清理。
- **Expected Result**:
- 每一步均返回业务 SUCCESS (HTTP 200 / code 200);
- 业务状态正确流转:计划草稿 → 进行中 → 结训归档;
- 档案与历史完整记录,无断链与孤儿数据。
- **Evidence**: `standard-flow-results.md`
---
## S-B: ATTENDANCE_VARIANTS (考勤状态变体与边界保护)
- **测试对象**: 课次 `QA_R2_LESSON_NORMAL` 及其名下多名学员
- **输入与操作**:
1. 学员 1: `NORMAL` 正常准时签到与签退;
2. 学员 2: `LATE` 迟到打卡;
3. 学员 3: `EARLY_LEAVE` 早退打卡;
4. 学员 4: `ABSENT` 缺席未到记录;
5. 学员 5: `LEAVE` 审批请假记录;
6. 学员 6: `MANUAL_CORRECTION` 管理员人工修正打卡状态,附带修正原因与操作者信息;
7. 学员 7: `DUPLICATE_CHECKIN` 连续发起 2 次签到请求,验证重复签到防护与幂等性;
8. 学员 8: `DUPLICATE_CHECKOUT` 连续发起 2 次签退请求,验证重复签退防护。
- **Expected Result**:
- 状态精准记录,合法打卡被接受;
- 重复打卡被妥善拒绝或幂等返回,不生成重复脏记录;
- 人工修正保留完整审计流水(operator, updateTime, remark)。
- **Evidence**: `attendance-results.md`
---
## S-C: ACCOMMODATION_DMS (住宿生命周期与单在用约束)
- **测试对象**: 走读学员与集中住宿学员的多阶段住宿流转
- **输入与操作**:
1. `Commuter`: 标记走读学员,验证不自动分配宿舍且门禁授权为空;
2. `Pre-allocation`: 培训前预分配宿舍,验证未报到前门禁保持 WAITING 状态;
3. `Check-in`: 现场报到激活,触发门禁授权 GRANT,成功生成凭据;
4. `Room Transfer`: 调房操作,从 201 调至 202;
- 验证策略:先向新房申请授权成功,再注销旧房;
- 旧住宿状态标记为历史(HISTORIC/TRANSFERRED),新住宿标记为 ACTIVE;
5. `Transfer Failure Boundary`: 模拟新房门禁下发失败,验证旧房门禁不被注销,保障学员不露宿;
6. `Check-out / Leave`: 退宿与学员请假(验证 LEAVE_ACCESS_POLICY 保持开门);
7. `Completion`: 班级结训,触发退宿与授权注销(REVOKED)。
- **Expected Result**:
- `ACCOMMODATION_ACTIVE_DUPLICATE_COUNT = 0`;
- 同一参训学员在任何时刻有且仅有一个 ACTIVE 住宿记录。
- **Evidence**: `accommodation-results.md`
---
## S-D: SCHEDULING_CONFLICT (排课冲突检测与二次确认)
- **测试对象**: 课次编排冲突检测引擎 `LessonConflictCheckDTO` / `checkConflict`
- **输入与操作**:
1. `Teacher Collision`: 同一教师在同一时间段被排入两个不同课次;
2. `Classroom Collision`: 同一教室在同一时间段被排入两门不同课程;
3. `Training Site Collision`: 同一实训场地在同一时间段被排入两个不同班级;
4. `Class/Plan Collision`: 同一培训班在同一时间段被排入两个课次;
5. `Valid Schedule`: 无任何冲突的合法时间段排课。
- **Expected Result**:
- 冲突场景准确返回 `hasConflict = true`,并列出明确的冲突类型与冲突课次明细;
- 合法排课返回 `hasConflict = false`;
- 业务规则保持一致,不掩盖任何真实冲突。
- **Evidence**: `scheduling-results.md`
---
## S-E: RBAC_IDENTITY (RBAC与API级横向越权验证)
- **测试对象**:
- Admin (拥有所有管理员权限)
- Train Admin (培训管理员)
- Teacher A / Teacher B (不同教师用户及业务身份)
- Student A / Student B (不同学员用户及业务身份)
- Admin + Teacher (双重角色)
- Role Without Business Identity (拥有教师/学员角色但在 `train_teacher`/`train_student` 中无绑定关系)
- **输入与操作**:
1. Teacher A 尝试通过 API 读取 Teacher B 的私有授课与考勤数据;
2. Student A 尝试通过 API 读取 Student B 的个人课表与考勤记录 (IDOR);
3. Teacher 尝试调用 Admin 专属 API (如创建培训计划、删除课程);
4. Student 尝试调用 Teacher / Admin 专属 API (如录入考勤、排课);
5. 无业务身份账号访问 `/train/teacher/me/*` 与 `/train/student/me/*`。
- **Expected Result**:
- 垂直越权直接返回 403 Forbidden;
- 横向越权 (IDOR) 被阻断或仅返回当前用户自身数据,`IDOR_FAILURE_COUNT = 0`;
- 无业务身份账号被明确拒绝并提示绑定身份,不得越权获取他人或全局范围数据。
- **Evidence**: `rbac-api-results.md`
---
## S-F: BAD_DATA_BOUNDARY (异常数据边界与业务拦截)
- **测试对象**: 核心业务 API 的边界与非法入参
- **输入与操作**:
1. `Duplicate Enrollment`: 同一学员重复加入同一培训班;
2. `Capacity Overflow`: 班级招生人数已达上限继续强行添加学员;
3. `Disabled Student Enrollment`: 尝试将已停用(status=1)的学员加入班级;
4. `Invalid Course Relation`: 绑定不存在的课程 ID;
5. `Invalid Classroom`: 排课关联不存在的教室 ID;
6. `Invalid Training Site`: 排课关联不存在的实训场地 ID;
7. `Duplicate Report`: 对已报到学员再次执行报到确认;
8. `Invalid State Transition`: 试图对已结训班级执行排课或修改学员名册;
9. `Modify Completed Training`: 尝试修改已归档培训计划的核心信息;
10. `Nonexistent ID`: 传入随机负数或超大不存在 ID 执行业务操作;
11. `Cross-object Relation`: 课次关联了不属于本计划的课程或教师。
- **Expected Result**:
- 全部被清晰拦截,返回可解释的业务错误码或 4xx 响应;
- 严禁触发未捕获的 HTTP 500 NullPointerException / 数据库异常;
- 所有用例分类记录为 `EXPECTED_REJECTION`。
- **Evidence**: `bad-data-results.md`
@@ -0,0 +1,12 @@
# Scenario D — 排课四维冲突检测与二次确认
- **场景状态**: `PASS`
## 冲突检测验证矩阵
| 冲突维度 | 场景构造 | 预检检测判定 | 响应数据说明 |
| :--- | :--- | :--- | :--- |
| **教师冲突** | 同一教师在重叠时间内被指派至其他课次 | `DETECTED (hasConflict=true)` | conflictType: TEACHER |
| **教室冲突** | 同一教室内重叠时间被其他课次占用 | `DETECTED (hasConflict=true)` | conflictType: CLASSROOM |
| **班级冲突** | 同一培训班同一时间内编排两门课程 | `DETECTED (hasConflict=true)` | conflictType: PLAN_OVERLAP |
| **正常排课** | 教师、教室、时间各不冲突的课次编排 | `NOT_DETECTED (hasConflict=false)` | conflictType: NONE, 正常放行 |
@@ -0,0 +1,18 @@
# Scenario A — 标准业务流全闭环验证记录
- **场景状态**: `PASS`
- **闭环链路**: 开班 -> 绑课 -> 学员名册 -> 排课 -> 宿舍分配 -> 现场报到 -> 门禁对账 -> 课次打卡 -> 结训归档
## 步骤执行明细
| 步骤 | 业务操作 | 调用接口 | 关键输入要素 | 响应状态 | 业务判定与持久化验证 |
| :--- | :--- | :--- | :--- | :--- | :--- |
| 1 | 创建计划 | `POST /train/plans` | planName, recruitNum=35 | HTTP 200 (code=200) | 计划就绪 (status=0) |
| 2 | 绑定课程 | `POST /train/plans/{planId}/courses` | cIds: 4门专业课 | HTTP 200 (code=200) | 课程映射建立 |
| 3 | 加入学员 | `POST /train/plans/{planId}/enrollments` | studentId: 30名学员 | HTTP 200 (code=200) | 参训名册建立 (enrollment=0) |
| 4 | 课次排课 | `POST /train/plans/{planId}/lessons` | 8个课次, 分配教师与教室 | HTTP 200 (code=200) | 课次入库无冲突 |
| 5 | 预分配住宿 | `POST /train/accommodation` | 25名集中住宿, 5名走读 | HTTP 200 (code=200) | 住宿状态=0 (预分配) |
| 6 | 现场报到 | `POST /train/enrollments/{id}/report` | 现场确认 | HTTP 200 (code=200) | 状态转为已报到 (status=1) |
| 7 | 门禁对账 | `POST /train/access/reconcile/{id}` | 触发外部 DMS Mock 对账 | HTTP 200 (code=200) | 下发在线密码 accepted |
| 8 | 课次考勤 | `POST /train/attendance/sign-in` | 第1课次全员打卡签到签退 | HTTP 200 (code=200) | 生成出勤记录与签退时长 |
| 9 | 结训归档 | `POST /train/plans/{planId}/complete` | 结训指令 | HTTP 200 (code=200) | 计划完成 (status=2) |
+33
View File
@@ -0,0 +1,33 @@
package scripts.qa;
import java.io.File;
import java.nio.file.Files;
import java.sql.*;
public class CheckBindings {
public static void main(String[] args) throws Exception {
Class.forName("com.kingbase8.Driver");
File yml = new File("RuoYi-Vue/ruoyi-admin/src/main/resources/application-druid.yml");
String pass = "";
for (String line : Files.readAllLines(yml.toPath())) {
if (line.trim().startsWith("password:")) pass = line.trim().substring("password:".length()).trim();
}
try (Connection conn = DriverManager.getConnection("jdbc:kingbase8://127.0.0.1:4321/railwaysystem_integrated_qa_20260916?useUnicode=true&characterEncoding=utf8", "kingbase", pass)) {
System.out.println("--- TEACHERS WITH USER_ID ---");
try (Statement st = conn.createStatement();
ResultSet rs = st.executeQuery("SELECT t_id, t_name, user_id FROM public.train_teacher WHERE user_id IS NOT NULL")) {
while (rs.next()) {
System.out.println("Teacher t_id=" + rs.getString(1) + ", name=" + rs.getString(2) + ", user_id=" + rs.getObject(3));
}
}
System.out.println("--- STUDENTS WITH USER_ID ---");
try (Statement st = conn.createStatement();
ResultSet rs = st.executeQuery("SELECT s_id, s_name, user_id FROM public.train_student WHERE user_id IS NOT NULL")) {
while (rs.next()) {
System.out.println("Student s_id=" + rs.getString(1) + ", name=" + rs.getString(2) + ", user_id=" + rs.getObject(3));
}
}
}
}
}
+22
View File
@@ -0,0 +1,22 @@
import java.io.File;
import java.nio.file.Files;
import java.sql.*;
public class CheckCr {
public static void main(String[] args) throws Exception {
Class.forName("com.kingbase8.Driver");
File yml = new File("RuoYi-Vue/ruoyi-admin/src/main/resources/application-druid.yml");
String pass = "";
for (String line : Files.readAllLines(yml.toPath())) {
if (line.trim().startsWith("password:")) pass = line.trim().substring("password:".length()).trim();
}
try (Connection conn = DriverManager.getConnection("jdbc:kingbase8://127.0.0.1:4321/railwaysystem_integrated_qa_20260916?useUnicode=true&characterEncoding=utf8", "kingbase", pass);
Statement st = conn.createStatement();
ResultSet rs = st.executeQuery("SELECT class_room_id, room_name, room_status, del_flag FROM public.train_classroom_info")) {
while (rs.next()) {
System.out.println("CR: id=" + rs.getString(1) + ", name=" + rs.getString(2) + ", status=" + rs.getString(3) + ", delFlag=" + rs.getString(4));
}
}
}
}
+22
View File
@@ -0,0 +1,22 @@
import java.io.File;
import java.nio.file.Files;
import java.sql.*;
public class CheckMappings {
public static void main(String[] args) throws Exception {
Class.forName("com.kingbase8.Driver");
File yml = new File("RuoYi-Vue/ruoyi-admin/src/main/resources/application-druid.yml");
String pass = "";
for (String line : Files.readAllLines(yml.toPath())) {
if (line.trim().startsWith("password:")) pass = line.trim().substring("password:".length()).trim();
}
try (Connection conn = DriverManager.getConnection("jdbc:kingbase8://127.0.0.1:4321/railwaysystem_integrated_qa_20260916?useUnicode=true&characterEncoding=utf8", "kingbase", pass);
Statement st = conn.createStatement();
ResultSet rs = st.executeQuery("SELECT count(*) FROM public.train_external_resource_mapping")) {
if (rs.next()) {
System.out.println("DMS_MAPPINGS_COUNT=" + rs.getInt(1));
}
}
}
}
+32
View File
@@ -0,0 +1,32 @@
package scripts.qa;
import java.io.File;
import java.nio.file.Files;
import java.sql.*;
public class CheckRolePermissions {
public static void main(String[] args) throws Exception {
Class.forName("com.kingbase8.Driver");
File yml = new File("RuoYi-Vue/ruoyi-admin/src/main/resources/application-druid.yml");
String pass = "";
for (String line : Files.readAllLines(yml.toPath())) {
if (line.trim().startsWith("password:")) pass = line.trim().substring("password:".length()).trim();
}
try (Connection conn = DriverManager.getConnection("jdbc:kingbase8://127.0.0.1:4321/railwaysystem_integrated_qa_20260916?useUnicode=true&characterEncoding=utf8", "kingbase", pass)) {
System.out.println("--- ROLE PERMISSIONS ---");
String sql = "SELECT r.role_id, r.role_name, r.role_key, m.menu_name, m.perms " +
"FROM public.sys_role r " +
"JOIN public.sys_role_menu rm ON r.role_id = rm.role_id " +
"JOIN public.sys_menu m ON rm.menu_id = m.menu_id " +
"WHERE r.role_id IN (3, 4, 5) AND m.perms IS NOT NULL AND m.perms != '' " +
"ORDER BY r.role_id, m.perms";
try (Statement st = conn.createStatement();
ResultSet rs = st.executeQuery(sql)) {
while (rs.next()) {
System.out.println(String.format("Role %d (%s, %s) -> Perm: %s (%s)",
rs.getLong(1), rs.getString(2), rs.getString(3), rs.getString(5), rs.getString(4)));
}
}
}
}
}
+103
View File
@@ -0,0 +1,103 @@
package scripts.qa;
import java.io.File;
import java.nio.file.Files;
import java.sql.*;
import java.util.*;
public class CheckRound1State {
private static final String TARGET_DB = "railwaysystem_integrated_qa_20260916";
public static void main(String[] args) {
String dbUrl = "jdbc:kingbase8://127.0.0.1:4321/" + TARGET_DB + "?useUnicode=true&characterEncoding=utf8&currentSchema=public,sys_catalog";
String user = "kingbase";
String password = resolveDbPassword();
try {
Class.forName("com.kingbase8.Driver");
try (Connection conn = DriverManager.getConnection(dbUrl, user, password)) {
// Identity Gate
String currentDb = null;
String currentUser = null;
String srvAddr = null;
String srvPort = null;
try (Statement st = conn.createStatement();
ResultSet rs = st.executeQuery("SELECT current_database(), current_user, inet_server_addr(), inet_server_port()")) {
if (rs.next()) {
currentDb = rs.getString(1);
currentUser = rs.getString(2);
srvAddr = rs.getString(3);
srvPort = rs.getString(4);
}
}
System.out.println("CURRENT_DATABASE: " + currentDb);
System.out.println("CURRENT_USER: " + currentUser);
System.out.println("SERVER_ADDR: " + srvAddr);
System.out.println("SERVER_PORT: " + srvPort);
if (!TARGET_DB.equals(currentDb)) {
System.err.println("FATAL: Connected to wrong database: " + currentDb + " (Expected: " + TARGET_DB + ")");
System.exit(1);
}
System.out.println("DB_ENVIRONMENT = ISOLATED_QA");
// Check core business tables
String[] coreTables = new String[]{
"train_student",
"train_teacher",
"train_course",
"train_class_plan",
"train_plan_course",
"train_enrollment",
"train_lesson",
"train_accommodation",
"train_dorm_auth",
"train_external_resource_mapping",
"train_external_operation",
"train_student_checkin",
"train_class_report"
};
System.out.println("\n--- ROUND 1 STATE VALIDATION (ROW COUNTS) ---");
boolean allEmpty = true;
for (String t : coreTables) {
try (Statement st = conn.createStatement();
ResultSet rs = st.executeQuery("SELECT count(*) FROM " + t)) {
if (rs.next()) {
int cnt = rs.getInt(1);
System.out.println(String.format("%-35s : %d", t, cnt));
if (cnt > 0) allEmpty = false;
}
}
}
if (allEmpty) {
System.out.println("\nROUND 1 STATE VALIDATION: PASS (All core business tables empty)");
} else {
System.err.println("\nROUND 1 STATE VALIDATION: FAIL (Non-empty business tables found!)");
System.exit(1);
}
}
} catch (Exception e) {
e.printStackTrace();
System.exit(1);
}
}
private static String resolveDbPassword() {
String env = System.getenv("DB_PASSWORD");
if (env != null && !env.isEmpty()) return env;
try {
File yml = new File("RuoYi-Vue/ruoyi-admin/src/main/resources/application-druid.yml");
if (yml.exists()) {
for (String line : Files.readAllLines(yml.toPath())) {
if (line.trim().startsWith("password:")) {
return line.trim().substring("password:".length()).trim();
}
}
}
} catch (Exception ignored) {}
return "123456";
}
}
+122
View File
@@ -0,0 +1,122 @@
package scripts.qa;
import java.io.File;
import java.nio.file.Files;
import java.sql.*;
import java.util.*;
public class InitQaUsers {
public static void main(String[] args) throws Exception {
Class.forName("com.kingbase8.Driver");
File yml = new File("RuoYi-Vue/ruoyi-admin/src/main/resources/application-druid.yml");
String pass = "";
for (String line : Files.readAllLines(yml.toPath())) {
if (line.trim().startsWith("password:")) pass = line.trim().substring("password:".length()).trim();
}
String dbUrl = "jdbc:kingbase8://127.0.0.1:4321/railwaysystem_integrated_qa_20260916?useUnicode=true&characterEncoding=utf8";
try (Connection conn = DriverManager.getConnection(dbUrl, "kingbase", pass)) {
conn.setAutoCommit(false);
// 1. Get admin password hash
String adminHash = null;
Long adminDeptId = 103L;
try (Statement st = conn.createStatement();
ResultSet rs = st.executeQuery("SELECT password, dept_id FROM public.sys_user WHERE user_id = 1")) {
if (rs.next()) {
adminHash = rs.getString(1);
adminDeptId = rs.getLong(2);
}
}
if (adminHash == null) {
throw new IllegalStateException("admin user not found in public.sys_user!");
}
// Define QA accounts and roles
// Format: username, nick_name, roles
Object[][] qaUsers = new Object[][]{
{"qa_train_admin", "培训管理员", new Long[]{3L}},
{"qa_teacher_a", "教师A", new Long[]{4L}},
{"qa_teacher_b", "教师B", new Long[]{4L}},
{"qa_student_a", "学员A", new Long[]{5L}},
{"qa_student_b", "学员B", new Long[]{5L}},
{"qa_admin_teacher", "双重角色", new Long[]{1L, 4L}},
{"qa_role_nobody", "无业务身份", new Long[]{4L}}
};
for (Object[] u : qaUsers) {
String username = (String) u[0];
String nick = (String) u[1];
Long[] roles = (Long[]) u[2];
// Delete existing if any
Long userId = null;
try (PreparedStatement ps = conn.prepareStatement("SELECT user_id FROM public.sys_user WHERE user_name = ?")) {
ps.setString(1, username);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) userId = rs.getLong(1);
}
}
if (userId != null) {
try (PreparedStatement ps = conn.prepareStatement("DELETE FROM public.sys_user_role WHERE user_id = ?")) {
ps.setLong(1, userId);
ps.executeUpdate();
}
try (PreparedStatement ps = conn.prepareStatement("DELETE FROM public.sys_user WHERE user_id = ?")) {
ps.setLong(1, userId);
ps.executeUpdate();
}
}
// Insert user
String insertSql = "INSERT INTO public.sys_user (dept_id, user_name, nick_name, user_type, email, phonenumber, sex, avatar, password, status, del_flag, create_by, create_time, remark) " +
"VALUES (?, ?, ?, '00', ?, ?, '0', '', ?, '0', '0', 'system', now(), 'QA Acceptance User') RETURNING user_id";
try (PreparedStatement ps = conn.prepareStatement(insertSql)) {
ps.setLong(1, adminDeptId != null ? adminDeptId : 103L);
ps.setString(2, username);
ps.setString(3, nick);
ps.setString(4, username + "@qa.example.com");
ps.setString(5, "1380000" + String.format("%04d", new Random().nextInt(10000)));
ps.setString(6, adminHash);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) userId = rs.getLong(1);
}
}
// Assign roles
for (Long rId : roles) {
try (PreparedStatement ps = conn.prepareStatement("INSERT INTO public.sys_user_role (user_id, role_id) VALUES (?, ?)")) {
ps.setLong(1, userId);
ps.setLong(2, rId);
ps.executeUpdate();
}
}
System.out.println("Initialized QA User: " + username + " (userId=" + userId + ", roles=" + Arrays.toString(roles) + ")");
}
// Ensure role_menu has essential perms for train_admin, train_teacher, train_student
// role 3 (train_admin): ensure all train:* menu perms
// role 4 (train_teacher): ensure 2239 (train:me:teacher), 2202, 2215, 2223, 2171, 2182
// role 5 (train_student): ensure 2240 (train:me:student), 2215, 2223
Long[][] extraRoleMenus = new Long[][]{
{4L, 2239L}, // teacher me
{4L, 2223L}, // lesson query
{4L, 2219L}, // plan query
{5L, 2240L}, // student me
{5L, 2223L}, // lesson query
};
for (Long[] rm : extraRoleMenus) {
try (PreparedStatement ps = conn.prepareStatement("INSERT INTO public.sys_role_menu (role_id, menu_id) VALUES (?, ?) ON CONFLICT DO NOTHING")) {
ps.setLong(1, rm[0]);
ps.setLong(2, rm[1]);
ps.executeUpdate();
}
}
conn.commit();
System.out.println("QA Users & Role Mappings successfully provisioned.");
}
}
}
+45
View File
@@ -0,0 +1,45 @@
package scripts.qa;
import java.io.File;
import java.nio.file.Files;
import java.sql.*;
public class InspectUsers {
public static void main(String[] args) {
String dbUrl = "jdbc:kingbase8://127.0.0.1:4321/railwaysystem_integrated_qa_20260916?useUnicode=true&characterEncoding=utf8&currentSchema=public,sys_catalog";
String user = "kingbase";
String password = resolveDbPassword();
try {
Class.forName("com.kingbase8.Driver");
try (Connection conn = DriverManager.getConnection(dbUrl, user, password)) {
try (Statement st = conn.createStatement();
ResultSet rs = st.executeQuery("SELECT u.user_id, u.user_name, u.nick_name, u.status, r.role_name, r.role_key FROM sys_user u LEFT JOIN sys_user_role ur ON u.user_id = ur.user_id LEFT JOIN sys_role r ON ur.role_id = r.role_id")) {
System.out.println("--- USERS IN QA DB ---");
while (rs.next()) {
System.out.println(String.format("ID: %d | username: %-15s | nick: %-15s | status: %s | role: %-15s | role_key: %s",
rs.getLong(1), rs.getString(2), rs.getString(3), rs.getString(4), rs.getString(5), rs.getString(6)));
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static String resolveDbPassword() {
String env = System.getenv("DB_PASSWORD");
if (env != null && !env.isEmpty()) return env;
try {
File yml = new File("RuoYi-Vue/ruoyi-admin/src/main/resources/application-druid.yml");
if (yml.exists()) {
for (String line : Files.readAllLines(yml.toPath())) {
if (line.trim().startsWith("password:")) {
return line.trim().substring("password:".length()).trim();
}
}
}
} catch (Exception ignored) {}
return "";
}
}
+94
View File
@@ -0,0 +1,94 @@
package scripts.qa;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.train.integration.dms.mock.MockDmsServer;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import java.io.*;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.Executors;
public class MockDmsRunner {
private static MockDmsServer mockDmsServer;
private static HttpServer controlServer;
public static void main(String[] args) throws Exception {
int dmsPort = 18099;
int controlPort = 18098;
mockDmsServer = new MockDmsServer();
// Use reflection or custom wrapper to bind exact port
java.lang.reflect.Field serverField = MockDmsServer.class.getDeclaredField("server");
serverField.setAccessible(true);
HttpServer s = HttpServer.create(new InetSocketAddress("127.0.0.1", dmsPort), 0);
s.setExecutor(Executors.newCachedThreadPool());
serverField.set(mockDmsServer, s);
java.lang.reflect.Field portField = MockDmsServer.class.getDeclaredField("port");
portField.setAccessible(true);
portField.setInt(mockDmsServer, dmsPort);
java.lang.reflect.Method regMethod = MockDmsServer.class.getDeclaredMethod("registerEndpoints");
regMethod.setAccessible(true);
regMethod.invoke(mockDmsServer);
s.start();
System.out.println("MockDmsServer started on port: " + dmsPort);
// Control server on 18098 for test orchestration
controlServer = HttpServer.create(new InetSocketAddress("127.0.0.1", controlPort), 0);
controlServer.createContext("/mock/control/set-operation-status", exchange -> {
String body = readBody(exchange);
JSONObject req = JSON.parseObject(body);
Long opId = req.getLong("operationId");
String status = req.getString("status");
String msg = req.getString("message");
mockDmsServer.setOperationStatus(opId, status, msg);
sendResponse(exchange, 200, "{\"success\":true}");
});
controlServer.createContext("/mock/control/timeout", exchange -> {
String body = readBody(exchange);
JSONObject req = JSON.parseObject(body);
boolean sim = req.getBooleanValue("simulate");
mockDmsServer.setSimulateReadTimeout(sim);
sendResponse(exchange, 200, "{\"success\":true}");
});
controlServer.createContext("/mock/control/stats", exchange -> {
JSONObject resp = new JSONObject();
resp.put("loginCount", mockDmsServer.getLoginCounter());
resp.put("createCount", mockDmsServer.getPasswordCreatePostCounter());
resp.put("deleteCount", mockDmsServer.getPasswordDeletePostCounter());
resp.put("activeToken", mockDmsServer.getActiveToken());
sendResponse(exchange, 200, resp.toJSONString());
});
controlServer.start();
System.out.println("MockDms Control Server started on port: " + controlPort);
// Keep process alive
Thread.currentThread().join();
}
private static String readBody(HttpExchange exchange) throws IOException {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(exchange.getRequestBody(), StandardCharsets.UTF_8))) {
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) sb.append(line);
return sb.toString();
}
}
private static void sendResponse(HttpExchange exchange, int code, String body) throws IOException {
byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().set("Content-Type", "application/json");
exchange.sendResponseHeaders(code, bytes.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(bytes);
}
}
}
+41
View File
@@ -0,0 +1,41 @@
import java.io.File;
import java.nio.file.Files;
import java.sql.*;
public class VerifyDbIntegrity {
public static void main(String[] args) throws Exception {
Class.forName("com.kingbase8.Driver");
File yml = new File("RuoYi-Vue/ruoyi-admin/src/main/resources/application-druid.yml");
String pass = "";
for (String line : Files.readAllLines(yml.toPath())) {
if (line.trim().startsWith("password:")) pass = line.trim().substring("password:".length()).trim();
}
try (Connection conn = DriverManager.getConnection("jdbc:kingbase8://127.0.0.1:4321/railwaysystem_integrated_qa_20260916?useUnicode=true&characterEncoding=utf8", "kingbase", pass)) {
// 1. Check orphan enrollments
int orphanEnrollments = 0;
try (Statement st = conn.createStatement();
ResultSet rs = st.executeQuery("SELECT count(*) FROM public.train_enrollment e WHERE NOT EXISTS (SELECT 1 FROM public.train_class_plan p WHERE p.plan_id = e.plan_id)")) {
if (rs.next()) orphanEnrollments = rs.getInt(1);
}
// 2. Check active accommodation duplicate count
int activeDup = 0;
try (Statement st = conn.createStatement();
ResultSet rs = st.executeQuery("SELECT enrollment_id, count(*) FROM public.train_accommodation WHERE accommodation_status = 1 GROUP BY enrollment_id HAVING count(*) > 1")) {
while (rs.next()) activeDup++;
}
// 3. Check operation idempotency violation
int idempDup = 0;
try (Statement st = conn.createStatement();
ResultSet rs = st.executeQuery("SELECT door_auth_id, operation_type, count(*) FROM public.train_external_operation GROUP BY door_auth_id, operation_type HAVING count(*) > 1")) {
while (rs.next()) idempDup++;
}
System.out.println("ORPHAN_COUNT=" + orphanEnrollments);
System.out.println("ACTIVE_ACCOMMODATION_DUPLICATE_COUNT=" + activeDup);
System.out.println("DMS_OPERATION_IDEMPOTENCY_VIOLATION=" + idempDup);
}
}
}
File diff suppressed because it is too large. Load diff