# 操作日志功能优化实施计划

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**目标**: 完善操作日志功能，修复已知问题，增强功能特性，提升用户体验和系统可维护性。

**架构**: 在现有注解驱动AOP架构基础上，修复H2数据库兼容性问题，添加集成测试和E2E测试，实现查询导出、统计分析、定时清理等增强功能。

**技术栈**: Java 21, Spring Boot 3.5.13, Spring WebFlux, R2DBC, H2 Database, Jackson, Playwright

---

## Phase 1: 短期优化（1-2周）

### Task 1: 修复H2数据库初始化问题

**问题分析**:
- H2测试环境启动时报错：`bad SQL grammar [SELECT sys_user.username, sys_user.password, sys_user.email, sys_user.phone, sys_user.nickname, sys_user.role_id, sys_user.status...]`
- 原因：实体类字段映射与H2 schema不匹配

**文件:**
- 检查: `novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/entity/SysUserEntity.java`
- 检查: `novalon-manage-api/manage-app/src/main/resources/schema-h2.sql`
- 检查: `novalon-manage-api/manage-app/src/main/resources/data-h2.sql`

**Step 1: 分析实体类字段映射**

运行: 
```bash
cd novalon-manage-api
grep -n "@Column" manage-db/src/main/java/cn/novalon/manage/db/entity/SysUserEntity.java
```

预期: 查看所有字段映射

**Step 2: 对比H2 schema定义**

运行:
```bash
cd novalon-manage-api/manage-app/src/main/resources
head -30 schema-h2.sql
```

预期: 查看H2表结构定义

**Step 3: 检查data-h2.sql中的测试数据**

运行:
```bash
cd novalon-manage-api/manage-app/src/main/resources
grep -A5 "INSERT INTO sys_user" data-h2.sql | head -20
```

预期: 查看测试数据插入语句

**Step 4: 分析问题根源**

根据错误信息和代码检查，确定以下可能的问题：
1. 实体类字段名与数据库列名不匹配
2. H2 schema中缺少某些字段
3. R2DBC映射配置问题

**Step 5: 修复方案选择**

根据分析结果，选择合适的修复方案：
- 方案A: 修改实体类的@Column注解，使其与H2 schema匹配
- 方案B: 修改H2 schema，使其与实体类字段匹配
- 方案C: 添加R2DBC自定义映射配置

**Step 6: 实施修复**

根据选择的方案，修改相应文件。

**Step 7: 验证修复**

运行:
```bash
cd novalon-manage-api
./mvnw spring-boot:run -pl manage-app -Dspring-boot.run.profiles=test
```

等待服务启动，检查是否还有SQL错误。

**Step 8: 提交修复**

```bash
git add <修改的文件>
git commit -m "fix: resolve H2 database initialization issue

- Fix entity field mapping mismatch
- Update H2 schema to match entity definitions
- Ensure test environment works correctly"
```

---

### Task 2: 添加操作日志集成测试

