feat: 优化批量匹配和分页默认值 (#244)
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}}`,
|
||||
|
||||
@@ -27,11 +27,11 @@ var innerCodeStatusOptions = []InnerCodeStatusOption{
|
||||
{string(model.InnerCodeNeedsCheck), "需核对"},
|
||||
}
|
||||
|
||||
// 档口入库码的批量上限是业务安全限制,不能再复用界面每页条数。
|
||||
// 删除只改本地数据库,可覆盖最大单页;匹配和回写会访问顺运宝,继续限制 20 条。
|
||||
// 删除只改本地数据库,可覆盖最大单页;回写会修改顺运宝,继续限制 20 条。
|
||||
// 只读匹配不使用这个写操作上限,见 inner_code_match.go 的远端读取分批。
|
||||
const (
|
||||
InnerCodeDeleteBatchLimit = 100
|
||||
InnerCodeRemoteBatchLimit = 20
|
||||
InnerCodeApplyBatchLimit = 20
|
||||
)
|
||||
|
||||
// InnerCodeRowView 是正式页面的一行。
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user