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

18 KiB
Raw Blame History

天工·商品标签 (OpenGoods) — 深化规划 (v3.0)

在最终版基础上,展开全部 10 个进阶方向。仍为规划,未写代码。 原则不变:只采集 + 只提供信息,绝不涉及购买/交易。

目录

  1. 完整 OpenAPI 规范草案
  2. 完整数据库 DDL
  3. 数据契约文档
  4. OFF 字段映射表
  5. 中国合规专项
  6. 测试与数据质量保障
  7. 安全与反滥用
  8. 商品图片处理
  9. 可用性与 SLA
  10. 竞品 / 同类项目分析

1. 完整 OpenAPI 规范草案(节选骨架,写代码时落到 docs/openapi.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(全部表)

-- 扩展
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)、dimensionsource.licenseimage.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_categorycategory 走 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/数据 dumpOFF dump、USDA、GS1 授权)——从源头规避爬虫风险。

5.4 食品信息合规

  • 展示食品参数时注明"信息仅供参考,以实物标签为准";营养/成分以官方/厂商标签为准。
  • 不做医疗/功效宣称;不构成消费建议。

5.5 价格与"不导购"

  • 价格仅为官方建议零售价历史快照,显著标注;全站无购买/下单/跳转购买链接,避免被认定为经营性电商导购。

6. 测试与数据质量保障

6.1 代码测试

  • Gohandler 单元测试 + store 层用 testcontainers/临时 PG 集成测试 + API 契约测试(对 openapi.yaml 校验响应)。
  • Python:ETL 纯函数单测(单位归一、分类映射、去重打分)+ adapter 用录制的样例数据测试(不打真实站点)。
  • CIPR 必跑 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-auditCI 中扫描。

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。