feat:微信通知

This commit is contained in:
苏竹红
2025-10-13 15:36:42 +08:00
parent 166e5c3d3c
commit 7752433027
9 changed files with 744 additions and 0 deletions

View File

@@ -0,0 +1,72 @@
package com.cool.store.enums.wechat;
import com.fasterxml.jackson.annotation.JsonValue;
/**
* @Author suzhuhong
* @Date 2025/10/10 14:39
* @Version 1.0
*/
public enum WechatTemplateEnum {
ORDER_PAY_SUCCESS("ORDER_PAY_SUCCESS", "TM00001", "订单支付成功通知",
"您的订单已支付成功\n订单号{{orderNo.DATA}}\n支付金额{{amount.DATA}}元\n支付时间{{payTime.DATA}}\n感谢您的购买"),
TEST("TEST", "T3sp5gBItHKD8oCeEiQMjn7JXpngFiz3dDcaArk84xY", "收到工单通知",
"测试模板"),
;
private final String code;
private final String templateId;
private final String title;
private final String content;
WechatTemplateEnum(String code, String templateId, String title, String content) {
this.code = code;
this.templateId = templateId;
this.title = title;
this.content = content;
}
@JsonValue
public String getCode() {
return code;
}
public String getTemplateId() {
return templateId;
}
public String getTitle() {
return title;
}
public String getContent() {
return content;
}
/**
* 根据code获取枚举
*/
public static WechatTemplateEnum getByCode(String code) {
for (WechatTemplateEnum template : values()) {
if (template.getCode().equals(code)) {
return template;
}
}
return null;
}
/**
* 根据模板ID获取枚举
*/
public static WechatTemplateEnum getByTemplateId(String templateId) {
for (WechatTemplateEnum template : values()) {
if (template.getTemplateId().equals(templateId)) {
return template;
}
}
return null;
}
}

View File

