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

65 KiB
Raw Blame History

Infrastructure Refactoring Phase 2 Implementation Plan

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: 完成剩余 Handler 的函数式风格迁移、完善 Router 配置、优化其他 Converter、引入 MapStruct 和添加单元测试。

Architecture: 保持已建立的 DAO 层设计模式,将所有 Handler 从注解式迁移到函数式 WebFlux 风格,使用 MapStruct 自动生成转换代码,建立完整的单元测试覆盖。

Tech Stack: Spring WebFlux, R2DBC, MapStruct, JUnit 5, Mockito, Maven


Phase 1: 完成其他 Handler 的函数式风格迁移

Task 1: SysUserHandler 函数式迁移

Files:

  • Modify: handler/user/SysUserHandler.java

Step 1: 备份当前实现

cp handler/user/SysUserHandler.java handler/user/SysUserHandler.java.bak

Step 2: 修改为函数式风格

@Component
public class SysUserHandler {
    private final ISysUserService userService;

    public Mono<ServerResponse> getAllUsers(ServerRequest request) {
        boolean includeDeleted = Boolean.valueOf(request.queryParam("includeDeleted").orElse("false"));
        return ServerResponse.ok()
                .body(userService.findAll(includeDeleted), SysUser.class);
    }

    public Mono<ServerResponse> getUsersByPage(ServerRequest request) {
        int page = Integer.parseInt(request.queryParam("page").orElse("0"));
        int size = Integer.parseInt(request.queryParam("size").orElse("10"));
        String sort = request.queryParam("sort").orElse("id");
        String order = request.queryParam("order").orElse("asc");
        String keyword = request.queryParam("keyword").orElse(null);

        PageRequest pageRequest = new PageRequest();
        pageRequest.setPage(page);
        pageRequest.setSize(size);
        pageRequest.setSort(sort);
        pageRequest.setOrder(order);
        pageRequest.setKeyword(keyword);

        return userService.findUsersByPage(pageRequest)
                .flatMap(response -> ServerResponse.ok().bodyValue(response));
    }

    public Mono<ServerResponse> getUserById(ServerRequest request) {
        Long id = Long.valueOf(request.pathVariable("id"));
        return userService.findById(id)
                .flatMap(user -> ServerResponse.ok().bodyValue(user))
                .switchIfEmpty(ServerResponse.notFound().build());
    }

    public Mono<ServerResponse> getUserByUsername(ServerRequest request) {
        String username = request.pathVariable("username");
        return userService.findByUsername(username)
                .flatMap(user -> ServerResponse.ok().bodyValue(user))
                .switchIfEmpty(ServerResponse.notFound().build());
    }

    public Mono<ServerResponse> createUser(ServerRequest request) {
        return request.bodyToMono(SysUser.class)
                .flatMap(userService::createUser)
                .flatMap(user -> ServerResponse.status(HttpStatus.CREATED).bodyValue(user));
    }

    public Mono<ServerResponse> updateUser(ServerRequest request) {
        Long id = Long.valueOf(request.pathVariable("id"));
        return request.bodyToMono(UserUpdateRequest.class)
                .flatMap(req -> userService.findById(id)
                        .flatMap(existing -> {
                            if (req.getEmail() != null) existing.setEmail(req.getEmail());
                            if (req.getStatus() != null) existing.setStatus(req.getStatus());
                            if (req.getRoleId() != null) existing.setRoleId(req.getRoleId());
                            return userService.updateUser(existing);
                        }))
                .flatMap(user -> ServerResponse.ok().bodyValue(user))
                .switchIfEmpty(ServerResponse.notFound().build());
    }

    public Mono<ServerResponse> deleteUser(ServerRequest request) {
        Long id = Long.valueOf(request.pathVariable("id"));
        return userService.deleteUser(id)
                .then(ServerResponse.noContent().build());
    }

    public Mono<ServerResponse> changePassword(ServerRequest request) {
        Long id = Long.valueOf(request.pathVariable("id"));
        return request.bodyToMono(PasswordChangeRequest.class)
                .flatMap(req -> userService.changePassword(id, req.getOldPassword(), req.getNewPassword()))
                .flatMap(user -> ServerResponse.ok().bodyValue(user));
    }

    public Mono<ServerResponse> logicalDeleteUser(ServerRequest request) {
        Long id = Long.valueOf(request.pathVariable("id"));
        return userService.logicalDeleteUser(id)
                .then(ServerResponse.noContent().build());
    }

    public Mono<ServerResponse> logicalDeleteUsers(ServerRequest request) {
        return request.bodyToMono(new ParameterizedTypeReference<List<Long>>() {})
                .flatMap(ids -> userService.logicalDeleteUsers(ids))
                .then(ServerResponse.noContent().build());
    }

    public Mono<ServerResponse> restoreUser(ServerRequest request) {
        Long id = Long.valueOf(request.pathVariable("id"));
        return userService.restoreUser(id)
                .then(ServerResponse.noContent().build());
    }

    public Mono<ServerResponse> restoreUsers(ServerRequest request) {
        return request.bodyToMono(new ParameterizedTypeReference<List<Long>>() {})
                .flatMap(ids -> userService.restoreUsers(ids))
                .then(ServerResponse.noContent().build());
    }

    public Mono<ServerResponse> checkUsernameExists(ServerRequest request) {
        String username = request.queryParam("username").orElse(null);
        return userService.existsByUsername(username)
                .flatMap(exists -> ServerResponse.ok().bodyValue(exists));
    }

    public Mono<ServerResponse> checkEmailExists(ServerRequest request) {
        String email = request.queryParam("email").orElse(null);
        return userService.existsByEmail(email)
                .flatMap(exists -> ServerResponse.ok().bodyValue(exists));
    }
}

Step 3: 测试路由配置

curl -X GET http://localhost:8080/api/users
curl -X GET http://localhost:8080/api/users/1
curl -X POST http://localhost:8080/api/users -H "Content-Type: application/json" -d '{"username":"test","password":"123456"}'

Step 4: 提交变更

git add handler/user/SysUserHandler.java
git commit -m "refactor: migrate SysUserHandler to functional WebFlux style"

Task 2: SysRoleHandler 函数式迁移

Files:

  • Modify: handler/role/SysRoleHandler.java

Step 1: 修改为函数式风格

@Component
public class SysRoleHandler {
    private final ISysRoleService roleService;

    public Mono<ServerResponse> getAllRoles(ServerRequest request) {
        return ServerResponse.ok()
                .body(roleService.findAll(), SysRole.class);
    }

    public Mono<ServerResponse> getRoleById(ServerRequest request) {
        Long id = Long.valueOf(request.pathVariable("id"));
        return roleService.findById(id)
                .flatMap(role -> ServerResponse.ok().bodyValue(role))
                .switchIfEmpty(ServerResponse.notFound().build());
    }

    public Mono<ServerResponse> createRole(ServerRequest request) {
        return request.bodyToMono(SysRole.class)
                .flatMap(roleService::save)
                .flatMap(role -> ServerResponse.status(HttpStatus.CREATED).bodyValue(role));
    }

    public Mono<ServerResponse> updateRole(ServerRequest request) {
        Long id = Long.valueOf(request.pathVariable("id"));
        return request.bodyToMono(SysRole.class)
                .flatMap(role -> roleService.update(id, role))
                .flatMap(updatedRole -> ServerResponse.ok().bodyValue(updatedRole))
                .switchIfEmpty(ServerResponse.notFound().build());
    }

    public Mono<ServerResponse> deleteRole(ServerRequest request) {
        Long id = Long.valueOf(request.pathVariable("id"));
        return roleService.deleteById(id)
                .then(ServerResponse.noContent().build());
    }
}

Step 2: 提交变更

