41 changed files with 2868 additions and 130 deletions
@ -0,0 +1,106 @@ |
|||
package com.zdxt.enforcementcode.controller.web.supervision; |
|||
|
|||
import java.util.List; |
|||
|
|||
import cn.dev33.satoken.annotation.SaIgnore; |
|||
import lombok.RequiredArgsConstructor; |
|||
import jakarta.servlet.http.HttpServletResponse; |
|||
import jakarta.validation.constraints.*; |
|||
import cn.dev33.satoken.annotation.SaCheckPermission; |
|||
import org.springframework.web.bind.annotation.*; |
|||
import org.springframework.validation.annotation.Validated; |
|||
import org.dromara.common.idempotent.annotation.RepeatSubmit; |
|||
import org.dromara.common.log.annotation.Log; |
|||
import org.dromara.common.web.core.BaseController; |
|||
import org.dromara.common.mybatis.core.page.PageQuery; |
|||
import org.dromara.common.core.domain.R; |
|||
import org.dromara.common.core.validate.AddGroup; |
|||
import org.dromara.common.core.validate.EditGroup; |
|||
import org.dromara.common.log.enums.BusinessType; |
|||
import org.dromara.common.excel.utils.ExcelUtil; |
|||
import com.zdxt.enforcementcode.domain.vo.EnforcementEducationServicesVo; |
|||
import com.zdxt.enforcementcode.domain.bo.EnforcementEducationServicesBo; |
|||
import com.zdxt.enforcementcode.service.IEnforcementEducationServicesService; |
|||
import org.dromara.common.mybatis.core.page.TableDataInfo; |
|||
|
|||
/** |
|||
* 监督端-普法服务主表 |
|||
* |
|||
* @author niujiyi |
|||
* @date 2026-05-26 |
|||
*/ |
|||
@Validated |
|||
@RequiredArgsConstructor |
|||
@RestController |
|||
@RequestMapping("/web/enforcementcode/educationServices") |
|||
public class EnforcementEducationServicesController extends BaseController { |
|||
|
|||
private final IEnforcementEducationServicesService enforcementEducationServicesService; |
|||
|
|||
/** |
|||
* 监督端-查询普法服务主表列表 |
|||
*/ |
|||
//@SaCheckPermission("enforcementcode:educationServices:list")
|
|||
@SaIgnore |
|||
@GetMapping("/list") |
|||
public TableDataInfo<EnforcementEducationServicesVo> list(EnforcementEducationServicesBo bo, PageQuery pageQuery) { |
|||
return enforcementEducationServicesService.queryPageList(bo, pageQuery); |
|||
} |
|||
|
|||
/** |
|||
* 监督端-导出普法服务主表列表 |
|||
*/ |
|||
@SaCheckPermission("enforcementcode:educationServices:export") |
|||
@Log(title = "普法服务主表", businessType = BusinessType.EXPORT) |
|||
@PostMapping("/export") |
|||
public void export(EnforcementEducationServicesBo bo, HttpServletResponse response) { |
|||
List<EnforcementEducationServicesVo> list = enforcementEducationServicesService.queryList(bo); |
|||
ExcelUtil.exportExcel(list, "普法服务主表", EnforcementEducationServicesVo.class, response); |
|||
} |
|||
|
|||
/** |
|||
* 监督端-获取普法服务主表详细信息 |
|||
* |
|||
* @param id 主键 |
|||
*/ |
|||
//@SaCheckPermission("enforcementcode:educationServices:query")
|
|||
@SaIgnore |
|||
@GetMapping("/details") |
|||
public R<EnforcementEducationServicesVo> details(String id) { |
|||
return R.ok(enforcementEducationServicesService.queryEducationServicesById(id)); |
|||
} |
|||
|
|||
/** |
|||
* 监督端-新增普法服务主表 |
|||
*/ |
|||
//@SaCheckPermission("enforcementcode:educationServices:add")
|
|||
@SaIgnore |
|||
@Log(title = "普法服务主表", businessType = BusinessType.INSERT) |
|||
@PostMapping("/add") |
|||
public R<Void> add(@Validated(AddGroup.class) @RequestBody EnforcementEducationServicesBo bo) { |
|||
return toAjax(enforcementEducationServicesService.insertEducationServices(bo)); |
|||
} |
|||
|
|||
/** |
|||
* 监督端-修改普法服务主表 |
|||
*/ |
|||
//@SaCheckPermission("enforcementcode:educationServices:edit")
|
|||
@Log(title = "普法服务主表", businessType = BusinessType.UPDATE) |
|||
@SaIgnore |
|||
@PostMapping("/edit") |
|||
public R<Void> edit(@RequestBody EnforcementEducationServicesBo bo) { |
|||
return toAjax(enforcementEducationServicesService.updateByBo(bo)); |
|||
} |
|||
|
|||
/** |
|||
* 监督端-删除普法服务主表 |
|||
* |
|||
* @param ids 主键串 |
|||
*/ |
|||
//@SaCheckPermission("enforcementcode:educationServices:remove")
|
|||
@SaIgnore |
|||
@GetMapping("/remove") |
|||
public R<Void> remove(String[] ids) { |
|||
return toAjax(enforcementEducationServicesService.deleteWithValidByIds(List.of(ids), true)); |
|||
} |
|||
} |
|||
@ -0,0 +1,163 @@ |
|||
<template> |
|||
<div class="login-page"> |
|||
<div class="login-content"> |
|||
<van-nav-bar> |
|||
<template #title> |
|||
<div class="title"> |
|||
执法端登录 |
|||
</div> |
|||
</template> |
|||
</van-nav-bar> |
|||
<van-form @submit="onSubmit"> |
|||
<van-field v-model="loginParam.secretName" name="secretName" label="用户名" placeholder="用户名" |
|||
:rules="[{ required: true, message: '请填写用户名' }]"/> |
|||
<van-field v-model="loginParam.secretKey" type="password" name="secretKey" label="密码" |
|||
placeholder="密码" |
|||
:rules="[{ required: true, message: '请填写密码' }]"/> |
|||
<div style="margin: 0.42rem"> |
|||
<van-button block type="primary" native-type="submit">登录</van-button> |
|||
</div> |
|||
</van-form> |
|||
<div style="margin: 0.42rem 0.42rem 0; text-align: center;"> |
|||
<van-button plain type="default" size="small" @click="showTestPicker = true" |
|||
style="background-color: darkred;color: white">选择测试用户 |
|||
</van-button> |
|||
</div> |
|||
<van-action-sheet v-model:show="showTestPicker" title="选择测试用户"> |
|||
<div class="test-enterprise-list"> |
|||
<van-cell |
|||
v-for="(item, index) in testUsers" |
|||
:key="index" |
|||
:title="item.name" |
|||
:label="'账号:' + item.account" |
|||
is-link |
|||
@click="selectTestUser(item)" |
|||
/> |
|||
</div> |
|||
</van-action-sheet> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
<script setup> |
|||
import {reactive, ref} from 'vue'; |
|||
import {enforcementLogin, enforcementLoginByPassword} from '../../api/enforcement'; |
|||
import CryptoJS from "crypto-js"; |
|||
import {showFailToast} from 'vant'; |
|||
import 'vant/es/toast/style'; |
|||
|
|||
const {useCommon} = $globalStore; |
|||
const router = useRouter(); |
|||
const loginParam = reactive({ |
|||
secretName: "", |
|||
secretKey: "" |
|||
}) |
|||
|
|||
const showTestPicker = ref(false); |
|||
const testUsers = [ |
|||
{account: 'liuqianlin', name: '刘倩琳', password: 'szsf@zfjd'}, |
|||
{account: 'baining', name: '白宁', password: 'szsf@zfjd'}, |
|||
{account: 'caizifan', name: '蔡梓帆', password: 'szsf@zfjd'}, |
|||
{account: 'chenchong4', name: '陈冲', password: 'szsf@zfjd'}, |
|||
{account: 'chenggaozhan', name: '程高展', password: 'szsf@zfjd'}, |
|||
{account: 'chenhaitao2', name: '陈海涛', password: 'szsf@zfjd'}, |
|||
{account: 'chenhanyu', name: '陈汉羽', password: 'szsf@zfjd'} |
|||
|
|||
]; |
|||
|
|||
const selectTestUser = (item) => { |
|||
loginParam.secretName = item.account; |
|||
loginParam.secretKey = item.password; |
|||
showTestPicker.value = false; |
|||
// 直接触发登录 |
|||
doLogin(item.account, item.password); |
|||
} |
|||
const doLogin = (secretName, secretKey) => { |
|||
const encryptedKey = Encrypt(secretKey); |
|||
enforcementLoginByPassword({"secretName": secretName, "secretKey": encryptedKey}, response => { |
|||
if (response.code === 200) { |
|||
// 验证关键组织信息 |
|||
if (!response.data.deptId) { |
|||
showFailToast("该用户未关联部门,无法登录"); |
|||
return; |
|||
} |
|||
if (!response.data.organId) { |
|||
showFailToast("该用户未关联认证机构,无法登录"); |
|||
return; |
|||
} |
|||
if (!response.data.bureauUnitId) { |
|||
showFailToast("该用户未关联执法单位,无法登录"); |
|||
return; |
|||
} |
|||
// 执法端登录成功 → 赋值用户基本信息 |
|||
useCommon.SET_USER_PHONENUMBER(response.data.phonenumber || ""); |
|||
useCommon.SET_USER_ID(response.data.userId); |
|||
useCommon.SET_USER_NAME(response.data.userName || ""); |
|||
useCommon.SET_NICK_NAME(response.data.nickName || ""); |
|||
useCommon.SET_TOKEN(response.data.token); |
|||
useCommon.SET_DEPT_ID(response.data.deptId); |
|||
useCommon.SET_DEPT_NAME(response.data.deptName || ""); |
|||
useCommon.SET_ORGAN_ID(response.data.organId); |
|||
useCommon.SET_ORGAN_NAME(response.data.organName || ""); |
|||
useCommon.SET_BUREAU_UNIT_ID(response.data.bureauUnitId); |
|||
useCommon.SET_BUREAU_UNIT_NAME(response.data.bureauUnitName || ""); |
|||
console.log("enforcement login success", useCommon.userId); |
|||
router.push({name: "enforcementHome"}); |
|||
} else { |
|||
showFailToast(response.msg || "登录失败"); |
|||
} |
|||
}, error => { |
|||
showFailToast(error?.msg || "登录失败,请检查网络"); |
|||
}) |
|||
}; |
|||
|
|||
const onSubmit = async () => { |
|||
doLogin(loginParam.secretName, loginParam.secretKey); |
|||
}; |
|||
|
|||
const Encrypt = (passWord) => { |
|||
var keyStr = '1234567898745621'; |
|||
var ivStr = keyStr; |
|||
var key = CryptoJS.enc.Utf8.parse(keyStr); |
|||
var iv = CryptoJS.enc.Utf8.parse(ivStr); |
|||
var srcs = CryptoJS.enc.Utf8.parse(passWord); |
|||
var encrypted = CryptoJS.AES.encrypt(srcs, key, { |
|||
iv: iv, |
|||
mode: CryptoJS.mode.CBC, |
|||
padding: CryptoJS.pad.ZeroPadding |
|||
}); |
|||
return CryptoJS.enc.Base64.stringify(encrypted.ciphertext); |
|||
} |
|||
</script> |
|||
|
|||
<style lang="less" scoped> |
|||
.login-page { |
|||
width: 100%; |
|||
height: 100%; |
|||
display: flex; |
|||
overflow: hidden; |
|||
background-image: url("~@/assets/images/login.jpg"); |
|||
background-repeat: no-repeat; |
|||
background-size: cover; |
|||
opacity: 0.9; |
|||
|
|||
.login-content { |
|||
margin: auto; |
|||
padding: 0.27rem 0.27rem 0.52rem 0.27rem; |
|||
background-color: #fff; |
|||
border-radius: 0.27rem; |
|||
box-shadow: 0 0 0.27rem rgba(0, 0, 0, 0.2); |
|||
opacity: 0.9; |
|||
|
|||
.title { |
|||
font-size: 0.52rem; |
|||
font-weight: 600; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.test-enterprise-list { |
|||
max-height: 60vh; |
|||
overflow-y: auto; |
|||
padding: 0.2rem; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,392 @@ |
|||
<template> |
|||
<div class="detail-page"> |
|||
<!-- 顶部导航 --> |
|||
<div class="nav-bar"> |
|||
<div class="nav-back" @click="onBack"> |
|||
<van-icon name="arrow-left" size="20" color="#333" /> |
|||
</div> |
|||
<div class="nav-title">任务详情</div> |
|||
<div class="nav-right"></div> |
|||
</div> |
|||
|
|||
<!-- 加载中 --> |
|||
<div v-if="loading" class="detail-loading"> |
|||
<van-loading size="24" color="#2382cf" /> |
|||
</div> |
|||
|
|||
<!-- 详情内容 --> |
|||
<div v-else-if="detail" class="detail-content"> |
|||
<!-- 顶部标题区 --> |
|||
<div class="header-card"> |
|||
<div class="header-top"> |
|||
<span class="plan-type-tag">【{{ planTypeText }}】</span> |
|||
<span v-if="detail.isUnion === 1" class="union-badge">联合执法</span> |
|||
</div> |
|||
<span class="header-title">{{ detail.enforceContent }}</span> |
|||
</div> |
|||
|
|||
<!-- 基本信息卡片 --> |
|||
<div class="detail-card"> |
|||
<div class="card-header"> |
|||
<div class="header-icon-wrap green"> |
|||
<van-icon name="info-o" size="15" color="#fff" /> |
|||
</div> |
|||
<span>执法计划</span> |
|||
</div> |
|||
<div class="info-list"> |
|||
<div class="info-item"> |
|||
<span class="info-label">计划日期</span> |
|||
<span class="info-value">{{ formatDate(detail.planDate) }}</span> |
|||
</div> |
|||
<div class="info-item"> |
|||
<span class="info-label">执行时段</span> |
|||
<span class="info-value">{{ timeRangeText }}</span> |
|||
</div> |
|||
<div v-if="detail.planActualDate && (formatDate(detail.planActualDate) !== formatDate(detail.planDate) || detail.planActualTime !== detail.planTime)" class="info-item highlight"> |
|||
<span class="info-label">实际日期</span> |
|||
<span class="info-value">{{ formatDate(detail.planActualDate) }} {{ actualTimeRangeText }}</span> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 企业信息卡片 --> |
|||
<div class="detail-card"> |
|||
<div class="card-header"> |
|||
<div class="header-icon-wrap orange"> |
|||
<van-icon name="wap-home-o" size="15" color="#fff" /> |
|||
</div> |
|||
<span>检查对象</span> |
|||
</div> |
|||
<div class="info-list"> |
|||
<div class="info-item"> |
|||
<span class="info-label">企业名称</span> |
|||
<span class="info-value bold">{{ detail.enterpriseName || '-' }}</span> |
|||
</div> |
|||
<div v-if="detail.enterpriseNumber" class="info-item"> |
|||
<span class="info-label">信用代码</span> |
|||
<span class="info-value bold">{{ detail.enterpriseNumber }}</span> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 执法人员卡片 --> |
|||
<div class="detail-card"> |
|||
<div class="card-header"> |
|||
<div class="header-icon-wrap purple"> |
|||
<van-icon name="friends-o" size="15" color="#fff" /> |
|||
</div> |
|||
<span>执法人员</span> |
|||
</div> |
|||
<div class="info-list"> |
|||
<div class="info-item"> |
|||
<span class="info-label">执法单位</span> |
|||
<span class="info-value">{{ detail.bureauDeptName || '-' }}</span> |
|||
</div> |
|||
<div class="info-item"> |
|||
<span class="info-label">执法人员</span> |
|||
<span class="info-value">{{ detail.enforceUserNames || '-' }}</span> |
|||
</div> |
|||
<div v-if="detail.isUnion === 1 && detail.unionBureauDeptNames" class="info-item"> |
|||
<span class="info-label">联合单位</span> |
|||
<span class="info-value">{{ detail.unionBureauDeptNames }}</span> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 备注卡片 --> |
|||
<div v-if="detail.remark" class="detail-card"> |
|||
<div class="card-header"> |
|||
<div class="header-icon-wrap gray"> |
|||
<van-icon name="edit" size="15" color="#fff" /> |
|||
</div> |
|||
<span>备注</span> |
|||
</div> |
|||
<div class="content-block"> |
|||
{{ detail.remark }} |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 空状态 --> |
|||
<div v-else class="detail-empty"> |
|||
<van-empty description="未找到任务信息" /> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup name="cpTaskDetail"> |
|||
import { useRouter, useRoute } from 'vue-router'; |
|||
import { getMyTaskDetail } from '@/api/enforcement/task'; |
|||
|
|||
const router = useRouter(); |
|||
const route = useRoute(); |
|||
|
|||
const loading = ref(true); |
|||
const detail = ref(null); |
|||
|
|||
// 返回 |
|||
const onBack = () => { |
|||
router.back(); |
|||
}; |
|||
|
|||
// 格式化日期 |
|||
const formatDate = (d) => { |
|||
if (!d) return '-'; |
|||
if (typeof d === 'string') return d.substring(0, 10); |
|||
const date = new Date(d); |
|||
const y = date.getFullYear(); |
|||
const m = String(date.getMonth() + 1).padStart(2, '0'); |
|||
const day = String(date.getDate()).padStart(2, '0'); |
|||
return `${y}-${m}-${day}`; |
|||
}; |
|||
|
|||
// 时段文字 |
|||
const getTimeText = (time) => { |
|||
if (time === 1) return '上午 (09:00-12:00)'; |
|||
if (time === 2) return '下午 (14:00-18:00)'; |
|||
return '其他'; |
|||
}; |
|||
|
|||
const timeRangeText = computed(() => getTimeText(detail.value?.planTime)); |
|||
const actualTimeRangeText = computed(() => getTimeText(detail.value?.planActualTime)); |
|||
|
|||
// 计划类型 |
|||
const planTypeText = computed(() => { |
|||
const t = detail.value?.planType; |
|||
if (t === 1) return '日常计划'; |
|||
if (t === 2) return '专项计划'; |
|||
return '其他'; |
|||
}); |
|||
|
|||
|
|||
const statusClass = computed(() => { |
|||
const s = detail.value?.reserveStatus; |
|||
if (s === 3) return 'status-done'; |
|||
if (s === 2) return 'status-progress'; |
|||
if (s === 1) return 'status-pending'; |
|||
return 'status-draft'; |
|||
}); |
|||
|
|||
// 加载详情 |
|||
const loadDetail = () => { |
|||
const planId = route.query.planId; |
|||
if (!planId) { |
|||
loading.value = false; |
|||
return; |
|||
} |
|||
loading.value = true; |
|||
getMyTaskDetail(planId, (res) => { |
|||
loading.value = false; |
|||
if (res.code === 200 && res.data) { |
|||
detail.value = res.data; |
|||
} |
|||
}, () => { |
|||
loading.value = false; |
|||
}); |
|||
}; |
|||
|
|||
onMounted(() => { |
|||
loadDetail(); |
|||
}); |
|||
</script> |
|||
|
|||
<style lang="less" scoped> |
|||
.detail-page { |
|||
display: flex; |
|||
flex-direction: column; |
|||
height: 100%; |
|||
background: #f4f7fc; |
|||
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif; |
|||
} |
|||
|
|||
/* 导航栏 */ |
|||
.nav-bar { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
padding: 14px 16px; |
|||
background: linear-gradient(135deg, #2382cf, #1a6db5); |
|||
flex-shrink: 0; |
|||
|
|||
.nav-back { |
|||
width: 32px; |
|||
cursor: pointer; |
|||
:deep(.van-icon) { |
|||
color: #fff !important; |
|||
} |
|||
} |
|||
.nav-title { |
|||
font-size: 17px; |
|||
font-weight: 600; |
|||
color: #fff; |
|||
} |
|||
.nav-right { |
|||
width: 32px; |
|||
} |
|||
} |
|||
|
|||
.detail-loading { |
|||
flex: 1; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
} |
|||
|
|||
.detail-empty { |
|||
flex: 1; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
} |
|||
|
|||
/* 详情内容 */ |
|||
.detail-content { |
|||
flex: 1; |
|||
overflow-y: auto; |
|||
padding: 0 14px 24px; |
|||
} |
|||
|
|||
/* 顶部标题区 */ |
|||
.header-card { |
|||
background: linear-gradient(135deg, #2382cf, #1a6db5); |
|||
border-radius: 16px 16px 16px 16px; |
|||
padding: 20px 18px 22px; |
|||
margin: 8px 0px; |
|||
color: #fff; |
|||
|
|||
.header-top { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 8px; |
|||
margin-bottom: 10px; |
|||
} |
|||
.plan-type-tag { |
|||
font-size: 14px; |
|||
padding: 6px 10px; |
|||
border-radius: 10px; |
|||
background: rgba(255,255,255,0.2); |
|||
color: #fff; |
|||
font-weight: 500; |
|||
} |
|||
.union-badge { |
|||
font-size: 11px; |
|||
padding: 2px 10px; |
|||
border-radius: 10px; |
|||
background: linear-gradient(135deg, #f5a623, #f7c948); |
|||
color: #fff; |
|||
font-weight: 600; |
|||
} |
|||
.header-title { |
|||
font-size: 18px; |
|||
font-weight: 700; |
|||
line-height: 1.4; |
|||
margin-bottom: 8px; |
|||
} |
|||
.header-meta { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 5px; |
|||
font-size: 13px; |
|||
color: rgba(255,255,255,0.85); |
|||
} |
|||
} |
|||
|
|||
/* 卡片 */ |
|||
.detail-card { |
|||
background: #fff; |
|||
border-radius: 12px; |
|||
padding: 16px; |
|||
margin-bottom: 12px; |
|||
box-shadow: 0 2px 8px rgba(35, 130, 207, 0.06); |
|||
} |
|||
|
|||
/* 卡片头部 */ |
|||
.card-header { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 8px; |
|||
font-size: 15px; |
|||
font-weight: 600; |
|||
color: #1a1a2e; |
|||
margin-bottom: 14px; |
|||
|
|||
.header-icon-wrap { |
|||
width: 26px; |
|||
height: 26px; |
|||
border-radius: 7px; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
flex-shrink: 0; |
|||
|
|||
&.blue { background: linear-gradient(135deg, #2382cf, #5ba8e6); } |
|||
&.green { background: linear-gradient(135deg, #36b37e, #57d9a3); } |
|||
&.orange { background: linear-gradient(135deg, #f5a623, #f7c948); } |
|||
&.purple { background: linear-gradient(135deg, #7b68ee, #a78bfa); } |
|||
&.gray { background: linear-gradient(135deg, #8e99a4, #b0bac5); } |
|||
} |
|||
} |
|||
|
|||
/* 信息列表 */ |
|||
.info-list { |
|||
.info-item { |
|||
display: flex; |
|||
padding: 8px 0; |
|||
border-bottom: 1px dashed #f2f3f5; |
|||
|
|||
&:last-child { |
|||
border-bottom: none; |
|||
padding-bottom: 0; |
|||
} |
|||
&:first-child { |
|||
padding-top: 0; |
|||
} |
|||
&.highlight { |
|||
background: #fef9ed; |
|||
margin: 4px -8px; |
|||
padding: 8px; |
|||
border-radius: 6px; |
|||
border-bottom: none; |
|||
} |
|||
|
|||
.info-label { |
|||
flex-shrink: 0; |
|||
width: 68px; |
|||
font-size: 13px; |
|||
color: #8c8c8c; |
|||
line-height: 1.6; |
|||
} |
|||
.info-value { |
|||
flex: 1; |
|||
font-size: 13px; |
|||
color: #333; |
|||
word-break: break-all; |
|||
line-height: 1.6; |
|||
|
|||
&.bold { |
|||
font-weight: 600; |
|||
color: #1a1a2e; |
|||
} |
|||
&.mono { |
|||
font-family: 'SF Mono', 'Menlo', monospace; |
|||
font-size: 12px; |
|||
color: #666; |
|||
letter-spacing: 0.3px; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
/* 内容块 */ |
|||
.content-block { |
|||
font-size: 13px; |
|||
color: #444; |
|||
line-height: 1.8; |
|||
word-break: break-all; |
|||
white-space: pre-wrap; |
|||
background: #f9fafb; |
|||
padding: 12px; |
|||
border-radius: 8px; |
|||
border-left: 3px solid #2382cf; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,578 @@ |
|||
<template> |
|||
<div class="task-page"> |
|||
<!-- 顶部导航 --> |
|||
<div class="nav-bar"> |
|||
<div class="nav-back" @click="onBack"> |
|||
<van-icon name="arrow-left" size="20" color="#333" /> |
|||
</div> |
|||
<div class="nav-title">我的任务</div> |
|||
<div class="nav-right"></div> |
|||
</div> |
|||
|
|||
<!-- 月份切换 --> |
|||
<div class="month-switch"> |
|||
<div class="month-arrow" @click="onPrevMonth"> |
|||
<van-icon name="arrow-left" size="16" color="#2382cf" /> |
|||
</div> |
|||
<div class="month-text" @click="showMonthPicker = true"> |
|||
{{ currentYear }}年{{ currentMonth }}月 |
|||
</div> |
|||
<div class="month-arrow" @click="onNextMonth"> |
|||
<van-icon name="arrow" size="16" color="#2382cf" /> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 日历 --> |
|||
<div class="calendar"> |
|||
<div class="week-header"> |
|||
<div class="week-cell" v-for="w in weekDays" :key="w">{{ w }}</div> |
|||
</div> |
|||
<div class="date-grid"> |
|||
<div |
|||
v-for="(cell, idx) in calendarCells" |
|||
:key="idx" |
|||
class="date-cell" |
|||
:class="{ |
|||
'other-month': !cell.isCurrentMonth, |
|||
'is-today': cell.isToday, |
|||
'is-selected': cell.isSelected, |
|||
}" |
|||
@click="onSelectDate(cell)" |
|||
> |
|||
<span class="date-num">{{ cell.day }}</span> |
|||
<span class="date-lunar">{{ cell.lunar }}</span> |
|||
<span v-if="cell.hasDot && cell.isCurrentMonth" class="date-dot"></span> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 任务数量 --> |
|||
<div class="task-count"> |
|||
今天共 <span class="count-num">{{ taskList.length }}</span> 个任务 |
|||
</div> |
|||
|
|||
<!-- 任务列表 --> |
|||
<div class="task-list-wrap"> |
|||
<div v-if="loading" class="task-loading"> |
|||
<van-loading size="24" color="#2382cf" /> |
|||
</div> |
|||
<div v-else-if="taskList.length === 0" class="task-empty"> |
|||
<van-empty description="当天暂无任务" /> |
|||
</div> |
|||
<div v-else class="task-list"> |
|||
<div |
|||
v-for="item in taskList" |
|||
:key="item.id" |
|||
class="task-card" |
|||
@click="onTaskClick(item)" |
|||
> |
|||
<div class="task-time-col"> |
|||
<div class="time-start">{{ getTimeStart(item) }}</div> |
|||
<div class="time-end">{{ getTimeEnd(item) }}</div> |
|||
</div> |
|||
<div class="task-info-col"> |
|||
<div class="task-title-row"> |
|||
<span class="task-title">{{ item.enforceContent }}</span> |
|||
<span v-if="item.isUnion === 1" class="union-badge">联</span> |
|||
</div> |
|||
<div class="task-detail"> |
|||
<van-icon name="wap-home-o" size="14" /> |
|||
<span>{{ item.enterpriseName }}</span> |
|||
</div> |
|||
<div class="task-detail"> |
|||
<van-icon name="clock-o" size="14" color="#999" /> |
|||
<span>{{ formatPlanDate(item) }} {{ getTimeRange(item) }}</span> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 月份选择弹窗 --> |
|||
<van-popup v-model:show="showMonthPicker" position="bottom" round> |
|||
<van-date-picker |
|||
v-model="monthPickerValue" |
|||
title="选择月份" |
|||
:columns-type="['year', 'month']" |
|||
@confirm="onMonthPickerConfirm" |
|||
@cancel="showMonthPicker = false" |
|||
/> |
|||
</van-popup> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup name="cpTaskMine"> |
|||
import { useRouter } from 'vue-router'; |
|||
import { getMyTaskList, getMyTaskDates } from '@/api/enforcement/task'; |
|||
|
|||
const router = useRouter(); |
|||
|
|||
// ========== 日历相关 ========== |
|||
const weekDays = ['日', '一', '二', '三', '四', '五', '六']; |
|||
const today = new Date(); |
|||
const currentYear = ref(today.getFullYear()); |
|||
const currentMonth = ref(today.getMonth() + 1); |
|||
const selectedDate = ref(new Date(today.getFullYear(), today.getMonth(), today.getDate())); |
|||
const taskDatesSet = ref(new Set()); |
|||
|
|||
// 月份选择器 |
|||
const showMonthPicker = ref(false); |
|||
const monthPickerValue = ref([ |
|||
String(today.getFullYear()), |
|||
String(today.getMonth() + 1).padStart(2, '0'), |
|||
]); |
|||
|
|||
// 简易农历(仅用于展示,不做精确计算) |
|||
const lunarDays = [ |
|||
'', '初一', '初二', '初三', '初四', '初五', '初六', '初七', '初八', '初九', '初十', |
|||
'十一', '十二', '十三', '十四', '十五', '十六', '十七', '十八', '十九', '二十', |
|||
'廿一', '廿二', '廿三', '廿四', '廿五', '廿六', '廿七', '廿八', '廿九', '三十', |
|||
]; |
|||
|
|||
const getLunarDay = (date) => { |
|||
// 简化处理:使用日期的日数模拟农历显示 |
|||
const d = date.getDate(); |
|||
return lunarDays[d] || ''; |
|||
}; |
|||
|
|||
// 生成日历单元格 |
|||
const calendarCells = computed(() => { |
|||
const year = currentYear.value; |
|||
const month = currentMonth.value; |
|||
const firstDay = new Date(year, month - 1, 1); |
|||
const lastDay = new Date(year, month, 0); |
|||
const startWeekDay = firstDay.getDay(); // 0=Sunday |
|||
const totalDays = lastDay.getDate(); |
|||
|
|||
const cells = []; |
|||
|
|||
// 上个月补位 |
|||
const prevMonthLastDay = new Date(year, month - 1, 0).getDate(); |
|||
for (let i = startWeekDay - 1; i >= 0; i--) { |
|||
const day = prevMonthLastDay - i; |
|||
const d = new Date(year, month - 2, day); |
|||
cells.push({ |
|||
day, |
|||
date: d, |
|||
lunar: getLunarDay(d), |
|||
isCurrentMonth: false, |
|||
isToday: false, |
|||
isSelected: false, |
|||
hasDot: false, |
|||
}); |
|||
} |
|||
|
|||
// 当月 |
|||
for (let day = 1; day <= totalDays; day++) { |
|||
const d = new Date(year, month - 1, day); |
|||
const dateStr = formatDateStr(d); |
|||
const isToday = dateStr === formatDateStr(today); |
|||
const isSelected = dateStr === formatDateStr(selectedDate.value); |
|||
cells.push({ |
|||
day, |
|||
date: d, |
|||
lunar: getLunarDay(d), |
|||
isCurrentMonth: true, |
|||
isToday, |
|||
isSelected, |
|||
hasDot: taskDatesSet.value.has(dateStr), |
|||
}); |
|||
} |
|||
|
|||
// 下个月补位(补满6行) |
|||
const remaining = 42 - cells.length; |
|||
for (let day = 1; day <= remaining; day++) { |
|||
const d = new Date(year, month, day); |
|||
cells.push({ |
|||
day, |
|||
date: d, |
|||
lunar: getLunarDay(d), |
|||
isCurrentMonth: false, |
|||
isToday: false, |
|||
isSelected: false, |
|||
hasDot: false, |
|||
}); |
|||
} |
|||
|
|||
return cells; |
|||
}); |
|||
|
|||
const formatDateStr = (d) => { |
|||
const y = d.getFullYear(); |
|||
const m = String(d.getMonth() + 1).padStart(2, '0'); |
|||
const day = String(d.getDate()).padStart(2, '0'); |
|||
return `${y}-${m}-${day}`; |
|||
}; |
|||
|
|||
const formatYearMonth = () => { |
|||
return `${currentYear.value}-${String(currentMonth.value).padStart(2, '0')}`; |
|||
}; |
|||
|
|||
// 切换月份 |
|||
const onPrevMonth = () => { |
|||
if (currentMonth.value === 1) { |
|||
currentYear.value--; |
|||
currentMonth.value = 12; |
|||
} else { |
|||
currentMonth.value--; |
|||
} |
|||
loadTaskDates(); |
|||
}; |
|||
|
|||
const onNextMonth = () => { |
|||
if (currentMonth.value === 12) { |
|||
currentYear.value++; |
|||
currentMonth.value = 1; |
|||
} else { |
|||
currentMonth.value++; |
|||
} |
|||
loadTaskDates(); |
|||
}; |
|||
|
|||
// 月份选择器确认 |
|||
const onMonthPickerConfirm = ({ selectedValues }) => { |
|||
currentYear.value = parseInt(selectedValues[0]); |
|||
currentMonth.value = parseInt(selectedValues[1]); |
|||
showMonthPicker.value = false; |
|||
loadTaskDates(); |
|||
}; |
|||
|
|||
// 选择日期 |
|||
const onSelectDate = (cell) => { |
|||
if (!cell.isCurrentMonth) return; |
|||
selectedDate.value = cell.date; |
|||
loadTaskList(); |
|||
}; |
|||
|
|||
// 返回 |
|||
const onBack = () => { |
|||
router.back(); |
|||
}; |
|||
|
|||
// 点击任务卡片跳转详情 |
|||
const onTaskClick = (item) => { |
|||
router.push({ path: '/enforcement/task/detail', query: { planId: item.planId } }); |
|||
}; |
|||
|
|||
// ========== 任务列表 ========== |
|||
const taskList = ref([]); |
|||
const loading = ref(false); |
|||
|
|||
// 时段转时间显示 |
|||
const getTimeStart = (item) => { |
|||
const time = item.planActualTime || item.planTime; |
|||
if (time === 1) return '09:00'; |
|||
if (time === 2) return '14:00'; |
|||
return '00:00'; |
|||
}; |
|||
|
|||
const getTimeEnd = (item) => { |
|||
const time = item.planActualTime || item.planTime; |
|||
if (time === 1) return '12:00'; |
|||
if (time === 2) return '18:00'; |
|||
return '23:59'; |
|||
}; |
|||
|
|||
const getTimeRange = (item) => { |
|||
return `${getTimeStart(item)}-${getTimeEnd(item)}`; |
|||
}; |
|||
|
|||
const formatPlanDate = (item) => { |
|||
const d = item.planActualDate || item.planDate; |
|||
if (!d) return ''; |
|||
if (typeof d === 'string') return d.substring(0, 10); |
|||
const date = new Date(d); |
|||
return formatDateStr(date); |
|||
}; |
|||
|
|||
// 加载任务日期(月份打点) |
|||
const loadTaskDates = () => { |
|||
const ym = formatYearMonth(); |
|||
getMyTaskDates(ym, (res) => { |
|||
if (res.code === 200 && res.data) { |
|||
taskDatesSet.value = new Set(res.data); |
|||
} |
|||
}); |
|||
// 如果选中日期不在当月,自动选中当月1号 |
|||
if (selectedDate.value.getFullYear() !== currentYear.value |
|||
|| selectedDate.value.getMonth() + 1 !== currentMonth.value) { |
|||
selectedDate.value = new Date(currentYear.value, currentMonth.value - 1, 1); |
|||
} |
|||
loadTaskList(); |
|||
}; |
|||
|
|||
// 加载任务列表 |
|||
const loadTaskList = () => { |
|||
loading.value = true; |
|||
const dateStr = formatDateStr(selectedDate.value); |
|||
getMyTaskList( |
|||
{ planDate: dateStr }, |
|||
(res) => { |
|||
loading.value = false; |
|||
if (res.code === 200 && res.data) { |
|||
taskList.value = res.data; |
|||
} else { |
|||
taskList.value = []; |
|||
} |
|||
}, |
|||
() => { |
|||
loading.value = false; |
|||
taskList.value = []; |
|||
} |
|||
); |
|||
}; |
|||
|
|||
// 初始化 |
|||
onMounted(() => { |
|||
loadTaskDates(); |
|||
}); |
|||
</script> |
|||
|
|||
<style lang="less" scoped> |
|||
.task-page { |
|||
display: flex; |
|||
flex-direction: column; |
|||
height: 100%; |
|||
background: #f0f6ff; |
|||
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif; |
|||
} |
|||
|
|||
/* 导航栏 */ |
|||
.nav-bar { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
padding: 12px 16px; |
|||
background: #fff; |
|||
flex-shrink: 0; |
|||
|
|||
.nav-back { |
|||
width: 32px; |
|||
cursor: pointer; |
|||
} |
|||
.nav-title { |
|||
font-size: 18px; |
|||
font-weight: 600; |
|||
color: #1a1a2e; |
|||
} |
|||
.nav-right { |
|||
width: 32px; |
|||
} |
|||
} |
|||
|
|||
/* 月份切换 */ |
|||
.month-switch { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
gap: 20px; |
|||
padding: 8px 0; |
|||
background: #fff; |
|||
flex-shrink: 0; |
|||
|
|||
.month-arrow { |
|||
width: 24px; |
|||
height: 24px; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
cursor: pointer; |
|||
} |
|||
.month-text { |
|||
font-size: 15px; |
|||
font-weight: 600; |
|||
color: #2382cf; |
|||
cursor: pointer; |
|||
} |
|||
} |
|||
|
|||
/* 日历 */ |
|||
.calendar { |
|||
background: #fff; |
|||
padding: 0 8px 8px; |
|||
flex-shrink: 0; |
|||
} |
|||
|
|||
.week-header { |
|||
display: grid; |
|||
grid-template-columns: repeat(7, 1fr); |
|||
text-align: center; |
|||
padding: 4px 0; |
|||
|
|||
.week-cell { |
|||
font-size: 11px; |
|||
color: #999; |
|||
font-weight: 500; |
|||
} |
|||
} |
|||
|
|||
.date-grid { |
|||
display: grid; |
|||
grid-template-columns: repeat(7, 1fr); |
|||
gap: 1px 0; |
|||
} |
|||
|
|||
.date-cell { |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
justify-content: center; |
|||
padding: 3px 0; |
|||
min-height: 38px; |
|||
cursor: pointer; |
|||
position: relative; |
|||
border-radius: 6px; |
|||
transition: background 0.15s; |
|||
|
|||
.date-num { |
|||
font-size: 13px; |
|||
font-weight: 600; |
|||
color: #333; |
|||
line-height: 1.3; |
|||
} |
|||
|
|||
.date-lunar { |
|||
font-size: 9px; |
|||
color: #999; |
|||
line-height: 1.1; |
|||
} |
|||
|
|||
.date-dot { |
|||
width: 4px; |
|||
height: 4px; |
|||
border-radius: 50%; |
|||
background: #2382cf; |
|||
margin-top: 1px; |
|||
} |
|||
|
|||
&.other-month { |
|||
.date-num { color: #ccc; } |
|||
.date-lunar { color: #ddd; } |
|||
} |
|||
|
|||
&.is-today:not(.is-selected) { |
|||
.date-num { |
|||
color: #2382cf; |
|||
font-weight: 700; |
|||
} |
|||
} |
|||
|
|||
&.is-selected { |
|||
background: #2382cf; |
|||
.date-num { color: #fff; font-weight: 700; } |
|||
.date-lunar { color: rgba(255,255,255,0.8); } |
|||
.date-dot { background: #fff; } |
|||
} |
|||
} |
|||
|
|||
/* 任务计数 */ |
|||
.task-count { |
|||
padding: 12px 16px 8px; |
|||
font-size: 14px; |
|||
color: #666; |
|||
flex-shrink: 0; |
|||
|
|||
.count-num { |
|||
font-size: 18px; |
|||
font-weight: 700; |
|||
color: #2382cf; |
|||
} |
|||
} |
|||
|
|||
/* 任务列表 */ |
|||
.task-list-wrap { |
|||
flex: 1; |
|||
overflow-y: auto; |
|||
padding: 0 12px 16px; |
|||
} |
|||
|
|||
.task-loading { |
|||
display: flex; |
|||
justify-content: center; |
|||
padding: 40px 0; |
|||
} |
|||
|
|||
.task-list { |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 12px; |
|||
} |
|||
|
|||
.task-card { |
|||
display: flex; |
|||
background: #fff; |
|||
border-radius: 14px; |
|||
padding: 16px; |
|||
box-shadow: 0 2px 10px rgba(44, 116, 190, 0.07); |
|||
gap: 14px; |
|||
} |
|||
|
|||
.task-time-col { |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
justify-content: center; |
|||
min-width: 48px; |
|||
flex-shrink: 0; |
|||
|
|||
.time-start, .time-end { |
|||
font-size: 15px; |
|||
font-weight: 700; |
|||
color: #333; |
|||
line-height: 1.6; |
|||
} |
|||
} |
|||
|
|||
.task-info-col { |
|||
flex: 1; |
|||
min-width: 0; |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 6px; |
|||
} |
|||
|
|||
.task-title-row { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 6px; |
|||
} |
|||
|
|||
.task-title { |
|||
font-size: 15px; |
|||
font-weight: 600; |
|||
color: #1a1a2e; |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
white-space: nowrap; |
|||
flex: 1; |
|||
} |
|||
|
|||
.union-badge { |
|||
display: inline-flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
width: 22px; |
|||
height: 22px; |
|||
border-radius: 4px; |
|||
background: #2382cf; |
|||
color: #fff; |
|||
font-size: 12px; |
|||
font-weight: 600; |
|||
flex-shrink: 0; |
|||
} |
|||
|
|||
.task-detail { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 4px; |
|||
font-size: 13px; |
|||
color: #393838; |
|||
overflow: hidden; |
|||
|
|||
span { |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
white-space: nowrap; |
|||
} |
|||
} |
|||
</style> |
|||
@ -1,5 +1,6 @@ |
|||
# 执法端开发环境 |
|||
VITE_PORT = 12002 |
|||
VITE_BASE_URL = '/api/enf-ep' |
|||
VITE_DEFAULT_LOGIN = '/enforcement/login' |
|||
VITE_PROXY_TARGET = 'http://localhost:17000' |
|||
VITE_DEFAULT_LOGIN = '/enforcement/loginByPassword' |
|||
VITE_APP_ENV = 'dev' |
|||
|
|||
@ -1,5 +1,6 @@ |
|||
# 执法端生产环境 |
|||
VITE_PORT = 12002 |
|||
VITE_BASE_URL = 'http://localhost:17000/enf-ep' |
|||
VITE_PROXY_TARGET = 'http://localhost:17000' |
|||
VITE_DEFAULT_LOGIN = '/enforce/login' |
|||
VITE_APP_ENV = 'prod' |
|||
|
|||
@ -1,5 +1,6 @@ |
|||
# 执法端测试环境 |
|||
VITE_PORT = 12002 |
|||
VITE_BASE_URL = '/api' |
|||
VITE_PROXY_TARGET = 'http://139.186.148.21:8080' |
|||
VITE_DEFAULT_LOGIN = '/enforcement/login' |
|||
VITE_APP_ENV = 'test' |
|||
|
|||
@ -0,0 +1,354 @@ |
|||
package com.zdxt.rest.test.cp; |
|||
|
|||
import cn.dev33.satoken.context.mock.SaTokenContextMockUtil; |
|||
import cn.dev33.satoken.stp.StpUtil; |
|||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
|||
import com.baomidou.mybatisplus.core.toolkit.Wrappers; |
|||
import com.zdxt.enforcementcode.domain.EfcodeCpPlan; |
|||
import com.zdxt.enforcementcode.domain.EfcodeCpReserve; |
|||
import com.zdxt.enforcementcode.mapper.EfcodeCpPlanMapper; |
|||
import com.zdxt.enforcementcode.mapper.EfcodeCpReserveMapper; |
|||
import com.zdxt.enforcementcode.service.IEfcodeCpPlanService; |
|||
import org.dromara.ZdxtAdminApplication; |
|||
import org.junit.jupiter.api.*; |
|||
import org.junit.jupiter.api.Tag; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; |
|||
import org.springframework.boot.test.context.SpringBootTest; |
|||
import org.springframework.mock.web.MockHttpServletRequest; |
|||
import org.springframework.test.context.ActiveProfiles; |
|||
import org.springframework.web.context.request.RequestContextHolder; |
|||
import org.springframework.web.context.request.ServletRequestAttributes; |
|||
|
|||
import java.time.LocalDate; |
|||
import java.time.ZoneId; |
|||
import java.util.*; |
|||
import java.util.stream.Collectors; |
|||
|
|||
/** |
|||
* 计划自动匹配预约 autoMatchReserve 方法测试 |
|||
* <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…
Reference in new issue