初始化前端目录

This commit is contained in:
2025-09-22 22:16:27 +08:00
parent 52e94288df
commit 69d559a920
23 changed files with 854 additions and 741 deletions

View File

@@ -13,4 +13,8 @@ import com.baomidou.mybatisplus.extension.service.IService;
*/
public interface MailAccountService extends IService<MailAccount> {
/**
* 获取启用的邮件账户(默认取第一个,可扩展为多账户轮询)
*/
MailAccount getEnabledAccount();
}

View File

@@ -13,4 +13,5 @@ import com.baomidou.mybatisplus.extension.service.IService;
*/
public interface MailReceivedService extends IService<MailReceived> {
void receiveUnreadMail();
}

View File

@@ -2,6 +2,7 @@ package com.mini.capi.biz.service;
import com.mini.capi.biz.domain.MailSent;
import com.baomidou.mybatisplus.extension.service.IService;
import org.springframework.web.multipart.MultipartFile;
/**
* <p>
@@ -13,4 +14,5 @@ import com.baomidou.mybatisplus.extension.service.IService;
*/
public interface MailSentService extends IService<MailSent> {
void sendMail(String[] toAddresses, String[] ccAddresses, String subject, String content, MultipartFile[] attachments);
}

View File

@@ -1,5 +1,6 @@
package com.mini.capi.biz.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.mini.capi.biz.domain.MailAccount;
import com.mini.capi.biz.mapper.MailAccountMapper;
import com.mini.capi.biz.service.MailAccountService;
@@ -17,4 +18,14 @@ import org.springframework.stereotype.Service;
@Service
public class MailAccountServiceImpl extends ServiceImpl<MailAccountMapper, MailAccount> implements MailAccountService {
/**
* 获取启用的账户status=1按创建时间倒序取第一个
*/
@Override
public MailAccount getEnabledAccount() {
return getOne(new LambdaQueryWrapper<MailAccount>()
.eq(MailAccount::getStatus, Boolean.TRUE) // 1-启用
.orderByDesc(MailAccount::getCreateTime)
.last("limit 1"));
}
}

View File

