wlzboy
2025-10-25 fefb649f462ae6b19dd8f0f6bc6096619db9a82e
feat:消息体推送
3个文件已修改
19个文件已添加
3487 ■■■■■ 已修改文件
app/api/message.js 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/pages/index.vue 23 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/pages/message/index.vue 75 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
prd/事件驱动消息推送-实现总结.md 508 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
prd/事件驱动消息推送-快速开始.md 366 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
prd/系统消息推送-快速开始.md 189 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
prd/系统消息推送-文件变更清单.md 245 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
prd/系统消息推送功能实现总结.md 305 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysMessageController.java 138 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-framework/src/main/java/com/ruoyi/framework/config/AsyncConfig.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/domain/SysMessage.java 186 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/event/TaskAssignedEvent.java 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/event/TaskCreatedEvent.java 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/event/TaskEvent.java 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/event/TaskStatusChangedEvent.java 93 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/listener/TaskMessageListener.java 224 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysMessageMapper.java 94 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/ISysMessageService.java 120 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysMessageServiceImpl.java 351 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskServiceImpl.java 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/resources/mapper/system/SysMessageMapper.xml 137 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/sys_message.sql 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/api/message.js
New file
@@ -0,0 +1,76 @@
import request from '@/utils/request'
// æŸ¥è¯¢å½“前用户的消息列表
export function getMyMessages() {
  return request({
    url: '/system/message/my',
    method: 'get'
  })
}
// æŸ¥è¯¢ç³»ç»Ÿæ¶ˆæ¯åˆ—表
export function listMessage(query) {
  return request({
    url: '/system/message/list',
    method: 'get',
    params: query
  })
}
// æŸ¥è¯¢ç³»ç»Ÿæ¶ˆæ¯è¯¦ç»†
export function getMessage(messageId) {
  return request({
    url: '/system/message/' + messageId,
    method: 'get'
  })
}
// æŸ¥è¯¢æœªè¯»æ¶ˆæ¯æ•°é‡
export function getUnreadCount() {
  return request({
    url: '/system/message/unread/count',
    method: 'get'
  })
}
// æ–°å¢žç³»ç»Ÿæ¶ˆæ¯
export function addMessage(data) {
  return request({
    url: '/system/message',
    method: 'post',
    data: data
  })
}
// ä¿®æ”¹ç³»ç»Ÿæ¶ˆæ¯
export function updateMessage(data) {
  return request({
    url: '/system/message',
    method: 'put',
    data: data
  })
}
// åˆ é™¤ç³»ç»Ÿæ¶ˆæ¯
export function delMessage(messageId) {
  return request({
    url: '/system/message/' + messageId,
    method: 'delete'
  })
}
// æ ‡è®°æ¶ˆæ¯ä¸ºå·²è¯»
export function markAsRead(messageId) {
  return request({
    url: '/system/message/read/' + messageId,
    method: 'put'
  })
}
// æ ‡è®°æ‰€æœ‰æ¶ˆæ¯ä¸ºå·²è¯»
export function markAllAsRead() {
  return request({
    url: '/system/message/read/all',
    method: 'put'
  })
}
app/pages/index.vue
@@ -162,6 +162,7 @@
  import { getMyTasks, changeTaskStatus } from '@/api/task'
  import { getUserProfile } from '@/api/system/user'
  import { getUserBoundVehicle } from '@/api/vehicle'
  import { getUnreadCount } from '@/api/message'
  
  export default {
    data() {
@@ -172,6 +173,7 @@
        
        // æ¶ˆæ¯æ•°æ®
        messages: [],
        unreadMessageCount: 0,
        
        // æ­£åœ¨è¿è¡Œçš„任务列表
        taskList: [],
@@ -190,11 +192,6 @@
          // åŒ…含待处理、出发中、已到达、返程中等所有未完成的状态
          return ['PENDING', 'DEPARTING', 'ARRIVED', 'RETURNING', 'IN_PROGRESS'].includes(task.taskStatus)
        });
      },
      // æœªè¯»æ¶ˆæ¯æ•°é‡
      unreadMessageCount() {
        return this.messages.filter(message => !message.read).length;
      }
    },
    onLoad() {
@@ -202,11 +199,14 @@
      this.loadUserVehicle()
      // åŠ è½½æ­£åœ¨è¿è¡Œçš„ä»»åŠ¡
      this.loadRunningTasks()
      // åŠ è½½æœªè¯»æ¶ˆæ¯æ•°é‡
      this.loadUnreadMessageCount()
    },
    onShow() {
      // æ¯æ¬¡æ˜¾ç¤ºé¡µé¢æ—¶åˆ·æ–°ä»»åŠ¡åˆ—è¡¨å’Œç»‘å®šè½¦è¾†
      // æ¯æ¬¡æ˜¾ç¤ºé¡µé¢æ—¶åˆ·æ–°ä»»åŠ¡åˆ—è¡¨ã€ç»‘å®šè½¦è¾†å’Œæ¶ˆæ¯æ•°é‡
      this.loadUserVehicle()
      this.loadRunningTasks()
      this.loadUnreadMessageCount()
    },
    onPullDownRefresh() {
      // ä¸‹æ‹‰åˆ·æ–°
@@ -243,6 +243,17 @@
        })
      },
      
      // åŠ è½½æœªè¯»æ¶ˆæ¯æ•°é‡
      loadUnreadMessageCount() {
        getUnreadCount().then(response => {
          if (response.code === 200) {
            this.unreadMessageCount = response.data || 0
          }
        }).catch(error => {
          console.error('获取未读消息数量失败:', error)
        })
      },
      // åŠ è½½ç”¨æˆ·ä¿¡æ¯ï¼ˆä¿ç•™ä»¥å…¼å®¹ä¹‹å‰çš„ä»£ç ï¼‰
      loadUserProfile() {
        const userId = this.currentUser.userId
app/pages/message/index.vue
@@ -15,11 +15,11 @@
        >
          <view class="message-main">
            <view class="message-title">
              <text class="title-text">{{ getMessageTypeText(message.type) }}</text>
              <view class="unread-dot" v-if="!message.read"></view>
              <text class="title-text">{{ getMessageTypeText(message.messageType) }}</text>
              <view class="unread-dot" v-if="message.isRead === '0'"></view>
            </view>
            <view class="message-content">{{ message.content }}</view>
            <view class="message-time">{{ message.time }}</view>
            <view class="message-content">{{ message.messageContent }}</view>
            <view class="message-time">{{ message.createTime }}</view>
          </view>
        </view>
        
@@ -33,47 +33,92 @@
</template>
<script>
  import { getMyMessages, markAsRead } from '@/api/message'
  export default {
    data() {
      return {
        // æ¶ˆæ¯åˆ—表
        messages: []
        messages: [],
        loading: false
      }
    },
    computed: {
      // æŒ‰æœªè¯»/已读排序,未读的在前面
      sortedMessages() {
        return [...this.messages].sort((a, b) => {
          if (a.read === b.read) {
          if (a.isRead === b.isRead) {
            // ç›¸åŒçŠ¶æ€æŒ‰æ—¶é—´å€’åº
            return new Date(b.time) - new Date(a.time);
            return new Date(b.createTime) - new Date(a.createTime);
          }
          // æœªè¯»çš„æŽ’在前面
          return a.read ? 1 : -1;
          return a.isRead === '0' ? -1 : 1;
        });
      }
    },
    onLoad() {
      this.loadMessages()
    },
    onShow() {
      // æ¯æ¬¡æ˜¾ç¤ºé¡µé¢æ—¶åˆ·æ–°æ¶ˆæ¯
      this.loadMessages()
    },
    onPullDownRefresh() {
      this.loadMessages().then(() => {
        uni.stopPullDownRefresh()
      })
    },
    methods: {
      // åŠ è½½æ¶ˆæ¯åˆ—è¡¨
      async loadMessages() {
        try {
          this.loading = true
          const response = await getMyMessages()
          if (response.code === 200) {
            this.messages = response.data || []
          } else {
            this.$modal.showToast(response.msg || '加载消息失败')
          }
        } catch (error) {
          console.error('加载消息失败:', error)
          this.$modal.showToast('加载消息失败')
        } finally {
          this.loading = false
        }
      },
      // èŽ·å–æ¶ˆæ¯ç±»åž‹æ–‡æœ¬
      getMessageTypeText(type) {
        const typeMap = {
          'create': '创建成功',
          'push': '任务推送',
          'status': '任务状态'
          'CREATE': '创建成功',
          'PUSH': '任务推送',
          'STATUS': '状态变更',
          'ASSIGN': '任务分配'
        }
        return typeMap[type] || '系统消息';
      },
      
      // æŸ¥çœ‹æ¶ˆæ¯è¯¦æƒ…(跳转到任务详情)
      viewMessageDetail(message) {
      async viewMessageDetail(message) {
        try {
        // æ ‡è®°ä¸ºå·²è¯»
        message.read = true;
          if (message.isRead === '0') {
            await markAsRead(message.messageId)
            message.isRead = '1'
          }
        
        // è·³è½¬åˆ°ä»»åŠ¡è¯¦æƒ…é¡µé¢
        if (message.taskId) {
          this.$tab.navigateTo(`/pages/task/detail?id=${message.taskId}`);
            this.$tab.navigateTo(`/pages/task/detail?id=${message.taskId}`)
        } else {
          this.$modal.showToast('无法找到关联任务');
            this.$modal.showToast('无法找到关联任务')
          }
        } catch (error) {
          console.error('标记消息已读失败:', error)
          // å³ä½¿æ ‡è®°å¤±è´¥ï¼Œä¹Ÿå…è®¸è·³è½¬
          if (message.taskId) {
            this.$tab.navigateTo(`/pages/task/detail?id=${message.taskId}`)
          }
        }
      }
    }
prd/ʼþÇý¶¯ÏûÏ¢ÍÆËÍ-ʵÏÖ×ܽá.md
New file
@@ -0,0 +1,508 @@
# äº‹ä»¶é©±åŠ¨æ¶ˆæ¯æŽ¨é€ç³»ç»Ÿ - å®žçŽ°æ€»ç»“
## åŠŸèƒ½æ¦‚è¿°
采用Spring事件驱动架构重构消息推送功能,实现业务逻辑与消息推送的完全解耦,使其他系统可以轻松发布事件来触发消息推送。
## æž¶æž„设计
### è®¾è®¡æ¨¡å¼
采用**观察者模式**(Observer Pattern)的Spring事件机制实现:
```
业务系统 --发布--> äº‹ä»¶ --监听--> æ¶ˆæ¯ç›‘听器 --保存--> æ¶ˆæ¯åº“
```
### æ ¸å¿ƒç»„ä»¶
```mermaid
graph LR
    A[任务服务] -->|发布事件| B[ApplicationEventPublisher]
    B --> C[TaskCreatedEvent]
    B --> D[TaskAssignedEvent]
    B --> E[TaskStatusChangedEvent]
    C -->|监听| F[TaskMessageListener]
    D -->|监听| F
    E -->|监听| F
    F -->|保存消息| G[sys_message表]
```
## å®žçް内容
### 1. äº‹ä»¶ç±»è®¾è®¡
#### 1.1 äº‹ä»¶åŸºç±»
**文件**: `ruoyi-system/src/main/java/com/ruoyi/system/event/TaskEvent.java`
```java
public abstract class TaskEvent extends ApplicationEvent {
    private Long taskId;          // ä»»åŠ¡ID
    private String taskCode;      // ä»»åŠ¡ç¼–å·
    private Long operatorId;      // æ“ä½œäººID
    private String operatorName;  // æ“ä½œäººå§“名
}
```
#### 1.2 ä»»åŠ¡åˆ›å»ºäº‹ä»¶
**文件**: `ruoyi-system/src/main/java/com/ruoyi/system/event/TaskCreatedEvent.java`
```java
public class TaskCreatedEvent extends TaskEvent {
    private String taskType;      // ä»»åŠ¡ç±»åž‹
    private Long creatorId;       // åˆ›å»ºäººID
    private String creatorName;   // åˆ›å»ºäººå§“名
}
```
#### 1.3 ä»»åŠ¡åˆ†é…äº‹ä»¶
**文件**: `ruoyi-system/src/main/java/com/ruoyi/system/event/TaskAssignedEvent.java`
```java
public class TaskAssignedEvent extends TaskEvent {
    private List<Long> assigneeIds;     // æ‰§è¡ŒäººID列表
    private List<String> assigneeNames; // æ‰§è¡Œäººå§“名列表
    private Long assignerId;            // åˆ†é…äººID
    private String assignerName;        // åˆ†é…äººå§“名
}
```
#### 1.4 ä»»åŠ¡çŠ¶æ€å˜æ›´äº‹ä»¶
**文件**: `ruoyi-system/src/main/java/com/ruoyi/system/event/TaskStatusChangedEvent.java`
```java
public class TaskStatusChangedEvent extends TaskEvent {
    private String oldStatus;         // æ—§çŠ¶æ€
    private String newStatus;         // æ–°çŠ¶æ€
    private String oldStatusDesc;     // æ—§çŠ¶æ€æè¿°
    private String newStatusDesc;     // æ–°çŠ¶æ€æè¿°
    private List<Long> assigneeIds;   // æ‰§è¡ŒäººID列表
    private Long creatorId;           // åˆ›å»ºäººID
}
```
### 2. æ¶ˆæ¯ç›‘听器
**文件**: `ruoyi-system/src/main/java/com/ruoyi/system/listener/TaskMessageListener.java`
#### 2.1 æ ¸å¿ƒç‰¹æ€§
- âœ… **异步处理**: ä½¿ç”¨ `@Async` æ³¨è§£,不阻塞主业务
- âœ… **事件监听**: ä½¿ç”¨ `@EventListener` æ³¨è§£ç›‘听事件
- âœ… **自动保存**: ç›‘听到事件后自动保存消息到数据库
- âœ… **容错处理**: å¼‚常不影响主业务流程
#### 2.2 ç›‘听器方法
```java
@Component
public class TaskMessageListener {
    // ç›‘听任务创建事件
    @Async
    @EventListener
    public void handleTaskCreatedEvent(TaskCreatedEvent event) {
        // ä¿å­˜åˆ›å»ºæˆåŠŸæ¶ˆæ¯
    }
    // ç›‘听任务分配事件
    @Async
    @EventListener
    public void handleTaskAssignedEvent(TaskAssignedEvent event) {
        // ç»™æ¯ä¸ªæ‰§è¡Œäººå‘送任务推送消息
    }
    // ç›‘听任务状态变更事件
    @Async
    @EventListener
    public void handleTaskStatusChangedEvent(TaskStatusChangedEvent event) {
        // ç»™æ‰§è¡Œäººå’Œåˆ›å»ºäººå‘送状态变更消息
    }
}
```
### 3. å¼‚步配置
**文件**: `ruoyi-framework/src/main/java/com/ruoyi/framework/config/AsyncConfig.java`
```java
@Configuration
@EnableAsync
public class AsyncConfig {
    @Bean(name = "taskExecutor")
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);         // æ ¸å¿ƒçº¿ç¨‹æ•°
        executor.setMaxPoolSize(10);         // æœ€å¤§çº¿ç¨‹æ•°
        executor.setQueueCapacity(100);      // é˜Ÿåˆ—容量
        executor.setThreadNamePrefix("async-task-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
}
```
### 4. ä»»åŠ¡æœåŠ¡é›†æˆ
**文件**: `ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskServiceImpl.java`
#### 4.1 æ³¨å…¥äº‹ä»¶å‘布器
```java
@Autowired
private ApplicationEventPublisher eventPublisher;
```
#### 4.2 å‘布事件
**创建任务时**:
```java
// å‘布任务创建事件
eventPublisher.publishEvent(new TaskCreatedEvent(
    this, taskId, taskCode, taskType, creatorId, creatorName
));
// å‘布任务分配事件
eventPublisher.publishEvent(new TaskAssignedEvent(
    this, taskId, taskCode, assigneeIds, assigneeNames, assignerId, assignerName
));
```
**分配任务时**:
```java
eventPublisher.publishEvent(new TaskAssignedEvent(
    this, taskId, taskCode, assigneeIds, null, assignerId, assignerName
));
```
**状态变更时**:
```java
eventPublisher.publishEvent(new TaskStatusChangedEvent(
    this, taskId, taskCode, oldStatus, newStatus,
    oldStatusDesc, newStatusDesc, assigneeIds, creatorId
));
```
## æž¶æž„优势
### 1. å®Œå…¨è§£è€¦
| å¯¹æ¯”项 | ç›´æŽ¥è°ƒç”¨ | äº‹ä»¶é©±åЍ |
|--------|---------|---------|
| è€¦åˆåº¦ | å¼ºè€¦åˆ | å®Œå…¨è§£è€¦ |
| ä¾èµ–关系 | éœ€è¦æ³¨å…¥Service | åªéœ€å‘布事件 |
| ä¿®æ”¹å½±å“ | å½±å“ä¸šåŠ¡ä»£ç  | ä¸å½±å“ä¸šåŠ¡ä»£ç  |
| æ‰©å±•性 | éœ€ä¿®æ”¹ä¸šåŠ¡ä»£ç  | åªéœ€æ·»åŠ ç›‘å¬å™¨ |
### 2. å¼‚步处理
```
同步方式:
业务处理 â†’ ä¿å­˜æ¶ˆæ¯ â†’ è¿”回结果
总耗时 = ä¸šåŠ¡è€—æ—¶ + æ¶ˆæ¯è€—æ—¶
异步方式:
业务处理 â†’ è¿”回结果
    â†“
发布事件 â†’ å¼‚步保存消息(不影响主流程)
总耗时 â‰ˆ ä¸šåŠ¡è€—æ—¶
```
### 3. é«˜æ‰©å±•性
其他系统需要触发消息推送时,只需:
```java
// 1. å‘布事件
eventPublisher.publishEvent(new TaskCreatedEvent(...));
// 2. æ— éœ€å…³å¿ƒæ¶ˆæ¯å¦‚何保存
// 3. æ— éœ€ä¿®æ”¹ä»»ä½•现有代码
```
### 4. æ˜“维护性
- **单一职责**: ä¸šåŠ¡æœåŠ¡åªè´Ÿè´£ä¸šåŠ¡é€»è¾‘ï¼Œç›‘å¬å™¨åªè´Ÿè´£æ¶ˆæ¯æŽ¨é€
- **独立测试**: å¯ä»¥ç‹¬ç«‹æµ‹è¯•事件发布和监听
- **日志清晰**: å¼‚步线程有独立的线程名称前缀
## ä½¿ç”¨æŒ‡å—
### å…¶ä»–系统如何使用
#### åœºæ™¯1: è®¢å•系统推送消息
```java
@Service
public class OrderServiceImpl {
    @Autowired
    private ApplicationEventPublisher eventPublisher;
    public void createOrder(Order order) {
        // 1. ä¸šåŠ¡é€»è¾‘
        orderMapper.insert(order);
        // 2. å‘布事件(推送消息)
        eventPublisher.publishEvent(new OrderCreatedEvent(
            this, order.getOrderId(), order.getOrderNo(), order.getCreatorId()
        ));
    }
}
```
#### åœºæ™¯2: å®¡æ‰¹ç³»ç»ŸæŽ¨é€æ¶ˆæ¯
```java
@Service
public class ApprovalServiceImpl {
    @Autowired
    private ApplicationEventPublisher eventPublisher;
    public void approveTask(Long taskId, String result) {
        // 1. ä¸šåŠ¡é€»è¾‘
        approvalMapper.updateStatus(taskId, result);
        // 2. å‘布事件(推送消息)
        eventPublisher.publishEvent(new ApprovalCompletedEvent(
            this, taskId, result, approverId, applicantId
        ));
    }
}
```
### æ·»åŠ æ–°çš„äº‹ä»¶ç±»åž‹
#### æ­¥éª¤1: åˆ›å»ºäº‹ä»¶ç±»
```java
package com.ruoyi.system.event;
public class OrderCreatedEvent extends ApplicationEvent {
    private Long orderId;
    private String orderNo;
    private Long creatorId;
    public OrderCreatedEvent(Object source, Long orderId, String orderNo, Long creatorId) {
        super(source);
        this.orderId = orderId;
        this.orderNo = orderNo;
        this.creatorId = creatorId;
    }
    // getters and setters
}
```
#### æ­¥éª¤2: æ·»åŠ ç›‘å¬å™¨æ–¹æ³•
```java
@Component
public class OrderMessageListener {
    @Autowired
    private SysMessageMapper sysMessageMapper;
    @Async
    @EventListener
    public void handleOrderCreatedEvent(OrderCreatedEvent event) {
        // ä¿å­˜æ¶ˆæ¯
        SysMessage message = new SysMessage();
        message.setMessageType("ORDER_CREATE");
        message.setMessageTitle("订单创建成功");
        message.setMessageContent("您的订单已创建成功");
        // ... è®¾ç½®å…¶ä»–字段
        sysMessageMapper.insertSysMessage(message);
    }
}
```
#### æ­¥éª¤3: å‘布事件
```java
eventPublisher.publishEvent(new OrderCreatedEvent(this, orderId, orderNo, creatorId));
```
## æ€§èƒ½ä¼˜åŒ–
### 1. çº¿ç¨‹æ± é…ç½®ä¼˜åŒ–
根据实际负载调整线程池参数:
```java
// é«˜å¹¶å‘场景
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(200);
// ä½Žå¹¶å‘场景
executor.setCorePoolSize(3);
executor.setMaxPoolSize(5);
executor.setQueueCapacity(50);
```
### 2. æ‰¹é‡ä¿å­˜ä¼˜åŒ–
如果同时有大量消息,可以批量保存:
```java
@Async
@EventListener
public void handleTaskAssignedEvent(TaskAssignedEvent event) {
    List<SysMessage> messages = new ArrayList<>();
    for (Long assigneeId : event.getAssigneeIds()) {
        SysMessage message = new SysMessage();
        // ... è®¾ç½®å­—段
        messages.add(message);
    }
    // æ‰¹é‡æ’å…¥
    sysMessageMapper.batchInsert(messages);
}
```
### 3. æ¶ˆæ¯åŽ»é‡
避免重复发送消息:
```java
@Async
@EventListener
public void handleTaskStatusChangedEvent(TaskStatusChangedEvent event) {
    // ä½¿ç”¨Set去重
    Set<Long> receiverIds = new HashSet<>();
    receiverIds.addAll(event.getAssigneeIds());
    receiverIds.add(event.getCreatorId());
    for (Long receiverId : receiverIds) {
        // ä¿å­˜æ¶ˆæ¯
    }
}
```
## ç›‘控与调试
### 1. æŸ¥çœ‹äº‹ä»¶æ—¥å¿—
```
2025-10-25 14:30:15.123 [async-task-1] INFO  TaskMessageListener - æ”¶åˆ°ä»»åŠ¡åˆ›å»ºäº‹ä»¶ï¼Œä»»åŠ¡ID:1001,任务编号:TASK-20251025-001
2025-10-25 14:30:15.234 [async-task-1] INFO  TaskMessageListener - ä»»åŠ¡åˆ›å»ºæ¶ˆæ¯å·²ä¿å­˜ï¼Œæ¶ˆæ¯ID:5001
2025-10-25 14:30:15.345 [async-task-2] INFO  TaskMessageListener - æ”¶åˆ°ä»»åŠ¡åˆ†é…äº‹ä»¶ï¼Œä»»åŠ¡ID:1001,执行人数量:3
2025-10-25 14:30:15.456 [async-task-2] INFO  TaskMessageListener - ä»»åŠ¡åˆ†é…æ¶ˆæ¯å·²ä¿å­˜ï¼Œæ¶ˆæ¯ID:5002,接收人:张三
```
### 2. ç›‘控线程池状态
```java
@Component
public class ThreadPoolMonitor {
    @Autowired
    @Qualifier("taskExecutor")
    private ThreadPoolTaskExecutor executor;
    @Scheduled(fixedDelay = 60000) // æ¯åˆ†é’Ÿ
    public void monitor() {
        log.info("线程池状态 - æ´»è·ƒçº¿ç¨‹ï¼š{},队列大小:{},完成任务:{}",
                executor.getActiveCount(),
                executor.getThreadPoolExecutor().getQueue().size(),
                executor.getThreadPoolExecutor().getCompletedTaskCount());
    }
}
```
## æ³¨æ„äº‹é¡¹
### 1. äº‹åŠ¡è¾¹ç•Œ
事件发布应该在事务提交之后:
```java
@Transactional
public int insertSysTask(TaskCreateVO createVO) {
    // 1. ä¿å­˜ä»»åŠ¡
    int result = sysTaskMapper.insertSysTask(task);
    // 2. åœ¨äº‹åŠ¡å†…å‘å¸ƒäº‹ä»¶ï¼ˆäº‹åŠ¡æäº¤åŽæ‰ä¼šè§¦å‘ç›‘å¬å™¨ï¼‰
    if (result > 0) {
        eventPublisher.publishEvent(new TaskCreatedEvent(...));
    }
    return result;
}
```
### 2. å¼‚常处理
监听器中的异常不会影响主业务:
```java
@Async
@EventListener
public void handleTaskCreatedEvent(TaskCreatedEvent event) {
    try {
        // å¤„理事件
    } catch (Exception e) {
        log.error("处理任务创建事件失败", e);
        // ä¸å‘上抛出异常
    }
}
```
### 3. é¡ºåºæ€§
异步事件不保证执行顺序:
```java
// è¿™ä¸¤ä¸ªäº‹ä»¶å¯èƒ½ä»¥ä»»æ„é¡ºåºæ‰§è¡Œ
eventPublisher.publishEvent(event1);
eventPublisher.publishEvent(event2);
```
如需保证顺序,使用同步监听器:
```java
@EventListener  // ä¸ä½¿ç”¨@Async
public void handleEvent(Event event) {
    // åŒæ­¥æ‰§è¡Œï¼Œä¿è¯é¡ºåº
}
```
## å¯¹æ¯”总结
| ç‰¹æ€§ | ç›´æŽ¥è°ƒç”¨Service | äº‹ä»¶é©±åЍ |
|------|---------------|---------|
| ä»£ç è€¦åˆåº¦ | é«˜ | ä½Ž |
| æ€§èƒ½å½±å“ | åŒæ­¥é˜»å¡ž | å¼‚步非阻塞 |
| æ‰©å±•性 | éœ€ä¿®æ”¹ä»£ç  | åªéœ€æ·»åŠ ç›‘å¬å™¨ |
| å¯æµ‹è¯•性 | éœ€æ¨¡æ‹ŸService | åªéœ€å‘布事件 |
| å¯ç»´æŠ¤æ€§ | ä¸šåŠ¡ä»£ç æ··æ‚ | èŒè´£æ¸…晰分离 |
| å­¦ä¹ æˆæœ¬ | ä½Ž | ä¸­ |
| é€‚用场景 | ç®€å•项目 | å¤æ‚/大型项目 |
## æ€»ç»“
通过引入Spring事件驱动架构,我们成功实现了:
1. âœ… **业务与消息完全解耦** - ä¸šåŠ¡ä»£ç åªéœ€å‘å¸ƒäº‹ä»¶
2. âœ… **异步非阻塞处理** - ä¸å½±å“ä¸»ä¸šåŠ¡æ€§èƒ½
3. âœ… **高度可扩展** - å…¶ä»–系统轻松集成
4. âœ… **职责清晰** - ç›‘听器专注消息推送
5. âœ… **易于维护** - ç‹¬ç«‹çš„事件和监听器管理
这种架构特别适合大型项目和需要高扩展性的系统!
---
**更新时间**: 2025-10-25
**版本**: v2.0
**架构**: äº‹ä»¶é©±åЍ
prd/ʼþÇý¶¯ÏûÏ¢ÍÆËÍ-¿ìËÙ¿ªÊ¼.md
New file
@@ -0,0 +1,366 @@
# äº‹ä»¶é©±åŠ¨æ¶ˆæ¯æŽ¨é€ - å¿«é€Ÿå¼€å§‹
## æ ¸å¿ƒæ¦‚念
使用Spring事件机制实现消息推送,业务系统只需发布事件,消息监听器自动保存消息到数据库。
## å¿«é€Ÿéƒ¨ç½²
### 1. æ— éœ€é¢å¤–部署
事件驱动架构已经集成到现有系统中,重启后端服务即可生效:
```bash
# Windows
bin\run.bat
# Linux
sh bin/run.sh
```
### 2. éªŒè¯éƒ¨ç½²
查看启动日志,确认以下内容:
```
... AsyncConfig           : Bean 'taskExecutor' created
... TaskMessageListener   : Bean created
```
## ä½¿ç”¨æ–¹å¼
### æ–¹å¼1: åœ¨ä»»åŠ¡ç³»ç»Ÿä¸­ï¼ˆå·²é›†æˆï¼‰
**任务创建、分配、状态变更时自动发布事件,无需额外代码**
### æ–¹å¼2: åœ¨å…¶ä»–系统中使用
#### æ­¥éª¤1: æ³¨å…¥äº‹ä»¶å‘布器
```java
@Service
public class YourService {
    @Autowired
    private ApplicationEventPublisher eventPublisher;
    // ...
}
```
#### æ­¥éª¤2: å‘布事件
**示例1: ä»»åŠ¡åˆ›å»ºæ—¶æŽ¨é€æ¶ˆæ¯**
```java
public void createTask(Task task) {
    // 1. ä¸šåŠ¡é€»è¾‘
    taskMapper.insert(task);
    // 2. å‘布事件(触发消息推送)
    eventPublisher.publishEvent(new TaskCreatedEvent(
        this,                    // source
        task.getTaskId(),        // ä»»åŠ¡ID
        task.getTaskCode(),      // ä»»åŠ¡ç¼–å·
        task.getTaskType(),      // ä»»åŠ¡ç±»åž‹
        task.getCreatorId(),     // åˆ›å»ºäººID
        "张三"                    // åˆ›å»ºäººå§“名
    ));
}
```
**示例2: ä»»åŠ¡åˆ†é…æ—¶æŽ¨é€æ¶ˆæ¯**
```java
public void assignTask(Long taskId, List<Long> assigneeIds) {
    // 1. ä¸šåŠ¡é€»è¾‘
    taskMapper.updateAssignees(taskId, assigneeIds);
    // 2. å‘布事件(触发消息推送)
    eventPublisher.publishEvent(new TaskAssignedEvent(
        this,
        taskId,
        "TASK-001",
        assigneeIds,            // æ‰§è¡ŒäººID列表
        null,                   // å§“名列表(可选,监听器会查询)
        currentUserId,          // åˆ†é…äººID
        "李四"                  // åˆ†é…äººå§“名
    ));
}
```
**示例3: çŠ¶æ€å˜æ›´æ—¶æŽ¨é€æ¶ˆæ¯**
```java
public void changeStatus(Long taskId, String newStatus) {
    Task task = taskMapper.selectById(taskId);
    String oldStatus = task.getStatus();
    // 1. ä¸šåŠ¡é€»è¾‘
    taskMapper.updateStatus(taskId, newStatus);
    // 2. å‘布事件(触发消息推送)
    eventPublisher.publishEvent(new TaskStatusChangedEvent(
        this,
        taskId,
        task.getTaskCode(),
        oldStatus,              // æ—§çŠ¶æ€
        newStatus,              // æ–°çŠ¶æ€
        "待处理",               // æ—§çŠ¶æ€æè¿°
        "已完成",               // æ–°çŠ¶æ€æè¿°
        task.getAssigneeIds(),  // æ‰§è¡ŒäººID列表
        task.getCreatorId()     // åˆ›å»ºäººID
    ));
}
```
## äº‹ä»¶ç±»åž‹
| äº‹ä»¶ç±» | ç”¨é€” | æ¶ˆæ¯ç±»åž‹ |
|--------|------|---------|
| TaskCreatedEvent | ä»»åŠ¡åˆ›å»º | CREATE |
| TaskAssignedEvent | ä»»åŠ¡åˆ†é… | PUSH/ASSIGN |
| TaskStatusChangedEvent | çŠ¶æ€å˜æ›´ | STATUS |
## æ‰©å±•新事件
### åœºæ™¯: è®¢å•系统需要推送消息
#### 1. åˆ›å»ºè®¢å•事件类
```java
package com.ruoyi.system.event;
import org.springframework.context.ApplicationEvent;
public class OrderCreatedEvent extends ApplicationEvent {
    private Long orderId;
    private String orderNo;
    private Long customerId;
    public OrderCreatedEvent(Object source, Long orderId, String orderNo, Long customerId) {
        super(source);
        this.orderId = orderId;
        this.orderNo = orderNo;
        this.customerId = customerId;
    }
    // getters...
}
```
#### 2. åˆ›å»ºç›‘听器(或在现有监听器中添加)
```java
@Component
public class OrderMessageListener {
    @Autowired
    private SysMessageMapper sysMessageMapper;
    @Async
    @EventListener
    public void handleOrderCreatedEvent(OrderCreatedEvent event) {
        SysMessage message = new SysMessage();
        message.setMessageType("ORDER_CREATE");
        message.setMessageTitle("订单创建成功");
        message.setMessageContent("您的订单" + event.getOrderNo() + "已创建");
        message.setReceiverId(event.getCustomerId());
        message.setIsRead("0");
        message.setCreateTime(DateUtils.getNowDate());
        message.setDelFlag("0");
        sysMessageMapper.insertSysMessage(message);
    }
}
```
#### 3. å‘布事件
```java
@Service
public class OrderServiceImpl {
    @Autowired
    private ApplicationEventPublisher eventPublisher;
    public void createOrder(Order order) {
        orderMapper.insert(order);
        // å‘布事件
        eventPublisher.publishEvent(new OrderCreatedEvent(
            this, order.getId(), order.getOrderNo(), order.getCustomerId()
        ));
    }
}
```
## æ ¸å¿ƒä¼˜åŠ¿
### 1. å®Œå…¨è§£è€¦
```java
// âŒ æ—§æ–¹å¼ï¼šéœ€è¦æ³¨å…¥MessageService
@Autowired
private IMessageService messageService;
public void createTask() {
    // ...
    messageService.pushMessage(...);  // å¼ºä¾èµ–
}
// âœ… æ–°æ–¹å¼ï¼šåªéœ€å‘布事件
@Autowired
private ApplicationEventPublisher eventPublisher;
public void createTask() {
    // ...
    eventPublisher.publishEvent(event);  // é›¶ä¾èµ–
}
```
### 2. å¼‚步处理
```
业务处理 200ms â†’ è¿”回结果
    â†“(不阻塞)
发布事件 â†’ å¼‚步保存消息 50ms
总耗时:200ms(而非250ms)
```
### 3. è½»æ¾æ‰©å±•
添加新的消息推送场景,只需:
1. å‘布事件 âœ…
2. æ— éœ€ä¿®æ”¹ä¸šåŠ¡ä»£ç  âœ…
3. æ— éœ€é‡æ–°éƒ¨ç½² âœ…
## ç›‘控和调试
### æŸ¥çœ‹äº‹ä»¶æ—¥å¿—
```bash
# æœç´¢äº‹ä»¶å‘布日志
grep "publishEvent" logs/ruoyi-admin.log
# æœç´¢äº‹ä»¶ç›‘听日志
grep "TaskMessageListener" logs/ruoyi-admin.log
```
### æ—¥å¿—示例
```
2025-10-25 14:30:15 [main] INFO  å‘布任务创建事件,任务ID:1001
2025-10-25 14:30:15 [async-task-1] INFO  æ”¶åˆ°ä»»åŠ¡åˆ›å»ºäº‹ä»¶ï¼Œä»»åŠ¡ID:1001
2025-10-25 14:30:15 [async-task-1] INFO  ä»»åŠ¡åˆ›å»ºæ¶ˆæ¯å·²ä¿å­˜ï¼Œæ¶ˆæ¯ID:5001
```
## å¸¸è§é—®é¢˜
### Q1: äº‹ä»¶å‘布后消息没有保存?
**原因**: å¯èƒ½æ˜¯å¼‚步线程池满了
**解决**:
1. æŸ¥çœ‹æ—¥å¿—是否有异常
2. è°ƒæ•´çº¿ç¨‹æ± é…ç½®ï¼ˆAsyncConfig.java)
3. æ£€æŸ¥æ•°æ®åº“连接
### Q2: å¦‚何确保消息一定发送?
**方案1**: ä½¿ç”¨åŒæ­¥ç›‘听器(去掉@Async)
```java
@EventListener  // ä¸ä½¿ç”¨@Async
public void handleEvent(Event event) {
    // åŒæ­¥æ‰§è¡Œï¼Œç¡®ä¿æ¶ˆæ¯ä¿å­˜
}
```
**方案2**: æ·»åŠ é‡è¯•æœºåˆ¶
```java
@Async
@EventListener
@Retryable(maxAttempts = 3)
public void handleEvent(Event event) {
    // å¤±è´¥è‡ªåŠ¨é‡è¯•3次
}
```
### Q3: å¦‚何禁用异步处理?
修改监听器,去掉 `@Async` æ³¨è§£ï¼š
```java
// @Async  // æ³¨é‡ŠæŽ‰
@EventListener
public void handleEvent(Event event) {
    // çŽ°åœ¨æ˜¯åŒæ­¥æ‰§è¡Œ
}
```
## æ€§èƒ½ä¼˜åŒ–
### æ‰¹é‡å¤„理消息
```java
// æ”¶é›†æ¶ˆæ¯ï¼Œæ‰¹é‡ä¿å­˜
List<SysMessage> messages = new ArrayList<>();
for (Long userId : userIds) {
    SysMessage msg = new SysMessage();
    // ...
    messages.add(msg);
}
sysMessageMapper.batchInsert(messages);  // æ‰¹é‡æ’å…¥
```
### è°ƒæ•´çº¿ç¨‹æ± 
根据实际负载调整 `AsyncConfig.java`:
```java
// é«˜å¹¶å‘场景
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
// ä½Žå¹¶å‘场景
executor.setCorePoolSize(3);
executor.setMaxPoolSize(5);
```
## å¯¹æ¯”方式
| ç‰¹æ€§ | ç›´æŽ¥è°ƒç”¨ | äº‹ä»¶é©±åЍ |
|------|---------|---------|
| ä»£ç è€¦åˆ | å¼ºä¾èµ–MessageService | é›¶ä¾èµ– |
| æ€§èƒ½ | åŒæ­¥é˜»å¡ž | å¼‚步非阻塞 |
| æ‰©å±•性 | éœ€ä¿®æ”¹ä»£ç  | åªéœ€æ·»åŠ ç›‘å¬å™¨ |
| æµ‹è¯• | éœ€æ¨¡æ‹ŸService | åªéœ€å‘布事件 |
## æ–‡ä»¶æ¸…单
### æ–°å¢žæ–‡ä»¶
- `TaskEvent.java` - äº‹ä»¶åŸºç±»
- `TaskCreatedEvent.java` - åˆ›å»ºäº‹ä»¶
- `TaskAssignedEvent.java` - åˆ†é…äº‹ä»¶
- `TaskStatusChangedEvent.java` - çŠ¶æ€äº‹ä»¶
- `TaskMessageListener.java` - æ¶ˆæ¯ç›‘听器
- `AsyncConfig.java` - å¼‚步配置
### ä¿®æ”¹æ–‡ä»¶
- `SysTaskServiceImpl.java` - æ”¹ä¸ºå‘布事件
## ä¸‹ä¸€æ­¥
1. ðŸ“– é˜…读详细文档:`prd/事件驱动消息推送-实现总结.md`
2. ðŸ”§ æ ¹æ®éœ€è¦è°ƒæ•´çº¿ç¨‹æ± é…ç½®
3. ðŸ“Š ç›‘æŽ§ç³»ç»Ÿè¿è¡Œæ—¥å¿—
4. ðŸš€ åœ¨å…¶ä»–系统中使用事件机制
---
**更新时间**: 2025-10-25
**版本**: v2.0
**推荐使用**: é€‚合所有需要消息推送的场景
prd/ϵͳÏûÏ¢ÍÆËÍ-¿ìËÙ¿ªÊ¼.md
New file
@@ -0,0 +1,189 @@
# ç³»ç»Ÿæ¶ˆæ¯æŽ¨é€åŠŸèƒ½ - å¿«é€Ÿå¼€å§‹
## åŠŸèƒ½è¯´æ˜Ž
实现了在任务创建、状态变更、分配用户时自动推送消息给相关人员的功能。
## å¿«é€Ÿéƒ¨ç½²
### 1. æ‰§è¡Œæ•°æ®åº“脚本
```bash
# è¿›å…¥é¡¹ç›®sql目录
cd d:\project\急救转运\code\Api\RuoYi-Vue-master\sql
# æ‰§è¡Œæ¶ˆæ¯è¡¨åˆ›å»ºè„šæœ¬
mysql -u root -p your_database < sys_message.sql
```
或者手动执行SQL:
```sql
-- åœ¨MySQL客户端中执行
source d:/project/急救转运/code/Api/RuoYi-Vue-master/sql/sys_message.sql;
```
### 2. é‡å¯åŽç«¯æœåŠ¡
```bash
# Windows
cd d:\project\急救转运\code\Api\RuoYi-Vue-master
bin\run.bat
# Linux
cd /path/to/RuoYi-Vue-master
sh bin/run.sh
```
### 3. å‰ç«¯å·²è‡ªåŠ¨é›†æˆï¼Œæ— éœ€é¢å¤–æ“ä½œ
## å¿«é€Ÿæµ‹è¯•
### æµ‹è¯•1:创建任务消息推送
1. åœ¨å‰ç«¯åˆ›å»ºä¸€ä¸ªä»»åŠ¡
2. è¿›å…¥"消息中心"页面
3. åº”该看到一条"创建成功"消息
### æµ‹è¯•2:任务分配消息推送
1. åˆ›å»ºä»»åŠ¡æ—¶é€‰æ‹©æ‰§è¡Œäººå‘˜
2. æ‰§è¡Œäººå‘˜ç™»å½•系统
3. è¿›å…¥"消息中心"页面
4. åº”该看到一条"任务推送"消息
### æµ‹è¯•3:状态变更消息推送
1. åœ¨é¦–页点击任务的"出发"按钮
2. ç›¸å…³äººå‘˜ï¼ˆæ‰§è¡Œäºº+创建人)进入"消息中心"
3. åº”该看到一条"状态变更"消息
## æ¶ˆæ¯ç±»åž‹
| ç±»åž‹ | åœºæ™¯ | æŽ¥æ”¶äºº | å†…容 |
|------|------|--------|------|
| åˆ›å»ºæˆåŠŸ | åˆ›å»ºä»»åŠ¡ | åˆ›å»ºäºº | æ‚¨åˆ›å»ºçš„任务已成功提交 |
| ä»»åŠ¡æŽ¨é€ | åˆ†é…æ‰§è¡Œäºº | æ‰§è¡Œäºº | æ‚¨æœ‰æ–°çš„任务,请及时处理 |
| çŠ¶æ€å˜æ›´ | æ›´æ–°çŠ¶æ€ | æ‰§è¡Œäºº+创建人 | ä»»åŠ¡çŠ¶æ€å˜æ›´ä¸ºï¼šXXX |
## æ ¸å¿ƒæ–‡ä»¶
### åŽç«¯
- `sql/sys_message.sql` - æ•°æ®åº“表
- `SysMessage.java` - å®žä½“ç±»
- `SysMessageMapper.java` - Mapper接口
- `SysMessageMapper.xml` - MyBatis映射
- `ISysMessageService.java` - Service接口
- `SysMessageServiceImpl.java` - Service实现
- `SysMessageController.java` - Controller
- `SysTaskServiceImpl.java` - ä»»åŠ¡æœåŠ¡ï¼ˆå·²é›†æˆæ¶ˆæ¯æŽ¨é€ï¼‰
### å‰ç«¯
- `app/api/message.js` - API接口
- `app/pages/message/index.vue` - æ¶ˆæ¯ä¸­å¿ƒé¡µé¢
- `app/pages/index.vue` - é¦–页(显示未读数量)
## ä¸»è¦API
### åŽç«¯æŽ¥å£
```
GET  /system/message/my              - èŽ·å–å½“å‰ç”¨æˆ·æ¶ˆæ¯åˆ—è¡¨
GET  /system/message/unread/count    - èŽ·å–æœªè¯»æ¶ˆæ¯æ•°é‡
PUT  /system/message/read/{id}       - æ ‡è®°æ¶ˆæ¯ä¸ºå·²è¯»
PUT  /system/message/read/all        - æ ‡è®°æ‰€æœ‰æ¶ˆæ¯ä¸ºå·²è¯»
```
### å‰ç«¯è°ƒç”¨ç¤ºä¾‹
```javascript
import { getMyMessages, getUnreadCount, markAsRead } from '@/api/message'
// èŽ·å–æ¶ˆæ¯åˆ—è¡¨
const messages = await getMyMessages()
// èŽ·å–æœªè¯»æ•°é‡
const count = await getUnreadCount()
// æ ‡è®°å·²è¯»
await markAsRead(messageId)
```
## æ¶ˆæ¯æŽ¨é€æ—¶æœº
### 1. åˆ›å»ºä»»åŠ¡ï¼ˆinsertSysTask)
```java
// æŽ¨é€åˆ›å»ºæˆåŠŸæ¶ˆæ¯ï¼ˆç»™åˆ›å»ºäººï¼‰
if (result > 0 && sysMessageService != null) {
    sysMessageService.pushTaskCreateMessage(task);
}
// æŽ¨é€ä»»åŠ¡åˆ†é…æ¶ˆæ¯ï¼ˆç»™æ‰§è¡Œäººï¼‰
if (result > 0 && createVO.getAssignees() != null && !createVO.getAssignees().isEmpty()) {
    List<Long> assigneeIds = createVO.getAssignees().stream()
        .map(assignee -> assignee.getUserId())
        .collect(Collectors.toList());
    sysMessageService.pushTaskAssignMessage(task, assigneeIds);
}
```
### 2. åˆ†é…ä»»åŠ¡ï¼ˆassignTask)
```java
// æŽ¨é€ä»»åŠ¡åˆ†é…æ¶ˆæ¯
if (result > 0 && sysMessageService != null) {
    List<Long> assigneeIds = new ArrayList<>();
    assigneeIds.add(assigneeId);
    sysMessageService.pushTaskAssignMessage(task, assigneeIds);
}
```
### 3. çŠ¶æ€å˜æ›´ï¼ˆchangeTaskStatusWithLocation)
```java
// æŽ¨é€ä»»åŠ¡çŠ¶æ€å˜æ›´æ¶ˆæ¯
if (result > 0 && sysMessageService != null) {
    sysMessageService.pushTaskStatusChangeMessage(
        oldTask,
        oldTaskStatus.getCode(),
        newStatus.getCode()
    );
}
```
## å¸¸è§é—®é¢˜
### Q1: æ¶ˆæ¯æ²¡æœ‰æŽ¨é€æ€Žä¹ˆåŠžï¼Ÿ
A: æ£€æŸ¥ä»¥ä¸‹å‡ ç‚¹ï¼š
1. æ•°æ®åº“表是否创建成功
2. åŽç«¯æœåŠ¡æ˜¯å¦é‡å¯
3. æŸ¥çœ‹åŽç«¯æ—¥å¿—是否有异常
4. ç¡®è®¤ `ISysMessageService` æ˜¯å¦æ³¨å…¥æˆåŠŸ
### Q2: æ¶ˆæ¯æŽ¨é€å¤±è´¥ä¼šå½±å“ä¸»ä¸šåŠ¡å—ï¼Ÿ
A: ä¸ä¼šã€‚消息推送使用 `@Autowired(required = false)`,且所有异常都被捕获,不会影响任务的创建、分配和状态变更。
### Q3: å¦‚何查看消息推送日志?
A: æŸ¥çœ‹åŽç«¯æ—¥å¿—,搜索关键字:
- "推送任务创建成功消息"
- "推送任务分配消息"
- "推送任务状态变更消息"
### Q4: æœªè¯»æ¶ˆæ¯æ•°é‡ä¸æ›´æ–°æ€Žä¹ˆåŠžï¼Ÿ
A:
1. ç¡®è®¤ `app/pages/index.vue` å·²æ›´æ–°
2. ç¡®è®¤ `app/api/message.js` å·²åˆ›å»º
3. é‡æ–°ç¼–译前端:`npm run build:h5`
## æ‰©å±•建议
1. **WebSocket实时推送**:集成WebSocket实现消息实时推送
2. **推送通知**:集成极光推送等第三方推送服务
3. **消息模板**:配置消息模板,支持变量替换
4. **消息分组**:增加消息分组(系统通知、任务提醒等)
## è¯¦ç»†æ–‡æ¡£
请查看完整文档:`prd/系统消息推送功能实现总结.md`
---
**更新时间**: 2025-10-25
**版本**: v1.0
prd/ϵͳÏûÏ¢ÍÆËÍ-Îļþ±ä¸üÇåµ¥.md
New file
@@ -0,0 +1,245 @@
# ç³»ç»Ÿæ¶ˆæ¯æŽ¨é€åŠŸèƒ½ - æ–‡ä»¶å˜æ›´æ¸…单
## æ–°å¢žæ–‡ä»¶
### æ•°æ®åº“脚本
- âœ… `sql/sys_message.sql` - æ¶ˆæ¯è¡¨åˆ›å»ºè„šæœ¬
### åŽç«¯ - å®žä½“ç±»
- âœ… `ruoyi-system/src/main/java/com/ruoyi/system/domain/SysMessage.java` - æ¶ˆæ¯å®žä½“ç±»
### åŽç«¯ - Mapper层
- âœ… `ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysMessageMapper.java` - Mapper接口
- âœ… `ruoyi-system/src/main/resources/mapper/system/SysMessageMapper.xml` - MyBatis映射文件
### åŽç«¯ - Service层
- âœ… `ruoyi-system/src/main/java/com/ruoyi/system/service/ISysMessageService.java` - Service接口
- âœ… `ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysMessageServiceImpl.java` - Service实现类
### åŽç«¯ - Controller层
- âœ… `ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysMessageController.java` - æ¶ˆæ¯Controller
### å‰ç«¯ - API
- âœ… `app/api/message.js` - æ¶ˆæ¯API接口
### æ–‡æ¡£
- âœ… `prd/系统消息推送功能实现总结.md` - å®Œæ•´å®žçŽ°æ–‡æ¡£
- âœ… `prd/系统消息推送-快速开始.md` - å¿«é€Ÿå¼€å§‹æŒ‡å—
- âœ… `prd/系统消息推送-文件变更清单.md` - æœ¬æ–‡ä»¶
## ä¿®æ”¹æ–‡ä»¶
### åŽç«¯
- âœ… `ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskServiceImpl.java`
  - å¯¼å…¥ `ISysMessageService`
  - æ³¨å…¥ `ISysMessageService` æœåŠ¡ï¼ˆrequired = false)
  - åœ¨ `insertSysTask` æ–¹æ³•中添加创建成功消息推送
  - åœ¨ `insertSysTask` æ–¹æ³•中添加任务分配消息推送
  - åœ¨ `assignTask` æ–¹æ³•中添加任务分配消息推送
  - åœ¨ `changeTaskStatusWithLocation` æ–¹æ³•中添加状态变更消息推送
### å‰ç«¯
- âœ… `app/pages/message/index.vue`
  - å¯¼å…¥ `message.js` API
  - å®žçް `loadMessages` æ–¹æ³•加载消息列表
  - å®žçް `loadUnreadMessageCount` æ–¹æ³•加载未读数量
  - ä¿®æ”¹ `viewMessageDetail` æ–¹æ³•调用API标记已读
  - æ·»åŠ  `onLoad` ç”Ÿå‘½å‘¨æœŸåŠ è½½æ•°æ®
  - æ·»åŠ  `onShow` ç”Ÿå‘½å‘¨æœŸåˆ·æ–°æ•°æ®
  - æ·»åŠ  `onPullDownRefresh` æ”¯æŒä¸‹æ‹‰åˆ·æ–°
  - ä¿®æ”¹æ¶ˆæ¯å­—段映射(type→messageType, content→messageContent等)
- âœ… `app/pages/index.vue`
  - å¯¼å…¥ `message.js` API中的 `getUnreadCount`
  - æ·»åŠ  `unreadMessageCount` æ•°æ®å­—段
  - æ·»åŠ  `loadUnreadMessageCount` æ–¹æ³•
  - åœ¨ `onLoad` ä¸­è°ƒç”¨ `loadUnreadMessageCount`
  - åœ¨ `onShow` ä¸­è°ƒç”¨ `loadUnreadMessageCount`
  - åˆ é™¤è®¡ç®—属性中的 `unreadMessageCount`(改为data字段)
## å˜æ›´ç»Ÿè®¡
### æ–°å¢žæ–‡ä»¶æ•°é‡
- æ•°æ®åº“脚本: 1
- Java文件: 6
- JavaScript文件: 1
- Vue文件: 0(已存在,仅修改)
- æ–‡æ¡£æ–‡ä»¶: 3
- **总计**: 11个新增文件
### ä¿®æ”¹æ–‡ä»¶æ•°é‡
- Java文件: 1
- Vue文件: 2
- **总计**: 3个修改文件
### ä»£ç è¡Œæ•°ç»Ÿè®¡
- æ–°å¢žä»£ç è¡Œ: çº¦1400行
  - SQL: 27行
  - Java: çº¦900行
  - JavaScript: 77行
  - Vue修改: çº¦90行
  - æ–‡æ¡£: çº¦500行
## åŠŸèƒ½æ¨¡å—
### 1. æ•°æ®åº“层(1个文件)
- `sys_message` è¡¨ç»“æž„
### 2. åŽç«¯å±‚(7个文件)
- å®žä½“类(1个)
- Mapper接口(1个)
- MyBatis映射(1个)
- Service接口(1个)
- Service实现(1个)
- Controller(1个)
- ä»»åŠ¡æœåŠ¡é›†æˆï¼ˆ1个修改)
### 3. å‰ç«¯å±‚(3个文件)
- API接口(1个新增)
- æ¶ˆæ¯ä¸­å¿ƒé¡µé¢ï¼ˆ1个修改)
- é¦–页未读数量(1个修改)
### 4. æ–‡æ¡£å±‚(3个文件)
- åŠŸèƒ½å®žçŽ°æ€»ç»“
- å¿«é€Ÿå¼€å§‹æŒ‡å—
- æ–‡ä»¶å˜æ›´æ¸…单
## ä¾èµ–关系
### åŽç«¯ä¾èµ–
```
SysMessageController
    â†“ ä¾èµ–
ISysMessageService (接口)
    â†“ å®žçް
SysMessageServiceImpl
    â†“ ä¾èµ–
SysMessageMapper (接口)
    â†“ é…ç½®
SysMessageMapper.xml
    â†“ æ“ä½œ
sys_message (数据表)
```
### ä»»åŠ¡æœåŠ¡é›†æˆ
```
SysTaskServiceImpl
    â†“ è°ƒç”¨
ISysMessageService.pushTaskCreateMessage()
ISysMessageService.pushTaskAssignMessage()
ISysMessageService.pushTaskStatusChangeMessage()
    â†“ æ“ä½œ
sys_message (数据表)
```
### å‰ç«¯ä¾èµ–
```
index.vue / message/index.vue
    â†“ è°ƒç”¨
api/message.js
    â†“ è¯·æ±‚
后端 SysMessageController
    â†“ æŸ¥è¯¢
sys_message (数据表)
```
## éƒ¨ç½²æ£€æŸ¥æ¸…单
- [ ] 1. æ‰§è¡Œæ•°æ®åº“脚本 `sql/sys_message.sql`
- [ ] 2. ç¡®è®¤ `sys_message` è¡¨åˆ›å»ºæˆåŠŸ
- [ ] 3. é‡æ–°ç¼–译后端项目
- [ ] 4. é‡å¯åŽç«¯æœåŠ¡
- [ ] 5. æ¸…理浏览器缓存
- [ ] 6. æµ‹è¯•创建任务消息推送
- [ ] 7. æµ‹è¯•任务分配消息推送
- [ ] 8. æµ‹è¯•状态变更消息推送
- [ ] 9. æµ‹è¯•消息列表查询
- [ ] 10. æµ‹è¯•消息标记已读
- [ ] 11. æµ‹è¯•未读消息数量显示
## Git提交建议
```bash
# æäº¤æ•°æ®åº“脚本
git add sql/sys_message.sql
git commit -m "feat: æ·»åŠ ç³»ç»Ÿæ¶ˆæ¯è¡¨ç»“æž„"
# æäº¤åŽç«¯ä»£ç 
git add ruoyi-system/src/main/java/com/ruoyi/system/domain/SysMessage.java
git add ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysMessageMapper.java
git add ruoyi-system/src/main/resources/mapper/system/SysMessageMapper.xml
git add ruoyi-system/src/main/java/com/ruoyi/system/service/ISysMessageService.java
git add ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysMessageServiceImpl.java
git add ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysMessageController.java
git commit -m "feat: å®žçŽ°ç³»ç»Ÿæ¶ˆæ¯æŽ¨é€åŽç«¯æœåŠ¡"
# æäº¤ä»»åŠ¡æœåŠ¡é›†æˆ
git add ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskServiceImpl.java
git commit -m "feat: åœ¨ä»»åŠ¡åˆ›å»ºã€åˆ†é…ã€çŠ¶æ€å˜æ›´æ—¶é›†æˆæ¶ˆæ¯æŽ¨é€"
# æäº¤å‰ç«¯ä»£ç 
git add app/api/message.js
git add app/pages/message/index.vue
git add app/pages/index.vue
git commit -m "feat: å®žçŽ°æ¶ˆæ¯ä¸­å¿ƒå‰ç«¯é¡µé¢å’Œæœªè¯»æ¶ˆæ¯è®¡æ•°"
# æäº¤æ–‡æ¡£
git add prd/系统消息推送功能实现总结.md
git add prd/系统消息推送-快速开始.md
git add prd/系统消息推送-文件变更清单.md
git commit -m "docs: æ·»åŠ ç³»ç»Ÿæ¶ˆæ¯æŽ¨é€åŠŸèƒ½æ–‡æ¡£"
# æˆ–者一次性提交
git add .
git commit -m "feat: å®žçŽ°ç³»ç»Ÿæ¶ˆæ¯æŽ¨é€åŠŸèƒ½
- åˆ›å»ºä»»åŠ¡æ—¶æŽ¨é€æ¶ˆæ¯ç»™åˆ›å»ºäººå’Œæ‰§è¡Œäºº
- ä»»åŠ¡åˆ†é…æ—¶æŽ¨é€æ¶ˆæ¯ç»™æ‰§è¡Œäºº
- çŠ¶æ€å˜æ›´æ—¶æŽ¨é€æ¶ˆæ¯ç»™ç›¸å…³äººå‘˜
- å®žçŽ°æ¶ˆæ¯ä¸­å¿ƒé¡µé¢
- å®žçŽ°æœªè¯»æ¶ˆæ¯æ•°é‡æ˜¾ç¤º
- æ”¯æŒæ¶ˆæ¯å·²è¯»æ ‡è®°
- å®Œå–„功能文档"
```
## å›žæ»šæ–¹æ¡ˆ
如果需要回滚此功能:
### 1. æ•°æ®åº“回滚
```sql
DROP TABLE IF EXISTS `sys_message`;
```
### 2. ä»£ç å›žæ»š
```bash
# åˆ é™¤æ–°å¢žæ–‡ä»¶
git rm ruoyi-system/src/main/java/com/ruoyi/system/domain/SysMessage.java
git rm ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysMessageMapper.java
git rm ruoyi-system/src/main/resources/mapper/system/SysMessageMapper.xml
git rm ruoyi-system/src/main/java/com/ruoyi/system/service/ISysMessageService.java
git rm ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysMessageServiceImpl.java
git rm ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysMessageController.java
git rm app/api/message.js
# æ¢å¤ä¿®æ”¹çš„æ–‡ä»¶
git checkout ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskServiceImpl.java
git checkout app/pages/message/index.vue
git checkout app/pages/index.vue
git commit -m "revert: å›žæ»šç³»ç»Ÿæ¶ˆæ¯æŽ¨é€åŠŸèƒ½"
```
## æ³¨æ„äº‹é¡¹
1. **数据库备份**:执行SQL脚本前建议备份数据库
2. **服务重启**:后端修改后必须重启服务才能生效
3. **缓存清理**:前端修改后需要清理浏览器缓存
4. **兼容性测试**:部署后需要全面测试各项功能
5. **性能监控**:关注消息推送对系统性能的影响
---
**更新时间**: 2025-10-25
**版本**: v1.0
**负责人**: AI Assistant
prd/ϵͳÏûÏ¢ÍÆË͹¦ÄÜʵÏÖ×ܽá.md
New file
@@ -0,0 +1,305 @@
# ç³»ç»Ÿæ¶ˆæ¯æŽ¨é€åŠŸèƒ½å®žçŽ°æ€»ç»“
## åŠŸèƒ½æ¦‚è¿°
实现了完整的系统消息推送功能,包括在任务创建、任务分配用户、任务状态变更时自动推送消息给相关用户。
## å®žçް内容
### 1. æ•°æ®åº“表结构
**文件位置**: `sql/sys_message.sql`
创建了 `sys_message` è¡¨ï¼ŒåŒ…含以下字段:
- `message_id`: æ¶ˆæ¯ID(主键)
- `message_type`: æ¶ˆæ¯ç±»åž‹ï¼ˆCREATE-创建成功, PUSH-任务推送, STATUS-状态变更, ASSIGN-分配任务)
- `message_title`: æ¶ˆæ¯æ ‡é¢˜
- `message_content`: æ¶ˆæ¯å†…容
- `task_id`: å…³è”任务ID
- `task_code`: ä»»åŠ¡ç¼–å·
- `receiver_id`: æŽ¥æ”¶äººID
- `receiver_name`: æŽ¥æ”¶äººå§“名
- `sender_id`: å‘送人ID
- `sender_name`: å‘送人姓名
- `is_read`: æ˜¯å¦å·²è¯»ï¼ˆ0-未读, 1-已读)
- `read_time`: è¯»å–æ—¶é—´
- `create_time`: åˆ›å»ºæ—¶é—´
- `update_time`: æ›´æ–°æ—¶é—´
- `del_flag`: åˆ é™¤æ ‡å¿—
### 2. åŽç«¯å®žçް
#### 2.1 å®žä½“ç±»
**文件位置**: `ruoyi-system/src/main/java/com/ruoyi/system/domain/SysMessage.java`
- åŒ…含所有数据库字段的getter/setter
- æ”¯æŒExcel导出注解
- æ—¥æœŸæ ¼å¼åŒ–注解
#### 2.2 Mapper层
**文件位置**:
- `ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysMessageMapper.java`
- `ruoyi-system/src/main/resources/mapper/system/SysMessageMapper.xml`
提供的方法:
- `selectSysMessageList`: æŸ¥è¯¢æ¶ˆæ¯åˆ—表
- `selectSysMessageListByReceiverId`: æŸ¥è¯¢ç”¨æˆ·æ¶ˆæ¯åˆ—表
- `countUnreadMessageByReceiverId`: æŸ¥è¯¢æœªè¯»æ¶ˆæ¯æ•°é‡
- `insertSysMessage`: æ–°å¢žæ¶ˆæ¯
- `updateSysMessage`: ä¿®æ”¹æ¶ˆæ¯
- `deleteSysMessageByMessageIds`: æ‰¹é‡åˆ é™¤æ¶ˆæ¯
- `markMessageAsRead`: æ ‡è®°æ¶ˆæ¯ä¸ºå·²è¯»
- `markAllMessagesAsRead`: æ ‡è®°æ‰€æœ‰æ¶ˆæ¯ä¸ºå·²è¯»
#### 2.3 Service层
**文件位置**:
- `ruoyi-system/src/main/java/com/ruoyi/system/service/ISysMessageService.java`
- `ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysMessageServiceImpl.java`
提供的业务方法:
- åŸºç¡€CRUD方法
- `pushTaskCreateMessage`: æŽ¨é€ä»»åŠ¡åˆ›å»ºæˆåŠŸæ¶ˆæ¯
- `pushTaskAssignMessage`: æŽ¨é€ä»»åŠ¡åˆ†é…æ¶ˆæ¯
- `pushTaskStatusChangeMessage`: æŽ¨é€ä»»åŠ¡çŠ¶æ€å˜æ›´æ¶ˆæ¯
#### 2.4 Controller层
**文件位置**: `ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysMessageController.java`
提供的API接口:
- `GET /system/message/list`: æŸ¥è¯¢æ¶ˆæ¯åˆ—表
- `GET /system/message/my`: æŸ¥è¯¢å½“前用户消息列表
- `GET /system/message/unread/count`: æŸ¥è¯¢æœªè¯»æ¶ˆæ¯æ•°é‡
- `GET /system/message/{messageId}`: èŽ·å–æ¶ˆæ¯è¯¦æƒ…
- `POST /system/message`: æ–°å¢žæ¶ˆæ¯
- `PUT /system/message`: ä¿®æ”¹æ¶ˆæ¯
- `DELETE /system/message/{messageIds}`: åˆ é™¤æ¶ˆæ¯
- `PUT /system/message/read/{messageId}`: æ ‡è®°æ¶ˆæ¯ä¸ºå·²è¯»
- `PUT /system/message/read/all`: æ ‡è®°æ‰€æœ‰æ¶ˆæ¯ä¸ºå·²è¯»
#### 2.5 ä»»åŠ¡æœåŠ¡é›†æˆ
**文件位置**: `ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskServiceImpl.java`
在以下场景自动推送消息:
1. **任务创建成功**
   - æ—¶æœºï¼š`insertSysTask` æ–¹æ³•执行成功后
   - æŽ¥æ”¶äººï¼šä»»åŠ¡åˆ›å»ºäºº
   - æ¶ˆæ¯ç±»åž‹ï¼šCREATE
   - æ¶ˆæ¯å†…容:"您创建的任务已成功提交"
