9.9 KiB
dealerhub · AGI 自主迭代报告(第 1 轮)
触发:用户要求"agi模式自己测试迭代"。 方法:不写新功能,而是系统性攻击自己的交付物——全路由遍历、API 权限矩阵、 边界模糊测试、并发不变量、迁移一致性。目标是找出"我之前说的没问题"里没被验证的部分。 结果:发现并修复 9 个真实缺陷(含 1 个资损级漏洞 + 1 个漏钱缺口), 新增 3 个测试模块(+45 用例)与 2 个可复用审计脚本。 回归:393 passed / 2 skipped(PG 专用),
check0 issues,迁移全绿。
一、发现的缺陷清单
| # | 缺陷 | 严重度 | 影响 | 状态 |
|---|---|---|---|---|
| 1 | 负数量/零数量可建单 | 🔴 资损级 | 负数量出库 = 凭空增加库存(反向出库);0 数量产生空单 | ✅ 已修 |
| 2 | 负单价可建单 | 🔴 资损级 | 倒贴出货,单据金额为负 | ✅ 已修 |
| 3 | 6 个页面 500(dev 库缺 tax_rate 列) |
🟠 阻断 | 商品/库存/批次/销售/采购/商城订单页全部打不开 | ✅ 已修 |
| 4 | notify.AlertRule.rule_type 漏生成迁移 |
🟠 隐患 | B1 的 risk_score 规则类型未落库,风控预警可能失效 |
✅ 已修 |
| 5 | page=0/page=-1 → 500 |
🟠 稳定性 | 负索引切片抛 ValueError | ✅ 已修 |
| 6 | page_size=0/-5 → 异常 |
🟡 稳定性 | 空切片/异常 | ✅ 已修 |
| 7 | lines 传非数组 → 500 |
🟠 稳定性 | 'str' object has no attribute 'get' |
✅ 已修 |
| 8 | lines 元素非 dict → 500 |
🟠 稳定性 | 同上,在 _load_refs 阶段崩 |
✅ 已修 |
| 9 | round_to 任意值被接受 |
🟡 数据正确性 | 抹零精度未白名单校验,非法值参与金额计算 | ✅ 已修 |
| 10 | 超大数量(21 位) | 🟡 稳定性 | Decimal 精度/计算异常风险 | ✅ 已修(上限 10 亿) |
注:1/2/9/10 都属同一根因——
resolve_line_quantity/resolve_line_price完全没有输入校验, 只做了类型转换。修复集中在这两个函数,所有业务线(销售/采购/商城)同时受益。
二、修复内容
1. 输入校验层(apps/core/services.py)
新增三个校验异常与配套解析器,收敛为单一入口:
parse_quantity(product, raw) # 非空 / 数字 / 正数 / ≤10 亿 / 有限数
parse_price(product, raw) # 数字 / ≥0 / ≤10 亿 / 有限数
resolve_round_to(raw) # 白名单 {0.01, 0.1, 1},其余拒绝
异常类型:InvalidLineQuantity / InvalidLinePrice,均携带 product_code + reason,
视图层翻译为 400 + code(前端可判别、可提示具体哪一行错)。
2. 视图层结构预校验(销售/采购建单)
在触碰数据库前先校验 lines 的结构(数组 + 每项是 dict + 有 product),
避免下游 .get() 抛 AttributeError → 500。
3. 分页防护(apps/core/viewset.py)
page < 1 归一为 1;page_size < 1 回落默认值。负索引不再进入切片。
4. 迁移补齐
catalog/sales/purchase的税率迁移:之前只生成没应用(dev 库缺列 → 6 页面 500)notify/0003_alter_alertrule_rule_type:B1 加risk_score时漏了 makemigrations
三、新增的审计工具(可复用)
scripts/check_migrations.py —— 迁移健康检查
四项检查:待生成迁移 / 未应用迁移 / 模型字段 vs 真实库列 / 关键表存在性。
为什么必须是脚本而不是 pytest:我一开始写成 pytest 用例,然后用"注入一个未迁移字段"
来验证它能否抓到——结果两次都假通过。原因是 pytest-django 的测试库按模型建表
并绕过迁移状态,MigrationAutodetector 在 pytest 进程里返回空。
改成独立脚本后,同样的注入被立刻抓到:
[1] 待生成的迁移(makemigrations --check)
✗ 存在未生成的迁移:
Migrations for 'catalog':
apps\catalog\migrations\0004_product_probe_field.py
+ Add field probe_field to product
这是本轮最重要的方法论收获:守门测试必须先验证它能抓到问题,否则就是一个 不报错的摆设。项目里已经有两个这样的反面例子(pytest 版迁移检查、最初的二维码假设)。
scripts/audit_api_permissions.py —— API 权限矩阵探测
枚举全部 URL(137 个可达端点),用四种身份发 GET,汇总状态码矩阵,标记异常:
匿名状态码分布:{200: 2, 400: 2, 401: 31, 403: 3, 404: 95, 405: 4}
正常请求状态码分布:{200: 24, 400: 3, 401: 2, 403: 3, 404: 95, 405: 10}
结论:权限边界干净 —— 匿名只有 2 个公开端点可访问(ping / billing-plans), 31 个被 401 拦,无越权、无 500。 被 JWT 拒绝的 5 个端点都是正确行为(openapi 用 API Key、storefront 用商城 token)。
四、新增测试模块
| 模块 | 用例 | 覆盖 |
|---|---|---|
test_migration_integrity.py |
5 | 模型字段可查询性、新 app 表存在性、迁移目录结构 |
test_concurrency.py |
11(2 skip) | 库存不超卖/不为负、过账幂等、失败整单回滚、F() 自增不丢更新、可用量锁定 |
test_boundary_inputs.py |
33 | 数量/单价/抹零/分页/畸形 lines/SQL 元字符/超长输入/AI 接口/商城接口 |
并发测试的设计取舍:SQLite 用数据库级写锁(实测 database table is locked),
真并发在测试里不可靠。因此拆成两层:
- 可跑的:顺序化的"超量请求"(等价于并发中第二个请求的语义)+ F() 表达式不丢更新
- PG 专用:
@pytest.mark.postgres多线程用例,默认 skip,连 PG 时验证真并发
五、验证证据
修复前后对比(真实 HTTP,非单元测试)
=== 修复前:负数量会 201 建单(资损漏洞)===
=== 修复后验证 ===
quantity=-1 → HTTP 400
quantity=0 → HTTP 400
quantity=abc → HTTP 400
quantity=999999999999999999999 → HTTP 400
unit_price=-5 → HTTP 400
unit_price=abc → HTTP 400
round_to=abc → HTTP 400
page=0 → HTTP 200 (修复前 500)
page=-1 → HTTP 200 (修复前 500)
page=abc → HTTP 200
page_size=-5 → HTTP 200 (修复前异常)
资损漏洞封堵确认
库存基线 on_hand = 74.0000
尝试用负数量过账 → HTTP 400
库存复查 on_hand = 74.0000
✓ 库存未被篡改(资损漏洞已封堵)
全路由遍历(修复后)
products rows=3 hasError=false
stocks rows=2 hasError=false
batches rows=2 hasError=false
sales-bills rows=3 hasError=false
purchase-bills rows=1 hasError=false
storefront-admin rows=2 hasError=false
errorCount: 0
回归
$ python -m pytest tests/ -p no:cacheprovider
393 passed, 2 skipped in 15.56s
$ python manage.py check
System check identified no issues (0 silenced).
$ python scripts/check_migrations.py
✓ 无待生成迁移 · ✓ 所有迁移已应用 · ✓ 51 个模型字段一致 · ✓ 7 张关键表可查
六、变更文件
backend/
├── apps/core/services.py [改] +parse_quantity/parse_price/resolve_round_to
│ +InvalidLineQuantity/InvalidLinePrice
├── apps/core/viewset.py [改] 分页 page/page_size 下界防护
├── apps/sales/views.py [改] lines 结构预校验 + 数量/单价异常 → 400
├── apps/purchase/views.py [改] 同上
├── apps/sales/services.py [改] round_to 白名单校验
├── apps/storefront/services.py [改] lines 结构校验
├── apps/website/migrations/ [新] 目录 + __init__(结构一致性)
├── apps/website/models.py [新] 说明模块(无模型)
├── apps/notify/migrations/0003_* [新] rule_type 选项补齐
├── scripts/check_migrations.py [新] 迁移健康检查(4 项)
├── scripts/audit_api_permissions.py [新] API 权限矩阵探测
└── tests/
├── test_migration_integrity.py [新] 5 例
├── test_concurrency.py [新] 11 例(2 PG-skip)
└── test_boundary_inputs.py [新] 33 例
七、方法论沉淀
- "能跑通" ≠ "没漏洞"。前几轮我用 curl 验证了"正常路径能跑", 但畸形输入完全没测——8 个缺陷都是畸形输入触发的。
- 守门测试必须先验证它能失败。pytest 版迁移检查假通过两次, 教训是:任何"守卫"都要用注入故障的方式证明它有效,否则是安慰剂。
- 同一根因要一次性收敛。数量/单价/抹零三个漏洞都源于"只转换不校验",
集中在
core/services修,销售/采购/商城三条业务线同时受益。 - 资损级校验要单独标记。数量与单价的非负校验不是"输入合法性"问题, 是"防止凭空造库存/倒贴出货"的资金安全问题,值得在代码里显式写明。
八、下一轮迭代候选
| 优先级 | 项目 | 说明 |
|---|---|---|
| P0 | 真实 PostgreSQL 并发验证 | 起 PG 容器跑 -m postgres 用例,验证行锁下的超卖防护 |
| P0 | 前端表单侧校验 | 后端已拦,但前端应即时提示(避免用户填完才 400) |
| P1 | 全局异常处理器 | 把 InvalidLineQuantity 等统一映射,减少每个视图的 try/except |
| P1 | 审计日志接入 | 关键写操作(过账/改价/放行)记 AuditLog,支持事后追溯 |
| P2 | 性能基线 | 万级商品/单据下的列表与报表响应时间 |
| P2 | 打印模板注入面复测 | 用户可编辑模板,需再确认 {{#each}} 嵌套不引入资源耗尽 |