**文件:**
- 创建: `novalon-manage-api/manage-sys/src/test/java/cn/novalon/manage/sys/audit/OperationLogIntegrationTest.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 org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.reactive.server.WebTestClient;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;

import java.time.Duration;

import static org.junit.jupiter.api.Assertions.*;

/**
 * 操作日志集成测试
 * 
 * @author 张翔
 * @date 2026-04-03
 */
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
class OperationLogIntegrationTest {

    @Autowired
    private WebTestClient webTestClient;

    @Autowired
    private IOperationLogService logService;

    @Test
    @WithMockUser(username = "test_user", roles = {"admin"})
    void testCreateUserOperation_ShouldLogOperation() {
        long initialCount = logService.count().block(Duration.ofSeconds(5));

        String userJson = """
            {
                "username": "test_integration_user",
                "password": "Test123!@#",
                "email": "test@example.com",
                "phone": "13900139000",
                "nickname": "集成测试用户"
            }
            """;

        webTestClient.post()
                .uri("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .bodyValue(userJson)
                .exchange()
                .expectStatus().isCreated();

        // 等待异步日志保存
        StepVerifier.create(logService.count())
                .expectNext(initialCount + 1)
                .verify(Duration.ofSeconds(5));

        // 验证日志内容
        StepVerifier.create(logService.findAll().last())
                .assertNext(log -> {
                    assertEquals("test_user", log.getUsername());
                    assertTrue(log.getOperation().contains("创建用户"));
                    assertEquals("0", log.getStatus());
                    assertNotNull(log.getParams());
                    assertNotNull(log.getDuration());
                })
                .verify(Duration.ofSeconds(5));
    }

    @Test
    @WithMockUser(username = "test_user", roles = {"admin"})
    void testDeleteUserOperation_ShouldLogOperation() {
        // 先创建一个用户
        String userJson = """
            {
                "username": "test_delete_user",
                "password": "Test123!@#",
                "email": "delete@example.com",
                "phone": "13900139001",
                "nickname": "待删除用户"
            }
            """;

        Long userId = webTestClient.post()
                .uri("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .bodyValue(userJson)
                .exchange()
                .expectStatus().isCreated()
                .expectBody(Long.class)
                .returnResult()
                .getResponseBody();

        long initialCount = logService.count().block(Duration.ofSeconds(5));

        // 删除用户
        webTestClient.delete()
                .uri("/api/users/{id}", userId)
                .exchange()
                .expectStatus().isOk();

        // 验证日志记录
        StepVerifier.create(logService.count())
                .expectNext(initialCount + 1)
                .verify(Duration.ofSeconds(5));
    }

    @Test
    @WithMockUser(username = "test_user", roles = {"admin"})
    void testFailedOperation_ShouldLogError() {
        // 尝试创建重复用户名（应该失败）
        String userJson = """
            {
                "username": "admin",
                "password": "Test123!@#",
                "email": "duplicate@example.com",
                "phone": "13900139002",
                "nickname": "重复用户"
            }
            """;

        webTestClient.post()
                .uri("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .bodyValue(userJson)
                .exchange()
                .expectStatus().is4xxClientError();

        // 验证错误日志记录
        StepVerifier.create(logService.findAll().last())
                .assertNext(log -> {
                    assertEquals("1", log.getStatus());
                    assertNotNull(log.getErrorMsg());
                })
                .verify(Duration.ofSeconds(5));
    }
}
```

**Step 2: 运行集成测试**

运行:
```bash
cd novalon-manage-api
./mvnw test -Dtest=OperationLogIntegrationTest -pl manage-sys
```

预期: 所有测试通过

**Step 3: 提交测试代码**

```bash
git add novalon-manage-api/manage-sys/src/test/java/cn/novalon/manage/sys/audit/OperationLogIntegrationTest.java
git commit -m "test: add integration tests for operation log

- Test successful operation logging
- Test failed operation error logging
- Verify log content and status"
```

---

### Task 3: 添加操作日志E2E测试

**文件:**
- 创建: `novalon-manage-web/e2e/operation-log.spec.ts`

**Step 1: 创建E2E测试文件**

