修正发货回调流程

This commit is contained in:
yml2213
2026-07-30 19:35:34 +08:00
parent 254ba9b721
commit 5858ff59ae
7 changed files with 122 additions and 112 deletions
-68
View File
@@ -144,74 +144,6 @@ func (h *OpenV1Handler) CancelOrder(c *gin.Context) {
response.OK(c, buildOpenOrderResponse(order))
}
type openShipNotifyReq struct {
OrderNo string `json:"order_no"`
ShipStatus string `json:"ship_status" binding:"required"`
ProviderOrderNo string `json:"provider_order_no"`
FailReason string `json:"fail_reason"`
Result json.RawMessage `json:"result"`
}
func (h *OpenV1Handler) ShipNotify(c *gin.Context) {
var req openShipNotifyReq
if err := c.ShouldBindJSON(&req); err != nil {
openlog.Warn(c, "client_ship_notify bind_fail err=%v", err)
response.BadRequest(c, "参数错误:ship_status 必填")
return
}
status := ""
switch req.ShipStatus {
case "processing":
status = model.FulfillmentStatusProcessing
case "success":
status = model.FulfillmentStatusSucceeded
case "failed":
status = model.FulfillmentStatusFailed
default:
openlog.Warn(c, "client_ship_notify bad_status=%s", req.ShipStatus)
response.BadRequest(c, "ship_status 仅支持 processing、success、failed")
return
}
var result interface{}
if len(req.Result) > 0 {
if err := json.Unmarshal(req.Result, &result); err != nil {
openlog.Warn(c, "client_ship_notify bad_result err=%v", err)
response.BadRequest(c, "result 必须是有效 JSON")
return
}
}
client := middleware.GetAPIClient(c)
orderNo := c.Param("order_no")
if orderNo == "" {
orderNo = req.OrderNo
}
if orderNo == "" {
openlog.Warn(c, "client_ship_notify missing_order_no")
response.BadRequest(c, "order_no 必填")
return
}
openlog.Info(c, "client_ship_notify start order_no=%s ship_status=%s provider_no=%s",
orderNo, req.ShipStatus, req.ProviderOrderNo)
order, err := h.fulfillmentSvc.UpdateFulfillment(service.FulfillmentUpdateInput{
MerchantID: middleware.GetMerchantID(c),
APIClientID: client.ID,
OrderNo: orderNo,
Status: status,
ProviderOrderNo: req.ProviderOrderNo,
FailureReason: req.FailReason,
ResultData: result,
})
if err != nil {
openlog.Warn(c, "client_ship_notify fail order_no=%s ship_status=%s err=%v",
orderNo, req.ShipStatus, err)
response.BadRequest(c, err.Error())
return
}
openlog.Info(c, "client_ship_notify ok order_no=%s status=%s",
order.OrderNo, order.FulfillmentStatus)
response.OK(c, buildOpenOrderResponse(order))
}
func (h *OpenV1Handler) GetWallet(c *gin.Context) {
openlog.Info(c, "get_wallet start")
wallet, err := h.fulfillmentSvc.GetWallet(middleware.GetMerchantID(c))
-1
View File
@@ -75,7 +75,6 @@ func Setup(h *Handlers) *gin.Engine {
clientOpen.POST("/orders", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureOrders), middleware.RequireAPIScope("orders:write"), h.Open.CreateOrder)
clientOpen.GET("/orders/:order_no", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureOrders), middleware.RequireAPIScope("orders:read", "fulfillment:read"), h.Open.QueryOrder)
clientOpen.POST("/orders/:order_no/cancel", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureOrders), middleware.RequireAPIScope("orders:write"), h.Open.CancelOrder)
clientOpen.POST("/orders/:order_no/ship-notify", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureOrders), middleware.RequireAPIScope("fulfillment:write"), h.Open.ShipNotify)
clientOpen.GET("/wallet", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureWallet), middleware.RequireAPIScope("wallet:read"), h.Open.GetWallet)
}
+43 -6
View File
@@ -928,13 +928,46 @@ func (s *FulfillmentService) HandleShipNotify(in ShipNotifyInput) (*ShipNotifyRe
msg = "订单已标记为发货中"
}
// 游戏相关字段与推送结果合并写入 ResultData,便于后续查询还原(新模型无独立列)。
updates["result_data"] = buildShipNotifyResultData(order.ResultData, in, shippedAt)
if err := s.db.Model(&model.FulfillmentOrder{}).Where("id = ?", order.ID).Updates(updates).Error; err != nil {
resultData := buildShipNotifyResultData(order.ResultData, in, shippedAt)
updates["result_data"] = resultData
var updated model.FulfillmentOrder
if err := s.db.Transaction(func(tx *gorm.DB) error {
if err := tx.Model(&model.FulfillmentOrder{}).Where("id = ?", order.ID).Updates(updates).Error; err != nil {
return err
}
jobStatus := model.FulfillmentJobStatusProcessing
if nextStatus == model.FulfillmentStatusSucceeded {
jobStatus = model.FulfillmentJobStatusSucceeded
} else if nextStatus == model.FulfillmentStatusFailed {
jobStatus = model.FulfillmentJobStatusFailed
}
jobUpdates := map[string]interface{}{
"status": jobStatus,
"result_payload": resultData,
"last_error": in.FailReason,
}
if in.ProviderOrderNo != "" {
jobUpdates["provider_order_no"] = in.ProviderOrderNo
}
if err := tx.Model(&model.FulfillmentJob{}).Where("order_id = ?", order.ID).Updates(jobUpdates).Error; err != nil {
return err
}
metadata := shipNotifyAuditMetadata(in, nextStatus, msg)
if err := writeAudit(tx, &order.MerchantID, nil, nil, "ship.notify", "fulfillment_order", order.OrderNo, metadata); err != nil {
return err
}
if err := tx.Preload("MerchantProduct.Product").First(&updated, order.ID).Error; err != nil {
return err
}
if s.callbacks != nil {
if err := s.callbacks.Enqueue(tx, order.MerchantID, "order.fulfillment.updated", orderCallbackData(&updated)); err != nil {
return err
}
}
return nil
}); err != nil {
return nil, err
}
_ = s.writeShipNotifyAudit(order, in, nextStatus, msg)
return &ShipNotifyResult{
OrderNo: order.OrderNo,
@@ -945,6 +978,10 @@ func (s *FulfillmentService) HandleShipNotify(in ShipNotifyInput) (*ShipNotifyRe
// writeShipNotifyAudit 将上游推送留痕写入 AuditLog(替代旧的 ShipLog 表)。
func (s *FulfillmentService) writeShipNotifyAudit(order *model.FulfillmentOrder, in ShipNotifyInput, resultStatus, message string) error {
return writeAudit(s.db, &order.MerchantID, nil, nil, "ship.notify", "fulfillment_order", order.OrderNo, shipNotifyAuditMetadata(in, resultStatus, message))
}
func shipNotifyAuditMetadata(in ShipNotifyInput, resultStatus, message string) map[string]interface{} {
metadata := map[string]interface{}{
"ship_status": in.ShipStatus,
"provider_order_no": in.ProviderOrderNo,
@@ -953,7 +990,7 @@ func (s *FulfillmentService) writeShipNotifyAudit(order *model.FulfillmentOrder,
"message": message,
"payload": in.RawPayload,
}
return writeAudit(s.db, &order.MerchantID, nil, nil, "ship.notify", "fulfillment_order", order.OrderNo, metadata)
return metadata
}
// buildShipNotifyResultData 把推送结果与游戏字段合并进 ResultData JSON。
@@ -20,6 +20,8 @@ func newServiceTestDB(t *testing.T) *gorm.DB {
&model.WalletLedgerEntry{},
&model.FulfillmentOrder{},
&model.FulfillmentJob{},
&model.CallbackSubscription{},
&model.CallbackDelivery{},
&model.AuditLog{},
)
}
@@ -455,3 +457,70 @@ func TestCreateTestOrderSupportsFailedFulfillableStatus(t *testing.T) {
t.Fatalf("failed test order should be re-fulfillable, got %+v", openOrder)
}
}
func TestHandleShipNotifyUpdatesOrderJobAndEnqueuesMerchantCallback(t *testing.T) {
db := newServiceTestDB(t)
merchantID, product := seedFulfillmentMerchant(t, db, "merchant-source-notify", 1000, 2, 100)
codec, err := NewSecretCodec("test-master-key")
if err != nil {
t.Fatalf("codec: %v", err)
}
callbackSvc := NewCallbackService(db, codec)
svc := NewFulfillmentService(db, callbackSvc)
created, err := svc.CreateOrder(CreateFulfillmentOrderInput{
MerchantID: merchantID,
APIClientID: 31,
ClientOrderNo: "client-source-notify",
SKU: product.SKU,
})
if err != nil {
t.Fatalf("create order: %v", err)
}
if _, err := callbackSvc.CreateSubscription(merchantID, CreateCallbackInput{
Name: "履约回调",
URL: "https://example.com/callback",
Events: "order.fulfillment.updated",
}, 7); err != nil {
t.Fatalf("create callback subscription: %v", err)
}
result, err := svc.HandleShipNotify(ShipNotifyInput{
OrderNo: created.Order.OrderNo,
ShipStatus: "success",
ProviderOrderNo: "SRC-10001",
FailReason: "ignored",
})
if err != nil {
t.Fatalf("handle ship notify: %v", err)
}
if result.Status != "delivered" {
t.Fatalf("unexpected notify result: %+v", result)
}
var order model.FulfillmentOrder
if err := db.First(&order, created.Order.ID).Error; err != nil {
t.Fatalf("query order: %v", err)
}
if order.FulfillmentStatus != model.FulfillmentStatusSucceeded || order.ProviderOrderNo != "SRC-10001" || order.DeliveredAt == nil {
t.Fatalf("unexpected order after source notify: %+v", order)
}
var job model.FulfillmentJob
if err := db.Where("order_id = ?", created.Order.ID).First(&job).Error; err != nil {
t.Fatalf("query fulfillment job: %v", err)
}
if job.Status != model.FulfillmentJobStatusSucceeded || job.ProviderOrderNo != "SRC-10001" {
t.Fatalf("unexpected job after source notify: %+v", job)
}
var delivery model.CallbackDelivery
if err := db.Where("merchant_id = ? AND event = ?", merchantID, "order.fulfillment.updated").First(&delivery).Error; err != nil {
t.Fatalf("query callback delivery: %v", err)
}
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)
}
var auditCount int64
db.Model(&model.AuditLog{}).Where("entity_id = ? AND action = ?", created.Order.OrderNo, "ship.notify").Count(&auditCount)
if auditCount != 1 {
t.Fatalf("expected one ship notify audit, got %d", auditCount)
}
}