import { BadRequestException, Injectable } from '@nestjs/common'; import { PrismaService } from '../../common/prisma/prisma.module'; import { serializeBigInt } from '../../common/decorators/current-user.decorator'; import { groupResourcesByProductId, mapProductMedia } from './catalog.mapper'; export type CatalogViewer = { /** C 端用户手机号;无则无法看到白名单商品 */ phone?: string | null; /** 仅总部代下单等运营场景跳过白名单;合伙人端必须遵守白名单 */ bypassWhitelist?: boolean; }; function normalizePhone(phone: string | null | undefined): string { return (phone || '').replace(/\D/g, '').trim(); } @Injectable() export class CatalogService { constructor(private readonly prisma: PrismaService) {} async listCities() { const cities = await this.prisma.commonCity.findMany({ where: { status: 'ACTIVE' }, include: { partnerAccounts: { where: { isPrimary: 1, bindingStatus: 'ACTIVE' }, orderBy: { createdAt: 'asc' }, select: { id: true, companyName: true, scopeType: true }, }, }, orderBy: { name: 'asc' }, }); return serializeBigInt( cities.map((city) => ({ ...city, partnerBindingCount: city.partnerAccounts.length, partnerBindings: city.partnerAccounts.map((bp) => ({ partnerAccountId: bp.id.toString(), partnerId: bp.id.toString(), companyName: bp.companyName, scopeType: bp.scopeType, })), partnerAccounts: undefined, })), ); } async listProducts(aromaType?: string, cityCode?: string, viewer?: CatalogViewer) { if (cityCode) { const city = await this.prisma.commonCity.findFirst({ where: { code: cityCode, status: 'ACTIVE' }, }); if (!city) throw new BadRequestException('该城市暂未开城'); } const products = await this.prisma.commonProductItem.findMany({ where: { status: 'ON_SALE', ...(aromaType ? { aromaType: aromaType as never } : {}) }, orderBy: { sortOrder: 'asc' }, include: { coverResource: true, visibilityPhones: { select: { phone: true } }, }, }); const visible = products.filter((p) => this.isVisibleToViewer(p, viewer)); const productIds = visible.map((p) => p.id); const resources = productIds.length ? await this.prisma.commonResource.findMany({ where: { ownerType: 'PRODUCT', ownerId: { in: productIds }, status: 'ACTIVE', bizType: { in: ['CAROUSEL', 'DETAIL'] }, }, orderBy: { sortOrder: 'asc' }, }) : []; const resourceMap = groupResourcesByProductId(resources); return serializeBigInt( visible.map((p) => { const media = mapProductMedia(p, resourceMap.get(p.id.toString()) ?? []); const { visibilityPhones, ...rest } = p; return { ...rest, visibilityWhitelistEnabled: p.visibilityWhitelistEnabled, visibilityPhones: visibilityPhones.map((row) => row.phone), benefitAmount: p.benefitAmount ?? p.price, price: Number(p.price), benefitDisplay: Number(p.benefitAmount ?? p.price), ...media, }; }), ); } async getProduct(id: bigint, viewer?: CatalogViewer) { const product = await this.prisma.commonProductItem.findUnique({ where: { id }, include: { coverResource: true, visibilityPhones: { select: { phone: true } }, }, }); if (!product) return null; if (!this.isVisibleToViewer(product, viewer)) { return null; } const resources = await this.prisma.commonResource.findMany({ where: { ownerType: 'PRODUCT', ownerId: id, status: 'ACTIVE', bizType: { in: ['CAROUSEL', 'DETAIL'] }, }, orderBy: { sortOrder: 'asc' }, }); const media = mapProductMedia(product, resources); const { visibilityPhones, ...rest } = product; return serializeBigInt({ ...rest, visibilityWhitelistEnabled: product.visibilityWhitelistEnabled, visibilityPhones: visibilityPhones.map((row) => row.phone), benefitAmount: product.benefitAmount ?? product.price, price: Number(product.price), ...media, }); } /** 下单前校验:白名单商品仅白名单手机号可买 */ async assertPurchasable(productId: bigint, viewerPhone?: string | null) { const product = await this.prisma.commonProductItem.findUnique({ where: { id: productId }, include: { visibilityPhones: { select: { phone: true } } }, }); if (!product || product.status !== 'ON_SALE') { throw new BadRequestException('商品不可购买'); } if (!this.isVisibleToViewer(product, { phone: viewerPhone })) { throw new BadRequestException('该商品暂不对当前账号开放'); } return product; } async resolveUserPhone(userId: bigint): Promise { const user = await this.prisma.user.findUnique({ where: { id: userId }, select: { phone: true }, }); return user?.phone ?? null; } isVisibleToViewer( product: { visibilityWhitelistEnabled: boolean; visibilityPhones: Array<{ phone: string }>; }, viewer?: CatalogViewer, ): boolean { if (viewer?.bypassWhitelist) return true; if (!product.visibilityWhitelistEnabled) return true; const phone = normalizePhone(viewer?.phone); if (!phone) return false; return product.visibilityPhones.some((row) => normalizePhone(row.phone) === phone); } }