# 操作日志记录功能实施计划 > **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 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 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 createUser(ServerRequest request) { // 现有代码保持不变 } ``` **Step 2: 在updateUser方法上添加注解** 找到 `updateUser` 方法,在方法上添加: ```java @OperationLog(operation = "更新用户", module = "用户管理") public Mono updateUser(ServerRequest request) { // 现有代码保持不变 } ``` **Step 3: 在deleteUser方法上添加注解** 找到 `deleteUser` 方法,在方法上添加: ```java @OperationLog(operation = "删除用户", module = "用户管理") public Mono deleteUser(ServerRequest request) { // 现有代码保持不变 } ``` **Step 4: 在changePassword方法上添加注解** 找到 `changePassword` 方法,在方法上添加: ```java @OperationLog(operation = "修改密码", module = "用户管理") public Mono changePassword(ServerRequest request) { // 现有代码保持不变 } ``` **Step 5: 在assignRoles方法上添加注解** 找到 `assignRoles` 方法,在方法上添加: ```java @OperationLog(operation = "分配角色", module = "用户管理") public Mono 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 createRole(ServerRequest request) { // 现有代码保持不变 } ``` **Step 2: 在updateRole方法上添加注解** ```java @OperationLog(operation = "更新角色", module = "角色管理") public Mono updateRole(ServerRequest request) { // 现有代码保持不变 } ``` **Step 3: 在deleteRole方法上添加注解** ```java @OperationLog(operation = "删除角色", module = "角色管理") public Mono 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 createMenu(ServerRequest request) { // 现有代码保持不变 } ``` **Step 2: 在updateMenu方法上添加注解** ```java @OperationLog(operation = "更新菜单", module = "菜单管理") public Mono updateMenu(ServerRequest request) { // 现有代码保持不变 } ``` **Step 3: 在deleteMenu方法上添加注解** ```java @OperationLog(operation = "删除菜单", module = "菜单管理") public Mono 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