Files
goods/docs/planning/02-advanced-topics-v3.0.md
T
lixu 54175238ad docs: 归档天工·商品标签(OpenGoods)规划文档
新增 docs/planning/ 规划文档归档:
- 00-final-plan: 最终规划(决策+架构+M0~M5任务清单)
- 01-detailed-design-v2.0: 分类/单位/数据库/API/治理/采集/部署/众包
- 02-advanced-topics-v3.0: OpenAPI/全表DDL/数据契约/OFF映射/中国合规/测试/安全/图片/SLA/竞品
- history/: v0.1~v1.0 演进记录
更新 README 为项目介绍并链接规划文档。

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-06-08 06:05:29 +00:00

409 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 天工·商品标签 (OpenGoods) — 深化规划 (v3.0)
> 在最终版基础上,展开全部 10 个进阶方向。仍为规划,未写代码。
> 原则不变:**只采集 + 只提供信息,绝不涉及购买/交易。**
**目录**
1. 完整 OpenAPI 规范草案
2. 完整数据库 DDL
3. 数据契约文档
4. OFF 字段映射表
5. 中国合规专项
6. 测试与数据质量保障
7. 安全与反滥用
8. 商品图片处理
9. 可用性与 SLA
10. 竞品 / 同类项目分析
---
## 1. 完整 OpenAPI 规范草案(节选骨架,写代码时落到 `docs/openapi.yaml`
```yaml
openapi: 3.1.0
info:
title: OpenGoods API (天工·商品标签)
version: "1.0.0"
description: >
公益商品参数查询 API。只提供信息,不提供购买/交易。
数据采用 ODbL 许可并署名来源。
license: {name: ODbL-1.0, url: https://opendatacommons.org/licenses/odbl/}
servers:
- {url: https://api.opengoods.org/api/v1}
paths:
/products/barcode/{gtin}:
get:
summary: 按条码查询商品档案
parameters:
- {name: gtin, in: path, required: true, schema: {type: string, pattern: '^[0-9]{8,14}$'}}
- {name: fields, in: query, schema: {type: string}, description: 逗号分隔字段裁剪}
responses:
'200': {description: OK, content: {application/json: {schema: {$ref: '#/components/schemas/ProductEnvelope'}}}}
'404': {description: 未找到, content: {application/json: {schema: {$ref: '#/components/schemas/Error'}}}}
'429': {description: 限流, headers: {Retry-After: {schema: {type: integer}}}}
/products/{id}:
get: { summary: 按ID查询, parameters: [{name: id, in: path, required: true, schema: {type: string, format: uuid}}], responses: {'200': {description: OK}} }
/products/search:
get:
summary: 搜索/过滤/分页
parameters:
- {name: q, in: query, schema: {type: string}}
- {name: brand, in: query, schema: {type: string}}
- {name: category, in: query, schema: {type: string}}
- {name: allergen_free, in: query, schema: {type: string}}
- {name: page, in: query, schema: {type: integer, default: 1}}
- {name: size, in: query, schema: {type: integer, default: 20, maximum: 100}}
responses: {'200': {description: OK, content: {application/json: {schema: {$ref: '#/components/schemas/SearchEnvelope'}}}}}
/products/{id}/nutriments: {get: {summary: 仅营养}}
/products/{id}/msrp: {get: {summary: 仅官方零售价(含免责)}}
/brands: {get: {summary: 品牌列表}}
/categories: {get: {summary: 品类树}}
/sources/{id}:{get: {summary: 数据来源透明说明}}
/healthz: {get: {summary: 健康检查}}
components:
schemas:
ProductEnvelope:
type: object
properties:
data: {$ref: '#/components/schemas/Product'}
meta: {type: object}
sources: {type: array, items: {$ref: '#/components/schemas/SourceRef'}}
Product:
type: object
properties:
id: {type: string, format: uuid}
gtin: {type: string}
name: {type: string}
brand: {type: string}
category: {type: string}
net_content: {$ref: '#/components/schemas/Quantity'}
food: {$ref: '#/components/schemas/FoodDetail'}
msrp: {$ref: '#/components/schemas/Msrp'}
quality_score: {type: number}
Quantity:
type: object
properties: {value: {type: number}, unit: {type: string}, canonical: {type: object}}
FoodDetail:
type: object
properties:
ingredients_text: {type: string}
allergens: {type: array, items: {type: string}}
additives: {type: array, items: {type: string}}
nutriments: {type: object}
nutrition_basis: {type: string, enum: [per_100g, per_100ml, per_serving]}
nutri_score: {type: string}
Msrp:
type: object
properties:
amount: {type: number}
currency: {type: string}
region: {type: string}
effective_date: {type: string, format: date}
note: {type: string, default: "官方建议零售价, 本站不提供购买"}
SourceRef:
type: object
properties: {source: {type: string}, url: {type: string}, fetched_at: {type: string}, license: {type: string}}
Error:
type: object
properties: {error: {type: object, properties: {code: {type: string}, message: {type: string}, request_id: {type: string}}}}
```
> 该 `openapi.yaml` 既是契约也是文档源:Go 端用它做路由校验/生成 Swagger UI,客户端可由它生成 SDK。
---
## 2. 完整数据库 DDL(全部表)
```sql
-- 扩展
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE EXTENSION IF NOT EXISTS ltree;
-- gen_random_uuid() 由 pgcrypto 提供
CREATE TABLE source (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL, -- Open Food Facts / USDA / GS1-China ...
homepage TEXT,
license TEXT, -- ODbL / CC0 / proprietary
trust_weight NUMERIC(3,2) DEFAULT 0.5, -- 来源可信度(冲突解决用)
notes TEXT
);
CREATE TABLE brand (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
normalized_name TEXT, -- 规范化(去空格/大小写/全半角)用于匹配
aliases TEXT[],
UNIQUE(normalized_name)
);
CREATE TABLE manufacturer (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
normalized_name TEXT,
country VARCHAR(64),
UNIQUE(normalized_name)
);
CREATE TABLE category (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name_zh TEXT NOT NULL,
name_en TEXT,
parent_id UUID REFERENCES category(id),
path LTREE, -- 物化路径, 子树查询
gpc_brick_code VARCHAR(8), -- 映射到 GS1 GPC
level INT,
UNIQUE(path)
);
CREATE TABLE category_schema ( -- 品类参数模板
category_id UUID PRIMARY KEY REFERENCES category(id),
required_attributes TEXT[],
recommended_attributes TEXT[],
nutriment_basis VARCHAR(16)
);
CREATE TABLE unit ( -- 单位字典
code VARCHAR(16) PRIMARY KEY,
dimension VARCHAR(16) NOT NULL, -- mass/volume/energy/count/ratio/length/duration
canonical VARCHAR(16) NOT NULL,
to_canonical_factor NUMERIC, -- code -> canonical 的换算因子
aliases TEXT[],
display TEXT
);
CREATE TABLE attribute_definition ( -- 参数字典(标准名/别名/单位)
key VARCHAR(64) PRIMARY KEY,
label_zh TEXT, label_en TEXT,
dimension VARCHAR(16),
default_unit VARCHAR(16) REFERENCES unit(code),
aliases TEXT[]
);
CREATE TABLE product (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
gtin VARCHAR(14) UNIQUE,
name TEXT NOT NULL,
brand_id UUID REFERENCES brand(id),
manufacturer_id UUID REFERENCES manufacturer(id),
category_id UUID REFERENCES category(id),
gpc_brick_code VARCHAR(8),
net_content_value NUMERIC,
net_content_unit VARCHAR(16),
net_content_canonical NUMERIC,
country_of_origin VARCHAR(64),
shelf_life_days INT,
storage TEXT,
attributes JSONB DEFAULT '{}',
quality_score NUMERIC(4,3) DEFAULT 0,
status VARCHAR(16) DEFAULT 'active', -- active/merged/deprecated
canonical_id UUID REFERENCES product(id), -- 被合并到哪个
search_tsv TSVECTOR,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE food_detail (
product_id UUID PRIMARY KEY REFERENCES product(id) ON DELETE CASCADE,
ingredients_text TEXT,
ingredients JSONB,
allergens TEXT[],
additives TEXT[],
nutriments JSONB,
nutrition_basis VARCHAR(16),
serving_size VARCHAR(32),
nutri_score CHAR(1),
labels TEXT[]
);
CREATE TABLE product_msrp (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
product_id UUID REFERENCES product(id) ON DELETE CASCADE,
amount NUMERIC(12,2) NOT NULL,
currency CHAR(3) NOT NULL,
region VARCHAR(8) DEFAULT 'CN',
source_id UUID REFERENCES source(id),
source_url TEXT,
effective_date DATE,
note TEXT,
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE product_image (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
product_id UUID REFERENCES product(id) ON DELETE CASCADE,
url TEXT, -- 对象存储 URL
kind VARCHAR(16), -- front/ingredients/nutrition
license TEXT,
source_id UUID REFERENCES source(id)
);
CREATE TABLE product_source ( -- 字段级溯源
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
product_id UUID REFERENCES product(id) ON DELETE CASCADE,
source_id UUID REFERENCES source(id),
url TEXT,
fields TEXT[],
fetched_at TIMESTAMPTZ,
raw JSONB
);
CREATE TABLE merge_log ( -- 合并/回滚
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
kept_id UUID, merged_id UUID,
reason TEXT, by TEXT, created_at TIMESTAMPTZ DEFAULT now()
);
-- 索引
CREATE UNIQUE INDEX idx_product_gtin ON product(gtin) WHERE gtin IS NOT NULL;
CREATE INDEX idx_product_name_trgm ON product USING gin (name gin_trgm_ops);
CREATE INDEX idx_product_attrs ON product USING gin (attributes);
CREATE INDEX idx_food_nutriments ON food_detail USING gin (nutriments);
CREATE INDEX idx_product_tsv ON product USING gin (search_tsv);
CREATE INDEX idx_category_path ON category USING gist (path);
CREATE INDEX idx_product_updated ON product(updated_at);
```
---
## 3. 数据契约文档(`docs/data-contract.md` 概要)
两端共享的"事实约定",避免 Go/Python 理解不一致:
- **枚举固定**`status`(active/merged/deprecated)、`nutrition_basis`(per_100g/per_100ml/per_serving)、`dimension``source.license``image.kind`
- **字段含义与可空性**:逐字段写明(如 `gtin` 可空、唯一;`quality_score` ∈ [0,1])。
- **单位规则**:原始 + canonical 双存;能量双存 kJ/kcal;换算因子来自 `unit` 表。
- **写入责任**:仅 Python(ingestion) 写库;Go 只读。所有写入走 ETL,保证归一化与溯源。
- **版本**:契约本身版本化;schema 变更需同步更新契约 + 迁移 + OpenAPI。
- **示例**:附 1 条完整 product JSON 作为"黄金样例",两端测试都对它断言。
---
## 4. OFFOpen Food Facts)字段映射表(导入直接用)
| OFF 字段 (CSV) | OpenGoods 字段 | 处理 |
|----------------|----------------|------|
| `code` | `product.gtin` | 校验 8/12/13/14 位 + 校验位 |
| `product_name` / `product_name_zh` | `product.name` | 优先中文, 回退英文 |
| `brands` | `brand.name` | 拆分取首个, 规范化, upsert brand |
| `categories` / `categories_tags` | `source_category``category` | 走 OFF→自建→GPC 映射表 |
| `quantity` | `net_content_*` | 解析数值+单位 → 归一化 |
| `countries` | `country_of_origin` | 取销售国/产地 |
| `ingredients_text` | `food_detail.ingredients_text` | 原文保留 |
| `allergens_tags` | `food_detail.allergens` | 标签清洗为中文 |
| `additives_tags` | `food_detail.additives` | E-number 解析 |
| `energy-kj_100g` / `energy-kcal_100g` | `nutriments.energy_kj/kcal` | 缺一个则换算, 标 derived |
| `fat_100g` `saturated-fat_100g` `carbohydrates_100g` `sugars_100g` `proteins_100g` `salt_100g` | `nutriments.*` | 归一 per_100g |
| `nutriscore_grade` | `food_detail.nutri_score` | AE |
| `serving_size` | `food_detail.serving_size` | 原值 |
| `image_url` / `image_front_url` 等 | `product_image.url` | 下载转存对象存储, 记 CC-BY-SA |
| `last_modified_t` | `product_source.fetched_at` | 增量基准 |
| (整行) | `product_source.raw` | 存原始快照 |
> 许可:OFF 数据=ODbL(衍生库需同样开放+署名);图片=CC-BY-SA。映射时全程记 `source_id=OFF`。
---
## 5. 中国合规专项(公益网站落地关键)
> 以下为工程与运营层面的合规要点梳理,**非法律意见**;正式上线前建议咨询专业法务。
### 5.1 网站备案
- 服务器在中国大陆 → 需 **ICP 备案**(公益网站可走非经营性 ICP 备案);部分地区/类目可能涉 **公安联网备案**
- 若用境外/港澳服务器可免 ICP,但访问速度与合规另作权衡。
### 5.2 数据合规(网络安全法 / 数据安全法 / 个人信息保护法)
- 本项目**只处理商品信息、不收集个人信息(PII)**,PIPL 风险低;众包阶段涉及用户账号时再做最小化收集 + 隐私政策。
- 《数据安全法》要求数据收集合法正当;做好数据分级与安全保护义务。
### 5.3 网络爬虫法律边界(重点)
依据中央网信办公开文章与司法实践,判断标准是**客观结果**——是否妨碍目标网站正常运行 / 危害合法权益:
- **守 robots.txt**、礼貌限速、错峰,**不得对目标站造成 DDoS 式压力**(否则可能触及破坏计算机信息系统罪等)。
- **不抓取非公开/需登录/绕过反爬**的数据(可能涉非法获取计算机信息系统数据罪)。
- 只采**客观公开的商品参数**;不抓取受版权保护内容、不抓个人信息。
- 优先用**官方开放数据/API/数据 dump**OFF dump、USDA、GS1 授权)——从源头规避爬虫风险。
### 5.4 食品信息合规
- 展示食品参数时注明"信息仅供参考,以实物标签为准";营养/成分以官方/厂商标签为准。
- 不做医疗/功效宣称;不构成消费建议。
### 5.5 价格与"不导购"
- 价格仅为**官方建议零售价历史快照**,显著标注;**全站无购买/下单/跳转购买链接**,避免被认定为经营性电商导购。
---
## 6. 测试与数据质量保障
### 6.1 代码测试
- **Go**handler 单元测试 + store 层用 `testcontainers`/临时 PG 集成测试 + API 契约测试(对 openapi.yaml 校验响应)。
- **Python**:ETL 纯函数单测(单位归一、分类映射、去重打分)+ adapter 用录制的样例数据测试(不打真实站点)。
- **CI**PR 必跑 lint + test;覆盖率门槛(如 ETL 核心 ≥80%)。
### 6.2 数据质量
- **入库校验**:gtin 校验位、单位可识别、营养数值合理区间、必填字段(按品类模板)。
- **质量评分**:见 v2.0 公式,低分进"待补全"队列。
- **数据回归**:黄金样例集 + 定期跑"数据健康检查"(孤儿记录、单位异常、重复 gtin、营养越界)。
- **可观测**:导入报表(新增/更新/拒绝条数、拒绝原因 top)。
---
## 7. 安全与反滥用
- **API 防刷**IP 限流 + 可选 API Key 分级配额;异常流量识别(突发高频降级/挑战)。
- **缓存挡压**:热点条码走 CDN/Redis,降低数据库压力,也抗刷。
- **输入校验**:所有参数严格校验(gtin 正则、size 上限),防注入(参数化查询,禁拼 SQL)。
- **密钥管理**:DB/对象存储/第三方 key 走环境变量/密钥管理,不入库不入仓。
- **最小权限**:Go 端用**只读** DB 账号;写权限仅 ingestion。
- **采集端被封应对**:合规限速 + 失败退避 + 死信队列 + 切换为官方 dump/API。
- **DDoS**CDN + 速率限制 + 云厂商防护;公益服务以可降级(只读缓存)保命。
- **依赖安全**Go `govulncheck`、Python `pip-audit`CI 中扫描。
---
## 8. 商品图片处理
- **版权**OFF 图片为 CC-BY-SA,须署名 + 同样开放;逐图记 `license` 与来源。
- **存储**:对象存储(MinIO/S3),路径按 `gtin/kind`;原图 + 生成多档缩略图(thumb/medium)。
- **处理管线**:下载 → 校验(类型/大小) → 去重(感知哈希避免重复) → 压缩 → 生成缩略图 → 记录。
- **分发**CDN 加速;API 只返回图片 URL,不内嵌二进制。
- **合规**:不展示含个人信息的图;提供权利方下架通道。
- **降级**:图片缺失返回占位;图片服务故障不影响参数 API。
---
## 9. 可用性与 SLA
| 项 | 目标(建议) |
|----|-----------|
| API 可用性 | 99.5%(公益项目务实目标,先保只读可用) |
| 读延迟 | p95 < 200ms(缓存命中 < 50ms |
| 数据新鲜度 | 增量同步 T+1(每日) |
| 降级策略 | DB 故障 → 只读缓存兜底;图片/搜索故障不影响核心参数查询 |
| 灾备 | 每日备份 + 异地副本;恢复演练季度一次 |
| 维护窗口 | 采集/重建索引放低峰;API 滚动发布不停服 |
> 公益项目优先"省成本 + 稳定只读";写入(采集)可异步、可补偿,读路径要稳。
---
## 10. 竞品 / 同类项目分析
| 项目 | 性质 | 数据 | 借鉴点 | 与我们差异 |
|------|------|------|--------|------------|
| **Open Food Facts** | 公益食品库 | ODbL, 海量, 可贡献, 有 dump/API | 字段模型、众包、Nutri-Score、API 设计 | 我们多语言架构(Go API)、聚焦中文/GPC、收 MSRP |
| **USDA FoodData Central** | 政府营养库 | CC0, 权威营养 | 营养数据补全、公共领域许可 | 偏美国/营养, 无条码生态 |
| **GS1 / Verified by GS1** | 官方条码登记 | 受限, 权威, 2亿+ | 条码→品牌/规格权威源 | 非开放、需授权 |
| **Wikidata** | 通用知识库 | CC0, 有 GTIN 属性(P3962) | 实体链接、结构化、开放 | 非商品专用、参数不规整 |
| **schema.org Product/gtin** | 数据标准 | 标准而非数据 | 用其词汇做对外结构化(SEO/互操作) | 仅规范, 需我们填数据 |
| **brocade.io / 各条码库** | 开放/商业条码库 | 参差 | 条码补全兜底 | 数据量/质量有限或收费 |
**结论与定位**
- 我们不是再造 OFF,而是做**面向中文世界、与 GS1 GPC 对齐、聚焦"商品参数标签"**的公益 API
- **站在巨人肩上**OFF/USDA 做种子与营养,GS1 做条码权威,Wikidata/schema.org 做实体与互操作标准;
- 差异化:中文优先、品类参数模板规整、官方 MSRP、字段级溯源、Go 高并发只读 API。
---
## 11. 小结
v3.0 已把工程落地与公益合规的关键面全部展开。规划层面已相当完整。
你之前说先不写代码,我**继续待命**:可以再深化任何一块,或等你说"开始",从 M0 搭骨架开 PR。