Compare commits
2 Commits
0da57a2fb3
...
856b81125c
| Author | SHA1 | Date | |
|---|---|---|---|
| 856b81125c | |||
| 5fe5774251 |
@@ -208,7 +208,22 @@
|
|||||||
"nutriments": { "type": "object", "additionalProperties": true, "nullable": true },
|
"nutriments": { "type": "object", "additionalProperties": true, "nullable": true },
|
||||||
"nutrition_basis": { "type": "string", "nullable": true },
|
"nutrition_basis": { "type": "string", "nullable": true },
|
||||||
"nutri_score": { "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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,6 +126,7 @@ type Product struct {
|
|||||||
Ingredients *string `json:"ingredients_text,omitempty"`
|
Ingredients *string `json:"ingredients_text,omitempty"`
|
||||||
Allergens []string `json:"allergens,omitempty"`
|
Allergens []string `json:"allergens,omitempty"`
|
||||||
Additives []string `json:"additives,omitempty"`
|
Additives []string `json:"additives,omitempty"`
|
||||||
|
MSRP []MSRP `json:"msrp,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProductBarcodes returns every barcode attached to a product, primary first.
|
// 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 {
|
if p.Specs, err = s.buildSpecs(ctx, p.ArchiveKind, attrs); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if p.MSRP, err = s.ListMSRP(ctx, p.ID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
s.cacheSet(ctx, suffix+gtin, p, productCacheTTL)
|
s.cacheSet(ctx, suffix+gtin, p, productCacheTTL)
|
||||||
return p, nil
|
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 {
|
if p.Specs, err = s.buildSpecs(ctx, p.ArchiveKind, attrs); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if p.MSRP, err = s.ListMSRP(ctx, p.ID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
s.cacheSet(ctx, suffix+id, p, productCacheTTL)
|
s.cacheSet(ctx, suffix+id, p, productCacheTTL)
|
||||||
return p, nil
|
return p, nil
|
||||||
}
|
}
|
||||||
@@ -445,11 +452,12 @@ type MSRP struct {
|
|||||||
Note *string `json:"note"`
|
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) {
|
func (s *Store) ListMSRP(ctx context.Context, id string) ([]MSRP, error) {
|
||||||
rows, err := s.pool.Query(ctx,
|
rows, err := s.pool.Query(ctx,
|
||||||
`SELECT amount, currency, region, effective_date::text, source_url, note
|
`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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,12 @@ export default function ProductView({ id, onBack }: { id: string; onBack: () =>
|
|||||||
const nutriEntries = Object.entries(p.nutriments || {}).filter(
|
const nutriEntries = Object.entries(p.nutriments || {}).filter(
|
||||||
([, v]) => v !== null && v !== undefined,
|
([, 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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -93,6 +99,17 @@ export default function ProductView({ id, onBack }: { id: string; onBack: () =>
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Row label="产地" value={p.country_of_origin} />
|
<Row label="产地" value={p.country_of_origin} />
|
||||||
|
<Row
|
||||||
|
label="建议零售价"
|
||||||
|
value={
|
||||||
|
msrpText ? (
|
||||||
|
<span className="inline-flex items-baseline gap-2">
|
||||||
|
<span className="font-medium text-gray-900">{msrpText}</span>
|
||||||
|
<span className="text-xs text-gray-400">厂商建议,仅供参考</span>
|
||||||
|
</span>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Row label="Nutri-Score" value={p.nutri_score} />
|
<Row label="Nutri-Score" value={p.nutri_score} />
|
||||||
<Row label="配料" value={p.ingredients_text} />
|
<Row label="配料" value={p.ingredients_text} />
|
||||||
<Row
|
<Row
|
||||||
|
|||||||
@@ -24,6 +24,15 @@ export interface ProductSpec {
|
|||||||
unit?: string;
|
unit?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Msrp {
|
||||||
|
amount: number;
|
||||||
|
currency: string;
|
||||||
|
region: string;
|
||||||
|
effective_date?: string | null;
|
||||||
|
source_url?: string | null;
|
||||||
|
note?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Product {
|
export interface Product {
|
||||||
id: string;
|
id: string;
|
||||||
gtin: string | null;
|
gtin: string | null;
|
||||||
@@ -43,6 +52,7 @@ export interface Product {
|
|||||||
ingredients_text?: string | null;
|
ingredients_text?: string | null;
|
||||||
allergens?: string[] | null;
|
allergens?: string[] | null;
|
||||||
additives?: string[] | null;
|
additives?: string[] | null;
|
||||||
|
msrp?: Msrp[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Category {
|
export interface Category {
|
||||||
|
|||||||
Reference in New Issue
Block a user