feat(store): 门店可见白名单(对齐商品)
HQ 可配置 visibilityWhitelistEnabled + 手机号;C 端公开门店列表/详情按登录手机号过滤;登录态变化后小程序重新拉门店列表。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -36,6 +36,17 @@ function normalizeOptionalTextField(value: unknown): string | null {
|
||||
return s;
|
||||
}
|
||||
|
||||
export type StoreViewer = {
|
||||
/** C 端用户手机号;无则无法看到白名单门店 */
|
||||
phone?: string | null;
|
||||
/** 运营/代下单等场景跳过白名单 */
|
||||
bypassWhitelist?: boolean;
|
||||
};
|
||||
|
||||
function normalizePhone(phone: string | null | undefined): string {
|
||||
return (phone || '').replace(/\D/g, '').trim();
|
||||
}
|
||||
|
||||
function parseOptionalCoord(value: unknown, kind: 'lat' | 'lng' = 'lng'): number | null {
|
||||
if (value == null || value === '') return null;
|
||||
const n = typeof value === 'number' ? value : Number(value);
|
||||
@@ -96,7 +107,34 @@ export class StoreService {
|
||||
return { latitude: geo.latitude, longitude: geo.longitude };
|
||||
}
|
||||
|
||||
async listOpenStores(cityCode?: string, userLat?: number, userLng?: number) {
|
||||
async resolveUserPhone(userId: bigint): Promise<string | null> {
|
||||
const user = await this.prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { phone: true },
|
||||
});
|
||||
return user?.phone ?? null;
|
||||
}
|
||||
|
||||
isVisibleToViewer(
|
||||
store: {
|
||||
visibilityWhitelistEnabled: boolean;
|
||||
visibilityPhones: Array<{ phone: string }>;
|
||||
},
|
||||
viewer?: StoreViewer,
|
||||
): boolean {
|
||||
if (viewer?.bypassWhitelist) return true;
|
||||
if (!store.visibilityWhitelistEnabled) return true;
|
||||
const phone = normalizePhone(viewer?.phone);
|
||||
if (!phone) return false;
|
||||
return store.visibilityPhones.some((row) => normalizePhone(row.phone) === phone);
|
||||
}
|
||||
|
||||
async listOpenStores(
|
||||
cityCode?: string,
|
||||
userLat?: number,
|
||||
userLng?: number,
|
||||
viewer?: StoreViewer,
|
||||
) {
|
||||
const where: Record<string, unknown> = { status: 'OPEN' };
|
||||
if (cityCode) {
|
||||
const city = await this.prisma.commonCity.findFirst({ where: { code: cityCode } });
|
||||
@@ -104,10 +142,16 @@ export class StoreService {
|
||||
}
|
||||
const stores = await this.prisma.store.findMany({
|
||||
where: where as never,
|
||||
include: { category: true, coverResource: true },
|
||||
include: {
|
||||
category: true,
|
||||
coverResource: true,
|
||||
visibilityPhones: { select: { phone: true } },
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
|
||||
const visible = stores.filter((s) => this.isVisibleToViewer(s, viewer));
|
||||
|
||||
const hasUser =
|
||||
userLat != null &&
|
||||
userLng != null &&
|
||||
@@ -121,10 +165,11 @@ export class StoreService {
|
||||
};
|
||||
|
||||
const items: StoreListItem[] = [];
|
||||
for (const store of stores) {
|
||||
for (const store of visible) {
|
||||
const coords = await this.ensureStoreCoordinates(store);
|
||||
const { visibilityPhones: _phones, visibilityWhitelistEnabled: _wl, ...rest } = store;
|
||||
const mapped = mapStoreCompat({
|
||||
...store,
|
||||
...rest,
|
||||
latitude: coords?.latitude ?? store.latitude,
|
||||
longitude: coords?.longitude ?? store.longitude,
|
||||
});
|
||||
@@ -146,20 +191,27 @@ export class StoreService {
|
||||
return serializeBigInt(items);
|
||||
}
|
||||
|
||||
async getStore(id: bigint) {
|
||||
async getStore(id: bigint, viewer?: StoreViewer) {
|
||||
const store = await this.prisma.store.findFirst({
|
||||
where: { id, status: 'OPEN' },
|
||||
include: { category: true, coverResource: true },
|
||||
include: {
|
||||
category: true,
|
||||
coverResource: true,
|
||||
visibilityPhones: { select: { phone: true } },
|
||||
},
|
||||
});
|
||||
if (!store) throw new NotFoundException('门店不存在');
|
||||
if (!store || !this.isVisibleToViewer(store, viewer)) {
|
||||
throw new NotFoundException('门店不存在');
|
||||
}
|
||||
const coords = await this.ensureStoreCoordinates(store);
|
||||
const media = await this.prisma.commonResource.findMany({
|
||||
where: { ownerType: 'STORE', ownerId: id, status: 'ACTIVE', bizType: 'ENV' },
|
||||
orderBy: { sortOrder: 'asc' },
|
||||
});
|
||||
const { visibilityPhones: _phones, visibilityWhitelistEnabled: _wl, ...rest } = store;
|
||||
return serializeBigInt(
|
||||
mapStoreCompat({
|
||||
...store,
|
||||
...rest,
|
||||
latitude: coords?.latitude ?? store.latitude,
|
||||
longitude: coords?.longitude ?? store.longitude,
|
||||
media,
|
||||
|
||||
Reference in New Issue
Block a user