Просмотр исходного кода

Merge remote-tracking branch 'origin/saas-api' into saas-api

xgb 7 часов назад
Родитель
Сommit
7ddbb6ac1a

+ 19 - 18
fs-company/src/main/java/com/fs/company/controller/qw/QwUserController.java

@@ -949,26 +949,27 @@ public class QwUserController extends BaseController
         List<String> strings = qwCompanyMapper.selectQwCompanyCorpIdListByCompanyId(loginUser.getCompany().getCompanyId());
         for (String string : strings) {
             if (string.equals(corpId)){
-                // 远程调用 fs-qw-api 同步企微用户
-                String syncUserUrl = OpenQwConfig.taskApi + "/app/common/syncQwUserAsync?corpId=" + corpId;
-                try {
-                    HttpResponse response = HttpRequest.post(syncUserUrl)
-                            .timeout(apiTimeout * 1000)
-                            .execute();
-                    if (response.getStatus() != 200) {
-                        log.error("同步企微用户失败,HTTP状态码: {}", response.getStatus());
-                        return R.error("同步企微用户失败,服务返回状态码: " + response.getStatus());
-                    }
-                } catch (Exception e) {
-                    log.error("同步企微用户异常, url={}", syncUserUrl, e);
-                    if (e.getCause() instanceof SocketTimeoutException) {
-                        return R.error("同步企微用户超时,请稍后重试");
-                    }
-                    return R.error("同步企微用户失败: " + e.getMessage());
-                }
+//                // 远程调用 fs-qw-api 同步企微用户
+//                String syncUserUrl = OpenQwConfig.taskApi + "/app/common/syncQwUserAsync?corpId=" + corpId;
+//                try {
+//                    HttpResponse response = HttpRequest.post(syncUserUrl)
+//                            .timeout(apiTimeout * 1000)
+//                            .execute();
+//                    if (response.getStatus() != 200) {
+//                        log.error("同步企微用户失败,HTTP状态码: {}", response.getStatus());
+//                        return R.error("同步企微用户失败,服务返回状态码: " + response.getStatus());
+//                    }
+//                } catch (Exception e) {
+//                    log.error("同步企微用户异常, url={}", syncUserUrl, e);
+//                    if (e.getCause() instanceof SocketTimeoutException) {
+//                        return R.error("同步企微用户超时,请稍后重试");
+//                    }
+//                    return R.error("同步企微用户失败: " + e.getMessage());
+//                }
+                return qwUserService.syncQwUser(corpId,loginUser.getTenantId());
 
             }
         }
-        return R.ok();
+        return R.error("同步企微用户失败");
     }
 }

+ 569 - 0
fs-company/src/main/java/com/fs/company/controller/qw/TaskManualController.java

