作废链接和取消订单

This commit is contained in:
yml2213
2026-08-14 12:58:20 +08:00
parent 33a30fcd99
commit ee3e427ab7
11 changed files with 364 additions and 39 deletions
+54 -6
View File
@@ -6,6 +6,7 @@ import (
"math"
"strings"
"time"
"unicode/utf8"
"affiliate_dash/internal/model"
@@ -446,12 +447,52 @@ func (s *FulfillmentService) UpdateFulfillment(in FulfillmentUpdateInput) (*mode
return &out, nil
}
// CancelOrderInput 取消订单的公共输入;开放 API 与商户后台共用同一取消事务。
type CancelOrderInput struct {
MerchantID uint
APIClientID uint
ActorUserID uint
OrderNo string
Reason string
Action string
}
// CancelOrder 商户开放接口取消订单(审计记录 API 客户端)。
func (s *FulfillmentService) CancelOrder(merchantID, apiClientID uint, orderNo, reason string) (*model.FulfillmentOrder, error) {
return s.cancelOrder(CancelOrderInput{
MerchantID: merchantID,
APIClientID: apiClientID,
OrderNo: orderNo,
Reason: reason,
Action: "open_order.cancel",
})
}
// CancelOrderByUser 商户后台取消订单(审计记录操作人)。
func (s *FulfillmentService) CancelOrderByUser(merchantID, actorUserID uint, orderNo, reason string) (*model.FulfillmentOrder, error) {
return s.cancelOrder(CancelOrderInput{
MerchantID: merchantID,
ActorUserID: actorUserID,
OrderNo: orderNo,
Reason: reason,
Action: "merchant_order.cancel",
})
}
func (s *FulfillmentService) cancelOrder(in CancelOrderInput) (*model.FulfillmentOrder, error) {
in.OrderNo = strings.TrimSpace(in.OrderNo)
in.Reason = strings.TrimSpace(in.Reason)
if in.MerchantID == 0 || in.OrderNo == "" {
return nil, errors.New("无效的商户或订单号")
}
if utf8.RuneCountInString(in.Reason) > 512 {
return nil, errors.New("取消原因最长 512 字")
}
var out model.FulfillmentOrder
err := s.db.Transaction(func(tx *gorm.DB) error {
var order model.FulfillmentOrder
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Where("merchant_id = ? AND order_no = ?", merchantID, orderNo).
Where("merchant_id = ? AND order_no = ?", in.MerchantID, in.OrderNo).
First(&order).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New("订单不存在")
@@ -465,15 +506,20 @@ func (s *FulfillmentService) CancelOrder(merchantID, apiClientID uint, orderNo,
if err := canCancelOrder(&order); err != nil {
return err
}
// 已提交上游正式订单的失败订单禁止取消:退款后上游仍可能继续发货,造成双重损失。
// 与发货重试的防重复逻辑(deliverySubmittedUpstream)保持一致。
if normalizeOrderStatus(&order) == model.OrderStatusShipFailed && deliverySubmittedUpstream(&order) {
return errors.New("订单已提交上游发货,为避免重复发货无法取消,请先在上游确认订单状态")
}
now := time.Now()
updates := map[string]interface{}{"order_status": model.OrderStatusCancelled}
updates["failure_reason"] = reason
updates["failure_reason"] = in.Reason
updates["cancelled_at"] = now
if err := tx.Model(&order).Updates(updates).Error; err != nil {
return err
}
var wallet model.WalletAccount
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("merchant_id = ?", merchantID).First(&wallet).Error; err != nil {
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("merchant_id = ?", in.MerchantID).First(&wallet).Error; err != nil {
return err
}
newBalance := wallet.AvailableBalance + order.Amount
@@ -482,7 +528,7 @@ func (s *FulfillmentService) CancelOrder(merchantID, apiClientID uint, orderNo,
}
idempotencyKey := "cancel:" + order.OrderNo
if err := tx.Create(&model.WalletLedgerEntry{
MerchantID: merchantID,
MerchantID: in.MerchantID,
WalletAccountID: wallet.ID,
EntryNo: "WL" + uuid.NewString(),
Type: model.WalletLedgerRefund,
@@ -507,11 +553,13 @@ func (s *FulfillmentService) CancelOrder(merchantID, apiClientID uint, orderNo,
if err := tx.First(&out, order.ID).Error; err != nil {
return err
}
if err := writeAudit(tx, &merchantID, nil, &apiClientID, "open_order.cancel", "fulfillment_order", order.OrderNo, nil); err != nil {
if err := writeAudit(tx, &in.MerchantID, optionalUint(in.ActorUserID), optionalUint(in.APIClientID), in.Action, "fulfillment_order", order.OrderNo, map[string]interface{}{
"reason": in.Reason,
}); err != nil {
return err
}
if s.callbacks != nil {
if err := s.callbacks.Enqueue(tx, merchantID, "order.cancelled", orderCallbackData(&out)); err != nil {
if err := s.callbacks.Enqueue(tx, in.MerchantID, "order.cancelled", orderCallbackData(&out)); err != nil {
return err
}
}