diff --git a/src/MilitaryTrainingManagement/Controllers/AuditLogsController.cs b/src/MilitaryTrainingManagement/Controllers/AuditLogsController.cs new file mode 100644 index 0000000..12f2457 --- /dev/null +++ b/src/MilitaryTrainingManagement/Controllers/AuditLogsController.cs @@ -0,0 +1,56 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using MilitaryTrainingManagement.Services.Interfaces; + +namespace MilitaryTrainingManagement.Controllers; + +/// +/// 审计日志控制器 +/// +[Authorize] +public class AuditLogsController : BaseApiController +{ + private readonly IAuditService _auditService; + + public AuditLogsController(IAuditService auditService) + { + _auditService = auditService; + } + + /// + /// 获取审计日志列表(分页) + /// + [HttpGet] + public async Task GetAll([FromQuery] AuditLogQueryParameters parameters) + { + var logs = await _auditService.GetLogsAsync(parameters); + var totalCount = await _auditService.GetLogCountAsync(parameters); + + var pageSize = parameters.PageSize; + var pageNumber = parameters.PageNumber; + var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize); + + return Ok(new + { + items = logs, + totalCount, + pageNumber, + pageSize, + totalPages + }); + } + + /// + /// 根据ID获取审计日志详情 + /// + [HttpGet("{id}")] + public async Task GetById(int id) + { + var log = await _auditService.GetLogByIdAsync(id); + if (log == null) + { + return NotFound(new { message = "审计日志不存在" }); + } + return Ok(log); + } +} diff --git a/src/MilitaryTrainingManagement/Data/AuditInterceptor.cs b/src/MilitaryTrainingManagement/Data/AuditInterceptor.cs index 44c8a19..1d73c70 100644 --- a/src/MilitaryTrainingManagement/Data/AuditInterceptor.cs +++ b/src/MilitaryTrainingManagement/Data/AuditInterceptor.cs @@ -47,11 +47,11 @@ public class AuditInterceptor : SaveChangesInterceptor if (httpContext != null) { - var userIdClaim = httpContext.User.FindFirst("UserId")?.Value; + var userIdClaim = httpContext.User.FindFirst("userId")?.Value; if (int.TryParse(userIdClaim, out var parsedUserId)) userId = parsedUserId; - var unitIdClaim = httpContext.User.FindFirst("OrganizationalUnitId")?.Value; + var unitIdClaim = httpContext.User.FindFirst("unitId")?.Value; if (int.TryParse(unitIdClaim, out var parsedUnitId)) organizationalUnitId = parsedUnitId; diff --git a/src/MilitaryTrainingManagement/Services/Implementations/AuditService.cs b/src/MilitaryTrainingManagement/Services/Implementations/AuditService.cs index f04ceac..e8b6dd2 100644 --- a/src/MilitaryTrainingManagement/Services/Implementations/AuditService.cs +++ b/src/MilitaryTrainingManagement/Services/Implementations/AuditService.cs @@ -205,6 +205,45 @@ public class AuditService : IAuditService .ToListAsync(); } + public async Task GetLogCountAsync(AuditLogQueryParameters parameters) + { + var query = _context.AuditLogs.AsQueryable(); + + if (!string.IsNullOrEmpty(parameters.EntityType)) + query = query.Where(l => l.EntityType == parameters.EntityType); + + if (parameters.EntityId.HasValue) + query = query.Where(l => l.EntityId == parameters.EntityId.Value); + + if (!string.IsNullOrEmpty(parameters.Action)) + query = query.Where(l => l.Action == parameters.Action); + + if (parameters.UserId.HasValue) + query = query.Where(l => l.UserId == parameters.UserId.Value); + + if (parameters.OrganizationalUnitId.HasValue) + query = query.Where(l => l.OrganizationalUnitId == parameters.OrganizationalUnitId.Value); + + if (parameters.FromDate.HasValue) + query = query.Where(l => l.Timestamp >= parameters.FromDate.Value); + + if (parameters.ToDate.HasValue) + query = query.Where(l => l.Timestamp <= parameters.ToDate.Value); + + if (parameters.IsSuccess.HasValue) + query = query.Where(l => l.IsSuccess == parameters.IsSuccess.Value); + + return await query.CountAsync(); + } + + public async Task GetLogByIdAsync(int id) + { + return await _context.AuditLogs + .Include(l => l.User) + .Include(l => l.OrganizationalUnit) + .FirstOrDefaultAsync(l => l.Id == id); + } + public async Task> GetLogsAsync(string? entityType = null, int? entityId = null, DateTime? fromDate = null, DateTime? toDate = null) { return await GetLogsAsync(new AuditLogQueryParameters diff --git a/src/MilitaryTrainingManagement/Services/Interfaces/IAuditService.cs b/src/MilitaryTrainingManagement/Services/Interfaces/IAuditService.cs index 3d9cd08..79621a5 100644 --- a/src/MilitaryTrainingManagement/Services/Interfaces/IAuditService.cs +++ b/src/MilitaryTrainingManagement/Services/Interfaces/IAuditService.cs @@ -47,6 +47,16 @@ public interface IAuditService /// Task> GetLogsAsync(AuditLogQueryParameters parameters); + /// + /// 获取审计日志总数 + /// + Task GetLogCountAsync(AuditLogQueryParameters parameters); + + /// + /// 根据ID获取审计日志 + /// + Task GetLogByIdAsync(int id); + /// /// 获取审计日志(简化版本) /// diff --git a/src/frontend/src/api/auditLogs.ts b/src/frontend/src/api/auditLogs.ts new file mode 100644 index 0000000..1d0a632 --- /dev/null +++ b/src/frontend/src/api/auditLogs.ts @@ -0,0 +1,53 @@ +import apiClient from './client' + +export interface AuditLog { + id: number + entityType: string + entityId: number + action: string + description?: string + oldValues?: string + newValues?: string + changedFields?: string + userId?: number + userName?: string + organizationalUnitId?: number + organizationalUnitName?: string + timestamp: string + ipAddress?: string + userAgent?: string + requestPath?: string + isSuccess: boolean + errorMessage?: string +} + +export interface AuditLogQueryParams { + entityType?: string + action?: string + userId?: number + organizationalUnitId?: number + fromDate?: string + toDate?: string + pageNumber?: number + pageSize?: number +} + +export interface PagedResult { + items: T[] + totalCount: number + pageNumber: number + pageSize: number + totalPages: number +} + +export const auditLogsApi = { + async getAll(params?: AuditLogQueryParams): Promise> { + const response = await apiClient.get>('/auditlogs', { params }) + return response.data + }, + + async getById(id: number): Promise { + const response = await apiClient.get(`/auditlogs/${id}`) + return response.data + } +} diff --git a/src/frontend/src/api/index.ts b/src/frontend/src/api/index.ts index 390d555..460de97 100644 --- a/src/frontend/src/api/index.ts +++ b/src/frontend/src/api/index.ts @@ -6,4 +6,5 @@ export { reportsApi } from './reports' export { personnelApi } from './personnel' export { approvalsApi } from './approvals' export { statsApi } from './stats' +export { auditLogsApi } from './auditLogs' export { default as apiClient } from './client' diff --git a/src/frontend/src/layouts/MainLayout.vue b/src/frontend/src/layouts/MainLayout.vue index 6caeb41..33de57d 100644 --- a/src/frontend/src/layouts/MainLayout.vue +++ b/src/frontend/src/layouts/MainLayout.vue @@ -53,6 +53,11 @@ 审批管理 + + + + 操作记录 + @@ -97,6 +102,7 @@ import { DataAnalysis, User, Checked, + Document, SwitchButton } from '@element-plus/icons-vue' diff --git a/src/frontend/src/router/index.ts b/src/frontend/src/router/index.ts index 721a261..fe12ffa 100644 --- a/src/frontend/src/router/index.ts +++ b/src/frontend/src/router/index.ts @@ -100,6 +100,12 @@ const routes: RouteRecordRaw[] = [ name: 'ApprovalDetail', component: () => import('@/views/approvals/ApprovalDetail.vue'), meta: { title: '审批详情' } + }, + { + path: 'audit-logs', + name: 'AuditLogs', + component: () => import('@/views/AuditLogs.vue'), + meta: { title: '操作记录', minLevel: 1 } } ] }, diff --git a/src/frontend/src/views/AuditLogs.vue b/src/frontend/src/views/AuditLogs.vue new file mode 100644 index 0000000..2d32d9d --- /dev/null +++ b/src/frontend/src/views/AuditLogs.vue @@ -0,0 +1,289 @@ + + + + +