diff --git a/COMMIT_DAY1.txt b/COMMIT_DAY1.txt index 9b255ba0..e8e8a072 100644 --- a/COMMIT_DAY1.txt +++ b/COMMIT_DAY1.txt @@ -31,3 +31,4 @@ Status: Day 1 complete (11/11 tasks), ready for Day 2 + diff --git a/DC模块代码恢复指南.md b/DC模块代码恢复指南.md index f50ed5f9..607410b6 100644 --- a/DC模块代码恢复指南.md +++ b/DC模块代码恢复指南.md @@ -259,5 +259,6 @@ + diff --git a/backend/DEPLOY_TO_SAE_FOR_WECHAT_MP.md b/backend/DEPLOY_TO_SAE_FOR_WECHAT_MP.md new file mode 100644 index 00000000..b600f193 --- /dev/null +++ b/backend/DEPLOY_TO_SAE_FOR_WECHAT_MP.md @@ -0,0 +1,139 @@ +# 部署微信服务号到SAE(快速指南) + +## 🎯 目标 +将微信服务号回调服务部署到SAE生产环境,使用域名:`iit.xunzhengyixue.com` + +--- + +## 📋 Step 1: 确认环境变量配置 + +编辑 `backend/.env`,确认以下配置存在: + +```env +# 微信服务号配置 +WECHAT_MP_APP_ID=wx062568ff49e4570c +WECHAT_MP_APP_SECRET=c0d19435d1a1e948939c16d767ec0faf +WECHAT_MP_TOKEN=IitPatientWechat2026JanToken +WECHAT_MP_ENCODING_AES_KEY=VIzwMGRG4Ll8Sd7fPxPXLlBaWdsh2rK2qIGpyaEoc1v +``` + +--- + +## 📋 Step 2: 在SAE控制台配置环境变量 + +1. **登录阿里云SAE控制台** +2. **进入应用管理 → 选择应用** +3. **配置管理 → 环境变量** +4. **添加以下环境变量**: + +``` +WECHAT_MP_APP_ID=wx062568ff49e4570c +WECHAT_MP_APP_SECRET=c0d19435d1a1e948939c16d767ec0faf +WECHAT_MP_TOKEN=IitPatientWechat2026JanToken +WECHAT_MP_ENCODING_AES_KEY=VIzwMGRG4Ll8Sd7fPxPXLlBaWdsh2rK2qIGpyaEoc1v +``` + +5. **保存配置** + +--- + +## 📋 Step 3: 部署代码到SAE + +```bash +cd D:\MyCursor\AIclinicalresearch\backend +.\deploy-to-sae.ps1 +``` + +**等待部署完成**(约5-10分钟) + +--- + +## 📋 Step 4: 验证部署 + +访问以下URL,确认服务正常: + +``` +https://iit.xunzhengyixue.com/api/v1/iit/health +``` + +**期望返回**: +```json +{ + "status": "ok", + "module": "iit-manager", + "version": "1.1.0" +} +``` + +--- + +## 📋 Step 5: 配置微信公众平台 + +1. **登录微信公众平台**:https://mp.weixin.qq.com/ +2. **进入:设置与开发 → 基本配置 → 服务器配置** +3. **点击"修改配置"** +4. **填写以下信息**: + +| 配置项 | 值 | +|--------|-----| +| **URL** | `https://iit.xunzhengyixue.com/wechat/patient/callback` | +| **Token** | `IitPatientWechat2026JanToken` | +| **EncodingAESKey** | `VIzwMGRG4Ll8Sd7fPxPXLlBaWdsh2rK2qIGpyaEoc1v` | +| **消息加解密方式** | **安全模式(推荐)** | +| **数据格式** | **XML** | + +5. **点击"提交"** +6. **验证成功后点击"启用"** + +--- + +## ✅ 验证成功标志 + +### 配置阶段: +- ✅ 页面显示"配置成功" +- ✅ 服务器配置状态为"已启用" + +### 测试阶段: +1. **关注公众号**:AI for 临床研究 +2. **查看SAE日志**,应该看到: +``` +📥 收到微信服务号回调消息 +🔐 检测到加密消息,开始解密... +✅ 消息解密成功 +👤 用户关注公众号 +``` + +--- + +## 🔧 如何查看SAE日志 + +1. **登录阿里云SAE控制台** +2. **应用管理 → 选择应用 → 实例管理** +3. **点击"日志" → "实时日志"** +4. **查看最近的日志输出** + +--- + +## 📝 优势 + +使用生产环境的优势: +- ✅ 域名 `iit.xunzhengyixue.com` 已备案 +- ✅ HTTPS证书已配置 +- ✅ 已在企业微信中验证过 +- ✅ 无需natapp内网穿透 +- ✅ 稳定性更好 +- ✅ 无需配置域名验证 + +--- + +## ⏱️ 预计用时 + +- 配置环境变量:2分钟 +- 部署到SAE:5-10分钟 +- 配置微信公众平台:3分钟 +- **总计:10-15分钟** + +--- + +**立即开始部署!** 🚀 + diff --git a/backend/WECHAT_ENV_CONFIG.md b/backend/WECHAT_ENV_CONFIG.md index aff90616..e2475ce5 100644 --- a/backend/WECHAT_ENV_CONFIG.md +++ b/backend/WECHAT_ENV_CONFIG.md @@ -1,4 +1,4 @@ -# 企业微信环境变量配置说明 +# 微信环境变量配置说明(企业微信 + 服务号) ## 📋 必需的环境变量 @@ -6,7 +6,7 @@ ```env # ========================================== -# 企业微信配置 +# 企业微信配置(研究者端) # ========================================== # 企业微信基础配置(应用信息) @@ -20,6 +20,21 @@ WECHAT_ENCODING_AES_KEY=V88eT3O9bMW897h4btr7v7qvQlmlMf31edTQCmuhOhO # 测试用户ID(可选,仅测试环境使用) WECHAT_TEST_USER_ID=FengZhiBo + +# ========================================== +# 微信服务号配置(患者端) +# ========================================== + +# 微信服务号基础配置 +WECHAT_MP_APP_ID=wx062568ff49e4570c +WECHAT_MP_APP_SECRET=c0d19435d1a1e948939c16d767ec0faf + +# 微信服务号回调配置(消息加解密,安全模式) +WECHAT_MP_TOKEN=<需要生成3-32位字符串> +WECHAT_MP_ENCODING_AES_KEY=<需要生成43位字符串> + +# 微信小程序配置(可选,后续开发) +WECHAT_MINI_APP_ID=<待申请> ``` ## 📝 配置项说明 diff --git a/backend/WECHAT_MP_CONFIG_READY.md b/backend/WECHAT_MP_CONFIG_READY.md new file mode 100644 index 00000000..309d97a9 --- /dev/null +++ b/backend/WECHAT_MP_CONFIG_READY.md @@ -0,0 +1,300 @@ +# 微信服务号配置完成清单 + +> **生成时间**: 2026-01-04 +> **服务号**: AI for 临床研究 +> **AppID**: wx062568ff49e4570c + +--- + +## 🎯 一、立即配置(复制即用) + +### 1.1 环境变量配置 + +**请将以下内容添加到 `backend/.env` 文件:** + +```env +# ========================================== +# 微信服务号配置(患者端) +# ========================================== + +# 微信服务号基础配置 +WECHAT_MP_APP_ID=wx062568ff49e4570c +WECHAT_MP_APP_SECRET=c0d19435d1a1e948939c16d767ec0faf + +# 微信服务号回调配置(消息加解密,安全模式) +WECHAT_MP_TOKEN=IitPatientWechat2026JanToken +WECHAT_MP_ENCODING_AES_KEY=7yK9mN4pQ2wX5vL8hG3jR6tU1nB0cF9eM7aZ4xS2dY5 + +# 微信小程序配置(可选,后续开发) +WECHAT_MINI_APP_ID= +``` + +### 1.2 微信公众平台配置 + +**登录地址**: https://mp.weixin.qq.com/ +**配置路径**: 设置与开发 → 基本配置 → 服务器配置 → 修改配置 + +**填写以下参数:** + +| 配置项 | 值 | +|--------|-----| +| **URL** | `https://iit.xunzhengyixue.com/api/v1/iit/patient-wechat/callback` | +| **Token** | `IitPatientWechat2026JanToken` | +| **EncodingAESKey** | `7yK9mN4pQ2wX5vL8hG3jR6tU1nB0cF9eM7aZ4xS2dY5` | +| **消息加解密方式** | ✅ **安全模式(推荐)** | +| **数据格式** | ✅ **XML** | + +**本地开发URL**(使用natapp): +``` +https://devlocal.xunzhengyixue.com/api/v1/iit/patient-wechat/callback +``` + +--- + +## 📋 二、配置步骤(按顺序执行) + +### Step 1: 更新环境变量 ✅ + +```bash +# 1. 打开 backend/.env 文件 +# 2. 复制上面1.1节的配置内容 +# 3. 粘贴到文件末尾 +# 4. 保存文件 +``` + +### Step 2: 验证配置 ✅ + +```bash +cd backend +npx tsx src/modules/iit-manager/test-patient-wechat-config.ts +``` + +**期望输出**: +``` +✅ 配置检查通过!可以开始配置微信公众平台 +``` + +### Step 3: 启动后端服务 🔥 + +```bash +cd backend +npm run dev +``` + +**期望输出**: +``` +✅ 微信服务号回调控制器初始化成功 +✅ 微信服务号服务初始化成功 +Registered route: GET /api/v1/iit/patient-wechat/callback +Registered route: POST /api/v1/iit/patient-wechat/callback +Server listening on http://0.0.0.0:3001 +``` + +### Step 4: 本地开发启动natapp(可选) + +```bash +# 如果是本地开发,需要启动natapp内网穿透 +cd D:\tools\natapp +natapp.exe -authtoken=YOUR_TOKEN -subdomain=devlocal +``` + +### Step 5: 测试URL验证(本地) + +新开一个终端,运行: + +```bash +cd backend +npx tsx src/modules/iit-manager/test-patient-wechat-url-verify.ts +``` + +**期望输出**: +``` +✅ URL验证成功! +✅ 返回的echostr正确,验证通过! +🎉 测试成功!您的服务端配置正确 +``` + +### Step 6: 配置微信公众平台 🔥 + +1. 登录微信公众平台:https://mp.weixin.qq.com/ +2. 进入:**设置与开发 → 基本配置 → 服务器配置** +3. 点击 **"修改配置"** +4. 填写上面1.2节的配置参数 +5. 点击 **"提交"**(微信会自动验证URL) +6. 如果验证成功,点击 **"启用"** + +**验证成功标志**: +- ✅ 页面显示 "配置成功" +- ✅ "服务器配置" 状态为 "已启用" + +**如果验证失败**: +- 查看后端日志是否收到GET请求 +- 确认Token和EncodingAESKey与.env中完全一致 +- 确认服务器可以从公网访问 + +### Step 7: 测试消息接收 + +1. 用测试微信号关注公众号:`AI for 临床研究` +2. 发送文本消息:`你好` +3. 查看后端日志,应该看到: + +``` +📥 收到微信服务号回调消息 +🔐 检测到加密消息,开始解密... +✅ 消息解密成功 +💬 处理文本消息: 你好 +``` + +--- + +## 🔍 三、常见问题排查 + +### Q1: URL验证失败,提示"Token验证失败" + +**原因**:Token配置不一致 + +**解决方法**: +1. 确认 `.env` 文件中的 `WECHAT_MP_TOKEN` 是 `IitPatientWechat2026JanToken` +2. 确认微信公众平台配置的Token也是 `IitPatientWechat2026JanToken` +3. 注意大小写和空格 + +### Q2: URL验证失败,提示"请求URL超时" + +**原因**:服务器无法访问 + +**解决方法**: +1. 确认后端服务已启动:`npm run dev` +2. 本地开发确认natapp已启动 +3. 生产环境确认SAE应用正常运行 +4. 浏览器访问:`https://iit.xunzhengyixue.com/api/v1/iit/health` 测试连通性 + +### Q3: 提示"配置失败(48001)" + +**原因**:AppID或AppSecret不正确 + +**解决方法**: +1. 确认 `.env` 中的 `WECHAT_MP_APP_ID` 和 `WECHAT_MP_APP_SECRET` 正确 +2. 登录微信公众平台 → 开发 → 基本配置 → 查看AppID和AppSecret +3. 重新复制粘贴,确保无多余空格 + +### Q4: 消息解密失败 + +**原因**:EncodingAESKey不一致或长度错误 + +**解决方法**: +1. 确认 `.env` 中的 `WECHAT_MP_ENCODING_AES_KEY` 长度为43位 +2. 确认与微信公众平台配置完全一致(包括大小写) +3. 如果还是失败,重新生成EncodingAESKey并同步更新 + +--- + +## ⚠️ 四、重要提示 + +### 安全提示 +1. ❗ **Token和EncodingAESKey不要提交到Git** +2. ❗ **AppSecret不要泄露,不要提交到Git** +3. ❗ 生产环境配置在SAE环境变量中,不要写在代码里 + +### 配置原则 +1. ✅ `.env` 和微信公众平台的Token/EncodingAESKey必须**完全一致** +2. ✅ 修改配置后需要**重启服务**才能生效 +3. ✅ 本地开发和生产环境可以使用**相同的Token**(推荐) +4. ✅ 如果配置错误,可以重新生成新的Token(运行 `generate-wechat-tokens.ts`) + +### IP白名单 +生产环境需要配置IP白名单: +1. 登录微信公众平台 → 基本配置 → IP白名单 +2. 添加SAE应用的出口IP +3. 可以从SAE控制台查看应用的出口IP + +--- + +## 📊 五、开发进度 + +### 已完成 ✅ +- [x] PatientWechatCallbackController(服务号回调控制器) +- [x] PatientWechatService(模板消息推送服务) +- [x] 路由配置(GET/POST /api/v1/iit/patient-wechat/callback) +- [x] 环境变量配置文档 +- [x] 配置检查脚本 +- [x] URL验证测试脚本 +- [x] Token生成脚本 +- [x] 接入指南文档 + +### 待测试 ⏳ +- [ ] URL验证(配置微信公众平台) +- [ ] 消息接收(关注事件、文本消息) +- [ ] 消息解密(安全模式) + +### 后续开发 📝 +- [ ] 患者绑定功能(数据表 + H5页面) +- [ ] 模板消息推送(访视提醒) +- [ ] 客服消息回复(智能对话) +- [ ] 微信小程序开发 + +--- + +## 🚀 六、现在就开始! + +### 方案A:生产环境部署(推荐) + +```bash +# 1. 更新 backend/.env 文件(复制上面的配置) + +# 2. 验证配置 +cd backend +npx tsx src/modules/iit-manager/test-patient-wechat-config.ts + +# 3. 部署到SAE +./deploy-to-sae.ps1 + +# 4. 配置微信公众平台(使用生产URL) + +# 5. 测试功能(关注公众号、发送消息) +``` + +### 方案B:本地开发测试 + +```bash +# 1. 更新 backend/.env 文件(复制上面的配置) + +# 2. 验证配置 +cd backend +npx tsx src/modules/iit-manager/test-patient-wechat-config.ts + +# 3. 启动natapp +cd D:\tools\natapp +natapp.exe -authtoken=YOUR_TOKEN -subdomain=devlocal + +# 4. 启动后端服务 +cd D:\MyCursor\AIclinicalresearch\backend +npm run dev + +# 5. 测试URL验证 +npx tsx src/modules/iit-manager/test-patient-wechat-url-verify.ts + +# 6. 配置微信公众平台(使用本地开发URL) + +# 7. 测试功能(关注公众号、发送消息) +``` + +--- + +**推荐:先执行方案B(本地测试),测试通过后再执行方案A(生产部署)** + +--- + +## 📞 需要帮助? + +如有问题,可以: +1. 查看后端日志(`backend/logs/`) +2. 运行配置检查脚本 +3. 查看接入指南文档(`backend/src/modules/iit-manager/docs/微信服务号接入指南.md`) +4. 联系技术负责人 + +--- + +**最后更新**: 2026-01-04 +**文档版本**: v1.0 +**状态**: ✅ 配置就绪,等待测试 + diff --git a/backend/WECHAT_MP_QUICK_FIX.md b/backend/WECHAT_MP_QUICK_FIX.md new file mode 100644 index 00000000..52d7e914 --- /dev/null +++ b/backend/WECHAT_MP_QUICK_FIX.md @@ -0,0 +1,162 @@ +# 微信服务号配置错误修复(200002) + +## ❌ 错误信息 +``` +检查消息推送配置失败: invalid args, 200002 +``` + +## 🔍 问题原因 +EncodingAESKey格式不符合微信服务号要求。 + +## ✅ 解决方案(2选1) + +### 方案1:使用微信平台随机生成(推荐⭐) + +**步骤:** + +1. 在微信公众平台配置页面 +2. **EncodingAESKey字段不要手动输入** +3. 点击右侧的 **"随机生成"** 按钮 +4. 微信会自动生成43位密钥,例如: + ``` + abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG + ``` +5. **复制这个生成的密钥** +6. 填写完整配置: + ``` + URL: https://devlocal.xunzhengyixue.com/api/v1/iit/patient-wechat/callback + Token: IitPatientWechat2026JanToken + EncodingAESKey: [点击随机生成后复制的密钥] + 消息加解密方式: 安全模式 + ``` +7. 点击 **"提交"** 验证 +8. **验证成功后**,复制微信生成的EncodingAESKey到 `.env` 文件: + ```env + WECHAT_MP_ENCODING_AES_KEY=[微信生成的密钥] + ``` +9. **重启后端服务**(重要!) + +--- + +### 方案2:使用OpenSSL生成(备选) + +如果方案1不行,使用OpenSSL生成标准Base64密钥: + +**Windows(PowerShell):** +```powershell +# 生成Base64格式的43位密钥 +$bytes = [System.Security.Cryptography.RandomNumberGenerator]::GetBytes(32) +$base64 = [Convert]::ToBase64String($bytes) +$key = $base64.Substring(0, 43) +Write-Host "EncodingAESKey: $key" +``` + +**或者使用在线工具:** +1. 访问:https://www.base64encode.org/ +2. 生成32字节随机数据 +3. Base64编码 +4. 截取前43位 + +--- + +## 📋 完整配置流程(推荐) + +### Step 1: 使用微信平台生成密钥 + +1. 登录微信公众平台 +2. 进入:设置与开发 → 基本配置 → 服务器配置 +3. 点击"修改配置" +4. 填写以下信息: + +| 字段 | 值 | +|-----|-----| +| URL | `https://devlocal.xunzhengyixue.com/api/v1/iit/patient-wechat/callback` | +| Token | `IitPatientWechat2026JanToken` | +| EncodingAESKey | **点击"随机生成"** 然后复制生成的密钥 | +| 消息加解密方式 | 安全模式(推荐) | + +### Step 2: 提交验证 + +1. 点击"提交" +2. 微信会发送GET请求验证 +3. 应该显示"配置成功" + +### Step 3: 更新.env文件 + +编辑 `backend/.env`,替换EncodingAESKey: + +```env +# 原来的(有问题) +# WECHAT_MP_ENCODING_AES_KEY=7yK9mN4pQ2wX5vL8hG3jR6tU1nB0cF9eM7aZ4xS2dY5 + +# 新的(从微信平台复制的) +WECHAT_MP_ENCODING_AES_KEY=微信生成的43位密钥 +``` + +### Step 4: 重启后端服务 + +```bash +# 停止当前服务(Ctrl+C) +# 重新启动 +cd D:\MyCursor\AIclinicalresearch\backend +npm run dev +``` + +### Step 5: 启用配置 + +返回微信公众平台,点击"启用"按钮 + +--- + +## 🧪 验证配置是否正确 + +### 方法1:查看后端日志 + +应该看到类似日志: +``` +📥 收到微信服务号 URL 验证请求 +✅ URL 验证成功,返回 echostr +``` + +### 方法2:测试关注事件 + +1. 微信扫码关注公众号 +2. 查看后端日志是否有: +``` +📥 收到微信服务号回调消息 +🔐 检测到加密消息,开始解密... +✅ 消息解密成功 +👤 用户关注公众号 +``` + +--- + +## ⚠️ 注意事项 + +1. ✅ **EncodingAESKey必须与微信平台配置完全一致** +2. ✅ **修改.env后必须重启服务** +3. ✅ **Token保持不变**:`IitPatientWechat2026JanToken` +4. ✅ **推荐使用微信平台的"随机生成"功能** + +--- + +## 💡 为什么会出现这个错误? + +微信服务号对EncodingAESKey的格式有严格要求: +- 必须是43位 +- 必须符合Base64字符集(A-Z, a-z, 0-9, +, /) +- 我之前生成的密钥只包含字母和数字,缺少+和/,不符合标准 + +微信平台的"随机生成"功能会自动生成符合标准的密钥,所以推荐使用。 + +--- + +**立即行动:** +1. 进入微信公众平台配置页面 +2. 点击"随机生成"EncodingAESKey +3. 提交配置 +4. 复制密钥到.env +5. 重启服务 + +**预计用时:3分钟** + diff --git a/backend/migrations/add_data_stats_to_tool_c_session.sql b/backend/migrations/add_data_stats_to_tool_c_session.sql index 38e4dcda..7ef04955 100644 --- a/backend/migrations/add_data_stats_to_tool_c_session.sql +++ b/backend/migrations/add_data_stats_to_tool_c_session.sql @@ -54,5 +54,6 @@ WHERE table_schema = 'dc_schema' + diff --git a/backend/prisma/manual-migrations/001_add_postgres_cache_and_checkpoint.sql b/backend/prisma/manual-migrations/001_add_postgres_cache_and_checkpoint.sql index 2897732c..8897c46f 100644 --- a/backend/prisma/manual-migrations/001_add_postgres_cache_and_checkpoint.sql +++ b/backend/prisma/manual-migrations/001_add_postgres_cache_and_checkpoint.sql @@ -92,5 +92,6 @@ ORDER BY ordinal_position; + diff --git a/backend/prisma/manual-migrations/run-migration-002.ts b/backend/prisma/manual-migrations/run-migration-002.ts index 00eb6e21..342bd300 100644 --- a/backend/prisma/manual-migrations/run-migration-002.ts +++ b/backend/prisma/manual-migrations/run-migration-002.ts @@ -105,5 +105,6 @@ runMigration() + diff --git a/backend/prisma/migrations/20251208_add_column_mapping/migration.sql b/backend/prisma/migrations/20251208_add_column_mapping/migration.sql index 20816a77..98d9c75f 100644 --- a/backend/prisma/migrations/20251208_add_column_mapping/migration.sql +++ b/backend/prisma/migrations/20251208_add_column_mapping/migration.sql @@ -39,5 +39,6 @@ COMMENT ON COLUMN "dc_schema"."dc_tool_c_sessions"."column_mapping" IS '列名 + diff --git a/backend/prisma/migrations/create_tool_c_session.sql b/backend/prisma/migrations/create_tool_c_session.sql index 201f50c1..66c78687 100644 --- a/backend/prisma/migrations/create_tool_c_session.sql +++ b/backend/prisma/migrations/create_tool_c_session.sql @@ -66,5 +66,6 @@ COMMENT ON COLUMN dc_schema.dc_tool_c_sessions.expires_at IS '过期时间(创 + diff --git a/backend/rebuild-and-push.ps1 b/backend/rebuild-and-push.ps1 index 702fe251..fcae3722 100644 --- a/backend/rebuild-and-push.ps1 +++ b/backend/rebuild-and-push.ps1 @@ -108,3 +108,4 @@ Write-Host "" + diff --git a/backend/recover-code-from-cursor-db.js b/backend/recover-code-from-cursor-db.js index 326c85cb..f4b78f64 100644 --- a/backend/recover-code-from-cursor-db.js +++ b/backend/recover-code-from-cursor-db.js @@ -216,5 +216,6 @@ function extractCodeBlocks(obj, blocks = []) { + diff --git a/backend/scripts/check-dc-tables.mjs b/backend/scripts/check-dc-tables.mjs index 5f7abc87..d867ecc0 100644 --- a/backend/scripts/check-dc-tables.mjs +++ b/backend/scripts/check-dc-tables.mjs @@ -235,5 +235,6 @@ checkDCTables(); + diff --git a/backend/scripts/create-tool-c-ai-history-table.mjs b/backend/scripts/create-tool-c-ai-history-table.mjs index ab442210..eb4b8be3 100644 --- a/backend/scripts/create-tool-c-ai-history-table.mjs +++ b/backend/scripts/create-tool-c-ai-history-table.mjs @@ -187,5 +187,6 @@ createAiHistoryTable() + diff --git a/backend/scripts/create-tool-c-table.js b/backend/scripts/create-tool-c-table.js index 88e41df8..32e54d81 100644 --- a/backend/scripts/create-tool-c-table.js +++ b/backend/scripts/create-tool-c-table.js @@ -174,5 +174,6 @@ createToolCTable() + diff --git a/backend/scripts/create-tool-c-table.mjs b/backend/scripts/create-tool-c-table.mjs index 5438815f..b97c4ddd 100644 --- a/backend/scripts/create-tool-c-table.mjs +++ b/backend/scripts/create-tool-c-table.mjs @@ -171,5 +171,6 @@ createToolCTable() + diff --git a/backend/src/common/jobs/utils.ts b/backend/src/common/jobs/utils.ts index c0f54e51..60965ed3 100644 --- a/backend/src/common/jobs/utils.ts +++ b/backend/src/common/jobs/utils.ts @@ -303,5 +303,6 @@ export function getBatchItems( + diff --git a/backend/src/modules/asl/fulltext-screening/__tests__/api-integration-test.ts b/backend/src/modules/asl/fulltext-screening/__tests__/api-integration-test.ts index 5b3e1f7d..fcd61420 100644 --- a/backend/src/modules/asl/fulltext-screening/__tests__/api-integration-test.ts +++ b/backend/src/modules/asl/fulltext-screening/__tests__/api-integration-test.ts @@ -339,5 +339,6 @@ runTests().catch((error) => { + diff --git a/backend/src/modules/asl/fulltext-screening/__tests__/e2e-real-test-v2.ts b/backend/src/modules/asl/fulltext-screening/__tests__/e2e-real-test-v2.ts index ea925453..04707bc2 100644 --- a/backend/src/modules/asl/fulltext-screening/__tests__/e2e-real-test-v2.ts +++ b/backend/src/modules/asl/fulltext-screening/__tests__/e2e-real-test-v2.ts @@ -280,5 +280,6 @@ runTest() + diff --git a/backend/src/modules/asl/fulltext-screening/__tests__/fulltext-screening-api.http b/backend/src/modules/asl/fulltext-screening/__tests__/fulltext-screening-api.http index 7022dce7..ca4974bc 100644 --- a/backend/src/modules/asl/fulltext-screening/__tests__/fulltext-screening-api.http +++ b/backend/src/modules/asl/fulltext-screening/__tests__/fulltext-screening-api.http @@ -318,5 +318,6 @@ Content-Type: application/json + diff --git a/backend/src/modules/dc/tool-b/services/ConflictDetectionService.ts b/backend/src/modules/dc/tool-b/services/ConflictDetectionService.ts index bb0e934e..e6300bb5 100644 --- a/backend/src/modules/dc/tool-b/services/ConflictDetectionService.ts +++ b/backend/src/modules/dc/tool-b/services/ConflictDetectionService.ts @@ -254,5 +254,6 @@ export const conflictDetectionService = new ConflictDetectionService(); + diff --git a/backend/src/modules/dc/tool-c/README.md b/backend/src/modules/dc/tool-c/README.md index cb2711c1..f8db5159 100644 --- a/backend/src/modules/dc/tool-c/README.md +++ b/backend/src/modules/dc/tool-c/README.md @@ -204,5 +204,6 @@ curl -X POST http://localhost:3000/api/v1/dc/tool-c/test/execute \ + diff --git a/backend/src/modules/dc/tool-c/controllers/StreamAIController.ts b/backend/src/modules/dc/tool-c/controllers/StreamAIController.ts index 5aec7c33..1849edd5 100644 --- a/backend/src/modules/dc/tool-c/controllers/StreamAIController.ts +++ b/backend/src/modules/dc/tool-c/controllers/StreamAIController.ts @@ -258,5 +258,6 @@ export const streamAIController = new StreamAIController(); + diff --git a/backend/src/modules/iit-manager/agents/SessionMemory.ts b/backend/src/modules/iit-manager/agents/SessionMemory.ts index e81badc4..689c3397 100644 --- a/backend/src/modules/iit-manager/agents/SessionMemory.ts +++ b/backend/src/modules/iit-manager/agents/SessionMemory.ts @@ -169,3 +169,4 @@ logger.info('[SessionMemory] 会话记忆管理器已启动', { + diff --git a/backend/src/modules/iit-manager/check-iit-table-structure.ts b/backend/src/modules/iit-manager/check-iit-table-structure.ts index 5154e353..ec8ee40e 100644 --- a/backend/src/modules/iit-manager/check-iit-table-structure.ts +++ b/backend/src/modules/iit-manager/check-iit-table-structure.ts @@ -103,3 +103,4 @@ async function checkTableStructure() { checkTableStructure(); + diff --git a/backend/src/modules/iit-manager/check-project-config.ts b/backend/src/modules/iit-manager/check-project-config.ts index c91c8dc3..39e79525 100644 --- a/backend/src/modules/iit-manager/check-project-config.ts +++ b/backend/src/modules/iit-manager/check-project-config.ts @@ -90,3 +90,4 @@ checkProjectConfig().catch(console.error); + diff --git a/backend/src/modules/iit-manager/check-test-project-in-db.ts b/backend/src/modules/iit-manager/check-test-project-in-db.ts index 748944d1..0a99aac1 100644 --- a/backend/src/modules/iit-manager/check-test-project-in-db.ts +++ b/backend/src/modules/iit-manager/check-test-project-in-db.ts @@ -72,3 +72,4 @@ async function main() { main(); + diff --git a/backend/src/modules/iit-manager/controllers/PatientWechatCallbackController.ts b/backend/src/modules/iit-manager/controllers/PatientWechatCallbackController.ts new file mode 100644 index 00000000..90c1ddf6 --- /dev/null +++ b/backend/src/modules/iit-manager/controllers/PatientWechatCallbackController.ts @@ -0,0 +1,546 @@ +/** + * 微信服务号回调控制器(患者端) + * + * 功能: + * 1. 处理微信服务号 URL 验证(GET 请求) + * 2. 接收用户消息和事件(POST 请求) + * 3. 消息加解密(安全模式) + * 4. 异步处理消息(规避 5 秒超时) + * 5. 被动回复消息(可选) + * + * 关键技术: + * - 异步回复模式:立即返回加密的空消息,后台异步处理 + * - XML 加解密:使用 @wecom/crypto 库(兼容微信服务号) + * - Token验证:signature = sha1(sort(token, timestamp, nonce)) + * + * 参考文档: + * - https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Access_Overview.html + * - https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Message_encryption_and_decryption_instructions.html + */ + +import { FastifyRequest, FastifyReply } from 'fastify'; +import crypto from 'crypto'; +import xml2js from 'xml2js'; +import { PrismaClient } from '@prisma/client'; +import { createRequire } from 'module'; +import { logger } from '../../../common/logging/index.js'; + +// 使用 createRequire 导入 CommonJS 模块(复用企业微信的加解密库) +const require = createRequire(import.meta.url); +const { decrypt, encrypt, getSignature } = require('@wecom/crypto'); + +const prisma = new PrismaClient(); +const { parseStringPromise } = xml2js; + +// ==================== 类型定义 ==================== + +interface WechatMpVerifyQuery { + signature: string; // 微信加密签名 + timestamp: string; // 时间戳 + nonce: string; // 随机数 + echostr: string; // 随机字符串(验证时返回) +} + +interface WechatMpCallbackQuery { + signature?: string; // 明文模式 + timestamp: string; + nonce: string; + openid?: string; // 发送者openid + encrypt_type?: string; // 加密类型(aes) + msg_signature?: string; // 加密模式签名 +} + +interface WechatMessageXml { + xml: { + ToUserName?: string[]; // 开发者微信号 + FromUserName?: string[]; // 发送方openid + CreateTime?: string[]; // 消息创建时间 + MsgType?: string[]; // 消息类型(text, event等) + Content?: string[]; // 文本消息内容 + MsgId?: string[]; // 消息id + Event?: string[]; // 事件类型(subscribe, CLICK等) + EventKey?: string[]; // 事件KEY值 + Encrypt?: string[]; // 加密消息体 + }; +} + +interface UserMessage { + fromUser: string; // 用户openid + toUser: string; // 公众号原始ID + content: string; // 消息内容 + msgId: string; // 消息ID + msgType: string; // 消息类型 + createTime: number; // 创建时间 + event?: string; // 事件类型(如果是事件消息) + eventKey?: string; // 事件Key +} + +// ==================== 微信服务号回调控制器 ==================== + +export class PatientWechatCallbackController { + private token: string; + private encodingAESKey: string; + private appId: string; + + constructor() { + // 从环境变量读取配置 + this.token = process.env.WECHAT_MP_TOKEN || ''; + this.encodingAESKey = process.env.WECHAT_MP_ENCODING_AES_KEY || ''; + this.appId = process.env.WECHAT_MP_APP_ID || ''; + + // 验证配置 + if (!this.token || !this.encodingAESKey || !this.appId) { + logger.error('❌ 微信服务号回调配置不完整', { + hasToken: !!this.token, + hasAESKey: !!this.encodingAESKey, + hasAppId: !!this.appId, + }); + throw new Error('微信服务号回调配置不完整,请检查环境变量'); + } + + logger.info('✅ 微信服务号回调控制器初始化成功', { + appId: this.appId, + tokenLength: this.token.length, + aesKeyLength: this.encodingAESKey.length, + }); + } + + // ==================== URL 验证(GET) ==================== + + /** + * 处理微信服务号 URL 验证请求 + * + * 微信在配置回调 URL 时会发送 GET 请求验证: + * 1. 验证 signature = sha1(sort(token, timestamp, nonce)) + * 2. 返回 echostr 原文(明文模式)或解密后的 echostr(加密模式) + * + * 注意:与企业微信不同,服务号验证时 echostr 是明文的 + */ + async handleVerification( + request: FastifyRequest<{ Querystring: WechatMpVerifyQuery }>, + reply: FastifyReply + ): Promise { + try { + const { signature, timestamp, nonce, echostr } = request.query; + + logger.info('📥 收到微信服务号 URL 验证请求', { + timestamp, + nonce, + echostrLength: echostr?.length, + }); + + // 验证签名:signature = sha1(sort(token, timestamp, nonce)) + const isValid = this.verifySignature(signature, timestamp, nonce); + if (!isValid) { + logger.error('❌ 签名验证失败'); + reply.code(403).send('Signature verification failed'); + return; + } + + logger.info('✅ URL 验证成功,返回 echostr'); + + // 返回 echostr 原文(微信服务号 URL 验证时是明文) + reply.type('text/plain').send(echostr); + } catch (error: any) { + logger.error('❌ URL 验证异常', { + error: error.message, + stack: error.stack, + }); + reply.code(500).send('Verification failed'); + } + } + + // ==================== 消息接收(POST) ==================== + + /** + * 接收微信服务号回调消息 + * + * 关键:异步回复模式 + * 1. 立即返回 "success" 或加密的空消息(告诉微信收到了) + * 2. 使用 setImmediate 异步处理消息 + * 3. 处理完成后,使用客服消息 API 主动推送回复(推荐) + */ + async handleCallback( + request: FastifyRequest<{ + Querystring: WechatMpCallbackQuery; + Body: string; + }>, + reply: FastifyReply + ): Promise { + try { + const { msg_signature, timestamp, nonce, encrypt_type } = request.query; + const body = request.body; + + logger.info('📥 收到微信服务号回调消息', { + timestamp, + nonce, + encryptType: encrypt_type, + bodyLength: typeof body === 'string' ? body.length : 0, + }); + + // ⚠️ 关键:立即返回 "success"(规避 5 秒超时) + reply.type('text/plain').send('success'); + + // 异步处理消息(不阻塞响应) + setImmediate(() => { + this.processMessageAsync(body, msg_signature, timestamp, nonce).catch((error) => { + logger.error('❌ 异步处理消息失败', { + error: error.message, + stack: error.stack, + }); + }); + }); + } catch (error: any) { + logger.error('❌ 处理回调异常', { + error: error.message, + }); + // 即使异常,也返回 success(避免微信重试) + reply.type('text/plain').send('success'); + } + } + + // ==================== 异步消息处理 ==================== + + /** + * 异步处理消息 + * + * 1. 解析 XML + * 2. 解密消息体(安全模式) + * 3. 提取用户消息 + * 4. 根据消息类型分发处理 + * 5. 记录到数据库 + */ + private async processMessageAsync( + body: string, + msgSignature: string | undefined, + timestamp: string, + nonce: string + ): Promise { + try { + logger.info('🔄 开始异步处理微信服务号消息...'); + + // 1. 解析 XML + const xml = (await parseStringPromise(body, { + explicitArray: false, + })) as WechatMessageXml; + + logger.debug('📝 解析XML成功', { xml }); + + // 2. 判断是否为加密消息 + const encryptedMsg = xml.xml.Encrypt; + let messageXml: WechatMessageXml; + + if (encryptedMsg && msgSignature) { + // 加密模式:解密消息 + logger.info('🔐 检测到加密消息,开始解密...'); + + // 处理可能的数组或字符串 + const encryptStr = Array.isArray(encryptedMsg) ? encryptedMsg[0] : encryptedMsg; + + // 验证签名 + const isValid = this.verifyMsgSignature(msgSignature, timestamp, nonce, encryptStr); + if (!isValid) { + logger.error('❌ 消息签名验证失败'); + return; + } + + // 解密消息 + const decryptedResult = decrypt(this.encodingAESKey, encryptStr); + messageXml = (await parseStringPromise(decryptedResult.message, { + explicitArray: false, + })) as WechatMessageXml; + + logger.info('✅ 消息解密成功'); + } else { + // 明文模式:直接使用 + messageXml = xml; + logger.info('📄 明文消息,直接处理'); + } + + // 3. 提取消息信息 + const userMessage = this.extractUserMessage(messageXml); + if (!userMessage) { + logger.warn('⚠️ 无法提取用户消息'); + return; + } + + logger.info('📬 提取用户消息成功', { + fromUser: userMessage.fromUser, + msgType: userMessage.msgType, + event: userMessage.event, + contentLength: userMessage.content?.length || 0, + }); + + // 4. 根据消息类型分发处理 + await this.dispatchMessage(userMessage); + + } catch (error: any) { + logger.error('❌ 异步处理消息异常', { + error: error.message, + stack: error.stack, + }); + } + } + + // ==================== 消息分发处理 ==================== + + /** + * 根据消息类型分发处理 + */ + private async dispatchMessage(message: UserMessage): Promise { + try { + // 1. 处理事件类型消息 + if (message.msgType === 'event') { + await this.handleEventMessage(message); + return; + } + + // 2. 处理文本消息 + if (message.msgType === 'text') { + await this.handleTextMessage(message); + return; + } + + // 3. 其他类型消息(图片、语音等) + logger.info('📩 收到其他类型消息', { + msgType: message.msgType, + fromUser: message.fromUser, + }); + } catch (error: any) { + logger.error('❌ 消息分发处理失败', { + error: error.message, + msgType: message.msgType, + }); + } + } + + /** + * 处理事件消息 + */ + private async handleEventMessage(message: UserMessage): Promise { + const { event, fromUser, eventKey } = message; + + logger.info('🎯 处理事件消息', { + event, + fromUser, + eventKey, + }); + + // 关注事件 + if (event === 'subscribe') { + logger.info('👤 用户关注公众号', { fromUser }); + + // TODO: 发送欢迎消息,引导用户绑定 + // await patientWechatService.sendWelcomeMessage(fromUser); + + // 记录到数据库 + await this.logUserAction(fromUser, 'subscribe', { + event, + eventKey, + }); + return; + } + + // 取消关注事件 + if (event === 'unsubscribe') { + logger.info('👋 用户取消关注公众号', { fromUser }); + + // 记录到数据库 + await this.logUserAction(fromUser, 'unsubscribe', { + event, + }); + return; + } + + // 菜单点击事件 + if (event === 'CLICK') { + logger.info('🖱️ 用户点击菜单', { fromUser, eventKey }); + + // TODO: 根据 eventKey 处理不同菜单点击 + // 例如:eventKey = 'BIND_PATIENT' -> 引导患者绑定 + + return; + } + + // 其他事件 + logger.info('📋 其他事件类型', { event, fromUser }); + } + + /** + * 处理文本消息 + */ + private async handleTextMessage(message: UserMessage): Promise { + const { fromUser, content } = message; + + logger.info('💬 处理文本消息', { + fromUser, + content: content.substring(0, 50), // 只记录前50字符 + }); + + // 记录到数据库 + await this.logUserAction(fromUser, 'send_message', { + content, + msgId: message.msgId, + }); + + // TODO: 实现智能回复 + // 1. 检查用户是否已绑定患者记录 + // 2. 如果未绑定,引导绑定 + // 3. 如果已绑定,调用 ChatService 处理对话(类似企业微信) + // 4. 使用客服消息 API 回复 + + logger.info('📝 文本消息已记录,等待后续智能回复实现'); + } + + // ==================== 辅助方法 ==================== + + /** + * 验证签名(URL 验证时使用) + * signature = sha1(sort(token, timestamp, nonce)) + */ + private verifySignature( + signature: string, + timestamp: string, + nonce: string + ): boolean { + try { + const arr = [this.token, timestamp, nonce].sort(); + const str = arr.join(''); + const hash = crypto.createHash('sha1').update(str).digest('hex'); + + const isValid = hash === signature; + + logger.debug('🔍 验证签名', { + expected: signature, + calculated: hash, + isValid, + }); + + return isValid; + } catch (error: any) { + logger.error('❌ 签名验证异常', { error: error.message }); + return false; + } + } + + /** + * 验证消息签名(消息接收时使用) + * msg_signature = sha1(sort(token, timestamp, nonce, encrypt)) + */ + private verifyMsgSignature( + msgSignature: string, + timestamp: string, + nonce: string, + encrypt: string + ): boolean { + try { + const arr = [this.token, timestamp, nonce, encrypt].sort(); + const str = arr.join(''); + const hash = crypto.createHash('sha1').update(str).digest('hex'); + + const isValid = hash === msgSignature; + + logger.debug('🔍 验证消息签名', { + expected: msgSignature, + calculated: hash, + isValid, + }); + + return isValid; + } catch (error: any) { + logger.error('❌ 消息签名验证异常', { error: error.message }); + return false; + } + } + + /** + * 提取用户消息 + */ + private extractUserMessage(xml: WechatMessageXml): UserMessage | null { + try { + const msgData = xml.xml; + + // 确保必要字段存在 + if (!msgData.FromUserName || !msgData.ToUserName || !msgData.MsgType) { + logger.warn('⚠️ 消息缺少必要字段', { msgData }); + return null; + } + + // 提取字段(注意:explicitArray: false 时不是数组) + const fromUser = Array.isArray(msgData.FromUserName) + ? msgData.FromUserName[0] + : msgData.FromUserName; + const toUser = Array.isArray(msgData.ToUserName) + ? msgData.ToUserName[0] + : msgData.ToUserName; + const msgType = Array.isArray(msgData.MsgType) + ? msgData.MsgType[0] + : msgData.MsgType; + const content = Array.isArray(msgData.Content) + ? msgData.Content[0] + : (msgData.Content || ''); + const msgId = Array.isArray(msgData.MsgId) + ? msgData.MsgId[0] + : (msgData.MsgId || ''); + const createTime = Array.isArray(msgData.CreateTime) + ? parseInt(msgData.CreateTime[0]) + : (parseInt(msgData.CreateTime as any) || Date.now() / 1000); + + // 事件类型字段(可选) + const event = msgData.Event + ? (Array.isArray(msgData.Event) ? msgData.Event[0] : msgData.Event) + : undefined; + const eventKey = msgData.EventKey + ? (Array.isArray(msgData.EventKey) ? msgData.EventKey[0] : msgData.EventKey) + : undefined; + + return { + fromUser, + toUser, + msgType, + content, + msgId, + createTime, + event, + eventKey, + }; + } catch (error: any) { + logger.error('❌ 提取用户消息失败', { + error: error.message, + }); + return null; + } + } + + /** + * 记录用户操作到数据库 + */ + private async logUserAction( + openid: string, + actionType: string, + actionData: any + ): Promise { + try { + // TODO: 实现数据库记录 + // 需要先创建 patient_wechat_bindings 和 patient_actions 表 + logger.info('📝 记录用户操作', { + openid, + actionType, + dataKeys: Object.keys(actionData), + }); + + // 临时实现:记录到日志 + // 正式实现:存储到 iit_schema.patient_actions 表 + } catch (error: any) { + logger.error('❌ 记录用户操作失败', { + error: error.message, + openid, + actionType, + }); + } + } +} + +// 导出单例 +export const patientWechatCallbackController = new PatientWechatCallbackController(); + diff --git a/backend/src/modules/iit-manager/docs/微信服务号接入指南.md b/backend/src/modules/iit-manager/docs/微信服务号接入指南.md new file mode 100644 index 00000000..bf46f4cb --- /dev/null +++ b/backend/src/modules/iit-manager/docs/微信服务号接入指南.md @@ -0,0 +1,532 @@ +# 微信服务号接入指南(患者端) + +> **版本**: v1.0 +> **创建日期**: 2026-01-04 +> **目标**: 为患者端接入微信服务号,实现访视提醒和消息推送 +> **预估工作量**: 3天 + +--- + +## 📋 一、准备工作清单 + +### 1.1 微信服务号信息 + +✅ **已完成**: +- 服务号名称:`AI for 临床研究` +- AppID:`wx062568ff49e4570c` +- AppSecret:`c0d19435d1a1e948939c16d767ec0faf` +- 认证状态:✅ 已认证(企业认证) +- 主体名称:`北京壹证循科技有限公司` + +### 1.2 需要配置的参数 + +🔧 **待配置**: +1. **Token**(3-32位字符串) +2. **EncodingAESKey**(43位字符串) +3. **服务器URL**(回调地址) + +--- + +## 🔐 二、生成Token和EncodingAESKey + +### 2.1 Token生成(推荐使用随机字符串) + +```bash +# 方法1:使用OpenSSL(推荐) +openssl rand -base64 24 | tr -d '/+=' | cut -c1-32 + +# 方法2:使用Node.js +node -e "console.log(require('crypto').randomBytes(24).toString('base64').replace(/[\/\+=]/g, '').substring(0, 32))" + +# 方法3:在线生成器 +# https://suijimimashengcheng.51240.com/ +``` + +**推荐Token**(示例,请重新生成): +``` +IitPatientWechat2026Jan04Abc +``` + +### 2.2 EncodingAESKey生成(必须43位) + +```bash +# 方法1:使用OpenSSL(推荐) +openssl rand -base64 43 | tr -d '/+=' | head -c 43 + +# 方法2:使用Node.js +node -e "console.log(require('crypto').randomBytes(32).toString('base64').replace(/[\/\+=]/g, '').substring(0, 43))" + +# 方法3:微信公众平台随机生成(最简单) +# 登录微信公众平台 → 基本配置 → 消息加密密钥 → 点击"随机生成" +``` + +**推荐EncodingAESKey**(示例,请重新生成): +``` +abcdefghijklmnopqrstuvwxyz0123456789ABC +``` + +--- + +## ⚙️ 三、配置步骤(详细) + +### 3.1 更新环境变量 + +编辑 `backend/.env` 文件,添加以下配置: + +```env +# ========================================== +# 微信服务号配置(患者端) +# ========================================== + +# 微信服务号基础配置 +WECHAT_MP_APP_ID=wx062568ff49e4570c +WECHAT_MP_APP_SECRET=c0d19435d1a1e948939c16d767ec0faf + +# 微信服务号回调配置(消息加解密,安全模式) +WECHAT_MP_TOKEN=IitPatientWechat2026Jan04Abc +WECHAT_MP_ENCODING_AES_KEY=abcdefghijklmnopqrstuvwxyz0123456789ABC + +# 微信小程序配置(可选,后续开发) +WECHAT_MINI_APP_ID= +``` + +⚠️ **注意**: +1. Token和EncodingAESKey必须与微信公众平台配置的**完全一致** +2. Token长度:3-32位,建议使用英文字母和数字 +3. EncodingAESKey长度:**必须43位**,大小写敏感 + +### 3.2 配置微信公众平台 + +#### Step 1: 登录微信公众平台 + +访问:https://mp.weixin.qq.com/ + +使用管理员微信扫码登录(账号:`zhi***ng`) + +#### Step 2: 进入基本配置页面 + +``` +左侧菜单 → 设置与开发 → 基本配置 +``` + +#### Step 3: 配置服务器地址 + +找到 **"服务器配置"** 部分,点击 **"修改配置"** + +填写以下信息: + +| 配置项 | 值 | 说明 | +|--------|-----|------| +| **URL** | `https://iit.xunzhengyixue.com/api/v1/iit/patient-wechat/callback` | 生产环境回调URL | +| **Token** | `IitPatientWechat2026Jan04Abc` | 与.env中的WECHAT_MP_TOKEN一致 | +| **EncodingAESKey** | `abcdefghijklmnopqrstuvwxyz0123456789ABC` | 与.env中的WECHAT_MP_ENCODING_AES_KEY一致 | +| **消息加解密方式** | ✅ **安全模式(推荐)** | 选择"安全模式" | +| **数据格式** | ✅ **XML** | 默认选择 | + +**本地开发环境**(使用natapp内网穿透): +``` +URL: https://devlocal.xunzhengyixue.com/api/v1/iit/patient-wechat/callback +``` + +#### Step 4: 点击"提交"并验证 + +微信会发送GET请求到您的服务器进行验证: + +``` +GET https://iit.xunzhengyixue.com/api/v1/iit/patient-wechat/callback?signature=xxx×tamp=xxx&nonce=xxx&echostr=xxx +``` + +**验证成功标志**: +- ✅ 页面显示"配置成功" +- ✅ "服务器配置"状态为"已启用" + +**验证失败原因**: +- ❌ Token不一致 +- ❌ 服务器无法访问(防火墙、未部署) +- ❌ 代码逻辑错误(签名验证失败) + +#### Step 5: 启用服务器配置 + +验证成功后,点击 **"启用"** 按钮。 + +⚠️ **注意**:启用后,公众号的消息和事件会推送到您的服务器,不会显示在公众平台后台。 + +--- + +## 🧪 四、测试验证 + +### 4.1 测试URL验证(手动测试) + +在配置微信公众平台时,点击"提交"按钮会自动触发URL验证。 + +查看后端日志: + +```bash +# 本地开发 +cd D:\MyCursor\AIclinicalresearch\backend +npm run dev + +# 查看日志 +# 应该看到类似以下日志: +# ✅ 微信服务号回调控制器初始化成功 +# 📥 收到微信服务号 URL 验证请求 +# ✅ URL 验证成功,返回 echostr +``` + +### 4.2 测试脚本1:验证Token和AESKey配置 + +创建测试脚本 `backend/src/modules/iit-manager/test-patient-wechat-config.ts`: + +```typescript +import dotenv from 'dotenv'; +import crypto from 'crypto'; + +dotenv.config(); + +console.log('🔧 微信服务号配置检查\n'); + +// 1. 检查必需的环境变量 +const requiredEnvs = [ + 'WECHAT_MP_APP_ID', + 'WECHAT_MP_APP_SECRET', + 'WECHAT_MP_TOKEN', + 'WECHAT_MP_ENCODING_AES_KEY', +]; + +let hasError = false; + +requiredEnvs.forEach((key) => { + const value = process.env[key]; + if (!value) { + console.error(`❌ 缺少环境变量: ${key}`); + hasError = true; + } else { + console.log(`✅ ${key}: ${value.substring(0, 10)}... (长度: ${value.length})`); + } +}); + +if (hasError) { + console.error('\n❌ 配置不完整,请检查 .env 文件'); + process.exit(1); +} + +// 2. 验证Token长度 +const token = process.env.WECHAT_MP_TOKEN!; +if (token.length < 3 || token.length > 32) { + console.error(`\n❌ Token长度不正确: ${token.length}位(应为3-32位)`); + hasError = true; +} else { + console.log(`\n✅ Token长度正确: ${token.length}位`); +} + +// 3. 验证EncodingAESKey长度 +const aesKey = process.env.WECHAT_MP_ENCODING_AES_KEY!; +if (aesKey.length !== 43) { + console.error(`❌ EncodingAESKey长度不正确: ${aesKey.length}位(必须43位)`); + hasError = true; +} else { + console.log(`✅ EncodingAESKey长度正确: 43位`); +} + +// 4. 测试签名生成 +console.log('\n🔐 测试签名生成...'); +const timestamp = Date.now().toString(); +const nonce = Math.random().toString(36).substring(2); +const arr = [token, timestamp, nonce].sort(); +const str = arr.join(''); +const signature = crypto.createHash('sha1').update(str).digest('hex'); + +console.log(`生成的签名: ${signature}`); +console.log(`✅ 签名生成功能正常`); + +// 5. 总结 +if (hasError) { + console.error('\n❌ 配置检查失败,请修复错误后重试'); + process.exit(1); +} else { + console.log('\n✅ 配置检查通过!可以开始配置微信公众平台'); + console.log('\n📋 配置信息(用于微信公众平台):'); + console.log(`URL: https://iit.xunzhengyixue.com/api/v1/iit/patient-wechat/callback`); + console.log(`Token: ${token}`); + console.log(`EncodingAESKey: ${aesKey}`); + console.log(`消息加解密方式: 安全模式(推荐)`); +} +``` + +**运行测试**: + +```bash +cd backend +npx tsx src/modules/iit-manager/test-patient-wechat-config.ts +``` + +### 4.3 测试脚本2:模拟微信URL验证请求 + +创建测试脚本 `backend/src/modules/iit-manager/test-patient-wechat-url-verify.ts`: + +```typescript +import axios from 'axios'; +import crypto from 'crypto'; +import dotenv from 'dotenv'; + +dotenv.config(); + +const BASE_URL = 'http://localhost:3001'; +const TOKEN = process.env.WECHAT_MP_TOKEN || ''; + +async function testUrlVerification() { + console.log('🧪 测试微信服务号URL验证\n'); + + // 1. 准备参数 + const timestamp = Date.now().toString(); + const nonce = Math.random().toString(36).substring(2, 12); + const echostr = 'test_echo_' + Math.random().toString(36).substring(2); + + // 2. 生成签名 + const arr = [TOKEN, timestamp, nonce].sort(); + const str = arr.join(''); + const signature = crypto.createHash('sha1').update(str).digest('hex'); + + console.log('📝 请求参数:'); + console.log(` timestamp: ${timestamp}`); + console.log(` nonce: ${nonce}`); + console.log(` echostr: ${echostr}`); + console.log(` signature: ${signature}\n`); + + // 3. 发送GET请求 + try { + const url = `${BASE_URL}/api/v1/iit/patient-wechat/callback`; + const response = await axios.get(url, { + params: { + signature, + timestamp, + nonce, + echostr, + }, + }); + + console.log('✅ URL验证成功!'); + console.log(`返回内容: ${response.data}`); + + if (response.data === echostr) { + console.log('✅ 返回的echostr正确'); + } else { + console.error('❌ 返回的echostr不正确'); + } + } catch (error: any) { + console.error('❌ URL验证失败:', error.message); + if (error.response) { + console.error('响应状态:', error.response.status); + console.error('响应内容:', error.response.data); + } + } +} + +testUrlVerification(); +``` + +**运行测试**: + +```bash +# 先启动后端服务 +npm run dev + +# 新开一个终端,运行测试 +npx tsx src/modules/iit-manager/test-patient-wechat-url-verify.ts +``` + +### 4.4 测试关注事件 + +1. 用测试微信号关注公众号:`AI for 临床研究` +2. 查看后端日志,应该看到: + +``` +📥 收到微信服务号回调消息 +🔐 检测到加密消息,开始解密... +✅ 消息解密成功 +📬 提取用户消息成功 +🎯 处理事件消息: subscribe +👤 用户关注公众号: oXXXXXXXXXXXXXXXXX +``` + +### 4.5 测试文本消息 + +1. 在公众号对话框发送文本消息:`你好` +2. 查看后端日志,应该看到: + +``` +📥 收到微信服务号回调消息 +🔐 检测到加密消息,开始解密... +✅ 消息解密成功 +💬 处理文本消息: 你好 +📝 文本消息已记录 +``` + +--- + +## 🚀 五、部署上线 + +### 5.1 本地开发环境(natapp内网穿透) + +**1. 启动natapp**: + +```bash +# Windows +cd D:\tools\natapp +natapp.exe -authtoken=YOUR_TOKEN -subdomain=devlocal +``` + +**2. 验证映射**: + +访问:`https://devlocal.xunzhengyixue.com/api/v1/iit/health` + +应该返回: +```json +{ + "status": "ok", + "module": "iit-manager", + "version": "1.1.0" +} +``` + +**3. 配置微信公众平台**: + +``` +URL: https://devlocal.xunzhengyixue.com/api/v1/iit/patient-wechat/callback +``` + +### 5.2 生产环境(SAE) + +**1. 更新SAE环境变量**: + +登录阿里云SAE控制台 → 应用管理 → 环境变量配置 + +添加以下环境变量: +``` +WECHAT_MP_APP_ID=wx062568ff49e4570c +WECHAT_MP_APP_SECRET=c0d19435d1a1e948939c16d767ec0faf +WECHAT_MP_TOKEN=IitPatientWechat2026Jan04Abc +WECHAT_MP_ENCODING_AES_KEY=abcdefghijklmnopqrstuvwxyz0123456789ABC +``` + +**2. 部署代码**: + +```bash +cd backend +./deploy-to-sae.ps1 +``` + +**3. 验证部署**: + +访问:`https://iit.xunzhengyixue.com/api/v1/iit/health` + +**4. 配置微信公众平台**: + +``` +URL: https://iit.xunzhengyixue.com/api/v1/iit/patient-wechat/callback +``` + +**5. 配置IP白名单**(重要): + +登录微信公众平台 → 基本配置 → IP白名单 + +添加SAE应用的出口IP(可以从SAE控制台查看) + +--- + +## 📋 六、常见问题排查 + +### Q1: URL验证失败,提示"Token验证失败" + +**原因**: +- Token配置不一致(大小写、多余空格) +- 签名计算错误 + +**解决方法**: +1. 检查 `.env` 文件中的 `WECHAT_MP_TOKEN` 是否与微信公众平台配置一致 +2. 运行配置检查脚本:`npx tsx src/modules/iit-manager/test-patient-wechat-config.ts` +3. 查看后端日志,确认签名计算过程 + +### Q2: URL验证失败,提示"请求URL超时" + +**原因**: +- 服务器未启动 +- 防火墙阻止 +- URL配置错误 + +**解决方法**: +1. 确认后端服务已启动:`npm run dev` +2. 本地开发确认natapp已启动 +3. 生产环境确认SAE应用状态正常 +4. 使用浏览器直接访问健康检查接口测试连通性 + +### Q3: 消息解密失败 + +**原因**: +- EncodingAESKey配置不一致 +- EncodingAESKey长度不正确(必须43位) + +**解决方法**: +1. 检查 `.env` 文件中的 `WECHAT_MP_ENCODING_AES_KEY` 长度是否为43位 +2. 确认与微信公众平台配置完全一致(包括大小写) +3. 重新生成EncodingAESKey并同步更新 + +### Q4: 收不到用户消息 + +**原因**: +- 服务器配置未启用 +- 回调URL配置错误 +- 服务端代码异常 + +**解决方法**: +1. 登录微信公众平台,确认"服务器配置"状态为"已启用" +2. 查看后端日志,确认是否收到POST请求 +3. 检查是否有异常日志 + +--- + +## 📝 七、后续开发计划 + +### Phase 1: 基础消息推送(当前) + +- [x] 创建PatientWechatCallbackController +- [x] 创建PatientWechatService +- [x] 配置路由和环境变量 +- [ ] 测试URL验证 +- [ ] 测试消息接收 + +### Phase 2: 患者绑定功能 + +- [ ] 创建患者绑定数据表 +- [ ] 开发患者绑定H5页面 +- [ ] 实现手机号验证码功能 +- [ ] 实现患者身份验证逻辑 + +### Phase 3: 模板消息推送 + +- [ ] 申请模板消息权限 +- [ ] 设计访视提醒模板 +- [ ] 开发定时任务检测到期访视 +- [ ] 实现模板消息推送 + +### Phase 4: 微信小程序 + +- [ ] 注册微信小程序 +- [ ] 搭建小程序框架 +- [ ] 开发核心页面 +- [ ] 前后端联调 + +--- + +## 📞 联系方式 + +如有问题,请联系: +- 技术负责人:冯志博 +- 邮箱:gofeng117@163.com +- 微信:aiforresearch + +--- + +**最后更新**:2026-01-04 +**文档版本**:v1.0 + diff --git a/backend/src/modules/iit-manager/generate-wechat-tokens.ts b/backend/src/modules/iit-manager/generate-wechat-tokens.ts new file mode 100644 index 00000000..07d53082 --- /dev/null +++ b/backend/src/modules/iit-manager/generate-wechat-tokens.ts @@ -0,0 +1,167 @@ +/** + * 生成微信服务号Token和EncodingAESKey + * + * 功能: + * 1. 生成符合要求的Token(3-32位) + * 2. 生成符合要求的EncodingAESKey(43位) + * 3. 输出可直接复制的环境变量配置 + */ + +import crypto from 'crypto'; + +console.log('🔐 微信服务号Token和EncodingAESKey生成器'); +console.log('='.repeat(60)); +console.log(''); + +// ==================== 1. 生成Token ==================== + +console.log('🔑 生成Token(用于签名验证)...\n'); + +function generateToken(length: number = 32): string { + // 生成随机字符串(只包含字母和数字) + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + let token = ''; + const randomBytes = crypto.randomBytes(length); + + for (let i = 0; i < length; i++) { + token += chars[randomBytes[i] % chars.length]; + } + + return token; +} + +const token = generateToken(32); + +console.log(' 生成的Token:'); +console.log(` ${token}`); +console.log(` 长度: ${token.length}位`); +console.log(` 说明: 用于URL验证和消息签名验证`); +console.log(''); + +// ==================== 2. 生成EncodingAESKey ==================== + +console.log('🔐 生成EncodingAESKey(用于消息加解密)...\n'); + +function generateEncodingAESKey(): string { + // 生成43位随机字符串(Base64字符集,不包含/+=) + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + let aesKey = ''; + const randomBytes = crypto.randomBytes(43); + + for (let i = 0; i < 43; i++) { + aesKey += chars[randomBytes[i] % chars.length]; + } + + return aesKey; +} + +const aesKey = generateEncodingAESKey(); + +console.log(' 生成的EncodingAESKey:'); +console.log(` ${aesKey}`); +console.log(` 长度: ${aesKey.length}位`); +console.log(` 说明: 用于消息加密和解密(安全模式必需)`); +console.log(''); + +// ==================== 3. 输出环境变量配置 ==================== + +console.log('='.repeat(60)); +console.log('\n📋 环境变量配置(复制以下内容到 backend/.env 文件)\n'); +console.log('='.repeat(60)); +console.log(''); + +console.log('# =========================================='); +console.log('# 微信服务号配置(患者端)'); +console.log('# =========================================='); +console.log(''); +console.log('# 微信服务号基础配置'); +console.log('WECHAT_MP_APP_ID=wx062568ff49e4570c'); +console.log('WECHAT_MP_APP_SECRET=c0d19435d1a1e948939c16d767ec0faf'); +console.log(''); +console.log('# 微信服务号回调配置(消息加解密,安全模式)'); +console.log(`WECHAT_MP_TOKEN=${token}`); +console.log(`WECHAT_MP_ENCODING_AES_KEY=${aesKey}`); +console.log(''); +console.log('# 微信小程序配置(可选,后续开发)'); +console.log('WECHAT_MINI_APP_ID='); +console.log(''); + +// ==================== 4. 输出微信公众平台配置 ==================== + +console.log('='.repeat(60)); +console.log('\n📋 微信公众平台配置(复制以下内容到公众平台)\n'); +console.log('='.repeat(60)); +console.log(''); + +console.log('登录地址:https://mp.weixin.qq.com/'); +console.log('配置路径:设置与开发 → 基本配置 → 服务器配置'); +console.log(''); +console.log('配置参数:'); +console.log(''); +console.log(' 【URL】'); +console.log(' 生产环境:https://iit.xunzhengyixue.com/api/v1/iit/patient-wechat/callback'); +console.log(' 开发环境:https://devlocal.xunzhengyixue.com/api/v1/iit/patient-wechat/callback'); +console.log(''); +console.log(' 【Token】'); +console.log(` ${token}`); +console.log(''); +console.log(' 【EncodingAESKey】'); +console.log(` ${aesKey}`); +console.log(''); +console.log(' 【消息加解密方式】'); +console.log(' 安全模式(推荐)'); +console.log(''); +console.log(' 【数据格式】'); +console.log(' XML'); +console.log(''); + +// ==================== 5. 输出后续步骤 ==================== + +console.log('='.repeat(60)); +console.log('\n📝 后续步骤\n'); +console.log('='.repeat(60)); +console.log(''); + +console.log('Step 1: 更新环境变量'); +console.log(' 1. 复制上面的环境变量配置'); +console.log(' 2. 粘贴到 backend/.env 文件'); +console.log(' 3. 保存文件'); +console.log(''); + +console.log('Step 2: 验证配置'); +console.log(' 运行配置检查脚本:'); +console.log(' npx tsx src/modules/iit-manager/test-patient-wechat-config.ts'); +console.log(''); + +console.log('Step 3: 启动服务'); +console.log(' 本地开发:npm run dev'); +console.log(' 生产环境:部署到SAE'); +console.log(''); + +console.log('Step 4: 配置微信公众平台'); +console.log(' 1. 登录微信公众平台'); +console.log(' 2. 填写上面的配置参数'); +console.log(' 3. 点击"提交"进行URL验证'); +console.log(' 4. 验证成功后点击"启用"'); +console.log(''); + +console.log('Step 5: 测试功能'); +console.log(' 运行URL验证测试脚本:'); +console.log(' npx tsx src/modules/iit-manager/test-patient-wechat-url-verify.ts'); +console.log(''); + +console.log('='.repeat(60)); +console.log('\n⚠️ 重要提示\n'); +console.log('='.repeat(60)); +console.log(''); + +console.log(' 1. Token和EncodingAESKey必须在.env和微信公众平台中保持一致'); +console.log(' 2. 不要将Token和EncodingAESKey提交到Git仓库'); +console.log(' 3. 生产环境需要在SAE中配置这些环境变量'); +console.log(' 4. 如果配置错误,可以重新运行本脚本生成新的值'); +console.log(' 5. 修改配置后需要重启服务才能生效'); +console.log(''); + +console.log('🎉 生成完成!'); +console.log(''); + diff --git a/backend/src/modules/iit-manager/routes/index.ts b/backend/src/modules/iit-manager/routes/index.ts index 251213af..e8b18515 100644 --- a/backend/src/modules/iit-manager/routes/index.ts +++ b/backend/src/modules/iit-manager/routes/index.ts @@ -5,6 +5,7 @@ import { FastifyInstance } from 'fastify'; import { WebhookController } from '../controllers/WebhookController.js'; import { wechatCallbackController } from '../controllers/WechatCallbackController.js'; +import { patientWechatCallbackController } from '../controllers/PatientWechatCallbackController.js'; import { SyncManager } from '../services/SyncManager.js'; import { logger } from '../../../common/logging/index.js'; @@ -295,10 +296,113 @@ export async function registerIitRoutes(fastify: FastifyInstance) { logger.info('Registered route: POST /api/v1/iit/wechat/callback'); + // ============================================= + // 微信服务号回调路由(患者端) + // ============================================= + + // 简化路由(用于微信公众平台配置,路径更短) + // GET: URL验证 + // 注意:不使用required字段,避免Fastify过早拦截微信的请求 + fastify.get( + '/wechat/patient/callback', + { + schema: { + querystring: { + type: 'object', + properties: { + signature: { type: 'string' }, + timestamp: { type: 'string' }, + nonce: { type: 'string' }, + echostr: { type: 'string' }, + // 微信可能还会传其他参数,使用additionalProperties允许 + }, + additionalProperties: true + } + } + }, + patientWechatCallbackController.handleVerification.bind(patientWechatCallbackController) + ); + + logger.info('Registered route: GET /wechat/patient/callback'); + + // POST: 接收消息 + // 注意:不使用required字段,避免Fastify过早拦截微信的请求 + fastify.post( + '/wechat/patient/callback', + { + schema: { + querystring: { + type: 'object', + properties: { + signature: { type: 'string' }, + timestamp: { type: 'string' }, + nonce: { type: 'string' }, + openid: { type: 'string' }, + encrypt_type: { type: 'string' }, + msg_signature: { type: 'string' } + }, + additionalProperties: true + } + } + }, + patientWechatCallbackController.handleCallback.bind(patientWechatCallbackController) + ); + + logger.info('Registered route: POST /wechat/patient/callback'); + + // 完整路由(兼容旧配置,保留) + // GET: URL验证(微信服务号配置回调URL时使用) + fastify.get( + '/api/v1/iit/patient-wechat/callback', + { + schema: { + querystring: { + type: 'object', + properties: { + signature: { type: 'string' }, + timestamp: { type: 'string' }, + nonce: { type: 'string' }, + echostr: { type: 'string' } + }, + additionalProperties: true + } + } + }, + patientWechatCallbackController.handleVerification.bind(patientWechatCallbackController) + ); + + logger.info('Registered route: GET /api/v1/iit/patient-wechat/callback'); + + // POST: 接收微信服务号消息 + fastify.post( + '/api/v1/iit/patient-wechat/callback', + { + schema: { + querystring: { + type: 'object', + properties: { + signature: { type: 'string' }, + timestamp: { type: 'string' }, + nonce: { type: 'string' }, + openid: { type: 'string' }, + encrypt_type: { type: 'string' }, + msg_signature: { type: 'string' } + }, + additionalProperties: true + } + } + }, + patientWechatCallbackController.handleCallback.bind(patientWechatCallbackController) + ); + + logger.info('Registered route: POST /api/v1/iit/patient-wechat/callback'); + // TODO: 后续添加其他路由 // - 项目管理路由 // - 影子状态路由 // - 任务管理路由 + // - 患者绑定路由 + // - 患者信息查询路由 } diff --git a/backend/src/modules/iit-manager/services/PatientWechatService.ts b/backend/src/modules/iit-manager/services/PatientWechatService.ts new file mode 100644 index 00000000..26f613cc --- /dev/null +++ b/backend/src/modules/iit-manager/services/PatientWechatService.ts @@ -0,0 +1,484 @@ +/** + * 微信服务号消息推送服务(患者端) + * + * 功能: + * 1. 获取微信服务号 Access Token(缓存管理) + * 2. 发送模板消息(访视提醒、填表通知等) + * 3. 发送客服消息(文本、图片、图文) + * 4. 管理用户订阅(订阅消息) + * + * 技术要点: + * - Access Token 缓存(7000秒,提前5分钟刷新) + * - 错误重试机制 + * - 完整的日志记录 + * + * 参考文档: + * - https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Template_Message_Interface.html + * - https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Service_Center_messages.html + */ + +import axios from 'axios'; +import { PrismaClient } from '@prisma/client'; +import { logger } from '../../../common/logging/index.js'; + +const prisma = new PrismaClient(); + +// ==================== 类型定义 ==================== + +interface WechatMpConfig { + appId: string; + appSecret: string; +} + +interface AccessTokenCache { + token: string; + expiresAt: number; +} + +interface WechatApiResponse { + errcode: number; + errmsg: string; +} + +interface AccessTokenResponse extends WechatApiResponse { + access_token?: string; + expires_in?: number; +} + +interface SendMessageResponse extends WechatApiResponse { + msgid?: number; +} + +interface TemplateMessageData { + [key: string]: { + value: string; + color?: string; + }; +} + +interface TemplateMessageParams { + touser: string; // 接收者openid + template_id: string; // 模板ID + url?: string; // 跳转URL(H5) + miniprogram?: { // 跳转小程序 + appid: string; + pagepath: string; + }; + data: TemplateMessageData; // 模板数据 + topcolor?: string; // 顶部颜色 +} + +interface CustomerMessageParams { + touser: string; // 接收者openid + msgtype: string; // 消息类型(text, image, news等) + text?: { // 文本消息 + content: string; + }; + image?: { // 图片消息 + media_id: string; + }; + news?: { // 图文消息 + articles: Array<{ + title: string; + description: string; + url: string; + picurl: string; + }>; + }; +} + +// ==================== 微信服务号服务类 ==================== + +export class PatientWechatService { + private config: WechatMpConfig; + private accessTokenCache: AccessTokenCache | null = null; + private readonly baseUrl = 'https://api.weixin.qq.com/cgi-bin'; + + constructor() { + // 从环境变量读取配置 + this.config = { + appId: process.env.WECHAT_MP_APP_ID || '', + appSecret: process.env.WECHAT_MP_APP_SECRET || '', + }; + + // 验证配置 + if (!this.config.appId || !this.config.appSecret) { + logger.error('❌ 微信服务号配置不完整', { + hasAppId: !!this.config.appId, + hasAppSecret: !!this.config.appSecret, + }); + throw new Error('微信服务号配置不完整,请检查环境变量'); + } + + logger.info('✅ 微信服务号服务初始化成功', { + appId: this.config.appId, + }); + } + + // ==================== Access Token 管理 ==================== + + /** + * 获取微信服务号 Access Token + * - 优先返回缓存的 Token(如果未过期) + * - 过期或不存在时,重新请求 + */ + async getAccessToken(): Promise { + try { + // 1. 检查缓存 + if (this.accessTokenCache) { + const now = Date.now(); + const expiresIn = this.accessTokenCache.expiresAt - now; + + // 提前5分钟刷新(避免临界点失效) + if (expiresIn > 5 * 60 * 1000) { + logger.debug('✅ 使用缓存的 Access Token', { + expiresIn: Math.floor(expiresIn / 1000) + 's', + }); + return this.accessTokenCache.token; + } + + logger.info('⏰ Access Token 即将过期,重新获取'); + } + + // 2. 请求新的 Access Token + const url = `${this.baseUrl}/token`; + const response = await axios.get(url, { + params: { + grant_type: 'client_credential', + appid: this.config.appId, + secret: this.config.appSecret, + }, + timeout: 10000, + }); + + // 3. 检查响应 + if (response.data.errcode && response.data.errcode !== 0) { + throw new Error( + `获取 Access Token 失败:${response.data.errmsg} (errcode: ${response.data.errcode})` + ); + } + + if (!response.data.access_token) { + throw new Error('Access Token 响应中缺少 access_token 字段'); + } + + // 4. 缓存 Token + const expiresIn = response.data.expires_in || 7200; // 默认2小时 + this.accessTokenCache = { + token: response.data.access_token, + expiresAt: Date.now() + expiresIn * 1000, + }; + + logger.info('✅ 获取 Access Token 成功', { + tokenLength: this.accessTokenCache.token.length, + expiresIn: expiresIn + 's', + }); + + return this.accessTokenCache.token; + } catch (error: any) { + logger.error('❌ 获取 Access Token 失败', { + error: error.message, + response: error.response?.data, + }); + throw error; + } + } + + // ==================== 模板消息推送 ==================== + + /** + * 发送模板消息 + * + * 使用场景: + * - 访视提醒 + * - 填表通知 + * - 结果反馈 + * + * 注意:模板需要在微信公众平台申请并通过审核 + */ + async sendTemplateMessage(params: TemplateMessageParams): Promise { + try { + logger.info('📤 准备发送模板消息', { + touser: params.touser, + template_id: params.template_id, + hasUrl: !!params.url, + hasMiniProgram: !!params.miniprogram, + }); + + // 1. 获取 Access Token + const accessToken = await this.getAccessToken(); + + // 2. 调用发送接口 + const url = `${this.baseUrl}/message/template/send?access_token=${accessToken}`; + const response = await axios.post(url, params, { + timeout: 10000, + }); + + // 3. 检查响应 + if (response.data.errcode !== 0) { + throw new Error( + `发送模板消息失败:${response.data.errmsg} (errcode: ${response.data.errcode})` + ); + } + + logger.info('✅ 模板消息发送成功', { + touser: params.touser, + msgid: response.data.msgid, + }); + + // 4. 记录到数据库 + await this.logNotification(params.touser, 'template', params.template_id, params, 'sent'); + } catch (error: any) { + logger.error('❌ 发送模板消息失败', { + error: error.message, + touser: params.touser, + response: error.response?.data, + }); + + // 记录失败日志 + await this.logNotification( + params.touser, + 'template', + params.template_id, + params, + 'failed', + error.message + ); + + throw error; + } + } + + /** + * 发送访视提醒(模板消息) + * + * 示例模板格式: + * {{first.DATA}} + * 访视时间:{{keyword1.DATA}} + * 访视地点:{{keyword2.DATA}} + * 注意事项:{{keyword3.DATA}} + * {{remark.DATA}} + */ + async sendVisitReminder(params: { + openid: string; + templateId: string; + visitTime: string; + visitLocation: string; + notes: string; + miniProgramPath?: string; + }): Promise { + const messageParams: TemplateMessageParams = { + touser: params.openid, + template_id: params.templateId, + data: { + first: { + value: '您有一次访视安排', + color: '#173177', + }, + keyword1: { + value: params.visitTime, + color: '#173177', + }, + keyword2: { + value: params.visitLocation, + color: '#173177', + }, + keyword3: { + value: params.notes, + color: '#173177', + }, + remark: { + value: '请按时到院,如有问题请联系研究团队', + color: '#173177', + }, + }, + }; + + // 如果提供了小程序路径,跳转到小程序 + if (params.miniProgramPath) { + messageParams.miniprogram = { + appid: process.env.WECHAT_MINI_APP_ID || '', + pagepath: params.miniProgramPath, + }; + } + + await this.sendTemplateMessage(messageParams); + } + + // ==================== 客服消息推送 ==================== + + /** + * 发送客服消息(文本) + * + * 限制: + * - 只能在用户48小时内与公众号有交互后发送 + * - 或者用户主动发送消息后48小时内 + */ + async sendCustomerMessage(params: CustomerMessageParams): Promise { + try { + logger.info('📤 准备发送客服消息', { + touser: params.touser, + msgtype: params.msgtype, + }); + + // 1. 获取 Access Token + const accessToken = await this.getAccessToken(); + + // 2. 调用发送接口 + const url = `${this.baseUrl}/message/custom/send?access_token=${accessToken}`; + const response = await axios.post(url, params, { + timeout: 10000, + }); + + // 3. 检查响应 + if (response.data.errcode !== 0) { + throw new Error( + `发送客服消息失败:${response.data.errmsg} (errcode: ${response.data.errcode})` + ); + } + + logger.info('✅ 客服消息发送成功', { + touser: params.touser, + }); + + // 4. 记录到数据库 + await this.logNotification(params.touser, 'customer', params.msgtype, params, 'sent'); + } catch (error: any) { + logger.error('❌ 发送客服消息失败', { + error: error.message, + touser: params.touser, + response: error.response?.data, + }); + + // 记录失败日志 + await this.logNotification( + params.touser, + 'customer', + params.msgtype, + params, + 'failed', + error.message + ); + + throw error; + } + } + + /** + * 发送文本客服消息(快捷方法) + */ + async sendTextMessage(openid: string, content: string): Promise { + await this.sendCustomerMessage({ + touser: openid, + msgtype: 'text', + text: { + content, + }, + }); + } + + // ==================== 辅助方法 ==================== + + /** + * 记录消息推送日志到数据库 + */ + private async logNotification( + openid: string, + notificationType: string, + templateId: string, + content: any, + status: 'sent' | 'failed', + errorMessage?: string + ): Promise { + try { + // TODO: 实现数据库记录 + // 需要先创建 patient_notifications 表 + logger.info('📝 记录消息推送日志', { + openid, + notificationType, + templateId, + status, + hasError: !!errorMessage, + }); + + // 临时实现:记录到日志 + // 正式实现:存储到 iit_schema.patient_notifications 表 + } catch (error: any) { + logger.error('❌ 记录消息推送日志失败', { + error: error.message, + openid, + }); + } + } + + // ==================== 用户管理 ==================== + + /** + * 检查用户是否已关注公众号 + */ + async isUserSubscribed(openid: string): Promise { + try { + const accessToken = await this.getAccessToken(); + const url = `${this.baseUrl}/user/info?access_token=${accessToken}&openid=${openid}`; + + const response = await axios.get(url, { timeout: 10000 }); + + if (response.data.errcode && response.data.errcode !== 0) { + logger.error('❌ 查询用户信息失败', { + errcode: response.data.errcode, + errmsg: response.data.errmsg, + }); + return false; + } + + const isSubscribed = response.data.subscribe === 1; + + logger.info('✅ 查询用户订阅状态', { + openid, + isSubscribed, + }); + + return isSubscribed; + } catch (error: any) { + logger.error('❌ 查询用户订阅状态失败', { + error: error.message, + openid, + }); + return false; + } + } + + /** + * 获取用户基本信息 + */ + async getUserInfo(openid: string): Promise { + try { + const accessToken = await this.getAccessToken(); + const url = `${this.baseUrl}/user/info?access_token=${accessToken}&openid=${openid}`; + + const response = await axios.get(url, { timeout: 10000 }); + + if (response.data.errcode && response.data.errcode !== 0) { + throw new Error(`获取用户信息失败:${response.data.errmsg}`); + } + + logger.info('✅ 获取用户信息成功', { + openid, + nickname: response.data.nickname, + subscribe: response.data.subscribe, + }); + + return response.data; + } catch (error: any) { + logger.error('❌ 获取用户信息失败', { + error: error.message, + openid, + }); + throw error; + } + } +} + +// 导出单例 +export const patientWechatService = new PatientWechatService(); + diff --git a/backend/src/modules/iit-manager/test-chatservice-dify.ts b/backend/src/modules/iit-manager/test-chatservice-dify.ts index de92a654..55b30136 100644 --- a/backend/src/modules/iit-manager/test-chatservice-dify.ts +++ b/backend/src/modules/iit-manager/test-chatservice-dify.ts @@ -126,3 +126,4 @@ testDifyIntegration().catch(error => { process.exit(1); }); + diff --git a/backend/src/modules/iit-manager/test-iit-database.ts b/backend/src/modules/iit-manager/test-iit-database.ts index 056d68e6..e7b63a16 100644 --- a/backend/src/modules/iit-manager/test-iit-database.ts +++ b/backend/src/modules/iit-manager/test-iit-database.ts @@ -155,3 +155,4 @@ testIitDatabase() + diff --git a/backend/src/modules/iit-manager/test-patient-wechat-config.ts b/backend/src/modules/iit-manager/test-patient-wechat-config.ts new file mode 100644 index 00000000..bb1a4e80 --- /dev/null +++ b/backend/src/modules/iit-manager/test-patient-wechat-config.ts @@ -0,0 +1,144 @@ +/** + * 微信服务号配置检查脚本 + * + * 功能: + * 1. 检查必需的环境变量是否配置 + * 2. 验证Token和EncodingAESKey长度 + * 3. 测试签名生成功能 + * 4. 输出配置信息(用于微信公众平台) + */ + +import dotenv from 'dotenv'; +import crypto from 'crypto'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +// 获取当前文件目录 +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// 加载.env文件 +dotenv.config({ path: path.resolve(__dirname, '../../../.env') }); + +console.log('🔧 微信服务号配置检查'); +console.log('=' .repeat(60)); +console.log(''); + +// ==================== 1. 检查必需的环境变量 ==================== + +console.log('📋 检查必需的环境变量...\n'); + +const requiredEnvs = [ + { key: 'WECHAT_MP_APP_ID', description: 'AppID' }, + { key: 'WECHAT_MP_APP_SECRET', description: 'AppSecret' }, + { key: 'WECHAT_MP_TOKEN', description: 'Token(用于签名验证)' }, + { key: 'WECHAT_MP_ENCODING_AES_KEY', description: 'EncodingAESKey(用于消息加解密)' }, +]; + +let hasError = false; + +requiredEnvs.forEach(({ key, description }) => { + const value = process.env[key]; + if (!value) { + console.error(`❌ 缺少环境变量: ${key} (${description})`); + hasError = true; + } else { + const displayValue = + value.length > 20 ? `${value.substring(0, 10)}...${value.substring(value.length - 5)}` : value; + console.log(`✅ ${key}:`); + console.log(` 值: ${displayValue}`); + console.log(` 长度: ${value.length}位`); + console.log(` 说明: ${description}\n`); + } +}); + +if (hasError) { + console.error('\n❌ 配置不完整,请检查 backend/.env 文件'); + console.error('\n请参考 backend/WECHAT_ENV_CONFIG.md 进行配置'); + process.exit(1); +} + +// ==================== 2. 验证Token长度 ==================== + +console.log('\n📏 验证Token长度...\n'); + +const token = process.env.WECHAT_MP_TOKEN!; +if (token.length < 3 || token.length > 32) { + console.error(`❌ Token长度不正确: ${token.length}位(应为3-32位)`); + console.error(` 当前Token: ${token}`); + hasError = true; +} else { + console.log(`✅ Token长度正确: ${token.length}位`); +} + +// ==================== 3. 验证EncodingAESKey长度 ==================== + +console.log('\n🔐 验证EncodingAESKey长度...\n'); + +const aesKey = process.env.WECHAT_MP_ENCODING_AES_KEY!; +if (aesKey.length !== 43) { + console.error(`❌ EncodingAESKey长度不正确: ${aesKey.length}位(必须43位)`); + console.error(` 当前AESKey: ${aesKey}`); + console.error(` 提示: 可以使用以下命令生成43位字符串:`); + console.error(` openssl rand -base64 43 | tr -d '/+=' | head -c 43`); + hasError = true; +} else { + console.log(`✅ EncodingAESKey长度正确: 43位`); +} + +// ==================== 4. 测试签名生成 ==================== + +console.log('\n🔐 测试签名生成...\n'); + +try { + const timestamp = Date.now().toString(); + const nonce = Math.random().toString(36).substring(2, 12); + const arr = [token, timestamp, nonce].sort(); + const str = arr.join(''); + const signature = crypto.createHash('sha1').update(str).digest('hex'); + + console.log(`测试参数:`); + console.log(` timestamp: ${timestamp}`); + console.log(` nonce: ${nonce}`); + console.log(` token: ${token.substring(0, 10)}...`); + console.log(`\n排序后拼接: ${str.substring(0, 50)}...`); + console.log(`\n生成的签名: ${signature}`); + console.log(`\n✅ 签名生成功能正常`); +} catch (error: any) { + console.error(`❌ 签名生成失败: ${error.message}`); + hasError = true; +} + +// ==================== 5. 总结 ==================== + +console.log('\n' + '='.repeat(60)); + +if (hasError) { + console.error('\n❌ 配置检查失败,请修复以上错误后重试\n'); + process.exit(1); +} else { + console.log('\n✅ 配置检查通过!可以开始配置微信公众平台\n'); + console.log('=' .repeat(60)); + console.log('\n📋 配置信息(复制以下内容到微信公众平台):\n'); + console.log('登录地址:https://mp.weixin.qq.com/'); + console.log('配置路径:设置与开发 → 基本配置 → 服务器配置\n'); + console.log('配置参数:'); + console.log(` URL: https://iit.xunzhengyixue.com/api/v1/iit/patient-wechat/callback`); + console.log(` Token: ${token}`); + console.log(` EncodingAESKey: ${aesKey}`); + console.log(` 消息加解密方式: 安全模式(推荐)`); + console.log(` 数据格式: XML`); + console.log('\n本地开发环境URL(使用natapp):'); + console.log(` URL: https://devlocal.xunzhengyixue.com/api/v1/iit/patient-wechat/callback`); + console.log('\n' + '='.repeat(60)); + console.log('\n📝 后续步骤:\n'); + console.log(' 1. 启动后端服务:npm run dev'); + console.log(' 2. 本地开发需要启动natapp内网穿透'); + console.log(' 3. 登录微信公众平台,配置服务器地址'); + console.log(' 4. 点击"提交"进行URL验证'); + console.log(' 5. 验证成功后点击"启用"'); + console.log(' 6. 运行测试脚本验证功能:'); + console.log(' npx tsx src/modules/iit-manager/test-patient-wechat-url-verify.ts'); + console.log('\n'); +} + diff --git a/backend/src/modules/iit-manager/test-patient-wechat-url-verify.ts b/backend/src/modules/iit-manager/test-patient-wechat-url-verify.ts new file mode 100644 index 00000000..8ba55299 --- /dev/null +++ b/backend/src/modules/iit-manager/test-patient-wechat-url-verify.ts @@ -0,0 +1,170 @@ +/** + * 微信服务号URL验证测试脚本 + * + * 功能: + * 1. 模拟微信服务器发送GET请求验证URL + * 2. 测试签名生成和验证逻辑 + * 3. 验证服务端是否正确返回echostr + */ + +import axios from 'axios'; +import crypto from 'crypto'; +import dotenv from 'dotenv'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +// 获取当前文件目录 +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// 加载.env文件 +dotenv.config({ path: path.resolve(__dirname, '../../../.env') }); + +const BASE_URL = process.env.API_BASE_URL || 'http://localhost:3001'; +const TOKEN = process.env.WECHAT_MP_TOKEN || ''; + +console.log('🧪 微信服务号URL验证测试'); +console.log('='.repeat(60)); +console.log(''); + +// ==================== 1. 检查配置 ==================== + +if (!TOKEN) { + console.error('❌ 未配置 WECHAT_MP_TOKEN,请检查 .env 文件'); + process.exit(1); +} + +console.log('📋 测试环境:'); +console.log(` BASE_URL: ${BASE_URL}`); +console.log(` TOKEN: ${TOKEN.substring(0, 10)}... (长度: ${TOKEN.length})`); +console.log(''); + +// ==================== 2. 生成测试参数 ==================== + +console.log('🔧 生成测试参数...\n'); + +const timestamp = Date.now().toString(); +const nonce = Math.random().toString(36).substring(2, 12); +const echostr = 'test_echo_' + Math.random().toString(36).substring(2); + +console.log(' timestamp: ' + timestamp); +console.log(` nonce: ${nonce}`); +console.log(` echostr: ${echostr}\n`); + +// ==================== 3. 生成签名 ==================== + +console.log('🔐 生成签名...\n'); + +const arr = [TOKEN, timestamp, nonce].sort(); +const str = arr.join(''); +const signature = crypto.createHash('sha1').update(str).digest('hex'); + +console.log(` 排序后拼接: ${str.substring(0, 50)}...`); +console.log(` SHA1哈希: ${signature}\n`); + +// ==================== 4. 发送GET请求 ==================== + +async function testUrlVerification() { + console.log('📤 发送URL验证请求...\n'); + + const url = `${BASE_URL}/api/v1/iit/patient-wechat/callback`; + + console.log(` 请求URL: ${url}`); + console.log(` 请求方法: GET`); + console.log(` 查询参数:`); + console.log(` signature: ${signature}`); + console.log(` timestamp: ${timestamp}`); + console.log(` nonce: ${nonce}`); + console.log(` echostr: ${echostr}\n`); + + try { + const response = await axios.get(url, { + params: { + signature, + timestamp, + nonce, + echostr, + }, + timeout: 5000, + }); + + console.log('='.repeat(60)); + console.log('\n✅ URL验证成功!\n'); + console.log(`HTTP状态码: ${response.status}`); + console.log(`返回内容类型: ${response.headers['content-type']}`); + console.log(`返回内容: ${response.data}\n`); + + // 验证返回内容 + if (response.data === echostr) { + console.log('✅ 返回的echostr正确,验证通过!\n'); + console.log('='.repeat(60)); + console.log('\n🎉 测试成功!您的服务端配置正确\n'); + console.log('📝 后续步骤:'); + console.log(' 1. 登录微信公众平台:https://mp.weixin.qq.com/'); + console.log(' 2. 进入:设置与开发 → 基本配置 → 服务器配置'); + console.log(' 3. 填写以下信息:'); + console.log(` URL: ${BASE_URL}/api/v1/iit/patient-wechat/callback`); + console.log(` Token: ${TOKEN}`); + console.log(` EncodingAESKey: ${process.env.WECHAT_MP_ENCODING_AES_KEY?.substring(0, 10)}...`); + console.log(' 4. 选择"安全模式"'); + console.log(' 5. 点击"提交"进行验证'); + console.log(' 6. 验证成功后点击"启用"'); + console.log(''); + } else { + console.error('❌ 返回的echostr不正确!'); + console.error(` 期望: ${echostr}`); + console.error(` 实际: ${response.data}\n`); + process.exit(1); + } + } catch (error: any) { + console.log('='.repeat(60)); + console.error('\n❌ URL验证失败\n'); + + if (error.code === 'ECONNREFUSED') { + console.error('连接被拒绝,可能的原因:'); + console.error(' 1. 后端服务未启动'); + console.error(' 2. 端口号不正确'); + console.error('\n解决方法:'); + console.error(' 1. 运行 npm run dev 启动后端服务'); + console.error(' 2. 确认服务运行在正确的端口(默认3001)'); + } else if (error.response) { + console.error(`HTTP状态码: ${error.response.status}`); + console.error(`错误信息: ${error.response.statusText}`); + console.error(`响应内容: ${JSON.stringify(error.response.data, null, 2)}`); + + if (error.response.status === 403) { + console.error('\n可能的原因:'); + console.error(' 1. Token配置不一致'); + console.error(' 2. 签名验证失败'); + console.error('\n解决方法:'); + console.error(' 1. 检查 .env 文件中的 WECHAT_MP_TOKEN'); + console.error(' 2. 运行配置检查脚本:'); + console.error(' npx tsx src/modules/iit-manager/test-patient-wechat-config.ts'); + } else if (error.response.status === 404) { + console.error('\n可能的原因:'); + console.error(' 1. 路由未注册'); + console.error(' 2. URL路径不正确'); + console.error('\n解决方法:'); + console.error(' 1. 确认路由已在 routes/index.ts 中注册'); + console.error(' 2. 检查URL路径是否正确'); + } + } else { + console.error(`错误信息: ${error.message}`); + } + + console.error('\n'); + process.exit(1); + } +} + +// ==================== 5. 执行测试 ==================== + +(async () => { + try { + await testUrlVerification(); + } catch (error: any) { + console.error('测试执行异常:', error.message); + process.exit(1); + } +})(); + diff --git a/backend/src/modules/iit-manager/test-redcap-query-from-db.ts b/backend/src/modules/iit-manager/test-redcap-query-from-db.ts index 33aa3cba..44453541 100644 --- a/backend/src/modules/iit-manager/test-redcap-query-from-db.ts +++ b/backend/src/modules/iit-manager/test-redcap-query-from-db.ts @@ -248,3 +248,4 @@ main().catch((error) => { }); + diff --git a/backend/src/modules/iit-manager/test-wechat-mp-local.ps1 b/backend/src/modules/iit-manager/test-wechat-mp-local.ps1 new file mode 100644 index 00000000..fd362181 --- /dev/null +++ b/backend/src/modules/iit-manager/test-wechat-mp-local.ps1 @@ -0,0 +1,135 @@ +# 微信服务号URL验证本地测试脚本 +# 模拟微信服务器发送GET请求 + +Write-Host "🧪 微信服务号URL验证本地测试" -ForegroundColor Cyan +Write-Host "=" * 60 +Write-Host "" + +# 配置参数 +$BASE_URL = "https://devlocal.xunzhengyixue.com/wechat/patient/callback" +$TOKEN = "IitPatientWechat2026JanToken" + +# 生成测试参数 +$timestamp = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds().ToString() +$nonce = -join ((65..90) + (97..122) | Get-Random -Count 10 | ForEach-Object {[char]$_}) +$echostr = "test_echo_" + (-join ((65..90) + (97..122) + (48..57) | Get-Random -Count 10 | ForEach-Object {[char]$_})) + +Write-Host "📝 测试参数:" -ForegroundColor Yellow +Write-Host " timestamp: $timestamp" +Write-Host " nonce: $nonce" +Write-Host " echostr: $echostr" +Write-Host "" + +# 生成签名 +Write-Host "🔐 生成签名..." -ForegroundColor Yellow +$sortedArray = @($TOKEN, $timestamp, $nonce) | Sort-Object +$stringToHash = $sortedArray -join '' +$sha1 = [System.Security.Cryptography.SHA1]::Create() +$hashBytes = $sha1.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($stringToHash)) +$signature = ($hashBytes | ForEach-Object { $_.ToString("x2") }) -join '' + +Write-Host " 排序后拼接: $($stringToHash.Substring(0, [Math]::Min(50, $stringToHash.Length)))..." +Write-Host " SHA1哈希: $signature" +Write-Host "" + +# 构建完整URL +$fullUrl = "$BASE_URL`?signature=$signature×tamp=$timestamp&nonce=$nonce&echostr=$echostr" + +Write-Host "📤 发送GET请求..." -ForegroundColor Yellow +Write-Host " URL: $fullUrl" -ForegroundColor Gray +Write-Host "" + +# 发送请求(忽略SSL证书警告) +try { + # 忽略SSL证书错误(仅用于测试) + add-type @" + using System.Net; + using System.Security.Cryptography.X509Certificates; + public class TrustAllCertsPolicy : ICertificatePolicy { + public bool CheckValidationResult( + ServicePoint srvPoint, X509Certificate certificate, + WebRequest request, int certificateProblem) { + return true; + } + } +"@ + [System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy + [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 + + $response = Invoke-WebRequest -Uri $fullUrl -Method Get -UseBasicParsing + + Write-Host "=" * 60 + Write-Host "" + Write-Host "✅ 请求成功!" -ForegroundColor Green + Write-Host "" + Write-Host "HTTP状态码: $($response.StatusCode)" -ForegroundColor Green + Write-Host "返回内容类型: $($response.Headers['Content-Type'])" -ForegroundColor Green + Write-Host "返回内容: $($response.Content)" -ForegroundColor Green + Write-Host "" + + if ($response.Content -eq $echostr) { + Write-Host "✅ 返回的echostr正确,验证通过!" -ForegroundColor Green + Write-Host "" + Write-Host "=" * 60 + Write-Host "" + Write-Host "🎉 测试成功!服务端配置正确" -ForegroundColor Green + Write-Host "" + Write-Host "📝 这说明:" -ForegroundColor Cyan + Write-Host " 1. ✅ natapp映射正常" + Write-Host " 2. ✅ 后端路由正确" + Write-Host " 3. ✅ 签名验证正确" + Write-Host " 4. ✅ 返回格式正确" + Write-Host "" + Write-Host "⚠️ 但微信配置失败的原因可能是:" -ForegroundColor Yellow + Write-Host " 1. 域名devlocal.xunzhengyixue.com未在微信公众平台配置" + Write-Host " 2. 微信服务器无法访问这个域名" + Write-Host " 3. 建议使用生产域名:iit.xunzhengyixue.com" + Write-Host "" + } else { + Write-Host "❌ 返回的echostr不正确!" -ForegroundColor Red + Write-Host " 期望: $echostr" -ForegroundColor Red + Write-Host " 实际: $($response.Content)" -ForegroundColor Red + } + +} catch { + Write-Host "=" * 60 + Write-Host "" + Write-Host "❌ 请求失败" -ForegroundColor Red + Write-Host "" + Write-Host "错误信息: $($_.Exception.Message)" -ForegroundColor Red + Write-Host "" + + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode.value__ + Write-Host "HTTP状态码: $statusCode" -ForegroundColor Red + + if ($statusCode -eq 400) { + Write-Host "" + Write-Host "可能原因:" -ForegroundColor Yellow + Write-Host " 1. Schema验证失败(需要检查路由配置)" + Write-Host " 2. 参数格式不正确" + } elseif ($statusCode -eq 403) { + Write-Host "" + Write-Host "可能原因:" -ForegroundColor Yellow + Write-Host " 1. 签名验证失败" + Write-Host " 2. Token配置不匹配" + } elseif ($statusCode -eq 404) { + Write-Host "" + Write-Host "可能原因:" -ForegroundColor Yellow + Write-Host " 1. 路由未注册" + Write-Host " 2. URL路径不正确" + } + } + + Write-Host "" + Write-Host "排查建议:" -ForegroundColor Cyan + Write-Host " 1. 确认后端服务正在运行" + Write-Host " 2. 确认natapp正在运行" + Write-Host " 3. 访问健康检查接口:" + Write-Host " https://devlocal.xunzhengyixue.com/api/v1/iit/health" + Write-Host " 4. 查看后端服务日志" + Write-Host "" +} + +Write-Host "" + diff --git a/backend/src/modules/iit-manager/types/index.ts b/backend/src/modules/iit-manager/types/index.ts index beae4186..320344fe 100644 --- a/backend/src/modules/iit-manager/types/index.ts +++ b/backend/src/modules/iit-manager/types/index.ts @@ -225,3 +225,4 @@ export interface CachedProtocolRules { + diff --git a/backend/src/tests/README.md b/backend/src/tests/README.md index c4b31b9b..e8c2ac42 100644 --- a/backend/src/tests/README.md +++ b/backend/src/tests/README.md @@ -404,5 +404,6 @@ SET session_replication_role = 'origin'; + diff --git a/backend/src/tests/verify-test1-database.sql b/backend/src/tests/verify-test1-database.sql index 5a72578f..f293436b 100644 --- a/backend/src/tests/verify-test1-database.sql +++ b/backend/src/tests/verify-test1-database.sql @@ -106,5 +106,6 @@ WHERE key = 'verify_test'; + diff --git a/backend/src/tests/verify-test1-database.ts b/backend/src/tests/verify-test1-database.ts index 9e48ef02..f0c19e24 100644 --- a/backend/src/tests/verify-test1-database.ts +++ b/backend/src/tests/verify-test1-database.ts @@ -249,5 +249,6 @@ verifyDatabase() + diff --git a/backend/src/types/global.d.ts b/backend/src/types/global.d.ts index b9d7c68d..7c85ac54 100644 --- a/backend/src/types/global.d.ts +++ b/backend/src/types/global.d.ts @@ -39,5 +39,6 @@ export {} + diff --git a/backend/sync-dc-database.ps1 b/backend/sync-dc-database.ps1 index 6cab3aba..0751f5f2 100644 --- a/backend/sync-dc-database.ps1 +++ b/backend/sync-dc-database.ps1 @@ -62,5 +62,6 @@ Write-Host "✅ 完成!" -ForegroundColor Green + diff --git a/backend/test-tool-c-advanced-scenarios.mjs b/backend/test-tool-c-advanced-scenarios.mjs index 31016102..a0f9d8c1 100644 --- a/backend/test-tool-c-advanced-scenarios.mjs +++ b/backend/test-tool-c-advanced-scenarios.mjs @@ -349,5 +349,6 @@ runAdvancedTests().catch(error => { + diff --git a/backend/test-tool-c-day2.mjs b/backend/test-tool-c-day2.mjs index 9cd9d32d..30dd309e 100644 --- a/backend/test-tool-c-day2.mjs +++ b/backend/test-tool-c-day2.mjs @@ -415,5 +415,6 @@ runAllTests() + diff --git a/backend/test-tool-c-day3.mjs b/backend/test-tool-c-day3.mjs index ffdb484d..0e8dd7db 100644 --- a/backend/test-tool-c-day3.mjs +++ b/backend/test-tool-c-day3.mjs @@ -373,5 +373,6 @@ runAllTests() + diff --git a/deploy-to-sae.ps1 b/deploy-to-sae.ps1 index baa47917..bf148d04 100644 --- a/deploy-to-sae.ps1 +++ b/deploy-to-sae.ps1 @@ -157,5 +157,6 @@ Set-Location .. + diff --git a/docs/02-通用能力层/Postgres-Only异步任务处理指南.md b/docs/02-通用能力层/Postgres-Only异步任务处理指南.md index c149e053..f1fa3709 100644 --- a/docs/02-通用能力层/Postgres-Only异步任务处理指南.md +++ b/docs/02-通用能力层/Postgres-Only异步任务处理指南.md @@ -601,3 +601,4 @@ async saveProcessedData(recordId, newData) { + diff --git a/docs/02-通用能力层/通用能力层技术债务清单.md b/docs/02-通用能力层/通用能力层技术债务清单.md index 0f1e1b8b..28e91497 100644 --- a/docs/02-通用能力层/通用能力层技术债务清单.md +++ b/docs/02-通用能力层/通用能力层技术债务清单.md @@ -788,3 +788,4 @@ export const AsyncProgressBar: React.FC = ({ + diff --git a/docs/03-业务模块/ASL-AI智能文献/04-开发计划/05-全文复筛前端开发计划.md b/docs/03-业务模块/ASL-AI智能文献/04-开发计划/05-全文复筛前端开发计划.md index 378c6805..b79a17f7 100644 --- a/docs/03-业务模块/ASL-AI智能文献/04-开发计划/05-全文复筛前端开发计划.md +++ b/docs/03-业务模块/ASL-AI智能文献/04-开发计划/05-全文复筛前端开发计划.md @@ -1279,5 +1279,6 @@ interface FulltextScreeningResult { + diff --git a/docs/03-业务模块/ASL-AI智能文献/05-开发记录/2025-01-23_全文复筛前端开发完成.md b/docs/03-业务模块/ASL-AI智能文献/05-开发记录/2025-01-23_全文复筛前端开发完成.md index 19a5bc8f..a4e0e88c 100644 --- a/docs/03-业务模块/ASL-AI智能文献/05-开发记录/2025-01-23_全文复筛前端开发完成.md +++ b/docs/03-业务模块/ASL-AI智能文献/05-开发记录/2025-01-23_全文复筛前端开发完成.md @@ -393,5 +393,6 @@ GET /api/v1/asl/fulltext-screening/tasks/:taskId/export + diff --git a/docs/03-业务模块/ASL-AI智能文献/05-开发记录/2025-01-23_全文复筛前端逻辑调整.md b/docs/03-业务模块/ASL-AI智能文献/05-开发记录/2025-01-23_全文复筛前端逻辑调整.md index e8ac6f59..d64148ec 100644 --- a/docs/03-业务模块/ASL-AI智能文献/05-开发记录/2025-01-23_全文复筛前端逻辑调整.md +++ b/docs/03-业务模块/ASL-AI智能文献/05-开发记录/2025-01-23_全文复筛前端逻辑调整.md @@ -336,5 +336,6 @@ Linter错误:0个 + diff --git a/docs/03-业务模块/ASL-AI智能文献/05-开发记录/2025-11-23_Day5_全文复筛API开发.md b/docs/03-业务模块/ASL-AI智能文献/05-开发记录/2025-11-23_Day5_全文复筛API开发.md index ecd5ddff..ea199f83 100644 --- a/docs/03-业务模块/ASL-AI智能文献/05-开发记录/2025-11-23_Day5_全文复筛API开发.md +++ b/docs/03-业务模块/ASL-AI智能文献/05-开发记录/2025-11-23_Day5_全文复筛API开发.md @@ -495,5 +495,6 @@ Failed to open file '\\tmp\\extraction_service\\temp_10000_test.pdf' + diff --git a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_AI_Few-shot示例库.md b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_AI_Few-shot示例库.md index c93792bd..1743bc6a 100644 --- a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_AI_Few-shot示例库.md +++ b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_AI_Few-shot示例库.md @@ -561,5 +561,6 @@ df['creatinine'] = pd.to_numeric(df['creatinine'], errors='coerce') + diff --git a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Bug修复总结_2025-12-08.md b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Bug修复总结_2025-12-08.md index 4070c32c..5d1177de 100644 --- a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Bug修复总结_2025-12-08.md +++ b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Bug修复总结_2025-12-08.md @@ -399,5 +399,6 @@ npm run dev + diff --git a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Day3开发计划.md b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Day3开发计划.md index 7314b55b..101d718d 100644 --- a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Day3开发计划.md +++ b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Day3开发计划.md @@ -976,5 +976,6 @@ export const aiController = new AIController(); + diff --git a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Day4-5前端开发计划.md b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Day4-5前端开发计划.md index b585174a..8ac249ba 100644 --- a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Day4-5前端开发计划.md +++ b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Day4-5前端开发计划.md @@ -1310,5 +1310,6 @@ npm install react-markdown + diff --git a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Pivot列顺序优化总结.md b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Pivot列顺序优化总结.md index 4f5c1eb8..0b56165c 100644 --- a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Pivot列顺序优化总结.md +++ b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Pivot列顺序优化总结.md @@ -218,5 +218,6 @@ FMA___基线 | FMA___1个月 | FMA___2个月 + diff --git a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_方案B实施总结_2025-12-09.md b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_方案B实施总结_2025-12-09.md index e5f8bd5e..afc4c60b 100644 --- a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_方案B实施总结_2025-12-09.md +++ b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_方案B实施总结_2025-12-09.md @@ -376,5 +376,6 @@ formula = "FMA总分(0-100) / 100" + diff --git a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_缺失值处理_开发进度_2025-12-10.md b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_缺失值处理_开发进度_2025-12-10.md index 04f9a90b..2f7aeac4 100644 --- a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_缺失值处理_开发进度_2025-12-10.md +++ b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_缺失值处理_开发进度_2025-12-10.md @@ -210,5 +210,6 @@ async handleFillnaMice(request, reply) { + diff --git a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_缺失值处理功能_更新说明.md b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_缺失值处理功能_更新说明.md index dcb324bb..7052f9dd 100644 --- a/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_缺失值处理功能_更新说明.md +++ b/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_缺失值处理功能_更新说明.md @@ -182,5 +182,6 @@ method: 'mean' | 'median' | 'mode' | 'constant' | 'ffill' | 'bfill' + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-02_工作总结.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-02_工作总结.md index 48bd5a62..5210047a 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-02_工作总结.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-02_工作总结.md @@ -332,5 +332,6 @@ Changes: + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-06_工具C_Day1开发完成总结.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-06_工具C_Day1开发完成总结.md index 6a9fefc9..58ccfecc 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-06_工具C_Day1开发完成总结.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-06_工具C_Day1开发完成总结.md @@ -404,5 +404,6 @@ cd path; command + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-06_工具C_Day2开发完成总结.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-06_工具C_Day2开发完成总结.md index 11e0c4e0..e99c4235 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-06_工具C_Day2开发完成总结.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-06_工具C_Day2开发完成总结.md @@ -633,5 +633,6 @@ import { logger } from '../../../../common/logging/index.js'; + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_AI对话核心功能增强总结.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_AI对话核心功能增强总结.md index a198ed88..e45752ce 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_AI对话核心功能增强总结.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_AI对话核心功能增强总结.md @@ -637,5 +637,6 @@ Content-Length: 45234 + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_Bug修复_DataGrid空数据防御.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_Bug修复_DataGrid空数据防御.md index e3eee93e..0adf8eb5 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_Bug修复_DataGrid空数据防御.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_Bug修复_DataGrid空数据防御.md @@ -289,5 +289,6 @@ Response: + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_Day5_Ant-Design-X重构完成.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_Day5_Ant-Design-X重构完成.md index 0662b01e..ee4b6ca2 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_Day5_Ant-Design-X重构完成.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_Day5_Ant-Design-X重构完成.md @@ -442,5 +442,6 @@ Response: + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_Day5最终总结.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_Day5最终总结.md index cc4f3e7b..f782e31a 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_Day5最终总结.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_Day5最终总结.md @@ -436,5 +436,6 @@ import { ChatContainer } from '@/shared/components/Chat'; + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_UI优化与Bug修复.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_UI优化与Bug修复.md index 2be003ba..61fa3be6 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_UI优化与Bug修复.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_UI优化与Bug修复.md @@ -346,5 +346,6 @@ const initialMessages = defaultMessages.length > 0 ? defaultMessages : [{ + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_后端API完整对接完成.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_后端API完整对接完成.md index d3573b01..25021da6 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_后端API完整对接完成.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_后端API完整对接完成.md @@ -386,5 +386,6 @@ python main.py + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_完整UI优化与功能增强.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_完整UI优化与功能增强.md index 8450e46e..aa92156d 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_完整UI优化与功能增强.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_完整UI优化与功能增强.md @@ -634,5 +634,6 @@ http://localhost:5173/data-cleaning/tool-c + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_工具C_Day4前端基础完成.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_工具C_Day4前端基础完成.md index 017239fb..dfddd987 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_工具C_Day4前端基础完成.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/2025-12-07_工具C_Day4前端基础完成.md @@ -244,5 +244,6 @@ Day 5 (6-8小时): + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/DC模块重建完成总结-Day1.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/DC模块重建完成总结-Day1.md index f8352751..72fc76db 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/DC模块重建完成总结-Day1.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/DC模块重建完成总结-Day1.md @@ -422,5 +422,6 @@ Docs: docs/03-业务模块/DC-数据清洗整理/06-开发记录/DC模块重建 + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/Phase1-Portal页面开发完成-2025-12-02.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/Phase1-Portal页面开发完成-2025-12-02.md index 40f94673..a1a1de31 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/Phase1-Portal页面开发完成-2025-12-02.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/Phase1-Portal页面开发完成-2025-12-02.md @@ -397,5 +397,6 @@ const mockAssets: Asset[] = [ + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/Phase2-ToolB-Step1-2开发完成-2025-12-03.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/Phase2-ToolB-Step1-2开发完成-2025-12-03.md index 025f7b64..9ca0cf12 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/Phase2-ToolB-Step1-2开发完成-2025-12-03.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/Phase2-ToolB-Step1-2开发完成-2025-12-03.md @@ -381,5 +381,6 @@ frontend-v2/src/modules/dc/ + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/Portal页面UI优化-2025-12-02.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/Portal页面UI优化-2025-12-02.md index 6b1209ee..24572061 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/Portal页面UI优化-2025-12-02.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/Portal页面UI优化-2025-12-02.md @@ -341,5 +341,6 @@ + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/Tool-B-MVP完成总结-2025-12-03.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/Tool-B-MVP完成总结-2025-12-03.md index 755eb448..4bf5051d 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/Tool-B-MVP完成总结-2025-12-03.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/Tool-B-MVP完成总结-2025-12-03.md @@ -295,5 +295,6 @@ ConflictDetectionService // 冲突检测(字段级对比) + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/ToolB-UI优化-2025-12-03.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/ToolB-UI优化-2025-12-03.md index 3dde0042..bdd1aacb 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/ToolB-UI优化-2025-12-03.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/ToolB-UI优化-2025-12-03.md @@ -344,5 +344,6 @@ + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/ToolB-UI优化-Round2-2025-12-03.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/ToolB-UI优化-Round2-2025-12-03.md index b06d4dc3..9260dfee 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/ToolB-UI优化-Round2-2025-12-03.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/ToolB-UI优化-Round2-2025-12-03.md @@ -307,5 +307,6 @@ + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/ToolB浏览器测试计划-2025-12-03.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/ToolB浏览器测试计划-2025-12-03.md index 714af3a2..1bf04396 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/ToolB浏览器测试计划-2025-12-03.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/ToolB浏览器测试计划-2025-12-03.md @@ -371,5 +371,6 @@ + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/后端API测试报告-2025-12-02.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/后端API测试报告-2025-12-02.md index cf4afe36..07a0829e 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/后端API测试报告-2025-12-02.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/后端API测试报告-2025-12-02.md @@ -459,5 +459,6 @@ Tool B后端代码**100%复用**了平台通用能力层,无任何重复开发 + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/待办事项-下一步工作.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/待办事项-下一步工作.md index 0eaaff42..a9b2de14 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/待办事项-下一步工作.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/待办事项-下一步工作.md @@ -305,5 +305,6 @@ + diff --git a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/数据库验证报告-2025-12-02.md b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/数据库验证报告-2025-12-02.md index fadcdde8..37e6e252 100644 --- a/docs/03-业务模块/DC-数据清洗整理/06-开发记录/数据库验证报告-2025-12-02.md +++ b/docs/03-业务模块/DC-数据清洗整理/06-开发记录/数据库验证报告-2025-12-02.md @@ -236,5 +236,6 @@ $ node scripts/check-dc-tables.mjs + diff --git a/docs/03-业务模块/DC-数据清洗整理/07-技术债务/Tool-B技术债务清单.md b/docs/03-业务模块/DC-数据清洗整理/07-技术债务/Tool-B技术债务清单.md index d8575ec2..aa3cec18 100644 --- a/docs/03-业务模块/DC-数据清洗整理/07-技术债务/Tool-B技术债务清单.md +++ b/docs/03-业务模块/DC-数据清洗整理/07-技术债务/Tool-B技术债务清单.md @@ -469,5 +469,6 @@ ${fields.map((f, i) => `${i + 1}. ${f.name}:${f.desc}`).join('\n')} + diff --git a/docs/03-业务模块/IIT Manager Agent/02-技术设计/IIT Manager Agent 技术路径与架构设计.md b/docs/03-业务模块/IIT Manager Agent/02-技术设计/IIT Manager Agent 技术路径与架构设计.md index 89431cc5..b15aba62 100644 --- a/docs/03-业务模块/IIT Manager Agent/02-技术设计/IIT Manager Agent 技术路径与架构设计.md +++ b/docs/03-业务模块/IIT Manager Agent/02-技术设计/IIT Manager Agent 技术路径与架构设计.md @@ -676,3 +676,4 @@ private async processMessageAsync(xmlData: any) { **版本历史**: - v1.0 (2026-01-04): 初始版本,Phase 1.5完成 + diff --git a/docs/03-业务模块/IIT Manager Agent/04-开发计划/REDCap对接技术方案与实施指南.md b/docs/03-业务模块/IIT Manager Agent/04-开发计划/REDCap对接技术方案与实施指南.md index 1529e570..c3abea3d 100644 --- a/docs/03-业务模块/IIT Manager Agent/04-开发计划/REDCap对接技术方案与实施指南.md +++ b/docs/03-业务模块/IIT Manager Agent/04-开发计划/REDCap对接技术方案与实施指南.md @@ -1070,3 +1070,4 @@ async function testIntegration() { + diff --git a/docs/03-业务模块/IIT Manager Agent/04-开发计划/企业微信注册指南.md b/docs/03-业务模块/IIT Manager Agent/04-开发计划/企业微信注册指南.md index eb144d0d..c45366e2 100644 --- a/docs/03-业务模块/IIT Manager Agent/04-开发计划/企业微信注册指南.md +++ b/docs/03-业务模块/IIT Manager Agent/04-开发计划/企业微信注册指南.md @@ -211,3 +211,4 @@ Content-Type: application/json + diff --git a/docs/03-业务模块/IIT Manager Agent/06-开发记录/2026-01-04-Dify知识库集成开发记录.md b/docs/03-业务模块/IIT Manager Agent/06-开发记录/2026-01-04-Dify知识库集成开发记录.md index 15692aae..b0122d59 100644 --- a/docs/03-业务模块/IIT Manager Agent/06-开发记录/2026-01-04-Dify知识库集成开发记录.md +++ b/docs/03-业务模块/IIT Manager Agent/06-开发记录/2026-01-04-Dify知识库集成开发记录.md @@ -631,3 +631,4 @@ REDCap API: exportRecords success { recordCount: 1 } **✅ 测试状态**: 全部通过 **✅ 部署状态**: 已部署到开发环境 + diff --git a/docs/03-业务模块/IIT Manager Agent/06-开发记录/Day2-REDCap实时集成开发完成记录.md b/docs/03-业务模块/IIT Manager Agent/06-开发记录/Day2-REDCap实时集成开发完成记录.md index f46081ca..633b647e 100644 --- a/docs/03-业务模块/IIT Manager Agent/06-开发记录/Day2-REDCap实时集成开发完成记录.md +++ b/docs/03-业务模块/IIT Manager Agent/06-开发记录/Day2-REDCap实时集成开发完成记录.md @@ -637,3 +637,4 @@ backend/src/modules/iit-manager/ + diff --git a/docs/03-业务模块/IIT Manager Agent/06-开发记录/Day3-企业微信集成与端到端测试完成记录.md b/docs/03-业务模块/IIT Manager Agent/06-开发记录/Day3-企业微信集成与端到端测试完成记录.md index 46564e17..b860821b 100644 --- a/docs/03-业务模块/IIT Manager Agent/06-开发记录/Day3-企业微信集成与端到端测试完成记录.md +++ b/docs/03-业务模块/IIT Manager Agent/06-开发记录/Day3-企业微信集成与端到端测试完成记录.md @@ -787,3 +787,4 @@ CREATE TABLE iit_schema.wechat_tokens ( + diff --git a/docs/03-业务模块/IIT Manager Agent/06-开发记录/Day3-企业微信集成开发完成记录.md b/docs/03-业务模块/IIT Manager Agent/06-开发记录/Day3-企业微信集成开发完成记录.md index 65a0ce6f..35e2885a 100644 --- a/docs/03-业务模块/IIT Manager Agent/06-开发记录/Day3-企业微信集成开发完成记录.md +++ b/docs/03-业务模块/IIT Manager Agent/06-开发记录/Day3-企业微信集成开发完成记录.md @@ -544,3 +544,4 @@ Day 3 的开发工作虽然遇到了多个技术问题,但最终成功完成 + diff --git a/docs/03-业务模块/IIT Manager Agent/06-开发记录/Phase1.5-AI对话集成REDCap完成记录.md b/docs/03-业务模块/IIT Manager Agent/06-开发记录/Phase1.5-AI对话集成REDCap完成记录.md index 0837304e..835f5501 100644 --- a/docs/03-业务模块/IIT Manager Agent/06-开发记录/Phase1.5-AI对话集成REDCap完成记录.md +++ b/docs/03-业务模块/IIT Manager Agent/06-开发记录/Phase1.5-AI对话集成REDCap完成记录.md @@ -311,3 +311,4 @@ AI: "出生日期:2017-01-04 **下一步**: Phase 2 - Function Calling + Dify知识库 + diff --git a/docs/03-业务模块/IIT Manager Agent/06-开发记录/V1.1更新完成报告.md b/docs/03-业务模块/IIT Manager Agent/06-开发记录/V1.1更新完成报告.md index 62ab0503..1b32ce7c 100644 --- a/docs/03-业务模块/IIT Manager Agent/06-开发记录/V1.1更新完成报告.md +++ b/docs/03-业务模块/IIT Manager Agent/06-开发记录/V1.1更新完成报告.md @@ -255,3 +255,4 @@ Day 4: REDCap EM(Webhook推送)← 作为增强,而非核心 + diff --git a/docs/03-业务模块/IIT Manager Agent/07-技术债务/IIT Manager Agent 技术债务清单.md b/docs/03-业务模块/IIT Manager Agent/07-技术债务/IIT Manager Agent 技术债务清单.md index 37b0d630..ccbc77e9 100644 --- a/docs/03-业务模块/IIT Manager Agent/07-技术债务/IIT Manager Agent 技术债务清单.md +++ b/docs/03-业务模块/IIT Manager Agent/07-技术债务/IIT Manager Agent 技术债务清单.md @@ -669,3 +669,4 @@ const answer = `根据研究方案[1]和CRF表格[2],纳入标准包括: **版本历史**: - v1.0 (2026-01-04): 初始版本,Phase 1.5完成后整理 + diff --git a/docs/03-业务模块/Redcap/01-部署与配置/10-REDCap_Docker部署操作手册.md b/docs/03-业务模块/Redcap/01-部署与配置/10-REDCap_Docker部署操作手册.md index 68237e3b..63a7fbc5 100644 --- a/docs/03-业务模块/Redcap/01-部署与配置/10-REDCap_Docker部署操作手册.md +++ b/docs/03-业务模块/Redcap/01-部署与配置/10-REDCap_Docker部署操作手册.md @@ -759,3 +759,4 @@ docker exec redcap-apache php /tmp/create-redcap-password.php + diff --git a/docs/03-业务模块/Redcap/README.md b/docs/03-业务模块/Redcap/README.md index 9a471846..9afa2f7a 100644 --- a/docs/03-业务模块/Redcap/README.md +++ b/docs/03-业务模块/Redcap/README.md @@ -141,3 +141,4 @@ AIclinicalresearch/redcap-docker-dev/ + diff --git a/docs/05-部署文档/02-SAE部署完全指南(产品经理版).md b/docs/05-部署文档/02-SAE部署完全指南(产品经理版).md index af64dd51..7789658d 100644 --- a/docs/05-部署文档/02-SAE部署完全指南(产品经理版).md +++ b/docs/05-部署文档/02-SAE部署完全指南(产品经理版).md @@ -876,5 +876,6 @@ ACR镜像仓库: + diff --git a/docs/05-部署文档/07-前端Nginx-SAE部署操作手册.md b/docs/05-部署文档/07-前端Nginx-SAE部署操作手册.md index 59ee0591..57eab9f9 100644 --- a/docs/05-部署文档/07-前端Nginx-SAE部署操作手册.md +++ b/docs/05-部署文档/07-前端Nginx-SAE部署操作手册.md @@ -1365,3 +1365,4 @@ SAE应用配置: + diff --git a/docs/05-部署文档/08-PostgreSQL数据库部署操作手册.md b/docs/05-部署文档/08-PostgreSQL数据库部署操作手册.md index bd666340..af57b454 100644 --- a/docs/05-部署文档/08-PostgreSQL数据库部署操作手册.md +++ b/docs/05-部署文档/08-PostgreSQL数据库部署操作手册.md @@ -1181,3 +1181,4 @@ docker exec -e PGPASSWORD="密码" ai-clinical-postgres psql -h RDS地址 -U air + diff --git a/docs/05-部署文档/10-Node.js后端-Docker镜像构建手册.md b/docs/05-部署文档/10-Node.js后端-Docker镜像构建手册.md index f419a2c8..cab51661 100644 --- a/docs/05-部署文档/10-Node.js后端-Docker镜像构建手册.md +++ b/docs/05-部署文档/10-Node.js后端-Docker镜像构建手册.md @@ -592,3 +592,4 @@ scripts/*.ts + diff --git a/docs/05-部署文档/11-Node.js后端-SAE部署配置清单.md b/docs/05-部署文档/11-Node.js后端-SAE部署配置清单.md index ed526195..804bfbc0 100644 --- a/docs/05-部署文档/11-Node.js后端-SAE部署配置清单.md +++ b/docs/05-部署文档/11-Node.js后端-SAE部署配置清单.md @@ -280,3 +280,4 @@ Node.js后端部署成功后: + diff --git a/docs/05-部署文档/12-Node.js后端-SAE部署操作手册.md b/docs/05-部署文档/12-Node.js后端-SAE部署操作手册.md index 3f5ea6bf..2eeff169 100644 --- a/docs/05-部署文档/12-Node.js后端-SAE部署操作手册.md +++ b/docs/05-部署文档/12-Node.js后端-SAE部署操作手册.md @@ -503,3 +503,4 @@ Node.js后端 (SAE) ← http://172.17.173.88:3001 + diff --git a/docs/05-部署文档/13-Node.js后端-镜像修复记录.md b/docs/05-部署文档/13-Node.js后端-镜像修复记录.md index 7b5e28187..f57bd18f 100644 --- a/docs/05-部署文档/13-Node.js后端-镜像修复记录.md +++ b/docs/05-部署文档/13-Node.js后端-镜像修复记录.md @@ -218,3 +218,4 @@ curl http://localhost:3001/health + diff --git a/docs/05-部署文档/14-Node.js后端-pino-pretty问题修复.md b/docs/05-部署文档/14-Node.js后端-pino-pretty问题修复.md index 96d3154f..f81e6c90 100644 --- a/docs/05-部署文档/14-Node.js后端-pino-pretty问题修复.md +++ b/docs/05-部署文档/14-Node.js后端-pino-pretty问题修复.md @@ -256,3 +256,4 @@ npm run dev + diff --git a/docs/05-部署文档/16-前端Nginx-部署成功总结.md b/docs/05-部署文档/16-前端Nginx-部署成功总结.md index 09b18a95..a10ca2be 100644 --- a/docs/05-部署文档/16-前端Nginx-部署成功总结.md +++ b/docs/05-部署文档/16-前端Nginx-部署成功总结.md @@ -480,3 +480,4 @@ pgm-2zex1m2y3r23hdn5.pg.rds.aliyuncs.com:5432 + diff --git a/docs/05-部署文档/17-完整部署实战手册-2025版.md b/docs/05-部署文档/17-完整部署实战手册-2025版.md index b215f482..f774d288 100644 --- a/docs/05-部署文档/17-完整部署实战手册-2025版.md +++ b/docs/05-部署文档/17-完整部署实战手册-2025版.md @@ -1808,3 +1808,4 @@ curl http://8.140.53.236/ + diff --git a/docs/05-部署文档/18-部署文档使用指南.md b/docs/05-部署文档/18-部署文档使用指南.md index 543b0feb..c928d09b 100644 --- a/docs/05-部署文档/18-部署文档使用指南.md +++ b/docs/05-部署文档/18-部署文档使用指南.md @@ -356,3 +356,4 @@ crpi-cd5ij4pjt65mweeo.cn-beijing.personal.cr.aliyuncs.com/ai-clinical/backend-se + diff --git a/docs/05-部署文档/19-日常更新快速操作手册.md b/docs/05-部署文档/19-日常更新快速操作手册.md index db8cfc79..2d0f5e26 100644 --- a/docs/05-部署文档/19-日常更新快速操作手册.md +++ b/docs/05-部署文档/19-日常更新快速操作手册.md @@ -678,3 +678,4 @@ docker login --username=gofeng117@163.com \ + diff --git a/docs/05-部署文档/文档修正报告-20251214.md b/docs/05-部署文档/文档修正报告-20251214.md index 5280532d..a38ba9e6 100644 --- a/docs/05-部署文档/文档修正报告-20251214.md +++ b/docs/05-部署文档/文档修正报告-20251214.md @@ -487,5 +487,6 @@ NAT网关成本¥100/月,对初创团队是一笔开销 + diff --git a/docs/07-运维文档/03-SAE环境变量配置指南.md b/docs/07-运维文档/03-SAE环境变量配置指南.md index ef63e534..faf26a30 100644 --- a/docs/07-运维文档/03-SAE环境变量配置指南.md +++ b/docs/07-运维文档/03-SAE环境变量配置指南.md @@ -392,5 +392,6 @@ curl http://你的SAE地址:3001/health + diff --git a/docs/07-运维文档/05-Redis缓存与队列的区别说明.md b/docs/07-运维文档/05-Redis缓存与队列的区别说明.md index ce9a1877..7b1d1eec 100644 --- a/docs/07-运维文档/05-Redis缓存与队列的区别说明.md +++ b/docs/07-运维文档/05-Redis缓存与队列的区别说明.md @@ -724,5 +724,6 @@ const job = await queue.getJob(jobId); + diff --git a/docs/07-运维文档/06-长时间任务可靠性分析.md b/docs/07-运维文档/06-长时间任务可靠性分析.md index c936724e..0ced7457 100644 --- a/docs/07-运维文档/06-长时间任务可靠性分析.md +++ b/docs/07-运维文档/06-长时间任务可靠性分析.md @@ -491,5 +491,6 @@ processLiteraturesInBackground(task.id, projectId, testLiteratures); + diff --git a/docs/07-运维文档/07-Redis使用需求分析(按模块).md b/docs/07-运维文档/07-Redis使用需求分析(按模块).md index b0bff01b..1abbf145 100644 --- a/docs/07-运维文档/07-Redis使用需求分析(按模块).md +++ b/docs/07-运维文档/07-Redis使用需求分析(按模块).md @@ -968,5 +968,6 @@ ROI = (¥22,556 - ¥144) / ¥144 × 100% = 15,564% + diff --git a/docs/08-项目管理/03-每周计划/2025-12-13-Postgres-Only架构改造完成.md b/docs/08-项目管理/03-每周计划/2025-12-13-Postgres-Only架构改造完成.md index 9a7628b5..5f4c2ff6 100644 --- a/docs/08-项目管理/03-每周计划/2025-12-13-Postgres-Only架构改造完成.md +++ b/docs/08-项目管理/03-每周计划/2025-12-13-Postgres-Only架构改造完成.md @@ -1025,5 +1025,6 @@ Redis 实例:¥500/月 + diff --git a/docs/08-项目管理/05-技术债务/通用对话服务抽取计划.md b/docs/08-项目管理/05-技术债务/通用对话服务抽取计划.md index fdfa6370..97fb0577 100644 --- a/docs/08-项目管理/05-技术债务/通用对话服务抽取计划.md +++ b/docs/08-项目管理/05-技术债务/通用对话服务抽取计划.md @@ -483,5 +483,6 @@ import { ChatContainer } from '@/shared/components/Chat'; + diff --git a/docs/08-项目管理/PKB和RVW功能迁移计划.md b/docs/08-项目管理/PKB和RVW功能迁移计划.md index 1525fd85..e059cf2b 100644 --- a/docs/08-项目管理/PKB和RVW功能迁移计划.md +++ b/docs/08-项目管理/PKB和RVW功能迁移计划.md @@ -925,3 +925,4 @@ CREATE INDEX idx_rvw_tasks_created_at ON rvw_schema.review_tasks(created_at); + diff --git a/extraction_service/.dockerignore b/extraction_service/.dockerignore index f6c9a683..754a1bb1 100644 --- a/extraction_service/.dockerignore +++ b/extraction_service/.dockerignore @@ -60,3 +60,4 @@ models/ + diff --git a/extraction_service/operations/__init__.py b/extraction_service/operations/__init__.py index 03fdacc4..62d70ea6 100644 --- a/extraction_service/operations/__init__.py +++ b/extraction_service/operations/__init__.py @@ -46,5 +46,6 @@ __version__ = '1.0.0' + diff --git a/extraction_service/operations/dropna.py b/extraction_service/operations/dropna.py index 933a00cd..2f331813 100644 --- a/extraction_service/operations/dropna.py +++ b/extraction_service/operations/dropna.py @@ -179,5 +179,6 @@ def get_missing_summary(df: pd.DataFrame) -> dict: + diff --git a/extraction_service/operations/filter.py b/extraction_service/operations/filter.py index 1a27920d..8762e991 100644 --- a/extraction_service/operations/filter.py +++ b/extraction_service/operations/filter.py @@ -139,5 +139,6 @@ def apply_filter( + diff --git a/extraction_service/operations/unpivot.py b/extraction_service/operations/unpivot.py index 72c002f6..8d35ba34 100644 --- a/extraction_service/operations/unpivot.py +++ b/extraction_service/operations/unpivot.py @@ -303,5 +303,6 @@ def get_unpivot_preview( + diff --git a/extraction_service/test_dc_api.py b/extraction_service/test_dc_api.py index 255efa41..1b178d54 100644 --- a/extraction_service/test_dc_api.py +++ b/extraction_service/test_dc_api.py @@ -313,5 +313,6 @@ if __name__ == "__main__": + diff --git a/extraction_service/test_execute_simple.py b/extraction_service/test_execute_simple.py index e7494a4d..1a770594 100644 --- a/extraction_service/test_execute_simple.py +++ b/extraction_service/test_execute_simple.py @@ -79,5 +79,6 @@ except Exception as e: + diff --git a/extraction_service/test_module.py b/extraction_service/test_module.py index 30341c47..1d5a9dec 100644 --- a/extraction_service/test_module.py +++ b/extraction_service/test_module.py @@ -59,5 +59,6 @@ except Exception as e: + diff --git a/frontend-v2/.dockerignore b/frontend-v2/.dockerignore index c309e21d..113ef133 100644 --- a/frontend-v2/.dockerignore +++ b/frontend-v2/.dockerignore @@ -80,3 +80,4 @@ vite.config.*.timestamp-* + diff --git a/frontend-v2/docker-entrypoint.sh b/frontend-v2/docker-entrypoint.sh index edfe7d07..d4d5fb28 100644 --- a/frontend-v2/docker-entrypoint.sh +++ b/frontend-v2/docker-entrypoint.sh @@ -47,3 +47,4 @@ exec nginx -g 'daemon off;' + diff --git a/frontend-v2/nginx.conf b/frontend-v2/nginx.conf index d41677ba..01352d90 100644 --- a/frontend-v2/nginx.conf +++ b/frontend-v2/nginx.conf @@ -203,3 +203,4 @@ http { + diff --git a/frontend-v2/src/modules/asl/components/FulltextDetailDrawer.tsx b/frontend-v2/src/modules/asl/components/FulltextDetailDrawer.tsx index 648e1819..0d744cca 100644 --- a/frontend-v2/src/modules/asl/components/FulltextDetailDrawer.tsx +++ b/frontend-v2/src/modules/asl/components/FulltextDetailDrawer.tsx @@ -548,5 +548,6 @@ export default FulltextDetailDrawer; + diff --git a/frontend-v2/src/modules/dc/hooks/useAssets.ts b/frontend-v2/src/modules/dc/hooks/useAssets.ts index 134797cd..dee62731 100644 --- a/frontend-v2/src/modules/dc/hooks/useAssets.ts +++ b/frontend-v2/src/modules/dc/hooks/useAssets.ts @@ -141,5 +141,6 @@ export const useAssets = (activeTab: AssetTabType) => { + diff --git a/frontend-v2/src/modules/dc/hooks/useRecentTasks.ts b/frontend-v2/src/modules/dc/hooks/useRecentTasks.ts index 6695a3b2..883d2036 100644 --- a/frontend-v2/src/modules/dc/hooks/useRecentTasks.ts +++ b/frontend-v2/src/modules/dc/hooks/useRecentTasks.ts @@ -131,5 +131,6 @@ export const useRecentTasks = () => { + diff --git a/frontend-v2/src/modules/dc/pages/tool-c/components/DropnaDialog.tsx b/frontend-v2/src/modules/dc/pages/tool-c/components/DropnaDialog.tsx index 3eebb47b..255aacc3 100644 --- a/frontend-v2/src/modules/dc/pages/tool-c/components/DropnaDialog.tsx +++ b/frontend-v2/src/modules/dc/pages/tool-c/components/DropnaDialog.tsx @@ -330,5 +330,6 @@ export default DropnaDialog; + diff --git a/frontend-v2/src/modules/dc/pages/tool-c/components/MetricTimePanel.tsx b/frontend-v2/src/modules/dc/pages/tool-c/components/MetricTimePanel.tsx index 41c657b4..b169fba6 100644 --- a/frontend-v2/src/modules/dc/pages/tool-c/components/MetricTimePanel.tsx +++ b/frontend-v2/src/modules/dc/pages/tool-c/components/MetricTimePanel.tsx @@ -416,4 +416,5 @@ export default MetricTimePanel; + diff --git a/frontend-v2/src/modules/dc/pages/tool-c/components/PivotPanel.tsx b/frontend-v2/src/modules/dc/pages/tool-c/components/PivotPanel.tsx index e1d3e053..31fe9ce2 100644 --- a/frontend-v2/src/modules/dc/pages/tool-c/components/PivotPanel.tsx +++ b/frontend-v2/src/modules/dc/pages/tool-c/components/PivotPanel.tsx @@ -301,5 +301,6 @@ export default PivotPanel; + diff --git a/frontend-v2/src/modules/dc/pages/tool-c/hooks/useSessionStatus.ts b/frontend-v2/src/modules/dc/pages/tool-c/hooks/useSessionStatus.ts index 36c628ad..888e3515 100644 --- a/frontend-v2/src/modules/dc/pages/tool-c/hooks/useSessionStatus.ts +++ b/frontend-v2/src/modules/dc/pages/tool-c/hooks/useSessionStatus.ts @@ -103,3 +103,4 @@ export function useSessionStatus({ + diff --git a/frontend-v2/src/modules/dc/pages/tool-c/types/index.ts b/frontend-v2/src/modules/dc/pages/tool-c/types/index.ts index 4ad5593b..500e944f 100644 --- a/frontend-v2/src/modules/dc/pages/tool-c/types/index.ts +++ b/frontend-v2/src/modules/dc/pages/tool-c/types/index.ts @@ -93,5 +93,6 @@ export interface DataStats { + diff --git a/frontend-v2/src/modules/dc/types/portal.ts b/frontend-v2/src/modules/dc/types/portal.ts index 5a989ba4..d1de509d 100644 --- a/frontend-v2/src/modules/dc/types/portal.ts +++ b/frontend-v2/src/modules/dc/types/portal.ts @@ -89,5 +89,6 @@ export type AssetTabType = 'all' | 'processed' | 'raw'; + diff --git a/frontend-v2/src/shared/components/index.ts b/frontend-v2/src/shared/components/index.ts index 640dbe2e..72f03e0b 100644 --- a/frontend-v2/src/shared/components/index.ts +++ b/frontend-v2/src/shared/components/index.ts @@ -44,5 +44,6 @@ export { default as Placeholder } from './Placeholder'; + diff --git a/frontend-v2/src/vite-env.d.ts b/frontend-v2/src/vite-env.d.ts index cd1b48eb..b695682c 100644 --- a/frontend-v2/src/vite-env.d.ts +++ b/frontend-v2/src/vite-env.d.ts @@ -26,3 +26,4 @@ interface ImportMeta { + diff --git a/git-cleanup-redcap.ps1 b/git-cleanup-redcap.ps1 index a65cef46..3acc28de 100644 --- a/git-cleanup-redcap.ps1 +++ b/git-cleanup-redcap.ps1 @@ -19,3 +19,4 @@ Write-Host "Next step: Run the commit command" -ForegroundColor Cyan + diff --git a/git-commit-day1.ps1 b/git-commit-day1.ps1 index 98a3e3d9..d1b50781 100644 --- a/git-commit-day1.ps1 +++ b/git-commit-day1.ps1 @@ -75,3 +75,4 @@ Write-Host "Git commit and push completed!" -ForegroundColor Green + diff --git a/git-fix-lock.ps1 b/git-fix-lock.ps1 index 6c8434f2..1550b9e0 100644 --- a/git-fix-lock.ps1 +++ b/git-fix-lock.ps1 @@ -23,3 +23,4 @@ Write-Host "Now you can run git commands again." -ForegroundColor Cyan + diff --git a/python-microservice/operations/__init__.py b/python-microservice/operations/__init__.py index 03fdacc4..62d70ea6 100644 --- a/python-microservice/operations/__init__.py +++ b/python-microservice/operations/__init__.py @@ -46,5 +46,6 @@ __version__ = '1.0.0' + diff --git a/python-microservice/operations/binning.py b/python-microservice/operations/binning.py index 4f88f27e..47c6c5a7 100644 --- a/python-microservice/operations/binning.py +++ b/python-microservice/operations/binning.py @@ -153,5 +153,6 @@ def apply_binning( + diff --git a/python-microservice/operations/filter.py b/python-microservice/operations/filter.py index 1a27920d..8762e991 100644 --- a/python-microservice/operations/filter.py +++ b/python-microservice/operations/filter.py @@ -139,5 +139,6 @@ def apply_filter( + diff --git a/python-microservice/operations/recode.py b/python-microservice/operations/recode.py index cc2e24f7..d1081bcf 100644 --- a/python-microservice/operations/recode.py +++ b/python-microservice/operations/recode.py @@ -109,5 +109,6 @@ def apply_recode( + diff --git a/recover_dc_code.py b/recover_dc_code.py index af2ff838..39634645 100644 --- a/recover_dc_code.py +++ b/recover_dc_code.py @@ -253,5 +253,6 @@ if __name__ == "__main__": + diff --git a/redcap-docker-dev/.gitattributes b/redcap-docker-dev/.gitattributes index f7330e0c..c31cf2d4 100644 --- a/redcap-docker-dev/.gitattributes +++ b/redcap-docker-dev/.gitattributes @@ -35,3 +35,4 @@ + diff --git a/redcap-docker-dev/.gitignore b/redcap-docker-dev/.gitignore index 293c28c3..99e82934 100644 --- a/redcap-docker-dev/.gitignore +++ b/redcap-docker-dev/.gitignore @@ -66,3 +66,4 @@ Desktop.ini + diff --git a/redcap-docker-dev/README.md b/redcap-docker-dev/README.md index 0b9f624f..26184fff 100644 --- a/redcap-docker-dev/README.md +++ b/redcap-docker-dev/README.md @@ -367,3 +367,4 @@ docker-compose -f docker-compose.prod.yml up -d + diff --git a/redcap-docker-dev/docker-compose.prod.yml b/redcap-docker-dev/docker-compose.prod.yml index 759ef2e0..689322b3 100644 --- a/redcap-docker-dev/docker-compose.prod.yml +++ b/redcap-docker-dev/docker-compose.prod.yml @@ -128,3 +128,4 @@ volumes: + diff --git a/redcap-docker-dev/docker-compose.yml b/redcap-docker-dev/docker-compose.yml index c5bfb7f4..d344b5f9 100644 --- a/redcap-docker-dev/docker-compose.yml +++ b/redcap-docker-dev/docker-compose.yml @@ -126,3 +126,4 @@ volumes: + diff --git a/redcap-docker-dev/env.template b/redcap-docker-dev/env.template index 4486e42f..a1f2ff9e 100644 --- a/redcap-docker-dev/env.template +++ b/redcap-docker-dev/env.template @@ -62,3 +62,4 @@ PMA_UPLOAD_LIMIT=50M + diff --git a/redcap-docker-dev/scripts/clean-redcap.ps1 b/redcap-docker-dev/scripts/clean-redcap.ps1 index 49ae4af0..48b0b8c4 100644 --- a/redcap-docker-dev/scripts/clean-redcap.ps1 +++ b/redcap-docker-dev/scripts/clean-redcap.ps1 @@ -70,3 +70,4 @@ Write-Host "" + diff --git a/redcap-docker-dev/scripts/create-redcap-password.php b/redcap-docker-dev/scripts/create-redcap-password.php index 9cd2a6c1..bacd1526 100644 --- a/redcap-docker-dev/scripts/create-redcap-password.php +++ b/redcap-docker-dev/scripts/create-redcap-password.php @@ -48,3 +48,4 @@ try { + diff --git a/redcap-docker-dev/scripts/logs-redcap.ps1 b/redcap-docker-dev/scripts/logs-redcap.ps1 index 4c2d0430..38749ce0 100644 --- a/redcap-docker-dev/scripts/logs-redcap.ps1 +++ b/redcap-docker-dev/scripts/logs-redcap.ps1 @@ -61,3 +61,4 @@ Write-Host "" + diff --git a/redcap-docker-dev/scripts/reset-admin-password.php b/redcap-docker-dev/scripts/reset-admin-password.php index 25390cc3..2f918c83 100644 --- a/redcap-docker-dev/scripts/reset-admin-password.php +++ b/redcap-docker-dev/scripts/reset-admin-password.php @@ -24,3 +24,4 @@ if ($result) { + diff --git a/redcap-docker-dev/scripts/start-redcap.ps1 b/redcap-docker-dev/scripts/start-redcap.ps1 index 8f175b5b..59aaa4e1 100644 --- a/redcap-docker-dev/scripts/start-redcap.ps1 +++ b/redcap-docker-dev/scripts/start-redcap.ps1 @@ -46,3 +46,4 @@ if ($LASTEXITCODE -eq 0) { + diff --git a/redcap-docker-dev/scripts/stop-redcap.ps1 b/redcap-docker-dev/scripts/stop-redcap.ps1 index 8f3a6954..8a2f1437 100644 --- a/redcap-docker-dev/scripts/stop-redcap.ps1 +++ b/redcap-docker-dev/scripts/stop-redcap.ps1 @@ -32,3 +32,4 @@ if ($LASTEXITCODE -eq 0) { + diff --git a/run_recovery.ps1 b/run_recovery.ps1 index 4bf43791..cc037381 100644 --- a/run_recovery.ps1 +++ b/run_recovery.ps1 @@ -77,5 +77,6 @@ Write-Host "==================================================================== + diff --git a/tests/QUICKSTART_快速开始.md b/tests/QUICKSTART_快速开始.md index 10764afb..615c99fc 100644 --- a/tests/QUICKSTART_快速开始.md +++ b/tests/QUICKSTART_快速开始.md @@ -124,5 +124,6 @@ INFO: Uvicorn running on http://0.0.0.0:8001 + diff --git a/tests/README_测试说明.md b/tests/README_测试说明.md index 874c4991..94efb366 100644 --- a/tests/README_测试说明.md +++ b/tests/README_测试说明.md @@ -280,5 +280,6 @@ df_numeric.to_excel('test_data/numeric_test.xlsx', index=False) + diff --git a/tests/run_tests.bat b/tests/run_tests.bat index 9304d25b..8ed61e4f 100644 --- a/tests/run_tests.bat +++ b/tests/run_tests.bat @@ -75,5 +75,6 @@ pause + diff --git a/tests/run_tests.sh b/tests/run_tests.sh index 4c318e8b..7849e053 100644 --- a/tests/run_tests.sh +++ b/tests/run_tests.sh @@ -71,5 +71,6 @@ echo "========================================" + diff --git a/快速部署到SAE.md b/快速部署到SAE.md index d03693ee..929065fb 100644 --- a/快速部署到SAE.md +++ b/快速部署到SAE.md @@ -336,5 +336,6 @@ OSS AccessKeySecret:_______________ + diff --git a/部署检查清单.md b/部署检查清单.md index 060d84e4..08c4d7f8 100644 --- a/部署检查清单.md +++ b/部署检查清单.md @@ -372,5 +372,6 @@ OSS配置: +