初始化参股

This commit is contained in:
2026-01-27 18:06:04 +08:00
commit 2774a539bf
254 changed files with 33255 additions and 0 deletions

View File

@@ -0,0 +1,22 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
describe('AppController', () => {
let appController: AppController;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService],
}).compile();
appController = app.get<AppController>(AppController);
});
describe('root', () => {
it('should return "Hello World!"', () => {
expect(appController.getHello()).toBe('Hello World!');
});
});
});

View File

@@ -0,0 +1,12 @@
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}

52
后端/src/app.module.ts Normal file
View File

@@ -0,0 +1,52 @@
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import databaseConfig from './config/database.config';
import jwtConfig from './config/jwt.config';
import { User } from './entities/user.entity';
import { Case } from './entities/case.entity';
import { Service } from './entities/service.entity';
import { Booking } from './entities/booking.entity';
import { AuthModule } from './auth/auth.module';
import { UserModule } from './user/user.module';
import { CaseModule } from './case/case.module';
import { ServiceModule } from './service/service.module';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [databaseConfig, jwtConfig],
envFilePath: '.env',
}),
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
type: 'mysql',
host: configService.get('database.host'),
port: configService.get('database.port'),
username: configService.get('database.username'),
password: configService.get('database.password'),
database: configService.get('database.database'),
entities: [User, Case, Service, Booking],
synchronize: configService.get('database.synchronize'),
logging: configService.get('database.logging'),
charset: configService.get('database.charset'),
timezone: configService.get('database.timezone'),
retryAttempts: 3, // 减少重试次数
retryDelay: 1000, // 重试延迟
autoLoadEntities: true,
}),
inject: [ConfigService],
}),
AuthModule,
UserModule,
CaseModule,
ServiceModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

View File

@@ -0,0 +1,8 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}

View File

@@ -0,0 +1,53 @@
import { Controller, Post, Body, HttpStatus, HttpCode } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
import { AuthService, AuthResult } from './auth.service';
import { RegisterDto, LoginDto, WechatLoginDto, WechatUserInfoDto } from './dto/auth.dto';
import { Public } from './guards/public.decorator';
@ApiTags('认证')
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Public()
@Post('register')
@ApiOperation({ summary: '用户注册' })
@ApiResponse({ status: 201, description: '注册成功' })
@ApiResponse({ status: 409, description: '用户已存在' })
async register(@Body() registerDto: RegisterDto): Promise<AuthResult> {
return this.authService.register(registerDto);
}
@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);
}
@Public()
@Post('refresh')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: '刷新访问令牌' })
@ApiResponse({ status: 200, description: '刷新成功' })
@ApiResponse({ status: 401, description: '刷新令牌无效' })
async refreshToken(@Body('refresh_token') refreshToken: string) {
return this.authService.refreshToken(refreshToken);
}
@Public()
@Post('wechat/login')
@HttpCode(HttpStatus.OK)
@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);
}
}

View File

@@ -0,0 +1,43 @@
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { PassportModule } from '@nestjs/passport';
import { APP_GUARD } from '@nestjs/core';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { WechatService } from './wechat.service';
import { JwtStrategy } from './strategies/jwt.strategy';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
import { RolesGuard } from './guards/roles.guard';
import { UserModule } from '../user/user.module';
@Module({
imports: [
UserModule,
PassportModule,
JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
secret: configService.get<string>('jwt.secret'),
signOptions: configService.get('jwt.signOptions'),
}),
inject: [ConfigService],
}),
],
controllers: [AuthController],
providers: [
AuthService,
WechatService,
JwtStrategy,
{
provide: APP_GUARD,
useClass: JwtAuthGuard,
},
{
provide: APP_GUARD,
useClass: RolesGuard,
},
],
exports: [AuthService],
})
export class AuthModule {}

View File

