云文件系统初始化

This commit is contained in:
2026-04-02 22:08:01 +08:00
parent 6addb74d32
commit 9ff222c22c
9 changed files with 101 additions and 68 deletions

View File

@@ -44,19 +44,16 @@ public class SecurityConfig {
.csrf(csrf -> csrf.disable()) .csrf(csrf -> csrf.disable())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth .authorizeHttpRequests(auth -> auth
// API 公开接口
.requestMatchers("/api/auth/login", "/api/auth/register", "/api/auth/logout").permitAll() .requestMatchers("/api/auth/login", "/api/auth/register", "/api/auth/logout").permitAll()
.requestMatchers("/api/files/test").permitAll() .requestMatchers("/api/files/test").permitAll()
// WebSocket
.requestMatchers("/ws/**").permitAll() .requestMatchers("/ws/**").permitAll()
.requestMatchers("/files/avatar/**").permitAll() // 静态资源
.requestMatchers("/api/files/avatar/**").permitAll() .requestMatchers("/webapp/**", "/assets/**", "/uploads/**", "/files/avatar/**", "/api/files/avatar/**", "/api/messages/file/**").permitAll()
.requestMatchers("/api/messages/file/**").permitAll() // 前端页面路由(所有非 /api/ 的路由,由 Vue Router 处理)
.requestMatchers("/webapp/**").permitAll() .requestMatchers("/", "/login", "/register", "/desktop/**", "/favicon.ico").permitAll()
.requestMatchers("/assets/**").permitAll() // 其他所有请求需要认证
.requestMatchers("/login", "/register").permitAll()
.requestMatchers("/").permitAll()
.requestMatchers("/desktop").permitAll()
.requestMatchers("/desktop/**").permitAll()
.requestMatchers("/favicon.ico").permitAll()
.anyRequest().authenticated() .anyRequest().authenticated()
) )
.exceptionHandling(exception -> exception .exceptionHandling(exception -> exception
@@ -112,8 +109,9 @@ public class SecurityConfig {
public CorsConfigurationSource corsConfigurationSource() { public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration(); CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOriginPatterns(Arrays.asList("*")); configuration.setAllowedOriginPatterns(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD"));
configuration.setAllowedHeaders(Arrays.asList("*")); configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setExposedHeaders(Arrays.asList("Authorization", "Content-Type"));
configuration.setAllowCredentials(true); configuration.setAllowCredentials(true);
configuration.setMaxAge(3600L); configuration.setMaxAge(3600L);

View File

@@ -52,7 +52,7 @@ public class AuthController {
return ResponseEntity.ok(body); return ResponseEntity.ok(body);
} catch (Exception e) { } catch (Exception e) {
return ResponseEntity.status(401).body(Map.of("message", e.getMessage())); return ResponseEntity.badRequest().body(Map.of("message", e.getMessage()));
} }
} }

View File

@@ -109,60 +109,84 @@ public class FileController {
public ResponseEntity<?> deleteFile( public ResponseEntity<?> deleteFile(
@AuthenticationPrincipal UserPrincipal principal, @AuthenticationPrincipal UserPrincipal principal,
@PathVariable Long id) { @PathVariable Long id) {
fileService.moveToTrash(id, principal.getUserId()); try {
return ResponseEntity.ok(Map.of("message", "已移至回收站")); fileService.moveToTrash(id, principal.getUserId());
return ResponseEntity.ok(Map.of("message", "已移至回收站"));
} catch (RuntimeException e) {
return ResponseEntity.badRequest().body(Map.of("message", e.getMessage()));
}
} }
@PostMapping("/{id}/restore") @PostMapping("/{id}/restore")
public ResponseEntity<?> restoreFile( public ResponseEntity<?> restoreFile(
@AuthenticationPrincipal UserPrincipal principal, @AuthenticationPrincipal UserPrincipal principal,
@PathVariable Long id) { @PathVariable Long id) {
fileService.restoreFile(id, principal.getUserId()); try {
return ResponseEntity.ok(Map.of("message", "已还原")); fileService.restoreFile(id, principal.getUserId());
return ResponseEntity.ok(Map.of("message", "已还原"));
} catch (RuntimeException e) {
return ResponseEntity.badRequest().body(Map.of("message", e.getMessage()));
}
} }
@DeleteMapping("/{id}/deletePermanent") @DeleteMapping("/{id}/deletePermanent")
public ResponseEntity<?> deletePermanently( public ResponseEntity<?> deletePermanently(
@AuthenticationPrincipal UserPrincipal principal, @AuthenticationPrincipal UserPrincipal principal,
@PathVariable Long id) { @PathVariable Long id) {
fileService.deletePermanently(id, principal.getUserId()); try {
return ResponseEntity.ok(Map.of("message", "已彻底删除")); fileService.deletePermanently(id, principal.getUserId());
return ResponseEntity.ok(Map.of("message", "已彻底删除"));
} catch (RuntimeException e) {
return ResponseEntity.badRequest().body(Map.of("message", e.getMessage()));
}
} }
@DeleteMapping("/emptyTrash") @DeleteMapping("/emptyTrash")
public ResponseEntity<?> emptyTrash(@AuthenticationPrincipal UserPrincipal principal) { public ResponseEntity<?> emptyTrash(@AuthenticationPrincipal UserPrincipal principal) {
fileService.emptyTrash(principal.getUserId()); try {
return ResponseEntity.ok(Map.of("message", "已清空回收站")); fileService.emptyTrash(principal.getUserId());
return ResponseEntity.ok(Map.of("message", "已清空回收站"));
} catch (RuntimeException e) {
return ResponseEntity.badRequest().body(Map.of("message", e.getMessage()));
}
} }
@GetMapping("/{id}/download") @GetMapping("/{id}/download")
public ResponseEntity<byte[]> downloadFile( public ResponseEntity<byte[]> downloadFile(
@AuthenticationPrincipal UserPrincipal principal, @AuthenticationPrincipal UserPrincipal principal,
@PathVariable Long id) throws IOException { @PathVariable Long id) throws IOException {
FileEntity file = fileService.getById(id); try {
if (file == null) return ResponseEntity.notFound().build(); FileEntity file = fileService.getById(id);
byte[] content = fileService.getFileContent(file); if (file == null) return ResponseEntity.notFound().build();
if (content == null) return ResponseEntity.notFound().build(); byte[] content = fileService.getFileContent(file);
String encodedName = URLEncoder.encode(file.getName(), StandardCharsets.UTF_8).replace("+", "%20"); if (content == null) return ResponseEntity.notFound().build();
return ResponseEntity.ok() String encodedName = URLEncoder.encode(file.getName(), StandardCharsets.UTF_8).replace("+", "%20");
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + encodedName + "\"") return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM) .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + encodedName + "\"")
.body(content); .contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(content);
} catch (RuntimeException e) {
return ResponseEntity.badRequest().build();
}
} }
@GetMapping("/{id}/preview") @GetMapping("/{id}/preview")
public ResponseEntity<byte[]> previewFile( public ResponseEntity<byte[]> previewFile(
@AuthenticationPrincipal UserPrincipal principal, @AuthenticationPrincipal UserPrincipal principal,
@PathVariable Long id) throws IOException { @PathVariable Long id) throws IOException {
FileEntity file = fileService.getById(id); try {
if (file == null) return ResponseEntity.notFound().build(); FileEntity file = fileService.getById(id);
byte[] content = fileService.getFileContent(file); if (file == null) return ResponseEntity.notFound().build();
if (content == null) return ResponseEntity.notFound().build(); byte[] content = fileService.getFileContent(file);
String encodedName = URLEncoder.encode(file.getName(), StandardCharsets.UTF_8).replace("+", "%20"); if (content == null) return ResponseEntity.notFound().build();
return ResponseEntity.ok() String encodedName = URLEncoder.encode(file.getName(), StandardCharsets.UTF_8).replace("+", "%20");
.header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + encodedName + "\"") return ResponseEntity.ok()
.contentType(getMediaType(file.getName())) .header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + encodedName + "\"")
.body(content); .contentType(getMediaType(file.getName()))
.body(content);
} catch (RuntimeException e) {
return ResponseEntity.badRequest().build();
}
} }
@PostMapping("/{id}/shareFile") @PostMapping("/{id}/shareFile")
@@ -170,18 +194,26 @@ public class FileController {
@AuthenticationPrincipal UserPrincipal principal, @AuthenticationPrincipal UserPrincipal principal,
@PathVariable Long id, @PathVariable Long id,
@RequestBody Map<String, Object> request) { @RequestBody Map<String, Object> request) {
Long shareToUserId = Long.valueOf(request.get("userId").toString()); try {
String permission = (String) request.getOrDefault("permission", "view"); Long shareToUserId = Long.valueOf(request.get("userId").toString());
FileShare share = fileService.shareFile(id, principal.getUserId(), shareToUserId, permission); String permission = (String) request.getOrDefault("permission", "view");
return ResponseEntity.ok(Map.of("data", share, "message", "共享成功")); FileShare share = fileService.shareFile(id, principal.getUserId(), shareToUserId, permission);
return ResponseEntity.ok(Map.of("data", share, "message", "共享成功"));
} catch (RuntimeException e) {
return ResponseEntity.badRequest().body(Map.of("message", e.getMessage()));
}
} }
@DeleteMapping("/{id}/cancelShare") @DeleteMapping("/{id}/cancelShare")
public ResponseEntity<?> cancelShare( public ResponseEntity<?> cancelShare(
@AuthenticationPrincipal UserPrincipal principal, @AuthenticationPrincipal UserPrincipal principal,
@PathVariable Long id) { @PathVariable Long id) {
fileService.cancelShare(id, principal.getUserId()); try {
return ResponseEntity.ok(Map.of("message", "已取消共享")); fileService.cancelShare(id, principal.getUserId());
return ResponseEntity.ok(Map.of("message", "已取消共享"));
} catch (RuntimeException e) {
return ResponseEntity.badRequest().body(Map.of("message", e.getMessage()));
}
} }
/** /**

View File

@@ -6,14 +6,9 @@ import org.springframework.web.bind.annotation.GetMapping;
@Controller @Controller
public class IndexController { public class IndexController {
@GetMapping({"/", "/login", "/register"}) // 所有非 /api/**、非 /ws/** 的前端路由,统一返回 index.html
@GetMapping({"/", "/login", "/register", "/desktop", "/desktop/**"})
public String forward() { public String forward() {
return "forward:/webapp/index.html"; return "forward:/webapp/index.html";
} }
// 捕获所有前端路由,返回 index.html 让 Vue Router 处理
@GetMapping("/desktop/**")
public String desktop() {
return "forward:/webapp/index.html";
}
} }

View File

@@ -115,12 +115,16 @@ public class UserController {
public ResponseEntity<?> updateProfile( public ResponseEntity<?> updateProfile(
@AuthenticationPrincipal UserPrincipal principal, @AuthenticationPrincipal UserPrincipal principal,
@RequestBody Map<String, String> request) { @RequestBody Map<String, String> request) {
String nickname = request.get("nickname"); try {
String signature = request.get("signature"); String nickname = request.get("nickname");
String phone = request.get("phone"); String signature = request.get("signature");
String email = request.get("email"); String phone = request.get("phone");
userService.updateProfile(principal.getUserId(), nickname, signature, phone, email); String email = request.get("email");
return ResponseEntity.ok(Map.of("message", "更新成功")); userService.updateProfile(principal.getUserId(), nickname, signature, phone, email);
return ResponseEntity.ok(Map.of("message", "更新成功"));
} catch (RuntimeException e) {
return ResponseEntity.badRequest().body(Map.of("message", e.getMessage()));
}
} }
/** /**

View File

@@ -1,5 +1,5 @@
server: server:
port: 18080 port: 18089
spring: spring:
datasource: datasource:

View File

@@ -22,7 +22,16 @@ request.interceptors.response.use(
const status = error.response?.status const status = error.response?.status
// 只有 401/403 才清理 token 并跳转登录页 // 只有 401/403 才清理 token 并跳转登录页
// 但在登录页时不跳转(避免死循环),登录接口的 401 也不跳转
if (status === 401 || status === 403) { if (status === 401 || status === 403) {
const isLoginPage = window.location.pathname === '/login'
const isAuthApi = error.config?.url?.startsWith('/auth/')
if (isLoginPage || isAuthApi) {
// 登录页或登录接口,不跳转,不清理
return Promise.reject(error)
}
localStorage.removeItem('token') localStorage.removeItem('token')
localStorage.removeItem('username') localStorage.removeItem('username')
localStorage.removeItem('userId') localStorage.removeItem('userId')
@@ -31,12 +40,9 @@ request.interceptors.response.use(
localStorage.removeItem('storageUsed') localStorage.removeItem('storageUsed')
localStorage.removeItem('storageLimit') localStorage.removeItem('storageLimit')
if (window.location.pathname !== '/login') { window.location.href = '/login'
window.location.href = '/login'
}
} }
// 其他错误404、500、网络错误等正常 reject不跳转
return Promise.reject(error) return Promise.reject(error)
} }
) )

View File

@@ -619,8 +619,6 @@ const handleConfirmBatchMove = async (targetFolderId) => {
finalFolderId = targetFolderId finalFolderId = targetFolderId
} }
console.log('targetFolderId:', targetFolderId, 'finalFolderId:', finalFolderId)
let successCount = 0 let successCount = 0
let failCount = 0 let failCount = 0

View File

@@ -22,19 +22,19 @@ export default defineConfig({
port: 5173, port: 5173,
proxy: { proxy: {
'/api': { '/api': {
target: 'http://localhost:8080', target: 'http://localhost:18089',
changeOrigin: true changeOrigin: true
}, },
'/ws': { '/ws': {
target: 'ws://localhost:8080', target: 'ws://localhost:18089',
ws: true ws: true
}, },
'/uploads': { '/uploads': {
target: 'http://localhost:8080', target: 'http://localhost:18089',
changeOrigin: true changeOrigin: true
}, },
'/files': { '/files': {
target: 'http://localhost:8080', target: 'http://localhost:18089',
changeOrigin: true changeOrigin: true
} }
} }