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:
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 导出单例 ====================
|
||||
|
||||
@@ -236,3 +236,9 @@ export const streamAIController = new StreamAIController();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user