@@ -0,0 +1,248 @@
import { Injectable, ConflictException, UnauthorizedException, BadRequestException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import * as bcrypt from 'bcrypt';
import { UserService } from '../user/user.service';
import { RegisterDto, LoginDto, WechatLoginDto, WechatUserInfoDto } from './dto/auth.dto';
import { JwtPayload } from './strategies/jwt.strategy';
import { WechatService } from './wechat.service';
export interface AuthResult {
user: {
id: number;
username: string;
email: string;
realName: string;
phone: string;
avatar: string;
role: string;
status: string;
};
access_token: string;
refresh_token: string;
}
@Injectable()
export class AuthService {
constructor(
private userService: UserService,
private jwtService: JwtService,
private configService: ConfigService,
private wechatService: WechatService,
) {}
async register(registerDto: RegisterDto): Promise<AuthResult> {
const { username, email, password, realName, phone } = registerDto;
// 检查用户名是否已存在
const existingUsername = await this.userService.findByUsername(username);
if (existingUsername) {
throw new ConflictException('用户名已存在');
}
// 检查邮箱是否已存在
const existingEmail = await this.userService.findByEmail(email);
if (existingEmail) {
throw new ConflictException('邮箱已存在');
}
// 加密密码
const saltRounds = 10;
const hashedPassword = await bcrypt.hash(password, saltRounds);
// 创建用户
const user = await this.userService.create({
username,
email,
password: hashedPassword,
realName,
phone,
role: 'customer',
status: 'active',
});
// 生成tokens
const tokens = await this.generateTokens(user);
return {
user: {
id: user.id,
username: user.username,
email: user.email,
realName: user.realName,
phone: user.phone,
avatar: user.avatar,
role: user.role,
status: user.status,
},
...tokens,
};
}
async login(loginDto: LoginDto): Promise<AuthResult> {
const { username, password } = loginDto;
// 查找用户(支持用户名或邮箱登录)
let user;
if (username.includes('@')) {
user = await this.userService.findByEmailWithPassword(username);
} else {
user = await this.userService.findByUsernameWithPassword(username);
}
if (!user) {
throw new UnauthorizedException('用户名或密码错误');
}
// 检查用户状态
if (user.status !== 'active') {
throw new UnauthorizedException('账户已被禁用,请联系管理员');
}
// 验证密码
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
throw new UnauthorizedException('用户名或密码错误');
}
// 生成tokens
const tokens = await this.generateTokens(user);
return {
user: {
id: user.id,
username: user.username,
email: user.email,
realName: user.realName,
phone: user.phone,
avatar: user.avatar,
role: user.role,
status: user.status,
},
...tokens,
};
}
async refreshToken(refreshToken: string): Promise<{ access_token: string }> {
try {
const payload = this.jwtService.verify(refreshToken, {
secret: this.configService.get('jwt.refreshSecret'),
});
const user = await this.userService.findById(payload.sub);
if (!user || user.status !== 'active') {
throw new UnauthorizedException('用户不存在或已被禁用');
}
const jwtPayload: JwtPayload = {
sub: user.id,
username: user.username,
email: user.email,
role: user.role,
};
return {
access_token: this.jwtService.sign(jwtPayload),
};
} catch (error) {
throw new UnauthorizedException('刷新令牌无效');
}
}
/**
* 微信小程序登录
*/
async wechatLogin(wechatLoginDto: WechatLoginDto, userInfo?: WechatUserInfoDto): Promise<AuthResult> {
const { code, encryptedData, iv, signature } = wechatLoginDto;
// 通过code获取微信session信息
const wechatSession = await this.wechatService.getWechatSession(code);
const { openid, session_key, unionid } = wechatSession;
// 查找是否已存在该微信用户
let user = await this.userService.findByOpenid(openid);
if (user) {
// 更新session_key
await this.userService.updateSessionKey(user.id, session_key);
// 生成tokens
const tokens = await this.generateTokens(user);
return {
user: {
id: user.id,
username: user.username,
email: user.email,
realName: user.realName,
phone: user.phone,
avatar: user.avatar,
role: user.role,
status: user.status,
},
...tokens,
};
}
// 新用户注册流程
let decryptedUserInfo: any = null;
// 如果提供了加密数据,解密获取用户信息
if (encryptedData && iv) {
decryptedUserInfo = this.wechatService.decryptWechatData(encryptedData, iv, session_key);
}
// 生成唯一用户名
const username = this.wechatService.generateUniqueUsername(openid);
// 创建新用户
user = await this.userService.create({
username,
email: `${openid}@wechat.local`, // 临时邮箱
realName: decryptedUserInfo?.nickName || userInfo?.nickName || '微信用户',
avatar: decryptedUserInfo?.avatarUrl || userInfo?.avatarUrl || null,
role: 'customer',
status: 'active',
openid,
unionid,
sessionKey: session_key,
});
// 生成tokens
const tokens = await this.generateTokens(user);
return {
user: {
id: user.id,
username: user.username,
email: user.email,
realName: user.realName,
phone: user.phone,
avatar: user.avatar,
role: user.role,
status: user.status,
},
...tokens,
};
}
private async generateTokens(user: any) {
const payload: JwtPayload = {
sub: user.id,
username: user.username,
email: user.email,
role: user.role,
};
const accessToken = this.jwtService.sign(payload);
const refreshToken = this.jwtService.sign(payload, {
secret: this.configService.get('jwt.refreshSecret'),
expiresIn: this.configService.get('jwt.refreshExpiresIn'),
});
return {
access_token: accessToken,
refresh_token: refreshToken,
};
}
}

View File

@@ -0,0 +1,15 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export interface CurrentUserData {
userId: number;
username: string;
email: string;
role: string;
}
export const CurrentUser = createParamDecorator(
(data: unknown, ctx: ExecutionContext): CurrentUserData => {
const request = ctx.switchToHttp().getRequest();
return request.user;
},
);

View File

@@ -0,0 +1,75 @@
import { IsEmail, IsNotEmpty, IsString, MinLength, IsOptional } from 'class-validator';
export class RegisterDto {
@IsNotEmpty({ message: '用户名不能为空' })
@IsString({ message: '用户名必须为字符串' })
username: string;
@IsEmail({}, { message: '邮箱格式不正确' })
email: string;
@IsNotEmpty({ message: '密码不能为空' })
@MinLength(6, { message: '密码长度至少6位' })
password: string;
@IsOptional()
realName?: string;
@IsOptional()
phone?: string;
}
export class LoginDto {
@IsNotEmpty({ message: '登录名不能为空' })
@IsString()
username: string; // 可以是用户名或邮箱
@IsNotEmpty({ message: '密码不能为空' })
password: string;
}
// 微信小程序登录DTO
export class WechatLoginDto {
@IsNotEmpty({ message: '微信登录码不能为空' })
@IsString()
code: string; // 微信小程序登录码
@IsOptional()
@IsString()
encryptedData?: string; // 加密数据
@IsOptional()
@IsString()
iv?: string; // 初始向量
@IsOptional()
@IsString()
signature?: string; // 数据签名
}
// 微信用户信息DTO
export class WechatUserInfoDto {
@IsOptional()
@IsString()
nickName?: string;
@IsOptional()
@IsString()
avatarUrl?: string;
@IsOptional()
@IsString()
gender?: string;
@IsOptional()
@IsString()
city?: string;
@IsOptional()
@IsString()
province?: string;
@IsOptional()
@IsString()
country?: string;
}

View File

@@ -0,0 +1,29 @@
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { IS_PUBLIC_KEY } from './public.decorator';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(private reflector: Reflector) {
super();
}
canActivate(context: ExecutionContext) {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) {
return true;
}
return super.canActivate(context);
}
handleRequest(err, user, info) {
if (err || !user) {
throw err || new UnauthorizedException('登录已过期,请重新登录');
}
return user;
}
}

