新增 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>
17 KiB
商品档案公益 API 系统 — 深化规划 (v2.0)
在 v1.0 定稿基础上,全面展开 8 个方向,并新增 商品分类体系 与 单位管理体系 两章。 不变原则:只采集 + 只提供信息,绝不涉及购买/交易行为。 全文仍为规划,未写代码。
目录
- A. 商品分类体系(新增)
- B. 单位管理体系(新增)
-
- 数据库详细设计
-
- API 详细契约
-
- 数据治理(去重/冲突/质量评分/溯源)
-
- 采集合规细则
-
- 部署与运维
-
- 众包贡献流程
-
- 项目治理(域名/许可/免责)
-
- 时间与里程碑估算
A. 商品分类体系(Taxonomy)
商品分类是整个档案库的骨架,直接影响搜索、参数模板、去重。建议对齐国际标准 + 自建可读品类树双轨。
A.1 采用 GS1 GPC 作为标准骨架
GS1 GPC(Global Product Classification) 是四层、规则化的全球商品分类,8 位数字编码:
Segment(段) → Family(族) → Class(类) → Brick(砖)
47000000 47100000 47101800 10000xxx
清洁/卫生 清洁用品 ... 具体品类(GTIN挂这里)
- 全球 44 个 Segment,食品快消主要落在 Food/Beverage/Tobacco 与 Cleaning/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_100g或per_100ml(按品类模板决定),并保留serving_size原值。 - 能量双单位:同时存 kJ + kcal(1 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.name用pg_trgmGIN(模糊搜索)。product.attributes与food_detail.nutriments建 JSONB GIN 索引(参数检索)。category.path用ltree或前缀索引(子树查询)。- 全文检索:初期
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+ ETag;CDN + Redis;条码查命中率高。 - CORS:开放 GET(公益 API)。
- 分页:
page/size(上限 100);大结果集用search_after游标(OpenSearch 阶段)。
3. 数据治理
3.1 实体去重 / 匹配
- 强匹配:相同
gtin→ 同一商品(条码是天然主键)。 - 弱匹配(无 gtin 或 gtin 缺失):
(规范化品牌 + 规范化型号/名称 + 净含量)相似度(trigram/编辑距离)+ 阈值;命中候选进人工/众包确认,不自动硬合并。 - 合并:保留一条 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 横向扩展。
- 规模化:K8s(API Deployment + HPA、worker Job/CronJob)、OpenSearch 集群、对象存储用云 S3。
5.2 可观测性
- 指标:Prometheus(QPS、延迟、缓存命中、限流计数、采集成功率)。
- 日志:结构化日志 + request_id 贯穿。
- 链路:OpenTelemetry(API → DB)。
- 告警:错误率/延迟/采集失败/磁盘。
5.3 备份与可靠性
- Postgres 每日全量 + WAL 归档;定期恢复演练。
- 对象存储多版本/冗余。
- 采集 worker 幂等 + 重试 + 死信队列。
5.4 成本(量级估算,公益项目控成本)
- MVP:单台小型云主机(2C4G)+ 对象存储即可(月成本很低)。
- OFF 食品子集约数百万条,PG 单实例可承载;图片走对象存储 + CDN(按流量)。
- 详细预算待定(取决于云厂商与访问量),可后续出一版成本表。
6. 众包贡献流程
公益库靠社区补全/纠错。流程:
- 提交:用户对某商品提交新增/修改(带可选来源链接、照片)。
- 校验:单位/格式/品类模板校验 + 反垃圾(限频、信誉分、验证码)。
- 审核:低风险字段自动接受并标
source=community;高风险(价格、品牌)进人工/资深用户审核队列。 - 版本化:每次修改存历史版本,可 diff、可回滚(类似 wiki)。
- 信誉系统:贡献被采纳提升信誉;高信誉用户审核权更大。
- 溯源透明:众包数据与官方数据在
sources中明确区分。
注意:众包内容也要遵守"只客观信息、不导购",并保留权利方下架通道。
7. 项目治理(域名/许可/免责)
- 品牌/域名:建议中性、表意清晰的名字(如 商品档案 / OpenGoods 之类),后续选定。
- 代码许可:开源(如 MIT/Apache-2.0),鼓励复用。
- 数据许可:ODbL + 署名(因含 OFF);API 文档明示再利用条款。
- 隐私:不收集个人数据(PII),只处理商品信息;众包账号信息最小化。
- 免责声明(站点显著位置):
- "本站为公益信息平台,仅提供商品参数信息,不提供任何购买/交易服务。"
- "价格为官方建议零售价历史快照,实际售价以零售商为准,不构成消费或购买建议。"
- "数据来自多来源并标注出处,可能存在误差;欢迎纠错,权利方可申请更正/下架。"
- 下架/纠错渠道:公开邮箱/表单,承诺响应时限。
8. 时间与里程碑估算
仅为相对工作量估算(以"理想工作日"计,非承诺排期);实际取决于投入人力与数据源接入难度。
| 里程碑 | 内容 | 估算 | 依赖 | 主要风险 |
|---|---|---|---|---|
| M0 地基 | Go/Python 骨架、Compose、CI、数据契约 | 3–5 d | — | 低 |
| M1 数据模型 | 迁移、分类树、单位字典、参数模板 | 4–6 d | M0 | 分类/单位建模需打磨 |
| M2 种子数据 | OFF dump 导入 + 单位归一 + 分类映射 | 5–8 d | M1 | dump 体量大、字段映射脏 |
| M3 MVP API(Go) | 端点 + 缓存/限流 + OpenAPI | 5–8 d | M1,M2 | 检索性能调优 |
| M4 采集管线(Python) | OFF/GS1 adapter + ETL + 去重 + 质量分 + 调度 | 8–12 d | M2 | 去重/冲突算法、合规 |
| M5 开放/规模化 | 搜索引擎、CDN、API Key、众包后台、文档站 | 10–15 d | M3,M4 | 众包审核与防滥用 |
关键路径:M0→M1→M2→M3(最快拿到可查询 MVP);M4/M5 可与后续并行迭代。
9. 待确认(本版新增点)
- 分类标准:认同以 GS1 GPC 为标准骨架 + 自建中文品类树映射吗?
- 单位策略:营养统一折算到
per_100g/per_100ml、能量双存 kJ+kcal,认同吗? - 质量评分权重:上面的 0.4/0.3/0.2/0.1 权重是否合适,或你有偏好?
- 众包:第一阶段就要做众包贡献,还是先纯采集、后期再开放众包?
- 项目命名/域名:有想好的名字吗?没有的话我可以提几个候选。
确认后我把 v2.0 收敛为可执行的工程任务清单。需要动手写代码时你说一声,我从 M0 开始搭骨架开 PR。