Files
novalon-manage-system/docs/plans/2026-03-12-system-quality-improvement.md
T

54 KiB

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 插件配置

<build><plugins> 部分添加:

<plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <version>0.8.12</version>
    <executions>
        <execution>
            <id>prepare-agent</id>
            <goals>
                <goal>prepare-agent</goal>
            </goals>
        </execution>
        <execution>
            <id>report</id>
            <phase>verify</phase>
            <goals>
                <goal>report</goal>
            </goals>
        </execution>
        <execution>
            <id>check</id>
            <phase>verify</phase>
            <goals>
                <goal>check</goal>
            </goals>
            <configuration>
                <rules>
                    <rule>
                        <element>BUNDLE</element>
                        <limits>
                            <limit>
                                <counter>INSTRUCTION</counter>
                                <value>COVEREDRATIO</value>
                                <minimum>0.80</minimum>
                            </limit>
                        </limits>
                    </rule>
                </rules>
            </configuration>
        </execution>
    </executions>
</plugin>

Step 2: 验证配置

cd novalon-manage-api
mvn clean verify

Expected: 构建成功,生成覆盖率报告在 target/site/jacoco/index.html

Step 3: 提交变更

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: 创建单元测试配置类

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: 创建集成测试配置类

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: 提交变更

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: 编写测试类框架

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: 运行测试

cd novalon-manage-api/manage-sys
mvn test -Dtest=DictionaryServiceTest

Expected: 所有测试通过

Step 3: 提交变更

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: 编写测试类

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: 运行测试

cd novalon-manage-api/manage-sys
mvn test -Dtest=SysUserServiceTest

Expected: 所有测试通过

Step 3: 提交变更

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: 运行所有测试

cd novalon-manage-api/manage-sys
mvn clean verify

Expected: 所有测试通过,生成覆盖率报告

Step 2: 检查覆盖率报告

open target/site/jacoco/index.html

Expected: 覆盖率 >= 80%

Step 3: 如果覆盖率不足,补充测试

根据覆盖率报告,补充缺失的测试用例

Step 4: 提交最终测试结果

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 配置

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: 提交变更

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 插件

<plugin>
    <groupId>com.github.spotbugs</groupId>
    <artifactId>spotbugs-maven-plugin</artifactId>
    <version>4.8.6.0</version>
    <dependencies>
        <dependency>
            <groupId>com.github.spotbugs</groupId>
            <artifactId>spotbugs</artifactId>
            <version>4.8.6</version>
        </dependency>
    </dependencies>
    <executions>
        <execution>
            <id>spotbugs-check</id>
            <phase>verify</phase>
            <goals>
                <goal>check</goal>
            </goals>
        </execution>
    </executions>
</plugin>

Step 2: 运行静态分析

cd novalon-manage-api
mvn spotbugs:check

Expected: 无严重 Bug

