云文件系统初始化

This commit is contained in:
2026-04-01 22:39:11 +08:00
commit 3a20f6e7ed
74 changed files with 8693 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
package com.filesystem;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("com.filesystem.mapper")
public class FileSystemApplication {
public static void main(String[] args) {
SpringApplication.run(FileSystemApplication.class, args);
}
}

View File

@@ -0,0 +1,35 @@
package com.filesystem.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.LocalDateTime;
@Configuration
public class MyBatisPlusConfig implements MetaObjectHandler {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
@Override
public void insertFill(MetaObject metaObject) {
// MyBatis-Plus 使用属性名,不是数据库列名
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
this.strictInsertFill(metaObject, "isDeleted", Integer.class, 0);
}
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
}
}

View File

@@ -0,0 +1,24 @@
package com.filesystem.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
template.afterPropertiesSet();
return template;
}
}

View File

@@ -0,0 +1,117 @@
package com.filesystem.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.filesystem.security.JwtAuthenticationFilter;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.io.IOException;
import java.util.Arrays;
import java.util.Map;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Resource
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/login", "/api/auth/register", "/api/auth/logout").permitAll()
.requestMatchers("/api/files/test").permitAll()
.requestMatchers("/ws/**").permitAll()
.requestMatchers("/files/avatar/**").permitAll()
.requestMatchers("/api/files/avatar/**").permitAll()
.requestMatchers("/api/messages/file/**").permitAll()
.anyRequest().authenticated()
)
.exceptionHandling(exception -> exception
.authenticationEntryPoint(authenticationEntryPoint())
.accessDeniedHandler(accessDeniedHandler())
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
/**
* 未认证处理:返回 401 JSON不跳转
*/
@Bean
public AuthenticationEntryPoint authenticationEntryPoint() {
return new AuthenticationEntryPoint() {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
objectMapper.writeValue(response.getWriter(),
Map.of("code", 401, "message", "未登录或登录已过期"));
}
};
}
/**
* 无权限处理:返回 403 JSON
*/
@Bean
public AccessDeniedHandler accessDeniedHandler() {
return new AccessDeniedHandler() {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
org.springframework.security.access.AccessDeniedException accessDeniedException) throws IOException {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
objectMapper.writeValue(response.getWriter(),
Map.of("code", 403, "message", "无权限访问"));
}
};
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOriginPatterns(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setAllowCredentials(true);
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}

View File

