Files
shop_helm/docs/04-architecture.md
T

19 KiB
Raw Blame History

架构设计

本文定义 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 备份与恢复

备份:

  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。

六、持久化位置

%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 成功替代真实结论。