微信公众平台Java开发全教程

第一部分:准备工作与核心概念

在开始编码之前,我们必须先理解微信公众平台的基本概念和准备工作。

微信公众平台开发java教程
(图片来源网络,侵删)

公众号类型选择

微信公众平台主要分为三种类型,你需要根据你的需求选择:

  • 订阅号:主要用于信息传递,适合媒体、个人等,每天可以群发一次消息,认证后可申请微信支付接口。
  • 服务号:主要为用户提供服务和功能,适合企业、政府等,每月可群发四次消息,认证后默认拥有微信支付接口。
  • 小程序:一个不需要下载安装即可使用的应用,提供了丰富的UI组件和API。

本教程以 服务号 为例进行讲解,因为其接口功能最全。

申请与配置

  1. 注册公众号:访问 微信公众平台官网,根据指引完成注册。
  2. 开发者身份验证
    • 登录后台,进入「开发」->「基本配置」。
    • 点击「成为开发者」,填写服务器配置(URL、Token、EncodingAESKey)。
    • URL:你的Java应用在公网上的访问地址,必须以 http://https:// 开头,并支持80/443端口,腾讯服务器会向这个地址发送验证请求。
    • Token:可以任意填写,用作生成签名(与后面提到的token不同,这里只是验证用的字符串)。
    • EncodingAESKey:消息加解密密钥,随机生成即可。
    • 消息加解密方式:选择「安全模式」或「兼容模式」,推荐「安全模式」。
  3. 获取凭证

    在「基本配置」页面,找到「开发者ID(AppID)」和「开发者密码(AppSecret)」,这两个是调用微信API的核心凭证,请妥善保管。

核心概念

  • 开发者ID (AppID)开发者密码 (AppSecret):用于获取access_token
  • Access Token:公众号的全局唯一接口调用凭据,有效期2小时,所有API调用都需要它,你需要自己编写代码定时刷新并缓存它。
  • OpenID:用户在单个公众号下的唯一标识,同一个用户在不同公众号下的OpenID是不同的。
  • UnionID:如果开发者拥有多个公众号,可通过UnionID来区分用户,只要用户在公众号或小程序中授权过,UnionID在所有应用中都唯一。

第二部分:环境搭建

我们将使用 Spring Boot 来快速搭建项目,因为它极大地简化了Java Web开发的配置。

微信公众平台开发java教程
(图片来源网络,侵删)

创建Spring Boot项目

使用 Spring Initializr 快速创建项目。

  • Project: Maven Project
  • Language: Java
  • Spring Boot: 选择一个稳定版本 (如 2.7.x 或 3.x.x)
  • Project Metadata:
    • Group: com.example
    • Artifact: wechat-demo
    • Name: wechat-demo
    • Packaging: Jar
    • Java: 11 或更高版本
  • Dependencies:
    • Spring Web: 用于构建Web应用。
    • Lombok (可选): 简化Java代码。
    • Spring Data Redis (推荐): 用于缓存access_token

项目结构

创建一个基本的项目结构,用于存放不同功能的代码。

src/main/java/com/example/wechatdemo/
├── config/          // 配置类
├── controller/      // 控制器,处理HTTP请求
├── service/         // 业务逻辑层
│   ├── impl/        // 服务实现
│   └── WeChatApiService.java
├── utils/           // 工具类
│   └── WeChatUtils.java
└── WeChatDemoApplication.java

第三部分:接入与验证

这是开发的第一步,验证你的服务器是否有效。

控制器代码

创建一个控制器来处理来自微信服务器的验证请求。

src/main/java/com/example/wechatdemo/controller/WeChatController.java

package com.example.wechatdemo.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
@RestController
@RequestMapping("/wechat")
public class WeChatController {
    // 从application.properties中读取配置
    @Value("${wechat.token}")
    private String token;
    @GetMapping
    public String validate(@RequestParam("signature") String signature,
                           @RequestParam("timestamp") String timestamp,
                           @RequestParam("nonce") String nonce,
                           @RequestParam("echostr") String echostr) {
        // 1. 将token, timestamp, nonce三个参数进行字典序排序
        String[] arr = new String[]{token, timestamp, nonce};
        Arrays.sort(arr);
        // 2. 将三个参数字符串拼接成一个字符串进行sha1加密
        StringBuilder content = new StringBuilder();
        for (String s : arr) {
            content.append(s);
        }
        String temp = sha1(content.toString());
        // 3. 开发者获得加密后的字符串可与signature对比,标识该请求来源于微信
        if (temp.equals(signature)) {
            // 验证成功,原样返回echostr
            return echostr;
        } else {
            // 验证失败
            return "error";
        }
    }
    /**
     * SHA1加密算法
     */
    private String sha1(String str) {
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-1");
            digest.update(str.getBytes());
            byte[] messageDigest = digest.digest();
            StringBuilder hexStr = new StringBuilder();
            for (byte b : messageDigest) {
                String shaHex = Integer.toHexString(b & 0xFF);
                if (shaHex.length() < 2) {
                    hexStr.append(0);
                }
                hexStr.append(shaHex);
            }
            return hexStr.toString();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return "";
    }
}

