Files
shop_helm/docs/api.md
T

18 KiB
Raw Blame History

本地模块合约

ShopHelm 首发没有 HTTP 后端。本文定义 Gio UI 可调用的 application service、平台 port、事件和错误合约。实现可以拆分文件,但不得另起一套语义不兼容的接口。

一、通用约定

  • 所有可能 I/O 的方法第一个参数是 context.Context。
  • application service 输入输出使用 domain/application DTO,不把 Gio widget 或 SQL row 暴露出去。
  • ID 使用 int64,0 表示尚未持久化,外部输入不接受负数。
  • 时间在 service 边界使用 time.Time,持久化时转 UTC RFC3339。
  • 金额输入先按货币精度解析成整数最小单位;比例和汇率使用十进制字符串或 big.Rat,不直接用二进制浮点作为业务事实。
  • 列表默认 limit=50,最大 200;必须有稳定的 ID 二级排序。
  • 写操作成功后返回持久化后的完整 view。
  • 错误使用稳定 code;底层 cause 只供日志,不直接显示给用户。

二、通用类型

目标形状:

type SortDirection string

const (
    SortAsc  SortDirection = "asc"
    SortDesc SortDirection = "desc"
)

type PageRequest struct {
    Offset    int
    Limit     int
    SortBy    string
    Direction SortDirection
}

type Page[T any] struct {
    Items  []T
    Total  int
    Offset int
    Limit  int
}

type ErrorCode string

type AppError struct {
    Code        ErrorCode
    Message     string
    FieldErrors map[string]string
    Retryable   bool
    Cause       error
}

Message 是安全的默认用户提示;UI 可以按 Code 替换为更具体中文,但不得展示 Cause。

三、店铺服务

3.1 输入与视图

type StoreInput struct {
    Platform     string
    CountryCode  string
    Name         string
    AccountLabel string
    Owner        string
    Tags         []string
    Status       string
    Note         string
    Profile      BrowserProfileInput
    Proxy        ProxyInput
}

type BrowserProfileInput struct {
    PathKind  string // managed | external
    ProfileDir string
    ChromePath string
    StartURL   string
}

type ProxyInput struct {
    Enabled bool
    Scheme  string // http | https | socks4 | socks5
    Host    string
    Port    int
}

type StoreFilter struct {
    Query       string
    Platforms   []string
    Countries   []string
    Owners      []string
    Tags        []string
    Statuses    []string
    IncludeArchived bool
    Page        PageRequest
}

type StoreView struct {
    Store          domain.Store
    Profile        domain.BrowserProfile
    Proxy          domain.ProxyConfig
    Tags           []string
    Runtime        BrowserRuntimeView
    ShortcutCount  int
    PendingTodoCount int
}

StoreInput 没有密码、Cookie、token 或代理认证字段。MVP 期间不得添加这些字段的“临时字符串”替代品。

3.2 服务接口

type StoreService interface {
    List(ctx context.Context, filter StoreFilter) (Page[StoreView], error)
    Get(ctx context.Context, id int64) (StoreView, error)
    Create(ctx context.Context, input StoreInput) (StoreView, error)
    Update(ctx context.Context, id int64, input StoreInput) (StoreView, error)
    Archive(ctx context.Context, id int64) error
    Restore(ctx context.Context, id int64) (StoreView, error)
}

规则:

  • Create 对 managed profile 生成 <profile-root>\<store-id>,因此可以先事务创建店铺再补 profile;任一步失败整体回滚。
  • external profile 必须已存在且是目录;managed profile 可由服务创建。
  • Archive 只写 archived_at,不关闭 Chrome,不删目录。
  • (platform, country, name) 在未归档记录中重复时返回 conflict。

四、快捷入口和待办

type StoreShortcutService interface {
    List(ctx context.Context, storeID int64) ([]domain.StoreShortcut, error)
    Create(ctx context.Context, storeID int64, input ShortcutInput) (domain.StoreShortcut, error)
    Update(ctx context.Context, id int64, input ShortcutInput) (domain.StoreShortcut, error)
    Delete(ctx context.Context, id int64) error
}

type TodoService interface {
    List(ctx context.Context, filter TodoFilter) (Page[domain.Todo], error)
    Create(ctx context.Context, input TodoInput) (domain.Todo, error)
    Update(ctx context.Context, id int64, input TodoInput) (domain.Todo, error)
    SetStatus(ctx context.Context, id int64, status string) (domain.Todo, error)
    Delete(ctx context.Context, id int64) error
}

