文章简介 本文总结了本人在开发过程中遇到的有关于微信开发的诸多常见功能,这些问题在网上找都是零散的回答,所以再此总结一下,方便后续开发。如果有错误之处,还望批评指出,下面我们开始吧。
主要包括以下教程:
创建代码生成器并集成 Swagger
微信接口配置,响应微信发送的Token验证
获取Token时自动更新过期时间并更新Token
Java处理微信普通消息和事件消息并相应
Java发送模板消息
点击模板消息携带参数跳转详情
也可以查看项目源码地址:微信模板消息的发送和跳转: 使用Java发送微信模板消息,并点击模板消息跳转到详情 (gitee.com)
前期准备 准备数据库 新建token表
字段
类型
注释
id
int
主键
token
varchar
tokne
expires
varchar
过期时间
建表语句
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 SET NAMES utf8mb4;SET FOREIGN_KEY_CHECKS = 0 ;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 ; 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文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 <?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 > <dependency > <groupId > com.baomidou</groupId > <artifactId > mybatis-plus-boot-starter</artifactId > <version > 3.0.5</version > </dependency > <dependency > <groupId > org.apache.velocity</groupId > <artifactId > velocity-engine-core</artifactId > <version > 2.0</version > </dependency > <dependency > <groupId > junit</groupId > <artifactId > junit</artifactId > </dependency > <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 > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > </dependency > <dependency > <groupId > io.springfox</groupId > <artifactId > springfox-swagger2</artifactId > <version > 2.9.2</version > </dependency > <dependency > <groupId > io.springfox</groupId > <artifactId > springfox-swagger-ui</artifactId > <version > 2.9.2</version > </dependency > <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
1 2 3 4 5 6 7 8 9 10 11 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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 min-idle: 2 max-active: 20 test-on-borrow: true validation-query: select 1 from dual wx: appid: wx2188729b190d357d secret: d976b0e6262b829ba003e9a24032447c template_id: 1B1nMIck2SmkVJOHo_3VVQbyVPVlMItK9al46qsLjE0 check_token: fawu123456
创建配置类 新建 config,用于放我们项目的配置文件
目录结构如下
ApplicationConfig 这个配置主要用于配置 ComponentScan 和 MapperScan
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package com.szx.java.config;import org.mybatis.spring.annotation.MapperScan;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;@Configuration @ComponentScan(basePackages = "com.szx") @MapperScan(basePackages = "com.szx") public class ApplicationConfig {}
SwaggerConfig 这个配置用来配置 Swagger
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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;@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地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 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;@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
注意:生成的代码位置修改成你自己的项目地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 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;public class CodeGenerator { @Test public void run () { AutoGenerator mpg = new AutoGenerator(); 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" ); gc.setIdType(IdType.ID_WORKER); gc.setDateType(DateType.ONLY_DATE); gc.setSwagger2(true ); mpg.setGlobalConfig(gc); 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); PackageConfig pc = new PackageConfig(); pc.setParent("com.szx" ); pc.setModuleName("java" ); pc.setController("controller" ); pc.setEntity("entity" ); pc.setService("service" ); pc.setMapper("mapper" ); mpg.setPackageInfo(pc); 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 ); strategy.setRestControllerStyle(true ); strategy.setControllerMappingHyphenStyle(true ); mpg.setStrategy(strategy); mpg.execute(); } }
地址说明
运行上面的代码即可自动根据数据库的字段,生成响应的代码
响应微信发送的Token验证 接入微信平台文档:接入概述 | 微信开放文档 (qq.com)
通过查看官方文档,我们可以看到下面这描述
所以我们要首先要编写一个get接口,来响应微信的请求,并且正确返回
在 WTokenController
中添加代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @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
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @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
,内容如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 package com.szx.java.utils;import com.szx.java.constants.WxConstants;import java.security.MessageDigest;import java.security.NoSuchAlgorithmException;import java.util.Arrays;public class SignUtil { private static String token = WxConstants.WX_CHECK_TOKEN; public static boolean checkSignature (String signature, String timestamp, String 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" ); byte [] digest = md.digest(content.toString().getBytes()); ciphertext = byteToStr(digest); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } return ciphertext != null ? ciphertext.equals(signature.toUpperCase()) : false ; } private static String byteToStr (byte [] byteArray) { String strDigest = "" ; for (int i = 0 ; i < byteArray.length; i++) { strDigest += byteToHexStr(byteArray[i]); } return strDigest; } 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,官网在这 ,可以自行研究。下面是我穿透后的公网地址
然后打开微信测试平台,复制这个公网地址 + 接口名称到下面输入框
点击提交后,如果一切正常,在会弹出配置成功的提醒
到此我们接入微信的工作就完成了,下面我们来学习如何发送模板消息
封装公共响应类 首先准备一个状态码的枚举类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package com.szx.java.utils;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; } }
然后是响应实体类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 package com.szx.java.utils;import com.fasterxml.jackson.annotation.JsonInclude;@JsonInclude(JsonInclude.Include.NON_NULL) public class Response <T > { private String code; private String msg; private T data; public static <T> Response<T> success () { return rspMsg(ResponseEnum.SUCCESS); } public static <T> Response<T> error () { return rspMsg(ResponseEnum.SERVER_INNER_ERR); } public static <T> Response<T> rspMsg (ResponseEnum responseEnum) { Response<T> message = new Response<T>(); message.setCode(responseEnum.getCode()); message.setMsg(responseEnum.getMsg()); return message; } public static <T> Response<T> rspMsg (String code , String msg) { Response<T> message = new Response<T>(); message.setCode(code); message.setMsg(msg); return message; } public static <T> Response<T> rspData (T data) { Response<T> responseData = new Response<T>(); responseData.setCode(ResponseEnum.SUCCESS.getCode()); responseData.setData(data); return responseData; } 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方法
1 2 3 4 5 @GetMapping("getAccessToken") public Response<String> gotAccessToken () { String accessToken = tokenService.getAccessToken(); return Response.rspData(accessToken); }
在 tokenService 中实现 getAccessToken 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 @Override public String getAccessToken () { WToken wToken = this .getById(1 ); if (DateTimeUtils.CompareTime(wToken.getExpires()) < 0 ){ return wToken.getToken(); }else { String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&" + "appid=" + WxConstants.WX_APPID + "&secret=" + WxConstants.WX_SECRET; String result_str = HttpUtil.get(url); JSONObject result_json = JSONUtil.parseObj(result_str); String access_token = result_json.get("access_token" ).toString(); wToken.setToken(access_token); wToken.setExpires(DateTimeUtils.FutureTime()); this .updateById(wToken); return wToken.getToken(); } }
这里面用到的 DateTimeUtils 工具类代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 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;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
1 2 3 4 5 6 7 8 9 10 11 12 <dependency > <groupId > xmlpull</groupId > <artifactId > xmlpull</artifactId > <version > 1.1.3.1</version > </dependency > <dependency > <groupId > dom4j</groupId > <artifactId > dom4j</artifactId > <version > 1.6</version > </dependency >
添加一个工具类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 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;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 = "]]>" ; 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(); } public static Map<String, String> parseXml (HttpServletRequest request) throws Exception { Map<String, String> map = new HashMap<String, String>(); InputStream inputStream = request.getInputStream(); SAXReader reader = new SAXReader(); Document document = reader.read(inputStream); 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
1 2 3 4 5 @ApiOperation("相应微信消息") @PostMapping public String postWeChar (HttpServletRequest request, HttpServletResponse response) { return tokenService.postWeChar(request,response); }
实现 postWeChar 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 package com.szx.java.utils;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" ; public static final String EVENT_TYPE_SUBSCRIBE = "subscribe" ; public static final String EVENT_TYPE_UNSUBSCRIBE = "unsubscribe" ; public static final String EVENT_TYPE_SCAN = "scan" ; public static final String EVENT_TYPE_LOCATION = "location" ; 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 处理微信推送过来的消息和事件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 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;@Service public class WeCharServiceImpl { 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" ); 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)) { 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" ); 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)) { 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
1 2 @Autowired WeCharServiceImpl weCharService;
调用这两个方法来响应
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 @Override public String postWeChar (HttpServletRequest request, HttpServletResponse response) { try { Map<String, String> xmlMap = XmlUtil.parseXml(request); String eventType = xmlMap.get("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来定义模板消息的字段
1 2 3 4 5 {{first.DATA}} 客户姓名:{{keyword1.DATA}} 联系电话:{{keyword2.DATA}} 业务类型:{{keyword3.DATA}} {{remark.DATA}}
根据文档,我们要发送一个post请求,并且传递json数据,我们根据json数据创建一个对应的实体类,方便设置参数
添加 WxTemplateVo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 package com.szx.java.entity.Vo;import lombok.Data;import java.util.TreeMap;@Data public class WxTemplateVo { private String touser; private String template_id; private String url; private TreeMap<String, TreeMap<String, String>> data; 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; } }
实现发送模板消息的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 @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" ; 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以及跳转的地址,但是我们这里并没有真实的业务场景,所以都是写死的变量
我们写一个接口,来手动触发一下发送模板消息
1 2 3 4 5 @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代码,吧下面的代码添加到代码中即可实现微信登录逻辑
结合自己的业务,稍微改动一下即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 import { getTokenByCode, getUserinfoByToken } from '../api/index.js' import { userInfo } from '../store/userInfo.js' const appid = import .meta.env.VITE_APPIDconst secret = import .meta.env.VITE_SECRETexport 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' } 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 ) } export function getTokenFormCode (code ) { getTokenByCode(appid, secret, code).then((res ) => { const { access_token, openid } = res.data.info getUserinfoByToken(access_token, openid).then((info ) => { if (info.code === 500 ) { jumpAuthPage() } else { localStorage .removeItem('localUrl' ) userInfo().setUserInfo(info.data.info, info.data) } }) }) } export default function ( ) { let code = getUrlCode('code' ) let scope = getUrlCode('scope' ) if (code) { getTokenFormCode(code) } else { if (!scope && !localStorage .getItem('localUrl' )) { localStorage .setItem('localUrl' , window .location.href) } jumpAuthPage() } }
然后在 main.js 中引入即可
1 2 3 import wxauth from '@/utils/wxauth.js' app.use(wxauth)
上面代码中用到了两个方法
getTokenByCode
getUserinfoByToken
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @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" ; JSONObject jsonObject = HttpUtils.DO_GET(url); return Msg.Ok().data("info" , jsonObject); }
/edu/wx/wxUserinfo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 @ApiOperation("获取用户基本信息") @GetMapping("wxUserinfo") public Msg getUserInfo (@RequestParam String token, @RequestParam String 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); 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()); }