Răsfoiți Sursa

新增消息发送前端页面

hsj 1 lună în urmă
părinte
comite
0c6188efff

+ 20 - 0
src/main/java/top/husj/husj_wx/config/RestTemplateConfig.java

@@ -0,0 +1,20 @@
+package top.husj.husj_wx.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.converter.StringHttpMessageConverter;
+import org.springframework.web.client.RestTemplate;
+
+import java.nio.charset.StandardCharsets;
+
+@Configuration
+public class RestTemplateConfig {
+
+    @Bean
+    public RestTemplate restTemplate() {
+        RestTemplate restTemplate = new RestTemplate();
+        // 添加StringHttpMessageConverter并设置为UTF-8编码
+        restTemplate.getMessageConverters().add(0, new StringHttpMessageConverter(StandardCharsets.UTF_8));
+        return restTemplate;
+    }
+}

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

@@ -0,0 +1,103 @@
+package top.husj.husj_wx.controller;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+import top.husj.husj_wx.entity.dto.SendMessageRequest;
+import top.husj.husj_wx.entity.model.User;
+import top.husj.husj_wx.service.UserService;
+import top.husj.husj_wx.service.WeChatService;
+
+@RestController
+@RequestMapping("/api")
+@Slf4j
+public class MessageController {
+
+    @Autowired
+    private UserService userService;
+
+    @Autowired
+    private WeChatService weChatService;
+
+    /**
+     * 分页查询用户列表,支持按name模糊搜索
+     * @param page 页码,从1开始
+     * @param size 每页大小,默认10
+     * @param name 用户名称,可选,用于模糊搜索
+     * @return 用户分页数据
+     */
+    @GetMapping("/users")
+    public ResponseEntity<IPage<User>> getUsers(
+            @RequestParam(defaultValue = "1") int page,
+            @RequestParam(defaultValue = "10") int size,
+            @RequestParam(required = false) String name) {
+        
+        log.info("查询用户列表 - page: {}, size: {}, name: {}", page, size, name);
+        IPage<User> users = userService.searchUsers(page, size, name);
+        return ResponseEntity.ok(users);
+    }
+
+    /**
+     * 发送消息给指定用户
+     * @param request 包含openId和content的请求体
+     * @return 发送结果
+     */
+    @PostMapping(value = "/message/send", produces = "application/json;charset=UTF-8", consumes = "application/json;charset=UTF-8")
+    public ResponseEntity<String> sendMessage(@RequestBody SendMessageRequest request) {
+        log.info("收到发送消息请求 - openId: {}, content: {}", request.getOpenId(), request.getContent());
+
+        if (request.getOpenId() == null || request.getOpenId().trim().isEmpty()) {
+            return ResponseEntity.badRequest().body("openId不能为空");
+        }
+
+        if (request.getContent() == null || request.getContent().trim().isEmpty()) {
+            return ResponseEntity.badRequest().body("消息内容不能为空");
+        }
+
+        boolean success = weChatService.sendCustomMessage(request.getOpenId(), request.getContent());
+        if (success) {
+            log.info("成功发送消息给用户: {}", request.getOpenId());
+            return ResponseEntity.ok("消息发送成功");
+        } else {
+            log.error("发送消息失败 - openId: {}", request.getOpenId());
+            return ResponseEntity.status(500).body("消息发送失败,请检查日志");
+        }
+    }
+
+    /**
+     * 发送模板消息给指定用户
+     * @param request 包含openId、name和content的请求体
+     * @return 发送结果
+     */
+    @PostMapping(value = "/message/sendTemplate", produces = "application/json;charset=UTF-8", consumes = "application/json;charset=UTF-8")
+    public ResponseEntity<String> sendTemplateMessage(@RequestBody java.util.Map<String, String> request) {
+        String openId = request.get("openId");
+        String name = request.get("name");
+        String content = request.get("content");
+
+        log.info("收到发送模板消息请求 - openId: {}, name: {}, content: {}", openId, name, content);
+
+        if (openId == null || openId.trim().isEmpty()) {
+            return ResponseEntity.badRequest().body("openId不能为空");
+        }
+
+        if (name == null || name.trim().isEmpty()) {
+            return ResponseEntity.badRequest().body("用户名不能为空");
+        }
+
+        if (content == null || content.trim().isEmpty()) {
+            return ResponseEntity.badRequest().body("消息内容不能为空");
+        }
+
+        boolean success = weChatService.sendTemplateMessage(openId, name, content);
+        if (success) {
+            log.info("成功发送模板消息给用户: {}", openId);
+            return ResponseEntity.ok("模板消息发送成功");
+        } else {
+            log.error("发送模板消息失败 - openId: {}", openId);
+            return ResponseEntity.status(500).body("模板消息发送失败,请检查日志");
+        }
+    }
+}

