feat(admin): Add user management and upgrade to module permission system
Features - User Management (Phase 4.1): - Database: Add user_modules table for fine-grained module permissions - Database: Add 4 user permissions (view/create/edit/delete) to role_permissions - Backend: UserService (780 lines) - CRUD with tenant isolation - Backend: UserController + UserRoutes (648 lines) - 13 API endpoints - Backend: Batch import users from Excel - Frontend: UserListPage (412 lines) - list/filter/search/pagination - Frontend: UserFormPage (341 lines) - create/edit with module config - Frontend: UserDetailPage (393 lines) - details/tenant/module management - Frontend: 3 modal components (592 lines) - import/assign/configure - API: GET/POST/PUT/DELETE /api/admin/users/* endpoints Architecture Upgrade - Module Permission System: - Backend: Add getUserModules() method in auth.service - Backend: Login API returns modules array in user object - Frontend: AuthContext adds hasModule() method - Frontend: Navigation filters modules based on user.modules - Frontend: RouteGuard checks requiredModule instead of requiredVersion - Frontend: Remove deprecated version-based permission system - UX: Only show accessible modules in navigation (clean UI) - UX: Smart redirect after login (avoid 403 for regular users) Fixes: - Fix UTF-8 encoding corruption in ~100 docs files - Fix pageSize type conversion in userService (String to Number) - Fix authUser undefined error in TopNavigation - Fix login redirect logic with role-based access check - Update Git commit guidelines v1.2 with UTF-8 safety rules Database Changes: - CREATE TABLE user_modules (user_id, tenant_id, module_code, is_enabled) - ADD UNIQUE CONSTRAINT (user_id, tenant_id, module_code) - INSERT 4 permissions + role assignments - UPDATE PUBLIC tenant with 8 module subscriptions Technical: - Backend: 5 new files (~2400 lines) - Frontend: 10 new files (~2500 lines) - Docs: 1 development record + 2 status updates + 1 guideline update - Total: ~4900 lines of code Status: User management 100% complete, module permission system operational
This commit is contained in:
525
backend/src/modules/admin/controllers/userController.ts
Normal file
525
backend/src/modules/admin/controllers/userController.ts
Normal file
@@ -0,0 +1,525 @@
|
||||
/**
|
||||
* 用户管理控制器
|
||||
* @description 处理用户管理相关的 HTTP 请求
|
||||
*/
|
||||
|
||||
import { FastifyRequest, FastifyReply } from 'fastify';
|
||||
import { logger } from '../../../common/logging/index.js';
|
||||
import * as userService from '../services/userService.js';
|
||||
import {
|
||||
ListUsersQuery,
|
||||
CreateUserRequest,
|
||||
UpdateUserRequest,
|
||||
AssignTenantRequest,
|
||||
UpdateUserModulesRequest,
|
||||
ImportUserRow,
|
||||
} from '../types/user.types.js';
|
||||
|
||||
/**
|
||||
* 获取用户列表
|
||||
*/
|
||||
export async function listUsers(
|
||||
request: FastifyRequest<{ Querystring: ListUsersQuery }>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const user = request.user!;
|
||||
const scope = await userService.getUserQueryScope(user.role, user.tenantId, user.userId);
|
||||
const result = await userService.listUsers(request.query, scope);
|
||||
|
||||
return reply.send({
|
||||
code: 0,
|
||||
message: 'success',
|
||||
data: result,
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error('[UserController] listUsers error:', error);
|
||||
return reply.status(500).send({
|
||||
code: 500,
|
||||
message: error.message || '获取用户列表失败',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户详情
|
||||
*/
|
||||
export async function getUserById(
|
||||
request: FastifyRequest<{ Params: { id: string } }>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const user = request.user!;
|
||||
const scope = await userService.getUserQueryScope(user.role, user.tenantId, user.userId);
|
||||
const result = await userService.getUserById(request.params.id, scope);
|
||||
|
||||
if (!result) {
|
||||
return reply.status(404).send({
|
||||
code: 404,
|
||||
message: '用户不存在',
|
||||
});
|
||||
}
|
||||
|
||||
return reply.send({
|
||||
code: 0,
|
||||
message: 'success',
|
||||
data: result,
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error('[UserController] getUserById error:', error);
|
||||
return reply.status(500).send({
|
||||
code: 500,
|
||||
message: error.message || '获取用户详情失败',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建用户
|
||||
*/
|
||||
export async function createUser(
|
||||
request: FastifyRequest<{ Body: CreateUserRequest }>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const creator = request.user!;
|
||||
|
||||
// HOSPITAL_ADMIN 和 PHARMA_ADMIN 只能在自己的租户内创建用户
|
||||
if (
|
||||
(creator.role === 'HOSPITAL_ADMIN' || creator.role === 'PHARMA_ADMIN') &&
|
||||
request.body.tenantId !== creator.tenantId
|
||||
) {
|
||||
return reply.status(403).send({
|
||||
code: 403,
|
||||
message: '无权限在其他租户创建用户',
|
||||
});
|
||||
}
|
||||
|
||||
const result = await userService.createUser(request.body, creator.userId);
|
||||
|
||||
return reply.status(201).send({
|
||||
code: 0,
|
||||
message: '用户创建成功',
|
||||
data: result,
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error('[UserController] createUser error:', error);
|
||||
|
||||
if (error.message.includes('已存在')) {
|
||||
return reply.status(400).send({
|
||||
code: 400,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
return reply.status(500).send({
|
||||
code: 500,
|
||||
message: error.message || '创建用户失败',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户
|
||||
*/
|
||||
export async function updateUser(
|
||||
request: FastifyRequest<{ Params: { id: string }; Body: UpdateUserRequest }>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const updater = request.user!;
|
||||
const scope = await userService.getUserQueryScope(updater.role, updater.tenantId, updater.userId);
|
||||
|
||||
const result = await userService.updateUser(
|
||||
request.params.id,
|
||||
request.body,
|
||||
scope,
|
||||
updater.userId
|
||||
);
|
||||
|
||||
return reply.send({
|
||||
code: 0,
|
||||
message: '用户更新成功',
|
||||
data: result,
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error('[UserController] updateUser error:', error);
|
||||
|
||||
if (error.message.includes('不存在') || error.message.includes('无权限')) {
|
||||
return reply.status(404).send({
|
||||
code: 404,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
if (error.message.includes('已存在')) {
|
||||
return reply.status(400).send({
|
||||
code: 400,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
return reply.status(500).send({
|
||||
code: 500,
|
||||
message: error.message || '更新用户失败',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户状态(启用/禁用)
|
||||
*/
|
||||
export async function updateUserStatus(
|
||||
request: FastifyRequest<{
|
||||
Params: { id: string };
|
||||
Body: { status: 'active' | 'disabled' };
|
||||
}>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const updater = request.user!;
|
||||
const scope = await userService.getUserQueryScope(updater.role, updater.tenantId, updater.userId);
|
||||
|
||||
await userService.updateUserStatus(
|
||||
request.params.id,
|
||||
request.body.status,
|
||||
scope,
|
||||
updater.userId
|
||||
);
|
||||
|
||||
return reply.send({
|
||||
code: 0,
|
||||
message: request.body.status === 'active' ? '用户已启用' : '用户已禁用',
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error('[UserController] updateUserStatus error:', error);
|
||||
|
||||
if (error.message.includes('不存在') || error.message.includes('无权限')) {
|
||||
return reply.status(404).send({
|
||||
code: 404,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
return reply.status(500).send({
|
||||
code: 500,
|
||||
message: error.message || '更新状态失败',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置用户密码
|
||||
*/
|
||||
export async function resetUserPassword(
|
||||
request: FastifyRequest<{ Params: { id: string } }>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const resetter = request.user!;
|
||||
const scope = await userService.getUserQueryScope(resetter.role, resetter.tenantId, resetter.userId);
|
||||
|
||||
await userService.resetUserPassword(request.params.id, scope, resetter.userId);
|
||||
|
||||
return reply.send({
|
||||
code: 0,
|
||||
message: '密码已重置为默认密码',
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error('[UserController] resetUserPassword error:', error);
|
||||
|
||||
if (error.message.includes('不存在') || error.message.includes('无权限')) {
|
||||
return reply.status(404).send({
|
||||
code: 404,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
return reply.status(500).send({
|
||||
code: 500,
|
||||
message: error.message || '重置密码失败',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分配租户给用户
|
||||
*/
|
||||
export async function assignTenantToUser(
|
||||
request: FastifyRequest<{
|
||||
Params: { id: string };
|
||||
Body: AssignTenantRequest;
|
||||
}>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const assigner = request.user!;
|
||||
|
||||
// 只有 SUPER_ADMIN 可以分配租户
|
||||
if (assigner.role !== 'SUPER_ADMIN') {
|
||||
return reply.status(403).send({
|
||||
code: 403,
|
||||
message: '无权限分配租户',
|
||||
});
|
||||
}
|
||||
|
||||
await userService.assignTenantToUser(request.params.id, request.body, assigner.userId);
|
||||
|
||||
return reply.send({
|
||||
code: 0,
|
||||
message: '租户分配成功',
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error('[UserController] assignTenantToUser error:', error);
|
||||
|
||||
if (error.message.includes('不存在')) {
|
||||
return reply.status(404).send({
|
||||
code: 404,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
if (error.message.includes('已是')) {
|
||||
return reply.status(400).send({
|
||||
code: 400,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
return reply.status(500).send({
|
||||
code: 500,
|
||||
message: error.message || '分配租户失败',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从租户移除用户
|
||||
*/
|
||||
export async function removeTenantFromUser(
|
||||
request: FastifyRequest<{
|
||||
Params: { id: string; tenantId: string };
|
||||
}>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const remover = request.user!;
|
||||
|
||||
// 只有 SUPER_ADMIN 可以移除租户
|
||||
if (remover.role !== 'SUPER_ADMIN') {
|
||||
return reply.status(403).send({
|
||||
code: 403,
|
||||
message: '无权限移除租户',
|
||||
});
|
||||
}
|
||||
|
||||
await userService.removeTenantFromUser(
|
||||
request.params.id,
|
||||
request.params.tenantId,
|
||||
remover.userId
|
||||
);
|
||||
|
||||
return reply.send({
|
||||
code: 0,
|
||||
message: '租户移除成功',
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error('[UserController] removeTenantFromUser error:', error);
|
||||
|
||||
if (error.message.includes('不存在') || error.message.includes('不是')) {
|
||||
return reply.status(404).send({
|
||||
code: 404,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
if (error.message.includes('不能移除')) {
|
||||
return reply.status(400).send({
|
||||
code: 400,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
return reply.status(500).send({
|
||||
code: 500,
|
||||
message: error.message || '移除租户失败',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户在指定租户的模块权限
|
||||
*/
|
||||
export async function updateUserModules(
|
||||
request: FastifyRequest<{
|
||||
Params: { id: string };
|
||||
Body: UpdateUserModulesRequest;
|
||||
}>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const updater = request.user!;
|
||||
|
||||
// SUPER_ADMIN 可以操作任意租户
|
||||
// HOSPITAL_ADMIN/PHARMA_ADMIN 只能操作自己租户的用户
|
||||
if (
|
||||
updater.role !== 'SUPER_ADMIN' &&
|
||||
request.body.tenantId !== updater.tenantId
|
||||
) {
|
||||
return reply.status(403).send({
|
||||
code: 403,
|
||||
message: '无权限修改其他租户的用户模块权限',
|
||||
});
|
||||
}
|
||||
|
||||
await userService.updateUserModules(request.params.id, request.body, updater.userId);
|
||||
|
||||
return reply.send({
|
||||
code: 0,
|
||||
message: '模块权限更新成功',
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error('[UserController] updateUserModules error:', error);
|
||||
|
||||
if (error.message.includes('不是')) {
|
||||
return reply.status(404).send({
|
||||
code: 404,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
if (error.message.includes('不在')) {
|
||||
return reply.status(400).send({
|
||||
code: 400,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
return reply.status(500).send({
|
||||
code: 500,
|
||||
message: error.message || '更新模块权限失败',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量导入用户
|
||||
*/
|
||||
export async function importUsers(
|
||||
request: FastifyRequest<{
|
||||
Body: { users: ImportUserRow[]; defaultTenantId?: string };
|
||||
}>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const importer = request.user!;
|
||||
|
||||
// 确定默认租户
|
||||
let defaultTenantId = request.body.defaultTenantId;
|
||||
if (!defaultTenantId) {
|
||||
if (importer.role === 'SUPER_ADMIN') {
|
||||
return reply.status(400).send({
|
||||
code: 400,
|
||||
message: '请指定默认租户',
|
||||
});
|
||||
}
|
||||
defaultTenantId = importer.tenantId;
|
||||
}
|
||||
|
||||
// HOSPITAL_ADMIN/PHARMA_ADMIN 只能导入到自己的租户
|
||||
if (
|
||||
(importer.role === 'HOSPITAL_ADMIN' || importer.role === 'PHARMA_ADMIN') &&
|
||||
defaultTenantId !== importer.tenantId
|
||||
) {
|
||||
return reply.status(403).send({
|
||||
code: 403,
|
||||
message: '无权限导入到其他租户',
|
||||
});
|
||||
}
|
||||
|
||||
const result = await userService.importUsers(
|
||||
request.body.users,
|
||||
defaultTenantId,
|
||||
importer.userId
|
||||
);
|
||||
|
||||
return reply.send({
|
||||
code: 0,
|
||||
message: `导入完成:成功 ${result.success} 条,失败 ${result.failed} 条`,
|
||||
data: result,
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error('[UserController] importUsers error:', error);
|
||||
return reply.status(500).send({
|
||||
code: 500,
|
||||
message: error.message || '批量导入失败',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有租户列表(用于下拉选择)
|
||||
*/
|
||||
export async function getAllTenants(request: FastifyRequest, reply: FastifyReply) {
|
||||
try {
|
||||
const result = await userService.getAllTenants();
|
||||
return reply.send({
|
||||
code: 0,
|
||||
message: 'success',
|
||||
data: result,
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error('[UserController] getAllTenants error:', error);
|
||||
return reply.status(500).send({
|
||||
code: 500,
|
||||
message: error.message || '获取租户列表失败',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取租户的科室列表(用于下拉选择)
|
||||
*/
|
||||
export async function getDepartmentsByTenant(
|
||||
request: FastifyRequest<{ Params: { tenantId: string } }>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const result = await userService.getDepartmentsByTenant(request.params.tenantId);
|
||||
return reply.send({
|
||||
code: 0,
|
||||
message: 'success',
|
||||
data: result,
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error('[UserController] getDepartmentsByTenant error:', error);
|
||||
return reply.status(500).send({
|
||||
code: 500,
|
||||
message: error.message || '获取科室列表失败',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取租户的模块列表(用于模块配置)
|
||||
*/
|
||||
export async function getModulesByTenant(
|
||||
request: FastifyRequest<{ Params: { tenantId: string } }>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const result = await userService.getModulesByTenant(request.params.tenantId);
|
||||
return reply.send({
|
||||
code: 0,
|
||||
message: 'success',
|
||||
data: result,
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error('[UserController] getModulesByTenant error:', error);
|
||||
return reply.status(500).send({
|
||||
code: 500,
|
||||
message: error.message || '获取模块列表失败',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user