2. **任务分配用户**
   - æ—¶æœºï¼š`insertSysTask` æ–¹æ³•保存执行人员后、`assignTask` æ–¹æ³•执行后
   - æŽ¥æ”¶äººï¼šæ‰€æœ‰æ‰§è¡Œäººå‘˜
   - æ¶ˆæ¯ç±»åž‹ï¼šPUSH/ASSIGN
   - æ¶ˆæ¯å†…容:"您有新的任务,请及时处理"
3. **任务状态变更**
   - æ—¶æœºï¼š`changeTaskStatusWithLocation` æ–¹æ³•执行成功后
   - æŽ¥æ”¶äººï¼šæ‰€æœ‰æ‰§è¡Œäººå‘˜ + åˆ›å»ºäºº
   - æ¶ˆæ¯ç±»åž‹ï¼šSTATUS
   - æ¶ˆæ¯å†…容:根据状态变化动态生成
     - PENDING: "任务状态变更为:待处理"
     - DEPARTED: "任务状态变更为:已出发"
     - ARRIVED: "任务状态变更为:已到达"
     - RETURNING: "任务状态变更为:返程中"
     - COMPLETED: "任务状态变更为:已完成"
     - CANCELLED: "任务状态变更为:已取消"
### 3. å‰ç«¯å®žçް
#### 3.1 API接口
**文件位置**: `app/api/message.js`
提供的方法:
- `getMyMessages`: èŽ·å–å½“å‰ç”¨æˆ·æ¶ˆæ¯åˆ—è¡¨
- `listMessage`: æŸ¥è¯¢æ¶ˆæ¯åˆ—表
- `getMessage`: èŽ·å–æ¶ˆæ¯è¯¦æƒ…
- `getUnreadCount`: èŽ·å–æœªè¯»æ¶ˆæ¯æ•°é‡
- `addMessage`: æ–°å¢žæ¶ˆæ¯
- `updateMessage`: ä¿®æ”¹æ¶ˆæ¯
- `delMessage`: åˆ é™¤æ¶ˆæ¯
- `markAsRead`: æ ‡è®°æ¶ˆæ¯ä¸ºå·²è¯»
- `markAllAsRead`: æ ‡è®°æ‰€æœ‰æ¶ˆæ¯ä¸ºå·²è¯»
#### 3.2 æ¶ˆæ¯ä¸­å¿ƒé¡µé¢
**文件位置**: `app/pages/message/index.vue`
功能特性:
- è‡ªåŠ¨åŠ è½½å½“å‰ç”¨æˆ·çš„æ¶ˆæ¯åˆ—è¡¨
- æœªè¯»æ¶ˆæ¯æ˜¾ç¤ºçº¢ç‚¹æ ‡è®°
- æœªè¯»æ¶ˆæ¯æŽ’在前面
- æ”¯æŒä¸‹æ‹‰åˆ·æ–°
- ç‚¹å‡»æ¶ˆæ¯è‡ªåŠ¨æ ‡è®°ä¸ºå·²è¯»å¹¶è·³è½¬åˆ°ä»»åŠ¡è¯¦æƒ…
- æ¶ˆæ¯ç±»åž‹æ˜¾ç¤ºï¼ˆåˆ›å»ºæˆåŠŸã€ä»»åŠ¡æŽ¨é€ã€çŠ¶æ€å˜æ›´ã€ä»»åŠ¡åˆ†é…ï¼‰
#### 3.3 é¦–页集成
**文件位置**: `app/pages/index.vue`
功能特性:
- æ¶ˆæ¯ä¸­å¿ƒå…¥å£
- æ˜¾ç¤ºæœªè¯»æ¶ˆæ¯æ•°é‡å¾½ç« 
- é¡µé¢æ˜¾ç¤ºæ—¶è‡ªåŠ¨åˆ·æ–°æœªè¯»æ¶ˆæ¯æ•°é‡
## æ¶ˆæ¯ç±»åž‹è¯´æ˜Ž
| æ¶ˆæ¯ç±»åž‹ | ä»£ç  | è§¦å‘场景 | æŽ¥æ”¶äºº | æ¶ˆæ¯å†…容 |
|---------|------|---------|--------|---------|
| åˆ›å»ºæˆåŠŸ | CREATE | ä»»åŠ¡åˆ›å»ºæˆåŠŸ | åˆ›å»ºäºº | æ‚¨åˆ›å»ºçš„任务已成功提交 |
| ä»»åŠ¡æŽ¨é€ | PUSH | ä»»åŠ¡åˆ†é…ç»™æ‰§è¡Œäºº | æ‰§è¡Œäºº | æ‚¨æœ‰æ–°çš„任务,请及时处理 |
| ä»»åŠ¡åˆ†é… | ASSIGN | é‡æ–°åˆ†é…ä»»åŠ¡ | æ–°æ‰§è¡Œäºº | æ‚¨æœ‰æ–°çš„任务,请及时处理 |
| çŠ¶æ€å˜æ›´ | STATUS | ä»»åŠ¡çŠ¶æ€å˜æ›´ | æ‰§è¡Œäºº+创建人 | ä»»åŠ¡çŠ¶æ€å˜æ›´ä¸ºï¼šXXX |
## ä½¿ç”¨æŒ‡å—
### éƒ¨ç½²æ­¥éª¤
1. **执行数据库脚本**
```bash
mysql -u root -p your_database < sql/sys_message.sql
```
2. **重新编译后端**
```bash
cd ruoyi-admin
mvn clean package
```
3. **重启后端服务**
```bash
# Windows
bin\run.bat
# Linux
sh bin/run.sh
```
4. **前端无需额外操作**,代码已自动集成
### æµ‹è¯•验证
1. **创建任务**
   - åˆ›å»ºä»»åŠ¡åŽï¼Œåˆ›å»ºäººåº”æ”¶åˆ°"创建成功"消息
   - æ‰§è¡Œäººåº”收到"任务推送"消息
