赛事保存积分和退款规则快照

This commit is contained in:
马梓彭 2026-05-15 13:46:58 +08:00
parent 50f0d86b91
commit 8c92e13e48
17 changed files with 1130 additions and 146 deletions

View File

@ -240,6 +240,66 @@ public class PointsRuleDto
public List<PointsRuleDetailDto>? Details { get; set; }
}
/// <summary>
/// 退款规则DTO
/// </summary>
public class RefundRuleDto
{
/// <summary>
/// 距离比赛开始天数(起)
/// </summary>
public int DayStart { get; set; }
/// <summary>
/// 距离比赛开始天数(止)
/// </summary>
public int DayEnd { get; set; }
/// <summary>
/// 扣除比例(%)
/// </summary>
public decimal DeductPercentage { get; set; }
/// <summary>
/// 退款备注
/// </summary>
public string? Remark { get; set; }
}
/// <summary>
/// 积分规则明细快照DTO
/// </summary>
public class PointsRuleDetailSnapshotDto
{
public int StartRank { get; set; }
public int EndRank { get; set; }
public int Points { get; set; }
}
/// <summary>
/// 积分规则快照DTO
/// </summary>
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<PointsRuleDetailSnapshotDto> Details { get; set; } = new List<PointsRuleDetailSnapshotDto>();
}
/// <summary>
/// 退款规则快照DTO
/// </summary>
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; }
}
/// <summary>
/// 赛事详情DTO
/// </summary>
@ -314,6 +374,11 @@ public class EventDetailDto : EventListDto
/// 积分规则详情
/// </summary>
public PointsRuleDto? PointsRuleDetail { get; set; }
/// <summary>
/// 退款规则详情列表
/// </summary>
public List<RefundRuleDto>? RefundRules { get; set; }
/// <summary>
/// 个人项目限报数量

View File

@ -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<EventInfo> 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<YD_Event.Application.Client.Dtos.RefundRuleDto>? 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<string>();
foreach (var config in refundRuleSnapshots)
{
var ruleParts = new List<string>();
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<string>();
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
/// <summary>
/// 计算退款金额
/// </summary>
private async Task<decimal> CalculateRefundAmount(decimal totalAmount, DateTime eventStartTime)
private async Task<decimal> 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);
}
/// <summary>

View File

@ -543,4 +543,61 @@ public class EventDetailOutput : AddEventInput
/// 二维码URL
/// </summary>
public string? QrCodeUrl { get; set; }
/// <summary>
/// 积分规则详情
/// </summary>
public PointsRuleDto? PointsRuleDetail { get; set; }
/// <summary>
/// 退款规则详情列表
/// </summary>
public List<RefundRuleDto>? RefundRules { get; set; }
}
/// <summary>
/// 积分规则明细DTO
/// </summary>
public class PointsRuleDetailDto
{
/// <summary>
/// 起始名次
/// </summary>
public int StartRank { get; set; }
/// <summary>
/// 截止名次
/// </summary>
public int EndRank { get; set; }
/// <summary>
/// 发放积分个数
/// </summary>
public int Points { get; set; }
}
/// <summary>
/// 积分规则DTO
/// </summary>
public class PointsRuleDto
{
/// <summary>
/// 积分规则名称
/// </summary>
public string RuleName { get; set; }
/// <summary>
/// 参与积分(发放积分个数)
/// </summary>
public int? ParticipationPoints { get; set; }
/// <summary>
/// 备注
/// </summary>
public string? Remark { get; set; }
/// <summary>
/// 名次积分规则明细
/// </summary>
public List<PointsRuleDetailDto>? Details { get; set; }
}

View File

@ -0,0 +1,13 @@
namespace YD_Event.Application.Event.Dtos;
public class Mapper : IRegister
{
public void Register(TypeAdapterConfig config)
{
// 配置EventInfo到EventDetailOutput的映射
config.ForType<Core.Entities.EventInfo, EventDetailOutput>();
// 配置EventInfo到UserEventDetailOutput的映射
config.ForType<Core.Entities.EventInfo, UserEventDetailOutput>();
}
}

