使用 NestJS 开发微信公众号消息接口实践
2026-01-31 2252字
最近重新启用了之前被冻结的微信公众号,打算尝试一下公众号接口的开发。虽然公众号自带的自动回复功能已经能满足基本需求,但当需要实现更复杂的动态回复功能时,就需要通过自定义开发来接管这些功能。
本文将以实现”接收用户消息,返回动态内容”的功能为例,记录使用 NestJS 开发微信公众号接口的全过程。
开发前准备
在开始开发之前,我们需要做好以下准备工作:
公众号准备
开发微信公众号功能,首先需要一个公众号。微信官方提供了测试号功能,可以直接在开发阶段使用,申请地址:微信公众平台测试号。
当然,也可以使用自己注册的公众号,无论是订阅号还是服务号都可以。虽然订阅号在某些高级功能上有所限制,但本文涉及的开发示例不受影响。
内网穿透工具
由于微信公众号开发要求提供公网可访问的安全域名,而开发阶段的接口通常运行在本地环境中,因此需要使用内网穿透工具将本地服务暴露到公网。
有多种内网穿透方案可供选择:
- frp:开源免费,但需要自己部署服务器
- natapp:现成的穿透服务,免费版本即可满足开发需求
本文推荐使用 natapp,因为它配置简单,适合快速开始开发。
公众号接口配置
无论是测试号还是正式公众号,都需要配置安全域名和消息接口。
- 安全域名配置:安全域名是指通过内网穿透工具暴露到公网的域名,例如使用 natapp 后的地址
xxx.natappfree.cc。 - 消息接口配置:消息接口是我们自定义的回调接口,用于接收微信服务器转发的用户消息。接口地址可以自定义,例如
http://xxx.natappfree.cc/api/wx/callback。
重要提醒:必须先在本地开发好接口,才能进行配置,因为微信在配置时会立即验证接口的有效性。
测试号配置

图中位置 1 配置代理域名,位置 2 配置接口地址。
正式公众号配置
本文案例主要基于我个人公众号实现,所以后面只讲正式公众号的配置实现。
对于正式公众号,配置入口已迁移到微信开发者平台,在”域名与消息推送配置”中设置:

JS 接口安全域名验证
配置 JS 接口安全域名时,需要下载验证文件并部署到服务器:

