重构云文件管理系统

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.security.UserPrincipal;
import com.filesystem.service.FileService; import com.filesystem.service.FileService;
import com.filesystem.utils.ApiResult; import com.filesystem.utils.ApiResult;
import com.filesystem.utils.CommonUtil;
import com.filesystem.utils.FileUtil;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Value; 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.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import java.io.IOException; 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.List;
import java.util.Map; import java.util.Map;
@@ -95,8 +89,8 @@ public class FileController {
public ApiResult<FileEntity> createFolder( public ApiResult<FileEntity> createFolder(
@AuthenticationPrincipal UserPrincipal principal, @AuthenticationPrincipal UserPrincipal principal,
@RequestBody Map<String, Object> request) { @RequestBody Map<String, Object> request) {
String name = (String) request.get("name"); String name = CommonUtil.getString(request, "name");
Long parentId = request.get("parentId") != null ? Long.valueOf(request.get("parentId").toString()) : null; Long parentId = CommonUtil.getLong(request, "parentId");
FileEntity folder = fileService.createFolder(name, principal.getUserId(), parentId); FileEntity folder = fileService.createFolder(name, principal.getUserId(), parentId);
return ApiResult.success("创建成功", folder); return ApiResult.success("创建成功", folder);
} }
@@ -148,7 +142,7 @@ public class FileController {
} }
/** /**
* 下载文件(返回 ResponseEntity<byte[]> 用于二进制响应) * 下载文件
*/ */
@GetMapping("/{id}/download") @GetMapping("/{id}/download")
public ResponseEntity<byte[]> downloadFile( public ResponseEntity<byte[]> downloadFile(
@@ -159,18 +153,14 @@ public class FileController {
if (file == null) return ResponseEntity.notFound().build(); if (file == null) return ResponseEntity.notFound().build();
byte[] content = fileService.getFileContent(file); byte[] content = fileService.getFileContent(file);
if (content == null) return ResponseEntity.notFound().build(); if (content == null) return ResponseEntity.notFound().build();
String encodedName = URLEncoder.encode(file.getName(), StandardCharsets.UTF_8).replace("+", "%20"); return FileUtil.buildDownloadResponse(content, file.getName());
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + encodedName + "\"")
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(content);
} catch (RuntimeException e) { } catch (RuntimeException e) {
return ResponseEntity.badRequest().build(); return ResponseEntity.badRequest().build();
} }
} }
/** /**
* 预览文件(返回 ResponseEntity<byte[]> 用于二进制响应) * 预览文件
*/ */
@GetMapping("/{id}/preview") @GetMapping("/{id}/preview")
public ResponseEntity<byte[]> previewFile( public ResponseEntity<byte[]> previewFile(
@@ -181,11 +171,7 @@ public class FileController {
if (file == null) return ResponseEntity.notFound().build(); if (file == null) return ResponseEntity.notFound().build();
byte[] content = fileService.getFileContent(file); byte[] content = fileService.getFileContent(file);
if (content == null) return ResponseEntity.notFound().build(); if (content == null) return ResponseEntity.notFound().build();
String encodedName = URLEncoder.encode(file.getName(), StandardCharsets.UTF_8).replace("+", "%20"); return FileUtil.buildPreviewResponse(content, file.getName());
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + encodedName + "\"")
.contentType(getMediaType(file.getName()))
.body(content);
} catch (RuntimeException e) { } catch (RuntimeException e) {
return ResponseEntity.badRequest().build(); return ResponseEntity.badRequest().build();
} }
@@ -197,8 +183,8 @@ public class FileController {
@PathVariable Long id, @PathVariable Long id,
@RequestBody Map<String, Object> request) { @RequestBody Map<String, Object> request) {
try { try {
Long shareToUserId = Long.valueOf(request.get("userId").toString()); Long shareToUserId = CommonUtil.getLong(request, "userId");
String permission = (String) request.getOrDefault("permission", "view"); String permission = CommonUtil.getString(request, "permission", "view");
FileShare share = fileService.shareFile(id, principal.getUserId(), shareToUserId, permission); FileShare share = fileService.shareFile(id, principal.getUserId(), shareToUserId, permission);
return ApiResult.success("共享成功", share); return ApiResult.success("共享成功", share);
} catch (RuntimeException e) { } catch (RuntimeException e) {
@@ -219,43 +205,21 @@ public class FileController {
} }
/** /**
* 获取头像图片(返回 ResponseEntity<byte[]> 用于二进制响应) * 获取头像图片
*/ */
@GetMapping("/avatar/**") @GetMapping("/avatar/**")
public ResponseEntity<byte[]> getAvatar(jakarta.servlet.http.HttpServletRequest request) throws IOException { public ResponseEntity<byte[]> getAvatar(jakarta.servlet.http.HttpServletRequest request) throws IOException {
String uri = request.getRequestURI(); String uri = request.getRequestURI();
String prefix = "/api/files/avatar/"; String prefix = "/api/files/avatar/";
String relativePath = uri.substring(uri.indexOf(prefix) + prefix.length()); String relativePath = uri.substring(uri.indexOf(prefix) + prefix.length());
// relativePath = "2026/04/xxx.jpg"
Path filePath = Paths.get(storagePath).toAbsolutePath().resolve("avatars").resolve(relativePath); java.nio.file.Path filePath = java.nio.file.Paths.get(storagePath).toAbsolutePath().resolve("avatars").resolve(relativePath);
if (!Files.exists(filePath)) { if (!java.nio.file.Files.exists(filePath)) {
return ResponseEntity.notFound().build(); 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 fileName = relativePath.contains("/") ? relativePath.substring(relativePath.lastIndexOf("/") + 1) : relativePath;
String ext = fileName.contains(".") ? fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase() : "jpg"; return FileUtil.buildImageResponse(content, fileName);
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") @PutMapping("/{id}/rename")
@@ -264,7 +228,7 @@ public class FileController {
@PathVariable Long id, @PathVariable Long id,
@RequestBody Map<String, String> request) { @RequestBody Map<String, String> request) {
String newName = request.get("name"); String newName = request.get("name");
if (newName == null || newName.trim().isEmpty()) { if (CommonUtil.isBlank(newName)) {
return ApiResult.error("名称不能为空"); return ApiResult.error("名称不能为空");
} }
try { try {
@@ -280,11 +244,7 @@ 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) {
Object folderIdObj = request.get("folderId"); Long folderId = CommonUtil.getLong(request, "folderId");
Long folderId = null;
if (folderIdObj != null && !folderIdObj.toString().isEmpty() && !"null".equals(folderIdObj.toString()) && !"undefined".equals(folderIdObj.toString())) {
folderId = Long.parseLong(folderIdObj.toString());
}
try { try {
fileService.moveFile(id, principal.getUserId(), folderId); fileService.moveFile(id, principal.getUserId(), folderId);
return ApiResult.success("移动成功"); return ApiResult.success("移动成功");
@@ -294,48 +254,27 @@ public class FileController {
} }
/** /**
* 批量下载(返回 ZIP 文件,使用 HttpServletResponse 直接写入) * 批量下载
*/ */
@PostMapping("/batchDownload") @PostMapping("/batchDownload")
public void batchDownload( public void batchDownload(
@AuthenticationPrincipal UserPrincipal principal, @AuthenticationPrincipal UserPrincipal principal,
@RequestBody Map<String, Object> request, @RequestBody Map<String, Object> request,
HttpServletResponse response) throws IOException { HttpServletResponse response) throws IOException {
Object idsObj = request.get("ids"); List<Long> ids = CommonUtil.parseIds(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));
}
}
}
if (ids.isEmpty()) { if (ids.isEmpty()) {
response.setContentType("application/json;charset=UTF-8"); com.filesystem.utils.ServletUtil.writeErrorJson(response, 400, "请选择要下载的文件");
response.getWriter().write("{\"code\":400,\"message\":\"请选择要下载的文件\"}");
return; return;
} }
try { try {
byte[] zipBytes = fileService.createZipArchive(ids, principal.getUserId()); byte[] zipBytes = fileService.createZipArchive(ids, principal.getUserId());
com.filesystem.utils.ServletUtil.setZipDownloadHeaders(response, zipBytes.length);
response.setContentType("application/zip");
response.setHeader("Content-Disposition", "attachment; filename=\"download.zip\"");
response.setContentLength(zipBytes.length);
response.getOutputStream().write(zipBytes); response.getOutputStream().write(zipBytes);
response.getOutputStream().flush(); response.getOutputStream().flush();
} catch (RuntimeException e) { } catch (RuntimeException e) {
response.setContentType("application/json;charset=UTF-8"); com.filesystem.utils.ServletUtil.writeErrorJson(response, 400, e.getMessage());
response.getWriter().write("{\"code\":400,\"message\":\"" + e.getMessage().replace("\"", "\\\"") + "\"}");
} }
} }
@@ -352,20 +291,10 @@ public class FileController {
public ApiResult<Void> batchCancelShare( public ApiResult<Void> batchCancelShare(
@AuthenticationPrincipal UserPrincipal principal, @AuthenticationPrincipal UserPrincipal principal,
@RequestBody Map<String, Object> request) { @RequestBody Map<String, Object> request) {
Object idsObj = request.get("ids"); List<Long> ids = CommonUtil.parseIds(request.get("ids"));
if (idsObj == null) { if (ids.isEmpty()) {
return ApiResult.error("请选择要取消共享的文件"); 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) { for (Long fileId : ids) {
fileService.cancelShare(fileId, principal.getUserId()); fileService.cancelShare(fileId, principal.getUserId());
} }
@@ -376,20 +305,10 @@ public class FileController {
public ApiResult<Void> batchRestore( public ApiResult<Void> batchRestore(
@AuthenticationPrincipal UserPrincipal principal, @AuthenticationPrincipal UserPrincipal principal,
@RequestBody Map<String, Object> request) { @RequestBody Map<String, Object> request) {
Object idsObj = request.get("ids"); List<Long> ids = CommonUtil.parseIds(request.get("ids"));
if (idsObj == null) { if (ids.isEmpty()) {
return ApiResult.error("请选择要还原的文件"); 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) { for (Long fileId : ids) {
fileService.restoreFile(fileId, principal.getUserId()); 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.MessageService;
import com.filesystem.service.UserService; import com.filesystem.service.UserService;
import com.filesystem.utils.ApiResult; 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.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Value; 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.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException; import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
@@ -41,8 +39,6 @@ public class MessageController {
@Value("${file.storage.path:./uploads}") @Value("${file.storage.path:./uploads}")
private String storagePath; private String storagePath;
private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy/MM");
// ==================== 聊天文件上传 ==================== // ==================== 聊天文件上传 ====================
@PostMapping("/upload") @PostMapping("/upload")
@@ -55,10 +51,9 @@ public class MessageController {
} }
String originalName = file.getOriginalFilename(); String originalName = file.getOriginalFilename();
String ext = originalName != null && originalName.contains(".") String ext = FileUtil.getExtensionWithDot(originalName);
? originalName.substring(originalName.lastIndexOf(".")) : "";
String storedName = UUID.randomUUID().toString() + ext; 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); Path targetDir = Paths.get(storagePath).toAbsolutePath().resolve("chat").resolve(datePath);
if (!Files.exists(targetDir)) { if (!Files.exists(targetDir)) {
@@ -72,7 +67,7 @@ public class MessageController {
return ApiResult.success("上传成功", Map.of("url", fileUrl)); return ApiResult.success("上传成功", Map.of("url", fileUrl));
} }
// ==================== 聊天文件访问(返回 ResponseEntity<byte[]> 用于二进制响应)==================== // ==================== 聊天文件访问 ====================
@GetMapping("/file/**") @GetMapping("/file/**")
public ResponseEntity<byte[]> getChatFile(HttpServletRequest request) throws IOException { public ResponseEntity<byte[]> getChatFile(HttpServletRequest request) throws IOException {
@@ -80,7 +75,6 @@ public class MessageController {
String prefix = "/api/messages/file/"; String prefix = "/api/messages/file/";
String relativePath = uri.substring(uri.indexOf(prefix) + prefix.length()); String relativePath = uri.substring(uri.indexOf(prefix) + prefix.length());
// relativePath = "2026/04/xxx.jpg"
int lastSlash = relativePath.lastIndexOf('/'); int lastSlash = relativePath.lastIndexOf('/');
String datePath = relativePath.substring(0, lastSlash); String datePath = relativePath.substring(0, lastSlash);
String fileName = relativePath.substring(lastSlash + 1); String fileName = relativePath.substring(lastSlash + 1);
@@ -93,31 +87,11 @@ public class MessageController {
} }
byte[] content = Files.readAllBytes(filePath); byte[] content = Files.readAllBytes(filePath);
String ext = fileName.contains(".") ? fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase() : ""; boolean isImage = FileUtil.isImage(fileName);
boolean isImage = Set.of("png", "jpg", "jpeg", "gif", "webp", "bmp", "svg").contains(ext); return isImage
? FileUtil.buildImageResponse(content, fileName)
String contentType = switch (ext) { : FileUtil.buildDownloadResponse(content, fileName);
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);
} }
// ==================== 消息收发 ==================== // ==================== 消息收发 ====================
@@ -158,7 +132,7 @@ public class MessageController {
m.put("fileName", msg.getFileName()); m.put("fileName", msg.getFileName());
m.put("fileSize", msg.getFileSize()); m.put("fileSize", msg.getFileSize());
m.put("isRead", msg.getIsRead()); 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()); User fromUser = userMap.get(msg.getFromUserId());
if (fromUser != null) { if (fromUser != null) {
@@ -196,12 +170,11 @@ public class MessageController {
public ApiResult<Map<String, Object>> sendMessage( public ApiResult<Map<String, Object>> sendMessage(
@AuthenticationPrincipal UserPrincipal principal, @AuthenticationPrincipal UserPrincipal principal,
@RequestBody Map<String, Object> request) { @RequestBody Map<String, Object> request) {
Long toUserId = Long.valueOf(request.get("toUserId").toString()); Long toUserId = CommonUtil.getLong(request, "toUserId");
String content = (String) request.get("content"); String content = CommonUtil.getString(request, "content");
String type = (String) request.getOrDefault("type", "text"); String type = CommonUtil.getString(request, "type", "text");
String fileName = (String) request.get("fileName"); String fileName = CommonUtil.getString(request, "fileName");
Object fileSizeObj = request.get("fileSize"); Long fileSize = CommonUtil.getLong(request, "fileSize");
Long fileSize = fileSizeObj != null ? Long.valueOf(fileSizeObj.toString()) : null;
Message message = messageService.sendMessage(principal.getUserId(), toUserId, content, type); Message message = messageService.sendMessage(principal.getUserId(), toUserId, content, type);
@@ -219,7 +192,7 @@ public class MessageController {
result.put("type", message.getType()); result.put("type", message.getType());
result.put("fileName", message.getFileName()); result.put("fileName", message.getFileName());
result.put("fileSize", message.getFileSize()); 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()); User fromUser = userService.findById(principal.getUserId());
if (fromUser != null) { if (fromUser != null) {
@@ -241,7 +214,6 @@ public class MessageController {
public ApiResult<List<Map<String, Object>>> getUnreadList(@AuthenticationPrincipal UserPrincipal principal) { public ApiResult<List<Map<String, Object>>> getUnreadList(@AuthenticationPrincipal UserPrincipal principal) {
List<Message> unreadMessages = messageService.getUnreadMessages(principal.getUserId()); List<Message> unreadMessages = messageService.getUnreadMessages(principal.getUserId());
// 按发送人分组
Map<Long, List<Message>> grouped = unreadMessages.stream() Map<Long, List<Message>> grouped = unreadMessages.stream()
.collect(Collectors.groupingBy(Message::getFromUserId, LinkedHashMap::new, Collectors.toList())); .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()) { for (Map.Entry<Long, List<Message>> entry : grouped.entrySet()) {
Long fromUserId = entry.getKey(); Long fromUserId = entry.getKey();
List<Message> msgs = entry.getValue(); List<Message> msgs = entry.getValue();
Message lastMsg = msgs.get(0); // 已按时间倒序,第一条就是最新的 Message lastMsg = msgs.get(0);
User fromUser = userService.findById(fromUserId); User fromUser = userService.findById(fromUserId);
Map<String, Object> item = new HashMap<>(); Map<String, Object> item = new HashMap<>();
@@ -259,7 +231,7 @@ public class MessageController {
? "[图片]" : (lastMsg.getType() != null && lastMsg.getType().equals("file") ? "[图片]" : (lastMsg.getType() != null && lastMsg.getType().equals("file")
? "[文件]" : (lastMsg.getContent() != null && lastMsg.getContent().length() > 30 ? "[文件]" : (lastMsg.getContent() != null && lastMsg.getContent().length() > 30
? lastMsg.getContent().substring(0, 30) + "..." : lastMsg.getContent()))); ? 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) { if (fromUser != null) {
item.put("username", fromUser.getUsername()); item.put("username", fromUser.getUsername());
item.put("nickname", fromUser.getNickname() != null ? fromUser.getNickname() : 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.security.UserPrincipal;
import com.filesystem.service.UserService; import com.filesystem.service.UserService;
import com.filesystem.utils.ApiResult; import com.filesystem.utils.ApiResult;
import com.filesystem.utils.CommonUtil;
import com.filesystem.utils.FileUtil;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.annotation.AuthenticationPrincipal;
@@ -14,8 +16,6 @@ import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -29,8 +29,6 @@ public class UserController {
@Value("${file.storage.path:./uploads}") @Value("${file.storage.path:./uploads}")
private String storagePath; 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) { public ApiResult<List<Map<String, Object>>> getAllUsers(@AuthenticationPrincipal UserPrincipal principal) {
List<User> users = userService.getAllUsersExcept(principal.getUserId()); List<User> users = userService.getAllUsersExcept(principal.getUserId());
List<Map<String, Object>> result = users.stream() List<Map<String, Object>> result = users.stream()
.filter(u -> u.getStatus() == 1) // 只返回启用状态的用户 .filter(u -> u.getStatus() == 1)
.map(u -> { .map(u -> {
Map<String, Object> m = new HashMap<>(); Map<String, Object> m = new HashMap<>();
m.put("id", u.getId()); m.put("id", u.getId());
@@ -64,20 +62,14 @@ public class UserController {
return ApiResult.error("请选择图片"); return ApiResult.error("请选择图片");
} }
// 限制文件大小 2MB
if (file.getSize() > 2 * 1024 * 1024) { if (file.getSize() > 2 * 1024 * 1024) {
return ApiResult.error("图片大小不能超过2MB"); return ApiResult.error("图片大小不能超过2MB");
} }
// 保存文件 String ext = FileUtil.getExtensionWithDot(file.getOriginalFilename());
String originalFilename = file.getOriginalFilename();
String ext = originalFilename != null && originalFilename.contains(".")
? originalFilename.substring(originalFilename.lastIndexOf("."))
: ".jpg";
String fileName = UUID.randomUUID().toString() + ext; 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); Path uploadPath = Paths.get(storagePath).toAbsolutePath().resolve("avatars").resolve(datePath);
if (!Files.exists(uploadPath)) { if (!Files.exists(uploadPath)) {
Files.createDirectories(uploadPath); Files.createDirectories(uploadPath);
@@ -86,7 +78,6 @@ public class UserController {
Path filePath = uploadPath.resolve(fileName); Path filePath = uploadPath.resolve(fileName);
Files.copy(file.getInputStream(), filePath); Files.copy(file.getInputStream(), filePath);
// 更新用户头像
String avatarUrl = "/api/files/avatar/" + datePath + "/" + fileName; String avatarUrl = "/api/files/avatar/" + datePath + "/" + fileName;
userService.updateAvatar(principal.getUserId(), avatarUrl); userService.updateAvatar(principal.getUserId(), avatarUrl);
@@ -118,11 +109,13 @@ public class UserController {
@AuthenticationPrincipal UserPrincipal principal, @AuthenticationPrincipal UserPrincipal principal,
@RequestBody Map<String, String> request) { @RequestBody Map<String, String> request) {
try { try {
String nickname = request.get("nickname"); userService.updateProfile(
String signature = request.get("signature"); principal.getUserId(),
String phone = request.get("phone"); request.get("nickname"),
String email = request.get("email"); request.get("signature"),
userService.updateProfile(principal.getUserId(), nickname, signature, phone, email); request.get("phone"),
request.get("email")
);
return ApiResult.success("更新成功"); return ApiResult.success("更新成功");
} catch (RuntimeException e) { } catch (RuntimeException e) {
return ApiResult.error(e.getMessage()); return ApiResult.error(e.getMessage());
@@ -139,7 +132,7 @@ public class UserController {
String oldPassword = request.get("oldPassword"); String oldPassword = request.get("oldPassword");
String newPassword = request.get("newPassword"); String newPassword = request.get("newPassword");
if (oldPassword == null || oldPassword.isEmpty() || newPassword == null || newPassword.isEmpty()) { if (CommonUtil.isBlank(oldPassword) || CommonUtil.isBlank(newPassword)) {
return ApiResult.error("请填写完整信息"); return ApiResult.error("请填写完整信息");
} }

View File

@@ -0,0 +1,133 @@
package com.filesystem.utils;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* 通用工具类
*/
public class CommonUtil {
private CommonUtil() {}
// ========== 日期格式化 ==========
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final DateTimeFormatter DATE_PATH_FORMATTER = DateTimeFormatter.ofPattern("yyyy/MM");
/**
* 格式化日期时间yyyy-MM-dd HH:mm:ss
*/
public static String formatDateTime(LocalDateTime dateTime) {
if (dateTime == null) return "";
return dateTime.format(DATE_TIME_FORMATTER);
}
/**
* 获取日期路径格式yyyy/MM用于文件存储路径
*/
public static String getDatePath() {
return LocalDateTime.now().format(DATE_PATH_FORMATTER);
}
/**
* 获取当前时间字符串
*/
public static String now() {
return formatDateTime(LocalDateTime.now());
}
// ========== ID 列表解析 ==========
/**
* 从 Map 中解析 ID 列表
* 支持 List<Number>、List<String>、Number、String 等多种格式
*/
public static List<Long> parseIds(Object idsObj) {
List<Long> ids = new ArrayList<>();
if (idsObj == null) {
return ids;
}
if (idsObj instanceof List) {
for (Object id : (List<?>) idsObj) {
if (id instanceof Number) {
ids.add(((Number) id).longValue());
} else if (id instanceof String) {
String str = ((String) id).trim();
if (!str.isEmpty()) {
ids.add(Long.parseLong(str));
}
}
}
} else if (idsObj instanceof Number) {
ids.add(((Number) idsObj).longValue());
} else if (idsObj instanceof String) {
String str = ((String) idsObj).trim();
if (!str.isEmpty()) {
ids.add(Long.parseLong(str));
}
}
return ids;
}
/**
* 从请求体中解析 ID 列表
*/
public static List<Long> parseIdsFromRequest(Map<String, Object> request, String key) {
return parseIds(request.get(key));
}
// ========== 参数提取 ==========
/**
* 从 Map 中获取 Long 值
*/
public static Long getLong(Map<String, Object> map, String key) {
Object value = map.get(key);
if (value == null) return null;
if (value instanceof Number) {
return ((Number) value).longValue();
}
String str = value.toString();
if (str.isEmpty() || "null".equals(str) || "undefined".equals(str)) {
return null;
}
return Long.parseLong(str);
}
/**
* 从 Map 中获取 String 值
*/
public static String getString(Map<String, Object> map, String key) {
Object value = map.get(key);
return value != null ? value.toString() : null;
}
/**
* 从 Map 中获取指定默认值的 String
*/
public static String getString(Map<String, Object> map, String key, String defaultValue) {
String value = getString(map, key);
return value != null ? value : defaultValue;
}
/**
* 判断字符串是否为空或空白
*/
public static boolean isBlank(String str) {
return str == null || str.trim().isEmpty();
}
/**
* 判断字符串是否不为空
*/
public static boolean isNotBlank(String str) {
return !isBlank(str);
}
}

View File

@@ -0,0 +1,161 @@
package com.filesystem.utils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Map;
/**
* 文件相关工具类
*/
public class FileUtil {
private FileUtil() {}
// ========== 文件名编码 ==========
/**
* URL 编码文件名(用于 Content-Disposition 头)
*/
public static String encodeFileName(String fileName) {
return URLEncoder.encode(fileName, StandardCharsets.UTF_8).replace("+", "%20");
}
// ========== 扩展名提取 ==========
/**
* 获取文件扩展名(不带点,如 "jpg"
*/
public static String getExtension(String fileName) {
if (fileName == null || !fileName.contains(".")) {
return "";
}
return fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();
}
/**
* 获取文件扩展名(带点,如 ".jpg"
*/
public static String getExtensionWithDot(String fileName) {
if (fileName == null || !fileName.contains(".")) {
return "";
}
return fileName.substring(fileName.lastIndexOf(".")).toLowerCase();
}
// ========== Content-Type 判断 ==========
/**
* 根据文件名获取图片 Content-Type
*/
public static String getImageContentType(String fileName) {
String ext = getExtension(fileName);
return switch (ext) {
case "png" -> "image/png";
case "gif" -> "image/gif";
case "webp" -> "image/webp";
case "svg" -> "image/svg+xml";
case "bmp" -> "image/bmp";
case "ico" -> "image/x-icon";
default -> "image/jpeg";
};
}
/**
* 根据文件名获取 MediaType用于 Spring ResponseEntity
*/
public static MediaType getMediaType(String fileName) {
String ext = getExtensionWithDot(fileName);
return switch (ext) {
case ".jpg", ".jpeg" -> MediaType.IMAGE_JPEG;
case ".png" -> MediaType.IMAGE_PNG;
case ".gif" -> MediaType.IMAGE_GIF;
case ".webp" -> MediaType.parseMediaType("image/webp");
case ".pdf" -> MediaType.APPLICATION_PDF;
case ".svg" -> MediaType.parseMediaType("image/svg+xml");
default -> MediaType.APPLICATION_OCTET_STREAM;
};
}
/**
* 根据文件名判断是否为图片
*/
public static boolean isImage(String fileName) {
String ext = getExtension(fileName);
return java.util.Set.of("jpg", "jpeg", "png", "gif", "webp", "svg", "bmp", "ico").contains(ext);
}
// ========== 文件类型判断 ==========
/**
* 根据扩展名获取文件类型
*/
public static String getFileType(String extension) {
if (extension == null || extension.isEmpty()) {
return "file";
}
String ext = extension.toLowerCase().replace(".", "");
// 图片
if (java.util.Set.of("jpg", "jpeg", "png", "gif", "webp", "svg", "bmp", "ico").contains(ext)) {
return "image";
}
// 视频
if (java.util.Set.of("mp4", "avi", "mov", "wmv", "flv", "mkv", "webm").contains(ext)) {
return "video";
}
// 音频
if (java.util.Set.of("mp3", "wav", "flac", "aac", "ogg", "wma").contains(ext)) {
return "audio";
}
// PDF
if ("pdf".equals(ext)) {
return "pdf";
}
// 文档
if (java.util.Set.of("doc", "docx", "xls", "xlsx", "ppt", "pptx").contains(ext)) {
return "document";
}
// 压缩包
if (java.util.Set.of("zip", "rar", "7z", "tar", "gz").contains(ext)) {
return "archive";
}
return "file";
}
// ========== 响应构建 ==========
/**
* 构建文件下载响应
*/
public static ResponseEntity<byte[]> buildDownloadResponse(byte[] content, String fileName) {
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + encodeFileName(fileName) + "\"")
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(content);
}
/**
* 构建文件预览响应
*/
public static ResponseEntity<byte[]> buildPreviewResponse(byte[] content, String fileName) {
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + encodeFileName(fileName) + "\"")
.contentType(getMediaType(fileName))
.header(HttpHeaders.CACHE_CONTROL, "max-age=86400")
.body(content);
}
/**
* 构建图片响应
*/
public static ResponseEntity<byte[]> buildImageResponse(byte[] content, String fileName) {
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(getImageContentType(fileName)))
.header(HttpHeaders.CACHE_CONTROL, "max-age=86400")
.body(content);
}
}

View File

@@ -0,0 +1,74 @@
package com.filesystem.utils;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpHeaders;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
/**
* Servlet 响应工具类
*/
public class ServletUtil {
private ServletUtil() {}
/**
* 输出 JSON 响应
*/
public static void writeJson(HttpServletResponse response, int status, String json) throws IOException {
response.setStatus(status);
response.setContentType("application/json;charset=UTF-8");
response.setCharacterEncoding("UTF-8");
PrintWriter writer = response.getWriter();
writer.write(json);
writer.flush();
}
/**
* 输出错误 JSON 响应
*/
public static void writeErrorJson(HttpServletResponse response, int code, String message) throws IOException {
String json = String.format("{\"code\":%d,\"message\":\"%s\"}", code, escapeJson(message));
writeJson(response, code == 400 ? 400 : code, json);
}
/**
* 输出成功 JSON 响应
*/
public static void writeSuccessJson(HttpServletResponse response, String message) throws IOException {
writeJson(response, 200, String.format("{\"code\":200,\"message\":\"%s\"}", escapeJson(message)));
}
/**
* 设置文件下载响应头
*/
public static void setDownloadHeaders(HttpServletResponse response, String fileName, long contentLength) {
response.setContentType("application/octet-stream");
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + FileUtil.encodeFileName(fileName) + "\"");
response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(contentLength));
response.setHeader(HttpHeaders.CACHE_CONTROL, "no-cache");
}
/**
* 设置 ZIP 下载响应头
*/
public static void setZipDownloadHeaders(HttpServletResponse response, long contentLength) {
response.setContentType("application/zip");
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"download.zip\"");
response.setContentLengthLong(contentLength);
}
/**
* 转义 JSON 字符串中的特殊字符
*/
public static String escapeJson(String str) {
if (str == null) return "";
return str.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t");
}
}