2150 lines
54 KiB
Markdown
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?**
|