git add handler/role/SysRoleHandler.java
git commit -m "refactor: migrate SysRoleHandler to functional WebFlux style"

Task 3: SysConfigHandler 函数式迁移

Files:

  • Modify: handler/config/SysConfigHandler.java

Step 1: 修改为函数式风格

@Component
public class SysConfigHandler {
    private final ISysConfigService configService;

    public Mono<ServerResponse> getAllConfigs(ServerRequest request) {
        return ServerResponse.ok()
                .body(configService.findAll(), SysConfig.class);
    }

    public Mono<ServerResponse> getConfigByKey(ServerRequest request) {
        String configKey = request.pathVariable("configKey");
        return configService.findByConfigKey(configKey)
                .flatMap(config -> ServerResponse.ok().bodyValue(config))
                .switchIfEmpty(ServerResponse.notFound().build());
    }

    public Mono<ServerResponse> createConfig(ServerRequest request) {
        return request.bodyToMono(SysConfig.class)
                .flatMap(configService::save)
                .flatMap(config -> ServerResponse.status(HttpStatus.CREATED).bodyValue(config));
    }

    public Mono<ServerResponse> updateConfig(ServerRequest request) {
        Long id = Long.valueOf(request.pathVariable("id"));
        return request.bodyToMono(SysConfig.class)
                .flatMap(config -> configService.update(id, config))
                .flatMap(updatedConfig -> ServerResponse.ok().bodyValue(updatedConfig))
                .switchIfEmpty(ServerResponse.notFound().build());
    }

    public Mono<ServerResponse> deleteConfig(ServerRequest request) {
        Long id = Long.valueOf(request.pathVariable("id"));
        return configService.deleteById(id)
                .then(ServerResponse.noContent().build());
    }
}

Step 2: 提交变更

git add handler/config/SysConfigHandler.java
git commit -m "refactor: migrate SysConfigHandler to functional WebFlux style"

Task 4: SysNoticeHandler 函数式迁移

Files:

  • Modify: handler/notice/SysNoticeHandler.java

Step 1: 修改为函数式风格

@Component
public class SysNoticeHandler {
    private final ISysNoticeService noticeService;

    public Mono<ServerResponse> getAllNotices(ServerRequest request) {
        return ServerResponse.ok()
                .body(noticeService.findAll(), SysNotice.class);
    }

    public Mono<ServerResponse> getNoticeById(ServerRequest request) {
        Long id = Long.valueOf(request.pathVariable("id"));
        return noticeService.findById(id)
                .flatMap(notice -> ServerResponse.ok().bodyValue(notice))
                .switchIfEmpty(ServerResponse.notFound().build());
    }

    public Mono<ServerResponse> createNotice(ServerRequest request) {
        return request.bodyToMono(SysNotice.class)
                .flatMap(noticeService::save)
                .flatMap(notice -> ServerResponse.status(HttpStatus.CREATED).bodyValue(notice));
    }

    public Mono<ServerResponse> updateNotice(ServerRequest request) {
        Long id = Long.valueOf(request.pathVariable("id"));
        return request.bodyToMono(SysNotice.class)
                .flatMap(notice -> noticeService.update(id, notice))
                .flatMap(updatedNotice -> ServerResponse.ok().bodyValue(updatedNotice))
                .switchIfEmpty(ServerResponse.notFound().build());
    }

    public Mono<ServerResponse> deleteNotice(ServerRequest request) {
        Long id = Long.valueOf(request.pathVariable("id"));
        return noticeService.deleteById(id)
                .then(ServerResponse.noContent().build());
    }
}

Step 2: 提交变更

git add handler/notice/SysNoticeHandler.java
git commit -m "refactor: migrate SysNoticeHandler to functional WebFlux style"

Task 5: SysFileHandler 函数式迁移

Files:

  • Modify: handler/file/SysFileHandler.java

Step 1: 修改为函数式风格

@Component
public class SysFileHandler {
    private final ISysFileService fileService;

    public Mono<ServerResponse> getAllFiles(ServerRequest request) {
        return ServerResponse.ok()
                .body(fileService.findAll(), SysFile.class);
    }

    public Mono<ServerResponse> getFileById(ServerRequest request) {
        Long id = Long.valueOf(request.pathVariable("id"));
        return fileService.findById(id)
                .flatMap(file -> ServerResponse.ok().bodyValue(file))
                .switchIfEmpty(ServerResponse.notFound().build());
    }

    public Mono<ServerResponse> uploadFile(ServerRequest request) {
        return request.multipartData()
                .flatMap(data -> {
                    String filename = data.toSingleValueMap().getFirst("file").filename();
                    return fileService.saveFile(data)
                            .flatMap(file -> ServerResponse.status(HttpStatus.CREATED).bodyValue(file));
                });
    }

    public Mono<ServerResponse> deleteFile(ServerRequest request) {
        Long id = Long.valueOf(request.pathVariable("id"));
        return fileService.deleteById(id)
                .then(ServerResponse.noContent().build());
    }
}

Step 2: 提交变更

git add handler/file/SysFileHandler.java
git commit -m "refactor: migrate SysFileHandler to functional WebFlux style"

Task 6: SysLogHandler 函数式迁移

Files:

  • Modify: handler/log/SysLogHandler.java

Step 1: 修改为函数式风格

@Component
public class SysLogHandler {
    private final IOperationLogService operationLogService;
    private final ISysLoginLogService loginLogService;

    public Mono<ServerResponse> getOperationLogs(ServerRequest request) {
        return ServerResponse.ok()
                .body(operationLogService.findAll(), OperationLog.class);
    }

    public Mono<ServerResponse> getOperationLogsByPage(ServerRequest request) {
        int page = Integer.parseInt(request.queryParam("page").orElse("0"));
        int size = Integer.parseInt(request.queryParam("size").orElse("10"));
        String sort = request.queryParam("sort").orElse("createdAt");
        String order = request.queryParam("order").orElse("desc");

        PageRequest pageRequest = new PageRequest();
        pageRequest.setPage(page);
        pageRequest.setSize(size);
        pageRequest.setSort(sort);
        pageRequest.setOrder(order);

        return operationLogService.findLogsByPage(pageRequest)
                .flatMap(response -> ServerResponse.ok().bodyValue(response));
    }

    public Mono<ServerResponse> getLoginLogs(ServerRequest request) {
        return ServerResponse.ok()
                .body(loginLogService.findAll(), SysLoginLog.class);
    }

    public Mono<ServerResponse> getLoginLogsByPage(ServerRequest request) {
        int page = Integer.parseInt(request.queryParam("page").orElse("0"));
        int size = Integer.parseInt(request.queryParam("size").orElse("10"));
        String sort = request.queryParam("sort").orElse("createdAt");
        String order = request.queryParam("order").orElse("desc");

        PageRequest pageRequest = new PageRequest();
        pageRequest.setPage(page);
        pageRequest.setSize(size);
        pageRequest.setSort(sort);
        pageRequest.setOrder(order);

        return loginLogService.findLogsByPage(pageRequest)
                .flatMap(response -> ServerResponse.ok().bodyValue(response));
    }
}

Step 2: 提交变更

git add handler/log/SysLogHandler.java
git commit -m "refactor: migrate SysLogHandler to functional WebFlux style"

Task 7: SysAuthHandler 函数式迁移

Files:

  • Modify: handler/auth/SysAuthHandler.java

Step 1: 修改为函数式风格

@Component
public class SysAuthHandler {
    private final ISysUserService userService;
    private final JwtTokenProvider tokenProvider;

    public Mono<ServerResponse> login(ServerRequest request) {
        return request.bodyToMono(LoginRequest.class)
                .flatMap(loginRequest -> userService.authenticate(loginRequest.getUsername(), loginRequest.getPassword()))
                .flatMap(user -> {
                    String token = tokenProvider.generateToken(user);
                    AuthResponse response = new AuthResponse(token, user);
                    return ServerResponse.ok().bodyValue(response);
                });
    }

