新增预警页面

This commit is contained in:
2025-12-14 21:54:26 +08:00
parent 0b40608761
commit f3da7da439
33 changed files with 1666 additions and 780 deletions

View File

@@ -0,0 +1,79 @@
package com.jeesite.modules.app.Job;
import com.jeesite.common.config.Global;
import com.jeesite.modules.app.dao.MailReceived;
import com.jeesite.modules.app.utils.LoggerUtils;
import com.jeesite.modules.app.utils.MailReceiveUtils;
import com.jeesite.modules.biz.entity.BizMailAccount;
import com.jeesite.modules.biz.entity.BizMailAttachments;
import com.jeesite.modules.biz.entity.BizMailReceived;
import com.jeesite.modules.biz.service.BizMailAccountService;
import com.jeesite.modules.biz.service.BizMailAttachmentsService;
import com.jeesite.modules.biz.service.BizMailReceivedService;
import jakarta.annotation.Resource;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Controller;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
@Controller
public class mailJob {
@Resource
private BizMailAccountService bizMailAccountService;
@Resource
private BizMailReceivedService receivedService;
@Resource
private BizMailAttachmentsService attachmentsService;
@Resource(name = "hostMonitorExecutor")
private ThreadPoolTaskExecutor hostMonitorExecutor;
private String MAIL_PATH = "/ogsapp/mail";
private static final LoggerUtils logger = LoggerUtils.getInstance();
private static final boolean CRON_JOB = Boolean.parseBoolean(Global.getConfig("biz.cron.MailJob", "false"));
@Scheduled(cron = "10 0/15 * * * ?")
public void getMailReceived() {
if (CRON_JOB) {
List<BizMailAccount> accounts = bizMailAccountService.findList(new BizMailAccount());
List<CompletableFuture<Void>> futures = new ArrayList<>(accounts.size());
for (BizMailAccount account : accounts) {
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
try {
List<MailReceived> receivedList = MailReceiveUtils.receiveUnreadMails(account, MAIL_PATH);
for (MailReceived mailReceived : receivedList) {
BizMailReceived received = mailReceived.getReceived();
List<BizMailAttachments> attachments = mailReceived.getAttachments();
for (BizMailAttachments mailAttachments : attachments) {
attachmentsService.insert(mailAttachments);
}
receivedService.save(received);
}
} catch (Exception e) {
logger.error(e.getMessage());
}
}, hostMonitorExecutor); // 指定使用配置的线程池
futures.add(future);
}
try {
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.get(60, TimeUnit.SECONDS); // 超时时间可根据业务调整
} catch (Exception e) {
logger.error(e.getMessage());
}
}
}
}

View File

