Browse Source

微信服务通知

zhangxin 4 days ago
parent
commit
c2416fdb91
18 changed files with 728 additions and 0 deletions
  1. 13 0
      national-motion-base-core/src/main/java/org/jeecg/config/vo/weixin/WechatMiniProgramProperties.java
  2. 46 0
      national-motion-base-core/src/main/java/org/jeecg/config/weixin/WechatConfig.java
  3. 19 0
      national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/app/vo/weixin/NotificationRequest.java
  4. 20 0
      national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/app/vo/weixin/NotificationResult.java
  5. 70 0
      national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/app/weixinService/WechatAccessTokenService.java
  6. 120 0
      national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/app/weixinService/WechatNotificationService.java
  7. 221 0
      national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/app/wxNotification/WxNotificationService.java
  8. 36 0
      national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/quartz/job/ClassNoticeJobService.java
  9. 44 0
      national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/quartz/vo/JobClassNoticeVo.java
  10. 40 0
      national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/quartz/vo/JobExtendedClassNoticeVo.java
  11. 12 0
      national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/system/app/entity/AppOrder.java
  12. 3 0
      national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/system/app/mapper/AppCoursesPriceRulesMapper.java
  13. 4 0
      national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/system/app/mapper/AppOrderMapper.java
  14. 8 0
      national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/system/app/mapper/xml/AppCoursesPriceRulesMapper.xml
  15. 17 0
      national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/system/app/mapper/xml/AppOrderMapper.xml
  16. 5 0
      national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/system/app/service/IAppOrderService.java
  17. 42 0
      national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/system/app/service/impl/AppCoureseServiceImpl.java
  18. 8 0
      national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/system/app/service/impl/AppOrderServiceImpl.java

+ 13 - 0
national-motion-base-core/src/main/java/org/jeecg/config/vo/weixin/WechatMiniProgramProperties.java

@@ -0,0 +1,13 @@
+package org.jeecg.config.vo.weixin;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+@Data
+@AllArgsConstructor
+public class WechatMiniProgramProperties {
+    private String appId;
+    private String appSecret;
+    private boolean tokenCacheEnabled;
+    private int tokenCacheTimeout;
+}

+ 46 - 0
national-motion-base-core/src/main/java/org/jeecg/config/weixin/WechatConfig.java

@@ -0,0 +1,46 @@
+package org.jeecg.config.weixin;
+
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
+import org.jeecg.config.vo.weixin.WechatMiniProgramProperties;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.concurrent.TimeUnit;
+
+@Configuration
+public class WechatConfig {
+
+    @Value("${wechat.miniprogram.appid}")
+    private String appId;
+
+    @Value("${wechat.miniprogram.appsecret}")
+    private String appSecret;
+
+    @Value("${wechat.miniprogram.token.cache.enabled:true}")
+    private boolean tokenCacheEnabled;
+
+    @Value("${wechat.miniprogram.token.cache.timeout:7000}")
+    private int tokenCacheTimeout;
+
+    @Bean
+    public WechatMiniProgramProperties wechatMiniProgramProperties() {
+        return new WechatMiniProgramProperties(appId, appSecret, tokenCacheEnabled, tokenCacheTimeout);
+    }
+
+    @Bean
+    public CloseableHttpClient httpClient() {
+        PoolingHttpClientConnectionManager connectionManager =
+                new PoolingHttpClientConnectionManager();
+        connectionManager.setMaxTotal(200);
+        connectionManager.setDefaultMaxPerRoute(50);
+
+        return HttpClientBuilder.create()
+                .setConnectionManager(connectionManager)
+                .evictIdleConnections(30, TimeUnit.SECONDS)
+                .setConnectionTimeToLive(60, TimeUnit.SECONDS)
+                .build();
+    }
+}

+ 19 - 0
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/app/vo/weixin/NotificationRequest.java

@@ -0,0 +1,19 @@
+package org.jeecg.modules.app.vo.weixin;
+
+
+import com.alibaba.fastjson.JSONObject;
+import lombok.Data;
+
+@Data
+public class NotificationRequest {
+    /**  用户openId */
+    private String openid;
+    /** 订阅模板编号 */
+    private String templateId;
+    /** 模板内容 */
+    private JSONObject data;
+    /** 点击消息卡片跳转的小程序页面路径*/
+    private String page;
+    /** 跳转小程序类型*/
+    private String miniprogramState;
+}

+ 20 - 0
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/app/vo/weixin/NotificationResult.java