    public Mono<ServerResponse> register(ServerRequest request) {
        return request.bodyToMono(UserRegisterRequest.class)
                .flatMap(userService::registerUser)
                .flatMap(user -> ServerResponse.status(HttpStatus.CREATED).bodyValue(user));
    }

    public Mono<ServerResponse> refreshToken(ServerRequest request) {
        return request.bodyToMono(RefreshTokenRequest.class)
                .flatMap(refreshRequest -> tokenProvider.validateToken(refreshRequest.getToken())
                        .flatMap(username -> userService.findByUsername(username))
                        .flatMap(user -> {
                            String newToken = tokenProvider.generateToken(user);
                            AuthResponse response = new AuthResponse(newToken, user);
                            return ServerResponse.ok().bodyValue(response);
                        }));
    }
}

Step 2: 提交变更

git add handler/auth/SysAuthHandler.java
git commit -m "refactor: migrate SysAuthHandler to functional WebFlux style"

Task 8: SysUserMessageHandler 函数式迁移

Files:

  • Modify: handler/message/SysUserMessageHandler.java

Step 1: 修改为函数式风格

@Component
public class SysUserMessageHandler {
    private final ISysUserMessageService messageService;

    public Mono<ServerResponse> getAllMessages(ServerRequest request) {
        return ServerResponse.ok()
                .body(messageService.findAll(), SysUserMessage.class);
    }

    public Mono<ServerResponse> getMessageById(ServerRequest request) {
        Long id = Long.valueOf(request.pathVariable("id"));
        return messageService.findById(id)
                .flatMap(message -> ServerResponse.ok().bodyValue(message))
                .switchIfEmpty(ServerResponse.notFound().build());
    }

    public Mono<ServerResponse> createMessage(ServerRequest request) {
        return request.bodyToMono(SysUserMessage.class)
                .flatMap(messageService::save)
                .flatMap(message -> ServerResponse.status(HttpStatus.CREATED).bodyValue(message));
    }

    public Mono<ServerResponse> markAsRead(ServerRequest request) {
        Long id = Long.valueOf(request.pathVariable("id"));
        return messageService.markAsRead(id)
                .then(ServerResponse.ok().build());
    }

    public Mono<ServerResponse> deleteMessage(ServerRequest request) {
        Long id = Long.valueOf(request.pathVariable("id"));
        return messageService.deleteById(id)
                .then(ServerResponse.noContent().build());
    }
}

Step 2: 提交变更

git add handler/message/SysUserMessageHandler.java
git commit -m "refactor: migrate SysUserMessageHandler to functional WebFlux style"

Task 9: StatsHandler 函数式迁移

Files:

  • Modify: handler/stats/StatsHandler.java

Step 1: 修改为函数式风格

@Component
public class StatsHandler {
    private final ISysUserService userService;
    private final IOperationLogService operationLogService;

    public Mono<ServerResponse> getUserStats(ServerRequest request) {
        return userService.count()
                .flatMap(count -> {
                    Map<String, Object> stats = new HashMap<>();
                    stats.put("totalUsers", count);
                    return ServerResponse.ok().bodyValue(stats);
                });
    }

    public Mono<ServerResponse> getOperationStats(ServerRequest request) {
        return operationLogService.count()
                .flatMap(count -> {
                    Map<String, Object> stats = new HashMap<>();
                    stats.put("totalOperations", count);
                    return ServerResponse.ok().bodyValue(stats);
                });
    }

    public Mono<ServerResponse> getSystemStats(ServerRequest request) {
        return Mono.zip(
                userService.count(),
                operationLogService.count()
        ).map(tuple -> {
            Map<String, Object> stats = new HashMap<>();
            stats.put("totalUsers", tuple.getT1());
            stats.put("totalOperations", tuple.getT2());
            return stats;
        }).flatMap(stats -> ServerResponse.ok().bodyValue(stats));
    }
}

Step 2: 提交变更

git add handler/stats/StatsHandler.java
git commit -m "refactor: migrate StatsHandler to functional WebFlux style"

Phase 2: 完善 Router 配置

Task 10: 扩展 SystemRouter 配置

Files:

  • Modify: config/SystemRouter.java

Step 1: 添加所有模块的路由配置

package cn.novalon.manage.sys.config;

import cn.novalon.manage.sys.handler.auth.SysAuthHandler;
import cn.novalon.manage.sys.handler.config.SysConfigHandler;
import cn.novalon.manage.sys.handler.dictionary.DictionaryHandler;
import cn.novalon.manage.sys.handler.file.SysFileHandler;
import cn.novalon.manage.sys.handler.log.SysLogHandler;
import cn.novalon.manage.sys.handler.message.SysUserMessageHandler;
import cn.novalon.manage.sys.handler.notice.SysNoticeHandler;
import cn.novalon.manage.sys.handler.role.SysRoleHandler;
import cn.novalon.manage.sys.handler.stats.StatsHandler;
import cn.novalon.manage.sys.handler.user.SysUserHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;

import static org.springframework.web.reactive.function.server.RequestPredicates.*;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;

@Configuration
public class SystemRouter {

    @Bean
    public RouterFunction<ServerResponse> systemRoutes(
            DictionaryHandler dictionaryHandler,
            SysUserHandler userHandler,
            SysRoleHandler roleHandler,
            SysConfigHandler configHandler,
            SysNoticeHandler noticeHandler,
            SysFileHandler fileHandler,
            SysLogHandler logHandler,
            SysAuthHandler authHandler,
            SysUserMessageHandler messageHandler,
            StatsHandler statsHandler) {

        return route()
                .path("/api", builder -> builder
                        .path("/dictionaries", dictRoutes(dictionaryHandler))
                        .path("/users", userRoutes(userHandler))
                        .path("/roles", roleRoutes(roleHandler))
                        .path("/configs", configRoutes(configHandler))
                        .path("/notices", noticeRoutes(noticeHandler))
                        .path("/files", fileRoutes(fileHandler))
                        .path("/logs", logRoutes(logHandler))
                        .path("/auth", authRoutes(authHandler))
                        .path("/messages", messageRoutes(messageHandler))
                        .path("/stats", statsRoutes(statsHandler))
                        .build())
                .build();
    }

    private RouterFunction<ServerResponse> dictRoutes(DictionaryHandler handler) {
        return route()
                .GET(handler::getAllDictionaries)
                .GET("/{id}", handler::getDictionaryById)
                .GET("/type/{type}", handler::getDictionariesByType)
                .GET("/check/exists", handler::checkTypeAndCodeExists)
                .POST(handler::createDictionary)
                .PUT("/{id}", handler::updateDictionary)
                .DELETE("/{id}", handler::deleteDictionary)
                .build();
    }

    private RouterFunction<ServerResponse> userRoutes(SysUserHandler handler) {
        return route()
                .GET(handler::getAllUsers)
                .GET("/page", handler::getUsersByPage)
                .GET("/{id}", handler::getUserById)
                .GET("/username/{username}", handler::getUserByUsername)
                .POST(handler::createUser)
                .PUT("/{id}", handler::updateUser)
                .DELETE("/{id}", handler::deleteUser)
                .POST("/{id}/password", handler::changePassword)
                .DELETE("/{id}/logical", handler::logicalDeleteUser)
                .POST("/logical-delete", handler::logicalDeleteUsers)
                .POST("/{id}/restore", handler::restoreUser)
                .POST("/restore", handler::restoreUsers)
                .GET("/check/username", handler::checkUsernameExists)
                .GET("/check/email", handler::checkEmailExists)
                .build();
    }

