diff --git a/YD_Event.Application/Auth/AuthService.cs b/YD_Event.Application/Auth/AuthService.cs index f8db809..fe19d5b 100644 --- a/YD_Event.Application/Auth/AuthService.cs +++ b/YD_Event.Application/Auth/AuthService.cs @@ -117,12 +117,6 @@ public class AuthService : IDynamicApiController throw Oops.Bah("验证码错误或已过期"); } - var existAccount = await _sysUseRepository.IsAnyAsync(x => x.Account == dto.Account); - if (existAccount) - { - throw Oops.Bah("该账号已被注册"); - } - var existPhone = await _sysUseRepository.IsAnyAsync(x => x.Mobile == dto.Phone); if (existPhone) { @@ -132,8 +126,8 @@ public class AuthService : IDynamicApiController var user = new SysUser { Id = _idGenerator.NewLong(), - Account = dto.Account, - Name = dto.Account, + Account = dto.Phone, + Name = dto.Phone, Mobile = dto.Phone, Gender = Gender.Unknown, OrgId = 0, diff --git a/YD_Event.Application/Auth/Dtos/AuthDtos.cs b/YD_Event.Application/Auth/Dtos/AuthDtos.cs index 9183f72..70cf189 100644 --- a/YD_Event.Application/Auth/Dtos/AuthDtos.cs +++ b/YD_Event.Application/Auth/Dtos/AuthDtos.cs @@ -15,14 +15,6 @@ public class RegisterInput [RegularExpression(@"^1[3456789]\d{9}$", ErrorMessage = "手机号格式不正确")] public string Phone { get; set; } = null!; - /// - /// 账号 - /// - [Required(ErrorMessage = "账号不能为空")] - [MinLength(3, ErrorMessage = "账号至少3个字符")] - [MaxLength(32, ErrorMessage = "账号最多32个字符")] - public string Account { get; set; } = null!; - /// /// 密码 /// diff --git a/YD_Event.Application/Certificate/CertificateTemplateService.cs b/YD_Event.Application/Certificate/CertificateTemplateService.cs index 5fc36c9..f24dc10 100644 --- a/YD_Event.Application/Certificate/CertificateTemplateService.cs +++ b/YD_Event.Application/Certificate/CertificateTemplateService.cs @@ -433,6 +433,20 @@ public class CertificateTemplateService : BaseService, ITra }; } + /// + /// 获取证书模板列表(用于选择) + /// + [DisplayName("获取证书模板列表")] + public async Task> GetTemplateList() + { + var templates = await _templateRepository.AsQueryable() + .Where(x => x.Status == Core.Enum.AvailabilityStatus.Enable && !x.DeleteMark) + .OrderByDescending(x => x.CreatedTime) + .ToListAsync(); + + return templates.Adapt>(); + } + /// /// 获取当前用户ID /// diff --git a/YD_Event.Application/Certificate/Dtos/CertificateTemplateDtos.cs b/YD_Event.Application/Certificate/Dtos/CertificateTemplateDtos.cs index 33dd9a7..f7ab7a8 100644 --- a/YD_Event.Application/Certificate/Dtos/CertificateTemplateDtos.cs +++ b/YD_Event.Application/Certificate/Dtos/CertificateTemplateDtos.cs @@ -354,4 +354,9 @@ public class CertificateTemplateSimpleOutput /// 创建时间 /// public DateTime CreatedTime { get; set; } + + /// + /// 显示名称 + /// + public string DisplayName => $"模板 {Id} (v{Version}) - {TemplateType}"; } diff --git a/YD_Event.Application/Client/MyController.cs b/YD_Event.Application/Client/MyController.cs index 9bdec08..8f9c6dd 100644 --- a/YD_Event.Application/Client/MyController.cs +++ b/YD_Event.Application/Client/MyController.cs @@ -7,6 +7,7 @@ using Furion.DatabaseAccessor; using Microsoft.AspNetCore.Authorization; using System.Security.Claims; using Furion.FriendlyException; +using EasyCaching.Core; using ProjectEntity = YD_Event.Core.Entities.Project; namespace YD_Event.Application.Client; @@ -29,6 +30,7 @@ public class MyController : IDynamicApiController private readonly ISqlSugarRepository _eventRepository; private readonly ISqlSugarRepository _merchantCertificationRepository; private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IEasyCachingProvider _cachingProvider; public MyController( ISqlSugarRepository appUserRepository, @@ -41,7 +43,8 @@ public class MyController : IDynamicApiController ISqlSugarRepository registrationItemRepository, ISqlSugarRepository eventRepository, ISqlSugarRepository merchantCertificationRepository, - IHttpContextAccessor httpContextAccessor) + IHttpContextAccessor httpContextAccessor, + IEasyCachingProvider cachingProvider) { _appUserRepository = appUserRepository; _teamRepository = teamRepository; @@ -54,6 +57,7 @@ public class MyController : IDynamicApiController _eventRepository = eventRepository; _merchantCertificationRepository = merchantCertificationRepository; _httpContextAccessor = httpContextAccessor; + _cachingProvider = cachingProvider; } private long GetCurrentUserId() @@ -178,6 +182,31 @@ public class MyController : IDynamicApiController if (string.IsNullOrEmpty(input.SmsCode)) throw Oops.Bah("手机号验证码不能为空"); + // 1. 验证短信验证码格式(6位数字) + if (input.SmsCode.Length != 6 || !input.SmsCode.All(char.IsDigit)) + { + throw Oops.Bah("验证码格式错误"); + } + + // 2. 从缓存中获取验证码,验证是否为系统最新生成且在有效期内 + var codeCacheKey = $"sms:code:{input.Mobile}"; + var cachedCode = await _cachingProvider.GetAsync(codeCacheKey); + + // 3. 验证验证码是否在有效期内 + if (!cachedCode.HasValue) + { + throw Oops.Bah("验证码已过期"); + } + + // 4. 验证验证码是否一致 + if (cachedCode.Value != input.SmsCode) + { + throw Oops.Bah("验证码不正确"); + } + + // 验证成功后,移除验证码缓存 + await _cachingProvider.RemoveAsync(codeCacheKey); + user.Name = input.Name; user.Mobile = input.Mobile; diff --git a/YD_Event.Application/Event/Dtos/ArrangementDtos.cs b/YD_Event.Application/Event/Dtos/ArrangementDtos.cs index 2693fd8..16a8e4f 100644 --- a/YD_Event.Application/Event/Dtos/ArrangementDtos.cs +++ b/YD_Event.Application/Event/Dtos/ArrangementDtos.cs @@ -41,7 +41,12 @@ public class ArrangementProjectListOutput public int SortIndex { get; set; } /// - /// 总时长(分钟) + /// 总时长(秒) + /// + public int TotalDurationSeconds { get; set; } + + /// + /// 总时长(分钟,向上取整) /// public int TotalDurationMinutes { get; set; } } diff --git a/YD_Event.Application/Event/EventArrangementService.cs b/YD_Event.Application/Event/EventArrangementService.cs index 3de1138..c1624e1 100644 --- a/YD_Event.Application/Event/EventArrangementService.cs +++ b/YD_Event.Application/Event/EventArrangementService.cs @@ -155,7 +155,8 @@ public class EventArrangementService : BaseService, ITransie private readonly ISqlSugarRepository _personRepository; private readonly ISqlSugarRepository _paymentRepository; private readonly ISqlSugarRepository _orderRepository; + private readonly ISqlSugarRepository _teamMemberRepository; + private readonly ISqlSugarRepository _appUserRepository; private readonly AuthManager _authManager; public EventRegistrationService( @@ -23,6 +25,8 @@ public class EventRegistrationService : BaseService, ITransie ISqlSugarRepository personRepository, ISqlSugarRepository paymentRepository, ISqlSugarRepository orderRepository, + ISqlSugarRepository teamMemberRepository, + ISqlSugarRepository appUserRepository, AuthManager authManager) : base(repository) { _repository = repository; @@ -30,6 +34,8 @@ public class EventRegistrationService : BaseService, ITransie _personRepository = personRepository; _paymentRepository = paymentRepository; _orderRepository = orderRepository; + _teamMemberRepository = teamMemberRepository; + _appUserRepository = appUserRepository; _authManager = authManager; } @@ -82,6 +88,75 @@ public class EventRegistrationService : BaseService, ITransie .Where(x => regIds.Contains(x.RegistrationId)) .ToListAsync(); + // 获取所有报名人员的 UserId + var allAthleteUserIds = persons + .Where(p => p.PersonType == YD_Event.Core.Enum.PersonType.Athlete && p.UserId > 0) + .Select(p => p.UserId) + .Distinct() + .ToList(); + + // 步骤1:通过运动员的 UserId 找到对应的 TeamId + var teamIds = new List(); + if (allAthleteUserIds.Any()) + { + teamIds = await _teamMemberRepository.AsQueryable() + .Where(tm => allAthleteUserIds.Contains(tm.UserId) && !tm.DeleteMark) + .Select(tm => tm.TeamId) + .Distinct() + .ToListAsync(); + } + + // 步骤2:通过 TeamId 获取同团队的教练/领队的 UserId,并获取用户详情 + Dictionary> teamLeadersDict = new(); + Dictionary> teamCoachesDict = new(); + + if (teamIds.Any()) + { + var teamMembers = await _teamMemberRepository.AsQueryable() + .Where(tm => teamIds.Contains(tm.TeamId) && !tm.DeleteMark) + .ToListAsync(); + + var coachLeaderUserIds = teamMembers + .Where(tm => tm.PersonType == YD_Event.Core.Enum.PersonType.Leader || + tm.PersonType == YD_Event.Core.Enum.PersonType.Coach) + .Select(tm => tm.UserId) + .Distinct() + .ToList(); + + if (coachLeaderUserIds.Any()) + { + var coachLeaderUsers = await _appUserRepository.AsQueryable() + .Where(u => coachLeaderUserIds.Contains(u.Id) && !u.DeleteMark) + .ToListAsync(); + + var userDict = coachLeaderUsers.ToDictionary(u => u.Id); + + // 按团队分组 + foreach (var teamId in teamIds) + { + var teamLeaders = teamMembers + .Where(tm => tm.TeamId == teamId && tm.PersonType == YD_Event.Core.Enum.PersonType.Leader) + .Select(tm => userDict.TryGetValue(tm.UserId, out var user) ? user : null) + .Where(u => u != null) + .Cast() + .ToList(); + + var teamCoaches = teamMembers + .Where(tm => tm.TeamId == teamId && tm.PersonType == YD_Event.Core.Enum.PersonType.Coach) + .Select(tm => userDict.TryGetValue(tm.UserId, out var user) ? user : null) + .Where(u => u != null) + .Cast() + .ToList(); + + if (teamLeaders.Any()) + teamLeadersDict[teamId] = teamLeaders; + + if (teamCoaches.Any()) + teamCoachesDict[teamId] = teamCoaches; + } + } + } + foreach (var dto in result.Rows) { var myItems = items.Where(x => x.RegistrationId == dto.Id).ToList(); @@ -120,17 +195,81 @@ public class EventRegistrationService : BaseService, ITransie dto.Items.Add(detail); } - // 设置领队信息和教练信息 + // 设置领队信息和教练信息 - 从团队中获取 var regPersons = persons.Where(p => p.RegistrationId == dto.Id).ToList(); - var leaders = regPersons.Where(p => p.PersonType == YD_Event.Core.Enum.PersonType.Leader).ToList(); - var coaches = regPersons.Where(p => p.PersonType == YD_Event.Core.Enum.PersonType.Coach).ToList(); - - dto.TeamLeaderInfo = leaders.Any() - ? string.Join(";", leaders.Select(l => $"{l.Name}({l.Phone})")) + var regAthleteUserIds = regPersons + .Where(p => p.PersonType == YD_Event.Core.Enum.PersonType.Athlete && p.UserId > 0) + .Select(p => p.UserId) + .Distinct() + .ToList(); + + // 获取这些运动员所属的团队 + var regTeamIds = new List(); + if (regAthleteUserIds.Any()) + { + regTeamIds = await _teamMemberRepository.AsQueryable() + .Where(tm => regAthleteUserIds.Contains(tm.UserId) && !tm.DeleteMark) + .Select(tm => tm.TeamId) + .Distinct() + .ToListAsync(); + } + + // 收集所有相关团队的领队和教练 + var allLeaders = new List(); + var allCoaches = new List(); + + foreach (var teamId in regTeamIds) + { + if (teamLeadersDict.TryGetValue(teamId, out var leaders)) + allLeaders.AddRange(leaders); + if (teamCoachesDict.TryGetValue(teamId, out var coaches)) + allCoaches.AddRange(coaches); + } + + // 去重 + allLeaders = allLeaders.GroupBy(l => l.Id).Select(g => g.First()).ToList(); + allCoaches = allCoaches.GroupBy(c => c.Id).Select(g => g.First()).ToList(); + + // 如果从团队中没有找到,再回退到从报名人员中查找 + if (!allLeaders.Any()) + { + var regLeaders = regPersons.Where(p => p.PersonType == YD_Event.Core.Enum.PersonType.Leader).ToList(); + allLeaders = regLeaders.Select(l => new YD_Event.Core.Entities.AppUser + { + Name = l.Name, + Mobile = l.Phone, + Gender = l.Gender + }).ToList(); + } + + if (!allCoaches.Any()) + { + var regCoaches = regPersons.Where(p => p.PersonType == YD_Event.Core.Enum.PersonType.Coach).ToList(); + allCoaches = regCoaches.Select(c => new YD_Event.Core.Entities.AppUser + { + Name = c.Name, + Mobile = c.Phone, + Gender = c.Gender + }).ToList(); + } + + // 辅助函数:获取性别文本 + Func getGenderText = (gender) => + { + return gender switch + { + YD_Event.Core.Enum.Gender.Male => "男", + YD_Event.Core.Enum.Gender.Female => "女", + _ => "未知" + }; + }; + + dto.TeamLeaderInfo = allLeaders.Any() + ? string.Join(",", allLeaders.Select(l => $"{l.Name}({l.Mobile})/{getGenderText(l.Gender)}")) : null; - dto.CoachInfo = coaches.Any() - ? string.Join(";", coaches.Select(c => $"{c.Name}({c.Phone})")) + dto.CoachInfo = allCoaches.Any() + ? string.Join(",", allCoaches.Select(c => $"{c.Name}({c.Mobile})/{getGenderText(c.Gender)}")) : null; } } diff --git a/YD_Event.Application/Event/PaymentExpirationBackgroundService.cs b/YD_Event.Application/Event/PaymentExpirationBackgroundService.cs index 8c9df01..3ff8ca2 100644 --- a/YD_Event.Application/Event/PaymentExpirationBackgroundService.cs +++ b/YD_Event.Application/Event/PaymentExpirationBackgroundService.cs @@ -54,6 +54,8 @@ public class PaymentExpirationBackgroundService : BackgroundService using var scope = _serviceProvider.CreateScope(); var registrationRepository = scope.ServiceProvider.GetRequiredService>(); var orderRepository = scope.ServiceProvider.GetRequiredService>(); + var userCouponRepository = scope.ServiceProvider.GetRequiredService>(); + var couponRepository = scope.ServiceProvider.GetRequiredService>(); var now = DateTime.Now; var expirationTime = now.AddMinutes(-30); @@ -80,6 +82,18 @@ public class PaymentExpirationBackgroundService : BackgroundService if (orderNos.Any()) { + // 获取这些订单并释放优惠券 + var orders = await orderRepository.AsQueryable() + .Where(x => orderNos.Contains(x.OrderNo) && x.CouponId.HasValue) + .ToListAsync(stoppingToken); + + foreach (var order in orders) + { + await ReleaseCouponAsync(order.CouponId.Value, order.CreatedUserId, order.Id, + userCouponRepository, couponRepository, stoppingToken); + } + + // 更新订单状态 await orderRepository.UpdateAsync(x => new YD_Event.Core.Entities.Order { OrderStatus = OrderStatus.Cancelled, @@ -96,6 +110,46 @@ public class PaymentExpirationBackgroundService : BackgroundService } } + /// + /// 释放优惠券,恢复为未使用状态 + /// + private async Task ReleaseCouponAsync(long couponId, long userId, long orderId, + ISqlSugarRepository userCouponRepository, + ISqlSugarRepository couponRepository, + CancellationToken stoppingToken) + { + var userCoupon = await userCouponRepository.GetFirstAsync(uc => + uc.CouponId == couponId && + uc.UserId == userId && + uc.OrderId == orderId && + !uc.DeleteMark, stoppingToken); + + if (userCoupon != null && userCoupon.Status == 1) + { + userCoupon.Status = 0; + userCoupon.UseTime = null; + userCoupon.OrderId = null; + await userCouponRepository.UpdateAsync(userCoupon, stoppingToken); + + // 减少优惠券的已使用数量 + var coupon = await couponRepository.GetByIdAsync(couponId, stoppingToken); + if (coupon != null && coupon.UsedQuantity > 0) + { + coupon.UsedQuantity--; + await couponRepository.UpdateAsync(coupon, stoppingToken); + } + + _logger.LogInformation($"优惠券已释放,用户优惠券ID={userCoupon.Id},订单ID={orderId}"); + } + else if (userCoupon != null && userCoupon.Status == 0) + { + // 如果优惠券还没有被标记为已使用,只需要清空OrderId + userCoupon.OrderId = null; + await userCouponRepository.UpdateAsync(userCoupon, stoppingToken); + _logger.LogInformation($"优惠券OrderId已清空,用户优惠券ID={userCoupon.Id},订单ID={orderId}"); + } + } + /// /// 处理微信未支付订单:定期检查并更新订单状态 /// @@ -182,12 +236,16 @@ public class PaymentExpirationBackgroundService : BackgroundService } else if (queryResult.TradeState == "CLOSED" || queryResult.TradeState == "REVOKED") { + var userCouponRepository = scope.ServiceProvider.GetRequiredService>(); + var couponRepository = scope.ServiceProvider.GetRequiredService>(); await ProcessWeChatPaymentClosedAsync( registration, order, now, registrationRepository, orderRepository, + userCouponRepository, + couponRepository, stoppingToken); } } @@ -277,6 +335,8 @@ public class PaymentExpirationBackgroundService : BackgroundService DateTime now, ISqlSugarRepository registrationRepository, ISqlSugarRepository orderRepository, + ISqlSugarRepository userCouponRepository, + ISqlSugarRepository couponRepository, CancellationToken stoppingToken) { _logger.LogInformation("订单 {OrderNo} 支付已关闭,更新订单状态为已取消", registration.OrderNo); @@ -288,6 +348,13 @@ public class PaymentExpirationBackgroundService : BackgroundService order.OrderStatus = OrderStatus.Cancelled; order.UpdatedTime = now; await orderRepository.UpdateAsync(order); + + // 释放优惠券 + if (order.CouponId.HasValue) + { + await ReleaseCouponAsync(order.CouponId.Value, order.CreatedUserId, order.Id, + userCouponRepository, couponRepository, stoppingToken); + } _logger.LogInformation("订单 {OrderNo} 状态更新为已取消成功", registration.OrderNo); } diff --git a/YD_Event.Application/Event/UnifiedRegistrationService.cs b/YD_Event.Application/Event/UnifiedRegistrationService.cs index 1e85276..440fe13 100644 --- a/YD_Event.Application/Event/UnifiedRegistrationService.cs +++ b/YD_Event.Application/Event/UnifiedRegistrationService.cs @@ -1728,6 +1728,34 @@ public class UnifiedRegistrationService : ITransient order.UpdatedTime = DateTime.Now; await _orderRepository.UpdateAsync(order); _logger.LogInformation($"Order 记录 {order.OrderNo} 状态已更新为已支付"); + + // 支付成功后,标记优惠券为已使用 + if (order.CouponId.HasValue) + { + var userCoupon = await _userCouponRepository.GetFirstAsync(uc => + uc.CouponId == order.CouponId.Value && + uc.UserId == order.CreatedUserId && + uc.Status == 0 && + !uc.DeleteMark); + + if (userCoupon != null) + { + userCoupon.Status = 1; + userCoupon.UseTime = successTime ?? DateTime.Now; + userCoupon.OrderId = order.Id; + await _userCouponRepository.UpdateAsync(userCoupon); + + // 更新优惠券的已使用数量 + var coupon = await _couponRepository.GetByIdAsync(order.CouponId.Value); + if (coupon != null) + { + coupon.UsedQuantity++; + await _couponRepository.UpdateAsync(coupon); + } + + _logger.LogInformation($"优惠券已标记为已使用,用户优惠券ID={userCoupon.Id},订单ID={order.Id}"); + } + } } } diff --git a/YD_Event.Application/YD_Event.Application.xml b/YD_Event.Application/YD_Event.Application.xml index c123103..997abea 100644 --- a/YD_Event.Application/YD_Event.Application.xml +++ b/YD_Event.Application/YD_Event.Application.xml @@ -508,11 +508,6 @@ 手机号 - - - 账号 - - 密码 @@ -1007,6 +1002,11 @@ 生成显示名称 + + + 获取证书模板列表(用于选择) + + 获取当前用户ID @@ -1452,6 +1452,11 @@ 创建时间 + + + 显示名称 + + 删除证书输入参数 @@ -4052,9 +4057,14 @@ 排序索引 + + + 总时长(秒) + + - 总时长(分钟) + 总时长(分钟,向上取整) @@ -5752,6 +5762,11 @@ 性别 + + + 出生日期 + + 身份证号 @@ -7687,6 +7702,11 @@ 支付超时处理后台服务 + + + 释放优惠券,恢复为未使用状态 + + 处理微信未支付订单:定期检查并更新订单状态 @@ -7697,7 +7717,7 @@ 处理微信支付成功的订单 - + 处理微信支付关闭的订单 diff --git a/YD_Event.Application/applicationsettings.json b/YD_Event.Application/applicationsettings.json index eadfc92..e045a0d 100644 --- a/YD_Event.Application/applicationsettings.json +++ b/YD_Event.Application/applicationsettings.json @@ -41,7 +41,7 @@ "DbType": "SqlServer", // MySql、SqlServer、Sqlite、Oracle、PostgreSQL、Dm、Kdbndp、Oscar、MySqlConnector、Access、OpenGauss、QuestDB、HG、ClickHouse、GBase、Odbc、Custom "ConnectionString": "Data Source=rm-uf64b8oxdt19pfyxfao.sqlserver.rds.aliyuncs.com;Database=EventServerDB_QA;User ID=qa;Password=Admin@123;Connect Timeout=500;Encrypt=True;TrustServerCertificate=True;", "IsAutoCloseConnection": true, - "EnableInitDb": true // 启用数据库以及数据表初始化(首次运行项目时开启可以自动创建数据库和初始化项目运行的基础数据) + "EnableInitDb": false // 启用数据库以及数据表初始化(首次运行项目时开启可以自动创建数据库和初始化项目运行的基础数据) } ] },