@@ -1,11 +1,30 @@
package com.mini.capi.biz.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.mini.capi.config.JavaMailConfig;
import com.mini.capi.biz.domain.MailAccount;
import com.mini.capi.biz.domain.MailAttachment;
import com.mini.capi.biz.domain.MailReceived;
import com.mini.capi.biz.mapper.MailReceivedMapper;
import com.mini.capi.biz.service.MailAccountService;
import com.mini.capi.biz.service.MailAttachmentService;
import com.mini.capi.biz.service.MailReceivedService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.mini.capi.utils.FileUtils;
import com.mini.capi.utils.MailParseUtils;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import javax.mail.*;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeUtility;
import javax.mail.search.FlagTerm;
import java.io.File;
import java.io.InputStream;
import java.time.LocalDateTime;
import java.util.List;
import java.util.concurrent.ExecutorService;
/**
* <p>
* 接收邮件表 服务实现类
@@ -17,4 +36,187 @@ import org.springframework.stereotype.Service;
@Service
public class MailReceivedServiceImpl extends ServiceImpl<MailReceivedMapper, MailReceived> implements MailReceivedService {
@Resource
private MailAccountService mailAccountService;
@Resource
private MailAttachmentService mailAttachmentService;
@Qualifier("attachmentThreadPool")
@Resource
private ExecutorService attachmentThreadPool;
/**
* 接收INBOX未读邮件同步保存邮件基本信息异步保存附件
*/
@Override
public void receiveUnreadMail() {
// 1. 获取启用的邮件账户
MailAccount account = mailAccountService.getEnabledAccount();
if (account == null) {
throw new RuntimeException("无启用的邮件账户,无法接收邮件");
}
// 2. 创建IMAP Session并连接邮箱
Session session = JavaMailConfig.createImapSession(account);
Store store = null;
Folder inbox = null;
try {
store = session.getStore("imap");
store.connect(account.getHost(), account.getUsername(), account.getPassword());
// 3. 打开INBOX文件夹READ_WRITE模式支持设置已读
inbox = store.getFolder("INBOX");
inbox.open(Folder.READ_WRITE);
// 4. 获取未读邮件Flags.Flag.SEEN=false
Message[] unreadMessages = inbox.search(new FlagTerm(new Flags(Flags.Flag.SEEN), false));
if (unreadMessages == null || unreadMessages.length == 0) {
System.out.print("INBOX无未读邮件");
return;
}
// 5. 遍历未读邮件,处理基本信息和附件
for (Message message : unreadMessages) {
MimeMessage mimeMsg = (MimeMessage) message;
handleSingleMail(mimeMsg, account, inbox);
}
} catch (Exception e) {
throw new RuntimeException("接收邮件失败", e);
} finally {
// 6. 关闭资源
try {
if (inbox != null && inbox.isOpen()) {
inbox.close(false); // false不删除邮件
}
if (store != null && store.isConnected()) {
store.close();
}
} catch (MessagingException e) {
System.out.print(e.getMessage());
}
}
}
/**
* 处理单封邮件:同步存基本信息,异步存附件
*/
private void handleSingleMail(MimeMessage message, MailAccount account, Folder inbox) throws Exception {
// -------------------------- 同步保存邮件基本信息 --------------------------
MailReceived mailReceived = new MailReceived();
// 基础字段
mailReceived.setAccountId(account.getId());
mailReceived.setMessageId(message.getMessageID());
mailReceived.setFromAddress(MailParseUtils.parseFrom(message));
mailReceived.setToAddresses(MailParseUtils.parseTo(message));
mailReceived.setCcAddresses(MailParseUtils.parseCc(message));
mailReceived.setSubject(MimeUtility.decodeText(message.getSubject())); // 处理中文主题乱码
mailReceived.setContent(MailParseUtils.parseContent(message));
// 时间字段(邮件发送时间 -> 转为LocalDateTime
mailReceived.setSendTime(LocalDateTime.ofInstant(message.getSentDate().toInstant(), java.time.ZoneId.systemDefault()));
mailReceived.setReceiveTime(LocalDateTime.now());
// 状态字段
mailReceived.setIsRead(Boolean.FALSE); // 初始未读,附件处理后设为已读
List<MailParseUtils.AttachmentInfo> attachments = MailParseUtils.extractAttachments(message);
mailReceived.setHasAttachment(!attachments.isEmpty() ? Boolean.TRUE : Boolean.FALSE);
// 公共字段
mailReceived.setCreateTime(LocalDateTime.now());
mailReceived.setUpdateTime(LocalDateTime.now());
mailReceived.setFTenantId(account.getFTenantId()); // 继承账户的租户ID
mailReceived.setFFlowId(account.getFFlowId());
mailReceived.setFFlowTaskId(account.getFFlowTaskId());
mailReceived.setFFlowState(account.getFFlowState());
// 保存到接收表获取主键ID用于附件关联
save(mailReceived);
Long receivedId = mailReceived.getId();
if (receivedId == null) {
throw new RuntimeException("保存接收邮件失败主键ID为空");
}
// -------------------------- 异步保存附件(多线程) --------------------------
if (!attachments.isEmpty()) {
attachmentThreadPool.submit(() -> {
try {
saveAttachments(attachments, receivedId, account);
// 附件保存完成后,设置邮件为已读
message.setFlag(Flags.Flag.SEEN, true);
// 更新接收表的已读状态
MailReceived updateRead = new MailReceived();
updateRead.setId(receivedId);
updateRead.setIsRead(Boolean.TRUE);
updateById(updateRead);
} catch (Exception e) {
System.out.print(e.getMessage());
}
});
} else {
// 无附件,直接设为已读
message.setFlag(Flags.Flag.SEEN, true);
MailReceived updateRead = new MailReceived();
updateRead.setId(receivedId);
updateRead.setIsRead(Boolean.TRUE);
updateById(updateRead);
}
}
/**
* 保存附件到本地目录和数据库
*
* @param attachments 附件信息列表
* @param refId 关联的接收邮件ID
* @param account 邮件账户(用于租户等公共字段)
*/
private void saveAttachments(List<MailParseUtils.AttachmentInfo> attachments, Long refId, MailAccount account) {
for (MailParseUtils.AttachmentInfo attachment : attachments) {
try (InputStream inputStream = attachment.getInputStream()) { // 自动关闭流
// 1. 生成附件存储信息
String originalFileName = attachment.getOriginalFileName();
String randomFileName = FileUtils.generate32RandomFileName(originalFileName);
String storagePath = FileUtils.ATTACHMENT_ROOT_DIR + "/" + randomFileName; // 完整存储路径
String fileNo = randomFileName.substring(0, 16); // 文件编号取32位随机名前16位
String directory = FileUtils.ATTACHMENT_ROOT_DIR; // 目录(根目录)
// 2. 保存附件到本地目录
FileUtils.saveFile(inputStream, storagePath);
File savedFile = new File(storagePath);
Long fileSize = FileUtils.getFileSize(savedFile);
// 3. 保存附件信息到数据库类型1收件附件
MailAttachment mailAttachment = new MailAttachment();
mailAttachment.setFileNo(fileNo);
mailAttachment.setDirectory(directory);
mailAttachment.setOriginalFileName(originalFileName);
mailAttachment.setStoragePath(storagePath);
mailAttachment.setFileSize(fileSize);
mailAttachment.setType(Boolean.TRUE); // 1-收件附件
mailAttachment.setRefId(refId); // 关联接收邮件ID
mailAttachment.setContentType(getContentType(originalFileName)); // 简单判断文件类型
mailAttachment.setDownloadCount(0); // 初始下载次数0
// 公共字段
mailAttachment.setCreateTime(LocalDateTime.now());
mailAttachment.setUpdateTime(LocalDateTime.now());
mailAttachment.setFTenantId(account.getFTenantId());
mailAttachment.setFFlowId(account.getFFlowId());
mailAttachment.setFFlowTaskId(account.getFFlowTaskId());
mailAttachment.setFFlowState(account.getFFlowState());
mailAttachmentService.save(mailAttachment);
} catch (Exception e) {
System.out.print(e.getMessage());
}
}
}
/**
* 简单判断文件类型(可扩展为更精准的判断)
*/
private String getContentType(String fileName) {
if (fileName.endsWith(".txt")) return "text/plain";
if (fileName.endsWith(".pdf")) return "application/pdf";
if (fileName.endsWith(".doc") || fileName.endsWith(".docx")) return "application/msword";
if (fileName.endsWith(".xls") || fileName.endsWith(".xlsx")) return "application/vnd.ms-excel";
if (fileName.endsWith(".jpg") || fileName.endsWith(".jpeg")) return "image/jpeg";
if (fileName.endsWith(".png")) return "image/png";
return "application/octet-stream"; // 默认二进制流
}
}

