Files
invest-mind-store/apps/api/src/modules/user/user.service.ts
T
2026-01-12 17:38:55 +08:00

358 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<User>,
private readonly storageService: StorageService,
) {}
/**
* 注册用户
*/
async create(createUserDto: CreateUserDto): Promise<User> {
// 检查用户名是否已存在
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<User[]> {
return this.userRepository.find({
order: {
createdAt: 'DESC',
},
});
}
/**
* 查询用户(支持多种查询条件和分页)
*/
async findAllPaginated(queryDto: QueryUserDto): Promise<{
list: User[];
pagination: PaginationInfo;
}> {
const where: FindOptionsWhere<User> = {};
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<string, 'ASC' | 'DESC'> = {};
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<User> {
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<User> {
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<User> {
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<void> {
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<void> {
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);
}
}