View File

@ -295,3 +295,34 @@ public class BatchPaymentAuditInput
/// </summary>
public string? ReviewRemark { get; set; }
}
/// <summary>
/// 报名统计输出
/// </summary>
public class RegistrationStatsOutput
{
/// <summary>
/// 报名总人数
/// </summary>
public int TotalParticipants { get; set; }
/// <summary>
/// 个人项目人数
/// </summary>
public int IndividualParticipants { get; set; }
/// <summary>
/// 团队项目队伍数
/// </summary>
public int TeamCount { get; set; }
/// <summary>
/// 团队项目人数
/// </summary>
public int TeamParticipants { get; set; }
/// <summary>
/// 待审核数量
/// </summary>
public int PendingReview { get; set; }
}

View File

@ -0,0 +1,88 @@
using System;
using System.Collections.Generic;
using YD_Event.Core.Enum;
namespace YD_Event.Application.Event.Dtos;
/// <summary>
/// 积分规则详情快照 DTO
/// </summary>
public class PointsRuleDetailSnapshotDto
{
/// <summary>
/// 起始名次
/// </summary>
public int StartRank { get; set; }
/// <summary>
/// 截止名次
/// </summary>
public int EndRank { get; set; }
/// <summary>
/// 发放积分个数
/// </summary>
public int Points { get; set; }
}
/// <summary>
/// 积分规则快照 DTO
/// </summary>
public class PointsRuleSnapshotDto
{
/// <summary>
/// 积分规则 ID(快照时的 ID)
/// </summary>
public long PointsRuleId { get; set; }
/// <summary>
/// 积分规则名称
/// </summary>
public string RuleName { get; set; }
/// <summary>
/// 参与积分(发放积分个数)
/// </summary>
public int? ParticipationPoints { get; set; }
/// <summary>
/// 备注
/// </summary>
public string? Remark { get; set; }
/// <summary>
/// 关联的名次积分规则
/// </summary>
public List<PointsRuleDetailSnapshotDto> Details { get; set; } = new List<PointsRuleDetailSnapshotDto>();
}
/// <summary>
/// 退款规则快照 DTO
/// </summary>
public class RefundRuleSnapshotDto
{
/// <summary>
/// 退款配置 ID(快照时的 ID)
/// </summary>
public long RefundConfigId { get; set; }
/// <summary>
/// 距离比赛开始天数(起)
/// </summary>
public int DayStart { get; set; }
/// <summary>
/// 距离比赛开始天数(止)
/// </summary>
public int DayEnd { get; set; }
/// <summary>
/// 扣除比例 (0-100)
/// </summary>
public decimal DeductPercentage { get; set; }
/// <summary>
/// 备注
/// </summary>
public string? Remark { get; set; }
}

View File

@ -668,4 +668,106 @@ public class EventRegistrationService : BaseService<EventRegistration>, ITransie
PaymentTime = input.PaymentStatus == 1 ? now : x.PaymentTime
}, x => input.Ids.Contains(x.Id));
}
/// <summary>
/// 获取报名统计数据
/// </summary>
[DisplayName("获取报名统计数据")]
[HttpGet("stats/{eventId}")]
public async Task<RegistrationStatsOutput> 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<EventRegistration>((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<long>();
var teamSet = new HashSet<(long EventProjectId, string GroupName, string TeamName, long RegistrationItemId)>();
var teamPersonSet = new HashSet<long>();
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;
}
}

View File

