From f5a55cc6dc0be592c790a4a1857528309ec4dba6 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: Tue, 26 May 2026 12:55:37 +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 --- .../013-计划任务表设计.sql | 8 +- .../enforcementcode/domain/EfcodeCpPlan.java | 5 + .../domain/bo/EfcodeCpPlanBo.java | 5 + .../domain/vo/EfcodeCpPlanVo.java | 6 + .../quartz/job/CpPlanAutoMatchJob.java | 11 +- .../service/IEfcodeCpPlanService.java | 9 +- .../service/impl/EfcodeCpPlanServiceImpl.java | 170 ++++++++- .../service/impl/EfcodeCpTaskServiceImpl.java | 12 + .../rest/test/cp/AutoMatchReserveTest.java | 354 ++++++++++++++++++ 9 files changed, 571 insertions(+), 9 deletions(-) create mode 100644 zdxt-web-server/zdxt-admin-web-server/src/test/java/com/zdxt/rest/test/cp/AutoMatchReserveTest.java 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("================================================================"); + } +}