YD_Event.Admin/YD_Event.Application/Event/UserEventRegistrationService.cs

1656 lines
69 KiB
C#
Raw Normal View History

2026-03-20 03:15:31 +08:00
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using YD_Event.Application.Event.Dtos;
using YD_Event.Application.Project;
2026-03-20 03:15:31 +08:00
using YD_Event.Core.Entities;
using YD_Event.Core.Enum;
2026-04-06 10:50:57 +08:00
using YD_Event.Core.Domain.Pricing;
2026-04-10 10:54:47 +08:00
using YD_Event.Core.Domain.Pricing.Models;
2026-04-10 22:39:32 +08:00
using YD_Event.Core.Domain;
2026-04-10 10:54:47 +08:00
using Easy.Core;
2026-03-20 03:15:31 +08:00
using SqlSugar;
using Mapster;
using YD_Event.Application.Auth;
using Furion.DynamicApiController;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Authorization;
using System.ComponentModel;
using Furion.FriendlyException;
using Furion.DatabaseAccessor;
using Furion.DependencyInjection;
2026-03-25 11:24:52 +08:00
using Microsoft.Extensions.Options;
2026-03-25 23:58:30 +08:00
using YD_Event.Core.Infrastructure.WeChat;
using YD_Event.Core.Infrastructure.WeChat.Dto;
2026-03-25 11:24:52 +08:00
using System.IO;
2026-04-06 09:21:47 +08:00
using Microsoft.Extensions.Logging;
2026-03-20 03:15:31 +08:00
namespace YD_Event.Application.Event;
/// <summary>
/// 用户端报名管理服务
/// </summary>
public class UserEventRegistrationService : IDynamicApiController, ITransient
{
private readonly ISqlSugarRepository<EventInfo> _eventRepository;
private readonly ISqlSugarRepository<EventUnit> _unitRepository;
private readonly ISqlSugarRepository<EventProject> _eventProjectRepository;
private readonly ISqlSugarRepository<EventProjectGroup> _eventProjectGroupRepository;
private readonly ISqlSugarRepository<ProjectGroup> _projectGroupRepository;
private readonly ISqlSugarRepository<EventProjectRegistrationLimit> _projectLimitRepository;
2026-03-20 03:15:31 +08:00
private readonly ISqlSugarRepository<YD_Event.Core.Entities.Project> _projectRepository;
private readonly ISqlSugarRepository<EventRegistration> _registrationRepository;
private readonly ISqlSugarRepository<EventRegistrationItem> _registrationItemRepository;
private readonly ISqlSugarRepository<EventRegistrationPerson> _registrationPersonRepository;
private readonly ISqlSugarRepository<YD_Event.Core.Entities.AppUser> _userRepository;
private readonly ISqlSugarRepository<SysUser> _sysUserRepository;
2026-03-20 03:15:31 +08:00
private readonly ISqlSugarRepository<EventFeeConfig> _feeConfigRepository;
private readonly ISqlSugarRepository<EventRegistrationLimit> _limitRepository;
private readonly ISqlSugarRepository<PointsRule> _pointsRuleRepository;
2026-03-25 11:24:52 +08:00
private readonly ISqlSugarRepository<YD_Event.Core.Entities.Order> _orderRepository;
2026-03-25 23:58:30 +08:00
private readonly ISqlSugarRepository<WeChatPayConfig> _weChatPayConfigRepository;
private readonly ISqlSugarRepository<YD_Event.Core.Entities.Merchant> _merchantRepository;
2026-03-30 17:06:12 +08:00
private readonly ISqlSugarClient _db;
2026-03-20 03:15:31 +08:00
private readonly AuthManager _authManager;
private readonly IIdGenerator _idGenerator;
2026-03-25 23:58:30 +08:00
private readonly WeChatPayService _weChatPayService;
2026-03-25 11:24:52 +08:00
private readonly IOptionsMonitor<YD_Event.Core.Options.WechatTenpayClientOptions> _wechatTenpayClientOptionsMonitor;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ProjectService _projectService;
2026-04-01 13:04:41 +08:00
private readonly ISqlSugarRepository<YD_Event.Core.Entities.Coupon> _couponRepository;
private readonly ISqlSugarRepository<YD_Event.Core.Entities.UserCoupon> _userCouponRepository;
2026-04-12 13:14:20 +08:00
private readonly ISqlSugarRepository<YD_Event.Core.Entities.CouponRedemptionCode> _redemptionCodeRepository;
2026-04-09 23:12:51 +08:00
private readonly ISqlSugarRepository<EventRegistrationPayment> _paymentRepository;
2026-04-06 09:21:47 +08:00
private readonly ILogger<UserEventRegistrationService> _logger;
2026-04-10 22:39:32 +08:00
private readonly OrderNoGeneratorService _orderNoGeneratorService;
2026-03-20 03:15:31 +08:00
public UserEventRegistrationService(
ISqlSugarRepository<EventInfo> eventRepository,
ISqlSugarRepository<EventUnit> unitRepository,
ISqlSugarRepository<EventProject> eventProjectRepository,
ISqlSugarRepository<EventProjectGroup> eventProjectGroupRepository,
ISqlSugarRepository<ProjectGroup> projectGroupRepository,
ISqlSugarRepository<EventProjectRegistrationLimit> projectLimitRepository,
2026-03-20 03:15:31 +08:00
ISqlSugarRepository<YD_Event.Core.Entities.Project> projectRepository,
ISqlSugarRepository<EventRegistration> registrationRepository,
ISqlSugarRepository<EventRegistrationItem> registrationItemRepository,
ISqlSugarRepository<EventRegistrationPerson> registrationPersonRepository,
ISqlSugarRepository<YD_Event.Core.Entities.AppUser> userRepository,
ISqlSugarRepository<SysUser> sysUserRepository,
2026-03-20 03:15:31 +08:00
ISqlSugarRepository<EventFeeConfig> feeConfigRepository,
ISqlSugarRepository<EventRegistrationLimit> limitRepository,
ISqlSugarRepository<PointsRule> pointsRuleRepository,
2026-03-25 11:24:52 +08:00
ISqlSugarRepository<YD_Event.Core.Entities.Order> orderRepository,
2026-03-25 23:58:30 +08:00
ISqlSugarRepository<WeChatPayConfig> weChatPayConfigRepository,
ISqlSugarRepository<YD_Event.Core.Entities.Merchant> merchantRepository,
2026-03-30 17:06:12 +08:00
ISqlSugarClient db,
2026-03-20 03:15:31 +08:00
AuthManager authManager,
2026-03-25 11:24:52 +08:00
IIdGenerator idGenerator,
2026-03-25 23:58:30 +08:00
WeChatPayService weChatPayService,
2026-03-25 11:24:52 +08:00
IOptionsMonitor<YD_Event.Core.Options.WechatTenpayClientOptions> wechatTenpayClientOptionsMonitor,
IHttpContextAccessor httpContextAccessor,
2026-04-01 13:04:41 +08:00
ProjectService projectService,
ISqlSugarRepository<YD_Event.Core.Entities.Coupon> couponRepository,
2026-04-06 09:21:47 +08:00
ISqlSugarRepository<YD_Event.Core.Entities.UserCoupon> userCouponRepository,
2026-04-12 13:14:20 +08:00
ISqlSugarRepository<YD_Event.Core.Entities.CouponRedemptionCode> redemptionCodeRepository,
2026-04-09 23:12:51 +08:00
ISqlSugarRepository<EventRegistrationPayment> paymentRepository,
2026-04-10 22:39:32 +08:00
ILogger<UserEventRegistrationService> logger,
OrderNoGeneratorService orderNoGeneratorService)
2026-03-20 03:15:31 +08:00
{
_eventRepository = eventRepository;
_unitRepository = unitRepository;
_eventProjectRepository = eventProjectRepository;
_eventProjectGroupRepository = eventProjectGroupRepository;
_projectGroupRepository = projectGroupRepository;
_projectLimitRepository = projectLimitRepository;
2026-03-20 03:15:31 +08:00
_projectRepository = projectRepository;
_registrationRepository = registrationRepository;
_registrationItemRepository = registrationItemRepository;
_registrationPersonRepository = registrationPersonRepository;
_userRepository = userRepository;
_sysUserRepository = sysUserRepository;
2026-03-20 03:15:31 +08:00
_feeConfigRepository = feeConfigRepository;
_limitRepository = limitRepository;
_pointsRuleRepository = pointsRuleRepository;
2026-03-25 11:24:52 +08:00
_orderRepository = orderRepository;
2026-03-25 23:58:30 +08:00
_weChatPayConfigRepository = weChatPayConfigRepository;
_merchantRepository = merchantRepository;
2026-03-30 17:06:12 +08:00
_db = db;
2026-03-20 03:15:31 +08:00
_authManager = authManager;
_idGenerator = idGenerator;
2026-03-25 23:58:30 +08:00
_weChatPayService = weChatPayService;
2026-03-25 11:24:52 +08:00
_wechatTenpayClientOptionsMonitor = wechatTenpayClientOptionsMonitor;
_httpContextAccessor = httpContextAccessor;
_projectService = projectService;
2026-04-01 13:04:41 +08:00
_couponRepository = couponRepository;
_userCouponRepository = userCouponRepository;
2026-04-12 13:14:20 +08:00
_redemptionCodeRepository = redemptionCodeRepository;
2026-04-09 23:12:51 +08:00
_paymentRepository = paymentRepository;
2026-04-06 09:21:47 +08:00
_logger = logger;
2026-04-10 22:39:32 +08:00
_orderNoGeneratorService = orderNoGeneratorService;
2026-03-20 03:15:31 +08:00
}
2026-04-10 14:29:41 +08:00
/// <summary>
/// 根据出生日期计算年龄
/// </summary>
/// <param name="birthDate">出生日期</param>
/// <returns>年龄</returns>
private static int CalculateAge(DateTime? birthDate)
{
if (!birthDate.HasValue) return 0;
var today = DateTime.Today;
var age = today.Year - birthDate.Value.Year;
if (birthDate.Value.Date > today.AddYears(-age)) age--;
return age;
}
2026-03-20 03:15:31 +08:00
/// <summary>
/// 1. 赛事列表展示接口
/// </summary>
[DisplayName("赛事列表展示")]
[HttpGet("list")]
public async Task<PageResult<UserEventListOutput>> Page([FromQuery] UserEventQueryInput input)
{
2026-04-06 09:21:47 +08:00
var pageResult = await _eventRepository.AsQueryable()
2026-03-20 03:15:31 +08:00
.Where(x => x.Status == AvailabilityStatus.Enable)
.WhereIF(!string.IsNullOrWhiteSpace(input.Keyword), x => x.EventName.Contains(input.Keyword))
.WhereIF(input.Status.HasValue && input.Status != 0, x => x.EventStatus == input.Status)
.WhereIF(input.Type.HasValue, x => x.EventType == input.Type)
.WhereIF(input.StartTime.HasValue, x => x.MatchStartTime >= input.StartTime)
.WhereIF(input.EndTime.HasValue, x => x.MatchEndTime <= input.EndTime)
2026-04-06 09:21:47 +08:00
.LeftJoin<EventFeeConfig>((e, fc) => e.Id == fc.EventId)
.OrderByDescending((e, fc) => e.CreatedTime)
.Select((e, fc) => new UserEventListOutput
{
Id = e.Id,
Name = e.EventName,
CoverImage = e.CoverImage,
Status = e.EventStatus.ToString(),
Type = e.EventType.ToString(),
RegistrationType = e.RegistrationType.ToString(),
ChargeMode = fc.ChargeMode.ToString(),
RegistrationStartTime = e.RegistrationStartTime,
RegistrationEndTime = e.RegistrationEndTime,
MatchStartTime = e.MatchStartTime,
MatchEndTime = e.MatchEndTime
}).ToPagedListAsync(input);
var userId = _authManager.UserId;
2026-03-30 17:06:12 +08:00
var certification = await _db.Queryable<MerchantCertification>()
.FirstAsync(c => c.UserId == userId && c.AuditStatus == AuditStatus.Pass && !c.DeleteMark);
var canRegister = certification != null;
foreach (var item in pageResult.Rows)
{
item.CanRegister = canRegister;
}
return pageResult;
2026-03-20 03:15:31 +08:00
}
/// <summary>
/// 2. 赛事详情查看接口
/// </summary>
[DisplayName("赛事详情查看")]
[HttpGet("detail/{id}")]
public async Task<UserEventDetailOutput> Detail([FromRoute] long id)
{
var eventInfo = await _eventRepository.GetByIdAsync(id);
if (eventInfo == null) throw Oops.Bah("赛事不存在");
var output = eventInfo.Adapt<UserEventDetailOutput>();
output.Name = eventInfo.EventName;
output.Description = eventInfo.EventIntroduction;
output.Address = eventInfo.DetailAddress;
output.EventVideo = eventInfo.EventVideo;
output.RegistrationType = eventInfo.RegistrationType.ToString();
output.IsNeedMaterial = eventInfo.IsNeedMaterial;
output.MaterialRemark = eventInfo.MaterialRemark;
output.MaterialUploadEndTime = eventInfo.MaterialUploadEndTime;
if (eventInfo.PointsRuleId.HasValue)
{
var rule = await _pointsRuleRepository.GetByIdAsync(eventInfo.PointsRuleId.Value);
output.PointsRuleName = rule?.RuleName ?? "";
}
var units = await _unitRepository.GetListAsync(x => x.EventId == id);
output.Units = units.OrderBy(x => x.Sort).Select(x => x.Adapt<EventUnitDto>()).ToList();
var limit = await _limitRepository.GetFirstAsync(x => x.EventId == id);
if (limit != null)
{
output.LimitConfig = new UserEventLimitOutput
{
IndividualProjectLimit = limit.IndividualProjectLimit,
TeamProjectLimit = limit.TeamProjectLimit
};
}
var fee = await _feeConfigRepository.GetFirstAsync(x => x.EventId == id);
if (fee != null)
{
output.FeeConfig = new UserEventFeeOutput
{
ChargeMode = fee.ChargeMode.ToString(),
Price = fee.Price
};
}
var eventProjects = await _eventProjectRepository.GetListAsync(x => x.EventId == id);
var eventProjectIds = eventProjects.Select(x => x.Id).ToList();
var eventProjectGroups = await _eventProjectGroupRepository.GetListAsync(x => eventProjectIds.Contains(x.EventProjectId));
var projectLimits = await _projectLimitRepository.GetListAsync(x => eventProjectIds.Contains(x.ProjectId));
2026-03-20 03:15:31 +08:00
var projectIds = eventProjects.Select(x => x.ProjectId).Distinct().ToList();
var baseProjects = await _projectRepository.GetListAsync(x => projectIds.Contains(x.Id) && x.Status == AvailabilityStatus.Enable);
var baseProjectGroups = await _projectGroupRepository.GetListAsync(x => projectIds.Contains(x.ProjectId));
2026-03-20 03:15:31 +08:00
output.Projects = eventProjects.Select(ep =>
{
var p = baseProjects.FirstOrDefault(b => b.Id == ep.ProjectId);
var dto = new UserEventProjectListOutput
2026-03-20 03:15:31 +08:00
{
Id = ep.Id,
Name = p?.Name ?? "",
2026-04-10 14:29:41 +08:00
Fee = ep.Price,
2026-04-09 20:49:36 +08:00
CompletionCount = p?.CompletionCount ?? 0
2026-03-20 03:15:31 +08:00
};
if (p != null)
{
2026-04-12 16:11:20 +08:00
dto.ProjectTypeName = p.ProjectType.Description();
}
var epGroups = eventProjectGroups.Where(g => g.EventProjectId == ep.Id).ToList();
dto.Groups = epGroups.Select(eg =>
{
var baseGroup = baseProjectGroups.FirstOrDefault(bg => bg.Id == eg.ProjectGroupId);
return new UserEventProjectGroupOutput
{
Id = eg.Id,
2026-04-05 20:58:17 +08:00
GroupName = eg.GroupName ?? baseGroup?.GroupName,
BirthDateStart = eg.BirthDateStart != DateTime.MinValue ? eg.BirthDateStart : baseGroup?.BirthDateStart,
BirthDateEnd = eg.BirthDateEnd != DateTime.MinValue ? eg.BirthDateEnd : baseGroup?.BirthDateEnd,
2026-04-06 14:13:43 +08:00
Gender = (int)eg.Gender
};
}).ToList();
var projLimit = projectLimits.FirstOrDefault(l => l.ProjectId == ep.Id);
if (projLimit != null) dto.LimitCount = projLimit.LimitCount;
return dto;
2026-03-20 03:15:31 +08:00
}).Where(x => !string.IsNullOrEmpty(x.Name)).ToList();
var userId = _authManager.UserId;
var registration = await _registrationRepository.GetFirstAsync(x => x.EventId == id && x.CreatedUserId == userId && !x.DeleteMark);
if (registration != null)
{
output.RegistrationId = registration.Id;
2026-04-09 23:12:51 +08:00
output.UserRegistrationStatus = GetPaymentStatusDesc(registration.PaymentStatus);
2026-03-20 03:15:31 +08:00
}
else
{
output.UserRegistrationStatus = "未报名";
}
return output;
}
/// <summary>
/// 3.1 选择报名项目接口
/// </summary>
[DisplayName("选择报名项目")]
[HttpGet("projects/{eventId}")]
public async Task<List<UserEventProjectListOutput>> GetProjects([FromRoute] long eventId)
{
var eventProjects = await _eventProjectRepository.GetListAsync(x => x.EventId == eventId);
var eventProjectIds = eventProjects.Select(x => x.Id).ToList();
var eventProjectGroups = await _eventProjectGroupRepository.GetListAsync(x => eventProjectIds.Contains(x.EventProjectId));
var projectLimits = await _projectLimitRepository.GetListAsync(x => eventProjectIds.Contains(x.ProjectId));
2026-03-20 03:15:31 +08:00
var projectIds = eventProjects.Select(x => x.ProjectId).Distinct().ToList();
var baseProjects = await _projectRepository.GetListAsync(x => projectIds.Contains(x.Id) && x.Status == AvailabilityStatus.Enable);
var baseProjectGroups = await _projectGroupRepository.GetListAsync(x => projectIds.Contains(x.ProjectId));
2026-03-20 03:15:31 +08:00
return eventProjects.Select(ep =>
{
var p = baseProjects.FirstOrDefault(b => b.Id == ep.ProjectId);
var dto = new UserEventProjectListOutput
2026-03-20 03:15:31 +08:00
{
Id = ep.Id,
Name = p?.Name ?? "",
2026-04-10 14:29:41 +08:00
Fee = ep.Price,
TeamSize = 1,
2026-04-09 20:49:36 +08:00
CompletionCount = p?.CompletionCount ?? 0
2026-03-20 03:15:31 +08:00
};
if (p != null)
{
2026-04-12 16:11:20 +08:00
dto.ProjectTypeName = p.ProjectType.Description();
}
var epGroups = eventProjectGroups.Where(g => g.EventProjectId == ep.Id).ToList();
dto.Groups = epGroups.Select(eg =>
{
var baseGroup = baseProjectGroups.FirstOrDefault(bg => bg.Id == eg.ProjectGroupId);
return new UserEventProjectGroupOutput
{
Id = eg.Id,
2026-04-05 20:58:17 +08:00
GroupName = eg.GroupName ?? baseGroup?.GroupName,
BirthDateStart = eg.BirthDateStart != DateTime.MinValue ? eg.BirthDateStart : baseGroup?.BirthDateStart,
BirthDateEnd = eg.BirthDateEnd != DateTime.MinValue ? eg.BirthDateEnd : baseGroup?.BirthDateEnd,
2026-04-06 14:13:43 +08:00
Gender = (int)eg.Gender
};
}).ToList();
var projLimit = projectLimits.FirstOrDefault(l => l.ProjectId == ep.Id);
if (projLimit != null) dto.LimitCount = projLimit.LimitCount;
return dto;
2026-03-20 03:15:31 +08:00
}).Where(x => !string.IsNullOrEmpty(x.Name)).ToList();
}
/// <summary>
/// 3.2 分配队员接口 - 搜索队员
/// </summary>
[DisplayName("搜索队员")]
[HttpGet("members/search")]
public async Task<List<UserTeamMemberOutput>> SearchMembers([FromQuery] string keyword)
{
if (string.IsNullOrWhiteSpace(keyword)) return new List<UserTeamMemberOutput>();
var users = await _userRepository.AsQueryable()
.Where(x => (x.Mobile.Contains(keyword) || x.Id.ToString() == keyword) && !x.DeleteMark)
.Take(10)
.ToListAsync();
return users.Select(u => new UserTeamMemberOutput
{
UserId = u.Id,
Name = u.Name,
Mobile = u.Mobile,
2026-03-25 09:09:45 +08:00
IdCardNumber = MaskIdCard(u.IdCardNumber),
2026-03-20 03:15:31 +08:00
Gender = u.Gender.ToString(),
IsEligible = true,
IneligibilityReason = ""
}).ToList();
}
private string MaskIdCard(string idCard)
{
if (string.IsNullOrEmpty(idCard) || idCard.Length < 10) return idCard;
return idCard.Substring(0, 6) + "********" + idCard.Substring(idCard.Length - 4);
}
/// <summary>
/// 3.2 分配队员接口 - 提交报名(含保存草稿)
/// </summary>
[DisplayName("提交报名")]
[HttpPost("submit")]
[UnitOfWork]
public async Task<long> SubmitRegistration([FromBody] UserSubmitRegistrationInput input)
{
var userId = _authManager.UserId;
var sysUser = await _sysUserRepository.GetByIdAsync(userId);
if (sysUser == null) throw Oops.Bah("用户不存在");
string userName = sysUser.Name;
string userMobile = sysUser.Mobile ?? "";
2026-03-20 03:15:31 +08:00
var eventInfo = await _eventRepository.GetByIdAsync(input.EventId);
if (eventInfo == null) throw Oops.Bah("赛事不存在");
2026-04-05 20:58:17 +08:00
// 验证2:一次报名只能选择单个团队内的运动员
if (input.MemberIds != null && input.MemberIds.Count > 0)
{
2026-04-06 10:50:57 +08:00
// 获取所有队员的团队关系
2026-04-05 20:58:17 +08:00
var teamMembers = await _db.Queryable<TeamMember>()
.Where(tm => input.MemberIds.Contains(tm.UserId))
.Where(tm => !tm.DeleteMark)
.ToListAsync();
if (teamMembers == null || teamMembers.Count == 0)
{
throw Oops.Bah("未找到队员信息");
}
2026-04-06 10:50:57 +08:00
// 按用户ID分组,获取每个用户所属的所有团队
var userTeamMap = teamMembers
.GroupBy(tm => tm.UserId)
.ToDictionary(g => g.Key, g => g.Select(tm => tm.TeamId).ToList());
// 检查是否所有用户都有团队信息
var missingUsers = input.MemberIds.Except(userTeamMap.Keys).ToList();
if (missingUsers.Any())
{
throw Oops.Bah("部分队员未找到团队信息");
}
// 验证所有队员是否都属于指定的 TeamId
var allBelongToTeam = userTeamMap.All(ut => ut.Value.Contains(input.TeamId));
if (!allBelongToTeam)
2026-04-05 20:58:17 +08:00
{
2026-04-06 10:50:57 +08:00
throw Oops.Bah("所有选择的运动员必须隶属于同一个团队");
2026-04-05 20:58:17 +08:00
}
}
2026-03-20 03:15:31 +08:00
2026-04-07 09:04:38 +08:00
// 验证3:调用检查报名限制API,确保所有限制都满足
var checkInput = new RegistrationLimitCheckInput
{
EventId = input.EventId,
MemberIds = input.MemberIds ?? new List<long>(),
GroupIds = input.GroupMembers?.Select(g => g.GroupId).Distinct().ToList() ?? new List<long>()
};
var checkResult = await CheckRegistrationLimit(checkInput);
if (checkResult.HasLimit)
{
var firstLimit = checkResult.LimitDetails.FirstOrDefault();
throw Oops.Bah(firstLimit?.LimitMessage ?? "报名限制检查失败");
}
2026-03-20 03:15:31 +08:00
var registration = await _registrationRepository.GetFirstAsync(x => x.EventId == input.EventId && x.CreatedUserId == userId && !x.DeleteMark);
if (registration == null)
{
registration = new EventRegistration
{
Id = _idGenerator.NewLong(),
EventId = input.EventId,
CreatedUserId = userId,
CreatedTime = DateTime.Now,
Status = AvailabilityStatus.Enable,
2026-04-09 23:12:51 +08:00
PaymentStatus = input.IsDraft ? 0 : 0, // 0-未支付/待审核
ContactName = userName,
ContactPhone = userMobile,
2026-03-20 03:15:31 +08:00
OrderNo = "",
2026-04-07 09:04:38 +08:00
RegistrationMode = 2 // PC端只做团队报名
2026-03-20 03:15:31 +08:00
};
await _registrationRepository.InsertAsync(registration);
}
else
{
2026-04-09 23:12:51 +08:00
registration.PaymentStatus = input.IsDraft ? 0 : 0;
registration.ContactName = userName;
registration.ContactPhone = userMobile;
2026-03-20 03:15:31 +08:00
registration.UpdatedTime = DateTime.Now;
registration.UpdatedUserId = userId;
await _registrationRepository.UpdateAsync(registration);
}
await _registrationItemRepository.DeleteAsync(x => x.RegistrationId == registration.Id);
await _registrationPersonRepository.DeleteAsync(x => x.RegistrationId == registration.Id);
var members = await _userRepository.GetListAsync(x => input.MemberIds.Contains(x.Id));
var groupIds = input.GroupMembers?.Select(g => g.GroupId).Distinct().ToList() ?? new List<long>();
var eventProjectGroups = await _eventProjectGroupRepository.GetListAsync(x => groupIds.Contains(x.Id));
2026-04-01 13:04:41 +08:00
// 通过EventProjectGroup获取对应的EventProject
var eventProjectIds = eventProjectGroups.Select(x => x.EventProjectId).Distinct().ToList();
var eventProjects = await _eventProjectRepository.GetListAsync(x => eventProjectIds.Contains(x.Id));
var baseProjectIds = eventProjects.Select(x => x.ProjectId).Distinct().ToList();
var baseProjects = await _projectRepository.GetListAsync(x => baseProjectIds.Contains(x.Id));
var baseProjectGroupIds = eventProjectGroups.Select(x => x.ProjectGroupId).Distinct().ToList();
var baseProjectGroups = await _projectGroupRepository.GetListAsync(x => baseProjectGroupIds.Contains(x.Id));
2026-04-09 15:16:43 +08:00
// 获取赛事费用配置
var feeConfig = await _feeConfigRepository.GetFirstAsync(x => x.EventId == input.EventId && !x.DeleteMark);
2026-03-20 03:15:31 +08:00
int totalPersonCount = 0;
2026-04-06 09:21:47 +08:00
FeeCalculationResult? feeCalculationResult = null;
2026-03-20 03:15:31 +08:00
if (input.GroupMembers != null && input.GroupMembers.Any())
2026-03-20 03:15:31 +08:00
{
foreach (var groupMember in input.GroupMembers)
2026-03-20 03:15:31 +08:00
{
var eventProjectGroup = eventProjectGroups.FirstOrDefault(g => g.Id == groupMember.GroupId);
if (eventProjectGroup == null) continue;
var eventProject = eventProjects.FirstOrDefault(p => p.Id == eventProjectGroup.EventProjectId);
if (eventProject == null) continue;
var baseProject = baseProjects.FirstOrDefault(x => x.Id == eventProject.ProjectId);
var baseProjectGroup = baseProjectGroups.FirstOrDefault(bg => bg.Id == eventProjectGroup.ProjectGroupId);
var groupMembers = members.Where(m => groupMember.MemberIds.Contains(m.Id)).ToList();
if (!groupMembers.Any()) continue;
2026-03-20 03:15:31 +08:00
2026-04-09 15:16:43 +08:00
// 根据收费模式设置单价和总价
decimal unitPrice = 0;
decimal totalPrice = 0;
switch (feeConfig.ChargeMode)
{
case ChargeMode.Free:
unitPrice = 0;
totalPrice = 0;
break;
case ChargeMode.FixedPrice:
2026-04-09 23:12:51 +08:00
// 定价(赛事)模式:使用赛事费用配置的单价
unitPrice = feeConfig.Price;
totalPrice = unitPrice * groupMembers.Count();
2026-04-09 15:16:43 +08:00
break;
case ChargeMode.UnitPriceEvent:
// 单价(赛事)模式:使用赛事单价
unitPrice = feeConfig.Price;
totalPrice = unitPrice * groupMembers.Count();
break;
case ChargeMode.UnitPriceProject:
// 单价(项目)模式:使用项目单价
2026-04-10 14:29:41 +08:00
unitPrice = eventProject.Price;
2026-04-09 15:16:43 +08:00
totalPrice = unitPrice * groupMembers.Count();
break;
}
var item = new EventRegistrationItem
{
Id = _idGenerator.NewLong(),
RegistrationId = registration.Id,
ProjectId = eventProject.Id,
EventProjectGroupId = eventProjectGroup.Id,
ProjectName = baseProject?.Name ?? "",
GroupName = baseProjectGroup?.GroupName ?? "",
2026-04-12 16:11:20 +08:00
ProjectTypeName = baseProject?.ProjectType.Description() ?? "",
2026-04-09 15:16:43 +08:00
UnitPrice = unitPrice,
PersonCount = groupMembers.Count(),
2026-04-09 15:16:43 +08:00
TotalPrice = totalPrice,
CreatedUserId = userId,
CreatedTime = DateTime.Now
};
await _registrationItemRepository.InsertAsync(item);
totalPersonCount += item.PersonCount;
foreach (var member in groupMembers)
{
var person = new EventRegistrationPerson
{
Id = _idGenerator.NewLong(),
RegistrationId = registration.Id,
RegistrationItemId = item.Id,
ProjectId = eventProject.Id,
EventProjectGroupId = eventProjectGroup.Id,
GroupName = baseProjectGroup?.GroupName ?? "",
ProjectName = baseProject?.Name ?? "",
Name = member.Name,
Gender = member.Gender,
IdCard = member.IdCardNumber,
Phone = member.Mobile,
2026-04-10 14:29:41 +08:00
Age = CalculateAge(member.BirthDate),
BirthDate = member.BirthDate ?? DateTime.MinValue,
PersonType = PersonType.Athlete,
CreatedUserId = userId,
CreatedTime = DateTime.Now
};
await _registrationPersonRepository.InsertAsync(person);
}
}
}
else
{
foreach (var eventProject in eventProjects)
2026-03-20 03:15:31 +08:00
{
var baseProject = baseProjects.FirstOrDefault(x => x.Id == eventProject.ProjectId);
2026-04-09 15:16:43 +08:00
// 根据收费模式设置单价和总价
decimal unitPrice = 0;
decimal totalPrice = 0;
if (feeConfig != null)
{
switch (feeConfig.ChargeMode)
{
case ChargeMode.Free:
unitPrice = 0;
totalPrice = 0;
break;
case ChargeMode.FixedPrice:
2026-04-09 23:12:51 +08:00
// 定价(赛事)模式:使用赛事费用配置的单价
unitPrice = feeConfig.Price;
totalPrice = unitPrice * members.Count();
2026-04-09 15:16:43 +08:00
break;
case ChargeMode.UnitPriceEvent:
// 单价(赛事)模式:使用赛事单价
unitPrice = feeConfig.Price;
totalPrice = unitPrice * members.Count();
break;
case ChargeMode.UnitPriceProject:
// 单价(项目)模式:使用项目单价
2026-04-10 14:29:41 +08:00
unitPrice = eventProject.Price;
2026-04-09 15:16:43 +08:00
totalPrice = unitPrice * members.Count();
break;
}
}
var item = new EventRegistrationItem
2026-03-20 03:15:31 +08:00
{
Id = _idGenerator.NewLong(),
RegistrationId = registration.Id,
ProjectId = eventProject.Id,
ProjectName = baseProject?.Name ?? "",
2026-04-12 16:11:20 +08:00
ProjectTypeName = baseProject?.ProjectType.Description() ?? "",
2026-04-09 15:16:43 +08:00
UnitPrice = unitPrice,
PersonCount = members.Count(),
2026-04-09 15:16:43 +08:00
TotalPrice = totalPrice,
2026-03-20 03:15:31 +08:00
CreatedUserId = userId,
CreatedTime = DateTime.Now
};
await _registrationItemRepository.InsertAsync(item);
totalPersonCount += item.PersonCount;
foreach (var member in members)
{
var person = new EventRegistrationPerson
{
Id = _idGenerator.NewLong(),
RegistrationId = registration.Id,
RegistrationItemId = item.Id,
ProjectId = eventProject.Id,
2026-04-01 13:04:41 +08:00
GroupName = baseProject?.Name ?? "",
ProjectName = baseProject?.Name ?? "",
Name = member.Name,
Gender = member.Gender,
IdCard = member.IdCardNumber,
Phone = member.Mobile,
2026-04-10 14:29:41 +08:00
Age = CalculateAge(member.BirthDate),
BirthDate = member.BirthDate ?? DateTime.MinValue,
PersonType = PersonType.Athlete,
CreatedUserId = userId,
CreatedTime = DateTime.Now
};
await _registrationPersonRepository.InsertAsync(person);
}
2026-03-20 03:15:31 +08:00
}
}
2026-04-06 09:21:47 +08:00
if (totalPersonCount > 0)
{
var feeInput = new FeeCalculationInput
{
EventId = input.EventId,
EventProjectIds = eventProjectIds,
ParticipantCount = totalPersonCount
};
feeCalculationResult = await CalculateFeeAsync(feeInput);
2026-04-06 10:50:57 +08:00
registration.TotalAmount = feeCalculationResult.TotalAmount;
2026-04-06 09:21:47 +08:00
}
2026-03-20 03:15:31 +08:00
registration.TotalPersonCount = totalPersonCount;
await _registrationRepository.UpdateAsync(registration);
return registration.Id;
}
/// <summary>
/// 3.3 预览支付接口
/// </summary>
[DisplayName("预览支付")]
[HttpGet("preview/{registrationId}")]
public async Task<UserOrderPreviewOutput> PreviewOrder([FromRoute] long registrationId)
{
var registration = await _registrationRepository.GetByIdAsync(registrationId);
if (registration == null) throw Oops.Bah("报名信息不存在");
var eventInfo = await _eventRepository.GetByIdAsync(registration.EventId);
var items = await _registrationItemRepository.GetListAsync(x => x.RegistrationId == registrationId);
var persons = await _registrationPersonRepository.GetListAsync(x => x.RegistrationId == registrationId);
2026-04-01 13:04:41 +08:00
var projectOutputs = items.Select(i =>
2026-03-20 03:15:31 +08:00
{
2026-04-01 13:04:41 +08:00
// 获取该项目的队员
var itemMembers = persons
.Where(p => p.RegistrationItemId == i.Id)
.Select(p => new UserTeamMemberOutput
{
UserId = p.Id,
Name = p.Name,
Mobile = p.Phone,
IdCardNumber = MaskIdCard(p.IdCard),
Gender = p.Gender.ToString(),
IsEligible = true,
IneligibilityReason = ""
})
.ToList();
return new UserEventProjectListOutput
{
Id = i.ProjectId,
Name = i.ProjectName,
GroupName = i.GroupName,
ProjectTypeName = i.ProjectTypeName,
Fee = i.UnitPrice,
RegisteredCount = i.PersonCount,
CompletionCount = i.PersonCount,
Members = itemMembers
};
2026-03-20 03:15:31 +08:00
}).ToList();
var memberOutputs = persons.DistinctBy(p => p.IdCard).Select(p => new UserTeamMemberOutput
{
2026-04-01 13:04:41 +08:00
UserId = p.Id,
2026-03-20 03:15:31 +08:00
Name = p.Name,
Mobile = p.Phone,
2026-03-25 09:09:45 +08:00
IdCardNumber = MaskIdCard(p.IdCard),
2026-04-01 13:04:41 +08:00
Gender = p.Gender.ToString(),
IsEligible = true,
IneligibilityReason = ""
2026-03-20 03:15:31 +08:00
}).ToList();
return new UserOrderPreviewOutput
{
EventId = eventInfo.Id,
EventName = eventInfo.EventName,
Projects = projectOutputs,
Members = memberOutputs,
TotalAmount = registration.TotalAmount,
FeeDetails = new Dictionary<string, decimal> { { "报名费", registration.TotalAmount } }
};
}
/// <summary>
/// 创建支付订单
/// </summary>
[DisplayName("创建支付订单")]
[HttpPost("order/create")]
[UnitOfWork]
public async Task<UserPaymentResultOutput> CreateOrder([FromBody] UserCreateOrderInput input)
{
var registration = await _registrationRepository.GetByIdAsync(input.RegistrationId);
if (registration == null) throw Oops.Bah("报名信息不存在");
2026-03-25 11:24:52 +08:00
var eventInfo = await _eventRepository.GetByIdAsync(registration.EventId);
if (eventInfo == null) throw Oops.Bah("赛事信息不存在");
2026-03-20 03:15:31 +08:00
2026-04-01 13:04:41 +08:00
var result = new UserPaymentResultOutput();
// 计算最终支付金额
decimal originalAmount = registration.TotalAmount;
decimal finalAmount = originalAmount;
decimal discountAmount = 0;
YD_Event.Core.Entities.Coupon? coupon = null;
YD_Event.Core.Entities.UserCoupon? userCoupon = null;
// 处理优惠券
if (input.CouponId.HasValue)
{
var userId = _authManager.UserId;
coupon = await _couponRepository.GetFirstAsync(c =>
c.Id == input.CouponId.Value &&
c.Status == AvailabilityStatus.Enable &&
!c.DeleteMark);
if (coupon != null)
{
userCoupon = await _userCouponRepository.GetFirstAsync(uc =>
uc.CouponId == coupon.Id &&
uc.UserId == userId &&
uc.Status == 0 && // 0-未使用
!uc.DeleteMark);
if (userCoupon != null)
{
// 计算折扣金额
if (coupon.Type == 0) // 折扣
{
discountAmount = originalAmount * (1 - coupon.Value / 100);
}
else if (coupon.Type == 1) // 定额
{
discountAmount = Math.Min(coupon.Value, originalAmount);
}
finalAmount = originalAmount - discountAmount;
finalAmount = Math.Max(0, finalAmount);
}
}
}
// 如果最终金额为0,直接完成报名,无需支付
if (finalAmount <= 0)
{
// 如果使用了优惠券,标记为已使用
if (userCoupon != null)
{
userCoupon.Status = 1; // 1-已使用
userCoupon.UseTime = DateTime.Now;
await _userCouponRepository.UpdateAsync(userCoupon);
}
registration.PaymentStatus = 1; // 已支付
registration.PaymentTime = DateTime.Now;
await _registrationRepository.UpdateAsync(registration);
result.Status = 1;
result.IsFree = true;
return result;
}
2026-03-30 17:06:12 +08:00
// 获取当前用户的单位认证ID
var certification = await _db.Queryable<MerchantCertification>()
.FirstAsync(c => c.UserId == registration.CreatedUserId && c.AuditStatus == AuditStatus.Pass && !c.DeleteMark);
2026-04-10 22:39:32 +08:00
var orderNo = await _orderNoGeneratorService.GenerateOrderNoAsync();
2026-03-20 03:15:31 +08:00
registration.OrderNo = orderNo;
registration.PaymentMethod = input.PaymentMethod;
registration.PaymentStatus = 0; // 未支付
await _registrationRepository.UpdateAsync(registration);
2026-03-25 11:24:52 +08:00
// 创建内部订单记录
var order = new YD_Event.Core.Entities.Order
{
Id = _idGenerator.NewLong(),
OrderNo = orderNo,
OrderType = 1, // Income
MerchantId = eventInfo.MerchantId,
2026-03-30 17:06:12 +08:00
MerchantCertificationId = certification?.Id, // 设置单位认证ID
2026-03-25 11:24:52 +08:00
EventId = registration.EventId,
RegistrationId = registration.Id,
2026-04-01 13:04:41 +08:00
OriginalAmount = originalAmount,
ActualAmount = finalAmount,
DiscountAmount = discountAmount,
CouponId = input.CouponId,
2026-03-25 11:24:52 +08:00
OrderStatus = OrderStatus.Pending,
CreatedTime = DateTime.Now,
CreatedUserId = registration.CreatedUserId
};
await _orderRepository.InsertAsync(order);
2026-04-01 13:04:41 +08:00
result.OrderNo = orderNo;
result.Status = 0;
result.IsFree = false;
2026-03-25 11:24:52 +08:00
// 如果是微信支付 (假设 1 是微信支付)
if (input.PaymentMethod == 1)
{
2026-03-25 23:58:30 +08:00
WeChatPayConfig? payConfig = null;
if (eventInfo.MerchantId.HasValue)
2026-03-25 11:24:52 +08:00
{
2026-03-25 23:58:30 +08:00
payConfig = await _weChatPayConfigRepository.GetFirstAsync(x =>
x.MerchantId == eventInfo.MerchantId.Value &&
x.Status == AvailabilityStatus.Enable &&
!x.DeleteMark);
}
if (payConfig == null)
{
throw Oops.Bah("商户微信支付配置不存在");
}
2026-03-25 11:24:52 +08:00
2026-03-25 23:58:30 +08:00
var configOptions = _wechatTenpayClientOptionsMonitor.CurrentValue;
var nativeRequest = new CreateNativePayRequest
2026-03-25 11:24:52 +08:00
{
2026-03-25 23:58:30 +08:00
OutTradeNo = orderNo,
2026-03-25 11:24:52 +08:00
AppId = configOptions.AppId,
Description = $"{eventInfo.EventName} - 报名费",
NotifyUrl = configOptions.NotifyUrl,
2026-04-09 20:49:36 +08:00
TotalAmount = AmountHelper.YuanToFen(finalAmount), // 单位为分,使用优惠后的金额
2026-03-25 23:58:30 +08:00
ExpireTime = DateTimeOffset.Now.AddMinutes(5)
2026-03-25 11:24:52 +08:00
};
2026-03-25 23:58:30 +08:00
var payResult = await _weChatPayService.CreateNativePayAsync(payConfig, nativeRequest);
if (payResult.Success)
2026-03-25 11:24:52 +08:00
{
2026-03-25 23:58:30 +08:00
result.PayUrl = payResult.QrcodeUrl ?? string.Empty;
result.QrcodeUrl = payResult.QrcodeUrl;
result.TradeType = payResult.TradeType;
2026-03-25 11:24:52 +08:00
}
else
{
2026-03-25 23:58:30 +08:00
throw Oops.Bah($"微信统一下单失败:{payResult.Message}");
2026-03-25 11:24:52 +08:00
}
}
else
{
result.PayUrl = "http://mock-pay-url.com/" + orderNo;
result.TradeType = "MOCK";
}
return result;
}
/// <summary>
/// 微信支付回调
/// </summary>
[DisplayName("微信支付回调")]
[HttpPost("wechat/notify")]
[AllowAnonymous]
2026-04-09 23:12:51 +08:00
[UnitOfWork]
2026-03-25 11:24:52 +08:00
[NonUnify]
public async Task<IActionResult> WechatPayNotify()
{
2026-04-09 23:12:51 +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("微信支付回调通知参数不完整");
return new JsonResult(new { code = "FAIL", message = "参数不完整" });
}
2026-03-25 11:24:52 +08:00
2026-04-09 23:12:51 +08:00
var platformPayConfig = await _weChatPayConfigRepository.GetFirstAsync(x =>
x.Status == AvailabilityStatus.Enable && !x.DeleteMark);
if (platformPayConfig == null)
{
_logger.LogError("未找到平台级别的微信支付配置");
return new JsonResult(new { code = "FAIL", message = "支付配置未找到" });
}
2026-03-25 11:24:52 +08:00
2026-04-09 23:12:51 +08:00
var notifyRequest = new WeChatPayNotifyRequest
{
Timestamp = timestamp,
Nonce = nonce,
Signature = signature,
SerialNumber = serialNumber,
Body = body
};
2026-03-25 11:24:52 +08:00
2026-04-09 23:12:51 +08:00
var isValid = await _weChatPayService.VerifyPayNotifyAsync(platformPayConfig, notifyRequest);
if (!isValid)
{
_logger.LogWarning("微信支付回调签名验证失败");
return new JsonResult(new { code = "FAIL", message = "签名验证失败" });
}
2026-03-25 23:58:30 +08:00
2026-04-09 23:12:51 +08:00
var notifyData = await _weChatPayService.DecryptPayNotifyDataAsync(platformPayConfig, notifyRequest);
if (notifyData == null || notifyData.TradeState != "SUCCESS")
{
_logger.LogWarning("微信支付回调数据无效或交易未成功");
return new JsonResult(new { code = "SUCCESS", message = "成功" });
}
2026-03-25 23:58:30 +08:00
2026-04-09 23:12:51 +08:00
var reg = await _registrationRepository.GetFirstAsync(r => r.OrderNo == notifyData.OutTradeNo);
if (reg == null)
{
_logger.LogWarning($"未找到订单:{notifyData.OutTradeNo}");
return new JsonResult(new { code = "SUCCESS", message = "成功" });
}
2026-03-25 11:24:52 +08:00
2026-04-09 23:12:51 +08:00
if (reg.PaymentStatus == 1)
{
_logger.LogInformation($"订单 {reg.OrderNo} 已处理过支付回调");
return new JsonResult(new { code = "SUCCESS", message = "成功" });
}
var evt = await _eventRepository.GetFirstAsync(e => e.Id == reg.EventId);
if (evt == null)
{
_logger.LogError($"未找到赛事信息,订单号:{reg.OrderNo},赛事ID:{reg.EventId}");
return new JsonResult(new { code = "FAIL", message = "赛事信息不存在" });
}
_logger.LogInformation("订单支付成功,订单号={OrderNo},赛事ID={EventId},商户ID={MerchantId}",
reg.OrderNo, evt.Id, evt.MerchantId);
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}");
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-03-25 11:24:52 +08:00
2026-04-09 23:12:51 +08:00
var order = await _orderRepository.GetFirstAsync(o => o.OrderNo == reg.OrderNo);
if (order != null)
2026-03-25 11:24:52 +08:00
{
order.OrderStatus = OrderStatus.Paid;
2026-04-09 23:12:51 +08:00
order.PaymentTime = notifyData.SuccessTime?.DateTime;
order.UpdatedTime = DateTime.Now;
2026-03-25 11:24:52 +08:00
await _orderRepository.UpdateAsync(order);
2026-04-09 23:12:51 +08:00
_logger.LogInformation($"Order 记录 {order.OrderNo} 状态已更新为已支付");
2026-03-25 11:24:52 +08:00
}
2026-04-09 23:12:51 +08:00
return new JsonResult(new { code = "SUCCESS", message = "成功" });
}
catch (ArgumentException ex)
{
_logger.LogError(ex, "处理微信支付回调通知时参数验证失败");
return new JsonResult(new { code = "FAIL", message = "业务验证失败" });
}
catch (Exception ex)
{
_logger.LogError(ex, "处理微信支付回调通知异常");
return new JsonResult(new { code = "FAIL", message = "处理失败" });
}
2026-03-25 11:24:52 +08:00
}
/// <summary>
/// 查询订单支付状态
/// </summary>
[DisplayName("查询订单支付状态")]
[HttpGet("order/status/{orderNo}")]
public async Task<int> QueryOrderStatus([FromRoute] string orderNo)
{
2026-04-10 14:29:41 +08:00
var query = _orderRepository.AsQueryable()
.LeftJoin<EventRegistration>((o, r) => o.RegistrationId == r.Id)
.Where((o, r) => o.OrderNo == orderNo)
.Select((o, r) => new { Order = o, Registration = r });
var orderInfo = await query.FirstAsync();
if (orderInfo == null || orderInfo.Order == null) throw Oops.Bah("订单不存在");
2026-03-25 11:24:52 +08:00
2026-04-10 14:29:41 +08:00
if (orderInfo.Order.OrderStatus == OrderStatus.Paid)
2026-03-25 11:24:52 +08:00
return 1; // 已支付
2026-04-10 14:29:41 +08:00
if (orderInfo.Registration?.PaymentMethod == 1)
2026-03-25 11:24:52 +08:00
{
2026-03-25 23:58:30 +08:00
WeChatPayConfig? payConfig = null;
2026-04-10 14:29:41 +08:00
if (orderInfo.Order.MerchantId.HasValue)
2026-03-25 11:24:52 +08:00
{
2026-03-25 23:58:30 +08:00
payConfig = await _weChatPayConfigRepository.GetFirstAsync(x =>
2026-04-10 14:29:41 +08:00
x.MerchantId == orderInfo.Order.MerchantId.Value &&
2026-03-25 23:58:30 +08:00
x.Status == AvailabilityStatus.Enable &&
!x.DeleteMark);
}
if (payConfig == null)
2026-03-25 11:24:52 +08:00
{
2026-03-25 23:58:30 +08:00
payConfig = await _weChatPayConfigRepository.GetFirstAsync(x =>
x.Status == AvailabilityStatus.Enable && !x.DeleteMark);
}
2026-03-25 11:24:52 +08:00
2026-03-25 23:58:30 +08:00
if (payConfig != null)
{
var queryResult = await _weChatPayService.QueryOrderAsync(payConfig, orderNo);
if (queryResult.Success && queryResult.TradeState == "SUCCESS")
2026-03-25 11:24:52 +08:00
{
2026-04-10 14:29:41 +08:00
orderInfo.Order.OrderStatus = OrderStatus.Paid;
orderInfo.Order.PaymentTime = queryResult.SuccessTime?.DateTime;
2026-03-25 23:58:30 +08:00
if (queryResult.PayerAmount.HasValue)
{
2026-04-10 14:29:41 +08:00
orderInfo.Order.ActualAmount = queryResult.PayerAmount.Value / 100m;
2026-03-25 23:58:30 +08:00
}
2026-04-10 14:29:41 +08:00
await _orderRepository.UpdateAsync(orderInfo.Order);
2026-03-25 23:58:30 +08:00
2026-04-10 14:29:41 +08:00
var registration = await _registrationRepository.GetByIdAsync(orderInfo.Order.RegistrationId);
2026-03-25 23:58:30 +08:00
if (registration != null && registration.PaymentStatus != 1)
{
registration.PaymentStatus = 1;
await _registrationRepository.UpdateAsync(registration);
}
return 1;
2026-03-25 11:24:52 +08:00
}
}
}
return 0; // 未支付
2026-03-20 03:15:31 +08:00
}
2026-04-01 13:04:41 +08:00
/// <summary>
/// 验证优惠券
/// </summary>
[DisplayName("验证优惠券")]
[HttpPost("coupon/validate")]
public async Task<ValidateCouponOutput> ValidateCoupon([FromBody] ValidateCouponInput input)
{
var result = new ValidateCouponOutput
{
IsValid = false,
InvalidReason = ""
};
2026-04-12 13:14:20 +08:00
// 查找兑换码
var redemptionCode = await _redemptionCodeRepository.GetFirstAsync(rc =>
rc.Code == input.CouponCode &&
rc.Status == 0 && // 0-未使用
!rc.DeleteMark);
if (redemptionCode == null)
{
result.InvalidReason = "兑换码不存在或已使用";
return result;
}
// 查找优惠券
2026-04-01 13:04:41 +08:00
var coupon = await _couponRepository.GetFirstAsync(c =>
2026-04-12 13:14:20 +08:00
c.Id == redemptionCode.CouponId &&
2026-04-01 13:04:41 +08:00
c.Status == AvailabilityStatus.Enable &&
!c.DeleteMark);
if (coupon == null)
{
result.InvalidReason = "优惠券不存在或已失效";
return result;
}
// 检查优惠券是否已过期
if (coupon.EndTime.HasValue && coupon.EndTime.Value < DateTime.Now)
{
result.InvalidReason = "优惠券已过期";
return result;
}
// 检查优惠券是否已开始
if (coupon.StartTime.HasValue && coupon.StartTime.Value > DateTime.Now)
{
result.InvalidReason = "优惠券尚未生效";
return result;
}
// 检查库存
if (coupon.TotalQuantity > 0 && coupon.ClaimedQuantity >= coupon.TotalQuantity)
{
result.InvalidReason = "优惠券已被领完";
return result;
}
// 检查优惠券是否适用于该赛事
if (!string.IsNullOrEmpty(coupon.EventIds))
{
var eventIds = coupon.EventIds.Split(',').Select(long.Parse).ToList();
if (!eventIds.Contains(input.EventId))
{
result.InvalidReason = "该优惠券不适用于当前赛事";
return result;
}
}
// 检查当前用户是否拥有该优惠券且未使用,或者可以直接领取
var userId = _authManager.UserId;
var userCoupon = await _userCouponRepository.GetFirstAsync(uc =>
uc.CouponId == coupon.Id &&
uc.UserId == userId &&
uc.Status == 0 && // 0-未使用
!uc.DeleteMark);
// 如果用户没有该优惠券,检查是否可以直接领取(假设可以直接通过兑换码领取)
if (userCoupon == null)
{
// 创建用户优惠券记录
userCoupon = new YD_Event.Core.Entities.UserCoupon
{
Id = _idGenerator.NewLong(),
UserId = userId,
CouponId = coupon.Id,
ClaimTime = DateTime.Now,
Status = 0, // 未使用
2026-04-12 13:14:20 +08:00
RedemptionCodeId = redemptionCode.Id,
RedemptionCode = redemptionCode.Code,
2026-04-01 13:04:41 +08:00
CreatedUserId = userId,
CreatedTime = DateTime.Now,
DeleteMark = false
};
await _userCouponRepository.InsertAsync(userCoupon);
2026-04-12 13:14:20 +08:00
// 更新兑换码状态和关联
redemptionCode.Status = 1; // 已使用
redemptionCode.UsedTime = DateTime.Now;
redemptionCode.UserCouponId = userCoupon.Id;
await _redemptionCodeRepository.UpdateAsync(redemptionCode);
2026-04-01 13:04:41 +08:00
// 更新优惠券已领取数量
coupon.ClaimedQuantity += 1;
await _couponRepository.UpdateAsync(coupon);
}
// 计算优惠金额
decimal discountAmount = 0;
decimal finalAmount = input.OriginalAmount;
// Type: 0-折扣, 1-定额
if (coupon.Type == 0) // 折扣
{
discountAmount = input.OriginalAmount * (1 - coupon.Value / 100);
}
else if (coupon.Type == 1) // 定额
{
discountAmount = Math.Min(coupon.Value, input.OriginalAmount);
}
finalAmount = input.OriginalAmount - discountAmount;
finalAmount = Math.Max(0, finalAmount); // 确保不小于0
result.IsValid = true;
result.CouponId = coupon.Id;
result.CouponName = coupon.Name ?? "优惠券";
result.DiscountType = coupon.Type;
result.DiscountValue = coupon.Value;
result.DiscountAmount = discountAmount;
result.FinalAmount = finalAmount;
return result;
}
2026-04-09 23:12:51 +08:00
private string GetPaymentStatusDesc(int status)
2026-03-20 03:15:31 +08:00
{
return status switch
{
2026-04-09 23:12:51 +08:00
0 => "待审核",
1 => "已支付",
2 => "支付失败",
3 => "已退款",
2026-03-20 03:15:31 +08:00
4 => "已取消",
_ => "未知"
};
}
2026-04-02 08:40:47 +08:00
/// <summary>
/// 验证队员是否符合报名条件
/// </summary>
[DisplayName("验证队员")]
[HttpPost("member/validate")]
public async Task<MemberValidateOutput> ValidateMember([FromBody] MemberValidateInput input)
{
var result = new MemberValidateOutput
{
IsValid = true,
AgeValid = true,
GenderValid = true,
RegistrationLimitValid = true,
Message = "验证通过"
};
var member = await _userRepository.GetByIdAsync(input.MemberId);
if (member == null)
{
result.IsValid = false;
result.Message = "队员不存在";
return result;
}
if (!member.BirthDate.HasValue)
{
result.IsValid = false;
result.AgeValid = false;
result.Message = "队员缺少出生日期信息";
return result;
}
var eventProjectGroup = await _eventProjectGroupRepository.GetByIdAsync(input.GroupId);
if (eventProjectGroup != null)
{
var projectGroup = await _projectGroupRepository.GetByIdAsync(eventProjectGroup.ProjectGroupId);
2026-04-05 20:58:17 +08:00
// 优先使用EventProjectGroup中保存的信息,如果没有则使用原始ProjectGroup的信息
var birthDateStart = eventProjectGroup.BirthDateStart != DateTime.MinValue ? eventProjectGroup.BirthDateStart : (projectGroup?.BirthDateStart ?? DateTime.MinValue);
var birthDateEnd = eventProjectGroup.BirthDateEnd != DateTime.MinValue ? eventProjectGroup.BirthDateEnd : (projectGroup?.BirthDateEnd ?? DateTime.MaxValue);
var gender = eventProjectGroup.Gender;
var birthDate = member.BirthDate.Value;
if (birthDate < birthDateStart || birthDate > birthDateEnd)
2026-04-02 08:40:47 +08:00
{
2026-04-05 20:58:17 +08:00
result.AgeValid = false;
result.IsValid = false;
result.Message = "队员年龄不符合组别要求";
}
2026-04-02 08:40:47 +08:00
2026-04-05 20:58:17 +08:00
if (gender != member.Gender && gender != Gender.Unknown && gender != Gender.All)
{
result.GenderValid = false;
result.IsValid = false;
result.Message = "队员性别不符合组别要求";
2026-04-02 08:40:47 +08:00
}
}
return result;
}
/// <summary>
/// 检查报名限制
/// </summary>
[DisplayName("检查报名限制")]
[HttpPost("registration-limit/check")]
public async Task<RegistrationLimitCheckOutput> CheckRegistrationLimit([FromBody] RegistrationLimitCheckInput input)
{
var result = new RegistrationLimitCheckOutput
{
HasLimit = false,
LimitDetails = new List<RegistrationLimitDetail>()
};
var limit = await _limitRepository.GetFirstAsync(x => x.EventId == input.EventId);
2026-04-07 09:04:38 +08:00
// 获取该赛事的项目级限报配置
var projectLimits = await _projectLimitRepository.GetListAsync(x => x.EventId == input.EventId && !x.DeleteMark);
// 获取所有赛事项目,用于关联
var eventProjects = await _eventProjectRepository.GetListAsync(x => x.EventId == input.EventId);
// 获取所有基础项目信息(用于获取项目名称)
var projectIds = eventProjects.Select(ep => ep.ProjectId).Distinct().ToList();
var projects = await _projectRepository.GetListAsync(x => projectIds.Contains(x.Id) && !x.DeleteMark);
// 获取所有赛事项目组
var eventProjectGroups = await _eventProjectGroupRepository.GetListAsync(x => x.EventId == input.EventId && !x.DeleteMark);
// 检查项目级限报(每个项目下的报名队伍数量)
if (input.GroupIds != null && input.GroupIds.Any())
{
foreach (var groupId in input.GroupIds)
{
var eventProjectGroup = eventProjectGroups.FirstOrDefault(epg => epg.Id == groupId);
if (eventProjectGroup == null) continue;
var eventProject = eventProjects.FirstOrDefault(ep => ep.Id == eventProjectGroup.EventProjectId);
if (eventProject == null) continue;
var projectLimit = projectLimits.FirstOrDefault(pl => pl.ProjectId == eventProject.ProjectId);
if (projectLimit == null) continue;
// 统计该项目下已报名的队伍数量
var currentTeams = await _registrationItemRepository.AsQueryable()
.Where(x => x.EventProjectGroupId == eventProjectGroup.Id && !x.DeleteMark)
.Select(x => x.RegistrationId)
.Distinct()
.CountAsync();
if (currentTeams >= projectLimit.LimitCount)
{
var project = projects.FirstOrDefault(p => p.Id == eventProject.ProjectId);
var projectName = project?.Name ?? "未知项目";
result.HasLimit = true;
result.LimitDetails.Add(new RegistrationLimitDetail
{
MemberId = 0,
MemberName = eventProjectGroup.GroupName ?? "未知组别",
LimitType = "ProjectLimit",
LimitMessage = $"项目\"{projectName}\"的组别\"{eventProjectGroup.GroupName}\"已报名{currentTeams}队,达到限报{projectLimit.LimitCount}队"
});
}
}
}
// 检查个人项目限报和团队项目限报
if (limit != null && input.MemberIds != null && input.MemberIds.Any())
2026-04-02 08:40:47 +08:00
{
var members = await _userRepository.GetListAsync(x => input.MemberIds.Contains(x.Id));
2026-04-07 09:04:38 +08:00
// 先查询该赛事的所有报名记录
var eventRegistrations = await _registrationRepository.GetListAsync(x =>
x.EventId == input.EventId &&
2026-04-09 23:12:51 +08:00
x.PaymentStatus != 4 &&
2026-04-07 09:04:38 +08:00
!x.DeleteMark);
var eventRegistrationIds = eventRegistrations.Select(r => r.Id).ToList();
// 只查询该赛事的报名人员
var registrationPersons = await _registrationPersonRepository.GetListAsync(x =>
eventRegistrationIds.Contains(x.RegistrationId) && !x.DeleteMark);
2026-04-02 08:40:47 +08:00
foreach (var member in members)
{
var matchedPersons = registrationPersons.Where(x => x.IdCard == member.IdCardNumber && !string.IsNullOrEmpty(x.IdCard)).ToList();
var registrationIds = matchedPersons.Select(x => x.RegistrationId).Distinct().ToList();
var registrations = await _registrationRepository.GetListAsync(x =>
registrationIds.Contains(x.Id) &&
x.EventId == input.EventId &&
2026-04-09 23:12:51 +08:00
x.PaymentStatus != 4 &&
2026-04-02 08:40:47 +08:00
!x.DeleteMark);
2026-04-07 09:04:38 +08:00
// 统计该队员已报名的项目数量(不区分个人/团队)
var registeredProjectCount = registrations.Count();
// 检查个人项目限报
if (registeredProjectCount >= limit.IndividualProjectLimit)
2026-04-02 08:40:47 +08:00
{
result.HasLimit = true;
result.LimitDetails.Add(new RegistrationLimitDetail
{
MemberId = member.Id,
MemberName = member.Name ?? member.NickName ?? "未知",
LimitType = "IndividualLimit",
2026-04-07 09:04:38 +08:00
LimitMessage = $"该队员已报名{registeredProjectCount}个项目,达到个人报名限制{limit.IndividualProjectLimit}个"
});
}
// 检查团队项目限报(PC端只做团队报名)
if (registeredProjectCount >= limit.TeamProjectLimit)
{
result.HasLimit = true;
result.LimitDetails.Add(new RegistrationLimitDetail
{
MemberId = member.Id,
MemberName = member.Name ?? member.NickName ?? "未知",
LimitType = "TeamLimit",
LimitMessage = $"该队员已报名{registeredProjectCount}个团队项目,达到团队报名限制{limit.TeamProjectLimit}个"
2026-04-02 08:40:47 +08:00
});
}
}
}
return result;
}
/// <summary>
/// 获取组别可报队伍数量配置
/// </summary>
[DisplayName("获取组别队伍限制")]
[HttpGet("group-team-limit/{eventId}")]
public async Task<List<GroupTeamLimitOutput>> GetGroupTeamLimit([FromRoute] long eventId)
{
var result = new List<GroupTeamLimitOutput>();
var eventProjectGroups = await _eventProjectGroupRepository.GetListAsync(x => x.EventId == eventId && !x.DeleteMark);
2026-04-07 09:04:38 +08:00
// 获取该赛事的项目级限报配置
var projectLimits = await _projectLimitRepository.GetListAsync(x => x.EventId == eventId && !x.DeleteMark);
// 获取所有赛事项目,用于关联 ProjectId
var eventProjects = await _eventProjectRepository.GetListAsync(x => x.EventId == eventId);
2026-04-02 08:40:47 +08:00
foreach (var eventProjectGroup in eventProjectGroups)
{
var currentTeams = await _registrationItemRepository.AsQueryable()
.Where(x => x.EventProjectGroupId == eventProjectGroup.Id && !x.DeleteMark)
.Select(x => x.RegistrationId)
.Distinct()
.CountAsync();
2026-04-07 09:04:38 +08:00
// 查找该组别的项目限报配置
var eventProject = eventProjects.FirstOrDefault(ep => ep.Id == eventProjectGroup.EventProjectId);
var projectLimit = projectLimits.FirstOrDefault(pl => pl.ProjectId == eventProject?.ProjectId);
var maxTeams = projectLimit?.LimitCount ?? 2; // 默认2队
2026-04-02 08:40:47 +08:00
result.Add(new GroupTeamLimitOutput
{
GroupId = eventProjectGroup.Id,
2026-04-07 09:04:38 +08:00
MaxTeams = maxTeams,
2026-04-02 08:40:47 +08:00
CurrentTeams = currentTeams
});
}
return result;
}
/// <summary>
/// 检查项目完成人数
/// </summary>
[DisplayName("检查项目完成人数")]
[HttpPost("project-completion/check")]
public async Task<ProjectCompletionCheckOutput> CheckProjectCompletion([FromBody] ProjectCompletionCheckInput input)
{
var result = new ProjectCompletionCheckOutput
{
HasError = false,
Errors = new List<ProjectCompletionError>()
};
var eventProjects = await _eventProjectRepository.GetListAsync(x => x.EventId == input.EventId);
var eventProjectGroups = await _eventProjectGroupRepository.GetListAsync(x => x.EventId == input.EventId && !x.DeleteMark);
var projects = await _projectRepository.GetListAsync(x => eventProjects.Select(ep => ep.ProjectId).Contains(x.Id) && !x.DeleteMark);
foreach (var eventProject in eventProjects)
{
var project = projects.FirstOrDefault(p => p.Id == eventProject.ProjectId);
if (project == null) continue;
var projectGroups = eventProjectGroups.Where(g => g.EventProjectId == eventProject.Id).ToList();
var assignedGroupIds = input.GroupMembers.Select(g => g.GroupId).ToList();
var assignedProjectGroups = projectGroups.Where(g => assignedGroupIds.Contains(g.Id)).ToList();
if (assignedProjectGroups.Count == 0)
{
continue;
}
var totalAssignedMembers = input.GroupMembers
.Where(g => assignedProjectGroups.Select(pg => pg.Id).Contains(g.GroupId))
.SelectMany(g => g.MemberIds)
.Distinct()
.Count();
if (project.CompletionCount > 0)
{
if (totalAssignedMembers < project.CompletionCount)
{
result.HasError = true;
result.Errors.Add(new ProjectCompletionError
{
ProjectId = eventProject.Id,
ProjectName = project.Name ?? "未知项目",
ErrorType = "Insufficient",
ErrorMessage = $"项目\"{project.Name}\"要求完成人数为{project.CompletionCount}人,当前只分配了{totalAssignedMembers}人"
});
}
else if (totalAssignedMembers > project.CompletionCount)
{
result.HasError = true;
result.Errors.Add(new ProjectCompletionError
{
ProjectId = eventProject.Id,
ProjectName = project.Name ?? "未知项目",
ErrorType = "Excessive",
ErrorMessage = $"项目\"{project.Name}\"要求完成人数为{project.CompletionCount}人,当前分配了{totalAssignedMembers}人,人数超额"
});
}
}
}
return result;
}
2026-04-06 09:21:47 +08:00
/// <summary>
/// 计算赛事报名费用
/// </summary>
/// <param name="input">费用计算输入</param>
/// <returns>费用计算结果</returns>
private async Task<FeeCalculationResult> CalculateFeeAsync(FeeCalculationInput input)
{
_logger.LogInformation("开始计算费用,赛事ID: {EventId}, 项目数: {ProjectCount}, 人数: {ParticipantCount}",
input.EventId, input.EventProjectIds.Count, input.ParticipantCount);
// 获取赛事费用配置
var feeConfig = await _feeConfigRepository.GetFirstAsync(x => x.EventId == input.EventId);
if (feeConfig == null)
{
_logger.LogWarning("未找到赛事费用配置,赛事ID: {EventId}", input.EventId);
2026-04-06 10:50:57 +08:00
var freeResult = new FeeCalculationResult
{
ChargeMode = "免费",
TotalAmount = 0,
TotalPersonCount = input.ParticipantCount
};
freeResult.CalculationLogs.Add($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 未找到费用配置,默认免费");
return freeResult;
2026-04-06 09:21:47 +08:00
}
2026-04-06 10:50:57 +08:00
// 获取项目信息(用于单价项目模式)
2026-04-06 09:21:47 +08:00
var eventProjects = await _eventProjectRepository.GetListAsync(x => input.EventProjectIds.Contains(x.Id));
var projectIds = eventProjects.Select(x => x.ProjectId).Distinct().ToList();
var baseProjects = await _projectRepository.GetListAsync(x => projectIds.Contains(x.Id));
2026-04-06 10:50:57 +08:00
// 构建统一计算服务的输入参数
2026-04-10 10:54:47 +08:00
var pricingInput = new PricingInput
2026-04-06 09:21:47 +08:00
{
2026-04-06 10:50:57 +08:00
ChargeMode = feeConfig.ChargeMode,
2026-04-09 15:16:43 +08:00
ParticipantCount = input.ParticipantCount,
EventPrice = feeConfig.Price
2026-04-06 10:50:57 +08:00
};
2026-04-06 09:21:47 +08:00
2026-04-06 10:50:57 +08:00
// 添加项目信息
foreach (var eventProject in eventProjects)
2026-04-06 09:21:47 +08:00
{
2026-04-06 10:50:57 +08:00
var baseProject = baseProjects.FirstOrDefault(p => p.Id == eventProject.ProjectId);
2026-04-10 10:54:47 +08:00
pricingInput.Projects.Add(new ProjectPricingInfo
2026-04-06 10:50:57 +08:00
{
ProjectId = eventProject.ProjectId,
ProjectName = baseProject?.Name ?? "未知项目",
2026-04-10 14:29:41 +08:00
UnitPrice = eventProject.Price
2026-04-06 10:50:57 +08:00
});
}
2026-04-06 09:21:47 +08:00
2026-04-06 10:50:57 +08:00
// 使用统一计算服务计算费用
var pricingResult = EventPricingService.Calculate(pricingInput);
2026-04-06 09:21:47 +08:00
2026-04-06 10:50:57 +08:00
// 转换为现有格式的结果
var result = new FeeCalculationResult
2026-04-06 09:21:47 +08:00
{
2026-04-06 10:50:57 +08:00
ChargeMode = GetChargeModeDescription(feeConfig.ChargeMode),
TotalAmount = pricingResult.TotalAmount,
TotalPersonCount = pricingResult.TotalPersonCount,
CalculationLogs = pricingResult.CalculationLogs
};
2026-04-06 09:21:47 +08:00
2026-04-06 10:50:57 +08:00
// 转换费用明细
foreach (var detail in pricingResult.Details)
2026-04-06 09:21:47 +08:00
{
result.FeeDetails.Add(new FeeDetailItem
{
2026-04-06 10:50:57 +08:00
ItemName = detail.ItemName,
UnitPrice = detail.UnitPrice,
Quantity = detail.Quantity,
Subtotal = detail.Subtotal,
Remark = detail.Remark
2026-04-06 09:21:47 +08:00
});
}
2026-04-06 10:50:57 +08:00
_logger.LogInformation("费用计算完成,总金额: {TotalAmount:F2} 元", result.TotalAmount);
2026-04-06 09:21:47 +08:00
return result;
}
/// <summary>
/// 获取收费模式描述
/// </summary>
private string GetChargeModeDescription(ChargeMode mode)
{
return mode switch
{
ChargeMode.Free => "免费",
ChargeMode.FixedPrice => "定价(赛事)",
ChargeMode.UnitPriceEvent => "单价(赛事)",
ChargeMode.UnitPriceProject => "单价(项目)",
_ => "未知模式"
};
}
2026-03-20 03:15:31 +08:00
}