@ -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;
/// <summary>
/// 赛事规则快照服务 - 管理积分规则和退款规则的快照
/// </summary>
public class EventRuleSnapshotService : ITransient
{
private readonly ISqlSugarRepository<EventInfo> _eventInfoRepository;
private readonly ISqlSugarRepository<PointsRule> _pointsRuleRepository;
private readonly ISqlSugarRepository<PointsRuleDetail> _pointsRuleDetailRepository;
private readonly ISqlSugarRepository<RefundConfig> _refundConfigRepository;
// JSON 序列化选项,支持中文正常显示
private static readonly System.Text.Json.JsonSerializerOptions _jsonOptions = new System.Text.Json.JsonSerializerOptions
{
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
WriteIndented = false
};
public EventRuleSnapshotService(
ISqlSugarRepository<EventInfo> eventInfoRepository,
ISqlSugarRepository<PointsRule> pointsRuleRepository,
ISqlSugarRepository<PointsRuleDetail> pointsRuleDetailRepository,
ISqlSugarRepository<RefundConfig> refundConfigRepository)
{
_eventInfoRepository = eventInfoRepository;
_pointsRuleRepository = pointsRuleRepository;
_pointsRuleDetailRepository = pointsRuleDetailRepository;
_refundConfigRepository = refundConfigRepository;
}
#region 积分规则快照
/// <summary>
/// 保存积分规则快照到赛事
/// </summary>
/// <param name="eventId">赛事 ID</param>
/// <param name="pointsRuleId">积分规则 ID</param>
/// <returns></returns>
[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);
}
/// <summary>
/// 获取赛事积分规则快照
/// </summary>
/// <param name="eventId">赛事 ID</param>
/// <returns>积分规则快照,如果不存在则返回 null</returns>
[DisplayName("获取积分规则快照")]
public async Task<PointsRuleSnapshotDto?> 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<PointsRuleSnapshotDto>(eventInfo.PointsRuleSnapshot, _jsonOptions);
}
catch (Exception)
{
return null;
}
}
#endregion
#region 退款规则快照
/// <summary>
/// 保存退款规则快照到赛事
/// </summary>
/// <param name="eventId">赛事 ID</param>
/// <returns></returns>
[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);
}
/// <summary>
/// 获取赛事退款规则快照
/// </summary>
/// <param name="eventId">赛事 ID</param>
/// <returns>退款规则快照列表,如果不存在则返回空列表</returns>
[DisplayName("获取退款规则快照")]
public async Task<List<RefundRuleSnapshotDto>> GetRefundRuleSnapshot(long eventId)
{
var eventInfo = await _eventInfoRepository.GetByIdAsync(eventId);
if (eventInfo == null)
throw new InvalidOperationException("赛事不存在");
if (string.IsNullOrWhiteSpace(eventInfo.RefundRuleSnapshot))
return new List<RefundRuleSnapshotDto>();
try
{
var snapshots = System.Text.Json.JsonSerializer.Deserialize<List<RefundRuleSnapshotDto>>(eventInfo.RefundRuleSnapshot, _jsonOptions);
return snapshots ?? new List<RefundRuleSnapshotDto>();
}
catch (Exception)
{
return new List<RefundRuleSnapshotDto>();
}
}
#endregion
#region 快捷方法
/// <summary>
/// 同时保存积分规则和退款规则快照
/// </summary>
/// <param name="eventId">赛事 ID</param>
/// <param name="pointsRuleId">积分规则 ID</param>
/// <returns></returns>
[DisplayName("保存所有规则快照")]
public async Task SaveAllRuleSnapshots(long eventId, long? pointsRuleId)
{
await SavePointsRuleSnapshot(eventId, pointsRuleId);
await SaveRefundRuleSnapshot(eventId);
}
/// <summary>
/// 根据赛事实体保存快照(用于创建/编辑时)
/// </summary>
/// <param name="eventInfo">赛事实体</param>
/// <returns></returns>
[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
}

View File

