重构云文件管理系统

This commit is contained in:
2026-04-02 23:48:57 +08:00
parent 453f05cca3
commit df1ff66c1f
6 changed files with 429 additions and 177 deletions

View File

@@ -5,23 +5,17 @@ import com.filesystem.entity.FileShare;
import com.filesystem.security.UserPrincipal;
import com.filesystem.service.FileService;
import com.filesystem.utils.ApiResult;
import com.filesystem.utils.CommonUtil;
import com.filesystem.utils.FileUtil;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
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.ArrayList;
import java.util.List;
import java.util.Map;
@@ -95,8 +89,8 @@ public class FileController {
public ApiResult<FileEntity> 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;
String name = CommonUtil.getString(request, "name");
Long parentId = CommonUtil.getLong(request, "parentId");
FileEntity folder = fileService.createFolder(name, principal.getUserId(), parentId);
return ApiResult.success("创建成功", folder);
}
@@ -148,7 +142,7 @@ public class FileController {
}
/**
* 下载文件(返回 ResponseEntity<byte[]> 用于二进制响应)
* 下载文件
*/
@GetMapping("/{id}/download")
public ResponseEntity<byte[]> downloadFile(
@@ -159,18 +153,14 @@ public class FileController {
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);
return FileUtil.buildDownloadResponse(content, file.getName());
} catch (RuntimeException e) {
return ResponseEntity.badRequest().build();
}
}
/**
* 预览文件(返回 ResponseEntity<byte[]> 用于二进制响应)
* 预览文件
*/
@GetMapping("/{id}/preview")
public ResponseEntity<byte[]> previewFile(
@@ -181,11 +171,7 @@ public class FileController {
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);
return FileUtil.buildPreviewResponse(content, file.getName());
} catch (RuntimeException e) {
return ResponseEntity.badRequest().build();
}
@@ -197,8 +183,8 @@ public class FileController {
@PathVariable Long id,
@RequestBody Map<String, Object> request) {
try {
Long shareToUserId = Long.valueOf(request.get("userId").toString());
String permission = (String) request.getOrDefault("permission", "view");
Long shareToUserId = CommonUtil.getLong(request, "userId");
String permission = CommonUtil.getString(request, "permission", "view");
FileShare share = fileService.shareFile(id, principal.getUserId(), shareToUserId, permission);
return ApiResult.success("共享成功", share);
} catch (RuntimeException e) {
@@ -219,43 +205,21 @@ public class FileController {
}
/**
* 获取头像图片(返回 ResponseEntity<byte[]> 用于二进制响应)
* 获取头像图片
*/
@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)) {
java.nio.file.Path filePath = java.nio.file.Paths.get(storagePath).toAbsolutePath().resolve("avatars").resolve(relativePath);
if (!java.nio.file.Files.exists(filePath)) {
return ResponseEntity.notFound().build();
}
byte[] content = Files.readAllBytes(filePath);
byte[] content = java.nio.file.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;
};
return FileUtil.buildImageResponse(content, fileName);
}
@PutMapping("/{id}/rename")
@@ -264,7 +228,7 @@ public class FileController {
@PathVariable Long id,
@RequestBody Map<String, String> request) {
String newName = request.get("name");
if (newName == null || newName.trim().isEmpty()) {
if (CommonUtil.isBlank(newName)) {
return ApiResult.error("名称不能为空");
}
try {
@@ -280,11 +244,7 @@ public class FileController {
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable Long id,
@RequestBody Map<String, Object> request) {
Object folderIdObj = request.get("folderId");
Long folderId = null;
if (folderIdObj != null && !folderIdObj.toString().isEmpty() && !"null".equals(folderIdObj.toString()) && !"undefined".equals(folderIdObj.toString())) {
folderId = Long.parseLong(folderIdObj.toString());
}
Long folderId = CommonUtil.getLong(request, "folderId");
try {
fileService.moveFile(id, principal.getUserId(), folderId);
return ApiResult.success("移动成功");
@@ -294,48 +254,27 @@ public class FileController {
}
/**
* 批量下载(返回 ZIP 文件,使用 HttpServletResponse 直接写入)
* 批量下载
*/
@PostMapping("/batchDownload")
public void batchDownload(
@AuthenticationPrincipal UserPrincipal principal,
@RequestBody Map<String, Object> request,
HttpServletResponse response) throws IOException {
Object idsObj = request.get("ids");
if (idsObj == null) {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":400,\"message\":\"请选择要下载的文件\"}");
return;
}
List<Long> ids = new ArrayList<>();
if (idsObj instanceof List) {
for (Object id : (List<?>) idsObj) {
if (id instanceof Number) {
ids.add(((Number) id).longValue());
} else if (id instanceof String) {
ids.add(Long.parseLong((String) id));
}
}
}
List<Long> ids = CommonUtil.parseIds(request.get("ids"));
if (ids.isEmpty()) {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":400,\"message\":\"请选择要下载的文件\"}");
com.filesystem.utils.ServletUtil.writeErrorJson(response, 400, "请选择要下载的文件");
return;
}
try {
byte[] zipBytes = fileService.createZipArchive(ids, principal.getUserId());
response.setContentType("application/zip");
response.setHeader("Content-Disposition", "attachment; filename=\"download.zip\"");
response.setContentLength(zipBytes.length);
com.filesystem.utils.ServletUtil.setZipDownloadHeaders(response, zipBytes.length);
response.getOutputStream().write(zipBytes);
response.getOutputStream().flush();
} catch (RuntimeException e) {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":400,\"message\":\"" + e.getMessage().replace("\"", "\\\"") + "\"}");
com.filesystem.utils.ServletUtil.writeErrorJson(response, 400, e.getMessage());
}
}
@@ -352,20 +291,10 @@ public class FileController {
public ApiResult<Void> batchCancelShare(
@AuthenticationPrincipal UserPrincipal principal,
@RequestBody Map<String, Object> request) {
Object idsObj = request.get("ids");
if (idsObj == null) {
List<Long> ids = CommonUtil.parseIds(request.get("ids"));
if (ids.isEmpty()) {
return ApiResult.error("请选择要取消共享的文件");
}
List<Long> ids = new ArrayList<>();
if (idsObj instanceof List) {
for (Object id : (List<?>) idsObj) {
if (id instanceof Number) {
ids.add(((Number) id).longValue());
} else if (id instanceof String) {
ids.add(Long.parseLong((String) id));
}
}
}
for (Long fileId : ids) {
fileService.cancelShare(fileId, principal.getUserId());
}
@@ -376,20 +305,10 @@ public class FileController {
public ApiResult<Void> batchRestore(
@AuthenticationPrincipal UserPrincipal principal,
@RequestBody Map<String, Object> request) {
Object idsObj = request.get("ids");
if (idsObj == null) {
List<Long> ids = CommonUtil.parseIds(request.get("ids"));
if (ids.isEmpty()) {
return ApiResult.error("请选择要还原的文件");
}
List<Long> ids = new ArrayList<>();
if (idsObj instanceof List) {
for (Object id : (List<?>) idsObj) {
if (id instanceof Number) {
ids.add(((Number) id).longValue());
} else if (id instanceof String) {
ids.add(Long.parseLong((String) id));
}
}
}
for (Long fileId : ids) {
fileService.restoreFile(fileId, principal.getUserId());
}

View File

@@ -6,24 +6,22 @@ import com.filesystem.security.UserPrincipal;
import com.filesystem.service.MessageService;
import com.filesystem.service.UserService;
import com.filesystem.utils.ApiResult;
import com.filesystem.utils.CommonUtil;
import com.filesystem.utils.FileUtil;
import com.filesystem.utils.ServletUtil;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
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;
@@ -41,8 +39,6 @@ public class MessageController {
@Value("${file.storage.path:./uploads}")
private String storagePath;
private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy/MM");
// ==================== 聊天文件上传 ====================
@PostMapping("/upload")
@@ -55,10 +51,9 @@ public class MessageController {
}
String originalName = file.getOriginalFilename();
String ext = originalName != null && originalName.contains(".")
? originalName.substring(originalName.lastIndexOf(".")) : "";
String ext = FileUtil.getExtensionWithDot(originalName);
String storedName = UUID.randomUUID().toString() + ext;
String datePath = LocalDateTime.now().format(DATE_FMT);
String datePath = CommonUtil.getDatePath();
Path targetDir = Paths.get(storagePath).toAbsolutePath().resolve("chat").resolve(datePath);
if (!Files.exists(targetDir)) {
@@ -72,7 +67,7 @@ public class MessageController {
return ApiResult.success("上传成功", Map.of("url", fileUrl));
}
// ==================== 聊天文件访问(返回 ResponseEntity<byte[]> 用于二进制响应)====================
// ==================== 聊天文件访问 ====================
@GetMapping("/file/**")
public ResponseEntity<byte[]> getChatFile(HttpServletRequest request) throws IOException {
@@ -80,7 +75,6 @@ public class MessageController {
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);
@@ -93,31 +87,11 @@ public class MessageController {
}
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);
boolean isImage = FileUtil.isImage(fileName);
return isImage
? FileUtil.buildImageResponse(content, fileName)
: FileUtil.buildDownloadResponse(content, fileName);
}
// ==================== 消息收发 ====================
@@ -158,7 +132,7 @@ public class MessageController {
m.put("fileName", msg.getFileName());
m.put("fileSize", msg.getFileSize());
m.put("isRead", msg.getIsRead());
m.put("createTime", msg.getCreateTime() != null ? msg.getCreateTime().toString() : "");
m.put("createTime", CommonUtil.formatDateTime(msg.getCreateTime()));
User fromUser = userMap.get(msg.getFromUserId());
if (fromUser != null) {
@@ -196,12 +170,11 @@ public class MessageController {
public ApiResult<Map<String, Object>> 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;
Long toUserId = CommonUtil.getLong(request, "toUserId");
String content = CommonUtil.getString(request, "content");
String type = CommonUtil.getString(request, "type", "text");
String fileName = CommonUtil.getString(request, "fileName");
Long fileSize = CommonUtil.getLong(request, "fileSize");
Message message = messageService.sendMessage(principal.getUserId(), toUserId, content, type);
@@ -219,7 +192,7 @@ public class MessageController {
result.put("type", message.getType());
result.put("fileName", message.getFileName());
result.put("fileSize", message.getFileSize());
result.put("createTime", message.getCreateTime() != null ? message.getCreateTime().toString() : "");
result.put("createTime", CommonUtil.formatDateTime(message.getCreateTime()));
User fromUser = userService.findById(principal.getUserId());
if (fromUser != null) {
@@ -241,7 +214,6 @@ public class MessageController {
public ApiResult<List<Map<String, Object>>> 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()));
@@ -249,7 +221,7 @@ public class MessageController {
for (Map.Entry<Long, List<Message>> entry : grouped.entrySet()) {
Long fromUserId = entry.getKey();
List<Message> msgs = entry.getValue();
Message lastMsg = msgs.get(0); // 已按时间倒序,第一条就是最新的
Message lastMsg = msgs.get(0);
User fromUser = userService.findById(fromUserId);
Map<String, Object> item = new HashMap<>();
@@ -259,7 +231,7 @@ public class MessageController {
? "[图片]" : (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() : "");
item.put("lastTime", CommonUtil.formatDateTime(lastMsg.getCreateTime()));
if (fromUser != null) {
item.put("username", fromUser.getUsername());
item.put("nickname", fromUser.getNickname() != null ? fromUser.getNickname() : fromUser.getUsername());

View File

@@ -4,6 +4,8 @@ import com.filesystem.entity.User;
import com.filesystem.security.UserPrincipal;
import com.filesystem.service.UserService;
import com.filesystem.utils.ApiResult;
import com.filesystem.utils.CommonUtil;
import com.filesystem.utils.FileUtil;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
@@ -14,8 +16,6 @@ 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.*;
import java.util.stream.Collectors;
@@ -29,8 +29,6 @@ public class UserController {
@Value("${file.storage.path:./uploads}")
private String storagePath;
private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy/MM");
/**
* 获取所有可用用户(用于文件共享等场景)
*/
@@ -38,7 +36,7 @@ public class UserController {
public ApiResult<List<Map<String, Object>>> getAllUsers(@AuthenticationPrincipal UserPrincipal principal) {
List<User> users = userService.getAllUsersExcept(principal.getUserId());
List<Map<String, Object>> result = users.stream()
.filter(u -> u.getStatus() == 1) // 只返回启用状态的用户
.filter(u -> u.getStatus() == 1)
.map(u -> {
Map<String, Object> m = new HashMap<>();
m.put("id", u.getId());
@@ -64,20 +62,14 @@ public class UserController {
return ApiResult.error("请选择图片");
}
// 限制文件大小 2MB
if (file.getSize() > 2 * 1024 * 1024) {
return ApiResult.error("图片大小不能超过2MB");
}
// 保存文件
String originalFilename = file.getOriginalFilename();
String ext = originalFilename != null && originalFilename.contains(".")
? originalFilename.substring(originalFilename.lastIndexOf("."))
: ".jpg";
String ext = FileUtil.getExtensionWithDot(file.getOriginalFilename());
String fileName = UUID.randomUUID().toString() + ext;
String datePath = LocalDateTime.now().format(DATE_FMT);
String datePath = CommonUtil.getDatePath();
// 使用配置文件中的路径 + 日期目录
Path uploadPath = Paths.get(storagePath).toAbsolutePath().resolve("avatars").resolve(datePath);
if (!Files.exists(uploadPath)) {
Files.createDirectories(uploadPath);
@@ -86,7 +78,6 @@ public class UserController {
Path filePath = uploadPath.resolve(fileName);
Files.copy(file.getInputStream(), filePath);
// 更新用户头像
String avatarUrl = "/api/files/avatar/" + datePath + "/" + fileName;
userService.updateAvatar(principal.getUserId(), avatarUrl);
@@ -118,11 +109,13 @@ public class UserController {
@AuthenticationPrincipal UserPrincipal principal,
@RequestBody Map<String, String> request) {
try {
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);
userService.updateProfile(
principal.getUserId(),
request.get("nickname"),
request.get("signature"),
request.get("phone"),
request.get("email")
);
return ApiResult.success("更新成功");
} catch (RuntimeException e) {
return ApiResult.error(e.getMessage());
@@ -139,7 +132,7 @@ public class UserController {
String oldPassword = request.get("oldPassword");
String newPassword = request.get("newPassword");
if (oldPassword == null || oldPassword.isEmpty() || newPassword == null || newPassword.isEmpty()) {
if (CommonUtil.isBlank(oldPassword) || CommonUtil.isBlank(newPassword)) {
return ApiResult.error("请填写完整信息");
}