@@ -0,0 +1,195 @@
package com.cool.store.utils;
import com.cool.store.enums.ErrorCodeEnum;
import com.cool.store.exception.ServiceException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.Map;
/**
* @Author suzhuhong
* @Date 2025/10/10 14:21
* @Version 1.0
* OkHttp工具类
*/
@Slf4j
@Component
public class OkHttpUtil {
@Autowired
private OkHttpClient okHttpClient;
@Autowired
private ObjectMapper objectMapper;
private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
/**
* GET请求
*/
public String doGet(String url) throws IOException {
return doGet(url, null);
}
/**
* GET请求 - 带请求头
*/
public String doGet(String url, Map<String, String> headers) throws IOException {
Request.Builder builder = new Request.Builder().url(url);
// 添加请求头
if (headers != null && !headers.isEmpty()) {
headers.forEach(builder::addHeader);
}
Request request = builder.build();
try (Response response = okHttpClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new IOException("Unexpected code: " + response);
}
ResponseBody body = response.body();
return body != null ? body.string() : null;
}
}
/**
* POST请求 - JSON数据
*/
public String doPostJson(String url, Object data) throws IOException {
return doPostJson(url, data, null);
}
/**
* POST请求 - JSON数据带请求头
*/
public String doPostJson(String url, Object data, Map<String, String> headers) throws IOException {
//打印日志
logRequest(url, data);
String json = objectMapper.writeValueAsString(data);
RequestBody requestBody = RequestBody.create( JSON,json);
Request.Builder builder = new Request.Builder()
.url(url)
.post(requestBody);
// 添加请求头
if (headers != null && !headers.isEmpty()) {
headers.forEach(builder::addHeader);
}
Request request = builder.build();
try (Response response = okHttpClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new ServiceException(ErrorCodeEnum.THIRD_API_ERROR,
"HTTP请求失败状态码: " + response.code());
}
ResponseBody body = response.body();
logResponse(url, response.code(), body.string());
return body != null ? body.string() : null;
}
}
/**
* 异步GET请求
*/
public void doGetAsync(String url, Callback callback) {
doGetAsync(url, null, callback);
}
/**
* 异步GET请求 - 带请求头
*/
public void doGetAsync(String url, Map<String, String> headers, Callback callback) {
Request.Builder builder = new Request.Builder().url(url);
if (headers != null && !headers.isEmpty()) {
headers.forEach(builder::addHeader);
}
Request request = builder.build();
okHttpClient.newCall(request).enqueue(callback);
}
/**
* 异步POST请求
*/
public void doPostAsync(String url, Object data, Callback callback) {
doPostAsync(url, data, null, callback);
}
/**
* 异步POST请求 - 带请求头
*/
public void doPostAsync(String url, Object data, Map<String, String> headers, Callback callback) {
try {
String json = objectMapper.writeValueAsString(data);
RequestBody requestBody = RequestBody.create( JSON,json);
Request.Builder builder = new Request.Builder()
.url(url)
.post(requestBody);
if (headers != null && !headers.isEmpty()) {
headers.forEach(builder::addHeader);
}
Request request = builder.build();
okHttpClient.newCall(request).enqueue(callback);
} catch (IOException e) {
callback.onFailure(null, e);
}
}
private void logRequest(String url, Object requestBody) {
if (log.isInfoEnabled()) {
try {
log.info("\n======= 请求开始 =======\n" +
"API地址: {}\n" +
"请求参数: {}\n" +
"======= 请求结束 =======",
url,
objectMapper.writerWithDefaultPrettyPrinter()
.writeValueAsString(requestBody));
} catch (JsonProcessingException e) {
log.warn("日志JSON序列化失败", e);
}
}
}
private void logResponse(String url, int statusCode, String responseBody) {
if (log.isInfoEnabled()) {
try {
// 尝试美化JSON输出
Object json = objectMapper.readValue(responseBody, Object.class);
String prettyResponse = objectMapper.writerWithDefaultPrettyPrinter()
.writeValueAsString(json);
log.info("\n======= 响应开始 =======\n" +
"API地址: {}\n" +
"HTTP状态码: {}\n" +
"响应内容: {}\n" +
"======= 响应结束 =======",
url,
statusCode,
prettyResponse);
} catch (Exception e) {
// 非JSON响应或解析失败时直接输出原始内容
log.info("\n======= 响应开始 =======\n" +
"API地址: {}\n" +
"HTTP状态码: {}\n" +
"原始响应: {}\n" +
"======= 响应结束 =======",
url,
statusCode,
responseBody);
}
}
}
}

View File

@@ -0,0 +1,17 @@
package com.cool.store.dto.wechat;
import lombok.Data;
/**
* @Author suzhuhong
* @Date 2025/10/10 15:01
* @Version 1.0
*/
@Data
public class AccessTokenDTO {
private String access_token;
private Integer expires_in;
}

View File

@@ -0,0 +1,90 @@
package com.cool.store.dto.wechat;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.Map;
/**
* @Author suzhuhong
* @Date 2025/10/10 14:36
* @Version 1.0
*/
@Data
public class WechatTemplateMessageDTO {
/**
* 接收者openid
*/
@JsonProperty("touser")
private String toUser;
/**
* 模板ID
*/
@JsonProperty("template_id")
private String templateId;
/**
* 模板跳转链接(非必须)
*/
private String url;
/**
* 跳小程序所需数据,不需跳小程序可不用传该数据
*/
private MiniprogramDTO miniprogram;
/**
* 模板数据
*/
private Map<String, TemplateDataItemDTO> data;
/**
* 小程序跳转DTO
*/
@Data
public static class MiniprogramDTO {
/**
* 所需跳转到的小程序appid
*/
private String appid;
/**
* 所需跳转到小程序的具体页面路径,支持带参数
*/
private String pagepath;
}
/**
* 模板数据项DTO
*/
@Data
public static class TemplateDataItemDTO {
/**
* 模板内容
*/
private String value;
/**
* 模板内容字体颜色,不填默认为黑色
*/
private String color;
public TemplateDataItemDTO() {
}
public TemplateDataItemDTO(String value) {
this.value = value;
this.color = "#333333";
}
public TemplateDataItemDTO(String value, String color) {
this.value = value;
this.color = color;
}
}
}

