面向 Client 开发者的实操文档,目标是让一台新设备出现在 Admin 的 客户端列表里。文中所有请求和响应都是在真实运行的 Admin 上跑出来的。 新增 docs/admin/07-设备登记联调手册.md - 先讲清"没有注册接口,登记是 claim 的副作用" - **重点提示 204 不是错误**:新设备第一次 claim 必然 204, 它同时意味着登记成功。这是最容易被 Client 误判成失败的地方 - 设备号必须持久化、永不变——变了 Admin 会当成新机器, 列表里会堆一串僵尸记录 - 设备名只在首次登记时采纳,之后 Admin 侧改名不会被覆盖。 这个行为会让人困惑("我改了 Client 怎么 Admin 没变"),先说清楚 - 四步验证流程 + 可直接复制的 curl + 手工插测试任务的 SQL - 7 条常见问题,含"列表里多出好几台一样的机器"这类实际会碰到的 - 列出 Client 侧待办清单,标明前两条做完就能登记成功 定案 client.name(原 [待定]) Client 设置页本来就有「设备名」输入框(deviceNameInput,50 字上限), 只是没接进 ClientInfo。Admin 仍容忍它缺失,缺了用 X-Client-Id 兜底, 所以 Client 先不发也不影响登记。 同步更新 - docs/client/04-admin-api-contract.md §5:请求示例补 client 字段, 加"204 不是错误"的必须项,并指向本手册 - docs/README.md:两处导航加入口,Client 侧"对接 Admin"那行也指过来 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
307 lines
11 KiB
Markdown
307 lines
11 KiB
Markdown
# 04 Client–Admin API v1 契约
|
||
|
||
- 文档状态:基线草案,待 Client 与 Admin 联合评审
|
||
- 基础路径:`/api/v1/client`
|
||
- 编码:UTF-8 JSON
|
||
- 时间:带时区 ISO 8601,服务端优先返回 UTC
|
||
- 金额:人民币分整数
|
||
|
||
本文档中 `[必须]` / `[建议]` / `[待定]` 的含义见 [文档索引](../README.md#文档标注说明)。没有标注的默认是 `[必须]`。看不懂的词(幂等、Outbox)查 [术语表](00-glossary.md)。
|
||
|
||
## 1. 设计原则
|
||
|
||
**核心:Admin 是调度器,Client 是执行器,执行器不参与调度决策。**
|
||
|
||
- Client 只有三个动作:领一个任务、提交结果、提交失败。**没有任何"去问 Admin 现在怎么想"的调用。**
|
||
- Client 拿到任务就做完,中途不管任务是否被取消、是否被重派。
|
||
- Admin 负责任务的创建、更新、取消和派发(含重派),这些 Client 一律不感知。
|
||
- 结果提交必须幂等;网络重试不能创建重复结果。
|
||
- Admin **必须无条件接受**已派发过的 Client 提交的结果,理由见 §6.1。
|
||
- 任务和结果使用版本化结构,未知字段应允许向前兼容。
|
||
- Admin 业务错误返回稳定错误代码,不要求 Client 解析自然语言判断逻辑。
|
||
|
||
### 1.1 为什么没有租约和心跳
|
||
|
||
早期方案有租约(lease)和心跳,用来防止两个 Client 做同一个任务导致重复下单。后来去掉了,原因是:
|
||
|
||
**本项目不自动付款**(见 [01 需求](01-requirements.md) §9),采购止于创建订单。
|
||
重复下单产生的是重复的**未付款**订单,人工审核时不付即可,代价和"白干一场"是一个量级。
|
||
为这点代价引入租约、心跳、过期判断和一整套中断逻辑,不划算。
|
||
|
||
Admin 想知道某个 Client 是不是卡死了,用**领取后超时重派**即可——这完全在 Admin 侧,Client 不参与。
|
||
|
||
> 注意:去掉租约**不代表**去掉防重复下单。防的是**本机崩溃重启后重复下单**,
|
||
> 靠 `task_runs.irreversible_action_at` 标记,见 [03 数据模型](03-data-model.md) §7.3。那套机制反而更重要了。
|
||
|
||
## 2. 通用请求头
|
||
|
||
```http
|
||
Authorization: Bearer <token>
|
||
X-Client-Id: client-001
|
||
X-Request-Id: <uuid>
|
||
Content-Type: application/json
|
||
```
|
||
|
||
结果和失败提交额外携带:
|
||
|
||
```http
|
||
Idempotency-Key: <stable-key>
|
||
```
|
||
|
||
认证方式仍待 Admin 联合评审。无论最终采用哪种方式,访问令牌不得写入普通日志。
|
||
|
||
## 3. 通用错误
|
||
|
||
```json
|
||
{
|
||
"error": {
|
||
"code": "IDEMPOTENCY_CONFLICT",
|
||
"message": "相同幂等键提交了不同内容",
|
||
"retryable": false,
|
||
"request_id": "7a5d...",
|
||
"details": {}
|
||
}
|
||
}
|
||
```
|
||
|
||
建议状态码:
|
||
|
||
| 状态码 | 场景 |
|
||
|---|---|
|
||
| `400` | 请求结构或字段无效 |
|
||
| `401` | 未认证或凭据过期 |
|
||
| `403` | Client 无权访问任务(从未派发给它) |
|
||
| `404` | 任务不存在 |
|
||
| `409` | 幂等冲突 |
|
||
| `422` | 业务规则不满足 |
|
||
| `429` | 请求过于频繁 |
|
||
| `500/503` | Admin 暂时故障,可按策略重试 |
|
||
|
||
注意 `403` 的含义:只有**从未派发给这个 Client** 的任务才返回 403。
|
||
任务已取消、已重派给别人,都**不能**返回 403,见 §6.1。
|
||
|
||
## 4. 任务对象
|
||
|
||
```json
|
||
{
|
||
"id": "PDD-20260806-0001",
|
||
"type": "purchase",
|
||
"version": 3,
|
||
"priority": 10,
|
||
"payload": {
|
||
"goods_id": "737116531267",
|
||
"goods_url": "https://mobile.yangkeduo.com/goods.html?goods_id=737116531267",
|
||
"expected_title": "商品标题",
|
||
"options": {
|
||
"color": "黑色",
|
||
"size": "L"
|
||
},
|
||
"quantity": 2,
|
||
"expected_price_cent": 3990,
|
||
"max_price_cent": 4200
|
||
},
|
||
"created_at": "2026-08-06T07:00:00Z",
|
||
"updated_at": "2026-08-06T07:05:00Z"
|
||
}
|
||
```
|
||
|
||
采集任务的 `payload` 不要求颜色、尺码、数量和价格字段。采购任务必须包含 `quantity` 以及 `max_price_cent` 或等价的明确价格保护规则。
|
||
|
||
任务对象里**不含 Admin 侧状态**。Client 不关心 Admin 那边把它标成什么,只管做完提交。
|
||
|
||
## 5. 领取任务
|
||
|
||
```http
|
||
POST /api/v1/client/tasks/claim
|
||
```
|
||
|
||
请求:
|
||
|
||
```json
|
||
{
|
||
"client": {
|
||
"name": "办公室-01"
|
||
},
|
||
"supported_types": ["collect", "purchase"],
|
||
"device": {
|
||
"address": "192.168.0.173:5555",
|
||
"platform": "android",
|
||
"pdd_package": "com.xunmeng.pinduoduo"
|
||
},
|
||
"capabilities": {
|
||
"purchase_mode": "dry_run",
|
||
"schema_versions": [1]
|
||
}
|
||
}
|
||
```
|
||
|
||
有任务时返回 `200`:
|
||
|
||
```json
|
||
{
|
||
"task": {}
|
||
}
|
||
```
|
||
|
||
无可领取任务时返回 `204 No Content`。
|
||
|
||
`[必须]` **`204` 不是错误。** 新客户端第一次调 `claim` 必然是 204——
|
||
它刚被登记,还没有人给它分配任务。Client 要把它当"暂时没活干"处理,
|
||
不要报错,也不要因此触发重试风暴。
|
||
|
||
`client.name` 是给人看的显示名,可以不填(不填时 Admin 用 `X-Client-Id` 兜底)。
|
||
它**只在首次登记时被采纳**,之后操作员在 Admin 改的名字不会被客户端覆盖。
|
||
|
||
> 怎么把这条链路跑通、怎么确认设备登记成功,见
|
||
> [Admin 侧的设备登记联调手册](../admin/07-设备登记联调手册.md)。
|
||
|
||
规则:
|
||
|
||
- `[必须]` 领取必须在 Admin 内部原子完成,一次调用最多返回一个任务。
|
||
- `[必须]` Admin 不得向只声明 `dry_run` 的 Client 分配要求真实下单的任务。
|
||
- `[必须]` 响应**不包含**租约。Client 拿到任务就开始做,做完再来领下一个。
|
||
|
||
### 5.1 Admin 侧的分配语义
|
||
|
||
已确认:**Admin 只把任务分配给指定的 Client**,`claim` 只会返回分配给本 `X-Client-Id` 的任务。
|
||
|
||
因此 Client 完全不需要看到全局任务池,本地也不缓存未领取的任务
|
||
(见 [03 数据模型](03-data-model.md) §3.1)。操作人员想知道队列里还有多少活,
|
||
去 Admin 自己的界面看([01 需求](01-requirements.md) §9 已把 Admin 界面列为非目标)。
|
||
|
||
Admin 可以在超时后把任务重派给别的 Client,Client 侧对此**无感知也不需要感知**。
|
||
|
||
## 6. 提交成功结果
|
||
|
||
```http
|
||
POST /api/v1/client/tasks/{task_id}/result
|
||
Idempotency-Key: task-id:attempt-id:result-v1
|
||
```
|
||
|
||
采集结果:
|
||
|
||
```json
|
||
{
|
||
"task_version": 3,
|
||
"attempt_id": "attempt-uuid",
|
||
"result_type": "collect",
|
||
"completed_at": "2026-08-06T08:03:00Z",
|
||
"pdd_data": {}
|
||
}
|
||
```
|
||
|
||
采购结果把 `result_type` 换成 `purchase`,其余结构相同。
|
||
|
||
响应:
|
||
|
||
```json
|
||
{
|
||
"accepted": true,
|
||
"result_id": "result-uuid",
|
||
"accepted_at": "2026-08-06T08:03:01Z"
|
||
}
|
||
```
|
||
|
||
规则:
|
||
|
||
- `[必须]` 相同 `Idempotency-Key` 和相同请求内容必须返回同一业务结果。
|
||
- `[必须]` 相同键但不同内容返回 `409 IDEMPOTENCY_CONFLICT`。
|
||
- `[必须]` Client 只有收到 `accepted: true` 后才能把本地任务标为 `succeeded`。
|
||
|
||
### 6.1 Admin 必须无条件接受
|
||
|
||
这是对 Admin 侧的**强约束**,实现时最容易被顺手违反,务必写进 Admin 的验收标准:
|
||
|
||
- `[必须]` Admin **不得**因为任务已取消而拒绝结果。
|
||
- `[必须]` Admin **不得**因为任务已重派给别的 Client 而拒绝结果。
|
||
- `[必须]` Admin **必须**能接受同一任务来自**多个 Client** 的多份结果。怎么去重、以哪份为准,是 Admin 内部的事,不要求 Client 配合。
|
||
- `[必须]` 只要这个任务曾经派发给该 Client,提交就必须被接受。只有从未派发过才返回 `403`。
|
||
|
||
原因:Client 中途不查任务状态(§1),所以它**必然**会提交一些"Admin 那边已经不要了"的结果。
|
||
如果 Admin 拒收,Client 侧就会出现大量无法处理的失败——而 Client 已经真的把事情做完了,
|
||
甚至可能已经下了单,这些数据必须能交上去留痕。
|
||
|
||
`accepted: true` 的含义是"**我收到并存下了**",不代表 Admin 认可这个任务仍然有效。
|
||
任务到底算不算数,由 Admin 和人工审核决定,与 Client 无关。
|
||
|
||
## 7. 提交失败或人工处理结果
|
||
|
||
```http
|
||
POST /api/v1/client/tasks/{task_id}/failure
|
||
Idempotency-Key: task-id:attempt-id:failure-v1
|
||
```
|
||
|
||
```json
|
||
{
|
||
"task_version": 3,
|
||
"attempt_id": "attempt-uuid",
|
||
"status": "manual_review",
|
||
"error": {
|
||
"code": "AMBIGUOUS_ORDER_MATCH",
|
||
"message": "发现多个可能属于当前任务的订单",
|
||
"retryable": false,
|
||
"step": "reconcile_order"
|
||
},
|
||
"diagnostics": {
|
||
"artifact_ids": ["artifact-uuid"]
|
||
},
|
||
"reported_at": "2026-08-06T08:03:00Z"
|
||
}
|
||
```
|
||
|
||
`status` 只能是 `retry_wait`、`manual_review`、`failed` 或 `cancelled`。
|
||
|
||
规则:
|
||
|
||
- `[必须]` §6.1 的无条件接受规则同样适用于本接口。
|
||
- `[必须]` Admin 决定是否创建新的执行机会,但对已经可能下单的任务不得通过普通重试触发再次购买。
|
||
|
||
## 8. 幂等与重试
|
||
|
||
- `[必须]` 结果和失败提交必须使用持久化的 `Idempotency-Key`,格式见 [03 数据模型](03-data-model.md) §5.2。
|
||
- `[必须]` Client 在发送前将完整请求写入 Outbox;重试使用相同键和相同内容。
|
||
- `[必须]` `claim` 需要 Admin 保证重复请求不会一次分配出多个任务。
|
||
- `[建议]` 对 `429`、`500` 和 `503` 使用有上限的指数退避,并遵守 `Retry-After`。
|
||
- `[必须]` 对认证、幂等冲突和业务校验错误不得无限重试。
|
||
|
||
## 9. Mock Admin 要求
|
||
|
||
`MockAdminGateway` 与 HTTP 实现暴露同一应用层接口,只有三个方法:
|
||
|
||
```text
|
||
claim_next(client, capabilities) # §5
|
||
submit_result(task_id, idempotency_key, result) # §6
|
||
submit_failure(task_id, idempotency_key, failure) # §7
|
||
```
|
||
|
||
Mock 必须支持:
|
||
|
||
- 无任务可领取(`claim` 返回 204);
|
||
- 采集和采购任务;
|
||
- 网络超时和暂时故障;
|
||
- 幂等重复提交(相同键相同内容);
|
||
- 幂等冲突(相同键不同内容);
|
||
- **提交一个 Admin 侧已取消的任务,仍返回 `accepted: true`**(用来验证 §6.1);
|
||
- 结果校验失败。
|
||
|
||
Mock 测试通过不能替代与真实 Admin 的契约测试。
|
||
|
||
## 10. 待联合确认
|
||
|
||
Admin 还没做完,下面这些要和 Admin 一起定。**不许因此停工**——先按"临时默认值"实现,Mock Gateway 也按这个值模拟,等定了再按工单改。
|
||
|
||
| # | 待确认什么 | 临时默认值(先这么做) |
|
||
|---|---|---|
|
||
| 1 | 认证、Client 注册和凭据刷新方式 | 请求头预留 `Authorization: Bearer <token>`,token 从设置读;Mock 不校验 |
|
||
| 2 | Admin 对人工处理任务的后续操作 | Client 只负责报告 `manual_review` 然后停手,不猜 Admin 会怎么处理 |
|
||
| 3 | Artifact 是独立上传还是只报本地引用 | **只报本地引用**(路径 + 哈希),不实现上传 |
|
||
| 4 | Admin 超时重派的等待时长 | 纯 Admin 侧策略,Client 不参与也不需要知道 |
|
||
|
||
**已定案(不再是待确认项):**
|
||
|
||
- Admin 只把任务分配给指定 Client,Client 不读全局任务池。见 §5.1。
|
||
- **没有租约、没有心跳、没有状态回查。** Client 只有 §5 / §6 / §7 三个调用。见 §1.1。
|
||
- Admin 必须无条件接受已派发过的 Client 提交的结果。见 §6.1。
|
||
|
||
改动这张表里任何一条,都属于会影响业务结果的变更,必须先更新工单并经用户确认。
|