feat(iit): Implement real-time quality control system

Summary:

- Add 4 new database tables: iit_field_metadata, iit_qc_logs, iit_record_summary, iit_qc_project_stats

- Implement pg-boss debounce mechanism in WebhookController

- Refactor QC Worker for dual output: QC logs + record summary

- Enhance HardRuleEngine to support form-based rule filtering

- Create QcService for QC data queries

- Optimize ChatService with new intents: query_enrollment, query_qc_status

- Add admin batch operations: one-click full QC + one-click full summary

- Create IIT Admin management module: project config, QC rules, user mapping

Status: Code complete, pending end-to-end testing
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-02-07 21:56:11 +08:00
parent 0c590854b5
commit 5db4a7064c
74 changed files with 13383 additions and 2129 deletions

View File

@@ -0,0 +1,354 @@
/**
* IIT 批量操作 Controller
*
* 功能:
* - 一键全量质控
* - 一键全量数据汇总
*
* 用途:
* - 运营管理端手动触发
* - 未来可作为 AI 工具暴露
*/
import { FastifyRequest, FastifyReply } from 'fastify';
import { PrismaClient } from '@prisma/client';
import { logger } from '../../../common/logging/index.js';
import { RedcapAdapter } from '../../iit-manager/adapters/RedcapAdapter.js';
import { createHardRuleEngine } from '../../iit-manager/engines/HardRuleEngine.js';
const prisma = new PrismaClient();
interface BatchRequest {
Params: { projectId: string };
}
export class IitBatchController {
/**
* 一键全量质控
*
* POST /api/v1/admin/iit-projects/:projectId/batch-qc
*
* 功能:
* 1. 获取 REDCap 中所有记录
* 2. 对每条记录执行质控
* 3. 存储质控日志到 iit_qc_logs
* 4. 更新项目统计到 iit_qc_project_stats
*/
async batchQualityCheck(
request: FastifyRequest<BatchRequest>,
reply: FastifyReply
) {
const { projectId } = request.params;
const startTime = Date.now();
try {
logger.info('🔄 开始全量质控', { projectId });
// 1. 获取项目配置
const project = await prisma.iitProject.findUnique({
where: { id: projectId }
});
if (!project) {
return reply.status(404).send({ error: '项目不存在' });
}
// 2. 从 REDCap 获取所有记录
const adapter = new RedcapAdapter(project.redcapUrl, project.redcapApiToken);
const allRecords = await adapter.exportRecords({});
if (!allRecords || allRecords.length === 0) {
return reply.send({
success: true,
message: '项目暂无记录',
stats: { totalRecords: 0 }
});
}
// 3. 按 record_id 分组
const recordMap = new Map<string, any>();
for (const record of allRecords) {
const recordId = record.record_id || record.id;
if (recordId) {
// 合并同一记录的多个事件数据
const existing = recordMap.get(recordId) || {};
recordMap.set(recordId, { ...existing, ...record });
}
}
// 4. 执行质控
const engine = await createHardRuleEngine(projectId);
const ruleVersion = new Date().toISOString().split('T')[0];
let passCount = 0;
let failCount = 0;
let warningCount = 0;
for (const [recordId, recordData] of recordMap.entries()) {
const qcResult = engine.execute(recordId, recordData);
// 存储质控日志
const issues = [
...qcResult.errors.map((e: any) => ({
field: e.field,
rule: e.ruleName,
level: 'RED',
message: e.message
})),
...qcResult.warnings.map((w: any) => ({
field: w.field,
rule: w.ruleName,
level: 'YELLOW',
message: w.message
}))
];
await prisma.iitQcLog.create({
data: {
projectId,
recordId,
qcType: 'holistic', // 全案质控
status: qcResult.overallStatus,
issues,
rulesEvaluated: qcResult.summary.totalRules,
rulesSkipped: 0,
rulesPassed: qcResult.summary.passed,
rulesFailed: qcResult.summary.failed,
ruleVersion,
triggeredBy: 'manual'
}
});
// 更新录入汇总表的质控状态
await prisma.iitRecordSummary.upsert({
where: {
projectId_recordId: { projectId, recordId }
},
create: {
projectId,
recordId,
lastUpdatedAt: new Date(),
latestQcStatus: qcResult.overallStatus,
latestQcAt: new Date(),
formStatus: {},
updateCount: 1
},
update: {
latestQcStatus: qcResult.overallStatus,
latestQcAt: new Date()
}
});
// 统计
if (qcResult.overallStatus === 'PASS') passCount++;
else if (qcResult.overallStatus === 'FAIL') failCount++;
else warningCount++;
}
// 5. 更新项目统计表
await prisma.iitQcProjectStats.upsert({
where: { projectId },
create: {
projectId,
totalRecords: recordMap.size,
passedRecords: passCount,
failedRecords: failCount,
warningRecords: warningCount
},
update: {
totalRecords: recordMap.size,
passedRecords: passCount,
failedRecords: failCount,
warningRecords: warningCount
}
});
const durationMs = Date.now() - startTime;
logger.info('✅ 全量质控完成', {
projectId,
totalRecords: recordMap.size,
passCount,
failCount,
warningCount,
durationMs
});
return reply.send({
success: true,
message: '全量质控完成',
stats: {
totalRecords: recordMap.size,
passed: passCount,
failed: failCount,
warnings: warningCount,
passRate: `${((passCount / recordMap.size) * 100).toFixed(1)}%`
},
durationMs
});
} catch (error: any) {
logger.error('❌ 全量质控失败', { projectId, error: error.message });
return reply.status(500).send({ error: `质控失败: ${error.message}` });
}
}
/**
* 一键全量数据汇总
*
* POST /api/v1/admin/iit-projects/:projectId/batch-summary
*
* 功能:
* 1. 获取 REDCap 中所有记录
* 2. 获取项目的所有表单instruments
* 3. 为每条记录生成/更新录入汇总
*/
async batchSummary(
request: FastifyRequest<BatchRequest>,
reply: FastifyReply
) {
const { projectId } = request.params;
const startTime = Date.now();
try {
logger.info('🔄 开始全量数据汇总', { projectId });
// 1. 获取项目配置
const project = await prisma.iitProject.findUnique({
where: { id: projectId }
});
if (!project) {
return reply.status(404).send({ error: '项目不存在' });
}
// 2. 从 REDCap 获取所有记录和表单信息
const adapter = new RedcapAdapter(project.redcapUrl, project.redcapApiToken);
const [allRecords, instruments] = await Promise.all([
adapter.exportRecords({}),
adapter.exportInstruments()
]);
if (!allRecords || allRecords.length === 0) {
return reply.send({
success: true,
message: '项目暂无记录',
stats: { totalRecords: 0 }
});
}
const totalForms = instruments?.length || 10;
// 3. 按 record_id 分组并计算表单完成状态
const recordMap = new Map<string, { data: any; forms: Set<string>; firstSeen: Date }>();
for (const record of allRecords) {
const recordId = record.record_id || record.id;
if (!recordId) continue;
const existing = recordMap.get(recordId);
if (existing) {
// 合并数据
existing.data = { ...existing.data, ...record };
// 记录表单
if (record.redcap_repeat_instrument) {
existing.forms.add(record.redcap_repeat_instrument);
}
} else {
recordMap.set(recordId, {
data: record,
forms: new Set(record.redcap_repeat_instrument ? [record.redcap_repeat_instrument] : []),
firstSeen: new Date()
});
}
}
// 4. 为每条记录更新汇总
let summaryCount = 0;
for (const [recordId, { data, forms, firstSeen }] of recordMap.entries()) {
// 计算表单完成状态(简化:有数据即认为完成)
const formStatus: Record<string, number> = {};
const completedForms = forms.size || 1; // 至少有1个表单有数据
for (const form of forms) {
formStatus[form] = 2; // 2 = 完成
}
const completionRate = Math.min(100, Math.round((completedForms / totalForms) * 100));
await prisma.iitRecordSummary.upsert({
where: {
projectId_recordId: { projectId, recordId }
},
create: {
projectId,
recordId,
enrolledAt: firstSeen,
lastUpdatedAt: new Date(),
formStatus,
totalForms,
completedForms,
completionRate,
updateCount: 1
},
update: {
lastUpdatedAt: new Date(),
formStatus,
totalForms,
completedForms,
completionRate,
updateCount: { increment: 1 }
}
});
summaryCount++;
}
// 5. 更新项目统计
const avgCompletionRate = await prisma.iitRecordSummary.aggregate({
where: { projectId },
_avg: { completionRate: true }
});
await prisma.iitQcProjectStats.upsert({
where: { projectId },
create: {
projectId,
totalRecords: recordMap.size,
avgCompletionRate: avgCompletionRate._avg.completionRate || 0
},
update: {
totalRecords: recordMap.size,
avgCompletionRate: avgCompletionRate._avg.completionRate || 0
}
});
const durationMs = Date.now() - startTime;
logger.info('✅ 全量数据汇总完成', {
projectId,
totalRecords: recordMap.size,
summaryCount,
durationMs
});
return reply.send({
success: true,
message: '全量数据汇总完成',
stats: {
totalRecords: recordMap.size,
summariesUpdated: summaryCount,
totalForms,
avgCompletionRate: `${(avgCompletionRate._avg.completionRate || 0).toFixed(1)}%`
},
durationMs
});
} catch (error: any) {
logger.error('❌ 全量数据汇总失败', { projectId, error: error.message });
return reply.status(500).send({ error: `汇总失败: ${error.message}` });
}
}
}
export const iitBatchController = new IitBatchController();

