diff --git a/script/sql/zfjd-refactoring-v1/013-计划任务表设计.sql b/script/sql/zfjd-refactoring-v1/013-计划任务表设计.sql
index a1b299a8..ead67c23 100644
--- a/script/sql/zfjd-refactoring-v1/013-计划任务表设计.sql
+++ b/script/sql/zfjd-refactoring-v1/013-计划任务表设计.sql
@@ -25,6 +25,8 @@ CREATE TABLE `efcode_cp_plan`
`union_bureau_dept_names` varchar(500) DEFAULT NULL COMMENT '联合执法单位(多个逗号分隔)',
`reserve_status` tinyint NOT NULL DEFAULT 0 COMMENT '预约状态:0=草稿,1=待预约(未发起联合),2=预约中,3=预约完成,4=重新预约中,5=待预约(还有位置可以加入)',
+ `task_id` varchar(50) NULL DEFAULT '' COMMENT '任务id:空串表示还没生成任务',
+
`system_reserved` tinyint NOT NULL DEFAULT 0 COMMENT '是否已经经过系统推荐预约:1=是,0=否',
`manual_reserved` tinyint NOT NULL DEFAULT 0 COMMENT '是否已经经过手动预约:1=是,0=否',
`bureau_dept_id` varchar(50) NOT NULL COMMENT '执法单位ID',
@@ -181,14 +183,14 @@ CREATE TABLE `efcode_cp_setting`
CREATE TABLE `efcode_cp_log`
(
- `id` varchar(50) NOT NULL COMMENT '主键ID' PRIMARY KEY,
+ `id` varchar(50) NOT NULL COMMENT '主键ID' PRIMARY KEY,
`record_id` varchar(50) NULL COMMENT '关联记录',
`table_name` varchar(50) NULL COMMENT '关联表',
`log_type` varchar(50) NULL COMMENT '日志类型',
`op_slog` varchar(2000) NULL COMMENT '短日志',
`op_llog` text NULL COMMENT '长日志',
- `op_user_id` varchar(50) NOT NULL COMMENT '操作人ID',
- `op_user_name` varchar(50) NOT NULL COMMENT '操作人姓名',
+ `op_user_id` varchar(50) NOT NULL COMMENT '操作人ID',
+ `op_user_name` varchar(50) NOT NULL COMMENT '操作人姓名',
`remark` varchar(500) NULL COMMENT '备注',
`create_by` varchar(64) DEFAULT '' COMMENT '创建者',
`create_dept` varchar(64) DEFAULT NULL COMMENT '创建人部门',
diff --git a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/EfcodeCpPlan.java b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/EfcodeCpPlan.java
index cbeab286..15f319a4 100644
--- a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/EfcodeCpPlan.java
+++ b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/EfcodeCpPlan.java
@@ -114,6 +114,11 @@ public class EfcodeCpPlan extends BaseEntity {
*/
private Long reserveStatus;
+ /**
+ * 任务id:空串表示还没生成任务
+ */
+ private String taskId;
+
/**
* 是否已经经过系统推荐预约:1=是,0=否
*/
diff --git a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/bo/EfcodeCpPlanBo.java b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/bo/EfcodeCpPlanBo.java
index 6b0774ac..f6aa8d53 100644
--- a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/bo/EfcodeCpPlanBo.java
+++ b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/bo/EfcodeCpPlanBo.java
@@ -128,6 +128,11 @@ public class EfcodeCpPlanBo extends BaseEntity {
@NotNull(message = "预约状态:0=草稿,1=待预约,2=预约中,3=预约完成不能为空", groups = { AddGroup.class, EditGroup.class })
private Long reserveStatus;
+ /**
+ * 任务id:空串表示还没生成任务
+ */
+ private String taskId;
+
/**
* 是否已经经过系统推荐预约:1=是,0=否
*/
diff --git a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/vo/EfcodeCpPlanVo.java b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/vo/EfcodeCpPlanVo.java
index a3c916ca..3d0e8bb3 100644
--- a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/vo/EfcodeCpPlanVo.java
+++ b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/vo/EfcodeCpPlanVo.java
@@ -145,6 +145,12 @@ public class EfcodeCpPlanVo implements Serializable {
@ExcelProperty(value = "预约状态:0=草稿,1=待预约,2=预约中,3=预约完成")
private Long reserveStatus;
+ /**
+ * 任务id:空串表示还没生成任务
+ */
+ @ExcelProperty(value = "任务ID")
+ private String taskId;
+
/**
* 是否已经经过系统推荐预约:1=是,0=否
*/
diff --git a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/quartz/job/CpPlanAutoMatchJob.java b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/quartz/job/CpPlanAutoMatchJob.java
index c2bc3f99..f5c7d217 100644
--- a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/quartz/job/CpPlanAutoMatchJob.java
+++ b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/quartz/job/CpPlanAutoMatchJob.java
@@ -20,9 +20,18 @@ public class CpPlanAutoMatchJob {
/**
* 任务执行入口(public,供 JobInvokeUtil 通过反射调用)
+ *
默认非强制模式:受 CP_SETTING_AUTO_MATCH_START 日期控制
*/
public void execute() {
log.info("[CpPlanAutoMatchJob] 定时任务触发:计划自动匹配预约");
- cpPlanService.autoMatchReserve();
+ cpPlanService.autoMatchReserve(false, true, true);
+ }
+
+ /**
+ * 强制执行入口(忽略日期校验,立即执行匹配)
+ */
+ public void executeForce() {
+ log.info("[CpPlanAutoMatchJob] 强制触发:计划自动匹配预约");
+ cpPlanService.autoMatchReserve(true, true, true);
}
}
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 7aae9304..a34c3680 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
@@ -7,6 +7,8 @@ import org.dromara.common.mybatis.core.page.PageQuery;
import java.util.Collection;
import java.util.List;
+import java.util.Map;
+import com.zdxt.enforcementcode.domain.EfcodeCpReserve;
/**
* 行政执法检查计划-计划Service接口
@@ -79,6 +81,11 @@ public interface IEfcodeCpPlanService {
/**
* 计划自动匹配预约(定时任务调用)
* 扫描待预约状态的计划,按规则自动匹配并生成预约记录
+ *
+ * @param forceMatch 是否强制匹配(忽略 CP_SETTING_AUTO_MATCH_START 日期校验)
+ * @param updatePlan 是否更新计划记录(system_reserved、reserve_status)
+ * @param insertReserve 是否将预约记录插入数据库;为false时仅计算匹配结果不落库
+ * @return 按 planGroupId 分组的预约记录列表(key=planGroupId, value=该组预约记录)
*/
- void autoMatchReserve();
+ Map> autoMatchReserve(boolean forceMatch, boolean updatePlan, boolean insertReserve);
}
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 054a418d..d2458359 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
@@ -19,14 +19,24 @@ import com.zdxt.enforcementcode.mapper.EfcodeCpPlanMapper;
import com.zdxt.enforcementcode.mapper.EfcodeCpPlanUsersMapper;
import com.zdxt.enforcementcode.service.IEfcodeCpPlanService;
+import java.time.LocalDate;
+import java.time.ZoneId;
import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.UUID;
import java.util.Collection;
import java.util.stream.Collectors;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.satoken.utils.LoginHelper;
+import com.zdxt.enforcementcode.domain.EfcodeCpReserve;
+import com.zdxt.enforcementcode.domain.EfcodeCpSetting;
+import com.zdxt.enforcementcode.mapper.EfcodeCpReserveMapper;
+import com.zdxt.enforcementcode.mapper.EfcodeCpSettingMapper;
/**
* 行政执法检查计划-计划Service业务层处理
@@ -41,6 +51,8 @@ public class EfcodeCpPlanServiceImpl implements IEfcodeCpPlanService {
private final EfcodeCpPlanMapper baseMapper;
private final EfcodeCpPlanUsersMapper efcodeCpPlanUsersMapper;
+ private final EfcodeCpReserveMapper efcodeCpReserveMapper;
+ private final EfcodeCpSettingMapper efcodeCpSettingMapper;
/**
* 查询行政执法检查计划-计划
@@ -348,11 +360,161 @@ public class EfcodeCpPlanServiceImpl implements IEfcodeCpPlanService {
/**
* 计划自动匹配预约(定时任务调用)
* 扫描待预约状态的计划,按规则自动匹配并生成预约记录
+ *
+ * @param forceMatch 是否强制匹配(忽略 CP_SETTING_AUTO_MATCH_START 日期校验)
+ * @param updatePlan 是否更新计划记录(system_reserved、reserve_status)
+ * @param insertReserve 是否将预约记录插入数据库;为false时仅计算匹配结果不落库
+ * @return 按 planGroupId 分组的预约记录列表
*/
+ @Transactional(rollbackFor = Exception.class)
@Override
- public void autoMatchReserve() {
- log.info("[CpPlan] 开始执行计划自动匹配预约...");
- // TODO 后续实现:扫描待预约计划 → 按企业+日期+时段匹配 → 自动生成预约记录
- log.info("[CpPlan] 计划自动匹配预约执行完毕");
+ public Map> autoMatchReserve(boolean forceMatch, boolean updatePlan, boolean insertReserve) {
+ log.info("[CpPlan] 开始执行计划自动匹配预约, forceMatch={}, updatePlan={}, insertReserve={}", forceMatch, updatePlan, insertReserve);
+ Map> resultMap = new HashMap<>();
+
+ // 1. 日期校验:如果不是强制匹配,则判断当前天数是否等于 CP_SETTING_AUTO_MATCH_START
+ if (!forceMatch) {
+ String autoMatchStartDay = getSettingValue("CP_SETTING_AUTO_MATCH_START");
+ int today = LocalDate.now().getDayOfMonth();
+ if (StringUtils.isNotBlank(autoMatchStartDay)) {
+ int settingDay = Integer.parseInt(autoMatchStartDay.trim());
+ if (today != settingDay) {
+ log.info("[CpPlan] 当前日期{}号,不等于设定的自动匹配日期{}号,跳过执行", today, settingDay);
+ return resultMap;
+ }
+ }
+ }
+
+ // 2. 获取最大联合单位数量
+ int maxUnionUnit = 3; // 默认值
+ String maxUnionStr = getSettingValue("CP_MAX_UNION_UNIT");
+ if (StringUtils.isNotBlank(maxUnionStr)) {
+ maxUnionUnit = Integer.parseInt(maxUnionStr.trim());
+ }
+
+ // 3. 查询待匹配的计划:plan_date > 当前时间 且 system_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::getReserveStatus, 1L);
+ lqw.eq(EfcodeCpPlan::getIsPublic, 1L);
+ lqw.eq(EfcodeCpPlan::getIsUnion, 1L);
+ lqw.eq(EfcodeCpPlan::getIsDelete, 0L);
+ lqw.orderByAsc(EfcodeCpPlan::getPlanDate);
+ List pendingPlans = baseMapper.selectList(lqw);
+
+ if (pendingPlans.isEmpty()) {
+ log.info("[CpPlan] 没有找到待匹配的计划记录,执行结束");
+ return resultMap;
+ }
+ log.info("[CpPlan] 共查询到{}条待匹配计划", pendingPlans.size());
+
+ // 4. 已处理过的企业信用代码集合(同一轮中已匹配过的不再重复处理)
+ Set processedEnterprises = new HashSet<>();
+ int matchedCount = 0;
+
+ for (EfcodeCpPlan plan : pendingPlans) {
+ String enterpriseNumber = plan.getEnterpriseNumber();
+ if (StringUtils.isBlank(enterpriseNumber)) {
+ continue;
+ }
+ // 跳过本轮已处理的企业
+ if (processedEnterprises.contains(enterpriseNumber)) {
+ continue;
+ }
+
+ // 5. 查找一周内同企业、不同执法单位的其他待匹配计划
+ LocalDate planLocalDate = plan.getPlanDate().toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
+ Date weekStart = Date.from(planLocalDate.minusDays(3).atStartOfDay(ZoneId.systemDefault()).toInstant());
+ Date weekEnd = Date.from(planLocalDate.plusDays(4).atStartOfDay(ZoneId.systemDefault()).toInstant());
+
+ LambdaQueryWrapper weekLqw = Wrappers.lambdaQuery();
+ weekLqw.ge(EfcodeCpPlan::getPlanDate, weekStart);
+ weekLqw.le(EfcodeCpPlan::getPlanDate, weekEnd);
+ weekLqw.eq(EfcodeCpPlan::getEnterpriseNumber, enterpriseNumber);
+ weekLqw.eq(EfcodeCpPlan::getSystemReserved, 0L);
+ weekLqw.eq(EfcodeCpPlan::getReserveStatus, 1L);
+ weekLqw.eq(EfcodeCpPlan::getIsPublic, 1L);
+ weekLqw.eq(EfcodeCpPlan::getIsUnion, 1L);
+ weekLqw.eq(EfcodeCpPlan::getIsDelete, 0L);
+ weekLqw.orderByAsc(EfcodeCpPlan::getPlanDate);
+ List samePlans = baseMapper.selectList(weekLqw);
+
+ // 6. 按不同执法单位去重,取不同 bureau_dept_id 的计划
+ List distinctUnitPlans = new ArrayList<>();
+ Set seenBureauDeptIds = new HashSet<>();
+ for (EfcodeCpPlan sp : samePlans) {
+ if (!seenBureauDeptIds.contains(sp.getBureauDeptId())) {
+ seenBureauDeptIds.add(sp.getBureauDeptId());
+ distinctUnitPlans.add(sp);
+ }
+ // 限制最大联合数量
+ if (distinctUnitPlans.size() >= maxUnionUnit) {
+ break;
+ }
+ }
+
+ // 7. 如果只有一个单位的计划,说明没有可联合的,跳过
+ if (distinctUnitPlans.size() <= 1) {
+ log.debug("[CpPlan] 企业[{}]一周内只有1个单位的计划,跳过", enterpriseNumber);
+ continue;
+ }
+
+ // 8. 生成预约记录 - 使用同一个 plan_group_id
+ String planGroupId = "Temp"+"_"+UUID.randomUUID().toString().replace("-", "");
+ Date reserveTime = new Date();
+ List groupReserves = new ArrayList<>();
+
+ for (int i = 0; i < distinctUnitPlans.size(); i++) {
+ EfcodeCpPlan matchedPlan = distinctUnitPlans.get(i);
+ EfcodeCpReserve reserve = new EfcodeCpReserve();
+ reserve.setPlanId(matchedPlan.getId());
+ reserve.setPlanGroupId(planGroupId);
+ reserve.setReserveTime(reserveTime);
+ reserve.setOpStatus(3L); // 待确认
+ reserve.setOpOrder((long) (i + 1));
+ reserve.setOpMessage("");
+ reserve.setOpSetDate(matchedPlan.getPlanDate());
+ reserve.setOpSetTime(matchedPlan.getPlanTime());
+ reserve.setResFrom(1L); // 系统匹配
+
+ if (insertReserve) {
+ efcodeCpReserveMapper.insert(reserve);
+ }
+ groupReserves.add(reserve);
+
+ // 9. 更新计划: system_reserved=1, reserve_status=2
+ if (updatePlan) {
+ EfcodeCpPlan planUpdate = new EfcodeCpPlan();
+ planUpdate.setId(matchedPlan.getId());
+ planUpdate.setSystemReserved(1L);
+ planUpdate.setReserveStatus(2L);
+ baseMapper.updateById(planUpdate);
+ }
+ }
+
+ resultMap.put(planGroupId, groupReserves);
+
+ // 标记该企业已处理
+ processedEnterprises.add(enterpriseNumber);
+ matchedCount++;
+ log.info("[CpPlan] 企业[{}]匹配成功,联合{}个单位,planGroupId={}",
+ enterpriseNumber, distinctUnitPlans.size(), planGroupId);
+ }
+
+ log.info("[CpPlan] 计划自动匹配预约执行完毕,共匹配{}组联合预约", matchedCount);
+ return resultMap;
+ }
+
+ /**
+ * 获取 efcode_cp_setting 配置值
+ *
+ * @param settingId 设置ID
+ * @return 设置值
+ */
+ private String getSettingValue(String settingId) {
+ EfcodeCpSetting setting = efcodeCpSettingMapper.selectById(settingId);
+ return setting != null ? setting.getSettingValue() : null;
}
}
diff --git a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/EfcodeCpTaskServiceImpl.java b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/EfcodeCpTaskServiceImpl.java
index fd620697..fd5c5d91 100644
--- a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/EfcodeCpTaskServiceImpl.java
+++ b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/EfcodeCpTaskServiceImpl.java
@@ -116,6 +116,18 @@ public class EfcodeCpTaskServiceImpl implements IEfcodeCpTaskService {
//TODO 做一些数据校验,如唯一约束
}
+ /**
+ * 验证是否符合生成任务的条件
+ * 校验预约分组内所有记录是否满足生成执法任务的前置条件
+ *
+ * @param planGroupId 计划分组ID
+ * @return true=符合生成条件, false=不符合
+ */
+ public boolean checkCanGenerateTask(String planGroupId) {
+ // TODO 后续实现:校验分组内预约状态、时间窗口、人员配置等前置条件
+ return true;
+ }
+
/**
* 校验并批量删除行政执法检查计划-任务信息
*
diff --git a/zdxt-web-server/zdxt-admin-web-server/src/test/java/com/zdxt/rest/test/cp/AutoMatchReserveTest.java b/zdxt-web-server/zdxt-admin-web-server/src/test/java/com/zdxt/rest/test/cp/AutoMatchReserveTest.java
new file mode 100644
index 00000000..bab27b94
--- /dev/null
+++ b/zdxt-web-server/zdxt-admin-web-server/src/test/java/com/zdxt/rest/test/cp/AutoMatchReserveTest.java
@@ -0,0 +1,354 @@
+package com.zdxt.rest.test.cp;
+
+import cn.dev33.satoken.context.mock.SaTokenContextMockUtil;
+import cn.dev33.satoken.stp.StpUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.zdxt.enforcementcode.domain.EfcodeCpPlan;
+import com.zdxt.enforcementcode.domain.EfcodeCpReserve;
+import com.zdxt.enforcementcode.mapper.EfcodeCpPlanMapper;
+import com.zdxt.enforcementcode.mapper.EfcodeCpReserveMapper;
+import com.zdxt.enforcementcode.service.IEfcodeCpPlanService;
+import org.dromara.ZdxtAdminApplication;
+import org.junit.jupiter.api.*;
+import org.junit.jupiter.api.Tag;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 计划自动匹配预约 autoMatchReserve 方法测试
+ *
+ * 测试场景:
+ * 1. 强制模式下执行自动匹配(绕过日期校验)
+ * 2. 验证匹配结果:同企业不同单位的计划被联合
+ * 3. 验证计划状态更新:system_reserved=1, reserve_status=2
+ * 4. 验证预约记录生成:相同 plan_group_id 关联多条记录
+ *
+ */
+@SpringBootTest(classes = ZdxtAdminApplication.class)
+@AutoConfigureMockMvc
+@ActiveProfiles({"dev", "dev-cust-zrb"})
+@DisplayName("autoMatchReserve 自动匹配预约测试")
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+@Tag("dev")
+public class AutoMatchReserveTest {
+
+ @Autowired
+ private IEfcodeCpPlanService cpPlanService;
+
+ @Autowired
+ private EfcodeCpPlanMapper cpPlanMapper;
+
+ @Autowired
+ private EfcodeCpReserveMapper cpReserveMapper;
+
+ @BeforeEach
+ public void setUp() {
+ SaTokenContextMockUtil.setMockContext();
+ MockHttpServletRequest mockRequest = new MockHttpServletRequest();
+ mockRequest.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)");
+ RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(mockRequest));
+ }
+
+ @AfterEach
+ public void tearDown() {
+ try {
+ StpUtil.logout();
+ } catch (Exception ignored) {
+ }
+ RequestContextHolder.resetRequestAttributes();
+ SaTokenContextMockUtil.clearContext();
+ }
+
+ @DisplayName("0. 插入1000条适合匹配的计划数据")
+ @Test
+ @Order(0)
+ public void testInsert1000MatchablePlans() {
+ // 模拟登录(避免自动填充 createBy 报空)
+ StpUtil.login(1);
+
+ // 准备数据:200个企业 × 每企业5个不同执法单位 = 1000条计划
+ int enterpriseCount = 200;
+ int unitsPerEnterprise = 5;
+ Random random = new Random();
+
+ // 模拟执法单位ID池(确保不同单位)
+ String[] bureauDeptIds = {
+ "BUREAU_TEST_A", "BUREAU_TEST_B", "BUREAU_TEST_C",
+ "BUREAU_TEST_D", "BUREAU_TEST_E", "BUREAU_TEST_F",
+ "BUREAU_TEST_G", "BUREAU_TEST_H"
+ };
+ String[] bureauDeptNames = {
+ "测试执法局A", "测试执法局B", "测试执法局C",
+ "测试执法局D", "测试执法局E", "测试执法局F",
+ "测试执法局G", "测试执法局H"
+ };
+
+ // 基准日期:从明天开始
+ LocalDate baseDate = LocalDate.now().plusDays(1);
+ int insertedCount = 0;
+
+ for (int e = 0; e < enterpriseCount; e++) {
+ String enterpriseNumber = String.format("91440300TEST%06d", e);
+ String enterpriseName = "测试企业" + e + "号";
+ String enterpriseId = "ENT_TEST_" + e;
+
+ // 同一企业的计划日期在基准日期附3天内(确保一周内可匹配)
+ Date planDate = Date.from(
+ baseDate.plusDays(random.nextInt(3))
+ .atStartOfDay(ZoneId.systemDefault()).toInstant()
+ );
+
+ for (int u = 0; u < unitsPerEnterprise; u++) {
+ int bureauIdx = (e + u) % bureauDeptIds.length;
+
+ EfcodeCpPlan plan = new EfcodeCpPlan();
+ plan.setId(UUID.randomUUID().toString().replace("-", ""));
+ plan.setPlanType(random.nextBoolean() ? 1L : 2L);
+ plan.setPlanDate(planDate);
+ plan.setPlanTime((long) (random.nextInt(3) + 1)); // 1=上午,2=下午,3=其他
+ plan.setPlanActualDate(plan.getPlanDate());
+ plan.setPlanActualTime(plan.getPlanTime()); // 1=上午,2=下午,3=其他
+ plan.setEnforceContent("自动匹配测试-执法事项" + e + "-" + u);
+ plan.setEnterpriseName(enterpriseName);
+ plan.setEnterpriseNumber(enterpriseNumber);
+ plan.setEnterpriseId(enterpriseId);
+ plan.setEnforceUserNames("测试人员" + u);
+ plan.setEnforceUserIds(String.valueOf(1000 + u));
+ plan.setEnforceUserCodes("CODE" + u);
+ plan.setEnforceUserTels("1380000" + String.format("%04d", u));
+ plan.setIsPublic(1L);
+ plan.setIsUnion(1L);
+ plan.setReserveStatus(1L); // 待预约
+ plan.setSystemReserved(0L); // 未经系统匹配
+ plan.setManualReserved(0L);
+ plan.setBureauDeptId(bureauDeptIds[bureauIdx]);
+ plan.setBureauDeptName(bureauDeptNames[bureauIdx]);
+ plan.setOrganId(plan.getBureauDeptId());
+ plan.setOrganName(plan.getBureauDeptName());
+ plan.setDeptId(plan.getBureauDeptId());
+ plan.setDeptName(plan.getBureauDeptName());
+ plan.setIsDelete(0L);
+ plan.setCreateBy(1L);
+ plan.setCreateTime(new Date());
+ plan.setUpdateBy(1L);
+ plan.setUpdateTime(new Date());
+
+ cpPlanMapper.insert(plan);
+ insertedCount++;
+ }
+ }
+
+ System.out.println("========== 插入匹配测试数据完成 ==========");
+ System.out.println("共插入计划数: " + insertedCount);
+ System.out.println("涉及企业数: " + enterpriseCount);
+ System.out.println("每企业不同单位数: " + unitsPerEnterprise);
+ System.out.println("预期可匹配企业组数: " + enterpriseCount + " (每企业都有>=2个单位)");
+ System.out.println("===========================================");
+
+ Assertions.assertEquals(1000, insertedCount, "应插入恰好1000条计划");
+ }
+
+ @DisplayName("1. 查看当前待匹配计划数据概况")
+ @Test
+ @Order(1)
+ public void testCheckPendingPlans() {
+ // 查询符合自动匹配条件的计划
+ LambdaQueryWrapper lqw = Wrappers.lambdaQuery();
+ lqw.gt(EfcodeCpPlan::getPlanDate, new Date());
+ lqw.eq(EfcodeCpPlan::getSystemReserved, 0L);
+ lqw.eq(EfcodeCpPlan::getReserveStatus, 1L);
+ lqw.eq(EfcodeCpPlan::getIsPublic, 1L);
+ lqw.eq(EfcodeCpPlan::getIsUnion, 1L);
+ lqw.eq(EfcodeCpPlan::getIsDelete, 0L);
+ List pendingPlans = cpPlanMapper.selectList(lqw);
+
+ System.out.println("========== 待匹配计划数据概况 ==========");
+ System.out.println("符合条件的计划总数: " + pendingPlans.size());
+
+ // 按企业信用代码分组统计
+ Map> groupByEnterprise = pendingPlans.stream()
+ .filter(p -> p.getEnterpriseNumber() != null)
+ .collect(Collectors.groupingBy(EfcodeCpPlan::getEnterpriseNumber));
+
+ int potentialMatchCount = 0;
+ for (Map.Entry> entry : groupByEnterprise.entrySet()) {
+ // 统计不同执法单位数量
+ long distinctUnits = entry.getValue().stream()
+ .map(EfcodeCpPlan::getBureauDeptId)
+ .distinct()
+ .count();
+ if (distinctUnits >= 2) {
+ potentialMatchCount++;
+ System.out.println(" 企业[" + entry.getKey() + "] - 有" + distinctUnits + "个不同单位的计划,可匹配");
+ }
+ }
+
+ System.out.println("潜在可匹配的企业数: " + potentialMatchCount);
+ System.out.println("=======================================");
+ }
+
+ @DisplayName("2. 强制执行 autoMatchReserve 并验证结果")
+ @Test
+ @Order(2)
+ public void testAutoMatchReserveForceMode() {
+ // 执行前:统计当前系统匹配预约记录数
+ long beforeReserveCount = cpReserveMapper.selectCount(
+ Wrappers.lambdaQuery().eq(EfcodeCpReserve::getResFrom, 1L)
+ );
+ System.out.println("执行前系统匹配预约记录数: " + beforeReserveCount);
+
+ // 执行前:统计已被系统预约的计划数
+ long beforeMatchedPlanCount = cpPlanMapper.selectCount(
+ Wrappers.lambdaQuery()
+ .eq(EfcodeCpPlan::getSystemReserved, 1L)
+ .eq(EfcodeCpPlan::getIsDelete, 0L)
+ );
+ System.out.println("执行前已系统匹配计划数: " + beforeMatchedPlanCount);
+
+ // 执行自动匹配(强制模式,忽略日期校验)
+ System.out.println("\n========== 开始执行 autoMatchReserve(true) ==========");
+ cpPlanService.autoMatchReserve(true, true, true);
+ System.out.println("========== 执行完毕 ==========\n");
+
+ // 执行后:统计系统匹配预约记录数
+ long afterReserveCount = cpReserveMapper.selectCount(
+ Wrappers.lambdaQuery().eq(EfcodeCpReserve::getResFrom, 1L)
+ );
+ System.out.println("执行后系统匹配预约记录数: " + afterReserveCount);
+ System.out.println("本次新增预约记录数: " + (afterReserveCount - beforeReserveCount));
+
+ // 执行后:统计已被系统预约的计划数
+ long afterMatchedPlanCount = cpPlanMapper.selectCount(
+ Wrappers.lambdaQuery()
+ .eq(EfcodeCpPlan::getSystemReserved, 1L)
+ .eq(EfcodeCpPlan::getIsDelete, 0L)
+ );
+ System.out.println("执行后已系统匹配计划数: " + afterMatchedPlanCount);
+ System.out.println("本次新增匹配计划数: " + (afterMatchedPlanCount - beforeMatchedPlanCount));
+
+ // 验证:如果有符合条件的数据,则应产生匹配结果
+ if (afterReserveCount > beforeReserveCount) {
+ System.out.println("\n[验证通过] 自动匹配产生了新的预约记录");
+ } else {
+ System.out.println("\n[提示] 未产生新预约记录(可能没有符合条件的跨单位同企业计划)");
+ }
+ }
+
+ @DisplayName("3. 验证匹配后的 plan_group_id 分组一致性")
+ @Test
+ @Order(3)
+ public void testVerifyPlanGroupConsistency() {
+ // 查询所有系统匹配生成的预约记录
+ LambdaQueryWrapper lqw = Wrappers.lambdaQuery();
+ lqw.eq(EfcodeCpReserve::getResFrom, 1L);
+ lqw.eq(EfcodeCpReserve::getOpStatus, 3L); // 待确认
+ lqw.orderByDesc(EfcodeCpReserve::getCreateTime);
+ List reserves = cpReserveMapper.selectList(lqw);
+
+ if (reserves.isEmpty()) {
+ System.out.println("[跳过] 无系统匹配的预约记录,无法验证分组一致性");
+ return;
+ }
+
+ // 按 plan_group_id 分组
+ Map> groupMap = reserves.stream()
+ .collect(Collectors.groupingBy(EfcodeCpReserve::getPlanGroupId));
+
+ System.out.println("========== plan_group_id 分组验证 ==========");
+ System.out.println("共有 " + groupMap.size() + " 组联合预约");
+
+ int validGroups = 0;
+ for (Map.Entry> entry : groupMap.entrySet()) {
+ List group = entry.getValue();
+ // 每组至少2条记录(2个不同单位)
+ if (group.size() >= 2) {
+ validGroups++;
+
+ // 验证同组内关联的计划属于不同执法单位
+ Set planIds = group.stream()
+ .map(EfcodeCpReserve::getPlanId)
+ .collect(Collectors.toSet());
+ List plans = cpPlanMapper.selectByIds(planIds);
+ Set bureauDeptIds = plans.stream()
+ .map(EfcodeCpPlan::getBureauDeptId)
+ .collect(Collectors.toSet());
+
+ System.out.println(" groupId=" + entry.getKey()
+ + " | 记录数=" + group.size()
+ + " | 涉及单位数=" + bureauDeptIds.size());
+
+ // 断言:同组关联的计划应来自不同单位
+ Assertions.assertTrue(bureauDeptIds.size() >= 2,
+ "同一planGroupId下应关联至少2个不同执法单位的计划");
+ }
+ }
+ System.out.println("有效分组数(>=2条记录): " + validGroups);
+ System.out.println("=============================================");
+ }
+
+ @DisplayName("4. 验证匹配后计划状态已更新")
+ @Test
+ @Order(4)
+ public void testVerifyPlanStatusUpdated() {
+ // 查询已被系统匹配的计划
+ LambdaQueryWrapper lqw = Wrappers.lambdaQuery();
+ lqw.eq(EfcodeCpPlan::getSystemReserved, 1L);
+ lqw.eq(EfcodeCpPlan::getReserveStatus, 2L); // 预约中
+ lqw.eq(EfcodeCpPlan::getIsDelete, 0L);
+ List matchedPlans = cpPlanMapper.selectList(lqw);
+
+ System.out.println("========== 计划状态验证 ==========");
+ System.out.println("system_reserved=1 且 reserve_status=2 的计划数: " + matchedPlans.size());
+
+ if (!matchedPlans.isEmpty()) {
+ // 验证每条计划都满足状态要求
+ for (EfcodeCpPlan plan : matchedPlans) {
+ Assertions.assertEquals(1L, plan.getSystemReserved(),
+ "计划[" + plan.getId() + "]的system_reserved应为1");
+ Assertions.assertEquals(2L, plan.getReserveStatus(),
+ "计划[" + plan.getId() + "]的reserve_status应为2(预约中)");
+ }
+ System.out.println("[验证通过] 所有已匹配计划状态正确");
+ } else {
+ System.out.println("[提示] 无已匹配计划数据");
+ }
+ System.out.println("==================================");
+ }
+
+ @DisplayName("5. 再次执行 autoMatchReserve 验证幂等性(不重复匹配)")
+ @Test
+ @Order(5)
+ public void testAutoMatchReserveIdempotent() {
+ // 先统计当前预约记录数
+ long beforeCount = cpReserveMapper.selectCount(
+ Wrappers.lambdaQuery().eq(EfcodeCpReserve::getResFrom, 1L)
+ );
+
+ // 再次执行
+ System.out.println("========== 二次执行 autoMatchReserve(true) 验证幂等性 ==========");
+ cpPlanService.autoMatchReserve(true, true, true);
+
+ long afterCount = cpReserveMapper.selectCount(
+ Wrappers.lambdaQuery().eq(EfcodeCpReserve::getResFrom, 1L)
+ );
+
+ long newRecords = afterCount - beforeCount;
+ System.out.println("二次执行新增预约记录数: " + newRecords);
+
+ // 已匹配过的计划不应再次匹配(system_reserved已设为1)
+ // 但如果有新的符合条件的数据也可能产生新记录
+ System.out.println("[说明] 已匹配的计划(system_reserved=1)不会被重复匹配");
+ System.out.println("================================================================");
+ }
+}