# 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 发现的问题与修复 #### 问题 1:SSA 路由未注册 **现象**: `/api/v1/ssa/*` 返回 404 **原因**: `backend/src/index.ts` 中未注册 SSA 模块路由 **修复**: 添加路由注册 ```typescript // 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` ```typescript // 修复后的逻辑 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:简化设计(推荐) ```typescript // 简化后的 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 ``` ```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 ``` ### 测试请求参数 ```json { "plan": { "tool_code": "ST_T_TEST_IND", "params": { "group_var": "group", "value_var": "score" }, "guardrails": { "check_normality": true } } } ``` ### 预期结果 ```json { "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 **最终代码**: ```typescript // 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 行 --- *文档结束。*