From 5fe57742513a053b6e076a8c591d5796fffb1f90 Mon Sep 17 00:00:00 2001 From: busenalbantoglu51424 Date: Thu, 25 Jun 2026 00:42:18 +0000 Subject: [PATCH] =?UTF-8?q?feat(public):=20=E5=95=86=E5=93=81=E8=AF=A6?= =?UTF-8?q?=E6=83=85=E9=A1=B5=E5=B1=95=E7=A4=BA=E5=8E=82=E5=95=86=E5=BB=BA?= =?UTF-8?q?=E8=AE=AE=E9=9B=B6=E5=94=AE=E4=BB=B7(MSRP)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 后端 ProductByID/ProductByGTIN 在商品详情 JSON 中内嵌 msrp 字段(按 effective_date 倒序,过滤 amount=0 的无效快照);公开前台详情页新增「建议零售价」展示。 Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- api/internal/handler/openapi.json | 17 ++++++++++++++++- api/internal/store/store.go | 12 ++++++++++-- public-frontend/src/components/ProductView.tsx | 17 +++++++++++++++++ public-frontend/src/types.ts | 10 ++++++++++ 4 files changed, 53 insertions(+), 3 deletions(-) diff --git a/api/internal/handler/openapi.json b/api/internal/handler/openapi.json index dd4a2e9..d2ff098 100644 --- a/api/internal/handler/openapi.json +++ b/api/internal/handler/openapi.json @@ -208,7 +208,22 @@ "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 } + "ingredients_text": { "type": "string", "nullable": true }, + "msrp": { + "type": "array", + "description": "Manufacturer suggested retail price snapshots (reference only, newest first; zero-amount entries omitted).", + "items": { + "type": "object", + "properties": { + "amount": { "type": "number" }, + "currency": { "type": "string" }, + "region": { "type": "string" }, + "effective_date": { "type": "string", "nullable": true }, + "source_url": { "type": "string", "nullable": true }, + "note": { "type": "string", "nullable": true } + } + } + } } } } diff --git a/api/internal/store/store.go b/api/internal/store/store.go index 1f31e6e..27f4d36 100644 --- a/api/internal/store/store.go +++ b/api/internal/store/store.go @@ -126,6 +126,7 @@ type Product struct { Ingredients *string `json:"ingredients_text,omitempty"` Allergens []string `json:"allergens,omitempty"` Additives []string `json:"additives,omitempty"` + MSRP []MSRP `json:"msrp,omitempty"` } // ProductBarcodes returns every barcode attached to a product, primary first. @@ -280,6 +281,9 @@ func (s *Store) ProductByGTIN(ctx context.Context, gtin string) (*Product, error if p.Specs, err = s.buildSpecs(ctx, p.ArchiveKind, attrs); err != nil { return nil, err } + if p.MSRP, err = s.ListMSRP(ctx, p.ID); err != nil { + return nil, err + } s.cacheSet(ctx, suffix+gtin, p, productCacheTTL) return p, nil } @@ -301,6 +305,9 @@ func (s *Store) ProductByID(ctx context.Context, id string) (*Product, error) { if p.Specs, err = s.buildSpecs(ctx, p.ArchiveKind, attrs); err != nil { return nil, err } + if p.MSRP, err = s.ListMSRP(ctx, p.ID); err != nil { + return nil, err + } s.cacheSet(ctx, suffix+id, p, productCacheTTL) return p, nil } @@ -445,11 +452,12 @@ type MSRP struct { Note *string `json:"note"` } -// ListMSRP returns all MSRP snapshots for a product. +// ListMSRP returns a product's suggested-retail-price snapshots, newest first. +// Zero-amount entries are excluded as they carry no price information. func (s *Store) ListMSRP(ctx context.Context, id string) ([]MSRP, error) { rows, err := s.pool.Query(ctx, `SELECT amount, currency, region, effective_date::text, source_url, note - FROM product_msrp WHERE product_id = $1 ORDER BY effective_date DESC NULLS LAST`, id) + FROM product_msrp WHERE product_id = $1 AND amount > 0 ORDER BY effective_date DESC NULLS LAST`, id) if err != nil { return nil, err } diff --git a/public-frontend/src/components/ProductView.tsx b/public-frontend/src/components/ProductView.tsx index 154eb02..c217295 100644 --- a/public-frontend/src/components/ProductView.tsx +++ b/public-frontend/src/components/ProductView.tsx @@ -42,6 +42,12 @@ export default function ProductView({ id, onBack }: { id: string; onBack: () => const nutriEntries = Object.entries(p.nutriments || {}).filter( ([, v]) => v !== null && v !== undefined, ); + const latestMsrp = (p.msrp || [])[0]; + const msrpText = latestMsrp + ? latestMsrp.currency === "CNY" + ? `¥${latestMsrp.amount.toFixed(2)}` + : `${latestMsrp.amount.toFixed(2)} ${latestMsrp.currency}` + : null; return (
@@ -93,6 +99,17 @@ export default function ProductView({ id, onBack }: { id: string; onBack: () => } /> + + {msrpText} + 厂商建议,仅供参考 + + ) : null + } + />