# HkInventorySchedulerServiceImpl.java

    package cloud.wconnect.workperformance.service.hkinventory.impl;

    import cloud.wconnect.commonlib.component.BeanMapper;
    import cloud.wconnect.commonlib.cons.*;
    import cloud.wconnect.commonlib.exception.BadRequestException;
    import cloud.wconnect.commonlib.exception.ForbiddenException;
    import cloud.wconnect.commonlib.pojo.dto.PropertyDTO;
    import cloud.wconnect.commonlib.pojo.dto.scheduler.JobBodyDTO;
    import cloud.wconnect.commonlib.redis.PropertyI18nRedisService;
    import cloud.wconnect.commonlib.service.SchedulerInternalService;
    import cloud.wconnect.commonlib.util.CommonUtils;
    import cloud.wconnect.commonlib.util.CronRuleUtil;
    import cloud.wconnect.commonlib.util.TransactionUtils;
    import cloud.wconnect.commonlib.util.UpdateLogUtil;
    import cloud.wconnect.workperformance.entity.hkinventory.HkInventoryScheduler;
    import cloud.wconnect.workperformance.mapper.hkinventory.HkInventorySchedulerMapper;
    import cloud.wconnect.workperformance.pojo.dto.hkinventory.HkInventorySchedulerDTO;
    import cloud.wconnect.workperformance.service.hkinventory.IHkInventorySchedulerService;
    import com.baomidou.dynamic.datasource.annotation.DS;
    import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
    import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
    import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    import com.fasterxml.jackson.core.JsonProcessingException;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import org.apache.commons.collections.CollectionUtils;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;

    import java.text.MessageFormat;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Objects;
    import java.util.concurrent.Executor;

    /**
     * <p>
     * 服务实现类
     * </p>
     *
     * @author lichen
     * @since 2025-04-19
     */
    @Service
    @DS("work_performance")
    public class HkInventorySchedulerServiceImpl extends ServiceImpl<HkInventorySchedulerMapper, HkInventoryScheduler> implements IHkInventorySchedulerService {

        @Autowired
        private BeanMapper beanMapper;
        @Autowired
        private Executor executor;
        @Autowired
        private ObjectMapper objectMapper;
        @Autowired
        private SchedulerInternalService schedulerInternalService;
        @Value("${default.timezone}")
        private String defaultTimezone;
        @Autowired
        private UpdateLogUtil updateLogUtil;
        @Autowired
        private PropertyI18nRedisService propertyI18nRedisService;
        private final static Integer MODULE_ID = MoudleIdConstant.WORK_PERFORMANCE; // work performance module id
        private final static Integer FUNCTION_ID = FunctionIdConstant.HOUSEKEEPING_INVENTORY; // Housekeeping Inventory feature id
        private final static String DS_NAME = "work_performance";

        /**
         * 校验定时任务执行时间是否唯一
         *
         * @param propertyId      资产id
         * @param category        分类
         * @param targetScheduler 目标定时任务执行规则
         * @param ignoreId        忽略的定时任务id
         * @return void
         * @author lichen
         * @date 2025/4/26
         */
        @Override
        public void checkCornRuleSole(Integer propertyId, Integer category, String targetScheduler, String ignoreId) {
            List<HkInventoryScheduler> existScheduler = list(new LambdaQueryWrapper<HkInventoryScheduler>()
                    .eq(HkInventoryScheduler::getPropertyId, propertyId)
                    .eq(HkInventoryScheduler::getCategory, category)
                    .ne(!ignoreId.equals("0"), HkInventoryScheduler::getId, ignoreId)
                    .ne(HkInventoryScheduler::getStatus, SchedulerCons.STATUS_DELETING));
            if (CollectionUtils.isEmpty(existScheduler)) return;

            //校验执行时间是否重复
            CronRuleUtil.checkSchedulerRepeat(objectMapper, existScheduler.stream().map(HkInventoryScheduler::getScheduler).toList(), targetScheduler);
        }

        /**
         * 校验调度任务是否存在
         *
         * @param id 调度任务id
         * @return cloud.wconnect.workperformance.entity.hkinventory.HkInventoryScheduler
         * @author lichen
         * @date 2025/4/26
         */
        @Override
        public HkInventoryScheduler checkExist(String id) {
            HkInventoryScheduler scheduler = getById(id);
            if (Objects.isNull(scheduler)) {
                throw new BadRequestException(CommonErrorI18nConstant.DATA_NOT_FOUND,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(), "Housekeeping Inventory SchedulerJob not found, id = " + id);
            }
            return scheduler;
        }

        /**
         * 校验是否属于资产
         *
         * @param scheduler  调度任务
         * @param propertyId 资产id
         * @return void
         * @author lichen
         * @date 2025/4/26
         */
        @Override
        public void checkBelong(HkInventoryScheduler scheduler, Integer propertyId) {
            if (!scheduler.getPropertyId().equals(propertyId)) {
                throw new ForbiddenException(BaseErrorCodeEnum.DATA_FORBIDDEN.getI18nCode(),
                        BaseErrorCodeEnum.DATA_FORBIDDEN.getCode(),
                        MessageFormat.format("Housekeeping Inventory scheduler not belong current property,schedulerId:{0},schedulerBelongPropertyId:{1},currentPropertyId:{2}",
                                scheduler.getId(), scheduler.getPropertyId(), propertyId));
            }
        }

        /**
         * 删除任务
         *
         * @param scheduler 调度任务
         * @return void
         * @author lichen
         * @date 2025/4/26
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void delete(HkInventoryScheduler scheduler) {
            //更新为未同步 删除中
            scheduler.setSyncState(false).setStatus(SchedulerCons.STATUS_DELETING);
            updateById(scheduler);

            // 同步任务调度中心
            TransactionUtils.afterCommitAsyncExecute(executor, () -> {
                try {
                    //调用内部接口删除任务
                    callInternalServiceDeleteJob(scheduler.getId());
                    //指定数据源
                    pushDynamicDataSource();
                    //删除记录
                    removeById(scheduler.getId());
                } catch (Exception e) {
                    log.error("Delete housekeeping inventory scheduler job[" + scheduler.getId() + "] failed", e);
                }
            });
        }

        /**
         * 查询调度任务（排除掉删除中）
         *
         * @param propertyId 资产id
         * @param category   分类
         * @return java.util.List<cloud.wconnect.workperformance.entity.hkinventory.HkInventoryScheduler>
         * @author lichen
         * @date 2025/4/26
         */
        @Override
        public List<HkInventoryScheduler> listByCategory(Integer propertyId, Integer category) {
            return list(new LambdaQueryWrapper<HkInventoryScheduler>()
                    .eq(HkInventoryScheduler::getPropertyId, propertyId)
                    .eq(HkInventoryScheduler::getCategory, category)
                    .ne(HkInventoryScheduler::getStatus, SchedulerCons.STATUS_DELETING)
                    .orderByDesc(HkInventoryScheduler::getId));
        }

        /**
         * 修改调度任务
         *
         * @param propertyId   资产id
         * @param oriScheduler 修改前的调度任务
         * @param schedulerDTO 修改后的调度任务DTO
         * @param loginUserId  登录用户id
         * @return java.lang.String
         * @author lichen
         * @date 2025/4/26
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public String update(Integer propertyId, HkInventoryScheduler oriScheduler, HkInventorySchedulerDTO schedulerDTO, int loginUserId) {
            PropertyDTO propertyDTO = propertyI18nRedisService.get(propertyId);
            //修改调度任务
            HkInventoryScheduler scheduler = beanMapper.map(schedulerDTO, HkInventoryScheduler.class);
            scheduler.setPropertyId(propertyId);
            scheduler.setRunTimes(0);
            scheduler.setUpdateLog(updateLogUtil.append(oriScheduler.getUpdateLog(), loginUserId));
            //设为未同步
            scheduler.setSyncState(false);

            String originJobId = oriScheduler.getId();
            //更新调度任务的 job id
            updateAndGenerateNewJobId(scheduler);
            String newJobId = scheduler.getId();

            //同步任务调度中心
            TransactionUtils.afterCommitAsyncExecute(executor, () -> {
                if (scheduler.getStatus() == SchedulerCons.STATUS_START) {
                    try {
                        //调用内部接口保存任务
                        callInternalServiceLaunchJob(originJobId, scheduler, propertyDTO, defaultTimezone);
                        //指定数据源
                        pushDynamicDataSource();
                        //更新为已同步
                        updateSyncState(newJobId, true);
                    } catch (Exception e) {
                        log.error("Launch housekeeping inventory scheduler job[" + newJobId + "] failed", e);
                    }
                } else {
                    try {
                        //调用内部接口删除任务
                        callInternalServiceDeleteJob(originJobId);
                        //指定数据源
                        pushDynamicDataSource();
                        //更新为已同步
                        updateSyncState(newJobId, true);
                    } catch (Exception e) {
                        log.error("Delete housekeeping inventory scheduler job[" + originJobId + "] failed", e);
                    }
                }
            });
            return newJobId;
        }

        /**
         * 创建调度任务
         *
         * @param propertyId   资产id
         * @param schedulerDTO 调度任务DTO
         * @param loginUserId  登录用户id
         * @return java.lang.String
         * @author lichen
         * @date 2025/4/26
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public String createScheduler(Integer propertyId, HkInventorySchedulerDTO schedulerDTO, int loginUserId) {
            PropertyDTO propertyDTO = propertyI18nRedisService.get(propertyId);

            HkInventoryScheduler scheduler = beanMapper.map(schedulerDTO, HkInventoryScheduler.class);
            scheduler.setRunTimes(0);
            scheduler.setPropertyId(propertyId);

            // 如果新任务的status为0-停止，则只创建DB记录，不创建定时任务job（DB记录的sync_state设为1）
            if (scheduler.getStatus() == SchedulerCons.STATUS_STOP) {
                scheduler.setSyncState(true);
            } else {
                scheduler.setSyncState(false);
            }
            scheduler.setUpdateLog(updateLogUtil.get(loginUserId));
            save(scheduler);

            //同步定时任务
            if (!scheduler.getSyncState()) {
                TransactionUtils.afterCommitAsyncExecute(executor, () -> {
                    // 同步任务调度中心
                    try {
                        //调用内部接口保存任务
                        callInternalServiceLaunchJob(StringUtils.EMPTY, scheduler, propertyDTO, defaultTimezone);
                        //指定数据源
                        pushDynamicDataSource();
                        //更新为已同步
                        updateSyncState(scheduler.getId(), true);
                    } catch (Exception e) {
                        log.error("Launch housekeeping inventory scheduler job[" + scheduler.getId() + "] failed", e);
                    }
                });
            }
            return scheduler.getId();
        }

        /**
         * 校验同一个category下最大可配置的数量
         *
         * @param propertyId 资产id
         * @param category   分类
         * @param maxNum     最大可配置的数量
         * @return void
         * @author lichen
         * @date 2025/4/26
         */
        @Override
        public void checkMaxNum(Integer propertyId, Integer category, int maxNum) {
            long count = count(new LambdaQueryWrapper<HkInventoryScheduler>().eq(HkInventoryScheduler::getPropertyId, propertyId)
                    .eq(HkInventoryScheduler::getCategory, category)
                    .ne(HkInventoryScheduler::getStatus, SchedulerCons.STATUS_DELETING));
            if (count >= maxNum) {
                throw new BadRequestException(CommonErrorI18nConstant.SCHEDULER_COUNT_TRANSFINITE,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        MessageFormat.format("Housekeeping inventory scheduler maximum configuration:{0}, propertyId{1}, categroy:{2}",
                                maxNum, propertyId, category));
            }
        }

        /**
         * 更新同步状态
         *
         * @param id        主键ID
         * @param syncState 同步状态
         * @return void
         * @author lichen
         * @date 2025/4/26
         */
        @Override
        public void updateSyncState(String id, boolean syncState) {
            updateById(new HkInventoryScheduler().setId(id).setSyncState(syncState));
        }

        /**
         * 刷新调度任务同步状态为已同步
         *
         * @param ids 调度任务ID集合
         * @return void
         * @author lichen
         * @date 2024/3/12
         */
        @Override
        public void refreshSyncStateByIds(List<String> ids) {
            update(new LambdaUpdateWrapper<HkInventoryScheduler>()
                    .in(HkInventoryScheduler::getId, ids)
                    .set(HkInventoryScheduler::getSyncState, Boolean.TRUE));
        }

        /**
         * 调度任务次数加1
         *
         * @param jobId 调度任务id
         * @return void
         * @author lichen
         * @date 2025/4/26
         */
        @Override
        public void plusRunTimes(String jobId) {
            baseMapper.plusRunTimes(jobId);
        }

        /**
         * 调用内部接口删除任务
         *
         * @param jobId 任务ID
         * @return void
         * @author lichen
         * @date 2025/4/26
         */
        private void callInternalServiceDeleteJob(String jobId) {
            schedulerInternalService.deleteJob(new JobBodyDTO().setJobId(jobId)
                    .setModuleId(MODULE_ID).setFunctionId(FUNCTION_ID));
        }

        /**
         * 设置数据源
         *
         * @return void
         * @author lichen
         * @date 2025/4/26
         */
        private void pushDynamicDataSource() {
            DynamicDataSourceContextHolder.push(DS_NAME);
        }

        /**
         * 调用内部接口保存任务
         *
         * @param originJobId     原调度任务 id
         * @param scheduler       调度任务对象
         * @param propertyDTO     资产DTO
         * @param defaultTimezone 默认时区
         * @return void
         * @author lichen
         * @date 2025/4/26
         */
        private void callInternalServiceLaunchJob(String originJobId, HkInventoryScheduler scheduler, PropertyDTO propertyDTO, String defaultTimezone) throws JsonProcessingException {
            JobBodyDTO task = new JobBodyDTO()
                    .setJobId(scheduler.getId()).setOriginJobId(originJobId)
                    .setModuleId(MODULE_ID).setFunctionId(FUNCTION_ID)
                    .setStartDate(scheduler.getStartDate())
                    .setCronRule(CronRuleUtil.generateCronRuleForScheduler(objectMapper.readValue(scheduler.getScheduler(), HashMap.class),
                            scheduler.getStartDate(), propertyDTO.getTimezone(), defaultTimezone));
            schedulerInternalService.launchJob(task);
        }

        /**
         * 更新调度任务和生成新的 job id
         *
         * @param scheduler 调度任务
         * @author lichen
         * @date 2025/4/26
         */
        private void updateAndGenerateNewJobId(HkInventoryScheduler scheduler) {
            String newJobId = CommonUtils.randomUuidWithoutDashes();
            //更新id和定时任务对象
            update(scheduler, new LambdaUpdateWrapper<HkInventoryScheduler>()
                    .set(HkInventoryScheduler::getId, newJobId)
                    .eq(HkInventoryScheduler::getId, scheduler.getId()));
            scheduler.setId(newJobId);
        }
    }