View File

@@ -0,0 +1,126 @@
package com.cool.store.builder;
import com.cool.store.config.weixin.WechatMiniappProperties;
import com.cool.store.dto.wechat.WechatTemplateMessageDTO;
import com.cool.store.enums.wechat.WechatTemplateEnum;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
/**
* @Author suzhuhong
* @Date 2025/10/10 14:34
* @Version 1.0
*/
@Component
public class TemplateMessageBuilder {
@Autowired
private WechatMiniappProperties wechatMiniappProperties;
/**
* 构建普通模板消息
*/
public WechatTemplateMessageDTO buildNormalTemplate(String openId,
WechatTemplateEnum template,
Map<String, Object> data) {
WechatTemplateMessageDTO messageDTO = new WechatTemplateMessageDTO();
messageDTO.setToUser(openId);
messageDTO.setTemplateId(template.getTemplateId());
// 设置URL如果data中包含url
if (data.containsKey("url")) {
messageDTO.setUrl((String) data.get("url"));
}
// 构建模板数据
messageDTO.setData(buildTemplateData(data));
return messageDTO;
}
/**
* 构建小程序跳转模板消息
*/
public WechatTemplateMessageDTO buildMiniappTemplate(String openId,
WechatTemplateEnum template,
Map<String, Object> data,
String miniAppPagePath) {
WechatTemplateMessageDTO messageDTO = new WechatTemplateMessageDTO();
messageDTO.setToUser(openId);
messageDTO.setTemplateId(template.getTemplateId());
// 设置小程序跳转
WechatTemplateMessageDTO.MiniprogramDTO miniProgram = new WechatTemplateMessageDTO.MiniprogramDTO();
miniProgram.setAppid(wechatMiniappProperties.getAppId());
miniProgram.setPagepath(miniAppPagePath != null ? miniAppPagePath : wechatMiniappProperties.getDefaultPagePath());
messageDTO.setMiniprogram(miniProgram);
// 设置备用URL如果data中包含url
if (data.containsKey("url")) {
messageDTO.setUrl((String) data.get("url"));
}
// 构建模板数据
messageDTO.setData(buildTemplateData(data));
return messageDTO;
}
/**
* 构建小程序跳转模板消息带备用URL
*/
public WechatTemplateMessageDTO buildMiniAppTemplateWithUrl(String openId,
WechatTemplateEnum template,
Map<String, Object> data,
String miniAppPagePath,
String backupUrl) {
WechatTemplateMessageDTO messageDTO = buildMiniappTemplate(openId, template, data, miniAppPagePath);
// 设置备用URL
if (backupUrl != null && !backupUrl.trim().isEmpty()) {
messageDTO.setUrl(backupUrl);
}
return messageDTO;
}
/**
* 构建模板数据
*/
private Map<String, WechatTemplateMessageDTO.TemplateDataItemDTO> buildTemplateData(Map<String, Object> data) {
Map<String, WechatTemplateMessageDTO.TemplateDataItemDTO> templateData = new HashMap<>();
data.forEach((key, value) -> {
if (!"url".equals(key) && value != null) {
WechatTemplateMessageDTO.TemplateDataItemDTO item =
new WechatTemplateMessageDTO.TemplateDataItemDTO(
value.toString(),
getColorByField(key)
);
templateData.put(key, item);
}
});
return templateData;
}
/**
* 根据字段名获取颜色
*/
private String getColorByField(String fieldName) {
switch (fieldName) {
case "amount":
case "refundAmount":
case "couponValue":
case "character_string2":
return "#FF0000"; // 金额类字段用红色
case "orderNo":
case "expressNo":
return "#173177"; // 编号类字段用蓝色
default:
return "#333333"; // 默认用深灰色
}
}
}

View File

@@ -0,0 +1,32 @@
package com.cool.store.config.weixin;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* @Author suzhuhong
* @Date 2025/10/10 14:41
* @Version 1.0
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "wechat.miniapp")
public class WechatMiniappProperties {
/**
* 小程序appId
*/
private String appId;
/**
* 小程序页面路径
*/
private String defaultPagePath ;
/**
* 是否使用小程序跳转
*/
private boolean enabled = false;
}