View File

@@ -0,0 +1,4 @@
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);

View File

@@ -0,0 +1,4 @@
import { SetMetadata } from '@nestjs/common';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);

View File

@@ -0,0 +1,27 @@
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from './roles.decorator';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles) {
return true;
}
const { user } = context.switchToHttp().getRequest();
if (!user) {
throw new ForbiddenException('用户未认证');
}
const hasRole = requiredRoles.includes(user.role);
if (!hasRole) {
throw new ForbiddenException('权限不足');
}
return true;
}
}

View File

@@ -0,0 +1,39 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { UserService } from '../../user/user.service';
export interface JwtPayload {
sub: number;
username: string;
email: string;
role: string;
}
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
private configService: ConfigService,
private userService: UserService,
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get('jwt.secret'),
});
}
async validate(payload: JwtPayload) {
const user = await this.userService.findById(payload.sub);
if (!user || user.status !== 'active') {
return null;
}
return {
userId: payload.sub,
username: payload.username,
email: payload.email,
role: payload.role,
};
}
}

View File

@@ -0,0 +1,125 @@
import { Injectable, Logger, BadRequestException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import axios from 'axios';
import * as crypto from 'crypto';
export interface WechatSession {
openid: string;
session_key: string;
unionid?: string;
errcode?: number;
errmsg?: string;
}
export interface WechatUserInfo {
openId: string;
nickName: string;
gender: number;
city: string;
province: string;
country: string;
avatarUrl: string;
unionId?: string;
watermark: {
timestamp: number;
appid: string;
};
}
@Injectable()
export class WechatService {
private readonly logger = new Logger(WechatService.name);
private readonly appId: string;
private readonly appSecret: string;
constructor(private configService: ConfigService) {
this.appId = this.configService.get<string>('WECHAT_APPID') || '';
this.appSecret = this.configService.get<string>('WECHAT_SECRET') || '';
}
/**
* 通过code换取微信用户的openid和session_key
*/
async getWechatSession(code: string): Promise<WechatSession> {
try {
const response = await axios.get('https://api.weixin.qq.com/sns/jscode2session', {
params: {
appid: this.appId,
secret: this.appSecret,
js_code: code,
grant_type: 'authorization_code',
},
});
const data = response.data;
if (data.errcode) {
this.logger.error(`微信登录失败: ${data.errcode} - ${data.errmsg}`);
throw new BadRequestException(`微信登录失败: ${data.errmsg}`);
}
return {
openid: data.openid,
session_key: data.session_key,
unionid: data.unionid,
};
} catch (error) {
this.logger.error('调用微信API失败:', error);
throw new BadRequestException('微信登录服务暂时不可用');
}
}
/**
* 解密微信用户信息
*/
decryptWechatData(encryptedData: string, iv: string, sessionKey: string): WechatUserInfo {
try {
const key = Buffer.from(sessionKey, 'base64');
const ivBuffer = Buffer.from(iv, 'base64');
const encrypted = Buffer.from(encryptedData, 'base64');
const decipher = crypto.createDecipheriv('aes-128-cbc', key, ivBuffer);
decipher.setAutoPadding(true);
let decrypted = decipher.update(encrypted, undefined, 'utf8');
decrypted += decipher.final('utf8');
const userInfo = JSON.parse(decrypted);
// 验证水印
if (userInfo.watermark.appid !== this.appId) {
throw new Error('水印验证失败');
}
return userInfo;
} catch (error) {
this.logger.error('解密微信用户信息失败:', error);
throw new BadRequestException('用户信息解密失败');
}
}
/**
* 验证数据签名
*/
verifySignature(rawData: string, signature: string, sessionKey: string): boolean {
try {
const hmac = crypto.createHmac('sha1', sessionKey);
hmac.update(rawData);
const computedSignature = hmac.digest('hex');
return computedSignature === signature;
} catch (error) {
this.logger.error('验证签名失败:', error);
return false;
}
}
/**
* 生成唯一用户名
*/
generateUniqueUsername(openid: string): string {
// 使用openid的一部分作为用户名后缀确保唯一性
const suffix = openid.slice(-8);
return `wx_${suffix}`;
}
}

View File

@@ -0,0 +1,89 @@
import { Controller, Get, Post, Body, Patch, Param, Delete, Query, UseGuards } 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';
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('案例管理')
@Controller('cases')
export class CaseController {
constructor(private readonly caseService: CaseService) {}
@ApiBearerAuth()
@Post()
@Roles('admin', 'worker')
@ApiOperation({ summary: '创建案例' })
@ApiResponse({ status: 201, description: '创建成功' })
create(@Body() createCaseDto: CreateCaseDto, @CurrentUser() user: CurrentUserData) {
return this.caseService.create(createCaseDto, user.userId);
}
@Public()
@Get()
@ApiOperation({ summary: '获取案例列表' })
@ApiQuery({ name: 'serviceType', required: false, enum: ['fabric', 'leather', '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);
}
@ApiBearerAuth()
@Get('my')
@ApiOperation({ summary: '获取我的案例列表' })
@ApiQuery({ name: 'serviceType', required: false, enum: ['fabric', 'leather', '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);
}
@Public()
@Get(':id')
@ApiOperation({ summary: '根据ID获取案例详情' })
@ApiResponse({ status: 200, description: '获取成功' })
@ApiResponse({ status: 404, description: '案例不存在' })
findOne(@Param('id') id: string) {
return this.caseService.findOne(+id);
}
@ApiBearerAuth()
@Patch(':id')
@ApiOperation({ summary: '更新案例' })
@ApiResponse({ status: 200, description: '更新成功' })
@ApiResponse({ status: 404, description: '案例不存在' })
@ApiResponse({ status: 403, description: '没有权限' })
update(
@Param('id') id: string,
@Body() updateCaseDto: UpdateCaseDto,
@CurrentUser() user: CurrentUserData
) {
return this.caseService.update(+id, updateCaseDto, user.userId, user.role);
}
@ApiBearerAuth()
@Delete(':id')
@ApiOperation({ summary: '删除案例' })
@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);
}
@Public()
@Post(':id/like')
@ApiOperation({ summary: '点赞案例' })
@ApiResponse({ status: 200, description: '点赞成功' })
@ApiResponse({ status: 404, description: '案例不存在' })
like(@Param('id') id: string) {
return this.caseService.like(+id);
}
}

View File

@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CaseService } from './case.service';
import { CaseController } from './case.controller';
import { Case } from '../entities/case.entity';
@Module({
imports: [TypeOrmModule.forFeature([Case])],
controllers: [CaseController],
providers: [CaseService],
exports: [CaseService],
})
export class CaseModule {}

View File

@@ -0,0 +1,207 @@
import { Injectable, NotFoundException, ForbiddenException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Case } from '../entities/case.entity';
import { CreateCaseDto, UpdateCaseDto, QueryCaseDto } from './dto/case.dto';
@Injectable()
export class CaseService {
constructor(
@InjectRepository(Case)
private caseRepository: Repository<Case>,
) {}
async create(createCaseDto: CreateCaseDto, createdBy: number): Promise<Case> {
const caseEntity = this.caseRepository.create({
...createCaseDto,
createdBy,
status: 'published',
});
return this.caseRepository.save(caseEntity);
}
async findAll(query: QueryCaseDto) {
const { serviceType, status, page = 1, limit = 10 } = query;
const queryBuilder = this.caseRepository
.createQueryBuilder('case')
.leftJoinAndSelect('case.creator', 'creator')
.select([
'case.id',
'case.title',
'case.description',
'case.beforeImages',
'case.afterImages',
'case.serviceType',
'case.price',
'case.materials',
'case.duration',
'case.status',
'case.views',
'case.likes',
'case.createdAt',
'case.updatedAt',
'creator.id',
'creator.username',
'creator.realName',
]);
if (serviceType) {
queryBuilder.andWhere('case.serviceType = :serviceType', { serviceType });
}
if (status) {
queryBuilder.andWhere('case.status = :status', { status });
}
const skip = (page - 1) * limit;
queryBuilder.skip(skip).take(limit);
queryBuilder.orderBy('case.createdAt', 'DESC');
const [items, total] = await queryBuilder.getManyAndCount();
return {
items,
total,
page,
limit,
totalPages: Math.ceil(total / limit),
};
}
async findOne(id: number): Promise<Case> {
const caseEntity = await this.caseRepository
.createQueryBuilder('case')
.leftJoinAndSelect('case.creator', 'creator')
.select([
'case.id',
'case.title',
'case.description',
'case.beforeImages',
'case.afterImages',
'case.serviceType',
'case.price',
'case.materials',
'case.duration',
'case.status',
'case.views',
'case.likes',
'case.createdAt',
'case.updatedAt',
'creator.id',
'creator.username',
'creator.realName',
])
.where('case.id = :id', { id })
.getOne();
if (!caseEntity) {
throw new NotFoundException('案例不存在');
}
// 增加浏览量
await this.caseRepository.update(id, { views: () => 'views + 1' });
return caseEntity;
}
async update(id: number, updateCaseDto: UpdateCaseDto, userId: number, userRole: string): Promise<Case> {
const caseEntity = await this.caseRepository.findOne({
where: { id },
relations: ['creator'],
});
if (!caseEntity) {
throw new NotFoundException('案例不存在');
}
// 检查权限:只有创建者或管理员可以修改
if (caseEntity.createdBy !== userId && userRole !== 'admin') {
throw new ForbiddenException('没有权限修改此案例');
}
await this.caseRepository.update(id, updateCaseDto);
return this.findOne(id);
}
async remove(id: number, userId: number, userRole: string): Promise<void> {
const caseEntity = await this.caseRepository.findOne({
where: { id },
});
if (!caseEntity) {
throw new NotFoundException('案例不存在');
}
// 检查权限:只有创建者或管理员可以删除
if (caseEntity.createdBy !== userId && userRole !== 'admin') {
throw new ForbiddenException('没有权限删除此案例');
}
await this.caseRepository.remove(caseEntity);
}
async like(id: number): Promise<{ likes: number }> {
const caseEntity = await this.caseRepository.findOne({ where: { id } });
if (!caseEntity) {
throw new NotFoundException('案例不存在');
}
await this.caseRepository.update(id, { likes: () => 'likes + 1' });
const updatedCase = await this.caseRepository.findOne({ where: { id } });
if (!updatedCase) {
throw new NotFoundException('更新后案例不存在');
}
return { likes: updatedCase.likes };
}
async getMyCases(userId: number, query: QueryCaseDto) {
const { serviceType, status, page = 1, limit = 10 } = query;
const queryBuilder = this.caseRepository
.createQueryBuilder('case')
.leftJoinAndSelect('case.creator', 'creator')
.where('case.createdBy = :userId', { userId })
.select([
'case.id',
'case.title',
'case.description',
'case.beforeImages',
'case.afterImages',
'case.serviceType',
'case.price',
'case.materials',
'case.duration',
'case.status',
'case.views',
'case.likes',
'case.createdAt',
'case.updatedAt',
'creator.id',
'creator.username',
'creator.realName',
]);
if (serviceType) {
queryBuilder.andWhere('case.serviceType = :serviceType', { serviceType });
}
if (status) {
queryBuilder.andWhere('case.status = :status', { status });
}
const skip = (page - 1) * limit;
queryBuilder.skip(skip).take(limit);
queryBuilder.orderBy('case.createdAt', 'DESC');
const [items, total] = await queryBuilder.getManyAndCount();
return {
items,
total,
page,
limit,
totalPages: Math.ceil(total / limit),
};
}
}

View File

@@ -0,0 +1,103 @@
import { IsNotEmpty, IsString, IsOptional, IsEnum, IsNumber, IsArray, Min } from 'class-validator';
import { Type } from 'class-transformer';
export class CreateCaseDto {
@IsNotEmpty({ message: '标题不能为空' })
@IsString()
title: string;
@IsNotEmpty({ message: '描述不能为空' })
@IsString()
description: string;
@IsOptional()
@IsArray()
beforeImages?: string[];
@IsOptional()
@IsArray()
afterImages?: string[];
@IsEnum(['fabric', 'leather', 'cleaning', 'repair', 'custom'])
serviceType: string;
@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(0)
price?: number;
@IsOptional()
@IsString()
materials?: string;
@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(0)
duration?: number;
}
export class UpdateCaseDto {
@IsOptional()
@IsString()
title?: string;
@IsOptional()
@IsString()
description?: string;
@IsOptional()
@IsArray()
beforeImages?: string[];
@IsOptional()
@IsArray()
afterImages?: string[];
@IsOptional()
@IsEnum(['fabric', 'leather', 'cleaning', 'repair', 'custom'])
serviceType?: string;
@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(0)
price?: number;
@IsOptional()
@IsString()
materials?: string;
@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(0)
duration?: number;
@IsOptional()
@IsEnum(['draft', 'published', 'archived'])
status?: string;
}
export class QueryCaseDto {
@IsOptional()
@IsEnum(['fabric', 'leather', 'cleaning', 'repair', 'custom'])
serviceType?: string;
@IsOptional()
@IsEnum(['draft', 'published', 'archived'])
status?: string = 'published';
@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(1)
page?: number = 1;
@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(1)
limit?: number = 10;
}

View File

@@ -0,0 +1,15 @@
import { registerAs } from '@nestjs/config';
export default registerAs('database', () => ({
type: 'mysql',
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '3306', 10),
username: process.env.DB_USER || 'root',
password: process.env.DB_PASS || '',
database: process.env.DB_NAME || 'sofa_renovation',
autoLoadEntities: true,
synchronize: process.env.NODE_ENV !== 'production', // 生产环境请设置为false使用迁移
logging: process.env.NODE_ENV === 'development',
charset: 'utf8mb4',
timezone: '+08:00',
}));

View File

@@ -0,0 +1,10 @@
import { registerAs } from '@nestjs/config';
export default registerAs('jwt', () => ({
secret: process.env.JWT_SECRET || 'your-secret-key',
signOptions: {
expiresIn: process.env.JWT_EXPIRES_IN || '7d',
},
refreshSecret: process.env.JWT_REFRESH_SECRET || 'your-refresh-secret',
refreshExpiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '30d',
}));

View File

@@ -0,0 +1,73 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm';
import { User } from './user.entity';
import { Service } from './service.entity';
@Entity('bookings')
export class Booking {
@PrimaryGeneratedColumn()
id: number;
@Column({ length: 32, unique: true })
bookingNumber: string; // 预约编号
@ManyToOne(() => User)
@JoinColumn({ name: 'customerId' })
customer: User;
@Column()
customerId: number;
@ManyToOne(() => Service)
@JoinColumn({ name: 'serviceId' })
service: Service;
@Column()
serviceId: number;
@Column({ length: 50 })
contactName: string;
@Column({ length: 20 })
contactPhone: string;
@Column({ type: 'text' })
address: string;
@Column({ type: 'datetime' })
appointmentTime: Date;
@Column({ type: 'text', nullable: true })
requirements: string; // 特殊要求
@Column({ type: 'json', nullable: true })
images: string[]; // 沙发现状图片
@Column({
type: 'enum',
enum: ['pending', 'confirmed', 'in_progress', 'completed', 'cancelled'],
default: 'pending'
})
status: string;
@Column({ type: 'decimal', precision: 10, scale: 2, nullable: true })
quotedPrice: number; // 报价
@Column({ type: 'decimal', precision: 10, scale: 2, nullable: true })
finalPrice: number; // 最终价格
@Column({ type: 'text', nullable: true })
notes: string; // 备注
@ManyToOne(() => User, { nullable: true })
@JoinColumn({ name: 'assignedWorkerId' })
assignedWorker: User;
@Column({ nullable: true })
assignedWorkerId: number;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}

View File

@@ -0,0 +1,62 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm';
import { User } from './user.entity';
@Entity('cases')
export class Case {
@PrimaryGeneratedColumn()
id: number;
@Column({ length: 100 })
title: string;
@Column({ type: 'text' })
description: string;
@Column({ type: 'json', nullable: true })
beforeImages: string[];
@Column({ type: 'json', nullable: true })
afterImages: string[];
@Column({
type: 'enum',
enum: ['fabric', 'leather', 'cleaning', 'repair', 'custom'],
default: 'fabric'
})
serviceType: string;
@Column({ type: 'decimal', precision: 10, scale: 2, nullable: true })
price: number;
@Column({ type: 'text', nullable: true })
materials: string;
@Column({ type: 'int', default: 0 })
duration: number; // 工作天数
@Column({
type: 'enum',
enum: ['draft', 'published', 'archived'],
default: 'published'
})
status: string;
@Column({ type: 'int', default: 0 })
views: number;
@Column({ type: 'int', default: 0 })
likes: number;
@ManyToOne(() => User)
@JoinColumn({ name: 'createdBy' })
creator: User;
@Column({ nullable: true })
createdBy: number;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}

View File

@@ -0,0 +1,48 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
@Entity('services')
export class Service {
@PrimaryGeneratedColumn()
id: number;
@Column({ length: 100 })
name: string;
@Column({ type: 'text' })
description: string;
@Column({
type: 'enum',
enum: ['fabric', 'leather', 'cleaning', 'repair', 'custom'],
unique: true
})
type: string;
@Column({ type: 'decimal', precision: 10, scale: 2 })
basePrice: number;
@Column({ type: 'json', nullable: true })
images: string[];
@Column({ type: 'json', nullable: true })
features: string[]; // 服务特点
@Column({ type: 'int', default: 1 })
estimatedDays: number; // 预估工期
@Column({
type: 'enum',
enum: ['active', 'inactive'],
default: 'active'
})
status: string;
@Column({ type: 'int', default: 0 })
sortOrder: number;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}

View File

@@ -0,0 +1,55 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
@Entity('users')
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true, length: 50, nullable: true })
username: string;
@Column({ unique: true, length: 100, nullable: true })
email: string;
@Column({ select: false, nullable: true })
password: string;
@Column({ length: 50, nullable: true })
realName: string;
@Column({ length: 20, nullable: true })
phone: string;
@Column({ type: 'text', nullable: true })
avatar: string;
// 微信小程序相关字段
@Column({ unique: true, length: 50, nullable: true })
openid: string; // 微信openid
@Column({ length: 100, nullable: true })
unionid: string; // 微信unionid
@Column({ type: 'text', nullable: true })
sessionKey: string; // 微信session_key
@Column({
type: 'enum',
enum: ['admin', 'customer', 'worker'],
default: 'customer'
})
role: string;
@Column({
type: 'enum',
enum: ['active', 'inactive', 'banned'],
default: 'active'
})
status: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}

