采购订单分页不能只靠Limit/Offset,因涉及状态流转、软删除、多表关联、权限过滤及高并发跳单问题;需用Scopes封装可复用过滤逻辑,并以created_at+id实现游标分页。

分页本身不难,但采购订单生命周期里涉及状态流转、多表关联、软删除、权限过滤——直接套用 Pagination 或 Limit/Offset 会漏数据、查慢、甚至返回已“删除”的订单。
为什么采购订单分页不能只靠 Limit 和 Offset
ERP 中采购订单常需按「创建中→审批中→已下发→部分收货→已完成→已关闭」等状态流转,且存在以下现实约束:
-
DeletedAt字段触发 GORM 软删除,但某些角色(如审计员)需查全部历史单,而普通采购员只能看deleted_at IS NULL的活跃单 - 订单主表
purchase_orders关联供应商、物料、审批流、收货单等多个表,Preload滥用会导致 N+1 或笛卡尔积爆炸 - 状态字段(如
status)通常为枚举或字典值,前端传的是中文或 code,后端需做映射再进 WHERE - 高并发下
OFFSET越大越慢,10 万条后翻页延迟明显,且可能因并发插入导致“跳单”
如何用 Scopes 组合生命周期过滤条件
不要在每个 handler 里拼 SQL,把生命周期逻辑沉淀为可复用的 Scope 函数:
func WithStatus(status string) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
switch status {
case "pending":
return db.Where("status IN ?", []string{"draft", "submitted", "reviewing"})
case "executing":
return db.Where("status IN ?", []string{"issued", "partial_received"})
case "closed":
return db.Where("status IN ?", []string{"completed", "canceled", "rejected"})
default:
return db
}
}
}
// 使用时链式调用
db.Scopes(WithStatus("pending"), SoftDeleteFilter(role)).Find(&orders)
注意:SoftDeleteFilter 需根据用户角色动态决定是否加 Where("deleted_at IS NULL"),不能硬编码。
用游标分页替代 OFFSET 防跳单和性能退化
采购订单有明确业务时间轴(如 created_at + id),适合基于最后一条记录的值做游标查询:
- 首次请求:不带游标,按
ORDER BY created_at DESC, id DESC LIMIT 20 - 后续请求:传上一页最后一条的
created_at和id,条件写成WHERE created_at - GORM 不内置游标支持,需手写
Where+Order+Limit,别依赖Offset - 必须确保
ORDER BY字段有联合索引,例如INDEX idx_created_id (created_at, id)
Preload 关联查采购订单时的三个关键点
一个采购订单页面常要展示供应商名、审批人、物料明细行数、最近收货时间——但不是所有字段都要实时 JOIN:
- 用
Preload("Supplier", func(db *gorm.DB) *gorm.DB { return db.Select("id, name, code") })控制只查必要字段,避免加载整张供应商表 - 明细行(
POItems)建议用Count替代全量Preload,除非页面真要渲染全部行:db.Model(&order).Association("Items").Count(&itemCount) - 审批流状态若来自另一服务(如工作流引擎),别强求 GORM 关联,改用异步填充或 API 聚合,避免跨库事务和超时风险
真正容易被忽略的是:采购订单的「生命周期状态」往往由多个子状态共同决定(比如「已下发」要求审批通过 且 发货单已生成 且 无驳回标记),这种复合判断不适合塞进 GORM Scope,得提前算好并落库字段,否则分页时无法走索引。


