View File

@@ -0,0 +1,37 @@
package com.cool.store.config.weixin;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* @Author suzhuhong
* @Date 2025/10/10 14:29
* @Version 1.0
* 微信服务号配置
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "wechat.mp")
public class WechatMpProperties {
/**
* 公众号appId
*/
private String appId;
/**
* 公众号appSecret
*/
private String appSecret;
/**
* 获取access_token的URL
*/
private String accessTokenUrl = "https://api.weixin.qq.com/cgi-bin/token";
/**
* 发送模板消息的URL
*/
private String sendTemplateMessageUrl = "https://api.weixin.qq.com/cgi-bin/message/template/send";
}

View File

@@ -0,0 +1,155 @@
package com.cool.store.service.wechat;
import com.cool.store.builder.TemplateMessageBuilder;
import com.cool.store.config.weixin.WechatMpProperties;
import com.cool.store.dto.wechat.AccessTokenDTO;
import com.cool.store.dto.wechat.WechatTemplateMessageDTO;
import com.cool.store.enums.wechat.WechatTemplateEnum;
import com.cool.store.utils.OkHttpUtil;
import com.cool.store.utils.poi.StringUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.Map;
/**
* @Author suzhuhong
* @Date 2025/10/10 14:15
* @Version 1.0
*/
@Slf4j
@Service
public class WechatTemplateService {
@Autowired
private WechatMpProperties wechatMpProperties;
@Autowired
private TemplateMessageBuilder templateMessageBuilder;
@Autowired
private OkHttpUtil okHttpUtil;
@Autowired
private ObjectMapper objectMapper;
public String getAccessToken() {
String url = String.format("%s?grant_type=client_credential&appid=%s&secret=%s",
wechatMpProperties.getAccessTokenUrl(),
wechatMpProperties.getAppId(),
wechatMpProperties.getAppSecret());
try {
String result = okHttpUtil.doGet(url);
log.info("获取access_token响应: {}", result);
if (StringUtils.isNotEmpty( result)){
AccessTokenDTO responseDTO = objectMapper.readValue(result, AccessTokenDTO.class);
return responseDTO.getAccess_token();
}
return null;
} catch (IOException e) {
log.error("获取access_token失败", e);
} catch (Exception e) {
log.error("解析access_token响应失败", e);
}
return null;
}
public boolean sendNormalTemplate(String openId, WechatTemplateEnum template, Map<String, Object> data) {
WechatTemplateMessageDTO messageDTO = templateMessageBuilder.buildNormalTemplate(openId, template, data);
return sendTemplateMessage(messageDTO);
}
public boolean sendMiniAppTemplate(String openId, WechatTemplateEnum template,
Map<String, Object> data, String miniAppPagePath) {
WechatTemplateMessageDTO messageDTO = templateMessageBuilder.buildMiniappTemplate(
openId, template, data, miniAppPagePath);
return sendTemplateMessage(messageDTO);
}
public boolean sendMiniAppTemplateWithUrl(String openId, WechatTemplateEnum template,
Map<String, Object> data, String miniAppPagePath, String backupUrl) {
WechatTemplateMessageDTO messageDTO = templateMessageBuilder.buildMiniAppTemplateWithUrl(
openId, template, data, miniAppPagePath, backupUrl);
return sendTemplateMessage(messageDTO);
}
private boolean sendTemplateMessage(WechatTemplateMessageDTO messageDTO) {
String accessToken = getAccessToken();
if (accessToken == null) {
log.error("获取access_token失败无法发送模板消息");
return false;
}
String url = String.format("%s?access_token=%s",
wechatMpProperties.getSendTemplateMessageUrl(), accessToken);
try {
String result = okHttpUtil.doPostJson(url, messageDTO);
log.info("发送模板消息响应: {}", result);
return Boolean.TRUE;
} catch (Exception e) {
log.error("解析模板消息响应失败", e);
return false;
}
}
public void sendTemplateMessageAsync(WechatTemplateMessageDTO messageDTO, WechatCallback callback) {
String accessToken = getAccessToken();
if (accessToken == null) {
log.error("获取access_token失败无法发送模板消息");
callback.onFailure(new IOException("获取access_token失败"));
return;
}
String url = String.format("%s?access_token=%s",
wechatMpProperties.getSendTemplateMessageUrl(), accessToken);
okHttpUtil.doPostAsync(url, messageDTO, new okhttp3.Callback() {
@Override
public void onResponse(okhttp3.Call call, okhttp3.Response response) throws IOException {
try {
String result = response.body() != null ? response.body().string() : null;
log.info("异步发送模板消息响应: {}", result);
if (response.isSuccessful() && result != null) {
callback.onSuccess(Boolean.TRUE);
} else {
callback.onFailure(new IOException("请求失败,状态码: " + response.code()));
}
} catch (Exception e) {
callback.onFailure(e);
}
}
@Override
public void onFailure(okhttp3.Call call, IOException e) {
log.error("异步发送模板消息失败", e);
callback.onFailure(e);
}
});
}
public void sendNormalTemplateAsync(String openId, WechatTemplateEnum template,
Map<String, Object> data, WechatCallback callback) {
WechatTemplateMessageDTO messageDTO = templateMessageBuilder.buildNormalTemplate(openId, template, data);
sendTemplateMessageAsync(messageDTO, callback);
}
public void sendMiniAppTemplateAsync(String openId, WechatTemplateEnum template,
Map<String, Object> data, String miniAppPagePath, WechatCallback callback) {
WechatTemplateMessageDTO messageDTO = templateMessageBuilder.buildMiniappTemplate(
openId, template, data, miniAppPagePath);
sendTemplateMessageAsync(messageDTO, callback);
}
public interface WechatCallback {
void onSuccess(Boolean successFlag);
void onFailure(Exception e);
}
}