52
后端/src/main.ts Normal file
View File

@@ -0,0 +1,52 @@
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 启用全局验证管道
app.useGlobalPipes(new ValidationPipe({
transform: true,
whitelist: true,
forbidNonWhitelisted: true,
}));
// 启用 CORS
app.enableCors({
origin: process.env.FRONTEND_URL || true,
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
credentials: true,
});
// API 前缀
app.setGlobalPrefix('api');
// Swagger 文档配置
const config = new DocumentBuilder()
.setTitle('优艺家沙发翻新 API')
.setDescription('沙发翻新小程序后端API文档')
.setVersion('1.0')
.addBearerAuth()
.addTag('认证', '用户认证相关接口')
.addTag('用户管理', '用户管理相关接口')
.addTag('案例管理', '案例展示相关接口')
.addTag('服务管理', '服务项目相关接口')
.addTag('预约管理', '预约订单相关接口')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('docs', app, document, {
swaggerOptions: {
persistAuthorization: true,
},
});
const port = process.env.PORT || 3000;
await app.listen(port);
console.log(`🚀 Application is running on: http://localhost:${port}`);
console.log(`📚 API Documentation: http://localhost:${port}/docs`);
}
bootstrap();

View File

@@ -0,0 +1,93 @@
import { IsNotEmpty, IsString, IsOptional, IsEnum, IsNumber, IsArray, Min } from 'class-validator';
import { Type } from 'class-transformer';
export class CreateServiceDto {
@IsNotEmpty({ message: '服务名称不能为空' })
@IsString()
name: string;
@IsNotEmpty({ message: '服务描述不能为空' })
@IsString()
description: string;
@IsEnum(['fabric', 'leather', 'cleaning', 'repair', 'custom'])
type: string;
@IsNotEmpty({ message: '基础价格不能为空' })
@Type(() => Number)
@IsNumber()
@Min(0)
basePrice: number;
@IsOptional()
@IsArray()
images?: string[];
@IsOptional()
@IsArray()
features?: string[];
@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(1)
estimatedDays?: number;
@IsOptional()
@Type(() => Number)
@IsNumber()
sortOrder?: number;
}
export class UpdateServiceDto {
@IsOptional()
@IsString()
name?: string;
@IsOptional()
@IsString()
description?: string;
@IsOptional()
@IsEnum(['fabric', 'leather', 'cleaning', 'repair', 'custom'])
type?: string;
@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(0)
basePrice?: number;
@IsOptional()
@IsArray()
images?: string[];
@IsOptional()
@IsArray()
features?: string[];
@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(1)
estimatedDays?: number;
@IsOptional()
@IsEnum(['active', 'inactive'])
status?: string;
@IsOptional()
@Type(() => Number)
@IsNumber()
sortOrder?: number;
}
export class QueryServiceDto {
@IsOptional()
@IsEnum(['fabric', 'leather', 'cleaning', 'repair', 'custom'])
type?: string;
@IsOptional()
@IsEnum(['active', 'inactive'])
status?: string = 'active';
}

