deploy(v2): h5 build with base /v2/h5, admin dockerfile & nginx route, shared compose
This commit is contained in:
@@ -60,6 +60,24 @@ const PUBLIC_GOOD_INCLUDE = {
|
||||
|
||||
type PublicGoodRow = Prisma.GoodGetPayload<{ include: typeof PUBLIC_GOOD_INCLUDE }>;
|
||||
|
||||
/**
|
||||
* 列表/首页卡片的精简 include:重 JSON(变体 design_data、detail、merged 副源、
|
||||
* 整份族矩阵)全部不进列表查询——全量商品拉这些字段实测要 ~2s,而列表 DTO
|
||||
* 一个都不用。详情接口仍走 PUBLIC_GOOD_INCLUDE。
|
||||
*/
|
||||
const PUBLIC_GOOD_LIST_INCLUDE = {
|
||||
country: true,
|
||||
category: true,
|
||||
tag: { include: { tagGroup: true } },
|
||||
position: true,
|
||||
originGood: {
|
||||
select: { sdsGoodId: true, goodImage: true, goodPrice: true },
|
||||
},
|
||||
goodTags: { include: { tag: { include: { tagGroup: true } } } },
|
||||
} satisfies Prisma.GoodInclude;
|
||||
|
||||
type PublicGoodListRow = Prisma.GoodGetPayload<{ include: typeof PUBLIC_GOOD_LIST_INCLUDE }>;
|
||||
|
||||
@Injectable()
|
||||
export class PublicService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
@@ -214,10 +232,11 @@ export class PublicService {
|
||||
// 再内存分组、分页作用于分组结果 —— 若量级上万需改为物化族表查询。
|
||||
const rows = await this.prisma.good.findMany({
|
||||
where,
|
||||
include: PUBLIC_GOOD_INCLUDE,
|
||||
include: PUBLIC_GOOD_LIST_INCLUDE,
|
||||
orderBy,
|
||||
});
|
||||
const grouped = new Map<string, PublicGoodRow[]>();
|
||||
const familyMinPrices = await this.loadFamilyMinPrices();
|
||||
const grouped = new Map<string, PublicGoodListRow[]>();
|
||||
for (const good of rows) {
|
||||
const key = good.familyId ? `f:${good.familyId}` : `g:${good.id}`;
|
||||
const bucket = grouped.get(key);
|
||||
@@ -228,8 +247,10 @@ export class PublicService {
|
||||
const rep = goods[0];
|
||||
const dto = this.toPublicGood(rep);
|
||||
// 列表价 = 族矩阵最低价("这个款之下有哪些价格"的起价);无矩阵回退链接价
|
||||
const familyMin = this.familyMinPrice(rep);
|
||||
if (rep.familyId && familyMin !== null) dto.price = familyMin;
|
||||
const familyMin = rep.familyId
|
||||
? familyMinPrices.get(rep.familyId.toString())
|
||||
: undefined;
|
||||
if (familyMin !== undefined) dto.price = familyMin;
|
||||
return dto;
|
||||
});
|
||||
if (query.sort === 'PRICE_ASC' || query.sort === 'PRICE_DESC') {
|
||||
@@ -249,14 +270,28 @@ export class PublicService {
|
||||
};
|
||||
}
|
||||
|
||||
/** 族物化矩阵的最低价;无族/无矩阵返回 null */
|
||||
private familyMinPrice(good: PublicGoodRow): string | null {
|
||||
const matrix = good.originGood.family?.priceMatrix as
|
||||
| { rows?: Array<{ price: string }> }
|
||||
| null
|
||||
| undefined;
|
||||
const prices = (matrix?.rows ?? []).map((r) => Number(r.price)).filter((n) => Number.isFinite(n));
|
||||
return prices.length ? String(Math.min(...prices)) : null;
|
||||
/**
|
||||
* 族最低价一次 SQL 聚合:price_matrix 是每族 ~11KB 的 JSONB,按行 include
|
||||
* 会让每个商品都携带整份矩阵(实测全量 ~330ms);PG 端展开聚合只回传
|
||||
* 每族一个数字。非数字/缺失 price 的行跳过,与旧内存版过滤语义一致。
|
||||
*/
|
||||
private async loadFamilyMinPrices(): Promise<Map<string, string>> {
|
||||
const rows = await this.prisma.$queryRaw<
|
||||
Array<{ family_id: bigint | string; min_price: Prisma.Decimal | null }>
|
||||
>`
|
||||
SELECT f.family_id, MIN((r->>'price')::numeric) AS min_price
|
||||
FROM product_families f
|
||||
CROSS JOIN LATERAL jsonb_array_elements(f.price_matrix->'rows') AS r
|
||||
WHERE (r->>'price') ~ '^-?[0-9]+(\.[0-9]+)?$'
|
||||
GROUP BY f.family_id
|
||||
`;
|
||||
const result = new Map<string, string>();
|
||||
for (const row of rows) {
|
||||
if (row.min_price !== null) {
|
||||
result.set(row.family_id.toString(), String(Number(row.min_price)));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -321,7 +356,7 @@ export class PublicService {
|
||||
originGood: { delisted: false },
|
||||
...(query.countryId ? { countryId: BigInt(query.countryId) } : {}),
|
||||
},
|
||||
include: PUBLIC_GOOD_INCLUDE,
|
||||
include: PUBLIC_GOOD_LIST_INCLUDE,
|
||||
orderBy: [
|
||||
{ position: { indexVal: 'asc' } },
|
||||
{ goodPriority: 'desc' },
|
||||
@@ -329,6 +364,7 @@ export class PublicService {
|
||||
],
|
||||
take: query.limit,
|
||||
});
|
||||
const familyMinPrices = await this.loadFamilyMinPrices();
|
||||
// 首页同样按族去重(同族多条位置配置只保留排序最前一条),再截取 limit
|
||||
const seen = new Set<string>();
|
||||
const items: PublicGoodDto[] = [];
|
||||
@@ -337,14 +373,17 @@ export class PublicService {
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
const dto = this.toPublicGood(good);
|
||||
const familyMin = this.familyMinPrice(good);
|
||||
if (good.familyId && familyMin !== null) dto.price = familyMin;
|
||||
const familyMin = good.familyId
|
||||
? familyMinPrices.get(good.familyId.toString())
|
||||
: undefined;
|
||||
if (familyMin !== undefined) dto.price = familyMin;
|
||||
items.push(dto);
|
||||
}
|
||||
return items.slice(0, query.limit);
|
||||
}
|
||||
|
||||
private toPublicGood(good: PublicGoodRow): PublicGoodDto {
|
||||
/** 入参用精简行类型:列表(PublicGoodListRow)与详情(PublicGoodRow,字段超集)都能传 */
|
||||
private toPublicGood(good: PublicGoodListRow): PublicGoodDto {
|
||||
const formatGroup = (group: { id: bigint; groupName: string; sortOrder: number } | null) =>
|
||||
group
|
||||
? { id: group.id.toString(), groupName: group.groupName, sortOrder: group.sortOrder }
|
||||
|
||||
Reference in New Issue
Block a user