    private RouterFunction<ServerResponse> roleRoutes(SysRoleHandler handler) {
        return route()
                .GET(handler::getAllRoles)
                .GET("/{id}", handler::getRoleById)
                .POST(handler::createRole)
                .PUT("/{id}", handler::updateRole)
                .DELETE("/{id}", handler::deleteRole)
                .build();
    }

    private RouterFunction<ServerResponse> configRoutes(SysConfigHandler handler) {
        return route()
                .GET(handler::getAllConfigs)
                .GET("/key/{configKey}", handler::getConfigByKey)
                .POST(handler::createConfig)
                .PUT("/{id}", handler::updateConfig)
                .DELETE("/{id}", handler::deleteConfig)
                .build();
    }

    private RouterFunction<ServerResponse> noticeRoutes(SysNoticeHandler handler) {
        return route()
                .GET(handler::getAllNotices)
                .GET("/{id}", handler::getNoticeById)
                .POST(handler::createNotice)
                .PUT("/{id}", handler::updateNotice)
                .DELETE("/{id}", handler::deleteNotice)
                .build();
    }

    private RouterFunction<ServerResponse> fileRoutes(SysFileHandler handler) {
        return route()
                .GET(handler::getAllFiles)
                .GET("/{id}", handler::getFileById)
                .POST("/upload", handler::uploadFile)
                .DELETE("/{id}", handler::deleteFile)
                .build();
    }

    private RouterFunction<ServerResponse> logRoutes(SysLogHandler handler) {
        return route()
                .GET("/operations", handler::getOperationLogs)
                .GET("/operations/page", handler::getOperationLogsByPage)
                .GET("/logins", handler::getLoginLogs)
                .GET("/logins/page", handler::getLoginLogsByPage)
                .build();
    }

    private RouterFunction<ServerResponse> authRoutes(SysAuthHandler handler) {
        return route()
                .POST("/login", handler::login)
                .POST("/register", handler::register)
                .POST("/refresh", handler::refreshToken)
                .build();
    }

    private RouterFunction<ServerResponse> messageRoutes(SysUserMessageHandler handler) {
        return route()
                .GET(handler::getAllMessages)
                .GET("/{id}", handler::getMessageById)
                .POST(handler::createMessage)
                .PUT("/{id}/read", handler::markAsRead)
                .DELETE("/{id}", handler::deleteMessage)
                .build();
    }

    private RouterFunction<ServerResponse> statsRoutes(StatsHandler handler) {
        return route()
                .GET("/users", handler::getUserStats)
                .GET("/operations", handler::getOperationStats)
                .GET("/system", handler::getSystemStats)
                .build();
    }
}

Step 2: 测试路由配置

curl -X GET http://localhost:8080/api/dictionaries
curl -X GET http://localhost:8080/api/users
curl -X GET http://localhost:8080/api/roles
curl -X GET http://localhost:8080/api/stats/system

Step 3: 提交变更

git add config/SystemRouter.java
git commit -m "feat: add comprehensive router configuration for all modules"

Phase 3: 优化其他 Converter

Task 11: 为其他 Converter 添加批量转换方法

Files:

  • Modify: converter/SysRoleConverter.java
  • Modify: converter/SysConfigConverter.java
  • Modify: converter/SysNoticeConverter.java
  • Modify: converter/SysFileConverter.java
  • Modify: converter/SysLoginLogConverter.java
  • Modify: converter/SysDictDataConverter.java
  • Modify: converter/SysDictTypeConverter.java
  • Modify: converter/SysMenuConverter.java
  • Modify: converter/SysUserMessageConverter.java
  • Modify: converter/SysExceptionLogConverter.java

Step 1: 为 SysRoleConverter 添加批量转换

public List<SysRole> toDomainList(List<SysRoleEntity> entities) {
    if (entities == null) {
        return null;
    }
    return entities.stream()
            .map(this::toDomain)
            .collect(Collectors.toList());
}

public List<SysRoleEntity> toEntityList(List<SysRole> domains) {
    if (domains == null) {
        return null;
    }
    return domains.stream()
            .map(this::toEntity)
            .collect(Collectors.toList());
}

Step 2: 为其他 Converter 添加批量转换方法

重复 Step 1 的模式,为所有其他 Converter 添加 toDomainList()toEntityList() 方法。

Step 3: 提交变更

git add converter/
git commit -m "feat: add batch conversion methods to all converters"

Phase 4: 引入 MapStruct

Task 12: 配置 MapStruct 依赖

Files:

  • Modify: pom.xml

Step 1: 添加 MapStruct 依赖

<properties>
    <org.mapstruct.version>1.5.5.Final</org.mapstruct.version>
    <lombok-mapstruct-binding.version>0.2.0</lombok-mapstruct-binding.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId>
        <version>${org.mapstruct.version}</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok-mapstruct-binding</artifactId>
        <version>${lombok-mapstruct-binding.version}</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.11.0</version>
            <configuration>
                <annotationProcessorPaths>
                    <path>
                        <groupId>org.mapstruct</groupId>
                        <artifactId>mapstruct-processor</artifactId>
                        <version>${org.mapstruct.version}</version>
                    </path>
                    <path>
                        <groupId>org.projectlombok</groupId>
                        <artifactId>lombok</artifactId>
                        <version>${lombok.version}</version>
                    </path>
                    <path>
                        <groupId>org.projectlombok</groupId>
                        <artifactId>lombok-mapstruct-binding</artifactId>
                        <version>${lombok-mapstruct-binding.version}</version>
                    </path>
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>

Step 2: 提交变更

git add pom.xml
git commit -m "feat: add MapStruct dependency and configuration"

Task 13: 创建 MapStruct Mapper 接口

Files:

  • Create: infrastructure/db/mapper/DictionaryMapper.java
  • Create: infrastructure/db/mapper/SysUserMapper.java
  • Create: infrastructure/db/mapper/SysRoleMapper.java
  • Create: infrastructure/db/mapper/SysConfigMapper.java
  • Create: infrastructure/db/mapper/SysNoticeMapper.java
  • Create: infrastructure/db/mapper/SysFileMapper.java
  • Create: infrastructure/db/mapper/OperationLogMapper.java
  • Create: infrastructure/db/mapper/SysLoginLogMapper.java
  • Create: infrastructure/db/mapper/SysDictDataMapper.java
  • Create: infrastructure/db/mapper/SysDictTypeMapper.java
  • Create: infrastructure/db/mapper/SysMenuMapper.java
  • Create: infrastructure/db/mapper/SysUserMessageMapper.java
  • Create: infrastructure/db/mapper/SysExceptionLogMapper.java

Step 1: 创建 DictionaryMapper 接口

package cn.novalon.manage.sys.infrastructure.db.mapper;

import cn.novalon.manage.sys.core.domain.Dictionary;
import cn.novalon.manage.sys.infrastructure.db.entity.DictionaryEntity;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Named;

import java.util.List;

@Mapper(componentModel = "spring")
public interface DictionaryMapper {

    Dictionary toDomain(DictionaryEntity entity);

    DictionaryEntity toEntity(Dictionary domain);

    List<Dictionary> toDomainList(List<DictionaryEntity> entities);

    List<DictionaryEntity> toEntityList(List<Dictionary> domains);
}

Step 2: 创建其他 Mapper 接口

重复 Step 1 的模式,为所有实体创建对应的 Mapper 接口。

Step 3: 提交变更

git add infrastructure/db/mapper/
git commit -m "feat: add MapStruct mapper interfaces for all entities"

Task 14: 使用 MapStruct 替换手动 Converter

