zhentao 1 week ago
parent
commit
664d67bd2c

+ 6 - 1
src/main/java/com/zhentao/groups/MongoDB/pojo/Message.java

@@ -38,5 +38,10 @@ public class Message {
     /**
     /**
      * 消息创建时间
      * 消息创建时间
      */
      */
-    private Date createdAt;
+    private String fileUrl;      // 文件访问URL
+    private String fileName;     // 原始文件名
+    private String fileType;     // 文件MIME类型
+    private Long fileSize;       // 文件大小(字节)
+    private String avatar;
+
 }
 }

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

@@ -0,0 +1,37 @@
+package com.zhentao.information.controller;
+import com.zhentao.osspicture.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;
+    }
+}

+ 14 - 9
src/main/java/com/zhentao/information/entity/ChatMessage.java

@@ -14,22 +14,27 @@ public class ChatMessage {
 
 
     @Id
     @Id
     private String id;
     private String id;
-
     @Indexed
     @Indexed
     private String fromUserId;
     private String fromUserId;
-
     @Indexed
     @Indexed
     private String toUserId;
     private String toUserId;
-
     private String content;
     private String content;
-
-    private String type;
-
+    private String type;          // 新增: text/image/video/file
     private Long timestamp;
     private Long timestamp;
-
     private Boolean isRead;
     private Boolean isRead;
-
-    // 复合索引:用于查询两个用户之间的聊天记录
     @Indexed
     @Indexed
     private String chatId;
     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;
+    }
 }
 }

+ 14 - 28
src/main/java/com/zhentao/information/entity/Message.java

@@ -7,37 +7,23 @@ import lombok.Data;
  */
  */
 @Data
 @Data
 public class Message {
 public class Message {
-    /**
-     * 消息类型
-     * connect: 连接消息
-     * text: 文本消息
-     * image: 图片消息
-     * voice: 语音消息
-     */
-    private String type;
-    
-    /**
-     * 发送者ID
-     */
+    private String type;  // 新增类型: image/video/file
     private String fromUserId;
     private String fromUserId;
-    
-    /**
-     * 接收者ID
-     */
     private String toUserId;
     private String toUserId;
-    
-    /**
-     * 群ID(群聊消息时使用)
-     */
     private Long groupId;
     private Long groupId;
-    
-    /**
-     * 消息内容
-     */
     private String content;
     private String content;
-    
-    /**
-     * 消息时间戳
-     */
     private Long timestamp;
     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;
+    }
 }
 }

+ 12 - 4
src/main/java/com/zhentao/information/handler/WebSocketHandler.java

