feat(dc): Add multi-metric transformation feature (direction 1+2)

Summary:
- Implement intelligent multi-metric grouping detection algorithm
- Add direction 1: timepoint-as-row, metric-as-column (analysis format)
- Add direction 2: timepoint-as-column, metric-as-row (display format)
- Fix column name pattern detection (FMA___ issue)
- Maintain original Record ID order in output
- Add full-select/clear buttons in UI
- Integrate into TransformDialog with Radio selection
- Update 3 documentation files

Technical Details:
- Python: detect_metric_groups(), apply_multi_metric_to_long(), apply_multi_metric_to_matrix()
- Backend: 3 new methods in QuickActionService
- Frontend: MultiMetricPanel.tsx (531 lines)
- Total: ~1460 lines of new code

Status: Fully tested and verified, ready for production
This commit is contained in:
2025-12-21 15:06:15 +08:00
parent 8be8cdcf53
commit 9b81aef9a7
123 changed files with 4781 additions and 150 deletions

View File

@@ -25,7 +25,7 @@ import { prisma } from '../../../../config/database.js';
interface QuickActionRequest {
sessionId: string;
action: 'filter' | 'recode' | 'binning' | 'conditional' | 'dropna' | 'dedup';
action: 'filter' | 'recode' | 'binning' | 'conditional' | 'dropna' | 'dedup' | 'compute' | 'pivot' | 'unpivot' | 'metric_time' | 'multi_metric_to_long' | 'multi_metric_to_matrix';
params: any;
userId?: string;
}
@@ -105,6 +105,18 @@ export class QuickActionController {
case 'pivot':
actionDescription = 'Pivot转换';
break;
case 'unpivot':
actionDescription = 'Unpivot转换宽→长表';
break;
case 'metric_time':
actionDescription = '指标-时间表转换';
break;
case 'multi_metric_to_long':
actionDescription = '多指标转长表';
break;
case 'multi_metric_to_matrix':
actionDescription = '多指标转矩阵';
break;
default:
logger.warn(`[QuickAction] 不支持的操作: ${action}`);
return reply.code(400).send({
@@ -184,6 +196,22 @@ export class QuickActionController {
pivotValueOrder
);
break;
case 'unpivot':
// Unpivot不需要columnMapping直接执行
executeResult = await quickActionService.executeUnpivot(fullData, params);
break;
case 'metric_time':
// 指标-时间表转换
executeResult = await quickActionService.executeMetricTime(fullData, params);
break;
case 'multi_metric_to_long':
// 多指标转长表
executeResult = await quickActionService.executeMultiMetricToLong(fullData, params);
break;
case 'multi_metric_to_matrix':
// 多指标转矩阵
executeResult = await quickActionService.executeMultiMetricToMatrix(fullData, params);
break;
}
if (!executeResult.success) {
@@ -340,9 +368,27 @@ export class QuickActionController {
case 'dropna':
executeResult = await quickActionService.executeDropna(fullData, params);
break;
case 'compute':
executeResult = await quickActionService.executeCompute(fullData, params);
break;
case 'dedup':
// TODO: 实现去重功能
return reply.code(400).send({ success: false, error: '去重功能尚未实现' });
case 'pivot':
executeResult = await quickActionService.executePivot(fullData, params);
break;
case 'unpivot':
executeResult = await quickActionService.executeUnpivot(fullData, params);
break;
case 'metric_time':
executeResult = await quickActionService.executeMetricTime(fullData, params);
break;
case 'multi_metric_to_long':
executeResult = await quickActionService.executeMultiMetricToLong(fullData, params);
break;
case 'multi_metric_to_matrix':
executeResult = await quickActionService.executeMultiMetricToMatrix(fullData, params);
break;
default:
return reply.code(400).send({ success: false, error: '不支持的操作' });
}
@@ -361,14 +407,29 @@ export class QuickActionController {
const newRows = resultData.length;
let estimatedChange = '';
if (action === 'filter' || action === 'dropna') {
estimatedChange = `将保留 ${newRows} 行(删除 ${originalRows - newRows} 行)`;
} else if (action === 'recode' || action === 'binning' || action === 'conditional' || action === 'compute') {
estimatedChange = `新增 1 列`;
} else if (action === 'pivot') {
const originalCols = Object.keys(fullData[0] || {}).length;
const newCols = Object.keys(resultData[0] || {}).length;
estimatedChange = `行数: ${originalRows}${newRows}, 列数: ${originalCols}${newCols}`;
switch (action) {
case 'filter':
case 'dropna':
estimatedChange = `保留 ${newRows} 行(删除 ${originalRows - newRows} 行)`;
break;
case 'recode':
case 'binning':
case 'conditional':
case 'compute':
estimatedChange = `将新增 1 列`;
break;
case 'pivot':
case 'unpivot':
case 'metric_time':
case 'multi_metric_to_long':
case 'multi_metric_to_matrix': {
const originalCols = Object.keys(fullData[0] || {}).length;
const newCols = Object.keys(resultData[0] || {}).length;
estimatedChange = `行数: ${originalRows}${newRows}, 列数: ${originalCols}${newCols}`;
break;
}
default:
estimatedChange = `操作完成`;
}
return reply.code(200).send({
@@ -541,6 +602,95 @@ export class QuickActionController {
});
}
}
/**
* POST /api/v1/dc/tool-c/metric-time/detect
* 检测指标-时间表转换模式
*/
async handleMetricTimeDetect(request: FastifyRequest, reply: FastifyReply) {
try {
const { sessionId, valueVars } = request.body as { sessionId: string; valueVars: string[] };
logger.info(`[QuickAction] 检测指标-时间表模式: session=${sessionId}, ${valueVars?.length || 0}`);
// 验证参数
if (!valueVars || valueVars.length < 2) {
return reply.code(400).send({
success: false,
error: '至少需要2列才能检测模式'
});
}
// 调用Service检测模式
const result = await quickActionService.detectMetricTimePattern(valueVars);
if (!result.success) {
return reply.code(500).send({
success: false,
error: result.error || '模式检测失败'
});
}
return reply.code(200).send({
success: true,
pattern: result.pattern,
execution_time: result.execution_time
});
} catch (error: any) {
logger.error(`[QuickAction] 模式检测失败: ${error.message}`);
return reply.code(500).send({
success: false,
error: error.message
});
}
}
/**
* POST /api/v1/dc/tool-c/multi-metric/detect
* 检测多指标分组
*/
async handleMultiMetricDetect(request: FastifyRequest, reply: FastifyReply) {
try {
const { sessionId, valueVars, separators } = request.body as {
sessionId: string;
valueVars: string[];
separators?: string[];
};
logger.info(`[QuickAction] 检测多指标分组: session=${sessionId}, ${valueVars?.length || 0}`);
// 验证参数
if (!valueVars || valueVars.length < 2) {
return reply.code(400).send({
success: false,
error: '至少需要2列才能检测分组'
});
}
// 调用Service检测分组
const result = await quickActionService.detectMultiMetricGroups(valueVars, separators);
if (!result.success) {
return reply.code(500).send({
success: false,
error: result.message || '分组检测失败'
});
}
return reply.code(200).send({
success: true,
grouping: result
});
} catch (error: any) {
logger.error(`[QuickAction] 多指标分组检测失败: ${error.message}`);
return reply.code(500).send({
success: false,
error: error.message
});
}
}
}
// ==================== 导出单例 ====================

View File

@@ -236,3 +236,9 @@ export const streamAIController = new StreamAIController();