View File

@@ -1,10 +1,25 @@
package com.mini.capi.biz.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.mini.capi.config.JavaMailConfig;
import com.mini.capi.biz.domain.MailAccount;
import com.mini.capi.biz.domain.MailAttachment;
import com.mini.capi.biz.domain.MailSent;
import com.mini.capi.biz.mapper.MailSentMapper;
import com.mini.capi.biz.service.MailAccountService;
import com.mini.capi.biz.service.MailAttachmentService;
import com.mini.capi.biz.service.MailSentService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.mini.capi.utils.FileUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import javax.activation.DataHandler;
import javax.mail.*;
import javax.mail.internet.*;
import java.time.LocalDateTime;
import java.util.Arrays;
/**
* <p>
@@ -17,4 +32,221 @@ import org.springframework.stereotype.Service;
@Service
public class MailSentServiceImpl extends ServiceImpl<MailSentMapper, MailSent> implements MailSentService {
@Autowired
private MailAccountService mailAccountService;
@Autowired
private MailAttachmentService mailAttachmentService;
/**
* 发送邮件(支持多收件人、多抄送、多附件)
*
* @param toAddresses 收件人列表(数组)
* @param ccAddresses 抄送列表数组可为null
* @param subject 邮件主题
* @param content 邮件内容支持HTML
* @param attachments 附件列表可为null
*/
@Override
public void sendMail(String[] toAddresses, String[] ccAddresses, String subject, String content, MultipartFile[] attachments) {
// 1. 校验参数
if (toAddresses == null || toAddresses.length == 0) {
throw new IllegalArgumentException("收件人不能为空");
}
if (subject == null || subject.trim().isEmpty()) {
throw new IllegalArgumentException("邮件主题不能为空");
}
// 2. 获取启用的邮件账户
MailAccount account = mailAccountService.getEnabledAccount();
if (account == null) {
throw new RuntimeException("无启用的邮件账户,无法发送邮件");
}
// 3. 创建SMTP Session和MimeMessage
Session session = JavaMailConfig.createSmtpSession(account);
MimeMessage message = new MimeMessage(session);
Transport transport = null;
try {
// -------------------------- 构建邮件内容 --------------------------
// 3.1 设置发件人
message.setFrom(new InternetAddress(account.getFromAddress()));
// 3.2 设置收件人
InternetAddress[] toAddrs = Arrays.stream(toAddresses)
.map(addr -> {
try {
return new InternetAddress(addr);
} catch (AddressException e) {
throw new RuntimeException("收件人地址格式错误:" + addr, e);
}
})
.toArray(InternetAddress[]::new);
message.setRecipients(Message.RecipientType.TO, toAddrs);
// 3.3 设置抄送(可选)
if (ccAddresses != null && ccAddresses.length > 0) {
InternetAddress[] ccAddrs = Arrays.stream(ccAddresses)
.map(addr -> {
try {
return new InternetAddress(addr);
} catch (AddressException e) {
throw new RuntimeException("抄送地址格式错误:" + addr, e);
}
})
.toArray(InternetAddress[]::new);
message.setRecipients(Message.RecipientType.CC, ccAddrs);
}
// 3.4 设置主题(处理中文)
message.setSubject(subject, "UTF-8");
// 3.5 构建邮件内容(支持附件)
Multipart multipart = new MimeMultipart();
// 文本部分
MimeBodyPart textPart = new MimeBodyPart();
textPart.setContent(content, "text/html;charset=UTF-8"); // 支持HTML内容
multipart.addBodyPart(textPart);
// 3.6 添加附件(可选)
boolean hasAttachment = attachments != null && attachments.length > 0;
if (hasAttachment) {
for (MultipartFile file : attachments) {
if (file.isEmpty()) {
continue;
}
MimeBodyPart attachmentPart = new MimeBodyPart();
// 附件流
attachmentPart.setDataHandler(new DataHandler(file.getInputStream(), file.getContentType()));
// 附件名(处理中文乱码)
attachmentPart.setFileName(MimeUtility.encodeText(file.getOriginalFilename()));
multipart.addBodyPart(attachmentPart);
}
}
// 3.7 设置邮件内容
message.setContent(multipart);
// 设置发送时间
message.setSentDate(new java.util.Date());
message.saveChanges();
// -------------------------- 发送邮件 --------------------------
transport = session.getTransport("smtp");
transport.connect(account.getHost(), account.getUsername(), account.getPassword());
transport.sendMessage(message, message.getAllRecipients());
// -------------------------- 保存发送记录到数据库 --------------------------
saveSentRecord(message, account, toAddresses, ccAddresses, subject, content, hasAttachment, null);
// -------------------------- 保存附件记录到数据库类型2发件附件 --------------------------
if (hasAttachment) {
saveSendAttachments(attachments, getSentIdByMessageId(message.getMessageID()), account);
}
} catch (Exception e) {
// 发送失败,保存失败记录
saveSentRecord(null, account, toAddresses, ccAddresses, subject, content,
attachments != null && attachments.length > 0, e.getMessage());
throw new RuntimeException("发送邮件失败", e);
} finally {
// 关闭资源
try {
if (transport != null && transport.isConnected()) {
transport.close();
}
} catch (MessagingException e) {
log.error("关闭SMTP连接失败", e);
}
}
}
/**
* 保存发送记录到biz_mail_sent
*/
private void saveSentRecord(MimeMessage message, MailAccount account, String[] toAddresses,
String[] ccAddresses, String subject, String content,
boolean hasAttachment, String errorMsg) {
try {
MailSent mailSent = new MailSent();
// 基础字段
mailSent.setMessageId(message != null ? message.getMessageID() : null);
mailSent.setAccountId(account.getId());
mailSent.setFromAddress(account.getFromAddress());
mailSent.setToAddresses(String.join(",", toAddresses));
mailSent.setCcAddresses(ccAddresses != null ? String.join(",", ccAddresses) : "");
mailSent.setSubject(subject);
mailSent.setContent(content);
mailSent.setSendTime(LocalDateTime.now());
// 状态字段
mailSent.setSendStatus(message != null ? Boolean.TRUE : Boolean.FALSE); // 1-成功0-失败
mailSent.setErrorMsg(errorMsg); // 失败时记录错误信息
mailSent.setHasAttachment(hasAttachment ? Boolean.TRUE : Boolean.FALSE);
// 公共字段
mailSent.setCreateTime(LocalDateTime.now());
mailSent.setUpdateTime(LocalDateTime.now());
mailSent.setFTenantId(account.getFTenantId());
mailSent.setFFlowId(account.getFFlowId());
mailSent.setFFlowTaskId(account.getFFlowTaskId());
mailSent.setFFlowState(account.getFFlowState());
save(mailSent);
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
/**
* 保存发件附件到本地和数据库类型2发件附件
*/
private void saveSendAttachments(MultipartFile[] multipartFiles, Long sentId, MailAccount account) {
for (MultipartFile file : multipartFiles) {
if (file.isEmpty()) {
continue;
}
try {
// 1. 生成附件存储信息
String originalFileName = file.getOriginalFilename();
String randomFileName = FileUtils.generate32RandomFileName(originalFileName);
String storagePath = FileUtils.ATTACHMENT_ROOT_DIR + "/" + randomFileName;
String fileNo = randomFileName.substring(0, 16);
String directory = FileUtils.ATTACHMENT_ROOT_DIR;
// 2. 保存附件到本地
FileUtils.saveFile(file.getInputStream(), storagePath);
Long fileSize = file.getSize();
// 3. 保存到附件表类型2发件附件
MailAttachment mailAttachment = new MailAttachment();
mailAttachment.setFileNo(fileNo);
mailAttachment.setDirectory(directory);
mailAttachment.setOriginalFileName(originalFileName);
mailAttachment.setStoragePath(storagePath);
mailAttachment.setFileSize(fileSize);
mailAttachment.setType(Boolean.FALSE); // 2-发件附件注意实体类type是Boolean用false对应2
mailAttachment.setRefId(sentId); // 关联发送邮件ID
mailAttachment.setContentType(file.getContentType());
mailAttachment.setDownloadCount(0);
// 公共字段
mailAttachment.setCreateTime(LocalDateTime.now());
mailAttachment.setUpdateTime(LocalDateTime.now());
mailAttachment.setFTenantId(account.getFTenantId());
mailAttachment.setFFlowId(account.getFFlowId());
mailAttachment.setFFlowTaskId(account.getFFlowTaskId());
mailAttachment.setFFlowState(account.getFFlowState());
mailAttachmentService.save(mailAttachment);
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
/**
* 根据messageId查询发送记录ID用于附件关联
*/
private Long getSentIdByMessageId(String messageId) {
MailSent mailSent = getOne(new LambdaQueryWrapper<MailSent>()
.eq(MailSent::getMessageId, messageId)
.last("limit 1"));
return mailSent != null ? mailSent.getId() : null;
}
}