# 操作日志记录功能实施计划

> **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