View File

@@ -0,0 +1,81 @@
/**
* IIT 批量操作路由
*
* API:
* - POST /api/v1/admin/iit-projects/:projectId/batch-qc 一键全量质控
* - POST /api/v1/admin/iit-projects/:projectId/batch-summary 一键全量数据汇总
*/
import { FastifyInstance } from 'fastify';
import { iitBatchController } from './iitBatchController.js';
export async function iitBatchRoutes(fastify: FastifyInstance) {
// 一键全量质控
fastify.post('/:projectId/batch-qc', {
schema: {
description: '一键全量质控 - 对项目中所有记录执行质控',
tags: ['IIT Admin - 批量操作'],
params: {
type: 'object',
properties: {
projectId: { type: 'string', description: 'IIT 项目 ID' }
},
required: ['projectId']
},
response: {
200: {
type: 'object',
properties: {
success: { type: 'boolean' },
message: { type: 'string' },
stats: {
type: 'object',
properties: {
totalRecords: { type: 'number' },
passed: { type: 'number' },
failed: { type: 'number' },
warnings: { type: 'number' },
passRate: { type: 'string' }
}
},
durationMs: { type: 'number' }
}
}
}
}
}, iitBatchController.batchQualityCheck.bind(iitBatchController));
// 一键全量数据汇总
fastify.post('/:projectId/batch-summary', {
schema: {
description: '一键全量数据汇总 - 从 REDCap 同步所有记录的录入状态',
tags: ['IIT Admin - 批量操作'],
params: {
type: 'object',
properties: {
projectId: { type: 'string', description: 'IIT 项目 ID' }
},
required: ['projectId']
},
response: {
200: {
type: 'object',
properties: {
success: { type: 'boolean' },
message: { type: 'string' },
stats: {
type: 'object',
properties: {
totalRecords: { type: 'number' },
summariesUpdated: { type: 'number' },
totalForms: { type: 'number' },
avgCompletionRate: { type: 'string' }
}
},
durationMs: { type: 'number' }
}
}
}
}
}, iitBatchController.batchSummary.bind(iitBatchController));
}

View File

