package service import ( "context" "database/sql" "errors" "fmt" "strings" "time" "cmautobuy/admin/model" "cmautobuy/admin/repository" "cmautobuy/admin/syb" ) // InnerCodeWriter 是安全回写所需的最小顺运宝接口。 type InnerCodeWriter interface { InnerCodeDetailReader DeleteInnerCode(context.Context, int64) error UpdateDetailCode(context.Context, int64, int64, string) error } // InnerCodeApplyResult 是一次用户确认操作的统计。 type InnerCodeApplyResult struct { Requested int Updated int AlreadyFilled int Skipped int Failed int NeedsCheck int } type innerCodeApplyOutcome struct { Status model.InnerCodeStatus Message string RemoteCode string } // ApplyInnerCodes 逐条原子领取并回写。每条写请求只发送一次,单条失败不阻断后续行。 func ApplyInnerCodes(ctx context.Context, db *sql.DB, writer InnerCodeWriter, ids []int64, actorUserID string) (*InnerCodeApplyResult, error) { ids = uniquePositiveInnerCodeIDs(ids) if len(ids) == 0 { return nil, fmt.Errorf("没有选择可回写记录") } if len(ids) > PageSize { return nil, fmt.Errorf("单次最多回写当前页 %d 条记录", PageSize) } result := &InnerCodeApplyResult{Requested: len(ids)} for _, id := range ids { now := model.NowISO() record, claimed, err := repository.ClaimInnerCodeForApply(db, id, actorUserID, now) if err != nil { return nil, err } if !claimed { result.Skipped++ continue } outcome := applyClaimedInnerCode(ctx, writer, *record) finishedAt := model.NowISO() if err := repository.FinishInnerCodeApply(db, id, outcome.Status, compactInnerCodeMessage(outcome.Message), outcome.RemoteCode, finishedAt); err != nil { return nil, err } switch outcome.Status { case model.InnerCodeUpdated: result.Updated++ case model.InnerCodeAlreadyFilled: result.AlreadyFilled++ case model.InnerCodeNeedsCheck: result.NeedsCheck++ case model.InnerCodeFailed: result.Failed++ default: result.Skipped++ } } return result, nil } func applyClaimedInnerCode(ctx context.Context, writer InnerCodeWriter, record model.InnerCodeRecord) innerCodeApplyOutcome { item, err := readCurrentInnerCodeDetail(ctx, writer, record) if err != nil { return innerCodeApplyOutcome{Status: model.InnerCodeNeedsCheck, Message: "写入前重新读取失败,没有发送删除或写入请求:" + err.Error(), RemoteCode: record.RemoteInnerCode} } currentCode := innerCodeRawText(item.Raw["innerExpCode"]) if !innerCodeDetailIdentityMatches(record, *item) { return innerCodeApplyOutcome{Status: model.InnerCodeSkipped, Message: "顺运宝商品规格或档口身份已变化,停止回写,请重新匹配", RemoteCode: currentCode} } platform := innerCodeRawText(item.Raw["purchasePlatform"]) purchaseCode := innerCodeRawText(item.Raw["purchaseCode"]) if platform != "" || purchaseCode != "" { return innerCodeApplyOutcome{Status: model.InnerCodeSkipped, Message: "顺运宝商品已有采购平台或采购单号,停止回写", RemoteCode: currentCode} } if currentCode == record.InnerCode { return innerCodeApplyOutcome{Status: model.InnerCodeAlreadyFilled, Message: "写入前核验发现远端已是目标入库码,无需重复写入", RemoteCode: currentCode} } if currentCode != record.RemoteInnerCode { return innerCodeApplyOutcome{Status: model.InnerCodeNeedsCheck, Message: "远端快递单号在规划后发生变化,已停止回写,请人工核对", RemoteCode: currentCode} } if 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} } 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 = "写入结果未知,禁止自动重试,请重新核对" } 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} } func readCurrentInnerCodeDetail(ctx context.Context, reader InnerCodeDetailReader, record model.InnerCodeRecord) (*syb.DetailItem, error) { if record.StockID <= 0 || record.DetailID <= 0 { return nil, fmt.Errorf("记录缺少有效的货运单或商品明细 ID") } stocks, err := reader.DetailListByStock(ctx, []int64{record.StockID}) if err != nil { return nil, err } if len(stocks) != 1 || stocks[0].ID != record.StockID { return nil, fmt.Errorf("顺运宝没有唯一返回货运单 id=%d", record.StockID) } var found *syb.DetailItem for index := range stocks[0].Details { if stocks[0].Details[index].ID != record.DetailID { continue } if found != nil { return 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 found, nil } // RecheckInnerCode 只重新读取一条 needs_check 记录,不发送任何写请求。 func RecheckInnerCode(ctx context.Context, db *sql.DB, reader InnerCodeDetailReader, id int64) (model.InnerCodeStatus, string, error) { record, err := repository.GetInnerCodeForRecheck(db, id) if err != nil { return "", "", err } if record == nil { return "", "", fmt.Errorf("档口入库码记录不存在") } if record.Status != model.InnerCodeNeedsCheck { return record.Status, "当前记录不需要核对", nil } item, readErr := readCurrentInnerCodeDetail(ctx, reader, *record) status := model.InnerCodeNeedsCheck remoteCode := record.RemoteInnerCode message := "重新读取失败,仍需人工核对:" + errorText(readErr) if readErr == nil { remoteCode = innerCodeRawText(item.Raw["innerExpCode"]) if !innerCodeDetailIdentityMatches(*record, *item) { message = "重新读取到的商品身份与规划不一致;保持需核对,系统没有写入" } else if remoteCode == record.InnerCode { status = model.InnerCodeUpdated message = "重新读取确认远端已是目标入库码;没有重复写入" } else { message = "重新读取后远端仍不是目标入库码;保持需核对,系统没有写入" } } checkedAt := model.NowISO() message = compactInnerCodeMessage(message) if err := repository.SaveInnerCodeRecheck(db, id, status, message, remoteCode, checkedAt); err != nil { return "", "", err } return status, message, nil } func innerCodeDetailIdentityMatches(record model.InnerCodeRecord, item syb.DetailItem) bool { return item.ProductSpec == record.SybSpec && innerCodeRawText(item.Raw["sku"]) == record.SybSKU && innerCodeRawText(item.Raw["variationSku"]) == record.SybVariationSKU } func errorText(err error) string { if err == nil { return "" } return err.Error() } func uniquePositiveInnerCodeIDs(ids []int64) []int64 { seen := make(map[int64]bool, len(ids)) result := make([]int64, 0, len(ids)) for _, id := range ids { if id > 0 && !seen[id] { seen[id] = true result = append(result, id) } } return result } // InterruptApplyingInnerCodes 在 Admin 启动时收敛未确认的远端写结果。 func InterruptApplyingInnerCodes(db *sql.DB, now time.Time) (int, error) { return repository.InterruptApplyingInnerCodes(db, now.UTC().Format(model.TimeLayout)) } func compactInnerCodeMessage(message string) string { message = strings.TrimSpace(message) if len([]rune(message)) <= 500 { return message } return string([]rune(message)[:500]) }