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) 入库
This commit is contained in:
郭庆泰
2026-09-10 20:40:49 +08:00
parent 56615f9806
commit 5a0a892574
171 changed files with 11032 additions and 876 deletions
+615
View File
@@ -0,0 +1,615 @@
# 测试设计方案(内部)
> 版本: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}` | 消息不存在 / 无权查看 |