Explorar o código

添加小程序订阅通知定时任务

xdd hai 1 mes
pai
achega
b0e0d2e6c1

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

@@ -0,0 +1,106 @@
+package com.fs.task;
+
+import cn.hutool.core.util.ObjectUtil;
+import com.alibaba.fastjson.JSONObject;
+import com.alibaba.fastjson.TypeReference;
+import com.fs.store.domain.FsMiniprogramSubNotifyTask;
+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.enums.MiniAppNotifyTaskStatus;
+import com.fs.store.mapper.FsMiniprogramSubNotifyTaskMapper;
+import com.fs.store.service.IWechatMiniProgrService;
+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 FsMiniprogramSubNotifyTaskMapper notifyTaskMapper;
+
+    private WxMaProperties.Config config = null;
+
+    @Autowired
+    public void setConfig(WxMaProperties properties) {
+        if(ObjectUtil.isNotNull(properties)){
+            this.config = properties.getConfigs().get(0);
+        }
+    }
+
+    /**
+     * 小程序订阅通知
+     */
+    public void notifyMiniAppSub(){
+        log.info("小程序订阅通知定时任务");
+        // 先获取所有可用待处理任务
+        List<FsMiniprogramSubNotifyTask> pendingData = notifyTaskMapper.selectPendingData();
+        if(CollectionUtils.isEmpty(pendingData)){
+            log.info("小程序订阅通知定时任务, 无待处理数据");
+            return;
+        }
+        for (FsMiniprogramSubNotifyTask pendingDatum : pendingData) {
+            pendingDatum.setUpdateTime(LocalDateTime.now());
+
+            ClientCredGrantReqDTO clientCredGrantReqDTO = new ClientCredGrantReqDTO();
+            clientCredGrantReqDTO.setAppid(config.getAppid());
+            clientCredGrantReqDTO.setSecret(config.getSecret());
+            clientCredGrantReqDTO.setGrantType("client_credential");
+
+           try{
+               // 获取accessToken
+               WeXinAccessTokenDTO stableToken = wechatMiniProgrService
+                       .getStableToken(clientCredGrantReqDTO);
+
+               String accessToken = stableToken.getAccessToken();
+
+               // 调用微信小程序订阅通知
+               TemplateMessageSendRequestDTO sendRequestDTO = new TemplateMessageSendRequestDTO();
+               sendRequestDTO.setTemplateId(pendingDatum.getTemplateId());
+               sendRequestDTO.setTouser(pendingDatum.getTouser());
+               sendRequestDTO.setPage(pendingDatum.getPage());
+               TypeReference<Map<String, TemplateMessageSendRequestDTO.TemplateDataValue>> typeReference = new TypeReference(){};
+               sendRequestDTO.setData(JSONObject.parseObject(pendingDatum.getData(),typeReference));
+               MiniGramSubsMsgResultDTO miniGramSubsMsgResultDTO = wechatMiniProgrService.sendSubscribeMsg(accessToken, sendRequestDTO);
+               pendingDatum.setRequestBody(JSONObject.toJSONString(sendRequestDTO));
+               pendingDatum.setResponseBody(JSONObject.toJSONString(miniGramSubsMsgResultDTO));
+
+               // 如果推送消息成功
+               if(miniGramSubsMsgResultDTO.getErrcode() == 0){
+                   pendingDatum.setStatus(MiniAppNotifyTaskStatus.SUCCESS.getValue());
+               } else {
+                   // 更新任务状态为执行失败
+                   pendingDatum.setStatus(MiniAppNotifyTaskStatus.FAILED.getValue());
+                   pendingDatum.setErrorMessage(JSONObject.toJSONString(miniGramSubsMsgResultDTO));
+                   pendingDatum.setRetryCount(pendingDatum.getRetryCount() +1);
+               }
+           }catch (Exception e){
+               // 更新任务状态为执行失败
+               pendingDatum.setStatus(MiniAppNotifyTaskStatus.FAILED.getValue());
+               pendingDatum.setErrorMessage(ExceptionUtils.getStackTrace(e));
+               pendingDatum.setRetryCount(pendingDatum.getRetryCount() +1);
+               log.error("小程序订阅通知定时任务异常: {}", ExceptionUtils.getStackTrace(e));
+           }
+        }
+
+        if(CollectionUtils.isNotEmpty(pendingData)){
+            notifyTaskMapper.updateBatchById(pendingData);
+        }
+
+    }
+}

