消息模块、爬虫

This commit is contained in:
2025-11-13 19:00:27 +08:00
parent 2982d53800
commit e20a7755f8
85 changed files with 8637 additions and 201 deletions

View File

@@ -0,0 +1,96 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.xyzh</groupId>
<artifactId>school-news</artifactId>
<version>1.0.0</version>
</parent>
<groupId>org.xyzh</groupId>
<artifactId>message</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
</properties>
<dependencies>
<!-- API依赖 -->
<dependency>
<groupId>org.xyzh</groupId>
<artifactId>api-message</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.xyzh</groupId>
<artifactId>api-system</artifactId>
<version>1.0.0</version>
</dependency>
<!-- Common模块依赖 -->
<dependency>
<groupId>org.xyzh</groupId>
<artifactId>common-all</artifactId>
<version>1.0.0</version>
</dependency>
<!-- System模块依赖需要部门、角色、用户相关服务-->
<dependency>
<groupId>org.xyzh</groupId>
<artifactId>system</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<parameters>true</parameters>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,70 @@
package org.xyzh.message.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import java.util.concurrent.Executor;
/**
* 定时任务和异步任务配置类
*
* @description 配置消息模块的定时任务线程池和异步任务线程池
* @filename SchedulingConfig.java
* @author Claude
* @copyright xyzh
* @since 2025-11-13
*/
@Configuration
@EnableAsync
public class SchedulingConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setScheduler(messageSchedulerExecutor());
}
@Bean(name = "messageSchedulerExecutor")
public TaskScheduler messageSchedulerExecutor() {
ThreadPoolTaskScheduler executor = new ThreadPoolTaskScheduler();
// 线程池大小5个线程用于定时任务扫描
executor.setPoolSize(5);
// 线程名称前缀
executor.setThreadNamePrefix("message-scheduler-");
// 等待所有任务完成后再关闭
executor.setWaitForTasksToCompleteOnShutdown(true);
// 等待时间(秒)
executor.setAwaitTerminationSeconds(60);
// 初始化
executor.initialize();
return executor;
}
/**
* 异步任务线程池配置
*/
@Bean(name = "messageAsyncExecutor")
public Executor messageAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 核心线程数10个线程用于异步发送消息
executor.setCorePoolSize(10);
// 最大线程数
executor.setMaxPoolSize(20);
// 队列容量
executor.setQueueCapacity(500);
// 线程名称前缀
executor.setThreadNamePrefix("message-async-");
// 等待所有任务完成后再关闭
executor.setWaitForTasksToCompleteOnShutdown(true);
// 等待时间(秒)
executor.setAwaitTerminationSeconds(60);
// 初始化
executor.initialize();
return executor;
}
}

View File

@@ -0,0 +1,251 @@
package org.xyzh.message.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.xyzh.api.message.MessageService;
import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.core.page.PageParam;
import org.xyzh.common.core.page.PageRequest;
import org.xyzh.common.dto.message.*;
import java.util.Date;
import java.util.List;
import java.util.Map;
/**
* 消息控制器
*
* @description 消息通知模块的REST API接口
* @filename MessageController.java
* @author Claude
* @copyright xyzh
* @since 2025-11-13
*/
@RestController
@RequestMapping("/message")
public class MessageController {
private static final Logger logger = LoggerFactory.getLogger(MessageController.class);
@Autowired
private MessageService messageService;
// ================== 消息管理接口(管理端) ==================
/**
* 创建消息
*
* @param message 消息对象
* @return ResultDomain<TbSysMessage>
*/
@PostMapping
public ResultDomain<TbSysMessage> createMessage(@RequestBody TbSysMessage message) {
logger.info("创建消息:{}", message.getTitle());
return messageService.createMessage(message);
}
/**
* 更新消息(仅草稿状态)
*
* @param message 消息对象
* @return ResultDomain<TbSysMessage>
*/
@PutMapping
public ResultDomain<TbSysMessage> updateMessage(@RequestBody TbSysMessage message) {
logger.info("更新消息:{}", message.getMessageID());
return messageService.updateMessage(message);
}
/**
* 删除消息
*
* @param messageID 消息ID
* @return ResultDomain<TbSysMessage>
*/
@DeleteMapping
public ResultDomain<TbSysMessage> deleteMessage(@RequestBody TbSysMessage message) {
return messageService.deleteMessage(message.getMessageID());
}
/**
* 查询消息详情
*
* @param messageID 消息ID
* @return ResultDomain<MessageVO>
*/
@GetMapping("/detail/{messageID}")
public ResultDomain<MessageVO> getMessageById(@PathVariable String messageID) {
return messageService.getMessageById(messageID);
}
/**
* 分页查询消息列表(管理端)
*
* @param filter 过滤条件
* @param pageParam 分页参数
* @return ResultDomain<MessageVO>
*/
@PostMapping("/page")
public ResultDomain<MessageVO> getMessagePage(@RequestBody TbSysMessage filter, PageParam pageParam) {
return messageService.getMessagePage(filter, pageParam);
}
// ================== 消息发送接口 ==================
/**
* 发送消息
*
* @param messageID 消息ID
* @return ResultDomain<TbSysMessage>
*/
@PostMapping("/send/{messageID}")
public ResultDomain<TbSysMessage> sendMessage(@PathVariable String messageID) {
logger.info("发送消息:{}", messageID);
return messageService.sendMessage(messageID);
}
/**
* 立即发送(定时消息改为立即发送)
*
* @param messageID 消息ID
* @return ResultDomain<TbSysMessage>
*/
@PostMapping("/sendNow/{messageID}")
public ResultDomain<TbSysMessage> sendNow(@PathVariable String messageID) {
logger.info("立即发送消息:{}", messageID);
return messageService.sendNow(messageID);
}
/**
* 取消定时消息
*
* @param messageID 消息ID
* @return ResultDomain<TbSysMessage>
*/
@PostMapping("/cancel/{messageID}")
public ResultDomain<TbSysMessage> cancelMessage(@PathVariable String messageID) {
logger.info("取消消息:{}", messageID);
return messageService.cancelMessage(messageID);
}
/**
* 修改定时发送时间
*
* @param messageID 消息ID
* @param request 包含scheduledTime的请求对象
* @return ResultDomain<TbSysMessage>
*/
@PutMapping("/reschedule/{messageID}")
public ResultDomain<TbSysMessage> rescheduleMessage(@PathVariable String messageID,
@RequestBody Map<String, Object> request) {
logger.info("修改消息发送时间:{}", messageID);
Date scheduledTime = (Date) request.get("scheduledTime");
return messageService.rescheduleMessage(messageID, scheduledTime);
}
/**
* 重试失败消息
*
* @param messageID 消息ID
* @return ResultDomain<TbSysMessage>
*/
@PostMapping("/retry/{messageID}")
public ResultDomain<TbSysMessage> retryMessage(@PathVariable String messageID) {
logger.info("重试消息:{}", messageID);
return messageService.retryMessage(messageID);
}
// ================== 用户消息接口(用户端) ==================
/**
* 分页查询我的消息列表
*
* @param filter 过滤条件
* @param pageParam 分页参数
* @return ResultDomain<MessageUserVO>
*/
@PostMapping("/my/page")
public ResultDomain<MessageUserVO> getMyMessagesPage(@RequestBody PageRequest<MessageUserVO> pageRequest) {
return messageService.getMyMessagesPage(pageRequest.getFilter(), pageRequest.getPageParam());
}
/**
* 查询我的消息详情
*
* @param messageID 消息ID
* @return ResultDomain<MessageUserVO>
*/
@GetMapping("/my/detail/{messageID}")
public ResultDomain<MessageUserVO> getMyMessageDetail(@PathVariable String messageID) {
return messageService.getMyMessageDetail(messageID);
}
/**
* 标记消息为已读
*
* @param messageID 消息ID
* @return ResultDomain<TbSysMessageUser>
*/
@PostMapping("/my/markRead/{messageID}")
public ResultDomain<TbSysMessageUser> markAsRead(@PathVariable String messageID) {
return messageService.markAsRead(messageID);
}
/**
* 批量标记消息为已读
*
* @param request 包含messageIDs的请求对象
* @return ResultDomain<Integer>
*/
@PostMapping("/my/batchMarkRead")
public ResultDomain<Integer> batchMarkAsRead(@RequestBody Map<String, List<String>> request) {
List<String> messageIDs = request.get("messageIDs");
return messageService.batchMarkAsRead(messageIDs);
}
/**
* 获取未读消息数量
*
* @return ResultDomain<Integer>
*/
@GetMapping("/my/unreadCount")
public ResultDomain<Integer> getUnreadCount() {
return messageService.getUnreadCount();
}
// ================== 辅助接口 ==================
/**
* 获取可选的部门树
*
* @return ResultDomain<Map>
*/
@GetMapping("/targets/depts")
public ResultDomain<Map<String, Object>> getTargetDepts() {
return messageService.getTargetDepts();
}
/**
* 获取可选的角色列表
*
* @return ResultDomain<Map>
*/
@GetMapping("/targets/roles")
public ResultDomain<Map<String, Object>> getTargetRoles() {
return messageService.getTargetRoles();
}
/**
* 获取可选的用户列表
*
* @return ResultDomain<Map>
*/
@GetMapping("/targets/users")
public ResultDomain<Map<String, Object>> getTargetUsers() {
return messageService.getTargetUsers();
}
}

View File

