3 Commits bac8d7c801 ... 0e69873b3a

Author SHA1 Message Date
  userName 0e69873b3a hch0704 3 days ago
  zhentao de38c80fdd wyc 6 days ago
  zhentao 6223282499 wyc 6 days ago
69 changed files with 4782 additions and 29 deletions
  1. 31 10
      pom.xml
  2. 5 0
      src/main/java/com/zhentao/ZyzApplication.java
  3. 227 0
      src/main/java/com/zhentao/common/oss/OssUtil.java
  4. 312 0
      src/main/java/com/zhentao/common/utils/HttpUtils.java
  5. 21 0
      src/main/java/com/zhentao/dto/DeeseekRequest.java
  6. 15 0
      src/main/java/com/zhentao/dto/TrueUser.java
  7. 46 0
      src/main/java/com/zhentao/groups/MongoDB/controller/GroupMessageController.java
  8. 46 0
      src/main/java/com/zhentao/groups/MongoDB/pojo/GroupMessage.java
  9. 46 0
      src/main/java/com/zhentao/groups/MongoDB/pojo/Message.java
  10. 23 0
      src/main/java/com/zhentao/groups/dto/AddGroupMembers.java
  11. 39 0
      src/main/java/com/zhentao/groups/dto/AddGroupsDto.java
  12. 14 0
      src/main/java/com/zhentao/groups/dto/AddUserLoginDto.java
  13. 23 0
      src/main/java/com/zhentao/groups/dto/DelGroupMembers.java
  14. 11 0
      src/main/java/com/zhentao/groups/dto/GroupDto.java
  15. 10 0
      src/main/java/com/zhentao/groups/dto/OutGroupDto.java
  16. 19 0
      src/main/java/com/zhentao/groups/dto/OutGroupsDto.java
  17. 13 0
      src/main/java/com/zhentao/groups/dto/UpdateGroupMemberRoleDto.java
  18. 20 0
      src/main/java/com/zhentao/groups/mapper/GroupMembersMapper.java
  19. 20 0
      src/main/java/com/zhentao/groups/mapper/GroupsMapper.java
  20. 114 0
      src/main/java/com/zhentao/groups/pojo/GroupMembers.java
  21. 139 0
      src/main/java/com/zhentao/groups/pojo/Groupss.java
  22. 13 0
      src/main/java/com/zhentao/groups/service/GroupMembersService.java
  23. 18 0
      src/main/java/com/zhentao/groups/service/GroupsService.java
  24. 22 0
      src/main/java/com/zhentao/groups/service/impl/GroupMembersServiceImpl.java
  25. 56 0
      src/main/java/com/zhentao/groups/service/impl/GroupsServiceImpl.java
  26. 69 0
      src/main/java/com/zhentao/groups/vo/GroupsVo.java
  27. 17 0
      src/main/java/com/zhentao/groups/vo/UserVo.java
  28. 87 0
      src/main/java/com/zhentao/information/cache/ChannelCache.java
  29. 73 0
      src/main/java/com/zhentao/information/cache/GroupChannelCache.java
  30. 97 0
      src/main/java/com/zhentao/information/cache/GroupMemberCache.java
  31. 128 0
      src/main/java/com/zhentao/information/config/NettyConfig.java
  32. 43 0
      src/main/java/com/zhentao/information/config/WebSocketHandshakeInterceptor.java
  33. 39 0
      src/main/java/com/zhentao/information/controller/FileController.java
  34. 109 0
      src/main/java/com/zhentao/information/controller/MessageController.java
  35. 48 0
      src/main/java/com/zhentao/information/entity/ChatMessage.java
  36. 29 0
      src/main/java/com/zhentao/information/entity/Message.java
  37. 76 0
      src/main/java/com/zhentao/information/handler/HeartbeatHandler.java
  38. 425 0
      src/main/java/com/zhentao/information/handler/WebSocketHandler.java
  39. 55 0
      src/main/java/com/zhentao/information/netty/NettyServer.java
  40. 29 0
      src/main/java/com/zhentao/information/repository/ChatMessageRepository.java
  41. 57 0
      src/main/java/com/zhentao/information/service/GroupInitService.java
  42. 347 0
      src/main/java/com/zhentao/information/service/WebSocketService.java
  43. 46 0
      src/main/java/com/zhentao/user/controller/UserController.java
  44. 34 0
      src/main/java/com/zhentao/user/controller/UserMoneyController.java
  45. 124 0
      src/main/java/com/zhentao/user/domain/User.java
  46. 76 0
      src/main/java/com/zhentao/user/domain/UserMoney.java
  47. 101 0
      src/main/java/com/zhentao/user/domain/UserOnlineStatus.java
  48. 19 0
      src/main/java/com/zhentao/user/dto/NoteDto.java
  49. 11 0
      src/main/java/com/zhentao/user/dto/UserDto.java
  50. 18 0
      src/main/java/com/zhentao/user/mapper/UserMapper.java
  51. 18 0
      src/main/java/com/zhentao/user/mapper/UserMoneyMapper.java
  52. 18 0
      src/main/java/com/zhentao/user/mapper/UserOnlineStatusMapper.java
  53. 20 0
      src/main/java/com/zhentao/user/service/UserMoneyService.java
  54. 20 0
      src/main/java/com/zhentao/user/service/UserOnlineStatusService.java
  55. 20 0
      src/main/java/com/zhentao/user/service/UserService.java
  56. 94 0
      src/main/java/com/zhentao/user/service/impl/UserMoneyServiceImpl.java
  57. 81 0
      src/main/java/com/zhentao/user/service/impl/UserOnlineStatusServiceImpl.java
  58. 262 0
      src/main/java/com/zhentao/user/service/impl/UserServiceImpl.java
  59. 14 0
      src/main/java/com/zhentao/utils/DateUtils.java
  60. 253 0
      src/main/java/com/zhentao/utils/IdVerificationUtil.java
  61. 136 0
      src/main/java/com/zhentao/utils/SnowflakeIdGenerator.java
  62. 241 0
      src/main/java/com/zhentao/utils/VolunteerNumberGenerator.java
  63. 16 0
      src/main/java/com/zhentao/vo/UserVo.java
  64. 12 6
      src/main/resources/application.yml
  65. 22 0
      src/main/resources/mapper/GroupMembersMapper.xml
  66. 26 0
      src/main/resources/mapper/GroupsMapper.xml
  67. 42 0
      src/main/resources/mapper/UserMapper.xml
  68. 27 0
      src/main/resources/mapper/UserMoneyMapper.xml
  69. 0 13
      src/test/java/com/zhentao/ZyzApplicationTests.java

+ 31 - 10
pom.xml

@@ -3,10 +3,10 @@
          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
     <modelVersion>4.0.0</modelVersion>
     <groupId>com.zhentao</groupId>
-    <artifactId>ZYZ</artifactId>
+    <artifactId>gongyi</artifactId>
     <version>0.0.1-SNAPSHOT</version>
-    <name>ZYZ</name>
-    <description>ZYZ</description>
+    <name>gongyi</name>
+    <description>gongyi</description>
     <properties>
         <java.version>1.8</java.version>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -15,6 +15,34 @@
     </properties>
     <dependencies>
         <dependency>
+            <groupId>com.squareup.okhttp3</groupId>
+            <artifactId>okhttp</artifactId>
+            <version>3.13.1</version>
+        </dependency>
+        <dependency>
+            <groupId>com.google.code.gson</groupId>
+            <artifactId>gson</artifactId>
+            <version>2.10.1</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-mongodb</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>fastjson</artifactId>
+            <version>1.2.83</version>
+        </dependency>
+        <dependency>
+            <groupId>io.netty</groupId>
+            <artifactId>netty-all</artifactId>
+            <version>4.1.86.Final</version>
+        </dependency>
+        <dependency>
+            <groupId>org.hibernate.validator</groupId>
+            <artifactId>hibernate-validator</artifactId>
+        </dependency>
+        <dependency>
             <groupId>org.redisson</groupId>
             <artifactId>redisson</artifactId>
             <version>3.17.3</version>
@@ -83,12 +111,6 @@
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter</artifactId>
         </dependency>
-
-        <dependency>
-            <groupId>org.springframework.boot</groupId>
-            <artifactId>spring-boot-starter-test</artifactId>
-            <scope>test</scope>
-        </dependency>
     </dependencies>
     <dependencyManagement>
         <dependencies>
@@ -120,7 +142,6 @@
                 <version>${spring-boot.version}</version>
                 <configuration>
                     <mainClass>com.zhentao.ZyzApplication</mainClass>
-                    <skip>true</skip>
                 </configuration>
                 <executions>
                     <execution>

+ 5 - 0
src/main/java/com/zhentao/ZyzApplication.java

@@ -1,9 +1,14 @@
 package com.zhentao;
 
+import org.mybatis.spring.annotation.MapperScan;
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.ComponentScan;
+
+import javax.annotation.ManagedBean;
 
 @SpringBootApplication
