按用户反馈(PLTM006 明明能查到配置数却不显示):左树徽标此前只在 同分类内 ≥2 个配置时才出现,单配置族一行上什么都不显示,而右树同族 链接都挂着绿色已配置徽标,两边数字对不上,反复造成「配置数没显示」 的误解。 - goods API:GOOD_INCLUDE 的 family 带 _count.originGoods, GoodDto.originGood.family 增加 memberCount(族成员链接数) - admin:distinctByFamily 记录 linkCount;一族一行的徽标常显 「N 条链接 · M 个配置」(链接数=族成员数,配置数=该族官网商品数), 悬浮仍列出全部配置可打开 - 文档同步徽标语义 效果:PLTM006 行显示「4 条链接 · 1 个配置」(真值:4 条链接仅配了 1 个官网商品);PLTF026 的「4 条链接 · 4 个配置」同屏可对比,其中 4 个配置为历史同国家重复配置,建议另行清理。 验证:admin vue-tsc 干净 + vitest 22/22;api goods spec 18/18; Prisma _count 查询实测返回 originGoods=4
294 lines
9.7 KiB
TypeScript
294 lines
9.7 KiB
TypeScript
import { ApiProperty } from '@nestjs/swagger';
|
|
import type { Good as PrismaGood } from '@prisma/client';
|
|
|
|
export interface GoodRelations {
|
|
country?: { id: bigint; countryName: string; countryIcon: string | null } | null;
|
|
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?: {
|
|
id: bigint;
|
|
sdsGoodId: string;
|
|
source: 'SDS' | 'CUSTOM';
|
|
goodName: string | null;
|
|
goodImage: string | null;
|
|
goodPrice: unknown;
|
|
family?: { id: bigint; familyCode: string | null; familyName: string; _count?: { originGoods: number } } | null;
|
|
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; tagGroupId: bigint | 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 {
|
|
@ApiProperty()
|
|
id!: string;
|
|
|
|
@ApiProperty()
|
|
goodName!: string;
|
|
|
|
@ApiProperty({ nullable: true })
|
|
goodImage!: string | null;
|
|
|
|
@ApiProperty()
|
|
goodPriority!: number;
|
|
|
|
@ApiProperty()
|
|
countryId!: string;
|
|
|
|
@ApiProperty()
|
|
categoryId!: string;
|
|
|
|
@ApiProperty({ nullable: true })
|
|
tagId!: string | null;
|
|
|
|
@ApiProperty({ nullable: true })
|
|
positionId!: string | null;
|
|
|
|
@ApiProperty()
|
|
originGoodId!: string;
|
|
|
|
@ApiProperty()
|
|
createdAt!: string;
|
|
|
|
@ApiProperty()
|
|
updatedAt!: string;
|
|
|
|
@ApiProperty({ required: false, nullable: true })
|
|
country?: { id: string; countryName: string; countryIcon: string | null } | null;
|
|
|
|
@ApiProperty({ required: false, nullable: true })
|
|
category?: { id: string; categoryName: string; categoryIcon: string | null } | null;
|
|
|
|
@ApiProperty({ required: false, nullable: true })
|
|
tag?: { id: string; tagName: string; tagColor: string | null; tagFontColor: string | null } | null;
|
|
|
|
@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?: {
|
|
id: string;
|
|
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;
|
|
family?: { familyId: string; familyCode: string | null; familyName: string; memberCount?: number } | null;
|
|
} | null;
|
|
|
|
static from(
|
|
good: PrismaGood,
|
|
rel: GoodRelations = {},
|
|
): GoodDto {
|
|
return {
|
|
id: good.id.toString(),
|
|
goodName: good.goodName,
|
|
goodImage: good.goodImage,
|
|
goodPriority: good.goodPriority,
|
|
countryId: good.countryId.toString(),
|
|
categoryId: good.categoryId.toString(),
|
|
tagId: good.tagId === null || good.tagId === undefined ? null : good.tagId.toString(),
|
|
positionId: good.positionId === null || good.positionId === undefined ? null : good.positionId.toString(),
|
|
originGoodId: good.originGoodId.toString(),
|
|
createdAt: good.createdAt.toISOString(),
|
|
updatedAt: good.updatedAt.toISOString(),
|
|
country: rel.country
|
|
? {
|
|
id: rel.country.id.toString(),
|
|
countryName: rel.country.countryName,
|
|
countryIcon: rel.country.countryIcon,
|
|
}
|
|
: null,
|
|
category: rel.category
|
|
? {
|
|
id: rel.category.id.toString(),
|
|
categoryName: rel.category.categoryName,
|
|
categoryIcon: rel.category.categoryIcon,
|
|
}
|
|
: null,
|
|
tag: rel.tag
|
|
? {
|
|
id: rel.tag.id.toString(),
|
|
tagName: rel.tag.tagName,
|
|
tagColor: rel.tag.tagColor,
|
|
tagFontColor: rel.tag.tagFontColor,
|
|
}
|
|
: null,
|
|
tags: rel.goodTags
|
|
? rel.goodTags.map((gt) => ({
|
|
id: gt.tag.id.toString(),
|
|
tagName: gt.tag.tagName,
|
|
tagColor: gt.tag.tagColor,
|
|
tagFontColor: gt.tag.tagFontColor,
|
|
tagGroupId: gt.tag.tagGroupId?.toString() ?? null,
|
|
}))
|
|
: [],
|
|
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
|
|
? {
|
|
id: rel.originGood.id.toString(),
|
|
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,
|
|
family: rel.originGood.family
|
|
? {
|
|
familyId: rel.originGood.family.id.toString(),
|
|
familyCode: rel.originGood.family.familyCode,
|
|
familyName: rel.originGood.family.familyName,
|
|
memberCount: rel.originGood.family._count?.originGoods,
|
|
}
|
|
: 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;
|
|
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;
|
|
}
|