626 lines
26 KiB
C#
626 lines
26 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 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 => "已取消",
|
||
_ => "未知"
|
||
};
|
||
}
|
||
}
|