```typescript
import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
import { DashboardPage } from './pages/DashboardPage';
import { UserManagementPage } from './pages/UserManagementPage';

test.describe('操作日志E2E测试', () => {
  let loginPage: LoginPage;
  let dashboardPage: DashboardPage;
  let userManagementPage: UserManagementPage;

  test.beforeEach(async ({ page }) => {
    loginPage = new LoginPage(page);
    dashboardPage = new DashboardPage(page);
    userManagementPage = new UserManagementPage(page);

    // 清理localStorage
    await page.goto('/');
    await page.evaluate(() => localStorage.clear());

    // 登录
    await loginPage.goto();
    await loginPage.login('e2e_test_user', 'admin123');
  });

  test('创建用户后Dashboard操作日志数量应增加', async ({ page }) => {
    // 获取初始操作日志数量
    await dashboardPage.goto();
    const initialCount = await dashboardPage.getOperationLogCount();
    console.log('初始操作日志数量:', initialCount);

    // 创建用户
    await dashboardPage.navigateToUserManagement();
    await userManagementPage.clickCreateUser();
    
    const timestamp = Date.now();
    const userData = {
      username: `oplog_test_${timestamp}`,
      nickname: `操作日志测试${timestamp}`,
      email: `oplog_${timestamp}@example.com`,
      phone: '13800138000',
      password: 'Test123!@#',
      confirmPassword: 'Test123!@#',
    };
    
    await userManagementPage.fillUserForm(userData);
    await userManagementPage.submitForm();
    await expect(userManagementPage.successMessage).toBeVisible();

    // 返回Dashboard，验证操作日志数量
    await dashboardPage.goto();
    await page.waitForTimeout(2000); // 等待异步日志保存
    
    const newCount = await dashboardPage.getOperationLogCount();
    console.log('新操作日志数量:', newCount);
    
    expect(newCount).toBeGreaterThan(initialCount);
  });

  test('删除用户后Dashboard操作日志数量应增加', async ({ page }) => {
    // 先创建一个用户
    await dashboardPage.navigateToUserManagement();
    await userManagementPage.clickCreateUser();
    
    const timestamp = Date.now();
    const userData = {
      username: `delete_oplog_${timestamp}`,
      nickname: `待删除用户${timestamp}`,
      email: `delete_${timestamp}@example.com`,
      phone: '13800138001',
      password: 'Test123!@#',
      confirmPassword: 'Test123!@#',
    };
    
    await userManagementPage.fillUserForm(userData);
    await userManagementPage.submitForm();
    await expect(userManagementPage.successMessage).toBeVisible();

    // 获取初始操作日志数量
    await dashboardPage.goto();
    const initialCount = await dashboardPage.getOperationLogCount();

    // 删除用户
    await dashboardPage.navigateToUserManagement();
    await userManagementPage.search(userData.username);
    await page.waitForTimeout(1000);
    await userManagementPage.deleteUser(1);
    await userManagementPage.confirmDelete();
    await expect(userManagementPage.successMessage).toBeVisible();

    // 验证操作日志数量
    await dashboardPage.goto();
    await page.waitForTimeout(2000);
    
    const newCount = await dashboardPage.getOperationLogCount();
    expect(newCount).toBeGreaterThan(initialCount);
  });

  test('操作失败后应记录错误日志', async ({ page }) => {
    // 获取初始操作日志数量
    await dashboardPage.goto();
    const initialCount = await dashboardPage.getOperationLogCount();

    // 尝试创建重复用户（应该失败）
    await dashboardPage.navigateToUserManagement();
    await userManagementPage.clickCreateUser();
    
    const userData = {
      username: 'admin', // 已存在的用户名
      nickname: '重复用户',
      email: 'duplicate@example.com',
      phone: '13800138002',
      password: 'Test123!@#',
      confirmPassword: 'Test123!@#',
    };
    
    await userManagementPage.fillUserForm(userData);
    await userManagementPage.submitForm();
    
    // 应该看到错误消息
    await expect(userManagementPage.errorMessage).toBeVisible();

    // 验证操作日志数量（失败操作也应该记录）
    await dashboardPage.goto();
    await page.waitForTimeout(2000);
    
    const newCount = await dashboardPage.getOperationLogCount();
    expect(newCount).toBeGreaterThan(initialCount);
  });
});
```

**Step 2: 更新DashboardPage添加getOperationLogCount方法**

在 `novalon-manage-web/e2e/pages/DashboardPage.ts` 中添加：

```typescript
async getOperationLogCount(): Promise<number> {
  const logCard = this.page.locator('.log-card .el-statistic__content');
  const text = await logCard.textContent();
  return parseInt(text || '0', 10);
}
```

**Step 3: 运行E2E测试**

运行:
```bash
cd novalon-manage-web
npx playwright test e2e/operation-log.spec.ts --project=chromium
```

预期: 所有测试通过

**Step 4: 提交测试代码**

```bash
git add novalon-manage-web/e2e/operation-log.spec.ts novalon-manage-web/e2e/pages/DashboardPage.ts
git commit -m "test: add E2E tests for operation log

- Verify operation log count increases after operations
- Test successful and failed operation logging
- Add getOperationLogCount method to DashboardPage"
```

---

### Task 4: 验证Dashboard操作日志显示

**文件:**
- 检查: `novalon-manage-web/src/views/system/Dashboard.vue`

**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: 手动测试Dashboard显示**

1. 打开浏览器访问 http://localhost:3002
2. 登录系统（用户名: e2e_test_user, 密码: admin123）
3. 查看Dashboard操作日志数量（初始值）
4. 执行用户管理操作（创建、更新、删除用户）
5. 返回Dashboard，查看操作日志数量是否增加
6. 检查操作日志显示是否正确

**Step 3: 检查API响应**

运行:
```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 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" | jq '.[0]'
```

预期: 返回最新的操作日志记录

**Step 5: 记录测试结果**

创建测试报告文档，记录：
- Dashboard显示是否正常
- 操作日志数量是否正确
- 操作日志内容是否完整
- 发现的问题和解决方案

**Step 6: 提交验证报告**

```bash
git add test-suite/reports/dashboard_operation_log_verification.md
git commit -m "docs: add Dashboard operation log verification report

- Verify Dashboard displays operation log count correctly
- Confirm operation log content is complete
- Document test results and findings"
```

---

## Phase 2: 中期优化（1-2个月）

### Task 5: 添加操作日志查询功能

**文件:**
- 修改: `novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/handler/log/OperationLogHandler.java`
- 创建: `novalon-manage-web/src/views/system/OperationLog.vue`

**Step 1: 扩展后端查询接口**

在 `OperationLogHandler.java` 中添加：

