Browse Source

执法端-计划

dev-V1.0.1
庄锐波 5 months ago
parent
commit
a5251cf4ee
  1. 19
      doc/庄锐波-工作周报.md
  2. 16
      zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/controller/app/enforcement/AppEnfCpReserveController.java
  3. 11
      zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/IEfcodeCpPlanService.java
  4. 9
      zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/IEfcodeCpReserveService.java
  5. 246
      zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/EfcodeCpPlanServiceImpl.java
  6. 63
      zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/EfcodeCpReserveServiceImpl.java
  7. 3
      zdxt-web-client/zdxt-efcode-enforcement-app/src/api/enforcement/reserve.js
  8. 99
      zdxt-web-client/zdxt-efcode-enforcement-app/src/views/enforcement/my/reserve/cp_reserve_system.vue
  9. 95
      zdxt-web-server/zdxt-admin-web-server/src/test/java/com/zdxt/rest/test/cp/AppEnfCpAutoReserveTest.java
  10. 76
      zdxt-web-server/zdxt-admin-web-server/src/test/java/com/zdxt/rest/test/cp/AppEnfCpPlanControllerTest.java
  11. 220
      zdxt-web-server/zdxt-admin-web-server/src/test/java/com/zdxt/rest/test/cp/AppEnfCpReserveTest.java
  12. 354
      zdxt-web-server/zdxt-admin-web-server/src/test/java/com/zdxt/rest/test/cp/AutoMatchReserveTest.java

19
doc/庄锐波-工作周报.md

@ -139,3 +139,22 @@
* 暂停后台管理的功能开发,开始进行计划,预约,任务,预告功能的开发.
* 预计:计划,预约,任务,预告 app端完成的时间,5-31号前.
* 计划,预约,任务,预告 web端不在初验的验收要求里边,暂不开发.
## 时间:2026-05-18 至 2026-05-24
**本周工作:**
* 计划,预约,任务,预告的表设计
* 提供非现场开发的框架和相关文档
**下周工作:**
* (执法端与企业端)计划,预约,任务,预告的功能开发,预计完成时间6-5
## 时间:2026-05-25 至 2026-05-31
**本周工作:**
* (执法端与企业端)计划,预约,任务,预告的功能开发 60%
**下周工作:**
* (执法端与企业端)计划,预约,任务,预告的功能开发及自测,预计完成时间6-5
* 预计6-8号可以由测试人员进行功能测试.

16
zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/controller/app/enforcement/AppEnfCpReserveController.java

@ -12,6 +12,8 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 行政执法检查计划-预约
*
@ -56,4 +58,18 @@ public class AppEnfCpReserveController extends EfcodeCpReserveController {
public R<Void> reject(@PathVariable String id) {
return cpReserveService.rejectReserve(id);
}
/**
* 查询同组关联计划(同一planGroupId下的其他预约记录关联的计划信息)
*
* @param planGroupId 计划分组ID
* @param excludeId 排除的预约记录ID(当前卡片自身)
*/
@GetMapping("/groupMembers")
public R<List<EfcodeCpReserveVo>> getGroupMembers(
@RequestParam String planGroupId,
@RequestParam(required = false) String excludeId) {
List<EfcodeCpReserveVo> members = cpReserveService.queryGroupMembers(planGroupId, excludeId);
return R.ok(members);
}
}

11
zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/IEfcodeCpPlanService.java

@ -88,4 +88,15 @@ public interface IEfcodeCpPlanService {
* @return 按 planGroupId 分组的预约记录列表(key=planGroupId, value=该组预约记录)
*/
Map<String, List<EfcodeCpReserve>> autoMatchReserve(boolean forceMatch, boolean updatePlan, boolean insertReserve);
/**
* 计划自动匹配预约V2(定时任务调用)
* <p>扫描待预约状态的计划,按自然周分组匹配,超出最大联合数量时拆分多组</p>
*
* @param forceMatch 是否强制匹配(忽略 CP_SETTING_AUTO_MATCH_START 日期校验)
* @param updatePlan 是否更新计划记录(system_reserved、reserve_status)
* @param insertReserve 是否将预约记录插入数据库;为false时仅计算匹配结果不落库
* @return 按 planGroupId 分组的预约记录列表
*/
Map<String, List<EfcodeCpReserve>> autoMatchReserveV2(boolean forceMatch, boolean updatePlan, boolean insertReserve);
}

9
zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/IEfcodeCpReserveService.java

@ -100,4 +100,13 @@ public interface IEfcodeCpReserveService {
* @return 操作结果
*/
R<Void> rejectReserve(String reserveId);
/**
* 查询同组关联计划(同一planGroupId下的其他预约记录关联的计划信息)
*
* @param planGroupId 计划分组ID
* @param excludeId 排除的预约记录ID
* @return 同组其他预约记录(关联计划信息)
*/
List<EfcodeCpReserveVo> queryGroupMembers(String planGroupId, String excludeId);
}

246
zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/EfcodeCpPlanServiceImpl.java

