Files
goods/docs/planning/01-detailed-design-v2.0.md
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

17 KiB
Raw Permalink Blame History

商品档案公益 API 系统 — 深化规划 (v2.0)

在 v1.0 定稿基础上,全面展开 8 个方向,并新增 商品分类体系单位管理体系 两章。 不变原则:只采集 + 只提供信息,绝不涉及购买/交易行为。 全文仍为规划,未写代码。

目录

  • A. 商品分类体系(新增)
  • B. 单位管理体系(新增)
    1. 数据库详细设计
    1. API 详细契约
    1. 数据治理(去重/冲突/质量评分/溯源)
    1. 采集合规细则
    1. 部署与运维
    1. 众包贡献流程
    1. 项目治理(域名/许可/免责)
    1. 时间与里程碑估算

A. 商品分类体系(Taxonomy

商品分类是整个档案库的骨架,直接影响搜索、参数模板、去重。建议对齐国际标准 + 自建可读品类树双轨。

A.1 采用 GS1 GPC 作为标准骨架

GS1 GPCGlobal Product Classification 是四层、规则化的全球商品分类,8 位数字编码:

Segment(段) → Family(族) → Class(类) → Brick(砖)
47000000     47100000      47101800    10000xxx
清洁/卫生     清洁用品       ...          具体品类(GTIN挂这里)
  • 全球 44 个 Segment,食品快消主要落在 Food/Beverage/TobaccoCleaning/Hygiene 等段。
  • Brick 是最细粒度,商品(GTIN)挂在 brick 上;每个 brick 可带 ≤25 个属性,正好对应我们的"品类参数模板"。
  • 好处:与 GS1/电商/数据池天然对齐,便于将来对接 OFF、USDA、GS1-China。

A.2 三层映射策略

用途 来源
标准码 (gpc_brick_code) 机器对齐、跨源映射 GS1 GPC
自建品类树 (category) 人类可读、网站导航、中文友好 自建,映射到 GPC
来源原始分类 (source_category) 保留溯源 OFF categories / USDA / GS1

OFF 有自己的 categories taxonomy(标签式、多语言),导入时做 OFF category → 自建 category → GPC brick 的映射表,未命中的进人工/众包校对队列。

A.3 品类参数模板(Category Schema

每个叶子品类定义"应有哪些参数",用于:① 数据完整度评分 ② 录入/校验约束 ③ API 返回结构提示。

// category_schema 示例: 包装水
{
  "category_id": "beverage/packaged_water",
  "gpc_brick_code": "10000159",
  "required_attributes": ["net_content", "shelf_life"],
  "recommended_attributes": ["ph", "tds", "water_type"],
  "nutriment_basis": "per_100ml"
}

A.4 分类落地要点

  • 分类树存为邻接表 + 物化路径path 列,便于子树查询)。
  • 多对一:一个商品归一个主品类(primary),可挂多个辅助标签(labels)。
  • 分类可演进:用 category_version 管理重命名/合并,旧 ID 重定向不破坏 API。

B. 单位管理体系(Units

食品参数单位混乱(g/kg/ml/L/份/%/kcal/kJ…),必须有统一的单位字典 + 量纲 + 归一化机制,否则无法比较和检索。

B.1 量纲与基准单位

量纲 (dimension) 基准单位 (canonical) 常见单位
质量 mass g mg, g, kg, 斤, oz, lb
体积 volume ml ml, L, cl, fl oz
能量 energy kJ kJ, kcal(同时存两者)
数量 count 个/瓶/包/片/粒
比例 ratio %(或无量纲) %, ‰, mg/100g
长度 length mm mm, cm, m, in
时间(保质期) duration 天/月/年

B.2 单位字典 unit

{
  "code": "kg",
  "dimension": "mass",
  "to_canonical_factor": 1000,     // 1 kg = 1000 g
  "canonical": "g",
  "aliases": ["千克", "公斤", "kgs"],
  "display": "kg"
}

B.3 归一化规则

  • 入库双存:原始值/单位 {value, unit} + 归一化值 {canonical_value, canonical_unit},原始保留供溯源与展示。
  • 营养基准统一:全部折算到 per_100gper_100ml(按品类模板决定),并保留 serving_size 原值。
  • 能量双单位:同时存 kJ + kcal1 kcal ≈ 4.184 kJ),缺一个则自动换算并标记 derived=true
  • 不可换算:count(个/瓶)等不跨量纲换算;只做单位别名归一。
  • 精度与舍入:用十进制(NUMERIC)避免浮点误差;记录有效数字。
  • 冲突处理:单位无法识别 → 入"待清洗队列",不丢数据。

B.4 单位与 API

  • API 默认返回原始单位 + 归一化值两套;可加 ?unit_system=metric|original 控制展示。
  • 搜索/过滤一律基于 canonical 值(如"热量<200kcal/100g")。

1. 数据库详细设计

PostgreSQL。核心:关系表 + JSONB 灵活属性 + 结构化营养子表。以下为 DDL 草案(写代码时落到 migrations/)。

1.1 ER 概览

brand 1───* product *───1 category ───* category_schema
manufacturer 1───* product
product 1───1 food_detail
product 1───* product_msrp
product 1───* product_source     (溯源, 字段级)
product *───* attribute (via product_attribute, 或 JSONB)
unit (字典)   attribute_definition (参数字典)
contribution / merge_log / source (治理与登记)

1.2 关键建表草案(节选)

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,               -- 归一化(g/ml)
  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',
  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,                          -- [{name,rank}]
  allergens     TEXT[],
  additives     TEXT[],
  nutriments    JSONB,                           -- 见单位章, 归一到 per_100g/ml
  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,               -- ISO 4217
  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_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                             -- 原始快照
);

