Files
dealerhub/PROGRESS_AGI_ITERATION_1.md

9.9 KiB
Raw Permalink Blame History

dealerhub · AGI 自主迭代报告(第 1 轮)

触发:用户要求"agi模式自己测试迭代"。 方法:不写新功能,而是系统性攻击自己的交付物——全路由遍历、API 权限矩阵、 边界模糊测试、并发不变量、迁移一致性。目标是找出"我之前说的没问题"里没被验证的部分。 结果:发现并修复 9 个真实缺陷(含 1 个资损级漏洞 + 1 个漏钱缺口), 新增 3 个测试模块(+45 用例)与 2 个可复用审计脚本。 回归:393 passed / 2 skipped(PG 专用),check 0 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 例

七、方法论沉淀

  1. "能跑通" ≠ "没漏洞"。前几轮我用 curl 验证了"正常路径能跑", 但畸形输入完全没测——8 个缺陷都是畸形输入触发的。
  2. 守门测试必须先验证它能失败。pytest 版迁移检查假通过两次, 教训是:任何"守卫"都要用注入故障的方式证明它有效,否则是安慰剂。
  3. 同一根因要一次性收敛。数量/单价/抹零三个漏洞都源于"只转换不校验", 集中在 core/services 修,销售/采购/商城三条业务线同时受益。
  4. 资损级校验要单独标记。数量与单价的非负校验不是"输入合法性"问题, 是"防止凭空造库存/倒贴出货"的资金安全问题,值得在代码里显式写明。

八、下一轮迭代候选

优先级 项目 说明
P0 真实 PostgreSQL 并发验证 起 PG 容器跑 -m postgres 用例,验证行锁下的超卖防护
P0 前端表单侧校验 后端已拦,但前端应即时提示(避免用户填完才 400)
P1 全局异常处理器 把 InvalidLineQuantity 等统一映射,减少每个视图的 try/except
P1 审计日志接入 关键写操作(过账/改价/放行)记 AuditLog,支持事后追溯
P2 性能基线 万级商品/单据下的列表与报表响应时间
P2 打印模板注入面复测 用户可编辑模板,需再确认 {{#each}} 嵌套不引入资源耗尽