赛事积分

This commit is contained in:
马梓彭 2026-04-14 15:11:41 +08:00
parent f7eed7e479
commit 5a86989ec5
5 changed files with 755 additions and 56 deletions

View File

@ -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
{

View File

@ -66,10 +66,15 @@ public class ScoreListOutput
public string UnitName { get; set; }
/// <summary>
/// 名次
/// 名次(数字)
/// </summary>
public int Rank { get; set; }
/// <summary>
/// 名次(汉字)
/// </summary>
public string RankText { get; set; }
/// <summary>
/// 性别
/// </summary>
@ -250,13 +255,13 @@ public class ScoreImportDto
/// <summary>
/// 名次(数字)
/// </summary>
[ExcelColumnName("名次")]
[ExcelColumnName("名次(数字)")]
public int Rank { get; set; }
/// <summary>
/// 名次说明(文字描述,如"冠军"、"亚军"等)
/// 名次(汉字)
/// </summary>
[ExcelColumnName("名次说明")]
[ExcelColumnName("名次(汉字)")]
public string RankText { get; set; }
}

View File

@ -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;
/// </summary>
public class EventScoreService : BaseService<EventResult>, ITransient
{
private static readonly string[] ChineseNumbers = { "零", "一", "二", "三", "四", "五", "六", "七", "八", "九", "十" };
/// <summary>
/// 将数字名次转换为汉字形式
/// </summary>
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<EventRegistrationPerson> _personRepository;
private readonly ISqlSugarRepository<EventResult> _resultRepository;
private readonly ISqlSugarRepository<EventCertificate> _certificateRepository;
private readonly ISqlSugarRepository<EventRegistration> _registrationRepository;
private readonly ISqlSugarRepository<CertificateTemplate> _templateRepository;
private readonly ISqlSugarRepository<YD_Event.Core.Entities.Project> _projectRepository;
private readonly ISqlSugarRepository<TeamMember> _teamMemberRepository;
private readonly ISqlSugarRepository<EventInfo> _eventRepository;
private readonly ISqlSugarRepository<PointsRule> _pointsRuleRepository;
private readonly ISqlSugarRepository<PointsRuleDetail> _pointsRuleDetailRepository;
private readonly ISqlSugarRepository<PointsRecord> _pointsRecordRepository;
private readonly ISqlSugarRepository<YD_Event.Core.Entities.AppUser> _appUserRepository;
private readonly IIdGenerator _idGenerator;
private readonly ILogger<EventScoreService> _logger;
@ -28,6 +61,13 @@ public class EventScoreService : BaseService<EventResult>, ITransient
ISqlSugarRepository<EventCertificate> certificateRepository,
ISqlSugarRepository<EventRegistration> registrationRepository,
ISqlSugarRepository<CertificateTemplate> templateRepository,
ISqlSugarRepository<YD_Event.Core.Entities.Project> projectRepository,
ISqlSugarRepository<TeamMember> teamMemberRepository,
ISqlSugarRepository<EventInfo> eventRepository,
ISqlSugarRepository<PointsRule> pointsRuleRepository,
ISqlSugarRepository<PointsRuleDetail> pointsRuleDetailRepository,
ISqlSugarRepository<PointsRecord> pointsRecordRepository,
ISqlSugarRepository<YD_Event.Core.Entities.AppUser> appUserRepository,
IIdGenerator idGenerator,
ILogger<EventScoreService> logger) : base(resultRepository)
{
@ -36,6 +76,13 @@ public class EventScoreService : BaseService<EventResult>, 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<EventResult>, ITransient
[DisplayName("运动员成绩列表")]
public async Task<PageResult<ScoreListOutput>> PageAthlete(ScoreQueryInput input)
{
return await _personRepository.AsQueryable()
var pageResult = await _personRepository.AsQueryable()
.LeftJoin<EventRegistration>((p, r) => p.RegistrationId == r.Id)
.LeftJoin<EventResult>((p, r, res) => p.Id == res.RegistrationPersonId)
.LeftJoin<EventCertificate>((p, r, res, c) => res.Id == c.ResultId)
@ -68,32 +115,79 @@ public class EventScoreService : BaseService<EventResult>, ITransient
CertificateId = c.Id
})
.ToPagedListAsync(input);
foreach (var item in pageResult.Rows)
{
item.RankText = ConvertRankToChinese(item.Rank);
}
return pageResult;
}
/// <summary>
/// 教练/领队列表
/// </summary>
/// <remarks>
/// 通过运动员所在的团队关联获取教练和领队人员信息
/// </remarks>
[DisplayName("教练领队列表")]
public async Task<PageResult<CoachLeaderListOutput>> PageCoachLeader(CoachLeaderQueryInput input)
{
return await _personRepository.AsQueryable()
.LeftJoin<EventCertificate>((p, c) => p.Id == c.ParticipationRecordId)
.InnerJoin<EventRegistration>((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<EventRegistration>((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<CoachLeaderListOutput>();
}
// 步骤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<CoachLeaderListOutput>();
}
// 步骤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<CoachLeaderListOutput>();
}
// 步骤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;
}
/// <summary>
@ -142,7 +236,7 @@ public class EventScoreService : BaseService<EventResult>, ITransient
p.ProjectName,
p.GroupName,
p.RegistrationId,
p.ProjectId
p.ProjectId
})
.ToListAsync();
@ -152,6 +246,11 @@ public class EventScoreService : BaseService<EventResult>, 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<EventResult>, 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<EventResult>, 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<EventResult>, ITransient
/// <summary>
/// 查看证书接口
/// </summary>
/// <param name="id">报名人员ID (EventRegistrationPerson的Id)</param>
/// <param name="input">输入参数</param>
/// <returns>证书详情</returns>
[DisplayName("查看证书")]
[HttpGet("/api/EventScore/GetCertificateDetail/{id}")]
public async Task<IActionResult> GetCertificateDetail([FromRoute] long id)
public async Task<object> 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 };
}
/// <summary>
@ -442,31 +537,47 @@ public class EventScoreService : BaseService<EventResult>, ITransient
[HttpGet]
public IActionResult DownloadTemplate()
{
var headers = new string[] {
"赛事地址",
"项目",
"组别",
"姓名",
"联系方式",
"单位",
"成绩",
"名次",
"名次说明"
};
var value = new List<Dictionary<string, object>>
{
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"
};
}
/// <summary>
/// 成绩模板导出器
/// </summary>
private class ScoreTemplateExporter : ExcelExportBase
{
public override async Task<byte[]> 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();
}
}
/// <summary>
/// 导出证书
@ -565,4 +676,144 @@ public class EventScoreService : BaseService<EventResult>, ITransient
return result;
}
/// <summary>
/// 发放积分输入模型
/// </summary>
public class IssuePointsInput
{
public long EventId { get; set; }
}
/// <summary>
/// 发放积分
/// </summary>
[DisplayName("发放积分")]
[HttpPost("issuePoints")]
[UnitOfWork]
public async Task<object> 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<EventRegistrationPerson>((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<PointsRecord>();
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} 条已发放记录"
};
}
}

