feat(ssa): Complete T-test end-to-end testing with 9 bug fixes - Phase 1 core 85% complete. R service: missing value auto-filter. Backend: error handling, variable matching, dynamic filename. Frontend: module activation, session isolation, error propagation. Full flow verified.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
318
docs/03-业务模块/SSA-智能统计分析/05-测试文档/SSA端到端测试与架构讨论-2026-02-19.md
Normal file
318
docs/03-业务模块/SSA-智能统计分析/05-测试文档/SSA端到端测试与架构讨论-2026-02-19.md
Normal file
@@ -0,0 +1,318 @@
|
||||
# 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 行
|
||||
|
||||
---
|
||||
|
||||
*文档结束。*
|
||||
Reference in New Issue
Block a user