@@ -0,0 +1,20 @@
+package org.jeecg.modules.app.vo.weixin;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+@Data
+@AllArgsConstructor
+public class NotificationResult {
+    private boolean success;
+    private Long msgId;
+    private String errorMsg;
+
+    public static NotificationResult success(Long msgId) {
+        return new NotificationResult(true, msgId, null);
+    }
+
+    public static NotificationResult failure(String errorMsg) {
+        return new NotificationResult(false, null, errorMsg);
+    }
+}

+ 70 - 0
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/app/weixinService/WechatAccessTokenService.java

@@ -0,0 +1,70 @@
+package org.jeecg.modules.app.weixinService;
+
+import com.alibaba.fastjson.JSONObject;
+import org.apache.http.HttpEntity;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.util.EntityUtils;
+import org.jeecg.config.vo.weixin.WechatMiniProgramProperties;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+
+import java.util.Date;
+
+@Service
+public class WechatAccessTokenService {
+
+    private static final Logger logger = LoggerFactory.getLogger(WechatAccessTokenService.class);
+
+    private final WechatMiniProgramProperties properties;
+    private final CloseableHttpClient httpClient;
+
+    private String accessToken;
+    private long expireTime;
+    private final Object lock = new Object();
+
+    public WechatAccessTokenService(WechatMiniProgramProperties properties,
+                                    CloseableHttpClient httpClient) {
+        this.properties = properties;
+        this.httpClient = httpClient;
+    }
+
+    public String getAccessToken() {
+        if (!properties.isTokenCacheEnabled() || accessToken == null || System.currentTimeMillis() > expireTime) {
+            synchronized (lock) {
+                if (!properties.isTokenCacheEnabled() || accessToken == null || System.currentTimeMillis() > expireTime) {
+                    refreshAccessToken();
+                }
+            }
+        }
+        return accessToken;
+    }
+
+    private void refreshAccessToken() {
+        String url = String.format("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s",
+                properties.getAppId(), properties.getAppSecret());
+
+        try {
+            HttpGet httpGet = new HttpGet(url);
+            try (CloseableHttpResponse response = httpClient.execute(httpGet)) {
+                HttpEntity entity = response.getEntity();
+                String result = EntityUtils.toString(entity);
+                JSONObject json = JSONObject.parseObject(result);
+
+                if (json.containsKey("access_token")) {
+                    accessToken = json.getString("access_token");
+                    expireTime = System.currentTimeMillis() + (properties.getTokenCacheTimeout() * 1000);
+                    logger.info("成功刷新微信AccessToken,有效期至: {}", new Date(expireTime));
+                } else {
+                    logger.error("获取微信AccessToken失败: {}", result);
+                    throw new RuntimeException("获取微信AccessToken失败: " + json.getString("errmsg"));
+                }
+            }
+        } catch (Exception e) {
+            logger.error("刷新微信AccessToken异常", e);
+            throw new RuntimeException("刷新微信AccessToken异常", e);
+        }
+    }
+}

+ 120 - 0
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/app/weixinService/WechatNotificationService.java

