feat(deploy): production deployment setup and fixes
- Debian-based api image (bookworm-slim), docker/debian mirrors, prisma binaryTargets for openssl 3.0 - nginx: admin SPA under /admin, TLS via acme.sh (ZeroSSL) + auto-renewal cron, http->https redirect - prisma: add origin_goods.delisted migration, sync missing schema (good_image/tag_font_color/good_tags), fix users.createdAt Timestamptz - api: CORS wildcard reflection, helmet CORP cross-origin, price backfill in persistProductDetail, categoryIcon ancestor fallback, mediaByColor per-color gallery in public goods detail - admin: /admin base path (vite + router) - import-data.mjs: udt_name casting, serial sequence advance fix
This commit is contained in:
@@ -205,7 +205,9 @@ export class PublicService {
|
||||
if (!good) {
|
||||
throw new NotFoundException({ message: '不存在商品', error: 'PRODUCT_NOT_FOUND' });
|
||||
}
|
||||
return this.toPublicGoodDetail(good);
|
||||
const dto = this.toPublicGoodDetail(good);
|
||||
dto.category.categoryIcon = await this.resolveCategoryIcon(good.category);
|
||||
return dto;
|
||||
}
|
||||
|
||||
async getHomeGoods(query: PublicHomeGoodsQueryDto): Promise<PublicGoodDto[]> {
|
||||
@@ -270,6 +272,66 @@ export class PublicService {
|
||||
};
|
||||
}
|
||||
|
||||
/** Group distinct variant images by color so the frontend can switch media per color.
|
||||
* Only color-specific photos are included (main / result / detail images);
|
||||
* design-layer素材图 and the product-level blank garment photo are excluded
|
||||
* because they are not per-color gallery photos. */
|
||||
private groupImagesByColor(
|
||||
variants: PublicGoodRow['originGood']['variants'],
|
||||
): Array<{ colorId: string | null; colorName: string | null; colorHex: string | null; images: string[] }> {
|
||||
const groups = new Map<string, {
|
||||
colorId: string | null;
|
||||
colorName: string | null;
|
||||
colorHex: string | null;
|
||||
images: string[];
|
||||
}>();
|
||||
for (const variant of variants) {
|
||||
const key = variant.colorId ?? `variant:${variant.sdsVariantId}`;
|
||||
let group = groups.get(key);
|
||||
if (!group) {
|
||||
group = {
|
||||
colorId: variant.colorId,
|
||||
colorName: variant.colorName,
|
||||
colorHex: variant.colorHex,
|
||||
images: [],
|
||||
};
|
||||
groups.set(key, group);
|
||||
}
|
||||
const design = (variant.designData ?? {}) as {
|
||||
detailImgUrls?: Array<{ imageUrl?: unknown }>;
|
||||
prototypeResultGroups?: Array<{ resultImage?: unknown }>;
|
||||
};
|
||||
const urls: unknown[] = [
|
||||
variant.imageUrl,
|
||||
...(design.prototypeResultGroups ?? []).map((item) => item?.resultImage),
|
||||
...(design.detailImgUrls ?? []).map((image) => image?.imageUrl),
|
||||
];
|
||||
for (const url of urls) {
|
||||
const value = typeof url === 'string' ? url.trim() : '';
|
||||
if (value && !group.images.includes(value)) {
|
||||
group.images.push(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [...groups.values()];
|
||||
}
|
||||
|
||||
/** Leaf categories often have no icon upstream; fall back to the nearest ancestor that has one. */
|
||||
private async resolveCategoryIcon(category: PublicGoodRow['category']): Promise<string | null> {
|
||||
if (category.categoryIcon) return category.categoryIcon;
|
||||
let cursor = category.parentCategoryId;
|
||||
for (let depth = 0; cursor !== null && depth < 10; depth++) {
|
||||
const parent = await this.prisma.category.findUnique({
|
||||
where: { id: cursor },
|
||||
select: { categoryIcon: true, parentCategoryId: true },
|
||||
});
|
||||
if (!parent) break;
|
||||
if (parent.categoryIcon) return parent.categoryIcon;
|
||||
cursor = parent.parentCategoryId;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private toPublicGoodDetail(good: PublicGoodRow): PublicGoodDetailDto {
|
||||
const base = this.toPublicGood(good);
|
||||
const detail = good.originGood.detail;
|
||||
@@ -292,6 +354,7 @@ export class PublicService {
|
||||
pictureRequest: detail?.pictureRequest ?? null,
|
||||
},
|
||||
media: (detail?.media as Record<string, unknown> | null) ?? null,
|
||||
mediaByColor: this.groupImagesByColor(good.originGood.variants),
|
||||
options: (detail?.options as Record<string, unknown> | null) ?? null,
|
||||
sizeChart: (detail?.sizeChart as Record<string, unknown> | null) ?? null,
|
||||
packageSpecs: (detail?.packageSpecs as Record<string, unknown> | null) ?? null,
|
||||
|
||||
Reference in New Issue
Block a user