快捷链接只接受绝对 http 或 https URL。javascript:、file: 和命令协议必须拒绝。

五、Chrome 与 profile 合约

5.1 Application service

type OpenStoreRequest struct {
    StoreID   int64
    ShortcutID *int64
}

type LaunchResult struct {
    StoreID   int64
    PID       int
    TargetURL string
    StartedAt time.Time
}

type BrowserRuntimeView struct {
    StoreID   int64
    Status    string
    PID       int
    ExitCode  *int
    LastError *AppError
    ChangedAt time.Time
}

type BrowserLaunchService interface {
    Open(ctx context.Context, request OpenStoreRequest) (LaunchResult, error)
    Runtime(ctx context.Context, storeID int64) (BrowserRuntimeView, error)
    ListRuntime(ctx context.Context) ([]BrowserRuntimeView, error)
}

Open 的副作用:

  • 可能创建 managed profile 目录。
  • 启动外部 Chrome。
  • 成功后更新 last_opened_at 和最后 PID。
  • 发布 browser.status.changed。

Open 不等待 Chrome 退出。应用退出时默认不结束已打开的 Chrome。

5.2 Platform ports

type LaunchSpec struct {
    Executable string
    ProfileDir string
    TargetURL  string
    ProxyURL   string
}

type ProcessHandle interface {
    PID() int
    Wait(ctx context.Context) (exitCode int, err error)
}

type ChromeLauncher interface {
    Start(ctx context.Context, spec LaunchSpec) (ProcessHandle, error)
}

type ProfileInspector interface {
    Inspect(ctx context.Context, profileDir string) (ProfileUse, error)
}

type ProfileUse struct {
    Occupied bool
    PID      int
    Source   string // shophelm_registry | chrome_lock | unknown
}

ChromeLauncher 的单元测试必须能注入 fake executable/command runner。只有 platform adapter 可构造 Chrome 参数。

六、商品服务

type ProductInput struct {
    StoreID       int64
    Name          string
    SKU           string
    Cost          string
    SalePrice     string
    Currency      string
    Stock         *int
    ProductURL    string
    Status        string
    Note          string
}

type ProductFilter struct {
    Query           string
    StoreIDs        []int64
    Statuses        []string
    IncludeArchived bool
    Page            PageRequest
}

type ProductService interface {
    List(ctx context.Context, filter ProductFilter) (Page[domain.Product], error)
    Get(ctx context.Context, id int64) (domain.Product, error)
    Create(ctx context.Context, input ProductInput) (domain.Product, error)
    Update(ctx context.Context, id int64, input ProductInput) (domain.Product, error)
    Archive(ctx context.Context, id int64) error
    Restore(ctx context.Context, id int64) (domain.Product, error)
}

type ContentTemplateService interface {
    List(ctx context.Context, filter TemplateFilter) (Page[domain.ContentTemplate], error)
    Create(ctx context.Context, input ContentTemplateInput) (domain.ContentTemplate, error)
    Update(ctx context.Context, id int64, input ContentTemplateInput) (domain.ContentTemplate, error)
    Archive(ctx context.Context, id int64) error
}

type ProductChecklistService interface {
    List(ctx context.Context, productID int64) ([]domain.ProductCheckItem, error)
    SeedDefaults(ctx context.Context, productID int64) ([]domain.ProductCheckItem, error)
    SetChecked(ctx context.Context, itemID int64, checked bool) (domain.ProductCheckItem, error)
}

商品 URL 可以为空;非空时只接受绝对 HTTP(S) URL。SKU 可为空,但同一店铺非空 SKU 重复时 UI 必须警告;是否阻止保存由 T-301 根据用户确认定稿并同步 schema。

七、价格计算

type PriceCalculationInput struct {
    SalePrice        string
    ProductCost      string
    ShippingCost     string
    OtherCost        string
    PlatformFeeRate  string
    ExchangeRate     string
    Currency         string
    CostCurrency     string
}

type PriceCalculationResult struct {
    Inputs            PriceCalculationInput
    PlatformFee       string
    TotalCost         string
    ReferenceMargin   string
    ReferenceMarginRate string
    Warnings          []string
}

type PriceCalculator interface {
    Calculate(input PriceCalculationInput) (PriceCalculationResult, error)
}

