1
0
forked from erp-dev/erp
Files
erpnew/docs/statements_pagination_performance_evaluation.md
2026-06-06 14:14:50 +08:00

6.4 KiB
Raw Blame History

客户对账单 API 分页与性能评估

本文档评估当前 /api/v1/customers/<id>/statements/ 接口的分页能力与性能瓶颈,供前后端共同讨论优化方案。


1. 当前现状

接口行为

  • 每次请求返回该客户的全部对账记录(销售单 + 退货单 + 收款单 + 外部业务依据)
  • 无分页参数,无日期过滤
  • 排序固定:occurred_at → recorded_at → source_id 倒序
  • 每条记录包含 cumulative_amount(滚动累计)和 arrears_amount(实时欠款)

数据规模参考

客户 销售单 退货单 收款单 总记录数 响应体积(估算)
木棉 ~200 1 ~50 ~250 ~100KB
腾飞纺织 ~1,500 9 ~92 ~1,600 ~800KB
张晓鹏 ~3,100 6 ~219 ~3,350 ~2MB

2. 为什么当前无法直接分页

核心原因:滚动累计字段(cumulative_amount / arrears_amount)依赖前序所有记录的计算结果。

结欠[n] = 结欠[n-1] + 本行应收 - 本行已收

要展示第 N 页的 arrears_amount,必须先计算前 N-1 页所有记录的累加值。这意味着:

  • 不能简单用 DB 的 OFFSET/LIMIT
  • 不能跳页
  • 后端必须从第一条开始逐条计算

3. 性能瓶颈分析

每次 API 调用的执行路径:

1. 4 次 DB 查询sales / returns / receipts / external_statements
   └── 每次都 prefetch_related('items__product') 加载全部明细
2. Python 内存中合并 + 排序全部记录
3. 逐条遍历计算 running totals
4. 全量序列化为 JSON含 items 明细数组)
5. HTTP 响应传输

对于张晓鹏3350 条记录):

  • DB 查询:~200ms4 次查询 + prefetch
  • Python 排序 + 计算:~50ms
  • JSON 序列化:~300ms
  • 网络传输2MB取决于带宽

StatementRecordView(单条查询)更严重:为了查 1 条记录,构建了完整对账单再过滤。


4. 优化方案对比

方案 A游标分页推荐短期方案

原理:前端传 page_size + 上一页最后一条的 cumulative_amount 作为 cursor后端从 cursor 继续累加。

GET /customers/6/statements/?page_size=50
GET /customers/6/statements/?page_size=50&cursor=eyJjdW11bGF0aXZlIjoiMzE4OTEuMDAiLCJsYXN0X2lkIjoxODE0MX0=
优点 缺点
减少响应体积 后端仍需全量查询(但只序列化一页)
前端按需加载 不能跳页,只能顺序翻页
向后兼容(不传参数 = 全量) 需要前端配合改造

后端改动:中等。排序 + running total 计算后,只返回 cursor 之后的 N 条。


方案 B日期范围过滤

原理:前端传 date_from / date_to,后端只查询范围内的记录。

GET /customers/6/statements/?date_from=2026-01-01&date_to=2026-05-18
优点 缺点
实现简单 running total 仍需从历史第一条开始算
减少返回数据量 或者放弃 running total 的准确性
前端可做"按年/按月"切换

后端改动:低。在 DB 查询加 occurred_at 过滤即可。但 cumulative_amount 需要决定:

  • 选项 1从第一条算到 date_to准确但慢
  • 选项 2只在返回范围内累计快但不连续

方案 C延迟加载 items 明细

原理:默认不返回 items 数组,前端需要时单独请求。

GET /customers/6/statements/                    → 不含 items
GET /customers/6/statements/?include_items=true → 含 items当前行为
GET /statements/record/?...&include_items=true  → 单条含 items
优点 缺点
响应体积减少 50%+ 前端需要额外请求获取明细
后端改动极小 如果前端表格需要展开明细,交互变复杂
完全向后兼容

后端改动:极低。序列化时根据参数决定是否包含 items。


方案 D预计算快照长期方案

原理:同步时预计算每条记录的 cumulative_amount / arrears_amount 存入 DB查询时直接分页。

优点 缺点
真正的 DB 级分页 需要新表或新字段
查询性能最优 数据一致性维护复杂(任何单据变动需重算)
支持跳页 开发成本高

后端改动:高。需要设计快照表 + 触发重算机制。


方案 E响应缓存

原理:对账单结果缓存 N 秒(或按数据版本号缓存),重复请求直接返回。

优点 缺点
零前端改动 数据有延迟(缓存过期前看不到最新)
后端改动极小 大客户首次请求仍然慢
对高频刷新场景效果显著

5. 推荐实施路径

第一步(立即可做):方案 C — 默认不返回 items减少 50%+ 响应体积
第二步(短期):方案 B — 加日期范围过滤,前端做"按月/按年"切换
第三步(中期):方案 A — 游标分页,前端改为滚动加载
第四步(按需):方案 E — 缓存,应对高频刷新

6. 需要前端确认的问题

  1. 对账单表格是否需要一次展示全部记录? 还是可以接受分页/滚动加载?
  2. items 明细是否默认展示? 还是用户点击展开时才加载?
  3. 是否需要"按月/按年"切换? 如果需要running total 是否可以只在当前范围内累计?
  4. cumulative_amount / arrears_amount 是否是必须字段? 如果前端不使用这两个字段,分页就变得简单很多。
  5. 导出 Excel 功能是否需要全量数据? 如果需要,导出可以走单独的异步接口。

7. 当前接口响应结构(供参考)

{
  "counterparty": 6,
  "counterparty_name": "张晓鹏",
  "records": [
    {
      "source_type": "external_sales_order",
      "source_label": "外部销售单",
      "source_id": 18141,
      "occurred_at": "2026-05-18",
      "recorded_at": "2026-05-18T21:45:03Z",
      "status": 2,
      "status_label": "已同步",
      "positive_amount": "2436.00",
      "negative_amount": "0.00",
      "cumulative_amount": "31891.00",
      "current_balance": "780454.00",
      "arrears_amount": "748563.00",
      "remarks": "...",
      "items": [ ... ]
    }
  ],
  "summary": {
    "positive_total": "26981509.80",
    "negative_total": "26201055.80"
  }
}