feat(admin): store detail edit with login phone sync; tighten HQ UI
CI / verify (pull_request) Has been cancelled
CI / verify (pull_request) Has been cancelled
HQ store update now syncs store_account.phone for shop login; expand editable store fields. System version only for SUPER_ADMIN; permissions checklist layout polish. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { Controller, Get, UseGuards } from '@nestjs/common';
|
||||
import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
|
||||
import { SuperAdminGuard } from '../../common/guards/super-admin.guard';
|
||||
import { AdminDashboardService } from './admin-dashboard.service';
|
||||
|
||||
@Controller('admin/dashboard')
|
||||
@@ -13,6 +14,7 @@ export class AdminDashboardController {
|
||||
}
|
||||
|
||||
@Get('version')
|
||||
@UseGuards(SuperAdminGuard)
|
||||
version() {
|
||||
return this.dashboardService.getLatestVersion();
|
||||
}
|
||||
|
||||
@@ -106,6 +106,8 @@ export class AdminStoresService {
|
||||
return serializeBigInt(mapStoreCompat({
|
||||
...store,
|
||||
account: store.bindings[0]?.storeAccount ?? null,
|
||||
/** 门店端登录手机号(store_account.phone),与 store.phone 应对齐 */
|
||||
loginPhone: store.bindings[0]?.storeAccount?.phone ?? store.phone,
|
||||
bindings: undefined,
|
||||
media,
|
||||
audits,
|
||||
@@ -208,49 +210,152 @@ export class AdminStoresService {
|
||||
}
|
||||
}
|
||||
|
||||
const store = await this.prisma.store.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(dto.name !== undefined ? { name: dto.name } : {}),
|
||||
...(dto.phone !== undefined ? { phone: dto.phone } : {}),
|
||||
...(dto.intro !== undefined ? { intro: dto.intro } : {}),
|
||||
...(dto.benefitUsageRule !== undefined
|
||||
? { benefitUsageRule: String(dto.benefitUsageRule).trim() || null }
|
||||
: {}),
|
||||
...(dto.address !== undefined ? { address: dto.address } : {}),
|
||||
...(dto.district !== undefined ? { district: dto.district } : {}),
|
||||
...(dto.settlementRate !== undefined ? { settlementRate: dto.settlementRate } : {}),
|
||||
...(dto.openTime !== undefined ? { openTime: dto.openTime } : {}),
|
||||
...(dto.closeTime !== undefined ? { closeTime: dto.closeTime } : {}),
|
||||
...(dto.openTime2 !== undefined ? { openTime2: dto.openTime2 || null } : {}),
|
||||
...(dto.closeTime2 !== undefined ? { closeTime2: dto.closeTime2 || null } : {}),
|
||||
...(dto.avgPrice !== undefined ? { avgPrice: dto.avgPrice } : {}),
|
||||
...(latitude != null && longitude != null ? { latitude, longitude } : {}),
|
||||
},
|
||||
});
|
||||
let categoryId: bigint | undefined;
|
||||
if (dto.categoryId !== undefined) {
|
||||
if (!dto.categoryId?.trim()) {
|
||||
throw new BadRequestException('请选择门店分类');
|
||||
}
|
||||
categoryId = BigInt(dto.categoryId);
|
||||
await this.storeCategoryService.assertLeafCategoryId(categoryId);
|
||||
}
|
||||
|
||||
if (dto.coverUrl) {
|
||||
if (current.coverResourceId) {
|
||||
await this.prisma.commonResource.update({
|
||||
where: { id: current.coverResourceId },
|
||||
data: { url: dto.coverUrl, ossKey: dto.coverUrl },
|
||||
});
|
||||
} else {
|
||||
const cover = await this.prisma.commonResource.create({
|
||||
data: {
|
||||
ownerType: 'STORE',
|
||||
ownerId: id,
|
||||
bizType: 'COVER',
|
||||
mediaType: 'IMAGE',
|
||||
ossBucket: 'legacy',
|
||||
ossKey: dto.coverUrl,
|
||||
url: dto.coverUrl,
|
||||
},
|
||||
});
|
||||
await this.prisma.store.update({ where: { id }, data: { coverResourceId: cover.id } });
|
||||
if (dto.phone !== undefined) {
|
||||
const normalizedPhone = dto.phone.trim();
|
||||
if (!/^1[3-9]\d{9}$/.test(normalizedPhone)) {
|
||||
throw new BadRequestException('请输入正确的手机号码');
|
||||
}
|
||||
}
|
||||
|
||||
const normalizedPhone = dto.phone !== undefined ? dto.phone.trim() : undefined;
|
||||
const bankTouched =
|
||||
dto.bankAccountName !== undefined ||
|
||||
dto.bankAccountNo !== undefined ||
|
||||
dto.bankBranch !== undefined;
|
||||
const needAccountSync =
|
||||
normalizedPhone !== undefined || dto.name !== undefined || bankTouched;
|
||||
|
||||
// 登录凭证在 store_account.phone;必须与门店展示手机号同步
|
||||
let primaryBinding: {
|
||||
storeAccount: { id: bigint; phone: string; name: string } | null;
|
||||
} | null = null;
|
||||
if (needAccountSync) {
|
||||
primaryBinding = await this.prisma.storeAccountStore.findFirst({
|
||||
where: { storeId: id, storeAccount: { isPrimary: 1 } },
|
||||
include: {
|
||||
storeAccount: { select: { id: true, phone: true, name: true } },
|
||||
},
|
||||
});
|
||||
if (!primaryBinding) {
|
||||
primaryBinding = await this.prisma.storeAccountStore.findFirst({
|
||||
where: { storeId: id },
|
||||
include: {
|
||||
storeAccount: { select: { id: true, phone: true, name: true } },
|
||||
},
|
||||
orderBy: { storeAccountId: 'asc' },
|
||||
});
|
||||
}
|
||||
if (normalizedPhone !== undefined && !primaryBinding?.storeAccount) {
|
||||
throw new BadRequestException('门店未绑定登录账号,无法修改手机号');
|
||||
}
|
||||
const primaryAccount = primaryBinding?.storeAccount;
|
||||
if (
|
||||
normalizedPhone !== undefined &&
|
||||
primaryAccount &&
|
||||
normalizedPhone !== primaryAccount.phone
|
||||
) {
|
||||
const occupied = await this.prisma.storeAccount.findUnique({
|
||||
where: { phone: normalizedPhone },
|
||||
});
|
||||
if (occupied && occupied.id !== primaryAccount.id) {
|
||||
throw new BadRequestException('该手机号已被其他门店账号使用');
|
||||
}
|
||||
}
|
||||
if (dto.bankAccountNo !== undefined) {
|
||||
const no = dto.bankAccountNo?.trim() || null;
|
||||
if (no && !/^\d{16,19}$/.test(no)) {
|
||||
throw new BadRequestException('银行卡号须为 16–19 位数字');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
await tx.store.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(dto.name !== undefined ? { name: dto.name.trim() } : {}),
|
||||
...(normalizedPhone !== undefined ? { phone: normalizedPhone } : {}),
|
||||
...(dto.intro !== undefined ? { intro: dto.intro } : {}),
|
||||
...(dto.benefitUsageRule !== undefined
|
||||
? { benefitUsageRule: String(dto.benefitUsageRule).trim() || null }
|
||||
: {}),
|
||||
...(dto.address !== undefined ? { address: dto.address.trim() } : {}),
|
||||
...(dto.province !== undefined ? { province: dto.province.trim() } : {}),
|
||||
...(dto.city !== undefined ? { cityName: dto.city.trim() } : {}),
|
||||
...(dto.district !== undefined ? { district: dto.district.trim() } : {}),
|
||||
...(categoryId !== undefined ? { categoryId } : {}),
|
||||
...(dto.settlementRate !== undefined ? { settlementRate: dto.settlementRate } : {}),
|
||||
...(dto.openTime !== undefined ? { openTime: dto.openTime } : {}),
|
||||
...(dto.closeTime !== undefined ? { closeTime: dto.closeTime } : {}),
|
||||
...(dto.openTime2 !== undefined ? { openTime2: dto.openTime2 || null } : {}),
|
||||
...(dto.closeTime2 !== undefined ? { closeTime2: dto.closeTime2 || null } : {}),
|
||||
...(dto.avgPrice !== undefined ? { avgPrice: dto.avgPrice } : {}),
|
||||
...(latitude != null && longitude != null ? { latitude, longitude } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
if (dto.coverUrl) {
|
||||
if (current.coverResourceId) {
|
||||
await tx.commonResource.update({
|
||||
where: { id: current.coverResourceId },
|
||||
data: { url: dto.coverUrl, ossKey: dto.coverUrl },
|
||||
});
|
||||
} else {
|
||||
const cover = await tx.commonResource.create({
|
||||
data: {
|
||||
ownerType: 'STORE',
|
||||
ownerId: id,
|
||||
bizType: 'COVER',
|
||||
mediaType: 'IMAGE',
|
||||
ossBucket: 'legacy',
|
||||
ossKey: dto.coverUrl,
|
||||
url: dto.coverUrl,
|
||||
},
|
||||
});
|
||||
await tx.store.update({ where: { id }, data: { coverResourceId: cover.id } });
|
||||
}
|
||||
}
|
||||
|
||||
if (primaryBinding?.storeAccount) {
|
||||
const account = primaryBinding.storeAccount;
|
||||
const accountData: {
|
||||
name?: string;
|
||||
phone?: string;
|
||||
bankAccountName?: string | null;
|
||||
bankAccountNo?: string | null;
|
||||
bankBranch?: string | null;
|
||||
} = {};
|
||||
if (dto.name !== undefined) accountData.name = dto.name.trim();
|
||||
if (normalizedPhone !== undefined && normalizedPhone !== account.phone) {
|
||||
accountData.phone = normalizedPhone;
|
||||
}
|
||||
if (dto.bankAccountName !== undefined) {
|
||||
accountData.bankAccountName = dto.bankAccountName?.trim() || null;
|
||||
}
|
||||
if (dto.bankAccountNo !== undefined) {
|
||||
accountData.bankAccountNo = dto.bankAccountNo?.trim() || null;
|
||||
}
|
||||
if (dto.bankBranch !== undefined) {
|
||||
accountData.bankBranch = dto.bankBranch?.trim() || null;
|
||||
}
|
||||
if (Object.keys(accountData).length) {
|
||||
await tx.storeAccount.update({
|
||||
where: { id: account.id },
|
||||
data: accountData,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return this.detailStore(id);
|
||||
}
|
||||
|
||||
|
||||
@@ -162,10 +162,22 @@ export class UpdateStoreDto {
|
||||
@IsNumber()
|
||||
longitude?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
province?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
city?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
district?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
categoryId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
@@ -191,6 +203,18 @@ export class UpdateStoreDto {
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
avgPrice?: number | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
bankAccountName?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
bankAccountNo?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
bankBranch?: string | null;
|
||||
}
|
||||
|
||||
export class CreateStoreAccountDto {
|
||||
|
||||
Reference in New Issue
Block a user