Files
cmautobuy/admin/service/inner_code_apply.go
T

244 lines
8.8 KiB
Go

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) > InnerCodeApplyBatchLimit {
return nil, fmt.Errorf("单次最多回写 %d 条记录", InnerCodeApplyBatchLimit)
}
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])
}