Files
AIclinicalresearch/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_方案B实施总结_2025-12-09.md
HaHafeng ef967d7d7c build(backend): Complete Node.js backend deployment preparation
Major changes:
- Add Docker configuration (Dockerfile, .dockerignore)
- Fix 200+ TypeScript compilation errors
- Add Prisma schema relations for all models (30+ relations)
- Update tsconfig.json to relax non-critical checks
- Optimize Docker build with local dist strategy

Technical details:
- Exclude test files from TypeScript compilation
- Add manual relations for ASL, PKB, DC, AIA modules
- Use type assertions for JSON/Buffer compatibility
- Fix pg-boss, extractionWorker, and other legacy code issues

Build result:
- Docker image: 838MB (compressed ~186MB)
- Successfully pushed to ACR
- Zero TypeScript compilation errors

Related docs:
- Update deployment documentation
- Add Python microservice SAE deployment guide
2025-12-24 22:12:00 +08:00

8.5 KiB
Raw Blame History

工具C - 方案B实施总结列名特殊字符解决方案

日期: 2025-12-09
版本: v1.0
实施方案: 方案B - Python负责列名替换


📋 问题背景

原始问题

用户上传的Excel文件表头包含特殊字符导致计算列功能失败

示例表头:

  • 体重kg
  • 1.高血压病(无=0有=1不知道=2
  • 身高cm

报错信息:

计算列失败:公式验证失败: 公式包含不允许的字符

🎯 方案选择

方案对比

方案 描述 优点 缺点 评分
方案A 用户使用序号引用col_0, col_1 技术最安全 用户体验差,不直观
方案B 用户使用原列名Python负责替换 用户体验好,技术可靠 需实现替换逻辑
方案C 前端替换列名 减少网络传输 边界识别困难,不可靠

最终选择: 方案B


🏗️ 架构设计

数据流

用户输入公式(原列名)
    ↓
前端体重kg / (身高cm/100)**2
    ↓
后端:获取 columnMapping
    ↓
传递给Python: {
  formula: "体重kg / (身高cm/100)**2",
  column_mapping: [
    {"originalName": "体重kg", "safeName": "col_0"},
    {"originalName": "身高cm", "safeName": "col_1"}
  ]
}
    ↓
Python替换: col_0 / (col_1/100)**2
    ↓
执行计算 ✅

职责划分

层级 职责 关键点
前端 UI交互、数据收集 用户看到和输入原列名
后端 获取columnMapping、传递给Python 从Session获取映射
Python 列名替换、公式执行 按长度排序、精确替换

💻 实施细节

1. 前端ComputeDialog.tsx

保持不变 - 已经使用原列名方式

// 用户点击列名标签,插入到公式框
<Tag onClick={() => setFormula(formula + col.name)}>
  {col.name}  {/* 显示原列名体重kg */}
</Tag>

// 提交时直接传递原公式
onApply({
  newColumnName: "BMI",
  formula: "体重kg / (身高cm/100)**2",  // 原列名
});

2. 后端QuickActionController.ts

修改: 获取session并传递columnMapping

// 获取session包含columnMapping
session = await sessionService.getSession(sessionId);

// 传递给QuickActionService
executeResult = await quickActionService.executeCompute(
  fullData, 
  params, 
  session.columnMapping  // ✅ 传递映射
);

3. 后端QuickActionService.ts

修改: 接收并传递columnMapping给Python

async executeCompute(
  data: any[], 
  params: ComputeParams, 
  columnMapping?: any[]  // ✅ 新增参数
): Promise<OperationResult> {
  const response = await axios.post(`${PYTHON_SERVICE_URL}/api/operations/compute`, {
    data,
    new_column_name: params.newColumnName,
    formula: params.formula,
    column_mapping: columnMapping || [],  // ✅ 传递映射
  });
  
  return response.data;
}

4. Pythonmain.py

修改: 更新请求模型

class ComputeRequest(BaseModel):
    data: List[Dict[str, Any]]
    new_column_name: str
    formula: str
    column_mapping: List[Dict[str, str]] = []  # ✅ 新增字段

@app.post("/api/operations/compute")
async def operation_compute(request: ComputeRequest):
    result_df = compute_column(
        df,
        request.new_column_name,
        request.formula,
        request.column_mapping  # ✅ 传递映射
    )

5. Pythoncompute.py

核心实现: 列名替换逻辑

def replace_column_names_in_formula(
    formula: str, 
    column_mapping: List[Dict[str, str]]
) -> str:
    """
    ✅ 核心算法:可靠的列名替换
    """
    safe_formula = formula
    
    # 关键1按列名长度倒序排序
    # 避免子串问题:先替换"高血压病史",再替换"高血压"
    sorted_mapping = sorted(
        column_mapping,
        key=lambda x: len(x['originalName']),
        reverse=True
    )
    
    # 关键2逐个精确替换不使用正则
    for item in sorted_mapping:
        original = item['originalName']
        safe = item['safeName']
        
        if original in safe_formula:
            safe_formula = safe_formula.replace(original, safe)
    
    return safe_formula

def compute_column(
    df: pd.DataFrame,
    new_column_name: str,
    formula: str,
    column_mapping: Optional[List[Dict[str, str]]] = None
) -> pd.DataFrame:
    """
    ✅ 方案BPython负责替换
    """
    # 1. 替换列名
    if column_mapping:
        safe_formula = replace_column_names_in_formula(formula, column_mapping)
    else:
        safe_formula = formula
    
    # 2. 准备执行环境
    env = {}
    for item in column_mapping:
        env[item['safeName']] = df[item['originalName']]
    env.update(ALLOWED_FUNCTIONS)
    
    # 3. 执行(不需要字符验证!)
    result = eval(safe_formula, {"__builtins__": {}}, env)
    
    return df.assign(**{new_column_name: result})

解决的问题

1. 特殊字符问题

  • 问题: 体重kg 包含中文括号
  • 解决: Python使用安全列名 col_0,不受特殊字符影响

2. 子串包含问题

  • 问题: "高血压" 和 "高血压病史" 可能误替换
  • 解决: 按长度倒序排序,先替换长列名

3. 边界识别问题

  • 问题: 正则\b对中文字符不可靠
  • 解决: 使用Python字符串replace,简单可靠

4. 字符白名单问题

  • 问题: 需要枚举所有允许的字符
  • 解决: 不需要验证Python只处理安全列名

🧪 测试用例

测试1基本功能

column_mapping = [
    {"originalName": "体重kg", "safeName": "col_0"},
    {"originalName": "身高cm", "safeName": "col_1"}
]
formula = "体重kg / (身高cm/100)**2"
# 预期: col_0 / (col_1/100)**2 ✅

测试2子串包含

column_mapping = [
    {"originalName": "高血压", "safeName": "col_0"},
    {"originalName": "高血压病史", "safeName": "col_1"}
]
formula = "高血压病史 + 高血压"
# 预期: col_1 + col_0 ✅(因为按长度排序)

测试3复杂特殊字符

column_mapping = [
    {"originalName": "1.高血压病(无=0有=1不知道=2", "safeName": "col_0"}
]
formula = "1.高血压病(无=0有=1不知道=2 * 2"
# 预期: col_0 * 2 ✅

测试4嵌套括号

column_mapping = [
    {"originalName": "FMA总分0-100", "safeName": "col_0"}
]
formula = "FMA总分0-100 / 100"
# 预期: col_0 / 100 ✅

📊 性能影响

指标 影响 说明
网络传输 +5KB columnMapping约5KB100列
替换时间 <1ms 字符串替换非常快
总体性能 可忽略 相比数据处理时间(秒级)可忽略

🎯 优势总结

用户体验

  • 用户看到和输入原列名
  • 公式直观易懂
  • 历史记录清晰

技术可靠性

  • 不依赖正则边界识别
  • 按长度排序避免子串问题
  • Python字符串操作简单可靠

可维护性

  • 职责清晰前端UI、Python逻辑
  • 易于调试(可打印替换日志)
  • 未来不会再有字符问题

📝 后续工作

已完成

  • 前端保持使用原列名
  • 后端传递columnMapping
  • Python实现替换逻辑
  • 移除字符验证
  • 更新Pivot操作

待测试

  • 用户实际测试
  • 边界情况验证
  • 性能测试

未来优化 💡

  • 添加公式语法高亮
  • 列名自动补全
  • 公式错误提示优化

🔗 相关文件

修改的文件

  1. backend/src/modules/dc/tool-c/controllers/QuickActionController.ts
  2. backend/src/modules/dc/tool-c/services/QuickActionService.ts
  3. extraction_service/main.py
  4. extraction_service/operations/compute.py
  5. extraction_service/operations/pivot.py

文档

  • 本文档:工具C_方案B实施总结_2025-12-09.md
  • 原Bug报告工具C_Bug修复总结_2025-12-08.md

总结

方案B成功实现了

  1. 用户体验优秀 - 使用原列名,直观易懂
  2. 技术可靠 - Python替换简单可控
  3. 彻底解决 - 不再有特殊字符问题

下一步: 等待用户测试验证