View File

@@ -0,0 +1,98 @@
import { Controller, Get, Post, Body, Patch, Param, Delete, Query, UseGuards } 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';
import { Roles } from '../auth/guards/roles.decorator';
import { Public } from '../auth/guards/public.decorator';
@ApiTags('服务管理')
@Controller('services')
export class ServiceController {
constructor(private readonly serviceService: ServiceService) {}
@ApiBearerAuth()
@Post()
@Roles('admin')
@ApiOperation({ summary: '创建服务(管理员)' })
@ApiResponse({ status: 201, description: '创建成功' })
@ApiResponse({ status: 409, description: '服务类型已存在' })
create(@Body() createServiceDto: CreateServiceDto) {
return this.serviceService.create(createServiceDto);
}
@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' })
@ApiResponse({ status: 200, description: '获取成功' })
findAll(@Query() query: QueryServiceDto) {
return this.serviceService.findAll(query);
}
@Public()
@Get('active')
@ApiOperation({ summary: '获取所有有效服务' })
@ApiResponse({ status: 200, description: '获取成功' })
getActiveServices() {
return this.serviceService.getActiveServices();
}
@Public()
@Get(':id')
@ApiOperation({ summary: '根据ID获取服务详情' })
@ApiResponse({ status: 200, description: '获取成功' })
@ApiResponse({ status: 404, description: '服务不存在' })
findOne(@Param('id') id: string) {
return this.serviceService.findOne(+id);
}
@Public()
@Get('type/:type')
@ApiOperation({ summary: '根据类型获取服务' })
@ApiResponse({ status: 200, description: '获取成功' })
@ApiResponse({ status: 404, description: '服务类型不存在' })
findByType(@Param('type') type: string) {
return this.serviceService.findByType(type);
}
@ApiBearerAuth()
@Patch(':id')
@Roles('admin')
@ApiOperation({ summary: '更新服务(管理员)' })
@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);
}
@ApiBearerAuth()
@Delete(':id')
@Roles('admin')
@ApiOperation({ summary: '删除服务(管理员)' })
@ApiResponse({ status: 200, description: '删除成功' })
@ApiResponse({ status: 404, description: '服务不存在' })
remove(@Param('id') id: string) {
return this.serviceService.remove(+id);
}
@ApiBearerAuth()
@Patch(':id/toggle-status')
@Roles('admin')
@ApiOperation({ summary: '切换服务状态(管理员)' })
@ApiResponse({ status: 200, description: '状态切换成功' })
@ApiResponse({ status: 404, description: '服务不存在' })
toggleStatus(@Param('id') id: string) {
return this.serviceService.toggleStatus(+id);
}
@ApiBearerAuth()
@Patch('sort-order')
@Roles('admin')
@ApiOperation({ summary: '更新服务排序(管理员)' })
@ApiResponse({ status: 200, description: '排序更新成功' })
updateSortOrder(@Body() serviceOrders: { id: number; sortOrder: number }[]) {
return this.serviceService.updateSortOrder(serviceOrders);
}
}

