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:
2026-02-19 20:57:00 +08:00
parent 8137e3cde2
commit 49b5c37cb1
86 changed files with 21207 additions and 252 deletions

View 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 发现的问题与修复
#### 问题 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 行
---
*文档结束。*