例如,如果代理域名是 http://xxx.natappfree.cc,验证文件需要能在 http://xxx.natappfree.cc/文件名.txt 地址访问到。
在 NestJS 项目中,可以使用 @nestjs/serve-static 插件设置静态资源目录:
// app.module.ts
import { ServeStaticModule } from '@nestjs/serve-static';
@Module({
imports: [
ServeStaticModule.forRoot({
rootPath: join(__dirname, '..', 'public'), // public 目录与 src 同级
}),
]
}
消息推送配置
- Token:自定义的令牌,接口验证时需要用到
- EncodingAESKey:用于消息加密解密(本文使用明文模式,无需配置)
- URL:消息回调接口地址(下面会介绍开发实现)
实现认证回调接口
我定义的消息接口为 /api/wx/callback(其中 /api 是项目公共前缀),在 NestJS 项目中创建 wx 模块来实现这个接口。
微信服务器在配置时会发送 GET 请求进行验证,我们需要实现相应的校验逻辑:
// wx.controller.ts
import { Controller, Get, Query } from '@nestjs/common';
import { WxService } from './wx.service';
import crypto from 'crypto';
function sha1(str: string) {
return crypto.createHash('sha1').update(str).digest('hex');
}
@Controller('wx')
export class WxController {
constructor(private readonly wxService: WxService) {}
@Get('/callback')
callback(
@Query('signature') signature: string,
@Query('timestamp') timestamp: string,
@Query('nonce') nonce: string,
@Query('echostr') echostr: string,
) {
// 参数完整性检查
if (!signature || !timestamp || !nonce || !echostr) {
return 'error';
}
// 微信验证逻辑
const str = [process.env.WX_TOKEN, timestamp, nonce].sort().join('');
const sha1Str = sha1(str);
if (sha1Str !== signature) {
return 'token error';
} else {
return echostr; // 验证通过,返回 echostr
}
}
}
微信服务器的验证流程如下(详细说明见官方文档):
- 参数排序:将 token、timestamp、nonce 三个参数按字典序排序
- 字符串拼接:将排序后的参数拼接成一个字符串
- SHA1 加密:对拼接后的字符串进行 SHA1 加密
- 签名比对:将加密结果与 signature 参数比对,确认请求来源
验证通过后,只需原样返回 echostr 参数即可完成配置。
注意:WX_TOKEN 环境变量的值需要与公众号配置中的 Token 保持一致。
实现消息回复接口
认证接口完成后,接下来需要实现真正的消息回复功能。我们的目标是接管公众号的自动回复功能,实现动态内容回复。
微信服务器在用户发送消息时,会通过 POST 请求将消息数据发送到同一个接口 /api/wx/callback。因此我们需要在同一个路径上定义 POST 接口:
// wx.controller.ts
import { Controller, Post, Body } from '@nestjs/common';
import { WxService } from './wx.service';
@Controller('wx')
export class WxController {
constructor(private readonly wxService: WxService) {}
// 处理 POST 请求
@Post('/callback')
async postCallback(@Body() body: any) {
// 处理 POST 请求
}
}
此时我们还没办法直接处理 POST 请求中的 body 数据,因为微信服务器发送的数据是 XML 格式,而 NestJS 默认无法直接解析 XML,我们需要手动处理下 XML 数据的获取。
XML 数据获取
可以借助 body-parser 插件来获取 POST 请求中的 XML 数据,封装成一个中间件使用:
// xml-body.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import * as bodyParser from 'body-parser';
@Injectable()
export class XmlBodyMiddleware implements NestMiddleware {
use(req: any, res: any, next: () => void) {
bodyParser.text({
type: ['text/xml', 'application/xml'],
})(req, res, next);
}
}
然后在 wx.module.ts 中配置中间件,使其仅对 POST 请求生效:
import {
MiddlewareConsumer,
Module,
NestModule,
RequestMethod,
} from '@nestjs/common';
import { WxService } from './wx.service';
import { WxController } from './wx.controller';
import { XmlBodyMiddleware } from '@/common/middlewares/xml-body.middleware';
@Module({
controllers: [WxController],
providers: [WxService],
})
export class WxModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(XmlBodyMiddleware).forRoutes({
path: 'wx/callback',
method: RequestMethod.POST,
});
}
}
配置完成后,就可以在 POST 接口中获取 XML 数据:
// wx.controller.ts
@Post('/callback')
async postCallback(@Body() body: any) {
// body 中包含 XML 格式的消息数据
}
解析 XML 消息
微信服务器支持发送多种类型的消息(详见官方文档),本文以文本消息为例。
文本消息的 XML 格式如下:
<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[fromUser]]></FromUserName>
<CreateTime>1348831860</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[this is a test]]></Content>
<MsgId>1234567890123456</MsgId>
<MsgDataId>xxxx</MsgDataId>
<Idx>xxxx</Idx>
</xml>
手动解析这种 XML 比较繁琐,我们可以使用 xml2js 库来简化这个过程,封装方法 parseWechatXml:
// wx.service.ts
import { Injectable } from '@nestjs/common';
import { parseStringPromise } from 'xml2js';
// 定义文本消息接口
interface WechatTextMessage {
ToUserName: string;
FromUserName: string;
CreateTime: string;
MsgType: 'text';
Content: string;
MsgId: string;
}
@Injectable()
export class WxService {
async parseWechatXml(xml: string): Promise<WechatTextMessage> {
const { xml: data } = await parseStringPromise(xml, {
explicitArray: false, // 不将子节点转为数组
trim: true, // 去除前后空格
});
return data as WechatTextMessage;
}
}
然后在控制器中使用解析方法:
// wx.controller.ts
@Post('/callback')
async postCallback(@Body() body: any) {
const message = await this.wxService.parseWechatXml(body.toString('utf8'));
// 解析后的消息对象结构:
// {
// ToUserName: '公众号原始ID',
// FromUserName: '用户OpenID',
// CreateTime: '时间戳',
// MsgType: 'text',
// Content: '用户发送的消息内容',
// MsgId: '消息ID'
// }
// 根据业务逻辑处理消息
}
生成回复消息
回复给用户的消息也需要是 XML 格式,我们可以封装一个回复消息生成方法:
// wx.service.ts
replyText({
toUser,
fromUser,
content,
}: {
toUser: string;
fromUser: string;
content: string;
}) {
return `
<xml>
<ToUserName><![CDATA[${toUser}]]></ToUserName>
<FromUserName><![CDATA[${fromUser}]]></FromUserName>
<CreateTime>${Math.floor(Date.now() / 1000)}</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[${content}]]></Content>
</xml>
`.trim();
}
重要提醒:回复消息时,ToUserName 和 FromUserName 需要与接收的消息调换:
this.wxService.replyText({
toUser: message.FromUserName, // 发送给用户
fromUser: message.ToUserName, // 来自公众号
content: '回复内容',
});
这样设计是因为消息是双向的:用户发给公众号,公众号再回复给用户。
完整示例:动态验证码回复
下面是一个完整的示例,实现当用户发送”验证码”时,动态生成验证码并回复给用户:
// wx.controller.ts
import type { Response } from 'express';
// 在 WxService 中添加验证码生成方法
// wx.service.ts
generateCode() {
return Math.floor(100000 + Math.random() * 900000).toString();
}
// 完整的 POST 接口实现
@Post('/callback')
async postCallback(@Body() body: any, @Res() res: Response) {
const message = await this.wxService.parseWechatXml(body.toString('utf8'));
let content = '';
// 业务逻辑:根据用户消息内容动态回复
if (message.Content === '验证码' && message.MsgType === 'text') {
const code = this.wxService.generateCode();
content = `你的登录验证码是:${code},5 分钟内有效`;
} else {
content = '你好,这是测试回复消息。';
}
// 生成回复消息
const replyXml = this.wxService.replyText({
toUser: message.FromUserName,
fromUser: message.ToUserName,
content,
});
// 设置响应头并返回 XML
res.set('Content-Type', 'text/xml; charset=utf-8');
return res.send(replyXml);
}
实现效果:

通过这个示例,我们可以看到如何基于用户消息内容实现动态回复功能,这正是自定义开发相比公众号自带自动回复功能的优势所在。