feat(todo): 新增待办事项模块及邮件服务优化

- 新增待办事项实体、接口、Mapper和服务实现,支持分页查询和批量标记已读
- 添加待办事项查询条件支持编号、标题、来源类型和已读状态过滤
- 实现待办事项相关的Controller和业务逻辑服务,提供REST接口
- 优化邮件服务,增加邮件配置缓存和Session缓存,提升性能
- 支持发送多收件人、抄送和密送的邮件功能,且新增异步发送邮件方法
- 增强邮件发送异常处理及日志记录,邮件主题和内容支持UTF-8编码格式
- 添加刷新邮件配置接口,支持动态更新邮件参数配置
This commit is contained in:
曹鹏飞 2026-04-14 19:41:54 +08:00
parent 6a691e4984
commit c010fd3bc7
10 changed files with 562 additions and 27 deletions

View File

@ -0,0 +1,43 @@
package com.nflg.qms.admin.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.nflg.qms.admin.service.TodoItemControllerService;
import com.nflg.wms.common.pojo.ApiResult;
import com.nflg.wms.common.pojo.PageData;
import com.nflg.wms.common.pojo.qo.QmsTodoItemSearchQO;
import com.nflg.wms.common.pojo.vo.QmsTodoItemVO;
import com.nflg.wms.starter.BaseController;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 待办事项
*/
@RestController
@RequestMapping("/todo-item")
public class TodoItemController extends BaseController {
@Resource
private TodoItemControllerService todoItemControllerService;
/**
* 分页查询待办事项列表
*/
@PostMapping("search")
public ApiResult<PageData<QmsTodoItemVO>> search(@Valid @RequestBody QmsTodoItemSearchQO request) {
IPage<QmsTodoItemVO> page = todoItemControllerService.search(request);
return ApiResult.success(page);
}
/**
* 批量标记已读
*/
@PostMapping("mark-as-read")
public ApiResult<Void> markAsRead(@RequestBody List<Long> ids) {
todoItemControllerService.markAsRead(ids);
return ApiResult.success();
}
}

View File

@ -0,0 +1,34 @@
package com.nflg.qms.admin.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.nflg.wms.common.pojo.qo.QmsTodoItemSearchQO;
import com.nflg.wms.common.pojo.vo.QmsTodoItemVO;
import com.nflg.wms.repository.service.IQmsTodoItemService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 待办事项业务逻辑
*/
@Component
public class TodoItemControllerService {
@Resource
private IQmsTodoItemService todoItemService;
/**
* 分页查询待办事项列表
*/
public IPage<QmsTodoItemVO> search(QmsTodoItemSearchQO request) {
return todoItemService.search(request);
}
/**
* 批量标记已读
*/
public void markAsRead(List<Long> ids) {
todoItemService.markAsRead(ids);
}
}

View File

