# System Quality Improvement Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** 建立完整的自动化测试基础设施、完善核心功能实现、优化系统性能和运维能力,将系统完成度从 68% 提升至 90% 以上。 **Architecture:** 采用"质量左移"策略,优先建立自动化测试和质量门禁,然后逐步完善功能,最后优化效能。保持现有的分层架构(Handler → Service → DAO → Entity),完成函数式 WebFlux 风格迁移,建立完整的单元测试和集成测试覆盖。 **Tech Stack:** Spring WebFlux, R2DBC, MapStruct, JUnit 5, Mockito, Testcontainers, JaCoCo, Maven, Vue 3, TypeScript, Playwright --- ## Phase 1: 质量基础设施(2-3周) ### Task 1: 配置 JaCoCo 代码覆盖率工具 **Files:** - Modify: `novalon-manage-api/pom.xml` **Step 1: 添加 JaCoCo Maven 插件配置** 在 `` 部分添加: ```xml org.jacoco jacoco-maven-plugin 0.8.12 prepare-agent prepare-agent report verify report check verify check BUNDLE INSTRUCTION COVEREDRATIO 0.80 ``` **Step 2: 验证配置** ```bash cd novalon-manage-api mvn clean verify ``` Expected: 构建成功,生成覆盖率报告在 `target/site/jacoco/index.html` **Step 3: 提交变更** ```bash git add novalon-manage-api/pom.xml git commit -m "feat: add JaCoCo code coverage plugin with 80% threshold" ``` --- ### Task 2: 创建测试基础配置类 **Files:** - Create: `novalon-manage-api/manage-sys/src/test/java/cn/novalon/manage/sys/config/UnitTestConfig.java` **Step 1: 创建单元测试配置类** ```java package cn.novalon.manage.sys.config; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Primary; import org.springframework.r2dbc.core.R2dbcEntityTemplate; import org.springframework.r2dbc.core.DefaultReactiveDataAccessStrategy; import io.r2dbc.spi.ConnectionFactory; import org.mockito.Mockito; @TestConfiguration public class UnitTestConfig { @Bean @Primary public ConnectionFactory testConnectionFactory() { return Mockito.mock(ConnectionFactory.class); } @Bean @Primary public R2dbcEntityTemplate testR2dbcEntityTemplate(ConnectionFactory connectionFactory) { return new R2dbcEntityTemplate(connectionFactory, new DefaultReactiveDataAccessStrategy()); } } ``` **Step 2: 创建集成测试配置类** ```java package cn.novalon.manage.sys.config; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Primary; import org.springframework.r2dbc.core.R2dbcEntityTemplate; import org.springframework.r2dbc.core.DefaultReactiveDataAccessStrategy; import io.r2dbc.spi.ConnectionFactory; import org.testcontainers.containers.PostgreSQLContainer; import org.testcontainers.utility.DockerImageName; @TestConfiguration public class IntegrationTestConfig { @Bean @Primary public PostgreSQLContainer postgresContainer() { return new PostgreSQLContainer<>(DockerImageName.parse("postgres:15-alpine")) .withDatabaseName("testdb") .withUsername("test") .withPassword("test"); } } ``` **Step 3: 提交变更** ```bash git add novalon-manage-api/manage-sys/src/test/java/cn/novalon/manage/sys/config/ git commit -m "test: add unit test and integration test configuration" ``` --- ### Task 3: 为 DictionaryService 编写单元测试 **Files:** - Create: `novalon-manage-api/manage-sys/src/test/java/cn/novalon/manage/sys/core/service/impl/DictionaryServiceTest.java` **Step 1: 编写测试类框架** ```java package cn.novalon.manage.sys.core.service.impl; import cn.novalon.manage.sys.core.domain.Dictionary; import cn.novalon.manage.sys.infrastructure.db.dao.DictionaryDao; import cn.novalon.manage.sys.infrastructure.db.entity.DictionaryEntity; import cn.novalon.manage.sys.infrastructure.db.converter.DictionaryConverter; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; 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 static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class DictionaryServiceTest { @Mock private DictionaryDao dictionaryDao; @Mock private DictionaryConverter dictionaryConverter; @InjectMocks private DictionaryService dictionaryService; private Dictionary testDictionary; private DictionaryEntity testEntity; @BeforeEach void setUp() { testDictionary = new Dictionary(); testDictionary.setId(1L); testDictionary.setDictType("test_type"); testDictionary.setDictLabel("Test Label"); testDictionary.setDictValue("test_value"); testDictionary.setStatus(1); testEntity = new DictionaryEntity(); testEntity.setId(1L); testEntity.setDictType("test_type"); testEntity.setDictLabel("Test Label"); testEntity.setDictValue("test_value"); testEntity.setStatus(1); } @Test void testFindAll() { when(dictionaryDao.findAll()).thenReturn(Flux.just(testEntity)); when(dictionaryConverter.toDomain(any())).thenReturn(testDictionary); StepVerifier.create(dictionaryService.findAll()) .expectNext(testDictionary) .verifyComplete(); verify(dictionaryDao, times(1)).findAll(); verify(dictionaryConverter, times(1)).toDomain(any()); } @Test void testFindById() { when(dictionaryDao.findById(1L)).thenReturn(Mono.just(testEntity)); when(dictionaryConverter.toDomain(testEntity)).thenReturn(testDictionary); StepVerifier.create(dictionaryService.findById(1L)) .expectNext(testDictionary) .verifyComplete(); verify(dictionaryDao, times(1)).findById(1L); } @Test void testFindById_NotFound() { when(dictionaryDao.findById(999L)).thenReturn(Mono.empty()); StepVerifier.create(dictionaryService.findById(999L)) .verifyComplete(); verify(dictionaryDao, times(1)).findById(999L); verify(dictionaryConverter, never()).toDomain(any()); } @Test void testSave() { when(dictionaryConverter.toEntity(any())).thenReturn(testEntity); when(dictionaryDao.save(any())).thenReturn(Mono.just(testEntity)); when(dictionaryConverter.toDomain(testEntity)).thenReturn(testDictionary); StepVerifier.create(dictionaryService.save(testDictionary)) .expectNext(testDictionary) .verifyComplete(); verify(dictionaryConverter, times(1)).toEntity(testDictionary); verify(dictionaryDao, times(1)).save(testEntity); } @Test void testUpdate() { when(dictionaryDao.findById(1L)).thenReturn(Mono.just(testEntity)); when(dictionaryDao.save(any())).thenReturn(Mono.just(testEntity)); when(dictionaryConverter.toDomain(testEntity)).thenReturn(testDictionary); StepVerifier.create(dictionaryService.update(1L, testDictionary)) .expectNext(testDictionary) .verifyComplete(); verify(dictionaryDao, times(1)).findById(1L); verify(dictionaryDao, times(1)).save(any()); } @Test void testDeleteById() { when(dictionaryDao.deleteById(1L)).thenReturn(Mono.empty()); StepVerifier.create(dictionaryService.deleteById(1L)) .verifyComplete(); verify(dictionaryDao, times(1)).deleteById(1L); } @Test void testFindByDictType() { when(dictionaryDao.findByDictType("test_type")).thenReturn(Flux.just(testEntity)); when(dictionaryConverter.toDomain(any())).thenReturn(testDictionary); StepVerifier.create(dictionaryService.findByDictType("test_type")) .expectNext(testDictionary) .verifyComplete(); verify(dictionaryDao, times(1)).findByDictType("test_type"); } } ``` **Step 2: 运行测试** ```bash cd novalon-manage-api/manage-sys mvn test -Dtest=DictionaryServiceTest ``` Expected: 所有测试通过 **Step 3: 提交变更** ```bash git add novalon-manage-api/manage-sys/src/test/java/cn/novalon/manage/sys/core/service/impl/DictionaryServiceTest.java git commit -m "test: add unit tests for DictionaryService" ``` --- ### Task 4: 为 SysUserService 编写单元测试 **Files:** - Create: `novalon-manage-api/manage-sys/src/test/java/cn/novalon/manage/sys/core/service/impl/SysUserServiceTest.java` **Step 1: 编写测试类** ```java package cn.novalon.manage.sys.core.service.impl; import cn.novalon.manage.sys.core.domain.SysUser; import cn.novalon.manage.sys.infrastructure.db.dao.SysUserDao; import cn.novalon.manage.sys.infrastructure.db.entity.SysUserEntity; import cn.novalon.manage.sys.infrastructure.db.converter.SysUserConverter; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.security.crypto.password.PasswordEncoder; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class SysUserServiceTest { @Mock private SysUserDao userDao; @Mock private SysUserConverter userConverter; @Mock private PasswordEncoder passwordEncoder; @InjectMocks private SysUserService userService; private SysUser testUser; private SysUserEntity testEntity; @BeforeEach void setUp() { testUser = new SysUser(); testUser.setId(1L); testUser.setUsername("testuser"); testUser.setPassword("encoded_password"); testUser.setEmail("test@example.com"); testUser.setStatus(1); testEntity = new SysUserEntity(); testEntity.setId(1L); testEntity.setUsername("testuser"); testEntity.setPassword("encoded_password"); testEntity.setEmail("test@example.com"); testEntity.setStatus(1); } @Test void testFindAll() { when(userDao.findAll()).thenReturn(Flux.just(testEntity)); when(userConverter.toDomain(any())).thenReturn(testUser); StepVerifier.create(userService.findAll()) .expectNext(testUser) .verifyComplete(); verify(userDao, times(1)).findAll(); } @Test void testFindById() { when(userDao.findById(1L)).thenReturn(Mono.just(testEntity)); when(userConverter.toDomain(testEntity)).thenReturn(testUser); StepVerifier.create(userService.findById(1L)) .expectNext(testUser) .verifyComplete(); verify(userDao, times(1)).findById(1L); } @Test void testFindByUsername() { when(userDao.findByUsername("testuser")).thenReturn(Mono.just(testEntity)); when(userConverter.toDomain(testEntity)).thenReturn(testUser); StepVerifier.create(userService.findByUsername("testuser")) .expectNext(testUser) .verifyComplete(); verify(userDao, times(1)).findByUsername("testuser"); } @Test void testCreateUser() { SysUser newUser = new SysUser(); newUser.setUsername("newuser"); newUser.setPassword("raw_password"); newUser.setEmail("new@example.com"); when(passwordEncoder.encode(anyString())).thenReturn("encoded_password"); when(userConverter.toEntity(any())).thenReturn(testEntity); when(userDao.save(any())).thenReturn(Mono.just(testEntity)); when(userConverter.toDomain(testEntity)).thenReturn(testUser); StepVerifier.create(userService.createUser(newUser)) .expectNext(testUser) .verifyComplete(); verify(passwordEncoder, times(1)).encode("raw_password"); verify(userDao, times(1)).save(any()); } @Test void testAuthenticate_Success() { when(userDao.findByUsername("testuser")).thenReturn(Mono.just(testEntity)); when(passwordEncoder.matches("correct_password", "encoded_password")).thenReturn(true); when(userConverter.toDomain(testEntity)).thenReturn(testUser); StepVerifier.create(userService.authenticate("testuser", "correct_password")) .expectNext(testUser) .verifyComplete(); verify(userDao, times(1)).findByUsername("testuser"); verify(passwordEncoder, times(1)).matches("correct_password", "encoded_password"); } @Test void testAuthenticate_Failure() { when(userDao.findByUsername("testuser")).thenReturn(Mono.just(testEntity)); when(passwordEncoder.matches("wrong_password", "encoded_password")).thenReturn(false); StepVerifier.create(userService.authenticate("testuser", "wrong_password")) .verifyError(); verify(passwordEncoder, times(1)).matches("wrong_password", "encoded_password"); verify(userConverter, never()).toDomain(any()); } @Test void testExistsByUsername() { when(userDao.existsByUsername("testuser")).thenReturn(Mono.just(true)); StepVerifier.create(userService.existsByUsername("testuser")) .expectNext(true) .verifyComplete(); verify(userDao, times(1)).existsByUsername("testuser"); } @Test void testExistsByEmail() { when(userDao.existsByEmail("test@example.com")).thenReturn(Mono.just(true)); StepVerifier.create(userService.existsByEmail("test@example.com")) .expectNext(true) .verifyComplete(); verify(userDao, times(1)).existsByEmail("test@example.com"); } @Test void testLogicalDeleteUser() { when(userDao.findById(1L)).thenReturn(Mono.just(testEntity)); testEntity.setDeleted(true); when(userDao.save(any())).thenReturn(Mono.just(testEntity)); when(userConverter.toDomain(testEntity)).thenReturn(testUser); StepVerifier.create(userService.logicalDeleteUser(1L)) .expectNext(testUser) .verifyComplete(); verify(userDao, times(1)).findById(1L); verify(userDao, times(1)).save(any()); } @Test void testRestoreUser() { when(userDao.findById(1L)).thenReturn(Mono.just(testEntity)); testEntity.setDeleted(false); when(userDao.save(any())).thenReturn(Mono.just(testEntity)); when(userConverter.toDomain(testEntity)).thenReturn(testUser); StepVerifier.create(userService.restoreUser(1L)) .expectNext(testUser) .verifyComplete(); verify(userDao, times(1)).findById(1L); verify(userDao, times(1)).save(any()); } } ``` **Step 2: 运行测试** ```bash cd novalon-manage-api/manage-sys mvn test -Dtest=SysUserServiceTest ``` Expected: 所有测试通过 **Step 3: 提交变更** ```bash git add novalon-manage-api/manage-sys/src/test/java/cn/novalon/manage/sys/core/service/impl/SysUserServiceTest.java git commit -m "test: add unit tests for SysUserService" ``` --- ### Task 5-14: 为其他 Service 编写单元测试 按照 Task 3-4 的模式,为以下 Service 编写完整的单元测试: - SysRoleService - SysConfigService - SysNoticeService - SysFileService - OperationLogService - SysLoginLogService - SysUserMessageService - SysMenuService - SysDictTypeService - SysDictDataService - SysExceptionLogService 每个 Service 测试应包含: - findAll() 测试 - findById() 测试 - save() 测试 - update() 测试(如果适用) - deleteById() 测试 - 业务特定方法测试 --- ### Task 15: 运行所有单元测试并生成覆盖率报告 **Files:** - None **Step 1: 运行所有测试** ```bash cd novalon-manage-api/manage-sys mvn clean verify ``` Expected: 所有测试通过,生成覆盖率报告 **Step 2: 检查覆盖率报告** ```bash open target/site/jacoco/index.html ``` Expected: 覆盖率 >= 80% **Step 3: 如果覆盖率不足,补充测试** 根据覆盖率报告,补充缺失的测试用例 **Step 4: 提交最终测试结果** ```bash git add . git commit -m "test: complete unit tests with 80%+ coverage" ``` --- ### Task 16: 配置 Woodpecker CI/CD 流水线 **Files:** - Modify: `.woodpecker.yml` **Step 1: 更新 CI/CD 配置** ```yaml pipeline: build: image: maven:3.9-eclipse-temurin-21 commands: - cd novalon-manage-api - mvn clean compile test: image: maven:3.9-eclipse-temurin-21 commands: - cd novalon-manage-api - mvn test coverage: image: maven:3.9-eclipse-temurin-21 commands: - cd novalon-manage-api - mvn verify - echo "Coverage report generated" frontend-test: image: node:20 commands: - cd novalon-manage-web - npm install - npm run test frontend-build: image: node:20 commands: - cd novalon-manage-web - npm install - npm run build ``` **Step 2: 提交变更** ```bash git add .woodpecker.yml git commit -m "ci: configure Woodpecker CI/CD pipeline with tests" ``` --- ### Task 17: 添加静态代码分析 **Files:** - Modify: `novalon-manage-api/pom.xml` **Step 1: 添加 SpotBugs 插件** ```xml com.github.spotbugs spotbugs-maven-plugin 4.8.6.0 com.github.spotbugs spotbugs 4.8.6 spotbugs-check verify check ``` **Step 2: 运行静态分析** ```bash cd novalon-manage-api mvn spotbugs:check ``` Expected: 无严重 Bug **Step 3: 提交变更** ```bash git add novalon-manage-api/pom.xml git commit -m "ci: add SpotBugs static code analysis" ``` --- ## Phase 2: 功能完善(3-4周) ### Task 18: 完成 SysUserHandler 函数式迁移 **Files:** - Modify: `novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/handler/user/SysUserHandler.java` **Step 1: 备份当前实现** ```bash cp novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/handler/user/SysUserHandler.java \ novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/handler/user/SysUserHandler.java.bak ``` **Step 2: 修改为函数式风格** ```java package cn.novalon.manage.sys.handler.user; import cn.novalon.manage.sys.core.domain.SysUser; import cn.novalon.manage.sys.core.service.ISysUserService; import cn.novalon.manage.sys.dto.request.PageRequest; import cn.novalon.manage.sys.dto.request.PasswordChangeRequest; import cn.novalon.manage.sys.dto.request.UserUpdateRequest; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.reactive.function.server.ServerResponse; import reactor.core.publisher.Mono; @Component public class SysUserHandler { private final ISysUserService userService; public SysUserHandler(ISysUserService userService) { this.userService = userService; } public Mono getAllUsers(ServerRequest request) { boolean includeDeleted = Boolean.parseBoolean( 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: 更新路由配置** 在 `SystemRouter.java` 中更新用户路由: ```java @Bean public RouterFunction userRoutes(SysUserHandler userHandler) { return RouterFunctions.route() .GET("/api/users", userHandler::getAllUsers) .GET("/api/users/page", userHandler::getUsersByPage) .GET("/api/users/{id}", userHandler::getUserById) .GET("/api/users/username/{username}", userHandler::getUserByUsername) .POST("/api/users", userHandler::createUser) .PUT("/api/users/{id}", userHandler::updateUser) .DELETE("/api/users/{id}", userHandler::deleteUser) .PUT("/api/users/{id}/password", userHandler::changePassword) .DELETE("/api/users/{id}/logical", userHandler::logicalDeleteUser) .POST("/api/users/logical-delete", userHandler::logicalDeleteUsers) .POST("/api/users/{id}/restore", userHandler::restoreUser) .POST("/api/users/restore", userHandler::restoreUsers) .GET("/api/users/check/username", userHandler::checkUsernameExists) .GET("/api/users/check/email", userHandler::checkEmailExists) .build(); } ``` **Step 4: 测试路由** ```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","email":"test@example.com"}' ``` **Step 5: 提交变更** ```bash git add novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/handler/user/SysUserHandler.java git commit -m "refactor: migrate SysUserHandler to functional WebFlux style" ``` --- ### Task 19: 完成其他 Handler 的函数式迁移 按照 Task 18 的模式,完成以下 Handler 的函数式迁移: - SysRoleHandler - SysConfigHandler - SysNoticeHandler - SysFileHandler - SysLogHandler - SysAuthHandler - SysUserMessageHandler - StatsHandler 每个 Handler 迁移应包含: 1. 备份当前实现 2. 修改为函数式风格 3. 更新路由配置 4. 测试路由 5. 提交变更 --- ### Task 20: 实现前端用户管理页面 **Files:** - Modify: `novalon-manage-web/src/views/system/UserManagement.vue` **Step 1: 实现用户列表功能** ```vue ``` **Step 2: 创建用户模态框组件** ```vue ``` **Step 3: 创建用户 API** ```typescript import request from '@/utils/request'; export interface User { id: number; username: string; email: string; status: number; createdAt: string; } export interface PageResponse { data: T[]; total: number; page: number; size: number; } export interface CreateUserRequest { username: string; email: string; password: string; status?: number; } export interface UpdateUserRequest { email?: string; status?: number; } export const getUsers = (params: { page: number; size: number }) => { return request.get>('/api/users/page', { params }); }; export const getUserById = (id: number) => { return request.get(`/api/users/${id}`); }; export const createUser = (data: CreateUserRequest) => { return request.post('/api/users', data); }; export const updateUser = (id: number, data: UpdateUserRequest) => { return request.put(`/api/users/${id}`, data); }; export const deleteUser = (id: number) => { return request.delete(`/api/users/${id}`); }; ``` **Step 4: 测试前端页面** ```bash cd novalon-manage-web npm run dev ``` 访问 http://localhost:5173/system/users **Step 5: 提交变更** ```bash git add novalon-manage-web/src/views/system/UserManagement.vue git add novalon-manage-web/src/views/system/UserModal.vue git add novalon-manage-web/src/api/user.ts git commit -m "feat: implement user management page" ``` --- ### Task 21: 实现其他前端管理页面 按照 Task 20 的模式,实现以下管理页面: - 角色管理页面 (RoleManagement.vue) - 菜单管理页面 (MenuManagement.vue) - 字典管理页面 (DictManagement.vue) - 系统配置页面 (ConfigManagement.vue) - 通知管理页面 (NoticeManagement.vue) - 文件管理页面 (FileManagement.vue) - 操作日志页面 (OperationLog.vue) - 登录日志页面 (LoginLog.vue) 每个页面应包含: 1. 列表展示 2. 新增/编辑/删除功能 3. 搜索/筛选功能 4. 分页功能 5. 表单验证 6. 错误处理 --- ### Task 22: 完善 API 文档 **Files:** - Modify: `novalon-manage-api/manage-sys/src/main/resources/application.yml` **Step 1: 配置 OpenAPI 文档** ```yaml springdoc: api-docs: path: /api-docs swagger-ui: path: /swagger-ui.html enabled: true show-actuator: true packages-to-scan: cn.novalon.manage.sys.handler ``` **Step 2: 为 Handler 添加 API 文档注解** ```java @Operation(summary = "获取所有用户", description = "获取系统中所有用户列表") @ApiResponse(responseCode = "200", description = "成功") public Mono getAllUsers(ServerRequest request) { // ... } @Operation(summary = "创建用户", description = "创建新用户") @ApiResponse(responseCode = "201", description = "创建成功") @ApiResponse(responseCode = "400", description = "请求参数错误") public Mono createUser(ServerRequest request) { // ... } ``` **Step 3: 访问 API 文档** 启动应用后访问:http://localhost:8080/swagger-ui.html **Step 4: 导出 API 文档** ```bash curl http://localhost:8080/api-docs -o api-docs.json ``` **Step 5: 提交变更** ```bash git add novalon-manage-api/manage-sys/src/main/resources/application.yml git commit -m "docs: configure OpenAPI documentation" ``` --- ## Phase 3: 效能优化(2-3周) ### Task 23: 性能测试 **Files:** - Create: `novalon-manage-api/manage-sys/src/test/k6/performance-test.js` **Step 1: 创建性能测试脚本** ```javascript import http from 'k6/http'; import { check, sleep } from 'k6'; export const options = { stages: [ { duration: '30s', target: 10 }, { duration: '1m', target: 50 }, { duration: '30s', target: 0 }, ], thresholds: { http_req_duration: ['p(95)<500'], http_req_failed: ['rate<0.01'], }, }; const BASE_URL = 'http://localhost:8080'; export default function () { let response = http.get(`${BASE_URL}/api/users`); check(response, { 'status is 200': (r) => r.status === 200, 'response time < 500ms': (r) => r.timings.duration < 500, }); sleep(1); } ``` **Step 2: 运行性能测试** ```bash k6 run novalon-manage-api/manage-sys/src/test/k6/performance-test.js ``` **Step 3: 分析性能测试结果** 根据测试结果,识别性能瓶颈: - 响应时间过长的 API - 并发处理能力不足的接口 - 数据库查询慢的问题 **Step 4: 提交性能测试脚本** ```bash git add novalon-manage-api/manage-sys/src/test/k6/performance-test.js git commit -m "test: add performance testing script with k6" ``` --- ### Task 24: 数据库查询优化 **Files:** - Create: `docs/sql/performance-optimization.sql` **Step 1: 分析慢查询** ```sql -- 启用慢查询日志 ALTER SYSTEM SET log_min_duration_statement = 1000; -- 查看慢查询 SELECT query, mean_exec_time, calls, total_exec_time FROM pg_stat_statements ORDER BY mean_exec_time DESC LIMIT 10; ``` **Step 2: 添加必要的索引** ```sql -- 用户表索引 CREATE INDEX idx_users_username ON sys_users(username); CREATE INDEX idx_users_email ON sys_users(email); CREATE INDEX idx_users_status ON sys_users(status); CREATE INDEX idx_users_deleted ON sys_users(deleted); -- 角色表索引 CREATE INDEX idx_roles_role_key ON sys_roles(role_key); CREATE INDEX idx_roles_status ON sys_roles(status); -- 菜单表索引 CREATE INDEX idx_menus_parent_id ON sys_menus(parent_id); CREATE INDEX idx_menus_status ON sys_menus(status); -- 操作日志索引 CREATE INDEX idx_operation_logs_created_at ON operation_logs(created_at); CREATE INDEX idx_operation_logs_user_id ON operation_logs(user_id); -- 登录日志索引 CREATE INDEX idx_login_logs_created_at ON sys_login_logs(created_at); CREATE INDEX idx_login_logs_username ON sys_login_logs(username); ``` **Step 3: 验证索引效果** ```sql EXPLAIN ANALYZE SELECT * FROM sys_users WHERE username = 'testuser'; ``` **Step 4: 提交优化脚本** ```bash git add docs/sql/performance-optimization.sql git commit -m "perf: add database indexes for performance optimization" ``` --- ### Task 25: 缓存策略优化 **Files:** - Modify: `novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/config/CacheConfig.java` **Step 1: 配置 Caffeine 缓存** ```java package cn.novalon.manage.sys.config; import com.github.benmanes.caffeine.cache.Caffeine; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.EnableCaching; import org.springframework.cache.caffeine.CaffeineCacheManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.concurrent.TimeUnit; @Configuration @EnableCaching public class CacheConfig { @Bean public CacheManager cacheManager() { CaffeineCacheManager cacheManager = new CaffeineCacheManager(); cacheManager.setCaffeine(caffeineCacheBuilder()); return cacheManager; } private Caffeine caffeineCacheBuilder() { return Caffeine.newBuilder() .initialCapacity(100) .maximumSize(500) .expireAfterWrite(30, TimeUnit.MINUTES) .recordStats(); } } ``` **Step 2: 为 Service 添加缓存注解** ```java @Cacheable(value = "users", key = "#id") public Mono findById(Long id) { return userDao.findById(id) .map(userConverter::toDomain); } @CacheEvict(value = "users", key = "#user.id") public Mono save(SysUser user) { return userDao.save(userConverter.toEntity(user)) .map(userConverter::toDomain); } @CacheEvict(value = "users", key = "#id") public Mono deleteById(Long id) { return userDao.deleteById(id); } ``` **Step 3: 测试缓存效果** ```bash # 第一次请求 curl http://localhost:8080/api/users/1 # 第二次请求(应该从缓存读取) curl http://localhost:8080/api/users/1 ``` **Step 4: 提交缓存配置** ```bash git add novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/config/CacheConfig.java git commit -m "perf: add Caffeine cache configuration" ``` --- ### Task 26: 添加监控和告警 **Files:** - Modify: `novalon-manage-api/manage-sys/src/main/resources/application.yml` **Step 1: 配置 Spring Boot Actuator** ```yaml management: endpoints: web: exposure: include: health,info,metrics,prometheus endpoint: health: show-details: always metrics: enabled: true metrics: export: prometheus: enabled: true ``` **Step 2: 添加 Prometheus 配置** ```yaml # prometheus.yml global: scrape_interval: 15s scrape_configs: - job_name: 'novalon-manage-system' metrics_path: '/actuator/prometheus' static_configs: - targets: ['localhost:8080'] ``` **Step 3: 配置 Grafana** 创建 Grafana Dashboard 监控: - JVM 内存使用 - HTTP 请求响应时间 - 数据库连接池状态 - 缓存命中率 - 错误率 **Step 4: 提交监控配置** ```bash git add novalon-manage-api/manage-sys/src/main/resources/application.yml git commit -m "monitor: add Prometheus and Grafana monitoring" ``` --- ### Task 27: 安全扫描 **Files:** - Create: `novalon-manage-api/manage-sys/src/test/owasp/dependency-check.sh` **Step 1: 创建依赖检查脚本** ```bash #!/bin/bash cd novalon-manage-api mvn org.owasp:dependency-check-maven:check echo "Dependency check completed. Check the report at: target/dependency-check-report.html" ``` **Step 2: 运行安全扫描** ```bash chmod +x novalon-manage-api/manage-sys/src/test/owasp/dependency-check.sh ./novalon-manage-api/manage-sys/src/test/owasp/dependency-check.sh ``` **Step 3: 修复安全漏洞** 根据扫描结果,修复发现的安全漏洞 **Step 4: 提交安全扫描脚本** ```bash git add novalon-manage-api/manage-sys/src/test/owasp/dependency-check.sh git commit -m "security: add OWASP dependency check" ``` --- ### Task 28: 编写架构设计文档 **Files:** - Create: `docs/architecture/system-architecture.md` **Step 1: 创建架构文档** ```markdown # 系统架构设计文档 ## 1. 系统概述 Novalon 管理系统是一个企业级后台管理系统,采用前后端分离架构,基于 Spring WebFlux 响应式编程模型。 ## 2. 技术架构 ### 2.1 后端架构 - **框架**: Spring Boot 3.4.1 - **编程模型**: 响应式 WebFlux - **数据库**: PostgreSQL + R2DBC - **认证**: JWT + Spring Security - **缓存**: Caffeine - **文档**: SpringDoc OpenAPI ### 2.2 前端架构 - **框架**: Vue 3 + TypeScript - **UI 组件**: Ant Design Vue - **状态管理**: Pinia - **路由**: Vue Router - **构建工具**: Vite ## 3. 分层架构 ``` ┌─────────────────────────────────────┐ │ Frontend (Vue 3) │ └──────────────┬──────────────────────┘ │ HTTP/WebSocket ┌──────────────▼──────────────────────┐ │ Handler Layer │ │ (Functional WebFlux Routes) │ └──────────────┬──────────────────────┘ │ ┌──────────────▼──────────────────────┐ │ Service Layer │ │ (Business Logic) │ └──────────────┬──────────────────────┘ │ ┌──────────────▼──────────────────────┐ │ DAO Layer │ │ (Data Access Object) │ └──────────────┬──────────────────────┘ │ ┌──────────────▼──────────────────────┐ │ Entity Layer │ │ (Database Entities) │ └──────────────┬──────────────────────┘ │ ┌──────────────▼──────────────────────┐ │ Database (PostgreSQL) │ └─────────────────────────────────────┘ ``` ## 4. 核心模块 ### 4.1 用户管理 - 用户 CRUD 操作 - 用户认证与授权 - 密码管理 - 角色分配 ### 4.2 角色管理 - 角色定义 - 权限配置 - 菜单关联 ### 4.3 菜单管理 - 菜单树结构 - 路由配置 - 权限控制 ### 4.4 字典管理 - 字典类型管理 - 字典数据管理 ### 4.5 系统配置 - 系统参数配置 - 配置管理 - 缓存刷新 ### 4.6 审计日志 - 操作日志 - 登录日志 - 异常日志 ### 4.7 通知中心 - 通知公告 - 用户消息 - WebSocket 推送 ### 4.8 文件管理 - 文件上传 - 文件下载 - 文件预览 ## 5. 数据流 ### 5.1 请求流程 1. 前端发送 HTTP 请求 2. Handler 层接收请求并解析 3. Service 层处理业务逻辑 4. DAO 层访问数据库 5. 数据库返回结果 6. 逐层返回给前端 ### 5.2 响应式数据流 ``` Frontend Request ↓ Handler (Mono/Flux) ↓ Service (Mono/Flux) ↓ DAO (Mono/Flux) ↓ Database (R2DBC) ↓ Response (Mono/Flux) ↓ Frontend ``` ## 6. 安全设计 ### 6.1 认证机制 - JWT Token 认证 - Token 刷新机制 - 密码 BCrypt 加密 ### 6.2 授权机制 - 基于角色的访问控制 (RBAC) - API 级别权限控制 - 菜单级别权限控制 ### 6.3 审计机制 - 操作日志记录 - 登录日志记录 - 异常日志记录 ## 7. 性能优化 ### 7.1 响应式编程 - 非阻塞 I/O - 背压机制 - 异步处理 ### 7.2 缓存策略 - Caffeine 本地缓存 - 缓存预热 - 缓存失效策略 ### 7.3 数据库优化 - 索引优化 - 查询优化 - 连接池配置 ## 8. 监控与运维 ### 8.1 健康检查 - Spring Boot Actuator - 数据库连接检查 - 缓存状态检查 ### 8.2 指标监控 - Prometheus 指标采集 - Grafana 可视化 - 告警规则配置 ### 8.3 日志管理 - 结构化日志 - 日志级别控制 - 日志归档策略 ## 9. 部署架构 ### 9.1 容器化部署 - Docker 镜像构建 - Docker Compose 编排 - Kubernetes 部署(可选) ### 9.2 CI/CD 流水线 - Woodpecker CI - 自动化测试 - 自动化部署 ## 10. 扩展性设计 ### 10.1 水平扩展 - 无状态设计 - 负载均衡 - 会话共享 ### 10.2 垂直扩展 - 资源优化 - 性能调优 - 缓存优化 ``` **Step 2: 提交架构文档** ```bash git add docs/architecture/system-architecture.md git commit -m "docs: add system architecture design document" ``` --- ### Task 29: 编写部署文档 **Files:** - Create: `docs/deployment/deployment-guide.md` **Step 1: 创建部署文档** ```markdown # 部署指南 ## 1. 环境要求 ### 1.1 开发环境 - JDK 21 - Node.js 20+ - PostgreSQL 15+ - Maven 3.9+ - Docker (可选) ### 1.2 生产环境 - JDK 21 - PostgreSQL 15+ - Nginx (可选) - Docker/Kubernetes ## 2. 本地开发部署 ### 2.1 后端部署 ```bash # 克隆项目 git clone cd novalon-manage-system # 配置数据库 cd novalon-manage-api/manage-sys/src/main/resources vi application.yml # 修改数据库配置 spring: r2dbc: url: r2dbc:postgresql://localhost:5432/novalon username: your_username password: your_password # 运行数据库迁移 cd novalon-manage-api mvn flyway:migrate # 启动后端服务 cd manage-sys mvn spring-boot:run ``` ### 2.2 前端部署 ```bash # 安装依赖 cd novalon-manage-web npm install # 配置 API 地址 vi .env.development VITE_API_BASE_URL=http://localhost:8080 # 启动开发服务器 npm run dev ``` ## 3. Docker 部署 ### 3.1 使用 Docker Compose ```bash # 构建并启动所有服务 docker-compose up -d # 查看日志 docker-compose logs -f # 停止服务 docker-compose down ``` ### 3.2 单独构建镜像 ```bash # 构建后端镜像 cd novalon-manage-api docker build -t novalon-manage-api:latest . # 构建前端镜像 cd novalon-manage-web docker build -t novalon-manage-web:latest . ``` ## 4. 生产环境部署 ### 4.1 数据库配置 ```sql -- 创建数据库 CREATE DATABASE novalon; -- 创建用户 CREATE USER novalon_user WITH PASSWORD 'secure_password'; -- 授权 GRANT ALL PRIVILEGES ON DATABASE novalon TO novalon_user; ``` ### 4.2 后端部署 ```bash # 构建生产包 cd novalon-manage-api mvn clean package -Pprod # 运行应用 java -jar manage-sys/target/manage-sys-1.0.0.jar \ --spring.profiles.active=prod \ --spring.r2dbc.url=r2dbc:postgresql://prod-db:5432/novalon \ --spring.r2dbc.username=novalon_user \ --spring.r2dbc.password=secure_password ``` ### 4.3 前端部署 ```bash # 构建生产包 cd novalon-manage-web npm run build:prod # 使用 Nginx 部署 cp -r dist/* /var/www/html/ ``` ### 4.4 Nginx 配置 ```nginx server { listen 80; server_name your-domain.com; root /var/www/html; index index.html; location / { try_files $uri $uri/ /index.html; } location /api { proxy_pass http://localhost:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } } ``` ## 5. 监控与日志 ### 5.1 健康检查 ```bash # 检查应用健康状态 curl http://localhost:8080/actuator/health ``` ### 5.2 查看日志 ```bash # 查看应用日志 tail -f logs/application.log # 查看错误日志 tail -f logs/error.log ``` ### 5.3 Prometheus 指标 访问 http://localhost:8080/actuator/prometheus 查看 Prometheus 指标 ## 6. 备份与恢复 ### 6.1 数据库备份 ```bash # 备份数据库 pg_dump -U novalon_user -h localhost -p 5432 novalon > backup.sql # 恢复数据库 psql -U novalon_user -h localhost -p 5432 novalon < backup.sql ``` ### 6.2 文件备份 ```bash # 备份上传的文件 tar -czf uploads-backup.tar.gz /path/to/uploads ``` ## 7. 故障排查 ### 7.1 常见问题 **问题**: 数据库连接失败 **解决**: 检查数据库服务是否启动,连接配置是否正确 **问题**: API 请求超时 **解决**: 检查网络连接,查看应用日志 **问题**: 前端页面无法访问 **解决**: 检查 Nginx 配置,确保静态文件路径正确 ### 7.2 日志分析 ```bash # 查看错误日志 grep ERROR logs/application.log # 查看特定时间段日志 grep "2024-03-12 10:" logs/application.log ``` ## 8. 升级指南 ### 8.1 数据库迁移 ```bash # 运行新的数据库迁移 mvn flyway:migrate ``` ### 8.2 应用升级 ```bash # 停止旧版本应用 systemctl stop novalon-manage-api # 备份当前版本 cp -r /opt/novalon-manage-api /opt/novalon-manage-api.backup # 部署新版本 cp manage-sys-1.0.0.jar /opt/novalon-manage-api/ # 启动新版本 systemctl start novalon-manage-api ``` ## 9. 安全建议 ### 9.1 密码安全 - 使用强密码 - 定期更换密码 - 使用密码管理工具 ### 9.2 网络安全 - 启用 HTTPS - 配置防火墙 - 限制访问 IP ### 9.3 应用安全 - 定期更新依赖 - 运行安全扫描 - 及时修复漏洞 ``` **Step 2: 提交部署文档** ```bash git add docs/deployment/deployment-guide.md git commit -m "docs: add deployment guide" ``` --- ## 总结 本实施计划将系统完成度从 68% 提升至 90% 以上,涵盖: ### Phase 1: 质量基础设施(2-3周) - ✅ 配置 JaCoCo 代码覆盖率工具 - ✅ 创建测试基础配置类 - ✅ 为所有 Service 编写单元测试 - ✅ 配置 Woodpecker CI/CD 流水线 - ✅ 添加静态代码分析 ### Phase 2: 功能完善(3-4周) - ✅ 完成 Handler 函数式迁移 - ✅ 实现前端管理页面 - ✅ 完善 API 文档 ### Phase 3: 效能优化(2-3周) - ✅ 性能测试与优化 - ✅ 数据库查询优化 - ✅ 缓存策略优化 - ✅ 添加监控和告警 - ✅ 安全扫描 - ✅ 编写架构和部署文档 ### 交付物 - 单元测试覆盖率 >= 80% - 所有 Handler 迁移完成 - 前端页面功能完整 - API 文档完善 - 性能测试报告 - 监控告警系统 - 完整的运维文档 --- **Plan complete and saved to `docs/plans/2026-03-12-system-quality-improvement.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?**