diff --git a/schoolNewsServ/.bin/mysql/sql/initCrontabMetaData.sql b/schoolNewsServ/.bin/mysql/sql/initCrontabMetaData.sql index 998e93c..a4183c3 100644 --- a/schoolNewsServ/.bin/mysql/sql/initCrontabMetaData.sql +++ b/schoolNewsServ/.bin/mysql/sql/initCrontabMetaData.sql @@ -275,3 +275,56 @@ INSERT INTO `tb_crontab_task_meta` ( 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); diff --git a/schoolNewsServ/.bin/mysql/sql/initMenuData.sql b/schoolNewsServ/.bin/mysql/sql/initMenuData.sql index 56cb959..ef8efc8 100644 --- a/schoolNewsServ/.bin/mysql/sql/initMenuData.sql +++ b/schoolNewsServ/.bin/mysql/sql/initMenuData.sql @@ -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), ('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), +('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) ('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) @@ -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()), ('234', 'perm_crontab_manage', 'menu_admin_crontab_log', '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()), diff --git a/schoolNewsServ/api/api-news/src/main/java/org/xyzh/api/news/resource/ResourceService.java b/schoolNewsServ/api/api-news/src/main/java/org/xyzh/api/news/resource/ResourceService.java index 8399ea4..f680bce 100644 --- a/schoolNewsServ/api/api-news/src/main/java/org/xyzh/api/news/resource/ResourceService.java +++ b/schoolNewsServ/api/api-news/src/main/java/org/xyzh/api/news/resource/ResourceService.java @@ -46,7 +46,8 @@ public interface ResourceService { * @author yslg * @since 2025-10-15 */ - ResultDomain getResourcePageOrderByViewCount(TbResource filter, PageParam pageParam); + ResultDomain getResourcePageOrder(TbResource filter, PageParam pageParam); + /** * @description 根据ID获取资源详情 * @param resourceID 资源ID diff --git a/schoolNewsServ/crontab/src/main/java/org/xyzh/crontab/controller/CrontabController.java b/schoolNewsServ/crontab/src/main/java/org/xyzh/crontab/controller/CrontabController.java index 78347d4..2991149 100644 --- a/schoolNewsServ/crontab/src/main/java/org/xyzh/crontab/controller/CrontabController.java +++ b/schoolNewsServ/crontab/src/main/java/org/xyzh/crontab/controller/CrontabController.java @@ -42,7 +42,7 @@ public class CrontabController { public ResultDomain getEnabledCrontabList(@RequestParam(required = false) String param) { try { // 从数据库查询所有任务元数据 - ResultDomain result = taskMetaService.getAllTaskMeta(); + ResultDomain result = taskMetaService.getTaskMetaByCategory(param); result.getDataList().forEach(item->{ item.setBeanName(""); item.setMethodName(""); diff --git a/schoolNewsServ/crontab/src/main/java/org/xyzh/crontab/task/recommendTask/TopRecommendTask.java b/schoolNewsServ/crontab/src/main/java/org/xyzh/crontab/task/recommendTask/TopRecommendTask.java new file mode 100644 index 0000000..30bd858 --- /dev/null +++ b/schoolNewsServ/crontab/src/main/java/org/xyzh/crontab/task/recommendTask/TopRecommendTask.java @@ -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 topViewCountResources = getTopResourcesByViewCount(10); + logger.info("获取到浏览量前10的资源数量: {}", topViewCountResources.size()); + + // 3. 获取发布时间前10的资源 + List topPublishTimeResources = getTopResourcesByPublishTime(10); + logger.info("获取到发布时间前10的资源数量: {}", topPublishTimeResources.size()); + + // 4. 合并并去重(使用 Set 确保资源ID唯一) + Set resourceIds = new HashSet<>(); + List 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 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 getTopResourcesByViewCount(int limit) { + TbResource filter = new TbResource(); + filter.setStatus(1); // 只查询已发布的资源 + + // 设置排序:按浏览量降序 + List 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 result = resourceService.getResourcePageOrder(filter, pageParam); + if (result.isSuccess() && result.getDataList() != null) { + return result.getDataList(); + } + return new ArrayList<>(); + } + + /** + * 获取发布时间前N的资源(最新发布) + */ + private List getTopResourcesByPublishTime(int limit) { + TbResource filter = new TbResource(); + filter.setStatus(1); // 只查询已发布的资源 + + // 设置排序:按发布时间降序 + List 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 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; + } +} diff --git a/schoolNewsServ/crontab/src/main/resources/mapper/CrontabTaskMapper.xml b/schoolNewsServ/crontab/src/main/resources/mapper/CrontabTaskMapper.xml index c0eea02..1a00ab8 100644 --- a/schoolNewsServ/crontab/src/main/resources/mapper/CrontabTaskMapper.xml +++ b/schoolNewsServ/crontab/src/main/resources/mapper/CrontabTaskMapper.xml @@ -274,7 +274,7 @@ AND ct.task_name LIKE CONCAT('%', #{filter.taskName}, '%') - AND ct.task_group = #{filter.taskGroup} + AND ct.task_group LIKE CONCAT('%', #{filter.taskGroup}, '%') AND ct.status = #{filter.status} @@ -337,7 +337,7 @@ AND ct.task_name LIKE CONCAT('%', #{filter.taskName}, '%') - AND ct.task_group = #{filter.taskGroup} + AND ct.task_group LIKE CONCAT('%', #{filter.taskGroup}, '%') AND ct.status = #{filter.status} diff --git a/schoolNewsServ/crontab/src/main/resources/mapper/TaskMetaMapper.xml b/schoolNewsServ/crontab/src/main/resources/mapper/TaskMetaMapper.xml index 5f78d5f..11367dc 100644 --- a/schoolNewsServ/crontab/src/main/resources/mapper/TaskMetaMapper.xml +++ b/schoolNewsServ/crontab/src/main/resources/mapper/TaskMetaMapper.xml @@ -115,7 +115,7 @@ diff --git a/schoolNewsServ/news/src/main/java/org/xyzh/news/controller/ResourceController.java b/schoolNewsServ/news/src/main/java/org/xyzh/news/controller/ResourceController.java index c29e98e..683665a 100644 --- a/schoolNewsServ/news/src/main/java/org/xyzh/news/controller/ResourceController.java +++ b/schoolNewsServ/news/src/main/java/org/xyzh/news/controller/ResourceController.java @@ -59,7 +59,7 @@ public class ResourceController { public ResultDomain getResourcePageOrderByViewCount(@RequestBody PageRequest request) { TbResource filter = request.getFilter(); PageParam pageParam = request.getPageParam(); - return resourceService.getResourcePageOrderByViewCount(filter, pageParam); + return resourceService.getResourcePageOrder(filter, pageParam); } /** diff --git a/schoolNewsServ/news/src/main/java/org/xyzh/news/mapper/ResourceMapper.java b/schoolNewsServ/news/src/main/java/org/xyzh/news/mapper/ResourceMapper.java index b171102..b1ba510 100644 --- a/schoolNewsServ/news/src/main/java/org/xyzh/news/mapper/ResourceMapper.java +++ b/schoolNewsServ/news/src/main/java/org/xyzh/news/mapper/ResourceMapper.java @@ -176,7 +176,7 @@ public interface ResourceMapper extends BaseMapper { * @author yslg * @since 2025-10-15 */ - List selectResourcesPageOrderByViewCount(@Param("filter") TbResource filter, @Param("pageParam") PageParam pageParam, @Param("userDeptRoles") List userDeptRoles); + List selectResourcesPageOrder(@Param("filter") TbResource filter, @Param("pageParam") PageParam pageParam, @Param("userDeptRoles") List userDeptRoles); /** * @description 统计资源总数 diff --git a/schoolNewsServ/news/src/main/java/org/xyzh/news/service/impl/NCResourceServiceImpl.java b/schoolNewsServ/news/src/main/java/org/xyzh/news/service/impl/NCResourceServiceImpl.java index 4f79152..d2d3683 100644 --- a/schoolNewsServ/news/src/main/java/org/xyzh/news/service/impl/NCResourceServiceImpl.java +++ b/schoolNewsServ/news/src/main/java/org/xyzh/news/service/impl/NCResourceServiceImpl.java @@ -161,7 +161,7 @@ public class NCResourceServiceImpl implements ResourceService { } @Override - public ResultDomain getResourcePageOrderByViewCount(TbResource filter, PageParam pageParam) { + public ResultDomain getResourcePageOrder(TbResource filter, PageParam pageParam) { ResultDomain resultDomain = new ResultDomain<>(); try { if (filter == null) { @@ -170,7 +170,7 @@ public class NCResourceServiceImpl implements ResourceService { // 获取当前用户的部门角色 List userDeptRoles = LoginUtil.getCurrentDeptRole(); // 直接查询ResourceVO列表 - List resourceVOList = resourceMapper.selectResourcesPageOrderByViewCount(filter, pageParam, userDeptRoles); + List resourceVOList = resourceMapper.selectResourcesPageOrder(filter, pageParam, userDeptRoles); logger.info("资源数量{}",resourceVOList.size()); long total = resourceMapper.countResources(filter, userDeptRoles); pageParam.setTotalElements(total); @@ -284,6 +284,7 @@ public class NCResourceServiceImpl implements ResourceService { // 进行审核 ResultDomain pass =auditService.auditText(resource.getTitle() + " "+resource.getContent()); if(pass.isSuccess()){ + resource.setPublishTime(new Date()); resource.setIsAudited(true); }else { auditService.sendAuditResultMessage(resource.getCreator(), "文章"+resource.getTitle(), pass.getDataList()); @@ -509,6 +510,7 @@ public class NCResourceServiceImpl implements ResourceService { ResultDomain pass = auditService.auditText(resource.getTitle()+" "+resource.getContent()); if (pass.isSuccess()) { resource.setIsAudited(true); + resource.setPublishTime(new Date()); } else { // 审核失败,标记状态为4(审核失败) resource.setStatus(4); @@ -562,6 +564,7 @@ public class NCResourceServiceImpl implements ResourceService { ResultDomain pass = auditService.auditText(resource.getTitle()+" "+resource.getContent()); if (pass.isSuccess()) { resource.setIsAudited(true); + resource.setPublishTime(new Date()); } else { // 审核失败,标记状态为3(审核失败) resource.setStatus(3); diff --git a/schoolNewsServ/news/src/main/resources/mapper/ResourceMapper.xml b/schoolNewsServ/news/src/main/resources/mapper/ResourceMapper.xml index 8e14912..3f997ca 100644 --- a/schoolNewsServ/news/src/main/resources/mapper/ResourceMapper.xml +++ b/schoolNewsServ/news/src/main/resources/mapper/ResourceMapper.xml @@ -378,7 +378,7 @@ - SELECT r.*, MAX(CASE WHEN rec.recommend_type = 1 THEN 1 ELSE 0 END) AS is_top_recommend, diff --git a/schoolNewsWeb/src/apis/crontab/index.ts b/schoolNewsWeb/src/apis/crontab/index.ts index 0058082..d993ac9 100644 --- a/schoolNewsWeb/src/apis/crontab/index.ts +++ b/schoolNewsWeb/src/apis/crontab/index.ts @@ -30,8 +30,8 @@ export const crontabApi = { * 获取可创建的定时任务列表(从数据库获取任务元数据) * @returns Promise> */ - async getEnabledCrontabList(): Promise> { - const response = await api.get(`${this.baseUrl}/getEnabledCrontabList`); + async getEnabledCrontabList(param: string): Promise> { + const response = await api.get(`${this.baseUrl}/getEnabledCrontabList`, { param }); return response.data; }, diff --git a/schoolNewsWeb/src/views/admin/manage/crontab/NewsCrawlerView.vue b/schoolNewsWeb/src/views/admin/manage/crontab/NewsCrawlerView.vue index 730cb50..7276e6b 100644 --- a/schoolNewsWeb/src/views/admin/manage/crontab/NewsCrawlerView.vue +++ b/schoolNewsWeb/src/views/admin/manage/crontab/NewsCrawlerView.vue @@ -152,12 +152,12 @@ -
+
([]); -const total = ref(0); // 爬虫元数据 const taskMetaList = ref([]); @@ -440,7 +439,9 @@ const searchForm = reactive({ // 分页参数 const pageParam = reactive({ pageNumber: 1, - pageSize: 10 + pageSize: 10, + totalElements: 0, + totalPages: 0 }); // 对话框状态 @@ -705,7 +706,7 @@ function resetUserSelector() { // 加载爬虫模板(从数据库加载TaskMeta,转换为CrontabItem结构) async function loadCrawlerTemplates() { try { - const result = await crontabApi.getEnabledCrontabList(); + const result = await crontabApi.getEnabledCrontabList("新闻爬取"); if (result.success && result.dataList) { taskMetaList.value = result.dataList; @@ -746,7 +747,7 @@ async function loadCrawlerList() { loading.value = true; try { const filter: Partial = { - taskGroup: '' + taskGroup: '新闻爬取' }; if (searchForm.taskName) filter.taskName = searchForm.taskName; if (searchForm.status !== undefined) filter.status = searchForm.status; @@ -754,25 +755,17 @@ async function loadCrawlerList() { const result = await crontabApi.getTaskPage(filter, pageParam); if (result.success) { // 根据后端返回结构处理数据 - if (result.pageDomain) { - crawlerList.value = result.pageDomain.dataList || []; - total.value = result.pageDomain.pageParam?.totalElements || 0; - } else if (result.dataList) { - crawlerList.value = result.dataList; - total.value = result.pageParam?.totalElements || 0; - } else { - crawlerList.value = []; - total.value = 0; + crawlerList.value = result.pageDomain?.dataList || []; + if (result.pageDomain?.pageParam) { + Object.assign(pageParam, result.pageDomain.pageParam); } } else { ElMessage.error(result.message || '加载爬虫列表失败'); crawlerList.value = []; - total.value = 0; } } catch (error) { ElMessage.error('加载爬虫列表失败'); crawlerList.value = []; - total.value = 0; } finally { loading.value = false; } @@ -1210,14 +1203,38 @@ onMounted(() => { .crawler-list { min-height: 400px; - .crawler-card { + // 让同一行的列等高 + :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; diff --git a/schoolNewsWeb/src/views/admin/manage/crontab/SystemTaskView.vue b/schoolNewsWeb/src/views/admin/manage/crontab/SystemTaskView.vue new file mode 100644 index 0000000..92580b2 --- /dev/null +++ b/schoolNewsWeb/src/views/admin/manage/crontab/SystemTaskView.vue @@ -0,0 +1,714 @@ + + + + + \ No newline at end of file