Files
AIclinicalresearch/docs/03-业务模块/SSA-智能统计分析/05-测试文档/SSA端到端测试与架构讨论-2026-02-19.md

8.6 KiB
Raw Blame History

SSA 端到端测试与架构讨论

日期: 2026-02-19
状态: 已决策 - 采用方案 B简化设计仅支持 OSS
参与者: 开发团队


1. 测试概述

1.1 测试目标

验证 SSA 智能统计分析模块的完整数据流:

前端 → Node.js 后端 → OSS 存储 → R 统计服务 → 返回结果

1.2 测试环境

组件 状态 端口
Node.js 后端 运行中 3001
R Docker 服务 运行中 8082
PostgreSQL 运行中 5432
OSS (开发环境) 可用 ai-clinical-data-dev

1.3 测试脚本

backend/tests/ssa-e2e-test.ps1

2. 测试结果

2.1 测试步骤与结果

步骤 描述 结果
Step 0 R 服务健康检查 通过
Step 1 用户登录认证 通过
Step 2 SSA 路由检查 通过(路由已注册)
Step 3 创建分析会话 通过
Step 4 上传 CSV 文件 通过(存入 OSS
Step 5 执行 T 检验 失败(修复前)→ 通过(修复后)

2.2 发现的问题与修复

问题 1SSA 路由未注册

现象: /api/v1/ssa/* 返回 404

原因: backend/src/index.ts 中未注册 SSA 模块路由

修复: 添加路由注册

// backend/src/index.ts
import { ssaRoutes } from './modules/ssa/index.js';
await fastify.register(ssaRoutes, { prefix: '/api/v1/ssa' });

问题 2数据源选择逻辑错误

现象: 上传 CSV 后执行分析R 服务报错"列名不存在"

原因: RClientService.buildDataSource() 优先读取 session.dataPayload(为空),返回空数组,忽略了 session.dataOssKey

修复: 调整优先级,先检查 dataOssKey

// 修复后的逻辑
private async buildDataSource(session: any) {
  // 1. 优先使用 OSS key已上传的文件
  if (session.dataOssKey) {
    const signedUrl = await storage.getUrl(session.dataOssKey);
    return { type: 'oss', oss_url: signedUrl };
  }
  
  // 2. 其次使用 inline payload
  if (session.dataPayload) {
    return { type: 'inline', data: session.dataPayload };
  }
  
  // 3. 无数据
  return { type: 'inline', data: [] };
}

3. 架构讨论:数据传输设计

3.1 当前设计

┌─────────────────────────────────────────────────────────────────┐
│  原设计:混合数据协议(根据大小选择传输方式)                     │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  数据来源 1: 用户上传文件 → 存入 OSS → dataOssKey               │
│  数据来源 2: 前端传 JSON → 存入内存 → dataPayload               │
│                                                                 │
│  执行分析时:                                                   │
│  - 有 dataOssKey → 生成预签名 URL → R 服务从 OSS 下载           │
│  - 有 dataPayload 且 < 2MB → 直接传 inline JSON                │
│  - 有 dataPayload 且 >= 2MB → 先存 OSS 再传 URL                │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

3.2 实际使用场景分析

场景 是否存在 说明
用户上传 CSV/Excel 文件 100% SSA 核心场景
前端直接传 JSON 数据 0% 产品设计不支持手动输入数据

3.3 问题:设计与场景不匹配

┌─────────────────────────────────────────────────────────────────┐
│  实际数据流100% 场景)                                         │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  用户 → 上传 CSV → Node.js → 存入 OSS → 记录 dataOssKey         │
│                                                                 │
│  执行分析 → 读取 dataOssKey → 生成预签名 URL → R 服务下载       │
│                                                                 │
│  dataPayload 永远为空!"判断大小"逻辑从不执行!                 │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

结论: 当前代码中的 dataPayload 和"根据大小判断"逻辑是死代码,永远不会执行。


4. 待讨论决策

4.1 方案 A保持现状保留灵活性

优点:

  • 未来可能支持"在线输入数据"功能
  • 代码改动小

缺点:

  • 存在永远不执行的代码
  • 逻辑复杂度高
  • 新开发者可能困惑

4.2 方案 B简化设计推荐

// 简化后的 buildDataSource
private async buildDataSource(session: any) {
  const ossKey = session.dataOssKey;
  
  if (!ossKey) {
    throw new Error('请先上传数据文件');
  }
  
  const signedUrl = await storage.getUrl(ossKey);
  return {
    type: 'oss',
    oss_url: signedUrl
  };
}

优点:

  • 代码简洁
  • 逻辑清晰
  • 符合实际使用场景

缺点:

  • 未来如需支持 inline JSON需要重新添加

4.3 方案 C完全移除 inline 支持

除了简化 buildDataSource,还可以:

  • 移除 ssaSession.dataPayload 字段
  • 移除 R 服务中的 inline JSON 解析逻辑
  • 只保留 OSS 数据流

优点:

  • 最简洁
  • 单一数据流,易于维护

缺点:

  • 改动较大
  • 需要修改数据库 schema

5. 相关文件清单

文件 作用
backend/src/modules/ssa/index.ts SSA 模块入口
backend/src/modules/ssa/routes/analysis.routes.ts 上传/执行路由
backend/src/modules/ssa/executor/RClientService.ts R 服务调用
r-statistics-service/utils/data_loader.R R 服务数据加载
backend/tests/ssa-e2e-test.ps1 端到端测试脚本

6. 建议结论

  1. 短期: 使用当前修复后的代码,完成 Week 1-2 开发
  2. 中期: 团队讨论后,决定是否采用方案 B 简化设计
  3. 长期: 根据产品需求,确定是否需要支持"在线输入数据"功能

7. 附录:测试数据

测试 CSV 文件

r-statistics-service/tests/fixtures/sample_t_test.csv
group,score
A,23
A,25
A,27
A,22
A,24
A,26
A,21
A,28
B,30
B,32
B,28
B,31
B,29
B,33
B,27
B,35

测试请求参数

{
  "plan": {
    "tool_code": "ST_T_TEST_IND",
    "params": {
      "group_var": "group",
      "value_var": "score"
    },
    "guardrails": {
      "check_normality": true
    }
  }
}

预期结果

{
  "status": "success",
  "results": {
    "method": "Welch Two Sample t-test",
    "statistic": -4.78,
    "df": 13.90,
    "p_value": 0.0003,
    "p_value_fmt": "< 0.001"
  }
}


8. 决策结果

决策日期: 2026-02-19

采用方案: B - 简化设计,仅支持 OSS

最终代码:

// backend/src/modules/ssa/executor/RClientService.ts
private async buildDataSource(session: any): Promise<{ type: string; oss_url: string }> {
  const ossKey = session.dataOssKey;
  
  if (!ossKey) {
    logger.error('[SSA:RClient] No data uploaded', { sessionId: session.id });
    throw new Error('请先上传数据文件');
  }
  
  logger.info('[SSA:RClient] Building OSS data source', { sessionId: session.id, ossKey });
  const signedUrl = await storage.getUrl(ossKey);
  
  return {
    type: 'oss',
    oss_url: signedUrl
  };
}

变更说明:

  • 移除 dataPayload 和 inline JSON 支持
  • 移除"根据大小判断"逻辑
  • 如果未上传文件,直接抛出用户友好错误
  • 代码从 30 行简化到 15 行

文档结束。