```java
@Operation(summary = "根据条件查询操作日志", description = "支持按用户名、操作类型、时间范围等条件查询")
public Mono<ServerResponse> searchOperationLogs(ServerRequest request) {
    Optional<String> username = request.queryParam("username");
    Optional<String> operation = request.queryParam("operation");
    Optional<String> startTime = request.queryParam("startTime");
    Optional<String> endTime = request.queryParam("endTime");
    Optional<String> status = request.queryParam("status");
    
    // 构建查询条件
    OperationLogQuery query = new OperationLogQuery();
    username.ifPresent(query::setUsername);
    operation.ifPresent(query::setOperation);
    startTime.ifPresent(query::setStartTime);
    endTime.ifPresent(query::setEndTime);
    status.ifPresent(query::setStatus);
    
    return logService.search(query)
            .collectList()
            .flatMap(logs -> ServerResponse.ok().bodyValue(logs));
}
```

**Step 2: 在SystemRouter中添加路由**

在 `SystemRouter.java` 中添加：

```java
.GET("/api/logs/operation/search", operationLogHandler::searchOperationLogs)
```

**Step 3: 创建前端查询页面**

创建 `OperationLog.vue`:

```vue
<template>
  <div class="operation-log">
    <el-card>
      <template #header>
        <div class="card-header">
          <span>操作日志查询</span>
        </div>
      </template>
      
      <el-form :model="searchForm" inline>
        <el-form-item label="用户名">
          <el-input v-model="searchForm.username" placeholder="请输入用户名" />
        </el-form-item>
        <el-form-item label="操作类型">
          <el-select v-model="searchForm.operation" placeholder="请选择操作类型">
            <el-option label="全部" value="" />
            <el-option label="创建用户" value="创建用户" />
            <el-option label="更新用户" value="更新用户" />
            <el-option label="删除用户" value="删除用户" />
          </el-select>
        </el-form-item>
        <el-form-item label="时间范围">
          <el-date-picker
            v-model="searchForm.timeRange"
            type="datetimerange"
            range-separator="至"
            start-placeholder="开始时间"
            end-placeholder="结束时间"
          />
        </el-form-item>
        <el-form-item label="状态">
          <el-select v-model="searchForm.status" placeholder="请选择状态">
            <el-option label="全部" value="" />
            <el-option label="成功" value="0" />
            <el-option label="失败" value="1" />
          </el-select>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="handleSearch">查询</el-button>
          <el-button @click="handleReset">重置</el-button>
        </el-form-item>
      </el-form>
      
      <el-table :data="tableData" v-loading="loading">
        <el-table-column prop="username" label="用户名" width="120" />
        <el-table-column prop="operation" label="操作" width="180" />
        <el-table-column prop="method" label="方法" show-overflow-tooltip />
        <el-table-column prop="ip" label="IP地址" width="140" />
        <el-table-column prop="duration" label="耗时(ms)" width="100" />
        <el-table-column prop="status" label="状态" width="80">
          <template #default="{ row }">
            <el-tag :type="row.status === '0' ? 'success' : 'danger'">
              {{ row.status === '0' ? '成功' : '失败' }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column prop="createdAt" label="操作时间" width="180" />
        <el-table-column label="操作" width="100">
          <template #default="{ row }">
            <el-button link type="primary" @click="handleViewDetail(row)">详情</el-button>
          </template>
        </el-table-column>
      </el-table>
      
      <el-pagination
        v-model:current-page="pagination.page"
        v-model:page-size="pagination.size"
        :total="pagination.total"
        :page-sizes="[10, 20, 50, 100]"
        layout="total, sizes, prev, pager, next, jumper"
        @size-change="handleSearch"
        @current-change="handleSearch"
      />
    </el-card>
    
    <el-dialog v-model="detailVisible" title="操作日志详情" width="60%">
      <el-descriptions :column="2" border>
        <el-descriptions-item label="用户名">{{ currentLog.username }}</el-descriptions-item>
        <el-descriptions-item label="操作">{{ currentLog.operation }}</el-descriptions-item>
        <el-descriptions-item label="方法">{{ currentLog.method }}</el-descriptions-item>
        <el-descriptions-item label="IP地址">{{ currentLog.ip }}</el-descriptions-item>
        <el-descriptions-item label="耗时">{{ currentLog.duration }}ms</el-descriptions-item>
        <el-descriptions-item label="状态">
          <el-tag :type="currentLog.status === '0' ? 'success' : 'danger'">
            {{ currentLog.status === '0' ? '成功' : '失败' }}
          </el-tag>
        </el-descriptions-item>
        <el-descriptions-item label="操作时间" :span="2">{{ currentLog.createdAt }}</el-descriptions-item>
        <el-descriptions-item label="参数" :span="2">
          <pre>{{ formatJson(currentLog.params) }}</pre>
        </el-descriptions-item>
        <el-descriptions-item label="结果" :span="2">
          <pre>{{ formatJson(currentLog.result) }}</pre>
        </el-descriptions-item>
        <el-descriptions-item v-if="currentLog.errorMsg" label="错误信息" :span="2">
          <el-alert type="error" :closable="false">{{ currentLog.errorMsg }}</el-alert>
        </el-descriptions-item>
      </el-descriptions>
    </el-dialog>
  </div>
</template>

<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import request from '@/utils/request'

const loading = ref(false)
const tableData = ref([])
const detailVisible = ref(false)
const currentLog = ref<any>({})

const searchForm = reactive({
  username: '',
  operation: '',
  timeRange: [],
  status: ''
})

const pagination = reactive({
  page: 1,
  size: 10,
  total: 0
})

const handleSearch = async () => {
  loading.value = true
  try {
    const params: any = {
      page: pagination.page,
      size: pagination.size,
      ...searchForm
    }
    
    if (searchForm.timeRange && searchForm.timeRange.length === 2) {
      params.startTime = searchForm.timeRange[0]
      params.endTime = searchForm.timeRange[1]
    }
    
    const res: any = await request.get('/logs/operation/search', { params })
    tableData.value = res.data || []
    pagination.total = res.total || 0
  } catch (error) {
    console.error('查询失败:', error)
  } finally {
    loading.value = false
  }
}

const handleReset = () => {
  Object.assign(searchForm, {
    username: '',
    operation: '',
    timeRange: [],
    status: ''
  })
  pagination.page = 1
  handleSearch()
}

const handleViewDetail = (row: any) => {
  currentLog.value = row
  detailVisible.value = true
}

const formatJson = (jsonStr: string) => {
  try {
    return JSON.stringify(JSON.parse(jsonStr), null, 2)
  } catch {
    return jsonStr
  }
}

onMounted(() => {
  handleSearch()
})
</script>

<style scoped lang="css">
.operation-log {
  padding: 20px;
}

.card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

pre {
  background: #f5f5f5;
  padding: 10px;
  border-radius: 4px;
  overflow-x: auto;
  max-height: 300px;
}
</style>
```

