使用 NestJS 开发微信公众号消息接口实践

2026-01-31 2252字

最近重新启用了之前被冻结的微信公众号,打算尝试一下公众号接口的开发。虽然公众号自带的自动回复功能已经能满足基本需求,但当需要实现更复杂的动态回复功能时,就需要通过自定义开发来接管这些功能。

本文将以实现”接收用户消息,返回动态内容”的功能为例,记录使用 NestJS 开发微信公众号接口的全过程。

开发前准备

在开始开发之前,我们需要做好以下准备工作:

公众号准备

开发微信公众号功能,首先需要一个公众号。微信官方提供了测试号功能,可以直接在开发阶段使用,申请地址:微信公众平台测试号

当然,也可以使用自己注册的公众号,无论是订阅号还是服务号都可以。虽然订阅号在某些高级功能上有所限制,但本文涉及的开发示例不受影响。

内网穿透工具

由于微信公众号开发要求提供公网可访问的安全域名,而开发阶段的接口通常运行在本地环境中,因此需要使用内网穿透工具将本地服务暴露到公网。

有多种内网穿透方案可供选择:

本文推荐使用 natapp,因为它配置简单,适合快速开始开发。

公众号接口配置

无论是测试号还是正式公众号,都需要配置安全域名和消息接口。

重要提醒:必须先在本地开发好接口,才能进行配置,因为微信在配置时会立即验证接口的有效性。

测试号配置

测试号配置

图中位置 1 配置代理域名,位置 2 配置接口地址。

正式公众号配置

本文案例主要基于我个人公众号实现,所以后面只讲正式公众号的配置实现。

对于正式公众号,配置入口已迁移到微信开发者平台,在”域名与消息推送配置”中设置:

公众号接口配置

JS 接口安全域名验证

配置 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 同级
    }),
  ]
}

消息推送配置

实现认证回调接口

我定义的消息接口为 /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
    }
  }
}

微信服务器的验证流程如下(详细说明见官方文档):

  1. 参数排序:将 token、timestamp、nonce 三个参数按字典序排序
  2. 字符串拼接:将排序后的参数拼接成一个字符串
  3. SHA1 加密:对拼接后的字符串进行 SHA1 加密
  4. 签名比对:将加密结果与 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();
}

重要提醒:回复消息时,ToUserNameFromUserName 需要与接收的消息调换:

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

实现效果:

公众号自动回复效果

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

#NestJS #微信公众号 #后端开发