# 消息管理模块完善实施计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 完善消息管理模块,增加消息发送测试功能,增强消息监控能力,方便管理员测试配置和监控发送状态。 **Architecture:** 创建独立的消息管理页面(`/message/manage`),包含发送测试面板和监控列表。新建后端 MessageManageController 和 Service,提供管理员专用的消息查询和监控接口。复用现有消息发送接口进行测试。 **Tech Stack:** Vue 3, Naive UI, MyBatis-Plus, Spring Boot --- ## 文件结构 ### 前端文件 - Create: `forge-admin-ui/src/views/message/manage.vue` - 消息管理页面(发送测试+监控列表) - Modify: `forge-admin-ui/src/api/message.js` - 新增管理接口调用方法 - Modify: `forge-admin-ui/src/router/index.js` - 新增路由配置(如需要) ### 后端文件 - Create: `forge-plugin-message/controller/MessageManageController.java` - 管理员消息管理控制器 - Create: `forge-plugin-message/service/MessageManageService.java` - 管理员消息服务接口 - Create: `forge-plugin-message/service/impl/MessageManageServiceImpl.java` - 管理员消息服务实现 - Create: `forge-plugin-message/domain/dto/MessageManageQueryDTO.java` - 查询DTO - Create: `forge-plugin-message/domain/vo/MessageManageVO.java` - 消息列表VO - Create: `forge-plugin-message/domain/vo/MessageDetailVO.java` - 消息详情VO - Create: `forge-plugin-message/domain/vo/ReceiverVO.java` - 接收人VO --- ## Task 1: 创建后端DTO和VO类 **Files:** - Create: `forge-plugin-message/domain/dto/MessageManageQueryDTO.java` - Create: `forge-plugin-message/domain/vo/MessageManageVO.java` - Create: `forge-plugin-message/domain/vo/MessageDetailVO.java` - Create: `forge-plugin-message/domain/vo/ReceiverVO.java` - [ ] **Step 1: 创建 MessageManageQueryDTO** 创建文件 `forge/forge-framework/forge-plugin-parent/forge-plugin-message/src/main/java/com/mdframe/forge/plugin/message/domain/dto/MessageManageQueryDTO.java`: ```java package com.mdframe.forge.plugin.message.domain.dto; import lombok.Data; @Data public class MessageManageQueryDTO { private String type; private String channel; private Integer status; private String startTime; private String endTime; private String keyword; } ``` - [ ] **Step 2: 创建 MessageManageVO** 创建文件 `forge/forge-framework/forge-plugin-parent/forge-plugin-message/src/main/java/com/mdframe/forge/plugin/message/domain/vo/MessageManageVO.java`: ```java package com.mdframe.forge.plugin.message.domain.vo; import lombok.Data; import java.time.LocalDateTime; @Data public class MessageManageVO { private Long id; private String title; private String type; private String channel; private Integer status; private Integer receiverCount; private Integer readCount; private Integer unreadCount; private LocalDateTime createTime; private String senderName; } ``` - [ ] **Step 3: 创建 ReceiverVO** 创建文件 `forge/forge-framework/forge-plugin-parent/forge-plugin-message/src/main/java/com/mdframe/forge/plugin/message/domain/vo/ReceiverVO.java`: ```java package com.mdframe.forge.plugin.message.domain.vo; import lombok.Data; import java.time.LocalDateTime; @Data public class ReceiverVO { private Long userId; private String userName; private String orgName; private Integer readFlag; private LocalDateTime readTime; } ``` - [ ] **Step 4: 创建 MessageDetailVO** 创建文件 `forge/forge-framework/forge-plugin-parent/forge-plugin-message/src/main/java/com/mdframe/forge/plugin/message/domain/vo/MessageDetailVO.java`: ```java package com.mdframe.forge.plugin.message.domain.vo; import com.mdframe.forge.plugin.message.domain.entity.SysMessage; import com.mdframe.forge.plugin.message.domain.entity.SysMessageSendRecord; import lombok.Data; import java.util.List; @Data public class MessageDetailVO { private SysMessage message; private SysMessageSendRecord sendRecord; private List receivers; } ``` - [ ] **Step 5: 提交代码** ```bash git add forge/forge-framework/forge-plugin-parent/forge-plugin-message/src/main/java/com/mdframe/forge/plugin/message/domain/dto/MessageManageQueryDTO.java git add forge/forge-framework/forge-plugin-parent/forge-plugin-message/src/main/java/com/mdframe/forge/plugin/message/domain/vo/MessageManageVO.java git add forge/forge-framework/forge-plugin-parent/forge-plugin-message/src/main/java/com/mdframe/forge/plugin/message/domain/vo/ReceiverVO.java git add forge/forge-framework/forge-plugin-parent/forge-plugin-message/src/main/java/com/mdframe/forge/plugin/message/domain/vo/MessageDetailVO.java git commit -m "feat: add message manage DTOs and VOs" ``` --- ## Task 2: 创建后端Service接口和实现 **Files:** - Create: `forge-plugin-message/service/MessageManageService.java` - Create: `forge-plugin-message/service/impl/MessageManageServiceImpl.java` - [ ] **Step 1: 创建 MessageManageService 接口** 创建文件 `forge/forge-framework/forge-plugin-parent/forge-plugin-message/src/main/java/com/mdframe/forge/plugin/message/service/MessageManageService.java`: ```java package com.mdframe.forge.plugin.message.service; import com.baomidou.mybatisplus.core.metadata.IPage; import com.mdframe.forge.plugin.message.domain.dto.MessageManageQueryDTO; import com.mdframe.forge.plugin.message.domain.vo.MessageDetailVO; import com.mdframe.forge.plugin.message.domain.vo.MessageManageVO; public interface MessageManageService { IPage pageMessages(MessageManageQueryDTO query, Integer pageNum, Integer pageSize); MessageDetailVO getMessageDetail(Long messageId); } ``` - [ ] **Step 2: 创建 MessageManageServiceImpl 实现类(查询列表方法)** 创建文件 `forge/forge-framework/forge-plugin-parent/forge-plugin-message/src/main/java/com/mdframe/forge/plugin/message/service/impl/MessageManageServiceImpl.java`: ```java package com.mdframe.forge.plugin.message.service.impl; import cn.hutool.core.util.StrUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.mdframe.forge.plugin.message.domain.dto.MessageManageQueryDTO; import com.mdframe.forge.plugin.message.domain.entity.SysMessage; import com.mdframe.forge.plugin.message.domain.entity.SysMessageReceiver; import com.mdframe.forge.plugin.message.domain.entity.SysMessageSendRecord; import com.mdframe.forge.plugin.message.domain.vo.MessageDetailVO; import com.mdframe.forge.plugin.message.domain.vo.MessageManageVO; import com.mdframe.forge.plugin.message.domain.vo.ReceiverVO; import com.mdframe.forge.plugin.message.mapper.SysMessageMapper; import com.mdframe.forge.plugin.message.mapper.SysMessageReceiverMapper; import com.mdframe.forge.plugin.message.mapper.SysMessageSendRecordMapper; import com.mdframe.forge.plugin.message.service.MessageManageService; import com.mdframe.forge.plugin.system.entity.SysOrg; import com.mdframe.forge.plugin.system.entity.SysUser; import com.mdframe.forge.plugin.system.service.ISysOrgService; import com.mdframe.forge.plugin.system.service.ISysUserService; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @Slf4j @Service public class MessageManageServiceImpl implements MessageManageService { private final SysMessageMapper messageMapper; private final SysMessageReceiverMapper receiverMapper; private final SysMessageSendRecordMapper sendRecordMapper; private final ISysUserService userService; private final ISysOrgService orgService; public MessageManageServiceImpl( SysMessageMapper messageMapper, SysMessageReceiverMapper receiverMapper, SysMessageSendRecordMapper sendRecordMapper, ISysUserService userService, ISysOrgService orgService) { this.messageMapper = messageMapper; this.receiverMapper = receiverMapper; this.sendRecordMapper = sendRecordMapper; this.userService = userService; this.orgService = orgService; } @Override public IPage pageMessages(MessageManageQueryDTO query, Integer pageNum, Integer pageSize) { Page page = new Page<>(pageNum, pageSize); LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); if (StrUtil.isNotBlank(query.getType())) { wrapper.eq(SysMessage::getType, query.getType()); } if (StrUtil.isNotBlank(query.getChannel())) { wrapper.eq(SysMessage::getSendChannel, query.getChannel()); } if (query.getStatus() != null) { wrapper.eq(SysMessage::getStatus, query.getStatus()); } if (StrUtil.isNotBlank(query.getStartTime())) { LocalDateTime startTime = LocalDateTime.parse(query.getStartTime(), DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); wrapper.ge(SysMessage::getCreateTime, startTime); } if (StrUtil.isNotBlank(query.getEndTime())) { LocalDateTime endTime = LocalDateTime.parse(query.getEndTime(), DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); wrapper.le(SysMessage::getCreateTime, endTime); } if (StrUtil.isNotBlank(query.getKeyword())) { wrapper.and(w -> w.like(SysMessage::getTitle, query.getKeyword()) .or() .like(SysMessage::getContent, query.getKeyword())); } wrapper.orderByDesc(SysMessage::getCreateTime); Page messagePage = messageMapper.selectPage(page, wrapper); List vos = messagePage.getRecords().stream().map(msg -> { MessageManageVO vo = new MessageManageVO(); vo.setId(msg.getId()); vo.setTitle(msg.getTitle()); vo.setType(msg.getType()); vo.setChannel(msg.getSendChannel()); vo.setStatus(msg.getStatus()); vo.setCreateTime(msg.getCreateTime()); vo.setSenderName(msg.getSenderName()); List receivers = receiverMapper.selectList( new LambdaQueryWrapper() .eq(SysMessageReceiver::getMessageId, msg.getId()) ); vo.setReceiverCount(receivers.size()); vo.setReadCount((int) receivers.stream().filter(r -> r.getReadFlag() == 1).count()); vo.setUnreadCount((int) receivers.stream().filter(r -> r.getReadFlag() == 0).count()); return vo; }).collect(Collectors.toList()); Page result = new Page<>(pageNum, pageSize); result.setRecords(vos); result.setTotal(messagePage.getTotal()); return result; } @Override public MessageDetailVO getMessageDetail(Long messageId) { SysMessage message = messageMapper.selectById(messageId); if (message == null) { return null; } SysMessageSendRecord sendRecord = sendRecordMapper.selectOne( new LambdaQueryWrapper() .eq(SysMessageSendRecord::getMessageId, messageId) ); List receivers = receiverMapper.selectList( new LambdaQueryWrapper() .eq(SysMessageReceiver::getMessageId, messageId) ); List userIds = receivers.stream() .map(SysMessageReceiver::getUserId) .distinct() .collect(Collectors.toList()); Map userNameMap = Map.of(); Map orgNameMap = Map.of(); if (!userIds.isEmpty()) { List users = userService.lambdaQuery() .in(SysUser::getId, userIds) .list(); userNameMap = users.stream() .collect(Collectors.toMap(SysUser::getId, SysUser::getUserName)); List orgIds = users.stream() .map(SysUser::getOrgId) .filter(id -> id != null) .distinct() .collect(Collectors.toList()); if (!orgIds.isEmpty()) { List orgs = orgService.lambdaQuery() .in(SysOrg::getId, orgIds) .list(); orgNameMap = orgs.stream() .collect(Collectors.toMap(SysOrg::getId, SysOrg::getOrgName)); } } List receiverVos = receivers.stream().map(r -> { ReceiverVO vo = new ReceiverVO(); vo.setUserId(r.getUserId()); vo.setUserName(userNameMap.getOrDefault(r.getUserId(), "")); vo.setOrgName(orgNameMap.getOrDefault(r.getOrgId(), "")); vo.setReadFlag(r.getReadFlag()); vo.setReadTime(r.getReadTime()); return vo; }).collect(Collectors.toList()); MessageDetailVO detailVO = new MessageDetailVO(); detailVO.setMessage(message); detailVO.setSendRecord(sendRecord); detailVO.setReceivers(receiverVos); return detailVO; } } ``` - [ ] **Step 3: 提交代码** ```bash git add forge/forge-framework/forge-plugin-parent/forge-plugin-message/src/main/java/com/mdframe/forge/plugin/message/service/MessageManageService.java git add forge/forge-framework/forge-plugin-parent/forge-plugin-message/src/main/java/com/mdframe/forge/plugin/message/service/impl/MessageManageServiceImpl.java git commit -m "feat: add message manage service" ``` --- ## Task 3: 创建后端Controller **Files:** - Create: `forge-plugin-message/controller/MessageManageController.java` - [ ] **Step 1: 创建 MessageManageController** 创建文件 `forge/forge-framework/forge-plugin-parent/forge-plugin-message/src/main/java/com/mdframe/forge/plugin/message/controller/MessageManageController.java`: ```java package com.mdframe.forge.plugin.message.controller; import com.mdframe.forge.plugin.message.domain.dto.MessageManageQueryDTO; import com.mdframe.forge.plugin.message.service.MessageManageService; import com.mdframe.forge.starter.core.domain.RespInfo; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; @Slf4j @RestController @RequestMapping("/api/message/manage") public class MessageManageController { private final MessageManageService messageManageService; public MessageManageController(MessageManageService messageManageService) { this.messageManageService = messageManageService; } @PostMapping("/page") public RespInfo page( @RequestBody MessageManageQueryDTO query, @RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize) { return RespInfo.success(messageManageService.pageMessages(query, pageNum, pageSize)); } @GetMapping("/{messageId}/detail") public RespInfo getDetail(@PathVariable Long messageId) { return RespInfo.success(messageManageService.getMessageDetail(messageId)); } } ``` - [ ] **Step 2: 提交代码** ```bash git add forge/forge-framework/forge-plugin-parent/forge-plugin-message/src/main/java/com/mdframe/forge/plugin/message/controller/MessageManageController.java git commit -m "feat: add message manage controller" ``` --- ## Task 4: 创建前端API调用方法 **Files:** - Modify: `forge-admin-ui/src/api/message.js:47-85` - [ ] **Step 1: 在 message.js 中新增管理接口方法** 修改文件 `forge-admin-ui/src/api/message.js`,在文件末尾添加: ```javascript // ========== 消息管理(管理员) ========== /** * 分页查询所有消息列表(管理员) */ getMessageManagePage: (query, pageNum, pageSize) => request.post(`/api/message/manage/page?pageNum=${pageNum}&pageSize=${pageSize}`, query), /** * 查询消息详细信息(包含发送记录和接收人) */ getMessageManageDetail: (messageId) => request.get(`/api/message/manage/${messageId}/detail`) ``` - [ ] **Step 2: 提交代码** ```bash git add forge-admin-ui/src/api/message.js git commit -m "feat: add message manage api methods" ``` --- ## Task 5: 创建前端消息管理页面 **Files:** - Create: `forge-admin-ui/src/views/message/manage.vue` - [ ] **Step 1: 创建消息管理页面** 创建文件 `forge-admin-ui/src/views/message/manage.vue`: ```vue ``` - [ ] **Step 2: 提交代码** ```bash git add forge-admin-ui/src/views/message/manage.vue git commit -m "feat: add message manage page" ``` --- ## Task 6: 添加前端路由配置 **Files:** - Modify: `forge-admin-ui/src/router/index.js` 或路由配置文件 - [ ] **Step 1: 检查路由配置文件** 使用 glob 查找路由配置文件: ```bash glob pattern: forge-admin-ui/src/router/**/*.js ``` - [ ] **Step 2: 在路由配置中添加消息管理页面路由** 根据实际路由配置文件结构,添加类似以下配置: ```javascript { path: '/message', name: 'Message', component: () => import('@/views/message/manage.vue'), meta: { title: '消息管理', icon: 'message', requiresAuth: true } } ``` - [ ] **Step 3: 提交代码** ```bash git add forge-admin-ui/src/router/相关文件.js git commit -m "feat: add message manage route" ``` --- ## Task 7: 测试和验证 - [ ] **Step 1: 启动后端服务** ```bash cd forge/forge-admin mvn spring-boot:run ``` Expected: 服务启动成功,无报错 - [ ] **Step 2: 启动前端服务** ```bash cd forge-admin-ui npm run dev ``` Expected: 前端服务启动成功,可以访问页面 - [ ] **Step 3: 测试消息发送功能** 1. 打开浏览器访问消息管理页面 2. 填写发送表单: - 选择发送渠道:WEB站内信 - 输入消息标题:测试消息 - 输入消息内容:这是一条测试消息 - 选择发送范围:指定人员 - 选择接收用户:选择一个用户 - 选择消息类型:系统消息 3. 点击"发送测试消息"按钮 Expected: 显示"消息发送成功"提示 - [ ] **Step 4: 测试消息列表查询** 1. 在消息监控列表中查看刚发送的消息 2. 点击"查看详情"按钮 Expected: 显示消息详情弹窗,包含发送记录和接收人信息 - [ ] **Step 5: 测试搜索筛选** 1. 在搜索栏中选择消息类型、发送渠道、发送状态 2. 输入关键词搜索 3. 点击"搜索"按钮 Expected: 消息列表正确筛选,显示符合条件的消息 - [ ] **Step 6: 测试短信和邮件发送** 1. 在发送表单中选择发送渠道:短信或邮件 2. 填写消息内容 3. 点击发送按钮 Expected: 如果配置正确,显示发送成功;如果配置错误,显示失败和错误信息 --- ## Spec Coverage Check 设计文档覆盖的功能: 1. ✅ 消息发送测试功能 - Task 5 创建发送测试面板 2. ✅ 查询所有消息列表 - Task 2 创建 pageMessages 方法,Task 5 实现列表显示 3. ✅ 查询消息详情(包含发送记录和接收人) - Task 2 创建 getMessageDetail 方法,Task 5 实现详情弹窗 4. ✅ 发送失败详情显示 - Task 5 在详情弹窗中显示错误信息 5. ✅ 接收人列表及状态 - Task 5 在详情弹窗中显示接收人表格 6. ✅ 支持WEB/短信/邮件渠道 - Task 5 支持选择不同渠道 7. ✅ 管理员权限 - 通过页面访问控制(实际项目中需要配置权限) ## Placeholder Scan 检查是否有占位符或模糊描述: - ✅ 所有代码都有完整实现 - ✅ 所有文件路径都有明确指定 - ✅ 所有命令都有完整内容 - ✅ 没有使用"TODO"、"TBD"等占位符 - ✅ 没有模糊的"添加验证"、"处理异常"等描述 ## Type Consistency 检查类型和方法签名的一致性: - ✅ MessageManageService 接口定义的方法在实现类中都实现了 - ✅ DTO 和 VO 类的字段定义与前端使用一致 - ✅ Controller 接口路径与前端 API 调用一致 - ✅ 数据库字段名与 Java 属性名一致 --- ## 成功标准验证 完成所有任务后,应该达到以下成功标准: 1. ✅ 管理员可以在消息管理页面发送测试消息,并能看到发送结果 2. ✅ 管理员可以在消息管理页面查看所有消息的发送记录和接收人信息 3. ✅ 发送失败的消息显示错误信息 4. ✅ 现有的消息配置页面和用户消息页面功能保持不变 5. ✅ 界面友好,操作流畅