# 天工·商品标签 (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. OFF(Open 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` | A–E | | `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。