**Step 4: 添加路由配置**

在 `router/index.ts` 中添加：

```typescript
{
  path: '/system/operation-log',
  name: 'OperationLog',
  component: () => import('@/views/system/OperationLog.vue'),
  meta: { title: '操作日志', icon: 'document' }
}
```

**Step 5: 测试查询功能**

运行:
```bash
# 启动服务
cd novalon-manage-api && ./mvnw spring-boot:run -pl manage-app -Dspring-boot.run.profiles=test
cd novalon-manage-web && pnpm dev

# 浏览器访问
http://localhost:3002/system/operation-log
```

**Step 6: 提交代码**

```bash
git add novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/handler/log/OperationLogHandler.java
git add novalon-manage-api/manage-app/src/main/java/cn/novalon/manage/app/config/SystemRouter.java
git add novalon-manage-web/src/views/system/OperationLog.vue
git add novalon-manage-web/src/router/index.ts
git commit -m "feat: add operation log search functionality

- Add search API with multiple filter conditions
- Create operation log query page
- Support pagination and detail view"
```

---

### Task 6: 添加操作日志导出功能

**文件:**
- 修改: `novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/handler/log/OperationLogHandler.java`
- 修改: `novalon-manage-web/src/views/system/OperationLog.vue`

**Step 1: 添加后端导出接口**

在 `OperationLogHandler.java` 中添加：

```java
@Operation(summary = "导出操作日志", description = "导出操作日志为Excel文件")
public Mono<ServerResponse> exportOperationLogs(ServerRequest request) {
    // 获取查询条件
    Optional<String> username = request.queryParam("username");
    Optional<String> operation = request.queryParam("operation");
    Optional<String> startTime = request.queryParam("startTime");
    Optional<String> endTime = request.queryParam("endTime");
    
    // 构建查询条件
    OperationLogQuery query = new OperationLogQuery();
    username.ifPresent(query::setUsername);
    operation.ifPresent(query::setOperation);
    startTime.ifPresent(query::setStartTime);
    endTime.ifPresent(query::setEndTime);
    
    return logService.search(query)
            .collectList()
            .flatMap(logs -> {
                // 生成Excel文件
                byte[] excelData = generateExcel(logs);
                
                return ServerResponse.ok()
                        .header("Content-Disposition", "attachment; filename=operation_logs.xlsx")
                        .contentType(MediaType.APPLICATION_OCTET_STREAM)
                        .bodyValue(excelData);
            });
}

private byte[] generateExcel(List<OperationLog> logs) {
    // 使用Apache POI或EasyExcel生成Excel
    // 实现细节省略
    return new byte[0];
}
```

