mirror of
https://git.ittx.com.cn/office-walle/will_server.git
synced 2026-10-19 16:16:57 +00:00
feat(job): 重构报表任务执行架构并新增邮件抄送功能
- 新增 AbstractZentaoReportBuilder 抽象类提供通用查询方法 - 新增 ReportBuilder 接口和 @ReportJob 注解实现报表构建器插件化 - 新增 ZentaoDailyEffortReportBuilder 实现日报工时统计报表 - 将 ZentaoWeComReportService 重命名为 BuildReportService 并重构为通用报表服务 - 在 job_config 表新增 mailCc 字段支持邮件抄送功能 - 更新 MailMessageClient 支持抄送人参数和收件人格式标准化 - 新增工作台菜单项并更新数据库补丁脚本 - 移除原有的 ZentaoEffortReportQueryService 和 ZentaoDailyEffortJobExecutor - 更新 XXL-JOB 调度分发器支持新的报表构建器模式
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
# willSystem 项目流程图
|
||||
|
||||
## 计划任务配置与执行流程
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[前端计划任务配置页面] --> B[AgentReportController]
|
||||
B --> C[JobConfigService]
|
||||
C --> D[(job_config)]
|
||||
C --> E[ConfigService]
|
||||
E --> F[(config_group / config_item)]
|
||||
F --> G[JOB_TYPE 任务功能下拉]
|
||||
|
||||
C --> H[JobScheduleSyncService]
|
||||
H --> I[XxlJobScheduleSyncService]
|
||||
I --> J[XxlJobAdminClient]
|
||||
J --> K[XXL-JOB Admin]
|
||||
K --> L[executorHandler: agentReportDispatchJob]
|
||||
K --> M[executorParam: configId]
|
||||
|
||||
L --> N[ZentaoEffortReportJobHandler]
|
||||
M --> N
|
||||
N --> C
|
||||
N --> O[JobExecutionDispatcher]
|
||||
O --> P[JobExecutor]
|
||||
O --> Q[BuildReportService]
|
||||
Q --> R["@ReportJob + ReportBuilder"]
|
||||
R --> S[ZentaoDailyEffortReportBuilder]
|
||||
R --> T[ZentaoWeeklyTaskDetailReportBuilder]
|
||||
S --> U[ZentaoEffortReportQueryService]
|
||||
T --> U
|
||||
U --> V[(Zentao DB)]
|
||||
Q --> W[RobotMessageClient / MailMessageClient]
|
||||
W --> X[企业微信 / 飞书 / 钉钉 / 邮件]
|
||||
```
|
||||
|
||||
## 统一配置流程
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[前端统一配置页面] --> B[ConfigController]
|
||||
B --> C[ConfigService]
|
||||
C --> D[(config_group)]
|
||||
C --> E[(config_item)]
|
||||
D --> F[配置组]
|
||||
E --> G[配置项]
|
||||
G --> H[JOB_TYPE]
|
||||
H --> I[计划任务配置的任务功能下拉]
|
||||
```
|
||||
|
||||
## 当前关键设置
|
||||
|
||||
- 计划任务配置表是 `job_config`。
|
||||
- 系统配置主子表是 `config_group` 和 `config_item`。
|
||||
- 任务功能下拉不写死在前端,来自 `config_group.code = JOB_TYPE` 下的启用配置项。
|
||||
- `job_config` 不再保存 `zone`、`daysAgo`、`groupName`。
|
||||
- 时区、执行范围、统计偏移等业务逻辑固定在后端执行逻辑中。
|
||||
- 计划任务保存时,`name`、`jobType`、`cron` 必填。
|
||||
- `jobType` 必须存在于启用的 `JOB_TYPE` 配置项中。
|
||||
- 任务执行采用插件式 `JobExecutor` 结构。
|
||||
- 报表类任务采用 `@ReportJob(jobType = "...") + ReportBuilder` 自动装载。
|
||||
- 新增报表类计划任务时,优先新增一个 `ReportBuilder` 实现类,不改 `BuildReportService` 和调度入口。
|
||||
- 非报表类任务仍可新增一个 `JobExecutor` 实现类。
|
||||
- XXL-JOB 统一使用 `agentReportDispatchJob` 作为执行器 Handler。
|
||||
- XXL-JOB 参数只传 `configId`,执行时再读取 `job_config` 获取完整配置。
|
||||
|
||||
## 新增计划任务推荐流程
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[新增任务需求] --> B[确认是否需要新增 jobType]
|
||||
B --> C[在 JOB_TYPE 配置项中新增任务功能]
|
||||
C --> D{是否报表发送类任务}
|
||||
D -->|是| E[新增 ReportBuilder 实现并标注 @ReportJob]
|
||||
D -->|否| F[新增 JobExecutor 实现]
|
||||
E --> G[实现 build ReportRequest]
|
||||
F --> H[实现 execute JobConfigDTO]
|
||||
G --> I[复用或新增业务 Service]
|
||||
H --> I
|
||||
I --> J[如需新字段再评估 job_config 表结构]
|
||||
J --> K[同步 SQL 脚本和前端表单]
|
||||
K --> L[运行后端编译和前端类型检查]
|
||||
```
|
||||
|
||||
## 后续需求调整原则
|
||||
|
||||
- 先判断需求属于配置管理、任务类型、任务执行器、业务报表查询、消息发送还是 XXL-JOB 同步。
|
||||
- 不在 `JobConfigService` 中增加具体业务执行分支。
|
||||
- 不在 `ZentaoEffortReportJobHandler` 中增加具体任务类型判断。
|
||||
- 不在 `BuildReportService` 中增加具体报表类型判断。
|
||||
- 不在前端写死任务功能下拉值。
|
||||
- 新增表字段前先确认是否是业务逻辑固定值,固定值优先放后端执行代码。
|
||||
- 旧表迁移和新建表脚本必须同步检查。
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package com.will.businessJob.builder;
|
||||
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
public abstract class AbstractZentaoReportBuilder {
|
||||
|
||||
private final JdbcTemplate zentaoJdbcTemplate;
|
||||
|
||||
protected AbstractZentaoReportBuilder(JdbcTemplate zentaoJdbcTemplate) {
|
||||
this.zentaoJdbcTemplate = zentaoJdbcTemplate;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void logZentaoJdbcTemplate() {
|
||||
Object dataSource = zentaoJdbcTemplate.getDataSource();
|
||||
if (dataSource instanceof HikariDataSource hikariDataSource) {
|
||||
log.info("[Job][Zentao] report builder bound jdbcUrl={}, username={}, builder={}",
|
||||
hikariDataSource.getJdbcUrl(), hikariDataSource.getUsername(), getClass().getSimpleName());
|
||||
} else {
|
||||
log.info("[Job][Zentao] report builder bound datasource={}, builder={}",
|
||||
dataSource == null ? "null" : dataSource.getClass().getName(), getClass().getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
protected List<Map<String, Object>> queryList(String sql, Object... args) {
|
||||
try {
|
||||
logSql(sql, args);
|
||||
return zentaoJdbcTemplate.queryForList(sql, args);
|
||||
} catch (Exception ex) {
|
||||
log.warn("[Job][Zentao][SQL] query failed: {}", ex.getMessage(), ex);
|
||||
return List.of(Map.of("count", ex.getMessage() == null ? "bad SQL grammar" : ex.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean isQueryFailed(List<Map<String, Object>> rows) {
|
||||
return rows.size() == 1 && rows.get(0).containsKey("count") && rows.get(0).size() == 1
|
||||
&& rows.get(0).get("count") instanceof String;
|
||||
}
|
||||
|
||||
private void logSql(String sql, Object... args) {
|
||||
log.debug("[Job][Zentao][SQL] {} | args={}", normalizeSql(sql), List.of(args));
|
||||
}
|
||||
|
||||
private String normalizeSql(String sql) {
|
||||
return sql == null ? "" : sql.replaceAll("\\s+", " ").trim();
|
||||
}
|
||||
}
|
||||
+39
-46
@@ -1,12 +1,13 @@
|
||||
package com.will.businessJob.zentao;
|
||||
package com.will.businessJob.builder;
|
||||
|
||||
import com.will.businessJob.config.ReportJob;
|
||||
import com.will.businessJob.config.ReportRequest;
|
||||
import com.will.businessJob.mail.MailMessageClient;
|
||||
import com.will.businessJob.robot.RobotMessageClient;
|
||||
import com.will.coreService.dto.JobConfigDTO;
|
||||
import com.will.coreService.enums.RobotType;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
@@ -14,37 +15,50 @@ import java.time.DayOfWeek;
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneId;
|
||||
import java.time.temporal.TemporalAdjusters;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ZentaoWeComReportService {
|
||||
public class BuildReportService {
|
||||
|
||||
private static final String DEFAULT_GROUP_NAME = "";
|
||||
private static final String DEFAULT_REPORT_TITLE = "禅道工时完成情况";
|
||||
private static final String DEFAULT_REPORT_TITLE = "";
|
||||
private static final String DEFAULT_ZONE = "Asia/Shanghai";
|
||||
private static final int WE_COM_MARKDOWN_MAX_LENGTH = 3900;
|
||||
|
||||
private final ObjectProvider<ZentaoEffortReportQueryService> effortReportQueryService;
|
||||
private final Map<String, ReportBuilder> reportBuilderMap;
|
||||
private final RobotMessageClient robotMessageClient;
|
||||
private final MailMessageClient mailMessageClient;
|
||||
|
||||
public String sendSummaryReport(JobConfigDTO jobConfig) {
|
||||
return sendReport(jobConfig, ZentaoEffortReportQueryService::getGroupTaskEffortRobotSummary);
|
||||
public BuildReportService(
|
||||
List<ReportBuilder> reportBuilders,
|
||||
RobotMessageClient robotMessageClient,
|
||||
MailMessageClient mailMessageClient) {
|
||||
this.reportBuilderMap = reportBuilders.stream()
|
||||
.collect(Collectors.toMap(this::jobType, Function.identity()));
|
||||
this.robotMessageClient = robotMessageClient;
|
||||
this.mailMessageClient = mailMessageClient;
|
||||
}
|
||||
|
||||
public String sendTaskDetailReport(JobConfigDTO jobConfig) {
|
||||
return sendReport(jobConfig, ZentaoEffortReportQueryService::getGroupTaskEffortDetailReport);
|
||||
public boolean supports(String jobType) {
|
||||
return reportBuilderMap.containsKey(jobType);
|
||||
}
|
||||
|
||||
private String sendReport(JobConfigDTO jobConfig, ReportExecutor executor) {
|
||||
public String sendReport(JobConfigDTO jobConfig) {
|
||||
String disabledReason = validateJobConfig(jobConfig);
|
||||
if (disabledReason != null) {
|
||||
return disabledReason;
|
||||
}
|
||||
ReportBuilder reportBuilder = reportBuilderMap.get(jobConfig.getJobType());
|
||||
if (reportBuilder == null) {
|
||||
throw new IllegalArgumentException("不支持的报表任务类型:" + jobConfig.getJobType());
|
||||
}
|
||||
|
||||
ReportRequest request = buildRequest(jobConfig);
|
||||
return sendReport(request, jobConfig.getId(), executor);
|
||||
return sendReport(request, jobConfig.getId(), reportBuilder);
|
||||
}
|
||||
|
||||
private String validateJobConfig(JobConfigDTO jobConfig) {
|
||||
@@ -66,21 +80,18 @@ public class ZentaoWeComReportService {
|
||||
RobotType.defaultName(jobConfig.getRobotType()),
|
||||
jobConfig.getWebhookUrl(),
|
||||
jobConfig.getMailTo(),
|
||||
jobConfig.getMailCc(),
|
||||
defaultString(jobConfig.getMailSubject(), defaultString(jobConfig.getName(), DEFAULT_REPORT_TITLE))
|
||||
);
|
||||
}
|
||||
|
||||
private String sendReport(ReportRequest request, String jobId, ReportExecutor executor) {
|
||||
ZentaoEffortReportQueryService queryService = getEffortReportQueryService();
|
||||
String report = executor.execute(
|
||||
queryService,
|
||||
request.startDate().toString(),
|
||||
request.endDate().toString(),
|
||||
request.groupName());
|
||||
String content = truncateMarkdown(request.robotType(), buildMarkdown(request, report));
|
||||
private String sendReport(ReportRequest request, String jobId, ReportBuilder reportBuilder) {
|
||||
String report = reportBuilder.build(request);
|
||||
String content = buildMarkdown(request, report);
|
||||
if (RobotType.EMAIL.name().equals(RobotType.defaultName(request.robotType()))) {
|
||||
mailMessageClient.sendMarkdown(request.mailTo(), request.mailSubject(), content);
|
||||
mailMessageClient.sendMarkdown(request.mailTo(), request.mailCc(), request.mailSubject(), content);
|
||||
} else {
|
||||
content = truncateMarkdown(request.robotType(), content);
|
||||
robotMessageClient.sendMarkdown(request.robotType(), content, request.webhookUrl());
|
||||
}
|
||||
|
||||
@@ -94,14 +105,6 @@ public class ZentaoWeComReportService {
|
||||
return content;
|
||||
}
|
||||
|
||||
private ZentaoEffortReportQueryService getEffortReportQueryService() {
|
||||
ZentaoEffortReportQueryService queryService = effortReportQueryService.getIfAvailable();
|
||||
if (queryService == null) {
|
||||
throw new IllegalStateException("禅道工时报表通知查询服务未启用");
|
||||
}
|
||||
return queryService;
|
||||
}
|
||||
|
||||
private String buildMarkdown(ReportRequest request, String report) {
|
||||
return """
|
||||
## %s
|
||||
@@ -145,21 +148,11 @@ public class ZentaoWeComReportService {
|
||||
return content.substring(0, cutIndex).trim() + suffix;
|
||||
}
|
||||
|
||||
private record ReportRequest(
|
||||
String title,
|
||||
LocalDate startDate,
|
||||
LocalDate endDate,
|
||||
String groupName,
|
||||
String robotType,
|
||||
String webhookUrl,
|
||||
String mailTo,
|
||||
String mailSubject
|
||||
) {
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface ReportExecutor {
|
||||
|
||||
String execute(ZentaoEffortReportQueryService queryService, String startDate, String endDate, String groupName);
|
||||
private String jobType(ReportBuilder reportBuilder) {
|
||||
ReportJob reportJob = AnnotationUtils.findAnnotation(reportBuilder.getClass(), ReportJob.class);
|
||||
if (reportJob == null || !StringUtils.hasText(reportJob.jobType())) {
|
||||
throw new IllegalArgumentException("报表构建器未配置 ReportJob:" + reportBuilder.getClass().getName());
|
||||
}
|
||||
return reportJob.jobType();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.will.businessJob.builder;
|
||||
|
||||
import com.will.businessJob.config.ReportRequest;
|
||||
|
||||
public interface ReportBuilder {
|
||||
|
||||
String build(ReportRequest request);
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.will.businessJob;
|
||||
package com.will.businessJob.config;
|
||||
|
||||
public final class JobTypes {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.will.businessJob.config;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface ReportJob {
|
||||
|
||||
String jobType();
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.will.businessJob.config;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
public record ReportRequest(
|
||||
String title,
|
||||
LocalDate startDate,
|
||||
LocalDate endDate,
|
||||
String groupName,
|
||||
String robotType,
|
||||
String webhookUrl,
|
||||
String mailTo,
|
||||
String mailCc,
|
||||
String mailSubject
|
||||
) {
|
||||
}
|
||||
@@ -19,7 +19,7 @@ public class MailMessageClient {
|
||||
private final ObjectProvider<JavaMailSender> mailSenderProvider;
|
||||
private final ObjectProvider<MailProperties> mailPropertiesProvider;
|
||||
|
||||
public void sendMarkdown(String mailTo, String subject, String content) {
|
||||
public void sendMarkdown(String mailTo, String mailCc, String subject, String content) {
|
||||
if (!StringUtils.hasText(mailTo)) {
|
||||
throw new IllegalStateException("邮件收件人未配置");
|
||||
}
|
||||
@@ -34,18 +34,30 @@ public class MailMessageClient {
|
||||
if (StringUtils.hasText(from)) {
|
||||
message.setFrom(new InternetAddress(from.trim()));
|
||||
}
|
||||
message.setRecipients(MimeMessage.RecipientType.TO, mailTo.trim());
|
||||
message.setRecipients(MimeMessage.RecipientType.TO, normalizeRecipients(mailTo));
|
||||
if (StringUtils.hasText(mailCc)) {
|
||||
message.setRecipients(MimeMessage.RecipientType.CC, normalizeRecipients(mailCc));
|
||||
}
|
||||
message.setSubject(defaultString(subject, "计划任务通知"), "UTF-8");
|
||||
message.setText(content == null ? "" : content, "UTF-8");
|
||||
mailSender.send(message);
|
||||
log.info("[MailMessage] markdown sent, mailTo={}, length={}", mailTo, content == null ? 0 : content.length());
|
||||
log.info("[MailMessage] markdown sent, mailTo={}, mailCc={}, length={}", mailTo, mailCc, content == null ? 0 : content.length());
|
||||
} catch (Exception ex) {
|
||||
log.warn("[MailMessage] send failed, mailTo={}", mailTo, ex);
|
||||
log.warn("[MailMessage] send failed, mailTo={}, mailCc={}", mailTo, mailCc, ex);
|
||||
throw new IllegalStateException("邮件发送失败:" + ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
private String normalizeRecipients(String recipients) {
|
||||
return recipients.trim()
|
||||
.replace(',', ',')
|
||||
.replace(';', ',')
|
||||
.replace(';', ',')
|
||||
.replace('\n', ',')
|
||||
.replace('\r', ',');
|
||||
}
|
||||
|
||||
private String defaultString(String value, String defaultValue) {
|
||||
return StringUtils.hasText(value) ? value.trim() : defaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-24
@@ -1,24 +0,0 @@
|
||||
package com.will.businessJob.zentao;
|
||||
|
||||
import com.will.coreService.dto.JobConfigDTO;
|
||||
import com.will.coreService.service.JobExecutor;
|
||||
import com.will.businessJob.JobTypes;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class ZentaoDailyEffortJobExecutor implements JobExecutor {
|
||||
|
||||
private final ZentaoWeComReportService reportService;
|
||||
|
||||
@Override
|
||||
public String jobType() {
|
||||
return JobTypes.ZENTAO_DAILY_EFFORT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String execute(JobConfigDTO jobConfig) {
|
||||
return reportService.sendSummaryReport(jobConfig);
|
||||
}
|
||||
}
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
package com.will.businessJob.zentao;
|
||||
|
||||
import com.will.businessJob.builder.AbstractZentaoReportBuilder;
|
||||
import com.will.businessJob.builder.ReportBuilder;
|
||||
import com.will.businessJob.config.JobTypes;
|
||||
import com.will.businessJob.config.ReportJob;
|
||||
import com.will.businessJob.config.ReportRequest;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Component
|
||||
@ConditionalOnBean(name = "zentaoJdbcTemplate")
|
||||
@ReportJob(jobType = JobTypes.ZENTAO_DAILY_EFFORT)
|
||||
public class ZentaoDailyEffortReportBuilder extends AbstractZentaoReportBuilder implements ReportBuilder {
|
||||
|
||||
private static final double MAX_WEEKLY_WARNING_HOURS = 35D;
|
||||
|
||||
public ZentaoDailyEffortReportBuilder(@Qualifier("zentaoJdbcTemplate") JdbcTemplate zentaoJdbcTemplate) {
|
||||
super(zentaoJdbcTemplate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String build(ReportRequest request) {
|
||||
LocalDate start = request.startDate();
|
||||
LocalDate end = request.endDate();
|
||||
if (start == null || end == null) {
|
||||
return "禅道用户组任务工时统计:日期格式错误,请使用 yyyy-MM-dd,例如 2026-08-01";
|
||||
}
|
||||
if (start.isAfter(end)) {
|
||||
return "禅道用户组任务工时统计:开始日期不能晚于结束日期";
|
||||
}
|
||||
String actualGroupName = StringUtils.hasText(request.groupName()) ? request.groupName().trim() : "TSO";
|
||||
List<Object> params = List.of(start.toString(), end.toString(), actualGroupName);
|
||||
|
||||
List<Map<String, Object>> rows = queryList("""
|
||||
SELECT u.account account,
|
||||
COALESCE(NULLIF(u.realname, ''), u.account) realname,
|
||||
COALESCE(effort.consumed, 0) consumed
|
||||
FROM zt_user u
|
||||
INNER JOIN zt_usergroup ug ON ug.account = u.account
|
||||
INNER JOIN zt_group g ON g.id = ug.`group`
|
||||
LEFT JOIN (
|
||||
SELECT e.account, ROUND(SUM(e.consumed), 2) consumed
|
||||
FROM zt_effort e
|
||||
WHERE e.objectType = 'task'
|
||||
AND e.`date` >= ?
|
||||
AND e.`date` <= ?
|
||||
AND e.deleted = '0'
|
||||
GROUP BY e.account
|
||||
) effort ON effort.account = u.account
|
||||
WHERE g.`name` = ?
|
||||
AND u.account <> 'sygu'
|
||||
ORDER BY u.realname ASC, u.account ASC
|
||||
""", params.toArray());
|
||||
|
||||
if (isQueryFailed(rows)) {
|
||||
return "禅道用户组任务工时统计:查询失败," + rows.get(0).get("count");
|
||||
}
|
||||
if (rows.isEmpty()) {
|
||||
return "禅道用户组任务工时统计:无成员数据";
|
||||
}
|
||||
|
||||
long days = end.toEpochDay() - start.toEpochDay() + 1;
|
||||
double threshold = Math.min(Math.max(days - 1, 0) * 7D, MAX_WEEKLY_WARNING_HOURS);
|
||||
List<UserEffort> userEfforts = rows.stream()
|
||||
.collect(Collectors.groupingBy(
|
||||
row -> row.getOrDefault("account", "-").toString(),
|
||||
LinkedHashMap::new,
|
||||
Collectors.toList()))
|
||||
.entrySet()
|
||||
.stream()
|
||||
.map(entry -> toUserEffort(entry.getKey(), entry.getValue()))
|
||||
.sorted(Comparator.comparing(UserEffort::realname).thenComparing(UserEffort::account))
|
||||
.toList();
|
||||
double totalConsumed = userEfforts.stream()
|
||||
.mapToDouble(UserEffort::consumed)
|
||||
.sum();
|
||||
String warningUsers = formatUsers(userEfforts.stream()
|
||||
.filter(user -> user.consumed() < threshold)
|
||||
.toList(), "warning");
|
||||
String normalUsers = formatUsers(userEfforts.stream()
|
||||
.filter(user -> user.consumed() >= threshold)
|
||||
.toList(), "info");
|
||||
String users = """
|
||||
工时不足:
|
||||
%s
|
||||
工时达标:
|
||||
%s
|
||||
""".formatted(warningUsers, normalUsers);
|
||||
|
||||
return """
|
||||
禅道 %s 组任务工时统计:
|
||||
- 时间范围:%s 至 %s
|
||||
- 人员数:%s
|
||||
- 工时不足人数:%s
|
||||
- 工时达标人数:%s
|
||||
- 工时预警阈值:%.2f
|
||||
- 总消耗工时:%.2f
|
||||
按成员:
|
||||
%s
|
||||
""".formatted(
|
||||
actualGroupName,
|
||||
start,
|
||||
end,
|
||||
userEfforts.size(),
|
||||
userEfforts.stream().filter(user -> user.consumed() < threshold).count(),
|
||||
userEfforts.stream().filter(user -> user.consumed() >= threshold).count(),
|
||||
threshold,
|
||||
totalConsumed,
|
||||
users);
|
||||
}
|
||||
|
||||
private UserEffort toUserEffort(String account, List<Map<String, Object>> rows) {
|
||||
String realname = rows.stream()
|
||||
.map(row -> row.getOrDefault("realname", account).toString())
|
||||
.filter(StringUtils::hasText)
|
||||
.findFirst()
|
||||
.orElse(account);
|
||||
double consumed = rows.stream()
|
||||
.map(row -> row.get("consumed"))
|
||||
.filter(Number.class::isInstance)
|
||||
.map(Number.class::cast)
|
||||
.mapToDouble(Number::doubleValue)
|
||||
.sum();
|
||||
return new UserEffort(account, realname, consumed);
|
||||
}
|
||||
|
||||
private String formatUsers(List<UserEffort> userEfforts, String color) {
|
||||
if (userEfforts.isEmpty()) {
|
||||
return "- 无";
|
||||
}
|
||||
return userEfforts.stream()
|
||||
.map(user -> "- <font color=\"%s\">姓名=%s | 账号=%s | 消耗工时=%.2f</font>"
|
||||
.formatted(color, user.realname(), user.account(), user.consumed()))
|
||||
.collect(Collectors.joining("\n"));
|
||||
}
|
||||
|
||||
private record UserEffort(String account, String realname, double consumed) {
|
||||
}
|
||||
}
|
||||
-219
@@ -1,219 +0,0 @@
|
||||
package com.will.businessJob.zentao;
|
||||
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@ConditionalOnBean(name = "zentaoJdbcTemplate")
|
||||
public class ZentaoEffortReportQueryService {
|
||||
|
||||
private final JdbcTemplate zentaoJdbcTemplate;
|
||||
|
||||
public ZentaoEffortReportQueryService(@Qualifier("zentaoJdbcTemplate") JdbcTemplate zentaoJdbcTemplate) {
|
||||
this.zentaoJdbcTemplate = zentaoJdbcTemplate;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void logZentaoJdbcTemplate() {
|
||||
Object dataSource = zentaoJdbcTemplate.getDataSource();
|
||||
if (dataSource instanceof HikariDataSource hikariDataSource) {
|
||||
log.info("[Job][Zentao] effort report bound jdbcUrl={}, username={}",
|
||||
hikariDataSource.getJdbcUrl(), hikariDataSource.getUsername());
|
||||
} else {
|
||||
log.info("[Job][Zentao] effort report bound datasource={}",
|
||||
dataSource == null ? "null" : dataSource.getClass().getName());
|
||||
}
|
||||
}
|
||||
|
||||
public String getGroupTaskEffortRobotSummary(String startDate, String endDate, String groupName) {
|
||||
LocalDate start = parseDate(startDate);
|
||||
LocalDate end = parseDate(endDate);
|
||||
if (start == null || end == null) {
|
||||
return "禅道用户组任务工时统计:日期格式错误,请使用 yyyy-MM-dd,例如 2026-08-01";
|
||||
}
|
||||
if (start.isAfter(end)) {
|
||||
return "禅道用户组任务工时统计:开始日期不能晚于结束日期";
|
||||
}
|
||||
String actualGroupName = StringUtils.hasText(groupName) ? groupName.trim() : "TSO";
|
||||
List<Object> params = List.of(start.toString(), end.toString(), actualGroupName);
|
||||
|
||||
List<Map<String, Object>> rows = queryList("""
|
||||
SELECT u.account account,
|
||||
COALESCE(NULLIF(u.realname, ''), u.account) realname,
|
||||
COALESCE(effort.consumed, 0) consumed
|
||||
FROM zt_user u
|
||||
INNER JOIN zt_usergroup ug ON ug.account = u.account
|
||||
INNER JOIN zt_group g ON g.id = ug.`group`
|
||||
LEFT JOIN (
|
||||
SELECT e.account, ROUND(SUM(e.consumed), 2) consumed
|
||||
FROM zt_effort e
|
||||
WHERE e.objectType = 'task'
|
||||
AND e.`date` >= ?
|
||||
AND e.`date` <= ?
|
||||
GROUP BY e.account
|
||||
) effort ON effort.account = u.account
|
||||
WHERE g.`name` = ?
|
||||
AND u.deleted = '0'
|
||||
AND u.account <> 'sygu'
|
||||
ORDER BY consumed ASC, u.account ASC
|
||||
""", params.toArray());
|
||||
|
||||
if (isQueryFailed(rows)) {
|
||||
return "禅道用户组任务工时统计:查询失败," + rows.get(0).get("count");
|
||||
}
|
||||
if (rows.isEmpty()) {
|
||||
return "禅道用户组任务工时统计:无成员数据";
|
||||
}
|
||||
|
||||
long days = end.toEpochDay() - start.toEpochDay() + 1;
|
||||
double threshold = Math.max(days - 1, 0) * 7D;
|
||||
double totalConsumed = rows.stream()
|
||||
.map(row -> row.get("consumed"))
|
||||
.filter(Number.class::isInstance)
|
||||
.map(Number.class::cast)
|
||||
.mapToDouble(Number::doubleValue)
|
||||
.sum();
|
||||
String users = rows.stream()
|
||||
.map(row -> {
|
||||
Object realname = row.getOrDefault("realname", "-");
|
||||
Object consumedValue = row.getOrDefault("consumed", 0);
|
||||
double consumed = consumedValue instanceof Number number ? number.doubleValue() : 0D;
|
||||
String line = "姓名=%s | 消耗工时=%s".formatted(realname, consumedValue);
|
||||
String color = consumed < threshold ? "warning" : "info";
|
||||
return "- <font color=\"" + color + "\">" + line + "</font>";
|
||||
})
|
||||
.collect(Collectors.joining("\n"));
|
||||
|
||||
return """
|
||||
禅道 %s 组任务工时统计:
|
||||
- 时间范围:%s 至 %s
|
||||
- 人员数:%s
|
||||
- 工时预警阈值:%.2f
|
||||
- 总消耗工时:%.2f
|
||||
按成员:
|
||||
%s
|
||||
""".formatted(actualGroupName, start, end, rows.size(), threshold, totalConsumed, users);
|
||||
}
|
||||
|
||||
public String getGroupTaskEffortDetailReport(String startDate, String endDate, String groupName) {
|
||||
LocalDate start = parseDate(startDate);
|
||||
LocalDate end = parseDate(endDate);
|
||||
if (start == null || end == null) {
|
||||
return "禅道任务明细报表:日期格式错误,请使用 yyyy-MM-dd,例如 2026-08-01";
|
||||
}
|
||||
if (start.isAfter(end)) {
|
||||
return "禅道任务明细报表:开始日期不能晚于结束日期";
|
||||
}
|
||||
String actualGroupName = StringUtils.hasText(groupName) ? groupName.trim() : "PRO";
|
||||
List<Object> params = List.of(start.toString(), end.toString(), actualGroupName);
|
||||
|
||||
List<Map<String, Object>> rows = queryList("""
|
||||
SELECT e.`date` effortDate,
|
||||
e.account account,
|
||||
COALESCE(NULLIF(u.realname, ''), e.account) realname,
|
||||
e.objectID taskId,
|
||||
COALESCE(NULLIF(t.`name`, ''), CONCAT('TASK-', e.objectID)) taskName,
|
||||
COALESCE(NULLIF(e.`work`, ''), '-') workContent,
|
||||
ROUND(e.consumed, 2) consumed
|
||||
FROM zt_effort e
|
||||
INNER JOIN zt_user u ON u.account = e.account
|
||||
INNER JOIN zt_usergroup ug ON ug.account = u.account
|
||||
INNER JOIN zt_group g ON g.id = ug.`group`
|
||||
LEFT JOIN zt_task t ON t.id = e.objectID
|
||||
WHERE e.objectType = 'task'
|
||||
AND e.`date` >= ?
|
||||
AND e.`date` <= ?
|
||||
AND g.`name` = ?
|
||||
AND u.deleted = '0'
|
||||
AND u.account <> 'sygu'
|
||||
ORDER BY e.`date` ASC, e.account ASC, e.objectID ASC, e.id ASC
|
||||
""", params.toArray());
|
||||
|
||||
if (isQueryFailed(rows)) {
|
||||
return "禅道任务明细报表:查询失败," + rows.get(0).get("count");
|
||||
}
|
||||
if (rows.isEmpty()) {
|
||||
return "禅道任务明细报表:本周暂无任务工时明细";
|
||||
}
|
||||
|
||||
double totalConsumed = rows.stream()
|
||||
.map(row -> row.get("consumed"))
|
||||
.filter(Number.class::isInstance)
|
||||
.map(Number.class::cast)
|
||||
.mapToDouble(Number::doubleValue)
|
||||
.sum();
|
||||
String details = rows.stream()
|
||||
.limit(80)
|
||||
.map(this::formatEffortDetail)
|
||||
.collect(Collectors.joining("\n"));
|
||||
String suffix = rows.size() > 80
|
||||
? "\n> 明细过多,仅展示前 80 条,共 " + rows.size() + " 条"
|
||||
: "";
|
||||
|
||||
return """
|
||||
禅道 %s 组本周任务明细:
|
||||
- 时间范围:%s 至 %s
|
||||
- 明细条数:%s
|
||||
- 合计工时:%.2f
|
||||
|
||||
%s%s
|
||||
""".formatted(actualGroupName, start, end, rows.size(), totalConsumed, details, suffix);
|
||||
}
|
||||
|
||||
private String formatEffortDetail(Map<String, Object> row) {
|
||||
return "- %s | %s | #%s %s | %s | 工时=%s".formatted(
|
||||
row.getOrDefault("effortDate", "-"),
|
||||
row.getOrDefault("realname", "-"),
|
||||
row.getOrDefault("taskId", "-"),
|
||||
row.getOrDefault("taskName", "-"),
|
||||
row.getOrDefault("workContent", "-"),
|
||||
row.getOrDefault("consumed", "-"));
|
||||
}
|
||||
|
||||
private LocalDate parseDate(String value) {
|
||||
if (!StringUtils.hasText(value)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return LocalDate.parse(value.trim());
|
||||
} catch (DateTimeParseException ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> queryList(String sql, Object... args) {
|
||||
try {
|
||||
logSql(sql, args);
|
||||
return zentaoJdbcTemplate.queryForList(sql, args);
|
||||
} catch (Exception ex) {
|
||||
log.warn("[Job][Zentao][SQL] query failed: {}", ex.getMessage(), ex);
|
||||
return List.of(Map.of("count", ex.getMessage() == null ? "bad SQL grammar" : ex.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isQueryFailed(List<Map<String, Object>> rows) {
|
||||
return rows.size() == 1 && rows.get(0).containsKey("count") && rows.get(0).size() == 1
|
||||
&& rows.get(0).get("count") instanceof String;
|
||||
}
|
||||
|
||||
private void logSql(String sql, Object... args) {
|
||||
log.debug("[Job][Zentao][SQL] {} | args={}", normalizeSql(sql), List.of(args));
|
||||
}
|
||||
|
||||
private String normalizeSql(String sql) {
|
||||
return sql == null ? "" : sql.replaceAll("\\s+", " ").trim();
|
||||
}
|
||||
}
|
||||
-24
@@ -1,24 +0,0 @@
|
||||
package com.will.businessJob.zentao;
|
||||
|
||||
import com.will.coreService.dto.JobConfigDTO;
|
||||
import com.will.coreService.service.JobExecutor;
|
||||
import com.will.businessJob.JobTypes;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class ZentaoWeeklyTaskDetailJobExecutor implements JobExecutor {
|
||||
|
||||
private final ZentaoWeComReportService reportService;
|
||||
|
||||
@Override
|
||||
public String jobType() {
|
||||
return JobTypes.ZENTAO_WEEKLY_TASK_DETAIL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String execute(JobConfigDTO jobConfig) {
|
||||
return reportService.sendTaskDetailReport(jobConfig);
|
||||
}
|
||||
}
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
package com.will.businessJob.zentao;
|
||||
|
||||
import com.will.businessJob.builder.AbstractZentaoReportBuilder;
|
||||
import com.will.businessJob.builder.ReportBuilder;
|
||||
import com.will.businessJob.config.JobTypes;
|
||||
import com.will.businessJob.config.ReportJob;
|
||||
import com.will.businessJob.config.ReportRequest;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Component
|
||||
@ConditionalOnBean(name = "zentaoJdbcTemplate")
|
||||
@ReportJob(jobType = JobTypes.ZENTAO_WEEKLY_TASK_DETAIL)
|
||||
public class ZentaoWeeklyTaskDetailReportBuilder extends AbstractZentaoReportBuilder implements ReportBuilder {
|
||||
|
||||
public ZentaoWeeklyTaskDetailReportBuilder(@Qualifier("zentaoJdbcTemplate") JdbcTemplate zentaoJdbcTemplate) {
|
||||
super(zentaoJdbcTemplate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String build(ReportRequest request) {
|
||||
LocalDate start = request.startDate();
|
||||
LocalDate end = request.endDate();
|
||||
if (start == null || end == null) {
|
||||
return "禅道任务明细报表:日期格式错误,请使用 yyyy-MM-dd,例如 2026-08-01";
|
||||
}
|
||||
if (start.isAfter(end)) {
|
||||
return "禅道任务明细报表:开始日期不能晚于结束日期";
|
||||
}
|
||||
String actualGroupName = StringUtils.hasText(request.groupName()) ? request.groupName().trim() : "PRO";
|
||||
List<Object> params = List.of(start.toString(), end.toString(), actualGroupName);
|
||||
|
||||
List<Map<String, Object>> rows = queryList("""
|
||||
SELECT e.`date` effortDate,
|
||||
e.account account,
|
||||
COALESCE(NULLIF(u.realname, ''), e.account) realname,
|
||||
e.objectID taskId,
|
||||
COALESCE(NULLIF(t.`name`, ''), CONCAT('TASK-', e.objectID)) taskName,
|
||||
COALESCE(NULLIF(e.`work`, ''), '-') workContent,
|
||||
ROUND(e.consumed, 2) consumed
|
||||
FROM zt_effort e
|
||||
INNER JOIN zt_user u ON u.account = e.account
|
||||
INNER JOIN zt_usergroup ug ON ug.account = u.account
|
||||
INNER JOIN zt_group g ON g.id = ug.`group`
|
||||
LEFT JOIN zt_task t ON t.id = e.objectID
|
||||
WHERE e.objectType = 'task'
|
||||
AND e.`date` >= ?
|
||||
AND e.`date` <= ?
|
||||
AND g.`name` = ?
|
||||
AND u.deleted = '0'
|
||||
AND u.account <> 'sygu'
|
||||
ORDER BY e.account ASC, e.`date` ASC, e.objectID ASC, e.id ASC
|
||||
""", params.toArray());
|
||||
|
||||
if (isQueryFailed(rows)) {
|
||||
return "禅道任务明细报表:查询失败," + rows.get(0).get("count");
|
||||
}
|
||||
if (rows.isEmpty()) {
|
||||
return "禅道任务明细报表:本周暂无任务工时明细";
|
||||
}
|
||||
|
||||
double totalConsumed = rows.stream()
|
||||
.map(row -> row.get("consumed"))
|
||||
.filter(Number.class::isInstance)
|
||||
.map(Number.class::cast)
|
||||
.mapToDouble(Number::doubleValue)
|
||||
.sum();
|
||||
List<UserTaskDetail> userDetails = rows.stream()
|
||||
.collect(Collectors.groupingBy(
|
||||
row -> row.getOrDefault("account", "-").toString(),
|
||||
LinkedHashMap::new,
|
||||
Collectors.toList()))
|
||||
.entrySet()
|
||||
.stream()
|
||||
.map(entry -> toUserTaskDetail(entry.getKey(), entry.getValue()))
|
||||
.sorted(Comparator.comparing(UserTaskDetail::account))
|
||||
.toList();
|
||||
String details = userDetails.stream()
|
||||
.map(this::formatUserTaskDetail)
|
||||
.collect(Collectors.joining("\n\n"));
|
||||
|
||||
return """
|
||||
禅道 %s 组本周任务明细:
|
||||
- 时间范围:%s 至 %s
|
||||
- 人员数:%s
|
||||
- 明细条数:%s
|
||||
- 合计工时:%.2f
|
||||
|
||||
%s
|
||||
""".formatted(actualGroupName, start, end, userDetails.size(), rows.size(), totalConsumed, details);
|
||||
}
|
||||
|
||||
private UserTaskDetail toUserTaskDetail(String account, List<Map<String, Object>> rows) {
|
||||
String realname = rows.stream()
|
||||
.map(row -> row.getOrDefault("realname", account).toString())
|
||||
.filter(StringUtils::hasText)
|
||||
.findFirst()
|
||||
.orElse(account);
|
||||
double consumed = rows.stream()
|
||||
.map(row -> row.get("consumed"))
|
||||
.filter(Number.class::isInstance)
|
||||
.map(Number.class::cast)
|
||||
.mapToDouble(Number::doubleValue)
|
||||
.sum();
|
||||
List<Map<String, Object>> details = rows.stream()
|
||||
.sorted(Comparator.comparing((Map<String, Object> row) -> row.getOrDefault("effortDate", "").toString())
|
||||
.thenComparing(row -> row.getOrDefault("taskId", "").toString()))
|
||||
.toList();
|
||||
return new UserTaskDetail(account, realname, consumed, details.size(), details);
|
||||
}
|
||||
|
||||
private String formatUserTaskDetail(UserTaskDetail userDetail) {
|
||||
String details = userDetail.details()
|
||||
.stream()
|
||||
.map(this::formatEffortDetail)
|
||||
.collect(Collectors.joining("\n"));
|
||||
return """
|
||||
### %s(%s)
|
||||
- 明细条数:%s
|
||||
- 合计工时:%.2f
|
||||
%s
|
||||
""".formatted(
|
||||
userDetail.realname(),
|
||||
userDetail.account(),
|
||||
userDetail.detailCount(),
|
||||
userDetail.consumed(),
|
||||
details);
|
||||
}
|
||||
|
||||
private String formatEffortDetail(Map<String, Object> row) {
|
||||
return "- %s | #%s %s | %s | 工时=%s".formatted(
|
||||
row.getOrDefault("effortDate", "-"),
|
||||
row.getOrDefault("taskId", "-"),
|
||||
row.getOrDefault("taskName", "-"),
|
||||
row.getOrDefault("workContent", "-"),
|
||||
row.getOrDefault("consumed", "-"));
|
||||
}
|
||||
|
||||
private record UserTaskDetail(
|
||||
String account,
|
||||
String realname,
|
||||
double consumed,
|
||||
int detailCount,
|
||||
List<Map<String, Object>> details
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,9 @@ public class JobConfig extends BaseEntity {
|
||||
@TableField("mailTo")
|
||||
private String mailTo;
|
||||
|
||||
@TableField("mailCc")
|
||||
private String mailCc;
|
||||
|
||||
@TableField("mailSubject")
|
||||
private String mailSubject;
|
||||
@TableField("xxlJobId")
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.will.coreJob.framework.xxl;
|
||||
|
||||
import com.will.businessJob.builder.BuildReportService;
|
||||
import com.will.coreService.dto.JobConfigDTO;
|
||||
import com.will.coreService.service.JobExecutor;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -13,13 +14,18 @@ import java.util.stream.Collectors;
|
||||
public class JobExecutionDispatcher {
|
||||
|
||||
private final Map<String, JobExecutor> executorMap;
|
||||
private final BuildReportService buildReportService;
|
||||
|
||||
public JobExecutionDispatcher(List<JobExecutor> executors) {
|
||||
public JobExecutionDispatcher(List<JobExecutor> executors, BuildReportService buildReportService) {
|
||||
this.executorMap = executors.stream()
|
||||
.collect(Collectors.toMap(JobExecutor::jobType, Function.identity()));
|
||||
this.buildReportService = buildReportService;
|
||||
}
|
||||
|
||||
public String execute(JobConfigDTO jobConfig) {
|
||||
if (buildReportService.supports(jobConfig.getJobType())) {
|
||||
return buildReportService.sendReport(jobConfig);
|
||||
}
|
||||
JobExecutor executor = executorMap.get(jobConfig.getJobType());
|
||||
if (executor == null) {
|
||||
throw new IllegalArgumentException("不支持的任务类型:" + jobConfig.getJobType());
|
||||
|
||||
@@ -13,6 +13,7 @@ public class JobConfigDTO {
|
||||
private String robotType;
|
||||
private String webhookUrl;
|
||||
private String mailTo;
|
||||
private String mailCc;
|
||||
private String mailSubject;
|
||||
private Integer xxlJobId;
|
||||
private Boolean localSchedulerEnabled;
|
||||
|
||||
@@ -77,6 +77,7 @@ public class JobConfigService {
|
||||
config.setRobotType(trimRobotTypeToNull(dto.getRobotType()));
|
||||
config.setWebhookUrl(trimToNull(dto.getWebhookUrl()));
|
||||
config.setMailTo(trimToNull(dto.getMailTo()));
|
||||
config.setMailCc(trimToNull(dto.getMailCc()));
|
||||
config.setMailSubject(trimToNull(dto.getMailSubject()));
|
||||
config.setLocalSchedulerEnabled(false);
|
||||
config.setRemark(trimToNull(dto.getRemark()));
|
||||
|
||||
@@ -210,9 +210,15 @@ public class MenuManageService {
|
||||
|
||||
private boolean isRouteVisible(AuthMenu menu, Set<String> permissions, boolean superAdmin) {
|
||||
|
||||
if (isPublicRoute(menu)) {
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
if (MenuTypes.DIR.equals(menu.getMenuType())) {
|
||||
|
||||
return isPublicRoute(menu) || superAdmin || hasVisibleDescendant(menu, permissions, superAdmin);
|
||||
return superAdmin || hasVisibleDescendant(menu, permissions, superAdmin);
|
||||
|
||||
}
|
||||
|
||||
@@ -230,7 +236,9 @@ public class MenuManageService {
|
||||
|
||||
private boolean isPublicRoute(AuthMenu menu) {
|
||||
|
||||
return StringUtils.hasText(menu.getCode()) && menu.getCode().startsWith(MenuTypes.ROUTE_CODE_PREFIX);
|
||||
String code = menu.getCode();
|
||||
|
||||
return "route:dashboard".equals(code) || "route:dashboard:workspace".equals(code);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ public class RbacMenuPatchRunner implements ApplicationRunner {
|
||||
patchJobConfigColumns();
|
||||
patchSqlResource("/db/patch_drop_agent_report_config.sql", "[Agent][DropReportConfig]");
|
||||
patchSqlResource("/db/patch_drop_agent_task_function.sql", "[Agent][DropTaskFunction]");
|
||||
patchSqlResource("/db/patch_dashboard_menu.sql", "[RBAC][DashboardMenu]");
|
||||
patchAgentMenu();
|
||||
}
|
||||
|
||||
@@ -85,10 +86,14 @@ public class RbacMenuPatchRunner implements ApplicationRunner {
|
||||
"job_config",
|
||||
"mailTo",
|
||||
"ALTER TABLE job_config ADD COLUMN mailTo VARCHAR(500) NULL COMMENT '邮件收件人' AFTER webhookUrl");
|
||||
addColumnIfMissing(
|
||||
"job_config",
|
||||
"mailCc",
|
||||
"ALTER TABLE job_config ADD COLUMN mailCc VARCHAR(500) NULL COMMENT '邮件抄送人' AFTER mailTo");
|
||||
addColumnIfMissing(
|
||||
"job_config",
|
||||
"mailSubject",
|
||||
"ALTER TABLE job_config ADD COLUMN mailSubject VARCHAR(200) NULL COMMENT '邮件主题' AFTER mailTo");
|
||||
"ALTER TABLE job_config ADD COLUMN mailSubject VARCHAR(200) NULL COMMENT '邮件主题' AFTER mailCc");
|
||||
addColumnIfMissing(
|
||||
"job_config",
|
||||
"xxlJobId",
|
||||
|
||||
@@ -29,6 +29,16 @@ spring:
|
||||
request-timeout: 60s
|
||||
stdio:
|
||||
servers-configuration: classpath:mcp-servers.json
|
||||
mail:
|
||||
host: ${MAIL_HOST:smtp.qiye.aliyun.com}
|
||||
port: ${MAIL_PORT:465}
|
||||
username: ${MAIL_USERNAME:wall-e-alert@ittx.com.cn}
|
||||
password: ${MAIL_PASSWORD:Ttx123456}
|
||||
properties:
|
||||
mail.from: ${MAIL_FROM:${MAIL_USERNAME:}}
|
||||
mail.smtp.auth: ${MAIL_SMTP_AUTH:true}
|
||||
mail.smtp.ssl.enable: ${MAIL_SMTP_SSL_ENABLE:true}
|
||||
mail.smtp.starttls.enable: ${MAIL_SMTP_STARTTLS_ENABLE:true}
|
||||
rabbitmq:
|
||||
host: ${RABBITMQ_HOST:118.178.233.171}
|
||||
port: ${RABBITMQ_PORT:5672}
|
||||
|
||||
@@ -29,6 +29,16 @@ spring:
|
||||
request-timeout: 60s
|
||||
stdio:
|
||||
servers-configuration: classpath:mcp-servers.json
|
||||
mail:
|
||||
host: ${MAIL_HOST}
|
||||
port: ${MAIL_PORT:465}
|
||||
username: ${MAIL_USERNAME}
|
||||
password: ${MAIL_PASSWORD}
|
||||
properties:
|
||||
mail.from: ${MAIL_FROM:${MAIL_USERNAME}}
|
||||
mail.smtp.auth: ${MAIL_SMTP_AUTH:true}
|
||||
mail.smtp.ssl.enable: ${MAIL_SMTP_SSL_ENABLE:true}
|
||||
mail.smtp.starttls.enable: ${MAIL_SMTP_STARTTLS_ENABLE:false}
|
||||
rabbitmq:
|
||||
host: ${RABBITMQ_HOST}
|
||||
port: ${RABBITMQ_PORT:5672}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
INSERT INTO auth_menu (
|
||||
id, code, name, parentId, menuType, routePath, routeName, component, redirect, icon, sortOrder, title,
|
||||
created, createdBy, lastUpdated, lastUpdatedBy, version
|
||||
) VALUES
|
||||
('route-dashboard', 'route:dashboard', '工作台', NULL, 'DIR', '/dashboard', 'Dashboard', NULL, '/dashboard/workspace', 'lucide:layout-dashboard', -1, 'page.dashboard.title', NOW(), 'SYSTEM', NOW(), 'SYSTEM', 0),
|
||||
('route-dashboard-analytics', 'route:dashboard:analytics', '分析页', 'route-dashboard', 'MENU', 'analytics', 'Analytics', '/dashboard/analytics/index', NULL, NULL, 1, 'page.dashboard.analytics', NOW(), 'SYSTEM', NOW(), 'SYSTEM', 0),
|
||||
('route-dashboard-workspace', 'route:dashboard:workspace', '工作区', 'route-dashboard', 'MENU', 'workspace', 'Workspace', '/dashboard/workspace/index', NULL, NULL, 2, 'page.dashboard.workspace', NOW(), 'SYSTEM', NOW(), 'SYSTEM', 0)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
code = VALUES(code), name = VALUES(name), parentId = VALUES(parentId), menuType = VALUES(menuType),
|
||||
routePath = VALUES(routePath), routeName = VALUES(routeName), component = VALUES(component),
|
||||
redirect = VALUES(redirect), icon = VALUES(icon), sortOrder = VALUES(sortOrder), title = VALUES(title);
|
||||
@@ -7,6 +7,7 @@ CREATE TABLE IF NOT EXISTS job_config (
|
||||
robotType VARCHAR(32) NULL COMMENT 'robot type override',
|
||||
webhookUrl VARCHAR(500) NULL COMMENT 'webhook override',
|
||||
mailTo VARCHAR(500) NULL COMMENT 'mail recipients',
|
||||
mailCc VARCHAR(500) NULL COMMENT 'mail cc recipients',
|
||||
mailSubject VARCHAR(200) NULL COMMENT 'mail subject',
|
||||
xxlJobId INT NULL COMMENT 'xxl-job id',
|
||||
localSchedulerEnabled TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'local scheduler enabled flag',
|
||||
|
||||
Reference in New Issue
Block a user