Browse Source

新增消息发送前端页面
新增发送模版消息

hsj 1 month ago
parent
commit
6541866896

+ 58 - 0
src/main/java/top/husj/husj_wx/controller/MessageController.java

@@ -21,6 +21,9 @@ public class MessageController {
     @Autowired
     private WeChatService weChatService;
 
+    @Autowired
+    private top.husj.husj_wx.service.MessageHistoryService messageHistoryService;
+
     /**
      * 分页查询用户列表,支持按name模糊搜索
      * @param page 页码,从1开始
@@ -100,4 +103,59 @@ public class MessageController {
             return ResponseEntity.status(500).body("模板消息发送失败,请检查日志");
         }
     }
+
+    /**
+     * 查询用户的历史消息
+     * @param openId 用户openId
+     * @param limit 查询数量,默认50条
+     * @return 历史消息列表
+     */
+    @GetMapping("/message/history")
+    public ResponseEntity<java.util.List<top.husj.husj_wx.entity.model.MessageHistory>> getMessageHistory(
+            @RequestParam String openId,
+            @RequestParam(defaultValue = "50") int limit) {
+        log.info("查询用户历史消息 - openId: {}, limit: {}", openId, limit);
+        
+        java.util.List<top.husj.husj_wx.entity.model.MessageHistory> history = 
+            messageHistoryService.getMessageHistory(openId, limit);
+        
+        return ResponseEntity.ok(history);
+    }
+
+    /**
+     * 标记用户的所有消息为已读
+     * @param openId 用户openId
+     * @return 操作结果
+     */
+    @PostMapping("/message/markAsRead")
+    public ResponseEntity<String> markAsRead(@RequestParam String openId) {
+        log.info("标记消息为已读 - openId: {}", openId);
+        messageHistoryService.markAsRead(openId);
+        return ResponseEntity.ok("标记成功");
+    }
+
+    /**
+     * 获取所有用户的未读消息数量
+     * @return Map<openId, unreadCount>
+     */
+    @GetMapping("/message/unreadCounts")
+    public ResponseEntity<java.util.Map<String, Integer>> getUnreadCounts() {
+        log.info("查询所有用户的未读消息数量");
+        
+        // 查询所有有未读消息的用户
+        java.util.List<top.husj.husj_wx.entity.model.MessageHistory> unreadMessages = 
+            messageHistoryService.lambdaQuery()
+                .eq(top.husj.husj_wx.entity.model.MessageHistory::getIsRead, 0)
+                .select(top.husj.husj_wx.entity.model.MessageHistory::getOpenId)
+                .list();
+        
+        // 按openId分组统计
+        java.util.Map<String, Integer> unreadCounts = new java.util.HashMap<>();
+        for (top.husj.husj_wx.entity.model.MessageHistory msg : unreadMessages) {
+            unreadCounts.put(msg.getOpenId(), 
+                unreadCounts.getOrDefault(msg.getOpenId(), 0) + 1);
+        }
+        
+        return ResponseEntity.ok(unreadCounts);
+    }
 }

+ 8 - 0
src/main/java/top/husj/husj_wx/controller/WeChatController.java