**Step 2: 在SystemRouter中添加路由**

```java
.GET("/api/logs/operation/export", operationLogHandler::exportOperationLogs)
```

**Step 3: 在前端添加导出按钮**

在 `OperationLog.vue` 中添加：

```vue
<el-form-item>
  <el-button type="primary" @click="handleSearch">查询</el-button>
  <el-button @click="handleReset">重置</el-button>
  <el-button type="success" @click="handleExport">导出</el-button>
</el-form-item>
```

```typescript
const handleExport = async () => {
  try {
    const params: any = { ...searchForm }
    
    if (searchForm.timeRange && searchForm.timeRange.length === 2) {
      params.startTime = searchForm.timeRange[0]
      params.endTime = searchForm.timeRange[1]
    }
    
    const response = await request.get('/logs/operation/export', {
      params,
      responseType: 'blob'
    })
    
    const url = window.URL.createObjectURL(new Blob([response]))
    const link = document.createElement('a')
    link.href = url
    link.setAttribute('download', 'operation_logs.xlsx')
    document.body.appendChild(link)
    link.click()
    document.body.removeChild(link)
  } catch (error) {
    console.error('导出失败:', error)
  }
}
```

**Step 4: 测试导出功能**

在浏览器中点击"导出"按钮，验证Excel文件是否正确下载。

**Step 5: 提交代码**

```bash
git add novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/handler/log/OperationLogHandler.java
git add novalon-manage-api/manage-app/src/main/java/cn/novalon/manage/app/config/SystemRouter.java
git add novalon-manage-web/src/views/system/OperationLog.vue
git commit -m "feat: add operation log export functionality

- Add export API endpoint
- Generate Excel file with operation logs
- Add export button in frontend"
```

---

### Task 7: 实现操作日志统计分析

**文件:**
- 创建: `novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/handler/stats/OperationLogStatsHandler.java`
- 创建: `novalon-manage-web/src/views/system/OperationLogStats.vue`

**Step 1: 创建统计分析接口**

```java
package cn.novalon.manage.sys.handler.stats;

import cn.novalon.manage.sys.core.service.IOperationLogService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;

@Component
@Tag(name = "操作日志统计", description = "操作日志统计分析")
public class OperationLogStatsHandler {

    private final IOperationLogService logService;

    public OperationLogStatsHandler(IOperationLogService logService) {
        this.logService = logService;
    }

    @Operation(summary = "获取操作日志统计概览", description = "获取操作日志的统计数据")
    public Mono<ServerResponse> getStatsOverview(ServerRequest request) {
        Mono<Long> totalCount = logService.count();
        Mono<Long> todayCount = logService.countToday();
        Mono<Long> successCount = logService.countByStatus("0");
        Mono<Long> failCount = logService.countByStatus("1");

        return Mono.zip(totalCount, todayCount, successCount, failCount)
                .flatMap(tuple -> {
                    Map<String, Object> stats = new HashMap<>();
                    stats.put("totalCount", tuple.getT1());
                    stats.put("todayCount", tuple.getT2());
                    stats.put("successCount", tuple.getT3());
                    stats.put("failCount", tuple.getT4());
                    stats.put("successRate", 
                            tuple.getT1() > 0 ? 
                            (double) tuple.getT3() / tuple.getT1() * 100 : 0);
                    return ServerResponse.ok().bodyValue(stats);
                });
    }

    @Operation(summary = "按操作类型统计", description = "统计各操作类型的数量")
    public Mono<ServerResponse> getStatsByOperation(ServerRequest request) {
        return logService.countByOperation()
                .collectList()
                .flatMap(stats -> ServerResponse.ok().bodyValue(stats));
    }

    @Operation(summary = "按用户统计", description = "统计各用户的操作数量")
    public Mono<ServerResponse> getStatsByUser(ServerRequest request) {
        return logService.countByUsername()
                .collectList()
                .flatMap(stats -> ServerResponse.ok().bodyValue(stats));
    }

    @Operation(summary = "按时间统计", description = "统计每日操作数量趋势")
    public Mono<ServerResponse> getStatsByTime(ServerRequest request) {
        Optional<String> days = request.queryParam("days");
        int dayCount = days.map(Integer::parseInt).orElse(7);
        
        LocalDateTime startTime = LocalDateTime.now().minusDays(dayCount);
        return logService.countByDate(startTime)
                .collectList()
                .flatMap(stats -> ServerResponse.ok().bodyValue(stats));
    }
}
```