规则:

  • 空费率、汇率或费用不能偷偷套默认值;返回校验错误或明确的零值警告。
  • 除数为零时返回 validation_failed。
  • 输出必须回显输入,界面称其为“基础利润参考”,不称财务报表。

八、商品文件导入导出

type ImportFormat string // csv | xlsx
type ImportStrategy string // all_or_nothing | valid_rows_only

type ProductImportPreview struct {
    SourcePath   string
    Fingerprint  string
    Format       ImportFormat
    Sheet        string
    ValidRows    []ProductImportRow
    Errors       []ImportRowError
    TotalRows    int
}

type ProductImportCommit struct {
    SourcePath  string
    Fingerprint string
    Strategy    ImportStrategy
    DefaultStoreID int64
}

type ImportResult struct {
    Created int
    Updated int
    Skipped int
}

type ProductImportExportService interface {
    PreviewImport(ctx context.Context, path string, format ImportFormat) (ProductImportPreview, error)
    CommitImport(ctx context.Context, request ProductImportCommit) (ImportResult, error)
    Export(ctx context.Context, filter ProductFilter, destination string, format ImportFormat) (FileResult, error)
}

提交时重新计算文件 SHA-256;与 preview fingerprint 不同则返回 source_changed,要求重新预览。

九、图片素材与合成

9.1 素材服务

type ImageAssetInput struct {
    StoreID    *int64
    ProductID  *int64
    AssetType  string
    SourcePath string
    CopyToLibrary bool
    Note       string
}

type ImageAssetService interface {
    List(ctx context.Context, filter ImageAssetFilter) (Page[domain.ImageAsset], error)
    Add(ctx context.Context, input ImageAssetInput) (domain.ImageAsset, error)
    Update(ctx context.Context, id int64, input ImageAssetInput) (domain.ImageAsset, error)
    Remove(ctx context.Context, id int64) error
    RefreshMetadata(ctx context.Context, id int64) (domain.ImageAsset, error)
}

Remove 删除数据库关联;仅当文件是 app-managed 且用户明确勾选删除文件时,才能进入独立确认流程。默认不删文件。

9.2 合成规格

type CompositionSpec struct {
    BaseAssetID    int64
    OverlayAssetID int64
    CanvasWidth    int
    CanvasHeight   int
    BaseFit        string  // contain | cover
    CenterX        float64 // normalized canvas coordinate
    CenterY        float64
    WidthRatio     float64
    Opacity        float64
    BackgroundRGBA uint32
}

type CompositionPlan struct {
    Canvas      image.Rectangle
    BaseSource  image.Rectangle
    BaseTarget  image.Rectangle
    OverlaySource image.Rectangle
    OverlayTarget image.Rectangle
    Opacity     uint8
}

type ExportImageRequest struct {
    StoreID      *int64
    ProductID    *int64
    Spec          CompositionSpec
    Format        string // png | jpg
    Destination   string
    Overwrite     bool
}

type ImageComposerService interface {
    Plan(ctx context.Context, spec CompositionSpec) (CompositionPlan, error)
    RenderPreview(ctx context.Context, spec CompositionSpec, maxWidth, maxHeight int) (image.Image, error)
    Export(ctx context.Context, request ExportImageRequest) (domain.ImageExport, error)
}

约束:

  • CanvasWidth、CanvasHeight 必须为正,并受实现中明确的像素/内存上限约束。
  • WidthRatio 大于 0;Opacity 在 0..1。
  • Plan 是预览和导出的唯一几何算法。
  • Overwrite=false 且目标存在时返回 destination_exists。
  • Overwrite=true 仍不得覆盖任一输入素材文件。
  • 导出成功前不写 image_exports;数据库记录失败时删除本次新建输出或报告可恢复的部分失败。

十、客服服务

type ReplyTemplateInput struct {
    Category string
    Language string
    Title    string
    Content  string
}

type ReplyTemplateService interface {
    List(ctx context.Context, filter ReplyTemplateFilter) (Page[domain.ReplyTemplate], error)
    Create(ctx context.Context, input ReplyTemplateInput) (domain.ReplyTemplate, error)
    Update(ctx context.Context, id int64, input ReplyTemplateInput) (domain.ReplyTemplate, error)
    Archive(ctx context.Context, id int64) error
    Copy(ctx context.Context, id int64) (CopyResult, error)
}

type CustomerFollowupInput struct {
    StoreID       int64
    BuyerLabel    string
    OrderRef      string
    IssueType     string
    Status        string
    NextFollowupAt *time.Time
    Note          string
}