@@ -0,0 +1,120 @@
+package org.jeecg.modules.app.weixinService;
+
+import com.alibaba.fastjson.JSONObject;
+import org.apache.commons.lang.StringUtils;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.util.EntityUtils;
+import org.jeecg.common.exception.JeecgBootException;
+import org.jeecg.modules.app.vo.weixin.NotificationRequest;
+import org.jeecg.modules.app.vo.weixin.NotificationResult;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+@Service
+public class WechatNotificationService {
+
+    private static final Logger logger = LoggerFactory.getLogger(WechatNotificationService.class);
+
+    private final WechatAccessTokenService tokenService;
+    private final CloseableHttpClient httpClient;
+
+    public WechatNotificationService(WechatAccessTokenService tokenService,
+                                     CloseableHttpClient httpClient) {
+        this.tokenService = tokenService;
+        this.httpClient = httpClient;
+    }
+
+    /**
+     * 发送小程序订阅消息
+     * @param request 通知请求参数
+     * @return 发送结果
+     */
+    public NotificationResult sendSubscribeMessage(NotificationRequest request) {
+        String accessToken = tokenService.getAccessToken();
+        String url = "https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=" + accessToken;
+
+        JSONObject requestBody = buildRequestBody(request);
+
+        try {
+            HttpPost httpPost = new HttpPost(url);
+            httpPost.setEntity(new StringEntity(requestBody.toJSONString(), StandardCharsets.UTF_8));
+            httpPost.setHeader("Content-Type", "application/json;charset=utf8");
+
+            try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
+                String result = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
+                JSONObject resultJson = JSONObject.parseObject(result);
+
+                if (resultJson.getInteger("errcode") == 0) {
+                    logger.info("成功发送小程序订阅消息: openid={}, templateId={}",
+                            request.getOpenid(), request.getTemplateId());
+                    throw new JeecgBootException(""+resultJson.getLong("msgid"));
+                } else {
+                    logger.error("发送小程序订阅消息失败: {}, 请求参数: {}",
+                            result, requestBody.toJSONString());
+                    throw new JeecgBootException(""+resultJson);
+                }
+            }
+        } catch (Exception e) {
+            logger.error("发送小程序订阅消息异常", e);
+            throw new JeecgBootException("系统异常: " + e.getMessage());
+        }
+    }
+
+    private JSONObject buildRequestBody(NotificationRequest request) {
+        JSONObject body = new JSONObject();
+        body.put("touser", request.getOpenid());
+        body.put("template_id", request.getTemplateId());
+        body.put("data", request.getData());
+
+        if (StringUtils.isNotBlank(request.getPage())) {
+            body.put("page", request.getPage());
+        }
+
+        if (StringUtils.isNotBlank(request.getMiniprogramState())) {
+            body.put("miniprogram_state", request.getMiniprogramState());
+        }
+
+        return body;
+    }
+
+    private NotificationResult handleErrorResponse(JSONObject resultJson) {
+        int errcode = resultJson.getInteger("errcode");
+        String errmsg = resultJson.getString("errmsg");
+
+        switch (errcode) {
+            case 40003: // 无效的openid
+            case 40037: // 无效的模板ID
+                return NotificationResult.failure("参数错误: " + errmsg);
+            case 43101: // 用户拒绝接受消息
+                return NotificationResult.failure("用户未订阅消息");
+            case 47003: // 模板参数不准确
+                return NotificationResult.failure("模板参数错误: " + errmsg);
+            default:
+                return NotificationResult.failure("微信接口错误: " + errmsg);
+        }
+    }
+
+    // 批量发送(异步)
+    public void batchSendSubscribeMessages(List<NotificationRequest> requests) {
+        ExecutorService executor = Executors.newFixedThreadPool(10);
+
+        for (NotificationRequest request : requests) {
+            executor.submit(() -> {
+                try {
+                    sendSubscribeMessage(request);
+                } catch (Exception e) {
+                    logger.error("批量发送小程序订阅消息异常", e);
+                }
+            });
+        }
+    }
+}

+ 221 - 0
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/app/wxNotification/WxNotificationService.java