@ -62,6 +62,7 @@ public class EventScoreService : BaseService<EventResult>, ITransient
private readonly CertificateGenerateService _certificateGenerateService;
private readonly IIdGenerator _idGenerator;
private readonly ILogger<EventScoreService> _logger;
private readonly EventRuleSnapshotService _ruleSnapshotService;
public EventScoreService(
ISqlSugarRepository<EventRegistrationPerson> personRepository,
@ -82,7 +83,8 @@ public class EventScoreService : BaseService<EventResult>, ITransient
ISqlSugarRepository<MerchantCertification> merchantCertificationRepository,
CertificateGenerateService certificateGenerateService,
IIdGenerator idGenerator,
ILogger<EventScoreService> logger) : base(resultRepository)
ILogger<EventScoreService> logger,
EventRuleSnapshotService ruleSnapshotService) : base(resultRepository)
{
_personRepository = personRepository;
_resultRepository = resultRepository;
@ -103,6 +105,7 @@ public class EventScoreService : BaseService<EventResult>, ITransient
_certificateGenerateService = certificateGenerateService;
_idGenerator = idGenerator;
_logger = logger;
_ruleSnapshotService = ruleSnapshotService;
}
/// <summary>
@ -1647,27 +1650,13 @@ public class EventScoreService : BaseService<EventResult>, 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<EventResult>, 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<EventResult>, 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;

View File

@ -31,6 +31,7 @@ public class EventService : BaseService<EventInfo>, 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<EventInfo> repository,
@ -49,7 +50,8 @@ public class EventService : BaseService<EventInfo>, ITransient
ISqlSugarRepository<YD_Event.Core.Entities.Merchant> 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<EventInfo>, ITransient
_idGenerator = idGenerator;
_sysMenuService = sysMenuService;
_authManager = authManager;
_ruleSnapshotService = ruleSnapshotService;
}
/// <summary>
@ -266,6 +269,37 @@ public class EventService : BaseService<EventInfo>, 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<EventInfo>, 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<EventInfo>, 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<EventInfo>, 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<EventInfo>, ITransient
var now = DateTime.Now;
eventInfo.EventStatus = EventStatusUpdateService.CalculateEventStatus(eventInfo, now);
// 保存规则快照
await _ruleSnapshotService.SaveAllRuleSnapshots(eventInfo);
await _repository.InsertAsync(eventInfo);
}

View File

@ -2742,6 +2742,13 @@ public class UnifiedRegistrationService : ITransient
return roundedRefundAmount;
}
public async Task<decimal> CalculateRefundAmountAsync(decimal totalAmount, DateTime eventStartTime, long eventId)
{
var refundAmount = await _refundConfigService.CalculateRefundAmountByEvent(totalAmount, eventStartTime, eventId);
var roundedRefundAmount = RoundUpToTwoDecimalPlaces(refundAmount);
return roundedRefundAmount;
}
/// <summary>
/// 统一取消报名预览方法
/// </summary>
@ -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);

View File

@ -69,6 +69,7 @@ public class UserEventRegistrationService : IDynamicApiController, ITransient
private readonly ILogger<UserEventRegistrationService> _logger;
private readonly OrderNoGeneratorService _orderNoGeneratorService;
private readonly UnifiedRegistrationService _unifiedRegistrationService;
private readonly EventRuleSnapshotService _ruleSnapshotService;
public UserEventRegistrationService(
ISqlSugarRepository<EventInfo> eventRepository,
@ -104,7 +105,8 @@ public class UserEventRegistrationService : IDynamicApiController, ITransient
ISqlSugarRepository<EventRegistrationPayment> paymentRepository,
ILogger<UserEventRegistrationService> 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;
}
/// <summary>
@ -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,

View File

@ -527,7 +527,7 @@ public class OrderService : BaseService<YD_Event.Core.Entities.Order>, 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);
}
/// <summary>

View File