配置文件

src/main/resources/application.properties 中添加你的配置。

# 服务器端口
server.port=8080
# 微信公众号配置
# 这里的token必须和公众号后台设置的完全一致
wechat.token=your_wechat_token
wechat.appId=your_app_id
wechat.appSecret=your_app_secret

本地测试与上线

  • 本地测试:由于你的电脑没有公网IP,微信服务器无法访问,你需要使用内网穿透工具,如 ngrok

    1. 下载并安装ngrok。
    2. 在命令行运行 ngrok http 8080,它会生成一个公网URL,如 https://abcdefg1234.ngrok.io
    3. 将这个URL(https://abcdefg1234.ngrok.io/wechat)填写到公众号后台的URL地址栏。
    4. 点击提交,如果提示成功,说明你的Java服务器已经成功接入微信公众平台。
  • 上线部署:将你的Spring Boot应用打包成jar包,上传到云服务器(如阿里云、腾讯云)上运行,确保云服务器的80/443端口开放,并将公网IP和路径配置到公众号后台。


第四部分:接收与处理用户消息

接入成功后,用户向公众号发送消息、点击菜单等事件,微信服务器都会通过你配置的URL以POST请求推送过来。

消息与事件格式

微信推送的消息是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>
    </xml>
  • 关注事件示例:
    <xml>
      <ToUserName><![CDATA[toUser]]></ToUserName>
      <FromUserName><![CDATA[fromUser]]></FromUserName>
      <CreateTime>1348831860</CreateTime>
      <MsgType><![CDATA[event]]></MsgType>
      <Event><![CDATA[subscribe]]></Event>
    </xml>

使用XStream解析XML

为了避免手动解析XML的繁琐,我们可以使用XStream库。

pom.xml中添加依赖:

<dependency>
    <groupId>com.thoughtworks.xstream</groupId>
    <artifactId>xstream</artifactId>
    <version>1.4.19</version>
</dependency>

创建消息实体类

创建Java类来映射XML结构。

src/main/java/com/example/wechatdemo/model/WeChatMessage.java

package com.example.wechatdemo.model;
import lombok.Data;
import com.thoughtworks.xstream.annotations.XStreamAlias;
@Data
public class WeChatMessage {
    @XStreamAlias("ToUserName")
    private String toUserName;
    @XStreamAlias("FromUserName")
    private String fromUserName;
    @XStreamAlias("CreateTime")
    private Long createTime;
    @XStreamAlias("MsgType")
    private String msgType;
    // ... 其他根据消息类型添加字段
}

src/main/java/com/example/wechatdemo/model/TextMessage.java

package com.example.wechatdemo.model;
import lombok.Data;
import com.thoughtworks.xstream.annotations.XStreamAlias;
@Data
@XStreamAlias("xml")
public class TextMessage extends WeChatMessage {
    @XStreamAlias("Content")
    private String content;
    // 可以添加回复文本消息需要的字段
    @XStreamAlias("Content")
    private String replyContent;
}

更新控制器处理POST请求

修改WeChatController,增加处理POST请求的逻辑。

src/main/java/com/example/wechatdemo/controller/WeChatController.java

package com.example.wechatdemo.controller;
// ... imports
import org.springframework.web.bind.annotation.PostMapping;
import com.example.wechatdemo.model.TextMessage;
import com.example.wechatdemo.model.WeChatMessage;
import com.thoughtworks.xstream.XStream;
import java.io.InputStream;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.InputStreamReader;
@RestController
@RequestMapping("/wechat")
public class WeChatController {
    // ... 前面的GET方法用于验证
    @PostMapping
    public String handlePost(HttpServletRequest request) {
        try {
            // 1. 获取输入流
            InputStream inputStream = request.getInputStream();
            // 2. 读取输入流
            BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
            StringBuilder sb = new StringBuilder();
            String line;
            while ((line = reader.readLine()) != null) {
                sb.append(line);
            }
            String xml = sb.toString();
            System.out.println("收到消息: " + xml);
            // 3. 使用XStream解析XML
            XStream xstream = new XStream();
            xstream.processAnnotations(WeChatMessage.class);
            xstream.processAnnotations(TextMessage.class);
            // 解决XML标签问题
            xstream.alias("xml", WeChatMessage.class);
            WeChatMessage message = (WeChatMessage) xstream.fromXML(xml);
            // 4. 根据消息类型处理
            if ("text".equals(message.getMsgType())) {
                TextMessage textMessage = (TextMessage) message;
                // 构造回复消息
                TextReply reply = new TextReply();
                reply.setToUserName(textMessage.getFromUserName());
                reply.setFromUserName(textMessage.getToUserName());
                reply.setCreateTime(System.currentTimeMillis() / 1000);
                reply.setMsgType("text");
                reply.setContent("你发送的是: " + textMessage.getContent());
                // 将回复消息对象转换为XML
                return xstream.toXML(reply);
            } else if ("event".equals(message.getMsgType())) {
                if ("subscribe".equals(((TextMessage)message).getEvent())) {
                    // 用户关注,返回欢迎消息
                    TextReply reply = new TextReply();
                    reply.setToUserName(message.getFromUserName());
                    reply.setFromUserName(message.getToUserName());
                    reply.setCreateTime(System.currentTimeMillis() / 1000);
                    reply.setMsgType("text");
                    reply.setContent("感谢关注!");
                    return xstream.toXML(reply);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "success"; // 无论处理成功与否,都必须返回"success",否则微信会认为消息处理失败
    }
}
// 新增一个用于回复的实体类
class TextReply extends WeChatMessage {
    private String content;
    // getters and setters
    public String getContent() { return content; }
    public void setContent(String content) { this.content = content; }
}

重启你的应用,当你关注公众号或向公众号发送文本消息时,它会给你相应的回复。


第五部分:调用微信API

接收消息只是第一步,更重要的是主动调用微信API来获取数据或执行操作。

获取Access Token

这是所有API调用前必须做的步骤,我们需要一个服务来定时获取并缓存它。

src/main/java/com/example/wechatdemo/service/WeChatApiService.java

package com.example.wechatdemo.service;
public interface WeChatApiService {
    String getAccessToken();
}

src/main/java/com/example/wechatdemo/service/impl/WeChatApiServiceImpl.java

package com.example.wechatdemo.service.impl;
import com.example.wechatdemo.service.WeChatApiService;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
@Service
public class WeChatApiServiceImpl implements WeChatApiService {
    @Value("${wechat.appId}")
    private String appId;
    @Value("${wechat.appSecret}")
    private String appSecret;
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private RestTemplate restTemplate; // 需要配置RestTemplate Bean
    private static final String ACCESS_TOKEN_KEY = "wechat:access_token";
    private static final String ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s";
    @Override
    public String getAccessToken() {
        // 1. 先从Redis缓存中获取
        String token = stringRedisTemplate.opsForValue().get(ACCESS_TOKEN_KEY);
        if (token != null) {
            return token;
        }
        // 2. 缓存中没有,则从微信服务器获取
        String url = String.format(ACCESS_TOKEN_URL, appId, appSecret);
        String response = restTemplate.getForObject(url, String.class);
        try {
            ObjectMapper mapper = new ObjectMapper();
            JsonNode rootNode = mapper.readTree(response);
            token = rootNode.get("access_token").asText();
            long expiresIn = rootNode.get("expires_in").asLong(); // 通常是7200秒
            // 3. 将token存入Redis,并设置过期时间(比实际过期时间短一点,比如7000秒)
            stringRedisTemplate.opsForValue().set(ACCESS_TOKEN_KEY, token, expiresIn - 200, TimeUnit.SECONDS);
            return token;
        } catch (Exception e) {
            // 处理异常,比如打印日志
            e.printStackTrace();
            return null;
        }
    }
}

注意:你需要在启动类或配置类中配置 RestTemplateRedisTemplate

获取用户信息

假设我们要获取刚刚关注用户的昵称和头像。

修改 WeChatController 中的关注事件处理逻辑。

src/main/java/com/example/wechatdemo/controller/WeChatController.java

// ... imports
import com.example.wechatdemo.service.WeChatApiService;
import org.springframework.beans.factory.annotation.Autowired;
@RestController
@RequestMapping("/wechat")
public class WeChatController {
    @Autowired
    private WeChatApiService weChatApiService;
    // ... 其他代码
    @PostMapping
    public String handlePost(HttpServletRequest request) {
        // ... 前面的XML解析代码
        if ("event".equals(message.getMsgType())) {
            if ("subscribe".equals(((TextMessage)message).getEvent())) {
                String openId = message.getFromUserName();
                // 1. 获取access_token
                String accessToken = weChatApiService.getAccessToken();
                if (accessToken == null) {
                    return "success"; // 获取token失败,无法回复
                }
                // 2. 调用获取用户信息API
                String userInfoUrl = String.format("https://api.weixin.qq.com/cgi-bin/user/info?access_token=%s&openid=%s&lang=zh_CN", accessToken, openId);
                String userInfoResponse = restTemplate.getForObject(userInfoUrl, String.class);
                // 3. 解析用户信息并回复
                try {
                    ObjectMapper mapper = new ObjectMapper();
                    JsonNode userNode = mapper.readTree(userInfoResponse);
                    String nickname = userNode.get("nickname").asText();
                    String headImgUrl = userNode.get("headimgurl").asText();
                    // 4. 构造回复消息
                    TextReply reply = new TextReply();
                    reply.setToUserName(openId);
                    reply.setFromUserName(message.getToUserName());
                    reply.setCreateTime(System.currentTimeMillis() / 1000);
                    reply.setMsgType("text");
                    reply.setContent("欢迎你," + nickname + "!\n你的头像链接是:" + headImgUrl);
                    XStream xstream = new XStream();
                    xstream.alias("xml", TextReply.class);
                    return xstream.toXML(reply);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
        return "success";
    }
    // ... 其他代码
}

当你再次关注公众号时,它会回复你的昵称和头像链接。


第六部分:高级功能示例

发送模板消息

模板消息用于向用户发送重要的服务通知,如订单确认、物流更新等。

步骤:

  1. 在公众号后台获取模板ID:登录公众号后台,「模板消息」->「我的模板」,选择一个模板并复制其template_id
  2. 编写发送代码
// 在 WeChatApiService 中添加方法
public void sendTemplateMessage(String openId, String templateId, String url, Map<String, TemplateData> dataMap) {
    String accessToken = getAccessToken();
    if (accessToken == null) {
        return;
    }
    String sendUrl = String.format("https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=%s", accessToken);
    // 构造请求体
    Map<String, Object> requestBody = new HashMap<>();
    requestBody.put("touser", openId);
    requestBody.put("template_id", templateId);
    requestBody.put("url", url); // 点击模板消息后跳转的链接
    requestBody.put("data", dataMap);
    RestTemplate restTemplate = new RestTemplate();
    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.APPLICATION_JSON);
    HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers);
    String response = restTemplate.postForObject(sendUrl, entity, String.class);
    System.out.println("发送模板消息响应: " + response);
    // 处理响应...
}

TemplateData

@Data
public class TemplateData {
    private String value;
    private String color;
}

调用示例

// 在某个业务逻辑中调用
Map<String, TemplateData> data = new HashMap<>();
data.put("first", new TemplateData("您的订单已支付成功!", "#173177"));
data.put("keyword1", new TemplateData("202510271234", "#173177"));
data.put("keyword2", new TemplateData("¥99.00", "#173177"));
data.put("remark", new TemplateData("感谢您的惠顾!", "#173177"));
weChatApiService.sendTemplateMessage("用户的OpenID", "模板ID", "https://www.your-site.com/order", data);

微信支付

微信支付是一个相对复杂的功能,涉及流程如下:

  1. 配置支付目录:在公众号后台「微信支付」->「开发配置」中,配置你的支付授权目录。
  2. 获取OpenID:用户必须在公众号内完成OAuth2.0授权,才能获取到用户的OpenID,这通常通过引导用户点击一个链接完成。
  3. 统一下单:你的服务器调用微信支付的「统一下单」API (https://api.mch.weixin.qq.com/pay/unifiedorder),生成预支付交易会话标识 (prepay_id)。
  4. 生成支付参数:你的服务器使用 prepay_id、AppID、时间戳、随机数等再次签名,生成JSAPI支付所需参数。
  5. 前端调起支付:将生成的参数传递给前端(网页或小程序),前端调用微信JS-SDK的 chooseWXPay 方法调起支付收银台。

由于微信支付涉及签名、证书、回调处理等多个复杂环节,强烈建议使用成熟的第三方SDK,如 Weixin-Java-Tools,它极大地简化了支付流程。


总结与建议

  • 官方文档是第一手资料:遇到任何问题,首先查阅 微信公众平台官方文档
  • 善用工具:使用Postman等工具来测试API,比直接在代码中调试更方便。
  • 安全性:注意保护你的AppSecret,对API调用进行签名验证,防止恶意请求。
  • 异步处理:对于耗时操作(如调用某些API),考虑使用消息队列(如RabbitMQ, Kafka)进行异步处理,避免用户等待超时。
  • 使用成熟框架:对于支付等复杂功能,不要重复造轮子,使用像WxJava这样经过大量项目验证的框架可以节省你大量的时间和精力。

这份教程为你提供了一个完整的开发路径,从最基础的接入,到消息处理,再到API调用,最后是高级功能,希望它能帮助你顺利开展微信公众平台的后端开发工作。