From c4bcfab31615e550ef7a76bcf82713cb0df6dd60 Mon Sep 17 00:00:00 2001 From: will Date: Sun, 9 Aug 2026 14:13:43 +0800 Subject: [PATCH] =?UTF-8?q?feat(job):=20=E9=87=8D=E6=9E=84=E6=8A=A5?= =?UTF-8?q?=E8=A1=A8=E4=BB=BB=E5=8A=A1=E6=89=A7=E8=A1=8C=E6=9E=B6=E6=9E=84?= =?UTF-8?q?=E5=B9=B6=E6=96=B0=E5=A2=9E=E9=82=AE=E4=BB=B6=E6=8A=84=E9=80=81?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 AbstractZentaoReportBuilder 抽象类提供通用查询方法 - 新增 ReportBuilder 接口和 @ReportJob 注解实现报表构建器插件化 - 新增 ZentaoDailyEffortReportBuilder 实现日报工时统计报表 - 将 ZentaoWeComReportService 重命名为 BuildReportService 并重构为通用报表服务 - 在 job_config 表新增 mailCc 字段支持邮件抄送功能 - 更新 MailMessageClient 支持抄送人参数和收件人格式标准化 - 新增工作台菜单项并更新数据库补丁脚本 - 移除原有的 ZentaoEffortReportQueryService 和 ZentaoDailyEffortJobExecutor - 更新 XXL-JOB 调度分发器支持新的报表构建器模式 --- PROJECT_FLOW.md | 93 ++++++++ .../builder/AbstractZentaoReportBuilder.java | 54 +++++ .../BuildReportService.java} | 85 ++++--- .../businessJob/builder/ReportBuilder.java | 8 + .../businessJob/{ => config}/JobTypes.java | 2 +- .../will/businessJob/config/ReportJob.java | 13 ++ .../businessJob/config/ReportRequest.java | 16 ++ .../businessJob/mail/MailMessageClient.java | 22 +- .../zentao/ZentaoDailyEffortJobExecutor.java | 24 -- .../ZentaoDailyEffortReportBuilder.java | 150 ++++++++++++ .../ZentaoEffortReportQueryService.java | 219 ------------------ .../ZentaoWeeklyTaskDetailJobExecutor.java | 24 -- .../ZentaoWeeklyTaskDetailReportBuilder.java | 157 +++++++++++++ .../com/will/coreData/entity/JobConfig.java | 3 + .../framework/xxl/JobExecutionDispatcher.java | 8 +- .../will/coreService/dto/JobConfigDTO.java | 1 + .../coreService/service/JobConfigService.java | 1 + .../service/MenuManageService.java | 12 +- .../wcsApp/startup/RbacMenuPatchRunner.java | 7 +- wcsApp/src/main/resources/application-dev.yml | 10 + .../src/main/resources/application-prod.yml | 10 + .../resources/db/patch_dashboard_menu.sql | 11 + .../main/resources/db/patch_job_config.sql | 1 + 23 files changed, 608 insertions(+), 323 deletions(-) create mode 100644 PROJECT_FLOW.md create mode 100644 businessJob/src/main/java/com/will/businessJob/builder/AbstractZentaoReportBuilder.java rename businessJob/src/main/java/com/will/businessJob/{zentao/ZentaoWeComReportService.java => builder/BuildReportService.java} (67%) create mode 100644 businessJob/src/main/java/com/will/businessJob/builder/ReportBuilder.java rename businessJob/src/main/java/com/will/businessJob/{ => config}/JobTypes.java (86%) create mode 100644 businessJob/src/main/java/com/will/businessJob/config/ReportJob.java create mode 100644 businessJob/src/main/java/com/will/businessJob/config/ReportRequest.java delete mode 100644 businessJob/src/main/java/com/will/businessJob/zentao/ZentaoDailyEffortJobExecutor.java create mode 100644 businessJob/src/main/java/com/will/businessJob/zentao/ZentaoDailyEffortReportBuilder.java delete mode 100644 businessJob/src/main/java/com/will/businessJob/zentao/ZentaoEffortReportQueryService.java delete mode 100644 businessJob/src/main/java/com/will/businessJob/zentao/ZentaoWeeklyTaskDetailJobExecutor.java create mode 100644 businessJob/src/main/java/com/will/businessJob/zentao/ZentaoWeeklyTaskDetailReportBuilder.java create mode 100644 wcsApp/src/main/resources/db/patch_dashboard_menu.sql diff --git a/PROJECT_FLOW.md b/PROJECT_FLOW.md new file mode 100644 index 0000000..b02fc9c --- /dev/null +++ b/PROJECT_FLOW.md @@ -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` 中增加具体报表类型判断。 +- 不在前端写死任务功能下拉值。 +- 新增表字段前先确认是否是业务逻辑固定值,固定值优先放后端执行代码。 +- 旧表迁移和新建表脚本必须同步检查。 diff --git a/businessJob/src/main/java/com/will/businessJob/builder/AbstractZentaoReportBuilder.java b/businessJob/src/main/java/com/will/businessJob/builder/AbstractZentaoReportBuilder.java new file mode 100644 index 0000000..83da86b --- /dev/null +++ b/businessJob/src/main/java/com/will/businessJob/builder/AbstractZentaoReportBuilder.java @@ -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> 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> 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(); + } +} diff --git a/businessJob/src/main/java/com/will/businessJob/zentao/ZentaoWeComReportService.java b/businessJob/src/main/java/com/will/businessJob/builder/BuildReportService.java similarity index 67% rename from businessJob/src/main/java/com/will/businessJob/zentao/ZentaoWeComReportService.java rename to businessJob/src/main/java/com/will/businessJob/builder/BuildReportService.java index cb0f452..75efa4c 100644 --- a/businessJob/src/main/java/com/will/businessJob/zentao/ZentaoWeComReportService.java +++ b/businessJob/src/main/java/com/will/businessJob/builder/BuildReportService.java @@ -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 effortReportQueryService; + private final Map reportBuilderMap; private final RobotMessageClient robotMessageClient; private final MailMessageClient mailMessageClient; - public String sendSummaryReport(JobConfigDTO jobConfig) { - return sendReport(jobConfig, ZentaoEffortReportQueryService::getGroupTaskEffortRobotSummary); + public BuildReportService( + List 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(); } } diff --git a/businessJob/src/main/java/com/will/businessJob/builder/ReportBuilder.java b/businessJob/src/main/java/com/will/businessJob/builder/ReportBuilder.java new file mode 100644 index 0000000..2b0c41a --- /dev/null +++ b/businessJob/src/main/java/com/will/businessJob/builder/ReportBuilder.java @@ -0,0 +1,8 @@ +package com.will.businessJob.builder; + +import com.will.businessJob.config.ReportRequest; + +public interface ReportBuilder { + + String build(ReportRequest request); +} diff --git a/businessJob/src/main/java/com/will/businessJob/JobTypes.java b/businessJob/src/main/java/com/will/businessJob/config/JobTypes.java similarity index 86% rename from businessJob/src/main/java/com/will/businessJob/JobTypes.java rename to businessJob/src/main/java/com/will/businessJob/config/JobTypes.java index 2f7a6d5..9e82bc2 100644 --- a/businessJob/src/main/java/com/will/businessJob/JobTypes.java +++ b/businessJob/src/main/java/com/will/businessJob/config/JobTypes.java @@ -1,4 +1,4 @@ -package com.will.businessJob; +package com.will.businessJob.config; public final class JobTypes { diff --git a/businessJob/src/main/java/com/will/businessJob/config/ReportJob.java b/businessJob/src/main/java/com/will/businessJob/config/ReportJob.java new file mode 100644 index 0000000..73bec57 --- /dev/null +++ b/businessJob/src/main/java/com/will/businessJob/config/ReportJob.java @@ -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(); +} diff --git a/businessJob/src/main/java/com/will/businessJob/config/ReportRequest.java b/businessJob/src/main/java/com/will/businessJob/config/ReportRequest.java new file mode 100644 index 0000000..5a8c8da --- /dev/null +++ b/businessJob/src/main/java/com/will/businessJob/config/ReportRequest.java @@ -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 +) { +} diff --git a/businessJob/src/main/java/com/will/businessJob/mail/MailMessageClient.java b/businessJob/src/main/java/com/will/businessJob/mail/MailMessageClient.java index 87a5d60..0064e87 100644 --- a/businessJob/src/main/java/com/will/businessJob/mail/MailMessageClient.java +++ b/businessJob/src/main/java/com/will/businessJob/mail/MailMessageClient.java @@ -19,7 +19,7 @@ public class MailMessageClient { private final ObjectProvider mailSenderProvider; private final ObjectProvider 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; } -} \ No newline at end of file +} diff --git a/businessJob/src/main/java/com/will/businessJob/zentao/ZentaoDailyEffortJobExecutor.java b/businessJob/src/main/java/com/will/businessJob/zentao/ZentaoDailyEffortJobExecutor.java deleted file mode 100644 index 5545f46..0000000 --- a/businessJob/src/main/java/com/will/businessJob/zentao/ZentaoDailyEffortJobExecutor.java +++ /dev/null @@ -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); - } -} diff --git a/businessJob/src/main/java/com/will/businessJob/zentao/ZentaoDailyEffortReportBuilder.java b/businessJob/src/main/java/com/will/businessJob/zentao/ZentaoDailyEffortReportBuilder.java new file mode 100644 index 0000000..8954a4e --- /dev/null +++ b/businessJob/src/main/java/com/will/businessJob/zentao/ZentaoDailyEffortReportBuilder.java @@ -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 params = List.of(start.toString(), end.toString(), actualGroupName); + + List> 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 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> 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 userEfforts, String color) { + if (userEfforts.isEmpty()) { + return "- 无"; + } + return userEfforts.stream() + .map(user -> "- 姓名=%s | 账号=%s | 消耗工时=%.2f" + .formatted(color, user.realname(), user.account(), user.consumed())) + .collect(Collectors.joining("\n")); + } + + private record UserEffort(String account, String realname, double consumed) { + } +} diff --git a/businessJob/src/main/java/com/will/businessJob/zentao/ZentaoEffortReportQueryService.java b/businessJob/src/main/java/com/will/businessJob/zentao/ZentaoEffortReportQueryService.java deleted file mode 100644 index a6a1588..0000000 --- a/businessJob/src/main/java/com/will/businessJob/zentao/ZentaoEffortReportQueryService.java +++ /dev/null @@ -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 params = List.of(start.toString(), end.toString(), actualGroupName); - - List> 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 "- " + line + ""; - }) - .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 params = List.of(start.toString(), end.toString(), actualGroupName); - - List> 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 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> 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> 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(); - } -} diff --git a/businessJob/src/main/java/com/will/businessJob/zentao/ZentaoWeeklyTaskDetailJobExecutor.java b/businessJob/src/main/java/com/will/businessJob/zentao/ZentaoWeeklyTaskDetailJobExecutor.java deleted file mode 100644 index 7b65c53..0000000 --- a/businessJob/src/main/java/com/will/businessJob/zentao/ZentaoWeeklyTaskDetailJobExecutor.java +++ /dev/null @@ -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); - } -} diff --git a/businessJob/src/main/java/com/will/businessJob/zentao/ZentaoWeeklyTaskDetailReportBuilder.java b/businessJob/src/main/java/com/will/businessJob/zentao/ZentaoWeeklyTaskDetailReportBuilder.java new file mode 100644 index 0000000..bd0304b --- /dev/null +++ b/businessJob/src/main/java/com/will/businessJob/zentao/ZentaoWeeklyTaskDetailReportBuilder.java @@ -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 params = List.of(start.toString(), end.toString(), actualGroupName); + + List> 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 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> 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> details = rows.stream() + .sorted(Comparator.comparing((Map 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 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> details + ) { + } +} diff --git a/coreData/src/main/java/com/will/coreData/entity/JobConfig.java b/coreData/src/main/java/com/will/coreData/entity/JobConfig.java index 66b47ed..5ae954e 100644 --- a/coreData/src/main/java/com/will/coreData/entity/JobConfig.java +++ b/coreData/src/main/java/com/will/coreData/entity/JobConfig.java @@ -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") diff --git a/coreJob/src/main/java/com/will/coreJob/framework/xxl/JobExecutionDispatcher.java b/coreJob/src/main/java/com/will/coreJob/framework/xxl/JobExecutionDispatcher.java index d989b7e..7571b88 100644 --- a/coreJob/src/main/java/com/will/coreJob/framework/xxl/JobExecutionDispatcher.java +++ b/coreJob/src/main/java/com/will/coreJob/framework/xxl/JobExecutionDispatcher.java @@ -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 executorMap; + private final BuildReportService buildReportService; - public JobExecutionDispatcher(List executors) { + public JobExecutionDispatcher(List 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()); diff --git a/coreService/src/main/java/com/will/coreService/dto/JobConfigDTO.java b/coreService/src/main/java/com/will/coreService/dto/JobConfigDTO.java index 8aa77f8..7125a2d 100644 --- a/coreService/src/main/java/com/will/coreService/dto/JobConfigDTO.java +++ b/coreService/src/main/java/com/will/coreService/dto/JobConfigDTO.java @@ -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; diff --git a/coreService/src/main/java/com/will/coreService/service/JobConfigService.java b/coreService/src/main/java/com/will/coreService/service/JobConfigService.java index 9dbdfb0..2adaeda 100644 --- a/coreService/src/main/java/com/will/coreService/service/JobConfigService.java +++ b/coreService/src/main/java/com/will/coreService/service/JobConfigService.java @@ -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())); diff --git a/coreService/src/main/java/com/will/coreService/service/MenuManageService.java b/coreService/src/main/java/com/will/coreService/service/MenuManageService.java index 1d3a27e..934d360 100644 --- a/coreService/src/main/java/com/will/coreService/service/MenuManageService.java +++ b/coreService/src/main/java/com/will/coreService/service/MenuManageService.java @@ -210,9 +210,15 @@ public class MenuManageService { private boolean isRouteVisible(AuthMenu menu, Set 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); } diff --git a/wcsApp/src/main/java/com/will/wcsApp/startup/RbacMenuPatchRunner.java b/wcsApp/src/main/java/com/will/wcsApp/startup/RbacMenuPatchRunner.java index 9d064da..f2e6abb 100644 --- a/wcsApp/src/main/java/com/will/wcsApp/startup/RbacMenuPatchRunner.java +++ b/wcsApp/src/main/java/com/will/wcsApp/startup/RbacMenuPatchRunner.java @@ -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", diff --git a/wcsApp/src/main/resources/application-dev.yml b/wcsApp/src/main/resources/application-dev.yml index 5c4795d..6d57fb2 100644 --- a/wcsApp/src/main/resources/application-dev.yml +++ b/wcsApp/src/main/resources/application-dev.yml @@ -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} diff --git a/wcsApp/src/main/resources/application-prod.yml b/wcsApp/src/main/resources/application-prod.yml index f0557d9..263fa6d 100644 --- a/wcsApp/src/main/resources/application-prod.yml +++ b/wcsApp/src/main/resources/application-prod.yml @@ -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} diff --git a/wcsApp/src/main/resources/db/patch_dashboard_menu.sql b/wcsApp/src/main/resources/db/patch_dashboard_menu.sql new file mode 100644 index 0000000..d34885c --- /dev/null +++ b/wcsApp/src/main/resources/db/patch_dashboard_menu.sql @@ -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); diff --git a/wcsApp/src/main/resources/db/patch_job_config.sql b/wcsApp/src/main/resources/db/patch_job_config.sql index 8bc3d9b..f522e0a 100644 --- a/wcsApp/src/main/resources/db/patch_job_config.sql +++ b/wcsApp/src/main/resources/db/patch_job_config.sql @@ -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',