# 商品档案公益 API 系统 — 深化规划 (v2.0) > 在 v1.0 定稿基础上,全面展开 8 个方向,并新增 **商品分类体系** 与 **单位管理体系** 两章。 > 不变原则:**只采集 + 只提供信息,绝不涉及购买/交易行为。** 全文仍为规划,未写代码。 **目录** - A. 商品分类体系(新增) - B. 单位管理体系(新增) - 1. 数据库详细设计 - 2. API 详细契约 - 3. 数据治理(去重/冲突/质量评分/溯源) - 4. 采集合规细则 - 5. 部署与运维 - 6. 众包贡献流程 - 7. 项目治理(域名/许可/免责) - 8. 时间与里程碑估算 --- ## 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 返回结构提示。 ```jsonc // 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` ```jsonc { "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 关键建表草案(节选) ```sql 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_trgm` GIN(模糊搜索)。 - `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 ``` ```jsonc { "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 实体去重 / 匹配 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 横向扩展。 - **规模化**: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. 众包贡献流程 公益库靠社区补全/纠错。流程: 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、数据契约 | 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. 待确认(本版新增点) 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。