YD_Event.Admin/YD_Event.Application/Event/UserEventRegistrationService.cs
2026-03-25 15:11:26 +08:00

626 lines
26 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 SKIT.FlurlHttpClient.Wechat.TenpayV3;
using SKIT.FlurlHttpClient.Wechat.TenpayV3.Models;
using SKIT.FlurlHttpClient.Wechat.TenpayV3.Settings;
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 AuthManager _authManager;
private readonly IIdGenerator _idGenerator;
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,
AuthManager authManager,
IIdGenerator idGenerator,
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;
_authManager = authManager;
_idGenerator = idGenerator;
_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);
return 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);
}
/// <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("赛事信息不存在");
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,
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)
{
var configOptions = _wechatTenpayClientOptionsMonitor.CurrentValue;
var options = new WechatTenpayClientOptions()
{
MerchantId = configOptions.MerchantId,
MerchantV3Secret = configOptions.MerchantV3Secret,
MerchantCertificateSerialNumber = configOptions.MerchantCertificateSerialNumber,
MerchantCertificatePrivateKey = configOptions.MerchantCertificatePrivateKey
};
var client = new WechatTenpayClient(options);
var request = new CreatePayTransactionNativeRequest()
{
OutTradeNumber = orderNo,
AppId = configOptions.AppId,
Description = $"{eventInfo.EventName} - 报名费",
NotifyUrl = configOptions.NotifyUrl,
Amount = new CreatePayTransactionNativeRequest.Types.Amount()
{
Total = (int)(registration.TotalAmount), // 单位为分
Currency = "CNY"
},
ExpireTime = DateTimeOffset.Now.AddMinutes(5) // 添加支付超时处理机制,默认设置为5分钟
};
var response = await client.ExecuteCreatePayTransactionNativeAsync(request);
if (response.IsSuccessful())
{
result.PayUrl = response.QrcodeUrl ?? string.Empty; // Native支付返回的是二维码链接内容
result.QrcodeUrl = response.QrcodeUrl;
result.TradeType = "NATIVE";
}
else
{
throw Oops.Bah($"微信统一下单失败:{response.ErrorMessage}");
}
}
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 configOptions = _wechatTenpayClientOptionsMonitor.CurrentValue;
var options = new WechatTenpayClientOptions()
{
MerchantId = configOptions.MerchantId,
MerchantV3Secret = configOptions.MerchantV3Secret,
MerchantCertificateSerialNumber = configOptions.MerchantCertificateSerialNumber,
MerchantCertificatePrivateKey = configOptions.MerchantCertificatePrivateKey
};
var client = new WechatTenpayClient(options);
// 验证签名
bool valid = client.VerifyEventSignature(timestamp, nonce, body, signature, serialNumber);
if (!valid)
{
return new BadRequestResult();
}
var callbackModel = client.DeserializeEvent(body);
if (callbackModel.EventType == "TRANSACTION.SUCCESS")
{
var eventData = client.DecryptEventResource<SKIT.FlurlHttpClient.Wechat.TenpayV3.Events.TransactionResource>(callbackModel);
var orderNo = eventData.OutTradeNumber;
var transactionId = eventData.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;
order.ActualAmount = eventData.Amount.Total / 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; // 假设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)
{
var configOptions = _wechatTenpayClientOptionsMonitor.CurrentValue;
var options = new WechatTenpayClientOptions()
{
MerchantId = configOptions.MerchantId,
MerchantV3Secret = configOptions.MerchantV3Secret,
MerchantCertificateSerialNumber = configOptions.MerchantCertificateSerialNumber,
MerchantCertificatePrivateKey = configOptions.MerchantCertificatePrivateKey
};
var client = new WechatTenpayClient(options);
var request = new GetPayTransactionByOutTradeNumberRequest()
{
OutTradeNumber = orderNo,
MerchantId = configOptions.MerchantId
};
var response = await client.ExecuteGetPayTransactionByOutTradeNumberAsync(request);
if (response.IsSuccessful() && response.TradeState == "SUCCESS")
{
// 更新状态
order.OrderStatus = OrderStatus.Paid;
order.PaymentTime = response.SuccessTime?.DateTime;
order.ActualAmount = response.Amount.Total / 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 => "已取消",
_ => "未知"
};
}
}