using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Logging;
using YD_Event.Application.Client.Dtos;
using YD_Event.Application.Event;
using YD_Event.Application.Event.Dtos;
using YD_Event.Core.Entities;
using YD_Event.Core.Enum;
using Easy.Core;
using YD_Event.Core.Infrastructure.WeChat;
using YD_Event.Core.Infrastructure.WeChat.Dto;
using YD_Event.Core.Options;
using YD_Event.Core.Domain.Pricing;
using YD_Event.Core.Domain.Pricing.Models;
using YD_Event.Core.Domain;
using SqlSugar;
using Furion.DatabaseAccessor;
using Microsoft.AspNetCore.Authorization;
using Furion.FriendlyException;
using System.Security.Claims;
using System.Text.Json;
using System.IO;
namespace YD_Event.Application.Client;
///
/// 小程序-赛事接口
///
[ApiDescriptionSettings("微信小程序接口")]
[Authorize]
public class EventClientController : IDynamicApiController
{
private readonly ISqlSugarRepository _eventRepository;
private readonly ISqlSugarRepository _projectRepository;
private readonly ISqlSugarRepository _eventProjectRepository;
private readonly ISqlSugarRepository _groupRepository;
private readonly ISqlSugarRepository _eventProjectGroupRepository;
private readonly ISqlSugarRepository _registrationRepository;
private readonly ISqlSugarRepository _registrationItemRepository;
private readonly ISqlSugarRepository _bannerRepository;
private readonly ISqlSugarRepository _unitRepository;
private readonly ISqlSugarRepository _personRepository;
private readonly ISqlSugarRepository _limitRepository;
private readonly ISqlSugarRepository _projectLimitRepository;
private readonly ISqlSugarRepository _feeConfigRepository;
private readonly ISqlSugarRepository _weChatPayConfigRepository;
private readonly ISqlSugarRepository _merchantRepository;
private readonly ISqlSugarRepository _pointsRuleRepository;
private readonly ISqlSugarRepository _pointsRuleDetailRepository;
private readonly ISqlSugarRepository _appUserRepository;
private readonly ISqlSugarRepository _provinceRepository;
private readonly ISqlSugarRepository _cityRepository;
private readonly ISqlSugarRepository _districtRepository;
private readonly ISqlSugarRepository _orderRepository;
private readonly ISqlSugarRepository _merchantCertificationRepository;
private readonly ISqlSugarRepository _paymentRepository;
private readonly ISqlSugarRepository _refundRecordRepository;
private readonly ISqlSugarRepository _refundConfigRepository;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IIdGenerator _idGenerator;
private readonly WeChatPayService _weChatPayService;
private readonly IOptionsMonitor _wechatTenpayClientOptionsMonitor;
private readonly ILogger _logger;
private readonly OrderNoGeneratorService _orderNoGeneratorService;
private readonly YD_Event.Application.Banner.BannerService _bannerService;
private readonly UnifiedRegistrationService _unifiedRegistrationService;
private readonly YD_Event.Application.Refund.RefundConfigService _refundConfigService;
public EventClientController(
ISqlSugarRepository eventRepository,
ISqlSugarRepository projectRepository,
ISqlSugarRepository eventProjectRepository,
ISqlSugarRepository groupRepository,
ISqlSugarRepository eventProjectGroupRepository,
ISqlSugarRepository registrationRepository,
ISqlSugarRepository registrationItemRepository,
ISqlSugarRepository bannerRepository,
ISqlSugarRepository unitRepository,
ISqlSugarRepository personRepository,
ISqlSugarRepository limitRepository,
ISqlSugarRepository projectLimitRepository,
ISqlSugarRepository feeConfigRepository,
ISqlSugarRepository weChatPayConfigRepository,
ISqlSugarRepository merchantRepository,
ISqlSugarRepository pointsRuleRepository,
ISqlSugarRepository pointsRuleDetailRepository,
ISqlSugarRepository appUserRepository,
ISqlSugarRepository provinceRepository,
ISqlSugarRepository cityRepository,
ISqlSugarRepository districtRepository,
ISqlSugarRepository orderRepository,
ISqlSugarRepository merchantCertificationRepository,
ISqlSugarRepository paymentRepository,
ISqlSugarRepository refundRecordRepository,
ISqlSugarRepository refundConfigRepository,
IHttpContextAccessor httpContextAccessor,
IIdGenerator idGenerator,
WeChatPayService weChatPayService,
IOptionsMonitor wechatTenpayClientOptionsMonitor,
ILogger logger,
OrderNoGeneratorService orderNoGeneratorService,
YD_Event.Application.Banner.BannerService bannerService,
UnifiedRegistrationService unifiedRegistrationService,
YD_Event.Application.Refund.RefundConfigService refundConfigService)
{
_eventRepository = eventRepository;
_projectRepository = projectRepository;
_eventProjectRepository = eventProjectRepository;
_groupRepository = groupRepository;
_eventProjectGroupRepository = eventProjectGroupRepository;
_registrationRepository = registrationRepository;
_registrationItemRepository = registrationItemRepository;
_bannerRepository = bannerRepository;
_unitRepository = unitRepository;
_personRepository = personRepository;
_limitRepository = limitRepository;
_projectLimitRepository = projectLimitRepository;
_feeConfigRepository = feeConfigRepository;
_weChatPayConfigRepository = weChatPayConfigRepository;
_merchantRepository = merchantRepository;
_pointsRuleRepository = pointsRuleRepository;
_pointsRuleDetailRepository = pointsRuleDetailRepository;
_appUserRepository = appUserRepository;
_provinceRepository = provinceRepository;
_cityRepository = cityRepository;
_districtRepository = districtRepository;
_orderRepository = orderRepository;
_merchantCertificationRepository = merchantCertificationRepository;
_paymentRepository = paymentRepository;
_refundRecordRepository = refundRecordRepository;
_refundConfigRepository = refundConfigRepository;
_httpContextAccessor = httpContextAccessor;
_idGenerator = idGenerator;
_weChatPayService = weChatPayService;
_wechatTenpayClientOptionsMonitor = wechatTenpayClientOptionsMonitor;
_logger = logger;
_orderNoGeneratorService = orderNoGeneratorService;
_bannerService = bannerService;
_unifiedRegistrationService = unifiedRegistrationService;
_refundConfigService = refundConfigService;
}
private long GetCurrentUserId()
{
var userIdStr = _httpContextAccessor.HttpContext?.User?.FindFirst(AuthClaimsConst.AuthIdKey)?.Value;
if (string.IsNullOrEmpty(userIdStr) || !long.TryParse(userIdStr, out long userId))
{
throw Oops.Bah("未登录或登录已失效");
}
return userId;
}
///
/// 获取轮播图列表
///
///
[HttpGet("banners")]
[AllowAnonymous]
[DisplayName("获取轮播图列表")]
public async Task> GetBannerList()
{
var list = await _bannerService.GetList();
return list.Select(b => new BannerDto
{
Title = b.Title,
ImageUrl = b.ImageUrl,
LinkType = b.LinkType,
LinkUrl = b.LinkUrl,
EventId = b.EventId
}).ToList();
}
///
/// 获取赛事列表
///
/// 查询参数
///
[HttpGet("list")]
[AllowAnonymous]
[DisplayName("获取赛事列表")]
public async Task> GetEventList([FromQuery] EventClientQueryInput input)
{
var query = _eventRepository.AsQueryable()
.Where(e => !e.DeleteMark && e.Status == AvailabilityStatus.Enable)
.WhereIF(!string.IsNullOrEmpty(input.Keyword), e => e.EventName.Contains(input.Keyword))
.WhereIF(input.EventType.HasValue, e => e.EventType == input.EventType)
.WhereIF(input.ChargeMode.HasValue, e => e.ChargeMode == input.ChargeMode)
.WhereIF(input.Status > 0, e => e.EventStatus == (EventStatus)input.Status);
query = query.OrderByDescending(e => e.CreatedTime);
var list = await query.ToPagedListAsync(input);
// 获取所有需要的省市区信息
var provinceIds = list.Rows.Where(e => e.ProvinceId.HasValue).Select(e => e.ProvinceId.Value).Distinct().ToList();
var cityIds = list.Rows.Where(e => e.CityId.HasValue).Select(e => e.CityId.Value).Distinct().ToList();
var districtIds = list.Rows.Where(e => e.DistrictId.HasValue).Select(e => e.DistrictId.Value).Distinct().ToList();
var provinces = provinceIds.Any()
? await _provinceRepository.AsQueryable().Where(p => provinceIds.Contains(p.Id) && !p.DeleteMark).ToListAsync()
: new List();
var cities = cityIds.Any()
? await _cityRepository.AsQueryable().Where(c => cityIds.Contains(c.Id) && !c.DeleteMark).ToListAsync()
: new List();
var districts = districtIds.Any()
? await _districtRepository.AsQueryable().Where(d => districtIds.Contains(d.Id) && !d.DeleteMark).ToListAsync()
: new List();
var result = new PageResult
{
PageNo = list.PageNo,
PageSize = list.PageSize,
Total = list.Total,
Pages = list.Pages,
Rows = list.Rows.Select(e =>
{
var provinceName = e.ProvinceId.HasValue
? provinces.FirstOrDefault(p => p.Id == e.ProvinceId.Value)?.ProvinceName ?? ""
: "";
var cityName = e.CityId.HasValue
? cities.FirstOrDefault(c => c.Id == e.CityId.Value)?.CityName ?? ""
: "";
var districtName = e.DistrictId.HasValue
? districts.FirstOrDefault(d => d.Id == e.DistrictId.Value)?.DistrictName ?? ""
: "";
return new EventListDto
{
Id = e.Id,
EventName = e.EventName,
CoverImage = e.CoverImage,
EventStatus = e.EventStatus,
RegistrationStartTime = e.RegistrationStartTime,
RegistrationEndTime = e.RegistrationEndTime,
MatchStartTime = e.MatchStartTime,
MatchEndTime = e.MatchEndTime,
Address = $"{provinceName}{cityName}{districtName}{e.DetailAddress}"
};
}).ToList()
};
return result;
}
///
/// 获取赛事详情
///
///
///
[HttpGet("{id}")]
[AllowAnonymous]
[DisplayName("获取赛事详情")]
public async Task GetEventDetail(long id)
{
var evt = await _eventRepository.GetFirstAsync(e => e.Id == id && !e.DeleteMark);
if (evt == null) throw Oops.Bah("赛事不存在");
bool isRegistered = false;
try
{
var userId = GetCurrentUserId();
isRegistered = await _registrationRepository.IsAnyAsync(r =>
r.EventId == id && r.CreatedUserId == userId && r.PaymentStatus != 4 && r.PaymentStatus == 1);
}
catch
{
isRegistered = false;
}
var feeConfig = await _feeConfigRepository.GetFirstAsync(f => f.EventId == id && !f.DeleteMark);
string provinceName = "";
string cityName = "";
string districtName = "";
if (evt.ProvinceId.HasValue && evt.ProvinceId > 0)
{
var province = await _provinceRepository.GetFirstAsync(p => p.Id == evt.ProvinceId.Value && !p.DeleteMark);
if (province != null) provinceName = province.ProvinceName;
}
if (evt.CityId.HasValue && evt.CityId > 0)
{
var city = await _cityRepository.GetFirstAsync(c => c.Id == evt.CityId.Value && !c.DeleteMark);
if (city != null) cityName = city.CityName;
}
if (evt.DistrictId.HasValue && evt.DistrictId > 0)
{
var district = await _districtRepository.GetFirstAsync(d => d.Id == evt.DistrictId.Value && !d.DeleteMark);
if (district != null) districtName = district.DistrictName;
}
PointsRuleDto? pointsRuleDetail = null;
if (evt.PointsRuleId.HasValue)
{
var pointsRuleEntity = await _pointsRuleRepository.GetFirstAsync(r => r.Id == evt.PointsRuleId.Value && !r.DeleteMark);
if (pointsRuleEntity != null)
{
var details = await _pointsRuleDetailRepository.AsQueryable()
.Where(d => d.PointsRuleId == pointsRuleEntity.Id && !d.DeleteMark)
.ToListAsync();
pointsRuleDetail = new PointsRuleDto
{
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()
};
}
}
var eventProjects = await _eventProjectRepository.AsQueryable()
.Where(ep => ep.EventId == id)
.ToListAsync();
var eventProjectIds = eventProjects.Select(ep => ep.Id).Distinct().ToList();
var projectIds = eventProjects.Select(ep => ep.ProjectId).Distinct().ToList();
var baseProjects = await _projectRepository.AsQueryable()
.Where(p => projectIds.Contains(p.Id) && !p.DeleteMark && p.Status == AvailabilityStatus.Enable)
.Select(p => new {
p.Id,
p.Name,
p.ProjectType
})
.ToListAsync();
var baseGroups = await _groupRepository.AsQueryable()
.Where(g => projectIds.Contains(g.ProjectId) && !g.DeleteMark)
.ToListAsync();
var eventProjectGroups = await _eventProjectGroupRepository.AsQueryable()
.Where(epg => eventProjectIds.Contains(epg.EventProjectId) && !epg.DeleteMark)
.ToListAsync();
var projectLimits = await _projectLimitRepository.AsQueryable()
.Where(pl => pl.EventId == id && !pl.DeleteMark)
.ToListAsync();
var units = await _unitRepository.AsQueryable()
.Where(u => u.EventId == id && !u.DeleteMark)
.OrderBy(u => u.Sort)
.ToListAsync();
var limit = await _limitRepository.GetFirstAsync(l => l.EventId == id && !l.DeleteMark);
string refundRule = "开赛前7天扣除金额5%,3-7天扣除金额10%,3天内扣除金额20%";
string pointsRule = "参与积分:5积分;名次积分:1-8名 15积分,9-20名 10积分";
// 优先从 RefundConfig 表读取配置并生成描述文本
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 ruleParts = new List();
foreach (var config in enabledConfigs)
{
if (config.DayEnd >= 999)
{
ruleParts.Add($"开赛前{config.DayStart}天及以上扣除金额{Math.Round(config.DeductPercentage)}%");
}
else
{
ruleParts.Add($"开赛前{config.DayStart}-{config.DayEnd}天扣除金额{Math.Round(config.DeductPercentage)}%");
}
}
refundRule = string.Join(",", ruleParts);
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "读取退款配置失败,赛事ID={EventId},使用默认规则", evt.Id);
}
// 如果有自定义配置,优先使用自定义配置(保持向后兼容)
if (!string.IsNullOrWhiteSpace(evt.CustomConfigJson))
{
try
{
var config = System.Text.Json.JsonSerializer.Deserialize>(evt.CustomConfigJson);
if (config != null)
{
if (config.TryGetValue("RefundRule", out var rule) && !string.IsNullOrWhiteSpace(rule))
{
refundRule = rule;
}
if (config.TryGetValue("PointsRule", out var pRule) && !string.IsNullOrWhiteSpace(pRule))
{
pointsRule = pRule;
}
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "解析赛事自定义配置失败,赛事ID={EventId}", evt.Id);
}
}
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)
{
feeConfigDto = new EventFeeConfigDto
{
ChargeMode = feeConfig.ChargeMode,
Price = feeConfig.Price,
MerchantId = evt.MerchantId
};
if (evt.MerchantId.HasValue && evt.MerchantId.Value > 0)
{
var merchant = await _merchantRepository.GetByIdAsync(evt.MerchantId.Value);
if (merchant != null)
{
feeConfigDto.MerchantName = merchant.MerchantName;
feeConfigDto.CreditCode = merchant.CreditCode;
feeConfigDto.BankName = merchant.BankName;
feeConfigDto.AccountNumber = merchant.AccountNumber;
feeConfigDto.BusinessLicenseUrl = merchant.BusinessLicenseUrl;
}
}
if (eventProjects.Any())
{
feeConfigDto.ProjectPrices = new Dictionary();
foreach (var ep in eventProjects)
{
feeConfigDto.ProjectPrices[ep.Id] = ep.Price / 100m;
}
}
}
var detail = new EventDetailDto
{
Id = evt.Id,
EventName = evt.EventName,
CoverImage = evt.CoverImage,
EventStatus = evt.EventStatus,
RegistrationStartTime = evt.RegistrationStartTime,
RegistrationEndTime = evt.RegistrationEndTime,
MatchStartTime = evt.MatchStartTime,
MatchEndTime = evt.MatchEndTime,
Address = $"{provinceName}{cityName}{districtName}{evt.DetailAddress}",
EventVideo = evt.EventVideo,
EventType = evt.EventType,
ChargeMode = evt.ChargeMode,
FeeConfig = feeConfigDto,
RegistrationType = evt.RegistrationType,
EventIntroduction = evt.EventIntroduction,
IsNeedMaterial = evt.IsNeedMaterial,
MaterialRemark = evt.MaterialRemark,
MaterialUploadStartTime = evt.MaterialUploadStartTime,
MaterialUploadEndTime = evt.MaterialUploadEndTime,
RefundRule = refundRule,
PointsRule = pointsRule,
PointsRuleDetail = pointsRuleDetail,
IsRegistered = isRegistered,
ProvinceName = provinceName,
CityName = cityName,
DistrictName = districtName,
IndividualProjectLimit = limit?.IndividualProjectLimit ?? 0,
TeamProjectLimit = limit?.TeamProjectLimit ?? 0,
Units = units.Select(u => new YD_Event.Application.Client.Dtos.EventUnitDto
{
UnitType = u.UnitType,
UnitName = u.UnitName,
LogoUrl = u.LogoUrl
}).ToList(),
Projects = eventProjects.Select(ep =>
{
var bp = baseProjects.FirstOrDefault(b => b.Id == ep.ProjectId);
var pl = projectLimits.FirstOrDefault(x => x.ProjectId == ep.Id);
var epGroups = eventProjectGroups.Where(g => g.EventProjectId == ep.Id).ToList();
return new EventProjectDto
{
Id = ep.Id,
Name = bp?.Name ?? "",
TypeName = bp?.ProjectType.Description() ?? "",
ProjectType = (long)(bp?.ProjectType ?? 0),
Fee = ep.Price / 100m,
ProjectLimitCount = pl?.LimitCount,
Groups = epGroups.Select(eg =>
{
var baseGroup = baseGroups.FirstOrDefault(bg => bg.Id == eg.ProjectGroupId);
var groupName = eg.GroupName ?? baseGroup?.GroupName ?? "";
var birthDateStart = eg.BirthDateStart != DateTime.MinValue ? eg.BirthDateStart : (baseGroup?.BirthDateStart ?? DateTime.MinValue);
var birthDateEnd = eg.BirthDateEnd != DateTime.MinValue ? eg.BirthDateEnd : (baseGroup?.BirthDateEnd ?? DateTime.MinValue);
var gender = eg.Gender;
return new EventProjectGroupDto
{
Id = eg.Id,
GroupName = groupName,
GenderRequirement = gender == Gender.Male ? "男" : (gender == Gender.Female ? "女" : "不限"),
AgeRequirement = $"{birthDateStart:yyyy-MM-dd} 至 {birthDateEnd:yyyy-MM-dd}"
};
}).ToList()
};
}).Where(x => !string.IsNullOrEmpty(x.Name)).ToList()
};
return detail;
}
///
/// 计算报名金额
///
///
///
[HttpPost("calculate-amount")]
[DisplayName("计算报名金额")]
public async Task CalculateRegistrationAmount([FromBody] CalculateAmountInput input)
{
var inputItems = input.Items.Select(i => new UnifiedRegistrationService.CalculateAmountInputItem
{
ProjectId = i.ProjectId,
PersonCount = i.PersonCount
}).ToList();
var (totalAmount, chargeMode, itemDetails) = await _unifiedRegistrationService.CalculateRegistrationAmountAsync(input.EventId, inputItems);
var result = new CalculateAmountOutput
{
ChargeMode = chargeMode,
TotalAmount = totalAmount,
ItemDetails = itemDetails.Select(d => new CalculateAmountItemOutput
{
ProjectId = d.projectId,
ProjectName = d.projectName,
UnitPrice = d.unitPrice,
PersonCount = d.personCount,
Subtotal = d.subtotal
}).ToList()
};
return result;
}
///
/// 提交赛事报名
///
///
///
[HttpPost("apply")]
[UnitOfWork]
[DisplayName("提交赛事报名")]
public async Task SubmitRegistration([FromBody] SubmitRegistrationInput input)
{
var userId = GetCurrentUserId();
// 0. 校验报名模式
if (input.RegistrationMode == 2)
throw Oops.Bah("团队报名请前往网页端进行操作");
// 校验重复报名 - 如果已有未取消的报名记录,直接返回现有记录ID
var existingRegistration = await _registrationRepository.GetFirstAsync(
r => r.EventId == input.EventId && r.CreatedUserId == userId && r.PaymentStatus != 4); // 4=已取消
if (existingRegistration != null)
{
// 已有未取消的报名记录,直接返回现有记录ID,让前端跳转到支付页面
return existingRegistration.Id;
}
// 获取当前用户信息(用于个人报名时自动填充)
var appUser = await _appUserRepository.GetFirstAsync(u => u.Id == userId && !u.DeleteMark);
if (appUser == null && input.RegistrationMode == 1 && appUser.AuditStatus != AuditStatus.Pass)
throw Oops.Bah("请先完成身份认证");
// 个人报名模式下自动填充联系人信息
if (input.RegistrationMode == 1)
{
if (string.IsNullOrWhiteSpace(input.ContactName))
input.ContactName = appUser?.Name;
if (string.IsNullOrWhiteSpace(input.ContactPhone))
input.ContactPhone = appUser?.Mobile;
// 验证必填联系人信息
if (string.IsNullOrWhiteSpace(input.ContactName))
throw Oops.Bah("联系人姓名不能为空,请先完善身份认证信息");
if (string.IsNullOrWhiteSpace(input.ContactPhone))
throw Oops.Bah("联系人电话不能为空,请先完善身份认证信息");
}
// 个人报名模式下自动填充参赛人员信息
if (input.RegistrationMode == 1 && input.Items != null)
{
foreach (var item in input.Items)
{
if (item.Persons == null || !item.Persons.Any())
{
if (appUser == null)
throw Oops.Bah("请先完成身份认证");
item.Persons = new List
{
new PersonInput
{
UserId = appUser.Id,
Name = appUser.Name ?? "",
Gender = appUser.Gender,
IdCardNumber = appUser.IdCardNumber ?? "",
BirthDate = appUser.BirthDate ?? DateTime.Now,
Mobile = appUser.Mobile
}
};
}
}
}
// 检查所有参赛人员是否都有UserId
if (input.Items != null)
{
foreach (var item in input.Items)
{
if (item.Persons != null)
{
foreach (var p in item.Persons)
{
if (!p.UserId.HasValue || p.UserId.Value <= 0)
{
throw Oops.Bah($"参赛人员 {p.Name} 的用户ID不能为空");
}
}
}
}
}
// 转换为统一输入模型
var unifiedInput = new UnifiedRegistrationInput
{
EventId = input.EventId,
RegistrationMode = input.RegistrationMode,
TeamName = input.TeamName,
ContactName = input.ContactName ?? "",
ContactPhone = input.ContactPhone ?? "",
CreatedUserId = userId,
IsDraft = false,
RegistrationChannel = 1, // 1-小程序渠道
Items = input.Items?.Select(item => new UnifiedRegistrationItemInput
{
ProjectId = item.ProjectId,
GroupId = item.GroupId,
GroupName = item.GroupName,
Persons = item.Persons?.Select(p => new UnifiedPersonInput
{
UserId = p.UserId,
Name = p.Name,
Gender = p.Gender,
IdCardNumber = p.IdCardNumber,
BirthDate = p.BirthDate,
Mobile = p.Mobile
}).ToList() ?? new List()
}).ToList() ?? new List()
};
// 调用统一报名服务
var result = await _unifiedRegistrationService.ProcessRegistrationAsync(unifiedInput);
return result.RegistrationId;
}
///
/// 验证优惠券
///
[HttpPost("coupon/validate")]
[DisplayName("验证优惠券")]
public async Task ValidateCoupon([FromBody] ValidateCouponInput input)
{
var userId = GetCurrentUserId();
return await _unifiedRegistrationService.ValidateAndUseCouponAsync(input, userId);
}
///
/// 获取支付信息
///
///
/// 用于获取赛事报名的支付信息,包括微信JSAPI支付参数
/// OpenId 参数必须通过请求体(JSON)传入,格式验证由DTO自动处理
///
/// 支付信息查询输入参数,包含报名记录ID和OpenId
/// 支付信息,包括微信支付参数(如果是微信支付)
[HttpPost("payment-info")]
[DisplayName("获取支付信息")]
public async Task GetPaymentInfo([FromBody] GetPaymentInfoInput input)
{
var userId = GetCurrentUserId();
// 验证报名记录归属权
var reg = await _registrationRepository.GetFirstAsync(r => r.Id == input.RegistrationId && r.CreatedUserId == userId);
if (reg == null) throw Oops.Bah("报名记录不存在");
var evt = await _eventRepository.GetFirstAsync(e => e.Id == reg.EventId);
if (evt == null) throw Oops.Bah("赛事不存在");
var merchant = await _merchantRepository.GetFirstAsync(m => m.Id == evt.MerchantId && !m.DeleteMark);
// 如果支付方式已经设置为微信支付或免费,直接调用统一支付服务
int paymentMethod = reg.PaymentMethod == 0 ? 2 : reg.PaymentMethod;
// 构建统一支付输入
var unifiedInput = new UnifiedPaymentInput
{
RegistrationId = input.RegistrationId,
PaymentMethod = paymentMethod,
PaymentMethodType = reg.PaymentMethodType,
CouponId = input.CouponId,
OpenId = input.OpenId,
PaymentProofUrl = null,
OperatorUserId = userId
};
// 调用统一支付服务
var unifiedResult = await _unifiedRegistrationService.ProcessPaymentAsync(unifiedInput);
// 构建返回结果
var paymentInfo = new PaymentInfoDto
{
RegistrationId = unifiedResult.RegistrationId,
Amount = unifiedResult.ActualAmount,
OriginalAmount = unifiedResult.OriginalAmount,
DiscountAmount = unifiedResult.DiscountAmount,
PaymentMethod = unifiedResult.PaymentStatus == 1 ? 0 : paymentMethod, // 如果已支付,不需要显示支付方式
BankAccountName = merchant?.MerchantName,
BankAccountNo = merchant?.AccountNumber,
BankName = merchant?.BankName,
WechatPayParams = unifiedResult.WechatPayParams
};
return paymentInfo;
}
///
/// 根据赛事ID精确获取对应的微信支付配置
///
///
/// 1. 通过赛事信息中的 MerchantId 查找对应的微信支付配置
/// 2. 必须确保赛事已关联商户配置,不使用兜底策略
/// 3. 只返回启用状态且未删除的配置
///
/// 赛事信息,必须包含有效的 MerchantId
/// 微信支付配置,如果未找到则返回 null
/// 当赛事没有关联商户配置时抛出
private async Task GetWeChatPayConfigByEventAsync(EventInfo eventInfo)
{
if (!eventInfo.MerchantId.HasValue)
{
_logger.LogError("赛事未关联商户配置,赛事ID={EventId},赛事名称={EventName}",
eventInfo.Id, eventInfo.EventName);
throw new ArgumentException($"赛事[{eventInfo.EventName}]未关联商户配置,请联系管理员配置", nameof(eventInfo));
}
var payConfig = await _weChatPayConfigRepository.GetFirstAsync(x =>
x.MerchantId == eventInfo.MerchantId.Value &&
x.Status == AvailabilityStatus.Enable &&
!x.DeleteMark);
if (payConfig == null)
{
_logger.LogError("未找到赛事关联的商户支付配置,赛事ID={EventId},商户ID={MerchantId}",
eventInfo.Id, eventInfo.MerchantId);
}
return payConfig;
}
///
/// 上传支付凭证(线下支付)
///
[HttpPost("upload-voucher/{registrationId}")]
[DisplayName("上传支付凭证")]
[UnitOfWork]
public async Task UploadPaymentVoucher(long registrationId, [FromBody] YD_Event.Application.Client.Dtos.UploadVoucherInput input)
{
var userId = GetCurrentUserId();
// 验证报名记录归属权
var reg = await _registrationRepository.GetFirstAsync(r => r.Id == registrationId && r.CreatedUserId == userId);
if (reg == null) throw Oops.Bah("报名记录不存在");
// 构建统一支付输入
var unifiedInput = new UnifiedPaymentInput
{
RegistrationId = registrationId,
PaymentMethod = 1, // 线下支付
CouponId = null,
OpenId = null,
PaymentProofUrl = input.VoucherUrl,
OperatorUserId = userId
};
// 调用统一支付服务
await _unifiedRegistrationService.ProcessPaymentAsync(unifiedInput);
return true;
}
///
/// 更新参赛人信息
///
[HttpPost("update-player")]
[DisplayName("更新参赛人信息")]
public async Task UpdateRegistrationPlayer([FromBody] UpdatePlayerInput input)
{
var userId = GetCurrentUserId();
var person = await _personRepository.GetFirstAsync(p => p.Id == input.PersonId);
if (person == null) throw Oops.Bah("参赛人不存在");
// 校验权限:确保是该用户创建的报名
var reg = await _registrationRepository.GetFirstAsync(r => r.Id == person.RegistrationId);
if (reg == null || reg.CreatedUserId != userId) throw Oops.Bah("无权操作");
person.Name = input.Name;
person.IdCard = input.IdCardNumber;
person.Gender = input.Gender;
person.Phone = input.Mobile;
person.UpdatedTime = DateTime.Now;
person.UpdatedUserId = userId;
return await _personRepository.UpdateAsync(person);
}
///
/// 获取我的已报赛事列表
///
///
[HttpGet("my-events")]
[DisplayName("获取我的已报赛事列表")]
public async Task> GetMyEventList([FromQuery] int page = 1, [FromQuery] int pageSize = 10)
{
var userId = GetCurrentUserId();
var query = _registrationRepository.AsQueryable()
.LeftJoin((r, e) => r.EventId == e.Id)
.Where(r => r.CreatedUserId == userId && !r.DeleteMark && r.PaymentStatus == 1)
.OrderByDescending(r => r.RegistrationTime)
.Select((r, e) => new MyEventListDto
{
RegistrationId = r.Id,
EventId = r.EventId,
EventName = e.EventName,
CoverImage = e.CoverImage,
PaymentStatus = r.PaymentStatus,
RegistrationTime = r.RegistrationTime,
ProjectCount = SqlFunc.Subqueryable().Where(i => i.RegistrationId == r.Id).Count(),
IsNeedMaterial = e.IsNeedMaterial,
MaterialRemark = e.MaterialRemark,
MaterialUploadStartTime = e.MaterialUploadStartTime,
MaterialUploadEndTime = e.MaterialUploadEndTime,
MaterialUrl = r.MaterialUrl
});
return await query.ToPagedListAsync(new Pagination { PageNo = page, PageSize = pageSize });
}
///
/// 获取报名详情(包含参赛人)
///
[HttpGet("my-registration/{registrationId}")]
[DisplayName("获取报名详情")]
public async Task GetRegistrationDetail(long registrationId)
{
var userId = GetCurrentUserId();
var reg = await _registrationRepository.AsQueryable()
.LeftJoin((r, e) => r.EventId == e.Id)
.Where(r => r.Id == registrationId && r.CreatedUserId == userId)
.Select((r, e) => new {
r.Id,
r.EventId,
e.EventName,
e.CoverImage,
r.PaymentStatus,
r.TotalAmount,
r.OrderNo,
r.RegistrationTime,
r.ReviewRemark,
// 为了计算 EventStatus
RegStart = e.RegistrationStartTime,
RegEnd = e.RegistrationEndTime,
MatchStart = e.MatchStartTime,
MatchEnd = e.MatchEndTime
})
.FirstAsync();
if (reg == null) throw Oops.Bah("报名记录不存在");
// 计算 EventStatus
EventStatus status = EventStatus.Enrolling;
var now = DateTime.Now;
if (now >= reg.RegStart && now <= reg.RegEnd) status = EventStatus.Enrolling;
else if (now >= reg.MatchStart && now <= reg.MatchEnd) status = EventStatus.Ongoing;
else if (now > reg.MatchEnd) status = EventStatus.Ended;
// 获取 Items 和 Persons
var items = await _registrationItemRepository.AsQueryable()
.Where(i => i.RegistrationId == registrationId)
.ToListAsync();
var persons = await _personRepository.AsQueryable()
.Where(p => p.RegistrationId == registrationId)
.ToListAsync();
return new RegistrationDetailDto
{
RegistrationId = reg.Id,
EventId = reg.EventId,
EventName = reg.EventName,
CoverImage = reg.CoverImage,
EventStatus = status,
PaymentStatus = reg.PaymentStatus,
TotalAmount = reg.TotalAmount,
OrderNo = reg.OrderNo,
RegistrationTime = reg.RegistrationTime,
ReviewRemark = reg.ReviewRemark,
Items = items.Select(i => new RegistrationItemDto
{
Id = i.Id,
ProjectName = i.ProjectName,
GroupName = i.GroupName,
Price = i.UnitPrice, // 单价
Persons = persons.Where(p => p.RegistrationItemId == i.Id).Select(p => new YD_Event.Application.Client.Dtos.RegistrationPersonDto
{
Id = p.Id,
Name = p.Name,
IdCardNumber = p.IdCard,
Gender = p.Gender,
Mobile = p.Phone
}).ToList()
}).ToList()
};
}
///
/// 查询报名状态
///
///
///
[HttpGet("status/{registrationId}")]
[DisplayName("查询报名状态")]
public async Task GetRegistrationStatus(long registrationId)
{
var userId = GetCurrentUserId();
var reg = await _registrationRepository.GetFirstAsync(r => r.Id == registrationId && r.CreatedUserId == userId);
if (reg == null) throw Oops.Bah("报名记录不存在");
return new RegistrationStatusDto
{
RegistrationId = reg.Id,
PaymentStatus = reg.PaymentStatus,
RejectReason = reg.ReviewRemark
};
}
///
/// 取消报名
///
///
///
[HttpPost("cancel/{registrationId}")]
[DisplayName("取消报名")]
[UnitOfWork]
public async Task CancelRegistration(long registrationId)
{
var userId = GetCurrentUserId();
return await _unifiedRegistrationService.CancelRegistrationAsync(registrationId, userId);
}
///
/// 选择支付方式
///
///
/// 用于选择赛事报名的支付方式
/// - 选择微信支付时,必须提供有效的 OpenId
/// - OpenId 的格式验证由 DTO 自动处理
///
/// 选择支付方式的输入参数
/// 操作是否成功
[HttpPost("select-payment-method")]
[UnitOfWork]
[DisplayName("选择支付方式")]
public async Task SelectPaymentMethod([FromBody] SelectPaymentMethodInput input)
{
var userId = GetCurrentUserId();
var reg = await _registrationRepository.GetFirstAsync(r => r.Id == input.RegistrationId && r.CreatedUserId == userId);
if (reg == null) throw Oops.Bah("报名记录不存在");
if (reg.PaymentStatus == 1) throw Oops.Bah("订单已支付,无法修改支付方式");
if (reg.PaymentStatus == 4) throw Oops.Bah("订单已取消");
// 如果选择微信支付,验证 OpenId 是否提供
if (input.PaymentMethod == 2)
{
if (string.IsNullOrWhiteSpace(input.OpenId))
{
_logger.LogWarning("选择微信支付时未提供 OpenId,报名ID={RegistrationId}", input.RegistrationId);
throw Oops.Bah("选择微信支付时必须提供 OpenId");
}
}
// 构建统一支付输入
var unifiedInput = new UnifiedPaymentInput
{
RegistrationId = input.RegistrationId,
PaymentMethod = input.PaymentMethod,
CouponId = null,
OpenId = input.OpenId,
PaymentProofUrl = null,
OperatorUserId = userId
};
// 调用统一支付服务(仅设置支付方式,不处理实际支付)
// 处理支付方式转换逻辑
if (input.PaymentMethodType == PaymentMethodType.WeChatPay ||
input.PaymentMethodType == PaymentMethodType.AliPay)
{
reg.PaymentMethod = 2; // 线上支付
}
else if (input.PaymentMethodType == PaymentMethodType.BankTransfer)
{
reg.PaymentMethod = 1; // 线下支付
}
else
{
// 如果没有指定具体支付方式,使用输入的支付方式
reg.PaymentMethod = input.PaymentMethod;
}
// 设置具体支付方式
reg.PaymentMethodType = input.PaymentMethodType;
// 线下支付订单状态设置为待审核
if (reg.PaymentMethod == 1)
{
reg.PaymentStatus = 0; // 待审核
}
reg.UpdatedTime = DateTime.Now;
reg.UpdatedUserId = userId;
_logger.LogInformation("成功修改支付方式,报名ID={RegistrationId},支付方式={PaymentMethod}",
input.RegistrationId, input.PaymentMethod);
return await _registrationRepository.UpdateAsync(reg);
}
///
/// 同步支付状态(主动查询微信支付)
///
[HttpPost("sync-payment/{registrationId}")]
[UnitOfWork]
[DisplayName("同步支付状态")]
public async Task SyncPaymentStatus(long registrationId)
{
var userId = GetCurrentUserId();
return await _unifiedRegistrationService.QueryAndSyncWeChatPaymentStatusAsync(registrationId, userId);
}
///
/// 微信支付回调通知处理
///
///
/// 处理流程:
/// 1. 接收微信支付平台回调通知
/// 2. 使用平台级别的支付配置验证签名和解密数据(微信平台证书是平台级别)
/// 3. 获取订单号后查找报名记录
/// 4. 根据报名记录获取赛事信息
/// 5. 根据赛事信息获取商户级别的支付配置(用于后续可能的退款等操作)
/// 6. 更新订单支付状态
///
/// 微信支付平台要求的响应格式
[HttpPost("wechat-pay-notify")]
[AllowAnonymous]
[UnitOfWork]
[DisplayName("微信支付回调通知")]
public async Task WeChatPayNotify()
{
try
{
using var reader = new StreamReader(_httpContextAccessor.HttpContext.Request.Body);
var body = await reader.ReadToEndAsync();
var headers = _httpContextAccessor.HttpContext.Request.Headers;
var timestamp = headers["Wechatpay-Timestamp"].FirstOrDefault();
var nonce = headers["Wechatpay-Nonce"].FirstOrDefault();
var signature = headers["Wechatpay-Signature"].FirstOrDefault();
var serialNumber = headers["Wechatpay-Serial"].FirstOrDefault();
if (string.IsNullOrEmpty(timestamp) || string.IsNullOrEmpty(nonce) ||
string.IsNullOrEmpty(signature) || string.IsNullOrEmpty(serialNumber))
{
_logger.LogWarning("微信支付回调通知参数不完整");
return new JsonResult(new { code = "FAIL", message = "参数不完整" });
}
// 第一步:使用平台级别的支付配置验证签名和解密数据
// 注意:微信支付签名验证使用的是微信平台证书(平台级别),所有商户共用
var platformPayConfig = await _weChatPayConfigRepository.GetFirstAsync(x =>
x.Status == AvailabilityStatus.Enable && !x.DeleteMark);
if (platformPayConfig == null)
{
_logger.LogError("未找到平台级别的微信支付配置");
return new JsonResult(new { code = "FAIL", message = "支付配置未找到" });
}
var notifyRequest = new WeChatPayNotifyRequest
{
Timestamp = timestamp,
Nonce = nonce,
Signature = signature,
SerialNumber = serialNumber,
Body = body
};
// 验证签名
var isValid = await _weChatPayService.VerifyPayNotifyAsync(platformPayConfig, notifyRequest);
if (!isValid)
{
_logger.LogWarning("微信支付回调签名验证失败");
return new JsonResult(new { code = "FAIL", message = "签名验证失败" });
}
// 解密通知数据
var notifyData = await _weChatPayService.DecryptPayNotifyDataAsync(platformPayConfig, notifyRequest);
if (notifyData == null || notifyData.TradeState != "SUCCESS")
{
_logger.LogWarning("微信支付回调数据无效或交易未成功");
return new JsonResult(new { code = "SUCCESS", message = "成功" });
}
// 根据商户订单号查找报名记录
var reg = await _registrationRepository.GetFirstAsync(r => r.OrderNo == notifyData.OutTradeNo);
if (reg == null)
{
_logger.LogWarning($"未找到订单:{notifyData.OutTradeNo}");
return new JsonResult(new { code = "SUCCESS", message = "成功" });
}
if (reg.PaymentStatus == 1)
{
_logger.LogInformation($"订单 {reg.OrderNo} 已处理过支付回调");
return new JsonResult(new { code = "SUCCESS", message = "成功" });
}
// 第二步:获取赛事信息并验证商户配置
var evt = await _eventRepository.GetFirstAsync(e => e.Id == reg.EventId);
if (evt == null)
{
_logger.LogError($"未找到赛事信息,订单号:{reg.OrderNo},赛事ID:{reg.EventId}");
return new JsonResult(new { code = "FAIL", message = "赛事信息不存在" });
}
// 验证赛事是否关联了有效的商户支付配置
var merchantPayConfig = await GetWeChatPayConfigByEventAsync(evt);
if (merchantPayConfig == null)
{
_logger.LogError("赛事未配置有效的商户支付配置,订单号={OrderNo},赛事ID={EventId}",
reg.OrderNo, evt.Id);
return new JsonResult(new { code = "FAIL", message = "商户支付配置异常" });
}
_logger.LogInformation("订单支付成功,订单号={OrderNo},赛事ID={EventId},商户ID={MerchantId}",
reg.OrderNo, evt.Id, evt.MerchantId);
// 使用统一的支付成功处理方法
await _unifiedRegistrationService.HandlePaymentSuccessAsync(
reg.OrderNo,
notifyData.TransactionId,
notifyData.SuccessTime?.DateTime,
reg.CreatedUserId);
return new JsonResult(new { code = "SUCCESS", message = "成功" });
}
catch (ArgumentException ex)
{
_logger.LogError(ex, "处理微信支付回调通知时参数验证失败");
return new JsonResult(new { code = "FAIL", message = "业务验证失败" });
}
catch (Exception ex)
{
_logger.LogError(ex, "处理微信支付回调通知异常");
return new JsonResult(new { code = "FAIL", message = "处理失败" });
}
}
///
/// 从请求中获取 OpenId 参数
///
///
/// 支持从以下来源获取 OpenId(按优先级顺序):
/// 1. 查询字符串 (QueryString):openId 或 openid 参数
/// 2. 表单数据 (FormData):openId 或 openid 字段
/// 3. 请求体 (JSON):openId 或 openid 属性
/// 4. 用户 Claims(兜底方案):openid claim
///
/// 获取到的 OpenId 值,如果未找到则返回空字符串
private string GetOpenIdFromRequest()
{
var httpContext = _httpContextAccessor.HttpContext;
if (httpContext == null)
{
_logger.LogWarning("无法获取 HttpContext,OpenId 获取失败");
return string.Empty;
}
// 1. 尝试从查询字符串获取
var openId = httpContext.Request.Query["openId"].FirstOrDefault() ??
httpContext.Request.Query["openid"].FirstOrDefault();
if (!string.IsNullOrEmpty(openId))
{
_logger.LogDebug("从查询字符串获取到 OpenId");
return openId;
}
// 2. 尝试从表单数据获取
if (httpContext.Request.HasFormContentType)
{
openId = httpContext.Request.Form["openId"].FirstOrDefault() ??
httpContext.Request.Form["openid"].FirstOrDefault();
if (!string.IsNullOrEmpty(openId))
{
_logger.LogDebug("从表单数据获取到 OpenId");
return openId;
}
}
// 3. 尝试从请求体 JSON 中获取(需要先读取)
// 注意:为了避免多次读取请求体,这里暂时不实现
// 如果需要从 JSON 中获取,可以在具体接口的 DTO 中添加 openId 字段
// 4. 兜底方案:从用户 Claims 中获取
openId = httpContext.User?.FindFirst("openid")?.Value;
if (!string.IsNullOrEmpty(openId))
{
_logger.LogDebug("从用户 Claims 中获取到 OpenId");
return openId;
}
_logger.LogWarning("未在请求中找到 OpenId 参数");
return string.Empty;
}
///
/// 验证 OpenId 参数的有效性
///
///
/// 验证规则:
/// 1. OpenId 不能为空或空白字符串
/// 2. OpenId 长度必须在合理范围内(微信 OpenId 通常为 28 个字符左右)
/// 3. OpenId 只能包含字母、数字和下划线
///
/// 待验证的 OpenId
/// 验证失败时的错误信息
/// 验证通过返回 true,否则返回 false
private bool ValidateOpenId(string openId, out string errorMessage)
{
errorMessage = string.Empty;
// 验证是否为空
if (string.IsNullOrWhiteSpace(openId))
{
errorMessage = "OpenId 参数不能为空";
_logger.LogWarning("OpenId 验证失败:参数为空");
return false;
}
// 验证长度(微信 OpenId 通常为 28 个字符,允许一定的波动范围)
if (openId.Length < 10 || openId.Length > 50)
{
errorMessage = $"OpenId 参数长度不合法,当前长度:{openId.Length}";
_logger.LogWarning("OpenId 验证失败:长度不合法,长度={Length}", openId.Length);
return false;
}
// 验证格式(只能包含字母、数字和下划线)
foreach (char c in openId)
{
if (!char.IsLetterOrDigit(c) && c != '_' && c != '-')
{
errorMessage = "OpenId 参数格式不合法,只能包含字母、数字、下划线和连字符";
_logger.LogWarning("OpenId 验证失败:格式不合法,OpenId={OpenId}", openId);
return false;
}
}
_logger.LogDebug("OpenId 验证通过");
return true;
}
private EventStatus CalculateEventStatus(EventInfo e)
{
var now = DateTime.Now;
if (now < e.RegistrationStartTime) return EventStatus.NotStarted; // 未开始
if (now >= e.RegistrationStartTime && now <= e.RegistrationEndTime) return EventStatus.Enrolling; // 报名中
if (now > e.RegistrationEndTime && now < e.MatchStartTime) return EventStatus.RegistrationEnded; // 报名结束
if (now >= e.MatchStartTime && now <= e.MatchEndTime) return EventStatus.Ongoing; // 比赛中
if (now > e.MatchEndTime) return EventStatus.Ended; // 已结束
return EventStatus.Enrolling; // 默认返回报名中
}
///
/// 上传赛事素材
///
/// 上传素材输入参数
/// 操作是否成功
[HttpPost("upload-material")]
[DisplayName("上传赛事素材")]
public async Task UploadMaterial([FromBody] YD_Event.Application.Client.Dtos.UploadMaterialInput input)
{
var userId = GetCurrentUserId();
var registration = await _registrationRepository.GetFirstAsync(r => r.Id == input.RegistrationId && !r.DeleteMark);
if (registration == null) throw Oops.Bah("报名记录不存在");
if (registration.CreatedUserId != userId) throw Oops.Bah("无权操作此报名记录");
if (registration.PaymentStatus == 4) throw Oops.Bah("报名已取消,无法上传素材");
var evt = await _eventRepository.GetFirstAsync(e => e.Id == registration.EventId && !e.DeleteMark);
if (evt == null) throw Oops.Bah("赛事不存在");
if (evt.IsNeedMaterial == false) throw Oops.Bah("此赛事不需要上传素材");
if (evt.MaterialUploadStartTime.HasValue && DateTime.Now < evt.MaterialUploadStartTime.Value)
{
throw Oops.Bah("素材上传尚未开始");
}
if (evt.MaterialUploadEndTime.HasValue && DateTime.Now > evt.MaterialUploadEndTime.Value)
{
throw Oops.Bah("素材上传已结束");
}
registration.MaterialUrl = input.MaterialUrl;
registration.UpdatedTime = DateTime.Now;
registration.UpdatedUserId = userId;
return await _registrationRepository.UpdateAsync(registration);
}
///
/// 取消报名并申请退款
///
[HttpPost("cancel-registration-refund")]
[UnitOfWork]
[DisplayName("取消报名退款")]
public async Task CancelRegistrationRefund([FromBody] CancelRegistrationRefundInput input)
{
var userId = GetCurrentUserId();
// 验证报名记录
var registration = await _registrationRepository.GetByIdAsync(input.RegistrationId);
if (registration == null) throw Oops.Bah("报名记录不存在");
if (registration.CreatedUserId != userId) throw Oops.Bah("无权操作此报名记录");
if (registration.PaymentStatus != 1) throw Oops.Bah("只有已支付的报名才能申请退款");
// 获取订单信息
var order = await _orderRepository.GetFirstAsync(x => x.RegistrationId == input.RegistrationId && x.OrderType == 1);
if (order == null) throw Oops.Bah("订单不存在");
if (order.OrderStatus != OrderStatus.Paid) throw Oops.Bah("订单未支付,无法退款");
// 获取赛事信息
var eventInfo = await _eventRepository.GetByIdAsync(order.EventId);
if (eventInfo == null) throw Oops.Bah("赛事信息不存在");
// 计算退款金额
var refundAmount = await CalculateRefundAmount(order.ActualAmount, eventInfo.MatchStartTime);
// 处理退款
await ProcessRefundAsync(order, registration, refundAmount, input.RefundReason ?? "用户取消报名", userId);
}
///
/// 检查报名限制
///
/// 报名限制检查输入参数
/// 报名限制检查结果
[HttpPost("registration-limit/check")]
[DisplayName("检查报名限制")]
public async Task CheckRegistrationLimit([FromBody] UnifiedRegistrationInput input)
{
var userId = GetCurrentUserId();
input.CreatedUserId = userId;
return await _unifiedRegistrationService.CheckRegistrationLimitAsync(input);
}
///
/// 计算退款金额
///
private async Task CalculateRefundAmount(decimal totalAmount, DateTime eventStartTime)
{
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;
}
///
/// 处理退款核心逻辑
///
private async Task ProcessRefundAsync(
YD_Event.Core.Entities.Order order,
EventRegistration registration,
decimal refundAmount,
string refundReason,
long userId)
{
var now = DateTime.Now;
// 获取微信支付配置
var weChatPayConfig = await _weChatPayConfigRepository.GetFirstAsync(x => !x.DeleteMark);
// 生成退款单号
var outRefundNo = "RF" + DateTime.Now.ToString("yyyyMMddHHmmss") + new Random().Next(1000, 9999);
// 创建退款记录
var refundRecord = new RefundRecord
{
Id = _idGenerator.NewLong(),
OutRefundNo = outRefundNo,
RegistrationId = registration.Id,
OutTradeNo = order.OrderNo,
RefundAmount = refundAmount,
TotalAmount = order.ActualAmount,
RefundReason = refundReason,
RefundStatus = 0,
ApplyTime = now,
WeChatPayConfigId = weChatPayConfig?.Id,
CreatedTime = now,
CreatedUserId = userId,
Status = Core.Enum.AvailabilityStatus.Enable
};
// 获取支付记录中的微信交易号
var payment = await _paymentRepository.GetFirstAsync(p => p.RegistrationId == registration.Id);
if (payment != null && !string.IsNullOrEmpty(payment.TransactionNo))
{
refundRecord.TransactionId = payment.TransactionNo;
}
await _refundRecordRepository.InsertAsync(refundRecord);
// 如果是线上支付,调用微信退款接口
if (registration.PaymentMethod == 2 && weChatPayConfig != null)
{
await CreateWeChatRefundAsync(refundRecord, order, weChatPayConfig, refundAmount, registration, userId);
}
else
{
// 线下支付直接标记退款成功
await CompleteRefundAsync(refundRecord, order, registration, userId);
}
}
///
/// 调用微信退款接口
///
private async Task CreateWeChatRefundAsync(
RefundRecord refundRecord,
YD_Event.Core.Entities.Order order,
WeChatPayConfig config,
decimal refundAmount,
EventRegistration registration,
long userId)
{
try
{
var request = new YD_Event.Core.Infrastructure.WeChat.Dto.CreateRefundRequest
{
OutTradeNo = order.OrderNo,
OutRefundNo = refundRecord.OutRefundNo,
RefundAmount = (int)(refundAmount * 100),
TotalAmount = (int)(order.ActualAmount * 100),
Reason = refundRecord.RefundReason
//NotifyUrl = config.RefundNotifyUrl
};
var result = await _weChatPayService.CreateRefundAsync(config, request);
if (result.Success)
{
refundRecord.RefundId = result.RefundId;
refundRecord.WeChatResponse = result.WeChatResponse;
if (result.RefundStatus == "SUCCESS")
{
await CompleteRefundAsync(refundRecord, order, registration, userId);
}
else
{
refundRecord.RefundStatus = 0;
await _refundRecordRepository.UpdateAsync(refundRecord);
}
}
else
{
refundRecord.RefundStatus = 2;
refundRecord.ErrorMessage = result.Message;
refundRecord.WeChatResponse = result.WeChatResponse;
await _refundRecordRepository.UpdateAsync(refundRecord);
throw Oops.Bah($"微信退款申请失败: {result.Message}");
}
}
catch (Exception ex)
{
refundRecord.RefundStatus = 2;
refundRecord.ErrorMessage = ex.Message;
await _refundRecordRepository.UpdateAsync(refundRecord);
throw;
}
}
///
/// 完成退款流程
///
private async Task CompleteRefundAsync(
RefundRecord refundRecord,
YD_Event.Core.Entities.Order originalOrder,
EventRegistration registration,
long userId)
{
var now = DateTime.Now;
// 更新退款记录状态
refundRecord.RefundStatus = 1;
refundRecord.SuccessTime = now;
await _refundRecordRepository.UpdateAsync(refundRecord);
// 创建退款订单
var refundOrder = new YD_Event.Core.Entities.Order
{
Id = _idGenerator.NewLong(),
OrderNo = refundRecord.OutRefundNo,
OrderType = 2,
MerchantId = originalOrder.MerchantId,
MerchantCertificationId = originalOrder.MerchantCertificationId,
EventId = originalOrder.EventId,
RegistrationId = originalOrder.RegistrationId,
RelatedOrderId = originalOrder.Id,
OriginalAmount = originalOrder.ActualAmount,
ActualAmount = refundRecord.RefundAmount,
OrderStatus = OrderStatus.Refunded,
PaymentTime = now,
Remark = refundRecord.RefundReason,
CreatedTime = now,
CreatedUserId = userId,
Status = Core.Enum.AvailabilityStatus.Enable
};
await _orderRepository.InsertAsync(refundOrder);
// 更新原订单状态
originalOrder.OrderStatus = OrderStatus.Refunded;
originalOrder.UpdatedTime = now;
originalOrder.UpdatedUserId = userId;
await _orderRepository.UpdateAsync(originalOrder);
// 更新报名状态
registration.PaymentStatus = 3;
registration.UpdatedTime = now;
registration.UpdatedUserId = userId;
await _registrationRepository.UpdateAsync(registration);
// 更新支付记录
var payment = await _paymentRepository.GetFirstAsync(p => p.RegistrationId == originalOrder.RegistrationId);
if (payment != null)
{
payment.RefundStatus = 1;
payment.UpdatedTime = now;
payment.UpdatedUserId = userId;
await _paymentRepository.UpdateAsync(payment);
}
}
}