@@ -0,0 +1,221 @@
+package org.jeecg.modules.app.wxNotification;
+
+
+import com.alibaba.fastjson.JSONObject;
+import lombok.extern.log4j.Log4j2;
+import org.jeecg.modules.app.vo.weixin.NotificationRequest;
+import org.jeecg.modules.app.weixinService.WechatNotificationService;
+import org.jeecg.modules.quartz.vo.JobClassNoticeVo;
+import org.jeecg.modules.quartz.vo.JobExtendedClassNoticeVo;
+import org.jeecg.modules.system.app.entity.AppOrder;
+import org.jeecg.modules.system.app.service.IAppOrderService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Service;
+
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+@Log4j2
+@Service
+public class WxNotificationService {
+
+
+    @Autowired
+    WechatNotificationService notificationService;
+    @Autowired
+    IAppOrderService iAppOrderService;
+    private final int maxRetry = 3;
+    private final List<Integer> notifyHoursBefore = Arrays.asList(8, 24);
+    @Autowired
+    private RedisTemplate<String, String> redisTemplate;
+
+    /**
+     * 上课时间服务通知  8小时和24小时
+     */
+    public void classNoticeJob() {
+
+        String lockKey = "order:notification:lock";
+        String lockValue = String.valueOf(System.currentTimeMillis());
+
+        try {
+            // 获取分布式锁,防止多实例重复执行
+            Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, 4, TimeUnit.MINUTES);
+            if (locked == null || !locked) {
+                log.debug("Failed to acquire notification lock, skipping this execution");
+                return;
+            }
+
+            Date now = new Date();
+            log.info("Start checking orders for notification at {}", now);
+
+            // 处理每个预设的通知时间点
+            for (Integer hours : notifyHoursBefore) {
+                processNotificationForTimeWindow(now, hours);
+            }
+
+            log.info("Finished checking orders for notification");
+        } catch (Exception e) {
+            log.error("Error occurred while checking orders for notification", e);
+        } finally {
+            // 释放锁
+            if (lockValue.equals(redisTemplate.opsForValue().get(lockKey))) {
+                redisTemplate.delete(lockKey);
+            }
+        }
+    }
+
+    /**
+     * 预约过场地通知 每天7点执行
+     */
+    public void siteJob() {
+
+        String lockKey = "site:notification:lock";
+        String lockValue = String.valueOf(System.currentTimeMillis());
+        try {
+            // 获取分布式锁,防止多实例重复执行
+            Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, 4, TimeUnit.MINUTES);
+            if (locked == null || !locked) {
+                log.debug("Failed to acquire notification lock, skipping this execution");
+                return;
+            }
+            Date now = new Date();
+            log.info("Start checking orders for notification at {}", now);
+
+            //查询预约过学校场地的用户并通知他
+//            iAppOrderService.findBySite();
+
+            log.info("Finished checking orders for notification");
+        } catch (Exception e) {
+            log.error("Error occurred while checking orders for notification", e);
+        } finally {
+            // 释放锁
+            if (lockValue.equals(redisTemplate.opsForValue().get(lockKey))) {
+                redisTemplate.delete(lockKey);
+            }
+        }
+    }
+    /**
+     * 延课通知
+     */
+    public void extendedClassesJob(List<JobExtendedClassNoticeVo> jobExtendedClassNoticeVos) {
+        String lockKey = "site:notification:lock";
+        String lockValue = String.valueOf(System.currentTimeMillis());
+        try {
+            // 获取分布式锁,防止多实例重复执行
+            Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, 4, TimeUnit.MINUTES);
+            if (locked == null || !locked) {
+                log.debug("Failed to acquire notification lock, skipping this execution");
+                return;
+            }
+            Date now = new Date();
+            log.info("Start checking orders for notification at {}", now);
+            ExecutorService executor = Executors.newFixedThreadPool(10);
+            for (JobExtendedClassNoticeVo jobExtendedClassNoticeVo : jobExtendedClassNoticeVos) {
+                NotificationRequest notificationRequest = new NotificationRequest();
+                notificationRequest.setOpenid(jobExtendedClassNoticeVo.getUserOpenId());
+                notificationRequest.setTemplateId("NPlIBEwBDPX23J3Jip0CwYUU4vF9ZlcK8U1d6Gs4yrM");
+                JSONObject data = new JSONObject();
+                // 根据模板内容设置数据项
+                data.put("thing2", new JSONObject().fluentPut("value", jobExtendedClassNoticeVo.getLassHourTime()));
+                data.put("thing1", new JSONObject().fluentPut("value", jobExtendedClassNoticeVo.getSiteName()));
+                data.put("thing3", new JSONObject().fluentPut("value", jobExtendedClassNoticeVo.getReasonClassExtension()));
+                executor.submit(() -> {
+                    try {
+                        notificationService.sendSubscribeMessage(notificationRequest);
+                    } catch (Exception e) {
+                        log.error("延课批量发送小程序订阅消息异常", e);
+                    }
+                });
+
+            }
+            //查询被延课的数据通知他
+//            iAppOrderService.findBySite();
+
+            log.info("Finished checking orders for notification");
+        } catch (Exception e) {
+            log.error("Error occurred while checking orders for notification", e);
+        } finally {
+            // 释放锁
+            if (lockValue.equals(redisTemplate.opsForValue().get(lockKey))) {
+                redisTemplate.delete(lockKey);
+            }
+        }
+    }
+
+
+
+    private void processNotificationForTimeWindow(Date now, Integer hoursBefore) {
+        long windowStart = now.getTime();
+        long windowEnd = now.getTime() + hoursBefore * 3600 * 1000;
+        ;
+        String logPrefix = hoursBefore + "h notification";
+
+        log.info("Processing {} for time window {} - {}", logPrefix, new Date(windowStart), new Date(windowEnd));
+
+        // 查询需要通知的订单(未通知或重试次数未达上限)
+        List<JobClassNoticeVo> list = iAppOrderService.findByJob(hoursBefore,new Date(windowEnd),maxRetry);
+        if (list.isEmpty()) {
+            log.debug("No orders found for {}", logPrefix);
+            return;
+        }
+
+        log.info("Found {} orders for {}", list.size(), logPrefix);
+        ExecutorService executor = Executors.newFixedThreadPool(10);
+        for (JobClassNoticeVo jobClassNoticeVo : list) {
+            try {
+                // 发送到消息队列异步处理
+                NotificationRequest notificationRequest = new NotificationRequest();
+                notificationRequest.setOpenid(jobClassNoticeVo.getUserOpenId());
+                notificationRequest.setTemplateId("Yi1Z1IKRwgF6-mpiFcOUTvavc4TUAsfsLynK_3Yu350");//上课时间模板
+                JSONObject data = new JSONObject();
+                String date = getDate(jobClassNoticeVo.getStartTime());
+                String siteName = null;
+                if (jobClassNoticeVo.getSiteName()!=null){
+                    siteName= jobClassNoticeVo.getSiteName().substring(0, Math.min(jobClassNoticeVo.getSiteName().length(), 20));
+                }
+                // 根据模板内容设置数据项
+                data.put("time3", new JSONObject().fluentPut("value", date));
+                data.put("thing4", new JSONObject().fluentPut("value", siteName));
+                data.put("thing11", new JSONObject().fluentPut("value", "如有特殊情况不能上课,记得沟通延课"));
+                executor.submit(() -> {
+                    AppOrder appOrder = iAppOrderService.getById(jobClassNoticeVo.getOrderId());
+                    if (appOrder==null){
+                        log.error("上课消息通知未查询到该订单:"+jobClassNoticeVo.getOrderNo()+",订单id:"+jobClassNoticeVo.getOrderId());
+                        return;
+                    }
+                    try {
+                        notificationService.sendSubscribeMessage(notificationRequest);
+                        if (hoursBefore==24){
+                            appOrder.setNotified24h(1);
+                        }else if (hoursBefore==8){
+                            appOrder.setNotified24h(1);
+                        }
+                        iAppOrderService.updateById(appOrder);
+                    } catch (Exception e) {
+                        // 更新重试次数
+                        appOrder.setNotifyRetryCount(appOrder.getNotifyRetryCount() + 1);
+                        iAppOrderService.updateById(appOrder);
+                        log.error("批量发送小程序订阅消息异常", e);
+                    }
+                });
+                log.debug("Sent notification task for order {} to", jobClassNoticeVo.getOrderNo());
+            } catch (Exception e) {
+                log.error("Failed to send notification task for order {}", jobClassNoticeVo.getOrderNo(), e);
+            }
+        }
+    }
+    private String getDate(Date date){
+        if (date==null){
+            return "未知时间";
+        }
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm");
+        return sdf.format(date);
+    }
+
+}