1.3 索引策略

  • product.gtin 唯一索引;product.namepg_trgm GIN(模糊搜索)。
  • product.attributesfood_detail.nutrimentsJSONB GIN 索引(参数检索)。
  • category.pathltree 或前缀索引(子树查询)。
  • 全文检索:初期 tsvector(name+brand+ingredients) GIN;规模化后迁 OpenSearch。
  • 时间列 updated_at 索引(增量同步)。

2. API 详细契约

只读、版本化、统一信封。下面给核心端点的示例。

2.1 按条码查询(最常用)

GET /api/v1/products/barcode/3017624010701?fields=name,brand,nutriments,msrp
{
  "data": {
    "id": "…", "gtin": "3017624010701",
    "name": "示例牌 巧克力榛子酱 400g", "brand": "示例牌",
    "category": "食品/酱料/巧克力酱",
    "net_content": {"value":400,"unit":"g","canonical":{"value":400,"unit":"g"}},
    "food": {
      "nutriments": {"energy_kcal":539,"energy_kj":2255,"fat_g":30.9,"sugars_g":56.3,"salt_g":0.107},
      "nutrition_basis":"per_100g", "allergens":["坚果","乳","大豆"]
    },
    "msrp": {"amount":29.90,"currency":"CNY","region":"CN","effective_date":"2026-01-01",
             "note":"官方建议零售价, 本站不提供购买"}
  },
  "meta": {"version":"v1"},
  "sources": [{"source":"Open Food Facts","url":"…","fetched_at":"…","license":"ODbL"}]
}

2.2 搜索

GET /api/v1/products/search?q=巧克力&brand=示例牌&category=酱料&allergen_free=花生&page=1&size=20&fields=…

返回 data:[…] + meta:{page,size,total,total_pages}

2.3 端点清单 & 错误码

端点 说明
GET /products/barcode/{gtin} 条码查
GET /products/{id} ID 查
GET /products/search 搜索/过滤/分页
GET /products/{id}/nutriments 仅营养
GET /products/{id}/msrp 仅官方价(含免责)
GET /brands GET /categories 品牌 / 品类树
GET /sources/{id} 数据来源透明说明
GET /healthz GET /openapi.json 健康检查 / 机读文档

错误码:400(参数错) 404(未找到, 返回 {error:{code:"not_found"}}) 429(限流, 带 Retry-After) 5xx(服务端)。统一错误信封 {error:{code,message,request_id}}

