文章目录
-
- 一、写在前面
- 二、技术架构
-
- [2.1 技术栈](#2.1 技术栈)
- [2.2 系统整体架构](#2.2 系统整体架构)
- [2.3 系统交互架构](#2.3 系统交互架构)
- [2.3 项目模块结构](#2.3 项目模块结构)
- 三、开发步骤详解
-
- [3.1 配置公众号服务器](#3.1 配置公众号服务器)
- [3.2 配置 application.yml](#3.2 配置 application.yml)
- [3.3 服务器地址验证(GET 请求)](#3.3 服务器地址验证(GET 请求))
- [3.4 接收消息推送(POST 请求)](#3.4 接收消息推送(POST 请求))
- [3.5 解析 XML 消息](#3.5 解析 XML 消息)
- [3.6 消息类型分发处理](#3.6 消息类型分发处理)
- [3.7 分阶段自动回复(核心功能)](#3.7 分阶段自动回复(核心功能))
- [3.8 生成小程序 URL Link](#3.8 生成小程序 URL Link)
- [3.9 数据表设计](#3.9 数据表设计)
- 四、实战中遇到的问题与解决方案
-
- [4.1 MySQL 低版本不支持 `ADD COLUMN IF NOT EXISTS`](#4.1 MySQL 低版本不支持
ADD COLUMN IF NOT EXISTS) - [4.2 SQL 客户端不支持存储过程 DELIMITER 语法](#4.2 SQL 客户端不支持存储过程 DELIMITER 语法)
- [4.3 小程序 URL Link 接口返回 412 错误](#4.3 小程序 URL Link 接口返回 412 错误)
- [4.4 Spring Boot 3.2 + MyBatis-Plus 启动异常](#4.4 Spring Boot 3.2 + MyBatis-Plus 启动异常)
- [4.1 MySQL 低版本不支持 `ADD COLUMN IF NOT EXISTS`](#4.1 MySQL 低版本不支持
- 五、部署清单
-
- [5.1 环境准备](#5.1 环境准备)
- [5.2 配置检查](#5.2 配置检查)
- [5.3 数据库初始化](#5.3 数据库初始化)
- [5.4 公众号配置](#5.4 公众号配置)
- 六、后续可扩展功能
- 七、总结
版本说明:本文基于 Spring Boot 3.2.0、MyBatis-Plus 3.5.5、MySQL 5.7+、Java 17 编写,代码示例可直接运行于对应版本环境。
一、写在前面
本文解决什么问题
本文面向已有 Spring Boot 后端开发基础、需要快速接入微信公众号服务的开发者,完整记录从公众号服务器配置、签名验证、消息接收,到分阶段自动回复、小程序 URL Link 跳转的完整开发流程。
阅读本文后,你将能够:
- 独立完成公众号服务器配置与签名验证
- 实现消息接收、解析与分类处理
- 基于用户交互次数(
interaction_count)实现分阶段自动回复 - 生成小程序 URL Link 并在回复中嵌入跳转链接
- 规避开发过程中常见的坑点(MySQL 兼容、412 错误等)
前置条件
- Java 17 及以上版本
- Spring Boot 3.2.x(与 MyBatis-Plus 3.5.5 兼容)
- MySQL 5.7+(或 8.0)
- 已注册微信公众号(订阅号/服务号均可)
- 如需 URL Link 功能,需准备已认证的小程序
二、技术架构
2.1 技术栈
| 层级 | 技术选型 | 版本 | 说明 |
|---|---|---|---|
| 后端框架 | Spring Boot | 3.2.0 | 主体框架 |
| ORM 框架 | MyBatis-Plus | 3.5.5 | 需确认与 Spring Boot 3.2 兼容 |
| 数据库 | MySQL | 5.7+ | 生产环境建议使用 8.0 |
| 构建工具 | Maven | 3.8+ | - |
| JDK | OpenJDK | 17 | Spring Boot 3.x 最低要求 |
| 通信协议 | HTTP/HTTPS + XML | - | 微信服务器交互协议 |
版本提示:Spring Boot 3.2 与 MyBatis-Plus 3.5.5 存在已知的兼容性问题,详见第 4.4 节。
2.2 系统整体架构
#mermaid-svg-3xnDShFB92HPENOK{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-3xnDShFB92HPENOK .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-3xnDShFB92HPENOK .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-3xnDShFB92HPENOK .error-icon{fill:#552222;}#mermaid-svg-3xnDShFB92HPENOK .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-3xnDShFB92HPENOK .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-3xnDShFB92HPENOK .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-3xnDShFB92HPENOK .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-3xnDShFB92HPENOK .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-3xnDShFB92HPENOK .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-3xnDShFB92HPENOK .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-3xnDShFB92HPENOK .marker{fill:#333333;stroke:#333333;}#mermaid-svg-3xnDShFB92HPENOK .marker.cross{stroke:#333333;}#mermaid-svg-3xnDShFB92HPENOK svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-3xnDShFB92HPENOK p{margin:0;}#mermaid-svg-3xnDShFB92HPENOK .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-3xnDShFB92HPENOK .cluster-label text{fill:#333;}#mermaid-svg-3xnDShFB92HPENOK .cluster-label span{color:#333;}#mermaid-svg-3xnDShFB92HPENOK .cluster-label span p{background-color:transparent;}#mermaid-svg-3xnDShFB92HPENOK .label text,#mermaid-svg-3xnDShFB92HPENOK span{fill:#333;color:#333;}#mermaid-svg-3xnDShFB92HPENOK .node rect,#mermaid-svg-3xnDShFB92HPENOK .node circle,#mermaid-svg-3xnDShFB92HPENOK .node ellipse,#mermaid-svg-3xnDShFB92HPENOK .node polygon,#mermaid-svg-3xnDShFB92HPENOK .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-3xnDShFB92HPENOK .rough-node .label text,#mermaid-svg-3xnDShFB92HPENOK .node .label text,#mermaid-svg-3xnDShFB92HPENOK .image-shape .label,#mermaid-svg-3xnDShFB92HPENOK .icon-shape .label{text-anchor:middle;}#mermaid-svg-3xnDShFB92HPENOK .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-3xnDShFB92HPENOK .rough-node .label,#mermaid-svg-3xnDShFB92HPENOK .node .label,#mermaid-svg-3xnDShFB92HPENOK .image-shape .label,#mermaid-svg-3xnDShFB92HPENOK .icon-shape .label{text-align:center;}#mermaid-svg-3xnDShFB92HPENOK .node.clickable{cursor:pointer;}#mermaid-svg-3xnDShFB92HPENOK .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-3xnDShFB92HPENOK .arrowheadPath{fill:#333333;}#mermaid-svg-3xnDShFB92HPENOK .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-3xnDShFB92HPENOK .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-3xnDShFB92HPENOK .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3xnDShFB92HPENOK .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-3xnDShFB92HPENOK .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3xnDShFB92HPENOK .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-3xnDShFB92HPENOK .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-3xnDShFB92HPENOK .cluster text{fill:#333;}#mermaid-svg-3xnDShFB92HPENOK .cluster span{color:#333;}#mermaid-svg-3xnDShFB92HPENOK div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-3xnDShFB92HPENOK .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-3xnDShFB92HPENOK rect.text{fill:none;stroke-width:0;}#mermaid-svg-3xnDShFB92HPENOK .icon-shape,#mermaid-svg-3xnDShFB92HPENOK .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3xnDShFB92HPENOK .icon-shape p,#mermaid-svg-3xnDShFB92HPENOK .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-3xnDShFB92HPENOK .icon-shape .label rect,#mermaid-svg-3xnDShFB92HPENOK .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3xnDShFB92HPENOK .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-3xnDShFB92HPENOK .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-3xnDShFB92HPENOK :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 数据层
后端服务
微信生态
发送消息
返回回复
HTTP/HTTPS + XML
MyBatis-Plus
微信用户
微信服务器
WechatController
消息入口
SignUtil
签名验证
WechatServiceImpl
业务处理
MiniAppUtil
URL Link生成
WechatUtil
XML解析/构造
MySQL 5.7+
2.3 系统交互架构
MySQL数据库 Spring Boot 后端 微信服务器 微信用户 MySQL数据库 Spring Boot 后端 微信服务器 微信用户 #mermaid-svg-XZIU4FWz8aYdVTLD{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-XZIU4FWz8aYdVTLD .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-XZIU4FWz8aYdVTLD .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-XZIU4FWz8aYdVTLD .error-icon{fill:#552222;}#mermaid-svg-XZIU4FWz8aYdVTLD .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-XZIU4FWz8aYdVTLD .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-XZIU4FWz8aYdVTLD .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-XZIU4FWz8aYdVTLD .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-XZIU4FWz8aYdVTLD .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-XZIU4FWz8aYdVTLD .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-XZIU4FWz8aYdVTLD .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-XZIU4FWz8aYdVTLD .marker{fill:#333333;stroke:#333333;}#mermaid-svg-XZIU4FWz8aYdVTLD .marker.cross{stroke:#333333;}#mermaid-svg-XZIU4FWz8aYdVTLD svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-XZIU4FWz8aYdVTLD p{margin:0;}#mermaid-svg-XZIU4FWz8aYdVTLD .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-XZIU4FWz8aYdVTLD text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-XZIU4FWz8aYdVTLD .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-XZIU4FWz8aYdVTLD .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-XZIU4FWz8aYdVTLD .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-XZIU4FWz8aYdVTLD .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-XZIU4FWz8aYdVTLD #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-XZIU4FWz8aYdVTLD .sequenceNumber{fill:white;}#mermaid-svg-XZIU4FWz8aYdVTLD #sequencenumber{fill:#333;}#mermaid-svg-XZIU4FWz8aYdVTLD #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-XZIU4FWz8aYdVTLD .messageText{fill:#333;stroke:none;}#mermaid-svg-XZIU4FWz8aYdVTLD .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-XZIU4FWz8aYdVTLD .labelText,#mermaid-svg-XZIU4FWz8aYdVTLD .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-XZIU4FWz8aYdVTLD .loopText,#mermaid-svg-XZIU4FWz8aYdVTLD .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-XZIU4FWz8aYdVTLD .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-XZIU4FWz8aYdVTLD .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-XZIU4FWz8aYdVTLD .noteText,#mermaid-svg-XZIU4FWz8aYdVTLD .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-XZIU4FWz8aYdVTLD .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-XZIU4FWz8aYdVTLD .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-XZIU4FWz8aYdVTLD .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-XZIU4FWz8aYdVTLD .actorPopupMenu{position:absolute;}#mermaid-svg-XZIU4FWz8aYdVTLD .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-XZIU4FWz8aYdVTLD .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-XZIU4FWz8aYdVTLD .actor-man circle,#mermaid-svg-XZIU4FWz8aYdVTLD line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-XZIU4FWz8aYdVTLD :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 1. 解析XML消息 2. 判断消息类型 3. 根据交互次数回复 发送消息 POST /wx/callback (XML) 查询/更新用户数据 返回用户交互次数 回复XML消息 (文本/链接) 返回回复内容
交互流程说明:
- 微信用户向公众号发送消息
- 微信服务器将消息以 XML 格式 POST 推送到后端服务
- 后端服务解析 XML,根据
MsgType分发到不同处理逻辑 - 处理过程中可能查询/更新 MySQL 中的用户交互次数
- 后端服务构造回复 XML 返回给微信服务器
- 微信服务器将回复内容展示给用户
2.3 项目模块结构
src/main/java/com/example/wxservice/
├── WxServiceApplication.java # 启动类
├── config/
│ └── WeChatConfig.java # 微信公众号配置类(@ConfigurationProperties)
├── controller/
│ └── WechatController.java # 消息接收入口(GET验证 + POST接收)
├── entity/
│ └── WxUser.java # 用户实体(含 interaction_count 字段)
├── mapper/
│ └── WxUserMapper.java # 用户数据访问(MyBatis-Plus BaseMapper)
├── service/
│ ├── WechatService.java # 业务接口
│ └── impl/
│ └── WechatServiceImpl.java # 业务实现(核心逻辑:关注处理、消息分发、分阶段回复)
└── util/
├── MiniAppUtil.java # 小程序 URL Link 生成(调用微信 API)
├── SignUtil.java # 签名验证工具(SHA1 字典序排序)
└── WechatUtil.java # XML 解析、消息构造(CDATA 兼容)
三、开发步骤详解
3.1 配置公众号服务器
登录 微信公众平台 → 开发 → 基本配置,填写以下信息:
| 配置项 | 说明 | 示例值 |
|---|---|---|
| 服务器 URL | 外网可访问的 HTTPS 地址 | https://域名/wx/callback |
| Token | 自定义令牌,用于签名验证 | 9DNI6IEfqN1lEVs5SvGLr5l5wCCtU |
| EncodingAESKey | 消息加解密密钥(可选) | 自动生成(明文模式可不填) |
| 消息加密方式 | 明文/兼容/安全模式 | 明文模式(开发阶段推荐) |
为什么需要 Token? Token 是微信服务器与后端服务之间的"暗号"。微信发送请求时会携带
signature(由 Token、timestamp、nonce 计算出的 SHA1 签名),后端通过相同的算法验证签名,确认请求确实来自微信官方服务器,防止恶意请求。
3.2 配置 application.yml
yaml
# application.yml
server:
port: 8088
spring:
application:
name: wx-service
datasource:
url: jdbc:mysql://localhost:3306/wx_service?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&useSSL=false
username: root
password: your_password
driver-class-name: com.mysql.cj.jdbc.Driver
# MyBatis-Plus 配置
mybatis-plus:
mapper-locations: classpath*:mapper/**/*.xml
type-aliases-package: com.example.wxservice.entity
configuration:
map-underscore-to-camel-case: true
# 微信公众号配置
wx:
# 公众号配置(从微信公众平台 -> 开发 -> 基本配置获取)
app-id: wx175ab95db5361a27
app-secret: 85fba4e2d45a1987c1ab9e7e5d19aa03
token: 9DNI6IEfqN1lEVs5SvGLr5l5wCCtU
debug: true
# 小程序配置(用于生成 URL Link 跳转小程序)
mini-app-id: wx8bba9e3c31c52c86
mini-app-secret: 0519397d042233f5b061d90ab0098503
mini-app-path: pages/index/index
3.3 服务器地址验证(GET 请求)
微信首次配置服务器 URL 时,会发送 GET 请求进行地址有效性验证。
核心代码(Controller):
java
@RestController
@RequestMapping("/wx")
public class WechatController {
private final WechatService wechatService;
/**
* 服务器地址验证(GET)
* 微信配置服务器URL时,会携带 signature、timestamp、nonce、echostr 参数访问此接口
*/
@GetMapping("/callback")
public String verifyServer(
@RequestParam("signature") String signature,
@RequestParam("timestamp") String timestamp,
@RequestParam("nonce") String nonce,
@RequestParam("echostr") String echostr) {
String result = wechatService.verifyServer(signature, timestamp, nonce, echostr);
return result != null ? result : "验证失败";
}
}
签名验证算法(SignUtil)------核心原理:
java
/**
* 微信签名验证算法(SHA1)
*
* 验证步骤:
* 1. 将 token、timestamp、nonce 三个参数按字典序排序
* 2. 将排序后的字符串拼接成一个字符串
* 3. 对拼接后的字符串进行 SHA1 加密
* 4. 比对加密结果与微信传来的 signature 是否一致
*/
public static boolean checkSignature(String token, String signature,
String timestamp, String nonce) {
// 1. 字典序排序
String[] arr = new String[]{token, timestamp, nonce};
Arrays.sort(arr);
// 2. 拼接字符串
StringBuilder content = new StringBuilder();
for (String s : arr) {
content.append(s);
}
// 3. SHA1 加密
String sha1 = sha1(content.toString());
// 4. 比对签名
return signature.equals(sha1);
}
签名验证流程图:
#mermaid-svg-7OZC8zTW4K6e2FY8{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-7OZC8zTW4K6e2FY8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-7OZC8zTW4K6e2FY8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-7OZC8zTW4K6e2FY8 .error-icon{fill:#552222;}#mermaid-svg-7OZC8zTW4K6e2FY8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-7OZC8zTW4K6e2FY8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-7OZC8zTW4K6e2FY8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-7OZC8zTW4K6e2FY8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-7OZC8zTW4K6e2FY8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-7OZC8zTW4K6e2FY8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-7OZC8zTW4K6e2FY8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-7OZC8zTW4K6e2FY8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-7OZC8zTW4K6e2FY8 .marker.cross{stroke:#333333;}#mermaid-svg-7OZC8zTW4K6e2FY8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-7OZC8zTW4K6e2FY8 p{margin:0;}#mermaid-svg-7OZC8zTW4K6e2FY8 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-7OZC8zTW4K6e2FY8 .cluster-label text{fill:#333;}#mermaid-svg-7OZC8zTW4K6e2FY8 .cluster-label span{color:#333;}#mermaid-svg-7OZC8zTW4K6e2FY8 .cluster-label span p{background-color:transparent;}#mermaid-svg-7OZC8zTW4K6e2FY8 .label text,#mermaid-svg-7OZC8zTW4K6e2FY8 span{fill:#333;color:#333;}#mermaid-svg-7OZC8zTW4K6e2FY8 .node rect,#mermaid-svg-7OZC8zTW4K6e2FY8 .node circle,#mermaid-svg-7OZC8zTW4K6e2FY8 .node ellipse,#mermaid-svg-7OZC8zTW4K6e2FY8 .node polygon,#mermaid-svg-7OZC8zTW4K6e2FY8 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-7OZC8zTW4K6e2FY8 .rough-node .label text,#mermaid-svg-7OZC8zTW4K6e2FY8 .node .label text,#mermaid-svg-7OZC8zTW4K6e2FY8 .image-shape .label,#mermaid-svg-7OZC8zTW4K6e2FY8 .icon-shape .label{text-anchor:middle;}#mermaid-svg-7OZC8zTW4K6e2FY8 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-7OZC8zTW4K6e2FY8 .rough-node .label,#mermaid-svg-7OZC8zTW4K6e2FY8 .node .label,#mermaid-svg-7OZC8zTW4K6e2FY8 .image-shape .label,#mermaid-svg-7OZC8zTW4K6e2FY8 .icon-shape .label{text-align:center;}#mermaid-svg-7OZC8zTW4K6e2FY8 .node.clickable{cursor:pointer;}#mermaid-svg-7OZC8zTW4K6e2FY8 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-7OZC8zTW4K6e2FY8 .arrowheadPath{fill:#333333;}#mermaid-svg-7OZC8zTW4K6e2FY8 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-7OZC8zTW4K6e2FY8 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-7OZC8zTW4K6e2FY8 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7OZC8zTW4K6e2FY8 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-7OZC8zTW4K6e2FY8 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7OZC8zTW4K6e2FY8 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-7OZC8zTW4K6e2FY8 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-7OZC8zTW4K6e2FY8 .cluster text{fill:#333;}#mermaid-svg-7OZC8zTW4K6e2FY8 .cluster span{color:#333;}#mermaid-svg-7OZC8zTW4K6e2FY8 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-7OZC8zTW4K6e2FY8 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-7OZC8zTW4K6e2FY8 rect.text{fill:none;stroke-width:0;}#mermaid-svg-7OZC8zTW4K6e2FY8 .icon-shape,#mermaid-svg-7OZC8zTW4K6e2FY8 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7OZC8zTW4K6e2FY8 .icon-shape p,#mermaid-svg-7OZC8zTW4K6e2FY8 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-7OZC8zTW4K6e2FY8 .icon-shape .label rect,#mermaid-svg-7OZC8zTW4K6e2FY8 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7OZC8zTW4K6e2FY8 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-7OZC8zTW4K6e2FY8 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-7OZC8zTW4K6e2FY8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
微信服务器发送 GET 请求
携带参数
signature, timestamp, nonce, echostr
后端接收参数
提取 token, timestamp, nonce
按字典序排序
拼接成字符串
SHA1 加密
加密结果 == signature?
返回 echostr
验证通过
返回失败
验证不通过
3.4 接收消息推送(POST 请求)
用户发送消息后,微信服务器会向配置的 URL 发送 POST 请求,请求体为 XML 格式。
java
/**
* 接收微信消息推送(POST)
*
* 注意:此接口的 Content-Type 是 application/xml,不是 application/json
*/
@PostMapping(value = "/callback", produces = "application/xml; charset=UTF-8")
public String receiveMessage(HttpServletRequest request) {
try {
// 读取请求体中的 XML 数据
StringBuilder sb = new StringBuilder();
BufferedReader reader = request.getReader();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
String xmlData = sb.toString();
// 交给 Service 层处理
return wechatService.handleMessage(xmlData);
} catch (Exception e) {
log.error("接收微信消息时发生错误", e);
return "success"; // 即使异常也要返回 success,否则微信会重试
}
}
3.5 解析 XML 消息
微信推送的 XML 消息示例:
xml
<xml>
<ToUserName><![CDATA[gh_xxx]]></ToUserName>
<FromUserName><![CDATA[openid_xxx]]></FromUserName>
<CreateTime>123456789</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[你好]]></Content>
</xml>
解析方法------为什么需要兼容 CDATA?
微信的 XML 节点值包裹在 <![CDATA[...]]> 中,这是为了防止特殊字符(如 <、>、&)破坏 XML 结构。解析时需要先尝试 CDATA 格式,再尝试普通标签格式。
java
/**
* 解析 XML 消息中的节点值
*
* 支持两种格式:
* 1. CDATA 格式:<TagName><![CDATA[value]]></TagName>
* 2. 普通格式:<TagName>value</TagName>
*/
public static String getXmlValue(String xml, String nodeName) {
if (xml == null || xml.isEmpty()) {
return null;
}
// 优先尝试 CDATA 格式
String cdataStart = "<" + nodeName + "><![CDATA[";
String cdataEnd = "]]></" + nodeName + ">";
int startIndex = xml.indexOf(cdataStart);
int endIndex = xml.indexOf(cdataEnd);
if (startIndex != -1 && endIndex != -1) {
return xml.substring(startIndex + cdataStart.length(), endIndex);
}
// 再尝试普通标签格式
String startTag = "<" + nodeName + ">";
String endTag = "</" + nodeName + ">";
startIndex = xml.indexOf(startTag);
endIndex = xml.indexOf(endTag);
if (startIndex != -1 && endIndex != -1) {
return xml.substring(startIndex + startTag.length(), endIndex);
}
return null;
}
3.6 消息类型分发处理
消息处理流程图:
#mermaid-svg-rhmKRgcgVKOFe1Tn{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-rhmKRgcgVKOFe1Tn .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-rhmKRgcgVKOFe1Tn .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-rhmKRgcgVKOFe1Tn .error-icon{fill:#552222;}#mermaid-svg-rhmKRgcgVKOFe1Tn .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-rhmKRgcgVKOFe1Tn .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-rhmKRgcgVKOFe1Tn .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-rhmKRgcgVKOFe1Tn .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-rhmKRgcgVKOFe1Tn .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-rhmKRgcgVKOFe1Tn .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-rhmKRgcgVKOFe1Tn .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-rhmKRgcgVKOFe1Tn .marker{fill:#333333;stroke:#333333;}#mermaid-svg-rhmKRgcgVKOFe1Tn .marker.cross{stroke:#333333;}#mermaid-svg-rhmKRgcgVKOFe1Tn svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-rhmKRgcgVKOFe1Tn p{margin:0;}#mermaid-svg-rhmKRgcgVKOFe1Tn .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-rhmKRgcgVKOFe1Tn .cluster-label text{fill:#333;}#mermaid-svg-rhmKRgcgVKOFe1Tn .cluster-label span{color:#333;}#mermaid-svg-rhmKRgcgVKOFe1Tn .cluster-label span p{background-color:transparent;}#mermaid-svg-rhmKRgcgVKOFe1Tn .label text,#mermaid-svg-rhmKRgcgVKOFe1Tn span{fill:#333;color:#333;}#mermaid-svg-rhmKRgcgVKOFe1Tn .node rect,#mermaid-svg-rhmKRgcgVKOFe1Tn .node circle,#mermaid-svg-rhmKRgcgVKOFe1Tn .node ellipse,#mermaid-svg-rhmKRgcgVKOFe1Tn .node polygon,#mermaid-svg-rhmKRgcgVKOFe1Tn .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-rhmKRgcgVKOFe1Tn .rough-node .label text,#mermaid-svg-rhmKRgcgVKOFe1Tn .node .label text,#mermaid-svg-rhmKRgcgVKOFe1Tn .image-shape .label,#mermaid-svg-rhmKRgcgVKOFe1Tn .icon-shape .label{text-anchor:middle;}#mermaid-svg-rhmKRgcgVKOFe1Tn .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-rhmKRgcgVKOFe1Tn .rough-node .label,#mermaid-svg-rhmKRgcgVKOFe1Tn .node .label,#mermaid-svg-rhmKRgcgVKOFe1Tn .image-shape .label,#mermaid-svg-rhmKRgcgVKOFe1Tn .icon-shape .label{text-align:center;}#mermaid-svg-rhmKRgcgVKOFe1Tn .node.clickable{cursor:pointer;}#mermaid-svg-rhmKRgcgVKOFe1Tn .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-rhmKRgcgVKOFe1Tn .arrowheadPath{fill:#333333;}#mermaid-svg-rhmKRgcgVKOFe1Tn .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-rhmKRgcgVKOFe1Tn .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-rhmKRgcgVKOFe1Tn .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rhmKRgcgVKOFe1Tn .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-rhmKRgcgVKOFe1Tn .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rhmKRgcgVKOFe1Tn .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-rhmKRgcgVKOFe1Tn .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-rhmKRgcgVKOFe1Tn .cluster text{fill:#333;}#mermaid-svg-rhmKRgcgVKOFe1Tn .cluster span{color:#333;}#mermaid-svg-rhmKRgcgVKOFe1Tn div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-rhmKRgcgVKOFe1Tn .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-rhmKRgcgVKOFe1Tn rect.text{fill:none;stroke-width:0;}#mermaid-svg-rhmKRgcgVKOFe1Tn .icon-shape,#mermaid-svg-rhmKRgcgVKOFe1Tn .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rhmKRgcgVKOFe1Tn .icon-shape p,#mermaid-svg-rhmKRgcgVKOFe1Tn .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-rhmKRgcgVKOFe1Tn .icon-shape .label rect,#mermaid-svg-rhmKRgcgVKOFe1Tn .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rhmKRgcgVKOFe1Tn .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-rhmKRgcgVKOFe1Tn .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-rhmKRgcgVKOFe1Tn :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} event
text/voice/file
其他
微信用户发送消息
微信服务器 POST 推送
后端接收 XML 消息
解析 MsgType
消息类型?
handleEvent
关注/取消关注
handleTextMessage
分阶段回复
返回 success
构造回复 XML
结束
返回微信服务器
微信展示给用户
微信消息有多种类型(text、voice、image、event 等),需要根据 MsgType 进行分发:
java
@Override
public String handleMessage(String xmlData) {
String msgType = WechatUtil.getXmlValue(xmlData, "MsgType");
String fromUser = WechatUtil.getXmlValue(xmlData, "FromUserName");
String toUser = WechatUtil.getXmlValue(xmlData, "ToUserName");
// 处理事件(关注、取消关注、扫码等)
if ("event".equals(msgType)) {
return handleEvent(xmlData, fromUser, toUser);
}
// 处理文本、语音、文档消息
if ("text".equals(msgType) || "voice".equals(msgType) || "file".equals(msgType)) {
return handleTextMessage(xmlData, fromUser, toUser);
}
// 其他类型消息,回复默认信息
return "success";
}
3.7 分阶段自动回复(核心功能)
设计思路:
通过数据库 interaction_count 字段记录用户与公众号的交互次数,根据次数返回不同的回复内容:
| 阶段 | 触发条件 | 回复内容 | 业务目的 |
|---|---|---|---|
| 关注之后 | 用户首次关注 | 纯文本欢迎语 | 建立第一印象 |
| 第一次 | interaction_count == 0 |
带小程序链接(报修/报装) | 引导用户使用服务 |
| 第二次 | interaction_count == 1 |
带链接 + 引导描述需求 | 收集用户需求 |
| 第 N 次 | interaction_count >= 2 |
纯文本安抚 | 告知已收到反馈 |
分阶段自动回复流程图:
#mermaid-svg-7zZ81iVZdReSwCMg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-7zZ81iVZdReSwCMg .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-7zZ81iVZdReSwCMg .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-7zZ81iVZdReSwCMg .error-icon{fill:#552222;}#mermaid-svg-7zZ81iVZdReSwCMg .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-7zZ81iVZdReSwCMg .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-7zZ81iVZdReSwCMg .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-7zZ81iVZdReSwCMg .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-7zZ81iVZdReSwCMg .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-7zZ81iVZdReSwCMg .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-7zZ81iVZdReSwCMg .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-7zZ81iVZdReSwCMg .marker{fill:#333333;stroke:#333333;}#mermaid-svg-7zZ81iVZdReSwCMg .marker.cross{stroke:#333333;}#mermaid-svg-7zZ81iVZdReSwCMg svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-7zZ81iVZdReSwCMg p{margin:0;}#mermaid-svg-7zZ81iVZdReSwCMg .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-7zZ81iVZdReSwCMg .cluster-label text{fill:#333;}#mermaid-svg-7zZ81iVZdReSwCMg .cluster-label span{color:#333;}#mermaid-svg-7zZ81iVZdReSwCMg .cluster-label span p{background-color:transparent;}#mermaid-svg-7zZ81iVZdReSwCMg .label text,#mermaid-svg-7zZ81iVZdReSwCMg span{fill:#333;color:#333;}#mermaid-svg-7zZ81iVZdReSwCMg .node rect,#mermaid-svg-7zZ81iVZdReSwCMg .node circle,#mermaid-svg-7zZ81iVZdReSwCMg .node ellipse,#mermaid-svg-7zZ81iVZdReSwCMg .node polygon,#mermaid-svg-7zZ81iVZdReSwCMg .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-7zZ81iVZdReSwCMg .rough-node .label text,#mermaid-svg-7zZ81iVZdReSwCMg .node .label text,#mermaid-svg-7zZ81iVZdReSwCMg .image-shape .label,#mermaid-svg-7zZ81iVZdReSwCMg .icon-shape .label{text-anchor:middle;}#mermaid-svg-7zZ81iVZdReSwCMg .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-7zZ81iVZdReSwCMg .rough-node .label,#mermaid-svg-7zZ81iVZdReSwCMg .node .label,#mermaid-svg-7zZ81iVZdReSwCMg .image-shape .label,#mermaid-svg-7zZ81iVZdReSwCMg .icon-shape .label{text-align:center;}#mermaid-svg-7zZ81iVZdReSwCMg .node.clickable{cursor:pointer;}#mermaid-svg-7zZ81iVZdReSwCMg .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-7zZ81iVZdReSwCMg .arrowheadPath{fill:#333333;}#mermaid-svg-7zZ81iVZdReSwCMg .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-7zZ81iVZdReSwCMg .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-7zZ81iVZdReSwCMg .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7zZ81iVZdReSwCMg .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-7zZ81iVZdReSwCMg .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7zZ81iVZdReSwCMg .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-7zZ81iVZdReSwCMg .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-7zZ81iVZdReSwCMg .cluster text{fill:#333;}#mermaid-svg-7zZ81iVZdReSwCMg .cluster span{color:#333;}#mermaid-svg-7zZ81iVZdReSwCMg div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-7zZ81iVZdReSwCMg .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-7zZ81iVZdReSwCMg rect.text{fill:none;stroke-width:0;}#mermaid-svg-7zZ81iVZdReSwCMg .icon-shape,#mermaid-svg-7zZ81iVZdReSwCMg .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7zZ81iVZdReSwCMg .icon-shape p,#mermaid-svg-7zZ81iVZdReSwCMg .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-7zZ81iVZdReSwCMg .icon-shape .label rect,#mermaid-svg-7zZ81iVZdReSwCMg .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7zZ81iVZdReSwCMg .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-7zZ81iVZdReSwCMg .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-7zZ81iVZdReSwCMg :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
是
否
用户发送消息
查询用户 interaction_count
interaction_count == 0?
第一次回复
带小程序链接
set interaction_count = 1
interaction_count == 1?
第二次回复
引导描述需求
set interaction_count = 2
第N次回复
纯文本安抚
interaction_count + 1
返回回复内容
核心实现:
java
private String handleTextMessage(String xmlData, String fromUser, String toUser) {
// ...
WxUser user = wxUserMapper.selectByOpenId(fromUser);
int interactionCount = user.getInteractionCount() != null ? user.getInteractionCount() : 0;
if (interactionCount == 0) {
// 第一次:引导使用服务(带小程序链接)
String reply = buildDefaultNewsReply(fromUser, toUser);
user.setInteractionCount(1);
wxUserMapper.updateById(user);
return reply;
} else if (interactionCount == 1) {
// 第二次:引导描述需求
String reply = buildSecondReply(fromUser, toUser);
user.setInteractionCount(2);
wxUserMapper.updateById(user);
return reply;
} else {
// 第N次:安抚用户
user.setInteractionCount(interactionCount + 1);
wxUserMapper.updateById(user);
return WechatUtil.buildTextMessage(fromUser, toUser,
"感谢您的反馈,稍后会有服务人员与您联系,请您关注公众号信息。");
}
}
3.8 生成小程序 URL Link
URL Link 生成流程图:
#mermaid-svg-ihNVeSPxSSjOj07a{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ihNVeSPxSSjOj07a .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ihNVeSPxSSjOj07a .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ihNVeSPxSSjOj07a .error-icon{fill:#552222;}#mermaid-svg-ihNVeSPxSSjOj07a .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ihNVeSPxSSjOj07a .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ihNVeSPxSSjOj07a .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ihNVeSPxSSjOj07a .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ihNVeSPxSSjOj07a .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ihNVeSPxSSjOj07a .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ihNVeSPxSSjOj07a .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ihNVeSPxSSjOj07a .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ihNVeSPxSSjOj07a .marker.cross{stroke:#333333;}#mermaid-svg-ihNVeSPxSSjOj07a svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ihNVeSPxSSjOj07a p{margin:0;}#mermaid-svg-ihNVeSPxSSjOj07a .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ihNVeSPxSSjOj07a .cluster-label text{fill:#333;}#mermaid-svg-ihNVeSPxSSjOj07a .cluster-label span{color:#333;}#mermaid-svg-ihNVeSPxSSjOj07a .cluster-label span p{background-color:transparent;}#mermaid-svg-ihNVeSPxSSjOj07a .label text,#mermaid-svg-ihNVeSPxSSjOj07a span{fill:#333;color:#333;}#mermaid-svg-ihNVeSPxSSjOj07a .node rect,#mermaid-svg-ihNVeSPxSSjOj07a .node circle,#mermaid-svg-ihNVeSPxSSjOj07a .node ellipse,#mermaid-svg-ihNVeSPxSSjOj07a .node polygon,#mermaid-svg-ihNVeSPxSSjOj07a .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ihNVeSPxSSjOj07a .rough-node .label text,#mermaid-svg-ihNVeSPxSSjOj07a .node .label text,#mermaid-svg-ihNVeSPxSSjOj07a .image-shape .label,#mermaid-svg-ihNVeSPxSSjOj07a .icon-shape .label{text-anchor:middle;}#mermaid-svg-ihNVeSPxSSjOj07a .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ihNVeSPxSSjOj07a .rough-node .label,#mermaid-svg-ihNVeSPxSSjOj07a .node .label,#mermaid-svg-ihNVeSPxSSjOj07a .image-shape .label,#mermaid-svg-ihNVeSPxSSjOj07a .icon-shape .label{text-align:center;}#mermaid-svg-ihNVeSPxSSjOj07a .node.clickable{cursor:pointer;}#mermaid-svg-ihNVeSPxSSjOj07a .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ihNVeSPxSSjOj07a .arrowheadPath{fill:#333333;}#mermaid-svg-ihNVeSPxSSjOj07a .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ihNVeSPxSSjOj07a .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ihNVeSPxSSjOj07a .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ihNVeSPxSSjOj07a .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ihNVeSPxSSjOj07a .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ihNVeSPxSSjOj07a .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ihNVeSPxSSjOj07a .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ihNVeSPxSSjOj07a .cluster text{fill:#333;}#mermaid-svg-ihNVeSPxSSjOj07a .cluster span{color:#333;}#mermaid-svg-ihNVeSPxSSjOj07a div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ihNVeSPxSSjOj07a .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ihNVeSPxSSjOj07a rect.text{fill:none;stroke-width:0;}#mermaid-svg-ihNVeSPxSSjOj07a .icon-shape,#mermaid-svg-ihNVeSPxSSjOj07a .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ihNVeSPxSSjOj07a .icon-shape p,#mermaid-svg-ihNVeSPxSSjOj07a .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ihNVeSPxSSjOj07a .icon-shape .label rect,#mermaid-svg-ihNVeSPxSSjOj07a .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ihNVeSPxSSjOj07a .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ihNVeSPxSSjOj07a .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ihNVeSPxSSjOj07a :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
接收 path 参数
path 包含 '?'?
分离 pagePath 和 query
pagePath = path
query =
获取 AccessToken
构造请求体
调用微信接口
wxa/generate_urllink
返回 URL Link
为什么需要分离 path 和 query?
微信 generate_urllink 接口要求 path 字段不能携带 query 参数 ,需要拆分为 path 和 query 两个字段分别传递。
java
public String generateUrlLink(String path) {
String accessToken = getAccessToken(); // 带本地缓存
// 分离 path 和 query
String pagePath = path;
String query = "";
int queryIndex = path.indexOf("?");
if (queryIndex != -1) {
pagePath = path.substring(0, queryIndex);
query = path.substring(queryIndex + 1);
}
// 调用微信接口
Map<String, Object> body = new HashMap<>();
body.put("path", pagePath);
body.put("query", query);
body.put("is_expire", false);
// ...
}
3.9 数据表设计
sql
CREATE TABLE wx_user (
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
open_id VARCHAR(64) NOT NULL COMMENT '微信用户的OpenID(唯一标识)',
nick_name VARCHAR(128) DEFAULT NULL COMMENT '用户昵称',
sex INT DEFAULT 0 COMMENT '性别 0:未知 1:男 2:女',
city VARCHAR(64) DEFAULT NULL COMMENT '用户所在城市',
province VARCHAR(64) DEFAULT NULL COMMENT '用户所在省份',
country VARCHAR(64) DEFAULT NULL COMMENT '用户所在国家',
head_img_url VARCHAR(512) DEFAULT NULL COMMENT '用户头像URL',
subscribe_status INT DEFAULT 1 COMMENT '关注状态 0:取消关注 1:已关注',
subscribe_time DATETIME DEFAULT NULL COMMENT '关注时间',
unsubscribe_time DATETIME DEFAULT NULL COMMENT '取消关注时间',
interaction_count INT DEFAULT 0 COMMENT '用户交互次数(用于自动回复规则)',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
UNIQUE KEY uk_open_id (open_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='微信公众号用户表';
四、实战中遇到的问题与解决方案
4.1 MySQL 低版本不支持 ADD COLUMN IF NOT EXISTS
现象:
#1064 - You have an error in your SQL syntax
原因: ALTER TABLE ... ADD COLUMN IF NOT EXISTS 是 MySQL 8.0+ 语法,MySQL 5.7 不支持。
解决方案: 直接使用标准的 ALTER TABLE 语句:
sql
ALTER TABLE wx_user ADD COLUMN interaction_count INT DEFAULT 0
COMMENT '用户交互次数(用于自动回复规则)';
风险提示 :如果字段已存在会提示
Duplicate column name,可手动忽略。
4.2 SQL 客户端不支持存储过程 DELIMITER 语法
现象: 在 Navicat、phpMyAdmin 等图形化客户端中执行含 DELIMITER $$ 的存储过程时报错。
原因: 图形化 SQL 客户端通常不支持自定义定界符语法。
解决方案: 避免使用存储过程,直接执行简单的 ALTER TABLE 语句。如果必须使用存储过程,建议在 MySQL 命令行客户端中执行。
4.3 小程序 URL Link 接口返回 412 错误
现象:
json
{"errcode": 412, "errmsg": "..."}
可能原因:
- 小程序未通过微信认证
- 服务器 IP 未加入小程序 IP 白名单
- 接口权限未开通(需企业级主体)
- 请求参数格式错误(path 携带了 query 参数)
排查步骤:
- 检查小程序认证状态
- 在"开发 → 开发管理 → 开发设置"中添加服务器 IP 白名单
- 确认接口权限已开通
- 检查请求参数是否符合微信文档要求(path 与 query 分离)
4.4 Spring Boot 3.2 + MyBatis-Plus 启动异常
现象: 启动时报 factoryBeanObjectType 类型错误。
原因: MyBatis-Plus 3.5.5 与 Spring Boot 3.2.0 存在兼容性问题。
解决方案: 确保使用兼容 Spring Boot 3.2 的 MyBatis-Plus 版本,或在 pom.xml 中排除冲突依赖。
五、部署清单
5.1 环境准备
- JDK 17 安装配置
- MySQL 5.7+ 数据库服务
- 公网可访问的服务器或内网穿透工具
5.2 配置检查
- 公众号 AppID、AppSecret、Token 配置正确
- 小程序 AppID、AppSecret 配置正确(如需 URL Link 功能)
- 数据库连接配置正确
- 服务器 IP 已加入小程序白名单
5.3 数据库初始化
- 执行建表 SQL(新环境)
- 执行 ALTER 语句添加新字段(已有数据环境)
5.4 公众号配置
- 配置服务器 URL
- 配置 Token
- 启用服务器配置
- 测试关注/取消关注事件
- 测试文本消息回复
- 测试语音/文档消息回复
六、后续可扩展功能
| 序号 | 扩展方向 | 核心说明 |
|---|---|---|
| 1 | 关键词自动回复(数据库化) | 从静态代码改为后台动态配置,支持正则匹配和多种回复类型 |
| 2 | 模板消息推送 | 48 小时内主动触达用户,用于工单通知、活动提醒等 |
| 3 | 自定义菜单与事件处理 | 底部菜单配置,click/view/miniprogram 三种类型 |
| 4 | 用户画像与数据分析 | 记录交互日志,统计活跃用户数、消息分布、留存率等 |
| 5 | 消息队列(MQ)异步处理 | 引入 RabbitMQ/Kafka 削峰填谷,避免消息丢失 |
| 6 | Redis 缓存优化 | 持久化 AccessToken、缓存用户信息、分布式锁 |
| 7 | 多公众号接入支持 | 抽象配置,按 ToUserName 路由不同公众号 |
| 8 | 消息加解密(安全模式) | 启用 AES 加解密,提升传输安全性 |
| 9 | 客服消息系统 | 特定关键词转人工,工单分配、聊天记录、满意度评价 |
| 10 | 素材管理与多媒体消息 | 图片/语音/视频素材管理,智能识别回复 |
七、总结
本文从公众号服务器配置 → 签名验证 → 消息接收 → XML 解析 → 分阶段自动回复 → 小程序 URL Link 六个环节,完整梳理了微信公众号后端服务的开发流程。
核心要点回顾:
- 签名验证是安全保障:通过 Token + SHA1 确保请求来自微信官方
- interaction_count 是实现分阶段回复的关键:关注→第一次→第二次→第 N 次,形成完整的用户交互链路
- path 与 query 分离是调用微信 API 的常识 :
generate_urllink等多个接口都有此要求 - MySQL 5.7 兼容性需提前考虑:避免使用高版本语法
如果你在开发过程中遇到其他问题,欢迎在评论区交流。