+@MapperScan("com.zhentao.*.mapper")
 public class ZyzApplication {
 
     public static void main(String[] args) {

+ 227 - 0
src/main/java/com/zhentao/common/oss/OssUtil.java

@@ -0,0 +1,227 @@
+package com.zhentao.common.oss;
+
+import com.aliyun.oss.OSS;
+import com.aliyun.oss.OSSClientBuilder;
+import com.aliyun.oss.model.ObjectMetadata;
+import com.aliyun.oss.model.PutObjectRequest;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+@Component
+@Slf4j
+public class OssUtil {
+
+    // 从配置文件中读取OSS相关信息
+    private static final String endpoint = "https://oss-cn-beijing.aliyuncs.com";
+    private static final String accessKeyId = "LTAI5tH9VHPZwGJu4UX3hrL5";
+    private static final String accessKeySecret = "mbsutFJYLkzosvvKNr0DD28XSg4mqA";
+    private static final String bucketName = "fjj1";
+    private static final boolean useCDN =  true;
+
+    private static final String cdnDomain = "https://cdn.yourdomain.com";
+
+    // MIME类型到扩展名的映射
+    private static final Map<String, String> MIME_TO_EXTENSION = new HashMap<>();
+    static {
+        // 图片类型
+        MIME_TO_EXTENSION.put("image/jpeg", "jpg");
+        MIME_TO_EXTENSION.put("image/png", "png");
+        MIME_TO_EXTENSION.put("image/gif", "gif");
+        MIME_TO_EXTENSION.put("image/bmp", "bmp");
+        MIME_TO_EXTENSION.put("image/webp", "webp");
+        MIME_TO_EXTENSION.put("image/svg+xml", "svg");
+
+        // 视频类型-
+        MIME_TO_EXTENSION.put("video/mp4", "mp4");
+        MIME_TO_EXTENSION.put("video/quicktime", "mov");
+        MIME_TO_EXTENSION.put("video/x-msvideo", "avi");
+        MIME_TO_EXTENSION.put("video/x-matroska", "mkv");
+        MIME_TO_EXTENSION.put("video/x-flv", "flv");
+        MIME_TO_EXTENSION.put("video/webm", "webm");
+
+        // 音频类型
+        MIME_TO_EXTENSION.put("audio/mpeg", "mp3");
+        MIME_TO_EXTENSION.put("audio/wav", "wav");
+        MIME_TO_EXTENSION.put("audio/ogg", "ogg");
+        MIME_TO_EXTENSION.put("audio/flac", "flac");
+        MIME_TO_EXTENSION.put("audio/aac", "aac");
+
+        // 文档类型
+        MIME_TO_EXTENSION.put("application/pdf", "pdf");
+        MIME_TO_EXTENSION.put("application/msword", "doc");
+        MIME_TO_EXTENSION.put("application/vnd.openxmlformats-officedocument.wordprocessingml.document", "docx");
+        MIME_TO_EXTENSION.put("application/vnd.ms-excel", "xls");
+        MIME_TO_EXTENSION.put("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "xlsx");
+        MIME_TO_EXTENSION.put("application/vnd.ms-powerpoint", "ppt");
+        MIME_TO_EXTENSION.put("application/vnd.openxmlformats-officedocument.presentationml.presentation", "pptx");
+        MIME_TO_EXTENSION.put("text/plain", "txt");
+    }
+
+
+    public String uploadFile(MultipartFile file) throws IOException {
+        if (file == null || file.isEmpty()) {
+            throw new IllegalArgumentException("上传文件不能为空");
+        }
+
+        OSS ossClient = null;
+        try {
+            ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
+
+            String originalFilename = file.getOriginalFilename();
+            String contentType = file.getContentType();
+            long fileSize = file.getSize();
+
+            // 确定文件类型和扩展名
+            FileTypeInfo fileTypeInfo = determineFileType(originalFilename, contentType);
+            String extension = fileTypeInfo.getExtension();
+            String fileCategory = fileTypeInfo.getCategory();
+
+            // 生成唯一的文件名
+            String fileName = generateUniqueFileName(fileCategory, extension);
+
+            // 创建文件元数据
+            ObjectMetadata metadata = createMetadata(originalFilename, contentType, fileSize, fileCategory);
+
+            // 上传文件
+            try (InputStream inputStream = file.getInputStream()) {
+                PutObjectRequest putObjectRequest = new PutObjectRequest(
+                        bucketName,
+                        fileName,
+                        inputStream,
+                        metadata
+                );
+                ossClient.putObject(putObjectRequest);
+            }
+
+            // 生成文件访问URL
+            String fileUrl = generateFileUrl(fileName);
+            log.info("文件上传成功: {} ({}), 大小: {} KB, URL: {}",
+                    originalFilename, fileCategory, fileSize / 1024, fileUrl);
+
+            return fileUrl;
+        } catch (Exception e) {
+            log.error("文件上传失败: {}", e.getMessage(), e);
+            throw new IOException("文件上传失败: " + e.getMessage(), e);
+        } finally {
+            if (ossClient != null) {
+                ossClient.shutdown();
+            }
+        }
+    }
+
+    private String generateUniqueFileName(String category, String extension) {
+        return "uploads/" + category + "/" + UUID.randomUUID() + "." + extension;
+    }
+
+    private ObjectMetadata createMetadata(String filename, String contentType,
+                                          long fileSize, String category) {
+        ObjectMetadata metadata = new ObjectMetadata();
+
+        // 设置基础元数据
+        metadata.setContentType(contentType != null ? contentType : "application/octet-stream");
+        metadata.setContentLength(fileSize);
+
+        // 设置下载时的文件名
+        if (filename != null) {
+            metadata.setContentDisposition("attachment; filename=\"" + filename + "\"");
+        }
+
+        // 添加自定义元数据
+        metadata.addUserMetadata("Original-Filename", filename != null ? filename : "");
+        metadata.addUserMetadata("File-Category", category);
+
+        return metadata;
+    }
+
+    private FileTypeInfo determineFileType(String filename, String contentType) {
+        String extension = "bin";
+        String category = "other";
+
+        // 1. 尝试从文件名获取扩展名
+        if (filename != null && filename.contains(".")) {
+            String fileExt = filename.substring(filename.lastIndexOf(".") + 1).toLowerCase();
+            if (isValidExtension(fileExt)) {
+                extension = fileExt;
+                category = determineCategory(extension);
+            }
+        }
+
+        // 2. 如果扩展名无效,尝试从内容类型获取
+        if ("bin".equals(extension) && contentType != null) {
+            // 使用映射表获取标准扩展名
+            String mappedExt = MIME_TO_EXTENSION.get(contentType.toLowerCase());
+            if (mappedExt != null) {
+                extension = mappedExt;
+                category = determineCategory(extension);
+            }
+        }
+
+        // 3. 最终确定文件分类
+        if ("other".equals(category)) {
+            category = determineCategory(extension);
+        }
+
+        return new FileTypeInfo(extension, category);
+    }
+
+    private boolean isValidExtension(String ext) {
+        // 检查扩展名是否有效(不含特殊字符)
+        return ext.matches("[a-z0-9]{1,10}");
+    }
+
+    private String determineCategory(String extension) {
+        if (extension.matches("png|jpe?g|gif|bmp|webp|svg|heic|heif")) {
+            return "images";
+        } else if (extension.matches("mp4|mov|avi|mkv|flv|webm|m4v|wmv|3gp")) {
+            return "videos";
+        } else if (extension.matches("mp3|wav|ogg|flac|aac|m4a|wma|amr")) {
+            return "audios";
+        } else if (extension.matches("pdf|docx?|xlsx?|pptx?|txt|rtf|csv|pages|numbers|key")) {
+            return "documents";
+        } else if (extension.matches("zip|rar|7z|tar|gz")) {
+            return "archives";
+        }
+        return "other";
+    }
+
+    private String generateFileUrl(String fileName) {
+        // 使用CDN加速域名(如果配置了),否则使用标准OSS域名
+        if (useCDN && cdnDomain != null && !cdnDomain.isEmpty()) {
+            return cdnDomain + "/" + fileName;
+        }
+
+        // 构建标准OSS域名
+        String domain = endpoint.startsWith("http") ?
+                endpoint : "https://" + endpoint;
+
+        return domain.replaceFirst("://", "://" + bucketName + ".") + "/" + fileName;
+    }
+
+    // 文件类型信息辅助类
+    private static class FileTypeInfo {
+        private final String extension;
+        private final String category;
+
+        public FileTypeInfo(String extension, String category) {
+            this.extension = extension;
+            this.category = category;
+        }
+
+        public String getExtension() {
+            return extension;
+        }
+
+        public String getCategory() {
+            return category;
+        }
+    }
+
+
+}

+ 312 - 0
src/main/java/com/zhentao/common/utils/HttpUtils.java

@@ -0,0 +1,312 @@
+package com.zhentao.common.utils;
+
+
+import com.baomidou.mybatisplus.core.toolkit.StringUtils;
+import org.apache.http.HttpResponse;
+import org.apache.http.NameValuePair;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.HttpDelete;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpPut;
+import org.apache.http.conn.ClientConnectionManager;
+import org.apache.http.conn.scheme.Scheme;
+import org.apache.http.conn.scheme.SchemeRegistry;
+import org.apache.http.conn.ssl.SSLSocketFactory;
+import org.apache.http.entity.ByteArrayEntity;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.message.BasicNameValuePair;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+public class HttpUtils {
+
+	/**
+	 * get
+	 *
+	 * @param host
+	 * @param path
+	 * @param method
+	 * @param headers
+	 * @param querys
+	 * @return
+	 * @throws Exception
+	 */
+	public static HttpResponse doGet(String host, String path, String method,
+			Map<String, String> headers,
+			Map<String, String> querys)
+            throws Exception {
+    	HttpClient httpClient = wrapClient(host);
+
+    	HttpGet request = new HttpGet(buildUrl(host, path, querys));
+        for (Map.Entry<String, String> e : headers.entrySet()) {
+        	request.addHeader(e.getKey(), e.getValue());
+        }
+
+        return httpClient.execute(request);
+    }
+
+	/**
+	 * post form
+	 *
+	 * @param host
+	 * @param path
+	 * @param method
+	 * @param headers
+	 * @param querys
+	 * @param bodys
+	 * @return
+	 * @throws Exception
+	 */
+	public static HttpResponse doPost(String host, String path, String method,
+			Map<String, String> headers,
+			Map<String, String> querys,
+			Map<String, String> bodys)
+            throws Exception {
+    	HttpClient httpClient = wrapClient(host);
+
+    	HttpPost request = new HttpPost(buildUrl(host, path, querys));
+        for (Map.Entry<String, String> e : headers.entrySet()) {
+        	request.addHeader(e.getKey(), e.getValue());
+        }
+
+        if (bodys != null) {
+            List<NameValuePair> nameValuePairList = new ArrayList<NameValuePair>();
+
+            for (String key : bodys.keySet()) {
+                nameValuePairList.add(new BasicNameValuePair(key, bodys.get(key)));
+            }
+            UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(nameValuePairList, "utf-8");
+            formEntity.setContentType("application/x-www-form-urlencoded; charset=UTF-8");
+            request.setEntity(formEntity);
+        }
+
+        return httpClient.execute(request);
+    }
+
+	/**
+	 * Post String
+	 *
+	 * @param host
+	 * @param path
+	 * @param method
+	 * @param headers
+	 * @param querys
+	 * @param body
+	 * @return
+	 * @throws Exception
+	 */
+	public static HttpResponse doPost(String host, String path, String method,
+			Map<String, String> headers,
+			Map<String, String> querys,
+			String body)
+            throws Exception {
+    	HttpClient httpClient = wrapClient(host);
+
+    	HttpPost request = new HttpPost(buildUrl(host, path, querys));
+        for (Map.Entry<String, String> e : headers.entrySet()) {
+        	request.addHeader(e.getKey(), e.getValue());
+        }
+
+        if (StringUtils.isNotBlank(body)) {
+        	request.setEntity(new StringEntity(body, "utf-8"));
+        }
+
+        return httpClient.execute(request);
+    }
+
+	/**
+	 * Post stream
+	 *
+	 * @param host
+	 * @param path
+	 * @param method
+	 * @param headers
+	 * @param querys
+	 * @param body
+	 * @return
+	 * @throws Exception
+	 */
+	public static HttpResponse doPost(String host, String path, String method,
+			Map<String, String> headers,
+			Map<String, String> querys,
+			byte[] body)
+            throws Exception {
+    	HttpClient httpClient = wrapClient(host);
+
+    	HttpPost request = new HttpPost(buildUrl(host, path, querys));
+        for (Map.Entry<String, String> e : headers.entrySet()) {
+        	request.addHeader(e.getKey(), e.getValue());
+        }
+
+        if (body != null) {
+        	request.setEntity(new ByteArrayEntity(body));
+        }
+
+        return httpClient.execute(request);
+    }
+
+	/**
+	 * Put String
+	 * @param host
+	 * @param path
+	 * @param method
+	 * @param headers
+	 * @param querys
+	 * @param body
+	 * @return
+	 * @throws Exception
+	 */
+	public static HttpResponse doPut(String host, String path, String method,
+			Map<String, String> headers,
+			Map<String, String> querys,
+			String body)
+            throws Exception {
+    	HttpClient httpClient = wrapClient(host);
+
+    	HttpPut request = new HttpPut(buildUrl(host, path, querys));
+        for (Map.Entry<String, String> e : headers.entrySet()) {
+        	request.addHeader(e.getKey(), e.getValue());
+        }
+
+        if (StringUtils.isNotBlank(body)) {
+        	request.setEntity(new StringEntity(body, "utf-8"));
+        }
+
+        return httpClient.execute(request);
+    }
+
+	/**
+	 * Put stream
+	 * @param host
+	 * @param path
+	 * @param method
+	 * @param headers
+	 * @param querys
+	 * @param body
+	 * @return
+	 * @throws Exception
+	 */
+	public static HttpResponse doPut(String host, String path, String method,
+			Map<String, String> headers,
+			Map<String, String> querys,
+			byte[] body)
+            throws Exception {
+    	HttpClient httpClient = wrapClient(host);
+
+    	HttpPut request = new HttpPut(buildUrl(host, path, querys));
+        for (Map.Entry<String, String> e : headers.entrySet()) {
+        	request.addHeader(e.getKey(), e.getValue());
+        }
+
+        if (body != null) {
+        	request.setEntity(new ByteArrayEntity(body));
+        }
+
+        return httpClient.execute(request);
+    }
+
+	/**
+	 * Delete
+	 *
+	 * @param host
+	 * @param path
+	 * @param method
+	 * @param headers
+	 * @param querys
+	 * @return
+	 * @throws Exception
+	 */
+	public static HttpResponse doDelete(String host, String path, String method,
+			Map<String, String> headers,
+			Map<String, String> querys)
+            throws Exception {
+    	HttpClient httpClient = wrapClient(host);
+
+    	HttpDelete request = new HttpDelete(buildUrl(host, path, querys));
+        for (Map.Entry<String, String> e : headers.entrySet()) {
+        	request.addHeader(e.getKey(), e.getValue());
+        }
+
+        return httpClient.execute(request);
+    }
+
+	private static String buildUrl(String host, String path, Map<String, String> querys) throws UnsupportedEncodingException {
+    	StringBuilder sbUrl = new StringBuilder();
+    	sbUrl.append(host);
+    	if (!StringUtils.isBlank(path)) {
+    		sbUrl.append(path);
+        }
+    	if (null != querys) {
+    		StringBuilder sbQuery = new StringBuilder();
+        	for (Map.Entry<String, String> query : querys.entrySet()) {
+        		if (0 < sbQuery.length()) {
+        			sbQuery.append("&");
+        		}
+        		if (StringUtils.isBlank(query.getKey()) && !StringUtils.isBlank(query.getValue())) {
+        			sbQuery.append(query.getValue());
+                }
+        		if (!StringUtils.isBlank(query.getKey())) {
+        			sbQuery.append(query.getKey());
+        			if (!StringUtils.isBlank(query.getValue())) {
+        				sbQuery.append("=");
+        				sbQuery.append(URLEncoder.encode(query.getValue(), "utf-8"));
+        			}
+                }
+        	}
+        	if (0 < sbQuery.length()) {
+        		sbUrl.append("?").append(sbQuery);
+        	}
+        }
+
+    	return sbUrl.toString();
+    }
+
+	private static HttpClient wrapClient(String host) {
+		HttpClient httpClient = new DefaultHttpClient();
+		if (host.startsWith("https://")) {
+			sslClient(httpClient);
+		}
+
+		return httpClient;
+	}
+
+	private static void sslClient(HttpClient httpClient) {
+        try {
+            SSLContext ctx = SSLContext.getInstance("TLS");
+            X509TrustManager tm = new X509TrustManager() {
+                public X509Certificate[] getAcceptedIssuers() {
+                    return null;
+                }
+                public void checkClientTrusted(X509Certificate[] xcs, String str) {
+
+                }
+                public void checkServerTrusted(X509Certificate[] xcs, String str) {
+
+                }
+            };
+            ctx.init(null, new TrustManager[] { tm }, null);
+            SSLSocketFactory ssf = new SSLSocketFactory(ctx);
+            ssf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
+            ClientConnectionManager ccm = httpClient.getConnectionManager();
+            SchemeRegistry registry = ccm.getSchemeRegistry();
+            registry.register(new Scheme("https", 443, ssf));
+        } catch (KeyManagementException ex) {
+            throw new RuntimeException(ex);
+        } catch (NoSuchAlgorithmException ex) {
+        	throw new RuntimeException(ex);
+        }
+    }
+}

+ 21 - 0
src/main/java/com/zhentao/dto/DeeseekRequest.java

@@ -0,0 +1,21 @@
+package com.zhentao.dto;
+
+import lombok.Builder;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+@Builder
+public class DeeseekRequest {
+    private String model;
+    private List<Message> messages;
+    private boolean stream;  // 关键:新增 stream 字段,控制是否流式输出
+
+    @Data
+    @Builder
+    public static class Message {
+        private String role;
+        private String content;
+    }
+}

+ 15 - 0
src/main/java/com/zhentao/dto/TrueUser.java

@@ -0,0 +1,15 @@
+package com.zhentao.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class TrueUser {
+    private String userId;
+    private String trueName;
+    private String idCard;
+    private String phone;
+}

+ 46 - 0
src/main/java/com/zhentao/groups/MongoDB/controller/GroupMessageController.java

@@ -0,0 +1,46 @@
+package com.zhentao.groups.MongoDB.controller;
+
+import com.zhentao.common.config.NullLogin;
+import com.zhentao.groups.MongoDB.pojo.GroupMessage;
+import com.zhentao.groups.MongoDB.pojo.Message;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.mongodb.core.MongoTemplate;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RequestMapping("/groupMessage")
+@RestController
+public class GroupMessageController {
+
+    @Autowired
+    RedisTemplate<String,String> redisTemplate;
+
+    @Autowired
+    private MongoTemplate mongoTemplate;
+
+
+    @PostMapping("/cunchugroupMessage")
+    @NullLogin
+    public String groupMessage(@RequestBody Message message)
+    {
+        GroupMessage groupMessage = new GroupMessage();
+        groupMessage.setGroupId(message.getGroupId());
+        groupMessage.setSenderId(message.getFromUserId());
+        groupMessage.setContent(message.getContent());
+        groupMessage.setType(message.getType());
+        groupMessage.setTimestamp(System.currentTimeMillis());
+        mongoTemplate.save(groupMessage);
+        return "success";
+    }
+
+
+
+
+
+
+
+
+}

+ 46 - 0
src/main/java/com/zhentao/groups/MongoDB/pojo/GroupMessage.java

@@ -0,0 +1,46 @@
+package com.zhentao.groups.MongoDB.pojo;
+
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.mongodb.core.mapping.Document;
+
+import java.util.Date;
+
+@Slf4j
+@Document(collection = "group_messages")
+@Data
+public class GroupMessage {
+    @Id
+    private String id;
+
+    /**
+     * 群ID
+     */
+    private Long groupId;
+
+    /**
+     * 发送者ID
+     */
+    private String senderId;
+
+    /**
+     * 消息内容
+     */
+    private String content;
+
+    /**
+     * 消息类型
+     */
+    private String type;
+
+    /**
+     * 消息时间戳
+     */
+    private Long timestamp;
+
+    /**
+     * 创建时间
+     */
+    private Date createdAt;
+}

+ 46 - 0
src/main/java/com/zhentao/groups/MongoDB/pojo/Message.java

@@ -0,0 +1,46 @@
+package com.zhentao.groups.MongoDB.pojo;
+
+import lombok.Data;
+
+@Data
+public class Message {
+    /**
+     * 消息类型
+     */
+    private String type;
+
+    /**
+     * 发送者ID
+     */
+    private String fromUserId;
+
+    /**
+     * 接收者ID
+     */
+    private String toUserId;
+
+    /**
+     * 群ID(群聊消息时使用)
+     */
+    private Long groupId;
+
+    /**
+     * 消息内容
+     */
+    private String content;
+
+    /**
+     * 消息时间戳
+     */
+    private Long timestamp;
+
+    /**
+     * 消息创建时间
+     */
+    private String fileUrl;      // 文件访问URL
+    private String fileName;     // 原始文件名
+    private String fileType;     // 文件MIME类型
+    private Long fileSize;       // 文件大小(字节)
+    private String avatar;
+
+}

+ 23 - 0
src/main/java/com/zhentao/groups/dto/AddGroupMembers.java

@@ -0,0 +1,23 @@
+package com.zhentao.groups.dto;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.util.List;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class AddGroupMembers  implements Serializable {
+
+//  群id
+    @JsonFormat(shape = JsonFormat.Shape.STRING)
+    private Long groupId;
+
+    private List<Long> userId;
+
+    private String groupssId;
+}

+ 39 - 0
src/main/java/com/zhentao/groups/dto/AddGroupsDto.java

@@ -0,0 +1,39 @@
+package com.zhentao.groups.dto;
+
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class AddGroupsDto {
+    /**
+     * 群名称
+     */
+    private String name;
+
+    /**
+     * 创建者ID
+     */
+    @JsonFormat(shape = JsonFormat.Shape.STRING)
+    private Long creatorId;
+
+    /**
+     * 群头像
+     */
+    private String avatar;
+
+    /**
+     * 群公告
+     */
+    private String announcement;
+
+    /**
+     * 群描述
+     */
+    private String description;
+
+}

+ 14 - 0
src/main/java/com/zhentao/groups/dto/AddUserLoginDto.java

@@ -0,0 +1,14 @@
+package com.zhentao.groups.dto;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+@Data
+public class AddUserLoginDto {
+//    群id
+    @JsonFormat(shape = JsonFormat.Shape.STRING)
+    private Long id;
+//    登录的用户
+    @JsonFormat(shape = JsonFormat.Shape.STRING)
+    private Long uid;
+}

+ 23 - 0
src/main/java/com/zhentao/groups/dto/DelGroupMembers.java

@@ -0,0 +1,23 @@
+package com.zhentao.groups.dto;
+
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class DelGroupMembers {
+//  组id
+    @JsonFormat(shape = JsonFormat.Shape.STRING)
+    Long groupssId;
+//  踢出的用户
+    @JsonFormat(shape = JsonFormat.Shape.STRING)
+    Long userId;
+//  当前登录的用户id
+    @JsonFormat(shape = JsonFormat.Shape.STRING)
+    Long  uId;
+
+}

+ 11 - 0
src/main/java/com/zhentao/groups/dto/GroupDto.java

@@ -0,0 +1,11 @@
+package com.zhentao.groups.dto;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class GroupDto {
+    private Long groupId;
+    private List<Long> uid;
+}

+ 10 - 0
src/main/java/com/zhentao/groups/dto/OutGroupDto.java

@@ -0,0 +1,10 @@
+package com.zhentao.groups.dto;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+@Data
+public class OutGroupDto {
+    @JsonFormat(shape = JsonFormat.Shape.STRING)
+    private Long groupsId;
+} 

+ 19 - 0
src/main/java/com/zhentao/groups/dto/OutGroupsDto.java

@@ -0,0 +1,19 @@
+package com.zhentao.groups.dto;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class OutGroupsDto {
+
+    @JsonFormat(shape = JsonFormat.Shape.STRING)
+    private Long groupsId;
+
+    @JsonFormat(shape = JsonFormat.Shape.STRING)
+    private Long uid;
+
+}

+ 13 - 0
src/main/java/com/zhentao/groups/dto/UpdateGroupMemberRoleDto.java

@@ -0,0 +1,13 @@
+package com.zhentao.groups.dto;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+@Data
+public class UpdateGroupMemberRoleDto {
+    @JsonFormat(shape = JsonFormat.Shape.STRING)
+    private Long groupssId;
+    @JsonFormat(shape = JsonFormat.Shape.STRING)
+    private Long userId;
+    private Integer role; // 0-普通成员, 1-管理员
+} 

+ 20 - 0
src/main/java/com/zhentao/groups/mapper/GroupMembersMapper.java

@@ -0,0 +1,20 @@
+package com.zhentao.groups.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zhentao.groups.pojo.GroupMembers;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+* @author lzy
+* @description 针对表【group_members(群组成员表)】的数据库操作Mapper
+* @createDate 2025-06-04 16:00:48
+* @Entity com.zhentao.groups.pojo.GroupMembers
+*/
+@Mapper
+public interface GroupMembersMapper extends BaseMapper<GroupMembers> {
+
+}
+
+
+
+

+ 20 - 0
src/main/java/com/zhentao/groups/mapper/GroupsMapper.java

@@ -0,0 +1,20 @@
+package com.zhentao.groups.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zhentao.groups.pojo.Groupss;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+* @author lzy
+* @description 针对表【groups(群组表)】的数据库操作Mapper
+* @createDate 2025-06-04 16:00:48
+* @Entity com.zhentao.groups.pojo.Groups
+*/
+@Mapper
+public interface GroupsMapper extends BaseMapper<Groupss> {
+
+}
+
+
+
+

+ 114 - 0
src/main/java/com/zhentao/groups/pojo/GroupMembers.java

@@ -0,0 +1,114 @@
+package com.zhentao.groups.pojo;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 群组成员表
+ * @TableName group_members
+ */
+@TableName(value ="group_members")
+@Data
+public class GroupMembers implements Serializable {
+    /**
+     *
+     */
+    @JsonFormat(shape = JsonFormat.Shape.STRING)
+    @TableId
+    private Long id;
+
+    /**
+     * 群ID
+     */
+    @JsonFormat(shape = JsonFormat.Shape.STRING)
+    private Long groupId;
+
+    /**
+     * 用户ID
+     */
+    @JsonFormat(shape = JsonFormat.Shape.STRING)
+    private Long userId;
+
+    /**
+     * 群昵称
+     */
+    private String nickname;
+
+    /**
+     * 角色(0-成员,1-管理员,2-群主)
+     */
+    private Integer role;
+
+    /**
+     * 加入时间
+     */
+    private Date joinTime;
+
+    /**
+     * 最后确认的消息ID
+     */
+    @JsonFormat(shape = JsonFormat.Shape.STRING)
+    private Long lastAckMsgId;
+
+    @TableField(exist = false)
+    private static final long serialVersionUID = 1L;
+
+    @Override
+    public boolean equals(Object that) {
+        if (this == that) {
+            return true;
+        }
+        if (that == null) {
+            return false;
+        }
+        if (getClass() != that.getClass()) {
+            return false;
+        }
+        GroupMembers other = (GroupMembers) that;
+        return (this.getId() == null ? other.getId() == null : this.getId().equals(other.getId()))
+            && (this.getGroupId() == null ? other.getGroupId() == null : this.getGroupId().equals(other.getGroupId()))
+            && (this.getUserId() == null ? other.getUserId() == null : this.getUserId().equals(other.getUserId()))
+            && (this.getNickname() == null ? other.getNickname() == null : this.getNickname().equals(other.getNickname()))
+            && (this.getRole() == null ? other.getRole() == null : this.getRole().equals(other.getRole()))
+            && (this.getJoinTime() == null ? other.getJoinTime() == null : this.getJoinTime().equals(other.getJoinTime()))
+            && (this.getLastAckMsgId() == null ? other.getLastAckMsgId() == null : this.getLastAckMsgId().equals(other.getLastAckMsgId()));
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((getId() == null) ? 0 : getId().hashCode());
+        result = prime * result + ((getGroupId() == null) ? 0 : getGroupId().hashCode());
+        result = prime * result + ((getUserId() == null) ? 0 : getUserId().hashCode());
+        result = prime * result + ((getNickname() == null) ? 0 : getNickname().hashCode());
+        result = prime * result + ((getRole() == null) ? 0 : getRole().hashCode());
+        result = prime * result + ((getJoinTime() == null) ? 0 : getJoinTime().hashCode());
+        result = prime * result + ((getLastAckMsgId() == null) ? 0 : getLastAckMsgId().hashCode());
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append(getClass().getSimpleName());
+        sb.append(" [");
+        sb.append("Hash = ").append(hashCode());
+        sb.append(", id=").append(id);
+        sb.append(", groupId=").append(groupId);
+        sb.append(", userId=").append(userId);
+        sb.append(", nickname=").append(nickname);
+        sb.append(", role=").append(role);
+        sb.append(", joinTime=").append(joinTime);
+        sb.append(", lastAckMsgId=").append(lastAckMsgId);
+        sb.append(", serialVersionUID=").append(serialVersionUID);
+        sb.append("]");
+        return sb.toString();
+    }
+}

+ 139 - 0
src/main/java/com/zhentao/groups/pojo/Groupss.java

@@ -0,0 +1,139 @@
+package com.zhentao.groups.pojo;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 群组表
+ * @TableName groups
+ */
+@TableName(value ="groupss")
+@Data
+public class Groupss implements Serializable {
+    /**
+     * id
+
+     */
+    @JsonFormat(shape = JsonFormat.Shape.STRING)
+    @TableId
+    private Long groupId;
+
+    /**
+     * 群名称
+     */
+    private String name;
+
+    /**
+     * 创建者ID
+     */
+    @JsonFormat(shape = JsonFormat.Shape.STRING)
+    private Long creatorId;
+
+    /**
+     * 群头像
+     */
+    private String avatar;
+
+    /**
+     * 群公告
+     */
+    private String announcement;
+
+    /**
+     * 群描述
+     */
+    private String description;
+
+    /**
+     * 最大成员数
+     */
+    private Integer maxMembers;
+
+    /**
+     * 状态(0-解散,1-正常)
+     */
+    private Integer status;
+
+    /**
+     * 创建时间
+     */
+    private Date createdAt;
+
+    /**
+     * 修改时间
+     */
+    private Date updatedAt;
+    @TableField(exist = false)
+    private List<GroupMembers> list;
+    @TableField(exist = false)
+    private static final long serialVersionUID = 1L;
+
+    @Override
+    public boolean equals(Object that) {
+        if (this == that) {
+            return true;
+        }
+        if (that == null) {
+            return false;
+        }
+        if (getClass() != that.getClass()) {
+            return false;
+        }
+        Groupss other = (Groupss) that;
+        return (this.getGroupId() == null ? other.getGroupId() == null : this.getGroupId().equals(other.getGroupId()))
+            && (this.getName() == null ? other.getName() == null : this.getName().equals(other.getName()))
+            && (this.getCreatorId() == null ? other.getCreatorId() == null : this.getCreatorId().equals(other.getCreatorId()))
+            && (this.getAvatar() == null ? other.getAvatar() == null : this.getAvatar().equals(other.getAvatar()))
+            && (this.getAnnouncement() == null ? other.getAnnouncement() == null : this.getAnnouncement().equals(other.getAnnouncement()))
+            && (this.getDescription() == null ? other.getDescription() == null : this.getDescription().equals(other.getDescription()))
+            && (this.getMaxMembers() == null ? other.getMaxMembers() == null : this.getMaxMembers().equals(other.getMaxMembers()))
+            && (this.getStatus() == null ? other.getStatus() == null : this.getStatus().equals(other.getStatus()))
+            && (this.getCreatedAt() == null ? other.getCreatedAt() == null : this.getCreatedAt().equals(other.getCreatedAt()))
+            && (this.getUpdatedAt() == null ? other.getUpdatedAt() == null : this.getUpdatedAt().equals(other.getUpdatedAt()));
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((getGroupId() == null) ? 0 : getGroupId().hashCode());
+        result = prime * result + ((getName() == null) ? 0 : getName().hashCode());
+        result = prime * result + ((getCreatorId() == null) ? 0 : getCreatorId().hashCode());
+        result = prime * result + ((getAvatar() == null) ? 0 : getAvatar().hashCode());
+        result = prime * result + ((getAnnouncement() == null) ? 0 : getAnnouncement().hashCode());
+        result = prime * result + ((getDescription() == null) ? 0 : getDescription().hashCode());
+        result = prime * result + ((getMaxMembers() == null) ? 0 : getMaxMembers().hashCode());
+        result = prime * result + ((getStatus() == null) ? 0 : getStatus().hashCode());
+        result = prime * result + ((getCreatedAt() == null) ? 0 : getCreatedAt().hashCode());
+        result = prime * result + ((getUpdatedAt() == null) ? 0 : getUpdatedAt().hashCode());
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append(getClass().getSimpleName());
+        sb.append(" [");
+        sb.append("Hash = ").append(hashCode());
+        sb.append(", groupId=").append(groupId);
+        sb.append(", name=").append(name);
+        sb.append(", creatorId=").append(creatorId);
+        sb.append(", avatar=").append(avatar);
+        sb.append(", announcement=").append(announcement);
+        sb.append(", description=").append(description);
+        sb.append(", maxMembers=").append(maxMembers);
+        sb.append(", status=").append(status);
+        sb.append(", createdAt=").append(createdAt);
+        sb.append(", updatedAt=").append(updatedAt);
+        sb.append(", serialVersionUID=").append(serialVersionUID);
+        sb.append("]");
+        return sb.toString();
+    }
+}

+ 13 - 0
src/main/java/com/zhentao/groups/service/GroupMembersService.java

@@ -0,0 +1,13 @@
+package com.zhentao.groups.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.zhentao.groups.pojo.GroupMembers;
+
+/**
+* @author lzy
+* @description 针对表【group_members(群组成员表)】的数据库操作Service
+* @createDate 2025-06-04 16:00:48
+*/
+public interface GroupMembersService extends IService<GroupMembers> {
+
+}

+ 18 - 0
src/main/java/com/zhentao/groups/service/GroupsService.java

@@ -0,0 +1,18 @@
+package com.zhentao.groups.service;
+
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.zhentao.groups.dto.*;
+import com.zhentao.groups.pojo.Groupss;
+
+import java.util.List;
+
+/**
+* @author lzy
+* @description 针对表【groups(群组表)】的数据库操作Service
+* @createDate 2025-06-04 16:00:48
+*/
+public interface GroupsService extends IService<Groupss> {
+//  查询所有的一个群ID
+    List<GroupDto> getList();
+}

+ 22 - 0
src/main/java/com/zhentao/groups/service/impl/GroupMembersServiceImpl.java

@@ -0,0 +1,22 @@
+package com.zhentao.groups.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.zhentao.groups.mapper.GroupMembersMapper;
+import com.zhentao.groups.pojo.GroupMembers;
+import com.zhentao.groups.service.GroupMembersService;
+import org.springframework.stereotype.Service;
+
+/**
+* @author lzy
+* @description 针对表【group_members(群组成员表)】的数据库操作Service实现
+* @createDate 2025-06-04 16:00:48
+*/
+@Service
+public class GroupMembersServiceImpl extends ServiceImpl<GroupMembersMapper, GroupMembers>
+    implements GroupMembersService{
+
+}
+
+
+
+

+ 56 - 0
src/main/java/com/zhentao/groups/service/impl/GroupsServiceImpl.java

@@ -0,0 +1,56 @@
+package com.zhentao.groups.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.zhentao.groups.dto.*;
+import com.zhentao.groups.mapper.GroupsMapper;
+import com.zhentao.groups.pojo.GroupMembers;
+import com.zhentao.groups.pojo.Groupss;
+import com.zhentao.groups.service.GroupMembersService;
+import com.zhentao.groups.service.GroupsService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+* @author lzy
+* @description 针对表【groups(群组表)】的数据库操作Service实现
+* @createDate 2025-06-04 16:00:48
+*/
+@Service
+public class GroupsServiceImpl extends ServiceImpl<GroupsMapper, Groupss>
+    implements GroupsService{
+
+    @Autowired
+    private GroupMembersService groupMembersService;
+
+    //查询所有的群ID和群里面成员ID
+        @Override
+        public List<GroupDto> getList() {
+            List<Groupss> list = this.list();
+            List<GroupDto> list3 = new ArrayList<>();
+    //        所有的群ID
+            List<Long> collect = list.stream().map(Groupss::getGroupId).collect(Collectors.toList());
+            List<GroupMembers> list1 = groupMembersService.list();
+            for (Long c:collect) {
+                List<Long> list2 = new ArrayList<>();
+                for (GroupMembers m: list1) {
+                    if (c.equals(m.getGroupId())){
+                        list2.add(m.getUserId());
+                    }
+                }
+                GroupDto groupDto = new GroupDto();
+                groupDto.setGroupId(c);
+                groupDto.setUid(list2);
+                list3.add(groupDto);
+            }
+            return list3;
+        }
+
+}
+
+
+
+
+

+ 69 - 0
src/main/java/com/zhentao/groups/vo/GroupsVo.java

@@ -0,0 +1,69 @@
+package com.zhentao.groups.vo;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.util.Date;
+import java.util.List;
+
+@Data
+public class GroupsVo<T> {
+
+
+
+    @JsonFormat(shape = JsonFormat.Shape.STRING)
+    @TableId
+    private Long groupId;
+
+    /**
+     * 群名称
+     */
+    private String name;
+
+    /**
+     * 创建者ID
+     */
+    @JsonFormat(shape = JsonFormat.Shape.STRING)
+    private Long creatorId;
+
+    /**
+     * 群头像
+     */
+    private String avatar;
+
+    /**
+     * 群公告
+     */
+    private String announcement;
+
+    /**
+     * 群描述
+     */
+    private String description;
+
+    /**
+     * 最大成员数
+     */
+    private Integer maxMembers;
+
+    /**
+     * 状态(0-解散,1-正常)
+     */
+    private Integer status;
+
+    /**
+     * 创建时间
+     */
+    private Date createdAt;
+
+    /**
+     * 修改时间
+     */
+    private Date updatedAt;
+
+    private List<UserVo> user;
+
+
+
+}

+ 17 - 0
src/main/java/com/zhentao/groups/vo/UserVo.java

@@ -0,0 +1,17 @@
+package com.zhentao.groups.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+@Data
+public class UserVo {
+    @JsonFormat(shape = JsonFormat.Shape.STRING)
+    private Long userId;
+    private String nickName;
+
+    private String img;
+
+
+    private Integer role;
+
+}

+ 87 - 0
src/main/java/com/zhentao/information/cache/ChannelCache.java

@@ -0,0 +1,87 @@
+package com.zhentao.information.cache;
+
+import io.netty.channel.ChannelHandlerContext;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Channel缓存管理类
+ * 用于管理用户Channel
+ */
+@Slf4j
+@Component
+public class ChannelCache {
+
+    /**
+     * 用户ID和Channel的映射关系
+     * key: 用户ID
+     * value: Channel上下文
+     */
+    private static final Map<String, ChannelHandlerContext> USER_CHANNEL_MAP = new ConcurrentHashMap<>();
+
+    private final Map<ChannelHandlerContext, String> reverseMap = new ConcurrentHashMap<>();
+
+    /**
+     * 添加用户Channel映射
+     * @param userId 用户ID
+     * @param ctx Channel上下文
+     */
+    public void addCache(String userId, ChannelHandlerContext ctx) {
+        USER_CHANNEL_MAP.put(userId, ctx);
+        reverseMap.put(ctx, userId);
+        log.info("用户 {} 的Channel已缓存", userId);
+    }
+
+    /**
+     * 获取用户的Channel
+     * @param userId 用户ID
+     * @return Channel上下文
+     */
+    public ChannelHandlerContext getCache(String userId) {
+        return USER_CHANNEL_MAP.get(userId);
+    }
+
+    /**
+     * 移除用户Channel映射
+     * @param ctx Channel上下文
+     */
+    public void removeCache(ChannelHandlerContext ctx) {
+        String userId = reverseMap.remove(ctx);
+        if (userId != null) {
+            USER_CHANNEL_MAP.remove(userId);
+            log.info("用户 {} 的Channel已移除", userId);
+        }
+    }
+
+    /**
+     * 移除用户Channel映射
+     * @param userId 用户ID
+     */
+    public void removeCache(String userId) {
+        ChannelHandlerContext ctx = USER_CHANNEL_MAP.remove(userId);
+        if (ctx != null) {
+            reverseMap.remove(ctx);
+            log.info("用户 {} 的Channel已移除", userId);
+        }
+    }
+
+    /**
+     * 获取所有用户Channel映射
+     * @return 用户Channel映射Map
+     */
+    public Map<String, ChannelHandlerContext> getAllCache() {
+        return USER_CHANNEL_MAP;
+    }
+
+    /**
+     * 判断用户是否在线
+     * @param userId 用户ID
+     * @return 是否在线
+     */
+    public boolean isOnline(String userId) {
+        return USER_CHANNEL_MAP.containsKey(userId);
+    }
+}

+ 73 - 0
src/main/java/com/zhentao/information/cache/GroupChannelCache.java

@@ -0,0 +1,73 @@
+package com.zhentao.information.cache;
+
+import io.netty.channel.group.ChannelGroup;
+import io.netty.channel.group.DefaultChannelGroup;
+import io.netty.util.concurrent.GlobalEventExecutor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 群聊Channel缓存管理类
+ * 用于管理群聊ChannelGroup
+ */
+@Slf4j
+@Component
+public class GroupChannelCache {
+    
+    /**
+     * 群ID和ChannelGroup的映射关系
+     * key: 群ID
+     * value: ChannelGroup
+     */
+    private static final Map<Long, ChannelGroup> GROUP_CHANNEL_MAP = new ConcurrentHashMap<>();
+
+    /**
+     * 添加群聊ChannelGroup
+     * @param groupId 群ID
+     * @return ChannelGroup
+     */
+    public ChannelGroup addGroup(Long groupId) {
+        ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
+        GROUP_CHANNEL_MAP.put(groupId, channelGroup);
+        log.info("群 {} 的ChannelGroup已创建", groupId);
+        return channelGroup;
+    }
+
+    /**
+     * 获取群聊ChannelGroup
+     * @param groupId 群ID
+     * @return ChannelGroup
+     */
+    public ChannelGroup getGroup(Long groupId) {
+        return GROUP_CHANNEL_MAP.get(groupId);
+    }
+
+    /**
+     * 移除群聊ChannelGroup
+     * @param groupId 群ID
+     */
+    public void removeGroup(Long groupId) {
+        GROUP_CHANNEL_MAP.remove(groupId);
+        log.info("群 {} 的ChannelGroup已移除", groupId);
+    }
+
+    /**
+     * 获取所有群聊ChannelGroup
+     * @return 群聊ChannelGroup映射Map
+     */
+    public Map<Long, ChannelGroup> getAllGroups() {
+        return GROUP_CHANNEL_MAP;
+    }
+
+    /**
+     * 判断群聊是否存在
+     * @param groupId 群ID
+     * @return 是否存在
+     */
+    public boolean exists(Long groupId) {
+        return GROUP_CHANNEL_MAP.containsKey(groupId);
+    }
+} 

+ 97 - 0
src/main/java/com/zhentao/information/cache/GroupMemberCache.java

@@ -0,0 +1,97 @@
+package com.zhentao.information.cache;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 群成员缓存管理类
+ * 用于缓存群ID和群成员ID的映射关系
+ */
+@Slf4j
+@Component
+public class GroupMemberCache {
+    
+    /**
+     * 群ID和群成员ID列表的映射关系
+     * key: 群ID
+     * value: 群成员ID列表
+     */
+    private final Map<Long, List<Long>> groupMemberMap = new ConcurrentHashMap<>();
+
+    /**
+     * 更新群成员缓存
+     * @param groupId 群ID
+     * @param memberIds 群成员ID列表
+     */
+    public void updateGroupMembers(Long groupId, List<Long> memberIds) {
+        groupMemberMap.put(groupId, memberIds);
+        log.info("群 {} 的成员缓存已更新,成员数:{}", groupId, memberIds.size());
+    }
+
+    /**
+     * 获取群成员列表
+     * @param groupId 群ID
+     * @return 群成员ID列表
+     */
+    public List<Long> getGroupMembers(Long groupId) {
+        return groupMemberMap.get(groupId);
+    }
+
+    /**
+     * 添加群成员
+     * @param groupId 群ID
+     * @param userId 用户ID
+     */
+    public void addGroupMember(Long groupId, Long userId) {
+        List<Long> members = groupMemberMap.get(groupId);
+        if (members != null && !members.contains(userId)) {
+            members.add(userId);
+            log.info("用户 {} 已添加到群 {} 的成员缓存", userId, groupId);
+        }
+    }
+
+    /**
+     * 移除群成员
+     * @param groupId 群ID
+     * @param userId 用户ID
+     */
+    public void removeGroupMember(Long groupId, Long userId) {
+        List<Long> members = groupMemberMap.get(groupId);
+        if (members != null) {
+            members.remove(userId);
+            log.info("用户 {} 已从群 {} 的成员缓存移除", userId, groupId);
+        }
+    }
+
+    /**
+     * 判断用户是否在群中
+     * @param groupId 群ID
+     * @param userId 用户ID
+     * @return 是否在群中
+     */
+    public boolean isUserInGroup(Long groupId, Long userId) {
+        List<Long> members = groupMemberMap.get(groupId);
+        return members != null && members.contains(userId);
+    }
+
+    /**
+     * 获取所有群成员映射
+     * @return 群成员映射Map
+     */
+    public Map<Long, List<Long>> getAllGroupMembers() {
+        return groupMemberMap;
+    }
+
+    /**
+     * 清除群成员缓存
+     * @param groupId 群ID
+     */
+    public void clearGroupMembers(Long groupId) {
+        groupMemberMap.remove(groupId);
+        log.info("群 {} 的成员缓存已清除", groupId);
+    }
+} 

+ 128 - 0
src/main/java/com/zhentao/information/config/NettyConfig.java

@@ -0,0 +1,128 @@
+package com.zhentao.information.config;
+
+import com.zhentao.information.handler.WebSocketHandler;
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.ChannelOption;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.SocketChannel;
+import io.netty.channel.socket.nio.NioServerSocketChannel;
+import io.netty.handler.codec.http.HttpObjectAggregator;
+import io.netty.handler.codec.http.HttpServerCodec;
+import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
+import io.netty.handler.stream.ChunkedWriteHandler;
+import io.netty.handler.timeout.IdleStateHandler;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import javax.annotation.PreDestroy;
+import javax.annotation.Resource;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Netty服务器配置类
+ * 配置WebSocket服务器的启动参数和处理器链,集成AI功能
+ */
+@Slf4j
+@Configuration
+public class NettyConfig {
+
+    /**
+     * WebSocket服务器端口
+     */
+    @Value("${netty.websocket.port:8888}")
+    private int port;
+
+    /**
+     * WebSocket消息处理器
+     */
+    @Resource
+    private WebSocketHandler webSocketHandler;
+
+    /**
+     * 线程组,用于优雅关闭
+     */
+    private EventLoopGroup bossGroup;
+    private EventLoopGroup workerGroup;
+
+    /**
+     * 配置并启动Netty服务器
+     * @return ServerBootstrap实例
+     */
+    @Bean
+    public ServerBootstrap serverBootstrap() {
+        // 创建主从线程组
+        // bossGroup用于接收客户端连接
+        bossGroup = new NioEventLoopGroup(1);
+        // workerGroup用于处理客户端数据,使用CPU核心数的2倍
+        workerGroup = new NioEventLoopGroup(Runtime.getRuntime().availableProcessors() * 2);
+
+        // 创建服务器启动对象
+        ServerBootstrap bootstrap = new ServerBootstrap();
+        bootstrap.group(bossGroup, workerGroup)
+                // 设置服务器通道实现
+                .channel(NioServerSocketChannel.class)
+                // 设置线程队列等待连接个数
+                .option(ChannelOption.SO_BACKLOG, 1024)
+                // 设置保持活动连接状态
+                .childOption(ChannelOption.SO_KEEPALIVE, true)
+                // 禁用Nagle算法,减少延迟
+                .childOption(ChannelOption.TCP_NODELAY, true)
+                // 设置接收缓冲区大小
+                .childOption(ChannelOption.SO_RCVBUF, 65536)
+                // 设置发送缓冲区大小
+                .childOption(ChannelOption.SO_SNDBUF, 65536)
+                // 设置处理器
+                .childHandler(new ChannelInitializer<SocketChannel>() {
+                    @Override
+                    protected void initChannel(SocketChannel ch) {
+                        // 获取管道
+                        ch.pipeline()
+                                // HTTP编解码器
+                                .addLast(new HttpServerCodec())
+                                // 支持大数据流
+                                .addLast(new ChunkedWriteHandler())
+                                // HTTP消息聚合器,增加缓冲区大小以支持AI流式响应
+                                .addLast(new HttpObjectAggregator(1024 * 1024))
+                                // 调整心跳检测时间:60秒没有收到消息就触发
+                                .addLast(new IdleStateHandler(60, 0, 0, TimeUnit.SECONDS))
+                                // WebSocket协议处理器,支持AI流式输出
+                                .addLast(new WebSocketServerProtocolHandler("/ws", null, true, 1024 * 1024))
+                                // 自定义消息处理器
+                                .addLast(webSocketHandler);
+                    }
+                });
+
+        try {
+            // 绑定端口并启动服务器
+            bootstrap.bind(port).sync();
+            log.info("Netty WebSocket服务器启动成功,端口:{}", port);
+            log.info("AI功能已集成,发送消息给用户ID: 1933707308387405824 将触发AI对话");
+        } catch (InterruptedException e) {
+            log.error("Netty WebSocket服务器启动失败", e);
+            Thread.currentThread().interrupt();
+            // 优雅关闭线程组
+            shutdownGracefully();
+        }
+
+        return bootstrap;
+    }
+
+    /**
+     * 应用关闭时优雅关闭Netty服务器
+     */
+    @PreDestroy
+    public void shutdownGracefully() {
+        log.info("正在关闭Netty WebSocket服务器...");
+        if (bossGroup != null) {
+            bossGroup.shutdownGracefully();
+        }
+        if (workerGroup != null) {
+            workerGroup.shutdownGracefully();
+        }
+        log.info("Netty WebSocket服务器已关闭");
+    }
+}

+ 43 - 0
src/main/java/com/zhentao/information/config/WebSocketHandshakeInterceptor.java

@@ -0,0 +1,43 @@
+package com.zhentao.information.config;
+
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.handler.codec.http.FullHttpRequest;
+import io.netty.util.AttributeKey;
+
+import java.net.URI;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+
+public class WebSocketHandshakeInterceptor extends ChannelInboundHandlerAdapter {
+    public static final AttributeKey<String> PEER_ID_KEY = AttributeKey.valueOf("peerId");
+    public static final AttributeKey<String> TOKEN_KEY = AttributeKey.valueOf("token");
+
+    @Override
+    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
+        if (msg instanceof FullHttpRequest) {
+            FullHttpRequest req = (FullHttpRequest) msg;
+            URI uri = new URI(req.uri());
+            String query = uri.getQuery();
+            if (query != null) {
+                Map<String, String> params = new HashMap<>();
+                for (String param : query.split("&")) {
+                    String[] pair = param.split("=");
+                    if (pair.length == 2) {
+                        params.put(URLDecoder.decode(pair[0], String.valueOf(StandardCharsets.UTF_8)),
+                                   URLDecoder.decode(pair[1], String.valueOf(StandardCharsets.UTF_8)));
+                    }
+                }
+                if (params.containsKey("peerId")) {
+                    ctx.channel().attr(PEER_ID_KEY).set(params.get("peerId"));
+                }
+                if (params.containsKey("token")) {
+                    ctx.channel().attr(TOKEN_KEY).set(params.get("token"));
+                }
+            }
+        }
+        super.channelRead(ctx, msg);
+    }
+}

+ 39 - 0
src/main/java/com/zhentao/information/controller/FileController.java

@@ -0,0 +1,39 @@
+package com.zhentao.information.controller;
+
+import com.zhentao.common.oss.OssUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@Slf4j
+@RestController
+public class FileController {
+
+    @Autowired
+    private OssUtil ossUtil;
+
+    @PostMapping("/api/file/upload")
+    public Map<String, Object> uploadFile(@RequestParam("file") MultipartFile file) {
+        Map<String, Object> result = new HashMap<>();
+        try {
+            String fileUrl = ossUtil.uploadFile(file);
+            result.put("url", fileUrl);
+            result.put("fileName", file.getOriginalFilename());
+            result.put("fileType", file.getContentType());
+            result.put("fileSize", file.getSize());
+            result.put("success", true);
+            log.info("文件上传成功: {}", fileUrl);
+        } catch (Exception e) {
+            log.error("文件上传失败", e);
+            result.put("success", false);
+            result.put("message", "上传失败: " + e.getMessage());
+        }
+        return result;
+    }
+}

+ 109 - 0
src/main/java/com/zhentao/information/controller/MessageController.java

@@ -0,0 +1,109 @@
+package com.zhentao.information.controller;
+
+import com.alibaba.fastjson.JSON;
+import com.zhentao.common.config.NullLogin;
+import com.zhentao.common.utils.TokenUtils;
+import com.zhentao.information.cache.ChannelCache;
+import com.zhentao.information.entity.ChatMessage;
+import com.zhentao.information.entity.Message;
+import com.zhentao.information.repository.ChatMessageRepository;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.List;
+
+/**
+ * 消息控制器
+ */
+@Slf4j
+@RestController
+@RequestMapping("/api/message")
+public class MessageController {
+
+    @Resource
+    private ChannelCache channelCache;
+
+    @Resource
+    private ChatMessageRepository chatMessageRepository;
+
+    /**
+     * 发送消息接口
+     */
+    @PostMapping("/send")
+    @NullLogin
+    public String sendMessage(@RequestBody Message message) {
+        log.info("收到消息:发送者={}, 接收者={}, 内容={}",
+                message.getFromUserId(),
+                message.getToUserId(),
+                message.getContent());
+
+        // 生成聊天ID(确保两个用户之间的聊天ID唯一)
+        String chatId = generateChatId(message.getFromUserId(), message.getToUserId());
+
+        // 创建MongoDB消息对象
+        ChatMessage chatMessage = new ChatMessage();
+        chatMessage.setFromUserId(message.getFromUserId());
+        chatMessage.setToUserId(message.getToUserId());
+        chatMessage.setContent(message.getContent());
+        chatMessage.setType(String.valueOf(message.getType()));
+        chatMessage.setTimestamp(System.currentTimeMillis());
+        chatMessage.setIsRead(false);
+        chatMessage.setChatId(chatId);
+
+        // 保存消息到MongoDB
+        chatMessageRepository.save(chatMessage);
+
+        // 获取接收者的Channel
+        ChannelHandlerContext toUserCtx = channelCache.getCache(message.getToUserId());
+
+        if (toUserCtx != null) {
+            // 发送消息给接收者
+            toUserCtx.channel().writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(message)));
+            return "消息已发送";
+        } else {
+            return "消息已保存,等待接收者上线";
+        }
+    }
+
+    /**
+     * 获取两个用户之间的聊天记录
+     */
+    @GetMapping("/history")
+    @NullLogin
+    public List<ChatMessage> getChatHistory(@RequestHeader("token") String token, @RequestParam String userId2) {
+        String userIdFromToken = TokenUtils.getUserIdFromToken(token);
+        String chatId = generateChatId(userIdFromToken, userId2);
+        return chatMessageRepository.findByChatId(chatId);
+    }
+
+    /**
+     * 获取用户的未读消息
+     */
+    @GetMapping("/unread")
+    @NullLogin
+    public List<ChatMessage> getUnreadMessages(@RequestParam String userId) {
+        return chatMessageRepository.findByToUserIdAndIsReadFalse(userId);
+    }
+
+    /**
+     * 查询某个群聊的所有消息
+     */
+    @GetMapping("/group/history")
+    @NullLogin
+    public List<ChatMessage> getGroupChatHistory(@RequestParam String groupId) {
+        // 查询chatId为group_群ID的所有消息
+        return chatMessageRepository.findByChatId("group_" + groupId);
+    }
+
+    /**
+     * 生成聊天ID
+     */
+    private String generateChatId(String userId1, String userId2) {
+        return userId1.compareTo(userId2) < 0 ?
+                userId1 + "_" + userId2 :
+                userId2 + "_" + userId1;
+    }
+}

