Files
AIclinicalresearch/docs/02-通用能力层/06-R统计引擎/01-R统计引擎架构与部署指南.md
HaHafeng 428a22adf2 feat(ssa): Complete Phase 2A frontend integration - multi-step workflow end-to-end
Phase 2A: WorkflowPlannerService, WorkflowExecutorService, Python data quality, 6 bug fixes, DescriptiveResultView, multi-step R code/Word export, MVP UI reuse. V11 UI: Gemini-style, multi-task, single-page scroll, Word export. Architecture: Block-based rendering consensus (4 block types). New R tools: chi_square, correlation, descriptive, logistic_binary, mann_whitney, t_test_paired. Docs: dev summary, block-based plan, status updates, task list v2.0.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-20 23:09:27 +08:00

898 lines
22 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.
# R 统计引擎架构与部署指南
> **版本:** v1.1
> **更新日期:** 2026-02-20
> **维护者:** SSA-Pro 开发团队
> **状态:** ✅ 生产就绪Phase 2A 完成)
---
## 📋 目录
1. [概述](#1-概述)
2. [架构设计](#2-架构设计)
3. [Docker 镜像构建](#3-docker-镜像构建)
4. [部署指南](#4-部署指南)
5. [API 参考](#5-api-参考)
6. [开发指南](#6-开发指南)
7. [运维指南](#7-运维指南)
8. [常见问题](#8-常见问题)
---
## 1. 概述
### 1.1 什么是 R 统计引擎
R 统计引擎是平台的**专用统计计算服务**,基于 Docker 容器化部署,提供:
- 🧮 **严谨的统计分析能力**T 检验、方差分析、回归等)
- 🛡️ **统计护栏**(正态性检验、方差齐性检验等)
- 📊 **可视化输出**Base64 编码的图表)
- 📝 **可复现代码生成**APA 格式的 R 脚本)
### 1.2 定位
```
┌─────────────────────────────────────────────────────────────┐
│ 业务模块层 │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ SSA-Pro │ │ 其他 │ │ 其他 │ │
│ │ 智能统计 │ │ 模块 │ │ 模块 │ │
│ └────┬────┘ └─────────┘ └─────────┘ │
├───────┼─────────────────────────────────────────────────────┤
│ ▼ 通用能力层 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ R 统计引擎 (Docker) │ │
│ │ • /health 健康检查 │ │
│ │ • /api/v1/tools 工具列表 │ │
│ │ • /api/v1/skills 技能执行 │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
### 1.3 技术栈
| 组件 | 版本 | 说明 |
|------|------|------|
| R | 4.3.3 | 统计计算核心 |
| plumber | 1.2.1 | REST API 框架 |
| ggplot2 | 最新 | 数据可视化 |
| car | 3.1-2 | 高级统计检验 |
| dplyr/tidyr | 最新 | 数据处理 |
| Docker | 24+ | 容器化部署 |
---
## 2. 架构设计
### 2.1 Brain-Hand 模型
R 统计引擎采用 **Brain-Hand 分离架构**
```
┌──────────────────┐ ┌──────────────────┐
│ Node.js │ │ R Docker │
│ (Brain) │ │ (Hand) │
├──────────────────┤ ├──────────────────┤
│ • 业务逻辑 │ HTTP │ • 统计计算 │
│ • 认证鉴权 │ ───────> │ • 数据处理 │
│ • OSS 签名 │ │ • 图表生成 │
│ • 结果解释 │ <─────── │ • 代码生成 │
└──────────────────┘ JSON └──────────────────┘
```
### 2.2 数据传输协议
支持两种数据传输方式:
| 方式 | 条件 | 字段 |
|------|------|------|
| **inline** | 数据 < 2MB | `data_source.data` (JSON) |
| **oss** | 数据 >= 2MB | `data_source.oss_url` (预签名 URL) |
```json
// 方式 1: inline
{
"data_source": {
"type": "inline",
"data": [{"group": "A", "value": 10}, ...]
}
}
// 方式 2: oss (预签名 URL)
{
"data_source": {
"type": "oss",
"oss_url": "https://bucket.oss.com/data.csv?signature=xxx"
}
}
```
#### 2.2.1 inline 数据格式详解
R 数据加载器 (`utils/data_loader.R`) 支持两种 JSON 数据格式:
| 格式 | 说明 | 示例 |
|------|------|------|
| **行格式** | JSON 对象数组,每个对象是一行 | `[{"sex": 1, "age": 25}, {"sex": 2, "age": 30}]` |
| **列格式** | JSON 对象,每个属性是一列 | `{"sex": [1, 2], "age": [25, 30]}` |
> **推荐**:使用**行格式**,与 JavaScript/TypeScript 的数据处理习惯一致。
**Node.js 调用示例:**
```typescript
// 推荐行格式Array of Objects
const data = [
{ sex: 1, age: 25, bmi: 22.5 },
{ sex: 2, age: 30, bmi: 24.1 },
// ...
];
const response = await axios.post('http://localhost:8082/api/v1/skills/ST_T_TEST_IND', {
data_source: {
type: 'inline',
data: data // 直接传入数组
},
params: {
group_var: 'sex',
value_var: 'age'
}
});
```
### 2.3 安全设计
| 安全措施 | 实现方式 |
|----------|----------|
| 非特权用户 | `USER appuser` |
| 路径遍历防护 | `tool_code` 正则白名单 `^[A-Z][A-Z0-9_]*$` |
| OSS 密钥隔离 | Node.js 生成预签名 URLR 无需持有密钥 |
| 健康检查 | Docker HEALTHCHECK |
---
## 3. Docker 镜像构建
### 3.1 完整 Dockerfile
```dockerfile
FROM rocker/r-ver:4.3
LABEL maintainer="dev-team@aiclinicalresearch.com"
LABEL version="1.0.1"
LABEL description="SSA-Pro R Statistics Service"
# 安装系统依赖(包括 R 包编译所需的库)
RUN apt-get update && apt-get install -y \
libcurl4-openssl-dev \
libssl-dev \
libxml2-dev \
libsodium-dev \
zlib1g-dev \
libnlopt-dev \
liblapack-dev \
libblas-dev \
gfortran \
pkg-config \
cmake \
curl \
&& rm -rf /var/lib/apt/lists/*
# 直接安装 R 包
RUN R -e "install.packages(c( \
'plumber', \
'jsonlite', \
'ggplot2', \
'glue', \
'dplyr', \
'tidyr', \
'base64enc', \
'yaml', \
'car', \
'httr' \
), repos='https://cloud.r-project.org/', Ncpus=2)"
# 安全加固:创建非特权用户
RUN useradd -m -s /bin/bash appuser
WORKDIR /app
# 复制应用代码
COPY plumber.R plumber.R
COPY utils/ utils/
COPY tools/ tools/
COPY tests/ tests/
# 设置目录权限
RUN chown -R appuser:appuser /app
# 切换到非特权用户
USER appuser
EXPOSE 8080
# 环境变量
ENV DEV_MODE="false"
# 健康检查
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
# 启动服务
CMD ["R", "-e", "plumber::plumb('plumber.R')$run(host='0.0.0.0', port=8080)"]
```
### 3.2 系统依赖说明
| 依赖包 | 用途 |
|--------|------|
| `libcurl4-openssl-dev` | httr 包HTTP 请求) |
| `libssl-dev` | openssl 包(加密) |
| `libxml2-dev` | xml2 包XML 解析) |
| `libsodium-dev` | sodium 包(加密) |
| `zlib1g-dev` | httpuv 包Web 服务器) |
| `libnlopt-dev` | nloptr 包(优化算法) |
| `liblapack-dev` | 线性代数计算 |
| `libblas-dev` | 基础线性代数 |
| `gfortran` | Fortran 编译器(部分 R 包需要) |
| `cmake` | nloptr 包构建 |
| `curl` | 健康检查 |
### 3.3 构建命令
```bash
# 本地构建
cd r-statistics-service
docker build -t ssa-r-statistics:1.0.1 .
# 查看镜像
docker images ssa-r-statistics
# 预期输出
REPOSITORY TAG IMAGE ID CREATED SIZE
ssa-r-statistics 1.0.1 xxxxxxxxxxxx x minutes ago 1.81GB
```
### 3.4 构建时间参考
| 阶段 | 耗时 |
|------|------|
| 基础镜像下载 | ~2 分钟(首次) |
| 系统依赖安装 | ~1 分钟 |
| R 包安装 | ~6 分钟 |
| **总计** | **~9 分钟** |
---
## 4. 部署指南
### 4.1 开发环境
使用 docker-compose
```yaml
# r-statistics-service/docker-compose.yml
services:
ssa-r-service:
build: .
container_name: ssa-r-statistics
ports:
- "8082:8080" # 主机8082 → 容器8080REDCap占用8080/8081
environment:
- DEV_MODE=true
volumes:
# 开发环境挂载:支持热重载
- ./plumber.R:/app/plumber.R # ⚠️ 重要API 入口也需要挂载
- ./tools:/app/tools
- ./utils:/app/utils
- ./tests:/app/tests
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
```
启动命令:
```bash
cd r-statistics-service
docker-compose up -d
```
#### 4.1.1 热重载机制详解
| 文件类型 | 热重载支持 | 说明 |
|----------|-----------|------|
| `tools/*.R` | ✅ 自动 | DEV_MODE=true 时每次请求重新加载 |
| `utils/*.R` | ⚠️ 需重启 | 服务启动时加载,修改后需 `docker-compose restart` |
| `plumber.R` | ⚠️ 需重启 | API 路由定义,修改后需 `docker-compose restart` |
**最佳实践:**
- 开发新工具时,只需修改 `tools/` 目录,无需重启
- 修改 `utils/``plumber.R` 后,执行 `docker-compose restart`
- 添加新的 API 端点后,需要 `docker-compose up -d --force-recreate`
### 4.2 生产环境 (SAE)
```yaml
# SAE 配置
容器镜像: registry.cn-beijing.aliyuncs.com/aiclinical/ssa-r-statistics:1.0.1
实例规格: 2 vCPU, 4 GB
最小实例: 1
最大实例: 5
端口: 8080
环境变量:
DEV_MODE: "false"
```
### 4.3 环境变量
| 变量 | 默认值 | 说明 |
|------|--------|------|
| `DEV_MODE` | `false` | 开发模式(启用热重载,每次请求重新加载工具脚本) |
> **说明**:开发环境和生产环境都使用真实 OSS无需 Mock 数据。
> - 开发环境:`ai-clinical-data-dev` bucket
> - 生产环境:`ai-clinical-data` bucket
### 4.4 端口配置
| 环境 | 主机端口 | 容器端口 | 说明 |
|------|----------|----------|------|
| **开发环境** | 8082 | 8080 | 避免与 REDCap 8080/8081 冲突 |
| **生产环境 (SAE)** | 8080 | 8080 | 云端无端口冲突 |
> **注意**Node.js 后端通过 `R_SERVICE_URL` 环境变量配置 R 服务地址,默认值为 `http://localhost:8082`。
---
## 5. API 参考
### 5.1 健康检查
```http
GET /health
```
**响应:**
```json
{
"status": "ok",
"timestamp": "2026-02-19 08:00:00",
"version": "1.0.1",
"dev_mode": true,
"tools_loaded": 1
}
```
### 5.2 工具列表
```http
GET /api/v1/tools
```
**响应:**
```json
{
"status": "ok",
"tools": [
"chi_square",
"correlation",
"descriptive",
"logistic_binary",
"mann_whitney",
"t_test_ind",
"t_test_paired"
],
"count": 7
}
```
#### 已实现的统计工具Phase 2A
| tool_code | 名称 | 场景 |
|-----------|------|------|
| `ST_T_TEST_IND` | 独立样本 T 检验 | 两组连续变量比较(正态) |
| `ST_MANN_WHITNEY` | Mann-Whitney U | 两组连续变量比较(非参数) |
| `ST_T_TEST_PAIRED` | 配对 T 检验 | 前后对比 |
| `ST_CHI_SQUARE` | 卡方检验 | 分类变量关联 |
| `ST_CORRELATION` | 相关分析 | Pearson/Spearman 相关 |
| `ST_LOGISTIC_BINARY` | 二元 Logistic 回归 | 多因素分析 |
| `ST_DESCRIPTIVE` | 描述性统计 | 基线表、数据概况 |
### 5.3 执行技能
```http
POST /api/v1/skills/{tool_code}
Content-Type: application/json
```
**请求体:**
```json
{
"data_source": {
"type": "inline",
"data": [...]
},
"params": {
"group_var": "group",
"value_var": "value"
},
"guardrails": {
"check_normality": true
}
}
```
**成功响应:**
```json
{
"status": "success",
"message": "分析完成",
"warnings": null,
"results": {
"method": "Welch Two Sample t-test",
"statistic": -5.196,
"df": 5.98,
"p_value": 0.002,
"p_value_fmt": "p = .002"
},
"plots": ["data:image/png;base64,..."],
"trace_log": [...],
"reproducible_code": "..."
}
```
**错误响应:**
```json
{
"status": "error",
"error_code": "E001",
"error_type": "business",
"message": "列名 'xxx' 在数据中不存在",
"user_hint": "请检查变量名是否拼写正确"
}
```
### 5.4 JIT 护栏检查Phase 2A 新增)
在执行核心统计工具前,调用此端点检验统计假设(正态性、方差齐性等)。
```http
POST /api/v1/guardrails/jit
Content-Type: application/json
```
**请求体:**
```json
{
"data_source": {
"type": "inline",
"data": [...]
},
"tool_code": "ST_T_TEST_IND",
"params": {
"group_var": "sex",
"value_var": "age"
}
}
```
**响应:**
```json
{
"status": "success",
"checks": [
{
"check_name": "正态性检验 (组: 1)",
"passed": true,
"p_value": 0.234,
"recommendation": "满足正态性"
},
{
"check_name": "方差齐性检验 (Levene)",
"passed": false,
"p_value": 0.012,
"recommendation": "建议使用 Welch 校正"
}
],
"suggested_tool": "ST_MANN_WHITNEY",
"can_proceed": true,
"all_checks_passed": false
}
```
**使用场景:**
- 工作流执行器在调用核心统计方法前,先调用 JIT 护栏
- 根据 `suggested_tool` 自动切换到更合适的方法
-`checks` 结果展示给用户
---
## 6. 开发指南
### 6.1 添加新工具
1.`tools/` 目录创建 R 脚本:
```r
# tools/my_analysis.R
#' @tool_code ST_MY_ANALYSIS
#' @name 我的分析工具
#' @version 1.0.0
#' @description 工具描述
#' @author SSA-Pro Team
library(glue)
library(ggplot2)
library(base64enc)
run_analysis <- function(input) {
# ===== 初始化日志 =====
logs <- c()
log_add <- function(msg) { logs <<- c(logs, paste0("[", Sys.time(), "] ", msg)) }
# ===== 数据加载 =====
log_add("开始加载输入数据")
df <- tryCatch(
load_input_data(input),
error = function(e) {
log_add(paste("数据加载失败:", e$message))
return(NULL)
}
)
if (is.null(df)) {
return(make_error(ERROR_CODES$E100_INTERNAL_ERROR, details = "数据加载失败"))
}
log_add(glue("数据加载成功: {nrow(df)} 行, {ncol(df)} 列"))
# ===== 参数提取 =====
p <- input$params
my_var <- p$my_var
# ===== 参数校验 =====
if (!(my_var %in% names(df))) {
return(make_error(ERROR_CODES$E001_COLUMN_NOT_FOUND, col = my_var))
}
# ===== 护栏检查 =====
guardrail_results <- list()
warnings_list <- c()
sample_check <- check_sample_size(nrow(df), min_required = 10, action = ACTION_WARN)
guardrail_results <- c(guardrail_results, list(sample_check))
guardrail_status <- run_guardrail_chain(guardrail_results)
if (guardrail_status$status == "blocked") {
return(list(status = "blocked", message = guardrail_status$reason, trace_log = logs))
}
# ===== 核心计算 =====
log_add("执行分析...")
# result <- your_analysis_function(df, ...)
# ===== 生成图表 =====
plot_base64 <- tryCatch({
p <- ggplot(df, aes(x = df[[my_var]])) + geom_histogram() + theme_minimal()
tmp_file <- tempfile(fileext = ".png")
ggsave(tmp_file, p, width = 7, height = 5, dpi = 100)
base64_str <- base64encode(tmp_file)
unlink(tmp_file)
paste0("data:image/png;base64,", base64_str)
}, error = function(e) NULL)
# ===== 生成可复现代码 =====
reproducible_code <- glue('
# SSA-Pro 自动生成代码
# 工具: 我的分析工具
# 时间: {Sys.time()}
# ================================
df <- read.csv("data.csv")
# 你的分析代码...
')
# ===== 返回结果 =====
log_add("分析完成")
return(list(
status = "success",
message = "分析完成",
warnings = if (length(warnings_list) > 0) warnings_list else NULL,
results = list(
# 统计结果(使用 jsonlite::unbox 保证单值不被包装成数组)
statistic = jsonlite::unbox(1.234),
p_value = jsonlite::unbox(0.05),
p_value_fmt = format_p_value(0.05)
),
plots = if (!is.null(plot_base64)) list(plot_base64) else list(),
trace_log = logs,
reproducible_code = as.character(reproducible_code)
))
}
```
2. **开发模式**:修改 `tools/` 下的文件后,无需重启,下次请求自动加载
3. 测试:
```bash
curl -X POST http://localhost:8082/api/v1/skills/ST_MY_ANALYSIS \
-H "Content-Type: application/json" \
-d '{"data_source": {"type": "inline", "data": [{"x": 1}, {"x": 2}]}, "params": {"my_var": "x"}}'
```
### 6.2 工具命名规范
| 项目 | 规范 |
|------|------|
| 文件名 | 小写下划线:`t_test_ind.R` |
| tool_code | 大写下划线:`ST_T_TEST_IND` |
| 入口函数 | 固定名称:`run_analysis` |
### 6.3 结果格式规范
```r
return(list(
status = "success" | "error" | "blocked",
message = "...",
warnings = c("...") | NULL,
results = list(
# 统计结果
),
plots = list(
"data:image/png;base64,..."
),
trace_log = c("..."),
reproducible_code = "..."
))
```
---
## 7. 运维指南
### 7.1 日志查看
```bash
# 实时日志
docker logs -f ssa-r-statistics
# 最近 100 行
docker logs --tail 100 ssa-r-statistics
```
### 7.2 性能监控
```bash
# 容器资源使用
docker stats ssa-r-statistics
```
### 7.3 重启服务
```bash
# 开发环境
docker-compose restart
# 生产环境 (SAE)
通过 SAE 控制台重启实例
```
### 7.4 镜像更新
```bash
# 1. 构建新镜像
docker build -t ssa-r-statistics:1.0.2 .
# 2. 推送到镜像仓库
docker tag ssa-r-statistics:1.0.2 registry.cn-beijing.aliyuncs.com/aiclinical/ssa-r-statistics:1.0.2
docker push registry.cn-beijing.aliyuncs.com/aiclinical/ssa-r-statistics:1.0.2
# 3. 更新 SAE 部署
```
---
## 8. 常见问题
### Q1: 构建时 httpuv 安装失败
**错误:** `fatal error: zlib.h: No such file or directory`
**解决:** 添加 `zlib1g-dev` 到系统依赖
### Q2: 构建时 nloptr 安装失败
**错误:** `CMAKE NOT FOUND`
**解决:** 添加 `cmake` 到系统依赖
### Q3: /tmp 权限问题
**错误:** `cannot open file '/tmp/Rtmpxxx': No such file or directory`
**解决:** 不要在启动命令中清理 /tmp
### Q4: DEV_MODE 热重载不生效
**原因:** 没有挂载 volumes
**解决:**
```yaml
volumes:
- ./tools:/app/tools
```
### Q5: 容器启动后无法访问
**检查:**
1. 端口映射是否正确
2. 健康检查是否通过
3. 查看容器日志
### Q6: 数据加载失败inline 模式)
**错误:** `内部错误: 数据加载失败`
**原因:** 数据格式不正确,或数据为空
**解决:**
1. 确保 `data_source.data` 是有效的 JSON 数组
2. 行格式:`[{"col1": val1}, {"col1": val2}]`
3. 检查是否有空数据或全 NA 列
### Q7: R 脚本语法错误
**错误:** `unexpected symbol``lexical error`
**常见原因:**
1. `glue()` 字符串中使用 `\'` 转义(应直接使用 `'`
2. 中文注释编码问题
3. 代码块中的花括号不匹配
**解决:**
```r
# 错误glue 中的转义
glue("# Cramer\'s V = ...") # ❌
# 正确:直接使用单引号或避免
glue("# Cramer V = ...") # ✅
```
### Q8: JSON 序列化失败
**错误:** `No method asJSON S3 class: table`
**原因:** R 的 `table` 对象无法直接序列化为 JSON
**解决:**
```r
# 错误
observed = as.matrix(contingency_table) # ❌ 可能保留 table 属性
# 正确:显式转换为纯数值矩阵
observed = matrix(
as.numeric(contingency_table),
nrow = nrow(contingency_table),
ncol = ncol(contingency_table)
) # ✅
```
### Q9: 新端点返回 404
**原因:** 修改 `plumber.R` 后未重启服务
**解决:**
```bash
# 修改 plumber.R 后必须重启
docker-compose restart
# 如果修改了 docker-compose.yml如添加新 volume
docker-compose up -d --force-recreate
```
### Q10: 变量类型判断错误missing value where TRUE/FALSE needed
**原因:** 对包含 NA 的数据进行布尔比较
**解决:**
```r
# 错误
if (var_type == "numeric") { ... } # var_type 可能是 NA
# 正确
if (identical(var_type, "numeric")) { ... } # ✅ 处理 NA
```
---
## 9. 测试指南
### 9.1 单工具测试
```bash
# 测试 T 检验
curl -s -X POST "http://localhost:8082/api/v1/skills/ST_T_TEST_IND" \
-H "Content-Type: application/json" \
-d '{
"data_source": {
"type": "inline",
"data": [
{"group": "A", "value": 23}, {"group": "A", "value": 25},
{"group": "B", "value": 30}, {"group": "B", "value": 32}
]
},
"params": {"group_var": "group", "value_var": "value"}
}'
```
### 9.2 健康检查
```bash
curl -s http://localhost:8082/health | jq
```
### 9.3 端到端测试脚本
项目提供了完整的端到端测试脚本:
```bash
cd docs/03-业务模块/SSA-智能统计分析/05-测试文档
node run_e2e_test.js
```
测试覆盖:
- 7 个统计工具
- JIT 护栏检查
- 数据加载(行格式/列格式)
---
## 附录:文件结构
```
r-statistics-service/
├── Dockerfile # 生产镜像定义
├── docker-compose.yml # 开发环境编排(含 volume 挂载)
├── renv.lock # R 包版本锁定(备用)
├── .Rprofile # R 启动配置(备用)
├── plumber.R # API 入口(含 JIT 护栏端点)
├── utils/
│ ├── data_loader.R # 数据加载(支持行格式/列格式)
│ ├── guardrails.R # 统计护栏 + JIT 检查
│ ├── error_codes.R # 错误映射
│ └── result_formatter.R # 结果格式化
├── tools/ # 统计工具Phase 2A: 7 个)
│ ├── t_test_ind.R # 独立样本 T 检验
│ ├── t_test_paired.R # 配对 T 检验
│ ├── mann_whitney.R # Mann-Whitney U 检验
│ ├── chi_square.R # 卡方检验
│ ├── correlation.R # 相关分析
│ ├── logistic_binary.R # 二元 Logistic 回归
│ └── descriptive.R # 描述性统计
├── tests/
│ └── fixtures/
│ └── normal_data.csv # 测试数据
├── metadata/ # 工具元数据(预留)
└── templates/ # 解释模板(预留)
```
---
## 更新日志
| 版本 | 日期 | 更新内容 |
|------|------|----------|
| v1.1 | 2026-02-20 | Phase 2A 完成7 个统计工具、JIT 护栏、热重载说明、常见问题补充 |
| v1.0 | 2026-02-19 | 初始版本架构设计、部署指南、T 检验工具 |
---
**文档结束**