feat: 优化批量匹配和分页默认值 (#244)

This commit is contained in:
chengma
2026-08-15 15:56:16 +08:00
parent f315b02d3d
commit ad5bd834d3
17 changed files with 340 additions and 82 deletions
+2 -2
View File
@@ -42,8 +42,8 @@ func ApplyInnerCodes(ctx context.Context, db *sql.DB, writer InnerCodeWriter, id
if len(ids) == 0 {
return nil, fmt.Errorf("没有选择可回写记录")
}
if len(ids) > InnerCodeRemoteBatchLimit {
return nil, fmt.Errorf("单次最多回写 %d 条记录", InnerCodeRemoteBatchLimit)
if len(ids) > InnerCodeApplyBatchLimit {
return nil, fmt.Errorf("单次最多回写 %d 条记录", InnerCodeApplyBatchLimit)
}
result := &InnerCodeApplyResult{Requested: len(ids)}
for _, id := range ids {
+1 -1
View File
@@ -21,7 +21,7 @@ type fakeInnerCodeWriter struct {
}
func TestApplyInnerCodes_远程批量上限不随页面容量放宽(t *testing.T) {
ids := make([]int64, InnerCodeRemoteBatchLimit+1)
ids := make([]int64, InnerCodeApplyBatchLimit+1)
for i := range ids {
ids[i] = int64(i + 1)
}
+31 -20
View File
@@ -21,6 +21,8 @@ type InnerCodeDetailReader interface {
DetailListByStock(context.Context, []int64) ([]syb.StockDetail, error)
}
const innerCodeReadBatchSize = 100
// InnerCodePlanResult 是一次规划的逐状态统计。
type InnerCodePlanResult struct {
Total int
@@ -65,9 +67,35 @@ func PlanInnerCodeRecords(ctx context.Context, db *sql.DB, reader InnerCodeDetai
}
stockIDsByOrder := innerCodeStockIDsByOrder(snapshots)
requested := uniqueInnerCodeStockIDs(records, stockIDsByOrder)
detailsByID, err := readInnerCodeDetails(ctx, reader, requested)
if err != nil {
return nil, err
}
plans := planInnerCodeRowsWithReserved(records, stockIDsByOrder, detailsByID, reservedDetails)
for _, plan := range plans {
switch plan.Status {
case model.InnerCodeReady:
result.Ready++
case model.InnerCodeAlreadyFilled:
result.AlreadyFilled++
case model.InnerCodeFailed:
result.Failed++
default:
result.Skipped++
}
}
if err := repository.SaveInnerCodePlans(db, plans, model.NowISO()); err != nil {
return nil, err
}
return result, nil
}
// readInnerCodeDetails 只限制单次顺运宝请求大小,不限制一次规划的总选择数量。
func readInnerCodeDetails(ctx context.Context, reader InnerCodeDetailReader, requested []int64) (map[int64]syb.StockDetail, error) {
detailsByID := make(map[int64]syb.StockDetail, len(requested))
for start := 0; start < len(requested); start += 100 {
end := start + 100
for start := 0; start < len(requested); start += innerCodeReadBatchSize {
end := start + innerCodeReadBatchSize
if end > len(requested) {
end = len(requested)
}
@@ -88,24 +116,7 @@ func PlanInnerCodeRecords(ctx context.Context, db *sql.DB, reader InnerCodeDetai
}
}
}
plans := planInnerCodeRowsWithReserved(records, stockIDsByOrder, detailsByID, reservedDetails)
for _, plan := range plans {
switch plan.Status {
case model.InnerCodeReady:
result.Ready++
case model.InnerCodeAlreadyFilled:
result.AlreadyFilled++
case model.InnerCodeFailed:
result.Failed++
default:
result.Skipped++
}
}
if err := repository.SaveInnerCodePlans(db, plans, model.NowISO()); err != nil {
return nil, err
}
return result, nil
return detailsByID, nil
}
func uniqueInnerCodeOrders(records []model.InnerCodeRecord) []string {
+34
View File
@@ -1,12 +1,46 @@
package service
import (
"context"
"testing"
"cmautobuy/admin/model"
"cmautobuy/admin/syb"
)
type batchingInnerCodeReader struct {
batches [][]int64
}
func (r *batchingInnerCodeReader) DetailListByStock(_ context.Context, ids []int64) ([]syb.StockDetail, error) {
r.batches = append(r.batches, append([]int64(nil), ids...))
result := make([]syb.StockDetail, 0, len(ids))
for _, id := range ids {
result = append(result, syb.StockDetail{ID: id})
}
return result, nil
}
func TestReadInnerCodeDetails_总量不限且每批最多100(t *testing.T) {
ids := make([]int64, 205)
for index := range ids {
ids[index] = int64(index + 1)
}
reader := &batchingInnerCodeReader{}
details, err := readInnerCodeDetails(context.Background(), reader, ids)
if err != nil {
t.Fatal(err)
}
if len(details) != len(ids) || len(reader.batches) != 3 {
t.Fatalf("只读匹配应处理全部选择并分 3 批,details=%d batches=%d", len(details), len(reader.batches))
}
for index, want := range []int{100, 100, 5} {
if len(reader.batches[index]) != want {
t.Errorf("第 %d 批数量=%d,期望 %d", index+1, len(reader.batches[index]), want)
}
}
}
func TestInnerCodeStockIDFromJSON(t *testing.T) {
for _, raw := range []string{
`{"stock":{"id":75104587},"detail":{"id":1}}`,
+3 -3
View File
@@ -27,11 +27,11 @@ var innerCodeStatusOptions = []InnerCodeStatusOption{
{string(model.InnerCodeNeedsCheck), "需核对"},
}
// 档口入库码的批量上限是业务安全限制,不能再复用界面每页条数。
// 删除只改本地数据库,可覆盖最大单页;匹配和回写会访问顺运宝,继续限制 20 条。
// 删除只改本地数据库,可覆盖最大单页;回写会修改顺运宝,继续限制 20 条。
// 只读匹配不使用这个写操作上限,见 inner_code_match.go 的远端读取分批。
const (
InnerCodeDeleteBatchLimit = 100
InnerCodeRemoteBatchLimit = 20
InnerCodeApplyBatchLimit = 20
)
// InnerCodeRowView 是正式页面的一行。
+4 -4
View File
@@ -14,9 +14,9 @@ import (
// DefaultPageSize 是主列表没有指定 page_size 时使用的每页条数。
//
// 20 行在 1366×768 上通常不需要滚动,因此继续作为默认值;实际页面请求必须
// 通过 ParsePageSize 白名单解析。
const DefaultPageSize = 20
// 采购员通常需要在同一页批量勾选较多记录,因此默认显示白名单中的最大值 100;
// 实际页面请求仍必须通过 ParsePageSize 白名单解析,不能把任意输入交给 SQL。
const DefaultPageSize = 100
var allowedPageSizes = [...]int{20, 50, 100}
@@ -34,7 +34,7 @@ func ParsePage(s string) int {
}
// ParsePageSize 解析 URL 上的 page_size,只接受界面提供的三个白名单值。
// 非法值回退为 20,避免有人手改 URL 造成一次读取过多数据。
// 非法值回退为默认 100;SQL 查询仍只可能使用固定白名单值。
func ParsePageSize(s string) int {
n, err := strconv.Atoi(strings.TrimSpace(s))
if err != nil {
+7 -1
View File
@@ -34,6 +34,12 @@ func TestParsePageSize_只接受白名单(t *testing.T) {
}
}
func TestParsePageSize_缺失时默认100(t *testing.T) {
if DefaultPageSize != 100 || ParsePageSize("") != 100 {
t.Fatalf("公共分页缺失参数时应默认 100,DefaultPageSize=%d got=%d", DefaultPageSize, ParsePageSize(""))
}
}
func TestTotalPages_总数为0返回1不是0(t *testing.T) {
if got := TotalPages(0, 100); got != 1 {
t.Errorf("TotalPages(0) = %d,想要 1(不能出现『第 1/0 页』)", got)
@@ -46,7 +52,7 @@ func TestTotalPages_按PageSize向上取整(t *testing.T) {
DefaultPageSize: 1,
DefaultPageSize + 1: 2,
DefaultPageSize * 2: 2,
5195: 260, // 工单 #43 实测样本量:5195 条 / 20 条一页 = 260 页
5195: 52, // 工单 #43 实测样本量:5195 条 / 100 条一页 = 52 页
}
for total, want := range cases {
if got := TotalPages(total, DefaultPageSize); got != want {