feat: 支持勾选部分档口入库码匹配 (#235)

This commit is contained in:
chengma
2026-08-15 09:41:45 +08:00
parent 05b59974e8
commit 63f8407780
10 changed files with 342 additions and 55 deletions
+65 -12
View File
@@ -30,22 +30,35 @@ type InnerCodePlanResult struct {
Failed int
}
// PlanInnerCodeRecords 对某个业务日期的可重规划记录读取最新远端详情并原子保存计划。
// PlanInnerCodeRecords 对某个业务日期选中的可重规划记录读取最新远端详情并原子保存计划。
// 本函数绝不调用删除或更新顺运宝接口。
func PlanInnerCodeRecords(ctx context.Context, db *sql.DB, reader InnerCodeDetailReader, businessDate string) (*InnerCodePlanResult, error) {
func PlanInnerCodeRecords(ctx context.Context, db *sql.DB, reader InnerCodeDetailReader, businessDate string, selectedIDs []int64) (*InnerCodePlanResult, error) {
if _, err := time.Parse("2006-01-02", businessDate); err != nil {
return nil, fmt.Errorf("业务日期格式应为 YYYY-MM-DD")
}
records, err := repository.ListInnerCodeRecordsForPlanning(db, businessDate)
selectedIDs = uniquePositiveInnerCodeIDs(selectedIDs)
if len(selectedIDs) == 0 {
return nil, fmt.Errorf("没有选中可匹配记录")
}
records, err := repository.ListInnerCodeRecordsForPlanning(db, businessDate, selectedIDs)
if err != nil {
return nil, err
}
result := &InnerCodePlanResult{Total: len(records)}
if len(records) == 0 {
return result, nil
if len(records) != len(selectedIDs) {
return nil, fmt.Errorf("部分选中记录不存在、业务日期不一致或状态已变化,请刷新后重新勾选")
}
result := &InnerCodePlanResult{Total: len(records)}
orders := uniqueInnerCodeOrders(records)
contextRecords, err := repository.ListInnerCodePlanningContext(db, businessDate, orders)
if err != nil {
return nil, err
}
selectedSet := make(map[int64]bool, len(records))
for _, record := range records {
selectedSet[record.ID] = true
}
reservedDetails := innerCodeReservedDetails(contextRecords, selectedSet)
snapshots, err := repository.ListInnerCodeSybSnapshots(db, orders)
if err != nil {
return nil, err
@@ -76,7 +89,7 @@ func PlanInnerCodeRecords(ctx context.Context, db *sql.DB, reader InnerCodeDetai
}
}
plans := planInnerCodeRows(records, stockIDsByOrder, detailsByID)
plans := planInnerCodeRowsWithReserved(records, stockIDsByOrder, detailsByID, reservedDetails)
for _, plan := range plans {
switch plan.Status {
case model.InnerCodeReady:
@@ -156,8 +169,42 @@ func uniqueInnerCodeStockIDs(records []model.InnerCodeRecord, byOrder map[string
return result
}
func innerCodeReservedDetails(records []model.InnerCodeRecord, selected map[int64]bool) map[string]map[int64]bool {
reserved := make(map[string]map[int64]bool)
for _, record := range records {
if selected[record.ID] || record.DetailID <= 0 || !innerCodeStatusReservesDetail(record.Status) {
continue
}
if reserved[record.OrderNumber] == nil {
reserved[record.OrderNumber] = make(map[int64]bool)
}
reserved[record.OrderNumber][record.DetailID] = true
}
return reserved
}
func innerCodeStatusReservesDetail(status model.InnerCodeStatus) bool {
switch status {
case model.InnerCodeReady, model.InnerCodeApplying, model.InnerCodeUpdated,
model.InnerCodeAlreadyFilled, model.InnerCodeNeedsCheck:
return true
default:
return false
}
}
func planInnerCodeRows(records []model.InnerCodeRecord, stockIDsByOrder map[string][]int64, detailsByID map[int64]syb.StockDetail) []model.InnerCodeRecord {
used := make(map[string]map[int64]bool)
return planInnerCodeRowsWithReserved(records, stockIDsByOrder, detailsByID, nil)
}
func planInnerCodeRowsWithReserved(records []model.InnerCodeRecord, stockIDsByOrder map[string][]int64, detailsByID map[int64]syb.StockDetail, reserved map[string]map[int64]bool) []model.InnerCodeRecord {
used := make(map[string]map[int64]bool, len(reserved))
for orderNumber, detailIDs := range reserved {
used[orderNumber] = make(map[int64]bool, len(detailIDs))
for detailID := range detailIDs {
used[orderNumber][detailID] = true
}
}
plans := make([]model.InnerCodeRecord, 0, len(records))
for _, record := range records {
plan := record
@@ -189,16 +236,22 @@ func planInnerCodeRows(records []model.InnerCodeRecord, stockIDsByOrder map[stri
used[record.OrderNumber] = make(map[int64]bool)
}
eligible := make([]syb.DetailItem, 0, len(stock.Details))
availableBeforeReservation := 0
for _, item := range stock.Details {
if used[record.OrderNumber][item.ID] {
continue
}
if innerCodeRawText(item.Raw["purchasePlatform"]) == "" && innerCodeRawText(item.Raw["purchaseCode"]) == "" {
availableBeforeReservation++
if used[record.OrderNumber][item.ID] {
continue
}
eligible = append(eligible, item)
}
}
if len(eligible) == 0 {
plans = append(plans, innerCodeSkippedPlan(plan, "没有采购平台和采购单号都为空的顺运宝商品"))
message := "没有采购平台和采购单号都为空的顺运宝商品"
if availableBeforeReservation > 0 {
message = "符合条件的顺运宝商品已被同订单其他记录占用"
}
plans = append(plans, innerCodeSkippedPlan(plan, message))
continue
}
matches := matchInnerCodeItems(record.SpecRaw, record.Stall, eligible)
+42
View File
@@ -72,6 +72,48 @@ func TestPlanInnerCodeRows_同一远端明细不会占用两次(t *testing.T) {
}
}
func TestPlanInnerCodeRows_部分匹配保留未选记录的明细占用(t *testing.T) {
selected := model.InnerCodeRecord{
ID: 1, BusinessDate: "2026-08-15", OrderNumber: "ORDER",
SpecRaw: "黑色,M", InnerCode: "DK1", SourceDuplicateCount: 1,
}
contextRecords := []model.InnerCodeRecord{
selected,
{ID: 2, BusinessDate: "2026-08-15", OrderNumber: "ORDER", DetailID: 99, Status: model.InnerCodeReady},
}
reserved := innerCodeReservedDetails(contextRecords, map[int64]bool{selected.ID: true})
detail := syb.StockDetail{ID: 10, Details: []syb.DetailItem{innerCodeTestItem(99, "黑色,M", nil)}}
plans := planInnerCodeRowsWithReserved(
[]model.InnerCodeRecord{selected},
map[string][]int64{"ORDER": {10}},
map[int64]syb.StockDetail{10: detail},
reserved,
)
if len(plans) != 1 || plans[0].Status != model.InnerCodeSkipped ||
plans[0].ResultMessage != "符合条件的顺运宝商品已被同订单其他记录占用" {
t.Fatalf("未选记录的明细占用没有生效: %+v", plans)
}
}
func TestInnerCodeReservedDetails_选中记录释放自身旧占用(t *testing.T) {
records := []model.InnerCodeRecord{
{ID: 1, OrderNumber: "ORDER", DetailID: 99, Status: model.InnerCodeReady},
{ID: 2, OrderNumber: "ORDER", DetailID: 100, Status: model.InnerCodeNeedsCheck},
{ID: 3, OrderNumber: "ORDER", DetailID: 101, Status: model.InnerCodeSkipped},
}
reserved := innerCodeReservedDetails(records, map[int64]bool{1: true})
if reserved["ORDER"][99] || !reserved["ORDER"][100] || reserved["ORDER"][101] {
t.Fatalf("明细占用集合不正确: %+v", reserved)
}
}
func TestUniquePositiveInnerCodeIDs_过滤无效和重复编号(t *testing.T) {
got := uniquePositiveInnerCodeIDs([]int64{3, 0, -1, 3, 2})
if len(got) != 2 || got[0] != 3 || got[1] != 2 {
t.Fatalf("选中编号清理结果不正确: %v", got)
}
}
func TestPlanInnerCodeRows_远端已是目标码(t *testing.T) {
record := model.InnerCodeRecord{ID: 1, OrderNumber: "ORDER", SpecRaw: "黑色,M", InnerCode: "DK1", SourceDuplicateCount: 1}
item := innerCodeTestItem(1, "黑色,M", map[string]any{"innerExpCode": "DK1"})
+10 -3
View File
@@ -42,7 +42,8 @@ type InnerCodeRowView struct {
StatusClass string
ResultMessage string
UpdatedAt string
CanSelect bool
CanMatch bool
CanApply bool
HasRemoteOldCode bool
CanRecheck bool
}
@@ -116,6 +117,11 @@ func innerCodeRowView(record model.InnerCodeRecord) InnerCodeRowView {
if statusText == "" {
statusText = "未知"
}
canMatch := record.Status == model.InnerCodePending ||
record.Status == model.InnerCodeReady ||
record.Status == model.InnerCodeSkipped ||
record.Status == model.InnerCodeFailed
canApply := record.Status == model.InnerCodeReady
return InnerCodeRowView{
ID: record.ID, BusinessDate: record.BusinessDate, OrderNumber: record.OrderNumber,
Stall: displayInnerCodeValue(record.Stall), SpecRaw: displayInnerCodeValue(record.SpecRaw),
@@ -123,8 +129,9 @@ func innerCodeRowView(record model.InnerCodeRecord) InnerCodeRowView {
RemoteInnerCode: displayInnerCodeValue(record.RemoteInnerCode), Status: string(record.Status),
StatusText: statusText, StatusClass: "inner-code-status-" + string(record.Status),
ResultMessage: displayInnerCodeValue(record.ResultMessage), UpdatedAt: formatLocalTime(record.UpdatedAt),
CanSelect: record.Status == model.InnerCodeReady,
HasRemoteOldCode: record.Status == model.InnerCodeReady && record.RemoteInnerCode != "",
CanMatch: canMatch,
CanApply: canApply,
HasRemoteOldCode: canApply && record.RemoteInnerCode != "",
CanRecheck: record.Status == model.InnerCodeNeedsCheck,
}
}
+8 -3
View File
@@ -6,15 +6,20 @@ import (
"cmautobuy/admin/model"
)
func TestInnerCodeRowView_只有可回写记录可选择(t *testing.T) {
func TestInnerCodeRowView_按状态区分匹配与回写能力(t *testing.T) {
for _, status := range []model.InnerCodeStatus{
model.InnerCodePending, model.InnerCodeReady, model.InnerCodeApplying,
model.InnerCodeUpdated, model.InnerCodeAlreadyFilled, model.InnerCodeSkipped,
model.InnerCodeFailed, model.InnerCodeNeedsCheck,
} {
view := innerCodeRowView(model.InnerCodeRecord{Status: status, RemoteInnerCode: "OLD"})
if view.CanSelect != (status == model.InnerCodeReady) {
t.Errorf("status=%s CanSelect=%v", status, view.CanSelect)
wantMatch := status == model.InnerCodePending || status == model.InnerCodeReady ||
status == model.InnerCodeSkipped || status == model.InnerCodeFailed
if view.CanMatch != wantMatch {
t.Errorf("status=%s CanMatch=%v", status, view.CanMatch)
}
if view.CanApply != (status == model.InnerCodeReady) {
t.Errorf("status=%s CanApply=%v", status, view.CanApply)
}
if view.CanRecheck != (status == model.InnerCodeNeedsCheck) {
t.Errorf("status=%s CanRecheck=%v", status, view.CanRecheck)