diff --git a/src/MilitaryTrainingManagement/Controllers/AllocationsController.cs b/src/MilitaryTrainingManagement/Controllers/AllocationsController.cs index 637743a..1c85689 100644 --- a/src/MilitaryTrainingManagement/Controllers/AllocationsController.cs +++ b/src/MilitaryTrainingManagement/Controllers/AllocationsController.cs @@ -15,13 +15,21 @@ public class AllocationsController : BaseApiController { private readonly IAllocationService _allocationService; private readonly IOrganizationalAuthorizationService _authorizationService; + private readonly IAuditService _auditService; public AllocationsController( IAllocationService allocationService, - IOrganizationalAuthorizationService authorizationService) + IOrganizationalAuthorizationService authorizationService, + IAuditService auditService) { _allocationService = allocationService; _authorizationService = authorizationService; + _auditService = auditService; + } + + private string? GetClientIpAddress() + { + return HttpContext.Connection.RemoteIpAddress?.ToString(); } /// @@ -202,6 +210,7 @@ public class AllocationsController : BaseApiController public async Task Create([FromBody] CreateAllocationRequest request) { var unitId = GetCurrentUnitId(); + var userId = GetCurrentUserId(); if (unitId == null) return Unauthorized(new { message = "无法获取用户组织信息" }); @@ -219,6 +228,16 @@ public class AllocationsController : BaseApiController unitId.Value, request.GetDistributionsDictionary()); + // 记录审计日志 + await _auditService.LogApprovalAsync( + "MaterialAllocation", + allocation.Id, + "Create", + $"创建物资配额:{request.Category} - {request.MaterialName},总配额:{request.TotalQuota}{request.Unit}", + userId, + unitId, + GetClientIpAddress()); + return CreatedAtAction(nameof(GetById), new { id = allocation.Id }, MapToResponse(allocation)); } catch (ArgumentException ex) @@ -235,6 +254,7 @@ public class AllocationsController : BaseApiController public async Task Update(int id, [FromBody] UpdateAllocationRequest request) { var unitId = GetCurrentUnitId(); + var userId = GetCurrentUserId(); if (unitId == null) return Unauthorized(new { message = "无法获取用户组织信息" }); @@ -260,6 +280,16 @@ public class AllocationsController : BaseApiController request.Unit, request.TotalQuota); + // 记录审计日志 + await _auditService.LogApprovalAsync( + "MaterialAllocation", + id, + "Update", + $"更新物资配额:{request.Category} - {request.MaterialName},总配额:{request.TotalQuota}{request.Unit}", + userId, + unitId, + GetClientIpAddress()); + return Ok(MapToResponse(await _allocationService.GetByIdAsync(id) ?? allocation)); } catch (ArgumentException ex) @@ -276,6 +306,7 @@ public class AllocationsController : BaseApiController public async Task UpdateDistributions(int id, [FromBody] Dictionary distributions) { var unitId = GetCurrentUnitId(); + var userId = GetCurrentUserId(); if (unitId == null) return Unauthorized(new { message = "无法获取用户组织信息" }); @@ -291,6 +322,17 @@ public class AllocationsController : BaseApiController try { var allocation = await _allocationService.UpdateDistributionsAsync(id, distributions); + + // 记录审计日志 + await _auditService.LogApprovalAsync( + "AllocationDistribution", + id, + "Update", + $"更新配额分配:{existingAllocation.MaterialName},分配给 {distributions.Count} 个单位", + userId, + unitId, + GetClientIpAddress()); + return Ok(MapToResponse(allocation)); } catch (ArgumentException ex) @@ -321,6 +363,17 @@ public class AllocationsController : BaseApiController request.ActualCompletion, request.Remarks); + // 记录审计日志 + var materialName = distribution.Allocation?.MaterialName ?? "未知物资"; + await _auditService.LogApprovalAsync( + "ConsumptionReport", + distributionId, + "Create", + $"上报消耗:{materialName},数量:{request.ActualCompletion},备注:{request.Remarks ?? "无"}", + userId, + unitId, + GetClientIpAddress()); + return Ok(MapDistributionToResponse(distribution)); } catch (UnauthorizedAccessException ex) @@ -341,6 +394,7 @@ public class AllocationsController : BaseApiController public async Task Delete(int id) { var unitId = GetCurrentUnitId(); + var userId = GetCurrentUserId(); if (unitId == null) return Unauthorized(new { message = "无法获取用户组织信息" }); @@ -355,7 +409,21 @@ public class AllocationsController : BaseApiController try { + var materialName = existingAllocation.MaterialName; + var category = existingAllocation.Category; + await _allocationService.DeleteAsync(id); + + // 记录审计日志 + await _auditService.LogApprovalAsync( + "MaterialAllocation", + id, + "Delete", + $"删除物资配额:{category} - {materialName}", + userId, + unitId, + GetClientIpAddress()); + return NoContent(); } catch (ArgumentException ex) diff --git a/src/MilitaryTrainingManagement/Controllers/ApprovalsController.cs b/src/MilitaryTrainingManagement/Controllers/ApprovalsController.cs index 165bcba..b47fca8 100644 --- a/src/MilitaryTrainingManagement/Controllers/ApprovalsController.cs +++ b/src/MilitaryTrainingManagement/Controllers/ApprovalsController.cs @@ -15,13 +15,21 @@ public class ApprovalsController : BaseApiController { private readonly IWorkflowService _workflowService; private readonly IAllocationService _allocationService; + private readonly IAuditService _auditService; public ApprovalsController( IWorkflowService workflowService, - IAllocationService allocationService) + IAllocationService allocationService, + IAuditService auditService) { _workflowService = workflowService; _allocationService = allocationService; + _auditService = auditService; + } + + private string? GetClientIpAddress() + { + return HttpContext.Connection.RemoteIpAddress?.ToString(); } /// @@ -198,12 +206,24 @@ public class ApprovalsController : BaseApiController public async Task Approve(int id, [FromBody] ProcessApprovalRequest request) { var userId = GetCurrentUserId(); + var unitId = GetCurrentUnitId(); if (userId == null) return Unauthorized(new { message = "无法获取用户信息" }); try { var approvalRequest = await _workflowService.ApproveAsync(id, userId.Value, request.Comments); + + // 记录审计日志 + await _auditService.LogApprovalAsync( + "ApprovalRequest", + id, + "Approve", + $"审批通过修改请求:{GetTypeName(approvalRequest.Type)},处理意见:{request.Comments ?? "无"}", + userId, + unitId, + GetClientIpAddress()); + return Ok(MapToResponse(approvalRequest)); } catch (ArgumentException ex) @@ -229,12 +249,24 @@ public class ApprovalsController : BaseApiController public async Task Reject(int id, [FromBody] ProcessApprovalRequest request) { var userId = GetCurrentUserId(); + var unitId = GetCurrentUnitId(); if (userId == null) return Unauthorized(new { message = "无法获取用户信息" }); try { var approvalRequest = await _workflowService.RejectAsync(id, userId.Value, request.Comments); + + // 记录审计日志 + await _auditService.LogApprovalAsync( + "ApprovalRequest", + id, + "Reject", + $"拒绝修改请求:{GetTypeName(approvalRequest.Type)},处理意见:{request.Comments ?? "无"}", + userId, + unitId, + GetClientIpAddress()); + return Ok(MapToResponse(approvalRequest)); } catch (ArgumentException ex) diff --git a/src/MilitaryTrainingManagement/Controllers/ConsumptionChangeRequestsController.cs b/src/MilitaryTrainingManagement/Controllers/ConsumptionChangeRequestsController.cs index 616e95a..80ae34e 100644 --- a/src/MilitaryTrainingManagement/Controllers/ConsumptionChangeRequestsController.cs +++ b/src/MilitaryTrainingManagement/Controllers/ConsumptionChangeRequestsController.cs @@ -4,6 +4,7 @@ using Microsoft.EntityFrameworkCore; using MilitaryTrainingManagement.Data; using MilitaryTrainingManagement.Models.Entities; using MilitaryTrainingManagement.Models.Enums; +using MilitaryTrainingManagement.Services.Interfaces; namespace MilitaryTrainingManagement.Controllers; @@ -15,13 +16,21 @@ public class ConsumptionChangeRequestsController : BaseApiController { private readonly ApplicationDbContext _context; private readonly ILogger _logger; + private readonly IAuditService _auditService; public ConsumptionChangeRequestsController( ApplicationDbContext context, - ILogger logger) + ILogger logger, + IAuditService auditService) { _context = context; _logger = logger; + _auditService = auditService; + } + + private string? GetClientIpAddress() + { + return HttpContext.Connection.RemoteIpAddress?.ToString(); } /// @@ -38,6 +47,8 @@ public class ConsumptionChangeRequestsController : BaseApiController // 验证消耗记录存在且属于当前单位 var report = await _context.ConsumptionReports .Include(r => r.ReportedByUnit) + .Include(r => r.AllocationDistribution) + .ThenInclude(d => d.Allocation) .FirstOrDefaultAsync(r => r.Id == request.ConsumptionReportId); if (report == null) @@ -71,6 +82,19 @@ public class ConsumptionChangeRequestsController : BaseApiController _logger.LogInformation("单位 {UnitId} 创建了消耗记录 {ReportId} 的{Type}申请", unitId, request.ConsumptionReportId, request.RequestType); + // 记录审计日志 + var requestTypeName = request.RequestType == ChangeRequestType.Delete ? "删除" : "修改"; + var materialName = report.AllocationDistribution?.Allocation?.MaterialName ?? "未知物资"; + + await _auditService.LogApprovalAsync( + "ConsumptionReportChangeRequest", + changeRequest.Id, + "Create", + $"提交消耗记录{requestTypeName}申请,物资:{materialName},申请原因:{request.Reason}", + userId, + unitId, + GetClientIpAddress()); + return Ok(new { message = "申请已提交", id = changeRequest.Id }); } @@ -167,6 +191,7 @@ public class ConsumptionChangeRequestsController : BaseApiController var changeRequest = await _context.ConsumptionReportChangeRequests .Include(r => r.ConsumptionReport) .ThenInclude(cr => cr.AllocationDistribution) + .ThenInclude(d => d.Allocation) .Include(r => r.RequestedByUnit) .FirstOrDefaultAsync(r => r.Id == id); @@ -211,6 +236,20 @@ public class ConsumptionChangeRequestsController : BaseApiController _logger.LogInformation("单位 {UnitId} {Action}了消耗记录删改申请 {RequestId}", unitId, action, id); + // 记录审计日志 + var requestTypeName = changeRequest.RequestType == ChangeRequestType.Delete ? "删除" : "修改"; + var materialName = changeRequest.ConsumptionReport?.AllocationDistribution?.Allocation?.MaterialName ?? "未知物资"; + var reportedByUnitName = changeRequest.RequestedByUnit?.Name ?? "未知单位"; + + await _auditService.LogApprovalAsync( + "ConsumptionReportChangeRequest", + id, + request.Approved ? "Approve" : "Reject", + $"{action}「{reportedByUnitName}」的消耗记录{requestTypeName}申请,物资:{materialName},原因:{changeRequest.Reason},处理意见:{request.Comments ?? "无"}", + userId, + unitId, + GetClientIpAddress()); + return Ok(new { message = $"已{action}该申请" }); } @@ -227,6 +266,7 @@ public class ConsumptionChangeRequestsController : BaseApiController var changeRequest = await _context.ConsumptionReportChangeRequests .Include(r => r.ConsumptionReport) .ThenInclude(cr => cr.AllocationDistribution) + .ThenInclude(d => d.Allocation) .FirstOrDefaultAsync(r => r.Id == id); if (changeRequest == null) @@ -258,6 +298,19 @@ public class ConsumptionChangeRequestsController : BaseApiController _logger.LogInformation("消耗记录 {ReportId} 已被修改,数量从 {OldAmount} 改为 {NewAmount}", report.Id, oldAmount, request.NewAmount); + // 记录审计日志 + var userId = GetCurrentUserId(); + var materialName = distribution.Allocation?.MaterialName ?? "未知物资"; + + await _auditService.LogApprovalAsync( + "ConsumptionReport", + report.Id, + "Update", + $"修改消耗记录,物资:{materialName},数量从 {oldAmount} 改为 {request.NewAmount}", + userId, + unitId, + GetClientIpAddress()); + return Ok(new { message = "修改成功" }); } diff --git a/src/MilitaryTrainingManagement/Controllers/MaterialCategoriesController.cs b/src/MilitaryTrainingManagement/Controllers/MaterialCategoriesController.cs index fc47ffe..7798fc8 100644 --- a/src/MilitaryTrainingManagement/Controllers/MaterialCategoriesController.cs +++ b/src/MilitaryTrainingManagement/Controllers/MaterialCategoriesController.cs @@ -4,6 +4,7 @@ using Microsoft.EntityFrameworkCore; using MilitaryTrainingManagement.Data; using MilitaryTrainingManagement.Models.DTOs; using MilitaryTrainingManagement.Models.Entities; +using MilitaryTrainingManagement.Services.Interfaces; namespace MilitaryTrainingManagement.Controllers; @@ -14,10 +15,17 @@ namespace MilitaryTrainingManagement.Controllers; public class MaterialCategoriesController : BaseApiController { private readonly ApplicationDbContext _context; + private readonly IAuditService _auditService; - public MaterialCategoriesController(ApplicationDbContext context) + public MaterialCategoriesController(ApplicationDbContext context, IAuditService auditService) { _context = context; + _auditService = auditService; + } + + private string? GetClientIpAddress() + { + return HttpContext.Connection.RemoteIpAddress?.ToString(); } /// @@ -61,6 +69,9 @@ public class MaterialCategoriesController : BaseApiController [HttpPost] public async Task Create([FromBody] CreateMaterialCategoryRequest request) { + var unitId = GetCurrentUnitId(); + var userId = GetCurrentUserId(); + // 检查名称是否已存在 var exists = await _context.MaterialCategories .AnyAsync(c => c.Name == request.Name); @@ -81,6 +92,16 @@ public class MaterialCategoriesController : BaseApiController _context.MaterialCategories.Add(category); await _context.SaveChangesAsync(); + // 记录审计日志 + await _auditService.LogApprovalAsync( + "MaterialCategory", + category.Id, + "Create", + $"创建物资类别:{request.Name}", + userId, + unitId, + GetClientIpAddress()); + return CreatedAtAction(nameof(GetById), new { id = category.Id }, category); } @@ -90,6 +111,9 @@ public class MaterialCategoriesController : BaseApiController [HttpPut("{id}")] public async Task Update(int id, [FromBody] UpdateMaterialCategoryRequest request) { + var unitId = GetCurrentUnitId(); + var userId = GetCurrentUserId(); + var category = await _context.MaterialCategories.FindAsync(id); if (category == null) { @@ -111,6 +135,16 @@ public class MaterialCategoriesController : BaseApiController await _context.SaveChangesAsync(); + // 记录审计日志 + await _auditService.LogApprovalAsync( + "MaterialCategory", + id, + "Update", + $"更新物资类别:{request.Name}", + userId, + unitId, + GetClientIpAddress()); + return Ok(category); } @@ -120,6 +154,9 @@ public class MaterialCategoriesController : BaseApiController [HttpDelete("{id}")] public async Task Delete(int id) { + var unitId = GetCurrentUnitId(); + var userId = GetCurrentUserId(); + var category = await _context.MaterialCategories.FindAsync(id); if (category == null) { @@ -134,9 +171,18 @@ public class MaterialCategoriesController : BaseApiController return BadRequest(new { message = "该类别已被使用,无法删除" }); } + var categoryName = category.Name; _context.MaterialCategories.Remove(category); await _context.SaveChangesAsync(); - return Ok(new { message = "删除成功" }); + // 记录审计日志 + await _auditService.LogApprovalAsync( + "MaterialCategory", + id, + "Delete", + $"删除物资类别:{categoryName}", + userId, + unitId, + GetClientIpAddress()); return Ok(new { message = "删除成功" }); } } diff --git a/src/MilitaryTrainingManagement/Controllers/OrganizationsController.cs b/src/MilitaryTrainingManagement/Controllers/OrganizationsController.cs index 939e3f7..ef3f09f 100644 --- a/src/MilitaryTrainingManagement/Controllers/OrganizationsController.cs +++ b/src/MilitaryTrainingManagement/Controllers/OrganizationsController.cs @@ -17,15 +17,23 @@ public class OrganizationsController : BaseApiController private readonly IOrganizationService _organizationService; private readonly IAuthenticationService _authService; private readonly ApplicationDbContext _context; + private readonly IAuditService _auditService; public OrganizationsController( IOrganizationService organizationService, IAuthenticationService authService, - ApplicationDbContext context) + ApplicationDbContext context, + IAuditService auditService) { _organizationService = organizationService; _authService = authService; _context = context; + _auditService = auditService; + } + + private string? GetClientIpAddress() + { + return HttpContext.Connection.RemoteIpAddress?.ToString(); } [HttpGet] @@ -87,23 +95,69 @@ public class OrganizationsController : BaseApiController [HttpPost] public async Task Create([FromBody] CreateOrganizationRequest request) { + var unitId = GetCurrentUnitId(); + var userId = GetCurrentUserId(); + var unit = await _organizationService.CreateAsync(request.Name, request.Level, request.ParentId); + + // 记录审计日志 + await _auditService.LogApprovalAsync( + "OrganizationalUnit", + unit.Id, + "Create", + $"创建组织单位:{request.Name},级别:{request.Level}", + userId, + unitId, + GetClientIpAddress()); + return CreatedAtAction(nameof(GetById), new { id = unit.Id }, unit); } [HttpPut("{id}")] public async Task Update(int id, [FromBody] UpdateOrganizationRequest request) { + var unitId = GetCurrentUnitId(); + var userId = GetCurrentUserId(); + var unit = await _organizationService.UpdateAsync(id, request.Name); + + // 记录审计日志 + await _auditService.LogApprovalAsync( + "OrganizationalUnit", + id, + "Update", + $"更新组织单位名称为:{request.Name}", + userId, + unitId, + GetClientIpAddress()); + return Ok(unit); } [HttpDelete("{id}")] public async Task Delete(int id) { + var unitId = GetCurrentUnitId(); + var userId = GetCurrentUserId(); + + // 获取组织信息用于日志 + var orgUnit = await _organizationService.GetByIdAsync(id); + var orgName = orgUnit?.Name ?? "未知"; + try { await _organizationService.DeleteAsync(id); + + // 记录审计日志 + await _auditService.LogApprovalAsync( + "OrganizationalUnit", + id, + "Delete", + $"删除组织单位:{orgName}", + userId, + unitId, + GetClientIpAddress()); + return NoContent(); } catch (ArgumentException ex) @@ -178,6 +232,18 @@ public class OrganizationsController : BaseApiController _context.UserAccounts.Add(account); await _context.SaveChangesAsync(); + // 记录审计日志 + var currentUnitId = GetCurrentUnitId(); + var currentUserId = GetCurrentUserId(); + await _auditService.LogApprovalAsync( + "UserAccount", + account.Id, + "Create", + $"创建账户:{request.Username},所属组织:{unit.Name}", + currentUserId, + currentUnitId, + GetClientIpAddress()); + return Ok(new { message = "账户创建成功", username = account.Username }); } @@ -219,9 +285,22 @@ public class OrganizationsController : BaseApiController return BadRequest(new { message = "该账户有审批记录,无法删除" }); } + var username = account.Username; _context.UserAccounts.Remove(account); await _context.SaveChangesAsync(); + // 记录审计日志 + var currentUnitId = GetCurrentUnitId(); + var currentUserId = GetCurrentUserId(); + await _auditService.LogApprovalAsync( + "UserAccount", + accountId, + "Delete", + $"删除账户:{username}", + currentUserId, + currentUnitId, + GetClientIpAddress()); + return Ok(new { message = "账户删除成功" }); } } diff --git a/src/MilitaryTrainingManagement/Controllers/PersonnelController.cs b/src/MilitaryTrainingManagement/Controllers/PersonnelController.cs index 98e250d..1da2269 100644 --- a/src/MilitaryTrainingManagement/Controllers/PersonnelController.cs +++ b/src/MilitaryTrainingManagement/Controllers/PersonnelController.cs @@ -17,15 +17,18 @@ public class PersonnelController : BaseApiController private readonly IPersonnelService _personnelService; private readonly IFileUploadService _fileUploadService; private readonly IOrganizationalAuthorizationService _authorizationService; + private readonly IAuditService _auditService; public PersonnelController( IPersonnelService personnelService, IFileUploadService fileUploadService, - IOrganizationalAuthorizationService authorizationService) + IOrganizationalAuthorizationService authorizationService, + IAuditService auditService) { _personnelService = personnelService; _fileUploadService = fileUploadService; _authorizationService = authorizationService; + _auditService = auditService; } /// @@ -533,8 +536,22 @@ public class PersonnelController : BaseApiController try { + // 获取人员信息用于审计日志 + var personnelBefore = await _personnelService.GetByIdAsync(id); + // 不传递level参数,让服务层根据人员所在单位自动确定等级 var personnel = await _personnelService.ApproveAsync(id, unitId.Value); + + // 记录审计日志 + await _auditService.LogApprovalAsync( + "Personnel", + id, + "Approve", + $"审批通过人才「{personnel.Name}」,等级:{GetLevelDisplayName(personnel.ApprovedLevel)}", + userId, + unitId, + GetClientIpAddress()); + return Ok(MapToResponse(personnel)); } catch (ArgumentException ex) @@ -542,6 +559,23 @@ public class PersonnelController : BaseApiController return BadRequest(new { message = ex.Message }); } } + + private string GetLevelDisplayName(PersonnelLevel? level) + { + return level switch + { + PersonnelLevel.Division => "师级", + PersonnelLevel.Regiment => "团级", + PersonnelLevel.Battalion => "营级", + PersonnelLevel.Company => "连级", + _ => "未定级" + }; + } + + private string? GetClientIpAddress() + { + return HttpContext.Connection.RemoteIpAddress?.ToString(); + } /// /// 拒绝人员 @@ -564,7 +598,21 @@ public class PersonnelController : BaseApiController return StatusCode(403, new { message = "您没有权限拒绝此人员" }); } + // 获取人员信息用于审计日志 + var personnelBefore = await _personnelService.GetByIdAsync(id); + var personnel = await _personnelService.RejectAsync(id, userId.Value, unitId.Value, request.Reason); + + // 记录审计日志 + await _auditService.LogApprovalAsync( + "Personnel", + id, + "Reject", + $"拒绝人才「{personnel.Name}」的审批申请,原因:{request.Reason ?? "无"}", + userId, + unitId, + GetClientIpAddress()); + return Ok(MapToResponse(personnel)); } @@ -777,7 +825,8 @@ public class PersonnelController : BaseApiController public async Task RequestUpgrade(int id) { var unitId = GetCurrentUnitId(); - if (unitId == null) + var userId = GetCurrentUserId(); + if (unitId == null || userId == null) { return Unauthorized(); } @@ -785,6 +834,17 @@ public class PersonnelController : BaseApiController try { var personnel = await _personnelService.RequestUpgradeAsync(id, unitId.Value); + + // 记录审计日志 + await _auditService.LogApprovalAsync( + "Personnel", + id, + "RequestUpgrade", + $"发起向上申报:人才「{personnel.Name}」({GetLevelDisplayName(personnel.ApprovedLevel)})申请升级", + userId, + unitId, + GetClientIpAddress()); + return Ok(MapToResponse(personnel)); } catch (ArgumentException ex) @@ -801,14 +861,30 @@ public class PersonnelController : BaseApiController public async Task ApproveUpgrade(int id) { var unitId = GetCurrentUnitId(); - if (unitId == null) + var userId = GetCurrentUserId(); + if (unitId == null || userId == null) { return Unauthorized(); } try { + // 获取人员信息用于审计日志 + var personnelBefore = await _personnelService.GetByIdAsync(id); + var previousLevel = personnelBefore?.ApprovedLevel; + var personnel = await _personnelService.ApproveUpgradeAsync(id, unitId.Value); + + // 记录审计日志 + await _auditService.LogApprovalAsync( + "Personnel", + id, + "ApproveUpgrade", + $"批准人才「{personnel.Name}」的向上申报,等级从{GetLevelDisplayName(previousLevel)}升级为{GetLevelDisplayName(personnel.ApprovedLevel)}", + userId, + unitId, + GetClientIpAddress()); + return Ok(MapToResponse(personnel)); } catch (ArgumentException ex) @@ -825,14 +901,29 @@ public class PersonnelController : BaseApiController public async Task RejectUpgrade(int id, [FromBody] RejectUpgradeRequest request) { var unitId = GetCurrentUnitId(); - if (unitId == null) + var userId = GetCurrentUserId(); + if (unitId == null || userId == null) { return Unauthorized(); } try { + // 获取人员信息用于审计日志 + var personnelBefore = await _personnelService.GetByIdAsync(id); + var personnel = await _personnelService.RejectUpgradeAsync(id, unitId.Value, request.Comments); + + // 记录审计日志 + await _auditService.LogApprovalAsync( + "Personnel", + id, + "RejectUpgrade", + $"拒绝人才「{personnel.Name}」的向上申报,原因:{request.Comments}", + userId, + unitId, + GetClientIpAddress()); + return Ok(MapToResponse(personnel)); } catch (ArgumentException ex) @@ -849,7 +940,8 @@ public class PersonnelController : BaseApiController public async Task DirectUpgrade(int id) { var unitId = GetCurrentUnitId(); - if (unitId == null) + var userId = GetCurrentUserId(); + if (unitId == null || userId == null) { return Unauthorized(); } @@ -857,6 +949,17 @@ public class PersonnelController : BaseApiController try { var personnel = await _personnelService.DirectUpgradeAsync(id, unitId.Value); + + // 记录审计日志 + await _auditService.LogApprovalAsync( + "Personnel", + id, + "DirectUpgrade", + $"师部直接升级人才「{personnel.Name}」为师级人才", + userId, + unitId, + GetClientIpAddress()); + return Ok(MapToResponse(personnel)); } catch (ArgumentException ex) @@ -865,4 +968,50 @@ public class PersonnelController : BaseApiController } } + /// + /// 重新提交被拒绝的人才审批 + /// + [HttpPost("{id}/resubmit")] + public async Task ResubmitPersonnel(int id) + { + var unitId = GetCurrentUnitId(); + var userId = GetCurrentUserId(); + if (unitId == null || userId == null) + { + return Unauthorized(); + } + + try + { + var personnel = await _personnelService.GetByIdAsync(id); + if (personnel == null) + { + return NotFound(new { message = "人员记录不存在" }); + } + + // 检查权限:只有提交单位可以重新提交 + if (personnel.SubmittedByUnitId != unitId.Value) + { + return Forbid(); + } + + var resubmitted = await _personnelService.ResubmitPersonnelAsync(id); + + // 记录审计日志 + await _auditService.LogApprovalAsync( + "Personnel", + id, + "Resubmit", + $"重新提交审批:人才「{resubmitted.Name}」重新提交审批申请", + userId, + unitId, + GetClientIpAddress()); + + return Ok(MapToResponse(resubmitted)); + } + catch (ArgumentException ex) + { + return BadRequest(new { message = ex.Message }); + } + } } diff --git a/src/MilitaryTrainingManagement/Services/Implementations/PersonnelService.cs b/src/MilitaryTrainingManagement/Services/Implementations/PersonnelService.cs index bde22a7..f60bfa5 100644 --- a/src/MilitaryTrainingManagement/Services/Implementations/PersonnelService.cs +++ b/src/MilitaryTrainingManagement/Services/Implementations/PersonnelService.cs @@ -353,8 +353,8 @@ public class PersonnelService : IPersonnelService if (personnel == null) return false; - // 只有待审批状态的人员可以直接修改 - if (personnel.Status == PersonnelStatus.Pending) + // 待审批状态和已拒绝状态的人员可以直接修改 + if (personnel.Status == PersonnelStatus.Pending || personnel.Status == PersonnelStatus.Rejected) return true; // 已审批的人员需要通过工作流修改 @@ -860,4 +860,39 @@ public class PersonnelService : IPersonnelService return personnel; } + + /// + /// 重新提交被拒绝的人才审批 + /// + public async Task ResubmitPersonnelAsync(int personnelId) + { + var personnel = await _context.Personnel + .Include(p => p.SubmittedByUnit) + .FirstOrDefaultAsync(p => p.Id == personnelId); + + if (personnel == null) + throw new ArgumentException("人员记录不存在"); + + if (personnel.Status != PersonnelStatus.Rejected) + throw new ArgumentException("只有已拒绝的人员才能重新提交"); + + var previousStatus = personnel.Status; + + // 重新提交后,状态变为待审批 + personnel.Status = PersonnelStatus.Pending; + personnel.SubmittedAt = DateTime.UtcNow; // 更新提交时间 + + await _context.SaveChangesAsync(); + + // 记录审批历史 + var userId = await GetUserIdByUnitAsync(personnel.SubmittedByUnitId); + await RecordApprovalHistoryAsync(personnelId, PersonnelApprovalAction.Approved, + previousStatus, PersonnelStatus.Pending, null, null, + userId, personnel.SubmittedByUnitId, "重新提交审批"); + + _logger.LogInformation("人员 {PersonnelId} 已由单位 {UnitId} 重新提交审批", + personnelId, personnel.SubmittedByUnitId); + + return personnel; + } } diff --git a/src/MilitaryTrainingManagement/Services/Interfaces/IPersonnelService.cs b/src/MilitaryTrainingManagement/Services/Interfaces/IPersonnelService.cs index bc7b339..9fb32c9 100644 --- a/src/MilitaryTrainingManagement/Services/Interfaces/IPersonnelService.cs +++ b/src/MilitaryTrainingManagement/Services/Interfaces/IPersonnelService.cs @@ -95,6 +95,11 @@ public interface IPersonnelService /// 师部直接升级团级人才为师级 /// Task DirectUpgradeAsync(int personnelId, int divisionUnitId); + + /// + /// 重新提交被拒绝的人才审批 + /// + Task ResubmitPersonnelAsync(int personnelId); } /// diff --git a/src/MilitaryTrainingManagement/wwwroot/uploads/documents/5361d03e-b068-452a-80c4-de732c36dc20.jpg b/src/MilitaryTrainingManagement/wwwroot/uploads/documents/5361d03e-b068-452a-80c4-de732c36dc20.jpg new file mode 100644 index 0000000..055625a Binary files /dev/null and b/src/MilitaryTrainingManagement/wwwroot/uploads/documents/5361d03e-b068-452a-80c4-de732c36dc20.jpg differ diff --git a/src/MilitaryTrainingManagement/wwwroot/uploads/documents/8e590f7d-3b77-4744-8749-3b3d25feb14c.jpg b/src/MilitaryTrainingManagement/wwwroot/uploads/documents/8e590f7d-3b77-4744-8749-3b3d25feb14c.jpg new file mode 100644 index 0000000..444ac7e Binary files /dev/null and b/src/MilitaryTrainingManagement/wwwroot/uploads/documents/8e590f7d-3b77-4744-8749-3b3d25feb14c.jpg differ diff --git a/src/MilitaryTrainingManagement/wwwroot/uploads/photos/3d15bd43-20e9-432a-bba3-ab1b224c0c92.jpg b/src/MilitaryTrainingManagement/wwwroot/uploads/photos/3d15bd43-20e9-432a-bba3-ab1b224c0c92.jpg new file mode 100644 index 0000000..055625a Binary files /dev/null and b/src/MilitaryTrainingManagement/wwwroot/uploads/photos/3d15bd43-20e9-432a-bba3-ab1b224c0c92.jpg differ diff --git a/src/MilitaryTrainingManagement/wwwroot/uploads/photos/3fccae6b-1d22-4612-bdf7-1fb425a8a0fa.jpg b/src/MilitaryTrainingManagement/wwwroot/uploads/photos/3fccae6b-1d22-4612-bdf7-1fb425a8a0fa.jpg new file mode 100644 index 0000000..444ac7e Binary files /dev/null and b/src/MilitaryTrainingManagement/wwwroot/uploads/photos/3fccae6b-1d22-4612-bdf7-1fb425a8a0fa.jpg differ diff --git a/src/frontend/src/api/personnel.ts b/src/frontend/src/api/personnel.ts index ffba0c9..174b3e6 100644 --- a/src/frontend/src/api/personnel.ts +++ b/src/frontend/src/api/personnel.ts @@ -83,6 +83,12 @@ export const personnelApi = { return response.data }, + // 重新提交被拒绝的人才审批 + async resubmit(personnelId: number): Promise { + const response = await apiClient.post(`/personnel/${personnelId}/resubmit`) + return response.data + }, + // 获取审批历史 async getApprovalHistory(personnelId: number): Promise { const response = await apiClient.get(`/personnel/${personnelId}/approval-history`) diff --git a/src/frontend/src/views/AuditLogs.vue b/src/frontend/src/views/AuditLogs.vue index 7b88f8f..81f4c5a 100644 --- a/src/frontend/src/views/AuditLogs.vue +++ b/src/frontend/src/views/AuditLogs.vue @@ -14,14 +14,21 @@ - - + + + + + + + - + + + @@ -158,7 +165,29 @@ const pagination = reactive({ }) function formatDate(dateStr: string): string { - return new Date(dateStr).toLocaleString('zh-CN') + // 后端返回的是 UTC 时间,需要转换为本地时间 + const date = new Date(dateStr) + // 如果日期字符串不包含时区信息,假设它是 UTC 时间 + if (!dateStr.includes('Z') && !dateStr.includes('+') && !dateStr.includes('-', 10)) { + // 添加 Z 后缀表示 UTC 时间 + const utcDate = new Date(dateStr + 'Z') + return utcDate.toLocaleString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }) + } + return date.toLocaleString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }) } function getActionName(action: string): string { @@ -166,10 +195,15 @@ function getActionName(action: string): string { 'Create': '创建', 'Update': '更新', 'Delete': '删除', - 'Approve': '审批', - 'Reject': '拒绝', + 'Approve': '审批通过', + 'Reject': '审批拒绝', + 'RequestUpgrade': '向上申报', + 'ApproveUpgrade': '批准升级', + 'RejectUpgrade': '拒绝升级', + 'DirectUpgrade': '直接升级', 'Submit': '提交', - 'Review': '审核' + 'Review': '审核', + 'Resubmit': '重新提交' } return actionMap[action] || action } @@ -181,8 +215,13 @@ function getActionTagType(action: string): string { 'Delete': 'danger', 'Approve': 'success', 'Reject': 'danger', + 'RequestUpgrade': 'primary', + 'ApproveUpgrade': 'success', + 'RejectUpgrade': 'danger', + 'DirectUpgrade': 'success', 'Submit': 'primary', - 'Review': 'info' + 'Review': 'info', + 'Resubmit': 'primary' } return typeMap[action] || 'info' } @@ -191,6 +230,8 @@ function getEntityTypeName(entityType: string): string { const nameMap: Record = { 'MaterialAllocation': '物资配额', 'AllocationDistribution': '配额分配', + 'ConsumptionReport': '消耗记录', + 'ConsumptionReportChangeRequest': '消耗删改申请', 'UserAccount': '用户账户', 'OrganizationalUnit': '组织单位', 'Personnel': '人员信息', @@ -224,7 +265,8 @@ async function loadLogs() { } const result = await auditLogsApi.getAll(params) - logs.value = result.items + // 过滤掉没有操作人的系统记录 + logs.value = result.items.filter(log => log.userId != null) pagination.totalCount = result.totalCount } catch { ElMessage.error('加载操作记录失败') diff --git a/src/frontend/src/views/personnel/PersonnelForm.vue b/src/frontend/src/views/personnel/PersonnelForm.vue index f5c45d9..36e66b6 100644 --- a/src/frontend/src/views/personnel/PersonnelForm.vue +++ b/src/frontend/src/views/personnel/PersonnelForm.vue @@ -300,6 +300,8 @@ async function loadPersonnel() { form.unit = person.unit || '' form.position = person.position form.rank = person.rank + form.gender = person.gender || '男' + form.age = person.age || 25 form.idNumber = person.idNumber || '' form.professionalTitle = person.professionalTitle || '' form.politicalStatus = person.politicalStatus || '' @@ -351,6 +353,8 @@ async function handleSubmit() { formData.append('name', form.name) formData.append('position', form.position) formData.append('rank', form.rank) + formData.append('gender', form.gender) + formData.append('age', form.age.toString()) // 可选字段 - 只有非空时才添加 if (form.unit) formData.append('unit', form.unit) diff --git a/src/frontend/src/views/personnel/PersonnelList.vue b/src/frontend/src/views/personnel/PersonnelList.vue index 8398b3f..5561263 100644 --- a/src/frontend/src/views/personnel/PersonnelList.vue +++ b/src/frontend/src/views/personnel/PersonnelList.vue @@ -30,9 +30,6 @@ 已批准 - - 已拒绝 - @@ -68,10 +65,7 @@ @@ -117,11 +111,16 @@ - + + + + + + @@ -206,7 +205,7 @@ import { ref, reactive, watch, onMounted } from 'vue' import { useRouter } from 'vue-router' import { ElMessage, ElMessageBox } from 'element-plus' -import { Plus, Search, View, Edit, Delete, Check, Close, User, Top } from '@element-plus/icons-vue' +import { Plus, Search, View, Edit, Delete, Check, Close, User, Top, Upload } from '@element-plus/icons-vue' import { useAuthStore } from '@/stores/auth' import { personnelApi } from '@/api' import type { Personnel } from '@/types' @@ -252,27 +251,68 @@ function getStatusName(status: PersonnelStatus): string { // 根据当前用户判断显示的状态文字 function getDisplayStatus(person: Personnel): string { + // 如果有待升级标记,显示升级相关状态 + if (person.pendingUpgradeByUnitId) { + if (canApproveUpgrade(person)) { + return '待审批' + } else { + return '待上级审批' + } + } + + // 已拒绝状态直接返回 + if (person.status === PersonnelStatus.Rejected) { + return '已拒绝' + } + + // 已批准状态直接返回 + if (person.status === PersonnelStatus.Approved) { + return '已批准' + } + + // Pending 状态的处理 if (person.status === PersonnelStatus.Pending) { - // 待审批状态:判断当前用户是否能审批 if (canApprovePersonnel(person)) { return '待审批' } else { return '待上级审批' } } + + // 其他情况 return getStatusName(person.status) } // 根据当前用户判断显示的状态标签类型 function getDisplayStatusType(person: Personnel): string { + // 如果有待升级标记,显示升级相关状态 + if (person.pendingUpgradeByUnitId) { + if (canApproveUpgrade(person)) { + return 'warning' // 待审批 - 黄色 + } else { + return 'primary' // 待上级审批 - 蓝色 + } + } + + // 已拒绝状态 - 红色 + if (person.status === PersonnelStatus.Rejected) { + return 'danger' + } + + // 已批准状态直接返回 + if (person.status === PersonnelStatus.Approved) { + return 'success' // 已批准 - 绿色 + } + + // Pending 状态的处理 if (person.status === PersonnelStatus.Pending) { - // 待审批状态:判断当前用户是否能审批 if (canApprovePersonnel(person)) { return 'warning' // 待审批 - 黄色 } else { return 'primary' // 待上级审批 - 蓝色 } } + return getStatusTagType(person.status) } @@ -422,7 +462,7 @@ function canApprovePersonnel(person: Personnel): boolean { if (person.submittedByUnitId === authStore.user.organizationalUnitId) return false // 用户层级必须高于提交单位层级(这里简化处理,假设能看到的数据都是下级提交的) // 实际权限检查由后端完成 - return true + return authStore.organizationalLevelNum < 4 // 只有营级及以上可以审批 } // 判断是否可以删除该人员 @@ -436,7 +476,17 @@ function canDelete(person: Personnel): boolean { return true } - // 已批准状态:用户层级必须高于或等于人员等级才能删除 + // 已拒绝状态:只有提交单位可以删除 + if (person.status === PersonnelStatus.Rejected) { + return person.submittedByUnitId === authStore.user.organizationalUnitId + } + + // 已批准状态但没有等级:这是异常状态,允许删除 + if (person.status === PersonnelStatus.Approved && !person.approvedLevel) { + return true + } + + // 已批准状态且有等级:用户层级必须高于或等于人员等级才能删除 if (person.status === PersonnelStatus.Approved && person.approvedLevel) { const personnelLevelNum = { 'Division': 1, @@ -451,6 +501,37 @@ function canDelete(person: Personnel): boolean { return false } +// 判断是否可以重新提交 +function canResubmit(person: Personnel): boolean { + if (!authStore.user) return false + // 只有提交单位可以重新提交被拒绝的人才 + return person.status === PersonnelStatus.Rejected && + person.submittedByUnitId === authStore.user.organizationalUnitId +} + +// 重新提交被拒绝的人才 +async function handleResubmit(person: Personnel) { + try { + await ElMessageBox.confirm( + `确定要重新提交 "${person.name}" 的审批申请吗?\n重新提交后,该人才将进入待审批状态。`, + '确认重新提交', + { + type: 'warning', + confirmButtonText: '确认提交', + cancelButtonText: '取消' + } + ) + + await personnelApi.resubmit(person.id) + ElMessage.success('已重新提交审批,等待上级审批') + await loadPersonnel() + } catch (error: any) { + if (error !== 'cancel') { + ElMessage.error(error.response?.data?.message || '重新提交失败') + } + } +} + function handleApprove(person: Personnel) { selectedPerson.value = person approvalForm.comments = ''