View File

@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ServiceService } from './service.service';
import { ServiceController } from './service.controller';
import { Service } from '../entities/service.entity';
@Module({
imports: [TypeOrmModule.forFeature([Service])],
controllers: [ServiceController],
providers: [ServiceService],
exports: [ServiceService],
})
export class ServiceModule {}

View File

@@ -0,0 +1,117 @@
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Service } from '../entities/service.entity';
import { CreateServiceDto, UpdateServiceDto, QueryServiceDto } from './dto/service.dto';
@Injectable()
export class ServiceService {
constructor(
@InjectRepository(Service)
private serviceRepository: Repository<Service>,
) {}
async create(createServiceDto: CreateServiceDto): Promise<Service> {
// 检查服务类型是否已存在
const existingService = await this.serviceRepository.findOne({
where: { type: createServiceDto.type }
});
if (existingService) {
throw new ConflictException('该服务类型已存在');
}
const service = this.serviceRepository.create({
...createServiceDto,
status: 'active',
});
return this.serviceRepository.save(service);
}
async findAll(query: QueryServiceDto): Promise<Service[]> {
const { type, status } = query;
const queryBuilder = this.serviceRepository.createQueryBuilder('service');
if (type) {
queryBuilder.andWhere('service.type = :type', { type });
}
if (status) {
queryBuilder.andWhere('service.status = :status', { status });
}
queryBuilder.orderBy('service.sortOrder', 'ASC');
queryBuilder.addOrderBy('service.createdAt', 'DESC');
return queryBuilder.getMany();
}
async findOne(id: number): Promise<Service> {
const service = await this.serviceRepository.findOne({
where: { id }
});
if (!service) {
throw new NotFoundException('服务不存在');
}
return service;
}
async findByType(type: string): Promise<Service> {
const service = await this.serviceRepository.findOne({
where: { type }
});
if (!service) {
throw new NotFoundException('服务类型不存在');
}
return service;
}
async update(id: number, updateServiceDto: UpdateServiceDto): Promise<Service> {
const service = await this.findOne(id);
// 如果要更新服务类型,检查新类型是否已被其他服务使用
if (updateServiceDto.type && updateServiceDto.type !== service.type) {
const existingService = await this.serviceRepository.findOne({
where: { type: updateServiceDto.type }
});
if (existingService) {
throw new ConflictException('该服务类型已存在');
}
}
await this.serviceRepository.update(id, updateServiceDto);
return this.findOne(id);
}
async remove(id: number): Promise<void> {
const service = await this.findOne(id);
await this.serviceRepository.remove(service);
}
async getActiveServices(): Promise<Service[]> {
return this.serviceRepository.find({
where: { status: 'active' },
order: { sortOrder: 'ASC', createdAt: 'DESC' }
});
}
async updateSortOrder(serviceOrders: { id: number; sortOrder: number }[]): Promise<void> {
for (const { id, sortOrder } of serviceOrders) {
await this.serviceRepository.update(id, { sortOrder });
}
}
async toggleStatus(id: number): Promise<Service> {
const service = await this.findOne(id);
const newStatus = service.status === 'active' ? 'inactive' : 'active';
await this.serviceRepository.update(id, { status: newStatus });
return this.findOne(id);
}
}

