fix: 兼容相同顺运宝明细逐件回写 (#289)

This commit is contained in:
chengma
2026-08-22 11:46:23 +08:00
parent de10fb6cf4
commit 18ad85d3cb
7 changed files with 460 additions and 32 deletions
+148 -16
View File
@@ -224,6 +224,7 @@ func planInnerCodeRowsWithReserved(records []model.InnerCodeRecord, stockIDsByOr
plan.StockID, plan.DetailID = 0, 0
plan.SybSpec, plan.SybSKU, plan.SybVariationSKU = "", "", ""
plan.PurchasePlatform, plan.PurchaseCode, plan.RemoteInnerCode = "", "", ""
plan.RemoteItemsJSON = ""
if strings.TrimSpace(record.SpecRaw) == "" {
plans = append(plans, innerCodeSkippedPlan(plan, "Excel 清洗后规格为空,跳过"))
continue
@@ -263,9 +264,48 @@ func planInnerCodeRowsWithReserved(records []model.InnerCodeRecord, stockIDsByOr
plans = append(plans, innerCodeSkippedPlan(plan, message))
continue
}
codes, codeErr := splitInnerCodes(record.InnerCode)
if codeErr != nil || len(codes) != record.SourceDuplicateCount {
plans = append(plans, innerCodeSkippedPlan(plan,
fmt.Sprintf("单件入库码数量与 Excel 源行数不一致(入库码 %d 个,源行 %d 行)", len(codes), record.SourceDuplicateCount)))
continue
}
specMatches := matchInnerCodeSpecItems(record.SpecRaw, eligible)
sourceMatches := matchInnerCodeSourceSKUItems(record.SourceSKURaw, specMatches)
matches, evidenceReason := matchInnerCodeItemsWithEvidence(record.Stall, record.SourceSKURaw, specMatches)
if evidenceReason != "" {
if evidenceReason == innerCodeSourceSKUDuplicateReason {
items, primary, multipleErr := planExistingMatchedInnerCodeItems(record, codes, sourceMatches)
if multipleErr == nil {
raw, marshalErr := json.Marshal(items)
if marshalErr != nil {
plans = append(plans, innerCodeSkippedPlan(plan, "保存多明细逐件规划失败"))
continue
}
for _, item := range items {
used[record.OrderNumber][item.DetailID] = true
}
plan.StockID = stockID
plan.DetailID = primary.ID
plan.SybSpec = primary.ProductSpec
plan.SybSKU = innerCodeRawText(primary.Raw["sku"])
plan.SybVariationSKU = innerCodeRawText(primary.Raw["variationSku"])
plan.RemoteInnerCode = innerCodeRawText(primary.Raw["innerExpCode"])
plan.RemoteItemsJSON = string(raw)
if allInnerCodeRemoteItemsConfirmed(items) {
plan.Status = model.InnerCodeAlreadyFilled
plan.ResultMessage = fmt.Sprintf("远端已存在全部 %d 个单件入库码,无需重复写入", len(codes))
} else {
plan.Status = model.InnerCodeReady
plan.ResultMessage = "多条现成商品已逐件唯一匹配,等待操作员确认回写"
}
plans = append(plans, plan)
continue
}
if multipleErr != nil {
evidenceReason = multipleErr.Error()
}
}
plans = append(plans, innerCodeSkippedPlan(plan, evidenceReason))
continue
}
@@ -282,12 +322,6 @@ func planInnerCodeRowsWithReserved(records []model.InnerCodeRecord, stockIDsByOr
continue
}
matched := matches[0]
codes, codeErr := splitInnerCodes(record.InnerCode)
if codeErr != nil || len(codes) != record.SourceDuplicateCount {
plans = append(plans, innerCodeSkippedPlan(plan,
fmt.Sprintf("单件入库码数量与 Excel 源行数不一致(入库码 %d 个,源行 %d 行)", len(codes), record.SourceDuplicateCount)))
continue
}
if matched.ProductQty != len(codes) {
plans = append(plans, innerCodeSkippedPlan(plan,
fmt.Sprintf("顺运宝商品数量为 %d,单件入库码为 %d 个,数量不一致", matched.ProductQty, len(codes))))
@@ -318,6 +352,96 @@ func planInnerCodeRowsWithReserved(records []model.InnerCodeRecord, stockIDsByOr
return plans
}
const (
innerCodeSourceSKUDuplicateReason = "原始 SKU 候选重复,不能自动选择"
innerCodeRemoteSourceExistingMatched = "existing_matched"
)
// planExistingMatchedInnerCodeItems 只接受“多个单件码恰好对应多个现成数量 1 明细”的确定场景。
// 返回项按 Excel 单件码顺序排列,明细候选使用稳定 ID 排序;已经存在的目标码优先保留原绑定。
func planExistingMatchedInnerCodeItems(record model.InnerCodeRecord, codes []string, matches []syb.DetailItem) ([]innerCodeRemoteItem, syb.DetailItem, error) {
if len(codes) < 2 || len(matches) != len(codes) {
return nil, syb.DetailItem{}, fmt.Errorf("原始 SKU 命中 %d 条相同商品明细,但单件入库码为 %d 个,数量不一致", len(matches), len(codes))
}
candidates := append([]syb.DetailItem(nil), matches...)
sort.Slice(candidates, func(i, j int) bool { return candidates[i].ID < candidates[j].ID })
first := candidates[0]
wantSpec := first.ProductSpec
wantSKU := innerCodeRawText(first.Raw["sku"])
wantVariationSKU := innerCodeRawText(first.Raw["variationSku"])
codeIndex := make(map[string]int, len(codes))
items := make([]innerCodeRemoteItem, len(codes))
for index, code := range codes {
codeIndex[code] = index
items[index] = innerCodeRemoteItem{Code: code, Source: innerCodeRemoteSourceExistingMatched, Status: "planned"}
}
blank := make([]syb.DetailItem, 0, len(candidates))
seenDetailIDs := make(map[int64]bool, len(candidates))
for _, candidate := range candidates {
if candidate.ID <= 0 || seenDetailIDs[candidate.ID] {
return nil, syb.DetailItem{}, fmt.Errorf("重复候选包含无效或重复的商品明细 ID,不能自动逐件分配")
}
seenDetailIDs[candidate.ID] = true
if candidate.ProductQty != 1 {
return nil, syb.DetailItem{}, fmt.Errorf("相同候选商品明细 id=%d 的数量为 %d,不能按现成明细逐件分配", candidate.ID, candidate.ProductQty)
}
if candidate.ProductSpec != wantSpec || innerCodeRawText(candidate.Raw["sku"]) != wantSKU ||
innerCodeRawText(candidate.Raw["variationSku"]) != wantVariationSKU {
return nil, syb.DetailItem{}, fmt.Errorf("重复候选的规格或 SKU 身份不一致,不能自动逐件分配")
}
if strings.TrimSpace(record.Stall) != "" && !innerCodeStallMatches(record.Stall, candidate) {
return nil, syb.DetailItem{}, fmt.Errorf("重复候选的档口及货号不一致,不能自动逐件分配")
}
if innerCodeRawText(candidate.Raw["purchasePlatform"]) != "" || innerCodeRawText(candidate.Raw["purchaseCode"]) != "" {
return nil, syb.DetailItem{}, fmt.Errorf("重复候选中存在已有采购信息的商品,不能自动逐件分配")
}
remoteCode := innerCodeRawText(candidate.Raw["innerExpCode"])
if remoteCode == "" {
blank = append(blank, candidate)
continue
}
index, ok := codeIndex[remoteCode]
if !ok {
return nil, syb.DetailItem{}, fmt.Errorf("重复候选中存在非目标快递单号,不能自动逐件分配")
}
if items[index].DetailID != 0 {
return nil, syb.DetailItem{}, fmt.Errorf("单件入库码 %s 在重复候选中出现多次,不能自动逐件分配", remoteCode)
}
items[index].DetailID = candidate.ID
items[index].Status = "confirmed"
}
blankIndex := 0
for index := range items {
if items[index].DetailID != 0 {
continue
}
if blankIndex >= len(blank) {
return nil, syb.DetailItem{}, fmt.Errorf("现成空白明细不足,不能完成逐件分配")
}
items[index].DetailID = blank[blankIndex].ID
blankIndex++
}
if blankIndex != len(blank) {
return nil, syb.DetailItem{}, fmt.Errorf("现成空白明细多于待写入单件码,不能自动逐件分配")
}
primaryID := items[0].DetailID
for _, candidate := range candidates {
if candidate.ID == primaryID {
return items, candidate, nil
}
}
return nil, syb.DetailItem{}, fmt.Errorf("多明细逐件规划缺少主商品明细")
}
func allInnerCodeRemoteItemsConfirmed(items []innerCodeRemoteItem) bool {
for _, item := range items {
if item.Status != "confirmed" {
return false
}
}
return len(items) > 0
}
func innerCodeSkippedPlan(plan model.InnerCodeRecord, message string) model.InnerCodeRecord {
plan.Status = model.InnerCodeSkipped
plan.ResultMessage = message
@@ -350,17 +474,10 @@ func matchInnerCodeSpecItems(spec string, eligible []syb.DetailItem) []syb.Detai
}
func matchInnerCodeItemsWithEvidence(stall, sourceSKURaw string, specMatches []syb.DetailItem) ([]syb.DetailItem, string) {
sourceSKU := normalizeInnerCodeSourceSKU(sourceSKURaw)
if sourceSKU != "" {
sourceMatches := make([]syb.DetailItem, 0)
for _, item := range specMatches {
if normalizeInnerCodeSourceSKU(innerCodeRawText(item.Raw["sku"])) == sourceSKU ||
normalizeInnerCodeSourceSKU(innerCodeRawText(item.Raw["variationSku"])) == sourceSKU {
sourceMatches = append(sourceMatches, item)
}
}
sourceMatches := matchInnerCodeSourceSKUItems(sourceSKURaw, specMatches)
if normalizeInnerCodeSourceSKU(sourceSKURaw) != "" {
if len(sourceMatches) > 1 {
return nil, "原始 SKU 候选重复,不能自动选择"
return nil, innerCodeSourceSKUDuplicateReason
}
if len(sourceMatches) == 1 {
stallMatches := filterInnerCodeItemsByStallStrict(specMatches, stall)
@@ -376,6 +493,21 @@ func matchInnerCodeItemsWithEvidence(stall, sourceSKURaw string, specMatches []s
return filterInnerCodeItemsByStall(specMatches, stall), ""
}
func matchInnerCodeSourceSKUItems(sourceSKURaw string, items []syb.DetailItem) []syb.DetailItem {
sourceSKU := normalizeInnerCodeSourceSKU(sourceSKURaw)
if sourceSKU == "" {
return nil
}
matches := make([]syb.DetailItem, 0)
for _, item := range items {
if normalizeInnerCodeSourceSKU(innerCodeRawText(item.Raw["sku"])) == sourceSKU ||
normalizeInnerCodeSourceSKU(innerCodeRawText(item.Raw["variationSku"])) == sourceSKU {
matches = append(matches, item)
}
}
return matches
}
func filterInnerCodeItemsByStall(items []syb.DetailItem, stall string) []syb.DetailItem {
stall = strings.TrimSpace(stall)
if stall == "" {