Step 3: 提交变更

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: 备份当前实现

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: 修改为函数式风格

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<ServerResponse> getAllUsers(ServerRequest request) {
        boolean includeDeleted = Boolean.parseBoolean(
            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: 更新路由配置

SystemRouter.java 中更新用户路由:

@Bean
public RouterFunction<ServerResponse> 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: 测试路由

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: 提交变更

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: 实现用户列表功能

<template>
  <div class="user-management">
    <a-card title="用户管理">
      <template #extra>
        <a-button type="primary" @click="showCreateModal">
          <template #icon><PlusOutlined /></template>
          新增用户
        </a-button>
      </template>

      <a-table
        :columns="columns"
        :data-source="users"
        :loading="loading"
        :pagination="pagination"
        @change="handleTableChange"
        rowKey="id"
      >
        <template #bodyCell="{ column, record }">
          <template v-if="column.key === 'status'">
            <a-tag :color="record.status === 1 ? 'green' : 'red'">
              {{ record.status === 1 ? '启用' : '禁用' }}
            </a-tag>
          </template>
          <template v-else-if="column.key === 'action'">
            <a-space>
              <a-button type="link" size="small" @click="showEditModal(record)">
                编辑
              </a-button>
              <a-button type="link" size="small" @click="handleDelete(record.id)">
                删除
              </a-button>
            </a-space>
          </template>
        </template>
      </a-table>
    </a-card>

    <UserModal
      v-model:visible="modalVisible"
      :user="currentUser"
      @success="loadUsers"
    />
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { message } from 'ant-design-vue';
import { PlusOutlined } from '@ant-design/icons-vue';
import { getUsers, deleteUser } from '@/api/user';
import UserModal from './UserModal.vue';

interface User {
  id: number;
  username: string;
  email: string;
  status: number;
  createdAt: string;
}

const users = ref<User[]>([]);
const loading = ref(false);
const modalVisible = ref(false);
const currentUser = ref<User | null>(null);
const pagination = ref({
  current: 1,
  pageSize: 10,
  total: 0,
});

const columns = [
  { title: 'ID', dataIndex: 'id', key: 'id' },
  { title: '用户名', dataIndex: 'username', key: 'username' },
  { title: '邮箱', dataIndex: 'email', key: 'email' },
  { title: '状态', dataIndex: 'status', key: 'status' },
  { title: '创建时间', dataIndex: 'createdAt', key: 'createdAt' },
  { title: '操作', key: 'action' },
];

const loadUsers = async () => {
  loading.value = true;
  try {
    const response = await getUsers({
      page: pagination.value.current - 1,
      size: pagination.value.pageSize,
    });
    users.value = response.data;
    pagination.value.total = response.total;
  } catch (error) {
    message.error('加载用户列表失败');
  } finally {
    loading.value = false;
  }
};

const handleTableChange = (pag: any) => {
  pagination.value.current = pag.current;
  pagination.value.pageSize = pag.pageSize;
  loadUsers();
};

const showCreateModal = () => {
  currentUser.value = null;
  modalVisible.value = true;
};

const showEditModal = (user: User) => {
  currentUser.value = user;
  modalVisible.value = true;
};

const handleDelete = async (id: number) => {
  try {
    await deleteUser(id);
    message.success('删除成功');
    loadUsers();
  } catch (error) {
    message.error('删除失败');
  }
};

onMounted(() => {
  loadUsers();
});
</script>

<style scoped>
.user-management {
  padding: 24px;
}
</style>

Step 2: 创建用户模态框组件

<template>
  <a-modal
    :visible="visible"
    :title="isEdit ? '编辑用户' : '新增用户'"
    @ok="handleSubmit"
    @cancel="handleCancel"
    :confirm-loading="loading"
  >
    <a-form :model="formState" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
      <a-form-item label="用户名" required>
        <a-input v-model:value="formState.username" :disabled="isEdit" />
      </a-form-item>
      <a-form-item label="邮箱" required>
        <a-input v-model:value="formState.email" />
      </a-form-item>
      <a-form-item v-if="!isEdit" label="密码" required>
        <a-input-password v-model:value="formState.password" />
      </a-form-item>
      <a-form-item label="状态">
        <a-select v-model:value="formState.status">
          <a-select-option :value="1">启用</a-select-option>
          <a-select-option :value="0">禁用</a-select-option>
        </a-select>
      </a-form-item>
    </a-form>
  </a-modal>
</template>

<script setup lang="ts">
import { ref, watch, computed } from 'vue';
import { message } from 'ant-design-vue';
import { createUser, updateUser } from '@/api/user';

interface Props {
  visible: boolean;
  user: any;
}

interface Emits {
  (e: 'update:visible', value: boolean): void;
  (e: 'success'): void;
}

const props = defineProps<Props>();
const emit = defineEmits<Emits>();

const loading = ref(false);
const formState = ref({
  username: '',
  email: '',
  password: '',
  status: 1,
});

const isEdit = computed(() => !!props.user);

watch(() => props.visible, (val) => {
  if (val && props.user) {
    formState.value = {
      username: props.user.username,
      email: props.user.email,
      password: '',
      status: props.user.status,
    };
  } else if (val) {
    formState.value = {
      username: '',
      email: '',
      password: '',
      status: 1,
    };
  }
});

const handleSubmit = async () => {
  loading.value = true;
  try {
    if (isEdit.value) {
      await updateUser(props.user.id, {
        email: formState.value.email,
        status: formState.value.status,
      });
      message.success('更新成功');
    } else {
      await createUser({
        username: formState.value.username,
        email: formState.value.email,
        password: formState.value.password,
        status: formState.value.status,
      });
      message.success('创建成功');
    }
    emit('update:visible', false);
    emit('success');
  } catch (error) {
    message.error(isEdit.value ? '更新失败' : '创建失败');
  } finally {
    loading.value = false;
  }
};

const handleCancel = () => {
  emit('update:visible', false);
};
</script>

Step 3: 创建用户 API

import request from '@/utils/request';

export interface User {
  id: number;
  username: string;
  email: string;
  status: number;
  createdAt: string;
}

export interface PageResponse<T> {
  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<PageResponse<User>>('/api/users/page', { params });
};

export const getUserById = (id: number) => {
  return request.get<User>(`/api/users/${id}`);
};

export const createUser = (data: CreateUserRequest) => {
  return request.post<User>('/api/users', data);
};

export const updateUser = (id: number, data: UpdateUserRequest) => {
  return request.put<User>(`/api/users/${id}`, data);
};

export const deleteUser = (id: number) => {
  return request.delete(`/api/users/${id}`);
};

Step 4: 测试前端页面

cd novalon-manage-web
npm run dev

访问 http://localhost:5173/system/users

Step 5: 提交变更

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 文档

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 文档注解

@Operation(summary = "获取所有用户", description = "获取系统中所有用户列表")
@ApiResponse(responseCode = "200", description = "成功")
public Mono<ServerResponse> getAllUsers(ServerRequest request) {
    // ...
}

@Operation(summary = "创建用户", description = "创建新用户")
@ApiResponse(responseCode = "201", description = "创建成功")
@ApiResponse(responseCode = "400", description = "请求参数错误")
public Mono<ServerResponse> createUser(ServerRequest request) {
    // ...
}

Step 3: 访问 API 文档

启动应用后访问:http://localhost:8080/swagger-ui.html

Step 4: 导出 API 文档

curl http://localhost:8080/api-docs -o api-docs.json

Step 5: 提交变更

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: 创建性能测试脚本

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: 运行性能测试

k6 run novalon-manage-api/manage-sys/src/test/k6/performance-test.js

Step 3: 分析性能测试结果

根据测试结果,识别性能瓶颈:

  • 响应时间过长的 API
  • 并发处理能力不足的接口
  • 数据库查询慢的问题

Step 4: 提交性能测试脚本

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: 分析慢查询

-- 启用慢查询日志
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: 添加必要的索引

-- 用户表索引
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: 验证索引效果

EXPLAIN ANALYZE SELECT * FROM sys_users WHERE username = 'testuser';

Step 4: 提交优化脚本

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 缓存

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<Object, Object> caffeineCacheBuilder() {
        return Caffeine.newBuilder()
                .initialCapacity(100)
                .maximumSize(500)
                .expireAfterWrite(30, TimeUnit.MINUTES)
                .recordStats();
    }
}

Step 2: 为 Service 添加缓存注解

@Cacheable(value = "users", key = "#id")
public Mono<SysUser> findById(Long id) {
    return userDao.findById(id)
            .map(userConverter::toDomain);
}

@CacheEvict(value = "users", key = "#user.id")
public Mono<SysUser> save(SysUser user) {
    return userDao.save(userConverter.toEntity(user))
            .map(userConverter::toDomain);
}

@CacheEvict(value = "users", key = "#id")
public Mono<Void> deleteById(Long id) {
    return userDao.deleteById(id);
}

Step 3: 测试缓存效果

# 第一次请求
curl http://localhost:8080/api/users/1

# 第二次请求(应该从缓存读取)
curl http://localhost:8080/api/users/1

Step 4: 提交缓存配置

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

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 配置

# 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: 提交监控配置

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: 创建依赖检查脚本

#!/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: 运行安全扫描

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: 提交安全扫描脚本

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: 创建架构文档

# 系统架构设计文档

## 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: 提交架构文档

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: 创建部署文档

# 部署指南

## 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 <repository-url>
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 前端部署

# 安装依赖
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

# 构建并启动所有服务
docker-compose up -d

# 查看日志
docker-compose logs -f

# 停止服务
docker-compose down

3.2 单独构建镜像

# 构建后端镜像
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 数据库配置

-- 创建数据库
CREATE DATABASE novalon;

-- 创建用户
CREATE USER novalon_user WITH PASSWORD 'secure_password';

-- 授权
GRANT ALL PRIVILEGES ON DATABASE novalon TO novalon_user;

4.2 后端部署

# 构建生产包
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 前端部署

# 构建生产包
cd novalon-manage-web
npm run build:prod

# 使用 Nginx 部署
cp -r dist/* /var/www/html/

4.4 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 健康检查

# 检查应用健康状态
curl http://localhost:8080/actuator/health

5.2 查看日志

# 查看应用日志
tail -f logs/application.log

# 查看错误日志
tail -f logs/error.log

5.3 Prometheus 指标

访问 http://localhost:8080/actuator/prometheus 查看 Prometheus 指标

6. 备份与恢复

6.1 数据库备份

# 备份数据库
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 文件备份

# 备份上传的文件
tar -czf uploads-backup.tar.gz /path/to/uploads

7. 故障排查

7.1 常见问题

问题: 数据库连接失败 解决: 检查数据库服务是否启动,连接配置是否正确

问题: API 请求超时 解决: 检查网络连接,查看应用日志

问题: 前端页面无法访问 解决: 检查 Nginx 配置,确保静态文件路径正确

7.2 日志分析

# 查看错误日志
grep ERROR logs/application.log

# 查看特定时间段日志
grep "2024-03-12 10:" logs/application.log

8. 升级指南

8.1 数据库迁移

# 运行新的数据库迁移
mvn flyway:migrate

8.2 应用升级

# 停止旧版本应用
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?