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}