@@ -42,7 +42,7 @@ public class WebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketF
         try {
         try {
             Message message = JSON.parseObject(text, Message.class);
             Message message = JSON.parseObject(text, Message.class);
             log.info("接收到的消息:{}", message);
             log.info("接收到的消息:{}", message);
-            
+
             // 如果是连接消息,处理token
             // 如果是连接消息,处理token
             if ("connect".equals(message.getType())) {
             if ("connect".equals(message.getType())) {
                 String userId = webSocketService.handleUserLogin(message.getContent(), ctx);
                 String userId = webSocketService.handleUserLogin(message.getContent(), ctx);
@@ -91,7 +91,7 @@ public class WebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketF
      */
      */
     private void handleGroupMessage(Message message) {
     private void handleGroupMessage(Message message) {
         // 设置群聊消息类型
         // 设置群聊消息类型
-        message.setType("group_chat");
+        message.setType(message.getType() != null ? message.getType() : "group_chat");
         // 广播消息给群内所有成员
         // 广播消息给群内所有成员
         boolean sent = webSocketService.handleGroupMessage(message);
         boolean sent = webSocketService.handleGroupMessage(message);
         if (sent) {
         if (sent) {
@@ -109,9 +109,17 @@ public class WebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketF
     private void handleMessage(Message message) {
     private void handleMessage(Message message) {
         // 生成聊天ID
         // 生成聊天ID
         String chatId = generateChatId(message.getFromUserId(), message.getToUserId());
         String chatId = generateChatId(message.getFromUserId(), message.getToUserId());
-
-        // 创建MongoDB消息对象
         ChatMessage chatMessage = new ChatMessage();
         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.setFromUserId(message.getFromUserId());
         chatMessage.setToUserId(message.getToUserId());
         chatMessage.setToUserId(message.getToUserId());
         chatMessage.setContent(message.getContent());
         chatMessage.setContent(message.getContent());

+ 35 - 3
src/main/java/com/zhentao/information/service/WebSocketService.java

@@ -116,7 +116,7 @@ public class WebSocketService {
         if (message.getType() == null) {
         if (message.getType() == null) {
             message.setType("text");
             message.setType("text");
         }
         }
-        
+
         ChannelHandlerContext ctx = channelCache.getCache(userId);
         ChannelHandlerContext ctx = channelCache.getCache(userId);
         if (ctx != null && ctx.channel().isActive()) {
         if (ctx != null && ctx.channel().isActive()) {
             try {
             try {
@@ -131,6 +131,23 @@ public class WebSocketService {
             }
             }
         } else {
         } else {
             log.info("用户 {} 不在线,消息将保存到数据库", userId);
             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;
             return false;
         }
         }
     }
     }
@@ -195,10 +212,18 @@ public class WebSocketService {
         chatMessage.setFromUserId(message.getFromUserId());
         chatMessage.setFromUserId(message.getFromUserId());
         chatMessage.setToUserId(String.valueOf(groupId));
         chatMessage.setToUserId(String.valueOf(groupId));
         chatMessage.setContent(message.getContent());
         chatMessage.setContent(message.getContent());
-        chatMessage.setType("group_chat");
+        chatMessage.setType(message.getType() != null ? message.getType() : "group_chat");
         chatMessage.setTimestamp(System.currentTimeMillis());
         chatMessage.setTimestamp(System.currentTimeMillis());
         chatMessage.setIsRead(false);
         chatMessage.setIsRead(false);
         chatMessage.setChatId("group_" + groupId);
         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);
         chatMessageRepository.save(chatMessage);
 
 
         // 获取群成员
         // 获取群成员
@@ -210,7 +235,7 @@ public class WebSocketService {
                 break;
                 break;
             }
             }
         }
         }
-        
+
         if (groupMembers == null || groupMembers.isEmpty()) {
         if (groupMembers == null || groupMembers.isEmpty()) {
             log.error("群 {} 不存在或没有成员", groupId);
             log.error("群 {} 不存在或没有成员", groupId);
             return false;
             return false;
@@ -312,4 +337,11 @@ public class WebSocketService {
             channelGroup.remove(ctx.channel());
             channelGroup.remove(ctx.channel());
         });
         });
     }
     }
+
+    // 新增:生成聊天ID
+    private String generateChatId(String userId1, String userId2) {
+        return userId1.compareTo(userId2) < 0 ?
+            userId1 + "_" + userId2 :
+            userId2 + "_" + userId1;
+    }
 }
 }

+ 2 - 2
src/main/java/com/zhentao/moment/service/impl/UserMomentsServiceImpl.java

@@ -570,7 +570,7 @@ public class UserMomentsServiceImpl extends ServiceImpl<UserMomentsMapper, UserM
                             UserLogin user = likeCommentUserMap.get(like.getUserId());
                             UserLogin user = likeCommentUserMap.get(like.getUserId());
                             if (user != null) {
                             if (user != null) {
                                 Map<String, Object> userInfo = new HashMap<>();
                                 Map<String, Object> userInfo = new HashMap<>();
-                                userInfo.put("id", user.getId());
+                                userInfo.put("id", user.getId()+"");
                                 userInfo.put("nickName", user.getNickName());
                                 userInfo.put("nickName", user.getNickName());
                                 userInfo.put("avatar", user.getAvatar());
                                 userInfo.put("avatar", user.getAvatar());
                                 userInfo.put("username", user.getUserUsername());
                                 userInfo.put("username", user.getUserUsername());
@@ -659,7 +659,7 @@ public class UserMomentsServiceImpl extends ServiceImpl<UserMomentsMapper, UserM
         UserLogin user = userMap.get(moment.getUserId());
         UserLogin user = userMap.get(moment.getUserId());
         if (user != null) {
         if (user != null) {
             Map<String, Object> userInfo = new HashMap<>();
             Map<String, Object> userInfo = new HashMap<>();
-            userInfo.put("id", user.getId());
+            userInfo.put("id", user.getId().toString());
             userInfo.put("nickName", user.getNickName());
             userInfo.put("nickName", user.getNickName());
             userInfo.put("avatar", user.getAvatar());
             userInfo.put("avatar", user.getAvatar());
             userInfo.put("username", user.getUserUsername());
             userInfo.put("username", user.getUserUsername());

+ 192 - 26
src/main/java/com/zhentao/osspicture/OssUtil.java

@@ -2,15 +2,21 @@ package com.zhentao.osspicture;
 
 
 import com.aliyun.oss.OSS;
 import com.aliyun.oss.OSS;
 import com.aliyun.oss.OSSClientBuilder;
 import com.aliyun.oss.OSSClientBuilder;
+import com.aliyun.oss.model.ObjectMetadata;
 import com.aliyun.oss.model.PutObjectRequest;
 import com.aliyun.oss.model.PutObjectRequest;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 import org.springframework.stereotype.Component;
 import org.springframework.web.multipart.MultipartFile;
 import org.springframework.web.multipart.MultipartFile;
 
 
 import java.io.IOException;
 import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
 import java.util.UUID;
 import java.util.UUID;
 
 
 @Component
 @Component
+@Slf4j
 public class OssUtil {
 public class OssUtil {
 
 
     // 从配置文件中读取OSS相关信息
     // 从配置文件中读取OSS相关信息
@@ -18,44 +24,204 @@ public class OssUtil {
     private static final String accessKeyId = "LTAI5tH9VHPZwGJu4UX3hrL5";
     private static final String accessKeyId = "LTAI5tH9VHPZwGJu4UX3hrL5";
     private static final String accessKeySecret = "mbsutFJYLkzosvvKNr0DD28XSg4mqA";
     private static final String accessKeySecret = "mbsutFJYLkzosvvKNr0DD28XSg4mqA";
     private static final String bucketName = "fjj1";
     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 {
     public String uploadFile(MultipartFile file) throws IOException {
-        // 创建OSSClient实例
-        OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
+        if (file == null || file.isEmpty()) {
+            throw new IllegalArgumentException("上传文件不能为空");
+        }
 
 
+        OSS ossClient = null;
         try {
         try {
-            // 生成唯一的文件名
-//            String fileName = UUID.randomUUID().toString() + "-" + file.getOriginalFilename();
+            ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
+
             String originalFilename = file.getOriginalFilename();
             String originalFilename = file.getOriginalFilename();
-            String extension = getFileExtension(originalFilename);
-            String fileName = UUID.randomUUID().toString() + "." + extension;
-            //上传图片
-            PutObjectRequest putObjectRequest = new PutObjectRequest(
-                    bucketName, fileName, file.getInputStream()
-            );
-            ossClient.putObject(putObjectRequest);
+            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);
 
 
-            // 拼接文件访问URL
-            String endpointWithoutProtocol = endpoint.replaceFirst("^https?://", "");
-            String imageUrl = "https://" + bucketName + "." + endpointWithoutProtocol + "/" + fileName;
+            // 创建文件元数据
+            ObjectMetadata metadata = createMetadata(originalFilename, contentType, fileSize, fileCategory);
 
 
-            System.err.println(imageUrl);
-            // 返回文件访问URL
-            return imageUrl;
+            // 上传文件
+            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);
 
 
-        }catch (IOException e){
-            e.printStackTrace();
+            return fileUrl;
+        } catch (Exception e) {
+            log.error("文件上传失败: {}", e.getMessage(), e);
+            throw new IOException("文件上传失败: " + e.getMessage(), e);
         } finally {
         } finally {
-            // 关闭OSSClient
-            ossClient.shutdown();
+            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;
         }
         }
-        return null;
+
+        // 构建标准OSS域名
+        String domain = endpoint.startsWith("http") ?
+                endpoint : "https://" + endpoint;
+
+        return domain.replaceFirst("://", "://" + bucketName + ".") + "/" + fileName;
     }
     }
-    private String getFileExtension(String fileName) {
-        if (fileName == null || !fileName.contains(".")) {
-            return "jpg";
+
+    // 文件类型信息辅助类
+    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;
         }
         }
-        return fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();
     }
     }
+
+
 }
 }

+ 1 - 0
src/main/java/com/zhentao/user/service/impl/UserLoginServiceImpl.java

@@ -229,6 +229,7 @@ public class UserLoginServiceImpl extends ServiceImpl<UserLoginMapper, UserLogin
                 Map<String,Object> map = new HashMap<>();
                 Map<String,Object> map = new HashMap<>();
                 map.put("token",jwtToken);
                 map.put("token",jwtToken);
                 map.put("userId",one.getId()+"");
                 map.put("userId",one.getId()+"");
+                map.put("image",one.getAvatar());
                 return Result.OK(map,"登录成功");
                 return Result.OK(map,"登录成功");
             }else {
             }else {
                 // 如果获取锁超时,返回错误信息
                 // 如果获取锁超时,返回错误信息