View File

@@ -0,0 +1,64 @@
import { IsOptional, IsString, IsEmail, IsEnum } from 'class-validator';
export class CreateUserDto {
@IsString()
username: string;
@IsEmail()
email: string;
@IsOptional()
@IsString()
password?: string;
@IsOptional()
@IsString()
realName?: string;
@IsOptional()
@IsString()
phone?: string;
@IsOptional()
@IsString()
avatar?: string;
@IsOptional()
@IsEnum(['admin', 'customer', 'worker'])
role?: string;
@IsOptional()
@IsEnum(['active', 'inactive', 'banned'])
status?: string;
// 微信相关字段
@IsOptional()
@IsString()
openid?: string;
@IsOptional()
@IsString()
unionid?: string;
@IsOptional()
@IsString()
sessionKey?: string;
}
export class UpdateUserDto {
@IsOptional()
@IsString()
realName?: string;
@IsOptional()
@IsString()
phone?: string;
@IsOptional()
@IsString()
avatar?: string;
@IsOptional()
@IsEnum(['active', 'inactive', 'banned'])
status?: string;
}

View File

@@ -0,0 +1,71 @@
import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
import { UserService } from './user.service';
import { CreateUserDto, UpdateUserDto } from './dto/user.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';
@ApiTags('用户管理')
@ApiBearerAuth()
@Controller('users')
export class UserController {
constructor(private readonly userService: UserService) {}
@Post()
@Roles('admin')
@ApiOperation({ summary: '创建用户(管理员)' })
@ApiResponse({ status: 201, description: '创建成功' })
create(@Body() createUserDto: CreateUserDto) {
return this.userService.create(createUserDto);
}
@Get()
@Roles('admin')
@ApiOperation({ summary: '获取所有用户(管理员)' })
@ApiResponse({ status: 200, description: '获取成功' })
findAll() {
return this.userService.findAll();
}
@Get('profile')
@ApiOperation({ summary: '获取当前用户信息' })
@ApiResponse({ status: 200, description: '获取成功' })
getProfile(@CurrentUser() user: CurrentUserData) {
return this.userService.getUserProfile(user.userId);
}
@Get(':id')
@Roles('admin')
@ApiOperation({ summary: '根据ID获取用户管理员' })
@ApiResponse({ status: 200, description: '获取成功' })
@ApiResponse({ status: 404, description: '用户不存在' })
findOne(@Param('id') id: string) {
return this.userService.findById(+id);
}
@Patch('profile')
@ApiOperation({ summary: '更新当前用户信息' })
@ApiResponse({ status: 200, description: '更新成功' })
updateProfile(@CurrentUser() user: CurrentUserData, @Body() updateUserDto: UpdateUserDto) {
return this.userService.update(user.userId, updateUserDto);
}
@Patch(':id')
@Roles('admin')
@ApiOperation({ summary: '更新用户信息(管理员)' })
@ApiResponse({ status: 200, description: '更新成功' })
@ApiResponse({ status: 404, description: '用户不存在' })
update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
return this.userService.update(+id, updateUserDto);
}
@Delete(':id')
@Roles('admin')
@ApiOperation({ summary: '删除用户(管理员)' })
@ApiResponse({ status: 200, description: '删除成功' })
@ApiResponse({ status: 404, description: '用户不存在' })
remove(@Param('id') id: string) {
return this.userService.remove(+id);
}
}