@@ -0,0 +1,569 @@
+package com.fs.company.controller.qw;
+
+
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.util.ObjectUtil;
+import com.fs.common.config.RedisTenantContext;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.domain.ResponseResult;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.common.utils.StringUtils;
+import com.fs.company.service.ICompanyService;
+import com.fs.company.vo.RedPacketMoneyVO;
+import com.fs.course.mapper.FsCourseRedPacketLogMapper;
+import com.fs.course.mapper.FsCourseWatchLogMapper;
+import com.fs.course.param.newfs.FsUserCourseAddCompanyUserParam;
+import com.fs.course.service.*;
+import com.fs.course.vo.FsUserCourseVideoQVO;
+import com.fs.framework.datasource.TenantDataSourceManager;
+import com.fs.his.domain.FsUser;
+import com.fs.his.service.IFsInquiryOrderService;
+import com.fs.his.utils.qrcode.QRCodeUtils;
+import com.fs.qw.domain.QwCompany;
+import com.fs.qw.domain.QwIpadServerLog;
+import com.fs.qw.domain.QwUser;
+import com.fs.qw.mapper.QwExternalContactMapper;
+import com.fs.qw.mapper.QwUserMapper;
+import com.fs.qw.service.*;
+import com.fs.qwApi.service.QwApiService;
+import com.fs.sop.mapper.QwSopLogsMapper;
+import com.fs.sop.mapper.QwSopMapper;
+import com.fs.sop.mapper.SopUserLogsMapper;
+import com.fs.sop.service.*;
+import com.fs.sop.vo.QwSopLogsDoSendListTVO;
+import com.fs.store.service.IFsUserCourseCountService;
+import com.fs.tenant.domain.TenantInfo;
+import com.fs.tenant.service.TenantInfoService;
+import com.fs.wxwork.dto.WxWorkGetQrCodeDTO;
+import com.fs.wxwork.service.WxWorkService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.time.Duration;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+
+@Api("公共接口")
+@RestController
+@RequestMapping(value="/app/common")
+public class TaskManualController {
+
+    private static final Logger log = LoggerFactory.getLogger(TaskManualController.class);
+
+//    @Autowired
+//    private SopLogsTaskService service;
+    @Autowired
+    private IFsUserCourseVideoService courseVideoService;
+    @Autowired
+    private IQwExternalContactService qwExternalContactService;
+    @Autowired
+    private IFsUserVideoService fsUserVideoService;
+    @Autowired
+    private IHuaweiObsService huaweiObsService;
+    @Autowired
+    private IFsCourseWatchLogService watchLogService;
+    @Autowired
+    private QwExternalContactMapper qwExternalContactMapper;
+    @Autowired
+    private IFsCourseRedPacketLogService fsCourseRedPacketLogService;
+
+    @Autowired
+    private IQwSopLogsService qwSopLogsService;
+
+    @Autowired
+    private QwSopMapper qwSopMapper;
+
+    @Autowired
+    private FsCourseWatchLogMapper fsCourseWatchLogMapper;
+
+    @Autowired
+    private IFsCourseLinkService courseLinkService;
+    @Autowired
+    private FsCourseRedPacketLogMapper fsCourseRedPacketLogMapper;
+    @Autowired
+    private ICompanyService companyService;
+
+    @Autowired
+    private SopUserLogsMapper sopUserLogsMapper;
+
+    @Autowired
+    private QwSopLogsMapper qwSopLogsMapper;
+    @Autowired
+    private IQwSopTempRulesService tempRulesService;
+    @Autowired
+    private IQwSopTempVoiceService qwSopTempVoiceService;
+
+//    @Autowired
+//    private QwExternalContactRatingService qwExternalContactRatingService;
+
+    @Autowired
+    private ISopUserLogsService iSopUserLogsService;
+
+    @Autowired
+    private IFsUserCourseCountService userCourseCountService;
+
+    @Autowired
+    private ISopUserLogsInfoService iSopUserLogsInfoService;
+
+    @Autowired
+    private IFsInquiryOrderService inquiryOrderService;
+
+    @Autowired
+    private IQwMaterialService iQwMaterialService;
+
+    @Autowired
+    private IFsCourseLinkService iFsCourseLinkService;
+
+//    @Autowired
+//    private SyncQwExternalContactService syncQwExternalContactService;
+    @Autowired
+    private IFsUserCourseVideoService fsUserCourseVideoService;
+
+    @Autowired
+    public RedisCache redisCache;
+
+    @Autowired
+    private QwUserMapper qwUserMapper;
+
+
+    @Autowired
+    IQwIpadServerService ipadServerService;
+
+    @Autowired
+    IQwIpadServerLogService qwIpadServerLogService;
+    @Autowired
+    IQwIpadServerUserService qwIpadServerUserService;
+
+    @Autowired
+    IQwExternalContactService externalContactService;
+    @Autowired
+    WxWorkService wxWorkService;
+    @Autowired
+    private IQwUserService qwUserService;
+    @Autowired
+    private IQwDeptService qwDeptService;
+    @Autowired
+    private TenantInfoService tenantInfoService;
+
+    @Autowired
+    private IQwCompanyService qwCompanyService;
+    @Autowired
+    private TenantDataSourceManager tenantDataSourceManager;
+
+    @RequestMapping("/syncQwUserAsync")
+    public void syncQwUserAsync(String corpId) {
+        QwCompany qwCompany = qwCompanyService.selectQwCompanyByCorpId(corpId);
+        if (ObjectUtil.isEmpty(corpId)) {
+            return; // 跳过无效租户,继续下一个
+        }
+
+        TenantInfo tenantInfo = null;
+        try {
+            tenantInfo = tenantInfoService.getById(qwCompany.getTenantId());
+            if (ObjectUtil.isEmpty(tenantInfo)) {
+                log.warn("租户信息不存在,tenantId={}", qwCompany.getTenantId());
+                return;
+            }
+
+            // 切换到租户数据源
+            tenantDataSourceManager.switchTenant(tenantInfo);
+            // 切换Redis租户上下文
+            RedisTenantContext.setTenantId(tenantInfo.getId());
+
+            log.info("开始同步企微用户,租户={}, corpId={}", tenantInfo.getId(), qwCompany.getCorpId());
+
+            // 执行同步操作
+            qwUserService.syncQwUser(qwCompany.getCorpId(),tenantInfo.getId());
+
+            log.info("同步完成,租户={}", tenantInfo.getId());
+
+        } catch (Exception e) {
+            log.error("同步企微员工和部门失败,租户={}, corpId={}",
+                    qwCompany.getTenantId(), qwCompany.getCorpId(), e);
+        } finally {
+            // 清理租户上下文(数据源和Redis)
+            try {
+                tenantDataSourceManager.clear(); // 假设有此方法,请根据实际API调整
+            } catch (Exception ignored) {}
+
+            try {
+                RedisTenantContext.clear(); // 或 RedisTenantContext.removeTenantId()
+            } catch (Exception ignored) {}
+        }
+    }
+    /**
+     *
+     */
+    @GetMapping("/selectQwUserByTest")
+    public void selectQwUserByTest() {
+        try {
+            List<QwUser> list = qwUserMapper.selectQwUserByTest();
+            for (QwUser qwUser : list) {
+                try {
+
+                     Long serverId = qwUser.getServerId();
+
+                    if (serverId==null){
+                        System.out.println("serverId不存在");
+                    }else {
+                        //没绑定销售 或者 已经离职
+                        if (qwUser.getStatus()==0 || qwUser.getIsDel()==2){
+
+                            updateIpadStatus(qwUser,serverId);
+                        }
+
+                        //绑定了销售-也绑定了ipad,但是长时间离线的(离线状态,无操作超过2天的,也自动解绑)
+                        if(qwUser.getUpdateTime()!=null){
+                            Date createTime = qwUser.getUpdateTime();
+                            Integer serverStatus = qwUser.getServerStatus();
+                            Integer ipadStatus = qwUser.getIpadStatus();
+
+                            boolean result = isCreateTimeMoreThanDaysWithOptional(createTime, 2);
+                            //大于2天 ,绑定了ipad,离线
+                            if(result && serverStatus==1 && ipadStatus==0){
+                                updateIpadStatus(qwUser,serverId);
+
+                            }
+                        }
+
+
+                    }
+
+
+                } catch (Exception e) {
+                    System.out.println("解绑ipad报错"+e);
+
+                }
+            }
+        } catch (Exception e) {
+            log.error("定时处理未绑定员工企微异常",e);
+        }
+
+    }
+
+
+    public void updateIpadStatus(QwUser qwUser,Long serverId){
+        QwUser u = new QwUser();
+        u.setId(qwUser.getId());
+        u.setServerId(null);
+        u.setServerStatus(0);
+        qwUserMapper.updateQwUser(u);
+        ipadServerService.addServer(serverId);
+        QwIpadServerLog qwIpadServerLog = new QwIpadServerLog();
+        qwIpadServerLog.setType(2);
+        qwIpadServerLog.setTilie("解绑");
+        qwIpadServerLog.setServerId(serverId);
+        qwIpadServerLog.setQwUserId(qwUser.getId());
+        qwIpadServerLog.setCompanyUserId(qwUser.getCompanyUserId());
+        qwIpadServerLog.setCompanyId(qwUser.getCompanyId());
+        qwIpadServerLog.setCreateTime(new Date());
+        qwIpadServerLogService.insertQwIpadServerLog(qwIpadServerLog);
+        qwIpadServerUserService.deleteQwIpadServerUserByQwUserId(qwUser.getId());
+        WxWorkGetQrCodeDTO wxWorkGetQrCodeDTO = new WxWorkGetQrCodeDTO();
+        wxWorkGetQrCodeDTO.setUuid(qwUser.getUid());
+        wxWorkService.LoginOut(wxWorkGetQrCodeDTO,qwUser.getServerId());
+        updateIpadStatus(qwUser.getId(),0);
+    }
+
+    public static boolean isCreateTimeMoreThanDaysWithOptional(Date createTime, int days) {
+        return Optional.ofNullable(createTime)
+                .map(time -> {
+                    LocalDateTime createDateTime = time.toInstant()
+                            .atZone(ZoneId.systemDefault())
+                            .toLocalDateTime();
+                    LocalDateTime now = LocalDateTime.now();
+                    Duration duration = Duration.between(createDateTime, now);
+                    return duration.toDays() > days;
+                })
+                .orElse(false); // 为null时返回false,可根据需求调整
+    }
+
+    void updateIpadStatus(Long id ,Integer status){
+        QwUser u = new QwUser();
+        u.setId(id);
+        u.setIpadStatus(status);
+        qwUserMapper.updateQwUser(u);
+    }
+    /**
+     *
+     */
+    @GetMapping("/countQwApiAopLogToken")
+    public void countQwApiAopLogToken() {
+
+        DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+        // 获取当前日期(只包含年月日)
+        LocalDate currentDate = LocalDate.now();
+
+        String todayStr = currentDate.format(dateFormatter);
+        qwSopLogsService.countQwApiAopLogToken(todayStr);
+
+    }
+
+    /**
+     * 查询视频时长
+     */
+    @GetMapping("/getVideoDuration")
+    public Long getVideoDuration(Long videoId) {
+
+            String redisKey = "h5user:video:duration:" + videoId;
+            Long duration = redisCache.getCacheObject(redisKey);
+
+            if (duration == null) {
+                FsUserCourseVideoQVO videoInfo = fsUserCourseVideoService.selectFsUserCourseVideoByVideoIdVO(videoId,null);
+                if (videoInfo == null || videoInfo.getDuration() == null) {
+                    throw new IllegalArgumentException("视频时长信息不存在");
+                }
+                duration = videoInfo.getDuration();
+
+                // 将查询结果缓存到Redis,设置适当过期时间
+                redisCache.setCacheObject(redisKey, duration);
+            }
+
+            return duration;
+
+    }
+
+
+
+    /**
+     * 获取跳转微信小程序的链接地址
+     */
+    @GetMapping("/getGotoWxAppLink")
+    @ApiOperation("获取跳转微信小程序的链接地址")
+    public ResponseResult<String> getGotoWxAppLink(String linkStr,String appid) {
+        return ResponseResult.ok(courseLinkService.getGotoWxAppLink(linkStr,appid));
+    }
+
+    /**
+    * 发官方通连
+    */
+    @GetMapping("/sopguanfanone")
+    public R sopguanfanone(String dateTime) throws Exception {
+
+        LocalDateTime localDateTime = DateUtil.parseLocalDateTime(dateTime);
+
+        int currentHour = localDateTime.getHour();
+        LocalDate localDate = localDateTime.toLocalDate();
+
+        String taskStartTime = localDate.atTime(currentHour, 0, 0)
+                .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
+        String taskEndTime = localDate.atTime(currentHour, 59, 59)
+                .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
+
+        qwSopLogsService.createCorpMassSendingByUserLogs( taskStartTime, taskEndTime);
+        return R.ok();
+    }
+
+    /**
+    * 发一对一
+    */
+    @GetMapping("/sopguanfantwo")
+    public R sopguanfantwo(String dateTime) throws Exception {
+
+        LocalDateTime localDateTime = DateUtil.parseLocalDateTime(dateTime);
+
+
+        LocalDate localDate = localDateTime.toLocalDate();
+        String date = localDate.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
+
+        qwSopLogsService.createCorpMassSending(date);
+        return R.ok();
+    }
+
+    /**
+     * 查官方的执行结果
+     */
+    @GetMapping("/sopguanfantResult")
+    public R sopguanfantResult() throws Exception {
+        qwSopLogsService.qwSopLogsResultNew();
+        return R.ok();
+    }
+
+
+    @GetMapping("/testMaterial")
+    public void testMaterial() throws Exception {
+
+        iQwMaterialService.updateQwMaterialByQw();
+
+    }
+
+    @GetMapping("/testSop")
+    public R testSop() throws Exception {
+
+        return iFsCourseLinkService.getWxaCodeGenerateScheme("/pages_course/video.html?course={\"companyId\":100,\"companyUserId\":2020,\"corpId\":\"wweb0666cc79d79da5\",\"courseId\":61,\"link\":\"1950497651577323520\",\"linkType\":3,\"qwExternalId\":2356946,\"qwUserId\":\"1682\",\"uNo\":\"b8b010e1-ee0f-42ec-8ad8-06681d1b449a\",\"videoId\":366}","wx34bba1ae94d34986");
+    }
+
+    @GetMapping("/testRatingSop")
+    public R testRatingSop(String sopId) throws Exception {
+
+        long startTimeMillis = System.currentTimeMillis();
+        log.info("====== 开始选择和处理 sop营期-用户分级 ======");
+
+        iSopUserLogsService.ratingUserLogs(sopId);
+
+        long endTimeMillis = System.currentTimeMillis();
+        log.info("====== sop营期-用户分级处理完成,耗时 {} 毫秒 ======", (endTimeMillis - startTimeMillis));
+        return R.ok();
+    }
+
+    // 定义一个方法来批量处理插入逻辑,支持每 500 条数据一次的批量插入
+    private void processAndInsertQwSopLogs(List<QwSopLogsDoSendListTVO> logsByJsApiNotExtId) {
+        // 定义批量插入的大小
+        int batchSize = 500;
+
+        // 循环处理外部用户 ID,每次处理批量大小的子集
+        for (int i = 0; i < logsByJsApiNotExtId.size(); i += batchSize) {
+
+            int endIndex = Math.min(i + batchSize, logsByJsApiNotExtId.size());
+            List<QwSopLogsDoSendListTVO> batchList = logsByJsApiNotExtId.subList(i, endIndex);  // 获取当前批次的子集
+
+            // 直接使用批次数据进行批量更新,不需要额外的 List
+            try {
+                qwSopLogsMapper.batchUpdateQwSopLogsBySendTime(batchList);
+            } catch (Exception e) {
+                // 记录异常日志,方便后续排查问题
+                log.error("批量更新数据时发生异常,处理的批次起始索引为: " + i, e);
+            }
+        }
+    }
+
+//    @GetMapping("/test")
+//    public R test(String time, String sopId) throws Exception {
+//        log.info("进入sop任务");
+////        LocalDateTime currentTime = DateUtil.parseLocalDateTime(time);
+////        // 计算下一个整点时间
+////        LocalDateTime nextHourTime = currentTime.withMinute(0).withSecond(0).withNano(0).plusHours(1);
+////
+////        // 打印日志,确认时间
+////        log.info("任务实际执行时间: {}", currentTime);
+////        log.info("传递给任务的时间参数: {}", nextHourTime);
+//        List<String> sopidList = new ArrayList<>();
+//        if(StringUtils.isNotEmpty(sopId)){
+//            sopidList = Arrays.asList(sopId.split(","));
+//        }
+//        sopLogsTaskService.selectSopUserLogsListByTime(DateUtil.parseLocalDateTime(time), sopidList);
+//        return R.ok();
+//    }
+//    @GetMapping("/testWx")
+//    public R testWx(String time) throws Exception {
+//        sopWxLogsService.wxSopLogsByTime(DateUtil.parseLocalDateTime(time));
+//        return R.ok();
+//    }
+
+
+    @GetMapping("/testVideo")
+    public R testVideo(String sopId) throws Exception {
+        qwSopTempVoiceService.synchronous(sopId, Arrays.asList(Arrays.asList(2020L, 100L), Arrays.asList(2758L, 170L)));
+        return R.ok();
+    }
+
+    @Autowired
+    IQwCompanyService iQwCompanyService;
+    @GetMapping("/testSop2")
+    public R testSop2() throws Exception {
+
+        String cropId="ww401085d7b785aae8";
+
+        QwCompany qwCompany = iQwCompanyService.getQwCompanyByRedis(cropId);
+
+        String status="100_asddas_6666";
+
+        String url="https://open.weixin.qq.com/connect/oauth2/authorize?appid="+cropId+"&redirect_uri=" +
+                "http://"+qwCompany.getRealmNameUrl()+"/qwh5/pages/user/index?corpId="+cropId +
+                "&response_type=code&scope=snsapi_base&state="+status+"&agentid="+qwCompany.getServerAgentId()+"#wechat_redirect";
+
+        R andUpload = QRCodeUtils.createAndUpload(url);
+
+        return R.ok().put("data",andUpload);
+    }
+
+    @Autowired
+    private QwApiService qwApiService;
+
+    @GetMapping("/testSop3")
+    public R testSop3(String date) throws Exception {
+//        qwSopLogsService.createCorpMassSending(date);
+//        QwGetGroupmsgSendParam qwGetGroupmsgSendParam = new QwGetGroupmsgSendParam();
+//        qwGetGroupmsgSendParam.setMsgid("msg7tWFCgAAjJC-HqurNKsOJif5oUHQiA");
+//        qwGetGroupmsgSendParam.setUserid("ZhangZhanYue");
+//
+//        QwGroupmsgSendResult groupmsgSendResult = qwApiService.getGroupmsgSendResult(qwGetGroupmsgSendParam, "ww5a88c4f879f204c5");
+        return R.ok();
+    }
+
+    @Autowired
+    IQwSopTagService qwSopTagService;
+    @GetMapping("/tag")
+    public R tag() throws Exception {
+        qwSopTagService.addTag();
+        return R.ok();
+    }
+
+
+//    @Autowired
+//    private SopLogsChatTaskService sopLogsChatTaskService;
+//    @GetMapping("/test2")
+//    public String selectChatSopUserLogsListByTime() throws Exception {
+//        userCourseCountService.insertFsUserCourseCountTask();
+//        return "s";
+//    }
+    @GetMapping("/isAddkf")
+    public ResponseResult<FsUser> isAddkf(FsUserCourseAddCompanyUserParam param) throws Exception {
+        return courseVideoService.isAddCompanyUser(param);
+    }
+
+    @PostMapping("/updateUrl")
+    public R updateUrl()
+    {
+        log.info("开始更新URL");
+        try {
+            fsUserVideoService.updateVideoUrl();
+            huaweiObsService.uploadByCOS();
+            log.info("更新URL成功完成");
+
+
+        } catch (Exception e) {
+            log.error("开始更新URL执行失败", e);
+        }
+        return R.ok();
+    }
+    @GetMapping("/updateRedPack")
+    public R updateRedPack(String start , String end    ){
+        LocalDateTime startTime = DateUtil.parseLocalDateTime(start);
+        LocalDateTime endTime = DateUtil.parseLocalDateTime(end);
+        List<RedPacketMoneyVO> redPacketMoneyVOS = fsCourseRedPacketLogMapper.selectFsCourseRedPacketLogHourseByCompany(startTime, endTime);
+        for (RedPacketMoneyVO redPacketMoneyVO : redPacketMoneyVOS) {
+            companyService.subtractCompanyMoneyHourse(redPacketMoneyVO.getMoney(),redPacketMoneyVO.getCompanyId(), startTime.toLocalTime(), endTime.toLocalTime());
+        }
+        return R.ok();
+    }
+
+//    @GetMapping("/syncQwExternalContactUnionid")
+//    public R syncQwExternalContactUnionid(){
+//        return syncQwExternalContactService.syncQwExternalContactUnionid();
+//    }
+
+
+    @GetMapping("/queryRedPacketResult")
+    public R queryRedPacketResult(String startTime , String  endTime) {
+        fsCourseRedPacketLogService.queryRedPacketResult(startTime, endTime);
+        return R.ok();
+    }
+
+//    @GetMapping("/autoPullGroup")
+//    public R autoPullGroup(){
+//        qwTask1.autoPullGroup();
+//        return R.ok();
+//    }
+
+}