**Step 2: 创建前端统计页面**

创建 `OperationLogStats.vue`:

```vue
<template>
  <div class="operation-log-stats">
    <el-row :gutter="16">
      <el-col :span="6">
        <el-card>
          <el-statistic title="总操作数" :value="stats.totalCount" />
        </el-card>
      </el-col>
      <el-col :span="6">
        <el-card>
          <el-statistic title="今日操作" :value="stats.todayCount" />
        </el-card>
      </el-col>
      <el-col :span="6">
        <el-card>
          <el-statistic title="成功率" :value="stats.successRate" suffix="%" />
        </el-card>
      </el-col>
      <el-col :span="6">
        <el-card>
          <el-statistic title="失败数" :value="stats.failCount" />
        </el-card>
      </el-col>
    </el-row>

    <el-row :gutter="16" style="margin-top: 16px">
      <el-col :span="12">
        <el-card>
          <template #header>
            <span>操作类型分布</span>
          </template>
          <div ref="operationChart" style="height: 300px"></div>
        </el-card>
      </el-col>
      <el-col :span="12">
        <el-card>
          <template #header>
            <span>用户操作排行</span>
          </template>
          <div ref="userChart" style="height: 300px"></div>
        </el-card>
      </el-col>
    </el-row>

    <el-card style="margin-top: 16px">
      <template #header>
        <span>操作趋势（最近7天）</span>
      </template>
      <div ref="trendChart" style="height: 300px"></div>
    </el-card>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import * as echarts from 'echarts'
import request from '@/utils/request'

const stats = ref({
  totalCount: 0,
  todayCount: 0,
  successRate: 0,
  failCount: 0
})

const operationChart = ref<HTMLElement>()
const userChart = ref<HTMLElement>()
const trendChart = ref<HTMLElement>()

const loadStats = async () => {
  const res: any = await request.get('/logs/operation/stats/overview')
  stats.value = res
}

const loadOperationChart = async () => {
  const res: any = await request.get('/logs/operation/stats/by-operation')
  
  const chart = echarts.init(operationChart.value!)
  chart.setOption({
    tooltip: { trigger: 'item' },
    legend: { orient: 'vertical', left: 'left' },
    series: [{
      type: 'pie',
      radius: '50%',
      data: res.map((item: any) => ({
        name: item.operation,
        value: item.count
      }))
    }]
  })
}

const loadUserChart = async () => {
  const res: any = await request.get('/logs/operation/stats/by-user')
  
  const chart = echarts.init(userChart.value!)
  chart.setOption({
    tooltip: { trigger: 'axis' },
    xAxis: { type: 'category', data: res.map((item: any) => item.username) },
    yAxis: { type: 'value' },
    series: [{
      type: 'bar',
      data: res.map((item: any) => item.count)
    }]
  })
}

const loadTrendChart = async () => {
  const res: any = await request.get('/logs/operation/stats/by-time?days=7')
  
  const chart = echarts.init(trendChart.value!)
  chart.setOption({
    tooltip: { trigger: 'axis' },
    xAxis: { 
      type: 'category', 
      data: res.map((item: any) => item.date) 
    },
    yAxis: { type: 'value' },
    series: [{
      type: 'line',
      data: res.map((item: any) => item.count),
      smooth: true
    }]
  })
}

onMounted(async () => {
  await loadStats()
  await loadOperationChart()
  await loadUserChart()
  await loadTrendChart()
})
</script>

<style scoped lang="css">
.operation-log-stats {
  padding: 20px;
}
</style>
```

**Step 3: 添加路由**

在 `SystemRouter.java` 中添加：

```java
.GET("/api/logs/operation/stats/overview", operationLogStatsHandler::getStatsOverview)
.GET("/api/logs/operation/stats/by-operation", operationLogStatsHandler::getStatsByOperation)
.GET("/api/logs/operation/stats/by-user", operationLogStatsHandler::getStatsByUser)
.GET("/api/logs/operation/stats/by-time", operationLogStatsHandler::getStatsByTime)
```

**Step 4: 测试统计功能**

访问 http://localhost:3002/system/operation-log-stats 查看统计图表。

**Step 5: 提交代码**

