feat(admin): add atomic task claim leases
This commit is contained in:
+86
-27
@@ -48,7 +48,7 @@
|
||||
"code": "version_conflict",
|
||||
"message": "任务已变化,请刷新后重选",
|
||||
"retryable": false,
|
||||
"request_id": "018f..."
|
||||
"request_id": "c3c9f507-7473-4fa6-8d71-8786c34c6301"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -85,7 +85,7 @@
|
||||
|
||||
```json
|
||||
{
|
||||
"create_key": "018f...",
|
||||
"create_key": "d3c9f507-7473-4fa6-8d71-8786c34c6301",
|
||||
"title": "纯棉短袖",
|
||||
"product_url": "https://mobile.yangkeduo.com/goods.html?goods_id=937122477375",
|
||||
"sku_color": "黑色CHA(纯棉)",
|
||||
@@ -103,10 +103,10 @@
|
||||
|
||||
```json
|
||||
{
|
||||
"start_key": "018f...",
|
||||
"start_key": "63c9f507-7473-4fa6-8d71-8786c34c6301",
|
||||
"tasks": [
|
||||
{"task_id": "018f-task-1", "expected_task_version": 1},
|
||||
{"task_id": "018f-task-2", "expected_task_version": 1}
|
||||
{"task_id": "83c9f507-7473-4fa6-8d71-8786c34c6301", "expected_task_version": 1},
|
||||
{"task_id": "93c9f507-7473-4fa6-8d71-8786c34c6301", "expected_task_version": 1}
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -125,11 +125,11 @@
|
||||
|
||||
```json
|
||||
{
|
||||
"start_key": "018f...",
|
||||
"start_key": "63c9f507-7473-4fa6-8d71-8786c34c6301",
|
||||
"authorized_count": 2,
|
||||
"tasks": [
|
||||
{"task_id": "018f-task-1", "task_version": 2, "authorization_id": "018f-auth-1"},
|
||||
{"task_id": "018f-task-2", "task_version": 2, "authorization_id": "018f-auth-2"}
|
||||
{"task_id": "83c9f507-7473-4fa6-8d71-8786c34c6301", "task_version": 2, "authorization_id": "a3c9f507-7473-4fa6-8d71-8786c34c6301"},
|
||||
{"task_id": "93c9f507-7473-4fa6-8d71-8786c34c6301", "task_version": 2, "authorization_id": "b3c9f507-7473-4fa6-8d71-8786c34c6301"}
|
||||
],
|
||||
"payment_automated": false
|
||||
}
|
||||
@@ -160,7 +160,7 @@
|
||||
|
||||
```json
|
||||
{
|
||||
"device_id": "desk-01",
|
||||
"device_id": "e3c9f507-7473-4fa6-8d71-8786c34c6301",
|
||||
"client_version": "0.1.0",
|
||||
"adb_serial": "192.168.0.173:5555",
|
||||
"android_release": "16",
|
||||
@@ -173,12 +173,22 @@
|
||||
|
||||
### `POST /api/v1/tasks/claim-next`
|
||||
|
||||
请求携带 `device_id`、`session_id`、`claim_request_id`。领取与授权绑定且具租约:
|
||||
设备 id 只来自已经认证的 `X-CMBuyer-Device-ID`,不得放进 JSON。请求体上限 4096 字节,只接受
|
||||
以下两个字段;二者都必须是规范小写 UUIDv4,未知字段、重复字段和额外 JSON 均拒绝:
|
||||
|
||||
```json
|
||||
{
|
||||
"session_id": "23c9f507-7473-4fa6-8d71-8786c34c6301",
|
||||
"claim_request_id": "33c9f507-7473-4fa6-8d71-8786c34c6301"
|
||||
}
|
||||
```
|
||||
|
||||
成功领取或同一会话恢复返回 `200`:
|
||||
|
||||
```json
|
||||
{
|
||||
"task": {
|
||||
"id": "018f-task",
|
||||
"id": "13c9f507-7473-4fa6-8d71-8786c34c6301",
|
||||
"version": 3,
|
||||
"title": "纯棉短袖",
|
||||
"product_url": "https://mobile.yangkeduo.com/goods.html?goods_id=937122477375",
|
||||
@@ -189,24 +199,71 @@
|
||||
"max_total_price": "30.00"
|
||||
},
|
||||
"authorization": {
|
||||
"id": "018f-auth",
|
||||
"id": "73c9f507-7473-4fa6-8d71-8786c34c6301",
|
||||
"task_version": 2,
|
||||
"expires_at": "2026-08-04T10:00:00Z"
|
||||
},
|
||||
"attempt": {
|
||||
"id": "018f-attempt",
|
||||
"claim_token": "opaque-single-claim-token",
|
||||
"id": "53c9f507-7473-4fa6-8d71-8786c34c6301",
|
||||
"claim_token": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
|
||||
"claim_generation": 1,
|
||||
"lease_expires_at": "2026-08-04T09:05:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- 只返回有 `ACTIVE` 授权的 `PENDING`;服务端在一个事务中转为 `CLAIMED` 并创建 attempt。
|
||||
- 同一 `claim_request_id` 同载荷重放同一结果;并发设备只有一个成功。
|
||||
- 一个设备有未结束领取时优先重放该领取,不能悄悄领第二条。
|
||||
- 设备认证发生在 Content-Type 解析和 body 读取前。事务的第一条数据库业务语句取得 SQLite 写入
|
||||
位置并再次条件确认设备仍为 `ACTIVE`;之后才允许查幂等记录、候选或返回 EMPTY/冲突。
|
||||
- 只返回 `PENDING + ACTIVE + 严格未过期` 且 task/version/规格/数量/总价快照完全一致的最早授权;
|
||||
服务端在同一事务中创建唯一 attempt/claim/request,并转为 `CLAIMED`。
|
||||
- 同一 `claim_request_id` 同设备、同 session 稳定重放原结果;同键异载荷返回
|
||||
`409 {"error":"idempotency_conflict"}`。没有候选返回空 `204`,且 EMPTY 也持久化稳定重放。
|
||||
- claim 持久化完整成功响应快照;领取后的 task/authorization 源行变化不得让旧 request 的标题、规格、
|
||||
数量、金额、版本或到期时间漂移。源快照不一致时,新恢复/续租失败闭合。
|
||||
- 一个设备最多有一个未关闭 claim。同 session 且租约有效时重放原 attempt;同一 attempt 已按服务端
|
||||
首事件原子进入 `ORDERING` 时也只在 task version 恰好为 claim 版本 +1 时恢复。不同 session、租约
|
||||
过期或业务状态异常固定返回 `409 {"error":"claim_requires_manual"}`,不释放、不转领、不新建 attempt。
|
||||
- `claim_token` 是 32 字节 HMAC 的 64 位小写十六进制表示,只证明一个 attempt 的归属,不是提交许可。
|
||||
SQLite 仅保存随机 nonce 与 token SHA-256;同一 secret 重启后重建相同 token,错误 secret 拒绝启动。
|
||||
- 响应不得包含自由动作脚本、CSS/XPath、通用坐标或支付能力。
|
||||
|
||||
### `POST /api/v1/tasks/{id}/lease/renew`
|
||||
|
||||
请求体同样限 4096 字节并执行严格 JSON 校验:
|
||||
|
||||
```json
|
||||
{
|
||||
"renew_request_id": "43c9f507-7473-4fa6-8d71-8786c34c6301",
|
||||
"session_id": "23c9f507-7473-4fa6-8d71-8786c34c6301",
|
||||
"attempt_id": "53c9f507-7473-4fa6-8d71-8786c34c6301",
|
||||
"claim_generation": 1,
|
||||
"claim_token": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
|
||||
"expected_lease_expires_at": "2026-08-04T09:05:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
成功返回 `200`;响应不回显 token:
|
||||
|
||||
```json
|
||||
{
|
||||
"task_id": "13c9f507-7473-4fa6-8d71-8786c34c6301",
|
||||
"attempt_id": "53c9f507-7473-4fa6-8d71-8786c34c6301",
|
||||
"claim_generation": 1,
|
||||
"lease_expires_at": "2026-08-04T09:06:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
- 原设备/session/attempt/generation/token 必须同时匹配,当前租约与授权都必须严格晚于服务端 UTC
|
||||
当前时间,`expected_lease_expires_at` 必须逐字等于数据库当前值;边界相等即过期且不能复活。
|
||||
- 新到期时间是 `min(server_now + CMBUYER_CLAIM_LEASE_TTL, authorization.expires_at)`。续租不改变
|
||||
token、generation、任务版本或业务状态。
|
||||
- 成功续租先持久化 request 与响应;同一 `renew_request_id` 同载荷只重放旧响应,不再次 CAS 或延长。
|
||||
同键异载荷返回 `idempotency_conflict`;非当前 claim 固定返回 `claim_not_current`,两者均为 `409`。
|
||||
|
||||
claim/renew 的格式错误固定为 `400 {"error":"invalid_request"}`,超限为
|
||||
`413 {"error":"request_too_large"}`,Content-Type 错误为
|
||||
`415 {"error":"unsupported_media_type"}`;设备认证/事务内撤销为无诊断 `401`,存储故障为无诊断 `503`。
|
||||
|
||||
### 事件与证据
|
||||
|
||||
事件只包含固定 `step` / `outcome` / `reason_code` 和非敏感摘要。禁止把完整 XML、地址、手机号、
|
||||
@@ -234,8 +291,10 @@
|
||||
- 恰好一个带 `Content-Type: image/png` 的显式文件;除上述六个元数据字段外,未知或重复字段均拒绝。
|
||||
- 单文件最多 10 MiB、单边最多 8192 px、总像素最多 16,777,216;服务端校验 PNG 魔数、完整解码、
|
||||
字节数、尺寸与调用方声明的 64 位小写 SHA-256。
|
||||
- `attempt_id` 必须由数据库复合外键证明属于 URL 中的 task。认证必须先于 Content-Type 解析和请求体读取。
|
||||
- 同一设备主体和 `upload_key` 的同载荷重放返回原资产;任务、attempt、截图或元数据变化返回 `409`。
|
||||
- `attempt_id` 必须由数据库复合外键证明属于 URL 中的 task,且首次写入必须存在由认证设备持有的
|
||||
未关闭 claim;设备 A 不能向设备 B 的 attempt 上传。认证必须先于 Content-Type 解析和请求体读取。
|
||||
- 同一设备主体和 `upload_key` 的同载荷重放返回原资产;即使 claim 后续由人工关闭,已成功资产仍先
|
||||
重放历史结果。关闭后不得用新 upload key 写新证据;任务、attempt、截图或元数据变化返回 `409`。
|
||||
- 首次成功返回 `201`,幂等重放返回 `200`。响应只含资产 id、关联 id、kind/tier、hash、字节数、
|
||||
MIME、宽高和采集时间,不含设备 token、原文件名或存储路径。
|
||||
- 生产上传使用逐请求 SQLite 设备认证;空凭据库、未知或已撤销设备均拒绝。不得使用管理员 session、
|
||||
@@ -247,11 +306,11 @@
|
||||
|
||||
```json
|
||||
{
|
||||
"fence_key": "018f-fence-request",
|
||||
"task_id": "018f-task",
|
||||
"fence_key": "e3c9f507-7473-4fa6-8d71-8786c34c6301",
|
||||
"task_id": "13c9f507-7473-4fa6-8d71-8786c34c6301",
|
||||
"expected_task_version": 3,
|
||||
"authorization_id": "018f-auth",
|
||||
"claim_token": "opaque-single-claim-token",
|
||||
"authorization_id": "73c9f507-7473-4fa6-8d71-8786c34c6301",
|
||||
"claim_token": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
|
||||
"selected_color": "黑色CHA(纯棉)",
|
||||
"selected_size": "M(建议100-115)",
|
||||
"gate1_unit_price": "12.88",
|
||||
@@ -271,7 +330,7 @@
|
||||
|
||||
```json
|
||||
{
|
||||
"submission_id": "018f-submission",
|
||||
"submission_id": "f3c9f507-7473-4fa6-8d71-8786c34c6301",
|
||||
"status": "FENCED",
|
||||
"click_permitted": true,
|
||||
"submit_text": "提交订单"
|
||||
@@ -288,10 +347,10 @@
|
||||
|
||||
```json
|
||||
{
|
||||
"result_key": "018f-result",
|
||||
"attempt_id": "018f-attempt",
|
||||
"result_key": "03c9f507-7473-4fa6-8d71-8786c34c6301",
|
||||
"attempt_id": "53c9f507-7473-4fa6-8d71-8786c34c6301",
|
||||
"observation": "SUBMITTED",
|
||||
"evidence_asset_id": "018f-asset"
|
||||
"evidence_asset_id": "63c9f507-7473-4fa6-8d71-8786c34c6301"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
Reference in New Issue
Block a user