# 架构设计 > 本文定义 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= --no-first-run --disable-default-apps [--proxy-server=] ``` 代理认证不通过命令行注入。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\ │ `- \ ├── 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 成功替代真实结论。