feat:"完成页面接口的对接"
This commit is contained in:
36
后端/.env
Normal file
36
后端/.env
Normal file
@@ -0,0 +1,36 @@
|
||||
# 数据库配置
|
||||
DB_HOST=8.130.78.179
|
||||
DB_PORT=9986
|
||||
DB_USER=sffx
|
||||
DB_PASS=wF5WKm35Ddm5NDTn
|
||||
DB_NAME=sffx
|
||||
|
||||
# JWT 配置
|
||||
JWT_SECRET=youyijia-sofa-jwt-secret-key-2024
|
||||
JWT_EXPIRES_IN=7d
|
||||
JWT_REFRESH_SECRET=youyijia-sofa-refresh-secret-key-2024
|
||||
JWT_REFRESH_EXPIRES_IN=30d
|
||||
|
||||
# 服务器配置
|
||||
PORT=3000
|
||||
NODE_ENV=development
|
||||
|
||||
# 微信小程序配置
|
||||
WECHAT_APPID=wx89f1cd89fbc55f54
|
||||
WECHAT_SECRET=cb0697a56ab07147285f77ef555d2750
|
||||
|
||||
# 服务器配置
|
||||
PORT=3000
|
||||
|
||||
# 文件上传配置
|
||||
UPLOAD_DIR=uploads
|
||||
MAX_FILE_SIZE=5242880
|
||||
|
||||
# 跨域配置
|
||||
CORS_ORIGIN=http://localhost:8080
|
||||
|
||||
# Redis 配置 (可选)
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=
|
||||
REDIS_DB=0
|
||||
56
后端/.gitignore
vendored
56
后端/.gitignore
vendored
@@ -1,56 +0,0 @@
|
||||
# compiled output
|
||||
/dist
|
||||
/node_modules
|
||||
/build
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
pnpm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
|
||||
# Tests
|
||||
/coverage
|
||||
/.nyc_output
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# temp directory
|
||||
.temp
|
||||
.tmp
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
@@ -13,6 +13,8 @@ import { AuthModule } from './auth/auth.module';
|
||||
import { UserModule } from './user/user.module';
|
||||
import { CaseModule } from './case/case.module';
|
||||
import { ServiceModule } from './service/service.module';
|
||||
import { BookingModule } from './booking/booking.module';
|
||||
import { DatabaseSeederService } from './database/database-seeder.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -41,12 +43,14 @@ import { ServiceModule } from './service/service.module';
|
||||
}),
|
||||
inject: [ConfigService],
|
||||
}),
|
||||
TypeOrmModule.forFeature([User]),
|
||||
AuthModule,
|
||||
UserModule,
|
||||
CaseModule,
|
||||
ServiceModule,
|
||||
BookingModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
providers: [AppService, DatabaseSeederService],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
@@ -12,20 +12,38 @@ export class AuthController {
|
||||
@Public()
|
||||
@Post('register')
|
||||
@ApiOperation({ summary: '用户注册' })
|
||||
@ApiResponse({ status: 201, description: '注册成功' })
|
||||
@ApiResponse({ status: 200, description: '注册成功' })
|
||||
@ApiResponse({ status: 409, description: '用户已存在' })
|
||||
async register(@Body() registerDto: RegisterDto): Promise<AuthResult> {
|
||||
return this.authService.register(registerDto);
|
||||
async register(@Body() registerDto: RegisterDto): Promise<any> {
|
||||
const result = await this.authService.register(registerDto);
|
||||
return {
|
||||
code: 0,
|
||||
message: 'success',
|
||||
data: result,
|
||||
};
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Post('login')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: '用户登录' })
|
||||
@ApiResponse({ status: 200, description: '登录成功' })
|
||||
@ApiResponse({ status: 401, description: '用户名或密码错误' })
|
||||
async login(@Body() loginDto: LoginDto): Promise<AuthResult> {
|
||||
return this.authService.login(loginDto);
|
||||
@ApiResponse({ status: 200, description: '登录成功或失败(通过 code 字段区分)' })
|
||||
async login(@Body() loginDto: LoginDto): Promise<any> {
|
||||
try {
|
||||
const result = await this.authService.login(loginDto);
|
||||
return {
|
||||
code: 0,
|
||||
message: '登录成功',
|
||||
data: result,
|
||||
};
|
||||
} catch (error) {
|
||||
// 登录失败返回统一格式,不抛出 HTTP 异常
|
||||
return {
|
||||
code: 401,
|
||||
message: error.message || '用户名或密码错误',
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@Public()
|
||||
@@ -35,7 +53,12 @@ export class AuthController {
|
||||
@ApiResponse({ status: 200, description: '刷新成功' })
|
||||
@ApiResponse({ status: 401, description: '刷新令牌无效' })
|
||||
async refreshToken(@Body('refresh_token') refreshToken: string) {
|
||||
return this.authService.refreshToken(refreshToken);
|
||||
const result = await this.authService.refreshToken(refreshToken);
|
||||
return {
|
||||
code: 0,
|
||||
message: 'success',
|
||||
data: result,
|
||||
};
|
||||
}
|
||||
|
||||
@Public()
|
||||
@@ -44,10 +67,17 @@ export class AuthController {
|
||||
@ApiOperation({ summary: '微信小程序登录' })
|
||||
@ApiResponse({ status: 200, description: '登录成功' })
|
||||
@ApiResponse({ status: 400, description: '微信登录失败' })
|
||||
async wechatLogin(
|
||||
@Body() loginData: { wechatLogin: WechatLoginDto; userInfo?: WechatUserInfoDto }
|
||||
): Promise<AuthResult> {
|
||||
const { wechatLogin, userInfo } = loginData;
|
||||
return this.authService.wechatLogin(wechatLogin, userInfo);
|
||||
async wechatLogin(@Body() body: any): Promise<any> {
|
||||
// 支持两种格式:
|
||||
// 1. { wechatLogin: { code, ... }, userInfo: {...} }
|
||||
// 2. { code, encryptedData, iv, signature }
|
||||
const wechatLogin: WechatLoginDto = body?.wechatLogin ?? body;
|
||||
const userInfo: WechatUserInfoDto | undefined = body?.userInfo;
|
||||
const result = await this.authService.wechatLogin(wechatLogin, userInfo);
|
||||
return {
|
||||
code: 0,
|
||||
message: 'success',
|
||||
data: result,
|
||||
};
|
||||
}
|
||||
}
|
||||
86
后端/src/booking/booking.controller.ts
Normal file
86
后端/src/booking/booking.controller.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { Controller, Get, Post, Body, Patch, Param, Delete, Query, UseGuards, HttpCode } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery } from '@nestjs/swagger';
|
||||
import { BookingService } from './booking.service';
|
||||
import { CreateBookingDto, UpdateBookingDto, QueryBookingDto } from './dto/booking.dto';
|
||||
import type { CurrentUserData } from '../auth/decorators/current-user.decorator';
|
||||
import { CurrentUser } from '../auth/decorators/current-user.decorator';
|
||||
import { Roles } from '../auth/guards/roles.decorator';
|
||||
import { Public } from '../auth/guards/public.decorator';
|
||||
|
||||
@ApiTags('预约管理')
|
||||
@ApiBearerAuth()
|
||||
@Controller('booking')
|
||||
export class BookingController {
|
||||
constructor(private readonly bookingService: BookingService) {}
|
||||
|
||||
@Post()
|
||||
@HttpCode(200) // 返回200而不是默认的201
|
||||
@Public() // 允许公开访问
|
||||
@ApiOperation({ summary: '创建预约' })
|
||||
@ApiResponse({ status: 200, description: '预约成功' })
|
||||
@ApiResponse({ status: 400, description: '服务不存在或不可用' })
|
||||
create(@Body() createBookingDto: CreateBookingDto) {
|
||||
// 暂时不需要userId,后续可以在小程序登录后传入
|
||||
return this.bookingService.create(createBookingDto, null);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@Roles('admin', 'worker')
|
||||
@ApiOperation({ summary: '获取所有预约(管理员/工人)' })
|
||||
@ApiQuery({ name: 'status', required: false, enum: ['pending', 'confirmed', 'in_progress', 'completed', 'cancelled'] })
|
||||
@ApiQuery({ name: 'page', required: false, type: Number })
|
||||
@ApiQuery({ name: 'limit', required: false, type: Number })
|
||||
@ApiResponse({ status: 200, description: '获取成功' })
|
||||
findAll(@Query() query: QueryBookingDto) {
|
||||
return this.bookingService.findAll(query);
|
||||
}
|
||||
|
||||
@Get('my')
|
||||
@ApiOperation({ summary: '获取我的预约列表' })
|
||||
@ApiQuery({ name: 'status', required: false, enum: ['pending', 'confirmed', 'in_progress', 'completed', 'cancelled'] })
|
||||
@ApiQuery({ name: 'page', required: false, type: Number })
|
||||
@ApiQuery({ name: 'limit', required: false, type: Number })
|
||||
@ApiResponse({ status: 200, description: '获取成功' })
|
||||
getMyBookings(@Query() query: QueryBookingDto, @CurrentUser() user: CurrentUserData) {
|
||||
return this.bookingService.getMyBookings(user.userId, query);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: '根据ID获取预约详情' })
|
||||
@ApiResponse({ status: 200, description: '获取成功' })
|
||||
@ApiResponse({ status: 404, description: '预约不存在' })
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.bookingService.findOne(+id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@ApiOperation({ summary: '更新预约信息' })
|
||||
@ApiResponse({ status: 200, description: '更新成功' })
|
||||
@ApiResponse({ status: 400, description: '无权限或状态不允许' })
|
||||
@ApiResponse({ status: 404, description: '预约不存在' })
|
||||
update(
|
||||
@Param('id') id: string,
|
||||
@Body() updateBookingDto: UpdateBookingDto,
|
||||
@CurrentUser() user: CurrentUserData
|
||||
) {
|
||||
return this.bookingService.update(+id, updateBookingDto, user.userId, user.role);
|
||||
}
|
||||
|
||||
@Post(':id/cancel')
|
||||
@ApiOperation({ summary: '取消预约' })
|
||||
@ApiResponse({ status: 200, description: '取消成功' })
|
||||
@ApiResponse({ status: 400, description: '无权限或状态不允许' })
|
||||
@ApiResponse({ status: 404, description: '预约不存在' })
|
||||
cancel(@Param('id') id: string, @CurrentUser() user: CurrentUserData) {
|
||||
return this.bookingService.cancel(+id, user.userId, user.role);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@Roles('admin')
|
||||
@ApiOperation({ summary: '删除预约(管理员)' })
|
||||
@ApiResponse({ status: 200, description: '删除成功' })
|
||||
@ApiResponse({ status: 404, description: '预约不存在' })
|
||||
remove(@Param('id') id: string) {
|
||||
return this.bookingService.remove(+id);
|
||||
}
|
||||
}
|
||||
14
后端/src/booking/booking.module.ts
Normal file
14
后端/src/booking/booking.module.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { BookingController } from './booking.controller';
|
||||
import { BookingService } from './booking.service';
|
||||
import { Booking } from '../entities/booking.entity';
|
||||
import { Service } from '../entities/service.entity';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Booking, Service])],
|
||||
controllers: [BookingController],
|
||||
providers: [BookingService],
|
||||
exports: [BookingService],
|
||||
})
|
||||
export class BookingModule {}
|
||||
210
后端/src/booking/booking.service.ts
Normal file
210
后端/src/booking/booking.service.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Booking } from '../entities/booking.entity';
|
||||
import { Service } from '../entities/service.entity';
|
||||
import { CreateBookingDto, UpdateBookingDto, QueryBookingDto } from './dto/booking.dto';
|
||||
|
||||
@Injectable()
|
||||
export class BookingService {
|
||||
constructor(
|
||||
@InjectRepository(Booking)
|
||||
private bookingRepository: Repository<Booking>,
|
||||
@InjectRepository(Service)
|
||||
private serviceRepository: Repository<Service>,
|
||||
) {}
|
||||
|
||||
// 生成预约编号
|
||||
private generateBookingNumber(): string {
|
||||
const date = new Date();
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const random = Math.random().toString(36).substring(2, 8).toUpperCase();
|
||||
return `BK${year}${month}${day}${random}`;
|
||||
}
|
||||
|
||||
async create(createBookingDto: CreateBookingDto, userId: number | null) {
|
||||
// 验证服务是否存在
|
||||
const service = await this.serviceRepository.findOne({
|
||||
where: { id: createBookingDto.serviceId }
|
||||
});
|
||||
|
||||
if (!service) {
|
||||
throw new BadRequestException('服务不存在');
|
||||
}
|
||||
|
||||
if (service.status !== 'active') {
|
||||
throw new BadRequestException('该服务暂不可用');
|
||||
}
|
||||
|
||||
const booking = this.bookingRepository.create({
|
||||
...createBookingDto,
|
||||
customerId: userId ?? undefined, // 将null转为undefined
|
||||
bookingNumber: this.generateBookingNumber(),
|
||||
status: 'pending',
|
||||
});
|
||||
|
||||
const savedBooking = await this.bookingRepository.save(booking);
|
||||
|
||||
return {
|
||||
code: 0,
|
||||
message: '预约成功',
|
||||
data: savedBooking,
|
||||
};
|
||||
}
|
||||
|
||||
async findAll(query: QueryBookingDto) {
|
||||
const { status, page = 1, limit = 10 } = query;
|
||||
|
||||
const queryBuilder = this.bookingRepository
|
||||
.createQueryBuilder('booking')
|
||||
.leftJoinAndSelect('booking.customer', 'customer')
|
||||
.leftJoinAndSelect('booking.service', 'service')
|
||||
.leftJoinAndSelect('booking.assignedWorker', 'worker')
|
||||
.orderBy('booking.createdAt', 'DESC');
|
||||
|
||||
if (status) {
|
||||
queryBuilder.andWhere('booking.status = :status', { status });
|
||||
}
|
||||
|
||||
const total = await queryBuilder.getCount();
|
||||
const bookings = await queryBuilder
|
||||
.skip((page - 1) * limit)
|
||||
.take(limit)
|
||||
.getMany();
|
||||
|
||||
return {
|
||||
code: 0,
|
||||
message: '获取成功',
|
||||
data: {
|
||||
list: bookings,
|
||||
total,
|
||||
page,
|
||||
pageSize: limit,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async getMyBookings(userId: number, query: QueryBookingDto) {
|
||||
const { status, page = 1, limit = 10 } = query;
|
||||
|
||||
const queryBuilder = this.bookingRepository
|
||||
.createQueryBuilder('booking')
|
||||
.leftJoinAndSelect('booking.service', 'service')
|
||||
.leftJoinAndSelect('booking.assignedWorker', 'worker')
|
||||
.where('booking.customerId = :userId', { userId })
|
||||
.orderBy('booking.createdAt', 'DESC');
|
||||
|
||||
if (status) {
|
||||
queryBuilder.andWhere('booking.status = :status', { status });
|
||||
}
|
||||
|
||||
const total = await queryBuilder.getCount();
|
||||
const bookings = await queryBuilder
|
||||
.skip((page - 1) * limit)
|
||||
.take(limit)
|
||||
.getMany();
|
||||
|
||||
return {
|
||||
code: 0,
|
||||
message: '获取成功',
|
||||
data: {
|
||||
list: bookings,
|
||||
total,
|
||||
page,
|
||||
pageSize: limit,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async findOne(id: number) {
|
||||
const booking = await this.bookingRepository.findOne({
|
||||
where: { id },
|
||||
relations: ['customer', 'service', 'assignedWorker'],
|
||||
});
|
||||
|
||||
if (!booking) {
|
||||
throw new NotFoundException('预约不存在');
|
||||
}
|
||||
|
||||
return {
|
||||
code: 0,
|
||||
message: '获取成功',
|
||||
data: booking,
|
||||
};
|
||||
}
|
||||
|
||||
async update(id: number, updateBookingDto: UpdateBookingDto, userId: number, userRole: string) {
|
||||
const booking = await this.bookingRepository.findOne({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!booking) {
|
||||
throw new NotFoundException('预约不存在');
|
||||
}
|
||||
|
||||
// 普通用户只能更新自己的预约
|
||||
if (userRole === 'customer' && booking.customerId !== userId) {
|
||||
throw new BadRequestException('无权限更新此预约');
|
||||
}
|
||||
|
||||
// 客户只能在待确认状态下更新部分信息
|
||||
if (userRole === 'customer' && booking.status !== 'pending') {
|
||||
throw new BadRequestException('预约已确认,无法修改');
|
||||
}
|
||||
|
||||
Object.assign(booking, updateBookingDto);
|
||||
const updatedBooking = await this.bookingRepository.save(booking);
|
||||
|
||||
return {
|
||||
code: 0,
|
||||
message: '更新成功',
|
||||
data: updatedBooking,
|
||||
};
|
||||
}
|
||||
|
||||
async cancel(id: number, userId: number, userRole: string) {
|
||||
const booking = await this.bookingRepository.findOne({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!booking) {
|
||||
throw new NotFoundException('预约不存在');
|
||||
}
|
||||
|
||||
// 普通用户只能取消自己的预约
|
||||
if (userRole === 'customer' && booking.customerId !== userId) {
|
||||
throw new BadRequestException('无权限取消此预约');
|
||||
}
|
||||
|
||||
if (booking.status === 'completed' || booking.status === 'cancelled') {
|
||||
throw new BadRequestException('预约已完成或已取消,无法取消');
|
||||
}
|
||||
|
||||
booking.status = 'cancelled';
|
||||
await this.bookingRepository.save(booking);
|
||||
|
||||
return {
|
||||
code: 0,
|
||||
message: '取消成功',
|
||||
};
|
||||
}
|
||||
|
||||
async remove(id: number) {
|
||||
const booking = await this.bookingRepository.findOne({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!booking) {
|
||||
throw new NotFoundException('预约不存在');
|
||||
}
|
||||
|
||||
await this.bookingRepository.remove(booking);
|
||||
|
||||
return {
|
||||
code: 0,
|
||||
message: '删除成功',
|
||||
};
|
||||
}
|
||||
}
|
||||
117
后端/src/booking/dto/booking.dto.ts
Normal file
117
后端/src/booking/dto/booking.dto.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { IsString, IsNotEmpty, IsOptional, IsNumber, IsEnum, IsDateString, IsArray } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
export class CreateBookingDto {
|
||||
@ApiProperty({ description: '服务ID' })
|
||||
@IsNumber()
|
||||
@IsNotEmpty()
|
||||
serviceId: number;
|
||||
|
||||
@ApiProperty({ description: '联系人姓名' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
contactName: string;
|
||||
|
||||
@ApiProperty({ description: '联系电话' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
contactPhone: string;
|
||||
|
||||
@ApiProperty({ description: '服务地址' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
address: string;
|
||||
|
||||
@ApiProperty({ description: '预约时间' })
|
||||
@IsDateString()
|
||||
@IsNotEmpty()
|
||||
appointmentTime: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '特殊要求' })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
requirements?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '沙发现状图片', type: [String] })
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
images?: string[];
|
||||
}
|
||||
|
||||
export class UpdateBookingDto {
|
||||
@ApiPropertyOptional({ description: '联系人姓名' })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
contactName?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '联系电话' })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
contactPhone?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '服务地址' })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
address?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '预约时间' })
|
||||
@IsDateString()
|
||||
@IsOptional()
|
||||
appointmentTime?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '特殊要求' })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
requirements?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '沙发现状图片', type: [String] })
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
images?: string[];
|
||||
|
||||
@ApiPropertyOptional({ description: '状态', enum: ['pending', 'confirmed', 'in_progress', 'completed', 'cancelled'] })
|
||||
@IsEnum(['pending', 'confirmed', 'in_progress', 'completed', 'cancelled'])
|
||||
@IsOptional()
|
||||
status?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '报价' })
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
quotedPrice?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: '最终价格' })
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
finalPrice?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: '备注' })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
notes?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '分配工人ID' })
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
assignedWorkerId?: number;
|
||||
}
|
||||
|
||||
export class QueryBookingDto {
|
||||
@ApiPropertyOptional({ description: '状态', enum: ['pending', 'confirmed', 'in_progress', 'completed', 'cancelled'] })
|
||||
@IsEnum(['pending', 'confirmed', 'in_progress', 'completed', 'cancelled'])
|
||||
@IsOptional()
|
||||
status?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '页码', default: 1 })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
page?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: '每页数量', default: 10 })
|
||||
@ApiPropertyOptional({ description: '每页数量', default: 10 })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
limit?: number;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Controller, Get, Post, Body, Patch, Param, Delete, Query, UseGuards } from '@nestjs/common';
|
||||
import { Controller, Get, Post, Body, Patch, Param, Delete, Query, UseGuards, HttpCode } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery } from '@nestjs/swagger';
|
||||
import { CaseService } from './case.service';
|
||||
import { CreateCaseDto, UpdateCaseDto, QueryCaseDto } from './dto/case.dto';
|
||||
@@ -14,35 +14,52 @@ export class CaseController {
|
||||
|
||||
@ApiBearerAuth()
|
||||
@Post()
|
||||
@HttpCode(200)
|
||||
@Roles('admin', 'worker')
|
||||
@ApiOperation({ summary: '创建案例' })
|
||||
@ApiResponse({ status: 201, description: '创建成功' })
|
||||
create(@Body() createCaseDto: CreateCaseDto, @CurrentUser() user: CurrentUserData) {
|
||||
return this.caseService.create(createCaseDto, user.userId);
|
||||
@ApiResponse({ status: 200, description: '创建成功' })
|
||||
async create(@Body() createCaseDto: CreateCaseDto, @CurrentUser() user: CurrentUserData) {
|
||||
const data = await this.caseService.create(createCaseDto, user.userId);
|
||||
return {
|
||||
code: 0,
|
||||
message: '创建成功',
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Get()
|
||||
@ApiOperation({ summary: '获取案例列表' })
|
||||
@ApiQuery({ name: 'serviceType', required: false, enum: ['fabric', 'leather', 'cleaning', 'repair', 'custom'] })
|
||||
@ApiQuery({ name: 'serviceType', required: false, enum: ['leather', 'fabric', 'functional', 'antique', 'office', 'cleaning', 'repair', 'custom'] })
|
||||
@ApiQuery({ name: 'status', required: false, enum: ['draft', 'published', 'archived'], description: '默认为published' })
|
||||
@ApiQuery({ name: 'page', required: false, type: Number, description: '页码,默认为1' })
|
||||
@ApiQuery({ name: 'limit', required: false, type: Number, description: '每页数量,默认为10' })
|
||||
@ApiResponse({ status: 200, description: '获取成功' })
|
||||
findAll(@Query() query: QueryCaseDto) {
|
||||
return this.caseService.findAll(query);
|
||||
async findAll(@Query() query: QueryCaseDto) {
|
||||
const result = await this.caseService.findAll(query);
|
||||
return {
|
||||
code: 0,
|
||||
message: '获取成功',
|
||||
data: result,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ApiBearerAuth()
|
||||
@Get('my')
|
||||
@ApiOperation({ summary: '获取我的案例列表' })
|
||||
@ApiQuery({ name: 'serviceType', required: false, enum: ['fabric', 'leather', 'cleaning', 'repair', 'custom'] })
|
||||
@ApiQuery({ name: 'serviceType', required: false, enum: ['leather', 'fabric', 'functional', 'antique', 'office', 'cleaning', 'repair', 'custom'] })
|
||||
@ApiQuery({ name: 'status', required: false, enum: ['draft', 'published', 'archived'] })
|
||||
@ApiQuery({ name: 'page', required: false, type: Number })
|
||||
@ApiQuery({ name: 'limit', required: false, type: Number })
|
||||
@ApiResponse({ status: 200, description: '获取成功' })
|
||||
getMyCases(@Query() query: QueryCaseDto, @CurrentUser() user: CurrentUserData) {
|
||||
return this.caseService.getMyCases(user.userId, query);
|
||||
async getMyCases(@Query() query: QueryCaseDto, @CurrentUser() user: CurrentUserData) {
|
||||
const data = await this.caseService.getMyCases(user.userId, query);
|
||||
return {
|
||||
code: 0,
|
||||
message: '获取成功',
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
@Public()
|
||||
@@ -50,8 +67,13 @@ export class CaseController {
|
||||
@ApiOperation({ summary: '根据ID获取案例详情' })
|
||||
@ApiResponse({ status: 200, description: '获取成功' })
|
||||
@ApiResponse({ status: 404, description: '案例不存在' })
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.caseService.findOne(+id);
|
||||
async findOne(@Param('id') id: string) {
|
||||
const data = await this.caseService.findOne(+id);
|
||||
return {
|
||||
code: 0,
|
||||
message: '获取成功',
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
@ApiBearerAuth()
|
||||
@@ -60,12 +82,17 @@ export class CaseController {
|
||||
@ApiResponse({ status: 200, description: '更新成功' })
|
||||
@ApiResponse({ status: 404, description: '案例不存在' })
|
||||
@ApiResponse({ status: 403, description: '没有权限' })
|
||||
update(
|
||||
async update(
|
||||
@Param('id') id: string,
|
||||
@Body() updateCaseDto: UpdateCaseDto,
|
||||
@CurrentUser() user: CurrentUserData
|
||||
) {
|
||||
return this.caseService.update(+id, updateCaseDto, user.userId, user.role);
|
||||
const data = await this.caseService.update(+id, updateCaseDto, user.userId, user.role);
|
||||
return {
|
||||
code: 0,
|
||||
message: '更新成功',
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
@ApiBearerAuth()
|
||||
@@ -74,8 +101,12 @@ export class CaseController {
|
||||
@ApiResponse({ status: 200, description: '删除成功' })
|
||||
@ApiResponse({ status: 404, description: '案例不存在' })
|
||||
@ApiResponse({ status: 403, description: '没有权限' })
|
||||
remove(@Param('id') id: string, @CurrentUser() user: CurrentUserData) {
|
||||
return this.caseService.remove(+id, user.userId, user.role);
|
||||
async remove(@Param('id') id: string, @CurrentUser() user: CurrentUserData) {
|
||||
await this.caseService.remove(+id, user.userId, user.role);
|
||||
return {
|
||||
code: 0,
|
||||
message: '删除成功',
|
||||
};
|
||||
}
|
||||
|
||||
@Public()
|
||||
@@ -83,7 +114,12 @@ export class CaseController {
|
||||
@ApiOperation({ summary: '点赞案例' })
|
||||
@ApiResponse({ status: 200, description: '点赞成功' })
|
||||
@ApiResponse({ status: 404, description: '案例不存在' })
|
||||
like(@Param('id') id: string) {
|
||||
return this.caseService.like(+id);
|
||||
async like(@Param('id') id: string) {
|
||||
const data = await this.caseService.like(+id);
|
||||
return {
|
||||
code: 0,
|
||||
message: '点赞成功',
|
||||
data,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -29,12 +29,15 @@ export class CaseService {
|
||||
'case.id',
|
||||
'case.title',
|
||||
'case.description',
|
||||
'case.images',
|
||||
'case.beforeImages',
|
||||
'case.afterImages',
|
||||
'case.serviceType',
|
||||
'case.location',
|
||||
'case.price',
|
||||
'case.materials',
|
||||
'case.duration',
|
||||
'case.tags',
|
||||
'case.status',
|
||||
'case.views',
|
||||
'case.likes',
|
||||
@@ -57,18 +60,17 @@ export class CaseService {
|
||||
queryBuilder.skip(skip).take(limit);
|
||||
queryBuilder.orderBy('case.createdAt', 'DESC');
|
||||
|
||||
const [items, total] = await queryBuilder.getManyAndCount();
|
||||
const [list, total] = await queryBuilder.getManyAndCount();
|
||||
|
||||
return {
|
||||
items,
|
||||
list,
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
pageSize: limit,
|
||||
};
|
||||
}
|
||||
|
||||
async findOne(id: number): Promise<Case> {
|
||||
async findOne(id: number): Promise<any> {
|
||||
const caseEntity = await this.caseRepository
|
||||
.createQueryBuilder('case')
|
||||
.leftJoinAndSelect('case.creator', 'creator')
|
||||
@@ -76,12 +78,15 @@ export class CaseService {
|
||||
'case.id',
|
||||
'case.title',
|
||||
'case.description',
|
||||
'case.images',
|
||||
'case.beforeImages',
|
||||
'case.afterImages',
|
||||
'case.serviceType',
|
||||
'case.location',
|
||||
'case.price',
|
||||
'case.materials',
|
||||
'case.duration',
|
||||
'case.tags',
|
||||
'case.status',
|
||||
'case.views',
|
||||
'case.likes',
|
||||
@@ -120,7 +125,16 @@ export class CaseService {
|
||||
}
|
||||
|
||||
await this.caseRepository.update(id, updateCaseDto);
|
||||
return this.findOne(id);
|
||||
const updated = await this.caseRepository.findOne({
|
||||
where: { id },
|
||||
relations: ['creator'],
|
||||
});
|
||||
|
||||
if (!updated) {
|
||||
throw new NotFoundException('更新后案例不存在');
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
async remove(id: number, userId: number, userRole: string): Promise<void> {
|
||||
@@ -166,12 +180,15 @@ export class CaseService {
|
||||
'case.id',
|
||||
'case.title',
|
||||
'case.description',
|
||||
'case.images',
|
||||
'case.beforeImages',
|
||||
'case.afterImages',
|
||||
'case.serviceType',
|
||||
'case.location',
|
||||
'case.price',
|
||||
'case.materials',
|
||||
'case.duration',
|
||||
'case.tags',
|
||||
'case.status',
|
||||
'case.views',
|
||||
'case.likes',
|
||||
@@ -194,14 +211,13 @@ export class CaseService {
|
||||
queryBuilder.skip(skip).take(limit);
|
||||
queryBuilder.orderBy('case.createdAt', 'DESC');
|
||||
|
||||
const [items, total] = await queryBuilder.getManyAndCount();
|
||||
const [list, total] = await queryBuilder.getManyAndCount();
|
||||
|
||||
return {
|
||||
items,
|
||||
list,
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
pageSize: limit,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -18,9 +18,13 @@ export class CreateCaseDto {
|
||||
@IsArray()
|
||||
afterImages?: string[];
|
||||
|
||||
@IsEnum(['fabric', 'leather', 'cleaning', 'repair', 'custom'])
|
||||
@IsEnum(['leather', 'fabric', 'functional', 'antique', 'office', 'cleaning', 'repair', 'custom'])
|
||||
serviceType: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
location?: string;
|
||||
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
@@ -36,6 +40,14 @@ export class CreateCaseDto {
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
duration?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
images?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
export class UpdateCaseDto {
|
||||
@@ -56,9 +68,13 @@ export class UpdateCaseDto {
|
||||
afterImages?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(['fabric', 'leather', 'cleaning', 'repair', 'custom'])
|
||||
@IsEnum(['leather', 'fabric', 'functional', 'antique', 'office', 'cleaning', 'repair', 'custom'])
|
||||
serviceType?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
location?: string;
|
||||
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
@@ -78,11 +94,19 @@ export class UpdateCaseDto {
|
||||
@IsOptional()
|
||||
@IsEnum(['draft', 'published', 'archived'])
|
||||
status?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
images?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
export class QueryCaseDto {
|
||||
@IsOptional()
|
||||
@IsEnum(['fabric', 'leather', 'cleaning', 'repair', 'custom'])
|
||||
@IsEnum(['leather', 'fabric', 'functional', 'antique', 'office', 'cleaning', 'repair', 'custom'])
|
||||
serviceType?: string;
|
||||
|
||||
@IsOptional()
|
||||
|
||||
60
后端/src/database/database-seeder.service.ts
Normal file
60
后端/src/database/database-seeder.service.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { Injectable, OnModuleInit, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { User } from '../entities/user.entity';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
|
||||
@Injectable()
|
||||
export class DatabaseSeederService implements OnModuleInit {
|
||||
private readonly logger = new Logger(DatabaseSeederService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(User)
|
||||
private userRepository: Repository<User>,
|
||||
) {}
|
||||
|
||||
async onModuleInit() {
|
||||
await this.seedDefaultAdmin();
|
||||
}
|
||||
|
||||
private async seedDefaultAdmin() {
|
||||
try {
|
||||
// 检查是否已存在管理员账号
|
||||
const existingAdmin = await this.userRepository.findOne({
|
||||
where: { username: 'admin' },
|
||||
});
|
||||
|
||||
if (existingAdmin) {
|
||||
// 如果存在但角色不是admin,则更新
|
||||
if (existingAdmin.role !== 'admin') {
|
||||
await this.userRepository.update(existingAdmin.id, { role: 'admin' });
|
||||
this.logger.log('✅ 已将 admin 用户角色更新为管理员');
|
||||
} else {
|
||||
this.logger.log('默认管理员账号已存在且角色正确');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建默认管理员账号
|
||||
const saltRounds = 10;
|
||||
const hashedPassword = await bcrypt.hash('admin123', saltRounds);
|
||||
|
||||
const adminUser = this.userRepository.create({
|
||||
username: 'admin',
|
||||
email: 'admin@youyijia.com',
|
||||
password: hashedPassword,
|
||||
realName: '系统管理员',
|
||||
phone: '18888888888',
|
||||
role: 'admin',
|
||||
status: 'active',
|
||||
});
|
||||
|
||||
await this.userRepository.save(adminUser);
|
||||
this.logger.log('✅ 默认管理员账号创建成功');
|
||||
this.logger.log(' 用户名: admin');
|
||||
this.logger.log(' 密码: admin123');
|
||||
} catch (error) {
|
||||
this.logger.error('创建默认管理员账号失败:', error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,11 +10,11 @@ export class Booking {
|
||||
@Column({ length: 32, unique: true })
|
||||
bookingNumber: string; // 预约编号
|
||||
|
||||
@ManyToOne(() => User)
|
||||
@ManyToOne(() => User, { nullable: true })
|
||||
@JoinColumn({ name: 'customerId' })
|
||||
customer: User;
|
||||
|
||||
@Column()
|
||||
@Column({ nullable: true })
|
||||
customerId: number;
|
||||
|
||||
@ManyToOne(() => Service)
|
||||
|
||||
@@ -18,13 +18,19 @@ export class Case {
|
||||
@Column({ type: 'json', nullable: true })
|
||||
afterImages: string[];
|
||||
|
||||
@Column({ type: 'json', nullable: true })
|
||||
images: string[];
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: ['fabric', 'leather', 'cleaning', 'repair', 'custom'],
|
||||
enum: ['leather', 'fabric', 'functional', 'antique', 'office', 'cleaning', 'repair', 'custom'],
|
||||
default: 'fabric'
|
||||
})
|
||||
serviceType: string;
|
||||
|
||||
@Column({ length: 100, nullable: true })
|
||||
location: string;
|
||||
|
||||
@Column({ type: 'decimal', precision: 10, scale: 2, nullable: true })
|
||||
price: number;
|
||||
|
||||
@@ -34,6 +40,9 @@ export class Case {
|
||||
@Column({ type: 'int', default: 0 })
|
||||
duration: number; // 工作天数
|
||||
|
||||
@Column({ type: 'json', nullable: true })
|
||||
tags: string[];
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: ['draft', 'published', 'archived'],
|
||||
|
||||
@@ -13,7 +13,7 @@ export class Service {
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: ['fabric', 'leather', 'cleaning', 'repair', 'custom'],
|
||||
enum: ['leather', 'fabric', 'functional', 'antique', 'office', 'cleaning', 'repair', 'custom'],
|
||||
unique: true
|
||||
})
|
||||
type: string;
|
||||
@@ -21,6 +21,9 @@ export class Service {
|
||||
@Column({ type: 'decimal', precision: 10, scale: 2 })
|
||||
basePrice: number;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
icon: string; // 服务图标URL
|
||||
|
||||
@Column({ type: 'json', nullable: true })
|
||||
images: string[];
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ export class CreateServiceDto {
|
||||
@IsString()
|
||||
description: string;
|
||||
|
||||
@IsEnum(['fabric', 'leather', 'cleaning', 'repair', 'custom'])
|
||||
@IsEnum(['leather', 'fabric', 'functional', 'antique', 'office', 'cleaning', 'repair', 'custom'])
|
||||
type: string;
|
||||
|
||||
@IsNotEmpty({ message: '基础价格不能为空' })
|
||||
@@ -19,6 +19,10 @@ export class CreateServiceDto {
|
||||
@Min(0)
|
||||
basePrice: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
icon?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
images?: string[];
|
||||
@@ -49,7 +53,7 @@ export class UpdateServiceDto {
|
||||
description?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(['fabric', 'leather', 'cleaning', 'repair', 'custom'])
|
||||
@IsEnum(['leather', 'fabric', 'functional', 'antique', 'office', 'cleaning', 'repair', 'custom'])
|
||||
type?: string;
|
||||
|
||||
@IsOptional()
|
||||
@@ -58,6 +62,10 @@ export class UpdateServiceDto {
|
||||
@Min(0)
|
||||
basePrice?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
icon?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
images?: string[];
|
||||
@@ -84,10 +92,14 @@ export class UpdateServiceDto {
|
||||
|
||||
export class QueryServiceDto {
|
||||
@IsOptional()
|
||||
@IsEnum(['fabric', 'leather', 'cleaning', 'repair', 'custom'])
|
||||
@IsEnum(['leather', 'fabric', 'functional', 'antique', 'office', 'cleaning', 'repair', 'custom'])
|
||||
type?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(['active', 'inactive'])
|
||||
status?: string = 'active';
|
||||
status?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
keyword?: string;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Controller, Get, Post, Body, Patch, Param, Delete, Query, UseGuards } from '@nestjs/common';
|
||||
import { Controller, Get, Post, Body, Patch, Param, Delete, Query, UseGuards, HttpCode } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery } from '@nestjs/swagger';
|
||||
import { ServiceService } from './service.service';
|
||||
import { CreateServiceDto, UpdateServiceDto, QueryServiceDto } from './dto/service.dto';
|
||||
@@ -12,30 +12,47 @@ export class ServiceController {
|
||||
|
||||
@ApiBearerAuth()
|
||||
@Post()
|
||||
@HttpCode(200)
|
||||
@Roles('admin')
|
||||
@ApiOperation({ summary: '创建服务(管理员)' })
|
||||
@ApiResponse({ status: 201, description: '创建成功' })
|
||||
@ApiResponse({ status: 200, description: '创建成功' })
|
||||
@ApiResponse({ status: 409, description: '服务类型已存在' })
|
||||
create(@Body() createServiceDto: CreateServiceDto) {
|
||||
return this.serviceService.create(createServiceDto);
|
||||
async create(@Body() createServiceDto: CreateServiceDto) {
|
||||
const data = await this.serviceService.create(createServiceDto);
|
||||
return {
|
||||
code: 0,
|
||||
message: '创建成功',
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Get()
|
||||
@ApiOperation({ summary: '获取服务列表' })
|
||||
@ApiQuery({ name: 'type', required: false, enum: ['fabric', 'leather', 'cleaning', 'repair', 'custom'] })
|
||||
@ApiQuery({ name: 'status', required: false, enum: ['active', 'inactive'], description: '默认为active' })
|
||||
@ApiQuery({ name: 'type', required: false, enum: ['leather', 'fabric', 'functional', 'antique', 'office', 'cleaning', 'repair', 'custom'] })
|
||||
@ApiQuery({ name: 'status', required: false, enum: ['active', 'inactive'] })
|
||||
@ApiQuery({ name: 'keyword', required: false, description: '搜索关键词' })
|
||||
@ApiResponse({ status: 200, description: '获取成功' })
|
||||
findAll(@Query() query: QueryServiceDto) {
|
||||
return this.serviceService.findAll(query);
|
||||
async findAll(@Query() query: QueryServiceDto) {
|
||||
const data = await this.serviceService.findAll(query);
|
||||
return {
|
||||
code: 0,
|
||||
message: '获取成功',
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Get('active')
|
||||
@ApiOperation({ summary: '获取所有有效服务' })
|
||||
@ApiResponse({ status: 200, description: '获取成功' })
|
||||
getActiveServices() {
|
||||
return this.serviceService.getActiveServices();
|
||||
async getActiveServices() {
|
||||
const data = await this.serviceService.getActiveServices();
|
||||
return {
|
||||
code: 0,
|
||||
message: '获取成功',
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
@Public()
|
||||
@@ -43,8 +60,13 @@ export class ServiceController {
|
||||
@ApiOperation({ summary: '根据ID获取服务详情' })
|
||||
@ApiResponse({ status: 200, description: '获取成功' })
|
||||
@ApiResponse({ status: 404, description: '服务不存在' })
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.serviceService.findOne(+id);
|
||||
async findOne(@Param('id') id: string) {
|
||||
const data = await this.serviceService.findOne(+id);
|
||||
return {
|
||||
code: 0,
|
||||
message: '获取成功',
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
@Public()
|
||||
@@ -52,8 +74,13 @@ export class ServiceController {
|
||||
@ApiOperation({ summary: '根据类型获取服务' })
|
||||
@ApiResponse({ status: 200, description: '获取成功' })
|
||||
@ApiResponse({ status: 404, description: '服务类型不存在' })
|
||||
findByType(@Param('type') type: string) {
|
||||
return this.serviceService.findByType(type);
|
||||
async findByType(@Param('type') type: string) {
|
||||
const data = await this.serviceService.findByType(type);
|
||||
return {
|
||||
code: 0,
|
||||
message: '获取成功',
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
@ApiBearerAuth()
|
||||
@@ -63,8 +90,13 @@ export class ServiceController {
|
||||
@ApiResponse({ status: 200, description: '更新成功' })
|
||||
@ApiResponse({ status: 404, description: '服务不存在' })
|
||||
@ApiResponse({ status: 409, description: '服务类型已存在' })
|
||||
update(@Param('id') id: string, @Body() updateServiceDto: UpdateServiceDto) {
|
||||
return this.serviceService.update(+id, updateServiceDto);
|
||||
async update(@Param('id') id: string, @Body() updateServiceDto: UpdateServiceDto) {
|
||||
const data = await this.serviceService.update(+id, updateServiceDto);
|
||||
return {
|
||||
code: 0,
|
||||
message: '更新成功',
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
@ApiBearerAuth()
|
||||
@@ -73,8 +105,12 @@ export class ServiceController {
|
||||
@ApiOperation({ summary: '删除服务(管理员)' })
|
||||
@ApiResponse({ status: 200, description: '删除成功' })
|
||||
@ApiResponse({ status: 404, description: '服务不存在' })
|
||||
remove(@Param('id') id: string) {
|
||||
return this.serviceService.remove(+id);
|
||||
async remove(@Param('id') id: string) {
|
||||
await this.serviceService.remove(+id);
|
||||
return {
|
||||
code: 0,
|
||||
message: '删除成功',
|
||||
};
|
||||
}
|
||||
|
||||
@ApiBearerAuth()
|
||||
@@ -83,8 +119,13 @@ export class ServiceController {
|
||||
@ApiOperation({ summary: '切换服务状态(管理员)' })
|
||||
@ApiResponse({ status: 200, description: '状态切换成功' })
|
||||
@ApiResponse({ status: 404, description: '服务不存在' })
|
||||
toggleStatus(@Param('id') id: string) {
|
||||
return this.serviceService.toggleStatus(+id);
|
||||
async toggleStatus(@Param('id') id: string) {
|
||||
const data = await this.serviceService.toggleStatus(+id);
|
||||
return {
|
||||
code: 0,
|
||||
message: '状态切换成功',
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
@ApiBearerAuth()
|
||||
@@ -92,7 +133,11 @@ export class ServiceController {
|
||||
@Roles('admin')
|
||||
@ApiOperation({ summary: '更新服务排序(管理员)' })
|
||||
@ApiResponse({ status: 200, description: '排序更新成功' })
|
||||
updateSortOrder(@Body() serviceOrders: { id: number; sortOrder: number }[]) {
|
||||
return this.serviceService.updateSortOrder(serviceOrders);
|
||||
async updateSortOrder(@Body() serviceOrders: { id: number; sortOrder: number }[]) {
|
||||
await this.serviceService.updateSortOrder(serviceOrders);
|
||||
return {
|
||||
code: 0,
|
||||
message: '排序更新成功',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -29,8 +29,8 @@ export class ServiceService {
|
||||
return this.serviceRepository.save(service);
|
||||
}
|
||||
|
||||
async findAll(query: QueryServiceDto): Promise<Service[]> {
|
||||
const { type, status } = query;
|
||||
async findAll(query: QueryServiceDto): Promise<any> {
|
||||
const { type, status, keyword } = query;
|
||||
const queryBuilder = this.serviceRepository.createQueryBuilder('service');
|
||||
|
||||
if (type) {
|
||||
@@ -41,10 +41,22 @@ export class ServiceService {
|
||||
queryBuilder.andWhere('service.status = :status', { status });
|
||||
}
|
||||
|
||||
if (keyword) {
|
||||
queryBuilder.andWhere(
|
||||
'(service.name LIKE :keyword OR service.description LIKE :keyword)',
|
||||
{ keyword: `%${keyword}%` }
|
||||
);
|
||||
}
|
||||
|
||||
queryBuilder.orderBy('service.sortOrder', 'ASC');
|
||||
queryBuilder.addOrderBy('service.createdAt', 'DESC');
|
||||
|
||||
return queryBuilder.getMany();
|
||||
const services = await queryBuilder.getMany();
|
||||
|
||||
return {
|
||||
list: services,
|
||||
total: services.length
|
||||
};
|
||||
}
|
||||
|
||||
async findOne(id: number): Promise<Service> {
|
||||
@@ -94,11 +106,16 @@ export class ServiceService {
|
||||
await this.serviceRepository.remove(service);
|
||||
}
|
||||
|
||||
async getActiveServices(): Promise<Service[]> {
|
||||
return this.serviceRepository.find({
|
||||
async getActiveServices(): Promise<any> {
|
||||
const services = await this.serviceRepository.find({
|
||||
where: { status: 'active' },
|
||||
order: { sortOrder: 'ASC', createdAt: 'DESC' }
|
||||
});
|
||||
|
||||
return {
|
||||
list: services,
|
||||
total: services.length
|
||||
};
|
||||
}
|
||||
|
||||
async updateSortOrder(serviceOrders: { id: number; sortOrder: number }[]): Promise<void> {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards } from '@nestjs/common';
|
||||
import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards, HttpCode } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { UserService } from './user.service';
|
||||
import { CreateUserDto, UpdateUserDto } from './dto/user.dto';
|
||||
@@ -13,26 +13,45 @@ export class UserController {
|
||||
constructor(private readonly userService: UserService) {}
|
||||
|
||||
@Post()
|
||||
@HttpCode(200)
|
||||
@Roles('admin')
|
||||
@ApiOperation({ summary: '创建用户(管理员)' })
|
||||
@ApiResponse({ status: 201, description: '创建成功' })
|
||||
create(@Body() createUserDto: CreateUserDto) {
|
||||
return this.userService.create(createUserDto);
|
||||
@ApiResponse({ status: 200, description: '创建成功' })
|
||||
async create(@Body() createUserDto: CreateUserDto) {
|
||||
const data = await this.userService.create(createUserDto);
|
||||
return {
|
||||
code: 0,
|
||||
message: '创建成功',
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
@Get()
|
||||
@Roles('admin')
|
||||
@ApiOperation({ summary: '获取所有用户(管理员)' })
|
||||
@ApiResponse({ status: 200, description: '获取成功' })
|
||||
findAll() {
|
||||
return this.userService.findAll();
|
||||
async findAll() {
|
||||
const list = await this.userService.findAll();
|
||||
return {
|
||||
code: 0,
|
||||
message: '获取成功',
|
||||
data: {
|
||||
list,
|
||||
total: list.length,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@Get('profile')
|
||||
@ApiOperation({ summary: '获取当前用户信息' })
|
||||
@ApiResponse({ status: 200, description: '获取成功' })
|
||||
getProfile(@CurrentUser() user: CurrentUserData) {
|
||||
return this.userService.getUserProfile(user.userId);
|
||||
async getProfile(@CurrentUser() user: CurrentUserData) {
|
||||
const data = await this.userService.getUserProfile(user.userId);
|
||||
return {
|
||||
code: 0,
|
||||
message: '获取成功',
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@@ -40,15 +59,25 @@ export class UserController {
|
||||
@ApiOperation({ summary: '根据ID获取用户(管理员)' })
|
||||
@ApiResponse({ status: 200, description: '获取成功' })
|
||||
@ApiResponse({ status: 404, description: '用户不存在' })
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.userService.findById(+id);
|
||||
async findOne(@Param('id') id: string) {
|
||||
const data = await this.userService.findById(+id);
|
||||
return {
|
||||
code: 0,
|
||||
message: '获取成功',
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
@Patch('profile')
|
||||
@ApiOperation({ summary: '更新当前用户信息' })
|
||||
@ApiResponse({ status: 200, description: '更新成功' })
|
||||
updateProfile(@CurrentUser() user: CurrentUserData, @Body() updateUserDto: UpdateUserDto) {
|
||||
return this.userService.update(user.userId, updateUserDto);
|
||||
async updateProfile(@CurrentUser() user: CurrentUserData, @Body() updateUserDto: UpdateUserDto) {
|
||||
const data = await this.userService.update(user.userId, updateUserDto);
|
||||
return {
|
||||
code: 0,
|
||||
message: '更新成功',
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@@ -56,8 +85,13 @@ export class UserController {
|
||||
@ApiOperation({ summary: '更新用户信息(管理员)' })
|
||||
@ApiResponse({ status: 200, description: '更新成功' })
|
||||
@ApiResponse({ status: 404, description: '用户不存在' })
|
||||
update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
|
||||
return this.userService.update(+id, updateUserDto);
|
||||
async update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
|
||||
const data = await this.userService.update(+id, updateUserDto);
|
||||
return {
|
||||
code: 0,
|
||||
message: '更新成功',
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@@ -65,7 +99,11 @@ export class UserController {
|
||||
@ApiOperation({ summary: '删除用户(管理员)' })
|
||||
@ApiResponse({ status: 200, description: '删除成功' })
|
||||
@ApiResponse({ status: 404, description: '用户不存在' })
|
||||
remove(@Param('id') id: string) {
|
||||
return this.userService.remove(+id);
|
||||
async remove(@Param('id') id: string) {
|
||||
await this.userService.remove(+id);
|
||||
return {
|
||||
code: 0,
|
||||
message: '删除成功',
|
||||
};
|
||||
}
|
||||
}
|
||||
36
后端/update-admin-role.js
Normal file
36
后端/update-admin-role.js
Normal file
@@ -0,0 +1,36 @@
|
||||
const mysql = require('mysql2/promise');
|
||||
|
||||
async function updateAdminRole() {
|
||||
const connection = await mysql.createConnection({
|
||||
host: '8.130.78.179',
|
||||
port: 9986,
|
||||
user: 'sffx',
|
||||
password: 'wF5WKm35Ddm5NDTn',
|
||||
database: 'sffx'
|
||||
});
|
||||
|
||||
try {
|
||||
// 更新 admin 用户角色
|
||||
const [result] = await connection.execute(
|
||||
'UPDATE users SET role = ? WHERE username = ?',
|
||||
['admin', 'admin']
|
||||
);
|
||||
|
||||
console.log('✅ 更新成功!受影响的行数:', result.affectedRows);
|
||||
|
||||
// 查询验证
|
||||
const [rows] = await connection.execute(
|
||||
'SELECT id, username, email, realName, role, status FROM users WHERE username = ?',
|
||||
['admin']
|
||||
);
|
||||
|
||||
console.log('\n当前 admin 用户信息:');
|
||||
console.table(rows);
|
||||
} catch (error) {
|
||||
console.error('❌ 更新失败:', error.message);
|
||||
} finally {
|
||||
await connection.end();
|
||||
}
|
||||
}
|
||||
|
||||
updateAdminRole();
|
||||
Reference in New Issue
Block a user