目的:基于源码对「测评预约/订单」做代码级深挖,校验
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。
既有场景 02-parent.md P-7 写成「申请测评 → 选择规划师 → 孩子快照 → 支付 → 规划师录入结果」。代码实际是三段式:
① 测评订单(AssessmentOrder: pending→paid→cancelled/refunded)
│ 支付成功后生成「测评额度」(AssessmentQuota)
② 测评预约(AssessmentAppointment)
│ 创建时 **消耗一条额度**(quotaService.consumeQuota)
③ 测评执行/结果(DanAssessmentExecution / AssessmentResult)
└ 录入结果后触发规则引擎生成方案
/create 先 consumeQuota,不足/过期直接返回 "额度不足或已过期",不落预约、不发放能量。AssessmentAppointmentController,而在 AssessmentResult / DanAssessment 链路(待核实具体入口)。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。
| 接口 | 权限证据 | 备注 |
|---|---|---|
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 查,影响面有限) |
| 编号 | 严重度 | 位置 | 现象 | 期望 |
|---|---|---|---|---|
| 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 记录;若并发创建,额度竞争可能超售 |
额度扣减需原子/乐观锁 |
优先级: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 失败 |
修正口径 |
cancelAppointment 增加 @RequestAttribute("userId") 并校验 appointment.getUserId().equals(userId) || 是指派者;目的:防越权取消;闭环:TC-DV-01 复测返回拒绝。confirmAppointment 前置 if(status!=0) return false;目的:状态机合法;闭环:TC-DV-02 复测失败。create 前查「同 quotaId+memberId+status=0」是否存在;目的:防重复预约/重复能量;闭环:TC-DV-04 复测第 2 次拒绝。consumeQuota 加行锁/乐观锁;目的:防并发超售;闭环:并发用例验证。以上为只读分析结论,修复由研发团队实施;本文件仅供测试与评审使用。
通过自包含规格 tests/e2e/assessment-appointment-functional.spec.js(去掉 globalSetup、用 direct-register 自写种子数据)实跑后,对原分析做如下校正:
code=0 / 错误 code≠0」。真实后端(见 common/Result.java):成功 code=200,业务错误 code=500(无家庭 5001)。所有用例断言口径以 200/500 为准,详见 tests/defects/DEFECTS-assessment-appointment.md。code=500, message="服务器内部错误",而非原预测的「额度不足或已过期」。说明 consumeQuota 对不存在的额度抛了未捕获异常(已登记为 缺陷 F8 / CFC-AP-008)。guide-list → 500「只有成长规划师可以查看此列表」;规划师访问 guide-list → 200 返回空数组。与原分析一致。500「确认失败,您不是被指派的测评师」。与原分析一致。/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 的独立配置)。