Compare commits

...

2 Commits

Author SHA1 Message Date
545d1e687f feat: 添加事件推送处理
- 当前不处理任何事件推送
2025-12-26 04:55:51 +00:00
ac4cea4658 chore: 添加微信事件推送类型 2025-12-26 04:41:16 +00:00
4 changed files with 298 additions and 98 deletions

View File

@ -51,6 +51,14 @@ export default defineEndpoint({
let replyContent; let replyContent;
let reply; let reply;
if(MsgType === 'event') {
// 不处理事件推送,回复空内容以避免重试
console.warn("Received event push, no reply sent.");
res.set('Content-Type', 'plain/text');
res.send('');
return;
}
if (MsgType !== 'text') { if (MsgType !== 'text') {
replyContent = "暂不支持该消息类型"; replyContent = "暂不支持该消息类型";
} else { } else {

114
src/types/received-event.ts Normal file
View File

@ -0,0 +1,114 @@
/**
* 微信公众号/服务号「接收事件推送」类型定义
*
* @remarks
* - 对应微信官方XML信息结构
* - 所有事件类型继承自 {@link WechatReceivedBaseEvent}
* - 通过 {@link Event} 字段区分不同事件类型
* @see 微信官方文档: https://developers.weixin.qq.com/doc/service/guide/product/message/Receiving_event_pushes.html
*
* @packageDocumentation
*/
/**
* 「接收事件推送」事件类型字面量联合类型
*
* @remarks
* 可用于类型收窄
*/
export type WechatReceivedEventType = | "subscribe"
| "unsubscribe"
| "SCAN"
| "LOCATION"
| "CLICK"
| "VIEW"
/**
* 「接收事件推送」基础类型定义
*
* @remarks
* 所有事件类型均包含这些公共字段
*/
export interface WechatReceivedBaseEvent {
/** 开发者微信号 */
ToUserName: string;
/** 发送方帐号一个OpenID */
FromUserName: string;
/** 消息创建时间 (整型) */
CreateTime: number;
/** 消息类型event */
MsgType: "event";
/** 事件类型
*
* @remarks
* 在具体事件类型中收窄为字面量类型(如"subscribe","CLICK"等)
*/
Event: string;
}
/**
* 「接收事件推送」用户未关注时,进行关注后的事件推送
*/
export interface WechatReceivedSubscribeEvent extends WechatReceivedBaseEvent {
/** 事件类型: subscribe */
Event: "subscribe";
/** 事件KEY值qrscene_为前缀后面为二维码的参数值 */
EventKey?: string;
/** 二维码的ticket可用来换取二维码图片 */
Ticket?: string;
}
/**
* 「接收事件推送」用户已关注时的事件推送
*/
export interface WechatReceivedScanEvent extends WechatReceivedBaseEvent {
/** 事件类型: SCAN */
Event: "SCAN";
/** 事件KEY值是一个32位无符号整数即创建二维码时的二维码scene_id */
EventKey: string;
/** 二维码的ticket可用来换取二维码图片 */
Ticket: string;
}
/**
* 「接收事件推送」上报地理位置事件
*/
export interface WechatReceivedLocationEvent extends WechatReceivedBaseEvent {
/** 事件类型: LOCATION */
Event: "LOCATION";
/** 地理位置纬度 */
Latitude: string;
/** 地理位置经度 */
Longitude: string;
/** 地理位置精度 */
Precision: string;
}
/**
* 「接收事件推送」自定义菜单点击事件
*/
export interface WechatReceivedClickEvent extends WechatReceivedBaseEvent {
/** 事件类型: CLICK */
Event: "CLICK";
/** 事件KEY值与自定义菜单接口中KEY值对应 */
EventKey: string;
}
/**
* 「接收事件推送」自定义菜单跳转链接事件
*/
export interface WechatReceivedViewEvent extends WechatReceivedBaseEvent {
/** 事件类型: VIEW */
Event: "VIEW";
/** 事件KEY值设置的跳转URL */
EventKey: string;
}
/**
* 「接收事件推送」类型联合类型
*/
export type WechatReceivedEvent = WechatReceivedSubscribeEvent
| WechatReceivedScanEvent
| WechatReceivedLocationEvent
| WechatReceivedClickEvent
| WechatReceivedViewEvent;

View File

@ -31,3 +31,24 @@ export function assertWechatReceivedMessageType(value: unknown, fieldName: strin
throw new Error(`Expected ${fieldName} to be one of ${WECHAT_RECEIVED_MESSAGE_TYPES.join(", ")}, but got ${typeof value === 'string' ? value : typeof value}`); throw new Error(`Expected ${fieldName} to be one of ${WECHAT_RECEIVED_MESSAGE_TYPES.join(", ")}, but got ${typeof value === 'string' ? value : typeof value}`);
} }
} }
const WECHAT_RECEIVED_EVENT_TYPES = [
"subscribe",
"SCAN",
"LOCATION",
"CLICK",
"VIEW"
] as const;
export type WechatReceivedEventType = typeof WECHAT_RECEIVED_EVENT_TYPES[number];
/** 断言值为微信接收推送类型字面量
*
* @param value 原始值
* @param fieldName 字段名称
*/
export function assertWechatReceivedEventType(value: unknown, fieldName: string): asserts value is WechatReceivedEventType {
if (typeof value !== 'string' || !WECHAT_RECEIVED_EVENT_TYPES.includes(value as WechatReceivedEventType)) {
throw new Error(`Expected ${fieldName} to be one of ${WECHAT_RECEIVED_EVENT_TYPES.join(", ")}, but got ${typeof value === 'string' ? value : typeof value}`);
}
}

View File

@ -4,7 +4,9 @@ import type { Request } from "express";
import type { WechatReceivedBaseMessage, WechatReceivedImageMessage, WechatReceivedLinkMessage, WechatReceivedLocationMessage, WechatReceivedMessage, WechatReceivedTextMessage, WechatReceivedVideoMessage, WechatReceivedVoiceMessage } from "../types/received-message"; import type { WechatReceivedBaseMessage, WechatReceivedImageMessage, WechatReceivedLinkMessage, WechatReceivedLocationMessage, WechatReceivedMessage, WechatReceivedTextMessage, WechatReceivedVideoMessage, WechatReceivedVoiceMessage } from "../types/received-message";
import type { WechatEncryptMessage } from "../types/encrypted-message"; import type { WechatEncryptMessage } from "../types/encrypted-message";
import { WechatXmlObject, NormalizedWechatXml } from "../types/wechat-xml"; import { WechatXmlObject, NormalizedWechatXml } from "../types/wechat-xml";
import { assertString, assertWechatReceivedMessageType } from "./assertUtils"; import { assertString, assertWechatReceivedEventType, assertWechatReceivedMessageType } from "./assertUtils";
import { WechatReceivedEvent } from "../types/received-event";
import { assert } from "console";
/** /**
* 从原始XML字符串解析为微信XML对象 * 从原始XML字符串解析为微信XML对象
@ -197,30 +199,84 @@ export async function parseWechatEncryptMessage(req: Request): Promise<WechatEnc
* // } * // }
* ``` * ```
*/ */
export async function parseDecryptedXML(raw: string): Promise<WechatReceivedMessage> { export async function parseDecryptedXML(raw: string): Promise<WechatReceivedMessage | WechatReceivedEvent> {
// const parser = new XMLParser({
// ignoreAttributes: false,
// parseTagValue: false,
// cdataPropName: '__cdata',
// trimValues: true,
// });
// const result = parser.parse(raw);
// const xml = result.xml as WechatXmlObject;
const xml = parseRawXml(raw); const xml = parseRawXml(raw);
// 将可能存在的 __cdata 提取 // 将可能存在的 __cdata 提取
const normalized = normalizeWechatXml(xml); const normalized = normalizeWechatXml(xml);
const msgType = normalized.MsgType; const msgType = normalized.MsgType;
assertString(msgType, 'MsgType'); let eventType: string | undefined;
assertWechatReceivedMessageType(msgType, 'MsgType');
// 基础字段类型断言 // 基础字段类型断言
assertString(normalized.ToUserName, 'ToUserName'); assertString(normalized.ToUserName, 'ToUserName');
assertString(normalized.FromUserName, 'FromUserName'); assertString(normalized.FromUserName, 'FromUserName');
assertString(normalized.CreateTime, 'CreateTime'); assertString(normalized.CreateTime, 'CreateTime');
assertString(msgType, 'MsgType');
if (msgType === 'event') {
assertString(normalized.Event, 'Event');
eventType = normalized.Event;
assertWechatReceivedEventType(eventType, 'Event');
const baseFields: Omit<WechatReceivedEvent, 'MsgType' | 'Event'> = {
ToUserName: normalized.ToUserName,
FromUserName: normalized.FromUserName,
CreateTime: Number(normalized.CreateTime),
};
// 根据 Event 进行具体事件类型的构建
switch (eventType) {
case "subscribe":
return {
...baseFields,
MsgType: "event",
Event: "subscribe",
EventKey: normalized.EventKey,
Ticket: normalized.Ticket,
} satisfies WechatReceivedEvent;
case "SCAN":
return {
...baseFields,
MsgType: "event",
Event: "SCAN",
EventKey: normalized.EventKey ?? '',
Ticket: normalized.Ticket ?? '',
} satisfies WechatReceivedEvent;
case "LOCATION":
assertString(normalized.Latitude, 'Latitude');
assertString(normalized.Longitude, 'Longitude');
assertString(normalized.Precision, 'Precision');
return {
...baseFields,
MsgType: "event",
Event: "LOCATION",
Latitude: normalized.Latitude,
Longitude: normalized.Longitude,
Precision: normalized.Precision,
} satisfies WechatReceivedEvent;
case "CLICK":
assertString(normalized.EventKey, 'EventKey');
return {
...baseFields,
MsgType: "event",
Event: "CLICK",
EventKey: normalized.EventKey,
} satisfies WechatReceivedEvent;
case "VIEW":
assertString(normalized.EventKey, 'EventKey');
return {
...baseFields,
MsgType: "event",
Event: "VIEW",
EventKey: normalized.EventKey,
} satisfies WechatReceivedEvent;
default:
const _exhaustiveCheck: never = eventType;
throw new Error(`Unsupported Event type: ${_exhaustiveCheck}`);
}
} else {
assertWechatReceivedMessageType(msgType, 'MsgType');
assertString(normalized.MsgId, 'MsgId'); assertString(normalized.MsgId, 'MsgId');
const baseFields: Omit<WechatReceivedBaseMessage, 'MsgType'> = { const baseFields: Omit<WechatReceivedBaseMessage, 'MsgType'> = {
@ -307,4 +363,5 @@ export async function parseDecryptedXML(raw: string): Promise<WechatReceivedMess
const _exhaustiveCheck: never = msgType; const _exhaustiveCheck: never = msgType;
throw new Error(`Unsupported MsgType: ${_exhaustiveCheck}`); throw new Error(`Unsupported MsgType: ${_exhaustiveCheck}`);
} }
}
} }