19 KiB
架构设计
本文定义 ShopHelm 的系统结构、模块职责、数据模型和关键数据流。依赖选择见
03-tech-stack.md,公开调用形状见api.md。
一、架构约束
- Windows 本地单用户桌面应用,无远程后端。
- Gio 负责控制台 UI;店铺后台在外部 Chrome 中运行。
- SQLite 是结构化业务数据的唯一持久化事实来源。
- 图片原文件按路径引用;应用管理缩略图、合成参数和导出记录。
- 平台密码不保存,运行时也不要求输入。
- 首发无 CDP、无平台 API、无自动页面操作。
- 所有可能阻塞的数据库、文件、图片和进程操作都在 Gio frame 之外执行。
二、系统结构
用户
|
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 + 用户选择的素材/导出目录
依赖方向固定为:
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。
- 事务、查询、扫描和约束错误映射。
- 备份验证所需的只读打开和完整性检查。
每次连接至少设置:
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 店铺保存
用户提交表单
-> UI 做必填和格式预检查
-> StoreService.Validate/Create/Update
-> domain 校验枚举、URL、路径和代理
-> repository transaction 写 stores/profile/proxy/tags
-> 返回 StoreView
-> UI 更新列表并显示结果
UI 校验用于快速反馈,domain/application 校验才是写入前的权威校验。
4.2 打开店铺 Chrome
用户点击店铺或快捷入口
-> 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 默认完全关闭。
标准参数形状:
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 图片预览与导出
底图 + 叠加图 + 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 导入
用户选择文件
-> parser 读取为 ImportRow
-> 全量字段校验并生成 Preview
-> UI 展示有效数、错误行和原因
-> 用户确认
-> 单事务写入有效数据
-> 返回创建/更新/跳过统计
首发不做“遇错继续并静默丢行”。导入策略必须由用户选择:全部有效才导入,或只导入有效行并明确列出跳过项。
4.5 备份与恢复
备份:
- 获取应用级写锁。
- 生成一致性 SQLite 副本到临时路径。
- 对副本运行
PRAGMA integrity_check。 - 写入备份元信息后原子改名。
- 释放写锁。
恢复:
- 用户选择备份并明确确认。
- 在测试连接中检查文件头、完整性和 schema 版本。
- 暂停写入并关闭活动数据库连接。
- 先把当前数据库移动为可回滚副本。
- 复制备份到临时路径并原子替换。
- 重新打开、迁移和 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。
六、持久化位置
%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 版本
CREATE TABLE schema_migrations (
version INTEGER PRIMARY KEY,
applied_at TEXT NOT NULL
);
migration 文件命名为 000001_initial.sql、000002_*.sql,只追加不修改。
7.3 店铺域
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 待办和商品域
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 图片域
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 客服域
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 设置与操作记录
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 文案与内部枚举通过集中映射,不把中文展示值写入状态列。
九、项目结构
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定义的稳定错误码。 - 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 成功替代真实结论。