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';

View File

@@ -98,9 +98,11 @@ logger.info('✅ Prompt管理路由已注册: /api/admin/prompts');
// 【运营管理】租户管理模块
// ============================================
import { tenantRoutes, moduleRoutes } from './modules/admin/routes/tenantRoutes.js';
import { userRoutes } from './modules/admin/routes/userRoutes.js';
await fastify.register(tenantRoutes, { prefix: '/api/admin/tenants' });
await fastify.register(moduleRoutes, { prefix: '/api/admin/modules' });
logger.info('✅ 租户管理路由已注册: /api/admin/tenants, /api/admin/modules');
await fastify.register(userRoutes, { prefix: '/api/admin/users' });
logger.info('✅ 运营管理路由已注册: /api/admin/tenants, /api/admin/modules, /api/admin/users');
// ============================================
// 【临时】平台基础设施测试API

View 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 || '获取模块列表失败',
});
}
}

View File

@@ -79,3 +79,5 @@ export async function moduleRoutes(fastify: FastifyInstance) {

View File

@@ -0,0 +1,117 @@
/**
* 用户管理路由
* @description 用户管理相关的 API 路由定义
* @prefix /api/admin/users
*/
import type { FastifyInstance } from 'fastify';
import { authenticate, requireRoles, requirePermission } from '../../../common/auth/auth.middleware.js';
import * as userController from '../controllers/userController.js';
/**
* 注册用户管理路由
*/
export async function userRoutes(fastify: FastifyInstance) {
// ==================== 用户 CRUD ====================
// 获取用户列表
// GET /api/admin/users?page=1&pageSize=20&search=&role=&tenantId=&status=&departmentId=
fastify.get('/', {
preHandler: [authenticate, requirePermission('user:view')],
handler: userController.listUsers,
});
// 获取用户详情
// GET /api/admin/users/:id
fastify.get('/:id', {
preHandler: [authenticate, requirePermission('user:view')],
handler: userController.getUserById,
});
// 创建用户
// POST /api/admin/users
fastify.post('/', {
preHandler: [authenticate, requirePermission('user:create')],
handler: userController.createUser,
});
// 更新用户
// PUT /api/admin/users/:id
fastify.put('/:id', {
preHandler: [authenticate, requirePermission('user:edit')],
handler: userController.updateUser,
});
// 更新用户状态(启用/禁用)
// PUT /api/admin/users/:id/status
fastify.put('/:id/status', {
preHandler: [authenticate, requirePermission('user:edit')],
handler: userController.updateUserStatus,
});
// 重置用户密码
// POST /api/admin/users/:id/reset-password
fastify.post('/:id/reset-password', {
preHandler: [authenticate, requirePermission('user:edit')],
handler: userController.resetUserPassword,
});
// ==================== 租户管理 ====================
// 分配租户给用户(仅超级管理员)
// POST /api/admin/users/:id/tenants
fastify.post('/:id/tenants', {
preHandler: [authenticate, requireRoles('SUPER_ADMIN')],
handler: userController.assignTenantToUser,
});
// 从租户移除用户(仅超级管理员)
// DELETE /api/admin/users/:id/tenants/:tenantId
fastify.delete('/:id/tenants/:tenantId', {
preHandler: [authenticate, requireRoles('SUPER_ADMIN')],
handler: userController.removeTenantFromUser,
});
// ==================== 模块权限管理 ====================
// 更新用户在指定租户的模块权限
// PUT /api/admin/users/:id/modules
fastify.put('/:id/modules', {
preHandler: [authenticate, requirePermission('user:edit')],
handler: userController.updateUserModules,
});
// ==================== 批量导入 ====================
// 批量导入用户
// POST /api/admin/users/import
fastify.post('/import', {
preHandler: [authenticate, requirePermission('user:create')],
handler: userController.importUsers,
});
// ==================== 辅助接口 ====================
// 获取所有租户列表(用于下拉选择)
// GET /api/admin/users/options/tenants
fastify.get('/options/tenants', {
preHandler: [authenticate, requirePermission('user:view')],
handler: userController.getAllTenants,
});
// 获取租户的科室列表
// GET /api/admin/users/options/tenants/:tenantId/departments
fastify.get('/options/tenants/:tenantId/departments', {
preHandler: [authenticate, requirePermission('user:view')],
handler: userController.getDepartmentsByTenant,
});
// 获取租户的模块列表(用于模块配置)
// GET /api/admin/users/options/tenants/:tenantId/modules
fastify.get('/options/tenants/:tenantId/modules', {
preHandler: [authenticate, requirePermission('user:view')],
handler: userController.getModulesByTenant,
});
}
export default userRoutes;

View File

@@ -0,0 +1,900 @@
/**
* 用户管理服务
* @description 提供用户 CRUD、租户隔离、模块权限管理等功能
*/
import { PrismaClient, UserRole, Prisma } from '@prisma/client';
import bcrypt from 'bcryptjs';
import { v4 as uuidv4 } from 'uuid';
import { logger } from '../../../common/logging/index.js';
import {
ListUsersQuery,
CreateUserRequest,
UpdateUserRequest,
AssignTenantRequest,
UpdateUserModulesRequest,
UserListItem,
UserDetail,
TenantMembership,
PaginatedResponse,
UserQueryScope,
ImportUserRow,
ImportResult,
ImportError,
} from '../types/user.types.js';
const prisma = new PrismaClient();
// 默认密码
const DEFAULT_PASSWORD = '123456';
/**
* 根据用户ID获取用户的 departmentId
*/
export async function getUserDepartmentId(userId: string): Promise<string | undefined> {
const user = await prisma.user.findUnique({
where: { id: userId },
select: { department_id: true },
});
return user?.department_id || undefined;
}
/**
* 根据用户角色获取查询范围
*/
export async function getUserQueryScope(
userRole: UserRole | string,
tenantId?: string,
userId?: string
): Promise<UserQueryScope> {
switch (userRole) {
case 'SUPER_ADMIN':
case 'PROMPT_ENGINEER':
return {}; // 无限制
case 'HOSPITAL_ADMIN':
case 'PHARMA_ADMIN':
return { tenantId }; // 只能查看本租户
case 'DEPARTMENT_ADMIN': {
// 科室主任需要查询其 departmentId
const departmentId = userId ? await getUserDepartmentId(userId) : undefined;
return { tenantId, departmentId };
}
default:
throw new Error('无权限访问用户管理');
}
}
/**
* 获取用户列表(支持分页、搜索、筛选)
*/
export async function listUsers(
query: ListUsersQuery,
scope: UserQueryScope
): Promise<PaginatedResponse<UserListItem>> {
// 确保 page 和 pageSize 是数字类型HTTP 查询参数默认是字符串)
const page = Number(query.page) || 1;
const pageSize = Number(query.pageSize) || 20;
const { search, role, tenantId, status, departmentId } = query;
const skip = (page - 1) * pageSize;
// 构建查询条件
const where: Prisma.UserWhereInput = {};
// 数据隔离:根据 scope 限制查询范围
if (scope.tenantId) {
where.tenant_id = scope.tenantId;
}
if (scope.departmentId) {
where.department_id = scope.departmentId;
}
// 搜索条件
if (search) {
where.OR = [
{ phone: { contains: search } },
{ name: { contains: search } },
{ email: { contains: search } },
];
}
// 筛选条件
if (role) {
where.role = role;
}
if (tenantId && !scope.tenantId) {
// 只有无限制 scope 才能按租户筛选
where.tenant_id = tenantId;
}
if (status) {
where.status = status;
}
if (departmentId && !scope.departmentId) {
where.department_id = departmentId;
}
// 查询总数
const total = await prisma.user.count({ where });
// 查询列表
const users = await prisma.user.findMany({
where,
skip,
take: pageSize,
orderBy: { createdAt: 'desc' },
include: {
tenants: {
select: {
id: true,
code: true,
name: true,
type: true,
},
},
departments: {
select: {
id: true,
name: true,
},
},
tenant_members: {
select: {
id: true,
},
},
},
});
// 转换为列表项
const data: UserListItem[] = users.map((user) => ({
id: user.id,
phone: user.phone,
name: user.name,
email: user.email,
role: user.role,
status: user.status,
isDefaultPassword: user.is_default_password,
defaultTenant: {
id: user.tenants.id,
code: user.tenants.code,
name: user.tenants.name,
type: user.tenants.type,
},
department: user.departments
? {
id: user.departments.id,
name: user.departments.name,
}
: null,
tenantCount: user.tenant_members.length,
createdAt: user.createdAt,
lastLoginAt: user.lastLoginAt,
}));
return {
data,
total,
page,
pageSize,
totalPages: Math.ceil(total / pageSize),
};
}
/**
* 获取用户详情
*/
export async function getUserById(userId: string, scope: UserQueryScope): Promise<UserDetail | null> {
const where: Prisma.UserWhereInput = { id: userId };
// 数据隔离
if (scope.tenantId) {
where.tenant_id = scope.tenantId;
}
const user = await prisma.user.findFirst({
where,
include: {
tenants: {
select: {
id: true,
code: true,
name: true,
type: true,
},
},
departments: {
select: {
id: true,
name: true,
},
},
tenant_members: {
include: {
tenants: {
select: {
id: true,
code: true,
name: true,
type: true,
},
},
},
},
user_modules: {
include: {
tenant: {
select: {
id: true,
code: true,
},
},
},
},
},
});
if (!user) {
return null;
}
// 获取每个租户的模块权限
const tenantMemberships: TenantMembership[] = await Promise.all(
user.tenant_members.map(async (tm) => {
// 获取租户订阅的模块
const tenantModules = await prisma.tenant_modules.findMany({
where: { tenant_id: tm.tenants.id, is_enabled: true },
});
// 获取用户在该租户的模块权限
const userModulesInTenant = user.user_modules.filter(
(um) => um.tenant_id === tm.tenants.id
);
// 计算最终模块权限
const allowedModules = tenantModules.map((tm) => {
const userModule = userModulesInTenant.find((um) => um.module_code === tm.module_code);
return {
code: tm.module_code,
name: getModuleName(tm.module_code),
isEnabled: userModule ? userModule.is_enabled : true, // 默认继承租户权限
};
});
return {
tenantId: tm.tenants.id,
tenantCode: tm.tenants.code,
tenantName: tm.tenants.name,
tenantType: tm.tenants.type,
role: tm.role,
joinedAt: tm.joined_at,
allowedModules,
};
})
);
// 获取用户权限(基于角色)
const rolePermissions = await prisma.role_permissions.findMany({
where: { role: user.role },
include: { permissions: true },
});
const permissions = rolePermissions.map((rp) => rp.permissions.code);
return {
id: user.id,
phone: user.phone,
name: user.name,
email: user.email,
role: user.role,
status: user.status,
isDefaultPassword: user.is_default_password,
defaultTenant: {
id: user.tenants.id,
code: user.tenants.code,
name: user.tenants.name,
type: user.tenants.type,
},
department: user.departments
? {
id: user.departments.id,
name: user.departments.name,
}
: null,
tenantCount: user.tenant_members.length,
createdAt: user.createdAt,
lastLoginAt: user.lastLoginAt,
tenantMemberships,
permissions,
};
}
/**
* 创建用户
*/
export async function createUser(data: CreateUserRequest, creatorId: string): Promise<UserDetail> {
// 检查手机号是否已存在
const existingUser = await prisma.user.findUnique({
where: { phone: data.phone },
});
if (existingUser) {
throw new Error('手机号已存在');
}
// 检查邮箱是否已存在
if (data.email) {
const existingEmail = await prisma.user.findUnique({
where: { email: data.email },
});
if (existingEmail) {
throw new Error('邮箱已存在');
}
}
// 检查租户是否存在
const tenant = await prisma.tenants.findUnique({
where: { id: data.tenantId },
});
if (!tenant) {
throw new Error('租户不存在');
}
// 检查科室是否存在(如果提供)
if (data.departmentId) {
const department = await prisma.departments.findFirst({
where: { id: data.departmentId, tenant_id: data.tenantId },
});
if (!department) {
throw new Error('科室不存在或不属于该租户');
}
}
// 加密密码
const hashedPassword = await bcrypt.hash(DEFAULT_PASSWORD, 10);
// 创建用户和租户成员关系
const userId = uuidv4();
const tenantMemberId = uuidv4();
const user = await prisma.$transaction(async (tx) => {
// 创建用户
const newUser = await tx.user.create({
data: {
id: userId,
phone: data.phone,
name: data.name,
email: data.email,
password: hashedPassword,
role: data.role,
tenant_id: data.tenantId,
department_id: data.departmentId,
is_default_password: true,
status: 'active',
},
});
// 创建租户成员关系
await tx.tenant_members.create({
data: {
id: tenantMemberId,
tenant_id: data.tenantId,
user_id: userId,
role: data.tenantRole || data.role,
},
});
// 如果指定了模块权限,创建用户模块记录
if (data.allowedModules && data.allowedModules.length > 0) {
await tx.user_modules.createMany({
data: data.allowedModules.map((moduleCode) => ({
id: uuidv4(),
user_id: userId,
tenant_id: data.tenantId,
module_code: moduleCode,
is_enabled: true,
})),
});
}
return newUser;
});
logger.info('[UserService] User created', {
userId: user.id,
phone: user.phone,
createdBy: creatorId,
});
// 返回用户详情
return (await getUserById(user.id, {}))!;
}
/**
* 更新用户
*/
export async function updateUser(
userId: string,
data: UpdateUserRequest,
scope: UserQueryScope,
updaterId: string
): Promise<UserDetail> {
// 检查用户是否存在且在权限范围内
const existingUser = await prisma.user.findFirst({
where: {
id: userId,
...(scope.tenantId ? { tenant_id: scope.tenantId } : {}),
},
});
if (!existingUser) {
throw new Error('用户不存在或无权限操作');
}
// 检查邮箱唯一性
if (data.email && data.email !== existingUser.email) {
const existingEmail = await prisma.user.findUnique({
where: { email: data.email },
});
if (existingEmail) {
throw new Error('邮箱已存在');
}
}
// 更新用户
await prisma.user.update({
where: { id: userId },
data: {
name: data.name,
email: data.email,
role: data.role,
department_id: data.departmentId,
status: data.status,
},
});
logger.info('[UserService] User updated', {
userId,
updatedFields: Object.keys(data),
updatedBy: updaterId,
});
return (await getUserById(userId, scope))!;
}
/**
* 更新用户状态(启用/禁用)
*/
export async function updateUserStatus(
userId: string,
status: 'active' | 'disabled',
scope: UserQueryScope,
updaterId: string
): Promise<void> {
const existingUser = await prisma.user.findFirst({
where: {
id: userId,
...(scope.tenantId ? { tenant_id: scope.tenantId } : {}),
},
});
if (!existingUser) {
throw new Error('用户不存在或无权限操作');
}
await prisma.user.update({
where: { id: userId },
data: { status },
});
logger.info('[UserService] User status updated', {
userId,
status,
updatedBy: updaterId,
});
}
/**
* 重置用户密码
*/
export async function resetUserPassword(
userId: string,
scope: UserQueryScope,
resetterId: string
): Promise<void> {
const existingUser = await prisma.user.findFirst({
where: {
id: userId,
...(scope.tenantId ? { tenant_id: scope.tenantId } : {}),
},
});
if (!existingUser) {
throw new Error('用户不存在或无权限操作');
}
const hashedPassword = await bcrypt.hash(DEFAULT_PASSWORD, 10);
await prisma.user.update({
where: { id: userId },
data: {
password: hashedPassword,
is_default_password: true,
password_changed_at: null,
},
});
logger.info('[UserService] User password reset', {
userId,
resetBy: resetterId,
});
}
/**
* 分配租户给用户
*/
export async function assignTenantToUser(
userId: string,
data: AssignTenantRequest,
assignerId: string
): Promise<void> {
// 检查用户是否存在
const user = await prisma.user.findUnique({ where: { id: userId } });
if (!user) {
throw new Error('用户不存在');
}
// 检查租户是否存在
const tenant = await prisma.tenants.findUnique({ where: { id: data.tenantId } });
if (!tenant) {
throw new Error('租户不存在');
}
// 检查是否已是该租户成员
const existingMember = await prisma.tenant_members.findUnique({
where: {
tenant_id_user_id: {
tenant_id: data.tenantId,
user_id: userId,
},
},
});
if (existingMember) {
throw new Error('用户已是该租户成员');
}
// 创建租户成员关系
await prisma.$transaction(async (tx) => {
await tx.tenant_members.create({
data: {
id: uuidv4(),
tenant_id: data.tenantId,
user_id: userId,
role: data.role,
},
});
// 如果指定了模块权限,创建用户模块记录
if (data.allowedModules && data.allowedModules.length > 0) {
await tx.user_modules.createMany({
data: data.allowedModules.map((moduleCode) => ({
id: uuidv4(),
user_id: userId,
tenant_id: data.tenantId,
module_code: moduleCode,
is_enabled: true,
})),
});
}
});
logger.info('[UserService] Tenant assigned to user', {
userId,
tenantId: data.tenantId,
assignedBy: assignerId,
});
}
/**
* 从租户移除用户
*/
export async function removeTenantFromUser(
userId: string,
tenantId: string,
removerId: string
): Promise<void> {
// 检查用户是否存在
const user = await prisma.user.findUnique({ where: { id: userId } });
if (!user) {
throw new Error('用户不存在');
}
// 不能移除默认租户
if (user.tenant_id === tenantId) {
throw new Error('不能移除用户的默认租户');
}
// 检查租户成员关系是否存在
const membership = await prisma.tenant_members.findUnique({
where: {
tenant_id_user_id: {
tenant_id: tenantId,
user_id: userId,
},
},
});
if (!membership) {
throw new Error('用户不是该租户的成员');
}
// 删除租户成员关系和模块权限
await prisma.$transaction(async (tx) => {
await tx.tenant_members.delete({
where: { id: membership.id },
});
await tx.user_modules.deleteMany({
where: {
user_id: userId,
tenant_id: tenantId,
},
});
});
logger.info('[UserService] Tenant removed from user', {
userId,
tenantId,
removedBy: removerId,
});
}
/**
* 更新用户在指定租户的模块权限
*/
export async function updateUserModules(
userId: string,
data: UpdateUserModulesRequest,
updaterId: string
): Promise<void> {
// 检查用户是否是该租户成员
const membership = await prisma.tenant_members.findUnique({
where: {
tenant_id_user_id: {
tenant_id: data.tenantId,
user_id: userId,
},
},
});
if (!membership) {
throw new Error('用户不是该租户的成员');
}
// 获取租户订阅的模块
const tenantModules = await prisma.tenant_modules.findMany({
where: { tenant_id: data.tenantId, is_enabled: true },
});
const tenantModuleCodes = tenantModules.map((tm) => tm.module_code);
// 验证请求的模块是否在租户订阅范围内
const invalidModules = data.modules.filter((m) => !tenantModuleCodes.includes(m));
if (invalidModules.length > 0) {
throw new Error(`以下模块不在租户订阅范围内: ${invalidModules.join(', ')}`);
}
// 更新用户模块权限
await prisma.$transaction(async (tx) => {
// 删除旧的模块权限
await tx.user_modules.deleteMany({
where: {
user_id: userId,
tenant_id: data.tenantId,
},
});
// 创建新的模块权限
if (data.modules.length > 0) {
await tx.user_modules.createMany({
data: data.modules.map((moduleCode) => ({
id: uuidv4(),
user_id: userId,
tenant_id: data.tenantId,
module_code: moduleCode,
is_enabled: true,
})),
});
}
});
logger.info('[UserService] User modules updated', {
userId,
tenantId: data.tenantId,
modules: data.modules,
updatedBy: updaterId,
});
}
/**
* 批量导入用户
*/
export async function importUsers(
rows: ImportUserRow[],
defaultTenantId: string,
importerId: string
): Promise<ImportResult> {
const result: ImportResult = {
success: 0,
failed: 0,
errors: [],
};
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
const rowNumber = i + 2; // Excel行号跳过表头
try {
// 验证手机号
if (!row.phone || !/^1[3-9]\d{9}$/.test(row.phone)) {
throw new Error('手机号格式不正确');
}
// 验证姓名
if (!row.name || row.name.trim().length === 0) {
throw new Error('姓名不能为空');
}
// 解析角色
const role = parseRole(row.role);
// 解析租户
let tenantId = defaultTenantId;
if (row.tenantCode) {
const tenant = await prisma.tenants.findUnique({
where: { code: row.tenantCode },
});
if (!tenant) {
throw new Error(`租户代码 ${row.tenantCode} 不存在`);
}
tenantId = tenant.id;
}
// 解析科室
let departmentId: string | undefined;
if (row.departmentName) {
const department = await prisma.departments.findFirst({
where: {
name: row.departmentName,
tenant_id: tenantId,
},
});
if (!department) {
throw new Error(`科室 ${row.departmentName} 不存在`);
}
departmentId = department.id;
}
// 解析模块
const modules = row.modules
? row.modules.split(',').map((m) => m.trim().toUpperCase())
: undefined;
// 创建用户
await createUser(
{
phone: row.phone,
name: row.name.trim(),
email: row.email,
role,
tenantId,
departmentId,
allowedModules: modules,
},
importerId
);
result.success++;
} catch (error: any) {
result.failed++;
result.errors.push({
row: rowNumber,
phone: row.phone || '',
error: error.message,
});
}
}
logger.info('[UserService] Batch import completed', {
success: result.success,
failed: result.failed,
importedBy: importerId,
});
return result;
}
/**
* 获取所有租户列表(用于下拉选择)
*/
export async function getAllTenants() {
return prisma.tenants.findMany({
where: { status: 'ACTIVE' },
select: {
id: true,
code: true,
name: true,
type: true,
},
orderBy: { name: 'asc' },
});
}
/**
* 获取租户的科室列表(用于下拉选择)
*/
export async function getDepartmentsByTenant(tenantId: string) {
return prisma.departments.findMany({
where: { tenant_id: tenantId },
select: {
id: true,
name: true,
parent_id: true,
},
orderBy: { name: 'asc' },
});
}
/**
* 获取租户的模块列表(用于模块配置)
*/
export async function getModulesByTenant(tenantId: string) {
const tenantModules = await prisma.tenant_modules.findMany({
where: { tenant_id: tenantId, is_enabled: true },
});
const allModules = await prisma.modules.findMany({
where: { is_active: true },
orderBy: { sort_order: 'asc' },
});
return allModules.map((m) => ({
code: m.code,
name: m.name,
isSubscribed: tenantModules.some((tm) => tm.module_code === m.code),
}));
}
// ============ 辅助函数 ============
function getModuleName(code: string): string {
const moduleNames: Record<string, string> = {
AIA: 'AI智能问答',
PKB: '个人知识库',
ASL: 'AI智能文献',
DC: '数据清洗整理',
IIT: 'IIT Manager',
RVW: '稿件审查',
SSA: '智能统计分析',
ST: '统计分析工具',
};
return moduleNames[code] || code;
}
function parseRole(roleStr?: string): UserRole {
if (!roleStr) return 'USER';
const roleMap: Record<string, UserRole> = {
: 'SUPER_ADMIN',
SUPER_ADMIN: 'SUPER_ADMIN',
PROMPT工程师: 'PROMPT_ENGINEER',
PROMPT_ENGINEER: 'PROMPT_ENGINEER',
: 'HOSPITAL_ADMIN',
HOSPITAL_ADMIN: 'HOSPITAL_ADMIN',
: 'PHARMA_ADMIN',
PHARMA_ADMIN: 'PHARMA_ADMIN',
: 'DEPARTMENT_ADMIN',
DEPARTMENT_ADMIN: 'DEPARTMENT_ADMIN',
: 'USER',
USER: 'USER',
};
return roleMap[roleStr.trim()] || 'USER';
}

