# 操作日志记录功能设计文档

**日期**: 2026-04-03  
**作者**: 张翔  
**版本**: 1.0

## 1. 概述

### 1.1 背景
当前系统的Dashboard操作日志一直显示0，原因是缺少操作日志记录功能。虽然数据库表、服务层和API都已就绪，但没有自动记录用户操作的机制。

### 1.2 目标
实现一个基于注解的操作日志记录功能，自动记录关键业务操作，为系统审计和问题追踪提供数据支持。

### 1.3 范围
只记录关键业务操作，包括：
- 用户管理：创建、更新、删除用户、修改密码、分配角色
- 角色管理：创建、更新、删除角色、分配权限
- 菜单管理：创建、更新、删除菜单
- 系统配置：创建、更新、删除配置
- 数据字典：创建、更新、删除字典
- 公告管理：创建、更新、删除公告

## 2. 架构设计

### 2.1 整体架构

采用**AOP切面 + 注解驱动**的架构：

```
用户请求 → Handler方法（带@OperationLog注解）
    ↓
OperationLogAspect拦截
    ↓
记录开始时间、获取请求参数
    ↓
执行业务方法
    ↓
记录结束时间、获取返回结果
    ↓
异步保存操作日志到数据库
    ↓
返回结果给用户
```

### 2.2 核心组件

1. **`@OperationLog`注解**：标记需要记录日志的方法
2. **`OperationLogAspect`切面**：拦截注解方法，自动记录操作日志
3. **`OperationLogService`服务**：已有的服务层，负责保存日志到数据库
4. **异步处理**：使用Reactor的异步机制，不阻塞主业务流程

### 2.3 关键设计点

- **响应式编程**：使用Reactor的Mono/Flux，与现有WebFlux架构保持一致
- **异步记录**：日志记录不影响主业务流程性能
- **错误容错**：日志记录失败不影响业务方法执行
- **自动获取上下文**：从SecurityContext获取当前用户，从ServerWebExchange获取IP地址

## 3. 详细设计

### 3.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();
}
```

### 3.2 切面实现

```java
@Aspect
@Component
public class OperationLogAspect {
    
    private final IOperationLogService logService;
    private final ObjectMapper objectMapper;
    
    @Around("@annotation(operationLog)")
    public Object around(ProceedingJoinPoint point, OperationLog operationLog) throws Throwable {
        long startTime = System.currentTimeMillis();
        
        // 1. 获取请求信息
        String username = getCurrentUsername();
        String ip = getCurrentIp();
        String method = point.getSignature().toShortString();
        String params = serializeParams(point.getArgs());
        
        // 2. 执行业务方法
        Object result = null;
        String status = "0";  // 0-成功, 1-失败
        String errorMsg = null;
        
        try {
            result = point.proceed();
            
            // 3. 处理响应式结果
            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 void saveLogAsync(OperationLog operationLog, String username, 
                             String ip, String method, String params, 
                             Object result, long duration, String status, 
                             String errorMsg) {
        // 异步保存日志，不阻塞主流程
        Mono.fromRunnable(() -> {
            cn.novalon.manage.sys.core.domain.OperationLog log = 
                new cn.novalon.manage.sys.core.domain.OperationLog();
            log.setUsername(username);
            log.setOperation(operationLog.module() + " - " + operationLog.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).subscribe();
        }).subscribeOn(Schedulers.boundedElastic()).subscribe();
    }
}
```

### 3.3 需要记录的操作

#### 用户管理模块
- `createUser()` - 创建用户
- `updateUser()` - 更新用户
- `deleteUser()` - 删除用户
- `changePassword()` - 修改密码
- `assignRoles()` - 分配角色

#### 角色管理模块
- `createRole()` - 创建角色
- `updateRole()` - 更新角色
- `deleteRole()` - 删除角色
- `assignPermissionsToRole()` - 分配权限

#### 菜单管理模块
- `createMenu()` - 创建菜单
- `updateMenu()` - 更新菜单
- `deleteMenu()` - 删除菜单

#### 其他模块
- 系统配置：创建、更新、删除配置
- 数据字典：创建、更新、删除字典
- 公告管理：创建、更新、删除公告

## 4. 测试策略

### 4.1 单元测试

**`OperationLogAspectTest`**：测试切面的核心逻辑
- 测试成功场景：方法执行成功，日志正确记录
- 测试失败场景：方法抛出异常，日志记录错误信息
- 测试响应式场景：Mono返回值的处理
- 测试上下文获取：用户、IP等信息的正确获取

### 4.2 集成测试

**`OperationLogIntegrationTest`**：测试完整的日志记录流程
- 调用带注解的API接口
- 验证日志是否正确保存到数据库
- 验证日志内容的完整性

### 4.3 E2E测试

在现有E2E测试中验证：
- 执行用户管理操作后，检查Dashboard操作日志数量是否增加
- 验证操作日志显示是否正确

## 5. 部署计划

### 5.1 阶段1：开发与测试（当前）
1. 创建`@OperationLog`注解
2. 实现`OperationLogAspect`切面
3. 编写单元测试和集成测试
4. 在关键Handler方法上添加注解

### 5.2 阶段2：验证
1. 运行所有测试，确保功能正常
2. 手动测试Dashboard操作日志显示
3. 验证日志记录不影响系统性能

### 5.3 阶段3：上线
1. 提交代码到Git
2. 更新文档
3. 部署到开发环境验证

## 6. 性能考虑

- **异步保存**：日志保存使用异步方式，不阻塞主业务流程
- **索引优化**：数据库表已有索引（created_at, username）
- **日志清理**：建议后续添加定时任务清理历史日志（保留最近3个月）

## 7. 风险与缓解

| 风险 | 影响 | 缓解措施 |
|------|------|----------|
| 日志记录失败影响业务 | 高 | 使用异步保存，失败时只记录错误日志，不影响业务流程 |
| 日志量过大影响性能 | 中 | 只记录关键操作，使用异步保存，定期清理历史日志 |
| 敏感信息泄露 | 高 | 参数序列化时排除敏感字段（如password） |

## 8. 后续优化

1. **日志查询优化**：添加更多查询条件（时间范围、操作类型等）
2. **日志导出功能**：支持导出操作日志为Excel
3. **日志统计分析**：统计用户操作频率、操作类型分布等
4. **日志清理任务**：定时清理历史日志，保留最近3个月数据
