diff --git a/apps/admin-web/src/lib/storeCreate.ts b/apps/admin-web/src/lib/storeCreate.ts index c7a363e..37fa198 100644 --- a/apps/admin-web/src/lib/storeCreate.ts +++ b/apps/admin-web/src/lib/storeCreate.ts @@ -27,6 +27,8 @@ export type StoreCreateForm = { bankAccountNo: string; bankBranch: string; settlementRate?: number; + visibilityWhitelistEnabled?: boolean; + visibilityPhones?: string[]; }; const PHONE_RE = /^1\d{10}$/; diff --git a/apps/admin-web/src/pages/StoresPage.tsx b/apps/admin-web/src/pages/StoresPage.tsx index 36f385a..1785d61 100644 --- a/apps/admin-web/src/pages/StoresPage.tsx +++ b/apps/admin-web/src/pages/StoresPage.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { useNavigate, useSearchParams } from 'react-router-dom'; import { Alert, @@ -13,6 +13,7 @@ import { Select, Space, Steps, + Switch, Table, Tabs, Tag, @@ -20,6 +21,7 @@ import { message, } from 'antd'; import type { ColumnsType } from 'antd/es/table'; +import type { FormInstance } from 'antd/es/form'; import { FilePdfOutlined, LinkOutlined, EnvironmentOutlined } from '@ant-design/icons'; import { request, type Paginated } from '../lib/api'; import { @@ -274,6 +276,89 @@ function StoreAuditMediaSection({ detail }: { detail: Record }) ); } +type UserPickRow = { id: string; phone?: string | null; nickname?: string | null; userNo?: string | null }; + +function StoreVisibilityWhitelistFields({ form }: { form: FormInstance }) { + const [userOptions, setUserOptions] = useState([]); + const [userSearching, setUserSearching] = useState(false); + const searchTimer = useRef | null>(null); + + async function searchUsers(keyword: string) { + const q = keyword.trim(); + if (searchTimer.current) clearTimeout(searchTimer.current); + if (!q) { + setUserOptions([]); + return; + } + searchTimer.current = setTimeout(() => { + void (async () => { + setUserSearching(true); + try { + const qs = new URLSearchParams({ page: '1', pageSize: '20', phone: q }); + const res = await request<{ items: UserPickRow[] }>(`/admin/users?${qs}`); + setUserOptions((res.items ?? []).filter((u) => !!u.phone)); + } catch { + setUserOptions([]); + } finally { + setUserSearching(false); + } + })(); + }, 300); + } + + const enabled = Form.useWatch('visibilityWhitelistEnabled', form); + + return ( + <> + + + + {enabled ? ( + <> + + ({ + value: u.phone!, + label: `${u.phone}${u.nickname ? ` · ${u.nickname}` : ''}${u.userNo ? `(${u.userNo})` : ''}`, + }))} + onSearch={searchUsers} + onSelect={(phone: string) => { + const cur = (form.getFieldValue('visibilityPhones') as string[] | undefined) ?? []; + if (!cur.includes(phone)) { + form.setFieldsValue({ visibilityPhones: [...cur, phone] }); + } + }} + notFoundContent={userSearching ? '搜索中…' : '输入手机号搜索'} + /> + + + ) : null} + + ); +} + type StoreRow = { id: string; name: string; @@ -287,6 +372,8 @@ type StoreRow = { intro: string | null; coverUrl: string | null; createdAt: string; + visibilityWhitelistEnabled?: boolean; + visibilityPhones?: string[]; cityRef?: { name: string; code: string }; partner?: { companyName: string }; account?: { @@ -504,6 +591,10 @@ export default function StoresPage() { bankAccountName: account?.bankAccountName || undefined, bankAccountNo: account?.bankAccountNo || undefined, bankBranch: account?.bankBranch || undefined, + visibilityWhitelistEnabled: !!d.visibilityWhitelistEnabled, + visibilityPhones: Array.isArray(d.visibilityPhones) + ? (d.visibilityPhones as string[]) + : [], }); setDrawerOpen(true); } @@ -543,6 +634,10 @@ export default function StoresPage() { bankAccountName: v.bankAccountName ?? null, bankAccountNo: v.bankAccountNo ?? null, bankBranch: v.bankBranch ?? null, + visibilityWhitelistEnabled: !!v.visibilityWhitelistEnabled, + visibilityPhones: ((v.visibilityPhones as string[] | undefined) ?? []) + .map((p) => String(p || '').replace(/\D/g, '').trim()) + .filter(Boolean), ...(hasCoords ? { latitude: Number(v.latitude), longitude: Number(v.longitude) } : {}), @@ -632,6 +727,8 @@ export default function StoresPage() { openTime2: undefined, closeTime2: undefined, avgPrice: undefined, + visibilityWhitelistEnabled: false, + visibilityPhones: [], }); setCreateStep(0); setCreateError(''); @@ -713,6 +810,10 @@ export default function StoresPage() { bankAccountNo: values.bankAccountNo.replace(/\s/g, ''), bankBranch: values.bankBranch.trim(), settlementRate: Number(values.settlementRate ?? 60) / 100, + visibilityWhitelistEnabled: !!values.visibilityWhitelistEnabled, + visibilityPhones: (values.visibilityPhones ?? []) + .map((p: string) => String(p || '').replace(/\D/g, '').trim()) + .filter(Boolean), }), }); message.success('门店已创建'); @@ -766,6 +867,13 @@ export default function StoresPage() { }, }, { title: '开城合伙人', dataIndex: ['partner', 'companyName'], width: 120 }, + { + title: '可见', + dataIndex: 'visibilityWhitelistEnabled', + width: 90, + render: (v, row) => + v ? 限{row.visibilityPhones?.length ?? 0}人 : 公开, + }, { title: '介绍', dataIndex: 'intro', width: 160, ellipsis: true, render: (v) => v || '—' }, { title: '店长', dataIndex: ['account', 'name'], width: 90, render: (v) => v || '—' }, { title: '创建', dataIndex: 'createdAt', width: 160, render: fmtTime }, @@ -1059,6 +1167,7 @@ export default function StoresPage() { + ), }, @@ -1279,6 +1388,7 @@ export default function StoresPage() { > +
diff --git a/apps/mini-user/src/lib/stores-session.ts b/apps/mini-user/src/lib/stores-session.ts index fb4c13c..ae62305 100644 --- a/apps/mini-user/src/lib/stores-session.ts +++ b/apps/mini-user/src/lib/stores-session.ts @@ -22,6 +22,8 @@ export type StoresSessionCategory = { export type StoresListCache = { cityKey: string; cityCode: string; + /** 登录态指纹:token 变化时需重新拉取(白名单) */ + authKey: string; listRegion: StoresSessionRegion; items: unknown[]; filterRegion: StoresSessionRegion; diff --git a/apps/mini-user/src/pages/store-detail/index.tsx b/apps/mini-user/src/pages/store-detail/index.tsx index 35ca786..e4cc089 100644 --- a/apps/mini-user/src/pages/store-detail/index.tsx +++ b/apps/mini-user/src/pages/store-detail/index.tsx @@ -1,6 +1,6 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useMemo, useState } from 'react'; import { View, Text, Image } from '@tarojs/components'; -import Taro, { usePageScroll, useRouter, useShareAppMessage, useShareTimeline } from '@tarojs/taro'; +import Taro, { useDidShow, usePageScroll, useRouter, useShareAppMessage, useShareTimeline } from '@tarojs/taro'; import PageShell from '../../components/PageShell'; import PageNavBar from '../../components/PageNavBar'; import ProductCarousel from '../../components/ProductCarousel'; @@ -78,20 +78,15 @@ export default function StoreDetailPage() { setHeaderSolid(scrollTop > 100); }); - useEffect(() => { + useDidShow(() => { if (!storeId) return; request(`/stores/${storeId}`) .then(setStore) .catch(() => { - request('/stores') - .then((list) => { - const found = (Array.isArray(list) ? list : []).find((s) => s.id === storeId); - if (found) setStore(found); - else toast('门店不存在'); - }) - .catch((e) => toast(e instanceof Error ? e.message : '加载失败')); + setStore(null); + toast('门店不存在或暂不可见'); }); - }, [storeId]); + }); const sharePayload = useMemo( () => { diff --git a/apps/mini-user/src/pages/stores/index.tsx b/apps/mini-user/src/pages/stores/index.tsx index 20cd50a..dea186a 100644 --- a/apps/mini-user/src/pages/stores/index.tsx +++ b/apps/mini-user/src/pages/stores/index.tsx @@ -27,7 +27,7 @@ import { } from '../../lib/user-location'; import { FALLBACK_CITY_CODE } from '../../lib/product-images'; import { formatDistanceMeters } from '../../lib/geo'; -import { request, toast } from '../../lib/api'; +import { getToken, request, toast } from '../../lib/api'; import { getStoresListCache, isStoresSessionBootstrapped, @@ -143,6 +143,7 @@ export default function StoresPage() { setStoresListCache({ cityKey, cityCode: nextCode, + authKey: getToken() || '', listRegion: toCityWideRegion(listRegion), items, filterRegion, @@ -160,12 +161,38 @@ export default function StoresPage() { /** * 首次进入:弹窗 + 定位 + 拉列表。 - * 同次再切回:只同步 tab 选中态,不改筛选、不拉接口、不 setState。 + * 同次再切回:只同步 tab 选中态(登录态未变)。 + * 登录/退出后 token 变化:按缓存失效重新拉列表(白名单)。 */ useDidShow(() => { syncTabBarSelected(1); + const authKey = getToken() || ''; + const cache = getStoresListCache(); if (isStoresSessionBootstrapped()) { + if (cache && (cache.authKey ?? '') === authKey) { + return; + } + // 登录态变了:保留筛选,重新拉列表 + void (async () => { + setLoading(true); + const nextCode = cache?.cityCode || FALLBACK_CITY_CODE; + const listRegion = cache?.listRegion + ? { + province: cache.listRegion.province, + city: cache.listRegion.city, + district: cache.listRegion.district || '全部', + } + : regionRef.current; + const nextCityKey = cache?.cityKey || makeCityKey(listRegion); + await fetchStores( + nextCode, + readCachedUserCoords(), + nextCityKey, + listRegion, + regionRef.current, + ); + })(); return; } markStoresSessionBootstrapped(); diff --git a/server/dukang-api/prisma/schema.prisma b/server/dukang-api/prisma/schema.prisma index 78e53ec..8adc5ef 100644 --- a/server/dukang-api/prisma/schema.prisma +++ b/server/dukang-api/prisma/schema.prisma @@ -1062,6 +1062,8 @@ model Store { openTime2 String? @map("open_time_2") @db.VarChar(8) closeTime2 String? @map("close_time_2") @db.VarChar(8) settlementRate Decimal @default(0.60) @map("settlement_rate") @db.Decimal(5, 4) + /// Online test: only listed phones can see store on C-end when enabled + visibilityWhitelistEnabled Boolean @default(false) @map("visibility_whitelist_enabled") createdAt DateTime @default(now()) @map("created_at") @db.DateTime(3) updatedAt DateTime @updatedAt @map("updated_at") @db.DateTime(3) @@ -1075,6 +1077,7 @@ model Store { ratings StoreRating[] payouts StorePayout[] storeBills StoreBill[] + visibilityPhones StoreVisibilityPhone[] @@index([cityId, status]) @@index([partnerAccountId]) @@ -1082,6 +1085,20 @@ model Store { @@map("store_store") } +/// Store visibility whitelist phones (match by bound user phone) +model StoreVisibilityPhone { + id BigInt @id @default(autoincrement()) @db.UnsignedBigInt + storeId BigInt @map("store_id") @db.UnsignedBigInt + phone String @db.VarChar(20) + createdAt DateTime @default(now()) @map("created_at") @db.DateTime(3) + + store Store @relation(fields: [storeId], references: [id], onDelete: Cascade) + + @@unique([storeId, phone]) + @@index([phone]) + @@map("store_visibility_phone") +} + model StoreAccount { id BigInt @id @default(autoincrement()) @db.UnsignedBigInt phone String @unique @db.VarChar(20) diff --git a/server/dukang-api/src/modules/ops/admin-stores.service.ts b/server/dukang-api/src/modules/ops/admin-stores.service.ts index 417fe68..ea5b1a6 100644 --- a/server/dukang-api/src/modules/ops/admin-stores.service.ts +++ b/server/dukang-api/src/modules/ops/admin-stores.service.ts @@ -25,6 +25,24 @@ function normalizeStoreOptionalText(value: unknown): string | null { return s; } +function normalizeVisibilityPhones(phones?: string[]): string[] { + if (!phones?.length) return []; + const out: string[] = []; + const seen = new Set(); + for (const raw of phones) { + const phone = String(raw || '') + .replace(/\D/g, '') + .trim(); + if (!phone || seen.has(phone)) continue; + if (!/^1\d{10}$/.test(phone)) { + throw new BadRequestException(`手机号格式无效:${raw}`); + } + seen.add(phone); + out.push(phone); + } + return out; +} + @Injectable() export class AdminStoresService { constructor( @@ -64,19 +82,23 @@ export class AdminStoresService { }, }, coverResource: { select: { id: true, url: true } }, + visibilityPhones: { select: { phone: true }, orderBy: { phone: 'asc' } }, }, }), this.prisma.store.count({ where }), ]); return serializeBigInt({ - items: items.map((s) => - mapStoreCompat({ - ...s, + items: items.map((s) => { + const { visibilityPhones, ...rest } = s; + return mapStoreCompat({ + ...rest, + visibilityWhitelistEnabled: s.visibilityWhitelistEnabled, + visibilityPhones: visibilityPhones.map((p) => p.phone), partner: s.partnerAccount, account: s.bindings[0]?.storeAccount ?? null, bindings: undefined, - }), - ), + }); + }), total, page, pageSize, @@ -96,6 +118,7 @@ export class AdminStoresService { include: { storeAccount: true }, }, coverResource: true, + visibilityPhones: { select: { phone: true }, orderBy: { phone: 'asc' } }, _count: { select: { redeemRecords: true, ratings: true } }, }, }); @@ -111,8 +134,12 @@ export class AdminStoresService { take: 5, }), ]); + const { visibilityPhones, ...rest } = store; return serializeBigInt(mapStoreCompat({ - ...store, + ...rest, + visibilityWhitelistEnabled: store.visibilityWhitelistEnabled, + visibilityPhones: visibilityPhones.map((p) => p.phone), + partner: store.partnerAccount, account: store.bindings[0]?.storeAccount ?? null, /** 门店端登录手机号(store_account.phone),与 store.phone 应对齐 */ loginPhone: store.bindings[0]?.storeAccount?.phone ?? store.phone, @@ -235,6 +262,26 @@ export class AdminStoresService { } const normalizedPhone = dto.phone !== undefined ? dto.phone.trim() : undefined; + if (dto.visibilityWhitelistEnabled !== undefined || dto.visibilityPhones !== undefined) { + const nextEnabled = + dto.visibilityWhitelistEnabled !== undefined + ? !!dto.visibilityWhitelistEnabled + : current.visibilityWhitelistEnabled; + if (nextEnabled) { + const phones = + dto.visibilityPhones !== undefined + ? normalizeVisibilityPhones(dto.visibilityPhones) + : ( + await this.prisma.storeVisibilityPhone.findMany({ + where: { storeId: id }, + select: { phone: true }, + }) + ).map((p) => p.phone); + if (!phones.length) { + throw new BadRequestException('开启白名单时请至少添加一个手机号'); + } + } + } const bankTouched = dto.bankAccountName !== undefined || dto.bankAccountNo !== undefined || @@ -307,10 +354,23 @@ export class AdminStoresService { ...(dto.openTime2 !== undefined ? { openTime2: dto.openTime2 || null } : {}), ...(dto.closeTime2 !== undefined ? { closeTime2: dto.closeTime2 || null } : {}), ...(dto.avgPrice !== undefined ? { avgPrice: dto.avgPrice } : {}), + ...(dto.visibilityWhitelistEnabled !== undefined + ? { visibilityWhitelistEnabled: !!dto.visibilityWhitelistEnabled } + : {}), ...(latitude != null && longitude != null ? { latitude, longitude } : {}), }, }); + if (dto.visibilityPhones !== undefined) { + const phones = normalizeVisibilityPhones(dto.visibilityPhones); + await tx.storeVisibilityPhone.deleteMany({ where: { storeId: id } }); + if (phones.length) { + await tx.storeVisibilityPhone.createMany({ + data: phones.map((phone) => ({ storeId: id, phone })), + }); + } + } + if (dto.coverUrl) { if (current.coverResourceId) { await tx.commonResource.update({ @@ -430,6 +490,12 @@ export class AdminStoresService { throw new BadRequestException('好客权益券使用规则最多 1000 字'); } + const visibilityPhones = normalizeVisibilityPhones(dto.visibilityPhones); + const whitelistEnabled = !!dto.visibilityWhitelistEnabled; + if (whitelistEnabled && !visibilityPhones.length) { + throw new BadRequestException('开启白名单时请至少添加一个手机号'); + } + const store = await this.prisma.store.create({ data: { cityId: city.id, @@ -449,11 +515,19 @@ export class AdminStoresService { closeTime, openTime2: openTime2 || null, closeTime2: closeTime2 || null, + visibilityWhitelistEnabled: whitelistEnabled, ...(latitude != null && longitude != null ? { latitude, longitude } : {}), status: 'OPEN', auditStatus: 'APPROVED', auditedAt: new Date(), rejectReason: null, + ...(visibilityPhones.length + ? { + visibilityPhones: { + create: visibilityPhones.map((phone) => ({ phone })), + }, + } + : {}), }, }); diff --git a/server/dukang-api/src/modules/ops/dto/admin-mutate.dto.ts b/server/dukang-api/src/modules/ops/dto/admin-mutate.dto.ts index 4ed54c2..6adcf75 100644 --- a/server/dukang-api/src/modules/ops/dto/admin-mutate.dto.ts +++ b/server/dukang-api/src/modules/ops/dto/admin-mutate.dto.ts @@ -127,6 +127,17 @@ export class CreateStoreDto { @IsNumber() @Min(0) avgPrice?: number; + + /** 开启后仅白名单手机号在 C 端可见 */ + @IsOptional() + @IsBoolean() + visibilityWhitelistEnabled?: boolean; + + /** 可见白名单手机号列表 */ + @IsOptional() + @IsArray() + @IsString({ each: true }) + visibilityPhones?: string[]; } export class UpdateStoreDto { @@ -215,6 +226,17 @@ export class UpdateStoreDto { @IsOptional() @IsString() bankBranch?: string | null; + + /** 开启后仅白名单手机号在 C 端可见 */ + @IsOptional() + @IsBoolean() + visibilityWhitelistEnabled?: boolean; + + /** 可见白名单手机号列表 */ + @IsOptional() + @IsArray() + @IsString({ each: true }) + visibilityPhones?: string[]; } export class CreateStoreAccountDto { diff --git a/server/dukang-api/src/modules/store/store.controller.ts b/server/dukang-api/src/modules/store/store.controller.ts index dfc0d0e..4e26add 100644 --- a/server/dukang-api/src/modules/store/store.controller.ts +++ b/server/dukang-api/src/modules/store/store.controller.ts @@ -3,6 +3,7 @@ import { StoreService } from './store.service'; import { StoreCategoryService } from './store-category.service'; import { RedeemService } from '../redeem/redeem.service'; import { JwtAuthGuard, AuthUser } from '../../common/guards/jwt-auth.guard'; +import { OptionalJwtAuthGuard } from '../../common/guards/optional-jwt-auth.guard'; import { PartnerPrimaryGuard } from '../../common/guards/partner-primary.guard'; import { RequirePartnerPermissions } from '../../common/decorators/partner-permission.decorator'; import { PartnerPermissionGuard } from '../../common/guards/partner-permission.guard'; @@ -14,19 +15,29 @@ export class PublicStoreController { constructor(private readonly storeService: StoreService) {} @Get() - list( + @UseGuards(OptionalJwtAuthGuard) + async list( + @CurrentUser() user: AuthUser | undefined, @Query('cityCode') cityCode?: string, @Query('lat') lat?: string, @Query('lng') lng?: string, ) { const userLat = lat != null && lat !== '' ? Number(lat) : undefined; const userLng = lng != null && lng !== '' ? Number(lng) : undefined; - return this.storeService.listOpenStores(cityCode, userLat, userLng); + const viewerPhone = await this.resolveViewerPhone(user); + return this.storeService.listOpenStores(cityCode, userLat, userLng, { phone: viewerPhone }); } @Get(':id') - detail(@Param('id') id: string) { - return this.storeService.getStore(BigInt(id)); + @UseGuards(OptionalJwtAuthGuard) + async detail(@CurrentUser() user: AuthUser | undefined, @Param('id') id: string) { + const viewerPhone = await this.resolveViewerPhone(user); + return this.storeService.getStore(BigInt(id), { phone: viewerPhone }); + } + + private async resolveViewerPhone(user?: AuthUser) { + if (!user || user.actorType !== 'USER') return null; + return this.storeService.resolveUserPhone(user.actorId); } } diff --git a/server/dukang-api/src/modules/store/store.service.ts b/server/dukang-api/src/modules/store/store.service.ts index cb7ae83..2046c8f 100644 --- a/server/dukang-api/src/modules/store/store.service.ts +++ b/server/dukang-api/src/modules/store/store.service.ts @@ -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 { + 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 = { 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,