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:
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* PKB璅∪<EFBFBD>API蝞<EFBFBD><EFBFBD>𡝗<EFBFBD>霂閗<EFBFBD><EFBFBD>?
|
||||
* PKB模块API简化测试脚<EFBFBD>?
|
||||
* 测试现有知识库的各项功能
|
||||
*/
|
||||
|
||||
@@ -17,12 +17,12 @@ interface TestResult {
|
||||
const results: TestResult[] = [];
|
||||
|
||||
function printResult(result: TestResult) {
|
||||
const icon = result.status === 'pass' ? '<27>? : '<EFBFBD>?;
|
||||
const icon = result.status === 'pass' ? '<27>? : '<EFBFBD>?;
|
||||
console.log(`${icon} ${result.name} ${result.duration ? `(${result.duration}ms)` : ''}`);
|
||||
console.log(` ${result.message}`);
|
||||
}
|
||||
|
||||
// 瘚贝<EFBFBD>1嚗𡁜<EFBFBD>摨瑟<EFBFBD><EFBFBD>?
|
||||
// 测试1:健康检<EFBFBD>?
|
||||
async function testHealthCheck(): Promise<TestResult> {
|
||||
const startTime = Date.now();
|
||||
try {
|
||||
@@ -31,22 +31,22 @@ async function testHealthCheck(): Promise<TestResult> {
|
||||
|
||||
if (response.data.status === 'ok') {
|
||||
return {
|
||||
name: '<EFBFBD>亙熒璉<EFBFBD><EFBFBD>伐<EFBFBD>v2嚗?,
|
||||
name: '健康检查(v2<EFBFBD>?,
|
||||
status: 'pass',
|
||||
message: `知识库数: ${response.data.database.knowledgeBases}, schema: ${response.data.database.schema}`,
|
||||
duration,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
name: '<EFBFBD>亙熒璉<EFBFBD><EFBFBD>伐<EFBFBD>v2嚗?,
|
||||
name: '健康检查(v2<EFBFBD>?,
|
||||
status: 'fail',
|
||||
message: '餈𥪜<EFBFBD><EFBFBD>嗆<EFBFBD><EFBFBD><EFBFBD>撣?,
|
||||
message: '返回状态异<EFBFBD>?,
|
||||
duration,
|
||||
};
|
||||
}
|
||||
} catch (error: any) {
|
||||
return {
|
||||
name: '<EFBFBD>亙熒璉<EFBFBD><EFBFBD>伐<EFBFBD>v2嚗?,
|
||||
name: '健康检查(v2<EFBFBD>?,
|
||||
status: 'fail',
|
||||
message: error.message,
|
||||
duration: Date.now() - startTime,
|
||||
@@ -54,7 +54,7 @@ async function testHealthCheck(): Promise<TestResult> {
|
||||
}
|
||||
}
|
||||
|
||||
// 瘚贝<EFBFBD>2嚗朞繮<EFBFBD>𣇉䰻霂<EFBFBD><EFBFBD><EFBFBD>𡑒”嚗ǒ1 vs v2嚗?
|
||||
// 测试2:获取知识库列表(v1 vs v2<EFBFBD>?
|
||||
async function testGetKnowledgeBases(): Promise<TestResult> {
|
||||
try {
|
||||
const startV1 = Date.now();
|
||||
@@ -70,14 +70,14 @@ async function testGetKnowledgeBases(): Promise<TestResult> {
|
||||
|
||||
if (v1Count === v2Count) {
|
||||
return {
|
||||
name: '<EFBFBD>瑕<EFBFBD><EFBFBD>亥<EFBFBD>摨枏<EFBFBD>銵剁<EFBFBD>v1 vs v2嚗?,
|
||||
name: '获取知识库列表(v1 vs v2<EFBFBD>?,
|
||||
status: 'pass',
|
||||
message: `v1: ${v1Count}銝?(${v1Duration}ms), v2: ${v2Count}銝?(${v2Duration}ms) <EFBFBD><EFBFBD>,
|
||||
message: `v1: ${v1Count}<EFBFBD>?(${v1Duration}ms), v2: ${v2Count}<EFBFBD>?(${v2Duration}ms) ✅`,
|
||||
duration: v1Duration + v2Duration,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
name: '<EFBFBD>瑕<EFBFBD><EFBFBD>亥<EFBFBD>摨枏<EFBFBD>銵剁<EFBFBD>v1 vs v2嚗?,
|
||||
name: '获取知识库列表(v1 vs v2<EFBFBD>?,
|
||||
status: 'fail',
|
||||
message: `数量不一致!v1: ${v1Count}, v2: ${v2Count}`,
|
||||
duration: v1Duration + v2Duration,
|
||||
@@ -85,14 +85,14 @@ async function testGetKnowledgeBases(): Promise<TestResult> {
|
||||
}
|
||||
} catch (error: any) {
|
||||
return {
|
||||
name: '<EFBFBD>瑕<EFBFBD><EFBFBD>亥<EFBFBD>摨枏<EFBFBD>銵剁<EFBFBD>v1 vs v2嚗?,
|
||||
name: '获取知识库列表(v1 vs v2<EFBFBD>?,
|
||||
status: 'fail',
|
||||
message: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 瘚贝<EFBFBD>3嚗朞繮<EFBFBD>𣇉䰻霂<EFBFBD><EFBFBD>霂行<EFBFBD>嚗ǒ1 vs v2嚗?
|
||||
// 测试3:获取知识库详情(v1 vs v2<EFBFBD>?
|
||||
async function testGetKnowledgeBaseById(kbId: string): Promise<TestResult> {
|
||||
try {
|
||||
const startV1 = Date.now();
|
||||
@@ -108,14 +108,14 @@ async function testGetKnowledgeBaseById(kbId: string): Promise<TestResult> {
|
||||
|
||||
if (v1Name === v2Name) {
|
||||
return {
|
||||
name: '<EFBFBD>瑕<EFBFBD><EFBFBD>亥<EFBFBD>摨栞祕<EFBFBD><EFBFBD><EFBFBD>v1 vs v2嚗?,
|
||||
name: '获取知识库详情(v1 vs v2<EFBFBD>?,
|
||||
status: 'pass',
|
||||
message: `<EFBFBD>滨妍銝<EFBFBD><EFBFBD>? "${v1Name}", v1: ${v1Duration}ms, v2: ${v2Duration}ms <EFBFBD><EFBFBD>,
|
||||
message: `名称一<EFBFBD>? "${v1Name}", v1: ${v1Duration}ms, v2: ${v2Duration}ms ✅`,
|
||||
duration: v1Duration + v2Duration,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
name: '<27>瑕<EFBFBD><E79195>亥<EFBFBD>摨栞祕<E6A09E><E7A595><EFBFBD>v1 vs v2嚗?,
|
||||
name: '获取知识库详情(v1 vs v2<EFBFBD>?,
|
||||
status: 'fail',
|
||||
message: `名称不一致!v1: "${v1Name}", v2: "${v2Name}"`,
|
||||
duration: v1Duration + v2Duration,
|
||||
@@ -123,14 +123,14 @@ async function testGetKnowledgeBaseById(kbId: string): Promise<TestResult> {
|
||||
}
|
||||
} catch (error: any) {
|
||||
return {
|
||||
name: '<EFBFBD>瑕<EFBFBD><EFBFBD>亥<EFBFBD>摨栞祕<EFBFBD><EFBFBD><EFBFBD>v1 vs v2嚗?,
|
||||
name: '获取知识库详情(v1 vs v2<EFBFBD>?,
|
||||
status: 'fail',
|
||||
message: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 瘚贝<EFBFBD>4嚗朞繮<EFBFBD>𣇉䰻霂<EFBFBD><EFBFBD>蝏蠘恣嚗ǒ1 vs v2嚗?
|
||||
// 测试4:获取知识库统计(v1 vs v2<EFBFBD>?
|
||||
async function testGetKnowledgeBaseStats(kbId: string): Promise<TestResult> {
|
||||
try {
|
||||
const v1Response = await axios.get(`${BASE_URL}/api/v1/knowledge-bases/${kbId}/stats`);
|
||||
@@ -141,27 +141,27 @@ async function testGetKnowledgeBaseStats(kbId: string): Promise<TestResult> {
|
||||
|
||||
if (v1Docs === v2Docs) {
|
||||
return {
|
||||
name: '<27>瑕<EFBFBD><E79195>亥<EFBFBD>摨梶<E691A8>霈∴<E99C88>v1 vs v2嚗?,
|
||||
name: '获取知识库统计(v1 vs v2<EFBFBD>?,
|
||||
status: 'pass',
|
||||
message: `<EFBFBD><EFBFBD>﹝<EFBFBD>唬<EFBFBD><EFBFBD>? ${v1Docs}銝?<EFBFBD><EFBFBD>,
|
||||
message: `文档数一<E695B0>? ${v1Docs}<7D>?✅`,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
name: '<27>瑕<EFBFBD><E79195>亥<EFBFBD>摨梶<E691A8>霈∴<E99C88>v1 vs v2嚗?,
|
||||
name: '获取知识库统计(v1 vs v2<EFBFBD>?,
|
||||
status: 'fail',
|
||||
message: `文档数不一致!v1: ${v1Docs}, v2: ${v2Docs}`,
|
||||
};
|
||||
}
|
||||
} catch (error: any) {
|
||||
return {
|
||||
name: '<EFBFBD>瑕<EFBFBD><EFBFBD>亥<EFBFBD>摨梶<EFBFBD>霈∴<EFBFBD>v1 vs v2嚗?,
|
||||
name: '获取知识库统计(v1 vs v2<76>?,
|
||||
status: 'fail',
|
||||
message: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 瘚贝<EFBFBD>5嚗鑹AG璉<EFBFBD>蝝g<EFBFBD>v1 vs v2嚗?
|
||||
// 测试5:RAG检索(v1 vs v2<EFBFBD>?
|
||||
async function testSearchKnowledgeBase(kbId: string): Promise<TestResult> {
|
||||
try {
|
||||
const query = '治疗';
|
||||
@@ -176,13 +176,13 @@ async function testSearchKnowledgeBase(kbId: string): Promise<TestResult> {
|
||||
const v2Count = v2Response.data.data?.records?.length || 0;
|
||||
|
||||
return {
|
||||
name: 'RAG璉<47>蝝g<E89D9D>v1 vs v2嚗?,
|
||||
name: 'RAG检索(v1 vs v2<EFBFBD>?,
|
||||
status: 'pass',
|
||||
message: `v1餈𥪜<E9A488>${v1Count}<7D>? v2餈𥪜<E9A488>${v2Count}<7D>?<3F><>,
|
||||
message: `v1返回${v1Count}<EFBFBD>? v2返回${v2Count}<EFBFBD>?✅`,
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
name: 'RAG璉<EFBFBD>蝝g<EFBFBD>v1 vs v2嚗?,
|
||||
name: 'RAG检索(v1 vs v2<76>?,
|
||||
status: 'fail',
|
||||
message: error.message,
|
||||
};
|
||||
@@ -203,13 +203,13 @@ async function testDocumentSelection(kbId: string): Promise<TestResult> {
|
||||
const v2Docs = v2Response.data.data?.selectedDocuments?.length || 0;
|
||||
|
||||
return {
|
||||
name: '<27><>﹝<EFBFBD>㗇𥋘-<2D>冽<EFBFBD><E586BD><EFBFBD>粉璅∪<E79285>嚗ǒ1 vs v2嚗?,
|
||||
name: '文档选择-全文阅读模式(v1 vs v2<EFBFBD>?,
|
||||
status: 'pass',
|
||||
message: `v1<76>㗇𥋘${v1Docs}銝芣<E98A9D>獢? v2<76>㗇𥋘${v2Docs}銝芣<E98A9D>獢?<3F><>,
|
||||
message: `v1选择${v1Docs}个文<EFBFBD>? v2选择${v2Docs}个文<EFBFBD>?✅`,
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
name: '<EFBFBD><EFBFBD>﹝<EFBFBD>㗇𥋘-<EFBFBD>冽<EFBFBD><EFBFBD><EFBFBD>粉璅∪<EFBFBD>嚗ǒ1 vs v2嚗?,
|
||||
name: '文档选择-全文阅读模式(v1 vs v2<76>?,
|
||||
status: 'fail',
|
||||
message: error.message,
|
||||
};
|
||||
@@ -226,26 +226,26 @@ async function testBatchTemplates(): Promise<TestResult> {
|
||||
const v2Count = v2Response.data.data?.length || 0;
|
||||
|
||||
return {
|
||||
name: '<27>孵<EFBFBD><E5ADB5><EFBFBD>芋<EFBFBD>選<EFBFBD>v1 vs v2嚗?,
|
||||
name: '批处理模板(v1 vs v2<EFBFBD>?,
|
||||
status: 'pass',
|
||||
message: `v1: ${v1Count}銝芣芋<EFBFBD>? v2: ${v2Count}銝芣芋<E88AA3>?<3F><>,
|
||||
message: `v1: ${v1Count}个模<EFBFBD>? v2: ${v2Count}个模<EFBFBD>?✅`,
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
name: '<EFBFBD>孵<EFBFBD><EFBFBD><EFBFBD>芋<EFBFBD>選<EFBFBD>v1 vs v2嚗?,
|
||||
name: '批处理模板(v1 vs v2<76>?,
|
||||
status: 'fail',
|
||||
message: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 銝餅<EFBFBD>霂訫遆<EFBFBD>?
|
||||
// 主测试函<EFBFBD>?
|
||||
async function runTests() {
|
||||
console.log('<27><> PKB API瘚贝<E7989A>撘<EFBFBD>憪?..\n');
|
||||
console.log('🚀 PKB API测试开<EFBFBD>?..\n');
|
||||
console.log('='.repeat(80));
|
||||
|
||||
// 瘚贝<EFBFBD>1嚗𡁜<EFBFBD>摨瑟<EFBFBD><EFBFBD>?
|
||||
console.log('\n<><6E> 瘚贝<E7989A>1嚗𡁜<E59A97>摨瑟<E691A8><E7919F>?);
|
||||
// 测试1:健康检<EFBFBD>?
|
||||
console.log('\n📋 测试1:健康检<EFBFBD>?);
|
||||
console.log('-'.repeat(80));
|
||||
results.push(await testHealthCheck());
|
||||
printResult(results[results.length - 1]);
|
||||
@@ -261,12 +261,12 @@ async function runTests() {
|
||||
const firstKb = kbListResponse.data.data?.[0];
|
||||
|
||||
if (!firstKb) {
|
||||
console.log('\n<EFBFBD>?瘝⊥<EFBFBD><EFBFBD>舐鍂<EFBFBD><EFBFBD>䰻霂<EFBFBD><EFBFBD>嚗<EFBFBD><EFBFBD>蝏剜<EFBFBD>霂閗歲餈?);
|
||||
console.log('\n<>?没有可用的知识库,后续测试跳<E8AF95>?);
|
||||
return;
|
||||
}
|
||||
|
||||
const kbId = firstKb.id;
|
||||
console.log(`\n雿輻鍂<EFBFBD>亥<EFBFBD>摨? ${firstKb.name} (ID: ${kbId})`);
|
||||
console.log(`\n使用知识<E79FA5>? ${firstKb.name} (ID: ${kbId})`);
|
||||
|
||||
// 测试3:获取知识库详情
|
||||
console.log('\n📋 测试3:知识库详情');
|
||||
@@ -280,8 +280,8 @@ async function runTests() {
|
||||
results.push(await testGetKnowledgeBaseStats(kbId));
|
||||
printResult(results[results.length - 1]);
|
||||
|
||||
// 瘚贝<EFBFBD>5嚗鑹AG璉<EFBFBD>蝝?
|
||||
console.log('\n<><6E> 瘚贝<E7989A>5嚗鑹AG璉<47>蝝?);
|
||||
// 测试5:RAG检<EFBFBD>?
|
||||
console.log('\n📋 测试5:RAG检<EFBFBD>?);
|
||||
console.log('-'.repeat(80));
|
||||
results.push(await testSearchKnowledgeBase(kbId));
|
||||
printResult(results[results.length - 1]);
|
||||
@@ -308,8 +308,8 @@ async function runTests() {
|
||||
const totalDuration = results.reduce((sum, r) => sum + (r.duration || 0), 0);
|
||||
|
||||
console.log(`\n总计: ${results.length}个测试`);
|
||||
console.log(`<60>?<3F>朞<EFBFBD>: ${passCount}銝注);
|
||||
console.log(`<60>?憭梯揖: ${failCount}銝注);
|
||||
console.log(`<EFBFBD>?通过: ${passCount}个`);
|
||||
console.log(`<EFBFBD>?失败: ${failCount}个`);
|
||||
console.log(`⏱️ 总耗时: ${totalDuration}ms`);
|
||||
|
||||
if (failCount === 0) {
|
||||
@@ -320,7 +320,7 @@ async function runTests() {
|
||||
}
|
||||
|
||||
runTests().catch(error => {
|
||||
console.error('<EFBFBD>?瘚贝<EFBFBD><EFBFBD>扯<EFBFBD>憭梯揖:', error);
|
||||
console.error('<27>?测试执行失败:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -337,3 +337,5 @@ runTests().catch(error => {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user