Files
goods/api/internal/handler/openapi.json
T
sulaimaannaasif6866 7434e5195e
CI / Go (api) (pull_request) Successful in 15s
CI / Python (ingestion) (pull_request) Successful in 10s
CI / Migrations (postgres) (pull_request) Successful in 16s
feat(api): tiered cumulative quota + self-service registration
Anonymous callers get a free cumulative quota (1000 calls per IP); once
exhausted they get 403 quota_exhausted and must register. Public users can
self-register (email+password) to obtain a higher-quota API key, view usage,
and regenerate the key. Quota counters live in Redis; the public API stays
read-only except for the registration writes.

- migration 0011: app_user table + api_key.quota_total + 'registered' tier
- ratelimit: IncrTotal/TotalUsed/CopyTotal lifetime counters
- middleware: enforce cumulative quota + X-Quota-* headers
- store: RegisterUser/Authenticate/RegenerateKey (bcrypt)
- handlers: POST /api/v1/register, /account, /account/regenerate
- admin: quota_total column + registered tier
- public: 'API 密钥' account page + API docs quota section

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-06-21 07:26:41 +00:00

217 lines
10 KiB
JSON

{
"openapi": "3.0.3",
"info": {
"title": "OpenGoods / 天工商品档案公共仓 API",
"version": "1.0.0",
"description": "Public, read-only product-facts REST API. Anonymous access is allowed at a lower per-minute rate; an optional API key grants a higher rate limit and attributes usage. No purchase or commerce endpoints by design.",
"license": { "name": "Data under each source's license (e.g. ODbL)" }
},
"servers": [{ "url": "https://goods.tangshasha.com/api/v1" }],
"tags": [
{ "name": "products" },
{ "name": "catalog" },
{ "name": "meta" },
{ "name": "account" }
],
"security": [{ "ApiKeyHeader": [] }, { "BearerKey": [] }, {}],
"paths": {
"/products/search": {
"get": {
"tags": ["products"],
"summary": "Search products",
"description": "Trigram-fuzzy name search (typo-tolerant) with optional category/brand/country filters, ranked by name similarity blended with data quality_score.",
"parameters": [
{ "name": "q", "in": "query", "schema": { "type": "string" }, "description": "Keyword (name or barcode); fuzzy-matched. Empty returns all, ordered by quality_score." },
{ "name": "category", "in": "query", "schema": { "type": "string" }, "description": "Category code (matches the subtree), e.g. food.beverages." },
{ "name": "brand", "in": "query", "schema": { "type": "string" }, "description": "Brand name (fuzzy)." },
{ "name": "country", "in": "query", "schema": { "type": "string" }, "description": "Country of origin (case-insensitive prefix)." },
{ "name": "page", "in": "query", "schema": { "type": "integer", "default": 1, "minimum": 1 } },
{ "name": "size", "in": "query", "schema": { "type": "integer", "default": 20, "maximum": 100 } }
],
"responses": {
"200": {
"description": "Paged search results.",
"headers": {
"X-RateLimit-Limit": { "schema": { "type": "integer" }, "description": "Max requests in the current window." },
"X-RateLimit-Remaining": { "schema": { "type": "integer" }, "description": "Remaining requests in the window." },
"X-RateLimit-Reset": { "schema": { "type": "integer" }, "description": "Unix timestamp when the window resets." }
},
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"items": { "type": "array", "items": { "$ref": "#/components/schemas/ProductSummary" } },
"page": { "type": "integer" },
"size": { "type": "integer" },
"total": { "type": "integer" }
}
}
}
}
},
"401": { "$ref": "#/components/responses/InvalidApiKey" },
"429": { "$ref": "#/components/responses/RateLimited" }
}
}
},
"/products/barcode/{gtin}": {
"get": {
"tags": ["products"],
"summary": "Get product by barcode (GTIN)",
"parameters": [{ "name": "gtin", "in": "path", "required": true, "schema": { "type": "string" } }],
"responses": {
"200": { "description": "Product", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Product" } } } },
"404": { "$ref": "#/components/responses/NotFound" },
"429": { "$ref": "#/components/responses/RateLimited" }
}
}
},
"/products/{id}": {
"get": {
"tags": ["products"],
"summary": "Get product detail by UUID",
"parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }],
"responses": {
"200": { "description": "Product", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Product" } } } },
"404": { "$ref": "#/components/responses/NotFound" }
}
}
},
"/products/{id}/nutriments": {
"get": {
"tags": ["products"],
"summary": "Get product nutriments",
"parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }],
"responses": { "200": { "description": "Nutriments" }, "404": { "$ref": "#/components/responses/NotFound" } }
}
},
"/products/{id}/msrp": {
"get": {
"tags": ["products"],
"summary": "Get manufacturer suggested retail price snapshots (reference only)",
"parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }],
"responses": { "200": { "description": "MSRP snapshots" } }
}
},
"/brands": {
"get": {
"tags": ["catalog"],
"summary": "List brands",
"parameters": [
{ "name": "page", "in": "query", "schema": { "type": "integer", "default": 1 } },
{ "name": "size", "in": "query", "schema": { "type": "integer", "default": 20, "maximum": 100 } }
],
"responses": { "200": { "description": "Paged brands" } }
}
},
"/categories": {
"get": { "tags": ["catalog"], "summary": "List the category tree", "responses": { "200": { "description": "Category tree" } } }
},
"/sources/{id}": {
"get": {
"tags": ["meta"],
"summary": "Get a data source",
"parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }],
"responses": { "200": { "description": "Source" }, "404": { "$ref": "#/components/responses/NotFound" } }
}
},
"/register": {
"post": {
"tags": ["account"],
"summary": "Register an account and issue an API key",
"description": "Self-service registration; returns the plaintext API key exactly once.",
"security": [],
"requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["email", "password"], "properties": { "email": { "type": "string", "format": "email" }, "password": { "type": "string", "minLength": 8 } } } } } },
"responses": { "201": { "description": "Account created; plaintext key returned once" }, "400": { "description": "Invalid email or weak password" }, "409": { "description": "Email already registered" } }
}
},
"/account": {
"post": {
"tags": ["account"],
"summary": "View account key metadata and cumulative quota usage",
"security": [],
"requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["email", "password"], "properties": { "email": { "type": "string", "format": "email" }, "password": { "type": "string" } } } } } },
"responses": { "200": { "description": "Account info with quota usage" }, "401": { "description": "Invalid credentials" } }
}
},
"/account/regenerate": {
"post": {
"tags": ["account"],
"summary": "Revoke the current key and issue a new one",
"description": "Cumulative usage carries over; returns the plaintext key exactly once.",
"security": [],
"requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["email", "password"], "properties": { "email": { "type": "string", "format": "email" }, "password": { "type": "string" } } } } } },
"responses": { "200": { "description": "New plaintext key returned once" }, "401": { "description": "Invalid credentials" } }
}
}
},
"components": {
"securitySchemes": {
"ApiKeyHeader": { "type": "apiKey", "in": "header", "name": "X-API-Key", "description": "API key, e.g. og_live_xxx. Optional." },
"BearerKey": { "type": "http", "scheme": "bearer", "description": "Authorization: Bearer og_live_xxx. Optional." }
},
"responses": {
"NotFound": {
"description": "Resource not found.",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
},
"RateLimited": {
"description": "Rate limit exceeded.",
"headers": { "Retry-After": { "schema": { "type": "integer" }, "description": "Seconds to wait before retrying." } },
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
},
"InvalidApiKey": {
"description": "API key invalid or revoked.",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
}
},
"schemas": {
"Error": {
"type": "object",
"properties": {
"error": {
"type": "object",
"properties": {
"code": { "type": "string" },
"message": { "type": "string" },
"request_id": { "type": "string" }
}
}
}
},
"ProductSummary": {
"type": "object",
"properties": {
"id": { "type": "string", "format": "uuid" },
"gtin": { "type": "string", "nullable": true },
"name": { "type": "string" },
"brand": { "type": "string", "nullable": true },
"category_path": { "type": "string", "nullable": true },
"country_of_origin": { "type": "string", "nullable": true },
"quality_score": { "type": "number", "format": "float" },
"score": { "type": "number", "format": "float", "nullable": true, "description": "Relevance (name word-similarity) when q is provided; null otherwise." }
}
},
"Product": {
"type": "object",
"properties": {
"id": { "type": "string", "format": "uuid" },
"gtin": { "type": "string", "nullable": true },
"name": { "type": "string" },
"brand": { "type": "string", "nullable": true },
"category_path": { "type": "string", "nullable": true },
"net_content_value": { "type": "number", "nullable": true },
"net_content_unit": { "type": "string", "nullable": true },
"country_of_origin": { "type": "string", "nullable": true },
"quality_score": { "type": "number", "format": "float" },
"nutriments": { "type": "object", "additionalProperties": true, "nullable": true },
"nutrition_basis": { "type": "string", "nullable": true },
"nutri_score": { "type": "string", "nullable": true },
"ingredients_text": { "type": "string", "nullable": true }
}
}
}
}
}