- 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
19 KiB
操作日志记录功能实施计划
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: 创建注解文件
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: 提交
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: 创建切面基础结构
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: 提交
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: 创建测试文件
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: 提交
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 方法,在方法上添加:
@OperationLog(operation = "创建用户", module = "用户管理")
public Mono<ServerResponse> createUser(ServerRequest request) {
// 现有代码保持不变
}
Step 2: 在updateUser方法上添加注解
找到 updateUser 方法,在方法上添加:
@OperationLog(operation = "更新用户", module = "用户管理")
public Mono<ServerResponse> updateUser(ServerRequest request) {
// 现有代码保持不变
}
Step 3: 在deleteUser方法上添加注解
找到 deleteUser 方法,在方法上添加:
@OperationLog(operation = "删除用户", module = "用户管理")
public Mono<ServerResponse> deleteUser(ServerRequest request) {
// 现有代码保持不变
}
Step 4: 在changePassword方法上添加注解
找到 changePassword 方法,在方法上添加:
@OperationLog(operation = "修改密码", module = "用户管理")
public Mono<ServerResponse> changePassword(ServerRequest request) {
// 现有代码保持不变
}
Step 5: 在assignRoles方法上添加注解
找到 assignRoles 方法,在方法上添加:
@OperationLog(operation = "分配角色", module = "用户管理")
public Mono<ServerResponse> assignRoles(ServerRequest request) {
// 现有代码保持不变
}
Step 6: 验证修改
运行: cd novalon-manage-api && ./mvnw compile -pl manage-sys
预期: 编译成功
Step 7: 提交
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方法上添加注解
@OperationLog(operation = "创建角色", module = "角色管理")
public Mono<ServerResponse> createRole(ServerRequest request) {
// 现有代码保持不变
}
Step 2: 在updateRole方法上添加注解
@OperationLog(operation = "更新角色", module = "角色管理")
public Mono<ServerResponse> updateRole(ServerRequest request) {
// 现有代码保持不变
}
Step 3: 在deleteRole方法上添加注解
@OperationLog(operation = "删除角色", module = "角色管理")
public Mono<ServerResponse> deleteRole(ServerRequest request) {
// 现有代码保持不变
}
Step 4: 验证修改
运行: cd novalon-manage-api && ./mvnw compile -pl manage-sys
预期: 编译成功
Step 5: 提交
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方法上添加注解
@OperationLog(operation = "创建菜单", module = "菜单管理")
public Mono<ServerResponse> createMenu(ServerRequest request) {
// 现有代码保持不变
}
Step 2: 在updateMenu方法上添加注解
@OperationLog(operation = "更新菜单", module = "菜单管理")
public Mono<ServerResponse> updateMenu(ServerRequest request) {
// 现有代码保持不变
}
Step 3: 在deleteMenu方法上添加注解
@OperationLog(operation = "删除菜单", module = "菜单管理")
public Mono<ServerResponse> deleteMenu(ServerRequest request) {
// 现有代码保持不变
}
Step 4: 验证修改
运行: cd novalon-manage-api && ./mvnw compile -pl manage-sys
预期: 编译成功
Step 5: 提交
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: 执行用户创建操作
运行:
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: 验证操作日志已记录
运行:
curl -X GET "http://localhost:8084/api/logs/operation/count" -H "Authorization: Bearer $TOKEN"
预期: 返回值大于0
Step 4: 查看操作日志详情
运行:
curl -X GET "http://localhost:8084/api/logs/operation" -H "Authorization: Bearer $TOKEN"
预期: 返回包含"创建用户"操作的日志记录
Step 5: 停止后端服务
按 Ctrl+C 停止服务
Task 8: 运行E2E测试验证
Step 1: 启动前端和后端服务
运行:
# 终端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
- 打开浏览器访问 http://localhost:3002
- 登录系统(用户名: e2e_test_user, 密码: admin123)
- 执行用户管理操作(创建、更新、删除用户)
- 查看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 中添加操作日志功能说明:
## 操作日志功能
系统自动记录关键业务操作,包括:
- 用户管理:创建、更新、删除用户、修改密码、分配角色
- 角色管理:创建、更新、删除角色
- 菜单管理:创建、更新、删除菜单
操作日志可在Dashboard中查看,用于系统审计和问题追踪。
Step 4: 最终提交
git add README.md
git commit -m "docs: update README with operation log feature description"
完成标准
- ✅ 所有单元测试通过
- ✅ 所有集成测试通过
- ✅ E2E测试通过
- ✅ Dashboard操作日志数量正常显示
- ✅ 代码质量检查通过
- ✅ 文档更新完成
- ✅ 所有代码已提交到Git