+ 6 - 0
fs-service/src/main/java/com/fs/quartz/mapper/TenantJobConfigMapper.java

@@ -45,4 +45,10 @@ public interface TenantJobConfigMapper {
                          @Param("syncMessage") String syncMessage);
 
     int updateStatus(@Param("id") Long id, @Param("status") String status);
+
+    /**
+     * 根据模板 ID 批量更新所有租户配置的状态
+     * 用于模板 defaultStatus 变更时同步更新已有配置
+     */
+    int updateStatusByTemplateId(@Param("templateId") Long templateId, @Param("status") String status);
 }

+ 21 - 0
fs-service/src/main/java/com/fs/quartz/service/impl/SysJobTemplateServiceImpl.java

@@ -7,20 +7,27 @@ import com.fs.common.utils.bean.BeanUtils;
 import com.fs.quartz.domain.SysJob;
 import com.fs.quartz.domain.SysJobTemplate;
 import com.fs.quartz.mapper.SysJobTemplateMapper;
+import com.fs.quartz.mapper.TenantJobConfigMapper;
 import com.fs.quartz.service.ISysJobService;
 import com.fs.quartz.service.ISysJobTemplateService;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 
 import javax.annotation.Resource;
 import java.util.List;
 
+@Slf4j
 @Service
 public class SysJobTemplateServiceImpl implements ISysJobTemplateService {
 
     @Resource
     private SysJobTemplateMapper sysJobTemplateMapper;
 
+    @Resource
+    private TenantJobConfigMapper tenantJobConfigMapper;
+
     @Autowired
     private ISysJobService sysJobService;
 
@@ -62,8 +69,14 @@ public class SysJobTemplateServiceImpl implements ISysJobTemplateService {
     }
 
     @Override
+    @Transactional
     @DataSource(DataSourceType.MASTER)
     public int updateTemplate(SysJobTemplate template) {
+        // 查询旧模板,判断 defaultStatus 是否变更
+        SysJobTemplate oldTemplate = sysJobTemplateMapper.selectTemplateById(template.getTemplateId());
+        String oldDefaultStatus = oldTemplate != null ? oldTemplate.getDefaultStatus() : null;
+        String newDefaultStatus = template.getDefaultStatus();
+
         int rows = sysJobTemplateMapper.updateTemplate(template);
 
         // 修改同步Job表
@@ -79,6 +92,14 @@ public class SysJobTemplateServiceImpl implements ISysJobTemplateService {
             }
         }
 
+        // 关键优化:如果 defaultStatus 变更,同步更新所有租户的 config 状态
+        if (rows > 0 && newDefaultStatus != null && !newDefaultStatus.equals(oldDefaultStatus)) {
+            int updatedConfigs = tenantJobConfigMapper.updateStatusByTemplateId(
+                template.getTemplateId(), newDefaultStatus);
+            log.info("[SysJobTemplate] defaultStatus 变更: templateId={}, old={}, new={}, 同步更新 {} 条租户配置",
+                template.getTemplateId(), oldDefaultStatus, newDefaultStatus, updatedConfigs);
+        }
+
         return rows;
     }
 