+ 48 - 0
src/main/java/com/zhentao/information/entity/ChatMessage.java

@@ -0,0 +1,48 @@
+package com.zhentao.information.entity;
+
+import lombok.Data;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.mongodb.core.index.Indexed;
+import org.springframework.data.mongodb.core.mapping.Document;
+
+/**
+ * 聊天消息实体类(MongoDB)
+ */
+@Data
+@Document(collection = "chat_messages")
+public class ChatMessage {
+
+    @Id
+    private String id;
+
+    @Indexed
+    private String fromUserId;
+
+    @Indexed
+    private String toUserId;
+
+    private String content;
+
+    private String type;
+
+    private Long timestamp;
+
+    private Boolean isRead;
+
+    // 复合索引:用于查询两个用户之间的聊天记录
+    @Indexed
+    private String chatId;
+    // 新增文件元数据
+    private String fileUrl;      // OSS访问地址
+    private String fileName;     // 原始文件名
+    private String fileType;     // MIME类型
+    private Long fileSize;       // 文件大小(字节)
+    private String avatar; // 新增:发送者头像
+
+    public String getAvatar() {
+        return avatar;
+    }
+    public void setAvatar(String avatar) {
+        this.avatar = avatar;
+    }
+}

+ 29 - 0
src/main/java/com/zhentao/information/entity/Message.java

