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

@@ -45,6 +45,8 @@ Status: Day 1 complete (11/11 tasks), ready for Day 2

View File

@@ -275,6 +275,8 @@

View File

@@ -224,3 +224,5 @@ https://iit.xunzhengyixue.com/api/v1/iit/health

View File

@@ -153,3 +153,5 @@ https://iit.xunzhengyixue.com/api/v1/iit/health

View File

@@ -54,3 +54,5 @@

View File

@@ -314,3 +314,5 @@ npx tsx src/modules/iit-manager/test-patient-wechat-url-verify.ts

View File

@@ -176,3 +176,5 @@ npm run dev

View File

@@ -53,3 +53,5 @@ main()

View File

@@ -47,3 +47,5 @@ main()

View File

@@ -42,3 +42,5 @@ main()

View File

@@ -74,3 +74,5 @@ main()

View File

@@ -37,3 +37,5 @@ main()

View File

@@ -78,3 +78,5 @@ main()

View File

@@ -25,3 +25,5 @@ main()

View File

@@ -113,3 +113,5 @@ main()

View File

@@ -84,3 +84,5 @@ main()

View File

@@ -70,3 +70,5 @@ main()

View File

@@ -112,3 +112,5 @@ main()

View File

@@ -23,3 +23,5 @@ ON CONFLICT (id) DO NOTHING;

View File

@@ -55,3 +55,5 @@ ON CONFLICT (id) DO NOTHING;

View File

@@ -70,6 +70,8 @@ WHERE table_schema = 'dc_schema'

View File

