YD_Event.Admin/YD_Event.Application/Event/UserEventRegistrationService.cs
2026-03-30 17:06:12 +08:00

665 lines
27 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using YD_Event.Application.Event.Dtos;
using YD_Event.Core.Entities;
using YD_Event.Core.Enum;
using SqlSugar;
using Mapster;
using YD_Event.Application.Auth;
using Furion.DynamicApiController;
using Microsoft.AspNetCore.Mvc;
using YD_Event.Application.File;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Authorization;
using System.ComponentModel;
using Furion.FriendlyException;
using Furion.DatabaseAccessor;
using Furion.DependencyInjection;
using Microsoft.Extensions.Options;
using YD_Event.Core.Infrastructure.WeChat;
using YD_Event.Core.Infrastructure.WeChat.Dto;
using System.IO;
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<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<EventFeeConfig> _feeConfigRepository;
private readonly ISqlSugarRepository<EventRegistrationLimit> _limitRepository;
private readonly ISqlSugarRepository<PointsRule> _pointsRuleRepository;
private readonly ISqlSugarRepository<YD_Event.Core.Entities.Order> _orderRepository;
private readonly ISqlSugarRepository<WeChatPayConfig> _weChatPayConfigRepository;
private readonly ISqlSugarRepository<YD_Event.Core.Entities.Merchant> _merchantRepository;
private readonly ISqlSugarClient _db;
private readonly AuthManager _authManager;
private readonly IIdGenerator _idGenerator;
private readonly WeChatPayService _weChatPayService;
private readonly IOptionsMonitor<YD_Event.Core.Options.WechatTenpayClientOptions> _wechatTenpayClientOptionsMonitor;
private readonly IHttpContextAccessor _httpContextAccessor;
public UserEventRegistrationService(
ISqlSugarRepository<EventInfo> eventRepository,
ISqlSugarRepository<EventUnit> unitRepository,
ISqlSugarRepository<EventProject> eventProjectRepository,
ISqlSugarRepository<YD_Event.Core.Entities.Project> projectRepository,
ISqlSugarRepository<EventRegistration> registrationRepository,
ISqlSugarRepository<EventRegistrationItem> registrationItemRepository,
ISqlSugarRepository<EventRegistrationPerson> registrationPersonRepository,
ISqlSugarRepository<YD_Event.Core.Entities.AppUser> userRepository,
ISqlSugarRepository<EventFeeConfig> feeConfigRepository,
ISqlSugarRepository<EventRegistrationLimit> limitRepository,
ISqlSugarRepository<PointsRule> pointsRuleRepository,
ISqlSugarRepository<YD_Event.Core.Entities.Order> orderRepository,
ISqlSugarRepository<WeChatPayConfig> weChatPayConfigRepository,
ISqlSugarRepository<YD_Event.Core.Entities.Merchant> merchantRepository,
ISqlSugarClient db,
AuthManager authManager,
IIdGenerator idGenerator,
WeChatPayService weChatPayService,
IOptionsMonitor<YD_Event.Core.Options.WechatTenpayClientOptions> wechatTenpayClientOptionsMonitor,
IHttpContextAccessor httpContextAccessor)
{
_eventRepository = eventRepository;
_unitRepository = unitRepository;
_eventProjectRepository = eventProjectRepository;
_projectRepository = projectRepository;
_registrationRepository = registrationRepository;
_registrationItemRepository = registrationItemRepository;
_registrationPersonRepository = registrationPersonRepository;
_userRepository = userRepository;
_feeConfigRepository = feeConfigRepository;
_limitRepository = limitRepository;
_pointsRuleRepository = pointsRuleRepository;
_orderRepository = orderRepository;
_weChatPayConfigRepository = weChatPayConfigRepository;
_merchantRepository = merchantRepository;
_db = db;
_authManager = authManager;
_idGenerator = idGenerator;
_weChatPayService = weChatPayService;
_wechatTenpayClientOptionsMonitor = wechatTenpayClientOptionsMonitor;
_httpContextAccessor = httpContextAccessor;
}
/// <summary>
/// 1. 赛事列表展示接口
/// </summary>
[DisplayName("赛事列表展示")]
[HttpGet("list")]
public async Task<PageResult<UserEventListOutput>> Page([FromQuery] UserEventQueryInput input)
{
var query = _eventRepository.AsQueryable()
.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)
.OrderByDescending(x => x.CreatedTime);
var pageResult = await query.Select(x => new UserEventListOutput
{
Id = x.Id,
Name = x.EventName,
CoverImage = x.CoverImage,
Status = x.EventStatus.ToString(),
Type = x.EventType.ToString(),
RegistrationEndTime = x.RegistrationEndTime,
MatchStartTime = x.MatchStartTime,
MatchEndTime = x.MatchEndTime
}).ToPagedListAsync(input);
var userId = _authManager.UserId;
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;
}
/// <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 projectIds = eventProjects.Select(x => x.ProjectId).Distinct().ToList();
var baseProjects = await _projectRepository.GetListAsync(x => projectIds.Contains(x.Id) && x.Status == AvailabilityStatus.Enable);
output.Projects = eventProjects.Select(ep =>
{
var p = baseProjects.FirstOrDefault(b => b.Id == ep.ProjectId);
return new UserEventProjectListOutput
{
Id = ep.Id,
Name = p?.Name ?? "",
Fee = ep.Price / 100m,
CompletionCount = p?.CompletionCount ?? 0
};
}).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;
output.UserRegistrationStatus = GetRegistrationStatusDesc(registration.RegistrationStatus);
}
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 projectIds = eventProjects.Select(x => x.ProjectId).Distinct().ToList();
var baseProjects = await _projectRepository.GetListAsync(x => projectIds.Contains(x.Id) && x.Status == AvailabilityStatus.Enable);
return eventProjects.Select(ep =>
{
var p = baseProjects.FirstOrDefault(b => b.Id == ep.ProjectId);
return new UserEventProjectListOutput
{
Id = ep.Id,
Name = p?.Name ?? "",
Fee = ep.Price / 100m,
TeamSize = 1, // 默认为1
CompletionCount = p?.CompletionCount ?? 0
};
}).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,
IdCardNumber = MaskIdCard(u.IdCardNumber),
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 user = await _userRepository.GetByIdAsync(userId);
if (user == null) throw Oops.Bah("用户不存在");
var eventInfo = await _eventRepository.GetByIdAsync(input.EventId);
if (eventInfo == null) throw Oops.Bah("赛事不存在");
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,
RegistrationStatus = input.IsDraft ? 0 : 1, // 0-待提交(草稿), 1-待审核
ContactName = user.Name,
ContactPhone = user.Mobile,
OrderNo = "",
RegistrationMode = 1
};
await _registrationRepository.InsertAsync(registration);
}
else
{
registration.RegistrationStatus = input.IsDraft ? 0 : 1;
registration.ContactName = user.Name;
registration.ContactPhone = user.Mobile;
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 eventProjects = await _eventProjectRepository.GetListAsync(x => input.ProjectIds.Contains(x.Id));
var baseProjectIds = eventProjects.Select(x => x.ProjectId).ToList();
var baseProjects = await _projectRepository.GetListAsync(x => baseProjectIds.Contains(x.Id));
var members = await _userRepository.GetListAsync(x => input.MemberIds.Contains(x.Id));
decimal totalAmount = 0;
int totalPersonCount = 0;
foreach (var eventProject in eventProjects)
{
var baseProject = baseProjects.FirstOrDefault(x => x.Id == eventProject.ProjectId);
var item = new EventRegistrationItem
{
Id = _idGenerator.NewLong(),
RegistrationId = registration.Id,
ProjectId = eventProject.Id,
ProjectName = baseProject?.Name ?? "",
UnitPrice = eventProject.Price / 100m,
PersonCount = members.Count(),
TotalPrice = (eventProject.Price / 100m) * members.Count(),
CreatedUserId = userId,
CreatedTime = DateTime.Now
};
await _registrationItemRepository.InsertAsync(item);
totalAmount += item.TotalPrice;
totalPersonCount += item.PersonCount;
foreach (var member in members)
{
var person = new EventRegistrationPerson
{
Id = _idGenerator.NewLong(),
RegistrationId = registration.Id,
RegistrationItemId = item.Id,
ProjectId = eventProject.Id,
Name = member.Name,
Gender = member.Gender,
IdCard = member.IdCardNumber,
Phone = member.Mobile,
Age = member.Age,
BirthDate = member.BirthDate ?? DateTime.MinValue,
PersonType = PersonType.Athlete,
CreatedUserId = userId,
CreatedTime = DateTime.Now
};
await _registrationPersonRepository.InsertAsync(person);
}
}
registration.TotalAmount = totalAmount;
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);
var projectOutputs = items.Select(i => new UserEventProjectListOutput
{
Id = i.ProjectId,
Name = i.ProjectName,
Fee = i.UnitPrice,
RegisteredCount = i.PersonCount
}).ToList();
var memberOutputs = persons.DistinctBy(p => p.IdCard).Select(p => new UserTeamMemberOutput
{
Name = p.Name,
Mobile = p.Phone,
IdCardNumber = MaskIdCard(p.IdCard),
Gender = p.Gender.ToString()
}).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("报名信息不存在");
var eventInfo = await _eventRepository.GetByIdAsync(registration.EventId);
if (eventInfo == null) throw Oops.Bah("赛事信息不存在");
// 获取当前用户的单位认证ID
var certification = await _db.Queryable<MerchantCertification>()
.FirstAsync(c => c.UserId == registration.CreatedUserId && c.AuditStatus == AuditStatus.Pass && !c.DeleteMark);
var orderNo = _idGenerator.NewLong().ToString();
registration.OrderNo = orderNo;
registration.PaymentMethod = input.PaymentMethod;
registration.PaymentStatus = 0; // 未支付
await _registrationRepository.UpdateAsync(registration);
// 创建内部订单记录
var order = new YD_Event.Core.Entities.Order
{
Id = _idGenerator.NewLong(),
OrderNo = orderNo,
OrderType = 1, // Income
PaymentMethod = input.PaymentMethod,
RegistrationMode = registration.RegistrationMode,
MerchantId = eventInfo.MerchantId,
MerchantCertificationId = certification?.Id, // 设置单位认证ID
EventId = registration.EventId,
RegistrationId = registration.Id,
OriginalAmount = registration.TotalAmount,
ActualAmount = registration.TotalAmount,
DiscountAmount = 0,
OrderStatus = OrderStatus.Pending,
CreatedTime = DateTime.Now,
CreatedUserId = registration.CreatedUserId
};
registration.TotalAmount = 1;
await _orderRepository.InsertAsync(order);
var result = new UserPaymentResultOutput
{
OrderNo = orderNo,
Status = 0
};
// 如果是微信支付 (假设 1 是微信支付)
if (input.PaymentMethod == 1)
{
WeChatPayConfig? payConfig = null;
if (eventInfo.MerchantId.HasValue)
{
payConfig = await _weChatPayConfigRepository.GetFirstAsync(x =>
x.MerchantId == eventInfo.MerchantId.Value &&
x.Status == AvailabilityStatus.Enable &&
!x.DeleteMark);
}
if (payConfig == null)
{
throw Oops.Bah("商户微信支付配置不存在");
}
var configOptions = _wechatTenpayClientOptionsMonitor.CurrentValue;
var nativeRequest = new CreateNativePayRequest
{
OutTradeNo = orderNo,
AppId = configOptions.AppId,
Description = $"{eventInfo.EventName} - 报名费",
NotifyUrl = configOptions.NotifyUrl,
TotalAmount = (int)(registration.TotalAmount * 100), // 单位为分
ExpireTime = DateTimeOffset.Now.AddMinutes(5)
};
var payResult = await _weChatPayService.CreateNativePayAsync(payConfig, nativeRequest);
if (payResult.Success)
{
result.PayUrl = payResult.QrcodeUrl ?? string.Empty;
result.QrcodeUrl = payResult.QrcodeUrl;
result.TradeType = payResult.TradeType;
}
else
{
throw Oops.Bah($"微信统一下单失败:{payResult.Message}");
}
}
else
{
result.PayUrl = "http://mock-pay-url.com/" + orderNo;
result.TradeType = "MOCK";
}
return result;
}
/// <summary>
/// 微信支付回调
/// </summary>
[DisplayName("微信支付回调")]
[HttpPost("wechat/notify")]
[AllowAnonymous]
[NonUnify]
public async Task<IActionResult> WechatPayNotify()
{
var request = _httpContextAccessor.HttpContext.Request;
using var reader = new StreamReader(request.Body);
var body = await reader.ReadToEndAsync();
var signature = request.Headers["Wechatpay-Signature"].ToString();
var timestamp = request.Headers["Wechatpay-Timestamp"].ToString();
var nonce = request.Headers["Wechatpay-Nonce"].ToString();
var serialNumber = request.Headers["Wechatpay-Serial"].ToString();
var notifyRequest = new WeChatPayNotifyRequest
{
Body = body,
Signature = signature,
Timestamp = timestamp,
Nonce = nonce,
SerialNumber = serialNumber
};
var payConfig = await _weChatPayConfigRepository.GetFirstAsync(x =>
x.Status == AvailabilityStatus.Enable && !x.DeleteMark);
if (payConfig == null)
{
return new BadRequestObjectResult(new { code = "FAIL", message = "支付配置不存在" });
}
bool valid = await _weChatPayService.VerifyPayNotifyAsync(payConfig, notifyRequest);
if (!valid)
{
return new BadRequestResult();
}
var notifyData = _weChatPayService.DecryptPayNotifyData(payConfig, notifyRequest);
if (notifyData != null && notifyData.EventType == "TRANSACTION.SUCCESS")
{
var orderNo = notifyData.OutTradeNo;
var transactionId = notifyData.TransactionId;
var order = await _orderRepository.GetFirstAsync(x => x.OrderNo == orderNo);
if (order != null && order.OrderStatus == OrderStatus.Pending)
{
order.OrderStatus = OrderStatus.Paid;
order.PaymentTime = DateTime.Now;
if (notifyData.PayerAmount.HasValue)
{
order.ActualAmount = notifyData.PayerAmount.Value / 100m;
}
order.Remark = $"微信支付单号: {transactionId}";
await _orderRepository.UpdateAsync(order);
var registration = await _registrationRepository.GetByIdAsync(order.RegistrationId);
if (registration != null && registration.PaymentStatus != 1)
{
registration.PaymentStatus = 1;
registration.RegistrationStatus = 1;
await _registrationRepository.UpdateAsync(registration);
}
}
}
return new OkObjectResult(new { code = "SUCCESS", message = "成功" });
}
/// <summary>
/// 查询订单支付状态
/// </summary>
[DisplayName("查询订单支付状态")]
[HttpGet("order/status/{orderNo}")]
public async Task<int> QueryOrderStatus([FromRoute] string orderNo)
{
var order = await _orderRepository.GetFirstAsync(x => x.OrderNo == orderNo);
if (order == null) throw Oops.Bah("订单不存在");
if (order.OrderStatus == OrderStatus.Paid)
return 1; // 已支付
if (order.PaymentMethod == 1)
{
WeChatPayConfig? payConfig = null;
if (order.MerchantId.HasValue)
{
payConfig = await _weChatPayConfigRepository.GetFirstAsync(x =>
x.MerchantId == order.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)
{
var queryResult = await _weChatPayService.QueryOrderAsync(payConfig, orderNo);
if (queryResult.Success && queryResult.TradeState == "SUCCESS")
{
order.OrderStatus = OrderStatus.Paid;
order.PaymentTime = queryResult.SuccessTime?.DateTime;
if (queryResult.PayerAmount.HasValue)
{
order.ActualAmount = queryResult.PayerAmount.Value / 100m;
}
await _orderRepository.UpdateAsync(order);
var registration = await _registrationRepository.GetByIdAsync(order.RegistrationId);
if (registration != null && registration.PaymentStatus != 1)
{
registration.PaymentStatus = 1;
registration.RegistrationStatus = 1;
await _registrationRepository.UpdateAsync(registration);
}
return 1;
}
}
}
return 0; // 未支付
}
private string GetRegistrationStatusDesc(int status)
{
return status switch
{
0 => "待提交",
1 => "审核中",
2 => "已报名",
3 => "审核拒绝",
4 => "已取消",
_ => "未知"
};
}
}