Files
novalon-manage-system/docs/plans/2026-03-12-infrastructure-refactoring-phase2.md
T

2199 lines
65 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Infrastructure Refactoring Phase 2 Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** 完成剩余 Handler 的函数式风格迁移、完善 Router 配置、优化其他 Converter、引入 MapStruct 和添加单元测试。
**Architecture:** 保持已建立的 DAO 层设计模式,将所有 Handler 从注解式迁移到函数式 WebFlux 风格,使用 MapStruct 自动生成转换代码,建立完整的单元测试覆盖。
**Tech Stack:** Spring WebFlux, R2DBC, MapStruct, JUnit 5, Mockito, Maven
---
## Phase 1: 完成其他 Handler 的函数式风格迁移
### Task 1: SysUserHandler 函数式迁移
**Files:**
- Modify: `handler/user/SysUserHandler.java`
**Step 1: 备份当前实现**
```bash
cp handler/user/SysUserHandler.java handler/user/SysUserHandler.java.bak
```
**Step 2: 修改为函数式风格**
```java
@Component
public class SysUserHandler {
private final ISysUserService userService;
public Mono<ServerResponse> getAllUsers(ServerRequest request) {
boolean includeDeleted = Boolean.valueOf(request.queryParam("includeDeleted").orElse("false"));
return ServerResponse.ok()
.body(userService.findAll(includeDeleted), SysUser.class);
}
public Mono<ServerResponse> getUsersByPage(ServerRequest request) {
int page = Integer.parseInt(request.queryParam("page").orElse("0"));
int size = Integer.parseInt(request.queryParam("size").orElse("10"));
String sort = request.queryParam("sort").orElse("id");
String order = request.queryParam("order").orElse("asc");
String keyword = request.queryParam("keyword").orElse(null);
PageRequest pageRequest = new PageRequest();
pageRequest.setPage(page);
pageRequest.setSize(size);
pageRequest.setSort(sort);
pageRequest.setOrder(order);
pageRequest.setKeyword(keyword);
return userService.findUsersByPage(pageRequest)
.flatMap(response -> ServerResponse.ok().bodyValue(response));
}
public Mono<ServerResponse> getUserById(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return userService.findById(id)
.flatMap(user -> ServerResponse.ok().bodyValue(user))
.switchIfEmpty(ServerResponse.notFound().build());
}
public Mono<ServerResponse> getUserByUsername(ServerRequest request) {
String username = request.pathVariable("username");
return userService.findByUsername(username)
.flatMap(user -> ServerResponse.ok().bodyValue(user))
.switchIfEmpty(ServerResponse.notFound().build());
}
public Mono<ServerResponse> createUser(ServerRequest request) {
return request.bodyToMono(SysUser.class)
.flatMap(userService::createUser)
.flatMap(user -> ServerResponse.status(HttpStatus.CREATED).bodyValue(user));
}
public Mono<ServerResponse> updateUser(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return request.bodyToMono(UserUpdateRequest.class)
.flatMap(req -> userService.findById(id)
.flatMap(existing -> {
if (req.getEmail() != null) existing.setEmail(req.getEmail());
if (req.getStatus() != null) existing.setStatus(req.getStatus());
if (req.getRoleId() != null) existing.setRoleId(req.getRoleId());
return userService.updateUser(existing);
}))
.flatMap(user -> ServerResponse.ok().bodyValue(user))
.switchIfEmpty(ServerResponse.notFound().build());
}
public Mono<ServerResponse> deleteUser(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return userService.deleteUser(id)
.then(ServerResponse.noContent().build());
}
public Mono<ServerResponse> changePassword(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return request.bodyToMono(PasswordChangeRequest.class)
.flatMap(req -> userService.changePassword(id, req.getOldPassword(), req.getNewPassword()))
.flatMap(user -> ServerResponse.ok().bodyValue(user));
}
public Mono<ServerResponse> logicalDeleteUser(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return userService.logicalDeleteUser(id)
.then(ServerResponse.noContent().build());
}
public Mono<ServerResponse> logicalDeleteUsers(ServerRequest request) {
return request.bodyToMono(new ParameterizedTypeReference<List<Long>>() {})
.flatMap(ids -> userService.logicalDeleteUsers(ids))
.then(ServerResponse.noContent().build());
}
public Mono<ServerResponse> restoreUser(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return userService.restoreUser(id)
.then(ServerResponse.noContent().build());
}
public Mono<ServerResponse> restoreUsers(ServerRequest request) {
return request.bodyToMono(new ParameterizedTypeReference<List<Long>>() {})
.flatMap(ids -> userService.restoreUsers(ids))
.then(ServerResponse.noContent().build());
}
public Mono<ServerResponse> checkUsernameExists(ServerRequest request) {
String username = request.queryParam("username").orElse(null);
return userService.existsByUsername(username)
.flatMap(exists -> ServerResponse.ok().bodyValue(exists));
}
public Mono<ServerResponse> checkEmailExists(ServerRequest request) {
String email = request.queryParam("email").orElse(null);
return userService.existsByEmail(email)
.flatMap(exists -> ServerResponse.ok().bodyValue(exists));
}
}
```
**Step 3: 测试路由配置**
```bash
curl -X GET http://localhost:8080/api/users
curl -X GET http://localhost:8080/api/users/1
curl -X POST http://localhost:8080/api/users -H "Content-Type: application/json" -d '{"username":"test","password":"123456"}'
```
**Step 4: 提交变更**
```bash
git add handler/user/SysUserHandler.java
git commit -m "refactor: migrate SysUserHandler to functional WebFlux style"
```
---
### Task 2: SysRoleHandler 函数式迁移
**Files:**
- Modify: `handler/role/SysRoleHandler.java`
**Step 1: 修改为函数式风格**
```java
@Component
public class SysRoleHandler {
private final ISysRoleService roleService;
public Mono<ServerResponse> getAllRoles(ServerRequest request) {
return ServerResponse.ok()
.body(roleService.findAll(), SysRole.class);
}
public Mono<ServerResponse> getRoleById(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return roleService.findById(id)
.flatMap(role -> ServerResponse.ok().bodyValue(role))
.switchIfEmpty(ServerResponse.notFound().build());
}
public Mono<ServerResponse> createRole(ServerRequest request) {
return request.bodyToMono(SysRole.class)
.flatMap(roleService::save)
.flatMap(role -> ServerResponse.status(HttpStatus.CREATED).bodyValue(role));
}
public Mono<ServerResponse> updateRole(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return request.bodyToMono(SysRole.class)
.flatMap(role -> roleService.update(id, role))
.flatMap(updatedRole -> ServerResponse.ok().bodyValue(updatedRole))
.switchIfEmpty(ServerResponse.notFound().build());
}
public Mono<ServerResponse> deleteRole(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return roleService.deleteById(id)
.then(ServerResponse.noContent().build());
}
}
```
**Step 2: 提交变更**
```bash
git add handler/role/SysRoleHandler.java
git commit -m "refactor: migrate SysRoleHandler to functional WebFlux style"
```
---
### Task 3: SysConfigHandler 函数式迁移
**Files:**
- Modify: `handler/config/SysConfigHandler.java`
**Step 1: 修改为函数式风格**
```java
@Component
public class SysConfigHandler {
private final ISysConfigService configService;
public Mono<ServerResponse> getAllConfigs(ServerRequest request) {
return ServerResponse.ok()
.body(configService.findAll(), SysConfig.class);
}
public Mono<ServerResponse> getConfigByKey(ServerRequest request) {
String configKey = request.pathVariable("configKey");
return configService.findByConfigKey(configKey)
.flatMap(config -> ServerResponse.ok().bodyValue(config))
.switchIfEmpty(ServerResponse.notFound().build());
}
public Mono<ServerResponse> createConfig(ServerRequest request) {
return request.bodyToMono(SysConfig.class)
.flatMap(configService::save)
.flatMap(config -> ServerResponse.status(HttpStatus.CREATED).bodyValue(config));
}
public Mono<ServerResponse> updateConfig(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return request.bodyToMono(SysConfig.class)
.flatMap(config -> configService.update(id, config))
.flatMap(updatedConfig -> ServerResponse.ok().bodyValue(updatedConfig))
.switchIfEmpty(ServerResponse.notFound().build());
}
public Mono<ServerResponse> deleteConfig(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return configService.deleteById(id)
.then(ServerResponse.noContent().build());
}
}
```
**Step 2: 提交变更**
```bash
git add handler/config/SysConfigHandler.java
git commit -m "refactor: migrate SysConfigHandler to functional WebFlux style"
```
---
### Task 4: SysNoticeHandler 函数式迁移
**Files:**
- Modify: `handler/notice/SysNoticeHandler.java`
**Step 1: 修改为函数式风格**
```java
@Component
public class SysNoticeHandler {
private final ISysNoticeService noticeService;
public Mono<ServerResponse> getAllNotices(ServerRequest request) {
return ServerResponse.ok()
.body(noticeService.findAll(), SysNotice.class);
}
public Mono<ServerResponse> getNoticeById(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return noticeService.findById(id)
.flatMap(notice -> ServerResponse.ok().bodyValue(notice))
.switchIfEmpty(ServerResponse.notFound().build());
}
public Mono<ServerResponse> createNotice(ServerRequest request) {
return request.bodyToMono(SysNotice.class)
.flatMap(noticeService::save)
.flatMap(notice -> ServerResponse.status(HttpStatus.CREATED).bodyValue(notice));
}
public Mono<ServerResponse> updateNotice(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return request.bodyToMono(SysNotice.class)
.flatMap(notice -> noticeService.update(id, notice))
.flatMap(updatedNotice -> ServerResponse.ok().bodyValue(updatedNotice))
.switchIfEmpty(ServerResponse.notFound().build());
}
public Mono<ServerResponse> deleteNotice(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return noticeService.deleteById(id)
.then(ServerResponse.noContent().build());
}
}
```
**Step 2: 提交变更**
```bash
git add handler/notice/SysNoticeHandler.java
git commit -m "refactor: migrate SysNoticeHandler to functional WebFlux style"
```
---
### Task 5: SysFileHandler 函数式迁移
**Files:**
- Modify: `handler/file/SysFileHandler.java`
**Step 1: 修改为函数式风格**
```java
@Component
public class SysFileHandler {
private final ISysFileService fileService;
public Mono<ServerResponse> getAllFiles(ServerRequest request) {
return ServerResponse.ok()
.body(fileService.findAll(), SysFile.class);
}
public Mono<ServerResponse> getFileById(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return fileService.findById(id)
.flatMap(file -> ServerResponse.ok().bodyValue(file))
.switchIfEmpty(ServerResponse.notFound().build());
}
public Mono<ServerResponse> uploadFile(ServerRequest request) {
return request.multipartData()
.flatMap(data -> {
String filename = data.toSingleValueMap().getFirst("file").filename();
return fileService.saveFile(data)
.flatMap(file -> ServerResponse.status(HttpStatus.CREATED).bodyValue(file));
});
}
public Mono<ServerResponse> deleteFile(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return fileService.deleteById(id)
.then(ServerResponse.noContent().build());
}
}
```
**Step 2: 提交变更**
```bash
git add handler/file/SysFileHandler.java
git commit -m "refactor: migrate SysFileHandler to functional WebFlux style"
```
---
### Task 6: SysLogHandler 函数式迁移
**Files:**
- Modify: `handler/log/SysLogHandler.java`
**Step 1: 修改为函数式风格**
```java
@Component
public class SysLogHandler {
private final IOperationLogService operationLogService;
private final ISysLoginLogService loginLogService;
public Mono<ServerResponse> getOperationLogs(ServerRequest request) {
return ServerResponse.ok()
.body(operationLogService.findAll(), OperationLog.class);
}
public Mono<ServerResponse> getOperationLogsByPage(ServerRequest request) {
int page = Integer.parseInt(request.queryParam("page").orElse("0"));
int size = Integer.parseInt(request.queryParam("size").orElse("10"));
String sort = request.queryParam("sort").orElse("createdAt");
String order = request.queryParam("order").orElse("desc");
PageRequest pageRequest = new PageRequest();
pageRequest.setPage(page);
pageRequest.setSize(size);
pageRequest.setSort(sort);
pageRequest.setOrder(order);
return operationLogService.findLogsByPage(pageRequest)
.flatMap(response -> ServerResponse.ok().bodyValue(response));
}
public Mono<ServerResponse> getLoginLogs(ServerRequest request) {
return ServerResponse.ok()
.body(loginLogService.findAll(), SysLoginLog.class);
}
public Mono<ServerResponse> getLoginLogsByPage(ServerRequest request) {
int page = Integer.parseInt(request.queryParam("page").orElse("0"));
int size = Integer.parseInt(request.queryParam("size").orElse("10"));
String sort = request.queryParam("sort").orElse("createdAt");
String order = request.queryParam("order").orElse("desc");
PageRequest pageRequest = new PageRequest();
pageRequest.setPage(page);
pageRequest.setSize(size);
pageRequest.setSort(sort);
pageRequest.setOrder(order);
return loginLogService.findLogsByPage(pageRequest)
.flatMap(response -> ServerResponse.ok().bodyValue(response));
}
}
```
**Step 2: 提交变更**
```bash
git add handler/log/SysLogHandler.java
git commit -m "refactor: migrate SysLogHandler to functional WebFlux style"
```
---
### Task 7: SysAuthHandler 函数式迁移
**Files:**
- Modify: `handler/auth/SysAuthHandler.java`
**Step 1: 修改为函数式风格**
```java
@Component
public class SysAuthHandler {
private final ISysUserService userService;
private final JwtTokenProvider tokenProvider;
public Mono<ServerResponse> login(ServerRequest request) {
return request.bodyToMono(LoginRequest.class)
.flatMap(loginRequest -> userService.authenticate(loginRequest.getUsername(), loginRequest.getPassword()))
.flatMap(user -> {
String token = tokenProvider.generateToken(user);
AuthResponse response = new AuthResponse(token, user);
return ServerResponse.ok().bodyValue(response);
});
}
public Mono<ServerResponse> register(ServerRequest request) {
return request.bodyToMono(UserRegisterRequest.class)
.flatMap(userService::registerUser)
.flatMap(user -> ServerResponse.status(HttpStatus.CREATED).bodyValue(user));
}
public Mono<ServerResponse> refreshToken(ServerRequest request) {
return request.bodyToMono(RefreshTokenRequest.class)
.flatMap(refreshRequest -> tokenProvider.validateToken(refreshRequest.getToken())
.flatMap(username -> userService.findByUsername(username))
.flatMap(user -> {
String newToken = tokenProvider.generateToken(user);
AuthResponse response = new AuthResponse(newToken, user);
return ServerResponse.ok().bodyValue(response);
}));
}
}
```
**Step 2: 提交变更**
```bash
git add handler/auth/SysAuthHandler.java
git commit -m "refactor: migrate SysAuthHandler to functional WebFlux style"
```
---
### Task 8: SysUserMessageHandler 函数式迁移
**Files:**
- Modify: `handler/message/SysUserMessageHandler.java`
**Step 1: 修改为函数式风格**
```java
@Component
public class SysUserMessageHandler {
private final ISysUserMessageService messageService;
public Mono<ServerResponse> getAllMessages(ServerRequest request) {
return ServerResponse.ok()
.body(messageService.findAll(), SysUserMessage.class);
}
public Mono<ServerResponse> getMessageById(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return messageService.findById(id)
.flatMap(message -> ServerResponse.ok().bodyValue(message))
.switchIfEmpty(ServerResponse.notFound().build());
}
public Mono<ServerResponse> createMessage(ServerRequest request) {
return request.bodyToMono(SysUserMessage.class)
.flatMap(messageService::save)
.flatMap(message -> ServerResponse.status(HttpStatus.CREATED).bodyValue(message));
}
public Mono<ServerResponse> markAsRead(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return messageService.markAsRead(id)
.then(ServerResponse.ok().build());
}
public Mono<ServerResponse> deleteMessage(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return messageService.deleteById(id)
.then(ServerResponse.noContent().build());
}
}
```
**Step 2: 提交变更**
```bash
git add handler/message/SysUserMessageHandler.java
git commit -m "refactor: migrate SysUserMessageHandler to functional WebFlux style"
```
---
### Task 9: StatsHandler 函数式迁移
**Files:**
- Modify: `handler/stats/StatsHandler.java`
**Step 1: 修改为函数式风格**
```java
@Component
public class StatsHandler {
private final ISysUserService userService;
private final IOperationLogService operationLogService;
public Mono<ServerResponse> getUserStats(ServerRequest request) {
return userService.count()
.flatMap(count -> {
Map<String, Object> stats = new HashMap<>();
stats.put("totalUsers", count);
return ServerResponse.ok().bodyValue(stats);
});
}
public Mono<ServerResponse> getOperationStats(ServerRequest request) {
return operationLogService.count()
.flatMap(count -> {
Map<String, Object> stats = new HashMap<>();
stats.put("totalOperations", count);
return ServerResponse.ok().bodyValue(stats);
});
}
public Mono<ServerResponse> getSystemStats(ServerRequest request) {
return Mono.zip(
userService.count(),
operationLogService.count()
).map(tuple -> {
Map<String, Object> stats = new HashMap<>();
stats.put("totalUsers", tuple.getT1());
stats.put("totalOperations", tuple.getT2());
return stats;
}).flatMap(stats -> ServerResponse.ok().bodyValue(stats));
}
}
```
**Step 2: 提交变更**
```bash
git add handler/stats/StatsHandler.java
git commit -m "refactor: migrate StatsHandler to functional WebFlux style"
```
---
## Phase 2: 完善 Router 配置
### Task 10: 扩展 SystemRouter 配置
**Files:**
- Modify: `config/SystemRouter.java`
**Step 1: 添加所有模块的路由配置**
```java
package cn.novalon.manage.sys.config;
import cn.novalon.manage.sys.handler.auth.SysAuthHandler;
import cn.novalon.manage.sys.handler.config.SysConfigHandler;
import cn.novalon.manage.sys.handler.dictionary.DictionaryHandler;
import cn.novalon.manage.sys.handler.file.SysFileHandler;
import cn.novalon.manage.sys.handler.log.SysLogHandler;
import cn.novalon.manage.sys.handler.message.SysUserMessageHandler;
import cn.novalon.manage.sys.handler.notice.SysNoticeHandler;
import cn.novalon.manage.sys.handler.role.SysRoleHandler;
import cn.novalon.manage.sys.handler.stats.StatsHandler;
import cn.novalon.manage.sys.handler.user.SysUserHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import static org.springframework.web.reactive.function.server.RequestPredicates.*;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
@Configuration
public class SystemRouter {
@Bean
public RouterFunction<ServerResponse> systemRoutes(
DictionaryHandler dictionaryHandler,
SysUserHandler userHandler,
SysRoleHandler roleHandler,
SysConfigHandler configHandler,
SysNoticeHandler noticeHandler,
SysFileHandler fileHandler,
SysLogHandler logHandler,
SysAuthHandler authHandler,
SysUserMessageHandler messageHandler,
StatsHandler statsHandler) {
return route()
.path("/api", builder -> builder
.path("/dictionaries", dictRoutes(dictionaryHandler))
.path("/users", userRoutes(userHandler))
.path("/roles", roleRoutes(roleHandler))
.path("/configs", configRoutes(configHandler))
.path("/notices", noticeRoutes(noticeHandler))
.path("/files", fileRoutes(fileHandler))
.path("/logs", logRoutes(logHandler))
.path("/auth", authRoutes(authHandler))
.path("/messages", messageRoutes(messageHandler))
.path("/stats", statsRoutes(statsHandler))
.build())
.build();
}
private RouterFunction<ServerResponse> dictRoutes(DictionaryHandler handler) {
return route()
.GET(handler::getAllDictionaries)
.GET("/{id}", handler::getDictionaryById)
.GET("/type/{type}", handler::getDictionariesByType)
.GET("/check/exists", handler::checkTypeAndCodeExists)
.POST(handler::createDictionary)
.PUT("/{id}", handler::updateDictionary)
.DELETE("/{id}", handler::deleteDictionary)
.build();
}
private RouterFunction<ServerResponse> userRoutes(SysUserHandler handler) {
return route()
.GET(handler::getAllUsers)
.GET("/page", handler::getUsersByPage)
.GET("/{id}", handler::getUserById)
.GET("/username/{username}", handler::getUserByUsername)
.POST(handler::createUser)
.PUT("/{id}", handler::updateUser)
.DELETE("/{id}", handler::deleteUser)
.POST("/{id}/password", handler::changePassword)
.DELETE("/{id}/logical", handler::logicalDeleteUser)
.POST("/logical-delete", handler::logicalDeleteUsers)
.POST("/{id}/restore", handler::restoreUser)
.POST("/restore", handler::restoreUsers)
.GET("/check/username", handler::checkUsernameExists)
.GET("/check/email", handler::checkEmailExists)
.build();
}
private RouterFunction<ServerResponse> roleRoutes(SysRoleHandler handler) {
return route()
.GET(handler::getAllRoles)
.GET("/{id}", handler::getRoleById)
.POST(handler::createRole)
.PUT("/{id}", handler::updateRole)
.DELETE("/{id}", handler::deleteRole)
.build();
}
private RouterFunction<ServerResponse> configRoutes(SysConfigHandler handler) {
return route()
.GET(handler::getAllConfigs)
.GET("/key/{configKey}", handler::getConfigByKey)
.POST(handler::createConfig)
.PUT("/{id}", handler::updateConfig)
.DELETE("/{id}", handler::deleteConfig)
.build();
}
private RouterFunction<ServerResponse> noticeRoutes(SysNoticeHandler handler) {
return route()
.GET(handler::getAllNotices)
.GET("/{id}", handler::getNoticeById)
.POST(handler::createNotice)
.PUT("/{id}", handler::updateNotice)
.DELETE("/{id}", handler::deleteNotice)
.build();
}
private RouterFunction<ServerResponse> fileRoutes(SysFileHandler handler) {
return route()
.GET(handler::getAllFiles)
.GET("/{id}", handler::getFileById)
.POST("/upload", handler::uploadFile)
.DELETE("/{id}", handler::deleteFile)
.build();
}
private RouterFunction<ServerResponse> logRoutes(SysLogHandler handler) {
return route()
.GET("/operations", handler::getOperationLogs)
.GET("/operations/page", handler::getOperationLogsByPage)
.GET("/logins", handler::getLoginLogs)
.GET("/logins/page", handler::getLoginLogsByPage)
.build();
}
private RouterFunction<ServerResponse> authRoutes(SysAuthHandler handler) {
return route()
.POST("/login", handler::login)
.POST("/register", handler::register)
.POST("/refresh", handler::refreshToken)
.build();
}
private RouterFunction<ServerResponse> messageRoutes(SysUserMessageHandler handler) {
return route()
.GET(handler::getAllMessages)
.GET("/{id}", handler::getMessageById)
.POST(handler::createMessage)
.PUT("/{id}/read", handler::markAsRead)
.DELETE("/{id}", handler::deleteMessage)
.build();
}
private RouterFunction<ServerResponse> statsRoutes(StatsHandler handler) {
return route()
.GET("/users", handler::getUserStats)
.GET("/operations", handler::getOperationStats)
.GET("/system", handler::getSystemStats)
.build();
}
}
```
**Step 2: 测试路由配置**
```bash
curl -X GET http://localhost:8080/api/dictionaries
curl -X GET http://localhost:8080/api/users
curl -X GET http://localhost:8080/api/roles
curl -X GET http://localhost:8080/api/stats/system
```
**Step 3: 提交变更**
```bash
git add config/SystemRouter.java
git commit -m "feat: add comprehensive router configuration for all modules"
```
---
## Phase 3: 优化其他 Converter
### Task 11: 为其他 Converter 添加批量转换方法
**Files:**
- Modify: `converter/SysRoleConverter.java`
- Modify: `converter/SysConfigConverter.java`
- Modify: `converter/SysNoticeConverter.java`
- Modify: `converter/SysFileConverter.java`
- Modify: `converter/SysLoginLogConverter.java`
- Modify: `converter/SysDictDataConverter.java`
- Modify: `converter/SysDictTypeConverter.java`
- Modify: `converter/SysMenuConverter.java`
- Modify: `converter/SysUserMessageConverter.java`
- Modify: `converter/SysExceptionLogConverter.java`
**Step 1: 为 SysRoleConverter 添加批量转换**
```java
public List<SysRole> toDomainList(List<SysRoleEntity> entities) {
if (entities == null) {
return null;
}
return entities.stream()
.map(this::toDomain)
.collect(Collectors.toList());
}
public List<SysRoleEntity> toEntityList(List<SysRole> domains) {
if (domains == null) {
return null;
}
return domains.stream()
.map(this::toEntity)
.collect(Collectors.toList());
}
```
**Step 2: 为其他 Converter 添加批量转换方法**
重复 Step 1 的模式,为所有其他 Converter 添加 `toDomainList()``toEntityList()` 方法。
**Step 3: 提交变更**
```bash
git add converter/
git commit -m "feat: add batch conversion methods to all converters"
```
---
## Phase 4: 引入 MapStruct
### Task 12: 配置 MapStruct 依赖
**Files:**
- Modify: `pom.xml`
**Step 1: 添加 MapStruct 依赖**
```xml
<properties>
<org.mapstruct.version>1.5.5.Final</org.mapstruct.version>
<lombok-mapstruct-binding.version>0.2.0</lombok-mapstruct-binding.version>
</properties>
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>${lombok-mapstruct-binding.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>${lombok-mapstruct-binding.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
```
**Step 2: 提交变更**
```bash
git add pom.xml
git commit -m "feat: add MapStruct dependency and configuration"
```
---
### Task 13: 创建 MapStruct Mapper 接口
**Files:**
- Create: `infrastructure/db/mapper/DictionaryMapper.java`
- Create: `infrastructure/db/mapper/SysUserMapper.java`
- Create: `infrastructure/db/mapper/SysRoleMapper.java`
- Create: `infrastructure/db/mapper/SysConfigMapper.java`
- Create: `infrastructure/db/mapper/SysNoticeMapper.java`
- Create: `infrastructure/db/mapper/SysFileMapper.java`
- Create: `infrastructure/db/mapper/OperationLogMapper.java`
- Create: `infrastructure/db/mapper/SysLoginLogMapper.java`
- Create: `infrastructure/db/mapper/SysDictDataMapper.java`
- Create: `infrastructure/db/mapper/SysDictTypeMapper.java`
- Create: `infrastructure/db/mapper/SysMenuMapper.java`
- Create: `infrastructure/db/mapper/SysUserMessageMapper.java`
- Create: `infrastructure/db/mapper/SysExceptionLogMapper.java`
**Step 1: 创建 DictionaryMapper 接口**
```java
package cn.novalon.manage.sys.infrastructure.db.mapper;
import cn.novalon.manage.sys.core.domain.Dictionary;
import cn.novalon.manage.sys.infrastructure.db.entity.DictionaryEntity;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Named;
import java.util.List;
@Mapper(componentModel = "spring")
public interface DictionaryMapper {
Dictionary toDomain(DictionaryEntity entity);
DictionaryEntity toEntity(Dictionary domain);
List<Dictionary> toDomainList(List<DictionaryEntity> entities);
List<DictionaryEntity> toEntityList(List<Dictionary> domains);
}
```
**Step 2: 创建其他 Mapper 接口**
重复 Step 1 的模式,为所有实体创建对应的 Mapper 接口。
**Step 3: 提交变更**
```bash
git add infrastructure/db/mapper/
git commit -m "feat: add MapStruct mapper interfaces for all entities"
```
---
### Task 14: 使用 MapStruct 替换手动 Converter
**Files:**
- Modify: `repository/DictionaryRepository.java`
- Modify: `repository/SysUserRepository.java`
- Modify: `repository/SysRoleRepository.java`
- Modify: `repository/SysConfigRepository.java`
- Modify: `repository/SysNoticeRepository.java`
- Modify: `repository/SysFileRepository.java`
- Modify: `repository/OperationLogRepository.java`
- Modify: `repository/SysLoginLogRepository.java`
- Modify: `repository/SysDictDataRepository.java`
- Modify: `repository/SysDictTypeRepository.java`
- Modify: `repository/SysMenuRepository.java`
- Modify: `repository/SysUserMessageRepository.java`
- Modify: `repository/SysExceptionLogRepository.java`
**Step 1: 修改 DictionaryRepository 使用 MapStruct**
```java
@Repository
public class DictionaryRepository {
private final DictionaryDao dao;
private final DictionaryMapper mapper;
public DictionaryRepository(DictionaryDao dao, DictionaryMapper mapper) {
this.dao = dao;
this.mapper = mapper;
}
public Flux<Dictionary> findByType(String type) {
return dao.findByType(type)
.map(mapper::toDomain);
}
public Mono<Dictionary> findByTypeAndCode(String type, String code) {
return dao.findByTypeAndCode(type, code)
.map(mapper::toDomain);
}
public Flux<Dictionary> findAll() {
return dao.findByDeletedAtIsNullOrderBySortAsc()
.map(mapper::toDomain);
}
public Mono<Dictionary> findById(Long id) {
return dao.findById(id)
.map(mapper::toDomain);
}
public Mono<Dictionary> save(Dictionary dictionary) {
return dao.save(mapper.toEntity(dictionary))
.map(mapper::toDomain);
}
public Mono<Void> deleteById(Long id) {
return dao.deleteByIdAndDeletedAtIsNull(id);
}
}
```
**Step 2: 修改其他 Repository 使用 MapStruct**
重复 Step 1 的模式,为所有 Repository 替换 Converter 为 Mapper。
**Step 3: 删除旧的 Converter 类**
```bash
rm converter/DictionaryConverter.java
rm converter/SysUserConverter.java
rm converter/SysRoleConverter.java
rm converter/SysConfigConverter.java
rm converter/SysNoticeConverter.java
rm converter/SysFileConverter.java
rm converter/OperationLogConverter.java
rm converter/SysLoginLogConverter.java
rm converter/SysDictDataConverter.java
rm converter/SysDictTypeConverter.java
rm converter/SysMenuConverter.java
rm converter/SysUserMessageConverter.java
rm converter/SysExceptionLogConverter.java
```
**Step 4: 提交变更**
```bash
git add repository/ converter/
git commit -m "refactor: replace manual converters with MapStruct mappers"
```
---
## Phase 5: 添加单元测试
### Task 15: 为 DictionaryRepository 添加单元测试
**Files:**
- Create: `infrastructure/db/repository/DictionaryRepositoryTest.java`
**Step 1: 创建测试类**
```java
package cn.novalon.manage.sys.infrastructure.db.repository;
import cn.novalon.manage.sys.core.domain.Dictionary;
import cn.novalon.manage.sys.infrastructure.db.converter.DictionaryConverter;
import cn.novalon.manage.sys.infrastructure.db.dao.DictionaryDao;
import cn.novalon.manage.sys.infrastructure.db.entity.DictionaryEntity;
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 java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class DictionaryRepositoryTest {
@Mock
private DictionaryDao dao;
@Mock
private DictionaryConverter converter;
private DictionaryRepository repository;
private DictionaryEntity testEntity;
private Dictionary testDomain;
@BeforeEach
void setUp() {
repository = new DictionaryRepository(dao, converter);
testEntity = new DictionaryEntity();
testEntity.setId(1L);
testEntity.setType("test_type");
testEntity.setCode("test_code");
testEntity.setName("Test Dictionary");
testEntity.setValue("test_value");
testEntity.setSort(1);
testEntity.setCreatedAt(LocalDateTime.now());
testEntity.setUpdatedAt(LocalDateTime.now());
testDomain = new Dictionary();
testDomain.setId(1L);
testDomain.setType("test_type");
testDomain.setCode("test_code");
testDomain.setName("Test Dictionary");
testDomain.setValue("test_value");
testDomain.setSort(1);
testDomain.setCreatedAt(LocalDateTime.now());
testDomain.setUpdatedAt(LocalDateTime.now());
}
@Test
void findByType_ShouldReturnDictionaries() {
when(dao.findByType("test_type")).thenReturn(Flux.just(testEntity));
when(converter.toDomain(testEntity)).thenReturn(testDomain);
Flux<Dictionary> result = repository.findByType("test_type");
StepVerifier.create(result)
.expectNext(testDomain)
.verifyComplete();
}
@Test
void findByTypeAndCode_ShouldReturnDictionary() {
when(dao.findByTypeAndCode("test_type", "test_code")).thenReturn(Mono.just(testEntity));
when(converter.toDomain(testEntity)).thenReturn(testDomain);
Mono<Dictionary> result = repository.findByTypeAndCode("test_type", "test_code");
StepVerifier.create(result)
.expectNext(testDomain)
.verifyComplete();
}
@Test
void findAll_ShouldReturnAllDictionaries() {
List<DictionaryEntity> entities = Arrays.asList(testEntity);
when(dao.findByDeletedAtIsNullOrderBySortAsc()).thenReturn(Flux.fromIterable(entities));
when(converter.toDomain(testEntity)).thenReturn(testDomain);
Flux<Dictionary> result = repository.findAll();
StepVerifier.create(result)
.expectNext(testDomain)
.verifyComplete();
}
@Test
void findById_ShouldReturnDictionary() {
when(dao.findById(1L)).thenReturn(Mono.just(testEntity));
when(converter.toDomain(testEntity)).thenReturn(testDomain);
Mono<Dictionary> result = repository.findById(1L);
StepVerifier.create(result)
.expectNext(testDomain)
.verifyComplete();
}
@Test
void save_ShouldSaveDictionary() {
when(converter.toEntity(testDomain)).thenReturn(testEntity);
when(dao.save(testEntity)).thenReturn(Mono.just(testEntity));
when(converter.toDomain(testEntity)).thenReturn(testDomain);
Mono<Dictionary> result = repository.save(testDomain);
StepVerifier.create(result)
.expectNext(testDomain)
.verifyComplete();
}
@Test
void deleteById_ShouldDeleteDictionary() {
when(dao.deleteByIdAndDeletedAtIsNull(1L)).thenReturn(Mono.empty());
Mono<Void> result = repository.deleteById(1L);
StepVerifier.create(result)
.verifyComplete();
}
}
```
**Step 2: 运行测试**
```bash
mvn test -Dtest=DictionaryRepositoryTest
```
**Step 3: 提交变更**
```bash
git add infrastructure/db/repository/DictionaryRepositoryTest.java
git commit -m "test: add unit tests for DictionaryRepository"
```
---
### Task 16: 为其他 Repository 添加单元测试
**Files:**
- Create: `infrastructure/db/repository/SysUserRepositoryTest.java`
- Create: `infrastructure/db/repository/SysRoleRepositoryTest.java`
- Create: `infrastructure/db/repository/SysConfigRepositoryTest.java`
- Create: `infrastructure/db/repository/OperationLogRepositoryTest.java`
**Step 1: 创建测试类**
重复 Task 15 的模式,为其他 Repository 创建单元测试类。
**Step 2: 运行所有测试**
```bash
mvn test
```
**Step 3: 提交变更**
```bash
git add infrastructure/db/repository/
git commit -m "test: add unit tests for all repositories"
```
---
### Task 17: 为 Service 层添加单元测试
**Files:**
- Create: `core/service/impl/DictionaryServiceTest.java`
- Create: `core/service/impl/SysUserServiceTest.java`
- Create: `core/service/impl/SysRoleServiceTest.java`
**Step 1: 创建 DictionaryServiceTest**
```java
package cn.novalon.manage.sys.core.service.impl;
import cn.novalon.manage.sys.core.domain.Dictionary;
import cn.novalon.manage.sys.core.exception.DictionaryAlreadyExistsException;
import cn.novalon.manage.sys.infrastructure.db.converter.DictionaryConverter;
import cn.novalon.manage.sys.infrastructure.db.dao.DictionaryDao;
import cn.novalon.manage.sys.infrastructure.db.entity.DictionaryEntity;
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 java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class DictionaryServiceTest {
@Mock
private DictionaryDao dao;
@Mock
private DictionaryConverter converter;
private DictionaryService service;
private Dictionary testDictionary;
private DictionaryEntity testEntity;
@BeforeEach
void setUp() {
service = new DictionaryService(dao, converter);
testDictionary = new Dictionary();
testDictionary.setId(1L);
testDictionary.setType("test_type");
testDictionary.setCode("test_code");
testDictionary.setName("Test Dictionary");
testDictionary.setValue("test_value");
testDictionary.setSort(1);
testEntity = new DictionaryEntity();
testEntity.setId(1L);
testEntity.setType("test_type");
testEntity.setCode("test_code");
testEntity.setName("Test Dictionary");
testEntity.setValue("test_value");
testEntity.setSort(1);
}
@Test
void findAll_ShouldReturnAllDictionaries() {
when(dao.findByDeletedAtIsNullOrderBySortAsc()).thenReturn(Flux.just(testEntity));
when(converter.toDomain(testEntity)).thenReturn(testDictionary);
Flux<Dictionary> result = service.findAll();
StepVerifier.create(result)
.expectNext(testDictionary)
.verifyComplete();
}
@Test
void save_NewDictionary_ShouldSaveSuccessfully() {
when(dao.findByTypeAndCode("test_type", "test_code")).thenReturn(Mono.empty());
when(converter.toEntity(testDictionary)).thenReturn(testEntity);
when(dao.save(testEntity)).thenReturn(Mono.just(testEntity));
when(converter.toDomain(testEntity)).thenReturn(testDictionary);
Mono<Dictionary> result = service.save(testDictionary);
StepVerifier.create(result)
.expectNextMatches(dict -> dict.getCreatedAt() != null && dict.getUpdatedAt() != null)
.verifyComplete();
}
@Test
void save_DuplicateDictionary_ShouldThrowException() {
when(dao.findByTypeAndCode("test_type", "test_code")).thenReturn(Mono.just(testEntity));
Mono<Dictionary> result = service.save(testDictionary);
StepVerifier.create(result)
.expectError(DictionaryAlreadyExistsException.class)
.verify();
}
@Test
void update_ShouldUpdateDictionary() {
Dictionary updateDict = new Dictionary();
updateDict.setName("Updated Name");
when(dao.findById(1L)).thenReturn(Mono.just(testEntity));
when(dao.save(testEntity)).thenReturn(Mono.just(testEntity));
when(converter.toDomain(testEntity)).thenReturn(testDictionary);
Mono<Dictionary> result = service.update(1L, updateDict);
StepVerifier.create(result)
.expectNextMatches(dict -> "Updated Name".equals(dict.getName()))
.verifyComplete();
}
@Test
void deleteById_ShouldDeleteDictionary() {
when(dao.deleteByIdAndDeletedAtIsNull(1L)).thenReturn(Mono.empty());
Mono<Void> result = service.deleteById(1L);
StepVerifier.create(result)
.verifyComplete();
}
}
```
**Step 2: 创建其他 Service 测试类**
重复 Step 1 的模式,为其他 Service 创建单元测试类。
**Step 3: 运行所有测试**
```bash
mvn test
```
**Step 4: 提交变更**
```bash
git add core/service/impl/
git commit -m "test: add unit tests for all services"
```
---
### Task 18: 为 Handler 层添加单元测试
**Files:**
- Create: `handler/dictionary/DictionaryHandlerTest.java`
- Create: `handler/user/SysUserHandlerTest.java`
- Create: `handler/role/SysRoleHandlerTest.java`
**Step 1: 创建 DictionaryHandlerTest**
```java
package cn.novalon.manage.sys.handler.dictionary;
import cn.novalon.manage.sys.core.domain.Dictionary;
import cn.novalon.manage.sys.core.service.IDictionaryService;
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.ServerResponse;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.util.List;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class DictionaryHandlerTest {
@Mock
private IDictionaryService dictionaryService;
private DictionaryHandler handler;
@BeforeEach
void setUp() {
handler = new DictionaryHandler(dictionaryService);
}
@Test
void getAllDictionaries_ShouldReturnAllDictionaries() {
List<Dictionary> dictionaries = List.of(new Dictionary());
when(dictionaryService.findAll()).thenReturn(Flux.fromIterable(dictionaries));
Mono<ServerResponse> result = handler.getAllDictionaries(MockServerRequest.builder().build());
StepVerifier.create(result)
.expectNextMatches(response -> response.statusCode().is2xxSuccessful())
.verifyComplete();
}
@Test
void getDictionaryById_ShouldReturnDictionary() {
Dictionary dictionary = new Dictionary();
dictionary.setId(1L);
when(dictionaryService.findById(1L)).thenReturn(Mono.just(dictionary));
MockServerRequest request = MockServerRequest.builder()
.pathVariable("id", "1")
.build();
Mono<ServerResponse> result = handler.getDictionaryById(request);
StepVerifier.create(result)
.expectNextMatches(response -> response.statusCode().is2xxSuccessful())
.verifyComplete();
}
@Test
void getDictionaryById_NotFound_ShouldReturn404() {
when(dictionaryService.findById(1L)).thenReturn(Mono.empty());
MockServerRequest request = MockServerRequest.builder()
.pathVariable("id", "1")
.build();
Mono<ServerResponse> result = handler.getDictionaryById(request);
StepVerifier.create(result)
.expectNextMatches(response -> response.statusCode() == HttpStatus.NOT_FOUND)
.verifyComplete();
}
@Test
void createDictionary_ShouldCreateDictionary() {
Dictionary dictionary = new Dictionary();
dictionary.setType("test_type");
dictionary.setCode("test_code");
dictionary.setName("Test Dictionary");
when(dictionaryService.save(any(Dictionary.class))).thenReturn(Mono.just(dictionary));
MockServerRequest request = MockServerRequest.builder()
.body(dictionary)
.build();
Mono<ServerResponse> result = handler.createDictionary(request);
StepVerifier.create(result)
.expectNextMatches(response -> response.statusCode() == HttpStatus.CREATED)
.verifyComplete();
}
@Test
void updateDictionary_ShouldUpdateDictionary() {
Dictionary existing = new Dictionary();
existing.setId(1L);
existing.setName("Old Name");
Dictionary update = new Dictionary();
update.setName("New Name");
when(dictionaryService.findById(1L)).thenReturn(Mono.just(existing));
when(dictionaryService.update(eq(1L), any(Dictionary.class))).thenReturn(Mono.just(update));
MockServerRequest request = MockServerRequest.builder()
.pathVariable("id", "1")
.body(update)
.build();
Mono<ServerResponse> result = handler.updateDictionary(request);
StepVerifier.create(result)
.expectNextMatches(response -> response.statusCode().is2xxSuccessful())
.verifyComplete();
}
@Test
void deleteDictionary_ShouldDeleteDictionary() {
when(dictionaryService.deleteById(1L)).thenReturn(Mono.empty());
MockServerRequest request = MockServerRequest.builder()
.pathVariable("id", "1")
.build();
Mono<ServerResponse> result = handler.deleteDictionary(request);
StepVerifier.create(result)
.expectNextMatches(response -> response.statusCode() == HttpStatus.NO_CONTENT)
.verifyComplete();
}
}
```
**Step 2: 创建其他 Handler 测试类**
重复 Step 1 的模式,为其他 Handler 创建单元测试类。
**Step 3: 运行所有测试**
```bash
mvn test
```
**Step 4: 提交变更**
```bash
git add handler/
git commit -m "test: add unit tests for all handlers"
```
---
## Phase 6: 集成测试和文档
### Task 19: 添加集成测试
**Files:**
- Create: `integration/DictionaryIntegrationTest.java`
- Create: `integration/SysUserIntegrationTest.java`
**Step 1: 创建集成测试**
```java
package cn.novalon.manage.sys.integration;
import cn.novalon.manage.sys.core.domain.Dictionary;
import cn.novalon.manage.sys.infrastructure.db.repository.DictionaryRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.utility.DockerImageName;
import reactor.test.StepVerifier;
@SpringBootTest
@ActiveProfiles("test")
class DictionaryIntegrationTest {
@Container
private static final PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>(
new DockerImageName("postgres:15-alpine"))
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@Autowired
private DictionaryRepository dictionaryRepository;
private Dictionary testDictionary;
@BeforeEach
void setUp() {
testDictionary = new Dictionary();
testDictionary.setType("test_type");
testDictionary.setCode("test_code");
testDictionary.setName("Test Dictionary");
testDictionary.setValue("test_value");
testDictionary.setSort(1);
}
@Test
void saveAndFindDictionary_ShouldWorkEndToEnd() {
StepVerifier.create(dictionaryRepository.save(testDictionary))
.expectNextMatches(dict -> dict.getId() != null)
.verifyComplete();
StepVerifier.create(dictionaryRepository.findByType("test_type"))
.expectNextMatches(dict -> "test_code".equals(dict.getCode()))
.verifyComplete();
}
@Test
void updateDictionary_ShouldPersistChanges() {
StepVerifier.create(dictionaryRepository.save(testDictionary))
.expectNextMatches(dict -> dict.getId() != null)
.verifyComplete();
testDictionary.setName("Updated Name");
StepVerifier.create(dictionaryRepository.update(testDictionary.getId(), testDictionary))
.expectNextMatches(dict -> "Updated Name".equals(dict.getName()))
.verifyComplete();
}
@Test
void deleteDictionary_ShouldRemoveFromDatabase() {
StepVerifier.create(dictionaryRepository.save(testDictionary))
.expectNextMatches(dict -> dict.getId() != null)
.verifyComplete();
StepVerifier.create(dictionaryRepository.deleteById(testDictionary.getId()))
.verifyComplete();
StepVerifier.create(dictionaryRepository.findById(testDictionary.getId()))
.verifyComplete();
}
}
```
**Step 2: 运行集成测试**
```bash
mvn test -Dtest=*IntegrationTest
```
**Step 3: 提交变更**
```bash
git add integration/
git commit -m "test: add integration tests for key modules"
```
---
### Task 20: 更新文档
**Files:**
- Modify: `README.md`
- Create: `docs/ARCHITECTURE.md`
- Create: `docs/CONTRIBUTING.md`
**Step 1: 更新 README.md**
```markdown
# Novalon Manage System
## 项目概述
Novalon 管理系统是一个基于 Spring WebFlux 的现代化管理系统,采用响应式编程和函数式路由设计。
## 技术栈
- **后端框架**: Spring Boot 3.x + WebFlux
- **数据库**: PostgreSQL + R2DBC
- **对象映射**: MapStruct
- **测试框架**: JUnit 5 + Mockito + Testcontainers
- **构建工具**: Maven
## 架构设计
系统采用分层架构设计:
```
Handler (函数式路由) → Service (业务逻辑) → Repository (数据访问) → DAO (数据库操作) → R2DBC Repository
```
### 层次职责
- **Handler 层**: 处理 HTTP 请求和响应,使用函数式 WebFlux 风格
- **Service 层**: 实现业务逻辑和事务管理
- **Repository 层**: 处理数据转换和复杂查询
- **DAO 层**: 直接数据库操作,继承 R2dbcRepository
- **Converter/Mapper 层**: 使用 MapStruct 自动生成转换代码
## 快速开始
### 环境要求
- JDK 17+
- Maven 3.8+
- PostgreSQL 15+
### 运行项目
```bash
# 克隆项目
git clone <repository-url>
cd novalon-manage-system
# 构建项目
mvn clean install
# 运行应用
mvn spring-boot:run
```
## 测试
```bash
# 运行单元测试
mvn test
# 运行集成测试
mvn test -Dtest=*IntegrationTest
# 运行所有测试
mvn verify
```
## 项目结构
```
novalon-manage-api/manage-sys/
├── config/ # 配置类
│ ├── SystemRouter.java # 统一路由配置
│ ├── SecurityConfig.java # 安全配置
│ └── WebFluxConfig.java # WebFlux 配置
├── core/ # 核心业务逻辑
│ ├── domain/ # 领域模型
│ ├── service/ # 服务接口和实现
│ └── repository/ # 仓储接口
├── infrastructure/ # 基础设施
│ └── db/
│ ├── dao/ # 数据访问对象
│ ├── entity/ # 数据库实体
│ ├── mapper/ # MapStruct 映射器
│ └── repository/ # 仓储实现
└── handler/ # 处理器(函数式路由)
├── dictionary/
├── user/
├── role/
└── ...
```
## 开发规范
### 代码风格
- 使用函数式 WebFlux 风格的 Handler
- 使用 MapStruct 进行对象转换
- 遵循 DRY 原则
- 使用 TDD 开发模式
### 命名规范
- **Handler**: `{Entity}Handler`
- **Service**: `I{Entity}Service` 接口,`{Entity}Service` 实现
- **Repository**: `{Entity}Repository`
- **DAO**: `{Entity}Dao`
- **Mapper**: `{Entity}Mapper`
- **Entity**: `{Entity}Entity`
### 提交规范
```bash
feat: 新功能
fix: 修复问题
refactor: 重构代码
test: 添加测试
docs: 更新文档
```
## 许可证
[License Information]
```
**Step 2: 创建架构文档**
```markdown
# 系统架构文档
## 概述
Novalon 管理系统采用现代化的分层架构设计,结合响应式编程和函数式路由,提供高性能和可扩展性。
## 架构层次
### 1. Handler 层(表现层)
**职责**
- 处理 HTTP 请求和响应
- 参数验证和类型转换
- 路由定义和配置
**技术实现**
- 使用函数式 WebFlux 风格
- 通过 `SystemRouter` 集中配置路由
- 使用 `ServerRequest` 和 `ServerResponse`
**示例**
```java
@Component
public class DictionaryHandler {
public Mono<ServerResponse> getAllDictionaries(ServerRequest request) {
return ServerResponse.ok()
.body(dictionaryService.findAll(), Dictionary.class);
}
}
```
### 2. Service 层(业务逻辑层)
**职责**
- 实现业务逻辑
- 事务管理
- 业务规则验证
- 跨 Repository 编排
**技术实现**
- 接口定义:`I{Entity}Service`
- 实现类:`{Entity}Service`
- 使用 `@Service` 注解
**示例**
```java
@Service
public class DictionaryService implements IDictionaryService {
private final DictionaryDao dao;
private final DictionaryMapper mapper;
public Mono<Dictionary> save(Dictionary dictionary) {
return dao.save(mapper.toEntity(dictionary))
.map(mapper::toDomain);
}
}
```
### 3. Repository 层(数据访问层)
**职责**
- 数据转换(Entity ↔ Domain
- 复杂查询实现
- 分页查询支持
- 批量操作支持
**技术实现**
- 使用 `{Entity}Repository`
- 依赖 `{Entity}Dao``{Entity}Mapper`
- 使用 `@Repository` 注解
**示例**
```java
@Repository
public class DictionaryRepository {
private final DictionaryDao dao;
private final DictionaryMapper mapper;
public Flux<Dictionary> findByType(String type) {
return dao.findByType(type)
.map(mapper::toDomain);
}
}
```
### 4. DAO 层(数据库操作层)
**职责**
- 直接数据库操作
- 继承 R2dbcRepository
- 定义查询方法
**技术实现**
- 接口定义:`{Entity}Dao`
- 继承 `R2dbcRepository<{Entity}Entity, Long>`
- 使用 `@Repository` 注解
**示例**
```java
@Repository
public interface DictionaryDao extends R2dbcRepository<DictionaryEntity, Long> {
Flux<DictionaryEntity> findByType(String type);
Mono<DictionaryEntity> findByTypeAndCode(String type, String code);
}
```
### 5. Mapper 层(对象转换层)
**职责**
- Entity ↔ Domain 转换
- 批量转换支持
- 自动代码生成
**技术实现**
- 使用 MapStruct
- 接口定义:`{Entity}Mapper`
- 使用 `@Mapper(componentModel = "spring")` 注解
**示例**
```java
@Mapper(componentModel = "spring")
public interface DictionaryMapper {
Dictionary toDomain(DictionaryEntity entity);
DictionaryEntity toEntity(Dictionary domain);
List<Dictionary> toDomainList(List<DictionaryEntity> entities);
List<DictionaryEntity> toEntityList(List<Dictionary> domains);
}
```
## 数据流
### 读取流程
```
HTTP Request → Handler → Service → Repository → DAO → R2DBC → Database
```
### 写入流程
```
HTTP Request → Handler → Service → Repository → Mapper → DAO → R2DBC → Database
```
## 技术选型
### 为什么选择函数式 WebFlux
1. **性能优势**: 非阻塞 I/O,高并发处理能力
2. **代码简洁**: 函数式编程减少样板代码
3. **类型安全**: 编译时类型检查
4. **测试友好**: 易于单元测试和模拟
### 为什么选择 MapStruct
1. **性能**: 编译时生成代码,运行时零开销
2. **维护性**: 自动生成,减少手动维护
3. **类型安全**: 编译时验证映射正确性
4. **批量支持**: 内置批量转换方法
## 设计原则
### DRY (Don't Repeat Yourself)
- 提取公共逻辑到工具类
- 使用继承减少重复代码
- 使用 MapStruct 自动生成转换代码
### YAGNI (You Aren't Gonna Need It)
- 只实现当前需要的功能
- 避免过度设计
- 保持代码简洁
### TDD (Test-Driven Development)
- 先写测试,再写实现
- 保持高测试覆盖率
- 频繁提交小步改进
### SOLID 原则
- **单一职责**: 每个类只负责一个功能
- **开闭原则**: 对扩展开放,对修改关闭
- **里氏替换**: 子类可以替换父类
- **接口隔离**: 客户端不应该依赖不需要的接口
- **依赖倒置**: 依赖抽象而非具体实现
## 性能优化
### 1. 批量操作
- 使用批量转换方法
- 批量数据库操作
- 减少数据库往返
### 2. 缓存策略
- 使用 Caffeine 缓存热点数据
- 合理设置缓存过期时间
- 缓存穿透保护
### 3. 响应式编程
- 非阻塞 I/O 操作
- 背压处理
- 资源高效利用
## 安全考虑
### 1. 认证授权
- JWT Token 认证
- 基于角色的访问控制
- 密码加密存储
### 2. 数据验证
- 输入参数验证
- 业务规则验证
- SQL 注入防护
### 3. 审计日志
- 操作日志记录
- 登录日志记录
- 异常日志记录
```
**Step 3: 创建贡献指南**
```markdown
# 贡献指南
## 如何贡献
我们欢迎任何形式的贡献,包括但不限于:
- 报告 Bug
- 提出新功能
- 改进文档
- 提交代码
## 开发流程
### 1. Fork 项目
```bash
git fork <repository-url>
git clone <your-fork-url>
cd novalon-manage-system
```
### 2. 创建分支
```bash
git checkout -b feature/your-feature-name
```
### 3. 编写代码
遵循项目开发规范:
- 使用函数式 WebFlux 风格
- 使用 MapStruct 进行对象转换
- 编写单元测试
- 遵循命名规范
### 4. 运行测试
```bash
mvn clean test
```
确保所有测试通过。
### 5. 提交代码
```bash
git add .
git commit -m "feat: add your feature"
```
提交信息格式:
- `feat:` 新功能
- `fix:` 修复问题
- `refactor:` 重构代码
- `test:` 添加测试
- `docs:` 更新文档
### 6. 推送分支
```bash
git push origin feature/your-feature-name
```
### 7. 创建 Pull Request
在 GitHub 上创建 Pull Request,描述:
- 变更内容
- 相关 Issue
- 测试情况
- 截图(如适用)
## 代码规范
### 命名规范
- **类名**: PascalCase,如 `DictionaryHandler`
- **方法名**: camelCase,如 `getAllDictionaries`
- **常量名**: UPPER_SNAKE_CASE,如 `MAX_SIZE`
- **包名**: 小写,如 `cn.novalon.manage.sys`
### 注释规范
- 类注释:描述类的职责和用途
- 方法注释:描述方法的功能、参数、返回值
- 复杂逻辑:添加行内注释
### 测试规范
- 测试类名:`{ClassName}Test`
- 测试方法名:`{MethodName}_{ExpectedBehavior}`
- 使用 Given-When-Then 模式
## Pull Request 检查清单
- [ ] 代码通过所有测试
- [ ] 遵循代码规范
- [ ] 添加了必要的测试
- [ ] 更新了相关文档
- [ ] 提交信息格式正确
- [ ] 没有 TODO 或 FIXME 注释
## 问题报告
使用 GitHub Issues 报告问题,请包含:
- 问题描述
- 复现步骤
- 预期行为
- 实际行为
- 环境信息(OS、JDK 版本等)
- 相关日志或截图
## 功能建议
使用 GitHub Issues 提出新功能建议,请描述:
- 功能描述
- 使用场景
- 预期收益
- 可能的实现方案
```
**Step 4: 提交文档变更**
```bash
git add README.md docs/
git commit -m "docs: update project documentation and architecture guide"
```
---
## 总结
本实施计划涵盖了以下主要方面:
### Phase 1: Handler 函数式迁移(9 个任务)
- Task 1-9: 将所有 Handler 从注解式迁移到函数式 WebFlux 风格
### Phase 2: Router 配置完善(1 个任务)
- Task 10: 扩展 SystemRouter 配置,支持所有模块
### Phase 3: Converter 优化(1 个任务)
- Task 11: 为所有 Converter 添加批量转换方法
### Phase 4: MapStruct 引入(3 个任务)
- Task 12: 配置 MapStruct 依赖
- Task 13: 创建 MapStruct Mapper 接口
- Task 14: 使用 MapStruct 替换手动 Converter
### Phase 5: 单元测试(4 个任务)
- Task 15-18: 为 Repository、Service、Handler 层添加完整的单元测试
### Phase 6: 集成测试和文档(2 个任务)
- Task 19: 添加集成测试
- Task 20: 更新项目文档
**总计**: 20 个任务,覆盖所有未完成的工作和后续建议。
---
**Plan complete and saved to `docs/plans/2026-03-12-infrastructure-refactoring-phase2.md`. Two execution options:**
**1. Subagent-Driven (this session)** - I dispatch fresh subagent per task, review between tasks, fast iteration
**2. Parallel Session (separate)** - Open new session with executing-plans, batch execution with checkpoints
**Which approach?**