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

2150 lines
54 KiB
Markdown

# 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>` 部分添加:
```xml
<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: 验证配置**
```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
<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: 运行静态分析**
```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<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` 中更新用户路由:
```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: 测试路由**
```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
<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: 创建用户模态框组件**
```vue
<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**
```typescript
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: 测试前端页面**
```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<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 文档**
```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<Object, Object> caffeineCacheBuilder() {
return Caffeine.newBuilder()
.initialCapacity(100)
.maximumSize(500)
.expireAfterWrite(30, TimeUnit.MINUTES)
.recordStats();
}
}
```
**Step 2: 为 Service 添加缓存注解**
```java
@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: 测试缓存效果**
```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 <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 前端部署
```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?**