2199 lines
65 KiB
Markdown
2199 lines
65 KiB
Markdown
# Infrastructure Refactoring Phase 2 Implementation Plan
|
||
|
||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||
|
||
**Goal:** 完成剩余 Handler 的函数式风格迁移、完善 Router 配置、优化其他 Converter、引入 MapStruct 和添加单元测试。
|
||
|
||
**Architecture:** 保持已建立的 DAO 层设计模式,将所有 Handler 从注解式迁移到函数式 WebFlux 风格,使用 MapStruct 自动生成转换代码,建立完整的单元测试覆盖。
|
||
|
||
**Tech Stack:** Spring WebFlux, R2DBC, MapStruct, JUnit 5, Mockito, Maven
|
||
|
||
---
|
||
|
||
## Phase 1: 完成其他 Handler 的函数式风格迁移
|
||
|
||
### Task 1: SysUserHandler 函数式迁移
|
||
|
||
**Files:**
|
||
- Modify: `handler/user/SysUserHandler.java`
|
||
|
||
**Step 1: 备份当前实现**
|
||
|
||
```bash
|
||
cp handler/user/SysUserHandler.java handler/user/SysUserHandler.java.bak
|
||
```
|
||
|
||
**Step 2: 修改为函数式风格**
|
||
|
||
```java
|
||
@Component
|
||
public class SysUserHandler {
|
||
private final ISysUserService userService;
|
||
|
||
public Mono<ServerResponse> getAllUsers(ServerRequest request) {
|
||
boolean includeDeleted = Boolean.valueOf(request.queryParam("includeDeleted").orElse("false"));
|
||
return ServerResponse.ok()
|
||
.body(userService.findAll(includeDeleted), SysUser.class);
|
||
}
|
||
|
||
public Mono<ServerResponse> getUsersByPage(ServerRequest request) {
|
||
int page = Integer.parseInt(request.queryParam("page").orElse("0"));
|
||
int size = Integer.parseInt(request.queryParam("size").orElse("10"));
|
||
String sort = request.queryParam("sort").orElse("id");
|
||
String order = request.queryParam("order").orElse("asc");
|
||
String keyword = request.queryParam("keyword").orElse(null);
|
||
|
||
PageRequest pageRequest = new PageRequest();
|
||
pageRequest.setPage(page);
|
||
pageRequest.setSize(size);
|
||
pageRequest.setSort(sort);
|
||
pageRequest.setOrder(order);
|
||
pageRequest.setKeyword(keyword);
|
||
|
||
return userService.findUsersByPage(pageRequest)
|
||
.flatMap(response -> ServerResponse.ok().bodyValue(response));
|
||
}
|
||
|
||
public Mono<ServerResponse> getUserById(ServerRequest request) {
|
||
Long id = Long.valueOf(request.pathVariable("id"));
|
||
return userService.findById(id)
|
||
.flatMap(user -> ServerResponse.ok().bodyValue(user))
|
||
.switchIfEmpty(ServerResponse.notFound().build());
|
||
}
|
||
|
||
public Mono<ServerResponse> getUserByUsername(ServerRequest request) {
|
||
String username = request.pathVariable("username");
|
||
return userService.findByUsername(username)
|
||
.flatMap(user -> ServerResponse.ok().bodyValue(user))
|
||
.switchIfEmpty(ServerResponse.notFound().build());
|
||
}
|
||
|
||
public Mono<ServerResponse> createUser(ServerRequest request) {
|
||
return request.bodyToMono(SysUser.class)
|
||
.flatMap(userService::createUser)
|
||
.flatMap(user -> ServerResponse.status(HttpStatus.CREATED).bodyValue(user));
|
||
}
|
||
|
||
public Mono<ServerResponse> updateUser(ServerRequest request) {
|
||
Long id = Long.valueOf(request.pathVariable("id"));
|
||
return request.bodyToMono(UserUpdateRequest.class)
|
||
.flatMap(req -> userService.findById(id)
|
||
.flatMap(existing -> {
|
||
if (req.getEmail() != null) existing.setEmail(req.getEmail());
|
||
if (req.getStatus() != null) existing.setStatus(req.getStatus());
|
||
if (req.getRoleId() != null) existing.setRoleId(req.getRoleId());
|
||
return userService.updateUser(existing);
|
||
}))
|
||
.flatMap(user -> ServerResponse.ok().bodyValue(user))
|
||
.switchIfEmpty(ServerResponse.notFound().build());
|
||
}
|
||
|
||
public Mono<ServerResponse> deleteUser(ServerRequest request) {
|
||
Long id = Long.valueOf(request.pathVariable("id"));
|
||
return userService.deleteUser(id)
|
||
.then(ServerResponse.noContent().build());
|
||
}
|
||
|
||
public Mono<ServerResponse> changePassword(ServerRequest request) {
|
||
Long id = Long.valueOf(request.pathVariable("id"));
|
||
return request.bodyToMono(PasswordChangeRequest.class)
|
||
.flatMap(req -> userService.changePassword(id, req.getOldPassword(), req.getNewPassword()))
|
||
.flatMap(user -> ServerResponse.ok().bodyValue(user));
|
||
}
|
||
|
||
public Mono<ServerResponse> logicalDeleteUser(ServerRequest request) {
|
||
Long id = Long.valueOf(request.pathVariable("id"));
|
||
return userService.logicalDeleteUser(id)
|
||
.then(ServerResponse.noContent().build());
|
||
}
|
||
|
||
public Mono<ServerResponse> logicalDeleteUsers(ServerRequest request) {
|
||
return request.bodyToMono(new ParameterizedTypeReference<List<Long>>() {})
|
||
.flatMap(ids -> userService.logicalDeleteUsers(ids))
|
||
.then(ServerResponse.noContent().build());
|
||
}
|
||
|
||
public Mono<ServerResponse> restoreUser(ServerRequest request) {
|
||
Long id = Long.valueOf(request.pathVariable("id"));
|
||
return userService.restoreUser(id)
|
||
.then(ServerResponse.noContent().build());
|
||
}
|
||
|
||
public Mono<ServerResponse> restoreUsers(ServerRequest request) {
|
||
return request.bodyToMono(new ParameterizedTypeReference<List<Long>>() {})
|
||
.flatMap(ids -> userService.restoreUsers(ids))
|
||
.then(ServerResponse.noContent().build());
|
||
}
|
||
|
||
public Mono<ServerResponse> checkUsernameExists(ServerRequest request) {
|
||
String username = request.queryParam("username").orElse(null);
|
||
return userService.existsByUsername(username)
|
||
.flatMap(exists -> ServerResponse.ok().bodyValue(exists));
|
||
}
|
||
|
||
public Mono<ServerResponse> checkEmailExists(ServerRequest request) {
|
||
String email = request.queryParam("email").orElse(null);
|
||
return userService.existsByEmail(email)
|
||
.flatMap(exists -> ServerResponse.ok().bodyValue(exists));
|
||
}
|
||
}
|
||
```
|
||
|
||
**Step 3: 测试路由配置**
|
||
|
||
```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"}'
|
||
```
|
||
|
||
**Step 4: 提交变更**
|
||
|
||
```bash
|
||
git add handler/user/SysUserHandler.java
|
||
git commit -m "refactor: migrate SysUserHandler to functional WebFlux style"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 2: SysRoleHandler 函数式迁移
|
||
|
||
**Files:**
|
||
- Modify: `handler/role/SysRoleHandler.java`
|
||
|
||
**Step 1: 修改为函数式风格**
|
||
|
||
```java
|
||
@Component
|
||
public class SysRoleHandler {
|
||
private final ISysRoleService roleService;
|
||
|
||
public Mono<ServerResponse> getAllRoles(ServerRequest request) {
|
||
return ServerResponse.ok()
|
||
.body(roleService.findAll(), SysRole.class);
|
||
}
|
||
|
||
public Mono<ServerResponse> getRoleById(ServerRequest request) {
|
||
Long id = Long.valueOf(request.pathVariable("id"));
|
||
return roleService.findById(id)
|
||
.flatMap(role -> ServerResponse.ok().bodyValue(role))
|
||
.switchIfEmpty(ServerResponse.notFound().build());
|
||
}
|
||
|
||
public Mono<ServerResponse> createRole(ServerRequest request) {
|
||
return request.bodyToMono(SysRole.class)
|
||
.flatMap(roleService::save)
|
||
.flatMap(role -> ServerResponse.status(HttpStatus.CREATED).bodyValue(role));
|
||
}
|
||
|
||
public Mono<ServerResponse> updateRole(ServerRequest request) {
|
||
Long id = Long.valueOf(request.pathVariable("id"));
|
||
return request.bodyToMono(SysRole.class)
|
||
.flatMap(role -> roleService.update(id, role))
|
||
.flatMap(updatedRole -> ServerResponse.ok().bodyValue(updatedRole))
|
||
.switchIfEmpty(ServerResponse.notFound().build());
|
||
}
|
||
|
||
public Mono<ServerResponse> deleteRole(ServerRequest request) {
|
||
Long id = Long.valueOf(request.pathVariable("id"));
|
||
return roleService.deleteById(id)
|
||
.then(ServerResponse.noContent().build());
|
||
}
|
||
}
|
||
```
|
||
|
||
**Step 2: 提交变更**
|
||
|
||
```bash
|
||
git add handler/role/SysRoleHandler.java
|
||
git commit -m "refactor: migrate SysRoleHandler to functional WebFlux style"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 3: SysConfigHandler 函数式迁移
|
||
|
||
**Files:**
|
||
- Modify: `handler/config/SysConfigHandler.java`
|
||
|
||
**Step 1: 修改为函数式风格**
|
||
|
||
```java
|
||
@Component
|
||
public class SysConfigHandler {
|
||
private final ISysConfigService configService;
|
||
|
||
public Mono<ServerResponse> getAllConfigs(ServerRequest request) {
|
||
return ServerResponse.ok()
|
||
.body(configService.findAll(), SysConfig.class);
|
||
}
|
||
|
||
public Mono<ServerResponse> getConfigByKey(ServerRequest request) {
|
||
String configKey = request.pathVariable("configKey");
|
||
return configService.findByConfigKey(configKey)
|
||
.flatMap(config -> ServerResponse.ok().bodyValue(config))
|
||
.switchIfEmpty(ServerResponse.notFound().build());
|
||
}
|
||
|
||
public Mono<ServerResponse> createConfig(ServerRequest request) {
|
||
return request.bodyToMono(SysConfig.class)
|
||
.flatMap(configService::save)
|
||
.flatMap(config -> ServerResponse.status(HttpStatus.CREATED).bodyValue(config));
|
||
}
|
||
|
||
public Mono<ServerResponse> updateConfig(ServerRequest request) {
|
||
Long id = Long.valueOf(request.pathVariable("id"));
|
||
return request.bodyToMono(SysConfig.class)
|
||
.flatMap(config -> configService.update(id, config))
|
||
.flatMap(updatedConfig -> ServerResponse.ok().bodyValue(updatedConfig))
|
||
.switchIfEmpty(ServerResponse.notFound().build());
|
||
}
|
||
|
||
public Mono<ServerResponse> deleteConfig(ServerRequest request) {
|
||
Long id = Long.valueOf(request.pathVariable("id"));
|
||
return configService.deleteById(id)
|
||
.then(ServerResponse.noContent().build());
|
||
}
|
||
}
|
||
```
|
||
|
||
**Step 2: 提交变更**
|
||
|
||
```bash
|
||
git add handler/config/SysConfigHandler.java
|
||
git commit -m "refactor: migrate SysConfigHandler to functional WebFlux style"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 4: SysNoticeHandler 函数式迁移
|
||
|
||
**Files:**
|
||
- Modify: `handler/notice/SysNoticeHandler.java`
|
||
|
||
**Step 1: 修改为函数式风格**
|
||
|
||
```java
|
||
@Component
|
||
public class SysNoticeHandler {
|
||
private final ISysNoticeService noticeService;
|
||
|
||
public Mono<ServerResponse> getAllNotices(ServerRequest request) {
|
||
return ServerResponse.ok()
|
||
.body(noticeService.findAll(), SysNotice.class);
|
||
}
|
||
|
||
public Mono<ServerResponse> getNoticeById(ServerRequest request) {
|
||
Long id = Long.valueOf(request.pathVariable("id"));
|
||
return noticeService.findById(id)
|
||
.flatMap(notice -> ServerResponse.ok().bodyValue(notice))
|
||
.switchIfEmpty(ServerResponse.notFound().build());
|
||
}
|
||
|
||
public Mono<ServerResponse> createNotice(ServerRequest request) {
|
||
return request.bodyToMono(SysNotice.class)
|
||
.flatMap(noticeService::save)
|
||
.flatMap(notice -> ServerResponse.status(HttpStatus.CREATED).bodyValue(notice));
|
||
}
|
||
|
||
public Mono<ServerResponse> updateNotice(ServerRequest request) {
|
||
Long id = Long.valueOf(request.pathVariable("id"));
|
||
return request.bodyToMono(SysNotice.class)
|
||
.flatMap(notice -> noticeService.update(id, notice))
|
||
.flatMap(updatedNotice -> ServerResponse.ok().bodyValue(updatedNotice))
|
||
.switchIfEmpty(ServerResponse.notFound().build());
|
||
}
|
||
|
||
public Mono<ServerResponse> deleteNotice(ServerRequest request) {
|
||
Long id = Long.valueOf(request.pathVariable("id"));
|
||
return noticeService.deleteById(id)
|
||
.then(ServerResponse.noContent().build());
|
||
}
|
||
}
|
||
```
|
||
|
||
**Step 2: 提交变更**
|
||
|
||
```bash
|
||
git add handler/notice/SysNoticeHandler.java
|
||
git commit -m "refactor: migrate SysNoticeHandler to functional WebFlux style"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 5: SysFileHandler 函数式迁移
|
||
|
||
**Files:**
|
||
- Modify: `handler/file/SysFileHandler.java`
|
||
|
||
**Step 1: 修改为函数式风格**
|
||
|
||
```java
|
||
@Component
|
||
public class SysFileHandler {
|
||
private final ISysFileService fileService;
|
||
|
||
public Mono<ServerResponse> getAllFiles(ServerRequest request) {
|
||
return ServerResponse.ok()
|
||
.body(fileService.findAll(), SysFile.class);
|
||
}
|
||
|
||
public Mono<ServerResponse> getFileById(ServerRequest request) {
|
||
Long id = Long.valueOf(request.pathVariable("id"));
|
||
return fileService.findById(id)
|
||
.flatMap(file -> ServerResponse.ok().bodyValue(file))
|
||
.switchIfEmpty(ServerResponse.notFound().build());
|
||
}
|
||
|
||
public Mono<ServerResponse> uploadFile(ServerRequest request) {
|
||
return request.multipartData()
|
||
.flatMap(data -> {
|
||
String filename = data.toSingleValueMap().getFirst("file").filename();
|
||
return fileService.saveFile(data)
|
||
.flatMap(file -> ServerResponse.status(HttpStatus.CREATED).bodyValue(file));
|
||
});
|
||
}
|
||
|
||
public Mono<ServerResponse> deleteFile(ServerRequest request) {
|
||
Long id = Long.valueOf(request.pathVariable("id"));
|
||
return fileService.deleteById(id)
|
||
.then(ServerResponse.noContent().build());
|
||
}
|
||
}
|
||
```
|
||
|
||
**Step 2: 提交变更**
|
||
|
||
```bash
|
||
git add handler/file/SysFileHandler.java
|
||
git commit -m "refactor: migrate SysFileHandler to functional WebFlux style"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 6: SysLogHandler 函数式迁移
|
||
|
||
**Files:**
|
||
- Modify: `handler/log/SysLogHandler.java`
|
||
|
||
**Step 1: 修改为函数式风格**
|
||
|
||
```java
|
||
@Component
|
||
public class SysLogHandler {
|
||
private final IOperationLogService operationLogService;
|
||
private final ISysLoginLogService loginLogService;
|
||
|
||
public Mono<ServerResponse> getOperationLogs(ServerRequest request) {
|
||
return ServerResponse.ok()
|
||
.body(operationLogService.findAll(), OperationLog.class);
|
||
}
|
||
|
||
public Mono<ServerResponse> getOperationLogsByPage(ServerRequest request) {
|
||
int page = Integer.parseInt(request.queryParam("page").orElse("0"));
|
||
int size = Integer.parseInt(request.queryParam("size").orElse("10"));
|
||
String sort = request.queryParam("sort").orElse("createdAt");
|
||
String order = request.queryParam("order").orElse("desc");
|
||
|
||
PageRequest pageRequest = new PageRequest();
|
||
pageRequest.setPage(page);
|
||
pageRequest.setSize(size);
|
||
pageRequest.setSort(sort);
|
||
pageRequest.setOrder(order);
|
||
|
||
return operationLogService.findLogsByPage(pageRequest)
|
||
.flatMap(response -> ServerResponse.ok().bodyValue(response));
|
||
}
|
||
|
||
public Mono<ServerResponse> getLoginLogs(ServerRequest request) {
|
||
return ServerResponse.ok()
|
||
.body(loginLogService.findAll(), SysLoginLog.class);
|
||
}
|
||
|
||
public Mono<ServerResponse> getLoginLogsByPage(ServerRequest request) {
|
||
int page = Integer.parseInt(request.queryParam("page").orElse("0"));
|
||
int size = Integer.parseInt(request.queryParam("size").orElse("10"));
|
||
String sort = request.queryParam("sort").orElse("createdAt");
|
||
String order = request.queryParam("order").orElse("desc");
|
||
|
||
PageRequest pageRequest = new PageRequest();
|
||
pageRequest.setPage(page);
|
||
pageRequest.setSize(size);
|
||
pageRequest.setSort(sort);
|
||
pageRequest.setOrder(order);
|
||
|
||
return loginLogService.findLogsByPage(pageRequest)
|
||
.flatMap(response -> ServerResponse.ok().bodyValue(response));
|
||
}
|
||
}
|
||
```
|
||
|
||
**Step 2: 提交变更**
|
||
|
||
```bash
|
||
git add handler/log/SysLogHandler.java
|
||
git commit -m "refactor: migrate SysLogHandler to functional WebFlux style"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 7: SysAuthHandler 函数式迁移
|
||
|
||
**Files:**
|
||
- Modify: `handler/auth/SysAuthHandler.java`
|
||
|
||
**Step 1: 修改为函数式风格**
|
||
|
||
```java
|
||
@Component
|
||
public class SysAuthHandler {
|
||
private final ISysUserService userService;
|
||
private final JwtTokenProvider tokenProvider;
|
||
|
||
public Mono<ServerResponse> login(ServerRequest request) {
|
||
return request.bodyToMono(LoginRequest.class)
|
||
.flatMap(loginRequest -> userService.authenticate(loginRequest.getUsername(), loginRequest.getPassword()))
|
||
.flatMap(user -> {
|
||
String token = tokenProvider.generateToken(user);
|
||
AuthResponse response = new AuthResponse(token, user);
|
||
return ServerResponse.ok().bodyValue(response);
|
||
});
|
||
}
|
||
|
||
public Mono<ServerResponse> register(ServerRequest request) {
|
||
return request.bodyToMono(UserRegisterRequest.class)
|
||
.flatMap(userService::registerUser)
|
||
.flatMap(user -> ServerResponse.status(HttpStatus.CREATED).bodyValue(user));
|
||
}
|
||
|
||
public Mono<ServerResponse> refreshToken(ServerRequest request) {
|
||
return request.bodyToMono(RefreshTokenRequest.class)
|
||
.flatMap(refreshRequest -> tokenProvider.validateToken(refreshRequest.getToken())
|
||
.flatMap(username -> userService.findByUsername(username))
|
||
.flatMap(user -> {
|
||
String newToken = tokenProvider.generateToken(user);
|
||
AuthResponse response = new AuthResponse(newToken, user);
|
||
return ServerResponse.ok().bodyValue(response);
|
||
}));
|
||
}
|
||
}
|
||
```
|
||
|
||
**Step 2: 提交变更**
|
||
|
||
```bash
|
||
git add handler/auth/SysAuthHandler.java
|
||
git commit -m "refactor: migrate SysAuthHandler to functional WebFlux style"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 8: SysUserMessageHandler 函数式迁移
|
||
|
||
**Files:**
|
||
- Modify: `handler/message/SysUserMessageHandler.java`
|
||
|
||
**Step 1: 修改为函数式风格**
|
||
|
||
```java
|
||
@Component
|
||
public class SysUserMessageHandler {
|
||
private final ISysUserMessageService messageService;
|
||
|
||
public Mono<ServerResponse> getAllMessages(ServerRequest request) {
|
||
return ServerResponse.ok()
|
||
.body(messageService.findAll(), SysUserMessage.class);
|
||
}
|
||
|
||
public Mono<ServerResponse> getMessageById(ServerRequest request) {
|
||
Long id = Long.valueOf(request.pathVariable("id"));
|
||
return messageService.findById(id)
|
||
.flatMap(message -> ServerResponse.ok().bodyValue(message))
|
||
.switchIfEmpty(ServerResponse.notFound().build());
|
||
}
|
||
|
||
public Mono<ServerResponse> createMessage(ServerRequest request) {
|
||
return request.bodyToMono(SysUserMessage.class)
|
||
.flatMap(messageService::save)
|
||
.flatMap(message -> ServerResponse.status(HttpStatus.CREATED).bodyValue(message));
|
||
}
|
||
|
||
public Mono<ServerResponse> markAsRead(ServerRequest request) {
|
||
Long id = Long.valueOf(request.pathVariable("id"));
|
||
return messageService.markAsRead(id)
|
||
.then(ServerResponse.ok().build());
|
||
}
|
||
|
||
public Mono<ServerResponse> deleteMessage(ServerRequest request) {
|
||
Long id = Long.valueOf(request.pathVariable("id"));
|
||
return messageService.deleteById(id)
|
||
.then(ServerResponse.noContent().build());
|
||
}
|
||
}
|
||
```
|
||
|
||
**Step 2: 提交变更**
|
||
|
||
```bash
|
||
git add handler/message/SysUserMessageHandler.java
|
||
git commit -m "refactor: migrate SysUserMessageHandler to functional WebFlux style"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 9: StatsHandler 函数式迁移
|
||
|
||
**Files:**
|
||
- Modify: `handler/stats/StatsHandler.java`
|
||
|
||
**Step 1: 修改为函数式风格**
|
||
|
||
```java
|
||
@Component
|
||
public class StatsHandler {
|
||
private final ISysUserService userService;
|
||
private final IOperationLogService operationLogService;
|
||
|
||
public Mono<ServerResponse> getUserStats(ServerRequest request) {
|
||
return userService.count()
|
||
.flatMap(count -> {
|
||
Map<String, Object> stats = new HashMap<>();
|
||
stats.put("totalUsers", count);
|
||
return ServerResponse.ok().bodyValue(stats);
|
||
});
|
||
}
|
||
|
||
public Mono<ServerResponse> getOperationStats(ServerRequest request) {
|
||
return operationLogService.count()
|
||
.flatMap(count -> {
|
||
Map<String, Object> stats = new HashMap<>();
|
||
stats.put("totalOperations", count);
|
||
return ServerResponse.ok().bodyValue(stats);
|
||
});
|
||
}
|
||
|
||
public Mono<ServerResponse> getSystemStats(ServerRequest request) {
|
||
return Mono.zip(
|
||
userService.count(),
|
||
operationLogService.count()
|
||
).map(tuple -> {
|
||
Map<String, Object> stats = new HashMap<>();
|
||
stats.put("totalUsers", tuple.getT1());
|
||
stats.put("totalOperations", tuple.getT2());
|
||
return stats;
|
||
}).flatMap(stats -> ServerResponse.ok().bodyValue(stats));
|
||
}
|
||
}
|
||
```
|
||
|
||
**Step 2: 提交变更**
|
||
|
||
```bash
|
||
git add handler/stats/StatsHandler.java
|
||
git commit -m "refactor: migrate StatsHandler to functional WebFlux style"
|
||
```
|
||
|
||
---
|
||
|
||
## Phase 2: 完善 Router 配置
|
||
|
||
### Task 10: 扩展 SystemRouter 配置
|
||
|
||
**Files:**
|
||
- Modify: `config/SystemRouter.java`
|
||
|
||
**Step 1: 添加所有模块的路由配置**
|
||
|
||
```java
|
||
package cn.novalon.manage.sys.config;
|
||
|
||
import cn.novalon.manage.sys.handler.auth.SysAuthHandler;
|
||
import cn.novalon.manage.sys.handler.config.SysConfigHandler;
|
||
import cn.novalon.manage.sys.handler.dictionary.DictionaryHandler;
|
||
import cn.novalon.manage.sys.handler.file.SysFileHandler;
|
||
import cn.novalon.manage.sys.handler.log.SysLogHandler;
|
||
import cn.novalon.manage.sys.handler.message.SysUserMessageHandler;
|
||
import cn.novalon.manage.sys.handler.notice.SysNoticeHandler;
|
||
import cn.novalon.manage.sys.handler.role.SysRoleHandler;
|
||
import cn.novalon.manage.sys.handler.stats.StatsHandler;
|
||
import cn.novalon.manage.sys.handler.user.SysUserHandler;
|
||
import org.springframework.context.annotation.Bean;
|
||
import org.springframework.context.annotation.Configuration;
|
||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||
|
||
import static org.springframework.web.reactive.function.server.RequestPredicates.*;
|
||
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
|
||
|
||
@Configuration
|
||
public class SystemRouter {
|
||
|
||
@Bean
|
||
public RouterFunction<ServerResponse> systemRoutes(
|
||
DictionaryHandler dictionaryHandler,
|
||
SysUserHandler userHandler,
|
||
SysRoleHandler roleHandler,
|
||
SysConfigHandler configHandler,
|
||
SysNoticeHandler noticeHandler,
|
||
SysFileHandler fileHandler,
|
||
SysLogHandler logHandler,
|
||
SysAuthHandler authHandler,
|
||
SysUserMessageHandler messageHandler,
|
||
StatsHandler statsHandler) {
|
||
|
||
return route()
|
||
.path("/api", builder -> builder
|
||
.path("/dictionaries", dictRoutes(dictionaryHandler))
|
||
.path("/users", userRoutes(userHandler))
|
||
.path("/roles", roleRoutes(roleHandler))
|
||
.path("/configs", configRoutes(configHandler))
|
||
.path("/notices", noticeRoutes(noticeHandler))
|
||
.path("/files", fileRoutes(fileHandler))
|
||
.path("/logs", logRoutes(logHandler))
|
||
.path("/auth", authRoutes(authHandler))
|
||
.path("/messages", messageRoutes(messageHandler))
|
||
.path("/stats", statsRoutes(statsHandler))
|
||
.build())
|
||
.build();
|
||
}
|
||
|
||
private RouterFunction<ServerResponse> dictRoutes(DictionaryHandler handler) {
|
||
return route()
|
||
.GET(handler::getAllDictionaries)
|
||
.GET("/{id}", handler::getDictionaryById)
|
||
.GET("/type/{type}", handler::getDictionariesByType)
|
||
.GET("/check/exists", handler::checkTypeAndCodeExists)
|
||
.POST(handler::createDictionary)
|
||
.PUT("/{id}", handler::updateDictionary)
|
||
.DELETE("/{id}", handler::deleteDictionary)
|
||
.build();
|
||
}
|
||
|
||
private RouterFunction<ServerResponse> userRoutes(SysUserHandler handler) {
|
||
return route()
|
||
.GET(handler::getAllUsers)
|
||
.GET("/page", handler::getUsersByPage)
|
||
.GET("/{id}", handler::getUserById)
|
||
.GET("/username/{username}", handler::getUserByUsername)
|
||
.POST(handler::createUser)
|
||
.PUT("/{id}", handler::updateUser)
|
||
.DELETE("/{id}", handler::deleteUser)
|
||
.POST("/{id}/password", handler::changePassword)
|
||
.DELETE("/{id}/logical", handler::logicalDeleteUser)
|
||
.POST("/logical-delete", handler::logicalDeleteUsers)
|
||
.POST("/{id}/restore", handler::restoreUser)
|
||
.POST("/restore", handler::restoreUsers)
|
||
.GET("/check/username", handler::checkUsernameExists)
|
||
.GET("/check/email", handler::checkEmailExists)
|
||
.build();
|
||
}
|
||
|
||
private RouterFunction<ServerResponse> roleRoutes(SysRoleHandler handler) {
|
||
return route()
|
||
.GET(handler::getAllRoles)
|
||
.GET("/{id}", handler::getRoleById)
|
||
.POST(handler::createRole)
|
||
.PUT("/{id}", handler::updateRole)
|
||
.DELETE("/{id}", handler::deleteRole)
|
||
.build();
|
||
}
|
||
|
||
private RouterFunction<ServerResponse> configRoutes(SysConfigHandler handler) {
|
||
return route()
|
||
.GET(handler::getAllConfigs)
|
||
.GET("/key/{configKey}", handler::getConfigByKey)
|
||
.POST(handler::createConfig)
|
||
.PUT("/{id}", handler::updateConfig)
|
||
.DELETE("/{id}", handler::deleteConfig)
|
||
.build();
|
||
}
|
||
|
||
private RouterFunction<ServerResponse> noticeRoutes(SysNoticeHandler handler) {
|
||
return route()
|
||
.GET(handler::getAllNotices)
|
||
.GET("/{id}", handler::getNoticeById)
|
||
.POST(handler::createNotice)
|
||
.PUT("/{id}", handler::updateNotice)
|
||
.DELETE("/{id}", handler::deleteNotice)
|
||
.build();
|
||
}
|
||
|
||
private RouterFunction<ServerResponse> fileRoutes(SysFileHandler handler) {
|
||
return route()
|
||
.GET(handler::getAllFiles)
|
||
.GET("/{id}", handler::getFileById)
|
||
.POST("/upload", handler::uploadFile)
|
||
.DELETE("/{id}", handler::deleteFile)
|
||
.build();
|
||
}
|
||
|
||
private RouterFunction<ServerResponse> logRoutes(SysLogHandler handler) {
|
||
return route()
|
||
.GET("/operations", handler::getOperationLogs)
|
||
.GET("/operations/page", handler::getOperationLogsByPage)
|
||
.GET("/logins", handler::getLoginLogs)
|
||
.GET("/logins/page", handler::getLoginLogsByPage)
|
||
.build();
|
||
}
|
||
|
||
private RouterFunction<ServerResponse> authRoutes(SysAuthHandler handler) {
|
||
return route()
|
||
.POST("/login", handler::login)
|
||
.POST("/register", handler::register)
|
||
.POST("/refresh", handler::refreshToken)
|
||
.build();
|
||
}
|
||
|
||
private RouterFunction<ServerResponse> messageRoutes(SysUserMessageHandler handler) {
|
||
return route()
|
||
.GET(handler::getAllMessages)
|
||
.GET("/{id}", handler::getMessageById)
|
||
.POST(handler::createMessage)
|
||
.PUT("/{id}/read", handler::markAsRead)
|
||
.DELETE("/{id}", handler::deleteMessage)
|
||
.build();
|
||
}
|
||
|
||
private RouterFunction<ServerResponse> statsRoutes(StatsHandler handler) {
|
||
return route()
|
||
.GET("/users", handler::getUserStats)
|
||
.GET("/operations", handler::getOperationStats)
|
||
.GET("/system", handler::getSystemStats)
|
||
.build();
|
||
}
|
||
}
|
||
```
|
||
|
||
**Step 2: 测试路由配置**
|
||
|
||
```bash
|
||
curl -X GET http://localhost:8080/api/dictionaries
|
||
curl -X GET http://localhost:8080/api/users
|
||
curl -X GET http://localhost:8080/api/roles
|
||
curl -X GET http://localhost:8080/api/stats/system
|
||
```
|
||
|
||
**Step 3: 提交变更**
|
||
|
||
```bash
|
||
git add config/SystemRouter.java
|
||
git commit -m "feat: add comprehensive router configuration for all modules"
|
||
```
|
||
|
||
---
|
||
|
||
## Phase 3: 优化其他 Converter
|
||
|
||
### Task 11: 为其他 Converter 添加批量转换方法
|
||
|
||
**Files:**
|
||
- Modify: `converter/SysRoleConverter.java`
|
||
- Modify: `converter/SysConfigConverter.java`
|
||
- Modify: `converter/SysNoticeConverter.java`
|
||
- Modify: `converter/SysFileConverter.java`
|
||
- Modify: `converter/SysLoginLogConverter.java`
|
||
- Modify: `converter/SysDictDataConverter.java`
|
||
- Modify: `converter/SysDictTypeConverter.java`
|
||
- Modify: `converter/SysMenuConverter.java`
|
||
- Modify: `converter/SysUserMessageConverter.java`
|
||
- Modify: `converter/SysExceptionLogConverter.java`
|
||
|
||
**Step 1: 为 SysRoleConverter 添加批量转换**
|
||
|
||
```java
|
||
public List<SysRole> toDomainList(List<SysRoleEntity> entities) {
|
||
if (entities == null) {
|
||
return null;
|
||
}
|
||
return entities.stream()
|
||
.map(this::toDomain)
|
||
.collect(Collectors.toList());
|
||
}
|
||
|
||
public List<SysRoleEntity> toEntityList(List<SysRole> domains) {
|
||
if (domains == null) {
|
||
return null;
|
||
}
|
||
return domains.stream()
|
||
.map(this::toEntity)
|
||
.collect(Collectors.toList());
|
||
}
|
||
```
|
||
|
||
**Step 2: 为其他 Converter 添加批量转换方法**
|
||
|
||
重复 Step 1 的模式,为所有其他 Converter 添加 `toDomainList()` 和 `toEntityList()` 方法。
|
||
|
||
**Step 3: 提交变更**
|
||
|
||
```bash
|
||
git add converter/
|
||
git commit -m "feat: add batch conversion methods to all converters"
|
||
```
|
||
|
||
---
|
||
|
||
## Phase 4: 引入 MapStruct
|
||
|
||
### Task 12: 配置 MapStruct 依赖
|
||
|
||
**Files:**
|
||
- Modify: `pom.xml`
|
||
|
||
**Step 1: 添加 MapStruct 依赖**
|
||
|
||
```xml
|
||
<properties>
|
||
<org.mapstruct.version>1.5.5.Final</org.mapstruct.version>
|
||
<lombok-mapstruct-binding.version>0.2.0</lombok-mapstruct-binding.version>
|
||
</properties>
|
||
|
||
<dependencies>
|
||
<dependency>
|
||
<groupId>org.mapstruct</groupId>
|
||
<artifactId>mapstruct</artifactId>
|
||
<version>${org.mapstruct.version}</version>
|
||
</dependency>
|
||
<dependency>
|
||
<groupId>org.projectlombok</groupId>
|
||
<artifactId>lombok-mapstruct-binding</artifactId>
|
||
<version>${lombok-mapstruct-binding.version}</version>
|
||
<scope>provided</scope>
|
||
</dependency>
|
||
</dependencies>
|
||
|
||
<build>
|
||
<plugins>
|
||
<plugin>
|
||
<groupId>org.apache.maven.plugins</groupId>
|
||
<artifactId>maven-compiler-plugin</artifactId>
|
||
<version>3.11.0</version>
|
||
<configuration>
|
||
<annotationProcessorPaths>
|
||
<path>
|
||
<groupId>org.mapstruct</groupId>
|
||
<artifactId>mapstruct-processor</artifactId>
|
||
<version>${org.mapstruct.version}</version>
|
||
</path>
|
||
<path>
|
||
<groupId>org.projectlombok</groupId>
|
||
<artifactId>lombok</artifactId>
|
||
<version>${lombok.version}</version>
|
||
</path>
|
||
<path>
|
||
<groupId>org.projectlombok</groupId>
|
||
<artifactId>lombok-mapstruct-binding</artifactId>
|
||
<version>${lombok-mapstruct-binding.version}</version>
|
||
</path>
|
||
</annotationProcessorPaths>
|
||
</configuration>
|
||
</plugin>
|
||
</plugins>
|
||
</build>
|
||
```
|
||
|
||
**Step 2: 提交变更**
|
||
|
||
```bash
|
||
git add pom.xml
|
||
git commit -m "feat: add MapStruct dependency and configuration"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 13: 创建 MapStruct Mapper 接口
|
||
|
||
**Files:**
|
||
- Create: `infrastructure/db/mapper/DictionaryMapper.java`
|
||
- Create: `infrastructure/db/mapper/SysUserMapper.java`
|
||
- Create: `infrastructure/db/mapper/SysRoleMapper.java`
|
||
- Create: `infrastructure/db/mapper/SysConfigMapper.java`
|
||
- Create: `infrastructure/db/mapper/SysNoticeMapper.java`
|
||
- Create: `infrastructure/db/mapper/SysFileMapper.java`
|
||
- Create: `infrastructure/db/mapper/OperationLogMapper.java`
|
||
- Create: `infrastructure/db/mapper/SysLoginLogMapper.java`
|
||
- Create: `infrastructure/db/mapper/SysDictDataMapper.java`
|
||
- Create: `infrastructure/db/mapper/SysDictTypeMapper.java`
|
||
- Create: `infrastructure/db/mapper/SysMenuMapper.java`
|
||
- Create: `infrastructure/db/mapper/SysUserMessageMapper.java`
|
||
- Create: `infrastructure/db/mapper/SysExceptionLogMapper.java`
|
||
|
||
**Step 1: 创建 DictionaryMapper 接口**
|
||
|
||
```java
|
||
package cn.novalon.manage.sys.infrastructure.db.mapper;
|
||
|
||
import cn.novalon.manage.sys.core.domain.Dictionary;
|
||
import cn.novalon.manage.sys.infrastructure.db.entity.DictionaryEntity;
|
||
import org.mapstruct.Mapper;
|
||
import org.mapstruct.Mapping;
|
||
import org.mapstruct.Named;
|
||
|
||
import java.util.List;
|
||
|
||
@Mapper(componentModel = "spring")
|
||
public interface DictionaryMapper {
|
||
|
||
Dictionary toDomain(DictionaryEntity entity);
|
||
|
||
DictionaryEntity toEntity(Dictionary domain);
|
||
|
||
List<Dictionary> toDomainList(List<DictionaryEntity> entities);
|
||
|
||
List<DictionaryEntity> toEntityList(List<Dictionary> domains);
|
||
}
|
||
```
|
||
|
||
**Step 2: 创建其他 Mapper 接口**
|
||
|
||
重复 Step 1 的模式,为所有实体创建对应的 Mapper 接口。
|
||
|
||
**Step 3: 提交变更**
|
||
|
||
```bash
|
||
git add infrastructure/db/mapper/
|
||
git commit -m "feat: add MapStruct mapper interfaces for all entities"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 14: 使用 MapStruct 替换手动 Converter
|
||
|
||
**Files:**
|
||
- Modify: `repository/DictionaryRepository.java`
|
||
- Modify: `repository/SysUserRepository.java`
|
||
- Modify: `repository/SysRoleRepository.java`
|
||
- Modify: `repository/SysConfigRepository.java`
|
||
- Modify: `repository/SysNoticeRepository.java`
|
||
- Modify: `repository/SysFileRepository.java`
|
||
- Modify: `repository/OperationLogRepository.java`
|
||
- Modify: `repository/SysLoginLogRepository.java`
|
||
- Modify: `repository/SysDictDataRepository.java`
|
||
- Modify: `repository/SysDictTypeRepository.java`
|
||
- Modify: `repository/SysMenuRepository.java`
|
||
- Modify: `repository/SysUserMessageRepository.java`
|
||
- Modify: `repository/SysExceptionLogRepository.java`
|
||
|
||
**Step 1: 修改 DictionaryRepository 使用 MapStruct**
|
||
|
||
```java
|
||
@Repository
|
||
public class DictionaryRepository {
|
||
|
||
private final DictionaryDao dao;
|
||
private final DictionaryMapper mapper;
|
||
|
||
public DictionaryRepository(DictionaryDao dao, DictionaryMapper mapper) {
|
||
this.dao = dao;
|
||
this.mapper = mapper;
|
||
}
|
||
|
||
public Flux<Dictionary> findByType(String type) {
|
||
return dao.findByType(type)
|
||
.map(mapper::toDomain);
|
||
}
|
||
|
||
public Mono<Dictionary> findByTypeAndCode(String type, String code) {
|
||
return dao.findByTypeAndCode(type, code)
|
||
.map(mapper::toDomain);
|
||
}
|
||
|
||
public Flux<Dictionary> findAll() {
|
||
return dao.findByDeletedAtIsNullOrderBySortAsc()
|
||
.map(mapper::toDomain);
|
||
}
|
||
|
||
public Mono<Dictionary> findById(Long id) {
|
||
return dao.findById(id)
|
||
.map(mapper::toDomain);
|
||
}
|
||
|
||
public Mono<Dictionary> save(Dictionary dictionary) {
|
||
return dao.save(mapper.toEntity(dictionary))
|
||
.map(mapper::toDomain);
|
||
}
|
||
|
||
public Mono<Void> deleteById(Long id) {
|
||
return dao.deleteByIdAndDeletedAtIsNull(id);
|
||
}
|
||
}
|
||
```
|
||
|
||
**Step 2: 修改其他 Repository 使用 MapStruct**
|
||
|
||
重复 Step 1 的模式,为所有 Repository 替换 Converter 为 Mapper。
|
||
|
||
**Step 3: 删除旧的 Converter 类**
|
||
|
||
```bash
|
||
rm converter/DictionaryConverter.java
|
||
rm converter/SysUserConverter.java
|
||
rm converter/SysRoleConverter.java
|
||
rm converter/SysConfigConverter.java
|
||
rm converter/SysNoticeConverter.java
|
||
rm converter/SysFileConverter.java
|
||
rm converter/OperationLogConverter.java
|
||
rm converter/SysLoginLogConverter.java
|
||
rm converter/SysDictDataConverter.java
|
||
rm converter/SysDictTypeConverter.java
|
||
rm converter/SysMenuConverter.java
|
||
rm converter/SysUserMessageConverter.java
|
||
rm converter/SysExceptionLogConverter.java
|
||
```
|
||
|
||
**Step 4: 提交变更**
|
||
|
||
```bash
|
||
git add repository/ converter/
|
||
git commit -m "refactor: replace manual converters with MapStruct mappers"
|
||
```
|
||
|
||
---
|
||
|
||
## Phase 5: 添加单元测试
|
||
|
||
### Task 15: 为 DictionaryRepository 添加单元测试
|
||
|
||
**Files:**
|
||
- Create: `infrastructure/db/repository/DictionaryRepositoryTest.java`
|
||
|
||
**Step 1: 创建测试类**
|
||
|
||
```java
|
||
package cn.novalon.manage.sys.infrastructure.db.repository;
|
||
|
||
import cn.novalon.manage.sys.core.domain.Dictionary;
|
||
import cn.novalon.manage.sys.infrastructure.db.converter.DictionaryConverter;
|
||
import cn.novalon.manage.sys.infrastructure.db.dao.DictionaryDao;
|
||
import cn.novalon.manage.sys.infrastructure.db.entity.DictionaryEntity;
|
||
import org.junit.jupiter.api.BeforeEach;
|
||
import org.junit.jupiter.api.Test;
|
||
import org.junit.jupiter.api.extension.ExtendWith;
|
||
import org.mockito.Mock;
|
||
import org.mockito.junit.jupiter.MockitoExtension;
|
||
import reactor.core.publisher.Flux;
|
||
import reactor.core.publisher.Mono;
|
||
import reactor.test.StepVerifier;
|
||
|
||
import java.time.LocalDateTime;
|
||
import java.util.Arrays;
|
||
import java.util.List;
|
||
|
||
import static org.mockito.ArgumentMatchers.any;
|
||
import static org.mockito.Mockito.when;
|
||
|
||
@ExtendWith(MockitoExtension.class)
|
||
class DictionaryRepositoryTest {
|
||
|
||
@Mock
|
||
private DictionaryDao dao;
|
||
|
||
@Mock
|
||
private DictionaryConverter converter;
|
||
|
||
private DictionaryRepository repository;
|
||
|
||
private DictionaryEntity testEntity;
|
||
private Dictionary testDomain;
|
||
|
||
@BeforeEach
|
||
void setUp() {
|
||
repository = new DictionaryRepository(dao, converter);
|
||
|
||
testEntity = new DictionaryEntity();
|
||
testEntity.setId(1L);
|
||
testEntity.setType("test_type");
|
||
testEntity.setCode("test_code");
|
||
testEntity.setName("Test Dictionary");
|
||
testEntity.setValue("test_value");
|
||
testEntity.setSort(1);
|
||
testEntity.setCreatedAt(LocalDateTime.now());
|
||
testEntity.setUpdatedAt(LocalDateTime.now());
|
||
|
||
testDomain = new Dictionary();
|
||
testDomain.setId(1L);
|
||
testDomain.setType("test_type");
|
||
testDomain.setCode("test_code");
|
||
testDomain.setName("Test Dictionary");
|
||
testDomain.setValue("test_value");
|
||
testDomain.setSort(1);
|
||
testDomain.setCreatedAt(LocalDateTime.now());
|
||
testDomain.setUpdatedAt(LocalDateTime.now());
|
||
}
|
||
|
||
@Test
|
||
void findByType_ShouldReturnDictionaries() {
|
||
when(dao.findByType("test_type")).thenReturn(Flux.just(testEntity));
|
||
when(converter.toDomain(testEntity)).thenReturn(testDomain);
|
||
|
||
Flux<Dictionary> result = repository.findByType("test_type");
|
||
|
||
StepVerifier.create(result)
|
||
.expectNext(testDomain)
|
||
.verifyComplete();
|
||
}
|
||
|
||
@Test
|
||
void findByTypeAndCode_ShouldReturnDictionary() {
|
||
when(dao.findByTypeAndCode("test_type", "test_code")).thenReturn(Mono.just(testEntity));
|
||
when(converter.toDomain(testEntity)).thenReturn(testDomain);
|
||
|
||
Mono<Dictionary> result = repository.findByTypeAndCode("test_type", "test_code");
|
||
|
||
StepVerifier.create(result)
|
||
.expectNext(testDomain)
|
||
.verifyComplete();
|
||
}
|
||
|
||
@Test
|
||
void findAll_ShouldReturnAllDictionaries() {
|
||
List<DictionaryEntity> entities = Arrays.asList(testEntity);
|
||
when(dao.findByDeletedAtIsNullOrderBySortAsc()).thenReturn(Flux.fromIterable(entities));
|
||
when(converter.toDomain(testEntity)).thenReturn(testDomain);
|
||
|
||
Flux<Dictionary> result = repository.findAll();
|
||
|
||
StepVerifier.create(result)
|
||
.expectNext(testDomain)
|
||
.verifyComplete();
|
||
}
|
||
|
||
@Test
|
||
void findById_ShouldReturnDictionary() {
|
||
when(dao.findById(1L)).thenReturn(Mono.just(testEntity));
|
||
when(converter.toDomain(testEntity)).thenReturn(testDomain);
|
||
|
||
Mono<Dictionary> result = repository.findById(1L);
|
||
|
||
StepVerifier.create(result)
|
||
.expectNext(testDomain)
|
||
.verifyComplete();
|
||
}
|
||
|
||
@Test
|
||
void save_ShouldSaveDictionary() {
|
||
when(converter.toEntity(testDomain)).thenReturn(testEntity);
|
||
when(dao.save(testEntity)).thenReturn(Mono.just(testEntity));
|
||
when(converter.toDomain(testEntity)).thenReturn(testDomain);
|
||
|
||
Mono<Dictionary> result = repository.save(testDomain);
|
||
|
||
StepVerifier.create(result)
|
||
.expectNext(testDomain)
|
||
.verifyComplete();
|
||
}
|
||
|
||
@Test
|
||
void deleteById_ShouldDeleteDictionary() {
|
||
when(dao.deleteByIdAndDeletedAtIsNull(1L)).thenReturn(Mono.empty());
|
||
|
||
Mono<Void> result = repository.deleteById(1L);
|
||
|
||
StepVerifier.create(result)
|
||
.verifyComplete();
|
||
}
|
||
}
|
||
```
|
||
|
||
**Step 2: 运行测试**
|
||
|
||
```bash
|
||
mvn test -Dtest=DictionaryRepositoryTest
|
||
```
|
||
|
||
**Step 3: 提交变更**
|
||
|
||
```bash
|
||
git add infrastructure/db/repository/DictionaryRepositoryTest.java
|
||
git commit -m "test: add unit tests for DictionaryRepository"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 16: 为其他 Repository 添加单元测试
|
||
|
||
**Files:**
|
||
- Create: `infrastructure/db/repository/SysUserRepositoryTest.java`
|
||
- Create: `infrastructure/db/repository/SysRoleRepositoryTest.java`
|
||
- Create: `infrastructure/db/repository/SysConfigRepositoryTest.java`
|
||
- Create: `infrastructure/db/repository/OperationLogRepositoryTest.java`
|
||
|
||
**Step 1: 创建测试类**
|
||
|
||
重复 Task 15 的模式,为其他 Repository 创建单元测试类。
|
||
|
||
**Step 2: 运行所有测试**
|
||
|
||
```bash
|
||
mvn test
|
||
```
|
||
|
||
**Step 3: 提交变更**
|
||
|
||
```bash
|
||
git add infrastructure/db/repository/
|
||
git commit -m "test: add unit tests for all repositories"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 17: 为 Service 层添加单元测试
|
||
|
||
**Files:**
|
||
- Create: `core/service/impl/DictionaryServiceTest.java`
|
||
- Create: `core/service/impl/SysUserServiceTest.java`
|
||
- Create: `core/service/impl/SysRoleServiceTest.java`
|
||
|
||
**Step 1: 创建 DictionaryServiceTest**
|
||
|
||
```java
|
||
package cn.novalon.manage.sys.core.service.impl;
|
||
|
||
import cn.novalon.manage.sys.core.domain.Dictionary;
|
||
import cn.novalon.manage.sys.core.exception.DictionaryAlreadyExistsException;
|
||
import cn.novalon.manage.sys.infrastructure.db.converter.DictionaryConverter;
|
||
import cn.novalon.manage.sys.infrastructure.db.dao.DictionaryDao;
|
||
import cn.novalon.manage.sys.infrastructure.db.entity.DictionaryEntity;
|
||
import org.junit.jupiter.api.BeforeEach;
|
||
import org.junit.jupiter.api.Test;
|
||
import org.junit.jupiter.api.extension.ExtendWith;
|
||
import org.mockito.Mock;
|
||
import org.mockito.junit.jupiter.MockitoExtension;
|
||
import reactor.core.publisher.Flux;
|
||
import reactor.core.publisher.Mono;
|
||
import reactor.test.StepVerifier;
|
||
|
||
import java.time.LocalDateTime;
|
||
import java.util.Arrays;
|
||
import java.util.List;
|
||
|
||
import static org.mockito.ArgumentMatchers.any;
|
||
import static org.mockito.Mockito.when;
|
||
|
||
@ExtendWith(MockitoExtension.class)
|
||
class DictionaryServiceTest {
|
||
|
||
@Mock
|
||
private DictionaryDao dao;
|
||
|
||
@Mock
|
||
private DictionaryConverter converter;
|
||
|
||
private DictionaryService service;
|
||
|
||
private Dictionary testDictionary;
|
||
private DictionaryEntity testEntity;
|
||
|
||
@BeforeEach
|
||
void setUp() {
|
||
service = new DictionaryService(dao, converter);
|
||
|
||
testDictionary = new Dictionary();
|
||
testDictionary.setId(1L);
|
||
testDictionary.setType("test_type");
|
||
testDictionary.setCode("test_code");
|
||
testDictionary.setName("Test Dictionary");
|
||
testDictionary.setValue("test_value");
|
||
testDictionary.setSort(1);
|
||
|
||
testEntity = new DictionaryEntity();
|
||
testEntity.setId(1L);
|
||
testEntity.setType("test_type");
|
||
testEntity.setCode("test_code");
|
||
testEntity.setName("Test Dictionary");
|
||
testEntity.setValue("test_value");
|
||
testEntity.setSort(1);
|
||
}
|
||
|
||
@Test
|
||
void findAll_ShouldReturnAllDictionaries() {
|
||
when(dao.findByDeletedAtIsNullOrderBySortAsc()).thenReturn(Flux.just(testEntity));
|
||
when(converter.toDomain(testEntity)).thenReturn(testDictionary);
|
||
|
||
Flux<Dictionary> result = service.findAll();
|
||
|
||
StepVerifier.create(result)
|
||
.expectNext(testDictionary)
|
||
.verifyComplete();
|
||
}
|
||
|
||
@Test
|
||
void save_NewDictionary_ShouldSaveSuccessfully() {
|
||
when(dao.findByTypeAndCode("test_type", "test_code")).thenReturn(Mono.empty());
|
||
when(converter.toEntity(testDictionary)).thenReturn(testEntity);
|
||
when(dao.save(testEntity)).thenReturn(Mono.just(testEntity));
|
||
when(converter.toDomain(testEntity)).thenReturn(testDictionary);
|
||
|
||
Mono<Dictionary> result = service.save(testDictionary);
|
||
|
||
StepVerifier.create(result)
|
||
.expectNextMatches(dict -> dict.getCreatedAt() != null && dict.getUpdatedAt() != null)
|
||
.verifyComplete();
|
||
}
|
||
|
||
@Test
|
||
void save_DuplicateDictionary_ShouldThrowException() {
|
||
when(dao.findByTypeAndCode("test_type", "test_code")).thenReturn(Mono.just(testEntity));
|
||
|
||
Mono<Dictionary> result = service.save(testDictionary);
|
||
|
||
StepVerifier.create(result)
|
||
.expectError(DictionaryAlreadyExistsException.class)
|
||
.verify();
|
||
}
|
||
|
||
@Test
|
||
void update_ShouldUpdateDictionary() {
|
||
Dictionary updateDict = new Dictionary();
|
||
updateDict.setName("Updated Name");
|
||
|
||
when(dao.findById(1L)).thenReturn(Mono.just(testEntity));
|
||
when(dao.save(testEntity)).thenReturn(Mono.just(testEntity));
|
||
when(converter.toDomain(testEntity)).thenReturn(testDictionary);
|
||
|
||
Mono<Dictionary> result = service.update(1L, updateDict);
|
||
|
||
StepVerifier.create(result)
|
||
.expectNextMatches(dict -> "Updated Name".equals(dict.getName()))
|
||
.verifyComplete();
|
||
}
|
||
|
||
@Test
|
||
void deleteById_ShouldDeleteDictionary() {
|
||
when(dao.deleteByIdAndDeletedAtIsNull(1L)).thenReturn(Mono.empty());
|
||
|
||
Mono<Void> result = service.deleteById(1L);
|
||
|
||
StepVerifier.create(result)
|
||
.verifyComplete();
|
||
}
|
||
}
|
||
```
|
||
|
||
**Step 2: 创建其他 Service 测试类**
|
||
|
||
重复 Step 1 的模式,为其他 Service 创建单元测试类。
|
||
|
||
**Step 3: 运行所有测试**
|
||
|
||
```bash
|
||
mvn test
|
||
```
|
||
|
||
**Step 4: 提交变更**
|
||
|
||
```bash
|
||
git add core/service/impl/
|
||
git commit -m "test: add unit tests for all services"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 18: 为 Handler 层添加单元测试
|
||
|
||
**Files:**
|
||
- Create: `handler/dictionary/DictionaryHandlerTest.java`
|
||
- Create: `handler/user/SysUserHandlerTest.java`
|
||
- Create: `handler/role/SysRoleHandlerTest.java`
|
||
|
||
**Step 1: 创建 DictionaryHandlerTest**
|
||
|
||
```java
|
||
package cn.novalon.manage.sys.handler.dictionary;
|
||
|
||
import cn.novalon.manage.sys.core.domain.Dictionary;
|
||
import cn.novalon.manage.sys.core.service.IDictionaryService;
|
||
import org.junit.jupiter.api.BeforeEach;
|
||
import org.junit.jupiter.api.Test;
|
||
import org.junit.jupiter.api.extension.ExtendWith;
|
||
import org.mockito.Mock;
|
||
import org.mockito.junit.jupiter.MockitoExtension;
|
||
import org.springframework.http.HttpStatus;
|
||
import org.springframework.mock.web.reactive.function.server.MockServerRequest;
|
||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||
import reactor.core.publisher.Flux;
|
||
import reactor.core.publisher.Mono;
|
||
import reactor.test.StepVerifier;
|
||
|
||
import java.util.List;
|
||
|
||
import static org.mockito.ArgumentMatchers.any;
|
||
import static org.mockito.ArgumentMatchers.eq;
|
||
import static org.mockito.Mockito.when;
|
||
|
||
@ExtendWith(MockitoExtension.class)
|
||
class DictionaryHandlerTest {
|
||
|
||
@Mock
|
||
private IDictionaryService dictionaryService;
|
||
|
||
private DictionaryHandler handler;
|
||
|
||
@BeforeEach
|
||
void setUp() {
|
||
handler = new DictionaryHandler(dictionaryService);
|
||
}
|
||
|
||
@Test
|
||
void getAllDictionaries_ShouldReturnAllDictionaries() {
|
||
List<Dictionary> dictionaries = List.of(new Dictionary());
|
||
when(dictionaryService.findAll()).thenReturn(Flux.fromIterable(dictionaries));
|
||
|
||
Mono<ServerResponse> result = handler.getAllDictionaries(MockServerRequest.builder().build());
|
||
|
||
StepVerifier.create(result)
|
||
.expectNextMatches(response -> response.statusCode().is2xxSuccessful())
|
||
.verifyComplete();
|
||
}
|
||
|
||
@Test
|
||
void getDictionaryById_ShouldReturnDictionary() {
|
||
Dictionary dictionary = new Dictionary();
|
||
dictionary.setId(1L);
|
||
when(dictionaryService.findById(1L)).thenReturn(Mono.just(dictionary));
|
||
|
||
MockServerRequest request = MockServerRequest.builder()
|
||
.pathVariable("id", "1")
|
||
.build();
|
||
|
||
Mono<ServerResponse> result = handler.getDictionaryById(request);
|
||
|
||
StepVerifier.create(result)
|
||
.expectNextMatches(response -> response.statusCode().is2xxSuccessful())
|
||
.verifyComplete();
|
||
}
|
||
|
||
@Test
|
||
void getDictionaryById_NotFound_ShouldReturn404() {
|
||
when(dictionaryService.findById(1L)).thenReturn(Mono.empty());
|
||
|
||
MockServerRequest request = MockServerRequest.builder()
|
||
.pathVariable("id", "1")
|
||
.build();
|
||
|
||
Mono<ServerResponse> result = handler.getDictionaryById(request);
|
||
|
||
StepVerifier.create(result)
|
||
.expectNextMatches(response -> response.statusCode() == HttpStatus.NOT_FOUND)
|
||
.verifyComplete();
|
||
}
|
||
|
||
@Test
|
||
void createDictionary_ShouldCreateDictionary() {
|
||
Dictionary dictionary = new Dictionary();
|
||
dictionary.setType("test_type");
|
||
dictionary.setCode("test_code");
|
||
dictionary.setName("Test Dictionary");
|
||
|
||
when(dictionaryService.save(any(Dictionary.class))).thenReturn(Mono.just(dictionary));
|
||
|
||
MockServerRequest request = MockServerRequest.builder()
|
||
.body(dictionary)
|
||
.build();
|
||
|
||
Mono<ServerResponse> result = handler.createDictionary(request);
|
||
|
||
StepVerifier.create(result)
|
||
.expectNextMatches(response -> response.statusCode() == HttpStatus.CREATED)
|
||
.verifyComplete();
|
||
}
|
||
|
||
@Test
|
||
void updateDictionary_ShouldUpdateDictionary() {
|
||
Dictionary existing = new Dictionary();
|
||
existing.setId(1L);
|
||
existing.setName("Old Name");
|
||
|
||
Dictionary update = new Dictionary();
|
||
update.setName("New Name");
|
||
|
||
when(dictionaryService.findById(1L)).thenReturn(Mono.just(existing));
|
||
when(dictionaryService.update(eq(1L), any(Dictionary.class))).thenReturn(Mono.just(update));
|
||
|
||
MockServerRequest request = MockServerRequest.builder()
|
||
.pathVariable("id", "1")
|
||
.body(update)
|
||
.build();
|
||
|
||
Mono<ServerResponse> result = handler.updateDictionary(request);
|
||
|
||
StepVerifier.create(result)
|
||
.expectNextMatches(response -> response.statusCode().is2xxSuccessful())
|
||
.verifyComplete();
|
||
}
|
||
|
||
@Test
|
||
void deleteDictionary_ShouldDeleteDictionary() {
|
||
when(dictionaryService.deleteById(1L)).thenReturn(Mono.empty());
|
||
|
||
MockServerRequest request = MockServerRequest.builder()
|
||
.pathVariable("id", "1")
|
||
.build();
|
||
|
||
Mono<ServerResponse> result = handler.deleteDictionary(request);
|
||
|
||
StepVerifier.create(result)
|
||
.expectNextMatches(response -> response.statusCode() == HttpStatus.NO_CONTENT)
|
||
.verifyComplete();
|
||
}
|
||
}
|
||
```
|
||
|
||
**Step 2: 创建其他 Handler 测试类**
|
||
|
||
重复 Step 1 的模式,为其他 Handler 创建单元测试类。
|
||
|
||
**Step 3: 运行所有测试**
|
||
|
||
```bash
|
||
mvn test
|
||
```
|
||
|
||
**Step 4: 提交变更**
|
||
|
||
```bash
|
||
git add handler/
|
||
git commit -m "test: add unit tests for all handlers"
|
||
```
|
||
|
||
---
|
||
|
||
## Phase 6: 集成测试和文档
|
||
|
||
### Task 19: 添加集成测试
|
||
|
||
**Files:**
|
||
- Create: `integration/DictionaryIntegrationTest.java`
|
||
- Create: `integration/SysUserIntegrationTest.java`
|
||
|
||
**Step 1: 创建集成测试**
|
||
|
||
```java
|
||
package cn.novalon.manage.sys.integration;
|
||
|
||
import cn.novalon.manage.sys.core.domain.Dictionary;
|
||
import cn.novalon.manage.sys.infrastructure.db.repository.DictionaryRepository;
|
||
import org.junit.jupiter.api.BeforeEach;
|
||
import org.junit.jupiter.api.Test;
|
||
import org.springframework.beans.factory.annotation.Autowired;
|
||
import org.springframework.boot.test.context.SpringBootTest;
|
||
import org.springframework.test.context.ActiveProfiles;
|
||
import org.testcontainers.containers.PostgreSQLContainer;
|
||
import org.testcontainers.junit.jupiter.Container;
|
||
import org.testcontainers.utility.DockerImageName;
|
||
import reactor.test.StepVerifier;
|
||
|
||
@SpringBootTest
|
||
@ActiveProfiles("test")
|
||
class DictionaryIntegrationTest {
|
||
|
||
@Container
|
||
private static final PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>(
|
||
new DockerImageName("postgres:15-alpine"))
|
||
.withDatabaseName("testdb")
|
||
.withUsername("test")
|
||
.withPassword("test");
|
||
|
||
@Autowired
|
||
private DictionaryRepository dictionaryRepository;
|
||
|
||
private Dictionary testDictionary;
|
||
|
||
@BeforeEach
|
||
void setUp() {
|
||
testDictionary = new Dictionary();
|
||
testDictionary.setType("test_type");
|
||
testDictionary.setCode("test_code");
|
||
testDictionary.setName("Test Dictionary");
|
||
testDictionary.setValue("test_value");
|
||
testDictionary.setSort(1);
|
||
}
|
||
|
||
@Test
|
||
void saveAndFindDictionary_ShouldWorkEndToEnd() {
|
||
StepVerifier.create(dictionaryRepository.save(testDictionary))
|
||
.expectNextMatches(dict -> dict.getId() != null)
|
||
.verifyComplete();
|
||
|
||
StepVerifier.create(dictionaryRepository.findByType("test_type"))
|
||
.expectNextMatches(dict -> "test_code".equals(dict.getCode()))
|
||
.verifyComplete();
|
||
}
|
||
|
||
@Test
|
||
void updateDictionary_ShouldPersistChanges() {
|
||
StepVerifier.create(dictionaryRepository.save(testDictionary))
|
||
.expectNextMatches(dict -> dict.getId() != null)
|
||
.verifyComplete();
|
||
|
||
testDictionary.setName("Updated Name");
|
||
|
||
StepVerifier.create(dictionaryRepository.update(testDictionary.getId(), testDictionary))
|
||
.expectNextMatches(dict -> "Updated Name".equals(dict.getName()))
|
||
.verifyComplete();
|
||
}
|
||
|
||
@Test
|
||
void deleteDictionary_ShouldRemoveFromDatabase() {
|
||
StepVerifier.create(dictionaryRepository.save(testDictionary))
|
||
.expectNextMatches(dict -> dict.getId() != null)
|
||
.verifyComplete();
|
||
|
||
StepVerifier.create(dictionaryRepository.deleteById(testDictionary.getId()))
|
||
.verifyComplete();
|
||
|
||
StepVerifier.create(dictionaryRepository.findById(testDictionary.getId()))
|
||
.verifyComplete();
|
||
}
|
||
}
|
||
```
|
||
|
||
**Step 2: 运行集成测试**
|
||
|
||
```bash
|
||
mvn test -Dtest=*IntegrationTest
|
||
```
|
||
|
||
**Step 3: 提交变更**
|
||
|
||
```bash
|
||
git add integration/
|
||
git commit -m "test: add integration tests for key modules"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 20: 更新文档
|
||
|
||
**Files:**
|
||
- Modify: `README.md`
|
||
- Create: `docs/ARCHITECTURE.md`
|
||
- Create: `docs/CONTRIBUTING.md`
|
||
|
||
**Step 1: 更新 README.md**
|
||
|
||
```markdown
|
||
# Novalon Manage System
|
||
|
||
## 项目概述
|
||
|
||
Novalon 管理系统是一个基于 Spring WebFlux 的现代化管理系统,采用响应式编程和函数式路由设计。
|
||
|
||
## 技术栈
|
||
|
||
- **后端框架**: Spring Boot 3.x + WebFlux
|
||
- **数据库**: PostgreSQL + R2DBC
|
||
- **对象映射**: MapStruct
|
||
- **测试框架**: JUnit 5 + Mockito + Testcontainers
|
||
- **构建工具**: Maven
|
||
|
||
## 架构设计
|
||
|
||
系统采用分层架构设计:
|
||
|
||
```
|
||
Handler (函数式路由) → Service (业务逻辑) → Repository (数据访问) → DAO (数据库操作) → R2DBC Repository
|
||
```
|
||
|
||
### 层次职责
|
||
|
||
- **Handler 层**: 处理 HTTP 请求和响应,使用函数式 WebFlux 风格
|
||
- **Service 层**: 实现业务逻辑和事务管理
|
||
- **Repository 层**: 处理数据转换和复杂查询
|
||
- **DAO 层**: 直接数据库操作,继承 R2dbcRepository
|
||
- **Converter/Mapper 层**: 使用 MapStruct 自动生成转换代码
|
||
|
||
## 快速开始
|
||
|
||
### 环境要求
|
||
|
||
- JDK 17+
|
||
- Maven 3.8+
|
||
- PostgreSQL 15+
|
||
|
||
### 运行项目
|
||
|
||
```bash
|
||
# 克隆项目
|
||
git clone <repository-url>
|
||
cd novalon-manage-system
|
||
|
||
# 构建项目
|
||
mvn clean install
|
||
|
||
# 运行应用
|
||
mvn spring-boot:run
|
||
```
|
||
|
||
## 测试
|
||
|
||
```bash
|
||
# 运行单元测试
|
||
mvn test
|
||
|
||
# 运行集成测试
|
||
mvn test -Dtest=*IntegrationTest
|
||
|
||
# 运行所有测试
|
||
mvn verify
|
||
```
|
||
|
||
## 项目结构
|
||
|
||
```
|
||
novalon-manage-api/manage-sys/
|
||
├── config/ # 配置类
|
||
│ ├── SystemRouter.java # 统一路由配置
|
||
│ ├── SecurityConfig.java # 安全配置
|
||
│ └── WebFluxConfig.java # WebFlux 配置
|
||
├── core/ # 核心业务逻辑
|
||
│ ├── domain/ # 领域模型
|
||
│ ├── service/ # 服务接口和实现
|
||
│ └── repository/ # 仓储接口
|
||
├── infrastructure/ # 基础设施
|
||
│ └── db/
|
||
│ ├── dao/ # 数据访问对象
|
||
│ ├── entity/ # 数据库实体
|
||
│ ├── mapper/ # MapStruct 映射器
|
||
│ └── repository/ # 仓储实现
|
||
└── handler/ # 处理器(函数式路由)
|
||
├── dictionary/
|
||
├── user/
|
||
├── role/
|
||
└── ...
|
||
```
|
||
|
||
## 开发规范
|
||
|
||
### 代码风格
|
||
|
||
- 使用函数式 WebFlux 风格的 Handler
|
||
- 使用 MapStruct 进行对象转换
|
||
- 遵循 DRY 原则
|
||
- 使用 TDD 开发模式
|
||
|
||
### 命名规范
|
||
|
||
- **Handler**: `{Entity}Handler`
|
||
- **Service**: `I{Entity}Service` 接口,`{Entity}Service` 实现
|
||
- **Repository**: `{Entity}Repository`
|
||
- **DAO**: `{Entity}Dao`
|
||
- **Mapper**: `{Entity}Mapper`
|
||
- **Entity**: `{Entity}Entity`
|
||
|
||
### 提交规范
|
||
|
||
```bash
|
||
feat: 新功能
|
||
fix: 修复问题
|
||
refactor: 重构代码
|
||
test: 添加测试
|
||
docs: 更新文档
|
||
```
|
||
|
||
## 许可证
|
||
|
||
[License Information]
|
||
```
|
||
|
||
**Step 2: 创建架构文档**
|
||
|
||
```markdown
|
||
# 系统架构文档
|
||
|
||
## 概述
|
||
|
||
Novalon 管理系统采用现代化的分层架构设计,结合响应式编程和函数式路由,提供高性能和可扩展性。
|
||
|
||
## 架构层次
|
||
|
||
### 1. Handler 层(表现层)
|
||
|
||
**职责**:
|
||
- 处理 HTTP 请求和响应
|
||
- 参数验证和类型转换
|
||
- 路由定义和配置
|
||
|
||
**技术实现**:
|
||
- 使用函数式 WebFlux 风格
|
||
- 通过 `SystemRouter` 集中配置路由
|
||
- 使用 `ServerRequest` 和 `ServerResponse`
|
||
|
||
**示例**:
|
||
```java
|
||
@Component
|
||
public class DictionaryHandler {
|
||
public Mono<ServerResponse> getAllDictionaries(ServerRequest request) {
|
||
return ServerResponse.ok()
|
||
.body(dictionaryService.findAll(), Dictionary.class);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 2. Service 层(业务逻辑层)
|
||
|
||
**职责**:
|
||
- 实现业务逻辑
|
||
- 事务管理
|
||
- 业务规则验证
|
||
- 跨 Repository 编排
|
||
|
||
**技术实现**:
|
||
- 接口定义:`I{Entity}Service`
|
||
- 实现类:`{Entity}Service`
|
||
- 使用 `@Service` 注解
|
||
|
||
**示例**:
|
||
```java
|
||
@Service
|
||
public class DictionaryService implements IDictionaryService {
|
||
private final DictionaryDao dao;
|
||
private final DictionaryMapper mapper;
|
||
|
||
public Mono<Dictionary> save(Dictionary dictionary) {
|
||
return dao.save(mapper.toEntity(dictionary))
|
||
.map(mapper::toDomain);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 3. Repository 层(数据访问层)
|
||
|
||
**职责**:
|
||
- 数据转换(Entity ↔ Domain)
|
||
- 复杂查询实现
|
||
- 分页查询支持
|
||
- 批量操作支持
|
||
|
||
**技术实现**:
|
||
- 使用 `{Entity}Repository` 类
|
||
- 依赖 `{Entity}Dao` 和 `{Entity}Mapper`
|
||
- 使用 `@Repository` 注解
|
||
|
||
**示例**:
|
||
```java
|
||
@Repository
|
||
public class DictionaryRepository {
|
||
private final DictionaryDao dao;
|
||
private final DictionaryMapper mapper;
|
||
|
||
public Flux<Dictionary> findByType(String type) {
|
||
return dao.findByType(type)
|
||
.map(mapper::toDomain);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 4. DAO 层(数据库操作层)
|
||
|
||
**职责**:
|
||
- 直接数据库操作
|
||
- 继承 R2dbcRepository
|
||
- 定义查询方法
|
||
|
||
**技术实现**:
|
||
- 接口定义:`{Entity}Dao`
|
||
- 继承 `R2dbcRepository<{Entity}Entity, Long>`
|
||
- 使用 `@Repository` 注解
|
||
|
||
**示例**:
|
||
```java
|
||
@Repository
|
||
public interface DictionaryDao extends R2dbcRepository<DictionaryEntity, Long> {
|
||
Flux<DictionaryEntity> findByType(String type);
|
||
Mono<DictionaryEntity> findByTypeAndCode(String type, String code);
|
||
}
|
||
```
|
||
|
||
### 5. Mapper 层(对象转换层)
|
||
|
||
**职责**:
|
||
- Entity ↔ Domain 转换
|
||
- 批量转换支持
|
||
- 自动代码生成
|
||
|
||
**技术实现**:
|
||
- 使用 MapStruct
|
||
- 接口定义:`{Entity}Mapper`
|
||
- 使用 `@Mapper(componentModel = "spring")` 注解
|
||
|
||
**示例**:
|
||
```java
|
||
@Mapper(componentModel = "spring")
|
||
public interface DictionaryMapper {
|
||
Dictionary toDomain(DictionaryEntity entity);
|
||
DictionaryEntity toEntity(Dictionary domain);
|
||
List<Dictionary> toDomainList(List<DictionaryEntity> entities);
|
||
List<DictionaryEntity> toEntityList(List<Dictionary> domains);
|
||
}
|
||
```
|
||
|
||
## 数据流
|
||
|
||
### 读取流程
|
||
|
||
```
|
||
HTTP Request → Handler → Service → Repository → DAO → R2DBC → Database
|
||
```
|
||
|
||
### 写入流程
|
||
|
||
```
|
||
HTTP Request → Handler → Service → Repository → Mapper → DAO → R2DBC → Database
|
||
```
|
||
|
||
## 技术选型
|
||
|
||
### 为什么选择函数式 WebFlux?
|
||
|
||
1. **性能优势**: 非阻塞 I/O,高并发处理能力
|
||
2. **代码简洁**: 函数式编程减少样板代码
|
||
3. **类型安全**: 编译时类型检查
|
||
4. **测试友好**: 易于单元测试和模拟
|
||
|
||
### 为什么选择 MapStruct?
|
||
|
||
1. **性能**: 编译时生成代码,运行时零开销
|
||
2. **维护性**: 自动生成,减少手动维护
|
||
3. **类型安全**: 编译时验证映射正确性
|
||
4. **批量支持**: 内置批量转换方法
|
||
|
||
## 设计原则
|
||
|
||
### DRY (Don't Repeat Yourself)
|
||
|
||
- 提取公共逻辑到工具类
|
||
- 使用继承减少重复代码
|
||
- 使用 MapStruct 自动生成转换代码
|
||
|
||
### YAGNI (You Aren't Gonna Need It)
|
||
|
||
- 只实现当前需要的功能
|
||
- 避免过度设计
|
||
- 保持代码简洁
|
||
|
||
### TDD (Test-Driven Development)
|
||
|
||
- 先写测试,再写实现
|
||
- 保持高测试覆盖率
|
||
- 频繁提交小步改进
|
||
|
||
### SOLID 原则
|
||
|
||
- **单一职责**: 每个类只负责一个功能
|
||
- **开闭原则**: 对扩展开放,对修改关闭
|
||
- **里氏替换**: 子类可以替换父类
|
||
- **接口隔离**: 客户端不应该依赖不需要的接口
|
||
- **依赖倒置**: 依赖抽象而非具体实现
|
||
|
||
## 性能优化
|
||
|
||
### 1. 批量操作
|
||
|
||
- 使用批量转换方法
|
||
- 批量数据库操作
|
||
- 减少数据库往返
|
||
|
||
### 2. 缓存策略
|
||
|
||
- 使用 Caffeine 缓存热点数据
|
||
- 合理设置缓存过期时间
|
||
- 缓存穿透保护
|
||
|
||
### 3. 响应式编程
|
||
|
||
- 非阻塞 I/O 操作
|
||
- 背压处理
|
||
- 资源高效利用
|
||
|
||
## 安全考虑
|
||
|
||
### 1. 认证授权
|
||
|
||
- JWT Token 认证
|
||
- 基于角色的访问控制
|
||
- 密码加密存储
|
||
|
||
### 2. 数据验证
|
||
|
||
- 输入参数验证
|
||
- 业务规则验证
|
||
- SQL 注入防护
|
||
|
||
### 3. 审计日志
|
||
|
||
- 操作日志记录
|
||
- 登录日志记录
|
||
- 异常日志记录
|
||
```
|
||
|
||
**Step 3: 创建贡献指南**
|
||
|
||
```markdown
|
||
# 贡献指南
|
||
|
||
## 如何贡献
|
||
|
||
我们欢迎任何形式的贡献,包括但不限于:
|
||
|
||
- 报告 Bug
|
||
- 提出新功能
|
||
- 改进文档
|
||
- 提交代码
|
||
|
||
## 开发流程
|
||
|
||
### 1. Fork 项目
|
||
|
||
```bash
|
||
git fork <repository-url>
|
||
git clone <your-fork-url>
|
||
cd novalon-manage-system
|
||
```
|
||
|
||
### 2. 创建分支
|
||
|
||
```bash
|
||
git checkout -b feature/your-feature-name
|
||
```
|
||
|
||
### 3. 编写代码
|
||
|
||
遵循项目开发规范:
|
||
- 使用函数式 WebFlux 风格
|
||
- 使用 MapStruct 进行对象转换
|
||
- 编写单元测试
|
||
- 遵循命名规范
|
||
|
||
### 4. 运行测试
|
||
|
||
```bash
|
||
mvn clean test
|
||
```
|
||
|
||
确保所有测试通过。
|
||
|
||
### 5. 提交代码
|
||
|
||
```bash
|
||
git add .
|
||
git commit -m "feat: add your feature"
|
||
```
|
||
|
||
提交信息格式:
|
||
- `feat:` 新功能
|
||
- `fix:` 修复问题
|
||
- `refactor:` 重构代码
|
||
- `test:` 添加测试
|
||
- `docs:` 更新文档
|
||
|
||
### 6. 推送分支
|
||
|
||
```bash
|
||
git push origin feature/your-feature-name
|
||
```
|
||
|
||
### 7. 创建 Pull Request
|
||
|
||
在 GitHub 上创建 Pull Request,描述:
|
||
- 变更内容
|
||
- 相关 Issue
|
||
- 测试情况
|
||
- 截图(如适用)
|
||
|
||
## 代码规范
|
||
|
||
### 命名规范
|
||
|
||
- **类名**: PascalCase,如 `DictionaryHandler`
|
||
- **方法名**: camelCase,如 `getAllDictionaries`
|
||
- **常量名**: UPPER_SNAKE_CASE,如 `MAX_SIZE`
|
||
- **包名**: 小写,如 `cn.novalon.manage.sys`
|
||
|
||
### 注释规范
|
||
|
||
- 类注释:描述类的职责和用途
|
||
- 方法注释:描述方法的功能、参数、返回值
|
||
- 复杂逻辑:添加行内注释
|
||
|
||
### 测试规范
|
||
|
||
- 测试类名:`{ClassName}Test`
|
||
- 测试方法名:`{MethodName}_{ExpectedBehavior}`
|
||
- 使用 Given-When-Then 模式
|
||
|
||
## Pull Request 检查清单
|
||
|
||
- [ ] 代码通过所有测试
|
||
- [ ] 遵循代码规范
|
||
- [ ] 添加了必要的测试
|
||
- [ ] 更新了相关文档
|
||
- [ ] 提交信息格式正确
|
||
- [ ] 没有 TODO 或 FIXME 注释
|
||
|
||
## 问题报告
|
||
|
||
使用 GitHub Issues 报告问题,请包含:
|
||
|
||
- 问题描述
|
||
- 复现步骤
|
||
- 预期行为
|
||
- 实际行为
|
||
- 环境信息(OS、JDK 版本等)
|
||
- 相关日志或截图
|
||
|
||
## 功能建议
|
||
|
||
使用 GitHub Issues 提出新功能建议,请描述:
|
||
|
||
- 功能描述
|
||
- 使用场景
|
||
- 预期收益
|
||
- 可能的实现方案
|
||
```
|
||
|
||
**Step 4: 提交文档变更**
|
||
|
||
```bash
|
||
git add README.md docs/
|
||
git commit -m "docs: update project documentation and architecture guide"
|
||
```
|
||
|
||
---
|
||
|
||
## 总结
|
||
|
||
本实施计划涵盖了以下主要方面:
|
||
|
||
### Phase 1: Handler 函数式迁移(9 个任务)
|
||
- Task 1-9: 将所有 Handler 从注解式迁移到函数式 WebFlux 风格
|
||
|
||
### Phase 2: Router 配置完善(1 个任务)
|
||
- Task 10: 扩展 SystemRouter 配置,支持所有模块
|
||
|
||
### Phase 3: Converter 优化(1 个任务)
|
||
- Task 11: 为所有 Converter 添加批量转换方法
|
||
|
||
### Phase 4: MapStruct 引入(3 个任务)
|
||
- Task 12: 配置 MapStruct 依赖
|
||
- Task 13: 创建 MapStruct Mapper 接口
|
||
- Task 14: 使用 MapStruct 替换手动 Converter
|
||
|
||
### Phase 5: 单元测试(4 个任务)
|
||
- Task 15-18: 为 Repository、Service、Handler 层添加完整的单元测试
|
||
|
||
### Phase 6: 集成测试和文档(2 个任务)
|
||
- Task 19: 添加集成测试
|
||
- Task 20: 更新项目文档
|
||
|
||
**总计**: 20 个任务,覆盖所有未完成的工作和后续建议。
|
||
|
||
---
|
||
|
||
**Plan complete and saved to `docs/plans/2026-03-12-infrastructure-refactoring-phase2.md`. Two execution options:**
|
||
|
||
**1. Subagent-Driven (this session)** - I dispatch fresh subagent per task, review between tasks, fast iteration
|
||
|
||
**2. Parallel Session (separate)** - Open new session with executing-plans, batch execution with checkpoints
|
||
|
||
**Which approach?** |