From fc03388ed46285a6eb5629f77388facb8fe31bf8 Mon Sep 17 00:00:00 2001 From: chengma Date: Sun, 9 Aug 2026 13:12:03 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E5=AE=9A=E4=B9=89=20Admin=20=E7=99=BB?= =?UTF-8?q?=E5=BD=95=E4=B8=8E=E8=B4=A6=E5=8F=B7=E7=AE=A1=E7=90=86=E5=9F=BA?= =?UTF-8?q?=E7=BA=BF=20(#49)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/admin/01-requirements.md | 20 ++++++++++---- docs/admin/02-architecture.md | 34 +++++++++++++++++++++++ docs/admin/03-data-model.md | 46 +++++++++++++++++++++++++++++++ docs/admin/05-ui-specification.md | 45 ++++++++++++++++++++++++++++++ docs/admin/06-quality-security.md | 21 ++++++++++++++ 5 files changed, 161 insertions(+), 5 deletions(-) diff --git a/docs/admin/01-requirements.md b/docs/admin/01-requirements.md index eb4d4fa..a9cf12a 100644 --- a/docs/admin/01-requirements.md +++ b/docs/admin/01-requirements.md @@ -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**。 diff --git a/docs/admin/02-architecture.md b/docs/admin/02-architecture.md index 0975a5d..c16d337 100644 --- a/docs/admin/02-architecture.md +++ b/docs/admin/02-architecture.md @@ -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. 相关文档 diff --git a/docs/admin/03-data-model.md b/docs/admin/03-data-model.md index eeeba92..e047697 100644 --- a/docs/admin/03-data-model.md +++ b/docs/admin/03-data-model.md @@ -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 可以在登录、退出或定期维护时清理,不需要后台常驻线程。 diff --git a/docs/admin/05-ui-specification.md b/docs/admin/05-ui-specification.md index 456d103..4f4c9b0 100644 --- a/docs/admin/05-ui-specification.md +++ b/docs/admin/05-ui-specification.md @@ -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. 反馈方式 | 场景 | 怎么反馈 | diff --git a/docs/admin/06-quality-security.md b/docs/admin/06-quality-security.md index b828dad..163e7c7 100644 --- a/docs/admin/06-quality-security.md +++ b/docs/admin/06-quality-security.md @@ -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 项开发者**不要自己判断放行**,把包名和许可证类型报给项目负责人。