@@ -0,0 +1,30 @@
package com.filesystem.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.nio.file.Paths;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Value("${file.upload-dir:./uploads}")
private String uploadDir;
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("*")
.allowedHeaders("*");
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/uploads/**")
.addResourceLocations("file:" + Paths.get(uploadDir).toAbsolutePath() + "/");
}
}

View File

@@ -0,0 +1,44 @@
package com.filesystem.config;
import com.filesystem.security.JwtUtil;
import jakarta.annotation.Resource;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
import java.util.Map;
@Component
public class WebSocketAuthInterceptor implements HandshakeInterceptor {
@Resource
private JwtUtil jwtUtil;
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
if (request instanceof ServletServerHttpRequest servletRequest) {
String token = servletRequest.getServletRequest().getParameter("token");
if (token != null) {
try {
Long userId = jwtUtil.getUserIdFromToken(token);
if (userId != null) {
attributes.put("userId", userId.toString());
return true;
}
} catch (Exception e) {
// JWT 解析失败
}
}
}
return false;
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Exception exception) {
}
}

View File

@@ -0,0 +1,26 @@
package com.filesystem.config;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import com.filesystem.websocket.ChatHandler;
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Resource
private ChatHandler chatHandler;
@Resource
private WebSocketAuthInterceptor authInterceptor;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(chatHandler, "/ws/chat")
.addInterceptors(authInterceptor)
.setAllowedOrigins("*");
}
}

View File

@@ -0,0 +1,112 @@
package com.filesystem.controller;
import com.filesystem.entity.User;
import com.filesystem.security.UserPrincipal;
import com.filesystem.service.UserService;
import jakarta.annotation.Resource;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Resource
private UserService userService;
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody Map<String, String> request) {
String username = request.get("username");
String password = request.get("password");
try {
String token = userService.login(username, password);
User user = userService.findByUsername(username);
// 精确重算存储空间
long storageUsed = userService.recalculateStorage(user.getId());
long storageLimit = user.getStorageLimit() != null ? user.getStorageLimit() : 20L * 1024 * 1024 * 1024;
Map<String, Object> userData = new HashMap<>();
userData.put("id", user.getId());
userData.put("username", user.getUsername());
userData.put("nickname", user.getNickname() != null ? user.getNickname() : "");
userData.put("signature", user.getSignature() != null ? user.getSignature() : "");
userData.put("avatar", user.getAvatar() != null ? user.getAvatar() : "");
userData.put("phone", user.getPhone() != null ? user.getPhone() : "");
userData.put("email", user.getEmail() != null ? user.getEmail() : "");
userData.put("storageUsed", storageUsed);
userData.put("storageLimit", storageLimit);
Map<String, Object> result = new HashMap<>();
result.put("token", token);
result.put("user", userData);
Map<String, Object> body = new HashMap<>();
body.put("data", result);
body.put("message", "登录成功");
return ResponseEntity.ok(body);
} catch (Exception e) {
return ResponseEntity.status(401).body(Map.of("message", e.getMessage()));
}
}
@PostMapping("/register")
public ResponseEntity<?> register(@RequestBody Map<String, String> request) {
String username = request.get("username");
String password = request.get("password");
String nickname = request.get("nickname");
if (userService.findByUsername(username) != null) {
return ResponseEntity.badRequest().body(Map.of("message", "用户名已存在"));
}
User user = userService.createUser(username, password, nickname != null ? nickname : username);
return ResponseEntity.ok(Map.of("message", "注册成功", "data", Map.of("id", user.getId())));
}
@PostMapping("/logout")
public ResponseEntity<?> logout(@AuthenticationPrincipal UserPrincipal principal) {
if (principal != null) {
userService.logout(principal.getUserId());
}
return ResponseEntity.ok(Map.of("message", "退出成功"));
}
@GetMapping("/info")
public ResponseEntity<?> getUserInfo(@AuthenticationPrincipal UserPrincipal principal) {
if (principal == null) {
return ResponseEntity.status(401).body(Map.of("message", "未登录"));
}
User user = userService.findById(principal.getUserId());
if (user == null) {
return ResponseEntity.status(401).body(Map.of("message", "用户不存在"));
}
// 精确重算存储空间
long storageUsed = userService.recalculateStorage(user.getId());
long storageLimit = user.getStorageLimit() != null ? user.getStorageLimit() : 20L * 1024 * 1024 * 1024;
Map<String, Object> userData = new HashMap<>();
userData.put("id", user.getId());
userData.put("username", user.getUsername());
userData.put("nickname", user.getNickname() != null ? user.getNickname() : "");
userData.put("signature", user.getSignature() != null ? user.getSignature() : "");
userData.put("avatar", user.getAvatar() != null ? user.getAvatar() : "");
userData.put("email", user.getEmail() != null ? user.getEmail() : "");
userData.put("phone", user.getPhone() != null ? user.getPhone() : "");
userData.put("storageUsed", storageUsed);
userData.put("storageLimit", storageLimit);
Map<String, Object> body = new HashMap<>();
body.put("data", userData);
return ResponseEntity.ok(body);
}
}

View File

@@ -0,0 +1,254 @@
package com.filesystem.controller;
import com.filesystem.entity.FileEntity;
import com.filesystem.entity.FileShare;
import com.filesystem.security.UserPrincipal;
import com.filesystem.service.FileService;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/files")
public class FileController {
@Resource
private FileService fileService;
@Value("${file.storage.path:./uploads}")
private String storagePath;
public FileController() {
}
@GetMapping("/test")
public ResponseEntity<?> test() {
return ResponseEntity.ok(Map.of("message", "Backend is running", "timestamp", System.currentTimeMillis()));
}
@GetMapping
public ResponseEntity<?> getFiles(
@AuthenticationPrincipal UserPrincipal principal,
@RequestParam(required = false) Long folderId,
@RequestParam(required = false) String keyword) {
List<FileEntity> files = fileService.getFiles(principal.getUserId(), folderId, keyword);
return ResponseEntity.ok(Map.of("data", files));
}
@GetMapping("/trashFiles")
public ResponseEntity<?> getTrashFiles(
@AuthenticationPrincipal UserPrincipal principal,
@RequestParam(required = false) Long folderId) {
return ResponseEntity.ok(Map.of("data", fileService.getTrashFiles(principal.getUserId(), folderId)));
}
@GetMapping("/sharedByMe")
public ResponseEntity<?> getSharedByMe(@AuthenticationPrincipal UserPrincipal principal) {
return ResponseEntity.ok(Map.of("data", fileService.getSharedByMe(principal.getUserId())));
}
@GetMapping("/sharedByMe/folder")
public ResponseEntity<?> getSharedByMeFolderFiles(
@AuthenticationPrincipal UserPrincipal principal,
@RequestParam Long folderId) {
List<FileEntity> files = fileService.getSharedByMeFolderFiles(principal.getUserId(), folderId);
return ResponseEntity.ok(Map.of("data", files));
}
@GetMapping("/sharedToMe")
public ResponseEntity<?> getSharedToMe(@AuthenticationPrincipal UserPrincipal principal) {
return ResponseEntity.ok(Map.of("data", fileService.getSharedToMe(principal.getUserId())));
}
@GetMapping("/sharedToMe/folder")
public ResponseEntity<?> getSharedFolderFiles(
@AuthenticationPrincipal UserPrincipal principal,
@RequestParam Long folderId) {
List<FileEntity> files = fileService.getSharedFolderFiles(principal.getUserId(), folderId);
return ResponseEntity.ok(Map.of("data", files));
}
@PostMapping("/uploadBatch")
public ResponseEntity<?> uploadFiles(
@AuthenticationPrincipal UserPrincipal principal,
@RequestParam("files") List<MultipartFile> files,
@RequestParam(required = false) Long folderId) throws IOException {
List<FileEntity> uploaded = fileService.uploadFiles(files, principal.getUserId(), folderId);
return ResponseEntity.ok(Map.of("data", uploaded, "message", "上传成功"));
}
@PostMapping("/createFolder")
public ResponseEntity<?> createFolder(
@AuthenticationPrincipal UserPrincipal principal,
@RequestBody Map<String, Object> request) {
String name = (String) request.get("name");
Long parentId = request.get("parentId") != null ? Long.valueOf(request.get("parentId").toString()) : null;
FileEntity folder = fileService.createFolder(name, principal.getUserId(), parentId);
return ResponseEntity.ok(Map.of("data", folder, "message", "创建成功"));
}
@DeleteMapping("/{id}")
public ResponseEntity<?> deleteFile(
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable Long id) {
fileService.moveToTrash(id, principal.getUserId());
return ResponseEntity.ok(Map.of("message", "已移至回收站"));
}
@PostMapping("/{id}/restore")
public ResponseEntity<?> restoreFile(
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable Long id) {
fileService.restoreFile(id, principal.getUserId());
return ResponseEntity.ok(Map.of("message", "已还原"));
}
@DeleteMapping("/{id}/deletePermanent")
public ResponseEntity<?> deletePermanently(
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable Long id) {
fileService.deletePermanently(id, principal.getUserId());
return ResponseEntity.ok(Map.of("message", "已彻底删除"));
}
@DeleteMapping("/emptyTrash")
public ResponseEntity<?> emptyTrash(@AuthenticationPrincipal UserPrincipal principal) {
fileService.emptyTrash(principal.getUserId());
return ResponseEntity.ok(Map.of("message", "已清空回收站"));
}
@GetMapping("/{id}/download")
public ResponseEntity<byte[]> downloadFile(
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable Long id) throws IOException {
FileEntity file = fileService.getById(id);
if (file == null) return ResponseEntity.notFound().build();
byte[] content = fileService.getFileContent(file);
if (content == null) return ResponseEntity.notFound().build();
String encodedName = URLEncoder.encode(file.getName(), StandardCharsets.UTF_8).replace("+", "%20");
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + encodedName + "\"")
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(content);
}
@GetMapping("/{id}/preview")
public ResponseEntity<byte[]> previewFile(
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable Long id) throws IOException {
FileEntity file = fileService.getById(id);
if (file == null) return ResponseEntity.notFound().build();
byte[] content = fileService.getFileContent(file);
if (content == null) return ResponseEntity.notFound().build();
String encodedName = URLEncoder.encode(file.getName(), StandardCharsets.UTF_8).replace("+", "%20");
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + encodedName + "\"")
.contentType(getMediaType(file.getName()))
.body(content);
}
@PostMapping("/{id}/shareFile")
public ResponseEntity<?> shareFile(
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable Long id,
@RequestBody Map<String, Object> request) {
Long shareToUserId = Long.valueOf(request.get("userId").toString());
String permission = (String) request.getOrDefault("permission", "view");
FileShare share = fileService.shareFile(id, principal.getUserId(), shareToUserId, permission);
return ResponseEntity.ok(Map.of("data", share, "message", "共享成功"));
}
@DeleteMapping("/{id}/cancelShare")
public ResponseEntity<?> cancelShare(
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable Long id) {
fileService.cancelShare(id, principal.getUserId());
return ResponseEntity.ok(Map.of("message", "已取消共享"));
}
/**
* 获取头像图片
*/
@GetMapping("/avatar/**")
public ResponseEntity<byte[]> getAvatar(jakarta.servlet.http.HttpServletRequest request) throws IOException {
String uri = request.getRequestURI();
String prefix = "/api/files/avatar/";
String relativePath = uri.substring(uri.indexOf(prefix) + prefix.length());
// relativePath = "2026/04/xxx.jpg"
Path filePath = Paths.get(storagePath).toAbsolutePath().resolve("avatars").resolve(relativePath);
if (!Files.exists(filePath)) {
return ResponseEntity.notFound().build();
}
byte[] content = Files.readAllBytes(filePath);
String fileName = relativePath.contains("/") ? relativePath.substring(relativePath.lastIndexOf("/") + 1) : relativePath;
String ext = fileName.contains(".") ? fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase() : "jpg";
String contentType = switch (ext) {
case "png" -> "image/png";
case "gif" -> "image/gif";
case "webp" -> "image/webp";
default -> "image/jpeg";
};
return ResponseEntity.ok()
.contentType(org.springframework.http.MediaType.parseMediaType(contentType))
.header(HttpHeaders.CACHE_CONTROL, "max-age=86400")
.body(content);
}
private MediaType getMediaType(String filename) {
String ext = filename.contains(".") ? filename.substring(filename.lastIndexOf(".")).toLowerCase() : "";
return switch (ext) {
case ".jpg", ".jpeg" -> MediaType.IMAGE_JPEG;
case ".png" -> MediaType.IMAGE_PNG;
case ".gif" -> MediaType.IMAGE_GIF;
case ".pdf" -> MediaType.APPLICATION_PDF;
default -> MediaType.APPLICATION_OCTET_STREAM;
};
}
@PutMapping("/{id}/rename")
public ResponseEntity<?> renameFile(
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable Long id,
@RequestBody Map<String, String> request) {
String newName = request.get("name");
if (newName == null || newName.trim().isEmpty()) {
return ResponseEntity.badRequest().body(Map.of("message", "名称不能为空"));
}
try {
fileService.renameFile(id, principal.getUserId(), newName.trim());
return ResponseEntity.ok(Map.of("message", "重命名成功"));
} catch (RuntimeException e) {
return ResponseEntity.badRequest().body(Map.of("message", e.getMessage()));
}
}
@PutMapping("/{id}/move")
public ResponseEntity<?> moveFile(
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable Long id,
@RequestBody Map<String, Long> request) {
Long folderId = request.get("folderId");
try {
fileService.moveFile(id, principal.getUserId(), folderId);
return ResponseEntity.ok(Map.of("message", "移动成功"));
} catch (RuntimeException e) {
return ResponseEntity.badRequest().body(Map.of("message", e.getMessage()));
}
}
}
}

