docs: 定义 Admin 登录与账号管理基线 (#49)

This commit is contained in:
chengma
2026-08-09 13:12:03 +08:00
parent 6e6e99a5d7
commit fc03388ed4
5 changed files with 161 additions and 5 deletions
+15 -5
View File
@@ -19,10 +19,12 @@ Admin 的职责是把这三方的数据串起来,最终产出 Client 能执行
3. 把蝦皮的颜色尺码,对应到拼多多的颜色尺码,**且这个对应关系可复用**。
4. 生成采购任务并分配给指定客户端,跟踪执行结果。
5. 管理客户端清单,知道谁在干活。
6. 通过登录保护 Admin 网页,并由管理员维护采购员账号。
## 2. 用户与外部系统
- **操作人员:**导入报表、填 PDD 链接、发起采集、做规格匹配、创建并分配任务、处理异常。
- **管理员:**首次初始化 Admin,执行全部业务操作,并创建、禁用或重置采购员账号。
- **采购员:**登录后执行现有业务操作,但不能管理用户。
- **Client:**领取任务、在安卓设备上执行、提交结果。契约见 [Client 侧文档](../client/04-admin-api-contract.md)。
- **蝦皮:**只提供 Excel 报表导出,**没有接口对接**。
- **顺运宝:**提供货运单,`[待定]` 同步方式待确认,MVP 先做占位按钮。
@@ -396,13 +398,15 @@ MVP 包含:
- 规格匹配弹窗与可复用映射;
- 创建采购任务并分配客户端;
- 给 Client 的四个接口:登记、领取、提交结果、提交失败;
- 客户端自动注册与在线状态。
- 客户端自动注册与在线状态;
- 第一次启动时初始化一个管理员;
- Admin 网页登录、退出和 Session;
- 管理员创建、禁用和重置采购员账号。
MVP 之后:
- 接入顺运宝真实同步;
- 打包成 exe;
- 权限与多用户;
- 统计报表。
## 9. 非目标
@@ -411,7 +415,9 @@ MVP 之后:
- 不直接对接蝦皮和拼多多的接口。
- 不自动决定买哪个商品——PDD 链接和规格匹配都由人确认。
- 不自动付款。
- MVP 不做用户登录和权限体系。
- 不开放用户自助注册;第一个管理员只能通过首次初始化创建。
- 不做复杂 RBAC、细粒度页面权限、单点登录和第三方登录。
- 本阶段不做 Client 登录或 API Key,不改变 Client 四接口契约。
## 10. 非功能需求
@@ -419,6 +425,7 @@ MVP 之后:
- 页面在 1366×768 上可正常使用。
- 导入 1 万行 Excel 应在可接受时间内完成,并显示进度或结果统计。
- 所有写操作有 CSRF 防护,所有 SQL 参数化。
- 密码只保存成熟算法生成的哈希;Session 有过期、退出和账号禁用失效机制。
- 日志、页面、导出不含 token、密码、Cookie。
- 数据放程序旁边的 `data/`,便携模式,见 [02 架构](02-architecture.md) §6。
@@ -430,7 +437,7 @@ MVP 之后:
|---|---|---|
| 1 | 顺运宝同步方式(接口?导出文件?) | 按钮占位,数据靠手工录入或造数 |
| 2 | 蝦皮有无全量规格导出 | 没有。靠反复导入累积 + 手动新增补齐 |
| 3 | Admin 是否需要登录 | MVP 不做,仅本机/内网访问 |
| 3 | Admin 是否需要登录 | 已定案:MVP 实现管理员初始化、网页登录和最小账号管理 |
| 4 | 是否需要利润和汇率展示 | 不做 |
**已定案:**
@@ -444,4 +451,7 @@ MVP 之后:
- SKU 映射**独立成表且可复用**,同一蝦皮 SKU 只人工匹配一次。
- 货运单的"规格 SKU"**直接对应蝦皮 `商品規格ID`**,不需要转换。
但**不加外键**——本地查不到该 SKU 是常态,见 [03 数据模型](03-data-model.md) §4。
- Admin 不提供固定默认密码;第一次启动由用户创建第一个管理员,之后初始化入口永久关闭。
- 账号只有 `admin`(管理员)和 `purchaser`(采购员)两种固定角色;采购员不能管理用户。
- Web 登录只保护 HTML 页面,`/api/v1/client/*` 不使用网页登录 Session,现有 Client 行为保持不变。
- Go 版本固定 **1.23.0**。
+34
View File
@@ -46,6 +46,31 @@
- `[必须]` `handler/web` 和 `handler/api` **分开**:
页面出错要渲染错误页,接口出错要返回 JSON;页面要 CSRF,接口要认证。
混在一起迟早写错。
- `[必须]` Web 登录中间件只挂在 `handler/web` 路由组,不能挂到整个 Gin Engine。
`/api/v1/client/*` 本阶段保持原有接口行为,不能返回登录页或 302 重定向。
### 2.1 登录与路由边界
```text
公开网页
├── GET/POST /setup 仅数据库没有用户时可用
├── GET/POST /login
└── /static/*
需要登录的网页
├── /shopee /pdd /syb /tasks /clients
├── POST /logout
└── /users 仅管理员
保持原样的 Client API
└── /api/v1/client/*
```
- `Session` 是服务器保存的网页登录状态,浏览器只持有随机 Cookie。
- 没有任何用户时,业务网页跳转到 `/setup`;初始化完成后 `/setup` 的服务端处理永久拒绝再次创建管理员。
- 有用户但未登录时,业务网页跳转到 `/login`;API 继续返回原有 JSON 或 `204`。
- Handler 从中间件写入的当前用户读取身份,不信任表单里的 `user_id` 或 `role`。
- 采购员可以使用现有业务页面,但访问 `/users` 必须返回 `403`。
## 3. 目录
@@ -56,6 +81,8 @@ admin/
├── config/ 端口、路径、超时
├── handler/
│ ├── web/ 五个模块的页面
│ │ ├── auth.go 初始化、登录、退出
│ │ ├── user.go 管理员维护采购员
│ │ ├── shopee.go
│ │ ├── syb.go
│ │ ├── task.go
@@ -70,6 +97,7 @@ admin/
│ └── clientreg.go 客户端注册与在线判断
├── repository/
│ ├── db.go 连接、迁移
│ ├── user.go 用户和 Web Session
│ ├── shopee.go
│ ├── syb.go
│ ├── task.go
@@ -90,6 +118,10 @@ admin/
```text
templates/
├── layout.html 整体骨架:导航 + 内容占位 + 状态条
├── auth/
│ ├── setup.html 首次管理员初始化
│ └── login.html
├── user/list.html 采购员账号管理
├── shopee/
│ ├── list.html 表格页
│ └── edit_modal.html 编辑弹窗(含 PDD 链接和采集按钮)
@@ -216,6 +248,8 @@ handler 渲染结果页:导入 5195 商品 / 6092 SKU,失败 0 行
- Admin 是任务的创建者和分配者,Client 是执行者。
- Admin **不知道** Client 执行到哪一步(没有心跳,是有意的)。
- Client 提交结果时,Admin **无条件接受**,哪怕任务已取消或已重派。
- Admin 用户登录与 Client 身份是两套边界。Web Session 不传给 Client,
Web 登录中间件也不覆盖 `/api/v1/client/*`。
- 详见 [04 Client 接口实现](04-client-api.md)。
## 9. 相关文档
+46
View File
@@ -95,6 +95,7 @@ SQLite 同一时刻只允许一个写事务,连接放太开会互相抢锁、
| v3 | 把 PDD 采集数据从 `shopee_products` 拆到独立的 `pdd_products`(本文档 §4 描述的最终结构);重建 `shopee_products`,去掉已经搬走的四个字段;重建 `sku_mappings`,主键改成 `(shopee_sku_id, pdd_goods_id)`(§6.1 的理由)。 |
| v4 | `pdd_products` 增加可空的 `shop_name`;老数据保持 `NULL`。 |
| v5 | 顺运宝货运单同步(工单 #46):新增 `syb_session`(会话缓存)、`syb_sync_state`(同步进度)两张表;`syb_orders` 增加可空的 `product_spec`(规格原文)。三条都是新增,v1–v4 一个字节没改。 |
| v6(规划) | Admin 网页登录:新增 `users` 和 `web_sessions`。正式实现时只能在迁移末尾追加,不得提前改写 v1–v5。 |
**v3 为什么丢弃旧 `sku_mappings` 数据(见 #20):** 新主键需要 `pdd_option_key`,
这是 Go 的 `service.OptionKey()` 用 `json.Marshal` 算出来的规范化键,SQL 语句
@@ -707,3 +708,48 @@ Admin 和 Client **各有一个 SQLite,互不相通**,只通过接口交换
`[必须]` **两边的状态是两套,不要试图同步。**
Admin 只知道"发出去了 / 收到结果了",中间过程看不到,这是有意的设计。
## 12. Admin 用户和 Web Session(v6 规划)
`users` 保存网页登录账号。没有公开注册,第一条管理员记录只能由首次初始化流程创建。
```sql
CREATE TABLE users (
user_id TEXT PRIMARY KEY,
username TEXT NOT NULL COLLATE NOCASE UNIQUE,
password_hash TEXT NOT NULL,
role TEXT NOT NULL CHECK (role IN ('admin', 'purchaser')),
status TEXT NOT NULL CHECK (status IN ('active', 'disabled')),
last_login_at TEXT,
password_changed_at TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
```
- `password_hash` 只保存成熟密码哈希算法的结果,绝不保存或记录明文密码。
- 用户不做物理删除;离职或停用改为 `disabled`,保留任务和操作记录的可追溯性。
- 首次初始化必须在写事务中再次确认 `users` 为空;SQLite 同一时刻只有一个写事务,
并发提交最多一个成功。即使所有账号都被禁用,初始化入口也不能重新开放。
- 系统不能禁用最后一个有效管理员。
`web_sessions` 保存网页登录状态。数据库只保存随机 Session Token 的 SHA-256 哈希,
Cookie 原文不能进入数据库、日志或页面。
```sql
CREATE TABLE web_sessions (
session_hash TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
expires_at TEXT NOT NULL,
created_at TEXT NOT NULL,
last_seen_at TEXT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(user_id)
);
CREATE INDEX idx_web_sessions_user ON web_sessions(user_id);
CREATE INDEX idx_web_sessions_expiry ON web_sessions(expires_at);
```
- Session 使用固定过期时间,MVP 默认 12 小时,不做复杂刷新令牌。
- 退出登录、密码重置或账号禁用时,删除该用户对应的 Session 记录。
- 过期 Session 可以在登录、退出或定期维护时清理,不需要后台常驻线程。
+45
View File
@@ -601,6 +601,51 @@ placeholder 写「任务编号 / 订单号 / 商品 ID」,**不要写全「PDD
- `[建议]` 界面上说明一句:"客户端执行长任务期间可能显示为离线,属正常现象。"
因为没有心跳,这是已知且接受的取舍(见 [04](04-client-api.md) §3)。
### 8.1 首次管理员初始化与登录
数据库没有任何用户时,访问 Admin 业务页面跳转到 `/setup`。初始化页只包含:
```text
初始化管理员
用户名 [admin____________]
密码 [••••••••••••••••]
确认密码 [••••••••••••••]
[创建管理员]
```
- 用户名可以预填 `admin`,但用户可以修改;不得提供固定默认密码。
- 密码框不回显,校验失败时清空密码和确认密码。
- 初始化成功后跳转登录页;此后访问 `/setup` 不再显示初始化表单。
- 两个浏览器并发初始化时,最多一个成功,另一个提示“管理员已经初始化,请登录”。
已有用户时,未登录访问业务页面跳转到 `/login`:
```text
登录 Admin
用户名 [_________________]
密码 [••••••••••••••••]
[登录]
```
- 用户名或密码错误统一提示“用户名或密码错误”,不透露账号是否存在。
- 登录成功回到原目标页面;目标地址只允许站内路径,不能跳转到外部网站。
- 顶部导航显示当前用户名和“退出登录”;退出必须使用 POST。
- Client API 不显示登录页,也不返回网页登录重定向。
### 8.2 用户管理页
只有管理员显示“用户管理”导航并可访问 `/users`。采购员访问时返回 `403`。
工具条:`用户名 [____] 状态 [全部⌄] [搜索] [新增采购员]`
表格列:用户名 / 角色 / 状态 / 最后登录 / 创建时间 / 操作
- 管理员只能新增 `purchaser` 角色,不通过普通页面新增第二个管理员。
- 操作包含“重置密码”“禁用/启用”;第一版不做物理删除。
- 禁用账号或重置密码前二次确认;成功后该用户现有 Session 全部失效。
- 不能禁用最后一个有效管理员,也不能让唯一管理员禁用自己。
- 完整密码不在列表、弹窗关闭后的页面或日志中出现。
## 9. 反馈方式
| 场景 | 怎么反馈 |
+21
View File
@@ -28,6 +28,9 @@
- 金额换算显示(分 ↔ 元),边界值 0 和大额;
- SKU 映射复用:第二次匹配同一 SKU 应自动带出;
- 在线状态派生:`last_seen_at` 刚好在边界前后。
- 密码哈希校验:正确密码成功,错误密码失败,数据库不出现明文密码;
- 角色校验:管理员可以管理用户,采购员访问用户管理返回 `403`;
- 最后管理员保护:不能禁用最后一个有效管理员。
`[必须]` 导入相关的测试用 `admin/testdata/` 下的**小样本**(几十行),
不要读完整报表。
@@ -61,6 +64,10 @@
- 空数据、少量数据、大量数据三种情况;
- 批量删除的二次确认存在;
- 校验失败时输入不丢。
- 没有用户时业务页面跳转初始化页,初始化后入口永久关闭;
- 未登录访问业务页面跳转登录页,登录后正常渲染;
- 退出、Session 过期、密码重置和账号禁用后不能继续访问;
- Client API 不返回登录页或 302 重定向。
### 2.4 契约测试
@@ -93,6 +100,15 @@
不能 panic 把整个进程带崩。
- `[必须]` 文件名不得直接用于拼路径(路径穿越),落盘时用自己生成的名字。
- `[建议]` MVP 只监听 `127.0.0.1`,不对外暴露。要给内网用再单独评估。
- `[必须]` 不提供固定默认密码和公开注册;第一位管理员由用户首次初始化。
- `[必须]` 密码使用成熟算法哈希,禁止自创加密、明文保存或可逆加密。
- `[必须]` 首次管理员创建必须在数据库写事务中完成,并发请求最多一个成功。
- `[必须]` 登录成功后使用新的随机 Session Token,数据库只保存其 SHA-256 哈希。
- `[必须]` 登录 Cookie 设置 `HttpOnly`、`SameSite=Lax`、`Path=/`;HTTPS 部署时设置 `Secure`。
- `[必须]` Session 默认 12 小时过期;退出、密码重置和账号禁用立即撤销对应 Session。
- `[必须]` `/setup`、`/login`、`/logout` 和用户管理写操作都保留 CSRF 防护。
- `[必须]` Web 登录中间件只保护 HTML 路由,不得覆盖 `/api/v1/client/*`。
- `[建议]` 对连续登录失败做简单限速;错误提示不区分用户名不存在和密码错误。
## 5. 错误处理
@@ -131,6 +147,8 @@
- `task_result_received`
- `task_result_received_but_cancelled` ← 这条要单独记,方便事后对账
- `client_registered`
- `admin_initialized`、`user_login_succeeded`、`user_login_failed`
- `user_created`、`user_disabled`、`user_enabled`、`user_password_reset`
`[必须]` **不得记录** token、密码、Cookie,也不要把完整请求体无脑打进日志。
@@ -157,6 +175,9 @@
| 8 | 干净环境启动 | 没装过本项目的机器上 `go run .` 或跑 exe | 开发者 |
| 9 | 日志和页面无敏感信息 | 翻一遍 `data/logs/` | 开发者 |
| 10 | 开源许可证已确认 | 新增依赖的许可证 | **项目负责人** |
| 11 | 登录与角色测试通过 | 初始化、登录、退出、禁用、最后管理员保护 | 开发者 |
| 12 | Client API 回归通过 | 四接口不重定向、不返回 HTML,契约测试全绿 | 开发者 |
| 13 | Session 安全属性正确 | 检查 Cookie 属性、过期和撤销 | 开发者 |
第 10 项开发者**不要自己判断放行**,把包名和许可证类型报给项目负责人。