T00 — 系统基础设施与跨切面集成测试
类型: 系统级 | 前置条件: 后端 + 前端服务运行中 | 优先级: P0
本文档覆盖角色测试计划(R01-R05)未涉及的跨切面关注点。
1. 环境启动验证
| # |
测试项 |
操作 |
预期结果 |
通过 |
| 1.1 |
后端启动 |
.\dev.ps1 或 cargo run -p erp-server |
服务在 3000 端口启动,日志显示迁移自动执行 |
☐ |
| 1.2 |
健康检查 |
curl http://localhost:3000/api/v1/health |
返回 200,包含各模块状态 |
☐ |
| 1.3 |
前端启动 |
cd apps/web && pnpm dev |
Vite 在 5174 端口启动,浏览器可访问 |
☐ |
| 1.4 |
OpenAPI 文档 |
浏览器打开 http://localhost:3000/api/docs/openapi.json |
返回完整 OpenAPI JSON |
☐ |
| 1.5 |
数据库连接 |
psql -U postgres -h localhost -d erp -c "\dt" |
显示所有表(30 基础 + 44 健康 + 3 AI) |
☐ |
| 1.6 |
Redis 连接 |
检查后端日志 |
Redis 连接成功,无超时警告 |
☐ |
| 1.7 |
Ollama 可达 |
curl http://127.0.0.1:11434/api/tags |
返回模型列表,包含 qwen3:4b |
☐ |
2. 多租户隔离验证
业务背景: HMS 为 SaaS 平台,不同医疗机构(租户)的数据必须完全隔离。
2.1 数据隔离
| # |
测试项 |
操作 |
预期结果 |
通过 |
| 2.1.1 |
租户 A 查询 |
以 admin 登录(租户 A)→ 查询患者列表 |
只看到租户 A 的患者 |
☐ |
| 2.1.2 |
跨租户 API |
用租户 A 的 token 直接请求租户 B 的患者 ID |
返回 404(不是 200+空数据) |
☐ |
| 2.1.3 |
租户 ID 注入 |
检查后端日志中的 SQL 查询 |
所有 SELECT 都包含 WHERE tenant_id = ? |
☐ |
| 2.1.4 |
新增数据隔离 |
创建患者 → 检查数据库 |
新记录的 tenant_id 自动注入为当前租户 |
☐ |
2.2 权限隔离
| # |
测试项 |
操作 |
预期结果 |
通过 |
| 2.2.1 |
菜单隔离 |
不同租户的相同角色登录 |
菜单列表由 menu_roles 关联决定,可能不同 |
☐ |
| 2.2.2 |
角色隔离 |
租户 A 的 doctor 无法访问租户 B 的数据 |
API 返回空列表或 403 |
☐ |
3. 事件总线端到端
业务背景: 模块间通过 EventBus 异步通信,事件必须可靠投递(Outbox 模式)。
| # |
测试项 |
操作 |
预期结果 |
通过 |
| 3.1 |
患者创建事件 |
创建新患者 → 检查 domain_events 表 |
出现 patient.created 事件记录 |
☐ |
| 3.2 |
事件消费 |
等待 5 秒 → 检查事件状态 |
事件被消费(状态 processed) |
☐ |
| 3.3 |
随访完成事件 |
完成一条随访 → 检查 domain_events |
出现 follow_up.completed 或类似事件 |
☐ |
| 3.4 |
死信队列 |
检查 dead-letter 存储 |
无消费失败的事件(或已知原因) |
☐ |
| 3.5 |
LISTEN/NOTIFY |
创建患者 → 检查 PostgreSQL NOTIFY 日志 |
通知已发出 |
☐ |
4. 权限码全量校验
业务背景: 50 个声明权限码(health 39 + ai 6 + dialysis 5),前端 AuthButton 覆盖率仅 26%。已知 CRITICAL 问题:health.alert.manage(单数)vs health.alerts.manage(复数)。
| # |
测试项 |
操作 |
预期结果 |
通过 |
| 4.1 |
权限码一致性 |
检查 permissions 表中的 50 个声明码 |
每个码格式为 模块.实体.操作(如 health.patient.list) |
☐ |
| 4.2 |
告警权限码修复 |
登录有告警管理权限的角色 → 打开告警页面 |
告警管理按钮(确认/处理)正常显示 |
☐ |
| 4.3 |
AuthButton 覆盖 |
逐一检查各页面的操作按钮 |
新增/编辑/删除按钮使用 AuthButton 包裹,权限码匹配 |
☐ |
| 4.4 |
API 权限守卫 |
以无权限角色调用受保护 API |
返回 403,不是 500 |
☐ |
| 4.5 |
菜单-权限关联 |
检查 menu_roles 表 |
每个角色关联的菜单与测试计划(R01-R05)一致 |
☐ |
5. 冻结模块路由拦截
业务背景: 7 个模块已冻结(care_plan、shift 等),路由守卫应拦截访问。
| # |
测试项 |
操作 |
预期结果 |
通过 |
| 5.1 |
冻结路由拦截 |
浏览器访问冻结模块路由(如 care_plan 相关路径) |
显示"模块已冻结"提示或重定向,不显示空白页 |
☐ |
| 5.2 |
菜单不可见 |
检查左侧菜单 |
冻结模块不显示在菜单中 |
☐ |
| 5.3 |
API 拦截 |
调用冻结模块的 API |
返回明确错误(如 403 或 410),不是 500 |
☐ |
6. 并发冲突场景
业务背景: 预约使用 CAS 乐观锁,排班满额时并发预约应被拒绝。
| # |
测试项 |
操作 |
预期结果 |
通过 |
| 6.1 |
预约并发 |
同一时段两个请求同时预约 |
只有一个成功,另一个返回冲突错误 |
☐ |
| 6.2 |
排班满额 |
预约数达到排班上限 → 再预约 |
返回"已满"错误,不超额 |
☐ |
| 6.3 |
乐观锁冲突 |
两个请求同时编辑同一条患者 |
第二个请求返回版本冲突错误 |
☐ |
| 6.4 |
软删除可见性 |
删除患者后 → 列表中不显示 |
列表排除 deleted_at IS NOT NULL 的记录 |
☐ |
7. PII 加密全链路
业务背景: 患者敏感信息(姓名、身份证、手机号)使用 AES-256-GCM 加密存储,HMAC 盲索引支持搜索。
| # |
测试项 |
操作 |
预期结果 |
通过 |
| 7.1 |
写入加密 |
创建患者 → 直接查询数据库 |
姓名/身份证/手机号字段为密文,非明文 |
☐ |
| 7.2 |
读取解密 |
通过 API 查看患者详情 |
返回明文,可正常显示 |
☐ |
| 7.3 |
盲索引搜索 |
按手机号搜索患者 |
搜索结果正确,命中目标患者 |
☐ |
| 7.4 |
跨租户加密隔离 |
租户 A 的加密数据用租户 B 的密钥解密 |
解密失败或返回乱码,不泄漏明文 |
☐ |
| 7.5 |
HMAC 索引一致性 |
创建患者 → 检查 blind_indexes 表 |
对应字段有 HMAC 索引记录 |
☐ |
8. FHIR API 访问控制
业务背景: 14 个公开 FHIR 路由需验证访问控制。
| # |
测试项 |
操作 |
预期结果 |
通过 |
| 8.1 |
FHIR 资源访问 |
GET /api/v1/fhir/Patient |
返回 FHIR 格式的患者资源 |
☐ |
| 8.2 |
无 token 访问 |
不带 Authorization 头访问 FHIR 端点 |
公开端点可访问 / 受保护端点返回 401 |
☐ |
| 8.3 |
越权访问 |
用普通患者 token 访问其他患者的 FHIR 资源 |
allowed_patient_ids 限制生效,返回 403 |
☐ |
| 8.4 |
FHIR 格式验证 |
检查返回的 JSON 结构 |
符合 FHIR R4 规范(resourceType 字段存在) |
☐ |
9. SSE / WebSocket 连通性
业务背景: 消息中心使用 SSE 推送未读计数,AI 分析使用 SSE 流式返回结果。
| # |
测试项 |
操作 |
预期结果 |
通过 |
| 9.1 |
SSE 连接 |
浏览器打开 → 检查 Network 面板 SSE 连接 |
EventSource 连接到 /api/v1/messages/stream,状态 200 |
☐ |
| 9.2 |
消息推送 |
管理员发送消息 → 切换到另一个角色的浏览器 |
未读计数实时更新(SSE 推送) |
☐ |
| 9.3 |
SSE 断线重连 |
断开网络 → 恢复 |
SSE 自动重连,消息不丢失 |
☐ |
| 9.4 |
AI SSE 分析 |
触发 AI 分析(需前端入口或直接 API 调用) |
SSE 流式返回分析结果 |
☐ |
10. 错误恢复场景
| # |
测试项 |
操作 |
预期结果 |
通过 |
| 10.1 |
Token 过期刷新 |
登录后等待 token 接近过期(或手动修改 localStorage) |
前端自动刷新 token,无需重新登录 |
☐ |
| 10.2 |
401 响应处理 |
后端返回 401 → 检查前端行为 |
跳转到登录页,不显示空白 |
☐ |
| 10.3 |
403 响应处理 |
访问无权限页面 |
显示 403 提示或重定向,不显示空白 |
☐ |
| 10.4 |
500 响应处理 |
触发后端错误(如发送异常数据) |
显示友好错误提示,不显示原始堆栈 |
☐ |
| 10.5 |
Redis 降级 |
停止 Redis → 发起 API 请求 |
限流降级为 fail-close(503),不是无限等待 |
☐ |
| 10.6 |
数据库连接恢复 |
短暂断开数据库 → 恢复 |
连接池自动重建,后续请求正常 |
☐ |
| 10.7 |
网络中断恢复 |
断开前端网络 → 恢复 |
页面恢复数据加载,SSE 重连 |
☐ |
11. 文件上传
业务背景: 化验单和体检报告支持文件上传。
| # |
测试项 |
操作 |
预期结果 |
通过 |
| 11.1 |
图片上传 |
患者详情 → 上传化验单图片 |
上传成功,图片可预览 |
☐ |
| 11.2 |
文件大小限制 |
上传超大文件(>10MB) |
返回文件大小限制错误 |
☐ |
| 11.3 |
文件类型限制 |
上传非允许类型文件(如 .exe) |
返回文件类型限制错误 |
☐ |
| 11.4 |
图片预览 |
点击已上传的图片 |
全屏预览正常显示 |
☐ |
12. 安全边界
基于专家评审安全建议
| # |
测试项 |
操作 |
预期结果 |
通过 |
| 12.1 |
SQL 注入 |
在搜索框输入 ' OR 1=1 -- |
不返回全量数据,搜索结果为空或正常过滤 |
☐ |
| 12.2 |
XSS 防护 |
在患者姓名中输入 <script>alert(1)</script> |
存储后显示为转义文本,不执行脚本 |
☐ |
| 12.3 |
display_name XSS |
检查数据库中的 display_name 字段 |
不包含未转义的 HTML(P1 已知问题) |
☐ |
| 12.4 |
CORS 限制 |
从非白名单 Origin 发起 API 请求 |
被拒绝(生产环境 CORS 不含通配符) |
☐ |
| 12.5 |
JWT 伪造 |
使用篡改的 JWT 发起请求 |
返回 401 |
☐ |
| 12.6 |
批量导出限制 |
尝试导出大量数据 |
有分页限制或超时保护 |
☐ |
13. 数据完整性
| # |
测试项 |
操作 |
预期结果 |
通过 |
| 13.1 |
UUID v7 主键 |
检查任意表的主键格式 |
为 UUID v7 格式(时间排序) |
☐ |
| 13.2 |
审计字段 |
创建任意记录 → 检查数据库 |
created_at/updated_at/created_by/updated_by 自动填充 |
☐ |
| 13.3 |
乐观锁版本 |
编辑记录两次 → 检查 version 字段 |
version 递增 |
☐ |
| 13.4 |
软删除 |
删除记录 → 直接查询数据库 |
deleted_at 非空,记录仍存在 |
☐ |
| 13.5 |
唯一约束 |
创建重复身份证号的患者 |
返回唯一约束错误 |
☐ |
测试结果
- 测试人: _________
- 测试日期: _________
- 通过数: ___ / 总数: ___
- 问题记录: