diff --git a/pom.xml b/pom.xml
index e3b56e9804..5c00af8b31 100644
--- a/pom.xml
+++ b/pom.xml
@@ -31,7 +31,7 @@
https://github.com/YunaiV/ruoyi-vue-pro
- 2.1.0-jdk8-snapshot
+ 2.2.0-jdk8-snapshot
1.8
${java.version}
diff --git a/yudao-dependencies/pom.xml b/yudao-dependencies/pom.xml
index 753cac0516..08bf8767d9 100644
--- a/yudao-dependencies/pom.xml
+++ b/yudao-dependencies/pom.xml
@@ -14,7 +14,7 @@
https://github.com/YunaiV/ruoyi-vue-pro
- 2.1.0-jdk8-snapshot
+ 2.2.0-jdk8-snapshot
1.6.0
2.7.18
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/java/cn/iocoder/yudao/framework/ip/core/Area.java b/yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/java/cn/iocoder/yudao/framework/ip/core/Area.java
index 262bc263ce..3027085871 100644
--- a/yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/java/cn/iocoder/yudao/framework/ip/core/Area.java
+++ b/yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/java/cn/iocoder/yudao/framework/ip/core/Area.java
@@ -1,6 +1,7 @@
package cn.iocoder.yudao.framework.ip.core;
import cn.iocoder.yudao.framework.ip.core.enums.AreaTypeEnum;
+import com.fasterxml.jackson.annotation.JsonBackReference;
import com.fasterxml.jackson.annotation.JsonManagedReference;
import lombok.AllArgsConstructor;
import lombok.Data;
@@ -54,7 +55,7 @@ public class Area {
/**
* 子节点
*/
- @JsonManagedReference
+ @JsonBackReference
private List children;
}
diff --git a/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/MessageTemplateConstants.java b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/MessageTemplateConstants.java
new file mode 100644
index 0000000000..9a6bdd41df
--- /dev/null
+++ b/yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/MessageTemplateConstants.java
@@ -0,0 +1,14 @@
+package cn.iocoder.yudao.module.promotion.enums;
+
+/**
+ * 通知模板枚举类
+ *
+ * @author HUIHUI
+ */
+public interface MessageTemplateConstants {
+
+ //======================= 小程序订阅消息模版 =======================
+
+ String COMBINATION_SUCCESS = "拼团结果通知";
+
+}
diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/social/vo/AppSocialWxaSubscribeTemplateRespVO.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/social/vo/AppSocialWxaSubscribeTemplateRespVO.java
new file mode 100644
index 0000000000..445122d3f6
--- /dev/null
+++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/social/vo/AppSocialWxaSubscribeTemplateRespVO.java
@@ -0,0 +1,27 @@
+package cn.iocoder.yudao.module.member.controller.app.social.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Schema(description = "用户 APP - 获得小程序订阅模版 Response VO")
+@Data
+public class AppSocialWxaSubscribeTemplateRespVO {
+
+ @Schema(description = "模版编号", requiredMode = Schema.RequiredMode.REQUIRED,
+ example = "9Aw5ZV1j9xdWTFEkqCpZ7mIBbSC34khK55OtzUPl0rU")
+ private String id;
+
+ @Schema(description = "模版标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "订单支付通知")
+ private String title;
+
+ @Schema(description = "模版内容", requiredMode = Schema.RequiredMode.REQUIRED,
+ example = "{ {result.DATA} }\\n\\n领奖金额:{ {withdrawMoney.DATA} }\\n领奖时间: { {withdrawTime.DATA} }")
+ private String content;
+
+ @Schema(description = "模板内容示例", requiredMode = Schema.RequiredMode.REQUIRED, example = "下单时间:2016年8月8日")
+ private String example;
+
+ @Schema(description = "模版类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+ private Integer type; // 2 为一次性订阅,3 为长期订阅
+
+}
diff --git a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/MessageTemplateConstants.java b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/MessageTemplateConstants.java
new file mode 100644
index 0000000000..9497d776b2
--- /dev/null
+++ b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/MessageTemplateConstants.java
@@ -0,0 +1,14 @@
+package cn.iocoder.yudao.module.pay.enums;
+
+/**
+ * 通知模板枚举类
+ *
+ * @author HUIHUI
+ */
+public interface MessageTemplateConstants {
+
+ // ======================= 小程序订阅消息 =======================
+
+ String WXA_WALLET_RECHARGER_PAID = "充值成功通知";
+
+}
diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialClientApi.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialClientApi.java
index cd6fc31be9..ef02dd1086 100644
--- a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialClientApi.java
+++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialClientApi.java
@@ -1,12 +1,12 @@
package cn.iocoder.yudao.module.system.api.social;
-import cn.iocoder.yudao.module.system.api.social.dto.SocialWxJsapiSignatureRespDTO;
-import cn.iocoder.yudao.module.system.api.social.dto.SocialWxPhoneNumberInfoRespDTO;
-import cn.iocoder.yudao.module.system.api.social.dto.SocialWxQrcodeReqDTO;
+import cn.iocoder.yudao.module.system.api.social.dto.*;
import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum;
import javax.validation.Valid;
+import java.util.List;
+
/**
* 社交应用的 API 接口
*
@@ -17,8 +17,8 @@ public interface SocialClientApi {
/**
* 获得社交平台的授权 URL
*
- * @param socialType 社交平台的类型 {@link SocialTypeEnum}
- * @param userType 用户类型
+ * @param socialType 社交平台的类型 {@link SocialTypeEnum}
+ * @param userType 用户类型
* @param redirectUri 重定向 URL
* @return 社交平台的授权 URL
*/
@@ -28,15 +28,17 @@ public interface SocialClientApi {
* 创建微信公众号 JS SDK 初始化所需的签名
*
* @param userType 用户类型
- * @param url 访问的 URL 地址
+ * @param url 访问的 URL 地址
* @return 签名
*/
SocialWxJsapiSignatureRespDTO createWxMpJsapiSignature(Integer userType, String url);
+ //======================= 微信小程序独有 =======================
+
/**
* 获得微信小程序的手机信息
*
- * @param userType 用户类型
+ * @param userType 用户类型
* @param phoneCode 手机授权码
* @return 手机信息
*/
@@ -50,4 +52,18 @@ public interface SocialClientApi {
*/
byte[] getWxaQrcode(@Valid SocialWxQrcodeReqDTO reqVO);
+ /**
+ * 获得微信小程订阅模板
+ *
+ * @return 小程序订阅消息模版
+ */
+ List getWxaSubscribeTemplateList(Integer userType);
+
+ /**
+ * 发送微信小程序订阅消息
+ *
+ * @param reqDTO 请求
+ */
+ void sendWxaSubscribeMessage(SocialWxaSubscribeMessageSendReqDTO reqDTO);
+
}
diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialWxaSubscribeMessageSendReqDTO.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialWxaSubscribeMessageSendReqDTO.java
new file mode 100644
index 0000000000..fa074a2c1f
--- /dev/null
+++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialWxaSubscribeMessageSendReqDTO.java
@@ -0,0 +1,61 @@
+package cn.iocoder.yudao.module.system.api.social.dto;
+
+import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
+import lombok.Data;
+
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 微信小程序订阅消息发送 Request DTO
+ *
+ * @author HUIHUI
+ */
+@Data
+public class SocialWxaSubscribeMessageSendReqDTO {
+
+ /**
+ * 用户编号
+ *
+ * 关联 MemberUserDO 的 id 编号
+ * 关联 AdminUserDO 的 id 编号
+ */
+ @NotNull(message = "用户编号不能为空")
+ private Long userId;
+ /**
+ * 用户类型
+ *
+ * 关联 {@link UserTypeEnum}
+ */
+ @NotNull(message = "用户类型不能为空")
+ private Integer userType;
+
+ /**
+ * 消息模版标题
+ */
+ @NotEmpty(message = "消息模版标题不能为空")
+ private String templateTitle;
+
+ /**
+ * 点击模板卡片后的跳转页面,仅限本小程序内的页面
+ *
+ * 支持带参数,(示例 index?foo=bar )。该字段不填则模板无跳转。
+ */
+ private String page;
+
+ /**
+ * 模板内容的参数
+ */
+ private Map messages;
+
+ public SocialWxaSubscribeMessageSendReqDTO addMessage(String key, String value) {
+ if (messages == null) {
+ messages = new HashMap<>();
+ }
+ messages.put(key, value);
+ return this;
+ }
+
+}
diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialWxaSubscribeTemplateRespDTO.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialWxaSubscribeTemplateRespDTO.java
new file mode 100644
index 0000000000..ae14cc5432
--- /dev/null
+++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialWxaSubscribeTemplateRespDTO.java
@@ -0,0 +1,42 @@
+package cn.iocoder.yudao.module.system.api.social.dto;
+
+import lombok.Data;
+
+
+/**
+ * 小程序订阅消息模版 Response DTO
+ *
+ * @author HUIHUI
+ */
+@Data
+public class SocialWxaSubscribeTemplateRespDTO {
+
+ /**
+ * 模版编号
+ */
+ private String id;
+
+ /**
+ * 模版标题
+ */
+ private String title;
+
+ /**
+ * 模版内容
+ */
+ private String content;
+
+ /**
+ * 模板内容示例
+ */
+ private String example;
+
+ /**
+ * 模版类型
+ *
+ * 2:为一次性订阅
+ * 3:为长期订阅
+ */
+ private Integer type;
+
+}
diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java
index 585308dd69..e360a426b9 100644
--- a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java
+++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java
@@ -117,8 +117,10 @@ public interface ErrorCodeConstants {
ErrorCode SOCIAL_CLIENT_WEIXIN_MINI_APP_PHONE_CODE_ERROR = new ErrorCode(1_002_018_200, "获得手机号失败");
ErrorCode SOCIAL_CLIENT_WEIXIN_MINI_APP_QRCODE_ERROR = new ErrorCode(1_002_018_201, "获得小程序码失败");
- ErrorCode SOCIAL_CLIENT_NOT_EXISTS = new ErrorCode(1_002_018_202, "社交客户端不存在");
- ErrorCode SOCIAL_CLIENT_UNIQUE = new ErrorCode(1_002_018_203, "社交客户端已存在配置");
+ ErrorCode SOCIAL_CLIENT_WEIXIN_MINI_APP_SUBSCRIBE_TEMPLATE_ERROR = new ErrorCode(1_002_018_202, "获得小程序订阅消息模版失败");
+ ErrorCode SOCIAL_CLIENT_WEIXIN_MINI_APP_SUBSCRIBE_MESSAGE_ERROR = new ErrorCode(1_002_018_203, "发送小程序订阅消息失败");
+ ErrorCode SOCIAL_CLIENT_NOT_EXISTS = new ErrorCode(1_002_018_210, "社交客户端不存在");
+ ErrorCode SOCIAL_CLIENT_UNIQUE = new ErrorCode(1_002_018_211, "社交客户端已存在配置");
// ========== OAuth2 客户端 1-002-020-000 =========
diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialClientApiImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialClientApiImpl.java
index 654857fafb..d0e60405c0 100644
--- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialClientApiImpl.java
+++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialClientApiImpl.java
@@ -1,16 +1,25 @@
package cn.iocoder.yudao.module.system.api.social;
import cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo;
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjUtil;
+import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
-import cn.iocoder.yudao.module.system.api.social.dto.SocialWxJsapiSignatureRespDTO;
-import cn.iocoder.yudao.module.system.api.social.dto.SocialWxPhoneNumberInfoRespDTO;
-import cn.iocoder.yudao.module.system.api.social.dto.SocialWxQrcodeReqDTO;
+import cn.iocoder.yudao.module.system.api.social.dto.*;
+import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum;
import cn.iocoder.yudao.module.system.service.social.SocialClientService;
+import cn.iocoder.yudao.module.system.service.social.SocialUserService;
+import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.bean.WxJsapiSignature;
+import me.chanjar.weixin.common.bean.subscribemsg.TemplateInfo;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
+import java.util.List;
+
+import static cn.hutool.core.collection.CollUtil.findOne;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
/**
* 社交应用的 API 实现类
@@ -19,10 +28,13 @@ import javax.annotation.Resource;
*/
@Service
@Validated
+@Slf4j
public class SocialClientApiImpl implements SocialClientApi {
@Resource
private SocialClientService socialClientService;
+ @Resource
+ private SocialUserService socialUserService;
@Override
public String getAuthorizeUrl(Integer socialType, Integer userType, String redirectUri) {
@@ -35,6 +47,8 @@ public class SocialClientApiImpl implements SocialClientApi {
return BeanUtils.toBean(signature, SocialWxJsapiSignatureRespDTO.class);
}
+ //======================= 微信小程序独有 =======================
+
@Override
public SocialWxPhoneNumberInfoRespDTO getWxMaPhoneNumberInfo(Integer userType, String phoneCode) {
WxMaPhoneNumberInfo info = socialClientService.getWxMaPhoneNumberInfo(userType, phoneCode);
@@ -46,4 +60,38 @@ public class SocialClientApiImpl implements SocialClientApi {
return socialClientService.getWxaQrcode(reqVO);
}
+ @Override
+ public List getWxaSubscribeTemplateList(Integer userType) {
+ List list = socialClientService.getSubscribeTemplateList(userType);
+ return convertList(list, item -> BeanUtils.toBean(item, SocialWxaSubscribeTemplateRespDTO.class).setId(item.getPriTmplId()));
+ }
+
+ @Override
+ public void sendWxaSubscribeMessage(SocialWxaSubscribeMessageSendReqDTO reqDTO) {
+ // 1.1 获得订阅模版列表
+ List templateList = socialClientService.getSubscribeTemplateList(reqDTO.getUserType());
+ if (CollUtil.isEmpty(templateList)) {
+ log.warn("[sendSubscribeMessage][reqDTO({}) 发送订阅消息失败,原因:没有找到订阅模板]", reqDTO);
+ return;
+ }
+ // 1.2 获得需要使用的模版
+ TemplateInfo template = findOne(templateList, item ->
+ ObjUtil.equal(item.getTitle(), reqDTO.getTemplateTitle()));
+ if (template == null) {
+ log.warn("[sendWxaSubscribeMessage][reqDTO({}) 发送订阅消息失败,原因:没有找到订阅模板]", reqDTO);
+ return;
+ }
+
+ // 2. 获得社交用户
+ SocialUserRespDTO socialUser = socialUserService.getSocialUserByUserId(reqDTO.getUserType(), reqDTO.getUserId(),
+ SocialTypeEnum.WECHAT_MINI_APP.getType());
+ if (StrUtil.isBlankIfStr(socialUser.getOpenid())) {
+ log.warn("[sendWxaSubscribeMessage][reqDTO({}) 发送订阅消息失败,原因:会员 openid 缺失]", reqDTO);
+ return;
+ }
+
+ // 3. 发送订阅消息
+ socialClientService.sendSubscribeMessage(reqDTO, template.getPriTmplId(), socialUser.getOpenid());
+ }
+
}
diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/SocialClientController.http b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/SocialClientController.http
new file mode 100644
index 0000000000..5ab64392a7
--- /dev/null
+++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/SocialClientController.http
@@ -0,0 +1,20 @@
+### 请求 /system/social-client/send-subscribe-message 接口 => 发送测试订阅消息
+POST {{baseUrl}}/system/social-client/send-subscribe-message
+Authorization: Bearer {{token}}
+Content-Type: application/json
+#Authorization: Bearer test100
+tenant-id: {{adminTenentId}}
+
+{
+ "userId": 247,
+ "userType": 1,
+ "socialType": 34,
+ "templateTitle": "充值成功通知",
+ "page": "",
+ "messages": {
+ "character_string1":"5616122165165",
+ "amount2":"1000.00",
+ "time3":"2024-01-01 10:10:10",
+ "phrase4": "充值成功"
+ }
+}
diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/SocialClientController.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/SocialClientController.java
index c9b5f4da71..5cfde18343 100644
--- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/SocialClientController.java
+++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/SocialClientController.java
@@ -3,6 +3,8 @@ package cn.iocoder.yudao.module.system.controller.admin.socail;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.system.api.social.SocialClientApi;
+import cn.iocoder.yudao.module.system.api.social.dto.SocialWxaSubscribeMessageSendReqDTO;
import cn.iocoder.yudao.module.system.controller.admin.socail.vo.client.SocialClientPageReqVO;
import cn.iocoder.yudao.module.system.controller.admin.socail.vo.client.SocialClientRespVO;
import cn.iocoder.yudao.module.system.controller.admin.socail.vo.client.SocialClientSaveReqVO;
@@ -28,6 +30,8 @@ public class SocialClientController {
@Resource
private SocialClientService socialClientService;
+ @Resource
+ private SocialClientApi socialClientApi;
@PostMapping("/create")
@Operation(summary = "创建社交客户端")
@@ -70,4 +74,11 @@ public class SocialClientController {
return success(BeanUtils.toBean(pageResult, SocialClientRespVO.class));
}
+ @PostMapping("/send-subscribe-message")
+ @Operation(summary = "发送订阅消息") // 用于测试
+ @PreAuthorize("@ss.hasPermission('system:social-client:query')")
+ public void sendSubscribeMessage(@RequestBody SocialWxaSubscribeMessageSendReqDTO reqDTO) {
+ socialClientApi.sendWxaSubscribeMessage(reqDTO);
+ }
+
}
diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/redis/RedisKeyConstants.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/redis/RedisKeyConstants.java
index 8b9e7681f5..b400ede02d 100644
--- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/redis/RedisKeyConstants.java
+++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/redis/RedisKeyConstants.java
@@ -98,4 +98,13 @@ public interface RedisKeyConstants {
* VALUE 数据格式:String 模版信息
*/
String SMS_TEMPLATE = "sms_template";
+
+ /**
+ * 小程序订阅模版的缓存
+ *
+ * KEY 格式:wxa_subscribe_template:{userType}
+ * VALUE 数据格式 String, 模版信息
+ */
+ String WXA_SUBSCRIBE_TEMPLATE = "wxa_subscribe_template";
+
}
diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialClientService.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialClientService.java
index b44673ac11..71c716c520 100644
--- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialClientService.java
+++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialClientService.java
@@ -3,12 +3,16 @@ package cn.iocoder.yudao.module.system.service.social;
import cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.system.api.social.dto.SocialWxQrcodeReqDTO;
+import cn.iocoder.yudao.module.system.api.social.dto.SocialWxaSubscribeMessageSendReqDTO;
import cn.iocoder.yudao.module.system.controller.admin.socail.vo.client.SocialClientPageReqVO;
import cn.iocoder.yudao.module.system.controller.admin.socail.vo.client.SocialClientSaveReqVO;
import cn.iocoder.yudao.module.system.dal.dataobject.social.SocialClientDO;
import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum;
import com.xingyuv.jushauth.model.AuthUser;
import me.chanjar.weixin.common.bean.WxJsapiSignature;
+import me.chanjar.weixin.common.bean.subscribemsg.TemplateInfo;
+
+import java.util.List;
import javax.validation.Valid;
@@ -70,6 +74,25 @@ public interface SocialClientService {
*/
byte[] getWxaQrcode(SocialWxQrcodeReqDTO reqVO);
+ /**
+ * 获得微信小程订阅模板
+ *
+ * 缓存的目的:考虑到微信小程序订阅消息选择好模版后几乎不会变动,缓存增加查询效率
+ *
+ * @param userType 用户类型
+ * @return 微信小程订阅模板
+ */
+ List getSubscribeTemplateList(Integer userType);
+
+ /**
+ * 发送微信小程序订阅消息
+ *
+ * @param reqDTO 请求
+ * @param templateId 模版编号
+ * @param openId 会员 openId
+ */
+ void sendSubscribeMessage(SocialWxaSubscribeMessageSendReqDTO reqDTO, String templateId, String openId);
+
// =================== 客户端管理 ===================
/**
diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialClientServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialClientServiceImpl.java
index 9fa6642fba..6907fa54bd 100644
--- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialClientServiceImpl.java
+++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialClientServiceImpl.java
@@ -1,10 +1,14 @@
package cn.iocoder.yudao.module.system.service.social;
import cn.binarywang.wx.miniapp.api.WxMaService;
+import cn.binarywang.wx.miniapp.api.WxMaSubscribeService;
import cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl;
import cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo;
+import cn.binarywang.wx.miniapp.bean.WxMaSubscribeMessage;
import cn.binarywang.wx.miniapp.config.impl.WxMaRedisBetterConfigImpl;
+import cn.binarywang.wx.miniapp.constant.WxMaConstants;
import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.ReflectUtil;
@@ -15,10 +19,12 @@ import cn.iocoder.yudao.framework.common.util.cache.CacheUtils;
import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.system.api.social.dto.SocialWxQrcodeReqDTO;
+import cn.iocoder.yudao.module.system.api.social.dto.SocialWxaSubscribeMessageSendReqDTO;
import cn.iocoder.yudao.module.system.controller.admin.socail.vo.client.SocialClientPageReqVO;
import cn.iocoder.yudao.module.system.controller.admin.socail.vo.client.SocialClientSaveReqVO;
import cn.iocoder.yudao.module.system.dal.dataobject.social.SocialClientDO;
import cn.iocoder.yudao.module.system.dal.mysql.social.SocialClientMapper;
+import cn.iocoder.yudao.module.system.dal.redis.RedisKeyConstants;
import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum;
import com.binarywang.spring.starter.wxjava.miniapp.properties.WxMaProperties;
import com.binarywang.spring.starter.wxjava.mp.properties.WxMpProperties;
@@ -35,20 +41,25 @@ import com.xingyuv.justauth.AuthRequestFactory;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.bean.WxJsapiSignature;
+import me.chanjar.weixin.common.bean.subscribemsg.TemplateInfo;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.common.redis.RedisTemplateWxRedisOps;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl;
import me.chanjar.weixin.mp.config.impl.WxMpRedisConfigImpl;
import org.springframework.beans.factory.annotation.Value;
+import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.time.Duration;
+import java.util.List;
+import java.util.Map;
import java.util.Objects;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen;
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*;
@@ -62,7 +73,7 @@ import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*;
public class SocialClientServiceImpl implements SocialClientService {
/**
- * 小程序版本
+ * 小程序码要打开的小程序版本
*
* 1. release:正式版
* 2. trial:体验版
@@ -70,6 +81,15 @@ public class SocialClientServiceImpl implements SocialClientService {
*/
@Value("${yudao.wxa-code.env-version:release}")
public String envVersion;
+ /**
+ * 订阅消息跳转小程序类型
+ *
+ * 1. developer:开发版
+ * 2. trial:体验版
+ * 3. formal:正式版
+ */
+ @Value("${yudao.wxa-subscribe-message.miniprogram-state:formal}")
+ public String miniprogramState;
@Resource
private AuthRequestFactory authRequestFactory;
@@ -254,11 +274,58 @@ public class SocialClientServiceImpl implements SocialClientService {
null,
ObjUtil.defaultIfNull(reqVO.getHyaline(), SocialWxQrcodeReqDTO.HYALINE));
} catch (WxErrorException e) {
- log.error("[getWxQrcode][reqVO({})) 获得小程序码失败]", reqVO, e);
+ log.error("[getWxQrcode][reqVO({}) 获得小程序码失败]", reqVO, e);
throw exception(SOCIAL_CLIENT_WEIXIN_MINI_APP_QRCODE_ERROR);
}
}
+ @Override
+ @Cacheable(cacheNames = RedisKeyConstants.WXA_SUBSCRIBE_TEMPLATE, key = "#userType", condition = "#result != null")
+ public List getSubscribeTemplateList(Integer userType) {
+ WxMaService service = getWxMaService(userType);
+ try {
+ WxMaSubscribeService subscribeService = service.getSubscribeService();
+ return subscribeService.getTemplateList();
+ } catch (WxErrorException e) {
+ log.error("[getSubscribeTemplate][userType({}) 获得小程序订阅消息模版]", userType, e);
+ throw exception(SOCIAL_CLIENT_WEIXIN_MINI_APP_SUBSCRIBE_TEMPLATE_ERROR);
+ }
+ }
+
+ @Override
+ public void sendSubscribeMessage(SocialWxaSubscribeMessageSendReqDTO reqDTO, String templateId, String openId) {
+ WxMaService service = getWxMaService(reqDTO.getUserType());
+ try {
+ WxMaSubscribeService subscribeService = service.getSubscribeService();
+ subscribeService.sendSubscribeMsg(buildMessageSendReqDTO(reqDTO, templateId, openId));
+ } catch (WxErrorException e) {
+ log.error("[sendSubscribeMessage][reqVO({}) templateId({}) openId({}) 发送小程序订阅消息失败]", reqDTO, templateId, openId, e);
+ throw exception(SOCIAL_CLIENT_WEIXIN_MINI_APP_SUBSCRIBE_MESSAGE_ERROR);
+ }
+ }
+
+ /**
+ * 构建发送消息请求参数
+ *
+ * @param reqDTO 请求
+ * @param templateId 模版编号
+ * @param openId 会员 openId
+ * @return 微信小程序订阅消息请求参数
+ */
+ private WxMaSubscribeMessage buildMessageSendReqDTO(SocialWxaSubscribeMessageSendReqDTO reqDTO,
+ String templateId, String openId) {
+ // 设置订阅消息基本参数
+ WxMaSubscribeMessage subscribeMessage = new WxMaSubscribeMessage().setLang(WxMaConstants.MiniProgramLang.ZH_CN)
+ .setMiniprogramState(miniprogramState).setTemplateId(templateId).setToUser(openId).setPage(reqDTO.getPage());
+ // 设置具体消息参数
+ Map messages = reqDTO.getMessages();
+ if (CollUtil.isNotEmpty(messages)) {
+ reqDTO.getMessages().keySet().forEach(key -> findAndThen(messages, key, value ->
+ subscribeMessage.addData(new WxMaSubscribeMessage.MsgData(key, value))));
+ }
+ return subscribeMessage;
+ }
+
/**
* 获得 clientId + clientSecret 对应的 WxMpService 对象
*
diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImpl.java
index 93ea16ab07..b07610a848 100644
--- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImpl.java
+++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImpl.java
@@ -9,6 +9,7 @@ import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
import cn.iocoder.yudao.framework.datapermission.core.util.DataPermissionUtils;
import cn.iocoder.yudao.module.infra.api.config.ConfigApi;
import cn.iocoder.yudao.module.infra.api.file.FileApi;
@@ -38,6 +39,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
+import javax.validation.ConstraintViolationException;
import java.io.InputStream;
import java.time.LocalDateTime;
import java.util.*;
@@ -443,7 +445,14 @@ public class AdminUserServiceImpl implements AdminUserService {
UserImportRespVO respVO = UserImportRespVO.builder().createUsernames(new ArrayList<>())
.updateUsernames(new ArrayList<>()).failureUsernames(new LinkedHashMap<>()).build();
importUsers.forEach(importUser -> {
- // 校验,判断是否有不符合的原因
+ // 2.1.1 校验字段是否符合要求
+ try {
+ ValidationUtils.validate(BeanUtils.toBean(importUser, UserSaveReqVO.class).setPassword(initPassword));
+ } catch (ConstraintViolationException ex){
+ respVO.getFailureUsernames().put(importUser.getUsername(), ex.getMessage());
+ return;
+ }
+ // 2.1.2 校验,判断是否有不符合的原因
try {
validateUserForCreateOrUpdate(null, null, importUser.getMobile(), importUser.getEmail(),
importUser.getDeptId(), null);
@@ -451,7 +460,8 @@ public class AdminUserServiceImpl implements AdminUserService {
respVO.getFailureUsernames().put(importUser.getUsername(), ex.getMessage());
return;
}
- // 判断如果不存在,在进行插入
+
+ // 2.2.1 判断如果不存在,在进行插入
AdminUserDO existUser = userMapper.selectByUsername(importUser.getUsername());
if (existUser == null) {
userMapper.insert(BeanUtils.toBean(importUser, AdminUserDO.class)
@@ -459,7 +469,7 @@ public class AdminUserServiceImpl implements AdminUserService {
respVO.getCreateUsernames().add(importUser.getUsername());
return;
}
- // 如果存在,判断是否允许更新
+ // 2.2.2 如果存在,判断是否允许更新
if (!isUpdateSupport) {
respVO.getFailureUsernames().put(importUser.getUsername(), USER_USERNAME_EXISTS.getMsg());
return;
diff --git a/yudao-server/src/main/resources/application-local.yaml b/yudao-server/src/main/resources/application-local.yaml
index 5407d70429..49497d74c8 100644
--- a/yudao-server/src/main/resources/application-local.yaml
+++ b/yudao-server/src/main/resources/application-local.yaml
@@ -174,6 +174,7 @@ logging:
cn.iocoder.yudao.module.statistics.dal.mysql: debug
cn.iocoder.yudao.module.crm.dal.mysql: debug
cn.iocoder.yudao.module.erp.dal.mysql: debug
+ cn.iocoder.yudao.module.ai.dal.mysql: debug
org.springframework.context.support.PostProcessorRegistrationDelegate: ERROR # TODO 芋艿:先禁用,Spring Boot 3.X 存在部分错误的 WARN 提示
debug: false
@@ -195,10 +196,12 @@ wx:
miniapp: # 小程序配置(必填),参见 https://github.com/Wechat-Group/WxJava/blob/develop/spring-boot-starters/wx-java-miniapp-spring-boot-starter/README.md 文档
# appid: wx62056c0d5e8db250 # 测试号(牛希尧提供的)
# secret: 333ae72f41552af1e998fe1f54e1584a
- appid: wx63c280fe3248a3e7 # wenhualian的接口测试号
- secret: 6f270509224a7ae1296bbf1c8cb97aed
+# appid: wx63c280fe3248a3e7 # wenhualian的接口测试号
+# secret: 6f270509224a7ae1296bbf1c8cb97aed
# appid: wxc4598c446f8a9cb3 # 测试号(Kongdy 提供的)
# secret: 4a1a04e07f6a4a0751b39c3064a92c8b
+ appid: wx66186af0759f47c9 # 测试号(puhui 提供的)
+ secret: 3218bcbd112cbc614c7264ceb20144ac
config-storage:
type: RedisTemplate # 采用 RedisTemplate 操作 Redis,会自动从 Spring 中获取
key-prefix: wa # Redis Key 的前缀
@@ -220,6 +223,8 @@ yudao:
demo: false # 关闭演示模式
wxa-code:
env-version: develop # 小程序版本: 正式版为 "release";体验版为 "trial";开发版为 "develop"
+ wxa-subscribe-message:
+ miniprogram-state: developer # 跳转小程序类型:开发版为 “developer”;体验版为 “trial”为;正式版为 “formal”
tencent-lbs-key: TVDBZ-TDILD-4ON4B-PFDZA-RNLKH-VVF6E # QQ 地图的密钥 https://lbs.qq.com/service/staticV2/staticGuide/staticDoc
justauth: