From 48ee6442b39a775e1cbfc95aac68323e3925cd1e Mon Sep 17 00:00:00 2001 From: wangys <3401275564@qq.com> Date: Tue, 25 Nov 2025 16:00:09 +0800 Subject: [PATCH] =?UTF-8?q?top=E6=8E=A8=E8=8D=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../.bin/mysql/sql/initCrontabMetaData.sql | 21 +- .../task/recommendTask/TopRecommendTask.java | 158 +++++++++++---- .../src/components/base/DynamicParamForm.vue | 191 ++++++++++++++++++ schoolNewsWeb/src/components/base/index.ts | 1 + .../admin/manage/crontab/NewsCrawlerView.vue | 107 +++------- .../admin/manage/crontab/SystemTaskView.vue | 74 ++++++- 6 files changed, 414 insertions(+), 138 deletions(-) create mode 100644 schoolNewsWeb/src/components/base/DynamicParamForm.vue diff --git a/schoolNewsServ/.bin/mysql/sql/initCrontabMetaData.sql b/schoolNewsServ/.bin/mysql/sql/initCrontabMetaData.sql index a4183c3..a48eaad 100644 --- a/schoolNewsServ/.bin/mysql/sql/initCrontabMetaData.sql +++ b/schoolNewsServ/.bin/mysql/sql/initCrontabMetaData.sql @@ -289,7 +289,24 @@ INSERT INTO `tb_crontab_task_meta` ( 'topRecommendTask', 'execute', '', - '[]', + '[ + { + "name": "viewNum", + "description": "按浏览量取多少条", + "type": "InputNumber", + "valueType": "Integer", + "value": 10, + "required": true + }, + { + "name": "timeNum", + "description": "按时间取多少条", + "type": "InputNumber", + "valueType": "Integer", + "value": 10, + "required": true + } + ]', 0, 9, 'system', @@ -310,7 +327,7 @@ INSERT INTO `tb_crontab_task` ( 'topRecommendTask', '0 0 1 * * ?', 'execute', - '{}', + '{"viewNum":10,"timeNum":10}', 1, 'system', NOW() 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 index 30bd858..4d632dc 100644 --- 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 @@ -43,49 +43,108 @@ public class TopRecommendTask extends BaseTask { logger.info("开始执行热门资源推荐任务"); try { + // 从参数中获取配置:浏览量和时间排序各取多少条,默认都为10 + Integer viewNum = taskParams != null ? taskParams.getParamAsInt("viewNum") : null; + Integer timeNum = taskParams != null ? taskParams.getParamAsInt("timeNum") : null; + + if (viewNum == null || viewNum <= 0) { + viewNum = 10; + } + if (timeNum == null || timeNum <= 0) { + timeNum = 10; + } + // 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唯一) + // 2. 合并并去重(使用 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++)); - } - } + int viewAdded = 0; + int timeAdded = 0; - // 添加发布时间前10 - for (ResourceVO vo : topPublishTimeResources) { - if (vo.getResource() != null && resourceIds.add(vo.getResource().getResourceID())) { - recommendList.add(createRecommend(vo.getResource().getResourceID(), orderNum++)); - } - } + // 4.1 按浏览量补足 viewNum 条,采用分页方式逐页向后拿 + int viewPageNumber = 1; + int viewPageSize = viewNum; // 每页拉 viewNum 条 - // 5. 批量插入新推荐 - if (!recommendList.isEmpty()) { - for (TbResourceRecommend recommend : recommendList) { - ResultDomain addResult = resourceRecommendService.addRecommend(recommend); - if (!addResult.isSuccess()) { - logger.warn("插入推荐失败: 资源ID={}, 原因={}", recommend.getResourceID(), addResult.getMessage()); + while (viewAdded < viewNum) { + List viewPageList = getViewCountPage(viewPageNumber, viewPageSize); + if (viewPageList == null || viewPageList.isEmpty()) { + logger.info("按浏览量补位时已经没有更多候选数据,page={}", viewPageNumber); + break; + } + + for (ResourceVO vo : viewPageList) { + if (viewAdded >= viewNum) { + break; } + if (vo.getResource() == null) { + continue; + } + + String resourceId = vo.getResource().getResourceID(); + + // 已经是热门推荐了,跳过,向后拿下一条 + if (isAlreadyHotRecommend(resourceId)) { + logger.info("资源 {} 已经是热门推荐(按浏览量列表),跳过并向后补位", resourceId); + continue; + } + + // 本次任务中已选过(可能来自另外一个列表),跳过并向后拿 + if (!resourceIds.add(resourceId)) { + continue; + } + + recommendList.add(createRecommend(resourceId, orderNum++)); + viewAdded++; } - logger.info("成功插入{}条热门推荐记录", recommendList.size()); - } else { - logger.warn("没有找到符合条件的资源,未插入推荐记录"); + + viewPageNumber++; } + // 4.2 按发布时间补足 timeNum 条,采用分页方式逐页向后拿 + int timePageNumber = 1; + int timePageSize = timeNum; // 每页拉 timeNum 条,基本一两页就足够 + + while (timeAdded < timeNum) { + List pageList = getPublishTimePage(timePageNumber, timePageSize); + if (pageList == null || pageList.isEmpty()) { + logger.info("按时间补位时已经没有更多候选数据,page={}", timePageNumber); + break; + } + + for (ResourceVO vo : pageList) { + if (timeAdded >= timeNum) { + break; + } + if (vo.getResource() == null) { + continue; + } + + String resourceId = vo.getResource().getResourceID(); + + // 已经是热门推荐了,跳过,向后拿下一条 + if (isAlreadyHotRecommend(resourceId)) { + logger.info("资源 {} 已经是热门推荐(按时间列表),跳过并向后补位", resourceId); + continue; + } + + // 本次任务中已选过(例如在浏览量列表中已经加入),跳过并向后拿 + if (!resourceIds.add(resourceId)) { + continue; + } + + recommendList.add(createRecommend(resourceId, orderNum++)); + timeAdded++; + } + + timePageNumber++; + } + + + logger.info("热门资源推荐任务执行成功,共推荐{}个资源", resourceIds.size()); } catch (Exception e) { @@ -104,21 +163,20 @@ public class TopRecommendTask extends BaseTask { } /** - * 获取浏览量前N的资源 + * 获取按浏览量排序的一页资源,用于分页向后补位 */ - private List getTopResourcesByViewCount(int limit) { + private List getViewCountPage(int pageNumber, int pageSize) { 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); + pageParam.setPageSize(pageSize); ResultDomain result = resourceService.getResourcePageOrder(filter, pageParam); if (result.isSuccess() && result.getDataList() != null) { @@ -128,21 +186,20 @@ public class TopRecommendTask extends BaseTask { } /** - * 获取发布时间前N的资源(最新发布) + * 获取按发布时间排序的一页资源(最新发布),用于分页向后补位 */ - private List getTopResourcesByPublishTime(int limit) { + private List getPublishTimePage(int pageNumber, int pageSize) { 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); + pageParam.setPageSize(pageSize); ResultDomain result = resourceService.getResourcePageOrder(filter, pageParam); if (result.isSuccess() && result.getDataList() != null) { @@ -165,4 +222,19 @@ public class TopRecommendTask extends BaseTask { recommend.setDeleted(false); return recommend; } + + /** + * 判断资源是否已经在热门推荐中(recommend_type = 1) + */ + private boolean isAlreadyHotRecommend(String resourceId) { + try { + ResultDomain result = resourceRecommendService.isResourceRecommendedByType(resourceId, 1); + if (result != null && result.isSuccess() && result.getData() != null) { + return Boolean.TRUE.equals(result.getData()); + } + } catch (Exception e) { + logger.error("检查资源是否已为热门推荐失败, resourceId={}", resourceId, e); + } + return false; + } } diff --git a/schoolNewsWeb/src/components/base/DynamicParamForm.vue b/schoolNewsWeb/src/components/base/DynamicParamForm.vue new file mode 100644 index 0000000..f6a7bb1 --- /dev/null +++ b/schoolNewsWeb/src/components/base/DynamicParamForm.vue @@ -0,0 +1,191 @@ + + + + + diff --git a/schoolNewsWeb/src/components/base/index.ts b/schoolNewsWeb/src/components/base/index.ts index c123b98..8337e35 100644 --- a/schoolNewsWeb/src/components/base/index.ts +++ b/schoolNewsWeb/src/components/base/index.ts @@ -12,3 +12,4 @@ export { default as GenericSelector } from './GenericSelector.vue'; export { default as TreeNode } from './TreeNode.vue'; export { default as Notice } from './Notice.vue'; export { default as ChangeHome } from './ChangeHome.vue'; +export { default as DynamicParamForm} from './DynamicParamForm.vue' \ No newline at end of file diff --git a/schoolNewsWeb/src/views/admin/manage/crontab/NewsCrawlerView.vue b/schoolNewsWeb/src/views/admin/manage/crontab/NewsCrawlerView.vue index 7276e6b..d8b86bc 100644 --- a/schoolNewsWeb/src/views/admin/manage/crontab/NewsCrawlerView.vue +++ b/schoolNewsWeb/src/views/admin/manage/crontab/NewsCrawlerView.vue @@ -185,16 +185,15 @@
爬虫模板 @@ -226,75 +225,10 @@
方法参数 -
-
- - {{ param.description }} - ({{ param.type }}) - - - - - - - - - - - - - - - -
-
+
@@ -405,7 +339,7 @@ import { crontabApi } from '@/apis/crontab'; import { userApi } from '@/apis/system/user'; import type { CrontabTask, TaskMeta, CrontabItem, CrontabMethod, CrontabParam, PageParam, CreateTaskRequest, RecipientUserInfo, UserVO, ResultDomain, EmailDefault } from '@/types'; import { AdminLayout } from '@/views/admin'; -import { GenericSelector } from '@/components'; +import { GenericSelector, DynamicParamForm } from '@/components'; defineOptions({ name: 'NewsCrawlerView' }); @@ -417,11 +351,17 @@ const crawlerList = ref([]); // 爬虫元数据 const taskMetaList = ref([]); const crawlerTemplates = ref([]); // 转换后的模板结构 -const selectedTemplate = ref(null); -const selectedMethodId = ref(''); // 选中的方法ID(metaId) -const selectedMetaId = ref(''); // 选中的元数据ID +const selectedCategory = ref(''); // 选中的模板分类名称 +const selectedMethodId = ref(''); // 选中的方法ID(metaId) +const selectedMetaId = ref(''); // 选中的元数据ID const dynamicParams = ref>({}); +// 当前选中的模板对象(通过 category 查找) +const selectedTemplate = computed(() => { + if (!selectedCategory.value) return null; + return crawlerTemplates.value.find(t => t.name === selectedCategory.value) || null; +}); + // 邮件接收人相关 const useDefaultRecipients = ref(false); const defaultRecipients = ref([]); @@ -512,10 +452,9 @@ const selectedRecipients = computed(() => { }); // 监听模板选择变化 -watch(selectedTemplate, (newTemplate, oldTemplate) => { - // 只在用户手动切换模板时重置(oldTemplate存在且不为null时才重置) - // 编辑回填时oldTemplate为null,不会触发重置 - if (newTemplate && oldTemplate) { +watch(selectedCategory, (newCategory, oldCategory) => { + // 只在用户手动切换模板时重置(oldCategory存在且不为空时才重置) + if (newCategory && oldCategory) { selectedMethodId.value = ''; dynamicParams.value = {}; } @@ -732,7 +671,7 @@ async function loadCrawlerTemplates() { params: meta.paramSchema ? JSON.parse(meta.paramSchema) : [] })) })); - + console.log(" crawlerTemplates.value",crawlerTemplates.value); } else { ElMessage.error(result.message || '加载爬虫模板失败'); @@ -801,7 +740,7 @@ function handleSizeChange(size: number) { function handleAdd() { isEdit.value = false; resetFormData(); - selectedTemplate.value = null; + selectedCategory.value = ''; selectedMethodId.value = ''; dynamicParams.value = {}; dialogVisible.value = true; @@ -813,7 +752,7 @@ async function handleEdit(row: CrontabTask) { Object.assign(formData, row); // 重置选择 - selectedTemplate.value = null; + selectedCategory.value = ''; selectedMethodId.value = ''; dynamicParams.value = {}; @@ -844,7 +783,7 @@ async function handleEdit(row: CrontabTask) { const method = template.methods.find(m => m.metaId === row.metaId); if (method) { // 找到匹配的方法,设置template和method - selectedTemplate.value = template; + selectedCategory.value = template.name; selectedMethodId.value = method.metaId || ''; selectedMetaId.value = method.metaId || ''; diff --git a/schoolNewsWeb/src/views/admin/manage/crontab/SystemTaskView.vue b/schoolNewsWeb/src/views/admin/manage/crontab/SystemTaskView.vue index 92580b2..ae429e1 100644 --- a/schoolNewsWeb/src/views/admin/manage/crontab/SystemTaskView.vue +++ b/schoolNewsWeb/src/views/admin/manage/crontab/SystemTaskView.vue @@ -244,11 +244,10 @@ - @@ -285,6 +284,7 @@ import { } from '@element-plus/icons-vue'; import { crontabApi } from '@/apis/crontab'; import AdminLayout from '@/views/admin/AdminLayout.vue'; +import { DynamicParamForm } from '@/components'; import type { CrontabTask, TaskMeta, PageParam, CreateTaskRequest } from '@/types'; // 搜索表单 @@ -306,6 +306,10 @@ const taskList = ref([]); const taskMetaList = ref([]); const loading = ref(false); +// 动态任务参数 +const dynamicParams = ref>({}); +const currentParamSchema = ref([]); + // 对话框 const dialogVisible = ref(false); const dialogTitle = ref('新增任务'); @@ -395,6 +399,9 @@ function handleAddTask() { methodParams: '{}', status: 1 }); + + dynamicParams.value = {}; + currentParamSchema.value = []; dialogVisible.value = true; } @@ -412,6 +419,25 @@ function handleEdit(task: CrontabTask) { methodParams: task.methodParams || '{}', status: task.status }); + + // 根据 metaId 加载参数 schema + const meta = taskMetaList.value.find(m => m.metaId === task.metaId); + if (meta) { + try { + currentParamSchema.value = meta.paramSchema ? JSON.parse(meta.paramSchema) : []; + } catch { + currentParamSchema.value = []; + } + } else { + currentParamSchema.value = []; + } + + // 回填动态参数 + try { + dynamicParams.value = task.methodParams ? JSON.parse(task.methodParams) : {}; + } catch { + dynamicParams.value = {}; + } dialogVisible.value = true; } @@ -421,6 +447,34 @@ function handleMetaChange(metaId: string) { if (meta) { taskForm.taskName = meta.name; taskForm.description = meta.description; + + // 解析参数 schema + try { + currentParamSchema.value = meta.paramSchema ? JSON.parse(meta.paramSchema) : []; + } catch { + currentParamSchema.value = []; + } + + // 根据 schema 默认值初始化 dynamicParams + const baseParams: Record = {}; + if (Array.isArray(currentParamSchema.value)) { + currentParamSchema.value.forEach((p: any) => { + if (p && p.name !== undefined && p.value !== undefined) { + baseParams[p.name] = p.value; + } + }); + } + + // 如果表单中已有 methodParams,合并覆盖默认值 + try { + const exist = taskForm.methodParams ? JSON.parse(taskForm.methodParams) : {}; + dynamicParams.value = { ...baseParams, ...exist }; + } catch { + dynamicParams.value = baseParams; + } + } else { + currentParamSchema.value = []; + dynamicParams.value = {}; } } @@ -432,11 +486,11 @@ async function handleSubmit() { if (!valid) return; try { - // 验证JSON格式 + // 根据动态参数生成 methodParams try { - JSON.parse(taskForm.methodParams || '{}'); + taskForm.methodParams = JSON.stringify(dynamicParams.value || {}); } catch { - ElMessage.error('任务参数必须是有效的JSON格式'); + ElMessage.error('任务参数序列化失败'); return; } @@ -614,7 +668,9 @@ onMounted(() => { margin-left: auto; } } - + .dynamic-params{ + width: 100%; + } .crawler-list { min-height: 400px;