统一订单单状态模型

This commit is contained in:
yml2213
2026-07-31 17:02:19 +08:00
parent 677bdbbc1c
commit 04130e5ec8
19 changed files with 300 additions and 345 deletions
+90 -116
View File
@@ -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
}
}