@ -23,6 +23,8 @@ import com.zdxt.enforcementcode.service.IEfcodeCpPlanService;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.time.DayOfWeek;
import java.time.temporal.TemporalAdjusters;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
@ -626,8 +628,8 @@ public class EfcodeCpPlanServiceImpl implements IEfcodeCpPlanService {
}
log.info("[CpPlan] 共查询到{}条待匹配计划", pendingPlans.size());
// 4. 已处理过的企业信用代码集合(同一轮中已匹配过的不再重复处理)
Set<String> processedEnterprises = new HashSet<>();
// 4. 已处理过的“企业+周”组合(同一轮中同企业同周不重复处理,但不同周分别匹配)
Set<String> processedKeys = new HashSet<>();
int matchedCount = 0;
for (EfcodeCpPlan plan : pendingPlans) {
@ -635,15 +637,19 @@ public class EfcodeCpPlanServiceImpl implements IEfcodeCpPlanService {
if (StringUtils.isBlank(enterpriseNumber)) {
continue;
}
// 跳过本轮已处理的企业
if (processedEnterprises.contains(enterpriseNumber)) {
continue;
}
// 5. 查找一周内同企业、不同执法单位的其他待匹配计划
// 5. 查找计划所在自然周(周一~周日)内同企业、不同执法单位的其他待匹配计划
LocalDate planLocalDate = plan.getPlanDate().toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
Date weekStart = Date.from(planLocalDate.minusDays(3).atStartOfDay(ZoneId.systemDefault()).toInstant());
Date weekEnd = Date.from(planLocalDate.plusDays(4).atStartOfDay(ZoneId.systemDefault()).toInstant());
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<EfcodeCpPlan> weekLqw = Wrappers.lambdaQuery();
weekLqw.ge(EfcodeCpPlan::getPlanDate, weekStart);
@ -682,6 +688,13 @@ public class EfcodeCpPlanServiceImpl implements IEfcodeCpPlanService {
Date reserveTime = new Date();
List<EfcodeCpReserve> 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();
@ -689,8 +702,8 @@ public class EfcodeCpPlanServiceImpl implements IEfcodeCpPlanService {
reserve.setPlanGroupId(planGroupId);
reserve.setReserveTime(reserveTime);
reserve.setOpStatus(3L); // 待确认
reserve.setOpOrder((long) (i + 1));
reserve.setOpMessage("");
reserve.setOpOrder(0L); // 系统匹配默认为0
reserve.setOpMessage(opMessage);
reserve.setOpSetDate(matchedPlan.getPlanDate());
reserve.setOpSetTime(matchedPlan.getPlanTime());
reserve.setResFrom(1L); // 系统匹配
@ -701,12 +714,20 @@ public class EfcodeCpPlanServiceImpl implements IEfcodeCpPlanService {
efcodeCpReserveMapper.insert(reserve);
}
groupReserves.add(reserve);
}
// 9. 更新计划: system_reserved=1, reserve_status=2
if (updatePlan) {
// 9. 更新计划: 将同企业一周内所有待匹配计划标记为已系统匹配,防止重复匹配
if (updatePlan) {
for (EfcodeCpPlan sp : samePlans) {
EfcodeCpPlan planUpdate = new EfcodeCpPlan();
planUpdate.setId(matchedPlan.getId());
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);
}
@ -715,7 +736,7 @@ public class EfcodeCpPlanServiceImpl implements IEfcodeCpPlanService {
resultMap.put(planGroupId, groupReserves);
// 标记该企业已处理
processedEnterprises.add(enterpriseNumber);
processedKeys.add(enterpriseNumber);
matchedCount++;
log.info("[CpPlan] 企业[{}]匹配成功,联合{}个单位,planGroupId={}",
enterpriseNumber, distinctUnitPlans.size(), planGroupId);
@ -725,6 +746,201 @@ public class EfcodeCpPlanServiceImpl implements IEfcodeCpPlanService {
return resultMap;
}
/**
* 计划自动匹配预约V2(定时任务调用)
* <p>扫描待预约状态的计划,按自然周分组匹配,超出最大联合数量时拆分多组</p>
*
* @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<String, List<EfcodeCpReserve>> autoMatchReserveV2(boolean forceMatch, boolean updatePlan, boolean insertReserve) {
log.info("[CpPlan-V2] 开始执行计划自动匹配预约, forceMatch={}, updatePlan={}, insertReserve={}", forceMatch, updatePlan, insertReserve);
Map<String, List<EfcodeCpReserve>> resultMap = new HashMap<>();
// 1. 日期校验:如果不是强制匹配,则判断当前天数是否等于 CP_SETTING_AUTO_MATCH_START
if (!forceMatch) {
String autoMatchStartDay = getSettingValue("CP_SETTING_AUTO_MATCH_START");
int today = LocalDate.now().getDayOfMonth();
if (StringUtils.isNotBlank(autoMatchStartDay)) {
int settingDay = Integer.parseInt(autoMatchStartDay.trim());
if (today != settingDay) {
log.info("[CpPlan-V2] 当前日期{}号,不等于设定的自动匹配日期{}号,跳过执行", today, settingDay);
return resultMap;
}
}
}
// 2. 获取最大联合单位数量
int maxUnionUnit = 3; // 默认值
String maxUnionStr = getSettingValue("CP_MAX_UNION_UNIT");
if (StringUtils.isNotBlank(maxUnionStr)) {
maxUnionUnit = Integer.parseInt(maxUnionStr.trim());
}
// 3. 查询待匹配的计划:plan_date > 当前时间 且 system_reserved=0, manual_reserved=0, reserve_status=1, is_public=1, is_union=1
Date now = new Date();
LambdaQueryWrapper<EfcodeCpPlan> 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<EfcodeCpPlan> pendingPlans = baseMapper.selectList(lqw);
if (pendingPlans.isEmpty()) {
log.info("[CpPlan-V2] 没有找到待匹配的计划记录,执行结束");
return resultMap;
}
log.info("[CpPlan-V2] 共查询到{}条待匹配计划", pendingPlans.size());
// 4. 已处理过的"企业+周"组合,同企业同周只处理一次
Set<String> 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;
}
processedKeys.add(processKey);
// 6. 查找该自然周内同企业的所有待匹配计划
Date weekStart = Date.from(weekStartDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
Date weekEnd = Date.from(weekEndDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
LambdaQueryWrapper<EfcodeCpPlan> 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<EfcodeCpPlan> samePlans = baseMapper.selectList(weekLqw);
// 7. 按不同执法单位去重,取每个 bureau_dept_id 的第一条计划作为代表
List<EfcodeCpPlan> distinctUnitPlans = new ArrayList<>();
Set<String> 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. 如果只有一个单位的计划,说明没有可联合的,跳过
if (distinctUnitPlans.size() <= 1) {
log.debug("[CpPlan-V2] 企业[{}] 周[{}~{}]内只有1个单位的计划,跳过", enterpriseNumber, weekStartDate, weekEndDate);
continue;
}
// 9. 按最大联合单位数量分组:如果不同单位数 > maxUnionUnit,则拆分成多组
List<List<EfcodeCpPlan>> groups = new ArrayList<>();
for (int i = 0; i < distinctUnitPlans.size(); i += maxUnionUnit) {
int end = Math.min(i + maxUnionUnit, distinctUnitPlans.size());
List<EfcodeCpPlan> group = distinctUnitPlans.subList(i, end);
// 每组至少需要2个不同单位才有联合意义
if (group.size() >= 2) {
groups.add(new ArrayList<>(group));
}
}
if (groups.isEmpty()) {
continue;
}
// 10. 为每组生成预约记录
for (List<EfcodeCpPlan> group : groups) {
String planGroupId = "Temp" + "_" + UUID.randomUUID().toString().replace("-", "");
Date reserveTime = new Date();
List<EfcodeCpReserve> groupReserves = new ArrayList<>();
// 拼接参与单位名称,用于 opMessage
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(预约中)
if (updatePlan) {
for (EfcodeCpPlan dp : group) {
EfcodeCpPlan planUpdate = new EfcodeCpPlan();
planUpdate.setId(dp.getId());
planUpdate.setReserveStatus(2L);
planUpdate.setSystemReserved(1L);
baseMapper.updateById(planUpdate);
}
}
resultMap.put(planGroupId, groupReserves);
matchedCount++;
log.info("[CpPlan-V2] 企业[{}] 周[{}~{}] 分组匹配成功,联合{}个单位,planGroupId={}",
enterpriseNumber, weekStartDate, weekEndDate, group.size(), planGroupId);
}
// 11. 将该企业该周所有待匹配计划的 system_reserved 标记为1,防止重复匹配
if (updatePlan) {
for (EfcodeCpPlan sp : samePlans) {
// 已在上面group循环中更新过的(reserve_status=2)跳过,避免覆盖
if (seenBureauDeptIds.contains(sp.getBureauDeptId())) {
// 该计划对应的单位已经参与联合,在group循环中已处理
continue;
}
EfcodeCpPlan planUpdate = new EfcodeCpPlan();
planUpdate.setId(sp.getId());
planUpdate.setSystemReserved(1L);
baseMapper.updateById(planUpdate);
}
}
}
log.info("[CpPlan-V2] 计划自动匹配预约执行完毕,共匹配{}组联合预约", matchedCount);
return resultMap;
}
/**
* 获取 efcode_cp_setting 配置值
*

63
zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/EfcodeCpReserveServiceImpl.java

@ -473,4 +473,67 @@ public class EfcodeCpReserveServiceImpl implements IEfcodeCpReserveService {
}
}
}
/**
* 查询同组关联计划(同一planGroupId下的其他预约记录关联的计划信息)
*/
@Override
public List<EfcodeCpReserveVo> queryGroupMembers(String planGroupId, String excludeId) {
if (StringUtils.isBlank(planGroupId)) {
return new ArrayList<>();
}
// 查询同组的所有预约记录
LambdaQueryWrapper<EfcodeCpReserve> lqw = Wrappers.lambdaQuery();
lqw.eq(EfcodeCpReserve::getPlanGroupId, planGroupId);
if (StringUtils.isNotBlank(excludeId)) {
lqw.ne(EfcodeCpReserve::getId, excludeId);
}
lqw.orderByAsc(EfcodeCpReserve::getOpOrder);
List<EfcodeCpReserve> reserves = baseMapper.selectList(lqw);
if (reserves.isEmpty()) {
return new ArrayList<>();
}
// 查询关联的计划详情
Set<String> planIds = reserves.stream()
.map(EfcodeCpReserve::getPlanId)
.filter(StringUtils::isNotBlank)
.collect(Collectors.toSet());
Map<String, EfcodeCpPlan> planMap = new HashMap<>();
if (!planIds.isEmpty()) {
List<EfcodeCpPlan> plans = cpPlanMapper.selectByIds(new ArrayList<>(planIds));
for (EfcodeCpPlan p : plans) {
planMap.put(p.getId(), p);
}
}
// 组装返回结果
List<EfcodeCpReserveVo> result = new ArrayList<>();
for (EfcodeCpReserve r : reserves) {
EfcodeCpReserveVo vo = new EfcodeCpReserveVo();
vo.setId(r.getId());
vo.setPlanId(r.getPlanId());
vo.setPlanGroupId(r.getPlanGroupId());
vo.setReserveTime(r.getReserveTime());
vo.setOpStatus(r.getOpStatus());
vo.setOpOrder(r.getOpOrder());
vo.setOpMessage(r.getOpMessage());
vo.setOpSetDate(r.getOpSetDate());
vo.setOpSetTime(r.getOpSetTime());
vo.setResFrom(r.getResFrom());
// 填充计划信息
EfcodeCpPlan plan = planMap.get(r.getPlanId());
if (plan != null) {
vo.setPlanDate(plan.getPlanDate());
vo.setPlanTime(plan.getPlanTime());
vo.setEnterpriseName(plan.getEnterpriseName());
vo.setEnforceContent(plan.getEnforceContent());
vo.setBureauDeptName(plan.getBureauDeptName());
vo.setEnforceUserNames(plan.getEnforceUserNames());
vo.setEnforceUserTels(plan.getEnforceUserTels());
}
result.add(vo);
}
return result;
}
}

3
zdxt-web-client/zdxt-efcode-enforcement-app/src/api/enforcement/reserve.js

@ -13,4 +13,7 @@ export const approveReserve = (id, success, error) => _put(reserveBaseUrl + "/ap
// 预约处理:拒绝
export const rejectReserve = (id, success, error) => _put(reserveBaseUrl + "/reject/" + id, {}, success, error);
// 查询同组关联计划(同planGroupId下的其他预约记录关联的计划信息)
export const getReserveGroupMembers = (planGroupId, excludeId, success, error) => _get(reserveBaseUrl + "/groupMembers", { planGroupId, excludeId }, success, error);
// endregion

99
zdxt-web-client/zdxt-efcode-enforcement-app/src/views/enforcement/my/reserve/cp_reserve_system.vue

@ -56,6 +56,27 @@
<div class="value">{{ formatDate(item.reserveTime) }}</div>
</div>
<!-- 选中时显示关联计划信息 -->
<div v-if="selectedId === item.id && groupMembers.length > 0" class="group-members">
<div class="group-title">关联计划</div>
<div v-for="(member, idx) in groupMembers" :key="member.id" class="member-item">
<div class="member-row">
<span class="member-label">执法单位:</span>
<span class="member-value">{{ member.bureauDeptName || '-' }}</span>
</div>
<div class="member-row">
<span class="member-label">执法事项:</span>
<span class="member-value">{{ member.enforceContent || '-' }}</span>
</div>
<div class="member-row">
<span class="member-label">计划时间:</span>
<span class="member-value">{{ formatDate(member.planDate) }} {{ getPlanTimeText(member.planTime) }}</span>
</div>
<div v-if="idx < groupMembers.length - 1" class="member-divider"></div>
</div>
</div>
<div v-if="selectedId === item.id && groupLoading" class="group-loading">加载中...</div>
<!-- 选中且待确认状态才显示操作按钮 -->
<div v-if="item.opStatus === 3 && selectedId === item.id" class="card-actions">
<div class="actions-left">
@ -87,9 +108,11 @@
<script setup name="cpReserveSystem">
import { showConfirmDialog, showSuccessToast, showDialog } from 'vant';
import { getReserveList, approveReserve, rejectReserve } from '@/api/enforcement/reserve';
import { getReserveList, approveReserve, rejectReserve, getReserveGroupMembers } from '@/api/enforcement/reserve';
const selectedId = ref(null);
const groupMembers = ref([]); // 选中卡片的同组关联计划
const groupLoading = ref(false);
const reserveList = ref([]);
const loading = ref(false);
const finished = ref(false);
@ -143,8 +166,28 @@ const onLoad = () => {
// 选中卡片(再次点击取消选中)
const onSelectCard = (item) => {
if (item.opStatus !== 3) return; // 非待确认状态不可选
selectedId.value = selectedId.value === item.id ? null : item.id;
if (selectedId.value === item.id) {
selectedId.value = null;
groupMembers.value = [];
return;
}
selectedId.value = item.id;
groupMembers.value = [];
// 加载同组关联计划
if (item.planGroupId) {
groupLoading.value = true;
getReserveGroupMembers(
item.planGroupId,
item.id,
(res) => {
groupLoading.value = false;
if (res.code === 200) {
groupMembers.value = res.data || [];
}
},
() => { groupLoading.value = false; }
);
}
};
// 下拉刷新
@ -439,4 +482,54 @@ const afterOperate = (item, action) => {
.reject-action {
color: #ee0a24;
}
.group-members {
margin-top: 12px;
padding-top: 10px;
border-top: 1px dashed rgba(255, 255, 255, 0.2);
}
.group-title {
font-size: 13px;
font-weight: 600;
color: rgba(255, 255, 255, 0.9);
margin-bottom: 8px;
}
.member-item {
padding: 6px 0;
}
.member-row {
display: flex;
align-items: flex-start;
margin-bottom: 4px;
font-size: 13px;
line-height: 1.4;
}
.member-label {
width: 80px;
flex-shrink: 0;
white-space: nowrap;
color: rgba(255, 255, 255, 0.75);
}
.member-value {
flex: 1;
color: rgba(255, 255, 255, 0.6);
}
.member-divider {
height: 1px;
background: rgba(255, 255, 255, 0.1);
margin: 6px 0;
}
.group-loading {
margin-top: 10px;
font-size: 12px;
color: rgba(255, 255, 255, 0.6);
text-align: center;
}
</style>

95
zdxt-web-server/zdxt-admin-web-server/src/test/java/com/zdxt/rest/test/cp/AppEnfCpAutoReserveTest.java

@ -0,0 +1,95 @@
package com.zdxt.rest.test.cp;
import com.zdxt.enforcementcode.domain.EfcodeCpReserve;
import com.zdxt.enforcementcode.service.IEfcodeCpPlanService;
import org.dromara.ZdxtAdminApplication;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import java.util.List;
import java.util.Map;
/**
* 计划自动匹配预约测试
* <p>
* 强制调用系统匹配方法,验证待预约计划的自动联合匹配逻辑。
* 匹配条件:同一企业 + 一周内 + 不同执法单位 ≥ 2 个单位的待预约计划
* </p>
*/
@SpringBootTest(classes = ZdxtAdminApplication.class)
@ActiveProfiles({"dev", "dev-cust-zrb"})
@DisplayName("计划自动匹配预约测试")
public class AppEnfCpAutoReserveTest {
@Autowired
private IEfcodeCpPlanService cpPlanService;
/**
* 强制执行系统自动匹配(干跑模式:不更新计划、不插入预约记录)
* <p>只计算匹配结果,不修改数据库,用于验证匹配逻辑是否正确</p>
*/
@DisplayName("干跑模式 - 强制系统匹配(不落库)")
@Test
@Order(1)
public void testAutoMatch_dryRun() {
System.out.println("========== 开始执行自动匹配预约(干跑模式) ==========");
// forceMatch=true: 跳过日期校验
// updatePlan=false: 不更新计划状态
// insertReserve=false: 不插入预约记录
Map<String, List<EfcodeCpReserve>> result = cpPlanService.autoMatchReserveV2(true, false, false);
System.out.println("匹配结果组数: " + result.size());
for (Map.Entry<String, List<EfcodeCpReserve>> entry : result.entrySet()) {
String planGroupId = entry.getKey();
List<EfcodeCpReserve> reserves = entry.getValue();
System.out.println(" planGroupId=" + planGroupId + ", 联合计划数=" + reserves.size());
for (EfcodeCpReserve r : reserves) {
System.out.println(" - planId=" + r.getPlanId()
+ ", receiverId=" + r.getReceiverId()
+ ", opStatus=" + r.getOpStatus());
}
}
System.out.println("========== 干跑模式执行完毕 ==========");
}
/**
* 强制执行系统自动匹配(实际执行:更新计划 + 插入预约记录)
* <p>将匹配结果落库,计划状态变为“预约中(2)”,生成预约记录</p>
*/
@DisplayName("实际执行 - 强制系统匹配(落库)")
@Test
@Order(2)
public void testAutoMatch_execute() {
System.out.println("========== 开始执行自动匹配预约(实际落库) ==========");
// forceMatch=true: 跳过日期校验
// updatePlan=true: 更新计划状态(system_reserved=1, reserve_status=2)
// insertReserve=true: 插入预约记录到数据库
Map<String, List<EfcodeCpReserve>> result = cpPlanService.autoMatchReserveV2(true, true, true);
System.out.println("匹配结果组数: " + result.size());
int totalReserves = 0;
for (Map.Entry<String, List<EfcodeCpReserve>> entry : result.entrySet()) {
String planGroupId = entry.getKey();
List<EfcodeCpReserve> reserves = entry.getValue();
totalReserves += reserves.size();
System.out.println(" planGroupId=" + planGroupId + ", 联合计划数=" + reserves.size());
for (EfcodeCpReserve r : reserves) {
System.out.println(" - planId=" + r.getPlanId()
+ ", receiverId=" + r.getReceiverId()
+ ", opStatus=" + r.getOpStatus()
+ ", opSetDate=" + r.getOpSetDate());
}
}
System.out.println("总计生成预约记录数: " + totalReserves);
System.out.println("========== 实际落库执行完毕 ==========");
// 验证:如果之前有多个账号创建了同一企业的计划,应能匹配到
Assertions.assertFalse(result.isEmpty(), "应至少匹配到一组联合预约");
}
}

76
zdxt-web-server/zdxt-admin-web-server/src/test/java/com/zdxt/rest/test/cp/AppEnfCpPlanControllerTest.java

@ -158,8 +158,12 @@ public class AppEnfCpPlanControllerTest {
/**
* 为指定账号批量新增计划
* 通过不同的 planDate 保证不触发重复校验
* 企业信息从数据库随机获取
* <p>
* 唯一性约束:同一人 + 日期 + 时间段 + 企业 只能有一条计划。
* 日期循环31天(7月1~31日),时间段2个(上午/下午),企业循环7家,
* 因 31、2、7 两两互素,由中国剩余定理(CRT)保证前 31×2×7=434 条组合均唯一,
* 故 count≤434 时所有计划不会重复。
* </p>
*/
private void batchCreatePlans(String account, int count) throws Exception {
Map<String, String> loginInfo = doLogin(account);
@ -167,26 +171,40 @@ public class AppEnfCpPlanControllerTest {
String userId = loginInfo.get("userId");
String nickName = loginInfo.get("nickName");
// 从数据库随机获取企业列表
// 从数据库加载指定企业列表
List<EnterpriseInformationVo> enterprises = loadEnterprises();
Assertions.assertFalse(enterprises.isEmpty(), "数据库中无可用企业数据");
System.out.println("[" + account + "] 获取到 " + enterprises.size() + " 条企业数据,将随机选取");
Random random = new Random();
// --- 唯一性预测验证 ---
int days = 31; // 7月有31天
int timeSlots = 2; // 上午/下午
int entCount = enterprises.size();
// 由CRT保证的最大不重复组合数 = lcm(days, timeSlots, entCount) 当两两互素时 = days * timeSlots * entCount
int maxUnique = days * timeSlots * entCount;
int expectedSuccess = Math.min(count, maxUnique);
System.out.println("[" + account + "] 企业数=" + entCount
+ ", 可用唯一组合(日期×时段×企业)=" + maxUnique
+ ", 计划生成数=" + count
+ ", 预计成功数=" + expectedSuccess);
Assertions.assertTrue(count <= maxUnique,
"计划数(" + count + ")超过最大唯一组合数(" + maxUnique + "),必然产生重复");
Random random = new Random();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Calendar cal = Calendar.getInstance();
cal.set(2026, Calendar.JULY, 1); // 从2026-07-01开始
int successCount = 0;
int failCount = 0;
for (int i = 0; i < count; i++) {
cal.add(Calendar.DAY_OF_MONTH, 1); // 每条计划日期+1天
// 所有计划日期限定在2026年7月内,循环使用7月1日~31日
int dayOfMonth = (i % days) + 1;
cal.set(2026, Calendar.JULY, dayOfMonth);
String planDate = sdf.format(cal.getTime());
long planTime = (i % 2 == 0) ? 1L : 2L; // 交替上午/下午
// 随机选取一个企业
EnterpriseInformationVo enterprise = enterprises.get(random.nextInt(enterprises.size()));
// 确定性循环选取企业,保证 (日期, 时段, 企业) 三元组唯一
EnterpriseInformationVo enterprise = enterprises.get(i % entCount);
Map<String, Object> planBo = buildPlanBo(planDate, planTime, i, nickName, userId, loginInfo, enterprise, random);
String requestBody = objectMapper.writeValueAsString(planBo);
@ -217,22 +235,38 @@ public class AppEnfCpPlanControllerTest {
System.out.println("========== [" + account + "] 批量新增完成 ==========");
System.out.println("总计: " + count + ", 成功: " + successCount + ", 失败: " + failCount);
Assertions.assertEquals(count, successCount, "[" + account + "] 应全部新增成功");
Assertions.assertEquals(expectedSuccess, successCount,
"[" + account + "] 预期成功" + expectedSuccess + "条,实际成功" + successCount + "条");
}
/** 指定的企业统一社会信用代码列表 */
private static final String[][] ENTERPRISE_DATA = {
{"吉田拉链(深圳)有限公司公明工厂", "91440300788302281W"},
{"欧姆龙电子部件(深圳)有限公司", "914403007261811621"},
{"深圳农牧美益肉业有限公司", "91440300662678582M"},
{"深圳市深水龙岗污水处理有限公司", "914403006820066603"},
{"深圳市强达电路股份有限公司", "91440300763461366N"},
{"深圳康泰生物制品股份有限公司", "91440300618837873J"},
{"深圳市里程食品有限公司", "91440300738848091F"},
};
/**
* 从数据库加载企业列表(取前200条可用企业)
* 从数据库加载指定的7家企业
*/
private List<EnterpriseInformationVo> loadEnterprises() {
EnterpriseInformationBo queryBo = new EnterpriseInformationBo();
queryBo.setIsDelete(0L);
List<EnterpriseInformationVo> list = enterpriseInformationService.queryList(queryBo);
// 最多取200条用于随机
if (list.size() > 200) {
Collections.shuffle(list);
return list.subList(0, 200);
List<EnterpriseInformationVo> result = new ArrayList<>();
for (String[] entry : ENTERPRISE_DATA) {
EnterpriseInformationBo queryBo = new EnterpriseInformationBo();
queryBo.setEnterpriseNumber(entry[1]);
queryBo.setIsDelete(0L);
List<EnterpriseInformationVo> list = enterpriseInformationService.queryList(queryBo);
if (!list.isEmpty()) {
result.add(list.get(0));
} else {
System.out.println("[警告] 未找到企业: " + entry[0] + " (" + entry[1] + ")");
}
}
return list;
return result;
}
/**
@ -256,8 +290,8 @@ public class AppEnfCpPlanControllerTest {
bo.put("enforceUserTels", loginInfo.get("phonenumber"));
bo.put("enforceUserCodes", "-");
bo.put("isPublic", random.nextInt(2) == 0 ? 0L : 1L); // 随机公开/不公开
bo.put("isUnion", random.nextInt(2) == 0 ? 0L : 1L); // 随机联合/普通
bo.put("reserveStatus", (long) random.nextInt(4)); // 0=草稿,1=待预约,2=预约中,3=预约完成
bo.put("isUnion", 1L); // 必须为联合执法,否则afterInsert会直接生成任务导致status变为3
bo.put("reserveStatus", 1L); // 待预约
bo.put("systemReserved", 0L);
bo.put("manualReserved", 0L);
bo.put("bureauDeptId", loginInfo.get("bureauUnitId"));

220
zdxt-web-server/zdxt-admin-web-server/src/test/java/com/zdxt/rest/test/cp/AppEnfCpReserveTest.java

@ -1,220 +0,0 @@
package com.zdxt.rest.test.cp;
import cn.dev33.satoken.context.mock.SaTokenContextMockUtil;
import cn.dev33.satoken.stp.StpUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.zdxt.enforcementcode.domain.EfcodeCpPlan;
import com.zdxt.enforcementcode.domain.EfcodeCpReserve;
import com.zdxt.enforcementcode.mapper.EfcodeCpPlanMapper;
import com.zdxt.enforcementcode.mapper.EfcodeCpReserveMapper;
import org.dromara.ZdxtAdminApplication;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.util.*;
import java.util.stream.Collectors;
/**
* 预约记录批量生成测试
* <p>
* 基于已创建的计划数据,为多个用户交替创建 efcode_cp_reserve 预约记录。
* 模拟场景:用户A的计划被用户B预约,用户B的计划被用户C预约...循环交替。
* 预约状态交替:待确认(3)、已同意(1)、已拒绝(2)
* 预约来源交替:手动预约(0)、系统匹配(1)
* </p>
*/
@SpringBootTest(classes = ZdxtAdminApplication.class)
@AutoConfigureMockMvc
@ActiveProfiles({"dev", "dev-cust-zrb"})
@DisplayName("AppEnfCpReserve 批量生成预约记录测试")
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@Tag("dev")
public class AppEnfCpReserveTest {
@Autowired
private EfcodeCpPlanMapper cpPlanMapper;
@Autowired
private EfcodeCpReserveMapper cpReserveMapper;
/**
* 测试账号对应的 userId(需从数据库匹配)
* 这里通过查询 plan 表的 create_by 字段获取对应用户ID
*/
private static final String[] TEST_ACCOUNTS = {"liuqianlin", "baining", "caizifan", "chenchong4"};
/** 每个用户最多取多少条计划来生成预约 */
private static final int MAX_PLANS_PER_USER = 50;
/** 预约留言模板 */
private static final String[] MESSAGES = {
"希望联合执法,请确认时间",
"我单位有相关检查需求,申请联合",
"建议合并检查,减轻企业负担",
"时间安排一致,申请联合执法",
"", // 部分无留言
"请尽快确认预约",
"",
"我方已做好准备,等待确认"
};
@BeforeEach
public void setUp() {
SaTokenContextMockUtil.setMockContext();
MockHttpServletRequest mockRequest = new MockHttpServletRequest();
mockRequest.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)");
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(mockRequest));
}
@AfterEach
public void tearDown() {
try {
StpUtil.logout();
} catch (Exception ignored) {
}
RequestContextHolder.resetRequestAttributes();
SaTokenContextMockUtil.clearContext();
}
@DisplayName("基于已有计划批量生成交替预约记录")
@Test
@Order(1)
public void testBatchCreateReserves() {
// 1. 收集每个用户的计划(通过 enforce_user_names LIKE 账号中文名识别)
// 实际通过 create_by 字段分组获取不同用户的计划
Map<Long, List<EfcodeCpPlan>> userPlansMap = loadUserPlansMap();
List<Long> userIds = new ArrayList<>(userPlansMap.keySet());
Assertions.assertTrue(userIds.size() >= 2, "至少需要2个用户的计划数据才能交叉预约");
System.out.println("========== 检测到 " + userIds.size() + " 个用户的计划数据 ==========");
for (Long uid : userIds) {
System.out.println(" userId=" + uid + ", 计划数=" + userPlansMap.get(uid).size());
}
Random random = new Random(42); // 固定种子,可重复
int totalCreated = 0;
int reserveIndex = 0;
// 2. 交替为每个用户的计划创建来自其他用户的预约
for (int ownerIdx = 0; ownerIdx < userIds.size(); ownerIdx++) {
Long ownerUserId = userIds.get(ownerIdx);
List<EfcodeCpPlan> ownerPlans = userPlansMap.get(ownerUserId);
// 限制数量
int planCount = Math.min(ownerPlans.size(), MAX_PLANS_PER_USER);
for (int planIdx = 0; planIdx < planCount; planIdx++) {
EfcodeCpPlan targetPlan = ownerPlans.get(planIdx);
// 选择预约发起者:循环选取其他用户
int reserverIdx = (ownerIdx + 1 + planIdx) % userIds.size();
if (reserverIdx == ownerIdx) {
reserverIdx = (reserverIdx + 1) % userIds.size();
}
Long reserverUserId = userIds.get(reserverIdx);
List<EfcodeCpPlan> reserverPlans = userPlansMap.get(reserverUserId);
// 找预约者匹配的计划(同日期+同时段+同企业),如果没有就用预约者的任意一条计划
EfcodeCpPlan reserverPlan = findMatchingPlan(reserverPlans, targetPlan);
if (reserverPlan == null && !reserverPlans.isEmpty()) {
reserverPlan = reserverPlans.get(planIdx % reserverPlans.size());
}
if (reserverPlan == null) continue;
// 3. 确定 planGroupId
String planGroupId = targetPlan.getId();
// 4. 交替 opStatus: 3(待确认), 1(已同意), 2(已拒绝), 3, 1, 2...
long opStatus = getAlternatingStatus(reserveIndex);
// 5. 交替 resFrom: 0(手动), 1(系统)
long resFrom = reserveIndex % 2;
// 6. 留言
String message = MESSAGES[reserveIndex % MESSAGES.length];
// 插入目标计划方的记录(计划拥有者)
insertReserve(targetPlan.getId(), planGroupId, "", targetPlan.getPlanDate(),
targetPlan.getPlanTime(), resFrom, opStatus);
// 插入预约发起方的记录
insertReserve(reserverPlan.getId(), planGroupId, message, targetPlan.getPlanDate(),
targetPlan.getPlanTime(), resFrom, 3L); // 发起方始终待确认
totalCreated += 2;
reserveIndex++;
}
System.out.println("[userId=" + ownerUserId + "] 已为其 " + planCount + " 条计划生成预约记录");
}
System.out.println("========== 预约记录生成完成 ==========");
System.out.println("总计插入: " + totalCreated + " 条预约记录");
Assertions.assertTrue(totalCreated > 0, "应至少生成1条预约记录");
}
/**
* 从数据库加载各用户的计划,按 create_by 分组
* 只取未删除、reserve_status=1(待预约)的计划
*/
private Map<Long, List<EfcodeCpPlan>> loadUserPlansMap() {
LambdaQueryWrapper<EfcodeCpPlan> lqw = Wrappers.lambdaQuery();
lqw.eq(EfcodeCpPlan::getIsDelete, 0L);
lqw.eq(EfcodeCpPlan::getReserveStatus, 1L); // 待预约状态
lqw.like(EfcodeCpPlan::getEnforceContent, "批量测试执法事项"); // 只取测试数据
lqw.orderByAsc(EfcodeCpPlan::getPlanDate);
List<EfcodeCpPlan> allPlans = cpPlanMapper.selectList(lqw);
return allPlans.stream()
.filter(p -> p.getCreateBy() != null)
.collect(Collectors.groupingBy(EfcodeCpPlan::getCreateBy));
}
/**
* 在预约者的计划列表中查找与目标计划匹配的计划(同日期+同时段+同企业)
*/
private EfcodeCpPlan findMatchingPlan(List<EfcodeCpPlan> plans, EfcodeCpPlan target) {
if (plans == null || plans.isEmpty()) return null;
for (EfcodeCpPlan p : plans) {
if (p.getPlanDate() != null && p.getPlanDate().equals(target.getPlanDate())
&& Objects.equals(p.getPlanTime(), target.getPlanTime())
&& Objects.equals(p.getEnterpriseId(), target.getEnterpriseId())) {
return p;
}
}
return null;
}
/**
* 交替状态: 3, 1, 2, 3, 1, 2...
*/
private long getAlternatingStatus(int index) {
int mod = index % 3;
return mod == 0 ? 3L : (mod == 1 ? 1L : 2L);
}
/**
* 插入一条预约记录
*/
private void insertReserve(String planId, String planGroupId, String opMessage,
Date opSetDate, Long opSetTime, Long resFrom, Long opStatus) {
EfcodeCpReserve record = new EfcodeCpReserve();
record.setPlanId(planId);
record.setPlanGroupId(planGroupId);
record.setReserveTime(new Date());
record.setOpStatus(opStatus);
record.setOpOrder(0L);
record.setOpMessage(opMessage);
record.setOpSetDate(opSetDate);
record.setOpSetTime(opSetTime);
record.setResFrom(resFrom);
cpReserveMapper.insert(record);
}
}

354
zdxt-web-server/zdxt-admin-web-server/src/test/java/com/zdxt/rest/test/cp/AutoMatchReserveTest.java

@ -1,354 +0,0 @@
package com.zdxt.rest.test.cp;
import cn.dev33.satoken.context.mock.SaTokenContextMockUtil;
import cn.dev33.satoken.stp.StpUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.zdxt.enforcementcode.domain.EfcodeCpPlan;
import com.zdxt.enforcementcode.domain.EfcodeCpReserve;
import com.zdxt.enforcementcode.mapper.EfcodeCpPlanMapper;
import com.zdxt.enforcementcode.mapper.EfcodeCpReserveMapper;
import com.zdxt.enforcementcode.service.IEfcodeCpPlanService;
import org.dromara.ZdxtAdminApplication;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.*;
import java.util.stream.Collectors;
/**
* 计划自动匹配预约 autoMatchReserve 方法测试
* <p>
* 测试场景:
* 1. 强制模式下执行自动匹配(绕过日期校验)
* 2. 验证匹配结果:同企业不同单位的计划被联合
* 3. 验证计划状态更新:system_reserved=1, reserve_status=2
* 4. 验证预约记录生成:相同 plan_group_id 关联多条记录
* </p>
*/
@SpringBootTest(classes = ZdxtAdminApplication.class)
@AutoConfigureMockMvc
@ActiveProfiles({"dev", "dev-cust-zrb"})
@DisplayName("autoMatchReserve 自动匹配预约测试")
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@Tag("dev")
public class AutoMatchReserveTest {
@Autowired
private IEfcodeCpPlanService cpPlanService;
@Autowired
private EfcodeCpPlanMapper cpPlanMapper;
@Autowired
private EfcodeCpReserveMapper cpReserveMapper;
@BeforeEach
public void setUp() {
SaTokenContextMockUtil.setMockContext();
MockHttpServletRequest mockRequest = new MockHttpServletRequest();
mockRequest.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)");
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(mockRequest));
}
@AfterEach
public void tearDown() {
try {
StpUtil.logout();
} catch (Exception ignored) {
}
RequestContextHolder.resetRequestAttributes();
SaTokenContextMockUtil.clearContext();
}
@DisplayName("0. 插入1000条适合匹配的计划数据")
@Test
@Order(0)
public void testInsert1000MatchablePlans() {
// 模拟登录(避免自动填充 createBy 报空)
StpUtil.login(1);
// 准备数据:200个企业 × 每企业5个不同执法单位 = 1000条计划
int enterpriseCount = 200;
int unitsPerEnterprise = 5;
Random random = new Random();
// 模拟执法单位ID池(确保不同单位)
String[] bureauDeptIds = {
"BUREAU_TEST_A", "BUREAU_TEST_B", "BUREAU_TEST_C",
"BUREAU_TEST_D", "BUREAU_TEST_E", "BUREAU_TEST_F",
"BUREAU_TEST_G", "BUREAU_TEST_H"
};
String[] bureauDeptNames = {
"测试执法局A", "测试执法局B", "测试执法局C",
"测试执法局D", "测试执法局E", "测试执法局F",
"测试执法局G", "测试执法局H"
};
// 基准日期:从明天开始
LocalDate baseDate = LocalDate.now().plusDays(1);
int insertedCount = 0;
for (int e = 0; e < enterpriseCount; e++) {
String enterpriseNumber = String.format("91440300TEST%06d", e);
String enterpriseName = "测试企业" + e + "号";
String enterpriseId = "ENT_TEST_" + e;
// 同一企业的计划日期在基准日期附3天内(确保一周内可匹配)
Date planDate = Date.from(
baseDate.plusDays(random.nextInt(3))
.atStartOfDay(ZoneId.systemDefault()).toInstant()
);
for (int u = 0; u < unitsPerEnterprise; u++) {
int bureauIdx = (e + u) % bureauDeptIds.length;
EfcodeCpPlan plan = new EfcodeCpPlan();
plan.setId(UUID.randomUUID().toString().replace("-", ""));
plan.setPlanType(random.nextBoolean() ? 1L : 2L);
plan.setPlanDate(planDate);
plan.setPlanTime((long) (random.nextInt(3) + 1)); // 1=上午,2=下午,3=其他
plan.setPlanActualDate(plan.getPlanDate());
plan.setPlanActualTime(plan.getPlanTime()); // 1=上午,2=下午,3=其他
plan.setEnforceContent("自动匹配测试-执法事项" + e + "-" + u);
plan.setEnterpriseName(enterpriseName);
plan.setEnterpriseNumber(enterpriseNumber);
plan.setEnterpriseId(enterpriseId);
plan.setEnforceUserNames("测试人员" + u);
plan.setEnforceUserIds(String.valueOf(1000 + u));
plan.setEnforceUserCodes("CODE" + u);
plan.setEnforceUserTels("1380000" + String.format("%04d", u));
plan.setIsPublic(1L);
plan.setIsUnion(1L);
plan.setReserveStatus(1L); // 待预约
plan.setSystemReserved(0L); // 未经系统匹配
plan.setManualReserved(0L);
plan.setBureauDeptId(bureauDeptIds[bureauIdx]);
plan.setBureauDeptName(bureauDeptNames[bureauIdx]);
plan.setOrganId(plan.getBureauDeptId());
plan.setOrganName(plan.getBureauDeptName());
plan.setDeptId(plan.getBureauDeptId());
plan.setDeptName(plan.getBureauDeptName());
plan.setIsDelete(0L);
plan.setCreateBy(1L);
plan.setCreateTime(new Date());
plan.setUpdateBy(1L);
plan.setUpdateTime(new Date());
cpPlanMapper.insert(plan);
insertedCount++;
}
}
System.out.println("========== 插入匹配测试数据完成 ==========");
System.out.println("共插入计划数: " + insertedCount);
System.out.println("涉及企业数: " + enterpriseCount);
System.out.println("每企业不同单位数: " + unitsPerEnterprise);
System.out.println("预期可匹配企业组数: " + enterpriseCount + " (每企业都有>=2个单位)");
System.out.println("===========================================");
Assertions.assertEquals(1000, insertedCount, "应插入恰好1000条计划");
}
@DisplayName("1. 查看当前待匹配计划数据概况")
@Test
@Order(1)
public void testCheckPendingPlans() {
// 查询符合自动匹配条件的计划
LambdaQueryWrapper<EfcodeCpPlan> lqw = Wrappers.lambdaQuery();
lqw.gt(EfcodeCpPlan::getPlanDate, new Date());
lqw.eq(EfcodeCpPlan::getSystemReserved, 0L);
lqw.eq(EfcodeCpPlan::getReserveStatus, 1L);
lqw.eq(EfcodeCpPlan::getIsPublic, 1L);
lqw.eq(EfcodeCpPlan::getIsUnion, 1L);
lqw.eq(EfcodeCpPlan::getIsDelete, 0L);
List<EfcodeCpPlan> pendingPlans = cpPlanMapper.selectList(lqw);
System.out.println("========== 待匹配计划数据概况 ==========");
System.out.println("符合条件的计划总数: " + pendingPlans.size());
// 按企业信用代码分组统计
Map<String, List<EfcodeCpPlan>> groupByEnterprise = pendingPlans.stream()
.filter(p -> p.getEnterpriseNumber() != null)
.collect(Collectors.groupingBy(EfcodeCpPlan::getEnterpriseNumber));
int potentialMatchCount = 0;
for (Map.Entry<String, List<EfcodeCpPlan>> entry : groupByEnterprise.entrySet()) {
// 统计不同执法单位数量
long distinctUnits = entry.getValue().stream()
.map(EfcodeCpPlan::getBureauDeptId)
.distinct()
.count();
if (distinctUnits >= 2) {
potentialMatchCount++;
System.out.println(" 企业[" + entry.getKey() + "] - 有" + distinctUnits + "个不同单位的计划,可匹配");
}
}
System.out.println("潜在可匹配的企业数: " + potentialMatchCount);
System.out.println("=======================================");
}
@DisplayName("2. 强制执行 autoMatchReserve 并验证结果")
@Test
@Order(2)
public void testAutoMatchReserveForceMode() {
// 执行前:统计当前系统匹配预约记录数
long beforeReserveCount = cpReserveMapper.selectCount(
Wrappers.<EfcodeCpReserve>lambdaQuery().eq(EfcodeCpReserve::getResFrom, 1L)
);
System.out.println("执行前系统匹配预约记录数: " + beforeReserveCount);
// 执行前:统计已被系统预约的计划数
long beforeMatchedPlanCount = cpPlanMapper.selectCount(
Wrappers.<EfcodeCpPlan>lambdaQuery()
.eq(EfcodeCpPlan::getSystemReserved, 1L)
.eq(EfcodeCpPlan::getIsDelete, 0L)
);
System.out.println("执行前已系统匹配计划数: " + beforeMatchedPlanCount);
// 执行自动匹配(强制模式,忽略日期校验)
System.out.println("\n========== 开始执行 autoMatchReserve(true) ==========");
cpPlanService.autoMatchReserve(true, true, true);
System.out.println("========== 执行完毕 ==========\n");
// 执行后:统计系统匹配预约记录数
long afterReserveCount = cpReserveMapper.selectCount(
Wrappers.<EfcodeCpReserve>lambdaQuery().eq(EfcodeCpReserve::getResFrom, 1L)
);
System.out.println("执行后系统匹配预约记录数: " + afterReserveCount);
System.out.println("本次新增预约记录数: " + (afterReserveCount - beforeReserveCount));
// 执行后:统计已被系统预约的计划数
long afterMatchedPlanCount = cpPlanMapper.selectCount(
Wrappers.<EfcodeCpPlan>lambdaQuery()
.eq(EfcodeCpPlan::getSystemReserved, 1L)
.eq(EfcodeCpPlan::getIsDelete, 0L)
);
System.out.println("执行后已系统匹配计划数: " + afterMatchedPlanCount);
System.out.println("本次新增匹配计划数: " + (afterMatchedPlanCount - beforeMatchedPlanCount));
// 验证:如果有符合条件的数据,则应产生匹配结果
if (afterReserveCount > beforeReserveCount) {
System.out.println("\n[验证通过] 自动匹配产生了新的预约记录");
} else {
System.out.println("\n[提示] 未产生新预约记录(可能没有符合条件的跨单位同企业计划)");
}
}
@DisplayName("3. 验证匹配后的 plan_group_id 分组一致性")
@Test
@Order(3)
public void testVerifyPlanGroupConsistency() {
// 查询所有系统匹配生成的预约记录
LambdaQueryWrapper<EfcodeCpReserve> lqw = Wrappers.lambdaQuery();
lqw.eq(EfcodeCpReserve::getResFrom, 1L);
lqw.eq(EfcodeCpReserve::getOpStatus, 3L); // 待确认
lqw.orderByDesc(EfcodeCpReserve::getCreateTime);
List<EfcodeCpReserve> reserves = cpReserveMapper.selectList(lqw);
if (reserves.isEmpty()) {
System.out.println("[跳过] 无系统匹配的预约记录,无法验证分组一致性");
return;
}
// 按 plan_group_id 分组
Map<String, List<EfcodeCpReserve>> groupMap = reserves.stream()
.collect(Collectors.groupingBy(EfcodeCpReserve::getPlanGroupId));
System.out.println("========== plan_group_id 分组验证 ==========");
System.out.println("共有 " + groupMap.size() + " 组联合预约");
int validGroups = 0;
for (Map.Entry<String, List<EfcodeCpReserve>> entry : groupMap.entrySet()) {
List<EfcodeCpReserve> group = entry.getValue();
// 每组至少2条记录(2个不同单位)
if (group.size() >= 2) {
validGroups++;
// 验证同组内关联的计划属于不同执法单位
Set<String> planIds = group.stream()
.map(EfcodeCpReserve::getPlanId)
.collect(Collectors.toSet());
List<EfcodeCpPlan> plans = cpPlanMapper.selectByIds(planIds);
Set<String> bureauDeptIds = plans.stream()
.map(EfcodeCpPlan::getBureauDeptId)
.collect(Collectors.toSet());
System.out.println(" groupId=" + entry.getKey()
+ " | 记录数=" + group.size()
+ " | 涉及单位数=" + bureauDeptIds.size());
// 断言:同组关联的计划应来自不同单位
Assertions.assertTrue(bureauDeptIds.size() >= 2,
"同一planGroupId下应关联至少2个不同执法单位的计划");
}
}
System.out.println("有效分组数(>=2条记录): " + validGroups);
System.out.println("=============================================");
}
@DisplayName("4. 验证匹配后计划状态已更新")
@Test
@Order(4)
public void testVerifyPlanStatusUpdated() {
// 查询已被系统匹配的计划
LambdaQueryWrapper<EfcodeCpPlan> lqw = Wrappers.lambdaQuery();
lqw.eq(EfcodeCpPlan::getSystemReserved, 1L);
lqw.eq(EfcodeCpPlan::getReserveStatus, 2L); // 预约中
lqw.eq(EfcodeCpPlan::getIsDelete, 0L);
List<EfcodeCpPlan> matchedPlans = cpPlanMapper.selectList(lqw);
System.out.println("========== 计划状态验证 ==========");
System.out.println("system_reserved=1 且 reserve_status=2 的计划数: " + matchedPlans.size());
if (!matchedPlans.isEmpty()) {
// 验证每条计划都满足状态要求
for (EfcodeCpPlan plan : matchedPlans) {
Assertions.assertEquals(1L, plan.getSystemReserved(),
"计划[" + plan.getId() + "]的system_reserved应为1");
Assertions.assertEquals(2L, plan.getReserveStatus(),
"计划[" + plan.getId() + "]的reserve_status应为2(预约中)");
}
System.out.println("[验证通过] 所有已匹配计划状态正确");
} else {
System.out.println("[提示] 无已匹配计划数据");
}
System.out.println("==================================");
}
@DisplayName("5. 再次执行 autoMatchReserve 验证幂等性(不重复匹配)")
@Test
@Order(5)
public void testAutoMatchReserveIdempotent() {
// 先统计当前预约记录数
long beforeCount = cpReserveMapper.selectCount(
Wrappers.<EfcodeCpReserve>lambdaQuery().eq(EfcodeCpReserve::getResFrom, 1L)
);
// 再次执行
System.out.println("========== 二次执行 autoMatchReserve(true) 验证幂等性 ==========");
cpPlanService.autoMatchReserve(true, true, true);
long afterCount = cpReserveMapper.selectCount(
Wrappers.<EfcodeCpReserve>lambdaQuery().eq(EfcodeCpReserve::getResFrom, 1L)
);
long newRecords = afterCount - beforeCount;
System.out.println("二次执行新增预约记录数: " + newRecords);
// 已匹配过的计划不应再次匹配(system_reserved已设为1)
// 但如果有新的符合条件的数据也可能产生新记录
System.out.println("[说明] 已匹配的计划(system_reserved=1)不会被重复匹配");
System.out.println("================================================================");
}
}
Loading…
Cancel
Save