Browse Source

防止直播订单重复提交

yuhongqi 2 days ago
parent
commit
bd807c7794
22 changed files with 630 additions and 24 deletions
  1. 5 1
      fs-admin/src/main/java/com/fs/hisStore/task/MallStoreTask.java
  2. 111 0
      fs-admin/src/main/java/com/fs/task/MiniProgramSubTask.java
  3. 6 3
      fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java
  4. 2 1
      fs-service/src/main/java/com/fs/live/mapper/LiveOrderMapper.java
  5. 8 12
      fs-service/src/main/java/com/fs/live/service/impl/LiveAfterSalesServiceImpl.java
  6. 9 3
      fs-service/src/main/java/com/fs/live/service/impl/LiveOrderServiceImpl.java
  7. 10 2
      fs-service/src/main/java/com/fs/live/service/impl/LiveServiceImpl.java
  8. 1 2
      fs-service/src/main/java/com/fs/live/service/impl/LiveWatchUserServiceImpl.java
  9. 1 0
      fs-service/src/main/java/com/fs/live/vo/FsMyLiveOrderListQueryVO.java
  10. 1 0
      fs-service/src/main/java/com/fs/live/vo/LiveDataDetailVo.java
  11. 1 0
      fs-service/src/main/java/com/fs/live/vo/LiveUserDetailVo.java
  12. 1 0
      fs-service/src/main/java/com/fs/live/vo/ProductSalesVo.java
  13. 43 0
      fs-service/src/main/java/com/fs/store/dto/ClientCredGrantReqDTO.java
  14. 52 0
      fs-service/src/main/java/com/fs/store/dto/MiniGramSubsMsgResultDTO.java
  15. 68 0
      fs-service/src/main/java/com/fs/store/dto/TemplateMessageSendRequestDTO.java
  16. 36 0
      fs-service/src/main/java/com/fs/store/dto/WeXinAccessTokenDTO.java
  17. 44 0
      fs-service/src/main/java/com/fs/store/enums/MiniAppNotifyTaskStatusEnum.java
  18. 28 0
      fs-service/src/main/java/com/fs/store/service/IWechatMiniProgrService.java
  19. 29 0
      fs-service/src/main/java/com/fs/store/service/impl/IWechatMiniProgrServiceImpl.java
  20. 95 0
      fs-service/src/main/java/com/fs/store/utils/MiniProgramHttp.java
  21. 43 0
      fs-service/src/main/java/com/fs/store/vo/ClientCredGrantReqDTO.java
  22. 36 0
      fs-service/src/main/java/com/fs/store/vo/WeXinAccessTokenDTO.java

+ 5 - 1
fs-admin/src/main/java/com/fs/hisStore/task/MallStoreTask.java

@@ -3,6 +3,7 @@ package com.fs.hisStore.task;
 
 import cn.hutool.core.util.StrUtil;
 import cn.hutool.json.JSONUtil;
+import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.utils.DateUtils;
 import com.fs.company.service.ICompanyService;
