import { Injectable, NotFoundException, ConflictException, BadRequestException, Logger, } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository, FindOptionsWhere } from 'typeorm'; import * as bcrypt from 'bcrypt'; import { User } from './user.entity'; import { CreateUserDto } from './dto/create-user.dto'; import { UpdateUserDto } from './dto/update-user.dto'; import { QueryUserDto } from './dto/query-user.dto'; import { ChangePasswordDto } from './dto/change-password.dto'; import { PaginationInfo } from '@/common/dto/pagination.dto'; import { StorageService } from '../storage/storage.service'; @Injectable() export class UserService { private readonly logger = new Logger(UserService.name); constructor( @InjectRepository(User) private readonly userRepository: Repository, private readonly storageService: StorageService, ) {} /** * 注册用户 */ async create(createUserDto: CreateUserDto): Promise { // 检查用户名是否已存在 const existingByUsername = await this.userRepository.findOne({ where: { username: createUserDto.username }, }); if (existingByUsername) { throw new ConflictException( `用户名 "${createUserDto.username}" 已存在`, ); } // 检查邮箱是否已存在 const existingByEmail = await this.userRepository.findOne({ where: { email: createUserDto.email }, }); if (existingByEmail) { throw new ConflictException(`邮箱 "${createUserDto.email}" 已存在`); } // 检查 phone 是否已存在(如果提供了) /* if (createUserDto.phone) { const existingByPhone = await this.userRepository.findOne({ where: { phone: createUserDto.phone }, }); if (existingByPhone) { throw new ConflictException( `Phone "${createUserDto.phone}" already exists`, ); } } // 检查 openId 是否已存在(如果提供了) if (createUserDto.openId) { const existingByOpenId = await this.userRepository.findOne({ where: { openId: createUserDto.openId }, }); if (existingByOpenId) { throw new ConflictException( `OpenId "${createUserDto.openId}" already exists`, ); } } // 检查 unionId 是否已存在(如果提供了) if (createUserDto.unionId) { const existingByUnionId = await this.userRepository.findOne({ where: { unionId: createUserDto.unionId }, }); if (existingByUnionId) { throw new ConflictException( `UnionId "${createUserDto.unionId}" already exists`, ); } } */ // 使用 bcrypt 加密密码 const saltRounds = 10; const passwordHash = await bcrypt.hash( createUserDto.password, saltRounds, ); // 创建用户 // 注意:所有注册用户的 role 固定为 'user',不允许通过注册接口设置其他角色 const user = this.userRepository.create({ username: createUserDto.username, passwordHash, email: createUserDto.email, nickname: createUserDto.nickname, avatarUrl: createUserDto.avatarUrl, phone: createUserDto.phone, openId: createUserDto.openId, unionId: createUserDto.unionId, status: 'active', role: 'user', // 固定为普通用户,不允许通过注册接口修改 }); return this.userRepository.save(user); } /** * 查询所有用户 */ async findAll(): Promise { return this.userRepository.find({ order: { createdAt: 'DESC', }, }); } /** * 查询用户(支持多种查询条件和分页) */ async findAllPaginated(queryDto: QueryUserDto): Promise<{ list: User[]; pagination: PaginationInfo; }> { const where: FindOptionsWhere = {}; if (queryDto.username) { where.username = queryDto.username; } if (queryDto.nickname) { where.nickname = queryDto.nickname; } if (queryDto.email) { where.email = queryDto.email; } if (queryDto.phone) { where.phone = queryDto.phone; } if (queryDto.role) { where.role = queryDto.role; } if (queryDto.status) { where.status = queryDto.status; } // 分页参数 const page = queryDto.page || 1; const limit = queryDto.limit || 10; const skip = (page - 1) * limit; // 排序字段映射 const sortBy = queryDto.sortBy || 'createdAt'; const sortOrder = queryDto.sortOrder || 'DESC'; // 构建排序对象 const order: Record = {}; if (sortBy === 'createdAt') { order.createdAt = sortOrder; } else if (sortBy === 'updatedAt') { order.updatedAt = sortOrder; } else if (sortBy === 'lastLoginAt') { order.lastLoginAt = sortOrder; } else { order.createdAt = 'DESC'; } // 添加默认排序 order.userId = 'ASC'; // 查询总数 const total = await this.userRepository.count({ where }); // 查询分页数据 const list = await this.userRepository.find({ where, order, skip, take: limit, }); // 移除 passwordHash 字段 const listWithoutPassword = list.map((user) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { passwordHash, ...userWithoutPassword } = user; return userWithoutPassword as User; }); // 计算总页数 const total_page = Math.ceil(total / limit); return { list: listWithoutPassword, pagination: { total, total_page, page_size: limit, current_page: page, }, }; } /** * 根据 username 或 email 查询单个用户 */ async findOne(queryDto: QueryUserDto): Promise { if (!queryDto.username && !queryDto.email) { throw new BadRequestException('必须提供用户名或邮箱'); } const where: { username?: string; email?: string } = {}; if (queryDto.username) { where.username = queryDto.username; } if (queryDto.email) { where.email = queryDto.email; } const user = await this.userRepository.findOne({ where }); if (!user) { const identifier = queryDto.username || queryDto.email; throw new NotFoundException( `未找到${queryDto.username ? '用户名' : '邮箱'}为 "${identifier}" 的用户`, ); } return user; } /** * 根据 ID 查询单个用户 */ async findOneById(id: number): Promise { const user = await this.userRepository.findOne({ where: { userId: id }, }); if (!user) { throw new NotFoundException(`未找到ID为 ${id} 的用户`); } // 移除 passwordHash 字段 // eslint-disable-next-line @typescript-eslint/no-unused-vars const { passwordHash, ...userWithoutPassword } = user; return userWithoutPassword as User; } /** * 更新用户信息(不允许修改 username、openId、unionId) */ async update(id: number, updateUserDto: UpdateUserDto): Promise { const user = await this.findOneById(id); // 如果更新邮箱,检查是否与其他用户冲突 if (updateUserDto.email && updateUserDto.email !== user.email) { const existingByEmail = await this.userRepository.findOne({ where: { email: updateUserDto.email }, }); if (existingByEmail) { throw new ConflictException( `邮箱 "${updateUserDto.email}" 已存在`, ); } } // 如果更新 phone,检查是否与其他用户冲突 if (updateUserDto.phone && updateUserDto.phone !== user.phone) { const existingByPhone = await this.userRepository.findOne({ where: { phone: updateUserDto.phone }, }); if (existingByPhone) { throw new ConflictException( `手机号 "${updateUserDto.phone}" 已存在`, ); } } // 更新用户信息 Object.assign(user, updateUserDto); return this.userRepository.save(user); } /** * 修改密码(需要先验证旧密码) */ async changePassword( id: number, changePasswordDto: ChangePasswordDto, ): Promise { const user = await this.findOneById(id); // 验证旧密码 const isOldPasswordValid = await bcrypt.compare( changePasswordDto.oldPassword, user.passwordHash, ); if (!isOldPasswordValid) { throw new BadRequestException('旧密码错误'); } // 加密新密码 const saltRounds = 10; const newPasswordHash = await bcrypt.hash( changePasswordDto.newPassword, saltRounds, ); // 更新密码 user.passwordHash = newPasswordHash; await this.userRepository.save(user); } /** * 删除用户(软删除,更新状态为 deleted) * 同时删除用户头像图片 */ async remove(id: number): Promise { const user = await this.findOneById(id); // 删除用户头像图片 if (user.avatarUrl) { try { const imagePath = this.storageService.extractStoragePath( user.avatarUrl, ); if (imagePath) { await this.storageService.delete(imagePath); this.logger.log(`已删除用户头像: ${imagePath}`); } } catch (error) { // 图片删除失败不影响用户删除操作 this.logger.warn(`删除用户头像失败: ${user.avatarUrl}`, error); } } user.status = 'deleted'; await this.userRepository.save(user); } }