@ -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;
/// <summary>
@ -11,15 +13,18 @@ public class RefundConfigService : BaseService<RefundConfig>, ITransient
private readonly ISqlSugarRepository<RefundConfig> _repository;
private readonly IIdGenerator _idGenerator;
private readonly IEasyCachingProvider _easyCachingProvider;
private readonly EventRuleSnapshotService _ruleSnapshotService;
private const string CacheKey = "RefundConfig:List";
public RefundConfigService(ISqlSugarRepository<RefundConfig> repository,
IIdGenerator idGenerator,
IEasyCachingProvider easyCachingProvider) : base(repository)
IEasyCachingProvider easyCachingProvider,
EventRuleSnapshotService ruleSnapshotService) : base(repository)
{
_repository = repository;
_idGenerator = idGenerator;
_easyCachingProvider = easyCachingProvider;
_ruleSnapshotService = ruleSnapshotService;
}
/// <summary>
@ -172,4 +177,37 @@ public class RefundConfigService : BaseService<RefundConfig>, ITransient
return totalAmount;
}
/// <summary>
/// 根据赛事快照计算退款金额
/// </summary>
/// <param name="totalAmount">总金额</param>
/// <param name="eventStartTime">赛事开始时间</param>
/// <param name="eventId">赛事ID</param>
/// <returns>应退金额</returns>
[NonAction]
public async Task<decimal> 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;
}
}

View File

