319 lines
8.6 KiB
Markdown
319 lines
8.6 KiB
Markdown
# 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 行
|
||
|
||
---
|
||
|
||
*文档结束。*
|