+ 89 - 0
fs-service-system/src/main/java/com/fs/store/domain/FsMiniprogramSubNotifyTask.java

@@ -0,0 +1,89 @@
+package com.fs.store.domain;
+
+import lombok.Data;
+import com.alibaba.fastjson.JSONObject; //如果data字段是fastjson类型
+import java.time.LocalDateTime;
+
+/**
+ * 小程序订阅通知定时任务表
+ * @author
+ */
+@Data
+public class FsMiniprogramSubNotifyTask {
+
+    /**
+     * 任务ID,唯一标识
+     */
+    private Long id;
+
+    /**
+     * 任务名称,用于描述任务
+     */
+    private String taskName;
+
+    /**
+     * 微信小程序订阅消息模板ID
+     */
+    private String templateId;
+
+    /**
+     * 要发送的用户openid
+     */
+    private String touser;
+
+    /**
+     * 点击消息跳转的页面路径(可选)
+     */
+    private String page;
+
+    /**
+     * 消息内容,JSON格式。每个键值对对应模板中的一个变量
+     */
+    private String data;
+
+    /**
+     * 任务状态:0=待执行, 1=执行中, 2=执行成功, 3=执行失败, 4=已取消
+     */
+    private Integer status;
+
+    /**
+     * 当前重试次数
+     */
+    private Integer retryCount;
+
+    /**
+     * 最大重试次数
+     */
+    private Integer maxRetries;
+
+    /**
+     * 请求参数(JSON格式,主要记录 access_token 获取方式)
+     */
+    private String requestParams;
+
+
+    /**
+     * 完整的请求体 (JSON格式)
+     */
+    private String requestBody;
+
+    /**
+     * API 响应结果 (JSON格式)
+     */
+    private String responseBody;
+
+    /**
+     * 错误信息 (如果执行失败)
+     */
+    private String errorMessage;
+
+    /**
+     * 任务创建时间
+     */
+    private LocalDateTime createTime;
+
+    /**
+     * 最后更新时间
+     */
+    private LocalDateTime updateTime;
+}

+ 43 - 0
fs-service-system/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 grantType;
+
+    /**
+     * 应用ID
+     * <p>
+     * 应用程序的唯一标识符。
+     * </p>
+     */
+    private String appid;
+
+    /**
+     * 应用密钥
+     * <p>
+     * 应用程序的密钥,用于验证请求的合法性。  <b>注意:应妥善保管,避免泄露。</b>
+     * </p>
+     */
+    private String secret;
+}

+ 52 - 0
fs-service-system/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;
+}

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

@@ -0,0 +1,67 @@
+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 templateId;
+
+    /**
+     * 跳转页面
+     * <p>
+     * 点击模板消息后跳转的页面,可以为空。
+     * </p>
+     */
+    private String page;
+
+    /**
+     * 模板数据
+     * <p>
+     * 模板内容,键值对形式,键名为模板中的变量名,值为要替换的内容。
+     * </p>
+     */
+    private Map<String, TemplateDataValue> data;
+
+    /**
+     * 模板数据值对象
+     * <p>
+     * 内部类,用于表示模板数据中的单个值。
+     * </p>
+     */
+    @Data
+    public static class TemplateDataValue {
+        /**
+         * 模板变量值
+         * <p>
+         * 要替换模板变量的具体内容。
+         * </p>
+         */
+        private String value;
+    }
+}

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

@@ -0,0 +1,33 @@
+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;
+}

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

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

+ 75 - 0
fs-service-system/src/main/java/com/fs/store/mapper/FsMiniprogramSubNotifyTaskMapper.java

