diff --git a/YD_Event.Application/Client/MyController.cs b/YD_Event.Application/Client/MyController.cs index 0c320f0..9bdec08 100644 --- a/YD_Event.Application/Client/MyController.cs +++ b/YD_Event.Application/Client/MyController.cs @@ -187,6 +187,8 @@ public class MyController : IDynamicApiController user.IdCardImageUrl = input.IdCardImageUrl; user.IdCardBackImageUrl = input.IdCardBackImageUrl; user.PassportPhotoUrl = null; // 清空护照图片 + user.Gender = (Gender)input.Gender.Value; + user.BirthDate = input.BirthDate.Value; } else { diff --git a/YD_Event.Application/Event/Dtos/ScoreDtos.cs b/YD_Event.Application/Event/Dtos/ScoreDtos.cs index 9347f4b..70189c6 100644 --- a/YD_Event.Application/Event/Dtos/ScoreDtos.cs +++ b/YD_Event.Application/Event/Dtos/ScoreDtos.cs @@ -66,10 +66,15 @@ public class ScoreListOutput public string UnitName { get; set; } /// - /// 名次 + /// 名次(数字) /// public int Rank { get; set; } + /// + /// 名次(汉字) + /// + public string RankText { get; set; } + /// /// 性别 /// @@ -250,13 +255,13 @@ public class ScoreImportDto /// /// 名次(数字) /// - [ExcelColumnName("名次")] + [ExcelColumnName("名次(数字)")] public int Rank { get; set; } /// - /// 名次说明(文字描述,如"冠军"、"亚军"等) + /// 名次(汉字) /// - [ExcelColumnName("名次说明")] + [ExcelColumnName("名次(汉字)")] public string RankText { get; set; } } diff --git a/YD_Event.Application/Event/EventScoreService.cs b/YD_Event.Application/Event/EventScoreService.cs index 1efaa9b..e21e3b0 100644 --- a/YD_Event.Application/Event/EventScoreService.cs +++ b/YD_Event.Application/Event/EventScoreService.cs @@ -4,8 +4,12 @@ using Microsoft.Extensions.Logging; using YD_Event.Application.Event.Dtos; using YD_Event.Core.Entities; using YD_Event.Core.Enum; +using YD_Event.Core.Domain.Export; using MiniExcelLibs; using SqlSugar; +using OfficeOpenXml; +using OfficeOpenXml.Style; +using System.Drawing; namespace YD_Event.Application.Event; @@ -14,11 +18,40 @@ namespace YD_Event.Application.Event; /// public class EventScoreService : BaseService, ITransient { + private static readonly string[] ChineseNumbers = { "零", "一", "二", "三", "四", "五", "六", "七", "八", "九", "十" }; + + /// + /// 将数字名次转换为汉字形式 + /// + private static string ConvertRankToChinese(int rank) + { + if (rank <= 0) + return string.Empty; + + if (rank <= 10) + return $"第{ChineseNumbers[rank]}名"; + + if (rank < 20) + return $"第十{ChineseNumbers[rank % 10]}名"; + + if (rank % 10 == 0) + return $"第{ChineseNumbers[rank / 10]}十名"; + + return $"第{ChineseNumbers[rank / 10]}十{ChineseNumbers[rank % 10]}名"; + } + private readonly ISqlSugarRepository _personRepository; private readonly ISqlSugarRepository _resultRepository; private readonly ISqlSugarRepository _certificateRepository; private readonly ISqlSugarRepository _registrationRepository; private readonly ISqlSugarRepository _templateRepository; + private readonly ISqlSugarRepository _projectRepository; + private readonly ISqlSugarRepository _teamMemberRepository; + private readonly ISqlSugarRepository _eventRepository; + private readonly ISqlSugarRepository _pointsRuleRepository; + private readonly ISqlSugarRepository _pointsRuleDetailRepository; + private readonly ISqlSugarRepository _pointsRecordRepository; + private readonly ISqlSugarRepository _appUserRepository; private readonly IIdGenerator _idGenerator; private readonly ILogger _logger; @@ -28,6 +61,13 @@ public class EventScoreService : BaseService, ITransient ISqlSugarRepository certificateRepository, ISqlSugarRepository registrationRepository, ISqlSugarRepository templateRepository, + ISqlSugarRepository projectRepository, + ISqlSugarRepository teamMemberRepository, + ISqlSugarRepository eventRepository, + ISqlSugarRepository pointsRuleRepository, + ISqlSugarRepository pointsRuleDetailRepository, + ISqlSugarRepository pointsRecordRepository, + ISqlSugarRepository appUserRepository, IIdGenerator idGenerator, ILogger logger) : base(resultRepository) { @@ -36,6 +76,13 @@ public class EventScoreService : BaseService, ITransient _certificateRepository = certificateRepository; _registrationRepository = registrationRepository; _templateRepository = templateRepository; + _projectRepository = projectRepository; + _teamMemberRepository = teamMemberRepository; + _eventRepository = eventRepository; + _pointsRuleRepository = pointsRuleRepository; + _pointsRuleDetailRepository = pointsRuleDetailRepository; + _pointsRecordRepository = pointsRecordRepository; + _appUserRepository = appUserRepository; _idGenerator = idGenerator; _logger = logger; } @@ -46,7 +93,7 @@ public class EventScoreService : BaseService, ITransient [DisplayName("运动员成绩列表")] public async Task> PageAthlete(ScoreQueryInput input) { - return await _personRepository.AsQueryable() + var pageResult = await _personRepository.AsQueryable() .LeftJoin((p, r) => p.RegistrationId == r.Id) .LeftJoin((p, r, res) => p.Id == res.RegistrationPersonId) .LeftJoin((p, r, res, c) => res.Id == c.ResultId) @@ -68,32 +115,79 @@ public class EventScoreService : BaseService, ITransient CertificateId = c.Id }) .ToPagedListAsync(input); + + foreach (var item in pageResult.Rows) + { + item.RankText = ConvertRankToChinese(item.Rank); + } + + return pageResult; } /// /// 教练/领队列表 /// + /// + /// 通过运动员所在的团队关联获取教练和领队人员信息 + /// [DisplayName("教练领队列表")] public async Task> PageCoachLeader(CoachLeaderQueryInput input) { - return await _personRepository.AsQueryable() - .LeftJoin((p, c) => p.Id == c.ParticipationRecordId) - .InnerJoin((p, c, r) => p.RegistrationId == r.Id) - .Where((p, c, r) => r.EventId == input.EventId && p.PersonType == input.PersonType) - .WhereIF(!string.IsNullOrWhiteSpace(input.Name), p => p.Name.Contains(input.Name)) - .WhereIF(!string.IsNullOrWhiteSpace(input.Mobile), p => p.Phone.Contains(input.Mobile)) - .WhereIF(input.Gender.HasValue, p => p.Gender == input.Gender.Value) - .Select((p, c) => new CoachLeaderListOutput + // 步骤1:获取该赛事的所有运动员的 UserId + var athleteUserIds = await _personRepository.AsQueryable() + .InnerJoin((p, r) => p.RegistrationId == r.Id) + .Where((p, r) => r.EventId == input.EventId && p.PersonType == PersonType.Athlete) + .Select(p => p.UserId) + .ToListAsync(); + + if (!athleteUserIds.Any()) + { + return new PageResult(); + } + + // 步骤2:通过运动员的 UserId 找到对应的 TeamId + var teamIds = await _teamMemberRepository.AsQueryable() + .Where(tm => athleteUserIds.Contains(tm.UserId) && !tm.DeleteMark) + .Select(tm => tm.TeamId) + .Distinct() + .ToListAsync(); + + if (!teamIds.Any()) + { + return new PageResult(); + } + + // 步骤3:通过 TeamId 获取同团队的教练/领队的 UserId + var coachLeaderUserIds = await _teamMemberRepository.AsQueryable() + .Where(tm => teamIds.Contains(tm.TeamId) && tm.PersonType == input.PersonType && !tm.DeleteMark) + .Select(tm => tm.UserId) + .Distinct() + .ToListAsync(); + + if (!coachLeaderUserIds.Any()) + { + return new PageResult(); + } + + // 步骤4:从 AppUser 表获取教练/领队的详细信息(需要关联查询) + var userQuery = _appUserRepository.AsQueryable() + .Where(p => coachLeaderUserIds.Contains(p.Id)) + .Where(p=> p.PersonType == input.PersonType); + + var pageResult = await userQuery + .Select(p => new CoachLeaderListOutput { Id = p.Id, Name = p.Name, Age = p.Age, - Gender = p.Gender.ToString(), + Gender = p.Gender.Description(), IdCardNumber = p.IdCard, Mobile = p.Phone, - CertificateId = c.Id + CertificateId = 0 // 需要单独查询,此处先设为0 }) .ToPagedListAsync(input); + + return pageResult; } /// @@ -142,7 +236,7 @@ public class EventScoreService : BaseService, ITransient p.ProjectName, p.GroupName, p.RegistrationId, - p.ProjectId + p.ProjectId }) .ToListAsync(); @@ -152,6 +246,11 @@ public class EventScoreService : BaseService, ITransient var personDict = allPersons .GroupBy(p => $"{p.Name}_{p.ProjectName}_{p.GroupName}") .ToDictionary(g => g.Key, g => g.First()); + + // 预加载项目信息,用于获取项目类型名称 + var projectIds = allPersons.Select(p => p.ProjectId).Distinct().ToList(); + var projects = await _projectRepository.GetListAsync(x => projectIds.Contains(x.Id)); + var projectDict = projects.ToDictionary(p => p.Id, p => p.ProjectType.Description()); // 步骤3:预加载该赛事的所有成绩记录 var personIds = allPersons.Select(p => p.Id).ToList(); @@ -182,6 +281,7 @@ public class EventScoreService : BaseService, ITransient existingResult.Score = row.Score; existingResult.Rank = row.Rank; existingResult.Remark = row.RankText; + existingResult.ProjectTypeName = projectDict.TryGetValue(person.ProjectId, out var projectTypeName) ? projectTypeName : string.Empty; existingResult.UpdatedTime = now; resultsToUpdate.Add(existingResult); } @@ -197,6 +297,7 @@ public class EventScoreService : BaseService, ITransient ProjectId = person.ProjectId, ParticipantName = person.Name, GroupName = person.GroupName, + ProjectTypeName = projectDict.TryGetValue(person.ProjectId, out var projectTypeName) ? projectTypeName : string.Empty, Score = row.Score, Rank = row.Rank, Remark = row.RankText, @@ -370,35 +471,29 @@ public class EventScoreService : BaseService, ITransient /// /// 查看证书接口 /// - /// 报名人员ID (EventRegistrationPerson的Id) + /// 输入参数 /// 证书详情 [DisplayName("查看证书")] - [HttpGet("/api/EventScore/GetCertificateDetail/{id}")] - public async Task GetCertificateDetail([FromRoute] long id) + public async Task GetCertificateDetail(YD_Event.Application.Certificate.Dtos.IdInput input) { - if (id <= 0) + if (input.Id <= 0) { - return new BadRequestObjectResult(new { message = "无效的 ID" }); + throw Oops.Bah("无效的 ID"); } - var personExists = await _personRepository.IsAnyAsync(x => x.Id == id); + var personExists = await _personRepository.IsAnyAsync(x => x.Id == input.Id); if (!personExists) { - return new NotFoundObjectResult(new { message = "人员记录不存在" }); + throw Oops.Bah("人员记录不存在"); } - var cert = await _certificateRepository.GetFirstAsync(x => x.ParticipationRecordId == id); + var cert = await _certificateRepository.GetFirstAsync(x => x.ParticipationRecordId == input.Id); if (cert == null) { - return new NotFoundObjectResult(new { message = "未找到对应的证书信息" }); + throw Oops.Bah("未找到对应的证书信息"); } - if (string.IsNullOrWhiteSpace(cert.CertificateUrl)) - { - return new NoContentResult(); - } - - return new JsonResult(new { url = cert.CertificateUrl }); + return new { Url = cert.CertificateUrl }; } /// @@ -442,31 +537,47 @@ public class EventScoreService : BaseService, ITransient [HttpGet] public IActionResult DownloadTemplate() { - var headers = new string[] { - "赛事地址", - "项目", - "组别", - "姓名", - "联系方式", - "单位", - "成绩", - "名次", - "名次说明" - }; - - var value = new List> - { - headers.ToDictionary(h => h, h => (object)null) - }; - - using var memoryStream = new MemoryStream(); - memoryStream.SaveAs(value); - - return new FileContentResult(memoryStream.ToArray(), "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + var exporter = new ScoreTemplateExporter(); + var bytes = exporter.ExportAsync().GetAwaiter().GetResult(); + + return new FileContentResult(bytes, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") { FileDownloadName = "成绩导入模板.xlsx" }; } + + /// + /// 成绩模板导出器 + /// + private class ScoreTemplateExporter : ExcelExportBase + { + public override async Task ExportAsync() + { + CreateWorksheet("成绩导入模板"); + + var headers = new string[] { + "赛事地址", + "项目", + "组别", + "姓名", + "联系方式", + "单位", + "成绩", + "名次(汉字)", + "名次(数字)" + }; + + var headerColor = Color.FromArgb(204, 255, 204); + SetHeaders(headers, 1, headerColor, 11); + + var widths = new double[] { 15, 15, 15, 12, 15, 20, 15, 15, 15 }; + SetColumnWidths(widths); + + SetRowHeight(1, 25); + + return await GetAsByteArrayAsync(); + } + } /// /// 导出证书 @@ -565,4 +676,144 @@ public class EventScoreService : BaseService, ITransient return result; } + + /// + /// 发放积分输入模型 + /// + public class IssuePointsInput + { + public long EventId { get; set; } + } + + /// + /// 发放积分 + /// + [DisplayName("发放积分")] + [HttpPost("issuePoints")] + [UnitOfWork] + public async Task IssuePoints([FromBody] IssuePointsInput input) + { + _logger.LogInformation("开始发放积分,赛事ID={EventId}", input.EventId); + + var eventInfo = await _eventRepository.GetByIdAsync(input.EventId); + if (eventInfo == null) + { + throw Oops.Bah("赛事不存在"); + } + + if (!eventInfo.PointsRuleId.HasValue) + { + 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(); + + var results = await _resultRepository.AsQueryable() + .InnerJoin((r, p) => r.RegistrationPersonId == p.Id) + .Where((r, p) => r.EventId == input.EventId && r.Rank > 0) + .Select((r, p) => new + { + ResultId = r.Id, + UserId = p.UserId, + ProjectId = r.ProjectId, + GroupName = r.GroupName, + Rank = r.Rank, + ParticipantName = r.ParticipantName, + ProjectIdForRecord = r.ProjectId + }) + .ToListAsync(); + + if (!results.Any()) + { + throw Oops.Bah("没有可发放积分的成绩记录"); + } + + var existingRecords = await _pointsRecordRepository.AsQueryable() + .Where(x => x.EventId == input.EventId) + .Select(x => x.Remark) + .ToListAsync(); + + var processedResultIds = existingRecords + .Where(x => !string.IsNullOrEmpty(x) && x.StartsWith("ResultId:")) + .Select(x => + { + var parts = x.Split('|'); + var resultIdPart = parts.FirstOrDefault(p => p.StartsWith("ResultId:")); + return resultIdPart != null && long.TryParse(resultIdPart.Replace("ResultId:", ""), out var id) ? id : 0; + }) + .Where(x => x > 0) + .ToHashSet(); + + int successCount = 0; + int skipCount = 0; + var pointsRecords = new List(); + + foreach (var result in results) + { + if (processedResultIds.Contains(result.ResultId)) + { + skipCount++; + continue; + } + + int pointsToAward = 0; + + if (pointsRule.ParticipationPoints.HasValue && pointsRule.ParticipationPoints.Value > 0) + { + pointsToAward += pointsRule.ParticipationPoints.Value; + } + + var matchingDetail = ruleDetails.FirstOrDefault(d => result.Rank >= d.StartRank && result.Rank <= d.EndRank); + if (matchingDetail != null) + { + pointsToAward += matchingDetail.Points; + } + + if (pointsToAward > 0) + { + var record = new PointsRecord + { + Id = _idGenerator.NextId(), + UserId = result.UserId, + Type = PointsOperationType.Acquire, + EventId = input.EventId, + ProjectId = result.ProjectIdForRecord, + Points = pointsToAward, + Remark = $"ResultId:{result.ResultId}|Rank:{result.Rank}", + Status = AvailabilityStatus.Enable, + CreatedTime = DateTime.Now + }; + pointsRecords.Add(record); + successCount++; + } + } + + if (pointsRecords.Any()) + { + await _pointsRecordRepository.InsertRangeAsync(pointsRecords); + } + + _logger.LogInformation("积分发放完成:赛事ID={EventId},成功发放={SuccessCount},跳过={SkipCount}", + input.EventId, successCount, skipCount); + + return new + { + Success = true, + Message = $"积分发放成功!已发放 {successCount} 条积分记录,跳过 {skipCount} 条已发放记录" + }; + } } diff --git a/YD_Event.Application/Event/PaymentExpirationBackgroundService.cs b/YD_Event.Application/Event/PaymentExpirationBackgroundService.cs index 0bf3517..8c9df01 100644 --- a/YD_Event.Application/Event/PaymentExpirationBackgroundService.cs +++ b/YD_Event.Application/Event/PaymentExpirationBackgroundService.cs @@ -1,6 +1,10 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using YD_Event.Core.Entities; +using YD_Event.Core.Enum; +using YD_Event.Core.Infrastructure.WeChat; +using YD_Event.Core.Infrastructure.WeChat.Dto; namespace YD_Event.Application.Event; @@ -31,6 +35,8 @@ public class PaymentExpirationBackgroundService : BackgroundService while (await _timer.WaitForNextTickAsync(stoppingToken)) { await ProcessExpiredPaymentsAsync(stoppingToken); + await ProcessUnpaidWeChatOrdersAsync(stoppingToken); + await ProcessRefundingWeChatOrdersAsync(stoppingToken); } } catch (OperationCanceledException) @@ -90,6 +96,383 @@ public class PaymentExpirationBackgroundService : BackgroundService } } + /// + /// 处理微信未支付订单:定期检查并更新订单状态 + /// + private async Task ProcessUnpaidWeChatOrdersAsync(CancellationToken stoppingToken) + { + try + { + _logger.LogInformation("开始检查微信未支付订单"); + + using var scope = _serviceProvider.CreateScope(); + var registrationRepository = scope.ServiceProvider.GetRequiredService>(); + var orderRepository = scope.ServiceProvider.GetRequiredService>(); + var weChatPayConfigRepository = scope.ServiceProvider.GetRequiredService>(); + var weChatPayService = scope.ServiceProvider.GetRequiredService(); + var paymentRepository = scope.ServiceProvider.GetRequiredService>(); + + var now = DateTime.Now; + + var unpaidRegistrations = await registrationRepository.AsQueryable() + .Where(x => x.PaymentMethod == 2 + && x.PaymentMethodType == PaymentMethodType.WeChatPay + && x.PaymentStatus == 0 + && !x.DeleteMark) + .OrderBy(x => x.CreatedTime) + .Take(50) + .ToListAsync(stoppingToken); + + if (!unpaidRegistrations.Any()) + { + _logger.LogInformation("没有发现需要检查的微信未支付订单"); + return; + } + + _logger.LogInformation("发现 {Count} 个微信未支付订单需要检查", unpaidRegistrations.Count); + + foreach (var registration in unpaidRegistrations) + { + try + { + if (string.IsNullOrEmpty(registration.OrderNo)) + { + _logger.LogWarning("报名记录 {RegistrationId} 没有订单编号", registration.Id); + continue; + } + + var order = await orderRepository.GetFirstAsync(o => o.OrderNo == registration.OrderNo, stoppingToken); + if (order == null) + { + _logger.LogWarning("找不到订单编号 {OrderNo} 对应的订单记录", registration.OrderNo); + continue; + } + + var weChatPayConfig = await weChatPayConfigRepository.GetFirstAsync( + c => c.MerchantId == order.MerchantId && c.Status == AvailabilityStatus.Enable && !c.DeleteMark, + stoppingToken); + + if (weChatPayConfig == null) + { + _logger.LogWarning("商户 {MerchantId} 没有找到可用的微信支付配置", order.MerchantId); + continue; + } + + var queryResult = await weChatPayService.QueryOrderAsync(weChatPayConfig, registration.OrderNo); + + if (!queryResult.Success) + { + _logger.LogWarning("查询订单 {OrderNo} 状态失败: {Message}", registration.OrderNo, queryResult.Message); + continue; + } + + _logger.LogInformation("订单 {OrderNo} 微信支付状态: {TradeState}", registration.OrderNo, queryResult.TradeState); + + if (queryResult.TradeState == "SUCCESS") + { + await ProcessWeChatPaymentSuccessAsync( + registration, + order, + queryResult, + now, + registrationRepository, + orderRepository, + paymentRepository, + stoppingToken); + } + else if (queryResult.TradeState == "CLOSED" || queryResult.TradeState == "REVOKED") + { + await ProcessWeChatPaymentClosedAsync( + registration, + order, + now, + registrationRepository, + orderRepository, + stoppingToken); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "处理报名记录 {RegistrationId} 时发生错误", registration.Id); + } + } + + _logger.LogInformation("微信未支付订单检查完成"); + } + catch (Exception ex) + { + _logger.LogError(ex, "检查微信未支付订单时发生错误"); + } + } + + /// + /// 处理微信支付成功的订单 + /// + private async Task ProcessWeChatPaymentSuccessAsync( + EventRegistration registration, + YD_Event.Core.Entities.Order order, + WeChatPayQueryResult queryResult, + DateTime now, + ISqlSugarRepository registrationRepository, + ISqlSugarRepository orderRepository, + ISqlSugarRepository paymentRepository, + CancellationToken stoppingToken) + { + _logger.LogInformation("订单 {OrderNo} 支付成功,更新订单状态", registration.OrderNo); + + var paymentTime = queryResult.SuccessTime.HasValue ? queryResult.SuccessTime.Value.LocalDateTime : now; + + registration.PaymentStatus = 1; + registration.PaymentTime = paymentTime; + registration.UpdatedTime = now; + await registrationRepository.UpdateAsync(registration); + + order.OrderStatus = OrderStatus.Paid; + order.PaymentTime = paymentTime; + order.UpdatedTime = now; + await orderRepository.UpdateAsync(order); + + var existingPayment = await paymentRepository.GetFirstAsync(p => p.RegistrationId == registration.Id, stoppingToken); + if (existingPayment != null) + { + existingPayment.PaymentStatus = 1; + existingPayment.PaymentTime = paymentTime; + existingPayment.UpdatedTime = now; + if (!string.IsNullOrEmpty(queryResult.TransactionId)) + { + existingPayment.TransactionNo = queryResult.TransactionId; + } + await paymentRepository.UpdateAsync(existingPayment); + } + else + { + var idGenerator = _serviceProvider.GetRequiredService(); + var newPayment = new EventRegistrationPayment + { + Id = idGenerator.NextId(), + RegistrationId = registration.Id, + TransactionNo = queryResult.TransactionId ?? string.Empty, + PaymentChannel = 1, + PaymentAmount = order.ActualAmount, + PaymentTime = paymentTime, + PaymentStatus = 1, + RefundStatus = 0, + RefundAmount = 0, + PayeeAccount = string.Empty, + CreatedTime = now, + CreatedUserId = registration.CreatedUserId + }; + await paymentRepository.InsertAsync(newPayment); + } + + _logger.LogInformation("订单 {OrderNo} 支付状态更新成功", registration.OrderNo); + } + + /// + /// 处理微信支付关闭的订单 + /// + private async Task ProcessWeChatPaymentClosedAsync( + EventRegistration registration, + YD_Event.Core.Entities.Order order, + DateTime now, + ISqlSugarRepository registrationRepository, + ISqlSugarRepository orderRepository, + CancellationToken stoppingToken) + { + _logger.LogInformation("订单 {OrderNo} 支付已关闭,更新订单状态为已取消", registration.OrderNo); + + registration.PaymentStatus = 4; + registration.UpdatedTime = now; + await registrationRepository.UpdateAsync(registration); + + order.OrderStatus = OrderStatus.Cancelled; + order.UpdatedTime = now; + await orderRepository.UpdateAsync(order); + + _logger.LogInformation("订单 {OrderNo} 状态更新为已取消成功", registration.OrderNo); + } + + /// + /// 处理微信退款中的订单 + /// + private async Task ProcessRefundingWeChatOrdersAsync(CancellationToken stoppingToken) + { + try + { + _logger.LogInformation("开始检查微信退款中的订单"); + + using var scope = _serviceProvider.CreateScope(); + var refundRecordRepository = scope.ServiceProvider.GetRequiredService>(); + var registrationRepository = scope.ServiceProvider.GetRequiredService>(); + var orderRepository = scope.ServiceProvider.GetRequiredService>(); + var weChatPayConfigRepository = scope.ServiceProvider.GetRequiredService>(); + var weChatPayService = scope.ServiceProvider.GetRequiredService(); + var paymentRepository = scope.ServiceProvider.GetRequiredService>(); + + var now = DateTime.Now; + + var refundingRecords = await refundRecordRepository.AsQueryable() + .Where(x => x.RefundStatus == 0 && !x.DeleteMark) + .OrderBy(x => x.ApplyTime) + .Take(50) + .ToListAsync(stoppingToken); + + if (!refundingRecords.Any()) + { + _logger.LogInformation("没有发现需要检查的微信退款订单"); + return; + } + + _logger.LogInformation("发现 {Count} 个微信退款订单需要检查", refundingRecords.Count); + + foreach (var refundRecord in refundingRecords) + { + try + { + if (string.IsNullOrEmpty(refundRecord.OutRefundNo)) + { + _logger.LogWarning("退款记录 {RefundRecordId} 没有退款单号", refundRecord.Id); + continue; + } + + WeChatPayConfig? weChatPayConfig = null; + if (refundRecord.WeChatPayConfigId.HasValue) + { + weChatPayConfig = await weChatPayConfigRepository.GetByIdAsync(refundRecord.WeChatPayConfigId.Value, stoppingToken); + } + else if (refundRecord.MerchantId.HasValue) + { + weChatPayConfig = await weChatPayConfigRepository.GetFirstAsync( + c => c.MerchantId == refundRecord.MerchantId && c.Status == AvailabilityStatus.Enable && !c.DeleteMark, + stoppingToken); + } + + if (weChatPayConfig == null) + { + _logger.LogWarning("退款记录 {RefundRecordId} 没有找到可用的微信支付配置", refundRecord.Id); + continue; + } + + var queryResult = await weChatPayService.QueryRefundAsync(weChatPayConfig, refundRecord.OutRefundNo); + + if (!queryResult.Success) + { + _logger.LogWarning("查询退款 {OutRefundNo} 状态失败: {Message}", refundRecord.OutRefundNo, queryResult.Message); + continue; + } + + _logger.LogInformation("退款 {OutRefundNo} 微信退款状态: {RefundStatus}", refundRecord.OutRefundNo, queryResult.RefundStatus); + + if (queryResult.RefundStatus == "SUCCESS") + { + await ProcessWeChatRefundSuccessAsync( + refundRecord, + queryResult, + now, + refundRecordRepository, + registrationRepository, + orderRepository, + paymentRepository, + stoppingToken); + } + else if (queryResult.RefundStatus == "CLOSED" || queryResult.RefundStatus == "ABNORMAL") + { + await ProcessWeChatRefundFailedAsync( + refundRecord, + queryResult, + now, + refundRecordRepository, + stoppingToken); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "处理退款记录 {RefundRecordId} 时发生错误", refundRecord.Id); + } + } + + _logger.LogInformation("微信退款订单检查完成"); + } + catch (Exception ex) + { + _logger.LogError(ex, "检查微信退款订单时发生错误"); + } + } + + /// + /// 处理微信退款成功的订单 + /// + private async Task ProcessWeChatRefundSuccessAsync( + RefundRecord refundRecord, + WeChatRefundQueryResult queryResult, + DateTime now, + ISqlSugarRepository refundRecordRepository, + ISqlSugarRepository registrationRepository, + ISqlSugarRepository orderRepository, + ISqlSugarRepository paymentRepository, + CancellationToken stoppingToken) + { + _logger.LogInformation("退款 {OutRefundNo} 成功,更新退款状态", refundRecord.OutRefundNo); + + var successTime = queryResult.SuccessTime.HasValue ? queryResult.SuccessTime.Value.LocalDateTime : now; + + refundRecord.RefundStatus = 1; + refundRecord.SuccessTime = successTime; + refundRecord.RefundId = queryResult.RefundId ?? refundRecord.RefundId; + refundRecord.WeChatResponse = queryResult.WeChatResponse ?? refundRecord.WeChatResponse; + refundRecord.UpdatedTime = now; + await refundRecordRepository.UpdateAsync(refundRecord); + + var registration = await registrationRepository.GetByIdAsync(refundRecord.RegistrationId, stoppingToken); + if (registration != null) + { + registration.PaymentStatus = 3; + registration.UpdatedTime = now; + await registrationRepository.UpdateAsync(registration); + } + + var order = await orderRepository.GetFirstAsync(o => o.OrderNo == refundRecord.OutTradeNo, stoppingToken); + if (order != null) + { + order.OrderStatus = OrderStatus.Refunded; + order.UpdatedTime = now; + await orderRepository.UpdateAsync(order); + } + + var payment = await paymentRepository.GetFirstAsync(p => p.RegistrationId == refundRecord.RegistrationId, stoppingToken); + if (payment != null) + { + payment.RefundStatus = 2; + payment.RefundAmount = refundRecord.RefundAmount; + payment.RefundTime = successTime; + payment.UpdatedTime = now; + await paymentRepository.UpdateAsync(payment); + } + + _logger.LogInformation("退款 {OutRefundNo} 状态更新成功", refundRecord.OutRefundNo); + } + + /// + /// 处理微信退款失败的订单 + /// + private async Task ProcessWeChatRefundFailedAsync( + RefundRecord refundRecord, + WeChatRefundQueryResult queryResult, + DateTime now, + ISqlSugarRepository refundRecordRepository, + CancellationToken stoppingToken) + { + _logger.LogInformation("退款 {OutRefundNo} 失败,更新退款状态", refundRecord.OutRefundNo); + + refundRecord.RefundStatus = queryResult.RefundStatus == "CLOSED" ? 3 : 2; + refundRecord.ErrorMessage = queryResult.Message ?? "退款失败"; + refundRecord.WeChatResponse = queryResult.WeChatResponse ?? refundRecord.WeChatResponse; + refundRecord.UpdatedTime = now; + await refundRecordRepository.UpdateAsync(refundRecord); + + _logger.LogInformation("退款 {OutRefundNo} 状态更新为失败成功", refundRecord.OutRefundNo); + } + public override void Dispose() { _timer.Dispose(); diff --git a/YD_Event.Application/YD_Event.Application.xml b/YD_Event.Application/YD_Event.Application.xml index ec01904..c123103 100644 --- a/YD_Event.Application/YD_Event.Application.xml +++ b/YD_Event.Application/YD_Event.Application.xml @@ -5674,7 +5674,12 @@ - 名次 + 名次(数字) + + + + + 名次(汉字) @@ -5849,7 +5854,7 @@ - 名次说明(文字描述,如"冠军"、"亚军"等) + 名次(汉字) @@ -7502,6 +7507,11 @@ 成绩与证书管理 + + + 将数字名次转换为汉字形式 + + 运动员成绩列表 @@ -7511,6 +7521,9 @@ 教练/领队列表 + + 通过运动员所在的团队关联获取教练和领队人员信息 + @@ -7518,11 +7531,11 @@ 发放证书(支持模板选择和批量发放) - + 查看证书接口 - 报名人员ID (EventRegistrationPerson的Id) + 输入参数 证书详情 @@ -7535,6 +7548,11 @@ 下载模板 + + + 成绩模板导出器 + + 导出证书 @@ -7550,6 +7568,16 @@ 验证导入数据 + + + 发放积分输入模型 + + + + + 发放积分 + + 赛事管理服务 @@ -7659,6 +7687,36 @@ 支付超时处理后台服务 + + + 处理微信未支付订单:定期检查并更新订单状态 + + + + + 处理微信支付成功的订单 + + + + + 处理微信支付关闭的订单 + + + + + 处理微信退款中的订单 + + + + + 处理微信退款成功的订单 + + + + + 处理微信退款失败的订单 + + 统一报名处理服务