feat(api): add mini program catalog endpoints and product details

This commit is contained in:
yeuimu
2026-08-21 02:11:20 +08:00
parent fcbf8bb494
commit 7d09077f1d
18 changed files with 1347 additions and 179 deletions
+36
View File
@@ -0,0 +1,36 @@
import { readFile } from 'fs/promises';
import { resolve } from 'path';
import { NestFactory } from '@nestjs/core';
import { AppModule } from '../src/app.module';
import { SyncService } from '../src/sync/sync.service';
import { SdsProductDetail } from '../src/sync/sds-client.service';
async function main(): Promise<void> {
const inputPath = process.argv[2];
if (!inputPath) {
throw new Error('Usage: pnpm --filter @inkreach/api import:product-detail -- <product_detail.txt>');
}
const absolutePath = resolve(inputPath);
const raw = await readFile(absolutePath, 'utf8');
const jsonStart = raw.indexOf('{');
if (jsonStart < 0) throw new Error('No JSON object found in product detail file');
const detail = JSON.parse(raw.slice(jsonStart)) as SdsProductDetail;
const app = await NestFactory.createApplicationContext(AppModule, { logger: ['error', 'warn'] });
try {
const result = await app.get(SyncService).importProductDetail(detail);
process.stdout.write(
`Imported SDS product ${result.goodId}: ${result.variants} variants, ` +
`${result.sizeRows} size rows, ${result.packageRows} package rows, ` +
`${result.configuredGoods} configured goods\n`,
);
} finally {
await app.close();
}
}
void main().catch((error: unknown) => {
const message = error instanceof Error ? error.stack ?? error.message : String(error);
process.stderr.write(`${message}\n`);
process.exitCode = 1;
});
@@ -0,0 +1,70 @@
-- Cache SDS product details separately from website merchandising configuration.
CREATE TABLE "origin_good_details" (
"origin_good_id" BIGINT NOT NULL,
"product_code" TEXT,
"english_name" TEXT,
"blank_design_url" TEXT,
"details_page_video_url" TEXT,
"texture_name" TEXT,
"production_cycle_hours" INTEGER,
"min_weight_g" DECIMAL(12,3),
"reminder" TEXT,
"production_process" TEXT,
"material_description" TEXT,
"product_performance" TEXT,
"applicable_scenarios" TEXT,
"washing_instructions" TEXT,
"special_description" TEXT,
"design_explanation" TEXT,
"design_area" TEXT,
"picture_request" TEXT,
"size_chart" JSONB,
"package_specs" JSONB,
"options" JSONB,
"media" JSONB,
"upstream_updated_at" TIMESTAMPTZ(6),
"synced_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "origin_good_details_pkey" PRIMARY KEY ("origin_good_id")
);
CREATE TABLE "origin_good_variants" (
"origin_good_variant_id" BIGSERIAL NOT NULL,
"origin_good_id" BIGINT NOT NULL,
"sds_variant_id" TEXT NOT NULL,
"sku" TEXT NOT NULL,
"size_id" TEXT,
"size_name" TEXT,
"color_id" TEXT,
"color_name" TEXT,
"color_hex" TEXT,
"image_url" TEXT,
"price" DECIMAL(12,2),
"original_price" DECIMAL(12,2),
"weight_g" DECIMAL(12,3),
"box_length_cm" DECIMAL(12,3),
"box_width_cm" DECIMAL(12,3),
"box_height_cm" DECIMAL(12,3),
"enabled" BOOLEAN NOT NULL DEFAULT true,
"sort_order" INTEGER NOT NULL DEFAULT 0,
"design_data" JSONB,
"created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "origin_good_variants_pkey" PRIMARY KEY ("origin_good_variant_id")
);
CREATE UNIQUE INDEX "origin_good_variants_origin_good_id_sds_variant_id_key"
ON "origin_good_variants"("origin_good_id", "sds_variant_id");
CREATE INDEX "origin_good_variants_origin_good_id_sort_order_idx"
ON "origin_good_variants"("origin_good_id", "sort_order");
CREATE INDEX "origin_good_variants_sku_idx" ON "origin_good_variants"("sku");
ALTER TABLE "origin_good_details"
ADD CONSTRAINT "origin_good_details_origin_good_id_fkey"
FOREIGN KEY ("origin_good_id") REFERENCES "origin_goods"("origin_good_id")
ON DELETE CASCADE ON UPDATE NO ACTION;
ALTER TABLE "origin_good_variants"
ADD CONSTRAINT "origin_good_variants_origin_good_id_fkey"
FOREIGN KEY ("origin_good_id") REFERENCES "origin_goods"("origin_good_id")
ON DELETE CASCADE ON UPDATE NO ACTION;
+100 -33
View File
@@ -15,23 +15,90 @@ datasource db {
// ---------- Origin Goods ----------
model OriginGood {
id BigInt @id @default(autoincrement()) @map("origin_good_id")
sdsGoodId String @unique @map("sds_good_id")
id BigInt @id @default(autoincrement()) @map("origin_good_id")
sdsGoodId String @unique @map("sds_good_id")
// Cached SDS product metadata (filled during sync)
sdsCategoryId String? @map("sds_category_id")
goodName String? @map("good_name")
goodImage String? @map("good_image")
goodPrice Decimal? @map("good_price") @db.Decimal(12, 2)
delisted Boolean @default(false)
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6)
sdsCategoryId String? @map("sds_category_id")
goodName String? @map("good_name")
goodImage String? @map("good_image")
goodPrice Decimal? @map("good_price") @db.Decimal(12, 2)
delisted Boolean @default(false)
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6)
goods Good[]
goods Good[]
detail OriginGoodDetail?
variants OriginGoodVariant[]
@@index([sdsCategoryId])
@@map("origin_goods")
}
// ---------- Origin Good Details (cached from SDS /products/{id}) ----------
model OriginGoodDetail {
originGoodId BigInt @id @map("origin_good_id")
productCode String? @map("product_code")
englishName String? @map("english_name")
blankDesignUrl String? @map("blank_design_url")
detailsPageVideoUrl String? @map("details_page_video_url")
textureName String? @map("texture_name")
productionCycleHours Int? @map("production_cycle_hours")
minWeightG Decimal? @map("min_weight_g") @db.Decimal(12, 3)
reminder String?
productionProcess String? @map("production_process")
materialDescription String? @map("material_description")
productPerformance String? @map("product_performance")
applicableScenarios String? @map("applicable_scenarios")
washingInstructions String? @map("washing_instructions")
specialDescription String? @map("special_description")
designExplanation String? @map("design_explanation")
designArea String? @map("design_area")
pictureRequest String? @map("picture_request")
sizeChart Json? @map("size_chart")
packageSpecs Json? @map("package_specs")
options Json?
media Json?
upstreamUpdatedAt DateTime? @map("upstream_updated_at") @db.Timestamptz(6)
syncedAt DateTime @default(now()) @map("synced_at") @db.Timestamptz(6)
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6)
originGood OriginGood @relation(fields: [originGoodId], references: [id], onDelete: Cascade, onUpdate: NoAction)
@@map("origin_good_details")
}
// ---------- Origin Good Variants (cached SDS child products / SKUs) ----------
model OriginGoodVariant {
id BigInt @id @default(autoincrement()) @map("origin_good_variant_id")
originGoodId BigInt @map("origin_good_id")
sdsVariantId String @map("sds_variant_id")
sku String
sizeId String? @map("size_id")
sizeName String? @map("size_name")
colorId String? @map("color_id")
colorName String? @map("color_name")
colorHex String? @map("color_hex")
imageUrl String? @map("image_url")
price Decimal? @db.Decimal(12, 2)
originalPrice Decimal? @map("original_price") @db.Decimal(12, 2)
weightG Decimal? @map("weight_g") @db.Decimal(12, 3)
boxLengthCm Decimal? @map("box_length_cm") @db.Decimal(12, 3)
boxWidthCm Decimal? @map("box_width_cm") @db.Decimal(12, 3)
boxHeightCm Decimal? @map("box_height_cm") @db.Decimal(12, 3)
enabled Boolean @default(true)
sortOrder Int @default(0) @map("sort_order")
designData Json? @map("design_data")
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6)
originGood OriginGood @relation(fields: [originGoodId], references: [id], onDelete: Cascade, onUpdate: NoAction)
@@unique([originGoodId, sdsVariantId])
@@index([originGoodId, sortOrder])
@@index([sku])
@@map("origin_good_variants")
}
// ---------- Countries ----------
model Country {
id BigInt @id @default(autoincrement()) @map("country_id")
@@ -48,18 +115,18 @@ model Country {
// ---------- Categories (self-referential tree) ----------
model Category {
id BigInt @id @default(autoincrement()) @map("category_id")
parentCategoryId BigInt? @map("parent_category_id")
categoryName String @map("category_name")
categoryIcon String? @map("category_icon")
sdsCategoryId String? @unique @map("sds_category_id")
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6)
id BigInt @id @default(autoincrement()) @map("category_id")
parentCategoryId BigInt? @map("parent_category_id")
categoryName String @map("category_name")
categoryIcon String? @map("category_icon")
sdsCategoryId String? @unique @map("sds_category_id")
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6)
parent Category? @relation("CategoryToCategory", fields: [parentCategoryId], references: [id], onDelete: Restrict, onUpdate: NoAction)
children Category[] @relation("CategoryToCategory")
goods Good[]
positions Position[]
parent Category? @relation("CategoryToCategory", fields: [parentCategoryId], references: [id], onDelete: Restrict, onUpdate: NoAction)
children Category[] @relation("CategoryToCategory")
goods Good[]
positions Position[]
@@index([parentCategoryId])
@@map("categories")
@@ -94,7 +161,7 @@ model Tag {
goods Good[]
goodTags GoodTag[]
tagGroup TagGroup? @relation(fields: [tagGroupId], references: [id], onDelete: SetNull, onUpdate: NoAction)
tagGroup TagGroup? @relation(fields: [tagGroupId], references: [id], onDelete: SetNull, onUpdate: NoAction)
@@index([tagGroupId])
@@index([tagGroupId, sortOrder])
@@ -122,17 +189,17 @@ model Position {
// ---------- Goods ----------
model Good {
id BigInt @id @default(autoincrement()) @map("good_id")
originGoodId BigInt @map("origin_good_id")
countryId BigInt @map("country_id")
categoryId BigInt @map("category_id")
tagId BigInt? @map("tag_id")
positionId BigInt? @map("position_id")
goodName String @map("good_name")
goodImage String? @map("good_image")
goodPriority Int @default(0) @map("good_priority")
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6)
id BigInt @id @default(autoincrement()) @map("good_id")
originGoodId BigInt @map("origin_good_id")
countryId BigInt @map("country_id")
categoryId BigInt @map("category_id")
tagId BigInt? @map("tag_id")
positionId BigInt? @map("position_id")
goodName String @map("good_name")
goodImage String? @map("good_image")
goodPriority Int @default(0) @map("good_priority")
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6)
originGood OriginGood @relation(fields: [originGoodId], references: [id], onDelete: Restrict, onUpdate: NoAction)
country Country @relation(fields: [countryId], references: [id], onDelete: Restrict, onUpdate: NoAction)