2026-03-14 07:11:25 +08:00
|
|
|
|
using Microsoft.AspNetCore.Mvc;
|
2026-04-03 09:42:10 +08:00
|
|
|
|
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-03 09:42:10 +08:00
|
|
|
|
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;
|
2026-04-03 09:42:10 +08:00
|
|
|
|
using System.Text.Json;
|
2026-03-14 07:11:25 +08:00
|
|
|
|
|
|
|
|
|
|
namespace YD_Event.Application.Client;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 小程序-赛事接口
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[ApiDescriptionSettings("微信小程序接口")]
|
2026-04-03 09:42:10 +08:00
|
|
|
|
[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;
|
|
|
|
|
|
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;
|
2026-04-03 09:42:10 +08:00
|
|
|
|
private readonly ISqlSugarRepository<WeChatPayConfig> _weChatPayConfigRepository;
|
|
|
|
|
|
private readonly ISqlSugarRepository<YD_Event.Core.Entities.Merchant> _merchantRepository;
|
|
|
|
|
|
private readonly ISqlSugarRepository<PointsRule> _pointsRuleRepository;
|
|
|
|
|
|
private readonly ISqlSugarRepository<YD_Event.Core.Entities.AppUser> _appUserRepository;
|
2026-04-07 13:10:11 +08:00
|
|
|
|
private readonly ISqlSugarRepository<ProjectType> _projectTypeRepository;
|
2026-03-14 07:11:25 +08:00
|
|
|
|
private readonly IHttpContextAccessor _httpContextAccessor;
|
|
|
|
|
|
private readonly IIdGenerator _idGenerator;
|
2026-04-03 09:42:10 +08:00
|
|
|
|
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,
|
|
|
|
|
|
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,
|
2026-04-03 09:42:10 +08:00
|
|
|
|
ISqlSugarRepository<WeChatPayConfig> weChatPayConfigRepository,
|
|
|
|
|
|
ISqlSugarRepository<YD_Event.Core.Entities.Merchant> merchantRepository,
|
|
|
|
|
|
ISqlSugarRepository<PointsRule> pointsRuleRepository,
|
|
|
|
|
|
ISqlSugarRepository<YD_Event.Core.Entities.AppUser> appUserRepository,
|
2026-04-07 13:10:11 +08:00
|
|
|
|
ISqlSugarRepository<ProjectType> projectTypeRepository,
|
2026-03-14 07:11:25 +08:00
|
|
|
|
IHttpContextAccessor httpContextAccessor,
|
2026-04-03 09:42:10 +08:00
|
|
|
|
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;
|
|
|
|
|
|
_registrationRepository = registrationRepository;
|
|
|
|
|
|
_registrationItemRepository = registrationItemRepository;
|
|
|
|
|
|
_bannerRepository = bannerRepository;
|
|
|
|
|
|
_unitRepository = unitRepository;
|
|
|
|
|
|
_personRepository = personRepository;
|
|
|
|
|
|
_limitRepository = limitRepository;
|
2026-04-04 23:31:38 +08:00
|
|
|
|
_projectLimitRepository = projectLimitRepository;
|
|
|
|
|
|
_feeConfigRepository = feeConfigRepository;
|
2026-04-03 09:42:10 +08:00
|
|
|
|
_weChatPayConfigRepository = weChatPayConfigRepository;
|
|
|
|
|
|
_merchantRepository = merchantRepository;
|
|
|
|
|
|
_pointsRuleRepository = pointsRuleRepository;
|
|
|
|
|
|
_appUserRepository = appUserRepository;
|
2026-04-07 13:10:11 +08:00
|
|
|
|
_projectTypeRepository = projectTypeRepository;
|
2026-03-14 07:11:25 +08:00
|
|
|
|
_httpContextAccessor = httpContextAccessor;
|
|
|
|
|
|
_idGenerator = idGenerator;
|
2026-04-03 09:42:10 +08:00
|
|
|
|
_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-03-18 16:25:49 +08:00
|
|
|
|
var eventProjects = await _eventProjectRepository.AsQueryable()
|
|
|
|
|
|
.Where(ep => ep.EventId == id)
|
|
|
|
|
|
.ToListAsync();
|
2026-04-01 10:12:06 +08:00
|
|
|
|
|
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-04-01 10:12:06 +08:00
|
|
|
|
|
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)
|
2026-04-01 10:12:06 +08:00
|
|
|
|
.Select((p, t) => new {
|
|
|
|
|
|
p.Id,
|
|
|
|
|
|
p.Name,
|
|
|
|
|
|
TypeName = t.Name
|
2026-03-14 07:11:25 +08:00
|
|
|
|
})
|
|
|
|
|
|
.ToListAsync();
|
2026-04-01 10:12:06 +08:00
|
|
|
|
|
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-04-01 10:12:06 +08:00
|
|
|
|
|
2026-03-14 07:11:25 +08:00
|
|
|
|
// 获取限制规则
|
|
|
|
|
|
var limit = await _limitRepository.GetFirstAsync(l => l.EventId == id && !l.DeleteMark);
|
|
|
|
|
|
|
2026-04-03 09:42:10 +08:00
|
|
|
|
// 从 CustomConfigJson 解析退款规则和积分规则
|
|
|
|
|
|
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
|
|
|
|
|
|
{
|
|
|
|
|
|
// 解析失败时使用默认值
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果有积分规则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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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,
|
|
|
|
|
|
Address = $"{evt.Province}{evt.City}{evt.District}{evt.DetailAddress}",
|
|
|
|
|
|
EventVideo = evt.EventVideo,
|
|
|
|
|
|
EventType = evt.EventType,
|
|
|
|
|
|
ChargeMode = evt.ChargeMode,
|
|
|
|
|
|
RegistrationType = evt.RegistrationType,
|
|
|
|
|
|
EventIntroduction = evt.EventIntroduction,
|
|
|
|
|
|
MaterialRemark = evt.MaterialRemark,
|
|
|
|
|
|
MaterialUploadStartTime = evt.MaterialUploadStartTime,
|
|
|
|
|
|
MaterialUploadEndTime = evt.MaterialUploadEndTime,
|
2026-04-03 09:42:10 +08:00
|
|
|
|
RefundRule = refundRule,
|
|
|
|
|
|
PointsRule = pointsRule,
|
2026-04-01 10:12:06 +08:00
|
|
|
|
|
2026-03-14 07:11:25 +08:00
|
|
|
|
IndividualProjectLimit = limit?.IndividualProjectLimit ?? 0,
|
|
|
|
|
|
TeamProjectLimit = limit?.TeamProjectLimit ?? 0,
|
2026-04-01 10:12:06 +08:00
|
|
|
|
|
|
|
|
|
|
Units = units.Select(u => new EventUnitDto
|
2026-03-14 07:11:25 +08:00
|
|
|
|
{
|
|
|
|
|
|
UnitType = u.UnitType,
|
|
|
|
|
|
UnitName = u.UnitName,
|
|
|
|
|
|
LogoUrl = u.LogoUrl
|
|
|
|
|
|
}).ToList(),
|
|
|
|
|
|
|
2026-04-01 10:12:06 +08:00
|
|
|
|
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();
|
|
|
|
|
|
|
|
|
|
|
|
decimal totalAmount = 0;
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
decimal unitPrice = 0;
|
|
|
|
|
|
decimal subtotal = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// 根据收费模式计算金额
|
|
|
|
|
|
switch (evt.ChargeMode)
|
|
|
|
|
|
{
|
|
|
|
|
|
case ChargeMode.FixedPrice:
|
|
|
|
|
|
// 定价(赛事):整个赛事一个固定价格,每人一份
|
|
|
|
|
|
if (feeConfig != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
unitPrice = feeConfig.Price;
|
|
|
|
|
|
subtotal = unitPrice * item.PersonCount;
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case ChargeMode.UnitPriceEvent:
|
|
|
|
|
|
// 单价(赛事):按人数收费
|
|
|
|
|
|
if (feeConfig != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
unitPrice = feeConfig.Price;
|
|
|
|
|
|
subtotal = unitPrice * item.PersonCount;
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case ChargeMode.UnitPriceProject:
|
|
|
|
|
|
// 单价(项目):每个项目有自己的价格
|
|
|
|
|
|
unitPrice = ep.Price / 100m;
|
|
|
|
|
|
subtotal = unitPrice * item.PersonCount;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
result.ItemDetails.Add(new CalculateAmountItemOutput
|
|
|
|
|
|
{
|
|
|
|
|
|
ProjectId = item.ProjectId,
|
|
|
|
|
|
ProjectName = bp?.Name ?? "",
|
|
|
|
|
|
UnitPrice = unitPrice,
|
|
|
|
|
|
PersonCount = item.PersonCount,
|
|
|
|
|
|
Subtotal = subtotal
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
totalAmount += subtotal;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
result.TotalAmount = totalAmount;
|
|
|
|
|
|
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("不在报名时间内");
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 校验重复报名 (同一赛事同一人只能报一次?还是可以多次?通常一次报名包含多个项目)
|
|
|
|
|
|
// 假设同一用户对同一赛事只能有一个“未取消”的报名记录
|
|
|
|
|
|
var exist = await _registrationRepository.IsAnyAsync(r => r.EventId == input.EventId && r.CreatedUserId == userId && r.RegistrationStatus != 4); // 4=已取消
|
|
|
|
|
|
if (exist) throw Oops.Bah("您已报名该赛事,请勿重复提交");
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 校验项目有效性 & 限制
|
|
|
|
|
|
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)
|
|
|
|
|
|
.Where((ri, r) => ri.ProjectId == item.ProjectId && r.RegistrationStatus != 4)
|
|
|
|
|
|
.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-03-14 07:11:25 +08:00
|
|
|
|
|
|
|
|
|
|
// 4. 创建报名主表
|
|
|
|
|
|
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,
|
|
|
|
|
|
RegistrationStatus = 0, // 待提交/待支付
|
|
|
|
|
|
PaymentStatus = 0, // 未支付
|
|
|
|
|
|
RegistrationTime = DateTime.Now,
|
|
|
|
|
|
CreatedUserId = userId,
|
|
|
|
|
|
CreatedTime = DateTime.Now,
|
|
|
|
|
|
Status = AvailabilityStatus.Enable,
|
|
|
|
|
|
TotalPersonCount = 0 // 稍后计算
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 5. 创建报名明细 & 参赛人员 & 计算费用
|
|
|
|
|
|
decimal totalAmount = 0;
|
|
|
|
|
|
int totalPerson = 0;
|
|
|
|
|
|
var regItems = new List<EventRegistrationItem>();
|
|
|
|
|
|
var persons = new List<EventRegistrationPerson>();
|
|
|
|
|
|
|
|
|
|
|
|
foreach (var item in input.Items)
|
|
|
|
|
|
{
|
2026-03-18 16:25:49 +08:00
|
|
|
|
var eventProject = await _eventProjectRepository.GetFirstAsync(p => p.Id == item.ProjectId);
|
|
|
|
|
|
if (eventProject == null) throw Oops.Bah($"项目不存在: {item.ProjectId}");
|
|
|
|
|
|
|
|
|
|
|
|
var project = await _projectRepository.GetFirstAsync(p => p.Id == eventProject.ProjectId);
|
|
|
|
|
|
if (project == null) throw Oops.Bah($"关联基础项目不存在: {item.ProjectId}");
|
2026-03-14 07:11:25 +08:00
|
|
|
|
|
|
|
|
|
|
// 校验组别
|
|
|
|
|
|
// 略,假设前端传参正确,或需根据 GroupId 查
|
|
|
|
|
|
|
|
|
|
|
|
if (item.Persons == null || !item.Persons.Any())
|
|
|
|
|
|
throw Oops.Bah($"项目[{project.Name}]未填写参赛人信息");
|
|
|
|
|
|
|
|
|
|
|
|
var regItemId = _idGenerator.NewLong();
|
|
|
|
|
|
var regItem = new EventRegistrationItem
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = regItemId,
|
|
|
|
|
|
RegistrationId = registrationId,
|
|
|
|
|
|
ProjectId = item.ProjectId,
|
|
|
|
|
|
ProjectName = project.Name,
|
|
|
|
|
|
GroupName = item.GroupName,
|
2026-03-18 16:25:49 +08:00
|
|
|
|
UnitPrice = eventProject.Price / 100m,
|
2026-03-14 07:11:25 +08:00
|
|
|
|
PersonCount = item.Persons.Count,
|
2026-03-18 16:25:49 +08:00
|
|
|
|
TotalPrice = (eventProject.Price / 100m) * item.Persons.Count,
|
2026-03-14 07:11:25 +08:00
|
|
|
|
CreatedUserId = userId,
|
|
|
|
|
|
CreatedTime = DateTime.Now
|
|
|
|
|
|
};
|
|
|
|
|
|
regItems.Add(regItem);
|
|
|
|
|
|
totalAmount += regItem.TotalPrice;
|
|
|
|
|
|
totalPerson += regItem.PersonCount;
|
|
|
|
|
|
|
|
|
|
|
|
// 添加参赛人员
|
|
|
|
|
|
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-03-14 07:11:25 +08:00
|
|
|
|
GroupName = item.GroupName,
|
|
|
|
|
|
ProjectName = project.Name,
|
|
|
|
|
|
CreatedUserId = userId,
|
|
|
|
|
|
CreatedTime = DateTime.Now
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
registration.TotalAmount = totalAmount;
|
|
|
|
|
|
registration.TotalPersonCount = totalPerson;
|
2026-04-03 09:42:10 +08:00
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
|
|
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("报名记录不存在");
|
|
|
|
|
|
|
2026-04-03 09:42:10 +08:00
|
|
|
|
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);
|
2026-04-03 09:42:10 +08:00
|
|
|
|
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
|
|
|
|
};
|
2026-04-03 09:42:10 +08:00
|
|
|
|
|
|
|
|
|
|
if (reg.PaymentMethod == 2 && reg.TotalAmount > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
WeChatPayConfig? payConfig = null;
|
|
|
|
|
|
if (evt.MerchantId.HasValue)
|
|
|
|
|
|
{
|
|
|
|
|
|
payConfig = await _weChatPayConfigRepository.GetFirstAsync(x =>
|
|
|
|
|
|
x.MerchantId == evt.MerchantId.Value &&
|
|
|
|
|
|
x.Status == AvailabilityStatus.Enable &&
|
|
|
|
|
|
!x.DeleteMark);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (payConfig == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
payConfig = await _weChatPayConfigRepository.GetFirstAsync(x =>
|
|
|
|
|
|
x.Status == AvailabilityStatus.Enable && !x.DeleteMark);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (payConfig != null)
|
|
|
|
|
|
{
|
2026-04-04 23:31:38 +08:00
|
|
|
|
_logger.LogInformation("开始创建微信 JSAPI 支付订单,订单号={OrderNo},OpenId={OpenId}", reg.OrderNo, input.OpenId);
|
|
|
|
|
|
|
2026-04-03 09:42:10 +08:00
|
|
|
|
var jsapiRequest = new CreateJsApiPayRequest
|
|
|
|
|
|
{
|
|
|
|
|
|
OutTradeNo = reg.OrderNo,
|
|
|
|
|
|
AppId = configOptions.AppId,
|
|
|
|
|
|
Description = $"{evt.EventName} - 报名费",
|
|
|
|
|
|
NotifyUrl = configOptions.NotifyUrl,
|
|
|
|
|
|
TotalAmount = (int)(reg.TotalAmount * 100),
|
|
|
|
|
|
ExpireTime = DateTimeOffset.Now.AddMinutes(30),
|
2026-04-04 23:31:38 +08:00
|
|
|
|
OpenId = input.OpenId
|
2026-04-03 09:42:10 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
var payResult = await _weChatPayService.CreateJsApiPayAsync(payConfig, jsapiRequest);
|
|
|
|
|
|
if (payResult.Success && payResult.JsApiParameters != null)
|
|
|
|
|
|
{
|
2026-04-04 23:31:38 +08:00
|
|
|
|
_logger.LogInformation("微信 JSAPI 支付下单成功,订单号={OrderNo}", reg.OrderNo);
|
2026-04-03 09:42:10 +08:00
|
|
|
|
paymentInfo.WechatPayParams = new
|
|
|
|
|
|
{
|
|
|
|
|
|
appId = payResult.JsApiParameters.AppId,
|
|
|
|
|
|
timeStamp = payResult.JsApiParameters.TimeStamp,
|
|
|
|
|
|
nonceStr = payResult.JsApiParameters.NonceStr,
|
|
|
|
|
|
package = payResult.JsApiParameters.Package,
|
|
|
|
|
|
signType = payResult.JsApiParameters.SignType,
|
|
|
|
|
|
paySign = payResult.JsApiParameters.PaySign
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
2026-04-04 23:31:38 +08:00
|
|
|
|
_logger.LogError("微信 JSAPI 支付下单失败:订单号={OrderNo},错误信息={Message}", reg.OrderNo, payResult.Message);
|
|
|
|
|
|
throw Oops.Bah($"微信支付下单失败:{payResult.Message}");
|
2026-04-03 09:42:10 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-04 23:31:38 +08:00
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
_logger.LogError("未找到可用的微信支付配置,赛事ID={EventId}", evt.Id);
|
|
|
|
|
|
throw Oops.Bah("未找到可用的微信支付配置");
|
|
|
|
|
|
}
|
2026-04-03 09:42:10 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return paymentInfo;
|
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;
|
|
|
|
|
|
reg.RegistrationStatus = 1; // 待审核
|
|
|
|
|
|
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)
|
|
|
|
|
|
.Where(r => r.CreatedUserId == userId && !r.DeleteMark)
|
|
|
|
|
|
.OrderByDescending(r => r.RegistrationTime)
|
|
|
|
|
|
.Select((r, e) => new MyEventListDto
|
|
|
|
|
|
{
|
|
|
|
|
|
RegistrationId = r.Id,
|
|
|
|
|
|
EventId = r.EventId,
|
|
|
|
|
|
EventName = e.EventName,
|
|
|
|
|
|
CoverImage = e.CoverImage,
|
|
|
|
|
|
RegistrationStatus = r.RegistrationStatus,
|
|
|
|
|
|
PaymentStatus = r.PaymentStatus,
|
|
|
|
|
|
RegistrationTime = r.RegistrationTime,
|
|
|
|
|
|
ProjectCount = SqlFunc.Subqueryable<EventRegistrationItem>().Where(i => i.RegistrationId == r.Id).Count()
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
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.RegistrationStatus,
|
|
|
|
|
|
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,
|
|
|
|
|
|
RegistrationStatus = reg.RegistrationStatus,
|
|
|
|
|
|
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,
|
|
|
|
|
|
RegistrationStatus = reg.RegistrationStatus,
|
|
|
|
|
|
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("已支付订单请联系客服退款");
|
|
|
|
|
|
if (reg.RegistrationStatus == 4) throw Oops.Bah("订单已取消,无需重复操作");
|
|
|
|
|
|
|
|
|
|
|
|
reg.RegistrationStatus = 4; // 4=已取消
|
|
|
|
|
|
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("订单已支付,无法修改支付方式");
|
|
|
|
|
|
if (reg.RegistrationStatus == 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");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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("赛事不存在");
|
|
|
|
|
|
|
|
|
|
|
|
WeChatPayConfig? payConfig = null;
|
|
|
|
|
|
if (evt.MerchantId.HasValue)
|
|
|
|
|
|
{
|
|
|
|
|
|
payConfig = await _weChatPayConfigRepository.GetFirstAsync(x =>
|
|
|
|
|
|
x.MerchantId == evt.MerchantId.Value &&
|
|
|
|
|
|
x.Status == AvailabilityStatus.Enable &&
|
|
|
|
|
|
!x.DeleteMark);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (payConfig == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
payConfig = await _weChatPayConfigRepository.GetFirstAsync(x =>
|
|
|
|
|
|
x.Status == AvailabilityStatus.Enable && !x.DeleteMark);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (payConfig == null) throw Oops.Bah("微信支付配置未设置");
|
|
|
|
|
|
|
|
|
|
|
|
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.RegistrationStatus = 1; // 待审核
|
|
|
|
|
|
reg.UpdatedTime = DateTime.Now;
|
|
|
|
|
|
reg.UpdatedUserId = userId;
|
|
|
|
|
|
|
|
|
|
|
|
await _registrationRepository.UpdateAsync(reg);
|
|
|
|
|
|
_logger.LogInformation($"订单 {reg.OrderNo} 支付状态同步成功,微信交易号:{queryResult.TransactionId}");
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 微信支付回调通知处理
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[HttpPost("wechat-pay-notify")]
|
|
|
|
|
|
[AllowAnonymous]
|
|
|
|
|
|
[UnitOfWork]
|
|
|
|
|
|
[NonAction]
|
|
|
|
|
|
public async Task<object> 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 { code = "FAIL", message = "参数不完整" };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取默认支付配置(后续可根据订单号找到对应的商户配置)
|
|
|
|
|
|
var payConfig = await _weChatPayConfigRepository.GetFirstAsync(x =>
|
|
|
|
|
|
x.Status == AvailabilityStatus.Enable && !x.DeleteMark);
|
|
|
|
|
|
if (payConfig == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
_logger.LogError("微信支付配置未找到");
|
|
|
|
|
|
return new { code = "FAIL", message = "支付配置未找到" };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var notifyRequest = new WeChatPayNotifyRequest
|
|
|
|
|
|
{
|
|
|
|
|
|
Timestamp = timestamp,
|
|
|
|
|
|
Nonce = nonce,
|
|
|
|
|
|
Signature = signature,
|
|
|
|
|
|
SerialNumber = serialNumber,
|
|
|
|
|
|
Body = body
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 验证签名
|
|
|
|
|
|
var isValid = await _weChatPayService.VerifyPayNotifyAsync(payConfig, notifyRequest);
|
|
|
|
|
|
if (!isValid)
|
|
|
|
|
|
{
|
|
|
|
|
|
_logger.LogWarning("微信支付回调签名验证失败");
|
|
|
|
|
|
return new { code = "FAIL", message = "签名验证失败" };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 解密通知数据
|
|
|
|
|
|
var notifyData = await _weChatPayService.DecryptPayNotifyDataAsync(payConfig, notifyRequest);
|
|
|
|
|
|
if (notifyData == null || notifyData.TradeState != "SUCCESS")
|
|
|
|
|
|
{
|
|
|
|
|
|
_logger.LogWarning("微信支付回调数据无效或交易未成功");
|
|
|
|
|
|
return new { code = "SUCCESS", message = "成功" };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 根据商户订单号查找报名记录
|
|
|
|
|
|
var reg = await _registrationRepository.GetFirstAsync(r => r.OrderNo == notifyData.OutTradeNo);
|
|
|
|
|
|
if (reg == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
_logger.LogWarning($"未找到订单:{notifyData.OutTradeNo}");
|
|
|
|
|
|
return new { code = "SUCCESS", message = "成功" };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (reg.PaymentStatus == 1)
|
|
|
|
|
|
{
|
|
|
|
|
|
_logger.LogInformation($"订单 {reg.OrderNo} 已处理过支付回调");
|
|
|
|
|
|
return new { code = "SUCCESS", message = "成功" };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新支付状态
|
|
|
|
|
|
reg.PaymentStatus = 1;
|
|
|
|
|
|
reg.PaymentTime = notifyData.SuccessTime?.DateTime;
|
|
|
|
|
|
reg.RegistrationRemark = notifyData.TransactionId; // 将第三方交易号存储在备注字段
|
|
|
|
|
|
reg.RegistrationStatus = 1; // 待审核
|
|
|
|
|
|
reg.UpdatedTime = DateTime.Now;
|
|
|
|
|
|
|
|
|
|
|
|
await _registrationRepository.UpdateAsync(reg);
|
|
|
|
|
|
_logger.LogInformation($"订单 {reg.OrderNo} 支付成功,微信交易号:{notifyData.TransactionId}");
|
|
|
|
|
|
|
|
|
|
|
|
return new { code = "SUCCESS", message = "成功" };
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
_logger.LogError(ex, "处理微信支付回调通知异常");
|
|
|
|
|
|
return new { code = "FAIL", message = "处理失败" };
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <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-07 13:10:11 +08:00
|
|
|
|
if (now >= e.RegistrationStartTime && now <= e.RegistrationEndTime) return EventStatus.Enrolling; // 报名中
|
|
|
|
|
|
if (now >= e.MatchStartTime && now <= e.MatchEndTime) return EventStatus.Ongoing; // 比赛中
|
|
|
|
|
|
if (now > e.MatchEndTime) return EventStatus.Ended; // 已结束
|
|
|
|
|
|
return EventStatus.Enrolling; // 默认? 或者新增未开始状态
|
2026-03-14 07:11:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|