feat(iit-manager): Add WeChat Official Account integration for patient notifications

Features:
- PatientWechatCallbackController for URL verification and message handling
- PatientWechatService for template and customer messages
- Support for secure mode (message encryption/decryption)
- Simplified route /wechat/patient/callback for WeChat config
- Event handlers for subscribe/unsubscribe/text messages
- Template message for visit reminders

Technical details:
- Reuse @wecom/crypto for encryption (compatible with Official Account)
- Relaxed Fastify schema validation to prevent early request blocking
- Access token caching (7000s with 5min pre-refresh)
- Comprehensive logging for debugging

Testing: Local URL verification passed, ready for SAE deployment

Status: Code complete, waiting for WeChat platform configuration
This commit is contained in:
2026-01-04 22:53:42 +08:00
parent dfc472810b
commit b31255031e
167 changed files with 3055 additions and 2 deletions

View File

@@ -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);
}
})();