View File

@@ -109,3 +109,5 @@ export interface PaginatedResponse<T> {

View File

@@ -0,0 +1,160 @@
/**
* 用户管理类型定义
* @description 用户管理相关的请求/响应类型
*/
import { UserRole, TenantType } from '@prisma/client';
// ============ 请求类型 ============
/** 用户列表查询参数 */
export interface ListUsersQuery {
page?: number;
pageSize?: number;
search?: string; // 搜索:手机号、姓名、邮箱
role?: UserRole; // 角色筛选
tenantId?: string; // 租户筛选
status?: 'active' | 'disabled'; // 状态筛选
departmentId?: string; // 科室筛选
}
/** 创建用户请求 */
export interface CreateUserRequest {
phone: string;
name: string;
email?: string;
role: UserRole;
tenantId: string; // 默认租户
departmentId?: string; // 科室(仅医院租户)
tenantRole?: UserRole; // 在该租户内的角色
allowedModules?: string[]; // 允许访问的模块(为空则继承租户全部模块)
}
/** 更新用户请求 */
export interface UpdateUserRequest {
name?: string;
email?: string;
role?: UserRole;
departmentId?: string;
status?: 'active' | 'disabled';
}
/** 分配租户请求 */
export interface AssignTenantRequest {
tenantId: string;
role: UserRole; // 在该租户内的角色
allowedModules?: string[]; // 允许访问的模块
}
/** 更新用户模块权限请求 */
export interface UpdateUserModulesRequest {
tenantId: string;
modules: string[]; // 允许访问的模块代码列表
}
/** 批量导入用户请求Excel数据行 */
export interface ImportUserRow {
phone: string;
name: string;
email?: string;
role?: string; // 角色名称,默认 USER
tenantCode?: string; // 租户代码,默认当前租户
departmentName?: string; // 科室名称
modules?: string; // 模块列表,逗号分隔
}
// ============ 响应类型 ============
/** 用户基本信息(列表用) */
export interface UserListItem {
id: string;
phone: string;
name: string;
email: string | null;
role: UserRole;
status: string;
isDefaultPassword: boolean;
defaultTenant: {
id: string;
code: string;
name: string;
type: TenantType;
};
department: {
id: string;
name: string;
} | null;
tenantCount: number; // 所属租户数量
createdAt: Date;
lastLoginAt: Date | null;
}
/** 用户详情 */
export interface UserDetail extends UserListItem {
tenantMemberships: TenantMembership[];
permissions: string[]; // 权限汇总
}
/** 租户成员关系 */
export interface TenantMembership {
tenantId: string;
tenantCode: string;
tenantName: string;
tenantType: TenantType;
role: UserRole;
joinedAt: Date;
allowedModules: ModulePermission[];
}
/** 模块权限 */
export interface ModulePermission {
code: string;
name: string;
isEnabled: boolean;
}
/** 分页响应 */
export interface PaginatedResponse<T> {
data: T[];
total: number;
page: number;
pageSize: number;
totalPages: number;
}
/** 批量导入结果 */
export interface ImportResult {
success: number;
failed: number;
errors: ImportError[];
}
/** 导入错误 */
export interface ImportError {
row: number;
phone: string;
error: string;
}
// ============ 服务层类型 ============
/** 用户查询范围(基于角色的数据隔离) */
export interface UserQueryScope {
tenantId?: string;
departmentId?: string;
}
/** 模块代码常量 */
export const MODULE_CODES = ['AIA', 'PKB', 'ASL', 'DC', 'IIT', 'RVW', 'SSA', 'ST'] as const;
export type ModuleCode = typeof MODULE_CODES[number];
/** 角色显示名称 */
export const ROLE_DISPLAY_NAMES: Record<UserRole, string> = {
SUPER_ADMIN: '超级管理员',
PROMPT_ENGINEER: 'Prompt工程师',
HOSPITAL_ADMIN: '医院管理员',
PHARMA_ADMIN: '药企管理员',
DEPARTMENT_ADMIN: '科室主任',
USER: '普通用户',
};

View File

@@ -231,3 +231,5 @@ async function matchIntent(query: string): Promise<{
};
}

