deep-dive-assessment-appointment.md 9.6 KB

专项深挖 · 测评预约模块(Assessment Appointment)

目的:基于源码对「测评预约/订单」做代码级深挖,校验 user-scenarios/ 与 test-cases/ 的口径,产出可落地的边界用例与缺陷清单。 阅读对象:QA / 研发 / 测试架构。本文件为只读分析,不修改源码。 证据来源:cfc-backend/src/main/java/com/etotem/cfc/controller/assessment/AssessmentAppointmentController.java、.../service/AssessmentAppointmentService.java、.../entity/AssessmentAppointment.java、.../entity/AssessmentOrder.java。


1. 真实业务模型(修正既有场景口径)

既有场景 02-parent.md P-7 写成「申请测评 → 选择规划师 → 孩子快照 → 支付 → 规划师录入结果」。代码实际是三段式:

① 测评订单(AssessmentOrder: pending→paid→cancelled/refunded)
       │ 支付成功后生成「测评额度」(AssessmentQuota)
② 测评预约(AssessmentAppointment)
       │ 创建时 **消耗一条额度**(quotaService.consumeQuota)
③ 测评执行/结果(DanAssessmentExecution / AssessmentResult)
       └ 录入结果后触发规则引擎生成方案
  • 额度是预约的前置条件:/create 先 consumeQuota,不足/过期直接返回 "额度不足或已过期",不落预约、不发放能量。
  • 「孩子快照」与「录入结果触发方案」不在 AssessmentAppointmentController,而在 AssessmentResult / DanAssessment 链路(待核实具体入口)。
  • 结论:P-7 场景应改为「购买套餐 → 订单 paid → 获得额度 → 创建预约(消耗额度) → 规划师确认/完成 → 录入结果生成方案」。

2. 状态机(权威口径)

AssessmentAppointment.status(entity 第 32 行):

值 含义 进入方式(代码证据)
0 待确认 createAppointment 初始化(service 第 40 行)
1 已确认 confirmAppointment(service 第 63 行)
2 已完成 completeAppointment(service 第 73 行)
3 已取消 cancelAppointment(service 第 91 行)
4 未到 代码中无 setter 调用(待核实由何入口置 4)

能量发放:create 时给 member +10(service 第 48 行);complete 时给 childId +30(service 第 78 行);均 try/catch 吞异常仅打 warn。

3. 接口清单与权限

接口 权限证据 备注
POST /api/assessment/appointment/create 需 userId(JWT),memberId+quotaId 必填,assessorId∨guideId 必填 消耗额度、建执行记录、发 10 能量
POST /api/assessment/appointment/confirm/{id} userId 须等于被指派者(assessorId 优先,否则 guideId) 消息「您不是被指派的测评师」
POST /api/assessment/appointment/cancel/{id} 仅 @PathVariable Long id,无 userId/role ⚠️ 无归属校验
POST /api/assessment/appointment/my-list userId 家长视角
POST /api/assessment/appointment/guide-list role == "teacher" 非 teacher 拒绝
POST /api/assessment/appointment/assessor-list userId 无 role 校验(仅按自身 userId 查,影响面有限)

4. 缺陷 / 边界发现(代码级)

编号 严重度 位置 现象 期望
F1 高(越权) cancelAppointment controller 第 112–116 行 / service 第 88–94 行 取消接口不接收调用者身份,任何登录用户凭 id 即可取消他人预约 取消须校验 userId 为预约归属者或被指派者
F2 中 confirmAppointment service 第 57–66 行 仅校验「是否是被指派者」,不校验当前状态;对已取消/已完成预约再 confirm 仍置 1 增加状态守卫:仅 status=0 可确认;非法跃迁返回错误
F3 中 completeAppointment service 第 68–86 行 置 status=2 的方法在本控制器无对应接口;status=4(未到) 全代码无 setter 确认「已完成/未到」的真实入口;补齐或说明
F4 中 createAppointment service 第 27–55 行 无幂等:同 quotaId/memberId 重复提交会建 2 条预约并发 2 次 10 能量 增加「同额度/成员已有待确认预约则拒绝或返回既有」
F5 低 assessor-list 第 140–146 行 无 role 校验(与 guide-list 不一致) 统一鉴权策略(影响小,按自身 userId 隔离)
F6 低 create 第 50–51 行 appointmentDate/Time 为自由字符串,无格式/未来时间校验 增加日期格式与合理性校验
F7 低 consumeQuota 调用顺序 先 consumeQuota 再取 quota 记录;若并发创建,额度竞争可能超售 额度扣减需原子/乐观锁

5. 深挖专项测试用例(补充到 parent/teacher 用例)

优先级:P0 主链路/越权 · P1 重要 · P2 一般。类型:功能/接口/权限/异常。