@@ -0,0 +1,147 @@
package org.xyzh.message.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.xyzh.common.dto.message.TbSysMessage;
import org.xyzh.common.dto.message.MessageVO;
import java.time.LocalDateTime;
import java.util.List;
/**
* 消息Mapper接口
*
* @description 消息主体表的数据访问接口
* @filename MessageMapper.java
* @author Claude
* @copyright xyzh
* @since 2025-11-13
*/
@Mapper
public interface MessageMapper extends BaseMapper<TbSysMessage> {
/**
* 根据消息ID查询消息
*
* @param messageID 消息ID
* @return TbSysMessage 消息信息
* @author Claude
* @since 2025-11-13
*/
TbSysMessage selectMessageById(@Param("messageID") String messageID);
/**
* 插入消息
*
* @param message 消息信息
* @return int 影响行数
* @author Claude
* @since 2025-11-13
*/
int insertMessage(@Param("message") TbSysMessage message);
/**
* 更新消息
*
* @param message 消息信息
* @return int 影响行数
* @author Claude
* @since 2025-11-13
*/
int updateMessage(@Param("message") TbSysMessage message);
/**
* 删除消息(逻辑删除)
*
* @param messageID 消息ID
* @return int 影响行数
* @author Claude
* @since 2025-11-13
*/
int deleteMessage(@Param("messageID") String messageID);
/**
* 统计消息总数(带权限过滤)
*
* @param filter 过滤条件
* @param currentUserDeptID 当前用户部门ID
* @return int 总数
* @author Claude
* @since 2025-11-13
*/
int countMessage(@Param("filter") TbSysMessage filter,
@Param("currentUserDeptID") String currentUserDeptID);
/**
* 分页查询消息列表(带权限过滤)
*
* @param filter 过滤条件
* @param currentUserDeptID 当前用户部门ID
* @return List<MessageVO> 消息列表
* @author Claude
* @since 2025-11-13
*/
List<MessageVO> selectMessagePage(@Param("filter") TbSysMessage filter,
@Param("currentUserDeptID") String currentUserDeptID);
/**
* 查询消息详情
*
* @param messageID 消息ID
* @return MessageVO 消息详情
* @author Claude
* @since 2025-11-13
*/
MessageVO selectMessageDetail(@Param("messageID") String messageID);
/**
* 查询待发送的定时消息
*
* @param currentTime 当前时间
* @return List<TbSysMessage> 待发送的消息列表
* @author Claude
* @since 2025-11-13
*/
List<TbSysMessage> selectPendingScheduledMessages(@Param("currentTime") LocalDateTime currentTime);
/**
* CAS更新消息状态防止并发
*
* @param messageID 消息ID
* @param expectedStatus 期望的当前状态
* @param newStatus 新状态
* @return int 更新的行数1表示成功0表示失败
* @author Claude
* @since 2025-11-13
*/
int compareAndSetStatus(@Param("messageID") String messageID,
@Param("expectedStatus") String expectedStatus,
@Param("newStatus") String newStatus);
/**
* 更新消息统计信息
*
* @param messageID 消息ID
* @param sentCount 已发送数量
* @param successCount 成功数量
* @param failedCount 失败数量
* @return int 更新的行数
* @author Claude
* @since 2025-11-13
*/
int updateStatistics(@Param("messageID") String messageID,
@Param("sentCount") Integer sentCount,
@Param("successCount") Integer successCount,
@Param("failedCount") Integer failedCount);
/**
* 更新已读数量
*
* @param messageID 消息ID
* @return int 更新的行数
* @author Claude
* @since 2025-11-13
*/
int incrementReadCount(@Param("messageID") String messageID);
}

View File

@@ -0,0 +1,51 @@
package org.xyzh.message.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.xyzh.common.dto.message.TbSysMessageTarget;
import java.util.List;
/**
* 消息接收对象Mapper接口
*
* @description 消息接收对象表的数据访问接口
* @filename MessageTargetMapper.java
* @author Claude
* @copyright xyzh
* @since 2025-11-13
*/
@Mapper
public interface MessageTargetMapper extends BaseMapper<TbSysMessageTarget> {
/**
* 根据消息ID查询接收对象列表
*
* @param messageID 消息ID
* @return List<TbSysMessageTarget> 接收对象列表
* @author Claude
* @since 2025-11-13
*/
List<TbSysMessageTarget> selectByMessageID(@Param("messageID") String messageID);
/**
* 批量插入接收对象
*
* @param targets 接收对象列表
* @return int 插入的行数
* @author Claude
* @since 2025-11-13
*/
int batchInsert(@Param("targets") List<TbSysMessageTarget> targets);
/**
* 根据消息ID删除接收对象
*
* @param messageID 消息ID
* @return int 删除的行数
* @author Claude
* @since 2025-11-13
*/
int deleteByMessageID(@Param("messageID") String messageID);
}

View File

@@ -0,0 +1,168 @@
package org.xyzh.message.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.xyzh.common.dto.message.TbSysMessageUser;
import org.xyzh.common.dto.message.MessageUserVO;
import java.util.List;
/**
* 用户消息Mapper接口
*
* @description 用户接收消息表的数据访问接口
* @filename MessageUserMapper.java
* @author Claude
* @copyright xyzh
* @since 2025-11-13
*/
@Mapper
public interface MessageUserMapper extends BaseMapper<TbSysMessageUser> {
/**
* 根据消息ID查询用户消息列表
*
* @param messageID 消息ID
* @return List<MessageUserVO> 用户消息列表
* @author Claude
* @since 2025-11-13
*/
List<MessageUserVO> selectByMessageID(@Param("messageID") String messageID);
/**
* 批量插入用户消息
*
* @param userMessages 用户消息列表
* @return int 插入的行数
* @author Claude
* @since 2025-11-13
*/
int batchInsert(@Param("userMessages") List<TbSysMessageUser> userMessages);
/**
* 分页查询当前用户的消息列表
*
* @param userID 用户ID
* @param filter 过滤条件
* @return List<MessageUserVO> 消息列表
* @author Claude
* @since 2025-11-13
*/
List<MessageUserVO> selectMyMessages(@Param("userID") String userID,
@Param("filter") TbSysMessageUser filter);
/**
* 查询当前用户的消息详情
*
* @param userID 用户ID
* @param messageID 消息ID
* @return MessageUserVO 消息详情
* @author Claude
* @since 2025-11-13
*/
MessageUserVO selectMyMessageDetail(@Param("userID") String userID,
@Param("messageID") String messageID);
/**
* 标记消息为已读
*
* @param userID 用户ID
* @param messageID 消息ID
* @return int 更新的行数
* @author Claude
* @since 2025-11-13
*/
int markAsRead(@Param("userID") String userID,
@Param("messageID") String messageID);
/**
* 批量标记消息为已读
*
* @param userID 用户ID
* @param messageIDs 消息ID列表
* @return int 更新的行数
* @author Claude
* @since 2025-11-13
*/
int batchMarkAsRead(@Param("userID") String userID,
@Param("messageIDs") List<String> messageIDs);
/**
* 查询未读消息数量
*
* @param userID 用户ID
* @return Integer 未读消息数量
* @author Claude
* @since 2025-11-13
*/
Integer countUnread(@Param("userID") String userID);
/**
* 动态计算未读消息数量(基于 target 配置)
*
* @param userID 用户ID
* @return Integer 未读消息数量
* @author Claude
* @since 2025-11-13
*/
Integer countUnreadWithDynamicTargets(@Param("userID") String userID);
/**
* 更新用户消息的发送状态
*
* @param id 用户消息ID
* @param sendStatus 发送状态
* @param failReason 失败原因(可选)
* @return int 更新的行数
* @author Claude
* @since 2025-11-13
*/
int updateSendStatus(@Param("id") String id,
@Param("sendStatus") String sendStatus,
@Param("failReason") String failReason);
/**
* 查询待发送的用户消息列表
*
* @param filter 过滤条件
* @return List<TbSysMessageUser> 用户消息列表
* @author Claude
* @since 2025-11-13
*/
List<TbSysMessageUser> selectPendingUserMessages(@Param("filter") TbSysMessageUser filter);
/**
* 动态查询当前用户可见的消息列表(基于 target 配置计算)
*
* @param userID 用户ID
* @param filter 过滤条件
* @return List<MessageUserVO> 消息列表
* @author Claude
* @since 2025-11-13
*/
List<MessageUserVO> selectMyMessagesWithDynamicTargets(@Param("userID") String userID,
@Param("filter") MessageUserVO filter);
/**
* 查询或创建用户消息记录upsert
*
* @param userID 用户ID
* @param messageID 消息ID
* @return MessageUserVO 用户消息记录
* @author Claude
* @since 2025-11-13
*/
MessageUserVO selectOrCreateUserMessage(@Param("userID") String userID,
@Param("messageID") String messageID);
/**
* 插入用户消息记录(如果不存在)
*
* @param userMessage 用户消息
* @return int 插入的行数
* @author Claude
* @since 2025-11-13
*/
int insertIfNotExists(@Param("userMessage") TbSysMessageUser userMessage);
}

View File

