优化发货接口对接流程

This commit is contained in:
yml2213
2026-07-30 23:44:50 +08:00
parent a16e518b6c
commit ee956e2e01
14 changed files with 428 additions and 141 deletions
+108 -95
View File
@@ -7,6 +7,7 @@ import (
"math"
"strings"
"time"
"unicode/utf8"
"affiliate_dash/internal/model"
@@ -764,7 +765,7 @@ type OpenOrderProduct struct {
// ShipNotifyInput 上游发货结果推送。
type ShipNotifyInput struct {
OrderNo string
ShipStatus string // success / failed / processing
ShipStatus string // success / failed
ProviderOrderNo string
ShippedAt *time.Time
FailReason string
@@ -836,110 +837,113 @@ func (s *FulfillmentService) QueryOpenOrder(orderNo string) (*OpenOrderQuery, er
// HandleShipNotify 处理上游发货结果推送(幂等),基于 FulfillmentOrder。
func (s *FulfillmentService) HandleShipNotify(in ShipNotifyInput) (*ShipNotifyResult, error) {
in.ShipStatus = strings.TrimSpace(in.ShipStatus)
in.FailReason = strings.TrimSpace(in.FailReason)
if in.OrderNo == "" {
return nil, errors.New("订单号不能为空")
}
if in.ShipStatus != "success" && in.ShipStatus != "failed" {
return nil, errors.New("无效的 ship_status,仅支持 success/failed")
}
if in.ShipStatus == "failed" && in.FailReason == "" {
return nil, errors.New("发货失败时 fail_reason 必填")
}
if utf8.RuneCountInString(in.FailReason) > 512 {
return nil, errors.New("fail_reason 最长 512 个字符")
}
var nextStatus string
switch in.ShipStatus {
case "success":
nextStatus = model.FulfillmentStatusSucceeded
case "failed":
nextStatus = model.FulfillmentStatusFailed
case "processing":
nextStatus = model.FulfillmentStatusProcessing
default:
return nil, errors.New("无效的 ship_status,仅支持 success/failed/processing")
}
order, err := s.GetByOrderNo(in.OrderNo)
if err != nil {
return nil, err
}
// 已履约成功:success 推送幂等成功
if order.FulfillmentStatus == model.FulfillmentStatusSucceeded && in.ShipStatus == "success" {
_ = s.writeShipNotifyAudit(order, in, order.FulfillmentStatus, "订单已履约成功,幂等忽略")
return &ShipNotifyResult{
OrderNo: order.OrderNo,
Status: fulfillmentStatusToLegacyStatus(order.FulfillmentStatus),
Message: "订单已交付,幂等成功",
}, nil
}
// 已取消不允许再推
if order.FulfillmentStatus == model.FulfillmentStatusCancelled {
_ = s.writeShipNotifyAudit(order, in, order.FulfillmentStatus, "订单已取消,拒绝更新")
return nil, errors.New("订单已取消,无法更新发货状态")
}
now := time.Now()
shippedAt := in.ShippedAt
if shippedAt == nil && in.ShipStatus == "success" {
shippedAt = &now
}
updates := map[string]interface{}{
"fulfillment_status": nextStatus,
}
var msg string
switch in.ShipStatus {
case "success":
// 仅 pending / failed / processing 可转为 succeeded
if order.FulfillmentStatus != model.FulfillmentStatusPending &&
order.FulfillmentStatus != model.FulfillmentStatusFailed &&
order.FulfillmentStatus != model.FulfillmentStatusProcessing {
_ = s.writeShipNotifyAudit(order, in, order.FulfillmentStatus, "当前状态不允许标记发货成功")
return nil, fmt.Errorf("当前状态 %s 不允许标记发货成功", order.FulfillmentStatus)
var result ShipNotifyResult
var rejectionErr error
if err := s.db.Transaction(func(tx *gorm.DB) error {
var order model.FulfillmentOrder
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Preload("MerchantProduct.Product").
Where("order_no = ?", in.OrderNo).
First(&order).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New("订单不存在")
}
return err
}
updates["delivered_at"] = shippedAt
updates["failure_reason"] = ""
if in.ProviderOrderNo != "" {
updates["provider_order_no"] = in.ProviderOrderNo
}
msg = "发货成功,订单已交付"
case "failed":
if order.FulfillmentStatus == model.FulfillmentStatusSucceeded {
_ = s.writeShipNotifyAudit(order, in, order.FulfillmentStatus, "订单已交付,忽略失败推送")
return nil, errors.New("订单已交付,不能标记发货失败")
}
updates["failure_reason"] = in.FailReason
if in.ProviderOrderNo != "" {
updates["provider_order_no"] = in.ProviderOrderNo
}
msg = "已记录发货失败"
case "processing":
if order.FulfillmentStatus == model.FulfillmentStatusSucceeded {
_ = s.writeShipNotifyAudit(order, in, order.FulfillmentStatus, "订单已交付,忽略发货中推送")
return &ShipNotifyResult{
// 已履约成功:success 推送幂等成功。状态读取和后续更新必须在同一把行锁内完成。
if order.FulfillmentStatus == model.FulfillmentStatusSucceeded && in.ShipStatus == "success" {
result = ShipNotifyResult{
OrderNo: order.OrderNo,
Status: fulfillmentStatusToLegacyStatus(order.FulfillmentStatus),
Message: "订单已交付,忽略 processing",
}, nil
Message: "订单已交付,幂等成功",
}
return writeAudit(tx, &order.MerchantID, nil, nil, "ship.notify", "fulfillment_order", order.OrderNo,
shipNotifyAuditMetadata(in, order.FulfillmentStatus, "订单已履约成功,幂等忽略"))
}
if order.FulfillmentStatus != model.FulfillmentStatusPending &&
order.FulfillmentStatus != model.FulfillmentStatusFailed &&
order.FulfillmentStatus != model.FulfillmentStatusProcessing {
_ = s.writeShipNotifyAudit(order, in, order.FulfillmentStatus, "当前状态不允许进入发货中")
return nil, fmt.Errorf("当前状态 %s 不允许进入发货中", order.FulfillmentStatus)
}
if in.ProviderOrderNo != "" {
updates["provider_order_no"] = in.ProviderOrderNo
}
msg = "订单已标记为发货中"
}
resultData := buildShipNotifyResultData(order.ResultData, in, shippedAt)
updates["result_data"] = resultData
var updated model.FulfillmentOrder
if err := s.db.Transaction(func(tx *gorm.DB) error {
if order.FulfillmentStatus == model.FulfillmentStatusCancelled {
if err := writeShipNotifyRejectedAudit(tx, &order, in, "订单已取消,拒绝更新"); err != nil {
return err
}
rejectionErr = errors.New("订单已取消,无法更新发货状态")
return nil
}
now := time.Now()
shippedAt := in.ShippedAt
if shippedAt == nil && in.ShipStatus == "success" {
shippedAt = &now
}
updates := map[string]interface{}{
"fulfillment_status": nextStatus,
}
var message string
switch in.ShipStatus {
case "success":
if order.FulfillmentStatus != model.FulfillmentStatusPending &&
order.FulfillmentStatus != model.FulfillmentStatusFailed &&
order.FulfillmentStatus != model.FulfillmentStatusProcessing {
if err := writeShipNotifyRejectedAudit(tx, &order, in, "当前状态不允许标记发货成功"); err != nil {
return err
}
rejectionErr = fmt.Errorf("当前状态 %s 不允许标记发货成功", order.FulfillmentStatus)
return nil
}
updates["delivered_at"] = shippedAt
updates["failure_reason"] = ""
if in.ProviderOrderNo != "" {
updates["provider_order_no"] = in.ProviderOrderNo
}
message = "发货成功,订单已交付"
case "failed":
if order.FulfillmentStatus == model.FulfillmentStatusSucceeded {
if err := writeShipNotifyRejectedAudit(tx, &order, in, "订单已交付,拒绝失败推送"); err != nil {
return err
}
rejectionErr = errors.New("订单已交付,不能标记发货失败")
return nil
}
updates["failure_reason"] = in.FailReason
if in.ProviderOrderNo != "" {
updates["provider_order_no"] = in.ProviderOrderNo
}
message = "已记录发货失败"
}
resultData := buildShipNotifyResultData(order.ResultData, in, shippedAt)
updates["result_data"] = resultData
if err := tx.Model(&model.FulfillmentOrder{}).Where("id = ?", order.ID).Updates(updates).Error; err != nil {
return err
}
jobStatus := model.FulfillmentJobStatusProcessing
jobStatus := model.FulfillmentJobStatusFailed
if nextStatus == model.FulfillmentStatusSucceeded {
jobStatus = model.FulfillmentJobStatusSucceeded
} else if nextStatus == model.FulfillmentStatusFailed {
jobStatus = model.FulfillmentJobStatusFailed
}
jobUpdates := map[string]interface{}{
"status": jobStatus,
@@ -952,10 +956,13 @@ func (s *FulfillmentService) HandleShipNotify(in ShipNotifyInput) (*ShipNotifyRe
if err := tx.Model(&model.FulfillmentJob{}).Where("order_id = ?", order.ID).Updates(jobUpdates).Error; err != nil {
return err
}
metadata := shipNotifyAuditMetadata(in, nextStatus, msg)
if err := writeAudit(tx, &order.MerchantID, nil, nil, "ship.notify", "fulfillment_order", order.OrderNo, metadata); err != nil {
if err := writeAudit(tx, &order.MerchantID, nil, nil, "ship.notify", "fulfillment_order", order.OrderNo,
shipNotifyAuditMetadata(in, nextStatus, message)); err != nil {
return err
}
var updated model.FulfillmentOrder
if err := tx.Preload("MerchantProduct.Product").First(&updated, order.ID).Error; err != nil {
return err
}
@@ -964,21 +971,25 @@ func (s *FulfillmentService) HandleShipNotify(in ShipNotifyInput) (*ShipNotifyRe
return err
}
}
result = ShipNotifyResult{
OrderNo: order.OrderNo,
Status: fulfillmentStatusToLegacyStatus(nextStatus),
Message: message,
}
return nil
}); err != nil {
return nil, err
}
if rejectionErr != nil {
return nil, rejectionErr
}
return &ShipNotifyResult{
OrderNo: order.OrderNo,
Status: fulfillmentStatusToLegacyStatus(nextStatus),
Message: msg,
}, nil
return &result, nil
}
// writeShipNotifyAudit 将上游推送留痕写入 AuditLog(替代旧的 ShipLog 表)。
func (s *FulfillmentService) writeShipNotifyAudit(order *model.FulfillmentOrder, in ShipNotifyInput, resultStatus, message string) error {
return writeAudit(s.db, &order.MerchantID, nil, nil, "ship.notify", "fulfillment_order", order.OrderNo, shipNotifyAuditMetadata(in, resultStatus, message))
func writeShipNotifyRejectedAudit(tx *gorm.DB, order *model.FulfillmentOrder, in ShipNotifyInput, message string) error {
return writeAudit(tx, &order.MerchantID, nil, nil, "ship.notify", "fulfillment_order", order.OrderNo,
shipNotifyAuditMetadata(in, order.FulfillmentStatus, message))
}
func shipNotifyAuditMetadata(in ShipNotifyInput, resultStatus, message string) map[string]interface{} {
@@ -1003,7 +1014,9 @@ func buildShipNotifyResultData(existing string, in ShipNotifyInput, shippedAt *t
if in.ProviderOrderNo != "" {
m["provider_order_no"] = in.ProviderOrderNo
}
if in.FailReason != "" {
if in.ShipStatus == "success" {
delete(m, "fail_reason")
} else if in.FailReason != "" {
m["fail_reason"] = in.FailReason
}
if shippedAt != nil {