feat: 增加客户端采购员归属管理 (#54)

This commit is contained in:
chengma
2026-08-09 16:02:25 +08:00
parent 0181170457
commit e0c0dac7a0
17 changed files with 766 additions and 37 deletions
+13 -2
View File
@@ -295,9 +295,10 @@ PDD 商品之所以单独一个模块,是因为它在数据上就是**独立
### 4.5 客户端列表模块
**顶部工具条:** 客户端名称搜索框、搜索按钮、删除按钮。
**顶部工具条:** 客户端名称搜索框、搜索按钮;管理员另有删除按钮。
**中间表格:** 勾选、名称、序列号、状态、最近活动时间、更新时间。
**中间表格:** 名称、序列号、状态、当前负责人、最近活动时间、更新时间;
管理员另有勾选和操作列。
**注册方式:**
@@ -308,6 +309,15 @@ PDD 商品之所以单独一个模块,是因为它在数据上就是**独立
最近活动时间在 N 分钟内算"在线",否则"离线"。`[建议]` N 默认 10 分钟。
- 名称由客户端上报,操作员可以在 Admin 这边改成好记的名字。
**采购员归属:**
- 一个采购员可以绑定多台客户端;一台客户端同一时间最多绑定一个采购员,也允许未绑定。
- 只有管理员能绑定、转交、解绑和删除客户端;采购员只读看到当前绑定给自己的客户端。
- 转交和解绑保留负责人、操作管理员、开始/结束时间等完整历史。
- 归属只影响 Web 可见范围和采购任务创建时的客户端候选,不改变 Client 四接口,
也不改动已分配、领取或执行中的任务。
- 新绑定目标必须是启用中的采购员。已禁用采购员的历史不删除。
**底部状态条:** 在线 / 离线数量统计。
## 5. 创建采购任务的校验
@@ -402,6 +412,7 @@ MVP 包含:
- 第一次启动时初始化一个管理员;
- Admin 网页登录、退出和 Session;
- 管理员创建、禁用和重置采购员账号。
- 管理员绑定、转交和解绑客户端;采购员只读查看自己的客户端。
MVP 之后:
+34
View File
@@ -96,6 +96,7 @@ SQLite 同一时刻只允许一个写事务,连接放太开会互相抢锁、
| v4 | `pdd_products` 增加可空的 `shop_name`;老数据保持 `NULL`。 |
| v5 | 顺运宝货运单同步(工单 #46):新增 `syb_session`(会话缓存)、`syb_sync_state`(同步进度)两张表;`syb_orders` 增加可空的 `product_spec`(规格原文)。三条都是新增,v1–v4 一个字节没改。 |
| v6(#50) | Admin 网页登录:新增 `users` 和 `web_sessions`,只在迁移末尾追加,未改写 v1–v5。 |
| v7(#54) | 客户端负责人:新增 `client_user_assignments` 和当前归属唯一索引,保留绑定、转交、解绑历史;未改写 v1–v6。 |
**v3 为什么丢弃旧 `sku_mappings` 数据(见 #20):** 新主键需要 `pdd_option_key`,
这是 Go 的 `service.OptionKey()` 用 `json.Marshal` 算出来的规范化键,SQL 语句
@@ -689,6 +690,8 @@ shopee_products ──1:N──→ shopee_skus
(skus_json 里是所有规格和价格)
syb_orders ──创建──→ tasks ──分配──→ clients
↑
users(采购员)──1:N 当前归属──────────┘
```
两条关联都可以变,这是有意的:
@@ -753,3 +756,34 @@ CREATE INDEX idx_web_sessions_expiry ON web_sessions(expires_at);
- Session 使用固定过期时间,MVP 默认 12 小时,不做复杂刷新令牌。
- 退出登录、密码重置或账号禁用时,删除该用户对应的 Session 记录。
- 过期 Session 可以在登录、退出或定期维护时清理,不需要后台常驻线程。
## 13. `client_user_assignments` 客户端负责人历史(v7)
`clients` 是执行任务的软件实例,`users` 是登录 Admin 的人,两者生命周期不同,
因此保持两张独立实体表,用归属历史表连接,不能合并字段。
```sql
CREATE TABLE client_user_assignments (
assignment_id TEXT PRIMARY KEY,
client_id TEXT NOT NULL,
user_id TEXT NOT NULL,
started_at TEXT NOT NULL,
ended_at TEXT,
assigned_by_user_id TEXT NOT NULL,
ended_by_user_id TEXT,
end_reason TEXT CHECK (end_reason IS NULL OR end_reason IN ('unbind', 'transfer')),
FOREIGN KEY (user_id) REFERENCES users(user_id),
FOREIGN KEY (assigned_by_user_id) REFERENCES users(user_id),
FOREIGN KEY (ended_by_user_id) REFERENCES users(user_id)
);
CREATE UNIQUE INDEX idx_client_assignment_current
ON client_user_assignments(client_id) WHERE ended_at IS NULL;
```
- `ended_at IS NULL` 表示当前归属;部分唯一索引保证一台客户端最多一个当前负责人。
- 首次绑定只新增记录;转交在同一事务结束旧记录并新增记录;解绑只结束旧记录。
- `assigned_by_user_id` / `ended_by_user_id` 都是执行操作的管理员,不是目标采购员。
- `client_id` 故意不设指向 `clients` 的外键:客户端清单允许删除后由同一稳定编号
重新登记,归属和审计历史不能随临时清单记录丢失。
- 归属记录不参与 Client API 的登记和领取判断,也不更新 `tasks.assigned_client`。
+13 -2
View File
@@ -609,9 +609,11 @@ placeholder 写「任务编号 / 订单号 / 商品 ID」,**不要写全「PDD
## 8. 客户端列表页
工具条:`名称 [____] [搜索] [删除]`
管理员工具条:`名称 [____] [搜索] [删除]`;采购员不显示删除按钮。
表格列:☐ / 名称 / 序列号 / 状态 / 最近活动 / 更新时间
管理员表格列:☐ / 名称 / 序列号 / 状态 / 当前负责人 / 最近活动 / 更新时间 / 操作
采购员表格列:名称 / 序列号 / 状态 / 当前负责人 / 最近活动 / 更新时间
- `[必须]` **状态是算出来的**:`最近活动` 在 N 分钟内为"在线",否则"离线"。
`[建议]` N 默认 10 分钟。
@@ -619,6 +621,13 @@ placeholder 写「任务编号 / 订单号 / 商品 ID」,**不要写全「PDD
**客户端上报的名称不再覆盖它**。
- `[建议]` 界面上说明一句:"客户端执行长任务期间可能显示为离线,属正常现象。"
因为没有心跳,这是已知且接受的取舍(见 [04](04-client-api.md) §3)。
- `[必须]` 管理员看到全部客户端;采购员只看到当前绑定给自己的客户端,页面只读。
- `[必须]` 未绑定显示文字“未绑定”,不能只靠颜色或留空表达。
- 管理员操作列:未绑定显示“绑定”;已绑定显示“转交”和“解绑”。
- 绑定/转交共用弹窗,负责人下拉框只列启用中的采购员。转交时明确显示当前负责人。
- 解绑前二次确认,说明采购员会立即看不到客户端,但归属历史和既有任务都会保留。
- 弹窗说明:“只改变负责人和网页可见范围,不影响 Client 接口或既有任务。”
- 采购员没有客户端时显示“请联系管理员绑定”,不显示容易误解为系统无数据的通用空状态。
### 8.1 首次管理员初始化与登录
@@ -675,6 +684,8 @@ placeholder 写「任务编号 / 订单号 / 商品 ID」,**不要写全「PDD
| 校验不通过 | 保留用户输入,**焦点移到第一个错误字段**,就近显示错误 |
| 批量操作部分失败 | 列出失败项和原因,不要只说"部分失败" |
| 删除 | 二次确认框,写明"将删除 N 条,不可恢复" |
| 客户端绑定/转交 | 弹窗确认目标采购员;成功后刷新负责人并显示结果 |
| 客户端解绑 | 二次确认,写明可见范围立即变化、历史和既有任务保留 |
`[必须]` 报错要说清**哪一步失败、下一步做什么**,不要把 Go 的错误堆栈贴到页面上。
堆栈写日志。
+7
View File
@@ -31,6 +31,8 @@
- 密码哈希校验:正确密码成功,错误密码失败,数据库不出现明文密码;
- 角色校验:管理员可以管理用户,采购员访问用户管理返回 `403`;
- 最后管理员保护:不能禁用最后一个有效管理员。
- 客户端归属:一人多客户端、一台客户端唯一当前负责人、转交/解绑历史完整;
- 归属权限:采购员只看到自己的客户端,不能绑定、解绑或删除;禁用采购员不能成为新目标。
`[必须]` 导入相关的测试用 `admin/testdata/` 下的**小样本**(几十行),
不要读完整报表。
@@ -68,6 +70,8 @@
- 未登录访问业务页面跳转登录页,登录后正常渲染;
- 退出、Session 过期、密码重置和账号禁用后不能继续访问;
- Client API 不返回登录页或 302 重定向。
- 管理员客户端页显示全量和负责人;采购员客户端页只读且只显示自己的客户端;
- 绑定/转交弹窗有明确标签和焦点,解绑有二次确认,操作结果可被辅助技术读到。
### 2.4 契约测试
@@ -107,6 +111,7 @@
- `[必须]` 登录 Cookie 设置 `HttpOnly`、`SameSite=Lax`、`Path=/`;HTTPS 部署时设置 `Secure`。
- `[必须]` Session 默认 12 小时过期;退出、密码重置和账号禁用立即撤销对应 Session。
- `[必须]` `/setup`、`/login`、`/logout` 和用户管理写操作都保留 CSRF 防护。
- `[必须]` 客户端绑定、转交、解绑和删除只能由管理员执行,并保留 CSRF 防护。
- `[必须]` Web 登录中间件只保护 HTML 路由,不得覆盖 `/api/v1/client/*`。
- `[建议]` 对连续登录失败做简单限速;错误提示不区分用户名不存在和密码错误。
@@ -149,6 +154,7 @@
- `client_registered`
- `admin_initialized`、`user_login_succeeded`、`user_login_failed`
- `user_created`、`user_disabled`、`user_enabled`、`user_password_reset`
- `client_assignment_changed`、`client_assignment_ended`(只记录管理员用户名和相关 ID)
`[必须]` **不得记录** token、密码、Cookie,也不要把完整请求体无脑打进日志。
@@ -178,6 +184,7 @@
| 11 | 登录与角色测试通过 | 初始化、登录、退出、禁用、最后管理员保护 | 开发者 |
| 12 | Client API 回归通过 | 四接口不重定向、不返回 HTML,契约测试全绿 | 开发者 |
| 13 | Session 安全属性正确 | 检查 Cookie 属性、过期和撤销 | 开发者 |
| 14 | 客户端归属正确 | 唯一当前负责人、历史、角色可见范围、既有任务不变 | 开发者 |
第 10 项开发者**不要自己判断放行**,把包名和许可证类型报给项目负责人。