View File

@@ -0,0 +1,279 @@
package com.filesystem.controller;
import com.filesystem.entity.Message;
import com.filesystem.entity.User;
import com.filesystem.security.UserPrincipal;
import com.filesystem.service.MessageService;
import com.filesystem.service.UserService;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
import java.util.LinkedHashMap;
@RestController
@RequestMapping("/api/messages")
public class MessageController {
@Resource
private MessageService messageService;
@Resource
private UserService userService;
@Value("${file.storage.path:./uploads}")
private String storagePath;
private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy/MM");
// ==================== 聊天文件上传 ====================
@PostMapping("/upload")
public ResponseEntity<?> uploadChatFile(
@AuthenticationPrincipal UserPrincipal principal,
@RequestParam("file") MultipartFile file) throws IOException {
if (file.isEmpty()) {
return ResponseEntity.badRequest().body(Map.of("message", "请选择文件"));
}
String originalName = file.getOriginalFilename();
String ext = originalName != null && originalName.contains(".")
? originalName.substring(originalName.lastIndexOf(".")) : "";
String storedName = UUID.randomUUID().toString() + ext;
String datePath = LocalDateTime.now().format(DATE_FMT);
Path targetDir = Paths.get(storagePath).toAbsolutePath().resolve("chat").resolve(datePath);
if (!Files.exists(targetDir)) {
Files.createDirectories(targetDir);
}
Path filePath = targetDir.resolve(storedName);
file.transferTo(filePath.toFile());
String fileUrl = "/api/messages/file/" + datePath + "/" + storedName;
return ResponseEntity.ok(Map.of("url", fileUrl, "message", "上传成功"));
}
// ==================== 聊天文件访问 ====================
@GetMapping("/file/**")
public ResponseEntity<byte[]> getChatFile(HttpServletRequest request) throws IOException {
String uri = request.getRequestURI();
String prefix = "/api/messages/file/";
String relativePath = uri.substring(uri.indexOf(prefix) + prefix.length());
// relativePath = "2026/04/xxx.jpg"
int lastSlash = relativePath.lastIndexOf('/');
String datePath = relativePath.substring(0, lastSlash);
String fileName = relativePath.substring(lastSlash + 1);
Path filePath = Paths.get(storagePath).toAbsolutePath()
.resolve("chat").resolve(datePath).resolve(fileName);
if (!Files.exists(filePath)) {
return ResponseEntity.notFound().build();
}
byte[] content = Files.readAllBytes(filePath);
String ext = fileName.contains(".") ? fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase() : "";
boolean isImage = Set.of("png", "jpg", "jpeg", "gif", "webp", "bmp", "svg").contains(ext);
String contentType = switch (ext) {
case "pdf" -> "application/pdf";
case "png" -> "image/png";
case "gif" -> "image/gif";
case "webp" -> "image/webp";
case "jpg", "jpeg" -> "image/jpeg";
case "bmp" -> "image/bmp";
case "svg" -> "image/svg+xml";
default -> "application/octet-stream";
};
String encodedName = URLEncoder.encode(fileName, StandardCharsets.UTF_8).replace("+", "%20");
String disposition = isImage
? "inline; filename=\"" + encodedName + "\""
: "attachment; filename=\"" + encodedName + "\"";
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(contentType))
.header(HttpHeaders.CONTENT_DISPOSITION, disposition)
.header(HttpHeaders.CACHE_CONTROL, "max-age=86400")
.body(content);
}
// ==================== 消息收发 ====================
@GetMapping
public ResponseEntity<?> getMessages(
@AuthenticationPrincipal UserPrincipal principal,
@RequestParam Long userId) {
List<Message> messages = messageService.getMessages(principal.getUserId(), userId);
for (Message msg : messages) {
if (msg.getToUserId().equals(principal.getUserId()) && msg.getIsRead() == 0) {
msg.setIsRead(1);
messageService.updateMessage(msg);
}
}
Set<Long> userIds = new HashSet<>();
userIds.add(principal.getUserId());
userIds.add(userId);
for (Message msg : messages) {
userIds.add(msg.getFromUserId());
userIds.add(msg.getToUserId());
}
Map<Long, User> userMap = new HashMap<>();
for (Long uid : userIds) {
User u = userService.findById(uid);
if (u != null) userMap.put(uid, u);
}
List<Map<String, Object>> result = messages.stream().map(msg -> {
Map<String, Object> m = new HashMap<>();
m.put("id", msg.getId());
m.put("fromUserId", msg.getFromUserId());
m.put("toUserId", msg.getToUserId());
m.put("content", msg.getContent());
m.put("type", msg.getType());
m.put("fileName", msg.getFileName());
m.put("fileSize", msg.getFileSize());
m.put("isRead", msg.getIsRead());
m.put("createTime", msg.getCreateTime() != null ? msg.getCreateTime().toString() : "");
User fromUser = userMap.get(msg.getFromUserId());
if (fromUser != null) {
m.put("fromUsername", fromUser.getUsername());
m.put("fromNickname", fromUser.getNickname() != null ? fromUser.getNickname() : fromUser.getUsername());
m.put("fromAvatar", fromUser.getAvatar());
m.put("fromSignature", fromUser.getSignature());
}
return m;
}).collect(Collectors.toList());
return ResponseEntity.ok(Map.of("data", result));
}
@GetMapping("/users")
public ResponseEntity<?> getUsers(@AuthenticationPrincipal UserPrincipal principal) {
List<User> users = userService.getAllUsersExcept(principal.getUserId());
List<Map<String, Object>> result = users.stream()
.map(u -> {
Map<String, Object> m = new HashMap<>();
m.put("id", u.getId());
m.put("username", u.getUsername());
m.put("nickname", u.getNickname() != null ? u.getNickname() : u.getUsername());
m.put("avatar", u.getAvatar());
m.put("signature", u.getSignature());
m.put("email", u.getEmail());
m.put("phone", u.getPhone());
return m;
})
.collect(Collectors.toList());
return ResponseEntity.ok(Map.of("data", result));
}
@PostMapping
public ResponseEntity<?> sendMessage(
@AuthenticationPrincipal UserPrincipal principal,
@RequestBody Map<String, Object> request) {
Long toUserId = Long.valueOf(request.get("toUserId").toString());
String content = (String) request.get("content");
String type = (String) request.getOrDefault("type", "text");
String fileName = (String) request.get("fileName");
Object fileSizeObj = request.get("fileSize");
Long fileSize = fileSizeObj != null ? Long.valueOf(fileSizeObj.toString()) : null;
Message message = messageService.sendMessage(principal.getUserId(), toUserId, content, type);
if (("file".equals(type) || "image".equals(type)) && fileName != null) {
message.setFileName(fileName);
message.setFileSize(fileSize);
messageService.updateMessage(message);
}
Map<String, Object> result = new HashMap<>();
result.put("id", message.getId());
result.put("fromUserId", message.getFromUserId());
result.put("toUserId", message.getToUserId());
result.put("content", message.getContent());
result.put("type", message.getType());
result.put("fileName", message.getFileName());
result.put("fileSize", message.getFileSize());
result.put("createTime", message.getCreateTime() != null ? message.getCreateTime().toString() : "");
User fromUser = userService.findById(principal.getUserId());
if (fromUser != null) {
result.put("fromUsername", fromUser.getUsername());
result.put("fromNickname", fromUser.getNickname() != null ? fromUser.getNickname() : fromUser.getUsername());
result.put("fromSignature", fromUser.getSignature());
}
return ResponseEntity.ok(Map.of("data", result, "message", "发送成功"));
}
@GetMapping("/unreadCount")
public ResponseEntity<?> getUnreadCount(@AuthenticationPrincipal UserPrincipal principal) {
int count = messageService.getUnreadCount(principal.getUserId());
return ResponseEntity.ok(Map.of("data", Map.of("count", count)));
}
@GetMapping("/unreadList")
public ResponseEntity<?> getUnreadList(@AuthenticationPrincipal UserPrincipal principal) {
List<Message> unreadMessages = messageService.getUnreadMessages(principal.getUserId());
// 按发送人分组
Map<Long, List<Message>> grouped = unreadMessages.stream()
.collect(Collectors.groupingBy(Message::getFromUserId, LinkedHashMap::new, Collectors.toList()));
List<Map<String, Object>> result = new ArrayList<>();
for (Map.Entry<Long, List<Message>> entry : grouped.entrySet()) {
Long fromUserId = entry.getKey();
List<Message> msgs = entry.getValue();
Message lastMsg = msgs.get(0); // 已按时间倒序,第一条就是最新的
User fromUser = userService.findById(fromUserId);
Map<String, Object> item = new HashMap<>();
item.put("userId", fromUserId);
item.put("unread", msgs.size());
item.put("lastMsg", lastMsg.getType() != null && lastMsg.getType().startsWith("image")
? "[图片]" : (lastMsg.getType() != null && lastMsg.getType().equals("file")
? "[文件]" : (lastMsg.getContent() != null && lastMsg.getContent().length() > 30
? lastMsg.getContent().substring(0, 30) + "..." : lastMsg.getContent())));
item.put("lastTime", lastMsg.getCreateTime() != null ? lastMsg.getCreateTime().toString() : "");
if (fromUser != null) {
item.put("username", fromUser.getUsername());
item.put("nickname", fromUser.getNickname() != null ? fromUser.getNickname() : fromUser.getUsername());
item.put("avatar", fromUser.getAvatar());
item.put("signature", fromUser.getSignature());
}
result.add(item);
}
return ResponseEntity.ok(Map.of("data", result));
}
@PostMapping("/{id}/read")
public ResponseEntity<?> markAsRead(@PathVariable Long id) {
messageService.markAsRead(id);
return ResponseEntity.ok(Map.of("message", "已标记已读"));
}
}

