新闻采集修改,完成发送邮件

This commit is contained in:
2025-11-18 17:56:10 +08:00
parent 049b6f2cf3
commit 9f3176194b
50 changed files with 3929 additions and 322 deletions

View File

@@ -7,6 +7,8 @@ CREATE TABLE `tb_crontab_task` (
`task_id` VARCHAR(64) NOT NULL COMMENT '任务ID',
`task_name` VARCHAR(100) NOT NULL COMMENT '任务名称',
`task_group` VARCHAR(50) NOT NULL DEFAULT 'DEFAULT' COMMENT '任务分组',
`meta_id` VARCHAR(64) NOT NULL COMMENT '任务元数据ID',
`default_recipient` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否使用默认接收人0:否 1:是)',
`bean_name` VARCHAR(100) NOT NULL COMMENT 'Bean名称',
`method_name` VARCHAR(100) NOT NULL COMMENT '方法名称',
`method_params` VARCHAR(500) DEFAULT NULL COMMENT '方法参数',
@@ -94,4 +96,78 @@ CREATE TABLE `tb_data_collection_item` (
KEY `idx_status` (`status`),
KEY `idx_publish_time` (`publish_time`),
KEY `idx_source_url` (`source_url`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='数据采集项表';
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='数据采集项表';
-- ====================================================
-- 定时任务元数据表(存储爬虫任务的元数据配置)
-- ====================================================
DROP TABLE IF EXISTS `tb_crontab_task_meta`;
CREATE TABLE `tb_crontab_task_meta` (
`id` VARCHAR(64) NOT NULL COMMENT '主键ID',
`meta_id` VARCHAR(64) NOT NULL COMMENT '元数据ID',
`name` VARCHAR(100) NOT NULL COMMENT '任务名称',
`description` VARCHAR(500) DEFAULT NULL COMMENT '任务描述',
`category` VARCHAR(50) NOT NULL COMMENT '任务分类(如:人民日报新闻爬取)',
`bean_name` VARCHAR(100) NOT NULL COMMENT 'Bean名称执行器类名',
`method_name` VARCHAR(100) NOT NULL COMMENT '执行方法名',
`script_path` VARCHAR(255) DEFAULT NULL COMMENT 'Python脚本路径相对于basePath',
`param_schema` TEXT DEFAULT NULL COMMENT '参数模板JSON格式定义参数名、类型、描述、默认值等',
`auto_publish` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否自动发布',
`sort_order` INT DEFAULT 0 COMMENT '排序号',
`creator` VARCHAR(64) DEFAULT NULL COMMENT '创建者',
`updater` VARCHAR(64) DEFAULT NULL COMMENT '更新者',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`delete_time` DATETIME DEFAULT NULL COMMENT '删除时间',
`deleted` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否删除0:否 1:是)',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_meta_id` (`meta_id`),
KEY `idx_category` (`category`),
KEY `idx_deleted` (`deleted`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='定时任务元数据表';
-- ====================================================
-- 定时任务邮件通知默认接收人员表(关联任务元数据,定义默认接收人员)
-- ====================================================
DROP TABLE IF EXISTS `tb_crontab_email_default`;
CREATE TABLE `tb_crontab_email_default` (
`id` VARCHAR(64) NOT NULL COMMENT '主键ID',
`default_id` VARCHAR(64) NOT NULL COMMENT '默认ID',
`meta_id` VARCHAR(64) NOT NULL COMMENT '关联任务元数据ID',
`user_id` VARCHAR(64) NOT NULL COMMENT '关联用户ID',
`creator` VARCHAR(64) DEFAULT NULL COMMENT '创建者',
`updater` VARCHAR(64) DEFAULT NULL COMMENT '更新者',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`delete_time` DATETIME DEFAULT NULL COMMENT '删除时间',
`deleted` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否删除0:否 1:是)',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_default_id` (`default_id`),
UNIQUE KEY `uk_meta_id` (`meta_id`),
KEY `idx_deleted` (`deleted`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='定时任务邮件通知默认接收人员表';
-- ====================================================
-- 定时任务邮件接收人表1对多
-- ====================================================
DROP TABLE IF EXISTS `tb_crontab_email_recipient`;
CREATE TABLE `tb_crontab_email_recipient` (
`id` VARCHAR(64) NOT NULL COMMENT '主键ID',
`recipient_id` VARCHAR(64) NOT NULL COMMENT '接收人ID',
`task_id` VARCHAR(64) DEFAULT NULL COMMENT '关联任务IDNULL表示不属于任何任务',
`user_id` VARCHAR(64) DEFAULT NULL COMMENT '关联用户ID',
`email` VARCHAR(100) NOT NULL COMMENT '邮箱地址',
`name` VARCHAR(100) DEFAULT NULL COMMENT '接收人姓名',
`creator` VARCHAR(64) DEFAULT NULL COMMENT '创建者',
`updater` VARCHAR(64) DEFAULT NULL COMMENT '更新者',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`delete_time` DATETIME DEFAULT NULL COMMENT '删除时间',
`deleted` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否删除0:否 1:是)',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_recipient_id` (`recipient_id`),
KEY `idx_task_id` (`task_id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_email` (`email`),
KEY `idx_deleted` (`deleted`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='定时任务邮件接收人表';

View File

@@ -38,7 +38,9 @@ INSERT INTO `tb_sys_config` (id, config_key, config_value, config_type, config_g
('10', 'system.banner.interval', '5000', 'number', 'banner', 'Banner切换间隔毫秒', 0, '1', now()),
('11', 'system.resource.auto_publish', 'false', 'boolean', 'resource', '资源自动发布', 0, '1', now()),
('12', 'system.resource.auto_publish_time', '08:00', 'string', 'resource', '自动发布时间', 0, '1', now()),
('13', 'system.ai.enabled', 'true', 'boolean', 'ai', '是否启用智能体', 0, '1', now());
('13', 'system.ai.enabled', 'true', 'boolean', 'ai', '是否启用智能体', 0, '1', now()),
('14', 'crawler.pythonPath', 'F:/Environment/Conda/envs/schoolNewsCrawler/python.exe', 'string', 'crawler', 'Python可执行文件路径', 1, '1', now()),
('15', 'crawler.basePath', 'F:/Project/schoolNews/schoolNewsCrawler', 'string', 'crawler', '爬虫脚本根目录', 1, '1', now());
-- 注意默认superadmin用户已在 initMenuData.sql 中创建,此处无需重复创建

View File

@@ -0,0 +1,102 @@
-- ====================================================
-- 定时任务元数据初始化脚本
-- ====================================================
-- 插入人民日报新闻爬取任务的元数据
-- 1. 关键字搜索爬取
INSERT INTO `tb_crontab_task_meta` (
`id`, `meta_id`, `name`, `description`, `category`,
`bean_name`, `method_name`, `script_path`, `param_schema`,
`sort_order`, `creator`, `create_time`
) VALUES (
'1',
'rmbr_keyword_search',
'关键字搜索爬取',
'根据关键字搜索人民日报新闻内容',
'人民日报新闻爬取',
'newsCrewerTask',
'execute',
'crawler/RmrbSearch.py',
'[
{
"name": "query",
"description": "搜索关键字",
"type": "String",
"value": "",
"required": true
},
{
"name": "total",
"description": "总新闻数量",
"type": "Integer",
"value": 10,
"required": true
}
]',
1,
'system',
NOW()
);
-- 2. 排行榜爬取
INSERT INTO `tb_crontab_task_meta` (
`id`, `meta_id`, `name`, `description`, `category`,
`bean_name`, `method_name`, `script_path`, `param_schema`,
`sort_order`, `creator`, `create_time`
) VALUES (
'2',
'rmbr_hotpoint',
'排行榜爬取',
'爬取人民日报热门排行榜新闻',
'人民日报新闻爬取',
'newsCrewerTask',
'execute',
'crawler/RmrbHotPoint.py',
'[]',
2,
'system',
NOW()
);
-- 3. 往日精彩头条爬取
INSERT INTO `tb_crontab_task_meta` (
`id`, `meta_id`, `name`, `description`, `category`,
`bean_name`, `method_name`, `script_path`, `param_schema`,
`sort_order`, `creator`, `create_time`
) VALUES (
'3',
'rmbr_trending',
'往日精彩头条爬取',
'爬取人民日报往日精彩头条新闻',
'人民日报新闻爬取',
'newsCrewerTask',
'execute',
'crawler/RmrbTrending.py',
'[
{
"name": "startDate",
"description": "开始日期",
"type": "String",
"value": "",
"required": false
},
{
"name": "endDate",
"description": "结束日期",
"type": "String",
"value": "",
"required": false
},
{
"name": "yesterday",
"description": "是否是昨天",
"type": "Boolean",
"value": true,
"required": false
}
]',
3,
'system',
NOW()
);

View File

@@ -52,7 +52,7 @@ public class AchievementController {
* 删除成就
*/
@DeleteMapping("/achievement")
public ResultDomain<Void> deleteAchievement(@RequestBody TbAchievement achievement) {
public ResultDomain<Boolean> deleteAchievement(@RequestBody TbAchievement achievement) {
return achievementService.deleteAchievement(achievement.getAchievementID());
}
@@ -125,7 +125,7 @@ public class AchievementController {
* 撤销用户成就
*/
@DeleteMapping("/revoke")
public ResultDomain<Void> revokeAchievement(
public ResultDomain<Boolean> revokeAchievement(
@RequestParam(name = "userID") String userID,
@RequestParam(name = "achievementID") String achievementID) {
return achievementService.revokeAchievement(userID, achievementID);

View File

@@ -168,8 +168,8 @@ public class ACHAchievementServiceImpl implements AchievementService {
@Override
@Transactional(rollbackFor = Exception.class)
public ResultDomain<Void> deleteAchievement(String achievementID) {
ResultDomain<Void> resultDomain = new ResultDomain<>();
public ResultDomain<Boolean> deleteAchievement(String achievementID) {
ResultDomain<Boolean> resultDomain = new ResultDomain<>();
try {
if (!StringUtils.hasText(achievementID)) {
resultDomain.fail("成就ID不能为空");
@@ -188,7 +188,7 @@ public class ACHAchievementServiceImpl implements AchievementService {
int result = achievementMapper.updateAchievement(achievement);
if (result > 0) {
resultDomain.success("删除成就成功", (Void) null);
resultDomain.success("删除成就成功", true);
} else {
resultDomain.fail("删除成就失败");
}
@@ -405,8 +405,8 @@ public class ACHAchievementServiceImpl implements AchievementService {
@Override
@Transactional(rollbackFor = Exception.class)
public ResultDomain<Void> revokeAchievement(String userID, String achievementID) {
ResultDomain<Void> resultDomain = new ResultDomain<>();
public ResultDomain<Boolean> revokeAchievement(String userID, String achievementID) {
ResultDomain<Boolean> resultDomain = new ResultDomain<>();
try {
if (!StringUtils.hasText(userID) || !StringUtils.hasText(achievementID)) {
resultDomain.fail("参数不能为空");
@@ -417,7 +417,7 @@ public class ACHAchievementServiceImpl implements AchievementService {
if (result > 0) {
// 重置进度
progressMapper.deleteProgress(userID, achievementID);
resultDomain.success("撤销成就成功", (Void) null);
resultDomain.success("撤销成就成功", true);
} else {
resultDomain.fail("撤销成就失败");
}

View File

@@ -38,9 +38,9 @@ public interface AchievementService {
/**
* @description 删除成就
* @param achievementID 成就ID
* @return ResultDomain<Void> 删除结果
* @return ResultDomain<Boolean> 删除结果
*/
ResultDomain<Void> deleteAchievement(String achievementID);
ResultDomain<Boolean> deleteAchievement(String achievementID);
/**
* @description 获取所有成就列表
@@ -112,9 +112,9 @@ public interface AchievementService {
* @description 撤销用户成就(管理员功能)
* @param userID 用户ID
* @param achievementID 成就ID
* @return ResultDomain<Void> 撤销结果
* @return ResultDomain<Boolean> 撤销结果
*/
ResultDomain<Void> revokeAchievement(String userID, String achievementID);
ResultDomain<Boolean> revokeAchievement(String userID, String achievementID);
// ==================== 成就进度管理 ====================

View File

@@ -2,6 +2,7 @@ package org.xyzh.api.crontab;
import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.core.page.PageParam;
import org.xyzh.common.dto.crontab.CreateTaskRequest;
import org.xyzh.common.dto.crontab.TbCrontabTask;
import org.xyzh.common.dto.crontab.TbCrontabLog;
import org.xyzh.common.vo.CrontabTaskVO;
@@ -27,6 +28,15 @@ public interface CrontabService {
*/
ResultDomain<TbCrontabTask> createTask(TbCrontabTask task);
/**
* @description 创建定时任务并绑定邮件接收人
* @param request 创建任务请求(包含任务信息、是否使用默认接收人、额外接收人列表)
* @return ResultDomain<TbCrontabTask> 创建结果
* @author yslg
* @since 2025-11-18
*/
ResultDomain<TbCrontabTask> createTaskWithRecipients(CreateTaskRequest request);
/**
* @description 更新定时任务
* @param task 任务对象
@@ -36,6 +46,15 @@ public interface CrontabService {
*/
ResultDomain<TbCrontabTask> updateTask(TbCrontabTask task);
/**
* @description 更新定时任务并更新邮件接收人
* @param request 更新任务请求(包含任务信息、是否使用默认接收人、额外接收人列表)
* @return ResultDomain<TbCrontabTask> 更新结果
* @author yslg
* @since 2025-11-18
*/
ResultDomain<TbCrontabTask> updateTaskWithRecipients(CreateTaskRequest request);
/**
* @description 删除定时任务
* @param taskId 任务ID

View File

@@ -0,0 +1,39 @@
package org.xyzh.api.crontab;
import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.dto.crontab.TbCrontabEmailDefault;
/**
* @description 定时任务邮件通知默认接收人服务接口
* @filename EmailDefaultService.java
* @author yslg
* @copyright xyzh
* @since 2025-11-18
*/
public interface EmailDefaultService {
/**
* @description 创建默认接收人
*/
ResultDomain<TbCrontabEmailDefault> createDefault(TbCrontabEmailDefault emailDefault);
/**
* @description 更新默认接收人
*/
ResultDomain<TbCrontabEmailDefault> updateDefault(TbCrontabEmailDefault emailDefault);
/**
* @description 删除默认接收人
*/
ResultDomain<Boolean> deleteDefault(String defaultId);
/**
* @description 根据defaultId查询
*/
ResultDomain<TbCrontabEmailDefault> getDefaultById(String defaultId);
/**
* @description 根据metaId查询
*/
ResultDomain<TbCrontabEmailDefault> getDefaultByMetaId(String metaId);
}

View File

@@ -0,0 +1,58 @@
package org.xyzh.api.crontab;
import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.core.page.PageRequest;
import org.xyzh.common.dto.crontab.TbCrontabEmailRecipient;
import java.util.List;
/**
* @description 定时任务邮件接收人服务接口
* @filename EmailRecipientService.java
* @author yslg
* @copyright xyzh
* @since 2025-11-18
*/
public interface EmailRecipientService {
/**
* @description 创建邮件接收人
*/
ResultDomain<TbCrontabEmailRecipient> createRecipient(TbCrontabEmailRecipient recipient);
/**
* @description 批量创建邮件接收人
*/
ResultDomain<Boolean> batchCreateRecipient(List<TbCrontabEmailRecipient> recipients);
/**
* @description 更新邮件接收人
*/
ResultDomain<TbCrontabEmailRecipient> updateRecipient(TbCrontabEmailRecipient recipient);
/**
* @description 删除邮件接收人
*/
ResultDomain<Boolean> deleteRecipient(String recipientId);
/**
* @description 根据ID查询
*/
ResultDomain<TbCrontabEmailRecipient> getRecipientById(String recipientId);
/**
* @description 根据任务ID查询接收人列表
*/
ResultDomain<TbCrontabEmailRecipient> getRecipientsByTaskId(String taskId);
/**
* @description 分页查询邮件接收人
*/
ResultDomain<TbCrontabEmailRecipient> getRecipientPage(PageRequest<TbCrontabEmailRecipient> request);
/**
* @description 删除任务的所有接收人
*/
ResultDomain<Boolean> deleteRecipientsByTaskId(String taskId);
}

View File

@@ -0,0 +1,57 @@
package org.xyzh.api.crontab;
import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.core.page.PageRequest;
import org.xyzh.common.dto.crontab.TbCrontabTaskMeta;
import java.util.List;
/**
* @description 定时任务元数据服务接口
* @filename TaskMetaService.java
* @author yslg
* @copyright xyzh
* @since 2025-11-18
*/
public interface TaskMetaService {
/**
* @description 创建任务元数据
*/
ResultDomain<TbCrontabTaskMeta> createTaskMeta(TbCrontabTaskMeta taskMeta);
/**
* @description 更新任务元数据
*/
ResultDomain<TbCrontabTaskMeta> updateTaskMeta(TbCrontabTaskMeta taskMeta);
/**
* @description 删除任务元数据
*/
ResultDomain<Boolean> deleteTaskMeta(String metaId);
/**
* @description 根据ID查询任务元数据
*/
ResultDomain<TbCrontabTaskMeta> getTaskMetaById(String metaId);
/**
* @description 根据任务ID查询任务元数据
*/
ResultDomain<TbCrontabTaskMeta> getTaskMetaByTaskId(String taskId);
/**
* @description 查询所有任务元数据
*/
ResultDomain<TbCrontabTaskMeta> getAllTaskMeta();
/**
* @description 根据分类查询任务元数据
*/
ResultDomain<TbCrontabTaskMeta> getTaskMetaByCategory(String category);
/**
* @description 分页查询任务元数据
*/
ResultDomain<TbCrontabTaskMeta> getTaskMetaPage(PageRequest<TbCrontabTaskMeta> request);
}

View File

@@ -9,6 +9,40 @@ package org.xyzh.api.system.config;
*/
public interface SysConfigService {
/** */
String getSysConfig(String key);
Object getSysConfig(String key);
/**
* 获取字符串类型配置
* @param key 配置键
* @return 配置值
*/
String getStringConfig(String key);
/**
* 获取整数类型配置
* @param key 配置键
* @return 配置值如果不存在或解析失败返回null
*/
Integer getIntConfig(String key);
/**
* 获取布尔类型配置
* @param key 配置键
* @return 配置值如果不存在或解析失败返回null
*/
Boolean getBooleanConfig(String key);
/**
* 获取浮点数类型配置
* @param key 配置键
* @return 配置值如果不存在或解析失败返回null
*/
Double getDoubleConfig(String key);
/**
* 获取长整数类型配置
* @param key 配置键
* @return 配置值如果不存在或解析失败返回null
*/
Long getLongConfig(String key);
}

View File

@@ -0,0 +1,114 @@
package org.xyzh.common.dto.crontab;
import java.util.List;
/**
* @description 创建定时任务请求
* @filename CreateTaskRequest.java
* @author yslg
* @copyright xyzh
* @since 2025-11-18
*/
public class CreateTaskRequest {
/**
* @description 任务信息
*/
private TbCrontabTask task;
/**
* @description 任务元数据ID用于查找默认接收人配置
*/
private String metaId;
/**
* @description 额外添加的接收人列表(从系统用户中选择,前端传入完整信息避免重复查询)
*/
private List<RecipientUserInfo> additionalRecipients;
public TbCrontabTask getTask() {
return task;
}
public void setTask(TbCrontabTask task) {
this.task = task;
}
public String getMetaId() {
return metaId;
}
public void setMetaId(String metaId) {
this.metaId = metaId;
}
public List<RecipientUserInfo> getAdditionalRecipients() {
return additionalRecipients;
}
public void setAdditionalRecipients(List<RecipientUserInfo> additionalRecipients) {
this.additionalRecipients = additionalRecipients;
}
/**
* @description 接收人用户信息从tb_sys_user选择
*/
public static class RecipientUserInfo {
/**
* @description 用户ID
*/
private String userId;
/**
* @description 用户邮箱
*/
private String userEmail;
/**
* @description 用户名称
*/
private String username;
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getUserEmail() {
return userEmail;
}
public void setUserEmail(String userEmail) {
this.userEmail = userEmail;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
@Override
public String toString() {
return "RecipientUserInfo{" +
"userId='" + userId + '\'' +
", userEmail='" + userEmail + '\'' +
", username='" + username + '\'' +
'}';
}
}
@Override
public String toString() {
return "CreateTaskRequest{" +
"task=" + task +
", metaId='" + metaId + '\'' +
", additionalRecipients=" + additionalRecipients +
'}';
}
}

View File

@@ -0,0 +1,117 @@
package org.xyzh.common.dto.crontab;
import org.xyzh.common.dto.BaseDTO;
/**
* @description 定时任务邮件通知默认接收人员表
* @filename TbCrontabEmailDefault.java
* @author yslg
* @copyright xyzh
* @since 2025-11-18
*/
public class TbCrontabEmailDefault extends BaseDTO {
private static final long serialVersionUID = 1L;
/**
* @description 默认ID
*/
private String defaultId;
/**
* @description 关联任务元数据ID
*/
private String metaId;
/**
* @description 关联用户ID
*/
private String userId;
/**
* @description 关联用户邮箱
*/
private String userEmail;
/**
* @description 关联用户名称
*/
private String username;
/**
* @description 创建者
*/
private String creator;
/**
* @description 更新者
*/
private String updater;
public String getDefaultId() {
return defaultId;
}
public void setDefaultId(String defaultId) {
this.defaultId = defaultId;
}
public String getMetaId() {
return metaId;
}
public void setMetaId(String metaId) {
this.metaId = metaId;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getUserEmail() {
return userEmail;
}
public void setUserEmail(String userEmail) {
this.userEmail = userEmail;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getCreator() {
return creator;
}
public void setCreator(String creator) {
this.creator = creator;
}
public String getUpdater() {
return updater;
}
public void setUpdater(String updater) {
this.updater = updater;
}
@Override
public String toString() {
return "TbCrontabEmailDefault{" +
"id=" + getID() +
", defaultId='" + defaultId + '\'' +
", metaId='" + metaId + '\'' +
", userId='" + userId + '\'' +
", userEmail='" + userEmail + '\'' +
", username='" + username + '\'' +
'}';
}
}

View File

@@ -0,0 +1,118 @@
package org.xyzh.common.dto.crontab;
import org.xyzh.common.dto.BaseDTO;
/**
* @description 定时任务邮件接收人表
* @filename TbCrontabEmailRecipient.java
* @author yslg
* @copyright xyzh
* @since 2025-11-18
*/
public class TbCrontabEmailRecipient extends BaseDTO {
private static final long serialVersionUID = 1L;
/**
* @description 接收人ID
*/
private String recipientId;
/**
* @description 关联任务IDNULL表示不属于任务
*/
private String taskId;
/**
* @description 关联用户ID
*/
private String userId;
/**
* @description 邮箱地址
*/
private String email;
/**
* @description 接收人姓名
*/
private String name;
/**
* @description 创建者
*/
private String creator;
/**
* @description 更新者
*/
private String updater;
public String getRecipientId() {
return recipientId;
}
public void setRecipientId(String recipientId) {
this.recipientId = recipientId;
}
public String getTaskId() {
return taskId;
}
public void setTaskId(String taskId) {
this.taskId = taskId;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getCreator() {
return creator;
}
public void setCreator(String creator) {
this.creator = creator;
}
public String getUpdater() {
return updater;
}
public void setUpdater(String updater) {
this.updater = updater;
}
@Override
public String toString() {
return "TbCrontabEmailRecipient{" +
"id=" + getID() +
", recipientId='" + recipientId + '\'' +
", taskId='" + taskId + '\'' +
", userId='" + userId + '\'' +
", email='" + email + '\'' +
", name='" + name + '\'' +
'}';
}
}

View File

@@ -43,6 +43,16 @@ public class TbCrontabTask extends BaseDTO {
*/
private String methodParams;
/**
* @description 元数据ID关联任务元数据表
*/
private String metaId;
/**
* @description 是否使用默认接收人false:否 true:是)
*/
private Boolean defaultRecipient;
/**
* @description Cron表达式
*/
@@ -126,6 +136,22 @@ public class TbCrontabTask extends BaseDTO {
this.methodParams = methodParams;
}
public String getMetaId() {
return metaId;
}
public void setMetaId(String metaId) {
this.metaId = metaId;
}
public Boolean getDefaultRecipient() {
return defaultRecipient;
}
public void setDefaultRecipient(Boolean defaultRecipient) {
this.defaultRecipient = defaultRecipient;
}
public String getCronExpression() {
return cronExpression;
}

View File

@@ -0,0 +1,187 @@
package org.xyzh.common.dto.crontab;
import org.xyzh.common.dto.BaseDTO;
/**
* @description 定时任务元数据表
* @filename TbCrontabTaskMeta.java
* @author yslg
* @copyright xyzh
* @since 2025-11-18
*/
public class TbCrontabTaskMeta extends BaseDTO {
private static final long serialVersionUID = 1L;
/**
* @description 元数据ID
*/
private String metaId;
/**
* @description 任务名称
*/
private String name;
/**
* @description 任务描述
*/
private String description;
/**
* @description 任务分类(如:人民日报新闻爬取)
*/
private String category;
/**
* @description Bean名称执行器类名
*/
private String beanName;
/**
* @description 执行方法名
*/
private String methodName;
/**
* @description Python脚本路径相对于basePath
*/
private String scriptPath;
/**
* @description 参数模板JSON格式
*/
private String paramSchema;
/**
* @description 是否自动发布
*/
private Boolean autoPublish;
/**
* @description 排序号
*/
private Integer sortOrder;
/**
* @description 创建者
*/
private String creator;
/**
* @description 更新者
*/
private String updater;
public String getMetaId() {
return metaId;
}
public void setMetaId(String metaId) {
this.metaId = metaId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getCategory() {
return category;
}
public void setCategory(String category) {
this.category = category;
}
public String getBeanName() {
return beanName;
}
public void setBeanName(String beanName) {
this.beanName = beanName;
}
public String getMethodName() {
return methodName;
}
public void setMethodName(String methodName) {
this.methodName = methodName;
}
public String getScriptPath() {
return scriptPath;
}
public void setScriptPath(String scriptPath) {
this.scriptPath = scriptPath;
}
public String getParamSchema() {
return paramSchema;
}
public void setParamSchema(String paramSchema) {
this.paramSchema = paramSchema;
}
public Boolean getAutoPublish() {
return autoPublish;
}
public void setAutoPublish(Boolean autoPublish) {
this.autoPublish = autoPublish;
}
public Integer getSortOrder() {
return sortOrder;
}
public void setSortOrder(Integer sortOrder) {
this.sortOrder = sortOrder;
}
public String getCreator() {
return creator;
}
public void setCreator(String creator) {
this.creator = creator;
}
public String getUpdater() {
return updater;
}
public void setUpdater(String updater) {
this.updater = updater;
}
@Override
public String toString() {
return "TbCrontabTaskMeta{" +
"id=" + getID() +
", metaId='" + metaId + '\'' +
", name='" + name + '\'' +
", category='" + category + '\'' +
", beanName='" + beanName + '\'' +
", methodName='" + methodName + '\'' +
", scriptPath='" + scriptPath + '\'' +
", paramSchema='" + paramSchema + '\'' +
", autoPublish=" + autoPublish +
", sortOrder=" + sortOrder +
'}';
}
}

View File

@@ -5,21 +5,14 @@ import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.xyzh.api.crontab.CrontabService;
import org.xyzh.api.crontab.TaskMetaService;
import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.dto.crontab.TbCrontabTaskMeta;
import org.xyzh.common.core.page.PageParam;
import org.xyzh.common.core.page.PageRequest;
import org.xyzh.common.dto.crontab.CreateTaskRequest;
import org.xyzh.common.dto.crontab.TbCrontabTask;
import org.xyzh.common.dto.crontab.TbCrontabLog;
import org.xyzh.crontab.pojo.CrontabItem;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import org.xyzh.common.utils.spring.SpringContextUtil;
import org.xyzh.crontab.config.CrontabProperties;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
/**
@@ -38,157 +31,124 @@ public class CrontabController {
@Autowired
private CrontabService crontabService;
/**
* 获取可创建的定时任务
* @return
*/
@GetMapping("/getEnabledCrontabList")
public ResultDomain<CrontabItem> getEnabledCrontabList(@RequestParam(required = false) String param) {
ResultDomain<CrontabItem> rd = new ResultDomain<>();
try {
// 仅返回爬虫能力的元信息(任务模版列表),不包含调度相关内容
CrontabProperties props =
SpringContextUtil.getBean(CrontabProperties.class);
String jString = JSON.toJSONString(props);
props = JSON.parseObject(jString, CrontabProperties.class);
props.getItems().forEach(item->item.getMethods().forEach(
method->{
method.setClazz(null);
method.setExcuete_method(null);
}));
rd.success("ok", props.getItems());
} catch (Exception e) {
rd.fail("获取可创建定时任务失败: " + e.getMessage());
}
return rd;
}
@Autowired
private TaskMetaService taskMetaService;
/**
* 创建定时任务
* @param crontabItem
* @return
* 获取可创建定时任务(从数据库获取任务元数据列表)
* @return ResultDomain<TbCrontabTaskMeta>
*/
@PostMapping("/crontabTask")
public ResultDomain<TbCrontabTask> createCrontab(@RequestBody TbCrontabTask crontabItem) {
ResultDomain<TbCrontabTask> rd = new ResultDomain<>();
@GetMapping("/getEnabledCrontabList")
public ResultDomain<TbCrontabTaskMeta> getEnabledCrontabList(@RequestParam(required = false) String param) {
try {
// 根据前端传入的taskGroup和methodName都是中文显示名查找配置
if (crontabItem.getTaskGroup() == null || crontabItem.getTaskGroup().isEmpty()) {
rd.fail("任务分组不能为空");
return rd;
}
if (crontabItem.getMethodName() == null || crontabItem.getMethodName().isEmpty()) {
rd.fail("方法名称不能为空");
return rd;
}
// 根据taskGroup和methodName查找配置
CrontabItem.CrontabMethod method = findMethodByTaskGroupAndMethodName(
crontabItem.getTaskGroup(),
crontabItem.getMethodName()
);
if (method != null) {
// 填充beanName和实际的Java方法名
crontabItem.setBeanName(method.getClazz());
crontabItem.setMethodName(method.getExcuete_method());
// 将scriptPath添加到methodParams中
JSONObject methodParams = JSON.parseObject(crontabItem.getMethodParams());
methodParams.put("scriptPath", method.getPath());
crontabItem.setMethodParams(methodParams.toJSONString());
logger.info("创建任务 - taskGroup: {}, methodName: {}, beanName: {}, excuete_method: {}, scriptPath: {}",
crontabItem.getTaskGroup(), method.getName(), method.getClazz(),
method.getExcuete_method(), method.getPath());
} else {
rd.fail("未找到对应的配置: taskGroup=" + crontabItem.getTaskGroup()
+ ", methodName=" + crontabItem.getMethodName());
return rd;
}
return crontabService.createTask(crontabItem);
// 从数据库查询所有任务元数据
ResultDomain<TbCrontabTaskMeta> result = taskMetaService.getAllTaskMeta();
result.getDataList().forEach(item->{
item.setBeanName("");
item.setMethodName("");
item.setScriptPath("");
});
return result;
} catch (Exception e) {
logger.error("创建定时任务失败", e);
rd.fail("创建定时任务失败: " + e.getMessage());
ResultDomain<TbCrontabTaskMeta> rd = new ResultDomain<>();
rd.fail("获取可创建定时任务失败: " + e.getMessage());
return rd;
}
}
/**
* 根据taskGroup和methodName查找对应的方法配置
* 创建定时任务并绑定邮件接收人(从数据库获取元数据配置
* @param request 创建任务请求包含任务信息、元数据ID、是否使用默认接收人、额外接收人列表
* @return ResultDomain<TbCrontabTask>
*/
private CrontabItem.CrontabMethod findMethodByTaskGroupAndMethodName(String taskGroup, String methodName) {
CrontabProperties props = SpringContextUtil.getBean(CrontabProperties.class);
if (props == null || props.getItems() == null) {
return null;
}
for (CrontabItem item : props.getItems()) {
if (item.getName().equals(taskGroup)) {
for (CrontabItem.CrontabMethod method : item.getMethods()) {
if (method.getName().equals(methodName)) {
return method;
}
}
@PostMapping("/crontabTask")
public ResultDomain<TbCrontabTask> createCrontab(@RequestBody CreateTaskRequest request) {
ResultDomain<TbCrontabTask> rd = new ResultDomain<>();
try {
TbCrontabTask crontabItem = request.getTask();
String metaId = request.getMetaId();
// 验证元数据ID
if (metaId == null || metaId.isEmpty()) {
rd.fail("任务元数据ID不能为空");
return rd;
}
// 从数据库查询任务元数据
ResultDomain<TbCrontabTaskMeta> metaResult = taskMetaService.getTaskMetaById(metaId);
if (!metaResult.isSuccess() || metaResult.getData() == null) {
rd.fail("未找到对应的任务元数据: metaId=" + metaId);
return rd;
}
TbCrontabTaskMeta taskMeta = metaResult.getData();
// 填充任务信息
crontabItem.setTaskGroup(taskMeta.getCategory()); // 任务分组使用category
crontabItem.setBeanName(taskMeta.getBeanName());
crontabItem.setMethodName(taskMeta.getMethodName());
crontabItem.setMetaId(metaId); // 保存metaId执行时从数据库读取scriptPath
logger.info("创建任务并绑定接收人 - metaId: {}, name: {}, category: {}, defaultRecipient: {}, additionalCount: {}",
metaId, taskMeta.getName(), taskMeta.getCategory(),
crontabItem.getDefaultRecipient(),
request.getAdditionalRecipients() != null ? request.getAdditionalRecipients().size() : 0);
return crontabService.createTaskWithRecipients(request);
} catch (Exception e) {
logger.error("创建定时任务并绑定接收人失败", e);
rd.fail("创建定时任务并绑定接收人失败: " + e.getMessage());
return rd;
}
return null;
}
/**
* 更新定时任务
* @param crontabItem
* @return
* 更新定时任务并更新邮件接收人(从数据库获取元数据配置)
* @param request 更新任务请求包含任务信息、元数据ID、是否使用默认接收人、额外接收人列表
* @return ResultDomain<TbCrontabTask>
*/
@PutMapping("/crontabTask")
public ResultDomain<TbCrontabTask> updateCrontab(@RequestBody TbCrontabTask crontabItem) {
public ResultDomain<TbCrontabTask> updateCrontab(@RequestBody CreateTaskRequest request) {
ResultDomain<TbCrontabTask> rd = new ResultDomain<>();
try {
TbCrontabTask crontabItem = request.getTask();
String metaId = request.getMetaId();
// 确保id字段正确传递用于数据库更新
if (crontabItem.getTaskId() != null && crontabItem.getID() == null) {
crontabItem.setID(crontabItem.getTaskId());
}
// 根据前端传入的taskGroup和methodName都是中文显示名查找配置
if (crontabItem.getTaskGroup() == null || crontabItem.getTaskGroup().isEmpty()) {
rd.fail("任务分组不能为空");
return rd;
}
if (crontabItem.getMethodName() == null || crontabItem.getMethodName().isEmpty()) {
rd.fail("方法名称不能为空");
// 验证元数据ID
if (metaId == null || metaId.isEmpty()) {
rd.fail("任务元数据ID不能为空");
return rd;
}
// 根据taskGroup和methodName查找配置
CrontabItem.CrontabMethod method = findMethodByTaskGroupAndMethodName(
crontabItem.getTaskGroup(),
crontabItem.getMethodName()
);
if (method != null) {
// 填充beanName和实际的Java方法名
crontabItem.setBeanName(method.getClazz());
crontabItem.setMethodName(method.getExcuete_method());
// 将scriptPath添加到methodParams中
JSONObject methodParams = JSON.parseObject(crontabItem.getMethodParams());
methodParams.put("scriptPath", method.getPath());
crontabItem.setMethodParams(methodParams.toJSONString());
logger.info("更新任务 - id: {}, taskGroup: {}, methodName: {}, beanName: {}, excuete_method: {}, scriptPath: {}",
crontabItem.getID(), crontabItem.getTaskGroup(), method.getName(), method.getClazz(),
method.getExcuete_method(), method.getPath());
} else {
rd.fail("未找到对应的配置: taskGroup=" + crontabItem.getTaskGroup()
+ ", methodName=" + crontabItem.getMethodName());
// 从数据库查询任务元数据
ResultDomain<TbCrontabTaskMeta> metaResult = taskMetaService.getTaskMetaById(metaId);
if (!metaResult.isSuccess() || metaResult.getData() == null) {
rd.fail("未找到对应的任务元数据: metaId=" + metaId);
return rd;
}
return crontabService.updateTask(crontabItem);
TbCrontabTaskMeta taskMeta = metaResult.getData();
// 填充任务信息
crontabItem.setTaskGroup(taskMeta.getCategory());
crontabItem.setBeanName(taskMeta.getBeanName());
crontabItem.setMethodName(taskMeta.getMethodName());
crontabItem.setMetaId(metaId); // 保存metaId执行时从数据库读取scriptPath
logger.info("更新任务 - id: {}, metaId: {}, name: {}, category: {}, defaultRecipient: {}",
crontabItem.getID(), metaId, taskMeta.getName(), taskMeta.getCategory(),
crontabItem.getDefaultRecipient());
// 调用带接收人更新的方法
return crontabService.updateTaskWithRecipients(request);
} catch (Exception e) {
logger.error("更新定时任务失败", e);
rd.fail("更新定时任务失败: " + e.getMessage());
logger.error("更新定时任务并更新接收人失败", e);
rd.fail("更新定时任务并更新接收人失败: " + e.getMessage());
return rd;
}
}

View File

@@ -0,0 +1,76 @@
package org.xyzh.crontab.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.xyzh.api.crontab.EmailDefaultService;
import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.dto.crontab.TbCrontabEmailDefault;
/**
* @description 定时任务邮件通知默认接收人控制器
* @filename EmailDefaultController.java
* @author yslg
* @copyright xyzh
* @since 2025-11-18
*/
@RestController
@RequestMapping("/crontab/email/default")
public class EmailDefaultController {
private static final Logger logger = LoggerFactory.getLogger(EmailDefaultController.class);
@Autowired
private EmailDefaultService emailDefaultService;
/**
* @description 创建默认接收人
* @param emailDefault 默认接收人
* @return ResultDomain<TbCrontabEmailDefault>
*/
@PostMapping
public ResultDomain<TbCrontabEmailDefault> createDefault(@RequestBody TbCrontabEmailDefault emailDefault) {
return emailDefaultService.createDefault(emailDefault);
}
/**
* @description 更新默认接收人
* @param emailDefault 默认接收人
* @return ResultDomain<TbCrontabEmailDefault>
*/
@PutMapping
public ResultDomain<TbCrontabEmailDefault> updateDefault(@RequestBody TbCrontabEmailDefault emailDefault) {
return emailDefaultService.updateDefault(emailDefault);
}
/**
* @description 删除默认接收人
* @param defaultId 默认ID
* @return ResultDomain<Boolean>
*/
@DeleteMapping("/{defaultId}")
public ResultDomain<Boolean> deleteDefault(@PathVariable String defaultId) {
return emailDefaultService.deleteDefault(defaultId);
}
/**
* @description 根据defaultId查询
* @param defaultId 默认ID
* @return ResultDomain<TbCrontabEmailDefault>
*/
@GetMapping("/{defaultId}")
public ResultDomain<TbCrontabEmailDefault> getDefaultById(@PathVariable String defaultId) {
return emailDefaultService.getDefaultById(defaultId);
}
/**
* @description 根据metaId查询
* @param metaId 元数据ID
* @return ResultDomain<TbCrontabEmailDefault>
*/
@GetMapping("/meta/{metaId}")
public ResultDomain<TbCrontabEmailDefault> getDefaultByMetaId(@PathVariable String metaId) {
return emailDefaultService.getDefaultByMetaId(metaId);
}
}

View File

@@ -0,0 +1,109 @@
package org.xyzh.crontab.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.xyzh.api.crontab.EmailRecipientService;
import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.core.page.PageRequest;
import org.xyzh.common.dto.crontab.TbCrontabEmailRecipient;
import java.util.List;
/**
* @description 定时任务邮件接收人控制器
* @filename EmailRecipientController.java
* @author yslg
* @copyright xyzh
* @since 2025-11-18
*/
@RestController
@RequestMapping("/crontab/email/recipient")
public class EmailRecipientController {
private static final Logger logger = LoggerFactory.getLogger(EmailRecipientController.class);
@Autowired
private EmailRecipientService emailRecipientService;
/**
* @description 创建邮件接收人
* @param recipient 邮件接收人
* @return ResultDomain<TbCrontabEmailRecipient>
*/
@PostMapping
public ResultDomain<TbCrontabEmailRecipient> createRecipient(@RequestBody TbCrontabEmailRecipient recipient) {
return emailRecipientService.createRecipient(recipient);
}
/**
* @description 批量创建邮件接收人
* @param recipients 邮件接收人列表
* @return ResultDomain<Boolean>
*/
@PostMapping("/batch")
public ResultDomain<Boolean> batchCreateRecipient(@RequestBody List<TbCrontabEmailRecipient> recipients) {
return emailRecipientService.batchCreateRecipient(recipients);
}
/**
* @description 更新邮件接收人
* @param recipient 邮件接收人
* @return ResultDomain<TbCrontabEmailRecipient>
*/
@PutMapping
public ResultDomain<TbCrontabEmailRecipient> updateRecipient(@RequestBody TbCrontabEmailRecipient recipient) {
return emailRecipientService.updateRecipient(recipient);
}
/**
* @description 删除邮件接收人
* @param recipientId 接收人ID
* @return ResultDomain<Boolean>
*/
@DeleteMapping("/{recipientId}")
public ResultDomain<Boolean> deleteRecipient(@PathVariable String recipientId) {
return emailRecipientService.deleteRecipient(recipientId);
}
/**
* @description 根据ID查询
* @param recipientId 接收人ID
* @return ResultDomain<TbCrontabEmailRecipient>
*/
@GetMapping("/{recipientId}")
public ResultDomain<TbCrontabEmailRecipient> getRecipientById(@PathVariable String recipientId) {
return emailRecipientService.getRecipientById(recipientId);
}
/**
* @description 根据任务ID查询接收人列表
* @param taskId 任务ID
* @return ResultDomain<TbCrontabEmailRecipient>
*/
@GetMapping("/task/{taskId}")
public ResultDomain<TbCrontabEmailRecipient> getRecipientsByTaskId(@PathVariable String taskId) {
return emailRecipientService.getRecipientsByTaskId(taskId);
}
/**
* @description 分页查询邮件接收人
* @param pageRequest 分页请求
* @return ResultDomain<TbCrontabEmailRecipient>
*/
@PostMapping("/page")
public ResultDomain<TbCrontabEmailRecipient> getRecipientPage(@RequestBody PageRequest<TbCrontabEmailRecipient> pageRequest) {
return emailRecipientService.getRecipientPage(pageRequest);
}
/**
* @description 删除任务的所有接收人
* @param taskId 任务ID
* @return ResultDomain<Boolean>
*/
@DeleteMapping("/task/{taskId}")
public ResultDomain<Boolean> deleteRecipientsByTaskId(@PathVariable String taskId) {
return emailRecipientService.deleteRecipientsByTaskId(taskId);
}
}

View File

@@ -0,0 +1,96 @@
package org.xyzh.crontab.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.xyzh.api.crontab.TaskMetaService;
import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.core.page.PageRequest;
import org.xyzh.common.dto.crontab.TbCrontabTaskMeta;
/**
* @description 定时任务元数据控制器
* @filename TaskMetaController.java
* @author yslg
* @copyright xyzh
* @since 2025-11-18
*/
@RestController
@RequestMapping("/crontab/meta")
public class TaskMetaController {
private static final Logger logger = LoggerFactory.getLogger(TaskMetaController.class);
@Autowired
private TaskMetaService taskMetaService;
/**
* @description 创建任务元数据
* @param taskMeta 任务元数据
* @return ResultDomain<TbCrontabTaskMeta>
*/
@PostMapping
public ResultDomain<TbCrontabTaskMeta> createTaskMeta(@RequestBody TbCrontabTaskMeta taskMeta) {
return taskMetaService.createTaskMeta(taskMeta);
}
/**
* @description 更新任务元数据
* @param taskMeta 任务元数据
* @return ResultDomain<TbCrontabTaskMeta>
*/
@PutMapping
public ResultDomain<TbCrontabTaskMeta> updateTaskMeta(@RequestBody TbCrontabTaskMeta taskMeta) {
return taskMetaService.updateTaskMeta(taskMeta);
}
/**
* @description 删除任务元数据
* @param metaId 元数据ID
* @return ResultDomain<Boolean>
*/
@DeleteMapping("/{metaId}")
public ResultDomain<Boolean> deleteTaskMeta(@PathVariable String metaId) {
return taskMetaService.deleteTaskMeta(metaId);
}
/**
* @description 根据ID查询任务元数据
* @param metaId 元数据ID
* @return ResultDomain<TbCrontabTaskMeta>
*/
@GetMapping("/{metaId}")
public ResultDomain<TbCrontabTaskMeta> getTaskMetaById(@PathVariable String metaId) {
return taskMetaService.getTaskMetaById(metaId);
}
/**
* @description 查询所有任务元数据
* @return ResultDomain<TbCrontabTaskMeta>
*/
@GetMapping("/all")
public ResultDomain<TbCrontabTaskMeta> getAllTaskMeta() {
return taskMetaService.getAllTaskMeta();
}
/**
* @description 根据分类查询任务元数据
* @param category 分类
* @return ResultDomain<TbCrontabTaskMeta>
*/
@GetMapping("/category/{category}")
public ResultDomain<TbCrontabTaskMeta> getTaskMetaByCategory(@PathVariable String category) {
return taskMetaService.getTaskMetaByCategory(category);
}
/**
* @description 分页查询任务元数据
* @param pageRequest 分页请求
* @return ResultDomain<TbCrontabTaskMeta>
*/
@PostMapping("/page")
public ResultDomain<TbCrontabTaskMeta> getTaskMetaPage(@RequestBody PageRequest<TbCrontabTaskMeta> pageRequest) {
return taskMetaService.getTaskMetaPage(pageRequest);
}
}

View File

@@ -0,0 +1,45 @@
package org.xyzh.crontab.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.xyzh.common.dto.crontab.TbCrontabEmailDefault;
/**
* @description 定时任务邮件通知默认接收人访问层
* @filename EmailDefaultMapper.java
* @author yslg
* @copyright xyzh
* @since 2025-11-18
*/
@Mapper
public interface EmailDefaultMapper extends BaseMapper<TbCrontabEmailDefault> {
/**
* @description 插入默认接收人
*/
int insertDefault(@Param("emailDefault") TbCrontabEmailDefault emailDefault);
/**
* @description 更新默认接收人
*/
int updateDefault(@Param("emailDefault") TbCrontabEmailDefault emailDefault);
/**
* @description 删除默认接收人(逻辑删除)
*/
int deleteDefault(@Param("defaultId") String defaultId);
/**
* @description 根据defaultId查询
*/
TbCrontabEmailDefault selectDefaultById(@Param("defaultId") String defaultId);
/**
* @description 根据metaId查询
*/
List<TbCrontabEmailDefault> selectDefaultByMetaId(@Param("metaId") String metaId);
}

View File

@@ -0,0 +1,66 @@
package org.xyzh.crontab.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.xyzh.common.core.page.PageParam;
import org.xyzh.common.dto.crontab.TbCrontabEmailRecipient;
import java.util.List;
/**
* @description 定时任务邮件接收人访问层
* @filename EmailRecipientMapper.java
* @author yslg
* @copyright xyzh
* @since 2025-11-18
*/
@Mapper
public interface EmailRecipientMapper extends BaseMapper<TbCrontabEmailRecipient> {
/**
* @description 插入邮件接收人
*/
int insertRecipient(@Param("recipient") TbCrontabEmailRecipient recipient);
/**
* @description 批量插入邮件接收人
*/
int batchInsertRecipient(@Param("recipients") List<TbCrontabEmailRecipient> recipients);
/**
* @description 更新邮件接收人
*/
int updateRecipient(@Param("recipient") TbCrontabEmailRecipient recipient);
/**
* @description 删除邮件接收人(逻辑删除)
*/
int deleteRecipient(@Param("recipientId") String recipientId);
/**
* @description 根据接收人ID查询
*/
TbCrontabEmailRecipient selectRecipientById(@Param("recipientId") String recipientId);
/**
* @description 根据任务ID查询接收人列表
*/
List<TbCrontabEmailRecipient> selectRecipientsByTaskId(@Param("taskId") String taskId);
/**
* @description 分页查询邮件接收人
*/
List<TbCrontabEmailRecipient> selectRecipientPage(@Param("filter") TbCrontabEmailRecipient filter, @Param("pageParam") PageParam pageParam);
/**
* @description 查询邮件接收人总数
*/
int countSelectRecipient(@Param("filter") TbCrontabEmailRecipient filter);
/**
* @description 删除任务的所有接收人
*/
int deleteRecipientsByTaskId(@Param("taskId") String taskId);
}

View File

@@ -0,0 +1,65 @@
package org.xyzh.crontab.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.xyzh.common.core.page.PageParam;
import org.xyzh.common.dto.crontab.TbCrontabTaskMeta;
import java.util.List;
/**
* @description 定时任务元数据访问层
* @filename TaskMetaMapper.java
* @author yslg
* @copyright xyzh
* @since 2025-11-18
*/
@Mapper
public interface TaskMetaMapper extends BaseMapper<TbCrontabTaskMeta> {
/**
* @description 插入任务元数据
*/
int insertTaskMeta(@Param("taskMeta") TbCrontabTaskMeta taskMeta);
/**
* @description 更新任务元数据
*/
int updateTaskMeta(@Param("taskMeta") TbCrontabTaskMeta taskMeta);
/**
* @description 删除任务元数据(逻辑删除)
*/
int deleteTaskMeta(@Param("metaId") String metaId);
/**
* @description 根据元数据ID查询
*/
TbCrontabTaskMeta selectTaskMetaById(@Param("metaId") String metaId);
/**
* @description 根据任务ID查询
*/
TbCrontabTaskMeta selectTaskMetaByTaskId(@Param("taskId") String taskId);
/**
* @description 查询所有任务元数据
*/
List<TbCrontabTaskMeta> selectAllTaskMeta();
/**
* @description 根据分类查询任务元数据
*/
List<TbCrontabTaskMeta> selectTaskMetaByCategory(@Param("category") String category);
/**
* @description 分页查询任务元数据
*/
List<TbCrontabTaskMeta> selectTaskMetaPage(@Param("filter") TbCrontabTaskMeta filter, @Param("pageParam") PageParam pageParam);
/**
* @description 查询任务元数据总数
*/
int countSelectTaskMeta(@Param("filter") TbCrontabTaskMeta filter);
}

View File

@@ -157,9 +157,12 @@ public class TaskExecutor {
Map<String, Object> params = JSON.parseObject(methodParams,
new TypeReference<Map<String, Object>>(){});
// 注入taskIdlogId
// 注入taskIdlogId和metaId
params.put("taskId", task.getTaskId());
params.put("logId", log.getID());
if (task.getMetaId() != null) {
params.put("metaId", task.getMetaId());
}
taskParams.setParams(params);

View File

@@ -7,9 +7,14 @@ import org.springframework.scheduling.support.CronExpression;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.xyzh.api.crontab.CrontabService;
import org.xyzh.api.crontab.EmailDefaultService;
import org.xyzh.api.crontab.EmailRecipientService;
import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.core.page.PageDomain;
import org.xyzh.common.core.page.PageParam;
import org.xyzh.common.dto.crontab.CreateTaskRequest;
import org.xyzh.common.dto.crontab.TbCrontabEmailDefault;
import org.xyzh.common.dto.crontab.TbCrontabEmailRecipient;
import org.xyzh.common.dto.crontab.TbCrontabTask;
import org.xyzh.common.dto.crontab.TbCrontabLog;
import org.xyzh.common.utils.IDUtils;
@@ -50,6 +55,12 @@ public class CrontabServiceImpl implements CrontabService {
@Autowired
private ResourcePermissionService resourcePermissionService;
@Autowired
private EmailDefaultService emailDefaultService;
@Autowired
private EmailRecipientService emailRecipientService;
// ----------------定时任务管理--------------------------------
@Override
@@ -115,6 +126,101 @@ public class CrontabServiceImpl implements CrontabService {
return resultDomain;
}
@Override
@Transactional(rollbackFor = Exception.class)
public ResultDomain<TbCrontabTask> createTaskWithRecipients(CreateTaskRequest request) {
ResultDomain<TbCrontabTask> resultDomain = new ResultDomain<>();
try {
TbCrontabTask task = request.getTask();
// 1. 创建任务
ResultDomain<TbCrontabTask> createResult = createTask(task);
if (!createResult.isSuccess()) {
return createResult;
}
String taskId = task.getTaskId();
String metaId = request.getMetaId();
// 3. 添加额外的接收人
if (request.getAdditionalRecipients() != null && !request.getAdditionalRecipients().isEmpty()) {
try {
for (CreateTaskRequest.RecipientUserInfo userInfo : request.getAdditionalRecipients()) {
TbCrontabEmailRecipient recipient = new TbCrontabEmailRecipient();
recipient.setTaskId(taskId);
recipient.setUserId(userInfo.getUserId());
recipient.setEmail(userInfo.getUserEmail());
recipient.setName(userInfo.getUsername());
recipient.setCreator(task.getCreator());
emailRecipientService.createRecipient(recipient);
}
logger.info("为任务{}添加了{}个额外接收人", taskId, request.getAdditionalRecipients().size());
} catch (Exception e) {
logger.error("添加额外接收人异常,但不影响任务创建: {}", e.getMessage(), e);
}
}
resultDomain.success("创建任务并绑定接收人成功", task);
} catch (Exception e) {
logger.error("创建任务并绑定接收人异常: ", e);
resultDomain.fail("创建任务并绑定接收人异常: " + e.getMessage());
}
return resultDomain;
}
@Override
@Transactional(rollbackFor = Exception.class)
public ResultDomain<TbCrontabTask> updateTaskWithRecipients(CreateTaskRequest request) {
ResultDomain<TbCrontabTask> resultDomain = new ResultDomain<>();
try {
TbCrontabTask task = request.getTask();
// 1. 更新任务
ResultDomain<TbCrontabTask> updateResult = updateTask(task);
if (!updateResult.isSuccess()) {
return updateResult;
}
String taskId = task.getTaskId();
String metaId = request.getMetaId();
// 2. 先删除该任务的旧接收人
try {
emailRecipientService.deleteRecipientsByTaskId(taskId);
logger.info("已删除任务{}的旧接收人配置", taskId);
} catch (Exception e) {
logger.error("删除旧接收人异常: {}", e.getMessage(), e);
}
// 4. 添加额外的接收人
if (request.getAdditionalRecipients() != null && !request.getAdditionalRecipients().isEmpty()) {
try {
for (CreateTaskRequest.RecipientUserInfo userInfo : request.getAdditionalRecipients()) {
TbCrontabEmailRecipient recipient = new TbCrontabEmailRecipient();
recipient.setTaskId(taskId);
recipient.setUserId(userInfo.getUserId());
recipient.setEmail(userInfo.getUserEmail());
recipient.setName(userInfo.getUsername());
recipient.setUpdater(task.getUpdater());
emailRecipientService.createRecipient(recipient);
}
logger.info("为任务{}添加了{}个额外接收人", taskId, request.getAdditionalRecipients().size());
} catch (Exception e) {
logger.error("添加额外接收人异常,但不影响任务更新: {}", e.getMessage(), e);
}
}
resultDomain.success("更新任务并更新接收人成功", task);
} catch (Exception e) {
logger.error("更新任务并更新接收人异常: ", e);
resultDomain.fail("更新任务并更新接收人异常: " + e.getMessage());
}
return resultDomain;
}
@Override
@Transactional(rollbackFor = Exception.class)
public ResultDomain<TbCrontabTask> updateTask(TbCrontabTask task) {

View File

@@ -0,0 +1,129 @@
package org.xyzh.crontab.service.impl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.xyzh.api.crontab.EmailDefaultService;
import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.dto.crontab.TbCrontabEmailDefault;
import org.xyzh.common.utils.IDUtils;
import org.xyzh.crontab.mapper.EmailDefaultMapper;
import java.util.Date;
import java.util.List;
/**
* @description 定时任务邮件通知默认接收人服务实现类
* @filename EmailDefaultServiceImpl.java
* @author yslg
* @copyright xyzh
* @since 2025-11-18
*/
@Service
public class EmailDefaultServiceImpl implements EmailDefaultService {
private static final Logger logger = LoggerFactory.getLogger(EmailDefaultServiceImpl.class);
@Autowired
private EmailDefaultMapper emailDefaultMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public ResultDomain<TbCrontabEmailDefault> createDefault(TbCrontabEmailDefault emailDefault) {
ResultDomain<TbCrontabEmailDefault> resultDomain = new ResultDomain<>();
try {
// 生成ID
emailDefault.setID(IDUtils.generateID());
if (emailDefault.getDefaultId() == null || emailDefault.getDefaultId().isEmpty()) {
emailDefault.setDefaultId(IDUtils.generateID());
}
emailDefault.setCreateTime(new Date());
emailDefault.setDeleted(false);
int count = emailDefaultMapper.insertDefault(emailDefault);
if (count > 0) {
resultDomain.success("创建默认接收人成功", emailDefault);
} else {
resultDomain.fail("创建默认接收人失败");
}
} catch (Exception e) {
logger.error("创建默认接收人异常: {}", e.getMessage(), e);
resultDomain.fail("创建默认接收人失败: " + e.getMessage());
}
return resultDomain;
}
@Override
@Transactional(rollbackFor = Exception.class)
public ResultDomain<TbCrontabEmailDefault> updateDefault(TbCrontabEmailDefault emailDefault) {
ResultDomain<TbCrontabEmailDefault> resultDomain = new ResultDomain<>();
try {
emailDefault.setUpdateTime(new Date());
int count = emailDefaultMapper.updateDefault(emailDefault);
if (count > 0) {
resultDomain.success("更新默认接收人成功", emailDefault);
} else {
resultDomain.fail("更新默认接收人失败");
}
} catch (Exception e) {
logger.error("更新默认接收人异常: {}", e.getMessage(), e);
resultDomain.fail("更新默认接收人失败: " + e.getMessage());
}
return resultDomain;
}
@Override
@Transactional(rollbackFor = Exception.class)
public ResultDomain<Boolean> deleteDefault(String defaultId) {
ResultDomain<Boolean> resultDomain = new ResultDomain<>();
try {
int count = emailDefaultMapper.deleteDefault(defaultId);
if (count > 0) {
resultDomain.success("删除默认接收人成功", true);
} else {
resultDomain.fail("删除默认接收人失败");
}
} catch (Exception e) {
logger.error("删除默认接收人异常: {}", e.getMessage(), e);
resultDomain.fail("删除默认接收人失败: " + e.getMessage());
}
return resultDomain;
}
@Override
public ResultDomain<TbCrontabEmailDefault> getDefaultById(String defaultId) {
ResultDomain<TbCrontabEmailDefault> resultDomain = new ResultDomain<>();
try {
TbCrontabEmailDefault emailDefault = emailDefaultMapper.selectDefaultById(defaultId);
if (emailDefault != null) {
resultDomain.success("查询默认接收人成功", emailDefault);
} else {
resultDomain.fail("默认接收人不存在");
}
} catch (Exception e) {
logger.error("查询默认接收人异常: {}", e.getMessage(), e);
resultDomain.fail("查询默认接收人失败: " + e.getMessage());
}
return resultDomain;
}
@Override
public ResultDomain<TbCrontabEmailDefault> getDefaultByMetaId(String metaId) {
ResultDomain<TbCrontabEmailDefault> resultDomain = new ResultDomain<>();
try {
List<TbCrontabEmailDefault> emailDefault = emailDefaultMapper.selectDefaultByMetaId(metaId);
if (emailDefault != null) {
resultDomain.success("查询默认接收人成功", emailDefault);
} else {
resultDomain.fail("该任务元数据未配置默认接收人");
}
} catch (Exception e) {
logger.error("查询默认接收人异常: {}", e.getMessage(), e);
resultDomain.fail("查询默认接收人失败: " + e.getMessage());
}
return resultDomain;
}
}

View File

@@ -0,0 +1,190 @@
package org.xyzh.crontab.service.impl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.xyzh.api.crontab.EmailRecipientService;
import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.core.page.PageDomain;
import org.xyzh.common.core.page.PageParam;
import org.xyzh.common.core.page.PageRequest;
import org.xyzh.common.dto.crontab.TbCrontabEmailRecipient;
import org.xyzh.common.utils.IDUtils;
import org.xyzh.crontab.mapper.EmailRecipientMapper;
import java.util.Date;
import java.util.List;
/**
* @description 定时任务邮件接收人服务实现类
* @filename EmailRecipientServiceImpl.java
* @author yslg
* @copyright xyzh
* @since 2025-11-18
*/
@Service
public class EmailRecipientServiceImpl implements EmailRecipientService {
private static final Logger logger = LoggerFactory.getLogger(EmailRecipientServiceImpl.class);
@Autowired
private EmailRecipientMapper emailRecipientMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public ResultDomain<TbCrontabEmailRecipient> createRecipient(TbCrontabEmailRecipient recipient) {
ResultDomain<TbCrontabEmailRecipient> resultDomain = new ResultDomain<>();
try {
// 生成ID
recipient.setID(IDUtils.generateID());
if (recipient.getRecipientId() == null || recipient.getRecipientId().isEmpty()) {
recipient.setRecipientId(IDUtils.generateID());
}
recipient.setCreateTime(new Date());
recipient.setDeleted(false);
int count = emailRecipientMapper.insertRecipient(recipient);
if (count > 0) {
resultDomain.success("创建邮件接收人成功", recipient);
} else {
resultDomain.fail("创建邮件接收人失败");
}
} catch (Exception e) {
logger.error("创建邮件接收人异常: {}", e.getMessage(), e);
resultDomain.fail("创建邮件接收人失败: " + e.getMessage());
}
return resultDomain;
}
@Override
@Transactional(rollbackFor = Exception.class)
public ResultDomain<Boolean> batchCreateRecipient(List<TbCrontabEmailRecipient> recipients) {
ResultDomain<Boolean> resultDomain = new ResultDomain<>();
try {
for (TbCrontabEmailRecipient recipient : recipients) {
recipient.setID(IDUtils.generateID());
if (recipient.getRecipientId() == null || recipient.getRecipientId().isEmpty()) {
recipient.setRecipientId(IDUtils.generateID());
}
recipient.setCreateTime(new Date());
recipient.setDeleted(false);
}
int count = emailRecipientMapper.batchInsertRecipient(recipients);
if (count > 0) {
resultDomain.setSuccess(true);
resultDomain.setMessage("批量创建邮件接收人成功");
} else {
resultDomain.fail("批量创建邮件接收人失败");
}
} catch (Exception e) {
logger.error("批量创建邮件接收人异常: {}", e.getMessage(), e);
resultDomain.fail("批量创建邮件接收人失败: " + e.getMessage());
}
return resultDomain;
}
@Override
@Transactional(rollbackFor = Exception.class)
public ResultDomain<TbCrontabEmailRecipient> updateRecipient(TbCrontabEmailRecipient recipient) {
ResultDomain<TbCrontabEmailRecipient> resultDomain = new ResultDomain<>();
try {
recipient.setUpdateTime(new Date());
int count = emailRecipientMapper.updateRecipient(recipient);
if (count > 0) {
resultDomain.success("更新邮件接收人成功", recipient);
} else {
resultDomain.fail("更新邮件接收人失败");
}
} catch (Exception e) {
logger.error("更新邮件接收人异常: {}", e.getMessage(), e);
resultDomain.fail("更新邮件接收人失败: " + e.getMessage());
}
return resultDomain;
}
@Override
@Transactional(rollbackFor = Exception.class)
public ResultDomain<Boolean> deleteRecipient(String recipientId) {
ResultDomain<Boolean> resultDomain = new ResultDomain<>();
try {
int count = emailRecipientMapper.deleteRecipient(recipientId);
if (count > 0) {
resultDomain.success("删除邮件接收人成功", true);
} else {
resultDomain.fail("删除邮件接收人失败");
}
} catch (Exception e) {
logger.error("删除邮件接收人异常: {}", e.getMessage(), e);
resultDomain.fail("删除邮件接收人失败: " + e.getMessage());
}
return resultDomain;
}
@Override
public ResultDomain<TbCrontabEmailRecipient> getRecipientById(String recipientId) {
ResultDomain<TbCrontabEmailRecipient> resultDomain = new ResultDomain<>();
try {
TbCrontabEmailRecipient recipient = emailRecipientMapper.selectRecipientById(recipientId);
if (recipient != null) {
resultDomain.success("查询邮件接收人成功", recipient);
} else {
resultDomain.fail("邮件接收人不存在");
}
} catch (Exception e) {
logger.error("查询邮件接收人异常: {}", e.getMessage(), e);
resultDomain.fail("查询邮件接收人失败: " + e.getMessage());
}
return resultDomain;
}
@Override
public ResultDomain<TbCrontabEmailRecipient> getRecipientsByTaskId(String taskId) {
ResultDomain<TbCrontabEmailRecipient> resultDomain = new ResultDomain<>();
try {
List<TbCrontabEmailRecipient> list = emailRecipientMapper.selectRecipientsByTaskId(taskId);
resultDomain.success("查询邮件接收人列表成功", list);
} catch (Exception e) {
logger.error("查询邮件接收人列表异常: {}", e.getMessage(), e);
resultDomain.fail("查询邮件接收人列表失败: " + e.getMessage());
}
return resultDomain;
}
@Override
public ResultDomain<TbCrontabEmailRecipient> getRecipientPage(PageRequest<TbCrontabEmailRecipient> request) {
ResultDomain<TbCrontabEmailRecipient> resultDomain = new ResultDomain<>();
try {
PageParam pageParam = request.getPageParam();
List<TbCrontabEmailRecipient> list = emailRecipientMapper.selectRecipientPage(request.getFilter(), pageParam);
int total = emailRecipientMapper.countSelectRecipient(request.getFilter());
PageDomain<TbCrontabEmailRecipient> pageDomain = new PageDomain<>();
pageDomain.setPageParam(pageParam);
pageParam.setTotalElements(total);
pageDomain.setDataList(list);
resultDomain.success("分页查询邮件接收人成功", pageDomain);
} catch (Exception e) {
logger.error("分页查询邮件接收人异常: {}", e.getMessage(), e);
resultDomain.fail("分页查询邮件接收人失败: " + e.getMessage());
}
return resultDomain;
}
@Override
@Transactional(rollbackFor = Exception.class)
public ResultDomain<Boolean> deleteRecipientsByTaskId(String taskId) {
ResultDomain<Boolean> resultDomain = new ResultDomain<>();
try {
int count = emailRecipientMapper.deleteRecipientsByTaskId(taskId);
resultDomain.success("删除任务的所有接收人成功,共" + count + "", true);
} catch (Exception e) {
logger.error("删除任务的所有接收人异常: {}", e.getMessage(), e);
resultDomain.fail("删除任务的所有接收人失败: " + e.getMessage());
}
return resultDomain;
}
}

View File

@@ -0,0 +1,187 @@
package org.xyzh.crontab.service.impl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.xyzh.api.crontab.TaskMetaService;
import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.core.page.PageDomain;
import org.xyzh.common.core.page.PageParam;
import org.xyzh.common.core.page.PageRequest;
import org.xyzh.common.dto.crontab.TbCrontabTaskMeta;
import org.xyzh.common.utils.IDUtils;
import org.xyzh.crontab.mapper.TaskMetaMapper;
import java.util.Date;
import java.util.List;
/**
* @description 定时任务元数据服务实现类
* @filename TaskMetaServiceImpl.java
* @author yslg
* @copyright xyzh
* @since 2025-11-18
*/
@Service
public class TaskMetaServiceImpl implements TaskMetaService {
private static final Logger logger = LoggerFactory.getLogger(TaskMetaServiceImpl.class);
@Autowired
private TaskMetaMapper taskMetaMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public ResultDomain<TbCrontabTaskMeta> createTaskMeta(TbCrontabTaskMeta taskMeta) {
ResultDomain<TbCrontabTaskMeta> resultDomain = new ResultDomain<>();
try {
// 检查meta_id是否已存在
TbCrontabTaskMeta existing = taskMetaMapper.selectTaskMetaById(taskMeta.getMetaId());
if (existing != null) {
resultDomain.fail("任务元数据ID已存在: " + taskMeta.getMetaId());
return resultDomain;
}
// 生成ID
taskMeta.setID(IDUtils.generateID());
taskMeta.setCreateTime(new Date());
taskMeta.setDeleted(false);
// 默认值
if (taskMeta.getSortOrder() == null) {
taskMeta.setSortOrder(0);
}
int count = taskMetaMapper.insertTaskMeta(taskMeta);
if (count > 0) {
resultDomain.success("创建任务元数据成功", taskMeta);
} else {
resultDomain.fail("创建任务元数据失败");
}
} catch (Exception e) {
logger.error("创建任务元数据异常: {}", e.getMessage(), e);
resultDomain.fail("创建任务元数据失败: " + e.getMessage());
}
return resultDomain;
}
@Override
@Transactional(rollbackFor = Exception.class)
public ResultDomain<TbCrontabTaskMeta> updateTaskMeta(TbCrontabTaskMeta taskMeta) {
ResultDomain<TbCrontabTaskMeta> resultDomain = new ResultDomain<>();
try {
taskMeta.setUpdateTime(new Date());
int count = taskMetaMapper.updateTaskMeta(taskMeta);
if (count > 0) {
resultDomain.success("更新任务元数据成功", taskMeta);
} else {
resultDomain.fail("更新任务元数据失败");
}
} catch (Exception e) {
logger.error("更新任务元数据异常: {}", e.getMessage(), e);
resultDomain.fail("更新任务元数据失败: " + e.getMessage());
}
return resultDomain;
}
@Override
@Transactional(rollbackFor = Exception.class)
public ResultDomain<Boolean> deleteTaskMeta(String metaId) {
ResultDomain<Boolean> resultDomain = new ResultDomain<>();
try {
int count = taskMetaMapper.deleteTaskMeta(metaId);
if (count > 0) {
resultDomain.success("删除任务元数据成功", true);
} else {
resultDomain.fail("删除任务元数据失败");
}
} catch (Exception e) {
logger.error("删除任务元数据异常: {}", e.getMessage(), e);
resultDomain.fail("删除任务元数据失败: " + e.getMessage());
}
return resultDomain;
}
@Override
public ResultDomain<TbCrontabTaskMeta> getTaskMetaById(String metaId) {
ResultDomain<TbCrontabTaskMeta> resultDomain = new ResultDomain<>();
try {
TbCrontabTaskMeta taskMeta = taskMetaMapper.selectTaskMetaById(metaId);
if (taskMeta != null) {
resultDomain.success("查询任务元数据成功", taskMeta);
} else {
resultDomain.fail("任务元数据不存在");
}
} catch (Exception e) {
logger.error("查询任务元数据异常: {}", e.getMessage(), e);
resultDomain.fail("查询任务元数据失败: " + e.getMessage());
}
return resultDomain;
}
@Override
public ResultDomain<TbCrontabTaskMeta> getTaskMetaByTaskId(String taskId) {
ResultDomain<TbCrontabTaskMeta> resultDomain = new ResultDomain<>();
try {
TbCrontabTaskMeta taskMeta = taskMetaMapper.selectTaskMetaByTaskId(taskId);
if (taskMeta != null) {
resultDomain.success("查询任务元数据成功", taskMeta);
} else {
resultDomain.fail("任务元数据不存在");
}
} catch (Exception e) {
logger.error("查询任务元数据异常: {}", e.getMessage(), e);
resultDomain.fail("查询任务元数据失败: " + e.getMessage());
}
return resultDomain;
}
@Override
public ResultDomain<TbCrontabTaskMeta> getAllTaskMeta() {
ResultDomain<TbCrontabTaskMeta> resultDomain = new ResultDomain<>();
try {
List<TbCrontabTaskMeta> list = taskMetaMapper.selectAllTaskMeta();
resultDomain.success("查询所有任务元数据成功", list);
} catch (Exception e) {
logger.error("查询所有任务元数据异常: {}", e.getMessage(), e);
resultDomain.fail("查询所有任务元数据失败: " + e.getMessage());
}
return resultDomain;
}
@Override
public ResultDomain<TbCrontabTaskMeta> getTaskMetaByCategory(String category) {
ResultDomain<TbCrontabTaskMeta> resultDomain = new ResultDomain<>();
try {
List<TbCrontabTaskMeta> list = taskMetaMapper.selectTaskMetaByCategory(category);
resultDomain.success("查询分类任务元数据成功", list);
} catch (Exception e) {
logger.error("查询分类任务元数据异常: {}", e.getMessage(), e);
resultDomain.fail("查询分类任务元数据失败: " + e.getMessage());
}
return resultDomain;
}
@Override
public ResultDomain<TbCrontabTaskMeta> getTaskMetaPage(PageRequest<TbCrontabTaskMeta> request) {
ResultDomain<TbCrontabTaskMeta> resultDomain = new ResultDomain<>();
try {
PageParam pageParam = request.getPageParam();
List<TbCrontabTaskMeta> list = taskMetaMapper.selectTaskMetaPage(request.getFilter(), pageParam);
int total = taskMetaMapper.countSelectTaskMeta(request.getFilter());
PageDomain<TbCrontabTaskMeta> pageDomain = new PageDomain<>();
pageDomain.setPageParam(pageParam);
pageParam.setTotalElements(total);
pageDomain.setDataList(list);
resultDomain.success("分页查询任务元数据成功", pageDomain);
} catch (Exception e) {
logger.error("分页查询任务元数据异常: {}", e.getMessage(), e);
resultDomain.fail("分页查询任务元数据失败: " + e.getMessage());
}
return resultDomain;
}
}

View File

@@ -1,14 +1,14 @@
package org.xyzh.crontab.task;
import org.springframework.beans.factory.annotation.Autowired;
import org.xyzh.crontab.config.CrawlerProperties;
import org.xyzh.api.system.config.SysConfigService;
import org.xyzh.crontab.pojo.TaskParams;
import java.util.ArrayList;
import java.util.List;
/**
* @description Python命令任务抽象类
* @description Python命令任务抽象类(从数据库获取配置)
* @filename PythonCommandTask.java
* @author yslg
* @copyright xyzh
@@ -17,23 +17,36 @@ import java.util.List;
public abstract class PythonCommandTask extends CommandTask {
@Autowired
protected CrawlerProperties crawlerProperties;
protected SysConfigService sysConfigService;
/**
* 获取Python可执行文件路径
* 获取Python可执行文件路径(从数据库系统配置获取)
*/
protected String getPythonPath() {
return crawlerProperties.getPythonPath() != null
? crawlerProperties.getPythonPath()
: "python";
try {
String value = sysConfigService.getStringConfig("crawler.pythonPath");
if (value != null && !value.isEmpty()) {
return value;
}
} catch (Exception e) {
logger.warn("获取Python路径配置失败使用默认值: {}", e.getMessage());
}
return "python"; // 默认值
}
/**
* 获取脚本基础路径
* 获取脚本基础路径(从数据库系统配置获取)
*/
protected String getScriptBasePath() {
return crawlerProperties.getBasePath() != null
? crawlerProperties.getBasePath()
: "../schoolNewsCrawler";
try {
String value = sysConfigService.getStringConfig("crawler.basePath");
if (value != null && !value.isEmpty()) {
return value;
}
} catch (Exception e) {
logger.warn("获取脚本基础路径配置失败,使用默认值: {}", e.getMessage());
}
return "../schoolNewsCrawler"; // 默认值
}
/**

View File

@@ -4,11 +4,19 @@ import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.TypeReference;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.xyzh.api.crontab.CrontabService;
import org.xyzh.api.crontab.DataCollectionItemService;
import org.xyzh.api.crontab.EmailDefaultService;
import org.xyzh.api.crontab.EmailRecipientService;
import org.xyzh.api.crontab.TaskMetaService;
import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.dto.crontab.TbCrontabEmailDefault;
import org.xyzh.common.dto.crontab.TbCrontabEmailRecipient;
import org.xyzh.common.dto.crontab.TbCrontabTask;
import org.xyzh.common.dto.crontab.TbCrontabTaskMeta;
import org.xyzh.common.dto.crontab.TbDataCollectionItem;
import org.xyzh.common.utils.EmailUtils;
import org.xyzh.common.utils.IDUtils;
import org.xyzh.crontab.config.CrontabProperties;
import org.xyzh.crontab.pojo.TaskParams;
import org.xyzh.crontab.task.PythonCommandTask;
@@ -18,9 +26,9 @@ import java.nio.file.Paths;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* @description 新闻爬虫定时任务
@@ -32,11 +40,23 @@ import java.util.Map;
@Component("newsCrewerTask")
public class NewsCrawlerTask extends PythonCommandTask {
@Autowired
private CrontabProperties crontabProperties;
@Autowired
private DataCollectionItemService itemService;
@Autowired
private TaskMetaService taskMetaService;
@Autowired
private CrontabService crontabService;
@Autowired
private EmailDefaultService emailDefaultService;
@Autowired
private EmailRecipientService emailRecipientService;
@Autowired
private EmailUtils emailUtils;
/**
* 构建Python脚本参数
@@ -45,11 +65,23 @@ public class NewsCrawlerTask extends PythonCommandTask {
protected List<String> buildPythonArgs(TaskParams taskParams) throws Exception {
List<String> args = new ArrayList<>();
// 1. 从params获取scriptPath
String scriptPath = taskParams.getParamAsString("scriptPath");
if (scriptPath == null || scriptPath.isEmpty()) {
throw new Exception("scriptPath参数缺失");
// 1. 从数据库读取scriptPath通过metaId
String metaId = taskParams.getParamAsString("metaId");
if (metaId == null || metaId.isEmpty()) {
throw new Exception("metaId参数缺失无法获取scriptPath");
}
ResultDomain<TbCrontabTaskMeta> metaResult = taskMetaService.getTaskMetaById(metaId);
if (!metaResult.isSuccess() || metaResult.getData() == null) {
throw new Exception("未找到任务元数据: metaId=" + metaId);
}
String scriptPath = metaResult.getData().getScriptPath();
if (scriptPath == null || scriptPath.isEmpty()) {
throw new Exception("任务元数据中scriptPath为空: metaId=" + metaId);
}
logger.info("从数据库读取scriptPath: {}, metaId: {}", scriptPath, metaId);
// 2. 生成输出文件名
String timestamp = String.valueOf(System.currentTimeMillis());
@@ -70,7 +102,7 @@ public class NewsCrawlerTask extends PythonCommandTask {
Object value = entry.getValue();
// 跳过特殊参数
if (key.startsWith("_") || key.equals("scriptPath") ||
if (key.startsWith("_") || key.equals("metaId") ||
key.equals("taskId") || key.equals("logId")) {
continue;
}
@@ -130,7 +162,12 @@ public class NewsCrawlerTask extends PythonCommandTask {
// 保存新闻数据到数据库
if (taskId != null && !taskId.isEmpty() && logId != null && !logId.isEmpty()) {
saveNewsToDatabase(newsList, taskId, logId);
ResultDomain<TbCrontabTask> taskResult = crontabService.getTaskById(taskId);
saveNewsToDatabase(newsList, taskResult.getData(), logId);
// 发送邮件通知
sendEmailNotification(taskId, taskResult.getData(), newsList);
} else {
logger.warn("未提供任务ID或日志ID跳过数据保存");
}
@@ -139,14 +176,20 @@ public class NewsCrawlerTask extends PythonCommandTask {
/**
* 将新闻数据保存到数据库
*/
private void saveNewsToDatabase(List<ArticleStruct> newsList, String taskId, String logId) {
private void saveNewsToDatabase(List<ArticleStruct> newsList, TbCrontabTask task, String logId) {
String taskId = task.getTaskId();
logger.info("开始保存 {} 条新闻到数据库任务ID: {}日志ID: {}", newsList.size(), taskId, logId);
try {
List<TbDataCollectionItem> itemList = new ArrayList<>();
ResultDomain<TbCrontabTaskMeta> metaResult = taskMetaService.getTaskMetaByTaskId(taskId);
if (!metaResult.isSuccess() || metaResult.getData() == null) {
throw new Exception("未找到任务元数据: taskId=" + taskId);
}
TbCrontabTaskMeta taskMeta = metaResult.getData();
Date now = new Date();
SimpleDateFormat parser = new SimpleDateFormat("yyyy年MM月dd日HH:mm");
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
for (ArticleStruct news : newsList) {
@@ -209,9 +252,155 @@ public class NewsCrawlerTask extends PythonCommandTask {
} else {
logger.warn("没有有效的新闻数据需要保存");
}
if (taskMeta.getAutoPublish()){
publishNewsToArticle(newsList, task, logId);
}
} catch (Exception e) {
logger.error("保存新闻数据到数据库异常: ", e);
}
}
/**
* 发送邮件通知
*/
private void sendEmailNotification(String taskId, TbCrontabTask task, List<ArticleStruct> newsList) {
try {
List<String> recipients = new ArrayList<>();
// 2. 如果使用默认接收人,查询默认接收人列表
if (Boolean.TRUE.equals(task.getDefaultRecipient()) && task.getMetaId() != null) {
ResultDomain<TbCrontabEmailDefault> defaultResult = emailDefaultService.getDefaultByMetaId(task.getMetaId());
if (defaultResult.isSuccess() && defaultResult.getDataList() != null) {
for (TbCrontabEmailDefault defaultRecipient : defaultResult.getDataList()) {
if (defaultRecipient.getUserEmail() != null && !defaultRecipient.getUserEmail().isEmpty()) {
recipients.add(defaultRecipient.getUserEmail());
}
}
}
}
// 3. 查询额外接收人
ResultDomain<TbCrontabEmailRecipient> recipientResult = emailRecipientService.getRecipientsByTaskId(taskId);
if (recipientResult.isSuccess() && recipientResult.getDataList() != null) {
for (TbCrontabEmailRecipient recipient : recipientResult.getDataList()) {
if (recipient.getEmail() != null && !recipient.getEmail().isEmpty()) {
recipients.add(recipient.getEmail());
}
}
}
// 4. 去重
recipients = recipients.stream().distinct().collect(Collectors.toList());
if (recipients.isEmpty()) {
logger.info("任务 {} 没有配置接收人,跳过邮件发送", task.getTaskName());
return;
}
// 5. 构建邮件内容
String subject = "【新闻爬虫通知】" + task.getTaskName() + " 执行完成";
String content = buildEmailContent(task.getTaskName(), newsList);
// 6. 发送邮件
int successCount = 0;
for (String email : recipients) {
if (emailUtils.sendHtmlEmail(email, subject, content)) {
successCount++;
}
}
logger.info("邮件发送完成,成功发送: {}/{},任务: {}", successCount, recipients.size(), task.getTaskName());
} catch (Exception e) {
logger.error("发送邮件通知异常,但不影响任务执行: ", e);
}
}
/**
* 构建邮件HTML内容
*/
private String buildEmailContent(String taskName, List<ArticleStruct> newsList) {
StringBuilder html = new StringBuilder();
html.append("<!DOCTYPE html>")
.append("<html>")
.append("<head>")
.append("<meta charset='UTF-8'>")
.append("<style>")
.append("body { font-family: 'Microsoft YaHei', Arial, sans-serif; background-color: #f5f5f5; margin: 0; padding: 20px; }")
.append(".container { max-width: 800px; margin: 0 auto; background-color: #ffffff; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); overflow: hidden; }")
.append(".header { background: linear-gradient(135deg, #C62828 0%, #E53935 100%); padding: 30px; text-align: center; color: #ffffff; }")
.append(".header h1 { margin: 0; font-size: 24px; }")
.append(".content { padding: 30px; }")
.append(".summary { background-color: #f8f9fa; border-left: 4px solid #C62828; padding: 15px; margin: 20px 0; }")
.append(".news-list { margin-top: 20px; }")
.append(".news-item { border-bottom: 1px solid #e0e0e0; padding: 15px 0; }")
.append(".news-item:last-child { border-bottom: none; }")
.append(".news-title { font-size: 16px; font-weight: bold; color: #333; margin-bottom: 8px; }")
.append(".news-meta { font-size: 14px; color: #666; }")
.append(".news-link { color: #C62828; text-decoration: none; }")
.append(".footer { background-color: #f8f9fa; padding: 20px; text-align: center; color: #999; font-size: 12px; }")
.append("</style>")
.append("</head>")
.append("<body>")
.append("<div class='container'>")
.append("<div class='header'>")
.append("<h1>新闻爬虫执行通知</h1>")
.append("</div>")
.append("<div class='content'>");
// 摘要信息
html.append("<div class='summary'>")
.append("<p><strong>任务名称:</strong>").append(taskName).append("</p>")
.append("<p><strong>执行时间:</strong>").append(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())).append("</p>")
.append("<p><strong>爬取数量:</strong>").append(newsList.size()).append(" 条</p>")
.append("</div>");
// 新闻列表
html.append("<div class='news-list'>")
.append("<h3>爬取内容:</h3>");
int count = Math.min(newsList.size(), 10); // 最多显示10条
for (int i = 0; i < count; i++) {
ArticleStruct news = newsList.get(i);
html.append("<div class='news-item'>")
.append("<div class='news-title'>").append(news.getTitle() != null ? news.getTitle() : "无标题").append("</div>")
.append("<div class='news-meta'>")
.append("来源:").append(news.getSource() != null ? news.getSource() : "未知")
.append(" | ")
.append("发布时间:").append(news.getPublishTime() != null ? news.getPublishTime() : "未知");
if (news.getUrl() != null && !news.getUrl().isEmpty()) {
html.append(" | <a href='").append(news.getUrl()).append("' class='news-link' target='_blank'>查看原文</a>");
}
html.append("</div>")
.append("</div>");
}
if (newsList.size() > 10) {
html.append("<p style='text-align: center; color: #666; margin-top: 15px;'>")
.append("还有 ").append(newsList.size() - 10).append(" 条新闻未显示,请登录系统查看详情")
.append("</p>");
}
html.append("</div>"); // news-list
html.append("</div>"); // content
// 页脚
html.append("<div class='footer'>")
.append("<p>此邮件由系统自动发送,请勿回复</p>")
.append("<p>Copyright © 红色思政智能体平台</p>")
.append("</div>")
.append("</div>") // container
.append("</body>")
.append("</html>");
return html.toString();
}
// TODO 自动发布功能,把采集的数据发布到文章表
private void publishNewsToArticle(List<ArticleStruct> newsList, TbCrontabTask task, String logId) {
}
}

View File

@@ -47,5 +47,3 @@ crontab:
description: 是否是昨天
type: Boolean
value: true

View File

@@ -11,6 +11,8 @@
<result column="bean_name" property="beanName" />
<result column="method_name" property="methodName" />
<result column="method_params" property="methodParams" />
<result column="meta_id" property="metaId" />
<result column="default_recipient" property="defaultRecipient" />
<result column="cron_expression" property="cronExpression" />
<result column="status" property="status" />
<result column="description" property="description" />
@@ -26,7 +28,7 @@
<!-- 字段列表 -->
<sql id="Base_Column_List">
id, task_id, task_name, task_group, bean_name, method_name, method_params,
id, task_id, task_name, task_group, bean_name, method_name, method_params, meta_id, default_recipient,
cron_expression, status, description, concurrent, misfire_policy,
creator, updater, create_time, update_time, delete_time, deleted
</sql>
@@ -132,6 +134,8 @@
<if test="task.beanName != null">bean_name,</if>
<if test="task.methodName != null">method_name,</if>
<if test="task.methodParams != null">method_params,</if>
<if test="task.metaId != null">meta_id,</if>
<if test="task.defaultRecipient != null">default_recipient,</if>
<if test="task.cronExpression != null">cron_expression,</if>
<if test="task.status != null">status,</if>
<if test="task.description != null">description,</if>
@@ -150,6 +154,8 @@
<if test="task.beanName != null">#{task.beanName},</if>
<if test="task.methodName != null">#{task.methodName},</if>
<if test="task.methodParams != null">#{task.methodParams},</if>
<if test="task.metaId != null">#{task.metaId},</if>
<if test="task.defaultRecipient != null">#{task.defaultRecipient},</if>
<if test="task.cronExpression != null">#{task.cronExpression},</if>
<if test="task.status != null">#{task.status},</if>
<if test="task.description != null">#{task.description},</if>
@@ -170,6 +176,8 @@
<if test="task.beanName != null">bean_name = #{task.beanName},</if>
<if test="task.methodName != null">method_name = #{task.methodName},</if>
<if test="task.methodParams != null">method_params = #{task.methodParams},</if>
<if test="task.metaId != null">meta_id = #{task.metaId},</if>
<if test="task.defaultRecipient != null">default_recipient = #{task.defaultRecipient},</if>
<if test="task.cronExpression != null">cron_expression = #{task.cronExpression},</if>
<if test="task.status != null">status = #{task.status},</if>
<if test="task.description != null">description = #{task.description},</if>

View File

@@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.xyzh.crontab.mapper.EmailDefaultMapper">
<!-- 结果映射 -->
<resultMap id="BaseResultMap" type="org.xyzh.common.dto.crontab.TbCrontabEmailDefault">
<id column="id" property="ID" />
<result column="default_id" property="defaultId" />
<result column="meta_id" property="metaId" />
<result column="user_id" property="userId" />
<result column="user_email" property="userEmail" />
<result column="username" property="username" />
<result column="creator" property="creator" />
<result column="updater" property="updater" />
<result column="create_time" property="createTime" />
<result column="update_time" property="updateTime" />
<result column="delete_time" property="deleteTime" />
<result column="deleted" property="deleted" />
</resultMap>
<!-- 字段列表 -->
<sql id="Base_Column_List">
id, default_id, meta_id, user_id,
creator, updater, create_time, update_time, delete_time, deleted
</sql>
<!-- 插入默认接收人 -->
<insert id="insertDefault" parameterType="org.xyzh.common.dto.crontab.TbCrontabEmailDefault">
INSERT INTO tb_crontab_email_default (
id, default_id, meta_id, user_id, creator, create_time
) VALUES (
#{emailDefault.ID}, #{emailDefault.defaultId}, #{emailDefault.metaId},
#{emailDefault.userId}, #{emailDefault.creator}, NOW()
)
</insert>
<!-- 更新默认接收人 -->
<update id="updateDefault" parameterType="org.xyzh.common.dto.crontab.TbCrontabEmailDefault">
UPDATE tb_crontab_email_default
SET meta_id = #{emailDefault.metaId},
user_id = #{emailDefault.userId},
updater = #{emailDefault.updater},
update_time = NOW()
WHERE default_id = #{emailDefault.defaultId}
AND deleted = 0
</update>
<!-- 删除默认接收人(逻辑删除) -->
<update id="deleteDefault">
UPDATE tb_crontab_email_default
SET deleted = 1,
delete_time = NOW()
WHERE default_id = #{defaultId}
AND deleted = 0
</update>
<!-- 根据defaultId查询 -->
<select id="selectDefaultById" resultMap="BaseResultMap">
SELECT tced.id, tced.default_id, tced.meta_id, tced.user_id, tu.email as user_email, tu.username
FROM tb_crontab_email_default tced
LEFT JOIN tb_sys_user tu ON tced.user_id = tu.id
WHERE tced.default_id = #{defaultId}
AND tced.deleted = 0
</select>
<!-- 根据metaId查询 -->
<select id="selectDefaultByMetaId" resultMap="BaseResultMap">
SELECT tced.id, tced.default_id, tced.meta_id, tced.user_id, tu.email as user_email, tu.username
FROM tb_crontab_email_default tced
LEFT JOIN tb_sys_user tu ON tced.user_id = tu.id
WHERE tced.meta_id = #{metaId}
AND tced.deleted = 0
LIMIT 1
</select>
</mapper>

View File

@@ -0,0 +1,141 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.xyzh.crontab.mapper.EmailRecipientMapper">
<!-- 结果映射 -->
<resultMap id="BaseResultMap" type="org.xyzh.common.dto.crontab.TbCrontabEmailRecipient">
<id column="id" property="ID" />
<result column="recipient_id" property="recipientId" />
<result column="task_id" property="taskId" />
<result column="user_id" property="userId" />
<result column="email" property="email" />
<result column="name" property="name" />
<result column="creator" property="creator" />
<result column="updater" property="updater" />
<result column="create_time" property="createTime" />
<result column="update_time" property="updateTime" />
<result column="delete_time" property="deleteTime" />
<result column="deleted" property="deleted" />
</resultMap>
<!-- 字段列表 -->
<sql id="Base_Column_List">
id, recipient_id, task_id, user_id, email, name,
creator, updater, create_time, update_time, delete_time, deleted
</sql>
<!-- 查询条件 -->
<sql id="Base_Where_Clause">
<where>
deleted = 0
<if test="filter.recipientId != null and filter.recipientId != ''">
AND recipient_id = #{filter.recipientId}
</if>
<if test="filter.taskId != null and filter.taskId != ''">
AND task_id = #{filter.taskId}
</if>
<if test="filter.userId != null and filter.userId != ''">
AND user_id = #{filter.userId}
</if>
<if test="filter.email != null and filter.email != ''">
AND email LIKE CONCAT('%', #{filter.email}, '%')
</if>
</where>
</sql>
<!-- 插入邮件接收人 -->
<insert id="insertRecipient" parameterType="org.xyzh.common.dto.crontab.TbCrontabEmailRecipient">
INSERT INTO tb_crontab_email_recipient (
id, recipient_id, task_id, user_id, email, name, creator, create_time
) VALUES (
#{recipient.ID}, #{recipient.recipientId}, #{recipient.taskId},
#{recipient.userId}, #{recipient.email}, #{recipient.name},
#{recipient.creator}, NOW()
)
</insert>
<!-- 批量插入邮件接收人 -->
<insert id="batchInsertRecipient">
INSERT INTO tb_crontab_email_recipient (
id, recipient_id, task_id, user_id, email, name, creator, create_time
) VALUES
<foreach collection="recipients" item="item" separator=",">
(#{item.ID}, #{item.recipientId}, #{item.taskId},
#{item.userId}, #{item.email}, #{item.name}, #{item.creator}, NOW())
</foreach>
</insert>
<!-- 更新邮件接收人 -->
<update id="updateRecipient" parameterType="org.xyzh.common.dto.crontab.TbCrontabEmailRecipient">
UPDATE tb_crontab_email_recipient
SET task_id = #{recipient.taskId},
user_id = #{recipient.userId},
email = #{recipient.email},
name = #{recipient.name},
updater = #{recipient.updater},
update_time = NOW()
WHERE recipient_id = #{recipient.recipientId}
AND deleted = 0
</update>
<!-- 删除邮件接收人(逻辑删除) -->
<update id="deleteRecipient">
UPDATE tb_crontab_email_recipient
SET deleted = 1,
delete_time = NOW()
WHERE recipient_id = #{recipientId}
AND deleted = 0
</update>
<!-- 根据ID查询 -->
<select id="selectRecipientById" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List" />
FROM tb_crontab_email_recipient
WHERE recipient_id = #{recipientId}
AND deleted = 0
</select>
<!-- 根据任务ID查询接收人列表 -->
<select id="selectRecipientsByTaskId" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List" />
FROM tb_crontab_email_recipient
WHERE task_id = #{taskId}
AND deleted = 0
ORDER BY create_time ASC
</select>
<!-- 查询所有接收人 -->
<select id="selectAllRecipients" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List" />
FROM tb_crontab_email_recipient
WHERE deleted = 0
ORDER BY create_time ASC
</select>
<!-- 分页查询 -->
<select id="selectRecipientPage" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List" />
FROM tb_crontab_email_recipient
<include refid="Base_Where_Clause" />
ORDER BY create_time DESC
LIMIT #{pageParam.offset}, #{pageParam.pageSize}
</select>
<!-- 查询总数 -->
<select id="countSelectRecipient" resultType="int">
SELECT COUNT(*)
FROM tb_crontab_email_recipient
<include refid="Base_Where_Clause" />
</select>
<!-- 删除任务的所有接收人 -->
<update id="deleteRecipientsByTaskId">
UPDATE tb_crontab_email_recipient
SET deleted = 1,
delete_time = NOW()
WHERE task_id = #{taskId}
AND deleted = 0
</update>
</mapper>

View File

@@ -0,0 +1,139 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.xyzh.crontab.mapper.TaskMetaMapper">
<!-- 结果映射 -->
<resultMap id="BaseResultMap" type="org.xyzh.common.dto.crontab.TbCrontabTaskMeta">
<id column="id" property="ID" />
<result column="meta_id" property="metaId" />
<result column="name" property="name" />
<result column="description" property="description" />
<result column="category" property="category" />
<result column="bean_name" property="beanName" />
<result column="method_name" property="methodName" />
<result column="script_path" property="scriptPath" />
<result column="param_schema" property="paramSchema" />
<result column="auto_publish" property="autoPublish" />
<result column="sort_order" property="sortOrder" />
<result column="creator" property="creator" />
<result column="updater" property="updater" />
<result column="create_time" property="createTime" />
<result column="update_time" property="updateTime" />
<result column="delete_time" property="deleteTime" />
<result column="deleted" property="deleted" />
</resultMap>
<!-- 字段列表 -->
<sql id="Base_Column_List">
id, meta_id, name, description, category, bean_name, method_name,
script_path, param_schema, auto_publish, sort_order,
creator, updater, create_time, update_time, delete_time, deleted
</sql>
<!-- 查询条件 -->
<sql id="Base_Where_Clause">
<where>
deleted = 0
<if test="filter.metaId != null and filter.metaId != ''">
AND meta_id = #{filter.metaId}
</if>
<if test="filter.name != null and filter.name != ''">
AND name LIKE CONCAT('%', #{filter.name}, '%')
</if>
<if test="filter.category != null and filter.category != ''">
AND category = #{filter.category}
</if>
</where>
</sql>
<!-- 插入任务元数据 -->
<insert id="insertTaskMeta" parameterType="org.xyzh.common.dto.crontab.TbCrontabTaskMeta">
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 (
#{taskMeta.ID}, #{taskMeta.metaId}, #{taskMeta.name}, #{taskMeta.description},
#{taskMeta.category}, #{taskMeta.beanName}, #{taskMeta.methodName},
#{taskMeta.scriptPath}, #{taskMeta.paramSchema}, #{taskMeta.autoPublish}, #{taskMeta.sortOrder},
#{taskMeta.creator}, NOW()
)
</insert>
<!-- 更新任务元数据 -->
<update id="updateTaskMeta" parameterType="org.xyzh.common.dto.crontab.TbCrontabTaskMeta">
UPDATE tb_crontab_task_meta
SET name = #{taskMeta.name},
description = #{taskMeta.description},
category = #{taskMeta.category},
bean_name = #{taskMeta.beanName},
method_name = #{taskMeta.methodName},
script_path = #{taskMeta.scriptPath},
param_schema = #{taskMeta.paramSchema},
auto_publish = #{taskMeta.autoPublish},
sort_order = #{taskMeta.sortOrder},
updater = #{taskMeta.updater},
update_time = NOW()
WHERE meta_id = #{taskMeta.metaId}
AND deleted = 0
</update>
<!-- 删除任务元数据(逻辑删除) -->
<update id="deleteTaskMeta">
UPDATE tb_crontab_task_meta
SET deleted = 1,
delete_time = NOW()
WHERE meta_id = #{metaId}
AND deleted = 0
</update>
<!-- 根据ID查询 -->
<select id="selectTaskMetaById" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List" />
FROM tb_crontab_task_meta
WHERE meta_id = #{metaId}
AND deleted = 0
</select>
<!-- 根据TaskID查询 -->
<select id="selectTaskMetaByTaskId" resultMap="BaseResultMap">
SELECT tcte.*
FROM tb_crontab_task tct
LEFT JOIN tb_crontab_task_meta tcte ON tct.meta_id = tcte.meta_id
WHERE tct.task_id = #{taskId}
AND tcte.deleted = 0
</select>
<!-- 查询所有任务元数据 -->
<select id="selectAllTaskMeta" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List" />
FROM tb_crontab_task_meta
WHERE deleted = 0
ORDER BY sort_order ASC, create_time DESC
</select>
<!-- 根据分类查询 -->
<select id="selectTaskMetaByCategory" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List" />
FROM tb_crontab_task_meta
WHERE category = #{category}
AND deleted = 0
ORDER BY sort_order ASC
</select>
<!-- 分页查询 -->
<select id="selectTaskMetaPage" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List" />
FROM tb_crontab_task_meta
<include refid="Base_Where_Clause" />
ORDER BY sort_order ASC, create_time DESC
LIMIT #{pageParam.offset}, #{pageParam.pageSize}
</select>
<!-- 查询总数 -->
<select id="countSelectTaskMeta" resultType="int">
SELECT COUNT(*)
FROM tb_crontab_task_meta
<include refid="Base_Where_Clause" />
</select>
</mapper>

View File

@@ -2,6 +2,8 @@ package org.xyzh.system.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.xyzh.common.core.page.PageParam;
import org.xyzh.common.dto.system.TbSysConfig;
import java.util.List;
@@ -16,12 +18,55 @@ import java.util.List;
@Mapper
public interface SysConfigMapper extends BaseMapper<TbSysConfig> {
/**
* @description 插入系统配置
*/
int insertSysConfig(@Param("config") TbSysConfig config);
/**
* @description 更新系统配置
*/
int updateSysConfig(@Param("config") TbSysConfig config);
/**
* @description 删除系统配置(逻辑删除)
*/
int deleteSysConfig(@Param("id") String id);
/**
* @description 根据配置键查询
*/
TbSysConfig selectSysConfigByKey(@Param("configKey") String configKey);
/**
* @description 根据ID查询
*/
TbSysConfig selectSysConfigById(@Param("id") String id);
/**
* @description 查询所有系统配置
*/
List<TbSysConfig> selectAllSysConfig();
/**
* @description 根据分组查询系统配置
*/
List<TbSysConfig> selectSysConfigByGroup(@Param("configGroup") String configGroup);
/**
* @description 查询系统配置列表
* @param filter 过滤条件
* @return List<TbSysConfig> 系统配置列表
* @author yslg
* @since 2025-10-15
*/
List<TbSysConfig> selectSysConfigs(TbSysConfig filter);
List<TbSysConfig> selectSysConfigs(@Param("filter") TbSysConfig filter);
/**
* @description 分页查询系统配置
*/
List<TbSysConfig> selectSysConfigPage(@Param("filter") TbSysConfig filter, @Param("pageParam") PageParam pageParam);
/**
* @description 查询系统配置总数
*/
int countSelectSysConfig(@Param("filter") TbSysConfig filter);
}

View File

@@ -1,13 +1,208 @@
package org.xyzh.system.service.config.impl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.xyzh.api.system.config.SysConfigService;
import org.xyzh.common.dto.system.TbSysConfig;
import org.xyzh.system.mapper.SysConfigMapper;
/**
* @description 系统配置服务实现
* @author AI Assistant
* @since 2025-11-18
*/
@Service
public class SysConfigServiceImpl implements SysConfigService{
public class SysConfigServiceImpl implements SysConfigService {
private static final Logger logger = LoggerFactory.getLogger(SysConfigServiceImpl.class);
@Autowired
private SysConfigMapper sysConfigMapper;
/**
* 根据key查询配置
*/
private TbSysConfig getConfigByKey(String key) {
if (key == null || key.isEmpty()) {
return null;
}
return sysConfigMapper.selectSysConfigByKey(key);
}
@Override
public String getSysConfig(String key) {
return null;
public Object getSysConfig(String key) {
try {
TbSysConfig config = getConfigByKey(key);
if (config == null) {
logger.warn("配置项不存在: {}", key);
return null;
}
String configType = config.getConfigType();
String configValue = config.getConfigValue();
if (configValue == null || configValue.isEmpty()) {
return null;
}
// 根据config_type返回对应的类型
if (configType == null || "string".equalsIgnoreCase(configType)) {
return configValue;
} else if ("number".equalsIgnoreCase(configType) || "integer".equalsIgnoreCase(configType)) {
try {
// 尝试解析为Integer如果失败则解析为Long
return Integer.parseInt(configValue);
} catch (NumberFormatException e) {
try {
return Long.parseLong(configValue);
} catch (NumberFormatException ex) {
logger.error("配置项 {} 的值无法转换为数字: {}", key, configValue);
return configValue;
}
}
} else if ("boolean".equalsIgnoreCase(configType)) {
String value = configValue.toLowerCase().trim();
if ("true".equals(value) || "1".equals(value) || "yes".equals(value) || "on".equals(value)) {
return true;
} else if ("false".equals(value) || "0".equals(value) || "no".equals(value) || "off".equals(value)) {
return false;
} else {
logger.warn("配置项 {} 的值无法识别为Boolean: {}", key, value);
return configValue;
}
} else if ("double".equalsIgnoreCase(configType) || "float".equalsIgnoreCase(configType)) {
try {
return Double.parseDouble(configValue);
} catch (NumberFormatException e) {
logger.error("配置项 {} 的值无法转换为Double: {}", key, configValue);
return configValue;
}
} else {
// 未知类型,直接返回字符串
return configValue;
}
} catch (Exception e) {
logger.error("获取配置失败: {}", key, e);
return null;
}
}
@Override
public String getStringConfig(String key) {
try {
TbSysConfig config = getConfigByKey(key);
if (config == null) {
logger.warn("配置项不存在: {}", key);
return null;
}
return config.getConfigValue();
} catch (Exception e) {
logger.error("获取字符串配置失败: {}", key, e);
return null;
}
}
@Override
public Integer getIntConfig(String key) {
try {
TbSysConfig config = getConfigByKey(key);
if (config == null) {
logger.warn("配置项不存在: {}", key);
return null;
}
String value = config.getConfigValue();
if (value == null || value.isEmpty()) {
return null;
}
return Integer.parseInt(value);
} catch (NumberFormatException e) {
logger.error("配置项 {} 的值无法转换为Integer: {}", key, e.getMessage());
return null;
} catch (Exception e) {
logger.error("获取Integer配置失败: {}", key, e);
return null;
}
}
@Override
public Boolean getBooleanConfig(String key) {
try {
TbSysConfig config = getConfigByKey(key);
if (config == null) {
logger.warn("配置项不存在: {}", key);
return null;
}
String value = config.getConfigValue();
if (value == null || value.isEmpty()) {
return null;
}
// 支持多种布尔值表示true/false, 1/0, yes/no, on/off
value = value.toLowerCase().trim();
if ("true".equals(value) || "1".equals(value) || "yes".equals(value) || "on".equals(value)) {
return true;
} else if ("false".equals(value) || "0".equals(value) || "no".equals(value) || "off".equals(value)) {
return false;
} else {
logger.warn("配置项 {} 的值无法识别为Boolean: {}", key, value);
return null;
}
} catch (Exception e) {
logger.error("获取Boolean配置失败: {}", key, e);
return null;
}
}
@Override
public Double getDoubleConfig(String key) {
try {
TbSysConfig config = getConfigByKey(key);
if (config == null) {
logger.warn("配置项不存在: {}", key);
return null;
}
String value = config.getConfigValue();
if (value == null || value.isEmpty()) {
return null;
}
return Double.parseDouble(value);
} catch (NumberFormatException e) {
logger.error("配置项 {} 的值无法转换为Double: {}", key, e.getMessage());
return null;
} catch (Exception e) {
logger.error("获取Double配置失败: {}", key, e);
return null;
}
}
@Override
public Long getLongConfig(String key) {
try {
TbSysConfig config = getConfigByKey(key);
if (config == null) {
logger.warn("配置项不存在: {}", key);
return null;
}
String value = config.getConfigValue();
if (value == null || value.isEmpty()) {
return null;
}
return Long.parseLong(value);
} catch (NumberFormatException e) {
logger.error("配置项 {} 的值无法转换为Long: {}", key, e.getMessage());
return null;
} catch (Exception e) {
logger.error("获取Long配置失败: {}", key, e);
return null;
}
}
}

View File

@@ -2,55 +2,138 @@
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.xyzh.system.mapper.SysConfigMapper">
<!-- 基础结果映射 -->
<!-- 结果映射 -->
<resultMap id="BaseResultMap" type="org.xyzh.common.dto.system.TbSysConfig">
<id column="id" property="id" jdbcType="VARCHAR"/>
<result column="config_key" property="configKey" jdbcType="VARCHAR"/>
<result column="config_value" property="configValue" jdbcType="LONGVARCHAR"/>
<result column="config_type" property="configType" jdbcType="VARCHAR"/>
<result column="config_group" property="configGroup" jdbcType="VARCHAR"/>
<result column="description" property="description" jdbcType="VARCHAR"/>
<result column="is_system" property="isSystem" jdbcType="BOOLEAN"/>
<result column="creator" property="creator" jdbcType="VARCHAR"/>
<result column="updater" property="updater" jdbcType="VARCHAR"/>
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
<result column="update_time" property="updateTime" jdbcType="TIMESTAMP"/>
<result column="delete_time" property="deleteTime" jdbcType="TIMESTAMP"/>
<result column="deleted" property="deleted" jdbcType="BOOLEAN"/>
<id column="id" property="ID" />
<result column="config_key" property="configKey" />
<result column="config_value" property="configValue" />
<result column="config_type" property="configType" />
<result column="config_group" property="configGroup" />
<result column="description" property="description" />
<result column="is_system" property="isSystem" />
<result column="creator" property="creator" />
<result column="updater" property="updater" />
<result column="create_time" property="createTime" />
<result column="update_time" property="updateTime" />
<result column="delete_time" property="deleteTime" />
<result column="deleted" property="deleted" />
</resultMap>
<!-- 基础字段 -->
<!-- 字段列表 -->
<sql id="Base_Column_List">
id, config_key, config_value, config_type, config_group, description,
is_system, creator, updater, create_time, update_time, delete_time, deleted
</sql>
<!-- 通用条件 -->
<sql id="Where_Clause">
<!-- 查询条件 -->
<sql id="Base_Where_Clause">
<where>
deleted = 0
<if test="configKey != null and configKey != ''">
AND config_key = #{configKey}
<if test="filter.configKey != null and filter.configKey != ''">
AND config_key = #{filter.configKey}
</if>
<if test="configGroup != null and configGroup != ''">
AND config_group = #{configGroup}
<if test="filter.configGroup != null and filter.configGroup != ''">
AND config_group = #{filter.configGroup}
</if>
<if test="configType != null and configType != ''">
AND config_type = #{configType}
<if test="filter.configType != null and filter.configType != ''">
AND config_type = #{filter.configType}
</if>
<if test="isSystem != null">
AND is_system = #{isSystem}
<if test="filter.isSystem != null">
AND is_system = #{filter.isSystem}
</if>
</where>
</sql>
<!-- selectSysConfigs -->
<select id="selectSysConfigs" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
<!-- 插入系统配置 -->
<insert id="insertSysConfig" parameterType="org.xyzh.common.dto.system.TbSysConfig">
INSERT INTO tb_sys_config (
id, config_key, config_value, config_type, config_group, description,
is_system, creator, create_time
) VALUES (
#{config.ID}, #{config.configKey}, #{config.configValue}, #{config.configType},
#{config.configGroup}, #{config.description}, #{config.isSystem},
#{config.creator}, NOW()
)
</insert>
<!-- 更新系统配置 -->
<update id="updateSysConfig" parameterType="org.xyzh.common.dto.system.TbSysConfig">
UPDATE tb_sys_config
SET config_value = #{config.configValue},
config_type = #{config.configType},
config_group = #{config.configGroup},
description = #{config.description},
is_system = #{config.isSystem},
updater = #{config.updater},
update_time = NOW()
WHERE id = #{config.ID}
AND deleted = 0
</update>
<!-- 删除系统配置(逻辑删除) -->
<update id="deleteSysConfig">
UPDATE tb_sys_config
SET deleted = 1,
delete_time = NOW()
WHERE id = #{id}
AND deleted = 0
</update>
<!-- 根据配置键查询 -->
<select id="selectSysConfigByKey" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List" />
FROM tb_sys_config
<include refid="Where_Clause"/>
WHERE config_key = #{configKey}
AND deleted = 0
</select>
<!-- 根据ID查询 -->
<select id="selectSysConfigById" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List" />
FROM tb_sys_config
WHERE id = #{id}
AND deleted = 0
</select>
<!-- 查询所有系统配置 -->
<select id="selectAllSysConfig" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List" />
FROM tb_sys_config
WHERE deleted = 0
ORDER BY config_group, config_key
</select>
<!-- 根据分组查询 -->
<select id="selectSysConfigByGroup" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List" />
FROM tb_sys_config
WHERE config_group = #{configGroup}
AND deleted = 0
ORDER BY config_key
</select>
<!-- 查询系统配置列表 -->
<select id="selectSysConfigs" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List" />
FROM tb_sys_config
<include refid="Base_Where_Clause" />
ORDER BY config_group, config_key
</select>
<!-- 分页查询 -->
<select id="selectSysConfigPage" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List" />
FROM tb_sys_config
<include refid="Base_Where_Clause" />
ORDER BY config_group, config_key
LIMIT #{pageParam.offset}, #{pageParam.pageSize}
</select>
<!-- 查询总数 -->
<select id="countSelectSysConfig" resultType="int">
SELECT COUNT(*)
FROM tb_sys_config
<include refid="Base_Where_Clause" />
</select>
</mapper>

View File

@@ -36,10 +36,10 @@ export const achievementApi = {
/**
* 删除成就
* @param achievement 成就信息包含achievementID
* @returns Promise<ResultDomain<void>>
* @returns Promise<ResultDomain<Boolean>>
*/
async deleteAchievement(achievement: Achievement): Promise<ResultDomain<void>> {
const response = await api.delete<void>('/achievements/achievement', achievement);
async deleteAchievement(achievement: Achievement): Promise<ResultDomain<Boolean>> {
const response = await api.delete<boolean>('/achievements/achievement', achievement);
return response.data;
},
@@ -138,10 +138,10 @@ export const achievementApi = {
* 撤销用户成就
* @param userID 用户ID
* @param achievementID 成就ID
* @returns Promise<ResultDomain<void>>
* @returns Promise<ResultDomain<Boolean>>
*/
async revokeAchievement(userID: string, achievementID: string): Promise<ResultDomain<void>> {
const response = await api.delete<void>('/achievements/revoke', null, {
async revokeAchievement(userID: string, achievementID: string): Promise<ResultDomain<Boolean>> {
const response = await api.delete<boolean>('/achievements/revoke', null, {
params: { userID, achievementID }
});
return response.data;

View File

@@ -169,15 +169,15 @@ export const documentSegmentApi = {
* @param documentId Dify文档ID
* @param segmentId 分段ID
* @param childChunkId 子块ID
* @returns Promise<ResultDomain<void>>
* @returns Promise<ResultDomain<Boolean>>
*/
async deleteChildChunk(
datasetId: string,
documentId: string,
segmentId: string,
childChunkId: string
): Promise<ResultDomain<void>> {
const response = await api.delete<void>(
): Promise<ResultDomain<Boolean>> {
const response = await api.delete<boolean>(
`/ai/dify/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}/child_chunks/${childChunkId}`
);
return response.data;

View File

@@ -147,15 +147,15 @@ export const fileUploadApi = {
* @param datasetId Dify数据集ID
* @param documentId Dify文档ID
* @param enabled 是否启用
* @returns Promise<ResultDomain<void>>
* @returns Promise<ResultDomain<Boolean>>
*/
async updateDocumentStatus(
datasetId: string,
documentId: string,
enabled: boolean
): Promise<ResultDomain<void>> {
): Promise<ResultDomain<Boolean>> {
const action = enabled ? 'enable' : 'disable';
const response = await api.post<void>(
const response = await api.post<boolean>(
`/ai/dify/datasets/${datasetId}/documents/status/${action}`,
{ document_ids: [documentId] }
);

View File

@@ -5,7 +5,18 @@
*/
import { api } from '@/apis/index';
import type { CrontabTask, CrontabLog, DataCollectionItem, CrontabItem, ResultDomain, PageParam } from '@/types';
import type {
CrontabTask,
CrontabLog,
DataCollectionItem,
CrontabItem,
TaskMeta,
EmailDefault,
EmailRecipient,
CreateTaskRequest,
ResultDomain,
PageParam
} from '@/types';
/**
* 定时任务API服务
@@ -16,11 +27,11 @@ export const crontabApi = {
// ==================== 定时任务管理 ====================
/**
* 获取可创建的定时任务模板列表
* @returns Promise<ResultDomain<CrontabItem>>
* 获取可创建的定时任务列表(从数据库获取任务元数据)
* @returns Promise<ResultDomain<TaskMeta>>
*/
async getEnabledCrontabList(): Promise<ResultDomain<CrontabItem>> {
const response = await api.get<CrontabItem>(`${this.baseUrl}/getEnabledCrontabList`);
async getEnabledCrontabList(): Promise<ResultDomain<TaskMeta>> {
const response = await api.get<TaskMeta>(`${this.baseUrl}/getEnabledCrontabList`);
return response.data;
},
@@ -29,18 +40,18 @@ export const crontabApi = {
* @param task 任务对象
* @returns Promise<ResultDomain<CrontabTask>>
*/
async createTask(task: CrontabTask): Promise<ResultDomain<CrontabTask>> {
async createTask(task: CreateTaskRequest): Promise<ResultDomain<CrontabTask>> {
const response = await api.post<CrontabTask>(`${this.baseUrl}/crontabTask`, task);
return response.data;
},
/**
* 更新定时任务
* @param task 任务对象
* @param request 更新任务请求包含任务信息、元数据ID等
* @returns Promise<ResultDomain<CrontabTask>>
*/
async updateTask(task: CrontabTask): Promise<ResultDomain<CrontabTask>> {
const response = await api.put<CrontabTask>(`${this.baseUrl}/crontabTask`, task);
async updateTask(request: CreateTaskRequest): Promise<ResultDomain<CrontabTask>> {
const response = await api.put<CrontabTask>(`${this.baseUrl}/crontabTask`, request);
return response.data;
},
@@ -255,5 +266,254 @@ export const crontabApi = {
const response = await api.put<string>(`${this.baseUrl}/collection/item/${itemId}/status/${status}`);
return response.data;
},
// ==================== 任务元数据管理 ====================
/**
* 创建任务元数据
* @param taskMeta 任务元数据
* @returns Promise<ResultDomain<TaskMeta>>
*/
async createTaskMeta(taskMeta: TaskMeta): Promise<ResultDomain<TaskMeta>> {
const response = await api.post<TaskMeta>(`${this.baseUrl}/meta`, taskMeta);
return response.data;
},
/**
* 更新任务元数据
* @param taskMeta 任务元数据
* @returns Promise<ResultDomain<TaskMeta>>
*/
async updateTaskMeta(taskMeta: TaskMeta): Promise<ResultDomain<TaskMeta>> {
const response = await api.put<TaskMeta>(`${this.baseUrl}/meta`, taskMeta);
return response.data;
},
/**
* 删除任务元数据
* @param metaId 元数据ID
* @returns Promise<ResultDomain<boolean>>
*/
async deleteTaskMeta(metaId: string): Promise<ResultDomain<boolean>> {
const response = await api.delete<boolean>(`${this.baseUrl}/meta/${metaId}`);
return response.data;
},
/**
* 根据ID查询任务元数据
* @param metaId 元数据ID
* @returns Promise<ResultDomain<TaskMeta>>
*/
async getTaskMetaById(metaId: string): Promise<ResultDomain<TaskMeta>> {
const response = await api.get<TaskMeta>(`${this.baseUrl}/meta/${metaId}`);
return response.data;
},
/**
* 查询所有任务元数据
* @returns Promise<ResultDomain<TaskMeta>>
*/
async getAllTaskMeta(): Promise<ResultDomain<TaskMeta>> {
const response = await api.get<TaskMeta>(`${this.baseUrl}/meta/all`);
return response.data;
},
/**
* 根据分类查询任务元数据
* @param category 分类
* @returns Promise<ResultDomain<TaskMeta>>
*/
async getTaskMetaByCategory(category: string): Promise<ResultDomain<TaskMeta>> {
const response = await api.get<TaskMeta>(`${this.baseUrl}/meta/category/${category}`);
return response.data;
},
/**
* 分页查询任务元数据
* @param filter 过滤条件
* @param pageParam 分页参数
* @returns Promise<ResultDomain<TaskMeta>>
*/
async getTaskMetaPage(filter?: Partial<TaskMeta>, pageParam?: PageParam): Promise<ResultDomain<TaskMeta>> {
const response = await api.post<TaskMeta>(`${this.baseUrl}/meta/page`, {
filter,
pageParam: {
pageNumber: pageParam?.pageNumber || 1,
pageSize: pageParam?.pageSize || 10
}
});
return response.data;
},
// ==================== 邮件默认接收人管理 ====================
/**
* 创建默认接收人
* @param emailDefault 默认接收人
* @returns Promise<ResultDomain<EmailDefault>>
*/
async createEmailDefault(emailDefault: EmailDefault): Promise<ResultDomain<EmailDefault>> {
const response = await api.post<EmailDefault>(`${this.baseUrl}/email/default`, emailDefault);
return response.data;
},
/**
* 更新默认接收人
* @param emailDefault 默认接收人
* @returns Promise<ResultDomain<EmailDefault>>
*/
async updateEmailDefault(emailDefault: EmailDefault): Promise<ResultDomain<EmailDefault>> {
const response = await api.put<EmailDefault>(`${this.baseUrl}/email/default`, emailDefault);
return response.data;
},
/**
* 删除默认接收人
* @param defaultId 默认ID
* @returns Promise<ResultDomain<boolean>>
*/
async deleteEmailDefault(defaultId: string): Promise<ResultDomain<boolean>> {
const response = await api.delete<boolean>(`${this.baseUrl}/email/default/${defaultId}`);
return response.data;
},
/**
* 根据defaultId查询
* @param defaultId 默认ID
* @returns Promise<ResultDomain<EmailDefault>>
*/
async getEmailDefaultById(defaultId: string): Promise<ResultDomain<EmailDefault>> {
const response = await api.get<EmailDefault>(`${this.baseUrl}/email/default/${defaultId}`);
return response.data;
},
/**
* 根据metaId查询默认接收人
* @param metaId 元数据ID
* @returns Promise<ResultDomain<EmailDefault>>
*/
async getEmailDefaultByMetaId(metaId: string): Promise<ResultDomain<EmailDefault>> {
const response = await api.get<EmailDefault>(`${this.baseUrl}/email/default/meta/${metaId}`);
return response.data;
},
// ==================== 邮件接收人管理 ====================
/**
* 创建邮件接收人
* @param recipient 邮件接收人
* @returns Promise<ResultDomain<EmailRecipient>>
*/
async createEmailRecipient(recipient: EmailRecipient): Promise<ResultDomain<EmailRecipient>> {
const response = await api.post<EmailRecipient>(`${this.baseUrl}/email/recipient`, recipient);
return response.data;
},
/**
* 批量创建邮件接收人
* @param recipients 邮件接收人列表
* @returns Promise<ResultDomain<boolean>>
*/
async batchCreateEmailRecipient(recipients: EmailRecipient[]): Promise<ResultDomain<boolean>> {
const response = await api.post<boolean>(`${this.baseUrl}/email/recipient/batch`, recipients);
return response.data;
},
/**
* 更新邮件接收人
* @param recipient 邮件接收人
* @returns Promise<ResultDomain<EmailRecipient>>
*/
async updateEmailRecipient(recipient: EmailRecipient): Promise<ResultDomain<EmailRecipient>> {
const response = await api.put<EmailRecipient>(`${this.baseUrl}/email/recipient`, recipient);
return response.data;
},
/**
* 删除邮件接收人
* @param recipientId 接收人ID
* @returns Promise<ResultDomain<boolean>>
*/
async deleteEmailRecipient(recipientId: string): Promise<ResultDomain<boolean>> {
const response = await api.delete<boolean>(`${this.baseUrl}/email/recipient/${recipientId}`);
return response.data;
},
/**
* 根据ID查询接收人
* @param recipientId 接收人ID
* @returns Promise<ResultDomain<EmailRecipient>>
*/
async getEmailRecipientById(recipientId: string): Promise<ResultDomain<EmailRecipient>> {
const response = await api.get<EmailRecipient>(`${this.baseUrl}/email/recipient/${recipientId}`);
return response.data;
},
/**
* 根据default_id查询接收人列表
* @param defaultId 默认ID
* @returns Promise<ResultDomain<EmailRecipient>>
*/
async getRecipientsByDefaultId(defaultId: string): Promise<ResultDomain<EmailRecipient>> {
const response = await api.get<EmailRecipient>(`${this.baseUrl}/email/recipient/default/${defaultId}`);
return response.data;
},
/**
* 根据任务ID查询接收人列表
* @param taskId 任务ID
* @returns Promise<ResultDomain<EmailRecipient>>
*/
async getRecipientsByTaskId(taskId: string): Promise<ResultDomain<EmailRecipient>> {
const response = await api.get<EmailRecipient>(`${this.baseUrl}/email/recipient/task/${taskId}`);
return response.data;
},
/**
* 查询所有启用的接收人
* @returns Promise<ResultDomain<EmailRecipient>>
*/
async getAllEnabledRecipients(): Promise<ResultDomain<EmailRecipient>> {
const response = await api.get<EmailRecipient>(`${this.baseUrl}/email/recipient/enabled`);
return response.data;
},
/**
* 分页查询邮件接收人
* @param filter 过滤条件
* @param pageParam 分页参数
* @returns Promise<ResultDomain<EmailRecipient>>
*/
async getEmailRecipientPage(filter?: Partial<EmailRecipient>, pageParam?: PageParam): Promise<ResultDomain<EmailRecipient>> {
const response = await api.post<EmailRecipient>(`${this.baseUrl}/email/recipient/page`, {
filter,
pageParam: {
pageNumber: pageParam?.pageNumber || 1,
pageSize: pageParam?.pageSize || 10
}
});
return response.data;
},
/**
* 删除default_id的所有接收人
* @param defaultId 默认ID
* @returns Promise<ResultDomain<boolean>>
*/
async deleteRecipientsByDefaultId(defaultId: string): Promise<ResultDomain<boolean>> {
const response = await api.delete<boolean>(`${this.baseUrl}/email/recipient/default/${defaultId}`);
return response.data;
},
/**
* 删除任务的所有接收人
* @param taskId 任务ID
* @returns Promise<ResultDomain<boolean>>
*/
async deleteRecipientsByTaskId(taskId: string): Promise<ResultDomain<boolean>> {
const response = await api.delete<boolean>(`${this.baseUrl}/email/recipient/task/${taskId}`);
return response.data;
},
};

View File

@@ -16,6 +16,10 @@ export interface CrontabTask extends BaseDTO {
taskName?: string;
/** 任务分组 */
taskGroup?: string;
/** 元数据ID关联任务元数据表 */
metaId?: string;
/** 是否使用默认接收人 */
defaultRecipient?: boolean;
/** Bean名称 */
beanName?: string;
/** 方法名称 */
@@ -172,6 +176,8 @@ export interface CrontabMethod {
excuete_method?: string;
/** Python脚本路径 */
path: string;
/** 元数据ID从数据库加载时使用 */
metaId?: string;
/** 参数定义列表 */
params?: CrontabParam[];
}
@@ -186,3 +192,95 @@ export interface CrontabItem {
methods: CrontabMethod[];
}
/**
* 定时任务元数据
*/
export interface TaskMeta extends BaseDTO {
/** 元数据ID */
metaId?: string;
/** 任务名称 */
name?: string;
/** 任务描述 */
description?: string;
/** 任务分类 */
category?: string;
/** Bean名称 */
beanName?: string;
/** 方法名称 */
methodName?: string;
/** 脚本路径 */
scriptPath?: string;
/** 参数模式(JSON Schema) */
paramSchema?: string;
/** 是否自动发布 */
autoPublish?: boolean;
/** 排序 */
sortOrder?: number;
/** 创建者 */
creator?: string;
/** 更新者 */
updater?: string;
}
/**
* 邮件默认接收人
*/
export interface EmailDefault extends BaseDTO {
/** 默认ID */
defaultId?: string;
/** 元数据ID */
metaId?: string;
/** 用户ID */
userId?: string;
userEmail?: string;
username?:string;
/** 创建者 */
creator?: string;
/** 更新者 */
updater?: string;
}
/**
* 邮件接收人
*/
export interface EmailRecipient extends BaseDTO {
/** 接收人ID */
recipientId?: string;
/** 任务ID */
taskId?: string;
/** 用户ID */
userId?: string;
/** 邮箱 */
email?: string;
/** 姓名 */
name?: string;
/** 创建者 */
creator?: string;
/** 更新者 */
updater?: string;
}
/**
* 接收人用户信息
*/
export interface RecipientUserInfo {
/** 用户ID */
userId: string;
/** 用户邮箱 */
userEmail: string;
/** 用户名称 */
username: string;
}
/**
* 创建任务请求
*/
export interface CreateTaskRequest {
/** 任务信息 */
task: CrontabTask;
/** 任务元数据ID */
metaId: string;
/** 额外添加的接收人列表 */
additionalRecipients?: RecipientUserInfo[];
}

View File

@@ -320,7 +320,7 @@ export interface TaskItemVO extends LearningTask {
username?: string;
deptID?: string;
deptName?: string;
parentDeptID?: string;
parentID?: string;
/** 是否必修 */
required?: boolean;
/** 排序号 */

View File

@@ -73,7 +73,7 @@ export interface UserVO extends BaseDTO {
/** 学习等级 */
level?: number;
deptID?: string;
parentDeptID?: string;
parentID?: string;
/** 部门名称 */
deptName?: string;
/** 角色名称 */

View File

@@ -206,16 +206,16 @@
<div class="form-item" v-if="selectedTemplate">
<span class="form-label required">爬取方法</span>
<el-select
v-model="selectedMethod"
v-model="selectedMethodId"
placeholder="请选择爬取方法"
style="width: 100%"
>
<el-option
v-for="method in selectedTemplate.methods"
:key="method.name"
:key="method.metaId"
:label="method.name"
:value="method"
:value="method.metaId"
/>
</el-select>
<span class="form-tip">
@@ -282,15 +282,42 @@
placeholder="请输入爬虫描述"
/>
</div>
<!-- 邮件接收人配置 -->
<div class="form-item">
<span class="form-label">是否允许并发</span>
<el-radio-group v-model="formData.concurrent">
<el-radio :label="1">允许</el-radio>
<el-radio :label="0">禁止</el-radio>
</el-radio-group>
<span class="form-tip">
建议禁止并发避免重复抓取
<span class="form-label">邮件通知</span>
<el-checkbox v-model="useDefaultRecipients">
使用默认接收人
</el-checkbox>
<span class="form-tip" v-if="useDefaultRecipients && defaultRecipients.length > 0">
默认接收人{{ defaultRecipients.map(r => r.username).join('、') }}
</span>
<span class="form-tip" v-else-if="useDefaultRecipients && defaultRecipients.length === 0">
该任务模板暂无默认接收人
</span>
</div>
<div class="form-item">
<span class="form-label">额外接收人</span>
<div class="recipient-list">
<el-tag
v-for="recipient in additionalRecipients"
:key="recipient.userId"
closable
@close="removeRecipient(recipient)"
style="margin-right: 8px; margin-bottom: 8px;"
>
{{ recipient.username }}
</el-tag>
</div>
<el-button
@click="showRecipientSelector"
size="small"
style="margin-top: 8px;"
>
选择接收人
</el-button>
<!-- TODO: 在这里添加自定义的用户选择组件 -->
</div>
</div>
@@ -305,17 +332,38 @@
</el-button>
</template>
</el-dialog>
<GenericSelector
v-model:visible="showUserSelector"
title="选择邮件接收人"
left-title="可选人员"
right-title="已选人员"
:fetch-available-api="fetchAllUsers"
:initialTargetItems="selectedRecipients"
:filter-selected="filterUsers"
:item-config="{ id: 'userId', label: 'username', sublabel: 'userEmail' }"
:use-tree="true"
:tree-transform="transformUserToTree"
:tree-props="{ children: 'children', label: 'username', id: 'userId' }"
:only-leaf-selectable="true"
unit-name=""
search-placeholder="搜索用户姓名或邮箱..."
@confirm="handleUserConfirm"
@cancel="resetUserSelector"
/>
</div>
</AdminLayout>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, watch } from 'vue';
import { ref, reactive, onMounted, watch, computed } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { Plus, Search, Refresh, DocumentCopy, VideoPlay, VideoPause, Promotion, Edit, Delete } from '@element-plus/icons-vue';
import { crontabApi } from '@/apis/crontab';
import type { CrontabTask, CrontabItem, CrontabMethod, PageParam } from '@/types';
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';
defineOptions({
name: 'NewsCrawlerView'
});
@@ -325,12 +373,20 @@ const submitting = ref(false);
const crawlerList = ref<CrontabTask[]>([]);
const total = ref(0);
// 爬虫模板数据
const crawlerTemplates = ref<CrontabItem[]>([]);
// 爬虫数据
const taskMetaList = ref<TaskMeta[]>([]);
const crawlerTemplates = ref<CrontabItem[]>([]); // 转换后的模板结构
const selectedTemplate = ref<CrontabItem | null>(null);
const selectedMethod = ref<CrontabMethod | null>(null);
const selectedMethodId = ref<string>(''); // 选中的方法ID(metaId)
const selectedMetaId = ref<string>(''); // 选中的元数据ID
const dynamicParams = ref<Record<string, any>>({});
// 邮件接收人相关
const useDefaultRecipients = ref<boolean>(false);
const defaultRecipients = ref<RecipientUserInfo[]>([]);
const additionalRecipients = ref<RecipientUserInfo[]>([]);
const showUserSelector = ref<boolean>(false);
// 搜索表单
const searchForm = reactive({
taskName: '',
@@ -361,40 +417,251 @@ const formData = reactive<Partial<CrontabTask>>({
description: ''
});
// 根据selectedMethodId获取完整的method对象
const selectedMethod = computed(() => {
if (!selectedTemplate.value || !selectedMethodId.value) {
return null;
}
return selectedTemplate.value.methods.find(m => m.metaId === selectedMethodId.value) || null;
});
// 计算已选接收人(包括默认接收人+额外添加的接收人)
const selectedRecipients = computed(() => {
if (useDefaultRecipients.value) {
// 合并默认接收人和额外接收人,去重
const all = [...defaultRecipients.value, ...additionalRecipients.value];
const uniqueMap = new Map<string, RecipientUserInfo>();
all.forEach(r => uniqueMap.set(r.userId, r));
return Array.from(uniqueMap.values());
} else {
return additionalRecipients.value;
}
});
// 监听模板选择变化
watch(selectedTemplate, (newTemplate, oldTemplate) => {
// 只在用户手动切换模板时重置oldTemplate存在且不为null时才重置
// 编辑回填时oldTemplate为null不会触发重置
if (newTemplate && oldTemplate) {
selectedMethod.value = null;
selectedMethodId.value = '';
dynamicParams.value = {};
}
});
// 监听方法选择变化
watch(selectedMethod, (newMethod) => {
if (newMethod) {
watch(selectedMethodId, (newMethodId) => {
if (newMethodId && selectedMethod.value) {
// 保存metaId
selectedMetaId.value = newMethodId;
dynamicParams.value = {};
// 遍历params数组提取默认值
if (newMethod.params && Array.isArray(newMethod.params)) {
newMethod.params.forEach(param => {
if (selectedMethod.value.params && Array.isArray(selectedMethod.value.params)) {
selectedMethod.value.params.forEach((param: CrontabParam) => {
dynamicParams.value[param.name] = param.value;
});
}
// 加载该任务模板的默认接收人
if (selectedMetaId.value) {
loadDefaultRecipients(selectedMetaId.value);
}
}
});
// 加载爬虫模板
// ==================== 人员选择器相关 ====================
/**
* 1. 获取所有人员列表的接口方法
*/
async function fetchAllUsers(): Promise<ResultDomain<any>> {
try {
const result = await userApi.getUserList({});
if (result.success && result.dataList) {
// 转换为 GenericSelector 需要的格式
const users = result.dataList.map((user: UserVO) => ({
userId: user.id || '',
username: user.username || user.email || 'Unknown',
userEmail: user.email || '',
deptID: user.deptID,
deptName: user.deptName,
parentID: user.parentID
}));
return {
...result,
dataList: users
};
}
return result;
} catch (error) {
ElMessage.error('获取用户列表失败');
return {
code: 500,
success: false,
login: true,
auth: true,
message: '获取用户列表失败',
dataList: []
} as ResultDomain<any>;
}
}
/**
* 2. 过滤方法:从可选项中移除已选项
*/
function filterUsers(available: any[], selected: any[]): any[] {
const selectedIds = new Set(selected.map(item => item.userId));
return available.filter(item => !selectedIds.has(item.userId));
}
/**
* 3. 构建多级部门树的方法
*/
function transformUserToTree(flatData: any[]): any[] {
if (!flatData || flatData.length === 0) {
return [];
}
// 第一步:按部门分组,收集每个部门下的用户
const deptMap = new Map<string, any>();
const tree: any[] = [];
flatData.forEach(item => {
if (!item.deptID) return;
if (!deptMap.has(item.deptID)) {
// 创建部门节点
deptMap.set(item.deptID, {
userId: `dept_${item.deptID}`,
username: item.deptName || '未分配部门',
userEmail: '',
deptID: item.deptID,
deptName: item.deptName,
parentID: item.parentID,
children: [],
isDept: true // 标记这是部门节点
});
}
// 添加用户到部门的children中
const deptNode = deptMap.get(item.deptID);
if (deptNode) {
deptNode.children.push({
...item,
isDept: false // 标记这是用户节点
});
}
});
// 第二步:构建部门层级关系
const allDepts = Array.from(deptMap.values());
const deptTreeMap = new Map<string, any>();
// 初始化所有部门节点(创建副本)
allDepts.forEach(dept => {
deptTreeMap.set(dept.deptID, { ...dept });
});
// 第三步:建立部门的父子关系
allDepts.forEach(dept => {
const node = deptTreeMap.get(dept.deptID);
if (!node) return;
if (!dept.parentID || dept.parentID === '0' || dept.parentID === '') {
// 根部门
tree.push(node);
} else {
// 子部门
const parent = deptTreeMap.get(dept.parentID);
if (parent) {
if (!parent.children) {
parent.children = [];
}
// 保存当前节点的用户列表
const users = node.children || [];
node.children = [];
// 将部门节点添加到父部门
parent.children.push(node);
// 恢复用户列表
node.children = users;
} else {
// 找不到父节点,作为根节点
tree.push(node);
}
}
});
return tree;
}
/**
* 4. 显示用户选择器
*/
function showRecipientSelector() {
showUserSelector.value = true;
}
/**
* 5. 确认选择用户
*/
function handleUserConfirm(selected: any[]) {
// 过滤掉部门节点,只保留用户节点
const userItems = selected.filter(item => item.isDept !== true && !defaultRecipients.value.find(r => r.userId === item.userId));
additionalRecipients.value = userItems.map(item => ({
userId: item.userId,
username: item.username,
userEmail: item.userEmail || ''
}));
}
/**
* 6. 取消/重置选择器
*/
function resetUserSelector() {
console.log('❌ 取消选择');
// 不做任何操作,保持原有选择
}
// 加载爬虫模板从数据库加载TaskMeta转换为CrontabItem结构
async function loadCrawlerTemplates() {
try {
const result = await crontabApi.getEnabledCrontabList();
if (result.success && result.dataList) {
crawlerTemplates.value = result.dataList;
taskMetaList.value = result.dataList;
// 将TaskMeta[]按category分组转换为CrontabItem[]
const grouped = new Map<string, TaskMeta[]>();
result.dataList.forEach((meta: TaskMeta) => {
const category = meta.category || '未分类';
if (!grouped.has(category)) {
grouped.set(category, []);
}
grouped.get(category)!.push(meta);
});
// 转换为CrontabItem结构
crawlerTemplates.value = Array.from(grouped.entries()).map(([category, metas]) => ({
name: category,
methods: metas.map(meta => ({
name: meta.name || '',
clazz: meta.beanName || '',
excuete_method: meta.methodName || '',
path: meta.scriptPath || '',
metaId: meta.metaId || '', // 保存metaId
params: meta.paramSchema ? JSON.parse(meta.paramSchema) : []
}))
}));
} else {
ElMessage.error(result.message || '加载爬虫模板失败');
}
} catch (error) {
console.error('加载爬虫模板失败:', error);
ElMessage.error('加载爬虫模板失败');
}
}
@@ -428,7 +695,6 @@ async function loadCrawlerList() {
total.value = 0;
}
} catch (error) {
console.error('加载爬虫列表失败:', error);
ElMessage.error('加载爬虫列表失败');
crawlerList.value = [];
total.value = 0;
@@ -468,50 +734,68 @@ function handleAdd() {
isEdit.value = false;
resetFormData();
selectedTemplate.value = null;
selectedMethod.value = null;
selectedMethodId.value = '';
dynamicParams.value = {};
dialogVisible.value = true;
}
// 编辑爬虫
function handleEdit(row: CrontabTask) {
async function handleEdit(row: CrontabTask) {
isEdit.value = true;
Object.assign(formData, row);
// 重置选择
selectedTemplate.value = null;
selectedMethod.value = null;
selectedMethodId.value = '';
dynamicParams.value = {};
// 尝试解析methodParams来回填表单
if (row.methodParams) {
// 回填邮件接收人配置
useDefaultRecipients.value = row.defaultRecipient || false;
additionalRecipients.value = [];
// 加载该任务的额外接收人
if (row.taskId) {
try {
const params = JSON.parse(row.methodParams);
// 如果有scriptPath,尝试匹配模板和方法
if (params.scriptPath) {
const template = crawlerTemplates.value.find(t =>
t.methods.some(m => m.path === params.scriptPath)
);
if (template) {
const method = template.methods.find(m => m.path === params.scriptPath);
if (method) {
// 先设置template和method触发watch填充默认值
selectedTemplate.value = template;
selectedMethod.value = method;
// 然后使用nextTick确保watch执行完后再覆盖为实际值
// 回填动态参数排除scriptPath
const { scriptPath, ...restParams } = params;
const recipientsResult = await crontabApi.getRecipientsByTaskId(row.taskId);
if (recipientsResult.success && recipientsResult.dataList) {
additionalRecipients.value = recipientsResult.dataList.map(item => ({
userId: item.userId || '',
username: item.name || '',
userEmail: item.email || ''
}));
}
} catch (error) {
console.error('加载额外接收人失败:', error);
}
}
// 通过metaId直接匹配
if (row.metaId) {
// 遍历所有模板和方法找到匹配的metaId
for (const template of crawlerTemplates.value) {
const method = template.methods.find(m => m.metaId === row.metaId);
if (method) {
// 找到匹配的方法设置template和method
selectedTemplate.value = template;
selectedMethodId.value = method.metaId || '';
selectedMetaId.value = method.metaId || '';
// 回填动态参数
if (row.methodParams) {
try {
const params = JSON.parse(row.methodParams);
// 排除系统参数
const { scriptPath, taskId, logId, ...restParams } = params;
// 延迟设置确保watch先执行完
setTimeout(() => {
dynamicParams.value = restParams;
console.log('📝 编辑回填 - template:', template.name, 'method:', method.name, 'params:', restParams);
}, 0);
} catch (error) {
console.error('解析methodParams失败:', error);
}
}
break;
}
} catch (error) {
console.warn('解析methodParams失败:', error);
}
}
@@ -529,7 +813,6 @@ async function handleStart(row: CrontabTask) {
ElMessage.error(result.message || '启动失败');
}
} catch (error) {
console.error('启动爬虫失败:', error);
ElMessage.error('启动爬虫失败');
}
}
@@ -545,7 +828,6 @@ async function handlePause(row: CrontabTask) {
ElMessage.error(result.message || '暂停失败');
}
} catch (error) {
console.error('暂停爬虫失败:', error);
ElMessage.error('暂停爬虫失败');
}
}
@@ -563,15 +845,21 @@ async function handleExecute(row: CrontabTask) {
}
);
const result = await crontabApi.executeTaskOnce(row.taskId!);
if (result.success) {
ElMessage.success('爬虫执行成功,请稍后查看执行日志');
} else {
ElMessage.error(result.message || '执行失败');
}
// 异步执行,不等待任务完成
crontabApi.executeTaskOnce(row.taskId!).then(result => {
if (result.success) {
ElMessage.success('任务已提交执行,请稍后查看执行日志');
} else {
ElMessage.error(result.message || '提交执行失败');
}
}).catch(() => {
ElMessage.error('提交执行失败');
});
// 立即提示用户任务已触发
ElMessage.info('任务执行已触发,正在后台运行...');
} catch (error: any) {
if (error !== 'cancel') {
console.error('执行爬虫失败:', error);
ElMessage.error('执行爬虫失败');
}
}
@@ -599,7 +887,6 @@ async function handleDelete(row: CrontabTask) {
}
} catch (error: any) {
if (error !== 'cancel') {
console.error('删除爬虫失败:', error);
ElMessage.error('删除爬虫失败');
}
}
@@ -620,7 +907,6 @@ async function validateCron() {
ElMessage.error(result.message || 'Cron表达式格式错误');
}
} catch (error) {
console.error('验证Cron表达式失败:', error);
ElMessage.error('验证失败');
}
}
@@ -640,6 +926,12 @@ async function handleSubmit() {
ElMessage.warning('请输入Cron表达式');
return;
}
// 校验additionRecipients的email存在
const recipientWithoutEmail = additionalRecipients.value.find(recipient => !recipient.userEmail);
if (recipientWithoutEmail) {
ElMessage.warning(`${recipientWithoutEmail.username} 邮箱不能为空`);
return;
}
// 验证必填参数
if (selectedMethod.value.params && Array.isArray(selectedMethod.value.params)) {
@@ -659,26 +951,24 @@ async function handleSubmit() {
submitting.value = true;
try {
// 传递taskGroup和methodName中文名后端根据这两个name查找配置并填充beanName、methodName和scriptPath
const data = {
...formData,
taskGroup: selectedTemplate.value.name, // 模板名称(中文)
methodName: selectedMethod.value.name, // 方法名称(中文)
methodParams: JSON.stringify({
...dynamicParams.value // 只传用户输入的参数scriptPath由后端填充
})
// 构建CreateTaskRequest
const requestData: CreateTaskRequest = {
metaId: selectedMetaId.value,
task: {
...formData,
defaultRecipient: useDefaultRecipients.value,
methodParams: JSON.stringify({
...dynamicParams.value
})
} as CrontabTask,
additionalRecipients: additionalRecipients.value
};
console.log('📤 准备提交的数据:', data);
console.log('📤 taskGroup:', selectedTemplate.value.name);
console.log('📤 methodName:', selectedMethod.value.name);
console.log('📤 动态参数:', dynamicParams.value);
let result;
if (isEdit.value) {
result = await crontabApi.updateTask(data as CrontabTask);
result = await crontabApi.updateTask(requestData);
} else {
result = await crontabApi.createTask(data as CrontabTask);
result = await crontabApi.createTask(requestData);
}
if (result.success) {
@@ -689,7 +979,6 @@ async function handleSubmit() {
ElMessage.error(result.message || (isEdit.value ? '更新失败' : '创建失败'));
}
} catch (error) {
console.error('提交失败:', error);
ElMessage.error('提交失败');
} finally {
submitting.value = false;
@@ -713,8 +1002,41 @@ function resetFormData() {
status: 0,
concurrent: 0,
misfirePolicy: 3,
description: ''
description: '',
defaultRecipient: false
});
useDefaultRecipients.value = false;
defaultRecipients.value = [];
additionalRecipients.value = [];
}
// 邮件接收人相关方法
async function loadDefaultRecipients(metaId: string) {
try {
const result = await crontabApi.getEmailDefaultByMetaId(metaId);
if (result.success && result.dataList && result.dataList.length > 0) {
defaultRecipients.value = result.dataList
.map(item => ({
userId: item.userId!,
username: item.username!,
userEmail: item.userEmail!
}));
} else {
defaultRecipients.value = [];
}
} catch (error) {
defaultRecipients.value = [];
}
}
// TODO: 用户可以在这里添加自定义的邮件接收人选择逻辑
function removeRecipient(recipient: RecipientUserInfo) {
const index = additionalRecipients.value.findIndex(r => r.userId === recipient.userId);
if (index > -1) {
additionalRecipients.value.splice(index, 1);
}
}
// 初始化

View File

@@ -551,7 +551,7 @@ async function fetchTaskUsers() {
username: item.username,
deptID: item.deptID,
deptName: item.deptName,
parentDeptID: item.parentDeptID
parentID: item.parentID
}));
return {
success: true,
@@ -604,7 +604,7 @@ function transformUsersToTree(flatData: any[]): any[] {
displayName: deptName,
deptID: deptID,
deptName: deptName,
parentDeptID: item.parentDeptID,
parentID: item.parentID,
children: [],
isDept: true // 标记这是部门节点
});
@@ -635,12 +635,12 @@ function transformUsersToTree(flatData: any[]): any[] {
const node = deptTreeMap.get(dept.deptID);
if (!node) return;
if (!dept.parentDeptID || dept.parentDeptID === '0' || dept.parentDeptID === '') {
if (!dept.parentID || dept.parentID === '0' || dept.parentID === '') {
// 根部门
tree.push(node);
} else {
// 子部门
const parent = deptTreeMap.get(dept.parentDeptID);
const parent = deptTreeMap.get(dept.parentID);
if (parent) {
// 将用户节点暂存
const users = node.children || [];

View File

@@ -588,8 +588,8 @@ function transformUsersToTree(flatData: any[]): any[] {
flatData.forEach(item => {
const deptID = item.deptID || 'unknown';
const deptName = item.deptName || '未分配部门';
// 优先使用 parentID如果不存在则使用 parentDeptID
const parentID = item.parentID || item.parentDeptID;
// 优先使用 parentID如果不存在则使用 parentID
const parentID = item.parentID || item.parentID;
if (!deptMap.has(deptID)) {
// 创建部门节点
@@ -598,7 +598,7 @@ function transformUsersToTree(flatData: any[]): any[] {
displayName: deptName,
deptID: deptID,
deptName: deptName,
parentDeptID: parentID,
parentID: parentID,
children: [],
isDept: true
});
@@ -629,12 +629,12 @@ function transformUsersToTree(flatData: any[]): any[] {
const node = deptTreeMap.get(dept.deptID);
if (!node) return;
if (!dept.parentDeptID || dept.parentDeptID === '0' || dept.parentDeptID === '') {
if (!dept.parentID || dept.parentID === '0' || dept.parentID === '') {
// 根部门
tree.push(node);
} else {
// 子部门
const parent = deptTreeMap.get(dept.parentDeptID);
const parent = deptTreeMap.get(dept.parentID);
if (parent) {
// 将用户节点暂存
const users = node.children || [];

View File

@@ -607,7 +607,7 @@ async function fetchTaskUsers() {
username: item.username,
deptID: item.deptID,
deptName: item.deptName,
parentDeptID: item.parentDeptID
parentID: item.parentID
}));
return {
success: true,
@@ -656,7 +656,7 @@ function transformUsersToTree(flatData: any[]): any[] {
displayName: deptName,
deptID: deptID,
deptName: deptName,
parentDeptID: item.parentDeptID,
parentID: item.parentID,
children: [],
isDept: true // 标记这是部门节点
});
@@ -687,12 +687,12 @@ function transformUsersToTree(flatData: any[]): any[] {
const node = deptTreeMap.get(dept.deptID);
if (!node) return;
if (!dept.parentDeptID || dept.parentDeptID === '0' || dept.parentDeptID === '') {
if (!dept.parentID || dept.parentID === '0' || dept.parentID === '') {
// 根部门
tree.push(node);
} else {
// 子部门
const parent = deptTreeMap.get(dept.parentDeptID);
const parent = deptTreeMap.get(dept.parentID);
if (parent) {
// 将用户节点暂存
const users = node.children || [];

View File

@@ -0,0 +1,59 @@
crawler:
# Python 可执行文件路径Windows 建议指向 python.exe如已在 PATH可直接用 "python"
pythonPath: F:/Environment/Conda/envs/schoolNewsCrawler/python.exe
# 爬虫脚本根目录NewsCrawlerTask 的工作目录)
basePath: F:/Project/schoolNews/schoolNewsCrawler
# 下面为原有的定时任务清单(保持不变,仅修正到正确文件)
crontab:
items:
- name: 人民日报新闻爬取
methods:
- name: 关键字搜索爬取
clazz: newsCrewerTask
excuete_method: execute
path: crawler/RmrbSearch.py
params:
- name: query
description: 搜索关键字
type: String
value: ""
required: true
- name: total
description: 总新闻数量
type: Integer
value: 10
required: true
- name: 排行榜爬取
clazz: newsCrewerTask
excuete_method: execute
path: crawler/RmrbHotPoint.py
- name: 往日精彩头条爬取
clazz: newsCrewerTask
excuete_method: execute
path: crawler/RmrbTrending.py
params:
- name: startDate
description: 开始日期
type: String
value: ""
required: false
- name: endDate
description: 结束日期
type: String
value: ""
required: false
- name: yesterday
description: 是否是昨天
type: Boolean
value: true
1. 改成系统表配置。
2. 通过system模块的config配置crawler的基本信息
3. 创建定时任务meta表存放定时任务的元数据
4. 创建定时任务meta邮件人员模板表存放定时任务的邮件人员模板
5. 创建定时任务邮件接收人员表,存放定时任务的邮件接收人员
6. 修改dto、service、mapper、controller等实现定时任务meta的查询。定时任务meta邮件人员模板的增删改查。定时任务邮件接收人员的增删改查。
7. 修改定时任务的param等内容实现定时任务的执行。
8. 修改前端接口请求逻辑,实现定时任务和邮件人员的增删改查。