TC 编号 所属场景 模块 角色 优先级 类型 前置条件 测试步骤 预期结果 关联发现
TC-DV-01 P-7(修正) 预约/权限 家长A vs 家长B P0 权限 A、B 各登录;A 有预约 id=X B 调用 POST /cancel/{X} 当前:返回 success 且 status=3(越权成功)→【缺陷 F1】;期望:403/拒绝 F1
TC-DV-02 P-7 预约/状态机 规划师 P1 异常 预约已 cancelled 对该 id 调 confirm 当前:status 置 1(非法回退)→【缺陷 F2】;期望:失败 F2
TC-DV-03 P-7 预约/额度 家长 P0 功能 无可用额度 POST /create(memberId+quotaId 无效) 返回「额度不足或已过期」;不建预约、不扣能量 修正口径
TC-DV-04 P-7 预约/幂等 家长 P1 异常 有 1 条有效额度 同一 quotaId 连续 create 两次 当前:2 条预约 + 20 能量 →【缺陷 F4】;期望:第 2 次拒绝/返回既有 F4
TC-DV-05 T-7 预约/权限 非指派用户 P0 权限 预约指派给规划师 T 其他人调 confirm/{id} 返回「您不是被指派的测评师」;status 不变 既有
TC-DV-06 P-7 预约/校验 家长 P1 接口 — create 同时不传 assessorId/guideId 返回「assessorId或guideId必须提供一个」 既有
TC-DV-07 T-7 列表/权限 非 teacher P2 权限 普通家长 调 guide-list 返回「只有成长规划师可以查看此列表」;assessor-list 仅见自身 F5
TC-DV-08 P-7 预约/校验 家长 P2 异常 — create 传过去日期/乱码时间 当前:无校验入库(F6);期望:400 F6
TC-DV-09 P-7 预约/状态机 规划师 P1 功能 新预约 status=0 confirm→complete 0→1→2 正确;能量 10+30 发放且流水一致 状态机
TC-DV-10 P-7(修正) 订单↔额度↔预约 家长 P0 功能 购买套餐(订单 paid) 订单 paid 后生成额度 → create 消耗额度 额度扣减正确;额度为 0 时 create 失败 修正口径

6. 给研发的修复建议(五要素摘要)

  1. F1 取消越权(严重):cancelAppointment 增加 @RequestAttribute("userId") 并校验 appointment.getUserId().equals(userId) || 是指派者;目的:防越权取消;闭环:TC-DV-01 复测返回拒绝。
  2. F2 确认状态守卫:confirmAppointment 前置 if(status!=0) return false;目的:状态机合法;闭环:TC-DV-02 复测失败。
  3. F4 创建幂等:create 前查「同 quotaId+memberId+status=0」是否存在;目的:防重复预约/重复能量;闭环:TC-DV-04 复测第 2 次拒绝。
  4. F3 状态入口:补全「已完成/未到」入口说明或接口;目的:状态机完整可观测;闭环:文档化入口。
  5. F7 额度原子:consumeQuota 加行锁/乐观锁;目的:防并发超售;闭环:并发用例验证。

以上为只读分析结论,修复由研发团队实施;本文件仅供测试与评审使用。


7. 实测勘误(2026-10-27 live 验证,目标 127.0.0.1:9082)

通过自包含规格 tests/e2e/assessment-appointment-functional.spec.js(去掉 globalSetup、用 direct-register 自写种子数据)实跑后,对原分析做如下校正:

  1. 码值约定更正:原文档多处假设「成功 code=0 / 错误 code≠0」。真实后端(见 common/Result.java):成功 code=200,业务错误 code=500(无家庭 5001)。所有用例断言口径以 200/500 为准,详见 tests/defects/DEFECTS-assessment-appointment.md。
  2. TC-DV-03 实测修正:额度不存在(quotaId=999999999)时,创建预约真实返回 code=500, message="服务器内部错误",而非原预测的「额度不足或已过期」。说明 consumeQuota 对不存在的额度抛了未捕获异常(已登记为 缺陷 F8 / CFC-AP-008)。
  3. TC-DV-07 实测确认:家长访问 guide-list → 500「只有成长规划师可以查看此列表」;规划师访问 guide-list → 200 返回空数组。与原分析一致。
  4. TC-DV-05 实测确认:非指派确认 → 500「确认失败,您不是被指派的测评师」。与原分析一致。
  5. 自注册可用:/api/auth/direct-register(测试专用)返回 code=200 并自动建家庭(familyId);可作为 e2e 的「写种子数据」手段,规避 globalSetup 对 192.168.16.251 的依赖。

校正结论已沉淀为:tests/defects/DEFECTS-assessment-appointment.md(F1–F8 缺陷单)、tests/e2e/assessment-appointment-functional.spec.js(可跑规格)、tests/playwright.config.func.js(去 globalSetup 的独立配置)。