2. **更新任务状态**
   - åœ¨å‰ç«¯ç‚¹å‡»"出发"、"已到达"等按钮
   - ç›¸å…³äººå‘˜åº”收到"状态变更"消息
3. **分配任务**
   - è°ƒç”¨ä»»åŠ¡åˆ†é…æŽ¥å£
   - æ–°æ‰§è¡Œäººåº”收到"任务分配"消息
4. **查看消息**
   - è¿›å…¥æ¶ˆæ¯ä¸­å¿ƒé¡µé¢
   - æœªè¯»æ¶ˆæ¯æ˜¾ç¤ºçº¢ç‚¹
   - ç‚¹å‡»æ¶ˆæ¯è·³è½¬åˆ°ä»»åŠ¡è¯¦æƒ…å¹¶æ ‡è®°ä¸ºå·²è¯»
## æŠ€æœ¯ç‰¹ç‚¹
### 1. å¼‚步推送
- æ¶ˆæ¯æŽ¨é€ä¸å½±å“ä¸»ä¸šåŠ¡æµç¨‹
- ä½¿ç”¨ `@Autowired(required = false)` ç¡®ä¿æœåŠ¡å¯é€‰
### 2. äº‹åС安免
- æ¶ˆæ¯æŽ¨é€åœ¨äº‹åŠ¡æäº¤åŽæ‰§è¡Œ
- é¿å…äº‹åŠ¡å›žæ»šå¯¼è‡´çš„æ•°æ®ä¸ä¸€è‡´
### 3. å®¹é”™å¤„理
- æ¶ˆæ¯æŽ¨é€å¤±è´¥ä¸å½±å“ä¸»ä¸šåŠ¡
- å®Œå–„的异常捕获和日志记录
### 4. æ€§èƒ½ä¼˜åŒ–
- æ‰¹é‡æŸ¥è¯¢ç”¨æˆ·ä¿¡æ¯
- ç´¢å¼•优化(receiver_id、task_id、is_read、create_time)
### 5. ç”¨æˆ·ä½“验
- å®žæ—¶æœªè¯»æ¶ˆæ¯è®¡æ•°
- æ¶ˆæ¯åˆ†ç±»æ˜¾ç¤º
- æœªè¯»æ¶ˆæ¯ä¼˜å…ˆæŽ’序
- ä¸€é”®æ ‡è®°å·²è¯»
## æ‰©å±•建议
### 1. WebSocket实时推送
可以集成WebSocket实现实时消息推送,而不需要用户刷新页面:
```java
@Service
public class WebSocketMessageService {
    @Autowired
    private SimpMessagingTemplate messagingTemplate;
    public void pushMessage(Long userId, SysMessage message) {
        messagingTemplate.convertAndSendToUser(
            userId.toString(),
            "/queue/messages",
            message
        );
    }
}
```
### 2. æŽ¨é€é€šçŸ¥
可以集成第三方推送服务(如极光推送、个推等)实现APP通知:
```java
public void sendPushNotification(Long userId, String title, String content) {
    // è°ƒç”¨æŽ¨é€æœåŠ¡SDK
    JPushClient.push(userId, title, content);
}
```
### 3. æ¶ˆæ¯æ¨¡æ¿
可以配置消息模板,支持变量替换:
```java
public class MessageTemplate {
    private String type;
    private String titleTemplate;
    private String contentTemplate;
    public String render(Map<String, Object> params) {
        // æ¨¡æ¿æ¸²æŸ“逻辑
    }
}
```
### 4. æ¶ˆæ¯åˆ†ç»„
可以增加消息分组功能:
- ç³»ç»Ÿé€šçŸ¥
- ä»»åŠ¡æé†’
- å®¡æ‰¹æ¶ˆæ¯
- å…¬å‘Šæ¶ˆæ¯
### 5. æ¶ˆæ¯å·²è¯»å›žæ‰§
可以记录消息的详细阅读记录:
```sql
CREATE TABLE sys_message_read_log (
    log_id BIGINT PRIMARY KEY,
    message_id BIGINT,
    user_id BIGINT,
    read_time DATETIME,
    device_type VARCHAR(20)
);
```
## æ³¨æ„äº‹é¡¹
1. **权限控制**:确保用户只能查看自己的消息
2. **数据清理**:定期清理过期的已读消息
3. **性能监控**:监控消息推送的性能,避免大量消息推送造成性能问题
4. **日志记录**:完整记录消息推送的日志,便于排查问题
## æ€»ç»“
本次实现了完整的系统消息推送功能,涵盖了任务创建、分配、状态变更三个核心场景。后端采用Service层统一管理消息推送逻辑,前端提供友好的消息中心界面。整个系统设计合理,易于扩展和维护。
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysMessageController.java
New file
@@ -0,0 +1,138 @@
package com.ruoyi.web.controller.system;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.system.domain.SysMessage;
import com.ruoyi.system.service.ISysMessageService;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.utils.SecurityUtils;
/**
 * ç³»ç»Ÿæ¶ˆæ¯Controller
 *
 * @author ruoyi
 * @date 2025-10-25
 */
