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:
2026-01-16 13:42:10 +08:00
parent 98d862dbd4
commit 66255368b7
560 changed files with 70424 additions and 52353 deletions

View File

@@ -55,6 +55,7 @@ export interface UserInfoResponse {
departmentName?: string | null;
isDefaultPassword: boolean;
permissions: string[];
modules: string[]; // 用户可访问的模块代码列表
}
/**
@@ -77,7 +78,7 @@ export class AuthService {
const { phone, password } = request;
// 1. 查找用户
const user = await prisma.User.findUnique({
const user = await prisma.user.findUnique({
where: { phone },
include: {
tenants: true,
@@ -103,8 +104,9 @@ export class AuthService {
throw new Error('账号已被禁用,请联系管理员');
}
// 4. 获取用户权限
// 4. 获取用户权限和模块列表
const permissions = await this.getUserPermissions(user.role);
const modules = await this.getUserModules(user.id);
// 5. 生成 JWT
const jwtPayload: JWTPayload = {
@@ -119,9 +121,9 @@ export class AuthService {
const tokens = jwtService.generateTokens(jwtPayload);
// 6. 更新最后登录时间
await prisma.User.update({
await prisma.user.update({
where: { id: user.id },
data: { updatedAt: new Date() },
data: { lastLoginAt: new Date() },
});
logger.info('用户登录成功(密码方式)', {
@@ -129,6 +131,7 @@ export class AuthService {
phone: user.phone,
role: user.role,
tenantId: user.tenant_id,
modules: modules.length,
});
return {
@@ -145,6 +148,7 @@ export class AuthService {
departmentName: user.departments?.name,
isDefaultPassword: user.is_default_password,
permissions,
modules, // 新增:返回模块列表
},
tokens,
};
@@ -180,7 +184,7 @@ export class AuthService {
});
// 3. 查找用户
const user = await prisma.User.findUnique({
const user = await prisma.user.findUnique({
where: { phone },
include: {
tenants: true,
@@ -199,8 +203,9 @@ export class AuthService {
throw new Error('账号已被禁用,请联系管理员');
}
// 5. 获取用户权限
// 5. 获取用户权限和模块列表
const permissions = await this.getUserPermissions(user.role);
const modules = await this.getUserModules(user.id);
// 6. 生成 JWT
const jwtPayload: JWTPayload = {
@@ -218,6 +223,7 @@ export class AuthService {
userId: user.id,
phone: user.phone,
role: user.role,
modules: modules.length,
});
return {
@@ -234,6 +240,7 @@ export class AuthService {
departmentName: user.departments?.name,
isDefaultPassword: user.is_default_password,
permissions,
modules, // 新增:返回模块列表
},
tokens,
};
@@ -243,7 +250,7 @@ export class AuthService {
* 获取当前用户信息
*/
async getCurrentUser(userId: string): Promise<UserInfoResponse> {
const user = await prisma.User.findUnique({
const user = await prisma.user.findUnique({
where: { id: userId },
include: {
tenants: true,
@@ -289,7 +296,7 @@ export class AuthService {
}
// 2. 获取用户
const user = await prisma.User.findUnique({
const user = await prisma.user.findUnique({
where: { id: userId },
});
@@ -309,7 +316,7 @@ export class AuthService {
const hashedPassword = await bcrypt.hash(newPassword, 10);
// 5. 更新密码
await prisma.User.update({
await prisma.user.update({
where: { id: userId },
data: {
password: hashedPassword,
@@ -326,7 +333,7 @@ export class AuthService {
*/
async sendVerificationCode(phone: string, type: 'LOGIN' | 'RESET_PASSWORD'): Promise<{ expiresIn: number }> {
// 1. 检查用户是否存在
const user = await prisma.User.findUnique({
const user = await prisma.user.findUnique({
where: { phone },
});
@@ -375,7 +382,7 @@ export class AuthService {
*/
async refreshToken(refreshToken: string): Promise<TokenResponse> {
return jwtService.refreshToken(refreshToken, async (userId) => {
const user = await prisma.User.findUnique({
const user = await prisma.user.findUnique({
where: { id: userId },
include: { tenants: true },
});
@@ -407,11 +414,59 @@ export class AuthService {
return rolePermissions.map(rp => rp.permissions.code);
}
/**
* 获取用户可访问的模块列表
*
* 逻辑:
* 1. 查询用户所有租户关系
* 2. 对每个租户,检查租户订阅的模块
* 3. 如果用户有自定义模块权限,使用自定义权限
* 4. 否则继承租户的全部模块权限
* 5. 去重后返回所有可访问模块
*/
private async getUserModules(userId: string): Promise<string[]> {
// 获取用户的所有租户关系
const tenantMembers = await prisma.tenant_members.findMany({
where: { user_id: userId },
});
const allAccessibleModules = new Set<string>();
for (const tm of tenantMembers) {
// 获取租户订阅的模块
const tenantModules = await prisma.tenant_modules.findMany({
where: {
tenant_id: tm.tenant_id,
is_enabled: true,
},
});
// 获取用户在该租户的自定义模块权限
const userModules = await prisma.user_modules.findMany({
where: {
user_id: userId,
tenant_id: tm.tenant_id,
is_enabled: true,
},
});
if (userModules.length > 0) {
// 有自定义权限,使用自定义权限
userModules.forEach(um => allAccessibleModules.add(um.module_code));
} else {
// 无自定义权限,继承租户所有模块
tenantModules.forEach(tm => allAccessibleModules.add(tm.module_code));
}
}
return Array.from(allAccessibleModules).sort();
}
/**
* 根据用户ID获取JWT Payload用于刷新Token
*/
async getUserPayloadById(userId: string): Promise<JWTPayload | null> {
const user = await prisma.User.findUnique({
const user = await prisma.user.findUnique({
where: { id: userId },
include: { tenants: true },
});

View File

@@ -190,3 +190,5 @@ export const jwtService = new JWTService();

View File

@@ -319,6 +319,8 @@ export function getBatchItems<T>(

View File

@@ -104,3 +104,5 @@ export function getAllFallbackCodes(): string[] {

View File

@@ -73,3 +73,5 @@ export interface VariableValidation {

View File

@@ -194,3 +194,5 @@ export function createOpenAIStreamAdapter(
return new OpenAIStreamAdapter(reply, model);
}

View File

@@ -200,3 +200,5 @@ export async function streamChat(
return service.streamGenerate(messages, callbacks);
}

View File

@@ -18,3 +18,5 @@ export type {
export { THINKING_TAGS } from './types';

View File

@@ -93,3 +93,5 @@ export type SSEEventType =
| 'error'
| 'done';