1
0

2 Commits e31834d066 ... 0c6188efff

Autor SHA1 Nachricht Datum
  hsj 0c6188efff 新增消息发送前端页面 vor 1 Monat
  hsj 4aad0984e0 新增消息发送前端页面 vor 1 Monat

+ 0 - 5
src/main/java/top/husj/husj_wx/Application.java

@@ -13,9 +13,4 @@ public class Application {
     public static void main(String[] args) {
         SpringApplication.run(Application.class, args);
     }
-
-    @Bean
-    public RestTemplate restTemplate() {
-        return new RestTemplate();
-    }
 }

+ 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("模板消息发送失败,请检查日志");
+        }
+    }
+}

+ 15 - 9
src/main/java/top/husj/husj_wx/controller/WeChatController.java

@@ -68,20 +68,26 @@ public class WeChatController {
         );
     }
 
-    // 3. 发送消息给最后一个活跃用户 (GET)
+    // 3. 发送消息给指定用户 (GET)
     @GetMapping("/send")
-    public ResponseEntity<String> sendMessageToLastUser(@RequestParam("msg") String msg) {
-        log.info("收到发送消息请求,消息内容: {}", msg);
+    public ResponseEntity<String> sendMessage(
+            @RequestParam("openId") String openId,
+            @RequestParam("msg") String msg) {
+        log.info("收到发送消息请求 - openId: {}, 消息内容: {}", openId, msg);
 
-        String lastUserOpenId = weChatService.getLastUserOpenId();
-        if (lastUserOpenId == null || lastUserOpenId.isEmpty()) {
-            log.warn("暂无活跃用户,无法发送消息");
-            return ResponseEntity.badRequest().body("暂无活跃用户,请先通过公众号发送一条消息");
+        if (openId == null || openId.trim().isEmpty()) {
+            log.warn("openId为空,无法发送消息");
+            return ResponseEntity.badRequest().body("openId不能为空");
         }
 
-        boolean success = weChatService.sendMessageToLastUser(msg);
+        if (msg == null || msg.trim().isEmpty()) {
+            log.warn("消息内容为空");
+            return ResponseEntity.badRequest().body("消息内容不能为空");
+        }
+
+        boolean success = weChatService.sendCustomMessage(openId, msg);
         if (success) {
-            log.info("成功发送消息给最后活跃用户: {}", lastUserOpenId);
+            log.info("成功发送消息给用户: {}", openId);
             return ResponseEntity.ok("消息发送成功");
         } else {
             log.error("发送消息失败");

+ 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;
+}

+ 2 - 0
src/main/java/top/husj/husj_wx/properties/WeChatProperties.java

@@ -15,4 +15,6 @@ public class WeChatProperties {
     private String appsecret;
     @Value("${wechat.encodingaeskey}")
     private String encodingAESKey;
+    @Value("${wechat.templateId}")
+    private String templateId;
 }

+ 10 - 0
src/main/java/top/husj/husj_wx/service/UserService.java

@@ -1,5 +1,6 @@
 package top.husj.husj_wx.service;
 
+import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.service.IService;
 import top.husj.husj_wx.entity.model.User;
 
@@ -16,4 +17,13 @@ public interface UserService extends IService<User> {
      * @return
      */
     User getUserByOpenId(String openId, Boolean searchDb);
+
+    /**
+     * 分页查询用户,支持按name模糊搜索
+     * @param page 页码 (从1开始)
+     * @param size 每页大小
+     * @param name 用户名称 (可选,用于模糊搜索)
+     * @return
+     */
+    IPage<User> searchUsers(int page, int size, String name);
 }

+ 76 - 1
src/main/java/top/husj/husj_wx/service/WeChatService.java

@@ -71,8 +71,17 @@ public class WeChatService {
         text.put("content", content);
         message.put("text", text);
 
+
         try {
-            String response = restTemplate.postForObject(url, message.toJSONString(), String.class);
+            // 设置请求头,确保使用UTF-8编码
+            org.springframework.http.HttpHeaders headers = new org.springframework.http.HttpHeaders();
+            headers.setContentType(org.springframework.http.MediaType.APPLICATION_JSON);
+            headers.set("Accept-Charset", "UTF-8");
+            
+            org.springframework.http.HttpEntity<String> entity = 
+                new org.springframework.http.HttpEntity<>(message.toJSONString(), headers);
+            
+            String response = restTemplate.postForObject(url, entity, String.class);
             JSONObject jsonResponse = JSONObject.parseObject(response);
             
             int errcode = jsonResponse.getIntValue("errcode");
@@ -103,4 +112,70 @@ public class WeChatService {
         }
         return sendCustomMessage(openId, content);
     }
+
+    /**
+     * 发送模板消息给指定用户
+     * 模板格式: 你好,{{name.DATA}},{{content.DATA}}
+     * @param openId 接收消息的用户OpenID
+     * @param name 用户名称
+     * @param content 消息内容
+     * @return 是否发送成功
+     */
+    public boolean sendTemplateMessage(String openId, String name, String content) {
+        String accessToken = AccessTokenUtil.getAccessToken();
+        if (accessToken == null || accessToken.isEmpty()) {
+            log.error("access_token为空,无法发送模板消息");
+            return false;
+        }
+
+        // 从配置中获取templateId (需要注入WeChatProperties)
+        String url = String.format("https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=%s", accessToken);
+
+        // 构建模板消息JSON
+        JSONObject message = new JSONObject();
+        message.put("touser", openId);
+        message.put("template_id", "cQevVsdm6hLYMGKcNv7KVr8OtYbYVA8nNYx4qQ2nWkE");
+        message.put("url", "https://www.baidu.com"); // 点击跳转到百度
+
+        // 构建模板数据
+        JSONObject data = new JSONObject();
+        
+        JSONObject nameData = new JSONObject();
+        nameData.put("value", name);
+        nameData.put("color", "#173177");
+        data.put("name", nameData);
+
+        JSONObject contentData = new JSONObject();
+        contentData.put("value", content);
+        contentData.put("color", "#173177");
+        data.put("content", contentData);
+
+        message.put("data", data);
+
+        try {
+            // 设置请求头,确保使用UTF-8编码
+            org.springframework.http.HttpHeaders headers = new org.springframework.http.HttpHeaders();
+            headers.setContentType(org.springframework.http.MediaType.APPLICATION_JSON);
+            headers.set("Accept-Charset", "UTF-8");
+            
+            org.springframework.http.HttpEntity<String> entity = 
+                new org.springframework.http.HttpEntity<>(message.toJSONString(), headers);
+            
+            String response = restTemplate.postForObject(url, entity, String.class);
+            JSONObject jsonResponse = JSONObject.parseObject(response);
+            
+            int errcode = jsonResponse.getIntValue("errcode");
+            if (errcode == 0) {
+                log.info("成功发送模板消息给用户 {}: name={}, content={}", openId, name, content);
+                return true;
+            } else {
+                String errmsg = jsonResponse.getString("errmsg");
+                log.error("发送模板消息失败 - errcode: {}, errmsg: {}", errcode, errmsg);
+                return false;
+            }
+        } catch (Exception e) {
+            log.error("发送模板消息时发生异常: ", e);
+            return false;
+        }
+    }
 }

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

@@ -1,6 +1,7 @@
 package top.husj.husj_wx.service.impl;
 
 import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import jakarta.annotation.PostConstruct;
 import lombok.RequiredArgsConstructor;
@@ -73,4 +74,18 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
         user.setName(name);
         return user;
     }
+
+    @Override
+    public IPage<User> searchUsers(int page, int size, String name) {
+        com.baomidou.mybatisplus.extension.plugins.pagination.Page<User> pageParam = 
+            new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>(page, size);
+        
+        if (name != null && !name.trim().isEmpty()) {
+            return lambdaQuery()
+                .like(User::getName, name.trim())
+                .page(pageParam);
+        } else {
+            return lambdaQuery().page(pageParam);
+        }
+    }
 }

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

@@ -8,6 +8,11 @@ spring:
     password: 15629747218hsjH
   jackson:
     time-zone: Asia/Shanghai
+  http:
+    encoding:
+      charset: UTF-8
+      enabled: true
+      force: true
 server:
   servlet:
     context-path: /wechat
@@ -18,3 +23,4 @@ wechat:
   appid: wxebbc42cc41337ad6
   appsecret: b0e5198cd442efe767f473f1445bd162
   encodingAESKey: TAuWfJHftr4CmGtLe8aHwkA9JVaEMCA3VoQi9VhLVpD
+  templateId: cQevVsdm6hLYMGKcNv7KVr8OtYbYVA8nNYx4qQ2nWkE

+ 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>