diff --git a/YD_Event.Application/Event/Dtos/UserCompetitionDtos.cs b/YD_Event.Application/Event/Dtos/UserCompetitionDtos.cs index e4a746a..6e93843 100644 --- a/YD_Event.Application/Event/Dtos/UserCompetitionDtos.cs +++ b/YD_Event.Application/Event/Dtos/UserCompetitionDtos.cs @@ -905,11 +905,7 @@ public class UploadVoucherInput [Required] public DateTime PayTime { get; set; } - /// - /// 支付渠道 (微信/支付宝/银行转账) - /// - [Required] - public string PayChannel { get; set; } + /// /// 交易流水号 (可选) diff --git a/YD_Event.Application/Event/EventArrangementService.cs b/YD_Event.Application/Event/EventArrangementService.cs index ce86e51..80fffc6 100644 --- a/YD_Event.Application/Event/EventArrangementService.cs +++ b/YD_Event.Application/Event/EventArrangementService.cs @@ -38,7 +38,9 @@ public class EventArrangementService : BaseService + /// 格式化时长显示(符号格式:X'X") + /// private string FormatDurationSymbol(int totalSeconds) { if (totalSeconds <= 0) @@ -88,20 +90,19 @@ public class EventArrangementService : BaseService x.EventId == eventId); var projectIds = eventProjects.Select(x => x.ProjectId).Distinct().ToList(); var projects = await _projectRepository.GetListAsync(x => projectIds.Contains(x.Id)); - + // 按照 SortIndex 对 projects 排序 var sortedProjects = projects.OrderBy(x => x.SortIndex).ToList(); - + var groups = await _groupRepository.GetListAsync(x => projectIds.Contains(x.ProjectId)); // 统计各组别报名人数(统计所有非取消状态的报名,并且只统计运动员 var counts = await _personRepository.AsQueryable() .InnerJoin((p, r) => p.RegistrationId == r.Id) - .Where((p, r) => r.EventId == eventId + .Where((p, r) => r.EventId == eventId && r.PaymentStatus != 4 // 排除已取消的报名 && !p.DeleteMark // 排除已删除的人员 - && p.PersonType == PersonType.Athlete // 只统计运动员 - ) + && p.PersonType == PersonType.Athlete) // 只统计运动员 .GroupBy((p, r) => new { p.ProjectId, p.GroupName }) .Select((p, r) => new { p.ProjectId, p.GroupName, Count = SqlFunc.AggregateCount(p.Id) }) .ToListAsync(); @@ -109,9 +110,9 @@ public class EventArrangementService : BaseService((p, r) => p.RegistrationId == r.Id) - .Where((p, r) => r.EventId == eventId - && r.PaymentStatus != 4 - && !p.DeleteMark + .Where((p, r) => r.EventId == eventId + && r.PaymentStatus != 4 + && !p.DeleteMark && p.PersonType == PersonType.Athlete && r.TeamName != null) .Select((p, r) => new { p.ProjectId, p.GroupName, r.TeamName }) @@ -136,7 +137,7 @@ public class EventArrangementService : BaseService() @@ -146,18 +147,18 @@ public class EventArrangementService : BaseService c.ProjectId == eventProj.Id && c.GroupName == group.GroupName); - dto.Groups.Add(new ArrangementGroupDto - { - GroupName = group.GroupName, - ParticipantCount = countObj?.Count ?? 0 + dto.Groups.Add(new ArrangementGroupDto + { + GroupName = group.GroupName, + ParticipantCount = countObj?.Count ?? 0 }); } - + // 根据项目类型计算总时长 int venueCount = proj.VenueCount > 0 ? proj.VenueCount : 1; int totalParticipants = dto.Groups.Sum(g => g.ParticipantCount); int totalTeams = 0; - + // 如果是团队项目,计算团队总数 if (proj.IsTeamProject()) { @@ -189,16 +190,16 @@ public class EventArrangementService : BaseService 0 ? totalTeams : 1; int rounds = (int)Math.Ceiling((double)teamCount / venueCount); - totalDurationSeconds = rounds * proj.DurationSeconds; + totalDurationSeconds = rounds * proj.GetTotalDurationInSeconds(); } else { // 个人项目:(报名人数 ÷ 场地数) × 项目时长 int participantCount = totalParticipants > 0 ? totalParticipants : 1; int rounds = (int)Math.Ceiling((double)participantCount / venueCount); - totalDurationSeconds = rounds * proj.DurationSeconds; + totalDurationSeconds = rounds * proj.GetTotalDurationInSeconds(); } - + // 设置总时长秒数和分钟数 dto.TotalDurationSeconds = totalDurationSeconds; dto.TotalDurationMinutes = (int)Math.Ceiling((double)totalDurationSeconds / 60); @@ -223,13 +224,14 @@ public class EventArrangementService : BaseService x.Id == setting.ProjectId); if (ep != null) { - await _projectRepository.UpdateAsync(x => new YD_Event.Core.Entities.Project - { - VenueCount = setting.VenueCount, - SortIndex = setting.SortIndex, - DurationSeconds = setting.DurationSeconds, - DurationMinutes = setting.DurationSeconds / 60 - }, x => x.Id == ep.ProjectId); + var project = await _projectRepository.GetByIdAsync(ep.ProjectId); + if (project != null) + { + project.VenueCount = setting.VenueCount; + project.SortIndex = setting.SortIndex; + project.SetTotalDurationInSeconds(setting.DurationSeconds); + await _projectRepository.UpdateAsync(project); + } } } } @@ -244,7 +246,7 @@ public class EventArrangementService : BaseService x.EventId == eventId); var projectIds = eventProjects.Select(x => x.ProjectId).Distinct().ToList(); var projects = await _projectRepository.GetListAsync(x => projectIds.Contains(x.Id)); - + // 按照 SortIndex 对 projects 排序 var sortedProjects = projects.OrderBy(x => x.SortIndex).ToList(); var groups = await _groupRepository.GetListAsync(x => projectIds.Contains(x.ProjectId)); @@ -252,9 +254,9 @@ public class EventArrangementService : BaseService((p, r) => p.RegistrationId == r.Id) - .Where((p, r) => r.EventId == eventId - && r.PaymentStatus != 4 - && !p.DeleteMark + .Where((p, r) => r.EventId == eventId + && r.PaymentStatus != 4 + && !p.DeleteMark && p.PersonType == PersonType.Athlete) .GroupBy((p, r) => new { p.ProjectId, p.GroupName }) .Select((p, r) => new { p.ProjectId, p.GroupName, Count = SqlFunc.AggregateCount(p.Id) }) @@ -263,9 +265,9 @@ public class EventArrangementService : BaseService((p, r) => p.RegistrationId == r.Id) - .Where((p, r) => r.EventId == eventId - && r.PaymentStatus != 4 - && !p.DeleteMark + .Where((p, r) => r.EventId == eventId + && r.PaymentStatus != 4 + && !p.DeleteMark && p.PersonType == PersonType.Athlete && r.TeamName != null) .Select((p, r) => new { p.ProjectId, p.GroupName, r.TeamName }) @@ -278,27 +280,29 @@ public class EventArrangementService : BaseService(); - - // 2. 生成排期 - 按照排序后的项目顺序 + + // 2. 生成排期 - 按照排序后的项目顺序处理 foreach (var proj in sortedProjects) { var eventProj = eventProjects.FirstOrDefault(x => x.ProjectId == proj.Id); if (eventProj == null) continue; int venueCount = proj.VenueCount > 0 ? proj.VenueCount : 1; - + // 获取参赛人员 var persons = await _personRepository.AsQueryable() .InnerJoin((p, r) => p.RegistrationId == r.Id) - .Where((p, r) => p.ProjectId == eventProj.Id - && r.PaymentStatus != 4 - && !p.DeleteMark + .Where((p, r) => p.ProjectId == eventProj.Id + && r.PaymentStatus != 4 + && !p.DeleteMark && p.PersonType == PersonType.Athlete) .OrderBy(p => p.GroupName) .OrderBy(p => p.Id) - .Select((p, r) => new { - p.Name, p.GroupName, r.TeamName, - Duration = proj.DurationSeconds + .Select((p, r) => new { + p.Name, + p.GroupName, + r.TeamName, + Duration = proj.GetTotalDurationInSeconds() }) .ToListAsync(); @@ -326,13 +330,13 @@ public class EventArrangementService : BaseService 0 ? totalTeams : 1; int rounds = (int)Math.Ceiling((double)teamCount / venueCount); - projectTotalDurationSeconds = rounds * proj.DurationSeconds; + projectTotalDurationSeconds = rounds * proj.GetTotalDurationInSeconds(); } else { int participantCount = totalParticipants > 0 ? totalParticipants : 1; int rounds = (int)Math.Ceiling((double)participantCount / venueCount); - projectTotalDurationSeconds = rounds * proj.DurationSeconds; + projectTotalDurationSeconds = rounds * proj.GetTotalDurationInSeconds(); } string projectTotalDuration = FormatDurationSymbol(projectTotalDurationSeconds); @@ -340,12 +344,12 @@ public class EventArrangementService : BaseService x.Id == registrationId && x.CreatedUserId == userId); if (registration == null) throw Oops.Bah("报名记录不存在"); - // 仅 status = 待付款 允许上传 - if (registration.PaymentStatus != 0) throw Oops.Bah("当前状态不允许上传凭证"); + var uploadResult = await _fileService.Upload(input.File); var fileInfo = uploadResult.First(); registration.PaymentProofUrl = fileInfo.Url; - registration.PaymentStatus = 0; registration.UpdatedTime = DateTime.Now; await _registrationRepository.UpdateAsync(registration); diff --git a/YD_Event.Application/Order/Dtos/OrderListOutput.cs b/YD_Event.Application/Order/Dtos/OrderListOutput.cs index 3c41889..ab5c715 100644 --- a/YD_Event.Application/Order/Dtos/OrderListOutput.cs +++ b/YD_Event.Application/Order/Dtos/OrderListOutput.cs @@ -116,6 +116,16 @@ public class OrderListOutput /// 退款时间 /// public DateTime? RefundTime { get; set; } + + /// + /// 关联原始订单的实付金额(仅退款订单时有效) + /// + public decimal? RelatedOrderActualAmount { get; set; } + + /// + /// 是否为退款订单 + /// + public bool IsRefundOrder { get; set; } } /// @@ -237,6 +247,16 @@ public class OrderDetailOutput /// 联系人电话 /// public string ContactPhone { get; set; } + + /// + /// 关联原始订单的实付金额(仅退款订单时有效) + /// + public decimal? RelatedOrderActualAmount { get; set; } + + /// + /// 是否为退款订单 + /// + public bool IsRefundOrder { get; set; } } /// diff --git a/YD_Event.Application/Order/OrderService.cs b/YD_Event.Application/Order/OrderService.cs index 5108507..fda4b3a 100644 --- a/YD_Event.Application/Order/OrderService.cs +++ b/YD_Event.Application/Order/OrderService.cs @@ -231,7 +231,9 @@ public class OrderService : BaseService, ITransien CreatedTime = o.CreatedTime, PaymentTime = o.PaymentTime, RefundAmount = o.OrderType == 2 ? o.ActualAmount : 0, // 如果是退款订单,ActualAmount即为退款金额 - RefundTime = o.OrderType == 2 ? o.PaymentTime : null // 退款时间 + RefundTime = o.OrderType == 2 ? o.PaymentTime : null, // 退款时间 + RelatedOrderActualAmount = o.OrderType == 2 ? ro.ActualAmount : null, // 退款订单显示原始订单实付金额 + IsRefundOrder = o.OrderType == 2 }) .ToPagedListAsync(input); @@ -250,7 +252,7 @@ public class OrderService : BaseService, ITransien .LeftJoin((o, e, r) => o.RegistrationId == r.Id) .LeftJoin((o, e, r, ro) => o.RelatedOrderId == ro.Id) .Where(o => o.Id == id) - .Select((o, e, r, ro) => new { Order = o, EventName = e.EventName, Registration = r, RelatedOrderNo = ro.OrderNo }); + .Select((o, e, r, ro) => new { Order = o, EventName = e.EventName, Registration = r, RelatedOrderNo = ro.OrderNo, RelatedOrderActualAmount = ro.ActualAmount }); var orderInfo = await query.FirstAsync(); @@ -307,7 +309,9 @@ public class OrderService : BaseService, ITransien CreatedTime = orderInfo.Order.CreatedTime, RelatedOrderNo = orderInfo.RelatedOrderNo, ContactName = orderInfo.Registration?.ContactName, - ContactPhone = orderInfo.Registration?.ContactPhone + ContactPhone = orderInfo.Registration?.ContactPhone, + RelatedOrderActualAmount = orderInfo.Order.OrderType == 2 ? orderInfo.RelatedOrderActualAmount : null, + IsRefundOrder = orderInfo.Order.OrderType == 2 }; } @@ -528,7 +532,9 @@ public class OrderService : BaseService, ITransien o.OrderStatus == OrderStatus.Refunded ? "已退款" : o.OrderStatus == OrderStatus.Shipped ? "已发货" : o.OrderStatus == OrderStatus.Completed ? "已完成" : "", - CreatedTime = o.CreatedTime + CreatedTime = o.CreatedTime, + RelatedOrderActualAmount = o.OrderType == 2 ? ro.ActualAmount : null, + IsRefundOrder = o.OrderType == 2 }) .ToListAsync(); @@ -559,7 +565,7 @@ public class OrderService : BaseService, ITransien var totalActual = orders.Where(x => x.OrderType == "收入").Sum(x => x.ActualAmount); // 汇总信息行 - var summaryRange = worksheet.Cells[rowIndex, 1, rowIndex, 14]; + var summaryRange = worksheet.Cells[rowIndex, 1, rowIndex, 15]; summaryRange.Merge = true; summaryRange.Value = $"汇总信息: 原金额:{totalOriginal}, 优惠金额:{totalDiscount}, 退款金额:{totalRefund}, 实付金额:{totalActual}"; var summaryStyle = summaryRange.Style; @@ -576,7 +582,7 @@ public class OrderService : BaseService, ITransien { "订单编号", "单位名称", "所属赛事", "订单类型", "关联订单", "支付方式", "报名方式", "报名用户", "报名账号", "原金额(元)", - "优惠金额(元)", "退款金额(元)", "实付金额(元)", "生成时间" + "优惠金额(元)", "退款金额(元)", "原始订单实付金额(元)", "实付金额(元)", "生成时间" }; for (int i = 0; i < headers.Length; i++) @@ -600,6 +606,7 @@ public class OrderService : BaseService, ITransien { var refundAmount = item.OrderType == "退款" ? item.ActualAmount : 0; var actualPayAmount = item.OrderType == "收入" ? item.ActualAmount : 0; + var relatedAmount = item.IsRefundOrder ? item.RelatedOrderActualAmount : null; worksheet.Cells[rowIndex, 1].Value = item.OrderNo; worksheet.Cells[rowIndex, 2].Value = item.UnitName; @@ -613,21 +620,23 @@ public class OrderService : BaseService, ITransien worksheet.Cells[rowIndex, 10].Value = item.OriginalAmount; worksheet.Cells[rowIndex, 11].Value = item.DiscountAmount; worksheet.Cells[rowIndex, 12].Value = refundAmount; - worksheet.Cells[rowIndex, 13].Value = actualPayAmount; - worksheet.Cells[rowIndex, 14].Value = item.CreatedTime.ToString("yyyy-MM-dd HH:mm:ss"); + worksheet.Cells[rowIndex, 13].Value = relatedAmount; + worksheet.Cells[rowIndex, 14].Value = actualPayAmount; + worksheet.Cells[rowIndex, 15].Value = item.CreatedTime.ToString("yyyy-MM-dd HH:mm:ss"); // 设置金额格式 worksheet.Cells[rowIndex, 10].Style.Numberformat.Format = "0.00"; worksheet.Cells[rowIndex, 11].Style.Numberformat.Format = "0.00"; worksheet.Cells[rowIndex, 12].Style.Numberformat.Format = "0.00"; worksheet.Cells[rowIndex, 13].Style.Numberformat.Format = "0.00"; + worksheet.Cells[rowIndex, 14].Style.Numberformat.Format = "0.00"; - SetDataRowStyle(worksheet, rowIndex, 14); + SetDataRowStyle(worksheet, rowIndex, 15); rowIndex++; } // 设置列宽 - var columnWidths = new double[] { 18, 15, 15, 10, 12, 10, 10, 12, 14, 12, 14, 14, 12, 18 }; + var columnWidths = new double[] { 18, 15, 15, 10, 12, 10, 10, 12, 14, 12, 14, 14, 18, 12, 18 }; for (int i = 0; i < columnWidths.Length; i++) { worksheet.Column(i + 1).Width = columnWidths[i]; diff --git a/YD_Event.Application/YD_Event.Application.xml b/YD_Event.Application/YD_Event.Application.xml index d21143d..8206cec 100644 --- a/YD_Event.Application/YD_Event.Application.xml +++ b/YD_Event.Application/YD_Event.Application.xml @@ -7276,11 +7276,6 @@ 支付时间 - - - 支付渠道 (微信/支付宝/银行转账) - - 交易流水号 (可选) @@ -7766,6 +7761,11 @@ 赛事编排服务 + + + 格式化时长显示(符号格式:X'X") + + 获取编排项目列表 @@ -9966,6 +9966,16 @@ 退款时间 + + + 关联原始订单的实付金额(仅退款订单时有效) + + + + + 是否为退款订单 + + 订单详情输出 @@ -10086,6 +10096,16 @@ 联系人电话 + + + 关联原始订单的实付金额(仅退款订单时有效) + + + + + 是否为退款订单 + + 订单项目DTO diff --git a/YD_Event.Core/Extensions/ProjectTimeExtensions.cs b/YD_Event.Core/Extensions/ProjectTimeExtensions.cs new file mode 100644 index 0000000..1c2cb9f --- /dev/null +++ b/YD_Event.Core/Extensions/ProjectTimeExtensions.cs @@ -0,0 +1,144 @@ +using YD_Event.Core.Entities; + +namespace YD_Event.Core.Extensions; + +/// +/// 项目时间处理扩展类 +/// 用于统一处理 Project 实体的时间计算逻辑 +/// +public static class ProjectTimeExtensions +{ + /// + /// 获取项目的总时长(秒) + /// + /// 项目实体 + /// 总时长(秒) + /// 当 project 为 null 时抛出 + public static int GetTotalDurationInSeconds(this Project project) + { + if (project == null) + throw new ArgumentNullException(nameof(project)); + + // 边界情况处理:确保值不小于 0 + int minutes = Math.Max(0, project.DurationMinutes); + int seconds = Math.Max(0, project.DurationSeconds); + + // 确保秒数不超过 59(一分钟内的秒数) + seconds = Math.Min(seconds, 59); + + return minutes * 60 + seconds; + } + + /// + /// 设置项目的总时长(秒) + /// 自动计算并更新 DurationMinutes 和 DurationSeconds + /// + /// 项目实体 + /// 总时长(秒) + /// 当 project 为 null 时抛出 + /// 当 totalSeconds 小于 0 时抛出 + public static void SetTotalDurationInSeconds(this Project project, int totalSeconds) + { + if (project == null) + throw new ArgumentNullException(nameof(project)); + + if (totalSeconds < 0) + throw new ArgumentOutOfRangeException(nameof(totalSeconds), "总时长不能小于 0"); + + // 计算分钟和秒 + project.DurationMinutes = totalSeconds / 60; + project.DurationSeconds = totalSeconds % 60; + } + + /// + /// 标准化项目的时间字段 + /// 确保 DurationSeconds 在 0-59 范围内,并正确处理负值 + /// + /// 项目实体 + /// 当 project 为 null 时抛出 + public static void NormalizeDuration(this Project project) + { + if (project == null) + throw new ArgumentNullException(nameof(project)); + + // 先获取总秒数 + int totalSeconds = project.GetTotalDurationInSeconds(); + + // 重新设置,确保标准化 + project.SetTotalDurationInSeconds(totalSeconds); + } + + /// + /// 获取格式化的时长显示字符串(例如:"2分30秒" 或 "30秒") + /// + /// 项目实体 + /// 格式化的时长字符串 + /// 当 project 为 null 时抛出 + public static string GetFormattedDuration(this Project project) + { + if (project == null) + throw new ArgumentNullException(nameof(project)); + + int totalSeconds = project.GetTotalDurationInSeconds(); + int minutes = totalSeconds / 60; + int seconds = totalSeconds % 60; + + if (minutes == 0) + { + return $"{seconds}秒"; + } + else if (seconds == 0) + { + return $"{minutes}分"; + } + else + { + return $"{minutes}分{seconds}秒"; + } + } + + /// + /// 获取符号格式的时长显示(例如:"2'30\"" 或 "30\"") + /// + /// 项目实体 + /// 符号格式的时长字符串 + /// 当 project 为 null 时抛出 + public static string GetSymbolFormattedDuration(this Project project) + { + if (project == null) + throw new ArgumentNullException(nameof(project)); + + int totalSeconds = project.GetTotalDurationInSeconds(); + int hours = totalSeconds / 3600; + int remainingSeconds = totalSeconds % 3600; + int minutes = remainingSeconds / 60; + int seconds = remainingSeconds % 60; + + if (hours == 0) + { + if (seconds == 0) + { + return $"{minutes}'"; + } + else + { + string paddedSeconds = seconds.ToString().PadLeft(2, '0'); + return $"{minutes}'{paddedSeconds}\""; + } + } + else + { + string result = $"{hours}h"; + if (minutes > 0 || seconds > 0) + { + result += $"{minutes}'"; + if (seconds > 0) + { + string paddedSeconds = seconds.ToString().PadLeft(2, '0'); + result += $"{paddedSeconds}\""; + } + } + return result; + } + } +} diff --git a/YD_Event.Core/YD_Event.Core.xml b/YD_Event.Core/YD_Event.Core.xml index 15bde8d..06e8120 100644 --- a/YD_Event.Core/YD_Event.Core.xml +++ b/YD_Event.Core/YD_Event.Core.xml @@ -4755,6 +4755,54 @@ 赞助单位 + + + 项目时间处理扩展类 + 用于统一处理 Project 实体的时间计算逻辑 + + + + + 获取项目的总时长(秒) + + 项目实体 + 总时长(秒) + 当 project 为 null 时抛出 + + + + 设置项目的总时长(秒) + 自动计算并更新 DurationMinutes 和 DurationSeconds + + 项目实体 + 总时长(秒) + 当 project 为 null 时抛出 + 当 totalSeconds 小于 0 时抛出 + + + + 标准化项目的时间字段 + 确保 DurationSeconds 在 0-59 范围内,并正确处理负值 + + 项目实体 + 当 project 为 null 时抛出 + + + + 获取格式化的时长显示字符串(例如:"2分30秒" 或 "30秒") + + 项目实体 + 格式化的时长字符串 + 当 project 为 null 时抛出 + + + + 获取符号格式的时长显示(例如:"2'30\"" 或 "30\"") + + 项目实体 + 符号格式的时长字符串 + 当 project 为 null 时抛出 + 项目类型判断扩展方法