feat(pkb): Complete PKB module frontend migration with V3 design
Summary: - Implement PKB Dashboard and Workspace pages based on V3 prototype - Add single-layer header with integrated Tab navigation - Implement 3 work modes: Full Text, Deep Read, Batch Processing - Integrate Ant Design X Chat component for AI conversations - Create BatchModeComplete with template selection and document processing - Add compact work mode selector with dropdown design Backend: - Migrate PKB controllers and services to /modules/pkb structure - Register v2 API routes at /api/v2/pkb/knowledge - Maintain dual API routes for backward compatibility Technical details: - Use Zustand for state management - Handle SSE streaming responses for AI chat - Support document selection for Deep Read mode - Implement batch processing with progress tracking Known issues: - Batch processing API integration pending - Knowledge assets page navigation needs optimization Status: Frontend functional, pending refinement
This commit is contained in:
@@ -170,3 +170,7 @@ logger.info('[SessionMemory] 会话记忆管理器已启动', {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -104,3 +104,7 @@ async function checkTableStructure() {
|
||||
checkTableStructure();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -91,3 +91,7 @@ checkProjectConfig().catch(console.error);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -73,3 +73,7 @@ main();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,335 @@
|
||||
/**
|
||||
* 微信服务号回调控制器 - 明文模式(Plain Text Mode)
|
||||
*
|
||||
* 功能:
|
||||
* 1. URL验证(GET请求)
|
||||
* 2. 接收消息推送(POST请求)
|
||||
* 3. 签名校验(SHA1)
|
||||
* 4. 不进行消息加解密
|
||||
*
|
||||
* 官方文档:
|
||||
* https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Access_Overview.html
|
||||
*
|
||||
* @author AI Assistant
|
||||
* @date 2026-01-04
|
||||
*/
|
||||
|
||||
import { FastifyRequest, FastifyReply } from 'fastify';
|
||||
import crypto from 'crypto';
|
||||
import { logger } from '../../../common/logging/index.js';
|
||||
|
||||
/**
|
||||
* URL验证请求参数
|
||||
*/
|
||||
interface WechatMpVerifyQuery {
|
||||
signature: string; // 微信加密签名
|
||||
timestamp: string; // 时间戳
|
||||
nonce: string; // 随机数
|
||||
echostr: string; // 随机字符串
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息推送回调请求参数
|
||||
*/
|
||||
interface WechatMpCallbackQuery {
|
||||
signature: string; // 微信加密签名
|
||||
timestamp: string; // 时间戳
|
||||
nonce: string; // 随机数
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信服务号回调控制器(明文模式)
|
||||
*/
|
||||
export class PatientWechatCallbackPlainController {
|
||||
private token: string;
|
||||
|
||||
constructor() {
|
||||
// 从环境变量读取Token
|
||||
this.token = process.env.WECHAT_MP_TOKEN || '';
|
||||
|
||||
if (!this.token) {
|
||||
logger.error('⚠️ WECHAT_MP_TOKEN 未配置!');
|
||||
throw new Error('微信服务号Token未配置');
|
||||
}
|
||||
|
||||
logger.info('✅ 微信服务号回调控制器已初始化(明文模式)', {
|
||||
token: this.token.substring(0, 10) + '...',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理URL验证(GET请求)
|
||||
*
|
||||
* 微信服务器会发送GET请求验证服务器地址的有效性:
|
||||
* GET /wechat/patient/callback?signature=xxx×tamp=xxx&nonce=xxx&echostr=xxx
|
||||
*
|
||||
* 验证步骤:
|
||||
* 1. 将token、timestamp、nonce三个参数进行字典序排序
|
||||
* 2. 将三个参数字符串拼接成一个字符串进行sha1加密
|
||||
* 3. 将加密后的字符串与signature对比,如果相同则返回echostr
|
||||
*/
|
||||
async handleVerification(
|
||||
request: FastifyRequest<{ Querystring: WechatMpVerifyQuery }>,
|
||||
reply: FastifyReply
|
||||
): Promise<void> {
|
||||
const { signature, timestamp, nonce, echostr } = request.query;
|
||||
|
||||
logger.info('📥 收到微信服务号 URL 验证请求(明文模式)', {
|
||||
signature,
|
||||
timestamp,
|
||||
nonce,
|
||||
echostr: echostr ? echostr.substring(0, 20) + '...' : undefined,
|
||||
});
|
||||
|
||||
try {
|
||||
// 验证签名
|
||||
const isValid = this.verifySignature(signature, timestamp, nonce);
|
||||
|
||||
if (!isValid) {
|
||||
logger.error('❌ URL 验证失败:签名不匹配', {
|
||||
signature,
|
||||
timestamp,
|
||||
nonce,
|
||||
});
|
||||
reply.code(403).send('Signature verification failed');
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info('✅ URL 验证成功,返回 echostr', {
|
||||
echostr: echostr.substring(0, 20) + '...',
|
||||
});
|
||||
|
||||
// 验证成功,原样返回echostr(纯文本)
|
||||
reply.type('text/plain').send(echostr);
|
||||
} catch (error) {
|
||||
logger.error('❌ URL 验证异常', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
});
|
||||
reply.code(500).send('Internal Server Error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理消息推送回调(POST请求)
|
||||
*
|
||||
* 微信服务器推送消息时会发送POST请求:
|
||||
* POST /wechat/patient/callback?signature=xxx×tamp=xxx&nonce=xxx
|
||||
* Body: XML格式的消息内容(明文模式)
|
||||
*/
|
||||
async handleCallback(
|
||||
request: FastifyRequest<{
|
||||
Querystring: WechatMpCallbackQuery;
|
||||
Body: string; // XML格式字符串
|
||||
}>,
|
||||
reply: FastifyReply
|
||||
): Promise<void> {
|
||||
const { signature, timestamp, nonce } = request.query;
|
||||
const body = request.body;
|
||||
|
||||
logger.info('📥 收到微信服务号回调消息(明文模式)', {
|
||||
signature,
|
||||
timestamp,
|
||||
nonce,
|
||||
bodyType: typeof body,
|
||||
bodyLength: JSON.stringify(body).length,
|
||||
});
|
||||
|
||||
try {
|
||||
// 验证签名
|
||||
const isValid = this.verifySignature(signature, timestamp, nonce);
|
||||
|
||||
if (!isValid) {
|
||||
logger.error('❌ 消息推送验证失败:签名不匹配', {
|
||||
signature,
|
||||
timestamp,
|
||||
nonce,
|
||||
});
|
||||
reply.code(403).send('Signature verification failed');
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info('✅ 签名验证成功');
|
||||
|
||||
// 立即返回success(5秒内必须返回)
|
||||
reply.type('text/plain').send('success');
|
||||
|
||||
// 异步处理消息
|
||||
this.processMessageAsync(body).catch((error) => {
|
||||
logger.error('❌ 异步消息处理失败', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('❌ 消息推送处理异常', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
});
|
||||
reply.code(500).send('Internal Server Error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证微信签名(明文模式)
|
||||
*
|
||||
* 签名计算方法:
|
||||
* 1. 将token、timestamp、nonce三个参数进行字典序排序
|
||||
* 2. 将三个参数字符串拼接成一个字符串
|
||||
* 3. 进行sha1加密
|
||||
* 4. 与signature参数对比
|
||||
*
|
||||
* @param signature 微信传递的签名
|
||||
* @param timestamp 时间戳
|
||||
* @param nonce 随机数
|
||||
* @returns 验证结果
|
||||
*/
|
||||
private verifySignature(signature: string, timestamp: string, nonce: string): boolean {
|
||||
try {
|
||||
// 1. 字典序排序
|
||||
const arr = [this.token, timestamp, nonce].sort();
|
||||
|
||||
// 2. 拼接字符串
|
||||
const str = arr.join('');
|
||||
|
||||
// 3. SHA1加密
|
||||
const hash = crypto.createHash('sha1').update(str).digest('hex');
|
||||
|
||||
logger.debug('🔐 签名验证详情', {
|
||||
token: this.token.substring(0, 10) + '...',
|
||||
timestamp,
|
||||
nonce,
|
||||
sortedArray: arr.map(s => s.substring(0, 10) + (s.length > 10 ? '...' : '')),
|
||||
concatenatedString: str.substring(0, 50) + (str.length > 50 ? '...' : ''),
|
||||
calculatedHash: hash,
|
||||
receivedSignature: signature,
|
||||
isMatch: hash === signature,
|
||||
});
|
||||
|
||||
// 4. 对比签名
|
||||
return hash === signature;
|
||||
} catch (error) {
|
||||
logger.error('❌ 签名验证计算失败', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步处理消息
|
||||
*
|
||||
* 在5秒内返回success后,异步处理实际的业务逻辑
|
||||
*
|
||||
* @param body 消息体(XML格式字符串)
|
||||
*/
|
||||
private async processMessageAsync(body: string): Promise<void> {
|
||||
try {
|
||||
logger.info('📝 开始异步处理消息', {
|
||||
bodyType: typeof body,
|
||||
bodyPreview: typeof body === 'string' ? body.substring(0, 200) : JSON.stringify(body).substring(0, 200),
|
||||
});
|
||||
|
||||
// 解析XML消息体
|
||||
const message = this.parseXmlMessage(body);
|
||||
|
||||
// 判断消息类型
|
||||
if (message.MsgType === 'event') {
|
||||
// 事件消息
|
||||
logger.info('🎯 处理事件消息', {
|
||||
event: message.Event,
|
||||
fromUser: message.FromUserName,
|
||||
});
|
||||
|
||||
// TODO: 根据不同的事件类型进行处理
|
||||
switch (message.Event) {
|
||||
case 'subscribe':
|
||||
logger.info('👤 用户关注公众号', { openid: message.FromUserName });
|
||||
break;
|
||||
case 'unsubscribe':
|
||||
logger.info('👋 用户取消关注公众号', { openid: message.FromUserName });
|
||||
break;
|
||||
default:
|
||||
logger.info('📌 其他事件', { event: message.Event });
|
||||
}
|
||||
} else if (message.MsgType === 'text') {
|
||||
// 文本消息
|
||||
logger.info('💬 处理文本消息', {
|
||||
fromUser: message.FromUserName,
|
||||
content: message.Content,
|
||||
});
|
||||
|
||||
// TODO: 调用ChatService处理文本消息,回复患者
|
||||
} else {
|
||||
logger.info('📦 其他类型消息', { msgType: message.MsgType });
|
||||
}
|
||||
|
||||
logger.info('✅ 消息处理完成');
|
||||
} catch (error) {
|
||||
logger.error('❌ 消息处理失败', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析XML消息体
|
||||
*
|
||||
* 微信推送的消息格式(明文模式):
|
||||
* <xml>
|
||||
* <ToUserName><![CDATA[gh_xxx]]></ToUserName>
|
||||
* <FromUserName><![CDATA[oXXX]]></FromUserName>
|
||||
* <CreateTime>1234567890</CreateTime>
|
||||
* <MsgType><![CDATA[text]]></MsgType>
|
||||
* <Content><![CDATA[你好]]></Content>
|
||||
* </xml>
|
||||
*
|
||||
* @param xml XML字符串
|
||||
* @returns 解析后的消息对象
|
||||
*/
|
||||
private parseXmlMessage(xml: string): any {
|
||||
try {
|
||||
const message: any = {};
|
||||
|
||||
// 简单的XML解析(提取标签内容)
|
||||
const extractTag = (tagName: string): string | undefined => {
|
||||
// 匹配 <tagName><![CDATA[...]]></tagName> 或 <tagName>...</tagName>
|
||||
const cdataMatch = xml.match(new RegExp(`<${tagName}><!\\[CDATA\\[([^\\]]+)\\]\\]><\\/${tagName}>`));
|
||||
if (cdataMatch) return cdataMatch[1];
|
||||
|
||||
const textMatch = xml.match(new RegExp(`<${tagName}>([^<]+)<\\/${tagName}>`));
|
||||
if (textMatch) return textMatch[1];
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
// 提取常见字段
|
||||
message.ToUserName = extractTag('ToUserName');
|
||||
message.FromUserName = extractTag('FromUserName');
|
||||
message.CreateTime = extractTag('CreateTime');
|
||||
message.MsgType = extractTag('MsgType');
|
||||
message.MsgId = extractTag('MsgId');
|
||||
|
||||
// 根据消息类型提取特定字段
|
||||
if (message.MsgType === 'text') {
|
||||
message.Content = extractTag('Content');
|
||||
} else if (message.MsgType === 'event') {
|
||||
message.Event = extractTag('Event');
|
||||
message.EventKey = extractTag('EventKey');
|
||||
}
|
||||
|
||||
logger.debug('📋 XML解析结果', {
|
||||
message,
|
||||
});
|
||||
|
||||
return message;
|
||||
} catch (error) {
|
||||
logger.error('❌ XML解析失败', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
xml: xml.substring(0, 200),
|
||||
});
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -530,3 +530,7 @@ URL: https://iit.xunzhengyixue.com/api/v1/iit/patient-wechat/callback
|
||||
**最后更新**:2026-01-04
|
||||
**文档版本**:v1.0
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -165,3 +165,7 @@ console.log('');
|
||||
console.log('🎉 生成完成!');
|
||||
console.log('');
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { FastifyInstance } from 'fastify';
|
||||
import { WebhookController } from '../controllers/WebhookController.js';
|
||||
import { wechatCallbackController } from '../controllers/WechatCallbackController.js';
|
||||
import { patientWechatCallbackController } from '../controllers/PatientWechatCallbackController.js';
|
||||
import { PatientWechatCallbackPlainController } from '../controllers/PatientWechatCallbackPlainController.js';
|
||||
import { SyncManager } from '../services/SyncManager.js';
|
||||
import { logger } from '../../../common/logging/index.js';
|
||||
|
||||
@@ -13,6 +14,33 @@ export async function registerIitRoutes(fastify: FastifyInstance) {
|
||||
// 初始化控制器和服务
|
||||
const webhookController = new WebhookController();
|
||||
const syncManager = new SyncManager();
|
||||
const patientWechatCallbackPlainController = new PatientWechatCallbackPlainController();
|
||||
|
||||
// 添加XML内容解析器(用于微信公众号明文模式)
|
||||
// 安全注册:避免重复注册错误
|
||||
try {
|
||||
fastify.addContentTypeParser('text/xml', { parseAs: 'string' }, function (req, body, done) {
|
||||
done(null, body);
|
||||
});
|
||||
} catch (error: any) {
|
||||
// 解析器可能已存在,忽略错误
|
||||
if (error.code !== 'FST_ERR_CTP_ALREADY_PRESENT') {
|
||||
throw error;
|
||||
}
|
||||
logger.debug('text/xml parser already exists, skipping');
|
||||
}
|
||||
|
||||
try {
|
||||
fastify.addContentTypeParser('application/xml', { parseAs: 'string' }, function (req, body, done) {
|
||||
done(null, body);
|
||||
});
|
||||
} catch (error: any) {
|
||||
// 解析器可能已存在,忽略错误
|
||||
if (error.code !== 'FST_ERR_CTP_ALREADY_PRESENT') {
|
||||
throw error;
|
||||
}
|
||||
logger.debug('application/xml parser already exists, skipping');
|
||||
}
|
||||
|
||||
// =============================================
|
||||
// 健康检查
|
||||
@@ -240,18 +268,8 @@ export async function registerIitRoutes(fastify: FastifyInstance) {
|
||||
// 企业微信回调路由
|
||||
// =============================================
|
||||
|
||||
// 注册text/xml解析器(企业微信回调使用此格式)
|
||||
fastify.addContentTypeParser(
|
||||
'text/xml',
|
||||
{ parseAs: 'string' },
|
||||
(req, body, done) => {
|
||||
// 企业微信发送的是XML字符串,直接返回字符串即可
|
||||
// 在控制器中使用xml2js进行解析
|
||||
done(null, body);
|
||||
}
|
||||
);
|
||||
|
||||
logger.info('Registered content parser: text/xml');
|
||||
// 注意:text/xml解析器已在文件开头注册(通用于企业微信和微信服务号)
|
||||
logger.info('Using shared text/xml parser for WeChat Work callbacks');
|
||||
|
||||
// GET: URL验证(企业微信配置回调URL时使用)
|
||||
fastify.get(
|
||||
@@ -300,8 +318,56 @@ export async function registerIitRoutes(fastify: FastifyInstance) {
|
||||
// 微信服务号回调路由(患者端)
|
||||
// =============================================
|
||||
|
||||
// 简化路由(用于微信公众平台配置,路径更短)
|
||||
// GET: URL验证
|
||||
// ===== 明文模式(Plain Text Mode) =====
|
||||
// 推荐:先用明文模式测试基础功能,成功后再切换到安全模式
|
||||
|
||||
// GET: URL验证(明文模式)
|
||||
fastify.get(
|
||||
'/wechat/patient/callback-plain',
|
||||
{
|
||||
schema: {
|
||||
querystring: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
signature: { type: 'string' },
|
||||
timestamp: { type: 'string' },
|
||||
nonce: { type: 'string' },
|
||||
echostr: { type: 'string' }
|
||||
},
|
||||
additionalProperties: true
|
||||
}
|
||||
}
|
||||
},
|
||||
patientWechatCallbackPlainController.handleVerification.bind(patientWechatCallbackPlainController)
|
||||
);
|
||||
|
||||
logger.info('Registered route: GET /wechat/patient/callback-plain (明文模式)');
|
||||
|
||||
// POST: 接收消息(明文模式,XML格式)
|
||||
// 注意:微信推送的是XML格式的消息体
|
||||
fastify.post(
|
||||
'/wechat/patient/callback-plain',
|
||||
{
|
||||
schema: {
|
||||
querystring: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
signature: { type: 'string' },
|
||||
timestamp: { type: 'string' },
|
||||
nonce: { type: 'string' }
|
||||
},
|
||||
additionalProperties: true
|
||||
}
|
||||
}
|
||||
},
|
||||
patientWechatCallbackPlainController.handleCallback.bind(patientWechatCallbackPlainController)
|
||||
);
|
||||
|
||||
logger.info('Registered route: POST /wechat/patient/callback-plain (明文模式, XML)');
|
||||
|
||||
// ===== 安全模式(Secure Mode / AES加密) =====
|
||||
|
||||
// GET: URL验证(安全模式)
|
||||
// 注意:不使用required字段,避免Fastify过早拦截微信的请求
|
||||
fastify.get(
|
||||
'/wechat/patient/callback',
|
||||
@@ -323,9 +389,9 @@ export async function registerIitRoutes(fastify: FastifyInstance) {
|
||||
patientWechatCallbackController.handleVerification.bind(patientWechatCallbackController)
|
||||
);
|
||||
|
||||
logger.info('Registered route: GET /wechat/patient/callback');
|
||||
logger.info('Registered route: GET /wechat/patient/callback (安全模式)');
|
||||
|
||||
// POST: 接收消息
|
||||
// POST: 接收消息(安全模式)
|
||||
// 注意:不使用required字段,避免Fastify过早拦截微信的请求
|
||||
fastify.post(
|
||||
'/wechat/patient/callback',
|
||||
@@ -348,7 +414,7 @@ export async function registerIitRoutes(fastify: FastifyInstance) {
|
||||
patientWechatCallbackController.handleCallback.bind(patientWechatCallbackController)
|
||||
);
|
||||
|
||||
logger.info('Registered route: POST /wechat/patient/callback');
|
||||
logger.info('Registered route: POST /wechat/patient/callback (安全模式)');
|
||||
|
||||
// 完整路由(兼容旧配置,保留)
|
||||
// GET: URL验证(微信服务号配置回调URL时使用)
|
||||
|
||||
@@ -482,3 +482,7 @@ export class PatientWechatService {
|
||||
// 导出单例
|
||||
export const patientWechatService = new PatientWechatService();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -127,3 +127,7 @@ testDifyIntegration().catch(error => {
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -156,3 +156,7 @@ testIitDatabase()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -142,3 +142,7 @@ if (hasError) {
|
||||
console.log('\n');
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -168,3 +168,7 @@ async function testUrlVerification() {
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -249,3 +249,7 @@ main().catch((error) => {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -133,3 +133,7 @@ try {
|
||||
|
||||
Write-Host ""
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
119
backend/src/modules/iit-manager/test-wechat-mp-plain.ps1
Normal file
119
backend/src/modules/iit-manager/test-wechat-mp-plain.ps1
Normal file
@@ -0,0 +1,119 @@
|
||||
# Test WeChat Official Account - Plain Text Mode URL Verification
|
||||
|
||||
# Configuration
|
||||
$Token = "IitPatientWechat2026JanToken"
|
||||
$Timestamp = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds().ToString()
|
||||
$Nonce = Get-Random -Minimum 100000000 -Maximum 999999999
|
||||
$Echostr = "test_echo_string_$(Get-Random)"
|
||||
|
||||
Write-Host "================================" -ForegroundColor Cyan
|
||||
Write-Host "WeChat MP Plain Mode URL Verification Test" -ForegroundColor Cyan
|
||||
Write-Host "================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
Write-Host "Configuration:" -ForegroundColor Yellow
|
||||
Write-Host " Token: $Token"
|
||||
Write-Host " Timestamp: $Timestamp"
|
||||
Write-Host " Nonce: $Nonce"
|
||||
Write-Host " Echostr: $Echostr"
|
||||
Write-Host ""
|
||||
|
||||
# Calculate signature
|
||||
Write-Host "Calculating signature..." -ForegroundColor Yellow
|
||||
|
||||
# 1. Sort by dictionary order
|
||||
$sortedArray = @($Token, $Timestamp, $Nonce) | Sort-Object
|
||||
Write-Host " Sorted array: [$($sortedArray -join ', ')]"
|
||||
|
||||
# 2. Concatenate string
|
||||
$concatenatedString = $sortedArray -join ''
|
||||
Write-Host " Concatenated: $concatenatedString"
|
||||
|
||||
# 3. SHA1 hash
|
||||
$sha1 = [System.Security.Cryptography.SHA1]::Create()
|
||||
$bytes = [System.Text.Encoding]::UTF8.GetBytes($concatenatedString)
|
||||
$hashBytes = $sha1.ComputeHash($bytes)
|
||||
$signature = [System.BitConverter]::ToString($hashBytes).Replace("-", "").ToLower()
|
||||
Write-Host " SHA1 signature: $signature" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
|
||||
# Test 1: Local server (via natapp)
|
||||
Write-Host "Test 1: Local server (via natapp)" -ForegroundColor Cyan
|
||||
Write-Host "----------------------------------------"
|
||||
$localUrl = "https://devlocal.xunzhengyixue.com/wechat/patient/callback-plain"
|
||||
$localFullUrl = "{0}?signature={1}`×tamp={2}`&nonce={3}`&echostr={4}" -f $localUrl, $signature, $Timestamp, $Nonce, $Echostr
|
||||
|
||||
Write-Host " Request URL: $localFullUrl" -ForegroundColor Gray
|
||||
|
||||
try {
|
||||
$response = Invoke-WebRequest -Uri $localFullUrl -Method Get -UseBasicParsing
|
||||
|
||||
if ($response.StatusCode -eq 200) {
|
||||
Write-Host " SUCCESS (200 OK)" -ForegroundColor Green
|
||||
Write-Host " Response content: $($response.Content)" -ForegroundColor Green
|
||||
|
||||
if ($response.Content -eq $Echostr) {
|
||||
Write-Host " echostr MATCHED!" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host " echostr MISMATCH!" -ForegroundColor Red
|
||||
Write-Host " Expected: $Echostr" -ForegroundColor Red
|
||||
Write-Host " Actual: $($response.Content)" -ForegroundColor Red
|
||||
}
|
||||
} else {
|
||||
Write-Host " FAILED ($($response.StatusCode))" -ForegroundColor Red
|
||||
}
|
||||
} catch {
|
||||
Write-Host " ERROR: $($_.Exception.Message)" -ForegroundColor Red
|
||||
if ($_.Exception.Response) {
|
||||
$statusCode = [int]$_.Exception.Response.StatusCode
|
||||
Write-Host " HTTP Status: $statusCode" -ForegroundColor Red
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
|
||||
# Test 2: Direct localhost (localhost:3001)
|
||||
Write-Host "Test 2: Direct localhost (localhost:3001)" -ForegroundColor Cyan
|
||||
Write-Host "----------------------------------------"
|
||||
$localhostUrl = "http://localhost:3001/wechat/patient/callback-plain"
|
||||
$localhostFullUrl = "{0}?signature={1}`×tamp={2}`&nonce={3}`&echostr={4}" -f $localhostUrl, $signature, $Timestamp, $Nonce, $Echostr
|
||||
|
||||
Write-Host " Request URL: $localhostFullUrl" -ForegroundColor Gray
|
||||
|
||||
try {
|
||||
$response = Invoke-WebRequest -Uri $localhostFullUrl -Method Get -UseBasicParsing
|
||||
|
||||
if ($response.StatusCode -eq 200) {
|
||||
Write-Host " SUCCESS (200 OK)" -ForegroundColor Green
|
||||
Write-Host " Response content: $($response.Content)" -ForegroundColor Green
|
||||
|
||||
if ($response.Content -eq $Echostr) {
|
||||
Write-Host " echostr MATCHED!" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host " echostr MISMATCH!" -ForegroundColor Red
|
||||
Write-Host " Expected: $Echostr" -ForegroundColor Red
|
||||
Write-Host " Actual: $($response.Content)" -ForegroundColor Red
|
||||
}
|
||||
} else {
|
||||
Write-Host " FAILED ($($response.StatusCode))" -ForegroundColor Red
|
||||
}
|
||||
} catch {
|
||||
Write-Host " ERROR: $($_.Exception.Message)" -ForegroundColor Red
|
||||
if ($_.Exception.Response) {
|
||||
$statusCode = [int]$_.Exception.Response.StatusCode
|
||||
Write-Host " HTTP Status: $statusCode" -ForegroundColor Red
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "================================" -ForegroundColor Cyan
|
||||
Write-Host "Next Step: WeChat MP Configuration" -ForegroundColor Green
|
||||
Write-Host "================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
Write-Host "If local tests passed, configure in WeChat MP:" -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
Write-Host " URL: https://devlocal.xunzhengyixue.com/wechat/patient/callback-plain" -ForegroundColor Cyan
|
||||
Write-Host " Token: $Token" -ForegroundColor Cyan
|
||||
Write-Host " Encryption Mode: Plain Text Mode" -ForegroundColor Cyan
|
||||
Write-Host " Data Format: XML (IMPORTANT!)" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
@@ -226,3 +226,7 @@ export interface CachedProtocolRules {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user