```bash
git add novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/handler/stats/OperationLogStatsHandler.java
git add novalon-manage-api/manage-app/src/main/java/cn/novalon/manage/app/config/SystemRouter.java
git add novalon-manage-web/src/views/system/OperationLogStats.vue
git commit -m "feat: add operation log statistics and analysis

- Add stats API endpoints
- Create statistics dashboard with charts
- Support operation type, user, and time analysis"
```

---

### Task 8: 添加操作日志定时清理任务

**文件:**
- 创建: `novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/scheduler/OperationLogCleanupScheduler.java`

**Step 1: 创建定时清理任务**

```java
package cn.novalon.manage.sys.scheduler;

import cn.novalon.manage.sys.core.service.IOperationLogService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;

/**
 * 操作日志定时清理任务
 * 
 * @author 张翔
 * @date 2026-04-03
 */
@Component
public class OperationLogCleanupScheduler {

    private static final Logger logger = LoggerFactory.getLogger(OperationLogCleanupScheduler.class);

    private final IOperationLogService logService;

    public OperationLogCleanupScheduler(IOperationLogService logService) {
        this.logService = logService;
    }

    /**
     * 每天凌晨2点清理3个月前的操作日志
     */
    @Scheduled(cron = "0 0 2 * * ?")
    public void cleanupOldLogs() {
        logger.info("开始清理操作日志...");
        
        LocalDateTime cutoffDate = LocalDateTime.now().minusMonths(3);
        
        logService.deleteByCreatedAtBefore(cutoffDate)
                .doOnSuccess(count -> logger.info("操作日志清理完成，删除 {} 条记录", count))
                .doOnError(error -> logger.error("操作日志清理失败: {}", error.getMessage(), error))
                .subscribe();
    }
}
```

**Step 2: 在IOperationLogService中添加删除方法**

```java
Mono<Long> deleteByCreatedAtBefore(LocalDateTime cutoffDate);
```

**Step 3: 在OperationLogService中实现删除方法**

```java
@Override
public Mono<Long> deleteByCreatedAtBefore(LocalDateTime cutoffDate) {
    return logRepository.deleteByCreatedAtBefore(cutoffDate);
}
```

**Step 4: 在IOperationLogRepository中添加删除方法**

```java
Mono<Long> deleteByCreatedAtBefore(LocalDateTime cutoffDate);
```

**Step 5: 启用定时任务**

在 `ManageApplication.java` 中添加：

```java
@EnableScheduling
@SpringBootApplication
public class ManageApplication {
    public static void main(String[] args) {
        SpringApplication.run(ManageApplication.class, args);
    }
}
```

**Step 6: 测试定时任务**

可以手动触发测试：

```java
@Test
void testCleanupScheduler() {
    cleanupScheduler.cleanupOldLogs();
    Thread.sleep(5000); // 等待异步操作完成
}
```

**Step 7: 提交代码**

```bash
git add novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/scheduler/OperationLogCleanupScheduler.java
git add novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/core/service/IOperationLogService.java
git add novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/core/service/impl/OperationLogService.java
git add novalon-manage-api/manage-db/src/main/java/cn/novalon/manage/db/repository/IOperationLogRepository.java
git add novalon-manage-api/manage-app/src/main/java/cn/novalon/manage/app/ManageApplication.java
git commit -m "feat: add operation log cleanup scheduler

- Add scheduled task to clean up old logs
- Keep logs for last 3 months
- Run cleanup at 2 AM daily"
```

---

## 完成标准

### Phase 1 完成标准
- ✅ H2数据库初始化问题已修复
- ✅ 集成测试全部通过
- ✅ E2E测试全部通过
- ✅ Dashboard操作日志显示正常
- ✅ 所有代码已提交到Git

### Phase 2 完成标准
- ✅ 操作日志查询功能可用
- ✅ 操作日志导出功能可用
- ✅ 操作日志统计分析功能可用
- ✅ 定时清理任务正常运行
- ✅ 所有测试通过
- ✅ 文档更新完成
- ✅ 所有代码已提交到Git

---

## 预估时间

### Phase 1: 短期优化
- Task 1: 修复H2数据库问题 - 2小时
- Task 2: 添加集成测试 - 3小时
- Task 3: 添加E2E测试 - 2小时
- Task 4: 验证Dashboard显示 - 1小时
- **总计**: 约8小时（1-2个工作日）

### Phase 2: 中期优化
- Task 5: 添加查询功能 - 4小时
- Task 6: 添加导出功能 - 3小时
- Task 7: 实现统计分析 - 5小时
- Task 8: 添加定时清理 - 2小时
- **总计**: 约14小时（2-3个工作日）

**总预估时间**: 约22小时（3-5个工作日）
