操作日志
This commit is contained in:
parent
adef288429
commit
ece16711f5
|
|
@ -0,0 +1,56 @@
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MilitaryTrainingManagement.Services.Interfaces;
|
||||
|
||||
namespace MilitaryTrainingManagement.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 审计日志控制器
|
||||
/// </summary>
|
||||
[Authorize]
|
||||
public class AuditLogsController : BaseApiController
|
||||
{
|
||||
private readonly IAuditService _auditService;
|
||||
|
||||
public AuditLogsController(IAuditService auditService)
|
||||
{
|
||||
_auditService = auditService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取审计日志列表(分页)
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> 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
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据ID获取审计日志详情
|
||||
/// </summary>
|
||||
[HttpGet("{id}")]
|
||||
public async Task<IActionResult> GetById(int id)
|
||||
{
|
||||
var log = await _auditService.GetLogByIdAsync(id);
|
||||
if (log == null)
|
||||
{
|
||||
return NotFound(new { message = "审计日志不存在" });
|
||||
}
|
||||
return Ok(log);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -205,6 +205,45 @@ public class AuditService : IAuditService
|
|||
.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
|
||||
|
|
|
|||
|
|
@ -47,6 +47,16 @@ public interface IAuditService
|
|||
/// </summary>
|
||||
Task<IEnumerable<AuditLog>> GetLogsAsync(AuditLogQueryParameters parameters);
|
||||
|
||||
/// <summary>
|
||||
/// 获取审计日志总数
|
||||
/// </summary>
|
||||
Task<int> GetLogCountAsync(AuditLogQueryParameters parameters);
|
||||
|
||||
/// <summary>
|
||||
/// 根据ID获取审计日志
|
||||
/// </summary>
|
||||
Task<AuditLog?> GetLogByIdAsync(int id);
|
||||
|
||||
/// <summary>
|
||||
/// 获取审计日志(简化版本)
|
||||
/// </summary>
|
||||
|
|
|
|||
53
src/frontend/src/api/auditLogs.ts
Normal file
53
src/frontend/src/api/auditLogs.ts
Normal file
|
|
@ -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<T> {
|
||||
items: T[]
|
||||
totalCount: number
|
||||
pageNumber: number
|
||||
pageSize: number
|
||||
totalPages: number
|
||||
}
|
||||
|
||||
export const auditLogsApi = {
|
||||
async getAll(params?: AuditLogQueryParams): Promise<PagedResult<AuditLog>> {
|
||||
const response = await apiClient.get<PagedResult<AuditLog>>('/auditlogs', { params })
|
||||
return response.data
|
||||
},
|
||||
|
||||
async getById(id: number): Promise<AuditLog> {
|
||||
const response = await apiClient.get<AuditLog>(`/auditlogs/${id}`)
|
||||
return response.data
|
||||
}
|
||||
}
|
||||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -53,6 +53,11 @@
|
|||
<el-icon><Checked /></el-icon>
|
||||
<span>审批管理</span>
|
||||
</el-menu-item>
|
||||
|
||||
<el-menu-item v-if="authStore.hasPermission(1)" index="/audit-logs">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>操作记录</span>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
|
||||
|
|
@ -97,6 +102,7 @@ import {
|
|||
DataAnalysis,
|
||||
User,
|
||||
Checked,
|
||||
Document,
|
||||
SwitchButton
|
||||
} from '@element-plus/icons-vue'
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
289
src/frontend/src/views/AuditLogs.vue
Normal file
289
src/frontend/src/views/AuditLogs.vue
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
<template>
|
||||
<div class="audit-logs">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>操作记录</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 筛选条件 -->
|
||||
<el-form :inline="true" :model="filters" class="filter-form">
|
||||
<el-form-item label="操作类型">
|
||||
<el-select v-model="filters.action" placeholder="全部" clearable style="width: 150px">
|
||||
<el-option label="创建" value="Create" />
|
||||
<el-option label="更新" value="Update" />
|
||||
<el-option label="删除" value="Delete" />
|
||||
<el-option label="审批" value="Approve" />
|
||||
<el-option label="拒绝" value="Reject" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="数据类型">
|
||||
<el-select v-model="filters.entityType" placeholder="全部" clearable style="width: 150px">
|
||||
<el-option label="物资配额" value="MaterialAllocation" />
|
||||
<el-option label="配额分配" value="AllocationDistribution" />
|
||||
<el-option label="用户账户" value="UserAccount" />
|
||||
<el-option label="组织单位" value="OrganizationalUnit" />
|
||||
<el-option label="人员信息" value="Personnel" />
|
||||
<el-option label="审批请求" value="ApprovalRequest" />
|
||||
<el-option label="物资类别" value="MaterialCategory" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="时间范围">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="datetimerange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
style="width: 360px"
|
||||
/>
|
||||
</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="logs" v-loading="loading" stripe>
|
||||
<el-table-column prop="timestamp" label="时间" width="160">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.timestamp) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作人" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ row.user?.displayName || '系统' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="所属单位" width="150" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
{{ row.organizationalUnit?.name || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="action" label="操作" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getActionTagType(row.action)" size="small">
|
||||
{{ getActionName(row.action) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="entityType" label="数据类型" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ getEntityTypeName(row.entityType) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="description" label="描述" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="ipAddress" label="IP地址" width="130" />
|
||||
<el-table-column prop="isSuccess" label="状态" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.isSuccess ? 'success' : 'danger'" size="small">
|
||||
{{ row.isSuccess ? '成功' : '失败' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="100" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" text size="small" @click="handleViewDetail(row)">详情</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.pageNumber"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:total="pagination.totalCount"
|
||||
:page-sizes="[20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSearch"
|
||||
@current-change="handleSearch"
|
||||
style="margin-top: 20px; justify-content: flex-end"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<!-- 详情对话框 -->
|
||||
<el-dialog v-model="showDetailDialog" title="操作详情" width="700px">
|
||||
<el-descriptions v-if="selectedLog" :column="2" border>
|
||||
<el-descriptions-item label="时间">{{ formatDate(selectedLog.timestamp) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="操作人">{{ selectedLog.user?.displayName || '系统' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="所属单位">{{ selectedLog.organizationalUnit?.name || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="操作类型">
|
||||
<el-tag :type="getActionTagType(selectedLog.action)" size="small">
|
||||
{{ getActionName(selectedLog.action) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="数据类型">{{ getEntityTypeName(selectedLog.entityType) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="数据ID">{{ selectedLog.entityId }}</el-descriptions-item>
|
||||
<el-descriptions-item label="IP地址">{{ selectedLog.ipAddress || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="selectedLog.isSuccess ? 'success' : 'danger'" size="small">
|
||||
{{ selectedLog.isSuccess ? '成功' : '失败' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="描述" :span="2">{{ selectedLog.description || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="请求路径" :span="2">{{ selectedLog.requestPath || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="User Agent" :span="2">{{ selectedLog.userAgent || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item v-if="selectedLog.oldValues" label="原始值" :span="2">
|
||||
<pre class="json-content">{{ formatJson(selectedLog.oldValues) }}</pre>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item v-if="selectedLog.newValues" label="新值" :span="2">
|
||||
<pre class="json-content">{{ formatJson(selectedLog.newValues) }}</pre>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item v-if="selectedLog.changedFields" label="变更字段" :span="2">
|
||||
<pre class="json-content">{{ formatJson(selectedLog.changedFields) }}</pre>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item v-if="selectedLog.errorMessage" label="错误信息" :span="2">
|
||||
<el-text type="danger">{{ selectedLog.errorMessage }}</el-text>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<template #footer>
|
||||
<el-button @click="showDetailDialog = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { auditLogsApi } from '@/api'
|
||||
import type { AuditLog } from '@/api/auditLogs'
|
||||
|
||||
const loading = ref(false)
|
||||
const logs = ref<AuditLog[]>([])
|
||||
const showDetailDialog = ref(false)
|
||||
const selectedLog = ref<AuditLog | null>(null)
|
||||
const dateRange = ref<[Date, Date] | null>(null)
|
||||
|
||||
const filters = reactive({
|
||||
action: '',
|
||||
entityType: ''
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
pageNumber: 1,
|
||||
pageSize: 20,
|
||||
totalCount: 0
|
||||
})
|
||||
|
||||
function formatDate(dateStr: string): string {
|
||||
return new Date(dateStr).toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
function getActionName(action: string): string {
|
||||
const actionMap: Record<string, string> = {
|
||||
'Create': '创建',
|
||||
'Update': '更新',
|
||||
'Delete': '删除',
|
||||
'Approve': '审批',
|
||||
'Reject': '拒绝',
|
||||
'Submit': '提交',
|
||||
'Review': '审核'
|
||||
}
|
||||
return actionMap[action] || action
|
||||
}
|
||||
|
||||
function getActionTagType(action: string): string {
|
||||
const typeMap: Record<string, string> = {
|
||||
'Create': 'success',
|
||||
'Update': 'warning',
|
||||
'Delete': 'danger',
|
||||
'Approve': 'success',
|
||||
'Reject': 'danger',
|
||||
'Submit': 'primary',
|
||||
'Review': 'info'
|
||||
}
|
||||
return typeMap[action] || 'info'
|
||||
}
|
||||
|
||||
function getEntityTypeName(entityType: string): string {
|
||||
const nameMap: Record<string, string> = {
|
||||
'MaterialAllocation': '物资配额',
|
||||
'AllocationDistribution': '配额分配',
|
||||
'UserAccount': '用户账户',
|
||||
'OrganizationalUnit': '组织单位',
|
||||
'Personnel': '人员信息',
|
||||
'ApprovalRequest': '审批请求',
|
||||
'MaterialCategory': '物资类别'
|
||||
}
|
||||
return nameMap[entityType] || entityType
|
||||
}
|
||||
|
||||
function formatJson(jsonStr: string): string {
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(jsonStr), null, 2)
|
||||
} catch {
|
||||
return jsonStr
|
||||
}
|
||||
}
|
||||
|
||||
async function loadLogs() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params: any = {
|
||||
pageNumber: pagination.pageNumber,
|
||||
pageSize: pagination.pageSize
|
||||
}
|
||||
|
||||
if (filters.action) params.action = filters.action
|
||||
if (filters.entityType) params.entityType = filters.entityType
|
||||
if (dateRange.value) {
|
||||
params.fromDate = dateRange.value[0].toISOString()
|
||||
params.toDate = dateRange.value[1].toISOString()
|
||||
}
|
||||
|
||||
const result = await auditLogsApi.getAll(params)
|
||||
logs.value = result.items
|
||||
pagination.totalCount = result.totalCount
|
||||
} catch {
|
||||
ElMessage.error('加载操作记录失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.pageNumber = 1
|
||||
loadLogs()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
filters.action = ''
|
||||
filters.entityType = ''
|
||||
dateRange.value = null
|
||||
pagination.pageNumber = 1
|
||||
loadLogs()
|
||||
}
|
||||
|
||||
function handleViewDetail(log: AuditLog) {
|
||||
selectedLog.value = log
|
||||
showDetailDialog.value = true
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadLogs()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filter-form {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.json-content {
|
||||
background-color: #f5f7fa;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
max-height: 300px;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in New Issue
Block a user