@@ -0,0 +1,195 @@
package org.xyzh.message.scheduler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.xyzh.common.dto.message.TbSysMessage;
import org.xyzh.message.mapper.MessageMapper;
import org.xyzh.message.service.impl.MessageSendService;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.List;
/**
* 消息定时任务扫描器
*
* @description 定时扫描待发送的定时消息并触发发送
* @filename MessageScheduler.java
* @author Claude
* @copyright xyzh
* @since 2025-11-13
*/
@Component
public class MessageScheduler {
private static final Logger logger = LoggerFactory.getLogger(MessageScheduler.class);
@Autowired
private MessageMapper messageMapper;
@Autowired
private MessageSendService messageSendService;
/**
* 扫描待发送的定时消息
* 每分钟执行一次
*/
@Scheduled(cron = "0 * * * * ?")
public void scanPendingScheduledMessages() {
try {
logger.debug("开始扫描待发送的定时消息...");
LocalDateTime now = LocalDateTime.now();
// 查询满足条件的消息:
// 1. status=pending待发送
// 2. sendMode=scheduled定时发送
// 3. scheduledTime <= 当前时间
// 4. deleted=0未删除
List<TbSysMessage> messages = messageMapper.selectPendingScheduledMessages(now);
if (messages.isEmpty()) {
logger.debug("没有待发送的定时消息");
return;
}
logger.info("发现 {} 条待发送的定时消息", messages.size());
// 处理每条消息
for (TbSysMessage message : messages) {
try {
// 使用CASCompare-And-Set更新状态防止并发重复触发
boolean updated = messageMapper.compareAndSetStatus(
message.getMessageID(),
"pending", // 期望的当前状态
"sending" // 新状态
) > 0;
if (!updated) {
logger.warn("消息 {} 状态已被修改,跳过处理", message.getMessageID());
continue;
}
// 更新实际发送时间和状态
message.setActualSendTime(new Date());
message.setStatus("sending");
message.setUpdateTime(new Date());
messageMapper.updateMessage(message);
// 异步发送消息
messageSendService.sendMessageAsync(message.getMessageID());
logger.info("定时消息 {} 已触发发送", message.getMessageID());
} catch (Exception e) {
logger.error("处理定时消息失败:{}", message.getMessageID(), e);
handleSendError(message, e);
}
}
logger.info("定时消息扫描完成,已处理 {} 条消息", messages.size());
} catch (Exception e) {
logger.error("扫描定时消息时发生错误", e);
}
}
/**
* 处理发送失败,支持重试机制
*
* @param message 消息对象
* @param e 异常信息
*/
private void handleSendError(TbSysMessage message, Exception e) {
try {
int retryCount = message.getRetryCount() != null ? message.getRetryCount() : 0;
int maxRetryCount = message.getMaxRetryCount() != null ? message.getMaxRetryCount() : 3;
if (retryCount < maxRetryCount) {
// 未达到最大重试次数,增加重试计数
message.setRetryCount(retryCount + 1);
message.setStatus("pending"); // 重新置为待发送状态
// 设置下次重试时间5分钟后
LocalDateTime nextRetryTime = LocalDateTime.now().plusMinutes(5);
message.setScheduledTime(convertToDate(nextRetryTime));
message.setLastError(e.getMessage());
message.setUpdateTime(new Date());
messageMapper.updateMessage(message);
logger.warn("消息 {} 发送失败将在5分钟后重试 ({}/{})",
message.getMessageID(), retryCount + 1, maxRetryCount);
} else {
// 超过最大重试次数,标记为失败
message.setStatus("failed");
message.setLastError("超过最大重试次数:" + e.getMessage());
message.setUpdateTime(new Date());
messageMapper.updateMessage(message);
logger.error("消息 {} 发送失败且已超过最大重试次数", message.getMessageID());
}
} catch (Exception ex) {
logger.error("处理发送错误时发生异常", ex);
}
}
/**
* 将LocalDateTime转换为Date
*/
private Date convertToDate(LocalDateTime localDateTime) {
return java.sql.Timestamp.valueOf(localDateTime);
}
/**
* 每小时清理一次已取消和已失败的旧消息(可选功能)
* 将超过30天的已取消/失败消息标记为删除
*/
@Scheduled(cron = "0 0 * * * ?")
public void cleanupOldMessages() {
try {
logger.debug("开始清理旧消息...");
// 计算30天前的时间
LocalDateTime thirtyDaysAgo = LocalDateTime.now().minusDays(30);
// TODO: 实现清理逻辑
// 清理条件:
// 1. status in ('cancelled', 'failed')
// 2. updateTime < 30天前
// 3. deleted = 0
logger.debug("旧消息清理完成");
} catch (Exception e) {
logger.error("清理旧消息时发生错误", e);
}
}
/**
* 每天凌晨生成消息统计报告(可选功能)
*/
@Scheduled(cron = "0 0 0 * * ?")
public void generateDailyReport() {
try {
logger.info("开始生成每日消息统计报告...");
// TODO: 实现统计报告生成逻辑
// 统计内容:
// 1. 今日发送消息总数
// 2. 发送成功率
// 3. 已读率
// 4. 各发送方式的使用情况
logger.info("每日消息统计报告生成完成");
} catch (Exception e) {
logger.error("生成每日报告时发生错误", e);
}
}
}

View File

@@ -0,0 +1,319 @@
package org.xyzh.message.service.impl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.xyzh.common.dto.message.TbSysMessage;
import org.xyzh.common.dto.message.TbSysMessageUser;
import org.xyzh.common.dto.user.TbSysUser;
import org.xyzh.common.utils.EmailUtils;
import org.xyzh.common.utils.SmsUtils;
import org.xyzh.message.mapper.MessageMapper;
import org.xyzh.message.mapper.MessageUserMapper;
import org.xyzh.system.mapper.UserMapper;
import java.util.Date;
import java.util.List;
/**
* 消息发送服务
*
* @description 异步发送消息的服务类
* @filename MessageSendService.java
* @author Claude
* @copyright xyzh
* @since 2025-11-13
*/
@Service
public class MessageSendService {
private static final Logger logger = LoggerFactory.getLogger(MessageSendService.class);
@Autowired
private MessageMapper messageMapper;
@Autowired
private MessageUserMapper messageUserMapper;
@Autowired
private UserMapper userMapper;
@Autowired
private EmailUtils emailUtils;
@Autowired
private SmsUtils smsUtils;
/**
* 异步发送消息
*
* @param messageID 消息ID
*/
@Async("messageAsyncExecutor")
@Transactional(rollbackFor = Exception.class)
public void sendMessageAsync(String messageID) {
try {
logger.info("开始发送消息:{}", messageID);
// 1. 查询消息
TbSysMessage message = messageMapper.selectMessageById(messageID);
if (message == null || message.getDeleted()) {
logger.error("消息不存在:{}", messageID);
return;
}
// 2. 查询所有待发送的用户消息
TbSysMessageUser filter = new TbSysMessageUser();
filter.setMessageID(messageID);
filter.setSendStatus("pending");
filter.setDeleted(false);
List<TbSysMessageUser> userMessages = messageUserMapper.selectPendingUserMessages(filter);
if (userMessages.isEmpty()) {
logger.warn("没有待发送的用户消息:{}", messageID);
updateMessageStatus(message, "sent");
return;
}
// 3. 遍历发送
int successCount = 0;
int failedCount = 0;
for (TbSysMessageUser userMessage : userMessages) {
try {
// 查询用户信息
TbSysUser user = userMapper.selectUserById(userMessage.getUserID());
if (user == null || user.getDeleted()) {
logger.warn("用户不存在:{}", userMessage.getUserID());
updateUserMessageStatus(userMessage.getID(), "failed", "用户不存在");
failedCount++;
continue;
}
// 根据发送方式发送消息
String[] methods = userMessage.getSendMethod().split(",");
boolean sent = false;
for (String method : methods) {
method = method.trim();
try {
switch (method) {
case "system":
// 系统消息已在数据库中,无需额外操作
sent = true;
break;
case "email":
sent = sendEmail(user, message);
break;
case "sms":
sent = sendSms(user, message);
break;
default:
logger.warn("未知的发送方式:{}", method);
}
if (sent) {
break; // 任意一种方式成功即可
}
} catch (Exception e) {
logger.error("发送消息失败 [{}] - 用户:{}, 方式:{}", messageID, user.getUsername(), method, e);
}
}
// 更新用户消息发送状态
if (sent) {
updateUserMessageStatus(userMessage.getID(), "success", null);
successCount++;
} else {
updateUserMessageStatus(userMessage.getID(), "failed", "所有发送方式均失败");
failedCount++;
}
} catch (Exception e) {
logger.error("处理用户消息失败:{}", userMessage.getID(), e);
updateUserMessageStatus(userMessage.getID(), "failed", e.getMessage());
failedCount++;
}
}
// 4. 更新消息统计信息
int sentCount = successCount + failedCount;
messageMapper.updateStatistics(messageID, sentCount, successCount, failedCount);
// 5. 更新消息状态
if (failedCount == userMessages.size()) {
// 全部失败
updateMessageStatus(message, "failed");
} else {
// 至少有一个成功
updateMessageStatus(message, "sent");
}
logger.info("消息发送完成:{} - 成功:{}, 失败:{}", messageID, successCount, failedCount);
} catch (Exception e) {
logger.error("发送消息异常:{}", messageID, e);
// 更新消息状态为失败
try {
TbSysMessage message = messageMapper.selectMessageById(messageID);
if (message != null && !message.getDeleted()) {
message.setStatus("failed");
message.setLastError(e.getMessage());
message.setUpdateTime(new Date());
messageMapper.updateMessage(message);
}
} catch (Exception ex) {
logger.error("更新消息状态失败", ex);
}
}
}
/**
* 发送邮件
*
* @param user 用户
* @param message 消息
* @return 是否成功
*/
private boolean sendEmail(TbSysUser user, TbSysMessage message) {
try {
if (user.getEmail() == null || user.getEmail().trim().isEmpty()) {
logger.warn("用户 {} 没有邮箱地址", user.getUsername());
return false;
}
// 构建HTML邮件内容
String htmlContent = buildEmailHtml(message);
// 发送邮件
boolean result = emailUtils.sendHtmlEmail(
user.getEmail(),
message.getTitle(),
htmlContent
);
if (result) {
logger.info("邮件发送成功 - 用户:{}, 邮箱:{}", user.getUsername(), user.getEmail());
} else {
logger.warn("邮件发送失败 - 用户:{}, 邮箱:{}", user.getUsername(), user.getEmail());
}
return result;
} catch (Exception e) {
logger.error("发送邮件异常 - 用户:{}", user.getUsername(), e);
return false;
}
}
/**
* 发送短信
*
* @param user 用户
* @param message 消息
* @return 是否成功
*/
private boolean sendSms(TbSysUser user, TbSysMessage message) {
try {
if (user.getPhone() == null || user.getPhone().trim().isEmpty()) {
logger.warn("用户 {} 没有手机号", user.getUsername());
return false;
}
// 短信内容(限制长度)
String smsContent = message.getTitle();
if (message.getContent() != null && message.getContent().length() < 50) {
smsContent += "" + message.getContent();
}
// 发送短信
boolean result = smsUtils.sendSms(user.getPhone(), smsContent, "xx");
if (result) {
logger.info("短信发送成功 - 用户:{}, 手机:{}", user.getUsername(), user.getPhone());
} else {
logger.warn("短信发送失败 - 用户:{}, 手机:{}", user.getUsername(), user.getPhone());
}
return result;
} catch (Exception e) {
logger.error("发送短信异常 - 用户:{}", user.getUsername(), e);
return false;
}
}
/**
* 构建邮件HTML内容
*/
private String buildEmailHtml(TbSysMessage message) {
StringBuilder html = new StringBuilder();
html.append("<!DOCTYPE html>");
html.append("<html>");
html.append("<head>");
html.append("<meta charset=\"UTF-8\">");
html.append("<style>");
html.append("body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }");
html.append(".container { max-width: 600px; margin: 20px auto; padding: 20px; border: 1px solid #ddd; border-radius: 5px; }");
html.append(".header { background-color: #c8232c; color: white; padding: 15px; border-radius: 5px 5px 0 0; }");
html.append(".content { padding: 20px; background-color: #f9f9f9; }");
html.append(".footer { text-align: center; padding: 15px; font-size: 12px; color: #999; }");
html.append(".priority-urgent { border-left: 4px solid #ff0000; }");
html.append(".priority-important { border-left: 4px solid #ff9900; }");
html.append(".priority-normal { border-left: 4px solid #00aa00; }");
html.append("</style>");
html.append("</head>");
html.append("<body>");
html.append("<div class=\"container priority-").append(message.getPriority()).append("\">");
html.append("<div class=\"header\">");
html.append("<h2>").append(message.getTitle()).append("</h2>");
html.append("</div>");
html.append("<div class=\"content\">");
html.append(message.getContent());
html.append("</div>");
html.append("<div class=\"footer\">");
html.append("<p>发送人:").append(message.getSenderName()).append(" (").append(message.getSenderDeptName()).append(")</p>");
html.append("<p>发送时间:").append(message.getActualSendTime() != null ? message.getActualSendTime().toString() : "").append("</p>");
html.append("<p>此邮件由系统自动发送,请勿回复。</p>");
html.append("</div>");
html.append("</div>");
html.append("</body>");
html.append("</html>");
return html.toString();
}
/**
* 更新消息状态
*/
private void updateMessageStatus(TbSysMessage message, String status) {
try {
message.setStatus(status);
if ("sent".equals(status)) {
message.setActualSendTime(new Date());
}
message.setUpdateTime(new Date());
messageMapper.updateMessage(message);
} catch (Exception e) {
logger.error("更新消息状态失败", e);
}
}
/**
* 更新用户消息发送状态
*/
private void updateUserMessageStatus(String id, String status, String failReason) {
try {
messageUserMapper.updateSendStatus(id, status, failReason);
} catch (Exception e) {
logger.error("更新用户消息状态失败", e);
}
}
}

