feat(gateway): 下单任务支持 callback_url 终结类事件异步通知
- POST /api/orders 新增可选 callback_url(仅 http/https,其余 422) - 仅终结类事件各通知一次:terminal report 推入终态(succeeded/failed/ needs_human)、租约过期被 sweep 置 stale;中间态与终结后的监控上报不通知 - 幂等重发不更新既有任务的回调地址;投递 best-effort 单次尝试,失败只记日志 - CallbackNotifier 发后不管(create_task + 在途任务强引用),关停等在途发完; 生产路径按回调地址逐次构造客户端,满足统一出站代理策略(test_proxy.py) - tasks 表加 callback_url 列,GatewayDB.start() 内置迁移兼容既有库 - 新增 RAKUTEN_CALLBACK_TIMEOUT_SECONDS(默认 10);文档补 §4.8; openapi.json 重新导出(gitignore 未跟踪);新增 17 条测试,全量 492 通过
This commit is contained in:
+48
-2
@@ -74,6 +74,7 @@ CREATE TABLE tasks (
|
||||
task_id TEXT PRIMARY KEY, -- 幂等键,见 §4.2
|
||||
site TEXT NOT NULL, -- rakuten(交易服务仅覆盖乐天市场)
|
||||
intent_json TEXT NOT NULL, -- 下单意图原文,gateway 不解释内容
|
||||
callback_url TEXT, -- 终结类事件通知地址(可选),见 §4.8
|
||||
status TEXT NOT NULL, -- 见 §3
|
||||
lease_owner TEXT, -- worker_id
|
||||
lease_expires_at TEXT, -- ISO8601 UTC
|
||||
@@ -116,14 +117,16 @@ CREATE TABLE workers (
|
||||
"variant_id": "...", // 多规格商品必填
|
||||
"options": {},
|
||||
"max_total_yen": 30000 // 可选,覆盖本次的金额上限
|
||||
}
|
||||
},
|
||||
"callback_url": "https://upstream.example.com/hooks/rakuten-order" // 可选,终结类事件通知,见 §4.8
|
||||
}
|
||||
```
|
||||
|
||||
响应 `data`:`{"task_id": "...", "status": "queued", "created": true}`
|
||||
|
||||
**幂等**:同一个 `task_id` 重复提交不新建任务,返回既有任务且 `created=false`。
|
||||
上游重发不会变成两单。
|
||||
上游重发不会变成两单。注意 `callback_url` 只在首次创建时写入,幂等重发不更新
|
||||
既有任务的任何字段——要换通知地址必须用新 `task_id`。
|
||||
|
||||
### 4.3 GET /api/orders/lease — 本地长轮询领取
|
||||
|
||||
@@ -198,6 +201,48 @@ CREATE TABLE workers (
|
||||
付款期限监控**不在 gateway**:本地机 7×24 在线,那套逻辑放本地(§6),
|
||||
gateway 不重复实现一遍定时器。
|
||||
|
||||
### 4.8 回调通知(callback_url,2026-08-16)
|
||||
|
||||
上游提交任务时可带 `callback_url`(http/https 绝对 URL,其余提交时 422)。
|
||||
登记后网关在**终结类事件**发生时向该地址 POST 一条 JSON 通知,上游免去轮询。
|
||||
|
||||
触发时机只有两类,每类一次:
|
||||
|
||||
1. **任务到达终态**(`succeeded` / `failed` / `needs_human`):worker 的
|
||||
`terminal=true` report 真正把任务推入终态的那一次。中间态 report
|
||||
(in_cart / ordered / awaiting_payment 非 terminal)不通知;任务已终结后的
|
||||
后续 report(如付款后监控的 shipped/delivered)也不重复通知。
|
||||
2. **任务被置 stale**:租约过期被 sweep 标记(见 §5)。上游收到后应走人工
|
||||
排查 + reclaim 流程。
|
||||
|
||||
终态事件的 payload(无信封,直接是 JSON 对象):
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"task_id": "po-20260727-0001",
|
||||
"site": "rakuten",
|
||||
"event": "terminal",
|
||||
"status": "succeeded", // 终态:succeeded / failed / needs_human
|
||||
"state": "paid", // 本次 report 的订单状态
|
||||
"payable_yen": 12800,
|
||||
"pay_deadline": "2026-07-30T14:59:00Z",
|
||||
"site_order_id": "266123-20260727-0001234",
|
||||
"evidence_ref": "po-20260727-0001/05-payment",
|
||||
"detail": "付款完成",
|
||||
"reported_at": "2026-07-27T09:15:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
stale 事件的 payload:`{"task_id", "site", "event": "stale", "status": "stale",
|
||||
"detail"}`。
|
||||
|
||||
投递语义是 **best-effort 单次尝试**:超时(`RAKUTEN_CALLBACK_TIMEOUT_SECONDS`,
|
||||
默认 10 秒)、非 2xx、连接错误都只记网关日志,不重试、不阻塞 report/sweep
|
||||
主流程。回调只是「省轮询」的提示,可能丢失——**权威状态仍以
|
||||
`GET /api/orders/{task_id}` 为准**,上游应保留对账轮询(可降低频率)。
|
||||
回调出站沿用统一代理策略(`app/shared/proxy.py`,按目标 host 判定 bypass);
|
||||
上游如需鉴权,可把 token 编进 callback_url 的 query 里自行校验。
|
||||
|
||||
## 5. 租约过期:绝不自动重投(本文最关键的一条)
|
||||
|
||||
常规任务队列在租约超时后会把任务放回队列重新分发。**这里必须禁止**:本地可能已经
|
||||
@@ -280,6 +325,7 @@ gateway 侧:
|
||||
- `RAKUTEN_LEASE_TTL_SECONDS`(默认 300)
|
||||
- `RAKUTEN_LEASE_MAX_WAIT_SECONDS`(默认 60)
|
||||
- `RAKUTEN_WORKER_OFFLINE_ALERT_SECONDS`(默认 300)
|
||||
- `RAKUTEN_CALLBACK_TIMEOUT_SECONDS`(默认 10,回调通知单次 HTTP 超时,见 §4.8)
|
||||
|
||||
trading 侧新增:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user