修正发货回调流程
This commit is contained in:
@@ -144,74 +144,6 @@ func (h *OpenV1Handler) CancelOrder(c *gin.Context) {
|
|||||||
response.OK(c, buildOpenOrderResponse(order))
|
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) {
|
func (h *OpenV1Handler) GetWallet(c *gin.Context) {
|
||||||
openlog.Info(c, "get_wallet start")
|
openlog.Info(c, "get_wallet start")
|
||||||
wallet, err := h.fulfillmentSvc.GetWallet(middleware.GetMerchantID(c))
|
wallet, err := h.fulfillmentSvc.GetWallet(middleware.GetMerchantID(c))
|
||||||
|
|||||||
@@ -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.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.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/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)
|
clientOpen.GET("/wallet", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureWallet), middleware.RequireAPIScope("wallet:read"), h.Open.GetWallet)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -928,13 +928,46 @@ func (s *FulfillmentService) HandleShipNotify(in ShipNotifyInput) (*ShipNotifyRe
|
|||||||
msg = "订单已标记为发货中"
|
msg = "订单已标记为发货中"
|
||||||
}
|
}
|
||||||
|
|
||||||
// 游戏相关字段与推送结果合并写入 ResultData,便于后续查询还原(新模型无独立列)。
|
resultData := buildShipNotifyResultData(order.ResultData, in, shippedAt)
|
||||||
updates["result_data"] = buildShipNotifyResultData(order.ResultData, in, shippedAt)
|
updates["result_data"] = resultData
|
||||||
|
var updated model.FulfillmentOrder
|
||||||
if err := s.db.Model(&model.FulfillmentOrder{}).Where("id = ?", order.ID).Updates(updates).Error; err != nil {
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
_ = s.writeShipNotifyAudit(order, in, nextStatus, msg)
|
|
||||||
|
|
||||||
return &ShipNotifyResult{
|
return &ShipNotifyResult{
|
||||||
OrderNo: order.OrderNo,
|
OrderNo: order.OrderNo,
|
||||||
@@ -945,6 +978,10 @@ func (s *FulfillmentService) HandleShipNotify(in ShipNotifyInput) (*ShipNotifyRe
|
|||||||
|
|
||||||
// writeShipNotifyAudit 将上游推送留痕写入 AuditLog(替代旧的 ShipLog 表)。
|
// writeShipNotifyAudit 将上游推送留痕写入 AuditLog(替代旧的 ShipLog 表)。
|
||||||
func (s *FulfillmentService) writeShipNotifyAudit(order *model.FulfillmentOrder, in ShipNotifyInput, resultStatus, message string) error {
|
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{}{
|
metadata := map[string]interface{}{
|
||||||
"ship_status": in.ShipStatus,
|
"ship_status": in.ShipStatus,
|
||||||
"provider_order_no": in.ProviderOrderNo,
|
"provider_order_no": in.ProviderOrderNo,
|
||||||
@@ -953,7 +990,7 @@ func (s *FulfillmentService) writeShipNotifyAudit(order *model.FulfillmentOrder,
|
|||||||
"message": message,
|
"message": message,
|
||||||
"payload": in.RawPayload,
|
"payload": in.RawPayload,
|
||||||
}
|
}
|
||||||
return writeAudit(s.db, &order.MerchantID, nil, nil, "ship.notify", "fulfillment_order", order.OrderNo, metadata)
|
return metadata
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildShipNotifyResultData 把推送结果与游戏字段合并进 ResultData JSON。
|
// buildShipNotifyResultData 把推送结果与游戏字段合并进 ResultData JSON。
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ func newServiceTestDB(t *testing.T) *gorm.DB {
|
|||||||
&model.WalletLedgerEntry{},
|
&model.WalletLedgerEntry{},
|
||||||
&model.FulfillmentOrder{},
|
&model.FulfillmentOrder{},
|
||||||
&model.FulfillmentJob{},
|
&model.FulfillmentJob{},
|
||||||
|
&model.CallbackSubscription{},
|
||||||
|
&model.CallbackDelivery{},
|
||||||
&model.AuditLog{},
|
&model.AuditLog{},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -455,3 +457,70 @@ func TestCreateTestOrderSupportsFailedFulfillableStatus(t *testing.T) {
|
|||||||
t.Fatalf("failed test order should be re-fulfillable, got %+v", openOrder)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export const paymentStatusTable: ParamSpec[] = [
|
|||||||
{ name: 'cancelled', 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: '调用方传入的幂等单号' },
|
||||||
@@ -50,8 +50,8 @@ const orderFields: ParamSpec[] = [
|
|||||||
{ name: 'currency', type: 'string', desc: '货币,默认 POINT' },
|
{ name: 'currency', type: 'string', desc: '货币,默认 POINT' },
|
||||||
{ name: 'buyer_reference', type: 'string', desc: '买家标识/备注' },
|
{ name: 'buyer_reference', type: 'string', desc: '买家标识/备注' },
|
||||||
{ name: 'data', type: 'object', desc: '下单时透传的请求数据(原样回显)' },
|
{ name: 'data', type: 'object', desc: '下单时透传的请求数据(原样回显)' },
|
||||||
{ name: 'result', type: 'object', desc: '履约器回传的结果数据(原样回显)' },
|
{ name: 'result', type: 'object', desc: '发货平台回调的结果数据(原样回显)' },
|
||||||
{ name: 'provider_order_no', type: 'string', desc: '履约器侧单号' },
|
{ name: 'provider_order_no', type: 'string', desc: '发货平台侧单号' },
|
||||||
{ name: 'failure_reason', type: 'string', desc: '最近一次失败原因' },
|
{ name: 'failure_reason', type: 'string', desc: '最近一次失败原因' },
|
||||||
{ name: 'created_at', type: 'datetime', desc: '创建时间' },
|
{ name: 'created_at', type: 'datetime', desc: '创建时间' },
|
||||||
{ name: 'delivered_at', type: 'datetime', desc: '履约成功时间' },
|
{ name: 'delivered_at', type: 'datetime', desc: '履约成功时间' },
|
||||||
@@ -200,32 +200,6 @@ export const endpoints: EndpointSpec[] = [
|
|||||||
'已进入 processing/succeeded 的订单不可取消,返回 400。',
|
'已进入 processing/succeeded 的订单不可取消,返回 400。',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: 'ship-notify',
|
|
||||||
method: 'POST',
|
|
||||||
path: '/api/client/v1/orders/{order_no}/ship-notify',
|
|
||||||
summary: '履约器回传履约结果(processing/success/failed)',
|
|
||||||
scope: 'fulfillment:write',
|
|
||||||
pathParams: [{ name: 'order_no', type: 'string', required: true, desc: '平台订单号', example: 'FO20260730000123' }],
|
|
||||||
bodyParams: [
|
|
||||||
{ name: 'ship_status', type: 'enum(processing|success|failed)', required: true, desc: '履约结果', example: 'success' },
|
|
||||||
{ name: 'provider_order_no', type: 'string', desc: '履约器侧单号', example: 'SRC20260720001' },
|
|
||||||
{ name: 'fail_reason', type: 'string', desc: '失败原因(failed 时建议填)' },
|
|
||||||
{ name: 'result', type: 'object', desc: '履约结果数据,原样存储并回显' },
|
|
||||||
],
|
|
||||||
requestExample: `{
|
|
||||||
"ship_status": "success",
|
|
||||||
"provider_order_no": "SRC20260720001",
|
|
||||||
"result": { "delivered_at": "2026-07-30T10:05:00+08:00" }
|
|
||||||
}`,
|
|
||||||
responseExample: orderResponseExample,
|
|
||||||
responseFields: orderFields,
|
|
||||||
notes: [
|
|
||||||
'processing → fulfillment_status=processing;success → succeeded;failed → failed(可重试)。',
|
|
||||||
'已 succeeded 的订单再推 success 仍返回成功,不重复处理(幂等)。',
|
|
||||||
'已 succeeded 推 failed、或 cancelled 订单推送 → 返回 400 拒绝。',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: 'get-wallet',
|
key: 'get-wallet',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
|
|||||||
@@ -67,7 +67,6 @@ const scopeOptions = [
|
|||||||
{ value: 'orders:read', label: '订单读取' },
|
{ value: 'orders:read', label: '订单读取' },
|
||||||
{ value: 'orders:write', label: '订单写入' },
|
{ value: 'orders:write', label: '订单写入' },
|
||||||
{ value: 'fulfillment:read', label: '履约读取' },
|
{ value: 'fulfillment:read', label: '履约读取' },
|
||||||
{ value: 'fulfillment:write', label: '履约写入' },
|
|
||||||
{ value: 'wallet:read', label: '钱包读取' },
|
{ value: 'wallet:read', label: '钱包读取' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -35,9 +35,9 @@ export default function OpenApiDocs() {
|
|||||||
开放接口
|
开放接口
|
||||||
</Title>
|
</Title>
|
||||||
<Paragraph type="secondary">
|
<Paragraph type="secondary">
|
||||||
面向商户系统、履约器和外部平台调用的通用开放 API。凭证在「商户中心 / API 客户端」创建,
|
面向商户系统调用的开放 API。凭证在「商户中心 / API 客户端」创建,
|
||||||
采用 <Text code>X-App-Key</Text> + HMAC-SHA256 签名鉴权。
|
采用 <Text code>X-App-Key</Text> + HMAC-SHA256 签名鉴权。
|
||||||
原 <Text code>/api/open/v1</Text> 保留给上游发货对接,不在本页展示。
|
<Text code>/api/open/v1</Text> 仅保留给发货平台查询与回传,不在本页展示。
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
|
|
||||||
<Tabs
|
<Tabs
|
||||||
@@ -80,7 +80,7 @@ function OverviewTab() {
|
|||||||
<li>在「商户中心 / API 客户端」创建凭证,获得 <Text code>AppKey</Text> 与 <Text code>AppSecret</Text>。</li>
|
<li>在「商户中心 / API 客户端」创建凭证,获得 <Text code>AppKey</Text> 与 <Text code>AppSecret</Text>。</li>
|
||||||
<li>每次请求携带 <Text code>X-App-Key / X-Timestamp / X-Nonce / X-Sign</Text> 四个鉴权头。</li>
|
<li>每次请求携带 <Text code>X-App-Key / X-Timestamp / X-Nonce / X-Sign</Text> 四个鉴权头。</li>
|
||||||
<li>调用「商品列表」拿到可售 <Text code>sku</Text>,调用「下单」创建订单并扣款。</li>
|
<li>调用「商品列表」拿到可售 <Text code>sku</Text>,调用「下单」创建订单并扣款。</li>
|
||||||
<li>履约器通过「发货回传」回写 processing/success/failed,状态可由「查询订单」轮询。</li>
|
<li>拿到 <Text code>order_no</Text> 后进入发货链接;订单状态由发货平台回调更新,可通过「查询订单」轮询。</li>
|
||||||
</ol>
|
</ol>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -234,13 +234,13 @@ function StatusTab() {
|
|||||||
↓
|
↓
|
||||||
POST /orders 下单(幂等,Idempotency-Key = client_order_no)
|
POST /orders 下单(幂等,Idempotency-Key = client_order_no)
|
||||||
↓
|
↓
|
||||||
(履约器)POST /orders/{order_no}/ship-notify ship_status=processing
|
拿响应里的 order_no 进入发货平台链接
|
||||||
↓
|
↓
|
||||||
按 product.sku 发货
|
发货平台查询订单并执行发货
|
||||||
↓
|
↓
|
||||||
POST /orders/{order_no}/ship-notify ship_status=success 或 failed
|
发货平台通过上游回调更新 fulfillment_status
|
||||||
↓
|
↓
|
||||||
(可选)GET /orders/{order_no} 轮询确认 fulfillment_status=succeeded`}</pre>
|
GET /orders/{order_no} 轮询,或接收 order.fulfillment.updated 回调`}</pre>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
|
|||||||
Reference in New Issue
Block a user