From 588493f4c961273e9129dec0a5a6fabf30006731 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E7=BF=94?= Date: Fri, 3 Apr 2026 21:10:01 +0800 Subject: [PATCH] docs: add operation log optimization implementation plan - Break down into 8 tasks across 2 phases - Phase 1: Short-term optimization (1-2 weeks) - Phase 2: Mid-term optimization (1-2 months) - Include detailed steps, code examples, and verification methods - Estimated total time: 22 hours (3-5 working days) --- .../2026-04-03-operation-log-optimization.md | 1318 +++++++++++++++++ 1 file changed, 1318 insertions(+) create mode 100644 docs/plans/2026-04-03-operation-log-optimization.md diff --git a/docs/plans/2026-04-03-operation-log-optimization.md b/docs/plans/2026-04-03-operation-log-optimization.md new file mode 100644 index 0000000..78478d2 --- /dev/null +++ b/docs/plans/2026-04-03-operation-log-optimization.md @@ -0,0 +1,1318 @@ +# 操作日志功能优化实施计划 + +> **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 { + 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 searchOperationLogs(ServerRequest request) { + Optional username = request.queryParam("username"); + Optional operation = request.queryParam("operation"); + Optional startTime = request.queryParam("startTime"); + Optional endTime = request.queryParam("endTime"); + Optional 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 + + + + + +``` + +**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 exportOperationLogs(ServerRequest request) { + // 获取查询条件 + Optional username = request.queryParam("username"); + Optional operation = request.queryParam("operation"); + Optional startTime = request.queryParam("startTime"); + Optional 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 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 + + 查询 + 重置 + 导出 + +``` + +```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 getStatsOverview(ServerRequest request) { + Mono totalCount = logService.count(); + Mono todayCount = logService.countToday(); + Mono successCount = logService.countByStatus("0"); + Mono failCount = logService.countByStatus("1"); + + return Mono.zip(totalCount, todayCount, successCount, failCount) + .flatMap(tuple -> { + Map 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 getStatsByOperation(ServerRequest request) { + return logService.countByOperation() + .collectList() + .flatMap(stats -> ServerResponse.ok().bodyValue(stats)); + } + + @Operation(summary = "按用户统计", description = "统计各用户的操作数量") + public Mono getStatsByUser(ServerRequest request) { + return logService.countByUsername() + .collectList() + .flatMap(stats -> ServerResponse.ok().bodyValue(stats)); + } + + @Operation(summary = "按时间统计", description = "统计每日操作数量趋势") + public Mono getStatsByTime(ServerRequest request) { + Optional 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 + + + + + +``` + +**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 deleteByCreatedAtBefore(LocalDateTime cutoffDate); +``` + +**Step 3: 在OperationLogService中实现删除方法** + +```java +@Override +public Mono deleteByCreatedAtBefore(LocalDateTime cutoffDate) { + return logRepository.deleteByCreatedAtBefore(cutoffDate); +} +``` + +**Step 4: 在IOperationLogRepository中添加删除方法** + +```java +Mono 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个工作日)