diff --git a/YD_Event.Application/AppUser/Dtos/AppUserDtos.cs b/YD_Event.Application/AppUser/Dtos/AppUserDtos.cs index 93bd129..c5ba7a0 100644 --- a/YD_Event.Application/AppUser/Dtos/AppUserDtos.cs +++ b/YD_Event.Application/AppUser/Dtos/AppUserDtos.cs @@ -177,6 +177,11 @@ public class CreateOrUpdateMemberInput /// public string? IdCardImageUrl { get; set; } + /// + /// 身份证背面图片URL + /// + public string? IdCardBackImageUrl { get; set; } + /// /// 护照图片URL /// @@ -273,6 +278,11 @@ public class AppUserOutput /// public string? IdCardImageUrl { get; set; } + /// + /// 身份证背面图片URL + /// + public string? IdCardBackImageUrl { get; set; } + /// /// 护照图片URL /// diff --git a/YD_Event.Application/Client/Dtos/MyDtos.cs b/YD_Event.Application/Client/Dtos/MyDtos.cs index 0601940..5d4c480 100644 --- a/YD_Event.Application/Client/Dtos/MyDtos.cs +++ b/YD_Event.Application/Client/Dtos/MyDtos.cs @@ -102,6 +102,11 @@ public class MyAuthInfoDto /// public string? IdCardImageUrl { get; set; } + /// + /// 身份证背面图片(认证类型1时使用) + /// + public string? IdCardBackImageUrl { get; set; } + /// /// 护照图片(认证类型2时使用) /// @@ -171,6 +176,12 @@ public class SubmitAuthInput [MaxLength(500, ErrorMessage = "图片URL不能超过500个字符")] public string? IdCardImageUrl { get; set; } + /// + /// 身份证背面图片(认证类型1时必填) + /// + [MaxLength(500, ErrorMessage = "图片URL不能超过500个字符")] + public string? IdCardBackImageUrl { get; set; } + /// /// 护照图片(认证类型2时必填) /// diff --git a/YD_Event.Application/Client/MyController.cs b/YD_Event.Application/Client/MyController.cs index 172aaf7..57aaf7f 100644 --- a/YD_Event.Application/Client/MyController.cs +++ b/YD_Event.Application/Client/MyController.cs @@ -87,25 +87,7 @@ public class MyController : IDynamicApiController var pointsBalance = (int)(pointsIn - pointsOut); // 认证状态: 0-待提交, 1-已认证, 2-审核中, 3-失败 - int authStatus = 0; - bool isAuthInfoSubmitted = !string.IsNullOrEmpty(user.Name) && !string.IsNullOrEmpty(user.IdCardNumber); - - if (!isAuthInfoSubmitted) - { - authStatus = 0; // 未认证 - } - else if (user.AuditStatus == AuditStatus.Pass) - { - authStatus = 1; // 已认证 - } - else if (user.AuditStatus == AuditStatus.PendingAudit) - { - authStatus = 2; // 审核中 - } - else - { - authStatus = 3; // 失败/其他 - } + int authStatus = (int)user.AuditStatus; // 获取单位名称 string? merchantName = null; @@ -155,6 +137,7 @@ public class MyController : IDynamicApiController BirthDate = user.BirthDate, IdCardNumber = user.IdCardNumber, IdCardImageUrl = user.IdCardImageUrl, + IdCardBackImageUrl = user.IdCardBackImageUrl, PassportPhotoUrl = user.PassportPhotoUrl, AuditStatus = user.AuditStatus, AuditRemark = user.AuditRemark @@ -180,6 +163,7 @@ public class MyController : IDynamicApiController { if (string.IsNullOrEmpty(input.IdCardNumber)) throw Oops.Bah("身份证号不能为空"); if (string.IsNullOrEmpty(input.IdCardImageUrl)) throw Oops.Bah("身份证正面图片不能为空"); + if (string.IsNullOrEmpty(input.IdCardBackImageUrl)) throw Oops.Bah("身份证背面图片不能为空"); } else if (input.AuthType == 2) { @@ -201,12 +185,14 @@ public class MyController : IDynamicApiController { user.IdCardNumber = input.IdCardNumber; user.IdCardImageUrl = input.IdCardImageUrl; + user.IdCardBackImageUrl = input.IdCardBackImageUrl; user.PassportPhotoUrl = null; // 清空护照图片 } else { user.PassportPhotoUrl = input.PassportPhotoUrl; user.IdCardImageUrl = null; // 清空身份证图片 + user.IdCardBackImageUrl = null; // 清空身份证背面图片 user.Gender = (Gender)input.Gender.Value; user.BirthDate = input.BirthDate.Value; } diff --git a/YD_Event.Application/Event/PaymentExpirationBackgroundService.cs b/YD_Event.Application/Event/PaymentExpirationBackgroundService.cs new file mode 100644 index 0000000..0bf3517 --- /dev/null +++ b/YD_Event.Application/Event/PaymentExpirationBackgroundService.cs @@ -0,0 +1,98 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace YD_Event.Application.Event; + +/// +/// 支付超时处理后台服务 +/// +public class PaymentExpirationBackgroundService : BackgroundService +{ + private readonly IServiceProvider _serviceProvider; + private readonly ILogger _logger; + private readonly PeriodicTimer _timer; + + public PaymentExpirationBackgroundService( + IServiceProvider serviceProvider, + ILogger logger) + { + _serviceProvider = serviceProvider; + _logger = logger; + _timer = new PeriodicTimer(TimeSpan.FromMinutes(5)); + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + _logger.LogInformation("支付超时处理后台服务已启动"); + + try + { + while (await _timer.WaitForNextTickAsync(stoppingToken)) + { + await ProcessExpiredPaymentsAsync(stoppingToken); + } + } + catch (OperationCanceledException) + { + _logger.LogInformation("支付超时处理后台服务已停止"); + } + } + + private async Task ProcessExpiredPaymentsAsync(CancellationToken stoppingToken) + { + try + { + _logger.LogInformation("开始处理超时未支付订单"); + + using var scope = _serviceProvider.CreateScope(); + var registrationRepository = scope.ServiceProvider.GetRequiredService>(); + var orderRepository = scope.ServiceProvider.GetRequiredService>(); + + var now = DateTime.Now; + var expirationTime = now.AddMinutes(-30); + + var expiredRegistrations = await registrationRepository.AsQueryable() + .Where(x => x.PaymentMethod == 2 + && x.PaymentStatus == 0 + && x.CreatedTime <= expirationTime + && !x.DeleteMark) + .ToListAsync(stoppingToken); + + if (expiredRegistrations.Any()) + { + _logger.LogInformation("发现 {Count} 个超时未支付的线上订单", expiredRegistrations.Count); + + var idsToCancel = expiredRegistrations.Select(x => x.Id).ToList(); + var orderNos = expiredRegistrations.Where(x => !string.IsNullOrEmpty(x.OrderNo)).Select(x => x.OrderNo).ToList(); + + await registrationRepository.UpdateAsync(x => new EventRegistration + { + PaymentStatus = 4, + UpdatedTime = now + }, x => idsToCancel.Contains(x.Id)); + + if (orderNos.Any()) + { + await orderRepository.UpdateAsync(x => new YD_Event.Core.Entities.Order + { + OrderStatus = OrderStatus.Cancelled, + UpdatedTime = now + }, x => orderNos.Contains(x.OrderNo)); + } + + _logger.LogInformation("已成功取消 {Count} 个超时未支付订单", expiredRegistrations.Count); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "处理超时未支付订单时发生错误"); + } + } + + public override void Dispose() + { + _timer.Dispose(); + base.Dispose(); + } +} diff --git a/YD_Event.Application/Event/UnifiedRegistrationService.cs b/YD_Event.Application/Event/UnifiedRegistrationService.cs index 8177f7e..4a4a295 100644 --- a/YD_Event.Application/Event/UnifiedRegistrationService.cs +++ b/YD_Event.Application/Event/UnifiedRegistrationService.cs @@ -37,6 +37,7 @@ public class UnifiedRegistrationService : ITransient private readonly ISqlSugarRepository _merchantRepository; private readonly ISqlSugarRepository _couponRepository; private readonly ISqlSugarRepository _userCouponRepository; + private readonly ISqlSugarRepository _redemptionCodeRepository; private readonly ISqlSugarRepository _paymentRepository; private readonly ISqlSugarRepository _merchantCertificationRepository; private readonly ISqlSugarClient _db; @@ -66,6 +67,7 @@ public class UnifiedRegistrationService : ITransient ISqlSugarRepository merchantRepository, ISqlSugarRepository couponRepository, ISqlSugarRepository userCouponRepository, + ISqlSugarRepository redemptionCodeRepository, ISqlSugarRepository paymentRepository, ISqlSugarRepository merchantCertificationRepository, ISqlSugarClient db, @@ -94,6 +96,7 @@ public class UnifiedRegistrationService : ITransient _merchantRepository = merchantRepository; _couponRepository = couponRepository; _userCouponRepository = userCouponRepository; + _redemptionCodeRepository = redemptionCodeRepository; _paymentRepository = paymentRepository; _merchantCertificationRepository = merchantCertificationRepository; _db = db; @@ -1512,4 +1515,137 @@ public class UnifiedRegistrationService : ITransient return payConfig; } + + /// + /// 统一优惠券验证和使用方法 + /// + [UnitOfWork] + public async Task ValidateAndUseCouponAsync(ValidateCouponInput input, long userId) + { + var result = new ValidateCouponOutput + { + IsValid = false, + InvalidReason = "" + }; + + // 查找兑换码 + var redemptionCode = await _redemptionCodeRepository.GetFirstAsync(rc => + rc.Code == input.CouponCode && + rc.Status == 0 && + !rc.DeleteMark); + + if (redemptionCode == null) + { + result.InvalidReason = "兑换码不存在或已使用"; + return result; + } + + // 查找优惠券 + var coupon = await _couponRepository.GetFirstAsync(c => + c.Id == redemptionCode.CouponId && + 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 userCoupon = await _userCouponRepository.GetFirstAsync(uc => + uc.CouponId == coupon.Id && + uc.UserId == userId && + uc.Status == 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, + RedemptionCodeId = redemptionCode.Id, + RedemptionCode = redemptionCode.Code, + CreatedUserId = userId, + CreatedTime = DateTime.Now, + DeleteMark = false + }; + await _userCouponRepository.InsertAsync(userCoupon); + + // 更新兑换码状态和关联 + redemptionCode.Status = 1; + redemptionCode.UsedTime = DateTime.Now; + redemptionCode.UserCouponId = userCoupon.Id; + await _redemptionCodeRepository.UpdateAsync(redemptionCode); + + // 更新优惠券已领取数量 + 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); + + 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; + } } diff --git a/YD_Event.Application/Event/UserEventRegistrationService.cs b/YD_Event.Application/Event/UserEventRegistrationService.cs index d4fea25..79a1975 100644 --- a/YD_Event.Application/Event/UserEventRegistrationService.cs +++ b/YD_Event.Application/Event/UserEventRegistrationService.cs @@ -801,132 +801,9 @@ public class UserEventRegistrationService : IDynamicApiController, ITransient [HttpPost("coupon/validate")] public async Task ValidateCoupon([FromBody] ValidateCouponInput input) { - var result = new ValidateCouponOutput - { - IsValid = false, - InvalidReason = "" - }; - - // 查找兑换码 - var redemptionCode = await _redemptionCodeRepository.GetFirstAsync(rc => - rc.Code == input.CouponCode && - rc.Status == 0 && // 0-未使用 - !rc.DeleteMark); - - if (redemptionCode == null) - { - result.InvalidReason = "兑换码不存在或已使用"; - return result; - } - - // 查找优惠券 - var coupon = await _couponRepository.GetFirstAsync(c => - c.Id == redemptionCode.CouponId && - 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, // 未使用 - RedemptionCodeId = redemptionCode.Id, - RedemptionCode = redemptionCode.Code, - CreatedUserId = userId, - CreatedTime = DateTime.Now, - DeleteMark = false - }; - await _userCouponRepository.InsertAsync(userCoupon); - - // 更新兑换码状态和关联 - redemptionCode.Status = 1; // 已使用 - redemptionCode.UsedTime = DateTime.Now; - redemptionCode.UserCouponId = userCoupon.Id; - await _redemptionCodeRepository.UpdateAsync(redemptionCode); - - // 更新优惠券已领取数量 - 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; + return await _unifiedRegistrationService.ValidateAndUseCouponAsync(input, userId); } private string GetPaymentStatusDesc(int status) diff --git a/YD_Event.Application/YD_Event.Application.xml b/YD_Event.Application/YD_Event.Application.xml index 3cf04d8..1b4bfbd 100644 --- a/YD_Event.Application/YD_Event.Application.xml +++ b/YD_Event.Application/YD_Event.Application.xml @@ -263,6 +263,11 @@ 身份证图片URL + + + 身份证背面图片URL + + 护照图片URL @@ -353,6 +358,11 @@ 身份证图片URL + + + 身份证背面图片URL + + 护照图片URL @@ -2780,6 +2790,11 @@ 身份证正面图片(认证类型1时使用) + + + 身份证背面图片(认证类型1时使用) + + 护照图片(认证类型2时使用) @@ -2840,6 +2855,11 @@ 身份证正面图片(认证类型1时必填) + + + 身份证背面图片(认证类型1时必填) + + 护照图片(认证类型2时必填) @@ -7344,6 +7364,11 @@ 当前时间 计算后的赛事状态 + + + 支付超时处理后台服务 + + 统一报名处理服务 @@ -7459,6 +7484,11 @@ 根据赛事获取对应的微信支付配置 + + + 统一优惠券验证和使用方法 + + 用户端参赛记录管理 diff --git a/YD_Event.Core/Entities/AppUser.cs b/YD_Event.Core/Entities/AppUser.cs index f654135..b25de03 100644 --- a/YD_Event.Core/Entities/AppUser.cs +++ b/YD_Event.Core/Entities/AppUser.cs @@ -85,11 +85,17 @@ public class AppUser : Entity, ISoftDelete, ICreatedUserId, ICreatedTime, public string? IdCardNumber { get; set; } /// - /// 身份证图片URL + /// 身份证正面图片URL /// [SugarColumn(Length = 512, IsNullable = true)] public string? IdCardImageUrl { get; set; } + /// + /// 身份证背面图片URL + /// + [SugarColumn(Length = 512, IsNullable = true)] + public string? IdCardBackImageUrl { get; set; } + /// /// 护照图片URL /// diff --git a/YD_Event.Core/Enum/CouponType.cs b/YD_Event.Core/Enum/CouponType.cs new file mode 100644 index 0000000..c297fa2 --- /dev/null +++ b/YD_Event.Core/Enum/CouponType.cs @@ -0,0 +1,17 @@ +namespace YD_Event.Core.Enum; + +/// +/// 优惠券类型 +/// +public enum CouponType +{ + /// + /// 折扣 + /// + Discount = 0, + + /// + /// 定额 + /// + FixedAmount = 1 +} diff --git a/YD_Event.Core/YD_Event.Core.xml b/YD_Event.Core/YD_Event.Core.xml index 6f06941..16334d5 100644 --- a/YD_Event.Core/YD_Event.Core.xml +++ b/YD_Event.Core/YD_Event.Core.xml @@ -601,7 +601,12 @@ - 身份证图片URL + 身份证正面图片URL + + + + + 身份证背面图片URL @@ -4272,6 +4277,21 @@ 单价(项目) + + + 优惠券类型 + + + + + 折扣 + + + + + 定额 + + 博客封面类型 diff --git a/YD_Event.Web.Core/Startup.cs b/YD_Event.Web.Core/Startup.cs index 6a1e068..6b59773 100644 --- a/YD_Event.Web.Core/Startup.cs +++ b/YD_Event.Web.Core/Startup.cs @@ -35,6 +35,9 @@ public class Startup : AppStartup // 注册赛事状态自动更新后台服务 services.AddHostedService(); + // 注册支付超时处理后台服务 + services.AddHostedService(); + //解决Encoding无法使用gb2312编码问题 Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);