feat:"完成页面接口的对接"

This commit is contained in:
2026-01-29 17:58:19 +08:00
parent 2774a539bf
commit 2b69da3c15
98 changed files with 9504 additions and 592 deletions

36
后端/.env Normal file
View 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
View File

@@ -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

View File

@@ -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 {}

View File

@@ -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,
};
}
}

View 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);
}
}

View 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 {}

View 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: '删除成功',
};
}
}

View 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;
}

View File

@@ -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,
};
}
}

View File

@@ -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,
};
}
}

View File

@@ -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()

View 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);
}
}
}

View File

@@ -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)

View File

@@ -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'],

View File

@@ -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[];

View File

@@ -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;
}

View File

@@ -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: '排序更新成功',
};
}
}

View File

@@ -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> {

View File

@@ -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: '删除成功',
};
}
}

View 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();