@@ -174,7 +175,10 @@ public class MallStoreTask
         // 单个异常影响全部,跳过异常单子
         for (Long id : ids) {
             try {
-                fsStoreOrderService.createOmsOrder(id);
+                R omsOrder = fsStoreOrderService.createOmsOrder(id);
+                if ("500".equals(omsOrder.get("code"))) {
+
+                }
             } catch (Exception e) {
                 log.error("创建商城oms订单失败:"+id);
                 log.error("创建商城oms订单失败:"+e.getMessage());

+ 111 - 0
fs-admin/src/main/java/com/fs/task/MiniProgramSubTask.java

@@ -0,0 +1,111 @@
+package com.fs.task;
+
+import cn.hutool.core.util.ObjectUtil;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.TypeReference;
+import com.fs.live.domain.LiveMiniprogramSubNotifyTask;
+import com.fs.live.mapper.LiveMiniprogramSubNotifyTaskMapper;
+import com.fs.store.enums.MiniAppNotifyTaskStatusEnum;
+import com.fs.store.service.IWechatMiniProgrService;
+import com.fs.store.dto.ClientCredGrantReqDTO;
+import com.fs.store.dto.MiniGramSubsMsgResultDTO;
+import com.fs.store.dto.TemplateMessageSendRequestDTO;
+import com.fs.store.dto.WeXinAccessTokenDTO;
+import com.fs.wx.miniapp.config.WxMaProperties;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections.CollectionUtils;
+import org.apache.commons.lang.exception.ExceptionUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 小程序订阅通知定时任务
+ */
+@Service("miniProgramSubTask")
+@Slf4j
+@RequiredArgsConstructor
+public class MiniProgramSubTask {
+    private final IWechatMiniProgrService wechatMiniProgrService;
+
+    private final LiveMiniprogramSubNotifyTaskMapper notifyTaskMapper;
+
+    private WxMaProperties.Config config = null;
+
+    @Autowired
+    public void setConfig(WxMaProperties properties) {
+        if(ObjectUtil.isNotNull(properties)){
+            this.config = properties.getConfigs().get(0);
+        }
+    }
+
+
+    /**
+     * 小程序订阅通知
+     */
+    public void notifyMiniLiveAppSub(){
+        log.info("小程序直播订阅通知定时任务");
+        // 先获取所有可用待处理任务
+        List<LiveMiniprogramSubNotifyTask> pendingData = notifyTaskMapper.selectLivePendingData();
+        if(CollectionUtils.isEmpty(pendingData)){
+            log.info("小程序直播阅通知定时任务, 无待处理数据");
+            return;
+        }
+        LocalDateTime now = LocalDateTime.now();
+        for (LiveMiniprogramSubNotifyTask pendingDatum : pendingData) {
+
+            if(pendingDatum.getUpdateTime().isAfter(now)) continue;
+
+            pendingDatum.setUpdateTime(LocalDateTime.now());
+
+            ClientCredGrantReqDTO clientCredGrantReqDTO = new ClientCredGrantReqDTO();
+            clientCredGrantReqDTO.setAppid(config.getAppid());
+            clientCredGrantReqDTO.setSecret(config.getSecret());
+            clientCredGrantReqDTO.setGrant_type("client_credential");
+
+            try{
+                // 获取accessToken
+                WeXinAccessTokenDTO stableToken = wechatMiniProgrService
+                        .getStableToken(clientCredGrantReqDTO);
+
+                String accessToken = stableToken.getAccessToken();
+
+                // 调用微信小程序订阅通知
+                TemplateMessageSendRequestDTO sendRequestDTO = new TemplateMessageSendRequestDTO();
+                sendRequestDTO.setTemplate_id(pendingDatum.getTemplateId());
+                sendRequestDTO.setTouser(pendingDatum.getTouser());
+                sendRequestDTO.setPage(pendingDatum.getPage());
+                TypeReference<Map<String, TemplateMessageSendRequestDTO.TemplateDataValue>> typeReference = new TypeReference<Map<String, TemplateMessageSendRequestDTO.TemplateDataValue>>() {};
+                sendRequestDTO.setData(JSON.parseObject(pendingDatum.getData(),typeReference));
+                MiniGramSubsMsgResultDTO miniGramSubsMsgResultDTO = wechatMiniProgrService.sendSubscribeMsg(accessToken, sendRequestDTO);
+                pendingDatum.setRequestBody(JSON.toJSONString(sendRequestDTO));
+                pendingDatum.setResponseBody(JSON.toJSONString(miniGramSubsMsgResultDTO));
+
+                // 如果推送消息成功
+                if(miniGramSubsMsgResultDTO.getErrcode() == 0){
+                    pendingDatum.setStatus(MiniAppNotifyTaskStatusEnum.SUCCESS.getValue());
+                } else {
+                    // 更新任务状态为执行失败
+                    pendingDatum.setStatus(MiniAppNotifyTaskStatusEnum.FAILED.getValue());
+                    pendingDatum.setErrorMessage(JSON.toJSONString(miniGramSubsMsgResultDTO));
+                    pendingDatum.setRetryCount(pendingDatum.getRetryCount() +1);
+                }
+            }catch (Throwable e){
+                // 更新任务状态为执行失败
+                pendingDatum.setStatus(MiniAppNotifyTaskStatusEnum.FAILED.getValue());
+                pendingDatum.setErrorMessage(ExceptionUtils.getStackTrace(e));
+                pendingDatum.setRetryCount(pendingDatum.getRetryCount() +1);
+                log.error("小程序直播订阅通知定时任务异常: {}", ExceptionUtils.getStackTrace(e));
+            }
+        }
+
+        if(CollectionUtils.isNotEmpty(pendingData)){
+            notifyTaskMapper.updateBatchById(pendingData);
+        }
+
+    }
+}

+ 6 - 3
fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java

@@ -146,7 +146,7 @@ public class WebSocketServer {
                 redisCache.incr(UNIQUE_VIEWERS_KEY + liveId, 1);
             }
             liveWatchUserVO.setMsgStatus(liveWatchUserVO.getMsgStatus());
-            if (1 == random.nextInt(4)) {
+            if (1 == random.nextInt(10)) {
                 SendMsgVo sendMsgVo = new SendMsgVo();
                 sendMsgVo.setLiveId(liveId);
                 sendMsgVo.setUserId(userId);
@@ -232,7 +232,7 @@ public class WebSocketServer {
 
 
             // 广播离开消息 添加一个概率问题 摇塞子,1-4 当为1的时候广播消息
-            if (1 == new Random().nextInt(4)) {
+            if (1 == new Random().nextInt(10)) {
                 SendMsgVo sendMsgVo = new SendMsgVo();
                 sendMsgVo.setLiveId(liveId);
                 sendMsgVo.setUserId(userId);
@@ -759,7 +759,7 @@ public class WebSocketServer {
         ConcurrentHashMap<Long, Session> room = getRoom(liveId);
         room.forEach((k, v) -> {
             if (v.isOpen()) {
-                sendWithRetry(v,message,7);
+                sendWithRetry(v,message,1);
             }
         });
     }
@@ -855,6 +855,9 @@ public class WebSocketServer {
                 }
                 LiveCouponIssue liveCouponIssue = liveCouponIssueService.selectLiveCouponIssueByCouponId(liveCoupon.getCouponId());
                 LiveCouponIssueRelation relation = liveCouponMapper.selectCouponRelation(task.getLiveId(), liveCouponIssue.getId());
+                if (liveCoupon != null) {
+                    redisCache.setCacheObject(String.format(LiveKeysConstant.LIVE_COUPON_NUM , liveCouponIssue.getId()), liveCouponIssue.getRemainCount().intValue(), 30, TimeUnit.MINUTES);
+                }
                 HashMap<String, Object> data = new HashMap<>();
                 data.put("liveId", task.getLiveId());
                 data.put("couponIssueId", liveCouponIssue.getId());

+ 2 - 1
fs-service/src/main/java/com/fs/live/mapper/LiveOrderMapper.java

@@ -370,7 +370,8 @@ public interface LiveOrderMapper {
     int batchUpdateErpByOrderIds(@Param("maps")ArrayList<Map<String, String>> maps);
 
     @Select({"<script> " +
-            "select o.order_id,o.total_num,o.create_time, o.discount_money ,o.live_id,o.order_code,o.item_json,o.pay_price,o.status,o.delivery_sn as delivery_id,o.finish_time  from live_order o  " +
+            "select a.id as afterSalesId,o.order_id,o.total_num,o.create_time, o.discount_money ,o.live_id,o.order_code,o.item_json,o.pay_price,o.status,o.delivery_sn as delivery_id,o.finish_time  from live_order o  " +
+            " left join ( SELECT t.*,ROW_NUMBER() OVER (PARTITION BY t.order_id ORDER BY t.create_time DESC) AS rn FROM live_after_sales t ) a ON o.order_id = a.order_id AND a.rn = 1 " +
             "where o.is_del=0 " +
             "<if test = 'maps.status != null and maps.status != \"\"     '> " +
             "and o.status =#{maps.status} " +

+ 8 - 12
fs-service/src/main/java/com/fs/live/service/impl/LiveAfterSalesServiceImpl.java

@@ -317,6 +317,7 @@ public class LiveAfterSalesServiceImpl implements ILiveAfterSalesService {
                     LiveOrder orderMap=new LiveOrder();
                     orderMap.setOrderId(order.getOrderId());
                     orderMap.setOrderCode(orderSn);
+                    orderMap.setStatus(order.getStatus());
                     liveOrderService.updateLiveOrder(orderMap);
                     liveOrderItemMapper.updateFsStoreOrderCode(order.getOrderId(),orderSn);
                     try {
@@ -426,6 +427,7 @@ public class LiveAfterSalesServiceImpl implements ILiveAfterSalesService {
         storeAfterSales.setSalesStatus(0);
         storeAfterSales.setCreateTime(Timestamp.valueOf(LocalDateTime.now()));
         storeAfterSales.setIsDel(0);
+        storeAfterSales.setOrderStatus(orderStatus);
         storeAfterSales.setUserId(Long.valueOf(userId));
         storeAfterSales.setOrderStatus(orderStatus);
         storeAfterSales.setCompanyId(order.getCompanyId());
@@ -459,16 +461,9 @@ public class LiveAfterSalesServiceImpl implements ILiveAfterSalesService {
         request.setRefund_state(1);
         request.setStoreAfterSalesId(storeAfterSales.getId());
         if (StringUtils.isNotBlank(order.getExtendOrderId())){
-            ErpOrderQueryRequert queryRequest = new ErpOrderQueryRequert();
-            queryRequest.setCode(order.getExtendOrderId());
-            ErpOrderQueryResponse response = erpOrderService.getLiveOrder(queryRequest);
-            if (response.getOrders() != null && response.getOrders().size() > 0) {
-                if (response.getOrders().get(0).getCancle() != null && !response.getOrders().get(0).getCancle()) {
-                    BaseResponse res = erpOrderService.refundUpdateLive(request);
-                    if(res.getSuccess()){
-                        return R.ok();
-                    }
-                }
+            BaseResponse response=erpOrderService.refundUpdateLive(request);
+            if(response.getSuccess()){
+                return R.ok();
             }
             else{
                 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
@@ -913,7 +908,7 @@ public class LiveAfterSalesServiceImpl implements ILiveAfterSalesService {
         logs.setStoreAfterSalesId(storeAfterSales.getId());
         logs.setChangeMessage(OrderInfoEnum.REFUND_STATUS_1.getDesc());
         liveAfterSalesLogsMapper.insertLiveAfterSalesLogs(logs);
-        if (storeAfterSales.getOrderStatus().equals(2)) {
+        if (storeAfterSales.getOrderStatus().equals(1)) {
             if (StringUtils.isNotEmpty(order.getExtendOrderId())) {
                 //更新订单code
                 String orderSn = IdUtil.getSnowflake(0, 0).nextIdStr();
@@ -923,6 +918,7 @@ public class LiveAfterSalesServiceImpl implements ILiveAfterSalesService {
                 LiveOrder orderMap = new LiveOrder();
                 orderMap.setOrderId(order.getOrderId());
                 orderMap.setOrderCode(orderSn);
+                orderMap.setStatus(order.getStatus());
                 liveOrderService.updateLiveOrder(orderMap);
                 //生成新的订单
                 List<LiveOrderPayment> payments = liveOrderPaymentMapper.selectLiveOrderPaymentByPay(5, order.getOrderId());
@@ -934,7 +930,7 @@ public class LiveAfterSalesServiceImpl implements ILiveAfterSalesService {
                 }
 
                 try {
-                    if (liveOrderPaymentMapper.selectByBuissnessId(Long.valueOf(order.getOrderCode())) != null) {
+                    if (liveOrderPaymentMapper.selectByBuissnessId(order.getOrderId()) != null) {
                         liveOrderService.createOmsOrder(order.getOrderId());
                     }
                 } catch (Exception e) {

+ 9 - 3
fs-service/src/main/java/com/fs/live/service/impl/LiveOrderServiceImpl.java

@@ -2631,6 +2631,12 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
     {
         liveOrder.setUpdateTime(DateUtils.getNowDate());
         liveUserLotteryRecordMapper.updateOrderStatusByOrderId(liveOrder.getOrderId(), liveOrder.getStatus());
+        //推送修改的商城订单地址到聚水潭ERP
+        try {
+            pushOrderAddressToErp(liveOrder);
+        }catch (Exception e){
+            log.error("修改商城订单地址推送到聚水潭ERP失败,orderId: {}", liveOrder.getOrderId(), e);
+        }
         return baseMapper.updateLiveOrder(liveOrder);
     }
 
@@ -3566,9 +3572,9 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
                     dto.setBarCode(fsStoreProductAttrValue.getBarCode());
                     dto.setGroupBarCode(fsStoreProductAttrValue.getGroupBarCode());
                 }
-                if (fsStoreProductAttrValue != null) {
-                    dto.setBarCode(fsStoreProductAttrValue.getBarCode());
-                    dto.setGroupBarCode(fsStoreProductAttrValue.getGroupBarCode());
+                if (attrValue != null) {
+                    dto.setBarCode(attrValue.getBarCode());
+                    dto.setGroupBarCode(attrValue.getGroupBarCode());
                 }
                 dto.setPrice(fsStoreProduct.getPrice());
                 dto.setProductName(fsStoreProduct.getProductName());

+ 10 - 2
fs-service/src/main/java/com/fs/live/service/impl/LiveServiceImpl.java

@@ -290,10 +290,18 @@ public class LiveServiceImpl implements ILiveService
                 .eq(FsUserWx::getFsUserId, userId)
                 .eq(FsUserWx::getAppId, StringUtils.isEmpty(param.getAppId()) ? "wx44beed5640bcb1ba" : param.getAppId()); // 卓美小程序
         FsUserWx fsUserWx = fsUserWxMapper.selectOne(queryWrapper);
-        String maOpenId = fsUserWx.getOpenId();
-        if (StringUtils.isEmpty(maOpenId)) {
+        String maOpenId = "";
+        if (fsUserWx == null) {
             maOpenId = param.getMaOpenId();
+        }else {
+            maOpenId = fsUserWx.getOpenId();
+        }
+
+        if (StringUtils.isEmpty(maOpenId)) {
+            log.error("用户没有绑定微信,无法发送预约提醒:{}", userId);
+            return R.ok();
         }
+
         notifyTask.setTouser(maOpenId);
         notifyTask.setPage(String.valueOf(1));
 

+ 1 - 2
fs-service/src/main/java/com/fs/live/service/impl/LiveWatchUserServiceImpl.java

@@ -201,13 +201,13 @@ public class LiveWatchUserServiceImpl implements ILiveWatchUserService {
 
     @Override
     public LiveWatchUser join(FsUserScrm fsUser,long liveId, long userId, String location) {
-        Date now = DateUtils.getNowDate();
 
         // 查询直播间信息
         Live live = liveMapper.selectLiveByLiveId(liveId);
         if (live == null) {
             throw new RuntimeException("直播间不存在");
         }
+        Date now = DateUtils.getNowDate();
 
         // 获取直播/回放状态(带缓存)
         Map<String, Integer> flagMap = getLiveFlagWithCache(liveId);
@@ -429,7 +429,6 @@ public class LiveWatchUserServiceImpl implements ILiveWatchUserService {
         liveWatchUser.setLiveId(liveId);
         List<LiveWatchUserVO> liveWatchUserVOS = selectOnlineUserList(liveWatchUser);
 
-        log.info("开始同步直播在线人数到缓存,共{}条数据", liveWatchUserVOS.size());
         if (CollUtil.isNotEmpty(liveWatchUserVOS)){
             ThreadUtil.execute(()->{
                 String hashKey  = String.format(LiveKeysConstant.LIVE_WATCH_USERS, liveId);

+ 1 - 0
fs-service/src/main/java/com/fs/live/vo/FsMyLiveOrderListQueryVO.java

@@ -26,6 +26,7 @@ public class FsMyLiveOrderListQueryVO implements Serializable
     private Long id;
     private Long orderId;
     private Long liveId;
+    private Long afterSalesId;
 
     /** 订单号 */
     private String orderCode;

+ 1 - 0
fs-service/src/main/java/com/fs/live/vo/LiveDataDetailVo.java

@@ -98,3 +98,4 @@ public class LiveDataDetailVo {
 
 
 
+

+ 1 - 0
fs-service/src/main/java/com/fs/live/vo/LiveUserDetailVo.java

@@ -40,3 +40,4 @@ public class LiveUserDetailVo {
 
 
 
+

+ 1 - 0
fs-service/src/main/java/com/fs/live/vo/ProductSalesVo.java

@@ -28,3 +28,4 @@ public class ProductSalesVo {
 
 
 
+

+ 43 - 0
fs-service/src/main/java/com/fs/store/dto/ClientCredGrantReqDTO.java

@@ -0,0 +1,43 @@
+package com.fs.store.dto;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 客户端凭证授权请求DTO
+ * <p>
+ * 用于构建客户端凭证授权模式下的请求参数。
+ * </p>
+ *
+ * @author xdd
+ * @version 1.0
+ * @since 2025-02-27
+ */
+@Data
+public class ClientCredGrantReqDTO implements Serializable {
+
+    /**
+     * 授权类型
+     * <p>
+     * 固定值 "client_credential",表示客户端凭证授权模式。
+     * </p>
+     */
+    private String grant_type;
+
+    /**
+     * 应用ID
+     * <p>
+     * 应用程序的唯一标识符。
+     * </p>
+     */
+    private String appid;
+
+    /**
+     * 应用密钥
+     * <p>
+     * 应用程序的密钥,用于验证请求的合法性。  <b>注意:应妥善保管,避免泄露。</b>
+     * </p>
+     */
+    private String secret;
+}

+ 52 - 0
fs-service/src/main/java/com/fs/store/dto/MiniGramSubsMsgResultDTO.java

@@ -0,0 +1,52 @@
+package com.fs.store.dto;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 消息发送结果DTO
+ * <p>
+ * 用于封装消息发送接口的响应结果。
+ * </p>
+ *
+ * @author xdd
+ * @version 1.0
+ * @since 2025-02-27
+ */
+@Data
+public class MiniGramSubsMsgResultDTO implements Serializable {
+
+    /**
+     * 错误码
+     * <p>
+     * 返回码,0表示成功,其他值表示失败。
+     * </p>
+     */
+    private Integer errcode;
+
+    /**
+     * 错误信息
+     * <p>
+     * 返回码的文本描述,成功时为 "ok",失败时包含具体的错误信息。
+     * </p>
+     */
+    private String errmsg;
+
+    /**
+     * 消息ID
+     * <p>
+     * 消息的唯一标识符,成功发送时返回。
+     * </p>
+     *  <p>
+     *     注意:这个字段可能为null,发送失败时,此字段可能为null
+     *  </p>
+     */
+    private Long msgid;
+
+    /**
+     * rid  请求的唯一标识
+     * 仅在发生错误时出现
+     */
+    private String rid;
+}

+ 68 - 0
fs-service/src/main/java/com/fs/store/dto/TemplateMessageSendRequestDTO.java

@@ -0,0 +1,68 @@
+package com.fs.store.dto;
+
+import lombok.Data;
+
+import java.util.Map;
+
+/**
+ * 模板消息发送请求DTO
+ * <p>
+ * 用于构建发送模板消息的请求体。
+ * </p>
+ *
+ * @author xdd
+ * @version 1.0
+ * @since 2025-02-27
+ */
+@Data
+public class TemplateMessageSendRequestDTO {
+
+    /**
+     * 接收者openid
+     * <p>
+     * 用户的唯一标识符。
+     * </p>
+     */
+    private String touser;
+
+    /**
+     * 模板ID
+     * <p>
+     * 所需下发的模板消息的id。
+     * </p>
+     */
+    private String template_id;
+
+    /**
+     * 跳转页面
+     * <p>
+     * 点击模板消息后跳转的页面,可以为空。
+     * </p>
+     */
+    private String page;
+
+    /**
+     * 模板数据
+     * <p>
+     * 模板内容,键值对形式,键名为模板中的变量名,值为要替换的内容。
+     * </p>
+     */
+    private Map<String, TemplateDataValue> data;
+
+    /**
+     * 模板数据值对象
+     * <p>
+     * 内部类,用于表示模板数据中的单个值。
+     * </p>
+     */
+    @Data
+    public static class TemplateDataValue {
+        /**
+         * 模板变量值
+         * <p>
+         * 要替换模板变量的具体内容。
+         * </p>
+         */
+        private String value;
+    }
+}

+ 36 - 0
fs-service/src/main/java/com/fs/store/dto/WeXinAccessTokenDTO.java

@@ -0,0 +1,36 @@
+package com.fs.store.dto;
+
+import lombok.Data;
+
+/**
+ * 访问令牌DTO
+ * <p>
+ * 用于存储从认证服务器获取的访问令牌及其相关信息。
+ * </p>
+ *
+ * @author xdd
+ * @version 1.0
+ * @since 2025-02-27
+ */
+@Data
+public class WeXinAccessTokenDTO {
+
+    /**
+     * 访问令牌
+     * <p>
+     * 用于访问受保护资源的令牌。
+     * </p>
+     */
+    private String accessToken;
+
+    /**
+     * 过期时间(秒)
+     * <p>
+     * 访问令牌的有效时间,单位为秒。
+     * </p>
+     */
+    private Integer expiresIn;
+
+    private Long errcode;
+    private String errmsg;
+}

+ 44 - 0
fs-service/src/main/java/com/fs/store/enums/MiniAppNotifyTaskStatusEnum.java

@@ -0,0 +1,44 @@
+package com.fs.store.enums;
+
+
+import lombok.Getter;
+
+@Getter
+public enum MiniAppNotifyTaskStatusEnum {
+    /**
+     * 待执行
+     */
+    WAITING(0),
+    /**
+     * 执行中
+     */
+    RUNNING(1),
+    /**
+     * 执行成功
+     */
+    SUCCESS(2),
+    /**
+     * 执行失败
+     */
+    FAILED(3),
+    /**
+     * 已取消
+     */
+    CANCELED(4);
+
+    private final int value;
+
+    MiniAppNotifyTaskStatusEnum(int value) {
+        this.value = value;
+    }
+
+
+    public static MiniAppNotifyTaskStatusEnum fromValue(int value) {
+        for (MiniAppNotifyTaskStatusEnum status : values()) {
+            if (status.getValue() == value) {
+                return status;
+            }
+        }
+        throw new IllegalArgumentException("Invalid value: " + value);
+    }
+}

+ 28 - 0
fs-service/src/main/java/com/fs/store/service/IWechatMiniProgrService.java

@@ -0,0 +1,28 @@
+package com.fs.store.service;
+
+
+import com.fs.store.dto.ClientCredGrantReqDTO;
+import com.fs.store.dto.MiniGramSubsMsgResultDTO;
+import com.fs.store.dto.TemplateMessageSendRequestDTO;
+import com.fs.store.dto.WeXinAccessTokenDTO;
+
+/**
+ * 小程序调用相关
+ */
+public interface IWechatMiniProgrService {
+    /**
+     * 获取稳定的token
+     *
+     * @param param 请求参数
+     * @return {@link com.fs.store.dto.WeXinAccessTokenDTO}
+     */
+    WeXinAccessTokenDTO getStableToken(ClientCredGrantReqDTO param);
+
+    /**
+     * 微信小程序发送订阅消息
+     *
+     * @param param 请求参数
+     * @return {@link MiniGramSubsMsgResultDTO}
+     */
+    MiniGramSubsMsgResultDTO sendSubscribeMsg(String accessToken, TemplateMessageSendRequestDTO param);
+}

+ 29 - 0
fs-service/src/main/java/com/fs/store/service/impl/IWechatMiniProgrServiceImpl.java

@@ -0,0 +1,29 @@
+package com.fs.store.service.impl;
+
+import com.fs.store.dto.ClientCredGrantReqDTO;
+import com.fs.store.dto.MiniGramSubsMsgResultDTO;
+import com.fs.store.dto.TemplateMessageSendRequestDTO;
+import com.fs.store.dto.WeXinAccessTokenDTO;
+import com.fs.store.service.IWechatMiniProgrService;
+import com.fs.store.utils.MiniProgramHttp;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+@RequiredArgsConstructor
+@Service
+@Slf4j
+public class IWechatMiniProgrServiceImpl implements IWechatMiniProgrService {
+
+    private final MiniProgramHttp miniProgramHttp;
+
+    @Override
+    public WeXinAccessTokenDTO getStableToken(ClientCredGrantReqDTO param) {
+        return miniProgramHttp.getStableAccessToken(param);
+    }
+
+    @Override
+    public MiniGramSubsMsgResultDTO sendSubscribeMsg(String accessToken,TemplateMessageSendRequestDTO param) {
+        return miniProgramHttp.sendSubscribeMessage(accessToken,param);
+    }
+}

+ 95 - 0
fs-service/src/main/java/com/fs/store/utils/MiniProgramHttp.java

@@ -0,0 +1,95 @@
+package com.fs.store.utils;
+
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.http.HttpRequest;
+import cn.hutool.http.HttpUtil;
+import com.alibaba.fastjson.JSONObject;
+import com.fs.store.dto.ClientCredGrantReqDTO;
+import com.fs.store.dto.MiniGramSubsMsgResultDTO;
+import com.fs.store.dto.TemplateMessageSendRequestDTO;
+import com.fs.store.dto.WeXinAccessTokenDTO;
+import com.hc.openapi.tool.fastjson.JSON;
+import com.hc.openapi.tool.util.StringUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+@Component
+@Slf4j
+public class MiniProgramHttp {
+
+    /**
+     * 微信小程序-发送订阅消息地址
+     */
+    private static final String BASE_URL = "https://api.weixin.qq.com/cgi-bin/message/subscribe/send";
+
+    /**
+     * 微信小程序-获取accessToken地址
+     */
+    private static final String TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/stable_token";
+
+
+    /**
+     * 发送微信订阅消息 (使用 Hutool)
+     * @param accessToken token
+     * @param param 请求数据
+     * @return String
+     */
+    public MiniGramSubsMsgResultDTO sendSubscribeMessage(String accessToken, TemplateMessageSendRequestDTO param) {
+        String url = BASE_URL + "?access_token=" + accessToken;
+
+        log.info("发送小程序订阅消息, 请求 URL: {}", url);
+
+        String requestBody = JSON.toJSONString(param);
+        log.info("发送小程序订阅消息, 请求参数: {}", requestBody);
+
+        try {
+            String response = HttpUtil.post(url, requestBody);
+            log.info("发送小程序订阅消息, HTTP 请求 URL: {}", url);
+            log.info("发送小程序订阅消息, HTTP 请求体: {}", requestBody);
+            log.info("发送小程序订阅消息, HTTP 响应: {}", response);
+
+            MiniGramSubsMsgResultDTO result = JSONObject.parseObject(response, MiniGramSubsMsgResultDTO.class);
+            if(ObjectUtil.notEqual(result.getErrcode(),0)){
+                throw new RuntimeException("发送小程序订阅消息失败, " + result.getErrmsg());
+            }
+            log.info("发送小程序订阅消息, 解析结果: {}", JSON.toJSONString(result));
+            return result;
+
+        } catch (Exception e) {
+            log.error("发送小程序订阅消息失败: {}", e.getMessage());
+            throw e;
+        }
+    }
+
+
+    /**
+     * 获取微信 Stable Access Token
+     * @return WeXinAccessTokenDTO
+     */
+    public WeXinAccessTokenDTO getStableAccessToken(ClientCredGrantReqDTO param) {
+        String requestBody = JSONObject.toJSONString(param);
+        log.info("获取微信 Stable Access Token, 请求参数: {}", requestBody); // 打印请求参数
+
+        try {
+            String responseJson =
+            HttpRequest.post(TOKEN_URL).contentType("application/json").body(requestBody).execute().body();
+            log.info("获取微信 Stable Access Token, HTTP 请求 URL: {}", TOKEN_URL);
+            log.info("获取微信 Stable Access Token, HTTP 请求体: {}", requestBody);
+            log.info("获取微信 Stable Access Token, HTTP 响应: {}", responseJson);
+
+            if(StringUtils.isBlank(responseJson)){
+                throw new RuntimeException("获取微信 Stable Access Token 失败,response为空");
+            }
+            WeXinAccessTokenDTO result = JSONObject.parseObject(responseJson, WeXinAccessTokenDTO.class);
+            log.info("获取微信 Stable Access Token, 解析结果: {}", JSONObject.toJSONString(result)); //记录解析结果
+            if(result == null || StringUtils.isBlank(result.getAccessToken())){
+                throw new RuntimeException("获取微信 Stable Access Token 失败, accessToken为空");
+            }
+            return result;
+
+        } catch (Exception e) {
+            log.error("获取微信 Stable Access Token 失败", e);
+            throw e;
+        }
+    }
+}

+ 43 - 0
fs-service/src/main/java/com/fs/store/vo/ClientCredGrantReqDTO.java

@@ -0,0 +1,43 @@
+package com.fs.store.vo;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 客户端凭证授权请求DTO
+ * <p>
+ * 用于构建客户端凭证授权模式下的请求参数。
+ * </p>
+ *
+ * @author xdd
+ * @version 1.0
+ * @since 2025-02-27
+ */
+@Data
+public class ClientCredGrantReqDTO implements Serializable {
+
+    /**
+     * 授权类型
+     * <p>
+     * 固定值 "client_credential",表示客户端凭证授权模式。
+     * </p>
+     */
+    private String grant_type;
+
+    /**
+     * 应用ID
+     * <p>
+     * 应用程序的唯一标识符。
+     * </p>
+     */
+    private String appid;
+
+    /**
+     * 应用密钥
+     * <p>
+     * 应用程序的密钥,用于验证请求的合法性。  <b>注意:应妥善保管,避免泄露。</b>
+     * </p>
+     */
+    private String secret;
+}

+ 36 - 0
fs-service/src/main/java/com/fs/store/vo/WeXinAccessTokenDTO.java

@@ -0,0 +1,36 @@
+package com.fs.store.vo;
+
+import lombok.Data;
+
+/**
+ * 访问令牌DTO
+ * <p>
+ * 用于存储从认证服务器获取的访问令牌及其相关信息。
+ * </p>
+ *
+ * @author xdd
+ * @version 1.0
+ * @since 2025-02-27
+ */
+@Data
+public class WeXinAccessTokenDTO {
+
+    /**
+     * 访问令牌
+     * <p>
+     * 用于访问受保护资源的令牌。
+     * </p>
+     */
+    private String accessToken;
+
+    /**
+     * 过期时间(秒)
+     * <p>
+     * 访问令牌的有效时间,单位为秒。
+     * </p>
+     */
+    private Integer expiresIn;
+
+    private Long errcode;
+    private String errmsg;
+}