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
270 lines
8.7 KiB
TypeScript
270 lines
8.7 KiB
TypeScript
/**
|
||
* REDCap查询测试脚本(从数据库读取配置)
|
||
*
|
||
* 目的:测试基础的REDCap查询功能,验证数据格式
|
||
* 数据来源:从数据库 iit_schema.projects 表读取项目配置
|
||
*
|
||
* 运行方式:
|
||
* ```bash
|
||
* cd backend
|
||
* npm run tsx src/modules/iit-manager/test-redcap-query-from-db.ts
|
||
* ```
|
||
*/
|
||
|
||
import { RedcapAdapter } from './adapters/RedcapAdapter.js';
|
||
import { PrismaClient } from '@prisma/client';
|
||
import { logger } from '../../common/logging/index.js';
|
||
import dotenv from 'dotenv';
|
||
|
||
dotenv.config();
|
||
|
||
const prisma = new PrismaClient();
|
||
|
||
async function main() {
|
||
console.log('='.repeat(70));
|
||
console.log('🧪 REDCap查询测试(为AI对话准备)');
|
||
console.log('='.repeat(70));
|
||
console.log();
|
||
|
||
try {
|
||
// =============================================
|
||
// 1. 从数据库读取test0102项目配置
|
||
// =============================================
|
||
console.log('📋 从数据库读取项目配置...');
|
||
|
||
const project = await prisma.iitProject.findFirst({
|
||
where: {
|
||
OR: [
|
||
{ redcapProjectId: '16' },
|
||
{ name: { contains: 'test0102' } }
|
||
],
|
||
status: 'active'
|
||
}
|
||
});
|
||
|
||
if (!project) {
|
||
console.log('❌ 未找到test0102项目(PID: 16)');
|
||
console.log();
|
||
console.log('💡 可能的原因:');
|
||
console.log(' 1. 项目未在数据库中');
|
||
console.log(' 2. 项目状态不是active');
|
||
console.log(' 3. redcapProjectId不是"16"');
|
||
console.log();
|
||
console.log('📝 解决方法:');
|
||
console.log(' 请检查数据库 iit_schema.projects 表');
|
||
console.log(' 或联系管理员添加test0102项目配置');
|
||
console.log();
|
||
process.exit(1);
|
||
}
|
||
|
||
console.log('✅ 项目配置读取成功');
|
||
console.log();
|
||
console.log('📋 项目信息:');
|
||
console.log(` 数据库ID: ${project.id}`);
|
||
console.log(` 项目名称: ${project.name}`);
|
||
console.log(` REDCap项目ID: ${project.redcapProjectId}`);
|
||
console.log(` REDCap URL: ${project.redcapUrl}`);
|
||
console.log(` API Token: ${project.redcapApiToken.substring(0, 8)}***`);
|
||
console.log(` 状态: ${project.status}`);
|
||
console.log();
|
||
|
||
// =============================================
|
||
// 2. 创建RedcapAdapter实例
|
||
// =============================================
|
||
console.log('📦 创建RedcapAdapter实例...');
|
||
const redcap = new RedcapAdapter(
|
||
project.redcapUrl,
|
||
project.redcapApiToken
|
||
);
|
||
console.log('✅ Adapter创建成功\n');
|
||
|
||
// =============================================
|
||
// 3. 测试连接
|
||
// =============================================
|
||
console.log('🔌 测试API连接...');
|
||
try {
|
||
const isConnected = await redcap.testConnection();
|
||
if (isConnected) {
|
||
console.log('✅ API连接成功\n');
|
||
} else {
|
||
console.log('❌ API连接失败');
|
||
console.log(' 请检查:');
|
||
console.log(' 1. REDCap服务是否启动');
|
||
console.log(` 2. URL是否正确: ${project.redcapUrl}`);
|
||
console.log(' 3. API Token是否有效');
|
||
console.log();
|
||
process.exit(1);
|
||
}
|
||
} catch (error: any) {
|
||
console.error('❌ 连接测试失败:', error.message);
|
||
process.exit(1);
|
||
}
|
||
|
||
// =============================================
|
||
// 4. 查询所有记录(获取记录ID列表)
|
||
// =============================================
|
||
console.log('📊 测试1: 查询所有记录(获取记录ID列表)');
|
||
console.log('-'.repeat(70));
|
||
|
||
const allRecords = await redcap.exportRecords({
|
||
fields: ['record_id']
|
||
});
|
||
|
||
// 提取唯一记录ID
|
||
const uniqueRecordIds = Array.from(
|
||
new Set(allRecords.map((r) => r.record_id || r.record))
|
||
);
|
||
|
||
console.log(`✅ 查询成功`);
|
||
console.log(` 总记录数: ${uniqueRecordIds.length}`);
|
||
console.log(` 记录ID列表: ${uniqueRecordIds.join(', ')}`);
|
||
console.log();
|
||
|
||
if (uniqueRecordIds.length === 0) {
|
||
console.log('⚠️ 项目中没有记录,请先在REDCap中创建测试数据');
|
||
console.log(' 建议:在test0102项目中添加几条测试记录');
|
||
console.log();
|
||
process.exit(0);
|
||
}
|
||
|
||
// =============================================
|
||
// 5. 查询特定记录的详细信息
|
||
// =============================================
|
||
const testRecordId = uniqueRecordIds[0]; // 使用第一条记录做测试
|
||
console.log(`📋 测试2: 查询特定记录的详细信息 (ID: ${testRecordId})`);
|
||
console.log('-'.repeat(70));
|
||
|
||
const specificRecord = await redcap.exportRecords({
|
||
records: [testRecordId]
|
||
});
|
||
|
||
if (specificRecord.length > 0) {
|
||
console.log('✅ 查询成功');
|
||
console.log(` 记录数: ${specificRecord.length} 条`);
|
||
console.log();
|
||
console.log('📄 第一条记录的数据结构:');
|
||
|
||
const firstRecord = specificRecord[0];
|
||
const fields = Object.keys(firstRecord);
|
||
|
||
console.log(` 共 ${fields.length} 个字段:`);
|
||
fields.slice(0, 10).forEach((field, index) => {
|
||
const value = firstRecord[field];
|
||
const displayValue = value === '' ? '(空值)' : value;
|
||
console.log(` ${(index + 1).toString().padStart(2, ' ')}. ${field.padEnd(30)} = ${displayValue}`);
|
||
});
|
||
if (fields.length > 10) {
|
||
console.log(` ... (还有 ${fields.length - 10} 个字段)`);
|
||
}
|
||
console.log();
|
||
} else {
|
||
console.log('❌ 未找到记录');
|
||
console.log();
|
||
}
|
||
|
||
// =============================================
|
||
// 6. 模拟AI对话场景的查询
|
||
// =============================================
|
||
console.log('🤖 测试3: 模拟AI对话场景');
|
||
console.log('-'.repeat(70));
|
||
console.log();
|
||
|
||
// 场景1: 用户问"有多少条记录?"
|
||
console.log('【场景1】用户问:"我们系统中已经有几条记录了?"');
|
||
const countResult = {
|
||
projectName: project.name,
|
||
totalRecords: uniqueRecordIds.length,
|
||
recordIds: uniqueRecordIds
|
||
};
|
||
console.log('💾 AI需要的数据:');
|
||
console.log(JSON.stringify(countResult, null, 2));
|
||
console.log();
|
||
console.log('🤖 AI应该回答:');
|
||
console.log(` "您好!根据REDCap系统记录,当前项目${project.name}已有 **${uniqueRecordIds.length}条** 患者数据记录。"`);
|
||
console.log();
|
||
|
||
// 场景2: 用户问特定记录的信息
|
||
const demoRecordId = uniqueRecordIds.includes('7') ? '7' : uniqueRecordIds[0];
|
||
console.log(`【场景2】用户问:"了解Redcap中记录为ID ${demoRecordId}的信息"`);
|
||
const recordDetailResult = await redcap.exportRecords({
|
||
records: [demoRecordId]
|
||
});
|
||
|
||
console.log('💾 AI需要的数据:');
|
||
console.log(JSON.stringify({
|
||
projectName: project.name,
|
||
recordId: demoRecordId,
|
||
data: recordDetailResult[0]
|
||
}, null, 2));
|
||
console.log();
|
||
|
||
// 场景3: 用户问"项目名称"
|
||
console.log('【场景3】用户问:"咱们当前的项目名称是什么?"');
|
||
console.log('💾 AI需要的数据:');
|
||
console.log(JSON.stringify({
|
||
projectName: project.name,
|
||
redcapProjectId: project.redcapProjectId,
|
||
recordCount: uniqueRecordIds.length,
|
||
lastSync: project.lastSyncAt
|
||
}, null, 2));
|
||
console.log();
|
||
console.log('🤖 AI应该回答:');
|
||
console.log(` "您好!当前项目名称为 **${project.name}**。如需查看完整方案或项目详情,请登录REDCap系统或查阅项目文档。"`);
|
||
console.log();
|
||
|
||
// =============================================
|
||
// 测试总结
|
||
// =============================================
|
||
console.log('='.repeat(70));
|
||
console.log('✅ 所有测试完成!REDCap查询功能正常!');
|
||
console.log('='.repeat(70));
|
||
console.log();
|
||
console.log('📝 测试总结:');
|
||
console.log(` 1. ✅ 项目配置从数据库读取成功`);
|
||
console.log(` 2. ✅ API连接正常`);
|
||
console.log(` 3. ✅ 可以查询所有记录 (${uniqueRecordIds.length} 条)`);
|
||
console.log(` 4. ✅ 可以查询特定记录`);
|
||
console.log(` 5. ✅ 数据格式符合AI对话需求`);
|
||
console.log();
|
||
console.log('🚀 下一步:');
|
||
console.log(' 将查询功能集成到ChatService,让AI能够基于真实数据回答问题');
|
||
console.log();
|
||
|
||
} catch (error: any) {
|
||
console.error('❌ 测试失败:', error.message);
|
||
console.error(' 错误详情:', error);
|
||
process.exit(1);
|
||
} finally {
|
||
await prisma.$disconnect();
|
||
}
|
||
|
||
process.exit(0);
|
||
}
|
||
|
||
// 执行测试
|
||
main().catch((error) => {
|
||
console.error('💥 测试脚本执行失败:', error);
|
||
process.exit(1);
|
||
});
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|