+ 9 - 0
src/main/java/top/husj/husj_wx/entity/dto/SendMessageRequest.java

@@ -0,0 +1,9 @@
+package top.husj.husj_wx.entity.dto;
+
+import lombok.Data;
+
+@Data
+public class SendMessageRequest {
+    private String openId;
+    private String content;
+}

+ 542 - 0
src/main/resources/static/send_message.html

@@ -0,0 +1,542 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>消息发送管理</title>
+    <style>
+        * {
+            margin: 0;
+            padding: 0;
+            box-sizing: border-box;
+        }
+
+        body {
+            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
+            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+            min-height: 100vh;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            padding: 20px;
+        }
+
+        .container {
+            max-width: 1400px;
+            width: 95%;
+            margin: 0 auto;
+            background: white;
+            border-radius: 12px;
+            box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
+            overflow: hidden;
+            display: flex;
+            min-height: 700px;
+        }
+
+        /* 左侧用户列表区域 */
+        .user-panel {
+            width: 400px;
+            background: #f8f9fa;
+            border-right: 1px solid #e0e0e0;
+            display: flex;
+            flex-direction: column;
+            overflow: hidden;
+        }
+
+        .user-panel-header {
+            padding: 20px;
+            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+            color: white;
+        }
+
+        .user-panel-header h2 {
+            font-size: 20px;
+            margin-bottom: 15px;
+        }
+
+        .search-box {
+            position: relative;
+        }
+
+        .search-box input {
+            width: 100%;
+            padding: 10px 40px 10px 15px;
+            border: none;
+            border-radius: 6px;
+            font-size: 14px;
+            outline: none;
+            transition: all 0.3s;
+        }
+
+        .search-box input:focus {
+            box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.3);
+        }
+
+        .search-box button {
+            position: absolute;
+            right: 5px;
+            top: 50%;
+            transform: translateY(-50%);
+            background: #667eea;
+            color: white;
+            border: none;
+            padding: 6px 12px;
+            border-radius: 4px;
+            cursor: pointer;
+            font-size: 13px;
+        }
+
+        .search-box button:hover {
+            background: #5568d3;
+        }
+
+        .user-list-container {
+            flex: 1;
+            overflow-y: auto;
+            padding: 10px;
+        }
+
+        .user-item {
+            padding: 15px;
+            background: white;
+            margin-bottom: 8px;
+            border-radius: 8px;
+            cursor: pointer;
+            transition: all 0.2s;
+            border: 2px solid transparent;
+        }
+
+        .user-item:hover {
+            transform: translateX(5px);
+            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+        }
+
+        .user-item.active {
+            border-color: #667eea;
+            background: #f0f3ff;
+        }
+
+        .user-name {
+            font-weight: 600;
+            color: #333;
+            margin-bottom: 5px;
+        }
+
+        .user-openid {
+            font-size: 12px;
+            color: #888;
+            word-break: break-all;
+        }
+
+        .pagination {
+            padding: 15px;
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+            border-top: 1px solid #e0e0e0;
+            background: white;
+        }
+
+        .pagination button {
+            padding: 8px 16px;
+            background: #667eea;
+            color: white;
+            border: none;
+            border-radius: 4px;
+            cursor: pointer;
+            font-size: 13px;
+        }
+
+        .pagination button:disabled {
+            background: #ccc;
+            cursor: not-allowed;
+        }
+
+        .pagination button:hover:not(:disabled) {
+            background: #5568d3;
+        }
+
+        .page-info {
+            font-size: 13px;
+            color: #666;
+        }
+
+        /* 右侧消息输入区域 */
+        .message-panel {
+            flex: 1;
+            display: flex;
+            flex-direction: column;
+            padding: 30px;
+        }
+
+        .message-panel h2 {
+            color: #333;
+            margin-bottom: 20px;
+            font-size: 24px;
+        }
+
+        .selected-user-info {
+            background: #f0f3ff;
+            padding: 15px;
+            border-radius: 8px;
+            margin-bottom: 20px;
+            border-left: 4px solid #667eea;
+        }
+
+        .selected-user-info p {
+            margin: 5px 0;
+            color: #555;
+        }
+
+        .selected-user-info strong {
+            color: #333;
+        }
+
+        .message-input-area {
+            flex: 1;
+            display: flex;
+            flex-direction: column;
+        }
+
+        .message-input-area label {
+            font-weight: 600;
+            color: #333;
+            margin-bottom: 10px;
+            display: block;
+        }
+
+        .message-input-area textarea {
+            flex: 1;
+            padding: 15px;
+            border: 2px solid #e0e0e0;
+            border-radius: 8px;
+            font-size: 14px;
+            font-family: inherit;
+            resize: none;
+            outline: none;
+            transition: border-color 0.3s;
+        }
+
+        .message-input-area textarea:focus {
+            border-color: #667eea;
+        }
+
+        .template-option {
+            margin-top: 15px;
+            padding: 12px;
+            background: #f8f9fa;
+            border-radius: 6px;
+            display: flex;
+            align-items: center;
+        }
+
+        .template-option input[type="checkbox"] {
+            width: 18px;
+            height: 18px;
+            margin-right: 10px;
+            cursor: pointer;
+        }
+
+        .template-option label {
+            cursor: pointer;
+            margin: 0;
+            color: #555;
+            font-weight: 500;
+        }
+
+        .send-button {
+            margin-top: 20px;
+            padding: 15px 30px;
+            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+            color: white;
+            border: none;
+            border-radius: 8px;
+            font-size: 16px;
+            font-weight: 600;
+            cursor: pointer;
+            transition: all 0.3s;
+        }
+
+        .send-button:hover:not(:disabled) {
+            transform: translateY(-2px);
+            box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
+        }
+
+        .send-button:disabled {
+            background: #ccc;
+            cursor: not-allowed;
+        }
+
+        .empty-state {
+            text-align: center;
+            padding: 40px 20px;
+            color: #999;
+        }
+
+        .loading {
+            text-align: center;
+            padding: 20px;
+            color: #667eea;
+        }
+
+        .alert {
+            padding: 12px 20px;
+            border-radius: 6px;
+            margin-bottom: 20px;
+            animation: slideDown 0.3s;
+        }
+
+        .alert-success {
+            background: #d4edda;
+            color: #155724;
+            border: 1px solid #c3e6cb;
+        }
+
+        .alert-error {
+            background: #f8d7da;
+            color: #721c24;
+            border: 1px solid #f5c6cb;
+        }
+
+        @keyframes slideDown {
+            from {
+                opacity: 0;
+                transform: translateY(-10px);
+            }
+
+            to {
+                opacity: 1;
+                transform: translateY(0);
+            }
+        }
+    </style>
+</head>
+
+<body>
+    <div class="container">
+        <!-- 左侧用户列表 -->
+        <div class="user-panel">
+            <div class="user-panel-header">
+                <h2>用户列表</h2>
+                <div class="search-box">
+                    <input type="text" id="searchInput" placeholder="搜索用户名称...">
+                    <button onclick="searchUsers()">搜索</button>
+                </div>
+            </div>
+            <div class="user-list-container" id="userList">
+                <div class="loading">加载中...</div>
+            </div>
+            <div class="pagination">
+                <button id="prevBtn" onclick="previousPage()" disabled>上一页</button>
+                <span class="page-info" id="pageInfo">第 1 页</span>
+                <button id="nextBtn" onclick="nextPage()">下一页</button>
+            </div>
+        </div>
+
+        <!-- 右侧消息输入 -->
+        <div class="message-panel">
+            <h2>发送消息</h2>
+            <div id="alertContainer"></div>
+            <div id="selectedUserInfo" style="display: none;" class="selected-user-info">
+                <p><strong>选中用户:</strong> <span id="selectedUserName"></span></p>
+                <p><strong>OpenID:</strong> <span id="selectedUserOpenId"></span></p>
+            </div>
+            <div class="message-input-area">
+                <label for="messageContent">消息内容</label>
+                <textarea id="messageContent" placeholder="请输入要发送的消息..."></textarea>
+            </div>
+            <div class="template-option">
+                <input type="checkbox" id="useTemplate">
+                <label for="useTemplate">发送模板消息(将使用预设模板发送)</label>
+            </div>
+            <button class="send-button" id="sendBtn" onclick="sendMessage()" disabled>发送消息</button>
+        </div>
+    </div>
+
+    <script>
+        let currentPage = 1;
+        const pageSize = 10;
+        let selectedUser = null;
+        let totalPages = 1;
+        let searchName = '';
+
+        // 页面加载时获取用户列表
+        window.onload = function () {
+            loadUsers();
+        };
+
+        // 加载用户列表
+        async function loadUsers() {
+            try {
+                const params = new URLSearchParams({
+                    page: currentPage,
+                    size: pageSize
+                });
+                if (searchName) {
+                    params.append('name', searchName);
+                }
+
+                const response = await fetch(`/wechat/api/users?${params}`);
+                const data = await response.json();
+
+                if (response.ok) {
+                    displayUsers(data.records || []);
+                    totalPages = data.pages || 1;
+                    updatePagination();
+                } else {
+                    showError('加载用户列表失败');
+                }
+            } catch (error) {
+                console.error('加载用户列表出错:', error);
+                showError('加载用户列表出错: ' + error.message);
+            }
+        }
+
+        // 显示用户列表
+        function displayUsers(users) {
+            const userList = document.getElementById('userList');
+
+            if (users.length === 0) {
+                userList.innerHTML = '<div class="empty-state">暂无用户数据</div>';
+                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('');
+        }
+
+        // 选择用户
+        function selectUser(openId, name) {
+            selectedUser = { openId, name };
+
+            // 更新UI
+            document.querySelectorAll('.user-item').forEach(item => {
+                item.classList.remove('active');
+            });
+            event.currentTarget.classList.add('active');
+
+            document.getElementById('selectedUserInfo').style.display = 'block';
+            document.getElementById('selectedUserName').textContent = name;
+            document.getElementById('selectedUserOpenId').textContent = openId;
+            document.getElementById('sendBtn').disabled = false;
+        }
+
+        // 搜索用户
+        function searchUsers() {
+            searchName = document.getElementById('searchInput').value.trim();
+            currentPage = 1;
+            loadUsers();
+        }
+
+        // 允许回车搜索
+        document.getElementById('searchInput').addEventListener('keypress', function (e) {
+            if (e.key === 'Enter') {
+                searchUsers();
+            }
+        });
+
+        // 上一页
+        function previousPage() {
+            if (currentPage > 1) {
+                currentPage--;
+                loadUsers();
+            }
+        }
+
+        // 下一页
+        function nextPage() {
+            if (currentPage < totalPages) {
+                currentPage++;
+                loadUsers();
+            }
+        }
+
+        // 更新分页按钮状态
+        function updatePagination() {
+            document.getElementById('prevBtn').disabled = currentPage <= 1;
+            document.getElementById('nextBtn').disabled = currentPage >= totalPages;
+            document.getElementById('pageInfo').textContent = `第 ${currentPage} / ${totalPages} 页`;
+        }
+
+        // 发送消息
+        async function sendMessage() {
+            if (!selectedUser) {
+                showError('请先选择一个用户');
+                return;
+            }
+
+            const content = document.getElementById('messageContent').value.trim();
+            if (!content) {
+                showError('请输入消息内容');
+                return;
+            }
+
+            const useTemplate = document.getElementById('useTemplate').checked;
+            const sendBtn = document.getElementById('sendBtn');
+            sendBtn.disabled = true;
+            sendBtn.textContent = '发送中...';
+
+            try {
+                const endpoint = useTemplate ? '/wechat/api/message/sendTemplate' : '/wechat/api/message/send';
+                const response = await fetch(endpoint, {
+                    method: 'POST',
+                    headers: {
+                        'Content-Type': 'application/json'
+                    },
+                    body: JSON.stringify({
+                        openId: selectedUser.openId,
+                        content: content,
+                        name: selectedUser.name
+                    })
+                });
+
+                const result = await response.text();
+
+                if (response.ok) {
+                    showSuccess(useTemplate ? '模板消息发送成功!' : '消息发送成功!');
+                    document.getElementById('messageContent').value = '';
+                } else {
+                    showError('发送失败: ' + result);
+                }
+            } catch (error) {
+                console.error('发送消息出错:', error);
+                showError('发送消息出错: ' + error.message);
+            } finally {
+                sendBtn.disabled = false;
+                sendBtn.textContent = '发送消息';
+            }
+        }
+
+        // 显示成功提示
+        function showSuccess(message) {
+            const alertContainer = document.getElementById('alertContainer');
+            alertContainer.innerHTML = `<div class="alert alert-success">${escapeHtml(message)}</div>`;
+            setTimeout(() => {
+                alertContainer.innerHTML = '';
+            }, 3000);
+        }
+
+        // 显示错误提示
+        function showError(message) {
+            const alertContainer = document.getElementById('alertContainer');
+            alertContainer.innerHTML = `<div class="alert alert-error">${escapeHtml(message)}</div>`;
+            setTimeout(() => {
+                alertContainer.innerHTML = '';
+            }, 5000);
+        }
+
+        // HTML转义,防止XSS
+        function escapeHtml(text) {
+            const div = document.createElement('div');
+            div.textContent = text;
+            return div.innerHTML;
+        }
+    </script>
+</body>
+
+</html>