YD_Event.Admin/YD_Event.Application/Client/EventClientController.cs

1874 lines
78 KiB
C#
Raw Normal View History

2026-03-14 07:11:25 +08:00
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Logging;
2026-03-14 07:11:25 +08:00
using YD_Event.Application.Client.Dtos;
using YD_Event.Core.Entities;
using YD_Event.Core.Enum;
2026-04-09 20:49:36 +08:00
using YD_Event.Core.Utils;
using YD_Event.Core.Infrastructure.WeChat;
using YD_Event.Core.Infrastructure.WeChat.Dto;
using YD_Event.Core.Options;
2026-03-14 07:11:25 +08:00
using SqlSugar;
using Furion.DatabaseAccessor;
using Microsoft.AspNetCore.Authorization;
using Furion.FriendlyException;
using System.Security.Claims;
using System.Text.Json;
2026-04-07 23:52:35 +08:00
using System.IO;
2026-04-09 23:45:24 +08:00
using YD_Event.Core.Entities;
2026-03-14 07:11:25 +08:00
namespace YD_Event.Application.Client;
/// <summary>
/// 小程序-赛事接口
/// </summary>
[ApiDescriptionSettings("微信小程序接口")]
[Authorize]
2026-03-14 07:11:25 +08:00
public class EventClientController : IDynamicApiController
{
private readonly ISqlSugarRepository<EventInfo> _eventRepository;
private readonly ISqlSugarRepository<YD_Event.Core.Entities.Project> _projectRepository;
2026-03-18 16:25:49 +08:00
private readonly ISqlSugarRepository<EventProject> _eventProjectRepository;
2026-03-14 07:11:25 +08:00
private readonly ISqlSugarRepository<ProjectGroup> _groupRepository;
2026-04-07 23:52:35 +08:00
private readonly ISqlSugarRepository<EventProjectGroup> _eventProjectGroupRepository;
2026-03-14 07:11:25 +08:00
private readonly ISqlSugarRepository<EventRegistration> _registrationRepository;
private readonly ISqlSugarRepository<EventRegistrationItem> _registrationItemRepository;
private readonly ISqlSugarRepository<YD_Event.Core.Entities.Banner> _bannerRepository;
private readonly ISqlSugarRepository<EventUnit> _unitRepository;
private readonly ISqlSugarRepository<EventRegistrationPerson> _personRepository;
private readonly ISqlSugarRepository<EventRegistrationLimit> _limitRepository;
2026-04-04 23:31:38 +08:00
private readonly ISqlSugarRepository<EventProjectRegistrationLimit> _projectLimitRepository;
private readonly ISqlSugarRepository<EventFeeConfig> _feeConfigRepository;
private readonly ISqlSugarRepository<WeChatPayConfig> _weChatPayConfigRepository;
private readonly ISqlSugarRepository<YD_Event.Core.Entities.Merchant> _merchantRepository;
private readonly ISqlSugarRepository<PointsRule> _pointsRuleRepository;
2026-04-09 09:09:50 +08:00
private readonly ISqlSugarRepository<PointsRuleDetail> _pointsRuleDetailRepository;
private readonly ISqlSugarRepository<YD_Event.Core.Entities.AppUser> _appUserRepository;
2026-04-07 13:10:11 +08:00
private readonly ISqlSugarRepository<ProjectType> _projectTypeRepository;
2026-04-09 09:09:50 +08:00
private readonly ISqlSugarRepository<SysProvince> _provinceRepository;
private readonly ISqlSugarRepository<SysCity> _cityRepository;
private readonly ISqlSugarRepository<SysDistrict> _districtRepository;
2026-04-09 15:16:43 +08:00
private readonly ISqlSugarRepository<YD_Event.Core.Entities.Order> _orderRepository;
private readonly ISqlSugarRepository<MerchantCertification> _merchantCertificationRepository;
2026-04-09 20:49:36 +08:00
private readonly ISqlSugarRepository<EventRegistrationPayment> _paymentRepository;
2026-04-09 23:45:24 +08:00
private readonly ISqlSugarRepository<RefundRecord> _refundRecordRepository;
private readonly ISqlSugarRepository<RefundConfig> _refundConfigRepository;
2026-03-14 07:11:25 +08:00
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IIdGenerator _idGenerator;
private readonly WeChatPayService _weChatPayService;
private readonly IOptionsMonitor<WechatTenpayClientOptions> _wechatTenpayClientOptionsMonitor;
private readonly ILogger<EventClientController> _logger;
2026-03-14 07:11:25 +08:00
public EventClientController(
ISqlSugarRepository<EventInfo> eventRepository,
ISqlSugarRepository<YD_Event.Core.Entities.Project> projectRepository,
2026-03-18 16:25:49 +08:00
ISqlSugarRepository<EventProject> eventProjectRepository,
2026-03-14 07:11:25 +08:00
ISqlSugarRepository<ProjectGroup> groupRepository,
2026-04-07 23:52:35 +08:00
ISqlSugarRepository<EventProjectGroup> eventProjectGroupRepository,
2026-03-14 07:11:25 +08:00
ISqlSugarRepository<EventRegistration> registrationRepository,
ISqlSugarRepository<EventRegistrationItem> registrationItemRepository,
ISqlSugarRepository<YD_Event.Core.Entities.Banner> bannerRepository,
ISqlSugarRepository<EventUnit> unitRepository,
ISqlSugarRepository<EventRegistrationPerson> personRepository,
ISqlSugarRepository<EventRegistrationLimit> limitRepository,
2026-04-04 23:31:38 +08:00
ISqlSugarRepository<EventProjectRegistrationLimit> projectLimitRepository,
ISqlSugarRepository<EventFeeConfig> feeConfigRepository,
ISqlSugarRepository<WeChatPayConfig> weChatPayConfigRepository,
ISqlSugarRepository<YD_Event.Core.Entities.Merchant> merchantRepository,
ISqlSugarRepository<PointsRule> pointsRuleRepository,
2026-04-09 09:09:50 +08:00
ISqlSugarRepository<PointsRuleDetail> pointsRuleDetailRepository,
ISqlSugarRepository<YD_Event.Core.Entities.AppUser> appUserRepository,
2026-04-07 13:10:11 +08:00
ISqlSugarRepository<ProjectType> projectTypeRepository,
2026-04-09 09:09:50 +08:00
ISqlSugarRepository<SysProvince> provinceRepository,
ISqlSugarRepository<SysCity> cityRepository,
ISqlSugarRepository<SysDistrict> districtRepository,
2026-04-09 15:16:43 +08:00
ISqlSugarRepository<YD_Event.Core.Entities.Order> orderRepository,
ISqlSugarRepository<MerchantCertification> merchantCertificationRepository,
2026-04-09 20:49:36 +08:00
ISqlSugarRepository<EventRegistrationPayment> paymentRepository,
2026-04-09 23:45:24 +08:00
ISqlSugarRepository<RefundRecord> refundRecordRepository,
ISqlSugarRepository<RefundConfig> refundConfigRepository,
2026-03-14 07:11:25 +08:00
IHttpContextAccessor httpContextAccessor,
IIdGenerator idGenerator,
WeChatPayService weChatPayService,
IOptionsMonitor<WechatTenpayClientOptions> wechatTenpayClientOptionsMonitor,
ILogger<EventClientController> logger)
2026-03-14 07:11:25 +08:00
{
_eventRepository = eventRepository;
_projectRepository = projectRepository;
2026-03-18 16:25:49 +08:00
_eventProjectRepository = eventProjectRepository;
2026-03-14 07:11:25 +08:00
_groupRepository = groupRepository;
2026-04-07 23:52:35 +08:00
_eventProjectGroupRepository = eventProjectGroupRepository;
2026-03-14 07:11:25 +08:00
_registrationRepository = registrationRepository;
_registrationItemRepository = registrationItemRepository;
_bannerRepository = bannerRepository;
_unitRepository = unitRepository;
_personRepository = personRepository;
_limitRepository = limitRepository;
2026-04-04 23:31:38 +08:00
_projectLimitRepository = projectLimitRepository;
_feeConfigRepository = feeConfigRepository;
_weChatPayConfigRepository = weChatPayConfigRepository;
_merchantRepository = merchantRepository;
_pointsRuleRepository = pointsRuleRepository;
2026-04-09 09:09:50 +08:00
_pointsRuleDetailRepository = pointsRuleDetailRepository;
_appUserRepository = appUserRepository;
2026-04-07 13:10:11 +08:00
_projectTypeRepository = projectTypeRepository;
2026-04-09 09:09:50 +08:00
_provinceRepository = provinceRepository;
_cityRepository = cityRepository;
_districtRepository = districtRepository;
2026-04-09 15:16:43 +08:00
_orderRepository = orderRepository;
_merchantCertificationRepository = merchantCertificationRepository;
2026-04-09 20:49:36 +08:00
_paymentRepository = paymentRepository;
2026-04-09 23:45:24 +08:00
_refundRecordRepository = refundRecordRepository;
_refundConfigRepository = refundConfigRepository;
2026-03-14 07:11:25 +08:00
_httpContextAccessor = httpContextAccessor;
_idGenerator = idGenerator;
_weChatPayService = weChatPayService;
_wechatTenpayClientOptionsMonitor = wechatTenpayClientOptionsMonitor;
_logger = logger;
2026-03-14 07:11:25 +08:00
}
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;
}
/// <summary>
/// 获取轮播图列表
/// </summary>
/// <returns></returns>
[HttpGet("banners")]
[AllowAnonymous]
2026-04-07 13:10:11 +08:00
[DisplayName("获取轮播图列表")]
2026-03-14 07:11:25 +08:00
public async Task<List<BannerDto>> GetBannerList()
{
var list = await _bannerRepository.AsQueryable()
.Where(b => !b.DeleteMark && b.Status == AvailabilityStatus.Enable)
.OrderByDescending(b => b.Sort)
.ToListAsync();
return list.Select(b => new BannerDto
{
Title = b.Title,
ImageUrl = b.ImageUrl,
LinkType = b.LinkType,
LinkUrl = b.LinkUrl,
EventId = b.EventId
}).ToList();
}
/// <summary>
/// 获取赛事列表
/// </summary>
2026-04-07 13:10:11 +08:00
/// <param name="input">查询参数</param>
2026-03-14 07:11:25 +08:00
/// <returns></returns>
[HttpGet("list")]
2026-04-07 13:10:11 +08:00
[AllowAnonymous]
[DisplayName("获取赛事列表")]
public async Task<PageResult<EventListDto>> GetEventList([FromQuery] EventClientQueryInput input)
2026-03-14 07:11:25 +08:00
{
var query = _eventRepository.AsQueryable()
.Where(e => !e.DeleteMark && e.Status == AvailabilityStatus.Enable)
2026-04-07 13:10:11 +08:00
.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);
2026-03-14 07:11:25 +08:00
var now = DateTime.Now;
2026-04-07 13:10:11 +08:00
if (input.Status == 1)
2026-03-14 07:11:25 +08:00
{
query = query.Where(e => now >= e.RegistrationStartTime && now <= e.RegistrationEndTime);
}
2026-04-07 13:10:11 +08:00
else if (input.Status == 2)
2026-03-14 07:11:25 +08:00
{
query = query.Where(e => now >= e.MatchStartTime && now <= e.MatchEndTime);
}
2026-04-07 13:10:11 +08:00
else if (input.Status == 3)
2026-03-14 07:11:25 +08:00
{
query = query.Where(e => now > e.MatchEndTime);
}
query = query.OrderByDescending(e => e.RegistrationStartTime);
2026-04-07 13:10:11 +08:00
var list = await query.ToPagedListAsync(input);
2026-03-14 07:11:25 +08:00
var result = new PageResult<EventListDto>
{
PageNo = list.PageNo,
PageSize = list.PageSize,
Total = list.Total,
Pages = list.Pages,
Rows = list.Rows.Select(e => new EventListDto
{
Id = e.Id,
EventName = e.EventName,
CoverImage = e.CoverImage,
2026-04-07 13:10:11 +08:00
EventStatus = CalculateEventStatus(e),
2026-03-14 07:11:25 +08:00
RegistrationStartTime = e.RegistrationStartTime,
RegistrationEndTime = e.RegistrationEndTime,
MatchStartTime = e.MatchStartTime,
MatchEndTime = e.MatchEndTime,
Address = $"{e.Province}{e.City}{e.District}{e.DetailAddress}"
}).ToList()
};
return result;
}
/// <summary>
/// 获取赛事详情
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
[HttpGet("{id}")]
[AllowAnonymous]
2026-04-07 13:10:11 +08:00
[DisplayName("获取赛事详情")]
2026-03-14 07:11:25 +08:00
public async Task<EventDetailDto> GetEventDetail(long id)
{
var evt = await _eventRepository.GetFirstAsync(e => e.Id == id && !e.DeleteMark);
if (evt == null) throw Oops.Bah("赛事不存在");
2026-04-09 09:09:50 +08:00
bool isRegistered = false;
try
{
var userId = GetCurrentUserId();
isRegistered = await _registrationRepository.IsAnyAsync(r =>
2026-04-09 23:12:51 +08:00
r.EventId == id && r.CreatedUserId == userId && r.PaymentStatus != 4 && r.PaymentStatus == 1);
2026-04-09 09:09:50 +08:00
}
catch
{
isRegistered = false;
}
var feeConfig = await _feeConfigRepository.GetFirstAsync(f => f.EventId == id && !f.DeleteMark);
string provinceName = evt.Province ?? "";
string cityName = evt.City ?? "";
string districtName = evt.District ?? "";
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()
};
}
}
2026-03-18 16:25:49 +08:00
var eventProjects = await _eventProjectRepository.AsQueryable()
.Where(ep => ep.EventId == id)
.ToListAsync();
2026-04-04 23:31:38 +08:00
var eventProjectIds = eventProjects.Select(ep => ep.Id).Distinct().ToList();
2026-03-18 16:25:49 +08:00
var projectIds = eventProjects.Select(ep => ep.ProjectId).Distinct().ToList();
2026-03-18 16:25:49 +08:00
var baseProjects = await _projectRepository.AsQueryable()
2026-03-14 07:11:25 +08:00
.LeftJoin<ProjectType>((p, t) => p.ProjectTypeId == t.Id)
2026-03-18 16:25:49 +08:00
.Where(p => projectIds.Contains(p.Id) && !p.DeleteMark && p.Status == AvailabilityStatus.Enable)
.Select((p, t) => new {
p.Id,
p.Name,
TypeName = t.Name
2026-03-14 07:11:25 +08:00
})
.ToListAsync();
2026-03-14 07:11:25 +08:00
var groups = await _groupRepository.AsQueryable()
.Where(g => projectIds.Contains(g.ProjectId) && !g.DeleteMark)
.ToListAsync();
2026-04-04 23:31:38 +08:00
var projectLimits = await _projectLimitRepository.AsQueryable()
.Where(pl => pl.EventId == id && !pl.DeleteMark)
.ToListAsync();
2026-03-14 07:11:25 +08:00
var units = await _unitRepository.AsQueryable()
.Where(u => u.EventId == id && !u.DeleteMark)
.OrderBy(u => u.Sort)
.ToListAsync();
2026-03-14 07:11:25 +08:00
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积分";
if (!string.IsNullOrWhiteSpace(evt.CustomConfigJson))
{
try
{
var config = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, string>>(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
{
}
}
if (evt.PointsRuleId.HasValue)
{
var pointsRuleEntity = await _pointsRuleRepository.GetFirstAsync(r => r.Id == evt.PointsRuleId.Value && !r.DeleteMark);
if (pointsRuleEntity != null)
{
pointsRule = pointsRuleEntity.Remark ?? pointsRule;
}
}
2026-04-09 09:09:50 +08:00
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<long, decimal>();
foreach (var ep in eventProjects)
{
feeConfigDto.ProjectPrices[ep.Id] = ep.Price / 100m;
}
}
}
2026-03-14 07:11:25 +08:00
var detail = new EventDetailDto
{
Id = evt.Id,
EventName = evt.EventName,
CoverImage = evt.CoverImage,
EventStatus = CalculateEventStatus(evt),
RegistrationStartTime = evt.RegistrationStartTime,
RegistrationEndTime = evt.RegistrationEndTime,
MatchStartTime = evt.MatchStartTime,
MatchEndTime = evt.MatchEndTime,
2026-04-09 09:09:50 +08:00
Address = $"{provinceName}{cityName}{districtName}{evt.DetailAddress}",
2026-03-14 07:11:25 +08:00
EventVideo = evt.EventVideo,
EventType = evt.EventType,
ChargeMode = evt.ChargeMode,
2026-04-09 09:09:50 +08:00
FeeConfig = feeConfigDto,
2026-03-14 07:11:25 +08:00
RegistrationType = evt.RegistrationType,
EventIntroduction = evt.EventIntroduction,
2026-04-09 09:09:50 +08:00
IsNeedMaterial = evt.IsNeedMaterial,
2026-03-14 07:11:25 +08:00
MaterialRemark = evt.MaterialRemark,
MaterialUploadStartTime = evt.MaterialUploadStartTime,
MaterialUploadEndTime = evt.MaterialUploadEndTime,
RefundRule = refundRule,
PointsRule = pointsRule,
2026-04-09 09:09:50 +08:00
PointsRuleDetail = pointsRuleDetail,
IsRegistered = isRegistered,
ProvinceName = provinceName,
CityName = cityName,
DistrictName = districtName,
2026-03-14 07:11:25 +08:00
IndividualProjectLimit = limit?.IndividualProjectLimit ?? 0,
TeamProjectLimit = limit?.TeamProjectLimit ?? 0,
Units = units.Select(u => new EventUnitDto
2026-03-14 07:11:25 +08:00
{
UnitType = u.UnitType,
UnitName = u.UnitName,
LogoUrl = u.LogoUrl
}).ToList(),
Projects = eventProjects.Select(ep =>
2026-03-14 07:11:25 +08:00
{
2026-03-18 16:25:49 +08:00
var bp = baseProjects.FirstOrDefault(b => b.Id == ep.ProjectId);
2026-04-04 23:31:38 +08:00
var pl = projectLimits.FirstOrDefault(x => x.ProjectId == ep.Id);
2026-03-18 16:25:49 +08:00
return new EventProjectDto
2026-03-14 07:11:25 +08:00
{
2026-03-18 16:25:49 +08:00
Id = ep.Id,
Name = bp?.Name ?? "",
TypeName = bp?.TypeName ?? "",
Fee = ep.Price / 100m,
2026-04-04 23:31:38 +08:00
ProjectLimitCount = pl?.LimitCount,
2026-03-18 16:25:49 +08:00
Groups = groups.Where(g => g.ProjectId == ep.ProjectId).Select(g => new EventProjectGroupDto
{
Id = g.Id,
GroupName = g.GroupName,
GenderRequirement = g.Gender == Gender.Male ? "男" : (g.Gender == Gender.Female ? "女" : "不限"),
AgeRequirement = $"{g.BirthDateStart:yyyy-MM-dd} 至 {g.BirthDateEnd:yyyy-MM-dd}"
}).ToList()
};
}).Where(x => !string.IsNullOrEmpty(x.Name)).ToList()
2026-03-14 07:11:25 +08:00
};
return detail;
}
2026-04-04 23:31:38 +08:00
/// <summary>
/// 计算报名金额
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[HttpPost("calculate-amount")]
2026-04-07 13:10:11 +08:00
[DisplayName("计算报名金额")]
2026-04-04 23:31:38 +08:00
public async Task<CalculateAmountOutput> CalculateRegistrationAmount([FromBody] CalculateAmountInput input)
{
var evt = await _eventRepository.GetFirstAsync(e => e.Id == input.EventId && !e.DeleteMark);
if (evt == null) throw Oops.Bah("赛事不存在");
var result = new CalculateAmountOutput
{
ChargeMode = evt.ChargeMode,
TotalAmount = 0,
ItemDetails = new List<CalculateAmountItemOutput>()
};
if (evt.ChargeMode == ChargeMode.Free)
{
return result;
}
// 获取赛事费用配置
var feeConfig = await _feeConfigRepository.GetFirstAsync(f => f.EventId == input.EventId && !f.DeleteMark);
// 获取项目信息
var eventProjectIds = input.Items.Select(i => i.ProjectId).Distinct().ToList();
var eventProjects = await _eventProjectRepository.AsQueryable()
.Where(ep => eventProjectIds.Contains(ep.Id))
.ToListAsync();
var projectIds = eventProjects.Select(ep => ep.ProjectId).Distinct().ToList();
var baseProjects = await _projectRepository.AsQueryable()
.Where(p => projectIds.Contains(p.Id))
.ToListAsync();
2026-04-09 15:16:43 +08:00
// 计算总人数
int totalParticipantCount = input.Items.Sum(i => i.PersonCount);
2026-04-04 23:31:38 +08:00
2026-04-09 15:16:43 +08:00
// 构建定价服务输入
var pricingInput = new YD_Event.Core.Domain.Pricing.EventPricingService.PricingInput
{
ChargeMode = evt.ChargeMode,
ParticipantCount = totalParticipantCount,
EventPrice = feeConfig?.Price ?? 0m
};
// 添加项目信息(用于单价项目模式)
2026-04-04 23:31:38 +08:00
foreach (var item in input.Items)
{
var ep = eventProjects.FirstOrDefault(p => p.Id == item.ProjectId);
if (ep == null) throw Oops.Bah($"项目不存在: {item.ProjectId}");
var bp = baseProjects.FirstOrDefault(p => p.Id == ep.ProjectId);
2026-04-09 15:16:43 +08:00
pricingInput.Projects.Add(new YD_Event.Core.Domain.Pricing.EventPricingService.ProjectPricingInfo
{
ProjectId = ep.Id,
ProjectName = bp?.Name ?? "",
UnitPrice = ep.Price / 100m
});
}
// 使用统一的定价服务计算费用
var pricingResult = YD_Event.Core.Domain.Pricing.EventPricingService.Calculate(pricingInput);
result.TotalAmount = pricingResult.TotalAmount;
// 构建明细输出
foreach (var item in input.Items)
{
var ep = eventProjects.FirstOrDefault(p => p.Id == item.ProjectId);
var bp = baseProjects.FirstOrDefault(p => p.Id == ep?.ProjectId);
2026-04-04 23:31:38 +08:00
decimal unitPrice = 0;
decimal subtotal = 0;
switch (evt.ChargeMode)
{
case ChargeMode.FixedPrice:
2026-04-09 15:16:43 +08:00
// 定价(赛事):每个项目显示0,但实际只收一次固定费用
unitPrice = 0;
subtotal = 0;
2026-04-04 23:31:38 +08:00
break;
case ChargeMode.UnitPriceEvent:
2026-04-09 15:16:43 +08:00
// 单价(赛事):按赛事单价计算
unitPrice = feeConfig?.Price ?? 0;
subtotal = unitPrice * item.PersonCount;
2026-04-04 23:31:38 +08:00
break;
case ChargeMode.UnitPriceProject:
2026-04-09 15:16:43 +08:00
// 单价(项目):按项目单价计算
unitPrice = ep?.Price / 100m ?? 0;
2026-04-04 23:31:38 +08:00
subtotal = unitPrice * item.PersonCount;
break;
}
result.ItemDetails.Add(new CalculateAmountItemOutput
{
ProjectId = item.ProjectId,
ProjectName = bp?.Name ?? "",
UnitPrice = unitPrice,
PersonCount = item.PersonCount,
Subtotal = subtotal
});
}
return result;
}
2026-03-14 07:11:25 +08:00
/// <summary>
/// 提交赛事报名
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[HttpPost("apply")]
[UnitOfWork]
2026-04-07 13:10:11 +08:00
[DisplayName("提交赛事报名")]
2026-03-14 07:11:25 +08:00
public async Task<long> SubmitRegistration([FromBody] SubmitRegistrationInput input)
{
var userId = GetCurrentUserId();
// 0. 校验报名模式
if (input.RegistrationMode == 2) // 假设2为团队
throw Oops.Bah("团队报名请前往网页端进行操作");
var evt = await _eventRepository.GetFirstAsync(e => e.Id == input.EventId);
if (evt == null) throw Oops.Bah("赛事不存在");
// 1. 校验时间
if (DateTime.Now < evt.RegistrationStartTime || DateTime.Now > evt.RegistrationEndTime)
throw Oops.Bah("不在报名时间内");
2026-04-07 23:52:35 +08:00
// 2. 校验重复报名 - 如果已有未取消的报名记录,直接返回现有记录ID
var existingRegistration = await _registrationRepository.GetFirstAsync(
2026-04-09 23:12:51 +08:00
r => r.EventId == input.EventId && r.CreatedUserId == userId && r.PaymentStatus != 4); // 4=已取消
2026-04-07 23:52:35 +08:00
if (existingRegistration != null)
{
// 已有未取消的报名记录,直接返回现有记录ID,让前端跳转到支付页面
return existingRegistration.Id;
}
// 3. 获取当前用户信息(用于个人报名时自动填充)
var appUser = await _appUserRepository.GetFirstAsync(u => u.CreatedUserId == userId && !u.DeleteMark);
if (appUser == null && input.RegistrationMode == 1)
throw Oops.Bah("请先完成身份认证");
// 3.5 个人报名模式下自动填充联系人信息
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("联系人电话不能为空,请先完善身份认证信息");
}
2026-03-14 07:11:25 +08:00
2026-04-07 23:52:35 +08:00
// 4. 校验项目有效性 & 限制
2026-03-14 07:11:25 +08:00
if (input.Items == null || !input.Items.Any()) throw Oops.Bah("请至少选择一个项目");
var limit = await _limitRepository.GetFirstAsync(l => l.EventId == input.EventId && !l.DeleteMark);
if (limit != null)
{
if (input.Items.Count > limit.IndividualProjectLimit)
throw Oops.Bah($"每人最多可报{limit.IndividualProjectLimit}个项目");
}
2026-04-07 13:10:11 +08:00
var projectLimits = await _projectLimitRepository.AsQueryable()
.Where(pl => pl.EventId == input.EventId && !pl.DeleteMark)
.ToListAsync();
foreach (var item in input.Items)
{
var projectLimit = projectLimits.FirstOrDefault(pl => pl.ProjectId == item.ProjectId);
if (projectLimit != null)
{
var currentCount = await _registrationItemRepository.AsQueryable()
.InnerJoin<EventRegistration>((ri, r) => ri.RegistrationId == r.Id)
2026-04-09 23:12:51 +08:00
.Where((ri, r) => ri.ProjectId == item.ProjectId && r.PaymentStatus != 4)
2026-04-07 13:10:11 +08:00
.CountAsync();
if (currentCount >= projectLimit.LimitCount)
{
var eventProject = await _eventProjectRepository.GetFirstAsync(ep => ep.Id == item.ProjectId);
var project = await _projectRepository.GetFirstAsync(p => p.Id == eventProject.ProjectId);
throw Oops.Bah($"项目[{project?.Name}]已达到报名上限,无法继续报名");
}
}
2026-04-07 23:52:35 +08:00
// 个人报名模式下自动填充参赛人员信息
if (input.RegistrationMode == 1 && (item.Persons == null || !item.Persons.Any()))
{
if (appUser == null)
throw Oops.Bah("请先完成身份认证");
item.Persons = new List<PersonInput>
{
new PersonInput
{
Name = appUser.Name ?? "",
Gender = appUser.Gender,
IdCardNumber = appUser.IdCardNumber ?? "",
BirthDate = appUser.BirthDate ?? DateTime.Now,
Mobile = appUser.Mobile
}
};
}
// 最终验证参赛人员信息
if (item.Persons == null || !item.Persons.Any())
throw Oops.Bah($"项目未填写参赛人信息");
2026-04-07 13:10:11 +08:00
}
2026-03-14 07:11:25 +08:00
2026-04-07 23:52:35 +08:00
// 5. 创建报名主表
2026-03-14 07:11:25 +08:00
var registrationId = _idGenerator.NewLong();
var registration = new EventRegistration
{
Id = registrationId,
EventId = input.EventId,
RegistrationMode = input.RegistrationMode,
TeamName = input.RegistrationMode == 2 ? input.TeamName : null,
ContactName = input.ContactName,
ContactPhone = input.ContactPhone,
PaymentStatus = 0, // 未支付
RegistrationTime = DateTime.Now,
CreatedUserId = userId,
CreatedTime = DateTime.Now,
Status = AvailabilityStatus.Enable,
TotalPersonCount = 0 // 稍后计算
};
2026-04-09 15:16:43 +08:00
// 5. 获取费用配置并使用统一的定价服务计算费用
var feeConfig = await _feeConfigRepository.GetFirstAsync(f => f.EventId == input.EventId && !f.DeleteMark);
2026-03-14 07:11:25 +08:00
2026-04-09 15:16:43 +08:00
// 先获取所有需要的 eventProjectGroup 信息和 eventProject 信息
2026-04-07 23:52:35 +08:00
var eventProjectIds = input.Items.Select(i => i.ProjectId).Distinct().ToList();
var allEventProjectGroups = await _eventProjectGroupRepository.GetListAsync(
eg => eventProjectIds.Contains(eg.EventProjectId) && !eg.DeleteMark);
2026-04-09 15:16:43 +08:00
var eventProjects = await _eventProjectRepository.AsQueryable()
.Where(ep => eventProjectIds.Contains(ep.Id))
.ToListAsync();
var projectIds = eventProjects.Select(ep => ep.ProjectId).Distinct().ToList();
var baseProjects = await _projectRepository.AsQueryable()
.Where(p => projectIds.Contains(p.Id))
.ToListAsync();
// 计算总人数
int totalPerson = input.Items.Sum(i => i.Persons!.Count);
// 构建定价服务输入
var pricingInput = new YD_Event.Core.Domain.Pricing.EventPricingService.PricingInput
{
ChargeMode = evt.ChargeMode,
ParticipantCount = totalPerson,
EventPrice = feeConfig?.Price ?? 0m
};
// 添加项目信息(用于单价项目模式)
foreach (var item in input.Items)
{
var ep = eventProjects.FirstOrDefault(p => p.Id == item.ProjectId);
var bp = baseProjects.FirstOrDefault(p => p.Id == ep?.ProjectId);
if (ep != null)
{
pricingInput.Projects.Add(new YD_Event.Core.Domain.Pricing.EventPricingService.ProjectPricingInfo
{
ProjectId = ep.Id,
ProjectName = bp?.Name ?? "",
UnitPrice = ep.Price / 100m
});
}
}
// 使用统一的定价服务计算费用
var pricingResult = YD_Event.Core.Domain.Pricing.EventPricingService.Calculate(pricingInput);
// 6. 创建报名明细 & 参赛人员
var regItems = new List<EventRegistrationItem>();
var persons = new List<EventRegistrationPerson>();
2026-04-07 23:52:35 +08:00
2026-03-14 07:11:25 +08:00
foreach (var item in input.Items)
{
2026-04-09 15:16:43 +08:00
var eventProject = eventProjects.FirstOrDefault(p => p.Id == item.ProjectId);
2026-03-18 16:25:49 +08:00
if (eventProject == null) throw Oops.Bah($"项目不存在: {item.ProjectId}");
2026-04-09 15:16:43 +08:00
var project = baseProjects.FirstOrDefault(p => p.Id == eventProject.ProjectId);
2026-03-18 16:25:49 +08:00
if (project == null) throw Oops.Bah($"关联基础项目不存在: {item.ProjectId}");
2026-03-14 07:11:25 +08:00
2026-04-07 23:52:35 +08:00
// 获取项目类型名称
var projectType = await _projectTypeRepository.GetFirstAsync(t => t.Id == project.ProjectTypeId);
var projectTypeName = projectType?.Name ?? "";
2026-03-14 07:11:25 +08:00
2026-04-07 23:52:35 +08:00
// 获取组别名称 - 优先从前端传的获取,否则从数据库获取默认组别
var groupName = item.GroupName;
if (string.IsNullOrWhiteSpace(groupName))
{
var eventProjectGroups = allEventProjectGroups.Where(eg => eg.EventProjectId == item.ProjectId).ToList();
if (item.GroupId.HasValue)
{
// 如果传了 groupId,优先使用
var selectedGroup = eventProjectGroups.FirstOrDefault(eg => eg.Id == item.GroupId.Value);
groupName = selectedGroup?.GroupName ?? "";
}
// 如果还是没有,使用第一个组别
if (string.IsNullOrWhiteSpace(groupName))
{
var firstGroup = eventProjectGroups.FirstOrDefault();
groupName = firstGroup?.GroupName ?? "";
}
// 如果还是没有,设置默认值
if (string.IsNullOrWhiteSpace(groupName))
{
groupName = "默认组别";
}
}
2026-04-09 15:16:43 +08:00
// 根据收费模式计算该项目的单价和总价
decimal unitPrice = 0;
decimal totalPrice = 0;
switch (evt.ChargeMode)
{
case ChargeMode.Free:
unitPrice = 0;
totalPrice = 0;
break;
case ChargeMode.FixedPrice:
2026-04-09 23:12:51 +08:00
// 定价(赛事):使用赛事费用配置的单价
unitPrice = feeConfig?.Price ?? 0;
totalPrice = unitPrice * item.Persons!.Count;
2026-04-09 15:16:43 +08:00
break;
case ChargeMode.UnitPriceEvent:
// 单价(赛事):使用赛事单价
unitPrice = feeConfig?.Price ?? 0;
totalPrice = unitPrice * item.Persons!.Count;
break;
case ChargeMode.UnitPriceProject:
// 单价(项目):使用项目单价
unitPrice = eventProject.Price / 100m;
totalPrice = unitPrice * item.Persons!.Count;
break;
}
2026-03-14 07:11:25 +08:00
var regItemId = _idGenerator.NewLong();
var regItem = new EventRegistrationItem
{
Id = regItemId,
RegistrationId = registrationId,
ProjectId = item.ProjectId,
ProjectName = project.Name,
2026-04-07 23:52:35 +08:00
ProjectTypeName = projectTypeName,
GroupName = groupName,
2026-04-09 15:16:43 +08:00
UnitPrice = unitPrice,
2026-04-07 23:52:35 +08:00
PersonCount = item.Persons!.Count,
2026-04-09 15:16:43 +08:00
TotalPrice = totalPrice,
2026-03-14 07:11:25 +08:00
CreatedUserId = userId,
CreatedTime = DateTime.Now
};
regItems.Add(regItem);
// 添加参赛人员
foreach (var pInput in item.Persons)
{
persons.Add(new EventRegistrationPerson
{
Id = _idGenerator.NewLong(),
RegistrationId = registrationId,
RegistrationItemId = regItemId,
ProjectId = item.ProjectId,
2026-03-20 02:00:34 +08:00
PersonType = PersonType.Athlete, // 运动员
2026-03-14 07:11:25 +08:00
Name = pInput.Name,
Gender = pInput.Gender,
2026-03-25 09:09:45 +08:00
IdCard = pInput.IdCardNumber,
2026-03-14 07:11:25 +08:00
BirthDate = pInput.BirthDate,
2026-03-25 09:09:45 +08:00
Phone = pInput.Mobile,
2026-04-07 23:52:35 +08:00
GroupName = groupName,
2026-03-14 07:11:25 +08:00
ProjectName = project.Name,
CreatedUserId = userId,
CreatedTime = DateTime.Now
});
}
}
2026-04-09 15:16:43 +08:00
registration.TotalAmount = pricingResult.TotalAmount;
2026-03-14 07:11:25 +08:00
registration.TotalPersonCount = totalPerson;
registration.OrderNo = _idGenerator.NewLong().ToString(); // 使用雪花ID生成订单号
2026-03-14 07:11:25 +08:00
await _registrationRepository.InsertAsync(registration);
await _registrationItemRepository.InsertRangeAsync(regItems);
await _personRepository.InsertRangeAsync(persons);
2026-04-09 15:16:43 +08:00
// 创建订单记录
var certification = await _merchantCertificationRepository.GetFirstAsync(c =>
c.UserId == userId && c.AuditStatus == AuditStatus.Pass && !c.DeleteMark);
var order = new YD_Event.Core.Entities.Order
{
Id = _idGenerator.NewLong(),
OrderNo = registration.OrderNo,
OrderType = 1, // 收入
PaymentMethod = registration.PaymentMethod,
RegistrationMode = registration.RegistrationMode,
MerchantId = evt.MerchantId,
MerchantCertificationId = certification?.Id,
EventId = input.EventId,
RegistrationId = registrationId,
OriginalAmount = pricingResult.TotalAmount,
DiscountAmount = 0,
ActualAmount = pricingResult.TotalAmount,
OrderStatus = OrderStatus.Pending, // 待支付
CreatedTime = DateTime.Now,
CreatedUserId = userId,
Status = AvailabilityStatus.Enable
};
await _orderRepository.InsertAsync(order);
2026-03-14 07:11:25 +08:00
return registrationId;
}
/// <summary>
/// 获取支付信息
/// </summary>
2026-04-04 23:31:38 +08:00
/// <remarks>
/// 用于获取赛事报名的支付信息,包括微信JSAPI支付参数
/// OpenId 参数必须通过请求体(JSON)传入,格式验证由DTO自动处理
/// </remarks>
/// <param name="input">支付信息查询输入参数,包含报名记录ID和OpenId</param>
/// <returns>支付信息,包括微信支付参数(如果是微信支付)</returns>
[HttpPost("payment-info")]
2026-04-07 13:10:11 +08:00
[DisplayName("获取支付信息")]
2026-04-04 23:31:38 +08:00
public async Task<PaymentInfoDto> GetPaymentInfo([FromBody] GetPaymentInfoInput input)
2026-03-14 07:11:25 +08:00
{
var userId = GetCurrentUserId();
2026-04-04 23:31:38 +08:00
var reg = await _registrationRepository.GetFirstAsync(r => r.Id == input.RegistrationId && r.CreatedUserId == userId);
2026-03-14 07:11:25 +08:00
if (reg == null) throw Oops.Bah("报名记录不存在");
var evt = await _eventRepository.GetFirstAsync(e => e.Id == reg.EventId);
if (evt == null) throw Oops.Bah("赛事不存在");
var configOptions = _wechatTenpayClientOptionsMonitor.CurrentValue;
2026-04-07 13:10:11 +08:00
var merchant = await _merchantRepository.GetFirstAsync(m => m.Id == evt.MerchantId && !m.DeleteMark);
var paymentInfo = new PaymentInfoDto
2026-03-14 07:11:25 +08:00
{
RegistrationId = reg.Id,
Amount = reg.TotalAmount,
PaymentMethod = reg.PaymentMethod,
2026-04-07 13:10:11 +08:00
BankAccountName = merchant?.MerchantName ?? "上海跃动文化科技有限公司",
BankAccountNo = merchant?.AccountNumber ?? "456498465198456261",
BankName = merchant?.BankName ?? "招商银行"
2026-03-14 07:11:25 +08:00
};
if (reg.PaymentMethod == 2 && reg.TotalAmount > 0)
{
2026-04-07 23:52:35 +08:00
// 根据赛事信息获取对应的商户支付配置
var payConfig = await GetWeChatPayConfigByEventAsync(evt);
if (payConfig == null)
{
2026-04-07 23:52:35 +08:00
_logger.LogError("未找到可用的微信支付配置,赛事ID={EventId},商户ID={MerchantId}", evt.Id, evt.MerchantId);
throw Oops.Bah("未找到可用的微信支付配置,请联系赛事管理员");
}
2026-04-07 23:52:35 +08:00
_logger.LogInformation("开始创建微信 JSAPI 支付订单,订单号={OrderNo},OpenId={OpenId},商户ID={MerchantId}",
reg.OrderNo, input.OpenId, payConfig.MerchantId);
var jsapiRequest = new CreateJsApiPayRequest
{
2026-04-07 23:52:35 +08:00
OutTradeNo = reg.OrderNo,
AppId = configOptions.AppId,
Description = $"{evt.EventName} - 报名费",
NotifyUrl = configOptions.NotifyUrl,
2026-04-09 20:49:36 +08:00
TotalAmount = AmountHelper.YuanToFen(reg.TotalAmount),
2026-04-07 23:52:35 +08:00
ExpireTime = DateTimeOffset.Now.AddMinutes(30),
OpenId = input.OpenId
};
2026-04-07 23:52:35 +08:00
var payResult = await _weChatPayService.CreateJsApiPayAsync(payConfig, jsapiRequest);
if (payResult.Success && payResult.JsApiParameters != null)
{
2026-04-07 23:52:35 +08:00
_logger.LogInformation("微信 JSAPI 支付下单成功,订单号={OrderNo}", reg.OrderNo);
paymentInfo.WechatPayParams = new
{
2026-04-07 23:52:35 +08:00
appId = payResult.JsApiParameters.AppId,
timeStamp = payResult.JsApiParameters.TimeStamp,
nonceStr = payResult.JsApiParameters.NonceStr,
package = payResult.JsApiParameters.Package,
signType = payResult.JsApiParameters.SignType,
paySign = payResult.JsApiParameters.PaySign
};
}
2026-04-04 23:31:38 +08:00
else
{
2026-04-07 23:52:35 +08:00
_logger.LogError("微信 JSAPI 支付下单失败:订单号={OrderNo},错误信息={Message}", reg.OrderNo, payResult.Message);
throw Oops.Bah($"微信支付下单失败:{payResult.Message}");
2026-04-04 23:31:38 +08:00
}
}
return paymentInfo;
2026-03-14 07:11:25 +08:00
}
2026-04-07 23:52:35 +08:00
/// <summary>
/// 根据赛事ID精确获取对应的微信支付配置
/// </summary>
/// <remarks>
/// 1. 通过赛事信息中的 MerchantId 查找对应的微信支付配置
/// 2. 必须确保赛事已关联商户配置,不使用兜底策略
/// 3. 只返回启用状态且未删除的配置
/// </remarks>
/// <param name="eventInfo">赛事信息,必须包含有效的 MerchantId</param>
/// <returns>微信支付配置,如果未找到则返回 null</returns>
/// <exception cref="ArgumentException">当赛事没有关联商户配置时抛出</exception>
private async Task<WeChatPayConfig?> 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;
}
2026-03-14 07:11:25 +08:00
/// <summary>
/// 上传支付凭证(线下支付)
/// </summary>
[HttpPost("upload-voucher/{registrationId}")]
2026-04-07 13:10:11 +08:00
[DisplayName("上传支付凭证")]
2026-03-14 07:11:25 +08:00
public async Task<bool> UploadPaymentVoucher(long registrationId, [FromBody] UploadVoucherInput input)
{
var userId = GetCurrentUserId();
var reg = await _registrationRepository.GetFirstAsync(r => r.Id == registrationId && r.CreatedUserId == userId);
if (reg == null) throw Oops.Bah("报名记录不存在");
reg.PaymentMethod = 1; // 线下
reg.PaymentProofUrl = input.VoucherUrl;
2026-04-09 23:12:51 +08:00
reg.PaymentStatus = 0; // 待审核
2026-03-14 07:11:25 +08:00
reg.UpdatedTime = DateTime.Now;
reg.UpdatedUserId = userId;
return await _registrationRepository.UpdateAsync(reg);
}
/// <summary>
/// 更新参赛人信息
/// </summary>
[HttpPost("update-player")]
2026-04-07 13:10:11 +08:00
[DisplayName("更新参赛人信息")]
2026-03-14 07:11:25 +08:00
public async Task<bool> 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;
2026-03-25 09:09:45 +08:00
person.IdCard = input.IdCardNumber;
2026-03-14 07:11:25 +08:00
person.Gender = input.Gender;
2026-03-25 09:09:45 +08:00
person.Phone = input.Mobile;
2026-03-14 07:11:25 +08:00
person.UpdatedTime = DateTime.Now;
person.UpdatedUserId = userId;
return await _personRepository.UpdateAsync(person);
}
/// <summary>
/// 获取我的已报赛事列表
/// </summary>
/// <returns></returns>
[HttpGet("my-events")]
2026-04-07 13:10:11 +08:00
[DisplayName("获取我的已报赛事列表")]
2026-03-14 07:11:25 +08:00
public async Task<PageResult<MyEventListDto>> GetMyEventList([FromQuery] int page = 1, [FromQuery] int pageSize = 10)
{
var userId = GetCurrentUserId();
var query = _registrationRepository.AsQueryable()
.LeftJoin<EventInfo>((r, e) => r.EventId == e.Id)
2026-04-09 15:16:43 +08:00
.Where(r => r.CreatedUserId == userId && !r.DeleteMark && r.PaymentStatus == 1)
2026-03-14 07:11:25 +08:00
.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,
2026-04-09 09:09:50 +08:00
ProjectCount = SqlFunc.Subqueryable<EventRegistrationItem>().Where(i => i.RegistrationId == r.Id).Count(),
IsNeedMaterial = e.IsNeedMaterial,
MaterialRemark = e.MaterialRemark,
MaterialUploadStartTime = e.MaterialUploadStartTime,
MaterialUploadEndTime = e.MaterialUploadEndTime,
MaterialUrl = r.MaterialUrl
2026-03-14 07:11:25 +08:00
});
return await query.ToPagedListAsync(new Pagination { PageNo = page, PageSize = pageSize });
}
/// <summary>
/// 获取报名详情(包含参赛人)
/// </summary>
[HttpGet("my-registration/{registrationId}")]
2026-04-07 13:10:11 +08:00
[DisplayName("获取报名详情")]
2026-03-14 07:11:25 +08:00
public async Task<RegistrationDetailDto> GetRegistrationDetail(long registrationId)
{
var userId = GetCurrentUserId();
var reg = await _registrationRepository.AsQueryable()
.LeftJoin<EventInfo>((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
2026-04-07 13:10:11 +08:00
EventStatus status = EventStatus.Enrolling;
2026-03-14 07:11:25 +08:00
var now = DateTime.Now;
2026-04-07 13:10:11 +08:00
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;
2026-03-14 07:11:25 +08:00
// 获取 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 RegistrationPersonDto
{
Id = p.Id,
Name = p.Name,
2026-03-25 09:09:45 +08:00
IdCardNumber = p.IdCard,
2026-03-14 07:11:25 +08:00
Gender = p.Gender,
2026-03-25 09:09:45 +08:00
Mobile = p.Phone
2026-03-14 07:11:25 +08:00
}).ToList()
}).ToList()
};
}
/// <summary>
/// 查询报名状态
/// </summary>
/// <param name="registrationId"></param>
/// <returns></returns>
[HttpGet("status/{registrationId}")]
2026-04-07 13:10:11 +08:00
[DisplayName("查询报名状态")]
2026-03-14 07:11:25 +08:00
public async Task<RegistrationStatusDto> 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
};
}
/// <summary>
/// 取消报名
/// </summary>
/// <param name="registrationId"></param>
/// <returns></returns>
[HttpPost("cancel/{registrationId}")]
2026-04-07 13:10:11 +08:00
[DisplayName("取消报名")]
2026-03-14 07:11:25 +08:00
public async Task<bool> CancelRegistration(long registrationId)
{
var userId = GetCurrentUserId();
var reg = await _registrationRepository.GetFirstAsync(r => r.Id == registrationId && r.CreatedUserId == userId);
if (reg == null) throw Oops.Bah("报名记录不存在");
// 校验状态:只有待提交、待审核、未支付状态可直接取消
// 已支付状态需走退款流程(暂不实现)
if (reg.PaymentStatus == 1) throw Oops.Bah("已支付订单请联系客服退款");
2026-04-09 23:12:51 +08:00
if (reg.PaymentStatus == 4) throw Oops.Bah("订单已取消,无需重复操作");
2026-03-14 07:11:25 +08:00
2026-04-09 23:12:51 +08:00
reg.PaymentStatus = 4; // 4=已取消
2026-03-14 07:11:25 +08:00
reg.UpdatedTime = DateTime.Now;
reg.UpdatedUserId = userId;
return await _registrationRepository.UpdateAsync(reg);
}
2026-04-04 23:31:38 +08:00
/// <summary>
/// 选择支付方式
/// </summary>
/// <remarks>
/// 用于选择赛事报名的支付方式
/// - 选择微信支付时,必须提供有效的 OpenId
/// - OpenId 的格式验证由 DTO 自动处理
/// </remarks>
/// <param name="input">选择支付方式的输入参数</param>
/// <returns>操作是否成功</returns>
[HttpPost("select-payment-method")]
[UnitOfWork]
2026-04-07 13:10:11 +08:00
[DisplayName("选择支付方式")]
2026-04-04 23:31:38 +08:00
public async Task<bool> 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("订单已支付,无法修改支付方式");
2026-04-09 23:12:51 +08:00
if (reg.PaymentStatus == 4) throw Oops.Bah("订单已取消");
2026-04-04 23:31:38 +08:00
// 如果选择微信支付,验证 OpenId 是否提供
if (input.PaymentMethod == 2)
{
if (string.IsNullOrWhiteSpace(input.OpenId))
{
_logger.LogWarning("选择微信支付时未提供 OpenId,报名ID={RegistrationId}", input.RegistrationId);
throw Oops.Bah("选择微信支付时必须提供 OpenId");
}
}
reg.PaymentMethod = input.PaymentMethod;
reg.UpdatedTime = DateTime.Now;
reg.UpdatedUserId = userId;
_logger.LogInformation("成功修改支付方式,报名ID={RegistrationId},支付方式={PaymentMethod}",
input.RegistrationId, input.PaymentMethod);
return await _registrationRepository.UpdateAsync(reg);
}
/// <summary>
/// 同步支付状态(主动查询微信支付)
/// </summary>
[HttpPost("sync-payment/{registrationId}")]
[UnitOfWork]
2026-04-07 13:10:11 +08:00
[DisplayName("同步支付状态")]
2026-04-04 23:31:38 +08:00
public async Task<bool> SyncPaymentStatus(long registrationId)
{
var userId = GetCurrentUserId();
var reg = await _registrationRepository.GetFirstAsync(r => r.Id == registrationId && r.CreatedUserId == userId);
if (reg == null) throw Oops.Bah("报名记录不存在");
if (reg.PaymentStatus == 1) return true; // 已支付无需同步
var evt = await _eventRepository.GetFirstAsync(e => e.Id == reg.EventId);
if (evt == null) throw Oops.Bah("赛事不存在");
2026-04-07 23:52:35 +08:00
// 根据赛事信息获取对应的商户支付配置
var payConfig = await GetWeChatPayConfigByEventAsync(evt);
2026-04-04 23:31:38 +08:00
if (payConfig == null)
{
2026-04-07 23:52:35 +08:00
_logger.LogError("未找到可用的微信支付配置,赛事ID={EventId},商户ID={MerchantId}", evt.Id, evt.MerchantId);
throw Oops.Bah("未找到可用的微信支付配置,请联系赛事管理员");
2026-04-04 23:31:38 +08:00
}
var queryResult = await _weChatPayService.QueryOrderAsync(payConfig, reg.OrderNo);
if (queryResult.Success && queryResult.TradeState == "SUCCESS")
{
// 更新支付状态
reg.PaymentStatus = 1;
reg.PaymentTime = queryResult.SuccessTime?.DateTime;
reg.RegistrationRemark = queryResult.TransactionId; // 将第三方交易号存储在备注字段
reg.UpdatedTime = DateTime.Now;
reg.UpdatedUserId = userId;
await _registrationRepository.UpdateAsync(reg);
_logger.LogInformation($"订单 {reg.OrderNo} 支付状态同步成功,微信交易号:{queryResult.TransactionId}");
2026-04-09 15:16:43 +08:00
2026-04-09 20:49:36 +08:00
// 创建支付记录
var payment = new EventRegistrationPayment
{
Id = _idGenerator.NewLong(),
RegistrationId = reg.Id,
TransactionNo = queryResult.TransactionId ?? "",
PaymentChannel = 1, // 微信支付
PaymentAmount = reg.TotalAmount,
PaymentTime = queryResult.SuccessTime?.DateTime ?? DateTime.Now,
PaymentStatus = 1, // 已支付
RefundStatus = 0, // 未退款
PayeeAccount = "", // 收款账户
CreatedUserId = userId,
CreatedTime = DateTime.Now
};
await _paymentRepository.InsertAsync(payment);
_logger.LogInformation($"支付记录创建成功,报名ID={reg.Id},交易号={queryResult.TransactionId}");
2026-04-09 15:16:43 +08:00
// 更新 Order 记录状态
var order = await _orderRepository.GetFirstAsync(o => o.OrderNo == reg.OrderNo);
if (order != null)
{
order.OrderStatus = OrderStatus.Paid;
order.PaymentTime = queryResult.SuccessTime?.DateTime;
order.UpdatedTime = DateTime.Now;
await _orderRepository.UpdateAsync(order);
_logger.LogInformation($"Order 记录 {order.OrderNo} 状态已更新为已支付");
}
2026-04-04 23:31:38 +08:00
return true;
}
return false;
}
/// <summary>
/// 微信支付回调通知处理
/// </summary>
2026-04-07 23:52:35 +08:00
/// <remarks>
/// 处理流程:
/// 1. 接收微信支付平台回调通知
/// 2. 使用平台级别的支付配置验证签名和解密数据(微信平台证书是平台级别)
/// 3. 获取订单号后查找报名记录
/// 4. 根据报名记录获取赛事信息
/// 5. 根据赛事信息获取商户级别的支付配置(用于后续可能的退款等操作)
/// 6. 更新订单支付状态
/// </remarks>
/// <returns>微信支付平台要求的响应格式</returns>
2026-04-04 23:31:38 +08:00
[HttpPost("wechat-pay-notify")]
[AllowAnonymous]
[UnitOfWork]
2026-04-07 23:52:35 +08:00
[DisplayName("微信支付回调通知")]
public async Task<IActionResult> WeChatPayNotify()
2026-04-04 23:31:38 +08:00
{
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("微信支付回调通知参数不完整");
2026-04-07 23:52:35 +08:00
return new JsonResult(new { code = "FAIL", message = "参数不完整" });
2026-04-04 23:31:38 +08:00
}
2026-04-07 23:52:35 +08:00
// 第一步:使用平台级别的支付配置验证签名和解密数据
// 注意:微信支付签名验证使用的是微信平台证书(平台级别),所有商户共用
var platformPayConfig = await _weChatPayConfigRepository.GetFirstAsync(x =>
2026-04-04 23:31:38 +08:00
x.Status == AvailabilityStatus.Enable && !x.DeleteMark);
2026-04-07 23:52:35 +08:00
if (platformPayConfig == null)
2026-04-04 23:31:38 +08:00
{
2026-04-07 23:52:35 +08:00
_logger.LogError("未找到平台级别的微信支付配置");
return new JsonResult(new { code = "FAIL", message = "支付配置未找到" });
2026-04-04 23:31:38 +08:00
}
var notifyRequest = new WeChatPayNotifyRequest
{
Timestamp = timestamp,
Nonce = nonce,
Signature = signature,
SerialNumber = serialNumber,
Body = body
};
// 验证签名
2026-04-07 23:52:35 +08:00
var isValid = await _weChatPayService.VerifyPayNotifyAsync(platformPayConfig, notifyRequest);
2026-04-04 23:31:38 +08:00
if (!isValid)
{
_logger.LogWarning("微信支付回调签名验证失败");
2026-04-07 23:52:35 +08:00
return new JsonResult(new { code = "FAIL", message = "签名验证失败" });
2026-04-04 23:31:38 +08:00
}
// 解密通知数据
2026-04-07 23:52:35 +08:00
var notifyData = await _weChatPayService.DecryptPayNotifyDataAsync(platformPayConfig, notifyRequest);
2026-04-04 23:31:38 +08:00
if (notifyData == null || notifyData.TradeState != "SUCCESS")
{
_logger.LogWarning("微信支付回调数据无效或交易未成功");
2026-04-07 23:52:35 +08:00
return new JsonResult(new { code = "SUCCESS", message = "成功" });
2026-04-04 23:31:38 +08:00
}
// 根据商户订单号查找报名记录
var reg = await _registrationRepository.GetFirstAsync(r => r.OrderNo == notifyData.OutTradeNo);
if (reg == null)
{
_logger.LogWarning($"未找到订单:{notifyData.OutTradeNo}");
2026-04-07 23:52:35 +08:00
return new JsonResult(new { code = "SUCCESS", message = "成功" });
2026-04-04 23:31:38 +08:00
}
if (reg.PaymentStatus == 1)
{
_logger.LogInformation($"订单 {reg.OrderNo} 已处理过支付回调");
2026-04-07 23:52:35 +08:00
return new JsonResult(new { code = "SUCCESS", message = "成功" });
2026-04-04 23:31:38 +08:00
}
2026-04-07 23:52:35 +08:00
// 第二步:获取赛事信息并验证商户配置
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);
2026-04-04 23:31:38 +08:00
// 更新支付状态
reg.PaymentStatus = 1;
reg.PaymentTime = notifyData.SuccessTime?.DateTime;
reg.RegistrationRemark = notifyData.TransactionId; // 将第三方交易号存储在备注字段
reg.UpdatedTime = DateTime.Now;
await _registrationRepository.UpdateAsync(reg);
_logger.LogInformation($"订单 {reg.OrderNo} 支付成功,微信交易号:{notifyData.TransactionId}");
2026-04-09 20:49:36 +08:00
// 创建支付记录
var payment = new EventRegistrationPayment
{
Id = _idGenerator.NewLong(),
RegistrationId = reg.Id,
TransactionNo = notifyData.TransactionId ?? "",
PaymentChannel = 1, // 微信支付
PaymentAmount = reg.TotalAmount,
PaymentTime = notifyData.SuccessTime?.DateTime ?? DateTime.Now,
PaymentStatus = 1, // 已支付
RefundStatus = 0, // 未退款
PayeeAccount = "", // 收款账户
CreatedUserId = reg.CreatedUserId,
CreatedTime = DateTime.Now
};
await _paymentRepository.InsertAsync(payment);
_logger.LogInformation($"支付记录创建成功,报名ID={reg.Id},交易号={notifyData.TransactionId}");
2026-04-09 15:16:43 +08:00
// 更新 Order 记录状态
var order = await _orderRepository.GetFirstAsync(o => o.OrderNo == reg.OrderNo);
if (order != null)
{
order.OrderStatus = OrderStatus.Paid;
order.PaymentTime = notifyData.SuccessTime?.DateTime;
order.UpdatedTime = DateTime.Now;
await _orderRepository.UpdateAsync(order);
_logger.LogInformation($"Order 记录 {order.OrderNo} 状态已更新为已支付");
}
2026-04-07 23:52:35 +08:00
return new JsonResult(new { code = "SUCCESS", message = "成功" });
}
catch (ArgumentException ex)
{
_logger.LogError(ex, "处理微信支付回调通知时参数验证失败");
return new JsonResult(new { code = "FAIL", message = "业务验证失败" });
2026-04-04 23:31:38 +08:00
}
catch (Exception ex)
{
_logger.LogError(ex, "处理微信支付回调通知异常");
2026-04-07 23:52:35 +08:00
return new JsonResult(new { code = "FAIL", message = "处理失败" });
2026-04-04 23:31:38 +08:00
}
}
/// <summary>
/// 从请求中获取 OpenId 参数
/// </summary>
/// <remarks>
/// 支持从以下来源获取 OpenId(按优先级顺序):
/// 1. 查询字符串 (QueryString):openId 或 openid 参数
/// 2. 表单数据 (FormData):openId 或 openid 字段
/// 3. 请求体 (JSON):openId 或 openid 属性
/// 4. 用户 Claims(兜底方案):openid claim
/// </remarks>
/// <returns>获取到的 OpenId 值,如果未找到则返回空字符串</returns>
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;
}
/// <summary>
/// 验证 OpenId 参数的有效性
/// </summary>
/// <remarks>
/// 验证规则:
/// 1. OpenId 不能为空或空白字符串
/// 2. OpenId 长度必须在合理范围内(微信 OpenId 通常为 28 个字符左右)
/// 3. OpenId 只能包含字母、数字和下划线
/// </remarks>
/// <param name="openId">待验证的 OpenId</param>
/// <param name="errorMessage">验证失败时的错误信息</param>
/// <returns>验证通过返回 true,否则返回 false</returns>
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;
}
2026-03-14 07:11:25 +08:00
private EventStatus CalculateEventStatus(EventInfo e)
{
var now = DateTime.Now;
2026-04-09 15:16:43 +08:00
if (now < e.RegistrationStartTime) return EventStatus.NotStarted; // 未开始
2026-04-07 13:10:11 +08:00
if (now >= e.RegistrationStartTime && now <= e.RegistrationEndTime) return EventStatus.Enrolling; // 报名中
2026-04-09 15:16:43 +08:00
if (now > e.RegistrationEndTime && now < e.MatchStartTime) return EventStatus.RegistrationEnded; // 报名结束
2026-04-07 13:10:11 +08:00
if (now >= e.MatchStartTime && now <= e.MatchEndTime) return EventStatus.Ongoing; // 比赛中
if (now > e.MatchEndTime) return EventStatus.Ended; // 已结束
2026-04-09 15:16:43 +08:00
return EventStatus.Enrolling; // 默认返回报名中
2026-03-14 07:11:25 +08:00
}
2026-04-09 09:09:50 +08:00
/// <summary>
/// 上传赛事素材
/// </summary>
/// <param name="input">上传素材输入参数</param>
/// <returns>操作是否成功</returns>
[HttpPost("upload-material")]
[DisplayName("上传赛事素材")]
public async Task<bool> UploadMaterial([FromBody] 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("无权操作此报名记录");
2026-04-09 23:12:51 +08:00
if (registration.PaymentStatus == 4) throw Oops.Bah("报名已取消,无法上传素材");
2026-04-09 09:09:50 +08:00
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);
}
2026-04-09 23:45:24 +08:00
/// <summary>
/// 取消报名并申请退款
/// </summary>
[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);
}
/// <summary>
/// 计算退款金额
/// </summary>
private async Task<decimal> 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;
}
/// <summary>
/// 处理退款核心逻辑
/// </summary>
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 (order.PaymentMethod == 2 && weChatPayConfig != null)
{
await CreateWeChatRefundAsync(refundRecord, order, weChatPayConfig, refundAmount, registration, userId);
}
else
{
// 线下支付直接标记退款成功
await CompleteRefundAsync(refundRecord, order, registration, userId);
}
}
/// <summary>
/// 调用微信退款接口
/// </summary>
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;
}
}
/// <summary>
/// 完成退款流程
/// </summary>
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,
PaymentMethod = originalOrder.PaymentMethod,
RegistrationMode = originalOrder.RegistrationMode,
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);
}
}
2026-03-14 07:11:25 +08:00
}