373 lines
10 KiB
TypeScript
373 lines
10 KiB
TypeScript
import { InjectRedis } from '@liaoliaots/nestjs-redis'
|
||
import { BadRequestException, Injectable } from '@nestjs/common'
|
||
import { InjectEntityManager, InjectRepository } from '@nestjs/typeorm'
|
||
import Redis from 'ioredis'
|
||
import { isEmpty, isNil, isNumber } from 'lodash'
|
||
|
||
import { EntityManager, In, Like, Repository } from 'typeorm'
|
||
|
||
import { BusinessException } from '~/common/exceptions/biz.exception'
|
||
import { ErrorEnum } from '~/constants/error-code.constant'
|
||
import { ROOT_ROLE_ID, SYS_USER_INITPASSWORD } from '~/constants/system.constant'
|
||
import { genAuthPVKey, genAuthPermKey, genAuthTokenKey } from '~/helper/genRedisKey'
|
||
|
||
import { paginate } from '~/helper/paginate'
|
||
import { Pagination } from '~/helper/paginate/pagination'
|
||
import { AccountUpdateDto } from '~/modules/auth/dto/account.dto'
|
||
import { RegisterDto } from '~/modules/auth/dto/auth.dto'
|
||
import { QQService } from '~/shared/helper/qq.service'
|
||
|
||
import { md5, randomValue } from '~/utils'
|
||
|
||
import { DeptEntity } from '../system/dept/dept.entity'
|
||
import { ParamConfigService } from '../system/param-config/param-config.service'
|
||
import { RoleEntity } from '../system/role/role.entity'
|
||
|
||
import { UserStatus } from './constant'
|
||
import { PasswordUpdateDto } from './dto/password.dto'
|
||
import { UserDto, UserQueryDto, UserUpdateDto } from './dto/user.dto'
|
||
import { UserEntity } from './user.entity'
|
||
import { AccountInfo } from './user.model'
|
||
|
||
@Injectable()
|
||
export class UserService {
|
||
constructor(
|
||
@InjectRedis()
|
||
private readonly redis: Redis,
|
||
@InjectRepository(UserEntity)
|
||
private readonly userRepository: Repository<UserEntity>,
|
||
@InjectRepository(RoleEntity)
|
||
private readonly roleRepository: Repository<RoleEntity>,
|
||
@InjectEntityManager() private entityManager: EntityManager,
|
||
private readonly paramConfigService: ParamConfigService,
|
||
private readonly qqService: QQService,
|
||
) {}
|
||
|
||
async findUserById(id: number): Promise<UserEntity | undefined> {
|
||
return this.userRepository
|
||
.createQueryBuilder('user')
|
||
.where({
|
||
id,
|
||
status: UserStatus.Enabled,
|
||
})
|
||
.getOne()
|
||
}
|
||
|
||
async findUserByUserName(username: string): Promise<UserEntity | undefined> {
|
||
return this.userRepository
|
||
.createQueryBuilder('user')
|
||
.where({
|
||
username,
|
||
status: UserStatus.Enabled,
|
||
})
|
||
.getOne()
|
||
}
|
||
|
||
/**
|
||
* 获取用户信息
|
||
* @param uid user id
|
||
*/
|
||
async getAccountInfo(uid: number): Promise<AccountInfo> {
|
||
const user: UserEntity = await this.userRepository
|
||
.createQueryBuilder('user')
|
||
.leftJoinAndSelect('user.roles', 'role')
|
||
.where(`user.id = :uid`, { uid })
|
||
.getOne()
|
||
|
||
if (isEmpty(user))
|
||
throw new BusinessException(ErrorEnum.USER_NOT_FOUND)
|
||
|
||
delete user?.psalt
|
||
|
||
return user
|
||
}
|
||
|
||
/**
|
||
* 更新个人信息
|
||
*/
|
||
async updateAccountInfo(uid: number, info: AccountUpdateDto): Promise<void> {
|
||
const user = await this.userRepository.findOneBy({ id: uid })
|
||
if (isEmpty(user))
|
||
throw new BusinessException(ErrorEnum.USER_NOT_FOUND)
|
||
|
||
const data = {
|
||
...(info.nickname ? { nickname: info.nickname } : null),
|
||
...(info.avatar ? { avatar: info.avatar } : null),
|
||
...(info.email ? { email: info.email } : null),
|
||
...(info.phone ? { phone: info.phone } : null),
|
||
...(info.qq ? { qq: info.qq } : null),
|
||
...(info.remark ? { remark: info.remark } : null),
|
||
}
|
||
|
||
if (!info.avatar && info.qq) {
|
||
// 如果qq不等于原qq,则更新qq头像
|
||
if (info.qq !== user.qq)
|
||
data.avatar = await this.qqService.getAvater(info.qq)
|
||
}
|
||
|
||
await this.userRepository.update(uid, data)
|
||
}
|
||
|
||
/**
|
||
* 更改密码
|
||
*/
|
||
async updatePassword(uid: number, dto: PasswordUpdateDto): Promise<void> {
|
||
const user = await this.userRepository.findOneBy({ id: uid })
|
||
if (isEmpty(user))
|
||
throw new BusinessException(ErrorEnum.USER_NOT_FOUND)
|
||
|
||
const comparePassword = md5(`${dto.oldPassword}${user.psalt}`)
|
||
// 原密码不一致,不允许更改
|
||
if (user.password !== comparePassword)
|
||
throw new BusinessException(ErrorEnum.PASSWORD_MISMATCH)
|
||
|
||
const password = md5(`${dto.newPassword}${user.psalt}`)
|
||
await this.userRepository.update({ id: uid }, { password })
|
||
await this.upgradePasswordV(user.id)
|
||
}
|
||
|
||
/**
|
||
* 直接更改密码
|
||
*/
|
||
async forceUpdatePassword(uid: number, password: string): Promise<void> {
|
||
const user = await this.userRepository.findOneBy({ id: uid })
|
||
|
||
const newPassword = md5(`${password}${user.psalt}`)
|
||
await this.userRepository.update({ id: uid }, { password: newPassword })
|
||
await this.upgradePasswordV(user.id)
|
||
}
|
||
|
||
/**
|
||
* 增加系统用户,如果返回false则表示已存在该用户
|
||
*/
|
||
async create({
|
||
username,
|
||
password,
|
||
roleIds,
|
||
deptId,
|
||
...data
|
||
}: UserDto): Promise<void> {
|
||
const exists = await this.userRepository.findOneBy({
|
||
username,
|
||
})
|
||
if (!isEmpty(exists))
|
||
throw new BusinessException(ErrorEnum.SYSTEM_USER_EXISTS)
|
||
|
||
await this.entityManager.transaction(async (manager) => {
|
||
const salt = randomValue(32)
|
||
|
||
if (!password) {
|
||
const initPassword = await this.paramConfigService.findValueByKey(
|
||
SYS_USER_INITPASSWORD,
|
||
)
|
||
password = md5(`${initPassword ?? '123456'}${salt}`)
|
||
}
|
||
else {
|
||
password = md5(`${password ?? '123456'}${salt}`)
|
||
}
|
||
const u = manager.create(UserEntity, {
|
||
username,
|
||
password,
|
||
...data,
|
||
psalt: salt,
|
||
roles: await this.roleRepository.findBy({ id: In(roleIds) }),
|
||
dept: await DeptEntity.findOneBy({ id: deptId }),
|
||
})
|
||
|
||
const result = await manager.save(u)
|
||
return result
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 更新用户信息
|
||
*/
|
||
async update(
|
||
id: number,
|
||
{ password, deptId, roleIds, status, ...data }: UserUpdateDto,
|
||
): Promise<void> {
|
||
await this.entityManager.transaction(async (manager) => {
|
||
if (password)
|
||
await this.forceUpdatePassword(id, password)
|
||
|
||
await manager.update(UserEntity, id, {
|
||
...data,
|
||
status,
|
||
})
|
||
|
||
const user = await this.userRepository
|
||
.createQueryBuilder('user')
|
||
.leftJoinAndSelect('user.roles', 'roles')
|
||
.leftJoinAndSelect('user.dept', 'dept')
|
||
.where('user.id = :id', { id })
|
||
.getOne()
|
||
|
||
await manager
|
||
.createQueryBuilder()
|
||
.relation(UserEntity, 'roles')
|
||
.of(id)
|
||
.addAndRemove(roleIds, user.roles)
|
||
|
||
await manager
|
||
.createQueryBuilder()
|
||
.relation(UserEntity, 'dept')
|
||
.of(id)
|
||
.set(deptId)
|
||
|
||
if (status === 0) {
|
||
// 禁用状态
|
||
await this.forbidden(id)
|
||
}
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 查找用户信息
|
||
* @param id 用户id
|
||
*/
|
||
async info(id: number): Promise<UserEntity> {
|
||
const user = await this.userRepository
|
||
.createQueryBuilder('user')
|
||
.leftJoinAndSelect('user.roles', 'roles')
|
||
.leftJoinAndSelect('user.dept', 'dept')
|
||
.where('user.id = :id', { id })
|
||
.getOne()
|
||
|
||
delete user.password
|
||
delete user.psalt
|
||
|
||
return user
|
||
}
|
||
|
||
/**
|
||
* 根据ID列表删除用户
|
||
*/
|
||
async delete(userIds: number[]): Promise<void | never> {
|
||
const rootUserId = await this.findRootUserId()
|
||
if (userIds.includes(rootUserId))
|
||
throw new BadRequestException('不能删除root用户!')
|
||
|
||
await this.userRepository.delete(userIds)
|
||
}
|
||
|
||
/**
|
||
* 查找超管的用户ID
|
||
*/
|
||
async findRootUserId(): Promise<number> {
|
||
const user = await this.userRepository.findOneBy({
|
||
roles: { id: ROOT_ROLE_ID },
|
||
})
|
||
return user.id
|
||
}
|
||
|
||
/**
|
||
* 查询用户列表
|
||
*/
|
||
async list({
|
||
page,
|
||
pageSize,
|
||
username,
|
||
nickname,
|
||
deptId,
|
||
email,
|
||
status,
|
||
}: UserQueryDto): Promise<Pagination<UserEntity>> {
|
||
const queryBuilder = this.userRepository
|
||
.createQueryBuilder('user')
|
||
.leftJoinAndSelect('user.dept', 'dept')
|
||
.leftJoinAndSelect('user.roles', 'role')
|
||
// .where('user.id NOT IN (:...ids)', { ids: [rootUserId, uid] })
|
||
.where({
|
||
...(username ? { username: Like(`%${username}%`) } : null),
|
||
...(nickname ? { nickname: Like(`%${nickname}%`) } : null),
|
||
...(email ? { email: Like(`%${email}%`) } : null),
|
||
...(isNumber(status) ? { status } : null),
|
||
})
|
||
|
||
if (deptId)
|
||
queryBuilder.andWhere('dept.id = :deptId', { deptId })
|
||
|
||
return paginate<UserEntity>(queryBuilder, {
|
||
page,
|
||
pageSize,
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 禁用用户
|
||
*/
|
||
async forbidden(uid: number): Promise<void> {
|
||
await this.redis.del(genAuthPVKey(uid))
|
||
await this.redis.del(genAuthTokenKey(uid))
|
||
await this.redis.del(genAuthPermKey(uid))
|
||
}
|
||
|
||
/**
|
||
* 禁用多个用户
|
||
*/
|
||
async multiForbidden(uids: number[]): Promise<void> {
|
||
if (uids) {
|
||
const pvs: string[] = []
|
||
const ts: string[] = []
|
||
const ps: string[] = []
|
||
uids.forEach((uid) => {
|
||
pvs.push(genAuthPVKey(uid))
|
||
ts.push(genAuthTokenKey(uid))
|
||
ps.push(genAuthPermKey(uid))
|
||
})
|
||
await this.redis.del(pvs)
|
||
await this.redis.del(ts)
|
||
await this.redis.del(ps)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 升级用户版本密码
|
||
*/
|
||
async upgradePasswordV(id: number): Promise<void> {
|
||
// admin:passwordVersion:${param.id}
|
||
const v = await this.redis.get(genAuthPVKey(id))
|
||
if (!isEmpty(v))
|
||
await this.redis.set(genAuthPVKey(id), Number.parseInt(v) + 1)
|
||
}
|
||
|
||
/**
|
||
* 判断用户名是否存在
|
||
*/
|
||
async exist(username: string) {
|
||
const user = await this.userRepository.findOneBy({ username })
|
||
if (isNil(user))
|
||
throw new BusinessException(ErrorEnum.SYSTEM_USER_EXISTS)
|
||
|
||
return true
|
||
}
|
||
|
||
/**
|
||
* 注册
|
||
*/
|
||
async register({ username, ...data }: RegisterDto): Promise<void> {
|
||
const exists = await this.userRepository.findOneBy({
|
||
username,
|
||
})
|
||
if (!isEmpty(exists))
|
||
throw new BusinessException(ErrorEnum.SYSTEM_USER_EXISTS)
|
||
|
||
await this.entityManager.transaction(async (manager) => {
|
||
const salt = randomValue(32)
|
||
|
||
const password = md5(`${data.password ?? 'a123456'}${salt}`)
|
||
|
||
const u = manager.create(UserEntity, {
|
||
username,
|
||
password,
|
||
status: 1,
|
||
psalt: salt,
|
||
})
|
||
|
||
const user = await manager.save(u)
|
||
|
||
return user
|
||
})
|
||
}
|
||
}
|