门店账号管理修改

This commit is contained in:
2026-07-07 00:01:16 +08:00
parent e85c4b9bcb
commit 7ca9fc8a35
25 changed files with 599 additions and 134 deletions
@@ -115,6 +115,17 @@ export class ShopAuthController {
wechatLogin(@Body() dto: LoginWechatDto) {
return this.authService.loginStoreWechat(dto.code, ClientApp.SHOP_H5, dto.platform ?? 'h5');
}
@Post('token/refresh')
refresh(@Body() dto: RefreshTokenDto) {
return this.authService.refreshAccessToken(dto.refreshToken, ClientApp.SHOP_H5);
}
@Get('me')
@UseGuards(JwtAuthGuard)
me(@CurrentUser() user: AuthUser) {
return this.authService.getMe(user.actorType, user.actorId);
}
}
@Controller('partner/auth')
@@ -80,6 +80,8 @@ export class AuthService {
switch (scene) {
case SmsScene.STORE_LOGIN:
return ClientApp.SHOP_H5;
case SmsScene.STORE_ACCOUNT_OPEN:
return ClientApp.HQ_WEB;
case SmsScene.PARTNER_LOGIN:
case SmsScene.PARTNER_STAFF_ADD:
return ClientApp.PARTNER_H5;
@@ -148,6 +150,24 @@ export class AuthService {
});
}
private async assertSmsSendAllowed(phone: string, scene: SmsScene) {
if (scene === SmsScene.STORE_LOGIN) {
const account = await this.prisma.storeAccount.findUnique({ where: { phone } });
if (!account) throw new BadRequestException('该手机号未绑定门店');
if (account.status !== 'ACTIVE') throw new BadRequestException('门店账号已停用');
return;
}
if (scene === SmsScene.STORE_ACCOUNT_OPEN) {
const existing = await this.prisma.storeAccount.findUnique({ where: { phone } });
if (existing) throw new BadRequestException('该手机号已绑定门店');
}
}
async verifySmsCode(phone: string, code: string, scene: SmsScene) {
const normalizedPhone = this.assertMobilePhone(phone);
await this.smsProvider.verify(normalizedPhone, code, scene);
}
private async verifySmsForUser(
phone: string,
code: string,
@@ -178,6 +198,7 @@ export class AuthService {
if (!Object.values(SmsScene).includes(scene as SmsScene)) {
throw new BadRequestException('无效的验证码场景');
}
await this.assertSmsSendAllowed(normalizedPhone, scene as SmsScene);
const clientApp = opts?.clientApp ?? this.clientAppForScene(scene);
const actorRef = await this.resolveSmsActorRef(normalizedPhone, scene, opts?.guestUserId);
const userId = actorRef?.refType === 'USER' ? actorRef.refId : opts?.guestUserId;
@@ -254,17 +275,40 @@ export class AuthService {
async refreshAccessToken(refreshToken: string, clientApp: ClientApp) {
try {
const payload = this.jwtService.verify(refreshToken);
if (payload.clientApp !== clientApp || payload.actorType !== 'USER') {
if (payload.clientApp !== clientApp) {
throw new UnauthorizedException('Invalid refresh token');
}
const user = await this.assertActiveUser(BigInt(payload.actorId));
return this.buildSessionResponse(user, clientApp, user.deviceKey);
if (payload.actorType === 'USER') {
const user = await this.assertActiveUser(BigInt(payload.actorId));
return this.buildSessionResponse(user, clientApp, user.deviceKey);
}
if (payload.actorType === 'STORE' && clientApp === ClientApp.SHOP_H5) {
return this.buildStoreSessionResponse(BigInt(payload.actorId), clientApp);
}
throw new UnauthorizedException('Invalid refresh token');
} catch (err) {
if (err instanceof UnauthorizedException) throw err;
throw new UnauthorizedException('Invalid refresh token');
}
}
private async buildStoreSessionResponse(accountId: bigint, clientApp: ClientApp) {
const account = await this.prisma.storeAccount.findUnique({
where: { id: accountId },
include: { store: true },
});
if (!account || account.status !== 'ACTIVE') {
throw new UnauthorizedException('Invalid refresh token');
}
return this.issueToken('STORE', account.id, clientApp, false, undefined, {
id: account.id.toString(),
storeId: account.storeId.toString(),
name: account.name,
phone: account.phone,
storeName: account.store.name,
});
}
async loginUser(phone: string, code: string, clientApp: ClientApp, guestId?: bigint) {
const normalizedPhone = this.assertMobilePhone(phone);
const existingUser = await this.prisma.user.findUnique({
@@ -396,7 +440,8 @@ export class AuthService {
where: { phone: normalizedPhone },
include: { store: true },
});
if (!account) throw new BadRequestException('门店账号不存在');
if (!account) throw new BadRequestException('该手机号未绑定门店');
if (account.status !== 'ACTIVE') throw new BadRequestException('门店账号已停用');
await this.prisma.storeAccount.update({
where: { id: account.id },
data: { lastLoginAt: new Date() },
@@ -28,6 +28,11 @@ export class AdminStoresController {
return this.service.listStores(query);
}
@Post('phone/sms/send')
sendOpenSms(@Body() body: { phone: string }) {
return this.service.sendStoreOpenSms(body.phone);
}
@Get(':id')
detail(@Param('id') id: string) {
return this.service.detailStore(BigInt(id));
@@ -1,8 +1,10 @@
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { ClientApp, SmsScene } from '@dukang/shared-types';
import { PrismaService } from '../../common/prisma/prisma.module';
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
import { mapStoreCompat } from '../../common/compat/v31-compat';
import { AuthService } from '../iam/auth.service';
import type { AdminStoreAccountsQueryDto, AdminStoreMediaQueryDto, AdminStoresQueryDto } from './dto/admin-query.dto';
import type {
CreateStoreAccountDto,
@@ -16,7 +18,16 @@ import type {
@Injectable()
export class AdminStoresService {
constructor(private readonly prisma: PrismaService) {}
constructor(
private readonly prisma: PrismaService,
private readonly authService: AuthService,
) {}
async sendStoreOpenSms(phone: string) {
return this.authService.sendSms(phone, SmsScene.STORE_ACCOUNT_OPEN, {
clientApp: ClientApp.HQ_WEB,
});
}
async listStores(query: AdminStoresQueryDto) {
const page = query.page ?? 1;
@@ -153,6 +164,16 @@ export class AdminStoresService {
}
async createStore(dto: CreateStoreDto) {
const normalizedPhone = dto.phone.trim();
if (!/^1[3-9]\d{9}$/.test(normalizedPhone)) {
throw new BadRequestException('请输入正确的手机号码');
}
await this.authService.verifySmsCode(normalizedPhone, dto.smsCode, SmsScene.STORE_ACCOUNT_OPEN);
const existingAccount = await this.prisma.storeAccount.findUnique({
where: { phone: normalizedPhone },
});
if (existingAccount) throw new BadRequestException('该手机号已绑定门店');
const partner = await this.prisma.partner.findUnique({ where: { id: BigInt(dto.partnerId) } });
if (!partner) throw new BadRequestException('开城合伙人不存在');
const city = await this.prisma.commonCity.findUnique({ where: { id: BigInt(dto.cityId) } });
@@ -167,7 +188,7 @@ export class AdminStoresService {
partnerId: partner.id,
categoryId: dto.categoryId ? BigInt(dto.categoryId) : null,
name: dto.name,
phone: dto.phone,
phone: normalizedPhone,
province: dto.province ?? city.province,
cityName: dto.city ?? city.name,
district: dto.district ?? '',
@@ -243,7 +264,7 @@ export class AdminStoresService {
await this.prisma.storeAccount.create({
data: {
storeId: store.id,
phone: dto.accountPhone ?? dto.phone,
phone: normalizedPhone,
name: dto.accountName ?? dto.name,
},
});
@@ -24,6 +24,10 @@ export class CreateStoreDto {
@IsNotEmpty()
phone: string;
@IsString()
@IsNotEmpty()
smsCode: string;
@IsOptional()
@IsString()
categoryId?: string;
@@ -52,10 +56,6 @@ export class CreateStoreDto {
@IsString()
coverUrl?: string;
@IsOptional()
@IsString()
accountPhone?: string;
@IsOptional()
@IsString()
accountName?: string;
@@ -83,6 +83,15 @@ export class StoreService {
async createStore(partnerAccountId: bigint, body: Record<string, unknown>) {
const account = await this.getPartnerAccount(partnerAccountId);
const normalizedPhone = String(body.phone).trim();
if (!/^1[3-9]\d{9}$/.test(normalizedPhone)) {
throw new BadRequestException('联系电话须为11位手机号');
}
const existingAccount = await this.prisma.storeAccount.findUnique({
where: { phone: normalizedPhone },
});
if (existingAccount) throw new BadRequestException('该手机号已绑定门店');
const city = await this.resolvePartnerCity(account.partnerId, body.cityId);
const coverUrl = body.coverUrl ? String(body.coverUrl).trim() : '';
const envPhotoUrls = Array.isArray(body.envPhotoUrls)
@@ -96,7 +105,7 @@ export class StoreService {
partnerId: account.partnerId,
categoryId: body.categoryId ? BigInt(String(body.categoryId)) : null,
name: String(body.name),
phone: String(body.phone),
phone: normalizedPhone,
province: String(body.province ?? city.province ?? '河南省'),
cityName: String(body.city ?? city.name ?? '郑州市'),
district: String(body.district ?? ''),
@@ -174,11 +183,8 @@ export class StoreService {
await this.prisma.storeAccount.create({
data: {
storeId: store.id,
phone: await this.resolveStoreAccountPhone(
String(body.accountPhone ?? body.phone),
store.id,
),
name: String(body.accountName ?? body.name),
phone: normalizedPhone,
name: String(body.name),
},
});
@@ -310,13 +316,4 @@ export class StoreService {
if (!city) throw new BadRequestException('合伙人未绑定开城');
return city;
}
private async resolveStoreAccountPhone(phone: string, storeId: bigint): Promise<string> {
const normalized = phone.trim();
const existing = await this.prisma.storeAccount.findUnique({ where: { phone: normalized } });
if (!existing) return normalized;
const suffix = String(storeId).slice(-4);
const candidate = `${normalized.slice(0, 15)}${suffix}`.slice(0, 20);
return candidate;
}
}