@ -2009,6 +2009,46 @@
名次积分规则明细
</summary>
</member>
<member name="T:YD_Event.Application.Client.Dtos.RefundRuleDto">
<summary>
退款规则DTO
</summary>
</member>
<member name="P:YD_Event.Application.Client.Dtos.RefundRuleDto.DayStart">
<summary>
距离比赛开始天数(起)
</summary>
</member>
<member name="P:YD_Event.Application.Client.Dtos.RefundRuleDto.DayEnd">
<summary>
距离比赛开始天数(止)
</summary>
</member>
<member name="P:YD_Event.Application.Client.Dtos.RefundRuleDto.DeductPercentage">
<summary>
扣除比例(%)
</summary>
</member>
<member name="P:YD_Event.Application.Client.Dtos.RefundRuleDto.Remark">
<summary>
退款备注
</summary>
</member>
<member name="T:YD_Event.Application.Client.Dtos.PointsRuleDetailSnapshotDto">
<summary>
积分规则明细快照DTO
</summary>
</member>
<member name="T:YD_Event.Application.Client.Dtos.PointsRuleSnapshotDto">
<summary>
积分规则快照DTO
</summary>
</member>
<member name="T:YD_Event.Application.Client.Dtos.RefundRuleSnapshotDto">
<summary>
退款规则快照DTO
</summary>
</member>
<member name="T:YD_Event.Application.Client.Dtos.EventDetailDto">
<summary>
赛事详情DTO
@ -2084,6 +2124,11 @@
积分规则详情
</summary>
</member>
<member name="P:YD_Event.Application.Client.Dtos.EventDetailDto.RefundRules">
<summary>
退款规则详情列表
</summary>
</member>
<member name="P:YD_Event.Application.Client.Dtos.EventDetailDto.IndividualProjectLimit">
<summary>
个人项目限报数量
@ -3733,7 +3778,7 @@
<param name="input">报名限制检查输入参数</param>
<returns>报名限制检查结果</returns>
</member>
<member name="M:YD_Event.Application.Client.EventClientController.CalculateRefundAmount(System.Decimal,System.DateTime)">
<member name="M:YD_Event.Application.Client.EventClientController.CalculateRefundAmount(System.Decimal,System.DateTime,System.Int64)">
<summary>
计算退款金额
</summary>
@ -4915,6 +4960,61 @@
二维码URL
</summary>
</member>
<member name="P:YD_Event.Application.Event.Dtos.EventDetailOutput.PointsRuleDetail">
<summary>
积分规则详情
</summary>
</member>
<member name="P:YD_Event.Application.Event.Dtos.EventDetailOutput.RefundRules">
<summary>
退款规则详情列表
</summary>
</member>
<member name="T:YD_Event.Application.Event.Dtos.PointsRuleDetailDto">
<summary>
积分规则明细DTO
</summary>
</member>
<member name="P:YD_Event.Application.Event.Dtos.PointsRuleDetailDto.StartRank">
<summary>
起始名次
</summary>
</member>
<member name="P:YD_Event.Application.Event.Dtos.PointsRuleDetailDto.EndRank">
<summary>
截止名次
</summary>
</member>
<member name="P:YD_Event.Application.Event.Dtos.PointsRuleDetailDto.Points">
<summary>
发放积分个数
</summary>
</member>
<member name="T:YD_Event.Application.Event.Dtos.PointsRuleDto">
<summary>
积分规则DTO
</summary>
</member>
<member name="P:YD_Event.Application.Event.Dtos.PointsRuleDto.RuleName">
<summary>
积分规则名称
</summary>
</member>
<member name="P:YD_Event.Application.Event.Dtos.PointsRuleDto.ParticipationPoints">
<summary>
参与积分(发放积分个数)
</summary>
</member>
<member name="P:YD_Event.Application.Event.Dtos.PointsRuleDto.Remark">
<summary>
备注
</summary>
</member>
<member name="P:YD_Event.Application.Event.Dtos.PointsRuleDto.Details">
<summary>
名次积分规则明细
</summary>
</member>
<member name="T:YD_Event.Application.Event.Dtos.EventRecordPageInput">
<summary>
赛事记录分页查询输入
@ -6392,6 +6492,116 @@
审核备注
</summary>
</member>
<member name="T:YD_Event.Application.Event.Dtos.RegistrationStatsOutput">
<summary>
报名统计输出
</summary>
</member>
<member name="P:YD_Event.Application.Event.Dtos.RegistrationStatsOutput.TotalParticipants">
<summary>
报名总人数
</summary>
</member>
<member name="P:YD_Event.Application.Event.Dtos.RegistrationStatsOutput.IndividualParticipants">
<summary>
个人项目人数
</summary>
</member>
<member name="P:YD_Event.Application.Event.Dtos.RegistrationStatsOutput.TeamCount">
<summary>
团队项目队伍数
</summary>
</member>
<member name="P:YD_Event.Application.Event.Dtos.RegistrationStatsOutput.TeamParticipants">
<summary>
团队项目人数
</summary>
</member>
<member name="P:YD_Event.Application.Event.Dtos.RegistrationStatsOutput.PendingReview">
<summary>
待审核数量
</summary>
</member>
<member name="T:YD_Event.Application.Event.Dtos.PointsRuleDetailSnapshotDto">
<summary>
积分规则详情快照 DTO
</summary>
</member>
<member name="P:YD_Event.Application.Event.Dtos.PointsRuleDetailSnapshotDto.StartRank">
<summary>
起始名次
</summary>
</member>
<member name="P:YD_Event.Application.Event.Dtos.PointsRuleDetailSnapshotDto.EndRank">
<summary>
截止名次
</summary>
</member>
<member name="P:YD_Event.Application.Event.Dtos.PointsRuleDetailSnapshotDto.Points">
<summary>
发放积分个数
</summary>
</member>
<member name="T:YD_Event.Application.Event.Dtos.PointsRuleSnapshotDto">
<summary>
积分规则快照 DTO
</summary>
</member>
<member name="P:YD_Event.Application.Event.Dtos.PointsRuleSnapshotDto.PointsRuleId">
<summary>
积分规则 ID(快照时的 ID)
</summary>
</member>
<member name="P:YD_Event.Application.Event.Dtos.PointsRuleSnapshotDto.RuleName">
<summary>
积分规则名称
</summary>
</member>
<member name="P:YD_Event.Application.Event.Dtos.PointsRuleSnapshotDto.ParticipationPoints">
<summary>
参与积分(发放积分个数)
</summary>
</member>
<member name="P:YD_Event.Application.Event.Dtos.PointsRuleSnapshotDto.Remark">
<summary>
备注
</summary>
</member>
<member name="P:YD_Event.Application.Event.Dtos.PointsRuleSnapshotDto.Details">
<summary>
关联的名次积分规则
</summary>
</member>
<member name="T:YD_Event.Application.Event.Dtos.RefundRuleSnapshotDto">
<summary>
退款规则快照 DTO
</summary>
</member>
<member name="P:YD_Event.Application.Event.Dtos.RefundRuleSnapshotDto.RefundConfigId">
<summary>
退款配置 ID(快照时的 ID)
</summary>
</member>
<member name="P:YD_Event.Application.Event.Dtos.RefundRuleSnapshotDto.DayStart">
<summary>
距离比赛开始天数(起)
</summary>
</member>
<member name="P:YD_Event.Application.Event.Dtos.RefundRuleSnapshotDto.DayEnd">
<summary>
距离比赛开始天数(止)
</summary>
</member>
<member name="P:YD_Event.Application.Event.Dtos.RefundRuleSnapshotDto.DeductPercentage">
<summary>
扣除比例 (0-100)
</summary>
</member>
<member name="P:YD_Event.Application.Event.Dtos.RefundRuleSnapshotDto.Remark">
<summary>
备注
</summary>
</member>
<member name="T:YD_Event.Application.Event.Dtos.ScoreQueryInput">
<summary>
成绩查询输入
@ -7258,6 +7468,11 @@
素材备注
</summary>
</member>
<member name="P:YD_Event.Application.Event.Dtos.UserEventDetailOutput.MaterialUploadStartTime">
<summary>
素材上传开始时间
</summary>
</member>
<member name="P:YD_Event.Application.Event.Dtos.UserEventDetailOutput.MaterialUploadEndTime">
<summary>
素材上传截止时间
@ -8563,6 +8778,60 @@
批量支付审核
</summary>
</member>
<member name="M:YD_Event.Application.Event.EventRegistrationService.GetStats(System.Int64)">
<summary>
获取报名统计数据
</summary>
</member>
<member name="T:YD_Event.Application.Event.EventRuleSnapshotService">
<summary>
赛事规则快照服务 - 管理积分规则和退款规则的快照
</summary>
</member>
<member name="M:YD_Event.Application.Event.EventRuleSnapshotService.SavePointsRuleSnapshot(System.Int64,System.Nullable{System.Int64})">
<summary>
保存积分规则快照到赛事
</summary>
<param name="eventId">赛事 ID</param>
<param name="pointsRuleId">积分规则 ID</param>
<returns></returns>
</member>
<member name="M:YD_Event.Application.Event.EventRuleSnapshotService.GetPointsRuleSnapshot(System.Int64)">
<summary>
获取赛事积分规则快照
</summary>
<param name="eventId">赛事 ID</param>
<returns>积分规则快照,如果不存在则返回 null</returns>
</member>
<member name="M:YD_Event.Application.Event.EventRuleSnapshotService.SaveRefundRuleSnapshot(System.Int64)">
<summary>
保存退款规则快照到赛事
</summary>
<param name="eventId">赛事 ID</param>
<returns></returns>
</member>
<member name="M:YD_Event.Application.Event.EventRuleSnapshotService.GetRefundRuleSnapshot(System.Int64)">
<summary>
获取赛事退款规则快照
</summary>
<param name="eventId">赛事 ID</param>
<returns>退款规则快照列表,如果不存在则返回空列表</returns>
</member>
<member name="M:YD_Event.Application.Event.EventRuleSnapshotService.SaveAllRuleSnapshots(System.Int64,System.Nullable{System.Int64})">
<summary>
同时保存积分规则和退款规则快照
</summary>
<param name="eventId">赛事 ID</param>
<param name="pointsRuleId">积分规则 ID</param>
<returns></returns>
</member>
<member name="M:YD_Event.Application.Event.EventRuleSnapshotService.SaveAllRuleSnapshots(YD_Event.Core.Entities.EventInfo)">
<summary>
根据赛事实体保存快照(用于创建/编辑时)
</summary>
<param name="eventInfo">赛事实体</param>
<returns></returns>
</member>
<member name="T:YD_Event.Application.Event.EventScoreService">
<summary>
成绩与证书管理
@ -9981,9 +10250,22 @@
</summary>
<returns></returns>
</member>
<member name="M:YD_Event.Application.Menu.SysMenuService.IsCurrentUserAdminAsync">
<summary>
检查当前登录用户是否是管理员
</summary>
<returns></returns>
</member>
<member name="M:YD_Event.Application.Menu.SysMenuService.FilterMenus(System.Collections.Generic.List{YD_Event.Core.Entities.SysMenu},System.Collections.Generic.List{System.Int64})">
<summary>
过滤菜单,移除配置中指定的菜单ID及其子菜单
</summary>
<param name="menus">菜单列表</param>
<param name="hiddenMenuIds">需要隐藏的菜单ID列表</param>
</member>
<member name="M:YD_Event.Application.Menu.SysMenuService.TreeMenuButton">
<summary>
菜单按钮树
菜单按钮树 - 仅在角色管理页面使用此接口时应用隐藏规则
</summary>
<returns></returns>
</member>
@ -11990,6 +12272,15 @@
<param name="eventStartTime">赛事开始时间</param>
<returns>应退金额</returns>
</member>
<member name="M:YD_Event.Application.Refund.RefundConfigService.CalculateRefundAmountByEvent(System.Decimal,System.DateTime,System.Int64)">
<summary>
根据赛事快照计算退款金额
</summary>
<param name="totalAmount">总金额</param>
<param name="eventStartTime">赛事开始时间</param>
<param name="eventId">赛事ID</param>
<returns>应退金额</returns>
</member>
<member name="T:YD_Event.Application.Region.Dtos.RegionPageInput">
<summary>
区域分页查询输入