View File

@@ -0,0 +1,125 @@
package com.filesystem.controller;
import com.filesystem.entity.User;
import com.filesystem.security.UserPrincipal;
import com.filesystem.service.UserService;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/api/users")
public class UserController {
@Resource
private UserService userService;
@Value("${file.storage.path:./uploads}")
private String storagePath;
/**
* 获取所有可用用户(用于文件共享等场景)
*/
@GetMapping
public ResponseEntity<?> getAllUsers(@AuthenticationPrincipal UserPrincipal principal) {
List<User> users = userService.getAllUsersExcept(principal.getUserId());
List<Map<String, Object>> result = users.stream()
.filter(u -> u.getStatus() == 1) // 只返回启用状态的用户
.map(u -> {
Map<String, Object> m = new java.util.HashMap<>();
m.put("id", u.getId());
m.put("username", u.getUsername());
m.put("nickname", u.getNickname() != null ? u.getNickname() : u.getUsername());
m.put("avatar", u.getAvatar());
m.put("signature", u.getSignature());
return m;
})
.collect(Collectors.toList());
return ResponseEntity.ok(Map.of("data", result));
}
/**
* 上传头像
*/
@PostMapping("/avatar")
public ResponseEntity<?> uploadAvatar(
@AuthenticationPrincipal UserPrincipal principal,
@RequestParam("avatar") MultipartFile file) throws IOException {
if (file.isEmpty()) {
return ResponseEntity.badRequest().body(Map.of("message", "请选择图片"));
}
// 限制文件大小 2MB
if (file.getSize() > 2 * 1024 * 1024) {
return ResponseEntity.badRequest().body(Map.of("message", "图片大小不能超过2MB"));
}
// 保存文件
String originalFilename = file.getOriginalFilename();
String ext = originalFilename != null && originalFilename.contains(".")
? originalFilename.substring(originalFilename.lastIndexOf("."))
: ".jpg";
String fileName = UUID.randomUUID().toString() + ext;
String datePath = java.time.LocalDateTime.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyy/MM"));
// 使用配置文件中的路径 + 日期目录
Path uploadPath = Paths.get(storagePath).toAbsolutePath().resolve("avatars").resolve(datePath);
if (!Files.exists(uploadPath)) {
Files.createDirectories(uploadPath);
}
Path filePath = uploadPath.resolve(fileName);
Files.copy(file.getInputStream(), filePath);
// 更新用户头像
String avatarUrl = "/api/files/avatar/" + datePath + "/" + fileName;
userService.updateAvatar(principal.getUserId(), avatarUrl);
return ResponseEntity.ok(Map.of("data", Map.of("url", avatarUrl), "message", "上传成功"));
}
/**
* 获取当前用户信息
*/
@GetMapping("/me")
public ResponseEntity<?> getCurrentUser(@AuthenticationPrincipal UserPrincipal principal) {
User user = userService.findById(principal.getUserId());
Map<String, Object> result = new java.util.HashMap<>();
result.put("id", user.getId());
result.put("username", user.getUsername());
result.put("nickname", user.getNickname());
result.put("avatar", user.getAvatar());
result.put("signature", user.getSignature());
result.put("email", user.getEmail());
result.put("phone", user.getPhone());
return ResponseEntity.ok(Map.of("data", result));
}
/**
* 更新个人信息
*/
@PutMapping("/profile")
public ResponseEntity<?> updateProfile(
@AuthenticationPrincipal UserPrincipal principal,
@RequestBody Map<String, String> request) {
String nickname = request.get("nickname");
String signature = request.get("signature");
String phone = request.get("phone");
String email = request.get("email");
userService.updateProfile(principal.getUserId(), nickname, signature, phone, email);
return ResponseEntity.ok(Map.of("message", "更新成功"));
}
}

View File

@@ -0,0 +1,21 @@
package com.filesystem.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class BaseEntity {
@TableId(type = IdType.AUTO)
private Long id;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@TableField("is_deleted")
private Integer isDeleted;
}

View File

@@ -0,0 +1,34 @@
package com.filesystem.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName(value = "sys_file", autoResultMap = true)
public class FileEntity extends BaseEntity {
private String name;
private String type;
private Long size;
private String path;
@TableField("folder_id")
private Long folderId;
@TableField("user_id")
private Long userId;
@TableField("is_folder")
private Integer isFolder;
@TableField("is_shared")
private Integer isShared;
@TableField("deleted_at")
private String deletedAt;
}

View File

@@ -0,0 +1,16 @@
package com.filesystem.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("sys_file_share")
public class FileShare extends BaseEntity {
private Long fileId;
private Long ownerId;
private Long shareToUserId;
private String permission;
}

View File

