This commit is contained in:
2026-04-22 00:34:13 +08:00
parent 25db7ecd66
commit 4047f87e8c
36 changed files with 12265 additions and 8193 deletions
+1 -1
View File
@@ -29,7 +29,7 @@
"@nestjs/platform-express": "^11.0.1",
"@nestjs/swagger": "^11.0.3",
"@nestjs/typeorm": "^11.0.0",
"bcrypt": "^5.1.1",
"bcrypt": "^6.0.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"ioredis": "^5.4.2",
+2
View File
@@ -5,6 +5,7 @@ import databaseConfig from './config/database.config';
import jwtConfig from './config/jwt.config';
import redisConfig from './config/redis.config';
import { AuthModule } from './modules/auth/auth.module';
import { SellerAuthModule } from './modules/seller-auth/seller-auth.module';
import { UserModule } from './modules/user/user.module';
import { MerchantModule } from './modules/merchant/merchant.module';
import { RoomModule } from './modules/room/room.module';
@@ -34,6 +35,7 @@ import { AdminAuthModule } from './modules/admin-auth/admin-auth.module';
}),
}),
AuthModule,
SellerAuthModule,
UserModule,
MerchantModule,
RoomModule,
@@ -0,0 +1,7 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const CurrentSeller = createParamDecorator((data: string | undefined, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
const seller = request.seller;
return data ? seller?.[data] : seller;
});
@@ -0,0 +1,42 @@
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { Request } from 'express';
@Injectable()
export class SellerJwtAuthGuard implements CanActivate {
constructor(
private jwtService: JwtService,
private configService: ConfigService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest<Request>();
const token = this.extractToken(request);
if (!token) {
throw new UnauthorizedException('未登录或登录已过期');
}
try {
const payload = await this.jwtService.verifyAsync(token, {
secret: this.configService.get<string>('jwt.secret'),
});
if (payload.role !== 'seller') {
throw new UnauthorizedException('非商家账户');
}
(request as any).seller = payload;
} catch {
throw new UnauthorizedException('登录已过期,请重新登录');
}
return true;
}
private extractToken(request: Request): string | null {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : null;
}
}
+2
View File
@@ -1,6 +1,8 @@
export * from './filters/all-exceptions.filter';
export * from './interceptors/transform.interceptor';
export * from './guards/jwt-auth.guard';
export * from './guards/seller-jwt-auth.guard';
export * from './guards/roles.guard';
export * from './decorators/roles.decorator';
export * from './decorators/current-user.decorator';
export * from './decorators/current-seller.decorator';
+1
View File
@@ -1,4 +1,5 @@
export { User } from './user.entity';
export { Seller } from './seller.entity';
export { Merchant } from './merchant.entity';
export { Room } from './room.entity';
export { RoomCalendar } from './room-calendar.entity';
+8 -8
View File
@@ -1,17 +1,17 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Index, ManyToOne, JoinColumn } from 'typeorm';
import { User } from './user.entity';
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Index, ManyToOne, JoinColumn, OneToOne } from 'typeorm';
import { Seller } from './seller.entity';
@Entity('merchants')
export class Merchant {
@PrimaryGeneratedColumn({ type: 'bigint', unsigned: true })
id: number;
@ManyToOne(() => User)
@JoinColumn({ name: 'user_id' })
user: User;
@OneToOne(() => Seller)
@JoinColumn({ name: 'seller_id' })
seller: Seller;
@Column({ name: 'user_id', type: 'bigint', unsigned: true, unique: true, comment: '关联户ID' })
userId: number;
@Column({ name: 'seller_id', type: 'bigint', unsigned: true, unique: true, comment: '关联商家账户ID' })
sellerId: number;
@Column({ length: 100, comment: '店铺名称' })
shopName: string;
@@ -57,7 +57,7 @@ export class Merchant {
status: 'pending' | 'approved' | 'rejected' | 'frozen';
@Column({ length: 500, nullable: true, comment: '拒绝原因' })
rejectReason: string;
rejectReason?: string;
@Column({ type: 'decimal', precision: 10, scale: 2, unsigned: true, default: 0, comment: '保证金' })
deposit: number;
+39
View File
@@ -0,0 +1,39 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Index, OneToOne } from 'typeorm';
import { Merchant } from './merchant.entity';
@Entity('sellers')
export class Seller {
@PrimaryGeneratedColumn({ type: 'bigint', unsigned: true })
id: number;
@Column({ length: 20, unique: true, comment: '手机号' })
phone: string;
@Column({ length: 255, nullable: true, select: false, comment: '密码(bcrypt哈希)' })
password?: string;
@Column({ length: 50, name: 'contact_name', comment: '联系人姓名' })
contactName: string;
@Column({ length: 100, nullable: true, comment: '邮箱' })
email: string;
@Index()
@Column({ type: 'enum', enum: ['active', 'frozen', 'deleted'], default: 'active', comment: '状态' })
status: 'active' | 'frozen' | 'deleted';
@Column({ type: 'datetime', nullable: true, name: 'last_login_at', comment: '最后登录时间' })
lastLoginAt: Date;
@Column({ length: 50, nullable: true, name: 'last_login_ip', comment: '最后登录IP' })
lastLoginIp: string;
@OneToOne(() => Merchant, merchant => merchant.seller)
merchant: Merchant;
@CreateDateColumn({ comment: '创建时间' })
createdAt: Date;
@UpdateDateColumn({ comment: '更新时间' })
updatedAt: Date;
}
-4
View File
@@ -26,10 +26,6 @@ export class User {
@Column({ length: 255, nullable: true, select: false, comment: '身份证号' })
idCard: string;
@Index()
@Column({ type: 'enum', enum: ['user', 'merchant'], default: 'user', comment: '角色' })
role: 'user' | 'merchant';
@Index()
@Column({ type: 'enum', enum: ['active', 'frozen', 'deleted'], default: 'active', comment: '状态' })
status: 'active' | 'frozen' | 'deleted';
+2 -2
View File
@@ -139,7 +139,7 @@ export class AuthService {
}
private async generateToken(user: User) {
const payload = { sub: user.id, phone: user.phone, role: user.role };
const payload = { sub: user.id, phone: user.phone };
const accessToken = await this.jwtService.signAsync(payload);
const refreshToken = await this.jwtService.signAsync(payload, {
expiresIn: (this.configService.get<string>('jwt.refreshExpiresIn') ||
@@ -154,7 +154,7 @@ export class AuthService {
phone: user.phone,
nickname: user.nickname,
avatar: user.avatar,
role: user.role,
status: user.status,
},
};
}
@@ -40,6 +40,7 @@ export class ApplyMerchantDto {
legalPerson?: string;
@IsOptional()
@IsString()
description?: string;
}
@@ -61,15 +62,19 @@ export class UpdateMerchantDto {
description?: string;
@IsOptional()
@IsString()
province?: string;
@IsOptional()
@IsString()
city?: string;
@IsOptional()
@IsString()
district?: string;
@IsOptional()
@IsString()
address?: string;
}
@@ -1,37 +1,35 @@
import { Controller, Get, Post, Put, Body, Param, Query, UseGuards } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import { MerchantService } from './merchant.service';
import { JwtAuthGuard } from '@/common/guards/jwt-auth.guard';
import { RolesGuard } from '@/common/guards/roles.guard';
import { SellerJwtAuthGuard } from '@/common/guards/seller-jwt-auth.guard';
import { JwtAuthGuard, RolesGuard } from '@/common';
import { Roles } from '@/common/decorators/roles.decorator';
import { CurrentUser } from '@/common/decorators/current-user.decorator';
import { CurrentSeller } from '@/common/decorators/current-seller.decorator';
import { ApplyMerchantDto, UpdateMerchantDto, QueryMerchantDto } from './dto/merchant.dto';
@ApiTags('商家')
@Controller('merchant')
@UseGuards(JwtAuthGuard)
@UseGuards(SellerJwtAuthGuard)
@ApiBearerAuth()
export class MerchantController {
constructor(private readonly merchantService: MerchantService) {}
@Post('apply')
@ApiOperation({ summary: '申请商家入驻' })
async apply(@CurrentUser('sub') userId: number, @Body() dto: ApplyMerchantDto) {
return this.merchantService.apply(userId, dto);
async apply(@CurrentSeller('sub') sellerId: number, @Body() dto: ApplyMerchantDto) {
return this.merchantService.apply(sellerId, dto);
}
@Get('mine')
@ApiOperation({ summary: '获取我的店铺信息' })
async getMine(@CurrentUser('sub') userId: number) {
return this.merchantService.findByUserId(userId);
async getMine(@CurrentSeller('sub') sellerId: number) {
return this.merchantService.findBySellerId(sellerId);
}
@Put('update')
@Roles('merchant')
@UseGuards(RolesGuard)
@ApiOperation({ summary: '更新店铺信息' })
async update(@CurrentUser('sub') userId: number, @Body() dto: UpdateMerchantDto) {
const merchant = await this.merchantService.findByUserId(userId);
async update(@CurrentSeller('sub') sellerId: number, @Body() dto: UpdateMerchantDto) {
const merchant = await this.merchantService.findBySellerId(sellerId);
if (!merchant) throw new Error('店铺不存在');
return this.merchantService.update(merchant.id, dto);
}
@@ -11,18 +11,18 @@ export class MerchantService {
private merchantRepo: Repository<Merchant>,
) {}
async apply(userId: number, dto: ApplyMerchantDto) {
const existing = await this.merchantRepo.findOne({ where: { userId } });
async apply(sellerId: number, dto: ApplyMerchantDto) {
const existing = await this.merchantRepo.findOne({ where: { sellerId } });
if (existing) {
throw new BadRequestException('您已提交过商家申请');
}
const merchant = this.merchantRepo.create({ userId, ...dto });
const merchant = this.merchantRepo.create({ sellerId, ...dto });
return this.merchantRepo.save(merchant);
}
async findByUserId(userId: number) {
return this.merchantRepo.findOne({ where: { userId } });
async findBySellerId(sellerId: number) {
return this.merchantRepo.findOne({ where: { sellerId } });
}
async findById(id: number) {
@@ -32,7 +32,13 @@ export class MerchantService {
}
async update(id: number, dto: UpdateMerchantDto) {
await this.merchantRepo.update(id, dto);
const merchant = await this.findById(id);
// 审核通过或审核拒绝后修改信息,需要重新进入审核
if (merchant.status === 'approved' || merchant.status === 'rejected') {
await this.merchantRepo.update(id, { ...dto, status: 'pending', rejectReason: '' });
} else {
await this.merchantRepo.update(id, dto);
}
return this.findById(id);
}
@@ -1,10 +1,11 @@
import { Controller, Get, Post, Put, Param, Body, Query, UseGuards, NotFoundException } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import { OrderService } from './order.service';
import { JwtAuthGuard } from '@/common/guards/jwt-auth.guard';
import { RolesGuard } from '@/common/guards/roles.guard';
import { JwtAuthGuard, RolesGuard } from '@/common';
import { SellerJwtAuthGuard } from '@/common/guards/seller-jwt-auth.guard';
import { Roles } from '@/common/decorators/roles.decorator';
import { CurrentUser } from '@/common/decorators/current-user.decorator';
import { CurrentSeller } from '@/common/decorators/current-seller.decorator';
import { MerchantService } from '../merchant/merchant.service';
import { CreateOrderDto, QueryOrderDto, ConfirmOrderDto } from './dto/order.dto';
@@ -42,8 +43,7 @@ export class UserOrderController {
@ApiTags('订单管理(商家)')
@Controller('merchant/orders')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('merchant')
@UseGuards(SellerJwtAuthGuard)
@ApiBearerAuth()
export class MerchantOrderController {
constructor(
@@ -53,32 +53,32 @@ export class MerchantOrderController {
@Get()
@ApiOperation({ summary: '商家订单列表' })
async findMine(@CurrentUser('sub') userId: number, @Query() query: QueryOrderDto) {
const merchant = await this.merchantService.findByUserId(userId);
async findMine(@CurrentSeller('sub') sellerId: number, @Query() query: QueryOrderDto) {
const merchant = await this.merchantService.findBySellerId(sellerId);
if (!merchant) throw new NotFoundException('店铺不存在');
return this.orderService.findByMerchant(merchant.id, query);
}
@Put(':id/confirm')
@ApiOperation({ summary: '确认订单' })
async confirm(@CurrentUser('sub') userId: number, @Param('id') id: number) {
const merchant = await this.merchantService.findByUserId(userId);
async confirm(@CurrentSeller('sub') sellerId: number, @Param('id') id: number) {
const merchant = await this.merchantService.findBySellerId(sellerId);
if (!merchant) throw new NotFoundException('店铺不存在');
return this.orderService.confirm(merchant.id, id);
}
@Put(':id/reject')
@ApiOperation({ summary: '拒绝订单' })
async reject(@CurrentUser('sub') userId: number, @Param('id') id: number, @Body('reason') reason: string) {
const merchant = await this.merchantService.findByUserId(userId);
async reject(@CurrentSeller('sub') sellerId: number, @Param('id') id: number, @Body('reason') reason: string) {
const merchant = await this.merchantService.findBySellerId(sellerId);
if (!merchant) throw new NotFoundException('店铺不存在');
return this.orderService.reject(merchant.id, id, reason);
}
@Put(':id/checkin')
@ApiOperation({ summary: '办理入住' })
async checkin(@CurrentUser('sub') userId: number, @Param('id') id: number) {
const merchant = await this.merchantService.findByUserId(userId);
async checkin(@CurrentSeller('sub') sellerId: number, @Param('id') id: number) {
const merchant = await this.merchantService.findBySellerId(sellerId);
if (!merchant) throw new NotFoundException('店铺不存在');
return this.orderService.checkin(merchant.id, id);
}
+11 -14
View File
@@ -12,10 +12,8 @@ import {
} from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import { RoomService } from './room.service';
import { JwtAuthGuard } from '@/common/guards/jwt-auth.guard';
import { RolesGuard } from '@/common/guards/roles.guard';
import { Roles } from '@/common/decorators/roles.decorator';
import { CurrentUser } from '@/common/decorators/current-user.decorator';
import { SellerJwtAuthGuard } from '@/common/guards/seller-jwt-auth.guard';
import { CurrentSeller } from '@/common/decorators/current-seller.decorator';
import { MerchantService } from '../merchant/merchant.service';
import { CreateRoomDto, UpdateRoomDto, QueryRoomDto } from './dto/room.dto';
@@ -39,8 +37,7 @@ export class RoomPublicController {
@ApiTags('房源管理(商家)')
@Controller('merchant/rooms')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('merchant')
@UseGuards(SellerJwtAuthGuard)
@ApiBearerAuth()
export class MerchantRoomController {
constructor(
@@ -51,18 +48,18 @@ export class MerchantRoomController {
@Get()
@ApiOperation({ summary: '我的房源列表' })
async findMine(
@CurrentUser('sub') userId: number,
@CurrentSeller('sub') sellerId: number,
@Query() query: QueryRoomDto,
) {
const merchant = await this.merchantService.findByUserId(userId);
const merchant = await this.merchantService.findBySellerId(sellerId);
if (!merchant) throw new NotFoundException('店铺不存在');
return this.roomService.findByMerchant(merchant.id, query);
}
@Post()
@ApiOperation({ summary: '添加房源' })
async create(@CurrentUser('sub') userId: number, @Body() dto: CreateRoomDto) {
const merchant = await this.merchantService.findByUserId(userId);
async create(@CurrentSeller('sub') sellerId: number, @Body() dto: CreateRoomDto) {
const merchant = await this.merchantService.findBySellerId(sellerId);
if (!merchant) throw new NotFoundException('店铺不存在');
return this.roomService.create(merchant.id, dto);
}
@@ -70,19 +67,19 @@ export class MerchantRoomController {
@Put(':id')
@ApiOperation({ summary: '更新房源' })
async update(
@CurrentUser('sub') userId: number,
@CurrentSeller('sub') sellerId: number,
@Param('id') id: number,
@Body() dto: UpdateRoomDto,
) {
const merchant = await this.merchantService.findByUserId(userId);
const merchant = await this.merchantService.findBySellerId(sellerId);
if (!merchant) throw new NotFoundException('店铺不存在');
return this.roomService.update(id, merchant.id, dto);
}
@Delete(':id')
@ApiOperation({ summary: '下架房源' })
async remove(@CurrentUser('sub') userId: number, @Param('id') id: number) {
const merchant = await this.merchantService.findByUserId(userId);
async remove(@CurrentSeller('sub') sellerId: number, @Param('id') id: number) {
const merchant = await this.merchantService.findBySellerId(sellerId);
if (!merchant) throw new NotFoundException('店铺不存在');
return this.roomService.remove(id, merchant.id);
}
@@ -0,0 +1,55 @@
import { IsString, IsNotEmpty, Length, Matches, IsOptional } from 'class-validator';
// 发送验证码
export class SellerSendCodeDto {
@IsString()
@IsNotEmpty({ message: '手机号不能为空' })
@Matches(/^1[3-9]\d{9}$/, { message: '手机号格式不正确' })
phone: string;
}
// 验证码注册
export class SellerRegisterDto {
@IsString()
@IsNotEmpty({ message: '手机号不能为空' })
@Matches(/^1[3-9]\d{9}$/, { message: '手机号格式不正确' })
phone: string;
@IsString()
@IsNotEmpty({ message: '验证码不能为空' })
@Length(6, 6, { message: '验证码为6位数字' })
code: string;
@IsString()
@IsNotEmpty({ message: '联系人姓名不能为空' })
@Length(2, 50, { message: '联系人姓名长度为2-50位' })
contactName: string;
@IsOptional()
@IsString()
@Matches(/^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/, { message: '邮箱格式不正确' })
email?: string;
@IsOptional()
@IsString()
@Length(6, 20, { message: '密码长度为6-20位' })
password?: string;
}
// 登录(支持验证码或密码)
export class SellerLoginDto {
@IsString()
@IsNotEmpty({ message: '手机号不能为空' })
@Matches(/^1[3-9]\d{9}$/, { message: '手机号格式不正确' })
phone: string;
@IsOptional()
@IsString()
@Length(6, 6, { message: '验证码为6位数字' })
code?: string;
@IsOptional()
@IsString()
@Length(6, 20, { message: '密码长度为6-20位' })
password?: string;
}
@@ -0,0 +1,44 @@
import { Controller, Post, Body, UseGuards, Get } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import { SellerAuthService } from './seller-auth.service';
import { SellerLoginDto, SellerRegisterDto, SellerSendCodeDto } from './dto/seller-auth.dto';
import { SellerJwtAuthGuard } from '@/common/guards/seller-jwt-auth.guard';
import { CurrentSeller } from '@/common/decorators/current-seller.decorator';
@ApiTags('商家认证')
@Controller('seller-auth')
export class SellerAuthController {
constructor(private readonly sellerAuthService: SellerAuthService) {}
@Post('send-code')
@ApiOperation({ summary: '发送商家验证码' })
async sendCode(@Body() dto: SellerSendCodeDto) {
return this.sellerAuthService.sendCode(dto.phone);
}
@Post('login')
@ApiOperation({ summary: '商家登录(验证码或密码)' })
async login(@Body() dto: SellerLoginDto) {
return this.sellerAuthService.login(dto);
}
@Post('register')
@ApiOperation({ summary: '商家注册(验证码)' })
async register(@Body() dto: SellerRegisterDto) {
return this.sellerAuthService.register(dto);
}
@Post('refresh')
@ApiOperation({ summary: '刷新令牌' })
async refresh(@Body('refreshToken') refreshToken: string) {
return this.sellerAuthService.refresh(refreshToken);
}
@Get('profile')
@UseGuards(SellerJwtAuthGuard)
@ApiBearerAuth()
@ApiOperation({ summary: '获取当前商家信息' })
async getProfile(@CurrentSeller('sub') sellerId: number) {
return this.sellerAuthService.findById(sellerId);
}
}
@@ -0,0 +1,30 @@
import { Module, Global } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { SellerAuthController } from './seller-auth.controller';
import { SellerAuthService } from './seller-auth.service';
import { Seller } from '@/entities/seller.entity';
import { Merchant } from '@/entities/merchant.entity';
@Global()
@Module({
imports: [
TypeOrmModule.forFeature([Seller, Merchant]),
JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
secret: configService.get<string>('jwt.secret') || 'dev_secret_key',
signOptions: {
expiresIn: (configService.get<string>('jwt.expiresIn') ||
'2h') as any,
},
}),
inject: [ConfigService],
}),
],
controllers: [SellerAuthController],
providers: [SellerAuthService],
exports: [SellerAuthService, JwtModule],
})
export class SellerAuthModule {}
@@ -0,0 +1,211 @@
import {
Injectable,
UnauthorizedException,
BadRequestException,
Logger,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import * as bcrypt from 'bcrypt';
import { Seller } from '@/entities/seller.entity';
import { Merchant } from '@/entities/merchant.entity';
import { SellerLoginDto, SellerRegisterDto } from './dto/seller-auth.dto';
// 内存缓存(生产环境应替换为 Redis)
const codeCache = new Map<string, { code: string; expiresAt: number }>();
@Injectable()
export class SellerAuthService {
private readonly logger = new Logger(SellerAuthService.name);
private readonly isDev = process.env.NODE_ENV === 'development';
private readonly devCode = '123456';
constructor(
@InjectRepository(Seller)
private sellerRepo: Repository<Seller>,
@InjectRepository(Merchant)
private merchantRepo: Repository<Merchant>,
private jwtService: JwtService,
private configService: ConfigService,
) {}
// 发送验证码
async sendCode(phone: string) {
const code = Math.random().toString().slice(2, 8);
await this.cacheCode(phone, code);
this.logger.log(`商家验证码已发送至 ${phone}: ${code}`);
return { message: '验证码已发送' };
}
// 注册(验证码方式)
async register(dto: SellerRegisterDto) {
// 验证验证码
await this.verifyCode(dto.phone, dto.code);
const existing = await this.sellerRepo.findOne({
where: { phone: dto.phone },
});
if (existing) {
throw new BadRequestException('该手机号已注册');
}
const sellerData: Partial<Seller> = {
phone: dto.phone,
contactName: dto.contactName,
email: dto.email,
};
if (dto.password) {
sellerData.password = await bcrypt.hash(dto.password, 10);
}
const seller = this.sellerRepo.create(sellerData);
await this.sellerRepo.save(seller);
this.clearCachedCode(dto.phone);
return this.generateToken(seller);
}
// 登录(支持验证码或密码)
async login(dto: SellerLoginDto) {
if (!dto.code && !dto.password) {
throw new BadRequestException('请输入验证码或密码');
}
if (dto.code) {
const seller = await this.validateByCode(dto.phone, dto.code);
this.clearCachedCode(dto.phone);
return this.generateToken(seller);
}
const seller = await this.validateByPassword(dto.phone, dto.password!);
return this.generateToken(seller);
}
async refresh(refreshToken: string) {
try {
const payload = await this.jwtService.verifyAsync(refreshToken, {
secret: this.configService.get<string>('jwt.secret'),
});
const seller = await this.sellerRepo.findOne({ where: { id: payload.sub } });
if (!seller) {
throw new UnauthorizedException('商家账户不存在');
}
return this.generateToken(seller);
} catch {
throw new UnauthorizedException('刷新令牌无效');
}
}
async findById(id: number) {
return this.sellerRepo.findOne({ where: { id } });
}
// 验证码验证
private async verifyCode(phone: string, code: string): Promise<void> {
if (this.isDev && code === this.devCode) {
return;
}
const cached = await this.getCachedCode(phone);
if (!cached || cached !== code) {
throw new BadRequestException('验证码错误或已过期');
}
}
// 验证码登录验证
private async validateByCode(phone: string, code: string): Promise<Seller> {
await this.verifyCode(phone, code);
const seller = await this.sellerRepo.findOne({ where: { phone } });
if (!seller) {
throw new UnauthorizedException('商家账户不存在');
}
if (seller.status !== 'active') {
throw new UnauthorizedException('商家账户已被冻结或注销');
}
return seller;
}
// 密码登录验证
private async validateByPassword(phone: string, password: string): Promise<Seller> {
const seller = await this.sellerRepo
.createQueryBuilder('seller')
.where('seller.phone = :phone', { phone })
.addSelect('seller.password')
.getOne();
if (!seller || !seller.password) {
throw new UnauthorizedException('手机号或密码错误');
}
const isMatch = await bcrypt.compare(password, seller.password);
if (!isMatch) {
throw new UnauthorizedException('手机号或密码错误');
}
return seller;
}
// 验证码缓存方法
private async cacheCode(phone: string, code: string): Promise<void> {
// TODO: 生产环境替换为 Redis,设置5分钟过期
const expiresAt = Date.now() + 5 * 60 * 1000;
codeCache.set(phone, { code, expiresAt });
this.logger.debug(`缓存商家验证码: ${phone} -> ${code}`);
}
private async getCachedCode(phone: string): Promise<string | null> {
// TODO: 生产环境替换为 Redis
const cached = codeCache.get(phone);
if (!cached) return null;
if (Date.now() > cached.expiresAt) {
codeCache.delete(phone);
return null;
}
return cached.code;
}
private async clearCachedCode(phone: string): Promise<void> {
codeCache.delete(phone);
this.logger.debug(`清除商家验证码: ${phone}`);
}
private async generateToken(seller: Seller) {
const merchant = await this.merchantRepo.findOne({
where: { sellerId: seller.id },
});
const payload = {
sub: seller.id,
phone: seller.phone,
role: 'seller',
merchantId: merchant?.id,
};
const accessToken = await this.jwtService.signAsync(payload);
const refreshToken = await this.jwtService.signAsync(payload, {
expiresIn: (this.configService.get<string>('jwt.refreshExpiresIn') ||
'7d') as any,
});
return {
accessToken,
refreshToken,
sellerInfo: {
id: seller.id,
phone: seller.phone,
contactName: seller.contactName,
email: seller.email,
status: seller.status,
merchantId: merchant?.id,
merchantStatus: merchant?.status,
},
};
}
}