diff --git a/README.md b/README.md index 032561e..de25b19 100644 --- a/README.md +++ b/README.md @@ -126,13 +126,14 @@ make docker-down # 停止并移除 ### 状态速查 -| 内部支付状态 | 内部履约状态 | 源头侧状态 | 含义 | -|--------------|--------------|------------|------| -| `paid` | `pending` | `paid` | 已付款,待发货 | -| `paid` | `processing` | `delivering` | 已提交履约,等待结果 | -| `paid` | `succeeded` | `delivered` | 履约成功 | -| `paid` | `failed` | `ship_failed` | 履约失败,可重试 | -| `refunded` | `cancelled` | `cancelled` | 已取消并退款 | +| 订单状态 | 含义 | 是否可发货 | +|----------|------|------------| +| `pending` | 待支付(当前无真实支付流程,仅预留) | 否 | +| `paid` | 已支付/已扣款,待发货 | 是 | +| `delivering` | 发货中 | 否 | +| `delivered` | 已交付 | 否 | +| `ship_failed` | 发货失败,可重试 | 是 | +| `cancelled` | 已取消并退款 | 否 | 上游皮肤源头系统调用源头侧接口时,需携带签名头 `X-Api-Key`、`X-Timestamp`、`X-Nonce`、`X-Sign`。 diff --git a/backend/internal/database/migrations/001_init.sql b/backend/internal/database/migrations/001_init.sql index ebb77c5..3f4816e 100644 --- a/backend/internal/database/migrations/001_init.sql +++ b/backend/internal/database/migrations/001_init.sql @@ -201,8 +201,7 @@ CREATE TABLE IF NOT EXISTS fulfillment_orders ( service_fee_amount BIGINT NOT NULL DEFAULT 0, amount BIGINT NOT NULL, currency VARCHAR(12) NOT NULL DEFAULT 'POINT', - payment_status VARCHAR(16) NOT NULL DEFAULT 'pending', - fulfillment_status VARCHAR(16) NOT NULL DEFAULT 'pending', + order_status VARCHAR(16) NOT NULL DEFAULT 'paid', buyer_reference VARCHAR(128), request_data TEXT, result_data TEXT, @@ -217,8 +216,7 @@ CREATE UNIQUE INDEX IF NOT EXISTS idx_order_client_no ON fulfillment_orders (mer CREATE INDEX IF NOT EXISTS idx_fulfillment_orders_deleted_at ON fulfillment_orders (deleted_at); CREATE INDEX IF NOT EXISTS idx_fulfillment_orders_merchant_id ON fulfillment_orders (merchant_id); CREATE INDEX IF NOT EXISTS idx_fulfillment_orders_merchant_product_id ON fulfillment_orders (merchant_product_id); -CREATE INDEX IF NOT EXISTS idx_fulfillment_orders_payment_status ON fulfillment_orders (payment_status); -CREATE INDEX IF NOT EXISTS idx_fulfillment_orders_fulfillment_status ON fulfillment_orders (fulfillment_status); +CREATE INDEX IF NOT EXISTS idx_fulfillment_orders_order_status ON fulfillment_orders (order_status); CREATE INDEX IF NOT EXISTS idx_fulfillment_orders_provider_order_no ON fulfillment_orders (provider_order_no); CREATE TABLE IF NOT EXISTS fulfillment_jobs ( diff --git a/backend/internal/handler/merchant.go b/backend/internal/handler/merchant.go index 286b1cb..0725b9a 100644 --- a/backend/internal/handler/merchant.go +++ b/backend/internal/handler/merchant.go @@ -126,7 +126,7 @@ func (h *MerchantHandler) UpdateProduct(c *gin.Context) { func (h *MerchantHandler) ListOrders(c *gin.Context) { page, size := pageParams(c) - list, total, err := h.fulfillmentSvc.ListOrders(middleware.GetMerchantID(c), page, size, c.Query("fulfillment_status")) + list, total, err := h.fulfillmentSvc.ListOrders(middleware.GetMerchantID(c), page, size, c.Query("order_status")) if err != nil { response.ServerError(c, err.Error()) return @@ -135,10 +135,10 @@ func (h *MerchantHandler) ListOrders(c *gin.Context) { } type merchantTestOrderReq struct { - SKU string `json:"sku" binding:"required"` - BuyerReference string `json:"buyer_reference"` - Note string `json:"note"` - FulfillmentStatus string `json:"fulfillment_status"` + SKU string `json:"sku" binding:"required"` + BuyerReference string `json:"buyer_reference"` + Note string `json:"note"` + OrderStatus string `json:"order_status"` } func (h *MerchantHandler) CreateTestOrder(c *gin.Context) { @@ -148,12 +148,12 @@ func (h *MerchantHandler) CreateTestOrder(c *gin.Context) { return } order, err := h.fulfillmentSvc.CreateTestOrder(service.CreateTestOrderInput{ - MerchantID: middleware.GetMerchantID(c), - ActorUserID: middleware.GetUserID(c), - SKU: req.SKU, - BuyerReference: req.BuyerReference, - Note: req.Note, - FulfillmentStatus: req.FulfillmentStatus, + MerchantID: middleware.GetMerchantID(c), + ActorUserID: middleware.GetUserID(c), + SKU: req.SKU, + BuyerReference: req.BuyerReference, + Note: req.Note, + OrderStatus: req.OrderStatus, }) if err != nil { response.BadRequest(c, err.Error()) diff --git a/backend/internal/handler/open_v1.go b/backend/internal/handler/open_v1.go index 2c53091..bd61aea 100644 --- a/backend/internal/handler/open_v1.go +++ b/backend/internal/handler/open_v1.go @@ -110,8 +110,8 @@ func (h *OpenV1Handler) QueryOrder(c *gin.Context) { response.ServerError(c, err.Error()) return } - openlog.Info(c, "query_order ok order_no=%s status=%s payment=%s", - order.OrderNo, order.FulfillmentStatus, order.PaymentStatus) + openlog.Info(c, "query_order ok order_no=%s status=%s", + order.OrderNo, order.OrderStatus) response.OK(c, buildOpenOrderResponse(order)) } @@ -259,8 +259,7 @@ func buildOpenOrderResponse(order *model.FulfillmentOrder) gin.H { data := gin.H{ "order_no": order.OrderNo, "client_order_no": order.ClientOrderNo, - "payment_status": order.PaymentStatus, - "fulfillment_status": order.FulfillmentStatus, + "order_status": order.OrderStatus, "can_fulfill": canFulfill, "cannot_fulfill_reason": reason, "product": gin.H{ diff --git a/backend/internal/model/merchant.go b/backend/internal/model/merchant.go index 8b2ac19..b53ba46 100644 --- a/backend/internal/model/merchant.go +++ b/backend/internal/model/merchant.go @@ -34,16 +34,12 @@ const ( ProductStatusActive = "active" ProductStatusInactive = "inactive" - PaymentStatusPending = "pending" - PaymentStatusPaid = "paid" - PaymentStatusRefunded = "refunded" - PaymentStatusCancelled = "cancelled" - - FulfillmentStatusPending = "pending" - FulfillmentStatusProcessing = "processing" - FulfillmentStatusSucceeded = "succeeded" - FulfillmentStatusFailed = "failed" - FulfillmentStatusCancelled = "cancelled" + OrderStatusPending = "pending" // 待支付(当前无真实支付流程,仅预留) + OrderStatusPaid = "paid" // 已扣款,待发货 + OrderStatusDelivering = "delivering" // 发货中 + OrderStatusDelivered = "delivered" // 已交付 + OrderStatusShipFailed = "ship_failed" // 发货失败(可重试) + OrderStatusCancelled = "cancelled" // 已取消并退款 WalletLedgerCredit = "credit" WalletLedgerDebit = "debit" @@ -189,7 +185,7 @@ type WalletLedgerEntry struct { Note string `gorm:"size:255" json:"note"` } -// FulfillmentOrder 将支付和履约状态拆分,并保留商品及请求快照。 +// FulfillmentOrder 保留订单状态、商品及请求快照。 type FulfillmentOrder struct { ID uint `gorm:"primarykey" json:"id"` CreatedAt time.Time `json:"created_at"` @@ -210,8 +206,7 @@ type FulfillmentOrder struct { ServiceFeeAmount int64 `gorm:"not null;default:0" json:"service_fee_amount"` Amount int64 `gorm:"not null" json:"amount"` Currency string `gorm:"size:12;not null;default:POINT" json:"currency"` - PaymentStatus string `gorm:"size:16;not null;default:pending;index" json:"payment_status"` - FulfillmentStatus string `gorm:"size:16;not null;default:pending;index" json:"fulfillment_status"` + OrderStatus string `gorm:"size:16;not null;default:paid;index" json:"order_status"` BuyerReference string `gorm:"size:128" json:"buyer_reference"` RequestFingerprint string `gorm:"size:64;not null;default:'';index" json:"-"` RequestData string `gorm:"type:text" json:"request_data"` diff --git a/backend/internal/service/delivery.go b/backend/internal/service/delivery.go index 133be09..e885f8f 100644 --- a/backend/internal/service/delivery.go +++ b/backend/internal/service/delivery.go @@ -242,8 +242,9 @@ func (s *DeliveryService) submit(orderNo, gameAccount, bindUUID string, apiClien if err != nil { return nil, err } - if order.FulfillmentStatus == model.FulfillmentStatusProcessing || - order.FulfillmentStatus == model.FulfillmentStatusSucceeded || + orderStatus := normalizeOrderStatus(order) + if orderStatus == model.OrderStatusDelivering || + orderStatus == model.OrderStatusDelivered || strings.TrimSpace(order.ProviderOrderNo) != "" || strings.Contains(order.ResultData, "\"queue_order_id\"") { if existing := buildExistingDeliverySubmitResult(order); existing != nil { @@ -311,7 +312,7 @@ func (s *DeliveryService) submit(orderNo, gameAccount, bindUUID string, apiClien MerchantID: order.MerchantID, APIClientID: apiClientID, OrderNo: order.OrderNo, - Status: "processing", + Status: model.OrderStatusDelivering, ProviderOrderNo: providerOrderNo, ResultData: resultData, }) @@ -320,7 +321,7 @@ func (s *DeliveryService) submit(orderNo, gameAccount, bindUUID string, apiClien } return &DeliverySubmitResult{ OrderNo: updated.OrderNo, - Status: updated.FulfillmentStatus, + Status: normalizeOrderStatus(updated), Message: "已提交上游发货,等待发货结果回传", ProviderOrderNo: providerOrderNo, GameAccount: boundAccount, @@ -341,11 +342,12 @@ func (s *DeliveryService) claimDeliverySubmission(merchantID, apiClientID uint, } return err } - if order.FulfillmentStatus == model.FulfillmentStatusProcessing || order.FulfillmentStatus == model.FulfillmentStatusSucceeded { + orderStatus := normalizeOrderStatus(&order) + if orderStatus == model.OrderStatusDelivering || orderStatus == model.OrderStatusDelivered { updated = order return nil } - if err := validateFulfillmentTransition(&order, model.FulfillmentStatusProcessing, fulfillmentTransitionUpdate); err != nil { + if err := validateOrderStatusTransition(&order, model.OrderStatusDelivering, fulfillmentTransitionUpdate); err != nil { return newDeliveryHTTPError(http.StatusConflict, "订单暂不可发货") } raw, _ := json.Marshal(map[string]interface{}{ @@ -354,9 +356,9 @@ func (s *DeliveryService) claimDeliverySubmission(merchantID, apiClientID uint, "provider_order_stage": "claimed", }) if err := tx.Model(&order).Updates(map[string]interface{}{ - "fulfillment_status": model.FulfillmentStatusProcessing, - "result_data": string(raw), - "failure_reason": "", + "order_status": model.OrderStatusDelivering, + "result_data": string(raw), + "failure_reason": "", }).Error; err != nil { return err } @@ -389,7 +391,7 @@ func (s *DeliveryService) markDeliverySubmissionFailed(order *model.FulfillmentO MerchantID: order.MerchantID, APIClientID: apiClientID, OrderNo: order.OrderNo, - Status: model.FulfillmentStatusFailed, + Status: model.OrderStatusShipFailed, FailureReason: reason, ResultData: map[string]interface{}{ "source": "delivery_proxy", @@ -492,8 +494,8 @@ func buildExistingDeliverySubmitResult(order *model.FulfillmentOrder) *DeliveryS if order == nil { return nil } - switch order.FulfillmentStatus { - case model.FulfillmentStatusProcessing, model.FulfillmentStatusSucceeded: + switch normalizeOrderStatus(order) { + case model.OrderStatusDelivering, model.OrderStatusDelivered: default: return nil } @@ -506,12 +508,13 @@ func buildExistingDeliverySubmitResult(order *model.FulfillmentOrder) *DeliveryS stringFromMap(resultData, "provider_order_no"), ) message := "订单暂时正在履约中,请稍后查询" - if order.FulfillmentStatus == model.FulfillmentStatusSucceeded { + status := normalizeOrderStatus(order) + if status == model.OrderStatusDelivered { message = "订单已履约成功" } return &DeliverySubmitResult{ OrderNo: order.OrderNo, - Status: order.FulfillmentStatus, + Status: status, Message: message, ProviderOrderNo: providerOrderNo, UpstreamOrder: resultData, diff --git a/backend/internal/service/delivery_test.go b/backend/internal/service/delivery_test.go index a71f8c2..cd222d7 100644 --- a/backend/internal/service/delivery_test.go +++ b/backend/internal/service/delivery_test.go @@ -216,15 +216,15 @@ func TestDeliveryMerchantApiBindAndSubmit(t *testing.T) { if err != nil { t.Fatalf("submit for merchant: %v", err) } - if submitted.Status != model.FulfillmentStatusProcessing || submitted.ProviderOrderNo != "provider-1" { + if submitted.Status != model.OrderStatusDelivering || submitted.ProviderOrderNo != "provider-1" { t.Fatalf("unexpected submit result: %+v", submitted) } var afterSubmit model.FulfillmentOrder if err := db.Where("order_no = ?", created.Order.OrderNo).First(&afterSubmit).Error; err != nil { t.Fatalf("reload after submit: %v", err) } - if afterSubmit.FulfillmentStatus != model.FulfillmentStatusProcessing || afterSubmit.ProviderOrderNo != "provider-1" { - t.Fatalf("submit should persist processing state: %+v", afterSubmit) + if afterSubmit.OrderStatus != model.OrderStatusDelivering || afterSubmit.ProviderOrderNo != "provider-1" { + t.Fatalf("submit should persist delivering state: %+v", afterSubmit) } if atomic.LoadInt32(&queueCreateCalls) != 1 || atomic.LoadInt32(&upstreamCalls) != 1 { t.Fatalf("first submit should only create one queue, queue=%d upstream=%d requests=%v", queueCreateCalls, upstreamCalls, requestLog) @@ -233,7 +233,7 @@ func TestDeliveryMerchantApiBindAndSubmit(t *testing.T) { if err != nil { t.Fatalf("repeat submit for merchant: %v", err) } - if second.ProviderOrderNo != "provider-1" || second.Status != model.FulfillmentStatusProcessing { + if second.ProviderOrderNo != "provider-1" || second.Status != model.OrderStatusDelivering { t.Fatalf("repeat submit should reuse existing result: %+v", second) } if atomic.LoadInt32(&queueCreateCalls) != 1 || atomic.LoadInt32(&upstreamCalls) != 1 { diff --git a/backend/internal/service/fulfillment.go b/backend/internal/service/fulfillment.go index bca0cef..caa4c0f 100644 --- a/backend/internal/service/fulfillment.go +++ b/backend/internal/service/fulfillment.go @@ -44,12 +44,12 @@ type CreateFulfillmentOrderResult struct { } type CreateTestOrderInput struct { - MerchantID uint - ActorUserID uint - SKU string - BuyerReference string - Note string - FulfillmentStatus string + MerchantID uint + ActorUserID uint + SKU string + BuyerReference string + Note string + OrderStatus string } func (s *FulfillmentService) CreateOrder(in CreateFulfillmentOrderInput) (*CreateFulfillmentOrderResult, error) { @@ -159,8 +159,7 @@ func (s *FulfillmentService) CreateOrder(in CreateFulfillmentOrderInput) (*Creat ServiceFeeAmount: serviceFee, Amount: totalAmount, Currency: product.Currency, - PaymentStatus: model.PaymentStatusPaid, - FulfillmentStatus: model.FulfillmentStatusPending, + OrderStatus: model.OrderStatusPaid, BuyerReference: in.BuyerReference, RequestFingerprint: fingerprint, RequestData: requestData, @@ -236,13 +235,13 @@ func (s *FulfillmentService) CreateTestOrder(in CreateTestOrderInput) (*model.Fu if len(in.Note) > 512 { return nil, errors.New("备注最长 512 位") } - if in.FulfillmentStatus == "" { - in.FulfillmentStatus = model.FulfillmentStatusPending + if in.OrderStatus == "" { + in.OrderStatus = model.OrderStatusPaid } - switch in.FulfillmentStatus { - case model.FulfillmentStatusPending, model.FulfillmentStatusFailed: + switch in.OrderStatus { + case model.OrderStatusPaid, model.OrderStatusShipFailed: default: - return nil, errors.New("测试订单仅支持待履约或发货失败状态") + return nil, errors.New("测试订单仅支持已支付待发货或发货失败状态") } requestData := map[string]interface{}{ @@ -255,7 +254,6 @@ func (s *FulfillmentService) CreateTestOrder(in CreateTestOrderInput) (*model.Fu if err != nil { return nil, errors.New("订单请求数据无法序列化") } - var out model.FulfillmentOrder err = s.db.Transaction(func(tx *gorm.DB) error { var merchant model.Merchant @@ -295,21 +293,20 @@ func (s *FulfillmentService) CreateTestOrder(in CreateTestOrderInput) (*model.Fu ServiceFeeAmount: 0, Amount: 0, Currency: product.Currency, - PaymentStatus: model.PaymentStatusPaid, - FulfillmentStatus: in.FulfillmentStatus, + OrderStatus: in.OrderStatus, BuyerReference: in.BuyerReference, RequestData: string(rawRequestData), } - if in.FulfillmentStatus == model.FulfillmentStatusFailed { + if in.OrderStatus == model.OrderStatusShipFailed { order.FailureReason = fallbackName(in.Note, "联调测试订单初始化为发货失败,可重新发货") } if err := tx.Create(order).Error; err != nil { return err } if err := writeAudit(tx, &in.MerchantID, &in.ActorUserID, nil, "merchant_test_order.create", "fulfillment_order", order.OrderNo, map[string]interface{}{ - "sku": in.SKU, - "fulfillment_status": in.FulfillmentStatus, - "can_ship": true, + "sku": in.SKU, + "order_status": in.OrderStatus, + "can_ship": true, }); err != nil { return err } @@ -337,11 +334,11 @@ func (s *FulfillmentService) GetOrder(merchantID uint, orderNo string) (*model.F return &order, nil } -func (s *FulfillmentService) ListOrders(merchantID uint, page, size int, fulfillmentStatus string) ([]model.FulfillmentOrder, int64, error) { +func (s *FulfillmentService) ListOrders(merchantID uint, page, size int, orderStatus string) ([]model.FulfillmentOrder, int64, error) { page, size = normalizePage(page, size) tx := s.db.Model(&model.FulfillmentOrder{}).Where("merchant_id = ?", merchantID) - if fulfillmentStatus != "" { - tx = tx.Where("fulfillment_status = ?", fulfillmentStatus) + if orderStatus != "" { + tx = tx.Where("order_status = ?", orderStatus) } var total int64 if err := tx.Count(&total).Error; err != nil { @@ -364,10 +361,11 @@ type FulfillmentUpdateInput struct { func (s *FulfillmentService) UpdateFulfillment(in FulfillmentUpdateInput) (*model.FulfillmentOrder, error) { switch in.Status { - case model.FulfillmentStatusProcessing, model.FulfillmentStatusSucceeded, model.FulfillmentStatusFailed: + case model.OrderStatusDelivering, model.OrderStatusDelivered, model.OrderStatusShipFailed: default: - return nil, errors.New("无效的履约状态") + return nil, errors.New("无效的订单状态") } + nextOrderStatus := in.Status resultData := "" if in.ResultData != nil { raw, err := json.Marshal(in.ResultData) @@ -387,26 +385,24 @@ func (s *FulfillmentService) UpdateFulfillment(in FulfillmentUpdateInput) (*mode } return err } - if order.FulfillmentStatus == model.FulfillmentStatusSucceeded && in.Status == model.FulfillmentStatusSucceeded { + if normalizeOrderStatus(&order) == model.OrderStatusDelivered && nextOrderStatus == model.OrderStatusDelivered { out = order return nil } - if err := validateFulfillmentTransition(&order, in.Status, fulfillmentTransitionUpdate); err != nil { + if err := validateOrderStatusTransition(&order, nextOrderStatus, fulfillmentTransitionUpdate); err != nil { return err } now := time.Now() - updates := map[string]interface{}{ - "fulfillment_status": in.Status, - "result_data": resultData, - } + updates := map[string]interface{}{"order_status": nextOrderStatus} + updates["result_data"] = resultData if in.ProviderOrderNo != "" { updates["provider_order_no"] = in.ProviderOrderNo } - switch in.Status { - case model.FulfillmentStatusSucceeded: + switch nextOrderStatus { + case model.OrderStatusDelivered: updates["delivered_at"] = now updates["failure_reason"] = "" - case model.FulfillmentStatusFailed: + case model.OrderStatusShipFailed: updates["failure_reason"] = in.FailureReason } if err := tx.Model(&order).Updates(updates).Error; err != nil { @@ -415,7 +411,7 @@ func (s *FulfillmentService) UpdateFulfillment(in FulfillmentUpdateInput) (*mode if err := tx.First(&out, order.ID).Error; err != nil { return err } - if err := writeAudit(tx, &in.MerchantID, nil, &in.APIClientID, "fulfillment.update", "fulfillment_order", order.OrderNo, map[string]string{"status": in.Status}); err != nil { + if err := writeAudit(tx, &in.MerchantID, nil, &in.APIClientID, "fulfillment.update", "fulfillment_order", order.OrderNo, map[string]string{"status": nextOrderStatus}); err != nil { return err } if s.callbacks != nil { @@ -442,7 +438,7 @@ func (s *FulfillmentService) MarkProcessingTimeouts(timeout time.Duration, limit cutoff := now.Add(-timeout) var ids []uint if err := s.db.Model(&model.FulfillmentOrder{}). - Where("payment_status = ? AND fulfillment_status = ? AND updated_at < ?", model.PaymentStatusPaid, model.FulfillmentStatusProcessing, cutoff). + Where("order_status = ? AND updated_at < ?", model.OrderStatusDelivering, cutoff). Order("updated_at ASC, id ASC"). Limit(limit). Pluck("id", &ids).Error; err != nil { @@ -474,15 +470,13 @@ func (s *FulfillmentService) markProcessingTimeout(id uint, timeout time.Duratio if !processingTimedOut(&order, timeout, now) { return nil } - if err := validateFulfillmentTransition(&order, model.FulfillmentStatusFailed, fulfillmentTransitionTimeout); err != nil { + if err := validateOrderStatusTransition(&order, model.OrderStatusShipFailed, fulfillmentTransitionTimeout); err != nil { return nil } - reason := fmt.Sprintf("履约超时:订单已处于 processing 超过 %d 分钟", int(timeout.Minutes())) - updates := map[string]interface{}{ - "fulfillment_status": model.FulfillmentStatusFailed, - "failure_reason": reason, - "result_data": buildProcessingTimeoutResultData(order.ResultData, timeout, now), - } + reason := fmt.Sprintf("履约超时:订单已处于 delivering 超过 %d 分钟", int(timeout.Minutes())) + updates := map[string]interface{}{"order_status": model.OrderStatusShipFailed} + updates["failure_reason"] = reason + updates["result_data"] = buildProcessingTimeoutResultData(order.ResultData, timeout, now) if err := tx.Model(&order).Updates(updates).Error; err != nil { return err } @@ -491,8 +485,8 @@ func (s *FulfillmentService) markProcessingTimeout(id uint, timeout time.Duratio return err } if err := writeAudit(tx, &order.MerchantID, nil, nil, "fulfillment.timeout", "fulfillment_order", order.OrderNo, map[string]interface{}{ - "from": order.FulfillmentStatus, - "to": model.FulfillmentStatusFailed, + "from": normalizeOrderStatus(&order), + "to": model.OrderStatusShipFailed, "timeout_minutes": int(timeout.Minutes()), "reason": reason, }); err != nil { @@ -545,7 +539,7 @@ func (s *FulfillmentService) CancelOrder(merchantID, apiClientID uint, orderNo, } return err } - if order.FulfillmentStatus == model.FulfillmentStatusCancelled { + if normalizeOrderStatus(&order) == model.OrderStatusCancelled { out = order return nil } @@ -553,12 +547,9 @@ func (s *FulfillmentService) CancelOrder(merchantID, apiClientID uint, orderNo, return err } now := time.Now() - updates := map[string]interface{}{ - "payment_status": model.PaymentStatusRefunded, - "fulfillment_status": model.FulfillmentStatusCancelled, - "failure_reason": reason, - "cancelled_at": now, - } + updates := map[string]interface{}{"order_status": model.OrderStatusCancelled} + updates["failure_reason"] = reason + updates["cancelled_at"] = now if err := tx.Model(&order).Updates(updates).Error; err != nil { return err } @@ -739,20 +730,19 @@ func calculateServiceFee(baseAmount int64, feeType string, feeRateBP, feeFixedAm } func CanFulfill(order *model.FulfillmentOrder) (bool, string) { - if order.PaymentStatus != model.PaymentStatusPaid { - return false, "订单未支付或已退款" - } - switch order.FulfillmentStatus { - case model.FulfillmentStatusPending, model.FulfillmentStatusFailed: + switch normalizeOrderStatus(order) { + case model.OrderStatusPaid, model.OrderStatusShipFailed: return true, "" - case model.FulfillmentStatusProcessing: - return false, "订单履约中" - case model.FulfillmentStatusSucceeded: - return false, "订单已履约成功" - case model.FulfillmentStatusCancelled: + case model.OrderStatusPending: + return false, "订单未支付" + case model.OrderStatusDelivering: + return false, "订单发货中" + case model.OrderStatusDelivered: + return false, "订单已交付" + case model.OrderStatusCancelled: return false, "订单已取消" default: - return false, "订单状态不可履约" + return false, "订单状态不可发货" } } @@ -768,8 +758,7 @@ func orderCallbackData(order *model.FulfillmentOrder) map[string]interface{} { "service_fee_amount": order.ServiceFeeAmount, "amount": order.Amount, "currency": order.Currency, - "payment_status": order.PaymentStatus, - "fulfillment_status": order.FulfillmentStatus, + "order_status": normalizeOrderStatus(order), "can_fulfill": canFulfill, "cannot_fulfill_reason": cannotFulfillReason, "provider_order_no": order.ProviderOrderNo, @@ -795,9 +784,10 @@ type DashboardStats struct { TotalFees int64 `json:"total_fees"` TodayFees int64 `json:"today_fees"` PendingOrderCount int64 `json:"pending_order_count"` - ProcessingOrderCount int64 `json:"processing_order_count"` - SucceededOrderCount int64 `json:"succeeded_order_count"` - FailedOrderCount int64 `json:"failed_order_count"` + PaidOrderCount int64 `json:"paid_order_count"` + DeliveringOrderCount int64 `json:"delivering_order_count"` + DeliveredOrderCount int64 `json:"delivered_order_count"` + ShipFailedOrderCount int64 `json:"ship_failed_order_count"` CancelledOrderCount int64 `json:"cancelled_order_count"` WalletAvailableBalance int64 `json:"wallet_available_balance"` WalletFrozenBalance int64 `json:"wallet_frozen_balance"` @@ -904,40 +894,42 @@ func (s *FulfillmentService) Dashboard(merchantID uint, isPlatformAdmin bool) (* if err := orderScope().Where("created_at >= ?", todayStart).Count(&stats.TodayOrderCount).Error; err != nil { return nil, err } - if err := orderScope().Where("payment_status = ?", model.PaymentStatusPaid). + if err := orderScope().Where("order_status <> ?", model.OrderStatusCancelled). Select("COALESCE(SUM(amount),0)").Scan(&stats.TotalSales).Error; err != nil { return nil, err } - if err := orderScope().Where("payment_status = ?", model.PaymentStatusPaid). + if err := orderScope().Where("order_status <> ?", model.OrderStatusCancelled). Where("created_at >= ?", todayStart). Select("COALESCE(SUM(amount),0)").Scan(&stats.TodaySales).Error; err != nil { return nil, err } - if err := orderScope().Where("payment_status = ?", model.PaymentStatusPaid). + if err := orderScope().Where("order_status <> ?", model.OrderStatusCancelled). Select("COALESCE(SUM(service_fee_amount),0)").Scan(&stats.TotalFees).Error; err != nil { return nil, err } - if err := orderScope().Where("payment_status = ?", model.PaymentStatusPaid). + if err := orderScope().Where("order_status <> ?", model.OrderStatusCancelled). Where("created_at >= ?", todayStart). Select("COALESCE(SUM(service_fee_amount),0)").Scan(&stats.TodayFees).Error; err != nil { return nil, err } var orderStatusCounts []dashboardStatusCount - if err := orderScope().Select("fulfillment_status AS status, COUNT(*) AS count"). - Group("fulfillment_status").Scan(&orderStatusCounts).Error; err != nil { + if err := orderScope().Select("order_status AS status, COUNT(*) AS count"). + Group("order_status").Scan(&orderStatusCounts).Error; err != nil { return nil, err } for _, item := range orderStatusCounts { switch item.Status { - case model.FulfillmentStatusPending: + case model.OrderStatusPending: stats.PendingOrderCount = item.Count - case model.FulfillmentStatusProcessing: - stats.ProcessingOrderCount = item.Count - case model.FulfillmentStatusSucceeded: - stats.SucceededOrderCount = item.Count - case model.FulfillmentStatusFailed: - stats.FailedOrderCount = item.Count - case model.FulfillmentStatusCancelled: + case model.OrderStatusPaid: + stats.PaidOrderCount = item.Count + case model.OrderStatusDelivering: + stats.DeliveringOrderCount = item.Count + case model.OrderStatusDelivered: + stats.DeliveredOrderCount = item.Count + case model.OrderStatusShipFailed: + stats.ShipFailedOrderCount = item.Count + case model.OrderStatusCancelled: stats.CancelledOrderCount = item.Count } } @@ -1039,7 +1031,7 @@ func (s *FulfillmentService) QueryOpenOrder(orderNo string) (*OpenOrderQuery, er canShip, reason := CanFulfill(order) out := &OpenOrderQuery{ OrderNo: order.OrderNo, - Status: fulfillmentStatusToLegacyStatus(order.FulfillmentStatus), + Status: normalizeOrderStatus(order), CanShip: canShip, CannotShipReason: reason, BuyerName: order.BuyerReference, @@ -1086,9 +1078,9 @@ func (s *FulfillmentService) HandleShipNotify(in ShipNotifyInput) (*ShipNotifyRe var nextStatus string switch in.ShipStatus { case "success": - nextStatus = model.FulfillmentStatusSucceeded + nextStatus = model.OrderStatusDelivered case "failed": - nextStatus = model.FulfillmentStatusFailed + nextStatus = model.OrderStatusShipFailed } var result ShipNotifyResult @@ -1106,17 +1098,17 @@ func (s *FulfillmentService) HandleShipNotify(in ShipNotifyInput) (*ShipNotifyRe } // 已履约成功:success 推送幂等成功。状态读取和后续更新必须在同一把行锁内完成。 - if order.FulfillmentStatus == model.FulfillmentStatusSucceeded && in.ShipStatus == "success" { + if normalizeOrderStatus(&order) == model.OrderStatusDelivered && in.ShipStatus == "success" { result = ShipNotifyResult{ OrderNo: order.OrderNo, - Status: fulfillmentStatusToLegacyStatus(order.FulfillmentStatus), + Status: normalizeOrderStatus(&order), Message: "订单已交付,幂等成功", } return writeAudit(tx, &order.MerchantID, nil, nil, "ship.notify", "fulfillment_order", order.OrderNo, - shipNotifyAuditMetadata(in, order.FulfillmentStatus, "订单已履约成功,幂等忽略")) + shipNotifyAuditMetadata(in, normalizeOrderStatus(&order), "订单已履约成功,幂等忽略")) } - if order.FulfillmentStatus == model.FulfillmentStatusCancelled { + if normalizeOrderStatus(&order) == model.OrderStatusCancelled { if err := writeShipNotifyRejectedAudit(tx, &order, in, "订单已取消,拒绝更新"); err != nil { return err } @@ -1131,14 +1123,14 @@ func (s *FulfillmentService) HandleShipNotify(in ShipNotifyInput) (*ShipNotifyRe } updates := map[string]interface{}{ - "fulfillment_status": nextStatus, + "order_status": nextStatus, } var message string switch in.ShipStatus { case "success": - if err := validateFulfillmentTransition(&order, nextStatus, fulfillmentTransitionShipNotify); err != nil { + if err := validateOrderStatusTransition(&order, nextStatus, fulfillmentTransitionShipNotify); err != nil { message := "当前状态不允许标记发货成功" - if order.PaymentStatus != model.PaymentStatusPaid { + if normalizeOrderStatus(&order) == model.OrderStatusPending { message = "订单未支付,拒绝成功推送" } if err := writeShipNotifyRejectedAudit(tx, &order, in, message); err != nil { @@ -1154,11 +1146,11 @@ func (s *FulfillmentService) HandleShipNotify(in ShipNotifyInput) (*ShipNotifyRe } message = "发货成功,订单已交付" case "failed": - if err := validateFulfillmentTransition(&order, nextStatus, fulfillmentTransitionShipNotify); err != nil { + if err := validateOrderStatusTransition(&order, nextStatus, fulfillmentTransitionShipNotify); err != nil { message := "当前状态不允许标记发货失败" - if order.FulfillmentStatus == model.FulfillmentStatusSucceeded { + if normalizeOrderStatus(&order) == model.OrderStatusDelivered { message = "订单已交付,拒绝失败推送" - } else if order.PaymentStatus != model.PaymentStatusPaid { + } else if normalizeOrderStatus(&order) == model.OrderStatusPending { message = "订单未支付,拒绝失败推送" } if err := writeShipNotifyRejectedAudit(tx, &order, in, message); err != nil { @@ -1196,7 +1188,7 @@ func (s *FulfillmentService) HandleShipNotify(in ShipNotifyInput) (*ShipNotifyRe } result = ShipNotifyResult{ OrderNo: order.OrderNo, - Status: fulfillmentStatusToLegacyStatus(nextStatus), + Status: nextStatus, Message: message, } return nil @@ -1212,7 +1204,7 @@ func (s *FulfillmentService) HandleShipNotify(in ShipNotifyInput) (*ShipNotifyRe 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)) + shipNotifyAuditMetadata(in, normalizeOrderStatus(order), message)) } func shipNotifyAuditMetadata(in ShipNotifyInput, resultStatus, message string) map[string]interface{} { @@ -1273,7 +1265,7 @@ func buildProcessingTimeoutResultData(existing string, timeout time.Duration, no m["timeout_minutes"] = int(timeout.Minutes()) m["timeout_at"] = timeutil.FormatAPITime(now) m["ship_status"] = "failed" - m["fail_reason"] = fmt.Sprintf("履约超时:订单已处于 processing 超过 %d 分钟", int(timeout.Minutes())) + m["fail_reason"] = fmt.Sprintf("履约超时:订单已处于 delivering 超过 %d 分钟", int(timeout.Minutes())) raw, err := json.Marshal(m) if err != nil { return existing @@ -1323,21 +1315,3 @@ func toInt(v interface{}) (int, bool) { } return 0, false } - -// fulfillmentStatusToLegacyStatus 将新的履约状态映射为上游兼容的旧状态字符串。 -func fulfillmentStatusToLegacyStatus(status string) string { - switch status { - case model.FulfillmentStatusPending: - return "paid" - case model.FulfillmentStatusProcessing: - return "delivering" - case model.FulfillmentStatusSucceeded: - return "delivered" - case model.FulfillmentStatusFailed: - return "ship_failed" - case model.FulfillmentStatusCancelled: - return "cancelled" - default: - return status - } -} diff --git a/backend/internal/service/fulfillment_state.go b/backend/internal/service/fulfillment_state.go index 021a71f..8a5ebc6 100644 --- a/backend/internal/service/fulfillment_state.go +++ b/backend/internal/service/fulfillment_state.go @@ -63,60 +63,77 @@ func ensureSameIdempotentOrder(existing *model.FulfillmentOrder, fingerprint str return nil } -func validateFulfillmentTransition(order *model.FulfillmentOrder, next string, kind fulfillmentTransitionKind) error { +func normalizeOrderStatus(order *model.FulfillmentOrder) string { + if order == nil { + return "" + } + if order.OrderStatus != "" { + return order.OrderStatus + } + return model.OrderStatusPaid +} + +func validateOrderStatusTransition(order *model.FulfillmentOrder, next string, kind fulfillmentTransitionKind) error { if order == nil { return errors.New("订单不存在") } - if order.PaymentStatus != model.PaymentStatusPaid { - return errors.New("订单未支付,不能履约") - } - if order.FulfillmentStatus == next { + current := normalizeOrderStatus(order) + if current == next { return nil } - switch order.FulfillmentStatus { - case model.FulfillmentStatusCancelled: - return errors.New("订单已取消,不能更新履约状态") - case model.FulfillmentStatusSucceeded: + switch current { + case model.OrderStatusPending: + if next == model.OrderStatusPaid || next == model.OrderStatusCancelled { + return nil + } + return errors.New("订单未支付,不能履约") + case model.OrderStatusCancelled: + return errors.New("订单已取消,不能更新状态") + case model.OrderStatusDelivered: return errors.New("订单已履约成功,不能回退状态") } switch next { - case model.FulfillmentStatusProcessing: - if order.FulfillmentStatus == model.FulfillmentStatusPending || order.FulfillmentStatus == model.FulfillmentStatusFailed { + case model.OrderStatusDelivering: + if current == model.OrderStatusPaid || current == model.OrderStatusShipFailed { return nil } - case model.FulfillmentStatusSucceeded: - if order.FulfillmentStatus == model.FulfillmentStatusPending || - order.FulfillmentStatus == model.FulfillmentStatusFailed || - order.FulfillmentStatus == model.FulfillmentStatusProcessing { + case model.OrderStatusDelivered: + if current == model.OrderStatusPaid || + current == model.OrderStatusShipFailed || + current == model.OrderStatusDelivering { return nil } - case model.FulfillmentStatusFailed: - if kind == fulfillmentTransitionTimeout && order.FulfillmentStatus != model.FulfillmentStatusProcessing { + case model.OrderStatusShipFailed: + if kind == fulfillmentTransitionTimeout && current != model.OrderStatusDelivering { return errors.New("只有履约中的订单可以标记超时") } - if order.FulfillmentStatus == model.FulfillmentStatusPending || - order.FulfillmentStatus == model.FulfillmentStatusFailed || - order.FulfillmentStatus == model.FulfillmentStatusProcessing { + if current == model.OrderStatusPaid || + current == model.OrderStatusShipFailed || + current == model.OrderStatusDelivering { + return nil + } + case model.OrderStatusCancelled: + if current == model.OrderStatusPaid || current == model.OrderStatusShipFailed { return nil } } - return errors.New("订单当前状态不允许该履约变更") + return errors.New("订单当前状态不允许该状态变更") } func canCancelOrder(order *model.FulfillmentOrder) error { if order == nil { return errors.New("订单不存在") } - if order.FulfillmentStatus == model.FulfillmentStatusCancelled { + status := normalizeOrderStatus(order) + if status == model.OrderStatusCancelled { return nil } - if order.PaymentStatus != model.PaymentStatusPaid { - return errors.New("订单未支付或已退款,不能取消") - } - switch order.FulfillmentStatus { - case model.FulfillmentStatusPending, model.FulfillmentStatusFailed: + switch status { + case model.OrderStatusPaid, model.OrderStatusShipFailed: return nil - case model.FulfillmentStatusProcessing, model.FulfillmentStatusSucceeded: + case model.OrderStatusPending: + return errors.New("订单未支付,不能取消") + case model.OrderStatusDelivering, model.OrderStatusDelivered: return errors.New("订单已进入履约流程,不能取消") default: return errors.New("订单当前状态不能取消") @@ -127,5 +144,5 @@ func processingTimedOut(order *model.FulfillmentOrder, timeout time.Duration, no if order == nil || timeout <= 0 { return false } - return order.FulfillmentStatus == model.FulfillmentStatusProcessing && order.UpdatedAt.Before(now.Add(-timeout)) + return normalizeOrderStatus(order) == model.OrderStatusDelivering && order.UpdatedAt.Before(now.Add(-timeout)) } diff --git a/backend/internal/service/fulfillment_test.go b/backend/internal/service/fulfillment_test.go index 6372906..934f1a9 100644 --- a/backend/internal/service/fulfillment_test.go +++ b/backend/internal/service/fulfillment_test.go @@ -80,7 +80,7 @@ func TestFulfillmentCreateOrderDebitsWalletAndIsIdempotent(t *testing.T) { if first.Idempotent { t.Fatalf("first create should not be idempotent") } - if first.Order.Amount != 400 || first.Order.PaymentStatus != model.PaymentStatusPaid || first.Order.FulfillmentStatus != model.FulfillmentStatusPending { + if first.Order.Amount != 400 || first.Order.OrderStatus != model.OrderStatusPaid { t.Fatalf("unexpected order: %+v", first.Order) } @@ -202,7 +202,7 @@ func TestDashboardScopesPlatformAndMerchantData(t *testing.T) { if merchantStats.ProductCount != 1 || merchantStats.ActiveProductCount != 1 { t.Fatalf("merchant product stats should only include current merchant, got %+v", merchantStats) } - if merchantStats.OrderCount != 1 || merchantStats.PendingOrderCount != 1 || merchantStats.TotalSales != 100 { + if merchantStats.OrderCount != 1 || merchantStats.PaidOrderCount != 1 || merchantStats.TotalSales != 100 { t.Fatalf("merchant order stats should only include current merchant, got %+v", merchantStats) } if merchantStats.WalletAvailableBalance != 900 { @@ -216,7 +216,7 @@ func TestDashboardScopesPlatformAndMerchantData(t *testing.T) { if platformStats.Scope != "platform" { t.Fatalf("expected platform scope, got %s", platformStats.Scope) } - if platformStats.OrderCount != 2 || platformStats.PendingOrderCount != 2 || platformStats.TotalSales != 400 { + if platformStats.OrderCount != 2 || platformStats.PaidOrderCount != 2 || platformStats.TotalSales != 400 { t.Fatalf("platform order stats should include all merchants, got %+v", platformStats) } if platformStats.WalletAvailableBalance < 2600 { @@ -319,7 +319,7 @@ func TestFulfillmentCancelRefundsOnceAndRestoresStock(t *testing.T) { if err != nil { t.Fatalf("cancel order: %v", err) } - if cancelled.PaymentStatus != model.PaymentStatusRefunded || cancelled.FulfillmentStatus != model.FulfillmentStatusCancelled { + if cancelled.OrderStatus != model.OrderStatusCancelled { t.Fatalf("unexpected cancelled order: %+v", cancelled) } if _, err := svc.CancelOrder(merchantID, 12, created.Order.OrderNo, "重复取消"); err != nil { @@ -343,7 +343,7 @@ func TestFulfillmentCancelRefundsOnceAndRestoresStock(t *testing.T) { } } -func TestFulfillmentStatusTransitions(t *testing.T) { +func TestOrderStatusTransitions(t *testing.T) { db := newServiceTestDB(t) merchantID, product := seedFulfillmentMerchant(t, db, "merchant-c", 1000, -1, 100) svc := NewFulfillmentService(db, nil) @@ -356,37 +356,37 @@ func TestFulfillmentStatusTransitions(t *testing.T) { if err != nil { t.Fatalf("create order: %v", err) } - processing, err := svc.UpdateFulfillment(FulfillmentUpdateInput{ + delivering, err := svc.UpdateFulfillment(FulfillmentUpdateInput{ MerchantID: merchantID, APIClientID: 13, OrderNo: created.Order.OrderNo, - Status: model.FulfillmentStatusProcessing, + Status: model.OrderStatusDelivering, }) if err != nil { - t.Fatalf("mark processing: %v", err) + t.Fatalf("mark delivering: %v", err) } - if processing.FulfillmentStatus != model.FulfillmentStatusProcessing { - t.Fatalf("expected processing, got %s", processing.FulfillmentStatus) + if delivering.OrderStatus != model.OrderStatusDelivering { + t.Fatalf("expected delivering, got %s", delivering.OrderStatus) } - succeeded, err := svc.UpdateFulfillment(FulfillmentUpdateInput{ + delivered, err := svc.UpdateFulfillment(FulfillmentUpdateInput{ MerchantID: merchantID, APIClientID: 13, OrderNo: created.Order.OrderNo, - Status: model.FulfillmentStatusSucceeded, + Status: model.OrderStatusDelivered, ProviderOrderNo: "provider-1", ResultData: map[string]string{"ok": "true"}, }) if err != nil { - t.Fatalf("mark succeeded: %v", err) + t.Fatalf("mark delivered: %v", err) } - if succeeded.FulfillmentStatus != model.FulfillmentStatusSucceeded || succeeded.DeliveredAt == nil { - t.Fatalf("unexpected succeeded order: %+v", succeeded) + if delivered.OrderStatus != model.OrderStatusDelivered || delivered.DeliveredAt == nil { + t.Fatalf("unexpected delivered order: %+v", delivered) } _, err = svc.UpdateFulfillment(FulfillmentUpdateInput{ MerchantID: merchantID, APIClientID: 13, OrderNo: created.Order.OrderNo, - Status: model.FulfillmentStatusFailed, + Status: model.OrderStatusShipFailed, }) if err == nil { t.Fatalf("should reject rollback after success") @@ -406,19 +406,19 @@ func TestMarkProcessingTimeoutsMarksStaleOrdersFailed(t *testing.T) { if err != nil { t.Fatalf("create order: %v", err) } - processing, err := svc.UpdateFulfillment(FulfillmentUpdateInput{ + delivering, err := svc.UpdateFulfillment(FulfillmentUpdateInput{ MerchantID: merchantID, APIClientID: 13, OrderNo: created.Order.OrderNo, - Status: model.FulfillmentStatusProcessing, + Status: model.OrderStatusDelivering, }) if err != nil { - t.Fatalf("mark processing: %v", err) + t.Fatalf("mark delivering: %v", err) } if err := db.Model(&model.FulfillmentOrder{}). - Where("id = ?", processing.ID). + Where("id = ?", delivering.ID). Update("updated_at", time.Now().Add(-time.Hour)).Error; err != nil { - t.Fatalf("age processing order: %v", err) + t.Fatalf("age delivering order: %v", err) } changed, err := svc.MarkProcessingTimeouts(30*time.Minute, 10) @@ -429,10 +429,10 @@ func TestMarkProcessingTimeoutsMarksStaleOrdersFailed(t *testing.T) { t.Fatalf("expected one timed out order, got %d", changed) } var order model.FulfillmentOrder - if err := db.First(&order, processing.ID).Error; err != nil { + if err := db.First(&order, delivering.ID).Error; err != nil { t.Fatalf("query order: %v", err) } - if order.FulfillmentStatus != model.FulfillmentStatusFailed || !strings.Contains(order.FailureReason, "履约超时") { + if order.OrderStatus != model.OrderStatusShipFailed || !strings.Contains(order.FailureReason, "履约超时") { t.Fatalf("expected failed timeout order, got %+v", order) } if !strings.Contains(order.ResultData, `"timeout":true`) { @@ -453,28 +453,28 @@ func TestMarkProcessingTimeoutsSkipsRecentOrders(t *testing.T) { if err != nil { t.Fatalf("create order: %v", err) } - processing, err := svc.UpdateFulfillment(FulfillmentUpdateInput{ + delivering, err := svc.UpdateFulfillment(FulfillmentUpdateInput{ MerchantID: merchantID, APIClientID: 13, OrderNo: created.Order.OrderNo, - Status: model.FulfillmentStatusProcessing, + Status: model.OrderStatusDelivering, }) if err != nil { - t.Fatalf("mark processing: %v", err) + t.Fatalf("mark delivering: %v", err) } changed, err := svc.MarkProcessingTimeouts(30*time.Minute, 10) if err != nil { t.Fatalf("mark timeouts: %v", err) } if changed != 0 { - t.Fatalf("recent processing order should not time out, got %d", changed) + t.Fatalf("recent delivering order should not time out, got %d", changed) } var order model.FulfillmentOrder - if err := db.First(&order, processing.ID).Error; err != nil { + if err := db.First(&order, delivering.ID).Error; err != nil { t.Fatalf("query order: %v", err) } - if order.FulfillmentStatus != model.FulfillmentStatusProcessing { - t.Fatalf("expected processing order, got %+v", order) + if order.OrderStatus != model.OrderStatusDelivering { + t.Fatalf("expected delivering order, got %+v", order) } } @@ -534,18 +534,16 @@ func TestWalletAdjustIsIdempotentPerMerchant(t *testing.T) { func TestCanFulfill(t *testing.T) { ok, reason := CanFulfill(&model.FulfillmentOrder{ - PaymentStatus: model.PaymentStatusPaid, - FulfillmentStatus: model.FulfillmentStatusFailed, + OrderStatus: model.OrderStatusShipFailed, }) if !ok || reason != "" { t.Fatalf("failed paid order should be fulfillable") } ok, _ = CanFulfill(&model.FulfillmentOrder{ - PaymentStatus: model.PaymentStatusRefunded, - FulfillmentStatus: model.FulfillmentStatusPending, + OrderStatus: model.OrderStatusCancelled, }) if ok { - t.Fatalf("refunded order should not be fulfillable") + t.Fatalf("cancelled order should not be fulfillable") } } @@ -588,7 +586,7 @@ func TestCreateTestOrderCreatesFulfillableOrderWithoutBilling(t *testing.T) { if !strings.HasPrefix(order.OrderNo, "O") || order.ClientOrderNo != "TEST-"+order.OrderNo { t.Fatalf("unexpected test order numbers: %+v", order) } - if order.PaymentStatus != model.PaymentStatusPaid || order.FulfillmentStatus != model.FulfillmentStatusPending { + if order.OrderStatus != model.OrderStatusPaid { t.Fatalf("unexpected test order status: %+v", order) } canShip, reason := CanFulfill(order) @@ -631,15 +629,15 @@ func TestCreateTestOrderSupportsFailedFulfillableStatus(t *testing.T) { svc := NewFulfillmentService(db, nil) order, err := svc.CreateTestOrder(CreateTestOrderInput{ - MerchantID: merchantID, - SKU: product.SKU, - FulfillmentStatus: model.FulfillmentStatusFailed, - Note: "等待重新发货", + MerchantID: merchantID, + SKU: product.SKU, + OrderStatus: model.OrderStatusShipFailed, + Note: "等待重新发货", }) if err != nil { t.Fatalf("create failed test order: %v", err) } - if order.FulfillmentStatus != model.FulfillmentStatusFailed || order.FailureReason != "等待重新发货" { + if order.OrderStatus != model.OrderStatusShipFailed || order.FailureReason != "等待重新发货" { t.Fatalf("unexpected failed test order: %+v", order) } openOrder, err := svc.QueryOpenOrder(order.OrderNo) @@ -694,7 +692,7 @@ func TestHandleShipNotifyUpdatesOrderAndEnqueuesMerchantCallback(t *testing.T) { if err := db.First(&order, created.Order.ID).Error; err != nil { t.Fatalf("query order: %v", err) } - if order.FulfillmentStatus != model.FulfillmentStatusSucceeded || order.ProviderOrderNo != "SRC-10001" || order.DeliveredAt == nil { + if order.OrderStatus != model.OrderStatusDelivered || order.ProviderOrderNo != "SRC-10001" || order.DeliveredAt == nil { t.Fatalf("unexpected order after source notify: %+v", order) } var delivery model.CallbackDelivery @@ -771,7 +769,7 @@ func TestHandleShipNotifySuccessClearsPreviousResultFailureReason(t *testing.T) if err := db.First(&order, created.Order.ID).Error; err != nil { t.Fatalf("query order: %v", err) } - if order.FulfillmentStatus != model.FulfillmentStatusSucceeded || order.FailureReason != "" { + if order.OrderStatus != model.OrderStatusDelivered || order.FailureReason != "" { t.Fatalf("success should clear order failure reason, got %+v", order) } if strings.Contains(order.ResultData, "fail_reason") { diff --git a/docs/API对接关系.md b/docs/API对接关系.md index 15a907b..0c574d1 100644 --- a/docs/API对接关系.md +++ b/docs/API对接关系.md @@ -28,12 +28,12 @@ ↓ POST /api/open/v1/orders/ship-notify 只回传 success 或 failed ↓ -本平台更新履约状态 +本平台更新订单状态 ↓ 商户回调 / 商户查询订单 ``` -当前流程不再维护独立的 `fulfillment_jobs` 任务表;履约事实以订单表里的 `fulfillment_status`、上游单号、发货时间和失败原因等字段为准。 +当前流程不再维护独立的 `fulfillment_jobs` 任务表;订单事实以订单表里的 `order_status`、上游单号、发货时间和失败原因等字段为准。 ## 3. 鉴权不要混用 @@ -51,17 +51,16 @@ ## 4. 状态语言 -内部订单使用 `payment_status` + `fulfillment_status` 两组状态;源头侧为了兼容对接,返回的是更贴近发货系统的旧状态名。 +平台内部、商户侧开放 API、源头侧查询和商户回调统一使用 `order_status`。 -| 内部 payment_status | 内部 fulfillment_status | 源头侧 `status` | 含义 | 是否可发货 | -|---------------------|-------------------------|-----------------|------|------------| -| `paid` | `pending` | `paid` | 已付款,等待发货 | 是 | -| `paid` | `processing` | `delivering` | 履约中 | 否 | -| `paid` | `succeeded` | `delivered` | 已交付 | 否 | -| `paid` | `failed` | `ship_failed` | 发货失败,可重试 | 是 | -| `refunded` | `cancelled` | `cancelled` | 已取消 / 已退款 | 否 | - -注意:源头侧返回的 `status=paid` 不是单独的支付状态,而是“这个订单已支付且处于待发货履约状态”的兼容表达。 +| order_status | 含义 | 是否可发货 | +|--------------|------|------------| +| `pending` | 待支付(当前无真实支付流程,仅预留) | 否 | +| `paid` | 已支付/已扣款,待发货 | 是 | +| `delivering` | 发货中 | 否 | +| `delivered` | 已交付 | 否 | +| `ship_failed` | 发货失败,可重试 | 是 | +| `cancelled` | 已取消 / 已退款 | 否 | ## 5. ship_notify 当前契约 diff --git a/docs/发货通知约定.md b/docs/发货通知约定.md index 1394bc1..ba65bc0 100644 --- a/docs/发货通知约定.md +++ b/docs/发货通知约定.md @@ -11,7 +11,7 @@ - `success` 表示发货成功 - `failed` 表示发货失败 - `failed` 时,`fail_reason` 必填,且要写详细失败原因 -- `processing` 是旧口径里曾出现过的中间状态,当前 `ship_notify` 不再接受 +- `ship_notify` 不接受中间态,提交发货时由平台内部将订单置为 `delivering` ## 2. 推荐推送格式 diff --git a/docs/开放接口-皮肤源头对接.md b/docs/开放接口-皮肤源头对接.md index feb668d..c424f11 100644 --- a/docs/开放接口-皮肤源头对接.md +++ b/docs/开放接口-皮肤源头对接.md @@ -216,7 +216,7 @@ GET /api/open/v1/orders/{order_no} ## 4. 接口二:发货结果推送 -> 当前接口只接受 `success` / `failed` 两种最终结果。`processing` 是旧口径里曾出现过的中间状态,当前 `ship_notify` 不再接受;当 `ship_status=failed` 时,`fail_reason` 必须填写详细失败原因。 +> 当前接口只接受 `success` / `failed` 两种最终结果;当 `ship_status=failed` 时,`fail_reason` 必须填写详细失败原因。 ### 4.1 请求 diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 4bd1157..827be0e 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -67,7 +67,7 @@ export const merchantApi = { sku: string buyer_reference?: string note?: string - fulfillment_status?: 'pending' | 'failed' + order_status?: 'paid' | 'ship_failed' }) => request.post('/merchant/orders/test', data).then((r) => r.data.data as CreateTestOrderResult), getDeliveryLink: (orderNo: string) => request.get(`/merchant/orders/${encodeURIComponent(orderNo)}/delivery-link`).then((r) => r.data.data as DeliveryLinkResult), diff --git a/frontend/src/openapi/endpoints.ts b/frontend/src/openapi/endpoints.ts index fc44332..4df1813 100644 --- a/frontend/src/openapi/endpoints.ts +++ b/frontend/src/openapi/endpoints.ts @@ -16,28 +16,21 @@ export const errorCodes: ParamSpec[] = [ { name: '500', type: 'int', desc: '服务端内部错误' }, ] -// 订单状态与可履约说明(payment_status / fulfillment_status)。 +// 订单状态与可履约说明。 export const orderStatusTable: ParamSpec[] = [ - { name: 'pending', type: 'fulfillment_status', desc: '待履约,可被履约器接单', example: 'can_fulfill=true' }, - { name: 'processing', type: 'fulfillment_status', desc: '履约中', example: 'can_fulfill=false' }, - { name: 'succeeded', type: 'fulfillment_status', desc: '履约成功', example: 'can_fulfill=false' }, - { name: 'failed', type: 'fulfillment_status', desc: '履约失败,可重试', example: 'can_fulfill=true' }, - { name: 'cancelled', type: 'fulfillment_status', desc: '已取消', example: 'can_fulfill=false' }, -] - -export const paymentStatusTable: ParamSpec[] = [ - { name: 'pending', type: 'payment_status', desc: '待支付' }, - { name: 'paid', type: 'payment_status', desc: '已支付(下单成功即为 paid)' }, - { name: 'refunded', type: 'payment_status', desc: '已退款(取消订单后)' }, - { name: 'cancelled', type: 'payment_status', desc: '已取消' }, + { name: 'pending', type: 'order_status', desc: '待支付(当前无真实支付流程,仅预留)', example: 'can_fulfill=false' }, + { name: 'paid', type: 'order_status', desc: '已支付/已扣款,可发货', example: 'can_fulfill=true' }, + { name: 'delivering', type: 'order_status', desc: '发货中', example: 'can_fulfill=false' }, + { name: 'delivered', type: 'order_status', desc: '已交付', example: 'can_fulfill=false' }, + { name: 'ship_failed', type: 'order_status', desc: '发货失败,可重试', example: 'can_fulfill=true' }, + { name: 'cancelled', type: 'order_status', desc: '已取消', example: 'can_fulfill=false' }, ] // 订单对象公共字段说明,下单/查询/取消响应共用。 const orderFields: ParamSpec[] = [ { name: 'order_no', type: 'string', required: true, desc: '平台订单号' }, { name: 'client_order_no', type: 'string', required: true, desc: '调用方传入的幂等单号' }, - { name: 'payment_status', type: 'enum', desc: '支付状态,见「状态说明」' }, - { name: 'fulfillment_status', type: 'enum', desc: '履约状态,见「状态说明」' }, + { name: 'order_status', type: 'enum', desc: '订单状态,见「状态说明」' }, { name: 'can_fulfill', type: 'bool', desc: '是否允许履约器接单/发货' }, { name: 'cannot_fulfill_reason', type: 'string', desc: 'can_fulfill=false 时的原因' }, { name: 'product.sku', type: 'string', desc: '商品标识,履约以此为准' }, @@ -64,8 +57,7 @@ const orderResponseExample = `{ "data": { "order_no": "FO20260730000123", "client_order_no": "shop-10001", - "payment_status": "paid", - "fulfillment_status": "pending", + "order_status": "paid", "can_fulfill": true, "cannot_fulfill_reason": "", "product": { "sku": "suit_pink_sheep", "name": "套装-糯粉咩咩" }, @@ -134,7 +126,7 @@ const deliverySubmitResponseExample = `{ "message": "ok", "data": { "order_no": "FO20260730000123", - "status": "processing", + "status": "delivering", "message": "已提交上游发货,等待发货结果回传", "provider_order_no": "provider-1", "game_account": { @@ -222,8 +214,7 @@ export const endpoints: EndpointSpec[] = [ "order": { "order_no": "FO20260730000123", "client_order_no": "shop-10001", - "payment_status": "paid", - "fulfillment_status": "pending", + "order_status": "paid", "can_fulfill": true } } @@ -234,7 +225,7 @@ export const endpoints: EndpointSpec[] = [ ], notes: [ 'client_order_no 是唯一幂等单号,请保证同一商户下不重复。', - '下单即扣款(payment_status=paid),余额不足返回 400。', + '当前无真实支付流程,下单成功即扣款并进入 order_status=paid,余额不足返回 400。', '同一 client_order_no 重复请求不会重复扣款,返回 idempotent=true。', ], }, @@ -254,7 +245,7 @@ export const endpoints: EndpointSpec[] = [ method: 'POST', path: '/api/client/v1/orders/{order_no}/cancel', title: '取消订单', - summary: '取消未履约订单并退款(仅 pending/failed 可取消)', + summary: '取消未发货订单并退款(仅 paid/ship_failed 可取消)', scope: 'orders:write', pathParams: [{ name: 'order_no', type: 'string', required: true, desc: '平台订单号', example: 'FO20260730000123' }], bodyParams: [{ name: 'reason', type: 'string', desc: '取消原因(可选)', example: '买家主动取消' }], @@ -262,8 +253,8 @@ export const endpoints: EndpointSpec[] = [ responseExample: orderResponseExample, responseFields: orderFields, notes: [ - '取消成功后 payment_status 变为 refunded,金额退回商户钱包。', - '已进入 processing/succeeded 的订单不可取消,返回 400。', + '取消成功后 order_status 变为 cancelled,金额退回商户钱包。', + '已进入 delivering/delivered 的订单不可取消,返回 400。', ], }, { diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 3c79c77..b36664b 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -131,10 +131,10 @@ export default function Dashboard() {