using System.Reflection; using System.Text.Json; using Microsoft.EntityFrameworkCore; using MilitaryTrainingManagement.Data; using MilitaryTrainingManagement.Models.Entities; using MilitaryTrainingManagement.Services.Interfaces; namespace MilitaryTrainingManagement.Services.Implementations; /// /// 审计服务实现 /// public class AuditService : IAuditService { private readonly ApplicationDbContext _context; private readonly ILogger _logger; private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = false, PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; public AuditService(ApplicationDbContext context, ILogger logger) { _context = context; _logger = logger; } public async Task LogAsync(AuditLogEntry entry) { var log = new AuditLog { EntityType = entry.EntityType, EntityId = entry.EntityId, Action = entry.Action, Description = entry.Description, OldValues = entry.OldValues, NewValues = entry.NewValues, ChangedFields = entry.ChangedFields, UserId = entry.UserId, OrganizationalUnitId = entry.OrganizationalUnitId, IpAddress = entry.IpAddress, UserAgent = entry.UserAgent, RequestPath = entry.RequestPath, IsSuccess = entry.IsSuccess, ErrorMessage = entry.ErrorMessage, Timestamp = DateTime.UtcNow }; _context.AuditLogs.Add(log); await _context.SaveChangesAsync(); _logger.LogInformation("审计日志已记录: {EntityType} {EntityId} {Action}", entry.EntityType, entry.EntityId, entry.Action); } public async Task LogAsync(string entityType, int entityId, string action, string? oldValues, string? newValues, int? userId, string? ipAddress) { await LogAsync(new AuditLogEntry { EntityType = entityType, EntityId = entityId, Action = action, OldValues = oldValues, NewValues = newValues, UserId = userId, IpAddress = ipAddress }); } public async Task LogCreateAsync(T entity, int? userId, int? organizationalUnitId, string? ipAddress = null, string? userAgent = null, string? requestPath = null) where T : class { var entityType = typeof(T).Name; var entityId = GetEntityId(entity); var newValues = SerializeEntity(entity); await LogAsync(new AuditLogEntry { EntityType = entityType, EntityId = entityId, Action = AuditActions.Create, Description = $"创建{GetEntityDisplayName(entityType)}记录", NewValues = newValues, UserId = userId, OrganizationalUnitId = organizationalUnitId, IpAddress = ipAddress, UserAgent = userAgent, RequestPath = requestPath }); } public async Task LogUpdateAsync(T originalEntity, T updatedEntity, int? userId, int? organizationalUnitId, string? ipAddress = null, string? userAgent = null, string? requestPath = null) where T : class { var entityType = typeof(T).Name; var entityId = GetEntityId(updatedEntity); var oldValues = SerializeEntity(originalEntity); var newValues = SerializeEntity(updatedEntity); var changedFields = GetChangedFields(originalEntity, updatedEntity); await LogAsync(new AuditLogEntry { EntityType = entityType, EntityId = entityId, Action = AuditActions.Update, Description = $"更新{GetEntityDisplayName(entityType)}记录", OldValues = oldValues, NewValues = newValues, ChangedFields = JsonSerializer.Serialize(changedFields, JsonOptions), UserId = userId, OrganizationalUnitId = organizationalUnitId, IpAddress = ipAddress, UserAgent = userAgent, RequestPath = requestPath }); } public async Task LogDeleteAsync(T entity, int? userId, int? organizationalUnitId, string? ipAddress = null, string? userAgent = null, string? requestPath = null) where T : class { var entityType = typeof(T).Name; var entityId = GetEntityId(entity); var oldValues = SerializeEntity(entity); await LogAsync(new AuditLogEntry { EntityType = entityType, EntityId = entityId, Action = AuditActions.Delete, Description = $"删除{GetEntityDisplayName(entityType)}记录", OldValues = oldValues, UserId = userId, OrganizationalUnitId = organizationalUnitId, IpAddress = ipAddress, UserAgent = userAgent, RequestPath = requestPath }); } public async Task LogApprovalAsync(string entityType, int entityId, string action, string? description, int? userId, int? organizationalUnitId, string? ipAddress = null) { await LogAsync(new AuditLogEntry { EntityType = entityType, EntityId = entityId, Action = action, Description = description ?? $"{action}{GetEntityDisplayName(entityType)}记录", UserId = userId, OrganizationalUnitId = organizationalUnitId, IpAddress = ipAddress }); } public async Task LogFailureAsync(string entityType, int entityId, string action, string errorMessage, int? userId, int? organizationalUnitId, string? ipAddress = null) { await LogAsync(new AuditLogEntry { EntityType = entityType, EntityId = entityId, Action = action, Description = $"{action}操作失败", UserId = userId, OrganizationalUnitId = organizationalUnitId, IpAddress = ipAddress, IsSuccess = false, ErrorMessage = errorMessage }); } public async Task> GetLogsAsync(AuditLogQueryParameters parameters) { var query = _context.AuditLogs .Include(l => l.User) .Include(l => l.OrganizationalUnit) .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 .OrderByDescending(l => l.Timestamp) .Skip((parameters.PageNumber - 1) * parameters.PageSize) .Take(parameters.PageSize) .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 { EntityType = entityType, EntityId = entityId, FromDate = fromDate, ToDate = toDate }); } public async Task> GetEntityHistoryAsync(string entityType, int entityId) { return await _context.AuditLogs .Include(l => l.User) .Include(l => l.OrganizationalUnit) .Where(l => l.EntityType == entityType && l.EntityId == entityId) .OrderByDescending(l => l.Timestamp) .ToListAsync(); } public async Task> GetUserActivityAsync(int userId, DateTime? fromDate = null, DateTime? toDate = null) { var query = _context.AuditLogs .Include(l => l.OrganizationalUnit) .Where(l => l.UserId == userId); if (fromDate.HasValue) query = query.Where(l => l.Timestamp >= fromDate.Value); if (toDate.HasValue) query = query.Where(l => l.Timestamp <= toDate.Value); return await query.OrderByDescending(l => l.Timestamp).ToListAsync(); } public async Task> GetOrganizationalUnitActivityAsync(int organizationalUnitId, DateTime? fromDate = null, DateTime? toDate = null) { var query = _context.AuditLogs .Include(l => l.User) .Where(l => l.OrganizationalUnitId == organizationalUnitId); if (fromDate.HasValue) query = query.Where(l => l.Timestamp >= fromDate.Value); if (toDate.HasValue) query = query.Where(l => l.Timestamp <= toDate.Value); return await query.OrderByDescending(l => l.Timestamp).ToListAsync(); } public async Task GetStatisticsAsync(DateTime? fromDate = null, DateTime? toDate = null) { var query = _context.AuditLogs.AsQueryable(); if (fromDate.HasValue) query = query.Where(l => l.Timestamp >= fromDate.Value); if (toDate.HasValue) query = query.Where(l => l.Timestamp <= toDate.Value); var logs = await query.ToListAsync(); return new AuditLogStatistics { TotalLogs = logs.Count, CreateOperations = logs.Count(l => l.Action == AuditActions.Create), UpdateOperations = logs.Count(l => l.Action == AuditActions.Update), DeleteOperations = logs.Count(l => l.Action == AuditActions.Delete), ApprovalOperations = logs.Count(l => l.Action == AuditActions.Approve || l.Action == AuditActions.Reject), FailedOperations = logs.Count(l => !l.IsSuccess), OperationsByEntityType = logs.GroupBy(l => l.EntityType).ToDictionary(g => g.Key, g => g.Count()), OperationsByAction = logs.GroupBy(l => l.Action).ToDictionary(g => g.Key, g => g.Count()) }; } #region Helper Methods private static int GetEntityId(T entity) where T : class { var idProperty = typeof(T).GetProperty("Id"); if (idProperty != null) { var value = idProperty.GetValue(entity); if (value is int intValue) return intValue; } return 0; } private static string SerializeEntity(T entity) where T : class { try { // 创建一个只包含简单属性的字典,避免循环引用 var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(p => IsSimpleType(p.PropertyType)) .ToDictionary(p => p.Name, p => p.GetValue(entity)); return JsonSerializer.Serialize(properties, JsonOptions); } catch (Exception) { return "{}"; } } private static bool IsSimpleType(Type type) { var underlyingType = Nullable.GetUnderlyingType(type) ?? type; return underlyingType.IsPrimitive || underlyingType == typeof(string) || underlyingType == typeof(decimal) || underlyingType == typeof(DateTime) || underlyingType == typeof(DateTimeOffset) || underlyingType == typeof(Guid) || underlyingType.IsEnum; } private static List GetChangedFields(T original, T updated) where T : class { var changedFields = new List(); var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(p => IsSimpleType(p.PropertyType)); foreach (var property in properties) { var originalValue = property.GetValue(original); var updatedValue = property.GetValue(updated); if (!Equals(originalValue, updatedValue)) { changedFields.Add(property.Name); } } return changedFields; } private static string GetEntityDisplayName(string entityType) { return entityType switch { "MaterialAllocation" => "物资配额", "AllocationDistribution" => "配额分配", "Personnel" => "人员", "OrganizationalUnit" => "组织单位", "UserAccount" => "用户账户", "ApprovalRequest" => "审批请求", _ => entityType }; } #endregion } /// /// 审计操作类型常量 /// public static class AuditActions { public const string Create = "Create"; public const string Update = "Update"; public const string Delete = "Delete"; public const string Approve = "Approve"; public const string Reject = "Reject"; public const string Submit = "Submit"; public const string Login = "Login"; public const string Logout = "Logout"; public const string Transfer = "Transfer"; public const string Distribute = "Distribute"; public const string Report = "Report"; }