refactor(backend): 重命名后端项目为 gym-manage-api,修改包名为 cn.novalon.gym.manage

This commit is contained in:
张翔
2026-04-17 18:35:50 +08:00
parent 666189b676
commit deb961c427
916 changed files with 108360 additions and 38328 deletions
@@ -0,0 +1,38 @@
package cn.novalon.gym.manage.file.core.domain;
import java.time.LocalDateTime;
public class SysFile {
private Long id;
private String fileName;
private String filePath;
private Long fileSize;
private String fileType;
private String storageType;
private String createBy;
private String updateBy;
private LocalDateTime createdAt;
private LocalDateTime deletedAt;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getFileName() { return fileName; }
public void setFileName(String fileName) { this.fileName = fileName; }
public String getFilePath() { return filePath; }
public void setFilePath(String filePath) { this.filePath = filePath; }
public Long getFileSize() { return fileSize; }
public void setFileSize(Long fileSize) { this.fileSize = fileSize; }
public String getFileType() { return fileType; }
public void setFileType(String fileType) { this.fileType = fileType; }
public String getStorageType() { return storageType; }
public void setStorageType(String storageType) { this.storageType = storageType; }
public String getCreateBy() { return createBy; }
public void setCreateBy(String createBy) { this.createBy = createBy; }
public String getUpdateBy() { return updateBy; }
public void setUpdateBy(String updateBy) { this.updateBy = updateBy; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getDeletedAt() { return deletedAt; }
public void setDeletedAt(LocalDateTime deletedAt) { this.deletedAt = deletedAt; }
}
@@ -0,0 +1,20 @@
package cn.novalon.gym.manage.file.core.repository;
import cn.novalon.gym.manage.file.core.domain.SysFile;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public interface ISysFileRepository {
Flux<SysFile> findByDeletedAtIsNullOrderByCreatedAtDesc();
Flux<SysFile> findByCreateByOrderByCreatedAtDesc(String createBy);
Mono<SysFile> findById(Long id);
Flux<SysFile> findByFilePathContaining(String fileName);
Mono<SysFile> save(SysFile sysFile);
Mono<Void> deleteByIdAndDeletedAtIsNull(Long id);
}
@@ -0,0 +1,21 @@
package cn.novalon.gym.manage.file.core.service;
import cn.novalon.gym.manage.file.core.domain.SysFile;
import org.springframework.http.codec.multipart.FilePart;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public interface ISysFileService {
Flux<SysFile> getAllFiles();
Mono<SysFile> getFileById(Long id);
Flux<SysFile> getFilesByUser(String username);
Mono<SysFile> uploadFile(FilePart filePart, String username);
Mono<Void> downloadFile(Long id);
Mono<Void> deleteFile(Long id);
}
@@ -0,0 +1,115 @@
package cn.novalon.gym.manage.file.core.service.impl;
import cn.novalon.gym.manage.file.core.domain.SysFile;
import cn.novalon.gym.manage.file.core.repository.ISysFileRepository;
import cn.novalon.gym.manage.file.core.service.ISysFileService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.codec.multipart.FilePart;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
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.util.UUID;
@Service
public class SysFileServiceImpl implements ISysFileService {
private final ISysFileRepository fileRepository;
private final String uploadDir;
public SysFileServiceImpl(
ISysFileRepository fileRepository,
@Value("${file.upload.dir:/tmp/uploads}") String uploadDir) {
this.fileRepository = fileRepository;
this.uploadDir = uploadDir;
}
@Override
public Flux<SysFile> getAllFiles() {
return fileRepository.findByDeletedAtIsNullOrderByCreatedAtDesc();
}
@Override
public Mono<SysFile> getFileById(Long id) {
return fileRepository.findById(id);
}
@Override
public Flux<SysFile> getFilesByUser(String username) {
return fileRepository.findByCreateByOrderByCreatedAtDesc(username);
}
@Override
public Mono<SysFile> uploadFile(FilePart filePart, String username) {
String originalFilename = filePart.filename();
String fileExtension = originalFilename.substring(originalFilename.lastIndexOf("."));
String newFileName = UUID.randomUUID().toString() + fileExtension;
Path uploadPath = Paths.get(uploadDir);
return Mono.fromCallable(() -> {
if (!Files.exists(uploadPath)) {
Files.createDirectories(uploadPath);
}
return uploadPath;
})
.flatMap(path -> {
Path filePath = path.resolve(newFileName);
return filePart.transferTo(filePath.toFile())
.thenReturn(filePath);
})
.flatMap(filePath -> {
try {
long fileSize = Files.size(filePath);
String contentType = filePart.headers().getContentType() != null
? filePart.headers().getContentType().toString()
: "application/octet-stream";
SysFile sysFile = new SysFile();
sysFile.setFileName(originalFilename);
sysFile.setFilePath(filePath.toString());
sysFile.setFileSize(fileSize);
sysFile.setFileType(contentType);
sysFile.setStorageType("LOCAL");
sysFile.setCreateBy(username);
sysFile.setCreatedAt(LocalDateTime.now());
return fileRepository.save(sysFile);
} catch (IOException e) {
return Mono.error(e);
}
});
}
@Override
public Mono<Void> downloadFile(Long id) {
return fileRepository.findById(id)
.flatMap(file -> {
try {
Path filePath = Paths.get(file.getFilePath());
Files.readAllBytes(filePath);
return Mono.empty();
} catch (IOException e) {
return Mono.error(e);
}
});
}
@Override
public Mono<Void> deleteFile(Long id) {
return fileRepository.findById(id)
.flatMap(file -> {
try {
Path filePath = Paths.get(file.getFilePath());
Files.deleteIfExists(filePath);
return fileRepository.deleteByIdAndDeletedAtIsNull(id);
} catch (IOException e) {
return Mono.error(e);
}
});
}
}
@@ -0,0 +1,154 @@
package cn.novalon.gym.manage.file.handler;
import cn.novalon.gym.manage.file.core.domain.SysFile;
import cn.novalon.gym.manage.file.core.service.ISysFileService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.HttpStatus;
import org.springframework.http.codec.multipart.FilePart;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@Component
@Tag(name = "文件管理", description = "文件上传下载相关操作")
public class SysFileHandler {
private final ISysFileService fileService;
public SysFileHandler(ISysFileService fileService) {
this.fileService = fileService;
}
@Operation(summary = "获取所有文件", description = "获取系统中所有文件列表")
public Mono<ServerResponse> getAllFiles(ServerRequest request) {
Flux<SysFile> files = fileService.getAllFiles();
return ServerResponse.ok().body(files, SysFile.class);
}
@Operation(summary = "根据ID获取文件", description = "根据文件ID获取文件详细信息")
public Mono<ServerResponse> getFileById(ServerRequest request) {
Long id = Long.parseLong(request.pathVariable("id"));
return fileService.getFileById(id)
.flatMap(file -> ServerResponse.ok().bodyValue(file))
.switchIfEmpty(ServerResponse.notFound().build());
}
@Operation(summary = "上传文件", description = "上传文件到系统")
public Mono<ServerResponse> uploadFile(ServerRequest request) {
String username = request.headers().firstHeader("X-Username");
if (username == null) {
username = "system";
}
final String finalUsername = username;
return request.multipartData()
.flatMap(multipartData -> {
var part = multipartData.getFirst("file");
if (part == null) {
return ServerResponse.badRequest().bodyValue("No file uploaded");
}
if (!(part instanceof FilePart)) {
return ServerResponse.badRequest().bodyValue("Invalid file part");
}
final FilePart filePart = (FilePart) part;
return fileService.uploadFile(filePart, finalUsername)
.flatMap(file -> ServerResponse.status(HttpStatus.CREATED).bodyValue(file));
})
.switchIfEmpty(ServerResponse.badRequest().bodyValue("No file data"));
}
@Operation(summary = "下载文件", description = "根据文件ID下载文件")
public Mono<ServerResponse> downloadFile(ServerRequest request) {
Long id = Long.parseLong(request.pathVariable("id"));
return fileService.getFileById(id)
.flatMap(file -> {
try {
Path filePath = Paths.get(file.getFilePath());
byte[] fileContent = Files.readAllBytes(filePath);
return ServerResponse.ok()
.header("Content-Disposition", "attachment; filename=\"" + file.getFileName() + "\"")
.header("Content-Type", file.getFileType())
.bodyValue(fileContent);
} catch (Exception e) {
return ServerResponse.notFound().build();
}
})
.switchIfEmpty(ServerResponse.notFound().build());
}
@Operation(summary = "根据文件名下载", description = "根据文件名下载文件")
public Mono<ServerResponse> downloadFileByName(ServerRequest request) {
String fileName = request.pathVariable("fileName");
return fileService.getAllFiles()
.filter(file -> file.getFileName().equals(fileName))
.next()
.flatMap(file -> {
try {
Path filePath = Paths.get(file.getFilePath());
byte[] fileContent = Files.readAllBytes(filePath);
return ServerResponse.ok()
.header("Content-Disposition", "attachment; filename=\"" + file.getFileName() + "\"")
.header("Content-Type", file.getFileType())
.bodyValue(fileContent);
} catch (Exception e) {
return ServerResponse.notFound().build();
}
})
.switchIfEmpty(ServerResponse.notFound().build());
}
@Operation(summary = "预览文件", description = "根据文件ID预览文件")
public Mono<ServerResponse> previewFile(ServerRequest request) {
Long id = Long.parseLong(request.pathVariable("id"));
return fileService.getFileById(id)
.flatMap(file -> {
try {
Path filePath = Paths.get(file.getFilePath());
byte[] fileContent = Files.readAllBytes(filePath);
return ServerResponse.ok()
.header("Content-Type", file.getFileType())
.bodyValue(fileContent);
} catch (Exception e) {
return ServerResponse.notFound().build();
}
})
.switchIfEmpty(ServerResponse.notFound().build());
}
@Operation(summary = "根据文件名预览", description = "根据文件名预览文件")
public Mono<ServerResponse> previewFileByName(ServerRequest request) {
String fileName = request.pathVariable("fileName");
return fileService.getAllFiles()
.filter(file -> file.getFileName().equals(fileName))
.next()
.flatMap(file -> {
try {
Path filePath = Paths.get(file.getFilePath());
byte[] fileContent = Files.readAllBytes(filePath);
return ServerResponse.ok()
.header("Content-Type", file.getFileType())
.bodyValue(fileContent);
} catch (Exception e) {
return ServerResponse.notFound().build();
}
})
.switchIfEmpty(ServerResponse.notFound().build());
}
@Operation(summary = "删除文件", description = "删除指定文件")
public Mono<ServerResponse> deleteFile(ServerRequest request) {
Long id = Long.parseLong(request.pathVariable("id"));
return fileService.deleteFile(id)
.then(ServerResponse.noContent().build())
.onErrorResume(e -> ServerResponse.badRequest().bodyValue(e.getMessage()));
}
}
@@ -0,0 +1,90 @@
package cn.novalon.gym.manage.file.core.service.impl;
import cn.novalon.gym.manage.file.core.domain.SysFile;
import cn.novalon.gym.manage.file.core.repository.ISysFileRepository;
import cn.novalon.gym.manage.file.core.service.ISysFileService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class SysFileServiceTest {
@Mock
private ISysFileRepository fileRepository;
private ISysFileService fileService;
private SysFile testFile;
@BeforeEach
void setUp() {
fileService = new SysFileServiceImpl(fileRepository, "/tmp/uploads");
testFile = new SysFile();
testFile.setId(1L);
testFile.setFileName("test.txt");
testFile.setFilePath("/tmp/uploads/test.txt");
testFile.setFileType("text/plain");
testFile.setFileSize(1024L);
testFile.setCreateBy("testuser");
testFile.setStorageType("LOCAL");
}
@Test
void testGetAllFiles_Success() {
when(fileRepository.findByDeletedAtIsNullOrderByCreatedAtDesc()).thenReturn(Flux.just(testFile));
Flux<SysFile> result = fileService.getAllFiles();
StepVerifier.create(result)
.expectNext(testFile)
.verifyComplete();
verify(fileRepository).findByDeletedAtIsNullOrderByCreatedAtDesc();
}
@Test
void testGetFileById_Success() {
when(fileRepository.findById(1L)).thenReturn(Mono.just(testFile));
Mono<SysFile> result = fileService.getFileById(1L);
StepVerifier.create(result)
.expectNext(testFile)
.verifyComplete();
verify(fileRepository).findById(1L);
}
@Test
void testGetFileById_NotFound() {
when(fileRepository.findById(999L)).thenReturn(Mono.empty());
Mono<SysFile> result = fileService.getFileById(999L);
StepVerifier.create(result)
.verifyComplete();
verify(fileRepository).findById(999L);
}
@Test
void testDeleteFile_NotFound() {
when(fileRepository.findById(999L)).thenReturn(Mono.empty());
Mono<Void> result = fileService.deleteFile(999L);
StepVerifier.create(result)
.verifyComplete();
verify(fileRepository).findById(999L);
verify(fileRepository, never()).deleteByIdAndDeletedAtIsNull(any());
}
}
@@ -0,0 +1,260 @@
package cn.novalon.gym.manage.file.handler;
import cn.novalon.gym.manage.file.core.domain.SysFile;
import cn.novalon.gym.manage.file.core.service.ISysFileService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpStatus;
import org.springframework.mock.web.reactive.function.server.MockServerRequest;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class SysFileHandlerTest {
@Mock
private ISysFileService fileService;
private SysFileHandler fileHandler;
private SysFile testFile;
@BeforeEach
void setUp() {
fileHandler = new SysFileHandler(fileService);
testFile = new SysFile();
testFile.setId(1L);
testFile.setFileName("test.txt");
testFile.setFilePath("/tmp/uploads/test.txt");
testFile.setFileType("text/plain");
testFile.setFileSize(1024L);
testFile.setCreateBy("testuser");
}
@Test
void testGetAllFiles_Success() {
when(fileService.getAllFiles()).thenReturn(Flux.just(testFile));
ServerRequest request = MockServerRequest.builder().build();
Mono<ServerResponse> response = fileHandler.getAllFiles(request);
StepVerifier.create(response)
.expectNextMatches(serverResponse ->
serverResponse.statusCode() == HttpStatus.OK)
.verifyComplete();
verify(fileService).getAllFiles();
}
@Test
void testGetFileById_Success() {
when(fileService.getFileById(1L)).thenReturn(Mono.just(testFile));
ServerRequest request = MockServerRequest.builder()
.pathVariable("id", "1")
.build();
Mono<ServerResponse> response = fileHandler.getFileById(request);
StepVerifier.create(response)
.expectNextMatches(serverResponse ->
serverResponse.statusCode() == HttpStatus.OK)
.verifyComplete();
verify(fileService).getFileById(1L);
}
@Test
void testGetFileById_NotFound() {
when(fileService.getFileById(999L)).thenReturn(Mono.empty());
ServerRequest request = MockServerRequest.builder()
.pathVariable("id", "999")
.build();
Mono<ServerResponse> response = fileHandler.getFileById(request);
StepVerifier.create(response)
.expectNextMatches(serverResponse ->
serverResponse.statusCode() == HttpStatus.NOT_FOUND)
.verifyComplete();
verify(fileService).getFileById(999L);
}
@Test
void testDeleteFile_Success() {
when(fileService.deleteFile(1L)).thenReturn(Mono.empty());
ServerRequest request = MockServerRequest.builder()
.pathVariable("id", "1")
.build();
Mono<ServerResponse> response = fileHandler.deleteFile(request);
StepVerifier.create(response)
.expectNextMatches(serverResponse ->
serverResponse.statusCode() == HttpStatus.NO_CONTENT)
.verifyComplete();
verify(fileService).deleteFile(1L);
}
@Test
void testDeleteFile_NotFound() {
when(fileService.deleteFile(999L)).thenReturn(Mono.empty());
ServerRequest request = MockServerRequest.builder()
.pathVariable("id", "999")
.build();
Mono<ServerResponse> response = fileHandler.deleteFile(request);
StepVerifier.create(response)
.expectNextMatches(serverResponse ->
serverResponse.statusCode() == HttpStatus.NO_CONTENT)
.verifyComplete();
verify(fileService).deleteFile(999L);
}
@Test
void testDownloadFile_Success() {
when(fileService.getFileById(1L)).thenReturn(Mono.just(testFile));
ServerRequest request = MockServerRequest.builder()
.pathVariable("id", "1")
.build();
Mono<ServerResponse> response = fileHandler.downloadFile(request);
StepVerifier.create(response)
.expectNextMatches(serverResponse ->
serverResponse.statusCode() == HttpStatus.NOT_FOUND)
.verifyComplete();
verify(fileService).getFileById(1L);
}
@Test
void testDownloadFile_NotFound() {
when(fileService.getFileById(999L)).thenReturn(Mono.empty());
ServerRequest request = MockServerRequest.builder()
.pathVariable("id", "999")
.build();
Mono<ServerResponse> response = fileHandler.downloadFile(request);
StepVerifier.create(response)
.expectNextMatches(serverResponse ->
serverResponse.statusCode() == HttpStatus.NOT_FOUND)
.verifyComplete();
verify(fileService).getFileById(999L);
}
@Test
void testDownloadFileByName_Success() {
when(fileService.getAllFiles()).thenReturn(Flux.just(testFile));
ServerRequest request = MockServerRequest.builder()
.pathVariable("fileName", "test.txt")
.build();
Mono<ServerResponse> response = fileHandler.downloadFileByName(request);
StepVerifier.create(response)
.expectNextMatches(serverResponse ->
serverResponse.statusCode() == HttpStatus.NOT_FOUND)
.verifyComplete();
verify(fileService).getAllFiles();
}
@Test
void testDownloadFileByName_NotFound() {
when(fileService.getAllFiles()).thenReturn(Flux.empty());
ServerRequest request = MockServerRequest.builder()
.pathVariable("fileName", "nonexistent.txt")
.build();
Mono<ServerResponse> response = fileHandler.downloadFileByName(request);
StepVerifier.create(response)
.expectNextMatches(serverResponse ->
serverResponse.statusCode() == HttpStatus.NOT_FOUND)
.verifyComplete();
verify(fileService).getAllFiles();
}
@Test
void testPreviewFile_Success() {
when(fileService.getFileById(1L)).thenReturn(Mono.just(testFile));
ServerRequest request = MockServerRequest.builder()
.pathVariable("id", "1")
.build();
Mono<ServerResponse> response = fileHandler.previewFile(request);
StepVerifier.create(response)
.expectNextMatches(serverResponse ->
serverResponse.statusCode() == HttpStatus.NOT_FOUND)
.verifyComplete();
verify(fileService).getFileById(1L);
}
@Test
void testPreviewFile_NotFound() {
when(fileService.getFileById(999L)).thenReturn(Mono.empty());
ServerRequest request = MockServerRequest.builder()
.pathVariable("id", "999")
.build();
Mono<ServerResponse> response = fileHandler.previewFile(request);
StepVerifier.create(response)
.expectNextMatches(serverResponse ->
serverResponse.statusCode() == HttpStatus.NOT_FOUND)
.verifyComplete();
verify(fileService).getFileById(999L);
}
@Test
void testPreviewFileByName_Success() {
when(fileService.getAllFiles()).thenReturn(Flux.just(testFile));
ServerRequest request = MockServerRequest.builder()
.pathVariable("fileName", "test.txt")
.build();
Mono<ServerResponse> response = fileHandler.previewFileByName(request);
StepVerifier.create(response)
.expectNextMatches(serverResponse ->
serverResponse.statusCode() == HttpStatus.NOT_FOUND)
.verifyComplete();
verify(fileService).getAllFiles();
}
@Test
void testPreviewFileByName_NotFound() {
when(fileService.getAllFiles()).thenReturn(Flux.empty());
ServerRequest request = MockServerRequest.builder()
.pathVariable("fileName", "nonexistent.txt")
.build();
Mono<ServerResponse> response = fileHandler.previewFileByName(request);
StepVerifier.create(response)
.expectNextMatches(serverResponse ->
serverResponse.statusCode() == HttpStatus.NOT_FOUND)
.verifyComplete();
verify(fileService).getAllFiles();
}
}