View File

@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserService } from './user.service';
import { UserController } from './user.controller';
import { User } from '../entities/user.entity';
@Module({
imports: [TypeOrmModule.forFeature([User])],
controllers: [UserController],
providers: [UserService],
exports: [UserService],
})
export class UserModule {}

View File

@@ -0,0 +1,100 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from '../entities/user.entity';
import { CreateUserDto, UpdateUserDto } from './dto/user.dto';
@Injectable()
export class UserService {
constructor(
@InjectRepository(User)
private userRepository: Repository<User>,
) {}
async create(createUserDto: CreateUserDto): Promise<User> {
const user = this.userRepository.create(createUserDto);
return this.userRepository.save(user);
}
async findAll(): Promise<User[]> {
return this.userRepository.find({
select: ['id', 'username', 'email', 'realName', 'phone', 'avatar', 'role', 'status', 'createdAt', 'updatedAt'],
});
}
async findById(id: number): Promise<User | null> {
return this.userRepository.findOne({
where: { id },
select: ['id', 'username', 'email', 'realName', 'phone', 'avatar', 'role', 'status', 'createdAt', 'updatedAt'],
});
}
async findByUsername(username: string): Promise<User | null> {
return this.userRepository.findOne({
where: { username },
select: ['id', 'username', 'email', 'realName', 'phone', 'avatar', 'role', 'status', 'createdAt', 'updatedAt'],
});
}
async findByEmail(email: string): Promise<User | null> {
return this.userRepository.findOne({
where: { email },
select: ['id', 'username', 'email', 'realName', 'phone', 'avatar', 'role', 'status', 'createdAt', 'updatedAt'],
});
}
async findByUsernameWithPassword(username: string): Promise<User | null> {
return this.userRepository.findOne({
where: { username },
select: ['id', 'username', 'email', 'password', 'realName', 'phone', 'avatar', 'role', 'status'],
});
}
async findByEmailWithPassword(email: string): Promise<User | null> {
return this.userRepository.findOne({
where: { email },
select: ['id', 'username', 'email', 'password', 'realName', 'phone', 'avatar', 'role', 'status'],
});
}
async update(id: number, updateUserDto: UpdateUserDto): Promise<User> {
const user = await this.findById(id);
if (!user) {
throw new NotFoundException('用户不存在');
}
await this.userRepository.update(id, updateUserDto);
const updatedUser = await this.findById(id);
if (!updatedUser) {
throw new NotFoundException('更新后用户不存在');
}
return updatedUser;
}
async remove(id: number): Promise<void> {
const user = await this.findById(id);
if (!user) {
throw new NotFoundException('用户不存在');
}
await this.userRepository.remove(user);
}
async getUserProfile(id: number): Promise<User> {
const user = await this.findById(id);
if (!user) {
throw new NotFoundException('用户不存在');
}
return user;
}
async findByOpenid(openid: string): Promise<User | null> {
return this.userRepository.findOne({
where: { openid },
select: ['id', 'username', 'email', 'realName', 'phone', 'avatar', 'role', 'status', 'openid', 'unionid', 'createdAt', 'updatedAt'],
});
}
async updateSessionKey(userId: number, sessionKey: string): Promise<void> {
await this.userRepository.update(userId, { sessionKey });
}
}