定时任务增加系统定时任务
This commit is contained in:
@@ -275,3 +275,56 @@ INSERT INTO `tb_crontab_task_meta` (
|
|||||||
NOW()
|
NOW()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- 9. 热门资源推荐任务
|
||||||
|
INSERT INTO `tb_crontab_task_meta` (
|
||||||
|
`id`, `meta_id`, `name`, `description`, `category`,
|
||||||
|
`bean_name`, `method_name`, `script_path`, `param_schema`, `auto_publish`,
|
||||||
|
`sort_order`, `creator`, `create_time`
|
||||||
|
) VALUES (
|
||||||
|
'9',
|
||||||
|
'top_recommend_task',
|
||||||
|
'热门资源推荐',
|
||||||
|
'每天凌晨1点自动更新热门推荐资源(浏览量TOP10+最新发布TOP10)',
|
||||||
|
'系统内部任务',
|
||||||
|
'topRecommendTask',
|
||||||
|
'execute',
|
||||||
|
'',
|
||||||
|
'[]',
|
||||||
|
0,
|
||||||
|
9,
|
||||||
|
'system',
|
||||||
|
NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 创建热门资源推荐任务实例
|
||||||
|
INSERT INTO `tb_crontab_task` (
|
||||||
|
`id`, `task_id`, `meta_id`, `task_name`,`task_group`, `description`,`bean_name`,
|
||||||
|
`cron_expression`, `method_name`, `method_params`, `status`, `creator`, `create_time`
|
||||||
|
) VALUES (
|
||||||
|
'9',
|
||||||
|
'task_top_recommend_daily',
|
||||||
|
'top_recommend_task',
|
||||||
|
'每日热门资源推荐更新',
|
||||||
|
'系统内部任务',
|
||||||
|
'每天凌晨1点自动更新热门推荐资源列表',
|
||||||
|
'topRecommendTask',
|
||||||
|
'0 0 1 * * ?',
|
||||||
|
'execute',
|
||||||
|
'{}',
|
||||||
|
1,
|
||||||
|
'system',
|
||||||
|
NOW()
|
||||||
|
);
|
||||||
|
-- 赋予root用户和superadmin角色对热门资源推荐任务的读写执行权限
|
||||||
|
INSERT INTO `tb_resource_permission` (`id`, `resource_type`, `resource_id`, `dept_id`,
|
||||||
|
`role_id`, `can_read`, `can_write`, `can_execute`, `creator`, `updater`,
|
||||||
|
`create_time`, `update_time`, `delete_time`, `deleted`)
|
||||||
|
VALUES ('671f0c40642e6a69c2be9b6d7a4e986e', 7, 'task_top_recommend_daily', 'root_department',
|
||||||
|
'superadmin', 1, 1, 1, '1', NULL,
|
||||||
|
'2025-11-25 13:57:16', '2025-11-25 13:57:16', NULL, 0);
|
||||||
|
INSERT INTO `tb_resource_permission` (`id`, `resource_type`, `resource_id`, `dept_id`,
|
||||||
|
`role_id`, `can_read`, `can_write`, `can_execute`, `creator`, `updater`,
|
||||||
|
`create_time`, `update_time`, `delete_time`, `deleted`)
|
||||||
|
VALUES ('c365853b6a0e38a9c504962de4403e57', 7, 'task_top_recommend_daily', NULL, NULL,
|
||||||
|
1, 0, 0, '1', NULL,
|
||||||
|
'2025-11-25 13:57:16', '2025-11-25 13:57:16', NULL, 0);
|
||||||
|
|||||||
@@ -170,6 +170,7 @@ INSERT INTO `tb_sys_menu` VALUES
|
|||||||
('8001', 'menu_admin_meta_email_default', '默认接收人配置', 'menu_admin_crontab_manage', '/admin/manage/crontab/meta-email-default', 'admin/manage/crontab/MetaEmailDefaultView', NULL, 1, 0, 'SidebarLayout', '1', NULL, '2025-11-18 18:00:00', '2025-11-18 18:00:00', NULL, 0),
|
('8001', 'menu_admin_meta_email_default', '默认接收人配置', 'menu_admin_crontab_manage', '/admin/manage/crontab/meta-email-default', 'admin/manage/crontab/MetaEmailDefaultView', NULL, 1, 0, 'SidebarLayout', '1', NULL, '2025-11-18 18:00:00', '2025-11-18 18:00:00', NULL, 0),
|
||||||
('8002', 'menu_admin_crontab_log', '执行日志', 'menu_admin_crontab_manage', '/admin/manage/crontab/log', 'admin/manage/crontab/LogManagementView', NULL, 2, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0),
|
('8002', 'menu_admin_crontab_log', '执行日志', 'menu_admin_crontab_manage', '/admin/manage/crontab/log', 'admin/manage/crontab/LogManagementView', NULL, 2, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0),
|
||||||
('8003', 'menu_admin_news_crawler', '新闻爬虫配置', 'menu_admin_crontab_manage', '/admin/manage/crontab/news-crawler', 'admin/manage/crontab/NewsCrawlerView', NULL, 3, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0),
|
('8003', 'menu_admin_news_crawler', '新闻爬虫配置', 'menu_admin_crontab_manage', '/admin/manage/crontab/news-crawler', 'admin/manage/crontab/NewsCrawlerView', NULL, 3, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0),
|
||||||
|
('8004', 'menu_admin_system_task', '系统定时任务配置', 'menu_admin_crontab_manage', '/admin/manage/crontab/system-task', 'admin/manage/crontab/SystemTaskView', NULL, 4, 0, 'SidebarLayout', '1', NULL, '2025-11-25 13:45:00', '2025-11-25 13:45:00', NULL, 0),
|
||||||
-- 消息通知模块菜单 (9000-9999)
|
-- 消息通知模块菜单 (9000-9999)
|
||||||
('9001', 'menu_admin_message_manage', '消息管理', NULL, '/admin/manage/message', 'admin/manage/message/MessageManageView', 'admin/notice.svg', 9, 0, 'SidebarLayout', '1', NULL, '2025-11-13 10:00:00', '2025-11-13 10:00:00', NULL, 0),
|
('9001', 'menu_admin_message_manage', '消息管理', NULL, '/admin/manage/message', 'admin/manage/message/MessageManageView', 'admin/notice.svg', 9, 0, 'SidebarLayout', '1', NULL, '2025-11-13 10:00:00', '2025-11-13 10:00:00', NULL, 0),
|
||||||
-- 用户端消息中心菜单 (650-699)
|
-- 用户端消息中心菜单 (650-699)
|
||||||
@@ -238,6 +239,7 @@ INSERT INTO `tb_sys_menu_permission` (id, permission_id, menu_id, creator, creat
|
|||||||
('233', 'perm_crontab_manage', 'menu_admin_crontab_task', '1', now()),
|
('233', 'perm_crontab_manage', 'menu_admin_crontab_task', '1', now()),
|
||||||
('234', 'perm_crontab_manage', 'menu_admin_crontab_log', '1', now()),
|
('234', 'perm_crontab_manage', 'menu_admin_crontab_log', '1', now()),
|
||||||
('235', 'perm_crontab_manage', 'menu_admin_news_crawler', '1', now()),
|
('235', 'perm_crontab_manage', 'menu_admin_news_crawler', '1', now()),
|
||||||
|
('252', 'perm_crontab_manage', 'menu_admin_system_task', '1', now()),
|
||||||
|
|
||||||
-- 消息通知管理菜单权限关联
|
-- 消息通知管理菜单权限关联
|
||||||
('240', 'perm_message_manage', 'menu_admin_message_manage', '1', now()),
|
('240', 'perm_message_manage', 'menu_admin_message_manage', '1', now()),
|
||||||
|
|||||||
@@ -46,7 +46,8 @@ public interface ResourceService {
|
|||||||
* @author yslg
|
* @author yslg
|
||||||
* @since 2025-10-15
|
* @since 2025-10-15
|
||||||
*/
|
*/
|
||||||
ResultDomain<ResourceVO> getResourcePageOrderByViewCount(TbResource filter, PageParam pageParam);
|
ResultDomain<ResourceVO> getResourcePageOrder(TbResource filter, PageParam pageParam);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 根据ID获取资源详情
|
* @description 根据ID获取资源详情
|
||||||
* @param resourceID 资源ID
|
* @param resourceID 资源ID
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ public class CrontabController {
|
|||||||
public ResultDomain<TbCrontabTaskMeta> getEnabledCrontabList(@RequestParam(required = false) String param) {
|
public ResultDomain<TbCrontabTaskMeta> getEnabledCrontabList(@RequestParam(required = false) String param) {
|
||||||
try {
|
try {
|
||||||
// 从数据库查询所有任务元数据
|
// 从数据库查询所有任务元数据
|
||||||
ResultDomain<TbCrontabTaskMeta> result = taskMetaService.getAllTaskMeta();
|
ResultDomain<TbCrontabTaskMeta> result = taskMetaService.getTaskMetaByCategory(param);
|
||||||
result.getDataList().forEach(item->{
|
result.getDataList().forEach(item->{
|
||||||
item.setBeanName("");
|
item.setBeanName("");
|
||||||
item.setMethodName("");
|
item.setMethodName("");
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
package org.xyzh.crontab.task.recommendTask;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.xyzh.api.news.recommend.ResourceRecommendService;
|
||||||
|
import org.xyzh.api.news.resource.ResourceService;
|
||||||
|
import org.xyzh.common.core.domain.ResultDomain;
|
||||||
|
import org.xyzh.common.core.page.PageParam;
|
||||||
|
import org.xyzh.common.dto.BaseDTO;
|
||||||
|
import org.xyzh.common.dto.resource.TbResource;
|
||||||
|
import org.xyzh.common.dto.resource.TbResourceRecommend;
|
||||||
|
import org.xyzh.common.utils.IDUtils;
|
||||||
|
import org.xyzh.common.vo.ResourceVO;
|
||||||
|
import org.xyzh.crontab.pojo.TaskParams;
|
||||||
|
import org.xyzh.crontab.task.BaseTask;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 热门资源推荐定时任务 - 每天凌晨1点自动更新热门推荐
|
||||||
|
* @filename TopRecommendTask.java
|
||||||
|
* @author yslg
|
||||||
|
* @copyright xyzh
|
||||||
|
* @since 2025-11-25
|
||||||
|
*/
|
||||||
|
@Component("topRecommendTask")
|
||||||
|
public class TopRecommendTask extends BaseTask {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ResourceService resourceService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ResourceRecommendService resourceRecommendService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
protected void doExecute(TaskParams taskParams) throws Exception {
|
||||||
|
logger.info("开始执行热门资源推荐任务");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 清除旧的热门推荐(recommend_type = 1)
|
||||||
|
// clearOldRecommends();
|
||||||
|
|
||||||
|
// 2. 获取浏览量前10的资源
|
||||||
|
List<ResourceVO> topViewCountResources = getTopResourcesByViewCount(10);
|
||||||
|
logger.info("获取到浏览量前10的资源数量: {}", topViewCountResources.size());
|
||||||
|
|
||||||
|
// 3. 获取发布时间前10的资源
|
||||||
|
List<ResourceVO> topPublishTimeResources = getTopResourcesByPublishTime(10);
|
||||||
|
logger.info("获取到发布时间前10的资源数量: {}", topPublishTimeResources.size());
|
||||||
|
|
||||||
|
// 4. 合并并去重(使用 Set 确保资源ID唯一)
|
||||||
|
Set<String> resourceIds = new HashSet<>();
|
||||||
|
List<TbResourceRecommend> recommendList = new ArrayList<>();
|
||||||
|
int orderNum = 1;
|
||||||
|
|
||||||
|
// 添加浏览量前10
|
||||||
|
for (ResourceVO vo : topViewCountResources) {
|
||||||
|
if (vo.getResource() != null && resourceIds.add(vo.getResource().getResourceID())) {
|
||||||
|
recommendList.add(createRecommend(vo.getResource().getResourceID(), orderNum++));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加发布时间前10
|
||||||
|
for (ResourceVO vo : topPublishTimeResources) {
|
||||||
|
if (vo.getResource() != null && resourceIds.add(vo.getResource().getResourceID())) {
|
||||||
|
recommendList.add(createRecommend(vo.getResource().getResourceID(), orderNum++));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 批量插入新推荐
|
||||||
|
if (!recommendList.isEmpty()) {
|
||||||
|
for (TbResourceRecommend recommend : recommendList) {
|
||||||
|
ResultDomain<TbResourceRecommend> addResult = resourceRecommendService.addRecommend(recommend);
|
||||||
|
if (!addResult.isSuccess()) {
|
||||||
|
logger.warn("插入推荐失败: 资源ID={}, 原因={}", recommend.getResourceID(), addResult.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.info("成功插入{}条热门推荐记录", recommendList.size());
|
||||||
|
} else {
|
||||||
|
logger.warn("没有找到符合条件的资源,未插入推荐记录");
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("热门资源推荐任务执行成功,共推荐{}个资源", resourceIds.size());
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("热门资源推荐任务执行失败: {}", e.getMessage(), e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除旧的热门推荐记录
|
||||||
|
*/
|
||||||
|
private void clearOldRecommends() {
|
||||||
|
logger.info("清除旧的热门推荐记录(recommend_type = 1)");
|
||||||
|
// 暂时跳过清除逻辑,Service层方法待添加
|
||||||
|
// TODO: 实现 deleteRecommendsByType 方法后启用
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取浏览量前N的资源
|
||||||
|
*/
|
||||||
|
private List<ResourceVO> getTopResourcesByViewCount(int limit) {
|
||||||
|
TbResource filter = new TbResource();
|
||||||
|
filter.setStatus(1); // 只查询已发布的资源
|
||||||
|
|
||||||
|
// 设置排序:按浏览量降序
|
||||||
|
List<BaseDTO.OrderType> orderTypes = new ArrayList<>();
|
||||||
|
orderTypes.add(new BaseDTO.OrderType("view_count", "DESC"));
|
||||||
|
orderTypes.add(new BaseDTO.OrderType("publish_time", "DESC"));
|
||||||
|
filter.setOrderTypes(orderTypes);
|
||||||
|
|
||||||
|
PageParam pageParam = new PageParam();
|
||||||
|
pageParam.setPageSize(limit);
|
||||||
|
pageParam.setOffset(0L);
|
||||||
|
|
||||||
|
ResultDomain<ResourceVO> result = resourceService.getResourcePageOrder(filter, pageParam);
|
||||||
|
if (result.isSuccess() && result.getDataList() != null) {
|
||||||
|
return result.getDataList();
|
||||||
|
}
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取发布时间前N的资源(最新发布)
|
||||||
|
*/
|
||||||
|
private List<ResourceVO> getTopResourcesByPublishTime(int limit) {
|
||||||
|
TbResource filter = new TbResource();
|
||||||
|
filter.setStatus(1); // 只查询已发布的资源
|
||||||
|
|
||||||
|
// 设置排序:按发布时间降序
|
||||||
|
List<BaseDTO.OrderType> orderTypes = new ArrayList<>();
|
||||||
|
orderTypes.add(new BaseDTO.OrderType("publish_time", "DESC"));
|
||||||
|
orderTypes.add(new BaseDTO.OrderType("create_time", "DESC"));
|
||||||
|
filter.setOrderTypes(orderTypes);
|
||||||
|
|
||||||
|
PageParam pageParam = new PageParam();
|
||||||
|
pageParam.setPageSize(limit);
|
||||||
|
pageParam.setOffset(0L);
|
||||||
|
|
||||||
|
ResultDomain<ResourceVO> result = resourceService.getResourcePageOrder(filter, pageParam);
|
||||||
|
if (result.isSuccess() && result.getDataList() != null) {
|
||||||
|
return result.getDataList();
|
||||||
|
}
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建推荐记录
|
||||||
|
*/
|
||||||
|
private TbResourceRecommend createRecommend(String resourceId, int orderNum) {
|
||||||
|
TbResourceRecommend recommend = new TbResourceRecommend();
|
||||||
|
recommend.setID(IDUtils.generateID());
|
||||||
|
recommend.setResourceID(resourceId);
|
||||||
|
recommend.setRecommendType(1); // 1-热门资源推荐
|
||||||
|
recommend.setOrderNum(orderNum);
|
||||||
|
recommend.setCreateTime(new Date());
|
||||||
|
recommend.setUpdateTime(new Date());
|
||||||
|
recommend.setDeleted(false);
|
||||||
|
return recommend;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -274,7 +274,7 @@
|
|||||||
AND ct.task_name LIKE CONCAT('%', #{filter.taskName}, '%')
|
AND ct.task_name LIKE CONCAT('%', #{filter.taskName}, '%')
|
||||||
</if>
|
</if>
|
||||||
<if test="filter.taskGroup != null and filter.taskGroup != ''">
|
<if test="filter.taskGroup != null and filter.taskGroup != ''">
|
||||||
AND ct.task_group = #{filter.taskGroup}
|
AND ct.task_group LIKE CONCAT('%', #{filter.taskGroup}, '%')
|
||||||
</if>
|
</if>
|
||||||
<if test="filter.status != null">
|
<if test="filter.status != null">
|
||||||
AND ct.status = #{filter.status}
|
AND ct.status = #{filter.status}
|
||||||
@@ -337,7 +337,7 @@
|
|||||||
AND ct.task_name LIKE CONCAT('%', #{filter.taskName}, '%')
|
AND ct.task_name LIKE CONCAT('%', #{filter.taskName}, '%')
|
||||||
</if>
|
</if>
|
||||||
<if test="filter.taskGroup != null and filter.taskGroup != ''">
|
<if test="filter.taskGroup != null and filter.taskGroup != ''">
|
||||||
AND ct.task_group = #{filter.taskGroup}
|
AND ct.task_group LIKE CONCAT('%', #{filter.taskGroup}, '%')
|
||||||
</if>
|
</if>
|
||||||
<if test="filter.status != null">
|
<if test="filter.status != null">
|
||||||
AND ct.status = #{filter.status}
|
AND ct.status = #{filter.status}
|
||||||
|
|||||||
@@ -115,7 +115,7 @@
|
|||||||
<select id="selectTaskMetaByCategory" resultMap="BaseResultMap">
|
<select id="selectTaskMetaByCategory" resultMap="BaseResultMap">
|
||||||
SELECT <include refid="Base_Column_List" />
|
SELECT <include refid="Base_Column_List" />
|
||||||
FROM tb_crontab_task_meta
|
FROM tb_crontab_task_meta
|
||||||
WHERE category = #{category}
|
WHERE category LIKE CONCAT('%', #{category}, '%')
|
||||||
AND deleted = 0
|
AND deleted = 0
|
||||||
ORDER BY sort_order ASC
|
ORDER BY sort_order ASC
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ public class ResourceController {
|
|||||||
public ResultDomain<ResourceVO> getResourcePageOrderByViewCount(@RequestBody PageRequest<TbResource> request) {
|
public ResultDomain<ResourceVO> getResourcePageOrderByViewCount(@RequestBody PageRequest<TbResource> request) {
|
||||||
TbResource filter = request.getFilter();
|
TbResource filter = request.getFilter();
|
||||||
PageParam pageParam = request.getPageParam();
|
PageParam pageParam = request.getPageParam();
|
||||||
return resourceService.getResourcePageOrderByViewCount(filter, pageParam);
|
return resourceService.getResourcePageOrder(filter, pageParam);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ public interface ResourceMapper extends BaseMapper<TbResource> {
|
|||||||
* @author yslg
|
* @author yslg
|
||||||
* @since 2025-10-15
|
* @since 2025-10-15
|
||||||
*/
|
*/
|
||||||
List<ResourceVO> selectResourcesPageOrderByViewCount(@Param("filter") TbResource filter, @Param("pageParam") PageParam pageParam, @Param("userDeptRoles") List<UserDeptRoleVO> userDeptRoles);
|
List<ResourceVO> selectResourcesPageOrder(@Param("filter") TbResource filter, @Param("pageParam") PageParam pageParam, @Param("userDeptRoles") List<UserDeptRoleVO> userDeptRoles);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 统计资源总数
|
* @description 统计资源总数
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ public class NCResourceServiceImpl implements ResourceService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ResultDomain<ResourceVO> getResourcePageOrderByViewCount(TbResource filter, PageParam pageParam) {
|
public ResultDomain<ResourceVO> getResourcePageOrder(TbResource filter, PageParam pageParam) {
|
||||||
ResultDomain<ResourceVO> resultDomain = new ResultDomain<>();
|
ResultDomain<ResourceVO> resultDomain = new ResultDomain<>();
|
||||||
try {
|
try {
|
||||||
if (filter == null) {
|
if (filter == null) {
|
||||||
@@ -170,7 +170,7 @@ public class NCResourceServiceImpl implements ResourceService {
|
|||||||
// 获取当前用户的部门角色
|
// 获取当前用户的部门角色
|
||||||
List<UserDeptRoleVO> userDeptRoles = LoginUtil.getCurrentDeptRole();
|
List<UserDeptRoleVO> userDeptRoles = LoginUtil.getCurrentDeptRole();
|
||||||
// 直接查询ResourceVO列表
|
// 直接查询ResourceVO列表
|
||||||
List<ResourceVO> resourceVOList = resourceMapper.selectResourcesPageOrderByViewCount(filter, pageParam, userDeptRoles);
|
List<ResourceVO> resourceVOList = resourceMapper.selectResourcesPageOrder(filter, pageParam, userDeptRoles);
|
||||||
logger.info("资源数量{}",resourceVOList.size());
|
logger.info("资源数量{}",resourceVOList.size());
|
||||||
long total = resourceMapper.countResources(filter, userDeptRoles);
|
long total = resourceMapper.countResources(filter, userDeptRoles);
|
||||||
pageParam.setTotalElements(total);
|
pageParam.setTotalElements(total);
|
||||||
@@ -284,6 +284,7 @@ public class NCResourceServiceImpl implements ResourceService {
|
|||||||
// 进行审核
|
// 进行审核
|
||||||
ResultDomain<String> pass =auditService.auditText(resource.getTitle() + " "+resource.getContent());
|
ResultDomain<String> pass =auditService.auditText(resource.getTitle() + " "+resource.getContent());
|
||||||
if(pass.isSuccess()){
|
if(pass.isSuccess()){
|
||||||
|
resource.setPublishTime(new Date());
|
||||||
resource.setIsAudited(true);
|
resource.setIsAudited(true);
|
||||||
}else {
|
}else {
|
||||||
auditService.sendAuditResultMessage(resource.getCreator(), "文章"+resource.getTitle(), pass.getDataList());
|
auditService.sendAuditResultMessage(resource.getCreator(), "文章"+resource.getTitle(), pass.getDataList());
|
||||||
@@ -509,6 +510,7 @@ public class NCResourceServiceImpl implements ResourceService {
|
|||||||
ResultDomain<String> pass = auditService.auditText(resource.getTitle()+" "+resource.getContent());
|
ResultDomain<String> pass = auditService.auditText(resource.getTitle()+" "+resource.getContent());
|
||||||
if (pass.isSuccess()) {
|
if (pass.isSuccess()) {
|
||||||
resource.setIsAudited(true);
|
resource.setIsAudited(true);
|
||||||
|
resource.setPublishTime(new Date());
|
||||||
} else {
|
} else {
|
||||||
// 审核失败,标记状态为4(审核失败)
|
// 审核失败,标记状态为4(审核失败)
|
||||||
resource.setStatus(4);
|
resource.setStatus(4);
|
||||||
@@ -562,6 +564,7 @@ public class NCResourceServiceImpl implements ResourceService {
|
|||||||
ResultDomain<String> pass = auditService.auditText(resource.getTitle()+" "+resource.getContent());
|
ResultDomain<String> pass = auditService.auditText(resource.getTitle()+" "+resource.getContent());
|
||||||
if (pass.isSuccess()) {
|
if (pass.isSuccess()) {
|
||||||
resource.setIsAudited(true);
|
resource.setIsAudited(true);
|
||||||
|
resource.setPublishTime(new Date());
|
||||||
} else {
|
} else {
|
||||||
// 审核失败,标记状态为3(审核失败)
|
// 审核失败,标记状态为3(审核失败)
|
||||||
resource.setStatus(3);
|
resource.setStatus(3);
|
||||||
|
|||||||
@@ -378,7 +378,7 @@
|
|||||||
</association>
|
</association>
|
||||||
</resultMap>
|
</resultMap>
|
||||||
|
|
||||||
<select id="selectResourcesPageOrderByViewCount" resultMap="ResourceVOResultMap">
|
<select id="selectResourcesPageOrder" resultMap="ResourceVOResultMap">
|
||||||
SELECT
|
SELECT
|
||||||
r.*,
|
r.*,
|
||||||
MAX(CASE WHEN rec.recommend_type = 1 THEN 1 ELSE 0 END) AS is_top_recommend,
|
MAX(CASE WHEN rec.recommend_type = 1 THEN 1 ELSE 0 END) AS is_top_recommend,
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ export const crontabApi = {
|
|||||||
* 获取可创建的定时任务列表(从数据库获取任务元数据)
|
* 获取可创建的定时任务列表(从数据库获取任务元数据)
|
||||||
* @returns Promise<ResultDomain<TaskMeta>>
|
* @returns Promise<ResultDomain<TaskMeta>>
|
||||||
*/
|
*/
|
||||||
async getEnabledCrontabList(): Promise<ResultDomain<TaskMeta>> {
|
async getEnabledCrontabList(param: string): Promise<ResultDomain<TaskMeta>> {
|
||||||
const response = await api.get<TaskMeta>(`${this.baseUrl}/getEnabledCrontabList`);
|
const response = await api.get<TaskMeta>(`${this.baseUrl}/getEnabledCrontabList`, { param });
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -152,12 +152,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 分页 -->
|
<!-- 分页 -->
|
||||||
<div class="pagination-container" v-if="total > 0">
|
<div class="pagination-container" v-if="pageParam.totalElements! > 0">
|
||||||
<el-pagination
|
<el-pagination
|
||||||
v-model:current-page="pageParam.pageNumber"
|
v-model:current-page="pageParam.pageNumber"
|
||||||
v-model:page-size="pageParam.pageSize"
|
v-model:page-size="pageParam.pageSize"
|
||||||
:page-sizes="[10, 20, 50, 100]"
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
:total="total"
|
:total="pageParam.totalElements"
|
||||||
layout="total, sizes, prev, pager, next, jumper"
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
@size-change="handleSizeChange"
|
@size-change="handleSizeChange"
|
||||||
@current-change="handlePageChange"
|
@current-change="handlePageChange"
|
||||||
@@ -413,7 +413,6 @@ defineOptions({
|
|||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const submitting = ref(false);
|
const submitting = ref(false);
|
||||||
const crawlerList = ref<CrontabTask[]>([]);
|
const crawlerList = ref<CrontabTask[]>([]);
|
||||||
const total = ref(0);
|
|
||||||
|
|
||||||
// 爬虫元数据
|
// 爬虫元数据
|
||||||
const taskMetaList = ref<TaskMeta[]>([]);
|
const taskMetaList = ref<TaskMeta[]>([]);
|
||||||
@@ -440,7 +439,9 @@ const searchForm = reactive({
|
|||||||
// 分页参数
|
// 分页参数
|
||||||
const pageParam = reactive<PageParam>({
|
const pageParam = reactive<PageParam>({
|
||||||
pageNumber: 1,
|
pageNumber: 1,
|
||||||
pageSize: 10
|
pageSize: 10,
|
||||||
|
totalElements: 0,
|
||||||
|
totalPages: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
// 对话框状态
|
// 对话框状态
|
||||||
@@ -705,7 +706,7 @@ function resetUserSelector() {
|
|||||||
// 加载爬虫模板(从数据库加载TaskMeta,转换为CrontabItem结构)
|
// 加载爬虫模板(从数据库加载TaskMeta,转换为CrontabItem结构)
|
||||||
async function loadCrawlerTemplates() {
|
async function loadCrawlerTemplates() {
|
||||||
try {
|
try {
|
||||||
const result = await crontabApi.getEnabledCrontabList();
|
const result = await crontabApi.getEnabledCrontabList("新闻爬取");
|
||||||
if (result.success && result.dataList) {
|
if (result.success && result.dataList) {
|
||||||
taskMetaList.value = result.dataList;
|
taskMetaList.value = result.dataList;
|
||||||
|
|
||||||
@@ -746,7 +747,7 @@ async function loadCrawlerList() {
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const filter: Partial<CrontabTask> = {
|
const filter: Partial<CrontabTask> = {
|
||||||
taskGroup: ''
|
taskGroup: '新闻爬取'
|
||||||
};
|
};
|
||||||
if (searchForm.taskName) filter.taskName = searchForm.taskName;
|
if (searchForm.taskName) filter.taskName = searchForm.taskName;
|
||||||
if (searchForm.status !== undefined) filter.status = searchForm.status;
|
if (searchForm.status !== undefined) filter.status = searchForm.status;
|
||||||
@@ -754,25 +755,17 @@ async function loadCrawlerList() {
|
|||||||
const result = await crontabApi.getTaskPage(filter, pageParam);
|
const result = await crontabApi.getTaskPage(filter, pageParam);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
// 根据后端返回结构处理数据
|
// 根据后端返回结构处理数据
|
||||||
if (result.pageDomain) {
|
crawlerList.value = result.pageDomain?.dataList || [];
|
||||||
crawlerList.value = result.pageDomain.dataList || [];
|
if (result.pageDomain?.pageParam) {
|
||||||
total.value = result.pageDomain.pageParam?.totalElements || 0;
|
Object.assign(pageParam, result.pageDomain.pageParam);
|
||||||
} else if (result.dataList) {
|
|
||||||
crawlerList.value = result.dataList;
|
|
||||||
total.value = result.pageParam?.totalElements || 0;
|
|
||||||
} else {
|
|
||||||
crawlerList.value = [];
|
|
||||||
total.value = 0;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error(result.message || '加载爬虫列表失败');
|
ElMessage.error(result.message || '加载爬虫列表失败');
|
||||||
crawlerList.value = [];
|
crawlerList.value = [];
|
||||||
total.value = 0;
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error('加载爬虫列表失败');
|
ElMessage.error('加载爬虫列表失败');
|
||||||
crawlerList.value = [];
|
crawlerList.value = [];
|
||||||
total.value = 0;
|
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
@@ -1210,14 +1203,38 @@ onMounted(() => {
|
|||||||
.crawler-list {
|
.crawler-list {
|
||||||
min-height: 400px;
|
min-height: 400px;
|
||||||
|
|
||||||
.crawler-card {
|
// 让同一行的列等高
|
||||||
|
:deep(.el-row) {
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
// el-col 作为 flex 容器,使卡片能撑满高度
|
||||||
|
:deep(.el-col) {
|
||||||
|
display: flex;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crawler-card {
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
transform: translateY(-4px);
|
transform: translateY(-4px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(.el-card__body) {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-card__footer) {
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.card-header {
|
.card-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|||||||
714
schoolNewsWeb/src/views/admin/manage/crontab/SystemTaskView.vue
Normal file
714
schoolNewsWeb/src/views/admin/manage/crontab/SystemTaskView.vue
Normal file
@@ -0,0 +1,714 @@
|
|||||||
|
<template>
|
||||||
|
<AdminLayout title="系统定时任务" subtitle="系统定时任务配置">
|
||||||
|
<div class="news-crawler">
|
||||||
|
<div class="header">
|
||||||
|
<h2>系统定时任务配置</h2>
|
||||||
|
<el-button type="primary" @click="handleAddTask">
|
||||||
|
<el-icon><Plus /></el-icon>
|
||||||
|
新增任务
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 说明卡片 -->
|
||||||
|
<el-alert
|
||||||
|
title="系统定时任务说明"
|
||||||
|
type="info"
|
||||||
|
:closable="false"
|
||||||
|
style="margin-bottom: 20px"
|
||||||
|
>
|
||||||
|
<p>系统定时任务用于执行平台内部的各类系统作业,例如热门资源推荐等。</p>
|
||||||
|
<p>配置完成后,系统会按照设定的 Cron 表达式定时执行任务。</p>
|
||||||
|
</el-alert>
|
||||||
|
|
||||||
|
<!-- 搜索筛选区域 -->
|
||||||
|
<div class="search-bar">
|
||||||
|
<div class="search-item">
|
||||||
|
<span class="search-label">任务名称</span>
|
||||||
|
<el-input
|
||||||
|
v-model="searchForm.taskName"
|
||||||
|
placeholder="请输入任务名称"
|
||||||
|
clearable
|
||||||
|
@keyup.enter="loadTaskList"
|
||||||
|
style="width: 200px"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="search-item">
|
||||||
|
<span class="search-label">状态</span>
|
||||||
|
<el-select
|
||||||
|
v-model="searchForm.status"
|
||||||
|
placeholder="请选择状态"
|
||||||
|
clearable
|
||||||
|
style="width: 150px"
|
||||||
|
>
|
||||||
|
<el-option label="运行中" :value="1" />
|
||||||
|
<el-option label="已暂停" :value="0" />
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
<div class="search-actions">
|
||||||
|
<el-button type="primary" @click="loadTaskList">
|
||||||
|
<el-icon><Search /></el-icon>
|
||||||
|
搜索
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="handleReset">
|
||||||
|
<el-icon><Refresh /></el-icon>
|
||||||
|
重置
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<el-empty
|
||||||
|
v-if="!loading && taskList.length === 0"
|
||||||
|
description="暂无系统任务"
|
||||||
|
style="margin-top: 40px"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 系统任务列表 -->
|
||||||
|
<div v-else class="crawler-list">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="8" v-for="task in taskList" :key="task.taskId">
|
||||||
|
<el-card class="crawler-card" shadow="hover">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-title">
|
||||||
|
<el-icon class="title-icon"><Timer /></el-icon>
|
||||||
|
<span>{{ task.taskName }}</span>
|
||||||
|
</div>
|
||||||
|
<el-tag
|
||||||
|
:type="task.status === 1 ? 'success' : 'info'"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{{ task.status === 1 ? '运行中' : '已暂停' }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">定时任务类型:</span>
|
||||||
|
<span class="info-value">{{ task.taskGroup }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">定时任务:</span>
|
||||||
|
<span class="info-value">{{ task.metaName }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">执行周期:</span>
|
||||||
|
<span class="info-value">{{ task.cronExpression }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item" v-if="task.description">
|
||||||
|
<span class="info-label">描述:</span>
|
||||||
|
<span class="info-value">{{ task.description }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="card-actions">
|
||||||
|
<el-button
|
||||||
|
v-if="task.status === 0"
|
||||||
|
type="success"
|
||||||
|
size="small"
|
||||||
|
@click="handleToggleStatus(task)"
|
||||||
|
>
|
||||||
|
<el-icon><VideoPlay /></el-icon>
|
||||||
|
启动
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-else
|
||||||
|
type="warning"
|
||||||
|
size="small"
|
||||||
|
@click="handleToggleStatus(task)"
|
||||||
|
>
|
||||||
|
<el-icon><VideoPause /></el-icon>
|
||||||
|
暂停
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
@click="handleExecuteNow(task)"
|
||||||
|
>
|
||||||
|
<el-icon><Promotion /></el-icon>
|
||||||
|
执行一次
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
@click="handleEdit(task)"
|
||||||
|
>
|
||||||
|
<el-icon><Edit /></el-icon>
|
||||||
|
编辑
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
size="small"
|
||||||
|
@click="handleDelete(task)"
|
||||||
|
>
|
||||||
|
<el-icon><Delete /></el-icon>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pagination-container" v-if="pageParam.totalElements! > 0">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="pageParam.pageNumber"
|
||||||
|
v-model:page-size="pageParam.pageSize"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
:total="pageParam.totalElements"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@size-change="loadTaskList"
|
||||||
|
@current-change="loadTaskList"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- 新增/编辑任务对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
:title="dialogTitle"
|
||||||
|
width="600px"
|
||||||
|
@close="handleDialogClose"
|
||||||
|
>
|
||||||
|
<el-form
|
||||||
|
ref="taskFormRef"
|
||||||
|
:model="taskForm"
|
||||||
|
:rules="taskFormRules"
|
||||||
|
label-width="120px"
|
||||||
|
>
|
||||||
|
<el-form-item label="任务类型" prop="metaId">
|
||||||
|
<el-select
|
||||||
|
v-model="taskForm.metaId"
|
||||||
|
placeholder="请选择任务类型"
|
||||||
|
style="width: 100%"
|
||||||
|
@change="handleMetaChange"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="meta in taskMetaList"
|
||||||
|
:key="meta.metaId"
|
||||||
|
:label="meta.name"
|
||||||
|
:value="meta.metaId"
|
||||||
|
>
|
||||||
|
<span>{{ meta.name }}</span>
|
||||||
|
<span style="float: right; color: #8492a6; font-size: 12px">
|
||||||
|
{{ meta.description }}
|
||||||
|
</span>
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="任务名称" prop="taskName">
|
||||||
|
<el-input
|
||||||
|
v-model="taskForm.taskName"
|
||||||
|
placeholder="请输入任务名称"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="任务描述" prop="description">
|
||||||
|
<el-input
|
||||||
|
v-model="taskForm.description"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
placeholder="请输入任务描述"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="Cron表达式" prop="cronExpression">
|
||||||
|
<el-input
|
||||||
|
v-model="taskForm.cronExpression"
|
||||||
|
placeholder="例如:0 0 1 * * ?(每天凌晨1点)"
|
||||||
|
>
|
||||||
|
<template #append>
|
||||||
|
<el-popover
|
||||||
|
placement="bottom"
|
||||||
|
:width="300"
|
||||||
|
trigger="click"
|
||||||
|
>
|
||||||
|
<template #reference>
|
||||||
|
<el-button>示例</el-button>
|
||||||
|
</template>
|
||||||
|
<div class="cron-examples">
|
||||||
|
<div>0 0 1 * * ? - 每天凌晨1点</div>
|
||||||
|
<div>0 0 */2 * * ? - 每2小时</div>
|
||||||
|
<div>0 */30 * * * ? - 每30分钟</div>
|
||||||
|
<div>0 0 0 * * ? - 每天零点</div>
|
||||||
|
<div>0 0 12 * * ? - 每天中午12点</div>
|
||||||
|
</div>
|
||||||
|
</el-popover>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="任务参数" prop="methodParams">
|
||||||
|
<el-input
|
||||||
|
v-model="taskForm.methodParams"
|
||||||
|
type="textarea"
|
||||||
|
:rows="4"
|
||||||
|
placeholder="请输入JSON格式的参数,例如:{}"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="状态" prop="status">
|
||||||
|
<el-radio-group v-model="taskForm.status">
|
||||||
|
<el-radio :label="1">启用</el-radio>
|
||||||
|
<el-radio :label="0">禁用</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</AdminLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, onMounted } from 'vue';
|
||||||
|
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';
|
||||||
|
import {
|
||||||
|
Plus,
|
||||||
|
Search,
|
||||||
|
Refresh,
|
||||||
|
Timer,
|
||||||
|
VideoPlay,
|
||||||
|
VideoPause,
|
||||||
|
Promotion,
|
||||||
|
Edit,
|
||||||
|
Delete
|
||||||
|
} from '@element-plus/icons-vue';
|
||||||
|
import { crontabApi } from '@/apis/crontab';
|
||||||
|
import AdminLayout from '@/views/admin/AdminLayout.vue';
|
||||||
|
import type { CrontabTask, TaskMeta, PageParam, CreateTaskRequest } from '@/types';
|
||||||
|
|
||||||
|
// 搜索表单
|
||||||
|
const searchForm = reactive({
|
||||||
|
taskName: '',
|
||||||
|
status: undefined as number | undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
// 分页参数
|
||||||
|
const pageParam = reactive<PageParam>({
|
||||||
|
pageNumber: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
totalPages: 0,
|
||||||
|
totalElements: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
// 任务列表
|
||||||
|
const taskList = ref<CrontabTask[]>([]);
|
||||||
|
const taskMetaList = ref<TaskMeta[]>([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
// 对话框
|
||||||
|
const dialogVisible = ref(false);
|
||||||
|
const dialogTitle = ref('新增任务');
|
||||||
|
const isEdit = ref(false);
|
||||||
|
const taskFormRef = ref<FormInstance>();
|
||||||
|
|
||||||
|
// 任务表单
|
||||||
|
const taskForm = reactive<Partial<CrontabTask>>({
|
||||||
|
taskId: '',
|
||||||
|
metaId: '',
|
||||||
|
taskName: '',
|
||||||
|
description: '',
|
||||||
|
cronExpression: '0 0 1 * * ?',
|
||||||
|
methodParams: '{}',
|
||||||
|
status: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
// 表单验证规则
|
||||||
|
const taskFormRules: FormRules = {
|
||||||
|
metaId: [{ required: true, message: '请选择任务类型', trigger: 'change' }],
|
||||||
|
taskName: [{ required: true, message: '请输入任务名称', trigger: 'blur' }],
|
||||||
|
cronExpression: [{ required: true, message: '请输入Cron表达式', trigger: 'blur' }]
|
||||||
|
};
|
||||||
|
|
||||||
|
// 加载任务元数据列表
|
||||||
|
async function loadTaskMetaList() {
|
||||||
|
try {
|
||||||
|
const result = await crontabApi.getEnabledCrontabList('系统内部任务');
|
||||||
|
if (result.success && result.dataList) {
|
||||||
|
taskMetaList.value = result.dataList;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载任务元数据失败:', error);
|
||||||
|
ElMessage.error('加载任务类型失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载任务列表
|
||||||
|
async function loadTaskList() {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const filter: Partial<CrontabTask> = {
|
||||||
|
taskGroup: '系统内部任务'
|
||||||
|
};
|
||||||
|
if (searchForm.taskName) filter.taskName = searchForm.taskName;
|
||||||
|
if (searchForm.status !== undefined) filter.status = searchForm.status;
|
||||||
|
|
||||||
|
const result = await crontabApi.getTaskPage(filter, pageParam);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
taskList.value = result.pageDomain?.dataList || [];
|
||||||
|
if (result.pageDomain?.pageParam) {
|
||||||
|
Object.assign(pageParam, result.pageDomain.pageParam);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
taskList.value = [];
|
||||||
|
pageParam.totalElements = pageParam.totalElements;
|
||||||
|
pageParam.totalPages = pageParam.totalPages;
|
||||||
|
ElMessage.error(result.message || '加载任务列表失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载任务列表失败:', error);
|
||||||
|
ElMessage.error('加载任务列表失败');
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置搜索
|
||||||
|
function handleReset() {
|
||||||
|
searchForm.taskName = '';
|
||||||
|
searchForm.status = undefined;
|
||||||
|
pageParam.pageNumber = 1;
|
||||||
|
loadTaskList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增任务
|
||||||
|
function handleAddTask() {
|
||||||
|
isEdit.value = false;
|
||||||
|
dialogTitle.value = '新增系统任务';
|
||||||
|
Object.assign(taskForm, {
|
||||||
|
taskId: '',
|
||||||
|
metaId: '',
|
||||||
|
taskName: '',
|
||||||
|
description: '',
|
||||||
|
cronExpression: '0 0 1 * * ?',
|
||||||
|
methodParams: '{}',
|
||||||
|
status: 1
|
||||||
|
});
|
||||||
|
dialogVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑任务
|
||||||
|
function handleEdit(task: CrontabTask) {
|
||||||
|
isEdit.value = true;
|
||||||
|
dialogTitle.value = '编辑系统任务';
|
||||||
|
Object.assign(taskForm, {
|
||||||
|
id: task.id,
|
||||||
|
taskId: task.taskId,
|
||||||
|
metaId: task.metaId,
|
||||||
|
taskName: task.taskName,
|
||||||
|
description: task.description,
|
||||||
|
cronExpression: task.cronExpression,
|
||||||
|
methodParams: task.methodParams || '{}',
|
||||||
|
status: task.status
|
||||||
|
});
|
||||||
|
dialogVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 元数据变更
|
||||||
|
function handleMetaChange(metaId: string) {
|
||||||
|
const meta = taskMetaList.value.find(m => m.metaId === metaId);
|
||||||
|
if (meta) {
|
||||||
|
taskForm.taskName = meta.name;
|
||||||
|
taskForm.description = meta.description;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交表单
|
||||||
|
async function handleSubmit() {
|
||||||
|
if (!taskFormRef.value) return;
|
||||||
|
|
||||||
|
await taskFormRef.value.validate(async (valid) => {
|
||||||
|
if (!valid) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 验证JSON格式
|
||||||
|
try {
|
||||||
|
JSON.parse(taskForm.methodParams || '{}');
|
||||||
|
} catch {
|
||||||
|
ElMessage.error('任务参数必须是有效的JSON格式');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const meta = taskMetaList.value.find(m => m.metaId === taskForm.metaId);
|
||||||
|
if (!meta) {
|
||||||
|
ElMessage.error('未找到对应的任务类型');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const taskData: CrontabTask = {
|
||||||
|
...taskForm,
|
||||||
|
beanName: meta.beanName,
|
||||||
|
methodName: meta.methodName || 'execute',
|
||||||
|
methodParams: taskForm.methodParams || '{}'
|
||||||
|
} as CrontabTask;
|
||||||
|
|
||||||
|
const requestData: CreateTaskRequest = {
|
||||||
|
task: taskData,
|
||||||
|
metaId: taskForm.metaId!
|
||||||
|
};
|
||||||
|
|
||||||
|
let result;
|
||||||
|
if (isEdit.value) {
|
||||||
|
result = await crontabApi.updateTask(requestData);
|
||||||
|
} else {
|
||||||
|
result = await crontabApi.createTask(requestData);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
ElMessage.success(isEdit.value ? '更新成功' : '创建成功');
|
||||||
|
dialogVisible.value = false;
|
||||||
|
loadTaskList();
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result.message || '操作失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('提交失败:', error);
|
||||||
|
ElMessage.error('操作失败');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭对话框
|
||||||
|
function handleDialogClose() {
|
||||||
|
taskFormRef.value?.resetFields();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换状态
|
||||||
|
async function handleToggleStatus(task: CrontabTask) {
|
||||||
|
try {
|
||||||
|
let result;
|
||||||
|
if (task.status === 1) {
|
||||||
|
result = await crontabApi.pauseTask(task.taskId!);
|
||||||
|
} else {
|
||||||
|
result = await crontabApi.startTask(task.taskId!);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
ElMessage.success(task.status === 1 ? '已禁用' : '已启用');
|
||||||
|
loadTaskList();
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result.message || '操作失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('切换状态失败:', error);
|
||||||
|
ElMessage.error('操作失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 立即执行
|
||||||
|
async function handleExecuteNow(task: CrontabTask) {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`确定要立即执行任务"${task.taskName}"吗?`,
|
||||||
|
'提示',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await crontabApi.executeTaskOnce(task.taskId!);
|
||||||
|
if (result.success) {
|
||||||
|
ElMessage.success('任务已提交执行');
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result.message || '执行失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
console.error('执行任务失败:', error);
|
||||||
|
ElMessage.error('执行失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除任务
|
||||||
|
async function handleDelete(task: CrontabTask) {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`确定要删除任务"${task.taskName}"吗?此操作不可恢复。`,
|
||||||
|
'警告',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await crontabApi.deleteTask(task);
|
||||||
|
if (result.success) {
|
||||||
|
ElMessage.success('删除成功');
|
||||||
|
loadTaskList();
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result.message || '删除失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
console.error('删除任务失败:', error);
|
||||||
|
ElMessage.error('删除失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
onMounted(() => {
|
||||||
|
loadTaskMetaList();
|
||||||
|
loadTaskList();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.news-crawler {
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #ffffff;
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.search-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.search-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #606266;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.crawler-list {
|
||||||
|
min-height: 400px;
|
||||||
|
|
||||||
|
// 让同一行的列等高
|
||||||
|
:deep(.el-row) {
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
// el-col 作为 flex 容器,使卡片能撑满高度
|
||||||
|
:deep(.el-col) {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crawler-card {
|
||||||
|
transition: all 0.3s;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-card__body) {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-card__footer) {
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
|
||||||
|
.title-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content {
|
||||||
|
.info-item {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
min-width: 80px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
flex: 1;
|
||||||
|
color: #606266;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cron-examples {
|
||||||
|
div {
|
||||||
|
padding: 6px 0;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user