GuDingZiChan/backend/src/controllers/userController.ts

368 lines
10 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 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<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) {
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 || '删除用户失败')
}
}