diff --git a/YD_Event.Application/Client/Dtos/EventClientDtos.cs b/YD_Event.Application/Client/Dtos/EventClientDtos.cs index 7216f11..e5687a7 100644 --- a/YD_Event.Application/Client/Dtos/EventClientDtos.cs +++ b/YD_Event.Application/Client/Dtos/EventClientDtos.cs @@ -240,6 +240,66 @@ public class PointsRuleDto public List? Details { get; set; } } +/// +/// 退款规则DTO +/// +public class RefundRuleDto +{ + /// + /// 距离比赛开始天数(起) + /// + public int DayStart { get; set; } + + /// + /// 距离比赛开始天数(止) + /// + public int DayEnd { get; set; } + + /// + /// 扣除比例(%) + /// + public decimal DeductPercentage { get; set; } + + /// + /// 退款备注 + /// + public string? Remark { get; set; } +} + +/// +/// 积分规则明细快照DTO +/// +public class PointsRuleDetailSnapshotDto +{ + public int StartRank { get; set; } + public int EndRank { get; set; } + public int Points { get; set; } +} + +/// +/// 积分规则快照DTO +/// +public class PointsRuleSnapshotDto +{ + public long PointsRuleId { get; set; } + public string RuleName { get; set; } + public int? ParticipationPoints { get; set; } + public string? Remark { get; set; } + public List Details { get; set; } = new List(); +} + +/// +/// 退款规则快照DTO +/// +public class RefundRuleSnapshotDto +{ + public long RefundConfigId { get; set; } + public int DayStart { get; set; } + public int DayEnd { get; set; } + public decimal DeductPercentage { get; set; } + public string? Remark { get; set; } +} + /// /// 赛事详情DTO /// @@ -314,6 +374,11 @@ public class EventDetailDto : EventListDto /// 积分规则详情 /// public PointsRuleDto? PointsRuleDetail { get; set; } + + /// + /// 退款规则详情列表 + /// + public List? RefundRules { get; set; } /// /// 个人项目限报数量 diff --git a/YD_Event.Application/Client/EventClientController.cs b/YD_Event.Application/Client/EventClientController.cs index 5b2795e..a13b837 100644 --- a/YD_Event.Application/Client/EventClientController.cs +++ b/YD_Event.Application/Client/EventClientController.cs @@ -65,6 +65,7 @@ public class EventClientController : IDynamicApiController private readonly YD_Event.Application.Banner.BannerService _bannerService; private readonly UnifiedRegistrationService _unifiedRegistrationService; private readonly YD_Event.Application.Refund.RefundConfigService _refundConfigService; + private readonly EventRuleSnapshotService _ruleSnapshotService; public EventClientController( ISqlSugarRepository eventRepository, @@ -101,7 +102,8 @@ public class EventClientController : IDynamicApiController OrderNoGeneratorService orderNoGeneratorService, YD_Event.Application.Banner.BannerService bannerService, UnifiedRegistrationService unifiedRegistrationService, - YD_Event.Application.Refund.RefundConfigService refundConfigService) + YD_Event.Application.Refund.RefundConfigService refundConfigService, + EventRuleSnapshotService ruleSnapshotService) { _eventRepository = eventRepository; _projectRepository = projectRepository; @@ -138,6 +140,7 @@ public class EventClientController : IDynamicApiController _bannerService = bannerService; _unifiedRegistrationService = unifiedRegistrationService; _refundConfigService = refundConfigService; + _ruleSnapshotService = ruleSnapshotService; } private long GetCurrentUserId() @@ -292,29 +295,39 @@ public class EventClientController : IDynamicApiController if (district != null) districtName = district.DistrictName; } - PointsRuleDto? pointsRuleDetail = null; - if (evt.PointsRuleId.HasValue) + // 获取规则快照 + var pointsRuleSnapshot = await _ruleSnapshotService.GetPointsRuleSnapshot(evt.Id); + var refundRuleSnapshots = await _ruleSnapshotService.GetRefundRuleSnapshot(evt.Id); + + // 使用快照数据转换为详情DTO + YD_Event.Application.Client.Dtos.PointsRuleDto? pointsRuleDetail = null; + List? refundRules = null; + + if (pointsRuleSnapshot != null) { - var pointsRuleEntity = await _pointsRuleRepository.GetFirstAsync(r => r.Id == evt.PointsRuleId.Value && !r.DeleteMark); - if (pointsRuleEntity != null) + pointsRuleDetail = new YD_Event.Application.Client.Dtos.PointsRuleDto { - var details = await _pointsRuleDetailRepository.AsQueryable() - .Where(d => d.PointsRuleId == pointsRuleEntity.Id && !d.DeleteMark) - .ToListAsync(); - - pointsRuleDetail = new PointsRuleDto + RuleName = pointsRuleSnapshot.RuleName, + ParticipationPoints = pointsRuleSnapshot.ParticipationPoints, + Remark = pointsRuleSnapshot.Remark, + Details = pointsRuleSnapshot.Details.Select(d => new YD_Event.Application.Client.Dtos.PointsRuleDetailDto { - RuleName = pointsRuleEntity.RuleName, - ParticipationPoints = pointsRuleEntity.ParticipationPoints, - Remark = pointsRuleEntity.Remark, - Details = details.Select(d => new PointsRuleDetailDto - { - StartRank = d.StartRank, - EndRank = d.EndRank, - Points = d.Points - }).ToList() - }; - } + StartRank = d.StartRank, + EndRank = d.EndRank, + Points = d.Points + }).ToList() + }; + } + + if (refundRuleSnapshots != null && refundRuleSnapshots.Any()) + { + refundRules = refundRuleSnapshots.Select(r => new YD_Event.Application.Client.Dtos.RefundRuleDto + { + DayStart = r.DayStart, + DayEnd = r.DayEnd, + DeductPercentage = r.DeductPercentage, + Remark = r.Remark + }).ToList(); } var eventProjects = await _eventProjectRepository.AsQueryable() @@ -355,31 +368,28 @@ public class EventClientController : IDynamicApiController string refundRule = "开赛前7天扣除金额5%,3-7天扣除金额10%,3天内扣除金额20%"; string pointsRule = "参与积分:5积分;名次积分:1-8名 15积分,9-20名 10积分"; - // 优先从 RefundConfig 表读取配置并生成描述文本 - try + // 优先从快照读取配置并生成描述文本 + if (refundRuleSnapshots != null && refundRuleSnapshots.Any()) { - var refundConfigs = await _refundConfigService.GetList(); - var enabledConfigs = refundConfigs.Where(x => x.Status == Core.Enum.AvailabilityStatus.Enable).OrderBy(x => x.DayStart).ToList(); - if (enabledConfigs.Any()) + var ruleParts = new List(); + foreach (var config in refundRuleSnapshots) { - var ruleParts = new List(); - foreach (var config in enabledConfigs) + if (config.DayEnd >= 999) { - if (config.DayEnd >= 999) - { - ruleParts.Add($"开赛前{config.DayStart}天及以上扣除金额{Math.Round(config.DeductPercentage)}%"); - } - else - { - ruleParts.Add($"开赛前{config.DayStart}-{config.DayEnd}天扣除金额{Math.Round(config.DeductPercentage)}%"); - } + ruleParts.Add($"开赛前{config.DayStart}天及以上扣除金额{Math.Round(config.DeductPercentage)}%"); + } + else + { + ruleParts.Add($"开赛前{config.DayStart}-{config.DayEnd}天扣除金额{Math.Round(config.DeductPercentage)}%"); } - refundRule = string.Join(",", ruleParts); } + refundRule = string.Join(",", ruleParts); } - catch (Exception ex) + + // 积分规则文本从快照获取 + if (pointsRuleSnapshot != null && !string.IsNullOrWhiteSpace(pointsRuleSnapshot.Remark)) { - _logger.LogWarning(ex, "读取退款配置失败,赛事ID={EventId},使用默认规则", evt.Id); + pointsRule = pointsRuleSnapshot.Remark; } // 如果有自定义配置,优先使用自定义配置(保持向后兼容) @@ -406,15 +416,6 @@ public class EventClientController : IDynamicApiController } } - if (evt.PointsRuleId.HasValue) - { - var pointsRuleEntity = await _pointsRuleRepository.GetFirstAsync(r => r.Id == evt.PointsRuleId.Value && !r.DeleteMark); - if (pointsRuleEntity != null) - { - pointsRule = pointsRuleEntity.Remark ?? pointsRule; - } - } - EventFeeConfigDto? feeConfigDto = null; if (feeConfig != null) { @@ -472,6 +473,7 @@ public class EventClientController : IDynamicApiController RefundRule = refundRule, PointsRule = pointsRule, PointsRuleDetail = pointsRuleDetail, + RefundRules = refundRules, IsRegistered = isRegistered, ProvinceName = provinceName, CityName = cityName, @@ -945,18 +947,17 @@ public class EventClientController : IDynamicApiController var order = await _orderRepository.GetFirstAsync(x => x.RegistrationId == registrationId && x.OrderType == 1); if (order != null) { - refundAmount = await CalculateRefundAmount(order.ActualAmount, reg.MatchStart); + refundAmount = await CalculateRefundAmount(order.ActualAmount, reg.MatchStart, reg.EventId); } - // 获取退款规则文本 + // 获取退款规则文本(从赛事快照获取) try { - var refundConfigs = await _refundConfigService.GetList(); - var enabledConfigs = refundConfigs.Where(x => x.Status == Core.Enum.AvailabilityStatus.Enable).OrderBy(x => x.DayStart).ToList(); - if (enabledConfigs.Any()) + var snapshots = await _ruleSnapshotService.GetRefundRuleSnapshot(reg.EventId); + if (snapshots.Any()) { var ruleParts = new List(); - foreach (var config in enabledConfigs) + foreach (var config in snapshots) { if (config.DayEnd >= 999 || config.DayEnd >= int.MaxValue) { @@ -1495,30 +1496,9 @@ public class EventClientController : IDynamicApiController /// /// 计算退款金额 /// - private async Task CalculateRefundAmount(decimal totalAmount, DateTime eventStartTime) + private async Task CalculateRefundAmount(decimal totalAmount, DateTime eventStartTime, long eventId) { - var configs = await _refundConfigRepository.AsQueryable() - .Where(x => x.Status == Core.Enum.AvailabilityStatus.Enable && !x.DeleteMark) - .OrderBy(x => x.DayStart) - .ToListAsync(); - - if (!configs.Any()) return totalAmount; - - var daysRemaining = (eventStartTime - DateTime.Now).TotalDays; - if (daysRemaining < 0) daysRemaining = 0; - - var matchConfig = configs.FirstOrDefault(c => - daysRemaining >= c.DayStart && - (c.DayEnd == int.MaxValue ? true : daysRemaining < c.DayEnd) - ); - - if (matchConfig != null) - { - var deductAmount = totalAmount * (matchConfig.DeductPercentage / 100m); - return totalAmount - deductAmount; - } - - return totalAmount; + return await _refundConfigService.CalculateRefundAmountByEvent(totalAmount, eventStartTime, eventId); } /// diff --git a/YD_Event.Application/Event/Dtos/EventDtos.cs b/YD_Event.Application/Event/Dtos/EventDtos.cs index 13024a9..85d20d4 100644 --- a/YD_Event.Application/Event/Dtos/EventDtos.cs +++ b/YD_Event.Application/Event/Dtos/EventDtos.cs @@ -543,4 +543,61 @@ public class EventDetailOutput : AddEventInput /// 二维码URL /// public string? QrCodeUrl { get; set; } + + /// + /// 积分规则详情 + /// + public PointsRuleDto? PointsRuleDetail { get; set; } + + /// + /// 退款规则详情列表 + /// + public List? RefundRules { get; set; } +} + +/// +/// 积分规则明细DTO +/// +public class PointsRuleDetailDto +{ + /// + /// 起始名次 + /// + public int StartRank { get; set; } + + /// + /// 截止名次 + /// + public int EndRank { get; set; } + + /// + /// 发放积分个数 + /// + public int Points { get; set; } +} + +/// +/// 积分规则DTO +/// +public class PointsRuleDto +{ + /// + /// 积分规则名称 + /// + public string RuleName { get; set; } + + /// + /// 参与积分(发放积分个数) + /// + public int? ParticipationPoints { get; set; } + + /// + /// 备注 + /// + public string? Remark { get; set; } + + /// + /// 名次积分规则明细 + /// + public List? Details { get; set; } } diff --git a/YD_Event.Application/Event/Dtos/Mapper.cs b/YD_Event.Application/Event/Dtos/Mapper.cs new file mode 100644 index 0000000..892e827 --- /dev/null +++ b/YD_Event.Application/Event/Dtos/Mapper.cs @@ -0,0 +1,13 @@ +namespace YD_Event.Application.Event.Dtos; + +public class Mapper : IRegister +{ + public void Register(TypeAdapterConfig config) + { + // 配置EventInfo到EventDetailOutput的映射 + config.ForType(); + + // 配置EventInfo到UserEventDetailOutput的映射 + config.ForType(); + } +} diff --git a/YD_Event.Application/Event/Dtos/RegistrationDtos.cs b/YD_Event.Application/Event/Dtos/RegistrationDtos.cs index 9db8807..7be742e 100644 --- a/YD_Event.Application/Event/Dtos/RegistrationDtos.cs +++ b/YD_Event.Application/Event/Dtos/RegistrationDtos.cs @@ -295,3 +295,34 @@ public class BatchPaymentAuditInput /// public string? ReviewRemark { get; set; } } + +/// +/// 报名统计输出 +/// +public class RegistrationStatsOutput +{ + /// + /// 报名总人数 + /// + public int TotalParticipants { get; set; } + + /// + /// 个人项目人数 + /// + public int IndividualParticipants { get; set; } + + /// + /// 团队项目队伍数 + /// + public int TeamCount { get; set; } + + /// + /// 团队项目人数 + /// + public int TeamParticipants { get; set; } + + /// + /// 待审核数量 + /// + public int PendingReview { get; set; } +} diff --git a/YD_Event.Application/Event/Dtos/RuleSnapshotDtos.cs b/YD_Event.Application/Event/Dtos/RuleSnapshotDtos.cs new file mode 100644 index 0000000..57949b3 --- /dev/null +++ b/YD_Event.Application/Event/Dtos/RuleSnapshotDtos.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using YD_Event.Core.Enum; + +namespace YD_Event.Application.Event.Dtos; + +/// +/// 积分规则详情快照 DTO +/// +public class PointsRuleDetailSnapshotDto +{ + /// + /// 起始名次 + /// + public int StartRank { get; set; } + + /// + /// 截止名次 + /// + public int EndRank { get; set; } + + /// + /// 发放积分个数 + /// + public int Points { get; set; } +} + +/// +/// 积分规则快照 DTO +/// +public class PointsRuleSnapshotDto +{ + /// + /// 积分规则 ID(快照时的 ID) + /// + public long PointsRuleId { get; set; } + + /// + /// 积分规则名称 + /// + public string RuleName { get; set; } + + /// + /// 参与积分(发放积分个数) + /// + public int? ParticipationPoints { get; set; } + + /// + /// 备注 + /// + public string? Remark { get; set; } + + /// + /// 关联的名次积分规则 + /// + public List Details { get; set; } = new List(); +} + +/// +/// 退款规则快照 DTO +/// +public class RefundRuleSnapshotDto +{ + /// + /// 退款配置 ID(快照时的 ID) + /// + public long RefundConfigId { get; set; } + + /// + /// 距离比赛开始天数(起) + /// + public int DayStart { get; set; } + + /// + /// 距离比赛开始天数(止) + /// + public int DayEnd { get; set; } + + /// + /// 扣除比例 (0-100) + /// + public decimal DeductPercentage { get; set; } + + /// + /// 备注 + /// + public string? Remark { get; set; } +} diff --git a/YD_Event.Application/Event/EventRegistrationService.cs b/YD_Event.Application/Event/EventRegistrationService.cs index 19732de..604f61d 100644 --- a/YD_Event.Application/Event/EventRegistrationService.cs +++ b/YD_Event.Application/Event/EventRegistrationService.cs @@ -668,4 +668,106 @@ public class EventRegistrationService : BaseService, ITransie PaymentTime = input.PaymentStatus == 1 ? now : x.PaymentTime }, x => input.Ids.Contains(x.Id)); } + + /// + /// 获取报名统计数据 + /// + [DisplayName("获取报名统计数据")] + [HttpGet("stats/{eventId}")] + public async Task GetStats(long eventId) + { + var stats = new RegistrationStatsOutput(); + + // 获取赛事项目信息 + var eventProjects = await _eventProjectRepository.GetListAsync(x => x.EventId == eventId); + var projectIds = eventProjects.Select(x => x.ProjectId).Distinct().ToList(); + var projects = await _projectRepository.GetListAsync(x => projectIds.Contains(x.Id)); + var eventProjectToProjectDict = eventProjects.ToDictionary(x => x.Id, x => x.ProjectId); + var projectDict = projects.ToDictionary(x => x.Id, x => x); + + // 统计符合条件的所有运动员(与赛事编排页面保持一致的筛选规则) + var athletes = await _personRepository.AsQueryable() + .InnerJoin((p, r) => p.RegistrationId == r.Id) + .Where((p, r) => r.EventId == eventId + // 线上支付只显示已支付(1),线下支付显示待审核(0)和已支付(1) + && ((r.PaymentMethod == 2 && r.PaymentStatus == 1) || (r.PaymentMethod == 1 && (r.PaymentStatus == 0 || r.PaymentStatus == 1))) + && !p.DeleteMark // 排除已删除的人员 + && p.PersonType == PersonType.Athlete) // 只统计运动员 + .Select((p, r) => new + { + PersonId = p.Id, + EventProjectId = p.ProjectId, + GroupName = p.GroupName, + TeamName = r.TeamName, + RegistrationItemId = p.RegistrationItemId, + UserId = p.UserId, + RegistrationId = r.Id, + PaymentMethod = r.PaymentMethod, + PaymentStatus = r.PaymentStatus + }) + .ToListAsync(); + + if (!athletes.Any()) + { + // 待审核数量:线下支付(1)且支付状态为0(待审核) + var pendingRegistrations = await _repository.GetListAsync(x => + x.EventId == eventId && !x.DeleteMark && x.PaymentMethod == 1 && x.PaymentStatus == 0); + stats.PendingReview = pendingRegistrations.Count; + return stats; + } + + // 计算总人数(所有符合条件的运动员) + stats.TotalParticipants = athletes.Select(x => x.UserId > 0 ? x.UserId : x.PersonId).Distinct().Count(); + + // 分别计算个人项目和团队项目的统计数据 + var individualSet = new HashSet(); + var teamSet = new HashSet<(long EventProjectId, string GroupName, string TeamName, long RegistrationItemId)>(); + var teamPersonSet = new HashSet(); + + foreach (var athlete in athletes) + { + // 判断项目类型 + bool isIndividualProject = false; + if (eventProjectToProjectDict.TryGetValue(athlete.EventProjectId, out var projectId) && + projectDict.TryGetValue(projectId, out var project)) + { + isIndividualProject = YD_Event.Core.Extensions.ProjectTypeExtensions.IsIndividualProject(project); + } + + var personKey = athlete.UserId > 0 ? athlete.UserId : athlete.PersonId; + + if (isIndividualProject) + { + // 个人项目:统计人数 + individualSet.Add(personKey); + } + else + { + // 团队项目:统计团队数(与赛事编排一致,按 EventProjectId、GroupName、TeamName、RegistrationItemId 分组) + if (!string.IsNullOrEmpty(athlete.TeamName)) + { + teamSet.Add(( + athlete.EventProjectId, + athlete.GroupName ?? "", + athlete.TeamName, + athlete.RegistrationItemId + )); + } + + // 团队项目人数 + teamPersonSet.Add(personKey); + } + } + + stats.IndividualParticipants = individualSet.Count; + stats.TeamCount = teamSet.Count; + stats.TeamParticipants = teamPersonSet.Count; + + // 待审核数量:线下支付(1)且支付状态为0(待审核) + var pendingRegs = await _repository.GetListAsync(x => + x.EventId == eventId && !x.DeleteMark && x.PaymentMethod == 1 && x.PaymentStatus == 0); + stats.PendingReview = pendingRegs.Count; + + return stats; + } } diff --git a/YD_Event.Application/Event/EventRuleSnapshotService.cs b/YD_Event.Application/Event/EventRuleSnapshotService.cs new file mode 100644 index 0000000..56c200f --- /dev/null +++ b/YD_Event.Application/Event/EventRuleSnapshotService.cs @@ -0,0 +1,257 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using YD_Event.Application.Event.Dtos; +using YD_Event.Core.Entities; +using SqlSugar; + +namespace YD_Event.Application.Event; + +/// +/// 赛事规则快照服务 - 管理积分规则和退款规则的快照 +/// +public class EventRuleSnapshotService : ITransient +{ + private readonly ISqlSugarRepository _eventInfoRepository; + private readonly ISqlSugarRepository _pointsRuleRepository; + private readonly ISqlSugarRepository _pointsRuleDetailRepository; + private readonly ISqlSugarRepository _refundConfigRepository; + + // JSON 序列化选项,支持中文正常显示 + private static readonly System.Text.Json.JsonSerializerOptions _jsonOptions = new System.Text.Json.JsonSerializerOptions + { + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + WriteIndented = false + }; + + public EventRuleSnapshotService( + ISqlSugarRepository eventInfoRepository, + ISqlSugarRepository pointsRuleRepository, + ISqlSugarRepository pointsRuleDetailRepository, + ISqlSugarRepository refundConfigRepository) + { + _eventInfoRepository = eventInfoRepository; + _pointsRuleRepository = pointsRuleRepository; + _pointsRuleDetailRepository = pointsRuleDetailRepository; + _refundConfigRepository = refundConfigRepository; + } + + #region 积分规则快照 + + /// + /// 保存积分规则快照到赛事 + /// + /// 赛事 ID + /// 积分规则 ID + /// + [DisplayName("保存积分规则快照")] + public async Task SavePointsRuleSnapshot(long eventId, long? pointsRuleId) + { + var eventInfo = await _eventInfoRepository.GetByIdAsync(eventId); + if (eventInfo == null) + throw new InvalidOperationException("赛事不存在"); + + if (!pointsRuleId.HasValue || pointsRuleId.Value <= 0) + { + eventInfo.PointsRuleSnapshot = null; + await _eventInfoRepository.UpdateAsync(eventInfo); + return; + } + + var pointsRule = await _pointsRuleRepository.GetByIdAsync(pointsRuleId.Value); + if (pointsRule == null) + throw new InvalidOperationException("积分规则不存在"); + + var details = await _pointsRuleDetailRepository.GetListAsync(x => + x.PointsRuleId == pointsRuleId.Value && !x.DeleteMark); + + var snapshot = new PointsRuleSnapshotDto + { + PointsRuleId = pointsRule.Id, + RuleName = pointsRule.RuleName, + ParticipationPoints = pointsRule.ParticipationPoints, + Remark = pointsRule.Remark, + Details = details.Select(d => new PointsRuleDetailSnapshotDto + { + StartRank = d.StartRank, + EndRank = d.EndRank, + Points = d.Points + }).ToList() + }; + + eventInfo.PointsRuleSnapshot = System.Text.Json.JsonSerializer.Serialize(snapshot, _jsonOptions); + await _eventInfoRepository.UpdateAsync(eventInfo); + } + + /// + /// 获取赛事积分规则快照 + /// + /// 赛事 ID + /// 积分规则快照,如果不存在则返回 null + [DisplayName("获取积分规则快照")] + public async Task GetPointsRuleSnapshot(long eventId) + { + var eventInfo = await _eventInfoRepository.GetByIdAsync(eventId); + if (eventInfo == null) + throw new InvalidOperationException("赛事不存在"); + + if (string.IsNullOrWhiteSpace(eventInfo.PointsRuleSnapshot)) + return null; + + try + { + return System.Text.Json.JsonSerializer.Deserialize(eventInfo.PointsRuleSnapshot, _jsonOptions); + } + catch (Exception) + { + return null; + } + } + + #endregion + + #region 退款规则快照 + + /// + /// 保存退款规则快照到赛事 + /// + /// 赛事 ID + /// + [DisplayName("保存退款规则快照")] + public async Task SaveRefundRuleSnapshot(long eventId) + { + var eventInfo = await _eventInfoRepository.GetByIdAsync(eventId); + if (eventInfo == null) + throw new InvalidOperationException("赛事不存在"); + + var refundConfigs = await _refundConfigRepository.GetListAsync(x => + x.Status == Core.Enum.AvailabilityStatus.Enable && !x.DeleteMark); + + if (!refundConfigs.Any()) + { + eventInfo.RefundRuleSnapshot = null; + await _eventInfoRepository.UpdateAsync(eventInfo); + return; + } + + var snapshots = refundConfigs.OrderBy(c => c.DayStart).Select(c => new RefundRuleSnapshotDto + { + RefundConfigId = c.Id, + DayStart = c.DayStart, + DayEnd = c.DayEnd, + DeductPercentage = c.DeductPercentage, + Remark = c.Remark + }).ToList(); + + eventInfo.RefundRuleSnapshot = System.Text.Json.JsonSerializer.Serialize(snapshots, _jsonOptions); + await _eventInfoRepository.UpdateAsync(eventInfo); + } + + /// + /// 获取赛事退款规则快照 + /// + /// 赛事 ID + /// 退款规则快照列表,如果不存在则返回空列表 + [DisplayName("获取退款规则快照")] + public async Task> GetRefundRuleSnapshot(long eventId) + { + var eventInfo = await _eventInfoRepository.GetByIdAsync(eventId); + if (eventInfo == null) + throw new InvalidOperationException("赛事不存在"); + + if (string.IsNullOrWhiteSpace(eventInfo.RefundRuleSnapshot)) + return new List(); + + try + { + var snapshots = System.Text.Json.JsonSerializer.Deserialize>(eventInfo.RefundRuleSnapshot, _jsonOptions); + return snapshots ?? new List(); + } + catch (Exception) + { + return new List(); + } + } + + #endregion + + #region 快捷方法 + + /// + /// 同时保存积分规则和退款规则快照 + /// + /// 赛事 ID + /// 积分规则 ID + /// + [DisplayName("保存所有规则快照")] + public async Task SaveAllRuleSnapshots(long eventId, long? pointsRuleId) + { + await SavePointsRuleSnapshot(eventId, pointsRuleId); + await SaveRefundRuleSnapshot(eventId); + } + + /// + /// 根据赛事实体保存快照(用于创建/编辑时) + /// + /// 赛事实体 + /// + [DisplayName("根据赛事实体保存快照")] + public async Task SaveAllRuleSnapshots(EventInfo eventInfo) + { + if (eventInfo.PointsRuleId.HasValue && eventInfo.PointsRuleId.Value > 0) + { + var pointsRule = await _pointsRuleRepository.GetByIdAsync(eventInfo.PointsRuleId.Value); + if (pointsRule != null) + { + var details = await _pointsRuleDetailRepository.GetListAsync(x => + x.PointsRuleId == eventInfo.PointsRuleId.Value && !x.DeleteMark); + + var snapshot = new PointsRuleSnapshotDto + { + PointsRuleId = pointsRule.Id, + RuleName = pointsRule.RuleName, + ParticipationPoints = pointsRule.ParticipationPoints, + Remark = pointsRule.Remark, + Details = details.Select(d => new PointsRuleDetailSnapshotDto + { + StartRank = d.StartRank, + EndRank = d.EndRank, + Points = d.Points + }).ToList() + }; + + eventInfo.PointsRuleSnapshot = System.Text.Json.JsonSerializer.Serialize(snapshot, _jsonOptions); + } + } + else + { + eventInfo.PointsRuleSnapshot = null; + } + + var refundConfigs = await _refundConfigRepository.GetListAsync(x => + x.Status == Core.Enum.AvailabilityStatus.Enable && !x.DeleteMark); + + if (refundConfigs.Any()) + { + var snapshots = refundConfigs.OrderBy(c => c.DayStart).Select(c => new RefundRuleSnapshotDto + { + RefundConfigId = c.Id, + DayStart = c.DayStart, + DayEnd = c.DayEnd, + DeductPercentage = c.DeductPercentage, + Remark = c.Remark + }).ToList(); + + eventInfo.RefundRuleSnapshot = System.Text.Json.JsonSerializer.Serialize(snapshots, _jsonOptions); + } + else + { + eventInfo.RefundRuleSnapshot = null; + } + } + + #endregion +} diff --git a/YD_Event.Application/Event/EventScoreService.cs b/YD_Event.Application/Event/EventScoreService.cs index 8dafea3..310a26d 100644 --- a/YD_Event.Application/Event/EventScoreService.cs +++ b/YD_Event.Application/Event/EventScoreService.cs @@ -62,6 +62,7 @@ public class EventScoreService : BaseService, ITransient private readonly CertificateGenerateService _certificateGenerateService; private readonly IIdGenerator _idGenerator; private readonly ILogger _logger; + private readonly EventRuleSnapshotService _ruleSnapshotService; public EventScoreService( ISqlSugarRepository personRepository, @@ -82,7 +83,8 @@ public class EventScoreService : BaseService, ITransient ISqlSugarRepository merchantCertificationRepository, CertificateGenerateService certificateGenerateService, IIdGenerator idGenerator, - ILogger logger) : base(resultRepository) + ILogger logger, + EventRuleSnapshotService ruleSnapshotService) : base(resultRepository) { _personRepository = personRepository; _resultRepository = resultRepository; @@ -103,6 +105,7 @@ public class EventScoreService : BaseService, ITransient _certificateGenerateService = certificateGenerateService; _idGenerator = idGenerator; _logger = logger; + _ruleSnapshotService = ruleSnapshotService; } /// @@ -1647,27 +1650,13 @@ public class EventScoreService : BaseService, ITransient throw Oops.Bah("赛事不存在"); } - if (!eventInfo.PointsRuleId.HasValue) + // 从快照获取积分规则 + var pointsRuleSnapshot = await _ruleSnapshotService.GetPointsRuleSnapshot(input.EventId); + if (pointsRuleSnapshot == null) { throw Oops.Bah("该赛事未配置积分规则"); } - var pointsRule = await _pointsRuleRepository.GetByIdAsync(eventInfo.PointsRuleId.Value); - if (pointsRule == null) - { - throw Oops.Bah("积分规则不存在"); - } - - if (pointsRule.Status != AvailabilityStatus.Enable) - { - throw Oops.Bah("积分规则未启用"); - } - - var ruleDetails = await _pointsRuleDetailRepository.AsQueryable() - .Where(x => x.PointsRuleId == pointsRule.Id) - .OrderBy(x => x.StartRank) - .ToListAsync(); - // 获取所有 EventProject 和对应的 Project 信息,用于判断项目类型 var eventProjects = await _eventProjectRepository.AsQueryable() .Where(ep => ep.EventId == input.EventId) @@ -1762,12 +1751,12 @@ public class EventScoreService : BaseService, ITransient int pointsToAward = 0; - if (pointsRule.ParticipationPoints.HasValue && pointsRule.ParticipationPoints.Value > 0) + if (pointsRuleSnapshot.ParticipationPoints.HasValue && pointsRuleSnapshot.ParticipationPoints.Value > 0) { - pointsToAward += pointsRule.ParticipationPoints.Value; + pointsToAward += pointsRuleSnapshot.ParticipationPoints.Value; } - var matchingDetail = ruleDetails.FirstOrDefault(d => teamResult.Rank >= d.StartRank && teamResult.Rank <= d.EndRank); + var matchingDetail = pointsRuleSnapshot.Details.FirstOrDefault(d => teamResult.Rank >= d.StartRank && teamResult.Rank <= d.EndRank); if (matchingDetail != null) { pointsToAward += matchingDetail.Points; @@ -1820,12 +1809,12 @@ public class EventScoreService : BaseService, ITransient int pointsToAward = 0; - if (pointsRule.ParticipationPoints.HasValue && pointsRule.ParticipationPoints.Value > 0) + if (pointsRuleSnapshot.ParticipationPoints.HasValue && pointsRuleSnapshot.ParticipationPoints.Value > 0) { - pointsToAward += pointsRule.ParticipationPoints.Value; + pointsToAward += pointsRuleSnapshot.ParticipationPoints.Value; } - var matchingDetail = ruleDetails.FirstOrDefault(d => result.Rank >= d.StartRank && result.Rank <= d.EndRank); + var matchingDetail = pointsRuleSnapshot.Details.FirstOrDefault(d => result.Rank >= d.StartRank && result.Rank <= d.EndRank); if (matchingDetail != null) { pointsToAward += matchingDetail.Points; diff --git a/YD_Event.Application/Event/EventService.cs b/YD_Event.Application/Event/EventService.cs index 7bfc01b..69eb4a6 100644 --- a/YD_Event.Application/Event/EventService.cs +++ b/YD_Event.Application/Event/EventService.cs @@ -31,6 +31,7 @@ public class EventService : BaseService, ITransient private readonly IIdGenerator _idGenerator; private readonly YD_Event.Application.Menu.SysMenuService _sysMenuService; private readonly YD_Event.Application.Auth.AuthManager _authManager; + private readonly EventRuleSnapshotService _ruleSnapshotService; public EventService( ISqlSugarRepository repository, @@ -49,7 +50,8 @@ public class EventService : BaseService, ITransient ISqlSugarRepository merchantRepository, IIdGenerator idGenerator, YD_Event.Application.Menu.SysMenuService sysMenuService, - YD_Event.Application.Auth.AuthManager authManager) : base(repository) + YD_Event.Application.Auth.AuthManager authManager, + EventRuleSnapshotService ruleSnapshotService) : base(repository) { _repository = repository; _unitRepository = unitRepository; @@ -68,6 +70,7 @@ public class EventService : BaseService, ITransient _idGenerator = idGenerator; _sysMenuService = sysMenuService; _authManager = authManager; + _ruleSnapshotService = ruleSnapshotService; } /// @@ -266,6 +269,37 @@ public class EventService : BaseService, ITransient } } + // 7. 获取积分规则和退款规则快照,并赋值到原有的规则DTO中 + var pointsRuleSnapshot = await _ruleSnapshotService.GetPointsRuleSnapshot(id); + var refundRuleSnapshots = await _ruleSnapshotService.GetRefundRuleSnapshot(id); + + if (pointsRuleSnapshot != null) + { + output.PointsRuleDetail = new PointsRuleDto + { + RuleName = pointsRuleSnapshot.RuleName, + ParticipationPoints = pointsRuleSnapshot.ParticipationPoints, + Remark = pointsRuleSnapshot.Remark, + Details = pointsRuleSnapshot.Details.Select(d => new PointsRuleDetailDto + { + StartRank = d.StartRank, + EndRank = d.EndRank, + Points = d.Points + }).ToList() + }; + } + + if (refundRuleSnapshots != null && refundRuleSnapshots.Any()) + { + output.RefundRules = refundRuleSnapshots.Select(r => new RefundRuleDto + { + DayStart = r.DayStart, + DayEnd = r.DayEnd, + DeductPercentage = r.DeductPercentage, + Remark = r.Remark + }).ToList(); + } + return output; } @@ -363,6 +397,9 @@ public class EventService : BaseService, ITransient var now = DateTime.Now; eventInfo.EventStatus = EventStatusUpdateService.CalculateEventStatus(eventInfo, now); + // 保存规则快照 + await _ruleSnapshotService.SaveAllRuleSnapshots(eventInfo); + await _repository.InsertAsync(eventInfo); // 1. 保存参与单位 @@ -526,6 +563,9 @@ public class EventService : BaseService, ITransient var now = DateTime.Now; eventInfo.EventStatus = EventStatusUpdateService.CalculateEventStatus(eventInfo, now); + // 保存规则快照 + await _ruleSnapshotService.SaveAllRuleSnapshots(eventInfo); + await _repository.UpdateAsync(eventInfo); // 1. 更新参与单位 @@ -827,6 +867,9 @@ public class EventService : BaseService, ITransient if (eventInfo == null) throw new InvalidOperationException("赛事不存在"); input.Adapt(eventInfo); eventInfo.UpdatedTime = DateTime.Now; + + // 保存规则快照 + await _ruleSnapshotService.SaveAllRuleSnapshots(eventInfo); } else { @@ -840,6 +883,9 @@ public class EventService : BaseService, ITransient var now = DateTime.Now; eventInfo.EventStatus = EventStatusUpdateService.CalculateEventStatus(eventInfo, now); + // 保存规则快照 + await _ruleSnapshotService.SaveAllRuleSnapshots(eventInfo); + await _repository.InsertAsync(eventInfo); } diff --git a/YD_Event.Application/Event/UnifiedRegistrationService.cs b/YD_Event.Application/Event/UnifiedRegistrationService.cs index 82fd67a..ea9653c 100644 --- a/YD_Event.Application/Event/UnifiedRegistrationService.cs +++ b/YD_Event.Application/Event/UnifiedRegistrationService.cs @@ -2742,6 +2742,13 @@ public class UnifiedRegistrationService : ITransient return roundedRefundAmount; } + public async Task CalculateRefundAmountAsync(decimal totalAmount, DateTime eventStartTime, long eventId) + { + var refundAmount = await _refundConfigService.CalculateRefundAmountByEvent(totalAmount, eventStartTime, eventId); + var roundedRefundAmount = RoundUpToTwoDecimalPlaces(refundAmount); + return roundedRefundAmount; + } + /// /// 统一取消报名预览方法 /// @@ -2780,7 +2787,7 @@ public class UnifiedRegistrationService : ITransient { var itemPersons = persons.Where(p => p.RegistrationItemId == item.Id && !p.DeleteMark).ToList(); var itemOriginalAmount = item.UnitPrice * itemPersons.Count; - var itemRefund = await CalculateRefundAmountAsync(itemOriginalAmount, eventInfo.MatchStartTime); + var itemRefund = await CalculateRefundAmountAsync(itemOriginalAmount, eventInfo.MatchStartTime, eventInfo.Id); originalRefundableAmount += itemRefund; } } @@ -2802,13 +2809,13 @@ public class UnifiedRegistrationService : ITransient // 剩余可退款的人数 var remainingPersonCount = personGroups.Count; var remainingOriginalAmount = eventUnitPrice * remainingPersonCount; - originalRefundableAmount = await CalculateRefundAmountAsync(remainingOriginalAmount, eventInfo.MatchStartTime); + originalRefundableAmount = await CalculateRefundAmountAsync(remainingOriginalAmount, eventInfo.MatchStartTime, eventInfo.Id); } else { // 定价(赛事)模式:使用传统方式计算 var remainingOriginalAmount = registration.TotalAmount - refundedAmount; - originalRefundableAmount = await CalculateRefundAmountAsync(remainingOriginalAmount, eventInfo.MatchStartTime); + originalRefundableAmount = await CalculateRefundAmountAsync(remainingOriginalAmount, eventInfo.MatchStartTime, eventInfo.Id); } // 确保可退款金额不小于0 @@ -3054,7 +3061,7 @@ public class UnifiedRegistrationService : ITransient { var itemPersons = persons.Where(p => p.RegistrationItemId == item.Id && !p.DeleteMark).ToList(); var itemOriginalAmount = item.UnitPrice * itemPersons.Count; - var itemRefund = await CalculateRefundAmountAsync(itemOriginalAmount, eventInfo.MatchStartTime); + var itemRefund = await CalculateRefundAmountAsync(itemOriginalAmount, eventInfo.MatchStartTime, eventInfo.Id); remainingOriginalRefundableAmount += itemRefund; } } @@ -3076,13 +3083,13 @@ public class UnifiedRegistrationService : ITransient // 剩余可退款的人数 var remainingPersonCount = personGroups.Count; var remainingOriginalAmount = eventUnitPrice * remainingPersonCount; - remainingOriginalRefundableAmount = await CalculateRefundAmountAsync(remainingOriginalAmount, eventInfo.MatchStartTime); + remainingOriginalRefundableAmount = await CalculateRefundAmountAsync(remainingOriginalAmount, eventInfo.MatchStartTime, eventInfo.Id); } else { // 定价(赛事)模式:使用传统方式计算 var remainingOriginalAmount = registration.TotalAmount - refundedAmount; - remainingOriginalRefundableAmount = await CalculateRefundAmountAsync(remainingOriginalAmount, eventInfo.MatchStartTime); + remainingOriginalRefundableAmount = await CalculateRefundAmountAsync(remainingOriginalAmount, eventInfo.MatchStartTime, eventInfo.Id); } // 按比例计算剩余可退款金额(基于实际支付金额) @@ -3181,7 +3188,7 @@ public class UnifiedRegistrationService : ITransient remainingOriginalTotal = remainingOriginalAmount; } - var fullRefundAmount = await CalculateRefundAmountAsync(remainingOriginalTotal, eventInfo.MatchStartTime); + var fullRefundAmount = await CalculateRefundAmountAsync(remainingOriginalTotal, eventInfo.MatchStartTime, eventInfo.Id); _logger.LogInformation("全部取消,退款金额={Amount}", fullRefundAmount); return fullRefundAmount; } @@ -3231,7 +3238,7 @@ public class UnifiedRegistrationService : ITransient } // 取消了所有队员,按退款规则退款 - var fixedPriceRefundAmount = await CalculateRefundAmountAsync(remainingOriginalAmount, eventInfo.MatchStartTime); + var fixedPriceRefundAmount = await CalculateRefundAmountAsync(remainingOriginalAmount, eventInfo.MatchStartTime, eventInfo.Id); _logger.LogInformation("定价赛事全部取消,按退款规则退款,金额={Amount}", fixedPriceRefundAmount); return fixedPriceRefundAmount; @@ -3292,7 +3299,7 @@ public class UnifiedRegistrationService : ITransient // 计算本次取消的原始金额 var unitPriceEventCancelAmount = eventUnitPrice * fullyCanceledPersonCount; // 直接对本次取消的原始金额应用退款规则 - var unitPriceEventFinalAmount = await CalculateRefundAmountAsync(unitPriceEventCancelAmount, eventInfo.MatchStartTime); + var unitPriceEventFinalAmount = await CalculateRefundAmountAsync(unitPriceEventCancelAmount, eventInfo.MatchStartTime, eventInfo.Id); _logger.LogInformation("单价(赛事)退款:完全取消所有项目的人数={CancelCount}, 赛事单价={UnitPrice}, 本次取消原始金额={CancelAmount}, 按规则退款金额={RefundAmount}", fullyCanceledPersonCount, eventUnitPrice, unitPriceEventCancelAmount, unitPriceEventFinalAmount); @@ -3333,7 +3340,7 @@ public class UnifiedRegistrationService : ITransient } // 直接对本次取消的原始总金额应用退款规则 - var projectRefundAmount = await CalculateRefundAmountAsync(totalProjectCancelAmount, eventInfo.MatchStartTime); + var projectRefundAmount = await CalculateRefundAmountAsync(totalProjectCancelAmount, eventInfo.MatchStartTime, eventInfo.Id); _logger.LogInformation("单价(项目)退款:本次取消总原始金额={TotalCancelAmount}, 按规则退款金额={RefundAmount}", totalProjectCancelAmount, projectRefundAmount); diff --git a/YD_Event.Application/Event/UserEventRegistrationService.cs b/YD_Event.Application/Event/UserEventRegistrationService.cs index 5d14e24..5aff311 100644 --- a/YD_Event.Application/Event/UserEventRegistrationService.cs +++ b/YD_Event.Application/Event/UserEventRegistrationService.cs @@ -69,6 +69,7 @@ public class UserEventRegistrationService : IDynamicApiController, ITransient private readonly ILogger _logger; private readonly OrderNoGeneratorService _orderNoGeneratorService; private readonly UnifiedRegistrationService _unifiedRegistrationService; + private readonly EventRuleSnapshotService _ruleSnapshotService; public UserEventRegistrationService( ISqlSugarRepository eventRepository, @@ -104,7 +105,8 @@ public class UserEventRegistrationService : IDynamicApiController, ITransient ISqlSugarRepository paymentRepository, ILogger logger, OrderNoGeneratorService orderNoGeneratorService, - UnifiedRegistrationService unifiedRegistrationService) + UnifiedRegistrationService unifiedRegistrationService, + EventRuleSnapshotService ruleSnapshotService) { _eventRepository = eventRepository; _unitRepository = unitRepository; @@ -140,6 +142,7 @@ public class UserEventRegistrationService : IDynamicApiController, ITransient _logger = logger; _orderNoGeneratorService = orderNoGeneratorService; _unifiedRegistrationService = unifiedRegistrationService; + _ruleSnapshotService = ruleSnapshotService; } /// @@ -245,40 +248,30 @@ public class UserEventRegistrationService : IDynamicApiController, ITransient output.DistrictName = district?.DistrictName ?? ""; } - if (eventInfo.PointsRuleId.HasValue) + // 从快照获取积分规则 + var pointsRuleSnapshot = await _ruleSnapshotService.GetPointsRuleSnapshot(id); + if (pointsRuleSnapshot != null) { - var rule = await _pointsRuleRepository.GetByIdAsync(eventInfo.PointsRuleId.Value); - output.PointsRuleName = rule?.RuleName ?? ""; + output.PointsRuleName = pointsRuleSnapshot.RuleName ?? ""; - if (rule != null) + output.PointsRuleDetail = new YD_Event.Application.Client.Dtos.PointsRuleDto { - var details = await _pointsRuleDetailRepository.AsQueryable() - .Where(d => d.PointsRuleId == rule.Id && !d.DeleteMark) - .OrderBy(d => d.StartRank) - .ToListAsync(); - - output.PointsRuleDetail = new YD_Event.Application.Client.Dtos.PointsRuleDto + RuleName = pointsRuleSnapshot.RuleName, + ParticipationPoints = pointsRuleSnapshot.ParticipationPoints, + Remark = pointsRuleSnapshot.Remark, + Details = pointsRuleSnapshot.Details.Select(d => new YD_Event.Application.Client.Dtos.PointsRuleDetailDto { - RuleName = rule.RuleName, - ParticipationPoints = rule.ParticipationPoints, - Remark = rule.Remark, - Details = details.Select(d => new YD_Event.Application.Client.Dtos.PointsRuleDetailDto - { - StartRank = d.StartRank, - EndRank = d.EndRank, - Points = d.Points - }).ToList() - }; - } + StartRank = d.StartRank, + EndRank = d.EndRank, + Points = d.Points + }).ToList() + }; } - // 获取退款规则 - var refundConfigs = await _refundConfigRepository.AsQueryable() - .Where(c => c.Status == AvailabilityStatus.Enable && !c.DeleteMark) - .OrderBy(c => c.DayStart) - .ToListAsync(); + // 从快照获取退款规则 + var refundRuleSnapshots = await _ruleSnapshotService.GetRefundRuleSnapshot(id); - output.RefundRules = refundConfigs.Select(c => new YD_Event.Application.Event.Dtos.RefundRuleDto + output.RefundRules = refundRuleSnapshots.Select(c => new YD_Event.Application.Event.Dtos.RefundRuleDto { DayStart = c.DayStart, DayEnd = c.DayEnd, diff --git a/YD_Event.Application/Order/OrderService.cs b/YD_Event.Application/Order/OrderService.cs index d06900a..c69f634 100644 --- a/YD_Event.Application/Order/OrderService.cs +++ b/YD_Event.Application/Order/OrderService.cs @@ -527,7 +527,7 @@ public class OrderService : BaseService, ITransien _logger.LogInformation("调用统一退款金额计算:订单ID={OrderId}, 总金额={TotalAmount}, 赛事开始时间={EventStartTime}", orderId, order.ActualAmount, eventInfo.MatchStartTime); - return await _unifiedRegistrationService.CalculateRefundAmountAsync(order.ActualAmount, eventInfo.MatchStartTime); + return await _unifiedRegistrationService.CalculateRefundAmountAsync(order.ActualAmount, eventInfo.MatchStartTime, eventInfo.Id); } /// diff --git a/YD_Event.Application/Refund/RefundConfigService.cs b/YD_Event.Application/Refund/RefundConfigService.cs index bcad5a5..a321adf 100644 --- a/YD_Event.Application/Refund/RefundConfigService.cs +++ b/YD_Event.Application/Refund/RefundConfigService.cs @@ -1,6 +1,8 @@ using YD_Event.Application.Refund.Dtos; using YD_Event.Core.Entities; +using YD_Event.Application.Event; + namespace YD_Event.Application.Refund; /// @@ -11,15 +13,18 @@ public class RefundConfigService : BaseService, ITransient private readonly ISqlSugarRepository _repository; private readonly IIdGenerator _idGenerator; private readonly IEasyCachingProvider _easyCachingProvider; + private readonly EventRuleSnapshotService _ruleSnapshotService; private const string CacheKey = "RefundConfig:List"; public RefundConfigService(ISqlSugarRepository repository, IIdGenerator idGenerator, - IEasyCachingProvider easyCachingProvider) : base(repository) + IEasyCachingProvider easyCachingProvider, + EventRuleSnapshotService ruleSnapshotService) : base(repository) { _repository = repository; _idGenerator = idGenerator; _easyCachingProvider = easyCachingProvider; + _ruleSnapshotService = ruleSnapshotService; } /// @@ -172,4 +177,37 @@ public class RefundConfigService : BaseService, ITransient return totalAmount; } + + /// + /// 根据赛事快照计算退款金额 + /// + /// 总金额 + /// 赛事开始时间 + /// 赛事ID + /// 应退金额 + [NonAction] + public async Task CalculateRefundAmountByEvent(decimal totalAmount, DateTime eventStartTime, long eventId) + { + // 从快照获取退款规则 + var snapshots = await _ruleSnapshotService.GetRefundRuleSnapshot(eventId); + + if (!snapshots.Any()) return totalAmount; + + var daysRemaining = (eventStartTime - DateTime.Now).TotalDays; + if (daysRemaining < 0) daysRemaining = 0; + + // 查找匹配的规则 + var matchConfig = snapshots.FirstOrDefault(c => + daysRemaining >= c.DayStart && + (c.DayEnd == int.MaxValue ? true : daysRemaining < c.DayEnd) + ); + + if (matchConfig != null) + { + var deductAmount = totalAmount * (matchConfig.DeductPercentage / 100m); + return totalAmount - deductAmount; + } + + return totalAmount; + } } diff --git a/YD_Event.Application/YD_Event.Application.xml b/YD_Event.Application/YD_Event.Application.xml index b69bf59..27217bb 100644 --- a/YD_Event.Application/YD_Event.Application.xml +++ b/YD_Event.Application/YD_Event.Application.xml @@ -2009,6 +2009,46 @@ 名次积分规则明细 + + + 退款规则DTO + + + + + 距离比赛开始天数(起) + + + + + 距离比赛开始天数(止) + + + + + 扣除比例(%) + + + + + 退款备注 + + + + + 积分规则明细快照DTO + + + + + 积分规则快照DTO + + + + + 退款规则快照DTO + + 赛事详情DTO @@ -2084,6 +2124,11 @@ 积分规则详情 + + + 退款规则详情列表 + + 个人项目限报数量 @@ -3733,7 +3778,7 @@ 报名限制检查输入参数 报名限制检查结果 - + 计算退款金额 @@ -4915,6 +4960,61 @@ 二维码URL + + + 积分规则详情 + + + + + 退款规则详情列表 + + + + + 积分规则明细DTO + + + + + 起始名次 + + + + + 截止名次 + + + + + 发放积分个数 + + + + + 积分规则DTO + + + + + 积分规则名称 + + + + + 参与积分(发放积分个数) + + + + + 备注 + + + + + 名次积分规则明细 + + 赛事记录分页查询输入 @@ -6392,6 +6492,116 @@ 审核备注 + + + 报名统计输出 + + + + + 报名总人数 + + + + + 个人项目人数 + + + + + 团队项目队伍数 + + + + + 团队项目人数 + + + + + 待审核数量 + + + + + 积分规则详情快照 DTO + + + + + 起始名次 + + + + + 截止名次 + + + + + 发放积分个数 + + + + + 积分规则快照 DTO + + + + + 积分规则 ID(快照时的 ID) + + + + + 积分规则名称 + + + + + 参与积分(发放积分个数) + + + + + 备注 + + + + + 关联的名次积分规则 + + + + + 退款规则快照 DTO + + + + + 退款配置 ID(快照时的 ID) + + + + + 距离比赛开始天数(起) + + + + + 距离比赛开始天数(止) + + + + + 扣除比例 (0-100) + + + + + 备注 + + 成绩查询输入 @@ -7258,6 +7468,11 @@ 素材备注 + + + 素材上传开始时间 + + 素材上传截止时间 @@ -8563,6 +8778,60 @@ 批量支付审核 + + + 获取报名统计数据 + + + + + 赛事规则快照服务 - 管理积分规则和退款规则的快照 + + + + + 保存积分规则快照到赛事 + + 赛事 ID + 积分规则 ID + + + + + 获取赛事积分规则快照 + + 赛事 ID + 积分规则快照,如果不存在则返回 null + + + + 保存退款规则快照到赛事 + + 赛事 ID + + + + + 获取赛事退款规则快照 + + 赛事 ID + 退款规则快照列表,如果不存在则返回空列表 + + + + 同时保存积分规则和退款规则快照 + + 赛事 ID + 积分规则 ID + + + + + 根据赛事实体保存快照(用于创建/编辑时) + + 赛事实体 + + 成绩与证书管理 @@ -9981,9 +10250,22 @@ + + + 检查当前登录用户是否是管理员 + + + + + + 过滤菜单,移除配置中指定的菜单ID及其子菜单 + + 菜单列表 + 需要隐藏的菜单ID列表 + - 菜单按钮树 + 菜单按钮树 - 仅在角色管理页面使用此接口时应用隐藏规则 @@ -11990,6 +12272,15 @@ 赛事开始时间 应退金额 + + + 根据赛事快照计算退款金额 + + 总金额 + 赛事开始时间 + 赛事ID + 应退金额 + 区域分页查询输入 diff --git a/YD_Event.Core/Entities/EventInfo.cs b/YD_Event.Core/Entities/EventInfo.cs index 4455e2c..3d8ed6e 100644 --- a/YD_Event.Core/Entities/EventInfo.cs +++ b/YD_Event.Core/Entities/EventInfo.cs @@ -60,11 +60,23 @@ public class EventInfo : Entity, ISoftDelete, ICreatedUserId, ICreatedTime public long? PointsRuleId { get; set; } /// - /// 积分发放规则(导航属性) + /// 积分规则(导航属性) /// [SugarColumn(IsIgnore = true)] public PointsRule? PointsRule { get; set; } + /// + /// 积分规则快照(JSON格式,保存创建/编辑赛事时的积分规则) + /// + [SugarColumn(ColumnDataType = "TEXT", IsNullable = true)] + public string? PointsRuleSnapshot { get; set; } + + /// + /// 退款规则快照(JSON格式,保存创建/编辑赛事时的退款规则) + /// + [SugarColumn(ColumnDataType = "TEXT", IsNullable = true)] + public string? RefundRuleSnapshot { get; set; } + /// /// 报名开始时间 /// diff --git a/YD_Event.Core/YD_Event.Core.xml b/YD_Event.Core/YD_Event.Core.xml index 0de2e5b..a2ec523 100644 --- a/YD_Event.Core/YD_Event.Core.xml +++ b/YD_Event.Core/YD_Event.Core.xml @@ -1334,7 +1334,17 @@ - 积分发放规则(导航属性) + 积分规则(导航属性) + + + + + 积分规则快照(JSON格式,保存创建/编辑赛事时的积分规则) + + + + + 退款规则快照(JSON格式,保存创建/编辑赛事时的退款规则) @@ -5861,6 +5871,11 @@ 单位用户角色ID + + + 需要对非管理员用户隐藏的菜单ID列表 + + 短信配置选项