+ 36 - 0
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/quartz/job/ClassNoticeJobService.java

@@ -0,0 +1,36 @@
+package org.jeecg.modules.quartz.job;
+
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.jeecg.modules.app.wxNotification.WxNotificationService;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.text.ParseException;
+
+/**
+ * 上课前8小时和24小时服务通知
+ * @author zx
+ * @date 2025/7/15 10:28
+ * @description
+ */
+@Slf4j
+@AllArgsConstructor
+@Component
+public class ClassNoticeJobService {
+
+   private final WxNotificationService wxNotificationService;
+
+    //    @Scheduled(cron = "0 * * * * ?")
+//    @Scheduled(cron = "0 0 0 1 12 *")
+    @Scheduled(cron = "0 */5 * * * ?")
+    @Transactional
+    public void execute() throws ParseException {
+        log.info("开始执行上课定时任务");
+        wxNotificationService.classNoticeJob();
+        log.info("结束执行上课定时任务");
+    }
+
+
+}

+ 44 - 0
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/quartz/vo/JobClassNoticeVo.java

@@ -0,0 +1,44 @@
+package org.jeecg.modules.quartz.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.util.Date;
+
+@Data
+@Accessors(chain = true)
+@EqualsAndHashCode(callSuper = false)
+@AllArgsConstructor
+@NoArgsConstructor
+@Schema(description="查询上课通知数据")
+public class JobClassNoticeVo {
+    /** 订单ID*/
+    private String orderId;
+    /** 订单No*/
+    private String orderNo;
+
+    /** 用户ID*/
+    private String userId;
+
+    /** 用户微信openID*/
+    private String userOpenId;
+
+    /** 上课开始时间*/
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date startTime;
+
+    /** 场地名称*/
+    private String siteName;
+
+
+    /** 延课原因*/
+    private String reasonClassExtension;
+
+}

+ 40 - 0
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/quartz/vo/JobExtendedClassNoticeVo.java