Files:

  • Modify: repository/DictionaryRepository.java
  • Modify: repository/SysUserRepository.java
  • Modify: repository/SysRoleRepository.java
  • Modify: repository/SysConfigRepository.java
  • Modify: repository/SysNoticeRepository.java
  • Modify: repository/SysFileRepository.java
  • Modify: repository/OperationLogRepository.java
  • Modify: repository/SysLoginLogRepository.java
  • Modify: repository/SysDictDataRepository.java
  • Modify: repository/SysDictTypeRepository.java
  • Modify: repository/SysMenuRepository.java
  • Modify: repository/SysUserMessageRepository.java
  • Modify: repository/SysExceptionLogRepository.java

Step 1: 修改 DictionaryRepository 使用 MapStruct

@Repository
public class DictionaryRepository {

    private final DictionaryDao dao;
    private final DictionaryMapper mapper;

    public DictionaryRepository(DictionaryDao dao, DictionaryMapper mapper) {
        this.dao = dao;
        this.mapper = mapper;
    }

    public Flux<Dictionary> findByType(String type) {
        return dao.findByType(type)
                .map(mapper::toDomain);
    }

    public Mono<Dictionary> findByTypeAndCode(String type, String code) {
        return dao.findByTypeAndCode(type, code)
                .map(mapper::toDomain);
    }

    public Flux<Dictionary> findAll() {
        return dao.findByDeletedAtIsNullOrderBySortAsc()
                .map(mapper::toDomain);
    }

    public Mono<Dictionary> findById(Long id) {
        return dao.findById(id)
                .map(mapper::toDomain);
    }

    public Mono<Dictionary> save(Dictionary dictionary) {
        return dao.save(mapper.toEntity(dictionary))
                .map(mapper::toDomain);
    }

    public Mono<Void> deleteById(Long id) {
        return dao.deleteByIdAndDeletedAtIsNull(id);
    }
}

Step 2: 修改其他 Repository 使用 MapStruct

重复 Step 1 的模式,为所有 Repository 替换 Converter 为 Mapper。

Step 3: 删除旧的 Converter 类

rm converter/DictionaryConverter.java
rm converter/SysUserConverter.java
rm converter/SysRoleConverter.java
rm converter/SysConfigConverter.java
rm converter/SysNoticeConverter.java
rm converter/SysFileConverter.java
rm converter/OperationLogConverter.java
rm converter/SysLoginLogConverter.java
rm converter/SysDictDataConverter.java
rm converter/SysDictTypeConverter.java
rm converter/SysMenuConverter.java
rm converter/SysUserMessageConverter.java
rm converter/SysExceptionLogConverter.java

Step 4: 提交变更

git add repository/ converter/
git commit -m "refactor: replace manual converters with MapStruct mappers"

Phase 5: 添加单元测试

Task 15: 为 DictionaryRepository 添加单元测试

Files:

  • Create: infrastructure/db/repository/DictionaryRepositoryTest.java

Step 1: 创建测试类

package cn.novalon.manage.sys.infrastructure.db.repository;

import cn.novalon.manage.sys.core.domain.Dictionary;
import cn.novalon.manage.sys.infrastructure.db.converter.DictionaryConverter;
import cn.novalon.manage.sys.infrastructure.db.dao.DictionaryDao;
import cn.novalon.manage.sys.infrastructure.db.entity.DictionaryEntity;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;

import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class DictionaryRepositoryTest {

    @Mock
    private DictionaryDao dao;

    @Mock
    private DictionaryConverter converter;

    private DictionaryRepository repository;

    private DictionaryEntity testEntity;
    private Dictionary testDomain;

    @BeforeEach
    void setUp() {
        repository = new DictionaryRepository(dao, converter);

        testEntity = new DictionaryEntity();
        testEntity.setId(1L);
        testEntity.setType("test_type");
        testEntity.setCode("test_code");
        testEntity.setName("Test Dictionary");
        testEntity.setValue("test_value");
        testEntity.setSort(1);
        testEntity.setCreatedAt(LocalDateTime.now());
        testEntity.setUpdatedAt(LocalDateTime.now());

        testDomain = new Dictionary();
        testDomain.setId(1L);
        testDomain.setType("test_type");
        testDomain.setCode("test_code");
        testDomain.setName("Test Dictionary");
        testDomain.setValue("test_value");
        testDomain.setSort(1);
        testDomain.setCreatedAt(LocalDateTime.now());
        testDomain.setUpdatedAt(LocalDateTime.now());
    }

    @Test
    void findByType_ShouldReturnDictionaries() {
        when(dao.findByType("test_type")).thenReturn(Flux.just(testEntity));
        when(converter.toDomain(testEntity)).thenReturn(testDomain);

        Flux<Dictionary> result = repository.findByType("test_type");

        StepVerifier.create(result)
                .expectNext(testDomain)
                .verifyComplete();
    }

    @Test
    void findByTypeAndCode_ShouldReturnDictionary() {
        when(dao.findByTypeAndCode("test_type", "test_code")).thenReturn(Mono.just(testEntity));
        when(converter.toDomain(testEntity)).thenReturn(testDomain);

        Mono<Dictionary> result = repository.findByTypeAndCode("test_type", "test_code");

        StepVerifier.create(result)
                .expectNext(testDomain)
                .verifyComplete();
    }

    @Test
    void findAll_ShouldReturnAllDictionaries() {
        List<DictionaryEntity> entities = Arrays.asList(testEntity);
        when(dao.findByDeletedAtIsNullOrderBySortAsc()).thenReturn(Flux.fromIterable(entities));
        when(converter.toDomain(testEntity)).thenReturn(testDomain);

        Flux<Dictionary> result = repository.findAll();

        StepVerifier.create(result)
                .expectNext(testDomain)
                .verifyComplete();
    }

    @Test
    void findById_ShouldReturnDictionary() {
        when(dao.findById(1L)).thenReturn(Mono.just(testEntity));
        when(converter.toDomain(testEntity)).thenReturn(testDomain);

        Mono<Dictionary> result = repository.findById(1L);

        StepVerifier.create(result)
                .expectNext(testDomain)
                .verifyComplete();
    }

    @Test
    void save_ShouldSaveDictionary() {
        when(converter.toEntity(testDomain)).thenReturn(testEntity);
        when(dao.save(testEntity)).thenReturn(Mono.just(testEntity));
        when(converter.toDomain(testEntity)).thenReturn(testDomain);

        Mono<Dictionary> result = repository.save(testDomain);

        StepVerifier.create(result)
                .expectNext(testDomain)
                .verifyComplete();
    }

    @Test
    void deleteById_ShouldDeleteDictionary() {
        when(dao.deleteByIdAndDeletedAtIsNull(1L)).thenReturn(Mono.empty());

        Mono<Void> result = repository.deleteById(1L);

        StepVerifier.create(result)
                .verifyComplete();
    }
}

Step 2: 运行测试

mvn test -Dtest=DictionaryRepositoryTest

Step 3: 提交变更

git add infrastructure/db/repository/DictionaryRepositoryTest.java
git commit -m "test: add unit tests for DictionaryRepository"

Task 16: 为其他 Repository 添加单元测试

Files:

  • Create: infrastructure/db/repository/SysUserRepositoryTest.java
  • Create: infrastructure/db/repository/SysRoleRepositoryTest.java
  • Create: infrastructure/db/repository/SysConfigRepositoryTest.java
  • Create: infrastructure/db/repository/OperationLogRepositoryTest.java

Step 1: 创建测试类

重复 Task 15 的模式,为其他 Repository 创建单元测试类。

Step 2: 运行所有测试

mvn test

Step 3: 提交变更

git add infrastructure/db/repository/
git commit -m "test: add unit tests for all repositories"

Task 17: 为 Service 层添加单元测试

Files:

  • Create: core/service/impl/DictionaryServiceTest.java
  • Create: core/service/impl/SysUserServiceTest.java
  • Create: core/service/impl/SysRoleServiceTest.java

