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

319 lines
8.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 模块路由
**修复**: 添加路由注册
```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 行
---
*文档结束。*