dev
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user