Step 1: 创建 DictionaryServiceTest

package cn.novalon.manage.sys.core.service.impl;

import cn.novalon.manage.sys.core.domain.Dictionary;
import cn.novalon.manage.sys.core.exception.DictionaryAlreadyExistsException;
import cn.novalon.manage.sys.infrastructure.db.converter.DictionaryConverter;
import cn.novalon.manage.sys.infrastructure.db.dao.DictionaryDao;
import cn.novalon.manage.sys.infrastructure.db.entity.DictionaryEntity;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;

import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class DictionaryServiceTest {

    @Mock
    private DictionaryDao dao;

    @Mock
    private DictionaryConverter converter;

    private DictionaryService service;

    private Dictionary testDictionary;
    private DictionaryEntity testEntity;

    @BeforeEach
    void setUp() {
        service = new DictionaryService(dao, converter);

        testDictionary = new Dictionary();
        testDictionary.setId(1L);
        testDictionary.setType("test_type");
        testDictionary.setCode("test_code");
        testDictionary.setName("Test Dictionary");
        testDictionary.setValue("test_value");
        testDictionary.setSort(1);

        testEntity = new DictionaryEntity();
        testEntity.setId(1L);
        testEntity.setType("test_type");
        testEntity.setCode("test_code");
        testEntity.setName("Test Dictionary");
        testEntity.setValue("test_value");
        testEntity.setSort(1);
    }

    @Test
    void findAll_ShouldReturnAllDictionaries() {
        when(dao.findByDeletedAtIsNullOrderBySortAsc()).thenReturn(Flux.just(testEntity));
        when(converter.toDomain(testEntity)).thenReturn(testDictionary);

        Flux<Dictionary> result = service.findAll();

        StepVerifier.create(result)
                .expectNext(testDictionary)
                .verifyComplete();
    }

    @Test
    void save_NewDictionary_ShouldSaveSuccessfully() {
        when(dao.findByTypeAndCode("test_type", "test_code")).thenReturn(Mono.empty());
        when(converter.toEntity(testDictionary)).thenReturn(testEntity);
        when(dao.save(testEntity)).thenReturn(Mono.just(testEntity));
        when(converter.toDomain(testEntity)).thenReturn(testDictionary);

        Mono<Dictionary> result = service.save(testDictionary);

        StepVerifier.create(result)
                .expectNextMatches(dict -> dict.getCreatedAt() != null && dict.getUpdatedAt() != null)
                .verifyComplete();
    }

    @Test
    void save_DuplicateDictionary_ShouldThrowException() {
        when(dao.findByTypeAndCode("test_type", "test_code")).thenReturn(Mono.just(testEntity));

        Mono<Dictionary> result = service.save(testDictionary);

        StepVerifier.create(result)
                .expectError(DictionaryAlreadyExistsException.class)
                .verify();
    }

    @Test
    void update_ShouldUpdateDictionary() {
        Dictionary updateDict = new Dictionary();
        updateDict.setName("Updated Name");

        when(dao.findById(1L)).thenReturn(Mono.just(testEntity));
        when(dao.save(testEntity)).thenReturn(Mono.just(testEntity));
        when(converter.toDomain(testEntity)).thenReturn(testDictionary);

        Mono<Dictionary> result = service.update(1L, updateDict);

        StepVerifier.create(result)
                .expectNextMatches(dict -> "Updated Name".equals(dict.getName()))
                .verifyComplete();
    }

    @Test
    void deleteById_ShouldDeleteDictionary() {
        when(dao.deleteByIdAndDeletedAtIsNull(1L)).thenReturn(Mono.empty());

        Mono<Void> result = service.deleteById(1L);

        StepVerifier.create(result)
                .verifyComplete();
    }
}

Step 2: 创建其他 Service 测试类

重复 Step 1 的模式,为其他 Service 创建单元测试类。

Step 3: 运行所有测试

mvn test

Step 4: 提交变更

git add core/service/impl/
git commit -m "test: add unit tests for all services"

Task 18: 为 Handler 层添加单元测试

Files:

  • Create: handler/dictionary/DictionaryHandlerTest.java
  • Create: handler/user/SysUserHandlerTest.java
  • Create: handler/role/SysRoleHandlerTest.java

Step 1: 创建 DictionaryHandlerTest

package cn.novalon.manage.sys.handler.dictionary;

import cn.novalon.manage.sys.core.domain.Dictionary;
import cn.novalon.manage.sys.core.service.IDictionaryService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpStatus;
import org.springframework.mock.web.reactive.function.server.MockServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;

import java.util.List;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class DictionaryHandlerTest {

    @Mock
    private IDictionaryService dictionaryService;

    private DictionaryHandler handler;

    @BeforeEach
    void setUp() {
        handler = new DictionaryHandler(dictionaryService);
    }

    @Test
    void getAllDictionaries_ShouldReturnAllDictionaries() {
        List<Dictionary> dictionaries = List.of(new Dictionary());
        when(dictionaryService.findAll()).thenReturn(Flux.fromIterable(dictionaries));

        Mono<ServerResponse> result = handler.getAllDictionaries(MockServerRequest.builder().build());

        StepVerifier.create(result)
                .expectNextMatches(response -> response.statusCode().is2xxSuccessful())
                .verifyComplete();
    }

    @Test
    void getDictionaryById_ShouldReturnDictionary() {
        Dictionary dictionary = new Dictionary();
        dictionary.setId(1L);
        when(dictionaryService.findById(1L)).thenReturn(Mono.just(dictionary));

        MockServerRequest request = MockServerRequest.builder()
                .pathVariable("id", "1")
                .build();

        Mono<ServerResponse> result = handler.getDictionaryById(request);

        StepVerifier.create(result)
                .expectNextMatches(response -> response.statusCode().is2xxSuccessful())
                .verifyComplete();
    }

    @Test
    void getDictionaryById_NotFound_ShouldReturn404() {
        when(dictionaryService.findById(1L)).thenReturn(Mono.empty());

        MockServerRequest request = MockServerRequest.builder()
                .pathVariable("id", "1")
                .build();

        Mono<ServerResponse> result = handler.getDictionaryById(request);

        StepVerifier.create(result)
                .expectNextMatches(response -> response.statusCode() == HttpStatus.NOT_FOUND)
                .verifyComplete();
    }

    @Test
    void createDictionary_ShouldCreateDictionary() {
        Dictionary dictionary = new Dictionary();
        dictionary.setType("test_type");
        dictionary.setCode("test_code");
        dictionary.setName("Test Dictionary");

        when(dictionaryService.save(any(Dictionary.class))).thenReturn(Mono.just(dictionary));

        MockServerRequest request = MockServerRequest.builder()
                .body(dictionary)
                .build();

        Mono<ServerResponse> result = handler.createDictionary(request);

        StepVerifier.create(result)
                .expectNextMatches(response -> response.statusCode() == HttpStatus.CREATED)
                .verifyComplete();
    }

    @Test
    void updateDictionary_ShouldUpdateDictionary() {
        Dictionary existing = new Dictionary();
        existing.setId(1L);
        existing.setName("Old Name");

        Dictionary update = new Dictionary();
        update.setName("New Name");

        when(dictionaryService.findById(1L)).thenReturn(Mono.just(existing));
        when(dictionaryService.update(eq(1L), any(Dictionary.class))).thenReturn(Mono.just(update));

        MockServerRequest request = MockServerRequest.builder()
                .pathVariable("id", "1")
                .body(update)
                .build();

        Mono<ServerResponse> result = handler.updateDictionary(request);

        StepVerifier.create(result)
                .expectNextMatches(response -> response.statusCode().is2xxSuccessful())
                .verifyComplete();
    }

    @Test
    void deleteDictionary_ShouldDeleteDictionary() {
        when(dictionaryService.deleteById(1L)).thenReturn(Mono.empty());

        MockServerRequest request = MockServerRequest.builder()
                .pathVariable("id", "1")
                .build();

        Mono<ServerResponse> result = handler.deleteDictionary(request);

        StepVerifier.create(result)
                .expectNextMatches(response -> response.statusCode() == HttpStatus.NO_CONTENT)
                .verifyComplete();
    }
}