@@ -0,0 +1,29 @@
+package com.zhentao.information.entity;
+
+import lombok.Data;
+
+/**
+ * WebSocket消息实体类
+ */
+@Data
+public class Message {
+    private String type;  // 新增类型: image/video/file
+    private String fromUserId;
+    private String toUserId;
+    private Long groupId;
+    private String content;
+    private Long timestamp;
+    // 新增文件元数据字段
+    private String fileUrl;      // 文件访问URL
+    private String fileName;     // 原始文件名
+    private String fileType;     // 文件MIME类型
+    private Long fileSize;       // 文件大小(字节)
+    private String avatar; // 新增:发送者头像
+
+    public String getAvatar() {
+        return avatar;
+    }
+    public void setAvatar(String avatar) {
+        this.avatar = avatar;
+    }
+}

+ 76 - 0
src/main/java/com/zhentao/information/handler/HeartbeatHandler.java

@@ -0,0 +1,76 @@
+package com.zhentao.information.handler;
+
+import io.netty.channel.ChannelHandler;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.handler.timeout.IdleState;
+import io.netty.handler.timeout.IdleStateEvent;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+/**
+ * 心跳处理器
+ * 处理客户端的心跳检测
+ */
+@Slf4j
+@Component
+@ChannelHandler.Sharable
+public class HeartbeatHandler extends ChannelInboundHandlerAdapter {
+
+    private static final int MAX_MISSED_HEARTBEATS = 3;
+    private int missedHeartbeats = 0;
+
+    /**
+     * 处理用户事件
+     * 当触发IdleStateEvent时调用
+     * @param ctx Channel上下文
+     * @param evt 事件对象
+     */
+    @Override
+    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
+        if (evt instanceof IdleStateEvent) {
+            IdleStateEvent event = (IdleStateEvent) evt;
+            
+            if (event.state() == IdleState.READER_IDLE) {
+                missedHeartbeats++;
+                log.warn("读空闲,已错过 {} 次心跳", missedHeartbeats);
+                
+                if (missedHeartbeats >= MAX_MISSED_HEARTBEATS) {
+                    log.error("读空闲超时,关闭连接:{}", ctx.channel().id().asLongText());
+                    ctx.close();
+                } else {
+                    // 发送心跳检测消息
+                    ctx.writeAndFlush("ping");
+                }
+            } else if (event.state() == IdleState.WRITER_IDLE) {
+                // 发送心跳检测消息
+                ctx.writeAndFlush("ping");
+            }
+        } else {
+            super.userEventTriggered(ctx, evt);
+        }
+    }
+
+    @Override
+    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
+        if (msg instanceof String && "pong".equals(msg)) {
+            // 收到心跳响应,重置计数器
+            missedHeartbeats = 0;
+            log.debug("收到心跳响应");
+        } else {
+            super.channelRead(ctx, msg);
+        }
+    }
+
+    @Override
+    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
+        log.info("连接断开:{}", ctx.channel().id().asLongText());
+        super.channelInactive(ctx);
+    }
+
+    @Override
+    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
+        log.error("心跳处理器异常", cause);
+        ctx.close();
+    }
+} 

+ 425 - 0
src/main/java/com/zhentao/information/handler/WebSocketHandler.java

@@ -0,0 +1,425 @@
+package com.zhentao.information.handler;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONException;
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import com.zhentao.dto.DeeseekRequest;
+import com.zhentao.information.entity.ChatMessage;
+import com.zhentao.information.entity.Message;
+import com.zhentao.information.repository.ChatMessageRepository;
+import com.zhentao.information.service.WebSocketService;
+import io.netty.channel.ChannelHandler;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.SimpleChannelInboundHandler;
+import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.SocketTimeoutException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * WebSocket消息处理器
+ * 处理WebSocket连接、消息接收和发送,集成AI功能
+ */
+@Slf4j
+@Component
+@ChannelHandler.Sharable
+public class WebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
+
+    private static final Gson gson = new Gson();
+    private static final String DEEPSEEK_API_URL = "https://api.deepseek.com/chat/completions";
+    private static final String API_KEY = "";
+    private static final String AI_USER_ID = "";
+
+    @Resource
+    private ChatMessageRepository chatMessageRepository;
+
+    @Resource
+    private WebSocketService webSocketService;
+
+    /**
+     * 处理接收到的WebSocket消息
+     */
+    @Override
+    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
+        String text = msg.text();
+        log.info("收到消息:{}", text);
+
+        // 检查消息是否为空或无效
+        if (text == null || text.trim().isEmpty()) {
+            log.warn("收到空消息或无效消息");
+            sendErrorMessage(ctx, "消息不能为空");
+            return;
+        }
+
+        try {
+            // 尝试解析JSON消息
+            Message message = parseMessage(text);
+            if (message == null) {
+                log.error("消息解析失败,原始消息:{}", text);
+                sendErrorMessage(ctx, "消息格式错误");
+                return;
+            }
+
+            log.info("接收到的消息:{}", message);
+
+            // 如果是连接消息,处理token
+            if ("connect".equals(message.getType())) {
+                String userId = webSocketService.handleUserLogin(message.getContent(), ctx);
+                log.info("用户 {} 登录成功", userId);
+                if (userId != null) {
+                    // 用户登录成功后,自动加入所有群聊
+                    webSocketService.joinAllGroups(userId);
+                    // 发送连接成功消息
+                    Message response = new Message();
+                    response.setType("connect_success");
+                    response.setContent("连接成功");
+                    ctx.channel().writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(response)));
+                }
+                return;
+            }
+
+            // 如果是心跳消息
+            if ("ping".equals(message.getType())) {
+                Message pongMessage = new Message();
+                pongMessage.setType("pong");
+                ctx.channel().writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(pongMessage)));
+                return;
+            }
+
+            // 如果是群聊消息
+            if (message.getGroupId() != null) {
+                handleGroupMessage(message);
+                return;
+            }
+
+            // 检查是否是发送给AI的消息
+            if (AI_USER_ID.equals(message.getToUserId())) {
+                handleAIMessage(message, ctx);
+                return;
+            }
+
+            // 处理普通消息
+            handleMessage(message);
+
+        } catch (JSONException e) {
+            log.error("JSON解析失败,原始消息:{},错误:{}", text, e.getMessage());
+            sendErrorMessage(ctx, "消息格式错误:" + e.getMessage());
+        } catch (Exception e) {
+            log.error("处理消息失败,原始消息:{},错误:{}", text, e.getMessage(), e);
+            // 发送错误消息给客户端
+            Message errorMessage = new Message();
+            errorMessage.setType("error");
+            errorMessage.setContent("消息处理失败:" + e.getMessage());
+            ctx.channel().writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(errorMessage)));
+        }
+    }
+
+    /**
+     * 解析消息,增加错误处理
+     */
+    private Message parseMessage(String text) {
+        try {
+            // 首先验证JSON格式
+            if (!text.trim().startsWith("{")) {
+                log.error("消息不是有效的JSON格式,期望以{开头,实际:{}", text);
+                return null;
+            }
+
+            Message message = JSON.parseObject(text, Message.class);
+
+            // 验证必要字段
+            if (message.getType() == null) {
+                log.warn("消息缺少type字段:{}", text);
+            }
+
+            return message;
+        } catch (JSONException e) {
+            log.error("JSON解析异常:{},原始消息:{}", e.getMessage(), text);
+            return null;
+        } catch (Exception e) {
+            log.error("消息解析异常:{},原始消息:{}", e.getMessage(), text);
+            return null;
+        }
+    }
+
+    /**
+     * 将information包的Message转换为MongoDB包的Message
+     */
+    private com.zhentao.groups.MongoDB.pojo.Message convertToMongoMessage(Message message) {
+        com.zhentao.groups.MongoDB.pojo.Message mongoMessage = new com.zhentao.groups.MongoDB.pojo.Message();
+        mongoMessage.setType(message.getType());
+        mongoMessage.setFromUserId(message.getFromUserId());
+        mongoMessage.setToUserId(message.getToUserId());
+        mongoMessage.setGroupId(message.getGroupId());
+        mongoMessage.setContent(message.getContent());
+        mongoMessage.setTimestamp(message.getTimestamp());
+        mongoMessage.setFileUrl(message.getFileUrl());
+        mongoMessage.setFileName(message.getFileName());
+        mongoMessage.setFileType(message.getFileType());
+        mongoMessage.setFileSize(message.getFileSize());
+        mongoMessage.setAvatar(message.getAvatar());
+        return mongoMessage;
+    }
+
+    /**
+     * 处理AI消息
+     */
+    private void handleAIMessage(Message message, ChannelHandlerContext ctx) {
+        String question = message.getContent();
+        if (question == null || question.trim().isEmpty()) {
+            sendErrorMessage(ctx, "问题不能为空");
+            return;
+        }
+
+        // 发送AI开始处理的消息
+        Message aiStartMessage = new Message();
+        aiStartMessage.setType("ai_start");
+        aiStartMessage.setContent("AI正在思考中...");
+        aiStartMessage.setFromUserId(AI_USER_ID);
+        aiStartMessage.setToUserId(message.getFromUserId());
+        ctx.channel().writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(aiStartMessage)));
+
+        // 使用线程池处理AI请求
+        ExecutorService executor = Executors.newSingleThreadExecutor();
+        executor.submit(() -> processAIStreamingResponse(message, ctx));
+        executor.shutdown();
+    }
+
+    /**
+     * 处理AI流式响应
+     */
+    private void processAIStreamingResponse(Message originalMessage, ChannelHandlerContext ctx) {
+        HttpURLConnection connection = null;
+        try {
+            URL url = new URL(DEEPSEEK_API_URL);
+            connection = (HttpURLConnection) url.openConnection();
+            connection.setRequestMethod("POST");
+            connection.setRequestProperty("Content-Type", "application/json");
+            connection.setRequestProperty("Authorization", "Bearer " + API_KEY);
+            connection.setRequestProperty("Accept", "text/event-stream");
+            connection.setDoOutput(true);
+            connection.setDoInput(true);
+            connection.setUseCaches(false);
+            connection.setConnectTimeout(5000);
+            connection.setReadTimeout(30000);
+
+            // 发送请求体
+            try (OutputStream os = connection.getOutputStream()) {
+                os.write(buildAIRequestBody(originalMessage.getContent()).getBytes(StandardCharsets.UTF_8));
+                os.flush();
+            }
+
+            // 处理流式响应
+            try (BufferedReader reader = new BufferedReader(
+                    new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
+
+                String line;
+                while ((line = reader.readLine()) != null && ctx.channel().isActive()) {
+                    if (line.startsWith("data: ") && !line.equals("data: [DONE]")) {
+                        String content = parseAIContent(line.substring(6));
+                        if (content != null) {
+                            // 发送AI流式响应
+                            Message aiResponse = new Message();
+                            aiResponse.setType("ai_stream");
+                            aiResponse.setContent(content);
+                            aiResponse.setFromUserId(AI_USER_ID);
+                            aiResponse.setToUserId(originalMessage.getFromUserId());
+                            ctx.channel().writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(aiResponse)));
+                        }
+                    }
+                }
+            }
+
+            // 发送AI完成消息
+            Message aiCompleteMessage = new Message();
+            aiCompleteMessage.setType("ai_complete");
+            aiCompleteMessage.setContent("AI回答完成");
+            aiCompleteMessage.setFromUserId(AI_USER_ID);
+            aiCompleteMessage.setToUserId(originalMessage.getFromUserId());
+            ctx.channel().writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(aiCompleteMessage)));
+
+        } catch (SocketTimeoutException e) {
+            sendErrorMessage(ctx, "AI响应超时,请重试");
+        } catch (IOException e) {
+            sendErrorMessage(ctx, "AI网络错误: " + e.getMessage());
+        } finally {
+            if (connection != null) connection.disconnect();
+        }
+    }
+
+    /**
+     * 构建AI请求体
+     */
+    private String buildAIRequestBody(String question) {
+        List<DeeseekRequest.Message> messages = new ArrayList<>();
+        messages.add(DeeseekRequest.Message.builder()
+                .role("system")
+                .content("你是一个佳佳聊天小助手,请用中文回答")
+                .build());
+        messages.add(DeeseekRequest.Message.builder()
+                .role("user")
+                .content(question)
+                .build());
+
+        return gson.toJson(DeeseekRequest.builder()
+                .model("deepseek-chat")
+                .messages(messages)
+                .stream(true)
+                .build());
+    }
+
+    /**
+     * 解析AI响应内容
+     */
+    private String parseAIContent(String json) {
+        try {
+            JsonObject obj = JsonParser.parseString(json).getAsJsonObject();
+            if (obj.has("choices")) {
+                JsonObject delta = obj.getAsJsonArray("choices")
+                        .get(0).getAsJsonObject()
+                        .getAsJsonObject("delta");
+                if (delta.has("content")) {
+                    return delta.get("content").getAsString();
+                }
+            }
+            return null;
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    /**
+     * 发送错误消息
+     */
+    private void sendErrorMessage(ChannelHandlerContext ctx, String message) {
+        try {
+            if (ctx.channel().isActive()) {
+                Message errorMessage = new Message();
+                errorMessage.setType("ai_error");
+                errorMessage.setContent(message);
+                ctx.channel().writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(errorMessage)));
+            }
+        } catch (Exception e) {
+            log.error("发送错误消息失败", e);
+        }
+    }
+
+    /**
+     * 处理群聊消息
+     */
+    private void handleGroupMessage(Message message) {
+        // 设置群聊消息类型
+        message.setType(message.getType() != null ? message.getType() : "group_chat");
+        // 转换为MongoDB Message类型
+        com.zhentao.groups.MongoDB.pojo.Message mongoMessage = convertToMongoMessage(message);
+        // 广播消息给群内所有成员
+        boolean sent = webSocketService.handleGroupMessage(mongoMessage);
+        if (sent) {
+            // 发送消息确认
+            Message ackMessage = new Message();
+            ackMessage.setType("message_ack");
+            ackMessage.setContent("群消息已发送");
+            webSocketService.sendMessageToUser(message.getFromUserId(), convertToMongoMessage(ackMessage));
+        }
+    }
+
+    /**
+     * 处理普通消息
+     */
+    private void handleMessage(Message message) {
+        // 生成聊天ID
+        String chatId = generateChatId(message.getFromUserId(), message.getToUserId());
+        ChatMessage chatMessage = new ChatMessage();
+        // 新增文件/图片/视频元数据保存
+        if (message.getFileUrl() != null) {
+            chatMessage.setFileUrl(message.getFileUrl());
+            chatMessage.setFileName(message.getFileName());
+            chatMessage.setFileType(message.getFileType());
+            chatMessage.setFileSize(message.getFileSize());
+        }
+        // 新增:保存发送者头像
+        chatMessage.setAvatar(message.getAvatar());
+        // 创建MongoDB消息对象
+        chatMessage.setFromUserId(message.getFromUserId());
+        chatMessage.setToUserId(message.getToUserId());
+        chatMessage.setContent(message.getContent());
+        chatMessage.setType(message.getType());
+        chatMessage.setTimestamp(System.currentTimeMillis());
+        chatMessage.setIsRead(false);
+        chatMessage.setChatId(chatId);
+
+        // 保存消息到MongoDB
+        chatMessageRepository.save(chatMessage);
+
+        // 转换为MongoDB Message类型并发送消息给接收者
+        com.zhentao.groups.MongoDB.pojo.Message mongoMessage = convertToMongoMessage(message);
+        boolean sent = webSocketService.sendMessageToUser(message.getToUserId(), mongoMessage);
+        if (sent) {
+            log.info("消息已发送给用户: {}, 内容: {}", message.getToUserId(), message.getContent());
+            // 发送消息确认给发送者
+            Message ackMessage = new Message();
+            ackMessage.setType("message_ack");
+            ackMessage.setContent("消息已发送");
+            webSocketService.sendMessageToUser(message.getFromUserId(), convertToMongoMessage(ackMessage));
+        } else {
+            log.info("用户 {} 不在线,消息已保存到MongoDB", message.getToUserId());
+            // 发送消息未送达通知给发送者
+            Message offlineMessage = new Message();
+            offlineMessage.setType("message_offline");
+            offlineMessage.setContent("对方不在线,消息已保存");
+            webSocketService.sendMessageToUser(message.getFromUserId(), convertToMongoMessage(offlineMessage));
+        }
+    }
+
+    /**
+     * 当新的WebSocket连接建立时调用
+     */
+    @Override
+    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
+        log.info("新的连接:{}", ctx.channel().id().asLongText());
+    }
+
+    /**
+     * 当WebSocket连接断开时调用
+     */
+    @Override
+    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
+        log.info("连接断开:{}", ctx.channel().id().asLongText());
+        // 清理用户连接
+        webSocketService.removeUserConnection(ctx);
+    }
+
+    /**
+     * 处理异常情况
+     */
+    @Override
+    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
+        log.error("WebSocket异常", cause);
+        ctx.close();
+    }
+
+    /**
+     * 生成聊天ID
+     */
+    private String generateChatId(String userId1, String userId2) {
+        // 确保两个用户之间的聊天ID唯一
+        return userId1.compareTo(userId2) < 0 ?
+            userId1 + "_" + userId2 :
+            userId2 + "_" + userId1;
+    }
+}