@@ -0,0 +1,162 @@
package com.jeesite.modules.app.utils;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.FileCopyUtils;
import java.io.*;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* 文件下载工具类
* 支持两种方式:
* 1. 基于HttpServletResponse直接输出适用于传统Servlet/SSM
* 2. 返回ResponseEntity<byte[]>适用于SpringBoot
* 修复点:解决文件名前后加下划线、中文乱码、浏览器兼容性问题
*/
public class FileDownloadUtils {
/**
* 私有构造方法,禁止实例化
*/
private FileDownloadUtils() {
throw new UnsupportedOperationException("工具类禁止实例化");
}
/**
* 方式1通过HttpServletResponse下载文件推荐SSM/Servlet使用
*
* @param orgFileName 文件完整路径例如D:/files/test.pdf
* @param fileName 下载时显示的文件名(例如:测试文件.pdf
* @param response HttpServletResponse对象
* @throws Exception 文件操作异常
*/
public static void downloadFile(String orgFileName, String fileName, HttpServletResponse response) throws Exception {
// 1. 构建完整文件路径
Path fullFilePath = Paths.get(orgFileName);
File file = fullFilePath.toFile();
// 2. 校验文件合法性
validateFile(file);
// 3. 清理并编码文件名(避免特殊字符导致的解析异常)
String encodedFileName = encodeFileName(fileName);
// 4. 设置响应头(核心修复:解决下划线/乱码问题)
setDownloadResponseHeader(response, encodedFileName, file.length());
// 5. 读取文件并写入响应输出流try-with-resources自动关闭流
try (InputStream inputStream = new BufferedInputStream(Files.newInputStream(fullFilePath));
OutputStream outputStream = new BufferedOutputStream(response.getOutputStream())) {
FileCopyUtils.copy(inputStream, outputStream);
outputStream.flush();
} catch (IOException e) {
throw new IOException("文件下载失败:" + e.getMessage(), e);
}
}
/**
* 方式2返回ResponseEntity<byte[]>推荐SpringBoot使用
*
* @param orgFileName 文件完整路径例如D:/files/test.pdf
* @param fileName 下载时显示的文件名(例如:测试文件.pdf
* @return ResponseEntity<byte [ ]> 下载响应实体
* @throws IOException 文件操作异常
*/
public static ResponseEntity<byte[]> downloadFile(String orgFileName, String fileName) throws IOException {
// 1. 构建完整文件路径
Path fullFilePath = Paths.get(orgFileName);
File file = fullFilePath.toFile();
// 2. 校验文件合法性
validateFile(file);
// 3. 读取文件字节数组
byte[] fileBytes = Files.readAllBytes(fullFilePath);
// 4. 清理并编码文件名
String encodedFileName = encodeFileName(fileName);
HttpHeaders headers = new HttpHeaders();
// 核心修复使用RFC 5987标准移除多余双引号避免下划线问题
headers.set(HttpHeaders.CONTENT_DISPOSITION,
String.format("attachment; filename=%s; filename*=UTF-8''%s",
encodedFileName, encodedFileName));
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
headers.setContentLength(fileBytes.length);
// 禁止缓存
headers.setCacheControl("no-cache, no-store, must-revalidate");
headers.setPragma("no-cache");
headers.setExpires(0);
// 6. 返回响应实体
return new ResponseEntity<>(fileBytes, headers, HttpStatus.OK);
}
/**
* 校验文件合法性(存在性、是否为文件、读取权限)
*
* @param file 待校验文件
* @throws FileNotFoundException 文件不合法异常
*/
private static void validateFile(File file) throws FileNotFoundException {
if (!file.exists()) {
throw new FileNotFoundException("文件不存在:" + file.getAbsolutePath());
}
if (file.isDirectory()) {
throw new FileNotFoundException("路径指向目录,不是文件:" + file.getAbsolutePath());
}
if (!file.canRead()) {
throw new FileNotFoundException("文件无读取权限:" + file.getAbsolutePath());
}
}
/**
* 设置下载响应头(核心修复:解决下划线/乱码/浏览器兼容问题)
*
* @param response HttpServletResponse
* @param encodedFileName 编码后的文件名
* @param fileSize 文件大小
*/
private static void setDownloadResponseHeader(HttpServletResponse response, String encodedFileName, long fileSize) {
// 设置响应内容类型为二进制流
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
// 设置字符编码
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
// 设置内容长度
response.setContentLengthLong(fileSize);
// 核心修复使用RFC 5987标准移除多余双引号避免浏览器解析出下划线
response.setHeader(HttpHeaders.CONTENT_DISPOSITION,
String.format("attachment; filename=%s; filename*=UTF-8''%s",
encodedFileName, encodedFileName));
// 禁止缓存
response.setHeader(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate");
response.setHeader(HttpHeaders.PRAGMA, "no-cache");
response.setDateHeader(HttpHeaders.EXPIRES, 0);
}
/**
* 编码文件名(解决中文乱码+空格转+号导致的下划线问题)
*
* @param fileName 清理后的文件名
* @return 编码后的文件名
*/
private static String encodeFileName(String fileName) {
try {
// 修复:将空格编码为%20而非+,避免浏览器解析为下划线
return URLEncoder.encode(fileName, StandardCharsets.UTF_8.name())
.replace("+", "%20");
} catch (UnsupportedEncodingException e) {
// 理论上UTF-8不会抛出此异常兜底返回原文件名
return fileName;
}
}
}

View File

@@ -598,11 +598,10 @@ public class MailReceiveUtils {
attachment.setFileMd5(md5);
attachment.setDownloadCount(0);
attachment.setIsCompressed(isCompressedFile(originalFileName) ? "1" : "0");
attachment.setIsEncrypted("N");
attachment.setIsEncrypted("0");
attachment.setDownloadStartTime(new Date(attachDownloadStartTime)); // 附件下载开始时间
attachment.setDownloadEndTime(new Date(attachDownloadEndTime)); // 附件下载结束时间
attachment.setDownloadCostTime(costTime); // 附件下载耗时(毫秒)
return attachment;
}

View File

@@ -0,0 +1,164 @@
package com.jeesite.modules.app.utils;
import com.jeesite.modules.biz.entity.BizMailAccount;
import com.jeesite.modules.biz.entity.BizMailSent;
import com.jeesite.modules.file.entity.FileUpload;
import javax.mail.*;
import javax.mail.internet.*;
import java.io.File;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class MailSendUtils {
private static final ExecutorService MAIL_EXECUTOR = Executors.newFixedThreadPool(5);
private static String FILE_PATH = "/ogsapp/files";
/**
* 同步发送HTML格式邮件
*
* @param mailAccount 邮件账户配置
* @param mailSent 邮件发送内容信息
* @return BizMailSent 填充发送结果后的对象
*/
public static BizMailSent sendHtmlMail(BizMailAccount mailAccount, BizMailSent mailSent, List<FileUpload> fileUploads) {
// 初始化发送结果对象
mailSent.setId(mailSent.getId());
mailSent.setCreateTime(new Date());
mailSent.setSendStatus("0"); // 默认失败状态
// 1. 参数校验
if (!validateParams(mailAccount, mailSent)) {
mailSent.setErrorMsg("参数校验失败:邮件账户或发送内容不完整");
return mailSent;
}
Properties props = new Properties();
props.put("mail.smtp.host", mailAccount.getHost());
props.put("mail.smtp.port", mailAccount.getSmtpPort().toString());
props.put("mail.smtp.auth", "true"); // 开启认证
props.put("mail.smtp.socketFactory.port", mailAccount.getSmtpPort().toString());
// SSL配置
if (mailAccount.getSslEnable().equals("true")) {
props.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
props.put("mail.smtp.starttls.enable", "true");
}
// 3. 创建认证器
Authenticator authenticator = new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(mailAccount.getUsername(), mailAccount.getPassword());
}
};
// 4. 创建邮件会话
Session session = Session.getInstance(props, authenticator);
session.setDebug(false); // 生产环境关闭调试
try {
// 5. 构建MIME邮件消息
MimeMessage message = new MimeMessage(session);
// 设置发件人
message.setFrom(new InternetAddress(mailAccount.getFromAddress()));
// 设置收件人
if (mailSent.getToAddresses() != null && !mailSent.getToAddresses().isEmpty()) {
String[] toArray = mailSent.getToAddresses().split(",");
InternetAddress[] toAddresses = new InternetAddress[toArray.length];
for (int i = 0; i < toArray.length; i++) {
toAddresses[i] = new InternetAddress(toArray[i].trim());
}
message.setRecipients(Message.RecipientType.TO, toAddresses);
}
// 设置抄送人
if (mailSent.getCcAddresses() != null && !mailSent.getCcAddresses().isEmpty()) {
String[] ccArray = mailSent.getCcAddresses().split(",");
InternetAddress[] ccAddresses = new InternetAddress[ccArray.length];
for (int i = 0; i < ccArray.length; i++) {
ccAddresses[i] = new InternetAddress(ccArray[i].trim());
}
message.setRecipients(Message.RecipientType.CC, ccAddresses);
}
// 设置邮件主题
message.setSubject(mailSent.getSubject(), "UTF-8");
// 构建邮件内容支持HTML和附件
MimeMultipart multipart = new MimeMultipart("mixed");
// HTML内容部分
MimeBodyPart contentPart = new MimeBodyPart();
contentPart.setContent(mailSent.getContent(), "text/html;charset=UTF-8");
multipart.addBodyPart(contentPart);
if (fileUploads.size() > 0) {
mailSent.setHasAttachment("1");
for (FileUpload upload : fileUploads) {
MimeBodyPart attachmentPart = new MimeBodyPart();
File file = new File(FILE_PATH + upload.getFileUrl());
attachmentPart.attachFile(file);
attachmentPart.setFileName(MimeUtility.encodeText(file.getName(), "UTF-8", "B"));
multipart.addBodyPart(attachmentPart);
}
}
// 设置邮件内容
message.setContent(multipart);
// 设置发送时间
message.setSentDate(new Date());
// 6. 发送邮件
Transport.send(message);
// 7. 更新发送结果
mailSent.setSendTime(new Date());
mailSent.setSendStatus("1");
mailSent.setErrorMsg("");
mailSent.setMessageId(message.getMessageID()); // 邮件服务器消息ID
} catch (Exception e) {
// 捕获所有异常,记录错误信息
mailSent.setErrorMsg("邮件发送失败:" + e.getMessage());
mailSent.setSendTime(new Date());
// 打印异常栈(生产环境建议日志记录)
e.printStackTrace();
}
return mailSent;
}
/**
* 异步发送HTML格式邮件
*
* @param mailAccount 邮件账户配置
* @param mailSent 邮件发送内容信息
*/
public static void sendHtmlMailAsync(BizMailAccount mailAccount, BizMailSent mailSent, List<FileUpload> fileUploads) {
MAIL_EXECUTOR.submit(() -> sendHtmlMail(mailAccount, mailSent, fileUploads));
}
/**
* 参数校验
*
* @param mailAccount 邮件账户
* @param mailSent 邮件内容
* @return 校验结果
*/
private static boolean validateParams(BizMailAccount mailAccount, BizMailSent mailSent) {
// 校验账户必填项
if (mailAccount == null || mailAccount.getHost() == null || mailAccount.getSmtpPort() == null
|| mailAccount.getUsername() == null || mailAccount.getPassword() == null
|| mailAccount.getFromAddress() == null) {
return false;
}
// 校验邮件内容必填项
return mailSent != null && mailSent.getToAddresses() != null && !mailSent.getToAddresses().isEmpty()
&& mailSent.getSubject() != null && mailSent.getContent() != null;
}
/**
* 关闭线程池(应用关闭时调用)
*/
public static void shutdownExecutor() {
MAIL_EXECUTOR.shutdown();
}
}