Soluções de cenário de uso comum para desenvolvimento de conta oficial Java
Introdução ao artigo
Este artigo resume muitas funções comuns relacionadas ao desenvolvimento do WeChat que encontrei durante o processo de desenvolvimento.Essas perguntas são respostas dispersas na Internet, então vou resumi-las aqui para facilitar o desenvolvimento subsequente. Se houver erros, ainda espero criticar e apontar, vamos começar agora.
Inclui principalmente os seguintes tutoriais:
- Crie um gerador de código e integre o Swagger
- Configuração da interface WeChat, verificação de token de resposta enviada pelo WeChat
- Atualizar automaticamente o prazo de validade e atualizar o Token ao obter o Token
- Java lida com mensagens comuns e mensagens de eventos do WeChat e responde de acordo
- Java enviar mensagem de modelo
- Login de autorização do WeChat de acesso H5
Você também pode visualizar o endereço do código-fonte do projeto: Enviando e redirecionando mensagens do modelo WeChat: Use Java para enviar mensagens do modelo WeChat e clique na mensagem do modelo para ir para os detalhes (gitee.com)
Preparação
preparar banco de dados
Crie uma nova tabela de tokens
campo | tipo | observação |
---|---|---|
eu ia | interno | chave primária |
símbolo | varchar | símbolo |
expira | varchar | Expiração |
Criar declaração de tabela
/*
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;
Aqui, dois dados são adicionados primeiro por padrão e, em seguida, os dados com id igual a 1 serão consultados fixamente ao atualizar e obter o token
adicionar dependências
Abaixo está meu arquivo 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>
editar arquivo de configuração
aplicativo.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
aplicativo-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
Crie uma classe de configuração
Crie um novo config para colocar o arquivo de configuração do nosso projeto
A estrutura do diretório é a seguinte
Configuração do aplicativo
Esta configuração é usada principalmente para configurar ComponentScan e 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
Esta configuração é usada para configurar o 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();
}
}
Modifique a classe de inicialização
Por padrão, não podemos nos dizer intuitivamente o endereço em execução do projeto após a inicialização ser bem-sucedida. Através da configuração a seguir, podemos ver intuitivamente o endereço da interface e o endereço do swagger após a operação bem-sucedida
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);
}
}
Efeito de inicialização:
Adicionar gerador de código
Copie o código a seguir diretamente para seu arquivo de teste e você poderá gerar automaticamente controlador, entidade, mapeador, serviço
Nota: Altere o local do código gerado para o endereço do seu próprio projeto
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();
}
}
descrição de endereço
Execute o código acima para gerar automaticamente o código de resposta de acordo com os campos do banco de dados
Responda à verificação do token enviada pelo WeChat
Acesso à documentação da plataforma WeChat: Visão geral do acesso | Documentação aberta do WeChat (qq.com)
Olhando a documentação oficial, podemos ver a seguinte descrição
Portanto, devemos primeiro escrever uma interface get para responder à solicitação do WeChat e devolvê-la corretamente
WTokenController
Adicione o código em
@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
implementar checkToken
o método em
/**
* 微信接口配置,响应微信发送的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
Uma classe de ferramenta de assinatura é usada aqui , então crie uma nova utils/SignUtil.java
, o conteúdo é o seguinte
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;
}
}
Reinicie o projeto, abra swagger-ui e veja a interface que escrevemos
Por ser desenvolvido localmente, o projeto só pode ser acessado localmente. Mas o WeChat não pode acessar nossa máquina local, então podemos usar a ferramenta de penetração da intranet para mapear o endereço IP local para o acesso à rede pública
Aqui estou usando o natapp, o site oficial está aqui , você mesmo pode estudar. A seguir está o endereço da rede pública após minha penetração
Em seguida, abra a plataforma de teste WeChat, copie o endereço da rede pública + nome da interface para a caixa de entrada abaixo
Após clicar em Enviar, se tudo estiver normal, um lembrete de configuração bem-sucedida aparecerá
Neste ponto, concluímos o trabalho de acesso ao WeChat, vamos aprender como enviar mensagens modelo
Encapsular classe de resposta pública
Primeiro prepare uma classe de enumeração de código de status
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;
}
}
e então a classe de entidade de resposta
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;
}
}
Obtenha token de acesso
Documentação aberta do WeChat (qq.com)
O access_token é a credencial de chamada de interface globalmente exclusiva da conta oficial, e o access_token é necessário quando a conta oficial chama cada interface. Os desenvolvedores precisam salvar adequadamente. O armazenamento do access_token deve reservar no mínimo 512 caracteres. O período de validade do access_token é atualmente de 2 horas e precisa ser atualizado regularmente. Obtê-lo repetidamente invalidará o access_token obtido da última vez.
adicionar método get
@GetMapping("getAccessToken")
public Response<String> gotAccessToken(){
String accessToken = tokenService.getAccessToken();
return Response.rspData(accessToken);
}
Implemente o método getAccessToken em 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();
}
}
Código da classe da ferramenta DateTimeUtils usado aqui
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);
}
}
Responder às solicitações do WeChat
Mensagem de texto | Documento aberto do WeChat (qq.com)
Quando um usuário comum do WeChat envia uma mensagem para uma conta pública, o servidor WeChat envia o pacote de dados XML da mensagem POST para a URL preenchida pelo desenvolvedor.
importar processamento de dependência 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>
Adicione uma classe de ferramenta
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;
}
}
Adicione um método POST para responder ao WeChat. O endereço deste método deve ser o mesmo do token de verificação configurado acima, exceto que o tipo de solicitação é alterado para postagem.
@ApiOperation("相应微信消息")
@PostMapping
public String postWeChar(HttpServletRequest request, HttpServletResponse response){
return tokenService.postWeChar(request,response);
}
Implementar o método 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;
}
Por exemplo, agora enviamos uma mensagem para a conta oficial
Você pode ver que uma mensagem foi impressa e podemos avaliar o tipo de mensagem de acordo com MsgType. Obtenha o openid do usuário e outras operações
Podemos criar uma classe de mapeamento MessageType de acordo com o tipo de mensagem
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";
}
Adicione WeCharServiceImpl para lidar com mensagens e eventos enviados pelo 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;
}
}
Injetar weCharService automaticamente
@Autowired
WeCharServiceImpl weCharService;
Chame esses dois métodos em resposta a
/**
* 相应微信请求
* @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 "";
}
Efeito:
![imagem-20230604190827452](https://szx-bucket1.oss-cn-hangzhou.aliyuncs.com/picgo/image-20230604190827452.png)
Quando eu parar de seguir e seguir novamente, uma nova mensagem também será enviada, e o método de mensagem do evento será chamado aqui
![imagem-20230604191036499](https://szx-bucket1.oss-cn-hangzhou.aliyuncs.com/picgo/image-20230604191036499.png)
enviar modelo de mensagem
Plataforma pública WeChat (qq.com) - enviar modelo de mensagem
Gere um ID de mensagem de modelo antes de enviar a mensagem de modelo
Conteúdo da mensagem, xxxx.DATA, o seguinte .DATA foi corrigido
O modelo de mensagem no ambiente de teste pode ser customizado, mas ao solicitar um modelo de mensagem na conta oficial oficial, o modelo de mensagem só pode ser selecionado de acordo com a categoria da conta oficial, e o modelo de mensagem não pode ser customizado, e o os campos são palavra-chave1 e palavra-chave2 , ..., portanto, sugere-se que também usemos palavra-chave1 e palavra-chave2 para definir os campos da mensagem do modelo no ambiente de teste
{
{first.DATA}}
客户姓名:{
{keyword1.DATA}}
联系电话:{
{keyword2.DATA}}
业务类型:{
{keyword3.DATA}}
{
{remark.DATA}}
De acordo com o documento, queremos enviar uma solicitação de postagem e passar os dados json, criamos uma classe de entidade correspondente com base nos dados json, o que é conveniente para definir parâmetros
Adicionar 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;
}
}
Implementar método para enviar mensagem modelo
/**
* 发送模板消息
*/
@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));
}
Em um cenário de negócios real, quando um determinado negócio é concluído, o código aciona o método de envio de uma mensagem de modelo e obtém dinamicamente o openId do usuário, o ID da mensagem de modelo e o endereço de salto, mas não temos um cenário de negócios real aqui, então elas são variáveis embutidas em código
Vamos escrever uma interface para acionar manualmente o envio de mensagens modelo
@ApiOperation("发送模板消息")
@GetMapping("sendTemplateMsg")
public void sendTemplateMsg(){
tokenService.sendTemplateMsg();
}
Após clicar no botão enviar, a conta oficial enviará um modelo de mensagem, o efeito é o seguinte
No ambiente de teste, a cor da fonte que definimos não entrou em vigor. Entrará em vigor na conta pública oficial
Acesso H5 Login WeChat
Para etapas detalhadas, consulte meu outro artigo: [Conta pública H5 de acesso à página 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)
Coloque o código js principal aqui, adicione o seguinte código ao código para realizar a lógica de login do WeChat
De acordo com o seu negócio, você pode fazer uma pequena alteração
// 这里是两个方法,一个
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()
}
}
Em seguida, importe-o em main.js
import wxauth from '@/utils/wxauth.js'
app.use(wxauth)
Existem dois métodos usados no código acima
- 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,
},
})
}
Os códigos Java correspondentes são os seguintes
/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());
}