+ 55 - 0
src/main/java/com/zhentao/information/netty/NettyServer.java

@@ -0,0 +1,55 @@
+package com.zhentao.information.netty;
+
+import com.zhentao.information.handler.WebSocketHandler;
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.socket.SocketChannel;
+import io.netty.handler.codec.http.HttpObjectAggregator;
+import io.netty.handler.codec.http.HttpServerCodec;
+import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
+import io.netty.handler.stream.ChunkedWriteHandler;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.Resource;
+
+/**
+ * Netty服务器启动类
+ * 启动动一个基于 Netty 的 WebSocket 服务器
+ * 启动 WebSocket 服务器:
+ * 它会配置一个 Netty 的 ServerBootstrap,并绑定到指定的端口(默认是 8888)。
+ * 它会初始化一个 Netty 的通道管道(ChannelPipeline),并添加各种处理器来处理 WebSocket 连接和消息。
+ */
+@Slf4j
+@Component
+public class NettyServer {
+
+    @Value("${netty.port:8766}")
+    private int port;
+
+    @Resource
+    private ServerBootstrap serverBootstrap;
+
+    @Resource
+    private WebSocketHandler webSocketHandler;
+
+    @PostConstruct
+    public void start() throws Exception {
+        serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
+            @Override
+            protected void initChannel(SocketChannel ch) {
+                ch.pipeline()
+                        .addLast(new HttpServerCodec())
+                        .addLast(new ChunkedWriteHandler())
+                        .addLast(new HttpObjectAggregator(65536))
+                        .addLast(new WebSocketServerProtocolHandler("/ws"))
+                        .addLast(webSocketHandler);
+            }
+        });
+
+        serverBootstrap.bind(port).sync();
+        log.info("Netty服务器启动成功,端口:{}", port);
+    }
+}

+ 29 - 0
src/main/java/com/zhentao/information/repository/ChatMessageRepository.java

@@ -0,0 +1,29 @@
+package com.zhentao.information.repository;
+
+import com.zhentao.information.entity.ChatMessage;
+import org.springframework.data.mongodb.repository.MongoRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+/**
+ * 聊天消息仓库
+ */
+@Repository
+public interface ChatMessageRepository extends MongoRepository<ChatMessage, String> {
+
+    /**
+     * 查询两个用户之间的聊天记录
+     */
+    List<ChatMessage> findByChatId(String chatId);
+
+    /**
+     * 查询用户的所有未读消息
+     */
+    List<ChatMessage> findByToUserIdAndIsReadFalse(String toUserId);
+
+    /**
+     * 查询toUserId为某个群ID的所有消息
+     */
+    List<ChatMessage> findByToUserId(String toUserId);
+}

+ 57 - 0
src/main/java/com/zhentao/information/service/GroupInitService.java

@@ -0,0 +1,57 @@
+package com.zhentao.information.service;
+
+import com.zhentao.groups.dto.GroupDto;
+import com.zhentao.groups.service.GroupsService;
+import com.zhentao.information.cache.GroupMemberCache;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.Resource;
+import java.util.List;
+
+/**
+ * 群聊初始化服务
+ * 用于初始化群成员缓存
+ */
+@Slf4j
+@Service
+public class GroupInitService {
+
+    @Resource
+    private GroupsService groupsService;
+
+    @Resource
+    private GroupMemberCache groupMemberCache;
+
+    /**
+     * 在应用启动时初始化群成员缓存
+     */
+    @PostConstruct
+    public void init() {
+        try {
+            List<GroupDto> groupList = groupsService.getList();
+            groupList.forEach(group -> {
+                groupMemberCache.updateGroupMembers(group.getGroupId(), group.getUid());
+            });
+            log.info("群成员缓存初始化完成,共 {} 个群", groupList.size());
+        } catch (Exception e) {
+            log.error("群成员缓存初始化失败", e);
+        }
+    }
+
+    /**
+     * 刷新群成员缓存
+     */
+    public void refreshGroupMemberCache() {
+        try {
+            List<GroupDto> groupList = groupsService.getList();
+            groupList.forEach(group -> {
+                groupMemberCache.updateGroupMembers(group.getGroupId(), group.getUid());
+            });
+            log.info("群成员缓存刷新完成,共 {} 个群", groupList.size());
+        } catch (Exception e) {
+            log.error("群成员缓存刷新失败", e);
+        }
+    }
+} 

+ 347 - 0
src/main/java/com/zhentao/information/service/WebSocketService.java

@@ -0,0 +1,347 @@
+package com.zhentao.information.service;
+
+import com.alibaba.fastjson.JSON;
+import com.zhentao.common.utils.TokenUtils;
+import com.zhentao.groups.MongoDB.pojo.Message;
+import com.zhentao.groups.dto.GroupDto;
+import com.zhentao.groups.service.GroupsService;
+import com.zhentao.information.cache.ChannelCache;
+import com.zhentao.information.cache.GroupChannelCache;
+import com.zhentao.information.cache.GroupMemberCache;
+import com.zhentao.information.entity.ChatMessage;
+import com.zhentao.information.repository.ChatMessageRepository;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.group.ChannelGroup;
+import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * WebSocket服务类
+ * 处理WebSocket连接、消息发送等业务逻辑
+ */
+@Slf4j
+@Service
+public class WebSocketService {
+
+    @Resource
+    private ChannelCache channelCache;
+
+    @Resource
+    private GroupChannelCache groupChannelCache;
+
+    @Resource
+    private GroupMemberCache groupMemberCache;
+
+    @Autowired
+    @Lazy
+    private GroupsService groupsService;
+
+    @Autowired
+    private ChatMessageRepository chatMessageRepository;
+
+    // 存储用户token的Map
+    private final Map<String, String> userTokenMap = new ConcurrentHashMap<>();
+    private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
+
+    /**
+     * 存储用户token
+     * @param userId 用户ID
+     * @param token 用户token
+     */
+    public void storeUserToken(String userId, String token) {
+        userTokenMap.put(userId, token);
+        log.info("用户 {} 的token已存储", userId);
+    }
+
+    /**
+     * 获取用户token
+     * @param userId 用户ID
+     * @return 用户token
+     */
+    public String getUserToken(String userId) {
+        return userTokenMap.get(userId);
+    }
+
+    /**
+     * 处理用户登录
+     * @param token 用户token
+     * @param ctx Channel上下文
+     * @return 用户ID,如果登录失败返回null
+     */
+    public String handleUserLogin(String token, ChannelHandlerContext ctx) {
+        String userId = TokenUtils.getUserIdFromToken(token);
+        if (userId != null) {
+            // 验证token是否与存储的token匹配
+            String storedToken = userTokenMap.get(userId);
+            if (storedToken != null && storedToken.equals(token)) {
+                // 将用户ID和Channel绑定
+                channelCache.addCache(userId, ctx);
+                log.info("用户 {} 连接成功", userId);
+
+                // 发送连接成功消息
+                Message response = new Message();
+                response.setType("connect_success");
+                response.setContent("连接成功");
+                ctx.channel().writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(response)));
+                return userId;
+            } else {
+                log.error("用户 {} 的token验证失败", userId);
+                ctx.close();
+            }
+        } else {
+            log.error("无效的token");
+            ctx.close();
+        }
+        return null;
+    }
+
+    /**
+     * 发送消息给指定用户
+     * @param userId 接收者用户ID
+     * @param message 消息内容
+     * @return 是否发送成功
+     */
+    public boolean sendMessageToUser(String userId, Message message) {
+        if (message.getType() == null) {
+            message.setType("text");
+        }
+
+        ChannelHandlerContext ctx = channelCache.getCache(userId);
+        if (ctx != null && ctx.channel().isActive()) {
+            try {
+                String messageJson = JSON.toJSONString(message);
+                log.info("发送消息给用户 {}: {}", userId, messageJson);
+                ctx.channel().writeAndFlush(new TextWebSocketFrame(messageJson));
+                return true;
+            } catch (Exception e) {
+                log.error("发送消息给用户 {} 失败", userId, e);
+                retrySendMessage(userId, message);
+                return false;
+            }
+        } else {
+            log.info("用户 {} 不在线,消息将保存到数据库", userId);
+            // 离线时也保存文件/图片/视频消息到MongoDB
+            if (message.getFileUrl() != null) {
+                ChatMessage chatMessage = new ChatMessage();
+                chatMessage.setFromUserId(message.getFromUserId());
+                chatMessage.setToUserId(userId);
+                chatMessage.setContent(message.getContent());
+                chatMessage.setType(message.getType());
+                chatMessage.setTimestamp(System.currentTimeMillis());
+                chatMessage.setIsRead(false);
+                chatMessage.setChatId(generateChatId(message.getFromUserId(), userId));
+                chatMessage.setFileUrl(message.getFileUrl());
+                chatMessage.setFileName(message.getFileName());
+                chatMessage.setFileType(message.getFileType());
+                chatMessage.setFileSize(message.getFileSize());
+                chatMessage.setAvatar(message.getAvatar());
+//                chatMessageRepository.save(chatMessage);
+            }
+            return false;
+        }
+    }
+
+    private void retrySendMessage(String userId, Message message) {
+        scheduler.schedule(() -> {
+            ChannelHandlerContext ctx = channelCache.getCache(userId);
+            if (ctx != null && ctx.channel().isActive()) {
+                try {
+                    String messageJson = JSON.toJSONString(message);
+                    log.info("重试发送消息给用户 {}: {}", userId, messageJson);
+                    ctx.channel().writeAndFlush(new TextWebSocketFrame(messageJson));
+                } catch (Exception e) {
+                    log.error("重试发送消息给用户 {} 失败", userId, e);
+                }
+            }
+        }, 1, TimeUnit.SECONDS);
+    }
+
+    /**
+     * 广播消息给所有在线用户
+     * @param message 消息内容
+     */
+    public void broadcastMessage(Message message) {
+        channelCache.getAllCache().forEach((userId, ctx) -> {
+            if (ctx.channel().isActive()) {
+                try {
+                    String messageJson = JSON.toJSONString(message);
+                    log.info("广播消息给用户 {}: {}", userId, messageJson);
+                    ctx.channel().writeAndFlush(new TextWebSocketFrame(messageJson));
+                } catch (Exception e) {
+                    log.error("广播消息给用户 {} 失败", userId, e);
+                }
+            }
+        });
+    }
+
+    /**
+     * 检查用户是否在线
+     * @param userId 用户ID
+     * @return 是否在线
+     */
+    public boolean isUserOnline(String userId) {
+        ChannelHandlerContext ctx = channelCache.getCache(userId);
+        return ctx != null && ctx.channel().isActive();
+    }
+
+    /**
+     * 处理群聊消息
+     * @param message 群聊消息
+     * @return 是否发送成功
+     */
+    public boolean handleGroupMessage(Message message) {
+        Long groupId = message.getGroupId();
+        if (groupId == null) {
+            log.error("群聊消息缺少群ID");
+            return false;
+        }
+
+        // 存储群聊消息到MongoDB
+        ChatMessage chatMessage = new ChatMessage();
+        chatMessage.setFromUserId(message.getFromUserId());
+        chatMessage.setToUserId(String.valueOf(groupId));
+        chatMessage.setContent(message.getContent());
+        chatMessage.setType(message.getType() != null ? message.getType() : "group_chat");
+        chatMessage.setTimestamp(System.currentTimeMillis());
+        chatMessage.setIsRead(false);
+        chatMessage.setChatId("group_" + groupId);
+        // 新增文件/图片/视频元数据
+        if (message.getFileUrl() != null) {
+            chatMessage.setFileUrl(message.getFileUrl());
+            chatMessage.setFileName(message.getFileName());
+            chatMessage.setFileType(message.getFileType());
+            chatMessage.setFileSize(message.getFileSize());
+        }
+        chatMessage.setAvatar(message.getAvatar());
+        chatMessageRepository.save(chatMessage);
+
+        // 获取群成员
+        List<GroupDto> groupList = groupsService.getList();
+        List<Long> groupMembers = null;
+        for (GroupDto group : groupList) {
+            if (group.getGroupId().equals(groupId)) {
+                groupMembers = group.getUid();
+                break;
+            }
+        }
+
+        if (groupMembers == null || groupMembers.isEmpty()) {
+            log.error("群 {} 不存在或没有成员", groupId);
+            return false;
+        }
+
+        boolean allSent = true;
+        for (Long memberId : groupMembers) {
+            String memberIdStr = String.valueOf(memberId);
+            if (!memberIdStr.equals(message.getFromUserId())) {
+                ChannelHandlerContext ctx = channelCache.getCache(memberIdStr);
+                if (ctx != null && ctx.channel().isActive()) {
+                    try {
+                        String messageJson = JSON.toJSONString(message);
+                        log.info("发送群消息给用户 {}: {}", memberIdStr, messageJson);
+                        ctx.channel().writeAndFlush(new TextWebSocketFrame(messageJson));
+                    } catch (Exception e) {
+                        log.error("发送群消息给用户 {} 失败", memberId, e);
+                        allSent = false;
+                    }
+                }
+            }
+        }
+        log.info("群 {} 的消息已广播,群成员数:{}", groupId, groupMembers.size());
+        return allSent;
+    }
+
+    /**
+     * 用户登录时,将其加入所有群聊的ChannelGroup
+     * @param userId 用户ID
+     */
+    public void joinAllGroups(String userId) {
+        Long userIdLong = Long.valueOf(userId);
+        Map<Long, List<Long>> allGroups = groupMemberCache.getAllGroupMembers();
+
+        allGroups.forEach((groupId, members) -> {
+            if (members.contains(userIdLong)) {
+                addUserToGroup(groupId, userId);
+            }
+        });
+
+        log.info("用户 {} 已加入所有群聊的ChannelGroup", userId);
+    }
+
+    /**
+     * 将用户添加到群聊ChannelGroup
+     * @param groupId 群ID
+     * @param userId 用户ID
+     * @return 是否添加成功
+     */
+    public boolean addUserToGroup(Long groupId, String userId) {
+        // 验证用户是否在群中
+        if (!groupMemberCache.isUserInGroup(groupId, Long.valueOf(userId))) {
+            log.error("用户 {} 不在群 {} 中", userId, groupId);
+            return false;
+        }
+
+        ChannelHandlerContext ctx = channelCache.getCache(userId);
+        if (ctx == null || !ctx.channel().isActive()) {
+            log.error("用户 {} 不在线", userId);
+            return false;
+        }
+
+        ChannelGroup channelGroup = groupChannelCache.getGroup(groupId);
+        if (channelGroup == null) {
+            channelGroup = groupChannelCache.addGroup(groupId);
+        }
+
+        channelGroup.add(ctx.channel());
+        log.info("用户 {} 已添加到群 {} 的ChannelGroup", userId, groupId);
+        return true;
+    }
+
+    /**
+     * 将用户从群聊ChannelGroup中移除
+     * @param groupId 群ID
+     * @param userId 用户ID
+     * @return 是否移除成功
+     */
+    public boolean removeUserFromGroup(Long groupId, String userId) {
+        ChannelHandlerContext ctx = channelCache.getCache(userId);
+        if (ctx == null || !ctx.channel().isActive()) {
+            return false;
+        }
+
+        ChannelGroup channelGroup = groupChannelCache.getGroup(groupId);
+        if (channelGroup != null) {
+            channelGroup.remove(ctx.channel());
+            log.info("用户 {} 已从群 {} 的ChannelGroup移除", userId, groupId);
+            return true;
+        }
+        return false;
+    }
+
+    public void removeUserConnection(ChannelHandlerContext ctx) {
+        // 从ChannelCache中移除用户连接
+        channelCache.removeCache(ctx);
+        // 从所有群组中移除用户
+        groupChannelCache.getAllGroups().forEach((groupId, channelGroup) -> {
+            channelGroup.remove(ctx.channel());
+        });
+    }
+
+    // 新增:生成聊天ID
+    private String generateChatId(String userId1, String userId2) {
+        return userId1.compareTo(userId2) < 0 ?
+            userId1 + "_" + userId2 :
+            userId2 + "_" + userId1;
+    }
+}

+ 46 - 0
src/main/java/com/zhentao/user/controller/UserController.java

@@ -0,0 +1,46 @@
+package com.zhentao.user.controller;
+
+import com.zhentao.common.config.NullLogin;
+import com.zhentao.common.utils.TokenUtils;
+import com.zhentao.common.vo.Result;
+import com.zhentao.user.dto.NoteDto;
+import com.zhentao.user.dto.UserDto;
+import com.zhentao.user.service.UserMoneyService;
+import com.zhentao.user.service.UserOnlineStatusService;
+import com.zhentao.user.service.UserService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletRequest;
+
+@RestController
+@RequestMapping("user")
+public class UserController {
+    @Autowired
+    private UserService userService;
+    @Resource
+    private UserOnlineStatusService userOnlineStatusService;
+
+    @PostMapping("register")
+    public Result register(@RequestBody UserDto userDto){
+        return userService.register(userDto);
+    }
+    @PostMapping("login")
+    public Result login(@RequestBody UserDto userDto){return userService.login(userDto);}
+    @RequestMapping("/logout")
+    public void logout(@RequestHeader("token") String token){
+        String userId = TokenUtils.getUserIdFromToken(token);
+        userOnlineStatusService.userGoOnline(Long.valueOf(userId));
+    }
+    @PostMapping("/code")
+    @NullLogin
+    public Result code(@RequestBody NoteDto noteDto) {
+        return userService.note(noteDto);
+    }
+    @PostMapping("/logincode")
+    @NullLogin
+    public Result logincode(@RequestBody NoteDto noteDto) {
+        return userService.loginnote(noteDto);
+    }
+}