@@ -0,0 +1,40 @@
+package org.jeecg.modules.quartz.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.Size;
+
+@Data
+@Accessors(chain = true)
+@EqualsAndHashCode(callSuper = false)
+@AllArgsConstructor
+@NoArgsConstructor
+@Schema(description="延课通知数据")
+public class JobExtendedClassNoticeVo {
+
+    /** 用户微信openID*/
+    @NotBlank(message = "用户微信openID不允许为空")
+    private String userOpenId;
+
+    /** 场地名称*/
+    @NotBlank(message = "场地名称不允许为空")
+    @Size(max = 20, message = "场地名称不能超过为20字符")
+    private String siteName;
+
+    /** 延课原因*/
+    @NotBlank(message = "延课原因不允许为空")
+    @Size(max = 20, message = "延课原因不能超过为20字符")
+    private String reasonClassExtension;
+
+    /** 课程时间*/
+    @NotBlank(message = "课程时间不允许为空")
+    @Size(max = 20, message = "课程时间不能超过为20字符")
+    private String lassHourTime;
+
+}

+ 12 - 0
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/system/app/entity/AppOrder.java

@@ -165,4 +165,16 @@ public class AppOrder implements Serializable {
     @Schema(description = "删除标志;删除状态(0-正常,1-已删除)")
     @TableLogic
     private Integer delFlag;
+
+    @Excel(name = "8小时微信通知标识 0-false 1-true)", width = 15)
+    @Schema(description = "8小时微信通知标识 0-false 1-true")
+    private Integer notified8h;
+
+    @Excel(name = "24小时微信通知标识 0-false 1-true)", width = 15)
+    @Schema(description = "24小时微信通知标识 0-false 1-true)")
+    private Integer notified24h;
+
+    @Excel(name = "通知次数标识 限制3次", width = 15)
+    @Schema(description = "通知次数标识 限制3次")
+    private Integer notifyRetryCount;
 }

+ 3 - 0
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/system/app/mapper/AppCoursesPriceRulesMapper.java

@@ -2,6 +2,7 @@ package org.jeecg.modules.system.app.mapper;
 
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import org.apache.ibatis.annotations.Param;
+import org.jeecg.modules.quartz.vo.JobExtendedClassNoticeVo;
 import org.jeecg.modules.system.app.entity.AppCoursesPriceRules;
 
 import java.util.List;
@@ -14,4 +15,6 @@ import java.util.List;
  */
 public interface AppCoursesPriceRulesMapper extends BaseMapper<AppCoursesPriceRules> {
     List<String> selectRuleIdsByCourseId(@Param("coursesId") String coursesId);
+
+    JobExtendedClassNoticeVo findByClassPrice(@Param("userId") String userId);
 }

+ 4 - 0
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/system/app/mapper/AppOrderMapper.java

@@ -7,10 +7,12 @@ import org.apache.ibatis.annotations.Param;
 import org.jeecg.modules.app.form.PageOrdersForm;
 import org.jeecg.modules.app.vo.PageOrdersVO;
 import org.jeecg.modules.app.vo.game.GameOrderVo;
+import org.jeecg.modules.quartz.vo.JobClassNoticeVo;
 import org.jeecg.modules.system.app.entity.AppOrder;
 import org.jeecg.modules.system.app.form.AppOrderPageForm;
 import org.jeecg.modules.system.app.vo.OrderPageVO;
 
