Files
novalon-manage-system/docs/plans/2026-04-03-operation-log-implementation.md
张翔 2bdf5a6c90 docs: add operation log implementation plan
- Break down into 9 bite-sized tasks
- Follow TDD approach with tests first
- Include exact file paths and complete code
- Provide verification steps for each task
2026-04-03 19:28:48 +08:00

673 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 操作日志记录功能实施计划
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**目标**: 实现基于注解的操作日志记录功能,自动记录关键业务操作到数据库,解决Dashboard操作日志显示0的问题。
**架构**: 采用AOP切面 + 注解驱动的架构,使用Spring AOP拦截带`@OperationLog`注解的方法,异步记录操作日志到数据库,不影响主业务流程性能。
**技术栈**: Java 21, Spring Boot 3.5.13, Spring AOP, Project Reactor, Jackson
---
## Task 1: 创建 @OperationLog 注解
**文件:**
- 创建: `novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/audit/OperationLog.java`
**Step 1: 创建注解文件**
```java
package cn.novalon.manage.sys.audit;
import java.lang.annotation.*;
/**
* 操作日志注解
* 标记需要记录操作日志的方法
*
* @author 张翔
* @date 2026-04-03
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OperationLog {
/**
* 操作名称
* 例如:"创建用户"、"删除角色"
*/
String operation();
/**
* 模块名称
* 例如:"用户管理"、"角色管理"
*/
String module();
}
```
**Step 2: 验证注解创建成功**
运行: `ls -la novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/audit/OperationLog.java`
预期: 文件存在且内容正确
**Step 3: 提交**
```bash
git add novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/audit/OperationLog.java
git commit -m "feat: add @OperationLog annotation for operation logging"
```
---
## Task 2: 创建 OperationLogAspect 切面(基础结构)
**文件:**
- 创建: `novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/audit/OperationLogAspect.java`
**Step 1: 创建切面基础结构**
```java
package cn.novalon.manage.sys.audit;
import cn.novalon.manage.sys.core.domain.OperationLog;
import cn.novalon.manage.sys.core.service.IOperationLogService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
/**
* 操作日志切面
*
* 文件定义:使用AOP自动拦截带@OperationLog注解的方法,记录操作日志
* 涉及业务:自动记录用户操作,包括操作人、操作时间、参数、结果、耗时等
* 算法:使用异步方式记录日志,不阻塞主流程
*
* @author 张翔
* @date 2026-04-03
*/
@Aspect
@Component
public class OperationLogAspect {
private static final Logger logger = LoggerFactory.getLogger(OperationLogAspect.class);
private final IOperationLogService logService;
private final ObjectMapper objectMapper;
public OperationLogAspect(IOperationLogService logService, ObjectMapper objectMapper) {
this.logService = logService;
this.objectMapper = objectMapper;
}
@Around("@annotation(operationLog)")
public Object around(ProceedingJoinPoint point, OperationLog operationLog) throws Throwable {
long startTime = System.currentTimeMillis();
// 获取基本信息
String username = getCurrentUsername();
String ip = "unknown";
String method = point.getSignature().toShortString();
String params = serializeParams(point.getArgs());
// 执行业务方法
Object result = null;
String status = "0";
String errorMsg = null;
try {
result = point.proceed();
// 处理响应式结果
if (result instanceof Mono) {
return ((Mono<?>) result)
.doOnSuccess(res -> {
long duration = System.currentTimeMillis() - startTime;
saveLogAsync(operationLog, username, ip, method,
params, res, duration, "0", null);
})
.doOnError(error -> {
long duration = System.currentTimeMillis() - startTime;
saveLogAsync(operationLog, username, ip, method,
params, null, duration, "1", error.getMessage());
});
}
return result;
} catch (Throwable error) {
status = "1";
errorMsg = error.getMessage();
throw error;
} finally {
if (!(result instanceof Mono)) {
long duration = System.currentTimeMillis() - startTime;
saveLogAsync(operationLog, username, ip, method,
params, result, duration, status, errorMsg);
}
}
}
private String getCurrentUsername() {
try {
return ReactiveSecurityContextHolder.getContext()
.map(ctx -> ctx.getAuthentication().getPrincipal())
.cast(String.class)
.blockOptional()
.orElse("system");
} catch (Exception e) {
logger.warn("获取当前用户名失败: {}", e.getMessage());
return "system";
}
}
private String serializeParams(Object[] args) {
try {
if (args == null || args.length == 0) {
return null;
}
return objectMapper.writeValueAsString(args);
} catch (Exception e) {
logger.warn("序列化参数失败: {}", e.getMessage());
return null;
}
}
private String serializeResult(Object result) {
try {
if (result == null) {
return null;
}
return objectMapper.writeValueAsString(result);
} catch (Exception e) {
logger.warn("序列化结果失败: {}", e.getMessage());
return null;
}
}
private void saveLogAsync(OperationLog annotation, String username,
String ip, String method, String params,
Object result, long duration, String status,
String errorMsg) {
Mono.fromRunnable(() -> {
OperationLog log = new OperationLog();
log.setUsername(username);
log.setOperation(annotation.module() + " - " + annotation.operation());
log.setMethod(method);
log.setParams(params);
log.setResult(serializeResult(result));
log.setIp(ip);
log.setDuration(duration);
log.setStatus(status);
log.setErrorMsg(errorMsg);
logService.save(log)
.doOnSuccess(saved -> logger.debug("操作日志保存成功: {} - {}",
annotation.module(), annotation.operation()))
.doOnError(error -> logger.error("操作日志保存失败: {}",
error.getMessage()))
.subscribe();
})
.subscribeOn(Schedulers.boundedElastic())
.subscribe();
}
}
```
**Step 2: 验证切面创建成功**
运行: `ls -la novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/audit/OperationLogAspect.java`
预期: 文件存在且内容正确
**Step 3: 提交**
```bash
git add novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/audit/OperationLogAspect.java
git commit -m "feat: implement OperationLogAspect for automatic operation logging"
```
---
## Task 3: 编写单元测试
**文件:**
- 创建: `novalon-manage-api/manage-sys/src/test/java/cn/novalon/manage/sys/audit/OperationLogAspectTest.java`
**Step 1: 创建测试文件**
```java
package cn.novalon.manage.sys.audit;
import cn.novalon.manage.sys.core.domain.OperationLog;
import cn.novalon.manage.sys.core.service.IOperationLogService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
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.Mono;
import reactor.test.StepVerifier;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
/**
* OperationLogAspect 单元测试
*
* @author 张翔
* @date 2026-04-03
*/
@ExtendWith(MockitoExtension.class)
class OperationLogAspectTest {
@Mock
private IOperationLogService logService;
@Mock
private ProceedingJoinPoint joinPoint;
@Mock
private Signature signature;
private OperationLogAspect aspect;
private ObjectMapper objectMapper;
@BeforeEach
void setUp() {
objectMapper = new ObjectMapper();
aspect = new OperationLogAspect(logService, objectMapper);
}
@Test
void testAround_WithMonoResult_ShouldSaveLog() throws Throwable {
OperationLog annotation = new OperationLog() {
@Override
public String operation() {
return "创建用户";
}
@Override
public String module() {
return "用户管理";
}
@Override
public Class<? extends java.lang.annotation.Annotation> annotationType() {
return OperationLog.class;
}
};
when(joinPoint.getSignature()).thenReturn(signature);
when(signature.toShortString()).thenReturn("SysUserHandler.createUser");
when(joinPoint.getArgs()).thenReturn(new Object[]{"test"});
when(joinPoint.proceed()).thenReturn(Mono.just("success"));
when(logService.save(any(OperationLog.class))).thenReturn(Mono.just(new OperationLog()));
Object result = aspect.around(joinPoint, annotation);
StepVerifier.create((Mono<?>) result)
.expectNext("success")
.verifyComplete();
verify(logService, timeout(1000)).save(any(OperationLog.class));
}
@Test
void testAround_WithException_ShouldSaveErrorLog() throws Throwable {
OperationLog annotation = new OperationLog() {
@Override
public String operation() {
return "删除用户";
}
@Override
public String module() {
return "用户管理";
}
@Override
public Class<? extends java.lang.annotation.Annotation> annotationType() {
return OperationLog.class;
}
};
when(joinPoint.getSignature()).thenReturn(signature);
when(signature.toShortString()).thenReturn("SysUserHandler.deleteUser");
when(joinPoint.getArgs()).thenReturn(new Object[]{1L});
when(joinPoint.proceed()).thenThrow(new RuntimeException("删除失败"));
try {
aspect.around(joinPoint, annotation);
} catch (RuntimeException e) {
assert e.getMessage().equals("删除失败");
}
verify(logService, timeout(1000)).save(any(OperationLog.class));
}
}
```
**Step 2: 运行测试验证**
运行: `cd novalon-manage-api && ./mvnw test -Dtest=OperationLogAspectTest -pl manage-sys`
预期: 测试通过
**Step 3: 提交**
```bash
git add novalon-manage-api/manage-sys/src/test/java/cn/novalon/manage/sys/audit/OperationLogAspectTest.java
git commit -m "test: add unit tests for OperationLogAspect"
```
---
## Task 4: 在用户管理Handler上添加注解
**文件:**
- 修改: `novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/handler/user/SysUserHandler.java`
**Step 1: 在createUser方法上添加注解**
找到 `createUser` 方法,在方法上添加:
```java
@OperationLog(operation = "创建用户", module = "用户管理")
public Mono<ServerResponse> createUser(ServerRequest request) {
// 现有代码保持不变
}
```
**Step 2: 在updateUser方法上添加注解**
找到 `updateUser` 方法,在方法上添加:
```java
@OperationLog(operation = "更新用户", module = "用户管理")
public Mono<ServerResponse> updateUser(ServerRequest request) {
// 现有代码保持不变
}
```
**Step 3: 在deleteUser方法上添加注解**
找到 `deleteUser` 方法,在方法上添加:
```java
@OperationLog(operation = "删除用户", module = "用户管理")
public Mono<ServerResponse> deleteUser(ServerRequest request) {
// 现有代码保持不变
}
```
**Step 4: 在changePassword方法上添加注解**
找到 `changePassword` 方法,在方法上添加:
```java
@OperationLog(operation = "修改密码", module = "用户管理")
public Mono<ServerResponse> changePassword(ServerRequest request) {
// 现有代码保持不变
}
```
**Step 5: 在assignRoles方法上添加注解**
找到 `assignRoles` 方法,在方法上添加:
```java
@OperationLog(operation = "分配角色", module = "用户管理")
public Mono<ServerResponse> assignRoles(ServerRequest request) {
// 现有代码保持不变
}
```
**Step 6: 验证修改**
运行: `cd novalon-manage-api && ./mvnw compile -pl manage-sys`
预期: 编译成功
**Step 7: 提交**
```bash
git add novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/handler/user/SysUserHandler.java
git commit -m "feat: add @OperationLog annotations to user management operations"
```
---
## Task 5: 在角色管理Handler上添加注解
**文件:**
- 修改: `novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/handler/role/SysRoleHandler.java`
**Step 1: 在createRole方法上添加注解**
```java
@OperationLog(operation = "创建角色", module = "角色管理")
public Mono<ServerResponse> createRole(ServerRequest request) {
// 现有代码保持不变
}
```
**Step 2: 在updateRole方法上添加注解**
```java
@OperationLog(operation = "更新角色", module = "角色管理")
public Mono<ServerResponse> updateRole(ServerRequest request) {
// 现有代码保持不变
}
```
**Step 3: 在deleteRole方法上添加注解**
```java
@OperationLog(operation = "删除角色", module = "角色管理")
public Mono<ServerResponse> deleteRole(ServerRequest request) {
// 现有代码保持不变
}
```
**Step 4: 验证修改**
运行: `cd novalon-manage-api && ./mvnw compile -pl manage-sys`
预期: 编译成功
**Step 5: 提交**
```bash
git add novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/handler/role/SysRoleHandler.java
git commit -m "feat: add @OperationLog annotations to role management operations"
```
---
## Task 6: 在菜单管理Handler上添加注解
**文件:**
- 修改: `novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/handler/menu/MenuHandler.java`
**Step 1: 在createMenu方法上添加注解**
```java
@OperationLog(operation = "创建菜单", module = "菜单管理")
public Mono<ServerResponse> createMenu(ServerRequest request) {
// 现有代码保持不变
}
```
**Step 2: 在updateMenu方法上添加注解**
```java
@OperationLog(operation = "更新菜单", module = "菜单管理")
public Mono<ServerResponse> updateMenu(ServerRequest request) {
// 现有代码保持不变
}
```
**Step 3: 在deleteMenu方法上添加注解**
```java
@OperationLog(operation = "删除菜单", module = "菜单管理")
public Mono<ServerResponse> deleteMenu(ServerRequest request) {
// 现有代码保持不变
}
```
**Step 4: 验证修改**
运行: `cd novalon-manage-api && ./mvnw compile -pl manage-sys`
预期: 编译成功
**Step 5: 提交**
```bash
git add novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/handler/menu/MenuHandler.java
git commit -m "feat: add @OperationLog annotations to menu management operations"
```
---
## Task 7: 运行集成测试验证
**Step 1: 启动后端服务**
运行: `cd novalon-manage-api && ./mvnw spring-boot:run -pl manage-app -Dspring-boot.run.profiles=test`
等待服务启动完成(约30秒)
**Step 2: 执行用户创建操作**
运行:
```bash
TOKEN=$(curl -s -X POST http://localhost:8084/api/auth/login -H "Content-Type: application/json" -d '{"username":"e2e_test_user","password":"admin123"}' | grep -o '"token":"[^"]*' | cut -d'"' -f4)
curl -X POST http://localhost:8084/api/users -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{"username":"test_op_log","password":"Test123!@#","email":"test@example.com","phone":"13900139001","nickname":"测试操作日志"}'
```
预期: 用户创建成功
**Step 3: 验证操作日志已记录**
运行:
```bash
curl -X GET "http://localhost:8084/api/logs/operation/count" -H "Authorization: Bearer $TOKEN"
```
预期: 返回值大于0
**Step 4: 查看操作日志详情**
运行:
```bash
curl -X GET "http://localhost:8084/api/logs/operation" -H "Authorization: Bearer $TOKEN"
```
预期: 返回包含"创建用户"操作的日志记录
**Step 5: 停止后端服务**
按 Ctrl+C 停止服务
---
## Task 8: 运行E2E测试验证
**Step 1: 启动前端和后端服务**
运行:
```bash
# 终端1: 启动后端
cd novalon-manage-api && ./mvnw spring-boot:run -pl manage-app -Dspring-boot.run.profiles=test
# 终端2: 启动前端
cd novalon-manage-web && pnpm dev
```
等待服务启动完成
**Step 2: 运行E2E测试**
运行: `cd novalon-manage-web && npx playwright test e2e/user-management.spec.ts --project=chromium`
预期: 测试通过
**Step 3: 手动验证Dashboard**
1. 打开浏览器访问 http://localhost:3002
2. 登录系统(用户名: e2e_test_user, 密码: admin123
3. 执行用户管理操作(创建、更新、删除用户)
4. 查看Dashboard操作日志数量是否增加
预期: 操作日志数量随操作增加
**Step 4: 停止服务**
按 Ctrl+C 停止所有服务
---
## Task 9: 最终验证和提交
**Step 1: 运行所有后端测试**
运行: `cd novalon-manage-api && ./mvnw test`
预期: 所有测试通过
**Step 2: 检查代码质量**
运行: `cd novalon-manage-api && ./mvnw checkstyle:check`
预期: 检查通过
**Step 3: 更新README文档**
`README.md` 中添加操作日志功能说明:
```markdown
## 操作日志功能
系统自动记录关键业务操作,包括:
- 用户管理:创建、更新、删除用户、修改密码、分配角色
- 角色管理:创建、更新、删除角色
- 菜单管理:创建、更新、删除菜单
操作日志可在Dashboard中查看,用于系统审计和问题追踪。
```
**Step 4: 最终提交**
```bash
git add README.md
git commit -m "docs: update README with operation log feature description"
```
---
## 完成标准
- ✅ 所有单元测试通过
- ✅ 所有集成测试通过
- ✅ E2E测试通过
- ✅ Dashboard操作日志数量正常显示
- ✅ 代码质量检查通过
- ✅ 文档更新完成
- ✅ 所有代码已提交到Git