+ 34 - 0
src/main/java/com/zhentao/user/controller/UserMoneyController.java

@@ -0,0 +1,34 @@
+package com.zhentao.user.controller;
+
+import com.zhentao.common.config.NullLogin;
+import com.zhentao.common.vo.Result;
+import com.zhentao.dto.TrueUser;
+import com.zhentao.user.service.UserMoneyService;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+
+@RestController
+@RequestMapping("/user/money")
+public class UserMoneyController {
+    @Resource
+    private UserMoneyService userMoneyService;
+    @RequestMapping("/Mine")
+    @NullLogin
+    public Result getMine(@RequestHeader("token") String token) {
+        return userMoneyService.getMine(token);
+    }
+    @RequestMapping("/getTrue")
+    @NullLogin
+    public Result getTrue(@RequestBody TrueUser trueUser) {
+        return userMoneyService.getTrue(trueUser);
+    }
+    @RequestMapping("/toVolunteer")
+    @NullLogin
+    public Result toVolunteer(@RequestHeader("token") String token) {
+        return userMoneyService.toVolunteer(token);
+    }
+}

+ 124 - 0
src/main/java/com/zhentao/user/domain/User.java

@@ -0,0 +1,124 @@
+package com.zhentao.user.domain;
+
+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 java.io.Serializable;
+import java.util.Date;
+import lombok.Data;
+
+/**
+ * 用户表
+ * @TableName user
+ */
+@TableName(value ="user")
+@Data
+public class User implements Serializable {
+    /**
+     * 用户ID
+     */
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 用户名
+     */
+    private String username;
+
+    /**
+     * 密码(BCrypt加密)
+     */
+    private String password;
+
+    /**
+     * 真实姓名
+     */
+    private String realName;
+
+    /**
+     * 手机号
+     */
+    private String phone;
+
+    /**
+     * 邮箱
+     */
+    private String email;
+
+    /**
+     * 头像URL
+     */
+    private String avatar;
+
+    /**
+     * 性别(0-未知,1-男,2-女)
+     */
+    private Integer gender;
+    private String address;
+    private String volunteerNumber;
+    private String userType;
+
+    /**
+     * 出生日期
+     */
+    private Date birthDate;
+
+    /**
+     * 状态(0-禁用,1-正常)
+     */
+    private Integer status;
+
+    /**
+     * 最后登录时间
+     */
+    private Date lastLoginTime;
+
+    /**
+     * 最后登录IP
+     */
+    private String lastLoginIp;
+
+    /**
+     * 最后登录设备
+     */
+    private String lastLoginDevice;
+
+    /**
+     * 人脸数据ID(关联人脸特征库)
+     */
+    private String faceDataId;
+
+    /**
+     * 人脸图片URL
+     */
+    private String faceImageUrl;
+
+    /**
+     * 是否启用人脸登录(0-禁用,1-启用)
+     */
+    private Integer faceLoginEnabled;
+
+    /**
+     * 创建时间
+     */
+    private Date createdAt;
+
+    /**
+     * 更新时间
+     */
+    private Date updatedAt;
+
+    /**
+     * 是否删除(0-未删除,1-已删除)
+     */
+    private Integer deleted;
+
+    /**
+     * 盐
+     */
+    private String salt;
+
+    @TableField(exist = false)
+    private static final long serialVersionUID = 1L;
+}

+ 76 - 0
src/main/java/com/zhentao/user/domain/UserMoney.java

@@ -0,0 +1,76 @@
+package com.zhentao.user.domain;
+
+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 java.io.Serializable;
+import java.util.Date;
+import lombok.Data;
+
+/**
+ * 
+ * @TableName user_money
+ */
+@TableName(value ="user_money")
+@Data
+public class UserMoney implements Serializable {
+    /**
+     * id
+     */
+    @TableId
+    private Long id;
+
+    /**
+     * 关联用户的id
+     */
+    private Long userId;
+
+    /**
+     * 用户姓名
+     */
+    private String username;
+
+    /**
+     * 用户手机号
+     */
+    private String phone;
+
+    /**
+     * 关联活动的ID
+     */
+    private Long eventId;
+
+    /**
+     * 积分
+     */
+    private Double jifen;
+
+    /**
+     * 钱包
+     */
+    private Double money;
+
+    /**
+     * 活动的名称
+     */
+    private String eventName;
+
+    /**
+     * 变动后的积分
+     */
+    private Double jifenChange;
+
+    /**
+     * 积分类型(1物资捐赠   2 特殊贡献  3 其他)
+     */
+    private Integer type;
+
+    /**
+     * 创建的时间
+     */
+    private Date createTime;
+
+    @TableField(exist = false)
+    private static final long serialVersionUID = 1L;
+}

+ 101 - 0
src/main/java/com/zhentao/user/domain/UserOnlineStatus.java

@@ -0,0 +1,101 @@
+package com.zhentao.user.domain;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 用户在线状态表
+ * @TableName user_online_status
+ */
+@TableName(value ="user_online_status")
+@Data
+public class UserOnlineStatus implements Serializable {
+    /**
+     * 用户ID
+     */
+    @TableId
+    private Long userId;
+
+    /**
+     * 是否在线(0:离线,1:在线)
+     */
+    private Integer isOnline;
+
+    /**
+     * 上线时间
+     */
+    private Date onlineTime;
+
+    /**
+     * 最后心跳时间
+     */
+    private Date lastHeartbeatTime;
+
+    /**
+     * 设备信息
+     */
+    private String deviceInfo;
+
+    /**
+     * 更新时间
+     */
+    private Date updateTime;
+
+    @TableField(exist = false)
+    private static final long serialVersionUID = 1L;
+
+    @Override
+    public boolean equals(Object that) {
+        if (this == that) {
+            return true;
+        }
+        if (that == null) {
+            return false;
+        }
+        if (getClass() != that.getClass()) {
+            return false;
+        }
+        UserOnlineStatus other = (UserOnlineStatus) that;
+        return (this.getUserId() == null ? other.getUserId() == null : this.getUserId().equals(other.getUserId()))
+            && (this.getIsOnline() == null ? other.getIsOnline() == null : this.getIsOnline().equals(other.getIsOnline()))
+            && (this.getOnlineTime() == null ? other.getOnlineTime() == null : this.getOnlineTime().equals(other.getOnlineTime()))
+            && (this.getLastHeartbeatTime() == null ? other.getLastHeartbeatTime() == null : this.getLastHeartbeatTime().equals(other.getLastHeartbeatTime()))
+            && (this.getDeviceInfo() == null ? other.getDeviceInfo() == null : this.getDeviceInfo().equals(other.getDeviceInfo()))
+            && (this.getUpdateTime() == null ? other.getUpdateTime() == null : this.getUpdateTime().equals(other.getUpdateTime()));
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((getUserId() == null) ? 0 : getUserId().hashCode());
+        result = prime * result + ((getIsOnline() == null) ? 0 : getIsOnline().hashCode());
+        result = prime * result + ((getOnlineTime() == null) ? 0 : getOnlineTime().hashCode());
+        result = prime * result + ((getLastHeartbeatTime() == null) ? 0 : getLastHeartbeatTime().hashCode());
+        result = prime * result + ((getDeviceInfo() == null) ? 0 : getDeviceInfo().hashCode());
+        result = prime * result + ((getUpdateTime() == null) ? 0 : getUpdateTime().hashCode());
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append(getClass().getSimpleName());
+        sb.append(" [");
+        sb.append("Hash = ").append(hashCode());
+        sb.append(", userId=").append(userId);
+        sb.append(", isOnline=").append(isOnline);
+        sb.append(", onlineTime=").append(onlineTime);
+        sb.append(", lastHeartbeatTime=").append(lastHeartbeatTime);
+        sb.append(", deviceInfo=").append(deviceInfo);
+        sb.append(", updateTime=").append(updateTime);
+        sb.append(", serialVersionUID=").append(serialVersionUID);
+        sb.append("]");
+        return sb.toString();
+    }
+}

+ 19 - 0
src/main/java/com/zhentao/user/dto/NoteDto.java

@@ -0,0 +1,19 @@
+package com.zhentao.user.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.Pattern;
+
+@Data
+public class NoteDto {
+
+    @NotBlank(message = "手机号不能为空")
+    @Pattern(regexp = "^1(3\\d|4[5-9]|5[0-35-9]|6[2567]|7[0-8]|8\\d|9[0-35-9])\\d{8}$", message = "手机号格式不正确")
+    private String phone;
+    private String code;
+
+
+
+
+}

+ 11 - 0
src/main/java/com/zhentao/user/dto/UserDto.java

@@ -0,0 +1,11 @@
+package com.zhentao.user.dto;
+
+import lombok.Data;
+
+@Data
+public class UserDto {
+    private String username;
+    private String password;
+    private String phone;
+    private String code;
+}

+ 18 - 0
src/main/java/com/zhentao/user/mapper/UserMapper.java

@@ -0,0 +1,18 @@
+package com.zhentao.user.mapper;
+
+import com.zhentao.user.domain.User;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+
+/**
+* @author 86183
+* @description 针对表【user(用户表)】的数据库操作Mapper
+* @createDate 2025-07-01 09:52:29
+* @Entity com.zhentao.user.domain.User
+*/
+public interface UserMapper extends BaseMapper<User> {
+
+}
+
+
+
+

+ 18 - 0
src/main/java/com/zhentao/user/mapper/UserMoneyMapper.java

@@ -0,0 +1,18 @@
+package com.zhentao.user.mapper;
+
+import com.zhentao.user.domain.UserMoney;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+
+/**
+* @author 86183
+* @description 针对表【user_money】的数据库操作Mapper
+* @createDate 2025-07-01 10:24:56
+* @Entity com.zhentao.user.domain.UserMoney
+*/
+public interface UserMoneyMapper extends BaseMapper<UserMoney> {
+
+}
+
+
+
+

+ 18 - 0
src/main/java/com/zhentao/user/mapper/UserOnlineStatusMapper.java

@@ -0,0 +1,18 @@
+package com.zhentao.user.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zhentao.user.domain.UserOnlineStatus;
+
+/**
+* @author 31810
+* @description 针对表【user_online_status(用户在线状态表)】的数据库操作Mapper
+* @createDate 2025-06-18 15:27:28
+* @Entity com.zhentao.user.domain.UserOnlineStatus
+*/
+public interface UserOnlineStatusMapper extends BaseMapper<UserOnlineStatus> {
+
+}
+
+
+
+

+ 20 - 0
src/main/java/com/zhentao/user/service/UserMoneyService.java

@@ -0,0 +1,20 @@
+package com.zhentao.user.service;
+
+import com.zhentao.common.vo.Result;
+import com.zhentao.dto.TrueUser;
+import com.zhentao.user.domain.UserMoney;
+import com.baomidou.mybatisplus.extension.service.IService;
+
+/**
+* @author 86183
+* @description 针对表【user_money】的数据库操作Service
+* @createDate 2025-07-01 10:24:56
+*/
+public interface UserMoneyService extends IService<UserMoney> {
+
+    Result getMine(String token);
+
+    Result getTrue(TrueUser trueUser);
+
+    Result toVolunteer(String token);
+}

+ 20 - 0
src/main/java/com/zhentao/user/service/UserOnlineStatusService.java

@@ -0,0 +1,20 @@
+package com.zhentao.user.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.zhentao.user.domain.UserOnlineStatus;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+* @author 31810
+* @description 针对表【user_online_status(用户在线状态表)】的数据库操作Service
+* @createDate 2025-06-18 15:27:28
+*/
+public interface UserOnlineStatusService extends IService<UserOnlineStatus> {
+
+    void userGoOnline(Long id);
+
+    void handleHeartbeat(HttpServletRequest request);
+
+    void userGoOffline(Long id);
+}

+ 20 - 0
src/main/java/com/zhentao/user/service/UserService.java

@@ -0,0 +1,20 @@
+package com.zhentao.user.service;
+
+import com.zhentao.common.vo.Result;
+import com.zhentao.user.domain.User;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.zhentao.user.dto.NoteDto;
+import com.zhentao.user.dto.UserDto;
+
+/**
+* @author 86183
+* @description 针对表【user(用户表)】的数据库操作Service
+* @createDate 2025-07-01 09:52:29
+*/
+public interface UserService extends IService<User> {
+    Result register(UserDto userDto);
+    Result login(UserDto userDto);
+    Result note(NoteDto noteDto);
+    Result loginnote(NoteDto noteDto);
+
+}

+ 94 - 0
src/main/java/com/zhentao/user/service/impl/UserMoneyServiceImpl.java

@@ -0,0 +1,94 @@
+package com.zhentao.user.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.zhentao.common.utils.TokenUtils;
+import com.zhentao.common.vo.Result;
+import com.zhentao.dto.TrueUser;
+import com.zhentao.user.domain.User;
+import com.zhentao.user.domain.UserMoney;
+import com.zhentao.user.mapper.UserMapper;
+import com.zhentao.user.service.UserMoneyService;
+import com.zhentao.user.mapper.UserMoneyMapper;
+import com.zhentao.utils.DateUtils;
+import com.zhentao.utils.IdVerificationUtil;
+import com.zhentao.utils.SnowflakeIdGenerator;
+import com.zhentao.utils.VolunteerNumberGenerator;
+import com.zhentao.vo.UserVo;
+import lombok.SneakyThrows;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import java.util.Date;
+import java.util.Map;
+
+/**
+* @author 86183
+* @description 针对表【user_money】的数据库操作Service实现
+* @createDate 2025-07-01 10:24:56
+*/
+@Service
+public class UserMoneyServiceImpl extends ServiceImpl<UserMoneyMapper, UserMoney>
+    implements UserMoneyService{
+    @Autowired
+    private UserMoneyMapper userMoneyMapper;
+    @Autowired
+    private UserMapper userMapper;
+    @Value("${aliyun.trueName.appCode}")
+    private String appCode;
+    @Override
+    public Result getMine(String token) {
+        String userId = TokenUtils.getUserIdFromToken(token);
+        User user = userMapper.selectById(userId);
+        QueryWrapper<UserMoney>userMoneyQueryWrapper=new QueryWrapper<>();
+        userMoneyQueryWrapper.eq("user_id",userId);
+        UserMoney userMoney = userMoneyMapper.selectOne(userMoneyQueryWrapper);
+        return Result.OK(new UserVo(user,userMoney), "用户信息");
+    }
+
+    @SneakyThrows
+    @Override
+    public Result getTrue(TrueUser trueUser) {
+        QueryWrapper<User>queryWrapper=new QueryWrapper<>();
+        queryWrapper.eq("id",trueUser.getUserId());
+        User user = userMapper.selectOne(queryWrapper);
+        if(user==null){
+            return Result.ERR(null, "用户不存在");
+        }
+        if(user.getRealName()!=null){
+            return Result.ERR(null, "用户已实名");
+        }
+        IdVerificationUtil idVerificationUtil = new IdVerificationUtil(appCode);
+        IdVerificationUtil.VerificationResult result = idVerificationUtil.verify(trueUser.getIdCard(), trueUser.getTrueName());
+        if(result.isSuccess()){
+            IdVerificationUtil.VerificationData data = result.getData();
+            user.setRealName(trueUser.getTrueName());
+            if(data.getSex().equals("男")){
+                user.setGender(1);
+            }else {
+                user.setGender(2);
+            }
+            String birthday = data.getBirthday();
+            Date date = DateUtils.parseBirthday(birthday);
+            user.setBirthDate(date);
+            user.setAddress(data.getAddress());
+            userMapper.updateById(user);
+            return Result.OK(null, "实名成功");
+        }
+        System.err.println(result.getErrorMessage());
+        return Result.ERR(null, result.getErrorMessage());
+    }
+
+    @Override
+    public Result toVolunteer(String token) {
+        String userId = TokenUtils.getUserIdFromToken(token);
+        User user = userMapper.selectById(userId);
+        if(user.getRealName()==null){
+            return Result.ERR(null, "用户未实名");
+        }
+        user.setUserType("1");
+        user.setVolunteerNumber(VolunteerNumberGenerator.getInstance().generateNumber(user.getAddress()));
+        return Result.OK(null, "已成为志愿者");
+    }
+}

+ 81 - 0
src/main/java/com/zhentao/user/service/impl/UserOnlineStatusServiceImpl.java

@@ -0,0 +1,81 @@
+package com.zhentao.user.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.zhentao.common.utils.TokenUtils;
+import com.zhentao.user.domain.UserOnlineStatus;
+import com.zhentao.user.mapper.UserOnlineStatusMapper;
+import com.zhentao.user.service.UserOnlineStatusService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Service;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.Date;
+import java.util.concurrent.TimeUnit;
+
+/**
+* @author 31810
+* @description 针对表【user_online_status(用户在线状态表)】的数据库操作Service实现
+* @createDate 2025-06-18 15:27:28
+*/
+@Service
+public class UserOnlineStatusServiceImpl extends ServiceImpl<UserOnlineStatusMapper, UserOnlineStatus>
+    implements UserOnlineStatusService{
+    private static final String KEY_ONLINE_USERS = "online_users";
+    private static final String KEY_USER_STATUS = "user_status:%s";
+    @Autowired
+    private UserOnlineStatusMapper userOnlineStatusMapper;
+    @Autowired
+    private RedisTemplate<String, Object> redisTemplate;
+    // 用户上线
+    @Override
+    public void userGoOnline(Long id) {
+        redisTemplate.opsForSet().add(KEY_ONLINE_USERS, id.toString());
+        String statusKey=KEY_USER_STATUS+id;
+        redisTemplate.opsForHash().put(statusKey,"online_time",System.currentTimeMillis());
+        redisTemplate.opsForHash().put(statusKey,"last_heartbeat",System.currentTimeMillis());
+        redisTemplate.expire(statusKey,30, TimeUnit.HOURS);
+        QueryWrapper<UserOnlineStatus> queryWrapper = new QueryWrapper<>();
+        queryWrapper.eq("user_id", id);
+        UserOnlineStatus onlineStatus = userOnlineStatusMapper.selectOne(queryWrapper);
+        if(onlineStatus == null){
+            UserOnlineStatus userOnlineStatus = new UserOnlineStatus();
+            userOnlineStatus.setUserId(id);
+            userOnlineStatus.setIsOnline(1);
+            userOnlineStatus.setOnlineTime(new Date());
+            userOnlineStatus.setLastHeartbeatTime(new Date());
+            userOnlineStatusMapper.insert(userOnlineStatus);
+            return;
+        }
+        onlineStatus.setIsOnline(1);
+        onlineStatus.setOnlineTime(new Date());
+        userOnlineStatusMapper.updateById(onlineStatus);
+    }
+    // 处理心跳包
+    @Override
+    public void handleHeartbeat(HttpServletRequest httpRequest) {
+        String token = httpRequest.getHeader("token");
+        Long uid= Long.valueOf(TokenUtils.getUserIdFromToken(token));
+        String statusKey =KEY_USER_STATUS + uid;
+        long l = System.currentTimeMillis();
+        redisTemplate.opsForHash().put(statusKey, "last_heartbeat", l);
+        redisTemplate.opsForSet().add(KEY_ONLINE_USERS, uid.toString());
+    }
+
+    @Override
+    public void userGoOffline(Long id) {
+        redisTemplate.opsForSet().remove(KEY_ONLINE_USERS, id.toString());
+        QueryWrapper<UserOnlineStatus> queryWrapper = new QueryWrapper<>();
+        queryWrapper.eq("user_id", id);
+        UserOnlineStatus onlineStatus = userOnlineStatusMapper.selectOne(queryWrapper);
+        onlineStatus.setIsOnline(0);
+        onlineStatus.setOnlineTime(new Date());
+        userOnlineStatusMapper.updateById(onlineStatus);
+    }
+
+}
+
+
+
+

