Files
AIclinicalresearch/backend/test-tool-c-day2.mjs
HaHafeng 66255368b7 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
2026-01-16 13:42:10 +08:00

439 lines
11 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
/**
* Tool C Day 2 API测试脚本
*
* 测试内容:
* 1. 创建测试Excel文件
* 2. 上传文件创建Session
* 3. 获取Session信息
* 4. 获取预览数据
* 5. 获取完整数据
* 6. 更新心跳
* 7. 删除Session
*
* 执行方式node test-tool-c-day2.mjs
*/
import FormData from 'form-data';
import axios from 'axios';
import * as XLSX from 'xlsx';
import { Buffer } from 'buffer';
const BASE_URL = 'http://localhost:3000';
const API_PREFIX = '/api/v1/dc/tool-c';
// ==================== 辅助函数 ====================
function printSection(title) {
console.log('\n' + '='.repeat(70));
console.log(` ${title}`);
console.log('='.repeat(70) + '\n');
}
function printSuccess(message) {
console.log('✅ ' + message);
}
function printError(message) {
console.log('❌ ' + message);
}
function printInfo(message) {
console.log(' ' + message);
}
// ==================== 创建测试Excel文件 ====================
function createTestExcelFile() {
printSection('创建测试Excel文件');
// 创建医疗数据
const testData = [
{ patient_id: 'P001', name: '张三', age: 25, gender: '男', diagnosis: '感冒', sbp: 120, dbp: 80 },
{ patient_id: 'P002', name: '李四', age: 65, gender: '女', diagnosis: '高血压', sbp: 150, dbp: 95 },
{ patient_id: 'P003', name: '王五', age: 45, gender: '男', diagnosis: '糖尿病', sbp: 135, dbp: 85 },
{ patient_id: 'P004', name: '赵六', age: 70, gender: '女', diagnosis: '冠心病', sbp: 160, dbp: 100 },
{ patient_id: 'P005', name: '钱七', age: 35, gender: '男', diagnosis: '胃炎', sbp: 110, dbp: 70 },
{ patient_id: 'P006', name: '孙八', age: 55, gender: '女', diagnosis: '肺炎', sbp: 125, dbp: 82 },
{ patient_id: 'P007', name: '周九', age: 48, gender: '男', diagnosis: '肝炎', sbp: 130, dbp: 88 },
{ patient_id: 'P008', name: '吴十', age: 62, gender: '女', diagnosis: '关节炎', sbp: 145, dbp: 92 },
];
// 创建工作簿
const ws = XLSX.utils.json_to_sheet(testData);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, 'Sheet1');
// 生成Buffer
const excelBuffer = XLSX.write(wb, { type: 'buffer', bookType: 'xlsx' });
printSuccess(`测试文件创建成功: ${testData.length}行 x ${Object.keys(testData[0]).length}`);
printInfo(`文件大小: ${(excelBuffer.length / 1024).toFixed(2)} KB`);
return {
buffer: excelBuffer,
fileName: 'test-medical-data.xlsx',
expectedRows: testData.length,
expectedCols: Object.keys(testData[0]).length,
};
}
// ==================== API测试函数 ====================
async function testUploadFile(excelData) {
printSection('测试1: 上传Excel文件创建Session');
try {
const form = new FormData();
form.append('file', excelData.buffer, {
filename: excelData.fileName,
contentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
});
printInfo(`上传文件: ${excelData.fileName}`);
const response = await axios.post(
`${BASE_URL}${API_PREFIX}/sessions/upload`,
form,
{
headers: form.getHeaders(),
timeout: 10000,
}
);
if (response.status === 201 && response.data.success) {
printSuccess('文件上传成功');
console.log('响应数据:', JSON.stringify(response.data.data, null, 2));
const { sessionId, totalRows, totalCols, columns } = response.data.data;
// 验证数据
if (totalRows === excelData.expectedRows && totalCols === excelData.expectedCols) {
printSuccess(`数据验证通过: ${totalRows}行 x ${totalCols}`);
} else {
printError(`数据不匹配: 预期${excelData.expectedRows}行x${excelData.expectedCols}列, 实际${totalRows}行x${totalCols}`);
}
printInfo(`Session ID: ${sessionId}`);
printInfo(`列名: ${columns.join(', ')}`);
return sessionId;
} else {
printError('上传失败: ' + JSON.stringify(response.data));
return null;
}
} catch (error) {
printError('上传异常: ' + (error.response?.data?.error || error.message));
if (error.response) {
console.log('错误详情:', JSON.stringify(error.response.data, null, 2));
}
return null;
}
}
async function testGetSession(sessionId) {
printSection('测试2: 获取Session信息');
try {
printInfo(`Session ID: ${sessionId}`);
const response = await axios.get(
`${BASE_URL}${API_PREFIX}/sessions/${sessionId}`,
{ timeout: 5000 }
);
if (response.status === 200 && response.data.success) {
printSuccess('Session信息获取成功');
console.log('Session信息:', JSON.stringify(response.data.data, null, 2));
return true;
} else {
printError('获取失败');
return false;
}
} catch (error) {
printError('获取异常: ' + (error.response?.data?.error || error.message));
return false;
}
}
async function testGetPreviewData(sessionId) {
printSection('测试3: 获取预览数据前100行');
try {
printInfo(`Session ID: ${sessionId}`);
const response = await axios.get(
`${BASE_URL}${API_PREFIX}/sessions/${sessionId}/preview`,
{ timeout: 10000 }
);
if (response.status === 200 && response.data.success) {
printSuccess('预览数据获取成功');
const { totalRows, previewRows, previewData } = response.data.data;
printInfo(`总行数: ${totalRows}, 预览行数: ${previewRows}`);
printInfo(`预览数据前3行:`);
console.log(JSON.stringify(previewData.slice(0, 3), null, 2));
return true;
} else {
printError('获取预览数据失败');
return false;
}
} catch (error) {
printError('获取预览数据异常: ' + (error.response?.data?.error || error.message));
return false;
}
}
async function testGetFullData(sessionId) {
printSection('测试4: 获取完整数据');
try {
printInfo(`Session ID: ${sessionId}`);
const response = await axios.get(
`${BASE_URL}${API_PREFIX}/sessions/${sessionId}/full`,
{ timeout: 10000 }
);
if (response.status === 200 && response.data.success) {
printSuccess('完整数据获取成功');
const { totalRows, data } = response.data.data;
printInfo(`总行数: ${totalRows}`);
printInfo(`完整数据前2行:`);
console.log(JSON.stringify(data.slice(0, 2), null, 2));
return true;
} else {
printError('获取完整数据失败');
return false;
}
} catch (error) {
printError('获取完整数据异常: ' + (error.response?.data?.error || error.message));
return false;
}
}
async function testUpdateHeartbeat(sessionId) {
printSection('测试5: 更新心跳');
try {
printInfo(`Session ID: ${sessionId}`);
const response = await axios.post(
`${BASE_URL}${API_PREFIX}/sessions/${sessionId}/heartbeat`,
{},
{ timeout: 5000 }
);
if (response.status === 200 && response.data.success) {
printSuccess('心跳更新成功');
console.log('新过期时间:', response.data.data.expiresAt);
return true;
} else {
printError('心跳更新失败');
return false;
}
} catch (error) {
printError('心跳更新异常: ' + (error.response?.data?.error || error.message));
return false;
}
}
async function testDeleteSession(sessionId) {
printSection('测试6: 删除Session');
try {
printInfo(`Session ID: ${sessionId}`);
const response = await axios.delete(
`${BASE_URL}${API_PREFIX}/sessions/${sessionId}`,
{ timeout: 5000 }
);
if (response.status === 200 && response.data.success) {
printSuccess('Session删除成功');
return true;
} else {
printError('Session删除失败');
return false;
}
} catch (error) {
printError('Session删除异常: ' + (error.response?.data?.error || error.message));
return false;
}
}
async function testGetDeletedSession(sessionId) {
printSection('测试7: 验证Session已删除');
try {
printInfo(`尝试获取已删除的Session: ${sessionId}`);
const response = await axios.get(
`${BASE_URL}${API_PREFIX}/sessions/${sessionId}`,
{ timeout: 5000 }
);
printError('Session仍然存在不应该');
return false;
} catch (error) {
if (error.response?.status === 404) {
printSuccess('Session已正确删除返回404');
return true;
} else {
printError('未预期的错误: ' + error.message);
return false;
}
}
}
// ==================== 主测试函数 ====================
async function runAllTests() {
console.log('\n' + '🚀'.repeat(35));
console.log(' Tool C Day 2 API测试');
console.log('🚀'.repeat(35));
const results = {};
try {
// 创建测试文件
const excelData = createTestExcelFile();
// 测试1: 上传文件
const sessionId = await testUploadFile(excelData);
results['上传文件'] = !!sessionId;
if (!sessionId) {
printError('上传失败,后续测试无法继续');
return results;
}
// 等待1秒
await new Promise(resolve => setTimeout(resolve, 1000));
// 测试2: 获取Session信息
results['获取Session'] = await testGetSession(sessionId);
await new Promise(resolve => setTimeout(resolve, 500));
// 测试3: 获取预览数据
results['获取预览数据'] = await testGetPreviewData(sessionId);
await new Promise(resolve => setTimeout(resolve, 500));
// 测试4: 获取完整数据
results['获取完整数据'] = await testGetFullData(sessionId);
await new Promise(resolve => setTimeout(resolve, 500));
// 测试5: 更新心跳
results['更新心跳'] = await testUpdateHeartbeat(sessionId);
await new Promise(resolve => setTimeout(resolve, 500));
// 测试6: 删除Session
results['删除Session'] = await testDeleteSession(sessionId);
await new Promise(resolve => setTimeout(resolve, 500));
// 测试7: 验证删除
results['验证删除'] = await testGetDeletedSession(sessionId);
} catch (error) {
printError('测试过程中发生异常: ' + error.message);
console.error(error);
}
// 汇总结果
printSection('测试结果汇总');
let passed = 0;
let total = 0;
for (const [testName, result] of Object.entries(results)) {
total++;
if (result) {
passed++;
console.log(`${testName.padEnd(20)}: ✅ 通过`);
} else {
console.log(`${testName.padEnd(20)}: ❌ 失败`);
}
}
console.log('\n' + '-'.repeat(70));
console.log(`总计: ${passed}/${total} 通过 (${((passed/total)*100).toFixed(1)}%)`);
console.log('-'.repeat(70));
if (passed === total) {
console.log('\n🎉 所有测试通过Day 2 Session管理功能完成\n');
} else {
console.log(`\n⚠️ 有 ${total - passed} 个测试失败,请检查\n`);
}
}
// 执行测试
runAllTests()
.then(() => {
console.log('测试完成');
process.exit(0);
})
.catch((error) => {
console.error('测试失败:', error);
process.exit(1);
});