# 测试设计方案(内部) > 版本:V2.0 > 读者:研发 / 测试 / QA(内部文档,含风险、覆盖缺口、遗留问题) > 被测系统:合规管理平台(六角色全流程协同,前后端分离 + H5 移动端) > 配套文档:《详细设计方案 V2.0》《数据库设计文档 V2.0》《待优化遗留项》 --- ## 0. 阅读约定 - 所有接口路径均以 `/business` 为前缀(完整示例:`POST /business/auth/smsLogin`)。 - 所有「预期结果」中标注的**错误文案**均取自后端 `Controller` 源码中的 `AjaxResult.error(...)` 字面量,测试时以 `msg` 字段为准,**逐字比对**。 - HTTP 语义:本系统所有业务接口统一返回 `HTTP 200` + `AjaxResult` 结构体 `{code, msg, data}`,**成功 code=200、失败 code=500**(部分全局异常/无权限由框架返回 `401`/`403`)。本文「预期结果」中提到的 code 均指 `AjaxResult.code`,除非特别注明 HTTP 状态码。 - 「返回体结构」统一约定:列表接口返回 `{code, msg, total, rows}`;详情/操作返回 `{code, msg, data}`;错误返回 `{code:500, msg:"<错误文案>"}`。 --- ## 1. 测试目的与范围 ### 1.1 测试目的 1. 验证六类角色(后台管理员 / 合规人员 / 医生 / 执行方 / 支持方 / 项目负责人)的**功能正确性**与**数据隔离正确性**。 2. 验证会议「事实 + 推导」状态机、材料两级审核、费用汇总与结算等**核心业务规则**符合设计。 3. 验证短信登录、注册、电子签、OCR、供应商同步、OA 对接等**跨服务 / 跨系统链路**的可用性与容错。 4. 在验收前暴露高风险缺陷,明确**覆盖缺口与遗留问题**,为回归与验收提供依据。 ### 1.2 测试范围(范围内) | 功能域 | 覆盖内容 | |---|---| | 账号与认证 | 短信验证码登录、三类注册、登录状态与角色跳转 | | 角色权限与数据隔离 | 六角色权限矩阵、主 / 子账号隔离、越权拦截、数据脱敏 | | 专家管理 | 建档、启用/停用、查询回填、导入导出 | | 机构 / 人员管理 | 机构、人员、供应商账号同步 | | 策划方案与专项计划 | 投稿、审核、立项、专项计划维护 | | 项目管理 | 立项、分配(执行方 / 监察员 / 执行人)、公示、意向、报名、评分、项目开通 | | 会议管理 | 建会校验、参会人、材料、发票、海报、会议级分配 | | 材料两级审核 | 提交、一级审核、二级审核、批量审核、退回重算、审核日志 | | 费用与结算 | 费用口径、费用汇总、结算 / 完结 / 解冻 | | 劳务电子签 | 推送、填写、手写签名、PDF 归档 | | 消息中心 | 列表、未读、已读、SSE 推送 | ### 1.3 测试范围(范围外 / 不覆盖) | 项 | 原因 | |---|---| | 单元测试 / 接口自动化脚本 | 本方案为**手工测试设计**,不含自动化用例代码 | | 生产环境数据迁移校验 | 由 DBA 单独负责(见《数据库设计文档 V2.0》) | | 性能压测(并发量级、TPS 阈值) | 仅做功能级性能冒烟(见 §5) | | 安全渗透 / 代码审计 | 由安全团队单独进行,本方案只覆盖权限与越权功能面 | --- ## 2. 测试环境与测试数据 ### 2.1 测试账号矩阵(基础账号,后续用例引用) > 统一短信验证码:**固定测试码 `123456`**(测试环境短信网关 mock,发码后固定回 `123456`,验证码有效期 5 分钟)。 > 账号状态列 `audit_status`:`'1'`=待审核(**拦截登录**)、`'2'`=通过(可登录)、`'3'`=拒绝;`biz_expert.status`:`'Y'`=正常、`'N'`=禁用。`smsLogin` 中 `"您的信息正在审核中"` 的判定值为 `audit_status='1'`。 | 编号 | 角色 | 用户名 | 手机号 | role_type | account_type | 关键字段 / 备注 | |---|---|---|---|---|---|---| | U01 | 后台管理员 | admin | 13800000000 | admin | MAIN | 框架管理员,可访问 admin 全部菜单 | | U02 | 合规经理 | manager01 | 13800000002 | manager | MAIN | create_user_id=U02 | | U03 | 医生(已通过) | doctor01 | 13800000001 | doctor | MAIN | audit_status='2' | | U04 | 医生(待审核) | doctor02 | 13800000003 | doctor | MAIN | audit_status='1' | | U05 | 执行方主账号 | execmain | 13800000010 | executor | MAIN | exec_user_id=U05, execution_unit_id=EU1 | | U06 | 执行方子账号 | execsub | 13800000011 | executor | SUB | parent_user_id=U05, staff_user_id=U06 | | U07 | 支持方主账号 | sponsormain | 13800000020 | sponsor | MAIN | sponsor_admin_user_id=U07, sponsor_org_id=SO1 | | U08 | 支持方子账号 | sponsorsub | 13800000021 | sponsor | SUB | parent_user_id=U07, monitor_user_id=U08 | | U09 | 项目负责人 | leader01 | 13800000030 | leader | MAIN | lead_user_id=U09 | | U10 | 执行方子账号2 | execsub2 | 13800000012 | executor | SUB | parent_user_id=U05, staff_user_id=U10 | | U11 | 支持方子账号2 | sponsorsub2 | 13800000022 | sponsor | SUB | parent_user_id=U07, monitor_user_id=U11 | ### 2.2 基础业务数据(测试前置,一次性准备) | 数据 | 字段值 | 说明 | |---|---|---| | 机构(支持方)SO1 | org_id=SO1, org_name="XX医药支持机构", status='0'(启用) | sponsor 主/子账号挂靠 | | 执行单位 EU1 | execution_unit_id=EU1, name="XX执行单位" | executor 主/子账号挂靠 | | 项目 P1 | project_no="P202609001", create_user_id=U02, 总场次 totalSessions=10, is_published='Y' | 由 U02 立项 | | 项目 P2 | project_no="P202609002", create_user_id=U02, is_published='N' | 未发布,用于公示过滤用例 | | 项目 P3 | project_no="P202609003", create_user_id=U02, is_finished='Y' | 已结题,用于「已结题不能分配」用例 | | 会议 M1 | meeting 属于 P1, 第 1 期, is_executed=0 | 未执行,用于建会/参会人/材料用例 | | 会议 M2 | 属于 P1, 第 2 期, is_executed=1, laborAuditStage=4, serviceAuditStage=4, fee_calc_status=1, 凭证已传 | 已执行且可结算,用于结算用例 | | 会议 M3 | 属于 P1, 第 3 期, is_frozen=1 | 已冻结,用于「冻结不能提交材料」用例 | | 专家 E1 | expert_id=E1, name="王医生", phone=13800000001, status='0'(启用), title="主任医师", dept="心内科" | 供参会人回填、评分用例 | ### 2.3 数据准备 / 清理规则 - **准备**:每轮回归前,按 §2.2 用 admin 或 manager 账号在界面/接口建好基础数据;短信验证码固定 `123456`。 - **清理**:费用结算、评分、意向等**有状态流转**的用例,执行前先重置目标对象状态(见各用例「前置条件」的还原说明),避免用例间互相污染。 - **原则**:涉及唯一性约束的用例(如「本机构已创建第 X 期会议」),先删掉同名对象再执行,保证可重复。 --- ## 3. 测试策略 ### 3.1 分层策略 | 层级 | 内容 | 手段 | |---|---|---| | L1 功能测试 | 每个用例逐条验证接口/界面行为 | 手工 + 接口联调(Postman/Apifox) | | L2 业务规则测试 | 状态机、审核链、费用口径、结算前置 | 场景组合(§4 核心用例) | | L3 权限/隔离测试 | 六角色越权、主/子账号隔离、脱敏 | 交叉矩阵(§4.3、§4.5) | | L4 链路测试 | 短信、OCR、OSS、电子签、SSE、供应商同步 | 端到端(§4.9、§4.11、§5) | ### 3.2 优先级定义 | 优先级 | 定义 | 举例 | |---|---|---| | P0 | 核心主流程,阻塞验收 | 登录、建会、结算、材料审核 | | P1 | 重要业务规则/权限隔离 | 越权、状态机、费用口径、脱敏 | | P2 | 一般功能/边界 | 分页、排序、字段回显、提示文案 | | P3 | 次要/低频 | 导出、日志、操作留痕 | ### 3.3 判定标准 - 每条用例结果分 **Pass / Fail / Block(阻塞)/ NA(不适用)**。 - 一条用例 Pass 的标准:**实际返回/表现与「预期结果」完全一致**,包括错误文案逐字一致、code 一致、字段值与脱敏规则一致。 - 出现任何一条 P0 用例 Fail → 该模块不可验收;P1 用例存在未关闭的 Fail → 整体验收不通过。 --- ## 4. 分模块测试设计 > 每个模块给出「测试点清单」(全部要点)+「代表用例」(**展开到可逐行执行的步骤**)。代表用例是本文的核心,覆盖最高风险路径。 ### 4.1 登录与账号体系(M1) **测试点清单** | 编号 | 测试点 | |---|---| | M1-01 | 已注册手机号发码成功,未注册手机号发码报错 | | M1-02 | 验证码登录成功返回 JWT,`role_type` 驱动跳转 | | M1-03 | 医生 `audit_status='0'` 登录拦截 | | M1-04 | 停用/删除账号登录拦截 | | M1-05 | 执行方/支持方子账号在主账号停用/机构禁用时的拦截 | | M1-06 | 执行方注册(校验链 + 密码/用户名规则) | | M1-07 | 支持方注册(企业/角色校验) | | M1-08 | 验证码错误、过期、手机号格式错误 | **代表用例 M1-02-01:验证码登录成功(医生)** - **前置**:U03(doctor01 / 13800000001,audit_status='1')存在且启用。 - **步骤**: 1. `POST /business/auth/smsSendCode`,请求体 `{"phone":"13800000001"}`。 - 预期:`{code:200, msg:"发送成功", data:...}`,data 含 `uuid`。 2. 取出 `uuid`,`POST /business/auth/smsLogin`,请求体 `{"phone":"13800000001", "smsCode":"123456", "uuid":"<第1步uuid>"}`。 - 预期:`{code:200, msg:"登录成功", data:{token:""}}`;`data.token` 非空;后续请求携带 `Authorization: Bearer ` 可正常访问医生首页。 3. `GET /business/auth/getInfo`(携带 token)。 - 预期:`data.user.roleType == "doctor"`,前端据此跳转医生首页。 **代表用例 M1-03-01:待审核医生登录拦截** - **前置**:U04(doctor02 / 13800000003,audit_status='0')。 - **步骤**:发码后 `POST /business/auth/smsLogin` `{"phone":"13800000003","smsCode":"123456","uuid":...}`。 - **预期**:`{code:500, msg:"您的信息正在审核中"}`;不返回 token;登录失败。 **代表用例 M1-04-01:停用/删除账号登录拦截(错误文案覆盖)** - **前置**:分别准备「已删除」账号(del_flag='2')与「已停用」账号(status='1')。 - **步骤**:对两个账号分别发码登录。 - **预期**:删除账号 → `msg:"账号已被删除"`;停用账号 → `msg:"账号已被停用"`。 **代表用例 M1-06-01:执行方注册——用户名/密码规则校验链** - **步骤**:`POST /business/auth/registerExecutor`,逐条发送以下 body 并断言 msg: 1. `{"username":"ab","unitName":"XX","businessNature":"服务商","phone":"13900000001","smsCode":"123456","password":"123456","confirmPassword":"123456","uuid":...}` → `msg:"用户名长度 4-20 位"`。 2. `{"username":"a b!","...同1}` → `msg:"用户名只能包含字母/数字/下划线"`。 3. `{"username":"valid01","unitName":"","...}` → `msg:"企业名称不能为空"`。 4. `{"username":"valid01","unitName":"XX","businessNature":"","...}` → `msg:"企业性质不能为空"`。 5. `{"username":"valid01","unitName":"XX","businessNature":"服务商","password":"123","confirmPassword":"123",...}` → `msg:"密码长度 6-20 位"`。 6. 密码 `123456` / 确认 `654321` → `msg:"两次密码输入不一致"`。 7. phone 填已注册手机号 → `msg:"该手机号已注册, 请直接登录"`。 8. username 填已占用用户名 → `msg:"用户名已被占用: <用户名>"`。 9. 全部合法 → `{code:200}`,且自动创建 sys_user。 **代表用例 M1-07-01:支持方注册——企业/角色校验链** - **步骤**:`POST /business/auth/registerSponsor`,逐条断言: 1. 缺少 `orgId` → `msg:"请选择企业"`。 2. 缺少 `realName` → `msg:"请输入联系人姓名"`。 3. `orgId` 不存在 → `msg:"所选企业不存在"`。 4. `orgId` 指向非支持方机构 → `msg:"所选企业不是支持方"`。 5. 全部合法 → `{code:200}`。 --- ### 4.2 专家管理(M2) **测试点清单** | 编号 | 测试点 | |---|---| | M2-01 | admin 建档(同建 sys_user,用户名/密码=手机号) | | M2-02 | 启用/停用专家(同步 biz_expert.status + sys_user.status) | | M2-03 | 按手机号查询回填(命中返回档案,未命中返回 null) | | M2-04 | profile 按当前登录用户路由 | | M2-05 | 编辑(注意:不改动 expertId,否则后端 WHERE 失效) | | M2-06 | 导入(dry-run 预检 / force 覆盖) | **代表用例 M2-01-01:admin 建档专家** - **步骤**:`POST /business/expert`,body `{"name":"李医生","phone":"13800000004","dept":"神经内科","title":"副主任医师","status":"0"}`。 - **预期**:`{code:200, data:{...sys_user 含明文 password}}`;库中 `biz_expert` 新增一条,且 `sys_user.user_name=13800000004`、`sys_user.password=13800000004` 对应的 BCrypt 密文。 **代表用例 M2-03-01:手机号放大镜回填** - **步骤**: 1. `GET /business/expert/byPhone/13800000001` → 预期 `data` 为 E1 档案。 2. `GET /business/expert/byPhone/19999999999` → 预期 `data == null`,前端不报错、显示「未找到」。 **代表用例 M2-05-01:编辑专家(回归已知缺陷)** - **前置**:E1 存在。 - **步骤**:`PUT /business/expert`,body 带 `expertId=E1`、修改 `dept="心内科二区"`(**必须携带原 expertId**)。 - **预期**:`{code:200}`,库中 E1.dept 已更新;`expertId` 字段不被清空。 --- ### 4.3 机构 / 人员管理(M3) **测试点清单** | 编号 | 测试点 | |---|---| | M3-01 | 机构启用/停用对子账号登录的影响 | | M3-02 | 人员(biz_person)显示登录账号(join sys_user) | | M3-03 | 供应商账号同步(开放接口账号密码校验链) | **代表用例 M3-01-01:机构停用后子账号登录拦截** - **前置**:U07/U08 挂靠 SO1;先验证 U08 可正常登录。 - **步骤**:admin 停用 SO1(status='1')→ U08 再次发码登录。 - **预期**:`msg:"您所属的机构已禁用"`,登录失败。恢复 SO1 后可再次登录。 **代表用例 M3-02-01:人员列表显示登录账号** - **步骤**:`GET /business/person/list`(manager)。 - **预期**:行数据含 `account` 字段(来自 `sys_user.user_name`),与 `loginUsername` 字段命名不冲突;未绑 sys_user 的人员 `account` 为空。 --- ### 4.4 项目管理(M4) **测试点清单** | 编号 | 测试点 | |---|---| | M4-01 | manager 只见自己 create_user_id 的项目 | | M4-02 | sponsor 主/子账号项目可见性(sponsorAdminUserId / monitorUserId 反查) | | M4-03 | executor 主/子账号项目可见性(execUserId / staffUserId 反查) | | M4-04 | 项目删除(仅 admin;软删除级联) | | M4-05 | 分配执行方/监察员/执行人(参数必填 + 已结题拦截) | | M4-06 | 公示过滤(按 is_published) | | M4-07 | 评分(角色限制 + 聚合口径 sum/(count×4)) | | M4-08 | 项目开通(openStatus / openDeadline) | **代表用例 M4-04-01:项目删除权限与软删级联** - **步骤**: 1. U09(leader)`DELETE /business/project/{P1}` → 预期 `{code:500, msg:"只有管理员可删除项目"}`。 2. U01(admin)`DELETE /business/project/{P1}` → 预期 `{code:200}`。 3. 断言级联:`biz_project`、项目关联的 5 张分配表、会议链记录 `is_deleted` 均为软删标记(非物理删除)。 **代表用例 M4-05-01:已结题项目不能分配** - **前置**:P3(is_finished='Y')。 - **步骤**:`POST /business/project/saveAssigns`,body 含 `projectId=P3`。 - **预期**:`msg:"已结题项目不能分配"`;未写入分配表。 **代表用例 M4-05-02:分配参数必填校验** - **步骤**: 1. sponsorAssign 缺 `monitorUserIds` → `msg:"monitorUserIds 必填"`(或 `"monitorUserId 必填"`)。 2. executorAssign 缺 `staffUserIds` → `msg:"staffUserIds 必填"`(或 `"staffUserId 必填"`)。 3. 批量分配传空数组 → `msg:"请提供至少一个分配记录"`。 **代表用例 M4-07-01:评分聚合口径** - **前置**:P1 已有 2 条评分(分值为 4、4)。 - **步骤**:U02(manager)`POST /business/project/rate`,body `{"projectId":"P1","score":4}`。 - **预期**:`data.score == 4.0`(`(4+4+4)/(3×4)=1.0 → ×4 = 4.0`,四舍五入保留 1 位)。 - **反向**:U03(doctor)评分 → `msg:"当前角色不可评分"`;缺 projectId → `msg:"projectId 必填"`。 --- ### 4.5 会议管理(M5)—— 状态机核心 **测试点清单** | 编号 | 测试点 | |---|---| | M5-01 | 建会校验链(projectId 必填、期数上限、重复期数、时间窗) | | M5-02 | 删除会议(仅 admin / 合规经理) | | M5-03 | 阶段推导(劳务/会务两轨 min 汇总;FROZEN 仍算已执行) | | M5-04 | 提交材料前置(非执行方 / 未执行 / 已冻结拦截) | | M5-05 | 一级审核 / 二级审核(合规人 / 监察员权限 + 拒绝意见必填) | | M5-06 | 批量审核(返回 successCount/failCount) | | M5-07 | 会议级分配与批量上传回填 | | M5-08 | sponsor 会议详情脱敏 | **代表用例 M5-01-01:建会校验链(逐条断言)** - **前置**:P1 总场次=10,已建 0 场;P1 开始时间 2026-09-01、结束时间 2026-09-30。 - **步骤**:`POST /business/meeting`,逐条断言: 1. 缺 `projectId` → `msg:"建会必须指定 projectId"`。 2. `projectId` 存在但 `meetingNo`(期数)填 11 → `msg:"期数不能超过分配给本公司的场次 (共 10 场)"`。 3. 首次建第 1 期成功 → 再次建第 1 期 → `msg:"本机构已创建第 1 期会议, 请勿重复"`。 4. 会议开始时间早于项目开始 → `msg:"会议开始时间不能早于项目开始时间"`;结束晚于项目结束 → `msg:"会议结束时间不能晚于项目结束时间"`。 - **上限分支**:把「分配给本公司场次」调成 1,建满后 → `msg:"本项目分配给本公司的场次为 1 场, 已建 1 场, 已达上限"`。 **代表用例 M5-03-01:阶段推导(两轨 min)** - **前置**:M1 处于 `laborAuditStage=4`(A/已审核) + `serviceAuditStage=1`(N/待审核)。 - **步骤**:`GET /business/meeting/{M1}`(manager)。 - **预期**:会议级 stage == 服务轨 min(`1`),而非 4;`is_executed` 保持推导值。 **代表用例 M5-04-01:提交材料前置拦截** - **步骤**:`POST /business/meeting/submit-material`,body `{"meetingId":"M1","types":["LABOR"]}`,逐条断言: 1. 以 U02(非执行方)提交 → `msg:"您不是该项目的执行方, 无法提交材料"`。 2. 以 U05(执行方)对「未执行会议」提交 → `msg:"会议尚未执行, 不能提交材料"`。 3. 以 U05 对 M3(已冻结)提交 → `msg:"会议已冻结, 不能提交材料"`。 4. types 为空 → `msg:"请选择要提交的材料类型"`。 5. types 含 `"XXX"` → `msg:"未知材料类型: XXX"`。 6. types 只含 `"SERVICE"` 且无会务材料 → `msg:"会务材料至少上传一条"`。 7. 提交 `"LABOR"` 但会议无参会人 → `msg:"请先添加参会人员 (劳务材料)"`。 **代表用例 M5-05-01:二级审核权限与拒绝意见** - **步骤**: 1. 非合规人 `POST /business/meeting/audit-compliance` → `msg:"仅合规人员可操作"`。 2. 合规人提交 `items=[{type:"LABOR",approved:false}]` 且 `opinion` 为空 → `msg:"拒绝时意见不能为空"`。 3. 非监察员 `POST /business/meeting/audit-supervision` → `msg:"您不是该会议监察员, 无权监察"`。 **代表用例 M5-06-01:批量审核返回统计** - **前置**:M1、M2 均处于待一级审核。 - **步骤**:`POST /business/meeting/batch-audit-compliance`,body `{"meetingIds":["M1","M2"],"approved":true,"opinion":"通过"}`。 - **预期**:`data == {total:2, successCount:2, failCount:0, failures:[]}`;两会议审核状态推进。 **代表用例 M5-08-01:sponsor 会议详情脱敏** - **前置**:M1 有参会人(姓名李小明、手机号 13800000005、银行卡 6222000000000000、身份证 110101199001011234)。 - **步骤**:`GET` sponsor 视角会议详情 / 参会人列表(走 maskForSponsor)。 - **预期**: - `maskName`:李小明→`李*明`;张三→`张*`。 - `maskPhone`:`138****0005`(前3+****+后4)。 - `maskBankCard`:`6222********0000`。 - `maskIdCard`:`110101********1234`。 - 操作区只读、身份证附件列隐藏。 --- ### 4.6 参会人管理(M6) **测试点清单** | 编号 | 测试点 | |---|---| | M6-01 | 邀请函打开即报名(权限校验) | | M6-02 | 新增参会人(劳务费字段) | | M6-03 | 编辑锁定(劳务已提交后不可改) | | M6-04 | 导入(dry-run 预检 vs force 覆盖,返回 ngList) | | M6-05 | 劳务费= fee_pre_tax 合计口径 | **代表用例 M6-01-01:邀请函打开即报名** - **前置**:M1 下存在参会人 A1(attendee_id=A1)。 - **步骤**: 1. `GET /business/meetingAttendee/invitation/{A1}`(无权限用户)→ `msg:"无权限查看该邀请函"`。 2. `GET /business/meetingAttendee/invitation/999999` → `msg:"参会记录不存在"`。 3. 合法用户 `GET .../invitation/{A1}` → `{code:200}`,且该参会人报名状态变为已报名。 **代表用例 M6-03-01:劳务已提交后不可修改参会人** - **前置**:M1 已提交劳务材料。 - **步骤**:`PUT /business/meetingAttendee`,body 修改 A1 的 `feePreTax`。 - **预期**:`msg:"劳务已提交, 不能修改参会人"`;或 `checkAttendeeEditable` 返回不可编辑标记,前端禁用编辑。 **代表用例 M6-04-01:导入预检与覆盖** - **前置**:准备参会人 Excel(含手机号/姓名/工作单位等,其中 1 条手机号与现有参会人冲突)。 - **步骤**: 1. dry-run 模式导入 → 预期返回 `{total, mismatches}`,`mismatches` 列出冲突行,**不落库**。 2. force 模式导入 → 预期返回 `ImportResult {okNum, ngNum, ngList}`,`okNum` 为成功数、`ngList` 为失败明细。 --- ### 4.7 材料与凭证(M7) **测试点清单** | 编号 | 测试点 | |---|---| | M7-01 | 凭证上传权限(仅合规/管理员) | | M7-02 | 拍照上传白名单(L_PANORAMA 拆 FRONT/BACK) | | M7-03 | 打包下载权限(会务/劳务) | | M7-04 | 会务材料上传异步 jobId | **代表用例 M7-01-01:凭证上传权限** - **步骤**:U03(医生)`POST /business/meetingMaterial/saveVouchers` → `msg:"只有合规或管理员可上传凭证"`。 - **预期**:U01/U02 可正常上传并返回 code 200。 **代表用例 M7-03-01:打包下载权限** - **步骤**:U03 `GET /business/meetingMaterial/downloadZip?meetingId=M1` → 拦截;U02 同请求 → 返回 zip 流。 - **预期**:劳务材料同理(`downloadLaborZip`),仅管理员/合规经理可下载。 --- ### 4.8 费用与结算(M8) **测试点清单** | 编号 | 测试点 | |---|---| | M8-01 | 劳务费=参会人 fee_pre_tax 合计 | | M8-02 | 会务费=总发票(M_INVOICE)合计(覆盖 SUB) | | M8-03 | fee_calc_status 门控(未汇总完成不能结算) | | M8-04 | 结算前置(两轨 APPROVED + 凭证上传) | | M8-05 | 结算即终态(settle 自动 finish) | | M8-06 | 完结/解冻权限与前置 | **代表用例 M8-03-01:费用未汇总完成不能结算** - **前置**:M2 两轨已审核通过 + 凭证已传,但 `fee_calc_status=0`。 - **步骤**:U01 `POST /business/meeting/settle` body `{"meetingId":"M2"}`。 - **预期**:`msg:"会议费用尚未汇总完成,无法结算"`。等 FeeCalcScheduler 汇总至 `fee_calc_status=1` 后再结算 → 成功。 **代表用例 M8-04-01:结算前置校验(逐条)** - **步骤**:对 M2 依次制造缺失条件,逐个断言: 1. 非合规/管理员 → `msg:"只有合规或管理员可结算"`。 2. 仅劳务轨通过 → `msg:"劳务与会务材料都审核通过后才能结算"`。 3. 已结算后再次结算 → `msg:"会议已结算"`。 4. 缺劳务凭证 → `msg:"请先上传劳务凭证"`;缺会务凭证 → `msg:"请先上传会务凭证"`;都缺 → `msg:"请先上传劳务凭证与会务凭证"`。 **代表用例 M8-05-01:结算即终态** - **步骤**:M2 满足全部前置后 `POST .../settle`。 - **预期**:`{code:200}`;库中 `is_settled=1`、`is_finished=1`,会议进入已完结终态,不可再结算/完结。 **代表用例 M8-06-01:完结/解冻** - **步骤**: 1. 未结算会议 `POST .../finish` → `msg:"会议尚未结算, 不能完结"`。 2. 已完结再次 finish → `msg:"会议已完结"`;非合规人 → `msg:"只有合规或管理员可完结"`。 3. 未冻结会议 `POST .../unfreeze` → `msg:"会议未冻结, 无需解冻"`;非合规人 → `msg:"只有合规或管理员可解冻"`。 --- ### 4.9 电子签章(M9) **测试点清单** | 编号 | 测试点 | |---|---| | M9-01 | sign info / resolve 查询 | | M9-02 | saveProfile 保存参保信息 | | M9-03 | contract 返回 HTML | | M9-04 | submit 手写签名 + 内容 + X-Real-IP | **代表用例 M9-04-01:提交签名** - **前置**:A1 已报名且处于可签署状态。 - **步骤**:`POST /business/sign/submit?attendeeId=A1`,header 加 `X-Real-IP: 1.2.3.4`,body `{"handsign":"data:image/png;base64,...","contentHtml":"..."}`。 - **预期**:`{code:200}`;库中签名记录落库,`labor_signed` 相关状态推进(若已接线);IP 字段记录 1.2.3.4。 --- ### 4.10 策划方案 / 专项计划(M10) **测试点清单** | 编号 | 测试点 | |---|---| | M10-01 | 投稿角色只见自己 submitter_id 的稿 | | M10-02 | 投稿角色新建/编辑强制 submitter_id 写自己 | | M10-03 | 管理角色排除 status='0' 草稿 | | M10-04 | 专项计划 status 0/1 语义 | **代表用例 M10-01-01:投稿角色数据隔离** - **步骤**:U03(doctor)`GET /business/projectPlan/list`。 - **预期**:返回行全部 `submitterId == U03`;不包含其他医生投稿。 **代表用例 M10-02-01:越权篡改 submitter 被后端强制覆盖** - **步骤**:U03 `POST /business/projectPlan`,body 里 `submitterId` 故意写 U04。 - **预期**:落库 `submitter_id` 被后端强制改为 U03(controller 覆盖),且 `status` 缺省为 `'0'`。 **代表用例 M10-03-01:管理角色排除草稿** - **步骤**:U02(manager)`GET /business/projectPlan/list`。 - **预期**:不返回任何 `status='0'` 的未提交草稿,仅返回已提交稿。 --- ### 4.11 消息中心与 SSE(M11) **测试点清单** | 编号 | 测试点 | |---|---| | M11-01 | my 分页/快捷模式返回 {rows, unread, total} | | M11-02 | 单条已读(归属校验) | | M11-03 | 详情归属校验(非管理员只能看自己的) | | M11-04 | 管理员发送(接收人/标题必填) | | M11-05 | SSE 推送不弹窗、静默刷新 + 角标 | **代表用例 M11-02-01:已读归属校验** - **前置**:U03 有一条消息 msgId=MSG1(receiver=U03)。 - **步骤**:U02 `PUT /business/message/read/MSG1`。 - **预期**:`msg:"消息不存在或无权操作"`,MSG1 未读状态不变。U03 自己操作 → code 200。 **代表用例 M11-04-01:管理员发送校验** - **步骤**:U01 `POST /business/message`: 1. 缺 `receiverUserId` → `msg:"接收人不能为空"`。 2. 缺 `title` → `msg:"标题不能为空"`。 3. 合法 → code 200。 **代表用例 M11-05-01:SSE 推送不打扰用户** - **步骤**:U03 登录并订阅 SSE;触发一条发送给 U03 的消息。 - **预期**:前端**不弹 ElNotification**,仅列表静默刷新 + 未读角标自增;重新进入后角标随已读动作递减。 --- ### 4.12 公示意向(M12) **测试点清单** | 编号 | 测试点 | |---|---| | M12-01 | 公示页匿名意向按钮落库到 publicity_support_intent / publicity_execution_intent | | M12-02 | SUB sponsor 可见性 UNION publicity_support_intent + sponsor_assign | | M12-03 | 意向数据不混入登录视角强绑表 | **代表用例 M12-01-01:匿名意向落库隔离** - **步骤**:公示页点「支持意向」→ 落库 `biz_publicity_support_intent`;点「执行意向」→ 落库 `biz_publicity_execution_intent`。 - **预期**:两张表独立、互不写入 `biz_support_intent`(已删表)或登录强绑表;同一用户可分别产生两类意向记录。 --- ## 5. 非功能测试 | 类别 | 测试点 | 方法 | 通过标准 | |---|---|---|---| | 性能冒烟 | 项目/会议列表查询耗时 | 200 条数据下 `GET .../list` | 响应 < 2s,无明显 N+1 慢查询 | | 性能冒烟 | OCR 识别单张发票 | 上传标准发票图片 | 无异常、结果可读(已知瓶颈 8–10s,记录即可) | | 兼容 | 六角色主要页面在 Chrome/Edge 正常 | 手工走查 | 无 JS 报错、布局正常 | | 移动端 | H5 相机拍照上传、无 margin 二次叠加 | 真机/模拟器 | 拍照回传、页面无 2x 间距 | | 容错 | 短信/OCR/OSS 服务不可用 | 断点注入 | 后端返回友好错误、前端有 toast、不白屏 | | 安全-功能面 | 越权访问他人数据、主/子账号越界 | §4.3/§4.5 交叉矩阵 | 全部拦截 | --- ## 6. 已知风险与覆盖缺口(GAP) > 来源于《待优化遗留项》与代码走查,测试时**必须**把以下项作为「已知风险」显式记录到缺陷/风险表,不能按「通过」处理。 | 编号 | 风险 / 缺口 | 影响 | 测试时的处理 | |---|---|---|---| | GAP-1 | `biz_special_plan.status` / `biz_doctor_title.status` / `biz_org.status` / `sys_user.status` / `biz_project_assign.status` 仍为 0/1,与新 Y/N 约定不一致 | 0/1 语义各表不同,易误判 | 逐表查 `COLUMN_COMMENT` 确认语义后再断言,不要照搬 Y/N 方向 | | GAP-2 | `biz_project_plan.is_finished` 是死字段(无列、无映射) | 接口/前端若读它恒为空 | 断言该字段应被移除;出现读取即 Bug | | GAP-3 | `biz_meeting.is_finished` 是 tinyint 0/1(与项目级同名不同型) | 会议/项目状态判断不一致 | 会议侧按 0/1 断言,项目侧按 Y/N 断言 | | GAP-4 | `biz_meeting.labor_signed` 签署链路未接线(全库恒 N,无 setLaborSigned 调用) | 「签署劳务」动作未实现 | M9 用例标注为 **NA/Block**,待实现后补测;落库值应为 `'Y'` | | GAP-5 | 执行方子账号项目可见性走 staff_user_id 反查,需严格隔离 | 子账号可能看到非本人项目 | M4-03 需覆盖子账号 A/B 互不可见 | | GAP-6 | 缴费口径两态无锁 + FeeCalcScheduler 每分钟 | 并发下可能重复/遗漏汇总 | M8-03 覆盖「汇总中」「汇总完成」两态门控 | | GAP-7 | sponsor 会议详情脱敏复用 labor_protocol_masked(未 alter table) | 若列不存在则脱敏失效 | M5-08 断言列存在且打码正确 | --- ## 7. 验收准出标准 1. 所有 **P0 用例 100% Pass**(无 Block/NA 说明未实现且已登记)。 2. **P1 用例 Pass 率 ≥ 98%**,未关闭 Fail 需有明确责任人与修复计划。 3. 每个 **GAP 项均已登记**,并明确「已处理 / 遗留 + 计划」。 4. 缺陷单完整:每条 Fail 有可复现步骤、实际结果、预期结果、截图/接口响应。 5. 回归通过:P0 全量 + 受影响模块 P1 重跑通过后,方可验收。 --- ## 8. 附录:接口/文案速查(供测试断言) | 模块 | 接口 | 关键错误文案 | |---|---|---| | 登录 | `POST /business/auth/smsLogin` | 手机号格式错误 / 请输入短信验证码 / 验证码错误或已过期 / 用户不存在 / 账号已被删除 / 账号已被停用 / 所属主账号不存在, 请联系管理员 / 您所属的机构已禁用 / 您的信息正在审核中 | | 注册 | `POST /business/auth/registerExecutor` | 用户名长度 4-20 位 / 用户名只能包含字母/数字/下划线 / 企业名称不能为空 / 企业性质不能为空 / 密码长度 6-20 位 / 两次密码输入不一致 / 该手机号已注册, 请直接登录 / 用户名已被占用: X | | 注册 | `POST /business/auth/registerSponsor` | 请选择企业 / 请输入联系人姓名 / 所选企业不存在 / 所选企业不是支持方 | | 项目 | `POST /business/project/rate` | projectId 必填 / 当前角色不可评分 | | 项目 | `DELETE /business/project/{id}` | 只有管理员可删除项目 | | 项目 | 分配 | monitorUserIds 必填 / staffUserIds 必填 / 项目不存在 / 已结题项目不能分配 / 请提供至少一个分配记录 | | 会议 | `POST /business/meeting` | 建会必须指定 projectId / 期数不能超过分配给本公司的场次 (共 X 场) / 本机构已创建第 X 期会议, 请勿重复 / 已达上限 / 会议开始时间不能早于项目开始时间 / 会议结束时间不能晚于项目结束时间 | | 会议 | `POST /business/meeting/submit-material` | 您不是该项目的执行方, 无法提交材料 / 会议尚未执行, 不能提交材料 / 会议已冻结, 不能提交材料 / 请选择要提交的材料类型 / 未知材料类型: X / 会务材料至少上传一条 / 请先添加参会人员 (劳务材料) | | 会议 | `audit-compliance` | 仅合规人员可操作 / 拒绝时意见不能为空 | | 会议 | `audit-supervision` | 您不是该会议监察员, 无权监察 | | 会议 | `settle` | 只有合规或管理员可结算 / 劳务与会务材料都审核通过后才能结算 / 会议已结算 / 会议费用尚未汇总完成,无法结算 / 请先上传劳务凭证与会务凭证 / 请先上传劳务凭证 / 请先上传会务凭证 | | 会议 | `finish` / `unfreeze` | 只有合规或管理员可完结 / 会议尚未结算, 不能完结 / 会议已完结 / 只有合规或管理员可解冻 / 会议未冻结, 无需解冻 | | 参会人 | `invitation` | 缺少参会记录 / 参会记录不存在 / 无权限查看该邀请函 | | 参会人 | 编辑 | 劳务已提交, 不能修改参会人 | | 材料 | `saveVouchers` | 只有合规或管理员可上传凭证 | | 材料 | `downloadZip` / `downloadLaborZip` | 只有管理员或合规经理可下载会务材料 / …劳务材料 | | 消息 | `POST /business/message` | 接收人不能为空 / 标题不能为空 | | 消息 | `PUT /business/message/read/{id}` | 消息不存在或无权操作 | | 消息 | `GET /business/message/{id}` | 消息不存在 / 无权查看 |