View File

@@ -0,0 +1,746 @@
package org.xyzh.message.service.impl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xyzh.api.message.MessageService;
import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.core.page.PageDomain;
import org.xyzh.common.core.page.PageParam;
import org.xyzh.common.dto.message.*;
import org.xyzh.common.utils.IDUtils;
import org.xyzh.message.mapper.MessageMapper;
import org.xyzh.message.mapper.MessageTargetMapper;
import org.xyzh.message.mapper.MessageUserMapper;
import org.xyzh.system.mapper.DepartmentMapper;
import org.xyzh.system.mapper.UserMapper;
import java.util.*;
/**
* 消息服务实现类
*
* @description 消息通知模块的服务实现
* @filename MessageServiceImpl.java
* @author Claude
* @copyright xyzh
* @since 2025-11-13
*/
@Service
public class MessageServiceImpl implements MessageService {
private static final Logger logger = LoggerFactory.getLogger(MessageServiceImpl.class);
@Autowired
private MessageMapper messageMapper;
@Autowired
private MessageTargetMapper messageTargetMapper;
@Autowired
private MessageUserMapper messageUserMapper;
@Autowired
private DepartmentMapper departmentMapper;
@Autowired
private UserMapper userMapper;
@Autowired
private MessageSendService messageSendService;
/**
* 创建消息
*/
@Override
@Transactional(rollbackFor = Exception.class)
public ResultDomain<TbSysMessage> createMessage(TbSysMessage message) {
ResultDomain<TbSysMessage> rt = new ResultDomain<>();
try {
// 1. 获取当前用户信息从Session或SecurityContext获取
String currentUserID = getCurrentUserID();
String currentDeptID = getCurrentUserDeptID();
// 2. 设置消息主体基本信息
if (message.getID() == null) {
message.setID(IDUtils.generateID());
}
if (message.getMessageID() == null) {
message.setMessageID(IDUtils.generateID());
}
message.setSenderID(currentUserID);
message.setSenderDeptID(currentDeptID);
// 设置状态
if (message.getStatus() == null) {
if ("immediate".equals(message.getSendMode())) {
message.setStatus("sending");
} else {
message.setStatus("pending");
}
}
if (message.getTargetUserCount() == null) {
message.setTargetUserCount(0);
}
if (message.getRetryCount() == null) {
message.setRetryCount(0);
}
if (message.getMaxRetryCount() == null) {
message.setMaxRetryCount(3);
}
message.setCreator(currentUserID);
message.setCreateTime(new Date());
message.setUpdateTime(new Date());
message.setDeleted(false);
// 保存消息主体
messageMapper.insertMessage(message);
// 3. 保存接收对象配置
List<TbSysMessageTarget> targets = message.getTargets();
if (targets != null && !targets.isEmpty()) {
for (TbSysMessageTarget target : targets) {
// 权限校验scopeDeptID必须是当前部门或子部门
if (!isCurrentOrSubDept(currentDeptID, target.getScopeDeptID())) {
rt.fail("无权向该部门发送消息");
return rt;
}
if (target.getID() == null) {
target.setID(IDUtils.generateID());
}
target.setMessageID(message.getMessageID());
target.setCreator(currentUserID);
target.setCreateTime(new Date());
target.setUpdateTime(new Date());
target.setDeleted(false);
}
messageTargetMapper.batchInsert(targets);
// 4. 解析接收对象,生成用户消息列表
List<TbSysMessageUser> userMessages = resolveTargetUsers(message.getMessageID(), targets, currentUserID);
if (!userMessages.isEmpty()) {
messageUserMapper.batchInsert(userMessages);
}
// 5. 更新目标用户总数
message.setTargetUserCount(userMessages.size());
messageMapper.updateMessage(message);
// 6. 如果是立即发送,异步发送消息
if ("immediate".equals(message.getSendMode())) {
messageSendService.sendMessageAsync(message.getMessageID());
}
}
rt.success("创建成功", message);
return rt;
} catch (Exception e) {
logger.error("创建消息失败", e);
rt.fail("创建消息失败:" + e.getMessage());
return rt;
}
}
/**
* 更新消息(仅允许更新草稿状态的消息)
*/
@Override
@Transactional(rollbackFor = Exception.class)
public ResultDomain<TbSysMessage> updateMessage(TbSysMessage message) {
ResultDomain<TbSysMessage> rt = new ResultDomain<>();
try {
// 查询原消息
TbSysMessage existingMessage = messageMapper.selectMessageById(message.getMessageID());
if (existingMessage == null || existingMessage.getDeleted()) {
rt.fail("消息不存在");
return rt;
}
// 只允许更新草稿状态的消息
if (!"draft".equals(existingMessage.getStatus())) {
rt.fail("只能更新草稿状态的消息");
return rt;
}
message.setUpdateTime(new Date());
message.setUpdater(getCurrentUserID());
messageMapper.updateMessage(message);
rt.success("更新成功", message);
return rt;
} catch (Exception e) {
logger.error("更新消息失败", e);
rt.fail("更新消息失败:"+e.getMessage());
return rt;
}
}
/**
* 删除消息(逻辑删除)
*/
@Override
@Transactional(rollbackFor = Exception.class)
public ResultDomain<TbSysMessage> deleteMessage(String messageID) {
ResultDomain<TbSysMessage> rt = new ResultDomain<>();
try {
TbSysMessage message = messageMapper.selectMessageById(messageID);
if (message == null || message.getDeleted()) {
rt.fail("消息不存在");
return rt;
}
int result = messageMapper.deleteMessage(messageID);
if (result > 0) {
// 同时删除关联的接收对象和用户消息
messageTargetMapper.deleteByMessageID(messageID);
rt.success("删除成功", message);
} else {
rt.fail("删除失败");
}
return rt;
} catch (Exception e) {
logger.error("删除消息失败", e);
rt.fail("删除消息失败:" + e.getMessage());
return rt;
}
}
/**
* 根据ID查询消息详情
*/
@Override
public ResultDomain<MessageVO> getMessageById(String messageID) {
ResultDomain<MessageVO> rt = new ResultDomain<>();
try {
MessageVO messageVO = messageMapper.selectMessageDetail(messageID);
if (messageVO == null) {
rt.fail("消息不存在");
return rt;
}
// 查询接收对象列表
List<TbSysMessageTarget> targets = messageTargetMapper.selectByMessageID(messageID);
messageVO.setTargets(targets);
// 查询用户接收记录
List<MessageUserVO> userMessages = messageUserMapper.selectByMessageID(messageID);
messageVO.setUserMessages(userMessages);
rt.success("查询成功", messageVO);
return rt;
} catch (Exception e) {
logger.error("查询消息详情失败", e);
rt.fail("查询消息详情失败:" + e.getMessage());
return rt;
}
}
/**
* 分页查询消息列表(管理端)
*/
@Override
public ResultDomain<MessageVO> getMessagePage(TbSysMessage filter, PageParam pageParam) {
ResultDomain<MessageVO> rt = new ResultDomain<>();
try {
if (filter == null) {
filter = new TbSysMessage();
}
filter.setDeleted(false);
if (pageParam == null) {
pageParam = new PageParam();
}
String currentDeptID = getCurrentUserDeptID();
List<MessageVO> list = messageMapper.selectMessagePage(filter, currentDeptID);
int total = messageMapper.countMessage(filter, currentDeptID);
PageDomain<MessageVO> pageDomain = new PageDomain<>();
pageDomain.setDataList(list);
pageParam.setTotalElements(total);
pageParam.setTotalPages((int) Math.ceil((double) total / pageParam.getPageSize()));
pageDomain.setPageParam(pageParam);
rt.success("查询成功", pageDomain);
return rt;
} catch (Exception e) {
logger.error("查询消息列表失败", e);
rt.fail("查询消息列表失败:" + e.getMessage());
return rt;
}
}
/**
* 发送消息
*/
@Override
@Transactional(rollbackFor = Exception.class)
public ResultDomain<TbSysMessage> sendMessage(String messageID) {
ResultDomain<TbSysMessage> rt = new ResultDomain<>();
try {
TbSysMessage message = messageMapper.selectMessageById(messageID);
if (message == null || message.getDeleted()) {
rt.fail("消息不存在");
return rt;
}
// 更新状态为发送中
message.setStatus("sending");
message.setActualSendTime(new Date());
message.setUpdateTime(new Date());
messageMapper.updateMessage(message);
// 异步发送
messageSendService.sendMessageAsync(messageID);
rt.success("发送成功", message);
return rt;
} catch (Exception e) {
logger.error("发送消息失败", e);
rt.fail("发送消息失败:" + e.getMessage());
return rt;
}
}
/**
* 立即发送(将定时消息改为立即发送)
*/
@Override
@Transactional(rollbackFor = Exception.class)
public ResultDomain<TbSysMessage> sendNow(String messageID) {
ResultDomain<TbSysMessage> rt = new ResultDomain<>();
try {
TbSysMessage message = messageMapper.selectMessageById(messageID);
if (message == null || message.getDeleted()) {
rt.fail("消息不存在");
return rt;
}
if (!"pending".equals(message.getStatus())) {
rt.fail("只能立即发送待发送状态的消息");
return rt;
}
// 更新为立即发送模式
message.setSendMode("immediate");
message.setStatus("sending");
message.setActualSendTime(new Date());
message.setUpdateTime(new Date());
messageMapper.updateMessage(message);
// 异步发送
messageSendService.sendMessageAsync(messageID);
rt.success("立即发送成功", message);
return rt;
} catch (Exception e) {
logger.error("立即发送消息失败", e);
rt.fail("立即发送消息失败:" + e.getMessage());
return rt;
}
}
/**
* 取消定时消息
*/
@Override
@Transactional(rollbackFor = Exception.class)
public ResultDomain<TbSysMessage> cancelMessage(String messageID) {
ResultDomain<TbSysMessage> rt = new ResultDomain<>();
try {
TbSysMessage message = messageMapper.selectMessageById(messageID);
if (message == null || message.getDeleted()) {
rt.fail("消息不存在");
return rt;
}
if (!"pending".equals(message.getStatus())) {
rt.fail("只能取消待发送状态的消息");
return rt;
}
message.setStatus("cancelled");
message.setUpdateTime(new Date());
messageMapper.updateMessage(message);
rt.success("取消成功", message);
return rt;
} catch (Exception e) {
logger.error("取消消息失败", e);
rt.fail("取消消息失败:" + e.getMessage());
return rt;
}
}
/**
* 修改定时发送时间
*/
@Override
@Transactional(rollbackFor = Exception.class)
public ResultDomain<TbSysMessage> rescheduleMessage(String messageID, Date scheduledTime) {
ResultDomain<TbSysMessage> rt = new ResultDomain<>();
try {
TbSysMessage message = messageMapper.selectMessageById(messageID);
if (message == null || message.getDeleted()) {
rt.fail("消息不存在");
return rt;
}
if (!"pending".equals(message.getStatus())) {
rt.fail("只能修改待发送状态的消息");
return rt;
}
message.setScheduledTime(scheduledTime);
message.setUpdateTime(new Date());
messageMapper.updateMessage(message);
rt.success("修改成功", message);
return rt;
} catch (Exception e) {
logger.error("修改定时时间失败", e);
rt.fail("修改定时时间失败:" + e.getMessage());
return rt;
}
}
/**
* 重试失败消息
*/
@Override
@Transactional(rollbackFor = Exception.class)
public ResultDomain<TbSysMessage> retryMessage(String messageID) {
ResultDomain<TbSysMessage> rt = new ResultDomain<>();
try {
TbSysMessage message = messageMapper.selectMessageById(messageID);
if (message == null || message.getDeleted()) {
rt.fail("消息不存在");
return rt;
}
if (!"failed".equals(message.getStatus())) {
rt.fail("只能重试失败状态的消息");
return rt;
}
// 重置状态
message.setStatus("sending");
message.setRetryCount(message.getRetryCount() + 1);
message.setUpdateTime(new Date());
messageMapper.updateMessage(message);
// 异步发送
messageSendService.sendMessageAsync(messageID);
rt.success("重试成功", message);
return rt;
} catch (Exception e) {
logger.error("重试消息失败", e);
rt.fail("重试消息失败:" + e.getMessage());
return rt;
}
}
// ================== 用户消息相关方法 ==================
@Override
public ResultDomain<MessageUserVO> getMyMessagesPage(MessageUserVO filter, PageParam pageParam) {
ResultDomain<MessageUserVO> rt = new ResultDomain<>();
try {
if (filter == null) {
filter = new MessageUserVO();
}
if (pageParam == null) {
pageParam = new PageParam();
}
String currentUserID = getCurrentUserID();
// 使用新的动态查询方法
List<MessageUserVO> list = messageUserMapper.selectMyMessagesWithDynamicTargets(currentUserID, filter);
PageDomain<MessageUserVO> pageDomain = new PageDomain<>();
pageDomain.setDataList(list);
pageParam.setTotalElements(list.size());
pageParam.setTotalPages((int) Math.ceil((double) list.size() / pageParam.getPageSize()));
pageDomain.setPageParam(pageParam);
rt.success("查询成功", pageDomain);
return rt;
} catch (Exception e) {
logger.error("查询我的消息失败", e);
rt.fail("查询我的消息失败:" + e.getMessage());
return rt;
}
}
@Override
public ResultDomain<MessageUserVO> getMyMessageDetail(String messageID) {
ResultDomain<MessageUserVO> rt = new ResultDomain<>();
try {
String currentUserID = getCurrentUserID();
MessageUserVO messageUserVO = messageUserMapper.selectOrCreateUserMessage(currentUserID, messageID);
if (messageUserVO == null) {
rt.fail("消息不存在");
return rt;
}
// 如果用户消息记录不存在id 为 null创建新记录
if (messageUserVO.getId() == null) {
logger.info("用户首次查看消息创建用户消息记录userID={}, messageID={}", currentUserID, messageID);
TbSysMessageUser userMessage = new TbSysMessageUser();
userMessage.setID(IDUtils.generateID());
userMessage.setMessageID(messageID);
userMessage.setUserID(currentUserID);
userMessage.setSendMethod("system"); // 默认发送方式
userMessage.setIsRead(false); // 初始为未读
userMessage.setSendStatus("success");
userMessage.setCreator(currentUserID);
userMessage.setCreateTime(new Date());
userMessage.setUpdateTime(new Date());
userMessage.setDeleted(false);
// 插入新记录
messageUserMapper.insertIfNotExists(userMessage);
// 重新查询以获取完整信息
messageUserVO = messageUserMapper.selectMyMessageDetail(currentUserID, messageID);
}
rt.success("查询成功", messageUserVO);
return rt;
} catch (Exception e) {
logger.error("查询消息详情失败", e);
rt.fail("查询消息详情失败:" + e.getMessage());
return rt;
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public ResultDomain<TbSysMessageUser> markAsRead(String messageID) {
ResultDomain<TbSysMessageUser> rt = new ResultDomain<>();
try {
String currentUserID = getCurrentUserID();
// 先尝试更新已有记录
int result = messageUserMapper.markAsRead(currentUserID, messageID);
// 如果没有更新任何记录,说明用户消息记录不存在,需要先插入
if (result == 0) {
logger.info("用户消息记录不存在创建新记录userID={}, messageID={}", currentUserID, messageID);
TbSysMessageUser userMessage = new TbSysMessageUser();
userMessage.setID(IDUtils.generateID());
userMessage.setMessageID(messageID);
userMessage.setUserID(currentUserID);
userMessage.setSendMethod("system"); // 默认发送方式
userMessage.setIsRead(true); // 直接设置为已读
userMessage.setSendStatus("success");
userMessage.setCreator(currentUserID);
userMessage.setCreateTime(new Date());
userMessage.setUpdateTime(new Date());
userMessage.setDeleted(false);
// 插入新记录
int insertResult = messageUserMapper.insertIfNotExists(userMessage);
if (insertResult > 0) {
// 插入成功后再次标记为已读(设置 read_time
messageUserMapper.markAsRead(currentUserID, messageID);
result = 1;
}
}
if (result > 0) {
// 更新消息的已读数量
messageMapper.incrementReadCount(messageID);
TbSysMessageUser messageUser = new TbSysMessageUser();
rt.success("标记成功", messageUser);
} else {
rt.fail("标记失败");
}
return rt;
} catch (Exception e) {
logger.error("标记已读失败", e);
rt.fail("标记已读失败:" + e.getMessage());
return rt;
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public ResultDomain<Integer> batchMarkAsRead(List<String> messageIDs) {
ResultDomain<Integer> rt = new ResultDomain<>();
try {
String currentUserID = getCurrentUserID();
int count = messageUserMapper.batchMarkAsRead(currentUserID, messageIDs);
// 更新每条消息的已读数量
for (String messageID : messageIDs) {
messageMapper.incrementReadCount(messageID);
}
rt.success("批量标记成功", count);
return rt;
} catch (Exception e) {
logger.error("批量标记已读失败", e);
rt.fail("批量标记已读失败:" + e.getMessage());
return rt;
}
}
@Override
public ResultDomain<Integer> getUnreadCount() {
ResultDomain<Integer> rt = new ResultDomain<>();
try {
String currentUserID = getCurrentUserID();
// 使用动态计算方法,统计用户应该看到的所有未读消息
Integer count = messageUserMapper.countUnreadWithDynamicTargets(currentUserID);
rt.success("查询成功", count != null ? count : 0);
return rt;
} catch (Exception e) {
logger.error("查询未读数量失败", e);
rt.fail("查询未读数量失败:" + e.getMessage());
return rt;
}
}
// ================== 辅助方法 ==================
@Override
public ResultDomain<Map<String, Object>> getTargetDepts() {
ResultDomain<Map<String, Object>> rt = new ResultDomain<>();
// TODO: 实现获取可选部门树
rt.success("查询成功", new HashMap<>());
return rt;
}
@Override
public ResultDomain<Map<String, Object>> getTargetRoles() {
ResultDomain<Map<String, Object>> rt = new ResultDomain<>();
// TODO: 实现获取可选角色列表
rt.success("查询成功", new HashMap<>());
return rt;
}
@Override
public ResultDomain<Map<String, Object>> getTargetUsers() {
ResultDomain<Map<String, Object>> rt = new ResultDomain<>();
// TODO: 实现获取可选用户列表
rt.success("查询成功", new HashMap<>());
return rt;
}
// ================== 私有辅助方法 ==================
/**
* 解析接收对象,生成用户消息列表
*/
private List<TbSysMessageUser> resolveTargetUsers(String messageID, List<TbSysMessageTarget> targets, String creator) {
Set<String> userIDSet = new HashSet<>();
Map<String, String> userMethodMap = new HashMap<>();
for (TbSysMessageTarget target : targets) {
List<String> userIDs = new ArrayList<>();
switch (target.getTargetType()) {
case "dept":
// 查询该部门及所有子部门的用户
userIDs = userMapper.selectUserIdsByDeptId(target.getTargetID());
logger.info("部门 {} 解析到 {} 个用户", target.getTargetID(), userIDs.size());
break;
case "role":
// 查询scopeDeptID及子部门中该角色的用户
String scopeDeptID = target.getScopeDeptID();
if (scopeDeptID == null || scopeDeptID.isEmpty()) {
logger.warn("角色目标缺少 scopeDeptID跳过{}", target.getTargetID());
break;
}
userIDs = userMapper.selectUserIdsByDeptRole(scopeDeptID, target.getTargetID());
logger.info("部门 {} 中角色 {} 解析到 {} 个用户", scopeDeptID, target.getTargetID(), userIDs.size());
break;
case "user":
userIDs.add(target.getTargetID());
break;
}
for (String userID : userIDs) {
userIDSet.add(userID);
userMethodMap.put(userID, target.getSendMethod());
}
}
// 生成用户消息列表
List<TbSysMessageUser> userMessages = new ArrayList<>();
for (String userID : userIDSet) {
TbSysMessageUser userMessage = new TbSysMessageUser();
userMessage.setID(IDUtils.generateID());
userMessage.setMessageID(messageID);
userMessage.setUserID(userID);
userMessage.setSendMethod(userMethodMap.get(userID));
userMessage.setIsRead(false);
userMessage.setSendStatus("pending");
userMessage.setCreator(creator);
userMessage.setCreateTime(new Date());
userMessage.setUpdateTime(new Date());
userMessage.setDeleted(false);
userMessages.add(userMessage);
}
logger.info("消息 {} 共解析到 {} 个目标用户", messageID, userMessages.size());
return userMessages;
}
/**
* 检查目标部门是否是当前部门或子部门
*/
private boolean isCurrentOrSubDept(String currentDeptID, String targetDeptID) {
// TODO: 实现部门层级检查
return true;
}
/**
* 获取当前用户ID
*/
private String getCurrentUserID() {
// TODO: 从SecurityContext或Session获取
return "1";
}
/**
* 获取当前用户部门ID
*/
private String getCurrentUserDeptID() {
// TODO: 从SecurityContext或Session获取
return "root_department";
}
}

View File

@@ -0,0 +1,44 @@
# Message模块配置文件
# 此配置文件定义message模块独立运行时的配置
# 如需作为微服务集成请参考admin模块的bootstrap.yml配置
# 消息模块配置
message:
# 定时任务扫描配置
scheduler:
# 扫描频率cron表达式默认每分钟扫描一次
cron: "0 * * * * ?"
# 是否启用定时任务扫描
enabled: true
# 消息发送配置
send:
# 异步发送线程池大小
thread-pool-size: 10
# 批量发送每批次大小
batch-size: 100
# Spring Boot配置如果需要独立运行
# server:
# port: 8087
#
# spring:
# application:
# name: message-service
#
# datasource:
# url: jdbc:mysql://localhost:3306/school_news?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
# username: root
# password: your_password
# driver-class-name: com.mysql.cj.jdbc.Driver
# hikari:
# maximum-pool-size: 20
# minimum-idle: 5
# connection-timeout: 30000
#
# mybatis-plus:
# mapper-locations: classpath:mapper/*.xml
# type-aliases-package: org.xyzh.common.dto.message
# configuration:
# map-underscore-to-camel-case: true
# log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl

View File

@@ -0,0 +1,279 @@
<?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="org.xyzh.message.mapper.MessageMapper">
<!-- Result Map -->
<resultMap id="BaseResultMap" type="org.xyzh.common.dto.message.TbSysMessage">
<id column="id" property="id" jdbcType="VARCHAR"/>
<result column="message_id" property="messageID" jdbcType="VARCHAR"/>
<result column="title" property="title" jdbcType="VARCHAR"/>
<result column="content" property="content" jdbcType="VARCHAR"/>
<result column="message_type" property="messageType" jdbcType="VARCHAR"/>
<result column="priority" property="priority" jdbcType="VARCHAR"/>
<result column="sender_id" property="senderID" jdbcType="VARCHAR"/>
<result column="sender_name" property="senderName" jdbcType="VARCHAR"/>
<result column="sender_dept_id" property="senderDeptID" jdbcType="VARCHAR"/>
<result column="sender_dept_name" property="senderDeptName" jdbcType="VARCHAR"/>
<result column="send_mode" property="sendMode" jdbcType="VARCHAR"/>
<result column="scheduled_time" property="scheduledTime" jdbcType="TIMESTAMP"/>
<result column="actual_send_time" property="actualSendTime" jdbcType="TIMESTAMP"/>
<result column="status" property="status" jdbcType="VARCHAR"/>
<result column="target_user_count" property="targetUserCount" jdbcType="INTEGER"/>
<result column="sent_count" property="sentCount" jdbcType="INTEGER"/>
<result column="success_count" property="successCount" jdbcType="INTEGER"/>
<result column="failed_count" property="failedCount" jdbcType="INTEGER"/>
<result column="read_count" property="readCount" jdbcType="INTEGER"/>
<result column="retry_count" property="retryCount" jdbcType="INTEGER"/>
<result column="max_retry_count" property="maxRetryCount" jdbcType="INTEGER"/>
<result column="last_error" property="lastError" jdbcType="VARCHAR"/>
<result column="creator" property="creator" jdbcType="VARCHAR"/>
<result column="updater" property="updater" jdbcType="VARCHAR"/>
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
<result column="update_time" property="updateTime" jdbcType="TIMESTAMP"/>
<result column="delete_time" property="deleteTime" jdbcType="TIMESTAMP"/>
<result column="deleted" property="deleted" jdbcType="BOOLEAN"/>
</resultMap>
<resultMap id="MessageVOMap" type="org.xyzh.common.dto.message.MessageVO">
<id column="id" property="id" jdbcType="VARCHAR"/>
<result column="message_id" property="messageID" jdbcType="VARCHAR"/>
<result column="title" property="title" jdbcType="VARCHAR"/>
<result column="content" property="content" jdbcType="VARCHAR"/>
<result column="message_type" property="messageType" jdbcType="VARCHAR"/>
<result column="priority" property="priority" jdbcType="VARCHAR"/>
<result column="sender_id" property="senderID" jdbcType="VARCHAR"/>
<result column="sender_name" property="senderName" jdbcType="VARCHAR"/>
<result column="sender_dept_id" property="senderDeptID" jdbcType="VARCHAR"/>
<result column="sender_dept_name" property="senderDeptName" jdbcType="VARCHAR"/>
<result column="send_mode" property="sendMode" jdbcType="VARCHAR"/>
<result column="scheduled_time" property="scheduledTime" jdbcType="TIMESTAMP"/>
<result column="actual_send_time" property="actualSendTime" jdbcType="TIMESTAMP"/>
<result column="status" property="status" jdbcType="VARCHAR"/>
<result column="target_user_count" property="targetUserCount" jdbcType="INTEGER"/>
<result column="sent_count" property="sentCount" jdbcType="INTEGER"/>
<result column="success_count" property="successCount" jdbcType="INTEGER"/>
<result column="failed_count" property="failedCount" jdbcType="INTEGER"/>
<result column="read_count" property="readCount" jdbcType="INTEGER"/>
<result column="retry_count" property="retryCount" jdbcType="INTEGER"/>
<result column="max_retry_count" property="maxRetryCount" jdbcType="INTEGER"/>
<result column="last_error" property="lastError" jdbcType="VARCHAR"/>
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
<result column="update_time" property="updateTime" jdbcType="TIMESTAMP"/>
</resultMap>
<!-- 分页查询消息列表(带权限过滤) -->
<select id="selectMessagePage" resultMap="MessageVOMap">
SELECT
m.*
FROM tb_sys_message m
WHERE m.deleted = 0
<if test="filter != null">
<if test="filter.title != null and filter.title != ''">
AND m.title LIKE CONCAT('%', #{filter.title}, '%')
</if>
<if test="filter.messageType != null and filter.messageType != ''">
AND m.message_type = #{filter.messageType}
</if>
<if test="filter.status != null and filter.status != ''">
AND m.status = #{filter.status}
</if>
<if test="filter.sendMode != null and filter.sendMode != ''">
AND m.send_mode = #{filter.sendMode}
</if>
<if test="filter.priority != null and filter.priority != ''">
AND m.priority = #{filter.priority}
</if>
</if>
<!-- 权限过滤:只能查看自己部门及子部门的消息 -->
AND (
m.sender_dept_id = #{currentUserDeptID}
OR m.sender_dept_id IN (
WITH RECURSIVE dept_tree AS (
SELECT dept_id FROM tb_sys_dept
WHERE dept_id = #{currentUserDeptID} AND deleted = 0
UNION ALL
SELECT d.dept_id FROM tb_sys_dept d
INNER JOIN dept_tree dt ON d.parent_id = dt.dept_id
WHERE d.deleted = 0
)
SELECT dept_id FROM dept_tree
)
)
ORDER BY m.create_time DESC
</select>
<!-- 查询消息详情 -->
<select id="selectMessageDetail" resultMap="MessageVOMap">
SELECT *
FROM tb_sys_message
WHERE message_id = #{messageID}
AND deleted = 0
</select>
<!-- 查询待发送的定时消息 -->
<select id="selectPendingScheduledMessages" resultMap="BaseResultMap">
SELECT *
FROM tb_sys_message
WHERE status = 'pending'
AND send_mode = 'scheduled'
AND scheduled_time <![CDATA[ <= ]]> #{currentTime}
AND deleted = 0
ORDER BY scheduled_time ASC
LIMIT 100
</select>
<!-- CAS更新消息状态 -->
<update id="compareAndSetStatus">
UPDATE tb_sys_message
SET status = #{newStatus},
update_time = NOW()
WHERE message_id = #{messageID}
AND status = #{expectedStatus}
AND deleted = 0
</update>
<!-- 更新消息统计信息 -->
<update id="updateStatistics">
UPDATE tb_sys_message
SET sent_count = #{sentCount},
success_count = #{successCount},
failed_count = #{failedCount},
update_time = NOW()
WHERE message_id = #{messageID}
AND deleted = 0
</update>
<!-- 更新已读数量 -->
<update id="incrementReadCount">
UPDATE tb_sys_message
SET read_count = read_count + 1,
update_time = NOW()
WHERE message_id = #{messageID}
AND deleted = 0
</update>
<!-- 根据消息ID查询消息 -->
<select id="selectMessageById" resultMap="BaseResultMap">
SELECT *
FROM tb_sys_message
WHERE message_id = #{messageID}
AND deleted = 0
</select>
<!-- 插入消息 -->
<insert id="insertMessage">
INSERT INTO tb_sys_message
(id, message_id, title, content, message_type, priority, sender_id, sender_dept_id,
send_mode, scheduled_time, status, target_user_count, retry_count, max_retry_count,
creator, create_time, update_time, deleted)
VALUES
(#{message.id}, #{message.messageID}, #{message.title}, #{message.content},
#{message.messageType}, #{message.priority}, #{message.senderID}, #{message.senderDeptID},
#{message.sendMode}, #{message.scheduledTime}, #{message.status}, #{message.targetUserCount},
#{message.retryCount}, #{message.maxRetryCount}, #{message.creator}, #{message.createTime},
#{message.updateTime}, #{message.deleted})
</insert>
<!-- 更新消息 -->
<update id="updateMessage">
UPDATE tb_sys_message
<set>
<if test="message.title != null and message.title != ''">
title = #{message.title},
</if>
<if test="message.content != null">
content = #{message.content},
</if>
<if test="message.messageType != null and message.messageType != ''">
message_type = #{message.messageType},
</if>
<if test="message.priority != null and message.priority != ''">
priority = #{message.priority},
</if>
<if test="message.sendMode != null and message.sendMode != ''">
send_mode = #{message.sendMode},
</if>
<if test="message.scheduledTime != null">
scheduled_time = #{message.scheduledTime},
</if>
<if test="message.actualSendTime != null">
actual_send_time = #{message.actualSendTime},
</if>
<if test="message.status != null and message.status != ''">
status = #{message.status},
</if>
<if test="message.targetUserCount != null">
target_user_count = #{message.targetUserCount},
</if>
<if test="message.retryCount != null">
retry_count = #{message.retryCount},
</if>
<if test="message.maxRetryCount != null">
max_retry_count = #{message.maxRetryCount},
</if>
<if test="message.lastError != null">
last_error = #{message.lastError},
</if>
<if test="message.updater != null and message.updater != ''">
updater = #{message.updater},
</if>
<if test="message.updateTime != null">
update_time = #{message.updateTime},
</if>
</set>
WHERE message_id = #{message.messageID}
AND deleted = 0
</update>
<!-- 删除消息(逻辑删除) -->
<update id="deleteMessage">
UPDATE tb_sys_message
SET deleted = 1,
delete_time = NOW(),
update_time = NOW()
WHERE message_id = #{messageID}
AND deleted = 0
</update>
<!-- 统计消息总数(带权限过滤) -->
<select id="countMessage" resultType="java.lang.Integer">
SELECT COUNT(*)
FROM tb_sys_message m
WHERE m.deleted = 0
<if test="filter != null">
<if test="filter.title != null and filter.title != ''">
AND m.title LIKE CONCAT('%', #{filter.title}, '%')
</if>
<if test="filter.messageType != null and filter.messageType != ''">
AND m.message_type = #{filter.messageType}
</if>
<if test="filter.status != null and filter.status != ''">
AND m.status = #{filter.status}
</if>
<if test="filter.sendMode != null and filter.sendMode != ''">
AND m.send_mode = #{filter.sendMode}
</if>
<if test="filter.priority != null and filter.priority != ''">
AND m.priority = #{filter.priority}
</if>
</if>
<!-- 权限过滤:只能查看自己部门及子部门的消息 -->
AND (
m.sender_dept_id = #{currentUserDeptID}
OR m.sender_dept_id IN (
WITH RECURSIVE dept_tree AS (
SELECT dept_id FROM tb_sys_dept
WHERE dept_id = #{currentUserDeptID} AND deleted = 0
UNION ALL
SELECT d.dept_id FROM tb_sys_dept d
INNER JOIN dept_tree dt ON d.parent_id = dt.dept_id
WHERE d.deleted = 0
)
SELECT dept_id FROM dept_tree
)
)
</select>
</mapper>

View File

@@ -0,0 +1,54 @@
<?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="org.xyzh.message.mapper.MessageTargetMapper">
<!-- Result Map -->
<resultMap id="BaseResultMap" type="org.xyzh.common.dto.message.TbSysMessageTarget">
<id column="id" property="id" jdbcType="VARCHAR"/>
<result column="message_id" property="messageID" jdbcType="VARCHAR"/>
<result column="send_method" property="sendMethod" jdbcType="VARCHAR"/>
<result column="target_type" property="targetType" jdbcType="VARCHAR"/>
<result column="target_id" property="targetID" jdbcType="VARCHAR"/>
<result column="target_name" property="targetName" jdbcType="VARCHAR"/>
<result column="scope_dept_id" property="scopeDeptID" jdbcType="VARCHAR"/>
<result column="creator" property="creator" jdbcType="VARCHAR"/>
<result column="updater" property="updater" jdbcType="VARCHAR"/>
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
<result column="update_time" property="updateTime" jdbcType="TIMESTAMP"/>
<result column="delete_time" property="deleteTime" jdbcType="TIMESTAMP"/>
<result column="deleted" property="deleted" jdbcType="BOOLEAN"/>
</resultMap>
<!-- 根据消息ID查询接收对象列表 -->
<select id="selectByMessageID" resultMap="BaseResultMap">
SELECT *
FROM tb_sys_message_target
WHERE message_id = #{messageID}
AND deleted = 0
ORDER BY create_time ASC
</select>
<!-- 批量插入接收对象 -->
<insert id="batchInsert">
INSERT INTO tb_sys_message_target
(id, message_id, send_method, target_type, target_id, target_name, scope_dept_id, creator, create_time, update_time, deleted)
VALUES
<foreach collection="targets" item="item" separator=",">
(#{item.id}, #{item.messageID}, #{item.sendMethod}, #{item.targetType}, #{item.targetID},
#{item.targetName}, #{item.scopeDeptID}, #{item.creator}, NOW(), NOW(), 0)
</foreach>
</insert>
<!-- 根据消息ID删除接收对象 -->
<update id="deleteByMessageID">
UPDATE tb_sys_message_target
SET deleted = 1,
delete_time = NOW(),
update_time = NOW()
WHERE message_id = #{messageID}
AND deleted = 0
</update>
</mapper>

View File

@@ -0,0 +1,352 @@
<?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="org.xyzh.message.mapper.MessageUserMapper">
<!-- Result Map -->
<resultMap id="BaseResultMap" type="org.xyzh.common.dto.message.TbSysMessageUser">
<id column="id" property="id" jdbcType="VARCHAR"/>
<result column="message_id" property="messageID" jdbcType="VARCHAR"/>
<result column="user_id" property="userID" jdbcType="VARCHAR"/>
<result column="send_method" property="sendMethod" jdbcType="VARCHAR"/>
<result column="is_read" property="isRead" jdbcType="BOOLEAN"/>
<result column="read_time" property="readTime" jdbcType="TIMESTAMP"/>
<result column="send_status" property="sendStatus" jdbcType="VARCHAR"/>
<result column="fail_reason" property="failReason" jdbcType="VARCHAR"/>
<result column="creator" property="creator" jdbcType="VARCHAR"/>
<result column="updater" property="updater" jdbcType="VARCHAR"/>
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
<result column="update_time" property="updateTime" jdbcType="TIMESTAMP"/>
<result column="delete_time" property="deleteTime" jdbcType="TIMESTAMP"/>
<result column="deleted" property="deleted" jdbcType="BOOLEAN"/>
</resultMap>
<resultMap id="MessageUserVOMap" type="org.xyzh.common.dto.message.MessageUserVO">
<id column="id" property="id" jdbcType="VARCHAR"/>
<result column="message_id" property="messageID" jdbcType="VARCHAR"/>
<result column="user_id" property="userID" jdbcType="VARCHAR"/>
<result column="username" property="username" jdbcType="VARCHAR"/>
<result column="full_name" property="fullName" jdbcType="VARCHAR"/>
<result column="dept_id" property="deptID" jdbcType="VARCHAR"/>
<result column="name" property="deptName" jdbcType="VARCHAR"/>
<result column="send_method" property="sendMethod" jdbcType="VARCHAR"/>
<result column="is_read" property="isRead" jdbcType="BOOLEAN"/>
<result column="read_time" property="readTime" jdbcType="TIMESTAMP"/>
<result column="send_status" property="sendStatus" jdbcType="VARCHAR"/>
<result column="fail_reason" property="failReason" jdbcType="VARCHAR"/>
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
<result column="title" property="title" jdbcType="VARCHAR"/>
<result column="content" property="content" jdbcType="VARCHAR"/>
<result column="message_type" property="messageType" jdbcType="VARCHAR"/>
<result column="priority" property="priority" jdbcType="VARCHAR"/>
<result column="sender_name" property="senderName" jdbcType="VARCHAR"/>
<result column="sender_dept_name" property="senderDeptName" jdbcType="VARCHAR"/>
<result column="actual_send_time" property="actualSendTime" jdbcType="TIMESTAMP"/>
</resultMap>
<!-- 根据消息ID查询用户消息列表 -->
<select id="selectByMessageID" resultMap="MessageUserVOMap">
SELECT
mu.*,
u.username,
ui.full_name as full_name,
d.dept_id as dept_id,
d.name as deptName
FROM tb_sys_message_user mu
LEFT JOIN tb_sys_user u ON mu.user_id = u.id
LEFT JOIN tb_sys_user_info ui ON u.id = ui.user_id
LEFT JOIN tb_sys_user_dept_role udr ON u.id = udr.user_id AND udr.deleted = 0
LEFT JOIN tb_sys_dept d ON udr.dept_id = d.dept_id AND d.deleted = 0
WHERE mu.message_id = #{messageID}
AND mu.deleted = 0
ORDER BY mu.create_time DESC
</select>
<!-- 批量插入用户消息 -->
<insert id="batchInsert">
INSERT INTO tb_sys_message_user
(id, message_id, user_id, send_method, is_read, send_status, creator, create_time, update_time, deleted)
VALUES
<foreach collection="userMessages" item="item" separator=",">
(#{item.id}, #{item.messageID}, #{item.userID}, #{item.sendMethod},
0, 'pending', #{item.creator}, NOW(), NOW(), 0)
</foreach>
</insert>
<!-- 分页查询当前用户的消息列表 -->
<select id="selectMyMessages" resultMap="MessageUserVOMap">
SELECT
mu.*,
m.title,
m.content,
m.message_type as message_type,
m.priority,
m.sender_name as sender_name,
m.sender_dept_name as sender_dept_name,
m.actual_send_time as actual_send_time
FROM tb_sys_message_user mu
INNER JOIN tb_sys_message m ON mu.message_id = m.message_id
WHERE mu.user_id = #{userID}
AND mu.deleted = 0
AND m.deleted = 0
<if test="filter != null">
<if test="filter.isRead != null">
AND mu.is_read = #{filter.isRead}
</if>
<if test="filter.sendMethod != null and filter.sendMethod != ''">
AND mu.send_method = #{filter.sendMethod}
</if>
</if>
ORDER BY m.actual_send_time DESC, mu.create_time DESC
</select>
<!-- 查询当前用户的消息详情 -->
<select id="selectMyMessageDetail" resultMap="MessageUserVOMap">
SELECT
mu.*,
m.title,
m.content,
m.message_type as message_type,
m.priority,
m.sender_name as sender_name,
m.sender_dept_name as sender_dept_name,
m.actual_send_time as actual_send_time
FROM tb_sys_message_user mu
INNER JOIN tb_sys_message m ON mu.message_id = m.message_id
WHERE mu.user_id = #{userID}
AND mu.message_id = #{messageID}
AND mu.deleted = 0
AND m.deleted = 0
</select>
<!-- 标记消息为已读 -->
<update id="markAsRead">
UPDATE tb_sys_message_user
SET is_read = 1,
send_status = 'sent',
read_time = NOW(),
update_time = NOW()
WHERE user_id = #{userID}
AND message_id = #{messageID}
AND deleted = 0
</update>
<!-- 批量标记消息为已读 -->
<update id="batchMarkAsRead">
UPDATE tb_sys_message_user
SET is_read = 1,
read_time = NOW(),
update_time = NOW()
WHERE user_id = #{userID}
AND message_id IN
<foreach collection="messageIDs" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
AND deleted = 0
</update>
<!-- 查询未读消息数量 -->
<select id="countUnread" resultType="java.lang.Integer">
SELECT COUNT(*)
FROM tb_sys_message_user
WHERE user_id = #{userID}
AND is_read = 0
AND deleted = 0
</select>
<!-- 动态计算未读消息数量(基于 target 配置) -->
<select id="countUnreadWithDynamicTargets" resultType="java.lang.Integer">
SELECT COUNT(DISTINCT m.message_id)
FROM tb_sys_message m
INNER JOIN tb_sys_message_target mt ON m.message_id = mt.message_id AND mt.deleted = 0
LEFT JOIN tb_sys_message_user mu ON m.message_id = mu.message_id AND mu.user_id = #{userID} AND mu.deleted = 0
WHERE m.deleted = 0
AND m.status IN ('sent', 'sending', 'failed')
AND COALESCE(mu.is_read, 0) = 0
AND (
-- 用户类型目标:直接匹配
(mt.target_type = 'user' AND mt.target_id = #{userID})
OR
-- 部门类型目标:用户所在部门是目标部门或其子部门
(mt.target_type = 'dept' AND EXISTS (
SELECT 1 FROM tb_sys_user_dept_role udr
INNER JOIN tb_sys_dept d ON udr.dept_id = d.dept_id AND d.deleted = 0
INNER JOIN tb_sys_dept target_dept ON target_dept.dept_id = mt.target_id AND target_dept.deleted = 0
WHERE udr.user_id = #{userID}
AND udr.deleted = 0
AND (
d.dept_id = target_dept.dept_id
OR d.dept_path LIKE CONCAT(target_dept.dept_path, '%')
)
))
OR
-- 角色类型目标:用户在指定部门范围内拥有该角色
(mt.target_type = 'role' AND EXISTS (
SELECT 1 FROM tb_sys_user_dept_role udr
INNER JOIN tb_sys_dept d ON udr.dept_id = d.dept_id AND d.deleted = 0
WHERE udr.user_id = #{userID}
AND udr.deleted = 0
AND udr.role_id = mt.target_id
AND d.dept_path LIKE CONCAT(
(SELECT dept_path FROM tb_sys_dept WHERE dept_id = mt.scope_dept_id AND deleted = 0),
'%'
)
))
)
</select>
<!-- 更新用户消息的发送状态 -->
<update id="updateSendStatus">
UPDATE tb_sys_message_user
SET send_status = #{sendStatus},
<if test="failReason != null and failReason != ''">
fail_reason = #{failReason},
</if>
update_time = NOW()
WHERE id = #{id}
AND deleted = 0
</update>
<!-- 查询待发送的用户消息列表 -->
<select id="selectPendingUserMessages" resultMap="BaseResultMap">
SELECT *
FROM tb_sys_message_user
WHERE deleted = 0
<if test="filter != null">
<if test="filter.messageID != null and filter.messageID != ''">
AND message_id = #{filter.messageID}
</if>
<if test="filter.sendStatus != null and filter.sendStatus != ''">
AND send_status = #{filter.sendStatus}
</if>
<if test="filter.deleted != null">
AND deleted = #{filter.deleted}
</if>
</if>
ORDER BY create_time ASC
</select>
<!-- 动态查询当前用户可见的消息列表(基于 target 配置计算) -->
<select id="selectMyMessagesWithDynamicTargets" resultMap="MessageUserVOMap">
SELECT
m.message_id,
m.title,
m.content,
m.message_type,
m.priority,
m.sender_name,
m.sender_dept_name,
m.actual_send_time,
MAX(COALESCE(mu.is_read, 0)) as is_read,
MAX(mu.read_time) as read_time,
MAX(COALESCE(mu.send_status, 'pending')) as send_status,
MAX(mu.id) as id,
MAX(mu.user_id) as user_id,
MAX(mu.send_method) as send_method,
MAX(mu.fail_reason) as fail_reason,
MAX(mu.create_time) as create_time
FROM tb_sys_message m
INNER JOIN tb_sys_message_target mt ON m.message_id = mt.message_id AND mt.deleted = 0
LEFT JOIN tb_sys_message_user mu ON m.message_id = mu.message_id AND mu.user_id = #{userID} AND mu.deleted = 0
WHERE m.deleted = 0
AND m.status IN ('sent', 'sending', 'failed')
AND (
-- 用户类型目标:直接匹配
(mt.target_type = 'user' AND mt.target_id = #{userID})
OR
-- 部门类型目标:用户所在部门是目标部门或其子部门
(mt.target_type = 'dept' AND EXISTS (
SELECT 1 FROM tb_sys_user_dept_role udr
INNER JOIN tb_sys_dept d ON udr.dept_id = d.dept_id AND d.deleted = 0
INNER JOIN tb_sys_dept target_dept ON target_dept.dept_id = mt.target_id AND target_dept.deleted = 0
WHERE udr.user_id = #{userID}
AND udr.deleted = 0
AND (
d.dept_id = target_dept.dept_id
OR d.dept_path LIKE CONCAT(target_dept.dept_path, '%')
)
))
OR
-- 角色类型目标:用户在指定部门范围内拥有该角色
(mt.target_type = 'role' AND EXISTS (
SELECT 1 FROM tb_sys_user_dept_role udr
INNER JOIN tb_sys_dept d ON udr.dept_id = d.dept_id AND d.deleted = 0
WHERE udr.user_id = #{userID}
AND udr.deleted = 0
AND udr.role_id = mt.target_id
AND d.dept_path LIKE CONCAT(
(SELECT dept_path FROM tb_sys_dept WHERE dept_id = mt.scope_dept_id AND deleted = 0),
'%'
)
))
)
<if test="filter != null">
<if test="filter.isRead != null">
AND COALESCE(mu.is_read, 0) = #{filter.isRead}
</if>
<if test="filter.sendMethod != null and filter.sendMethod != ''">
AND mt.send_method = #{filter.sendMethod}
</if>
<if test="filter.sendStatus != null and filter.sendStatus != ''">
AND mu.send_status = #{filter.sendStatus}
</if>
<if test="filter.priority != null and filter.priority != ''">
AND m.priority = #{filter.priority}
</if>
<if test="filter.messageType != null and filter.messageType != ''">
AND m.message_type = #{filter.messageType}
</if>
<if test="filter.title != null and filter.title != ''">
AND m.title LIKE CONCAT('%', #{filter.title}, '%')
</if>
</if>
GROUP BY m.message_id, m.title, m.content, m.message_type, m.priority,
m.sender_name, m.sender_dept_name, m.actual_send_time
ORDER BY m.actual_send_time DESC, m.create_time DESC
</select>
<!-- 查询或创建用户消息记录 -->
<select id="selectOrCreateUserMessage" resultMap="MessageUserVOMap">
SELECT
mu.*,
m.title,
m.content,
m.message_type,
m.priority,
m.sender_name,
m.sender_dept_name,
m.actual_send_time
FROM tb_sys_message m
LEFT JOIN tb_sys_message_user mu ON m.message_id = mu.message_id
AND mu.user_id = #{userID}
AND mu.deleted = 0
WHERE m.message_id = #{messageID}
AND m.deleted = 0
</select>
<!-- 插入用户消息记录(如果不存在) -->
<insert id="insertIfNotExists">
INSERT INTO tb_sys_message_user
(id, message_id, user_id, send_method, is_read, send_status, creator, create_time, update_time, deleted)
SELECT
#{userMessage.id},
#{userMessage.messageID},
#{userMessage.userID},
#{userMessage.sendMethod},
0,
'pending',
#{userMessage.creator},
NOW(),
NOW(),
0
FROM DUAL
WHERE NOT EXISTS (
SELECT 1 FROM tb_sys_message_user
WHERE message_id = #{userMessage.messageID}
AND user_id = #{userMessage.userID}
AND deleted = 0
)
</insert>
</mapper>