feat: 新增幂等 Client 登记接口 (#12)

PUT /api/v1/client/registration —— 设置页点"保存"时调用,
只登记客户端,不碰任务。

为什么需要它
原设计"注册就在 claim 里做"有个真问题:设置页保存被迫调 claim,
而 claim 可能真的领到一个任务——Admin 那边已把任务标成 claimed,
Client 必须可靠落库否则任务就丢了。一个"保存设置"的动作
不该承担"领取任务并保证不丢"的责任。这违反了本项目自己的原则
(05 §1:界面上只有一个会产生外部后果的命令)。

实现
- ClientProfileRequest + Validate() 由**登记和领取共用**,
  避免两个入口的结构和校验各写一份、迟早漂移
- 校验:名称 <=50 字(按字符不按字节,中文一个字三字节)、
  supported_types 非空且只含 collect/purchase、platform 只支持 android、
  purchase_mode 必填且只允许 dry_run/live、schema_versions 均为正整数
- 非法内容返回 422 INVALID_CLIENT_PROFILE,错误消息指明具体字段
- UpsertClient 加 explicit 参数区分名称规则:
  显式登记(用户点保存)带非空名称时更新名称;
  隐式登记(claim 顺带)永不更新,否则操作员改的名字会被反复冲掉

已验证(Go 1.23.0)
- 单元测试 40 个全过,含"登记不产生任何任务副作用"的快照比对
- 端到端逐条走完手册 §5.2~5.7:重复登记记录数恒为 1;
  更新/空名称行为正确;插入任务后登记 3 次任务字段完全未变且仍可领取;
  四种非法输入均 422 且不写库;claim 不受影响

一处行为变更需注意
名称归属规则改了:原来是"Admin 操作员永远赢",现在是"最后一次
显式操作赢"——用户在 Client 点保存会覆盖 Admin 侧改的名字。
按 #12 文档实现,已拆成三个独立测试盯住三种情况。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
chengma
2026-08-06 17:31:38 +08:00
co-authored by Claude Opus 5
parent 02073eab37
commit 5a5c1f1f68
9 changed files with 516 additions and 79 deletions
+18 -4
View File
@@ -1,6 +1,6 @@
# 07 设备登记联调手册
- 文档状态:登记接口契约已确认,等待 Gitea #12 实现和真实联调
- 文档状态:Admin 侧(#12)已实现并通过 curl 验证,等待 Client 侧(#11)接入
- 读者:Admin 登记接口、Client `HttpAdminGateway` 开发者
- 目标:让 Client 设置页安全登记设备,同时不领取任务
@@ -11,7 +11,7 @@
- Gitea #12:Admin 新增幂等 Client 登记接口
- Gitea #11:Client 保存当前设备并登记 Client
本文中的登记请求是 #12 的目标契约。在 #12 完成并通过真实 curl 验证前,不得声称登记接口已经可用。
本文中的登记请求**已在真实运行的 Admin 上验证通过**(#12 已完成),下面 §5 的每条命令都是实跑结果。
---
@@ -231,7 +231,21 @@ curl -i -X PUT http://127.0.0.1:8080/api/v1/client/registration \
至少验证缺少 `X-Client-Id`、名称超过 50 字、空 `supported_types` 和非法 `purchase_mode`。
#12 完成时,把上述真实命令的结果补回本节,并勾选 Gitea 验收项。
**实跑结果(Go 1.23.0,2026-08-06):**
| 验证项 | 结果 |
|---|---|
| 5.2 首次登记 | `200`,`registered: true` |
| 5.3 重复登记 3 次 | `200`,客户端记录数保持 **1** |
| 5.4 更新名称 | `200`,名称变为"联调测试机-已更新",记录数仍为 1 |
| 5.5 空名称更新 | `200`,**名称保持不变** |
| 5.6 无任务副作用 | 插入任务后登记 3 次,任务的 `status` / `claimed_at` / `updated_at` **完全未变**;响应**不含 `task` 字段**;登记后任务仍能被正常领取 |
| 5.7 缺 `X-Client-Id` | `400 MISSING_CLIENT_ID` |
| 5.7 名称 51 字 | `422 INVALID_CLIENT_PROFILE`,消息指明"client.name 最多 50 个字,收到 51 个" |
| 5.7 `supported_types` 为空 | `422 INVALID_CLIENT_PROFILE` |
| 5.7 `purchase_mode` 非法 | `422 INVALID_CLIENT_PROFILE` |
| 校验不过时不写库 | 三次非法请求后记录数仍为 1 |
| claim 仍可用 | `200`,正常领到任务 |
---
@@ -284,7 +298,7 @@ X-Client-Id: test-device-001
| 能力 | 状态 |
|---|---|
| 独立登记契约 | **已确认** |
| Admin 独立登记实现 | 等待 #12 |
| Admin 独立登记实现 | **已完成(#12)** |
| Client 设置页登记实现 | 等待 #11 和 #12 |
| claim 隐式登记 | 已有,保留兼容 |
| 领取任务 | 已有 |