Common usage scenarios and solutions for Java official account development

Common usage scenario solutions for Java official account development

Introduction to the article

This article summarizes many common functions related to WeChat development that I encountered during the development process. These questions are all scattered answers on the Internet, so I will summarize them here to facilitate subsequent development. If there are mistakes, I still hope to criticize and point out, let's start now.

It mainly includes the following tutorials:

  • Create a code generator and integrate Swagger
  • WeChat interface configuration, response Token verification sent by WeChat
  • Automatically update the expiration time and update the Token when obtaining the Token
  • Java handles WeChat ordinary messages and event messages and responds accordingly
  • Java send template message
  • H5 Access WeChat Authorization Login

You can also view the project source code address: Sending and redirecting WeChat template messages: Use Java to send WeChat template messages, and click the template message to jump to details (gitee.com)

Preparation

prepare database

Create a new token table

field type note
id int primary key
token varchar token
expires varchar Expiration

Create table statement

/*
 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;

Here, two pieces of data are added by default first, and then the data with id equal to 1 will be fixedly queried when updating and obtaining token

add dependencies

Below is my pom file

<?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>

edit configuration file

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

Create a configuration class

Create a new config to put the configuration file of our project

The directory structure is as follows

image-20230531085338122

ApplicationConfig

This configuration is mainly used to configure ComponentScan and 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

This configuration is used to configure 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","[email protected]"))
                .build();
    }
}

Modify the startup class

By default, we cannot intuitively tell us the running address of the project after the startup is successful. Through the following configuration, we can intuitively see the interface address and swagger address after the successful operation

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

Startup effect:

image-20230531085613569

Add code generator

Copy the following code directly to your test file, and you can automatically generate controller, entity, mapper, service

Note: Change the generated code location to your own project address

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

address description

image-20230531090028713

image-20230531090116022

image-20230531090303495

image-20230531090340263

Run the above code to automatically generate the response code according to the fields of the database

Respond to the Token verification sent by WeChat

Access to WeChat Platform Documentation: Access Overview | WeChat Open Documentation (qq.com)

By looking at the official documentation, we can see the following description

image-20230531090930529

So we must first write a get interface to respond to WeChat's request and return it correctly

WTokenControllerAdd code in

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

WTokenServiceImplto implement checkTokenthe method in

/**
 * 微信接口配置,响应微信发送的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 "";
}

SignUtilA signature tool class is used here , so create a new one utils/SignUtil.java, the content is as follows

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

Restart the project, open swagger-ui, and view the interface we wrote

image-20230531091538462

Since it is developed locally, the project can only be accessed locally. But WeChat cannot access our local machine, so we can use the intranet penetration tool to map the local IP address to public network access

Here I am using natapp, the official website is here , you can study it yourself. The following is the public network address after my penetration

image-20230531091933376

Then open the WeChat test platform, copy the public network address + interface name to the input box below

image-20230531092124294

After clicking Submit, if everything is normal, a reminder of successful configuration will pop up

image-20230531092134909

At this point, we have completed the work of accessing WeChat, let's learn how to send template messages

Encapsulate public response class

First prepare an enumeration class of status code

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

and then the response entity class

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

Get Access token

WeChat Open Documentation (qq.com)

The access_token is the globally unique interface call credential of the official account, and the access_token is required when the official account calls each interface. Developers need to properly save. The storage of access_token must reserve at least 512 characters. The validity period of access_token is currently 2 hours and needs to be refreshed regularly. Obtaining it repeatedly will invalidate the access_token obtained last time.

add get method

@GetMapping("getAccessToken")
public Response<String> gotAccessToken(){
    
    
    String accessToken = tokenService.getAccessToken();
    return Response.rspData(accessToken);
}

Implement the getAccessToken method in tokenService

/**
     * 获取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 tool class code used here

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

Respond to WeChat requests

Text message | WeChat open document (qq.com)

When an ordinary WeChat user sends a message to a public account, the WeChat server sends the XML data packet of the POST message to the URL filled in by the developer.

import dependency processing 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>

Add a tool class

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

Add a POST method to respond to WeChat. The address of this method should be the same as that of the verification token configured above, except that the request type is changed to post.

@ApiOperation("相应微信消息")
@PostMapping
public String postWeChar(HttpServletRequest request, HttpServletResponse response){
    
    
    return tokenService.postWeChar(request,response);
}

Implement the postWeChar method

/**
 * 相应微信请求
 * @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;
}

For example, now we send a message to the official account

image-20230604182124942

You can see that a message is printed out, and we can judge the message type according to MsgType. Obtain user openid and other operations

We can create a mapping class MessageType according to the message type

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";

}

Add WeCharServiceImpl to handle messages and events pushed by WeChat

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

Automatically inject weCharService

@Autowired
WeCharServiceImpl weCharService;

Call these two methods in response to

/**
 * 相应微信请求
 * @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 "";
}

Effect:

image-20230604190827452

When I unfollow and re-follow, a new message will also be sent, and the event message method will be called here

image-20230604191036499

send template message

WeChat public platform (qq.com) - send template message

Generate a template message id before sending the template message

image-20230604214518783

Message content, xxxx.DATA, the following .DATA is fixed

The message template in the test environment can be customized, but when applying for a template message on the official official account, the template message can only be selected according to the category of the official account, and the template message cannot be customized, and the fields are keyword1 and keyword2 , ..., so it is suggested that we also use keyword1 and keyword2 to define the fields of the template message in the test environment

{
   
   {first.DATA}} 
客户姓名:{
   
   {keyword1.DATA}} 
联系电话:{
   
   {keyword2.DATA}} 
业务类型:{
   
   {keyword3.DATA}} 
{
   
   {remark.DATA}}

According to the document, we want to send a post request and pass the json data, we create a corresponding entity class based on the json data, which is convenient for setting parameters

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

Implement method to send template message

/**
 * 发送模板消息
 */
@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));
}

In a real business scenario, when a certain business is completed, the code triggers the method of sending a template message, and dynamically obtains the user's openId, template message id, and jump address, but we do not have a real business scenario here , so they are hard-coded variables

Let's write an interface to manually trigger sending template messages

@ApiOperation("发送模板消息")
@GetMapping("sendTemplateMsg")
public void sendTemplateMsg(){
    
    
    tokenService.sendTemplateMsg();
}

After clicking the send button, the official account will send a template message, the effect is as follows

In the test environment, the font color we set did not take effect. It will take effect under the official public account

image-20230604220626246

H5 access WeChat login

For detailed steps, see my other article: [Public account H5 page access WeChat login process_Public account h5 WeChat login_szx’s blog of development notes-CSDN blog](https://blog.csdn.net/SongZhengxing_/article /details/121036115?utm_source = uc_fansmsg)

Put the core js code here, add the following code to the code to realize the WeChat login logic

According to your own business, you can make a slight change

// 这里是两个方法,一个
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()
  }
}

Then import it in main.js

import wxauth from '@/utils/wxauth.js'

app.use(wxauth)

There are two methods used in the above code

  • 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,
    },
  })
}

The corresponding Java codes are as follows

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

Guess you like

Origin blog.csdn.net/SongZhengxing_/article/details/131038356