Java进行公众号开发的常见使用场景解决方案
文章简介
本文总结了本人在开发过程中遇到的有关于微信开发的诸多常见功能,这些问题在网上找都是零散的回答,所以再此总结一下,方便后续开发。如果有错误之处,还望批评指出,下面我们开始吧。
主要包括以下教程:
- 创建代码生成器并集成 Swagger
- 微信接口配置,响应微信发送的Token验证
- 获取Token时自动更新过期时间并更新Token
- Java处理微信普通消息和事件消息并相应
- Java发送模板消息
- H5接入微信授权登录
也可以查看项目源码地址:微信模板消息的发送和跳转: 使用Java发送微信模板消息,并点击模板消息跳转到详情 (gitee.com)
前期准备
准备数据库
新建token表
字段 | 类型 | 注释 |
---|---|---|
id | int | 主键 |
token | varchar | tokne |
expires | varchar | 过期时间 |
建表语句
/*
Navicat Premium Data Transfer
Source Server : localhost
Source Server Type : MySQL
Source Server Version : 80019
Source Host : localhost:3306
Source Schema : wxtemplate
Target Server Type : MySQL
Target Server Version : 80019
File Encoding : 65001
Date: 30/05/2023 17:47:36
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for w_token
-- ----------------------------
DROP TABLE IF EXISTS `w_token`;
CREATE TABLE `w_token` (
`id` int(0) NOT NULL AUTO_INCREMENT,
`token` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 'toekn',
`expires` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '过期时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of w_token
-- ----------------------------
INSERT INTO `w_token` VALUES (1, 'initvalue', '2023-01-01 00:00:01');
INSERT INTO `w_token` VALUES (2, 'initvalue', '2023-01-01 00:00:01');
SET FOREIGN_KEY_CHECKS = 1;
这里先默认添加两条数据,后面在更新和获取token时将固定查询id等于1的数据
添加依赖
下面是我的pom文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>java-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<artifactId>spring-boot-starter-parent</artifactId>
<groupId>org.springframework.boot</groupId>
<version>2.3.10.RELEASE</version>
</parent>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>
<!-- velocity 模板引擎, Mybatis Plus 代码生成器需要 -->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.0</version>
</dependency>
<!--junit-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
<!--druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.5</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.22</version>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--swagger-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<!--swagger ui-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<!--java-jwt-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.8.2</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.5.9</version>
</dependency>
</dependencies>
</project>
编辑配置文件
application.yml
server:
port: 8003
spring:
application:
name: service-edu
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
profiles:
active: dev
application-dev.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/wxtemplate?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
username: root
password: abc123
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
druid:
initial-size: 5 # 初始化连接池中连接数量为5
min-idle: 2 # 最小空闲连接数为2,即连接池中最少保持2个空闲连接不被释放
max-active: 20 # 最大活跃连接数为20,即连接池中最多同时存在20个活跃连接
test-on-borrow: true # 每次获取连接时是否进行连接测试,默认为true。如果设置为true,则在从连接池中获取连接时,先执行validation-query配置的测试SQL语句,判断连接是否可用,若不可用则重新创建连接。
validation-query: select 1 from dual # 连接测试SQL语句,用于检测连接是否可用,在上述的test-on-borrow为true时生效。这里的SQL语句是SELECT 1 FROM DUAL,DUAL表是Oracle数据库中自带的一个虚拟表名,该语句的作用是返回一个固定值1,以此来测试连接是否正常。
wx:
appid: wx2188729b190d357d #微信公众号appid
secret: d976b0e6262b829ba003e9a24032447c #微信公众号AppSecret
template_id: 1B1nMIck2SmkVJOHo_3VVQbyVPVlMItK9al46qsLjE0 # 跟进提醒
check_token: fawu123456 # 响应微信请求用到的token
创建配置类
新建 config,用于放我们项目的配置文件
目录结构如下
ApplicationConfig
这个配置主要用于配置 ComponentScan 和 MapperScan
package com.szx.java.config;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
/**
* @author songzx
* @create 2023-05-30 16:03
*/
@Configuration
@ComponentScan(basePackages = "com.szx")
@MapperScan(basePackages = "com.szx")
public class ApplicationConfig {
}
SwaggerConfig
这个配置用来配置 Swagger
package com.szx.java.config;
import com.google.common.base.Predicates;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
/**
* @author songzx
* @create 2022-09-22 11:21
*/
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket webApiConfig(){
return new Docket(DocumentationType.SWAGGER_2)
.groupName("webApi")
.apiInfo(webApiInfo())
.select()
.paths(Predicates.not(PathSelectors.regex("/admin/.*")))
.paths(Predicates.not(PathSelectors.regex("/error.*")))
.build();
}
public ApiInfo webApiInfo(){
return new ApiInfoBuilder()
.title("Api文档")
.description("文本档描述了定义的接口")
.version("1.0")
.contact(new Contact("szx", "https://blog.csdn.net/SongZhengxing_?spm=1010.2135.3001.5343","2501954467@qq.com"))
.build();
}
}
修改启动类
默认情况下,启动成功后不能直观的告诉我们项目的运行地址,通过以下配置,可以直观的看出运行成功后的接口地址和swagger地址
package com.szx.java;
import lombok.SneakyThrows;
import lombok.extern.log4j.Log4j2;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.Environment;
import java.net.InetAddress;
/**
* @author songzx
* @create 2023-05-12 14:41
*/
@Log4j2
@SpringBootApplication
public class SzxApplication {
@SneakyThrows
public static void main(String[] args) {
ConfigurableApplicationContext application = SpringApplication.run(SzxApplication.class, args);
Environment env = application.getEnvironment();
String host = InetAddress.getLocalHost().getHostAddress();
String port = env.getProperty("server.port");
log.info("\n ----------------------------------------------------------\n\t" +
"Application '{}' 正在运行中... Access URLs:\n\t" +
"Local: \t\thttp://localhost:{}\n\t" +
"External: \thttp://{}:{}\n\t" +
"Doc: \thttp://{}:{}/doc.html\n\t" +
"SwaggerDoc: \thttp://{}:{}/swagger-ui.html\n\t" +
"----------------------------------------------------------",
env.getProperty("spring.application.name"),
env.getProperty("server.port"),
host, port,
host, port,
host, port);
}
}
启动效果:
添加代码生成器
直接复制下面的代码,到你的测试文件中,可以自动生成 controller、entity、mapper、service
注意:生成的代码位置修改成你自己的项目地址
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
import com.baomidou.mybatisplus.generator.config.GlobalConfig;
import com.baomidou.mybatisplus.generator.config.PackageConfig;
import com.baomidou.mybatisplus.generator.config.StrategyConfig;
import com.baomidou.mybatisplus.generator.config.rules.DateType;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import org.junit.Test;
/**
* @author
* @since 2018/12/13
*/
public class CodeGenerator {
@Test
public void run() {
// 1、创建代码生成器
AutoGenerator mpg = new AutoGenerator();
// 2、全局配置
GlobalConfig gc = new GlobalConfig();
String projectPath = System.getProperty("user.dir");
gc.setOutputDir("D:\\mygitee\\项目大全\\Java玩转微信模板消息\\wxtemplatemsg\\java-demo\\src\\main\\java");
gc.setAuthor("szx");
gc.setOpen(false); //生成后是否打开资源管理器
gc.setFileOverride(false); //重新生成时文件是否覆盖
gc.setServiceName("%sService"); //去掉Service接口的首字母I
gc.setIdType(IdType.ID_WORKER); //主键策略
gc.setDateType(DateType.ONLY_DATE);//定义生成的实体类中日期类型
gc.setSwagger2(true);//开启Swagger2模式
mpg.setGlobalConfig(gc);
// 3、数据源配置
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl("jdbc:mysql://127.0.0.1:3306/wxtemplate?serverTimezone=GMT%2B8&useUnicode=yes&characterEncoding=utf8");
dsc.setDriverName("com.mysql.cj.jdbc.Driver");
dsc.setUsername("root");
dsc.setPassword("abc123");
dsc.setDbType(DbType.MYSQL);
mpg.setDataSource(dsc);
// 4、包配置
PackageConfig pc = new PackageConfig();
pc.setParent("com.szx"); // 主包名称
pc.setModuleName("java"); //模块名,生成的结构为:com.szx.edu
pc.setController("controller");
pc.setEntity("entity");
pc.setService("service");
pc.setMapper("mapper");
mpg.setPackageInfo(pc);
// 5、策略配置
StrategyConfig strategy = new StrategyConfig();
strategy.setInclude("w_token"); // 数据库表名
strategy.setNaming(NamingStrategy.underline_to_camel);//数据库表映射到实体的命名策略
strategy.setTablePrefix(pc.getModuleName() + "_"); //生成实体时去掉表前缀
strategy.setColumnNaming(NamingStrategy.underline_to_camel);//数据库表字段映射到实体的命名策略
strategy.setEntityLombokModel(true); // lombok 模型 @Accessors(chain = true) setter链式操作
strategy.setRestControllerStyle(true); //restful api风格控制器
strategy.setControllerMappingHyphenStyle(true); //url中驼峰转连字符
mpg.setStrategy(strategy);
// 6、执行
mpg.execute();
}
}
地址说明
运行上面的代码即可自动根据数据库的字段,生成响应的代码
响应微信发送的Token验证
接入微信平台文档:接入概述 | 微信开放文档 (qq.com)
通过查看官方文档,我们可以看到下面这描述
所以我们要首先要编写一个get接口,来响应微信的请求,并且正确返回
在 WTokenController
中添加代码
@Api(tags = "token管理")
@RestController
@RequestMapping("/wtoken")
public class WTokenController {
@Autowired
WTokenService tokenService;
@ApiOperation("微信接口配置,响应微信发送的Token验证")
@GetMapping
public String checkToken(HttpServletRequest request, HttpServletResponse response){
return tokenService.checkToken(request,response);
}
}
到 WTokenServiceImpl
中实现 checkToken
方法
/**
* 微信接口配置,响应微信发送的Token验证
*/
@Override
public String checkToken(HttpServletRequest request, HttpServletResponse response) {
if (StringUtils.isNotBlank(request.getParameter("signature"))) {
String signature = request.getParameter("signature");
String timestamp = request.getParameter("timestamp");
String nonce = request.getParameter("nonce");
String echostr = request.getParameter("echostr");
if (SignUtil.checkSignature(signature, timestamp, nonce)) {
return echostr;
}
}
return "";
}
这里用到了一个 SignUtil
签名工具类,所以新建 utils/SignUtil.java
,内容如下
package com.szx.java.utils;
import com.szx.java.constants.WxConstants;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
/**
* @author songzx
* @create 2022-11-22 13:52
*/
public class SignUtil {
// 与开发模式接口配置信息中的Token保持一致.
private static String token = WxConstants.WX_CHECK_TOKEN;
/**
* 校验签名
* @param signature 微信加密签名.
* @param timestamp 时间戳.
* @param nonce 随机数.
* @return
*/
public static boolean checkSignature(String signature, String timestamp, String nonce) {
// 对token、timestamp、和nonce按字典排序.
String[] paramArr = new String[] {token, timestamp, nonce};
Arrays.sort(paramArr);
// 将排序后的结果拼接成一个字符串.
String content = paramArr[0].concat(paramArr[1]).concat(paramArr[2]);
String ciphertext = null;
try {
MessageDigest md = MessageDigest.getInstance("SHA-1");
// 对拼接后的字符串进行sha1加密.
byte[] digest = md.digest(content.toString().getBytes());
ciphertext = byteToStr(digest);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
// 将sha1加密后的字符串与signature进行对比.
return ciphertext != null ? ciphertext.equals(signature.toUpperCase()) : false;
}
/**
* 将字节数组转换为十六进制字符串.
* @param byteArray
* @return
*/
private static String byteToStr(byte[] byteArray) {
String strDigest = "";
for (int i = 0; i < byteArray.length; i++) {
strDigest += byteToHexStr(byteArray[i]);
}
return strDigest;
}
/**
* 将字节转换为十六进制字符串.
* @param mByte
* @return
*/
private static String byteToHexStr(byte mByte) {
char[] Digit = { '0', '1' , '2', '3', '4' , '5', '6', '7' , '8', '9', 'A' , 'B', 'C', 'D' , 'E', 'F'};
char[] tempArr = new char[2];
tempArr[0] = Digit[(mByte >>> 4) & 0X0F];
tempArr[1] = Digit[mByte & 0X0F];
String s = new String(tempArr);
return s;
}
}
重启项目,打开 swagger-ui ,查看我们写好的接口
这里由于是本地开发,项目只能通过本机访问。但是微信是访问不到我们本机的,所以我们可以通过内网穿透工具,将本地IP地址映射到公网上访问
这里我使用的是 natapp,官网在这,可以自行研究。下面是我穿透后的公网地址
然后打开微信测试平台,复制这个公网地址 + 接口名称到下面输入框
点击提交后,如果一切正常,在会弹出配置成功的提醒
到此我们接入微信的工作就完成了,下面我们来学习如何发送模板消息
封装公共响应类
首先准备一个状态码的枚举类
package com.szx.java.utils;
/**
* @author songzx
* @date 2023/6/4
* @apiNote
*/
public enum ResponseEnum {
// 可以根据自己的实际需要增加状态码
SUCCESS("0", "ok"),
SERVER_INNER_ERR("500","系统繁忙"),
PARAM_LACK("100" , "非法参数"),
OPERATION_FAILED("101" ,"操作失败");
private String code;
private String msg;
ResponseEnum(String code, String msg) {
this.code = code;
this.msg = msg;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
然后是响应实体类
package com.szx.java.utils;
import com.fasterxml.jackson.annotation.JsonInclude;
/**
* @author songzx
* @date 2023/6/4
* @apiNote
*/
@JsonInclude(JsonInclude.Include.NON_NULL) // 值等于null的属性不返回
public class Response<T> {
private String code;
private String msg;
private T data;
/**
* @title 成功消息
* @return
*/
public static <T> Response<T> success() {
return rspMsg(ResponseEnum.SUCCESS);
}
/**
* @title 失败消息
* @return
*/
public static <T> Response<T> error() {
return rspMsg(ResponseEnum.SERVER_INNER_ERR);
}
/**
* @title 自定义消息
* @return
*/
public static <T> Response<T> rspMsg(ResponseEnum responseEnum) {
Response<T> message = new Response<T>();
message.setCode(responseEnum.getCode());
message.setMsg(responseEnum.getMsg());
return message;
}
/**
* @title 自定义消息
* @return
*/
public static <T> Response<T> rspMsg(String code , String msg) {
Response<T> message = new Response<T>();
message.setCode(code);
message.setMsg(msg);
return message;
}
/**
* @title 返回数据
* @param data
* @return
*/
public static <T> Response<T> rspData(T data) {
Response<T> responseData = new Response<T>();
responseData.setCode(ResponseEnum.SUCCESS.getCode());
responseData.setData(data);
return responseData;
}
/**
* @title 返回数据-自定义code
* @param data
* @return
*/
public static <T> Response<T> rspData(String code , T data) {
Response<T> responseData = new Response<T>();
responseData.setCode(code);
responseData.setData(data);
return responseData;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
获取Access token
微信开放文档 (qq.com)
access_token是公众号的全局唯一接口调用凭据,公众号调用各接口时都需使用access_token。开发者需要进行妥善保存。access_token的存储至少要保留512个字符空间。access_token的有效期目前为2个小时,需定时刷新,重复获取将导致上次获取的access_token失效。
添加get方法
@GetMapping("getAccessToken")
public Response<String> gotAccessToken(){
String accessToken = tokenService.getAccessToken();
return Response.rspData(accessToken);
}
在 tokenService 中实现 getAccessToken 方法
/**
* 获取AccessToken
* @return
*/
@Override
public String getAccessToken() {
// 固定查询id等于1的数据
WToken wToken = this.getById(1);
// 判断当前的accessToken是否在有效期内,小于0表示在有效期内
if(DateTimeUtils.CompareTime(wToken.getExpires()) < 0){
return wToken.getToken();
}else{
// 不再有效期内时调用接口获取新的token
String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&" +
"appid=" + WxConstants.WX_APPID +
"&secret=" + WxConstants.WX_SECRET;
// 发送请求,获取json格式字符串
String result_str = HttpUtil.get(url);
// 将json格式的字符串转成JSONObject类型,方便获取里面的数据
JSONObject result_json = JSONUtil.parseObj(result_str);
// 从json中读取access_token字段,并转成string
String access_token = result_json.get("access_token").toString();
// 更新token
wToken.setToken(access_token);
// 更新过期时间
wToken.setExpires(DateTimeUtils.FutureTime());
// 更新数据库中的值
this.updateById(wToken);
// 返回最新的token
return wToken.getToken();
}
}
这里面用到的 DateTimeUtils 工具类代码
package com.szx.java.utils;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Date;
/**
* @author songzx
* @date 2023/6/4
* @apiNote
*/
public class DateTimeUtils {
/**
* 拿当前时间和传递过来时间做比较,如果当前时间小于传递进来的时间,则返回负数,否则返回正数
*/
public static int CompareTime(String time){
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime dateTime = LocalDateTime.parse(time, formatter);
Instant instant = dateTime.atZone(ZoneId.systemDefault()).toInstant();
long timestamp = instant.toEpochMilli();
long currentTimestamp = System.currentTimeMillis();
return Long.compare(currentTimestamp, timestamp);
}
/**
* 更新过期时间
*/
public static String FutureTime(){
int seconds = 7000;
LocalDateTime dateTime = LocalDateTime.now().plusSeconds(seconds);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
return dateTime.format(formatter);
}
}
响应微信请求
文本消息 | 微信开放文档 (qq.com)
当普通微信用户向公众账号发消息时,微信服务器将POST消息的XML数据包到开发者填写的URL上。
导入依赖处理xml
<!--处理xml-->
<dependency>
<groupId>xmlpull</groupId>
<artifactId>xmlpull</artifactId>
<version>1.1.3.1</version>
</dependency>
<!--XML解析器-->
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6</version>
</dependency>
添加一个工具类
package com.szx.java.utils;
import cn.hutool.core.collection.CollectionUtil;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import javax.servlet.http.HttpServletRequest;
import java.io.InputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
/**
* 封装和处理xml文件
* @author Administrator
*
*/
public class XmlUtil {
private static final String PREFIX_XML = "<xml>";
private static final String SUFFIX_XML = "</xml>";
private static final String PREFIX_CDATA = "<![CDATA[";
private static final String SUFFIX_CDATA = "]]>";
/**
* 转化成xml, 单层无嵌套
*/
public static String xmlFormat(Map<String, String> parm, boolean isAddCDATA) {
StringBuffer strbuff = new StringBuffer(PREFIX_XML);
if (CollectionUtil.isNotEmpty(parm)) {
for (Entry<String, String> entry : parm.entrySet()) {
strbuff.append("<").append(entry.getKey()).append(">");
if (isAddCDATA) {
strbuff.append(PREFIX_CDATA);
if (StringUtils.isNotEmpty(entry.getValue())) {
strbuff.append(entry.getValue());
}
strbuff.append(SUFFIX_CDATA);
} else {
if (StringUtils.isNotEmpty(entry.getValue())) {
strbuff.append(entry.getValue());
}
}
strbuff.append("</").append(entry.getKey()).append(">");
}
}
return strbuff.append(SUFFIX_XML).toString();
}
/**
* 解析微信发来的请求(XML)
*
* @param request
* @return
* @throws Exception
*/
public static Map<String, String> parseXml(HttpServletRequest request) throws Exception {
// 将解析结果存储在HashMap中
Map<String, String> map = new HashMap<String, String>();
// 从request中取得输入流
InputStream inputStream = request.getInputStream();
// 读取输入流
SAXReader reader = new SAXReader();
Document document = reader.read(inputStream);
// 得到xml根元素
Element root = document.getRootElement();
// 得到根元素的所有子节点
@SuppressWarnings("unchecked")
List<Element> elementList = root.elements();
// 遍历所有子节点
for (Element e : elementList) {
map.put(e.getName(), e.getText());
}
// 释放资源
inputStream.close();
inputStream = null;
return map;
}
}
添加POST方法用于响应微信,这个方法地址要和上面配置验证token地址一样,只不过请求类型变成post
@ApiOperation("相应微信消息")
@PostMapping
public String postWeChar(HttpServletRequest request, HttpServletResponse response){
return tokenService.postWeChar(request,response);
}
实现 postWeChar 方法
/**
* 相应微信请求
* @param request
* @param response
* @return
*/
@Override
public String postWeChar(HttpServletRequest request, HttpServletResponse response) {
try {
Map<String, String> xmlMap = XmlUtil.parseXml(request);
System.out.println(xmlMap);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
例如现在我们往公众号发送一个消息
可以看到有消息打印出来,我们可以根据 MsgType 来判断消息类型。获取用户openid等操作
我们可以根据消息类型,创建一个映射类 MessageType
package com.szx.java.utils;
/**
* @author songzx
* @date 2023/6/4
* @apiNote
*/
public class MessageType {
/*
* 文本消息
*/
public static final String TEXT_MESSAGE = "text";
/*
* 图片消息
*/
public static final String IMAGE_MESSAGE = "image";
/*
* 语音消息
*/
public static final String VOICE_MESSAGE = "voice";
/*
* 视频消息
*/
public static final String VIDEO_MESSAGE = "video";
/*
* 小视频消息消息
*/
public static final String SHORTVIDEO_MESSAGE = "shortvideo";
/*
* 地理位置消息
*/
public static final String POSOTION_MESSAGE = "location";
/*
* 链接消息
*/
public static final String LINK_MESSAGE = "link";
/*
* 音乐消息
*/
public static final String MUSIC_MESSAGE = "music";
/*
* 图文消息
*/
public static final String IMAGE_TEXT_MESSAGE = "news";
/*
* 请求消息类型:事件推送
*/
public static final String REQ_MESSAGE_TYPE_EVENT = "event";
/*
* 事件类型:subscribe(订阅)
*/
public static final String EVENT_TYPE_SUBSCRIBE = "subscribe";
/*
* 事件类型:unsubscribe(取消订阅)
*/
public static final String EVENT_TYPE_UNSUBSCRIBE = "unsubscribe";
/*
* 事件类型:scan(用户已关注时的扫描带参数二维码)
*/
public static final String EVENT_TYPE_SCAN = "scan";
/*
* 事件类型:LOCATION(上报地理位置)
*/
public static final String EVENT_TYPE_LOCATION = "location";
/*
* 事件类型:CLICK(自定义菜单)
*/
public static final String EVENT_TYPE_CLICK = "click";
/*
* 响应消息类型:文本
*/
public static final String RESP_MESSAGE_TYPE_TEXT = "text";
/*
* 响应消息类型:图片
*/
public static final String RESP_MESSAGE_TYPE_IMAGE = "image";
/*
* 响应消息类型:语音
*/
public static final String RESP_MESSAGE_TYPE_VOICE = "voice";
/*
* 响应消息类型:视频
*/
public static final String RESP_MESSAGE_TYPE_VIDEO = "video";
/*
* 响应消息类型:音乐
*/
public static final String RESP_MESSAGE_TYPE_MUSIC = "music";
/*
* 响应消息类型:图文
*/
public static final String RESP_MESSAGE_TYPE_NEWS = "news";
}
添加 WeCharServiceImpl 处理微信推送过来的消息和事件
package com.szx.java.service.impl;
import com.szx.java.utils.MessageType;
import com.szx.java.utils.XmlUtil;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* @author songzx
* @date 2023/6/4
* @apiNote
*/
@Service
public class WeCharServiceImpl {
// 处理微信发来的请求 map 消息业务处理分发
public String parseMessage(Map<String, String> map) {
String respXml = null;
try {
// 发送方帐号
String fromUserName = map.get("FromUserName");
// 开发者微信号
String toUserName = map.get("ToUserName");
// 取得消息类型
String MsgType = map.get("MsgType");
// 发现直接把要返回的信息直接封装成replyMap集合,然后转换成 xml文件,是不是实体类可以不用了
Map<String, String> replyMap = new HashMap<String, String>();
replyMap.put("ToUserName", fromUserName);
replyMap.put("FromUserName", toUserName);
replyMap.put("CreateTime", String.valueOf(new Date().getTime()));
if (MsgType.equals(MessageType.TEXT_MESSAGE)) {
// 用map集合封装
replyMap.put("MsgType", MessageType.RESP_MESSAGE_TYPE_TEXT);
replyMap.put("Content", "您发送的是文本消息");
respXml = XmlUtil.xmlFormat(replyMap, true);
} else if (MsgType.equals(MessageType.IMAGE_MESSAGE)) {
// 以下方式根据需要来操作
replyMap.put("MsgType", MessageType.RESP_MESSAGE_TYPE_TEXT);
replyMap.put("Content", "您发送的是图片消息");
respXml = XmlUtil.xmlFormat(replyMap, true);
} else if (MsgType.equals(MessageType.VOICE_MESSAGE)) {
// 以下方式根据需要来操作
replyMap.put("MsgType", MessageType.RESP_MESSAGE_TYPE_TEXT);
replyMap.put("Content", "您发送的是语音消息");
respXml = XmlUtil.xmlFormat(replyMap, true);
} else if (MsgType.equals(MessageType.VIDEO_MESSAGE)) {
replyMap.put("MsgType", MessageType.RESP_MESSAGE_TYPE_TEXT);
replyMap.put("Content", "您发送的是视频消息");
respXml = XmlUtil.xmlFormat(replyMap, true);
} else if (MsgType.equals(MessageType.SHORTVIDEO_MESSAGE)) {
replyMap.put("MsgType", MessageType.RESP_MESSAGE_TYPE_TEXT);
replyMap.put("Content", "您发送的是小视频消息");
respXml = XmlUtil.xmlFormat(replyMap, true);
} else if (MsgType.equals(MessageType.POSOTION_MESSAGE)) {
replyMap.put("MsgType", MessageType.RESP_MESSAGE_TYPE_TEXT);
replyMap.put("Content", "您发送的是地理位置消息");
respXml = XmlUtil.xmlFormat(replyMap, true);
} else if (MsgType.equals(MessageType.LINK_MESSAGE)) {
replyMap.put("MsgType", MessageType.RESP_MESSAGE_TYPE_TEXT);
replyMap.put("Content", "您发送的是链接消息");
respXml = XmlUtil.xmlFormat(replyMap, true);
}
} catch (Exception e) {
e.printStackTrace();
}
return respXml;
}
// 事件消息业务分发
public String parseEvent(Map<String, String> map) {
String respXml = null;
try {
// 发送方帐号
String fromUserName = map.get("FromUserName");
// 开发者微信号
String toUserName = map.get("ToUserName");
// 取得消息类型
String MsgType = map.get("MsgType");
//获取事件类型
String eventType = map.get("Event");
// 发现直接把要返回的信息直接封装成replyMap集合,然后转换成 xml文件,是不是实体类可以不用了
Map<String, String> replyMap = new HashMap<String, String>();
replyMap.put("ToUserName", fromUserName);
replyMap.put("FromUserName", toUserName);
replyMap.put("CreateTime", String.valueOf(new Date().getTime()));
if (eventType.equals(MessageType.EVENT_TYPE_SUBSCRIBE)) {// 关注
// 用map集合封装
replyMap.put("MsgType", MessageType.RESP_MESSAGE_TYPE_TEXT);
replyMap.put("Content", "欢迎关注");
respXml = XmlUtil.xmlFormat(replyMap, true);
}
if (eventType.equals(MessageType.EVENT_TYPE_UNSUBSCRIBE)) {// 取消关注
}
if (eventType.equals(MessageType.EVENT_TYPE_SCAN)) {// 用户已关注时的扫描带参数二维码
}
if (eventType.equals(MessageType.EVENT_TYPE_LOCATION)) {// 上报地理位置
}
if (eventType.equals(MessageType.EVENT_TYPE_CLICK)) {// 自定义菜单
}
} catch (Exception e) {
e.printStackTrace();
}
return respXml;
}
}
自动注入 weCharService
@Autowired
WeCharServiceImpl weCharService;
调用这两个方法来响应
/**
* 相应微信请求
* @param request
* @param response
* @return
*/
@Override
public String postWeChar(HttpServletRequest request, HttpServletResponse response) {
try {
// 解析request中的xml得到一个map
Map<String, String> xmlMap = XmlUtil.parseXml(request);
// 判断是否事件类型
String eventType = xmlMap.get("Event");
// 判断map中是否存在Event,由此判断这个事件是普通消息还是事件推送
if(StringUtils.isNotEmpty(eventType)){
// 事件推送
return weCharService.parseEvent(xmlMap);
}else{
// 普通消息
return weCharService.parseMessage(xmlMap);
}
} catch (Exception e) {
e.printStackTrace();
}
return "";
}
效果:
当我取消关注重新关注时也会发送一个新的消息,这里就会调用事件消息方法
发送模板消息
微信公众平台 (qq.com)-发送模板消息
发送模板消息前要先生成一个模板消息id
消息内容,xxxx.DATA,后面的 .DATA 是固定的
在测试环境下的消息模板是可以自定义的,但是到正式的公众号申请模板消息时,只能根据公众号的类目选择模板消息,并且模板消息不能自定义,并且字段都是keyword1、keyword2、…,所以这里建议在测试环境下我们也用keyword1、keyword2来定义模板消息的字段
{{first.DATA}}
客户姓名:{{keyword1.DATA}}
联系电话:{{keyword2.DATA}}
业务类型:{{keyword3.DATA}}
{{remark.DATA}}
根据文档,我们要发送一个post请求,并且传递json数据,我们根据json数据创建一个对应的实体类,方便设置参数
添加 WxTemplateVo
package com.szx.java.entity.Vo;
import lombok.Data;
import java.util.TreeMap;
/**
* @author songzx
* @create 2022-11-29 15:45
*/
@Data
public class WxTemplateVo {
/**
* 接收者openId
*/
private String touser;
/**
* 模板ID
*/
private String template_id;
/**
* 模板跳转链接
*/
private String url;
/**
* data数据
*/
private TreeMap<String, TreeMap<String, String>> data;
/**
* 参数
*
* @param value 值
* @param color 颜色 可不填
* @return params
*/
public static TreeMap<String, String> item(String value, String color) {
TreeMap<String, String> params = new TreeMap<String, String>();
params.put("value", value);
params.put("color", color);
return params;
}
}
实现发送模板消息的方法
/**
* 发送模板消息
*/
@Override
public void sendTemplateMsg() {
// 发送模板请求的地址
String postUrl = "https://api.weixin.qq.com/cgi-bin/message/template/send" +
"?access_token=" + getToken();
// 要给那个用户发送模板消息
String openId = "olttN6WJOYe-lTysV8_tsnZ7-HMQ";
// 模板消息ID
String templeID = "vRGjpYZ-uL3CCREW9c6Kl9csekoW9tVbVl7hf_y3k5U";
// 点击模板消息要跳转的地址,如果不设置则不会跳转
String templeUrl = "http://baidu.com";
// 构造模板消息内容
TreeMap<String, TreeMap<String, String>> params = new TreeMap<>();
params.put("keyword1", WxTemplateVo.item("第一行消息", "#409EFF"));
params.put("keyword2", WxTemplateVo.item("第二行消息", "#409EFF"));
params.put("keyword3", WxTemplateVo.item("第三行消息", "#409EFF"));
// 将模板消息放进实体类中
WxTemplateVo wxTemplateMsg = new WxTemplateVo();
wxTemplateMsg.setTemplate_id(templeID);
wxTemplateMsg.setTouser(openId);
wxTemplateMsg.setData(params);
wxTemplateMsg.setUrl(templeUrl);
// 请求请求
HttpUtil.post(postUrl, JSONUtil.toJsonStr(wxTemplateMsg));
}
在真实的业务场景中,应该是在完成某个业务时,由代码触发发送模板消息的方法,并且动态获取用户的openId和模板消息id以及跳转的地址,但是我们这里并没有真实的业务场景,所以都是写死的变量
我们写一个接口,来手动触发一下发送模板消息
@ApiOperation("发送模板消息")
@GetMapping("sendTemplateMsg")
public void sendTemplateMsg(){
tokenService.sendTemplateMsg();
}
点击发送按钮后,公众号会发送过来一个模板消息,效果如下
在测试环境下,我们设置的字体颜色并没有生效。到正式公众号下就会生效了
H5接入微信登录
详细步骤见我的另外一个文章:[公众号H5页面接入微信登录流程_公众号h5微信登录_szx的开发笔记的博客-CSDN博客](https://blog.csdn.net/SongZhengxing_/article/details/121036115?utm_source = uc_fansmsg)
这里放核心的js代码,吧下面的代码添加到代码中即可实现微信登录逻辑
结合自己的业务,稍微改动一下即可
// 这里是两个方法,一个
import { getTokenByCode, getUserinfoByToken } from '../api/index.js'
// 这里使用的是Vue3的Pinia,如果是Vue2可以换成Vuex
import { userInfo } from '../store/userInfo.js'
// 公众号的appid
const appid = import.meta.env.VITE_APPID
// 公众号的secret
const secret = import.meta.env.VITE_SECRET
// 获取当前页面地址作为回调地址,并且对地址进行urlEncode处理
export function jumpAuthPage() {
let url = localStorage.getItem('localUrl')
url = processUrl(url)
let local = encodeURIComponent(url)
// 跳转到授权页面
window.location.href =
'https://open.weixin.qq.com/connect/oauth2/authorize?appid=' +
appid +
'&redirect_uri=' +
local +
'&response_type=code&scope=snsapi_userinfo&state=1#wechat_redirect'
}
// 解析原始URL
function processUrl(url) {
if (url.indexOf('code') !== -1) {
let start = url.indexOf('?')
let end = url.indexOf('#')
return url.slice(0, start) + url.slice(end)
} else {
return url
}
}
// 获取路径上参数
export function getUrlCode(name) {
return (
decodeURIComponent(
(new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)').exec(
location.href
) || [, ''])[1].replace(/\+/g, '%20')
) || null
)
}
// 根据code获取网站授权token
export function getTokenFormCode(code) {
// 根据code获取到openid
getTokenByCode(appid, secret, code).then((res) => {
const { access_token, openid } = res.data.info
// 根据token获取用户基本信息
getUserinfoByToken(access_token, openid).then((info) => {
if (info.code === 500) {
// 如果根据token没有拿到用户信息则重新授权获取新的code重新获取用户信息
jumpAuthPage()
} else {
// 获取到用户信息后删除缓存的地址
localStorage.removeItem('localUrl')
// 保存用户基本信息到Pinia
userInfo().setUserInfo(info.data.info, info.data)
}
})
})
}
export default function () {
// 判断是否有code
let code = getUrlCode('code')
let scope = getUrlCode('scope')
// 当获取到code后再调用获取token的方法
if (code) {
getTokenFormCode(code)
} else {
// 这里使用缓存获取最初进来的页面地址,这样实现在授权完成后回调时还展示最开始的页面,实现从那个页面进来,还回调到那个页面
if (!scope && !localStorage.getItem('localUrl')) {
localStorage.setItem('localUrl', window.location.href)
}
jumpAuthPage()
}
}
然后在 main.js 中引入即可
import wxauth from '@/utils/wxauth.js'
app.use(wxauth)
上面代码中用到了两个方法
- getTokenByCode
- getUserinfoByToken
// 根据code获取网站授权token
export function getTokenByCode(appid, secret, code) {
return server({
method: 'get',
url: `/edu/wx/oauth2`,
params: {
appid,
secret,
code,
},
})
}
// 拉取用户信息
export function getUserinfoByToken(token, openid) {
return server({
method: 'get',
url: `/edu/wx/wxUserinfo`,
params: {
token,
openid,
},
})
}
分别对应Java代码如下
/edu/wx/oauth2
/**
* 根据code获取网站授权token
*/
@ApiOperation("根据code获取网站授权token")
@GetMapping("oauth2")
public Msg oauth(@RequestParam String appid,
@RequestParam String secret,
@RequestParam String code) {
// 构造请求地址
String url = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=" + appid +
"&secret=" + secret +
"&code=" + code +
"&grant_type=authorization_code";
// 发送get请求
JSONObject jsonObject = HttpUtils.DO_GET(url);
// 将请求信息返回给前端
return Msg.Ok().data("info", jsonObject);
}
/edu/wx/wxUserinfo
/**
* 获取用户基本信息
*/
@ApiOperation("获取用户基本信息")
@GetMapping("wxUserinfo")
public Msg getUserInfo(@RequestParam String token,
@RequestParam String openid) {
// 根据openid查询用户表,判断这个用户是否存在
LfUser fUser = lfUserService.zcUserByOpenid(openid);
// 构造请求地址
String url = "https://api.weixin.qq.com/sns/userinfo?access_token=" + token +
"&openid=" + openid +
"&lang=zh_CN";
// 发送请求
String s = HttpUtil.get(url);
// 将得到的结果字符串json转成JSONObject
cn.hutool.json.JSONObject jsonObject = JSONUtil.parseObj(s);
// 从结果中获取微信昵称
String nickname = jsonObject.get("nickname").toString();
// 从结果中获取微信头像
String headimgurl = jsonObject.get("headimgurl").toString();
// 如果该用户不曾存在,则注册用户,否则更新用户最新的微信昵称和头像
if (fUser == null) {
fUser = new LfUser();
fUser.setOpenid(openid);
fUser.setNickname(nickname);
fUser.setPhoto(headimgurl);
lfUserService.addLfUser(fUser);
} else {
fUser.setNickname(nickname);
fUser.setPhoto(headimgurl);
lfUserService.updateById(fUser);
}
// 将结果返回
return Msg.Ok()
.data("info", fUser)
.data("userId", fUser.getId())
.data("loginTime", fUser.getGmtModified());
}