云文件系统初始化

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

View File

@@ -0,0 +1,46 @@
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://127.0.0.1:33069/prd?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&createDatabaseIfNotExist=true
username: root
password: root_dream
driver-class-name: com.mysql.cj.jdbc.Driver
data:
redis:
host: 192.168.31.194
port: 16379
database: 0
timeout: 10000ms
password: admin
servlet:
multipart:
enabled: true
max-file-size: 1024MB
max-request-size: 2048MB
file-size-threshold: 0
mybatis-plus:
mapper-locations: classpath*:/mapper/**/*.xml
type-aliases-package: com.filesystem.entity
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
id-type: auto
file:
storage:
path: /ogsapp/uploads
jwt:
secret: mySecretKeyForJWTTokenGenerationThatIsLongEnough256BitsForHS256Algorithm
expiration: 86400000
logging:
level:
com.filesystem: DEBUG

View File

@@ -0,0 +1,78 @@
-- 创建数据库
CREATE DATABASE IF NOT EXISTS file_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE file_system;
-- 用户表
CREATE TABLE IF NOT EXISTS sys_user (
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
username VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名',
password VARCHAR(255) NOT NULL COMMENT '密码',
nickname VARCHAR(50) COMMENT '昵称',
avatar VARCHAR(255) COMMENT '头像',
signature VARCHAR(255) COMMENT '个性签名',
email VARCHAR(100) COMMENT '邮箱',
phone VARCHAR(20) COMMENT '手机号',
status INT DEFAULT 1 COMMENT '状态 0-禁用 1-启用',
storage_used BIGINT DEFAULT 0 COMMENT '已用存储空间(字节)',
storage_limit BIGINT DEFAULT 10737418240 COMMENT '存储限制(字节) 默认10GB',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
is_deleted INT DEFAULT 0 COMMENT '是否删除 0-否 1-是'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
-- 文件表
CREATE TABLE IF NOT EXISTS sys_file (
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
name VARCHAR(255) NOT NULL COMMENT '文件名',
type VARCHAR(50) COMMENT '文件类型',
size BIGINT DEFAULT 0 COMMENT '文件大小(字节)',
path VARCHAR(500) COMMENT '存储路径',
folder_id BIGINT COMMENT '所属文件夹ID',
user_id BIGINT NOT NULL COMMENT '所属用户ID',
is_folder INT DEFAULT 0 COMMENT '是否文件夹 0-否 1-是',
is_shared INT DEFAULT 0 COMMENT '是否共享 0-否 1-是',
deleted_at DATETIME COMMENT '删除时间',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
is_deleted INT DEFAULT 0 COMMENT '是否删除 0-否 1-是',
INDEX idx_user_id (user_id),
INDEX idx_folder_id (folder_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文件表';
-- 文件共享表
CREATE TABLE IF NOT EXISTS sys_file_share (
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
file_id BIGINT NOT NULL COMMENT '文件ID',
owner_id BIGINT NOT NULL COMMENT '所有者ID',
share_to_user_id BIGINT NOT NULL COMMENT '共享给用户ID',
permission VARCHAR(20) DEFAULT 'view' COMMENT '权限 view-查看 edit-编辑',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
is_deleted INT DEFAULT 0 COMMENT '是否删除 0-否 1-是',
INDEX idx_file_id (file_id),
INDEX idx_owner_id (owner_id),
INDEX idx_share_to_user_id (share_to_user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文件共享表';
-- 消息表
CREATE TABLE IF NOT EXISTS sys_message (
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
from_user_id BIGINT NOT NULL COMMENT '发送者ID',
to_user_id BIGINT NOT NULL COMMENT '接收者ID',
content TEXT COMMENT '消息内容',
type VARCHAR(20) DEFAULT 'text' COMMENT '消息类型 text-文本 image-图片 file-文件 emoji-表情',
file_name VARCHAR(255) COMMENT '文件名',
file_size BIGINT COMMENT '文件大小',
is_read INT DEFAULT 0 COMMENT '是否已读 0-未读 1-已读',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
is_deleted INT DEFAULT 0 COMMENT '是否删除 0-否 1-是',
INDEX idx_from_user_id (from_user_id),
INDEX idx_to_user_id (to_user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='消息表';
-- 插入默认管理员账户 (密码: admin123)
INSERT INTO sys_user (username, password, nickname, status, storage_limit)
VALUES ('admin', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iAt6Z5EH', '管理员', 1, 10737418240)
ON DUPLICATE KEY UPDATE username = username;