2.4 跨切面

  • 版本化 /v1/;破坏性变更升 /v2/,旧版保留过渡期。
  • 限流:匿名 IP 默认 60 req/min(可调);免费 API Key 提配额。响应头 X-RateLimit-*
  • 缓存Cache-Control + ETagCDN + Redis;条码查命中率高。
  • CORS:开放 GET(公益 API)。
  • 分页page/size(上限 100);大结果集用 search_after 游标(OpenSearch 阶段)。

3. 数据治理

3.1 实体去重 / 匹配

  1. 强匹配:相同 gtin → 同一商品(条码是天然主键)。
  2. 弱匹配(无 gtin 或 gtin 缺失):(规范化品牌 + 规范化型号/名称 + 净含量) 相似度(trigram/编辑距离)+ 阈值;命中候选进人工/众包确认,不自动硬合并。
  3. 合并:保留一条 canonical,其余标 status=merged 并写 merge_log(可回滚)。

3.2 多源字段冲突解决

  • 每个字段记录来源 + 时间 + 来源可信度权重。
  • 冲突时:① 按来源可信度GS1官方 > 厂商官网 > OFF众包 > 第三方)② 同级取最新 ③ 数值类可取多数/中位数。
  • 保留所有来源值于 product_source.raw,对外 sources 字段透明展示"该字段来自谁"。

3.3 质量评分公式(0~1

quality_score = 0.4*完整度 + 0.3*来源权威度 + 0.2*多源一致性 + 0.1*新鲜度
  完整度   = 命中品类模板 required/recommended 字段的比例
  权威度   = 贡献字段的来源权重加权
  一致性   = 多源同字段一致的比例
  新鲜度   = 最近更新时间衰减

低分商品在搜索中降权,并进入"待补全"队列(可派给众包)。

3.4 溯源(Provenance

字段级溯源:每条数据可回答"这个营养值/价格来自哪个来源、什么时间、什么许可"。这是公益项目可信度的核心,也用于许可合规标注。


4. 采集合规细则

4.1 通用护栏

  • 严格遵守 robots.txt 与各源服务条款;礼貌限速(OFF 读 ≤15 req/min/IP);错峰;明确 User-Agent 标识本项目身份与联系方式。
  • 只采客观参数;不照搬受版权保护的营销文案/评测原文(链接来源即可)。
  • 增量优先:用 last_modified/dump 差异做增量,避免重复抓取。

4.2 各源接入步骤

步骤 许可
OFF dump(首批种子) 下载 en.openfoodfacts.org.products.csv.gz(~0.9G压缩) → 解析 → 映射字段 → 入库 ODbL(衍生库需 ODbL+署名)
OFF API(增量) 按 gtin 拉取/按更新时间增量;遵守限速 同上
USDA FoodData Central 申请免费 API key;或下载 Branded/Foundation JSON;补全营养 CC0(最宽松)
GS1 / 中国商品信息服务平台 条码→品牌/规格/厂商;API ≤1000 GTIN/次(需授权) 受限,按授权使用
厂商官网 逐站 adapter,遵守 robots,取官方规格表/MSRP 取客观参数

4.3 许可合规

  • OFF=ODbL(传染性,衍生数据库须同样开放+署名 OFF);USDA=CC0。
  • 对外数据库整体采用 ODbL + 署名;每条数据按 sources[].license 标注其来源许可,避免冲突。

5. 部署与运维

5.1 演进路径

  • 初期Docker Compose 一键起 postgres + redis + minio + go-api + python-worker,单机即可跑通 MVP。
  • 成长期:API 多副本 + 读副本数据库 + CDN;worker 横向扩展。
  • 规模化K8sAPI Deployment + HPA、worker Job/CronJob)、OpenSearch 集群、对象存储用云 S3。

5.2 可观测性

  • 指标:Prometheus(QPS、延迟、缓存命中、限流计数、采集成功率)。
  • 日志:结构化日志 + request_id 贯穿。
  • 链路:OpenTelemetryAPI → DB)。
  • 告警:错误率/延迟/采集失败/磁盘。

