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:
@@ -45,6 +45,8 @@ Status: Day 1 complete (11/11 tasks), ready for Day 2
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -275,6 +275,8 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -224,3 +224,5 @@ https://iit.xunzhengyixue.com/api/v1/iit/health
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -153,3 +153,5 @@ https://iit.xunzhengyixue.com/api/v1/iit/health
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -54,3 +54,5 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -314,3 +314,5 @@ npx tsx src/modules/iit-manager/test-patient-wechat-url-verify.ts
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -176,3 +176,5 @@ npm run dev
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -53,3 +53,5 @@ main()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -47,3 +47,5 @@ main()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -42,3 +42,5 @@ main()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -74,3 +74,5 @@ main()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -37,3 +37,5 @@ main()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -78,3 +78,5 @@ main()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -25,3 +25,5 @@ main()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -113,3 +113,5 @@ main()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -84,3 +84,5 @@ main()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -70,3 +70,5 @@ main()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -112,3 +112,5 @@ main()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -23,3 +23,5 @@ ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -55,3 +55,5 @@ ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -70,6 +70,8 @@ WHERE table_schema = 'dc_schema'
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
8
backend/package-lock.json
generated
8
backend/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -108,6 +108,8 @@ ORDER BY ordinal_position;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -121,6 +121,8 @@ runMigration()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -55,6 +55,8 @@ COMMENT ON COLUMN "dc_schema"."dc_tool_c_sessions"."column_mapping" IS '列名
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -82,6 +82,8 @@ COMMENT ON COLUMN dc_schema.dc_tool_c_sessions.expires_at IS '过期时间(创
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -122,6 +122,8 @@ Write-Host ""
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -232,6 +232,8 @@ function extractCodeBlocks(obj, blocks = []) {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -32,3 +32,5 @@ CREATE TABLE IF NOT EXISTS platform_schema.job_common (
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -106,3 +106,5 @@ CREATE OR REPLACE FUNCTION platform_schema.delete_queue(queue_name text) RETURNS
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -251,6 +251,8 @@ checkDCTables();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -7,3 +7,5 @@ CREATE SCHEMA IF NOT EXISTS capability_schema;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -203,6 +203,8 @@ createAiHistoryTable()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -190,6 +190,8 @@ createToolCTable()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -187,6 +187,8 @@ createToolCTable()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -117,3 +117,5 @@ main()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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>蝝g<EFBFBD>撖寞<EFBFBD>v1<EFBFBD>綋2嚗?async function testSearchKnowledgeBase(kbId: string): Promise<TestResult> {
|
||||
// 测试7:RAG检索(对比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>蝝g<E89D9D>v1 vs v2嚗?,
|
||||
name: 'RAG检索(v1 vs v2<EFBFBD>?,
|
||||
status: 'pass',
|
||||
message: '检索成功,两个版本都返回了结果',
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
name: 'RAG璉<EFBFBD>蝝g<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>嗆挾7嚗鑹AG璉<EFBFBD>蝝?);
|
||||
// 测试7:RAG检<EFBFBD>? console.log('\n📋 阶段7:RAG检<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);
|
||||
});
|
||||
|
||||
|
||||
@@ -83,3 +83,5 @@ testAPI().catch(console.error);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -302,3 +302,5 @@ verifySchemas()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
|
||||
@@ -190,3 +190,5 @@ export const jwtService = new JWTService();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -319,6 +319,8 @@ export function getBatchItems<T>(
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -104,3 +104,5 @@ export function getAllFallbackCodes(): string[] {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -73,3 +73,5 @@ export interface VariableValidation {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -194,3 +194,5 @@ export function createOpenAIStreamAdapter(
|
||||
return new OpenAIStreamAdapter(reply, model);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -200,3 +200,5 @@ export async function streamChat(
|
||||
return service.streamGenerate(messages, callbacks);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -18,3 +18,5 @@ export type {
|
||||
|
||||
export { THINKING_TAGS } from './types';
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -93,3 +93,5 @@ export type SSEEventType =
|
||||
| 'error'
|
||||
| 'done';
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
525
backend/src/modules/admin/controllers/userController.ts
Normal file
525
backend/src/modules/admin/controllers/userController.ts
Normal 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 || '获取模块列表失败',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,3 +79,5 @@ export async function moduleRoutes(fastify: FastifyInstance) {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
117
backend/src/modules/admin/routes/userRoutes.ts
Normal file
117
backend/src/modules/admin/routes/userRoutes.ts
Normal 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;
|
||||
900
backend/src/modules/admin/services/userService.ts
Normal file
900
backend/src/modules/admin/services/userService.ts
Normal 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';
|
||||
}
|
||||
|
||||
@@ -109,3 +109,5 @@ export interface PaginatedResponse<T> {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
160
backend/src/modules/admin/types/user.types.ts
Normal file
160
backend/src/modules/admin/types/user.types.ts
Normal 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: '普通用户',
|
||||
};
|
||||
|
||||
@@ -231,3 +231,5 @@ async function matchIntent(query: string): Promise<{
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -14,3 +14,5 @@ import aiaRoutes from './routes/index.js';
|
||||
|
||||
export { aiaRoutes };
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -111,3 +111,5 @@ function estimateTokens(text: string): number {
|
||||
return Math.ceil(chineseChars / 1.5 + otherChars / 4);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -199,3 +199,5 @@ export interface PaginatedResponse<T> {
|
||||
pagination: Pagination;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -355,6 +355,8 @@ runTests().catch((error) => {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -296,6 +296,8 @@ runTest()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -334,6 +334,8 @@ Content-Type: application/json
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -270,6 +270,8 @@ export const conflictDetectionService = new ConflictDetectionService();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -220,6 +220,8 @@ curl -X POST http://localhost:3000/api/v1/dc/tool-c/test/execute \
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -274,6 +274,8 @@ export const streamAIController = new StreamAIController();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -184,5 +184,7 @@ logger.info('[SessionMemory] 会话记忆管理器已启动', {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -120,3 +120,5 @@ checkTableStructure();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -105,5 +105,7 @@ checkProjectConfig().catch(console.error);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -88,4 +88,6 @@ main();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -546,3 +546,5 @@ URL: https://iit.xunzhengyixue.com/api/v1/iit/patient-wechat/callback
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -181,3 +181,5 @@ console.log('');
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -498,3 +498,5 @@ export const patientWechatService = new PatientWechatService();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -143,3 +143,5 @@ testDifyIntegration().catch(error => {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -169,6 +169,8 @@ testIitDatabase()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -158,3 +158,5 @@ if (hasError) {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -184,3 +184,5 @@ async function testUrlVerification() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -264,4 +264,6 @@ main().catch((error) => {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -149,3 +149,5 @@ Write-Host ""
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -239,6 +239,8 @@ export interface CachedProtocolRules {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -55,3 +55,5 @@ export default async function healthRoutes(fastify: FastifyInstance) {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -32,3 +32,5 @@ export * from './services/utils.js';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -123,3 +123,5 @@ export function validateAgentSelection(agents: string[]): void {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -420,6 +420,8 @@ SET session_replication_role = 'origin';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -122,6 +122,8 @@ WHERE key = 'verify_test';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -265,6 +265,8 @@ verifyDatabase()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
2
backend/src/types/global.d.ts
vendored
2
backend/src/types/global.d.ts
vendored
@@ -55,6 +55,8 @@ export {}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -78,6 +78,8 @@ Write-Host "✅ 完成!" -ForegroundColor Green
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -6,3 +6,5 @@ SELECT schema_name FROM information_schema.schemata WHERE schema_name NOT IN ('p
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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}}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -365,6 +365,8 @@ runAdvancedTests().catch(error => {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -431,6 +431,8 @@ runAllTests()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -389,6 +389,8 @@ runAllTests()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -26,3 +26,5 @@ main()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -24,3 +24,5 @@ main()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -36,3 +36,5 @@ main()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user