diff --git a/coolstore-partner-common/src/main/java/com/cool/store/enums/wechat/WechatTemplateEnum.java b/coolstore-partner-common/src/main/java/com/cool/store/enums/wechat/WechatTemplateEnum.java new file mode 100644 index 000000000..adba1821f --- /dev/null +++ b/coolstore-partner-common/src/main/java/com/cool/store/enums/wechat/WechatTemplateEnum.java @@ -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; + } +} diff --git a/coolstore-partner-common/src/main/java/com/cool/store/utils/OkHttpUtil.java b/coolstore-partner-common/src/main/java/com/cool/store/utils/OkHttpUtil.java new file mode 100644 index 000000000..668b1b7a0 --- /dev/null +++ b/coolstore-partner-common/src/main/java/com/cool/store/utils/OkHttpUtil.java @@ -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 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 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 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 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); + } + } + } +} diff --git a/coolstore-partner-model/src/main/java/com/cool/store/dto/wechat/AccessTokenDTO.java b/coolstore-partner-model/src/main/java/com/cool/store/dto/wechat/AccessTokenDTO.java new file mode 100644 index 000000000..2b046715e --- /dev/null +++ b/coolstore-partner-model/src/main/java/com/cool/store/dto/wechat/AccessTokenDTO.java @@ -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; + +} diff --git a/coolstore-partner-model/src/main/java/com/cool/store/dto/wechat/WechatTemplateMessageDTO.java b/coolstore-partner-model/src/main/java/com/cool/store/dto/wechat/WechatTemplateMessageDTO.java new file mode 100644 index 000000000..33f7a681a --- /dev/null +++ b/coolstore-partner-model/src/main/java/com/cool/store/dto/wechat/WechatTemplateMessageDTO.java @@ -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 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; + } + } + +} diff --git a/coolstore-partner-service/src/main/java/com/cool/store/builder/TemplateMessageBuilder.java b/coolstore-partner-service/src/main/java/com/cool/store/builder/TemplateMessageBuilder.java new file mode 100644 index 000000000..9881c7cb3 --- /dev/null +++ b/coolstore-partner-service/src/main/java/com/cool/store/builder/TemplateMessageBuilder.java @@ -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 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 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 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 buildTemplateData(Map data) { + Map 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"; // 默认用深灰色 + } + } +} diff --git a/coolstore-partner-service/src/main/java/com/cool/store/config/weixin/WechatMiniappProperties.java b/coolstore-partner-service/src/main/java/com/cool/store/config/weixin/WechatMiniappProperties.java new file mode 100644 index 000000000..b1be5c0f6 --- /dev/null +++ b/coolstore-partner-service/src/main/java/com/cool/store/config/weixin/WechatMiniappProperties.java @@ -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; + +} diff --git a/coolstore-partner-service/src/main/java/com/cool/store/config/weixin/WechatMpProperties.java b/coolstore-partner-service/src/main/java/com/cool/store/config/weixin/WechatMpProperties.java new file mode 100644 index 000000000..10acc5f25 --- /dev/null +++ b/coolstore-partner-service/src/main/java/com/cool/store/config/weixin/WechatMpProperties.java @@ -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"; +} diff --git a/coolstore-partner-service/src/main/java/com/cool/store/service/wechat/WechatTemplateService.java b/coolstore-partner-service/src/main/java/com/cool/store/service/wechat/WechatTemplateService.java new file mode 100644 index 000000000..58912b529 --- /dev/null +++ b/coolstore-partner-service/src/main/java/com/cool/store/service/wechat/WechatTemplateService.java @@ -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 data) { + WechatTemplateMessageDTO messageDTO = templateMessageBuilder.buildNormalTemplate(openId, template, data); + return sendTemplateMessage(messageDTO); + } + + public boolean sendMiniAppTemplate(String openId, WechatTemplateEnum template, + Map data, String miniAppPagePath) { + WechatTemplateMessageDTO messageDTO = templateMessageBuilder.buildMiniappTemplate( + openId, template, data, miniAppPagePath); + return sendTemplateMessage(messageDTO); + } + + public boolean sendMiniAppTemplateWithUrl(String openId, WechatTemplateEnum template, + Map 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 data, WechatCallback callback) { + WechatTemplateMessageDTO messageDTO = templateMessageBuilder.buildNormalTemplate(openId, template, data); + sendTemplateMessageAsync(messageDTO, callback); + } + + public void sendMiniAppTemplateAsync(String openId, WechatTemplateEnum template, + Map 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); + } +} diff --git a/coolstore-partner-web/src/main/java/com/cool/store/controller/webb/PCTestController.java b/coolstore-partner-web/src/main/java/com/cool/store/controller/webb/PCTestController.java index 64c87bda8..2f6cdc355 100644 --- a/coolstore-partner-web/src/main/java/com/cool/store/controller/webb/PCTestController.java +++ b/coolstore-partner-web/src/main/java/com/cool/store/controller/webb/PCTestController.java @@ -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 testMiniAppTemplate() { + Map 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); + } + + }