Files
guoju0808/测试设计方案.md
T
郭庆泰 5a0a892574 feat: 邀请函邮件发送 + 首次设密/密码为空 + 姓名单一可信源
邀请发送 (菜单暂隐藏, 待确认参数见 发送邀请-待确认参数.md):
- 后端: BizInvite / BizInviteRecipient + Controller/Service/Mapper/XML
- 邮件: InviteMailSender (spring-boot-starter-mail SMTP) + application*.yml 邮件配置
- 上传进度: UploadProgressRegistry + UploadProgressController
- 前端: InviteList / InviteNew / InviteDetail / InviteView + api/business/invite.js
- 原型: proto/html/components/invite-detail / new-invitation / send-invitation

登录/账号:
- 首次设密: /getInfo 返回 isPasswordEmpty, SysProfileController 密码为空时跳过旧密码校验, ForcePasswordDialog 强制弹窗
- 姓名单一可信源 resolveDisplayName: doctor→biz_expert.name, sponsor/executor→biz_person.name, 其余回退 nick_name
- OA compliance 门禁改为按手机号查 ecology 视图 (不再限定 manager/leader)

其它:
- OSS zip 在线查看 (列清单+取单文件, 公开只读) + SecurityConfig permitAll
- doctor 项目详情 ProjectDetail.vue
- 数据库/测试/设计文档 (md) 入库
2026-09-10 20:40:49 +08:00

616 lines
34 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 测试设计方案(内部)
> 版本: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:验证码登录成功(医生)**
- **前置**U03doctor01 / 13800000001audit_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:"<JWT>"}}``data.token` 非空;后续请求携带 `Authorization: Bearer <token>` 可正常访问医生首页。
3. `GET /business/auth/getInfo`(携带 token)。
- 预期:`data.user.roleType == "doctor"`,前端据此跳转医生首页。
**代表用例 M1-03-01:待审核医生登录拦截**
- **前置**U04doctor02 / 13800000003audit_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-01admin 建档专家**
- **步骤**`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 停用 SO1status='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. U09leader`DELETE /business/project/{P1}` → 预期 `{code:500, msg:"只有管理员可删除项目"}`
2. U01admin`DELETE /business/project/{P1}` → 预期 `{code:200}`
3. 断言级联:`biz_project`、项目关联的 5 张分配表、会议链记录 `is_deleted` 均为软删标记(非物理删除)。
**代表用例 M4-05-01:已结题项目不能分配**
- **前置**P3is_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)。
- **步骤**U02manager`POST /business/project/rate`body `{"projectId":"P1","score":4}`
- **预期**`data.score == 4.0``(4+4+4)/(3×4)=1.0 → ×4 = 4.0`,四舍五入保留 1 位)。
- **反向**U03doctor)评分 → `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-01sponsor 会议详情脱敏**
- **前置**: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 下存在参会人 A1attendee_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":"<html>...</html>"}`
- **预期**`{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:投稿角色数据隔离**
- **步骤**U03doctor`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:管理角色排除草稿**
- **步骤**U02manager`GET /business/projectPlan/list`
- **预期**:不返回任何 `status='0'` 的未提交草稿,仅返回已提交稿。
---
### 4.11 消息中心与 SSEM11
**测试点清单**
| 编号 | 测试点 |
|---|---|
| M11-01 | my 分页/快捷模式返回 {rows, unread, total} |
| M11-02 | 单条已读(归属校验) |
| M11-03 | 详情归属校验(非管理员只能看自己的) |
| M11-04 | 管理员发送(接收人/标题必填) |
| M11-05 | SSE 推送不弹窗、静默刷新 + 角标 |
**代表用例 M11-02-01:已读归属校验**
- **前置**U03 有一条消息 msgId=MSG1receiver=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-01SSE 推送不打扰用户**
- **步骤**: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}` | 消息不存在 / 无权查看 |