View File

@@ -14,3 +14,5 @@ import aiaRoutes from './routes/index.js';
export { aiaRoutes };

View File

@@ -111,3 +111,5 @@ function estimateTokens(text: string): number {
return Math.ceil(chineseChars / 1.5 + otherChars / 4);
}

View File

@@ -199,3 +199,5 @@ export interface PaginatedResponse<T> {
pagination: Pagination;
}

View File

@@ -355,6 +355,8 @@ runTests().catch((error) => {

View File

@@ -334,6 +334,8 @@ Content-Type: application/json

View File

@@ -270,6 +270,8 @@ export const conflictDetectionService = new ConflictDetectionService();

View File

@@ -220,6 +220,8 @@ curl -X POST http://localhost:3000/api/v1/dc/tool-c/test/execute \

View File

@@ -274,6 +274,8 @@ export const streamAIController = new StreamAIController();

View File

@@ -184,5 +184,7 @@ logger.info('[SessionMemory] 会话记忆管理器已启动', {

View File

@@ -120,3 +120,5 @@ checkTableStructure();

View File

@@ -105,5 +105,7 @@ checkProjectConfig().catch(console.error);

View File

@@ -88,4 +88,6 @@ main();

View File

@@ -546,3 +546,5 @@ URL: https://iit.xunzhengyixue.com/api/v1/iit/patient-wechat/callback

View File

@@ -181,3 +181,5 @@ console.log('');

View File

@@ -498,3 +498,5 @@ export const patientWechatService = new PatientWechatService();

View File

@@ -143,3 +143,5 @@ testDifyIntegration().catch(error => {

View File

@@ -169,6 +169,8 @@ testIitDatabase()

View File

@@ -158,3 +158,5 @@ if (hasError) {

View File

@@ -184,3 +184,5 @@ async function testUrlVerification() {

View File

@@ -264,4 +264,6 @@ main().catch((error) => {

View File

@@ -149,3 +149,5 @@ Write-Host ""

View File

@@ -239,6 +239,8 @@ export interface CachedProtocolRules {

View File

@@ -55,3 +55,5 @@ export default async function healthRoutes(fastify: FastifyInstance) {

View File

@@ -9,7 +9,7 @@
### POST /api/v1/rvw/tasks
### ========================================
# 注æ„<EFBFBD>:需è¦<EFBFBD>使用工具(å¦Postman)上传æ‡ä»?
# 注意需要使用工具如Postman上传文<EFBFBD>?
# curl -X POST http://localhost:3001/api/v1/rvw/tasks \
# -F "file=@test.docx" \
# -F "modelType=deepseek-v3"
@@ -24,11 +24,11 @@
GET {{baseUrl}}/api/v1/rvw/tasks
Content-Type: application/json
### 获å<EFBFBD>待处ç<EFBFBD>†ä»»åŠ?
### 获取待处理任<EFBFBD>?
GET {{baseUrl}}/api/v1/rvw/tasks?status=pending
Content-Type: application/json
### 获å<EFBFBD>已完æˆ<EFBFBD>ä»»åŠ?
### 获取已完成任<EFBFBD>?
GET {{baseUrl}}/api/v1/rvw/tasks?status=completed
Content-Type: application/json
@@ -58,7 +58,7 @@ Content-Type: application/json
"agents": ["methodology"]
}
### å<EFBFBD>Œæ—¶é€‰æ©ä¸¤ä¸ªæ™ºèƒ½ä½“(默认ï¼?
### 同时选择两个智能体默认<EFBFBD>?
POST {{baseUrl}}/api/v1/rvw/tasks/{{taskId}}/run
Content-Type: application/json
@@ -112,15 +112,15 @@ Content-Type: application/json
### 旧版API兼容性测试
### ========================================
### 旧版:获å<EFBFBD>任务列è¡?
### 旧版获取任务列<EFBFBD>?
GET {{baseUrl}}/api/v1/review/tasks
Content-Type: application/json
### 旧版:获å<EFBFBD>任务状æ€?
### 旧版获取任务状<EFBFBD>?
GET {{baseUrl}}/api/v1/review/tasks/{{taskId}}
Content-Type: application/json
### 旧版:获å<EFBFBD>报å?
### 旧版获取报<EFBFBD>?
GET {{baseUrl}}/api/v1/review/tasks/{{taskId}}/report
Content-Type: application/json
@@ -133,3 +133,5 @@ Content-Type: application/json

View File

@@ -9,12 +9,12 @@ Write-Host "RVW模块 Phase 1 API测试" -ForegroundColor Cyan
Write-Host "========================================`n" -ForegroundColor Cyan
# 检查服务器是否运行
Write-Host "1. <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>?.." -ForegroundColor Yellow
Write-Host "1. 检查服务器状<EFBFBD>?.." -ForegroundColor Yellow
try {
$health = Invoke-RestMethod -Uri "$BaseUrl/health" -Method Get
Write-Host " <20>?<EFBFBD>滚𦛚<EFBFBD><EFBFBD>銵䔶葉" -ForegroundColor Green
Write-Host " <20>?服务器运行中" -ForegroundColor Green
} catch {
Write-Host " <20>?<EFBFBD>滚𦛚<EFBFBD>冽𧊋餈鞱<EFBFBD>嚗諹窈<EFBFBD><EFBFBD><EFBFBD><EFBFBD>? cd backend && npm run dev" -ForegroundColor Red
Write-Host " <20>?服务器未运行请先启动后<EFBFBD>? cd backend && npm run dev" -ForegroundColor Red
exit 1
}
@@ -22,28 +22,28 @@ try {
Write-Host "`n2. 测试获取任务列表 (GET /api/v1/rvw/tasks)..." -ForegroundColor Yellow
try {
$response = Invoke-RestMethod -Uri "$BaseUrl/api/v1/rvw/tasks" -Method Get
Write-Host " <20>?<EFBFBD>𣂼<EFBFBD>! 敶枏<E695B6>隞餃𦛚<EFBFBD>? $($response.pagination.total)" -ForegroundColor Green
Write-Host " <20>?成功! 当前任务<EFBFBD>? $($response.pagination.total)" -ForegroundColor Green
if ($response.data.Count -gt 0) {
Write-Host " <EFBFBD><EFBFBD>餈睲遙<EFBFBD>? $($response.data[0].fileName) - $($response.data[0].status)" -ForegroundColor Gray
Write-Host " 最近任<EFBFBD>? $($response.data[0].fileName) - $($response.data[0].status)" -ForegroundColor Gray
}
} catch {
Write-Host " <20>?憭梯揖: $($_.Exception.Message)" -ForegroundColor Red
Write-Host " <20>?失败: $($_.Exception.Message)" -ForegroundColor Red
}
# 瘚贝<EFBFBD>2: <20>厩𠶖<E58EA9><F0A0B696><EFBFBD><EFBFBD>?
# 测试2: 按状态筛<EFBFBD>?
Write-Host "`n3. 测试筛选待处理任务 (GET /api/v1/rvw/tasks?status=pending)..." -ForegroundColor Yellow
try {
$response = Invoke-RestMethod -Uri "$BaseUrl/api/v1/rvw/tasks?status=pending" -Method Get
Write-Host " <20>?<EFBFBD>𣂼<EFBFBD>! 敺<><E695BA><EFBFBD><EFBFBD><EFBFBD>⊥㺭: $($response.pagination.total)" -ForegroundColor Green
Write-Host " <20>?成功! 待处理任务数: $($response.pagination.total)" -ForegroundColor Green
} catch {
Write-Host " <20>?憭梯揖: $($_.Exception.Message)" -ForegroundColor Red
Write-Host " <20>?失败: $($_.Exception.Message)" -ForegroundColor Red
}
# 测试3: 上传文件创建任务
Write-Host "`n4. 测试上传文件 (POST /api/v1/rvw/tasks)..." -ForegroundColor Yellow
if (Test-Path $TestFile) {
try {
# 雿輻鍂curl銝𠹺<EFBFBD>嚗㇊owerShell<EFBFBD><EFBFBD>nvoke-RestMethod撖雋ultipart<EFBFBD><EFBFBD>銝滚末嚗?
# 使用curl上传PowerShell的Invoke-RestMethod对multipart支持不好<EFBFBD>?
$curlResult = & curl.exe -s -X POST "$BaseUrl/api/v1/rvw/tasks" `
-F "file=@$TestFile" `
-F "modelType=deepseek-v3"
@@ -51,53 +51,53 @@ if (Test-Path $TestFile) {
$uploadResponse = $curlResult | ConvertFrom-Json
if ($uploadResponse.success) {
$taskId = $uploadResponse.data.taskId
Write-Host " <20>?銝𠹺<EFBFBD><EFBFBD>𣂼<EFBFBD>! TaskId: $taskId" -ForegroundColor Green
Write-Host " <EFBFBD><EFBFBD><EFBFBD>? $($uploadResponse.data.fileName)" -ForegroundColor Gray
Write-Host " <20>?上传成功! TaskId: $taskId" -ForegroundColor Green
Write-Host " 文件<EFBFBD>? $($uploadResponse.data.fileName)" -ForegroundColor Gray
# 等待文档提取
Write-Host "`n5. 蝑匧<EFBFBD><EFBFBD><EFBFBD><EFBFBD>𣂼<EFBFBD>嚗?蝘𡜐<E89D98>..." -ForegroundColor Yellow
Write-Host "`n5. 等待文档提取<EFBFBD>?秒)..." -ForegroundColor Yellow
Start-Sleep -Seconds 3
# 测试4: 获取任务详情
Write-Host "`n6. 测试获取任务详情 (GET /api/v1/rvw/tasks/$taskId)..." -ForegroundColor Yellow
try {
$detail = Invoke-RestMethod -Uri "$BaseUrl/api/v1/rvw/tasks/$taskId" -Method Get
Write-Host " <20>?<EFBFBD>𣂼<EFBFBD>! <20><EFBFBD>? $($detail.data.status)" -ForegroundColor Green
Write-Host " <20>?成功! 状<EFBFBD>? $($detail.data.status)" -ForegroundColor Green
} catch {
Write-Host " <20>?憭梯揖: $($_.Exception.Message)" -ForegroundColor Red
Write-Host " <20>?失败: $($_.Exception.Message)" -ForegroundColor Red
}
# 测试5: 运行审查(只选规范性)
Write-Host "`n7. 瘚贝<EFBFBD>餈鞱<EFBFBD>摰⊥䰻-<2D><EFBFBD><EFBFBD><E39591><EFBFBD><EFBFBD>?(POST /api/v1/rvw/tasks/$taskId/run)..." -ForegroundColor Yellow
Write-Host "`n7. 测试运行审查-只选规范<EFBFBD>?(POST /api/v1/rvw/tasks/$taskId/run)..." -ForegroundColor Yellow
try {
$body = @{ agents = @("editorial") } | ConvertTo-Json
$runResult = Invoke-RestMethod -Uri "$BaseUrl/api/v1/rvw/tasks/$taskId/run" `
-Method Post -Body $body -ContentType "application/json"
Write-Host " <20>?摰⊥䰻隞餃𦛚撌脣鍳<EFBFBD>?" -ForegroundColor Green
Write-Host " <20>?瘜冽<EFBFBD>嚗鋫I霂<EFBFBD><EFBFBD><EFBFBD>閬?-2<><32><EFBFBD><EFBFBD>虾蝔滚<E89D94><E6BB9A><EFBFBD><E4BAA6><EFBFBD>" -ForegroundColor Yellow
Write-Host " <20>?审查任务已启<EFBFBD>?" -ForegroundColor Green
Write-Host " <20>?注意AI评估需<EFBFBD>?-2分钟可稍后查看报告" -ForegroundColor Yellow
} catch {
$errorBody = $_.ErrorDetails.Message | ConvertFrom-Json
Write-Host " ⚠️ $($errorBody.message)" -ForegroundColor Yellow
}
} else {
Write-Host " <20>?銝𠹺<EFBFBD>憭梯揖: $($uploadResponse.message)" -ForegroundColor Red
Write-Host " <20>?上传失败: $($uploadResponse.message)" -ForegroundColor Red
}
} catch {
Write-Host " <20>?憭梯揖: $($_.Exception.Message)" -ForegroundColor Red
Write-Host " <20>?失败: $($_.Exception.Message)" -ForegroundColor Red
}
} else {
Write-Host " <EFBFBD>𩤃<EFBFBD> 瘚贝<E7989A><E8B49D><EFBFBD>辣銝滚<E98A9D><EFBFBD>? $TestFile" -ForegroundColor Yellow
Write-Host " ⚠️ 测试文件不存<EFBFBD>? $TestFile" -ForegroundColor Yellow
Write-Host " 跳过上传测试,请手动测试" -ForegroundColor Gray
}
# 瘚贝<EFBFBD>6: <20><EFBFBD>API<50>澆捆<EFBFBD>?
Write-Host "`n8. 瘚贝<EFBFBD><EFBFBD><EFBFBD>API<EFBFBD>澆捆<EFBFBD>?(GET /api/v1/review/tasks)..." -ForegroundColor Yellow
# 测试6: 旧版API兼容<EFBFBD>?
Write-Host "`n8. 测试旧版API兼容<EFBFBD>?(GET /api/v1/review/tasks)..." -ForegroundColor Yellow
try {
$response = Invoke-RestMethod -Uri "$BaseUrl/api/v1/review/tasks" -Method Get
Write-Host " <20>?<EFBFBD><EFBFBD>API甇<EFBFBD>虜! 隞餃𦛚<EFBFBD>? $($response.pagination.total)" -ForegroundColor Green
Write-Host " <20>?旧版API正常! 任务<EFBFBD>? $($response.pagination.total)" -ForegroundColor Green
} catch {
Write-Host " <20>?<EFBFBD><EFBFBD>API撘<EFBFBD>: $($_.Exception.Message)" -ForegroundColor Red
Write-Host " <20>?旧版API异常: $($_.Exception.Message)" -ForegroundColor Red
}
Write-Host "`n========================================" -ForegroundColor Cyan
@@ -118,3 +118,5 @@ Write-Host " - 删除任务: DELETE $BaseUrl/api/v1/rvw/tasks/{taskId}" -Foregr

View File

@@ -32,3 +32,5 @@ export * from './services/utils.js';

View File

@@ -123,3 +123,5 @@ export function validateAgentSelection(agents: string[]): void {

View File

@@ -420,6 +420,8 @@ SET session_replication_role = 'origin';

View File

@@ -122,6 +122,8 @@ WHERE key = 'verify_test';

View File

@@ -265,6 +265,8 @@ verifyDatabase()

View File

@@ -55,6 +55,8 @@ export {}