Files
AIclinicalresearch/backend/scripts/test-pkb-apis-simple.ts
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

342 lines
10 KiB
TypeScript
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.
/**
* PKB模块API简化测试脚<E8AF95>?
* 测试现有知识库的各项功能
*/
import axios from 'axios';
const BASE_URL = 'http://localhost:3000';
interface TestResult {
name: string;
status: 'pass' | 'fail';
message: string;
duration?: number;
}
const results: TestResult[] = [];
function printResult(result: TestResult) {
const icon = result.status === 'pass' ? '<27>? : '<EFBFBD>?;
console.log(`${icon} ${result.name} ${result.duration ? `(${result.duration}ms)` : ''}`);
console.log(` ${result.message}`);
}
// 测试1健康检<E5BAB7>?
async function testHealthCheck(): Promise<TestResult> {
const startTime = Date.now();
try {
const response = await axios.get(`${BASE_URL}/api/v1/pkb/health`);
const duration = Date.now() - startTime;
if (response.data.status === 'ok') {
return {
name: '健康检查v2<76>?,
status: 'pass',
message: `知识库数: ${response.data.database.knowledgeBases}, schema: ${response.data.database.schema}`,
duration,
};
} else {
return {
name: 'v2<EFBFBD>?,
status: 'fail',
message: '返回状态异<E68081>?,
duration,
};
}
} catch (error: any) {
return {
name: 'v2<EFBFBD>?,
status: 'fail',
message: error.message,
duration: Date.now() - startTime,
};
}
}
// 测试2获取知识库列表v1 vs v2<76>?
async function testGetKnowledgeBases(): Promise<TestResult> {
try {
const startV1 = Date.now();
const v1Response = await axios.get(`${BASE_URL}/api/v1/knowledge-bases`);
const v1Duration = Date.now() - startV1;
const startV2 = Date.now();
const v2Response = await axios.get(`${BASE_URL}/api/v1/pkb/knowledge/knowledge-bases`);
const v2Duration = Date.now() - startV2;
const v1Count = v1Response.data.data?.length || 0;
const v2Count = v2Response.data.data?.length || 0;
if (v1Count === v2Count) {
return {
name: '获取知识库列表v1 vs v2<76>?,
status: 'pass',
message: `v1: ${v1Count}<7D>?(${v1Duration}ms), v2: ${v2Count}<7D>?(${v2Duration}ms) ✅`,
duration: v1Duration + v2Duration,
};
} else {
return {
name: 'v1 vs v2<EFBFBD>?,
status: 'fail',
message: `数量不一致v1: ${v1Count}, v2: ${v2Count}`,
duration: v1Duration + v2Duration,
};
}
} catch (error: any) {
return {
name: '获取知识库列表v1 vs v2<76>?,
status: 'fail',
message: error.message,
};
}
}
// 测试3获取知识库详情v1 vs v2<76>?
async function testGetKnowledgeBaseById(kbId: string): Promise<TestResult> {
try {
const startV1 = Date.now();
const v1Response = await axios.get(`${BASE_URL}/api/v1/knowledge-bases/${kbId}`);
const v1Duration = Date.now() - startV1;
const startV2 = Date.now();
const v2Response = await axios.get(`${BASE_URL}/api/v1/pkb/knowledge/knowledge-bases/${kbId}`);
const v2Duration = Date.now() - startV2;
const v1Name = v1Response.data.data?.name;
const v2Name = v2Response.data.data?.name;
if (v1Name === v2Name) {
return {
name: 'v1 vs v2<EFBFBD>?,
status: 'pass',
message: `名称一<EFBFBD>? "${v1Name}", v1: ${v1Duration}ms, v2: ${v2Duration}ms ✅`,
duration: v1Duration + v2Duration,
};
} else {
return {
name: '获取知识库详情v1 vs v2<76>?,
status: 'fail',
message: `名称不一致v1: "${v1Name}", v2: "${v2Name}"`,
duration: v1Duration + v2Duration,
};
}
} catch (error: any) {
return {
name: 'v1 vs v2<EFBFBD>?,
status: 'fail',
message: error.message,
};
}
}
// 测试4获取知识库统计v1 vs v2<76>?
async function testGetKnowledgeBaseStats(kbId: string): Promise<TestResult> {
try {
const v1Response = await axios.get(`${BASE_URL}/api/v1/knowledge-bases/${kbId}/stats`);
const v2Response = await axios.get(`${BASE_URL}/api/v1/pkb/knowledge/knowledge-bases/${kbId}/stats`);
const v1Docs = v1Response.data.data.totalDocuments;
const v2Docs = v2Response.data.data.totalDocuments;
if (v1Docs === v2Docs) {
return {
name: '获取知识库统计v1 vs v2<76>?,
status: 'pass',
message: `文档数一<E695B0>? ${v1Docs}<7D>?✅`,
};
} else {
return {
name: 'v1 vs v2<EFBFBD>?,
status: 'fail',
message: `文档数不一致v1: ${v1Docs}, v2: ${v2Docs}`,
};
}
} catch (error: any) {
return {
name: '获取知识库统计v1 vs v2<76>?,
status: 'fail',
message: error.message,
};
}
}
// 测试5RAG检索v1 vs v2<76>?
async function testSearchKnowledgeBase(kbId: string): Promise<TestResult> {
try {
const query = '';
const v1Response = await axios.get(`${BASE_URL}/api/v1/knowledge-bases/${kbId}/search`, {
params: { query, top_k: 3 },
});
const v2Response = await axios.get(`${BASE_URL}/api/v1/pkb/knowledge/knowledge-bases/${kbId}/search`, {
params: { query, top_k: 3 },
});
const v1Count = v1Response.data.data?.records?.length || 0;
const v2Count = v2Response.data.data?.records?.length || 0;
return {
name: 'RAG检索v1 vs v2<EFBFBD>?,
status: 'pass',
message: `v1返回${v1Count}<EFBFBD>? v2返回${v2Count}<EFBFBD>?✅`,
};
} catch (error: any) {
return {
name: 'RAG检索v1 vs v2<76>?,
status: 'fail',
message: error.message,
};
}
}
// 测试6文档选择全文阅读模式
async function testDocumentSelection(kbId: string): Promise<TestResult> {
try {
const v1Response = await axios.get(`${BASE_URL}/api/v1/knowledge-bases/${kbId}/document-selection`, {
params: { max_files: 5, max_tokens: 100000 },
});
const v2Response = await axios.get(`${BASE_URL}/api/v1/pkb/knowledge/knowledge-bases/${kbId}/document-selection`, {
params: { max_files: 5, max_tokens: 100000 },
});
const v1Docs = v1Response.data.data?.selectedDocuments?.length || 0;
const v2Docs = v2Response.data.data?.selectedDocuments?.length || 0;
return {
name: '-v1 vs v2<EFBFBD>?,
status: 'pass',
message: `v1选择${v1Docs}个文<EFBFBD>? v2选择${v2Docs}个文<EFBFBD>?✅`,
};
} catch (error: any) {
return {
name: '文档选择-全文阅读模式v1 vs v2<76>?,
status: 'fail',
message: error.message,
};
}
}
// 测试7批处理模板
async function testBatchTemplates(): Promise<TestResult> {
try {
const v1Response = await axios.get(`${BASE_URL}/api/v1/batch/templates`);
const v2Response = await axios.get(`${BASE_URL}/api/v1/pkb/batch-tasks/batch/templates`);
const v1Count = v1Response.data.data?.length || 0;
const v2Count = v2Response.data.data?.length || 0;
return {
name: 'v1 vs v2<EFBFBD>?,
status: 'pass',
message: `v1: ${v1Count}个模<EFBFBD>? v2: ${v2Count}个模<EFBFBD>?✅`,
};
} catch (error: any) {
return {
name: '批处理模板v1 vs v2<76>?,
status: 'fail',
message: error.message,
};
}
}
// 主测试函<E8AF95>?
async function runTests() {
console.log('🚀 PKB API测试开<EFBFBD>?..\n');
console.log('='.repeat(80));
// 测试1健康检<E5BAB7>?
console.log('\n📋 1<EFBFBD>?);
console.log('-'.repeat(80));
results.push(await testHealthCheck());
printResult(results[results.length - 1]);
// 测试2获取知识库列表
console.log('\n📋 测试2知识库列表');
console.log('-'.repeat(80));
results.push(await testGetKnowledgeBases());
printResult(results[results.length - 1]);
// 获取第一个知识库ID用于后续测试
const kbListResponse = await axios.get(`${BASE_URL}/api/v1/pkb/knowledge/knowledge-bases`);
const firstKb = kbListResponse.data.data?.[0];
if (!firstKb) {
console.log('\n<>?没有可用的知识库后续测试跳<E8AF95>?);
return;
}
const kbId = firstKb.id;
console.log(`\n使用知识<E79FA5>? ${firstKb.name} (ID: ${kbId})`);
// 测试3获取知识库详情
console.log('\n📋 3');
console.log('-'.repeat(80));
results.push(await testGetKnowledgeBaseById(kbId));
printResult(results[results.length - 1]);
// 测试4知识库统计
console.log('\n📋 4');
console.log('-'.repeat(80));
results.push(await testGetKnowledgeBaseStats(kbId));
printResult(results[results.length - 1]);
// 测试5RAG检<47>?
console.log('\n📋 5RAG检<EFBFBD>?);
console.log('-'.repeat(80));
results.push(await testSearchKnowledgeBase(kbId));
printResult(results[results.length - 1]);
// 测试6文档选择
console.log('\n📋 测试6文档选择全文阅读');
console.log('-'.repeat(80));
results.push(await testDocumentSelection(kbId));
printResult(results[results.length - 1]);
// 测试7批处理模板
console.log('\n📋 测试7批处理模板');
console.log('-'.repeat(80));
results.push(await testBatchTemplates());
printResult(results[results.length - 1]);
// 总结
console.log('\n' + '='.repeat(80));
console.log('📊 测试总结');
console.log('='.repeat(80));
const passCount = results.filter(r => r.status === 'pass').length;
const failCount = results.filter(r => r.status === 'fail').length;
const totalDuration = results.reduce((sum, r) => sum + (r.duration || 0), 0);
console.log(`\n总计: ${results.length}个测试`);
console.log(`<EFBFBD>?通过: ${passCount}`);
console.log(`<EFBFBD>?失败: ${failCount}`);
console.log(`⏱️ 总耗时: ${totalDuration}ms`);
if (failCount === 0) {
console.log('\n🎉 所有测试通过v1和v2功能完全一致');
} else {
console.log('\n⚠ 部分测试失败,请查看详情');
}
}
runTests().catch(error => {
console.error('<27>?测试执行失败:', error);
process.exit(1);
});