diff --git a/.gitignore b/.gitignore index 7e59678..12c9ef3 100644 --- a/.gitignore +++ b/.gitignore @@ -64,8 +64,9 @@ Thumbs.db .DS_Store # Application specific -db/ -logs/ +# Database files (local development databases) +/db/ +/logs/ *.pid *.seed *.pid.lock \ No newline at end of file diff --git a/docs/plans/2026-03-12-infrastructure-refactoring-phase2.md b/docs/plans/2026-03-12-infrastructure-refactoring-phase2.md new file mode 100644 index 0000000..be23c1e --- /dev/null +++ b/docs/plans/2026-03-12-infrastructure-refactoring-phase2.md @@ -0,0 +1,2199 @@ +# 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 getAllUsers(ServerRequest request) { + boolean includeDeleted = Boolean.valueOf(request.queryParam("includeDeleted").orElse("false")); + return ServerResponse.ok() + .body(userService.findAll(includeDeleted), SysUser.class); + } + + public Mono 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 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 getUserByUsername(ServerRequest request) { + String username = request.pathVariable("username"); + return userService.findByUsername(username) + .flatMap(user -> ServerResponse.ok().bodyValue(user)) + .switchIfEmpty(ServerResponse.notFound().build()); + } + + public Mono createUser(ServerRequest request) { + return request.bodyToMono(SysUser.class) + .flatMap(userService::createUser) + .flatMap(user -> ServerResponse.status(HttpStatus.CREATED).bodyValue(user)); + } + + public Mono 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 deleteUser(ServerRequest request) { + Long id = Long.valueOf(request.pathVariable("id")); + return userService.deleteUser(id) + .then(ServerResponse.noContent().build()); + } + + public Mono 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 logicalDeleteUser(ServerRequest request) { + Long id = Long.valueOf(request.pathVariable("id")); + return userService.logicalDeleteUser(id) + .then(ServerResponse.noContent().build()); + } + + public Mono logicalDeleteUsers(ServerRequest request) { + return request.bodyToMono(new ParameterizedTypeReference>() {}) + .flatMap(ids -> userService.logicalDeleteUsers(ids)) + .then(ServerResponse.noContent().build()); + } + + public Mono restoreUser(ServerRequest request) { + Long id = Long.valueOf(request.pathVariable("id")); + return userService.restoreUser(id) + .then(ServerResponse.noContent().build()); + } + + public Mono restoreUsers(ServerRequest request) { + return request.bodyToMono(new ParameterizedTypeReference>() {}) + .flatMap(ids -> userService.restoreUsers(ids)) + .then(ServerResponse.noContent().build()); + } + + public Mono checkUsernameExists(ServerRequest request) { + String username = request.queryParam("username").orElse(null); + return userService.existsByUsername(username) + .flatMap(exists -> ServerResponse.ok().bodyValue(exists)); + } + + public Mono 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 getAllRoles(ServerRequest request) { + return ServerResponse.ok() + .body(roleService.findAll(), SysRole.class); + } + + public Mono 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 createRole(ServerRequest request) { + return request.bodyToMono(SysRole.class) + .flatMap(roleService::save) + .flatMap(role -> ServerResponse.status(HttpStatus.CREATED).bodyValue(role)); + } + + public Mono 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 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 getAllConfigs(ServerRequest request) { + return ServerResponse.ok() + .body(configService.findAll(), SysConfig.class); + } + + public Mono getConfigByKey(ServerRequest request) { + String configKey = request.pathVariable("configKey"); + return configService.findByConfigKey(configKey) + .flatMap(config -> ServerResponse.ok().bodyValue(config)) + .switchIfEmpty(ServerResponse.notFound().build()); + } + + public Mono createConfig(ServerRequest request) { + return request.bodyToMono(SysConfig.class) + .flatMap(configService::save) + .flatMap(config -> ServerResponse.status(HttpStatus.CREATED).bodyValue(config)); + } + + public Mono 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 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 getAllNotices(ServerRequest request) { + return ServerResponse.ok() + .body(noticeService.findAll(), SysNotice.class); + } + + public Mono 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 createNotice(ServerRequest request) { + return request.bodyToMono(SysNotice.class) + .flatMap(noticeService::save) + .flatMap(notice -> ServerResponse.status(HttpStatus.CREATED).bodyValue(notice)); + } + + public Mono 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 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 getAllFiles(ServerRequest request) { + return ServerResponse.ok() + .body(fileService.findAll(), SysFile.class); + } + + public Mono 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 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 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 getOperationLogs(ServerRequest request) { + return ServerResponse.ok() + .body(operationLogService.findAll(), OperationLog.class); + } + + public Mono 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 getLoginLogs(ServerRequest request) { + return ServerResponse.ok() + .body(loginLogService.findAll(), SysLoginLog.class); + } + + public Mono 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 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 register(ServerRequest request) { + return request.bodyToMono(UserRegisterRequest.class) + .flatMap(userService::registerUser) + .flatMap(user -> ServerResponse.status(HttpStatus.CREATED).bodyValue(user)); + } + + public Mono 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 getAllMessages(ServerRequest request) { + return ServerResponse.ok() + .body(messageService.findAll(), SysUserMessage.class); + } + + public Mono 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 createMessage(ServerRequest request) { + return request.bodyToMono(SysUserMessage.class) + .flatMap(messageService::save) + .flatMap(message -> ServerResponse.status(HttpStatus.CREATED).bodyValue(message)); + } + + public Mono markAsRead(ServerRequest request) { + Long id = Long.valueOf(request.pathVariable("id")); + return messageService.markAsRead(id) + .then(ServerResponse.ok().build()); + } + + public Mono 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 getUserStats(ServerRequest request) { + return userService.count() + .flatMap(count -> { + Map stats = new HashMap<>(); + stats.put("totalUsers", count); + return ServerResponse.ok().bodyValue(stats); + }); + } + + public Mono getOperationStats(ServerRequest request) { + return operationLogService.count() + .flatMap(count -> { + Map stats = new HashMap<>(); + stats.put("totalOperations", count); + return ServerResponse.ok().bodyValue(stats); + }); + } + + public Mono getSystemStats(ServerRequest request) { + return Mono.zip( + userService.count(), + operationLogService.count() + ).map(tuple -> { + Map 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 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 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 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 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 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 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 fileRoutes(SysFileHandler handler) { + return route() + .GET(handler::getAllFiles) + .GET("/{id}", handler::getFileById) + .POST("/upload", handler::uploadFile) + .DELETE("/{id}", handler::deleteFile) + .build(); + } + + private RouterFunction 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 authRoutes(SysAuthHandler handler) { + return route() + .POST("/login", handler::login) + .POST("/register", handler::register) + .POST("/refresh", handler::refreshToken) + .build(); + } + + private RouterFunction 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 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 toDomainList(List entities) { + if (entities == null) { + return null; + } + return entities.stream() + .map(this::toDomain) + .collect(Collectors.toList()); +} + +public List toEntityList(List 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 + + 1.5.5.Final + 0.2.0 + + + + + org.mapstruct + mapstruct + ${org.mapstruct.version} + + + org.projectlombok + lombok-mapstruct-binding + ${lombok-mapstruct-binding.version} + provided + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + + + org.mapstruct + mapstruct-processor + ${org.mapstruct.version} + + + org.projectlombok + lombok + ${lombok.version} + + + org.projectlombok + lombok-mapstruct-binding + ${lombok-mapstruct-binding.version} + + + + + + +``` + +**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 toDomainList(List entities); + + List toEntityList(List 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 findByType(String type) { + return dao.findByType(type) + .map(mapper::toDomain); + } + + public Mono findByTypeAndCode(String type, String code) { + return dao.findByTypeAndCode(type, code) + .map(mapper::toDomain); + } + + public Flux findAll() { + return dao.findByDeletedAtIsNullOrderBySortAsc() + .map(mapper::toDomain); + } + + public Mono findById(Long id) { + return dao.findById(id) + .map(mapper::toDomain); + } + + public Mono save(Dictionary dictionary) { + return dao.save(mapper.toEntity(dictionary)) + .map(mapper::toDomain); + } + + public Mono 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 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 result = repository.findByTypeAndCode("test_type", "test_code"); + + StepVerifier.create(result) + .expectNext(testDomain) + .verifyComplete(); + } + + @Test + void findAll_ShouldReturnAllDictionaries() { + List entities = Arrays.asList(testEntity); + when(dao.findByDeletedAtIsNullOrderBySortAsc()).thenReturn(Flux.fromIterable(entities)); + when(converter.toDomain(testEntity)).thenReturn(testDomain); + + Flux 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 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 result = repository.save(testDomain); + + StepVerifier.create(result) + .expectNext(testDomain) + .verifyComplete(); + } + + @Test + void deleteById_ShouldDeleteDictionary() { + when(dao.deleteByIdAndDeletedAtIsNull(1L)).thenReturn(Mono.empty()); + + Mono 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 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 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 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 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 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 dictionaries = List.of(new Dictionary()); + when(dictionaryService.findAll()).thenReturn(Flux.fromIterable(dictionaries)); + + Mono 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 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 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 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 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 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 +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 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 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 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 { + Flux findByType(String type); + Mono 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 toDomainList(List entities); + List toEntityList(List 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 +git clone +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?** \ No newline at end of file diff --git a/novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/infrastructure/db/dao/OperationLogDao.java b/novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/infrastructure/db/dao/OperationLogDao.java index 832b97c..fdeceb7 100644 --- a/novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/infrastructure/db/dao/OperationLogDao.java +++ b/novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/infrastructure/db/dao/OperationLogDao.java @@ -4,6 +4,9 @@ import cn.novalon.manage.sys.infrastructure.db.entity.OperationLogEntity; import org.springframework.data.r2dbc.repository.R2dbcRepository; import org.springframework.stereotype.Repository; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.time.LocalDateTime; @Repository public interface OperationLogDao extends R2dbcRepository { @@ -11,4 +14,8 @@ public interface OperationLogDao extends R2dbcRepository findByUsernameAndDeletedAtIsNull(String username); Flux findByDeletedAtIsNull(); -} + + Mono countByDeletedAtIsNull(); + + Mono countByCreatedAtAfterAndDeletedAtIsNull(LocalDateTime dateTime); +} \ No newline at end of file diff --git a/novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/infrastructure/db/dao/SysFileDao.java b/novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/infrastructure/db/dao/SysFileDao.java index f90a98f..65b1838 100644 --- a/novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/infrastructure/db/dao/SysFileDao.java +++ b/novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/infrastructure/db/dao/SysFileDao.java @@ -25,4 +25,6 @@ public interface SysFileDao extends R2dbcRepository { Mono countByDeletedAtIsNull(); Mono deleteByIdAndDeletedAtIsNull(Long id); + + Flux findByFilePathContaining(String fileName); } diff --git a/novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/infrastructure/db/dao/SysRoleDao.java b/novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/infrastructure/db/dao/SysRoleDao.java index 973c68a..f2ce2bd 100644 --- a/novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/infrastructure/db/dao/SysRoleDao.java +++ b/novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/infrastructure/db/dao/SysRoleDao.java @@ -1,6 +1,7 @@ package cn.novalon.manage.sys.infrastructure.db.dao; import cn.novalon.manage.sys.infrastructure.db.entity.SysRoleEntity; +import org.springframework.data.domain.Sort; import org.springframework.data.r2dbc.repository.R2dbcRepository; import org.springframework.stereotype.Repository; import reactor.core.publisher.Flux; @@ -9,7 +10,21 @@ import reactor.core.publisher.Mono; @Repository public interface SysRoleDao extends R2dbcRepository { + Mono findByIdAndDeletedAtIsNull(Long id); + Mono findByRoleKeyAndDeletedAtIsNull(String roleKey); Flux findByDeletedAtIsNull(); + + Flux findByDeletedAtIsNull(Sort sort); + + Flux findByRoleNameLikeAndRoleKeyLikeAndDeletedAtIsNull(String roleName, String roleKey, Sort sort); + + Mono countByDeletedAtIsNull(); + + Mono countByRoleNameLikeAndRoleKeyLikeAndDeletedAtIsNull(String roleName, String roleKey); + + Mono findByRoleNameAndDeletedAtIsNull(String roleName); + + Mono existsByRoleNameAndDeletedAtIsNull(String roleName); } diff --git a/novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/infrastructure/db/dao/SysUserDao.java b/novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/infrastructure/db/dao/SysUserDao.java index 6467fcc..ca7d20d 100644 --- a/novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/infrastructure/db/dao/SysUserDao.java +++ b/novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/infrastructure/db/dao/SysUserDao.java @@ -1,6 +1,7 @@ package cn.novalon.manage.sys.infrastructure.db.dao; import cn.novalon.manage.sys.infrastructure.db.entity.SysUserEntity; +import org.springframework.data.domain.Sort; import org.springframework.data.r2dbc.repository.R2dbcRepository; import org.springframework.stereotype.Repository; import reactor.core.publisher.Flux; @@ -11,5 +12,15 @@ public interface SysUserDao extends R2dbcRepository { Mono findByUsernameAndDeletedAtIsNull(String username); + Mono findByEmailAndDeletedAtIsNull(String email); + + Flux findAll(); + + Flux findAll(Sort sort); + Flux findByDeletedAtIsNull(); -} + + Flux findByDeletedAtIsNull(Sort sort); + + Mono countByDeletedAtIsNull(); +} \ No newline at end of file diff --git a/novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/infrastructure/db/dao/SysUserMessageDao.java b/novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/infrastructure/db/dao/SysUserMessageDao.java index 4afe975..4694ef1 100644 --- a/novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/infrastructure/db/dao/SysUserMessageDao.java +++ b/novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/infrastructure/db/dao/SysUserMessageDao.java @@ -13,11 +13,5 @@ public interface SysUserMessageDao extends R2dbcRepository findByUserIdOrderByCreateTimeDesc(Long userId); - Flux findByDeletedAtIsNull(); - Mono countByUserIdAndIsRead(Long userId, String isRead); - - Mono countByDeletedAtIsNull(); - - Mono deleteByIdAndDeletedAtIsNull(Long id); }