generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件) wiki_page: Architecture-and-Code-Map wiki_url: https://git.ilapage.cn/OPC/goauto/wiki/Architecture-and-Code-Map.- wiki_revision: 9bb0da18b68d368b37c060af9867983ace6b49f6 synchronized_at: 2026-08-24T03:42:12Z # 架构与代码地图 ## MVP 架构 ```text go-admin-ui │ REST:PDD商品、规则、任务、任务详情 ▼ go-admin Server ───────── 数据库 │ ├─ pdd_product │ REST:注册/心跳/任务 ├─ collection_rule │ ├─ collection_task(任务+结果) ▼ └─ dimension/value/color_price/sku 子表 Android Portal/Agent ├─ 注册、心跳与设备串行锁 ├─ 领取指定任务或空闲领取未指定任务 ├─ 执行任务内的 URL 与规则快照 └─ 提交结构化结果或明确错误 ``` ## 最小闭环 1. 管理员添加 PDD URL,服务端规范化 URL 并提取唯一 `goods_id`。 2. 管理员创建规则;规则创建后立即可用于创建任务。 3. 管理员从一个 PDD 商品创建任务,可指定设备,也可留空等待空闲设备领取。 4. 任务固化 URL、goods_id 和完整规则快照。 5. Android 串行执行并把结果写回同一条任务;规格与 SKU 写入任务结果子表。 6. 同一事务把 `completed` 结果全量覆盖到 PDD 商品最新档案,把 `completed_partial` 明确采集到的字段和规格安全合并;`failed` 不修改商品。 7. 管理员在任务详情查看原始结构化结果,也可在 PDD 商品页面查看和人工覆盖最新商品资料。 ## 关键设计 - `pdd_product.goods_id` 唯一;重复时提示商品已存在,不重复新增。 - 规则没有草稿、发布和版本流程;软删除只阻止创建新任务。 - 已有任务不依赖规则当前状态,始终执行自身 `rule_snapshot`。 - `collection_task` 同时保存任务生命周期和结果摘要,不建立 `collection_result` 主表。 - 同一 PDD 商品最多存在一个 `pending` 或 `running` 任务。 - 指定设备只能由该设备领取;未指定任务由在线空闲设备原子领取。 - 重置在事务中清空原结果,保留 URL、goods_id、规则和设备快照,状态恢复为 `pending`。 - Android 本地互斥与服务端原子领取共同保证单设备串行。 - 正式采购在 Android 本地 SQLite 事务中先保存不可逆状态、稳定服务端请求 ID 和脱敏最终确认快照,再通知服务端并只允许一次创建订单点击;重启后只重放服务端标记、只读核单或上报,不再次点击。 - 原始控件树和截图不持久化;Android Agent 端第一期不使用 OCR/VLM。服务端 SYB 登录验证码识别是唯一例外,见 [#48](https://git.ilapage.cn/OPC/goauto/issues/48)。 - 蝦皮规格映射独立保存在 `shopee_product.specs_json`:颜色只能选择关联 PDD 当前可选颜色,允许多个蝦皮颜色共用一个 PDD 颜色;尺码自动匹配只预览格式统一后的唯一确定结果。PDD 目标规格消失后页面标记失效,映射保存和采购创建均拒绝继续使用;无需新增数据库表或 Android 能力。 ## 最小业务数据 | 表 | 必要内容 | |---|---| | `agent_device` | 唯一 `install_id`、设备信息、状态、Token 摘要、版本化能力、最后心跳 | | `pdd_product` | 唯一 `goods_id`、当前 URL、标题、店铺、数字销量/评价、三态状态和通用多维 `specs_json` 最新值 | | `collection_rule` | `id`、`name`、`content_json`、创建/更新时间、`deleted_at` | | `collection_task` | 商品/设备外键、五态状态、URL/goods_id/规则快照、租约、结果摘要、错误和时间 | | `collection_dimension` | 任务、维度键、名称、排序 | | `collection_dimension_value` | 维度、值、排序 | | `collection_color_price` | 任务、颜色、该颜色统一使用的整数分价格 | | `collection_sku` | 任务、整数分价格、可用性、完整性 | | `collection_sku_value` | SKU 与规格值的多对多关联 | | `pdd_account` | 可选的账号调度引用,只保存名称和状态,不保存凭据 | | `purchase_task` | 商品外键和不可变快照、执行模式、状态/租约 guard、价格边界、订单、人工支付复核、物流与回填事实 | | `purchase_task_attempt` | `task_id + attempt_id` 幂等执行记录、阶段、规则哈希、固化规格决策和结构化错误 | | `ai_matching_setting` | 唯一单例的启用状态、OpenAI-compatible Base URL、模型、超时、内部部署明文 API Key 和更新人;仅管理员设置接口可以读取该字段 | `collection_task` 的状态仅为 `pending`、`running`、`completed`、`completed_partial`、`failed`。设备身份和心跳表属于 Agent 领取任务的必要基础,不承载 PDD 业务数据。 设备以 JSON 数组保存最后一次注册或心跳上报的版本化能力。v1 任务兼容未上报能力的旧 Agent;v2 任务在创建指定设备任务、获取下一任务、领取和开始四个边界重复校验能力,避免旧 APK 执行未知规则。 数据库使用两个可空 guard 列表达跨数据库唯一约束:活动任务的 `active_slot=1`,运行中设备的 `device_run_slot=1`;终态记录对应列为 `NULL`。复合唯一索引据此保证同商品最多一个活动任务、同设备最多一个运行中任务,同时允许保留任意数量的终态历史任务。状态与 guard 列还有数据库检查约束,必须在同一条状态变更语句中更新。 采购表使用同一 guard 思路:`purchase_task.active_slot` 保证同一 SYB 明细最多一个活动任务,`device_run_slot` 保证设备串行,`account_run_slot` 在账号已知时保证账号串行。账号未知是合法状态。正式任务必须引用 SYB 的规则由模型钩子和后续创建服务双重校验;MySQL 8.4 不允许 `syb_product_id` 同时参与带参照动作的外键和跨字段 CHECK,因此不在该列添加数据库 CHECK。 ## 配置分层 配置分三层,下层覆盖上层: | 层 | 文件 | 是否进 Git | 放什么 | |---|---|---|---| | 1 默认值 | `server/config/settings.yml` | **是** | 应用配置和非机密运维参数(`extend.syb` 的 `baseurl` / `pagesize` / `maxmatches` / `ocrurl`) | | 2 部署本地值 | `config.yaml`(仓库根) | **否**(`.gitignore`) | 数据库、端口、顺云宝凭据 | | 3 覆盖值 | `GOAUTO_*` 环境变量 | — | 服务、容器和 CI 用;优先级最高 | `[必须]` **凭据只允许出现在第 2、3 层。** `settings.yml` 被 Git 跟踪,任何时候都不能往里写账号密码。 服务端自己读第 2 层(`config/local.go` 的 `ApplyLocalConfig`),查找顺序为 `GOAUTO_CONFIG` 指定的路径 → 当前目录 `./config.yaml` → 可执行文件同级目录。**找不到不是错误**:容器场景只用环境变量,本来就没有这个文件。回调注册在 `ApplyEnvironment` 之前,环境变量因此始终有最后决定权。 `[必须]` 第 2 层的标量按 YAML 原始类型读取后统一转字符串。纯数字密码不加引号会被 YAML 读成整数,这里不能因此启动失败——但仍应加引号,否则前导零会丢。 开发启动脚本把 `config.yaml` 的路径通过 `GOAUTO_CONFIG` 传给服务端,不在 PowerShell 里重复解析 YAML。 ## 前端伺服 `web/.env.production` 的 `VUE_APP_BASE_API` 为空,即生产构建发的是相对请求,**前端与接口必须同源**。服务端在 `dist/index.html` 存在时伺服构建产物并为 history 路由回退到 `index.html`(`app/admin/router/spa.go`)。 `[必须]` 回退只对非接口路径的 GET 生效。给写错的接口路径回 200 + HTML,客户端看到的会是 JSON 解析错误而不是 404。 `[必须]` `dist` 不存在时不注册 `NoRoute`。开发环境前端跑在 vite 独立端口上,装了回退会把每个真 404 变成一张 HTML 页。 ## 已建立的工程入口 | 功能 | 当前目录 | |---|---| | 服务端基线 | `server/`(go-admin v2.3.0) | | 最小闭环数据模型 | `server/app/goauto/models/` | | 数据迁移与约束测试 | `server/app/goauto/migrations/` | | go-admin 迁移注册 | `server/cmd/migrate/migration/version-local/1786700000000_goauto_schema.go` | | 设备注册、认证、停用与吊销 | `server/app/goauto/device/` | | PDD 商品与规则 | `server/app/goauto/product/`、`server/app/goauto/rule/` | | PDD 商品档案增量迁移 | `server/cmd/migrate/migration/version-local/1786700500000_pdd_product_archive.go` | | 虾皮商品档案与规格映射 | `server/app/goauto/shopeeproduct/`、`web/src/views/goauto/shopee-products/`;Admin 按确认原型提供颜色下拉人工映射、确定性尺码批量预览、失效提示和单一保存入口,SYB 快捷入口可突出目标颜色;不修改两端原始规格 | | 虾皮商品档案增量迁移 | `server/cmd/migrate/migration/version-local/1786700600000_shopee_product_archive.go` | | SYB 商品明细导入、解析与虾皮档案合并 | `server/app/goauto/sybimport/`(解析、幂等落库、同步编排、导入端点、管理端 API 和 Admin 页面均已实现) | | 顺云宝(SYB)ERP HTTP 客户端与登录会话 | `server/app/goauto/sybclient/`(登录、OCR 验证码、会话缓存、列表与明细读取;见 [SYB-ERP-Interface-Contract](SYB-ERP-Interface-Contract)) | | SYB 商品明细增量迁移 | `server/cmd/migrate/migration/version-local/1786700700000_syb_product_import.go` | | SYB 店铺准入、发现与过滤 | `server/app/goauto/sybshop/`、`server/app/goauto/sybimport/`;迁移 `server/cmd/migrate/migration/version-local/1786700900000_syb_shop.go` | | SYB 后台导入任务、进度、单任务互斥与启动恢复 | `server/app/goauto/sybimport/sync_run.go`、`sync_run_handler.go`;表 `syb_sync_run`,迁移 `server/cmd/migrate/migration/version-local/1786701000000_syb_sync_run.go` | | 采购任务数据与类型化规则契约 | `server/app/goauto/models/purchase.go`、`server/app/goauto/purchasecontract/`;迁移 `server/cmd/migrate/migration/version-local/1786701100000_purchase_contract.go` | | 采购任务单条/批量预检与创建、租约、attempt 幂等、Admin 只读查询和人工处置状态机 | `server/app/goauto/purchase/`;`batch-preview` 通过批量预加载 SYB、蝦皮、PDD 与最新任务执行快速只读预检,不访问 AI;`batch-create` 仍按最新数据逐项完整复核并可在必要时调用 AI;既有追加迁移为 `server/cmd/migrate/migration/version-local/1786701200000_purchase_state_machine.go` | | 采购规格标准化匹配与 AI Provider 设置 | `server/app/goauto/aimatching/`;服务端先做繁简、空白/全半角/大小写和公斤/斤的唯一确定性匹配,再按需调用单一 OpenAI-compatible Provider;`1786701300000_ai_matching_setting.go` 创建设置表,`1786701400000_ai_matching_setting_plain_api_key.go` 将原加密列迁移为内部明文 `api_key`,仅管理员读取 | | 任务领取、结果、重置与删除 | `server/app/goauto/task/` | | 管理端基线 | `web/`(go-admin-ui v3.0.0) | | 管理端闭环页面 | `web/src/views/goauto/` | | Admin 采购任务列表、详情与人工处理 | `web/src/views/goauto/purchase-tasks/`、`web/src/api/goauto/purchase-tasks.js`;创建入口不在本模块 | | Admin AI 规格匹配设置 | `web/src/views/goauto/ai-matching-settings/`、`web/src/api/goauto/ai-matching-settings.js`;管理员可查看、维护和测试 Provider(包括内部明文 API Key),采购员只可查看启用状态 | | Admin 失败采购任务批量重试 | `POST /api/admin/v1/purchase-tasks/batch-retry`;服务端 `server/app/goauto/purchase/retry.go` 负责资格判定、逐项幂等创建与部分成功结果,Admin 页面只允许选择服务端标记可重试的行;不修改 Android Agent | | SYB 店铺管理页面与接口封装 | `web/src/views/goauto/syb-shops/`、`web/src/api/goauto/syb-shops.js`;确认原型快照 `prototypes/49/v1/index.html` | | SYB 异步导入、当前页采购选择/确认/逐条结果与同步记录页面 | `web/src/views/goauto/syb-products/`、`web/src/views/goauto/syb-sync-runs/`、`web/src/api/goauto/syb-products.js`、`web/src/api/goauto/purchase-tasks.js`;确认原型见 #44 设计证据,导入原型快照为 `prototypes/50/v2/index.html` | | Android Agent 基线 | `android/app/src/main/java/cn/ilapage/goauto/agent/` | | Android 无障碍规则执行 | `android/app/src/main/java/cn/ilapage/goauto/agent/automation/` | | PDD 安全规格入口与评论页误触恢复 | `automation/PddProductDetailCollector.kt`:明确选择语义、底部购买文字、评论上下文排除、一次返回重试和结构化错误;不保存控件树或截图 | | Android 采购规则解释器与正式地址/订单动作 | `android/app/src/main/java/cn/ilapage/goauto/agent/automation/PurchaseRuleContract.kt`、`PurchaseRehearsalExecutor.kt`、`PurchaseLiveAutomation.kt` | | Android 采购任务、attempt 与 Outbox 持久化 | `android/app/src/main/java/cn/ilapage/goauto/agent/persistence/`;调度入口 `service/AgentForegroundService.kt` | | 三端统一验证 | `scripts/verify.ps1` | 以上入口均已落地;新增 API、表或执行动作时必须同步更新本代码地图与共享契约。 ## cmautobuy 商品离线导入 `server/cmd/import-cmautobuy-products/` 是 #70 的独立单向导入入口,转换逻辑位于 `server/app/goauto/cmautobuyimport/`。它不注册 HTTP 路由、不参与服务启动,也不是数据库 schema migration。 导入器从 cmautobuy MySQL 只读一致性快照读取有效 `pdd_products`、`shopee_products` 和 `shopee_skus`,再写入 GoAuto 的 `pdd_product`、`shopee_product`。旧 PDD 组合 SKU 聚合为 GoAuto 通用维度;同一颜色出现多个不同价格时不猜测价格。旧组合 `spec_mappings`、SYB、任务、订单和其他域数据不进入导入范围。 默认模式是 dry-run;显式 `--apply` 才会在目标 MySQL 单事务覆盖同业务键商品。来源与目标配置都来自未跟踪 YAML,连接串和密码不输出。来源蝦皮的字符串 `pdd_goods_id` 必须通过目标 PDD `goods_id` 换算为数字 `pdd_product_id`,提交前重新校验 JSON、唯一键和关联完整性。