5.3 备份与可靠性

  • Postgres 每日全量 + WAL 归档;定期恢复演练。
  • 对象存储多版本/冗余。
  • 采集 worker 幂等 + 重试 + 死信队列。

5.4 成本(量级估算,公益项目控成本)

  • MVP:单台小型云主机(2C4G)+ 对象存储即可(月成本很低)。
  • OFF 食品子集约数百万条,PG 单实例可承载;图片走对象存储 + CDN(按流量)。
  • 详细预算待定(取决于云厂商与访问量),可后续出一版成本表。

6. 众包贡献流程

公益库靠社区补全/纠错。流程:

  1. 提交:用户对某商品提交新增/修改(带可选来源链接、照片)。
  2. 校验:单位/格式/品类模板校验 + 反垃圾(限频、信誉分、验证码)。
  3. 审核:低风险字段自动接受并标 source=community;高风险(价格、品牌)进人工/资深用户审核队列。
  4. 版本化:每次修改存历史版本,可 diff、可回滚(类似 wiki)。
  5. 信誉系统:贡献被采纳提升信誉;高信誉用户审核权更大。
  6. 溯源透明:众包数据与官方数据在 sources 中明确区分。

注意:众包内容也要遵守"只客观信息、不导购",并保留权利方下架通道。


7. 项目治理(域名/许可/免责)

  • 品牌/域名:建议中性、表意清晰的名字(如 商品档案 / OpenGoods 之类),后续选定。
  • 代码许可:开源(如 MIT/Apache-2.0),鼓励复用。
  • 数据许可ODbL + 署名(因含 OFF);API 文档明示再利用条款。
  • 隐私:不收集个人数据(PII),只处理商品信息;众包账号信息最小化。
  • 免责声明(站点显著位置)
    • "本站为公益信息平台,仅提供商品参数信息,不提供任何购买/交易服务。"
    • "价格为官方建议零售价历史快照,实际售价以零售商为准,不构成消费或购买建议。"
    • "数据来自多来源并标注出处,可能存在误差;欢迎纠错,权利方可申请更正/下架。"
  • 下架/纠错渠道:公开邮箱/表单,承诺响应时限。

8. 时间与里程碑估算

仅为相对工作量估算(以"理想工作日"计,非承诺排期);实际取决于投入人力与数据源接入难度。

里程碑 内容 估算 依赖 主要风险
M0 地基 Go/Python 骨架、Compose、CI、数据契约 35 d
M1 数据模型 迁移、分类树、单位字典、参数模板 46 d M0 分类/单位建模需打磨
M2 种子数据 OFF dump 导入 + 单位归一 + 分类映射 58 d M1 dump 体量大、字段映射脏
M3 MVP API(Go) 端点 + 缓存/限流 + OpenAPI 58 d M1,M2 检索性能调优
M4 采集管线(Python) OFF/GS1 adapter + ETL + 去重 + 质量分 + 调度 812 d M2 去重/冲突算法、合规
M5 开放/规模化 搜索引擎、CDN、API Key、众包后台、文档站 1015 d M3,M4 众包审核与防滥用

关键路径:M0→M1→M2→M3(最快拿到可查询 MVP);M4/M5 可与后续并行迭代。


9. 待确认(本版新增点)

  1. 分类标准:认同以 GS1 GPC 为标准骨架 + 自建中文品类树映射吗?
  2. 单位策略:营养统一折算到 per_100g/per_100ml、能量双存 kJ+kcal,认同吗?
  3. 质量评分权重:上面的 0.4/0.3/0.2/0.1 权重是否合适,或你有偏好?
  4. 众包:第一阶段就要做众包贡献,还是先纯采集、后期再开放众包?
  5. 项目命名/域名:有想好的名字吗?没有的话我可以提几个候选。

确认后我把 v2.0 收敛为可执行的工程任务清单。需要动手写代码时你说一声,我从 M0 开始搭骨架开 PR。