View File

@ -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
}
}
/// <summary>
/// 处理微信未支付订单:定期检查并更新订单状态
/// </summary>
private async Task ProcessUnpaidWeChatOrdersAsync(CancellationToken stoppingToken)
{
try
{
_logger.LogInformation("开始检查微信未支付订单");
using var scope = _serviceProvider.CreateScope();
var registrationRepository = scope.ServiceProvider.GetRequiredService<ISqlSugarRepository<EventRegistration>>();
var orderRepository = scope.ServiceProvider.GetRequiredService<ISqlSugarRepository<YD_Event.Core.Entities.Order>>();
var weChatPayConfigRepository = scope.ServiceProvider.GetRequiredService<ISqlSugarRepository<WeChatPayConfig>>();
var weChatPayService = scope.ServiceProvider.GetRequiredService<WeChatPayService>();
var paymentRepository = scope.ServiceProvider.GetRequiredService<ISqlSugarRepository<EventRegistrationPayment>>();
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, "检查微信未支付订单时发生错误");
}
}
/// <summary>
/// 处理微信支付成功的订单
/// </summary>
private async Task ProcessWeChatPaymentSuccessAsync(
EventRegistration registration,
YD_Event.Core.Entities.Order order,
WeChatPayQueryResult queryResult,
DateTime now,
ISqlSugarRepository<EventRegistration> registrationRepository,
ISqlSugarRepository<YD_Event.Core.Entities.Order> orderRepository,
ISqlSugarRepository<EventRegistrationPayment> 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<IIdGenerator>();
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);
}
/// <summary>
/// 处理微信支付关闭的订单
/// </summary>
private async Task ProcessWeChatPaymentClosedAsync(
EventRegistration registration,
YD_Event.Core.Entities.Order order,
DateTime now,
ISqlSugarRepository<EventRegistration> registrationRepository,
ISqlSugarRepository<YD_Event.Core.Entities.Order> 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);
}
/// <summary>
/// 处理微信退款中的订单
/// </summary>
private async Task ProcessRefundingWeChatOrdersAsync(CancellationToken stoppingToken)
{
try
{
_logger.LogInformation("开始检查微信退款中的订单");
using var scope = _serviceProvider.CreateScope();
var refundRecordRepository = scope.ServiceProvider.GetRequiredService<ISqlSugarRepository<RefundRecord>>();
var registrationRepository = scope.ServiceProvider.GetRequiredService<ISqlSugarRepository<EventRegistration>>();
var orderRepository = scope.ServiceProvider.GetRequiredService<ISqlSugarRepository<YD_Event.Core.Entities.Order>>();
var weChatPayConfigRepository = scope.ServiceProvider.GetRequiredService<ISqlSugarRepository<WeChatPayConfig>>();
var weChatPayService = scope.ServiceProvider.GetRequiredService<WeChatPayService>();
var paymentRepository = scope.ServiceProvider.GetRequiredService<ISqlSugarRepository<EventRegistrationPayment>>();
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, "检查微信退款订单时发生错误");
}
}
/// <summary>
/// 处理微信退款成功的订单
/// </summary>
private async Task ProcessWeChatRefundSuccessAsync(
RefundRecord refundRecord,
WeChatRefundQueryResult queryResult,
DateTime now,
ISqlSugarRepository<RefundRecord> refundRecordRepository,
ISqlSugarRepository<EventRegistration> registrationRepository,
ISqlSugarRepository<YD_Event.Core.Entities.Order> orderRepository,
ISqlSugarRepository<EventRegistrationPayment> 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);
}
/// <summary>
/// 处理微信退款失败的订单
/// </summary>
private async Task ProcessWeChatRefundFailedAsync(
RefundRecord refundRecord,
WeChatRefundQueryResult queryResult,
DateTime now,
ISqlSugarRepository<RefundRecord> 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();

View File

@ -5674,7 +5674,12 @@
</member>
<member name="P:YD_Event.Application.Event.Dtos.ScoreListOutput.Rank">
<summary>
名次
名次(数字)
</summary>
</member>
<member name="P:YD_Event.Application.Event.Dtos.ScoreListOutput.RankText">
<summary>
名次(汉字)
</summary>
</member>
<member name="P:YD_Event.Application.Event.Dtos.ScoreListOutput.Gender">
@ -5849,7 +5854,7 @@
</member>
<member name="P:YD_Event.Application.Event.Dtos.ScoreImportDto.RankText">
<summary>
名次说明(文字描述,如"冠军"、"亚军"等)
名次(汉字)
</summary>
</member>
<member name="T:YD_Event.Application.Event.Dtos.CertificateExportDto">
@ -7502,6 +7507,11 @@
成绩与证书管理
</summary>
</member>
<member name="M:YD_Event.Application.Event.EventScoreService.ConvertRankToChinese(System.Int32)">
<summary>
将数字名次转换为汉字形式
</summary>
</member>
<member name="M:YD_Event.Application.Event.EventScoreService.PageAthlete(YD_Event.Application.Event.Dtos.ScoreQueryInput)">
<summary>
运动员成绩列表
@ -7511,6 +7521,9 @@
<summary>
教练/领队列表
</summary>
<remarks>
通过运动员所在的团队关联获取教练和领队人员信息
</remarks>
</member>
<!-- Badly formed XML comment ignored for member "M:YD_Event.Application.Event.EventScoreService.ImportScore(System.Int64,Microsoft.AspNetCore.Http.IFormFile)" -->
<member name="M:YD_Event.Application.Event.EventScoreService.IssueCertificate(YD_Event.Application.Event.Dtos.IssueCertificateInput)">
@ -7518,11 +7531,11 @@
发放证书(支持模板选择和批量发放)
</summary>
</member>
<member name="M:YD_Event.Application.Event.EventScoreService.GetCertificateDetail(System.Int64)">
<member name="M:YD_Event.Application.Event.EventScoreService.GetCertificateDetail(YD_Event.Application.Certificate.Dtos.IdInput)">
<summary>
查看证书接口
</summary>
<param name="id">报名人员ID (EventRegistrationPerson的Id)</param>
<param name="input">输入参数</param>
<returns>证书详情</returns>
</member>
<member name="M:YD_Event.Application.Event.EventScoreService.VerifyCertificate(System.String)">
@ -7535,6 +7548,11 @@
下载模板
</summary>
</member>
<member name="T:YD_Event.Application.Event.EventScoreService.ScoreTemplateExporter">
<summary>
成绩模板导出器
</summary>
</member>
<member name="M:YD_Event.Application.Event.EventScoreService.ExportCertificate(YD_Event.Application.Event.Dtos.ScoreQueryInput)">
<summary>
导出证书
@ -7550,6 +7568,16 @@
验证导入数据
</summary>
</member>
<member name="T:YD_Event.Application.Event.EventScoreService.IssuePointsInput">
<summary>
发放积分输入模型
</summary>
</member>
<member name="M:YD_Event.Application.Event.EventScoreService.IssuePoints(YD_Event.Application.Event.EventScoreService.IssuePointsInput)">
<summary>
发放积分
</summary>
</member>
<member name="T:YD_Event.Application.Event.EventService">
<summary>
赛事管理服务
@ -7659,6 +7687,36 @@
支付超时处理后台服务
</summary>
</member>
<member name="M:YD_Event.Application.Event.PaymentExpirationBackgroundService.ProcessUnpaidWeChatOrdersAsync(System.Threading.CancellationToken)">
<summary>
处理微信未支付订单:定期检查并更新订单状态
</summary>
</member>
<member name="M:YD_Event.Application.Event.PaymentExpirationBackgroundService.ProcessWeChatPaymentSuccessAsync(YD_Event.Core.Entities.EventRegistration,YD_Event.Core.Entities.Order,YD_Event.Core.Infrastructure.WeChat.Dto.WeChatPayQueryResult,System.DateTime,SqlSugar.ISqlSugarRepository{YD_Event.Core.Entities.EventRegistration},SqlSugar.ISqlSugarRepository{YD_Event.Core.Entities.Order},SqlSugar.ISqlSugarRepository{YD_Event.Core.Entities.EventRegistrationPayment},System.Threading.CancellationToken)">
<summary>
处理微信支付成功的订单
</summary>
</member>
<member name="M:YD_Event.Application.Event.PaymentExpirationBackgroundService.ProcessWeChatPaymentClosedAsync(YD_Event.Core.Entities.EventRegistration,YD_Event.Core.Entities.Order,System.DateTime,SqlSugar.ISqlSugarRepository{YD_Event.Core.Entities.EventRegistration},SqlSugar.ISqlSugarRepository{YD_Event.Core.Entities.Order},System.Threading.CancellationToken)">
<summary>
处理微信支付关闭的订单
</summary>
</member>
<member name="M:YD_Event.Application.Event.PaymentExpirationBackgroundService.ProcessRefundingWeChatOrdersAsync(System.Threading.CancellationToken)">
<summary>
处理微信退款中的订单
</summary>
</member>
<member name="M:YD_Event.Application.Event.PaymentExpirationBackgroundService.ProcessWeChatRefundSuccessAsync(YD_Event.Core.Entities.RefundRecord,YD_Event.Core.Infrastructure.WeChat.Dto.WeChatRefundQueryResult,System.DateTime,SqlSugar.ISqlSugarRepository{YD_Event.Core.Entities.RefundRecord},SqlSugar.ISqlSugarRepository{YD_Event.Core.Entities.EventRegistration},SqlSugar.ISqlSugarRepository{YD_Event.Core.Entities.Order},SqlSugar.ISqlSugarRepository{YD_Event.Core.Entities.EventRegistrationPayment},System.Threading.CancellationToken)">
<summary>
处理微信退款成功的订单
</summary>
</member>
<member name="M:YD_Event.Application.Event.PaymentExpirationBackgroundService.ProcessWeChatRefundFailedAsync(YD_Event.Core.Entities.RefundRecord,YD_Event.Core.Infrastructure.WeChat.Dto.WeChatRefundQueryResult,System.DateTime,SqlSugar.ISqlSugarRepository{YD_Event.Core.Entities.RefundRecord},System.Threading.CancellationToken)">
<summary>
处理微信退款失败的订单
</summary>
</member>
<member name="T:YD_Event.Application.Event.UnifiedRegistrationService">
<summary>
统一报名处理服务