Java公式アカウント開発の一般的な利用シナリオソリューション
記事の紹介
この記事では、私が開発プロセス中に遭遇した WeChat 開発に関連する多くの一般的な機能をまとめました。これらの質問はすべてインターネット上に散在する回答なので、その後の開発を容易にするためにここにまとめます。間違いがあれば、それでも批判や指摘をしていきたいと思っていますので、今から始めましょう。
主に次のチュートリアルが含まれています。
- コードジェネレーターを作成してSwaggerを統合する
- WeChat インターフェース構成、WeChat によって送信される応答トークン検証
- 有効期限を自動的に更新し、トークンを取得するときにトークンを更新します
- Java は WeChat の通常のメッセージとイベント メッセージを処理し、それに応じて応答します
- Java 送信テンプレート メッセージ
- H5 アクセス WeChat 認証ログイン
プロジェクトのソース コード アドレスを表示することもできます。WeChat テンプレート メッセージの送信とリダイレクト: Java を使用して WeChat テンプレート メッセージを送信し、テンプレート メッセージをクリックして詳細にジャンプします (gitee.com)
準備
データベースを準備する
新しいトークンテーブルを作成する
分野 | タイプ | 注記 |
---|---|---|
ID | 整数 | 主キー |
トークン | 可変長文字 | トークン |
有効期限が切れます | 可変長文字 | 有効期限 |
テーブル作成ステートメント
/*
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;
ここでは、最初にデフォルトで 2 つのデータが追加され、トークンの更新および取得時に ID が 1 のデータが固定的にクエリされます。
依存関係を追加する
以下は私のpomファイルです
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>java-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<artifactId>spring-boot-starter-parent</artifactId>
<groupId>org.springframework.boot</groupId>
<version>2.3.10.RELEASE</version>
</parent>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>
<!-- velocity 模板引擎, Mybatis Plus 代码生成器需要 -->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.0</version>
</dependency>
<!--junit-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
<!--druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.5</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.22</version>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--swagger-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<!--swagger ui-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<!--java-jwt-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.8.2</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.5.9</version>
</dependency>
</dependencies>
</project>
設定ファイルを編集する
アプリケーション.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
アプリケーション開発.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
構成クラスを作成する
新しい構成を作成して、プロジェクトの構成ファイルを配置します
ディレクトリ構造は次のとおりです
アプリケーション構成
この構成は主に ComponentScan と MapperScan を構成するために使用されます。
package com.szx.java.config;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
/**
* @author songzx
* @create 2023-05-30 16:03
*/
@Configuration
@ComponentScan(basePackages = "com.szx")
@MapperScan(basePackages = "com.szx")
public class ApplicationConfig {
}
SwaggerConfig
この構成は Swagger の構成に使用されます
package com.szx.java.config;
import com.google.common.base.Predicates;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
/**
* @author songzx
* @create 2022-09-22 11:21
*/
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket webApiConfig(){
return new Docket(DocumentationType.SWAGGER_2)
.groupName("webApi")
.apiInfo(webApiInfo())
.select()
.paths(Predicates.not(PathSelectors.regex("/admin/.*")))
.paths(Predicates.not(PathSelectors.regex("/error.*")))
.build();
}
public ApiInfo webApiInfo(){
return new ApiInfoBuilder()
.title("Api文档")
.description("文本档描述了定义的接口")
.version("1.0")
.contact(new Contact("szx", "https://blog.csdn.net/SongZhengxing_?spm=1010.2135.3001.5343","[email protected]"))
.build();
}
}
スタートアップクラスを変更する
デフォルトでは、起動が成功した後にプロジェクトの実行アドレスを直感的に知ることはできませんが、次の設定により、操作が成功した後のインターフェイス アドレスと Swagger アドレスを直感的に確認できます。
package com.szx.java;
import lombok.SneakyThrows;
import lombok.extern.log4j.Log4j2;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.Environment;
import java.net.InetAddress;
/**
* @author songzx
* @create 2023-05-12 14:41
*/
@Log4j2
@SpringBootApplication
public class SzxApplication {
@SneakyThrows
public static void main(String[] args) {
ConfigurableApplicationContext application = SpringApplication.run(SzxApplication.class, args);
Environment env = application.getEnvironment();
String host = InetAddress.getLocalHost().getHostAddress();
String port = env.getProperty("server.port");
log.info("\n ----------------------------------------------------------\n\t" +
"Application '{}' 正在运行中... Access URLs:\n\t" +
"Local: \t\thttp://localhost:{}\n\t" +
"External: \thttp://{}:{}\n\t" +
"Doc: \thttp://{}:{}/doc.html\n\t" +
"SwaggerDoc: \thttp://{}:{}/swagger-ui.html\n\t" +
"----------------------------------------------------------",
env.getProperty("spring.application.name"),
env.getProperty("server.port"),
host, port,
host, port,
host, port);
}
}
起動効果:
コードジェネレーターを追加
次のコードをテスト ファイルに直接コピーすると、コントローラー、エンティティ、マッパー、サービスを自動的に生成できます
注: 生成されたコードの場所を独自のプロジェクト アドレスに変更します。
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();
}
}
アドレスの説明
上記のコードを実行すると、データベースのフィールドに従って応答コードが自動的に生成されます。
WeChat から送信されたトークン検証に応答します
WeChat プラットフォーム ドキュメントへのアクセス:アクセスの概要 | WeChat オープン ドキュメント (qq.com)
公式ドキュメントを見ると、次のような記述が見られます。
したがって、まず WeChat のリクエストに応答してそれを正しく返す get インターフェースを作成する必要があります。
WTokenController
コードを追加します
@Api(tags = "token管理")
@RestController
@RequestMapping("/wtoken")
public class WTokenController {
@Autowired
WTokenService tokenService;
@ApiOperation("微信接口配置,响应微信发送的Token验证")
@GetMapping
public String checkToken(HttpServletRequest request, HttpServletResponse response){
return tokenService.checkToken(request,response);
}
}
メソッドをWTokenServiceImpl
実装するにはcheckToken
/**
* 微信接口配置,响应微信发送的Token验证
*/
@Override
public String checkToken(HttpServletRequest request, HttpServletResponse response) {
if (StringUtils.isNotBlank(request.getParameter("signature"))) {
String signature = request.getParameter("signature");
String timestamp = request.getParameter("timestamp");
String nonce = request.getParameter("nonce");
String echostr = request.getParameter("echostr");
if (SignUtil.checkSignature(signature, timestamp, nonce)) {
return echostr;
}
}
return "";
}
SignUtil
ここでは署名ツールクラスを使用するので新規作成しますutils/SignUtil.java
。内容は以下の通りです
package com.szx.java.utils;
import com.szx.java.constants.WxConstants;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
/**
* @author songzx
* @create 2022-11-22 13:52
*/
public class SignUtil {
// 与开发模式接口配置信息中的Token保持一致.
private static String token = WxConstants.WX_CHECK_TOKEN;
/**
* 校验签名
* @param signature 微信加密签名.
* @param timestamp 时间戳.
* @param nonce 随机数.
* @return
*/
public static boolean checkSignature(String signature, String timestamp, String nonce) {
// 对token、timestamp、和nonce按字典排序.
String[] paramArr = new String[] {
token, timestamp, nonce};
Arrays.sort(paramArr);
// 将排序后的结果拼接成一个字符串.
String content = paramArr[0].concat(paramArr[1]).concat(paramArr[2]);
String ciphertext = null;
try {
MessageDigest md = MessageDigest.getInstance("SHA-1");
// 对拼接后的字符串进行sha1加密.
byte[] digest = md.digest(content.toString().getBytes());
ciphertext = byteToStr(digest);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
// 将sha1加密后的字符串与signature进行对比.
return ciphertext != null ? ciphertext.equals(signature.toUpperCase()) : false;
}
/**
* 将字节数组转换为十六进制字符串.
* @param byteArray
* @return
*/
private static String byteToStr(byte[] byteArray) {
String strDigest = "";
for (int i = 0; i < byteArray.length; i++) {
strDigest += byteToHexStr(byteArray[i]);
}
return strDigest;
}
/**
* 将字节转换为十六进制字符串.
* @param mByte
* @return
*/
private static String byteToHexStr(byte mByte) {
char[] Digit = {
'0', '1' , '2', '3', '4' , '5', '6', '7' , '8', '9', 'A' , 'B', 'C', 'D' , 'E', 'F'};
char[] tempArr = new char[2];
tempArr[0] = Digit[(mByte >>> 4) & 0X0F];
tempArr[1] = Digit[mByte & 0X0F];
String s = new String(tempArr);
return s;
}
}
プロジェクトを再起動し、swagger-ui を開き、作成したインターフェイスを表示します。
ローカルで開発されているため、プロジェクトにはローカルでのみアクセスできます。ただし、WeChat はローカル マシンにアクセスできないため、イントラネット侵入ツールを使用してローカル IP アドレスをパブリック ネットワーク アクセスにマッピングできます。
ここではnatappを使用しています。公式Webサイトはここにあります。自分で勉強できます。以下は、侵入後のパブリック ネットワーク アドレスです。
次に、WeChat テスト プラットフォームを開き、パブリック ネットワーク アドレスとインターフェイス名を下の入力ボックスにコピーします。
「送信」をクリックした後、すべてが正常であれば、構成が成功したことを示すリマインダーがポップアップ表示されます。
この時点で、WeChat へのアクセス作業は完了しました。テンプレート メッセージの送信方法を学びましょう。
パブリック応答クラスをカプセル化する
まずステータスコードの列挙クラスを準備します。
package com.szx.java.utils;
/**
* @author songzx
* @date 2023/6/4
* @apiNote
*/
public enum ResponseEnum {
// 可以根据自己的实际需要增加状态码
SUCCESS("0", "ok"),
SERVER_INNER_ERR("500","系统繁忙"),
PARAM_LACK("100" , "非法参数"),
OPERATION_FAILED("101" ,"操作失败");
private String code;
private String msg;
ResponseEnum(String code, String msg) {
this.code = code;
this.msg = msg;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
次に応答エンティティクラス
package com.szx.java.utils;
import com.fasterxml.jackson.annotation.JsonInclude;
/**
* @author songzx
* @date 2023/6/4
* @apiNote
*/
@JsonInclude(JsonInclude.Include.NON_NULL) // 值等于null的属性不返回
public class Response<T> {
private String code;
private String msg;
private T data;
/**
* @title 成功消息
* @return
*/
public static <T> Response<T> success() {
return rspMsg(ResponseEnum.SUCCESS);
}
/**
* @title 失败消息
* @return
*/
public static <T> Response<T> error() {
return rspMsg(ResponseEnum.SERVER_INNER_ERR);
}
/**
* @title 自定义消息
* @return
*/
public static <T> Response<T> rspMsg(ResponseEnum responseEnum) {
Response<T> message = new Response<T>();
message.setCode(responseEnum.getCode());
message.setMsg(responseEnum.getMsg());
return message;
}
/**
* @title 自定义消息
* @return
*/
public static <T> Response<T> rspMsg(String code , String msg) {
Response<T> message = new Response<T>();
message.setCode(code);
message.setMsg(msg);
return message;
}
/**
* @title 返回数据
* @param data
* @return
*/
public static <T> Response<T> rspData(T data) {
Response<T> responseData = new Response<T>();
responseData.setCode(ResponseEnum.SUCCESS.getCode());
responseData.setData(data);
return responseData;
}
/**
* @title 返回数据-自定义code
* @param data
* @return
*/
public static <T> Response<T> rspData(String code , T data) {
Response<T> responseData = new Response<T>();
responseData.setCode(code);
responseData.setData(data);
return responseData;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
アクセストークンを取得する
access_token は、公式アカウントのグローバルに一意なインターフェース呼び出し資格情報であり、公式アカウントが各インターフェースを呼び出すときに access_token が必要です。開発者は適切に保存する必要があります。access_token のストレージには、少なくとも 512 文字を予約する必要があります。access_tokenの有効期限は現在2時間で定期的に更新する必要があり、繰り返し取得すると前回取得したaccess_tokenが無効になってしまいます。
getメソッドを追加
@GetMapping("getAccessToken")
public Response<String> gotAccessToken(){
String accessToken = tokenService.getAccessToken();
return Response.rspData(accessToken);
}
tokenService に getAccessToken メソッドを実装する
/**
* 获取AccessToken
* @return
*/
@Override
public String getAccessToken() {
// 固定查询id等于1的数据
WToken wToken = this.getById(1);
// 判断当前的accessToken是否在有效期内,小于0表示在有效期内
if(DateTimeUtils.CompareTime(wToken.getExpires()) < 0){
return wToken.getToken();
}else{
// 不再有效期内时调用接口获取新的token
String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&" +
"appid=" + WxConstants.WX_APPID +
"&secret=" + WxConstants.WX_SECRET;
// 发送请求,获取json格式字符串
String result_str = HttpUtil.get(url);
// 将json格式的字符串转成JSONObject类型,方便获取里面的数据
JSONObject result_json = JSONUtil.parseObj(result_str);
// 从json中读取access_token字段,并转成string
String access_token = result_json.get("access_token").toString();
// 更新token
wToken.setToken(access_token);
// 更新过期时间
wToken.setExpires(DateTimeUtils.FutureTime());
// 更新数据库中的值
this.updateById(wToken);
// 返回最新的token
return wToken.getToken();
}
}
ここで使用される DateTimeUtils ツール クラス コード
package com.szx.java.utils;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Date;
/**
* @author songzx
* @date 2023/6/4
* @apiNote
*/
public class DateTimeUtils {
/**
* 拿当前时间和传递过来时间做比较,如果当前时间小于传递进来的时间,则返回负数,否则返回正数
*/
public static int CompareTime(String time){
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime dateTime = LocalDateTime.parse(time, formatter);
Instant instant = dateTime.atZone(ZoneId.systemDefault()).toInstant();
long timestamp = instant.toEpochMilli();
long currentTimestamp = System.currentTimeMillis();
return Long.compare(currentTimestamp, timestamp);
}
/**
* 更新过期时间
*/
public static String FutureTime(){
int seconds = 7000;
LocalDateTime dateTime = LocalDateTime.now().plusSeconds(seconds);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
return dateTime.format(formatter);
}
}
WeChat リクエストに応答する
テキスト メッセージ | WeChat オープン ドキュメント (qq.com)
一般の WeChat ユーザーが公開アカウントにメッセージを送信すると、WeChat サーバーは開発者が入力した URL に POST メッセージの XML データ パケットを送信します。
依存関係処理 XML をインポートします
<!--处理xml-->
<dependency>
<groupId>xmlpull</groupId>
<artifactId>xmlpull</artifactId>
<version>1.1.3.1</version>
</dependency>
<!--XML解析器-->
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6</version>
</dependency>
ツールクラスを追加する
package com.szx.java.utils;
import cn.hutool.core.collection.CollectionUtil;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import javax.servlet.http.HttpServletRequest;
import java.io.InputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
/**
* 封装和处理xml文件
* @author Administrator
*
*/
public class XmlUtil {
private static final String PREFIX_XML = "<xml>";
private static final String SUFFIX_XML = "</xml>";
private static final String PREFIX_CDATA = "<![CDATA[";
private static final String SUFFIX_CDATA = "]]>";
/**
* 转化成xml, 单层无嵌套
*/
public static String xmlFormat(Map<String, String> parm, boolean isAddCDATA) {
StringBuffer strbuff = new StringBuffer(PREFIX_XML);
if (CollectionUtil.isNotEmpty(parm)) {
for (Entry<String, String> entry : parm.entrySet()) {
strbuff.append("<").append(entry.getKey()).append(">");
if (isAddCDATA) {
strbuff.append(PREFIX_CDATA);
if (StringUtils.isNotEmpty(entry.getValue())) {
strbuff.append(entry.getValue());
}
strbuff.append(SUFFIX_CDATA);
} else {
if (StringUtils.isNotEmpty(entry.getValue())) {
strbuff.append(entry.getValue());
}
}
strbuff.append("</").append(entry.getKey()).append(">");
}
}
return strbuff.append(SUFFIX_XML).toString();
}
/**
* 解析微信发来的请求(XML)
*
* @param request
* @return
* @throws Exception
*/
public static Map<String, String> parseXml(HttpServletRequest request) throws Exception {
// 将解析结果存储在HashMap中
Map<String, String> map = new HashMap<String, String>();
// 从request中取得输入流
InputStream inputStream = request.getInputStream();
// 读取输入流
SAXReader reader = new SAXReader();
Document document = reader.read(inputStream);
// 得到xml根元素
Element root = document.getRootElement();
// 得到根元素的所有子节点
@SuppressWarnings("unchecked")
List<Element> elementList = root.elements();
// 遍历所有子节点
for (Element e : elementList) {
map.put(e.getName(), e.getText());
}
// 释放资源
inputStream.close();
inputStream = null;
return map;
}
}
WeChat に応答する POST メソッドを追加します。このメソッドのアドレスは、リクエスト タイプが post に変更されることを除き、上で構成した検証トークンのアドレスと同じである必要があります。
@ApiOperation("相应微信消息")
@PostMapping
public String postWeChar(HttpServletRequest request, HttpServletResponse response){
return tokenService.postWeChar(request,response);
}
postWeCharメソッドを実装する
/**
* 相应微信请求
* @param request
* @param response
* @return
*/
@Override
public String postWeChar(HttpServletRequest request, HttpServletResponse response) {
try {
Map<String, String> xmlMap = XmlUtil.parseXml(request);
System.out.println(xmlMap);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
たとえば、今は公式アカウントにメッセージを送信します
メッセージが出力されていることがわかり、MsgType に従ってメッセージの種類を判断できます。ユーザーの openid の取得とその他の操作
メッセージタイプに応じてマッピングクラス MessageType を作成できます。
package com.szx.java.utils;
/**
* @author songzx
* @date 2023/6/4
* @apiNote
*/
public class MessageType {
/*
* 文本消息
*/
public static final String TEXT_MESSAGE = "text";
/*
* 图片消息
*/
public static final String IMAGE_MESSAGE = "image";
/*
* 语音消息
*/
public static final String VOICE_MESSAGE = "voice";
/*
* 视频消息
*/
public static final String VIDEO_MESSAGE = "video";
/*
* 小视频消息消息
*/
public static final String SHORTVIDEO_MESSAGE = "shortvideo";
/*
* 地理位置消息
*/
public static final String POSOTION_MESSAGE = "location";
/*
* 链接消息
*/
public static final String LINK_MESSAGE = "link";
/*
* 音乐消息
*/
public static final String MUSIC_MESSAGE = "music";
/*
* 图文消息
*/
public static final String IMAGE_TEXT_MESSAGE = "news";
/*
* 请求消息类型:事件推送
*/
public static final String REQ_MESSAGE_TYPE_EVENT = "event";
/*
* 事件类型:subscribe(订阅)
*/
public static final String EVENT_TYPE_SUBSCRIBE = "subscribe";
/*
* 事件类型:unsubscribe(取消订阅)
*/
public static final String EVENT_TYPE_UNSUBSCRIBE = "unsubscribe";
/*
* 事件类型:scan(用户已关注时的扫描带参数二维码)
*/
public static final String EVENT_TYPE_SCAN = "scan";
/*
* 事件类型:LOCATION(上报地理位置)
*/
public static final String EVENT_TYPE_LOCATION = "location";
/*
* 事件类型:CLICK(自定义菜单)
*/
public static final String EVENT_TYPE_CLICK = "click";
/*
* 响应消息类型:文本
*/
public static final String RESP_MESSAGE_TYPE_TEXT = "text";
/*
* 响应消息类型:图片
*/
public static final String RESP_MESSAGE_TYPE_IMAGE = "image";
/*
* 响应消息类型:语音
*/
public static final String RESP_MESSAGE_TYPE_VOICE = "voice";
/*
* 响应消息类型:视频
*/
public static final String RESP_MESSAGE_TYPE_VIDEO = "video";
/*
* 响应消息类型:音乐
*/
public static final String RESP_MESSAGE_TYPE_MUSIC = "music";
/*
* 响应消息类型:图文
*/
public static final String RESP_MESSAGE_TYPE_NEWS = "news";
}
WeCharServiceImpl を追加して、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;
}
}
weCharService を自動的に挿入する
@Autowired
WeCharServiceImpl weCharService;
に応答してこれら 2 つのメソッドを呼び出します
/**
* 相应微信请求
* @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 "";
}
効果:
フォローを解除して再度フォローすると、新しいメッセージも送信され、ここでイベント メッセージ メソッドが呼び出されます
テンプレートメッセージを送信する
WeChat パブリック プラットフォーム (qq.com) - テンプレート メッセージを送信
テンプレート メッセージを送信する前にテンプレート メッセージ ID を生成します
メッセージ内容 xxxx.DATA、以下の.DATAは修正済みです
テスト環境のメッセージテンプレートはカスタマイズ可能ですが、公式公式アカウントでテンプレートメッセージを申請する場合、テンプレートメッセージは公式アカウントのカテゴリーに応じて選択することしかできず、テンプレートメッセージのカスタマイズはできません。フィールドはkeyword1とkeyword2、...であるため、テスト環境でテンプレートメッセージのフィールドを定義するためにkeyword1とkeyword2も使用することをお勧めします。
{
{first.DATA}}
客户姓名:{
{keyword1.DATA}}
联系电话:{
{keyword2.DATA}}
业务类型:{
{keyword3.DATA}}
{
{remark.DATA}}
ドキュメントによると、postリクエストを送信してjsonデータを渡したいので、パラメータの設定に便利なjsonデータに基づいて対応するエンティティクラスを作成します
WxTemplateVoを追加
package com.szx.java.entity.Vo;
import lombok.Data;
import java.util.TreeMap;
/**
* @author songzx
* @create 2022-11-29 15:45
*/
@Data
public class WxTemplateVo {
/**
* 接收者openId
*/
private String touser;
/**
* 模板ID
*/
private String template_id;
/**
* 模板跳转链接
*/
private String url;
/**
* data数据
*/
private TreeMap<String, TreeMap<String, String>> data;
/**
* 参数
*
* @param value 值
* @param color 颜色 可不填
* @return params
*/
public static TreeMap<String, String> item(String value, String color) {
TreeMap<String, String> params = new TreeMap<String, String>();
params.put("value", value);
params.put("color", color);
return params;
}
}
テンプレートメッセージを送信するメソッドを実装する
/**
* 发送模板消息
*/
@Override
public void sendTemplateMsg() {
// 发送模板请求的地址
String postUrl = "https://api.weixin.qq.com/cgi-bin/message/template/send" +
"?access_token=" + getToken();
// 要给那个用户发送模板消息
String openId = "olttN6WJOYe-lTysV8_tsnZ7-HMQ";
// 模板消息ID
String templeID = "vRGjpYZ-uL3CCREW9c6Kl9csekoW9tVbVl7hf_y3k5U";
// 点击模板消息要跳转的地址,如果不设置则不会跳转
String templeUrl = "http://baidu.com";
// 构造模板消息内容
TreeMap<String, TreeMap<String, String>> params = new TreeMap<>();
params.put("keyword1", WxTemplateVo.item("第一行消息", "#409EFF"));
params.put("keyword2", WxTemplateVo.item("第二行消息", "#409EFF"));
params.put("keyword3", WxTemplateVo.item("第三行消息", "#409EFF"));
// 将模板消息放进实体类中
WxTemplateVo wxTemplateMsg = new WxTemplateVo();
wxTemplateMsg.setTemplate_id(templeID);
wxTemplateMsg.setTouser(openId);
wxTemplateMsg.setData(params);
wxTemplateMsg.setUrl(templeUrl);
// 请求请求
HttpUtil.post(postUrl, JSONUtil.toJsonStr(wxTemplateMsg));
}
実際のビジネス シナリオでは、特定のビジネスが完了すると、コードはテンプレート メッセージを送信するメソッドをトリガーし、ユーザーの openId、テンプレート メッセージ ID、およびジャンプ アドレスを動的に取得しますが、ここでは実際のビジネス シナリオはありません。したがって、それらはハードコードされた変数です
テンプレート メッセージの送信を手動でトリガーするインターフェイスを作成しましょう
@ApiOperation("发送模板消息")
@GetMapping("sendTemplateMsg")
public void sendTemplateMsg(){
tokenService.sendTemplateMsg();
}
送信ボタンをクリックすると、公式アカウントからテンプレートメッセージが送信されます。効果は次のとおりです。
テスト環境では、設定した文字色は反映されませんでした。公式パブリックアカウントの下で発効します
H5 アクセス WeChat ログイン
詳細な手順については、私の他の記事を参照してください: [パブリック アカウント H5 ページへの WeChat ログイン プロセス_パブリック アカウント h5 WeChat ログイン_szx の開発ノートのブログ - CSDN ブログ](https://blog.csdn.net/SongZhengxing_/article /details/121036115?utm_source) = uc_fansmsg)
ここにコアの js コードを配置し、コードに次のコードを追加して WeChat ログイン ロジックを実現します。
自分のビジネスに応じて、少し変更することができます
// 这里是两个方法,一个
import {
getTokenByCode, getUserinfoByToken } from '../api/index.js'
// 这里使用的是Vue3的Pinia,如果是Vue2可以换成Vuex
import {
userInfo } from '../store/userInfo.js'
// 公众号的appid
const appid = import.meta.env.VITE_APPID
// 公众号的secret
const secret = import.meta.env.VITE_SECRET
// 获取当前页面地址作为回调地址,并且对地址进行urlEncode处理
export function jumpAuthPage() {
let url = localStorage.getItem('localUrl')
url = processUrl(url)
let local = encodeURIComponent(url)
// 跳转到授权页面
window.location.href =
'https://open.weixin.qq.com/connect/oauth2/authorize?appid=' +
appid +
'&redirect_uri=' +
local +
'&response_type=code&scope=snsapi_userinfo&state=1#wechat_redirect'
}
// 解析原始URL
function processUrl(url) {
if (url.indexOf('code') !== -1) {
let start = url.indexOf('?')
let end = url.indexOf('#')
return url.slice(0, start) + url.slice(end)
} else {
return url
}
}
// 获取路径上参数
export function getUrlCode(name) {
return (
decodeURIComponent(
(new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)').exec(
location.href
) || [, ''])[1].replace(/\+/g, '%20')
) || null
)
}
// 根据code获取网站授权token
export function getTokenFormCode(code) {
// 根据code获取到openid
getTokenByCode(appid, secret, code).then((res) => {
const {
access_token, openid } = res.data.info
// 根据token获取用户基本信息
getUserinfoByToken(access_token, openid).then((info) => {
if (info.code === 500) {
// 如果根据token没有拿到用户信息则重新授权获取新的code重新获取用户信息
jumpAuthPage()
} else {
// 获取到用户信息后删除缓存的地址
localStorage.removeItem('localUrl')
// 保存用户基本信息到Pinia
userInfo().setUserInfo(info.data.info, info.data)
}
})
})
}
export default function () {
// 判断是否有code
let code = getUrlCode('code')
let scope = getUrlCode('scope')
// 当获取到code后再调用获取token的方法
if (code) {
getTokenFormCode(code)
} else {
// 这里使用缓存获取最初进来的页面地址,这样实现在授权完成后回调时还展示最开始的页面,实现从那个页面进来,还回调到那个页面
if (!scope && !localStorage.getItem('localUrl')) {
localStorage.setItem('localUrl', window.location.href)
}
jumpAuthPage()
}
}
次に、main.js にインポートします。
import wxauth from '@/utils/wxauth.js'
app.use(wxauth)
上記のコードでは 2 つのメソッドが使用されています
- getTokenByCode
- getUserinfoByToken
// 根据code获取网站授权token
export function getTokenByCode(appid, secret, code) {
return server({
method: 'get',
url: `/edu/wx/oauth2`,
params: {
appid,
secret,
code,
},
})
}
// 拉取用户信息
export function getUserinfoByToken(token, openid) {
return server({
method: 'get',
url: `/edu/wx/wxUserinfo`,
params: {
token,
openid,
},
})
}
対応するJavaコードは以下のとおりです。
/edu/wx/oauth2
/**
* 根据code获取网站授权token
*/
@ApiOperation("根据code获取网站授权token")
@GetMapping("oauth2")
public Msg oauth(@RequestParam String appid,
@RequestParam String secret,
@RequestParam String code) {
// 构造请求地址
String url = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=" + appid +
"&secret=" + secret +
"&code=" + code +
"&grant_type=authorization_code";
// 发送get请求
JSONObject jsonObject = HttpUtils.DO_GET(url);
// 将请求信息返回给前端
return Msg.Ok().data("info", jsonObject);
}
/edu/wx/wxUserinfo
/**
* 获取用户基本信息
*/
@ApiOperation("获取用户基本信息")
@GetMapping("wxUserinfo")
public Msg getUserInfo(@RequestParam String token,
@RequestParam String openid) {
// 根据openid查询用户表,判断这个用户是否存在
LfUser fUser = lfUserService.zcUserByOpenid(openid);
// 构造请求地址
String url = "https://api.weixin.qq.com/sns/userinfo?access_token=" + token +
"&openid=" + openid +
"&lang=zh_CN";
// 发送请求
String s = HttpUtil.get(url);
// 将得到的结果字符串json转成JSONObject
cn.hutool.json.JSONObject jsonObject = JSONUtil.parseObj(s);
// 从结果中获取微信昵称
String nickname = jsonObject.get("nickname").toString();
// 从结果中获取微信头像
String headimgurl = jsonObject.get("headimgurl").toString();
// 如果该用户不曾存在,则注册用户,否则更新用户最新的微信昵称和头像
if (fUser == null) {
fUser = new LfUser();
fUser.setOpenid(openid);
fUser.setNickname(nickname);
fUser.setPhoto(headimgurl);
lfUserService.addLfUser(fUser);
} else {
fUser.setNickname(nickname);
fUser.setPhoto(headimgurl);
lfUserService.updateById(fUser);
}
// 将结果返回
return Msg.Ok()
.data("info", fUser)
.data("userId", fUser.getId())
.data("loginTime", fUser.getGmtModified());
}