PayrollManagementSystem/backend/tests/README.md
liuwenxin ca4cc72eea test(business): 业务数据管理模块集成用例(70 例)与两处缺陷修复
- 覆盖:编号格式与序号递增/状态机终态拦截/三处汇总冗余一致性/跨业务批次
  400/已核算禁改禁删/毛利润金标准与防除零/asOf 回溯/评级重叠/审计双记录
- 缺陷修复①:change_status 留痕 old_status 取到赋值后属性(时间线失真 3→3)
- 缺陷修复②:freight_amount 漏金额校验(负数/三位小数可落库污染运费合计)
- conftest 扩展业务六表清理(编号前缀 + FK 逆序收口)与评级种子基线恢复
- 全量回归 603 passed(单元 381 / 集成 222)

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-13 13:39:09 +08:00

161 lines
23 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.

# 后端测试
> AI摘要:本项目后端的测试用例清单与运行记录。按「基础设施层随实现即时测、业务模块整模块完成时统一测」分层组织。当前 603 个用例全绿(单元 381 / 集成 222)。
## 一、测试约定
### 编写时机(分层对待)
- `core/` 等**基础设施**随实现即时补测 —— 它们是被业务复用多次的地基,越早锁定越省事
- **业务模块**(系统管理、基础数据管理、业务数据管理、绩效工资核算、报表统计)在**整个模块功能完成时**统一编写用例并执行,不在模块做到一半时零散补测
### 目录二分
| 目录 | 依赖 | 用途 |
| --- | --- | --- |
| `tests/unit/` | 无(不碰 Docker、不连库) | 纯函数与元数据验证,秒级跑完,开发中可高频执行 |
| `tests/integration/` | 完整应用实例或 MySQL/Redis 容器 | 接口级、跨产物一致性验证 |
单元测试里的 Redis 依赖通过 `fake_redis` 夹具(fakeredis)替换,因此验证码 GETDEL、限流计数、黑名单 TTL 这些逻辑无需起容器即可覆盖。
### 注释要求
- 测试类用中文 docstring 说明「这组用例守的是什么」——即**如果不测会出什么问题**
- 用例名描述场景与预期(`test_中文按字节而非字符计算边界`)
- 关键断言补注释说明为何该断言重要,尤其是防回归的用例
### 运行方式
```bash
cd backend
uv run pytest tests/unit -q # 纯单元,无需容器,秒级
uv run pytest -q # 全部(集成部分需容器在跑)
uv run pytest tests/integration -q # 仅集成
```
## 二、用例清单
### 基础设施层(core)
| 测试文件 | 守什么 | 用例数 | 关键覆盖点 |
| --- | --- | :---: | --- |
| `unit/test_models.py` | 建表规范红线 | 49 | 表名小写蛇形复数;每列中文注释**且确实输出到 DDL**;`uk_`/`idx_`/`fk_` 命名前缀;ENGINE/CHARSET 显式指定;`ON UPDATE CURRENT_TIMESTAMP` 存在(`server_onupdate` 不会生成该子句,是 SQLAlchemy 的行为陷阱);日志表反向断言不含该子句 |
| `unit/test_logging.py` | 日志脱敏与格式化 | 16 | 密码/令牌/Bearer/手机号/身份证五类脱敏;**脱敏必须后置于 %-格式化**(早期实现在 filter 阶段改写 `record.msg`,抹掉 `%s` 占位符导致 logging 抛 TypeError、整条日志丢失);setup_logging 幂等 |
| `unit/test_config.py` | 配置加载与安全边界 | 14 | 密钥强度 fail-fast;JWT ≤120 分钟、验证码 ≤300 秒的硬约束;密码含 `@ : / #` 时 URL 构造不破坏;渲染 URL 默认隐藏密码 |
| `unit/test_security.py` | 密码哈希 | 11 | BCrypt 格式与定长 60;**72 字节边界**(24 个汉字正好、25 个超限);超长密码校验时返回 False 而非抛异常(防侧信道);哈希串损坏不炸 |
| `unit/test_response.py` | 统一响应契约 | 11 | 字段固定为 `code/message/data`;分页序列化为规范要求的驼峰 `list/total/pageNum/pageSize` |
| `unit/test_exception_handlers.py` | 异常转统一信封 | 5 | 校验错误 422→400 并压成单行中文;HTTP 状态码与 body 的 `code` 必须一致 |
### 系统管理 · 登录鉴权
| 测试文件 | 守什么 | 用例数 | 关键覆盖点 |
| --- | --- | :---: | --- |
| `unit/test_jwt.py` | 令牌有效期语义与版本机制 | 12 | `exp` 基于 **UTC** 而非本地时间(混算会产生 8 小时偏差,表现为「刚登录就过期」或「过期了还能用」);过期/篡改签名/**alg=none 攻击**/畸形字符串一律收敛为 401 而非 500;jti 唯一 |
| `unit/test_captcha.py` | 验证码**一次性语义** | 12 | GETDEL 后二次使用必然失败(防重放);**校验失败也消费**(否则可用一个验证码反复试密码);字符集剔除 `0O1Il`;每次渲染含随机干扰不可比对;TTL 300 秒 |
| `unit/test_login_limiter.py` | 阈值与**滑动窗口** | 9 | 达阈值即锁;每次失败刷新 TTL(否则「4 次失败 + 等 14 分钟 + 1 次失败」可绕过锁定);按用户名隔离,一个账号被攻击不牵连他人;登录成功清除计数 |
| `unit/test_token_blacklist.py` | 黑名单 **TTL 语义** | 7 | TTL 等于令牌剩余有效期(写死固定值会导致「登出 N 分钟后又活了」,不设过期则 Redis 无限增长);已过期令牌不写入;jti 互不串号 |
| `unit/test_auth_schemas.py` | 密码**按字节**校验 | 9 | 24 个汉字 = 72 字节正好、25 个被拒 —— 按字符数校验(如 `max_length=20`)根本拦不住,会让登录接口 500 |
| `integration/test_auth_flow.py` | 整条链路**串联正确性** | 16 | 验证码→登录→持令牌访问→登出失效;验证码重放被拒;失败原因如实落库且**日志表无任何密码字段**;账号不存在与密码错误文案完全相同(防枚举);禁用账号与 `token_version` 提升均使旧令牌立即失效 |
### 系统管理 · 用户与角色管理
| 测试文件 | 守什么 | 用例数 | 关键覆盖点 |
| --- | --- | :---: | --- |
| `unit/test_audit_service.py` | 审计上下文的语义 | 16 | 正常退出记成功、抛异常记失败;**失败时先 rollback 再写日志**且原异常原样重抛(顺序颠倒时数据库异常会让失败记录丢成 PendingRollbackError);**操作人标量在进入时固化**(失败路径不再触碰会过期的 ORM 对象);进入时脏会话告警(防嵌套半提交);审计自身失败不外抛、不替换业务异常 |
| `unit/test_audit_serialization.py` | 入库前截断与脱敏不丢数据、不泄密 | 16 | 各列按模型常量截断;60KB 请求参数按 **UTF-8 字节边界**截断后是原串的干净前缀(中文不切半);**结构化抹除**(按敏感键名整值替换):前导空白/空格/逗号/引号密码全被抹除且 JSON 结构完好、嵌套 dict/list 与键名大小写、非敏感键不误伤;手机号仍由 scrub 文本兜底;孤立代理字符不再抛错丢审计 |
| `unit/test_role_schemas.py` | 角色编码格式红线 | 16 | `^[a-z][a-z0-9_]{1,31}$` 全边界(大写/中文/数字或下划线开头/单字符/超长被拒);编辑模型**不接受 role_code**(多余字段忽略);状态只能 0/1 |
| `integration/test_user_management.py` | 用户 CRUD 串联正确性 + **审计留痕** | 23 | 分页以 `pageNum/pageSize` 生效(钉死别名契约,改蛇形会静默失效);自删/自禁/移除自己 admin 角色 403;禁用与重置密码使旧令牌立即失效;**每个写操作(含失败的)在 operation_logs 有对应记录**、request_data 全文扫不到明文密码;账号删除后审计行 `user_id` 置空而 username 保留;**编辑时保留既有角色不撞唯一键**(1062 回归);**编辑未修改字段不把 NULL 污染成空串**;**含下划线用户名不误配**(LIKE 转义回归) |
| `integration/test_role_management.py` | 角色保护规则(系统自举与授权安全的最低防线) | 13 | admin 角色禁删禁停 403;有绑定用户的角色禁删 400 且消息含用户数、解绑后可删;编码创建后不可改;**停用角色不可再分配**(接口层拦截);停用后不在 `/users/meta/roles` 但仍在管理列表;`user_count` 正确;普通用户 403 |
### 系统管理 · 日志查询
| 测试文件 | 守什么 | 用例数 | 关键覆盖点 |
| --- | --- | :---: | --- |
| `unit/test_log_schemas.py` | 时间范围校验与时区归一 | 13 | 起始晚于结束拒绝(两类日志共用基类,放过去则 SQL 条件互斥、查询恒空且无提示);**带时区输入转本地 naive**(Z 后缀 → 东八区 08:00;混用 aware/naive 不再抛 TypeError 穿透 500);边界相等通过;仅传一端不校验 |
| `integration/test_log_query.py` | 写入与查询的**口径一致** | 13 | 写操作产生的审计可按 username/module/action/success/时间范围检索到,摘要含目标用户名;失败记录带中文 `error_message`;**列表不含 request_data、详情端点返回已脱敏参数**(两段式,含 404/403);起始晚于结束 400;登录日志直插造数后可按 ip 精确筛选;两类日志普通用户 403 |
### 应用骨架与交付产物
| 测试文件 | 守什么 | 用例数 | 关键覆盖点 |
| --- | --- | :---: | --- |
| `integration/test_health.py` | 统一信封与全局异常 | 7 | `/api/health` 始终 200 且依赖状态如实上报;404/405 返回统一信封而非框架默认 `detail` 数组;`/api/docs` 可访问 |
| `integration/test_schema_sync.py` | DDL 不漂移 | 6 | `db/migration/V1__*.sql` 与 `db/init/init-dev.sql` 的**表集合与列集合逐表比对**——两份 DDL 手工同步迟早漂移,且漂移后果隐蔽(开发库结构与生产不一致,上线才发现) |
### M1 计提参数与岗位角色(2026-10-13 完成)
| 文件 | 守什么 | 用例数 |
| --- | --- | --- |
| `tests/integration/test_business_roles.py` | 岗位 CRUD 链路、内置岗位禁删禁停(种子 3 个)、模板编码校验、编辑换模板、权限 403、模板下拉 5 项 | 8 |
| `tests/integration/test_param_management.py` | 目录 16 项与七分组、企业户 2‰→3‰ 按到账日回溯、斜率 4/360 分数原样存储、变更生命周期(立即/未来生效不泄漏/同日取最新/撤回回退/已生效禁撤)、值校验(上限/枚举/除零分数/过去日期)、预览金标准(27 天=97/10000、折算 15000、绩效 145.5、可付 52.38;361 天=4‰)、经理聚合口径拒绝、权限 403 | 18 |
| `tests/unit/test_formula.py`、`tests/unit/test_param_registry.py` | 八个公式纯函数精确分数断言(第 1 天=899/90000、360=6‰、361=4‰、27 天、防除零、负绩效不归零、R×R)、注册表 16 项与迁移种子同源、模板声明参数闭环 | 43 |
| `tests/unit/test_param_snapshot.py` | 快照覆盖链(角色>产品类别>默认)、注册表默认值兜底、同日取最新、冻结不可变 | 12 |
运行:`uv run pytest -q` → **406 passed**(2026-10-13 全量含回归;T5 冒烟另验证提交/生效/撤回/对比/导出/预览全链路)。
### 基础数据管理(2026-10-13 完成)
| 文件 | 守什么 | 用例数 |
| --- | --- | --- |
| `tests/integration/test_personnel.py` | 未登录 401、新增默认在职、工资标准 str 直出、经理自引用/不存在或已离职/岗位停用 400、删除保护(团队成员与客户归属引用→引导改离职)、辐射区域与拓展计划全量替换(含清空与详情回显)、在职/离职切换、审计成功与失败双记录 | 20 |
| `tests/integration/test_products.py` | 未登录 401、类别值域 400、名称+规格组合唯一(同名称不同规格放行)、单价负数/非数字 400、启停切换、改类别、审计留痕 | 14 |
| `tests/integration/test_customers.py` | 未登录 401、客户分类值域 400、「公司名称+负责人」组合唯一(同公司不同负责人放行)、归属变更(主表更新+留痕字段断言)、归属人员离职 400、新旧相同含双方为空 400、留痕分页 join 姓名、编辑端点不触归属、审计留痕 | 14 |
运行:`uv run pytest tests/integration/test_personnel.py tests/integration/test_products.py tests/integration/test_customers.py -q` → **48 passed**(2026-10-13,连跑两次幂等)。
### 业务数据管理(2026-10-13 完成)
| 文件 | 守什么 | 用例数 |
| --- | --- | --- |
| `tests/integration/test_businesses.py` | 未登录 401;编号 `YW{YYYYMMDD}-{4位}` 与当日序号递增;空明细/数量 0/非法类别/单价非数字 400;**销售额服务层固化**(schema 无 sale_amount 入参,塞了忽略);特殊业务必填说明;编辑全量替换后**三处冗余一致**(明细 SUM == 业务冗余列 == DB SUM);状态机(终止无原因 400 → 留痕 old=1/new=3 → 终态再变更 400);归属变更主表+留痕同生(old/new owner 与 assistant、owner_type=2)、归属未变化 400、创建时归属人员已离职 400;有发货/回款禁换客户禁删;空业务删除连带清理明细/状态留痕/业务归属留痕;毛利润视图金标准(销售 10000/成本 30 → 率 997/1000、市政 30% → 折算 6970;**销售额 0 全 0 防除零**;**成本缺失行**派生为 null 不计类别汇总;**asOf 按业务日期回溯旧阈值**);审计成功/失败双记录 | 25 |
| `tests/integration/test_shipments.py` | 未登录 401;编号 `SH{YYYYMMDD}-{4位}`;业务不存在 404;终态业务禁新发货 400;空明细/数量 0/运费负数/运费三位小数 400;**行金额与发货单总额服务层固化**;业务 total_ship/total_freight 回写一致;编辑后两处汇总同步;被回款指定禁删(解除后可删);删除后业务级汇总回退;终态业务发货单禁改禁删;审计双记录 | 15 |
| `tests/integration/test_receipts.py` | 未登录 401;批次 `HK{YYYYMMDD}-{4位}`;未指定批次可登记且 payment_days 为 null;**指定批次派生「发货日首日计第 1 天」**(2026-10-05 → 2026-11-02 = 29 天,编辑清指定后回 null);业务不存在 404 / 终态禁新增 400;金额负数/三位小数 400;**跨业务指定批次 400**(「发货批次不属于该业务,无法指定」,编辑路同校验);合计回写与删除回退;**已核算(改库 settlement_status=1)禁改禁删**;settlement_status 筛选 0/1 两路;审计双记录 | 16 |
| `tests/integration/test_ratings.py` | 未登录 401;种子四等级全量按 sort_order 升序(A 90-100/B 75-89/C 60-74/D 0-59);区间倒挂/超上限/负下限 → 400「分值区间不合法」;名称唯一 → 400「该等级名称已存在」;**新增与编辑两路重叠 400 点名冲突等级**;**边界相邻不算重叠正例**(删 C 后新增 60-74 / 把 D 编辑为 0-74,74 与 75 相邻放行);编辑改名改区间回显;删除后判定表更新、二次删除 404;审计双记录。种子删改由 conftest 兜底恢复 | 14 |
运行:`uv run pytest tests/integration/test_{businesses,shipments,receipts,ratings}.py -q` → **70 passed**(2026-10-13,连跑两次幂等);全量回归 `uv run pytest -q` → **603 passed**(单元 381 / 集成 222)。
## 三、运行记录
| 日期 | 范围 | 用例数 | 结果 | 备注 |
| --- | --- | :---: | --- | --- |
| 2026-10-09 | 全部 | 120 | ✅ 通过(4.99s) | 阶段 1 骨架 + 阶段 2 数据库完成后首次全量运行 |
| 2026-10-09 | 全部 | 119 | ✅ 通过(5.01s) | ORM 由异步改回同步后回归;净减 1 例(`sync_database_url` 独立配置已合并) |
| 2026-10-09 | 全部 | 184 | ✅ 通过(25.46s) | **登录鉴权模块完成**后统一编写并执行该模块测试套件(新增 65 例)。集成用例需容器在跑 |
| 2026-10-10 | 全部 | 282 | ✅ 通过(103.81s) | **系统管理模块(用户/角色/日志)完成**后统一编写该模块套件(新增 91 例:单元 48 / 集成 43)。含操作日志审计机制专项;**e2e 发现并修复 `replace_roles` 唯一键缺陷**(见四、缺陷表);集成用例需容器在跑 |
| 2026-10-10 | 全部 | 301 | ✅ 通过(87.40s) | **code-review 缺陷修复后全量回归**(新增/改造 19 例)。密码结构化脱敏、时区归一、日志详情两段式各有专项;集成套件因密码哈希模块级预计算提速(63s→42s) |
| 2026-10-13 | 基础数据集成三文件 | 48 | ✅ 通过(27.60s) | **基础数据管理模块完成**后统一编写(人员 20/产品 14/客户 14)。审计上下文内外的校验位置差异为本次新发现的缺陷类别(见下) |
| 2026-10-13 | 业务数据集成四文件 | 70 | ✅ 通过(54.73s,连跑两次幂等) | **业务数据管理(后端接口)完成**后统一编写(业务 25/发货 15/回款 16/评级 14)。全量回归 603 例通过 |
| 2026-10-13 | 全部 | 603 | ✅ 通过(5:17) | **T11 全量回归**(单元 381 / 集成 222),含本模块 70 例 |
## 四、测试发现并修复的缺陷
| 日期 | 缺陷 | 影响 | 处理 |
| --- | --- | --- | --- |
| 2026-10-09 | 日志脱敏在 `logging.Filter` 阶段改写 `record.msg` | 抹掉 `%s` 占位符 → logging 内部 `msg % args` 抛 TypeError → **整条日志丢失**(比不脱敏更危险) | 改为 `SensitiveFormatter` 在格式化**之后**脱敏;补回归用例 |
| 2026-10-09 | `PasswordHash.recommended()` 在 pwdlib 0.3.x 推荐 Argon2 而非 BCrypt | 密码算法与规范不符,且随库版本变化——同一批密码可能混用两种算法 | 改为显式 `PasswordHash((BcryptHasher(),))` 钉死算法 |
| 2026-10-09 | 模型未设置表级 `comment`,`update_time` 用 `server_onupdate` | 违反规范红线;`server_onupdate` 只是 ORM 标记,**不生成 `ON UPDATE CURRENT_TIMESTAMP` 子句**,更新时间永不刷新 | 补表注释;改用 `text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP")` |
| 2026-10-09 | `pymysql` 未带 `[rsa]` extra,`cryptography` 缺失 | MySQL 8.0 默认 `caching_sha2_password`,容器重启后首次连接走全量认证时抛 `RuntimeError`,且报错在建连阶段易被误读为「密码错误」 | 依赖改为 `pymysql[rsa]` |
| 2026-10-09 | `setex` 在 redis-py 8 已弃用 | 规范明确**禁用弃用 API**;弃用接口在未来大版本会被移除 | 验证码与黑名单改用 `set(key, value, ex=…)` |
| 2026-10-09 | 测试中 TTL 断言写死等值 | 属**测试自身缺陷**:Redis 的 TTL 以毫秒精度存绝对过期时刻、返回时向下取整,刚写入就查询必然可能少 1 秒——会变成偶发失败的闪烁测试 | 断言改为容忍 1 秒偏差,并注明原因 |
| 2026-10-09 | 验证码沿用 `captcha` 库默认渲染 | 随机彩色字符 + 重噪点,与界面视觉体系冲突;该库最后更新于 2025-03,内部还引用了 Pillow 的旧别名 | 改为自绘(`core/captcha_image.py`):配色与界面统一,难度由逐字旋转、抖动、干扰曲线与噪点保证;仅沿用其内置字体文件 |
| 2026-10-09 | 噪点用 `ellipse` 绘制 | 3×3 包围盒画出的椭圆恰好填充中心与四个边中点,形状就是「+」,形成规则的十字纹理——既显眼又容易被整体滤波抹掉,起不到干扰作用 | 改用单像素点与 2×2 方块 |
| 2026-10-09 | `tests/unit/test_jwt.py` 两处赋值了未使用的 `settings`(ruff F841) | 变量未被引用,属无效代码 | 删除赋值;该文件的 `get_settings` 导入仍被其他用例使用 |
| 2026-10-10 | `replace_roles` 先清后建但中间未落库 | SQLAlchemy 在同一 flush 中**先处理 INSERT 再处理 DELETE**,编辑用户保留既有角色时重插同 `(user_id, role_id)` 撞唯一键(1062)——编辑用户接口 500。**由前端 e2e「编辑用户」步骤暴露**:集成测试当时只覆盖了「换掉全部角色」(无交集)的路径 | `clear()` 后立即 `session.flush()` 把删除落库,再追加新集合;补集成回归用例(编辑时保留既有角色) |
| 2026-10-10 | 审计脱敏依赖文本正则 | 三种逃逸形态:密码**以空白开头时完全不脱敏**、含空格/逗号只抹首段、含引号既残留明文又破坏 JSON——明文/残段写入 `operation_logs.request_data` 且日志页可直接展开查看(**由 code-review 发现并逐条实证复现**;既有测试所用密码均不含空格,恰好掩盖该路径) | 改为按键名的**结构化抹除**(递归 dict/list 整值替换 `***`),scrub 降级为文本兜底;补前导空白/空格/引号/嵌套/大小写五类边界用例 |
| 2026-10-10 | `.gitignore` 的 `logs/` 规则吞掉日志查询页目录 | 规则无前导斜杠、匹配任意层级:`frontend/src/views/system/logs/` 整体被忽略,文件**进不了版本库**——干净克隆构建失败(本地因文件已在磁盘上而一切"正常",隐蔽性极强) | 收窄为 `/backend/logs/`(该规则本意即后端运行日志);以 `git check-ignore` 实证不再命中 |
| 2026-10-10 | 时间筛选未做时区归一 | 混用 aware/naive 抛 TypeError 穿透成 **500**;带时区输入被驱动静默丢弃偏移(查询窗口偏 8 小时且无提示) | `LogQueryBase` 加 field_validator:aware → `astimezone()` 本地 naive、naive 原样;补 4 例专项 |
| 2026-10-10 | 审计时序两处缺陷 | ① 失败路径 rollback 后读 operator 触发额外查询,账号并发被删时 ObjectDeletedError 被兜底吞掉、失败审计静默丢失;② 成功/失败两分支 8 个实参重复展开,且 role/user service 在 commit 之后仍做查询与响应构造——后置查询一旦抛错,「已提交成功」会被记成失败 | operator 标量进入时固化;审计链 DTO 化(`audit` 4 参 / `_safe_write` 2 参 / `add` 2 参);service 的 commit 后查询与响应构造全部前置 |
| 2026-10-13 | 基础数据三 service 的**前置校验在审计上下文之外** | 校验失败(岗位停用/经理自引用/分类不合法等 400)完全不留操作日志,违背「成功与失败都记」的既定原则——与 2026-10-10 审计时序缺陷同源,新写的 service 又犯了(各自独立实现时口径漂移) | personnel/product/customer 三 service 的值域与引用校验统一移入 `audit_service.audit` 上下文内;集成用例锁定(审计成功与失败双记录) |
| 2026-10-13 | `change_status` 状态留痕的 old_status 取到**赋值后**的 ORM 属性 | 时间线记成 `old==new`(如 1→3 的变更记成 3→3)——「变更前状态」无法回溯,状态流转追溯失效。由集成用例断言 `(old,new)==(1,3)` 抓住 | 赋值前固化 `old_status = business.status` 再流转;应用日志一并修正;回归用例锁定 |
| 2026-10-13 | 发货 `freight_amount` 漏做金额校验 | 负数/三位小数/超容量的运费直接落库(schema 对 quantity/sale_price 都复用了 `_check_amount`,唯独运费没套用),业务运费合计与报表随之错乱 | shipments.py schema 的 Create/Update 加 `field_validator("freight_amount")`(同套口径,文案「运费金额不能为负数 / 最多两位小数」);集成用例锁定 |
## 五、待补用例
| 模块 | 状态 | 说明 |
| --- | --- | --- |
| 基础数据管理 | ✅ 已完成 | 2026-10-13 完成(48 例,见上表);「被业务数据引用」的删除保护已随 T11 的业务/发货禁删用例覆盖 |
| 业务数据管理 | ✅ 后端已完成 | 2026-10-13 完成(70 例,见上表);前端页面与绩效核算联动(M3 台账核销回写 settlement_status)待 M3 补测 |
| 绩效工资核算 | 未开始 | 需求已澄清;金额精度需专项用例(`Fraction` 与 1/90 日斜率已在 M1 公式层覆盖) |
| 报表统计 | 未开始 | 依赖报表口径确认 |
> 系统管理模块(用户/角色/日志查询)已于 2026-10-10 完成并补齐用例,见上表。