仓库从单子项目变成两个:client(Python + PyQt5 桌面端)和 admin(Go + Gin + HTML 模板 Web 管理端)。两者技术栈完全不同, 规则各自独立,只通过三个 HTTP 接口交互。 新增 admin/AGENTS.md 和 docs/admin/ 共 9 份文档,结构与 docs/client/ 对齐。 关键设计决策(均已与用户确认) - 四模块:蝦皮数据 / 顺运宝数据 / 采购任务 / 客户端列表, 统一三段式布局(工具条 / 带勾选的表格 / 状态条) - 蝦皮数据拆成商品表 + SKU 表:报表本身就是两层结构, 且 PDD 链接是商品级的,放 SKU 级会重复维护 - pdd_data 存商品级,一个 PDD 商品采一次,所有订单共用 - SKU 映射独立成表且可复用,同一蝦皮 SKU 只需人工匹配一次 - PDD 链接人工填写,采集按钮就放在填链接的编辑弹窗里 - 任务分配给指定客户端;不加心跳,注册在领取时完成, 在线状态由 last_seen_at 派生 - SQLite 驱动固定 modernc.org/sqlite(纯 Go 免 cgo,go build 直接出 exe) - 不引入前端框架和 npm 构建,只允许原生 JS 或 htmx 基于样本文件 raw_data/蝦皮数据样本.xlsx 实测得出的约束 - 11287 行 = 5195 商品汇总行 + 6092 SKU 行,导入必须分开处理 - 商品規格ID 零重复,是天然主键 - 规格原文两种格式各占 53.4% / 46.6%,只解析【】会漏掉一半 - 平均每商品仅 1.17 个 SKU,报表不是全量目录, 因此必须支持手动新增,且货运单外键不能加硬约束 同步更新 - 根 AGENTS.md 开头的指针改为两个子项目对照表 - docs/README.md 重构为双子项目索引 - .gitignore 加 admin/data/、admin.exe、Excel 锁文件 说明:Gitea 尚未配置,本次无对应工单号。 raw_data/ 未提交,含台币销售额等商业数据,待用户决定。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
8.7 KiB
8.7 KiB
02 Admin 系统架构
- 文档状态:基线草案,待架构评审
- 适用范围:
admin/
本文档中 [必须] / [建议] / [待定] 的含义见 文档索引。
没有标注的默认是 [必须]。看不懂的词查 术语表。
1. 架构目标
- 结构简单到初级程序员能独立加一个模块;
- 页面服务端渲染,不引入前端构建链;
- 业务逻辑和 Web 框架解耦,方便单测;
- 给 Client 的接口和给浏览器的页面互不干扰;
go build出单个 exe,不依赖 C 编译器。
2. 分层
┌──────────────────────────────────────────────┐
│ handler/web 页面:解析表单 → 调 service │
│ → 渲染模板 │
│ handler/api 给 Client 的 JSON 接口 │
└───────────────────────┬──────────────────────┘
│ 只传普通结构体
┌───────────────────────▼──────────────────────┐
│ service 业务逻辑 │
│ 导入解析 / 建任务 / 匹配复用 │
└───────────────────────┬──────────────────────┘
│
┌───────────────────────▼──────────────────────┐
│ repository SQLite 读写 │
│ 只有这一层能写 SQL │
└───────────────────────┬──────────────────────┘
│
┌───────────────────────▼──────────────────────┐
│ model 纯数据结构,不导入 Gin/驱动 │
└──────────────────────────────────────────────┘
规则:
[必须]handler不写业务逻辑、不拼 SQL。它只做三件事:取参数、调 service、渲染。[必须]service不认识*gin.Context。这样才能不起服务器就单测。[必须]只有repository能写 SQL,且一律参数化查询。[必须]handler/web和handler/api分开: 页面出错要渲染错误页,接口出错要返回 JSON;页面要 CSRF,接口要认证。 混在一起迟早写错。
3. 目录
admin/
├── main.go 启动、路由注册
├── go.mod
├── config/ 端口、路径、超时
├── handler/
│ ├── web/ 四个模块的页面
│ │ ├── shopee.go
│ │ ├── syb.go
│ │ ├── task.go
│ │ └── client.go
│ └── api/ 给 Client 的接口
│ └── client_api.go
├── service/
│ ├── shopee_import.go Excel 解析与 upsert
│ ├── collect.go 创建采集任务
│ ├── mapping.go SKU 映射复用
│ ├── purchase.go 创建采购任务与校验
│ └── clientreg.go 客户端注册与在线判断
├── repository/
│ ├── db.go 连接、迁移
│ ├── shopee.go
│ ├── syb.go
│ ├── task.go
│ └── client.go
├── model/ 纯结构体
├── templates/ 见 §4
├── static/
│ ├── css/
│ └── js/
├── testdata/ 测试用的小样本
└── data/ 运行时数据,见 §6(不进 Git)
[建议] 一个模块一个文件,文件名和模块对应。初级程序员找代码时不用猜。
4. 模板组织
templates/
├── layout.html 整体骨架:导航 + 内容占位 + 状态条
├── shopee/
│ ├── list.html 表格页
│ └── edit_modal.html 编辑弹窗(含 PDD 链接和采集按钮)
├── syb/
│ ├── list.html
│ └── match_modal.html 规格匹配弹窗
├── task/list.html
├── client/list.html
└── partials/
├── toolbar.html 顶部工具条
├── table.html 带勾选的表格
└── statusbar.html 底部状态条
[必须] 三段式布局做成 partials/ 里的公共片段,四个模块复用。
不要每个页面复制一份——改一次样式要改四个地方,必然改漏。
[必须] 模板输出走 html/template 的自动转义。
禁止用 template.HTML 包裹用户可控的内容(商品名、订单号都是外部来的)。
5. 前端约束
[必须] 不引入 React / Vue / npm / 打包器。
| 功能 | 怎么实现 |
|---|---|
| 搜索、删除、导入 | 普通表单 POST,服务端渲染,零 JS |
| 全选 / 反选 | 原生 JS,十几行 |
| 双击行开弹窗 | 原生 JS 或 htmx |
| 导入进度 | MVP 先做成"提交后显示结果统计",不做实时进度条 |
[必须] 破坏性操作(删除、导入)用 POST,不得用 GET。
浏览器和爬虫会预取 GET 链接,用 GET 删数据迟早出事故。
6. 文件位置
跟 Client 一样是便携模式:程序自己产生的东西全在 data/ 下。
data/
├── admin.db SQLite 数据库
├── logs/ 运行日志
└── uploads/ 上传的 Excel 原件
| 情况 | data/ 在哪 |
|---|---|
| 打包成 exe 后 | exe 旁边 |
直接 go run . |
admin/data/ |
[必须] 路径统一走一个函数,代码里不许出现第二处拼路径的地方:
package config
import (
"os"
"path/filepath"
)
// DataDir 返回可写数据目录,不存在就创建。
// 打包后 = exe 旁边的 data/;go run 时 = 当前目录下的 data/。
func DataDir() (string, error) {
exe, err := os.Executable()
if err != nil {
return "", err
}
dir := filepath.Join(filepath.Dir(exe), "data")
// go run 会把程序编译到临时目录,那种情况下用当前工作目录
if isTempBuild(exe) {
wd, err := os.Getwd()
if err != nil {
return "", err
}
dir = filepath.Join(wd, "data")
}
if err := os.MkdirAll(dir, 0o755); err != nil {
return "", err
}
return dir, nil
}
| 别这么做 | 为什么 |
|---|---|
硬编码 D:\...\data |
换台机器就废了 |
直接用 os.Getwd() |
双击 exe 和命令行启动,当前目录不一样 |
往 exe 所在目录写日志但不建 data/ |
打包后和程序文件混在一起,升级时会被删 |
[必须] 升级方式:换掉 exe,data/ 原样保留。所以数据库必须支持迁移,见 03 §2。
7. 请求流程示例
以"导入蝦皮 Excel"为例,看清各层职责:
浏览器 POST /shopee/import (multipart 文件)
↓
handler/web/shopee.go
- 校验文件大小和扩展名
- 存到 data/uploads/
- 调 service.ImportShopeeExcel(path)
↓
service/shopee_import.go
- 用 excelize 逐行读
- 按 商品規格ID 是否为 "-" 分成商品行/SKU 行
- 解析规格原文 → 颜色/尺码/建议(失败留空)
- 调 repository 做 upsert
- 返回 {商品数, SKU数, 失败行号列表}
↓
repository/shopee.go
- INSERT ... ON CONFLICT(...) DO UPDATE
- 只更新报表来的字段,人工填的 pdd_goods_url 不动
↓
handler 渲染结果页:导入 5195 商品 / 6092 SKU,失败 0 行
[必须] 注意最后一步:upsert 时不得覆盖人工维护的字段。
这是导入逻辑里最容易写错、后果最严重的一处,见 03 §3.3。
8. 与 Client 的边界
- Admin 是任务的创建者和分配者,Client 是执行者。
- Admin 不知道 Client 执行到哪一步(没有心跳,是有意的)。
- Client 提交结果时,Admin 无条件接受,哪怕任务已取消或已重派。
- 详见 04 Client 接口实现。
9. 相关文档
- 上手指南
- 术语表
- 产品需求基线
- 数据模型
- Client 接口实现
- 界面规范
- 质量与安全
- Client 侧接口契约(对接时以它为准)