@@ -0,0 +1,75 @@
+package com.fs.store.mapper;
+
+import com.fs.store.domain.FsMiniprogramSubNotifyTask;
+import org.apache.ibatis.annotations.*;
+import com.alibaba.fastjson.JSONObject;
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * FsMiniprogramSubNotifyTaskMapper接口,用于定义微信小程序子通知任务的数据访问方法。
+ *
+ * @author xdd
+ * @version 1.0.0
+ * @since 2025-03-10
+ */
+@Mapper
+public interface FsMiniprogramSubNotifyTaskMapper {
+
+    /**
+     * 根据ID查询任务
+     * @param id 任务ID
+     * @return 任务实体
+     */
+    @Select("SELECT * FROM fs_miniprogram_sub_notify_task WHERE id = #{id}")
+    FsMiniprogramSubNotifyTask findById(Long id);
+
+    /**
+     * 插入新任务
+     * @param task 任务实体
+     * @return
+     */
+    @Insert("INSERT INTO fs_miniprogram_sub_notify_task (task_name, template_id, touser, page, `data`, status, " +
+            "retry_count, max_retries, request_params,request_body, response_body, error_message, create_time, update_time) " +
+            "VALUES (#{taskName}, #{templateId}, #{touser}, #{page}, #{data}, #{status}, #{retryCount}, " +
+            "#{maxRetries}, #{requestParams},#{requestBody}, #{responseBody}, #{errorMessage}, #{createTime}, #{updateTime})")
+    @Options(useGeneratedKeys = true, keyProperty = "id")
+    int insert(FsMiniprogramSubNotifyTask task);
+    /**
+     * 更新任务状态
+     *
+     * @param id     任务ID
+     * @param status 任务状态
+     * @param retryCount     重试次数
+     * @param responseBody     响应体
+     * @param errorMessage      错误信息
+     * @param updateTime     更新时间
+     * @return 受影响的行数
+     */
+    @Update("<script>" +
+            "UPDATE fs_miniprogram_sub_notify_task " +
+            "SET update_time = #{updateTime} " +
+            "<if test='status != null'>, status = #{status}</if>" +
+            "<if test='retryCount != null'>, retry_count = #{retryCount}</if>" +
+            "<if test='responseBody != null'>, response_body = #{responseBody}</if>" +
+            "<if test='errorMessage != null'>, error_message = #{errorMessage}</if>" +
+            "WHERE id = #{id}" +
+            "</script>")
+    int updateStatus(@Param("id") Long id, @Param("status") Integer status,
+                     @Param("retryCount") Integer retryCount,
+                     @Param("responseBody") String responseBody, @Param("errorMessage")String errorMessage,
+                     @Param("updateTime") LocalDateTime updateTime);
+
+    /**
+     * 查询所有待处理数据
+     * @return
+     */
+    @Select("SELECT * FROM fs_miniprogram_sub_notify_task WHERE retry_count<3 and status in (0,3)")
+    List<FsMiniprogramSubNotifyTask> selectPendingData();
+
+    /**
+     * 批量更新数据
+     * @param pendingData 更新的数据
+     */
+    void updateBatchById(@Param("list") List<FsMiniprogramSubNotifyTask> pendingData);
+}

+ 28 - 0
fs-service-system/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 WeXinAccessTokenDTO}
+     */
+    WeXinAccessTokenDTO getStableToken(ClientCredGrantReqDTO param);
+
+    /**
+     * 微信小程序发送订阅消息
+     *
+     * @param param 请求参数
+     * @return {@link MiniGramSubsMsgResultDTO}
+     */
+    MiniGramSubsMsgResultDTO sendSubscribeMsg(String accessToken,TemplateMessageSendRequestDTO param);
+}

+ 29 - 0
fs-service-system/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);
+    }
+}

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

@@ -0,0 +1,88 @@
+package com.fs.store.utils;
+
+import cn.hutool.http.HttpUtil;
+import cn.hutool.json.JSONUtil;
+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);
+            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 = HttpUtil.post(TOKEN_URL, requestBody);
+
+            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)); //记录解析结果
+            return result;
+
+        } catch (Exception e) {
+            log.error("获取微信 Stable Access Token 失败", e);
+            throw e;
+        }
+    }
+}

+ 28 - 0
fs-service-system/src/main/resources/mapper/store/FsAdvMapper_.xml

@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.store.mapper.FsMiniprogramSubNotifyTaskMapper">
+
+    <update id="updateBatchById" parameterType="java.util.List">
+        <foreach collection="list" item="item" separator=";">
+            UPDATE FsMiniprogramSubNotifyTask
+            SET
+            task_name = #{item.taskName},
+            template_id = #{item.templateId},
+            touser = #{item.touser},
+            page = #{item.page},
+            data = #{item.data, typeHandler=com.alibaba.fastjson.typehandler.JSONObjectTypeHandler},
+            status = #{item.status},
+            retry_count = #{item.retryCount},
+            max_retries = #{item.maxRetries},
+            request_params = #{item.requestParams, typeHandler=com.alibaba.fastjson.typehandler.JSONObjectTypeHandler},
+            request_body = #{item.requestBody},
+            response_body = #{item.responseBody},
+            error_message = #{item.errorMessage},
+            update_time = #{item.updateTime}
+            WHERE id = #{item.id}
+        </foreach>
+    </update>
+
+</mapper>