@@ -44,6 +44,7 @@
"@types/js-yaml": "^4.0.9",
"@types/jsonwebtoken": "^9.0.7",
"@types/node": "^24.7.1",
"@types/uuid": "^10.0.0",
"@types/winston": "^2.4.4",
"@types/xml2js": "^0.4.14",
"better-sqlite3": "^12.4.6",
@@ -1098,6 +1099,13 @@
"license": "MIT",
"optional": true
},
"node_modules/@types/uuid": {
"version": "10.0.0",
"resolved": "https://registry.npmmirror.com/@types/uuid/-/uuid-10.0.0.tgz",
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/winston": {
"version": "2.4.4",
"resolved": "https://registry.npmmirror.com/@types/winston/-/winston-2.4.4.tgz",

View File

@@ -30,22 +30,22 @@
"@fastify/jwt": "^10.0.0",
"@fastify/multipart": "^9.2.1",
"@prisma/client": "^6.17.0",
"bcryptjs": "^2.4.3",
"handlebars": "^4.7.8",
"jsonwebtoken": "^9.0.2",
"@types/form-data": "^2.2.1",
"@wecom/crypto": "^1.0.1",
"ajv": "^8.17.1",
"axios": "^1.12.2",
"bcryptjs": "^2.4.3",
"bullmq": "^5.65.0",
"diff-match-patch": "^1.0.5",
"dotenv": "^17.2.3",
"exceljs": "^4.4.0",
"fastify": "^5.6.1",
"form-data": "^4.0.4",
"handlebars": "^4.7.8",
"html2canvas": "^1.4.1",
"js-yaml": "^4.1.0",
"jsonrepair": "^3.13.1",
"jsonwebtoken": "^9.0.2",
"jspdf": "^3.0.3",
"p-queue": "^9.0.1",
"pg-boss": "^12.5.2",
@@ -61,6 +61,7 @@
"@types/js-yaml": "^4.0.9",
"@types/jsonwebtoken": "^9.0.7",
"@types/node": "^24.7.1",
"@types/uuid": "^10.0.0",
"@types/winston": "^2.4.4",
"@types/xml2js": "^0.4.14",
"better-sqlite3": "^12.4.6",

View File

@@ -108,6 +108,8 @@ ORDER BY ordinal_position;

View File

@@ -121,6 +121,8 @@ runMigration()

View File

@@ -55,6 +55,8 @@ COMMENT ON COLUMN "dc_schema"."dc_tool_c_sessions"."column_mapping" IS '列名

View File

@@ -82,6 +82,8 @@ COMMENT ON COLUMN dc_schema.dc_tool_c_sessions.expires_at IS '过期时间(创

View File

@@ -45,6 +45,7 @@ model User {
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
tenant_members tenant_members[]
user_modules user_modules[]
departments departments? @relation(fields: [department_id], references: [id])
tenants tenants @relation(fields: [tenant_id], references: [id])
@@ -1042,6 +1043,27 @@ model tenant_modules {
@@schema("platform_schema")
}
/// 用户模块权限表(精细控制用户可访问的模块)
model user_modules {
id String @id @default(uuid())
user_id String
tenant_id String /// 在哪个租户内的权限
module_code String /// 模块代码: RVW, PKB, ASL, DC, IIT, AIA
is_enabled Boolean @default(true) /// 是否启用
created_at DateTime @default(now())
updated_at DateTime @updatedAt
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
tenant tenants @relation(fields: [tenant_id], references: [id], onDelete: Cascade)
@@unique([user_id, tenant_id, module_code])
@@index([user_id])
@@index([tenant_id])
@@index([module_code])
@@map("user_modules")
@@schema("platform_schema")
}
model tenant_quota_allocations {
id Int @id @default(autoincrement())
tenant_id String
@@ -1095,6 +1117,7 @@ model tenants {
tenant_quota_allocations tenant_quota_allocations[]
tenant_quotas tenant_quotas[]
users User[]
user_modules user_modules[]
@@index([code], map: "idx_tenants_code")
@@index([status], map: "idx_tenants_status")

View File

@@ -122,6 +122,8 @@ Write-Host ""

View File

@@ -232,6 +232,8 @@ function extractCodeBlocks(obj, blocks = []) {

View File

@@ -32,3 +32,5 @@ CREATE TABLE IF NOT EXISTS platform_schema.job_common (

View File

@@ -106,3 +106,5 @@ CREATE OR REPLACE FUNCTION platform_schema.delete_queue(queue_name text) RETURNS

View File

@@ -251,6 +251,8 @@ checkDCTables();

View File

@@ -7,3 +7,5 @@ CREATE SCHEMA IF NOT EXISTS capability_schema;

View File

@@ -203,6 +203,8 @@ createAiHistoryTable()

View File

@@ -190,6 +190,8 @@ createToolCTable()

View File

@@ -187,6 +187,8 @@ createToolCTable()

View File

@@ -117,3 +117,5 @@ main()

View File

@@ -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><EFBFBD>v1 vs v2?
// 测试5RAG检索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><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><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>蝝?);
// 测试5RAG检<EFBFBD>?
console.log('\n📋 5RAG检<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 => {

View File

@@ -1,15 +1,15 @@
/**
* PKB<EFBFBD>API<EFBFBD>芸𢆡<EFBFBD>𡝗<EFBFBD>霂閗<EFBFBD><EFBFBD>? *
* <20><EFBFBD>? * 1. 瘚贝<EFBFBD><EFBFBD><EFBFBD><EFBFBD>侨KB API蝡舐<E89DA1>嚗ǒ1<C792>綋2嚗? * 2. 撖寞<E69296>v1<76>綋2<E7B68B><32><EFBFBD><EFBFBD><EFBFBD><E482BF>? * 3. 撉諹<E69289><E8ABB9>唳旿銝<E697BF><E98A9D><EFBFBD>? * 4. <EFBFBD><EFBFBD>撖寞<EFBFBD>
* PKB模块API自动化测试脚<EFBFBD>? *
* 功能<EFBFBD>? * 1. 测试所有PKB API端点v1和v2<76>? * 2. 对比v1和v2的返回结<E59B9E>? * 3. 验证数据一致<EFBFBD>? * 4. 性能对比
* 5. 边界条件测试
*
* 餈鞱<EFBFBD><EFBFBD><EFBFBD>? * npx tsx scripts/test-pkb-apis.ts
* 运行方式<EFBFBD>? * npx tsx scripts/test-pkb-apis.ts
*/
import axios, { AxiosError } from 'axios';
const BASE_URL = 'http://localhost:3000';
const TEST_KB_NAME = `瘚贝<EFBFBD><EFBFBD><EFBFBD>?${Date.now()}`;
const TEST_KB_NAME = `测试知识<EFBFBD>?${Date.now()}`;
interface TestResult {
name: string;
@@ -23,19 +23,19 @@ interface TestResult {
const results: TestResult[] = [];
let testKbId: string | null = null;
// 撌亙<EFBFBD><EFBFBD>賣㺭嚗𡁏<EFBFBD><EFBFBD>舅銝芸<EFBFBD>摨娍糓<EFBFBD><EFBFBD><EFBFBD>?function compareResponses(v1: any, v2: any): boolean {
// 工具函数:比较两个响应是否一<EFBFBD>?function compareResponses(v1: any, v2: any): boolean {
return JSON.stringify(v1) === JSON.stringify(v2);
}
// 撌亙<EFBFBD><EFBFBD>賣㺭嚗𡁏<EFBFBD><EFBFBD><EFBFBD>霂閧<EFBFBD><EFBFBD>?function printResult(result: TestResult) {
const icon = result.status === 'pass' ? '<27>? : result.status === 'fail' ? '<EFBFBD>? : '<EFBFBD><EFBFBD>';
// 工具函数:打印测试结<EFBFBD>?function printResult(result: TestResult) {
const icon = result.status === 'pass' ? '<27>? : result.status === 'fail' ? '<EFBFBD>? : '⏭️';
console.log(`${icon} ${result.name} ${result.duration ? `(${result.duration}ms)` : ''}`);
if (result.message) {
console.log(` ${result.message}`);
}
}
// 瘚贝<EFBFBD>1嚗𡁜<EFBFBD>摨瑟<EFBFBD><EFBFBD>?async function testHealthCheck(): Promise<TestResult> {
// 测试1健康检<EFBFBD>?async function testHealthCheck(): Promise<TestResult> {
const startTime = Date.now();
try {
const response = await axios.get(`${BASE_URL}/api/v1/pkb/health`);
@@ -43,22 +43,22 @@ let testKbId: string | null = null;
if (response.data.status === 'ok' && response.data.module === 'pkb' && response.data.version === 'v2') {
return {
name: '<EFBFBD>亙熒璉<EFBFBD><EFBFBD>?,
name: '健康检<EFBFBD>?,
status: 'pass',
message: `<EFBFBD><EFBFBD>? ${response.data.status}, <EFBFBD><EFBFBD>摨𤘪㺭: ${response.data.database.knowledgeBases}`,
message: `<EFBFBD>? ${response.data.status}, 知识库数: ${response.data.database.knowledgeBases}`,
duration,
};
} else {
return {
name: '<EFBFBD><EFBFBD><EFBFBD>?,
name: '<EFBFBD>?,
status: 'fail',
message: '餈𥪜<EFBFBD><EFBFBD>唳旿<EFBFBD><EFBFBD>銝齿迤蝖?,
message: '返回数据格式不正<EFBFBD>?,
duration,
};
}
} catch (error: any) {
return {
name: '<EFBFBD><EFBFBD><EFBFBD>?,
name: '<EFBFBD>?,
status: 'fail',
message: error.message,
duration: Date.now() - startTime,
@@ -66,7 +66,7 @@ let testKbId: string | null = null;
}
}
// 瘚贝<EFBFBD>2嚗朞繮<EFBFBD>𣇉䰻霂<EFBFBD><EFBFBD><EFBFBD>𡑒”嚗<EFBFBD>笆瘥畕1<EFBFBD>綋2嚗?async function testGetKnowledgeBases(): Promise<TestResult> {
// 测试2获取知识库列表对比v1和v2<EFBFBD>?async function testGetKnowledgeBases(): Promise<TestResult> {
try {
const startV1 = Date.now();
const v1Response = await axios.get(`${BASE_URL}/api/v1/knowledge-bases`);
@@ -81,31 +81,31 @@ let testKbId: string | null = null;
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><EFBFBD><EFBFBD>`,
message: `v1: ${v1Count}<EFBFBD>?(${v1Duration}ms), v2: ${v2Count}<EFBFBD>?(${v2Duration}ms), 数据一致✅`,
duration: v1Duration + v2Duration,
v1Response: v1Response.data,
v2Response: v2Response.data,
};
} else {
return {
name: '<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>v1 vs v2?,
name: 'v1 vs v2<EFBFBD>?,
status: 'fail',
message: `<EFBFBD><EFBFBD>銝滢<EFBFBD><EFBFBD><EFBFBD>v1: ${v1Count}? v2: ${v2Count}銝注,
message: `数量不一致!v1: ${v1Count}<EFBFBD>? v2: ${v2Count}`,
duration: v1Duration + v2Duration,
};
}
} catch (error: any) {
return {
name: '<27><EFBFBD><E79195><EFBFBD>摨枏<E691A8>銵剁<E98AB5>v1 vs v2?,
name: '获取知识库列表(v1 vs v2<EFBFBD>?,
status: 'fail',
message: error.message,
};
}
}
// 瘚贝<EFBFBD>3嚗𡁜<EFBFBD>撱箇䰻霂<EFBFBD><EFBFBD>嚗ǒ2嚗?async function testCreateKnowledgeBase(): Promise<TestResult> {
// 测试3创建知识库v2<EFBFBD>?async function testCreateKnowledgeBase(): Promise<TestResult> {
const startTime = Date.now();
try {
const response = await axios.post(`${BASE_URL}/api/v1/pkb/knowledge/knowledge-bases`, {
@@ -121,14 +121,14 @@ let testKbId: string | null = null;
if (response.data.success && response.data.data.id) {
testKbId = response.data.data.id;
return {
name: '<EFBFBD>𥕦遣<EFBFBD><EFBFBD>摨橒<EFBFBD>v2嚗?,
name: 'v2<EFBFBD>?,
status: 'pass',
message: `成功创建ID: ${testKbId}`,
duration,
};
} else {
return {
name: '<27>𥕦遣<F0A595A6><EFBFBD>摨橒<E691A8>v2?,
name: '创建知识库(v2<EFBFBD>?,
status: 'fail',
message: '',
duration,
@@ -139,7 +139,7 @@ let testKbId: string | null = null;
JSON.stringify(error.response.data) :
(error.response?.data?.message || error.message);
return {
name: '<EFBFBD>𥕦遣<EFBFBD><EFBFBD>摨橒<EFBFBD>v2嚗?,
name: 'v2<EFBFBD>?,
status: 'fail',
message: errorDetail,
duration: Date.now() - startTime,
@@ -147,7 +147,7 @@ let testKbId: string | null = null;
}
}
// 瘚贝<EFBFBD>4嚗朞繮<EFBFBD>𣇉䰻霂<EFBFBD><EFBFBD>霂行<EFBFBD><EFBFBD>笆瘥畕1<EFBFBD>綋2嚗?async function testGetKnowledgeBaseById(kbId: string): Promise<TestResult> {
// 测试4获取知识库详情对比v1和v2<EFBFBD>?async function testGetKnowledgeBaseById(kbId: string): Promise<TestResult> {
try {
const startV1 = Date.now();
const v1Response = await axios.get(`${BASE_URL}/api/v1/knowledge-bases/${kbId}`);
@@ -162,14 +162,14 @@ let testKbId: string | null = null;
if (v1Name === v2Name) {
return {
name: '<27><EFBFBD><E79195><EFBFBD>摨栞祕<E6A09E><E7A595><EFBFBD>v1 vs v2?,
name: '获取知识库详情(v1 vs v2<EFBFBD>?,
status: 'pass',
message: `v1: ${v1Duration}ms, v2: ${v2Duration}ms, <EFBFBD><EFBFBD><EFBFBD>? "${v1Name}"<EFBFBD><EFBFBD>,
message: `v1: ${v1Duration}ms, v2: ${v2Duration}ms, 名称一<E7A7B0>? "${v1Name}"✅`,
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,
@@ -177,32 +177,32 @@ let testKbId: string | null = null;
}
} catch (error: any) {
return {
name: '<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>v1 vs v2嚗?,
name: '获取知识库详情v1 vs v2<76>?,
status: 'fail',
message: error.message,
};
}
}
// 瘚贝<EFBFBD>5嚗𡁏凒<EFBFBD>啁䰻霂<EFBFBD><EFBFBD>嚗ǒ2嚗?async function testUpdateKnowledgeBase(kbId: string): Promise<TestResult> {
// 测试5更新知识库v2<EFBFBD>?async function testUpdateKnowledgeBase(kbId: string): Promise<TestResult> {
const startTime = Date.now();
try {
const response = await axios.put(`${BASE_URL}/api/v1/pkb/knowledge/knowledge-bases/${kbId}`, {
name: `${TEST_KB_NAME}-已更新`,
description: '<EFBFBD>讛膩撌脫凒<EFBFBD>?,
description: '<EFBFBD>?,
});
const duration = Date.now() - startTime;
if (response.data.success) {
return {
name: '<27>湔鰵<E6B994><EFBFBD>摨橒<E691A8>v2?,
name: '更新知识库(v2<EFBFBD>?,
status: 'pass',
message: '',
duration,
};
} else {
return {
name: '<EFBFBD>湔鰵<EFBFBD><EFBFBD>摨橒<EFBFBD>v2嚗?,
name: 'v2<EFBFBD>?,
status: 'fail',
message: '更新失败',
duration,
@@ -210,7 +210,7 @@ let testKbId: string | null = null;
}
} catch (error: any) {
return {
name: '<27>湔鰵<E6B994><EFBFBD>摨橒<E691A8>v2?,
name: '更新知识库(v2<EFBFBD>?,
status: 'fail',
message: error.response?.data?.message || error.message,
duration: Date.now() - startTime,
@@ -218,7 +218,7 @@ let testKbId: string | null = null;
}
}
// 瘚贝<EFBFBD>6嚗朞繮<EFBFBD>𣇉䰻霂<EFBFBD><EFBFBD>蝏蠘恣嚗<EFBFBD>笆瘥畕1<EFBFBD>綋2嚗?async function testGetKnowledgeBaseStats(kbId: string): Promise<TestResult> {
// 测试6获取知识库统计对比v1和v2<EFBFBD>?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`);
@@ -228,27 +228,27 @@ let testKbId: string | null = null;
if (v1Stats.totalDocuments === v2Stats.totalDocuments) {
return {
name: '<EFBFBD><EFBFBD><EFBFBD><EFBFBD>摨梶<EFBFBD>霈∴<EFBFBD>v1 vs v2嚗?,
name: 'v1 vs v2<EFBFBD>?,
status: 'pass',
message: `<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>? ${v1Stats.totalDocuments}<EFBFBD>`,
message: `文档数一<EFBFBD>? ${v1Stats.totalDocuments}个✅`,
};
} else {
return {
name: '<27><EFBFBD><E79195><EFBFBD>摨梶<E691A8>霈∴<E99C88>v1 vs v2?,
name: '获取知识库统计(v1 vs v2<EFBFBD>?,
status: 'fail',
message: `文档数不一致v1: ${v1Stats.totalDocuments}, v2: ${v2Stats.totalDocuments}`,
};
}
} 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>7嚗鑹AG璉<EFBFBD><EFBFBD>撖寞<EFBFBD>v1<EFBFBD>綋2嚗?async function testSearchKnowledgeBase(kbId: string): Promise<TestResult> {
// 测试7RAG检索对比v1和v2<EFBFBD>?async function testSearchKnowledgeBase(kbId: string): Promise<TestResult> {
try {
const query = '测试查询';
const v1Response = await axios.get(`${BASE_URL}/api/v1/knowledge-bases/${kbId}/search`, {
@@ -259,20 +259,20 @@ let testKbId: string | null = null;
});
return {
name: 'RAG璉<47><E89D9D>v1 vs v2?,
name: 'RAG检索v1 vs v2<EFBFBD>?,
status: 'pass',
message: '',
};
} catch (error: any) {
return {
name: 'RAG<EFBFBD><EFBFBD>v1 vs v2嚗?,
name: 'RAG检索v1 vs v2<EFBFBD>?,
status: 'fail',
message: error.message,
};
}
}
// 瘚贝<EFBFBD>8嚗朞器<EFBFBD>峕辺隞?- 銝滚<E98A9D><E6BB9A><EFBFBD><E587BD><EFBFBD>?async function testNotFoundKnowledgeBase(): Promise<TestResult> {
// 测试8边界条<EFBFBD>?- 不存在的知识<E79FA5>?async function testNotFoundKnowledgeBase(): Promise<TestResult> {
try {
await axios.get(`${BASE_URL}/api/v1/pkb/knowledge/knowledge-bases/00000000-0000-0000-0000-000000000000`);
return {
@@ -285,7 +285,7 @@ let testKbId: string | null = null;
return {
name: '边界测试:不存在的知识库',
status: 'pass',
message: `<EFBFBD>𥪜<EFBFBD><EFBFBD><EFBFBD><EFBFBD>? ${error.response.status}<EFBFBD><EFBFBD>,
message: `正确返回错误状<EFBFBD>? ${error.response.status}`,
};
} else {
return {
@@ -297,7 +297,7 @@ let testKbId: string | null = null;
}
}
// 瘚贝<EFBFBD>9嚗𡁏<EFBFBD><EFBFBD>?- <20>𣳇膄瘚贝<E7989A><E8B49D><EFBFBD>?async function testDeleteKnowledgeBase(kbId: string): Promise<TestResult> {
// 测试9<EFBFBD>?- 删除测试知识<E79FA5>?async function testDeleteKnowledgeBase(kbId: string): Promise<TestResult> {
const startTime = Date.now();
try {
const response = await axios.delete(`${BASE_URL}/api/v1/pkb/knowledge/knowledge-bases/${kbId}`);
@@ -305,14 +305,14 @@ let testKbId: string | null = null;
if (response.data.success) {
return {
name: '<EFBFBD>𣳇膄<EFBFBD><EFBFBD>摨橒<EFBFBD>v2?,
name: '删除知识库(v2<EFBFBD>?,
status: 'pass',
message: '',
duration,
};
} else {
return {
name: '<EFBFBD>𣳇<EFBFBD><EFBFBD><EFBFBD>v2嚗?,
name: 'v2<EFBFBD>?,
status: 'fail',
message: '删除失败',
duration,
@@ -320,7 +320,7 @@ let testKbId: string | null = null;
}
} catch (error: any) {
return {
name: '<EFBFBD>𣳇膄<EFBFBD><EFBFBD>摨橒<EFBFBD>v2?,
name: '删除知识库(v2<EFBFBD>?,
status: 'fail',
message: error.response?.data?.message || error.message,
duration: Date.now() - startTime,
@@ -328,11 +328,11 @@ let testKbId: string | null = null;
}
}
// 銝餅<EFBFBD>霂訫遆<EFBFBD>?async function runTests() {
console.log('<EFBFBD><EFBFBD> <EFBFBD>KB API<EFBFBD>𢆡<EFBFBD>𡝗<EFBFBD>?..\n');
// 主测试函<EFBFBD>?async function runTests() {
console.log('🚀 PKB API自动化测<EFBFBD>?..\n');
console.log('='.repeat(80));
// 瘚贝<EFBFBD>1嚗𡁜<EFBFBD>摨瑟<EFBFBD><EFBFBD>? console.log('\n<EFBFBD><EFBFBD> <EFBFBD>1𡁜<EFBFBD><EFBFBD><EFBFBD>?);
// 测试1健康检<EFBFBD>? console.log('\n📋 1<EFBFBD>?);
console.log('-'.repeat(80));
results.push(await testHealthCheck());
printResult(results[results.length - 1]);
@@ -350,7 +350,7 @@ let testKbId: string | null = null;
printResult(results[results.length - 1]);
if (!testKbId) {
console.log('\n<>?<EFBFBD><EFBFBD><EFBFBD><EFBFBD>瘚贝<EFBFBD><EFBFBD><EFBFBD>摨𨧻D嚗<EFBFBD><EFBFBD>蝏剜<EFBFBD>霂閗歲餈?);
console.log('\n<>?无法获取测试知识库ID后续测试跳<EFBFBD>?);
return;
}
@@ -366,22 +366,22 @@ let testKbId: string | null = null;
results.push(await testUpdateKnowledgeBase(testKbId));
printResult(results[results.length - 1]);
// 瘚贝<EFBFBD>6嚗朞繮<EFBFBD>𣇉<EFBFBD>霈∩縑<EFBFBD>? console.log('\n<EFBFBD><EFBFBD> <EFBFBD>6𡁶<EFBFBD><EFBFBD>');
// 测试6获取统计信<EFBFBD>? console.log('\n📋 6');
console.log('-'.repeat(80));
results.push(await testGetKnowledgeBaseStats(testKbId));
printResult(results[results.length - 1]);
// 瘚贝<EFBFBD>7嚗鑹AG璉<EFBFBD>? console.log('\n<EFBFBD><EFBFBD> <EFBFBD>7AG<EFBFBD>?);
// 测试7RAG检<EFBFBD>? console.log('\n📋 7RAG<EFBFBD>?);
console.log('-'.repeat(80));
results.push(await testSearchKnowledgeBase(testKbId));
printResult(results[results.length - 1]);
// 瘚贝<EFBFBD>8嚗朞器<EFBFBD>峕辺隞? console.log('\n<EFBFBD><EFBFBD> <20>嗆挾8嚗朞器<E69C9E>峕辺隞嗆<E99A9E>?);
// 测试8边界条<EFBFBD>? console.log('\n📋 阶段8边界条件测<E4BBB6>?);
console.log('-'.repeat(80));
results.push(await testNotFoundKnowledgeBase());
printResult(results[results.length - 1]);
// 瘚贝<EFBFBD>9嚗𡁏<EFBFBD><EFBFBD>? console.log('\n<EFBFBD><EFBFBD> <20>嗆挾9嚗𡁏<E59A97><F0A1818F><EFBFBD><EFBFBD>霂閙㺭<EFBFBD>?);
// 测试9<EFBFBD>? console.log('\n📋 阶段9清理测试数<EFBFBD>?);
console.log('-'.repeat(80));
results.push(await testDeleteKnowledgeBase(testKbId));
printResult(results[results.length - 1]);
@@ -397,13 +397,13 @@ let testKbId: string | null = null;
const totalDuration = results.reduce((sum, r) => sum + (r.duration || 0), 0);
console.log(`\n总计: ${results.length}个测试`);
console.log(`<EFBFBD>?<EFBFBD><EFBFBD>: ${passCount});
console.log(`<EFBFBD>?憭梯揖: ${failCount}銝注);
console.log(`<EFBFBD>?通过: ${passCount}`);
console.log(`<EFBFBD>?失败: ${failCount}`);
console.log(`⏭️ 跳过: ${skipCount}`);
console.log(`⏱️ 总耗时: ${totalDuration}ms`);
if (failCount === 0) {
console.log('\n<EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>霂閖<E99C82><EFBFBD>?);
console.log('\n🎉 所有测试通过<E9809A>?);
} else {
console.log('\n ');
}
@@ -411,7 +411,7 @@ let testKbId: string | null = null;
// 执行测试
runTests().catch(error => {
console.error('<EFBFBD>?<EFBFBD><EFBFBD><EFBFBD>:', error);
console.error('<EFBFBD>?:', error);
process.exit(1);
});

View File

@@ -83,3 +83,5 @@ testAPI().catch(console.error);

View File

@@ -302,3 +302,5 @@ verifySchemas()

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 {}

View File

@@ -78,6 +78,8 @@ Write-Host "✅ 完成!" -ForegroundColor Green

View File

@@ -6,3 +6,5 @@ SELECT schema_name FROM information_schema.schemata WHERE schema_name NOT IN ('p

View File

@@ -1,16 +1,16 @@
###
# PKB模块迁移 - API测试脚本
# 娴嬭瘯v1鍜寁2璺<EFBFBD>敱鐨勫姛鑳藉畬鏁存€у拰涓€鑷存€?
# 测试v1和v2路由的功能完整性和一致<EFBFBD>?
###
@baseUrl = http://localhost:3000
@userId = user-mock-001
### ============================================
### 闃舵<EFBFBD>3.1: 鍋ュ悍妫€鏌?
### 阶段3.1: 健康检<EFBFBD>?
### ============================================
### 1. PKB v2鍋ュ悍妫€鏌?
### 1. PKB v2健康检<EFBFBD>?
GET {{baseUrl}}/api/v1/pkb/health
Accept: application/json
@@ -18,15 +18,15 @@ Accept: application/json
### 阶段3.2: 知识库CRUD测试
### ============================================
### 2. 鑾峰彇鐭ヨ瘑搴撳垪琛<EFBFBD>v1?
### 2. 获取知识库列表(v1<EFBFBD>?
GET {{baseUrl}}/api/v1/knowledge-bases
Accept: application/json
### 3. 鑾峰彇鐭ヨ瘑搴撳垪琛<EFBFBD>v2?
### 3. 获取知识库列表(v2<EFBFBD>?
GET {{baseUrl}}/api/v1/pkb/knowledge/knowledge-bases
Accept: application/json
### 4. 鍒涘缓鐭ヨ瘑搴擄紙v2?
### 4. 创建知识库(v2<EFBFBD>?
POST {{baseUrl}}/api/v1/pkb/knowledge/knowledge-bases
Content-Type: application/json
@@ -35,7 +35,7 @@ Content-Type: application/json
"description": "这是一个通过v2 API创建的测试知识库"
}
### 5. 鍒涘缓鐭ヨ瘑搴擄紙v1 - 瀵规瘮锛?
### 5. 创建知识库(v1 - 对比<EFBFBD>?
POST {{baseUrl}}/api/v1/knowledge-bases
Content-Type: application/json
@@ -44,54 +44,54 @@ Content-Type: application/json
"description": "这是一个通过v1 API创建的测试知识库"
}
### 6. 鑾峰彇鐭ヨ瘑搴撹<EFBFBD>鎯咃紙v2?
### 6. 获取知识库详情(v2<EFBFBD>?
# 替换为实际的知识库ID
@kbId = f6ebe476-c50f-4222-83d2-c2525edc6054
GET {{baseUrl}}/api/v1/pkb/knowledge/knowledge-bases/{{kbId}}
Accept: application/json
### 7. 鑾峰彇鐭ヨ瘑搴撹<EFBFBD>鎯咃紙v1 - 瀵规瘮锛?
### 7. 获取知识库详情(v1 - 对比<EFBFBD>?
GET {{baseUrl}}/api/v1/knowledge-bases/{{kbId}}
Accept: application/json
### 8. 鏇存柊鐭ヨ瘑搴擄紙v2?
### 8. 更新知识库(v2<EFBFBD>?
PUT {{baseUrl}}/api/v1/pkb/knowledge/knowledge-bases/{{kbId}}
Content-Type: application/json
{
"name": "更新后的知识库名称v2",
"description": "閫氳繃v2 API鏇存柊鐨勬弿杩?
"description": "通过v2 API更新的描<EFBFBD>?
}
### 9. 鑾峰彇鐭ヨ瘑搴撶粺璁★紙v2?
### 9. 获取知识库统计(v2<EFBFBD>?
GET {{baseUrl}}/api/v1/pkb/knowledge/knowledge-bases/{{kbId}}/stats
Accept: application/json
### 10. 鑾峰彇鐭ヨ瘑搴撶粺璁★紙v1 - 瀵规瘮锛?
### 10. 获取知识库统计(v1 - 对比<EFBFBD>?
GET {{baseUrl}}/api/v1/knowledge-bases/{{kbId}}/stats
Accept: application/json
### ============================================
### 闃舵<EFBFBD>3.3: RAG妫€绱㈡祴璇?
### 阶段3.3: RAG检索测<EFBFBD>?
### ============================================
### 11. RAG妫€绱<EFBFBD>v2?
GET {{baseUrl}}/api/v1/pkb/knowledge/knowledge-bases/{{kbId}}/search?query=闃垮皵鍏规捣榛樼棁鐨勬不鐤楁柟娉?top_k=5
### 11. RAG检索(v2<EFBFBD>?
GET {{baseUrl}}/api/v1/pkb/knowledge/knowledge-bases/{{kbId}}/search?query=阿尔兹海默症的治疗方<EFBFBD>?top_k=5
Accept: application/json
### 12. RAG妫€绱<EFBFBD>v1 - 瀵规瘮锛?
GET {{baseUrl}}/api/v1/knowledge-bases/{{kbId}}/search?query=闃垮皵鍏规捣榛樼棁鐨勬不鐤楁柟娉?top_k=5
### 12. RAG检索(v1 - 对比<EFBFBD>?
GET {{baseUrl}}/api/v1/knowledge-bases/{{kbId}}/search?query=阿尔兹海默症的治疗方<EFBFBD>?top_k=5
Accept: application/json
### ============================================
### 阶段3.4: 文档选择(全文阅读模式)
### ============================================
### 13. 鑾峰彇鏂囨。閫夋嫨锛坴2锛?
### 13. 获取文档选择v2<EFBFBD>?
GET {{baseUrl}}/api/v1/pkb/knowledge/knowledge-bases/{{kbId}}/document-selection?max_files=7&max_tokens=750000
Accept: application/json
### 14. 鑾峰彇鏂囨。閫夋嫨锛坴1 - 瀵规瘮锛?
### 14. 获取文档选择v1 - 对比<E5AFB9>?
GET {{baseUrl}}/api/v1/knowledge-bases/{{kbId}}/document-selection?max_files=7&max_tokens=750000
Accept: application/json
@@ -103,21 +103,21 @@ Accept: application/json
GET {{baseUrl}}/api/v1/pkb/knowledge/knowledge-bases/{{kbId}}
Accept: application/json
### 16. 鑾峰彇鍗曚釜鏂囨。璇︽儏锛坴2锛?
### 16. 获取单个文档详情v2<EFBFBD>?
# 替换为实际的文档ID
@docId = your-document-id
GET {{baseUrl}}/api/v1/pkb/knowledge/documents/{{docId}}
Accept: application/json
### ============================================
### 闃舵<EFBFBD>3.6: 鎵瑰<EFBFBD>鐞嗗姛鑳芥祴璇?
### 阶段3.6: 批处理功能测<EFBFBD>?
### ============================================
### 17. 鑾峰彇鎵瑰<EFBFBD>鐞嗘ā鏉匡紙v2?
### 17. 获取批处理模板(v2<EFBFBD>?
GET {{baseUrl}}/api/v1/pkb/batch-tasks/batch/templates
Accept: application/json
### 18. 鍒涘缓鎵瑰<EFBFBD>鐞嗕换鍔★紙v2?
### 18. 创建批处理任务(v2<EFBFBD>?
POST {{baseUrl}}/api/v1/pkb/batch-tasks/batch/execute
Content-Type: application/json
@@ -134,15 +134,15 @@ Content-Type: application/json
### 阶段3.7: 边界条件测试
### ============================================
### 19. 娴嬭瘯涓嶅瓨鍦ㄧ殑鐭ヨ瘑搴擄紙v2?
### 19. 测试不存在的知识库(v2<EFBFBD>?
GET {{baseUrl}}/api/v1/pkb/knowledge/knowledge-bases/00000000-0000-0000-0000-000000000000
Accept: application/json
### 20. 娴嬭瘯鏃犳晥鐨勬煡璇㈠弬鏁帮紙v2?
### 20. 测试无效的查询参数(v2<EFBFBD>?
GET {{baseUrl}}/api/v1/pkb/knowledge/knowledge-bases/{{kbId}}/search?query=&top_k=0
Accept: application/json
### 21. 娴嬭瘯瓒呭ぇtop_k鍙傛暟锛坴2锛?
### 21. 测试超大top_k参数v2<EFBFBD>?
GET {{baseUrl}}/api/v1/pkb/knowledge/knowledge-bases/{{kbId}}/search?query=测试&top_k=1000
Accept: application/json
@@ -150,7 +150,7 @@ Accept: application/json
### 阶段3.8: 清理测试数据(可选)
### ============================================
### 22. 鍒犻櫎娴嬭瘯鐭ヨ瘑搴擄紙v2?
### 22. 删除测试知识库(v2<EFBFBD>?
# @testKbId = 从创建响应中获取的ID
DELETE {{baseUrl}}/api/v1/pkb/knowledge/knowledge-bases/{{testKbId}}
@@ -169,3 +169,5 @@ DELETE {{baseUrl}}/api/v1/pkb/knowledge/knowledge-bases/{{testKbId}}

View File

@@ -365,6 +365,8 @@ runAdvancedTests().catch(error => {

View File

@@ -431,6 +431,8 @@ runAllTests()

View File

@@ -389,6 +389,8 @@ runAllTests()

View File

@@ -26,3 +26,5 @@ main()

View File

@@ -24,3 +24,5 @@ main()

View File

@@ -36,3 +36,5 @@ main()

Some files were not shown because too many files have changed in this diff Show More