View File

@@ -8,11 +8,13 @@ import com.cool.store.dto.FoodTokenDTO;
import com.cool.store.dto.GetAccessTokenDTO;
import com.cool.store.dto.HqtTokenDTO;
import com.cool.store.dto.ModifyPasswordDTO;
import com.cool.store.dto.wechat.WechatTemplateMessageDTO;
import com.cool.store.entity.*;
import com.cool.store.enums.DownSystemTypeEnum;
import com.cool.store.enums.MessageEnum;
import com.cool.store.enums.SMSMsgEnum;
import com.cool.store.enums.point.ShopSubStageStatusEnum;
import com.cool.store.enums.wechat.WechatTemplateEnum;
import com.cool.store.job.XxlJobHandler;
import com.cool.store.mapper.FranchiseFeeMapper;
import com.cool.store.mapper.LineInfoMapper;
@@ -36,6 +38,7 @@ import com.cool.store.service.*;
import com.cool.store.service.impl.CommonService;
import com.cool.store.service.impl.OrderSysInfoServiceImpl;
import com.cool.store.service.impl.UserAuthMappingServiceImpl;
import com.cool.store.service.wechat.WechatTemplateService;
import com.cool.store.utils.CoolDateUtils;
import com.cool.store.utils.RedisConstantUtil;
import com.cool.store.utils.RedisUtilPool;
@@ -556,4 +559,21 @@ public class PCTestController {
return request;
}
@Resource
WechatTemplateService wechatTemplateService;
@ApiOperation("测试小程序模板消息")
@PostMapping("/testMiniAppTemplate")
public ApiResponse<Boolean> testMiniAppTemplate() {
Map<String, Object> data = new HashMap<>();
data.put("character_string2", "ceshi002");
data.put("thing10", "测试通知功能");
data.put("time14", "2025-10-01 12:00:00");
data.put("thing25", "正新管理有限公司");
data.put("thing60", "上海市-松江区");
wechatTemplateService.sendNormalTemplate("o9_unvpJy1SGdnkeun7igRBSLuB0", WechatTemplateEnum.TEST, data);
return ApiResponse.success(true);
}
}