feat: 定义运行时规格解析契约与审计模型 (#254)
This commit is contained in:
@@ -127,6 +127,8 @@ v18(#208)新增 `shops` 业务店铺和 `shop_channel_aliases` 渠道名称
|
||||
名称精确回填,无法确认的记录保持未关联。
|
||||
v19(#212)将产品概念收敛为唯一店铺名称和唯一启停状态。迁移按“任一旧开关停用
|
||||
则保持停用”的安全规则合并 v18 状态,重建由 `shops` 派生的兼容数据和两侧关联。
|
||||
v26(#254)新增 `purchase_spec_resolutions`,用一张表保存采购执行中的候选观察、
|
||||
幂等身份和最终规格决策;不修改历史 SQLite migration,也不覆盖 PDD 商品主数据。
|
||||
|
||||
**v3 为什么丢弃旧 `sku_mappings` 数据(见 #20):** 新主键需要 `pdd_option_key`,
|
||||
这是 Go 的 `service.OptionKey()` 用 `json.Marshal` 算出来的规范化键,SQL 语句
|
||||
@@ -884,8 +886,8 @@ syb_orders ─(shopee_goods_id, spec_key)─┘
|
||||
(skus_json 里是所有规格和价格)
|
||||
|
||||
syb_orders ──创建──→ tasks ──分配──→ clients
|
||||
↑
|
||||
users(采购员)──1:N 当前归属──────────┘
|
||||
└──1:N──→ purchase_spec_resolutions
|
||||
users(采购员)──1:N 当前归属──────────→ clients
|
||||
```
|
||||
|
||||
两条关联都可以变,这是有意的:
|
||||
@@ -906,6 +908,7 @@ Admin 使用集中 MySQL 8.4,Client 仍使用各自的本地 SQLite;两者
|
||||
| 任务编号 | `tasks.task_id` | `pdd_tasks.remote_task_id` |
|
||||
| 任务状态 | 7 个(§6) | 8 个,是本机执行状态 |
|
||||
| 采集结果 | `pdd_products.skus_json` | `pdd_tasks.pdd_data` |
|
||||
| 运行时规格解析 | `purchase_spec_resolutions` 候选观察与最终决策 | #257 在本地保存请求、幂等键和响应;原任务 payload 不覆盖 |
|
||||
|
||||
`[必须]` **两边的状态是两套,不要试图同步。**
|
||||
Admin 只知道"发出去了 / 收到结果了",中间过程看不到,这是有意的设计。
|
||||
@@ -1052,3 +1055,93 @@ CREATE UNIQUE INDEX idx_client_assignment_current
|
||||
不新增批次表:提交时所选 `ready` 记录必须在同一事务内全部转为 `queued`;后台每次最多
|
||||
读取 20 条并逐条领取为 `applying`。页面按 `apply_batch_id` 聚合当前批次进度。Admin
|
||||
重启时,`queued` 恢复为 `ready` 且不会自动写远端,`applying` 转为 `needs_check`。
|
||||
|
||||
## 18. `purchase_spec_resolutions` 采购运行时规格解析(MySQL v26)
|
||||
|
||||
Client 在同一次采购中选中颜色后,如果任务尺码和 PDD 页面尺码无法精确对应,就通过
|
||||
[Client 接口契约](../client/04-admin-api-contract.md) §7.1 提交当前可购买候选。本表用
|
||||
**一条记录**同时保存候选观察、幂等身份和最终决策,不拆观察表、决策表或模型响应表。
|
||||
|
||||
```sql
|
||||
CREATE TABLE purchase_spec_resolutions (
|
||||
resolution_id VARCHAR(191) COLLATE utf8mb4_bin PRIMARY KEY,
|
||||
task_id VARCHAR(191) COLLATE utf8mb4_bin NOT NULL,
|
||||
attempt_id VARCHAR(191) COLLATE utf8mb4_bin NOT NULL,
|
||||
client_id VARCHAR(191) COLLATE utf8mb4_bin NOT NULL,
|
||||
task_version BIGINT NOT NULL CHECK (task_version > 0),
|
||||
pdd_goods_id VARCHAR(191) COLLATE utf8mb4_bin NOT NULL,
|
||||
original_options_json JSON NOT NULL,
|
||||
selected_color VARCHAR(191) NOT NULL,
|
||||
target_size VARCHAR(191) NOT NULL,
|
||||
candidates_json JSON NOT NULL,
|
||||
candidate_snapshot_hash CHAR(64) COLLATE utf8mb4_bin NOT NULL,
|
||||
request_hash CHAR(64) COLLATE utf8mb4_bin NOT NULL,
|
||||
observed_at VARCHAR(35) NOT NULL,
|
||||
|
||||
outcome VARCHAR(16) NOT NULL DEFAULT 'pending',
|
||||
decision_source VARCHAR(16),
|
||||
chosen_candidate_id VARCHAR(16) COLLATE utf8mb4_bin,
|
||||
resolved_options_json JSON,
|
||||
confidence_bps INT,
|
||||
reason VARCHAR(500),
|
||||
provider_id VARCHAR(191) COLLATE utf8mb4_bin,
|
||||
source_model VARCHAR(191),
|
||||
config_fingerprint CHAR(64) COLLATE utf8mb4_bin,
|
||||
rules_version VARCHAR(32),
|
||||
prompt_version VARCHAR(32),
|
||||
created_at VARCHAR(35) NOT NULL,
|
||||
decided_at VARCHAR(35),
|
||||
|
||||
UNIQUE KEY uq_purchase_spec_resolution_identity
|
||||
(task_id, attempt_id, candidate_snapshot_hash),
|
||||
KEY idx_purchase_spec_resolution_task
|
||||
(task_id, created_at DESC, resolution_id),
|
||||
KEY idx_purchase_spec_resolution_outcome
|
||||
(outcome, created_at, resolution_id),
|
||||
CONSTRAINT chk_purchase_spec_resolution_options CHECK (
|
||||
JSON_TYPE(original_options_json) = 'OBJECT'
|
||||
AND JSON_LENGTH(original_options_json) BETWEEN 1 AND 16
|
||||
AND JSON_TYPE(candidates_json) = 'ARRAY'
|
||||
AND JSON_LENGTH(candidates_json) BETWEEN 1 AND 100
|
||||
AND (resolved_options_json IS NULL
|
||||
OR JSON_TYPE(resolved_options_json) = 'OBJECT')),
|
||||
CONSTRAINT chk_purchase_spec_resolution_outcome CHECK (
|
||||
outcome IN ('pending', 'matched', 'uncertain', 'rejected', 'failed')),
|
||||
CONSTRAINT chk_purchase_spec_resolution_source CHECK (
|
||||
decision_source IS NULL OR decision_source IN ('rule', 'ai', 'reused')),
|
||||
CONSTRAINT chk_purchase_spec_resolution_confidence CHECK (
|
||||
confidence_bps IS NULL OR confidence_bps BETWEEN 0 AND 10000),
|
||||
CONSTRAINT chk_purchase_spec_resolution_decision CHECK (
|
||||
(outcome = 'pending' AND decided_at IS NULL
|
||||
AND decision_source IS NULL AND chosen_candidate_id IS NULL
|
||||
AND resolved_options_json IS NULL)
|
||||
OR (outcome = 'matched' AND decided_at IS NOT NULL
|
||||
AND decision_source IS NOT NULL AND chosen_candidate_id IS NOT NULL
|
||||
AND resolved_options_json IS NOT NULL AND reason IS NOT NULL)
|
||||
OR (outcome IN ('uncertain', 'rejected', 'failed')
|
||||
AND decided_at IS NOT NULL AND chosen_candidate_id IS NULL
|
||||
AND resolved_options_json IS NULL AND reason IS NOT NULL))
|
||||
);
|
||||
```
|
||||
|
||||
- `(task_id, attempt_id, candidate_snapshot_hash)` 是业务唯一身份;`request_hash` 用于发现
|
||||
同一身份改了目标尺码、候选原文或其他内容。HTTP 的 `idempotency_keys` 仍保存可安全
|
||||
重放的完整响应,两层幂等不能互相替代。
|
||||
- `pending` 是内部恢复状态,不返回给 Client。先用短事务写候选观察,规则或 AI 调用在
|
||||
事务外执行;最终 outcome 与 `idempotency_keys.response_body` 在另一个短事务一起提交。
|
||||
崩溃留下 pending 时,相同请求读取原记录继续恢复,不能新建第二条。
|
||||
- `matched` 必须有来源、候选短编号、原始 options、原因和决定时间;候选必须逐字来自
|
||||
`candidates_json`。`uncertain/rejected/failed` 不得填写候选和 resolved options,避免
|
||||
Client 把不确定结果当作可点击规格。
|
||||
- `confidence_bps` 可空。NULL 表示规则或复用路径没有可比较分数,不得转换成 0;AI
|
||||
分数使用 0~10000 基点。服务商、模型、配置指纹和规则/提示版本都是非敏感快照,
|
||||
表中没有 API Key、完整提示词或完整模型响应。
|
||||
- 本表故意不设指向 `tasks` 或 `clients` 的外键。现有页面允许硬删除任务和临时 Client
|
||||
清单记录,规格决策审计不能因此阻塞原流程或被级联删除;Repository 查询始终使用稳定
|
||||
ID,不把缺少主表记录解释成新的业务状态。
|
||||
- 本表不参与任务状态机,不更新 `tasks.status/result_data`,也不写
|
||||
`pdd_products.skus_json` 或长期 `spec_mappings`。它只回答这一执行尝试在这一候选快照下
|
||||
可以安全选择哪个原始候选。
|
||||
- `original_options_json`、`candidates_json` 和 `resolved_options_json` 只保存接口定义的
|
||||
颜色/尺码字符串。不得保存原始无障碍 XML、截图、订单号、收货信息、Cookie、Token、
|
||||
密码或 API Key;时间统一保存为 UTC ISO 8601。
|
||||
|
||||
Reference in New Issue
Block a user