8.6 KiB
8.6 KiB
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 模块路由
修复: 添加路由注册
// 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. 建议结论
- 短期: 使用当前修复后的代码,完成 Week 1-2 开发
- 中期: 团队讨论后,决定是否采用方案 B 简化设计
- 长期: 根据产品需求,确定是否需要支持"在线输入数据"功能
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 行
文档结束。