@RestController
@RequestMapping("/system/message")
public class SysMessageController extends BaseController {
    @Autowired
    private ISysMessageService sysMessageService;
    /**
     * æŸ¥è¯¢ç³»ç»Ÿæ¶ˆæ¯åˆ—表
     */
    @PreAuthorize("@ss.hasPermi('system:message:list')")
    @GetMapping("/list")
    public TableDataInfo list(SysMessage sysMessage) {
        startPage();
        List<SysMessage> list = sysMessageService.selectSysMessageList(sysMessage);
        return getDataTable(list);
    }
    /**
     * æŸ¥è¯¢å½“前用户的消息列表
     */
    @GetMapping("/my")
    public AjaxResult myMessages() {
        Long userId = SecurityUtils.getUserId();
        List<SysMessage> list = sysMessageService.selectSysMessageListByReceiverId(userId);
        return AjaxResult.success(list);
    }
    /**
     * æŸ¥è¯¢å½“前用户未读消息数量
     */
    @GetMapping("/unread/count")
    public AjaxResult unreadCount() {
        Long userId = SecurityUtils.getUserId();
        int count = sysMessageService.countUnreadMessageByReceiverId(userId);
        return AjaxResult.success(count);
    }
    /**
     * å¯¼å‡ºç³»ç»Ÿæ¶ˆæ¯åˆ—表
     */
    @PreAuthorize("@ss.hasPermi('system:message:export')")
    @Log(title = "系统消息", businessType = BusinessType.EXPORT)
    @PostMapping("/export")
    public void export(HttpServletResponse response, SysMessage sysMessage) {
        List<SysMessage> list = sysMessageService.selectSysMessageList(sysMessage);
        ExcelUtil<SysMessage> util = new ExcelUtil<SysMessage>(SysMessage.class);
        util.exportExcel(response, list, "系统消息数据");
    }
    /**
     * èŽ·å–ç³»ç»Ÿæ¶ˆæ¯è¯¦ç»†ä¿¡æ¯
     */
    @PreAuthorize("@ss.hasPermi('system:message:query')")
    @GetMapping(value = "/{messageId}")
    public AjaxResult getInfo(@PathVariable("messageId") Long messageId) {
        return AjaxResult.success(sysMessageService.selectSysMessageByMessageId(messageId));
    }
    /**
     * æ–°å¢žç³»ç»Ÿæ¶ˆæ¯
     */
    @PreAuthorize("@ss.hasPermi('system:message:add')")
    @Log(title = "系统消息", businessType = BusinessType.INSERT)
    @PostMapping
    public AjaxResult add(@RequestBody SysMessage sysMessage) {
        return toAjax(sysMessageService.insertSysMessage(sysMessage));
    }
    /**
     * ä¿®æ”¹ç³»ç»Ÿæ¶ˆæ¯
     */
    @PreAuthorize("@ss.hasPermi('system:message:edit')")
    @Log(title = "系统消息", businessType = BusinessType.UPDATE)
    @PutMapping
    public AjaxResult edit(@RequestBody SysMessage sysMessage) {
        return toAjax(sysMessageService.updateSysMessage(sysMessage));
    }
    /**
     * åˆ é™¤ç³»ç»Ÿæ¶ˆæ¯
     */
    @PreAuthorize("@ss.hasPermi('system:message:remove')")
    @Log(title = "系统消息", businessType = BusinessType.DELETE)
    @DeleteMapping("/{messageIds}")
    public AjaxResult remove(@PathVariable Long[] messageIds) {
        return toAjax(sysMessageService.deleteSysMessageByMessageIds(messageIds));
    }
    /**
     * æ ‡è®°æ¶ˆæ¯ä¸ºå·²è¯»
     */
    @Log(title = "系统消息", businessType = BusinessType.UPDATE)
    @PutMapping("/read/{messageId}")
    public AjaxResult markAsRead(@PathVariable Long messageId) {
        return toAjax(sysMessageService.markMessageAsRead(messageId));
    }
    /**
     * æ ‡è®°æ‰€æœ‰æ¶ˆæ¯ä¸ºå·²è¯»
     */
    @Log(title = "系统消息", businessType = BusinessType.UPDATE)
    @PutMapping("/read/all")
    public AjaxResult markAllAsRead() {
        Long userId = SecurityUtils.getUserId();
        return toAjax(sysMessageService.markAllMessagesAsRead(userId));
    }
}
ruoyi-framework/src/main/java/com/ruoyi/framework/config/AsyncConfig.java
New file
@@ -0,0 +1,40 @@
package com.ruoyi.framework.config;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
/**
 * å¼‚步任务配置
 * ç”¨äºŽæ”¯æŒ@Async注解的异步方法执行
 *
 * @author ruoyi
 */