@@ -0,0 +1,348 @@
/**
* IIT 项目管理控制器
*/
import { FastifyRequest, FastifyReply } from 'fastify';
import { getIitProjectService, CreateProjectInput, UpdateProjectInput } from './iitProjectService.js';
import { prisma } from '../../../config/database.js';
import { logger } from '../../../common/logging/index.js';
// ==================== 类型定义 ====================
interface ProjectIdParams {
id: string;
}
interface ListProjectsQuery {
status?: string;
search?: string;
}
interface TestConnectionBody {
redcapUrl: string;
redcapApiToken: string;
}
interface LinkKbBody {
knowledgeBaseId: string;
}
// ==================== 控制器函数 ====================
/**
* 获取项目列表
*/
export async function listProjects(
request: FastifyRequest<{ Querystring: ListProjectsQuery }>,
reply: FastifyReply
) {
try {
const { status, search } = request.query;
const service = getIitProjectService(prisma);
const projects = await service.listProjects({ status, search });
return reply.send({
success: true,
data: projects,
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error('获取 IIT 项目列表失败', { error: message });
return reply.status(500).send({
success: false,
error: message,
});
}
}
/**
* 获取项目详情
*/
export async function getProject(
request: FastifyRequest<{ Params: ProjectIdParams }>,
reply: FastifyReply
) {
try {
const { id } = request.params;
const service = getIitProjectService(prisma);
const project = await service.getProject(id);
if (!project) {
return reply.status(404).send({
success: false,
error: '项目不存在',
});
}
return reply.send({
success: true,
data: project,
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error('获取 IIT 项目详情失败', { error: message });
return reply.status(500).send({
success: false,
error: message,
});
}
}
/**
* 创建项目
*/
export async function createProject(
request: FastifyRequest<{ Body: CreateProjectInput }>,
reply: FastifyReply
) {
try {
const input = request.body;
// 验证必填字段
if (!input.name) {
return reply.status(400).send({
success: false,
error: '项目名称为必填项',
});
}
if (!input.redcapUrl || !input.redcapProjectId || !input.redcapApiToken) {
return reply.status(400).send({
success: false,
error: 'REDCap 配置信息为必填项URL、项目ID、API Token',
});
}
const service = getIitProjectService(prisma);
const project = await service.createProject(input);
return reply.status(201).send({
success: true,
data: project,
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error('创建 IIT 项目失败', { error: message });
if (message.includes('连接测试失败')) {
return reply.status(400).send({
success: false,
error: message,
});
}
return reply.status(500).send({
success: false,
error: message,
});
}
}
/**
* 更新项目
*/
export async function updateProject(
request: FastifyRequest<{ Params: ProjectIdParams; Body: UpdateProjectInput }>,
reply: FastifyReply
) {
try {
const { id } = request.params;
const input = request.body;
const service = getIitProjectService(prisma);
const project = await service.updateProject(id, input);
return reply.send({
success: true,
data: project,
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error('更新 IIT 项目失败', { error: message });
if (message.includes('连接测试失败')) {
return reply.status(400).send({
success: false,
error: message,
});
}
return reply.status(500).send({
success: false,
error: message,
});
}
}
/**
* 删除项目
*/
export async function deleteProject(
request: FastifyRequest<{ Params: ProjectIdParams }>,
reply: FastifyReply
) {
try {
const { id } = request.params;
const service = getIitProjectService(prisma);
await service.deleteProject(id);
return reply.send({
success: true,
message: '删除成功',
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error('删除 IIT 项目失败', { error: message });
return reply.status(500).send({
success: false,
error: message,
});
}
}
/**
* 测试 REDCap 连接(新配置)
*/
export async function testConnection(
request: FastifyRequest<{ Body: TestConnectionBody }>,
reply: FastifyReply
) {
try {
const { redcapUrl, redcapApiToken } = request.body;
if (!redcapUrl || !redcapApiToken) {
return reply.status(400).send({
success: false,
error: '请提供 REDCap URL 和 API Token',
});
}
const service = getIitProjectService(prisma);
const result = await service.testConnection(redcapUrl, redcapApiToken);
return reply.send({
success: result.success,
data: result,
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error('测试 REDCap 连接失败', { error: message });
return reply.status(500).send({
success: false,
error: message,
});
}
}
/**
* 测试项目的 REDCap 连接
*/
export async function testProjectConnection(
request: FastifyRequest<{ Params: ProjectIdParams }>,
reply: FastifyReply
) {
try {
const { id } = request.params;
const service = getIitProjectService(prisma);
const result = await service.testProjectConnection(id);
return reply.send({
success: result.success,
data: result,
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error('测试项目连接失败', { error: message });
return reply.status(500).send({
success: false,
error: message,
});
}
}
/**
* 同步 REDCap 元数据
*/
export async function syncMetadata(
request: FastifyRequest<{ Params: ProjectIdParams }>,
reply: FastifyReply
) {
try {
const { id } = request.params;
const service = getIitProjectService(prisma);
const result = await service.syncMetadata(id);
return reply.send({
success: true,
data: result,
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error('同步元数据失败', { error: message });
return reply.status(500).send({
success: false,
error: message,
});
}
}
/**
* 关联知识库
*/
export async function linkKnowledgeBase(
request: FastifyRequest<{ Params: ProjectIdParams; Body: LinkKbBody }>,
reply: FastifyReply
) {
try {
const { id } = request.params;
const { knowledgeBaseId } = request.body;
if (!knowledgeBaseId) {
return reply.status(400).send({
success: false,
error: '请提供知识库 ID',
});
}
const service = getIitProjectService(prisma);
await service.linkKnowledgeBase(id, knowledgeBaseId);
return reply.send({
success: true,
message: '关联成功',
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error('关联知识库失败', { error: message });
return reply.status(500).send({
success: false,
error: message,
});
}
}
/**
* 解除知识库关联
*/
export async function unlinkKnowledgeBase(
request: FastifyRequest<{ Params: ProjectIdParams }>,
reply: FastifyReply
) {
try {
const { id } = request.params;
const service = getIitProjectService(prisma);
await service.unlinkKnowledgeBase(id);
return reply.send({
success: true,
message: '解除关联成功',
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error('解除知识库关联失败', { error: message });
return reply.status(500).send({
success: false,
error: message,
});
}
}

View File

@@ -0,0 +1,44 @@
/**
* IIT 项目管理路由
*/
import { FastifyInstance } from 'fastify';
import * as controller from './iitProjectController.js';
export async function iitProjectRoutes(fastify: FastifyInstance) {
// ==================== 项目 CRUD ====================
// 获取项目列表
fastify.get('/', controller.listProjects);
// 获取项目详情
fastify.get('/:id', controller.getProject);
// 创建项目
fastify.post('/', controller.createProject);
// 更新项目
fastify.put('/:id', controller.updateProject);
// 删除项目
fastify.delete('/:id', controller.deleteProject);
// ==================== REDCap 连接 ====================
// 测试 REDCap 连接(新配置,不需要项目 ID
fastify.post('/test-connection', controller.testConnection);
// 测试指定项目的 REDCap 连接
fastify.post('/:id/test-connection', controller.testProjectConnection);
// 同步 REDCap 元数据
fastify.post('/:id/sync-metadata', controller.syncMetadata);
// ==================== 知识库关联 ====================
// 关联知识库
fastify.post('/:id/knowledge-base', controller.linkKnowledgeBase);
// 解除知识库关联
fastify.delete('/:id/knowledge-base', controller.unlinkKnowledgeBase);
}

View File

@@ -0,0 +1,372 @@
/**
* IIT 项目管理服务
* 提供 IIT 项目的 CRUD 操作及相关功能
*/
import { PrismaClient, Prisma } from '@prisma/client';
import { logger } from '../../../common/logging/index.js';
import { RedcapAdapter } from '../../iit-manager/adapters/RedcapAdapter.js';
// ==================== 类型定义 ====================
export interface CreateProjectInput {
name: string;
description?: string;
redcapUrl: string;
redcapProjectId: string;
redcapApiToken: string;
fieldMappings?: Record<string, unknown>;
knowledgeBaseId?: string;
}
export interface UpdateProjectInput {
name?: string;
description?: string;
redcapUrl?: string;
redcapProjectId?: string;
redcapApiToken?: string;
fieldMappings?: Record<string, unknown>;
knowledgeBaseId?: string;
status?: string;
}
export interface TestConnectionResult {
success: boolean;
version?: string;
projectTitle?: string;
recordCount?: number;
error?: string;
}
export interface ProjectListFilters {
status?: string;
search?: string;
}
// ==================== 服务实现 ====================
export class IitProjectService {
constructor(private prisma: PrismaClient) {}
/**
* 获取项目列表
*/
async listProjects(filters?: ProjectListFilters) {
const where: Prisma.IitProjectWhereInput = {
deletedAt: null,
};
if (filters?.status) {
where.status = filters.status;
}
if (filters?.search) {
where.OR = [
{ name: { contains: filters.search, mode: 'insensitive' } },
{ description: { contains: filters.search, mode: 'insensitive' } },
];
}
const projects = await this.prisma.iitProject.findMany({
where,
orderBy: { createdAt: 'desc' },
select: {
id: true,
name: true,
description: true,
redcapProjectId: true,
redcapUrl: true,
knowledgeBaseId: true,
status: true,
lastSyncAt: true,
createdAt: true,
updatedAt: true,
_count: {
select: {
userMappings: true,
},
},
},
});
return projects.map((p) => ({
...p,
userMappingCount: p._count.userMappings,
}));
}
/**
* 获取项目详情
*/
async getProject(id: string) {
const project = await this.prisma.iitProject.findFirst({
where: { id, deletedAt: null },
include: {
userMappings: {
select: {
id: true,
systemUserId: true,
redcapUsername: true,
wecomUserId: true,
role: true,
createdAt: true,
},
},
},
});
if (!project) {
return null;
}
// 获取关联的质控规则数量
const skillCount = await this.prisma.iitSkill.count({
where: { projectId: id },
});
// 获取关联的知识库信息
let knowledgeBase = null;
if (project.knowledgeBaseId) {
knowledgeBase = await this.prisma.ekbKnowledgeBase.findUnique({
where: { id: project.knowledgeBaseId },
select: {
id: true,
name: true,
_count: { select: { documents: true } },
},
});
}
return {
...project,
skillCount,
knowledgeBase: knowledgeBase
? {
id: knowledgeBase.id,
name: knowledgeBase.name,
documentCount: knowledgeBase._count.documents,
}
: null,
// 返回完整 token 供编辑使用
redcapApiToken: project.redcapApiToken,
};
}
/**
* 创建项目
*/
async createProject(input: CreateProjectInput) {
// 验证 REDCap 连接
const testResult = await this.testConnection(
input.redcapUrl,
input.redcapApiToken
);
if (!testResult.success) {
throw new Error(`REDCap 连接测试失败: ${testResult.error}`);
}
const project = await this.prisma.iitProject.create({
data: {
name: input.name,
description: input.description,
redcapUrl: input.redcapUrl,
redcapProjectId: input.redcapProjectId,
redcapApiToken: input.redcapApiToken,
fieldMappings: input.fieldMappings || {},
knowledgeBaseId: input.knowledgeBaseId,
status: 'active',
},
});
logger.info('创建 IIT 项目成功', { projectId: project.id, name: project.name });
return project;
}
/**
* 更新项目
*/
async updateProject(id: string, input: UpdateProjectInput) {
// 如果更新了 REDCap 配置,需要验证连接
if (input.redcapUrl && input.redcapApiToken) {
const testResult = await this.testConnection(
input.redcapUrl,
input.redcapApiToken
);
if (!testResult.success) {
throw new Error(`REDCap 连接测试失败: ${testResult.error}`);
}
}
const project = await this.prisma.iitProject.update({
where: { id },
data: {
name: input.name,
description: input.description,
redcapUrl: input.redcapUrl,
redcapProjectId: input.redcapProjectId,
redcapApiToken: input.redcapApiToken,
fieldMappings: input.fieldMappings,
knowledgeBaseId: input.knowledgeBaseId,
status: input.status,
updatedAt: new Date(),
},
});
logger.info('更新 IIT 项目成功', { projectId: project.id });
return project;
}
/**
* 删除项目(软删除)
*/
async deleteProject(id: string) {
await this.prisma.iitProject.update({
where: { id },
data: {
deletedAt: new Date(),
status: 'deleted',
},
});
logger.info('删除 IIT 项目成功', { projectId: id });
}
/**
* 测试 REDCap 连接
*/
async testConnection(
redcapUrl: string,
redcapApiToken: string
): Promise<TestConnectionResult> {
try {
const adapter = new RedcapAdapter(redcapUrl, redcapApiToken);
// 测试连接
const connected = await adapter.testConnection();
if (!connected) {
return {
success: false,
error: 'REDCap API 连接失败',
};
}
// 获取记录数量
const recordCount = await adapter.getRecordCount();
return {
success: true,
version: 'REDCap API',
recordCount,
};
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error('REDCap 连接测试失败', { error: message });
return {
success: false,
error: message,
};
}
}
/**
* 测试指定项目的 REDCap 连接
*/
async testProjectConnection(projectId: string): Promise<TestConnectionResult> {
const project = await this.prisma.iitProject.findFirst({
where: { id: projectId, deletedAt: null },
});
if (!project) {
return {
success: false,
error: '项目不存在',
};
}
return this.testConnection(project.redcapUrl, project.redcapApiToken);
}
/**
* 同步 REDCap 元数据
*/
async syncMetadata(projectId: string) {
const project = await this.prisma.iitProject.findFirst({
where: { id: projectId, deletedAt: null },
});
if (!project) {
throw new Error('项目不存在');
}
try {
const adapter = new RedcapAdapter(project.redcapUrl, project.redcapApiToken);
const metadata = await adapter.exportMetadata();
// 更新最后同步时间
await this.prisma.iitProject.update({
where: { id: projectId },
data: { lastSyncAt: new Date() },
});
logger.info('同步 REDCap 元数据成功', {
projectId,
fieldCount: metadata.length,
});
return {
success: true,
fieldCount: metadata.length,
metadata,
};
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error('同步 REDCap 元数据失败', { projectId, error: message });
throw new Error(`同步失败: ${message}`);
}
}
/**
* 关联知识库
*/
async linkKnowledgeBase(projectId: string, knowledgeBaseId: string) {
// 验证知识库存在
const kb = await this.prisma.ekbKnowledgeBase.findUnique({
where: { id: knowledgeBaseId },
});
if (!kb) {
throw new Error('知识库不存在');
}
await this.prisma.iitProject.update({
where: { id: projectId },
data: { knowledgeBaseId },
});
logger.info('关联知识库成功', { projectId, knowledgeBaseId });
}
/**
* 解除知识库关联
*/
async unlinkKnowledgeBase(projectId: string) {
await this.prisma.iitProject.update({
where: { id: projectId },
data: { knowledgeBaseId: null },
});
logger.info('解除知识库关联成功', { projectId });
}
}
// 单例工厂函数
let serviceInstance: IitProjectService | null = null;
export function getIitProjectService(prisma: PrismaClient): IitProjectService {
if (!serviceInstance) {
serviceInstance = new IitProjectService(prisma);
}
return serviceInstance;
}

View File

@@ -0,0 +1,288 @@
/**
* IIT 质控规则管理控制器
*/
import { FastifyRequest, FastifyReply } from 'fastify';
import { getIitQcRuleService, CreateRuleInput, UpdateRuleInput, TestRuleInput } from './iitQcRuleService.js';
import { prisma } from '../../../config/database.js';
import { logger } from '../../../common/logging/index.js';
// ==================== 类型定义 ====================
interface ProjectIdParams {
projectId: string;
}
interface RuleIdParams {
projectId: string;
ruleId: string;
}
interface ImportRulesBody {
rules: CreateRuleInput[];
}
// ==================== 控制器函数 ====================
/**
* 获取项目的所有质控规则
*/
export async function listRules(
request: FastifyRequest<{ Params: ProjectIdParams }>,
reply: FastifyReply
) {
try {
const { projectId } = request.params;
const service = getIitQcRuleService(prisma);
const rules = await service.listRules(projectId);
return reply.send({
success: true,
data: rules,
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error('获取质控规则列表失败', { error: message });
return reply.status(500).send({
success: false,
error: message,
});
}
}
/**
* 获取单条规则
*/
export async function getRule(
request: FastifyRequest<{ Params: RuleIdParams }>,
reply: FastifyReply
) {
try {
const { projectId, ruleId } = request.params;
const service = getIitQcRuleService(prisma);
const rule = await service.getRule(projectId, ruleId);
if (!rule) {
return reply.status(404).send({
success: false,
error: '规则不存在',
});
}
return reply.send({
success: true,
data: rule,
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error('获取质控规则失败', { error: message });
return reply.status(500).send({
success: false,
error: message,
});
}
}
/**
* 添加规则
*/
export async function addRule(
request: FastifyRequest<{ Params: ProjectIdParams; Body: CreateRuleInput }>,
reply: FastifyReply
) {
try {
const { projectId } = request.params;
const input = request.body;
// 验证必填字段
if (!input.name || !input.field || !input.logic || !input.message || !input.severity || !input.category) {
return reply.status(400).send({
success: false,
error: '缺少必填字段name, field, logic, message, severity, category',
});
}
const service = getIitQcRuleService(prisma);
const rule = await service.addRule(projectId, input);
return reply.status(201).send({
success: true,
data: rule,
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error('添加质控规则失败', { error: message });
return reply.status(500).send({
success: false,
error: message,
});
}
}
/**
* 更新规则
*/
export async function updateRule(
request: FastifyRequest<{ Params: RuleIdParams; Body: UpdateRuleInput }>,
reply: FastifyReply
) {
try {
const { projectId, ruleId } = request.params;
const input = request.body;
const service = getIitQcRuleService(prisma);
const rule = await service.updateRule(projectId, ruleId, input);
return reply.send({
success: true,
data: rule,
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error('更新质控规则失败', { error: message });
if (message.includes('规则不存在')) {
return reply.status(404).send({
success: false,
error: message,
});
}
return reply.status(500).send({
success: false,
error: message,
});
}
}
/**
* 删除规则
*/
export async function deleteRule(
request: FastifyRequest<{ Params: RuleIdParams }>,
reply: FastifyReply
) {
try {
const { projectId, ruleId } = request.params;
const service = getIitQcRuleService(prisma);
await service.deleteRule(projectId, ruleId);
return reply.send({
success: true,
message: '删除成功',
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error('删除质控规则失败', { error: message });
if (message.includes('规则不存在')) {
return reply.status(404).send({
success: false,
error: message,
});
}
return reply.status(500).send({
success: false,
error: message,
});
}
}
/**
* 批量导入规则
*/
export async function importRules(
request: FastifyRequest<{ Params: ProjectIdParams; Body: ImportRulesBody }>,
reply: FastifyReply
) {
try {
const { projectId } = request.params;
const { rules } = request.body;
if (!rules || !Array.isArray(rules) || rules.length === 0) {
return reply.status(400).send({
success: false,
error: '请提供规则数组',
});
}
const service = getIitQcRuleService(prisma);
const importedRules = await service.importRules(projectId, rules);
return reply.status(201).send({
success: true,
data: {
count: importedRules.length,
rules: importedRules,
},
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error('批量导入质控规则失败', { error: message });
return reply.status(500).send({
success: false,
error: message,
});
}
}
/**
* 测试规则逻辑
*/
export async function testRule(
request: FastifyRequest<{ Body: TestRuleInput }>,
reply: FastifyReply
) {
try {
const input = request.body;
if (!input.logic || !input.testData) {
return reply.status(400).send({
success: false,
error: '请提供 logic 和 testData',
});
}
const service = getIitQcRuleService(prisma);
const result = await service.testRule(input);
return reply.send({
success: true,
data: result,
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error('测试规则失败', { error: message });
return reply.status(400).send({
success: false,
error: message,
});
}
}
/**
* 获取规则统计
*/
export async function getRuleStats(
request: FastifyRequest<{ Params: ProjectIdParams }>,
reply: FastifyReply
) {
try {
const { projectId } = request.params;
const service = getIitQcRuleService(prisma);
const stats = await service.getRuleStats(projectId);
return reply.send({
success: true,
data: stats,
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error('获取规则统计失败', { error: message });
return reply.status(500).send({
success: false,
error: message,
});
}
}

View File

@@ -0,0 +1,32 @@
/**
* IIT 质控规则管理路由
*/
import { FastifyInstance } from 'fastify';
import * as controller from './iitQcRuleController.js';
export async function iitQcRuleRoutes(fastify: FastifyInstance) {
// 获取项目的所有质控规则
fastify.get('/:projectId/rules', controller.listRules);
// 获取规则统计
fastify.get('/:projectId/rules/stats', controller.getRuleStats);
// 获取单条规则
fastify.get('/:projectId/rules/:ruleId', controller.getRule);
// 添加规则
fastify.post('/:projectId/rules', controller.addRule);
// 更新规则
fastify.put('/:projectId/rules/:ruleId', controller.updateRule);
// 删除规则
fastify.delete('/:projectId/rules/:ruleId', controller.deleteRule);
// 批量导入规则
fastify.post('/:projectId/rules/import', controller.importRules);
// 测试规则逻辑(不需要项目 ID
fastify.post('/rules/test', controller.testRule);
}

View File

@@ -0,0 +1,272 @@
/**
* IIT 质控规则管理服务
* 管理 JSON Logic 格式的质控规则
*/
import { PrismaClient } from '@prisma/client';
import { logger } from '../../../common/logging/index.js';
import jsonLogic from 'json-logic-js';
// ==================== 类型定义 ====================
export interface QCRule {
id: string;
name: string;
field: string | string[];
logic: Record<string, unknown>;
message: string;
severity: 'error' | 'warning' | 'info';
category: 'inclusion' | 'exclusion' | 'lab_values' | 'logic_check';
metadata?: Record<string, unknown>;
}
export interface CreateRuleInput {
name: string;
field: string | string[];
logic: Record<string, unknown>;
message: string;
severity: 'error' | 'warning' | 'info';
category: 'inclusion' | 'exclusion' | 'lab_values' | 'logic_check';
metadata?: Record<string, unknown>;
}
export interface UpdateRuleInput {
name?: string;
field?: string | string[];
logic?: Record<string, unknown>;
message?: string;
severity?: 'error' | 'warning' | 'info';
category?: 'inclusion' | 'exclusion' | 'lab_values' | 'logic_check';
metadata?: Record<string, unknown>;
}
export interface QCRuleConfig {
rules: QCRule[];
version: number;
updatedAt: string;
}
export interface TestRuleInput {
logic: Record<string, unknown>;
testData: Record<string, unknown>;
}
// ==================== 服务实现 ====================
export class IitQcRuleService {
constructor(private prisma: PrismaClient) {}
/**
* 获取项目的所有质控规则
*/
async listRules(projectId: string): Promise<QCRule[]> {
const skill = await this.prisma.iitSkill.findFirst({
where: {
projectId,
skillType: 'qc_process',
},
});
if (!skill || !skill.config) {
return [];
}
const config = skill.config as unknown as QCRuleConfig;
return config.rules || [];
}
/**
* 获取单条规则
*/
async getRule(projectId: string, ruleId: string): Promise<QCRule | null> {
const rules = await this.listRules(projectId);
return rules.find((r) => r.id === ruleId) || null;
}
/**
* 添加规则
*/
async addRule(projectId: string, input: CreateRuleInput): Promise<QCRule> {
const skill = await this.getOrCreateSkill(projectId);
const config = (skill.config as unknown as QCRuleConfig) || { rules: [], version: 1, updatedAt: '' };
// 生成规则 ID
const ruleId = `rule_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const newRule: QCRule = {
id: ruleId,
...input,
};
config.rules.push(newRule);
config.version += 1;
config.updatedAt = new Date().toISOString();
await this.prisma.iitSkill.update({
where: { id: skill.id },
data: { config: config as unknown as object },
});
logger.info('添加质控规则成功', { projectId, ruleId, ruleName: input.name });
return newRule;
}
/**
* 更新规则
*/
async updateRule(projectId: string, ruleId: string, input: UpdateRuleInput): Promise<QCRule> {
const skill = await this.getOrCreateSkill(projectId);
const config = (skill.config as unknown as QCRuleConfig) || { rules: [], version: 1, updatedAt: '' };
const ruleIndex = config.rules.findIndex((r) => r.id === ruleId);
if (ruleIndex === -1) {
throw new Error('规则不存在');
}
const updatedRule: QCRule = {
...config.rules[ruleIndex],
...input,
};
config.rules[ruleIndex] = updatedRule;
config.version += 1;
config.updatedAt = new Date().toISOString();
await this.prisma.iitSkill.update({
where: { id: skill.id },
data: { config: config as unknown as object },
});
logger.info('更新质控规则成功', { projectId, ruleId });
return updatedRule;
}
/**
* 删除规则
*/
async deleteRule(projectId: string, ruleId: string): Promise<void> {
const skill = await this.getOrCreateSkill(projectId);
const config = (skill.config as unknown as QCRuleConfig) || { rules: [], version: 1, updatedAt: '' };
const ruleIndex = config.rules.findIndex((r) => r.id === ruleId);
if (ruleIndex === -1) {
throw new Error('规则不存在');
}
config.rules.splice(ruleIndex, 1);
config.version += 1;
config.updatedAt = new Date().toISOString();
await this.prisma.iitSkill.update({
where: { id: skill.id },
data: { config: config as unknown as object },
});
logger.info('删除质控规则成功', { projectId, ruleId });
}
/**
* 批量导入规则
*/
async importRules(projectId: string, rules: CreateRuleInput[]): Promise<QCRule[]> {
const skill = await this.getOrCreateSkill(projectId);
const newRules: QCRule[] = rules.map((input, index) => ({
id: `rule_${Date.now()}_${index}_${Math.random().toString(36).substr(2, 9)}`,
...input,
}));
const config: QCRuleConfig = {
rules: newRules,
version: 1,
updatedAt: new Date().toISOString(),
};
await this.prisma.iitSkill.update({
where: { id: skill.id },
data: { config: config as unknown as object },
});
logger.info('批量导入质控规则成功', { projectId, ruleCount: newRules.length });
return newRules;
}
/**
* 测试规则逻辑
*/
async testRule(input: TestRuleInput): Promise<{ passed: boolean; result: unknown }> {
try {
const result = jsonLogic.apply(input.logic, input.testData);
return {
passed: !!result,
result,
};
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new Error(`规则执行失败: ${message}`);
}
}
/**
* 获取规则分类统计
*/
async getRuleStats(projectId: string) {
const rules = await this.listRules(projectId);
const stats = {
total: rules.length,
byCategory: {} as Record<string, number>,
bySeverity: {} as Record<string, number>,
};
rules.forEach((rule) => {
stats.byCategory[rule.category] = (stats.byCategory[rule.category] || 0) + 1;
stats.bySeverity[rule.severity] = (stats.bySeverity[rule.severity] || 0) + 1;
});
return stats;
}
/**
* 获取或创建 qc_process Skill
*/
private async getOrCreateSkill(projectId: string) {
let skill = await this.prisma.iitSkill.findFirst({
where: {
projectId,
skillType: 'qc_process',
},
});
if (!skill) {
skill = await this.prisma.iitSkill.create({
data: {
projectId,
skillType: 'qc_process',
name: '质控规则',
description: '数据质量控制规则集',
config: {
rules: [],
version: 1,
updatedAt: new Date().toISOString(),
},
isActive: true,
},
});
logger.info('创建 qc_process Skill', { projectId, skillId: skill.id });
}
return skill;
}
}
// 单例工厂函数
let serviceInstance: IitQcRuleService | null = null;
export function getIitQcRuleService(prisma: PrismaClient): IitQcRuleService {
if (!serviceInstance) {
serviceInstance = new IitQcRuleService(prisma);
}
return serviceInstance;
}

View File

@@ -0,0 +1,256 @@
/**
* IIT 用户映射管理控制器
*/
import { FastifyRequest, FastifyReply } from 'fastify';
import { getIitUserMappingService, CreateUserMappingInput, UpdateUserMappingInput } from './iitUserMappingService.js';
import { prisma } from '../../../config/database.js';
import { logger } from '../../../common/logging/index.js';
// ==================== 类型定义 ====================
interface ProjectIdParams {
projectId: string;
}
interface MappingIdParams {
projectId: string;
mappingId: string;
}
interface ListMappingsQuery {
role?: string;
search?: string;
}
// ==================== 控制器函数 ====================
/**
* 获取项目的用户映射列表
*/
export async function listUserMappings(
request: FastifyRequest<{ Params: ProjectIdParams; Querystring: ListMappingsQuery }>,
reply: FastifyReply
) {
try {
const { projectId } = request.params;
const { role, search } = request.query;
const service = getIitUserMappingService(prisma);
const mappings = await service.listUserMappings(projectId, { role, search });
return reply.send({
success: true,
data: mappings,
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error('获取用户映射列表失败', { error: message });
return reply.status(500).send({
success: false,
error: message,
});
}
}
/**
* 获取单个用户映射
*/
export async function getUserMapping(
request: FastifyRequest<{ Params: MappingIdParams }>,
reply: FastifyReply
) {
try {
const { projectId, mappingId } = request.params;
const service = getIitUserMappingService(prisma);
const mapping = await service.getUserMapping(projectId, mappingId);
if (!mapping) {
return reply.status(404).send({
success: false,
error: '用户映射不存在',
});
}
return reply.send({
success: true,
data: mapping,
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error('获取用户映射失败', { error: message });
return reply.status(500).send({
success: false,
error: message,
});
}
}
/**
* 创建用户映射
*/
export async function createUserMapping(
request: FastifyRequest<{ Params: ProjectIdParams; Body: CreateUserMappingInput }>,
reply: FastifyReply
) {
try {
const { projectId } = request.params;
const input = request.body;
// 验证必填字段 - 只有企业微信用户 ID 是必填的
if (!input.wecomUserId) {
return reply.status(400).send({
success: false,
error: '请输入企业微信用户 ID',
});
}
// 如果没有提供 systemUserId使用 wecomUserId 作为默认值
if (!input.systemUserId) {
input.systemUserId = input.wecomUserId;
}
// 如果没有提供 redcapUsername使用 wecomUserId 作为默认值
if (!input.redcapUsername) {
input.redcapUsername = input.wecomUserId;
}
// 如果没有提供 role默认为 PI
if (!input.role) {
input.role = 'PI';
}
const service = getIitUserMappingService(prisma);
const mapping = await service.createUserMapping(projectId, input);
return reply.status(201).send({
success: true,
data: mapping,
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error('创建用户映射失败', { error: message });
if (message.includes('已存在')) {
return reply.status(409).send({
success: false,
error: message,
});
}
return reply.status(500).send({
success: false,
error: message,
});
}
}
/**
* 更新用户映射
*/
export async function updateUserMapping(
request: FastifyRequest<{ Params: MappingIdParams; Body: UpdateUserMappingInput }>,
reply: FastifyReply
) {
try {
const { projectId, mappingId } = request.params;
const input = request.body;
const service = getIitUserMappingService(prisma);
const mapping = await service.updateUserMapping(projectId, mappingId, input);
return reply.send({
success: true,
data: mapping,
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error('更新用户映射失败', { error: message });
if (message.includes('不存在')) {
return reply.status(404).send({
success: false,
error: message,
});
}
return reply.status(500).send({
success: false,
error: message,
});
}
}
/**
* 删除用户映射
*/
export async function deleteUserMapping(
request: FastifyRequest<{ Params: MappingIdParams }>,
reply: FastifyReply
) {
try {
const { projectId, mappingId } = request.params;
const service = getIitUserMappingService(prisma);
await service.deleteUserMapping(projectId, mappingId);
return reply.send({
success: true,
message: '删除成功',
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error('删除用户映射失败', { error: message });
if (message.includes('不存在')) {
return reply.status(404).send({
success: false,
error: message,
});
}
return reply.status(500).send({
success: false,
error: message,
});
}
}
/**
* 获取角色选项
*/
export async function getRoleOptions(
_request: FastifyRequest,
reply: FastifyReply
) {
const service = getIitUserMappingService(prisma);
const options = service.getRoleOptions();
return reply.send({
success: true,
data: options,
});
}
/**
* 获取用户映射统计
*/
export async function getUserMappingStats(
request: FastifyRequest<{ Params: ProjectIdParams }>,
reply: FastifyReply
) {
try {
const { projectId } = request.params;
const service = getIitUserMappingService(prisma);
const stats = await service.getUserMappingStats(projectId);
return reply.send({
success: true,
data: stats,
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error('获取用户映射统计失败', { error: message });
return reply.status(500).send({
success: false,
error: message,
});
}
}

View File

@@ -0,0 +1,29 @@
/**
* IIT 用户映射管理路由
*/
import { FastifyInstance } from 'fastify';
import * as controller from './iitUserMappingController.js';
export async function iitUserMappingRoutes(fastify: FastifyInstance) {
// 获取角色选项(不需要项目 ID
fastify.get('/roles', controller.getRoleOptions);
// 获取项目的用户映射列表
fastify.get('/:projectId/users', controller.listUserMappings);
// 获取用户映射统计
fastify.get('/:projectId/users/stats', controller.getUserMappingStats);
// 获取单个用户映射
fastify.get('/:projectId/users/:mappingId', controller.getUserMapping);
// 创建用户映射
fastify.post('/:projectId/users', controller.createUserMapping);
// 更新用户映射
fastify.put('/:projectId/users/:mappingId', controller.updateUserMapping);
// 删除用户映射
fastify.delete('/:projectId/users/:mappingId', controller.deleteUserMapping);
}

View File

@@ -0,0 +1,206 @@
/**
* IIT 用户映射管理服务
* 管理企业微信用户与 REDCap 用户的映射关系
*/
import { PrismaClient, Prisma } from '@prisma/client';
import { logger } from '../../../common/logging/index.js';
// ==================== 类型定义 ====================
export interface CreateUserMappingInput {
systemUserId: string;
redcapUsername: string;
wecomUserId?: string;
role: string;
}
export interface UpdateUserMappingInput {
systemUserId?: string;
redcapUsername?: string;
wecomUserId?: string;
role?: string;
}
export interface UserMappingListFilters {
role?: string;
search?: string;
}
// ==================== 服务实现 ====================
export class IitUserMappingService {
constructor(private prisma: PrismaClient) {}
/**
* 获取项目的用户映射列表
*/
async listUserMappings(projectId: string, filters?: UserMappingListFilters) {
const where: Prisma.IitUserMappingWhereInput = {
projectId,
};
if (filters?.role) {
where.role = filters.role;
}
if (filters?.search) {
where.OR = [
{ systemUserId: { contains: filters.search, mode: 'insensitive' } },
{ redcapUsername: { contains: filters.search, mode: 'insensitive' } },
{ wecomUserId: { contains: filters.search, mode: 'insensitive' } },
];
}
const mappings = await this.prisma.iitUserMapping.findMany({
where,
orderBy: { createdAt: 'desc' },
});
return mappings;
}
/**
* 获取单个用户映射
*/
async getUserMapping(projectId: string, mappingId: string) {
return this.prisma.iitUserMapping.findFirst({
where: {
id: mappingId,
projectId,
},
});
}
/**
* 创建用户映射
*/
async createUserMapping(projectId: string, input: CreateUserMappingInput) {
// 检查项目是否存在
const project = await this.prisma.iitProject.findFirst({
where: { id: projectId, deletedAt: null },
});
if (!project) {
throw new Error('项目不存在');
}
// 检查是否已存在
const existing = await this.prisma.iitUserMapping.findFirst({
where: {
projectId,
OR: [
{ systemUserId: input.systemUserId },
{ redcapUsername: input.redcapUsername },
],
},
});
if (existing) {
throw new Error('该用户或 REDCap 用户名已存在映射');
}
const mapping = await this.prisma.iitUserMapping.create({
data: {
projectId,
systemUserId: input.systemUserId,
redcapUsername: input.redcapUsername,
wecomUserId: input.wecomUserId,
role: input.role,
},
});
logger.info('创建用户映射成功', { projectId, mappingId: mapping.id });
return mapping;
}
/**
* 更新用户映射
*/
async updateUserMapping(projectId: string, mappingId: string, input: UpdateUserMappingInput) {
const existing = await this.prisma.iitUserMapping.findFirst({
where: { id: mappingId, projectId },
});
if (!existing) {
throw new Error('用户映射不存在');
}
const mapping = await this.prisma.iitUserMapping.update({
where: { id: mappingId },
data: {
systemUserId: input.systemUserId,
redcapUsername: input.redcapUsername,
wecomUserId: input.wecomUserId,
role: input.role,
},
});
logger.info('更新用户映射成功', { projectId, mappingId });
return mapping;
}
/**
* 删除用户映射
*/
async deleteUserMapping(projectId: string, mappingId: string) {
const existing = await this.prisma.iitUserMapping.findFirst({
where: { id: mappingId, projectId },
});
if (!existing) {
throw new Error('用户映射不存在');
}
await this.prisma.iitUserMapping.delete({
where: { id: mappingId },
});
logger.info('删除用户映射成功', { projectId, mappingId });
}
/**
* 获取可用角色列表
*/
getRoleOptions() {
return [
{ value: 'PI', label: '主要研究者 (PI)' },
{ value: 'Sub-I', label: '次要研究者 (Sub-I)' },
{ value: 'CRC', label: '临床研究协调员 (CRC)' },
{ value: 'CRA', label: '临床监查员 (CRA)' },
{ value: 'DM', label: '数据管理员 (DM)' },
{ value: 'Statistician', label: '统计师' },
{ value: 'Other', label: '其他' },
];
}
/**
* 按角色统计用户数量
*/
async getUserMappingStats(projectId: string) {
const mappings = await this.prisma.iitUserMapping.findMany({
where: { projectId },
select: { role: true },
});
const stats: Record<string, number> = {};
mappings.forEach((m) => {
stats[m.role] = (stats[m.role] || 0) + 1;
});
return {
total: mappings.length,
byRole: stats,
};
}
}
// 单例工厂函数
let serviceInstance: IitUserMappingService | null = null;
export function getIitUserMappingService(prisma: PrismaClient): IitUserMappingService {
if (!serviceInstance) {
serviceInstance = new IitUserMappingService(prisma);
}
return serviceInstance;
}

View File

@@ -0,0 +1,15 @@
/**
* IIT 项目管理模块导出
*/
export { iitProjectRoutes } from './iitProjectRoutes.js';
export { iitQcRuleRoutes } from './iitQcRuleRoutes.js';
export { iitUserMappingRoutes } from './iitUserMappingRoutes.js';
export { iitBatchRoutes } from './iitBatchRoutes.js';
export { IitProjectService, getIitProjectService } from './iitProjectService.js';
export { IitQcRuleService, getIitQcRuleService } from './iitQcRuleService.js';
export { IitUserMappingService, getIitUserMappingService } from './iitUserMappingService.js';
export * from './iitProjectController.js';
export * from './iitQcRuleController.js';
export * from './iitUserMappingController.js';
export * from './iitBatchController.js';