65 KiB
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: 备份当前实现
cp handler/user/SysUserHandler.java handler/user/SysUserHandler.java.bak
Step 2: 修改为函数式风格
@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: 测试路由配置
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: 提交变更
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: 修改为函数式风格
@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: 提交变更
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: 修改为函数式风格
@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: 提交变更
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: 修改为函数式风格
@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: 提交变更
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: 修改为函数式风格
@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: 提交变更
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: 修改为函数式风格
@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: 提交变更
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: 修改为函数式风格
@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: 提交变更
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: 修改为函数式风格
@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: 提交变更
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: 修改为函数式风格
@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: 提交变更
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: 添加所有模块的路由配置
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: 测试路由配置
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: 提交变更
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 添加批量转换
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: 提交变更
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 依赖
<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: 提交变更
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 接口
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: 提交变更
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
@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 类
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: 提交变更
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: 创建测试类
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: 运行测试
mvn test -Dtest=DictionaryRepositoryTest
Step 3: 提交变更
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: 运行所有测试
mvn test
Step 3: 提交变更
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
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: 运行所有测试
mvn test
Step 4: 提交变更
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
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: 运行所有测试
mvn test
Step 4: 提交变更
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: 创建集成测试
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: 运行集成测试
mvn test -Dtest=*IntegrationTest
Step 3: 提交变更
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
# 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
测试
# 运行单元测试
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
提交规范
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注解
示例:
@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注解
示例:
@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注解
示例:
@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")注解
示例:
@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?
- 性能优势: 非阻塞 I/O,高并发处理能力
- 代码简洁: 函数式编程减少样板代码
- 类型安全: 编译时类型检查
- 测试友好: 易于单元测试和模拟
为什么选择 MapStruct?
- 性能: 编译时生成代码,运行时零开销
- 维护性: 自动生成,减少手动维护
- 类型安全: 编译时验证映射正确性
- 批量支持: 内置批量转换方法
设计原则
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. 创建分支
git checkout -b feature/your-feature-name
3. 编写代码
遵循项目开发规范:
- 使用函数式 WebFlux 风格
- 使用 MapStruct 进行对象转换
- 编写单元测试
- 遵循命名规范
4. 运行测试
mvn clean test
确保所有测试通过。
5. 提交代码
git add .
git commit -m "feat: add your feature"
提交信息格式:
feat:新功能fix:修复问题refactor:重构代码test:添加测试docs:更新文档
6. 推送分支
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?