From 2bdf5a6c903d6ff64eec7018efb4a3a23532af38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E7=BF=94?= Date: Fri, 3 Apr 2026 19:28:48 +0800 Subject: [PATCH] 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-operation-log-implementation.md | 672 ++++++++++++++++++ 1 file changed, 672 insertions(+) create mode 100644 docs/plans/2026-04-03-operation-log-implementation.md diff --git a/docs/plans/2026-04-03-operation-log-implementation.md b/docs/plans/2026-04-03-operation-log-implementation.md new file mode 100644 index 0000000..40ad030 --- /dev/null +++ b/docs/plans/2026-04-03-operation-log-implementation.md @@ -0,0 +1,672 @@ +# 操作日志记录功能实施计划 + +> **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