feat(api): add mini program catalog endpoints and product details
This commit is contained in:
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user