@@ -0,0 +1,30 @@
package com.filesystem.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName(value = "sys_message", autoResultMap = true)
public class Message extends BaseEntity {
@TableField("from_user_id")
private Long fromUserId;
@TableField("to_user_id")
private Long toUserId;
private String content;
private String type; // text, image, file, emoji
@TableField("file_name")
private String fileName;
@TableField("file_size")
private Long fileSize;
@TableField("is_read")
private Integer isRead;
}

View File

@@ -0,0 +1,33 @@
package com.filesystem.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName(value = "sys_user", autoResultMap = true)
public class User extends BaseEntity {
private String username;
private String password;
private String nickname;
private String avatar;
private String signature;
private String email;
private String phone;
private Integer status;
@TableField("storage_used")
private Long storageUsed;
@TableField("storage_limit")
private Long storageLimit;
}

View File

@@ -0,0 +1,9 @@
package com.filesystem.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.filesystem.entity.FileEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface FileMapper extends BaseMapper<FileEntity> {
}

View File

@@ -0,0 +1,9 @@
package com.filesystem.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.filesystem.entity.FileShare;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface FileShareMapper extends BaseMapper<FileShare> {
}

View File

@@ -0,0 +1,9 @@
package com.filesystem.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.filesystem.entity.Message;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface MessageMapper extends BaseMapper<Message> {
}

View File

@@ -0,0 +1,9 @@
package com.filesystem.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.filesystem.entity.User;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper extends BaseMapper<User> {
}

View File

@@ -0,0 +1,82 @@
package com.filesystem.security;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.annotation.Resource;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Resource
private JwtUtil jwtUtil;
private static final List<AntPathRequestMatcher> EXCLUDE_MATCHERS = List.of(
new AntPathRequestMatcher("/api/auth/login"),
new AntPathRequestMatcher("/api/auth/register"),
new AntPathRequestMatcher("/api/auth/logout"),
new AntPathRequestMatcher("/api/files/test"),
new AntPathRequestMatcher("/ws/**"),
new AntPathRequestMatcher("/api/files/avatar/**"),
new AntPathRequestMatcher("/api/messages/file/**")
);
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
for (AntPathRequestMatcher matcher : EXCLUDE_MATCHERS) {
if (matcher.matches(request)) return true;
}
return false;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
String token = null;
if (authHeader != null && authHeader.startsWith("Bearer ")) {
token = authHeader.substring(7);
} else {
token = request.getParameter("token");
}
if (token != null && !token.isEmpty()) {
try {
if (jwtUtil.validateToken(token)) {
String username = jwtUtil.getUsernameFromToken(token);
Long userId = jwtUtil.getUserIdFromToken(token);
UserPrincipal principal = new UserPrincipal(userId, username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(principal, null, new ArrayList<>());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception e) {
// token 无效,清除认证上下文,继续放行(由 Security 决定是否拦截)
SecurityContextHolder.clearContext();
}
}
filterChain.doFilter(request, response);
}
}

View File

@@ -0,0 +1,130 @@
package com.filesystem.security;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Component;
import jakarta.annotation.PostConstruct;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.concurrent.TimeUnit;
@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration:86400000}")
private Long expiration;
@Resource
private RedisTemplate<String, Object> redisTemplate;
private SecretKey signingKey;
@PostConstruct
public void init() {
// 确保 secret 至少 256 位 (32 字节)
String paddedSecret = secret;
while (paddedSecret.getBytes(StandardCharsets.UTF_8).length < 32) {
paddedSecret = paddedSecret + secret;
}
this.signingKey = Keys.hmacShaKeyFor(paddedSecret.getBytes(StandardCharsets.UTF_8));
// 启动时清空所有 token强制重新登录
try {
var keys = redisTemplate.keys("token:*");
if (keys != null && !keys.isEmpty()) {
redisTemplate.delete(keys);
}
} catch (Exception e) {
System.out.println("Redis unavailable on startup, skip token cleanup");
}
}
private SecretKey getSigningKey() {
return signingKey;
}
public String generateToken(String username, Long userId) {
String token = Jwts.builder()
.subject(username)
.claim("userId", userId)
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + expiration))
.signWith(getSigningKey())
.compact();
// 存储到 Redis
try {
String key = "token:" + userId;
redisTemplate.opsForValue().set(key, token, expiration, TimeUnit.MILLISECONDS);
} catch (Exception e) {
// Redis 不可用时忽略,继续返回 token
System.out.println("Redis unavailable, token stored in memory only");
}
return token;
}
public String getUsernameFromToken(String token) {
return Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload()
.getSubject();
}
public Long getUserIdFromToken(String token) {
return Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload()
.get("userId", Long.class);
}
public boolean validateToken(String token) {
try {
// 先验证 JWT 签名
Claims claims = Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
// 如果 JWT 已过期,直接返回 false
if (claims.getExpiration().before(new Date())) {
return false;
}
// 检查 Redis 中的 token 是否匹配
try {
Long userId = getUserIdFromToken(token);
String key = "token:" + userId;
Object storedToken = redisTemplate.opsForValue().get(key);
return storedToken != null && storedToken.equals(token);
} catch (Exception e) {
// Redis 不可用时,只要 JWT 本身未过期就返回 true
return true;
}
} catch (Exception e) {
return false;
}
}
public void invalidateToken(Long userId) {
try {
String key = "token:" + userId;
redisTemplate.delete(key);
} catch (Exception e) {
System.out.println("Redis unavailable, cannot invalidate token");
}
}
}

View File

@@ -0,0 +1,11 @@
package com.filesystem.security;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class UserPrincipal {
private Long userId;
private String username;
}

View File

