2026-10-09 06:46:36 -04:00
# 后端测试
2026-10-28 04:31:55 -04:00
> AI 摘要: 2026-10-28 的 P0.5 连接修复已通过 19 项定点测试和 1001 项完整回归;使用本轮独立 MySQL/Redis 容器,未连接客户 T+ 源库。真实备份源 10.50.1600 验收仍未执行。
## 连接策略修复核验( 2026-10-28)
- Ruff( 含兼容性脚本) 通过; 定点命令 `uv run pytest tests/unit/test_tplus_config.py tests/unit/test_tplus_connection_file.py tests/unit/test_tplus_compatibility_script.py -q` : 19 passed。
- 新增覆盖:数据库名/用户名/密码中的分号、花括号和中文; 默认加密与显式回退; 脚本复用连接文件校验; 10.50.1600 与其他补丁、2022 版本区分;连接失败不自动降级且报告不带驱动原始异常。
- 完整回归: 1001 passed, 1 warning, 869.47 秒。使用已安装的 `mysql:8.0` 、`redis:7-alpine` 镜像启动独立容器、随机宿主端口和命名卷,以 `db/init/init-dev.sql` 初始化;未连接现有开发业务库或 T+ 源库。
- 完整测试入口为 `uv run python ../.ai/run_isolated_regression.py` ,内部调用 `pytest.main(["-q", ...])` ,不筛选或跳过用例。运行期配置指向临时隔离实例;单测的 `_env_file=None` 仍保持默认值测试语义。结束后已清理本轮容器、卷与临时凭据配置。
- 兼容性脚本、worker 的 `--help` 可运行;保留现有 Starlette TestClient 弃用提醒。本轮未改前端,未重复前端构建。
- 版本判断与失败分支是单元模拟结果,不能据此宣称真实 2008 R2 的驱动、TLS、断连和权限已验收。下方 2026-10-27 的结果保留为历史记录。
2026-10-27 23:42:15 -04:00
## T+ 本轮核验( 2026-10-27)
- 完整回归在独立 MySQL `payroll_tplus_validation_20261027` 、Redis 15 库运行: 984 passed、2 failed。两项失败均为配置单测预期默认 Redis 0 库,而进程继承了集成用的 `REDIS_DB=15` ;没有修改默认配置或削弱断言。
- 后续将单元测试放在不设置上述集成覆盖值的独立进程中运行: 602 passed。最终管理 API、真实并发和结构一致性定点测试: 18 passed, 包含新增加的配置关闭及创建/重试审计。
- 真实并发:相同请求只生成一个批次;不同键同时重试同父批次只有一个成功,无死锁;执行锁跨 ORM commit 仍有效,退出后可再获取。
- V15 在隔离库 downgrade 到 V14, 再 upgrade 到 head 通过;当前 head `e5f607a8b9c0` 。模型、Alembic、留存 SQL 和初始化脚本的结构检查通过;此前 V1– V14 留存 SQL 已从空库执行通过。
- 人员重读 847 条,新增 0、重复跳过 847、失败 0; 关联报告覆盖 103,318 条销货类收款明细, 50,044 条有明确订单关联,六类业务表行数不变。独立证据位于 D 盘 reports/management_verification_2026-10-27.json。
- Chrome 真实接口检查通过:排队、受控失败、异常查看、页面重试、人员去重、关联检查、筛选、普通用户拒绝访问,以及 320/768/1024/1440 宽度。无页面运行错误;临时账号和批次已清理。
- 加载、空态、配置关闭、概况失败另外使用浏览器响应模拟验证,不能与真实源链路混记。证据位于 .ai/archive/ai-aux/preview/tplus/。
- 320 像素主内容可操作,现有全局顶栏仍有文字挤压;未扩大本轮范围修改共享布局。宽表保留横向滚动,截图和限制见同步管理验收报告。
- 前端类型检查、生产构建和 Ruff 通过;构建保留既有的大分块提醒,测试有现存 Starlette TestClient 弃用提醒。
- 源查询保持参数化与旧版语法,时间增量和业务表写入均关闭。合法 Windows/SQL Server 2008 R2 安装介质仍未取得,不能把 2022 的结果标记为客户版本兼容。
单元测试与集成测试建议分独立终端运行:单元测试不设置集成覆盖值;集成测试使用隔离 MySQL 和 Redis。通用集成夹具会清理数据, 不能指向开发或客户业务库。Windows 可设置 `PYTEST_DEBUG_TEMPROOT` 到项目 D 盘的 ai-aux/pytest-tmp, 避免不同权限令牌共享系统临时目录。
2026-10-09 06:46:36 -04:00
## 一、测试约定
### 编写时机(分层对待)
- `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` 提升均使旧令牌立即失效 |
2026-10-10 04:28:04 -04:00
### 系统管理 · 用户与角色管理
| 测试文件 | 守什么 | 用例数 | 关键覆盖点 |
| --- | --- | :---: | --- |
| `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 |
2026-10-09 06:46:36 -04:00
### 应用骨架与交付产物
| 测试文件 | 守什么 | 用例数 | 关键覆盖点 |
| --- | --- | :---: | --- |
| `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 手工同步迟早漂移,且漂移后果隐蔽(开发库结构与生产不一致,上线才发现) |
2026-10-12 13:05:07 -04:00
### 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-12 23:24:29 -04:00
### 基础数据管理( 2026-10-13 完成)
| 文件 | 守什么 | 用例数 |
| --- | --- | --- |
| `tests/integration/test_personnel.py` | 未登录 401、新增默认在职、工资标准 str 直出、经理自引用/不存在或已离职/岗位停用 400、删除保护( 团队成员与客户归属引用→引导改离职) 、辐射区域与拓展计划全量替换( 含清空与详情回显) 、在职/离职切换、审计成功与失败双记录 | 20 |
| `tests/integration/test_products.py` | 未登录 401、类别值域 400、名称+规格组合唯一(同名称不同规格放行)、单价负数/非数字 400、启停切换、改类别、审计留痕 | 14 |
2026-10-22 22:59:33 -04:00
| `tests/integration/test_customers.py` | 未登录 401、客户分类值域 400、「公司名称+负责人」组合唯一(同公司不同负责人放行)、归属变更(主表更新+留痕字段断言)、归属人员离职 400、新旧相同含双方为空 400、留痕分页 join 姓名、编辑端点不触归属、审计留痕、评级保存(回显总分/时间、覆盖式保存)、评分 101→400、按评分排序未评分垫底( asc/desc) | 18 |
2026-10-12 23:24:29 -04:00
2026-10-22 22:59:33 -04:00
运行:`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-23 评级功能后客户单文件 **18 passed** ( 18.5s)。
2026-10-12 23:24:29 -04:00
2026-10-13 01:39:09 -04:00
### 业务数据管理( 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-16 13:08:31 -04:00
### M3-core ④ 核算引擎 + 月度结算( 2026-10-16 完成)
| 文件 | 守什么 | 用例数 |
| --- | --- | --- |
| `tests/unit/test_formula.py` (扩) | R 钳制(向下取整/[0,100] 封顶封底/销售额 0 防御) 、月工资三口径( INCLUDE 加/DEDUCT 减可负/EXCLUDE 仅绩效/基本缺失按 0/未知口径报错)、试用期年累计说明(未超无 note/超额精确差额)、经理 12.5%+5% 聚合(#12 暂口径) | +12 |
| `tests/integration/test_calc.py` | **金标准双类别复算到分** (市政 4000 + 轨交 0 = 折算 4000; 88/9000 约分原文落 rate_text; 绩效 39.11; R=100 应付 39.11, DB 锚定);**业务级一次性计提 #25 **(同业务第二笔回款 repeated=True 仅登记进度不新增行);已核算再核算 400; 未指定批次 400; **成本缺失行毛利按 0 计**(折算保守 − 3000、missing_cost=1) ; 负绩效照算不归零( 折算 − 2000) ; 准入( normal 403/marketing_ops 可用) ; 列表筛选; 重算( finance 授权、v2 生效 v1 保留、reason 留痕、原因缺失 400、未核算重算 400、越权 403) ; 批量逐笔成败清单 | 13 |
| `tests/integration/test_settlements.py` | 生成按人汇总+台账回填 settlement_id; 无核算记录 400; **已审定月份重生成 400**; 开关关闭提交即自动通过锁定( AP 单号/locked_at 回填);**锁定后回款禁改 400**( M2 已核算禁改防线先于锁定拦截) ; 开关开启两节点流转( finance→leader) 至锁定; #12 暂口径聚合不实现( items 按人汇总不聚合验证) | 6 |
运行:`uv run pytest tests/integration/test_{calc,settlements}.py tests/unit/test_formula.py -q` → **32 passed** ( 2026-10-16) ; 全量回归见运行记录。
2026-10-16 00:01:52 -04:00
### M3 增量批次( 2026-10-16 完成:公司信息 / 二次分配录入 / 审批框架)
| 文件 | 守什么 | 用例数 |
| --- | --- | --- |
| `tests/integration/test_settings.py` | 公司信息单行语义( PUT 只改名称不增行);**public 端点无令牌 200 且字段白名单仅 2 项**(复用 admin schema 即泄漏面);名称空 400; 非 admin 403; logo 三重校验(伪造扩展名被 magic bytes 拦 / gif 拒 / 超 2MB 拒);合法 png 上传回读 content-type; 匿名上传 401; 修改留审计日志 | 11 |
| `tests/unit/test_secondary_allocation_schema.py` | 比例边界矩阵( 0 拒 / 0.01-100 过 / 100.01 拒 / 负数拒 / **三位小数拒绝不静默舍入** / 非数字拒);人员 ID 非正拒;**创建/编辑两入参的参与人去重**( uk 前置友好提示);不同参与人合法 | 13 |
| `tests/integration/test_secondary_allocation.py` | 带方案创建详情回显(**personnel_name 冗余固化**:离职后回显有名);不建方案详情空且无 hint; 合计满额 hint 只报合计无「余额」噪音;**离职人员参与 400**「分摊参与人员不存在或已离职」;终态业务禁改方案 400; 编辑全量替换与空数组清空; **被方案引用人员禁删( RESTRICT→400) 且方案回显有名**;删除业务后方案 CASCADE 随删、人员可删 | 9 |
| `tests/integration/test_approval_flow.py` | **开关关闭(种子默认 0) : 提单即两级「自动通过」留痕** ( status=3、steps 两条 action=3、operator 记「系统」——留痕不断链) ; 方案为空不提单; 开关开启两节点流转( 1→财务通过→2→领导通过→3) ; 财务驳回整体 status=4 且意见留痕;驳回缺意见 400; **节点准入:领导/admin 不可代审财务节点( 403) **;终态单重复操作 400; 再编辑撤旧建新( 旧单 status=5 + node=0/action=4 撤回留痕) ; 模块准入( normal/marketing_ops 403, finance/leader 200) 。开关切换经参数变更批次端到端, 不直改库; 用后 conftest 恢复种子基线 | 11 |
| `tests/unit/test_param_registry.py` (改) | 注册表 16→**18 项**( V6 首例 BOOL 两个审批开关);**BOOL 校验边界专项**( 0/1 过、true/2 拒) | +1 |
| `tests/integration/test_schema_sync.py` (改) | 角色种子断言 2→**5 角色**( marketing_ops/finance/leader 入列) | 参数化 |
运行:`uv run pytest tests/integration/test_{settings,secondary_allocation,approval_flow}.py tests/unit/test_secondary_allocation_schema.py -q` → **44 passed** ( 2026-10-16) ; 全量回归 `uv run pytest -q` → **697 passed** (基线 625 + 新增 72, 5:10) 。
2026-10-19 04:33:37 -04:00
### 报表统计( M4 ⑤, 2026-10-19 后端完成)
| 文件 | 守什么 | 用例数 |
| --- | --- | --- |
2026-10-19 21:47:03 -04:00
| `tests/integration/test_reports.py` | 个人明细与台账逐笔一致 + **pageSize=1 时 summary 仍为全筛选合计** ;部门汇总团队基数=成员绩效合计、分成按参数、无成员经理出 0 值行;**无台账经理挂上级不 500、仅有调整单人员并入团队/未分配、经理个人户开关两侧口径、按经理筛选不误判其他团队下属、对账恒等式**; **应发与月结 `total_payable` 对拍(两位经理各计一次 5%)、有调整单时等于月结+调整净额、筛选态只汇总展示团队**;导出页眉 ID→名称/中文标签、回款维度 sheet 透传筛选、**导出超限与取数异常的失败审计**、非法月份查询/导出均 400; 回款统计 8 档桶恒在、空桶为 0、**二次分摊拆行时回款金额只计一次**;业务统计排除经理聚合行;普通用户 403 | 27 |
| `tests/unit/test_report_helpers.py` | 区间分桶边界与 8 档编码唯一; 标签覆盖全部枚举; Excel 模板页眉/签字栏/空数据/金额格式/合计/公式注入/维度 sheet/状态中文;**部门汇总签字稿五块顺序、零成员团队与未分配行、5% 按计入次数累计、应发合计取对账口径、聚合行舍入差单列、比例标题参数化** | 17 |
2026-10-19 06:44:15 -04:00
| `tests/unit/test_month_validation.py` | 非法月份(`2026-13`/`2026-00`/`0000-01` 等)在报表、月结、调整单 schema 入口即拒绝;可选月份空串规范为 None | 17 |
2026-10-19 04:33:37 -04:00
2026-10-19 22:10:32 -04:00
运行:`uv run pytest tests/integration/test_reports.py -q` → **27 passed** ( 2026-10-20 复核);`uv run pytest tests/unit/test_report_helpers.py tests/unit/test_month_validation.py -q` → **34 passed** ;全量 **862 passed** ( 2026-10-20, 11:32) 。
2026-10-19 04:33:37 -04:00
2026-10-19 22:10:32 -04:00
**e2e 实跑( 2026-10-20) **: `frontend/e2e/report_flow.py` 12/12 通过。回款统计改为展开目标桶后按**本次业务编号**定位下钻行(不依赖桶累计值,可重复运行);另覆盖多桶同时展开、部门下钻筛选、打印截断提示、部门汇总打印五块结构。
2026-10-19 06:44:15 -04:00
### 工作台( M4 ⑧, 2026-10-19 完成)
| 文件 | 守什么 | 用例数 |
| --- | --- | --- |
2026-10-19 21:47:03 -04:00
| `tests/integration/test_dashboard.py` | 权限 403 且文案「无权访问工作台」;总览绩效合计与台账一致 + 非法月份 400; 待办( 月结提交→待审批命中、回款未指定批次命中、**同步未建显式降级**);风险预警(审批超时命中、**参数关闭后不产生条目**);最近动态来源操作日志;**待办按角色过滤(财务/领导/多角色/admin) + 真实流转;回款逾期命中与阈值参数生效;终止业务排除;`warnings.total` 不受展示上限截断;回款趋势跨月归属** | 11 |
2026-10-19 06:44:15 -04:00
| `tests/unit/test_param_registry.py` (改) | 注册表 19→**22 项**( V10 预警规则三项);编码集合与迁移种子一致 | 改 |
2026-10-19 21:47:03 -04:00
运行:`uv run pytest tests/integration/test_dashboard.py -q` → **11 passed** ( 2026-10-19 code-review 修复回归)。
2026-10-19 06:44:15 -04:00
**实施中发现并修复**: `alembic upgrade --sql` 在 Windows 控制台按 GBK 输出,重定向生成的 `V10__*.sql` 被 `test_schema_sync` 按 UTF-8 读取时抛 `UnicodeDecodeError` ——导出命令须加 `PYTHONIOENCODING=utf-8 PYTHONUTF8=1` (已写入 backend/README) 。
2026-10-19 04:33:37 -04:00
2026-10-20 06:30:51 -04:00
### code-review 遗留 2 项( 2026-10-20 完成:方案版本化 / 审批内容快照)
| 文件 | 守什么 | 用例数 |
| --- | --- | --- |
| `tests/integration/test_approval_flow.py` (扩) | **提交时快照固化** ( version_no=1、content_source=snapshot、方案比例/合计/余额与业务编号);编辑方案为 80% 后新单 V2、旧单快照仍 50.00 且旧在途单撤回;**历史单无快照回退当前内容并标 live**( captured_at=null) ; 引用对象缺失且无快照 → missing; **方案版本化**:编辑后 history=[(1,0,50.00),(2,1,80.00)]、生效行仅 80.00; 驳回后生效行为空但历史行保留( is_active=0) | +5 |
| `tests/unit/test_approval_schema.py` | 对象类型筛选 1/2/3 均可、0/4 拒绝(修复原上界 `le=2` 导致调整单审批单无法筛选);不传类型表示全部;驳回必须填意见、通过可不填 | 6 |
| `tests/unit/test_approval_helpers.py` | **方案内容比较** ( `business_service._plan_changed`):完全相同/行序不同判未变、比例变/人员增减/同比例换人判已变、比例写法 `50` 与 `50.00` 等价、空方案对照 | 10 |
| `tests/integration/test_secondary_allocation.py` (扩) | **同一会话内旧方案行随置历史同步刷新** ( Core UPDATE 的会话同步策略退化为不刷新时,行对象会停留在 is_active=1 的过期内存值) | +1 |
| `tests/integration/test_approval_flow.py` (扩) | **送审内容未变不重提** :方案与金额都没动 → 方案不新增版本、审批单不重提(旧单仍待财务复核);**只改金额** → 方案版本不变但审批单 version_no+1 且新快照金额为改后值;**改方案比例** → 方案版本+1、审批单 version_no+1 且旧单撤回;**无在途单时保存仍提新单**(保守规则);`pending_matches_snapshot` 四态(一致/不一致/无在途单/快照为 None) | +5 |
2026-10-22 01:31:01 -04:00
### 审批单版本号并发防线( 2026-10-21 完成: V12 唯一键 + 锁读取号)
| 文件 | 守什么 | 用例数 |
| --- | --- | --- |
| `tests/integration/test_approval_version.py` (新) | **取号读最新已提交值** ( 两会话法: s1 锚定快照 + s2 提交 version_no=2 → s1 必须取到 3, 守住「快照读 vs 当前读」根因);**取号查询带 FOR UPDATE 且不含聚合**(事件钩子抓真实 SQL——MySQL 官方不允许聚合函数携带锁子句);**唯一键生效**(重复 (order_type, ref_id, version_no) 直插撞 1062) ; **编辑路径冲突收敛为 400** 且业务数据不写坏( monkeypatch `_next_version` 恒返回 1) ; **正常路径版本号仍递增**( 1→2→3) | +5 |
| `tests/unit/test_models.py` (扩) | 审批单唯一键覆盖 (order_type, ref_id, version_no) 三列且约束名与迁移 DDL 一致 | +1 |
运行( 2026-10-22, 容器恢复后) : `tests/integration/test_approval_version.py` + `test_dashboard.py` → **16 passed** ;全量 `uv run pytest -q` → **897 passed( 12:02) ** ; `uv run ruff check .` 绿、前端 `pnpm type-check` 绿。首次全量跑出两处**测试代码**适配(非产品缺陷):① 工作台 `_insert_approval` 用同一 `(order_type=2, ref_id=0)` 连插两张单,撞 V12 新唯一键 → 每张单改给不同 `ref_id` ;② 新用例跨模块引用 `switched_on` 夹具( pytest 夹具只在定义模块与 conftest 可见,报 fixture not found) → 用 `_set_switch` 本地重建。
2026-10-20 06:30:51 -04:00
2026-10-09 06:46:36 -04:00
## 三、运行记录
| 日期 | 范围 | 用例数 | 结果 | 备注 |
| --- | --- | :---: | --- | --- |
| 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 04:28:04 -04:00
| 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-12 23:24:29 -04:00
| 2026-10-13 | 基础数据集成三文件 | 48 | ✅ 通过( 27.60s) | **基础数据管理模块完成**后统一编写(人员 20/产品 14/客户 14) 。审计上下文内外的校验位置差异为本次新发现的缺陷类别( 见下) |
2026-10-13 01:39:09 -04:00
| 2026-10-13 | 业务数据集成四文件 | 70 | ✅ 通过( 54.73s,连跑两次幂等) | **业务数据管理(后端接口)完成**后统一编写(业务 25/发货 15/回款 16/评级 14) 。全量回归 603 例通过 |
2026-10-13 05:13:21 -04:00
| 2026-10-13 | 全量回归 + 两个 e2e | 605 | ✅ 605 passed( 208s) ; e2e 基础数据 13 步 + 业务全链路 16 步全过 | **M2 收尾** :删除保护补齐(+2 用例)后最终回归 605; e2e 期间修复三处缺陷(见下表),均已用例锁定 |
2026-10-13 01:39:09 -04:00
| 2026-10-13 | 全部 | 603 | ✅ 通过( 5:17) | **T11 全量回归** (单元 381 / 集成 222) , 含本模块 70 例 |
2026-10-14 22:58:44 -04:00
| 2026-10-15 | 全量回归 + e2e | 639 | ✅ 全部通过 | **code-review 32 条修复后全量回归** (新增 34 例:跨位宽 3 / 参数预览与维度 3 / 审计重试 2 / 终态编辑与 quantize 2 / 产品保护 2+6 / 评级锁 1 / formula 钳制 1 等) 。e2e 冒烟:登录 8 项 + 系统管理 13 项 + 基础数据 13 步 + 业务全链路 16 步全过(拦截器/正则收口/params 分页改造后复跑) |
2026-10-16 13:08:31 -04:00
| 2026-10-17 | M3-core 定点 | 32 | ✅ 通过 | ** ④ 核算引擎+月结完成**( calc 13 / settlements 6 / formula 扩 12 / schemas 等)。金标准复算到分;重算版本链;月结锁定与审批联动 |
| 2026-10-17 | 全量回归 | 701+N | ✅ 全部通过 | **M3-core 收尾回归** ( T1-T6: V7 四表+核算+月结+调整单;前端台账/月结两页 order17/18) 。e2e 补 calc_settlement_flow 待下批 |
2026-10-16 00:01:52 -04:00
| 2026-10-16 | 全量回归 + 7 个 e2e | 697 | ✅ 697 passed( 5:10) ; e2e 七脚本全过 | **M3 增量批次完成** ( +72 例: settings 11 / 分配 schema 13+集成 9 / 审批 11 / registry 与 schema_sync 断言扩充等) 。e2e: 登录 8 + 系统管理 13 + 基础数据 12 + 业务 16 + 参数 7 步回归全过;**新增 settings_flow 4 步 / secondary_allocation_flow 6 步**(首跑即抓出 public 缓存与 logo 清理两处缺陷,见下表) |
2026-10-19 00:30:41 -04:00
| 2026-10-19 | M3 定点( calc/settlements) | 34 | ✅ 通过( 55s) | **code-review P0+P1 修复中** :经理聚合比例/调整单即生效/进度登记重算 400/财务节点不提前锁定/锁定拦截三路径/回填与聚合落库/月末取参(+新增 `test_settlement_helpers` 5 例、`test_calc_helpers` 4 例锁定 `_q2` HALF_UP) |
| 2026-10-19 | 全量回归 | 780 | ✅ 780 passed( 8:53) | **M3 code-review 21 项修复后全量回归** ( P0 算错钱/不可用 6 项 → P1 锁定不变量与双轨漂移 7 项 → P2 健壮性 6 项 → calc_receipt 重构;新增回归约 20 例,含跨会话并入的 #12 聚合基数开关用例) 。type-check 绿; e2e calc_settlement_flow 仍待补 |
2026-10-19 01:29:37 -04:00
| 2026-10-19 | 拆行收口批次 | 全量 788 | ✅ 788 passed( 9:20) | **2026-10-19 业务答复落实完成** : #14 首日取整 10‰( 公式段/锚点不变,用例反转);#12 单笔侧开关消费( 1=业务员递减计提);**二次分摊拆行**(参与人份额+负责人余额守恒, uk 三列版) ; 权限分层( params 查看放开/写仅 admin) ; V8 迁移(审批开关默认开/撤试用期参数/增基数开关) 。定点: calc 20 / settlements 13( 含 #12 开关两档金标准 6.84→24.34) / businesses 30( 保存侧+3) / formula 35。e2e calc_settlement_flow 交接并行会话 |
2026-10-19 06:44:15 -04:00
| 2026-10-19 | **报表统计修复回归** | 全量 847 | ✅ 847 passed( 10:13) | **M4 报表 code-review 15 项修复后全量回归** (后端 8 + 前端 7) : 归属三集合/调整单全量并入/经理个人户拆分/对账出参、分块签字稿导出、导出审计覆盖取数、筛选页眉 ID→名称、月份校验 400、岗位只读选项。报表集成 24 / 报表单元 16 / 月份校验 17。type-check + build 绿; e2e `report_flow.py` 扩至 12 步,待 `E2E_ADMIN_PASSWORD` 实跑 |
| 2026-10-19 | 全部 | 852 | ✅ 852 passed( 9:17) | **M4 ⑧ 工作台完成** (四区块实时聚合 + ECharts 5.6 前端 + V10 预警参数三项;新增 5 例;参数注册表 19→22; 同步模块显式降级) 。期间修复: alembic `--sql` 导出 GBK 导致 schema 同步误报 |
2026-10-19 21:47:03 -04:00
| 2026-10-19 | **code-review 修复回归** | 全量 862 | ✅ 862 passed( 9:36) | **M4 工作台 + 报表 3 必须 + 8 建议修复** :普通用户落地页/守卫(报表集成 27、工作台集成 11、报表单元 17, 新增 10 例);部门汇总 5% 按计入次数累计并与月结 `total_payable` 对拍、聚合行舍入差;工作台待办按角色过滤、逾期命中与阈值生效、终止业务排除、`warnings.total` 截断、趋势跨月归属。ruff 绿; type-check + build 绿 |
| 2026-10-20 | e2e 全链路 | — | ✅ login 8 / system 13 / role_home 4 / report 12 / calc_settlement 8 | 重启后端加载新代码后实跑:普通用户落地页与受限页回退、报表四页/导出/打印、核算与月结链路全过。`report_flow` 回款统计改为展开目标桶后按本次业务编号定位明细,不依赖桶累计值,可重复运行 |
2026-10-19 22:10:32 -04:00
| 2026-10-20 | 全量回归 | 862 | ✅ 862 passed( 11:32) | **工作台/报表修复提交后的全量复核** ; e2e 三脚本复跑: role_home 4/4、report 12/12、calc_settlement 8/8; calc 审批开关已还原为 1, 无 `e2e_role_*` 残留 |
2026-10-20 06:30:51 -04:00
| 2026-10-20 | code-review 遗留 2 项 | 全量 875 | ✅ 875 passed( 12:13) | **方案版本化 + 审批内容快照** ( V11 迁移):审批单 `version_no` /`snapshot_json`,方案行 `is_active` 版本化;编辑/驳回不再物理删方案;历史单无快照时 live 回退并显式标注。新增集成 7 + 单元 6; 定点审批/月结/核算/业务/报表全绿; ruff 与 type-check/build 绿 |
| 2026-10-20 | 遗留项自评审收口 | 全量 891 | ✅ 891 passed( 10:23) | **列表 defer 快照列** (审批列表/工作台待办/超时预警)+ **送审内容未变不重提、方案未变不新增版本** (两个 version_no 语义解耦)+ **月结提交加行锁** 。新增单元 10 + 集成 5; 单元 525、集成定点 96 全绿 |
2026-10-22 07:43:20 -04:00
| 2026-10-22 | 清理批次(散参数 DTO 化) | 全量 900 | ✅ 900 passed( 11:08) | **仓储层散参数收敛为查询 DTO** ( `list_*(session, query)` 取代 keyword/status/offset/limit 四参数),同步调整服务层与测试调用;单元 526 绿、ruff 绿、前端 type-check 与 build 绿 |
2026-10-19 04:33:37 -04:00
| 2026-10-19 | 单元全量 + 报表定点 | 484 | ✅ 484 passed( 45.94s);报表单元 11 例 0.70s | **M4 报表模块收尾** (前端四页/打印视图/导出与单元测试补齐) 。ruff 绿;前端 `pnpm type-check` 绿。报表集成 7 例记录见上节 |
| 2026-10-19 | 全部 | 806 | ✅ 806 passed( 7:59) | **M4 ⑤ 报表统计全量回归** (报表新增 18 例:集成 7 + 单元 11; 另含并行会话并入的 M3 前端修复与字体合规) 。ruff 绿。e2e: `calc_settlement_flow` 8 步由并行会话跑通;`report_flow.py` 待 `E2E_ADMIN_PASSWORD` 运行 |
2026-10-09 06:46:36 -04:00
2026-10-23 04:23:14 -04:00
| 2026-10-23 | 参数文案中文化 | 单元 526 + 参数集成 24 | ✅ 526 passed( 45.15s) / 24 passed( 10.60s) | **计提参数页全面中文化** : `param_registry` 22 条 `description` 改写为业务语言(去掉「澄清 #10 」「M3」「禁止按 0.011111 截断」等开发者语汇),`db/init/init-dev.sql` 种子同源同步;`param_service` 模板报错改中文模板名、产品类别报错去掉英文枚举;`dashboard_service` 概览口径说明重写。**这些文案无测试断言**( `rate_text` 断言 `11/1125` 、`聚合:20%+5%` 未动) , 故用例数不变、全绿即口径未被破坏。ruff 绿;前端 `pnpm type-check` 绿 |
| 2026-10-23 | 台账/预览的计提说明口语化 | 单元 526 + 集成 87( calc/settlements/param/reports) | ✅ 526 passed( 47.29s) / 87 passed( 7:41) | ** `rate_text` /`rate_note`/聚合行 `note` 改业务话**: `企业户固定计提比例`→「企业户:不按回款天数,统一按固定比例计提」、`业务员递减: 10‰ 起, 20 天(分界 360 天…)`→「回款 20 天,从起始 10‰ 按日递减(满 360 天后固定按 4‰ 计算)」、`信息员:本人销售额 × 计提比例`→「信息员:按本人销售额乘计提比例,不随回款天数递减」、聚合行 `#12 参数化: 基数不含经理个人户业绩; 9.8 方案公式` →「团队分成:基数不含经理本人名下业绩,按本团队与全公司两级比例合计计提」。**用例同步**: `test_经理聚合口径_预览拒绝` 原断言消息含「聚合」,改为断言含「营销经理」+「月度结算」(守「拒绝且给下一步」,不再绑定措辞);`rate_text` 的 `11/1125` 、`聚合:20%+5%` 断言未动(那是分数原文与百分比拼装,非说明文案) |
2026-10-09 06:46:36 -04:00
## 四、测试发现并修复的缺陷
| 日期 | 缺陷 | 影响 | 处理 |
| --- | --- | --- | --- |
2026-10-19 06:44:15 -04:00
| 2026-10-19 | 部门汇总导出/打印只展平成员行 | 团队分成、全公司 5% 块、未分配人员、零成员团队全部缺失——**签字稿合计小于应发**,财务按签字稿复核会漏发经理分成与未分配绩效 | 导出改为单 sheet 分块签字稿(团队/公司/对账/成员/未分配)+ 打印五块;应发公式与对账块由后端 Decimal 计算 |
| 2026-10-19 | `_member_rows` 直接 `member_map[pid]` 索引 | 无台账行的营销经理自身挂上级时 KeyError → **部门汇总接口与导出 500** (当月完全无台账也会触发) | 归属只从 member_map/adjustments 生成;`manager_role` 覆盖所有成员引用的上级 |
| 2026-10-19 | 生效调整单只并入有台账人员 | 只有调整单、当月无回款的人被静默丢弃——补发/扣减金额不出现,**按报表发放会少发** | 调整单人员补零值成员后参与团队/未分配归属;每人调整净额只计一次 |
| 2026-10-19 | 经理个人户成员行取该人全部绩效 | 团队基数只含个人户行,成员行却含普通行——**基数≠成员合计,同一笔业绩在成员行与未分配重复** | 普通行与个人户分行记账;经理成员行只取个人户金额,普通行留在上级团队/未分配 |
| 2026-10-19 | 导出取数与超限校验在审计上下文之外 | 超限 400 与取数异常**不留失败日志**,与 M4 计划 T12「导出 400/失败审计」不符 | `_export_payload` 移入 `with audit` ; 静态标题映射( AuditRecord frozen) |
| 2026-10-19 | 月份只校验长度 | `2026-13` 传到 `calendar.monthrange` 抛 ValueError → **500** ;导出手工构造模型抛裸 ValidationError 同样 500 | 新增 `core/month.py` (格式+01-12+年份);四类 query、月结、调整单接入; 导出 ValidationError 转 400 |
2026-10-20 06:30:51 -04:00
| 2026-10-20 | 审批类型筛选上界写死 2 | `ApprovalQuery.order_type` 为 `le=2` : 调整单( order_type=3) 审批单在列表无法筛选, 只能翻全量 | 上界改 3; 单元用例锁定 1/2/3 与越界拒绝 |
| 2026-10-20 | 编辑业务时送审快照取旧汇总 | `update_business` 中 `_recalc_totals` 在提单之后,快照里的销售额/成本额会固化上一版本金额 | 汇总重算移到提单之前;快照用例断言送审内容与本次编辑后方案一致 |
| 2026-10-20 | 快照金额把「空」写成 0.00 | 业务尚无成本时 `total_cost_amount=None` 被文本化成 `0.00` ,复核者会把「无成本」误读成「成本为零」 | `_amount_text(None)` 保留为空,前端显示「—」 |
| 2026-10-20 | 每次保存带方案的业务都刷出版本与审批留痕 | 只改备注、方案一字未动,也会「置历史+新增版本+撤回旧单+提新单」,方案历史与审批留痕被噪音淹没,两个 `version_no` 语义混同 | 判据统一为**送审内容**:方案未变不新增版本行;在途单送审内容一致则不重提(`pending_matches_snapshot`,只比 `data` ) |
| 2026-10-20 | 三处列表查询整列拉快照 | 审批单列表、工作台待办、审批超时预警都用 `select(ApprovalOrder)` 取全列,快照 JSON 可达上百 KB, 翻页与工作台负担随快照体积线性放大 | 三处加 `defer(ApprovalOrder.snapshot_json)` (沿用操作日志列表排除 `request_data` 的既有模式) |
| 2026-10-20 | 月结提交未锁行 | 并发/双击提交同一月结时两次 `_next_version` 可能算出同一 `version_no` ,送审次序失真 | `settlement_repository.get_by_id` 加 `for_update` 参数,提交路径锁行 |
2026-10-22 01:31:01 -04:00
| 2026-10-21 | 审批单 `version_no` 无唯一约束 | 应用层取号即使正确,并发下 DB 层无兜底;且唯一键是「一个对象至多一张在途单」红线之外版本语义的最后防线 | V12 迁移加 `uk_approval_orders_order_type_ref_version (order_type, ref_id, version_no)` ; 加键前先重排存量重复分组( V11 把存量一律置 1 而 V11 前重提是新建行,直接加键必报 1062) |
| 2026-10-21 | `_next_version` 是快照读 | 请求级鉴权与业务共用同一 DbSession, 鉴权的普通 SELECT 已锚定 REPEATABLE READ 快照——父行锁只串行化写入时刻、**不刷新快照**,并发提单的 MAX 停在请求开始时刻 → 版本号重复 | 取号改锁读(`select(version_no)...with_for_update()`,当前读),最大值 Python 侧算;**不能用聚合 + FOR UPDATE**——MySQL 官方不允许聚合函数携带锁子句 |
| 2026-10-21 | `act()` 驳回清空方案的注释与行为相反 | 注释写「清空该业务的二次分配行…如需保留历史版本,在 ④ 设计时演进」,而 V11 已实现 append-only( 只置 `is_active=0` 行不删)——误导后来者按旧语义改代码 | 注释改为「置历史不删行,历史方案可回溯」 |
2026-10-19 06:44:15 -04:00
| 2026-10-19 | 前端报表五处状态缺陷 | 打印超限判定恒假(静默截断)、部门下钻 query 未读取、回款多桶共用一个 drill 状态( 面板串行) 、summary 在请求序号守卫前写入( 合计串号) 、e2e 全页行数选择器必失败 | 分别修正判定/读取 query/按桶独立状态+数组参数/`onData` 回调/限定主桶表;补 e2e 多桶展开、下钻、截断、部门打印用例 |
2026-10-19 21:47:03 -04:00
| 2026-10-19 | 工作台权限收紧后守卫仍回退 `/dashboard` | 普通用户登录/直连受限页被反复重定向到同一受限页 → **无法进入系统** (导航被判定无限重定向) | `router/access.ts` 统一准入与落地页解析(工作台 → 业务数据 → 回款);登录回跳经 `canAccessPath` 校验;菜单/面包屑复用同一判定 |
| 2026-10-19 | 部门汇总应发只计一次全公司 5% | 月结每位有聚合行的经理各计一次 5%,签字稿少计 N-1 份;**导出/打印金额小于月结应发** | 团队行增 `has_aggregate/aggregate_amount` (优先冻结聚合行);对账改按聚合额求和,新增「聚合行舍入差」吸收分列 q2 尾差;补「与 `total_payable` 对拍」用例 |
| 2026-10-19 | 工作台待办未按角色过滤 | 财务看得到「待领导审核」、领导看得到「待财务复核」,与 FR-034「仅看到与自己职责相关」不符 | `pending_statuses_for_roles` 按角色映射在途节点; repository 参数化 statuses; admin 保留全部在途视角 |
2026-10-09 06:46:36 -04:00
| 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 04:28:04 -04:00
| 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-12 23:24:29 -04:00
| 2026-10-13 | 基础数据三 service 的**前置校验在审计上下文之外** | 校验失败(岗位停用/经理自引用/分类不合法等 400) 完全不留操作日志, 违背「成功与失败都记」的既定原则——与 2026-10-10 审计时序缺陷同源,新写的 service 又犯了(各自独立实现时口径漂移) | personnel/product/customer 三 service 的值域与引用校验统一移入 `audit_service.audit` 上下文内;集成用例锁定(审计成功与失败双记录) |
2026-10-13 01:39:09 -04:00
| 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 05:13:21 -04:00
| 2026-10-13 | **客户/人员删除保护漏检业务引用** ( T4 占位注释「待业务表就位后补充」在 T7 交付后无人补齐) | 被业务引用的客户、被业务归属(负责/协助)引用的人员删除 → FK RESTRICT 穿透成 **500** 。**由 e2e 清理步骤暴露**(集成测试只覆盖了 T2/T4 当时的引用面,业务表就位后引用面扩大未回看) | customer 删除加 `count_business_refs` 检查 + IntegrityError 兜底 400; personnel 删除加 `count_business_roles_refs` (负责/协助合并统计);补 2 个集成用例锁定;文案更新「客户归属或业务归属引用」 |
| 2026-10-13 | **单价列( DECIMAL(14,4))被两位小数校验拒收** (前后端同源) | 编辑任何带精度的历史单价(回显 "5000.0000")被前端 `AMOUNT_PATTERN` 与后端 `_check_amount` 双双拒绝——**创建后单价带小数位的记录永远无法编辑**。**由 e2e「编辑业务」步骤暴露**(创建用整数、编辑回显带 4 位小数,恰好只在这条路径相遇) | 后端 `_check_amount` 加 `scale` 参数(金额/数量 2 位、单价类 4 位);前端业务/发货明细行单价类放行 4 位;集成用例锁定 |
2026-10-13 01:39:09 -04:00
| 2026-10-13 | 发货 `freight_amount` 漏做金额校验 | 负数/三位小数/超容量的运费直接落库( schema 对 quantity/sale_price 都复用了 `_check_amount` ,唯独运费没套用),业务运费合计与报表随之错乱 | shipments.py schema 的 Create/Update 加 `field_validator("freight_amount")` (同套口径,文案「运费金额不能为负数 / 最多两位小数」);集成用例锁定 |
2026-10-14 22:58:44 -04:00
| 2026-10-15 | **计算预览把 ‰ 存储值当纯比例相乘** (企业户/信息员分支) | `enterprise.rate` =3( 3‰) 、`info.rate`=1( 1‰) 未经 ÷1000 直接乘——绩效金额放大 1000 倍( 15000× 3=45000) 。**既有测试只覆盖业务员路径**( `salesperson_rate` 内部有 ÷1000) , 两分支零覆盖使缺陷存活; 该预览是 M3 核算共用契约 | 两分支显式 `/1000` 并注释声明换算;补企业户/信息员精确数值用例锁定 |
| 2026-10-15 | **编号撞唯一键的重试写在 audit 上下文内 `return _retry_create`** (业务/发货/回款三处同构) | 吞异常后正常退出被记 `success=True` (假成功审计,业务实际已回滚);重入整个 `create_*` 违反「不支持嵌套」约定;`_NO_RETRY=1` 死常量致持续冲突时**无界递归**至 RecursionError | 改为 audit 上下文内 for 循环(上限 `_NO_RETRY` ) , rollback 后重新锁行与取号;新增集成用例锁定「成功/最终失败各恰好一条日志」 |
| 2026-10-15 | **单号 MAX 检索用 VARCHAR 字典序** ( `no_generator`) | 序号破 4 位宽度( 9999→10000) 后字典序反转( "-10000"< "-9999") , MAX 恒取旧值 → seq 恒重算同一号码 → 当日该前缀所有建单持续撞唯一键 | 排序改「长度优先、同级字典序」(同前缀下长度大即数值大);跨位宽/同宽/空表三用例锁定; param 版本号一并复用 |
| 2026-10-15 | **编辑业务全程无锁读 + 无终态门槛** | ① 「有发货禁换客户」走无锁快照读,与发货/回款写路径( FOR UPDATE) 不互斥——并发新增发货可旁路保护; ② 已完成/已终止业务仍可全量改明细并重算 M3 核算基线(发货/回款服务禁写终态,父单据反而放行) | `get_with_names` 加 `for_update` ; update 补终态门槛;前端编辑按钮终态禁用;`has_docs` 单点收口( update/delete 共用) |
| 2026-10-15 | **删除产品无引用保护** ( docstring「待 V5 补充」在 V5 就位后未兑现) | 被业务/发货明细引用的产品删除 → FK RESTRICT 穿透成 500( 与 2026-10-13 客户/人员删除保护漏检同源——「占位注释交付后无人回看」第二次发生) | `count_refs` (两明细表)+ IntegrityError 兜底 400; **全局注册 IntegrityError handler** 兜底未来遗漏(漏网的引用冲突收敛 400 而非 500) |
| 2026-10-15 | **产品 schema 私写校验不查小数位与容量** | `sale_price` 用 `_parse_non_negative` (非负/有限) , 5 位小数被 Numeric(14,4) **静默截断** (舍入即算错钱)、超容量 500——与 fba8cfc 修掉的「schema 小数位口径」同源残留 | 复用 `_check_amount` ( PRICE_MAX+scale=4 / AMOUNT_MAX) ; 6 个秒级 schema 单测锁定 |
| 2026-10-15 | 行金额乘积( 数量2位× 单价4位=6位小数) 直写 DECIMAL(14,2) | MySQL 静默舍入且回显漂移:创建响应读内存值 "5.166615"、复查读库值 "5.17"——同一行金额前后显示不一致 | 显式 `quantize(HALF_UP)` 固化(与 MySQL 舍入方向一致);回显/库值一致性用例锁定 |
| 2026-10-15 | 先款后货( payment_days 为负)计提比例线性外插 | 比例 = 起始 + \|天数\|×斜率,**高于起始值甚至超 100%**——M3 接入真实回款后直接多算绩效工资(预览被 schema 拦住、回款登记不校验日期先后,缺陷在 M3 前不可见) | `salesperson_rate` 钳制 days≥0( 按起始比例计提; payment_days 出参仍照实输出负值,口径经用户 2026-10-15 裁决) |
| 2026-10-15 | 评级区间重叠先查后写无锁、无唯一键兜底 | 两管理员并发提交重叠区间互判不重叠而双双入库——同一总分命中多等级且结果取决于插入顺序 | 判重叠的 `list_all` 加 FOR UPDATE 锁全表行串行化;只读直出不加锁 |
| 2026-10-15 | 岗位维度值大小写校验( ci) 与快照匹配( 精确) 不对称 | API 提交 "SalesPerson" 校验通过入库,快照字典按精确键取值不命中——**静默按错误参数算钱且无报错**; param_code 变形大写则 get_spec 抛 ValueError 成 500 | 校验命中后一律以库里规范值落库( role_code/definition.param_code) ; 批量预载字典键 casefold 归一保持等价命中 |
2026-10-16 00:01:52 -04:00
| 2026-10-16 | **logo 上传同扩展名覆盖时先写后删** | `save_logo` 在写入新文件**之后**清理旧扩展名——同扩展名重传时把刚落盘的新文件一并删掉,回读 404( 首次单跑通过、集成连跑即暴露, 依赖「库里已有同扩展名旧记录」的前置状态) | 清理旧扩展名移到写新文件**之前**且仅异扩展名执行;集成连跑两遍幂等锁定 |
| 2026-10-16 | **public 端点 300s 响应缓存让登录页拿旧公司名** | 公司名保存后退出登录,登录页命中浏览器缓存副本仍显示旧品牌名(设置页「保存后生效」承诺失效)——**由 settings_flow e2e 首跑暴露**(新标签页直接打开正常、退出回跳的登录页异常) | 登录页改用带时间戳的原生 `fetch` + `cache: no-store` 绕过磁盘缓存( axios 对响应 max-age 无能为力);手动解包统一信封 |
| 2026-10-16 | V6 迁移 downgrade 保留 autogenerate 的逐个 `drop_index` 且漏删行级种子 | 外键唯一支撑索引单独删除报 MySQL 1553 回滚中断; roles/param_definitions/param_values 的行级种子(表由 V1/V3 建) drop_table 不会带走——重放 upgrade 时唯一键冲突 | downgrade 只保留 `drop_table` ( V1/V3 先例)+ 三条 DELETE 反向清理行级种子; downgrade→upgrade 往返实测通过 |
2026-10-19 00:30:41 -04:00
| 2026-10-19 | **经理团队聚合分成放大 100 倍** ( settlement_service) | 参数按百分数存储( 12.5=12.5%) , `parse_fraction` 后调用方又 × 100、`manager_team_share` 内部再 ÷100——净效应「绩效× 12.5」;单元测试直传 Fraction 未覆盖调用方换算使缺陷存活 | 去掉调用方 × 100; `rate_text` 改用实际参数原文( 原硬编码默认值失真) ; 集成用例两档金标准( 6.84/9.78 vs 错误值 684.43)锁定 |
| 2026-10-19 | **审批联动不判终审** ( approval_service.act) | 财务节点通过( 1→2) 即 mark_locked 月结/调整单置生效——领导驳回后 locked_at/locked_by 残留、两级审批形同虚设(锁定后源数据写路径立即被拦) | 生效类联动收敛到 `final_approved` ( 终态才触发) ; 驳回任意节点即时; mark_rejected 补清锁定字段;用例断言财务通过后仍 status=2 且无锁定信息 |
| 2026-10-19 | **调整单创建完全不可用** (两处叠加) | ① `ENABLE_CODE_BY_TYPE` 漏登记 type=3 → is_enabled 恒 400「未知的审批对象类型」; ② 修复①后暴露:自动生效分支手工插 `ApprovalStep(order_id=调整单id)` ,而该外键指向 approval_orders, 必然 errno 1452 | 补登记 `approval.adjustment.enabled` ;自动通过留痕统一交 `submit_order` (不再手工插步);新建调整单集成用例锁定 |
| 2026-10-19 | **重算绕过业务级一次性计提防线** | 「进度登记」回款(#25 第二笔及以后)无独立台账行,重算会凭空落一行生效行——同业务两行生效行、绩效按两倍计入工资 | 无旧版本行一律 400「进度登记无可重算」; 用例锁定「重算后同业务仍只有一行生效行」 |
| 2026-10-19 | **前端重算传错 ID** (台账页) | 把台账行主键当 receipt_id 传给重算接口——会作废并重算**无关回款**的台账(若目标回款已核算),或报错无法重算目标行 | 改传 `row.receipt_id` (聚合行 receipt_id 空值防御);`LedgerRow` 本含该字段 |
| 2026-10-19 | **quantize 缺省银行家舍入** ( 9 处金额固化) | docstring 声称 HALF_UP, 实际未传 rounding 走 ROUND_HALF_EVEN——x.xx5 舍成偶数尾数,与业务四舍五入口径差 1 分(台账固化金额比应发少 1 分) | 9 处显式 `rounding=ROUND_HALF_UP` ; `_q2` 提为模块级并单测锁定( 2.345→2.35) |
| 2026-10-19 | **月结锁定拦截只覆盖 1/4 写路径** | 仅新增回款被拦;回款编辑(可跨月搬数)/删除、发货增/改/删、核算与重算全部放行——锁定月份快照与源数据脱钩且无留痕 | 五处写路径补齐 `ensure_month_not_locked` (编辑按原月+新月双查);拦截先于「被引用/已核算」判定;三用例锁定 |
| 2026-10-19 | **台账 settlement_id 回填从未发生** (死代码) | `update_ledger_settlement_id` 全库零调用——「未入结」判据恒真,重生成会把已固化行反复纳入(或补上回填却不先解绑导致漏算整月) | generate 回填 + 重生成前解绑(新增 `clear_ledger_settlement_id` );用例断言回填与重生成金额不翻倍 |
| 2026-10-19 | **经理聚合行从不落库** | `_build_manager_aggregates` 只构造内存对象参与 items 汇总——台账表永远无 calc_type=2 行, 报表下钻( business_id 锚点)落空 | 聚合行经 repository 落库并回填 settlement_id; 重生成先作废旧聚合行( `deactivate_month_aggregates`) ; `_confirm_scope` 限定 calc_type=1 防聚合行混入单笔池 |
| 2026-10-19 | **月结取参用生成日** ( date.today) | 补生成历史月份时经理分成/月工资口径/试用期阈值按生成日参数计算——同一月份两次生成金额不同,违背「参数按生效日期时点化」红线 | 新增 `_month_end` (所属月月末)为聚合与月工资口径取参锚点;单测 5 例 + 集成用例( 10-25 预约变更影响 10 月聚合) |
| 2026-10-19 | **迁移双轨三处漂移** | ① V6 就地修改加入 uk 致旧库补不上( V8 也未补)→ V9 条件式补齐(幂等裸 SQL, 临时库验证两分支) ; ② V7 留存 SQL 约束/索引名单数(`fk_performance_ledger_*`)与 alembic op.f/init-dev 漂移;③ V8 迁移 revision 缺类型注解,`test_schema_sync` 的 head 推断**静默漏检 V8**、init-dev 版本号落后未被发现 | V9 迁移 + V7 SQL 名修正 + V8 注解修正 + init-dev 版本推进; schema 同步测试恢复全量断言 |
| 2026-10-19 | 运维脚本两处( restore_drill/health_check) | ① 行数核对把查询失败回显 `'?'` 当数据比较(`'?'=='?'` 判 ✓)且 TABLES 缺 V7 四表等 16 张——备份漏表时演练全绿误报「可恢复」;② `redis-cli -a "$REDIS_PASSWORD"` 密码进宿主进程命令行 | TABLES 补全 32 张业务表 + `'?'` 一律判 ✗; Redis 探活改容器内环境变量展开( compose 补 REDIS_PASSWORD) , 实测 PONG |
| 2026-10-19 | 调整单金额无校验 / INT 参数原文 `int()` / logo 清理裸抛 | ① amount 负数反转补发-扣减语义、'abc' 直落库 500; ② `int('360.0')` 使整月核算 500( INT 参数合法入库原文);③ 旧 logo 清理 OSError 裸抛把成功更新记成失败审计并 500 | schema 正则 + 服务层 Decimal>0; `int(parse_fraction(...))`;清理包 try 仅 warning; 各补回归用例 |
2026-10-09 06:46:36 -04:00
## 五、待补用例
| 模块 | 状态 | 说明 |
| --- | --- | --- |
2026-10-13 01:39:09 -04:00
| 基础数据管理 | ✅ 已完成 | 2026-10-13 完成( 48 例,见上表);「被业务数据引用」的删除保护已随 T11 的业务/发货禁删用例覆盖 |
| 业务数据管理 | ✅ 后端已完成 | 2026-10-13 完成( 70 例, 见上表) ; 前端页面与绩效核算联动( M3 台账核销回写 settlement_status) 待 M3 补测 |
2026-10-16 00:01:52 -04:00
| M3 增量(设置/分配/审批) | ✅ 已完成 | 2026-10-16 完成( 44 例,见上表);审批流的**月结审批对象**( order_type=2) 随 ④ 月结子块接入时补测 |
2026-10-19 22:10:32 -04:00
| 绩效工资核算( M3-core) | ✅ 已完成 | 2026-10-17 完成(核算/月结/调整单, 见运行记录) ; 2026-10-19 code-review 21 项修复并补回归约 20 例; 2026-10-20 e2e `calc_settlement_flow` 8/8 实跑通过(台账/月结真实浏览器链路) |
| 报表统计 | ✅ 已完成 | 2026-10-19 完成并补齐回归; 2026-10-20 e2e `report_flow` 12/12 实跑通过,全量 862 passed |
2026-10-10 04:28:04 -04:00
> 系统管理模块(用户/角色/日志查询)已于 2026-10-10 完成并补齐用例,见上表。