云文件系统初始化
This commit is contained in:
14
src/main/java/com/filesystem/FileSystemApplication.java
Normal file
14
src/main/java/com/filesystem/FileSystemApplication.java
Normal 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);
|
||||
}
|
||||
}
|
||||
35
src/main/java/com/filesystem/config/MyBatisPlusConfig.java
Normal file
35
src/main/java/com/filesystem/config/MyBatisPlusConfig.java
Normal 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());
|
||||
}
|
||||
}
|
||||
24
src/main/java/com/filesystem/config/RedisConfig.java
Normal file
24
src/main/java/com/filesystem/config/RedisConfig.java
Normal 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;
|
||||
}
|
||||
}
|
||||
117
src/main/java/com/filesystem/config/SecurityConfig.java
Normal file
117
src/main/java/com/filesystem/config/SecurityConfig.java
Normal 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;
|
||||
}
|
||||
}
|
||||
30
src/main/java/com/filesystem/config/WebConfig.java
Normal file
30
src/main/java/com/filesystem/config/WebConfig.java
Normal 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() + "/");
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
26
src/main/java/com/filesystem/config/WebSocketConfig.java
Normal file
26
src/main/java/com/filesystem/config/WebSocketConfig.java
Normal 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("*");
|
||||
}
|
||||
}
|
||||
112
src/main/java/com/filesystem/controller/AuthController.java
Normal file
112
src/main/java/com/filesystem/controller/AuthController.java
Normal 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);
|
||||
}
|
||||
}
|
||||
254
src/main/java/com/filesystem/controller/FileController.java
Normal file
254
src/main/java/com/filesystem/controller/FileController.java
Normal 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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
279
src/main/java/com/filesystem/controller/MessageController.java
Normal file
279
src/main/java/com/filesystem/controller/MessageController.java
Normal 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", "已标记已读"));
|
||||
}
|
||||
}
|
||||
125
src/main/java/com/filesystem/controller/UserController.java
Normal file
125
src/main/java/com/filesystem/controller/UserController.java
Normal 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", "更新成功"));
|
||||
}
|
||||
}
|
||||
21
src/main/java/com/filesystem/entity/BaseEntity.java
Normal file
21
src/main/java/com/filesystem/entity/BaseEntity.java
Normal 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;
|
||||
}
|
||||
34
src/main/java/com/filesystem/entity/FileEntity.java
Normal file
34
src/main/java/com/filesystem/entity/FileEntity.java
Normal 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;
|
||||
}
|
||||
16
src/main/java/com/filesystem/entity/FileShare.java
Normal file
16
src/main/java/com/filesystem/entity/FileShare.java
Normal 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;
|
||||
}
|
||||
30
src/main/java/com/filesystem/entity/Message.java
Normal file
30
src/main/java/com/filesystem/entity/Message.java
Normal 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;
|
||||
}
|
||||
33
src/main/java/com/filesystem/entity/User.java
Normal file
33
src/main/java/com/filesystem/entity/User.java
Normal 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;
|
||||
}
|
||||
9
src/main/java/com/filesystem/mapper/FileMapper.java
Normal file
9
src/main/java/com/filesystem/mapper/FileMapper.java
Normal 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> {
|
||||
}
|
||||
9
src/main/java/com/filesystem/mapper/FileShareMapper.java
Normal file
9
src/main/java/com/filesystem/mapper/FileShareMapper.java
Normal 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> {
|
||||
}
|
||||
9
src/main/java/com/filesystem/mapper/MessageMapper.java
Normal file
9
src/main/java/com/filesystem/mapper/MessageMapper.java
Normal 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> {
|
||||
}
|
||||
9
src/main/java/com/filesystem/mapper/UserMapper.java
Normal file
9
src/main/java/com/filesystem/mapper/UserMapper.java
Normal 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> {
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
130
src/main/java/com/filesystem/security/JwtUtil.java
Normal file
130
src/main/java/com/filesystem/security/JwtUtil.java
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
11
src/main/java/com/filesystem/security/UserPrincipal.java
Normal file
11
src/main/java/com/filesystem/security/UserPrincipal.java
Normal 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;
|
||||
}
|
||||
502
src/main/java/com/filesystem/service/FileService.java
Normal file
502
src/main/java/com/filesystem/service/FileService.java
Normal 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);
|
||||
}
|
||||
}
|
||||
85
src/main/java/com/filesystem/service/MessageService.java
Normal file
85
src/main/java/com/filesystem/service/MessageService.java
Normal 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)
|
||||
);
|
||||
}
|
||||
}
|
||||
150
src/main/java/com/filesystem/service/UserService.java
Normal file
150
src/main/java/com/filesystem/service/UserService.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
140
src/main/java/com/filesystem/websocket/ChatHandler.java
Normal file
140
src/main/java/com/filesystem/websocket/ChatHandler.java
Normal 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;
|
||||
}
|
||||
}
|
||||
46
src/main/resources/application.yml
Normal file
46
src/main/resources/application.yml
Normal 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
|
||||
78
src/main/resources/db/init.sql
Normal file
78
src/main/resources/db/init.sql
Normal 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;
|
||||
Reference in New Issue
Block a user