Files
cmautobuy/docs/client/04-admin-api-contract.md
T
chengmaandClaude Opus 5 62d46bf10a docs: 新增设备登记联调手册,并定案 client.name
面向 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>
2026-08-06 17:08:58 +08:00

307 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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。
改动这张表里任何一条,都属于会影响业务结果的变更,必须先更新工单并经用户确认。