feat(M0): 工程地基 - Go API + Python 采集骨架 + CI + 数据契约

- api/: Go(chi) 只读 API 骨架, /healthz + 版本化路由(占位), Dockerfile, 单测
- ingestion/: Python 采集/ETL 包骨架, units 单位归一化(纯函数+测试), adapter 协议
- docker-compose.yml: postgres + redis + minio + api
- .github/workflows/ci.yml: Go build/vet/test + Python ruff/pytest
- docs/data-contract.md(两端共享契约) + docs/disclaimer.md(不提供购买声明)
- migrations/ 占位(M1 起填充)

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
2026-06-08 06:18:31 +00:00
parent 729127661e
commit 786b7d3721
22 changed files with 572 additions and 0 deletions
+68
View File
@@ -0,0 +1,68 @@
// Package handler wires up the public, read-only OpenGoods HTTP API.
//
// The OpenGoods service is a public-good product information API: it only
// collects and serves product facts. It exposes no purchase, checkout, or
// commerce endpoints by design.
package handler
import (
"encoding/json"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
// APIVersion is the current public API version prefix.
const APIVersion = "v1"
// Router builds the top-level HTTP handler with middleware and routes mounted.
func Router() http.Handler {
r := chi.NewRouter()
r.Use(middleware.RequestID)
r.Use(middleware.RealIP)
r.Use(middleware.Recoverer)
r.Get("/healthz", Healthz)
r.Route("/api/"+APIVersion, func(r chi.Router) {
r.Route("/products", func(r chi.Router) {
r.Get("/barcode/{gtin}", notImplemented)
r.Get("/search", notImplemented)
r.Get("/{id}", notImplemented)
r.Get("/{id}/nutriments", notImplemented)
r.Get("/{id}/msrp", notImplemented)
})
r.Get("/brands", notImplemented)
r.Get("/categories", notImplemented)
r.Get("/sources/{id}", notImplemented)
})
return r
}
// Healthz reports liveness of the service.
func Healthz(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}
// notImplemented is a placeholder for endpoints scoped to later milestones.
func notImplemented(w http.ResponseWriter, r *http.Request) {
writeError(w, r, http.StatusNotImplemented, "not_implemented", "endpoint not implemented yet")
}
func writeJSON(w http.ResponseWriter, status int, body any) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(body)
}
func writeError(w http.ResponseWriter, r *http.Request, status int, code, message string) {
writeJSON(w, status, map[string]any{
"error": map[string]string{
"code": code,
"message": message,
"request_id": middleware.GetReqID(r.Context()),
},
})
}
+38
View File
@@ -0,0 +1,38 @@
package handler
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
)
func TestHealthz(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/healthz", nil)
rec := httptest.NewRecorder()
Router().ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected status %d, got %d", http.StatusOK, rec.Code)
}
var body map[string]string
if err := json.NewDecoder(rec.Body).Decode(&body); err != nil {
t.Fatalf("failed to decode body: %v", err)
}
if body["status"] != "ok" {
t.Fatalf("expected status ok, got %q", body["status"])
}
}
func TestProductEndpointNotImplemented(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/api/"+APIVersion+"/products/barcode/3017624010701", nil)
rec := httptest.NewRecorder()
Router().ServeHTTP(rec, req)
if rec.Code != http.StatusNotImplemented {
t.Fatalf("expected status %d, got %d", http.StatusNotImplemented, rec.Code)
}
}