feat: 支持勾选部分档口入库码匹配 (#235)
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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"})
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user