View File

@ -60,11 +60,23 @@ public class EventInfo : Entity<long>, ISoftDelete, ICreatedUserId, ICreatedTime
public long? PointsRuleId { get; set; }
/// <summary>
/// 积分发放规则(导航属性)
/// 积分规则(导航属性)
/// </summary>
[SugarColumn(IsIgnore = true)]
public PointsRule? PointsRule { get; set; }
/// <summary>
/// 积分规则快照(JSON格式,保存创建/编辑赛事时的积分规则)
/// </summary>
[SugarColumn(ColumnDataType = "TEXT", IsNullable = true)]
public string? PointsRuleSnapshot { get; set; }
/// <summary>
/// 退款规则快照(JSON格式,保存创建/编辑赛事时的退款规则)
/// </summary>
[SugarColumn(ColumnDataType = "TEXT", IsNullable = true)]
public string? RefundRuleSnapshot { get; set; }
/// <summary>
/// 报名开始时间
/// </summary>

View File

@ -1334,7 +1334,17 @@
</member>
<member name="P:YD_Event.Core.Entities.EventInfo.PointsRule">
<summary>
积分发放规则(导航属性)
积分规则(导航属性)
</summary>
</member>
<member name="P:YD_Event.Core.Entities.EventInfo.PointsRuleSnapshot">
<summary>
积分规则快照(JSON格式,保存创建/编辑赛事时的积分规则)
</summary>
</member>
<member name="P:YD_Event.Core.Entities.EventInfo.RefundRuleSnapshot">
<summary>
退款规则快照(JSON格式,保存创建/编辑赛事时的退款规则)
</summary>
</member>
<member name="P:YD_Event.Core.Entities.EventInfo.RegistrationStartTime">
@ -5861,6 +5871,11 @@
单位用户角色ID
</summary>
</member>
<member name="P:YD_Event.Core.Options.RoleOptions.HiddenMenuIds">
<summary>
需要对非管理员用户隐藏的菜单ID列表
</summary>
</member>
<member name="T:YD_Event.Core.Options.SmsOptions">
<summary>
短信配置选项