feat(goods): support merged secondary origin goods in create/update/detail

This commit is contained in:
yeuimu
2026-08-27 18:15:56 +08:00
parent 848eed0b6f
commit 9c279ae393
6 changed files with 729 additions and 418 deletions
+146 -80
View File
@@ -6,33 +6,58 @@ export interface GoodRelations {
category?: { id: bigint; categoryName: string; categoryIcon: string | null } | null;
tag?: { id: bigint; tagName: string; tagColor: string | null; tagFontColor: string | null } | null;
position?: { id: bigint; indexVal: number } | null;
originGood?: {
originGood?: {
id: bigint;
sdsGoodId: string;
source: 'SDS' | 'CUSTOM';
sdsGoodId: string;
source: 'SDS' | 'CUSTOM';
goodName: string | null;
goodImage: string | null;
goodPrice: unknown;
detail?: {
productCode: string | null;
syncedAt: Date;
sizeChart: unknown;
packageSpecs: unknown;
[key: string]: unknown;
} | null;
variants?: Array<{
sdsVariantId: string;
sku: string;
sizeName: string | null;
colorName: string | null;
colorHex: string | null;
price: unknown;
enabled: boolean;
[key: string]: unknown;
}>;
_count?: { variants: number };
goodPrice: unknown;
detail?: {
productCode: string | null;
syncedAt: Date;
sizeChart: unknown;
packageSpecs: unknown;
[key: string]: unknown;
} | null;
variants?: Array<{
sdsVariantId: string;
sku: string;
sizeName: string | null;
colorName: string | null;
colorHex: string | null;
price: unknown;
enabled: boolean;
[key: string]: unknown;
}>;
_count?: { variants: number };
} | null;
goodTags?: { tag: { id: bigint; tagName: string; tagColor: string | null; tagFontColor: string | null } }[];
mergedOriginGoods?: Array<{
originGood: {
id: bigint;
sdsGoodId: string;
source: 'SDS' | 'CUSTOM';
goodName: string | null;
goodImage: string | null;
goodPrice: unknown;
detail?: { syncedAt: Date } | Record<string, unknown> | null;
variants?: Array<{ [key: string]: unknown }>;
_count?: { variants: number };
};
}>;
}
export interface MergedOriginGoodSummary {
id: string;
sdsGoodId: string;
source: 'SDS' | 'CUSTOM';
isCustom: boolean;
goodName: string | null;
goodImage: string | null;
goodPrice: string | null;
hasDetail: boolean;
variantCount: number;
}
export class GoodDto {
@@ -81,24 +106,27 @@ export class GoodDto {
@ApiProperty({ required: false, type: Array })
tags!: Array<{ id: string; tagName: string; tagColor: string | null; tagFontColor: string | null }>;
@ApiProperty({ required: false, type: Array })
mergedOriginGoods!: MergedOriginGoodSummary[];
@ApiProperty({ required: false, nullable: true })
position?: { id: string; indexVal: number } | null;
@ApiProperty({ required: false, nullable: true })
originGood?: {
originGood?: {
id: string;
sdsGoodId: string;
source: 'SDS' | 'CUSTOM';
isCustom: boolean;
sdsGoodId: string;
source: 'SDS' | 'CUSTOM';
isCustom: boolean;
goodName: string | null;
goodImage: string | null;
goodPrice: string | null;
hasDetail: boolean;
detailSyncedAt: string | null;
variantCount: number;
sizeRowCount: number;
packageRowCount: number;
productCode: string | null;
goodPrice: string | null;
hasDetail: boolean;
detailSyncedAt: string | null;
variantCount: number;
sizeRowCount: number;
packageRowCount: number;
productCode: string | null;
} | null;
static from(
@@ -147,70 +175,108 @@ export class GoodDto {
tagFontColor: gt.tag.tagFontColor,
}))
: [],
mergedOriginGoods: (rel.mergedOriginGoods ?? []).map((m) => ({
id: m.originGood.id.toString(),
sdsGoodId: m.originGood.sdsGoodId,
source: m.originGood.source,
isCustom: m.originGood.source === 'CUSTOM',
goodName: m.originGood.goodName,
goodImage: m.originGood.goodImage,
goodPrice:
m.originGood.goodPrice === null || m.originGood.goodPrice === undefined
? null
: (m.originGood.goodPrice as { toString(): string }).toString(),
hasDetail: Boolean(m.originGood.detail),
variantCount:
m.originGood._count?.variants ?? m.originGood.variants?.length ?? 0,
})),
position: rel.position
? {
id: rel.position.id.toString(),
indexVal: rel.position.indexVal,
}
: null,
originGood: rel.originGood
? {
originGood: rel.originGood
? {
id: rel.originGood.id.toString(),
sdsGoodId: rel.originGood.sdsGoodId,
source: rel.originGood.source,
isCustom: rel.originGood.source === 'CUSTOM',
sdsGoodId: rel.originGood.sdsGoodId,
source: rel.originGood.source,
isCustom: rel.originGood.source === 'CUSTOM',
goodName: rel.originGood.goodName,
goodImage: rel.originGood.goodImage,
goodPrice:
rel.originGood.goodPrice === null ||
rel.originGood.goodPrice === undefined
? null
: (rel.originGood.goodPrice as { toString(): string }).toString(),
hasDetail: Boolean(rel.originGood.detail),
detailSyncedAt: rel.originGood.detail?.syncedAt.toISOString() ?? null,
variantCount: rel.originGood._count?.variants ?? rel.originGood.variants?.length ?? 0,
sizeRowCount: GoodDto.jsonRows(rel.originGood.detail?.sizeChart),
packageRowCount: GoodDto.jsonRows(rel.originGood.detail?.packageSpecs),
productCode: rel.originGood.detail?.productCode ?? null,
}
: (rel.originGood.goodPrice as { toString(): string }).toString(),
hasDetail: Boolean(rel.originGood.detail),
detailSyncedAt: rel.originGood.detail?.syncedAt.toISOString() ?? null,
variantCount: rel.originGood._count?.variants ?? rel.originGood.variants?.length ?? 0,
sizeRowCount: GoodDto.jsonRows(rel.originGood.detail?.sizeChart),
packageRowCount: GoodDto.jsonRows(rel.originGood.detail?.packageSpecs),
productCode: rel.originGood.detail?.productCode ?? null,
}
: null,
};
}
private static jsonRows(value: unknown): number {
if (!value || typeof value !== 'object' || !('rows' in value)) return 0;
const rows = (value as { rows?: unknown }).rows;
return Array.isArray(rows) ? rows.length : 0;
}
}
export class GoodDetailDto extends GoodDto {
@ApiProperty({ nullable: true, type: Object })
originDetail!: Record<string, unknown> | null;
@ApiProperty({ type: Array })
variants!: Array<Record<string, unknown>>;
static fromGood(good: PrismaGood, rel: GoodRelations): GoodDetailDto {
const base = GoodDto.from(good, rel);
const detail = rel.originGood?.detail;
return {
...base,
originDetail: detail ? { ...detail, syncedAt: detail.syncedAt.toISOString() } : null,
variants: (rel.originGood?.variants ?? []).map((variant) => ({
...variant,
price:
variant.price === null || variant.price === undefined
? null
: (variant.price as { toString(): string }).toString(),
})),
};
}
}
};
}
private static jsonRows(value: unknown): number {
if (!value || typeof value !== 'object' || !('rows' in value)) return 0;
const rows = (value as { rows?: unknown }).rows;
return Array.isArray(rows) ? rows.length : 0;
}
}
export class GoodDetailDto extends GoodDto {
@ApiProperty({ nullable: true, type: Object })
originDetail!: Record<string, unknown> | null;
@ApiProperty({ type: Array })
variants!: Array<Record<string, unknown>>;
static fromGood(good: PrismaGood, rel: GoodRelations): GoodDetailDto {
const base = GoodDto.from(good, rel);
const detail = rel.originGood?.detail;
const toAnnotated = (
variant: Record<string, unknown>,
originGoodId: string,
originGoodName: string | null,
) => ({
...variant,
price:
variant.price === null || variant.price === undefined
? null
: (variant.price as unknown as { toString(): string }).toString(),
originGoodId,
originGoodName,
});
const primaryId = good.originGoodId.toString();
const primaryName = rel.originGood?.goodName ?? null;
const mergedVariants = [
...(rel.originGood?.variants ?? []).map((variant) =>
toAnnotated(variant as Record<string, unknown>, primaryId, primaryName),
),
...(rel.mergedOriginGoods ?? []).flatMap((m) =>
(m.originGood.variants ?? []).map((variant) =>
toAnnotated(
variant,
m.originGood.id.toString(),
m.originGood.goodName,
),
),
),
];
return {
...base,
originDetail: detail ? { ...detail, syncedAt: detail.syncedAt.toISOString() } : null,
variants: mergedVariants,
};
}
}
export interface PaginatedGoods {
items: GoodDto[];
total: number;
page: number;
pageSize: number;
}
}