+ 262 - 0
src/main/java/com/zhentao/user/service/impl/UserServiceImpl.java

@@ -0,0 +1,262 @@
+package com.zhentao.user.service.impl;
+
+import cn.hutool.core.util.IdUtil;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.zhentao.common.enums.ApiServerException;
+import com.zhentao.common.exception.AsynException;
+import com.zhentao.common.utils.HttpUtils;
+import com.zhentao.common.utils.TokenUtils;
+import com.zhentao.common.vo.Result;
+import com.zhentao.information.service.WebSocketService;
+import com.zhentao.user.domain.User;
+import com.zhentao.user.domain.UserMoney;
+import com.zhentao.user.dto.NoteDto;
+import com.zhentao.user.dto.UserDto;
+import com.zhentao.user.service.UserMoneyService;
+import com.zhentao.user.service.UserOnlineStatusService;
+import com.zhentao.user.service.UserService;
+import com.zhentao.user.mapper.UserMapper;
+import org.apache.http.HttpResponse;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.stereotype.Service;
+import org.springframework.util.DigestUtils;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+/**
+* @author 86183
+* @description 针对表【user(用户表)】的数据库操作Service实现
+* @createDate 2025-07-01 09:52:29
+*/
+@Service
+public class UserServiceImpl extends ServiceImpl<UserMapper, User>
+    implements UserService{
+    @Autowired
+    private RedissonClient redissonClient;
+    @Autowired
+    private StringRedisTemplate stringRedisTemplate;
+    @Autowired
+    public WebSocketService webSocketService;
+    @Autowired
+    private UserOnlineStatusService onlineStatusService;
+    @Autowired
+    private UserMoneyService userMoneyService;
+    @Override
+    public Result register(UserDto userDto) {
+        RLock lock = redissonClient.getLock(userDto.getUsername()+userDto.getPhone());
+        try {
+            boolean b = lock.tryLock(10, 30, TimeUnit.SECONDS);
+            if (b){
+                QueryWrapper<User> queryWrapper = new QueryWrapper<>();
+                queryWrapper.eq("avatar",userDto.getPhone());
+                User one = this.getOne(queryWrapper);
+                if (one!=null){
+                    return Result.ERR(null,"改手机号已注册");
+                }
+                String s1 = stringRedisTemplate.opsForValue().get(userDto.getPhone());
+                if (!s1.equals(userDto.getCode())){
+                    return Result.ERR(null,"请勿重复发送验证码");
+                }
+                User user = new User();
+                long l = IdUtil.getSnowflake(1, 1).nextId();
+                user.setId(l);
+                String string = UUID.randomUUID().toString();
+                user.setSalt(string);
+                user.setAvatar(userDto.getPhone());
+                String s = DigestUtils.md5DigestAsHex((string + userDto.getPassword()).getBytes());
+                user.setPassword(s);
+                user.setUsername(userDto.getUsername());
+                boolean save = this.save(user);
+                UserMoney userMoney = new UserMoney();
+                long l1 = IdUtil.getSnowflake(1, 1).nextId();
+                userMoney.setId(l1);
+                userMoney.setUserId(l);
+                userMoney.setPhone(userDto.getPhone());
+                userMoneyService.save(userMoney);
+                return Result.OK(save,"注册成功");
+            }
+        } catch (InterruptedException e) {
+            throw new RuntimeException(e);
+        }finally {
+            lock.unlock();
+        }
+        return null;
+    }
+
+    @Override
+    public Result login(UserDto userDto) {
+        RLock lock = redissonClient.getLock(userDto.getUsername()+userDto.getPhone());
+        try {
+            boolean b = lock.tryLock(10, 30, TimeUnit.SECONDS);
+            if (b){
+                // 查询用户信息,根据用户名
+                QueryWrapper<User> queryWrapper = new QueryWrapper<>();
+                queryWrapper.eq("username",userDto.getUsername());
+                User one = this.getOne(queryWrapper);
+                // 如果用户不存在,抛出异常
+                if (one==null){
+                    QueryWrapper<User> queryWrapper1 = new QueryWrapper<>();
+                    queryWrapper1.eq("avatar",userDto.getUsername());
+                    User one1 = this.getOne(queryWrapper1);
+                    if (one1==null){
+                        throw new AsynException(ApiServerException.NULL_USERNAME);
+                    }else {
+                        Result token = this.token(one1, userDto);
+                        return token;
+                    }
+                }else {
+                    Result token = this.token(one, userDto);
+                    return token;
+                }
+            }
+        } catch (InterruptedException e) {
+            throw new RuntimeException(e);
+        }finally {
+            lock.unlock();
+        }
+        return null;
+    }
+    public Result token(User one,UserDto userDto){
+        // 获取用户盐值,用于密码加密
+        String salt = one.getSalt();
+        // 加密用户输入的密码,并与数据库中的密码进行比较
+        String s = DigestUtils.md5DigestAsHex((salt + userDto.getPassword()).getBytes());
+        if (!s.equals(one.getPassword())){
+            throw new AsynException(ApiServerException.NULL_PASSWORD);
+        }
+        // 生成JWT令牌
+        String jwtToken = TokenUtils.generateToken(one.getId()+"");
+        stringRedisTemplate.opsForValue().set(one.getId().toString(),jwtToken);
+        System.err.println(stringRedisTemplate.opsForValue().get(one.getId().toString()));
+        // 返回登录成功结果和JWT令牌
+
+        // 将用户ID和token存储到WebSocketService中
+        webSocketService.storeUserToken(one.getId()+"", jwtToken);
+
+        Map<String,Object> map = new HashMap<>();
+        map.put("token",jwtToken);
+        map.put("userId",one.getId()+"");
+        map.put("image",one.getAvatar()+"");
+        //用户上线
+        onlineStatusService.userGoOnline(one.getId());
+
+        return Result.OK(map,"登录成功");
+    }
+    //验证码
+    @Override
+    public Result note(NoteDto noteDto) {
+        //随机生成六位数
+        int randomSixDigit=100000 + (int)(Math.random() * 900000);
+        System.err.println("手机号:"+noteDto.getPhone());
+        System.err.println("验证码:"+randomSixDigit);
+        stringRedisTemplate.opsForValue().set(noteDto.getPhone(),randomSixDigit+"");
+        String host = "https://gyytz.market.alicloudapi.com";
+        String path = "/sms/smsSend";
+        String method = "POST";
+        String appcode = "b685e5e231ce404c855db67359acb1e1";
+        Map<String, String> headers = new HashMap<String, String>();
+        // 最后在 header 中的格式(中间是英文空格)为 Authorization:APPCODE 83359fd73fe94948385f570e3c139105
+        headers.put("Authorization", "APPCODE " + appcode);
+        Map<String, String> querys = new HashMap<String, String>();
+        System.out.println(noteDto.getPhone());
+        querys.put("mobile",noteDto.getPhone());
+        querys.put("param", "**code**:"+randomSixDigit+",**minute**:5");
+        // smsSignId(短信前缀)和 templateId(短信模板),可登录国阳云控制台自助申请。参考文档:http://help.guoyangyun.com/Problem/Qm.html
+        querys.put("smsSignId", "2e65b1bb3d054466b82f0c9d125465e2");
+        querys.put("templateId", "908e94ccf08b4476ba6c876d13f084ad");
+        Map<String, String> bodys = new HashMap<String, String>();
+        try {
+            /**
+             * 重要提示如下:
+             * HttpUtils 请从
+             * https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/src/main/java/com/aliyun/api/gateway/demo/util/HttpUtils.java
+             * 下载
+             *
+             * 相应的依赖请参照
+             * https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/pom.xml
+             */
+            HttpResponse response = HttpUtils.doPost(host, path, method, headers, querys, bodys);
+            System.out.println(response.toString());
+            // 获取 response 的 body
+            // System.out.println(EntityUtils.toString(response.getEntity()));
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return Result.OK(null,"发送成功");
+    }
+
+    @Override
+    public Result loginnote(NoteDto noteDto) {
+        RLock lock = redissonClient.getLock(noteDto.getCode());
+        try {
+            boolean b = lock.tryLock(10, 30, TimeUnit.SECONDS);
+            if (b){
+                String code = stringRedisTemplate.opsForValue().get(noteDto.getPhone());
+                if (code.equals(noteDto.getCode())){
+                    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
+                    queryWrapper.eq("avatar",noteDto.getPhone());
+                    User one = this.getOne(queryWrapper);
+                    if (one!=null){
+                        // 生成JWT令牌
+                        String jwtToken = TokenUtils.generateToken(one.getId()+"");
+                        stringRedisTemplate.opsForValue().set(one.getId().toString(),jwtToken);
+                        System.err.println(stringRedisTemplate.opsForValue().get(one.getId().toString()));
+                        // 返回登录成功结果和JWT令牌
+
+                        // 将用户ID和token存储到WebSocketService中
+                        webSocketService.storeUserToken(one.getId()+"", jwtToken);
+
+                        Map<String,Object> map = new HashMap<>();
+                        map.put("token",jwtToken);
+                        map.put("userId",one.getId()+"");
+                        map.put("image",one.getAvatar()+"");
+                        //用户上线
+                        onlineStatusService.userGoOnline(one.getId());
+                        return Result.OK(map,"登录成功");
+                    }else {
+                        User user = new User();
+                        long l = IdUtil.getSnowflake(1, 1).nextId();
+                        user.setId(l);
+                        user.setAvatar(noteDto.getPhone());
+                        this.save(user);
+                        String jwtToken = TokenUtils.generateToken(l+"");
+                        stringRedisTemplate.opsForValue().set(l+"",jwtToken);
+                        // 返回登录成功结果和JWT令牌
+
+                        // 将用户ID和token存储到WebSocketService中
+                        webSocketService.storeUserToken(l+"", jwtToken);
+
+                        Map<String,Object> map = new HashMap<>();
+                        map.put("token",jwtToken);
+                        map.put("userId",user.getId()+"");
+                        map.put("image",user.getAvatar()+"");
+                        //用户上线
+                        onlineStatusService.userGoOnline(one.getId());
+                        return Result.OK(map,"登录成功");
+                    }
+                }else {
+                    return Result.ERR(null,"验证码错误");
+                }
+            }
+        } catch (InterruptedException e) {
+            throw new RuntimeException(e);
+        }finally {
+            lock.unlock();
+        }
+        return Result.OK(null,"登录成功");
+    }
+
+
+
+}
+
+
+
+

+ 14 - 0
src/main/java/com/zhentao/utils/DateUtils.java

@@ -0,0 +1,14 @@
+package com.zhentao.utils;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.Date;
+
+public class DateUtils {
+    public static Date parseBirthday(String birthdayStr) throws ParseException {
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
+        return sdf.parse(birthdayStr);
+    }
+}

+ 253 - 0
src/main/java/com/zhentao/utils/IdVerificationUtil.java

@@ -0,0 +1,253 @@
+package com.zhentao.utils;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import okhttp3.FormBody;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
+import com.google.gson.Gson;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * 阿里云实名认证工具类(适配实际API响应结构)
+ * 支持身份证信息核验及结果解析
+ */
+public class IdVerificationUtil {
+    private static final String DEFAULT_API_URL = "https://eid.shumaidata.com/eid/checkbody";
+    private final String appCode;
+    private final String apiUrl;
+    private final OkHttpClient client;
+    private final Gson gson;
+
+    /**
+     * 使用默认API地址初始化工具类
+     */
+    public IdVerificationUtil(String appCode) {
+        this(appCode, DEFAULT_API_URL);
+    }
+
+    /**
+     * 自定义API地址初始化工具类(支持测试环境)
+     */
+    public IdVerificationUtil(String appCode, String apiUrl) {
+        this.appCode = appCode;
+        this.apiUrl = apiUrl;
+        this.client = new OkHttpClient.Builder().build();
+        this.gson = new Gson();
+    }
+
+    /**
+     * 执行身份证实名认证
+     * @param idCard 身份证号
+     * @param name 姓名
+     * @return 验证结果对象
+     */
+    public VerificationResult verify(String idCard, String name) {
+        Map<String, String> params = new HashMap<>();
+        params.put("idcard", idCard);
+        params.put("name", name);
+
+        try {
+            String responseJson = sendPostRequest(params);
+            ApiResponse apiResponse = gson.fromJson(responseJson, ApiResponse.class);
+
+            // 业务成功判断:code为"0"且message为"成功"
+            if ("0".equals(apiResponse.getCode()) && "成功".equals(apiResponse.getMessage())) {
+                return new VerificationResult(true, apiResponse.getResult(), null);
+            } else {
+                String errorMsg = "认证业务失败 [code=" + apiResponse.getCode() +
+                        ", message=" + apiResponse.getMessage() + "]";
+                return new VerificationResult(false, null, errorMsg);
+            }
+        } catch (Exception e) {
+            String errorMsg = "认证处理异常: " + e.getClass().getSimpleName() + ": " + e.getMessage();
+            return new VerificationResult(false, null, errorMsg);
+        }
+    }
+
+    /**
+     * 发送POST请求并获取响应
+     */
+    private String sendPostRequest(Map<String, String> params) throws IOException {
+        FormBody.Builder formBuilder = new FormBody.Builder();
+        Iterator<String> it = params.keySet().iterator();
+
+        while (it.hasNext()) {
+            String key = it.next();
+            formBuilder.add(key, params.get(key));
+        }
+
+        FormBody body = formBuilder.build();
+        Request request = new Request.Builder()
+                .url(apiUrl)
+                .addHeader("Authorization", "APPCODE " + appCode)
+                .post(body)
+                .build();
+
+        try (Response response = client.newCall(request).execute()) {
+            if (!response.isSuccessful()) {
+                throw new IOException("HTTP请求失败 [code=" + response.code() + "]");
+            }
+            return response.body().string();
+        }
+    }
+
+    /**
+     * 验证结果封装类
+     */
+    public static class VerificationResult {
+        private final boolean success;
+        private final VerificationData data;
+        private final String errorMessage;
+
+        public VerificationResult(boolean success, VerificationData data, String errorMessage) {
+            this.success = success;
+            this.data = data;
+            this.errorMessage = errorMessage;
+        }
+
+        public boolean isSuccess() {
+            return success;
+        }
+
+        public VerificationData getData() {
+            return data;
+        }
+
+        public String getErrorMessage() {
+            return errorMessage;
+        }
+
+        @Override
+        public String toString() {
+            return "VerificationResult{" +
+                    "success=" + success +
+                    ", data=" + data +
+                    ", errorMsg='" + errorMessage + '\'' +
+                    '}';
+        }
+    }
+
+    /**
+     * API响应根数据结构(匹配实际JSON格式)
+     */
+    private static class ApiResponse {
+        @SerializedName("code")
+        private String code;
+        @SerializedName("message")
+        private String message;
+        @SerializedName("result")
+        private VerificationData result;
+
+        public String getCode() {
+            return code;
+        }
+
+        public String getMessage() {
+            return message;
+        }
+
+        public VerificationData getResult() {
+            return result;
+        }
+    }
+
+    /**
+     * 认证详情数据结构
+     */
+    public static class VerificationData {
+        @SerializedName("name")
+        private String name;
+        @SerializedName("idcard")
+        private String idcard;
+        @SerializedName("res")
+        private String res;        // 匹配结果标识("1"表示一致)
+        @SerializedName("description")
+        private String description;// 结果描述
+        @SerializedName("sex")
+        private String sex;
+        @SerializedName("birthday")
+        private String birthday;
+        @SerializedName("address")
+        private String address;
+
+        public String getName() {
+            return name;
+        }
+
+        public String getIdcard() {
+            return idcard;
+        }
+
+        public String getRes() {
+            return res;
+        }
+
+        /**
+         * 判断身份信息是否一致
+         * @return true表示信息一致
+         */
+        public boolean isInformationMatch() {
+            return "1".equals(res) || "一致".equals(description);
+        }
+
+        public String getDescription() {
+            return description;
+        }
+
+        public String getSex() {
+            return sex;
+        }
+
+        public String getBirthday() {
+            return birthday;
+        }
+
+        public String getAddress() {
+            return address;
+        }
+
+        @Override
+        public String toString() {
+            return "VerificationData{" +
+                    "name='" + name + '\'' +
+                    ", idcard='" + idcard + '\'' +
+                    ", matchRes='" + res + '\'' +
+                    ", description='" + description + '\'' +
+                    ", sex='" + sex + '\'' +
+                    ", birthday='" + birthday + '\'' +
+                    ", address='" + address + '\'' +
+                    '}';
+        }
+    }
+
+    /**
+     * 示例用法
+     */
+    public static void main(String[] args) {
+        String appCode = "b685e5e231ce404c855db67359acb1e1";
+        IdVerificationUtil util = new IdVerificationUtil(appCode);
+
+        // 执行认证
+        VerificationResult result = util.verify("130627200509242626", "曹林萱");
+
+        // 处理结果
+        if (result.isSuccess()) {
+            VerificationData data = result.getData();
+            System.out.println("【认证成功】");
+            System.out.println("姓名: " + data.getName());
+            System.out.println("身份证: " + data.getIdcard());
+            System.out.println("性别: " + data.getSex());
+            System.out.println("地址: " + data.getAddress());
+            System.out.println("生日: " + data.getBirthday());
+            System.out.println("认证结果: " + (data.isInformationMatch() ? "信息一致" : "信息不一致"));
+            System.out.println("描述: " + data.getDescription());
+        } else {
+            System.out.println("【认证失败】" + result.getErrorMessage());
+        }
+    }
+}

+ 136 - 0
src/main/java/com/zhentao/utils/SnowflakeIdGenerator.java

@@ -0,0 +1,136 @@
+package com.zhentao.utils;
+
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+
+public class SnowflakeIdGenerator {
+    // 基础配置(Java 8+)
+    private static final long EPOCH = 1288834974657L; // 2010-11-04 00:00:00 UTC
+    private static final int DATACENTER_ID_BITS = 5;
+    private static final int WORKER_ID_BITS = 5;
+    private static final int SEQUENCE_BITS = 12;
+    // 位运算掩码
+    private static final long DATACENTER_ID_MASK = (1L << DATACENTER_ID_BITS) - 1;
+    private static final long WORKER_ID_MASK = (1L << WORKER_ID_BITS) - 1;
+    private static final long SEQUENCE_MASK = (1L << SEQUENCE_BITS) - 1;
+
+    // 位移偏移量
+    private static final int WORKER_ID_SHIFT = SEQUENCE_BITS;
+    private static final int DATACENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;
+    private static final int TIMESTAMP_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATACENTER_ID_BITS;
+
+    private final long datacenterId;
+    private final long workerId;
+    private long sequence = 0L;
+    private long lastTimestamp = -1L;
+
+    private final Object lock = new Object();
+
+    public SnowflakeIdGenerator(long datacenterId, long workerId) {
+        if ((datacenterId & DATACENTER_ID_MASK) != datacenterId) {
+            throw new IllegalArgumentException("DataCenter ID 必须在 0-31 之间");
+        }
+        if ((workerId & WORKER_ID_MASK) != workerId) {
+            throw new IllegalArgumentException("Worker ID 必须在 0-31 之间");
+        }
+        this.datacenterId = datacenterId;
+        this.workerId = workerId;
+    }
+
+    public synchronized long nextId() {
+        long currentTimestamp = timeGen();
+
+        if (currentTimestamp < lastTimestamp) {
+            throw new IllegalStateException(
+                    String.format("时钟回退 %d 毫秒,禁止生成ID(上次: %d,当前: %d)",
+                            lastTimestamp - currentTimestamp, lastTimestamp, currentTimestamp));
+        }
+
+        if (currentTimestamp == lastTimestamp) {
+            sequence = (sequence + 1) & SEQUENCE_MASK;
+            if (sequence == 0) {
+                currentTimestamp = waitNextMillis(lastTimestamp);
+            }
+        } else {
+            sequence = 0;
+        }
+
+        lastTimestamp = currentTimestamp;
+
+        return ((currentTimestamp - EPOCH) << TIMESTAMP_SHIFT) |
+                (datacenterId << DATACENTER_ID_SHIFT) |
+                (workerId << WORKER_ID_SHIFT) |
+                sequence;
+    }
+
+    private long waitNextMillis(long lastTimestamp) {
+        long timestamp = timeGen();
+        while (timestamp <= lastTimestamp) {
+            timestamp = System.currentTimeMillis(); // 改用更可靠的时间源
+        }
+        return timestamp;
+    }
+
+    private long timeGen() {
+        return System.currentTimeMillis();
+    }
+
+    // 添加 parseId 方法
+    public static SnowflakeMeta parseId(long snowflakeId) {
+        long sequence = snowflakeId & SEQUENCE_MASK;
+        long workerId = (snowflakeId >> WORKER_ID_SHIFT) & WORKER_ID_MASK;
+        long datacenterId = (snowflakeId >> DATACENTER_ID_SHIFT) & DATACENTER_ID_MASK;
+        long timestamp = (snowflakeId >> TIMESTAMP_SHIFT) + EPOCH;
+
+        return new SnowflakeMeta(
+                timestamp,
+                datacenterId,
+                workerId,
+                sequence,
+                LocalDateTime.ofEpochSecond(timestamp / 1000, 0, ZoneOffset.ofHours(8))
+        );
+    }
+    public static Long getSnowId(){
+        SnowflakeIdGenerator generator=new SnowflakeIdGenerator(1,1);
+        Long id=generator.nextId();
+        SnowflakeMeta meta=SnowflakeIdGenerator.parseId(id);
+        return id;
+    }
+    // ---------------- 修复:添加完整的getter方法 ----------------
+    public static class SnowflakeMeta {
+        private final long timestamp;
+        private final long datacenterId;
+        private final long workerId;
+        private final long sequence;
+        private final LocalDateTime dateTime;
+
+        public SnowflakeMeta(long timestamp, long datacenterId, long workerId, long sequence, LocalDateTime dateTime) {
+            this.timestamp = timestamp;
+            this.datacenterId = datacenterId;
+            this.workerId = workerId;
+            this.sequence = sequence;
+            this.dateTime = dateTime;
+        }
+
+        // 显式定义getter(修复爆红)
+        public long getTimestamp() {
+            return timestamp;
+        }
+
+        public long getDatacenterId() {
+            return datacenterId;
+        }
+
+        public long getWorkerId() {
+            return workerId;
+        }
+
+        public long getSequence() {
+            return sequence;
+        }
+
+        public LocalDateTime getDateTime() {
+            return dateTime;
+        }
+    }
+}

+ 241 - 0
src/main/java/com/zhentao/utils/VolunteerNumberGenerator.java

@@ -0,0 +1,241 @@
+package com.zhentao.utils;
+
+import java.util.*;
+import java.util.concurrent.atomic.AtomicLong;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * 简化版志愿者编号生成工具类
+ * 编号格式:地址前缀_安全哈希码(如 BJ_d84f0d)
+ * 移除日期部分,更简洁且保护隐私
+ */
+public class VolunteerNumberGenerator {
+    // 地址前缀映射表
+    private static final Map<String, String> ADDRESS_PREFIX_MAP = new HashMap<>();
+    // 安全哈希码长度
+    private static final int SECURE_CODE_LENGTH = 6;
+    // 地址-计数器映射(线程安全)
+    private static final Map<String, AtomicLong> ADDRESS_COUNTER = new HashMap<>();
+    // 单例实例
+    private static final VolunteerNumberGenerator INSTANCE = new VolunteerNumberGenerator();
+    // 安全盐值
+    private static final String SECURITY_SALT = "volunteer_simplified_2025";
+    // 哈希缓存
+    private static final Map<String, String> HASH_CACHE = new HashMap<>();
+
+    // 静态代码块初始化地址前缀映射
+    static {
+        // 省份地址前缀映射
+        ADDRESS_PREFIX_MAP.put("北京", "BJ");
+        ADDRESS_PREFIX_MAP.put("上海", "SH");
+        ADDRESS_PREFIX_MAP.put("天津", "TJ");
+        ADDRESS_PREFIX_MAP.put("重庆", "CQ");
+        ADDRESS_PREFIX_MAP.put("河北", "HE");
+        ADDRESS_PREFIX_MAP.put("山西", "SX");
+        ADDRESS_PREFIX_MAP.put("辽宁", "LN");
+        ADDRESS_PREFIX_MAP.put("吉林", "JL");
+        ADDRESS_PREFIX_MAP.put("黑龙江", "HL");
+        ADDRESS_PREFIX_MAP.put("江苏", "JS");
+        ADDRESS_PREFIX_MAP.put("浙江", "ZJ");
+        ADDRESS_PREFIX_MAP.put("安徽", "AH");
+        ADDRESS_PREFIX_MAP.put("福建", "FJ");
+        ADDRESS_PREFIX_MAP.put("江西", "JX");
+        ADDRESS_PREFIX_MAP.put("山东", "SD");
+        ADDRESS_PREFIX_MAP.put("河南", "HA");
+        ADDRESS_PREFIX_MAP.put("湖北", "HB");
+        ADDRESS_PREFIX_MAP.put("湖南", "HN");
+        ADDRESS_PREFIX_MAP.put("广东", "GD");
+        ADDRESS_PREFIX_MAP.put("海南", "HI");
+        ADDRESS_PREFIX_MAP.put("四川", "SC");
+        ADDRESS_PREFIX_MAP.put("贵州", "GZ");
+        ADDRESS_PREFIX_MAP.put("云南", "YN");
+        ADDRESS_PREFIX_MAP.put("陕西", "SN");
+        ADDRESS_PREFIX_MAP.put("甘肃", "GS");
+        ADDRESS_PREFIX_MAP.put("青海", "QH");
+        ADDRESS_PREFIX_MAP.put("台湾", "TW");
+        ADDRESS_PREFIX_MAP.put("内蒙古", "NM");
+        ADDRESS_PREFIX_MAP.put("广西", "GX");
+        ADDRESS_PREFIX_MAP.put("西藏", "XZ");
+        ADDRESS_PREFIX_MAP.put("宁夏", "NX");
+        ADDRESS_PREFIX_MAP.put("新疆", "XJ");
+        ADDRESS_PREFIX_MAP.put("香港", "HK");
+        ADDRESS_PREFIX_MAP.put("澳门", "MO");
+
+        // 常见城市别名映射
+        ADDRESS_PREFIX_MAP.put("杭州", "HZ");
+        ADDRESS_PREFIX_MAP.put("宁波", "NB");
+        ADDRESS_PREFIX_MAP.put("温州", "WZ");
+        ADDRESS_PREFIX_MAP.put("金华", "JH");
+        ADDRESS_PREFIX_MAP.put("绍兴", "SX");
+        ADDRESS_PREFIX_MAP.put("南京", "NJ");
+        ADDRESS_PREFIX_MAP.put("苏州", "SZ");
+        ADDRESS_PREFIX_MAP.put("无锡", "WX");
+        ADDRESS_PREFIX_MAP.put("广州", "GZ");
+        ADDRESS_PREFIX_MAP.put("深圳", "SZ");
+        ADDRESS_PREFIX_MAP.put("成都", "CD");
+        ADDRESS_PREFIX_MAP.put("武汉", "WH");
+        // 可根据需要继续扩展...
+    }
+
+    private VolunteerNumberGenerator() {
+        // 私有构造函数,防止外部实例化
+    }
+
+    /**
+     * 获取单例实例
+     */
+    public static VolunteerNumberGenerator getInstance() {
+        return INSTANCE;
+    }
+
+    /**
+     * 生成简化版志愿者编号(地址前缀_安全哈希码)
+     */
+    public synchronized String generateNumber(String address) {
+        if (address == null || address.trim().isEmpty()) {
+            throw new IllegalArgumentException("地址不能为空");
+        }
+
+        String prefix = parseAddressPrefix(address);
+        long counter = getAndIncrementCounter(prefix);
+
+        // 生成安全哈希码(基于地址前缀和计数器)
+        String secureCode = generateSecureCode(prefix, counter);
+
+        return prefix + "_" + secureCode;
+    }
+
+    /**
+     * 生成安全哈希码(使用SHA-256算法)
+     */
+    private String generateSecureCode(String prefix, long counter) {
+        String input = prefix + counter + SECURITY_SALT;
+
+        // 检查缓存
+        if (HASH_CACHE.containsKey(input)) {
+            return HASH_CACHE.get(input);
+        }
+
+        try {
+            MessageDigest md = MessageDigest.getInstance("SHA-256");
+            byte[] hashBytes = md.digest(input.getBytes());
+
+            // 转换为16进制并截取中间部分
+            StringBuilder sb = new StringBuilder();
+            for (byte b : hashBytes) {
+                sb.append(String.format("%02x", b));
+            }
+
+            int startIndex = (sb.length() - SECURE_CODE_LENGTH) / 2;
+            String secureCode = sb.substring(startIndex, startIndex + SECURE_CODE_LENGTH);
+            HASH_CACHE.put(input, secureCode);
+
+            return secureCode;
+        } catch (NoSuchAlgorithmException e) {
+            // 备选方案:生成随机码
+            return randomCode();
+        }
+    }
+
+    /**
+     * 生成随机码(算法异常时的备选方案)
+     */
+    private String randomCode() {
+        return UUID.randomUUID().toString().substring(0, SECURE_CODE_LENGTH);
+    }
+
+    /**
+     * 解析地址前缀(提取省份或城市信息)
+     */
+    private String parseAddressPrefix(String address) {
+        // 优先匹配完整省份名称
+        for (Map.Entry<String, String> entry : ADDRESS_PREFIX_MAP.entrySet()) {
+            if (address.startsWith(entry.getKey())) {
+                return entry.getValue();
+            }
+        }
+
+        // 其次匹配省份简称或城市(长度<4的关键词)
+        for (Map.Entry<String, String> entry : ADDRESS_PREFIX_MAP.entrySet()) {
+            if (address.contains(entry.getKey()) && entry.getKey().length() < 4) {
+                return entry.getValue();
+            }
+        }
+
+        // 未匹配到预设地址时的默认处理
+        return "OTHER";
+    }
+
+    /**
+     * 获取并递增地址计数器(保证同地址编号唯一性)
+     */
+    private long getAndIncrementCounter(String prefix) {
+        AtomicLong counter = ADDRESS_COUNTER.computeIfAbsent(prefix, k -> new AtomicLong(1));
+        return counter.getAndIncrement();
+    }
+
+    /**
+     * 从编号中提取地址前缀(用于排序)
+     */
+    public String extractPrefix(String number) {
+        if (number.contains("_")) {
+            return number.split("_")[0];
+        }
+        return "OTHER";
+    }
+
+    /**
+     * 获取地址前缀比较器(按字母顺序排序)
+     */
+    public Comparator<String> getPrefixComparator() {
+        return (n1, n2) -> extractPrefix(n1).compareTo(extractPrefix(n2));
+    }
+
+    /**
+     * 批量生成编号(用于初始化或批量注册)
+     */
+    public List<String> batchGenerateNumbers(List<String> addresses) {
+        List<String> result = new ArrayList<>();
+        for (String address : addresses) {
+            result.add(generateNumber(address));
+        }
+        return result;
+    }
+
+    /**
+     * 示例用法
+     */
+    public static void main(String[] args) {
+        VolunteerNumberGenerator generator = VolunteerNumberGenerator.getInstance();
+
+        // 生成简化编号
+        System.out.println("北京: " + generator.generateNumber("北京市朝阳区"));
+        System.out.println("上海: " + generator.generateNumber("上海市浦东新区"));
+        System.out.println("浙江杭州: " + generator.generateNumber("浙江省杭州市"));
+        System.out.println("浙江宁波: " + generator.generateNumber("浙江省宁波市"));
+        System.out.println("广东深圳: " + generator.generateNumber("广东省深圳市"));
+
+        // 生成重复地址编号
+        System.out.println("\n北京(2): " + generator.generateNumber("北京市海淀区"));
+        System.out.println("北京(3): " + generator.generateNumber("北京市西城区"));
+
+        // 生成未知地址编号
+        System.out.println("\n海外地址: " + generator.generateNumber("美国纽约州"));
+
+        // 排序示例
+        List<String> numbers = Arrays.asList(
+                generator.generateNumber("上海市"),
+                generator.generateNumber("北京市"),
+                generator.generateNumber("浙江省"),
+                generator.generateNumber("广东省"),
+                generator.generateNumber("江苏省")
+        );
+
+        // 按地址前缀排序
+        numbers.sort(generator.getPrefixComparator());
+
+        System.out.println("\n按地址前缀排序结果:");
+        numbers.forEach(System.out::println);
+    }
+}

+ 16 - 0
src/main/java/com/zhentao/vo/UserVo.java

@@ -0,0 +1,16 @@
+package com.zhentao.vo;
+
+import com.zhentao.user.domain.User;
+import com.zhentao.user.domain.UserMoney;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class UserVo {
+    private User user;
+    private UserMoney userMoney;
+
+}

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

@@ -1,20 +1,24 @@
 server:
-  port: 8500
+  port: 8666
+  netty:
+    port: 8766
+    websocket:
+      port: 8866
 spring:
   # MySQL配置
   datasource:
     driver-class-name: com.mysql.cj.jdbc.Driver
-    url: jdbc:mysql://:3306/IM?useSSL=false&useServerTime=UTC
+    url: jdbc:mysql://121.43.148.220:3306/gongyi?useSSL=false&useServerTime=UTC
     username: root
-    password:
+    password: Wyc1563226
   # MongoDB配置
   data:
     mongodb:
-      host:
+      host: 121.43.148.220
       port: 27017
-      database:
+      database: im_message_db
   redis:
-    host:
+    host: 121.43.148.220
     port: 6379
     database: 0
 aliyun:
@@ -23,3 +27,5 @@ aliyun:
     accessKeyId: LTAI5tH9VHPZwGJu4UX3hrL5          # 替换为您的AccessKey ID
     accessKeySecret: mbsutFJYLkzosvvKNr0DD28XSg4mqA  # 替换为您的AccessKey Secret
     bucketName: fjj1             # 替换为您的Bucket名称
+  trueName:
+    appCode: b685e5e231ce404c855db67359acb1e1

+ 22 - 0
src/main/resources/mapper/GroupMembersMapper.xml

@@ -0,0 +1,22 @@
+<?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.zhentao.groups.mapper.GroupMembersMapper">
+
+    <resultMap id="BaseResultMap" type="com.zhentao.groups.pojo.GroupMembers">
+            <id property="id" column="id" jdbcType="BIGINT"/>
+            <result property="groupId" column="group_id" jdbcType="BIGINT"/>
+            <result property="userId" column="user_id" jdbcType="BIGINT"/>
+            <result property="nickname" column="nickname" jdbcType="VARCHAR"/>
+            <result property="role" column="role" jdbcType="TINYINT"/>
+            <result property="joinTime" column="join_time" jdbcType="TIMESTAMP"/>
+            <result property="lastAckMsgId" column="last_ack_msg_id" jdbcType="BIGINT"/>
+    </resultMap>
+
+    <sql id="Base_Column_List">
+        id,group_id,user_id,
+        nickname,role,join_time,
+        last_ack_msg_id
+    </sql>
+</mapper>

+ 26 - 0
src/main/resources/mapper/GroupsMapper.xml

@@ -0,0 +1,26 @@
+<?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.zhentao.groups.mapper.GroupsMapper">
+
+    <resultMap id="BaseResultMap" type="com.zhentao.groups.pojo.Groupss">
+            <id property="groupId" column="group_id" jdbcType="BIGINT"/>
+            <result property="name" column="name" jdbcType="VARCHAR"/>
+            <result property="creatorId" column="creator_id" jdbcType="BIGINT"/>
+            <result property="avatar" column="avatar" jdbcType="VARCHAR"/>
+            <result property="announcement" column="announcement" jdbcType="VARCHAR"/>
+            <result property="description" column="description" jdbcType="VARCHAR"/>
+            <result property="maxMembers" column="max_members" jdbcType="INTEGER"/>
+            <result property="status" column="status" jdbcType="TINYINT"/>
+            <result property="createdAt" column="created_at" jdbcType="TIMESTAMP"/>
+            <result property="updatedAt" column="updated_at" jdbcType="TIMESTAMP"/>
+    </resultMap>
+
+    <sql id="Base_Column_List">
+        group_id,name,creator_id,
+        avatar,announcement,description,
+        max_members,status,created_at,
+        updated_at
+    </sql>
+</mapper>

+ 42 - 0
src/main/resources/mapper/UserMapper.xml

@@ -0,0 +1,42 @@
+<?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.zhentao.user.mapper.UserMapper">
+
+    <resultMap id="BaseResultMap" type="com.zhentao.user.domain.User">
+            <id property="id" column="id" jdbcType="BIGINT"/>
+            <result property="username" column="username" jdbcType="VARCHAR"/>
+            <result property="password" column="password" jdbcType="VARCHAR"/>
+            <result property="realName" column="real_name" jdbcType="VARCHAR"/>
+            <result property="phone" column="phone" jdbcType="VARCHAR"/>
+            <result property="address" column="address" jdbcType="VARCHAR"/>
+            <result property="volunteerNumber" column="volunteer_number" jdbcType="VARCHAR"/>
+            <result property="userType" column="user_type" jdbcType="VARCHAR"/>
+            <result property="email" column="email" jdbcType="VARCHAR"/>
+            <result property="avatar" column="avatar" jdbcType="VARCHAR"/>
+            <result property="gender" column="gender" jdbcType="TINYINT"/>
+            <result property="birthDate" column="birth_date" jdbcType="DATE"/>
+            <result property="status" column="status" jdbcType="TINYINT"/>
+            <result property="lastLoginTime" column="last_login_time" jdbcType="TIMESTAMP"/>
+            <result property="lastLoginIp" column="last_login_ip" jdbcType="VARCHAR"/>
+            <result property="lastLoginDevice" column="last_login_device" jdbcType="VARCHAR"/>
+            <result property="faceDataId" column="face_data_id" jdbcType="VARCHAR"/>
+            <result property="faceImageUrl" column="face_image_url" jdbcType="VARCHAR"/>
+            <result property="faceLoginEnabled" column="face_login_enabled" jdbcType="TINYINT"/>
+            <result property="createdAt" column="created_at" jdbcType="TIMESTAMP"/>
+            <result property="updatedAt" column="updated_at" jdbcType="TIMESTAMP"/>
+            <result property="deleted" column="deleted" jdbcType="TINYINT"/>
+            <result property="salt" column="salt" jdbcType="VARCHAR"/>
+    </resultMap>
+
+    <sql id="Base_Column_List">
+        id,username,password,
+        real_name,phone,email,
+        avatar,gender,birth_date,
+        status,last_login_time,last_login_ip,
+        last_login_device,face_data_id,face_image_url,
+        face_login_enabled,created_at,updated_at,
+        deleted,salt
+    </sql>
+</mapper>

+ 27 - 0
src/main/resources/mapper/UserMoneyMapper.xml

@@ -0,0 +1,27 @@
+<?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.zhentao.user.mapper.UserMoneyMapper">
+
+    <resultMap id="BaseResultMap" type="com.zhentao.user.domain.UserMoney">
+            <id property="id" column="id" jdbcType="BIGINT"/>
+            <result property="userId" column="user_id" jdbcType="BIGINT"/>
+            <result property="username" column="username" jdbcType="VARCHAR"/>
+            <result property="phone" column="phone" jdbcType="VARCHAR"/>
+            <result property="eventId" column="event_id" jdbcType="BIGINT"/>
+            <result property="jifen" column="jifen" jdbcType="FLOAT"/>
+            <result property="money" column="money" jdbcType="FLOAT"/>
+            <result property="eventName" column="event_name" jdbcType="VARCHAR"/>
+            <result property="jifenChange" column="jifen_change" jdbcType="FLOAT"/>
+            <result property="type" column="type" jdbcType="INTEGER"/>
+            <result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
+    </resultMap>
+
+    <sql id="Base_Column_List">
+        id,user_id,username,
+        phone,event_id,jifen,
+        money,event_name,jifen_change,
+        type,create_time
+    </sql>
+</mapper>

+ 0 - 13
src/test/java/com/zhentao/ZyzApplicationTests.java

@@ -1,13 +0,0 @@
-package com.zhentao;
-
-import org.junit.jupiter.api.Test;
-import org.springframework.boot.test.context.SpringBootTest;
-
-@SpringBootTest
-class ZyzApplicationTests {
-
-    @Test
-    void contextLoads() {
-    }
-
-}