@Configuration
@EnableAsync
public class AsyncConfig {
    /**
     * è‡ªå®šä¹‰å¼‚步任务线程池
     */
    @Bean(name = "taskExecutor")
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // æ ¸å¿ƒçº¿ç¨‹æ•°
        executor.setCorePoolSize(5);
        // æœ€å¤§çº¿ç¨‹æ•°
        executor.setMaxPoolSize(10);
        // é˜Ÿåˆ—容量
        executor.setQueueCapacity(100);
        // çº¿ç¨‹åç§°å‰ç¼€
        executor.setThreadNamePrefix("async-task-");
        // æ‹’绝策略:由调用线程执行
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        // åˆå§‹åŒ–
        executor.initialize();
        return executor;
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/domain/SysMessage.java
New file
@@ -0,0 +1,186 @@
package com.ruoyi.system.domain;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.core.domain.BaseEntity;
/**
 * ç³»ç»Ÿæ¶ˆæ¯å¯¹è±¡ sys_message
 *
 * @author ruoyi
 * @date 2025-10-25
 */
public class SysMessage extends BaseEntity {
    private static final long serialVersionUID = 1L;
    /** æ¶ˆæ¯ID */
    private Long messageId;
    /** æ¶ˆæ¯ç±»åž‹ï¼šCREATE-创建成功,PUSH-任务推送,STATUS-状态变更,ASSIGN-分配任务 */
    @Excel(name = "消息类型")
    private String messageType;
    /** æ¶ˆæ¯æ ‡é¢˜ */
    @Excel(name = "消息标题")
    private String messageTitle;
    /** æ¶ˆæ¯å†…容 */
    @Excel(name = "消息内容")
    private String messageContent;
    /** å…³è”任务ID */
    private Long taskId;
    /** ä»»åŠ¡ç¼–å· */
    @Excel(name = "任务编号")
    private String taskCode;
    /** æŽ¥æ”¶äººID */
    private Long receiverId;
    /** æŽ¥æ”¶äººå§“名 */
    @Excel(name = "接收人姓名")
    private String receiverName;
    /** å‘送人ID */
    private Long senderId;
    /** å‘送人姓名 */
    @Excel(name = "发送人姓名")
    private String senderName;
    /** æ˜¯å¦å·²è¯»ï¼š0-未读,1-已读 */
    @Excel(name = "是否已读", readConverterExp = "0=未读,1=已读")
    private String isRead;
    /** è¯»å–æ—¶é—´ */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date readTime;
    /** åˆ é™¤æ ‡å¿— */
    private String delFlag;
    public void setMessageId(Long messageId) {
        this.messageId = messageId;
    }
    public Long getMessageId() {
        return messageId;
    }
    public void setMessageType(String messageType) {
        this.messageType = messageType;
    }
    public String getMessageType() {
        return messageType;
    }
    public void setMessageTitle(String messageTitle) {
        this.messageTitle = messageTitle;
    }
    public String getMessageTitle() {
        return messageTitle;
    }
    public void setMessageContent(String messageContent) {
        this.messageContent = messageContent;
    }
    public String getMessageContent() {
        return messageContent;
    }
    public void setTaskId(Long taskId) {
        this.taskId = taskId;
    }
    public Long getTaskId() {
        return taskId;
    }
    public void setTaskCode(String taskCode) {
        this.taskCode = taskCode;
    }
    public String getTaskCode() {
        return taskCode;
    }
    public void setReceiverId(Long receiverId) {
        this.receiverId = receiverId;
    }
    public Long getReceiverId() {
        return receiverId;
    }
    public void setReceiverName(String receiverName) {
        this.receiverName = receiverName;
    }
    public String getReceiverName() {
        return receiverName;
    }
    public void setSenderId(Long senderId) {
        this.senderId = senderId;
    }
    public Long getSenderId() {
        return senderId;
    }
    public void setSenderName(String senderName) {
        this.senderName = senderName;
    }
    public String getSenderName() {
        return senderName;
    }
    public void setIsRead(String isRead) {
        this.isRead = isRead;
    }
    public String getIsRead() {
        return isRead;
    }
    public void setReadTime(Date readTime) {
        this.readTime = readTime;
    }
    public Date getReadTime() {
        return readTime;
    }
    public void setDelFlag(String delFlag) {
        this.delFlag = delFlag;
    }
    public String getDelFlag() {
        return delFlag;
    }
    @Override
    public String toString() {
        return "SysMessage{" +
                "messageId=" + messageId +
                ", messageType='" + messageType + '\'' +
                ", messageTitle='" + messageTitle + '\'' +
                ", messageContent='" + messageContent + '\'' +
                ", taskId=" + taskId +
                ", taskCode='" + taskCode + '\'' +
                ", receiverId=" + receiverId +
                ", receiverName='" + receiverName + '\'' +
                ", senderId=" + senderId +
                ", senderName='" + senderName + '\'' +
                ", isRead='" + isRead + '\'' +
                ", readTime=" + readTime +
                ", delFlag='" + delFlag + '\'' +
                '}';
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/event/TaskAssignedEvent.java
New file
@@ -0,0 +1,68 @@
package com.ruoyi.system.event;
import java.util.List;
/**
 * ä»»åŠ¡åˆ†é…äº‹ä»¶
 *
 * @author ruoyi
 * @date 2025-10-25
 */
public class TaskAssignedEvent extends TaskEvent {
    private static final long serialVersionUID = 1L;
    /** æ‰§è¡ŒäººID列表 */
    private List<Long> assigneeIds;
    /** æ‰§è¡Œäººå§“名列表 */
    private List<String> assigneeNames;
    /** åˆ†é…äººID */
    private Long assignerId;
    /** åˆ†é…äººå§“名 */
    private String assignerName;
    public TaskAssignedEvent(Object source, Long taskId, String taskCode,
                            List<Long> assigneeIds, List<String> assigneeNames,
                            Long assignerId, String assignerName) {
        super(source, taskId, taskCode, assignerId, assignerName);
        this.assigneeIds = assigneeIds;
        this.assigneeNames = assigneeNames;
        this.assignerId = assignerId;
        this.assignerName = assignerName;
    }
    public List<Long> getAssigneeIds() {
        return assigneeIds;
    }
    public void setAssigneeIds(List<Long> assigneeIds) {
        this.assigneeIds = assigneeIds;
    }
    public List<String> getAssigneeNames() {
        return assigneeNames;
    }
    public void setAssigneeNames(List<String> assigneeNames) {
        this.assigneeNames = assigneeNames;
    }
    public Long getAssignerId() {
        return assignerId;
    }
    public void setAssignerId(Long assignerId) {
        this.assignerId = assignerId;
    }
    public String getAssignerName() {
        return assignerName;
    }
    public void setAssignerName(String assignerName) {
        this.assignerName = assignerName;
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/event/TaskCreatedEvent.java
New file
@@ -0,0 +1,55 @@
package com.ruoyi.system.event;
import java.util.List;
/**
 * ä»»åŠ¡åˆ›å»ºäº‹ä»¶
 *
 * @author ruoyi
 * @date 2025-10-25
 */
public class TaskCreatedEvent extends TaskEvent {
    private static final long serialVersionUID = 1L;
    /** ä»»åŠ¡ç±»åž‹ */
    private String taskType;
    /** åˆ›å»ºäººID */
    private Long creatorId;
    /** åˆ›å»ºäººå§“名 */
    private String creatorName;
    public TaskCreatedEvent(Object source, Long taskId, String taskCode, String taskType,
                           Long creatorId, String creatorName) {
        super(source, taskId, taskCode, creatorId, creatorName);
        this.taskType = taskType;
        this.creatorId = creatorId;
        this.creatorName = creatorName;
    }
    public String getTaskType() {
        return taskType;
    }
    public void setTaskType(String taskType) {
        this.taskType = taskType;
    }
    public Long getCreatorId() {
        return creatorId;
    }
    public void setCreatorId(Long creatorId) {
        this.creatorId = creatorId;
    }
    public String getCreatorName() {
        return creatorName;
    }
    public void setCreatorName(String creatorName) {
        this.creatorName = creatorName;
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/event/TaskEvent.java
New file
@@ -0,0 +1,72 @@
package com.ruoyi.system.event;
import org.springframework.context.ApplicationEvent;
/**
 * ä»»åŠ¡äº‹ä»¶åŸºç±»
 *
 * @author ruoyi
 * @date 2025-10-25
 */
public abstract class TaskEvent extends ApplicationEvent {
    private static final long serialVersionUID = 1L;
    /** ä»»åŠ¡ID */
    private Long taskId;
    /** ä»»åŠ¡ç¼–å· */
    private String taskCode;
    /** æ“ä½œäººID */
    private Long operatorId;
    /** æ“ä½œäººå§“名 */
    private String operatorName;
    public TaskEvent(Object source, Long taskId, String taskCode) {
        super(source);
        this.taskId = taskId;
        this.taskCode = taskCode;
    }
    public TaskEvent(Object source, Long taskId, String taskCode, Long operatorId, String operatorName) {
        super(source);
        this.taskId = taskId;
        this.taskCode = taskCode;
        this.operatorId = operatorId;
        this.operatorName = operatorName;
    }
    public Long getTaskId() {
        return taskId;
    }
    public void setTaskId(Long taskId) {
        this.taskId = taskId;
    }
    public String getTaskCode() {
        return taskCode;
    }
    public void setTaskCode(String taskCode) {
        this.taskCode = taskCode;
    }
    public Long getOperatorId() {
        return operatorId;
    }
    public void setOperatorId(Long operatorId) {
        this.operatorId = operatorId;
    }
    public String getOperatorName() {
        return operatorName;
    }
    public void setOperatorName(String operatorName) {
        this.operatorName = operatorName;
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/event/TaskStatusChangedEvent.java
New file
@@ -0,0 +1,93 @@
package com.ruoyi.system.event;
import java.util.List;
/**
 * ä»»åŠ¡çŠ¶æ€å˜æ›´äº‹ä»¶
 *
 * @author ruoyi
 * @date 2025-10-25
 */
public class TaskStatusChangedEvent extends TaskEvent {
    private static final long serialVersionUID = 1L;
    /** æ—§çŠ¶æ€ */
    private String oldStatus;
    /** æ–°çŠ¶æ€ */
    private String newStatus;
    /** æ—§çŠ¶æ€æè¿° */
    private String oldStatusDesc;
    /** æ–°çŠ¶æ€æè¿° */
    private String newStatusDesc;
    /** æ‰§è¡ŒäººID列表 */
    private List<Long> assigneeIds;
    /** åˆ›å»ºäººID */
    private Long creatorId;
    public TaskStatusChangedEvent(Object source, Long taskId, String taskCode,
                                 String oldStatus, String newStatus,
                                 String oldStatusDesc, String newStatusDesc,
                                 List<Long> assigneeIds, Long creatorId) {
        super(source, taskId, taskCode);
        this.oldStatus = oldStatus;
        this.newStatus = newStatus;
        this.oldStatusDesc = oldStatusDesc;
        this.newStatusDesc = newStatusDesc;
        this.assigneeIds = assigneeIds;
        this.creatorId = creatorId;
    }
    public String getOldStatus() {
        return oldStatus;
    }
    public void setOldStatus(String oldStatus) {
        this.oldStatus = oldStatus;
    }
    public String getNewStatus() {
        return newStatus;
    }
    public void setNewStatus(String newStatus) {
        this.newStatus = newStatus;
    }
    public String getOldStatusDesc() {
        return oldStatusDesc;
    }
    public void setOldStatusDesc(String oldStatusDesc) {
        this.oldStatusDesc = oldStatusDesc;
    }
    public String getNewStatusDesc() {
        return newStatusDesc;
    }
    public void setNewStatusDesc(String newStatusDesc) {
        this.newStatusDesc = newStatusDesc;
    }
    public List<Long> getAssigneeIds() {
        return assigneeIds;
    }
    public void setAssigneeIds(List<Long> assigneeIds) {
        this.assigneeIds = assigneeIds;
    }
    public Long getCreatorId() {
        return creatorId;
    }
    public void setCreatorId(Long creatorId) {
        this.creatorId = creatorId;
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/listener/TaskMessageListener.java
New file
@@ -0,0 +1,224 @@
package com.ruoyi.system.listener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.system.domain.SysMessage;
import com.ruoyi.system.event.TaskCreatedEvent;
import com.ruoyi.system.event.TaskAssignedEvent;
import com.ruoyi.system.event.TaskStatusChangedEvent;
import com.ruoyi.system.mapper.SysMessageMapper;
import com.ruoyi.system.mapper.SysUserMapper;
import com.ruoyi.common.core.domain.entity.SysUser;
/**
 * ä»»åŠ¡æ¶ˆæ¯ç›‘å¬å™¨
 * ç›‘听任务相关事件并保存消息到消息库
 *
 * @author ruoyi
 * @date 2025-10-25
 */
@Component
public class TaskMessageListener {
    private static final Logger log = LoggerFactory.getLogger(TaskMessageListener.class);
    @Autowired
    private SysMessageMapper sysMessageMapper;
    @Autowired
    private SysUserMapper sysUserMapper;
    /**
     * ç›‘听任务创建事件
     *
     * @param event ä»»åŠ¡åˆ›å»ºäº‹ä»¶
     */
    @Async
    @EventListener
    public void handleTaskCreatedEvent(TaskCreatedEvent event) {
        try {
            log.info("收到任务创建事件,任务ID:{},任务编号:{}", event.getTaskId(), event.getTaskCode());
            // èŽ·å–åˆ›å»ºäººä¿¡æ¯
            SysUser creator = sysUserMapper.selectUserById(event.getCreatorId());
            if (creator == null) {
                log.warn("找不到创建人信息,用户ID:{}", event.getCreatorId());
                return;
            }
            // åˆ›å»ºæ¶ˆæ¯
            SysMessage message = new SysMessage();
            message.setMessageType("CREATE");
            message.setMessageTitle("任务创建成功");
            message.setMessageContent("您创建的任务已成功提交");
            message.setTaskId(event.getTaskId());
            message.setTaskCode(event.getTaskCode());
            message.setReceiverId(event.getCreatorId());
            message.setReceiverName(creator.getNickName());
            message.setSenderId(event.getCreatorId());
            message.setSenderName("系统");
            message.setIsRead("0");
            message.setCreateTime(DateUtils.getNowDate());
            message.setDelFlag("0");
            // ä¿å­˜æ¶ˆæ¯
            sysMessageMapper.insertSysMessage(message);
            log.info("任务创建消息已保存,消息ID:{}", message.getMessageId());
        } catch (Exception e) {
            log.error("处理任务创建事件失败", e);
        }
    }
    /**
     * ç›‘听任务分配事件
     *
     * @param event ä»»åŠ¡åˆ†é…äº‹ä»¶
     */
    @Async
    @EventListener
    public void handleTaskAssignedEvent(TaskAssignedEvent event) {
        try {
            log.info("收到任务分配事件,任务ID:{},任务编号:{},执行人数量:{}",
                    event.getTaskId(), event.getTaskCode(),
                    event.getAssigneeIds() != null ? event.getAssigneeIds().size() : 0);
            if (event.getAssigneeIds() == null || event.getAssigneeIds().isEmpty()) {
                log.warn("执行人ID列表为空,无法推送消息");
                return;
            }
            // ç»™æ¯ä¸ªæ‰§è¡Œäººå‘送消息
            for (int i = 0; i < event.getAssigneeIds().size(); i++) {
                Long assigneeId = event.getAssigneeIds().get(i);
                // èŽ·å–æ‰§è¡Œäººä¿¡æ¯
                SysUser assignee = sysUserMapper.selectUserById(assigneeId);
                if (assignee == null) {
                    log.warn("找不到执行人信息,用户ID:{}", assigneeId);
                    continue;
                }
                // åˆ›å»ºæ¶ˆæ¯
                SysMessage message = new SysMessage();
                message.setMessageType("PUSH");
                message.setMessageTitle("任务推送");
                message.setMessageContent("您有新的任务,请及时处理");
                message.setTaskId(event.getTaskId());
                message.setTaskCode(event.getTaskCode());
                message.setReceiverId(assigneeId);
                message.setReceiverName(assignee.getNickName());
                message.setSenderId(event.getAssignerId());
                message.setSenderName(event.getAssignerName() != null ? event.getAssignerName() : "系统");
                message.setIsRead("0");
                message.setCreateTime(DateUtils.getNowDate());
                message.setDelFlag("0");
                // ä¿å­˜æ¶ˆæ¯
                sysMessageMapper.insertSysMessage(message);
                log.info("任务分配消息已保存,消息ID:{},接收人:{}", message.getMessageId(), assignee.getNickName());
            }
        } catch (Exception e) {
            log.error("处理任务分配事件失败", e);
        }
    }
    /**
     * ç›‘听任务状态变更事件
     *
     * @param event ä»»åŠ¡çŠ¶æ€å˜æ›´äº‹ä»¶
     */
    @Async
    @EventListener
    public void handleTaskStatusChangedEvent(TaskStatusChangedEvent event) {
        try {
            log.info("收到任务状态变更事件,任务ID:{},旧状态:{},新状态:{}",
                    event.getTaskId(), event.getOldStatus(), event.getNewStatus());
            // æž„建状态变更内容
            String statusContent = getStatusChangeContent(event.getNewStatus(), event.getNewStatusDesc());
            // æ”¶é›†æ‰€æœ‰éœ€è¦é€šçŸ¥çš„用户ID(执行人+创建人,去重)
            java.util.Set<Long> receiverIds = new java.util.HashSet<>();
            // æ·»åŠ æ‰§è¡Œäºº
            if (event.getAssigneeIds() != null) {
                receiverIds.addAll(event.getAssigneeIds());
            }
            // æ·»åŠ åˆ›å»ºäººï¼ˆå¦‚æžœä¸æ˜¯æ‰§è¡Œäººï¼‰
            if (event.getCreatorId() != null) {
                receiverIds.add(event.getCreatorId());
            }
            // ç»™æ¯ä¸ªç”¨æˆ·å‘送消息
            for (Long receiverId : receiverIds) {
                SysUser user = sysUserMapper.selectUserById(receiverId);
                if (user == null) {
                    log.warn("找不到用户信息,用户ID:{}", receiverId);
                    continue;
                }
                // åˆ›å»ºæ¶ˆæ¯
                SysMessage message = new SysMessage();
                message.setMessageType("STATUS");
                message.setMessageTitle("任务状态变更");
                message.setMessageContent(statusContent);
                message.setTaskId(event.getTaskId());
                message.setTaskCode(event.getTaskCode());
                message.setReceiverId(receiverId);
                message.setReceiverName(user.getNickName());
                message.setSenderId(event.getCreatorId());
                message.setSenderName("系统");
                message.setIsRead("0");
                message.setCreateTime(DateUtils.getNowDate());
                message.setDelFlag("0");
                // ä¿å­˜æ¶ˆæ¯
                sysMessageMapper.insertSysMessage(message);
                log.info("任务状态变更消息已保存,消息ID:{},新状态:{},接收人:{}",
                        message.getMessageId(), event.getNewStatus(), user.getNickName());
            }
        } catch (Exception e) {
            log.error("处理任务状态变更事件失败", e);
        }
    }
    /**
     * æ ¹æ®çŠ¶æ€èŽ·å–çŠ¶æ€å˜æ›´å†…å®¹
     *
     * @param status ä»»åŠ¡çŠ¶æ€
     * @param statusDesc çŠ¶æ€æè¿°
     * @return çŠ¶æ€å˜æ›´å†…å®¹
     */
    private String getStatusChangeContent(String status, String statusDesc) {
        if (statusDesc != null && !statusDesc.isEmpty()) {
            return "任务状态变更为:" + statusDesc;
        }
        switch (status) {
            case "PENDING":
                return "任务状态变更为:待处理";
            case "DEPARTING":
            case "DEPARTED":
                return "任务状态变更为:已出发";
            case "ARRIVED":
                return "任务状态变更为:已到达";
            case "RETURNING":
                return "任务状态变更为:返程中";
            case "COMPLETED":
                return "任务状态变更为:已完成";
            case "CANCELLED":
                return "任务状态变更为:已取消";
            default:
                return "任务状态已更新";
        }
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysMessageMapper.java
New file
@@ -0,0 +1,94 @@
package com.ruoyi.system.mapper;
import java.util.List;
import com.ruoyi.system.domain.SysMessage;
import org.apache.ibatis.annotations.Param;
/**
 * ç³»ç»Ÿæ¶ˆæ¯Mapper接口
 *
 * @author ruoyi
 * @date 2025-10-25
 */
public interface SysMessageMapper {
    /**
     * æŸ¥è¯¢ç³»ç»Ÿæ¶ˆæ¯
     *
     * @param messageId ç³»ç»Ÿæ¶ˆæ¯ä¸»é”®
     * @return ç³»ç»Ÿæ¶ˆæ¯
     */
    public SysMessage selectSysMessageByMessageId(Long messageId);
    /**
     * æŸ¥è¯¢ç³»ç»Ÿæ¶ˆæ¯åˆ—表
     *
     * @param sysMessage ç³»ç»Ÿæ¶ˆæ¯
     * @return ç³»ç»Ÿæ¶ˆæ¯é›†åˆ
     */
    public List<SysMessage> selectSysMessageList(SysMessage sysMessage);
    /**
     * æŸ¥è¯¢ç”¨æˆ·çš„æ¶ˆæ¯åˆ—表
     *
     * @param receiverId æŽ¥æ”¶äººID
     * @return ç³»ç»Ÿæ¶ˆæ¯é›†åˆ
     */
    public List<SysMessage> selectSysMessageListByReceiverId(@Param("receiverId") Long receiverId);
    /**
     * æŸ¥è¯¢ç”¨æˆ·æœªè¯»æ¶ˆæ¯æ•°é‡
     *
     * @param receiverId æŽ¥æ”¶äººID
     * @return æœªè¯»æ¶ˆæ¯æ•°é‡
     */
    public int countUnreadMessageByReceiverId(@Param("receiverId") Long receiverId);
    /**
     * æ–°å¢žç³»ç»Ÿæ¶ˆæ¯
     *
     * @param sysMessage ç³»ç»Ÿæ¶ˆæ¯
     * @return ç»“æžœ
     */
    public int insertSysMessage(SysMessage sysMessage);
    /**
     * ä¿®æ”¹ç³»ç»Ÿæ¶ˆæ¯
     *
     * @param sysMessage ç³»ç»Ÿæ¶ˆæ¯
     * @return ç»“æžœ
     */
    public int updateSysMessage(SysMessage sysMessage);
    /**
     * åˆ é™¤ç³»ç»Ÿæ¶ˆæ¯
     *
     * @param messageId ç³»ç»Ÿæ¶ˆæ¯ä¸»é”®
     * @return ç»“æžœ
     */
    public int deleteSysMessageByMessageId(Long messageId);
    /**
     * æ‰¹é‡åˆ é™¤ç³»ç»Ÿæ¶ˆæ¯
     *
     * @param messageIds éœ€è¦åˆ é™¤çš„æ•°æ®ä¸»é”®é›†åˆ
     * @return ç»“æžœ
     */
    public int deleteSysMessageByMessageIds(Long[] messageIds);
    /**
     * æ ‡è®°æ¶ˆæ¯ä¸ºå·²è¯»
     *
     * @param messageId æ¶ˆæ¯ID
     * @return ç»“æžœ
     */
    public int markMessageAsRead(@Param("messageId") Long messageId);
    /**
     * æ ‡è®°ç”¨æˆ·æ‰€æœ‰æ¶ˆæ¯ä¸ºå·²è¯»
     *
     * @param receiverId æŽ¥æ”¶äººID
     * @return ç»“æžœ
     */
    public int markAllMessagesAsRead(@Param("receiverId") Long receiverId);
}
ruoyi-system/src/main/java/com/ruoyi/system/service/ISysMessageService.java
New file
@@ -0,0 +1,120 @@
package com.ruoyi.system.service;
import java.util.List;
import com.ruoyi.system.domain.SysMessage;
import com.ruoyi.system.domain.SysTask;
/**
 * ç³»ç»Ÿæ¶ˆæ¯Service接口
 *
 * @author ruoyi
 * @date 2025-10-25
 */
public interface ISysMessageService {
    /**
     * æŸ¥è¯¢ç³»ç»Ÿæ¶ˆæ¯
     *
     * @param messageId ç³»ç»Ÿæ¶ˆæ¯ä¸»é”®
     * @return ç³»ç»Ÿæ¶ˆæ¯
     */
    public SysMessage selectSysMessageByMessageId(Long messageId);
    /**
     * æŸ¥è¯¢ç³»ç»Ÿæ¶ˆæ¯åˆ—表
     *
     * @param sysMessage ç³»ç»Ÿæ¶ˆæ¯
     * @return ç³»ç»Ÿæ¶ˆæ¯é›†åˆ
     */
    public List<SysMessage> selectSysMessageList(SysMessage sysMessage);
    /**
     * æŸ¥è¯¢ç”¨æˆ·çš„æ¶ˆæ¯åˆ—表
     *
     * @param receiverId æŽ¥æ”¶äººID
     * @return ç³»ç»Ÿæ¶ˆæ¯é›†åˆ
     */
    public List<SysMessage> selectSysMessageListByReceiverId(Long receiverId);
    /**
     * æŸ¥è¯¢ç”¨æˆ·æœªè¯»æ¶ˆæ¯æ•°é‡
     *
     * @param receiverId æŽ¥æ”¶äººID
     * @return æœªè¯»æ¶ˆæ¯æ•°é‡
     */
    public int countUnreadMessageByReceiverId(Long receiverId);
    /**
     * æ–°å¢žç³»ç»Ÿæ¶ˆæ¯
     *
     * @param sysMessage ç³»ç»Ÿæ¶ˆæ¯
     * @return ç»“æžœ
     */
    public int insertSysMessage(SysMessage sysMessage);
    /**
     * ä¿®æ”¹ç³»ç»Ÿæ¶ˆæ¯
     *
     * @param sysMessage ç³»ç»Ÿæ¶ˆæ¯
     * @return ç»“æžœ
     */
    public int updateSysMessage(SysMessage sysMessage);
    /**
     * æ‰¹é‡åˆ é™¤ç³»ç»Ÿæ¶ˆæ¯
     *
     * @param messageIds éœ€è¦åˆ é™¤çš„系统消息主键集合
     * @return ç»“æžœ
     */
    public int deleteSysMessageByMessageIds(Long[] messageIds);
    /**
     * åˆ é™¤ç³»ç»Ÿæ¶ˆæ¯ä¿¡æ¯
     *
     * @param messageId ç³»ç»Ÿæ¶ˆæ¯ä¸»é”®
     * @return ç»“æžœ
     */
    public int deleteSysMessageByMessageId(Long messageId);
    /**
     * æ ‡è®°æ¶ˆæ¯ä¸ºå·²è¯»
     *
     * @param messageId æ¶ˆæ¯ID
     * @return ç»“æžœ
     */
    public int markMessageAsRead(Long messageId);
    /**
     * æ ‡è®°ç”¨æˆ·æ‰€æœ‰æ¶ˆæ¯ä¸ºå·²è¯»
     *
     * @param receiverId æŽ¥æ”¶äººID
     * @return ç»“æžœ
     */
    public int markAllMessagesAsRead(Long receiverId);
    // ========== æ¶ˆæ¯æŽ¨é€ä¸šåŠ¡æ–¹æ³• ==========
    /**
     * æŽ¨é€ä»»åŠ¡åˆ›å»ºæˆåŠŸæ¶ˆæ¯ï¼ˆç»™åˆ›å»ºäººï¼‰
     *
     * @param task ä»»åŠ¡å¯¹è±¡
     */
    public void pushTaskCreateMessage(SysTask task);
    /**
     * æŽ¨é€ä»»åŠ¡åˆ†é…æ¶ˆæ¯ï¼ˆç»™æ‰§è¡Œäººï¼‰
     *
     * @param task ä»»åŠ¡å¯¹è±¡
     * @param assigneeIds æ‰§è¡ŒäººID列表
     */
    public void pushTaskAssignMessage(SysTask task, List<Long> assigneeIds);
    /**
     * æŽ¨é€ä»»åŠ¡çŠ¶æ€å˜æ›´æ¶ˆæ¯ï¼ˆç»™ç›¸å…³äººå‘˜ï¼‰
     *
     * @param task ä»»åŠ¡å¯¹è±¡
     * @param oldStatus æ—§çŠ¶æ€
     * @param newStatus æ–°çŠ¶æ€
     */
    public void pushTaskStatusChangeMessage(SysTask task, String oldStatus, String newStatus);
}
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysMessageServiceImpl.java
New file
@@ -0,0 +1,351 @@
package com.ruoyi.system.service.impl;
import java.util.Date;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.mapper.SysMessageMapper;
import com.ruoyi.system.mapper.SysUserMapper;
import com.ruoyi.system.mapper.SysTaskAssigneeMapper;
import com.ruoyi.system.domain.SysMessage;
import com.ruoyi.system.domain.SysTask;
import com.ruoyi.system.domain.SysTaskAssignee;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.system.service.ISysMessageService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
 * ç³»ç»Ÿæ¶ˆæ¯Service业务层处理
 *
 * @author ruoyi
 * @date 2025-10-25
 */
@Service
public class SysMessageServiceImpl implements ISysMessageService {
    private static final Logger log = LoggerFactory.getLogger(SysMessageServiceImpl.class);
    @Autowired
    private SysMessageMapper sysMessageMapper;
    @Autowired
    private SysUserMapper sysUserMapper;
    @Autowired
    private SysTaskAssigneeMapper sysTaskAssigneeMapper;
    /**
     * æŸ¥è¯¢ç³»ç»Ÿæ¶ˆæ¯
     *
     * @param messageId ç³»ç»Ÿæ¶ˆæ¯ä¸»é”®
     * @return ç³»ç»Ÿæ¶ˆæ¯
     */
    @Override
    public SysMessage selectSysMessageByMessageId(Long messageId) {
        return sysMessageMapper.selectSysMessageByMessageId(messageId);
    }
    /**
     * æŸ¥è¯¢ç³»ç»Ÿæ¶ˆæ¯åˆ—表
     *
     * @param sysMessage ç³»ç»Ÿæ¶ˆæ¯
     * @return ç³»ç»Ÿæ¶ˆæ¯
     */
    @Override
    public List<SysMessage> selectSysMessageList(SysMessage sysMessage) {
        return sysMessageMapper.selectSysMessageList(sysMessage);
    }
    /**
     * æŸ¥è¯¢ç”¨æˆ·çš„æ¶ˆæ¯åˆ—表
     *
     * @param receiverId æŽ¥æ”¶äººID
     * @return ç³»ç»Ÿæ¶ˆæ¯é›†åˆ
     */
    @Override
    public List<SysMessage> selectSysMessageListByReceiverId(Long receiverId) {
        return sysMessageMapper.selectSysMessageListByReceiverId(receiverId);
    }
    /**
     * æŸ¥è¯¢ç”¨æˆ·æœªè¯»æ¶ˆæ¯æ•°é‡
     *
     * @param receiverId æŽ¥æ”¶äººID
     * @return æœªè¯»æ¶ˆæ¯æ•°é‡
     */
    @Override
    public int countUnreadMessageByReceiverId(Long receiverId) {
        return sysMessageMapper.countUnreadMessageByReceiverId(receiverId);
    }
    /**
     * æ–°å¢žç³»ç»Ÿæ¶ˆæ¯
     *
     * @param sysMessage ç³»ç»Ÿæ¶ˆæ¯
     * @return ç»“æžœ
     */
    @Override
    public int insertSysMessage(SysMessage sysMessage) {
        if (sysMessage.getCreateTime() == null) {
            sysMessage.setCreateTime(DateUtils.getNowDate());
        }
        if (sysMessage.getIsRead() == null) {
            sysMessage.setIsRead("0");
        }
        if (sysMessage.getDelFlag() == null) {
            sysMessage.setDelFlag("0");
        }
        return sysMessageMapper.insertSysMessage(sysMessage);
    }
    /**
     * ä¿®æ”¹ç³»ç»Ÿæ¶ˆæ¯
     *
     * @param sysMessage ç³»ç»Ÿæ¶ˆæ¯
     * @return ç»“æžœ
     */
    @Override
    public int updateSysMessage(SysMessage sysMessage) {
        sysMessage.setUpdateTime(DateUtils.getNowDate());
        return sysMessageMapper.updateSysMessage(sysMessage);
    }
    /**
     * æ‰¹é‡åˆ é™¤ç³»ç»Ÿæ¶ˆæ¯
     *
     * @param messageIds éœ€è¦åˆ é™¤çš„系统消息主键
     * @return ç»“æžœ
     */
    @Override
    public int deleteSysMessageByMessageIds(Long[] messageIds) {
        return sysMessageMapper.deleteSysMessageByMessageIds(messageIds);
    }
    /**
     * åˆ é™¤ç³»ç»Ÿæ¶ˆæ¯ä¿¡æ¯
     *
     * @param messageId ç³»ç»Ÿæ¶ˆæ¯ä¸»é”®
     * @return ç»“æžœ
     */
    @Override
    public int deleteSysMessageByMessageId(Long messageId) {
        return sysMessageMapper.deleteSysMessageByMessageId(messageId);
    }
    /**
     * æ ‡è®°æ¶ˆæ¯ä¸ºå·²è¯»
     *
     * @param messageId æ¶ˆæ¯ID
     * @return ç»“æžœ
     */
    @Override
    public int markMessageAsRead(Long messageId) {
        return sysMessageMapper.markMessageAsRead(messageId);
    }
    /**
     * æ ‡è®°ç”¨æˆ·æ‰€æœ‰æ¶ˆæ¯ä¸ºå·²è¯»
     *
     * @param receiverId æŽ¥æ”¶äººID
     * @return ç»“æžœ
     */
    @Override
    public int markAllMessagesAsRead(Long receiverId) {
        return sysMessageMapper.markAllMessagesAsRead(receiverId);
    }
    // ========== æ¶ˆæ¯æŽ¨é€ä¸šåŠ¡æ–¹æ³• ==========
    /**
     * æŽ¨é€ä»»åŠ¡åˆ›å»ºæˆåŠŸæ¶ˆæ¯ï¼ˆç»™åˆ›å»ºäººï¼‰
     *
     * @param task ä»»åŠ¡å¯¹è±¡
     */
    @Override
    public void pushTaskCreateMessage(SysTask task) {
        try {
            if (task == null || task.getCreatorId() == null) {
                log.warn("任务对象或创建人ID为空,无法推送创建成功消息");
                return;
            }
            // èŽ·å–åˆ›å»ºäººä¿¡æ¯
            SysUser creator = sysUserMapper.selectUserById(task.getCreatorId());
            if (creator == null) {
                log.warn("找不到创建人信息,用户ID:{}", task.getCreatorId());
                return;
            }
            SysMessage message = new SysMessage();
            message.setMessageType("CREATE");
            message.setMessageTitle("任务创建成功");
            message.setMessageContent("您创建的任务已成功提交");
            message.setTaskId(task.getTaskId());
            message.setTaskCode(task.getTaskCode());
            message.setReceiverId(task.getCreatorId());
            message.setReceiverName(creator.getNickName());
            message.setSenderId(task.getCreatorId());
            message.setSenderName("系统");
            insertSysMessage(message);
            log.info("推送任务创建成功消息,任务编号:{},接收人:{}", task.getTaskCode(), creator.getNickName());
        } catch (Exception e) {
            log.error("推送任务创建成功消息失败", e);
        }
    }
    /**
     * æŽ¨é€ä»»åŠ¡åˆ†é…æ¶ˆæ¯ï¼ˆç»™æ‰§è¡Œäººï¼‰
     *
     * @param task ä»»åŠ¡å¯¹è±¡
     * @param assigneeIds æ‰§è¡ŒäººID列表
     */
    @Override
    public void pushTaskAssignMessage(SysTask task, List<Long> assigneeIds) {
        try {
            if (task == null || assigneeIds == null || assigneeIds.isEmpty()) {
                log.warn("任务对象或执行人ID列表为空,无法推送任务分配消息");
                return;
            }
            // èŽ·å–åˆ›å»ºäººä¿¡æ¯
            SysUser creator = null;
            if (task.getCreatorId() != null) {
                creator = sysUserMapper.selectUserById(task.getCreatorId());
            }
            String senderName = (creator != null && StringUtils.isNotEmpty(creator.getNickName()))
                ? creator.getNickName() : "系统";
            // ç»™æ¯ä¸ªæ‰§è¡Œäººå‘送消息
            for (Long assigneeId : assigneeIds) {
                SysUser assignee = sysUserMapper.selectUserById(assigneeId);
                if (assignee == null) {
                    log.warn("找不到执行人信息,用户ID:{}", assigneeId);
                    continue;
                }
                SysMessage message = new SysMessage();
                message.setMessageType("PUSH");
                message.setMessageTitle("任务推送");
                message.setMessageContent("您有新的任务,请及时处理");
                message.setTaskId(task.getTaskId());
                message.setTaskCode(task.getTaskCode());
                message.setReceiverId(assigneeId);
                message.setReceiverName(assignee.getNickName());
                message.setSenderId(task.getCreatorId());
                message.setSenderName(senderName);
                insertSysMessage(message);
                log.info("推送任务分配消息,任务编号:{},接收人:{}", task.getTaskCode(), assignee.getNickName());
            }
        } catch (Exception e) {
            log.error("推送任务分配消息失败", e);
        }
    }
    /**
     * æŽ¨é€ä»»åŠ¡çŠ¶æ€å˜æ›´æ¶ˆæ¯ï¼ˆç»™ç›¸å…³äººå‘˜ï¼‰
     *
     * @param task ä»»åŠ¡å¯¹è±¡
     * @param oldStatus æ—§çŠ¶æ€
     * @param newStatus æ–°çŠ¶æ€
     */
    @Override
    public void pushTaskStatusChangeMessage(SysTask task, String oldStatus, String newStatus) {
        try {
            if (task == null || StringUtils.isEmpty(newStatus)) {
                log.warn("任务对象或新状态为空,无法推送状态变更消息");
                return;
            }
            // æž„建状态变更内容
            String statusContent = getStatusChangeContent(newStatus);
            // æŸ¥è¯¢ä»»åŠ¡çš„æ‰€æœ‰æ‰§è¡Œäºº
            List<SysTaskAssignee> assignees = sysTaskAssigneeMapper.selectSysTaskAssigneeByTaskId(task.getTaskId());
            if (assignees != null && !assignees.isEmpty()) {
                // ç»™æ¯ä¸ªæ‰§è¡Œäººå‘送消息
                for (SysTaskAssignee assignee : assignees) {
                    SysUser user = sysUserMapper.selectUserById(assignee.getUserId());
                    if (user == null) {
                        log.warn("找不到执行人信息,用户ID:{}", assignee.getUserId());
                        continue;
                    }
                    SysMessage message = new SysMessage();
                    message.setMessageType("STATUS");
                    message.setMessageTitle("任务状态变更");
                    message.setMessageContent(statusContent);
                    message.setTaskId(task.getTaskId());
                    message.setTaskCode(task.getTaskCode());
                    message.setReceiverId(assignee.getUserId());
                    message.setReceiverName(user.getNickName());
                    message.setSenderId(task.getCreatorId());
                    message.setSenderName("系统");
                    insertSysMessage(message);
                    log.info("推送任务状态变更消息,任务编号:{},新状态:{},接收人:{}",
                            task.getTaskCode(), newStatus, user.getNickName());
                }
            }
            // åŒæ—¶ç»™åˆ›å»ºäººå‘送消息(如果创建人不是执行人)
            if (task.getCreatorId() != null) {
                boolean isCreatorAlsoAssignee = assignees != null && assignees.stream()
                    .anyMatch(a -> a.getUserId().equals(task.getCreatorId()));
                if (!isCreatorAlsoAssignee) {
                    SysUser creator = sysUserMapper.selectUserById(task.getCreatorId());
                    if (creator != null) {
                        SysMessage message = new SysMessage();
                        message.setMessageType("STATUS");
                        message.setMessageTitle("任务状态变更");
                        message.setMessageContent(statusContent);
                        message.setTaskId(task.getTaskId());
                        message.setTaskCode(task.getTaskCode());
                        message.setReceiverId(task.getCreatorId());
                        message.setReceiverName(creator.getNickName());
                        message.setSenderId(task.getCreatorId());
                        message.setSenderName("系统");
                        insertSysMessage(message);
                        log.info("推送任务状态变更消息给创建人,任务编号:{},新状态:{},接收人:{}",
                                task.getTaskCode(), newStatus, creator.getNickName());
                    }
                }
            }
        } catch (Exception e) {
            log.error("推送任务状态变更消息失败", e);
        }
    }
    /**
     * æ ¹æ®çŠ¶æ€èŽ·å–çŠ¶æ€å˜æ›´å†…å®¹
     *
     * @param status ä»»åŠ¡çŠ¶æ€
     * @return çŠ¶æ€å˜æ›´å†…å®¹
     */
    private String getStatusChangeContent(String status) {
        switch (status) {
            case "PENDING":
                return "任务状态变更为:待处理";
            case "DEPARTED":
                return "任务状态变更为:已出发";
            case "ARRIVED":
                return "任务状态变更为:已到达";
            case "RETURNING":
                return "任务状态变更为:返程中";
            case "COMPLETED":
                return "任务状态变更为:已完成";
            case "CANCELLED":
                return "任务状态变更为:已取消";
            default:
                return "任务状态已更新";
        }
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskServiceImpl.java
@@ -42,7 +42,11 @@
import com.ruoyi.system.domain.VehicleInfo;
import com.ruoyi.system.service.ISysTaskService;
import com.ruoyi.system.service.ILegacySystemSyncService;
import com.ruoyi.system.event.TaskCreatedEvent;
import com.ruoyi.system.event.TaskAssignedEvent;
import com.ruoyi.system.event.TaskStatusChangedEvent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
/**
 * ä»»åŠ¡ç®¡ç†Service业务层处理
@@ -79,6 +83,9 @@
    @Autowired(required = false)
    private ILegacySystemSyncService legacySystemSyncService;
    @Autowired
    private ApplicationEventPublisher eventPublisher;
    /**
     * æŸ¥è¯¢ä»»åŠ¡ç®¡ç†
@@ -229,6 +236,38 @@
                         "任务类型:" + createVO.getTaskType(), SecurityUtils.getUserId(), SecurityUtils.getUsername());
        }
        
        // å‘布任务创建事件
        if (result > 0) {
            eventPublisher.publishEvent(new TaskCreatedEvent(
                this,
                task.getTaskId(),
                task.getTaskCode(),
                task.getTaskType(),
                task.getCreatorId(),
                SecurityUtils.getUsername()
            ));
        }
        // å‘布任务分配事件
        if (result > 0 && createVO.getAssignees() != null && !createVO.getAssignees().isEmpty()) {
            List<Long> assigneeIds = createVO.getAssignees().stream()
                .map(assignee -> assignee.getUserId())
                .collect(Collectors.toList());
            List<String> assigneeNames = createVO.getAssignees().stream()
                .map(assignee -> assignee.getUserName())
                .collect(Collectors.toList());
            eventPublisher.publishEvent(new TaskAssignedEvent(
                this,
                task.getTaskId(),
                task.getTaskCode(),
                assigneeIds,
                assigneeNames,
                SecurityUtils.getUserId(),
                SecurityUtils.getUsername()
            ));
        }
        // å¼‚步同步急救转运任务到旧系统
        if (result > 0 && "EMERGENCY_TRANSFER".equals(createVO.getTaskType()) && legacySystemSyncService != null) {
            final Long finalTaskId = task.getTaskId();
@@ -320,19 +359,40 @@
    @Override
    @Transactional
    public int assignTask(Long taskId, Long assigneeId, String remark) {
        SysTask task = new SysTask();
        task.setTaskId(taskId);
        task.setAssigneeId(assigneeId);
        task.setUpdateBy(SecurityUtils.getUsername());
        task.setUpdateTime(DateUtils.getNowDate());
        SysTask task = sysTaskMapper.selectSysTaskByTaskId(taskId);
        if (task == null) {
            throw new RuntimeException("任务不存在");
        }
        
        int result = sysTaskMapper.assignTask(task);
        SysTask updateTask = new SysTask();
        updateTask.setTaskId(taskId);
        updateTask.setAssigneeId(assigneeId);
        updateTask.setUpdateBy(SecurityUtils.getUsername());
        updateTask.setUpdateTime(DateUtils.getNowDate());
        int result = sysTaskMapper.assignTask(updateTask);
        
        // è®°å½•操作日志
        if (result > 0) {
            recordTaskLog(taskId, "ASSIGN", "分配任务", null, 
                         "分配给用户ID:" + assigneeId + ",备注:" + remark, 
                         SecurityUtils.getUserId(), SecurityUtils.getUsername());
        }
        // å‘布任务分配事件
        if (result > 0) {
            List<Long> assigneeIds = new ArrayList<>();
            assigneeIds.add(assigneeId);
            eventPublisher.publishEvent(new TaskAssignedEvent(
                this,
                task.getTaskId(),
                task.getTaskCode(),
                assigneeIds,
                null, // å§“名列表在监听器中查询
                SecurityUtils.getUserId(),
                SecurityUtils.getUsername()
            ));
        }
        
        return result;
@@ -404,6 +464,30 @@
                         locationLog);
        }
        
        // å‘布任务状态变更事件
        if (result > 0) {
            // æŸ¥è¯¢ä»»åŠ¡çš„æ‰€æœ‰æ‰§è¡Œäºº
            List<SysTaskAssignee> assignees = sysTaskAssigneeMapper.selectSysTaskAssigneeByTaskId(taskId);
            List<Long> assigneeIds = null;
            if (assignees != null && !assignees.isEmpty()) {
                assigneeIds = assignees.stream()
                    .map(SysTaskAssignee::getUserId)
                    .collect(Collectors.toList());
            }
            eventPublisher.publishEvent(new TaskStatusChangedEvent(
                this,
                oldTask.getTaskId(),
                oldTask.getTaskCode(),
                oldTaskStatus.getCode(),
                newStatus.getCode(),
                oldTaskStatus.getInfo(),
                newStatus.getInfo(),
                assigneeIds,
                oldTask.getCreatorId()
            ));
        }
        return result;
    }
ruoyi-system/src/main/resources/mapper/system/SysMessageMapper.xml
New file
@@ -0,0 +1,137 @@
<?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.ruoyi.system.mapper.SysMessageMapper">
    <resultMap type="SysMessage" id="SysMessageResult">
        <result property="messageId"    column="message_id"    />
        <result property="messageType"    column="message_type"    />
        <result property="messageTitle"    column="message_title"    />
        <result property="messageContent"    column="message_content"    />
        <result property="taskId"    column="task_id"    />
        <result property="taskCode"    column="task_code"    />
        <result property="receiverId"    column="receiver_id"    />
        <result property="receiverName"    column="receiver_name"    />
        <result property="senderId"    column="sender_id"    />
        <result property="senderName"    column="sender_name"    />
        <result property="isRead"    column="is_read"    />
        <result property="readTime"    column="read_time"    />
        <result property="createTime"    column="create_time"    />
        <result property="updateTime"    column="update_time"    />
        <result property="delFlag"    column="del_flag"    />
    </resultMap>
    <sql id="selectSysMessageVo">
        select message_id, message_type, message_title, message_content, task_id, task_code,
               receiver_id, receiver_name, sender_id, sender_name, is_read, read_time,
               create_time, update_time, del_flag
        from sys_message
    </sql>
    <select id="selectSysMessageList" parameterType="SysMessage" resultMap="SysMessageResult">
        <include refid="selectSysMessageVo"/>
        <where>
            del_flag = '0'
            <if test="messageType != null  and messageType != ''"> and message_type = #{messageType}</if>
            <if test="receiverId != null "> and receiver_id = #{receiverId}</if>
            <if test="taskId != null "> and task_id = #{taskId}</if>
            <if test="isRead != null  and isRead != ''"> and is_read = #{isRead}</if>
        </where>
        order by is_read asc, create_time desc
    </select>
    <select id="selectSysMessageByMessageId" parameterType="Long" resultMap="SysMessageResult">
        <include refid="selectSysMessageVo"/>
        where message_id = #{messageId} and del_flag = '0'
    </select>
    <select id="selectSysMessageListByReceiverId" parameterType="Long" resultMap="SysMessageResult">
        <include refid="selectSysMessageVo"/>
        where receiver_id = #{receiverId} and del_flag = '0'
        order by is_read asc, create_time desc
    </select>
    <select id="countUnreadMessageByReceiverId" parameterType="Long" resultType="int">
        select count(*) from sys_message
        where receiver_id = #{receiverId} and is_read = '0' and del_flag = '0'
    </select>
    <insert id="insertSysMessage" parameterType="SysMessage" useGeneratedKeys="true" keyProperty="messageId">
        insert into sys_message
        <trim prefix="(" suffix=")" suffixOverrides=",">
            <if test="messageType != null and messageType != ''">message_type,</if>
            <if test="messageTitle != null and messageTitle != ''">message_title,</if>
            <if test="messageContent != null and messageContent != ''">message_content,</if>
            <if test="taskId != null">task_id,</if>
            <if test="taskCode != null and taskCode != ''">task_code,</if>
            <if test="receiverId != null">receiver_id,</if>
            <if test="receiverName != null and receiverName != ''">receiver_name,</if>
            <if test="senderId != null">sender_id,</if>
            <if test="senderName != null and senderName != ''">sender_name,</if>
            <if test="isRead != null">is_read,</if>
            <if test="readTime != null">read_time,</if>
            <if test="createTime != null">create_time,</if>
            <if test="delFlag != null">del_flag,</if>
        </trim>
        <trim prefix="values (" suffix=")" suffixOverrides=",">
            <if test="messageType != null and messageType != ''">#{messageType},</if>
            <if test="messageTitle != null and messageTitle != ''">#{messageTitle},</if>
            <if test="messageContent != null and messageContent != ''">#{messageContent},</if>
            <if test="taskId != null">#{taskId},</if>
            <if test="taskCode != null and taskCode != ''">#{taskCode},</if>
            <if test="receiverId != null">#{receiverId},</if>
            <if test="receiverName != null and receiverName != ''">#{receiverName},</if>
            <if test="senderId != null">#{senderId},</if>
            <if test="senderName != null and senderName != ''">#{senderName},</if>
            <if test="isRead != null">#{isRead},</if>
            <if test="readTime != null">#{readTime},</if>
            <if test="createTime != null">#{createTime},</if>
            <if test="delFlag != null">#{delFlag},</if>
        </trim>
    </insert>
    <update id="updateSysMessage" parameterType="SysMessage">
        update sys_message
        <trim prefix="SET" suffixOverrides=",">
            <if test="messageType != null and messageType != ''">message_type = #{messageType},</if>
            <if test="messageTitle != null and messageTitle != ''">message_title = #{messageTitle},</if>
            <if test="messageContent != null and messageContent != ''">message_content = #{messageContent},</if>
            <if test="taskId != null">task_id = #{taskId},</if>
            <if test="taskCode != null and taskCode != ''">task_code = #{taskCode},</if>
            <if test="receiverId != null">receiver_id = #{receiverId},</if>
            <if test="receiverName != null and receiverName != ''">receiver_name = #{receiverName},</if>
            <if test="senderId != null">sender_id = #{senderId},</if>
            <if test="senderName != null and senderName != ''">sender_name = #{senderName},</if>
            <if test="isRead != null">is_read = #{isRead},</if>
            <if test="readTime != null">read_time = #{readTime},</if>
            <if test="updateTime != null">update_time = #{updateTime},</if>
            <if test="delFlag != null">del_flag = #{delFlag},</if>
        </trim>
        where message_id = #{messageId}
    </update>
    <delete id="deleteSysMessageByMessageId" parameterType="Long">
        update sys_message set del_flag = '1' where message_id = #{messageId}
    </delete>
    <delete id="deleteSysMessageByMessageIds" parameterType="Long">
        update sys_message set del_flag = '1' where message_id in
        <foreach item="messageId" collection="array" open="(" separator="," close=")">
            #{messageId}
        </foreach>
    </delete>
    <update id="markMessageAsRead" parameterType="Long">
        update sys_message
        set is_read = '1', read_time = now()
        where message_id = #{messageId} and del_flag = '0'
    </update>
    <update id="markAllMessagesAsRead" parameterType="Long">
        update sys_message
        set is_read = '1', read_time = now()
        where receiver_id = #{receiverId} and is_read = '0' and del_flag = '0'
    </update>
</mapper>
sql/sys_message.sql
New file
@@ -0,0 +1,26 @@
-- ----------------------------
-- ç³»ç»Ÿæ¶ˆæ¯è¡¨
-- ----------------------------
DROP TABLE IF EXISTS `sys_message`;
CREATE TABLE `sys_message` (
  `message_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '消息ID',
  `message_type` varchar(20) NOT NULL COMMENT '消息类型:CREATE-创建成功,PUSH-任务推送,STATUS-状态变更,ASSIGN-分配任务',
  `message_title` varchar(100) NOT NULL COMMENT '消息标题',
  `message_content` varchar(500) NOT NULL COMMENT '消息内容',
  `task_id` bigint(20) DEFAULT NULL COMMENT '关联任务ID',
  `task_code` varchar(50) DEFAULT NULL COMMENT '任务编号',
  `receiver_id` bigint(20) NOT NULL COMMENT '接收人ID',
  `receiver_name` varchar(50) DEFAULT NULL COMMENT '接收人姓名',
  `sender_id` bigint(20) DEFAULT NULL COMMENT '发送人ID',
  `sender_name` varchar(50) DEFAULT NULL COMMENT '发送人姓名',
  `is_read` char(1) DEFAULT '0' COMMENT '是否已读:0-未读,1-已读',
  `read_time` datetime DEFAULT NULL COMMENT '读取时间',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `del_flag` char(1) DEFAULT '0' COMMENT '删除标志:0-正常,1-删除',
  PRIMARY KEY (`message_id`),
  KEY `idx_receiver_id` (`receiver_id`),
  KEY `idx_task_id` (`task_id`),
  KEY `idx_create_time` (`create_time`),
  KEY `idx_is_read` (`is_read`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统消息表';