@ -11,11 +11,14 @@ import jakarta.mail.*;
import jakarta.mail.internet.InternetAddress;
import jakarta.mail.internet.MimeMessage;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import java.io.UnsupportedEncodingException;
import java.util.List;
import java.util.Objects;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
@Component
@ -24,45 +27,180 @@ public class EmailService {
@Resource
private IParamConfigService paramConfigService;
public void sendEmail(String to, String subject, String content) throws MessagingException {
log.info("准备发送邮件,to:{},subject:{},content:{}", to, subject, content);
List<ParamConfig> configs = paramConfigService.lambdaQuery().eq(ParamConfig::getGroupName, "EmailSet").list();
VUtil.trueThrowBusinessError(CollectionUtil.isNotEmpty(configs)).throwMessage("未配置邮件参数");
// EmailConfigDTO emailConfig = JSONUtil.toBean(config.getValue(), EmailConfigDTO.class);
EmailConfigDTO emailConfig = new EmailConfigDTO();
ParamConfig cfg = configs.stream().filter(c -> StrUtil.equals("host", c.getCode())).findFirst().orElse(null);
VUtil.trueThrowBusinessError(Objects.isNull(cfg)).throwMessage("主机名未配置");
emailConfig.setHost(cfg.getValue());
cfg = configs.stream().filter(c -> StrUtil.equals("port", c.getCode())).findFirst().orElse(null);
VUtil.trueThrowBusinessError(Objects.isNull(cfg)).throwMessage("端口号未配置");
emailConfig.setPort(Integer.valueOf(cfg.getValue()));
cfg = configs.stream().filter(c -> StrUtil.equals("username", c.getCode())).findFirst().orElse(null);
VUtil.trueThrowBusinessError(Objects.isNull(cfg)).throwMessage("用户名未配置");
emailConfig.setUsername(cfg.getValue());
cfg = configs.stream().filter(c -> StrUtil.equals("password", c.getCode())).findFirst().orElse(null);
VUtil.trueThrowBusinessError(Objects.isNull(cfg)).throwMessage("密码未配置");
emailConfig.setPassword(cfg.getValue());
/**
* 邮件配置缓存
*/
private volatile EmailConfigDTO cachedEmailConfig;
/**
* Session缓存
*/
private final ConcurrentHashMap<String, Session> sessionCache = new ConcurrentHashMap<>();
/**
* 发送邮件单个收件人
*
* @param to 收件人邮箱
* @param subject 邮件主题
* @param content 邮件内容HTML格式
*/
public void sendEmail(String to, String subject, String content) {
sendEmail(to, null, null, subject, content);
}
/**
* 发送邮件支持多收件人抄送密送
*
* @param to 收件人邮箱多个用逗号分隔
* @param cc 抄送人邮箱多个用逗号分隔可为null
* @param bcc 密送人邮箱多个用逗号分隔可为null
* @param subject 邮件主题
* @param content 邮件内容HTML格式
*/
public void sendEmail(String to, String cc, String bcc, String subject, String content) {
try {
log.info("准备发送邮件, to:{}, cc:{}, bcc:{}, subject:{}", to, cc, bcc, subject);
EmailConfigDTO emailConfig = getEmailConfig();
Session session = getSession(emailConfig);
MimeMessage message = createMessage(session, emailConfig, to, cc, bcc, subject, content);
Transport.send(message);
log.info("发送邮件完成, to:{}, subject:{}", to, subject);
} catch (Exception e) {
log.error("发送邮件失败, to:{}, subject:{}, error:{}", to, subject, e.getMessage(), e);
throw new RuntimeException("发送邮件失败: " + e.getMessage(), e);
}
}
/**
* 异步发送邮件
*
* @param to 收件人邮箱
* @param subject 邮件主题
* @param content 邮件内容HTML格式
*/
@Async
public void sendEmailAsync(String to, String subject, String content) {
sendEmail(to, subject, content);
}
/**
* 异步发送邮件支持多收件人抄送密送
*
* @param to 收件人邮箱多个用逗号分隔
* @param cc 抄送人邮箱多个用逗号分隔可为null
* @param bcc 密送人邮箱多个用逗号分隔可为null
* @param subject 邮件主题
* @param content 邮件内容HTML格式
*/
@Async
public void sendEmailAsync(String to, String cc, String bcc, String subject, String content) {
sendEmail(to, cc, bcc, subject, content);
}
/**
* 刷新邮件配置缓存
*/
public void refreshConfig() {
cachedEmailConfig = null;
sessionCache.clear();
log.info("邮件配置缓存已刷新");
}
/**
* 获取邮件配置带缓存
*/
private EmailConfigDTO getEmailConfig() {
if (cachedEmailConfig != null) {
return cachedEmailConfig;
}
synchronized (this) {
if (cachedEmailConfig != null) {
return cachedEmailConfig;
}
List<ParamConfig> configs = paramConfigService.lambdaQuery()
.eq(ParamConfig::getGroupName, "EmailSet")
.list();
VUtil.trueThrowBusinessError(CollectionUtil.isEmpty(configs)).throwMessage("未配置邮件参数");
EmailConfigDTO emailConfig = new EmailConfigDTO();
ParamConfig cfg = configs.stream().filter(c -> StrUtil.equals("host", c.getCode())).findFirst().orElse(null);
VUtil.trueThrowBusinessError(Objects.isNull(cfg)).throwMessage("主机名未配置");
emailConfig.setHost(cfg.getValue());
cfg = configs.stream().filter(c -> StrUtil.equals("port", c.getCode())).findFirst().orElse(null);
VUtil.trueThrowBusinessError(Objects.isNull(cfg)).throwMessage("端口号未配置");
emailConfig.setPort(Integer.valueOf(cfg.getValue()));
cfg = configs.stream().filter(c -> StrUtil.equals("username", c.getCode())).findFirst().orElse(null);
VUtil.trueThrowBusinessError(Objects.isNull(cfg)).throwMessage("用户名未配置");
emailConfig.setUsername(cfg.getValue());
cfg = configs.stream().filter(c -> StrUtil.equals("password", c.getCode())).findFirst().orElse(null);
VUtil.trueThrowBusinessError(Objects.isNull(cfg)).throwMessage("密码未配置");
emailConfig.setPassword(cfg.getValue());
cachedEmailConfig = emailConfig;
return emailConfig;
}
}
/**
* 获取Session带缓存
*/
private Session getSession(EmailConfigDTO emailConfig) {
String cacheKey = emailConfig.getHost() + ":" + emailConfig.getPort();
return sessionCache.computeIfAbsent(cacheKey, k -> createSession(emailConfig));
}
/**
* 创建Session
*/
private Session createSession(EmailConfigDTO emailConfig) {
Properties properties = new Properties();
properties.put("mail.smtp.host", emailConfig.getHost());
properties.put("mail.smtp.port", emailConfig.getPort().toString());
properties.put("mail.smtp.auth", "true");
properties.put("mail.smtp.ssl.enable", "true");
// 设置超时时间单位毫秒
properties.put("mail.smtp.connectiontimeout", "5000"); // 连接超时
properties.put("mail.smtp.timeout", "5000"); // 读取超时
properties.put("mail.smtp.writetimeout", "5000"); // 写入超时
Session session = Session.getInstance(properties, new Authenticator() {
properties.put("mail.smtp.connectiontimeout", "5000");
properties.put("mail.smtp.timeout", "5000");
properties.put("mail.smtp.writetimeout", "5000");
return Session.getInstance(properties, new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(emailConfig.getUsername(), emailConfig.getPassword());
}
});
}
/**
* 创建邮件消息
*/
private MimeMessage createMessage(Session session, EmailConfigDTO emailConfig,
String to, String cc, String bcc,
String subject, String content) throws MessagingException, UnsupportedEncodingException {
MimeMessage message = new MimeMessage(session);
message.setFrom(new InternetAddress(emailConfig.getUsername()));
message.addRecipient(Message.RecipientType.TO, new InternetAddress(to));
message.setSubject(subject);
// 设置收件人
if (StrUtil.isNotBlank(to)) {
InternetAddress[] toAddresses = InternetAddress.parse(to);
message.setRecipients(Message.RecipientType.TO, toAddresses);
}
// 设置抄送
if (StrUtil.isNotBlank(cc)) {
InternetAddress[] ccAddresses = InternetAddress.parse(cc);
message.setRecipients(Message.RecipientType.CC, ccAddresses);
}
// 设置密送
if (StrUtil.isNotBlank(bcc)) {
InternetAddress[] bccAddresses = InternetAddress.parse(bcc);
message.setRecipients(Message.RecipientType.BCC, bccAddresses);
}
message.setSubject(subject, "UTF-8");
message.setContent(content, "text/html; charset=UTF-8");
Transport.send(message);
log.info("发送邮件完成,to:{},subject:{},content:{}", to, subject, content);
return message;
}
}

View File

@ -0,0 +1,32 @@
package com.nflg.wms.common.pojo.qo;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 待办事项 列表查询参数
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class QmsTodoItemSearchQO extends SearchBaseQO {
/**
* 编号
*/
private String code;
/**
* 标题
*/
private String title;
/**
* 来源类型ID
*/
private Long sourceTypeId;
/**
* 是否已读
*/
private Boolean isRead;
}

View File

@ -0,0 +1,74 @@
package com.nflg.wms.common.pojo.vo;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 待办事项 信息VO
*/
@Data
public class QmsTodoItemVO {
private Long id;
/**
* 编号
*/
private String code;
/**
* 标题
*/
private String title;
/**
* 来源类型ID
*/
private Long sourceTypeId;
/**
* 来源类型名称
*/
private String sourceTypeName;
/**
* 来源ID
*/
private Long sourceId;
/**
* 是否已读
*/
private Boolean isRead;
/**
* 创建人ID
*/
private Long createUserId;
/**
* 创建人姓名
*/
private String createUserName;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新人ID
*/
private Long updateUserId;
/**
* 更新人姓名
*/
private String updateUserName;
/**
* 更新时间
*/
private LocalDateTime updateTime;
}

View File

@ -0,0 +1,83 @@
package com.nflg.wms.repository.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 待办事项表
*/
@Getter
@Setter
@ToString
@Accessors(chain = true)
@TableName("qms_todo_item")
public class QmsTodoItem implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
/**
* 编号
*/
private String code;
/**
* 标题
*/
private String title;
/**
* 来源类型ID关联dictionary_item表字典编码MessageType
*/
private Long sourceTypeId;
/**
* 来源ID
*/
private Long sourceId;
/**
* 是否已读
*/
private Boolean isRead;
/**
* 创建人ID
*/
private Long createUserId;
/**
* 创建人姓名
*/
private String createUserName;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新人ID
*/
private Long updateUserId;
/**
* 更新人姓名
*/
private String updateUserName;
/**
* 更新时间
*/
private LocalDateTime updateTime;
}

