加强发货提交可靠性并统一契约口径
- 发货提交记录阶段推进与失败分类,失败不再清空 result_data - 超时巡检区分已提交上游与提交中断两类卡单,避免误判 - 已提交上游的失败订单禁止自动重发,防止重复发货 - ship_attempts 仅在 claim 时计数,失败阶段只记录分类信息 - CanFulfill 对已提交上游的失败单返回不可发货 - 删除预留的 pending 订单状态,统一订单状态模型 - 契约改名:order.fulfillment.updated -> order.shipping.updated,fulfillment:read -> shipping:read - 文档修正 scope 为或关系
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
@@ -418,7 +419,7 @@ func (s *FulfillmentService) UpdateFulfillment(in FulfillmentUpdateInput) (*mode
|
||||
return err
|
||||
}
|
||||
if s.callbacks != nil {
|
||||
if err := s.callbacks.Enqueue(tx, in.MerchantID, "order.fulfillment.updated", orderCallbackData(&out)); err != nil {
|
||||
if err := s.callbacks.Enqueue(tx, in.MerchantID, "order.shipping.updated", orderCallbackData(&out)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -476,10 +477,16 @@ func (s *FulfillmentService) markProcessingTimeout(id uint, timeout time.Duratio
|
||||
if err := validateOrderStatusTransition(&order, model.OrderStatusShipFailed, fulfillmentTransitionTimeout); err != nil {
|
||||
return nil
|
||||
}
|
||||
reason := fmt.Sprintf("发货超时:订单已处于 delivering 超过 %d 分钟", int(timeout.Minutes()))
|
||||
submittedUpstream := deliverySubmittedUpstream(&order)
|
||||
var reason string
|
||||
if submittedUpstream {
|
||||
reason = fmt.Sprintf("发货超时:订单已提交上游但超过 %d 分钟未回传结果,可能仍在处理;请勿直接重试,先在上游确认订单状态", int(timeout.Minutes()))
|
||||
} else {
|
||||
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)
|
||||
updates["result_data"] = buildProcessingTimeoutResultData(order.ResultData, timeout, now, reason)
|
||||
if err := tx.Model(&order).Updates(updates).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -496,7 +503,7 @@ func (s *FulfillmentService) markProcessingTimeout(id uint, timeout time.Duratio
|
||||
return err
|
||||
}
|
||||
if s.callbacks != nil {
|
||||
if err := s.callbacks.Enqueue(tx, order.MerchantID, "order.fulfillment.updated", orderCallbackData(&out)); err != nil {
|
||||
if err := s.callbacks.Enqueue(tx, order.MerchantID, "order.shipping.updated", orderCallbackData(&out)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -734,10 +741,13 @@ func calculateServiceFee(baseAmount int64, feeType string, feeRateBP, feeFixedAm
|
||||
|
||||
func CanFulfill(order *model.FulfillmentOrder) (bool, string) {
|
||||
switch normalizeOrderStatus(order) {
|
||||
case model.OrderStatusPaid, model.OrderStatusShipFailed:
|
||||
case model.OrderStatusPaid:
|
||||
return true, ""
|
||||
case model.OrderStatusShipFailed:
|
||||
if deliverySubmittedUpstream(order) {
|
||||
return false, "订单已提交上游,为避免重复发货请先确认上游状态"
|
||||
}
|
||||
return true, ""
|
||||
case model.OrderStatusPending:
|
||||
return false, "订单未支付"
|
||||
case model.OrderStatusDelivering:
|
||||
return false, "订单发货中"
|
||||
case model.OrderStatusDelivered:
|
||||
@@ -782,16 +792,15 @@ type DashboardStats struct {
|
||||
UserCount int64 `json:"user_count"`
|
||||
OrderCount int64 `json:"order_count"`
|
||||
TodayOrderCount int64 `json:"today_order_count"`
|
||||
TotalSales int64 `json:"total_sales"`
|
||||
TodaySales int64 `json:"today_sales"`
|
||||
TotalFees int64 `json:"total_fees"`
|
||||
TodayFees int64 `json:"today_fees"`
|
||||
PendingOrderCount int64 `json:"pending_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"`
|
||||
TotalSales int64 `json:"total_sales"`
|
||||
TodaySales int64 `json:"today_sales"`
|
||||
TotalFees int64 `json:"total_fees"`
|
||||
TodayFees int64 `json:"today_fees"`
|
||||
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"`
|
||||
APIClientCount int64 `json:"api_client_count"`
|
||||
@@ -922,8 +931,6 @@ func (s *FulfillmentService) Dashboard(merchantID uint, isPlatformAdmin bool) (*
|
||||
}
|
||||
for _, item := range orderStatusCounts {
|
||||
switch item.Status {
|
||||
case model.OrderStatusPending:
|
||||
stats.PendingOrderCount = item.Count
|
||||
case model.OrderStatusPaid:
|
||||
stats.PaidOrderCount = item.Count
|
||||
case model.OrderStatusDelivering:
|
||||
@@ -1133,9 +1140,6 @@ func (s *FulfillmentService) HandleShipNotify(in ShipNotifyInput) (*ShipNotifyRe
|
||||
case "success":
|
||||
if err := validateOrderStatusTransition(&order, nextStatus, fulfillmentTransitionShipNotify); err != nil {
|
||||
message := "当前状态不允许标记发货成功"
|
||||
if normalizeOrderStatus(&order) == model.OrderStatusPending {
|
||||
message = "订单未支付,拒绝成功推送"
|
||||
}
|
||||
if err := writeShipNotifyRejectedAudit(tx, &order, in, message); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1153,8 +1157,6 @@ func (s *FulfillmentService) HandleShipNotify(in ShipNotifyInput) (*ShipNotifyRe
|
||||
message := "当前状态不允许标记发货失败"
|
||||
if normalizeOrderStatus(&order) == model.OrderStatusDelivered {
|
||||
message = "订单已交付,拒绝失败推送"
|
||||
} else if normalizeOrderStatus(&order) == model.OrderStatusPending {
|
||||
message = "订单未支付,拒绝失败推送"
|
||||
}
|
||||
if err := writeShipNotifyRejectedAudit(tx, &order, in, message); err != nil {
|
||||
return err
|
||||
@@ -1185,7 +1187,7 @@ func (s *FulfillmentService) HandleShipNotify(in ShipNotifyInput) (*ShipNotifyRe
|
||||
return err
|
||||
}
|
||||
if s.callbacks != nil {
|
||||
if err := s.callbacks.Enqueue(tx, order.MerchantID, "order.fulfillment.updated", orderCallbackData(&updated)); err != nil {
|
||||
if err := s.callbacks.Enqueue(tx, order.MerchantID, "order.shipping.updated", orderCallbackData(&updated)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -1259,16 +1261,28 @@ func buildShipNotifyResultData(existing string, in ShipNotifyInput, shippedAt *t
|
||||
return string(raw)
|
||||
}
|
||||
|
||||
func buildProcessingTimeoutResultData(existing string, timeout time.Duration, now time.Time) string {
|
||||
func buildProcessingTimeoutResultData(existing string, timeout time.Duration, now time.Time, reason string) string {
|
||||
return mergeResultData(existing, map[string]interface{}{
|
||||
"timeout": true,
|
||||
"timeout_minutes": int(timeout.Minutes()),
|
||||
"timeout_at": timeutil.FormatAPITime(now),
|
||||
"ship_status": "failed",
|
||||
"fail_reason": reason,
|
||||
})
|
||||
}
|
||||
|
||||
// mergeResultData 保留已有 JSON 字段,仅覆盖或新增 patch 中的字段,避免各发货阶段互相清空上下文。
|
||||
func mergeResultData(existing string, patch map[string]interface{}) string {
|
||||
if len(patch) == 0 {
|
||||
return existing
|
||||
}
|
||||
m := map[string]interface{}{}
|
||||
if existing != "" && json.Valid([]byte(existing)) {
|
||||
_ = json.Unmarshal([]byte(existing), &m)
|
||||
}
|
||||
m["timeout"] = true
|
||||
m["timeout_minutes"] = int(timeout.Minutes())
|
||||
m["timeout_at"] = timeutil.FormatAPITime(now)
|
||||
m["ship_status"] = "failed"
|
||||
m["fail_reason"] = fmt.Sprintf("发货超时:订单已处于 delivering 超过 %d 分钟", int(timeout.Minutes()))
|
||||
for k, v := range patch {
|
||||
m[k] = v
|
||||
}
|
||||
raw, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return existing
|
||||
@@ -1276,6 +1290,37 @@ func buildProcessingTimeoutResultData(existing string, timeout time.Duration, no
|
||||
return string(raw)
|
||||
}
|
||||
|
||||
func resultDataMap(raw string) map[string]interface{} {
|
||||
m := map[string]interface{}{}
|
||||
if raw != "" && json.Valid([]byte(raw)) {
|
||||
_ = json.Unmarshal([]byte(raw), &m)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func resultDataString(raw, key string) string {
|
||||
if v, ok := resultDataMap(raw)[key].(string); ok {
|
||||
return v
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func resultDataNumber(raw, key string) int64 {
|
||||
switch v := resultDataMap(raw)[key].(type) {
|
||||
case float64:
|
||||
return int64(v)
|
||||
case int64:
|
||||
return v
|
||||
case int:
|
||||
return int64(v)
|
||||
case string:
|
||||
if n, err := strconv.ParseInt(v, 10, 64); err == nil {
|
||||
return n
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// extractGameFields 从 JSON 文本中还原游戏相关字段(仅填充当前为空的字段)。
|
||||
func extractGameFields(raw string, out *OpenOrderQuery) {
|
||||
if raw == "" || !json.Valid([]byte(raw)) {
|
||||
|
||||
Reference in New Issue
Block a user