|
|
@@ -0,0 +1,639 @@
|
|
|
+package com.lc.ibps.platform.plan.job;
|
|
|
+
|
|
|
+import com.alibaba.fastjson.JSON;
|
|
|
+import com.alibaba.fastjson.JSONObject;
|
|
|
+import com.lc.ibps.base.core.util.AppUtil;
|
|
|
+import com.lc.ibps.base.core.util.BeanUtils;
|
|
|
+import com.lc.ibps.base.core.util.JacksonUtil;
|
|
|
+import com.lc.ibps.org.party.persistence.entity.QualityMetricsForHcqdDto;
|
|
|
+import com.lc.ibps.platform.dao.QualityMetricsForHcqdDao;
|
|
|
+import org.apache.commons.lang3.StringUtils;
|
|
|
+import org.quartz.JobDataMap;
|
|
|
+import org.quartz.JobExecutionContext;
|
|
|
+import org.slf4j.Logger;
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
+
|
|
|
+import java.time.*;
|
|
|
+import java.time.format.DateTimeFormatter;
|
|
|
+import java.util.*;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 华创启德专用质量指标推送任务
|
|
|
+ * 该定时任务用于处理华创启德相关的质量指标计算和推送
|
|
|
+ *
|
|
|
+ * 主要业务流程:
|
|
|
+ * 1. 获取所有启用的质量指标计划(shi_fou_qi_yong_='Y')
|
|
|
+ * 2. 解析每个计划的周期参数(zhou_qi_can_shu_1 JSON格式)
|
|
|
+ * 3. 计算从计划创建到今天的所有周期日期
|
|
|
+ * 4. 检查每个周期是否已经推送过(避免重复)
|
|
|
+ * 5. 判断今天是否需要推送(周期日期=今天且日期偏移满足条件)
|
|
|
+ * 6. 插入填写记录到t_zlzbtxjlb表
|
|
|
+ * 7. 刷新计划的下次推送时间(xia_ci_tui_song_t)
|
|
|
+ *
|
|
|
+ * 核心方法调用链:
|
|
|
+ * executeJob() → processAllPlans() → processSinglePlan() → processCyclePush()
|
|
|
+ */
|
|
|
+public class QualityMetricsForHcqdJob extends AbstractJob {
|
|
|
+
|
|
|
+
|
|
|
+ private static final Logger logger = LoggerFactory.getLogger(QualityMetricsForHcqdJob.class);
|
|
|
+
|
|
|
+ // 日期格式化器
|
|
|
+ private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
|
|
+ private static final DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void executeJob(JobExecutionContext context) throws Exception {
|
|
|
+ JobDataMap dataMap = context.getMergedJobDataMap();
|
|
|
+ logger.warn("group={} job={} trigger={} is running.",
|
|
|
+ context.getJobDetail().getKey().getGroup(),
|
|
|
+ context.getJobDetail().getKey().getName(),
|
|
|
+ context.getTrigger().getKey().getName());
|
|
|
+ logger.warn("jobDataMap=is {}.", JacksonUtil.toJsonString(dataMap.getWrappedMap()));
|
|
|
+
|
|
|
+ logger.warn("QualityMetricsForHcqdJob定时任务开始执行...");
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 1. 获取DAO实例
|
|
|
+ QualityMetricsForHcqdDao qualityMetricsDao = AppUtil.getBean(QualityMetricsForHcqdDao.class);
|
|
|
+
|
|
|
+ // 2. 获取所有启用的计划
|
|
|
+ List<QualityMetricsForHcqdDto> enabledPlans = qualityMetricsDao.findEnabledPlans();
|
|
|
+ if (BeanUtils.isEmpty(enabledPlans)) {
|
|
|
+ logger.warn("没有找到启用的质量指标计划");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ logger.warn("找到 {} 个启用的质量指标计划", enabledPlans.size());
|
|
|
+
|
|
|
+ // 3. 处理所有计划
|
|
|
+ processAllPlans(enabledPlans, qualityMetricsDao);
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ logger.error("QualityMetricsForHcqdJob执行异常: {}", e.getMessage(), e);
|
|
|
+ throw e;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理所有计划的主方法
|
|
|
+ */
|
|
|
+ private void processAllPlans(List<QualityMetricsForHcqdDto> plans, QualityMetricsForHcqdDao dao) {
|
|
|
+ LocalDate today = LocalDate.now();
|
|
|
+ int totalProcessed = 0;
|
|
|
+
|
|
|
+ for (QualityMetricsForHcqdDto plan : plans) {
|
|
|
+ try {
|
|
|
+ //执行推送
|
|
|
+ int processed = processSinglePlan(plan, today, dao);
|
|
|
+ totalProcessed += processed;
|
|
|
+
|
|
|
+ // 刷新下次推送时间
|
|
|
+ refreshNextPushTime(plan, dao);
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ logger.error("处理计划 {} 时发生异常: {}", plan.getJiHuaMingCheng(), e.getMessage(), e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ logger.warn("QualityMetricsForHcqdJob定时任务执行完成,共处理了 {} 条记录", totalProcessed);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理单个计划
|
|
|
+ */
|
|
|
+ private int processSinglePlan(QualityMetricsForHcqdDto plan, LocalDate today, QualityMetricsForHcqdDao dao) {
|
|
|
+ // 1. 解析周期参数
|
|
|
+ CycleParams cycleParams = parseCycleParams(plan.getZhouQiCanShu1(), plan.getZhouQiLeiXing());
|
|
|
+ if (cycleParams == null) {
|
|
|
+ logger.warn("计划 {} 的周期参数解析失败", plan.getJiHuaMingCheng());
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 获取计划的起始时间(编制时间)
|
|
|
+ LocalDate StartDate = getPlanStartDate(plan);
|
|
|
+ if (StartDate == null) {
|
|
|
+ logger.warn("计划 {} 的创建时间无效", plan.getJiHuaMingCheng());
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 获取已推送日期集合
|
|
|
+ Set<String> existingDateSet = getExistingRecordDates(plan.getId(), dao);
|
|
|
+
|
|
|
+ // 4. 计算所有周期日期
|
|
|
+ List<LocalDate> cycleDates = calculateAllCycleDates(StartDate, today, cycleParams);
|
|
|
+
|
|
|
+ // 5. 处理周期推送
|
|
|
+ return processCyclePush(plan, cycleDates, today, cycleParams, existingDateSet, dao);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 刷新下次推送时间
|
|
|
+ */
|
|
|
+ private void refreshNextPushTime(QualityMetricsForHcqdDto plan, QualityMetricsForHcqdDao dao) {
|
|
|
+ try {
|
|
|
+ // 检查计划是否启用,只有启用的计划才刷新下次推送时间
|
|
|
+ if (!"Y".equals(plan.getShiFouQiYong())) {
|
|
|
+ logger.debug("计划 {} 未启用,不刷新下次推送时间", plan.getJiHuaMingCheng());
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 1. 解析周期参数
|
|
|
+ CycleParams cycleParams = parseCycleParams(plan.getZhouQiCanShu1(), plan.getZhouQiLeiXing());
|
|
|
+ if (cycleParams == null) {
|
|
|
+ logger.warn("计划 {} 的周期参数解析失败,无法刷新下次推送时间", plan.getJiHuaMingCheng());
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 计算下次推送时间
|
|
|
+ LocalDate nextPushDate = calculateNextPushDate(plan, cycleParams, dao);
|
|
|
+
|
|
|
+ // 3. 更新数据库
|
|
|
+ String nextPushStr = nextPushDate.format(DATE_FORMATTER);
|
|
|
+ int result = dao.updateNextPushTime(plan.getId(), nextPushStr);
|
|
|
+
|
|
|
+ if (result > 0) {
|
|
|
+ logger.debug("计划 {} 的下次推送时间已更新为: {}", plan.getJiHuaMingCheng(), nextPushStr);
|
|
|
+ } else {
|
|
|
+ logger.warn("计划 {} 的下次推送时间更新失败", plan.getJiHuaMingCheng());
|
|
|
+ }
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ logger.error("刷新计划 {} 的下次推送时间时发生异常: {}", plan.getJiHuaMingCheng(), e.getMessage(), e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算下次推送时间
|
|
|
+ */
|
|
|
+ private LocalDate calculateNextPushDate(QualityMetricsForHcqdDto plan, CycleParams cycleParams, QualityMetricsForHcqdDao dao) {
|
|
|
+ LocalDate today = LocalDate.now();
|
|
|
+ logger.debug("calculateNextPushDate: 计划={}, 今天={}", plan.getJiHuaMingCheng(), today);
|
|
|
+
|
|
|
+ LocalDate nextPushDate;
|
|
|
+
|
|
|
+ // 获取已推送的最近日期
|
|
|
+ LocalDate latestRecordDate = getLatestRecordDate(plan, dao);
|
|
|
+ logger.debug(" 最近记录日期: {}", latestRecordDate);
|
|
|
+
|
|
|
+ if (latestRecordDate != null && !latestRecordDate.isBefore(today)) {
|
|
|
+ // 如果最近记录日期在今天或之后,从最近记录日期计算
|
|
|
+ logger.debug(" 最近记录日期在今天或之后,从最近记录日期计算");
|
|
|
+ nextPushDate = calculateNextCycleDate(latestRecordDate, cycleParams);
|
|
|
+ logger.debug(" 从最近记录日期计算的下次推送日期: {}", nextPushDate);
|
|
|
+ } else {
|
|
|
+ // 否则从今天开始计算
|
|
|
+ logger.debug(" 从今天开始计算第一个有效日期");
|
|
|
+ nextPushDate = calculateFirstValidDate(today, cycleParams);
|
|
|
+ logger.debug(" 计算出的第一个有效日期: {}", nextPushDate);
|
|
|
+
|
|
|
+ // 如果今天已经达到或超过推送日,计算下一个周期
|
|
|
+ if (isTodayAfterOrEqualPushDate(nextPushDate, today)) {
|
|
|
+ // 已经推送过或今天就是推送日,计算下一个周期
|
|
|
+ logger.debug(" 今天已达到或超过推送日,计算下一个周期");
|
|
|
+ nextPushDate = calculateNextCycleDate(nextPushDate, cycleParams);
|
|
|
+ logger.debug(" 下一个周期日期: {}", nextPushDate);
|
|
|
+ } else {
|
|
|
+ logger.debug(" 今天未达到推送日,使用当前计算日期");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ logger.debug(" 最终下次推送日期: {}", nextPushDate);
|
|
|
+ return nextPushDate;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取计划的最新记录日期
|
|
|
+ */
|
|
|
+ private LocalDate getLatestRecordDate(QualityMetricsForHcqdDto plan, QualityMetricsForHcqdDao dao) {
|
|
|
+ try {
|
|
|
+ String latestDateStr = dao.findLatestRecordDateByPlan(plan.getId());
|
|
|
+ if (StringUtils.isNotBlank(latestDateStr)) {
|
|
|
+ return LocalDate.parse(latestDateStr, DATE_FORMATTER);
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ logger.warn("获取计划 {} 的最新记录日期失败: {}", plan.getJiHuaMingCheng(), e.getMessage());
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算第一个符合条件的日期(从指定日期开始)
|
|
|
+ */
|
|
|
+ private LocalDate calculateFirstValidDate(LocalDate startDate, CycleParams params) {
|
|
|
+ LocalDate currentDate = startDate;
|
|
|
+
|
|
|
+ logger.debug("calculateFirstValidDate: start={}, type={}, monthOffset={}, dayOffset={}",
|
|
|
+ startDate, params.cycleType, params.monthOffset, params.dayOffset);
|
|
|
+
|
|
|
+ // 步骤1:根据周期类型调整到周期起始点
|
|
|
+ // - 季度计划:调整到所在季度的第一天(1月1日、4月1日等)
|
|
|
+ // - 半年度计划:调整到所在半年度的第一天(1月1日或7月1日)
|
|
|
+ // - 年度计划:调整到当年的第一天(1月1日)
|
|
|
+ // - 月度计划:不需要调整,直接使用传入的日期
|
|
|
+ switch (params.cycleType) {
|
|
|
+ case "季度":
|
|
|
+ currentDate = adjustToQuarterStart(currentDate);
|
|
|
+ logger.debug(" 调整到季度开始: {}", currentDate);
|
|
|
+ break;
|
|
|
+ case "半年度":
|
|
|
+ currentDate = adjustToHalfYearStart(currentDate);
|
|
|
+ logger.debug(" 调整到半年度开始: {}", currentDate);
|
|
|
+ break;
|
|
|
+ case "年度":
|
|
|
+ currentDate = adjustToYearStart(currentDate);
|
|
|
+ logger.debug(" 调整到年度开始: {}", currentDate);
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ // 月度计划不需要调整周期开始
|
|
|
+ logger.debug(" 月度计划,不调整周期开始");
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 步骤2:应用月份偏移
|
|
|
+ // monthOffset表示在周期内的第几个月执行(1表示第1个月,0表示不使用月份偏移)
|
|
|
+ // 例如:monthOffset=1 表示第1个月(加0个月),monthOffset=2 表示第2个月(加1个月)
|
|
|
+ // 对于季度计划:monthOffset=2 表示季度的第2个月(需要加1个月)
|
|
|
+ // 对于年度计划:monthOffset=6 表示年度第6个月(需要加5个月)
|
|
|
+ // monthOffset=0 表示不使用月份偏移(如月度计划)
|
|
|
+ if (params.monthOffset > 0) {
|
|
|
+ // monthOffset=1表示第1个月,需要加0个月
|
|
|
+ // monthOffset=2表示第2个月,需要加1个月
|
|
|
+ // 以此类推:需要加 (monthOffset - 1) 个月
|
|
|
+ int monthsToAdd = params.monthOffset - 1;
|
|
|
+ currentDate = currentDate.plusMonths(monthsToAdd);
|
|
|
+ logger.debug(" 应用月份偏移: +{}个月 = {}", monthsToAdd, currentDate);
|
|
|
+ } else {
|
|
|
+ logger.debug(" 月份偏移为0,不应用月份偏移");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 步骤3:应用日期偏移
|
|
|
+ // dayOffset表示在月份内的第几天执行(从1开始)
|
|
|
+ // 例如:dayOffset=15 表示每月15日
|
|
|
+ // 需要确保日期不超过当月的最后一天
|
|
|
+ if (params.dayOffset > 0) {
|
|
|
+ // 确保日期不超过当月最后一天
|
|
|
+ int day = Math.min(params.dayOffset, currentDate.lengthOfMonth());
|
|
|
+ currentDate = LocalDate.of(currentDate.getYear(), currentDate.getMonth(), day);
|
|
|
+ logger.debug(" 应用日期偏移: 第{}天 = {}", day, currentDate);
|
|
|
+ } else {
|
|
|
+ logger.debug(" 日期偏移为0,不应用日期偏移");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 步骤4:处理开始日期已过推送日的情况
|
|
|
+ // 例如:计划创建于1月20日,但推送日是每月15日
|
|
|
+ // 那么第一个推送日应该是下个月的15日,而不是上个月的15日
|
|
|
+ if (startDate.isAfter(currentDate)) {
|
|
|
+ logger.debug(" 开始日期{}已过推送日{},计算下一个周期", startDate, currentDate);
|
|
|
+ currentDate = calculateNextCycleDate(currentDate, params);
|
|
|
+ }
|
|
|
+
|
|
|
+ logger.debug(" 最终计算出的第一个有效推送日期: {}", currentDate);
|
|
|
+ return currentDate;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 以下是原有的辅助方法,保持原有逻辑 ====================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取计划的始时间(编制时间)
|
|
|
+ */
|
|
|
+ private LocalDate getPlanStartDate(QualityMetricsForHcqdDto plan) {
|
|
|
+ // 获取原始日期字符串
|
|
|
+ String dateStr = plan.getBianZhiShiJian();
|
|
|
+ // 如果长度是10,说明只有日期部分,补上时间 00:00:00
|
|
|
+ if (dateStr.length() == 10) {
|
|
|
+ dateStr = dateStr + " 00:00:00";
|
|
|
+ }
|
|
|
+ LocalDateTime ldt = LocalDateTime.parse(dateStr, DATETIME_FORMATTER);
|
|
|
+ Date startTime = Date.from(ldt.atZone(java.time.ZoneId.systemDefault()).toInstant());
|
|
|
+ if (startTime == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return startTime.toInstant()
|
|
|
+ .atZone(ZoneId.systemDefault())
|
|
|
+ .toLocalDate();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取已存在的记录日期集合
|
|
|
+ */
|
|
|
+ private Set<String> getExistingRecordDates(String planId, QualityMetricsForHcqdDao dao) {
|
|
|
+ List<String> existingDates = dao.findRecordDatesByPlan(planId);
|
|
|
+ return new HashSet<>(existingDates);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理周期推送
|
|
|
+ */
|
|
|
+ private int processCyclePush(QualityMetricsForHcqdDto plan, List<LocalDate> cycleDates,
|
|
|
+ LocalDate today, CycleParams cycleParams,
|
|
|
+ Set<String> existingDateSet, QualityMetricsForHcqdDao dao) {
|
|
|
+ int processedCount = 0;
|
|
|
+
|
|
|
+ // 遍历所有计算出的周期日期
|
|
|
+ // cycleDates包含从计划创建到今天的所有应该推送的日期
|
|
|
+ for (LocalDate cycleDate : cycleDates) {
|
|
|
+ // 将日期格式化为字符串,用于数据库比较
|
|
|
+ String cycleDateStr = cycleDate.format(DATE_FORMATTER);
|
|
|
+
|
|
|
+ // 步骤1:检查该周期是否已经推送过
|
|
|
+ // 通过比对已存在的记录日期集合,避免重复插入
|
|
|
+ if (existingDateSet.contains(cycleDateStr)) {
|
|
|
+ continue; // 已存在,跳过处理
|
|
|
+ }
|
|
|
+
|
|
|
+ // 步骤2:插入填写记录
|
|
|
+ // cycleDate已经通过calculateFirstValidDate正确计算,应该直接推送
|
|
|
+ // 取消shouldPushDate检查,避免大小月问题
|
|
|
+ // 将计划数据复制到填写记录表,关联计划ID和周期日期
|
|
|
+ if (insertRecordForPlan(plan, cycleDateStr, dao)) {
|
|
|
+ processedCount++;
|
|
|
+ logger.warn("计划 {} 的 {} 周期记录已插入", plan.getJiHuaMingCheng(), cycleDateStr);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 记录处理结果
|
|
|
+ if (processedCount > 0) {
|
|
|
+ logger.warn("计划 {} 处理完成,新增了 {} 条记录", plan.getJiHuaMingCheng(), processedCount);
|
|
|
+ }
|
|
|
+
|
|
|
+ return processedCount;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 解析周期参数
|
|
|
+ */
|
|
|
+ private CycleParams parseCycleParams(String cycleParamStr, String cycleType) {
|
|
|
+ if (StringUtils.isBlank(cycleParamStr)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ JSONObject json = JSON.parseObject(cycleParamStr);
|
|
|
+ int[] params = null;
|
|
|
+
|
|
|
+ switch (cycleType) {
|
|
|
+ case "月度":
|
|
|
+ params = parseIntArray(json.getJSONArray("month"));
|
|
|
+ break;
|
|
|
+ case "季度":
|
|
|
+ params = parseIntArray(json.getJSONArray("quarter"));
|
|
|
+ break;
|
|
|
+ case "半年度":
|
|
|
+ params = parseIntArray(json.getJSONArray("halfYear"));
|
|
|
+ break;
|
|
|
+ case "年度":
|
|
|
+ params = parseIntArray(json.getJSONArray("year"));
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ logger.warn("未知的周期类型: {}", cycleType);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (params == null || params.length < 2) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 记录解析结果用于调试
|
|
|
+ logger.debug("解析周期参数: type={}, monthOffset={}, dayOffset={}, raw={}",
|
|
|
+ cycleType, params[0], params[1], cycleParamStr);
|
|
|
+
|
|
|
+ return new CycleParams(cycleType, params[0], params[1]);
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ logger.error("解析周期参数失败: {}, 原始参数: {}", e.getMessage(), cycleParamStr);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 解析FastJSON数组为Java整型数组
|
|
|
+ * 主要处理周期参数中的月份和日期偏移量,数组固定长度为2
|
|
|
+ * 支持null值处理,null转换为0表示不使用偏移(对于月份)或第1天(对于日期)
|
|
|
+ * 确保即使JSON数组长度不足也能安全处理
|
|
|
+ *
|
|
|
+ * 注意:对于月份偏移,0表示第1个月或不使用月份偏移(月度计划)
|
|
|
+ * 对于日期偏移,0表示第1天
|
|
|
+ */
|
|
|
+ private int[] parseIntArray(com.alibaba.fastjson.JSONArray jsonArray) {
|
|
|
+ if (jsonArray == null || jsonArray.isEmpty()) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ int[] result = new int[2];
|
|
|
+ for (int i = 0; i < 2 && i < jsonArray.size(); i++) {
|
|
|
+ Object value = jsonArray.get(i);
|
|
|
+ if (value == null) {
|
|
|
+ result[i] = 0;
|
|
|
+ } else {
|
|
|
+ result[i] = jsonArray.getInteger(i);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算指定时间范围内的所有周期日期
|
|
|
+ * 从开始日期找到第一个符合条件的日期作为起始点
|
|
|
+ * 按周期类型连续计算下一个日期,直到超过结束日期
|
|
|
+ * 返回所有符合条件的日期列表,用于判断哪些周期需要推送
|
|
|
+ *
|
|
|
+ * 周期计算示例:
|
|
|
+ * 1. 月度计划(每月15日,开始日期2024-01-01,结束日期2024-12-31):
|
|
|
+ * → 2024-01-15, 2024-02-15, ..., 2024-12-15
|
|
|
+ * 2. 季度计划(每季度第2个月第5天):
|
|
|
+ * → 2024-02-05, 2024-05-05, 2024-08-05, 2024-11-05
|
|
|
+ * 3. 半年度计划(每半年第5个月第8天):
|
|
|
+ * → 2024-05-08, 2024-11-08
|
|
|
+ * 4. 年度计划(每年第6个月第16天):
|
|
|
+ * → 2024-06-16
|
|
|
+ *
|
|
|
+ * 关键处理逻辑:
|
|
|
+ * - 考虑月份偏移(monthOffset):在周期内的第几个月(0表示第1个月)
|
|
|
+ * - 考虑日期偏移(dayOffset):在月份内的第几天(1表示第1天)
|
|
|
+ * - 考虑周期类型(cycleType):月/季/半年/年
|
|
|
+ * - 避免重复计算:通过existingDateSet检查
|
|
|
+ * - 时间边界处理:确保不计算超过结束日期的日期
|
|
|
+ *
|
|
|
+ * 注意:此方法使用当前周期参数计算历史日期
|
|
|
+ * 如果周期配置变更(如从每月15日改为17日),会重新计算历史周期
|
|
|
+ * 这可能产生"补推"效果,但也可能导致同一时间段内多个记录
|
|
|
+ * 例如:原配置每月15日已推送1月15日记录,新配置每月17日会补推1月17日记录
|
|
|
+ * 这取决于业务需求:是否允许补推变更前的周期
|
|
|
+ */
|
|
|
+ private List<LocalDate> calculateAllCycleDates(LocalDate startDate, LocalDate endDate, CycleParams params) {
|
|
|
+ List<LocalDate> dates = new ArrayList<>();
|
|
|
+
|
|
|
+ // 步骤1:找到第一个有效的周期日期
|
|
|
+ // 从计划的创建日期开始,根据周期类型、月份偏移和日期偏移
|
|
|
+ // 计算出第一个应该推送的日期
|
|
|
+ // 例如:月度计划每月15日,季度计划每季度第2个月第5天等
|
|
|
+ LocalDate currentDate = calculateFirstValidDate(startDate, params);
|
|
|
+
|
|
|
+ // 步骤2:循环生成所有周期日期
|
|
|
+ // 从第一个有效日期开始,按周期类型(月/季/半年/年)递增
|
|
|
+ // 直到生成的日期超过指定的结束日期(通常是今天)
|
|
|
+ while (!currentDate.isAfter(endDate)) {
|
|
|
+ // 将当前周期日期添加到结果列表
|
|
|
+ dates.add(currentDate);
|
|
|
+
|
|
|
+ // 步骤3:计算下一个周期日期
|
|
|
+ // 根据周期类型增加相应的时间间隔:
|
|
|
+ // - 月度:增加1个月
|
|
|
+ // - 季度:增加3个月
|
|
|
+ // - 半年度:增加6个月
|
|
|
+ // - 年度:增加1年
|
|
|
+ currentDate = calculateNextCycleDate(currentDate, params);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 步骤4:返回所有计算出的周期日期
|
|
|
+ // 这个列表包含了从计划创建到指定结束日期之间
|
|
|
+ // 所有应该触发推送的日期
|
|
|
+ return dates;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 调整日期到当前季度的开始
|
|
|
+ * 将任意日期调整为所在季度的第1个月第1天
|
|
|
+ * 用于季度计划的周期计算,确保从季度起始点开始
|
|
|
+ * 1-3月调整为1月1日,4-6月调整为4月1日,以此类推
|
|
|
+ */
|
|
|
+ private LocalDate adjustToQuarterStart(LocalDate date) {
|
|
|
+ int monthValue = date.getMonthValue();
|
|
|
+ if (monthValue <= 3) {
|
|
|
+ return LocalDate.of(date.getYear(), 1, 1);
|
|
|
+ } else if (monthValue <= 6) {
|
|
|
+ return LocalDate.of(date.getYear(), 4, 1);
|
|
|
+ } else if (monthValue <= 9) {
|
|
|
+ return LocalDate.of(date.getYear(), 7, 1);
|
|
|
+ } else {
|
|
|
+ return LocalDate.of(date.getYear(), 10, 1);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 调整日期到当前半年度的开始
|
|
|
+ * 将任意日期调整为所在半年度的第1个月第1天
|
|
|
+ * 用于半年度计划的周期计算,确保从半年度起始点开始
|
|
|
+ * 1-6月调整为1月1日,7-12月调整为7月1日
|
|
|
+ */
|
|
|
+ private LocalDate adjustToHalfYearStart(LocalDate date) {
|
|
|
+ int monthValue = date.getMonthValue();
|
|
|
+ if (monthValue <= 6) {
|
|
|
+ return LocalDate.of(date.getYear(), 1, 1);
|
|
|
+ } else {
|
|
|
+ return LocalDate.of(date.getYear(), 7, 1);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 调整日期到当前年度的开始
|
|
|
+ * 将任意日期调整为当年的1月1日
|
|
|
+ * 用于年度计划的周期计算,确保从年度起始点开始
|
|
|
+ * 无论输入日期是哪一天,都返回该年的第一天
|
|
|
+ */
|
|
|
+ private LocalDate adjustToYearStart(LocalDate date) {
|
|
|
+ return LocalDate.of(date.getYear(), 1, 1);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 根据周期类型计算下一个周期日期
|
|
|
+ * 支持四种周期类型:月度加1个月,季度加3个月,半年度加6个月,年度加1年
|
|
|
+ */
|
|
|
+ private LocalDate calculateNextCycleDate(LocalDate currentDate, CycleParams params) {
|
|
|
+ switch (params.cycleType) {
|
|
|
+ case "月度":
|
|
|
+ return currentDate.plusMonths(1);
|
|
|
+ case "季度":
|
|
|
+ return currentDate.plusMonths(3);
|
|
|
+ case "半年度":
|
|
|
+ return currentDate.plusMonths(6);
|
|
|
+ case "年度":
|
|
|
+ return currentDate.plusYears(1);
|
|
|
+ default:
|
|
|
+ return currentDate.plusMonths(1);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 判断今天是否已经达到或超过推送日
|
|
|
+ * 专门用于计算下次推送时间的逻辑
|
|
|
+ * 检查今天的日期是否已经达到或超过计算出的推送日期
|
|
|
+ * 用于决定是否需要计算下一个周期
|
|
|
+ */
|
|
|
+ private boolean isTodayAfterOrEqualPushDate(LocalDate pushDate, LocalDate today) {
|
|
|
+ // 检查今天是否已经达到或超过推送日期
|
|
|
+ return !today.isBefore(pushDate);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 为计划插入填写记录到数据库
|
|
|
+ * 首先检查相同计划相同日期是否已有记录,避免重复插入
|
|
|
+ * 生成去除横杠的UUID作为主键,确保唯一性
|
|
|
+ * 复制计划表的模板内容和属性到填写记录表
|
|
|
+ * 包含完整的错误处理和日志记录,确保数据一致性
|
|
|
+ */
|
|
|
+ private boolean insertRecordForPlan(QualityMetricsForHcqdDto plan, String cycleDateStr, QualityMetricsForHcqdDao dao) {
|
|
|
+ try {
|
|
|
+ // 检查是否已经存在
|
|
|
+ int existingCount = dao.countRecordByPlanAndDate(plan.getId(), cycleDateStr);
|
|
|
+ if (existingCount > 0) {
|
|
|
+ logger.warn("计划 {} 的 {} 记录已存在,跳过", plan.getJiHuaMingCheng(), cycleDateStr);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建填写记录
|
|
|
+ QualityMetricsForHcqdDto record = new QualityMetricsForHcqdDto();
|
|
|
+
|
|
|
+ // 生成ID(去除横杠的UUID)
|
|
|
+ String uuid = UUID.randomUUID().toString().replace("-", "");
|
|
|
+ record.setId(uuid);
|
|
|
+
|
|
|
+ // 复制字段
|
|
|
+ record.setTenantId(plan.getTenantId());
|
|
|
+ record.setIp(plan.getIp());
|
|
|
+ record.setCreateBy(plan.getCreateBy());
|
|
|
+ record.setCreateTime(new Date());
|
|
|
+ record.setUpdateBy(plan.getUpdateBy());
|
|
|
+ record.setUpdateTime(new Date());
|
|
|
+ record.setShiFouGuoShen(plan.getShiFouGuoShen());
|
|
|
+ record.setDiDian(plan.getDiDian());
|
|
|
+ record.setBianZhiRen(plan.getBianZhiRen());
|
|
|
+ record.setBianZhiBuMen(plan.getSuoShuBuMen());
|
|
|
+ record.setBianZhiShiJian(cycleDateStr);
|
|
|
+ record.setKuaiZhao(plan.getKuaiZhao());
|
|
|
+ record.setJiHuaBiaoId(plan.getId());
|
|
|
+ record.setBiaoGeNeiRong(plan.getBiaoGeNeiRong());
|
|
|
+ record.setJiHuaMingCheng(plan.getJiHuaMingCheng());
|
|
|
+ record.setZhouQiLeiXing(plan.getZhouQiLeiXing());
|
|
|
+ record.setZhouQiXiangQin(plan.getZhouQiXiangQin());
|
|
|
+
|
|
|
+ // 插入记录
|
|
|
+ int result = dao.insertRecord(record);
|
|
|
+ return result > 0;
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ logger.error("插入填写记录失败,计划: {}, 日期: {}, 错误: {}",
|
|
|
+ plan.getJiHuaMingCheng(), cycleDateStr, e.getMessage(), e);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 周期参数内部类,封装计划周期的核心参数
|
|
|
+ * 用于统一管理周期类型、月份偏移和日期偏移三个相关参数
|
|
|
+ * 提供类型安全的数据结构,避免参数传递时的错误
|
|
|
+ * 作为周期计算的基础数据单元,贯穿整个定时任务流程
|
|
|
+ */
|
|
|
+ private static class CycleParams {
|
|
|
+ String cycleType; // 周期类型:月度、季度、半年度、年度
|
|
|
+ int monthOffset; // 月份偏移:1表示第1个月,2表示第2个月,以此类推;0表示不使用月份偏移(月度计划专用)
|
|
|
+ int dayOffset; // 日期偏移:1表示第1天,15表示第15天
|
|
|
+
|
|
|
+ CycleParams(String cycleType, int monthOffset, int dayOffset) {
|
|
|
+ this.cycleType = cycleType;
|
|
|
+ this.monthOffset = monthOffset;
|
|
|
+ this.dayOffset = dayOffset;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|