feat(store): separate login phone from public contactPhone
Store.phone stays login/boss; contactPhone is C-end dial. Public store APIs keep phone compat via publicDial mapping. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1248,7 +1248,10 @@ model Store {
|
||||
partnerAccountId BigInt @map("partner_account_id") @db.UnsignedBigInt
|
||||
categoryId BigInt? @map("category_id") @db.UnsignedBigInt
|
||||
name String @db.VarChar(128)
|
||||
/// 门店主账号登录手机号(老板);与 StoreAccount.phone 对齐
|
||||
phone String @db.VarChar(20)
|
||||
/// 对外联系电话(店长等);C 端拨号展示用;空则回退 phone
|
||||
contactPhone String? @map("contact_phone") @db.VarChar(20)
|
||||
province String @db.VarChar(32)
|
||||
cityName String @map("city_name") @db.VarChar(32)
|
||||
district String @db.VarChar(32)
|
||||
|
||||
@@ -44,11 +44,36 @@ export function mapOrderCompat<T extends OrderLike & {
|
||||
};
|
||||
}
|
||||
|
||||
export function mapStoreCompat<T extends { coverResource?: { url?: string } | null }>(store: T) {
|
||||
return {
|
||||
/** 对外联系电话;未单独配置时回退登录手机号 */
|
||||
export function resolveStoreContactPhone(store: {
|
||||
phone?: string | null;
|
||||
contactPhone?: string | null;
|
||||
}): string {
|
||||
const contact = String(store.contactPhone ?? '').trim();
|
||||
if (contact) return contact;
|
||||
return String(store.phone ?? '').trim();
|
||||
}
|
||||
|
||||
export function mapStoreCompat<
|
||||
T extends {
|
||||
coverResource?: { url?: string } | null;
|
||||
phone?: string | null;
|
||||
contactPhone?: string | null;
|
||||
},
|
||||
>(store: T, opts?: { /** C 端:phone 字段对外可拨打号码 */ publicDial?: boolean }) {
|
||||
const contactPhone = resolveStoreContactPhone(store) || null;
|
||||
const mapped = {
|
||||
...store,
|
||||
coverUrl: store.coverResource?.url ?? null,
|
||||
contactPhone,
|
||||
};
|
||||
if (opts?.publicDial) {
|
||||
return {
|
||||
...mapped,
|
||||
phone: contactPhone || store.phone || null,
|
||||
};
|
||||
}
|
||||
return mapped;
|
||||
}
|
||||
|
||||
export function mapStatusLogCompat(events: CommonEvent[]) {
|
||||
|
||||
@@ -146,8 +146,10 @@ export class AdminStoresService {
|
||||
visibilityPhones: visibilityPhones.map((p) => p.phone),
|
||||
partner: store.partnerAccount,
|
||||
account: store.bindings[0]?.storeAccount ?? null,
|
||||
/** 门店端登录手机号(store_account.phone),与 store.phone 应对齐 */
|
||||
/** 门店端登录手机号(store_account.phone);与 store.phone 对齐 */
|
||||
loginPhone: store.bindings[0]?.storeAccount?.phone ?? store.phone,
|
||||
/** 对外联系电话;空则回退登录号 */
|
||||
contactPhone: store.contactPhone?.trim() || store.phone,
|
||||
bindings: undefined,
|
||||
media,
|
||||
audits,
|
||||
@@ -275,11 +277,19 @@ export class AdminStoresService {
|
||||
if (dto.phone !== undefined) {
|
||||
const normalizedPhone = dto.phone.trim();
|
||||
if (!/^1[3-9]\d{9}$/.test(normalizedPhone)) {
|
||||
throw new BadRequestException('请输入正确的手机号码');
|
||||
throw new BadRequestException('请输入正确的登录手机号');
|
||||
}
|
||||
}
|
||||
if (dto.contactPhone !== undefined) {
|
||||
const contact = dto.contactPhone.trim();
|
||||
if (contact && !/^1[3-9]\d{9}$/.test(contact)) {
|
||||
throw new BadRequestException('请输入正确的联系电话');
|
||||
}
|
||||
}
|
||||
|
||||
const normalizedPhone = dto.phone !== undefined ? dto.phone.trim() : undefined;
|
||||
const normalizedContactPhone =
|
||||
dto.contactPhone !== undefined ? dto.contactPhone.trim() || null : undefined;
|
||||
if (dto.visibilityWhitelistEnabled !== undefined || dto.visibilityPhones !== undefined) {
|
||||
const nextEnabled =
|
||||
dto.visibilityWhitelistEnabled !== undefined
|
||||
@@ -296,7 +306,7 @@ export class AdminStoresService {
|
||||
const needAccountSync =
|
||||
normalizedPhone !== undefined || dto.name !== undefined || bankTouched;
|
||||
|
||||
// 登录凭证在 store_account.phone;必须与门店展示手机号同步
|
||||
// 登录凭证在 store_account.phone;与 store.phone(老板登录号)同步,不与对外联系电话绑定
|
||||
let primaryBinding: {
|
||||
storeAccount: { id: bigint; phone: string; name: string } | null;
|
||||
} | null = null;
|
||||
@@ -346,6 +356,9 @@ export class AdminStoresService {
|
||||
data: {
|
||||
...(dto.name !== undefined ? { name: dto.name.trim() } : {}),
|
||||
...(normalizedPhone !== undefined ? { phone: normalizedPhone } : {}),
|
||||
...(normalizedContactPhone !== undefined
|
||||
? { contactPhone: normalizedContactPhone }
|
||||
: {}),
|
||||
...(dto.intro !== undefined ? { intro: dto.intro } : {}),
|
||||
...(dto.benefitUsageRule !== undefined
|
||||
? { benefitUsageRule: normalizeStoreOptionalText(dto.benefitUsageRule) }
|
||||
@@ -562,6 +575,11 @@ export class AdminStoresService {
|
||||
? !!dto.isTest
|
||||
: await this.testWhitelist.isPhoneInWhitelist(normalizedPhone);
|
||||
|
||||
const contactPhoneRaw = dto.contactPhone?.trim() || normalizedPhone;
|
||||
if (!/^1[3-9]\d{9}$/.test(contactPhoneRaw)) {
|
||||
throw new BadRequestException('请输入正确的联系电话');
|
||||
}
|
||||
|
||||
const store = await this.prisma.store.create({
|
||||
data: {
|
||||
cityId: city.id,
|
||||
@@ -570,6 +588,7 @@ export class AdminStoresService {
|
||||
categoryId,
|
||||
name: dto.name,
|
||||
phone: normalizedPhone,
|
||||
contactPhone: contactPhoneRaw,
|
||||
province: dto.province ?? city.province,
|
||||
cityName: dto.city ?? city.name,
|
||||
district: dto.district ?? '',
|
||||
|
||||
@@ -38,6 +38,11 @@ export class CreateStoreDto {
|
||||
@IsNotEmpty()
|
||||
phone: string;
|
||||
|
||||
/** 对外联系电话(店长);不传则默认与登录手机号相同 */
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
contactPhone?: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
categoryId: string;
|
||||
@@ -162,6 +167,11 @@ export class UpdateStoreDto {
|
||||
@IsString()
|
||||
phone?: string;
|
||||
|
||||
/** 对外联系电话(店长) */
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
contactPhone?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
intro?: string;
|
||||
|
||||
@@ -179,11 +179,14 @@ export class StoreService {
|
||||
for (const store of visible) {
|
||||
const coords = await this.ensureStoreCoordinates(store);
|
||||
const { visibilityWhitelistEnabled: _wl, ...rest } = store;
|
||||
const mapped = mapStoreCompat({
|
||||
...rest,
|
||||
latitude: coords?.latitude ?? store.latitude,
|
||||
longitude: coords?.longitude ?? store.longitude,
|
||||
});
|
||||
const mapped = mapStoreCompat(
|
||||
{
|
||||
...rest,
|
||||
latitude: coords?.latitude ?? store.latitude,
|
||||
longitude: coords?.longitude ?? store.longitude,
|
||||
},
|
||||
{ publicDial: true },
|
||||
);
|
||||
const distanceMeters =
|
||||
hasUser && coords
|
||||
? Math.round(haversineMeters(userLat!, userLng!, coords.latitude, coords.longitude))
|
||||
@@ -230,29 +233,32 @@ export class StoreService {
|
||||
});
|
||||
const { visibilityWhitelistEnabled: _wl, ...rest } = store;
|
||||
return serializeBigInt(
|
||||
mapStoreCompat({
|
||||
...rest,
|
||||
latitude: coords?.latitude ?? store.latitude,
|
||||
longitude: coords?.longitude ?? store.longitude,
|
||||
media,
|
||||
packages: packageRows.map((p) => {
|
||||
const imageUrls = normalizeStorePackageImageUrls({
|
||||
imageUrl: p.imageUrl,
|
||||
imageUrls: p.imageUrls,
|
||||
});
|
||||
return {
|
||||
name: p.name,
|
||||
price: p.price.toFixed(2),
|
||||
dishes: p.dishes,
|
||||
usableTime: p.usableTime,
|
||||
otherNotes: p.otherNotes,
|
||||
imageUrl: imageUrls[0] ?? null,
|
||||
/** C 端始终返回数组,便于多图轮播 */
|
||||
imageUrls,
|
||||
sortOrder: p.sortOrder,
|
||||
};
|
||||
}),
|
||||
}),
|
||||
mapStoreCompat(
|
||||
{
|
||||
...rest,
|
||||
latitude: coords?.latitude ?? store.latitude,
|
||||
longitude: coords?.longitude ?? store.longitude,
|
||||
media,
|
||||
packages: packageRows.map((p) => {
|
||||
const imageUrls = normalizeStorePackageImageUrls({
|
||||
imageUrl: p.imageUrl,
|
||||
imageUrls: p.imageUrls,
|
||||
});
|
||||
return {
|
||||
name: p.name,
|
||||
price: p.price.toFixed(2),
|
||||
dishes: p.dishes,
|
||||
usableTime: p.usableTime,
|
||||
otherNotes: p.otherNotes,
|
||||
imageUrl: imageUrls[0] ?? null,
|
||||
/** C 端始终返回数组,便于多图轮播 */
|
||||
imageUrls,
|
||||
sortOrder: p.sortOrder,
|
||||
};
|
||||
}),
|
||||
},
|
||||
{ publicDial: true },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -275,7 +281,7 @@ export class StoreService {
|
||||
include: { category: true, coverResource: true },
|
||||
orderBy: [{ sortOrder: 'asc' }, { createdAt: 'desc' }],
|
||||
});
|
||||
return serializeBigInt(stores.map(mapStoreCompat));
|
||||
return serializeBigInt(stores.map((s) => mapStoreCompat(s)));
|
||||
}
|
||||
|
||||
async partnerGetStore(partnerAccountId: bigint, storeId: bigint) {
|
||||
@@ -307,10 +313,10 @@ export class StoreService {
|
||||
async partnerCheckStorePhone(phone: string) {
|
||||
const normalizedPhone = phone.trim();
|
||||
if (!normalizedPhone) {
|
||||
return { available: false, message: '请填写联系电话' };
|
||||
return { available: false, message: '请填写门店登录手机号' };
|
||||
}
|
||||
if (!/^1[3-9]\d{9}$/.test(normalizedPhone)) {
|
||||
return { available: false, message: '联系电话须为11位手机号' };
|
||||
return { available: false, message: '门店登录手机号须为11位手机号' };
|
||||
}
|
||||
const existingAccount = await this.prisma.storeAccount.findUnique({
|
||||
where: { phone: normalizedPhone },
|
||||
@@ -360,6 +366,14 @@ export class StoreService {
|
||||
const confirmBindExisting = body.confirmBindExisting === true || body.confirmBindExisting === 'true';
|
||||
await this.assertStorePhoneAvailable(normalizedPhone, confirmBindExisting);
|
||||
|
||||
const contactPhoneRaw =
|
||||
body.contactPhone != null && String(body.contactPhone).trim()
|
||||
? String(body.contactPhone).trim()
|
||||
: normalizedPhone;
|
||||
if (!/^1[3-9]\d{9}$/.test(contactPhoneRaw)) {
|
||||
throw new BadRequestException('联系电话须为11位手机号');
|
||||
}
|
||||
|
||||
const city = await this.resolvePartnerCity(partnerAccountId, body.cityId);
|
||||
const coverUrl = body.coverUrl ? String(body.coverUrl).trim() : '';
|
||||
const envPhotoUrls = this.normalizeEnvPhotoUrls(body.envPhotoUrls);
|
||||
@@ -416,6 +430,7 @@ export class StoreService {
|
||||
categoryId,
|
||||
name: String(body.name),
|
||||
phone: normalizedPhone,
|
||||
contactPhone: contactPhoneRaw,
|
||||
province: String(body.province ?? city.province ?? '河南省'),
|
||||
cityName: String(body.city ?? city.name ?? '郑州市'),
|
||||
district: String(body.district ?? ''),
|
||||
@@ -614,7 +629,13 @@ export class StoreService {
|
||||
}
|
||||
|
||||
const name = body.name !== undefined ? String(body.name).trim() : undefined;
|
||||
const phone = body.phone !== undefined ? String(body.phone).trim() : undefined;
|
||||
// 合伙人资料修改:联系电话 → contactPhone(兼容旧客户端把 phone 当联系电话提交)
|
||||
const contactPhone =
|
||||
body.contactPhone !== undefined
|
||||
? String(body.contactPhone).trim()
|
||||
: body.phone !== undefined
|
||||
? String(body.phone).trim()
|
||||
: undefined;
|
||||
const address = body.address !== undefined ? String(body.address).trim() : undefined;
|
||||
const introRaw = body.intro !== undefined ? String(body.intro).trim() : undefined;
|
||||
const benefitUsageRuleRaw =
|
||||
@@ -627,7 +648,7 @@ export class StoreService {
|
||||
body.longitude !== undefined ? parseOptionalCoord(body.longitude, 'lng') : undefined;
|
||||
|
||||
if (name !== undefined && !name) throw new BadRequestException('请填写门店名称');
|
||||
if (phone !== undefined && !/^1\d{10}$/.test(phone)) {
|
||||
if (contactPhone !== undefined && !/^1[3-9]\d{9}$/.test(contactPhone)) {
|
||||
throw new BadRequestException('联系电话须为11位手机号');
|
||||
}
|
||||
if (address !== undefined && !address) throw new BadRequestException('请填写详细地址');
|
||||
@@ -650,7 +671,7 @@ export class StoreService {
|
||||
where: { id: storeId },
|
||||
data: {
|
||||
...(name !== undefined ? { name } : {}),
|
||||
...(phone !== undefined ? { phone } : {}),
|
||||
...(contactPhone !== undefined ? { contactPhone } : {}),
|
||||
...(address !== undefined
|
||||
? hasCoordsUpdate
|
||||
? { address }
|
||||
|
||||
Reference in New Issue
Block a user