统一订单单状态模型
This commit is contained in:
@@ -126,13 +126,14 @@ make docker-down # 停止并移除
|
|||||||
|
|
||||||
### 状态速查
|
### 状态速查
|
||||||
|
|
||||||
| 内部支付状态 | 内部履约状态 | 源头侧状态 | 含义 |
|
| 订单状态 | 含义 | 是否可发货 |
|
||||||
|--------------|--------------|------------|------|
|
|----------|------|------------|
|
||||||
| `paid` | `pending` | `paid` | 已付款,待发货 |
|
| `pending` | 待支付(当前无真实支付流程,仅预留) | 否 |
|
||||||
| `paid` | `processing` | `delivering` | 已提交履约,等待结果 |
|
| `paid` | 已支付/已扣款,待发货 | 是 |
|
||||||
| `paid` | `succeeded` | `delivered` | 履约成功 |
|
| `delivering` | 发货中 | 否 |
|
||||||
| `paid` | `failed` | `ship_failed` | 履约失败,可重试 |
|
| `delivered` | 已交付 | 否 |
|
||||||
| `refunded` | `cancelled` | `cancelled` | 已取消并退款 |
|
| `ship_failed` | 发货失败,可重试 | 是 |
|
||||||
|
| `cancelled` | 已取消并退款 | 否 |
|
||||||
|
|
||||||
上游皮肤源头系统调用源头侧接口时,需携带签名头 `X-Api-Key`、`X-Timestamp`、`X-Nonce`、`X-Sign`。
|
上游皮肤源头系统调用源头侧接口时,需携带签名头 `X-Api-Key`、`X-Timestamp`、`X-Nonce`、`X-Sign`。
|
||||||
|
|
||||||
|
|||||||
@@ -201,8 +201,7 @@ CREATE TABLE IF NOT EXISTS fulfillment_orders (
|
|||||||
service_fee_amount BIGINT NOT NULL DEFAULT 0,
|
service_fee_amount BIGINT NOT NULL DEFAULT 0,
|
||||||
amount BIGINT NOT NULL,
|
amount BIGINT NOT NULL,
|
||||||
currency VARCHAR(12) NOT NULL DEFAULT 'POINT',
|
currency VARCHAR(12) NOT NULL DEFAULT 'POINT',
|
||||||
payment_status VARCHAR(16) NOT NULL DEFAULT 'pending',
|
order_status VARCHAR(16) NOT NULL DEFAULT 'paid',
|
||||||
fulfillment_status VARCHAR(16) NOT NULL DEFAULT 'pending',
|
|
||||||
buyer_reference VARCHAR(128),
|
buyer_reference VARCHAR(128),
|
||||||
request_data TEXT,
|
request_data TEXT,
|
||||||
result_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_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_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_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_order_status ON fulfillment_orders (order_status);
|
||||||
CREATE INDEX IF NOT EXISTS idx_fulfillment_orders_fulfillment_status ON fulfillment_orders (fulfillment_status);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_fulfillment_orders_provider_order_no ON fulfillment_orders (provider_order_no);
|
CREATE INDEX IF NOT EXISTS idx_fulfillment_orders_provider_order_no ON fulfillment_orders (provider_order_no);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS fulfillment_jobs (
|
CREATE TABLE IF NOT EXISTS fulfillment_jobs (
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ func (h *MerchantHandler) UpdateProduct(c *gin.Context) {
|
|||||||
|
|
||||||
func (h *MerchantHandler) ListOrders(c *gin.Context) {
|
func (h *MerchantHandler) ListOrders(c *gin.Context) {
|
||||||
page, size := pageParams(c)
|
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 {
|
if err != nil {
|
||||||
response.ServerError(c, err.Error())
|
response.ServerError(c, err.Error())
|
||||||
return
|
return
|
||||||
@@ -135,10 +135,10 @@ func (h *MerchantHandler) ListOrders(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type merchantTestOrderReq struct {
|
type merchantTestOrderReq struct {
|
||||||
SKU string `json:"sku" binding:"required"`
|
SKU string `json:"sku" binding:"required"`
|
||||||
BuyerReference string `json:"buyer_reference"`
|
BuyerReference string `json:"buyer_reference"`
|
||||||
Note string `json:"note"`
|
Note string `json:"note"`
|
||||||
FulfillmentStatus string `json:"fulfillment_status"`
|
OrderStatus string `json:"order_status"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *MerchantHandler) CreateTestOrder(c *gin.Context) {
|
func (h *MerchantHandler) CreateTestOrder(c *gin.Context) {
|
||||||
@@ -148,12 +148,12 @@ func (h *MerchantHandler) CreateTestOrder(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
order, err := h.fulfillmentSvc.CreateTestOrder(service.CreateTestOrderInput{
|
order, err := h.fulfillmentSvc.CreateTestOrder(service.CreateTestOrderInput{
|
||||||
MerchantID: middleware.GetMerchantID(c),
|
MerchantID: middleware.GetMerchantID(c),
|
||||||
ActorUserID: middleware.GetUserID(c),
|
ActorUserID: middleware.GetUserID(c),
|
||||||
SKU: req.SKU,
|
SKU: req.SKU,
|
||||||
BuyerReference: req.BuyerReference,
|
BuyerReference: req.BuyerReference,
|
||||||
Note: req.Note,
|
Note: req.Note,
|
||||||
FulfillmentStatus: req.FulfillmentStatus,
|
OrderStatus: req.OrderStatus,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.BadRequest(c, err.Error())
|
response.BadRequest(c, err.Error())
|
||||||
|
|||||||
@@ -110,8 +110,8 @@ func (h *OpenV1Handler) QueryOrder(c *gin.Context) {
|
|||||||
response.ServerError(c, err.Error())
|
response.ServerError(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
openlog.Info(c, "query_order ok order_no=%s status=%s payment=%s",
|
openlog.Info(c, "query_order ok order_no=%s status=%s",
|
||||||
order.OrderNo, order.FulfillmentStatus, order.PaymentStatus)
|
order.OrderNo, order.OrderStatus)
|
||||||
response.OK(c, buildOpenOrderResponse(order))
|
response.OK(c, buildOpenOrderResponse(order))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,8 +259,7 @@ func buildOpenOrderResponse(order *model.FulfillmentOrder) gin.H {
|
|||||||
data := gin.H{
|
data := gin.H{
|
||||||
"order_no": order.OrderNo,
|
"order_no": order.OrderNo,
|
||||||
"client_order_no": order.ClientOrderNo,
|
"client_order_no": order.ClientOrderNo,
|
||||||
"payment_status": order.PaymentStatus,
|
"order_status": order.OrderStatus,
|
||||||
"fulfillment_status": order.FulfillmentStatus,
|
|
||||||
"can_fulfill": canFulfill,
|
"can_fulfill": canFulfill,
|
||||||
"cannot_fulfill_reason": reason,
|
"cannot_fulfill_reason": reason,
|
||||||
"product": gin.H{
|
"product": gin.H{
|
||||||
|
|||||||
@@ -34,16 +34,12 @@ const (
|
|||||||
ProductStatusActive = "active"
|
ProductStatusActive = "active"
|
||||||
ProductStatusInactive = "inactive"
|
ProductStatusInactive = "inactive"
|
||||||
|
|
||||||
PaymentStatusPending = "pending"
|
OrderStatusPending = "pending" // 待支付(当前无真实支付流程,仅预留)
|
||||||
PaymentStatusPaid = "paid"
|
OrderStatusPaid = "paid" // 已扣款,待发货
|
||||||
PaymentStatusRefunded = "refunded"
|
OrderStatusDelivering = "delivering" // 发货中
|
||||||
PaymentStatusCancelled = "cancelled"
|
OrderStatusDelivered = "delivered" // 已交付
|
||||||
|
OrderStatusShipFailed = "ship_failed" // 发货失败(可重试)
|
||||||
FulfillmentStatusPending = "pending"
|
OrderStatusCancelled = "cancelled" // 已取消并退款
|
||||||
FulfillmentStatusProcessing = "processing"
|
|
||||||
FulfillmentStatusSucceeded = "succeeded"
|
|
||||||
FulfillmentStatusFailed = "failed"
|
|
||||||
FulfillmentStatusCancelled = "cancelled"
|
|
||||||
|
|
||||||
WalletLedgerCredit = "credit"
|
WalletLedgerCredit = "credit"
|
||||||
WalletLedgerDebit = "debit"
|
WalletLedgerDebit = "debit"
|
||||||
@@ -189,7 +185,7 @@ type WalletLedgerEntry struct {
|
|||||||
Note string `gorm:"size:255" json:"note"`
|
Note string `gorm:"size:255" json:"note"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// FulfillmentOrder 将支付和履约状态拆分,并保留商品及请求快照。
|
// FulfillmentOrder 保留订单状态、商品及请求快照。
|
||||||
type FulfillmentOrder struct {
|
type FulfillmentOrder struct {
|
||||||
ID uint `gorm:"primarykey" json:"id"`
|
ID uint `gorm:"primarykey" json:"id"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
@@ -210,8 +206,7 @@ type FulfillmentOrder struct {
|
|||||||
ServiceFeeAmount int64 `gorm:"not null;default:0" json:"service_fee_amount"`
|
ServiceFeeAmount int64 `gorm:"not null;default:0" json:"service_fee_amount"`
|
||||||
Amount int64 `gorm:"not null" json:"amount"`
|
Amount int64 `gorm:"not null" json:"amount"`
|
||||||
Currency string `gorm:"size:12;not null;default:POINT" json:"currency"`
|
Currency string `gorm:"size:12;not null;default:POINT" json:"currency"`
|
||||||
PaymentStatus string `gorm:"size:16;not null;default:pending;index" json:"payment_status"`
|
OrderStatus string `gorm:"size:16;not null;default:paid;index" json:"order_status"`
|
||||||
FulfillmentStatus string `gorm:"size:16;not null;default:pending;index" json:"fulfillment_status"`
|
|
||||||
BuyerReference string `gorm:"size:128" json:"buyer_reference"`
|
BuyerReference string `gorm:"size:128" json:"buyer_reference"`
|
||||||
RequestFingerprint string `gorm:"size:64;not null;default:'';index" json:"-"`
|
RequestFingerprint string `gorm:"size:64;not null;default:'';index" json:"-"`
|
||||||
RequestData string `gorm:"type:text" json:"request_data"`
|
RequestData string `gorm:"type:text" json:"request_data"`
|
||||||
|
|||||||
@@ -242,8 +242,9 @@ func (s *DeliveryService) submit(orderNo, gameAccount, bindUUID string, apiClien
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if order.FulfillmentStatus == model.FulfillmentStatusProcessing ||
|
orderStatus := normalizeOrderStatus(order)
|
||||||
order.FulfillmentStatus == model.FulfillmentStatusSucceeded ||
|
if orderStatus == model.OrderStatusDelivering ||
|
||||||
|
orderStatus == model.OrderStatusDelivered ||
|
||||||
strings.TrimSpace(order.ProviderOrderNo) != "" ||
|
strings.TrimSpace(order.ProviderOrderNo) != "" ||
|
||||||
strings.Contains(order.ResultData, "\"queue_order_id\"") {
|
strings.Contains(order.ResultData, "\"queue_order_id\"") {
|
||||||
if existing := buildExistingDeliverySubmitResult(order); existing != nil {
|
if existing := buildExistingDeliverySubmitResult(order); existing != nil {
|
||||||
@@ -311,7 +312,7 @@ func (s *DeliveryService) submit(orderNo, gameAccount, bindUUID string, apiClien
|
|||||||
MerchantID: order.MerchantID,
|
MerchantID: order.MerchantID,
|
||||||
APIClientID: apiClientID,
|
APIClientID: apiClientID,
|
||||||
OrderNo: order.OrderNo,
|
OrderNo: order.OrderNo,
|
||||||
Status: "processing",
|
Status: model.OrderStatusDelivering,
|
||||||
ProviderOrderNo: providerOrderNo,
|
ProviderOrderNo: providerOrderNo,
|
||||||
ResultData: resultData,
|
ResultData: resultData,
|
||||||
})
|
})
|
||||||
@@ -320,7 +321,7 @@ func (s *DeliveryService) submit(orderNo, gameAccount, bindUUID string, apiClien
|
|||||||
}
|
}
|
||||||
return &DeliverySubmitResult{
|
return &DeliverySubmitResult{
|
||||||
OrderNo: updated.OrderNo,
|
OrderNo: updated.OrderNo,
|
||||||
Status: updated.FulfillmentStatus,
|
Status: normalizeOrderStatus(updated),
|
||||||
Message: "已提交上游发货,等待发货结果回传",
|
Message: "已提交上游发货,等待发货结果回传",
|
||||||
ProviderOrderNo: providerOrderNo,
|
ProviderOrderNo: providerOrderNo,
|
||||||
GameAccount: boundAccount,
|
GameAccount: boundAccount,
|
||||||
@@ -341,11 +342,12 @@ func (s *DeliveryService) claimDeliverySubmission(merchantID, apiClientID uint,
|
|||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if order.FulfillmentStatus == model.FulfillmentStatusProcessing || order.FulfillmentStatus == model.FulfillmentStatusSucceeded {
|
orderStatus := normalizeOrderStatus(&order)
|
||||||
|
if orderStatus == model.OrderStatusDelivering || orderStatus == model.OrderStatusDelivered {
|
||||||
updated = order
|
updated = order
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err := validateFulfillmentTransition(&order, model.FulfillmentStatusProcessing, fulfillmentTransitionUpdate); err != nil {
|
if err := validateOrderStatusTransition(&order, model.OrderStatusDelivering, fulfillmentTransitionUpdate); err != nil {
|
||||||
return newDeliveryHTTPError(http.StatusConflict, "订单暂不可发货")
|
return newDeliveryHTTPError(http.StatusConflict, "订单暂不可发货")
|
||||||
}
|
}
|
||||||
raw, _ := json.Marshal(map[string]interface{}{
|
raw, _ := json.Marshal(map[string]interface{}{
|
||||||
@@ -354,9 +356,9 @@ func (s *DeliveryService) claimDeliverySubmission(merchantID, apiClientID uint,
|
|||||||
"provider_order_stage": "claimed",
|
"provider_order_stage": "claimed",
|
||||||
})
|
})
|
||||||
if err := tx.Model(&order).Updates(map[string]interface{}{
|
if err := tx.Model(&order).Updates(map[string]interface{}{
|
||||||
"fulfillment_status": model.FulfillmentStatusProcessing,
|
"order_status": model.OrderStatusDelivering,
|
||||||
"result_data": string(raw),
|
"result_data": string(raw),
|
||||||
"failure_reason": "",
|
"failure_reason": "",
|
||||||
}).Error; err != nil {
|
}).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -389,7 +391,7 @@ func (s *DeliveryService) markDeliverySubmissionFailed(order *model.FulfillmentO
|
|||||||
MerchantID: order.MerchantID,
|
MerchantID: order.MerchantID,
|
||||||
APIClientID: apiClientID,
|
APIClientID: apiClientID,
|
||||||
OrderNo: order.OrderNo,
|
OrderNo: order.OrderNo,
|
||||||
Status: model.FulfillmentStatusFailed,
|
Status: model.OrderStatusShipFailed,
|
||||||
FailureReason: reason,
|
FailureReason: reason,
|
||||||
ResultData: map[string]interface{}{
|
ResultData: map[string]interface{}{
|
||||||
"source": "delivery_proxy",
|
"source": "delivery_proxy",
|
||||||
@@ -492,8 +494,8 @@ func buildExistingDeliverySubmitResult(order *model.FulfillmentOrder) *DeliveryS
|
|||||||
if order == nil {
|
if order == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
switch order.FulfillmentStatus {
|
switch normalizeOrderStatus(order) {
|
||||||
case model.FulfillmentStatusProcessing, model.FulfillmentStatusSucceeded:
|
case model.OrderStatusDelivering, model.OrderStatusDelivered:
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -506,12 +508,13 @@ func buildExistingDeliverySubmitResult(order *model.FulfillmentOrder) *DeliveryS
|
|||||||
stringFromMap(resultData, "provider_order_no"),
|
stringFromMap(resultData, "provider_order_no"),
|
||||||
)
|
)
|
||||||
message := "订单暂时正在履约中,请稍后查询"
|
message := "订单暂时正在履约中,请稍后查询"
|
||||||
if order.FulfillmentStatus == model.FulfillmentStatusSucceeded {
|
status := normalizeOrderStatus(order)
|
||||||
|
if status == model.OrderStatusDelivered {
|
||||||
message = "订单已履约成功"
|
message = "订单已履约成功"
|
||||||
}
|
}
|
||||||
return &DeliverySubmitResult{
|
return &DeliverySubmitResult{
|
||||||
OrderNo: order.OrderNo,
|
OrderNo: order.OrderNo,
|
||||||
Status: order.FulfillmentStatus,
|
Status: status,
|
||||||
Message: message,
|
Message: message,
|
||||||
ProviderOrderNo: providerOrderNo,
|
ProviderOrderNo: providerOrderNo,
|
||||||
UpstreamOrder: resultData,
|
UpstreamOrder: resultData,
|
||||||
|
|||||||
@@ -216,15 +216,15 @@ func TestDeliveryMerchantApiBindAndSubmit(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("submit for merchant: %v", err)
|
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)
|
t.Fatalf("unexpected submit result: %+v", submitted)
|
||||||
}
|
}
|
||||||
var afterSubmit model.FulfillmentOrder
|
var afterSubmit model.FulfillmentOrder
|
||||||
if err := db.Where("order_no = ?", created.Order.OrderNo).First(&afterSubmit).Error; err != nil {
|
if err := db.Where("order_no = ?", created.Order.OrderNo).First(&afterSubmit).Error; err != nil {
|
||||||
t.Fatalf("reload after submit: %v", err)
|
t.Fatalf("reload after submit: %v", err)
|
||||||
}
|
}
|
||||||
if afterSubmit.FulfillmentStatus != model.FulfillmentStatusProcessing || afterSubmit.ProviderOrderNo != "provider-1" {
|
if afterSubmit.OrderStatus != model.OrderStatusDelivering || afterSubmit.ProviderOrderNo != "provider-1" {
|
||||||
t.Fatalf("submit should persist processing state: %+v", afterSubmit)
|
t.Fatalf("submit should persist delivering state: %+v", afterSubmit)
|
||||||
}
|
}
|
||||||
if atomic.LoadInt32(&queueCreateCalls) != 1 || atomic.LoadInt32(&upstreamCalls) != 1 {
|
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)
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("repeat submit for merchant: %v", err)
|
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)
|
t.Fatalf("repeat submit should reuse existing result: %+v", second)
|
||||||
}
|
}
|
||||||
if atomic.LoadInt32(&queueCreateCalls) != 1 || atomic.LoadInt32(&upstreamCalls) != 1 {
|
if atomic.LoadInt32(&queueCreateCalls) != 1 || atomic.LoadInt32(&upstreamCalls) != 1 {
|
||||||
|
|||||||
@@ -44,12 +44,12 @@ type CreateFulfillmentOrderResult struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CreateTestOrderInput struct {
|
type CreateTestOrderInput struct {
|
||||||
MerchantID uint
|
MerchantID uint
|
||||||
ActorUserID uint
|
ActorUserID uint
|
||||||
SKU string
|
SKU string
|
||||||
BuyerReference string
|
BuyerReference string
|
||||||
Note string
|
Note string
|
||||||
FulfillmentStatus string
|
OrderStatus string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *FulfillmentService) CreateOrder(in CreateFulfillmentOrderInput) (*CreateFulfillmentOrderResult, error) {
|
func (s *FulfillmentService) CreateOrder(in CreateFulfillmentOrderInput) (*CreateFulfillmentOrderResult, error) {
|
||||||
@@ -159,8 +159,7 @@ func (s *FulfillmentService) CreateOrder(in CreateFulfillmentOrderInput) (*Creat
|
|||||||
ServiceFeeAmount: serviceFee,
|
ServiceFeeAmount: serviceFee,
|
||||||
Amount: totalAmount,
|
Amount: totalAmount,
|
||||||
Currency: product.Currency,
|
Currency: product.Currency,
|
||||||
PaymentStatus: model.PaymentStatusPaid,
|
OrderStatus: model.OrderStatusPaid,
|
||||||
FulfillmentStatus: model.FulfillmentStatusPending,
|
|
||||||
BuyerReference: in.BuyerReference,
|
BuyerReference: in.BuyerReference,
|
||||||
RequestFingerprint: fingerprint,
|
RequestFingerprint: fingerprint,
|
||||||
RequestData: requestData,
|
RequestData: requestData,
|
||||||
@@ -236,13 +235,13 @@ func (s *FulfillmentService) CreateTestOrder(in CreateTestOrderInput) (*model.Fu
|
|||||||
if len(in.Note) > 512 {
|
if len(in.Note) > 512 {
|
||||||
return nil, errors.New("备注最长 512 位")
|
return nil, errors.New("备注最长 512 位")
|
||||||
}
|
}
|
||||||
if in.FulfillmentStatus == "" {
|
if in.OrderStatus == "" {
|
||||||
in.FulfillmentStatus = model.FulfillmentStatusPending
|
in.OrderStatus = model.OrderStatusPaid
|
||||||
}
|
}
|
||||||
switch in.FulfillmentStatus {
|
switch in.OrderStatus {
|
||||||
case model.FulfillmentStatusPending, model.FulfillmentStatusFailed:
|
case model.OrderStatusPaid, model.OrderStatusShipFailed:
|
||||||
default:
|
default:
|
||||||
return nil, errors.New("测试订单仅支持待履约或发货失败状态")
|
return nil, errors.New("测试订单仅支持已支付待发货或发货失败状态")
|
||||||
}
|
}
|
||||||
|
|
||||||
requestData := map[string]interface{}{
|
requestData := map[string]interface{}{
|
||||||
@@ -255,7 +254,6 @@ func (s *FulfillmentService) CreateTestOrder(in CreateTestOrderInput) (*model.Fu
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("订单请求数据无法序列化")
|
return nil, errors.New("订单请求数据无法序列化")
|
||||||
}
|
}
|
||||||
|
|
||||||
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 merchant model.Merchant
|
var merchant model.Merchant
|
||||||
@@ -295,21 +293,20 @@ func (s *FulfillmentService) CreateTestOrder(in CreateTestOrderInput) (*model.Fu
|
|||||||
ServiceFeeAmount: 0,
|
ServiceFeeAmount: 0,
|
||||||
Amount: 0,
|
Amount: 0,
|
||||||
Currency: product.Currency,
|
Currency: product.Currency,
|
||||||
PaymentStatus: model.PaymentStatusPaid,
|
OrderStatus: in.OrderStatus,
|
||||||
FulfillmentStatus: in.FulfillmentStatus,
|
|
||||||
BuyerReference: in.BuyerReference,
|
BuyerReference: in.BuyerReference,
|
||||||
RequestData: string(rawRequestData),
|
RequestData: string(rawRequestData),
|
||||||
}
|
}
|
||||||
if in.FulfillmentStatus == model.FulfillmentStatusFailed {
|
if in.OrderStatus == model.OrderStatusShipFailed {
|
||||||
order.FailureReason = fallbackName(in.Note, "联调测试订单初始化为发货失败,可重新发货")
|
order.FailureReason = fallbackName(in.Note, "联调测试订单初始化为发货失败,可重新发货")
|
||||||
}
|
}
|
||||||
if err := tx.Create(order).Error; err != nil {
|
if err := tx.Create(order).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := writeAudit(tx, &in.MerchantID, &in.ActorUserID, nil, "merchant_test_order.create", "fulfillment_order", order.OrderNo, map[string]interface{}{
|
if err := writeAudit(tx, &in.MerchantID, &in.ActorUserID, nil, "merchant_test_order.create", "fulfillment_order", order.OrderNo, map[string]interface{}{
|
||||||
"sku": in.SKU,
|
"sku": in.SKU,
|
||||||
"fulfillment_status": in.FulfillmentStatus,
|
"order_status": in.OrderStatus,
|
||||||
"can_ship": true,
|
"can_ship": true,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -337,11 +334,11 @@ func (s *FulfillmentService) GetOrder(merchantID uint, orderNo string) (*model.F
|
|||||||
return &order, nil
|
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)
|
page, size = normalizePage(page, size)
|
||||||
tx := s.db.Model(&model.FulfillmentOrder{}).Where("merchant_id = ?", merchantID)
|
tx := s.db.Model(&model.FulfillmentOrder{}).Where("merchant_id = ?", merchantID)
|
||||||
if fulfillmentStatus != "" {
|
if orderStatus != "" {
|
||||||
tx = tx.Where("fulfillment_status = ?", fulfillmentStatus)
|
tx = tx.Where("order_status = ?", orderStatus)
|
||||||
}
|
}
|
||||||
var total int64
|
var total int64
|
||||||
if err := tx.Count(&total).Error; err != nil {
|
if err := tx.Count(&total).Error; err != nil {
|
||||||
@@ -364,10 +361,11 @@ type FulfillmentUpdateInput struct {
|
|||||||
|
|
||||||
func (s *FulfillmentService) UpdateFulfillment(in FulfillmentUpdateInput) (*model.FulfillmentOrder, error) {
|
func (s *FulfillmentService) UpdateFulfillment(in FulfillmentUpdateInput) (*model.FulfillmentOrder, error) {
|
||||||
switch in.Status {
|
switch in.Status {
|
||||||
case model.FulfillmentStatusProcessing, model.FulfillmentStatusSucceeded, model.FulfillmentStatusFailed:
|
case model.OrderStatusDelivering, model.OrderStatusDelivered, model.OrderStatusShipFailed:
|
||||||
default:
|
default:
|
||||||
return nil, errors.New("无效的履约状态")
|
return nil, errors.New("无效的订单状态")
|
||||||
}
|
}
|
||||||
|
nextOrderStatus := in.Status
|
||||||
resultData := ""
|
resultData := ""
|
||||||
if in.ResultData != nil {
|
if in.ResultData != nil {
|
||||||
raw, err := json.Marshal(in.ResultData)
|
raw, err := json.Marshal(in.ResultData)
|
||||||
@@ -387,26 +385,24 @@ func (s *FulfillmentService) UpdateFulfillment(in FulfillmentUpdateInput) (*mode
|
|||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if order.FulfillmentStatus == model.FulfillmentStatusSucceeded && in.Status == model.FulfillmentStatusSucceeded {
|
if normalizeOrderStatus(&order) == model.OrderStatusDelivered && nextOrderStatus == model.OrderStatusDelivered {
|
||||||
out = order
|
out = order
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err := validateFulfillmentTransition(&order, in.Status, fulfillmentTransitionUpdate); err != nil {
|
if err := validateOrderStatusTransition(&order, nextOrderStatus, fulfillmentTransitionUpdate); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
updates := map[string]interface{}{
|
updates := map[string]interface{}{"order_status": nextOrderStatus}
|
||||||
"fulfillment_status": in.Status,
|
updates["result_data"] = resultData
|
||||||
"result_data": resultData,
|
|
||||||
}
|
|
||||||
if in.ProviderOrderNo != "" {
|
if in.ProviderOrderNo != "" {
|
||||||
updates["provider_order_no"] = in.ProviderOrderNo
|
updates["provider_order_no"] = in.ProviderOrderNo
|
||||||
}
|
}
|
||||||
switch in.Status {
|
switch nextOrderStatus {
|
||||||
case model.FulfillmentStatusSucceeded:
|
case model.OrderStatusDelivered:
|
||||||
updates["delivered_at"] = now
|
updates["delivered_at"] = now
|
||||||
updates["failure_reason"] = ""
|
updates["failure_reason"] = ""
|
||||||
case model.FulfillmentStatusFailed:
|
case model.OrderStatusShipFailed:
|
||||||
updates["failure_reason"] = in.FailureReason
|
updates["failure_reason"] = in.FailureReason
|
||||||
}
|
}
|
||||||
if err := tx.Model(&order).Updates(updates).Error; err != nil {
|
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 {
|
if err := tx.First(&out, order.ID).Error; err != nil {
|
||||||
return err
|
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
|
return err
|
||||||
}
|
}
|
||||||
if s.callbacks != nil {
|
if s.callbacks != nil {
|
||||||
@@ -442,7 +438,7 @@ func (s *FulfillmentService) MarkProcessingTimeouts(timeout time.Duration, limit
|
|||||||
cutoff := now.Add(-timeout)
|
cutoff := now.Add(-timeout)
|
||||||
var ids []uint
|
var ids []uint
|
||||||
if err := s.db.Model(&model.FulfillmentOrder{}).
|
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").
|
Order("updated_at ASC, id ASC").
|
||||||
Limit(limit).
|
Limit(limit).
|
||||||
Pluck("id", &ids).Error; err != nil {
|
Pluck("id", &ids).Error; err != nil {
|
||||||
@@ -474,15 +470,13 @@ func (s *FulfillmentService) markProcessingTimeout(id uint, timeout time.Duratio
|
|||||||
if !processingTimedOut(&order, timeout, now) {
|
if !processingTimedOut(&order, timeout, now) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err := validateFulfillmentTransition(&order, model.FulfillmentStatusFailed, fulfillmentTransitionTimeout); err != nil {
|
if err := validateOrderStatusTransition(&order, model.OrderStatusShipFailed, fulfillmentTransitionTimeout); err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
reason := fmt.Sprintf("履约超时:订单已处于 processing 超过 %d 分钟", int(timeout.Minutes()))
|
reason := fmt.Sprintf("履约超时:订单已处于 delivering 超过 %d 分钟", int(timeout.Minutes()))
|
||||||
updates := map[string]interface{}{
|
updates := map[string]interface{}{"order_status": model.OrderStatusShipFailed}
|
||||||
"fulfillment_status": model.FulfillmentStatusFailed,
|
updates["failure_reason"] = reason
|
||||||
"failure_reason": reason,
|
updates["result_data"] = buildProcessingTimeoutResultData(order.ResultData, timeout, now)
|
||||||
"result_data": buildProcessingTimeoutResultData(order.ResultData, timeout, 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
|
||||||
}
|
}
|
||||||
@@ -491,8 +485,8 @@ func (s *FulfillmentService) markProcessingTimeout(id uint, timeout time.Duratio
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := writeAudit(tx, &order.MerchantID, nil, nil, "fulfillment.timeout", "fulfillment_order", order.OrderNo, map[string]interface{}{
|
if err := writeAudit(tx, &order.MerchantID, nil, nil, "fulfillment.timeout", "fulfillment_order", order.OrderNo, map[string]interface{}{
|
||||||
"from": order.FulfillmentStatus,
|
"from": normalizeOrderStatus(&order),
|
||||||
"to": model.FulfillmentStatusFailed,
|
"to": model.OrderStatusShipFailed,
|
||||||
"timeout_minutes": int(timeout.Minutes()),
|
"timeout_minutes": int(timeout.Minutes()),
|
||||||
"reason": reason,
|
"reason": reason,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
@@ -545,7 +539,7 @@ func (s *FulfillmentService) CancelOrder(merchantID, apiClientID uint, orderNo,
|
|||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if order.FulfillmentStatus == model.FulfillmentStatusCancelled {
|
if normalizeOrderStatus(&order) == model.OrderStatusCancelled {
|
||||||
out = order
|
out = order
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -553,12 +547,9 @@ func (s *FulfillmentService) CancelOrder(merchantID, apiClientID uint, orderNo,
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
updates := map[string]interface{}{
|
updates := map[string]interface{}{"order_status": model.OrderStatusCancelled}
|
||||||
"payment_status": model.PaymentStatusRefunded,
|
updates["failure_reason"] = reason
|
||||||
"fulfillment_status": model.FulfillmentStatusCancelled,
|
updates["cancelled_at"] = now
|
||||||
"failure_reason": reason,
|
|
||||||
"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
|
||||||
}
|
}
|
||||||
@@ -739,20 +730,19 @@ func calculateServiceFee(baseAmount int64, feeType string, feeRateBP, feeFixedAm
|
|||||||
}
|
}
|
||||||
|
|
||||||
func CanFulfill(order *model.FulfillmentOrder) (bool, string) {
|
func CanFulfill(order *model.FulfillmentOrder) (bool, string) {
|
||||||
if order.PaymentStatus != model.PaymentStatusPaid {
|
switch normalizeOrderStatus(order) {
|
||||||
return false, "订单未支付或已退款"
|
case model.OrderStatusPaid, model.OrderStatusShipFailed:
|
||||||
}
|
|
||||||
switch order.FulfillmentStatus {
|
|
||||||
case model.FulfillmentStatusPending, model.FulfillmentStatusFailed:
|
|
||||||
return true, ""
|
return true, ""
|
||||||
case model.FulfillmentStatusProcessing:
|
case model.OrderStatusPending:
|
||||||
return false, "订单履约中"
|
return false, "订单未支付"
|
||||||
case model.FulfillmentStatusSucceeded:
|
case model.OrderStatusDelivering:
|
||||||
return false, "订单已履约成功"
|
return false, "订单发货中"
|
||||||
case model.FulfillmentStatusCancelled:
|
case model.OrderStatusDelivered:
|
||||||
|
return false, "订单已交付"
|
||||||
|
case model.OrderStatusCancelled:
|
||||||
return false, "订单已取消"
|
return false, "订单已取消"
|
||||||
default:
|
default:
|
||||||
return false, "订单状态不可履约"
|
return false, "订单状态不可发货"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -768,8 +758,7 @@ func orderCallbackData(order *model.FulfillmentOrder) map[string]interface{} {
|
|||||||
"service_fee_amount": order.ServiceFeeAmount,
|
"service_fee_amount": order.ServiceFeeAmount,
|
||||||
"amount": order.Amount,
|
"amount": order.Amount,
|
||||||
"currency": order.Currency,
|
"currency": order.Currency,
|
||||||
"payment_status": order.PaymentStatus,
|
"order_status": normalizeOrderStatus(order),
|
||||||
"fulfillment_status": order.FulfillmentStatus,
|
|
||||||
"can_fulfill": canFulfill,
|
"can_fulfill": canFulfill,
|
||||||
"cannot_fulfill_reason": cannotFulfillReason,
|
"cannot_fulfill_reason": cannotFulfillReason,
|
||||||
"provider_order_no": order.ProviderOrderNo,
|
"provider_order_no": order.ProviderOrderNo,
|
||||||
@@ -795,9 +784,10 @@ type DashboardStats struct {
|
|||||||
TotalFees int64 `json:"total_fees"`
|
TotalFees int64 `json:"total_fees"`
|
||||||
TodayFees int64 `json:"today_fees"`
|
TodayFees int64 `json:"today_fees"`
|
||||||
PendingOrderCount int64 `json:"pending_order_count"`
|
PendingOrderCount int64 `json:"pending_order_count"`
|
||||||
ProcessingOrderCount int64 `json:"processing_order_count"`
|
PaidOrderCount int64 `json:"paid_order_count"`
|
||||||
SucceededOrderCount int64 `json:"succeeded_order_count"`
|
DeliveringOrderCount int64 `json:"delivering_order_count"`
|
||||||
FailedOrderCount int64 `json:"failed_order_count"`
|
DeliveredOrderCount int64 `json:"delivered_order_count"`
|
||||||
|
ShipFailedOrderCount int64 `json:"ship_failed_order_count"`
|
||||||
CancelledOrderCount int64 `json:"cancelled_order_count"`
|
CancelledOrderCount int64 `json:"cancelled_order_count"`
|
||||||
WalletAvailableBalance int64 `json:"wallet_available_balance"`
|
WalletAvailableBalance int64 `json:"wallet_available_balance"`
|
||||||
WalletFrozenBalance int64 `json:"wallet_frozen_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 {
|
if err := orderScope().Where("created_at >= ?", todayStart).Count(&stats.TodayOrderCount).Error; err != nil {
|
||||||
return nil, err
|
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 {
|
Select("COALESCE(SUM(amount),0)").Scan(&stats.TotalSales).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := orderScope().Where("payment_status = ?", model.PaymentStatusPaid).
|
if err := orderScope().Where("order_status <> ?", model.OrderStatusCancelled).
|
||||||
Where("created_at >= ?", todayStart).
|
Where("created_at >= ?", todayStart).
|
||||||
Select("COALESCE(SUM(amount),0)").Scan(&stats.TodaySales).Error; err != nil {
|
Select("COALESCE(SUM(amount),0)").Scan(&stats.TodaySales).Error; err != nil {
|
||||||
return nil, err
|
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 {
|
Select("COALESCE(SUM(service_fee_amount),0)").Scan(&stats.TotalFees).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := orderScope().Where("payment_status = ?", model.PaymentStatusPaid).
|
if err := orderScope().Where("order_status <> ?", model.OrderStatusCancelled).
|
||||||
Where("created_at >= ?", todayStart).
|
Where("created_at >= ?", todayStart).
|
||||||
Select("COALESCE(SUM(service_fee_amount),0)").Scan(&stats.TodayFees).Error; err != nil {
|
Select("COALESCE(SUM(service_fee_amount),0)").Scan(&stats.TodayFees).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
var orderStatusCounts []dashboardStatusCount
|
var orderStatusCounts []dashboardStatusCount
|
||||||
if err := orderScope().Select("fulfillment_status AS status, COUNT(*) AS count").
|
if err := orderScope().Select("order_status AS status, COUNT(*) AS count").
|
||||||
Group("fulfillment_status").Scan(&orderStatusCounts).Error; err != nil {
|
Group("order_status").Scan(&orderStatusCounts).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
for _, item := range orderStatusCounts {
|
for _, item := range orderStatusCounts {
|
||||||
switch item.Status {
|
switch item.Status {
|
||||||
case model.FulfillmentStatusPending:
|
case model.OrderStatusPending:
|
||||||
stats.PendingOrderCount = item.Count
|
stats.PendingOrderCount = item.Count
|
||||||
case model.FulfillmentStatusProcessing:
|
case model.OrderStatusPaid:
|
||||||
stats.ProcessingOrderCount = item.Count
|
stats.PaidOrderCount = item.Count
|
||||||
case model.FulfillmentStatusSucceeded:
|
case model.OrderStatusDelivering:
|
||||||
stats.SucceededOrderCount = item.Count
|
stats.DeliveringOrderCount = item.Count
|
||||||
case model.FulfillmentStatusFailed:
|
case model.OrderStatusDelivered:
|
||||||
stats.FailedOrderCount = item.Count
|
stats.DeliveredOrderCount = item.Count
|
||||||
case model.FulfillmentStatusCancelled:
|
case model.OrderStatusShipFailed:
|
||||||
|
stats.ShipFailedOrderCount = item.Count
|
||||||
|
case model.OrderStatusCancelled:
|
||||||
stats.CancelledOrderCount = item.Count
|
stats.CancelledOrderCount = item.Count
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1039,7 +1031,7 @@ func (s *FulfillmentService) QueryOpenOrder(orderNo string) (*OpenOrderQuery, er
|
|||||||
canShip, reason := CanFulfill(order)
|
canShip, reason := CanFulfill(order)
|
||||||
out := &OpenOrderQuery{
|
out := &OpenOrderQuery{
|
||||||
OrderNo: order.OrderNo,
|
OrderNo: order.OrderNo,
|
||||||
Status: fulfillmentStatusToLegacyStatus(order.FulfillmentStatus),
|
Status: normalizeOrderStatus(order),
|
||||||
CanShip: canShip,
|
CanShip: canShip,
|
||||||
CannotShipReason: reason,
|
CannotShipReason: reason,
|
||||||
BuyerName: order.BuyerReference,
|
BuyerName: order.BuyerReference,
|
||||||
@@ -1086,9 +1078,9 @@ func (s *FulfillmentService) HandleShipNotify(in ShipNotifyInput) (*ShipNotifyRe
|
|||||||
var nextStatus string
|
var nextStatus string
|
||||||
switch in.ShipStatus {
|
switch in.ShipStatus {
|
||||||
case "success":
|
case "success":
|
||||||
nextStatus = model.FulfillmentStatusSucceeded
|
nextStatus = model.OrderStatusDelivered
|
||||||
case "failed":
|
case "failed":
|
||||||
nextStatus = model.FulfillmentStatusFailed
|
nextStatus = model.OrderStatusShipFailed
|
||||||
}
|
}
|
||||||
|
|
||||||
var result ShipNotifyResult
|
var result ShipNotifyResult
|
||||||
@@ -1106,17 +1098,17 @@ func (s *FulfillmentService) HandleShipNotify(in ShipNotifyInput) (*ShipNotifyRe
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 已履约成功:success 推送幂等成功。状态读取和后续更新必须在同一把行锁内完成。
|
// 已履约成功:success 推送幂等成功。状态读取和后续更新必须在同一把行锁内完成。
|
||||||
if order.FulfillmentStatus == model.FulfillmentStatusSucceeded && in.ShipStatus == "success" {
|
if normalizeOrderStatus(&order) == model.OrderStatusDelivered && in.ShipStatus == "success" {
|
||||||
result = ShipNotifyResult{
|
result = ShipNotifyResult{
|
||||||
OrderNo: order.OrderNo,
|
OrderNo: order.OrderNo,
|
||||||
Status: fulfillmentStatusToLegacyStatus(order.FulfillmentStatus),
|
Status: normalizeOrderStatus(&order),
|
||||||
Message: "订单已交付,幂等成功",
|
Message: "订单已交付,幂等成功",
|
||||||
}
|
}
|
||||||
return writeAudit(tx, &order.MerchantID, nil, nil, "ship.notify", "fulfillment_order", order.OrderNo,
|
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 {
|
if err := writeShipNotifyRejectedAudit(tx, &order, in, "订单已取消,拒绝更新"); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1131,14 +1123,14 @@ func (s *FulfillmentService) HandleShipNotify(in ShipNotifyInput) (*ShipNotifyRe
|
|||||||
}
|
}
|
||||||
|
|
||||||
updates := map[string]interface{}{
|
updates := map[string]interface{}{
|
||||||
"fulfillment_status": nextStatus,
|
"order_status": nextStatus,
|
||||||
}
|
}
|
||||||
var message string
|
var message string
|
||||||
switch in.ShipStatus {
|
switch in.ShipStatus {
|
||||||
case "success":
|
case "success":
|
||||||
if err := validateFulfillmentTransition(&order, nextStatus, fulfillmentTransitionShipNotify); err != nil {
|
if err := validateOrderStatusTransition(&order, nextStatus, fulfillmentTransitionShipNotify); err != nil {
|
||||||
message := "当前状态不允许标记发货成功"
|
message := "当前状态不允许标记发货成功"
|
||||||
if order.PaymentStatus != model.PaymentStatusPaid {
|
if normalizeOrderStatus(&order) == model.OrderStatusPending {
|
||||||
message = "订单未支付,拒绝成功推送"
|
message = "订单未支付,拒绝成功推送"
|
||||||
}
|
}
|
||||||
if err := writeShipNotifyRejectedAudit(tx, &order, in, message); err != nil {
|
if err := writeShipNotifyRejectedAudit(tx, &order, in, message); err != nil {
|
||||||
@@ -1154,11 +1146,11 @@ func (s *FulfillmentService) HandleShipNotify(in ShipNotifyInput) (*ShipNotifyRe
|
|||||||
}
|
}
|
||||||
message = "发货成功,订单已交付"
|
message = "发货成功,订单已交付"
|
||||||
case "failed":
|
case "failed":
|
||||||
if err := validateFulfillmentTransition(&order, nextStatus, fulfillmentTransitionShipNotify); err != nil {
|
if err := validateOrderStatusTransition(&order, nextStatus, fulfillmentTransitionShipNotify); err != nil {
|
||||||
message := "当前状态不允许标记发货失败"
|
message := "当前状态不允许标记发货失败"
|
||||||
if order.FulfillmentStatus == model.FulfillmentStatusSucceeded {
|
if normalizeOrderStatus(&order) == model.OrderStatusDelivered {
|
||||||
message = "订单已交付,拒绝失败推送"
|
message = "订单已交付,拒绝失败推送"
|
||||||
} else if order.PaymentStatus != model.PaymentStatusPaid {
|
} else if normalizeOrderStatus(&order) == model.OrderStatusPending {
|
||||||
message = "订单未支付,拒绝失败推送"
|
message = "订单未支付,拒绝失败推送"
|
||||||
}
|
}
|
||||||
if err := writeShipNotifyRejectedAudit(tx, &order, in, message); err != nil {
|
if err := writeShipNotifyRejectedAudit(tx, &order, in, message); err != nil {
|
||||||
@@ -1196,7 +1188,7 @@ func (s *FulfillmentService) HandleShipNotify(in ShipNotifyInput) (*ShipNotifyRe
|
|||||||
}
|
}
|
||||||
result = ShipNotifyResult{
|
result = ShipNotifyResult{
|
||||||
OrderNo: order.OrderNo,
|
OrderNo: order.OrderNo,
|
||||||
Status: fulfillmentStatusToLegacyStatus(nextStatus),
|
Status: nextStatus,
|
||||||
Message: message,
|
Message: message,
|
||||||
}
|
}
|
||||||
return nil
|
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 {
|
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,
|
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{} {
|
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_minutes"] = int(timeout.Minutes())
|
||||||
m["timeout_at"] = timeutil.FormatAPITime(now)
|
m["timeout_at"] = timeutil.FormatAPITime(now)
|
||||||
m["ship_status"] = "failed"
|
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)
|
raw, err := json.Marshal(m)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return existing
|
return existing
|
||||||
@@ -1323,21 +1315,3 @@ func toInt(v interface{}) (int, bool) {
|
|||||||
}
|
}
|
||||||
return 0, false
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -63,60 +63,77 @@ func ensureSameIdempotentOrder(existing *model.FulfillmentOrder, fingerprint str
|
|||||||
return nil
|
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 {
|
if order == nil {
|
||||||
return errors.New("订单不存在")
|
return errors.New("订单不存在")
|
||||||
}
|
}
|
||||||
if order.PaymentStatus != model.PaymentStatusPaid {
|
current := normalizeOrderStatus(order)
|
||||||
return errors.New("订单未支付,不能履约")
|
if current == next {
|
||||||
}
|
|
||||||
if order.FulfillmentStatus == next {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
switch order.FulfillmentStatus {
|
switch current {
|
||||||
case model.FulfillmentStatusCancelled:
|
case model.OrderStatusPending:
|
||||||
return errors.New("订单已取消,不能更新履约状态")
|
if next == model.OrderStatusPaid || next == model.OrderStatusCancelled {
|
||||||
case model.FulfillmentStatusSucceeded:
|
return nil
|
||||||
|
}
|
||||||
|
return errors.New("订单未支付,不能履约")
|
||||||
|
case model.OrderStatusCancelled:
|
||||||
|
return errors.New("订单已取消,不能更新状态")
|
||||||
|
case model.OrderStatusDelivered:
|
||||||
return errors.New("订单已履约成功,不能回退状态")
|
return errors.New("订单已履约成功,不能回退状态")
|
||||||
}
|
}
|
||||||
switch next {
|
switch next {
|
||||||
case model.FulfillmentStatusProcessing:
|
case model.OrderStatusDelivering:
|
||||||
if order.FulfillmentStatus == model.FulfillmentStatusPending || order.FulfillmentStatus == model.FulfillmentStatusFailed {
|
if current == model.OrderStatusPaid || current == model.OrderStatusShipFailed {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
case model.FulfillmentStatusSucceeded:
|
case model.OrderStatusDelivered:
|
||||||
if order.FulfillmentStatus == model.FulfillmentStatusPending ||
|
if current == model.OrderStatusPaid ||
|
||||||
order.FulfillmentStatus == model.FulfillmentStatusFailed ||
|
current == model.OrderStatusShipFailed ||
|
||||||
order.FulfillmentStatus == model.FulfillmentStatusProcessing {
|
current == model.OrderStatusDelivering {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
case model.FulfillmentStatusFailed:
|
case model.OrderStatusShipFailed:
|
||||||
if kind == fulfillmentTransitionTimeout && order.FulfillmentStatus != model.FulfillmentStatusProcessing {
|
if kind == fulfillmentTransitionTimeout && current != model.OrderStatusDelivering {
|
||||||
return errors.New("只有履约中的订单可以标记超时")
|
return errors.New("只有履约中的订单可以标记超时")
|
||||||
}
|
}
|
||||||
if order.FulfillmentStatus == model.FulfillmentStatusPending ||
|
if current == model.OrderStatusPaid ||
|
||||||
order.FulfillmentStatus == model.FulfillmentStatusFailed ||
|
current == model.OrderStatusShipFailed ||
|
||||||
order.FulfillmentStatus == model.FulfillmentStatusProcessing {
|
current == model.OrderStatusDelivering {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
case model.OrderStatusCancelled:
|
||||||
|
if current == model.OrderStatusPaid || current == model.OrderStatusShipFailed {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return errors.New("订单当前状态不允许该履约变更")
|
return errors.New("订单当前状态不允许该状态变更")
|
||||||
}
|
}
|
||||||
|
|
||||||
func canCancelOrder(order *model.FulfillmentOrder) error {
|
func canCancelOrder(order *model.FulfillmentOrder) error {
|
||||||
if order == nil {
|
if order == nil {
|
||||||
return errors.New("订单不存在")
|
return errors.New("订单不存在")
|
||||||
}
|
}
|
||||||
if order.FulfillmentStatus == model.FulfillmentStatusCancelled {
|
status := normalizeOrderStatus(order)
|
||||||
|
if status == model.OrderStatusCancelled {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if order.PaymentStatus != model.PaymentStatusPaid {
|
switch status {
|
||||||
return errors.New("订单未支付或已退款,不能取消")
|
case model.OrderStatusPaid, model.OrderStatusShipFailed:
|
||||||
}
|
|
||||||
switch order.FulfillmentStatus {
|
|
||||||
case model.FulfillmentStatusPending, model.FulfillmentStatusFailed:
|
|
||||||
return nil
|
return nil
|
||||||
case model.FulfillmentStatusProcessing, model.FulfillmentStatusSucceeded:
|
case model.OrderStatusPending:
|
||||||
|
return errors.New("订单未支付,不能取消")
|
||||||
|
case model.OrderStatusDelivering, model.OrderStatusDelivered:
|
||||||
return errors.New("订单已进入履约流程,不能取消")
|
return errors.New("订单已进入履约流程,不能取消")
|
||||||
default:
|
default:
|
||||||
return errors.New("订单当前状态不能取消")
|
return errors.New("订单当前状态不能取消")
|
||||||
@@ -127,5 +144,5 @@ func processingTimedOut(order *model.FulfillmentOrder, timeout time.Duration, no
|
|||||||
if order == nil || timeout <= 0 {
|
if order == nil || timeout <= 0 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return order.FulfillmentStatus == model.FulfillmentStatusProcessing && order.UpdatedAt.Before(now.Add(-timeout))
|
return normalizeOrderStatus(order) == model.OrderStatusDelivering && order.UpdatedAt.Before(now.Add(-timeout))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ func TestFulfillmentCreateOrderDebitsWalletAndIsIdempotent(t *testing.T) {
|
|||||||
if first.Idempotent {
|
if first.Idempotent {
|
||||||
t.Fatalf("first create should not be 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)
|
t.Fatalf("unexpected order: %+v", first.Order)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,7 +202,7 @@ func TestDashboardScopesPlatformAndMerchantData(t *testing.T) {
|
|||||||
if merchantStats.ProductCount != 1 || merchantStats.ActiveProductCount != 1 {
|
if merchantStats.ProductCount != 1 || merchantStats.ActiveProductCount != 1 {
|
||||||
t.Fatalf("merchant product stats should only include current merchant, got %+v", merchantStats)
|
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)
|
t.Fatalf("merchant order stats should only include current merchant, got %+v", merchantStats)
|
||||||
}
|
}
|
||||||
if merchantStats.WalletAvailableBalance != 900 {
|
if merchantStats.WalletAvailableBalance != 900 {
|
||||||
@@ -216,7 +216,7 @@ func TestDashboardScopesPlatformAndMerchantData(t *testing.T) {
|
|||||||
if platformStats.Scope != "platform" {
|
if platformStats.Scope != "platform" {
|
||||||
t.Fatalf("expected platform scope, got %s", platformStats.Scope)
|
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)
|
t.Fatalf("platform order stats should include all merchants, got %+v", platformStats)
|
||||||
}
|
}
|
||||||
if platformStats.WalletAvailableBalance < 2600 {
|
if platformStats.WalletAvailableBalance < 2600 {
|
||||||
@@ -319,7 +319,7 @@ func TestFulfillmentCancelRefundsOnceAndRestoresStock(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("cancel order: %v", err)
|
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)
|
t.Fatalf("unexpected cancelled order: %+v", cancelled)
|
||||||
}
|
}
|
||||||
if _, err := svc.CancelOrder(merchantID, 12, created.Order.OrderNo, "重复取消"); err != nil {
|
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)
|
db := newServiceTestDB(t)
|
||||||
merchantID, product := seedFulfillmentMerchant(t, db, "merchant-c", 1000, -1, 100)
|
merchantID, product := seedFulfillmentMerchant(t, db, "merchant-c", 1000, -1, 100)
|
||||||
svc := NewFulfillmentService(db, nil)
|
svc := NewFulfillmentService(db, nil)
|
||||||
@@ -356,37 +356,37 @@ func TestFulfillmentStatusTransitions(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("create order: %v", err)
|
t.Fatalf("create order: %v", err)
|
||||||
}
|
}
|
||||||
processing, err := svc.UpdateFulfillment(FulfillmentUpdateInput{
|
delivering, err := svc.UpdateFulfillment(FulfillmentUpdateInput{
|
||||||
MerchantID: merchantID,
|
MerchantID: merchantID,
|
||||||
APIClientID: 13,
|
APIClientID: 13,
|
||||||
OrderNo: created.Order.OrderNo,
|
OrderNo: created.Order.OrderNo,
|
||||||
Status: model.FulfillmentStatusProcessing,
|
Status: model.OrderStatusDelivering,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("mark processing: %v", err)
|
t.Fatalf("mark delivering: %v", err)
|
||||||
}
|
}
|
||||||
if processing.FulfillmentStatus != model.FulfillmentStatusProcessing {
|
if delivering.OrderStatus != model.OrderStatusDelivering {
|
||||||
t.Fatalf("expected processing, got %s", processing.FulfillmentStatus)
|
t.Fatalf("expected delivering, got %s", delivering.OrderStatus)
|
||||||
}
|
}
|
||||||
succeeded, err := svc.UpdateFulfillment(FulfillmentUpdateInput{
|
delivered, err := svc.UpdateFulfillment(FulfillmentUpdateInput{
|
||||||
MerchantID: merchantID,
|
MerchantID: merchantID,
|
||||||
APIClientID: 13,
|
APIClientID: 13,
|
||||||
OrderNo: created.Order.OrderNo,
|
OrderNo: created.Order.OrderNo,
|
||||||
Status: model.FulfillmentStatusSucceeded,
|
Status: model.OrderStatusDelivered,
|
||||||
ProviderOrderNo: "provider-1",
|
ProviderOrderNo: "provider-1",
|
||||||
ResultData: map[string]string{"ok": "true"},
|
ResultData: map[string]string{"ok": "true"},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("mark succeeded: %v", err)
|
t.Fatalf("mark delivered: %v", err)
|
||||||
}
|
}
|
||||||
if succeeded.FulfillmentStatus != model.FulfillmentStatusSucceeded || succeeded.DeliveredAt == nil {
|
if delivered.OrderStatus != model.OrderStatusDelivered || delivered.DeliveredAt == nil {
|
||||||
t.Fatalf("unexpected succeeded order: %+v", succeeded)
|
t.Fatalf("unexpected delivered order: %+v", delivered)
|
||||||
}
|
}
|
||||||
_, err = svc.UpdateFulfillment(FulfillmentUpdateInput{
|
_, err = svc.UpdateFulfillment(FulfillmentUpdateInput{
|
||||||
MerchantID: merchantID,
|
MerchantID: merchantID,
|
||||||
APIClientID: 13,
|
APIClientID: 13,
|
||||||
OrderNo: created.Order.OrderNo,
|
OrderNo: created.Order.OrderNo,
|
||||||
Status: model.FulfillmentStatusFailed,
|
Status: model.OrderStatusShipFailed,
|
||||||
})
|
})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("should reject rollback after success")
|
t.Fatalf("should reject rollback after success")
|
||||||
@@ -406,19 +406,19 @@ func TestMarkProcessingTimeoutsMarksStaleOrdersFailed(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("create order: %v", err)
|
t.Fatalf("create order: %v", err)
|
||||||
}
|
}
|
||||||
processing, err := svc.UpdateFulfillment(FulfillmentUpdateInput{
|
delivering, err := svc.UpdateFulfillment(FulfillmentUpdateInput{
|
||||||
MerchantID: merchantID,
|
MerchantID: merchantID,
|
||||||
APIClientID: 13,
|
APIClientID: 13,
|
||||||
OrderNo: created.Order.OrderNo,
|
OrderNo: created.Order.OrderNo,
|
||||||
Status: model.FulfillmentStatusProcessing,
|
Status: model.OrderStatusDelivering,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("mark processing: %v", err)
|
t.Fatalf("mark delivering: %v", err)
|
||||||
}
|
}
|
||||||
if err := db.Model(&model.FulfillmentOrder{}).
|
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 {
|
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)
|
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)
|
t.Fatalf("expected one timed out order, got %d", changed)
|
||||||
}
|
}
|
||||||
var order model.FulfillmentOrder
|
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)
|
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)
|
t.Fatalf("expected failed timeout order, got %+v", order)
|
||||||
}
|
}
|
||||||
if !strings.Contains(order.ResultData, `"timeout":true`) {
|
if !strings.Contains(order.ResultData, `"timeout":true`) {
|
||||||
@@ -453,28 +453,28 @@ func TestMarkProcessingTimeoutsSkipsRecentOrders(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("create order: %v", err)
|
t.Fatalf("create order: %v", err)
|
||||||
}
|
}
|
||||||
processing, err := svc.UpdateFulfillment(FulfillmentUpdateInput{
|
delivering, err := svc.UpdateFulfillment(FulfillmentUpdateInput{
|
||||||
MerchantID: merchantID,
|
MerchantID: merchantID,
|
||||||
APIClientID: 13,
|
APIClientID: 13,
|
||||||
OrderNo: created.Order.OrderNo,
|
OrderNo: created.Order.OrderNo,
|
||||||
Status: model.FulfillmentStatusProcessing,
|
Status: model.OrderStatusDelivering,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("mark processing: %v", err)
|
t.Fatalf("mark delivering: %v", err)
|
||||||
}
|
}
|
||||||
changed, err := svc.MarkProcessingTimeouts(30*time.Minute, 10)
|
changed, err := svc.MarkProcessingTimeouts(30*time.Minute, 10)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("mark timeouts: %v", err)
|
t.Fatalf("mark timeouts: %v", err)
|
||||||
}
|
}
|
||||||
if changed != 0 {
|
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
|
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)
|
t.Fatalf("query order: %v", err)
|
||||||
}
|
}
|
||||||
if order.FulfillmentStatus != model.FulfillmentStatusProcessing {
|
if order.OrderStatus != model.OrderStatusDelivering {
|
||||||
t.Fatalf("expected processing order, got %+v", order)
|
t.Fatalf("expected delivering order, got %+v", order)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -534,18 +534,16 @@ func TestWalletAdjustIsIdempotentPerMerchant(t *testing.T) {
|
|||||||
|
|
||||||
func TestCanFulfill(t *testing.T) {
|
func TestCanFulfill(t *testing.T) {
|
||||||
ok, reason := CanFulfill(&model.FulfillmentOrder{
|
ok, reason := CanFulfill(&model.FulfillmentOrder{
|
||||||
PaymentStatus: model.PaymentStatusPaid,
|
OrderStatus: model.OrderStatusShipFailed,
|
||||||
FulfillmentStatus: model.FulfillmentStatusFailed,
|
|
||||||
})
|
})
|
||||||
if !ok || reason != "" {
|
if !ok || reason != "" {
|
||||||
t.Fatalf("failed paid order should be fulfillable")
|
t.Fatalf("failed paid order should be fulfillable")
|
||||||
}
|
}
|
||||||
ok, _ = CanFulfill(&model.FulfillmentOrder{
|
ok, _ = CanFulfill(&model.FulfillmentOrder{
|
||||||
PaymentStatus: model.PaymentStatusRefunded,
|
OrderStatus: model.OrderStatusCancelled,
|
||||||
FulfillmentStatus: model.FulfillmentStatusPending,
|
|
||||||
})
|
})
|
||||||
if ok {
|
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 {
|
if !strings.HasPrefix(order.OrderNo, "O") || order.ClientOrderNo != "TEST-"+order.OrderNo {
|
||||||
t.Fatalf("unexpected test order numbers: %+v", order)
|
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)
|
t.Fatalf("unexpected test order status: %+v", order)
|
||||||
}
|
}
|
||||||
canShip, reason := CanFulfill(order)
|
canShip, reason := CanFulfill(order)
|
||||||
@@ -631,15 +629,15 @@ func TestCreateTestOrderSupportsFailedFulfillableStatus(t *testing.T) {
|
|||||||
svc := NewFulfillmentService(db, nil)
|
svc := NewFulfillmentService(db, nil)
|
||||||
|
|
||||||
order, err := svc.CreateTestOrder(CreateTestOrderInput{
|
order, err := svc.CreateTestOrder(CreateTestOrderInput{
|
||||||
MerchantID: merchantID,
|
MerchantID: merchantID,
|
||||||
SKU: product.SKU,
|
SKU: product.SKU,
|
||||||
FulfillmentStatus: model.FulfillmentStatusFailed,
|
OrderStatus: model.OrderStatusShipFailed,
|
||||||
Note: "等待重新发货",
|
Note: "等待重新发货",
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("create failed test order: %v", err)
|
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)
|
t.Fatalf("unexpected failed test order: %+v", order)
|
||||||
}
|
}
|
||||||
openOrder, err := svc.QueryOpenOrder(order.OrderNo)
|
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 {
|
if err := db.First(&order, created.Order.ID).Error; err != nil {
|
||||||
t.Fatalf("query order: %v", err)
|
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)
|
t.Fatalf("unexpected order after source notify: %+v", order)
|
||||||
}
|
}
|
||||||
var delivery model.CallbackDelivery
|
var delivery model.CallbackDelivery
|
||||||
@@ -771,7 +769,7 @@ func TestHandleShipNotifySuccessClearsPreviousResultFailureReason(t *testing.T)
|
|||||||
if err := db.First(&order, created.Order.ID).Error; err != nil {
|
if err := db.First(&order, created.Order.ID).Error; err != nil {
|
||||||
t.Fatalf("query order: %v", err)
|
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)
|
t.Fatalf("success should clear order failure reason, got %+v", order)
|
||||||
}
|
}
|
||||||
if strings.Contains(order.ResultData, "fail_reason") {
|
if strings.Contains(order.ResultData, "fail_reason") {
|
||||||
|
|||||||
+11
-12
@@ -28,12 +28,12 @@
|
|||||||
↓ POST /api/open/v1/orders/ship-notify
|
↓ POST /api/open/v1/orders/ship-notify
|
||||||
只回传 success 或 failed
|
只回传 success 或 failed
|
||||||
↓
|
↓
|
||||||
本平台更新履约状态
|
本平台更新订单状态
|
||||||
↓
|
↓
|
||||||
商户回调 / 商户查询订单
|
商户回调 / 商户查询订单
|
||||||
```
|
```
|
||||||
|
|
||||||
当前流程不再维护独立的 `fulfillment_jobs` 任务表;履约事实以订单表里的 `fulfillment_status`、上游单号、发货时间和失败原因等字段为准。
|
当前流程不再维护独立的 `fulfillment_jobs` 任务表;订单事实以订单表里的 `order_status`、上游单号、发货时间和失败原因等字段为准。
|
||||||
|
|
||||||
## 3. 鉴权不要混用
|
## 3. 鉴权不要混用
|
||||||
|
|
||||||
@@ -51,17 +51,16 @@
|
|||||||
|
|
||||||
## 4. 状态语言
|
## 4. 状态语言
|
||||||
|
|
||||||
内部订单使用 `payment_status` + `fulfillment_status` 两组状态;源头侧为了兼容对接,返回的是更贴近发货系统的旧状态名。
|
平台内部、商户侧开放 API、源头侧查询和商户回调统一使用 `order_status`。
|
||||||
|
|
||||||
| 内部 payment_status | 内部 fulfillment_status | 源头侧 `status` | 含义 | 是否可发货 |
|
| order_status | 含义 | 是否可发货 |
|
||||||
|---------------------|-------------------------|-----------------|------|------------|
|
|--------------|------|------------|
|
||||||
| `paid` | `pending` | `paid` | 已付款,等待发货 | 是 |
|
| `pending` | 待支付(当前无真实支付流程,仅预留) | 否 |
|
||||||
| `paid` | `processing` | `delivering` | 履约中 | 否 |
|
| `paid` | 已支付/已扣款,待发货 | 是 |
|
||||||
| `paid` | `succeeded` | `delivered` | 已交付 | 否 |
|
| `delivering` | 发货中 | 否 |
|
||||||
| `paid` | `failed` | `ship_failed` | 发货失败,可重试 | 是 |
|
| `delivered` | 已交付 | 否 |
|
||||||
| `refunded` | `cancelled` | `cancelled` | 已取消 / 已退款 | 否 |
|
| `ship_failed` | 发货失败,可重试 | 是 |
|
||||||
|
| `cancelled` | 已取消 / 已退款 | 否 |
|
||||||
注意:源头侧返回的 `status=paid` 不是单独的支付状态,而是“这个订单已支付且处于待发货履约状态”的兼容表达。
|
|
||||||
|
|
||||||
## 5. ship_notify 当前契约
|
## 5. ship_notify 当前契约
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -11,7 +11,7 @@
|
|||||||
- `success` 表示发货成功
|
- `success` 表示发货成功
|
||||||
- `failed` 表示发货失败
|
- `failed` 表示发货失败
|
||||||
- `failed` 时,`fail_reason` 必填,且要写详细失败原因
|
- `failed` 时,`fail_reason` 必填,且要写详细失败原因
|
||||||
- `processing` 是旧口径里曾出现过的中间状态,当前 `ship_notify` 不再接受
|
- `ship_notify` 不接受中间态,提交发货时由平台内部将订单置为 `delivering`
|
||||||
|
|
||||||
## 2. 推荐推送格式
|
## 2. 推荐推送格式
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -216,7 +216,7 @@ GET /api/open/v1/orders/{order_no}
|
|||||||
|
|
||||||
## 4. 接口二:发货结果推送
|
## 4. 接口二:发货结果推送
|
||||||
|
|
||||||
> 当前接口只接受 `success` / `failed` 两种最终结果。`processing` 是旧口径里曾出现过的中间状态,当前 `ship_notify` 不再接受;当 `ship_status=failed` 时,`fail_reason` 必须填写详细失败原因。
|
> 当前接口只接受 `success` / `failed` 两种最终结果;当 `ship_status=failed` 时,`fail_reason` 必须填写详细失败原因。
|
||||||
|
|
||||||
### 4.1 请求
|
### 4.1 请求
|
||||||
|
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export const merchantApi = {
|
|||||||
sku: string
|
sku: string
|
||||||
buyer_reference?: string
|
buyer_reference?: string
|
||||||
note?: 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),
|
}) => request.post('/merchant/orders/test', data).then((r) => r.data.data as CreateTestOrderResult),
|
||||||
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),
|
||||||
|
|||||||
@@ -16,28 +16,21 @@ export const errorCodes: ParamSpec[] = [
|
|||||||
{ name: '500', type: 'int', desc: '服务端内部错误' },
|
{ name: '500', type: 'int', desc: '服务端内部错误' },
|
||||||
]
|
]
|
||||||
|
|
||||||
// 订单状态与可履约说明(payment_status / fulfillment_status)。
|
// 订单状态与可履约说明。
|
||||||
export const orderStatusTable: ParamSpec[] = [
|
export const orderStatusTable: ParamSpec[] = [
|
||||||
{ name: 'pending', type: 'fulfillment_status', desc: '待履约,可被履约器接单', example: 'can_fulfill=true' },
|
{ name: 'pending', type: 'order_status', desc: '待支付(当前无真实支付流程,仅预留)', example: 'can_fulfill=false' },
|
||||||
{ name: 'processing', type: 'fulfillment_status', desc: '履约中', example: 'can_fulfill=false' },
|
{ name: 'paid', type: 'order_status', desc: '已支付/已扣款,可发货', example: 'can_fulfill=true' },
|
||||||
{ name: 'succeeded', type: 'fulfillment_status', desc: '履约成功', example: 'can_fulfill=false' },
|
{ name: 'delivering', type: 'order_status', desc: '发货中', example: 'can_fulfill=false' },
|
||||||
{ name: 'failed', type: 'fulfillment_status', desc: '履约失败,可重试', example: 'can_fulfill=true' },
|
{ name: 'delivered', type: 'order_status', desc: '已交付', example: 'can_fulfill=false' },
|
||||||
{ name: 'cancelled', type: 'fulfillment_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' },
|
||||||
|
|
||||||
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: '已取消' },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
// 订单对象公共字段说明,下单/查询/取消响应共用。
|
// 订单对象公共字段说明,下单/查询/取消响应共用。
|
||||||
const orderFields: ParamSpec[] = [
|
const orderFields: ParamSpec[] = [
|
||||||
{ name: 'order_no', type: 'string', required: true, desc: '平台订单号' },
|
{ name: 'order_no', type: 'string', required: true, desc: '平台订单号' },
|
||||||
{ name: 'client_order_no', type: 'string', required: true, desc: '调用方传入的幂等单号' },
|
{ name: 'client_order_no', type: 'string', required: true, desc: '调用方传入的幂等单号' },
|
||||||
{ name: 'payment_status', type: 'enum', desc: '支付状态,见「状态说明」' },
|
{ name: 'order_status', type: 'enum', desc: '订单状态,见「状态说明」' },
|
||||||
{ name: 'fulfillment_status', type: 'enum', desc: '履约状态,见「状态说明」' },
|
|
||||||
{ name: 'can_fulfill', type: 'bool', desc: '是否允许履约器接单/发货' },
|
{ name: 'can_fulfill', type: 'bool', desc: '是否允许履约器接单/发货' },
|
||||||
{ name: 'cannot_fulfill_reason', type: 'string', desc: 'can_fulfill=false 时的原因' },
|
{ name: 'cannot_fulfill_reason', type: 'string', desc: 'can_fulfill=false 时的原因' },
|
||||||
{ name: 'product.sku', type: 'string', desc: '商品标识,履约以此为准' },
|
{ name: 'product.sku', type: 'string', desc: '商品标识,履约以此为准' },
|
||||||
@@ -64,8 +57,7 @@ const orderResponseExample = `{
|
|||||||
"data": {
|
"data": {
|
||||||
"order_no": "FO20260730000123",
|
"order_no": "FO20260730000123",
|
||||||
"client_order_no": "shop-10001",
|
"client_order_no": "shop-10001",
|
||||||
"payment_status": "paid",
|
"order_status": "paid",
|
||||||
"fulfillment_status": "pending",
|
|
||||||
"can_fulfill": true,
|
"can_fulfill": true,
|
||||||
"cannot_fulfill_reason": "",
|
"cannot_fulfill_reason": "",
|
||||||
"product": { "sku": "suit_pink_sheep", "name": "套装-糯粉咩咩" },
|
"product": { "sku": "suit_pink_sheep", "name": "套装-糯粉咩咩" },
|
||||||
@@ -134,7 +126,7 @@ const deliverySubmitResponseExample = `{
|
|||||||
"message": "ok",
|
"message": "ok",
|
||||||
"data": {
|
"data": {
|
||||||
"order_no": "FO20260730000123",
|
"order_no": "FO20260730000123",
|
||||||
"status": "processing",
|
"status": "delivering",
|
||||||
"message": "已提交上游发货,等待发货结果回传",
|
"message": "已提交上游发货,等待发货结果回传",
|
||||||
"provider_order_no": "provider-1",
|
"provider_order_no": "provider-1",
|
||||||
"game_account": {
|
"game_account": {
|
||||||
@@ -222,8 +214,7 @@ export const endpoints: EndpointSpec[] = [
|
|||||||
"order": {
|
"order": {
|
||||||
"order_no": "FO20260730000123",
|
"order_no": "FO20260730000123",
|
||||||
"client_order_no": "shop-10001",
|
"client_order_no": "shop-10001",
|
||||||
"payment_status": "paid",
|
"order_status": "paid",
|
||||||
"fulfillment_status": "pending",
|
|
||||||
"can_fulfill": true
|
"can_fulfill": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -234,7 +225,7 @@ export const endpoints: EndpointSpec[] = [
|
|||||||
],
|
],
|
||||||
notes: [
|
notes: [
|
||||||
'client_order_no 是唯一幂等单号,请保证同一商户下不重复。',
|
'client_order_no 是唯一幂等单号,请保证同一商户下不重复。',
|
||||||
'下单即扣款(payment_status=paid),余额不足返回 400。',
|
'当前无真实支付流程,下单成功即扣款并进入 order_status=paid,余额不足返回 400。',
|
||||||
'同一 client_order_no 重复请求不会重复扣款,返回 idempotent=true。',
|
'同一 client_order_no 重复请求不会重复扣款,返回 idempotent=true。',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -254,7 +245,7 @@ export const endpoints: EndpointSpec[] = [
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: '/api/client/v1/orders/{order_no}/cancel',
|
path: '/api/client/v1/orders/{order_no}/cancel',
|
||||||
title: '取消订单',
|
title: '取消订单',
|
||||||
summary: '取消未履约订单并退款(仅 pending/failed 可取消)',
|
summary: '取消未发货订单并退款(仅 paid/ship_failed 可取消)',
|
||||||
scope: 'orders:write',
|
scope: 'orders:write',
|
||||||
pathParams: [{ name: 'order_no', type: 'string', required: true, desc: '平台订单号', example: 'FO20260730000123' }],
|
pathParams: [{ name: 'order_no', type: 'string', required: true, desc: '平台订单号', example: 'FO20260730000123' }],
|
||||||
bodyParams: [{ name: 'reason', type: 'string', desc: '取消原因(可选)', example: '买家主动取消' }],
|
bodyParams: [{ name: 'reason', type: 'string', desc: '取消原因(可选)', example: '买家主动取消' }],
|
||||||
@@ -262,8 +253,8 @@ export const endpoints: EndpointSpec[] = [
|
|||||||
responseExample: orderResponseExample,
|
responseExample: orderResponseExample,
|
||||||
responseFields: orderFields,
|
responseFields: orderFields,
|
||||||
notes: [
|
notes: [
|
||||||
'取消成功后 payment_status 变为 refunded,金额退回商户钱包。',
|
'取消成功后 order_status 变为 cancelled,金额退回商户钱包。',
|
||||||
'已进入 processing/succeeded 的订单不可取消,返回 400。',
|
'已进入 delivering/delivered 的订单不可取消,返回 400。',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -131,10 +131,10 @@ export default function Dashboard() {
|
|||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} sm={12} lg={8}>
|
<Col xs={24} sm={12} lg={8}>
|
||||||
<MetricCard
|
<MetricCard
|
||||||
title="待履约订单"
|
title="待发货订单"
|
||||||
value={stats?.pending_order_count ?? 0}
|
value={stats?.paid_order_count ?? 0}
|
||||||
icon={<ClockCircleOutlined />}
|
icon={<ClockCircleOutlined />}
|
||||||
color={(stats?.pending_order_count ?? 0) > 0 ? '#cf1322' : undefined}
|
color={(stats?.paid_order_count ?? 0) > 0 ? '#cf1322' : undefined}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
@@ -162,26 +162,32 @@ export default function Dashboard() {
|
|||||||
<Card title="订单状态">
|
<Card title="订单状态">
|
||||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||||
<StatusLine
|
<StatusLine
|
||||||
label="已成功"
|
label="待支付"
|
||||||
value={stats?.succeeded_order_count ?? 0}
|
|
||||||
total={stats?.order_count ?? 0}
|
|
||||||
color="#52c41a"
|
|
||||||
/>
|
|
||||||
<StatusLine
|
|
||||||
label="待履约"
|
|
||||||
value={stats?.pending_order_count ?? 0}
|
value={stats?.pending_order_count ?? 0}
|
||||||
total={stats?.order_count ?? 0}
|
total={stats?.order_count ?? 0}
|
||||||
color="#faad14"
|
color="#faad14"
|
||||||
/>
|
/>
|
||||||
<StatusLine
|
<StatusLine
|
||||||
label="履约中"
|
label="待发货"
|
||||||
value={stats?.processing_order_count ?? 0}
|
value={stats?.paid_order_count ?? 0}
|
||||||
total={stats?.order_count ?? 0}
|
total={stats?.order_count ?? 0}
|
||||||
color="#1677ff"
|
color="#1677ff"
|
||||||
/>
|
/>
|
||||||
<StatusLine
|
<StatusLine
|
||||||
label="已失败"
|
label="发货中"
|
||||||
value={stats?.failed_order_count ?? 0}
|
value={stats?.delivering_order_count ?? 0}
|
||||||
|
total={stats?.order_count ?? 0}
|
||||||
|
color="#13c2c2"
|
||||||
|
/>
|
||||||
|
<StatusLine
|
||||||
|
label="已交付"
|
||||||
|
value={stats?.delivered_order_count ?? 0}
|
||||||
|
total={stats?.order_count ?? 0}
|
||||||
|
color="#52c41a"
|
||||||
|
/>
|
||||||
|
<StatusLine
|
||||||
|
label="发货失败"
|
||||||
|
value={stats?.ship_failed_order_count ?? 0}
|
||||||
total={stats?.order_count ?? 0}
|
total={stats?.order_count ?? 0}
|
||||||
color="#ff4d4f"
|
color="#ff4d4f"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -41,18 +41,12 @@ import type {
|
|||||||
WalletLedgerEntry,
|
WalletLedgerEntry,
|
||||||
} from '../types'
|
} from '../types'
|
||||||
|
|
||||||
const fulfillmentStatusMap: Record<string, { color: string; text: string }> = {
|
const orderStatusMap: Record<string, { color: string; text: string }> = {
|
||||||
pending: { color: 'orange', text: '待履约' },
|
|
||||||
processing: { color: 'cyan', text: '履约中' },
|
|
||||||
succeeded: { color: 'green', text: '已成功' },
|
|
||||||
failed: { color: 'red', text: '已失败' },
|
|
||||||
cancelled: { color: 'default', text: '已取消' },
|
|
||||||
}
|
|
||||||
|
|
||||||
const paymentStatusMap: Record<string, { color: string; text: string }> = {
|
|
||||||
pending: { color: 'orange', text: '待支付' },
|
pending: { color: 'orange', text: '待支付' },
|
||||||
paid: { color: 'blue', text: '已支付' },
|
paid: { color: 'blue', text: '待发货' },
|
||||||
refunded: { color: 'purple', text: '已退款' },
|
delivering: { color: 'cyan', text: '发货中' },
|
||||||
|
delivered: { color: 'green', text: '已交付' },
|
||||||
|
ship_failed: { color: 'red', text: '发货失败' },
|
||||||
cancelled: { color: 'default', text: '已取消' },
|
cancelled: { color: 'default', text: '已取消' },
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,8 +72,8 @@ const eventOptions = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
const testOrderStatusOptions = [
|
const testOrderStatusOptions = [
|
||||||
{ value: 'pending', label: '已支付,可发货' },
|
{ value: 'paid', label: '已支付,可发货' },
|
||||||
{ value: 'failed', label: '发货失败,可重试' },
|
{ value: 'ship_failed', label: '发货失败,可重试' },
|
||||||
]
|
]
|
||||||
|
|
||||||
type MerchantCenterTab = 'products' | 'orders' | 'wallet' | 'api' | 'callbacks' | 'members'
|
type MerchantCenterTab = 'products' | 'orders' | 'wallet' | 'api' | 'callbacks' | 'members'
|
||||||
@@ -347,7 +341,7 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer
|
|||||||
testOrderForm.setFieldsValue({
|
testOrderForm.setFieldsValue({
|
||||||
buyer_reference: '测试买家',
|
buyer_reference: '测试买家',
|
||||||
note: '联调测试订单',
|
note: '联调测试订单',
|
||||||
fulfillment_status: 'pending',
|
order_status: 'paid',
|
||||||
})
|
})
|
||||||
setTestOrderOpen(true)
|
setTestOrderOpen(true)
|
||||||
setTestOrderProductsLoading(true)
|
setTestOrderProductsLoading(true)
|
||||||
@@ -374,7 +368,7 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer
|
|||||||
sku: values.sku,
|
sku: values.sku,
|
||||||
buyer_reference: values.buyer_reference,
|
buyer_reference: values.buyer_reference,
|
||||||
note: values.note,
|
note: values.note,
|
||||||
fulfillment_status: values.fulfillment_status,
|
order_status: values.order_status,
|
||||||
})
|
})
|
||||||
setTestOrderOpen(false)
|
setTestOrderOpen(false)
|
||||||
setTestOrderResult(result)
|
setTestOrderResult(result)
|
||||||
@@ -494,8 +488,7 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer
|
|||||||
{ title: '基础金额', dataIndex: 'base_amount', width: 88, render: money },
|
{ title: '基础金额', dataIndex: 'base_amount', width: 88, render: money },
|
||||||
{ title: '手续费', dataIndex: 'service_fee_amount', width: 82, render: money },
|
{ title: '手续费', dataIndex: 'service_fee_amount', width: 82, render: money },
|
||||||
{ title: '扣款合计', dataIndex: 'amount', width: 88, render: money },
|
{ title: '扣款合计', dataIndex: 'amount', width: 88, render: money },
|
||||||
{ title: '支付', dataIndex: 'payment_status', width: 76, render: paymentStatusTag },
|
{ title: '状态', dataIndex: 'order_status', width: 92, render: orderStatusTag },
|
||||||
{ title: '履约', dataIndex: 'fulfillment_status', width: 86, render: fulfillmentStatusTag },
|
|
||||||
{ title: '上游单号', dataIndex: 'provider_order_no', width: 120, ellipsis: true, render: (v) => v || '-' },
|
{ title: '上游单号', dataIndex: 'provider_order_no', width: 120, ellipsis: true, render: (v) => v || '-' },
|
||||||
{
|
{
|
||||||
title: '链接有效期',
|
title: '链接有效期',
|
||||||
@@ -791,7 +784,7 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer
|
|||||||
<Form.Item name="buyer_reference" label="买家名称" rules={[{ max: 128 }]}>
|
<Form.Item name="buyer_reference" label="买家名称" rules={[{ max: 128 }]}>
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="fulfillment_status" label="可发货状态" rules={[{ required: true }]}>
|
<Form.Item name="order_status" label="订单状态" rules={[{ required: true }]}>
|
||||||
<Select options={testOrderStatusOptions} />
|
<Select options={testOrderStatusOptions} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="note" label="备注" rules={[{ max: 512 }]}>
|
<Form.Item name="note" label="备注" rules={[{ max: 512 }]}>
|
||||||
@@ -1004,14 +997,9 @@ function productOptionLabel(item: MerchantProduct) {
|
|||||||
return name === item.sku ? item.sku : `${name} / ${item.sku}`
|
return name === item.sku ? item.sku : `${name} / ${item.sku}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function paymentStatusTag(value: string) {
|
function orderStatusTag(value: string) {
|
||||||
const item = paymentStatusMap[value] || { color: 'default', text: value }
|
const item = orderStatusMap[value] || { color: 'default', text: value }
|
||||||
return <Tag color={item.color}>{item.text}</Tag>
|
return <Tag color={item.color}>{item.text}</Tag>
|
||||||
}
|
|
||||||
|
|
||||||
function fulfillmentStatusTag(value: string) {
|
|
||||||
const item = fulfillmentStatusMap[value] || { color: 'default', text: value }
|
|
||||||
return <Tag color={item.color}>{item.text}</Tag>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ledgerTypeTag(value: string) {
|
function ledgerTypeTag(value: string) {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
endpoints,
|
endpoints,
|
||||||
errorCodes,
|
errorCodes,
|
||||||
orderStatusTable,
|
orderStatusTable,
|
||||||
paymentStatusTable,
|
|
||||||
} from '../openapi/endpoints'
|
} from '../openapi/endpoints'
|
||||||
import { endpointSections, type EndpointSpec } from '../openapi/types'
|
import { endpointSections, type EndpointSpec } from '../openapi/types'
|
||||||
|
|
||||||
@@ -453,7 +452,7 @@ function EndpointSection({ spec }: { spec: EndpointSpec }) {
|
|||||||
function StatusSection() {
|
function StatusSection() {
|
||||||
return (
|
return (
|
||||||
<SectionWrap id="status" title="状态说明">
|
<SectionWrap id="status" title="状态说明">
|
||||||
<Card className="api-docs__card" size="small" title="履约状态 fulfillment_status 与 can_fulfill">
|
<Card className="api-docs__card" size="small" title="订单状态 order_status 与 can_fulfill">
|
||||||
<Table
|
<Table
|
||||||
className="api-docs__table"
|
className="api-docs__table"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -473,19 +472,6 @@ function StatusSection() {
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
<Card className="api-docs__card" size="small" title="支付状态 payment_status">
|
|
||||||
<Table
|
|
||||||
className="api-docs__table"
|
|
||||||
size="small"
|
|
||||||
pagination={false}
|
|
||||||
rowKey="name"
|
|
||||||
dataSource={paymentStatusTable}
|
|
||||||
columns={[
|
|
||||||
{ title: '状态', dataIndex: 'name', width: 140, render: (v) => <Text code>{v}</Text> },
|
|
||||||
{ title: '说明', dataIndex: 'desc' },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
<Alert
|
<Alert
|
||||||
className="api-docs__alert"
|
className="api-docs__alert"
|
||||||
type="info"
|
type="info"
|
||||||
@@ -501,7 +487,7 @@ POST /orders 下单(client_order_no 幂等)
|
|||||||
↓
|
↓
|
||||||
发货平台查询订单并执行发货
|
发货平台查询订单并执行发货
|
||||||
↓
|
↓
|
||||||
发货平台通过上游回调更新 fulfillment_status
|
发货平台通过上游回调更新 order_status
|
||||||
↓
|
↓
|
||||||
GET /orders/{order_no} 轮询,或接收 order.fulfillment.updated 回调`}</pre>
|
GET /orders/{order_no} 轮询,或接收 order.fulfillment.updated 回调`}</pre>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,8 +108,7 @@ export interface FulfillmentOrder {
|
|||||||
service_fee_amount: number
|
service_fee_amount: number
|
||||||
amount: number
|
amount: number
|
||||||
currency: string
|
currency: string
|
||||||
payment_status: string
|
order_status: string
|
||||||
fulfillment_status: string
|
|
||||||
buyer_reference: string
|
buyer_reference: string
|
||||||
provider_order_no: string
|
provider_order_no: string
|
||||||
failure_reason: string
|
failure_reason: string
|
||||||
@@ -222,9 +221,10 @@ export interface DashboardStats {
|
|||||||
total_fees: number
|
total_fees: number
|
||||||
today_fees: number
|
today_fees: number
|
||||||
pending_order_count: number
|
pending_order_count: number
|
||||||
processing_order_count: number
|
paid_order_count: number
|
||||||
succeeded_order_count: number
|
delivering_order_count: number
|
||||||
failed_order_count: number
|
delivered_order_count: number
|
||||||
|
ship_failed_order_count: number
|
||||||
cancelled_order_count: number
|
cancelled_order_count: number
|
||||||
wallet_available_balance: number
|
wallet_available_balance: number
|
||||||
wallet_frozen_balance: number
|
wallet_frozen_balance: number
|
||||||
|
|||||||
Reference in New Issue
Block a user