GuDingZiChan/backend/src/controllers/userController.ts

445 lines
14 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 { Request, Response } from 'express'
import bcrypt from 'bcryptjs'
import type { Prisma } from '@prisma/client'
import prisma from '../utils/prisma'
import { successResponse, errorResponse } from '../utils/response'
import { getPermissionsForRole, isUserRole } from '../utils/permissions'
const SALT_ROUNDS = 10
const USER_STATUSES = ['active', 'inactive'] as const
class UserValidationError extends Error {
constructor(message: string, public readonly status = 422) {
super(message)
this.name = 'UserValidationError'
}
}
const sanitizeUser = (user: any) => {
const person = user.person
? {
id: user.person.id,
real_name: user.person.real_name,
nickname: user.person.nickname,
status: user.person.status,
source_employee_status: user.person.source_employee_status
}
: null
return {
id: user.id,
username: user.username,
name: user.name,
email: user.email,
phone: user.phone,
role: user.role,
roles: [user.role],
permissions: getPermissionsForRole(user.role),
status: user.status,
person_id: user.person_id,
person_bound: Boolean(user.person_id),
dingtalk_ready: Boolean(
user.status === 'active' &&
user.person?.status === 'active' &&
user.person?.source_userid?.trim()
),
person,
last_login_at: user.last_login_at,
created_at: user.created_at,
updated_at: user.updated_at
}
}
const personSelect = {
id: true,
real_name: true,
nickname: true,
status: true,
source_employee_status: true,
source_userid: true
} as const
const normalizePersonId = (value: unknown) => {
if (value === undefined) return undefined
if (value === null || value === '') return null
const personId = Number(value)
if (!Number.isInteger(personId) || personId <= 0) {
throw new UserValidationError('员工绑定无效')
}
return personId
}
const normalizeUserStatus = (value: unknown, fallback?: string) => {
const status = value === undefined || value === null || value === ''
? fallback
: String(value).trim()
if (!status || !USER_STATUSES.includes(status as (typeof USER_STATUSES)[number])) {
throw new UserValidationError('状态必须是 active 或 inactive')
}
return status
}
const getCurrentUserId = (res: Response) => {
const userId = Number((res as any).locals?.user?.id)
return Number.isInteger(userId) && userId > 0 ? userId : null
}
const lockActiveAdministrators = async (tx: Prisma.TransactionClient) => {
return tx.$queryRawUnsafe<Array<{ id: number }>>(
"SELECT id FROM `user` WHERE role = 'admin' AND status = 'active' ORDER BY id FOR UPDATE"
)
}
const assertActiveAdministratorRemains = async (
tx: Prisma.TransactionClient,
targetUserId: number
) => {
const activeAdministrators = await lockActiveAdministrators(tx)
const targetIsActiveAdministrator = activeAdministrators.some((item) => Number(item.id) === targetUserId)
if (targetIsActiveAdministrator && activeAdministrators.length <= 1) {
throw new UserValidationError('系统必须至少保留一个启用的管理员账户', 409)
}
}
const ensurePersonCanBind = async (personId: number, excludeUserId?: number) => {
const person = await prisma.person.findUnique({ where: { id: personId } })
if (!person) {
throw new UserValidationError('绑定的员工档案不存在')
}
if (person.status !== 'active') {
throw new UserValidationError('只能绑定在职且启用的员工档案')
}
if (!person.source_userid?.trim()) {
throw new UserValidationError('该员工档案缺少钉钉 userId,暂时不能绑定')
}
const occupied = await prisma.user.findFirst({
where: {
person_id: personId,
...(excludeUserId ? { id: { not: excludeUserId } } : {})
},
select: { id: true }
})
if (occupied) {
throw new UserValidationError('该员工档案已绑定其他用户', 409)
}
}
const sendUserWriteError = (res: Response, error: any, fallbackMessage: string) => {
if (error?.code === 'P2002') {
const targets = Array.isArray(error?.meta?.target)
? error.meta.target.map(String)
: [String(error?.meta?.target || '')]
const message = targets.some((target: string) => target.includes('person_id'))
? '该员工档案已绑定其他用户'
: targets.some((target: string) => target.includes('username'))
? '用户名已存在'
: '用户资料与现有记录冲突'
return errorResponse(res, message, 409)
}
return errorResponse(res, error.message || fallbackMessage, error.status || 500)
}
export const getUserList = async (req: Request, res: Response) => {
try {
const { page = 1, pageSize = 10, keyword, role, status } = req.query
const where: any = {}
if (keyword) {
where.OR = [
{ username: { contains: keyword as string } },
{ name: { contains: keyword as string } },
{ email: { contains: keyword as string } }
]
}
if (role) {
where.role = role
}
if (status) {
where.status = status
}
const skip = (Number(page) - 1) * Number(pageSize)
const [list, total] = await Promise.all([
prisma.user.findMany({
where,
skip,
take: Number(pageSize),
orderBy: { created_at: 'desc' },
include: { person: { select: personSelect } }
}),
prisma.user.count({ where })
])
successResponse(
res,
{
list: list.map(sanitizeUser),
total,
page: Number(page),
pageSize: Number(pageSize)
},
'获取成功'
)
} catch (error: any) {
errorResponse(res, error.message || '获取用户列表失败')
}
}
export const createUser = async (req: Request, res: Response) => {
try {
const { username, password, name, email, phone, roles, role, status, person_id } = req.body as Record<
string,
any
>
if (!username || !password || !name) {
return errorResponse(res, '用户名、姓名和密码为必填项', 422)
}
const existing = await prisma.user.findUnique({ where: { username } })
if (existing) {
return errorResponse(res, '用户名已存在', 409)
}
const primaryRole =
(Array.isArray(roles) ? roles[0] : roles) || role || (username === 'admin' ? 'admin' : 'user')
if (!isUserRole(primaryRole)) {
return errorResponse(res, '角色必须是 admin、administrative、it 或 user', 422)
}
const normalizedStatus = normalizeUserStatus(status, 'active')
const personId = normalizePersonId(person_id)
if (typeof personId === 'number') {
await ensurePersonCanBind(personId)
}
const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS)
const created = await prisma.user.create({
data: {
username,
password: hashedPassword,
name,
email,
phone,
role: primaryRole,
status: normalizedStatus,
person_id: personId ?? null
},
include: { person: { select: personSelect } }
})
successResponse(res, sanitizeUser(created), '创建成功')
} catch (error: any) {
sendUserWriteError(res, error, '创建用户失败')
}
}
export const updateUser = async (req: Request, res: Response) => {
try {
const { id } = req.params
const { name, email, phone, roles, role, status, person_id } = req.body as Record<string, any>
const user = await prisma.user.findUnique({ where: { id: Number(id) } })
if (!user) {
return errorResponse(res, '用户不存在', 404)
}
const updateData: any = {
name: name ?? user.name,
email: email ?? user.email,
phone: phone ?? user.phone
}
const newRole = (Array.isArray(roles) ? roles[0] : roles) || role
if (newRole) {
if (!isUserRole(newRole)) {
return errorResponse(res, '角色必须是 admin、administrative、it 或 user', 422)
}
updateData.role = newRole
}
if (status !== undefined) {
updateData.status = normalizeUserStatus(status)
}
const currentUserId = getCurrentUserId(res)
if (currentUserId === user.id) {
if (newRole && newRole !== 'admin') {
return errorResponse(res, '不能降低当前登录管理员自己的角色', 422)
}
if (updateData.status && updateData.status !== 'active') {
return errorResponse(res, '不能停用当前登录管理员自己的账户', 422)
}
}
const personId = normalizePersonId(person_id)
if (typeof personId === 'number') {
await ensurePersonCanBind(personId, user.id)
updateData.person_id = personId
} else if (personId === null) {
updateData.person_id = null
}
const changesRoleOrStatus = Boolean(updateData.role || updateData.status)
const updated = changesRoleOrStatus
? await prisma.$transaction(async (tx) => {
const activeAdministrators = await lockActiveAdministrators(tx)
const current = await tx.user.findUnique({ where: { id: user.id } })
if (!current) {
throw new UserValidationError('用户不存在', 404)
}
const finalRole = updateData.role || current.role
const finalStatus = updateData.status || current.status
const removesActiveAdministrator = activeAdministrators.some(
(item) => Number(item.id) === current.id
) && (finalRole !== 'admin' || finalStatus !== 'active')
if (removesActiveAdministrator && activeAdministrators.length <= 1) {
throw new UserValidationError('系统必须至少保留一个启用的管理员账户', 409)
}
return tx.user.update({
where: { id: current.id },
data: updateData,
include: { person: { select: personSelect } }
})
})
: await prisma.user.update({
where: { id: user.id },
data: updateData,
include: { person: { select: personSelect } }
})
successResponse(res, sanitizeUser(updated), '更新成功')
} catch (error: any) {
sendUserWriteError(res, error, '更新用户失败')
}
}
export const resetPassword = async (req: Request, res: Response) => {
try {
const { id } = req.params
const { new_password } = req.body as { new_password?: string }
if (!new_password) {
return errorResponse(res, '新密码不能为空', 422)
}
const user = await prisma.user.findUnique({ where: { id: Number(id) } })
if (!user) {
return errorResponse(res, '用户不存在', 404)
}
const hashedPassword = await bcrypt.hash(new_password, SALT_ROUNDS)
await prisma.user.update({
where: { id: Number(id) },
data: { password: hashedPassword }
})
successResponse(res, null, '密码重置成功')
} catch (error: any) {
errorResponse(res, error.message || '重置密码失败')
}
}
export const updateUserStatus = async (req: Request, res: Response) => {
try {
const { id } = req.params
const { status } = req.body as { status?: string }
const normalizedStatus = normalizeUserStatus(status)
const user = await prisma.user.findUnique({ where: { id: Number(id) } })
if (!user) {
return errorResponse(res, '用户不存在', 404)
}
if (getCurrentUserId(res) === user.id && normalizedStatus !== 'active') {
return errorResponse(res, '不能停用当前登录管理员自己的账户', 422)
}
const updated = await prisma.$transaction(async (tx) => {
if (normalizedStatus !== 'active') {
await assertActiveAdministratorRemains(tx, user.id)
} else {
await lockActiveAdministrators(tx)
}
return tx.user.update({
where: { id: user.id },
data: { status: normalizedStatus },
include: { person: { select: personSelect } }
})
})
successResponse(res, sanitizeUser(updated), '状态更新成功')
} catch (error: any) {
errorResponse(res, error.message || '更新用户状态失败', error.status || 500)
}
}
export const deleteUser = async (req: Request, res: Response) => {
try {
const { id } = req.params
const userId = Number(id)
if (getCurrentUserId(res) === userId) {
return errorResponse(res, '不能删除当前登录账户', 422)
}
const user = await prisma.user.findUnique({
where: { id: userId },
include: {
_count: {
select: {
site_configs: true,
mount_histories: true,
disposal_applied: true,
disposal_approved: true,
disposal_completed: true,
disposal_actions: true,
maintenance_records: true,
asset_transfers_operated: true,
dingtalk_deliveries: true
}
}
}
})
if (!user) {
return errorResponse(res, '用户不存在', 404)
}
if (
user._count.site_configs > 0 ||
user._count.mount_histories > 0 ||
user._count.disposal_applied > 0 ||
user._count.disposal_approved > 0 ||
user._count.disposal_completed > 0 ||
user._count.disposal_actions > 0 ||
user._count.maintenance_records > 0
|| user._count.asset_transfers_operated > 0
|| user._count.dingtalk_deliveries > 0
) {
return errorResponse(res, '该用户已存在业务历史记录,无法删除')
}
await prisma.$transaction(async (tx) => {
await assertActiveAdministratorRemains(tx, userId)
await tx.operationLog.deleteMany({
where: { user_id: userId }
})
await tx.user.delete({ where: { id: userId } })
})
successResponse(res, null, '删除成功')
} catch (error: any) {
errorResponse(res, error.message || '删除用户失败', error.status || 500)
}
}