type CustomerFollowupService interface {
    List(ctx context.Context, filter FollowupFilter) (Page[domain.CustomerFollowup], error)
    Create(ctx context.Context, input CustomerFollowupInput) (domain.CustomerFollowup, error)
    Update(ctx context.Context, id int64, input CustomerFollowupInput) (domain.CustomerFollowup, error)
    Archive(ctx context.Context, id int64) error
}

Copy 成功写入系统剪贴板后才增加 usage_count。剪贴板失败时不增加计数。

十一、工作台

type DashboardQuery struct {
    Now             time.Time
    RecentStoreLimit int
    RecentExportLimit int
}

type DashboardView struct {
    RecentStores   []StoreView
    DueTodos       []domain.Todo
    DueFollowups   []domain.CustomerFollowup
    PendingProducts []domain.Product
    RecentExports  []domain.ImageExport
}

type DashboardService interface {
    Load(ctx context.Context, query DashboardQuery) (DashboardView, error)
}

Dashboard 允许各分区分别失败并显示局部错误,但不得把失败分区伪装为空数据。

十二、设置、备份与恢复

type SettingsService interface {
    Get(ctx context.Context) (domain.Settings, error)
    Update(ctx context.Context, input SettingsInput) (domain.Settings, error)
}

type BackupService interface {
    Create(ctx context.Context, destination string) (BackupResult, error)
    Inspect(ctx context.Context, source string) (BackupManifest, error)
    Restore(ctx context.Context, source string, confirmation RestoreConfirmation) (RestoreResult, error)
}

RestoreConfirmation 必须包含 UI 展示过的备份 fingerprint 和用户确认标志。恢复期间 application 进入 maintenance 状态,其他写操作返回 maintenance_mode。

十三、应用事件

事件 触发时机 负载 UI 结果
operation.started 异步 intent 接受 request ID、operation、entity 显示 busy,禁用重复提交
operation.completed 操作成功 request ID、result summary 更新页面并反馈成功
operation.failed 操作失败 request ID、AppError 保留输入,显示可行动错误
browser.status.changed Chrome 启动、运行或退出 BrowserRuntimeView 更新店铺行状态
data.restored 恢复并重新打开数据库成功 backup ID、schema version 清空页面缓存并重载
preview.ready 最新图片预览完成 request ID、preview image 仅接收当前 request ID 的结果

旧请求的预览事件到达时,UI 根据 request ID 丢弃,避免快速拖动后显示过期画面。

十四、错误码

Code 含义 是否可重试
validation_failed 字段或组合不合法 修正输入后
not_found 资源不存在或已归档 否
conflict 唯一约束或状态冲突 修改输入后
profile_in_use Chrome profile 已占用 关闭对应进程后
chrome_not_found Chrome 路径无效 配置后
chrome_start_failed 进程启动失败 视 cause
proxy_invalid 代理格式无效 修正配置后
source_missing 素材或导入文件不存在 重新选择
source_changed 导入 preview 后文件已变化 重新预览
unsupported_format 文件或图片格式不支持 选择支持格式
destination_exists 输出已存在且未允许覆盖 改名或确认覆盖
file_access_denied 文件权限不足 修改目录权限后
database_busy SQLite 超时仍被占用 是
database_corrupt 完整性检查失败 使用有效备份
backup_incompatible schema 版本不支持 升级应用或选其他备份
maintenance_mode 恢复期间拒绝写入 恢复完成后
cancelled 用户取消或 context 结束 可重新发起
internal 未分类内部错误 视日志

新增错误码必须同步 UI 映射和测试,不得把底层错误字符串当稳定合约。

十五、副作用边界

操作 允许的副作用 禁止的副作用
保存店铺 SQLite 写入;创建 managed profile 目录 登录平台、删除已有 profile
打开店铺 启动 Chrome;记录状态 注入凭证、开启 CDP、操作网页
导入商品 读取用户文件;确认后事务写 SQLite 修改源文件、静默跳过错误
添加素材 读取图片;可选复制到素材库 修改原图
导出图片 创建新输出;写导出记录 用 UI 截图导出、默认覆盖
复制话术 写系统剪贴板;增加使用次数 自动发送到平台
备份 创建一致性数据库副本 复制全部 Chrome profile
恢复 验证后替换数据库并保留回滚副本 未确认覆盖、删除外部素材