421 lines
15 KiB
C#
421 lines
15 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// 审计服务实现
|
|
/// </summary>
|
|
public class AuditService : IAuditService
|
|
{
|
|
private readonly ApplicationDbContext _context;
|
|
private readonly ILogger<AuditService> _logger;
|
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
|
{
|
|
WriteIndented = false,
|
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
|
};
|
|
|
|
public AuditService(ApplicationDbContext context, ILogger<AuditService> 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>(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>(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>(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<IEnumerable<AuditLog>> 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<int> 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<AuditLog?> GetLogByIdAsync(int id)
|
|
{
|
|
return await _context.AuditLogs
|
|
.Include(l => l.User)
|
|
.Include(l => l.OrganizationalUnit)
|
|
.FirstOrDefaultAsync(l => l.Id == id);
|
|
}
|
|
|
|
public async Task<IEnumerable<AuditLog>> 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<IEnumerable<AuditLog>> 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<IEnumerable<AuditLog>> 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<IEnumerable<AuditLog>> 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<AuditLogStatistics> 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>(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>(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<string> GetChangedFields<T>(T original, T updated) where T : class
|
|
{
|
|
var changedFields = new List<string>();
|
|
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
|
|
}
|
|
|
|
/// <summary>
|
|
/// 审计操作类型常量
|
|
/// </summary>
|
|
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";
|
|
}
|