@@ -0,0 +1,502 @@
package com.filesystem.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.filesystem.entity.FileEntity;
import com.filesystem.entity.FileShare;
import com.filesystem.mapper.FileMapper;
import com.filesystem.mapper.FileShareMapper;
import com.filesystem.mapper.UserMapper;
import com.filesystem.entity.User;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
@Service
public class FileService {
@Resource
private FileMapper fileMapper;
@Resource
private FileShareMapper fileShareMapper;
@Resource
private UserMapper userMapper;
@Resource
private UserService userService;
@Value("${file.storage.path:./uploads}")
private String storagePath;
public List<FileEntity> getFiles(Long userId, Long folderId, String keyword) {
LambdaQueryWrapper<FileEntity> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(FileEntity::getUserId, userId)
.eq(FileEntity::getIsDeleted, 0);
if (folderId != null) {
wrapper.eq(FileEntity::getFolderId, folderId);
} else {
wrapper.isNull(FileEntity::getFolderId);
}
if (keyword != null && !keyword.isEmpty()) {
wrapper.like(FileEntity::getName, keyword);
}
wrapper.orderByDesc(FileEntity::getIsFolder)
.orderByDesc(FileEntity::getCreateTime);
return fileMapper.selectList(wrapper);
}
public List<FileEntity> getTrashFiles(Long userId, Long folderId) {
LambdaQueryWrapper<FileEntity> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(FileEntity::getUserId, userId)
.eq(FileEntity::getIsDeleted, 1);
if (folderId != null) {
// 查询指定文件夹下的已删除文件
wrapper.eq(FileEntity::getFolderId, folderId);
} else {
// 查询根目录下已删除的文件和文件夹
wrapper.isNull(FileEntity::getFolderId)
.or(w -> w.eq(FileEntity::getFolderId, 0));
}
wrapper.orderByDesc(FileEntity::getDeletedAt);
return fileMapper.selectList(wrapper);
}
public FileEntity getById(Long id) {
return fileMapper.selectById(id);
}
@Transactional
public void moveToTrash(Long id, Long userId) {
FileEntity file = fileMapper.selectById(id);
if (file != null && file.getUserId().equals(userId)) {
// 使用 LambdaUpdateWrapper 明确指定要更新的字段
LambdaUpdateWrapper<FileEntity> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(FileEntity::getId, id)
.set(FileEntity::getIsDeleted, 1)
.set(FileEntity::getDeletedAt, LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
fileMapper.update(null, wrapper);
}
}
@Transactional
public void restoreFile(Long id, Long userId) {
FileEntity file = fileMapper.selectById(id);
if (file == null || !file.getUserId().equals(userId)) return;
LambdaUpdateWrapper<FileEntity> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(FileEntity::getId, id)
.set(FileEntity::getIsDeleted, 0)
.set(FileEntity::getDeletedAt, null);
fileMapper.update(null, wrapper);
// 如果是文件夹,递归还原所有子文件
if (file.getIsFolder() != null && file.getIsFolder() == 1) {
restoreChildren(id, userId);
}
}
private void restoreChildren(Long parentFolderId, Long userId) {
List<FileEntity> children = fileMapper.selectList(
new LambdaQueryWrapper<FileEntity>()
.eq(FileEntity::getFolderId, parentFolderId)
.eq(FileEntity::getIsDeleted, 1)
.eq(FileEntity::getUserId, userId)
);
for (FileEntity child : children) {
LambdaUpdateWrapper<FileEntity> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(FileEntity::getId, child.getId())
.set(FileEntity::getIsDeleted, 0)
.set(FileEntity::getDeletedAt, null);
fileMapper.update(null, wrapper);
if (child.getIsFolder() != null && child.getIsFolder() == 1) {
restoreChildren(child.getId(), userId);
}
}
}
@Transactional
public void deletePermanently(Long id, Long userId) {
FileEntity file = fileMapper.selectById(id);
if (file == null || !file.getUserId().equals(userId)) return;
// 如果是文件夹,先递归删除所有子文件
if (file.getIsFolder() != null && file.getIsFolder() == 1) {
deleteChildrenPermanently(id, userId);
}
// 删除当前文件的物理文件并扣减存储
if (file.getPath() != null && !file.getPath().isEmpty()) {
try {
Path filePath = Paths.get(storagePath).toAbsolutePath().resolve("files").resolve(file.getPath());
Files.deleteIfExists(filePath);
} catch (IOException e) {
// ignore
}
if (file.getSize() != null && file.getSize() > 0) {
userService.decreaseStorage(userId, file.getSize());
}
}
fileMapper.deleteById(id);
}
private void deleteChildrenPermanently(Long parentFolderId, Long userId) {
List<FileEntity> children = fileMapper.selectList(
new LambdaQueryWrapper<FileEntity>()
.eq(FileEntity::getFolderId, parentFolderId)
.eq(FileEntity::getIsDeleted, 1)
.eq(FileEntity::getUserId, userId)
);
for (FileEntity child : children) {
if (child.getIsFolder() != null && child.getIsFolder() == 1) {
deleteChildrenPermanently(child.getId(), userId);
}
if (child.getPath() != null && !child.getPath().isEmpty()) {
try {
Path filePath = Paths.get(storagePath).toAbsolutePath().resolve("files").resolve(child.getPath());
Files.deleteIfExists(filePath);
} catch (IOException e) {
// ignore
}
if (child.getSize() != null && child.getSize() > 0) {
userService.decreaseStorage(userId, child.getSize());
}
}
fileMapper.deleteById(child.getId());
}
}
@Transactional
public void emptyTrash(Long userId) {
List<FileEntity> trashFiles = getTrashFiles(userId, null);
for (FileEntity file : trashFiles) {
deletePermanently(file.getId(), userId);
}
}
@Transactional
public FileEntity createFolder(String name, Long userId, Long parentId) {
FileEntity folder = new FileEntity();
folder.setName(name);
folder.setType("folder");
folder.setIsFolder(1);
folder.setUserId(userId);
folder.setFolderId(parentId);
folder.setSize(0L);
folder.setIsShared(0);
folder.setIsDeleted(0);
fileMapper.insert(folder);
return folder;
}
@Transactional
public List<FileEntity> uploadFiles(List<MultipartFile> files, Long userId, Long folderId) throws IOException {
List<FileEntity> uploadedFiles = new ArrayList<>();
// 确保存储目录存在(使用配置文件中的路径 + files 子目录)
Path uploadDir = Paths.get(storagePath).toAbsolutePath().resolve("files");
if (!Files.exists(uploadDir)) {
Files.createDirectories(uploadDir);
}
for (MultipartFile file : files) {
if (file.isEmpty()) continue;
String originalName = file.getOriginalFilename();
String extension = originalName.contains(".") ? originalName.substring(originalName.lastIndexOf(".")) : "";
String storedName = UUID.randomUUID().toString() + extension;
String datePath = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy/MM"));
Path targetDir = uploadDir.resolve(datePath);
if (!Files.exists(targetDir)) {
Files.createDirectories(targetDir);
}
Path targetPath = targetDir.resolve(storedName);
file.transferTo(targetPath.toFile());
FileEntity fileEntity = new FileEntity();
fileEntity.setName(originalName);
fileEntity.setType(getFileType(extension));
fileEntity.setSize(file.getSize());
fileEntity.setPath(datePath + "/" + storedName);
fileEntity.setUserId(userId);
fileEntity.setFolderId(folderId);
fileEntity.setIsFolder(0);
fileEntity.setIsShared(0);
fileEntity.setIsDeleted(0);
fileMapper.insert(fileEntity);
uploadedFiles.add(fileEntity);
// 更新用户存储空间
userService.updateStorage(userId, file.getSize());
}
return uploadedFiles;
}
private String getFileType(String extension) {
if (extension == null || extension.isEmpty()) return "file";
extension = extension.toLowerCase();
if (".jpg".equals(extension) || ".jpeg".equals(extension) || ".png".equals(extension)
|| ".gif".equals(extension) || ".webp".equals(extension) || ".svg".equals(extension)) {
return "image";
}
if (".mp4".equals(extension) || ".avi".equals(extension) || ".mov".equals(extension)) {
return "video";
}
if (".mp3".equals(extension) || ".wav".equals(extension) || ".flac".equals(extension)) {
return "audio";
}
if (".pdf".equals(extension)) {
return "pdf";
}
return "file";
}
public byte[] getFileContent(FileEntity file) throws IOException {
if (file == null || file.getPath() == null) return null;
Path filePath = Paths.get(storagePath).toAbsolutePath().resolve("files").resolve(file.getPath());
return Files.readAllBytes(filePath);
}
// 共享相关
public List<FileEntity> getSharedByMe(Long userId) {
List<FileShare> shares = fileShareMapper.selectList(
new LambdaQueryWrapper<FileShare>().eq(FileShare::getOwnerId, userId)
);
return shares.stream()
.map(share -> fileMapper.selectById(share.getFileId()))
.filter(f -> f != null && f.getIsDeleted() == 0)
.collect(Collectors.toList());
}
public List<FileEntity> getSharedByMeFolderFiles(Long userId, Long folderId) {
try {
// 检查该文件夹是否由当前用户共享
FileEntity folder = fileMapper.selectById(folderId);
if (folder == null || folder.getIsDeleted() == 1 || !folder.getUserId().equals(userId)) {
return new ArrayList<>();
}
// 检查是否有共享记录
List<FileShare> shares = fileShareMapper.selectList(
new LambdaQueryWrapper<FileShare>()
.eq(FileShare::getFileId, folderId)
.eq(FileShare::getOwnerId, userId)
);
if (shares.isEmpty()) {
return new ArrayList<>();
}
// 返回该文件夹内的文件
LambdaQueryWrapper<FileEntity> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(FileEntity::getFolderId, folderId)
.eq(FileEntity::getIsDeleted, 0)
.isNull(FileEntity::getDeletedAt);
return fileMapper.selectList(wrapper);
} catch (Exception e) {
return new ArrayList<>();
}
}
public List<FileEntity> getSharedToMe(Long userId) {
List<FileShare> shares = fileShareMapper.selectList(
new LambdaQueryWrapper<FileShare>().eq(FileShare::getShareToUserId, userId)
);
return shares.stream()
.map(share -> fileMapper.selectById(share.getFileId()))
.filter(f -> f != null && f.getIsDeleted() == 0)
.collect(Collectors.toList());
}
public List<FileEntity> getSharedFolderFiles(Long userId, Long folderId) {
try {
// 检查该文件夹是否被共享给当前用户
FileEntity folder = fileMapper.selectById(folderId);
if (folder == null || folder.getIsDeleted() == 1) {
return new ArrayList<>();
}
// 检查是否有共享权限
List<FileShare> shares = fileShareMapper.selectList(
new LambdaQueryWrapper<FileShare>()
.eq(FileShare::getFileId, folderId)
.eq(FileShare::getShareToUserId, userId)
);
if (shares.isEmpty()) {
return new ArrayList<>();
}
// 返回该文件夹内的文件
LambdaQueryWrapper<FileEntity> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(FileEntity::getFolderId, folderId)
.eq(FileEntity::getIsDeleted, 0)
.isNull(FileEntity::getDeletedAt);
return fileMapper.selectList(wrapper);
} catch (Exception e) {
return new ArrayList<>();
}
}
@Transactional
public FileShare shareFile(Long fileId, Long ownerId, Long shareToUserId, String permission) {
FileShare share = new FileShare();
share.setFileId(fileId);
share.setOwnerId(ownerId);
share.setShareToUserId(shareToUserId);
share.setPermission(permission != null ? permission : "view");
fileShareMapper.insert(share);
// 更新文件共享状态
FileEntity file = fileMapper.selectById(fileId);
if (file != null) {
file.setIsShared(1);
fileMapper.updateById(file);
}
return share;
}
@Transactional
public void cancelShare(Long fileId, Long ownerId) {
fileShareMapper.delete(
new LambdaQueryWrapper<FileShare>()
.eq(FileShare::getFileId, fileId)
.eq(FileShare::getOwnerId, ownerId)
);
// 检查是否还有其他共享
Long count = fileShareMapper.selectCount(
new LambdaQueryWrapper<FileShare>().eq(FileShare::getFileId, fileId)
);
if (count == 0) {
FileEntity file = fileMapper.selectById(fileId);
if (file != null) {
file.setIsShared(0);
fileMapper.updateById(file);
}
}
}
public String getOwnerName(Long userId) {
User user = userMapper.selectById(userId);
return user != null ? user.getNickname() : "未知用户";
}
public void renameFile(Long fileId, Long userId, String newName) {
FileEntity file = fileMapper.selectById(fileId);
if (file == null) {
throw new RuntimeException("文件不存在");
}
if (!file.getUserId().equals(userId)) {
throw new RuntimeException("无权操作此文件");
}
if (file.getIsDeleted() == 1) {
throw new RuntimeException("无法重命名回收站中的文件");
}
// 检查新名称是否已存在(同一目录下)
FileEntity existing = fileMapper.selectOne(
new LambdaQueryWrapper<FileEntity>()
.eq(FileEntity::getUserId, userId)
.eq(FileEntity::getFolderId, file.getFolderId())
.eq(FileEntity::getName, newName)
.eq(FileEntity::getIsDeleted, 0)
.ne(FileEntity::getId, fileId)
);
if (existing != null) {
throw new RuntimeException("该名称已存在");
}
file.setName(newName);
fileMapper.updateById(file);
}
public void moveFile(Long fileId, Long userId, Long targetFolderId) {
FileEntity file = fileMapper.selectById(fileId);
if (file == null) {
throw new RuntimeException("文件不存在");
}
if (!file.getUserId().equals(userId)) {
throw new RuntimeException("无权操作此文件");
}
if (file.getIsDeleted() == 1) {
throw new RuntimeException("无法移动回收站中的文件");
}
// 检查目标文件夹是否存在(如果不是根目录)
if (targetFolderId != null) {
FileEntity targetFolder = fileMapper.selectById(targetFolderId);
if (targetFolder == null) {
throw new RuntimeException("目标文件夹不存在");
}
if (!targetFolder.getUserId().equals(userId)) {
throw new RuntimeException("无权访问目标文件夹");
}
if (targetFolder.getIsDeleted() == 1) {
throw new RuntimeException("目标文件夹在回收站中");
}
// 检查是否移动到自己里面(如果是文件夹)
if (file.getIsFolder() == 1) {
if (file.getId().equals(targetFolderId)) {
throw new RuntimeException("不能移动到自己里面");
}
// 检查目标是否是自己子文件夹
Long parentId = targetFolder.getFolderId();
while (parentId != null) {
if (parentId.equals(file.getId())) {
throw new RuntimeException("不能移动到自己的子文件夹中");
}
FileEntity parent = fileMapper.selectById(parentId);
if (parent == null) break;
parentId = parent.getFolderId();
}
}
}
// 检查目标位置是否已有同名文件
FileEntity existing = fileMapper.selectOne(
new LambdaQueryWrapper<FileEntity>()
.eq(FileEntity::getUserId, userId)
.eq(FileEntity::getFolderId, targetFolderId)
.eq(FileEntity::getName, file.getName())
.eq(FileEntity::getIsDeleted, 0)
.ne(FileEntity::getId, fileId)
);
if (existing != null) {
throw new RuntimeException("目标位置已存在同名文件");
}
file.setFolderId(targetFolderId);
fileMapper.updateById(file);
}
}

View File

@@ -0,0 +1,85 @@
package com.filesystem.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.filesystem.entity.Message;
import com.filesystem.mapper.MessageMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.stream.Collectors;
@Service
public class MessageService {
@Resource
private MessageMapper messageMapper;
public List<Message> getMessages(Long userId1, Long userId2) {
return messageMapper.selectList(
new LambdaQueryWrapper<Message>()
.and(wrapper -> wrapper
.eq(Message::getFromUserId, userId1).eq(Message::getToUserId, userId2)
.or()
.eq(Message::getFromUserId, userId2).eq(Message::getToUserId, userId1)
)
.orderByAsc(Message::getCreateTime)
);
}
public Message sendMessage(Long fromUserId, Long toUserId, String content, String type) {
Message message = new Message();
message.setFromUserId(fromUserId);
message.setToUserId(toUserId);
message.setContent(content);
message.setType(type);
message.setIsRead(0);
messageMapper.insert(message);
return message;
}
public void updateMessage(Message message) {
messageMapper.updateById(message);
}
public Message sendFileMessage(Long fromUserId, Long toUserId, String fileName, Long fileSize) {
Message message = new Message();
message.setFromUserId(fromUserId);
message.setToUserId(toUserId);
message.setContent(fileName);
message.setType("file");
message.setFileName(fileName);
message.setFileSize(fileSize);
message.setIsRead(0);
messageMapper.insert(message);
return message;
}
public int getUnreadCount(Long userId) {
return Math.toIntExact(messageMapper.selectCount(
new LambdaQueryWrapper<Message>()
.eq(Message::getToUserId, userId)
.eq(Message::getIsRead, 0)
));
}
public void markAsRead(Long messageId) {
Message message = messageMapper.selectById(messageId);
if (message != null) {
message.setIsRead(1);
messageMapper.updateById(message);
}
}
/**
* 获取未读消息列表:按发送人分组,返回每个联系人的未读数和最后一条消息
*/
public List<Message> getUnreadMessages(Long userId) {
return messageMapper.selectList(
new LambdaQueryWrapper<Message>()
.eq(Message::getToUserId, userId)
.eq(Message::getIsRead, 0)
.orderByDesc(Message::getCreateTime)
);
}
}

View File

@@ -0,0 +1,150 @@
package com.filesystem.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.filesystem.entity.FileEntity;
import com.filesystem.entity.User;
import com.filesystem.mapper.FileMapper;
import com.filesystem.mapper.UserMapper;
import com.filesystem.security.JwtUtil;
import jakarta.annotation.Resource;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserService {
@Resource
private UserMapper userMapper;
@Resource
private FileMapper fileMapper;
@Resource
private JwtUtil jwtUtil;
@Resource
private BCryptPasswordEncoder passwordEncoder;
public User findByUsername(String username) {
return userMapper.selectOne(
new LambdaQueryWrapper<User>().eq(User::getUsername, username)
);
}
public User findById(Long id) {
return userMapper.selectById(id);
}
public String login(String username, String password) {
User user = findByUsername(username);
if (user == null) {
throw new RuntimeException("用户不存在");
}
if (!passwordEncoder.matches(password, user.getPassword())) {
throw new RuntimeException("密码错误");
}
return jwtUtil.generateToken(username, user.getId());
}
public void logout(Long userId) {
jwtUtil.invalidateToken(userId);
}
public User createUser(String username, String password, String nickname) {
User user = new User();
user.setUsername(username);
user.setPassword(passwordEncoder.encode(password));
user.setNickname(nickname);
user.setStatus(1);
user.setStorageUsed(0L);
user.setStorageLimit(20L * 1024 * 1024 * 1024); // 20GB
userMapper.insert(user);
return user;
}
/**
* 精确重算用户存储空间:统计该用户所有未删除(非回收站)文件的总大小
*/
public long recalculateStorage(Long userId) {
List<FileEntity> userFiles = fileMapper.selectList(
new LambdaQueryWrapper<FileEntity>()
.eq(FileEntity::getUserId, userId)
.eq(FileEntity::getIsDeleted, 0)
.ne(FileEntity::getIsFolder, 1)
);
long total = 0L;
for (FileEntity f : userFiles) {
if (f.getSize() != null) {
total += f.getSize();
}
}
// 更新到用户表
User user = userMapper.selectById(userId);
if (user != null) {
user.setStorageUsed(total);
// 同步存储上限为 20GB兼容老用户
if (user.getStorageLimit() == null || user.getStorageLimit() < 20L * 1024 * 1024 * 1024) {
user.setStorageLimit(20L * 1024 * 1024 * 1024);
}
userMapper.updateById(user);
}
return total;
}
public void updateStorage(Long userId, Long size) {
User user = userMapper.selectById(userId);
if (user != null) {
user.setStorageUsed(user.getStorageUsed() + size);
userMapper.updateById(user);
}
}
/**
* 扣减存储空间(用于彻底删除文件时)
*/
public void decreaseStorage(Long userId, Long size) {
User user = userMapper.selectById(userId);
if (user != null) {
long newValue = user.getStorageUsed() - size;
user.setStorageUsed(Math.max(0L, newValue));
userMapper.updateById(user);
}
}
public List<User> getAllUsersExcept(Long excludeId) {
return userMapper.selectList(
new LambdaQueryWrapper<User>()
.ne(User::getId, excludeId)
.eq(User::getStatus, 1)
);
}
public void updateAvatar(Long userId, String avatarUrl) {
User user = userMapper.selectById(userId);
if (user != null) {
user.setAvatar(avatarUrl);
userMapper.updateById(user);
}
}
public void updateProfile(Long userId, String nickname, String signature, String phone, String email) {
User user = userMapper.selectById(userId);
if (user != null) {
if (nickname != null) {
user.setNickname(nickname);
}
if (signature != null) {
user.setSignature(signature);
}
if (phone != null) {
user.setPhone(phone);
}
if (email != null) {
user.setEmail(email);
}
userMapper.updateById(user);
}
}
}

View File

@@ -0,0 +1,140 @@
package com.filesystem.websocket;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.filesystem.entity.User;
import com.filesystem.security.JwtUtil;
import com.filesystem.service.MessageService;
import com.filesystem.service.UserService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Component
public class ChatHandler extends TextWebSocketHandler {
@Resource
private JwtUtil jwtUtil;
@Resource
private MessageService messageService;
@Resource
private UserService userService;
private final Map<Long, WebSocketSession> onlineUsers = new ConcurrentHashMap<>();
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
Long userId = getUserIdFromSession(session);
if (userId != null) {
onlineUsers.put(userId, session);
broadcastOnlineUsers();
}
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
Long fromUserId = getUserIdFromSession(session);
if (fromUserId == null) return;
Map<String, Object> data = objectMapper.readValue(message.getPayload(), Map.class);
String type = (String) data.get("type");
switch (type) {
case "chat" -> handleChat(fromUserId, data);
case "ping" -> session.sendMessage(new TextMessage("{\"type\":\"pong\"}"));
}
}
private void handleChat(Long fromUserId, Map<String, Object> data) throws IOException {
Long toUserId = Long.valueOf(data.get("toUserId").toString());
String content = (String) data.get("content");
String msgType = (String) data.getOrDefault("msgType", "text");
String fileName = (String) data.get("fileName");
Object fileSizeObj = data.get("fileSize");
Long fileSize = fileSizeObj != null ? Long.valueOf(fileSizeObj.toString()) : null;
// 保存到数据库
com.filesystem.entity.Message msg = messageService.sendMessage(fromUserId, toUserId, content, msgType);
if ("file".equals(msgType) && fileName != null) {
msg.setFileName(fileName);
msg.setFileSize(fileSize);
messageService.updateMessage(msg);
}
// 查询发送者用户信息
User fromUser = userService.findById(fromUserId);
Map<String, Object> messageData = new HashMap<>();
messageData.put("id", msg.getId());
messageData.put("fromUserId", msg.getFromUserId());
messageData.put("toUserId", msg.getToUserId());
messageData.put("content", msg.getContent());
messageData.put("type", msg.getType());
messageData.put("fileName", msg.getFileName());
messageData.put("fileSize", msg.getFileSize());
messageData.put("createTime", msg.getCreateTime() != null ? msg.getCreateTime().toString() : "");
if (fromUser != null) {
messageData.put("fromUsername", fromUser.getUsername());
messageData.put("fromNickname", fromUser.getNickname() != null ? fromUser.getNickname() : fromUser.getUsername());
messageData.put("fromAvatar", fromUser.getAvatar());
messageData.put("fromSignature", fromUser.getSignature());
}
Map<String, Object> resp = Map.of("type", "chat", "message", messageData);
String respJson = objectMapper.writeValueAsString(resp);
// 发送给接收者
WebSocketSession toSession = onlineUsers.get(toUserId);
if (toSession != null && toSession.isOpen()) {
toSession.sendMessage(new TextMessage(respJson));
}
// 发送回发送者确认
WebSocketSession fromSession = onlineUsers.get(fromUserId);
if (fromSession != null && fromSession.isOpen()) {
fromSession.sendMessage(new TextMessage(respJson));
}
}
private void broadcastOnlineUsers() throws IOException {
Map<String, Object> resp = Map.of("type", "online", "users", onlineUsers.keySet());
String json = objectMapper.writeValueAsString(resp);
for (WebSocketSession session : onlineUsers.values()) {
if (session.isOpen()) {
session.sendMessage(new TextMessage(json));
}
}
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
Long userId = getUserIdFromSession(session);
if (userId != null) {
onlineUsers.remove(userId);
broadcastOnlineUsers();
}
}
private Long getUserIdFromSession(WebSocketSession session) {
Object userId = session.getAttributes().get("userId");
if (userId != null) {
try {
return Long.valueOf(userId.toString());
} catch (Exception e) {
return null;
}
}
return null;
}
}