From 8374ed739d1e42557a358260ee291bd5e9af08bd Mon Sep 17 00:00:00 2001 From: vincent Date: Wed, 2 Sep 2026 17:23:34 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=96=E9=83=A8=E6=8E=A5=E5=8F=A3=E7=94=9F?= =?UTF-8?q?=E6=88=90=E8=AE=A1=E5=88=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/util/DataFilterUtil.java | 47 +- .../common/commonenum/ChannelNoEnums.java | 93 ++ .../LongGangInterfaceController.java | 188 ++- .../enforcementcode/domain/EfcodeCpTask.java | 10 + .../LongGangCreateTaskBo.java | 108 +- .../service/IEfcodeCpPlanService.java | 8 + .../service/IOutEfcodeCpPlanService.java | 150 ++ .../service/IOutEfcodeCpTaskService.java | 122 ++ .../service/impl/EfcodeCpPlanServiceImpl.java | 17 +- .../impl/EfcodeCpReserveServiceImpl.java | 3 +- .../impl/OutEfcodeCpPlanServiceImpl.java | 1426 +++++++++++++++++ .../impl/OutEfcodeCpTaskServiceImpl.java | 328 ++++ .../enforcementcode/EfcodeCpPlanMapper.xml | 4 +- 13 files changed, 2470 insertions(+), 34 deletions(-) create mode 100644 zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/common/commonenum/ChannelNoEnums.java create mode 100644 zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/IOutEfcodeCpPlanService.java create mode 100644 zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/IOutEfcodeCpTaskService.java create mode 100644 zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/OutEfcodeCpPlanServiceImpl.java create mode 100644 zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/OutEfcodeCpTaskServiceImpl.java diff --git a/zdxt-modules/zdxt-enforcement-code-common/src/main/java/com/zdxt/enforcementcode/common/util/DataFilterUtil.java b/zdxt-modules/zdxt-enforcement-code-common/src/main/java/com/zdxt/enforcementcode/common/util/DataFilterUtil.java index d375bb08..f4a64fac 100644 --- a/zdxt-modules/zdxt-enforcement-code-common/src/main/java/com/zdxt/enforcementcode/common/util/DataFilterUtil.java +++ b/zdxt-modules/zdxt-enforcement-code-common/src/main/java/com/zdxt/enforcementcode/common/util/DataFilterUtil.java @@ -664,6 +664,22 @@ public class DataFilterUtil { return findOrganDeptByDeptId(srzDeptMapper, deptId, 0); } + /** + * 根据部门ID(srz_dept.organizationid)递归查找 unitTypeCode=10 的市认证机构。 + *

+ * 从传入的 organizationId 开始,在 srz_dept 表中逐级向上查找父级, + * 直到找到 unitTypeCode="10" 的执法单位机构。 + * + * @param organizationId 部门ID(srz_dept.organizationid) + * @return 市认证机构的 organizationId;未找到则返回 null + * @throws IllegalStateException 当 unitTypeCode=99 或递归超过4层时抛出 + */ + public static SrzDeptVo getOrganIdByDeptId(String organizationId) { + SrzDeptMapper srzDeptMapper = SpringUtils.getBean(SrzDeptMapper.class); + SrzDeptVo organDept = findOrganDeptByDeptId(srzDeptMapper, organizationId, 0); + return organDept; + } + /** * 根据 organizationId 递归查找 unitTypeCode=10 的执法单位 * @@ -783,8 +799,30 @@ public class DataFilterUtil { if (StrUtil.isBlank(deptId)) { return null; } + return getBureauDeptByDeptId(deptId); + } + + /** + * 根据部门ID查找对应的执法单位。 + *

+ * 逻辑: + *

    + *
  1. 用 deptId 查找 efcode_virtual_unit(按 sys_dept_id 匹配)
  2. + *
  3. 若存在匹配记录,优先返回 vir_show_at_business='是' 的记录对应的 SysDeptVo; + * 若无 business 记录且只有一条匹配也直接返回
  4. + *
  5. 若不存在匹配记录,通过 sys_dept 查找父节点,再用父节点 dept_id 重复上述查找
  6. + *
  7. 递归向上最多查 5 层(自身 + 4 层父节点)
  8. + *
+ * + * @param deptId 部门ID(sys_dept.dept_id) + * @return 对应的执法单位 SysDeptVo;如果向上 5 层仍未找到则返回 null + * @throws IllegalStateException 当某层匹配到多条 vir_show_at_business='是' 的记录,或多条匹配但无法确定唯一执法单位时抛出 + */ + public static SysDeptVo getBureauDeptByDeptId(String deptId) { + if (StrUtil.isBlank(deptId)) { + return null; + } - // 2. 从当前 deptId 开始,向上查找 sys_dept 父链,最多查 5 层(自身 + 4 层父节点) IEfcodeVirtualUnitService virtualUnitService = SpringUtils.getBean(IEfcodeVirtualUnitService.class); ISysDeptService sysDeptService = SpringUtils.getBean(ISysDeptService.class); @@ -810,15 +848,13 @@ public class DataFilterUtil { foundDeptId = businessUnits.get(0).getSysDeptId(); } else if (businessUnits.size() > 1) { throw new IllegalStateException( - "用户(userId=" + userId + ")的部门(deptId=" + currentDeptId + "部门(deptId=" + currentDeptId + ")在虚拟单位表中匹配到多条 vir_show_at_business='是' 的记录,无法确定唯一执法单位"); } else if (allMatched.size() == 1) { - // 没有 business 记录,如果只有一条匹配则返回 foundDeptId = allMatched.get(0).getSysDeptId(); } else { - // 多条匹配但无 business 标记,无法确定唯一执法单位 throw new IllegalStateException( - "用户(userId=" + userId + ")的部门(deptId=" + currentDeptId + "部门(deptId=" + currentDeptId + ")在虚拟单位表中匹配到多条记录且无 vir_show_at_business='是',无法确定唯一执法单位"); } @@ -833,7 +869,6 @@ public class DataFilterUtil { currentDeptId = dept.getParentId(); } - // 向上查找 4 层仍未找到对应的虚拟单位 return null; } diff --git a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/common/commonenum/ChannelNoEnums.java b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/common/commonenum/ChannelNoEnums.java new file mode 100644 index 00000000..b0141466 --- /dev/null +++ b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/common/commonenum/ChannelNoEnums.java @@ -0,0 +1,93 @@ +package com.zdxt.enforcementcode.common.commonenum; + +/** + * 渠道编号枚举定义 + *

对应 efcode_cp_plan / efcode_cp_task 表的 channel_no 字段

+ */ +public enum ChannelNoEnums { + + /** + * 市局 + */ + SHENZHEN("shenzhen", "市局"), + + /** + * 龙岗 + */ + LONGGANG("longgang", "龙岗"), + + /** + * 龙华 + */ + LONGHUA("longhua", "龙华"); + + private final String key; + private final String value; + + ChannelNoEnums(String key, String value) { + this.key = key; + this.value = value; + } + + public String getKey() { + return key; + } + + public String getValue() { + return value; + } + + /** + * 根据 key 获取 value + * + * @param key 渠道编号 + * @return 渠道描述 + */ + public static String getValueByKey(String key) { + if (key == null || key.isEmpty()) { + return ""; + } + for (ChannelNoEnums channel : ChannelNoEnums.values()) { + if (channel.getKey().equals(key)) { + return channel.getValue(); + } + } + return ""; + } + + /** + * 根据 value 获取 key + * + * @param value 渠道描述 + * @return 渠道编号 + */ + public static String getKeyByValue(String value) { + if (value == null || value.isEmpty()) { + return null; + } + for (ChannelNoEnums channel : ChannelNoEnums.values()) { + if (channel.getValue().equals(value)) { + return channel.getKey(); + } + } + return null; + } + + /** + * 判断 key 是否为合法渠道编号 + * + * @param key 渠道编号 + * @return true=合法, false=非法 + */ + public static boolean isValid(String key) { + if (key == null || key.isEmpty()) { + return false; + } + for (ChannelNoEnums channel : ChannelNoEnums.values()) { + if (channel.getKey().equals(key)) { + return true; + } + } + return false; + } +} diff --git a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/controller/externalInterface/LongGangInterfaceController.java b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/controller/externalInterface/LongGangInterfaceController.java index 2d076cba..8f13c4d7 100644 --- a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/controller/externalInterface/LongGangInterfaceController.java +++ b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/controller/externalInterface/LongGangInterfaceController.java @@ -2,15 +2,17 @@ package com.zdxt.enforcementcode.controller.externalInterface; import cn.dev33.satoken.annotation.SaIgnore; import cn.hutool.core.util.StrUtil; +import com.zdxt.enforcementcode.common.util.DataFilterUtil; +import com.zdxt.enforcementcode.common.util.StringUtils; import com.zdxt.enforcementcode.domain.EfcodeCpPlan; import com.zdxt.enforcementcode.domain.SysApiPlatformConfigEntity; import com.zdxt.enforcementcode.domain.bo.EfcodeCpPlanBo; import com.zdxt.enforcementcode.domain.bo.externalInterface.LongGangCreateTaskBo; import com.zdxt.enforcementcode.domain.vo.EfcodeCpPlanVo; +import com.zdxt.enforcementcode.domain.vo.EnterpriseInformationVo; +import com.zdxt.enforcementcode.domain.vo.SrzDeptVo; import com.zdxt.enforcementcode.domain.vo.externalInterface.LongGangCreateTaskVo; -import com.zdxt.enforcementcode.service.IEfcodeCpPlanService; -import com.zdxt.enforcementcode.service.IEfcodeCpTaskService; -import com.zdxt.enforcementcode.service.ISysApiPlatformConfigService; +import com.zdxt.enforcementcode.service.*; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.dromara.common.core.domain.R; @@ -18,9 +20,13 @@ import org.dromara.common.core.utils.MapstructUtils; import org.dromara.common.idempotent.annotation.RepeatSubmit; import org.dromara.common.log.annotation.Log; import org.dromara.common.log.enums.BusinessType; +import org.dromara.system.domain.vo.SysDeptVo; +import org.dromara.system.service.ISysDeptService; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +import java.util.List; + @Slf4j @SaIgnore @Validated @@ -31,17 +37,38 @@ public class LongGangInterfaceController { private static final String HEADER_APP_KEY = "X-App-Key"; - private final IEfcodeCpPlanService planService; - private final IEfcodeCpTaskService taskService; + private final IOutEfcodeCpPlanService outEfcodeCpPlanService; + private final IOutEfcodeCpTaskService outEfcodeCpTaskService; private final ISysApiPlatformConfigService sysApiPlatformConfigService; + private final IEnterpriseInformationService enterpriseInformationService; + private final ISysDeptService sysDeptService; + /** + * 外部接口-新增执法检查任务(龙岗渠道) + *

+ * 通过 appKey 识别调用渠道,接收执法检查计划参数,完成以下流程: + *

    + *
  1. 渠道认证:校验 appKey 有效性,获取 channelNo
  2. + *
  3. 部门校验:校验 deptId 在 sys_dept 中存在,并通过虚拟单位表查找所属执法单位(bureauDeptId)
  4. + *
  5. 市认证机构映射:通过 deptId 在 srz_dept 中递归查找 unittypecode=10 的顶层机构(organId)
  6. + *
  7. 企业匹配:通过 enterpriseNumber(统一社会信用代码)查询 enterprise_information 表, + * 存在则设置 enterpriseId,不存在则置空
  8. + *
  9. 计划入库:通过 insertPlanOnly 插入执法计划(不触发自动任务生成)
  10. + *
  11. 任务生成:调用 forceGenerateTask 生成检查任务,回写 plan.taskId 和 reserveStatus=3
  12. + *
+ * + * @param appKey 请求头 X-App-Key,用于识别调用渠道 + * @param bo 创建任务请求参数 + * @return 包含 planId、taskId、outSerialNo 的响应结果 + */ @SaIgnore @RepeatSubmit() - @Log(title = "外部接口-新增任务", businessType = BusinessType.INSERT) - @PostMapping("/createTask") - public R createTask( + @Log(title = "外部接口-新增计划", businessType = BusinessType.INSERT) + @PostMapping("/createPlan") + public R createPlan( @RequestHeader(value = HEADER_APP_KEY, required = false) String appKey, @Validated @RequestBody LongGangCreateTaskBo bo) { + // ========== 1. 渠道认证:校验 appKey 并获取 channelNo ========== if (StrUtil.isBlank(appKey)) { return R.fail("缺少请求头: " + HEADER_APP_KEY); } @@ -56,20 +83,75 @@ public class LongGangInterfaceController { return R.fail("渠道未配置 channelNo"); } + // ========== 流水号唯一校验:同一渠道下 outSerialNo 不可重复 ========== + if (StrUtil.isNotBlank(bo.getOutSerialNo())) { + EfcodeCpPlanBo queryBo = new EfcodeCpPlanBo(); + queryBo.setOutSerialNo(bo.getOutSerialNo()); + queryBo.setChannelNo(channelNo); + List existing = outEfcodeCpPlanService.queryList(queryBo); + if (!existing.isEmpty()) { + return R.fail("外部流水号重复: " + bo.getOutSerialNo()); + } + } + + // ========== 2. 部门校验:校验 deptId 在 sys_dept 表中存在 ========== + SysDeptVo deptVo = sysDeptService.selectDeptById(bo.getDeptId()); + if (deptVo == null) { + return R.fail("部门不存在: " + bo.getDeptId()); + } + + // ========== 3. 市认证机构映射:通过 deptId(srz_dept.organizationid) 递归查找 unittypecode=10 的执法机构 ========== + SrzDeptVo srzDeptVo = DataFilterUtil.getOrganIdByDeptId(bo.getDeptId()); + + // ========== 4. 执法单位查找:通过虚拟单位表向上查找所属执法单位(zone!=0 且 zone!=2) ========== + SysDeptVo bureauDeptVo = DataFilterUtil.getBureauDeptByDeptId(bo.getDeptId()); + if (bureauDeptVo == null) { + return R.fail("执法单位不存在"); + } + + // ========== 5. 企业匹配:通过统一社会信用代码查询 enterprise_information 表 ========== + EnterpriseInformationVo enterprise = enterpriseInformationService.queryByEnterpriseNumber(bo.getEnterpriseNumber()); + String enterpriseId; + if (enterprise != null) { + enterpriseId = enterprise.getId(); + } else { + enterpriseId = ""; + } + + // ========== 6. 构建计划对象并设置默认值 ========== EfcodeCpPlanBo planBo = MapstructUtils.convert(bo, EfcodeCpPlanBo.class); planBo.setChannelNo(channelNo); - planBo.setTaskId(""); + if(StringUtils.isEmpty(bo.getTaskId())){ + planBo.setTaskId(""); + } planBo.setReserveStatus(0L); planBo.setSystemReserved(0L); planBo.setManualReserved(0L); planBo.setIsDelete(0L); + planBo.setEnforceUserCodes("-"); + // 设置执法单位(bureauDeptId 来自 sys_dept,是本地系统的局级执法单位) + planBo.setBureauDeptId(bureauDeptVo.getDeptId()); + planBo.setBureauDeptName(bureauDeptVo.getDeptName()); + // 设置企业ID(存在则取主键,不存在则置空) + planBo.setEnterpriseId(enterpriseId); + // 设置市认证机构(organId 来自 srz_dept,是市认证系统的顶层执法单位) + if(srzDeptVo != null){ + planBo.setOrganId(srzDeptVo.getOrganizationid()); + planBo.setOrganName(srzDeptVo.getName()); + }else{ + planBo.setOrganId(""); + planBo.setOrganName(""); + } - planService.insertByBo(planBo); + // ========== 7. 计划入库:仅插入计划,不触发 afterInsert 中的自动任务生成 ========== + outEfcodeCpPlanService.insertPlanOnly(planBo); - EfcodeCpPlanVo planVo = planService.queryById(planBo.getId()); + // ========== 8. 生成任务:创建 task 记录,回写 plan.taskId 和 plan.reserveStatus=3 ========== + EfcodeCpPlanVo planVo = outEfcodeCpPlanService.queryById(planBo.getId()); EfcodeCpPlan plan = MapstructUtils.convert(planVo, EfcodeCpPlan.class); - String taskId = taskService.forceGenerateTask(plan); + String taskId = outEfcodeCpTaskService.forceGenerateTask(plan); + // ========== 9. 构建返回结果 ========== LongGangCreateTaskVo vo = new LongGangCreateTaskVo(); vo.setPlanId(planBo.getId()); vo.setTaskId(taskId); @@ -79,4 +161,86 @@ public class LongGangInterfaceController { appKey, channelNo, planBo.getId(), taskId); return R.ok(vo); } + + /** + * 外部接口-修改执法检查计划 + *

+ * 通过 planId 定位已有计划,校验状态后更新字段。 + * 仅 reserveStatus=0(草稿) 或 1(待预约) 的计划允许修改。 + * + * @param appKey 请求头 X-App-Key,用于识别调用渠道 + * @param bo 修改计划请求参数(planId 必填) + * @return 修改结果 + */ + @SaIgnore + @RepeatSubmit() + @Log(title = "外部接口-修改计划", businessType = BusinessType.UPDATE) + @PostMapping("/updatePlan") + public R updatePlan( + @RequestHeader(value = HEADER_APP_KEY, required = false) String appKey, + @Validated @RequestBody LongGangCreateTaskBo bo) { + // ========== 1. 渠道认证 ========== + if (StrUtil.isBlank(appKey)) { + return R.fail("缺少请求头: " + HEADER_APP_KEY); + } + SysApiPlatformConfigEntity consumer = sysApiPlatformConfigService.findByAppKey(appKey).orElse(null); + if (consumer == null) { + return R.fail("无效的 appKey"); + } + String channelNo = consumer.getChannelNo(); + if (StrUtil.isBlank(channelNo)) { + return R.fail("渠道未配置 channelNo"); + } + + // ========== 2. 校验 planId ========== + if (StrUtil.isBlank(bo.getPlanId())) { + return R.fail("计划ID不能为空"); + } + EfcodeCpPlanVo existingPlan = outEfcodeCpPlanService.queryById(bo.getPlanId()); + if (existingPlan == null) { + return R.fail("计划不存在: " + bo.getPlanId()); + } + // 仅草稿(0)和待预约(1)状态允许修改 +// Long reserveStatus = existingPlan.getReserveStatus(); +// if (reserveStatus == null || (reserveStatus != 0L && reserveStatus != 1L)) { +// return R.fail("当前计划状态不允许修改"); +// } + + // ========== 3. 部门校验 ========== + SysDeptVo deptVo = sysDeptService.selectDeptById(bo.getDeptId()); + if (deptVo == null) { + return R.fail("部门不存在: " + bo.getDeptId()); + } + SrzDeptVo srzDeptVo = DataFilterUtil.getOrganIdByDeptId(bo.getDeptId()); + SysDeptVo bureauDeptVo = DataFilterUtil.getBureauDeptByDeptId(bo.getDeptId()); + if (bureauDeptVo == null) { + return R.fail("执法单位不存在"); + } + + // ========== 4. 企业匹配 ========== + EnterpriseInformationVo enterprise = enterpriseInformationService.queryByEnterpriseNumber(bo.getEnterpriseNumber()); + String enterpriseId = (enterprise != null) ? enterprise.getId() : ""; + + // ========== 5. 构建更新对象 ========== + EfcodeCpPlanBo planBo = MapstructUtils.convert(bo, EfcodeCpPlanBo.class); + planBo.setId(bo.getPlanId()); + planBo.setChannelNo(channelNo); + planBo.setBureauDeptId(bureauDeptVo.getDeptId()); + planBo.setBureauDeptName(bureauDeptVo.getDeptName()); + planBo.setEnterpriseId(enterpriseId); + if (srzDeptVo != null) { + planBo.setOrganId(srzDeptVo.getOrganizationid()); + planBo.setOrganName(srzDeptVo.getName()); + } else { + planBo.setOrganId(""); + planBo.setOrganName(""); + } + + // ========== 6. 执行更新 ========== + outEfcodeCpPlanService.updateByBo(planBo); + + log.info("[外部接口] 修改计划, appKey={}, channelNo={}, planId={}", + appKey, channelNo, bo.getPlanId()); + return R.ok(); + } } diff --git a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/EfcodeCpTask.java b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/EfcodeCpTask.java index 077de1a5..aede276e 100644 --- a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/EfcodeCpTask.java +++ b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/EfcodeCpTask.java @@ -37,6 +37,16 @@ public class EfcodeCpTask extends BaseEntity { */ private String bureauDeptId; + /** + * 渠道编号 shenzhen|市局,longgang|龙岗,longhua|龙华 + */ + private String channelNo; + + /** + * 外部流水号 + */ + private String outSerialNo; + /** 预约是否已读 0:未读 1:已读 */ private int isRead; } diff --git a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/bo/externalInterface/LongGangCreateTaskBo.java b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/bo/externalInterface/LongGangCreateTaskBo.java index c9d21e9d..2474a156 100644 --- a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/bo/externalInterface/LongGangCreateTaskBo.java +++ b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/bo/externalInterface/LongGangCreateTaskBo.java @@ -1,68 +1,152 @@ package com.zdxt.enforcementcode.domain.bo.externalInterface; import com.fasterxml.jackson.annotation.JsonFormat; +import com.zdxt.enforcementcode.domain.bo.EfcodeCpPlanBo; +import io.github.linpeilie.annotations.AutoMapper; import lombok.Data; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import java.util.Date; +/** + * 外部接口-新增任务请求参数(龙岗渠道) + */ @Data +@AutoMapper(target = EfcodeCpPlanBo.class) public class LongGangCreateTaskBo { + /** + * 计划ID(修改时必传,新增时不传) + */ + private String planId; + + /** + * 计划类别:1=日常计划,2=专项计划 + */ @NotNull(message = "计划类别不能为空") private Long planType; + /** + * 计划执行日期(如 2025-05-08) + */ @NotNull(message = "计划执行日期不能为空") @JsonFormat(pattern = "yyyy-MM-dd") private Date planDate; + /** + * 执行时段:1=上午,2=下午,3=其他 + */ @NotNull(message = "执行时段不能为空") private Long planTime; + /** + * 执法事项(最多1000字) + */ @NotBlank(message = "执法事项不能为空") private String enforceContent; + /** + * 被检查企业/机构名称 + */ @NotBlank(message = "企业名称不能为空") private String enterpriseName; + /** + * 被检查企业/机构统一社会信用代码 + */ @NotBlank(message = "企业信用代码不能为空") private String enterpriseNumber; - private String enterpriseId; +// /** +// * 被检查企业/机构ID(系统内主键) +// */ +// private String enterpriseId; + /** + * 企业联系人 + */ private String contactPerson; + /** + * 企业联系电话 + */ private String contactPhone; + /** + * 执法人员姓名(多个逗号分隔) + */ @NotBlank(message = "执法人员姓名不能为空") private String enforceUserNames; + /** + * 执法人员ID(多个逗号分隔,与 enforceUserNames 顺序对应) + */ + @NotBlank(message = "执法人员ID不能为空") private String enforceUserIds; - private String enforceUserCodes; + /** + * 执法人员证件号码、电话号码,有执法证件号传证件号,没有证件号传手机号码,手机号码中间4位脱敏(多个逗号分隔,与 enforceUserNames 顺序对应) + */ private String enforceUserTels; + /** + * 是否公开:1=是,0=否(默认0) + */ private Long isPublic = 0L; + /** + * 是否联合执法:1=是,0=否(默认0) + */ private Long isUnion = 0L; - @NotBlank(message = "执法单位ID不能为空") - private String bureauDeptId; - - @NotBlank(message = "执法单位名称不能为空") - private String bureauDeptName; - - private String organId; - - private String organName; - +// /** +// * 执法单位ID +// */ +// @NotBlank(message = "执法单位ID不能为空") +// private String bureauDeptId; +// +// /** +// * 执法单位名称 +// */ +// @NotBlank(message = "执法单位名称不能为空") +// private String bureauDeptName; + +// /** +// * 市认证机构ID +// */ +// private String organId; +// +// /** +// * 市认证机构名称 +// */ +// private String organName; + + /** + * 市认证部门ID + */ + @NotBlank(message = "部门ID不能为空") private String deptId; + /** + * 市认证部门名称 + */ + @NotBlank(message = "部门名称不能为空") private String deptName; + /** + * 备注 + */ private String remark; + /** + * 外部流水号(调用方自定义,用于幂等/对账) + */ private String outSerialNo; + + /** + * 任务id:空串表示还没生成任务 + */ + private String taskId; } 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 d9c2b542..3b12e048 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 @@ -52,6 +52,14 @@ public interface IEfcodeCpPlanService { */ Boolean insertByBo(EfcodeCpPlanBo bo); + /** + * 外部接口专用:仅插入计划记录,不触发后置任务生成逻辑 + * + * @param bo 行政执法检查计划-计划 + * @return 是否新增成功 + */ + Boolean insertPlanOnly(EfcodeCpPlanBo bo); + /** * 修改行政执法检查计划-计划 * diff --git a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/IOutEfcodeCpPlanService.java b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/IOutEfcodeCpPlanService.java new file mode 100644 index 00000000..c7b83dc6 --- /dev/null +++ b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/IOutEfcodeCpPlanService.java @@ -0,0 +1,150 @@ +package com.zdxt.enforcementcode.service; + +import com.zdxt.enforcementcode.domain.EfcodeCpReserve; +import com.zdxt.enforcementcode.domain.bo.EfcodeCpPlanBo; +import com.zdxt.enforcementcode.domain.vo.EfcodeCpPlanVo; +import org.dromara.common.mybatis.core.page.PageQuery; +import org.dromara.common.mybatis.core.page.TableDataInfo; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * 行政执法检查计划-计划Service接口 + * + * @author Lion Li + * @date 2026-05-17 + */ +public interface IOutEfcodeCpPlanService { + + /** + * 查询行政执法检查计划-计划 + * + * @param id 主键 + * @return 行政执法检查计划-计划 + */ + EfcodeCpPlanVo queryById(String id); + + /** + * 分页查询行政执法检查计划-计划列表 + * + * @param bo 查询条件 + * @param pageQuery 分页参数 + * @return 行政执法检查计划-计划分页列表 + */ + TableDataInfo queryPageList(EfcodeCpPlanBo bo, PageQuery pageQuery); + + /** + * 查询符合条件的行政执法检查计划-计划列表 + * + * @param bo 查询条件 + * @return 行政执法检查计划-计划列表 + */ + List queryList(EfcodeCpPlanBo bo); + + /** + * 新增行政执法检查计划-计划 + * + * @param bo 行政执法检查计划-计划 + * @return 是否新增成功 + */ + Boolean insertByBo(EfcodeCpPlanBo bo); + + /** + * 外部接口专用:仅插入计划记录,不触发后置任务生成逻辑 + * + * @param bo 行政执法检查计划-计划 + * @return 是否新增成功 + */ + Boolean insertPlanOnly(EfcodeCpPlanBo bo); + + /** + * 修改行政执法检查计划-计划 + * + * @param bo 行政执法检查计划-计划 + * @return 是否修改成功 + */ + Boolean updateByBo(EfcodeCpPlanBo bo); + + /** + * 分页查询其他计划列表(本单位公开+不公开计划 + 其他单位公开计划,仅待预约状态) + * + * @param bo 搜索条件 + * @param pageQuery 分页参数 + * @param bureauDeptId 当前用户的执法单位ID + * @return 计划分页列表 + */ + TableDataInfo queryOtherPageList(EfcodeCpPlanBo bo, PageQuery pageQuery, String bureauDeptId); + + /** + * 分页查询重叠计划列表:查找与当前用户计划同一时间段、同一企业的其他用户的计划 + * + * @param bo 搜索条件 + * @param pageQuery 分页参数 + * @param bureauDeptId 当前用户的执法单位ID + * @return 计划分页列表 + */ + TableDataInfo queryOverlapPageList(EfcodeCpPlanBo bo, PageQuery pageQuery, String bureauDeptId); + + /** + * 校验并批量删除行政执法检查计划-计划信息 + * + * @param ids 待删除的主键集合 + * @param isValid 是否进行有效性校验 + * @return 是否删除成功 + */ + Boolean deleteWithValidByIds(Collection ids, Boolean isValid); + + /** + * 计划自动匹配预约(定时任务调用) + *

扫描待预约状态的计划,按规则自动匹配并生成预约记录

+ * + * @param forceMatch 是否强制匹配(忽略 CP_SETTING_AUTO_MATCH_START 日期校验) + * @param updatePlan 是否更新计划记录(system_reserved、reserve_status) + * @param insertReserve 是否将预约记录插入数据库;为false时仅计算匹配结果不落库 + * @return 按 planGroupId 分组的预约记录列表(key=planGroupId, value=该组预约记录) + */ + //Map> autoMatchReserve(boolean forceMatch, boolean updatePlan, boolean insertReserve); + + @Transactional(rollbackFor = Exception.class) + Map> autoMatchReserveV3(boolean forceMatch, boolean updatePlan, boolean insertReserve); + + /** + * 计划自动匹配预约V2(定时任务调用) + *

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

+ * + * @param forceMatch 是否强制匹配(忽略 CP_SETTING_AUTO_MATCH_START 日期校验) + * @param updatePlan 是否更新计划记录(system_reserved、reserve_status) + * @param insertReserve 是否将预约记录插入数据库;为false时仅计算匹配结果不落库 + * @return 按 planGroupId 分组的预约记录列表 + */ + //Map> autoMatchReserveV2(boolean forceMatch, boolean updatePlan, boolean insertReserve); + + /** + * 计划到期自动生成任务 + *

距计划执行日期 <= CP_SETTING_MANUAL_MATCH_DAYS 天时, + * 系统自动取消剩余未处理的预约(含系统匹配与手动预约),并为计划生成任务。

+ * + * @return 处理的计划数量 + */ + int processPlanDeadlineAutoGenTask(); + + /** + * 计划到期自动生成任务(支持忽略时间) + * + * @param ignoreTime true=忽略时间窗口限制,处理所有未生成任务的计划;false=仅处理到期窗口内的计划 + * @return 处理的计划数量 + */ + int processPlanDeadlineAutoGenTask(boolean ignoreTime); + + /** + * 取消超出自动匹配时间的联合执法计划 + *

当计划的自动匹配期限(计划所在周起始日 + systemMatchWeeks 周)已过期, + * 且仍处于联合执法状态(isUnion=1),则将 isUnion 置为 0 取消联合。

+ * + * @return 被取消联合的计划数量 + */ + int updateIsUnion(); +} diff --git a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/IOutEfcodeCpTaskService.java b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/IOutEfcodeCpTaskService.java new file mode 100644 index 00000000..ea97a2e2 --- /dev/null +++ b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/IOutEfcodeCpTaskService.java @@ -0,0 +1,122 @@ +package com.zdxt.enforcementcode.service; + +import com.zdxt.enforcementcode.domain.EfcodeCpPlan; +import com.zdxt.enforcementcode.domain.bo.EfcodeCpTaskBo; +import com.zdxt.enforcementcode.domain.vo.EfcodeCpTaskVo; +import org.dromara.common.mybatis.core.page.PageQuery; +import org.dromara.common.mybatis.core.page.TableDataInfo; + +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * 行政执法检查计划-任务Service接口 + * + * @author Lion Li + * @date 2026-05-17 + */ +public interface IOutEfcodeCpTaskService { + + /** + * 查询行政执法检查计划-任务 + * + * @param id 主键 + * @return 行政执法检查计划-任务 + */ + EfcodeCpTaskVo queryById(String id); + + /** + * 分页查询行政执法检查计划-任务列表 + * + * @param bo 查询条件 + * @param pageQuery 分页参数 + * @return 行政执法检查计划-任务分页列表 + */ + TableDataInfo queryPageList(EfcodeCpTaskBo bo, PageQuery pageQuery); + + /** + * 查询符合条件的行政执法检查计划-任务列表 + * + * @param bo 查询条件 + * @return 行政执法检查计划-任务列表 + */ + List queryList(EfcodeCpTaskBo bo); + + /** + * 新增行政执法检查计划-任务 + * + * @param bo 行政执法检查计划-任务 + * @return 是否新增成功 + */ + Boolean insertByBo(EfcodeCpTaskBo bo); + + /** + * 修改行政执法检查计划-任务 + * + * @param bo 行政执法检查计划-任务 + * @return 是否修改成功 + */ + Boolean updateByBo(EfcodeCpTaskBo bo); + + Boolean updateIsRead(String taskId); + + /** + * 校验并批量删除行政执法检查计划-任务信息 + * + * @param ids 待删除的主键集合 + * @param isValid 是否进行有效性校验 + * @return 是否删除成功 + */ + Boolean deleteWithValidByIds(Collection ids, Boolean isValid); + + /** + * 查询我的任务列表(通过计划表关联,筛选当前用户参与的任务) + * + * @param userId 当前用户ID + * @param planDate 计划执行日期(可为null) + * @return 任务列表 + */ + List queryMyTaskList(Long userId, Date planDate,String searchValue); + + /** + * 查询指定月份中有任务的日期列表(用于日历打点) + * + * @param userId 当前用户ID + * @param yearMonth 年月,格式如 "2026-03" + * @return 日期字符串列表 + */ + List queryMyTaskDates(Long userId, String yearMonth); + + /** + * 查询我的任务详情(通过 planId 查询任务+计划完整信息) + * + * @param planId 计划ID + * @return 任务详情 + */ + EfcodeCpTaskVo queryMyTaskDetail(String planId); + + /** + * 批量查询关联计划信息(用于列表页第二次请求加载联合执法关联单位信息) + * + * @param planIds 计划ID列表(通常为isUnion=1的任务对应的planId) + * @return Map: key=planId, value=该计划关联的其他单位信息 + */ + Map> queryAssociatedPlansBatch(List planIds); + + /** + * 强制创建任务(忽略其他条件),根据计划信息直接生成任务记录并回写 plan.task_id + * + * @param plan 来源计划 + * @return 新建任务ID + */ + String forceGenerateTask(EfcodeCpPlan plan); + + /** + * 统计当前登录用户的执任务未读个数 + * + * @return 未读执法任务数量 + */ + Long countUnreadTasksForCurrentUser(); +} 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 cf8c853b..f7c543e9 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 @@ -1,5 +1,6 @@ package com.zdxt.enforcementcode.service.impl; +import com.zdxt.enforcementcode.common.commonenum.ChannelNoEnums; import org.dromara.common.core.utils.MapstructUtils; import org.dromara.common.core.utils.StringUtils; import org.dromara.common.mybatis.core.page.TableDataInfo; @@ -360,6 +361,20 @@ public class EfcodeCpPlanServiceImpl implements IEfcodeCpPlanService { return flag; } + @Transactional(rollbackFor = Exception.class) + @Override + public Boolean insertPlanOnly(EfcodeCpPlanBo bo) { + EfcodeCpPlan add = MapstructUtils.convert(bo, EfcodeCpPlan.class); + fillContactInfo(add); + validEntityBeforeSave(add); + boolean flag = baseMapper.insert(add) > 0; + if (flag) { + bo.setId(add.getId()); + syncPlanUsers(add.getId(), bo.getEnforceUserIds(), bo.getEnforceUserNames(), bo.getEnforceUserTels()); + } + return flag; + } + /** * 修改行政执法检查计划-计划 * @@ -792,7 +807,7 @@ public class EfcodeCpPlanServiceImpl implements IEfcodeCpPlanService { lqw.eq(EfcodeCpPlan::getIsPublic, 1L); lqw.eq(EfcodeCpPlan::getIsUnion, 1L); lqw.eq(EfcodeCpPlan::getIsDelete, 0L); - lqw.eq(EfcodeCpPlan::getChannelNo, "0"); + lqw.eq(EfcodeCpPlan::getChannelNo, ChannelNoEnums.SHENZHEN.getKey()); lqw.isNotNull(EfcodeCpPlan::getCreateBy); lqw.orderByAsc(EfcodeCpPlan::getPlanDate); List pendingPlans = baseMapper.selectList(lqw); 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 52a70afd..4d35a145 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 @@ -1,6 +1,7 @@ package com.zdxt.enforcementcode.service.impl; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.zdxt.enforcementcode.common.commonenum.ChannelNoEnums; import com.zdxt.enforcementcode.common.commonenum.MsgSourceTypeEnum; import com.zdxt.enforcementcode.common.commonenum.PlatformTypeEnums; import com.zdxt.enforcementcode.domain.EfcodeCpTask; @@ -200,7 +201,7 @@ public class EfcodeCpReserveServiceImpl implements IEfcodeCpReserveService { if (targetPlan == null) { return R.fail("目标计划不存在"); } - if (!"0".equals(targetPlan.getChannelNo())) {//只有市司法局的计划支持预约 + if (!ChannelNoEnums.SHENZHEN.getKey().equals(targetPlan.getChannelNo())) {//只有市司法局的计划支持预约 return R.fail("该渠道的计划不支持预约"); } // 1.1 校验目标计划状态:只有待预约(1)、已取消(4)、预约中可加入(5)的计划才允许预约 diff --git a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/OutEfcodeCpPlanServiceImpl.java b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/OutEfcodeCpPlanServiceImpl.java new file mode 100644 index 00000000..35924970 --- /dev/null +++ b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/OutEfcodeCpPlanServiceImpl.java @@ -0,0 +1,1426 @@ +package com.zdxt.enforcementcode.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.zdxt.enforcementcode.common.commonenum.ChannelNoEnums; +import com.zdxt.enforcementcode.domain.EfcodeCpPlan; +import com.zdxt.enforcementcode.domain.EfcodeCpPlanUsers; +import com.zdxt.enforcementcode.domain.EfcodeCpReserve; +import com.zdxt.enforcementcode.domain.bo.EfcodeCpPlanBo; +import com.zdxt.enforcementcode.domain.vo.EfcodeCpPlanVo; +import com.zdxt.enforcementcode.domain.vo.EfcodeCpReserveVo; +import com.zdxt.enforcementcode.domain.vo.EnterpriseInformationVo; +import com.zdxt.enforcementcode.domain.vo.SsztJbxx110497NewVo; +import com.zdxt.enforcementcode.mapper.EfcodeCpPlanMapper; +import com.zdxt.enforcementcode.mapper.EfcodeCpPlanUsersMapper; +import com.zdxt.enforcementcode.mapper.EfcodeCpReserveMapper; +import com.zdxt.enforcementcode.service.*; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.dromara.common.core.exception.ServiceException; +import org.dromara.common.core.utils.MapstructUtils; +import org.dromara.common.core.utils.StringUtils; +import org.dromara.common.mybatis.core.page.PageQuery; +import org.dromara.common.mybatis.core.page.TableDataInfo; +import org.dromara.common.satoken.utils.LoginHelper; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; +import java.time.temporal.TemporalAdjusters; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 行政执法检查计划-计划Service业务层处理 + * + * @author Lion Li + * @date 2026-05-17 + */ +@Slf4j +@RequiredArgsConstructor +@Service +public class OutEfcodeCpPlanServiceImpl implements IOutEfcodeCpPlanService { + + private final EfcodeCpPlanMapper baseMapper; + private final EfcodeCpPlanUsersMapper efcodeCpPlanUsersMapper; + private final EfcodeCpReserveMapper efcodeCpReserveMapper; + private final IEfcodeCpSettingService cpSettingService; + private final IEfcodeCpTaskService cpTaskService; + private final IEnterpriseInformationService enterpriseInformationService; + private final ISsztJbxx110497NewService ssztJbxx110497NewService; + + /** + * 查询行政执法检查计划-计划 + * + * @param id 主键 + * @return 行政执法检查计划-计划 + */ + @Override + public EfcodeCpPlanVo queryById(String id){ + return baseMapper.selectVoById(id); + } + + /** + * 分页查询行政执法检查计划-计划列表 + * + * @param bo 查询条件 + * @param pageQuery 分页参数 + * @return 行政执法检查计划-计划分页列表 + */ + @Override + public TableDataInfo queryPageList(EfcodeCpPlanBo bo, PageQuery pageQuery) { + LambdaQueryWrapper lqw = buildQueryWrapper(bo); + Page result = baseMapper.selectVoPage(pageQuery.build(), lqw); + // 后置计算 editEnable + computeEditEnable(result.getRecords()); + // 后置填充关联预约记录 + fillReserves(result.getRecords()); + return TableDataInfo.build(result); + } + + /** + * 查询符合条件的行政执法检查计划-计划列表 + * + * @param bo 查询条件 + * @return 行政执法检查计划-计划列表 + */ + @Override + public List queryList(EfcodeCpPlanBo bo) { + LambdaQueryWrapper lqw = buildQueryWrapper(bo); + List list = baseMapper.selectVoList(lqw); + // 后置计算 editEnable + computeEditEnable(list); + return list; + } + + /** + * 分页查询其他计划列表 + *

查询范围:本执法单位的所有计划 + 其他单位的公开计划,仅待预约状态,排除当前用户自己创建的计划。

+ */ + @Override + public TableDataInfo queryOtherPageList(EfcodeCpPlanBo bo, PageQuery pageQuery, String bureauDeptId) { + Long currentUserId = LoginHelper.getUserId(); + + Page page = pageQuery.build(); + Page result = baseMapper.selectOtherPageList(page, bo, String.valueOf(currentUserId), bureauDeptId); + // 后置处理:控制记录是否可预约 + afterQueryOtherList(result.getRecords(), bureauDeptId, currentUserId); + return TableDataInfo.build(result); + } + + /** + * 分页查询重叠计划列表 + *

查找与当前用户计划同一时间段、同一企业的其他用户的计划

+ */ + @Override + public TableDataInfo queryOverlapPageList(EfcodeCpPlanBo bo, PageQuery pageQuery, String bureauDeptId) { + Long currentUserId = LoginHelper.getUserId(); + + Page page = pageQuery.build(); + Page result = baseMapper.selectOverlapPageList(page, bo, String.valueOf(currentUserId), bureauDeptId); + // 后置处理:控制记录是否可预约 + afterQueryOtherList(result.getRecords(), bureauDeptId, currentUserId); + return TableDataInfo.build(result); + } + + /** + * 其他计划列表后置处理:控制每条记录是否可以预约 + *

当前规则:待预约状态均可预约(后续可扩展更多业务规则)

+ * + * @param records 查询结果列表 + * @param bureauDeptId 当前用户执法单位ID + * @param currentUserId 当前用户ID + */ + private void afterQueryOtherList(List records, String bureauDeptId, Long currentUserId) { + if (records == null || records.isEmpty()) { + return; + } + for (EfcodeCpPlanVo vo : records) { + // 只有联合执法的计划才可以预约 + vo.setCanReserve(vo.getIsUnion() != null && vo.getIsUnion() == 1L); + } + } + + /** + * 后置计算 editEnable / deleteEnable:判断记录是否可编辑、可删除 + *

editEnable 规则:预约状态为草稿(0)或待预约(1)时可编辑,其他状态不可编辑

+ *

deleteEnable 规则:可编辑时可删除;或者不联合执法(isUnion=0)且计划执行时间还未到时也可删除

+ * + * @param records 查询结果列表 + */ + private void computeEditEnable(List records) { + if (records == null || records.isEmpty()) { + return; + } + LocalDate today = LocalDate.now(); + for (EfcodeCpPlanVo vo : records) { + Long status = vo.getReserveStatus(); + // 草稿(0)或待预约(1)可编辑,其他状态不可编辑 + boolean canEdit = status != null && status <= 1L; + vo.setEditEnable(canEdit ? 1L : 0L); + + // 删除条件:可编辑 OR (不联合执法 AND 执行时间未到) + boolean canDelete = canEdit; + if (!canDelete && vo.getIsUnion() != null && vo.getIsUnion() == 0L && vo.getPlanDate() != null) { + LocalDate planLocalDate = vo.getPlanDate().toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); + if (planLocalDate.isAfter(today)) { + canDelete = true; + } + } + vo.setDeleteEnable(canDelete ? 1L : 0L); + } + } + + /** + * 后置填充关联预约记录:通过plan_group_id查询同组的其他计划信息,组装为1:N结构 + *

展示逻辑:对于我的每条计划,找到同一plan_group_id下的其他计划,将对方的单位/人员/电话/执法事项填充到reserve VO中

+ */ + private void fillReserves(List records) { + if (records == null || records.isEmpty()) { + return; + } + List myPlanIds = records.stream().map(EfcodeCpPlanVo::getId).collect(Collectors.toList()); + if (myPlanIds.isEmpty()) { + return; + } + + // 1. 查询我的计划关联的预约记录,获取plan_group_id + List myReserves = efcodeCpReserveMapper.selectList( + Wrappers.lambdaQuery() + .in(EfcodeCpReserve::getPlanId, myPlanIds) + ); + if (myReserves.isEmpty()) { + records.forEach(vo -> vo.setReserves(new ArrayList<>())); + return; + } + + // 2. 建立 我的planId → plan_group_id 的映射 + Map planToGroupMap = new HashMap<>(); + Set groupIds = new HashSet<>(); + for (EfcodeCpReserve r : myReserves) { + if (StringUtils.isNotBlank(r.getPlanGroupId())) { + planToGroupMap.put(r.getPlanId(), r.getPlanGroupId()); + groupIds.add(r.getPlanGroupId()); + } + } + + if (groupIds.isEmpty()) { + records.forEach(vo -> vo.setReserves(new ArrayList<>())); + return; + } + + // 3. 查询同组的其他预约记录(plan_id不是我的计划) + List otherReserves = efcodeCpReserveMapper.selectList( + Wrappers.lambdaQuery() + .in(EfcodeCpReserve::getPlanGroupId, groupIds) + .notIn(EfcodeCpReserve::getPlanId, myPlanIds) + .orderByDesc(EfcodeCpReserve::getReserveTime) + ); + + // 4. 查询其他计划的详情(单位、人员、电话、执法事项) + Set otherPlanIds = otherReserves.stream() + .map(EfcodeCpReserve::getPlanId) + .filter(StringUtils::isNotBlank) + .collect(Collectors.toSet()); + Map otherPlanMap = new HashMap<>(); + if (!otherPlanIds.isEmpty()) { + List otherPlans = baseMapper.selectByIds(new ArrayList<>(otherPlanIds)); + for (EfcodeCpPlan p : otherPlans) { + otherPlanMap.put(p.getId(), p); + } + } + + // 5. 按plan_group_id分组其他预约记录 + Map> groupToOtherReserves = otherReserves.stream() + .collect(Collectors.groupingBy(EfcodeCpReserve::getPlanGroupId)); + + // 6. 填充到每条计划VO + for (EfcodeCpPlanVo vo : records) { + String groupId = planToGroupMap.get(vo.getId()); + if (StringUtils.isBlank(groupId)) { + vo.setReserves(new ArrayList<>()); + continue; + } + List others = groupToOtherReserves.getOrDefault(groupId, new ArrayList<>()); + List voList = others.stream().map(r -> { + EfcodeCpReserveVo rv = new EfcodeCpReserveVo(); + rv.setId(r.getId()); + rv.setPlanId(r.getPlanId()); + rv.setPlanGroupId(r.getPlanGroupId()); + rv.setReserveTime(r.getReserveTime()); + rv.setOpStatus(r.getOpStatus()); + rv.setOpOrder(r.getOpOrder()); + rv.setOpTime(r.getOpTime()); + rv.setOpMessage(r.getOpMessage()); + rv.setOpSetDate(r.getOpSetDate()); + rv.setOpSetTime(r.getOpSetTime()); + rv.setResFrom(r.getResFrom()); + rv.setSenderId(r.getSenderId()); + rv.setReceiverId(r.getReceiverId()); + // 填充对方计划详情 + EfcodeCpPlan otherPlan = otherPlanMap.get(r.getPlanId()); + if (otherPlan != null) { + rv.setBureauDeptName(otherPlan.getBureauDeptName()); + rv.setEnforceUserNames(otherPlan.getEnforceUserNames()); + rv.setEnforceUserTels(otherPlan.getEnforceUserTels()); + rv.setEnforceContent(otherPlan.getEnforceContent()); + rv.setPlanDate(otherPlan.getPlanDate()); + rv.setPlanTime(otherPlan.getPlanTime()); + rv.setEnterpriseName(otherPlan.getEnterpriseName()); + } + return rv; + }).collect(Collectors.toList()); + vo.setReserves(voList); + } + } + + private LambdaQueryWrapper buildQueryWrapper(EfcodeCpPlanBo bo) { + Map params = bo.getParams(); + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.orderByDesc(EfcodeCpPlan::getCreateTime); + lqw.eq(bo.getCreateBy() != null, EfcodeCpPlan::getCreateBy, bo.getCreateBy()); + lqw.eq(bo.getPlanType() != null, EfcodeCpPlan::getPlanType, bo.getPlanType()); + lqw.eq(bo.getPlanDate() != null, EfcodeCpPlan::getPlanDate, bo.getPlanDate()); + lqw.eq(bo.getPlanTime() != null, EfcodeCpPlan::getPlanTime, bo.getPlanTime()); + lqw.like(StringUtils.isNotBlank(bo.getEnforceContent()), EfcodeCpPlan::getEnforceContent, bo.getEnforceContent()); + lqw.like(StringUtils.isNotBlank(bo.getEnterpriseName()), EfcodeCpPlan::getEnterpriseName, bo.getEnterpriseName()); + lqw.eq(StringUtils.isNotBlank(bo.getEnterpriseNumber()), EfcodeCpPlan::getEnterpriseNumber, bo.getEnterpriseNumber()); + lqw.eq(StringUtils.isNotBlank(bo.getEnterpriseId()), EfcodeCpPlan::getEnterpriseId, bo.getEnterpriseId()); + lqw.eq(StringUtils.isNotBlank(bo.getEnforceUserNames()), EfcodeCpPlan::getEnforceUserNames, bo.getEnforceUserNames()); + lqw.eq(StringUtils.isNotBlank(bo.getEnforceUserIds()), EfcodeCpPlan::getEnforceUserIds, bo.getEnforceUserIds()); + lqw.eq(StringUtils.isNotBlank(bo.getEnforceUserCodes()), EfcodeCpPlan::getEnforceUserCodes, bo.getEnforceUserCodes()); + 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()); + // 支持多值状态过滤(逗号分隔) + String reserveStatusList = params != null ? (String) params.get("reserveStatusList") : null; + if (StringUtils.isNotBlank(reserveStatusList)) { + List statusList = 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()); + lqw.like(StringUtils.isNotBlank(bo.getBureauDeptName()), EfcodeCpPlan::getBureauDeptName, bo.getBureauDeptName()); + lqw.eq(StringUtils.isNotBlank(bo.getOrganId()), EfcodeCpPlan::getOrganId, bo.getOrganId()); + lqw.like(StringUtils.isNotBlank(bo.getOrganName()), EfcodeCpPlan::getOrganName, bo.getOrganName()); + lqw.eq(StringUtils.isNotBlank(bo.getDeptId()), EfcodeCpPlan::getDeptId, bo.getDeptId()); + lqw.like(StringUtils.isNotBlank(bo.getDeptName()), EfcodeCpPlan::getDeptName, bo.getDeptName()); + lqw.eq(bo.getIsDelete() != null, EfcodeCpPlan::getIsDelete, bo.getIsDelete()); + lqw.eq(StringUtils.isNotBlank(bo.getMyRemark()), EfcodeCpPlan::getMyRemark, bo.getMyRemark()); + lqw.eq(StringUtils.isNotBlank(bo.getChannelNo()), EfcodeCpPlan::getChannelNo, bo.getChannelNo()); + lqw.eq(StringUtils.isNotBlank(bo.getOutSerialNo()), EfcodeCpPlan::getOutSerialNo, bo.getOutSerialNo()); + return lqw; + } + + /** + * 新增行政执法检查计划-计划 + * + * @param bo 行政执法检查计划-计划 + * @return 是否新增成功 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public Boolean insertByBo(EfcodeCpPlanBo bo) { + // 重复校验:同一人+同一日期+同一时段+同一企业 不可重复 + checkDuplicate(bo); + EfcodeCpPlan add = MapstructUtils.convert(bo, EfcodeCpPlan.class); + // 尝试根据企业信用代码填充联系人和电话 + fillContactInfo(add); + validEntityBeforeSave(add); + boolean flag = baseMapper.insert(add) > 0; + if (flag) { + bo.setId(add.getId()); + // 同步写入计划参与人员表 + syncPlanUsers(add.getId(), bo.getEnforceUserIds(), bo.getEnforceUserNames(), bo.getEnforceUserTels()); + } + // 新增后置处理 + afterInsert(bo, add); + return flag; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public Boolean insertPlanOnly(EfcodeCpPlanBo bo) { + EfcodeCpPlan add = MapstructUtils.convert(bo, EfcodeCpPlan.class); + fillContactInfo(add); + validEntityBeforeSave(add); + boolean flag = baseMapper.insert(add) > 0; + if (flag) { + bo.setId(add.getId()); + syncPlanUsers(add.getId(), bo.getEnforceUserIds(), bo.getEnforceUserNames(), bo.getEnforceUserTels()); + } + return flag; + } + + /** + * 修改行政执法检查计划-计划 + * + * @param bo 行政执法检查计划-计划 + * @return 是否修改成功 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public Boolean updateByBo(EfcodeCpPlanBo bo) { + // 重复校验:同一人+同一日期+同一时段+同一企业 不可重复 + checkDuplicate(bo); + EfcodeCpPlan update = MapstructUtils.convert(bo, EfcodeCpPlan.class); + // 尝试根据企业信用代码填充联系人和电话 + fillContactInfo(update); + validEntityBeforeSave(update); + boolean flag = baseMapper.updateById(update) > 0; + if (flag) { + // 重建计划参与人员关联(先删后插,保证与表头字段一致) + syncPlanUsers(bo.getId(), bo.getEnforceUserIds(), bo.getEnforceUserNames(), bo.getEnforceUserTels()); + } + // 修改后置处理 + afterUpdate(bo, update); + return flag; + } + + /** + * 新增后置处理(预留扩展点) + * + * @param bo 业务对象 + * @param entity 已持久化的实体 + */ + private void afterInsert(EfcodeCpPlanBo bo, EfcodeCpPlan entity) { + // 条件1:不联合的计划,直接生成任务 + if (entity.getIsUnion() != null && entity.getIsUnion() == 0L) { + log.info("[计划提交] 不联合计划,直接生成任务, planId={}", entity.getId()); + cpTaskService.forceGenerateTask(entity); + return; + } + // 条件2:执行时间距今天 <= CP_SETTING_MANUAL_MATCH_DAYS,直接生成任务 + if (entity.getPlanDate() != null) { + int manualMatchDays = cpSettingService.getManualMatchDays(); + LocalDate planLocalDate = entity.getPlanDate().toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); + long daysUntilPlan = ChronoUnit.DAYS.between(LocalDate.now(), planLocalDate); + if (daysUntilPlan <= manualMatchDays) { + log.info("[计划提交] 执行时间距今{}<={}(CP_SETTING_MANUAL_MATCH_DAYS),直接生成任务, planId={}", + daysUntilPlan, manualMatchDays, entity.getId()); + cpTaskService.forceGenerateTask(entity); + } + } + } + + /** + * 修改后置处理(预留扩展点) + * + * @param bo 业务对象 + * @param entity 已持久化的实体 + */ + private void afterUpdate(EfcodeCpPlanBo bo, EfcodeCpPlan entity) { + // TODO 后续扩展:如发送通知、记录日志等 + } + + /** + * 同步计划参与人员表 efcode_cp_plan_users + *

策略:先根据 plan_id 清除原有关联,再按 enforceUserIds / enforceUserNames / enforceUserTels 的逗号分隔顺序逐条写入。

+ */ + private void syncPlanUsers(String planId, String userIdsCsv, String userNamesCsv, String userTelsCsv) { + if (StringUtils.isBlank(planId)) { + return; + } + // 删除原有关联 + efcodeCpPlanUsersMapper.delete( + Wrappers.lambdaQuery().eq(EfcodeCpPlanUsers::getPlanId, planId) + ); + if (StringUtils.isBlank(userIdsCsv)) { + return; + } + String[] ids = userIdsCsv.split(","); + String[] names = StringUtils.isNotBlank(userNamesCsv) ? userNamesCsv.split(",") : new String[0]; + String[] tels = StringUtils.isNotBlank(userTelsCsv) ? userTelsCsv.split(",") : new String[0]; + List list = new ArrayList<>(ids.length); + for (int i = 0; i < ids.length; i++) { + String uid = ids[i] == null ? "" : ids[i].trim(); + if (StringUtils.isBlank(uid)) { + continue; + } + EfcodeCpPlanUsers u = new EfcodeCpPlanUsers(); + u.setPlanId(planId); + u.setUserId(uid); + u.setUserName(i < names.length && names[i] != null ? names[i].trim() : ""); + u.setUserTel(i < tels.length && tels[i] != null ? tels[i].trim() : ""); + list.add(u); + } + for (EfcodeCpPlanUsers u : list) { + efcodeCpPlanUsersMapper.insert(u); + } + } + + /** + * 重复计划校验:当前操作用户 + 同一计划类型 + 同一日期 + 同一执行时段 + 同一企业,只能存在一条记录。 + *

仅校验当前登录用户,不对其他参与人员做判重。

+ * + * @param bo 待保存的计划BO + * @throws ServiceException 如果存在重复记录则抛出 + */ + private void checkDuplicate(EfcodeCpPlanBo bo) { + if (bo.getPlanDate() == null || bo.getPlanTime() == null + || StringUtils.isBlank(bo.getEnterpriseId())) { + return; + } + // 获取当前操作用户ID + String currentUserId = String.valueOf(LoginHelper.getUserId()); + if (StringUtils.isBlank(currentUserId) || "null".equals(currentUserId)) { + return; + } + // 1) 查找同日期+同时段+同企业的已有计划(不限制计划类型) + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.eq(EfcodeCpPlan::getPlanDate, bo.getPlanDate()); + lqw.eq(EfcodeCpPlan::getPlanTime, bo.getPlanTime()); + lqw.eq(EfcodeCpPlan::getEnterpriseId, bo.getEnterpriseId()); + // 编辑时排除自身 + if (StringUtils.isNotBlank(bo.getId())) { + lqw.ne(EfcodeCpPlan::getId, bo.getId()); + } + lqw.select(EfcodeCpPlan::getId); + List existingPlans = baseMapper.selectList(lqw); + if (existingPlans.isEmpty()) { + return; + } + // 2) 检查这些计划中是否包含当前用户 + List existingPlanIds = existingPlans.stream() + .map(EfcodeCpPlan::getId).collect(Collectors.toList()); + long count = efcodeCpPlanUsersMapper.selectCount( + Wrappers.lambdaQuery() + .in(EfcodeCpPlanUsers::getPlanId, existingPlanIds) + .eq(EfcodeCpPlanUsers::getUserId, currentUserId) + ); + if (count > 0) { + throw new ServiceException("您在该日期该时段对该企业已有执法计划,不可重复提交"); + } + } + + /** + * 根据企业统一社会信用代码(enterpriseNumber)尝试填充联系人和电话 + *

优先查 enterprise_information 表,找不到则查 sszt_jbxx110497_new 表

+ */ + private void fillContactInfo(EfcodeCpPlan entity) { + String enterpriseNumber = entity.getEnterpriseNumber(); + if (StringUtils.isBlank(enterpriseNumber)) { + return; + } + try { + // 1. 先查 enterprise_information 表 + EnterpriseInformationVo entInfo = enterpriseInformationService.queryByEnterpriseNumber(enterpriseNumber); + if (entInfo != null) { + if (StringUtils.isNotBlank(entInfo.getLegalPerson())) { + entity.setContactPerson(entInfo.getLegalPerson()); + } + if (StringUtils.isNotBlank(entInfo.getEnterpriseTel())) { + entity.setContactPhone(entInfo.getEnterpriseTel()); + } + return; + } + // 2. enterprise_information 未找到,查 sszt_jbxx110497_new 表 + List ssztList = ssztJbxx110497NewService.queryByTyshxydm(enterpriseNumber); + if (ssztList != null && !ssztList.isEmpty()) { + SsztJbxx110497NewVo sszt = ssztList.get(0); + if (StringUtils.isNotBlank(sszt.getFddbr())) { + entity.setContactPerson(sszt.getFddbr()); + } + // sszt_jbxx110497_new 表无电话字段,contactPhone 留空 + } + } catch (Exception e) { + log.warn("[计划] 填充企业联系人信息失败, enterpriseNumber={}, error={}", enterpriseNumber, e.getMessage()); + } + } + + /** + * 保存前的数据校验 + */ + private void validEntityBeforeSave(EfcodeCpPlan entity){ + //TODO 做一些数据校验,如唯一约束 + } + + /** + * 校验并批量删除行政执法检查计划-计划信息 + * + * @param ids 待删除的主键集合 + * @param isValid 是否进行有效性校验 + * @return 是否删除成功 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public Boolean deleteWithValidByIds(Collection ids, Boolean isValid) { + if (ids == null || ids.isEmpty()) { + return false; + } + // 查询待删除的计划记录 + List plans = baseMapper.selectByIds(ids); + if (isValid) { + LocalDate today = LocalDate.now(); + for (EfcodeCpPlan plan : plans) { + Long status = plan.getReserveStatus(); + boolean canEdit = status != null && status <= 1L; + boolean canDelete = canEdit; + // 不联合执法且执行时间未到,也允许删除 + if (!canDelete && plan.getIsUnion() != null && plan.getIsUnion() == 0L && plan.getPlanDate() != null) { + LocalDate planLocalDate = plan.getPlanDate().toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); + if (planLocalDate.isAfter(today)) { + canDelete = true; + } + } + if (!canDelete) { + throw new ServiceException("计划[" + + (plan.getEnterpriseName() != null ? plan.getEnterpriseName() : plan.getId()) + + "]当前状态不允许删除"); + } + } + } + // 级联删除关联的任务记录 + List taskIds = plans.stream() + .map(EfcodeCpPlan::getTaskId) + .filter(StringUtils::isNotBlank) + .distinct() + .collect(Collectors.toList()); + if (!taskIds.isEmpty()) { + log.info("[删除计划] 级联删除任务记录, taskIds={}", taskIds); + cpTaskService.deleteWithValidByIds(taskIds, false); + } + // 级联删除计划参与人员表关联记录 + efcodeCpPlanUsersMapper.delete( + Wrappers.lambdaQuery().in(EfcodeCpPlanUsers::getPlanId, ids) + ); + return baseMapper.deleteByIds(ids) > 0; + } + + /** + * 计划自动匹配预约(定时任务调用) + *

扫描待预约状态的计划,按规则自动匹配并生成预约记录

+ * + * @param forceMatch 是否强制匹配(忽略 CP_SETTING_AUTO_MATCH_START 日期校验) + * @param updatePlan 是否更新计划记录(system_reserved、reserve_status) + * @param insertReserve 是否将预约记录插入数据库;为false时仅计算匹配结果不落库 + * @return 按 planGroupId 分组的预约记录列表 + */ +// @Transactional(rollbackFor = Exception.class) +// @Override +// 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) { +// int autoMatchStart = cpSettingService.getAutoMatchStart(); +// int today = LocalDate.now().getDayOfMonth(); +// if (today != autoMatchStart) { +// log.info("[CpPlan] 当前日期{}号,不等于设定的自动匹配日期{}号,跳过执行", today, autoMatchStart); +// return resultMap; +// } +// } +// +// // 2. 获取最大联合单位数量 +// int maxAutoMatchUnionUnit = cpSettingService.getMaxAutoMatchUnionUnit(); +// +// // 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 processedKeys = new HashSet<>(); +// int matchedCount = 0; +// +// for (EfcodeCpPlan plan : pendingPlans) { +// String enterpriseNumber = plan.getEnterpriseNumber(); +// if (StringUtils.isBlank(enterpriseNumber)) { +// continue; +// } +// +// // 5. 查找计划所在自然周(周一~周日)内同企业、不同执法单位的其他待匹配计划 +// LocalDate planLocalDate = plan.getPlanDate().toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); +// LocalDate weekStartDate = planLocalDate.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)); +// LocalDate weekEndDate = planLocalDate.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY)); +// +// // 按“企业+周起始日”去重,同企业同周只处理一次 +// String processKey = enterpriseNumber + "_" + weekStartDate; +// if (processedKeys.contains(processKey)) { +// continue; +// } +// Date weekStart = Date.from(weekStartDate.atStartOfDay(ZoneId.systemDefault()).toInstant()); +// Date weekEnd = Date.from(weekEndDate.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() >= maxAutoMatchUnionUnit) { +// 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<>(); +// +// // 拼接参与单位名称,用于 opMessage +// String unitNames = distinctUnitPlans.stream() +// .map(EfcodeCpPlan::getBureauDeptName) +// .filter(StringUtils::isNotBlank) +// .collect(Collectors.joining(",")); +// String opMessage = "由于计划执法时间安排较近,系统推荐进行联合,参与单位为:" + unitNames + ".请确认."; +// +// 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(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); +// } +// +// // 9. 更新计划: 将同企业一周内所有待匹配计划标记为已系统匹配,防止重复匹配 +// if (updatePlan) { +// for (EfcodeCpPlan sp : samePlans) { +// EfcodeCpPlan planUpdate = new EfcodeCpPlan(); +// planUpdate.setId(sp.getId()); +// planUpdate.setSystemReserved(1L); +// baseMapper.updateById(planUpdate); +// } +// // 被选中参与联合的计划额外更新 reserve_status=2(预约中) +// for (EfcodeCpPlan dp : distinctUnitPlans) { +// EfcodeCpPlan planUpdate = new EfcodeCpPlan(); +// planUpdate.setId(dp.getId()); +// planUpdate.setReserveStatus(2L); +// baseMapper.updateById(planUpdate); +// } +// } +// +// resultMap.put(planGroupId, groupReserves); +// +// // 标记该企业已处理 +// processedKeys.add(enterpriseNumber); +// matchedCount++; +// log.info("[CpPlan] 企业[{}]匹配成功,联合{}个单位,planGroupId={}", +// enterpriseNumber, distinctUnitPlans.size(), planGroupId); +// } +// +// log.info("[CpPlan] 计划自动匹配预约执行完毕,共匹配{}组联合预约", matchedCount); +// return resultMap; +// } + + @Transactional(rollbackFor = Exception.class) + @Override + public Map> autoMatchReserveV3(boolean forceMatch, boolean updatePlan, boolean insertReserve) { + log.info("[CpPlan-V3] 开始执行计划自动匹配预约, 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-V3] 当前日期{}号,不等于设定的自动匹配日期{}号,跳过执行", today, autoMatchStart); + return resultMap; + } + } + + // 2. 获取最大联合单位数量 + int maxUnionUnit = cpSettingService.getMaxAutoMatchUnionUnit(); + + // 2.1 获取系统匹配预约周数范围 + int systemMatchWeeks = cpSettingService.getSystemMatchWeeks(); + log.info("[CpPlan-V3] 系统匹配周数范围={}", 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.in(EfcodeCpPlan::getReserveStatus, 1L, 4L, 5L); + lqw.eq(EfcodeCpPlan::getIsPublic, 1L); + lqw.eq(EfcodeCpPlan::getIsUnion, 1L); + lqw.eq(EfcodeCpPlan::getIsDelete, 0L); + lqw.eq(EfcodeCpPlan::getChannelNo, ChannelNoEnums.SHENZHEN.getKey()); + lqw.isNotNull(EfcodeCpPlan::getCreateBy); + lqw.orderByAsc(EfcodeCpPlan::getPlanDate); + List pendingPlans = baseMapper.selectList(lqw); + + if (pendingPlans.isEmpty()) { + log.info("[CpPlan-V3] 没有找到待匹配的计划记录,执行结束"); + return resultMap; + } + + log.info("[CpPlan-V3] 共查询到{}条待匹配计划", pendingPlans.size()); + + // 预查询所有待匹配计划的手动联合记录,构建互斥关系映射(已手动联合的计划对不应再被放入同一系统组) + Set pendingPlanIdSet = pendingPlans.stream() + .map(EfcodeCpPlan::getId) + .collect(Collectors.toSet()); + List allManualReserves = efcodeCpReserveMapper.selectList( + Wrappers.lambdaQuery() + .in(EfcodeCpReserve::getPlanId, pendingPlanIdSet) + .eq(EfcodeCpReserve::getResFrom, 0L) + ); + // 按planGroupId分组,找出同组内的计划(即已手动联合过的计划对) + Map> manualGroupToPlanIds = allManualReserves.stream() + .filter(r -> StringUtils.isNotBlank(r.getPlanGroupId())) + .collect(Collectors.groupingBy( + EfcodeCpReserve::getPlanGroupId, + Collectors.mapping(EfcodeCpReserve::getPlanId, Collectors.toSet()) + )); + // 构建每个计划的手动联合伙伴映射: planId -> 已与其手动联合过的其他planId集合 + Map> manualUnionPeers = new HashMap<>(); + for (Set groupPlanIds : manualGroupToPlanIds.values()) { + if (groupPlanIds.size() < 2) continue; + for (String pid : groupPlanIds) { + manualUnionPeers.computeIfAbsent(pid, k -> new HashSet<>()).addAll(groupPlanIds); + } + } + manualUnionPeers.forEach((pid, peers) -> peers.remove(pid)); + log.info("[CpPlan-V3] 存在手动联合互斥关系的计划数量: {}", manualUnionPeers.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-V3] 开始处理计划记录,计划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-V3] 计划ID={}的时间范围是:{} ~ {}", plan.getId(), weekStartDate, weekEndDate); + + // 6. 获取当前计划的手动联合伙伴(与当前计划已手动联合过的计划ID) + Set currentPlanPeers = manualUnionPeers.getOrDefault(plan.getId(), Collections.emptySet()); + + // 7. 从待匹配计划中筛选:同企业 + 时间范围内 + 不是当前计划创建者发布的 + 未被处理过 + 未与当前计划手动联合过 + 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; + } + // 排除与当前计划已手动联合过的计划 + if (currentPlanPeers.contains(p.getId())) { + return false; + } + // 在时间范围内 + LocalDate pDate = p.getPlanDate().toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); + return !pDate.isBefore(weekStartDate) && !pDate.isAfter(weekEndDate); + }) + .collect(Collectors.toList()); + + log.info("[CpPlan-V3] 计划ID={}, 在时间范围内找到{}条非本人发布的同企业候选计划", plan.getId(), matchedPlans.size()); + + // 8. 从matchedPlans中选取计划,确保同组内不含已手动联合过的计划对 + int limit = maxUnionUnit - 1; + List selectedPlans = new ArrayList<>(); + for (EfcodeCpPlan candidate : matchedPlans) { + if (selectedPlans.size() >= limit) break; + // 检查候选计划与已选中计划之间是否存在手动联合互斥关系 + Set candidatePeers = manualUnionPeers.getOrDefault(candidate.getId(), Collections.emptySet()); + boolean hasConflict = selectedPlans.stream() + .anyMatch(sp -> candidatePeers.contains(sp.getId())); + if (!hasConflict) { + selectedPlans.add(candidate); + } + } + + log.info("[CpPlan-V3] 计划ID={}, 最终选取{}条计划参与联合(最大允许{}条)", plan.getId(), selectedPlans.size(), limit); + + // 8. 如果没有可联合的计划,跳过 + if (selectedPlans.isEmpty()) { + log.debug("[CpPlan-V3] 计划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-V3] 计划ID={} 匹配成功,联合{}个单位,planGroupId={}", + plan.getId(), groupPlans.size(), planGroupId); + } + + log.info("[CpPlan-V3] 计划自动匹配预约执行完毕,共匹配{}组联合预约,共生成{}条预约记录", matchedCount, unionReserveList.size()); + + return resultMap; + } + + /** + * 获取指定计划之前通过手动匹配已联合过的其他计划ID集合 + * + * @param planId 当前计划ID + * @return 已手动联合过的计划ID集合,无则返回空集合 + */ + private Set getManualUnionPlanIds(String planId) { + // 查询当前plan在efcode_cp_reserve中手动匹配(res_from=0)的记录 + List manualReserves = efcodeCpReserveMapper.selectList( + Wrappers.lambdaQuery() + .eq(EfcodeCpReserve::getPlanId, planId) + .eq(EfcodeCpReserve::getResFrom, 0L) + ); + if (manualReserves.isEmpty()) { + return Collections.emptySet(); + } + Set manualGroupIds = manualReserves.stream() + .map(EfcodeCpReserve::getPlanGroupId) + .filter(StringUtils::isNotBlank) + .collect(Collectors.toSet()); + if (manualGroupIds.isEmpty()) { + return Collections.emptySet(); + } + // 查询同组中其他计划的planId + List groupedReserves = efcodeCpReserveMapper.selectList( + Wrappers.lambdaQuery() + .in(EfcodeCpReserve::getPlanGroupId, manualGroupIds) + .ne(EfcodeCpReserve::getPlanId, planId) + ); + return groupedReserves.stream() + .map(EfcodeCpReserve::getPlanId) + .collect(Collectors.toSet()); + } + + /** + * 计划自动匹配预约V2(定时任务调用) + *

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

+ * + * @param forceMatch 是否强制匹配(忽略 CP_SETTING_AUTO_MATCH_START 日期校验) + * @param updatePlan 是否更新计划记录(system_reserved、reserve_status) + * @param insertReserve 是否将预约记录插入数据库;为false时仅计算匹配结果不落库 + * @return 按 planGroupId 分组的预约记录列表 + */ +// @Transactional(rollbackFor = Exception.class) +// @Override +// public Map> autoMatchReserveV2(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.orderByAsc(EfcodeCpPlan::getPlanDate); +// List pendingPlans = baseMapper.selectList(lqw); +// +// if (pendingPlans.isEmpty()) { +// log.info("[CpPlan-V2] 没有找到待匹配的计划记录,执行结束"); +// return resultMap; +// } +// log.info("[CpPlan-V2] 共查询到{}条待匹配计划", pendingPlans.size()); +// +// // 4. 已处理过的"企业+周"组合,同企业同周只处理一次 +// Set processedKeys = new HashSet<>(); +// int matchedCount = 0; +// +// for (EfcodeCpPlan plan : pendingPlans) { +// 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周后的周日 +// +// // 按"企业+周起始日"去重,同企业同周只处理一次 +// String processKey = enterpriseNumber + "_" + weekStartDate; +// if (processedKeys.contains(processKey)) { +// continue; +// } +// processedKeys.add(processKey); +// +// // 6. 查找该自然周内同企业的所有待匹配计划 +// Date weekStart = Date.from(weekStartDate.atStartOfDay(ZoneId.systemDefault()).toInstant()); +// Date weekEnd = Date.from(weekEndDate.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::getManualReserved, 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); +// +// // 7. 按 bureau_dept_id 去重,每个单位取一条代表计划 +// List distinctUnitPlans = new ArrayList<>(); +// Set seenBureauDeptIds = new HashSet<>(); +// for (EfcodeCpPlan sp : samePlans) { +// String deptId = sp.getBureauDeptId(); +// if (StringUtils.isNotBlank(deptId) && !seenBureauDeptIds.contains(deptId)) { +// seenBureauDeptIds.add(deptId); +// distinctUnitPlans.add(sp); +// } +// } +// +// // 8. 只有1个单位的计划,无法联合,跳过 +// if (distinctUnitPlans.size() <= 1) { +// log.debug("[CpPlan-V2] 企业[{}] 周[{}~{}]内只有1个单位的计划,跳过", enterpriseNumber, weekStartDate, weekEndDate); +// continue; +// } +// +// // 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); +// if (group.size() >= 2) { +// groups.add(new ArrayList<>(group)); +// } +// } +// +// if (groups.isEmpty()) { +// continue; +// } +// +// // 10. 为每组生成预约记录 +// Set matchedPlanIds = new HashSet<>(); +// for (List group : groups) { +// String planGroupId = "Temp" + "_" + UUID.randomUUID().toString().replace("-", ""); +// Date reserveTime = new Date(); +// List groupReserves = new ArrayList<>(); +// +// String unitNames = group.stream() +// .map(EfcodeCpPlan::getBureauDeptName) +// .filter(StringUtils::isNotBlank) +// .collect(Collectors.joining(",")); +// String opMessage = "由于计划执法时间安排较近,系统推荐进行联合,参与单位为:" + unitNames + ".请确认."; +// +// for (EfcodeCpPlan matchedPlan : group) { +// 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); +// } +// +// // 更新被选中参与联合的计划: reserve_status=2(预约中), system_reserved=1 +// if (updatePlan) { +// for (EfcodeCpPlan dp : group) { +// EfcodeCpPlan planUpdate = new EfcodeCpPlan(); +// planUpdate.setId(dp.getId()); +// planUpdate.setReserveStatus(2L); +// planUpdate.setSystemReserved(1L); +// baseMapper.updateById(planUpdate); +// matchedPlanIds.add(dp.getId()); +// } +// } +// +// resultMap.put(planGroupId, groupReserves); +// matchedCount++; +// log.info("[CpPlan-V2] 企业[{}] 周[{}~{}] 匹配成功,联合{}个单位,planGroupId={}", +// enterpriseNumber, weekStartDate, weekEndDate, group.size(), planGroupId); +// } +// +// // 11. 将已成功匹配的单位在该企业该周内的所有计划标记为 system_reserved=1,防止重复匹配 +// if (updatePlan && !groups.isEmpty()) { +// for (EfcodeCpPlan sp : samePlans) { +// if (matchedPlanIds.contains(sp.getId())) { +// continue; +// } +// if (seenBureauDeptIds.contains(sp.getBureauDeptId())) { +// EfcodeCpPlan planUpdate = new EfcodeCpPlan(); +// planUpdate.setId(sp.getId()); +// planUpdate.setSystemReserved(1L); +// baseMapper.updateById(planUpdate); +// } +// } +// } +// } +// +// log.info("[CpPlan-V2] 计划自动匹配预约执行完毕,共匹配{}组联合预约", matchedCount); +// return resultMap; +// } + + /** + * 计划到期自动生成任务 + *

距计划执行日期 <= CP_SETTING_MANUAL_MATCH_DAYS 天时, + * 系统自动取消剩余未处理的预约(含系统匹配与手动预约),并为计划生成任务。

+ *
    + *
  • 部分联合(有已同意的预约):取消剩余待确认预约,已同意的计划共享生成任务
  • + *
  • 完全没有联合记录:直接为计划生成任务
  • + *
  • 全部待确认(无人同意):取消全部待确认预约,为每个计划各自生成任务
  • + *
+ */ + @Transactional(rollbackFor = Exception.class) + @Override + public int processPlanDeadlineAutoGenTask() { + return processPlanDeadlineAutoGenTask(false); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public int processPlanDeadlineAutoGenTask(boolean ignoreTime) { + int manualMatchDays = cpSettingService.getManualMatchDays(); + LocalDate today = LocalDate.now(); + LocalDate deadlineDate = today.plusDays(manualMatchDays); + + // 1. 查找即将到期的计划:plan_date <= today + manualMatchDays,且未生成任务 + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + if (!ignoreTime) { + lqw.le(EfcodeCpPlan::getPlanDate, java.sql.Date.valueOf(deadlineDate)); + lqw.ge(EfcodeCpPlan::getPlanDate, java.sql.Date.valueOf(today)); + } + lqw.eq(EfcodeCpPlan::getIsDelete, 0L); + lqw.and(w -> w.isNull(EfcodeCpPlan::getTaskId).or().eq(EfcodeCpPlan::getTaskId, "")); + lqw.in(EfcodeCpPlan::getReserveStatus, Arrays.asList(1L, 2L, 4L, 5L, 6L)); + List plans = baseMapper.selectList(lqw); + + if (plans.isEmpty()) { + log.info("[processPlanDeadlineAutoGenTask] 无即将到期的计划需要处理"); + return 0; + } + log.info("[processPlanDeadlineAutoGenTask] 找到{}条即将到期的计划(截止日期={})", plans.size(), deadlineDate); + + // 2. 查找这些计划的预约记录 + List planIds = plans.stream().map(EfcodeCpPlan::getId).collect(Collectors.toList()); + List reserves = efcodeCpReserveMapper.selectList( + Wrappers.lambdaQuery().in(EfcodeCpReserve::getPlanId, planIds) + ); + + // 建立 planId → reserves 映射 + Map> planReserveMap = reserves.stream() + .collect(Collectors.groupingBy(EfcodeCpReserve::getPlanId)); + + // 记录已处理的 plan_group_id,避免重复处理同一分组 + Set processedGroups = new HashSet<>(); + int processedCount = 0; + + for (EfcodeCpPlan plan : plans) { + // 跳过已经在分组处理中被赋予了 taskId 的计划 + if (StringUtils.isNotBlank(plan.getTaskId())) { + continue; + } + + List planReserves = planReserveMap.get(plan.getId()); + + if (planReserves == null || planReserves.isEmpty()) { + // Case B: 完全没有联合记录 → 直接生成任务 + log.info("[processPlanDeadlineAutoGenTask] 计划[{}]无联合记录,直接生成任务, 企业={}", + plan.getId(), plan.getEnterpriseName()); + cpTaskService.forceGenerateTask(plan); + processedCount++; + } else { + // Case A: 有联合记录 → 按 plan_group_id 处理 + for (EfcodeCpReserve reserve : planReserves) { + String groupId = reserve.getPlanGroupId(); + if (StringUtils.isBlank(groupId) || processedGroups.contains(groupId)) { + continue; + } + processedGroups.add(groupId); + processedCount += processDeadlineGroup(groupId); + } + } + } + + log.info("[processPlanDeadlineAutoGenTask] 处理完毕,共处理{}条计划", processedCount); + return processedCount; + } + + /** + * 处理一个预约分组的截止逻辑: + * 1. 取消所有待确认(opStatus=3)的预约 → 设为超期取消(4) + * 2. 为已同意(opStatus=1)的计划生成共享任务 + * 3. 为被取消的计划各自生成独立任务 + * + * @param groupId 预约分组ID + * @return 处理的计划数量 + */ + private int processDeadlineGroup(String groupId) { + // 查询分组内所有预约记录 + List groupReserves = efcodeCpReserveMapper.selectList( + Wrappers.lambdaQuery().eq(EfcodeCpReserve::getPlanGroupId, groupId) + ); + if (groupReserves.isEmpty()) { + return 0; + } + + // 1. 取消所有待确认的预约 (opStatus=3 → 4) + List pendingReserves = groupReserves.stream() + .filter(r -> Long.valueOf(3L).equals(r.getOpStatus())) + .collect(Collectors.toList()); + for (EfcodeCpReserve pending : pendingReserves) { + EfcodeCpReserve update = new EfcodeCpReserve(); + update.setId(pending.getId()); + update.setOpStatus(4L); // 超期取消 + update.setOpTime(new Date()); + efcodeCpReserveMapper.updateById(update); + log.info("[processDeadlineGroup] 取消待确认预约: reserveId={}, planId={}, groupId={}", + pending.getId(), pending.getPlanId(), groupId); + } + + // 2. 找出已同意的预约 (opStatus=1) + List approvedReserves = groupReserves.stream() + .filter(r -> Long.valueOf(1L).equals(r.getOpStatus())) + .collect(Collectors.toList()); + + int processedCount = 0; + + if (!approvedReserves.isEmpty()) { + // 有部分或全部同意 → 为同意的计划生成共享任务 + String sharedTaskId = null; + for (EfcodeCpReserve approved : approvedReserves) { + EfcodeCpPlan approvedPlan = baseMapper.selectById(approved.getPlanId()); + if (approvedPlan == null || StringUtils.isNotBlank(approvedPlan.getTaskId())) { + continue; + } + if (sharedTaskId == null) { + sharedTaskId = cpTaskService.forceGenerateTask(approvedPlan); + log.info("[processDeadlineGroup] 已同意计划[{}]生成共享任务taskId={}", + approvedPlan.getId(), sharedTaskId); + } else { + EfcodeCpPlan planUpdate = new EfcodeCpPlan(); + planUpdate.setId(approvedPlan.getId()); + planUpdate.setTaskId(sharedTaskId); + planUpdate.setReserveStatus(3L); + baseMapper.updateById(planUpdate); + log.info("[processDeadlineGroup] 已同意计划[{}]加入共享任务taskId={}", + approvedPlan.getId(), sharedTaskId); + } + processedCount++; + } + } + + // 3. 为被取消的计划各自生成独立任务 + for (EfcodeCpReserve pending : pendingReserves) { + EfcodeCpPlan pendingPlan = baseMapper.selectById(pending.getPlanId()); + if (pendingPlan == null || StringUtils.isNotBlank(pendingPlan.getTaskId())) { + continue; + } + cpTaskService.forceGenerateTask(pendingPlan); + log.info("[processDeadlineGroup] 被取消计划[{}]生成独立任务, 企业={}", + pendingPlan.getId(), pendingPlan.getEnterpriseName()); + processedCount++; + } + + return processedCount; + } + + /** + * 取消超出自动匹配时间的联合执法计划 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public int updateIsUnion() { + log.info("[updateIsUnion] 开始执行:取消超出自动匹配时间的联合执法计划"); + + // 1. 获取系统匹配预约周数范围 + int systemMatchWeeks = cpSettingService.getSystemMatchWeeks(); + log.info("[updateIsUnion] 系统匹配周数范围={}", systemMatchWeeks); + + // 2. 查询所有联合执法状态的计划(is_union = 1) + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.eq(EfcodeCpPlan::getIsUnion, 1L); + List unionPlans = baseMapper.selectList(lqw); + + if (unionPlans.isEmpty()) { + log.info("[updateIsUnion] 无联合执法计划需要处理"); + return 0; + } + log.info("[updateIsUnion] 找到{}条联合执法计划", unionPlans.size()); + + LocalDate today = LocalDate.now(); + int cancelledCount = 0; + + // 3. 遍历每条计划,判断是否超出自动匹配期限 + for (EfcodeCpPlan plan : unionPlans) { + if (plan.getPlanDate() == null) { + log.warn("[updateIsUnion] 计划[{}]的planDate为空,跳过", plan.getId()); + continue; + } + + // 计算自动匹配截止日期:计划所在自然周的周一 + systemMatchWeeks 周 + LocalDate planLocalDate = plan.getPlanDate().toInstant() + .atZone(ZoneId.systemDefault()).toLocalDate(); + LocalDate weekStartDate = planLocalDate.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)); + LocalDate autoMatchDeadline = weekStartDate.plusWeeks(systemMatchWeeks); + + // 如果截止日期已过,则取消联合 + if (autoMatchDeadline.isBefore(today)) { + EfcodeCpPlan updatePlan = new EfcodeCpPlan(); + updatePlan.setId(plan.getId()); + updatePlan.setIsUnion(0L); + baseMapper.updateById(updatePlan); + log.info("[updateIsUnion] 计划[{}]自动匹配期限已过(截止日期={}),取消联合(isUnion=0), 企业={}", + plan.getId(), autoMatchDeadline, plan.getEnterpriseName()); + cancelledCount++; + } + } + + log.info("[updateIsUnion] 执行完成:共取消{}条联合执法计划", cancelledCount); + return cancelledCount; + } +} diff --git a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/OutEfcodeCpTaskServiceImpl.java b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/OutEfcodeCpTaskServiceImpl.java new file mode 100644 index 00000000..8b3ac2be --- /dev/null +++ b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/OutEfcodeCpTaskServiceImpl.java @@ -0,0 +1,328 @@ +package com.zdxt.enforcementcode.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.zdxt.enforcementcode.common.commonenum.PlatformTypeEnums; +import com.zdxt.enforcementcode.domain.EfcodeCpPlan; +import com.zdxt.enforcementcode.domain.EfcodeCpTask; +import com.zdxt.enforcementcode.domain.bo.EfcodeCpTaskBo; +import com.zdxt.enforcementcode.domain.vo.EfcodeCpTaskVo; +import com.zdxt.enforcementcode.mapper.EfcodeCpPlanMapper; +import com.zdxt.enforcementcode.mapper.EfcodeCpTaskMapper; +import com.zdxt.enforcementcode.service.IEfcodeCpTaskService; +import com.zdxt.enforcementcode.service.IOutEfcodeCpTaskService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.dromara.common.core.utils.MapstructUtils; +import org.dromara.common.core.utils.StringUtils; +import org.dromara.common.mybatis.core.page.PageQuery; +import org.dromara.common.mybatis.core.page.TableDataInfo; +import org.dromara.common.satoken.utils.LoginHelper; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 行政执法检查计划-任务Service业务层处理 + * + * @author Lion Li + * @date 2026-05-17 + */ +@Slf4j +@RequiredArgsConstructor +@Service +public class OutEfcodeCpTaskServiceImpl implements IOutEfcodeCpTaskService { + + private final EfcodeCpTaskMapper baseMapper; + private final EfcodeCpPlanMapper cpPlanMapper; + + /** + * 查询行政执法检查计划-任务 + * + * @param id 主键 + * @return 行政执法检查计划-任务 + */ + @Override + public EfcodeCpTaskVo queryById(String id){ + return baseMapper.selectVoById(id); + } + + /** + * 分页查询行政执法检查计划-任务列表 + * + * @param bo 查询条件 + * @param pageQuery 分页参数 + * @return 行政执法检查计划-任务分页列表 + */ + @Override + public TableDataInfo queryPageList(EfcodeCpTaskBo bo, PageQuery pageQuery) { + LambdaQueryWrapper lqw = buildQueryWrapper(bo); + Page result = baseMapper.selectVoPage(pageQuery.build(), lqw); + return TableDataInfo.build(result); + } + + /** + * 查询符合条件的行政执法检查计划-任务列表 + * + * @param bo 查询条件 + * @return 行政执法检查计划-任务列表 + */ + @Override + public List queryList(EfcodeCpTaskBo bo) { + LambdaQueryWrapper lqw = buildQueryWrapper(bo); + return baseMapper.selectVoList(lqw); + } + + private LambdaQueryWrapper buildQueryWrapper(EfcodeCpTaskBo bo) { + Map params = bo.getParams(); + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.orderByAsc(EfcodeCpTask::getId); + lqw.like(StringUtils.isNotBlank(bo.getTaskName()), EfcodeCpTask::getTaskName, bo.getTaskName()); + lqw.eq(StringUtils.isNotBlank(bo.getBureauDeptId()), EfcodeCpTask::getBureauDeptId, bo.getBureauDeptId()); + return lqw; + } + + /** + * 新增行政执法检查计划-任务 + * + * @param bo 行政执法检查计划-任务 + * @return 是否新增成功 + */ + @Override + public Boolean insertByBo(EfcodeCpTaskBo bo) { + EfcodeCpTask add = MapstructUtils.convert(bo, EfcodeCpTask.class); + validEntityBeforeSave(add); + boolean flag = baseMapper.insert(add) > 0; + if (flag) { + bo.setId(add.getId()); + } + return flag; + } + + /** + * 修改行政执法检查计划-任务 + * + * @param bo 行政执法检查计划-任务 + * @return 是否修改成功 + */ + @Override + public Boolean updateByBo(EfcodeCpTaskBo bo) { + EfcodeCpTask update = MapstructUtils.convert(bo, EfcodeCpTask.class); + validEntityBeforeSave(update); + return baseMapper.updateById(update) > 0; + } + + /** + * 修改任务已读状态 + * + * @param taskId 任务ID + * @return 是否修改成功 + */ + @Override + public Boolean updateIsRead(String taskId) { + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate(); + wrapper.eq(EfcodeCpTask::getId, taskId) + .set(EfcodeCpTask::getIsRead, 1) + .set(EfcodeCpTask::getUpdateBy, LoginHelper.getUserId()) + .set(EfcodeCpTask::getUpdateTime, new Date()); + return baseMapper.update(null, wrapper) > 0; + } + + + /** + * 保存前的数据校验 + */ + private void validEntityBeforeSave(EfcodeCpTask entity){ + //TODO 做一些数据校验,如唯一约束 + } + + /** + * 验证是否符合生成任务的条件 + *

校验预约分组内所有记录是否满足生成执法任务的前置条件

+ * + * @param planGroupId 计划分组ID + * @return true=符合生成条件, false=不符合 + */ + public boolean checkCanGenerateTask(String planGroupId) { + // TODO 后续实现:校验分组内预约状态、时间窗口、人员配置等前置条件 + return true; + } + + /** + * 查询我的任务列表 + */ + @Override + public List queryMyTaskList(Long userId, Date planDate,String searchValue) { + return baseMapper.selectMyTaskList(userId, planDate,searchValue); + } + + /** + * 查询指定月份中有任务的日期列表 + */ + @Override + public List queryMyTaskDates(Long userId, String yearMonth) { + return baseMapper.selectMyTaskDates(userId, yearMonth); + } + + /** + * 查询我的任务详情 + */ + @Override + public EfcodeCpTaskVo queryMyTaskDetail(String planId) { + EfcodeCpTaskVo detail = baseMapper.selectMyTaskDetail(planId); + if (detail == null || StringUtils.isBlank(detail.getId())) { + return detail; + } + // 查询同一 task_id 下其他单位的计划(排除当前 planId) + List otherPlans = cpPlanMapper.selectList( + Wrappers.lambdaQuery() + .eq(EfcodeCpPlan::getTaskId, detail.getId()) + .ne(EfcodeCpPlan::getId, planId) + .eq(EfcodeCpPlan::getIsDelete, 0L) + ); + if (otherPlans != null && !otherPlans.isEmpty()) { + List associated = new ArrayList<>(); + for (EfcodeCpPlan p : otherPlans) { + EfcodeCpTaskVo.AssociatedPlan ap = new EfcodeCpTaskVo.AssociatedPlan(); + ap.setBureauDeptName(p.getBureauDeptName()); + ap.setEnforceUserNames(p.getEnforceUserNames()); + ap.setEnforceUserTels(p.getEnforceUserTels()); + ap.setEnforceContent(p.getEnforceContent()); + associated.add(ap); + } + detail.setAssociatedPlans(associated); + } + return detail; + } + + /** + * 批量查询关联计划信息(用于列表页第二次请求加载联合执法关联单位信息) + */ + @Override + public Map> queryAssociatedPlansBatch(List planIds) { + Map> result = new HashMap<>(); + if (planIds == null || planIds.isEmpty()) { + return result; + } + // 1. 查询这些planId对应的计划,获取各自的taskId + List sourcePlans = cpPlanMapper.selectList( + Wrappers.lambdaQuery() + .in(EfcodeCpPlan::getId, planIds) + .eq(EfcodeCpPlan::getIsDelete, 0L) + .isNotNull(EfcodeCpPlan::getTaskId) + ); + if (sourcePlans == null || sourcePlans.isEmpty()) { + return result; + } + // 2. 收集所有taskId,并建立 planId -> taskId 的映射 + Map planToTask = new HashMap<>(); + List taskIds = new ArrayList<>(); + for (EfcodeCpPlan p : sourcePlans) { + planToTask.put(p.getId(), p.getTaskId()); + if (!taskIds.contains(p.getTaskId())) { + taskIds.add(p.getTaskId()); + } + } + // 3. 查询所有相关taskId下的计划(排除当前用户的planId) + List allRelatedPlans = cpPlanMapper.selectList( + Wrappers.lambdaQuery() + .in(EfcodeCpPlan::getTaskId, taskIds) + .notIn(EfcodeCpPlan::getId, planIds) + .eq(EfcodeCpPlan::getIsDelete, 0L) + ); + if (allRelatedPlans == null || allRelatedPlans.isEmpty()) { + return result; + } + // 4. 按taskId分组 + Map> groupedByTask = allRelatedPlans.stream() + .collect(Collectors.groupingBy(EfcodeCpPlan::getTaskId)); + // 5. 映射回各planId + for (Map.Entry entry : planToTask.entrySet()) { + String planId = entry.getKey(); + String taskId = entry.getValue(); + List otherPlans = groupedByTask.get(taskId); + if (otherPlans != null && !otherPlans.isEmpty()) { + List associated = new ArrayList<>(); + for (EfcodeCpPlan p : otherPlans) { + EfcodeCpTaskVo.AssociatedPlan ap = new EfcodeCpTaskVo.AssociatedPlan(); + ap.setBureauDeptName(p.getBureauDeptName()); + ap.setEnforceUserNames(p.getEnforceUserNames()); + ap.setEnforceUserTels(p.getEnforceUserTels()); + ap.setEnforceContent(p.getEnforceContent()); + associated.add(ap); + } + result.put(planId, associated); + } + } + return result; + } + + /** + * 强制创建任务(忽略其他条件),根据计划信息直接生成任务记录并回写 plan.task_id + * + * @param plan 来源计划 + * @return 新建任务ID + */ + @Override + public String forceGenerateTask(EfcodeCpPlan plan) { + // 1. 创建任务记录 + EfcodeCpTask task = new EfcodeCpTask(); + task.setTaskName(plan.getEnterpriseName() + " - 执法检查任务"); + task.setBureauDeptId(plan.getBureauDeptId()); + task.setChannelNo(plan.getChannelNo()); + task.setOutSerialNo(plan.getOutSerialNo()); + baseMapper.insert(task); + log.info("[强制创建任务] 新建任务 taskId={}, planId={}, 企业={}", task.getId(), plan.getId(), plan.getEnterpriseName()); + + // 2. 回写计划的 task_id,同时将 reserve_status 设为 3(预约完成/形成任务) + EfcodeCpPlan planUpdate = new EfcodeCpPlan(); + planUpdate.setId(plan.getId()); + planUpdate.setTaskId(task.getId()); + planUpdate.setReserveStatus(3L); + cpPlanMapper.updateById(planUpdate); + + return task.getId(); + } + + + /** + * 校验并批量删除行政执法检查计划-任务信息 + * + * @param ids 待删除的主键集合 + * @param isValid 是否进行有效性校验 + * @return 是否删除成功 + */ + @Override + public Boolean deleteWithValidByIds(Collection ids, Boolean isValid) { + if(isValid){ + //TODO 做一些业务上的校验,判断是否需要校验 + } + return baseMapper.deleteByIds(ids) > 0; + } + + /** + * 统计当前用户未读任务数量 + */ + + @Override + public Long countUnreadTasksForCurrentUser() { + Integer platformType = LoginHelper.getPlatformType(); + Long userId = LoginHelper.getUserId(); + // 默认来源类型:执法任务 + final Integer sourceType = 4; + // 执法端:根据用户ID统计 + if (PlatformTypeEnums.ENFORCEMENT.getKey().equals(platformType)) { + if (userId == null) { + log.warn("当前用户未登录,无法统计未读执法任务消息"); + return 0L; + } + return baseMapper.countTasksForCurrentUser(userId,sourceType); + } + return 0L; + } + + +} 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 f45de5f2..60a0c932 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 @@ -20,7 +20,7 @@ reserve_status in (1,4,5) AND is_delete = 0 - AND channel_no = '0' + AND channel_no = 'shenzhen' AND create_by != #{currentUserId} @@ -74,7 +74,7 @@ cp.is_delete = 0 and cp.reserve_status!=2 - AND cp.channel_no = '0' + AND cp.channel_no = 'shenzhen' AND cp.create_by != #{currentUserId}