统一发货口径并保护结果数据
This commit is contained in:
@@ -266,7 +266,7 @@ func (s *DeliveryService) submit(orderNo, gameAccount, bindUUID string, apiClien
|
||||
if existing := buildExistingDeliverySubmitResult(claimed); existing != nil {
|
||||
return existing, nil
|
||||
}
|
||||
return nil, newDeliveryHTTPError(http.StatusConflict, "订单暂时正在履约中,请稍后查询")
|
||||
return nil, newDeliveryHTTPError(http.StatusConflict, "订单暂时正在发货中,请稍后查询")
|
||||
}
|
||||
boundAccount, err := s.accountBound(bindUUID, goodID)
|
||||
if err != nil {
|
||||
@@ -507,10 +507,10 @@ func buildExistingDeliverySubmitResult(order *model.FulfillmentOrder) *DeliveryS
|
||||
order.ProviderOrderNo,
|
||||
stringFromMap(resultData, "provider_order_no"),
|
||||
)
|
||||
message := "订单暂时正在履约中,请稍后查询"
|
||||
message := "订单暂时正在发货中,请稍后查询"
|
||||
status := normalizeOrderStatus(order)
|
||||
if status == model.OrderStatusDelivered {
|
||||
message = "订单已履约成功"
|
||||
message = "订单已交付"
|
||||
}
|
||||
return &DeliverySubmitResult{
|
||||
OrderNo: order.OrderNo,
|
||||
|
||||
@@ -366,11 +366,12 @@ func (s *FulfillmentService) UpdateFulfillment(in FulfillmentUpdateInput) (*mode
|
||||
return nil, errors.New("无效的订单状态")
|
||||
}
|
||||
nextOrderStatus := in.Status
|
||||
resultData := ""
|
||||
var resultData string
|
||||
hasResultData := in.ResultData != nil
|
||||
if in.ResultData != nil {
|
||||
raw, err := json.Marshal(in.ResultData)
|
||||
if err != nil {
|
||||
return nil, errors.New("履约结果无法序列化")
|
||||
return nil, errors.New("发货结果无法序列化")
|
||||
}
|
||||
resultData = string(raw)
|
||||
}
|
||||
@@ -394,7 +395,9 @@ func (s *FulfillmentService) UpdateFulfillment(in FulfillmentUpdateInput) (*mode
|
||||
}
|
||||
now := time.Now()
|
||||
updates := map[string]interface{}{"order_status": nextOrderStatus}
|
||||
updates["result_data"] = resultData
|
||||
if hasResultData {
|
||||
updates["result_data"] = resultData
|
||||
}
|
||||
if in.ProviderOrderNo != "" {
|
||||
updates["provider_order_no"] = in.ProviderOrderNo
|
||||
}
|
||||
@@ -473,7 +476,7 @@ 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()))
|
||||
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)
|
||||
@@ -747,22 +750,22 @@ func CanFulfill(order *model.FulfillmentOrder) (bool, string) {
|
||||
}
|
||||
|
||||
func orderCallbackData(order *model.FulfillmentOrder) map[string]interface{} {
|
||||
canFulfill, cannotFulfillReason := CanFulfill(order)
|
||||
canShip, cannotShipReason := CanFulfill(order)
|
||||
return map[string]interface{}{
|
||||
"order_no": order.OrderNo,
|
||||
"client_order_no": order.ClientOrderNo,
|
||||
"product_sku": order.ProductSKU,
|
||||
"quantity": order.Quantity,
|
||||
"base_amount": order.BaseAmount,
|
||||
"fee_type": order.FeeType,
|
||||
"service_fee_amount": order.ServiceFeeAmount,
|
||||
"amount": order.Amount,
|
||||
"currency": order.Currency,
|
||||
"order_status": normalizeOrderStatus(order),
|
||||
"can_fulfill": canFulfill,
|
||||
"cannot_fulfill_reason": cannotFulfillReason,
|
||||
"provider_order_no": order.ProviderOrderNo,
|
||||
"failure_reason": order.FailureReason,
|
||||
"order_no": order.OrderNo,
|
||||
"client_order_no": order.ClientOrderNo,
|
||||
"product_sku": order.ProductSKU,
|
||||
"quantity": order.Quantity,
|
||||
"base_amount": order.BaseAmount,
|
||||
"fee_type": order.FeeType,
|
||||
"service_fee_amount": order.ServiceFeeAmount,
|
||||
"amount": order.Amount,
|
||||
"currency": order.Currency,
|
||||
"order_status": normalizeOrderStatus(order),
|
||||
"can_ship": canShip,
|
||||
"cannot_ship_reason": cannotShipReason,
|
||||
"provider_order_no": order.ProviderOrderNo,
|
||||
"failure_reason": order.FailureReason,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1097,7 +1100,7 @@ func (s *FulfillmentService) HandleShipNotify(in ShipNotifyInput) (*ShipNotifyRe
|
||||
return err
|
||||
}
|
||||
|
||||
// 已履约成功:success 推送幂等成功。状态读取和后续更新必须在同一把行锁内完成。
|
||||
// 已交付:success 推送幂等成功。状态读取和后续更新必须在同一把行锁内完成。
|
||||
if normalizeOrderStatus(&order) == model.OrderStatusDelivered && in.ShipStatus == "success" {
|
||||
result = ShipNotifyResult{
|
||||
OrderNo: order.OrderNo,
|
||||
@@ -1105,7 +1108,7 @@ func (s *FulfillmentService) HandleShipNotify(in ShipNotifyInput) (*ShipNotifyRe
|
||||
Message: "订单已交付,幂等成功",
|
||||
}
|
||||
return writeAudit(tx, &order.MerchantID, nil, nil, "ship.notify", "fulfillment_order", order.OrderNo,
|
||||
shipNotifyAuditMetadata(in, normalizeOrderStatus(&order), "订单已履约成功,幂等忽略"))
|
||||
shipNotifyAuditMetadata(in, normalizeOrderStatus(&order), "订单已交付,幂等忽略"))
|
||||
}
|
||||
|
||||
if normalizeOrderStatus(&order) == model.OrderStatusCancelled {
|
||||
@@ -1265,7 +1268,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("履约超时:订单已处于 delivering 超过 %d 分钟", int(timeout.Minutes()))
|
||||
m["fail_reason"] = fmt.Sprintf("发货超时:订单已处于 delivering 超过 %d 分钟", int(timeout.Minutes()))
|
||||
raw, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return existing
|
||||
|
||||
@@ -86,11 +86,11 @@ func validateOrderStatusTransition(order *model.FulfillmentOrder, next string, k
|
||||
if next == model.OrderStatusPaid || next == model.OrderStatusCancelled {
|
||||
return nil
|
||||
}
|
||||
return errors.New("订单未支付,不能履约")
|
||||
return errors.New("订单未支付,不能发货")
|
||||
case model.OrderStatusCancelled:
|
||||
return errors.New("订单已取消,不能更新状态")
|
||||
case model.OrderStatusDelivered:
|
||||
return errors.New("订单已履约成功,不能回退状态")
|
||||
return errors.New("订单已交付,不能回退状态")
|
||||
}
|
||||
switch next {
|
||||
case model.OrderStatusDelivering:
|
||||
@@ -105,7 +105,7 @@ func validateOrderStatusTransition(order *model.FulfillmentOrder, next string, k
|
||||
}
|
||||
case model.OrderStatusShipFailed:
|
||||
if kind == fulfillmentTransitionTimeout && current != model.OrderStatusDelivering {
|
||||
return errors.New("只有履约中的订单可以标记超时")
|
||||
return errors.New("只有发货中的订单可以标记超时")
|
||||
}
|
||||
if current == model.OrderStatusPaid ||
|
||||
current == model.OrderStatusShipFailed ||
|
||||
@@ -134,7 +134,7 @@ func canCancelOrder(order *model.FulfillmentOrder) error {
|
||||
case model.OrderStatusPending:
|
||||
return errors.New("订单未支付,不能取消")
|
||||
case model.OrderStatusDelivering, model.OrderStatusDelivered:
|
||||
return errors.New("订单已进入履约流程,不能取消")
|
||||
return errors.New("订单已进入发货流程,不能取消")
|
||||
default:
|
||||
return errors.New("订单当前状态不能取消")
|
||||
}
|
||||
|
||||
@@ -393,6 +393,47 @@ func TestOrderStatusTransitions(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateFulfillmentKeepsResultDataWhenOmitted(t *testing.T) {
|
||||
db := newServiceTestDB(t)
|
||||
merchantID, product := seedFulfillmentMerchant(t, db, "merchant-result-data", 1000, -1, 100)
|
||||
svc := NewFulfillmentService(db, nil)
|
||||
created, err := svc.CreateOrder(CreateFulfillmentOrderInput{
|
||||
MerchantID: merchantID,
|
||||
APIClientID: 13,
|
||||
ClientOrderNo: "client-result-data",
|
||||
SKU: product.SKU,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create order: %v", err)
|
||||
}
|
||||
delivering, err := svc.UpdateFulfillment(FulfillmentUpdateInput{
|
||||
MerchantID: merchantID,
|
||||
APIClientID: 13,
|
||||
OrderNo: created.Order.OrderNo,
|
||||
Status: model.OrderStatusDelivering,
|
||||
ResultData: map[string]string{"stage": "claimed"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("mark delivering: %v", err)
|
||||
}
|
||||
if !strings.Contains(delivering.ResultData, `"stage":"claimed"`) {
|
||||
t.Fatalf("expected initial result_data, got %s", delivering.ResultData)
|
||||
}
|
||||
failed, err := svc.UpdateFulfillment(FulfillmentUpdateInput{
|
||||
MerchantID: merchantID,
|
||||
APIClientID: 13,
|
||||
OrderNo: created.Order.OrderNo,
|
||||
Status: model.OrderStatusShipFailed,
|
||||
FailureReason: "上游暂不可用",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("mark failed: %v", err)
|
||||
}
|
||||
if failed.ResultData != delivering.ResultData {
|
||||
t.Fatalf("result_data should be kept when omitted, before=%s after=%s", delivering.ResultData, failed.ResultData)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkProcessingTimeoutsMarksStaleOrdersFailed(t *testing.T) {
|
||||
db := newServiceTestDB(t)
|
||||
merchantID, product := seedFulfillmentMerchant(t, db, "merchant-timeout", 1000, -1, 100)
|
||||
@@ -432,7 +473,7 @@ func TestMarkProcessingTimeoutsMarksStaleOrdersFailed(t *testing.T) {
|
||||
if err := db.First(&order, delivering.ID).Error; err != nil {
|
||||
t.Fatalf("query order: %v", err)
|
||||
}
|
||||
if order.OrderStatus != model.OrderStatusShipFailed || !strings.Contains(order.FailureReason, "履约超时") {
|
||||
if order.OrderStatus != model.OrderStatusShipFailed || !strings.Contains(order.FailureReason, "发货超时") {
|
||||
t.Fatalf("expected failed timeout order, got %+v", order)
|
||||
}
|
||||
if !strings.Contains(order.ResultData, `"timeout":true`) {
|
||||
@@ -668,7 +709,7 @@ func TestHandleShipNotifyUpdatesOrderAndEnqueuesMerchantCallback(t *testing.T) {
|
||||
t.Fatalf("create order: %v", err)
|
||||
}
|
||||
if _, err := callbackSvc.CreateSubscription(merchantID, CreateCallbackInput{
|
||||
Name: "履约回调",
|
||||
Name: "发货回调",
|
||||
URL: "https://example.com/callback",
|
||||
Events: "order.fulfillment.updated",
|
||||
}, 7); err != nil {
|
||||
@@ -702,6 +743,9 @@ func TestHandleShipNotifyUpdatesOrderAndEnqueuesMerchantCallback(t *testing.T) {
|
||||
if !strings.Contains(delivery.Payload, created.Order.OrderNo) || !strings.Contains(delivery.Payload, "SRC-10001") {
|
||||
t.Fatalf("callback payload should contain updated order data, got %s", delivery.Payload)
|
||||
}
|
||||
if !strings.Contains(delivery.Payload, `"can_ship":false`) || strings.Contains(delivery.Payload, "can_fulfill") {
|
||||
t.Fatalf("callback payload should use can_ship fields, got %s", delivery.Payload)
|
||||
}
|
||||
var auditCount int64
|
||||
db.Model(&model.AuditLog{}).Where("entity_id = ? AND action = ?", created.Order.OrderNo, "ship.notify").Count(&auditCount)
|
||||
if auditCount != 1 {
|
||||
|
||||
Reference in New Issue
Block a user