View File

@ -0,0 +1,16 @@
package com.nflg.wms.repository.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.nflg.wms.common.pojo.qo.QmsTodoItemSearchQO;
import com.nflg.wms.common.pojo.vo.QmsTodoItemVO;
import com.nflg.wms.repository.entity.QmsTodoItem;
/**
* 待办事项 Mapper 接口
*/
public interface QmsTodoItemMapper extends BaseMapper<QmsTodoItem> {
IPage<QmsTodoItemVO> search(QmsTodoItemSearchQO request, Page<Object> page);
}

View File

@ -0,0 +1,26 @@
package com.nflg.wms.repository.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.IService;
import com.nflg.wms.common.pojo.qo.QmsTodoItemSearchQO;
import com.nflg.wms.common.pojo.vo.QmsTodoItemVO;
import com.nflg.wms.repository.entity.QmsTodoItem;
import java.util.List;
/**
* 待办事项 服务类
*/
public interface IQmsTodoItemService extends IService<QmsTodoItem> {
/**
* 分页查询待办事项列表
*/
IPage<QmsTodoItemVO> search(QmsTodoItemSearchQO request);
/**
* 批量标记已读
* @param ids 待办事项ID列表
*/
void markAsRead(List<Long> ids);
}

View File

@ -0,0 +1,45 @@
package com.nflg.wms.repository.service.impl;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.nflg.wms.common.pojo.qo.QmsTodoItemSearchQO;
import com.nflg.wms.common.pojo.vo.QmsTodoItemVO;
import com.nflg.wms.common.util.UserUtil;
import com.nflg.wms.repository.entity.QmsTodoItem;
import com.nflg.wms.repository.mapper.QmsTodoItemMapper;
import com.nflg.wms.repository.service.IQmsTodoItemService;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.List;
/**
* 待办事项 服务实现类
*/
@Service
public class QmsTodoItemServiceImpl extends ServiceImpl<QmsTodoItemMapper, QmsTodoItem> implements IQmsTodoItemService {
@Override
public IPage<QmsTodoItemVO> search(QmsTodoItemSearchQO request) {
return baseMapper.search(request, new Page<>(request.getPage(), request.getPageSize()));
}
@Override
public void markAsRead(List<Long> ids) {
if (ids == null || ids.isEmpty()) {
return;
}
Long userId = UserUtil.getUserId();
String userName = UserUtil.getUserName();
LocalDateTime now = LocalDateTime.now();
lambdaUpdate()
.eq(QmsTodoItem::getIsRead, false)
.in(QmsTodoItem::getId, ids)
.set(QmsTodoItem::getIsRead, true)
.set(QmsTodoItem::getUpdateUserId, userId)
.set(QmsTodoItem::getUpdateUserName, userName)
.set(QmsTodoItem::getUpdateTime, now)
.update();
}
}

View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.nflg.wms.repository.mapper.QmsTodoItemMapper">
<select id="search" resultType="com.nflg.wms.common.pojo.vo.QmsTodoItemVO">
SELECT t.id,
t.code,
t.title,
t.source_type_id,
di.name as source_type_name,
t.source_id,
t.is_read,
t.create_user_id,
t.create_user_name,
t.create_time,
t.update_user_id,
t.update_user_name,
t.update_time
FROM qms_todo_item t
LEFT JOIN dictionary_item di ON t.source_type_id = di.id
<where>
<if test="request.code != null and request.code != ''">
AND t.code ilike concat('%', #{request.code}, '%')
</if>
<if test="request.title != null and request.title != ''">
AND t.title ilike concat('%', #{request.title}, '%')
</if>
<if test="request.sourceTypeId != null">
AND t.source_type_id = #{request.sourceTypeId}
</if>
<if test="request.isRead != null">
AND t.is_read = #{request.isRead}
</if>
<if test="request.startDate != null">
AND t.create_time &gt;= #{request.startDate}
</if>
<if test="request.endDate != null">
AND t.create_time &lt;= #{request.endDate}
</if>
</where>
ORDER BY t.id DESC
</select>
</mapper>