Step 2: 创建其他 Handler 测试类

重复 Step 1 的模式,为其他 Handler 创建单元测试类。

Step 3: 运行所有测试

mvn test

Step 4: 提交变更

git add handler/
git commit -m "test: add unit tests for all handlers"

Phase 6: 集成测试和文档

Task 19: 添加集成测试

Files:

  • Create: integration/DictionaryIntegrationTest.java
  • Create: integration/SysUserIntegrationTest.java

Step 1: 创建集成测试

package cn.novalon.manage.sys.integration;

import cn.novalon.manage.sys.core.domain.Dictionary;
import cn.novalon.manage.sys.infrastructure.db.repository.DictionaryRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.utility.DockerImageName;
import reactor.test.StepVerifier;

@SpringBootTest
@ActiveProfiles("test")
class DictionaryIntegrationTest {

    @Container
    private static final PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>(
            new DockerImageName("postgres:15-alpine"))
            .withDatabaseName("testdb")
            .withUsername("test")
            .withPassword("test");

    @Autowired
    private DictionaryRepository dictionaryRepository;

    private Dictionary testDictionary;

    @BeforeEach
    void setUp() {
        testDictionary = new Dictionary();
        testDictionary.setType("test_type");
        testDictionary.setCode("test_code");
        testDictionary.setName("Test Dictionary");
        testDictionary.setValue("test_value");
        testDictionary.setSort(1);
    }

    @Test
    void saveAndFindDictionary_ShouldWorkEndToEnd() {
        StepVerifier.create(dictionaryRepository.save(testDictionary))
                .expectNextMatches(dict -> dict.getId() != null)
                .verifyComplete();

        StepVerifier.create(dictionaryRepository.findByType("test_type"))
                .expectNextMatches(dict -> "test_code".equals(dict.getCode()))
                .verifyComplete();
    }

    @Test
    void updateDictionary_ShouldPersistChanges() {
        StepVerifier.create(dictionaryRepository.save(testDictionary))
                .expectNextMatches(dict -> dict.getId() != null)
                .verifyComplete();

        testDictionary.setName("Updated Name");

        StepVerifier.create(dictionaryRepository.update(testDictionary.getId(), testDictionary))
                .expectNextMatches(dict -> "Updated Name".equals(dict.getName()))
                .verifyComplete();
    }

    @Test
    void deleteDictionary_ShouldRemoveFromDatabase() {
        StepVerifier.create(dictionaryRepository.save(testDictionary))
                .expectNextMatches(dict -> dict.getId() != null)
                .verifyComplete();

        StepVerifier.create(dictionaryRepository.deleteById(testDictionary.getId()))
                .verifyComplete();

        StepVerifier.create(dictionaryRepository.findById(testDictionary.getId()))
                .verifyComplete();
    }
}

Step 2: 运行集成测试

mvn test -Dtest=*IntegrationTest

Step 3: 提交变更

git add integration/
git commit -m "test: add integration tests for key modules"

Task 20: 更新文档

Files:

  • Modify: README.md
  • Create: docs/ARCHITECTURE.md
  • Create: docs/CONTRIBUTING.md

Step 1: 更新 README.md

# Novalon Manage System

## 项目概述

Novalon 管理系统是一个基于 Spring WebFlux 的现代化管理系统,采用响应式编程和函数式路由设计。

## 技术栈

- **后端框架**: Spring Boot 3.x + WebFlux
- **数据库**: PostgreSQL + R2DBC
- **对象映射**: MapStruct
- **测试框架**: JUnit 5 + Mockito + Testcontainers
- **构建工具**: Maven

## 架构设计

系统采用分层架构设计:

Handler (函数式路由) → Service (业务逻辑) → Repository (数据访问) → DAO (数据库操作) → R2DBC Repository


### 层次职责

- **Handler 层**: 处理 HTTP 请求和响应,使用函数式 WebFlux 风格
- **Service 层**: 实现业务逻辑和事务管理
- **Repository 层**: 处理数据转换和复杂查询
- **DAO 层**: 直接数据库操作,继承 R2dbcRepository
- **Converter/Mapper 层**: 使用 MapStruct 自动生成转换代码

## 快速开始

### 环境要求

- JDK 17+
- Maven 3.8+
- PostgreSQL 15+

### 运行项目

```bash
# 克隆项目
git clone <repository-url>
cd novalon-manage-system

# 构建项目
mvn clean install

# 运行应用
mvn spring-boot:run

测试

# 运行单元测试
mvn test

# 运行集成测试
mvn test -Dtest=*IntegrationTest

# 运行所有测试
mvn verify

项目结构

novalon-manage-api/manage-sys/
├── config/                    # 配置类
│   ├── SystemRouter.java       # 统一路由配置
│   ├── SecurityConfig.java     # 安全配置
│   └── WebFluxConfig.java    # WebFlux 配置
├── core/                      # 核心业务逻辑
│   ├── domain/               # 领域模型
│   ├── service/              # 服务接口和实现
│   └── repository/           # 仓储接口
├── infrastructure/            # 基础设施
│   └── db/
│       ├── dao/             # 数据访问对象
│       ├── entity/          # 数据库实体
│       ├── mapper/          # MapStruct 映射器
│       └── repository/      # 仓储实现
└── handler/                   # 处理器(函数式路由)
    ├── dictionary/
    ├── user/
    ├── role/
    └── ...

开发规范

代码风格

  • 使用函数式 WebFlux 风格的 Handler
  • 使用 MapStruct 进行对象转换
  • 遵循 DRY 原则
  • 使用 TDD 开发模式

命名规范

  • Handler: {Entity}Handler
  • Service: I{Entity}Service 接口,{Entity}Service 实现
  • Repository: {Entity}Repository
  • DAO: {Entity}Dao
  • Mapper: {Entity}Mapper
  • Entity: {Entity}Entity

提交规范

feat: 新功能
fix: 修复问题
refactor: 重构代码
test: 添加测试
docs: 更新文档

许可证

[License Information]


**Step 2: 创建架构文档**

```markdown
# 系统架构文档

## 概述

Novalon 管理系统采用现代化的分层架构设计,结合响应式编程和函数式路由,提供高性能和可扩展性。

## 架构层次

### 1. Handler 层(表现层)

**职责**
- 处理 HTTP 请求和响应
- 参数验证和类型转换
- 路由定义和配置

**技术实现**:
- 使用函数式 WebFlux 风格
- 通过 `SystemRouter` 集中配置路由
- 使用 `ServerRequest` 和 `ServerResponse`

