feat(barcode): admin barcode CRUD endpoints + UI; show extra barcodes publicly
CI / Python (ingestion) (pull_request) Successful in 10s
CI / Go (api) (pull_request) Failing after 15s
CI / Migrations (postgres) (pull_request) Failing after 11m35s

Wire the multi-barcode store layer to HTTP and the operator console:

- adminhandler: add POST /products/{id}/barcodes, DELETE
  /products/{id}/barcodes/{barcodeID}, and POST .../primary. A barcode
  owned by another product returns 409 with the conflicting product
  (gtin/product_id/product_name); an invalid GTIN returns 400.
- admin-frontend: BarcodesCard on the product detail page lists all
  barcodes (primary starred), adds with type/pack-level/region, sets
  primary, and deletes; audit labels for the new actions.
- public-frontend: product detail surfaces non-primary barcodes so a
  case/region code resolves and is visible to consumers.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
novaalphastrikeomegaz663
2026-06-20 07:06:07 +00:00
parent 1d5f775d33
commit 98b5f1575d
6 changed files with 286 additions and 0 deletions
@@ -60,6 +60,29 @@ export default function ProductView({ id, onBack }: { id: string; onBack: () =>
<div className="mt-4">
<Row label="品牌" value={p.brand} />
<Row label="条码 (GTIN)" value={p.gtin} />
{(() => {
const others = (p.barcodes || []).filter(
(b) => !b.is_primary && b.gtin !== p.gtin,
);
return others.length ? (
<Row
label="其他条码"
value={
<div className="flex flex-wrap gap-1.5">
{others.map((b) => (
<span
key={b.gtin}
className="font-mono text-xs bg-gray-100 text-gray-600 rounded px-1.5 py-0.5"
title={`${b.gtin_type} · ${b.pack_level}`}
>
{b.gtin}
</span>
))}
</div>
}
/>
) : null;
})()}
<Row label="品类" value={p.category_path} />
<Row
label="净含量"
+9
View File
@@ -6,9 +6,18 @@ export interface ProductSummary {
category_path: string | null;
}
export interface Barcode {
gtin: string;
gtin_type: string;
pack_level: string;
region: string | null;
is_primary: boolean;
}
export interface Product {
id: string;
gtin: string | null;
barcodes?: Barcode[] | null;
name: string;
brand: string | null;
category_path: string | null;