Files
shop_helm/docs/04-architecture.md

623 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 架构设计
> 本文定义 ShopHelm 的系统结构、模块职责、数据模型和关键数据流。依赖选择见 [`03-tech-stack.md`](03-tech-stack.md),公开调用形状见 [`api.md`](api.md)。
## 一、架构约束
- Windows 本地单用户桌面应用,无远程后端。
- Gio 负责控制台 UI;店铺后台在外部 Chrome 中运行。
- SQLite 是结构化业务数据的唯一持久化事实来源。
- 图片原文件按路径引用;应用管理缩略图、合成参数和导出记录。
- 平台密码不保存,运行时也不要求输入。
- 首发无 CDP、无平台 API、无自动页面操作。
- 所有可能阻塞的数据库、文件、图片和进程操作都在 Gio frame 之外执行。
## 二、系统结构
```text
用户
|
v
Gio Desktop UI
|- 今日工作台
|- 店铺管理
|- 商品与图片
|- 客服助手
`- 设置
|
| UI Intent / Application Event
v
Application Services
|- StoreService / DashboardService
|- BrowserProfileService / BrowserLaunchService
|- ProductService / ImportExportService
|- ImageAssetService / ImageComposerService
|- ReplyTemplateService / FollowupService
`- BackupService / SettingsService
|
+---------------------------+
| |
v v
Domain + Repository Ports Platform Ports
| |- Chrome process adapter
v |- Filesystem / clipboard
SQLite Repositories `- Clock / path resolver
|
v
%LOCALAPPDATA%\ShopHelm + 用户选择的素材/导出目录
```
依赖方向固定为:
```text
internal/ui
-> internal/application
-> internal/domain
-> repository/platform interfaces
internal/infrastructure/sqlite
-> internal/domain
-> repository interfaces
internal/platform
-> platform interfaces
```
`domain` 不得导入 Gio、SQLite driver 或 Windows API。`application` 不得依赖具体 repository 实现。
## 三、模块职责
### 3.1 `internal/domain`
包含:
- 实体和值对象:Store、BrowserProfile、ProxyConfig、Product、ImageAsset、ImageComposition、ReplyTemplate、CustomerFollowup、Todo。
- 枚举和状态转换。
- 无 I/O 的校验、价格计算和图片几何计算。
- 领域错误,不包含 UI 文案。
不包含:
- SQL。
- Gio widget。
- `exec.Command`。
- 文件读写或剪贴板调用。
### 3.2 `internal/application`
负责用例编排:
- 校验输入并调用 repository。
- 用事务协调多表写入。
- 将平台错误映射为稳定的应用错误码。
- 发布 UI 可消费的完成事件。
- 维护异步任务的 context、取消和并发边界。
application service 不直接绘制 UI,不拼 SQL,不依赖全局单例。
### 3.3 `internal/infrastructure/sqlite`
负责:
- 打开数据库并设置 PRAGMA。
- 应用嵌入 migration。
- 实现 repository interface。
- 事务、查询、扫描和约束错误映射。
- 备份验证所需的只读打开和完整性检查。
每次连接至少设置:
```sql
PRAGMA foreign_keys = ON;
PRAGMA busy_timeout = 5000;
```
WAL 是否启用由 T-003 用 Windows 实测决定;确定后写入 migration/连接配置和本文,不在不同启动路径中采用不同值。
### 3.4 `internal/platform/chrome`
负责:
- 查找或使用配置的 Chrome 可执行文件。
- 规范化 profile 路径。
- 检查 ShopHelm 已知进程和 Chrome profile 占用。
- 以参数数组启动 Chrome,不经过 shell。
- 监听进程退出并发布状态。
- 对普通无认证代理生成启动参数。
该模块不判断店铺业务状态,也不写 SQLite。
### 3.5 `internal/platform/files`
负责:
- 应用数据目录和用户目录解析。
- 原子文件写入。
- 图片文件探测、校验和与编码。
- CSV/XLSX 文件打开和输出。
- 备份、恢复临时文件管理。
### 3.6 `internal/ui`
负责:
- Gio 窗口、导航和主题。
- 页面状态、输入校验显示、加载/空/错误状态。
- 将用户动作转成 application intent。
- 消费 application event 并使窗口失效重绘。
- 表格、表单、弹窗、toast、状态标识和图片画布等项目内组件。
frame/layout 函数只能布局、处理已到达的 UI 事件和提交 intent;不能等待数据库、文件、网络或进程。
## 四、关键数据流
### 4.1 店铺保存
```text
用户提交表单
-> UI 做必填和格式预检查
-> StoreService.Validate/Create/Update
-> domain 校验枚举、URL、路径和代理
-> repository transaction 写 stores/profile/proxy/tags
-> 返回 StoreView
-> UI 更新列表并显示结果
```
UI 校验用于快速反馈,domain/application 校验才是写入前的权威校验。
### 4.2 打开店铺 Chrome
```text
用户点击店铺或快捷入口
-> BrowserLaunchService 读取 StoreLaunchConfig
-> 校验 Chrome、URL、profile、proxy
-> ProfileInspector 检查占用
-> 状态改为 starting(内存)
-> ChromeLauncher.Start(args)
-> 记录 PID、last_opened_at 和 launch log
-> 状态改为 running
-> Wait goroutine 监听退出
-> 状态改为 exited/failed
```
规则:
- `running` 是运行时事实,不以数据库布尔值作为权威来源。
- 可以持久化最后 PID、退出码和时间用于诊断,但应用启动时必须重新核对真实进程。
- 同一 profile 不允许由 ShopHelm 重复启动。
- 归档店铺不杀进程、不删除 profile。
- 远程调试端口和 CDP 默认完全关闭。
标准参数形状:
```text
chrome.exe
--user-data-dir=<absolute-profile-dir>
--no-first-run
--disable-default-apps
[--proxy-server=<scheme://host:port>]
<target-url>
```
代理认证不通过命令行注入。URL、路径和 host/port 分别校验后再作为独立参数传递。
### 4.3 图片预览与导出
```text
底图 + 叠加图 + CompositionSpec
-> ImageProbe 读取尺寸和格式
-> CompositionPlanner 生成目标像素矩形
|- UI 预览按 viewport 比例绘制
`- Exporter 按输出分辨率 CPU 合成
-> 编码临时 PNG/JPG
-> fsync / close / 原子改名
-> 写 image_exports
```
`CompositionSpec` 使用与画布无关的归一化参数:
- `center_x`、`center_y`:叠加层中心相对画布宽高的位置,建议范围 `0..1`,允许用户拖出边缘但必须有上限。
- `width_ratio`:叠加层宽度 / 画布宽度,保持原始宽高比。
- `opacity`:`0..1`。
- `canvas_width`、`canvas_height`:正式输出像素尺寸。
- `base_fit`:首发固定支持 `contain` 和 `cover`。
- `background`:PNG 可透明;JPG 必须是明确 RGBA 颜色,默认白色。
预览和导出必须调用同一个 `CompositionPlanner`,不得分别实现位置公式。Gio 截图不得作为导出文件。
### 4.4 CSV/XLSX 导入
```text
用户选择文件
-> parser 读取为 ImportRow
-> 全量字段校验并生成 Preview
-> UI 展示有效数、错误行和原因
-> 用户确认
-> 单事务写入有效数据
-> 返回创建/更新/跳过统计
```
首发不做“遇错继续并静默丢行”。导入策略必须由用户选择:全部有效才导入,或只导入有效行并明确列出跳过项。
### 4.5 备份与恢复
备份:
1. 获取应用级写锁。
2. 生成一致性 SQLite 副本到临时路径。
3. 对副本运行 `PRAGMA integrity_check`。
4. 写入备份元信息后原子改名。
5. 释放写锁。
恢复:
1. 用户选择备份并明确确认。
2. 在测试连接中检查文件头、完整性和 schema 版本。
3. 暂停写入并关闭活动数据库连接。
4. 先把当前数据库移动为可回滚副本。
5. 复制备份到临时路径并原子替换。
6. 重新打开、迁移和 smoke;失败则恢复旧数据库。
备份默认只包含数据库和应用管理的配置,不复制外部 Chrome profile 或外部原图。备份清单必须说明引用文件可能仍在原路径。
## 五、运行时并发模型
- Gio window event loop 由 UI 层独占。
- application 使用有界 worker 或按操作启动的 goroutine,不创建无限队列。
- 每个异步 intent 携带 `context.Context` 和稳定 request ID。
- 相同资源的保存操作串行化;按钮在请求进行中显示 busy 并防重复提交。
- repository 可供多个 goroutine 使用,但事务对象不跨 goroutine。
- Chrome `Wait` 每个已启动进程一个 goroutine,退出后立即释放 registry。
- 大图解码、缩放和导出可取消;新预览请求替换旧请求时取消旧任务。
- UI 销毁时取消根 context,等待必要的数据库和日志 flush,不强制杀死用户 Chrome。
## 六、持久化位置
```text
%LOCALAPPDATA%\ShopHelm\
├── data\
│ `- shophelm.db
├── profiles\
│ `- <store-id>\
├── assets\
│ |- imported\
│ `- thumbnails\
├── backups\
└── logs\
```
- 应用管理的 profile 只能建立在配置的 profile 根目录下。
- 用户可绑定已有外部 profile,但系统将其标为 `external`,绝不移动或删除。
- 原图默认只记录规范化绝对路径;用户选择“复制到素材库”时才复制到 `assets/imported`。
- 导出默认写到 `%USERPROFILE%\Pictures\ShopHelm\Exports`。
## 七、数据模型
### 7.1 通用规则
- 主键使用 SQLite `INTEGER PRIMARY KEY`。
- 时间以 UTC RFC3339 字符串存储,UI 按本地时区显示。
- `created_at`、`updated_at` 由 application 通过注入的 Clock 生成。
- 外键始终开启。
- 店铺和核心资料使用归档时间 `archived_at`,默认查询排除已归档记录。
- 用户可见顺序需要稳定的二级排序:主要排序字段相同后按 `id`。
- 不存在 `password`、`cookie`、`token` 或代理秘密列。
### 7.2 Schema 版本
```sql
CREATE TABLE schema_migrations (
version INTEGER PRIMARY KEY,
applied_at TEXT NOT NULL
);
```
migration 文件命名为 `000001_initial.sql`、`000002_*.sql`,只追加不修改。
### 7.3 店铺域
```sql
CREATE TABLE stores (
id INTEGER PRIMARY KEY,
platform TEXT NOT NULL,
country_code TEXT NOT NULL DEFAULT '',
name TEXT NOT NULL,
account_label TEXT NOT NULL DEFAULT '',
owner TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL,
note TEXT NOT NULL DEFAULT '',
last_opened_at TEXT,
archived_at TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE UNIQUE INDEX ux_stores_identity_active
ON stores(platform, country_code, name)
WHERE archived_at IS NULL;
CREATE TABLE browser_profiles (
id INTEGER PRIMARY KEY,
store_id INTEGER NOT NULL UNIQUE REFERENCES stores(id),
profile_dir TEXT NOT NULL UNIQUE,
path_kind TEXT NOT NULL,
chrome_path TEXT NOT NULL DEFAULT '',
start_url TEXT NOT NULL,
last_pid INTEGER,
last_exit_code INTEGER,
last_exited_at TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE proxy_configs (
id INTEGER PRIMARY KEY,
store_id INTEGER NOT NULL UNIQUE REFERENCES stores(id),
enabled INTEGER NOT NULL DEFAULT 0,
scheme TEXT NOT NULL DEFAULT 'http',
host TEXT NOT NULL DEFAULT '',
port INTEGER,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE store_shortcuts (
id INTEGER PRIMARY KEY,
store_id INTEGER NOT NULL REFERENCES stores(id),
kind TEXT NOT NULL,
label TEXT NOT NULL,
url TEXT NOT NULL,
sort_order INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
UNIQUE(store_id, kind, label)
);
CREATE TABLE tags (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL UNIQUE
);
CREATE TABLE store_tags (
store_id INTEGER NOT NULL REFERENCES stores(id),
tag_id INTEGER NOT NULL REFERENCES tags(id),
PRIMARY KEY(store_id, tag_id)
);
```
`path_kind` 只允许 `managed` 或 `external`。`proxy_configs` 不增加用户名和密码列。
### 7.4 待办和商品域
```sql
CREATE TABLE todos (
id INTEGER PRIMARY KEY,
store_id INTEGER REFERENCES stores(id),
title TEXT NOT NULL,
status TEXT NOT NULL,
due_at TEXT,
note TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE products (
id INTEGER PRIMARY KEY,
store_id INTEGER NOT NULL REFERENCES stores(id),
name TEXT NOT NULL,
sku TEXT NOT NULL DEFAULT '',
cost_minor INTEGER,
sale_price_minor INTEGER,
currency TEXT NOT NULL DEFAULT '',
stock INTEGER,
product_url TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL,
note TEXT NOT NULL DEFAULT '',
archived_at TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE INDEX ix_products_store_status
ON products(store_id, status, updated_at);
CREATE TABLE content_templates (
id INTEGER PRIMARY KEY,
store_id INTEGER REFERENCES stores(id),
kind TEXT NOT NULL,
language TEXT NOT NULL DEFAULT '',
title TEXT NOT NULL,
content TEXT NOT NULL,
archived_at TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE product_check_items (
id INTEGER PRIMARY KEY,
product_id INTEGER NOT NULL REFERENCES products(id),
item_key TEXT NOT NULL,
label TEXT NOT NULL,
checked INTEGER NOT NULL DEFAULT 0,
sort_order INTEGER NOT NULL DEFAULT 0,
updated_at TEXT NOT NULL,
UNIQUE(product_id, item_key)
);
```
金额以最小货币单位整数保存,避免 `REAL` 舍入误差。汇率和平台费率必须是当次计算的显式输入;首发不把计算结果持久化为财务事实。
### 7.5 图片域
```sql
CREATE TABLE image_assets (
id INTEGER PRIMARY KEY,
store_id INTEGER REFERENCES stores(id),
product_id INTEGER REFERENCES products(id),
asset_type TEXT NOT NULL,
file_path TEXT NOT NULL,
storage_kind TEXT NOT NULL,
width INTEGER,
height INTEGER,
checksum_sha256 TEXT,
note TEXT NOT NULL DEFAULT '',
missing INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE image_exports (
id INTEGER PRIMARY KEY,
store_id INTEGER REFERENCES stores(id),
product_id INTEGER REFERENCES products(id),
base_asset_id INTEGER NOT NULL REFERENCES image_assets(id),
overlay_asset_id INTEGER NOT NULL REFERENCES image_assets(id),
output_path TEXT NOT NULL,
output_format TEXT NOT NULL,
output_width INTEGER NOT NULL,
output_height INTEGER NOT NULL,
composition_json TEXT NOT NULL,
checksum_sha256 TEXT NOT NULL,
created_at TEXT NOT NULL
);
```
首发不需要保存 `image_templates`。模板和批量导出进入 Backlog 后新增 migration,不能提前把未使用的通用图层系统塞进 MVP。
### 7.6 客服域
```sql
CREATE TABLE reply_templates (
id INTEGER PRIMARY KEY,
category TEXT NOT NULL,
language TEXT NOT NULL,
title TEXT NOT NULL,
content TEXT NOT NULL,
usage_count INTEGER NOT NULL DEFAULT 0,
archived_at TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE customer_followups (
id INTEGER PRIMARY KEY,
store_id INTEGER NOT NULL REFERENCES stores(id),
buyer_label TEXT NOT NULL DEFAULT '',
order_ref TEXT NOT NULL DEFAULT '',
issue_type TEXT NOT NULL,
status TEXT NOT NULL,
next_followup_at TEXT,
note TEXT NOT NULL DEFAULT '',
archived_at TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE INDEX ix_followups_due
ON customer_followups(status, next_followup_at);
```
`buyer_label` 只保存完成跟进所需的最小标识,不要求保存买家完整个人资料。
### 7.7 设置与操作记录
```sql
CREATE TABLE settings (
key TEXT PRIMARY KEY,
value_json TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE operation_logs (
id INTEGER PRIMARY KEY,
action TEXT NOT NULL,
entity_type TEXT NOT NULL,
entity_id INTEGER,
result TEXT NOT NULL,
error_code TEXT,
detail_json TEXT NOT NULL DEFAULT '{}',
created_at TEXT NOT NULL
);
```
operation log 只记录业务动作摘要、路径的脱敏形式和稳定错误码,不记录话术正文、买家详细信息、账号、Cookie 或命令行秘密。
## 八、状态枚举
| 领域 | 合法值 |
| --- | --- |
| 店铺业务状态 | `active`、`login_required`、`attention`、`paused` |
| Chrome 运行状态 | `stopped`、`occupied`、`starting`、`running`、`exited`、`failed` |
| 待办 | `pending`、`done`、`cancelled` |
| 商品 | `draft`、`pending_listing`、`listed`、`optimize`、`paused` |
| 图片素材 | `main`、`detail`、`logo`、`badge`、`watermark` |
| 内容模板 | `title`、`description` |
| 客服跟进 | `pending`、`in_progress`、`waiting_buyer`、`resolved`、`escalated` |
数据库约束或 domain 校验必须拒绝其他值。UI 文案与内部枚举通过集中映射,不把中文展示值写入状态列。
## 九、项目结构
```text
shop_helm/
├── cmd/
│ `-- shophelm/
│ `-- main.go
├── internal/
│ ├── application/
│ ├── domain/
│ ├── infrastructure/
│ │ `-- sqlite/
│ ├── platform/
│ │ ├── chrome/
│ │ ├── clipboard/
│ │ `-- files/
│ `-- ui/
│ ├── components/
│ ├── pages/
│ `-- theme/
├── migrations/
├── assets/
├── testdata/
├── docs/
├── init.ps1
├── init.sh
├── go.mod
└── go.sum
```
Go 测试优先与被测包同目录。跨模块验收夹具放 `testdata/`,不得包含真实账号或隐私数据。
## 十、错误处理
- domain 返回可判定的 sentinel/typed errors。
- application 映射为 [`api.md`](api.md) 定义的稳定错误码。
- infrastructure 附加底层原因供日志诊断,但 UI 不直接展示 SQL、绝对秘密路径或系统堆栈。
- UI 显示“发生了什么 + 用户能做什么”,例如“该店铺浏览器环境正在使用,请先关闭对应 Chrome 后重试”。
- 可重试操作明确提供重试;不可恢复操作不做无限重试。
## 十一、测试策略
| 层 | 测试 |
| --- | --- |
| domain | 状态校验、价格计算、图片几何的表驱动单元测试 |
| application | fake repository/platform adapter,验证编排、事务和错误映射 |
| SQLite | 临时目录真实数据库、migration、约束、CRUD、备份恢复 |
| Chrome adapter | 参数生成单元测试 + fake executable;真实 Chrome 手工 smoke |
| Image composer | 固定小图 golden/pixel 测试、PNG alpha、JPG 背景、原图不变 |
| CSV/XLSX | round-trip、错误行、编码和事务行为 |
| Gio UI | 页面状态和 intent 单测 + Windows 手工 smoke;不依赖像素脆弱截图作为唯一证据 |
## 十二、关键风险和闸门
| 风险 | 必须先通过的闸门 |
| --- | --- |
| Go 1.23.0 与目标 Gio 不兼容 | T-001 最小窗口在目标 Windows 上运行并锁版本 |
| Gio 管理台组件成本过高 | T-101 用真实店铺字段完成表格、筛选和表单原型 |
| profile 占用判断不可靠 | T-102 覆盖本应用启动、外部占用、进程退出和脏状态 |
| 预览和导出不一致 | T-103 使用同一 CompositionPlanner 通过像素测试 |
| SQLite 恢复损坏现有数据 | T-208 在测试目录执行成功和故障注入恢复 |
| RPA 导致平台风险 | 首发无 RPA;任何引入必须新建需求与合规任务 |
高风险闸门未通过时,不继续依赖它的业务任务,也不以 mock 成功替代真实结论。