# 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?**