**示例**
```java
@Component
public class DictionaryHandler {
    public Mono<ServerResponse> getAllDictionaries(ServerRequest request) {
        return ServerResponse.ok()
                .body(dictionaryService.findAll(), Dictionary.class);
    }
}

2. Service 层(业务逻辑层)

职责

  • 实现业务逻辑
  • 事务管理
  • 业务规则验证
  • 跨 Repository 编排

技术实现

  • 接口定义:I{Entity}Service
  • 实现类:{Entity}Service
  • 使用 @Service 注解

示例

@Service
public class DictionaryService implements IDictionaryService {
    private final DictionaryDao dao;
    private final DictionaryMapper mapper;

    public Mono<Dictionary> save(Dictionary dictionary) {
        return dao.save(mapper.toEntity(dictionary))
                .map(mapper::toDomain);
    }
}

3. Repository 层(数据访问层)

职责

  • 数据转换(Entity ↔ Domain
  • 复杂查询实现
  • 分页查询支持
  • 批量操作支持

技术实现

  • 使用 {Entity}Repository
  • 依赖 {Entity}Dao{Entity}Mapper
  • 使用 @Repository 注解

示例

@Repository
public class DictionaryRepository {
    private final DictionaryDao dao;
    private final DictionaryMapper mapper;

    public Flux<Dictionary> findByType(String type) {
        return dao.findByType(type)
                .map(mapper::toDomain);
    }
}

4. DAO 层(数据库操作层)

职责

  • 直接数据库操作
  • 继承 R2dbcRepository
  • 定义查询方法

技术实现

  • 接口定义:{Entity}Dao
  • 继承 R2dbcRepository<{Entity}Entity, Long>
  • 使用 @Repository 注解

示例

@Repository
public interface DictionaryDao extends R2dbcRepository<DictionaryEntity, Long> {
    Flux<DictionaryEntity> findByType(String type);
    Mono<DictionaryEntity> findByTypeAndCode(String type, String code);
}

5. Mapper 层(对象转换层)

职责

  • Entity ↔ Domain 转换
  • 批量转换支持
  • 自动代码生成

技术实现

  • 使用 MapStruct
  • 接口定义:{Entity}Mapper
  • 使用 @Mapper(componentModel = "spring") 注解

示例

@Mapper(componentModel = "spring")
public interface DictionaryMapper {
    Dictionary toDomain(DictionaryEntity entity);
    DictionaryEntity toEntity(Dictionary domain);
    List<Dictionary> toDomainList(List<DictionaryEntity> entities);
    List<DictionaryEntity> toEntityList(List<Dictionary> domains);
}

数据流

读取流程

HTTP Request → Handler → Service → Repository → DAO → R2DBC → Database

写入流程

HTTP Request → Handler → Service → Repository → Mapper → DAO → R2DBC → Database

技术选型

为什么选择函数式 WebFlux

  1. 性能优势: 非阻塞 I/O,高并发处理能力
  2. 代码简洁: 函数式编程减少样板代码
  3. 类型安全: 编译时类型检查
  4. 测试友好: 易于单元测试和模拟

为什么选择 MapStruct

  1. 性能: 编译时生成代码,运行时零开销
  2. 维护性: 自动生成,减少手动维护
  3. 类型安全: 编译时验证映射正确性
  4. 批量支持: 内置批量转换方法

设计原则

DRY (Don't Repeat Yourself)

  • 提取公共逻辑到工具类
  • 使用继承减少重复代码
  • 使用 MapStruct 自动生成转换代码

YAGNI (You Aren't Gonna Need It)

  • 只实现当前需要的功能
  • 避免过度设计
  • 保持代码简洁

TDD (Test-Driven Development)

  • 先写测试,再写实现
  • 保持高测试覆盖率
  • 频繁提交小步改进

SOLID 原则

  • 单一职责: 每个类只负责一个功能
  • 开闭原则: 对扩展开放,对修改关闭
  • 里氏替换: 子类可以替换父类
  • 接口隔离: 客户端不应该依赖不需要的接口
  • 依赖倒置: 依赖抽象而非具体实现

性能优化

1. 批量操作

  • 使用批量转换方法
  • 批量数据库操作
  • 减少数据库往返

2. 缓存策略

  • 使用 Caffeine 缓存热点数据
  • 合理设置缓存过期时间
  • 缓存穿透保护

3. 响应式编程

  • 非阻塞 I/O 操作
  • 背压处理
  • 资源高效利用

安全考虑

1. 认证授权

  • JWT Token 认证
  • 基于角色的访问控制
  • 密码加密存储

2. 数据验证

  • 输入参数验证
  • 业务规则验证
  • SQL 注入防护

3. 审计日志

  • 操作日志记录
  • 登录日志记录
  • 异常日志记录

**Step 3: 创建贡献指南**

```markdown
# 贡献指南

## 如何贡献

我们欢迎任何形式的贡献,包括但不限于:

- 报告 Bug
- 提出新功能
- 改进文档
- 提交代码

## 开发流程

### 1. Fork 项目

```bash
git fork <repository-url>
git clone <your-fork-url>
cd novalon-manage-system

2. 创建分支

git checkout -b feature/your-feature-name

3. 编写代码

遵循项目开发规范:

  • 使用函数式 WebFlux 风格
  • 使用 MapStruct 进行对象转换
  • 编写单元测试
  • 遵循命名规范

4. 运行测试

mvn clean test

确保所有测试通过。

5. 提交代码

git add .
git commit -m "feat: add your feature"

提交信息格式:

  • feat: 新功能
  • fix: 修复问题
  • refactor: 重构代码
  • test: 添加测试
  • docs: 更新文档

6. 推送分支

git push origin feature/your-feature-name

7. 创建 Pull Request

在 GitHub 上创建 Pull Request,描述:

  • 变更内容
  • 相关 Issue
  • 测试情况
  • 截图(如适用)

代码规范

命名规范

  • 类名: PascalCase,如 DictionaryHandler
  • 方法名: camelCase,如 getAllDictionaries
  • 常量名: UPPER_SNAKE_CASE,如 MAX_SIZE
  • 包名: 小写,如 cn.novalon.manage.sys

注释规范

  • 类注释:描述类的职责和用途
  • 方法注释:描述方法的功能、参数、返回值
  • 复杂逻辑:添加行内注释

测试规范

  • 测试类名:{ClassName}Test
  • 测试方法名:{MethodName}_{ExpectedBehavior}
  • 使用 Given-When-Then 模式

Pull Request 检查清单

  • 代码通过所有测试
  • 遵循代码规范
  • 添加了必要的测试
  • 更新了相关文档
  • 提交信息格式正确
  • 没有 TODO 或 FIXME 注释

问题报告

使用 GitHub Issues 报告问题,请包含:

  • 问题描述
  • 复现步骤
  • 预期行为
  • 实际行为
  • 环境信息(OS、JDK 版本等)
  • 相关日志或截图

功能建议

使用 GitHub Issues 提出新功能建议,请描述:

  • 功能描述
  • 使用场景
  • 预期收益
  • 可能的实现方案

**Step 4: 提交文档变更**

```bash
git add README.md docs/
git commit -m "docs: update project documentation and architecture guide"

总结

本实施计划涵盖了以下主要方面:

Phase 1: Handler 函数式迁移(9 个任务)

  • Task 1-9: 将所有 Handler 从注解式迁移到函数式 WebFlux 风格

Phase 2: Router 配置完善(1 个任务)

  • Task 10: 扩展 SystemRouter 配置,支持所有模块

Phase 3: Converter 优化(1 个任务)

  • Task 11: 为所有 Converter 添加批量转换方法

Phase 4: MapStruct 引入(3 个任务)

  • Task 12: 配置 MapStruct 依赖
  • Task 13: 创建 MapStruct Mapper 接口
  • Task 14: 使用 MapStruct 替换手动 Converter

Phase 5: 单元测试(4 个任务)

  • Task 15-18: 为 Repository、Service、Handler 层添加完整的单元测试

Phase 6: 集成测试和文档(2 个任务)

  • Task 19: 添加集成测试
  • Task 20: 更新项目文档

总计: 20 个任务,覆盖所有未完成的工作和后续建议。


Plan complete and saved to docs/plans/2026-03-12-infrastructure-refactoring-phase2.md. Two execution options:

1. Subagent-Driven (this session) - I dispatch fresh subagent per task, review between tasks, fast iteration

2. Parallel Session (separate) - Open new session with executing-plans, batch execution with checkpoints

Which approach?