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>( "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 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) } }