+ 6 - 0
fs-service/src/main/resources/mapper/quartz/TenantJobConfigMapper.xml

@@ -148,4 +148,10 @@
     <update id="updateStatus">
         update tenant_job_config set status = #{status} where id = #{id}
     </update>
+
+    <!-- 根据模板 ID 批量更新所有租户配置的状态 -->
+    <update id="updateStatusByTemplateId">
+        update tenant_job_config set status = #{status}, update_time = sysdate()
+        where template_id = #{templateId}
+    </update>
 </mapper>

+ 17 - 4
fs-task/src/main/java/com/fs/quartz/config/ScheduleConfig.java

@@ -25,17 +25,30 @@ public class ScheduleConfig {
         factory.setDataSource(dataSource);
 
         Properties prop = new Properties();
+        // 调度器基础配置
         prop.put("org.quartz.scheduler.instanceName", "FsScheduler");
         prop.put("org.quartz.scheduler.instanceId", "AUTO");
+        
+        // 线程池配置 - 增加线程数以避免任务排队
         prop.put("org.quartz.threadPool.class", "org.quartz.simpl.SimpleThreadPool");
-        prop.put("org.quartz.threadPool.threadCount", "20");
+        prop.put("org.quartz.threadPool.threadCount", "30");  // 从20增加到30
         prop.put("org.quartz.threadPool.threadPriority", "5");
+        
+        // JobStore 集群配置 - 优化以减少锁竞争
         prop.put("org.quartz.jobStore.isClustered", "true");
-        prop.put("org.quartz.jobStore.clusterCheckinInterval", "15000");
-        prop.put("org.quartz.jobStore.maxMisfiresToHandleAtATime", "1");
+        prop.put("org.quartz.jobStore.clusterCheckinInterval", "10000");  // 从15秒改为10秒,更快检测节点失效
+        prop.put("org.quartz.jobStore.maxMisfiresToHandleAtATime", "5");  // 从1增加到5,批量处理失火
         prop.put("org.quartz.jobStore.txIsolationLevelSerializable", "true");
-        prop.put("org.quartz.jobStore.misfireThreshold", "12000");
+        prop.put("org.quartz.jobStore.misfireThreshold", "60000");  // 从12秒增加到60秒,减少误判失火
         prop.put("org.quartz.jobStore.tablePrefix", "QRTZ_");
+        
+        // 关键优化:批量获取触发器,减少锁竞争
+        prop.put("org.quartz.scheduler.batchTriggerAcquisitionMaxCount", "10");
+        prop.put("org.quartz.scheduler.batchTriggerAcquisitionFireAheadTimeWindow", "30000");  // 30秒窗口
+        
+        // 优化:使用集群锁获取触发器,避免等待锁导致的延迟
+        prop.put("org.quartz.jobStore.acquireTriggersWithinLock", "true");
+        
         factory.setQuartzProperties(prop);
 
         factory.setSchedulerName("FsScheduler");

+ 3 - 2
fs-task/src/main/java/com/fs/quartz/config/ScheduleJobRedisConfig.java

@@ -23,7 +23,7 @@ import java.util.concurrent.TimeUnit;
 @ConditionalOnProperty(name = "spring.redis.listener.enabled", havingValue = "true", matchIfMissing = false)
 public class ScheduleJobRedisConfig {
 
-    @Bean(initMethod = "start", destroyMethod = "stop")
+    @Bean
     public RedisMessageListenerContainer scheduleJobRedisListenerContainer(
             RedisConnectionFactory connectionFactory,
             ScheduleJobSyncSubscriber scheduleJobSyncSubscriber) {
@@ -31,7 +31,8 @@ public class ScheduleJobRedisConfig {
         container.setConnectionFactory(connectionFactory);
         container.addMessageListener(scheduleJobSyncSubscriber,
                 new ChannelTopic(ScheduleConstants.REDIS_CHANNEL_JOB_SYNC));
-        container.setMaxSubscriptionRegistrationWaitingTime(TimeUnit.SECONDS.toMillis(10));
+        // 增加超时时间到30秒,避免启动时 Redis 连接未就绪导致超时
+        container.setMaxSubscriptionRegistrationWaitingTime(TimeUnit.SECONDS.toMillis(30));
         container.setTaskExecutor(Executors.newFixedThreadPool(2, r -> new Thread(r, "job-sync-listener")));
         log.info("[ScheduleJobRedis] Redis pub/sub 监听器已启用,频道={}", ScheduleConstants.REDIS_CHANNEL_JOB_SYNC);
         return container;

+ 13 - 3
fs-task/src/main/java/com/fs/quartz/util/AbstractQuartzJob.java

@@ -60,8 +60,14 @@ public abstract class AbstractQuartzJob implements Job {
 
         try {
             before(context, sysJob);
-            doExecute(context, sysJob);
-            after(context, sysJob, null);
+            boolean hasExecuted = doExecute(context, sysJob);
+            // 只有实际执行了才记录日志(租户级任务所有租户暂停时不记录)
+            if (hasExecuted) {
+                after(context, sysJob, null);
+            } else {
+                log.debug("任务未实际执行,跳过日志记录: jobId={}, jobName={}",
+                        sysJob.getJobId(), sysJob.getJobName());
+            }
         } catch (Exception e) {
             log.error("任务执行异常:jobId={}, jobName={}", sysJob.getJobId(), sysJob.getJobName(), e);
             after(context, sysJob, e);
@@ -103,7 +109,11 @@ public abstract class AbstractQuartzJob implements Job {
         }
     }
 
-    protected abstract void doExecute(JobExecutionContext context, SysJob sysJob) throws Exception;
+    /**
+     * 执行具体任务
+     * @return true 表示实际执行了任务,需要记录日志;false 表示未实际执行(如所有租户暂停),不记录日志
+     */
+    protected abstract boolean doExecute(JobExecutionContext context, SysJob sysJob) throws Exception;
 
     /** 从主库 sys_job 读取最新状态(Redis 未同步时也能生效) */
     private SysJob refreshJobStatus(SysJob cached) {

+ 41 - 11
fs-task/src/main/java/com/fs/quartz/util/MultiScopeJobDispatcher.java

@@ -49,19 +49,20 @@ public class MultiScopeJobDispatcher {
     @Value("${saas.task.parallel.threads:4}") private int parallelThreads;
     private volatile ExecutorService tenantExecutor;
 
-    public void dispatch(JobExecutionContext context, SysJob sysJob, ScopedJobExecutor executor) throws Exception {
+    public boolean dispatch(JobExecutionContext context, SysJob sysJob, ScopedJobExecutor executor) throws Exception {
         if (!JobInvokeUtil.isInvokeTargetAvailable(sysJob)) {
             log.warn("[MultiScopeJob] invokeTarget 在当前进程不可用,跳过执行: jobName={}, invokeTarget={}, jobGroup={}",
                     sysJob.getJobName(), sysJob.getInvokeTarget(), sysJob.getJobGroup());
-            return;
+            return false;
         }
         String scope = resolveScope(sysJob);
         if (ScheduleConstants.JobScope.TENANT.getValue().equals(scope)) {
             log.info("[MultiScopeJob] 租户级任务: jobName={}, templateId={}", sysJob.getJobName(), sysJob.getJobId());
-            executeTenantJob(context, sysJob, executor);
+            return executeTenantJob(context, sysJob, executor);
         } else {
             log.debug("[MultiScopeJob] 平台级任务: jobName={}", sysJob.getJobName());
             executePlatformJob(context, sysJob, executor);
+            return true;
         }
     }
 
@@ -103,19 +104,40 @@ public class MultiScopeJobDispatcher {
         }
     }
 
-    private void executeTenantJob(JobExecutionContext context, SysJob sysJob, ScopedJobExecutor executor) throws Exception {
+    private boolean executeTenantJob(JobExecutionContext context, SysJob sysJob, ScopedJobExecutor executor) throws Exception {
         List<TenantJobConfig> tenantConfigs = queryAssignedTenants(sysJob);
         if (tenantConfigs.isEmpty()) {
             log.warn("[MultiScopeJob] 租户级任务未分配任何租户: jobName={}, templateId={}",
                     sysJob.getJobName(), sysJob.getJobId());
-            return;
+            return false;
+        }
+        
+        // 关键优化:过滤掉已暂停的租户配置
+        List<TenantJobConfig> enabledConfigs = tenantConfigs.stream()
+            .filter(config -> "0".equals(config.getStatus()))
+            .collect(Collectors.toList());
+        
+        List<Long> pausedTenantIds = tenantConfigs.stream()
+            .filter(config -> !"0".equals(config.getStatus()))
+            .map(TenantJobConfig::getTenantId)
+            .collect(Collectors.toList());
+        
+        if (!pausedTenantIds.isEmpty()) {
+            log.info("[MultiScopeJob] 跳过已暂停的租户: jobName={}, tenantIds={}", 
+                sysJob.getJobName(), pausedTenantIds);
+        }
+        
+        if (enabledConfigs.isEmpty()) {
+            log.warn("[MultiScopeJob] 所有租户配置均已暂停: jobName={}, templateId={}",
+                sysJob.getJobName(), sysJob.getJobId());
+            return false;
         }
-        log.info("[MultiScopeJob] 开始并行执行租户任务: jobName={}, templateId={}, 租户数={}, tenantIds={}",
-                sysJob.getJobName(), sysJob.getJobId(), tenantConfigs.size(),
-                tenantConfigs.stream().map(TenantJobConfig::getTenantId).collect(Collectors.toList()));
-        CountDownLatch latch = new CountDownLatch(tenantConfigs.size());
+        
+        log.info("[MultiScopeJob] 开始并行执行租户任务: jobName={}, templateId={}, 总租户数={}, 启用租户数={}",
+                sysJob.getJobName(), sysJob.getJobId(), tenantConfigs.size(), enabledConfigs.size());
+        CountDownLatch latch = new CountDownLatch(enabledConfigs.size());
         AtomicInteger failCount = new AtomicInteger(0);
-        for (TenantJobConfig config : tenantConfigs) {
+        for (TenantJobConfig config : enabledConfigs) {
             getTenantExecutor().submit(() -> {
                 if (executeForOneTenant(context, sysJob, config, executor)) failCount.incrementAndGet();
                 latch.countDown();
@@ -132,18 +154,26 @@ public class MultiScopeJobDispatcher {
         if (failCount.get() > 0) {
             throw new IllegalStateException("租户任务部分失败: jobName=" + sysJob.getJobId() + ", 失败数=" + failCount.get());
         }
+        return true;
     }
 
     private List<TenantJobConfig> queryAssignedTenants(SysJob sysJob) {
         try {
             DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
             Long templateId = resolveTemplateId(sysJob);
+            Long jobId = sysJob.getJobId();
+            log.info("[MultiScopeJob] 查询租户配置: jobId={}, resolvedTemplateId={}, jobName={}",
+                    jobId, templateId, sysJob.getJobName());
             if (templateId == null) {
                 log.warn("[MultiScopeJob] 无法解析 templateId,跳过租户分发: jobName={}, jobId={}",
-                        sysJob.getJobName(), sysJob.getJobId());
+                        sysJob.getJobName(), jobId);
                 return Collections.emptyList();
             }
             List<TenantJobConfig> list = tenantJobConfigMapper.selectTenantsByTemplateId(templateId);
+            int enabledCount = list != null ? (int) list.stream().filter(c -> "0".equals(c.getStatus())).count() : 0;
+            int pausedCount = list != null ? (int) list.stream().filter(c -> !"0".equals(c.getStatus())).count() : 0;
+            log.info("[MultiScopeJob] 查询结果: templateId={}, 总数={}, 启用={}, 暂停={}",
+                    templateId, list != null ? list.size() : 0, enabledCount, pausedCount);
             return list != null ? list : Collections.emptyList();
         } finally {
             DynamicDataSourceContextHolder.clearDataSourceType();

+ 2 - 2
fs-task/src/main/java/com/fs/quartz/util/QuartzDisallowConcurrentExecution.java

@@ -15,8 +15,8 @@ import org.quartz.JobExecutionContext;
 public class QuartzDisallowConcurrentExecution extends AbstractQuartzJob {
 
     @Override
-    protected void doExecute(JobExecutionContext context, SysJob sysJob) throws Exception {
+    protected boolean doExecute(JobExecutionContext context, SysJob sysJob) throws Exception {
         MultiScopeJobDispatcher dispatcher = SpringUtils.getBean(MultiScopeJobDispatcher.class);
-        dispatcher.dispatch(context, sysJob, (ctx, job) -> JobInvokeUtil.invokeMethod(job));
+        return dispatcher.dispatch(context, sysJob, (ctx, job) -> JobInvokeUtil.invokeMethod(job));
     }
 }

+ 2 - 2
fs-task/src/main/java/com/fs/quartz/util/QuartzJobExecution.java

@@ -13,8 +13,8 @@ import org.quartz.JobExecutionContext;
 public class QuartzJobExecution extends AbstractQuartzJob {
 
     @Override
-    protected void doExecute(JobExecutionContext context, SysJob sysJob) throws Exception {
+    protected boolean doExecute(JobExecutionContext context, SysJob sysJob) throws Exception {
         MultiScopeJobDispatcher dispatcher = SpringUtils.getBean(MultiScopeJobDispatcher.class);
-        dispatcher.dispatch(context, sysJob, (ctx, job) -> JobInvokeUtil.invokeMethod(job));
+        return dispatcher.dispatch(context, sysJob, (ctx, job) -> JobInvokeUtil.invokeMethod(job));
     }
 }

+ 116 - 0
fs-task/src/main/java/com/fs/quartz/util/QuartzMonitor.java

@@ -0,0 +1,116 @@
+package com.fs.quartz.util;
+
+import com.fs.quartz.domain.SysJob;
+import com.fs.quartz.mapper.SysJobMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.quartz.JobExecutionContext;
+import org.quartz.JobKey;
+import org.quartz.Scheduler;
+import org.quartz.SchedulerException;
+import org.quartz.Trigger;
+import org.quartz.TriggerKey;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+
+/**
+ * Quartz Monitor Tool
+ */
+@Slf4j
+@Component
+public class QuartzMonitor {
+
+    @Autowired
+    private Scheduler scheduler;
+
+    @Autowired
+    private SysJobMapper sysJobMapper;
+
+    private static final DateTimeFormatter TIME_FORMATTER =
+        DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.systemDefault());
+
+    /**
+     * Diagnose job execution status
+     */
+    public void diagnoseJobExecution(Long jobId) {
+        log.info("========== Quartz Job Diagnose Start [jobId={}] ==========", jobId);
+        try {
+            SysJob dbJob = sysJobMapper.selectJobById(jobId);
+            if (dbJob == null) {
+                log.warn("Job not found in database: jobId={}", jobId);
+                return;
+            }
+            log.info("Database status: jobId={}, jobName={}, status={}, concurrent={}",
+                jobId, dbJob.getJobName(), dbJob.getStatus(), dbJob.getConcurrent());
+
+            String jobGroup = dbJob.getJobGroup();
+            JobKey jobKey = ScheduleUtils.getJobKey(jobId, jobGroup);
+            TriggerKey triggerKey = ScheduleUtils.getTriggerKey(jobId, jobGroup);
+
+            if (scheduler.checkExists(triggerKey)) {
+                Trigger trigger = scheduler.getTrigger(triggerKey);
+                log.info("Quartz Trigger exists: nextFireTime={}, prevFireTime={}",
+                    trigger.getNextFireTime() != null ? TIME_FORMATTER.format(trigger.getNextFireTime().toInstant()) : "null",
+                    trigger.getPreviousFireTime() != null ? TIME_FORMATTER.format(trigger.getPreviousFireTime().toInstant()) : "null");
+            } else {
+                log.warn("Quartz Trigger not exists");
+            }
+
+            if (scheduler.checkExists(jobKey)) {
+                List<JobExecutionContext> executingJobs = scheduler.getCurrentlyExecutingJobs();
+                boolean isExecuting = executingJobs.stream()
+                    .anyMatch(ctx -> ctx.getJobDetail().getKey().equals(jobKey));
+                log.info("Quartz Job exists, executing: {}", isExecuting);
+            } else {
+                log.warn("Quartz Job not exists");
+            }
+
+            if ("1".equals(dbJob.getConcurrent())) {
+                log.info("Job is non-concurrent, only one instance can run at same time");
+            }
+        } catch (SchedulerException e) {
+            log.error("Diagnose error", e);
+        }
+        log.info("========== Quartz Job Diagnose End [jobId={}] ==========", jobId);
+    }
+
+    /**
+     * Print executing jobs
+     */
+    public void printExecutingJobs() {
+        log.info("========== Current Executing Jobs ==========");
+        try {
+            List<JobExecutionContext> jobs = scheduler.getCurrentlyExecutingJobs();
+            if (jobs.isEmpty()) {
+                log.info("No executing jobs");
+            } else {
+                jobs.forEach(ctx -> log.info("Executing: jobKey={}", ctx.getJobDetail().getKey()));
+            }
+        } catch (SchedulerException e) {
+            log.error("Get executing jobs failed", e);
+        }
+    }
+
+    /**
+     * Force trigger job
+     */
+    public void forceTriggerJob(Long jobId, String jobGroup) {
+        log.info("Force trigger job: jobId={}", jobId);
+        try {
+            JobKey jobKey = ScheduleUtils.getJobKey(jobId, jobGroup);
+            if (!scheduler.checkExists(jobKey)) {
+                SysJob job = sysJobMapper.selectJobById(jobId);
+                if (job != null) {
+                    ScheduleUtils.createScheduleJob(scheduler, job);
+                }
+            }
+            scheduler.triggerJob(jobKey);
+            log.info("Force trigger success");
+        } catch (Exception e) {
+            log.error("Force trigger failed", e);
+        }
+    }
+}