import { Request, Response } from 'express' import bcrypt from 'bcryptjs' import prisma from '../utils/prisma' import { successResponse, errorResponse } from '../utils/response' import { getPermissionsForRole, isUserRole } from '../utils/permissions' const SALT_ROUNDS = 10 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 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 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: status || 'active', 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) { updateData.status = status } 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 updated = await prisma.user.update({ where: { id: Number(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 } if (!status) { return errorResponse(res, '状态不能为空', 422) } const user = await prisma.user.findUnique({ where: { id: Number(id) } }) if (!user) { return errorResponse(res, '用户不存在', 404) } const updated = await prisma.user.update({ where: { id: Number(id) }, data: { status }, include: { person: { select: personSelect } } }) successResponse(res, sanitizeUser(updated), '状态更新成功') } catch (error: any) { errorResponse(res, error.message || '更新用户状态失败') } } export const deleteUser = async (req: Request, res: Response) => { try { const { id } = req.params const userId = Number(id) 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) } // 不允许删除 admin 用户 if (user.username === 'admin') { return errorResponse(res, '不能删除管理员账户') } 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 tx.operationLog.deleteMany({ where: { user_id: userId } }) await tx.user.delete({ where: { id: userId } }) }) successResponse(res, null, '删除成功') } catch (error: any) { errorResponse(res, error.message || '删除用户失败') } }