From 41ff2bd6c6323efe73daba162fc2156b5230f6a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BA=84=E9=94=90=E6=B3=A2?= <10549902+zhuangruibo@user.noreply.gitee.com> Date: Sun, 31 May 2026 16:15:03 +0800 Subject: [PATCH] =?UTF-8?q?=E6=89=A7=E6=B3=95=E7=AB=AF-=E8=AE=A1=E5=88=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/IEfcodeCpPlanService.java | 4 + .../service/impl/EfcodeCpPlanServiceImpl.java | 218 ++++++++++++++++-- .../impl/EfcodeCpReserveServiceImpl.java | 58 +++++ .../enforcementcode/EfcodeCpPlanMapper.xml | 1 - .../enforcement/my/plan/cp_plan_mine.vue | 60 ++++- .../rest/test/cp/AppEnfCpAutoReserveTest.java | 4 +- 6 files changed, 323 insertions(+), 22 deletions(-) diff --git a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/IEfcodeCpPlanService.java b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/IEfcodeCpPlanService.java index f7292734..a96a3bde 100644 --- a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/IEfcodeCpPlanService.java +++ b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/IEfcodeCpPlanService.java @@ -9,6 +9,7 @@ import java.util.Collection; import java.util.List; import java.util.Map; import com.zdxt.enforcementcode.domain.EfcodeCpReserve; +import org.springframework.transaction.annotation.Transactional; /** * 行政执法检查计划-计划Service接口 @@ -89,6 +90,9 @@ public interface IEfcodeCpPlanService { */ Map> autoMatchReserve(boolean forceMatch, boolean updatePlan, boolean insertReserve); + @Transactional(rollbackFor = Exception.class) + Map> autoMatchReserveV3(boolean forceMatch, boolean updatePlan, boolean insertReserve); + /** * 计划自动匹配预约V2(定时任务调用) *

扫描待预约状态的计划,按自然周分组匹配,超出最大联合数量时拆分多组

diff --git a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/EfcodeCpPlanServiceImpl.java b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/EfcodeCpPlanServiceImpl.java index dd6c8ff4..4d2dbe5a 100644 --- a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/EfcodeCpPlanServiceImpl.java +++ b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/EfcodeCpPlanServiceImpl.java @@ -293,7 +293,15 @@ public class EfcodeCpPlanServiceImpl implements IEfcodeCpPlanService { lqw.eq(bo.getIsPublic() != null, EfcodeCpPlan::getIsPublic, bo.getIsPublic()); lqw.eq(bo.getIsUnion() != null, EfcodeCpPlan::getIsUnion, bo.getIsUnion()); lqw.eq(StringUtils.isNotBlank(bo.getUnionBureauDeptNames()), EfcodeCpPlan::getUnionBureauDeptNames, bo.getUnionBureauDeptNames()); - lqw.eq(bo.getReserveStatus() != null, EfcodeCpPlan::getReserveStatus, bo.getReserveStatus()); + // 支持多值状态过滤(逗号分隔) + String reserveStatusList = params != null ? (String) params.get("reserveStatusList") : null; + if (StringUtils.isNotBlank(reserveStatusList)) { + List statusList = java.util.Arrays.stream(reserveStatusList.split(",")) + .map(String::trim).map(Long::valueOf).collect(Collectors.toList()); + lqw.in(EfcodeCpPlan::getReserveStatus, statusList); + } else { + lqw.eq(bo.getReserveStatus() != null, EfcodeCpPlan::getReserveStatus, bo.getReserveStatus()); + } lqw.eq(bo.getSystemReserved() != null, EfcodeCpPlan::getSystemReserved, bo.getSystemReserved()); lqw.eq(bo.getManualReserved() != null, EfcodeCpPlan::getManualReserved, bo.getManualReserved()); lqw.eq(StringUtils.isNotBlank(bo.getBureauDeptId()), EfcodeCpPlan::getBureauDeptId, bo.getBureauDeptId()); @@ -732,6 +740,182 @@ public class EfcodeCpPlanServiceImpl implements IEfcodeCpPlanService { return resultMap; } + @Transactional(rollbackFor = Exception.class) + @Override + public Map> autoMatchReserveV3(boolean forceMatch, boolean updatePlan, boolean insertReserve) { + log.info("[CpPlan-V2] 开始执行计划自动匹配预约, forceMatch={}, updatePlan={}, insertReserve={}", forceMatch, updatePlan, insertReserve); + Map> resultMap = new HashMap<>(); + + // 1. 日期校验:如果不是强制匹配,则判断当前天数是否等于 CP_SETTING_AUTO_MATCH_START + if (!forceMatch) { + int autoMatchStart = cpSettingService.getAutoMatchStart(); + int today = LocalDate.now().getDayOfMonth(); + if (today != autoMatchStart) { + log.info("[CpPlan-V2] 当前日期{}号,不等于设定的自动匹配日期{}号,跳过执行", today, autoMatchStart); + return resultMap; + } + } + + // 2. 获取最大联合单位数量 + int maxUnionUnit = cpSettingService.getMaxUnionUnit(); + + // 2.1 获取系统匹配预约周数范围 + int systemMatchWeeks = cpSettingService.getSystemMatchWeeks(); + log.info("[CpPlan-V2] 系统匹配周数范围={}", systemMatchWeeks); + + // 3. 查询待匹配的计划:plan_date > 当前时间 且 system_reserved=0, manual_reserved=0, reserve_status=1, is_public=1, is_union=1 + Date now = new Date(); + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.gt(EfcodeCpPlan::getPlanDate, now); + lqw.eq(EfcodeCpPlan::getSystemReserved, 0L); + lqw.eq(EfcodeCpPlan::getManualReserved, 0L); + lqw.eq(EfcodeCpPlan::getReserveStatus, 1L); + lqw.eq(EfcodeCpPlan::getIsPublic, 1L); + lqw.eq(EfcodeCpPlan::getIsUnion, 1L); + lqw.eq(EfcodeCpPlan::getIsDelete, 0L); + lqw.isNotNull(EfcodeCpPlan::getCreateBy); + lqw.orderByAsc(EfcodeCpPlan::getPlanDate); + List pendingPlans = baseMapper.selectList(lqw); + + if (pendingPlans.isEmpty()) { + log.info("[CpPlan-V2] 没有找到待匹配的计划记录,执行结束"); + return resultMap; + } + + log.info("[CpPlan-V2] 共查询到{}条待匹配计划", pendingPlans.size()); + + // 存储将要保存的联合记录列表 + List unionReserveList = new ArrayList<>(); + + // 记录已被处理(system_reserved标记为1)的计划ID,避免重复处理 + Set processedPlanIds = new HashSet<>(); + int matchedCount = 0; + + for (EfcodeCpPlan plan : pendingPlans) { + // 已被处理过的计划跳过 + if (processedPlanIds.contains(plan.getId())) { + continue; + } + log.info("[CpPlan-V2] 开始处理计划记录,计划ID={}", plan.getId()); + String enterpriseNumber = plan.getEnterpriseNumber(); + if (StringUtils.isBlank(enterpriseNumber)) { + continue; + } + + // 5. 计算计划所在的时间范围:以计划所在自然周的周一为起始,向后扩展 systemMatchWeeks 周 + LocalDate planLocalDate = plan.getPlanDate().toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); + LocalDate weekStartDate = planLocalDate.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)); + LocalDate weekEndDate = weekStartDate.plusWeeks(systemMatchWeeks).minusDays(1); // 扩展N周后的周日 + log.info("[CpPlan-V2] 计划ID={}的时间范围是:{} ~ {}", plan.getId(), weekStartDate, weekEndDate); + + // 6. 从待匹配计划中筛选:同企业 + 时间范围内 + 不是当前计划创建者发布的 + 未被处理过 + List matchedPlans = pendingPlans.stream() + .filter(p -> { + // 排除自身 + if (p.getId().equals(plan.getId())) { + return false; + } + // 排除已处理过的 + if (processedPlanIds.contains(p.getId())) { + return false; + } + // 同企业 + if (!enterpriseNumber.equals(p.getEnterpriseNumber())) { + return false; + } + // 不是当前计划创建者发布的(不是我的计划) + if (plan.getCreateBy() != null && plan.getCreateBy().equals(p.getCreateBy())) { + return false; + } + // 在时间范围内 + LocalDate pDate = p.getPlanDate().toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); + return !pDate.isBefore(weekStartDate) && !pDate.isAfter(weekEndDate); + }) + .collect(Collectors.toList()); + + log.info("[CpPlan-V2] 计划ID={}, 在时间范围内找到{}条非本人发布的同企业计划", plan.getId(), matchedPlans.size()); + + // 7. 从matchedPlans中取出不超过 maxUnionUnit-1 条记录(当前计划本身占1个名额) + int limit = maxUnionUnit - 1; + List selectedPlans = matchedPlans.stream() + .limit(limit) + .collect(Collectors.toList()); + + log.info("[CpPlan-V2] 计划ID={}, 最终选取{}条计划参与联合(最大允许{}条)", plan.getId(), selectedPlans.size(), limit); + + // 8. 如果没有可联合的计划,跳过 + if (selectedPlans.isEmpty()) { + log.debug("[CpPlan-V2] 计划ID={} 没有找到可联合的计划,跳过", plan.getId()); + continue; + } + + // 9. 将当前plan和selectedPlans作为一组,生成预约记录 + String planGroupId = "Temp" + "_" + UUID.randomUUID().toString().replace("-", ""); + Date reserveTime = new Date(); + List groupReserves = new ArrayList<>(); + + // 组内所有计划(当前计划 + 选中的计划) + List groupPlans = new ArrayList<>(); + groupPlans.add(plan); + groupPlans.addAll(selectedPlans); + + // 拼接参与单位名称 + String unitNames = groupPlans.stream() + .map(EfcodeCpPlan::getBureauDeptName) + .filter(StringUtils::isNotBlank) + .collect(Collectors.joining(",")); + String opMessage = "由于计划执法时间安排较近,系统推荐进行联合,参与单位为:" + unitNames + ".请确认."; + + for (EfcodeCpPlan matchedPlan : groupPlans) { + EfcodeCpReserve reserve = new EfcodeCpReserve(); + reserve.setPlanId(matchedPlan.getId()); + reserve.setPlanGroupId(planGroupId); + reserve.setReserveTime(reserveTime); + reserve.setOpStatus(3L); // 待确认 + reserve.setOpOrder(0L); // 系统匹配默认为0 + reserve.setOpMessage(opMessage); + reserve.setOpSetDate(matchedPlan.getPlanDate()); + reserve.setOpSetTime(matchedPlan.getPlanTime()); + reserve.setResFrom(1L); // 系统匹配 + reserve.setSenderId("from_system"); + reserve.setReceiverId(String.valueOf(matchedPlan.getCreateBy())); + + if (insertReserve) { + efcodeCpReserveMapper.insert(reserve); + } + groupReserves.add(reserve); + } + + // 加入总的待处理列表 + unionReserveList.addAll(groupReserves); + resultMap.put(planGroupId, groupReserves); + + // 10. 将组内所有计划标记为 system_reserved=1,并更新 reserve_status=2(预约中) + if (updatePlan) { + for (EfcodeCpPlan gp : groupPlans) { + EfcodeCpPlan planUpdate = new EfcodeCpPlan(); + planUpdate.setId(gp.getId()); + planUpdate.setSystemReserved(1L); + planUpdate.setReserveStatus(2L); + baseMapper.updateById(planUpdate); + } + } + + // 将组内所有计划ID加入已处理集合,防止重复匹配 + for (EfcodeCpPlan gp : groupPlans) { + processedPlanIds.add(gp.getId()); + } + + matchedCount++; + log.info("[CpPlan-V2] 计划ID={} 匹配成功,联合{}个单位,planGroupId={}", + plan.getId(), groupPlans.size(), planGroupId); + } + + log.info("[CpPlan-V2] 计划自动匹配预约执行完毕,共匹配{}组联合预约,共生成{}条预约记录", matchedCount, unionReserveList.size()); + + return resultMap; + } + /** * 计划自动匹配预约V2(定时任务调用) *

扫描待预约状态的计划,按自然周分组匹配,超出最大联合数量时拆分多组

@@ -822,7 +1006,7 @@ public class EfcodeCpPlanServiceImpl implements IEfcodeCpPlanService { weekLqw.orderByAsc(EfcodeCpPlan::getPlanDate); List samePlans = baseMapper.selectList(weekLqw); - // 7. 按不同执法单位去重,取每个 bureau_dept_id 的第一条计划作为代表 + // 7. 按 bureau_dept_id 去重,每个单位取一条代表计划 List distinctUnitPlans = new ArrayList<>(); Set seenBureauDeptIds = new HashSet<>(); for (EfcodeCpPlan sp : samePlans) { @@ -833,18 +1017,17 @@ public class EfcodeCpPlanServiceImpl implements IEfcodeCpPlanService { } } - // 8. 如果只有一个单位的计划,说明没有可联合的,跳过 + // 8. 只有1个单位的计划,无法联合,跳过 if (distinctUnitPlans.size() <= 1) { log.debug("[CpPlan-V2] 企业[{}] 周[{}~{}]内只有1个单位的计划,跳过", enterpriseNumber, weekStartDate, weekEndDate); continue; } - // 9. 按最大联合单位数量分组:如果不同单位数 > maxUnionUnit,则拆分成多组 + // 9. 按最大联合单位数量分组 List> groups = new ArrayList<>(); for (int i = 0; i < distinctUnitPlans.size(); i += maxUnionUnit) { int end = Math.min(i + maxUnionUnit, distinctUnitPlans.size()); List group = distinctUnitPlans.subList(i, end); - // 每组至少需要2个不同单位才有联合意义 if (group.size() >= 2) { groups.add(new ArrayList<>(group)); } @@ -855,12 +1038,12 @@ public class EfcodeCpPlanServiceImpl implements IEfcodeCpPlanService { } // 10. 为每组生成预约记录 + Set matchedPlanIds = new HashSet<>(); for (List group : groups) { String planGroupId = "Temp" + "_" + UUID.randomUUID().toString().replace("-", ""); Date reserveTime = new Date(); List groupReserves = new ArrayList<>(); - // 拼接参与单位名称,用于 opMessage String unitNames = group.stream() .map(EfcodeCpPlan::getBureauDeptName) .filter(StringUtils::isNotBlank) @@ -887,7 +1070,7 @@ public class EfcodeCpPlanServiceImpl implements IEfcodeCpPlanService { groupReserves.add(reserve); } - // 更新被选中参与联合的计划: reserve_status=2(预约中) + // 更新被选中参与联合的计划: reserve_status=2(预约中), system_reserved=1 if (updatePlan) { for (EfcodeCpPlan dp : group) { EfcodeCpPlan planUpdate = new EfcodeCpPlan(); @@ -895,27 +1078,28 @@ public class EfcodeCpPlanServiceImpl implements IEfcodeCpPlanService { planUpdate.setReserveStatus(2L); planUpdate.setSystemReserved(1L); baseMapper.updateById(planUpdate); + matchedPlanIds.add(dp.getId()); } } resultMap.put(planGroupId, groupReserves); matchedCount++; - log.info("[CpPlan-V2] 企业[{}] 周[{}~{}] 分组匹配成功,联合{}个单位,planGroupId={}", + log.info("[CpPlan-V2] 企业[{}] 周[{}~{}] 匹配成功,联合{}个单位,planGroupId={}", enterpriseNumber, weekStartDate, weekEndDate, group.size(), planGroupId); } - // 11. 将该企业该周所有待匹配计划的 system_reserved 标记为1,防止重复匹配 - if (updatePlan) { + // 11. 将已成功匹配的单位在该企业该周内的所有计划标记为 system_reserved=1,防止重复匹配 + if (updatePlan && !groups.isEmpty()) { for (EfcodeCpPlan sp : samePlans) { - // 已在上面group循环中更新过的(reserve_status=2)跳过,避免覆盖 - if (seenBureauDeptIds.contains(sp.getBureauDeptId())) { - // 该计划对应的单位已经参与联合,在group循环中已处理 + if (matchedPlanIds.contains(sp.getId())) { continue; } - EfcodeCpPlan planUpdate = new EfcodeCpPlan(); - planUpdate.setId(sp.getId()); - planUpdate.setSystemReserved(1L); - baseMapper.updateById(planUpdate); + if (seenBureauDeptIds.contains(sp.getBureauDeptId())) { + EfcodeCpPlan planUpdate = new EfcodeCpPlan(); + planUpdate.setId(sp.getId()); + planUpdate.setSystemReserved(1L); + baseMapper.updateById(planUpdate); + } } } } diff --git a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/EfcodeCpReserveServiceImpl.java b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/EfcodeCpReserveServiceImpl.java index ae0012ef..fcaef5c3 100644 --- a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/EfcodeCpReserveServiceImpl.java +++ b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/EfcodeCpReserveServiceImpl.java @@ -767,6 +767,9 @@ public class EfcodeCpReserveServiceImpl implements IEfcodeCpReserveService { return 0; } + // 收集受影响的分组ID(系统匹配用) + Set affectedGroupIds = new HashSet<>(); + Date now = new Date(); for (EfcodeCpReserve reserve : overdueList) { // 更新预约记录状态为超期取消(4) @@ -783,9 +786,64 @@ public class EfcodeCpReserveServiceImpl implements IEfcodeCpReserveService { planUpdate.setReserveStatus(4L); cpPlanMapper.updateById(planUpdate); } + + // 系统匹配记录收集分组ID + if (Long.valueOf(1L).equals(resFrom) && StringUtils.isNotBlank(reserve.getPlanGroupId())) { + affectedGroupIds.add(reserve.getPlanGroupId()); + } } log.info("[超期取消] resFrom={} ignoreTime={} 共取消{}条记录", resFrom, ignoreTime, overdueList.size()); + + // 系统匹配:取消后检查受影响的分组,若有已同意的记录则触发任务生成 + if (Long.valueOf(1L).equals(resFrom) && !affectedGroupIds.isEmpty()) { + for (String groupId : affectedGroupIds) { + afterCancelGroupCheck(groupId); + } + } + return overdueList.size(); } + + /** + * 超期取消后分组检查:若同组中有已同意的记录,且全部处理完成,则生成任务 + */ + private void afterCancelGroupCheck(String planGroupId) { + // 查询同组全部记录 + List allGroupReserves = baseMapper.selectList( + Wrappers.lambdaQuery() + .eq(EfcodeCpReserve::getPlanGroupId, planGroupId) + ); + // 检查是否全部处理完成(无待确认记录) + boolean allProcessed = allGroupReserves.stream() + .noneMatch(r -> Long.valueOf(3L).equals(r.getOpStatus())); + if (!allProcessed) { + return; + } + // 筛选同意的记录 + List approvedReserves = allGroupReserves.stream() + .filter(r -> Long.valueOf(1L).equals(r.getOpStatus())) + .collect(java.util.stream.Collectors.toList()); + if (approvedReserves.isEmpty()) { + log.info("[超期取消后置] planGroupId={} 无已同意记录,不生成任务", planGroupId); + return; + } + // 至少一家同意:生成1条任务,只关联同意的计划 + String sharedTaskId = null; + for (EfcodeCpReserve r : approvedReserves) { + EfcodeCpPlan plan = cpPlanMapper.selectById(r.getPlanId()); + if (plan == null || StringUtils.isNotBlank(plan.getTaskId())) continue; + if (sharedTaskId == null) { + sharedTaskId = cpTaskService.forceGenerateTask(plan); + } else { + EfcodeCpPlan planUpdate = new EfcodeCpPlan(); + planUpdate.setId(plan.getId()); + planUpdate.setTaskId(sharedTaskId); + planUpdate.setReserveStatus(3L); + cpPlanMapper.updateById(planUpdate); + } + } + log.info("[超期取消后置] planGroupId={} 有{}家已同意,生成任务taskId={}", + planGroupId, approvedReserves.size(), sharedTaskId); + } } diff --git a/zdxt-modules/zdxt-enforcement-code/src/main/resources/mapper/enforcementcode/EfcodeCpPlanMapper.xml b/zdxt-modules/zdxt-enforcement-code/src/main/resources/mapper/enforcementcode/EfcodeCpPlanMapper.xml index 4b178290..8769ee87 100644 --- a/zdxt-modules/zdxt-enforcement-code/src/main/resources/mapper/enforcementcode/EfcodeCpPlanMapper.xml +++ b/zdxt-modules/zdxt-enforcement-code/src/main/resources/mapper/enforcementcode/EfcodeCpPlanMapper.xml @@ -8,7 +8,6 @@