@@ -22,6 +22,9 @@ public class WeChatController {
     @Autowired
     private WeChatService weChatService;
 
+    @Autowired
+    private top.husj.husj_wx.service.MessageHistoryService messageHistoryService;
+
 
     // 1. 微信后台验证 URL 时使用 (GET)
     @GetMapping("/callback")
@@ -55,6 +58,11 @@ public class WeChatController {
         // 记录最后一个发消息的用户
         weChatService.recordLastUser(msg.getFromUserName());
 
+        // 保存消息到数据库
+        if (msg.getContent() != null && !msg.getContent().trim().isEmpty()) {
+            messageHistoryService.saveMessage(msg.getFromUserName(), msg.getContent());
+        }
+
         // 构造被动回复(同样是XML)
         return String.format(
                 "<xml>" +

+ 9 - 0
src/main/java/top/husj/husj_wx/dao/MessageHistoryMapper.java

@@ -0,0 +1,9 @@
+package top.husj.husj_wx.dao;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+import top.husj.husj_wx.entity.model.MessageHistory;
+
+@Mapper
+public interface MessageHistoryMapper extends BaseMapper<MessageHistory> {
+}

+ 28 - 0
src/main/java/top/husj/husj_wx/entity/model/MessageHistory.java

@@ -0,0 +1,28 @@
+package top.husj.husj_wx.entity.model;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+@TableName("message_history")
+public class MessageHistory {
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    @TableField("open_id")
+    private String openId;
+
+    @TableField("content")
+    private String content;
+
+    @TableField("is_read")
+    private Integer isRead; // 0-未读, 1-已读
+
+    @TableField("create_time")
+    private LocalDateTime createTime;
+}

+ 16 - 0
src/main/java/top/husj/husj_wx/properties/EmailProperties.java

@@ -0,0 +1,16 @@
+package top.husj.husj_wx.properties;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+@Component
+@ConfigurationProperties(prefix = "email")
+@Data
+public class EmailProperties {
+    private String host;
+    private String port;
+    private String username;
+    private String password;
+    private String notifyTo;
+}

+ 127 - 0
src/main/java/top/husj/husj_wx/schedule/UnreadMessageNotificationSchedule.java

@@ -0,0 +1,127 @@
+package top.husj.husj_wx.schedule;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+import top.husj.husj_wx.entity.model.MessageHistory;
+import top.husj.husj_wx.entity.model.User;
+import top.husj.husj_wx.properties.EmailProperties;
+import top.husj.husj_wx.service.MessageHistoryService;
+import top.husj.husj_wx.service.UserService;
+import top.husj.husj_wx.utils.EmailUtil;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Component
+@Slf4j
+public class UnreadMessageNotificationSchedule {
+
+    @Autowired
+    private MessageHistoryService messageHistoryService;
+
+    @Autowired
+    private UserService userService;
+
+    @Autowired
+    private EmailProperties emailProperties;
+
+    /**
+     * 每10分钟检查一次未读消息,如果有则发送邮件通知
+     */
+    @Scheduled(cron = "0 */10 * * * ?")
+    public void checkUnreadMessages() {
+        try {
+            log.info("开始检查未读消息...");
+
+            // 查询所有未读消息
+            List<MessageHistory> unreadMessages = messageHistoryService.lambdaQuery()
+                    .eq(MessageHistory::getIsRead, 0)
+                    .orderByDesc(MessageHistory::getCreateTime)
+                    .list();
+
+            if (unreadMessages.isEmpty()) {
+                log.info("没有未读消息");
+                return;
+            }
+
+            // 按openId分组统计未读数量
+            Map<String, Long> unreadCountMap = unreadMessages.stream()
+                    .collect(Collectors.groupingBy(MessageHistory::getOpenId, Collectors.counting()));
+
+            log.info("发现 {} 个用户有未读消息,共 {} 条未读消息", 
+                    unreadCountMap.size(), unreadMessages.size());
+
+            // 构建邮件内容
+            StringBuilder emailContent = new StringBuilder();
+            emailContent.append("<h2>未读消息提醒</h2>");
+            emailContent.append("<p>您有 <strong>").append(unreadMessages.size())
+                    .append("</strong> 条未读消息,来自 <strong>")
+                    .append(unreadCountMap.size()).append("</strong> 个用户。</p>");
+            emailContent.append("<hr>");
+
+            // 按用户分组显示消息
+            for (Map.Entry<String, Long> entry : unreadCountMap.entrySet()) {
+                String openId = entry.getKey();
+                Long count = entry.getValue();
+
+                // 获取用户信息
+                User user = userService.getUserByOpenId(openId, true);
+                String userName = user != null && user.getName() != null ? 
+                        user.getName() : "未知用户";
+
+                emailContent.append("<h3>").append(userName)
+                        .append(" (").append(count).append(" 条未读)</h3>");
+                emailContent.append("<ul>");
+
+                // 获取该用户的未读消息
+                List<MessageHistory> userMessages = unreadMessages.stream()
+                        .filter(msg -> msg.getOpenId().equals(openId))
+                        .limit(5) // 最多显示5条
+                        .collect(Collectors.toList());
+
+                for (MessageHistory msg : userMessages) {
+                    emailContent.append("<li>")
+                            .append("<span style='color: #666;'>")
+                            .append(msg.getCreateTime())
+                            .append("</span>: ")
+                            .append(msg.getContent())
+                            .append("</li>");
+                }
+
+                if (count > 5) {
+                    emailContent.append("<li><em>...还有 ")
+                            .append(count - 5)
+                            .append(" 条消息</em></li>");
+                }
+
+                emailContent.append("</ul>");
+            }
+
+            emailContent.append("<hr>");
+            emailContent.append("<p style='color: #999; font-size: 12px;'>");
+            emailContent.append("请访问管理后台查看和回复消息。</p>");
+
+            // 发送邮件
+            EmailUtil emailUtil = new EmailUtil(
+                    emailProperties.getHost(),
+                    emailProperties.getPort(),
+                    emailProperties.getUsername(),
+                    emailProperties.getPassword()
+            );
+
+            emailUtil.sendHtmlEmail(
+                    emailProperties.getNotifyTo(),
+                    "【微信公众号】未读消息提醒 - " + unreadMessages.size() + "条未读",
+                    emailContent.toString()
+            );
+
+            log.info("未读消息提醒邮件已发送到: {}", emailProperties.getNotifyTo());
+
+        } catch (Exception e) {
+            log.error("检查未读消息或发送邮件时发生异常: ", e);
+        }
+    }
+}

+ 37 - 0
src/main/java/top/husj/husj_wx/service/MessageHistoryService.java

@@ -0,0 +1,37 @@
+package top.husj.husj_wx.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import top.husj.husj_wx.entity.model.MessageHistory;
+
+import java.util.List;
+
+public interface MessageHistoryService extends IService<MessageHistory> {
+    
+    /**
+     * 保存用户发送的消息
+     * @param openId 用户openId
+     * @param content 消息内容
+     */
+    void saveMessage(String openId, String content);
+
+    /**
+     * 查询用户的历史消息
+     * @param openId 用户openId
+     * @param limit 查询数量限制
+     * @return 历史消息列表
+     */
+    List<MessageHistory> getMessageHistory(String openId, int limit);
+
+    /**
+     * 标记用户的所有消息为已读
+     * @param openId 用户openId
+     */
+    void markAsRead(String openId);
+
+    /**
+     * 获取用户的未读消息数量
+     * @param openId 用户openId
+     * @return 未读数量
+     */
+    int getUnreadCount(String openId);
+}

+ 54 - 0
src/main/java/top/husj/husj_wx/service/impl/MessageHistoryServiceImpl.java

@@ -0,0 +1,54 @@
+package top.husj.husj_wx.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import top.husj.husj_wx.dao.MessageHistoryMapper;
+import top.husj.husj_wx.entity.model.MessageHistory;
+import top.husj.husj_wx.service.MessageHistoryService;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Service
+@Slf4j
+public class MessageHistoryServiceImpl extends ServiceImpl<MessageHistoryMapper, MessageHistory> implements MessageHistoryService {
+
+    @Override
+    public void saveMessage(String openId, String content) {
+        MessageHistory message = new MessageHistory();
+        message.setOpenId(openId);
+        message.setContent(content);
+        message.setIsRead(0); // 默认未读
+        message.setCreateTime(LocalDateTime.now());
+        save(message);
+        log.info("保存用户消息历史 - openId: {}, content: {}", openId, content);
+    }
+
+    @Override
+    public List<MessageHistory> getMessageHistory(String openId, int limit) {
+        return lambdaQuery()
+                .eq(MessageHistory::getOpenId, openId)
+                .orderByDesc(MessageHistory::getCreateTime)
+                .last("LIMIT " + limit)
+                .list();
+    }
+
+    @Override
+    public void markAsRead(String openId) {
+        lambdaUpdate()
+                .eq(MessageHistory::getOpenId, openId)
+                .eq(MessageHistory::getIsRead, 0)
+                .set(MessageHistory::getIsRead, 1)
+                .update();
+        log.info("标记用户消息为已读 - openId: {}", openId);
+    }
+
+    @Override
+    public int getUnreadCount(String openId) {
+        return lambdaQuery()
+                .eq(MessageHistory::getOpenId, openId)
+                .eq(MessageHistory::getIsRead, 0)
+                .count().intValue();
+    }
+}

+ 1 - 1
src/main/java/top/husj/husj_wx/service/impl/UserServiceImpl.java

@@ -57,7 +57,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
     public User getUserByOpenId(String openId, Boolean searchDb) {
         User user = null;
         if (searchDb) {
-            user = lambdaQuery().eq(User::getOpenId, openId).getEntity();
+            user = lambdaQuery().eq(User::getOpenId, openId).one();
         }
         if (user != null)
             return user;

+ 7 - 0
src/main/resources/application.yml

@@ -24,3 +24,10 @@ wechat:
   appsecret: b0e5198cd442efe767f473f1445bd162
   encodingAESKey: TAuWfJHftr4CmGtLe8aHwkA9JVaEMCA3VoQi9VhLVpD
   templateId: cQevVsdm6hLYMGKcNv7KVr8OtYbYVA8nNYx4qQ2nWkE
+
+email:
+  host: smtp.030208.xyz
+  port: 465
+  username: husj@030208.xyz
+  password: 15629747218hsjH
+  notifyTo: 807244836@qq.com

+ 243 - 7
src/main/resources/static/send_message.html

@@ -105,6 +105,7 @@
             cursor: pointer;
             transition: all 0.2s;
             border: 2px solid transparent;
+            position: relative;
         }
 
         .user-item:hover {
@@ -121,6 +122,20 @@
             font-weight: 600;
             color: #333;
             margin-bottom: 5px;
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
+        }
+
+        .unread-badge {
+            background: #ff4444;
+            color: white;
+            font-size: 11px;
+            padding: 2px 6px;
+            border-radius: 10px;
+            font-weight: 600;
+            min-width: 18px;
+            text-align: center;
         }
 
         .user-openid {
@@ -310,6 +325,105 @@
                 transform: translateY(0);
             }
         }
+
+        /* 消息历史模态框 */
+        .modal {
+            display: none;
+            position: fixed;
+            z-index: 1000;
+            left: 0;
+            top: 0;
+            width: 100%;
+            height: 100%;
+            background-color: rgba(0, 0, 0, 0.5);
+            animation: fadeIn 0.3s;
+        }
+
+        .modal.show {
+            display: flex;
+            align-items: center;
+            justify-content: center;
+        }
+
+        .modal-content {
+            background: white;
+            border-radius: 12px;
+            width: 90%;
+            max-width: 600px;
+            max-height: 80vh;
+            display: flex;
+            flex-direction: column;
+            box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
+        }
+
+        .modal-header {
+            padding: 20px;
+            border-bottom: 1px solid #e0e0e0;
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+        }
+
+        .modal-header h3 {
+            margin: 0;
+            color: #333;
+        }
+
+        .close-btn {
+            background: none;
+            border: none;
+            font-size: 24px;
+            color: #999;
+            cursor: pointer;
+            padding: 0;
+            width: 30px;
+            height: 30px;
+        }
+
+        .close-btn:hover {
+            color: #333;
+        }
+
+        .modal-body {
+            flex: 1;
+            overflow-y: auto;
+            padding: 20px;
+        }
+
+        .message-item {
+            margin-bottom: 15px;
+            padding: 12px;
+            background: #f8f9fa;
+            border-radius: 8px;
+            border-left: 3px solid #667eea;
+        }
+
+        .message-time {
+            font-size: 12px;
+            color: #888;
+            margin-bottom: 5px;
+        }
+
+        .message-text {
+            color: #333;
+            line-height: 1.5;
+        }
+
+        .no-history {
+            text-align: center;
+            padding: 40px;
+            color: #999;
+        }
+
+        @keyframes fadeIn {
+            from {
+                opacity: 0;
+            }
+
+            to {
+                opacity: 1;
+            }
+        }
     </style>
 </head>
 
@@ -354,18 +468,49 @@
         </div>
     </div>
 
+    <!-- 消息历史模态框 -->
+    <div class="modal" id="historyModal">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h3 id="modalTitle">消息历史</h3>
+                <button class="close-btn" onclick="closeHistoryModal()">&times;</button>
+            </div>
+            <div class="modal-body" id="historyContent">
+                <div class="loading">加载中...</div>
+            </div>
+        </div>
+    </div>
+
     <script>
         let currentPage = 1;
         const pageSize = 10;
+        let unreadCounts = {};
         let selectedUser = null;
         let totalPages = 1;
         let searchName = '';
 
-        // 页面加载时获取用户列表
+        // 页面加载时获取用户列表和未读消息数量
         window.onload = function () {
             loadUsers();
+            loadUnreadCounts();
         };
 
+        // 加载未读消息数量
+        async function loadUnreadCounts() {
+            try {
+                const response = await fetch('/wechat/api/message/unreadCounts');
+                if (response.ok) {
+                    unreadCounts = await response.json();
+                    // 重新渲染用户列表以显示未读徽章
+                    if (document.querySelectorAll('.user-item').length > 0) {
+                        loadUsers();
+                    }
+                }
+            } catch (error) {
+                console.error('加载未读数量失败:', error);
+            }
+        }
+
         // 加载用户列表
         async function loadUsers() {
             try {
@@ -402,12 +547,23 @@
                 return;
             }
 
-            userList.innerHTML = users.map(user => `
-                <div class="user-item" onclick="selectUser('${user.openId}', '${escapeHtml(user.name || '未知用户')}')">
-                    <div class="user-name">${escapeHtml(user.name || '未知用户')}</div>
-                    <div class="user-openid">${escapeHtml(user.openId)}</div>
-                </div>
-            `).join('');
+            userList.innerHTML = users.map(user => {
+                const unreadCount = unreadCounts[user.openId] || 0;
+                const unreadBadge = unreadCount > 0 ?
+                    `<span class="unread-badge">${unreadCount}</span>` : '';
+
+                return `
+                    <div class="user-item" 
+                         onclick="selectUser('${user.openId}', '${escapeHtml(user.name || '未知用户')}')"
+                         ondblclick="viewMessageHistory('${user.openId}', '${escapeHtml(user.name || '未知用户')}')">
+                        <div class="user-name">
+                            <span>${escapeHtml(user.name || '未知用户')}</span>
+                            ${unreadBadge}
+                        </div>
+                        <div class="user-openid">${escapeHtml(user.openId)}</div>
+                    </div>
+                `;
+            }).join('');
         }
 
         // 选择用户
@@ -536,6 +692,86 @@
             div.textContent = text;
             return div.innerHTML;
         }
+
+        // 查看用户的消息历史
+        async function viewMessageHistory(openId, name) {
+            const modal = document.getElementById('historyModal');
+            const modalTitle = document.getElementById('modalTitle');
+            const historyContent = document.getElementById('historyContent');
+
+            modalTitle.textContent = `${name} 的消息历史`;
+            historyContent.innerHTML = '<div class="loading">加载中...</div>';
+            modal.classList.add('show');
+
+            try {
+                const response = await fetch(`/wechat/api/message/history?openId=${openId}&limit=50`);
+
+                if (response.ok) {
+                    const messages = await response.json();
+
+                    if (messages.length === 0) {
+                        historyContent.innerHTML = '<div class="no-history">暂无历史消息</div>';
+                    } else {
+                        historyContent.innerHTML = messages.map(msg => `
+                            <div class="message-item">
+                                <div class="message-time">${formatDateTime(msg.createTime)}</div>
+                                <div class="message-text">${escapeHtml(msg.content)}</div>
+                            </div>
+                        `).join('');
+
+                        // 标记消息为已读
+                        await markAsRead(openId);
+                    }
+                } else {
+                    historyContent.innerHTML = '<div class="no-history">加载历史消息失败</div>';
+                }
+            } catch (error) {
+                console.error('加载历史消息失败:', error);
+                historyContent.innerHTML = '<div class="no-history">加载历史消息失败</div>';
+            }
+        }
+
+        // 关闭消息历史模态框
+        function closeHistoryModal() {
+            const modal = document.getElementById('historyModal');
+            modal.classList.remove('show');
+        }
+
+        // 点击模态框背景关闭
+        document.getElementById('historyModal')?.addEventListener('click', function (e) {
+            if (e.target === this) {
+                closeHistoryModal();
+            }
+        });
+
+        // 标记消息为已读
+        async function markAsRead(openId) {
+            try {
+                const response = await fetch(`/wechat/api/message/markAsRead?openId=${openId}`, {
+                    method: 'POST'
+                });
+
+                if (response.ok) {
+                    // 更新未读数量
+                    delete unreadCounts[openId];
+                    loadUsers(); // 重新加载用户列表以更新徽章
+                }
+            } catch (error) {
+                console.error('标记已读失败:', error);
+            }
+        }
+
+        // 格式化日期时间
+        function formatDateTime(dateTimeStr) {
+            if (!dateTimeStr) return '';
+            const date = new Date(dateTimeStr);
+            const year = date.getFullYear();
+            const month = String(date.getMonth() + 1).padStart(2, '0');
+            const day = String(date.getDate()).padStart(2, '0');
+            const hours = String(date.getHours()).padStart(2, '0');
+            const minutes = String(date.getMinutes()).padStart(2, '0');
+            return `${year}-${month}-${day} ${hours}:${minutes}`;
+        }
     </script>
 </body>