docs: define idempotent Client registration contract (#12)
This commit is contained in:
@@ -172,8 +172,9 @@ Admin 的职责是把这三方的数据串起来,最终产出 Client 能执行
|
||||
|
||||
**注册方式:**
|
||||
|
||||
- `[必须]` 客户端**调用领取接口时自动注册**:新序列号就新增,已有就更新。
|
||||
**不设单独的注册或心跳接口**,理由见 [04 Client 接口实现](04-client-api.md) §3。
|
||||
- `[必须]` 设置页通过独立登记接口幂等新增或更新 Client;该接口不得领取或修改任务。
|
||||
- `[必须]` 领取接口保留隐式登记作为旧 Client 的兼容兜底。
|
||||
- `[必须]` 不设心跳接口,在线状态由登记、领取和提交产生的最近活动时间派生。
|
||||
- `[必须]` `状态` 是**派生字段,不存库**:
|
||||
最近活动时间在 N 分钟内算"在线",否则"离线"。`[建议]` N 默认 10 分钟。
|
||||
- 名称由客户端上报,操作员可以在 Admin 这边改成好记的名字。
|
||||
@@ -261,7 +262,7 @@ MVP 包含:
|
||||
- 顺运宝数据的**手工录入或造数**(同步按钮占位);
|
||||
- 规格匹配弹窗与可复用映射;
|
||||
- 创建采购任务并分配客户端;
|
||||
- 给 Client 的三个接口:领取、提交结果、提交失败;
|
||||
- 给 Client 的四个接口:登记、领取、提交结果、提交失败;
|
||||
- 客户端自动注册与在线状态。
|
||||
|
||||
MVP 之后:
|
||||
@@ -303,7 +304,7 @@ MVP 之后:
|
||||
|
||||
- PDD 链接**人工填写**,入口在蝦皮数据模块的编辑弹窗,采集按钮也在那里。
|
||||
- 任务**分配给指定客户端**,Client 只领分给自己的。
|
||||
- **不加心跳接口**,注册在领取时完成,在线状态由最近活动时间派生。
|
||||
- **不加心跳接口**;设置页显式登记,领取接口保留兼容登记,在线状态由最近活动时间派生。
|
||||
- SKU 映射**独立成表且可复用**,同一蝦皮 SKU 只人工匹配一次。
|
||||
- 货运单的"规格 SKU"**直接对应蝦皮 `商品規格ID`**,不需要转换。
|
||||
但**不加外键**——本地查不到该 SKU 是常态,见 [03 数据模型](03-data-model.md) §4。
|
||||
|
||||
@@ -372,7 +372,7 @@ CREATE TABLE clients (
|
||||
device_address TEXT,
|
||||
platform TEXT,
|
||||
pdd_package TEXT,
|
||||
capabilities TEXT, -- claim 请求里的 capabilities 原文
|
||||
capabilities TEXT, -- 登记或 claim 请求里的 capabilities 原文
|
||||
last_seen_at TEXT NOT NULL, -- 每次调接口都刷新
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
@@ -382,8 +382,8 @@ CREATE TABLE clients (
|
||||
- `[必须]` **没有 `status` 字段。** 在线状态是**算出来的**:
|
||||
`last_seen_at` 在 N 分钟内算在线,否则离线。`[建议]` N 默认 10 分钟。
|
||||
存成字段会和真实情况不同步。
|
||||
- `[必须]` 注册发生在**领取任务时**,新序列号新增、已有的更新,
|
||||
**不设单独的注册或心跳接口**,见 [04](04-client-api.md) §3。
|
||||
- `[必须]` 设置页使用独立登记接口幂等新增或更新 Client,claim 保留隐式登记作为兼容兜底。
|
||||
- `[必须]` 不设心跳接口;登记、claim、result 和 failure 都刷新 `last_seen_at`,见 [04](04-client-api.md) §1.1、§3。
|
||||
|
||||
## 9. 数据关系总览
|
||||
|
||||
|
||||
+39
-10
@@ -8,9 +8,10 @@
|
||||
|
||||
本文档中 `[必须]` / `[建议]` / `[待定]` 的含义见 [文档索引](../README.md#文档标注说明)。
|
||||
|
||||
## 1. 一共只有三个接口
|
||||
## 1. 一共四个接口
|
||||
|
||||
```http
|
||||
PUT /api/v1/client/registration 登记或更新 Client
|
||||
POST /api/v1/client/tasks/claim 领一个任务
|
||||
POST /api/v1/client/tasks/{task_id}/result 提交成功结果
|
||||
POST /api/v1/client/tasks/{task_id}/failure 提交失败/需人工
|
||||
@@ -20,6 +21,31 @@ POST /api/v1/client/tasks/{task_id}/failure 提交失败/需人工
|
||||
理由见 Client 契约 §1.1:本项目人工付款,重复下单只产生重复的**未付款**订单,
|
||||
不值得为它引入一整套中断逻辑。
|
||||
|
||||
### 1.1 独立登记接口
|
||||
|
||||
设置页保存当前设备时调用:
|
||||
|
||||
```http
|
||||
PUT /api/v1/client/registration
|
||||
X-Client-Id: <stable-client-id>
|
||||
```
|
||||
|
||||
请求体与 claim 的 Client、设备和能力部分相同。相同 `X-Client-Id` 重复调用执行 upsert,统一返回 `200 OK`:
|
||||
|
||||
```json
|
||||
{
|
||||
"registered": true,
|
||||
"client_id": "CLIENT-123456",
|
||||
"registered_at": "2026-08-06T09:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
`[必须]` 本接口只登记 Client,不查询、领取或修改任务,也不返回任务。
|
||||
|
||||
`[必须]` 显式登记携带非空名称时允许更新名称;空名称更新保留现有名称,新建时使用 Client ID 兜底。设备、能力、`last_seen_at` 和 `updated_at` 正常更新。
|
||||
|
||||
`[必须]` 校验 `client.name` 最多 50 字、任务类型、执行模式和结构版本。错误使用统一 JSON 结构,至少覆盖 `MISSING_CLIENT_ID`、`INVALID_BODY`、`INVALID_CLIENT_PROFILE` 和 `CLIENT_REGISTER_FAILED`。
|
||||
|
||||
## 2. 领取任务
|
||||
|
||||
```http
|
||||
@@ -76,11 +102,11 @@ WHERE task_id = ? AND status = 'assigned';
|
||||
这些在建任务时就该校验住(见 [01 需求](01-requirements.md) §5),
|
||||
不要等到这里才发现发不出去。
|
||||
|
||||
## 3. 客户端注册就在领取里做
|
||||
## 3. claim 保留隐式登记作为兼容兜底
|
||||
|
||||
`[必须]` **不设单独的注册接口,也不设心跳接口。**
|
||||
`[必须]` **有独立登记接口,但不设心跳接口。**
|
||||
|
||||
`claim` 请求里已经带齐了初始化需要的东西:
|
||||
旧 Client 可能直接调用 `claim`,其请求里已经带齐初始化需要的信息:
|
||||
|
||||
```http
|
||||
X-Client-Id: client-001 ← 序列号,主键
|
||||
@@ -96,14 +122,14 @@ X-Client-Id: client-001 ← 序列号,主键
|
||||
}
|
||||
```
|
||||
|
||||
> **已定案:** `client.name` 是对 Client 契约 §5 的一处小扩展。
|
||||
> **已定案:** `client.name` 同时用于显式登记和 claim 兼容登记。
|
||||
> Client 的设置页本来就有「设备名」输入框(`deviceNameInput`,50 字上限),
|
||||
> 把它带上即可。**Admin 仍然容忍它缺失**——没有就用 `X-Client-Id` 当显示名,
|
||||
> 所以 Client 先不发也不影响登记。
|
||||
> 显式登记会更新非空名称;claim 对已有 Client 不覆盖名称。
|
||||
> Admin 容忍名称缺失,新建时用 `X-Client-Id` 当显示名。
|
||||
>
|
||||
> 登记流程的完整说明见 [07 设备登记联调手册](07-设备登记联调手册.md)。
|
||||
|
||||
处理:
|
||||
claim 的兼容登记处理:
|
||||
|
||||
```text
|
||||
upsert clients:
|
||||
@@ -122,8 +148,7 @@ upsert clients:
|
||||
`[必须]` `result` 和 `failure` 接口**也要刷新 `last_seen_at`**。
|
||||
否则客户端执行长任务期间不调 claim,会被误判成离线。
|
||||
|
||||
**新客户端第一次来必然拿不到任务**(还没人给它分配),返回 `204` 是正常的。
|
||||
它这时已经登记进列表了,操作员就能给它分配任务。
|
||||
新客户端直接调用 claim 时,`204` 是正常的无任务结果,Client 必须正常处理;如果已经预先分配任务,也可能首次调用就返回 `200`。
|
||||
|
||||
## 4. 提交结果
|
||||
|
||||
@@ -231,6 +256,10 @@ MVP 阶段只在内网/本机运行。
|
||||
|
||||
Client 契约里散落的 Admin 侧硬要求,汇总在这里,**可以直接当验收标准用**:
|
||||
|
||||
- [ ] `PUT /registration` 相同 Client ID 幂等 upsert,不创建重复记录
|
||||
- [ ] 登记接口不读取、领取或修改任务
|
||||
- [ ] 显式登记更新非空名称,空名称更新保留已有名称
|
||||
- [ ] 登记接口校验名称、任务类型、执行模式和结构版本
|
||||
- [ ] `claim` 一次只返回一个任务,且只返回分配给该客户端的
|
||||
- [ ] `claim` 原子完成,并发下不会把同一任务发给两个客户端
|
||||
- [ ] `claim` 无任务时返回 `204`,不是 `200` 加空对象
|
||||
|
||||
+192
-179
@@ -1,86 +1,108 @@
|
||||
# 07 设备登记联调手册
|
||||
|
||||
- 文档状态:联调用,随实现更新
|
||||
- 读者:**实现 Client 侧 `HttpAdminGateway` 的开发者**
|
||||
- 目标:让一台新 Client 出现在 Admin 的「客户端列表」里,并确认登记成功
|
||||
- 文档状态:登记接口契约已确认,等待 Gitea #12 实现和真实联调
|
||||
- 读者:Admin 登记接口、Client `HttpAdminGateway` 开发者
|
||||
- 目标:让 Client 设置页安全登记设备,同时不领取任务
|
||||
|
||||
本文所有请求和响应都是**在真实运行的 Admin 上跑出来的**,不是手写示例。
|
||||
接口的权威定义在 [Client 侧契约](../client/04-admin-api-contract.md),
|
||||
本文只讲怎么把它跑通。
|
||||
接口权威定义:
|
||||
|
||||
- [Client–Admin API 契约](../client/04-admin-api-contract.md) §4.1
|
||||
- [Admin 侧 Client 接口实现](04-client-api.md) §1.1
|
||||
- Gitea #12:Admin 新增幂等 Client 登记接口
|
||||
- Gitea #11:Client 保存当前设备并登记 Client
|
||||
|
||||
本文中的登记请求是 #12 的目标契约。在 #12 完成并通过真实 curl 验证前,不得声称登记接口已经可用。
|
||||
|
||||
---
|
||||
|
||||
## 1. 先理解一件事:没有注册接口
|
||||
## 1. 登记、领取和心跳是三件不同的事
|
||||
|
||||
**登记是领取任务的副作用。**
|
||||
### 1.1 显式登记
|
||||
|
||||
Client 调 `claim` 的时候,Admin 顺手就把这台机器记下来了。
|
||||
没有 `/register`,也没有心跳接口——这是有意的设计,
|
||||
理由见 [04 Client 接口实现](04-client-api.md) §3。
|
||||
|
||||
所以"登记新设备"这件事,Client 侧要做的就是:**调一次 `claim`**。
|
||||
|
||||
```text
|
||||
Client 启动
|
||||
↓
|
||||
调 POST /api/v1/client/tasks/claim(带上设备号和设备名)
|
||||
↓
|
||||
Admin:没见过这个设备号 → 新增一条记录
|
||||
见过 → 更新设备信息和最近活动时间
|
||||
↓
|
||||
返回 204(没有分给它的任务)或 200(有任务)
|
||||
```
|
||||
|
||||
> **最容易误判的一点:新设备第一次调 `claim` 必然返回 `204`。**
|
||||
>
|
||||
> 因为它刚登记,还没有人给它分配任务。
|
||||
> **`204` 不是失败,它同时意味着登记成功了。**
|
||||
> Client 必须把它当正常情况处理——不要报错、不要弹提示、
|
||||
> 更不要因此触发重试风暴。
|
||||
|
||||
---
|
||||
|
||||
## 2. Client 要准备两样东西
|
||||
|
||||
### 2.1 设备号(`X-Client-Id`)
|
||||
|
||||
对应 Client 设置页那个只读的**「设备号」**输入框(现在显示"待生成")。
|
||||
|
||||
`[必须]` 规则:
|
||||
|
||||
- **首次启动时生成一次**(UUID 或任何全局唯一的串都行);
|
||||
- 生成后存进 `app_settings`,键名 `admin.client_id`;
|
||||
- **之后永远不变**——重启、升级、改配置都不能变。
|
||||
|
||||
**为什么不能变:** Admin 是按设备号认机器的。设备号变了,
|
||||
Admin 会认为这是一台新机器,客户端列表里就会多出一条,
|
||||
原来那条变成永远离线的僵尸记录。
|
||||
|
||||
换电脑、重装系统算新设备,那是对的;但同一台机器重启不能变。
|
||||
|
||||
### 2.2 设备名(`client.name`)
|
||||
|
||||
对应设置页那个可编辑的**「设备名」**输入框(50 字上限)。
|
||||
|
||||
- 给人看的,比如"办公室-01"、"仓库那台";
|
||||
- **可以不填**——不填的话 Admin 用设备号当显示名,不影响功能。
|
||||
|
||||
> **一个会让人困惑的行为,先说清楚:**
|
||||
>
|
||||
> 设备名**只在第一次登记时被采纳**。之后操作员在 Admin 界面上把它
|
||||
> 改成别的名字,Client 再上报也**不会覆盖**。
|
||||
>
|
||||
> 这是故意的——避免操作员精心起的名字被客户端的默认值冲掉。
|
||||
> 所以你在 Client 那边改了设备名,发现 Admin 没变,**不是 bug**。
|
||||
|
||||
---
|
||||
|
||||
## 3. 请求长什么样
|
||||
设置页点击“保存”后调用:
|
||||
|
||||
```http
|
||||
POST /api/v1/client/tasks/claim HTTP/1.1
|
||||
PUT /api/v1/client/registration
|
||||
```
|
||||
|
||||
它只新增或更新 Client,**不读取、领取或修改任务**,也不返回任务。
|
||||
|
||||
### 1.2 领取任务
|
||||
|
||||
“获取任务”流程调用:
|
||||
|
||||
```http
|
||||
POST /api/v1/client/tasks/claim
|
||||
```
|
||||
|
||||
它可能返回 `204 No Content`,也可能返回 `200 + task`。返回任务时 Admin 已经改变任务状态,Client 必须可靠写入本地 SQLite,不能丢弃响应。
|
||||
|
||||
claim 保留隐式登记作为旧 Client 的兼容兜底,但设置保存入口不得用 claim 代替登记。
|
||||
|
||||
### 1.3 没有心跳接口
|
||||
|
||||
本项目仍然不做定时心跳。登记、claim、result 和 failure 都刷新 `last_seen_at`,Admin 根据最近活动时间计算在线或离线。
|
||||
|
||||
客户端执行长任务期间超过在线阈值,Admin 暂时显示离线是已知且接受的取舍。
|
||||
|
||||
---
|
||||
|
||||
## 2. Client 要准备的数据
|
||||
|
||||
### 2.1 设备号:`X-Client-Id`
|
||||
|
||||
对应设置页只读“设备号”。
|
||||
|
||||
- 首次保存时生成一次;
|
||||
- 保存到 Client SQLite `app_settings`,键名 `admin.client_id`;
|
||||
- 重启、升级、修改设置时保持不变;
|
||||
- 换电脑或清空本地数据后生成新编号;
|
||||
- 只向 Admin 发送生成后的编号,不发送原始硬件参数。
|
||||
|
||||
Admin 使用它作为 `clients.client_id` 主键。设备号变化会产生一条新的 Client 记录。
|
||||
|
||||
### 2.2 设备名:`client.name`
|
||||
|
||||
对应设置页可编辑“设备名”,最多 50 字,可以为空。
|
||||
|
||||
- 显式登记携带非空名称时,Admin 更新名称;
|
||||
- 显式登记名称为空时:新 Client 使用设备号兜底,已有 Client 保留原名称;
|
||||
- claim 隐式登记已有 Client 时不覆盖名称;
|
||||
- Admin 页面仍可人工修改名称。
|
||||
|
||||
这意味着用户明确点击“保存”可以同步非空名称,而后台领取不会反复覆盖名称。
|
||||
|
||||
### 2.3 设备和能力
|
||||
|
||||
```json
|
||||
{
|
||||
"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]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `supported_types` 必须非空,只允许 `collect`、`purchase`;
|
||||
- `device` 可选,提供时当前只支持 `android`;
|
||||
- `purchase_mode` 只允许 `dry_run`、`live`,MVP 固定 `dry_run`;
|
||||
- `schema_versions` 只包含正整数,当前使用 `[1]`。
|
||||
|
||||
---
|
||||
|
||||
## 3. 登记请求
|
||||
|
||||
```http
|
||||
PUT /api/v1/client/registration HTTP/1.1
|
||||
Host: 127.0.0.1:8080
|
||||
X-Client-Id: 7f3a1c62-9b04-4e8d-a1f2-5c7e9d0b3a11
|
||||
X-Client-Id: CLIENT-123456
|
||||
X-Request-Id: 7a5d0000-0000-0000-0000-000000000001
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
@@ -105,88 +127,78 @@ Authorization: Bearer <token>
|
||||
|
||||
| 字段 | 必填 | 说明 |
|
||||
|---|---|---|
|
||||
| `X-Client-Id` 头 | **是** | 设备号。缺了直接 400 |
|
||||
| `client.name` | 否 | 设备名。缺了用设备号当显示名 |
|
||||
| `supported_types` | 是 | 支持的任务类型。只写 `["collect"]` 就不会拿到采购任务 |
|
||||
| `device.*` | 否 | 会存进 Admin 供排查用 |
|
||||
| `capabilities.purchase_mode` | 是 | `dry_run` 或 `live`。演练模式期间固定 `dry_run` |
|
||||
| `Authorization` | 否 | `[待定]` 认证方式未定,当前 Admin **读但不校验** |
|
||||
| `X-Client-Id` | 是 | 稳定设备号,Admin Client 唯一键 |
|
||||
| `X-Request-Id` | 建议 | 每次请求使用新的 UUID,便于排查 |
|
||||
| `client.name` | 否 | 最多 50 字;非空时显式更新名称 |
|
||||
| `supported_types` | 是 | 非空任务类型列表 |
|
||||
| `device` | 否 | 当前选择的 Android 设备信息 |
|
||||
| `capabilities.purchase_mode` | 是 | MVP 使用 `dry_run` |
|
||||
| `capabilities.schema_versions` | 是 | 当前使用 `[1]` |
|
||||
| `Authorization` | 当前可选 | Admin 当前读取但不校验,正式认证待定 |
|
||||
|
||||
访问令牌不得出现在普通日志、错误提示或测试固件中。
|
||||
|
||||
---
|
||||
|
||||
## 4. 响应怎么处理
|
||||
## 4. 登记响应
|
||||
|
||||
### 4.1 `204 No Content` —— 登记成功,暂时没活干
|
||||
|
||||
**没有响应体。** 这是新设备的正常结果,也是引擎空转时的常态。
|
||||
|
||||
Client 该做的:按轮询周期退避后再来,**不要当成错误**。
|
||||
|
||||
### 4.2 `200 OK` —— 领到任务了
|
||||
新增和更新统一返回 `200 OK`:
|
||||
|
||||
```json
|
||||
{
|
||||
"task": {
|
||||
"id": "PDD-TEST-0001",
|
||||
"type": "purchase",
|
||||
"version": 1,
|
||||
"priority": 0,
|
||||
"payload": {
|
||||
"goods_id": "737116531267",
|
||||
"goods_url": "https://mobile.yangkeduo.com/goods.html?goods_id=737116531267",
|
||||
"options": { "color": "黑色", "size": "M码" },
|
||||
"quantity": 2,
|
||||
"max_price_cent": 4200
|
||||
},
|
||||
"created_at": "2026-08-06T08:00:00Z",
|
||||
"updated_at": "2026-08-06T08:53:20Z"
|
||||
}
|
||||
"registered": true,
|
||||
"client_id": "CLIENT-123456",
|
||||
"registered_at": "2026-08-06T09:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
注意三点:
|
||||
Client 必须校验:
|
||||
|
||||
- **没有租约字段**,也没有 Admin 侧状态——不用找,本来就没有;
|
||||
- `options` 是**对象**不是字符串;
|
||||
- `max_price_cent` 是**人民币分的整数**(4200 = ¥42.00)。
|
||||
采购任务必然带 `quantity` 和 `max_price_cent`,这是价格保护。
|
||||
- HTTP 状态为 `200`;
|
||||
- `registered` 为 `true`;
|
||||
- `client_id` 与请求的 `X-Client-Id` 相同;
|
||||
- `registered_at` 是带时区 ISO 8601 时间。
|
||||
|
||||
### 4.3 `400` —— 请求有问题
|
||||
常见错误:
|
||||
|
||||
| HTTP | code | retryable | 处理 |
|
||||
|---|---|---|---|
|
||||
| 400 | `MISSING_CLIENT_ID` | false | 检查本地设备号,不自动重试 |
|
||||
| 400 | `INVALID_BODY` | false | 修正 JSON,不自动重试 |
|
||||
| 422 | `INVALID_CLIENT_PROFILE` | false | 修正名称或能力字段 |
|
||||
| 500 | `CLIENT_REGISTER_FAILED` | true | 保留本地设置,允许稍后重试 |
|
||||
|
||||
错误响应使用统一结构:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": "MISSING_CLIENT_ID",
|
||||
"message": "缺少 X-Client-Id 请求头",
|
||||
"code": "INVALID_CLIENT_PROFILE",
|
||||
"message": "Client 能力字段无效",
|
||||
"retryable": false,
|
||||
"request_id": "",
|
||||
"request_id": "7a5d...",
|
||||
"details": {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`retryable: false` 的错误**不要重试**,重试多少次都是一样的结果。
|
||||
|
||||
---
|
||||
|
||||
## 5. 怎么确认登记成功
|
||||
## 5. Admin 实现后的 curl 验证
|
||||
|
||||
### 第一步:起 Admin
|
||||
### 5.1 启动 Admin
|
||||
|
||||
```powershell
|
||||
cd D:\chengma\cmautobuy\admin
|
||||
go run .
|
||||
```
|
||||
|
||||
看到 `Admin 已启动: http://127.0.0.1:8080` 就绪。
|
||||
|
||||
### 第二步:发一次 claim
|
||||
|
||||
不用等 Client 写完,先用 curl 打通链路:
|
||||
### 5.2 首次登记
|
||||
|
||||
```bash
|
||||
curl -i -X POST http://127.0.0.1:8080/api/v1/client/tasks/claim \
|
||||
curl -i -X PUT http://127.0.0.1:8080/api/v1/client/registration \
|
||||
-H 'X-Client-Id: test-device-001' \
|
||||
-H 'X-Request-Id: registration-create-001' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"client": {"name": "联调测试机"},
|
||||
@@ -197,84 +209,85 @@ curl -i -X POST http://127.0.0.1:8080/api/v1/client/tasks/claim \
|
||||
}'
|
||||
```
|
||||
|
||||
预期:`HTTP/1.1 204 No Content`,无响应体。
|
||||
预期:`200 OK`,`registered: true`,Admin `/clients` 页面出现且只出现一条 `test-device-001`。
|
||||
|
||||
### 第三步:在界面上确认
|
||||
### 5.3 重复登记
|
||||
|
||||
浏览器打开 <http://127.0.0.1:8080/clients>,应该看到:
|
||||
使用同一 `X-Client-Id`、新的 `X-Request-Id` 再调用一次。预期仍为 `200`,客户端列表记录数不增加。
|
||||
|
||||
| 名称 | 序列号 | 状态 | 最近活动 |
|
||||
|---|---|---|---|
|
||||
| 联调测试机 | test-device-001 | 在线 | 2026-08-06T09:03:55Z |
|
||||
### 5.4 更新名称
|
||||
|
||||
底部状态条显示 `共 1 台客户端 · 在线 1 · 离线 0`。
|
||||
把 `client.name` 改为“联调测试机-已更新”再次调用。预期记录数不增加,名称更新。
|
||||
|
||||
**看到这一行 = 登记打通了。**
|
||||
### 5.5 空名称更新
|
||||
|
||||
### 第四步:让它领到一个任务(可选)
|
||||
发送 `"client": {"name": ""}`。预期已有名称保持不变。
|
||||
|
||||
想验证 `200` 那条路径,得先有任务。现在还没有创建任务的界面
|
||||
(要等 Excel 导入那条链路做完),手工插一条:
|
||||
### 5.6 验证无任务副作用
|
||||
|
||||
用 DB Browser 打开 `admin/data/admin.db`,执行:
|
||||
登记前后检查任务表:任务状态、领取时间和领取历史均不得变化。登记响应不得出现 `task` 字段。
|
||||
|
||||
```sql
|
||||
INSERT INTO tasks (task_id, task_type, status, assigned_client, order_no,
|
||||
pdd_goods_url, pdd_goods_id, pdd_options,
|
||||
quantity, max_price_cent, created_at, updated_at)
|
||||
VALUES ('PDD-TEST-0001', 'purchase', 'assigned', 'test-device-001',
|
||||
'TEST-ORDER-001',
|
||||
'https://mobile.yangkeduo.com/goods.html?goods_id=737116531267',
|
||||
'737116531267', '{"color":"黑色","size":"M码"}',
|
||||
2, 4200,
|
||||
'2026-08-06T08:00:00Z', '2026-08-06T08:00:00Z');
|
||||
### 5.7 错误响应
|
||||
|
||||
至少验证缺少 `X-Client-Id`、名称超过 50 字、空 `supported_types` 和非法 `purchase_mode`。
|
||||
|
||||
#12 完成时,把上述真实命令的结果补回本节,并勾选 Gitea 验收项。
|
||||
|
||||
---
|
||||
|
||||
## 6. claim 兼容登记与领取验证
|
||||
|
||||
claim 的请求结构与能力字段保持不变:
|
||||
|
||||
```http
|
||||
POST /api/v1/client/tasks/claim
|
||||
X-Client-Id: test-device-001
|
||||
```
|
||||
|
||||
注意 `assigned_client` 要填**你自己的设备号**,任务只发给指定的客户端。
|
||||
- `204 No Content`:登记/活动时间更新成功,暂时没有任务;
|
||||
- `200 OK`:领到一个任务,Client 必须立即写入 SQLite;
|
||||
- 首次 claim 不保证一定是 204,预先分配过任务时可以直接返回 200;
|
||||
- claim 更新已有 Client 时不覆盖名称。
|
||||
|
||||
再调一次 claim,这次会拿到 `200` 和上面 §4.2 那个结构。
|
||||
**再调第三次又是 `204`**——任务已经被领走了,这是对的。
|
||||
设置页保存按钮不得调用 claim。只有“获取任务”流程可以调用它。
|
||||
|
||||
---
|
||||
|
||||
## 6. 常见问题
|
||||
## 7. Client 保存流程
|
||||
|
||||
| 现象 | 原因 | 怎么办 |
|
||||
|---|---|---|
|
||||
| 一直 `204`,列表里也没有 | 请求根本没到 Admin | 看 Admin 控制台有没有请求日志;确认端口和地址 |
|
||||
| `400 MISSING_CLIENT_ID` | 没带 `X-Client-Id` 头 | 它是**请求头**不是 body 字段 |
|
||||
| `400 INVALID_BODY` | body 不是合法 JSON | 即使没内容也要发 `{}`,不能发空 |
|
||||
| 列表里多出好几台一样的机器 | **设备号每次启动都在变** | 设备号必须持久化,见 §2.1 |
|
||||
| 改了 Client 的设备名,Admin 没变 | 正常行为,不是 bug | 见 §2.2 |
|
||||
| 状态显示"离线"但客户端在跑 | 超过 10 分钟没调任何接口 | 正常。本项目**有意不做心跳**,执行长任务期间会显示离线 |
|
||||
| 明明有任务却拿到 204 | `assigned_client` 不是你的设备号;或 `supported_types` 不含该任务类型 | 查库确认这两项 |
|
||||
```text
|
||||
点击“保存”
|
||||
→ 从 SQLite 读取 admin.client_id
|
||||
→ 不存在:生成并保存
|
||||
→ 已存在:保持不变
|
||||
→ 保存设备名
|
||||
→ 页面显示“本地已保存”
|
||||
→ 后台调用 PUT /registration
|
||||
├─ 成功:显示“已登记到 Admin”
|
||||
└─ 失败:显示“本地已保存,Admin 登记失败”
|
||||
```
|
||||
|
||||
实现要求:
|
||||
|
||||
- SQLite 是设备号权威来源,不能只看输入框是否显示“待生成”;
|
||||
- Admin 失败不得回滚本地设置;
|
||||
- HTTP 使用 `QObject + moveToThread`,不得阻塞 Qt 主线程;
|
||||
- 保存期间禁用重复点击;
|
||||
- 窗口关闭时断开信号,忽略迟到结果;
|
||||
- `MockAdminGateway` 和 `HttpAdminGateway` 使用相同登记契约测试;
|
||||
- 新增 `requests` 依赖前按项目依赖门禁确认固定版本和许可证。
|
||||
|
||||
---
|
||||
|
||||
## 7. Client 侧要改什么
|
||||
|
||||
按现在的代码(`client/src/admin_gateway.py`),要做这几件事:
|
||||
|
||||
- [ ] `requirements.txt` 加 HTTP 库(`requests`,版本要过工单确认)
|
||||
- [ ] **`ClientInfo` 加 `name` 字段** —— 设置页的「设备名」输入框已经有了,
|
||||
只是没接到网关里
|
||||
- [ ] 设备号生成一次并持久化到 `app_settings`(键 `admin.client_id`),
|
||||
填进设置页那个只读的「设备号」框
|
||||
- [ ] 实现 `HttpAdminGateway`,先只做 `claim_next`
|
||||
- [ ] **把 `204` 当正常情况**返回"暂无任务",不要抛异常
|
||||
- [ ] 契约测试:Mock 和 HTTP 两个实现跑同一套用例
|
||||
|
||||
前两条做完就能登记成功了,后面的可以分批。
|
||||
|
||||
---
|
||||
|
||||
## 8. 还没做完的部分
|
||||
## 8. 能力状态
|
||||
|
||||
| 能力 | 状态 |
|
||||
|---|---|
|
||||
| 登记新设备 | **可用** |
|
||||
| 领取任务 | **可用**(任务需手工插库) |
|
||||
| 提交结果 / 提交失败 | **可用**,含幂等 |
|
||||
| 创建任务的界面 | 未做,等 Excel 导入链路 |
|
||||
| 认证 | `[待定]`,当前读 `Authorization` 头但不校验 |
|
||||
| 改派任务 | 未做,需要时直接改库 |
|
||||
| 独立登记契约 | **已确认** |
|
||||
| Admin 独立登记实现 | 等待 #12 |
|
||||
| Client 设置页登记实现 | 等待 #11 和 #12 |
|
||||
| claim 隐式登记 | 已有,保留兼容 |
|
||||
| 领取任务 | 已有 |
|
||||
| 提交结果 / 提交失败 | 已有 |
|
||||
| 定时心跳 | 不实现 |
|
||||
| 正式认证 | `[待定]`,当前读取但不校验 |
|
||||
|
||||
@@ -241,5 +241,5 @@ CMAutoBuy/ 整个文件夹拷到任何机器都能用
|
||||
**已定案(不再是待确认项):**
|
||||
|
||||
- Admin 只分配任务给指定 Client,Client 不读全局任务池。见 [04](04-admin-api-contract.md) §5.1。
|
||||
- **没有租约、没有心跳、没有状态回查。** Client 只有领取、提交结果、提交失败三个调用。见 [04](04-admin-api-contract.md) §1.1。
|
||||
- **没有租约、没有心跳、没有状态回查。** 任务流程只有领取、提交结果、提交失败三个调用;设置页另有无任务副作用的幂等 Client 登记调用。见 [04](04-admin-api-contract.md) §1、§4.1。
|
||||
- 本地只存已领取任务,已完成任务**永久保留**。诊断产物(截图、XML、日志)仍按 `diagnostics.retention_days` 清理,两者不是一回事。见 [03](03-data-model.md) §3.1。
|
||||
|
||||
@@ -315,7 +315,9 @@ stopped → polling → claiming → executing → submitting
|
||||
|
||||
## 8. 核心流程
|
||||
|
||||
Client 与 Admin 的全部交互只有三次调用:领一个任务、提交结果、提交失败。
|
||||
Client 与 Admin 的任务交互只有三种调用:领一个任务、提交结果、提交失败。
|
||||
设置页另有一个幂等 Client 登记调用;它不读取、领取或修改任务。
|
||||
项目仍然没有心跳、租约和 Admin 状态回查。
|
||||
**没有任何"去问 Admin 现在怎么想"的调用**,理由见 [04 接口契约](04-admin-api-contract.md) §1.1。
|
||||
|
||||
### 任务领取与执行
|
||||
|
||||
@@ -385,6 +385,7 @@ CREATE TABLE app_settings (
|
||||
|
||||
- `admin.base_url`
|
||||
- `admin.client_id`
|
||||
- `admin.client_name`
|
||||
- `admin.request_timeout_seconds`
|
||||
- `device.address`
|
||||
- `device.pdd_package`
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
|
||||
**核心:Admin 是调度器,Client 是执行器,执行器不参与调度决策。**
|
||||
|
||||
- Client 只有三个动作:领一个任务、提交结果、提交失败。**没有任何"去问 Admin 现在怎么想"的调用。**
|
||||
- Client 的任务流程只有三个动作:领一个任务、提交结果、提交失败。
|
||||
设置页另有一个幂等 Client 登记动作。**没有任何"去问 Admin 现在怎么想"的调用。**
|
||||
- Client 拿到任务就做完,中途不管任务是否被取消、是否被重派。
|
||||
- Admin 负责任务的创建、更新、取消和派发(含重派),这些 Client 一律不感知。
|
||||
- 结果提交必须幂等;网络重试不能创建重复结果。
|
||||
@@ -109,6 +110,59 @@ Idempotency-Key: <stable-key>
|
||||
|
||||
任务对象里**不含 Admin 侧状态**。Client 不关心 Admin 那边把它标成什么,只管做完提交。
|
||||
|
||||
## 4.1 登记或更新 Client
|
||||
|
||||
```http
|
||||
PUT /api/v1/client/registration
|
||||
X-Client-Id: <stable-client-id>
|
||||
X-Request-Id: <uuid>
|
||||
```
|
||||
|
||||
请求:
|
||||
|
||||
```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 OK`:
|
||||
|
||||
```json
|
||||
{
|
||||
"registered": true,
|
||||
"client_id": "CLIENT-123456",
|
||||
"registered_at": "2026-08-06T09:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
规则:
|
||||
|
||||
- `[必须]` `X-Client-Id` 是 Client 唯一键;相同编号重复 `PUT` 执行幂等 upsert,不创建重复记录。
|
||||
- `[必须]` 登记接口不读取、领取或修改任务,也不返回任务。
|
||||
- `[必须]` `client.name` 可选且最多 50 字。显式登记携带非空名称时允许更新 Admin 名称;空名称更新保留已有名称,新建时用 Client ID 兜底。
|
||||
- `[必须]` 更新设备、能力、`last_seen_at` 和 `updated_at`。
|
||||
- `[必须]` `supported_types` 非空且只包含 `collect`、`purchase`;`purchase_mode` 只允许 `dry_run`、`live`;`schema_versions` 只包含正整数。
|
||||
- `[必须]` Client 必须先把设备号和名称保存到 SQLite,再在后台线程调用本接口;远端失败不得回滚本地保存。
|
||||
- `[必须]` 设备号生成后稳定复用,不向 Admin 发送生成设备号所用的原始硬件参数。
|
||||
- `[必须]` 不增加定时心跳。在线状态仍由登记、领取和提交产生的 `last_seen_at` 派生。
|
||||
|
||||
错误至少包括 `MISSING_CLIENT_ID`、`INVALID_BODY`、`INVALID_CLIENT_PROFILE` 和 `CLIENT_REGISTER_FAILED`,并使用 §3 的统一结构。
|
||||
|
||||
`claim` 继续支持隐式登记作为旧 Client 的兼容兜底:新编号可以创建 Client,已有 Client 只更新设备、能力和最近活动时间,不覆盖名称。
|
||||
|
||||
## 5. 领取任务
|
||||
|
||||
```http
|
||||
@@ -145,12 +199,11 @@ POST /api/v1/client/tasks/claim
|
||||
|
||||
无可领取任务时返回 `204 No Content`。
|
||||
|
||||
`[必须]` **`204` 不是错误。** 新客户端第一次调 `claim` 必然是 204——
|
||||
它刚被登记,还没有人给它分配任务。Client 要把它当"暂时没活干"处理,
|
||||
不要报错,也不要因此触发重试风暴。
|
||||
`[必须]` **`204` 不是错误。** Client 要把它当"暂时没活干"处理,
|
||||
不要报错,也不要因此触发重试风暴。首次 claim 通常返回 204;如果该编号已经预先分配任务,也可以直接返回 200。
|
||||
|
||||
`client.name` 是给人看的显示名,可以不填(不填时 Admin 用 `X-Client-Id` 兜底)。
|
||||
它**只在首次登记时被采纳**,之后操作员在 Admin 改的名字不会被客户端覆盖。
|
||||
`client.name` 是给人看的显示名,可以不填(新建时 Admin 用 `X-Client-Id` 兜底)。
|
||||
在 claim 兼容登记中,它只在首次创建时被采纳,已有 Client 的名称不会被后台领取覆盖;用户通过 §4.1 显式保存非空名称时可以更新。
|
||||
|
||||
> 怎么把这条链路跑通、怎么确认设备登记成功,见
|
||||
> [Admin 侧的设备登记联调手册](../admin/07-设备登记联调手册.md)。
|
||||
@@ -266,9 +319,10 @@ Idempotency-Key: task-id:attempt-id:failure-v1
|
||||
|
||||
## 9. Mock Admin 要求
|
||||
|
||||
`MockAdminGateway` 与 HTTP 实现暴露同一应用层接口,只有三个方法:
|
||||
`MockAdminGateway` 与 HTTP 实现暴露同一应用层接口。登记和三个任务方法都必须使用同一契约:
|
||||
|
||||
```text
|
||||
register_client(client, capabilities) # §4.1
|
||||
claim_next(client, capabilities) # §5
|
||||
submit_result(task_id, idempotency_key, result) # §6
|
||||
submit_failure(task_id, idempotency_key, failure) # §7
|
||||
@@ -300,7 +354,7 @@ Admin 还没做完,下面这些要和 Admin 一起定。**不许因此停工**
|
||||
**已定案(不再是待确认项):**
|
||||
|
||||
- Admin 只把任务分配给指定 Client,Client 不读全局任务池。见 §5.1。
|
||||
- **没有租约、没有心跳、没有状态回查。** Client 只有 §5 / §6 / §7 三个调用。见 §1.1。
|
||||
- **没有租约、没有心跳、没有状态回查。** 任务流程只有 §5 / §6 / §7 三个调用;设置页另有 §4.1 的幂等登记调用。
|
||||
- Admin 必须无条件接受已派发过的 Client 提交的结果。见 §6.1。
|
||||
|
||||
改动这张表里任何一条,都属于会影响业务结果的变更,必须先更新工单并经用户确认。
|
||||
|
||||
Reference in New Issue
Block a user