新增预警页面
This commit is contained in:
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user