+import java.util.Date;
 import java.util.List;
 
 /**
@@ -43,4 +45,6 @@ public interface AppOrderMapper extends BaseMapper<AppOrder> {
                          @Param("endTime") String endTime);
 
     IPage<PageOrdersVO> pageOrders(Page<PageOrdersVO> page, @Param("pageOrdersForm") PageOrdersForm pageOrdersForm);
+
+    List<JobClassNoticeVo> findByJob(@Param("hoursBefore") Integer hoursBefore,@Param("date") Date date,@Param("maxRetry")  int maxRetry);
 }

+ 8 - 0
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/system/app/mapper/xml/AppCoursesPriceRulesMapper.xml

@@ -4,4 +4,12 @@
     <select id="selectRuleIdsByCourseId" resultType="string">
         SELECT id FROM nm_courses_price_rules WHERE courses_id = #{coursesId}
     </select>
+    <select id="findByClassPrice" resultType="org.jeecg.modules.quartz.vo.JobExtendedClassNoticeVo">
+        select b.user_open_id,d.`name` as siteName from nm_courses_verification_record  a
+        LEFT JOIN nm_order b on b.id =a.order_id
+        LEFT JOIN nm_courses c on a.courses_id = c.id
+        LEFT JOIN nm_site d on  c.address_site_id =d.id
+        where a.use_user_id = #{userId}
+        GROUP BY b.user_open_id,d.`name`
+    </select>
 </mapper>

+ 17 - 0
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/system/app/mapper/xml/AppOrderMapper.xml

@@ -107,4 +107,21 @@
         </where>
           ORDER BY o.create_time DESC
     </select>
+    <select id="findByJob" resultType="org.jeecg.modules.quartz.vo.JobClassNoticeVo">
+        select a.id,a.user_id,c.user_open_id ,b.start_time,d.`name` as siteName
+        from  nm_courses a
+                  LEFT JOIN  nm_courses_price_rules b on a.id = b.courses_id and b.del_flag=0
+                  LEFT JOIN nm_order c on  a.id =c.product_ids and order_status =1
+                  LEFT JOIN nm_site d on a.address_site_id =d.id  and d.del_flag=0
+        WHERE 1=1
+        <if test="hoursBefore != null and hoursBefore==8">
+            and c.notified8h = false
+            <![CDATA[  and DATE_FORMAT(b.start_time, '%Y-%m-%d %H:%i') <= DATE_FORMAT(#{date}, '%Y-%m-%d %H:%i') ]]>
+        </if>
+        <if test="hoursBefore != null and hoursBefore==24">
+            and c.notified24h = false
+            <![CDATA[  and DATE_FORMAT(b.start_time, '%Y-%m-%d %H:%i') <= DATE_FORMAT(#{date}, '%Y-%m-%d %H:%i') ]]>
+        </if>
+        and notify_retry_count = #{maxRetry}
+    </select>
 </mapper>

+ 5 - 0
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/system/app/service/IAppOrderService.java

@@ -3,12 +3,14 @@ package org.jeecg.modules.system.app.service;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.IService;
 import org.jeecg.modules.app.vo.ScanCodeQueryOrderVO;
+import org.jeecg.modules.quartz.vo.JobClassNoticeVo;
 import org.jeecg.modules.system.app.dto.AppOrderDTO;
 import org.jeecg.modules.system.app.entity.AppOrder;
 import org.jeecg.modules.system.app.form.AppOrderPageForm;
 import org.jeecg.modules.system.app.vo.AppOrderInfoVO;
 import org.jeecg.modules.system.app.vo.OrderPageVO;
 
+import java.util.Date;
 import java.util.List;
 
 /**
@@ -53,4 +55,7 @@ public interface IAppOrderService extends IService<AppOrder> {
     ScanCodeQueryOrderVO scanCodeQueryOrder(String orderId);
 
     Boolean scanCodeVerification(List<String> orderProInfoIds);
+
+
+    List<JobClassNoticeVo> findByJob(Integer hoursBefore,Date date, int maxRetry);
 }

+ 42 - 0
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/system/app/service/impl/AppCoureseServiceImpl.java

@@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import org.apache.commons.lang3.ObjectUtils;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.shiro.SecurityUtils;
 import org.jeecg.common.constant.CommonConstant;
 import org.jeecg.common.exception.JeecgBootException;
@@ -15,17 +16,22 @@ import org.jeecg.modules.app.form.TemporaryCourseForm;
 import org.jeecg.modules.app.vo.AppCoursesPageVO;
 import org.jeecg.modules.app.vo.CoursesPriceRulesVO;
 import org.jeecg.modules.app.vo.FamilyUserVO;
+import org.jeecg.modules.app.wxNotification.WxNotificationService;
+import org.jeecg.modules.quartz.vo.JobExtendedClassNoticeVo;
 import org.jeecg.modules.system.app.dto.*;
 import org.jeecg.modules.system.app.entity.*;
 import org.jeecg.modules.system.app.mapper.*;
 import org.jeecg.modules.system.app.service.IAppCoureseService;
 import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
 import javax.annotation.Resource;
 import java.time.LocalDate;
+import java.time.LocalDateTime;
 import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
 import java.util.*;
 import java.util.stream.Collectors;
 
@@ -52,6 +58,10 @@ public class AppCoureseServiceImpl extends ServiceImpl<AppCoursesMapper, AppCour
     @Resource
     private AppOrderMapper appOrderMapper;
 
+    @Autowired
+    WxNotificationService wxNotificationService;
+
+
     @Override
     @Transactional(rollbackFor = Exception.class)
     public Boolean saveWitchPriceRules(AppCoursesDTO appCoursesDTO) {
@@ -392,11 +402,15 @@ public class AppCoureseServiceImpl extends ServiceImpl<AppCoursesMapper, AppCour
 //        AppOrder appOrder = appOrderMapper.selectById(form.getOrderId());
         //原课时
         AppCoursesPriceRules coursesPriceRules = priceRulesMapper.selectById(form.getPriceRulesId());
+        if (coursesPriceRules==null){
+            throw new JeecgBootException("未查询到原课时");
+        }
         //补课课时
         AppCoursesPriceRules appCoursesPriceRules = priceRulesMapper.selectById(form.getCoursePriceRulesId());
         //设置为延期状态
         coursesPriceRules.setClassStatus(CommonConstant.STATUS_1_INT);
         priceRulesMapper.updateById(appCoursesPriceRules);
+        List<JobExtendedClassNoticeVo> jobExtendedClassNoticeVos = new ArrayList<>();
 
         for (FamilyUserVO familyUserVO : form.getFamilyUserVOList()) {
 
@@ -429,7 +443,15 @@ public class AppCoureseServiceImpl extends ServiceImpl<AppCoursesMapper, AppCour
             verificationRecord.setVerifyStatus(CommonConstant.STATUS_0_INT);
             verificationRecord.setCoursesType(CommonConstant.STATUS_1_INT);
             appCoursesVerificationRecordMapper.insert(verificationRecord);
+            JobExtendedClassNoticeVo jobExtendedClassNoticeVo =  priceRulesMapper.findByClassPrice(familyMembers.getId());
+            if (jobExtendedClassNoticeVo==null||StringUtils.isEmpty(jobExtendedClassNoticeVo.getUserOpenId())|| StringUtils.isEmpty(jobExtendedClassNoticeVo.getSiteName())){
+                continue;
+            }
+            getDate(coursesPriceRules.getStartTime(),coursesPriceRules.getEndTime());
+            jobExtendedClassNoticeVos.add(jobExtendedClassNoticeVo);
+
         }
+        wxNotificationService.extendedClassesJob(jobExtendedClassNoticeVos);
         return Boolean.TRUE;
     }
 
@@ -453,4 +475,24 @@ public class AppCoureseServiceImpl extends ServiceImpl<AppCoursesMapper, AppCour
         if (!loginUser.getOrgCode().equals(course.getOrgCode()))
             throw new JeecgBootException("无权限操作", SC_INTERNAL_SERVER_ERROR_500);
     }
+
+    private String getDate(Date startTime,Date endTime){
+
+        if (startTime==null|| endTime==null){
+            return "请联系授课老师确认时间";
+        }
+        // 将 Date 转换为 LocalDateTime
+        LocalDateTime startLdt = LocalDateTime.ofInstant(startTime.toInstant(), java.time.ZoneId.systemDefault());
+        LocalDateTime endLdt = LocalDateTime.ofInstant(endTime.toInstant(), java.time.ZoneId.systemDefault());
+
+        DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("MM.dd");
+        DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HH:mm");
+
+        String formattedDate = startLdt.format(dateFormatter);
+        String formattedStartTime = startLdt.format(timeFormatter);
+        String formattedEndTime = endLdt.format(timeFormatter);
+
+        return   formattedDate + " " + formattedStartTime + "-" + formattedEndTime;
+    }
+
 }

+ 8 - 0
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/system/app/service/impl/AppOrderServiceImpl.java

@@ -11,6 +11,7 @@ import org.jeecg.common.system.vo.LoginUser;
 import org.jeecg.modules.app.vo.AppGameScheduleVO;
 import org.jeecg.modules.app.vo.AppOrderProInfoVerifyVO;
 import org.jeecg.modules.app.vo.ScanCodeQueryOrderVO;
+import org.jeecg.modules.quartz.vo.JobClassNoticeVo;
 import org.jeecg.modules.system.app.dto.AppOrderDTO;
 import org.jeecg.modules.system.app.dto.IsinUserInfoDTO;
 import org.jeecg.modules.system.app.dto.VerificationRecordDTO;
@@ -29,6 +30,7 @@ import org.springframework.transaction.annotation.Transactional;
 import javax.annotation.Resource;
 import java.math.BigDecimal;
 import java.util.ArrayList;
+import java.util.Date;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Collectors;
@@ -283,4 +285,10 @@ public class AppOrderServiceImpl extends ServiceImpl<AppOrderMapper, AppOrder> i
         }
         return Boolean.TRUE;
     }
+
+    @Override
+    public List<JobClassNoticeVo> findByJob(Integer hoursBefore,Date date, int maxRetry) {
+        return appOrderMapper.findByJob(hoursBefore,date,maxRetry);
+    }
+
 }