fix: 多件档口入库码逐件回写 (#250)
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"crypto/rand"
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
@@ -19,6 +20,7 @@ import (
|
||||
type InnerCodeWriter interface {
|
||||
InnerCodeDetailReader
|
||||
DeleteInnerCode(context.Context, int64) error
|
||||
CreateInnerCodeDetail(context.Context, int64, string) (int64, error)
|
||||
UpdateDetailCode(context.Context, int64, int64, string) error
|
||||
}
|
||||
|
||||
@@ -46,6 +48,16 @@ type innerCodeApplyOutcome struct {
|
||||
RemoteCode string
|
||||
}
|
||||
|
||||
type innerCodeRemoteItem struct {
|
||||
Code string `json:"code"`
|
||||
DetailID int64 `json:"detail_id,omitempty"`
|
||||
Source string `json:"source,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type innerCodeCheckpoint func([]innerCodeRemoteItem, string) error
|
||||
|
||||
// QueueInnerCodeApplyBatch 把任意数量的已选记录原子排队,远端请求由后台执行器处理。
|
||||
func QueueInnerCodeApplyBatch(db *sql.DB, ids []int64, actorUserID string) (*InnerCodeApplyBatch, error) {
|
||||
ids = uniquePositiveInnerCodeIDs(ids)
|
||||
@@ -95,7 +107,14 @@ func RunInnerCodeApplyBatch(ctx context.Context, db *sql.DB, writer InnerCodeWri
|
||||
result.Skipped++
|
||||
continue
|
||||
}
|
||||
outcome := applyClaimedInnerCode(ctx, writer, *record)
|
||||
checkpoint := func(items []innerCodeRemoteItem, message string) error {
|
||||
raw, err := json.Marshal(items)
|
||||
if err != nil {
|
||||
return fmt.Errorf("序列化档口入库码逐件检查点失败: %w", err)
|
||||
}
|
||||
return repository.SaveInnerCodeRemoteItems(db, id, string(raw), compactInnerCodeMessage(message), model.NowISO())
|
||||
}
|
||||
outcome := applyClaimedInnerCode(ctx, writer, *record, checkpoint)
|
||||
finishedAt := model.NowISO()
|
||||
if err := repository.FinishInnerCodeApply(db, id, outcome.Status, compactInnerCodeMessage(outcome.Message), outcome.RemoteCode, finishedAt); err != nil {
|
||||
return result, err
|
||||
@@ -138,80 +157,121 @@ func newInnerCodeApplyBatchID() (string, error) {
|
||||
return "ICB-" + time.Now().UTC().Format("20060102T150405") + "-" + hex.EncodeToString(random), nil
|
||||
}
|
||||
|
||||
func applyClaimedInnerCode(ctx context.Context, writer InnerCodeWriter, record model.InnerCodeRecord) innerCodeApplyOutcome {
|
||||
item, err := readCurrentInnerCodeDetail(ctx, writer, record)
|
||||
func applyClaimedInnerCode(ctx context.Context, writer InnerCodeWriter, record model.InnerCodeRecord, checkpoint innerCodeCheckpoint) innerCodeApplyOutcome {
|
||||
codes, err := splitInnerCodes(record.InnerCode)
|
||||
if err != nil || len(codes) != record.SourceDuplicateCount {
|
||||
return innerCodeApplyOutcome{Status: model.InnerCodeFailed,
|
||||
Message: fmt.Sprintf("导入单件码数量与源行数不一致(入库码 %d 个,源行 %d 行),没有发送远端写请求", len(codes), record.SourceDuplicateCount), RemoteCode: record.RemoteInnerCode}
|
||||
}
|
||||
stock, original, err := readCurrentInnerCodeStock(ctx, writer, record)
|
||||
if err != nil {
|
||||
return innerCodeApplyOutcome{Status: model.InnerCodeNeedsCheck,
|
||||
Message: "写入前重新读取失败,没有发送删除或写入请求:" + err.Error(), RemoteCode: record.RemoteInnerCode}
|
||||
Message: "写入前重新读取失败,没有发送远端写请求:" + err.Error(), RemoteCode: record.RemoteInnerCode}
|
||||
}
|
||||
currentCode := innerCodeRawText(item.Raw["innerExpCode"])
|
||||
if !innerCodeDetailIdentityMatches(record, *item) {
|
||||
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: currentCode}
|
||||
Message: "顺运宝商品规格或档口身份已变化,停止回写,请重新匹配", RemoteCode: innerCodeRawText(original.Raw["innerExpCode"])}
|
||||
}
|
||||
platform := innerCodeRawText(item.Raw["purchasePlatform"])
|
||||
purchaseCode := innerCodeRawText(item.Raw["purchaseCode"])
|
||||
if platform != "" || purchaseCode != "" {
|
||||
if innerCodeRawText(original.Raw["purchasePlatform"]) != "" || innerCodeRawText(original.Raw["purchaseCode"]) != "" {
|
||||
return innerCodeApplyOutcome{Status: model.InnerCodeSkipped,
|
||||
Message: "顺运宝商品已有采购平台或采购单号,停止回写", RemoteCode: currentCode}
|
||||
Message: "顺运宝商品已有采购平台或采购单号,停止回写", RemoteCode: innerCodeRawText(original.Raw["innerExpCode"])}
|
||||
}
|
||||
if currentCode == record.InnerCode {
|
||||
items, missing, prepareErr := planInnerCodeRemoteItems(record, stock, codes)
|
||||
if prepareErr != nil {
|
||||
return innerCodeApplyOutcome{Status: model.InnerCodeNeedsCheck, Message: prepareErr.Error(), RemoteCode: record.RemoteInnerCode}
|
||||
}
|
||||
if checkpoint == nil {
|
||||
checkpoint = func([]innerCodeRemoteItem, string) error { return nil }
|
||||
}
|
||||
if len(missing) == 0 {
|
||||
if err := checkpoint(items, fmt.Sprintf("远端已存在全部 %d 个单件入库码", len(codes))); err != nil {
|
||||
return innerCodeApplyOutcome{Status: model.InnerCodeNeedsCheck, Message: err.Error(), RemoteCode: strings.Join(codes, ",")}
|
||||
}
|
||||
return innerCodeApplyOutcome{Status: model.InnerCodeAlreadyFilled,
|
||||
Message: "写入前核验发现远端已是目标入库码,无需重复写入", RemoteCode: currentCode}
|
||||
Message: fmt.Sprintf("远端已存在全部 %d 个单件入库码,无需重复写入", len(codes)), RemoteCode: strings.Join(codes, ",")}
|
||||
}
|
||||
if currentCode != record.RemoteInnerCode {
|
||||
return innerCodeApplyOutcome{Status: model.InnerCodeNeedsCheck,
|
||||
Message: "远端快递单号在规划后发生变化,已停止回写,请人工核对", RemoteCode: currentCode}
|
||||
}
|
||||
if currentCode != "" {
|
||||
|
||||
currentCode := innerCodeRawText(original.Raw["innerExpCode"])
|
||||
if currentCode != "" && !containsInnerCode(codes, currentCode) {
|
||||
if currentCode != record.RemoteInnerCode {
|
||||
return innerCodeApplyOutcome{Status: model.InnerCodeNeedsCheck,
|
||||
Message: "原商品快递单号在规划后发生变化,已停止回写,请人工核对", RemoteCode: currentCode}
|
||||
}
|
||||
if err := checkpoint(items, "准备清除原商品上的旧快递单号"); err != nil {
|
||||
return innerCodeApplyOutcome{Status: model.InnerCodeNeedsCheck, Message: err.Error(), RemoteCode: currentCode}
|
||||
}
|
||||
if err := writer.DeleteInnerCode(ctx, record.DetailID); err != nil {
|
||||
status := model.InnerCodeFailed
|
||||
message := "删除旧快递单号失败,未发送新值写入请求:" + err.Error()
|
||||
if errors.Is(err, syb.ErrWriteResultUnknown) {
|
||||
status = model.InnerCodeNeedsCheck
|
||||
message = "删除旧快递单号的结果未知,禁止自动继续写入,请重新核对"
|
||||
}
|
||||
return innerCodeApplyOutcome{Status: status, Message: message, RemoteCode: currentCode}
|
||||
return withInnerCodeProgress(unknownOrFailedInnerCodeOutcome(err, "删除旧快递单号", currentCode), items)
|
||||
}
|
||||
currentCode = ""
|
||||
}
|
||||
if err := writer.UpdateDetailCode(ctx, record.StockID, record.DetailID, record.InnerCode); err != nil {
|
||||
status := model.InnerCodeFailed
|
||||
message := "写入档口入库码失败,系统不会自动重试:" + err.Error()
|
||||
if errors.Is(err, syb.ErrWriteResultUnknown) {
|
||||
status = model.InnerCodeNeedsCheck
|
||||
message = "写入结果未知,禁止自动重试,请重新核对"
|
||||
|
||||
for _, codeIndex := range missing {
|
||||
item := &items[codeIndex]
|
||||
if item.DetailID == 0 && currentCode == "" {
|
||||
item.DetailID = record.DetailID
|
||||
item.Source = "original"
|
||||
currentCode = "reserved"
|
||||
}
|
||||
if item.DetailID == 0 {
|
||||
item.Title = innerCodePlaceholderTitle(record.ID, codeIndex+1)
|
||||
item.Status = "creating"
|
||||
if err := checkpoint(items, fmt.Sprintf("准备为第 %d 件创建零价明细", codeIndex+1)); err != nil {
|
||||
return innerCodeApplyOutcome{Status: model.InnerCodeNeedsCheck, Message: err.Error(), RemoteCode: record.RemoteInnerCode}
|
||||
}
|
||||
detailID, err := writer.CreateInnerCodeDetail(ctx, record.StockID, item.Title)
|
||||
if err != nil {
|
||||
return withInnerCodeProgress(unknownOrFailedInnerCodeOutcome(err, fmt.Sprintf("创建第 %d 件明细", codeIndex+1), record.RemoteInnerCode), items)
|
||||
}
|
||||
item.DetailID = detailID
|
||||
item.Source = "created"
|
||||
item.Status = "created"
|
||||
if err := checkpoint(items, fmt.Sprintf("第 %d 件明细已创建,准备写入入库码", codeIndex+1)); err != nil {
|
||||
return innerCodeApplyOutcome{Status: model.InnerCodeNeedsCheck, Message: "新增明细后保存检查点失败,禁止继续写入:" + err.Error(), RemoteCode: record.RemoteInnerCode}
|
||||
}
|
||||
}
|
||||
item.Status = "writing"
|
||||
if err := checkpoint(items, fmt.Sprintf("准备写入第 %d 个单件入库码", codeIndex+1)); err != nil {
|
||||
return innerCodeApplyOutcome{Status: model.InnerCodeNeedsCheck, Message: err.Error(), RemoteCode: record.RemoteInnerCode}
|
||||
}
|
||||
if err := writer.UpdateDetailCode(ctx, record.StockID, item.DetailID, item.Code); err != nil {
|
||||
return withInnerCodeProgress(unknownOrFailedInnerCodeOutcome(err, fmt.Sprintf("写入第 %d 个单件入库码", codeIndex+1), record.RemoteInnerCode), items)
|
||||
}
|
||||
verifiedStock, _, err := readCurrentInnerCodeStock(ctx, writer, record)
|
||||
if err != nil {
|
||||
return innerCodeApplyOutcome{Status: model.InnerCodeNeedsCheck, Message: "写入成功响应后重新读取失败,禁止自动重试", RemoteCode: record.RemoteInnerCode}
|
||||
}
|
||||
if countInnerCodeInStock(verifiedStock, item.Code) != 1 || innerCodeDetailCode(verifiedStock, item.DetailID) != item.Code {
|
||||
return innerCodeApplyOutcome{Status: model.InnerCodeNeedsCheck, Message: "写入后单件入库码未唯一出现在预期明细,禁止自动重试", RemoteCode: record.RemoteInnerCode}
|
||||
}
|
||||
item.Status = "confirmed"
|
||||
if err := checkpoint(items, fmt.Sprintf("第 %d 个单件入库码已回读确认", codeIndex+1)); err != nil {
|
||||
return innerCodeApplyOutcome{Status: model.InnerCodeNeedsCheck, Message: "写入确认后保存检查点失败,请人工核对:" + err.Error(), RemoteCode: record.RemoteInnerCode}
|
||||
}
|
||||
return innerCodeApplyOutcome{Status: status, Message: message, RemoteCode: currentCode}
|
||||
}
|
||||
verified, err := readCurrentInnerCodeDetail(ctx, writer, record)
|
||||
if err != nil {
|
||||
return innerCodeApplyOutcome{Status: model.InnerCodeNeedsCheck,
|
||||
Message: "写入请求已成功响应,但重新读取失败,请核对远端结果", RemoteCode: currentCode}
|
||||
}
|
||||
verifiedCode := innerCodeRawText(verified.Raw["innerExpCode"])
|
||||
if !innerCodeDetailIdentityMatches(record, *verified) {
|
||||
return innerCodeApplyOutcome{Status: model.InnerCodeNeedsCheck,
|
||||
Message: "写入后商品规格或档口身份发生变化,请人工核对", RemoteCode: verifiedCode}
|
||||
}
|
||||
if verifiedCode != record.InnerCode {
|
||||
return innerCodeApplyOutcome{Status: model.InnerCodeNeedsCheck,
|
||||
Message: "写入后远端值与目标入库码不一致,禁止自动重试,请人工核对", RemoteCode: verifiedCode}
|
||||
}
|
||||
return innerCodeApplyOutcome{Status: model.InnerCodeUpdated,
|
||||
Message: "回写完成,远端再次读取结果一致", RemoteCode: verifiedCode}
|
||||
Message: fmt.Sprintf("回写完成:%d 个单件入库码均已逐件回读确认", len(codes)), RemoteCode: strings.Join(codes, ",")}
|
||||
}
|
||||
|
||||
func readCurrentInnerCodeDetail(ctx context.Context, reader InnerCodeDetailReader, record model.InnerCodeRecord) (*syb.DetailItem, error) {
|
||||
_, found, err := readCurrentInnerCodeStock(ctx, reader, record)
|
||||
return found, err
|
||||
}
|
||||
|
||||
func readCurrentInnerCodeStock(ctx context.Context, reader InnerCodeDetailReader, record model.InnerCodeRecord) (syb.StockDetail, *syb.DetailItem, error) {
|
||||
if record.StockID <= 0 || record.DetailID <= 0 {
|
||||
return nil, fmt.Errorf("记录缺少有效的货运单或商品明细 ID")
|
||||
return syb.StockDetail{}, nil, fmt.Errorf("记录缺少有效的货运单或商品明细 ID")
|
||||
}
|
||||
stocks, err := reader.DetailListByStock(ctx, []int64{record.StockID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return syb.StockDetail{}, nil, err
|
||||
}
|
||||
if len(stocks) != 1 || stocks[0].ID != record.StockID {
|
||||
return nil, fmt.Errorf("顺运宝没有唯一返回货运单 id=%d", record.StockID)
|
||||
return syb.StockDetail{}, nil, fmt.Errorf("顺运宝没有唯一返回货运单 id=%d", record.StockID)
|
||||
}
|
||||
var found *syb.DetailItem
|
||||
for index := range stocks[0].Details {
|
||||
@@ -219,15 +279,134 @@ func readCurrentInnerCodeDetail(ctx context.Context, reader InnerCodeDetailReade
|
||||
continue
|
||||
}
|
||||
if found != nil {
|
||||
return nil, fmt.Errorf("顺运宝重复返回商品明细 id=%d", record.DetailID)
|
||||
return syb.StockDetail{}, nil, fmt.Errorf("顺运宝重复返回商品明细 id=%d", record.DetailID)
|
||||
}
|
||||
item := stocks[0].Details[index]
|
||||
found = &item
|
||||
}
|
||||
if found == nil {
|
||||
return nil, fmt.Errorf("顺运宝未返回商品明细 id=%d", record.DetailID)
|
||||
return syb.StockDetail{}, nil, fmt.Errorf("顺运宝未返回商品明细 id=%d", record.DetailID)
|
||||
}
|
||||
return found, nil
|
||||
return stocks[0], found, nil
|
||||
}
|
||||
|
||||
func splitInnerCodes(raw string) ([]string, error) {
|
||||
parts := strings.Split(raw, ",")
|
||||
codes := make([]string, 0, len(parts))
|
||||
seen := make(map[string]bool, len(parts))
|
||||
for _, part := range parts {
|
||||
code := strings.TrimSpace(part)
|
||||
if code == "" {
|
||||
return nil, fmt.Errorf("单件入库码不能为空")
|
||||
}
|
||||
if seen[code] {
|
||||
return nil, fmt.Errorf("单件入库码 %q 重复", code)
|
||||
}
|
||||
seen[code] = true
|
||||
codes = append(codes, code)
|
||||
}
|
||||
return codes, nil
|
||||
}
|
||||
|
||||
func planInnerCodeRemoteItems(record model.InnerCodeRecord, stock syb.StockDetail, codes []string) ([]innerCodeRemoteItem, []int, error) {
|
||||
items := make([]innerCodeRemoteItem, len(codes))
|
||||
indexByCode := make(map[string]int, len(codes))
|
||||
for index, code := range codes {
|
||||
items[index] = innerCodeRemoteItem{Code: code, Status: "planned"}
|
||||
indexByCode[code] = index
|
||||
}
|
||||
for _, detail := range stock.Details {
|
||||
code := innerCodeRawText(detail.Raw["innerExpCode"])
|
||||
if index, ok := indexByCode[code]; ok {
|
||||
if items[index].DetailID != 0 {
|
||||
return nil, nil, fmt.Errorf("单件入库码 %s 在货运单中出现多次,停止自动回写", code)
|
||||
}
|
||||
source := "existing"
|
||||
if detail.ID == record.DetailID {
|
||||
source = "original"
|
||||
}
|
||||
items[index].DetailID = detail.ID
|
||||
items[index].Source = source
|
||||
items[index].Status = "confirmed"
|
||||
continue
|
||||
}
|
||||
for index := range items {
|
||||
title := innerCodePlaceholderTitle(record.ID, index+1)
|
||||
if detail.ProductTitle != title {
|
||||
continue
|
||||
}
|
||||
if items[index].DetailID != 0 {
|
||||
return nil, nil, fmt.Errorf("第 %d 件占位明细在货运单中出现多次,停止自动回写", index+1)
|
||||
}
|
||||
if code != "" {
|
||||
return nil, nil, fmt.Errorf("第 %d 件占位明细已有非目标快递单号,停止自动回写", index+1)
|
||||
}
|
||||
items[index].DetailID = detail.ID
|
||||
items[index].Source = "existing_placeholder"
|
||||
items[index].Title = title
|
||||
items[index].Status = "created"
|
||||
}
|
||||
}
|
||||
missing := make([]int, 0, len(items))
|
||||
for index := range items {
|
||||
if items[index].Status != "confirmed" {
|
||||
missing = append(missing, index)
|
||||
}
|
||||
}
|
||||
return items, missing, nil
|
||||
}
|
||||
|
||||
func innerCodePlaceholderTitle(recordID int64, ordinal int) string {
|
||||
return fmt.Sprintf("档口第%d件-IC%d", ordinal, recordID)
|
||||
}
|
||||
|
||||
func containsInnerCode(codes []string, value string) bool {
|
||||
for _, code := range codes {
|
||||
if code == value {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func countInnerCodeInStock(stock syb.StockDetail, code string) int {
|
||||
count := 0
|
||||
for _, detail := range stock.Details {
|
||||
if innerCodeRawText(detail.Raw["innerExpCode"]) == code {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func innerCodeDetailCode(stock syb.StockDetail, detailID int64) string {
|
||||
for _, detail := range stock.Details {
|
||||
if detail.ID == detailID {
|
||||
return innerCodeRawText(detail.Raw["innerExpCode"])
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func unknownOrFailedInnerCodeOutcome(err error, action, remoteCode string) innerCodeApplyOutcome {
|
||||
status := model.InnerCodeFailed
|
||||
message := action + "失败,系统不会自动重试:" + err.Error()
|
||||
if errors.Is(err, syb.ErrWriteResultUnknown) {
|
||||
status = model.InnerCodeNeedsCheck
|
||||
message = action + "结果未知,禁止自动重试,请重新核对"
|
||||
}
|
||||
return innerCodeApplyOutcome{Status: status, Message: message, RemoteCode: remoteCode}
|
||||
}
|
||||
|
||||
func withInnerCodeProgress(outcome innerCodeApplyOutcome, items []innerCodeRemoteItem) innerCodeApplyOutcome {
|
||||
confirmed := 0
|
||||
for _, item := range items {
|
||||
if item.Status == "confirmed" {
|
||||
confirmed++
|
||||
}
|
||||
}
|
||||
outcome.Message = fmt.Sprintf("已确认 %d/%d 件;%s", confirmed, len(items), outcome.Message)
|
||||
return outcome
|
||||
}
|
||||
|
||||
// RecheckInnerCode 只重新读取一条 needs_check 记录,不发送任何写请求。
|
||||
@@ -242,24 +421,35 @@ func RecheckInnerCode(ctx context.Context, db *sql.DB, reader InnerCodeDetailRea
|
||||
if record.Status != model.InnerCodeNeedsCheck {
|
||||
return record.Status, "当前记录不需要核对", nil
|
||||
}
|
||||
item, readErr := readCurrentInnerCodeDetail(ctx, reader, *record)
|
||||
stock, item, readErr := readCurrentInnerCodeStock(ctx, reader, *record)
|
||||
status := model.InnerCodeNeedsCheck
|
||||
remoteCode := record.RemoteInnerCode
|
||||
remoteItemsJSON := record.RemoteItemsJSON
|
||||
message := "重新读取失败,仍需人工核对:" + errorText(readErr)
|
||||
if readErr == nil {
|
||||
codes, splitErr := splitInnerCodes(record.InnerCode)
|
||||
items, missing, planErr := planInnerCodeRemoteItems(*record, stock, codes)
|
||||
if splitErr == nil && planErr == nil {
|
||||
if raw, err := json.Marshal(items); err == nil {
|
||||
remoteItemsJSON = string(raw)
|
||||
}
|
||||
}
|
||||
remoteCode = innerCodeRawText(item.Raw["innerExpCode"])
|
||||
if !innerCodeDetailIdentityMatches(*record, *item) {
|
||||
message = "重新读取到的商品身份与规划不一致;保持需核对,系统没有写入"
|
||||
} else if remoteCode == record.InnerCode {
|
||||
} else if splitErr != nil || planErr != nil {
|
||||
message = "重新读取到的逐件入库码存在冲突;保持需核对,系统没有写入"
|
||||
} else if len(missing) == 0 {
|
||||
status = model.InnerCodeUpdated
|
||||
message = "重新读取确认远端已是目标入库码;没有重复写入"
|
||||
remoteCode = strings.Join(codes, ",")
|
||||
message = fmt.Sprintf("重新读取确认远端已存在全部 %d 个单件入库码;没有重复写入", len(codes))
|
||||
} else {
|
||||
message = "重新读取后远端仍不是目标入库码;保持需核对,系统没有写入"
|
||||
message = fmt.Sprintf("重新读取后仍缺少 %d 个单件入库码;保持需核对,系统没有写入", len(missing))
|
||||
}
|
||||
}
|
||||
checkedAt := model.NowISO()
|
||||
message = compactInnerCodeMessage(message)
|
||||
if err := repository.SaveInnerCodeRecheck(db, id, status, message, remoteCode, checkedAt); err != nil {
|
||||
if err := repository.SaveInnerCodeRecheck(db, id, status, message, remoteCode, remoteItemsJSON, checkedAt); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return status, message, nil
|
||||
|
||||
@@ -13,14 +13,17 @@ import (
|
||||
)
|
||||
|
||||
type fakeInnerCodeWriter struct {
|
||||
stock syb.StockDetail
|
||||
readErr error
|
||||
deleteErr error
|
||||
updateErr error
|
||||
readCount int
|
||||
deleteCount int
|
||||
updateCount int
|
||||
updatedCode string
|
||||
stock syb.StockDetail
|
||||
readErr error
|
||||
deleteErr error
|
||||
createErr error
|
||||
updateErr error
|
||||
readCount int
|
||||
deleteCount int
|
||||
createCount int
|
||||
updateCount int
|
||||
updatedCode string
|
||||
nextDetailID int64
|
||||
}
|
||||
|
||||
func TestQueueInnerCodeApplyBatch_超过20条可一次排队(t *testing.T) {
|
||||
@@ -81,6 +84,20 @@ func (f *fakeInnerCodeWriter) DeleteInnerCode(_ context.Context, detailID int64)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeInnerCodeWriter) CreateInnerCodeDetail(_ context.Context, stockID int64, title string) (int64, error) {
|
||||
f.createCount++
|
||||
if f.createErr != nil {
|
||||
return 0, f.createErr
|
||||
}
|
||||
if f.nextDetailID == 0 {
|
||||
f.nextDetailID = 100
|
||||
}
|
||||
f.nextDetailID++
|
||||
f.stock.Details = append(f.stock.Details, syb.DetailItem{ID: f.nextDetailID, ProductTitle: title,
|
||||
ProductQty: 1, ProductPrice: 0, Raw: map[string]any{"innerExpCode": ""}})
|
||||
return f.nextDetailID, nil
|
||||
}
|
||||
|
||||
func (f *fakeInnerCodeWriter) UpdateDetailCode(_ context.Context, _, detailID int64, code string) error {
|
||||
f.updateCount++
|
||||
f.updatedCode = code
|
||||
@@ -95,21 +112,93 @@ func (f *fakeInnerCodeWriter) UpdateDetailCode(_ context.Context, _, detailID in
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestApplyClaimedInnerCode_聚合码只写一次并按完整值复读(t *testing.T) {
|
||||
func TestApplyClaimedInnerCode_两个单件码写入两个明细(t *testing.T) {
|
||||
record := innerCodeApplyTestRecord("")
|
||||
record.InnerCode = "DK260815A160101,DK260815A160102"
|
||||
record.SourceDuplicateCount = 2
|
||||
writer := innerCodeApplyTestWriter(record, "")
|
||||
outcome := applyClaimedInnerCode(context.Background(), writer, record)
|
||||
if outcome.Status != model.InnerCodeUpdated || writer.updateCount != 1 || writer.updatedCode != record.InnerCode ||
|
||||
outcome.RemoteCode != record.InnerCode || writer.readCount != 2 {
|
||||
t.Fatalf("outcome=%+v update=%d code=%q read=%d", outcome, writer.updateCount, writer.updatedCode, writer.readCount)
|
||||
outcome := applyClaimedInnerCode(context.Background(), writer, record, nil)
|
||||
if outcome.Status != model.InnerCodeUpdated || writer.updateCount != 2 || writer.createCount != 1 ||
|
||||
outcome.RemoteCode != record.InnerCode || writer.readCount != 3 {
|
||||
t.Fatalf("outcome=%+v update=%d create=%d read=%d", outcome, writer.updateCount, writer.createCount, writer.readCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyClaimedInnerCode_三个单件码创建两条零价明细(t *testing.T) {
|
||||
record := innerCodeApplyTestRecord("")
|
||||
record.InnerCode = "DK-001,DK-002,DK-003"
|
||||
record.SourceDuplicateCount = 3
|
||||
writer := innerCodeApplyTestWriter(record, "")
|
||||
outcome := applyClaimedInnerCode(context.Background(), writer, record, nil)
|
||||
if outcome.Status != model.InnerCodeUpdated || writer.updateCount != 3 || writer.createCount != 2 {
|
||||
t.Fatalf("outcome=%+v update=%d create=%d", outcome, writer.updateCount, writer.createCount)
|
||||
}
|
||||
for _, detail := range writer.stock.Details[1:] {
|
||||
if detail.ProductQty != 1 || detail.ProductPrice != 0 {
|
||||
t.Fatalf("占位明细必须数量 1、价格 0: %+v", detail)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyClaimedInnerCode_复用人工明细已有目标码(t *testing.T) {
|
||||
record := innerCodeApplyTestRecord("")
|
||||
record.InnerCode = "DK-001,DK-002"
|
||||
record.SourceDuplicateCount = 2
|
||||
writer := innerCodeApplyTestWriter(record, "")
|
||||
writer.stock.Details = append(writer.stock.Details, syb.DetailItem{ID: 21, ProductTitle: "人工明细", ProductQty: 1,
|
||||
Raw: map[string]any{"innerExpCode": "DK-002"}})
|
||||
outcome := applyClaimedInnerCode(context.Background(), writer, record, nil)
|
||||
if outcome.Status != model.InnerCodeUpdated || writer.updateCount != 1 || writer.createCount != 0 || writer.deleteCount != 0 {
|
||||
t.Fatalf("outcome=%+v update=%d create=%d delete=%d", outcome, writer.updateCount, writer.createCount, writer.deleteCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyClaimedInnerCode_数量不一致零写入(t *testing.T) {
|
||||
record := innerCodeApplyTestRecord("")
|
||||
record.InnerCode = "DK-001,DK-002"
|
||||
record.SourceDuplicateCount = 2
|
||||
writer := innerCodeApplyTestWriter(record, "")
|
||||
writer.stock.Details[0].ProductQty = 1
|
||||
outcome := applyClaimedInnerCode(context.Background(), writer, record, nil)
|
||||
if outcome.Status != model.InnerCodeFailed || writer.updateCount != 0 || writer.createCount != 0 || writer.deleteCount != 0 {
|
||||
t.Fatalf("outcome=%+v update=%d create=%d delete=%d", outcome, writer.updateCount, writer.createCount, writer.deleteCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyClaimedInnerCode_创建结果未知立即停止(t *testing.T) {
|
||||
record := innerCodeApplyTestRecord("")
|
||||
record.InnerCode = "DK-001,DK-002"
|
||||
record.SourceDuplicateCount = 2
|
||||
writer := innerCodeApplyTestWriter(record, "")
|
||||
writer.createErr = fmtUnknownInnerCodeWriteError()
|
||||
checkpoints := 0
|
||||
outcome := applyClaimedInnerCode(context.Background(), writer, record, func([]innerCodeRemoteItem, string) error {
|
||||
checkpoints++
|
||||
return nil
|
||||
})
|
||||
if outcome.Status != model.InnerCodeNeedsCheck || writer.createCount != 1 || writer.updateCount != 1 || checkpoints < 3 {
|
||||
t.Fatalf("outcome=%+v create=%d update=%d checkpoints=%d", outcome, writer.createCount, writer.updateCount, checkpoints)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyClaimedInnerCode_崩溃遗留占位明细会复用而不重复创建(t *testing.T) {
|
||||
record := innerCodeApplyTestRecord("")
|
||||
record.InnerCode = "DK-001,DK-002"
|
||||
record.SourceDuplicateCount = 2
|
||||
writer := innerCodeApplyTestWriter(record, "")
|
||||
writer.stock.Details = append(writer.stock.Details, syb.DetailItem{ID: 99,
|
||||
ProductTitle: innerCodePlaceholderTitle(record.ID, 2), ProductQty: 1, ProductPrice: 0,
|
||||
Raw: map[string]any{"innerExpCode": ""}})
|
||||
outcome := applyClaimedInnerCode(context.Background(), writer, record, nil)
|
||||
if outcome.Status != model.InnerCodeUpdated || writer.createCount != 0 || writer.updateCount != 2 {
|
||||
t.Fatalf("outcome=%+v create=%d update=%d", outcome, writer.createCount, writer.updateCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyClaimedInnerCode_空旧值直接写并复读确认(t *testing.T) {
|
||||
record := innerCodeApplyTestRecord("")
|
||||
writer := innerCodeApplyTestWriter(record, "")
|
||||
outcome := applyClaimedInnerCode(context.Background(), writer, record)
|
||||
outcome := applyClaimedInnerCode(context.Background(), writer, record, nil)
|
||||
if outcome.Status != model.InnerCodeUpdated || writer.deleteCount != 0 || writer.updateCount != 1 || writer.readCount != 2 {
|
||||
t.Fatalf("outcome=%+v counts read=%d delete=%d update=%d", outcome, writer.readCount, writer.deleteCount, writer.updateCount)
|
||||
}
|
||||
@@ -118,7 +207,7 @@ func TestApplyClaimedInnerCode_空旧值直接写并复读确认(t *testing.T) {
|
||||
func TestApplyClaimedInnerCode_有旧值先删后写(t *testing.T) {
|
||||
record := innerCodeApplyTestRecord("OLD")
|
||||
writer := innerCodeApplyTestWriter(record, "OLD")
|
||||
outcome := applyClaimedInnerCode(context.Background(), writer, record)
|
||||
outcome := applyClaimedInnerCode(context.Background(), writer, record, nil)
|
||||
if outcome.Status != model.InnerCodeUpdated || writer.deleteCount != 1 || writer.updateCount != 1 {
|
||||
t.Fatalf("outcome=%+v delete=%d update=%d", outcome, writer.deleteCount, writer.updateCount)
|
||||
}
|
||||
@@ -127,7 +216,7 @@ func TestApplyClaimedInnerCode_有旧值先删后写(t *testing.T) {
|
||||
func TestApplyClaimedInnerCode_规划后远端值变化不写入(t *testing.T) {
|
||||
record := innerCodeApplyTestRecord("OLD")
|
||||
writer := innerCodeApplyTestWriter(record, "OTHER")
|
||||
outcome := applyClaimedInnerCode(context.Background(), writer, record)
|
||||
outcome := applyClaimedInnerCode(context.Background(), writer, record, nil)
|
||||
if outcome.Status != model.InnerCodeNeedsCheck || writer.deleteCount != 0 || writer.updateCount != 0 {
|
||||
t.Fatalf("outcome=%+v delete=%d update=%d", outcome, writer.deleteCount, writer.updateCount)
|
||||
}
|
||||
@@ -137,7 +226,7 @@ func TestApplyClaimedInnerCode_删除明确失败不继续写(t *testing.T) {
|
||||
record := innerCodeApplyTestRecord("OLD")
|
||||
writer := innerCodeApplyTestWriter(record, "OLD")
|
||||
writer.deleteErr = errors.New("已打单数据不能清除")
|
||||
outcome := applyClaimedInnerCode(context.Background(), writer, record)
|
||||
outcome := applyClaimedInnerCode(context.Background(), writer, record, nil)
|
||||
if outcome.Status != model.InnerCodeFailed || writer.deleteCount != 1 || writer.updateCount != 0 {
|
||||
t.Fatalf("outcome=%+v delete=%d update=%d", outcome, writer.deleteCount, writer.updateCount)
|
||||
}
|
||||
@@ -147,7 +236,7 @@ func TestApplyClaimedInnerCode_写入未知结果不复读也不重试(t *testin
|
||||
record := innerCodeApplyTestRecord("")
|
||||
writer := innerCodeApplyTestWriter(record, "")
|
||||
writer.updateErr = fmtUnknownInnerCodeWriteError()
|
||||
outcome := applyClaimedInnerCode(context.Background(), writer, record)
|
||||
outcome := applyClaimedInnerCode(context.Background(), writer, record, nil)
|
||||
if outcome.Status != model.InnerCodeNeedsCheck || writer.updateCount != 1 || writer.readCount != 1 {
|
||||
t.Fatalf("outcome=%+v update=%d read=%d", outcome, writer.updateCount, writer.readCount)
|
||||
}
|
||||
@@ -157,7 +246,7 @@ func TestApplyClaimedInnerCode_采购字段出现后停止(t *testing.T) {
|
||||
record := innerCodeApplyTestRecord("")
|
||||
writer := innerCodeApplyTestWriter(record, "")
|
||||
writer.stock.Details[0].Raw["purchaseCode"] = "CG-1"
|
||||
outcome := applyClaimedInnerCode(context.Background(), writer, record)
|
||||
outcome := applyClaimedInnerCode(context.Background(), writer, record, nil)
|
||||
if outcome.Status != model.InnerCodeSkipped || writer.updateCount != 0 {
|
||||
t.Fatalf("outcome=%+v update=%d", outcome, writer.updateCount)
|
||||
}
|
||||
@@ -168,7 +257,7 @@ func TestApplyClaimedInnerCode_档口身份变化后停止(t *testing.T) {
|
||||
record.SybSKU = "A#1"
|
||||
writer := innerCodeApplyTestWriter(record, "")
|
||||
writer.stock.Details[0].Raw["sku"] = "B#2"
|
||||
outcome := applyClaimedInnerCode(context.Background(), writer, record)
|
||||
outcome := applyClaimedInnerCode(context.Background(), writer, record, nil)
|
||||
if outcome.Status != model.InnerCodeSkipped || writer.updateCount != 0 {
|
||||
t.Fatalf("outcome=%+v update=%d", outcome, writer.updateCount)
|
||||
}
|
||||
@@ -176,11 +265,11 @@ func TestApplyClaimedInnerCode_档口身份变化后停止(t *testing.T) {
|
||||
|
||||
func innerCodeApplyTestRecord(oldCode string) model.InnerCodeRecord {
|
||||
return model.InnerCodeRecord{ID: 1, StockID: 10, DetailID: 20, SybSpec: "黑色,M",
|
||||
InnerCode: "DK-001", RemoteInnerCode: oldCode, Status: model.InnerCodeApplying}
|
||||
InnerCode: "DK-001", SourceDuplicateCount: 1, RemoteInnerCode: oldCode, Status: model.InnerCodeApplying}
|
||||
}
|
||||
|
||||
func innerCodeApplyTestWriter(record model.InnerCodeRecord, currentCode string) *fakeInnerCodeWriter {
|
||||
item := syb.DetailItem{ID: record.DetailID, ProductSpec: record.SybSpec, Raw: map[string]any{
|
||||
item := syb.DetailItem{ID: record.DetailID, ProductSpec: record.SybSpec, ProductQty: record.SourceDuplicateCount, Raw: map[string]any{
|
||||
"id": record.DetailID, "productSpec": record.SybSpec, "innerExpCode": currentCode,
|
||||
"purchasePlatform": "", "purchaseCode": "",
|
||||
}}
|
||||
|
||||
@@ -277,6 +277,17 @@ 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))))
|
||||
continue
|
||||
}
|
||||
used[record.OrderNumber][matched.ID] = true
|
||||
plan.StockID = stockID
|
||||
plan.DetailID = matched.ID
|
||||
@@ -286,9 +297,13 @@ func planInnerCodeRowsWithReserved(records []model.InnerCodeRecord, stockIDsByOr
|
||||
plan.PurchasePlatform = innerCodeRawText(matched.Raw["purchasePlatform"])
|
||||
plan.PurchaseCode = innerCodeRawText(matched.Raw["purchaseCode"])
|
||||
plan.RemoteInnerCode = innerCodeRawText(matched.Raw["innerExpCode"])
|
||||
if plan.RemoteInnerCode == record.InnerCode {
|
||||
_, missing, remoteErr := planInnerCodeRemoteItems(plan, stock, codes)
|
||||
if remoteErr != nil {
|
||||
plan.Status = model.InnerCodeSkipped
|
||||
plan.ResultMessage = remoteErr.Error()
|
||||
} else if len(missing) == 0 {
|
||||
plan.Status = model.InnerCodeAlreadyFilled
|
||||
plan.ResultMessage = "远端已是相同入库码,无需重复写入"
|
||||
plan.ResultMessage = fmt.Sprintf("远端已存在全部 %d 个单件入库码,无需重复写入", len(codes))
|
||||
} else {
|
||||
plan.Status = model.InnerCodeReady
|
||||
plan.ResultMessage = "唯一匹配,等待操作员确认回写"
|
||||
|
||||
@@ -179,9 +179,9 @@ func TestPlanInnerCodeRows_精确与标准化规格均受档口约束(t *testing
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanInnerCodeRows_聚合码放行且其他异常继续阻断(t *testing.T) {
|
||||
func TestPlanInnerCodeRows_多件数量一致放行且其他异常继续阻断(t *testing.T) {
|
||||
records := []model.InnerCodeRecord{
|
||||
{ID: 1, OrderNumber: "DUP", SpecRaw: "黑色,M", InnerCode: "DK1", SourceDuplicateCount: 2},
|
||||
{ID: 1, OrderNumber: "DUP", SpecRaw: "黑色,M", InnerCode: "DK1,DK2", SourceDuplicateCount: 2},
|
||||
{ID: 2, OrderNumber: "EMPTY", SpecRaw: "", InnerCode: "DK2", SourceDuplicateCount: 1},
|
||||
{ID: 3, OrderNumber: "PURCHASED", SpecRaw: "黑色,M", InnerCode: "DK3", SourceDuplicateCount: 1},
|
||||
{ID: 4, OrderNumber: "AMBIGUOUS", SpecRaw: "黑色,M", InnerCode: "DK4", SourceDuplicateCount: 1},
|
||||
@@ -189,7 +189,7 @@ func TestPlanInnerCodeRows_聚合码放行且其他异常继续阻断(t *testing
|
||||
}
|
||||
stocks := map[string][]int64{"DUP": {1}, "EMPTY": {2}, "PURCHASED": {3}, "AMBIGUOUS": {4}}
|
||||
details := map[int64]syb.StockDetail{
|
||||
1: {ID: 1, Details: []syb.DetailItem{innerCodeTestItem(1, "黑色,M", nil)}},
|
||||
1: {ID: 1, Details: []syb.DetailItem{func() syb.DetailItem { item := innerCodeTestItem(1, "黑色,M", nil); item.ProductQty = 2; return item }()}},
|
||||
2: {ID: 2, Details: []syb.DetailItem{innerCodeTestItem(2, "黑色,M", nil)}},
|
||||
3: {ID: 3, Details: []syb.DetailItem{innerCodeTestItem(3, "黑色,M", map[string]any{"purchasePlatform": "PDD"})}},
|
||||
4: {ID: 4, Details: []syb.DetailItem{innerCodeTestItem(4, "黑色,M", nil), innerCodeTestItem(5, "黑色,M", nil)}},
|
||||
@@ -275,5 +275,5 @@ func innerCodeTestItem(id int64, spec string, raw map[string]any) syb.DetailItem
|
||||
}
|
||||
raw["id"] = id
|
||||
raw["productSpec"] = spec
|
||||
return syb.DetailItem{ID: id, ProductSpec: spec, Raw: raw}
|
||||
return syb.DetailItem{ID: id, ProductSpec: spec, ProductQty: 1, Raw: raw}
|
||||
}
|
||||
|
||||
@@ -49,6 +49,8 @@ type InnerCodeRowView struct {
|
||||
CanMatch bool
|
||||
CanApply bool
|
||||
HasRemoteOldCode bool
|
||||
CodeCount int
|
||||
ExpectedAddCount int
|
||||
CanRecheck bool
|
||||
}
|
||||
|
||||
@@ -133,6 +135,10 @@ func innerCodeRowView(record model.InnerCodeRecord) InnerCodeRowView {
|
||||
record.Status == model.InnerCodeSkipped ||
|
||||
record.Status == model.InnerCodeFailed
|
||||
canApply := record.Status == model.InnerCodeReady
|
||||
codeCount := record.SourceDuplicateCount
|
||||
if codeCount < 1 {
|
||||
codeCount = 1
|
||||
}
|
||||
return InnerCodeRowView{
|
||||
ID: record.ID, BusinessDate: record.BusinessDate, OrderNumber: record.OrderNumber,
|
||||
Stall: displayInnerCodeValue(record.Stall), SpecRaw: displayInnerCodeValue(record.SpecRaw),
|
||||
@@ -143,6 +149,8 @@ func innerCodeRowView(record model.InnerCodeRecord) InnerCodeRowView {
|
||||
CanMatch: canMatch,
|
||||
CanApply: canApply,
|
||||
HasRemoteOldCode: canApply && record.RemoteInnerCode != "",
|
||||
CodeCount: codeCount,
|
||||
ExpectedAddCount: max(codeCount-1, 0),
|
||||
CanRecheck: record.Status == model.InnerCodeNeedsCheck,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user