作废链接和取消订单
This commit is contained in:
@@ -189,6 +189,28 @@ func (h *MerchantHandler) CreateTestOrder(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type merchantCancelOrderReq struct {
|
||||||
|
Reason string `json:"reason" binding:"required,max=512"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CancelOrder 商户后台取消订单:全额退还积分并回补库存,取消后订单不可再发货。
|
||||||
|
func (h *MerchantHandler) CancelOrder(c *gin.Context) {
|
||||||
|
var req merchantCancelOrderReq
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.BadRequest(c, "参数错误:取消原因必填且最长 512 字")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
order, err := h.fulfillmentSvc.CancelOrderByUser(middleware.GetMerchantID(c), middleware.GetUserID(c), c.Param("order_no"), req.Reason)
|
||||||
|
if err != nil {
|
||||||
|
response.BadRequest(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.OK(c, gin.H{
|
||||||
|
"order": order,
|
||||||
|
"refund_amount": order.Amount,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (h *MerchantHandler) GetDeliveryLink(c *gin.Context) {
|
func (h *MerchantHandler) GetDeliveryLink(c *gin.Context) {
|
||||||
link, err := h.deliverySvc.GetOrCreateDeliveryLink(middleware.GetMerchantID(c), c.Param("order_no"), requestBaseURL(c))
|
link, err := h.deliverySvc.GetOrCreateDeliveryLink(middleware.GetMerchantID(c), c.Param("order_no"), requestBaseURL(c))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -199,7 +221,7 @@ func (h *MerchantHandler) GetDeliveryLink(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *MerchantHandler) RevokeDeliveryLink(c *gin.Context) {
|
func (h *MerchantHandler) RevokeDeliveryLink(c *gin.Context) {
|
||||||
if err := h.deliverySvc.RevokeDeliveryLink(middleware.GetMerchantID(c), c.Param("order_no")); err != nil {
|
if err := h.deliverySvc.RevokeDeliveryLink(middleware.GetMerchantID(c), middleware.GetUserID(c), c.Param("order_no")); err != nil {
|
||||||
writeDeliveryError(c, err)
|
writeDeliveryError(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -207,7 +229,7 @@ func (h *MerchantHandler) RevokeDeliveryLink(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *MerchantHandler) RestoreDeliveryLink(c *gin.Context) {
|
func (h *MerchantHandler) RestoreDeliveryLink(c *gin.Context) {
|
||||||
link, err := h.deliverySvc.RestoreDeliveryLink(middleware.GetMerchantID(c), c.Param("order_no"), requestBaseURL(c))
|
link, err := h.deliverySvc.RestoreDeliveryLink(middleware.GetMerchantID(c), middleware.GetUserID(c), c.Param("order_no"), requestBaseURL(c))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeDeliveryError(c, err)
|
writeDeliveryError(c, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ func (h *OpenV1Handler) QueryOrder(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type openCancelOrderReq struct {
|
type openCancelOrderReq struct {
|
||||||
Reason string `json:"reason"`
|
Reason string `json:"reason" binding:"max=512"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *OpenV1Handler) CancelOrder(c *gin.Context) {
|
func (h *OpenV1Handler) CancelOrder(c *gin.Context) {
|
||||||
@@ -129,7 +129,7 @@ func (h *OpenV1Handler) CancelOrder(c *gin.Context) {
|
|||||||
var req openCancelOrderReq
|
var req openCancelOrderReq
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
openlog.Warn(c, "cancel_order bind_fail err=%v", err)
|
openlog.Warn(c, "cancel_order bind_fail err=%v", err)
|
||||||
response.BadRequest(c, "参数错误")
|
response.BadRequest(c, "参数错误:reason 最长 512 字")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
orderNo := c.Param("order_no")
|
orderNo := c.Param("order_no")
|
||||||
|
|||||||
@@ -163,6 +163,7 @@ func Setup(h *Handlers) *gin.Engine {
|
|||||||
merchant.GET("/orders/:order_no/delivery-link", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureOrders), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionOrdersManage), h.Merchant.GetDeliveryLink)
|
merchant.GET("/orders/:order_no/delivery-link", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureOrders), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionOrdersManage), h.Merchant.GetDeliveryLink)
|
||||||
merchant.POST("/orders/:order_no/delivery-link/revoke", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureOrders), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionOrdersManage), h.Merchant.RevokeDeliveryLink)
|
merchant.POST("/orders/:order_no/delivery-link/revoke", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureOrders), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionOrdersManage), h.Merchant.RevokeDeliveryLink)
|
||||||
merchant.POST("/orders/:order_no/delivery-link/restore", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureOrders), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionOrdersManage), h.Merchant.RestoreDeliveryLink)
|
merchant.POST("/orders/:order_no/delivery-link/restore", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureOrders), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionOrdersManage), h.Merchant.RestoreDeliveryLink)
|
||||||
|
merchant.POST("/orders/:order_no/cancel", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureOrders), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionOrdersManage), h.Merchant.CancelOrder)
|
||||||
merchant.GET("/wallet", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureWallet), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionWalletView), h.Merchant.GetWallet)
|
merchant.GET("/wallet", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureWallet), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionWalletView), h.Merchant.GetWallet)
|
||||||
merchant.GET("/wallet/ledger", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureWallet), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionWalletLedger), h.Merchant.ListWalletLedger)
|
merchant.GET("/wallet/ledger", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureWallet), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionWalletLedger), h.Merchant.ListWalletLedger)
|
||||||
merchant.POST("/recharge/applications", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureWallet), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionRechargeManage), h.Recharge.CreateApplication)
|
merchant.POST("/recharge/applications", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureWallet), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionRechargeManage), h.Recharge.CreateApplication)
|
||||||
|
|||||||
@@ -617,6 +617,10 @@ func (s *DeliveryService) GetOrCreateDeliveryLink(merchantID uint, orderNo, requ
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
// 订单已取消后禁止获取发货链接(含复用未过期链接的路径),与 authorizeDeliveryLink 保持一致。
|
||||||
|
if normalizeOrderStatus(order) == model.OrderStatusCancelled {
|
||||||
|
return nil, newDeliveryHTTPError(http.StatusBadRequest, "订单已取消,无法获取发货链接")
|
||||||
|
}
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
if order.DeliveryLinkRevokedAt != nil {
|
if order.DeliveryLinkRevokedAt != nil {
|
||||||
return nil, newDeliveryHTTPError(http.StatusForbidden, "发货链接已作废")
|
return nil, newDeliveryHTTPError(http.StatusForbidden, "发货链接已作废")
|
||||||
@@ -643,7 +647,7 @@ func (s *DeliveryService) GetOrCreateDeliveryLink(merchantID uint, orderNo, requ
|
|||||||
return s.buildDeliveryLinkResult(order.OrderNo, expiresAt, requestBaseURL), nil
|
return s.buildDeliveryLinkResult(order.OrderNo, expiresAt, requestBaseURL), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DeliveryService) RevokeDeliveryLink(merchantID uint, orderNo string) error {
|
func (s *DeliveryService) RevokeDeliveryLink(merchantID, actorUserID uint, orderNo string) error {
|
||||||
orderNo = strings.TrimSpace(orderNo)
|
orderNo = strings.TrimSpace(orderNo)
|
||||||
if orderNo == "" {
|
if orderNo == "" {
|
||||||
return errors.New("订单号不能为空")
|
return errors.New("订单号不能为空")
|
||||||
@@ -653,14 +657,21 @@ func (s *DeliveryService) RevokeDeliveryLink(merchantID uint, orderNo string) er
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
now := time.Now().UTC().Truncate(time.Second)
|
now := time.Now().UTC().Truncate(time.Second)
|
||||||
return s.fulfillment.db.Model(&model.FulfillmentOrder{}).
|
return s.fulfillment.db.Transaction(func(tx *gorm.DB) error {
|
||||||
Where("id = ?", order.ID).
|
if err := tx.Model(&model.FulfillmentOrder{}).
|
||||||
Updates(map[string]interface{}{
|
Where("id = ?", order.ID).
|
||||||
"delivery_link_revoked_at": now,
|
Updates(map[string]interface{}{
|
||||||
}).Error
|
"delivery_link_revoked_at": now,
|
||||||
|
}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return writeAudit(tx, &merchantID, optionalUint(actorUserID), nil, "delivery_link.revoke", "fulfillment_order", order.OrderNo, map[string]interface{}{
|
||||||
|
"revoked_at": timeutil.FormatAPITime(now),
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DeliveryService) RestoreDeliveryLink(merchantID uint, orderNo, requestBaseURL string) (*DeliveryLinkResult, error) {
|
func (s *DeliveryService) RestoreDeliveryLink(merchantID, actorUserID uint, orderNo, requestBaseURL string) (*DeliveryLinkResult, error) {
|
||||||
orderNo = strings.TrimSpace(orderNo)
|
orderNo = strings.TrimSpace(orderNo)
|
||||||
if orderNo == "" {
|
if orderNo == "" {
|
||||||
return nil, errors.New("订单号不能为空")
|
return nil, errors.New("订单号不能为空")
|
||||||
@@ -680,12 +691,19 @@ func (s *DeliveryService) RestoreDeliveryLink(merchantID uint, orderNo, requestB
|
|||||||
if order.DeliveryLinkExpiresAt != nil && expiresAt.Equal(order.DeliveryLinkExpiresAt.UTC().Truncate(time.Second)) {
|
if order.DeliveryLinkExpiresAt != nil && expiresAt.Equal(order.DeliveryLinkExpiresAt.UTC().Truncate(time.Second)) {
|
||||||
expiresAt = expiresAt.Add(time.Second)
|
expiresAt = expiresAt.Add(time.Second)
|
||||||
}
|
}
|
||||||
if err := s.fulfillment.db.Model(&model.FulfillmentOrder{}).
|
if err := s.fulfillment.db.Transaction(func(tx *gorm.DB) error {
|
||||||
Where("id = ?", order.ID).
|
if err := tx.Model(&model.FulfillmentOrder{}).
|
||||||
Updates(map[string]interface{}{
|
Where("id = ?", order.ID).
|
||||||
"delivery_link_expires_at": expiresAt,
|
Updates(map[string]interface{}{
|
||||||
"delivery_link_revoked_at": nil,
|
"delivery_link_expires_at": expiresAt,
|
||||||
}).Error; err != nil {
|
"delivery_link_revoked_at": nil,
|
||||||
|
}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return writeAudit(tx, &merchantID, optionalUint(actorUserID), nil, "delivery_link.restore", "fulfillment_order", order.OrderNo, map[string]interface{}{
|
||||||
|
"expires_at": timeutil.FormatAPITime(expiresAt),
|
||||||
|
})
|
||||||
|
}); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return s.buildDeliveryLinkResult(order.OrderNo, expiresAt, requestBaseURL), nil
|
return s.buildDeliveryLinkResult(order.OrderNo, expiresAt, requestBaseURL), nil
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ func (s *DeliveryService) authorizeDeliveryLink(order *model.FulfillmentOrder, a
|
|||||||
if order.DeliveryLinkRevokedAt != nil {
|
if order.DeliveryLinkRevokedAt != nil {
|
||||||
return newDeliveryHTTPError(http.StatusForbidden, "发货链接已作废")
|
return newDeliveryHTTPError(http.StatusForbidden, "发货链接已作废")
|
||||||
}
|
}
|
||||||
|
// 订单已取消后链接立即失效,防止已退款订单仍通过链接查看或继续发货。
|
||||||
|
if normalizeOrderStatus(order) == model.OrderStatusCancelled {
|
||||||
|
return newDeliveryHTTPError(http.StatusForbidden, "订单已取消,发货链接不可用")
|
||||||
|
}
|
||||||
if order.DeliveryLinkExpiresAt == nil {
|
if order.DeliveryLinkExpiresAt == nil {
|
||||||
return newDeliveryHTTPError(http.StatusForbidden, "发货链接未生成")
|
return newDeliveryHTTPError(http.StatusForbidden, "发货链接未生成")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ func TestDeliveryLinkGenerateAuthorizeAndRevoke(t *testing.T) {
|
|||||||
t.Fatalf("authorize delivery link: %v", err)
|
t.Fatalf("authorize delivery link: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := deliverySvc.RevokeDeliveryLink(merchantID, created.Order.OrderNo); err != nil {
|
if err := deliverySvc.RevokeDeliveryLink(merchantID, 0, created.Order.OrderNo); err != nil {
|
||||||
t.Fatalf("revoke delivery link: %v", err)
|
t.Fatalf("revoke delivery link: %v", err)
|
||||||
}
|
}
|
||||||
if _, err := deliverySvc.GetOrder(created.Order.OrderNo, DeliveryLinkAuth{Exp: link.Exp, Sign: link.Sign}); err == nil {
|
if _, err := deliverySvc.GetOrder(created.Order.OrderNo, DeliveryLinkAuth{Exp: link.Exp, Sign: link.Sign}); err == nil {
|
||||||
@@ -61,7 +61,7 @@ func TestDeliveryLinkGenerateAuthorizeAndRevoke(t *testing.T) {
|
|||||||
t.Fatalf("delivery_link_revoked_at should be stored")
|
t.Fatalf("delivery_link_revoked_at should be stored")
|
||||||
}
|
}
|
||||||
|
|
||||||
restored, err := deliverySvc.RestoreDeliveryLink(merchantID, created.Order.OrderNo, "https://shop.example")
|
restored, err := deliverySvc.RestoreDeliveryLink(merchantID, 0, created.Order.OrderNo, "https://shop.example")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("restore delivery link: %v", err)
|
t.Fatalf("restore delivery link: %v", err)
|
||||||
}
|
}
|
||||||
@@ -78,6 +78,44 @@ func TestDeliveryLinkGenerateAuthorizeAndRevoke(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDeliveryLinkRejectedAfterOrderCancelled(t *testing.T) {
|
||||||
|
db := newServiceTestDB(t)
|
||||||
|
merchantID, product := seedFulfillmentMerchant(t, db, "delivery-cancel", 5000, 5, 100)
|
||||||
|
fulfillmentSvc := NewFulfillmentService(db, nil)
|
||||||
|
deliverySvc := NewDeliveryService(fulfillmentSvc, "https://bff.example", "dlc", "https://shop.example", "link-secret", 60)
|
||||||
|
|
||||||
|
created, err := fulfillmentSvc.CreateOrder(CreateFulfillmentOrderInput{
|
||||||
|
MerchantID: merchantID,
|
||||||
|
APIClientID: 1,
|
||||||
|
ClientOrderNo: "delivery-cancel-001",
|
||||||
|
SKU: product.SKU,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create order: %v", err)
|
||||||
|
}
|
||||||
|
link, err := deliverySvc.GetOrCreateDeliveryLink(merchantID, created.Order.OrderNo, "https://shop.example")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("get delivery link: %v", err)
|
||||||
|
}
|
||||||
|
// 未提交上游的已支付订单允许取消。
|
||||||
|
if _, err := fulfillmentSvc.CancelOrderByUser(merchantID, 1, created.Order.OrderNo, "测试取消"); err != nil {
|
||||||
|
t.Fatalf("cancel order: %v", err)
|
||||||
|
}
|
||||||
|
// 取消后既有链接立即失效,无法再打开。
|
||||||
|
if _, err := deliverySvc.GetOrder(created.Order.OrderNo, DeliveryLinkAuth{Exp: link.Exp, Sign: link.Sign}); err == nil {
|
||||||
|
t.Fatalf("cancelled order link should be rejected")
|
||||||
|
} else if sc, ok := err.(interface{ HTTPStatus() int }); !ok || sc.HTTPStatus() != 403 {
|
||||||
|
t.Fatalf("expected 403 for cancelled order link, got %v", err)
|
||||||
|
}
|
||||||
|
// 取消后也不能再生成新链接或恢复链接。
|
||||||
|
if _, err := deliverySvc.GetOrCreateDeliveryLink(merchantID, created.Order.OrderNo, "https://shop.example"); err == nil {
|
||||||
|
t.Fatalf("cancelled order should not generate a new link")
|
||||||
|
}
|
||||||
|
if _, err := deliverySvc.RestoreDeliveryLink(merchantID, 1, created.Order.OrderNo, "https://shop.example"); err == nil {
|
||||||
|
t.Fatalf("cancelled order should not restore a link")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDeliveryGoodIDIncludesMedalAndCoinProducts(t *testing.T) {
|
func TestDeliveryGoodIDIncludesMedalAndCoinProducts(t *testing.T) {
|
||||||
cases := map[string]string{
|
cases := map[string]string{
|
||||||
"honor_medal_x2": "682ef39ca8f40c4234c59f45",
|
"honor_medal_x2": "682ef39ca8f40c4234c59f45",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
"affiliate_dash/internal/model"
|
"affiliate_dash/internal/model"
|
||||||
|
|
||||||
@@ -446,12 +447,52 @@ func (s *FulfillmentService) UpdateFulfillment(in FulfillmentUpdateInput) (*mode
|
|||||||
return &out, nil
|
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) {
|
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
|
var out model.FulfillmentOrder
|
||||||
err := s.db.Transaction(func(tx *gorm.DB) error {
|
err := s.db.Transaction(func(tx *gorm.DB) error {
|
||||||
var order model.FulfillmentOrder
|
var order model.FulfillmentOrder
|
||||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
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 {
|
First(&order).Error; err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return errors.New("订单不存在")
|
return errors.New("订单不存在")
|
||||||
@@ -465,15 +506,20 @@ func (s *FulfillmentService) CancelOrder(merchantID, apiClientID uint, orderNo,
|
|||||||
if err := canCancelOrder(&order); err != nil {
|
if err := canCancelOrder(&order); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
// 已提交上游正式订单的失败订单禁止取消:退款后上游仍可能继续发货,造成双重损失。
|
||||||
|
// 与发货重试的防重复逻辑(deliverySubmittedUpstream)保持一致。
|
||||||
|
if normalizeOrderStatus(&order) == model.OrderStatusShipFailed && deliverySubmittedUpstream(&order) {
|
||||||
|
return errors.New("订单已提交上游发货,为避免重复发货无法取消,请先在上游确认订单状态")
|
||||||
|
}
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
updates := map[string]interface{}{"order_status": model.OrderStatusCancelled}
|
updates := map[string]interface{}{"order_status": model.OrderStatusCancelled}
|
||||||
updates["failure_reason"] = reason
|
updates["failure_reason"] = in.Reason
|
||||||
updates["cancelled_at"] = now
|
updates["cancelled_at"] = now
|
||||||
if err := tx.Model(&order).Updates(updates).Error; err != nil {
|
if err := tx.Model(&order).Updates(updates).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
var wallet model.WalletAccount
|
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
|
return err
|
||||||
}
|
}
|
||||||
newBalance := wallet.AvailableBalance + order.Amount
|
newBalance := wallet.AvailableBalance + order.Amount
|
||||||
@@ -482,7 +528,7 @@ func (s *FulfillmentService) CancelOrder(merchantID, apiClientID uint, orderNo,
|
|||||||
}
|
}
|
||||||
idempotencyKey := "cancel:" + order.OrderNo
|
idempotencyKey := "cancel:" + order.OrderNo
|
||||||
if err := tx.Create(&model.WalletLedgerEntry{
|
if err := tx.Create(&model.WalletLedgerEntry{
|
||||||
MerchantID: merchantID,
|
MerchantID: in.MerchantID,
|
||||||
WalletAccountID: wallet.ID,
|
WalletAccountID: wallet.ID,
|
||||||
EntryNo: "WL" + uuid.NewString(),
|
EntryNo: "WL" + uuid.NewString(),
|
||||||
Type: model.WalletLedgerRefund,
|
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 {
|
if err := tx.First(&out, order.ID).Error; err != nil {
|
||||||
return err
|
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
|
return err
|
||||||
}
|
}
|
||||||
if s.callbacks != nil {
|
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
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -404,6 +404,103 @@ func TestFulfillmentCancelRefundsOnceAndRestoresStock(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFulfillmentCancelByUserRefundsAndAuditsActor(t *testing.T) {
|
||||||
|
db := newServiceTestDB(t)
|
||||||
|
merchantID, product := seedFulfillmentMerchant(t, db, "merchant-bu", 1000, 2, 300)
|
||||||
|
svc := NewFulfillmentService(db, nil)
|
||||||
|
created, err := svc.CreateOrder(CreateFulfillmentOrderInput{
|
||||||
|
MerchantID: merchantID,
|
||||||
|
APIClientID: 12,
|
||||||
|
ClientOrderNo: "client-cancel-byuser",
|
||||||
|
SKU: product.SKU,
|
||||||
|
Quantity: 1,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create order: %v", err)
|
||||||
|
}
|
||||||
|
cancelled, err := svc.CancelOrderByUser(merchantID, 99, created.Order.OrderNo, "商户后台取消")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("cancel by user: %v", err)
|
||||||
|
}
|
||||||
|
if cancelled.OrderStatus != model.OrderStatusCancelled {
|
||||||
|
t.Fatalf("unexpected cancelled order: %+v", cancelled)
|
||||||
|
}
|
||||||
|
var wallet model.WalletAccount
|
||||||
|
_ = db.Where("merchant_id = ?", merchantID).First(&wallet).Error
|
||||||
|
if wallet.AvailableBalance != 1000 {
|
||||||
|
t.Fatalf("wallet should refund full amount, got %d", wallet.AvailableBalance)
|
||||||
|
}
|
||||||
|
var audit model.AuditLog
|
||||||
|
if err := db.Where("action = ? AND entity_id = ?", "merchant_order.cancel", created.Order.OrderNo).First(&audit).Error; err != nil {
|
||||||
|
t.Fatalf("cancel audit missing: %v", err)
|
||||||
|
}
|
||||||
|
if audit.ActorUserID == nil || *audit.ActorUserID != 99 {
|
||||||
|
t.Fatalf("cancel audit should record actor user 99, got %v", audit.ActorUserID)
|
||||||
|
}
|
||||||
|
if audit.APIClientID != nil {
|
||||||
|
t.Fatalf("cancel audit should not record api client for backend path, got %v", *audit.APIClientID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFulfillmentCancelRejectedWhenSubmittedUpstream(t *testing.T) {
|
||||||
|
db := newServiceTestDB(t)
|
||||||
|
merchantID, product := seedFulfillmentMerchant(t, db, "merchant-bd", 1000, 2, 300)
|
||||||
|
svc := NewFulfillmentService(db, nil)
|
||||||
|
create := func(clientOrderNo string) *model.FulfillmentOrder {
|
||||||
|
t.Helper()
|
||||||
|
created, err := svc.CreateOrder(CreateFulfillmentOrderInput{
|
||||||
|
MerchantID: merchantID,
|
||||||
|
APIClientID: 12,
|
||||||
|
ClientOrderNo: clientOrderNo,
|
||||||
|
SKU: product.SKU,
|
||||||
|
Quantity: 1,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create order: %v", err)
|
||||||
|
}
|
||||||
|
return created.Order
|
||||||
|
}
|
||||||
|
|
||||||
|
// 已提交上游(provider_order_no 非空)的失败订单禁止取消,避免退款后上游仍发货。
|
||||||
|
submitted := create("client-cancel-submitted")
|
||||||
|
if err := db.Model(&model.FulfillmentOrder{}).Where("id = ?", submitted.ID).Updates(map[string]interface{}{
|
||||||
|
"order_status": model.OrderStatusShipFailed,
|
||||||
|
"provider_order_no": "PROV-UPSTREAM-001",
|
||||||
|
"failure_reason": "上游角色校验失败",
|
||||||
|
}).Error; err != nil {
|
||||||
|
t.Fatalf("seed submitted failed order: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := svc.CancelOrder(merchantID, 12, submitted.OrderNo, "想取消"); err == nil {
|
||||||
|
t.Fatalf("cancel must be rejected when order already submitted upstream")
|
||||||
|
}
|
||||||
|
var walletAfterReject model.WalletAccount
|
||||||
|
_ = db.Where("merchant_id = ?", merchantID).First(&walletAfterReject).Error
|
||||||
|
if walletAfterReject.AvailableBalance != 700 {
|
||||||
|
t.Fatalf("wallet must not be refunded after rejected cancel, got %d", walletAfterReject.AvailableBalance)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 未提交上游的失败订单(提交中断)可以正常取消退款。
|
||||||
|
interrupted := create("client-cancel-interrupted")
|
||||||
|
if err := db.Model(&model.FulfillmentOrder{}).Where("id = ?", interrupted.ID).Updates(map[string]interface{}{
|
||||||
|
"order_status": model.OrderStatusShipFailed,
|
||||||
|
"failure_reason": "发货提交中断",
|
||||||
|
}).Error; err != nil {
|
||||||
|
t.Fatalf("seed interrupted failed order: %v", err)
|
||||||
|
}
|
||||||
|
cancelled, err := svc.CancelOrder(merchantID, 12, interrupted.OrderNo, "确认中断,取消退款")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("cancel interrupted failed order: %v", err)
|
||||||
|
}
|
||||||
|
if cancelled.OrderStatus != model.OrderStatusCancelled {
|
||||||
|
t.Fatalf("expected cancelled, got %s", cancelled.OrderStatus)
|
||||||
|
}
|
||||||
|
var walletFinal model.WalletAccount
|
||||||
|
_ = db.Where("merchant_id = ?", merchantID).First(&walletFinal).Error
|
||||||
|
if walletFinal.AvailableBalance != 700 {
|
||||||
|
t.Fatalf("wallet should refund the interrupted order (1000-300-300+300), got %d", walletFinal.AvailableBalance)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestOrderStatusTransitions(t *testing.T) {
|
func TestOrderStatusTransitions(t *testing.T) {
|
||||||
db := newServiceTestDB(t)
|
db := newServiceTestDB(t)
|
||||||
merchantID, product := seedFulfillmentMerchant(t, db, "merchant-c", 1000, -1, 100)
|
merchantID, product := seedFulfillmentMerchant(t, db, "merchant-c", 1000, -1, 100)
|
||||||
|
|||||||
@@ -85,6 +85,8 @@ export const merchantApi = {
|
|||||||
game_account?: string
|
game_account?: string
|
||||||
note?: string
|
note?: string
|
||||||
}) => request.post('/merchant/orders/manual', data).then((r) => r.data.data as CreateManualOrderResult),
|
}) => request.post('/merchant/orders/manual', data).then((r) => r.data.data as CreateManualOrderResult),
|
||||||
|
cancelOrder: (orderNo: string, reason: string) =>
|
||||||
|
request.post(`/merchant/orders/${encodeURIComponent(orderNo)}/cancel`, { reason }).then((r) => r.data.data as { order: FulfillmentOrder; refund_amount: number }),
|
||||||
getDeliveryLink: (orderNo: string) =>
|
getDeliveryLink: (orderNo: string) =>
|
||||||
request.get(`/merchant/orders/${encodeURIComponent(orderNo)}/delivery-link`).then((r) => r.data.data as DeliveryLinkResult),
|
request.get(`/merchant/orders/${encodeURIComponent(orderNo)}/delivery-link`).then((r) => r.data.data as DeliveryLinkResult),
|
||||||
revokeDeliveryLink: (orderNo: string) =>
|
revokeDeliveryLink: (orderNo: string) =>
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer
|
|||||||
message.success('发货链接已作废')
|
message.success('发货链接已作废')
|
||||||
patchDeliveryLinkState(orderNo, { delivery_link_revoked_at: revokedAt })
|
patchDeliveryLinkState(orderNo, { delivery_link_revoked_at: revokedAt })
|
||||||
})
|
})
|
||||||
.catch((e) => message.error(e instanceof Error ? e.message : '作废失败'))
|
.catch((e) => message.error(e instanceof Error ? e.message : '链接作废失败'))
|
||||||
}, [patchDeliveryLinkState])
|
}, [patchDeliveryLinkState])
|
||||||
|
|
||||||
const restoreDeliveryLink = useCallback((orderNo: string) => {
|
const restoreDeliveryLink = useCallback((orderNo: string) => {
|
||||||
@@ -355,6 +355,12 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer
|
|||||||
loadOrders(1, orders.size, orderSource, filters).catch((e) => message.error(e instanceof Error ? e.message : '订单加载失败'))
|
loadOrders(1, orders.size, orderSource, filters).catch((e) => message.error(e instanceof Error ? e.message : '订单加载失败'))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cancelOrder = useCallback(async (orderNo: string, reason: string) => {
|
||||||
|
const result = await merchantApi.cancelOrder(orderNo, reason)
|
||||||
|
message.success(`订单已取消,已退还 ${Number(result.refund_amount || 0)} 积分`)
|
||||||
|
await loadOrders()
|
||||||
|
}, [loadOrders])
|
||||||
|
|
||||||
const submitAPIClient = async () => {
|
const submitAPIClient = async () => {
|
||||||
const values = await apiClientForm.validateFields()
|
const values = await apiClientForm.validateFields()
|
||||||
try {
|
try {
|
||||||
@@ -510,7 +516,7 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer
|
|||||||
<ProductsTab loading={loading} products={products} canManage={canManage} onToggleProduct={openProductToggle} onLoadProducts={loadProducts} />
|
<ProductsTab loading={loading} products={products} canManage={canManage} onToggleProduct={openProductToggle} onLoadProducts={loadProducts} />
|
||||||
) },
|
) },
|
||||||
{ key: 'orders', label: '发货订单', disabled: !hasFeature('orders'), children: (
|
{ key: 'orders', label: '发货订单', disabled: !hasFeature('orders'), children: (
|
||||||
<OrdersTab loading={loading} orders={orders} orderSource={orderSource} filters={orderFilters} canManage={canOrderManage} onCreateManualOrder={openManualOrderCreate} onOrderSourceChange={changeOrderSource} onFilterChange={changeOrderFilters} onLoadOrders={loadOrders} onCopyLink={copyDeliveryLink} onOpenLink={openDeliveryLink} onRevokeLink={revokeDeliveryLink} onRestoreLink={restoreDeliveryLink} />
|
<OrdersTab loading={loading} orders={orders} orderSource={orderSource} filters={orderFilters} canManage={canOrderManage} onCreateManualOrder={openManualOrderCreate} onOrderSourceChange={changeOrderSource} onFilterChange={changeOrderFilters} onLoadOrders={loadOrders} onCopyLink={copyDeliveryLink} onOpenLink={openDeliveryLink} onRevokeLink={revokeDeliveryLink} onRestoreLink={restoreDeliveryLink} onCancelOrder={cancelOrder} />
|
||||||
) },
|
) },
|
||||||
{ key: 'wallet', label: '钱包', disabled: !hasFeature('wallet'), children: (
|
{ key: 'wallet', label: '钱包', disabled: !hasFeature('wallet'), children: (
|
||||||
<WalletTab loading={loading} wallet={wallet} canFinance={canFinance} ledger={ledger} form={walletFilterForm} onFilter={handleWalletFilter} onResetFilter={handleWalletFilterReset} onLoadLedger={handleLoadLedger} />
|
<WalletTab loading={loading} wallet={wallet} canFinance={canFinance} ledger={ledger} form={walletFilterForm} onFilter={handleWalletFilter} onResetFilter={handleWalletFilterReset} onLoadLedger={handleLoadLedger} />
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { Button, Card, Col, Form, Input, Popconfirm, Row, Select, Space, Table, Tabs, Tag, Typography, message } from 'antd'
|
import { Alert, Button, Card, Col, Form, Input, Modal, Popconfirm, Row, Select, Space, Table, Tabs, Tag, Typography, message } from 'antd'
|
||||||
import type { FormInstance } from 'antd'
|
import type { FormInstance } from 'antd'
|
||||||
import {
|
import {
|
||||||
ClockCircleOutlined,
|
ClockCircleOutlined,
|
||||||
@@ -183,6 +183,7 @@ export function OrdersTab({
|
|||||||
onOpenLink,
|
onOpenLink,
|
||||||
onRevokeLink,
|
onRevokeLink,
|
||||||
onRestoreLink,
|
onRestoreLink,
|
||||||
|
onCancelOrder,
|
||||||
}: {
|
}: {
|
||||||
loading: boolean
|
loading: boolean
|
||||||
orders: PageResult<FulfillmentOrder>
|
orders: PageResult<FulfillmentOrder>
|
||||||
@@ -197,8 +198,12 @@ export function OrdersTab({
|
|||||||
onOpenLink: (orderNo: string) => void
|
onOpenLink: (orderNo: string) => void
|
||||||
onRevokeLink: (orderNo: string) => void
|
onRevokeLink: (orderNo: string) => void
|
||||||
onRestoreLink: (orderNo: string) => void
|
onRestoreLink: (orderNo: string) => void
|
||||||
|
onCancelOrder: (orderNo: string, reason: string) => Promise<void> | void
|
||||||
}) {
|
}) {
|
||||||
const [keyword, setKeyword] = useState(filters.keyword)
|
const [keyword, setKeyword] = useState(filters.keyword)
|
||||||
|
const [cancelling, setCancelling] = useState<FulfillmentOrder | null>(null)
|
||||||
|
const [cancelReason, setCancelReason] = useState('')
|
||||||
|
const [cancelSubmitting, setCancelSubmitting] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setKeyword(filters.keyword)
|
setKeyword(filters.keyword)
|
||||||
@@ -206,6 +211,22 @@ export function OrdersTab({
|
|||||||
|
|
||||||
const submitKeyword = () => onFilterChange({ ...filters, keyword: keyword.trim() })
|
const submitKeyword = () => onFilterChange({ ...filters, keyword: keyword.trim() })
|
||||||
|
|
||||||
|
const submitCancel = async () => {
|
||||||
|
if (!cancelling) return
|
||||||
|
if (!cancelReason.trim()) {
|
||||||
|
message.warning('请填写取消原因')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setCancelSubmitting(true)
|
||||||
|
try {
|
||||||
|
await onCancelOrder(cancelling.order_no, cancelReason.trim())
|
||||||
|
setCancelling(null)
|
||||||
|
setCancelReason('')
|
||||||
|
} finally {
|
||||||
|
setCancelSubmitting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const orderColumns: ColumnsType<FulfillmentOrder> = [
|
const orderColumns: ColumnsType<FulfillmentOrder> = [
|
||||||
{
|
{
|
||||||
title: '平台订单号',
|
title: '平台订单号',
|
||||||
@@ -237,16 +258,55 @@ export function OrdersTab({
|
|||||||
{
|
{
|
||||||
title: '发货链接',
|
title: '发货链接',
|
||||||
key: 'delivery_link',
|
key: 'delivery_link',
|
||||||
width: 150,
|
width: 160,
|
||||||
render: (_, record) => record.delivery_link_revoked_at ? (
|
render: (_, record) => {
|
||||||
<Button type="link" size="small" onClick={() => onRestoreLink(record.order_no)}>取消作废</Button>
|
if (record.order_status === 'cancelled') {
|
||||||
) : (
|
return <Typography.Text type="secondary">订单已取消</Typography.Text>
|
||||||
<Space size={4} wrap>
|
}
|
||||||
<Button type="link" size="small" onClick={() => onCopyLink(record.order_no)}>复制</Button>
|
if (record.delivery_link_revoked_at) {
|
||||||
<Button type="link" size="small" onClick={() => onOpenLink(record.order_no)}>打开</Button>
|
return <Button type="link" size="small" onClick={() => onRestoreLink(record.order_no)}>恢复链接</Button>
|
||||||
<Button type="link" danger size="small" onClick={() => onRevokeLink(record.order_no)}>作废</Button>
|
}
|
||||||
</Space>
|
return (
|
||||||
),
|
<Space size={4} wrap>
|
||||||
|
<Button type="link" size="small" onClick={() => onCopyLink(record.order_no)}>复制</Button>
|
||||||
|
<Button type="link" size="small" onClick={() => onOpenLink(record.order_no)}>打开</Button>
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
danger
|
||||||
|
size="small"
|
||||||
|
title="仅作废发货链接,不影响订单状态与积分"
|
||||||
|
onClick={() => onRevokeLink(record.order_no)}
|
||||||
|
>
|
||||||
|
链接作废
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
width: 110,
|
||||||
|
fixed: 'right',
|
||||||
|
render: (_, record) => {
|
||||||
|
if (!canManage) return '-'
|
||||||
|
if (record.order_status === 'paid' || record.order_status === 'ship_failed') {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
danger
|
||||||
|
size="small"
|
||||||
|
onClick={() => {
|
||||||
|
setCancelling(record)
|
||||||
|
setCancelReason('')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
取消订单
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return '-'
|
||||||
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -300,9 +360,38 @@ export function OrdersTab({
|
|||||||
columns={orderColumns}
|
columns={orderColumns}
|
||||||
dataSource={orders.list}
|
dataSource={orders.list}
|
||||||
tableLayout="fixed"
|
tableLayout="fixed"
|
||||||
scroll={{ x: 1660 }}
|
scroll={{ x: 1780 }}
|
||||||
pagination={pageConfig(orders, onLoadOrders)}
|
pagination={pageConfig(orders, onLoadOrders)}
|
||||||
/>
|
/>
|
||||||
|
<Modal
|
||||||
|
title={`取消订单:${cancelling?.order_no || ''}`}
|
||||||
|
open={!!cancelling}
|
||||||
|
confirmLoading={cancelSubmitting}
|
||||||
|
onOk={submitCancel}
|
||||||
|
onCancel={() => {
|
||||||
|
setCancelling(null)
|
||||||
|
setCancelReason('')
|
||||||
|
}}
|
||||||
|
okText="确认取消并退款"
|
||||||
|
okButtonProps={{ danger: true }}
|
||||||
|
destroyOnClose
|
||||||
|
>
|
||||||
|
<Alert
|
||||||
|
type="warning"
|
||||||
|
showIcon
|
||||||
|
message="取消后订单将不可再发货"
|
||||||
|
description="已扣积分将全额退还商户钱包,商品库存自动回补;已提交上游发货的失败订单不可取消。"
|
||||||
|
style={{ marginBottom: 16 }}
|
||||||
|
/>
|
||||||
|
<Input.TextArea
|
||||||
|
rows={3}
|
||||||
|
maxLength={512}
|
||||||
|
showCount
|
||||||
|
value={cancelReason}
|
||||||
|
onChange={(e) => setCancelReason(e.target.value)}
|
||||||
|
placeholder="请填写取消原因(必填)"
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
</Space>
|
</Space>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user