From 54175238ad139586fcb957b56cfc016388f8fb4c Mon Sep 17 00:00:00 2001 From: John Doe <1115084741@qq.com> Date: Mon, 8 Jun 2026 06:05:29 +0000 Subject: [PATCH] =?UTF-8?q?docs:=20=E5=BD=92=E6=A1=A3=E5=A4=A9=E5=B7=A5?= =?UTF-8?q?=C2=B7=E5=95=86=E5=93=81=E6=A0=87=E7=AD=BE(OpenGoods)=E8=A7=84?= =?UTF-8?q?=E5=88=92=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 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> --- README.md | 26 +- docs/planning/00-final-plan.md | 115 +++++ docs/planning/01-detailed-design-v2.0.md | 373 ++++++++++++++++ docs/planning/02-advanced-topics-v3.0.md | 408 ++++++++++++++++++ docs/planning/README.md | 40 ++ docs/planning/history/v0.1-initial-plan.md | 216 ++++++++++ .../history/v0.2-go-python-foodfmcg.md | 203 +++++++++ .../planning/history/v1.0-locked-decisions.md | 107 +++++ 8 files changed, 1486 insertions(+), 2 deletions(-) create mode 100644 docs/planning/00-final-plan.md create mode 100644 docs/planning/01-detailed-design-v2.0.md create mode 100644 docs/planning/02-advanced-topics-v3.0.md create mode 100644 docs/planning/README.md create mode 100644 docs/planning/history/v0.1-initial-plan.md create mode 100644 docs/planning/history/v0.2-go-python-foodfmcg.md create mode 100644 docs/planning/history/v1.0-locked-decisions.md diff --git a/README.md b/README.md index 7482f81..2ec9390 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,24 @@ -# goods -商品档案公开API +# 天工·商品标签 (OpenGoods) + +商品档案公开 API —— 公益网站/服务:**采集全网商品信息,对外提供商品参数查询 API**。 + +> 核心原则:**只采集 + 只提供信息,绝不涉及任何购买/下单/比价导购。** + +## 这是什么 + +- 开放、中立、可溯源的「商品参数百科 + 开放 API」 +- 首批聚焦 **食品快消**,对外提供按条码/名称查询商品参数(成分、营养、规格、官方建议零售价等) +- 技术栈:**Go**(对外只读 API) + **Python**(采集/ETL),经 PostgreSQL + Redis/队列解耦 + +## 项目状态 + +规划阶段。完整方案见 [`docs/planning/`](./docs/planning/README.md): + +- [最终规划](./docs/planning/00-final-plan.md)(决策 + 架构 + 任务清单) +- [详细设计 v2.0](./docs/planning/01-detailed-design-v2.0.md)(分类/单位/数据库/API/治理/采集/部署) +- [进阶专题 v3.0](./docs/planning/02-advanced-topics-v3.0.md)(OpenAPI/DDL/合规/测试/安全/SLA/竞品) + +## 许可 + +- 代码:拟用 Apache-2.0 / MIT(待定) +- 数据:拟用 **ODbL + 署名**(因采用 Open Food Facts 等开放数据源) diff --git a/docs/planning/00-final-plan.md b/docs/planning/00-final-plan.md new file mode 100644 index 0000000..36e3d32 --- /dev/null +++ b/docs/planning/00-final-plan.md @@ -0,0 +1,115 @@ +# 天工·商品标签 (OpenGoods) — 最终规划 (Final) + +> 公益网站/服务:采集全网商品信息,提供商品参数查询 API。 +> **核心原则:只采集 + 只提供信息,绝不涉及任何购买/下单/比价导购。** +> 本文档为前几版(v0.1 → v2.0)的最终收敛版,所有关键决策已锁定。详细设计见 v2.0 附件。 + +--- + +## 0. 项目标识 +- **中文名**:天工·商品标签(呼应《天工开物》) +- **英文名**:OpenGoods +- **定位**:开放、中立、可溯源的"商品参数百科 + 开放 API" + +--- + +## 1. 已锁定的全部决策 + +| 维度 | 决策 | +|------|------| +| 首批品类 | **食品快消** | +| 价格 | 只收 **官方标准零售价 (MSRP)**:静态字段,带 currency/region/source/effective_date + 免责;**不收实时电商价、无购买入口** | +| 技术栈 | **Go**(对外只读 API/核心服务) + **Python**(采集/ETL/爬虫),经 **PostgreSQL + Redis/队列** 解耦 | +| 种子数据 | **Open Food Facts 食品 dump** 先导入,最快有真实数据 | +| 数据许可 | 对外数据库用 **ODbL + 署名**;CC0 来源(USDA)自由混入;每条数据按来源标注许可 | +| 商品分类 | **GS1 GPC 四层标准码 (Segment→Family→Class→Brick)** 为骨架 + **自建中文品类树** 映射 + 保留来源原始分类 | +| 单位管理 | 量纲字典;**原始值 + 归一化值双存**;营养统一折算到 `per_100g/per_100ml`;能量 **双存 kJ+kcal**;用十进制(NUMERIC)防误差 | +| 质量评分 | `0.4*完整度 + 0.3*来源权威 + 0.2*多源一致 + 0.1*新鲜度` | +| 众包 | **一期不做众包,先纯采集**;二期再开放贡献/纠错(带审核与版本化) | +| Go 框架 | `chi` + 标准库 `net/http`(轻量) | +| 迁移工具 | `golang-migrate`(纯 SQL,两端共享 schema) | +| 部署 | 初期 Docker Compose(postgres+redis+minio+go-api+python-worker)→ 后期 K8s | +| 地域 | 先用 OFF 全球食品库起步,后接 GS1-China 补强中国数据 | + +--- + +## 2. 架构(定稿) + +``` + 数据源: OFF dump / OFF API / USDA(CC0) / GS1-China + │ + ▼ Python: 采集 adapters → ETL(清洗/单位归一/分类映射/去重/质量评分) + │ 写入 + ┌────▼─────────┐ 图片 ┌──────────┐ + │ PostgreSQL │◀───────▶│ MinIO/S3 │ + │ (商品档案主库)│ └──────────┘ + └────▲─────────┘ + │ 只读 (+Redis 缓存/限流) + ▼ Go: 公开 REST API + OpenAPI 文档 + 各种软件 / 开发者 (无任何交易端点) +``` +两端不直接互调,通过共享 PostgreSQL schema + 《数据契约文档》协作。 + +--- + +## 3. 仓库结构(写代码时落地) +``` +goods/ (OpenGoods 天工·商品标签) +├── README.md +├── LICENSE # 代码: Apache-2.0/MIT; 数据: ODbL 说明 +├── docker-compose.yml +├── docs/{data-contract.md, openapi.yaml, disclaimer.md} +├── migrations/ # golang-migrate 共享 SQL +├── api/ # Go 只读 API (chi) +│ ├── cmd/server/main.go +│ └── internal/{handler,store,model,middleware}/ +└── ingestion/ # Python 采集 + ETL + ├── adapters/{openfoodfacts,usda,gs1}.py + ├── etl/{normalize_units,map_category,dedup,quality}.py + └── jobs/{seed_off_dump,scheduler}.py +``` + +--- + +## 4. 最终可执行任务清单(按里程碑) + +**M0 — 工程地基**(~3–5d) +- [ ] Go module + Python 项目骨架 +- [ ] docker-compose(postgres+redis+minio) +- [ ] CI(Go build/vet/test;Python ruff/pytest) +- [ ] `docs/data-contract.md`、`docs/disclaimer.md`(不提供购买声明)初版 + +**M1 — 数据模型 + 分类 + 单位**(~4–6d) +- [ ] migrations:product / food_detail / product_msrp / product_source / brand / manufacturer / category / category_schema / unit / attribute_definition / merge_log +- [ ] 导入 GS1 GPC 骨架 + 建自建中文品类树 + 映射表 +- [ ] 单位字典 + 归一化规则 +- [ ] 索引:gtin 唯一、name trigram、JSONB GIN、category ltree + +**M2 — 种子数据 (Python)**(~5–8d) +- [ ] 下载 OFF 食品 dump → 字段映射(成分/营养/过敏原/图片) +- [ ] 单位归一 + 分类映射入库 +- [ ] USDA(CC0) 营养补全(可选) + +**M3 — MVP API (Go)**(~5–8d) +- [ ] 端点:barcode / id / search / nutriments / msrp / brands / categories / sources / healthz +- [ ] 统一响应信封、分页、`fields=` 裁剪、错误码 +- [ ] Redis 缓存 + IP 限流 + OpenAPI 文档 + +**M4 — 采集管线 (Python)**(~8–12d) +- [ ] adapter:OFF API 增量 + GS1 条码补全 +- [ ] ETL:清洗/归一/去重合并/冲突解决/质量评分/字段级溯源 +- [ ] 调度(定时增量更新) + +**M5 — 开放与规模化**(~10–15d) +- [ ] 搜索引擎(PG 全文 → OpenSearch)、CDN +- [ ] 免费 API Key(防滥用+统计) +- [ ] 众包贡献后台(提交/审核/版本/信誉) +- [ ] 开发者文档站 + 开放数据许可与免责声明上线 + +> 关键路径:M0→M1→M2→M3(最快拿到可查询 MVP);M4/M5 后续并行迭代。 + +--- + +## 5. 下一步 +规划已全部定稿。你之前说"先不写代码",所以我**停在这里待命**。 +等你说"开始",我从 **M0 工程地基** 动手,搭好骨架后开 PR 给你看(也可指定先只做某几个里程碑,例如 M0+M1)。 diff --git a/docs/planning/01-detailed-design-v2.0.md b/docs/planning/01-detailed-design-v2.0.md new file mode 100644 index 0000000..34d5ab3 --- /dev/null +++ b/docs/planning/01-detailed-design-v2.0.md @@ -0,0 +1,373 @@ +# 商品档案公益 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。 diff --git a/docs/planning/02-advanced-topics-v3.0.md b/docs/planning/02-advanced-topics-v3.0.md new file mode 100644 index 0000000..b180273 --- /dev/null +++ b/docs/planning/02-advanced-topics-v3.0.md @@ -0,0 +1,408 @@ +# 天工·商品标签 (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。 diff --git a/docs/planning/README.md b/docs/planning/README.md new file mode 100644 index 0000000..dfe2fe1 --- /dev/null +++ b/docs/planning/README.md @@ -0,0 +1,40 @@ +# 天工·商品标签 (OpenGoods) — 规划文档归档 + +本目录归档了项目从立项到方案定稿的全部规划文档。 + +## 项目一句话 +公益网站/服务:**采集全网商品信息,对外提供商品参数查询 API**。 +核心原则:**只采集 + 只提供信息,绝不涉及任何购买/下单/比价导购。** + +## 当前文档(最新,建议优先阅读) + +| 文档 | 内容 | +|------|------| +| [00-final-plan.md](./00-final-plan.md) | **最终规划**:锁定的全部决策 + 架构 + 仓库结构 + M0~M5 可执行任务清单 | +| [01-detailed-design-v2.0.md](./01-detailed-design-v2.0.md) | **详细设计**:商品分类体系(GS1 GPC)、单位管理、数据库设计、API 契约、数据治理、采集合规、部署运维、众包、里程碑估算 | +| [02-advanced-topics-v3.0.md](./02-advanced-topics-v3.0.md) | **进阶专题**:完整 OpenAPI、全表 DDL、数据契约、OFF 字段映射、中国合规专项、测试、安全反滥用、图片处理、SLA、竞品分析 | + +## 演进历史(History) + +| 文档 | 阶段 | +|------|------| +| [history/v0.1-initial-plan.md](./history/v0.1-initial-plan.md) | 初版总体规划 | +| [history/v0.2-go-python-foodfmcg.md](./history/v0.2-go-python-foodfmcg.md) | 确定 Go+Python 架构、聚焦食品快消、数据源调研 | +| [history/v1.0-locked-decisions.md](./history/v1.0-locked-decisions.md) | 决策定稿(默认值) | + +## 已锁定的关键决策(速览) + +| 维度 | 决策 | +|------|------| +| 首批品类 | 食品快消 | +| 价格 | 只收官方标准零售价 (MSRP),静态字段,无购买入口 | +| 技术栈 | Go(对外只读 API) + Python(采集/ETL),经 PostgreSQL + Redis/队列解耦 | +| 种子数据 | Open Food Facts 食品 dump | +| 数据许可 | 对外 ODbL + 署名;CC0 来源(USDA)自由混入 | +| 商品分类 | GS1 GPC 四层标准码 + 自建中文品类树映射 | +| 单位管理 | 原始值+归一化双存;营养统一 per_100g/ml;能量双存 kJ+kcal | +| 质量评分 | 0.4 完整度 + 0.3 来源权威 + 0.2 多源一致 + 0.1 新鲜度 | +| 众包 | 一期不做,先纯采集;二期开放 | +| Go 框架 / 迁移 / 部署 | chi + 标准库 / golang-migrate / Docker Compose | + +> 注:文档中"中国合规专项"为工程与运营层面梳理,**非法律意见**;正式上线前请咨询专业法务。 diff --git a/docs/planning/history/v0.1-initial-plan.md b/docs/planning/history/v0.1-initial-plan.md new file mode 100644 index 0000000..9f995a1 --- /dev/null +++ b/docs/planning/history/v0.1-initial-plan.md @@ -0,0 +1,216 @@ +# 商品档案公益 API 系统 — 规划方案 (v0.1) + +> 一个公益性质的网站/服务:**采集全网商品信息**,对外提供**商品参数查询 API**。 +> 核心原则:**只收集信息、只提供信息,不涉及任何购买、下单、比价导购等交易行为。** + +--- + +## 1. 项目定位与原则 + +| 维度 | 说明 | +|------|------| +| 定位 | 公益的"商品参数百科 / 商品档案库",类似商品界的 Wikipedia + 开放 API | +| 提供什么 | 商品的客观参数(规格、型号、成分、能效、尺寸、条码等) | +| **不提供什么** | 价格交易、加购物车、下单、跳转购买链接、联盟分佣、比价导购 | +| 服务对象 | 第三方软件 / 开发者,通过 API 查询商品参数 | +| 数据态度 | 客观、中立、可溯源(每条数据标注来源与采集时间) | + +> ⚠️ 关于价格:建议**默认不收录价格**。价格属于交易属性,且实时性强、争议大。如果一定要做,只做"历史参考价"且明确标注来源时间,绝不提供购买入口。**这一点需要你确认。** + +--- + +## 2. 系统总体架构 + +``` + ┌─────────────────────────────┐ + │ 数据来源 (Sources) │ + │ 官网/厂商 / 开放数据 / GS1 │ + │ 条码库 / 用户贡献 / 监管公开 │ + └──────────────┬──────────────┘ + │ + ┌──────────────────────────▼──────────────────────────┐ + │ 采集层 Ingestion (Workers) │ + │ 爬虫调度 + 适配器 + 限速 + robots 合规 + 去重 │ + └──────────────────────────┬──────────────────────────┘ + │ 原始数据 (raw) + ┌──────────────────────────▼──────────────────────────┐ + │ 清洗/标准化 ETL (Normalize & Dedup) │ + │ 字段映射 / 单位归一 / 实体匹配 / 质量评分 │ + └──────────────────────────┬──────────────────────────┘ + │ 结构化商品档案 + ┌──────────────────────────▼──────────────────────────┐ + │ 存储层 Storage │ + │ PostgreSQL(主) + 对象存储(图片) + 搜索引擎(检索) │ + └──────────────────────────┬──────────────────────────┘ + │ + ┌──────────────────────────▼──────────────────────────┐ + │ 公开 API 服务 (FastAPI) │ + │ REST/JSON + 文档 + 限流 + 缓存 + API Key(可选) │ + └──────────────────────────┬──────────────────────────┘ + │ + ┌──────────────▼──────────────┐ + │ 消费者:各种软件/开发者 │ + └─────────────────────────────┘ +``` + +分为四个相对独立的子系统: +1. **采集子系统**(爬虫/适配器,离线运行) +2. **数据处理子系统**(清洗、标准化、去重、质量评分) +3. **存储子系统**(关系库 + 搜索 + 对象存储) +4. **API 子系统**(对外只读公开 API + 文档站) + +--- + +## 3. 核心数据模型(商品档案 Schema) + +商品的本质是"一个实体 + 一组可扩展的参数"。建议采用 **核心字段 + 灵活属性(KV)** 的混合模型,以适配不同品类(手机、食品、家电、化妆品……参数差异极大)。 + +### 3.1 核心实体 + +```jsonc +// Product 商品档案 +{ + "id": "uuid", // 内部唯一ID + "gtin": "6901234567892", // 全球贸易项目代码(条码), 可空 + "name": "示例牌 1.5L 纯净水", + "brand": "示例牌", // -> Brand 实体 + "manufacturer": "示例食品有限公司", + "category": "饮料/包装水", // -> Category 树 + "model": "型号/SKU标识", + "description": "客观描述, 非营销文案", + "images": ["对象存储URL", ...], + "attributes": [ // 灵活参数(见下) + {"key": "容量", "value": "1.5", "unit": "L"}, + {"key": "保质期", "value": "12", "unit": "月"} + ], + "identifiers": { // 其他标识 + "ean": "...", "upc": "...", "asin": "...", "mpn": "..." + }, + "sources": [ // 数据溯源(每个字段可标来源) + {"source_id": "...", "url": "...", "fetched_at": "2026-06-08T...", "field": "容量"} + ], + "quality_score": 0.87, // 数据质量/可信度评分 + "status": "active|merged|deprecated", + "created_at": "...", "updated_at": "..." +} +``` + +### 3.2 灵活属性 (EAV / JSONB) +- 不同品类参数差异巨大,核心表存通用字段,品类专属参数存 `attributes`(PostgreSQL `JSONB`,可建 GIN 索引)。 +- 配合**品类参数模板**(Category Schema)约束某品类应有哪些参数,保证质量。 + +### 3.3 辅助实体 +- `Brand`(品牌)、`Manufacturer`(厂商)、`Category`(品类树)、`Source`(数据来源登记)、`AttributeDefinition`(参数字典:标准名/别名/单位)。 +- 实体去重/合并需要 `merge` 机制(同一商品多来源 → 合并为一条,保留溯源)。 + +--- + +## 4. 数据采集策略(最关键、也最需合规) + +### 4.1 来源优先级(从"最合规"到"需谨慎") +1. **官方开放数据 / 标准库**:GS1 条码库、各国监管公开数据(能效标识、食品备案、药品/化妆品备案等)。✅ 最佳 +2. **厂商官网 / 官方规格表**:参数最权威。需遵守 robots.txt。 +3. **厂商/平台开放 API**:若有官方 API 走 API。 +4. **用户/社区贡献**:众包补全与纠错(带审核)。 +5. **第三方网页抓取**:⚠️ 合规风险最高,需严格遵守 robots、限速、只取客观参数、标注来源。 + +### 4.2 采集器设计 +- **适配器模式**:每个来源一个 adapter(解析规则独立、可热插拔)。 +- **调度**:任务队列(Celery / RQ / arq)+ 定时(cron)+ 增量更新。 +- **合规护栏**:尊重 `robots.txt`、礼貌限速、`User-Agent` 标识身份、错峰、缓存避免重复抓取。 +- **去重与匹配**:以 GTIN/条码为主键,无条码时用 (品牌+型号+关键参数) 做模糊匹配。 + +### 4.3 数据质量 +- 每个字段记录来源 + 时间;多来源冲突时按来源可信度加权。 +- 质量评分 `quality_score`:字段完整度 + 来源权威度 + 一致性。 + +--- + +## 5. 公开 API 设计(只读、RESTful) + +基础原则:**只读、无副作用、无购买入口、稳定版本化、有文档**。 + +``` +GET /api/v1/products/{id} # 按内部ID查询商品档案 +GET /api/v1/products/barcode/{gtin} # 按条码(GTIN/EAN/UPC)查询 ★最常用 +GET /api/v1/products/search # 搜索: ?q=&brand=&category=&page=&size= +GET /api/v1/products/{id}/attributes # 仅取参数 +GET /api/v1/brands / categories # 品牌/品类树 +GET /api/v1/sources/{id} # 数据来源说明(透明溯源) +GET /healthz / /api/v1/openapi.json # 健康检查 / 机读文档 +``` + +设计要点: +- **版本化** `/api/v1/`,破坏性变更升 `v2`。 +- **分页 + 字段筛选**(`fields=` 减少传输)。 +- **限流**:匿名按 IP 限流;可选 API Key 提升配额(免费,仅用于防滥用与统计)。 +- **缓存**:CDN + 服务端缓存(商品参数变化慢,缓存命中率高)。 +- **响应统一**:JSON,含 `data` / `meta`(分页) / `sources`(溯源)。 +- **开放协议**:数据采用开放许可(如 CC BY / ODbL),鼓励署名引用。 +- **自动文档**:FastAPI 自带 Swagger UI / ReDoc。 + +--- + +## 6. 技术选型建议 + +| 层 | 选型 | 理由 | +|----|------|------| +| API 框架 | **Python + FastAPI** | 与仓库定位一致、异步性能好、自带 OpenAPI 文档 | +| 主数据库 | **PostgreSQL** (JSONB) | 关系 + 灵活属性兼得,GIN 索引支持检索 | +| 搜索 | **OpenSearch / Elasticsearch / 或 PG 全文** | 商品名/参数全文与分面检索 | +| 缓存 | **Redis** | 热点缓存 + 限流计数 + 任务队列后端 | +| 采集任务 | **arq / Celery / RQ** | 异步调度爬虫与 ETL | +| 爬虫 | **httpx + selectolax/BeautifulSoup**,动态页用 **Playwright** | 轻量为主,必要时浏览器渲染 | +| 对象存储 | **S3 兼容 (MinIO / 云)** | 存商品图片 | +| 部署 | **Docker + Compose**(初期) → K8s(规模化) | 渐进式 | +| 文档站 | FastAPI 文档 + 静态站(MkDocs) | 开发者文档 | + +> 如果你更偏好 Node.js / Go 也可以,我按你的偏好调整。仓库描述像是 FastAPI,所以我默认 Python。 + +--- + +## 7. 合规与法律(公益项目尤其重要) + +- **爬取合规**:遵守 robots.txt、服务条款、合理限速;只采集**客观商品参数**,不抓取受版权保护的营销文案/评测原文(可链接来源)。 +- **数据来源透明**:每条数据可溯源,标注来源与时间,尊重原始来源。 +- **隐私**:只处理商品信息,不涉及个人数据(无 PII)。 +- **商标/品牌**:品牌名仅用于客观标识商品,不做背书或贬损。 +- **明确边界**:网站显著声明"仅提供信息、不提供购买、不构成消费建议"。 +- **数据开放许可**:选择 CC BY 4.0 或 ODbL,明确他人使用条款。 +- **下架机制**:提供来源方/权利方的纠错与下架联系渠道。 + +> 建议这块后续找法务/合规确认,我可以先把"合规护栏"写进采集器与站点声明。 + +--- + +## 8. 建议的实施路线图(分阶段,先跑起来再扩展) + +**阶段 0 — 地基(先做)** +- 仓库工程化:FastAPI 项目骨架、PostgreSQL、Docker Compose、CI、代码规范。 +- 定义核心数据模型 + 迁移。 + +**阶段 1 — MVP API** +- 实现只读 API:按条码/ID 查询 + 搜索 + 自动文档。 +- 导入一批种子数据(用开放数据/手工录入几百条),让 API 真正可用。 + +**阶段 2 — 采集管线** +- 1~2 个合规来源的 adapter + 调度 + 清洗/去重/入库。 +- 质量评分与溯源。 + +**阶段 3 — 规模化与开放** +- 搜索引擎、缓存/CDN、限流、API Key、众包贡献与审核后台。 +- 开放数据许可与开发者文档站上线。 + +--- + +## 9. 需要你拍板的关键问题 + +1. **是否收录价格?**(建议默认不收录,只做客观参数) +2. **首批聚焦哪些品类?**(如:食品/快消、3C 数码、家电…… 建议先聚焦 1~2 个品类把模型打磨好) +3. **技术栈确认 Python + FastAPI 吗?**(还是有其他偏好) +4. **数据来源**:你手上有没有已知的合规数据源 / 官方 API / 已有数据集? +5. **首要交付**:先搭"可查询的 MVP API + 一批种子数据",还是先做"采集管线"?(建议先 MVP API) + +--- + +> 这是 v0.1 规划草案,确认大方向后我会细化成可执行的工程任务,并开始搭建仓库骨架 + MVP。 diff --git a/docs/planning/history/v0.2-go-python-foodfmcg.md b/docs/planning/history/v0.2-go-python-foodfmcg.md new file mode 100644 index 0000000..4284243 --- /dev/null +++ b/docs/planning/history/v0.2-go-python-foodfmcg.md @@ -0,0 +1,203 @@ +# 商品档案公益 API 系统 — 规划方案 (v0.2) + +> 公益网站/服务:**采集全网商品信息**,对外提供**商品参数查询 API**。 +> 原则:**只收集 + 只提供信息,不涉及任何购买/下单/比价导购**。 +> 本版根据你的反馈定稿四件事:① 收录**官方标准零售价(MSRP)** ② 首批聚焦**食品快消** ③ **Go(系统) + Python(采集)** 多语言架构 ④ 附**开放数据源清单**。 + +--- + +## 0. 你已确认的决策 + +| # | 决策 | 说明 | +|---|------|------| +| 1 | **价格 = 官方标准零售价 (MSRP)** | 厂商指导价/官方建议零售价,属**静态属性**,带来源+时间+币种标注;**不收录实时电商售价、不提供购买入口** | +| 2 | **首批品类 = 食品快消 (Food & FMCG)** | 先把食品的数据模型打磨好(成分、营养、过敏原、规格、保质期…) | +| 3 | **技术栈 = Go + Python** | Go 写对外 API/核心服务;Python 写采集/ETL/爬虫;通过 PostgreSQL + 消息队列解耦 | +| 4 | **数据源 = 暂无,后期提供** | 本版先给出可立即接入的开放数据源清单 | + +--- + +## 1. Go + Python 多语言架构(核心) + +这是一个很经典且合理的组合。两端**不直接互相调用**,而是通过**共享数据库 + 消息队列**解耦,各自独立部署、独立扩展。 + +``` + ┌────────────────────── Python 侧 (采集/数据) ──────────────────────┐ + │ │ + 数据源 ─▶│ 采集 Workers (爬虫/适配器) ─▶ ETL 清洗/标准化/去重 ─▶ 入库 │ + │ httpx / Playwright / scrapy pandas / 规则引擎 │ + └───────────────────────────┬────────────────────────────────────────┘ + │ 写入 + ┌───────▼────────┐ ┌──────────────┐ + │ PostgreSQL │◀──────▶│ 对象存储 S3 │ (商品图) + │ (商品档案主库) │ └──────────────┘ + └───────▲────────┘ + │ 只读 + ┌───────────────────────────┴────────────────────────────────────────┐ + │ Go 侧 (对外服务) │ + │ 公开 API (REST/JSON) + Redis 缓存/限流 + 搜索网关 + OpenAPI │ + │ Gin/Echo/Chi/标准库 │ + └───────────────────────────┬────────────────────────────────────────┘ + │ + 各种软件 / 开发者消费 +``` + +### 1.1 职责划分 + +| 子系统 | 语言 | 职责 | +|--------|------|------| +| **公开 API 服务** | **Go** | 对外只读 API、限流、缓存、鉴权(可选 API Key)、检索网关、高并发承载 | +| **采集 Workers** | **Python** | 每个数据源一个 adapter,抓取/调用 API、遵守 robots、限速、产出原始数据 | +| **ETL / 数据处理** | **Python** | 清洗、字段映射、单位归一、实体去重与合并、质量评分 | +| **调度 / 队列** | Python(worker) + Redis/消息队列 | 定时任务、增量更新、任务分发 | +| **存储** | PostgreSQL + Redis + S3 | 主库 / 缓存+限流 / 图片 | +| **检索** | 初期 PG 全文 → 后期 OpenSearch | 商品名/参数搜索与分面 | + +### 1.2 为什么这样分? +- **Go 做 API**:编译型、单二进制部署、并发模型适合高 QPS 的只读公益 API,运维简单。 +- **Python 做采集**:爬虫/解析/数据处理生态最强(scrapy、playwright、pandas),迭代快。 +- **解耦点 = 数据库**:Go 端**只读**主库(或读副本),Python 端负责写入。两端通过稳定的表结构约定协作,互不阻塞;将来任一端换语言/重写都不影响另一端。 +- **契约**:用数据库 schema + 一份内部「数据契约文档」固定字段含义,避免两端理解不一致。 + +--- + +## 2. 食品快消数据模型(细化) + +食品参数差异大,沿用 **核心字段 + JSONB 灵活属性 + 营养结构化子表**。字段设计大量参考 Open Food Facts(成熟的食品开放库)。 + +### 2.1 商品主表 `product` +```jsonc +{ + "id": "uuid", + "gtin": "6901234567892", // 条码(主键标识), EAN-13/UPC/EAN-8 + "name": "示例牌 巧克力榛子酱 400g", + "brand": "示例牌", // -> brand + "manufacturer": "示例食品有限公司", // 生产商 + "category": "食品/酱料/巧克力酱", // -> category 树 (可对齐 GS1 GPC / OFF categories) + "net_content": {"value": 400, "unit": "g"}, // 净含量 + "country_of_origin": "中国", + "shelf_life": {"value": 12, "unit": "月"}, // 保质期 + "storage": "常温避光保存", + "images": ["S3_URL", ...], + "msrp": { ... }, // 官方标准零售价, 见 2.3 + "food": { ... }, // 食品专属结构化字段, 见 2.2 + "attributes": [ {"key":"","value":"","unit":""} ], // 其余灵活参数(JSONB) + "identifiers": {"ean":"", "upc":"", "off_id":""}, + "sources": [ {"source":"", "url":"", "fetched_at":"", "fields":["msrp"]} ], + "quality_score": 0.0, + "status": "active|merged|deprecated", + "created_at": "", "updated_at": "" +} +``` + +### 2.2 食品专属字段 `food`(结构化) +```jsonc +{ + "ingredients_text": "白砂糖, 棕榈油, 榛子(13%), ...", // 配料表原文 + "ingredients": [ {"name":"白砂糖","rank":1}, ... ], // 解析后(可选) + "allergens": ["坚果", "大豆", "乳"], // 过敏原 + "additives": ["E322 卵磷脂"], // 添加剂 + "nutriments": { // 营养成分(每100g/100ml) + "energy_kj": 2252, "energy_kcal": 539, + "fat_g": 30.9, "saturated_fat_g": 10.6, + "carbohydrates_g": 57.5, "sugars_g": 56.3, + "protein_g": 6.3, "salt_g": 0.107 + }, + "nutrition_basis": "per_100g", // per_100g | per_100ml | per_serving + "serving_size": "15g", + "is_vegetarian": null, "is_vegan": null, // 可空 + "nutri_score": "C", // 若引用 OFF + "labels": ["无添加", "清真"] // 认证/标签 +} +``` + +### 2.3 官方标准零售价 `msrp`(重点) +```jsonc +{ + "amount": 29.90, + "currency": "CNY", + "type": "msrp", // 仅 msrp/官方指导价; 不存实时电商成交价 + "region": "CN", // 适用地区(价格随地区不同) + "source": "厂商官网/官方价目表", + "source_url": "https://...", + "effective_date": "2026-01-01", // 价格生效/采集时间 + "note": "官方建议零售价, 实际售价以零售商为准; 本站不提供购买" +} +``` +> 设计要点:价格是**带时间戳的历史快照**而非实时报价;明确 `type=msrp`、标注地区与来源;响应里附免责说明。**坚决不出现购买/跳转链接。** + +### 2.4 辅助实体 +`brand` / `manufacturer` / `category`(品类树) / `source`(数据来源登记) / `attribute_definition`(参数字典: 标准名·别名·单位) / `merge_log`(实体合并记录, 保留溯源)。 + +--- + +## 3. 可立即接入的开放数据源清单(食品快消) + +按"合规性 / 可用性"排序。这些可作为**种子数据 + 采集 adapter 的首批对象**。 + +| 数据源 | 内容 | 许可 | 接入方式 | 备注 | +|--------|------|------|----------|------| +| **Open Food Facts** ⭐ | 全球食品(成分/营养/过敏原/Nutri-Score/图片) | **ODbL**(数据)+DbCL+CC-BY-SA(图) | REST API + **每夜全量 dump**(CSV/MongoDB, ~9GB) | 食品首选;可贡献回写;限速 15 req/min/IP(读) | +| **USDA FoodData Central** ⭐ | 美国食品营养成分(含 Branded 品牌库) | **CC0(公共领域)** | REST API(需免费 key) + JSON/CSV 下载 | 营养数据权威;商业可用 | +| **GS1 / Verified by GS1**(中国商品信息服务平台) | 条码→品牌/规格/厂商(官方登记) | 受限(需企业/接口授权) | 网页查询 + API(≤1000 GTIN/次) | **条码→商品**最权威来源;2亿+条;中国数据首选 | +| **brocade.io** | 开放 GTIN/条码产品库 | 开源/开放 | 免费 REST(免鉴权读) | 数据量有限,可作补充 | +| **3023data 等条码接口** | 中国物品编码+UPC+ISBN | 商业(0.005~0.02元/次) | REST API | **付费**,作兜底补全,非首选 | +| 各国**监管公开数据** | 食品备案/标签/能效等 | 多为公开 | 各平台 | 后续按需逐个评估合规 | + +**参考用开源项目(架构/数据模型借鉴,非数据源)**: +- Open Food Facts Server (Product Opener) — 食品库的完整实现,可学其字段与流程 +- UnoPIM / PCMT / brocade.io — 开源 PIM / 商品主数据系统,借鉴建模与去重 + +> 建议:**先用 Open Food Facts 全量 dump 作种子数据**(直接有海量真实食品),再用 GS1/USDA 做补全与校验。这样 MVP 阶段就有真实可查的数据。 + +--- + +## 4. 公开 API 契约(Go 实现,只读) + +``` +GET /api/v1/products/barcode/{gtin} # ★最常用: 条码查档案 +GET /api/v1/products/{id} # 内部ID查 +GET /api/v1/products/search # ?q=&brand=&category=&allergen_free=&page=&size=&fields= +GET /api/v1/products/{id}/nutriments # 仅营养 +GET /api/v1/products/{id}/msrp # 仅官方零售价(含来源/时间/免责) +GET /api/v1/brands | /categories # 品牌 / 品类树 +GET /api/v1/sources/{id} # 数据来源透明说明 +GET /healthz | /api/v1/openapi.json # 健康检查 / 机读文档 +``` +约定:版本化 `/v1/`;统一响应 `{data, meta(分页), sources(溯源)}`;分页 + `fields=` 裁剪;匿名按 IP 限流,可选免费 API Key 提配额;CDN+Redis 缓存(参数变化慢,命中率高);数据采用开放许可(CC BY / ODbL,注意 OFF 的 ODbL 传染性);**无任何购买/交易端点**。 + +--- + +## 5. 合规与边界(公益项目重点) + +- **数据源许可要分清**:OFF 是 **ODbL**(衍生数据库需同样开放+署名),USDA 是 **CC0**(最宽松)。混用时要按最严格许可对外标注,避免许可冲突。 +- 爬取守 robots.txt / 服务条款,礼貌限速,标明 User-Agent 身份。 +- 只采**客观参数**;营销文案/评测原文不照搬(链接来源即可)。 +- 无个人数据(PII),只处理商品信息。 +- 站点显著声明:**仅提供信息、不提供购买、不构成消费建议**;价格为官方指导价历史快照。 +- 提供权利方**纠错/下架**联系渠道。 + +--- + +## 6. 里程碑(仍不写代码,仅规划,供确认) + +| 阶段 | 目标 | 关键产出 | +|------|------|----------| +| **M0 工程地基** | 仓库骨架 | Go API 骨架 + Python 采集骨架 + PostgreSQL + Docker Compose + CI + 数据契约文档 | +| **M1 数据模型** | 食品 schema | 主表/食品字段/MSRP/辅助实体 的迁移与字典 | +| **M2 种子数据** | 有真实数据 | 导入 Open Food Facts dump(食品子集) + USDA 营养补全 | +| **M3 MVP API (Go)** | 可查询 | 条码/ID/搜索/营养/MSRP + OpenAPI 文档 + 限流缓存 | +| **M4 采集管线 (Python)** | 自动更新 | 1~2 个 adapter(OFF API / GS1) + ETL + 去重 + 质量评分 + 调度 | +| **M5 开放与规模化** | 上线 | 搜索引擎 + CDN + API Key + 众包纠错后台 + 开发者文档站 + 开放数据许可 | + +--- + +## 7. 待你确认/补充 + +1. **价格范围**:确认只收「官方指导价 (MSRP)」、不碰实时电商价?(建议是) +2. **OFF 的 ODbL 许可**:可接受(意味着我们对外的数据库也要用 ODbL 并署名 OFF)?还是更想用 CC0 来源(USDA)为主以保持宽松? +3. **种子数据**:同意先导入 Open Food Facts 食品 dump 作为启动数据吗? +4. **Go Web 框架偏好**:Gin / Echo / Chi / 标准库 net/http,有偏好吗?(无偏好我默认 Chi 或标准库,轻量) +5. **地域范围**:首批面向中国市场商品,还是中外都收?(影响优先用 GS1-China 还是 OFF 全球库) + +> 你确认后,我把它定为 v1.0 规划,并据此拆成可执行的工程任务清单(仍按你的节奏,需要我动手写代码时再开始)。 diff --git a/docs/planning/history/v1.0-locked-decisions.md b/docs/planning/history/v1.0-locked-decisions.md new file mode 100644 index 0000000..49eaf1c --- /dev/null +++ b/docs/planning/history/v1.0-locked-decisions.md @@ -0,0 +1,107 @@ +# 商品档案公益 API 系统 — 规划方案 (v1.0 定稿) + +> 公益网站/服务:采集全网商品信息,提供商品参数查询 API。**只收集 + 只提供信息,不涉及任何购买行为。** + +--- + +## 1. 已锁定的决策 + +| # | 决策 | 结论 | +|---|------|------| +| 1 | 品类 | 首批 **食品快消** | +| 2 | 价格 | 只收 **官方标准零售价 (MSRP)**,静态字段,带来源/时间/地区/币种 + 免责说明;**不收实时电商价、不提供购买入口** | +| 3 | 技术栈 | **Go**(对外API/核心服务) + **Python**(采集/ETL/爬虫),经 PostgreSQL + Redis/队列解耦 | +| 4 | 种子数据 | ✅ **先导入 Open Food Facts 食品 dump**,最快拥有真实数据 | +| 5 | 数据许可 | 因采用 OFF → 对外数据库用 **ODbL** 并署名来源;CC0 来源(USDA)可自由混入 | + +### 1.1 我先用的默认值(如不同意请指出,否则按此执行) +- **Go Web 框架**:`chi` + 标准库 `net/http`(轻量、稳定、易维护)。 +- **地域范围**:先用 OFF **全球食品库**起步,后续接 **GS1-China** 补强中国市场数据。 +- **数据库迁移工具**:Go 侧用 `golang-migrate`(纯 SQL 迁移,两端共享同一套 schema)。 +- **部署**:初期 Docker Compose 一键起全套(Postgres/Redis/Go API/Python worker)。 + +--- + +## 2. 目标架构(定稿) + +``` + 数据源(OFF dump / OFF API / USDA / GS1) + │ + ▼ Python: 采集 adapters → ETL(清洗/归一/去重/质量评分) + │ + ┌────▼─────────┐ 图片 ┌──────────┐ + │ PostgreSQL │◀───────▶│ S3/MinIO │ + │ (商品档案主库)│ └──────────┘ + └────▲─────────┘ + │ 只读 (+Redis缓存/限流) + ▼ Go: 公开 REST API + OpenAPI 文档 + 各种软件 / 开发者 +``` + +- **解耦契约**:两端通过共享 PostgreSQL schema + 一份《数据契约文档》协作,互不直接调用。 +- **Go 端只读主库**(或读副本);**Python 端负责写入**。 + +--- + +## 3. 仓库结构(计划,写代码时落地) + +``` +goods/ +├── README.md +├── docker-compose.yml # postgres + redis + minio + api + worker +├── docs/ +│ ├── data-contract.md # 两端共享的字段契约 +│ └── openapi.yaml # API 契约 +├── migrations/ # 共享 SQL 迁移 (golang-migrate) +├── api/ # Go: 对外只读 API +│ ├── cmd/server/main.go +│ ├── internal/{handler,store,model,middleware}/ +│ └── go.mod +└── ingestion/ # Python: 采集 + ETL + ├── pyproject.toml + ├── adapters/{openfoodfacts,usda,gs1}.py + ├── etl/{normalize,dedup,quality}.py + └── jobs/{seed_off_dump,scheduler}.py +``` + +## 4. 数据模型 & API 契约 +(沿用 v0.2:`product` 主表 + `food` 食品字段 + `msrp` 价格 + 辅助实体;API 以 `GET /products/barcode/{gtin}` 为核心,全只读、无交易端点。详见 v0.2 附件。) + +--- + +## 5. 可执行任务拆分(按里程碑,写代码时逐项落地) + +**M0 — 工程地基** +- [ ] 初始化 Go module (`api/`) + Python 项目 (`ingestion/`) +- [ ] `docker-compose.yml`:Postgres + Redis + MinIO +- [ ] CI(Go: build/vet/test;Python: ruff/pytest) +- [ ] `docs/data-contract.md` 初版 + +**M1 — 数据模型** +- [ ] `migrations/`:product / food / msrp / brand / manufacturer / category / source / attribute_definition / merge_log +- [ ] JSONB + GIN 索引;gtin 唯一索引 + +**M2 — 种子数据 (Python)** +- [ ] 下载 OFF 食品 dump(CSV) +- [ ] `seed_off_dump`:字段映射 → 入库(含营养/成分/过敏原/图片URL) +- [ ] USDA(CC0) 营养补全(可选) + +**M3 — MVP API (Go)** +- [ ] 路由 + handler:barcode / id / search / nutriments / msrp / brands / categories / sources +- [ ] 统一响应、分页、`fields=` 裁剪、错误处理 +- [ ] Redis 缓存 + IP 限流;`/healthz` + OpenAPI 文档 + +**M4 — 采集管线 (Python)** +- [ ] adapter:OFF API(增量更新)+ GS1(条码补全) +- [ ] ETL:清洗/单位归一/去重合并/质量评分/溯源 +- [ ] 调度(定时增量更新) + +**M5 — 开放与规模化** +- [ ] 搜索引擎(PG 全文 → OpenSearch)、CDN 缓存 +- [ ] 免费 API Key(防滥用+统计)、众包纠错后台 +- [ ] 开发者文档站 + 开放数据许可声明 + 站点"不提供购买"声明 + +--- + +## 6. 下一步 +规划已定稿。**你说先不写代码,所以我暂停在这里**。等你说"开始",我就从 **M0 工程地基** 动手,搭好骨架后开 PR 给你看。也可以先只做某个里程碑(比如先 M0+M1 把骨架和数据模型立起来)。