fix: 兼容相同顺运宝明细逐件回写 (#289)
This commit is contained in:
@@ -168,19 +168,35 @@ func applyClaimedInnerCode(ctx context.Context, writer InnerCodeWriter, record m
|
||||
return innerCodeApplyOutcome{Status: model.InnerCodeNeedsCheck,
|
||||
Message: "写入前重新读取失败,没有发送远端写请求:" + err.Error(), RemoteCode: record.RemoteInnerCode}
|
||||
}
|
||||
if original.ProductQty != len(codes) {
|
||||
plannedExisting, hasExistingPlan, plannedErr := parseExistingMatchedInnerCodePlan(record, codes)
|
||||
if plannedErr != nil {
|
||||
return innerCodeApplyOutcome{Status: model.InnerCodeFailed,
|
||||
Message: fmt.Sprintf("顺运宝商品数量为 %d,单件入库码为 %d 个,数量不一致;没有发送远端写请求", original.ProductQty, len(codes)), RemoteCode: innerCodeRawText(original.Raw["innerExpCode"])}
|
||||
Message: "现成多明细逐件规划无效,没有发送远端写请求:" + plannedErr.Error(), RemoteCode: record.RemoteInnerCode}
|
||||
}
|
||||
if !innerCodeDetailIdentityMatches(record, *original) {
|
||||
return innerCodeApplyOutcome{Status: model.InnerCodeSkipped,
|
||||
Message: "顺运宝商品规格或档口身份已变化,停止回写,请重新匹配", RemoteCode: innerCodeRawText(original.Raw["innerExpCode"])}
|
||||
var items []innerCodeRemoteItem
|
||||
var missing []int
|
||||
var prepareErr error
|
||||
if hasExistingPlan {
|
||||
items, missing, prepareErr = reconcileExistingMatchedInnerCodePlan(record, stock, plannedExisting)
|
||||
if prepareErr != nil {
|
||||
return innerCodeApplyOutcome{Status: model.InnerCodeSkipped,
|
||||
Message: "现成多明细规划在写入前已变化,停止回写,请重新匹配:" + prepareErr.Error(), RemoteCode: record.RemoteInnerCode}
|
||||
}
|
||||
} else {
|
||||
if original.ProductQty != len(codes) {
|
||||
return innerCodeApplyOutcome{Status: model.InnerCodeFailed,
|
||||
Message: fmt.Sprintf("顺运宝商品数量为 %d,单件入库码为 %d 个,数量不一致;没有发送远端写请求", original.ProductQty, len(codes)), RemoteCode: innerCodeRawText(original.Raw["innerExpCode"])}
|
||||
}
|
||||
if !innerCodeDetailIdentityMatches(record, *original) {
|
||||
return innerCodeApplyOutcome{Status: model.InnerCodeSkipped,
|
||||
Message: "顺运宝商品规格或档口身份已变化,停止回写,请重新匹配", RemoteCode: innerCodeRawText(original.Raw["innerExpCode"])}
|
||||
}
|
||||
if innerCodeRawText(original.Raw["purchasePlatform"]) != "" || innerCodeRawText(original.Raw["purchaseCode"]) != "" {
|
||||
return innerCodeApplyOutcome{Status: model.InnerCodeSkipped,
|
||||
Message: "顺运宝商品已有采购平台或采购单号,停止回写", RemoteCode: innerCodeRawText(original.Raw["innerExpCode"])}
|
||||
}
|
||||
items, missing, prepareErr = planInnerCodeRemoteItems(record, stock, codes)
|
||||
}
|
||||
if innerCodeRawText(original.Raw["purchasePlatform"]) != "" || innerCodeRawText(original.Raw["purchaseCode"]) != "" {
|
||||
return innerCodeApplyOutcome{Status: model.InnerCodeSkipped,
|
||||
Message: "顺运宝商品已有采购平台或采购单号,停止回写", RemoteCode: innerCodeRawText(original.Raw["innerExpCode"])}
|
||||
}
|
||||
items, missing, prepareErr := planInnerCodeRemoteItems(record, stock, codes)
|
||||
if prepareErr != nil {
|
||||
return innerCodeApplyOutcome{Status: model.InnerCodeNeedsCheck, Message: prepareErr.Error(), RemoteCode: record.RemoteInnerCode}
|
||||
}
|
||||
@@ -196,7 +212,7 @@ func applyClaimedInnerCode(ctx context.Context, writer InnerCodeWriter, record m
|
||||
}
|
||||
|
||||
currentCode := innerCodeRawText(original.Raw["innerExpCode"])
|
||||
if currentCode != "" && !containsInnerCode(codes, currentCode) {
|
||||
if !hasExistingPlan && currentCode != "" && !containsInnerCode(codes, currentCode) {
|
||||
if currentCode != record.RemoteInnerCode {
|
||||
return innerCodeApplyOutcome{Status: model.InnerCodeNeedsCheck,
|
||||
Message: "原商品快递单号在规划后发生变化,已停止回写,请人工核对", RemoteCode: currentCode}
|
||||
@@ -212,6 +228,10 @@ func applyClaimedInnerCode(ctx context.Context, writer InnerCodeWriter, record m
|
||||
|
||||
for _, codeIndex := range missing {
|
||||
item := &items[codeIndex]
|
||||
if hasExistingPlan && item.DetailID <= 0 {
|
||||
return withInnerCodeProgress(innerCodeApplyOutcome{Status: model.InnerCodeNeedsCheck,
|
||||
Message: fmt.Sprintf("第 %d 件现成明细绑定丢失,停止回写", codeIndex+1), RemoteCode: record.RemoteInnerCode}, items)
|
||||
}
|
||||
if item.DetailID == 0 && currentCode == "" {
|
||||
item.DetailID = record.DetailID
|
||||
item.Source = "original"
|
||||
@@ -245,7 +265,14 @@ func applyClaimedInnerCode(ctx context.Context, writer InnerCodeWriter, record m
|
||||
if err != nil {
|
||||
return innerCodeApplyOutcome{Status: model.InnerCodeNeedsCheck, Message: "写入成功响应后重新读取失败,禁止自动重试", RemoteCode: record.RemoteInnerCode}
|
||||
}
|
||||
if countInnerCodeInStock(verifiedStock, item.Code) != 1 || innerCodeDetailCode(verifiedStock, item.DetailID) != item.Code {
|
||||
if hasExistingPlan {
|
||||
verifiedItems, _, verifyErr := reconcileExistingMatchedInnerCodePlan(record, verifiedStock, items)
|
||||
if verifyErr != nil || verifiedItems[codeIndex].Status != "confirmed" {
|
||||
return innerCodeApplyOutcome{Status: model.InnerCodeNeedsCheck, Message: "写入后现成多明细绑定或单件入库码核验失败,禁止自动重试", RemoteCode: record.RemoteInnerCode}
|
||||
}
|
||||
items = verifiedItems
|
||||
item = &items[codeIndex]
|
||||
} else if countInnerCodeInStock(verifiedStock, item.Code) != 1 || innerCodeDetailCode(verifiedStock, item.DetailID) != item.Code {
|
||||
return innerCodeApplyOutcome{Status: model.InnerCodeNeedsCheck, Message: "写入后单件入库码未唯一出现在预期明细,禁止自动重试", RemoteCode: record.RemoteInnerCode}
|
||||
}
|
||||
item.Status = "confirmed"
|
||||
@@ -356,6 +383,94 @@ func planInnerCodeRemoteItems(record model.InnerCodeRecord, stock syb.StockDetai
|
||||
return items, missing, nil
|
||||
}
|
||||
|
||||
// parseExistingMatchedInnerCodePlan 识别匹配阶段保存的“现成多明细逐件绑定”。
|
||||
// 普通 #250 检查点没有 existing_matched 来源,继续走原有创建/复用路径。
|
||||
func parseExistingMatchedInnerCodePlan(record model.InnerCodeRecord, codes []string) ([]innerCodeRemoteItem, bool, error) {
|
||||
raw := strings.TrimSpace(record.RemoteItemsJSON)
|
||||
if raw == "" {
|
||||
return nil, false, nil
|
||||
}
|
||||
var items []innerCodeRemoteItem
|
||||
if err := json.Unmarshal([]byte(raw), &items); err != nil {
|
||||
return nil, false, fmt.Errorf("逐件规划 JSON 无法解析")
|
||||
}
|
||||
hasExistingMatched := false
|
||||
for _, item := range items {
|
||||
if item.Source == innerCodeRemoteSourceExistingMatched {
|
||||
hasExistingMatched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasExistingMatched {
|
||||
return nil, false, nil
|
||||
}
|
||||
if len(items) != len(codes) || len(items) == 0 {
|
||||
return nil, false, fmt.Errorf("逐件绑定数量与单件入库码数量不一致")
|
||||
}
|
||||
seenDetailIDs := make(map[int64]bool, len(items))
|
||||
for index, item := range items {
|
||||
if item.Source != innerCodeRemoteSourceExistingMatched || item.Code != codes[index] || item.DetailID <= 0 {
|
||||
return nil, false, fmt.Errorf("第 %d 件逐件绑定身份无效", index+1)
|
||||
}
|
||||
if seenDetailIDs[item.DetailID] {
|
||||
return nil, false, fmt.Errorf("商品明细 id=%d 在逐件绑定中重复", item.DetailID)
|
||||
}
|
||||
seenDetailIDs[item.DetailID] = true
|
||||
}
|
||||
if record.DetailID != items[0].DetailID {
|
||||
return nil, false, fmt.Errorf("主商品明细与第一件逐件绑定不一致")
|
||||
}
|
||||
return items, true, nil
|
||||
}
|
||||
|
||||
// reconcileExistingMatchedInnerCodePlan 用远端最新整单状态复核全部现成明细。
|
||||
// 任一明细身份、数量、采购状态或快递单号不符合规划时,调用方必须停止写入。
|
||||
func reconcileExistingMatchedInnerCodePlan(record model.InnerCodeRecord, stock syb.StockDetail, planned []innerCodeRemoteItem) ([]innerCodeRemoteItem, []int, error) {
|
||||
details := make(map[int64]syb.DetailItem, len(stock.Details))
|
||||
for _, detail := range stock.Details {
|
||||
if _, duplicate := details[detail.ID]; duplicate {
|
||||
return nil, nil, fmt.Errorf("顺运宝重复返回商品明细 id=%d", detail.ID)
|
||||
}
|
||||
details[detail.ID] = detail
|
||||
}
|
||||
items := append([]innerCodeRemoteItem(nil), planned...)
|
||||
missing := make([]int, 0, len(items))
|
||||
for index := range items {
|
||||
item := &items[index]
|
||||
detail, ok := details[item.DetailID]
|
||||
if !ok {
|
||||
return nil, nil, fmt.Errorf("顺运宝未返回已规划商品明细 id=%d", item.DetailID)
|
||||
}
|
||||
if detail.ProductQty != 1 {
|
||||
return nil, nil, fmt.Errorf("商品明细 id=%d 数量已变为 %d", detail.ID, detail.ProductQty)
|
||||
}
|
||||
if !innerCodeDetailIdentityMatches(record, detail) {
|
||||
return nil, nil, fmt.Errorf("商品明细 id=%d 的规格或 SKU 身份已变化", detail.ID)
|
||||
}
|
||||
if innerCodeRawText(detail.Raw["purchasePlatform"]) != "" || innerCodeRawText(detail.Raw["purchaseCode"]) != "" {
|
||||
return nil, nil, fmt.Errorf("商品明细 id=%d 已有采购平台或采购单号", detail.ID)
|
||||
}
|
||||
remoteCode := innerCodeRawText(detail.Raw["innerExpCode"])
|
||||
if remoteCode != "" && remoteCode != item.Code {
|
||||
return nil, nil, fmt.Errorf("商品明细 id=%d 已有非目标快递单号", detail.ID)
|
||||
}
|
||||
count := countInnerCodeInStock(stock, item.Code)
|
||||
if remoteCode == item.Code {
|
||||
if count != 1 {
|
||||
return nil, nil, fmt.Errorf("单件入库码 %s 在货运单中不是唯一值", item.Code)
|
||||
}
|
||||
item.Status = "confirmed"
|
||||
continue
|
||||
}
|
||||
if count != 0 {
|
||||
return nil, nil, fmt.Errorf("单件入库码 %s 出现在非规划明细", item.Code)
|
||||
}
|
||||
item.Status = "planned"
|
||||
missing = append(missing, index)
|
||||
}
|
||||
return items, missing, nil
|
||||
}
|
||||
|
||||
func innerCodePlaceholderTitle(recordID int64, ordinal int) string {
|
||||
return fmt.Sprintf("档口第%d件-IC%d", ordinal, recordID)
|
||||
}
|
||||
@@ -428,7 +543,17 @@ func RecheckInnerCode(ctx context.Context, db *sql.DB, reader InnerCodeDetailRea
|
||||
message := "重新读取失败,仍需人工核对:" + errorText(readErr)
|
||||
if readErr == nil {
|
||||
codes, splitErr := splitInnerCodes(record.InnerCode)
|
||||
items, missing, planErr := planInnerCodeRemoteItems(*record, stock, codes)
|
||||
plannedExisting, hasExistingPlan, parseErr := parseExistingMatchedInnerCodePlan(*record, codes)
|
||||
var items []innerCodeRemoteItem
|
||||
var missing []int
|
||||
var planErr error
|
||||
if parseErr != nil {
|
||||
planErr = parseErr
|
||||
} else if hasExistingPlan {
|
||||
items, missing, planErr = reconcileExistingMatchedInnerCodePlan(*record, stock, plannedExisting)
|
||||
} else {
|
||||
items, missing, planErr = planInnerCodeRemoteItems(*record, stock, codes)
|
||||
}
|
||||
if splitErr == nil && planErr == nil {
|
||||
if raw, err := json.Marshal(items); err == nil {
|
||||
remoteItemsJSON = string(raw)
|
||||
|
||||
Reference in New Issue
Block a user