using Microsoft.AspNetCore.Mvc; using YD_Event.Application.Client.Dtos; using YD_Event.Core.Entities; using YD_Event.Core.Enum; using SqlSugar; using Furion.DatabaseAccessor; using Microsoft.AspNetCore.Authorization; using Furion.FriendlyException; using System.Security.Claims; namespace YD_Event.Application.Client; /// /// 小程序-赛事接口 /// [ApiDescriptionSettings("微信小程序接口")] public class EventClientController : IDynamicApiController { private readonly ISqlSugarRepository _eventRepository; private readonly ISqlSugarRepository _projectRepository; private readonly ISqlSugarRepository _eventProjectRepository; private readonly ISqlSugarRepository _groupRepository; private readonly ISqlSugarRepository _registrationRepository; private readonly ISqlSugarRepository _registrationItemRepository; private readonly ISqlSugarRepository _bannerRepository; private readonly ISqlSugarRepository _unitRepository; private readonly ISqlSugarRepository _personRepository; private readonly ISqlSugarRepository _limitRepository; private readonly IHttpContextAccessor _httpContextAccessor; private readonly IIdGenerator _idGenerator; public EventClientController( ISqlSugarRepository eventRepository, ISqlSugarRepository projectRepository, ISqlSugarRepository eventProjectRepository, ISqlSugarRepository groupRepository, ISqlSugarRepository registrationRepository, ISqlSugarRepository registrationItemRepository, ISqlSugarRepository bannerRepository, ISqlSugarRepository unitRepository, ISqlSugarRepository personRepository, ISqlSugarRepository limitRepository, IHttpContextAccessor httpContextAccessor, IIdGenerator idGenerator) { _eventRepository = eventRepository; _projectRepository = projectRepository; _eventProjectRepository = eventProjectRepository; _groupRepository = groupRepository; _registrationRepository = registrationRepository; _registrationItemRepository = registrationItemRepository; _bannerRepository = bannerRepository; _unitRepository = unitRepository; _personRepository = personRepository; _limitRepository = limitRepository; _httpContextAccessor = httpContextAccessor; _idGenerator = idGenerator; } private long GetCurrentUserId() { var userIdStr = _httpContextAccessor.HttpContext?.User?.FindFirst(AuthClaimsConst.AuthIdKey)?.Value; if (string.IsNullOrEmpty(userIdStr) || !long.TryParse(userIdStr, out long userId)) { throw Oops.Bah("未登录或登录已失效"); } return userId; } /// /// 获取轮播图列表 /// /// [HttpGet("banners")] [AllowAnonymous] public async Task> GetBannerList() { var list = await _bannerRepository.AsQueryable() .Where(b => !b.DeleteMark && b.Status == AvailabilityStatus.Enable) .OrderByDescending(b => b.Sort) .ToListAsync(); return list.Select(b => new BannerDto { Title = b.Title, ImageUrl = b.ImageUrl, LinkType = b.LinkType, LinkUrl = b.LinkUrl, EventId = b.EventId }).ToList(); } /// /// 获取赛事列表 /// /// 页码 /// 页大小 /// 状态筛选:0-全部 1-报名中 2-比赛中 3-已结束 /// 搜索关键字 /// [HttpGet("list")] [AllowAnonymous] // 允许未登录查看列表?根据需求,原型中有“我的”,可能需要登录。但通常列表可公开。这里先设为允许匿名,获取详情再校验。 // 修正:Class上有[Authorize],如果需要匿名需显式[AllowAnonymous]。 // 假设小程序进入即登录,保持Authorize public async Task> GetEventList([FromQuery] int page = 1, [FromQuery] int pageSize = 10, [FromQuery] int status = 0, [FromQuery] string? keyword = null) { var query = _eventRepository.AsQueryable() .Where(e => !e.DeleteMark && e.Status == AvailabilityStatus.Enable) .WhereIF(!string.IsNullOrEmpty(keyword), e => e.EventName.Contains(keyword)); // 状态筛选逻辑 // 1-报名中: Now >= RegStart && Now <= RegEnd // 2-比赛中: Now >= MatchStart && Now <= MatchEnd // 3-已结束: Now > MatchEnd var now = DateTime.Now; if (status == 1) { query = query.Where(e => now >= e.RegistrationStartTime && now <= e.RegistrationEndTime); } else if (status == 2) { query = query.Where(e => now >= e.MatchStartTime && now <= e.MatchEndTime); } else if (status == 3) { query = query.Where(e => now > e.MatchEndTime); } // 排序:优先按报名开始时间倒序 query = query.OrderByDescending(e => e.RegistrationStartTime); var list = await query.ToPagedListAsync(new Pagination { PageNo = page, PageSize = pageSize }); var result = new PageResult { PageNo = list.PageNo, PageSize = list.PageSize, Total = list.Total, Pages = list.Pages, Rows = list.Rows.Select(e => new EventListDto { Id = e.Id, EventName = e.EventName, CoverImage = e.CoverImage, EventStatus = CalculateEventStatus(e), // 动态计算状态 RegistrationStartTime = e.RegistrationStartTime, RegistrationEndTime = e.RegistrationEndTime, MatchStartTime = e.MatchStartTime, MatchEndTime = e.MatchEndTime, Address = $"{e.Province}{e.City}{e.District}{e.DetailAddress}" }).ToList() }; return result; } /// /// 获取赛事详情 /// /// /// [HttpGet("{id}")] [AllowAnonymous] public async Task GetEventDetail(long id) { var evt = await _eventRepository.GetFirstAsync(e => e.Id == id && !e.DeleteMark); if (evt == null) throw Oops.Bah("赛事不存在"); // 获取项目及组别 var eventProjects = await _eventProjectRepository.AsQueryable() .Where(ep => ep.EventId == id) .ToListAsync(); var projectIds = eventProjects.Select(ep => ep.ProjectId).Distinct().ToList(); var baseProjects = await _projectRepository.AsQueryable() .LeftJoin((p, t) => p.ProjectTypeId == t.Id) .Where(p => projectIds.Contains(p.Id) && !p.DeleteMark && p.Status == AvailabilityStatus.Enable) .Select((p, t) => new { p.Id, p.Name, TypeName = t.Name }) .ToListAsync(); var groups = await _groupRepository.AsQueryable() .Where(g => projectIds.Contains(g.ProjectId) && !g.DeleteMark) .ToListAsync(); // 获取单位 var units = await _unitRepository.AsQueryable() .Where(u => u.EventId == id && !u.DeleteMark) .OrderBy(u => u.Sort) .ToListAsync(); // 获取限制规则 var limit = await _limitRepository.GetFirstAsync(l => l.EventId == id && !l.DeleteMark); var detail = new EventDetailDto { Id = evt.Id, EventName = evt.EventName, CoverImage = evt.CoverImage, EventStatus = CalculateEventStatus(evt), RegistrationStartTime = evt.RegistrationStartTime, RegistrationEndTime = evt.RegistrationEndTime, MatchStartTime = evt.MatchStartTime, MatchEndTime = evt.MatchEndTime, Address = $"{evt.Province}{evt.City}{evt.District}{evt.DetailAddress}", EventVideo = evt.EventVideo, EventType = evt.EventType, ChargeMode = evt.ChargeMode, RegistrationType = evt.RegistrationType, EventIntroduction = evt.EventIntroduction, MaterialRemark = evt.MaterialRemark, MaterialUploadStartTime = evt.MaterialUploadStartTime, MaterialUploadEndTime = evt.MaterialUploadEndTime, // 模拟规则文本,实际应从 EventInfo 扩展字段或关联表读取 RefundRule = "开赛前7天扣除金额5%,3-7天扣除金额10%,3天内扣除金额20%", PointsRule = "参与积分:5积分;名次积分:1-8名 15积分,9-20名 10积分", IndividualProjectLimit = limit?.IndividualProjectLimit ?? 0, TeamProjectLimit = limit?.TeamProjectLimit ?? 0, Units = units.Select(u => new EventUnitDto { UnitType = u.UnitType, UnitName = u.UnitName, LogoUrl = u.LogoUrl }).ToList(), Projects = eventProjects.Select(ep => { var bp = baseProjects.FirstOrDefault(b => b.Id == ep.ProjectId); return new EventProjectDto { Id = ep.Id, Name = bp?.Name ?? "", TypeName = bp?.TypeName ?? "", Fee = ep.Price / 100m, Groups = groups.Where(g => g.ProjectId == ep.ProjectId).Select(g => new EventProjectGroupDto { Id = g.Id, GroupName = g.GroupName, GenderRequirement = g.Gender == Gender.Male ? "男" : (g.Gender == Gender.Female ? "女" : "不限"), AgeRequirement = $"{g.BirthDateStart:yyyy-MM-dd} 至 {g.BirthDateEnd:yyyy-MM-dd}" }).ToList() }; }).Where(x => !string.IsNullOrEmpty(x.Name)).ToList() }; return detail; } /// /// 提交赛事报名 /// /// /// [HttpPost("apply")] [UnitOfWork] public async Task SubmitRegistration([FromBody] SubmitRegistrationInput input) { var userId = GetCurrentUserId(); // 0. 校验报名模式 if (input.RegistrationMode == 2) // 假设2为团队 throw Oops.Bah("团队报名请前往网页端进行操作"); var evt = await _eventRepository.GetFirstAsync(e => e.Id == input.EventId); if (evt == null) throw Oops.Bah("赛事不存在"); // 1. 校验时间 if (DateTime.Now < evt.RegistrationStartTime || DateTime.Now > evt.RegistrationEndTime) throw Oops.Bah("不在报名时间内"); // 2. 校验重复报名 (同一赛事同一人只能报一次?还是可以多次?通常一次报名包含多个项目) // 假设同一用户对同一赛事只能有一个“未取消”的报名记录 var exist = await _registrationRepository.IsAnyAsync(r => r.EventId == input.EventId && r.CreatedUserId == userId && r.RegistrationStatus != 4); // 4=已取消 if (exist) throw Oops.Bah("您已报名该赛事,请勿重复提交"); // 3. 校验项目有效性 & 限制 if (input.Items == null || !input.Items.Any()) throw Oops.Bah("请至少选择一个项目"); var limit = await _limitRepository.GetFirstAsync(l => l.EventId == input.EventId && !l.DeleteMark); if (limit != null) { // 简单校验项目总数,更严谨需按项目类型统计 if (input.Items.Count > limit.IndividualProjectLimit) throw Oops.Bah($"每人最多可报{limit.IndividualProjectLimit}个项目"); } // 4. 创建报名主表 var registrationId = _idGenerator.NewLong(); var registration = new EventRegistration { Id = registrationId, EventId = input.EventId, RegistrationMode = input.RegistrationMode, TeamName = input.RegistrationMode == 2 ? input.TeamName : null, ContactName = input.ContactName, ContactPhone = input.ContactPhone, RegistrationStatus = 0, // 待提交/待支付 PaymentStatus = 0, // 未支付 RegistrationTime = DateTime.Now, CreatedUserId = userId, CreatedTime = DateTime.Now, Status = AvailabilityStatus.Enable, TotalPersonCount = 0 // 稍后计算 }; // 5. 创建报名明细 & 参赛人员 & 计算费用 decimal totalAmount = 0; int totalPerson = 0; var regItems = new List(); var persons = new List(); foreach (var item in input.Items) { var eventProject = await _eventProjectRepository.GetFirstAsync(p => p.Id == item.ProjectId); if (eventProject == null) throw Oops.Bah($"项目不存在: {item.ProjectId}"); var project = await _projectRepository.GetFirstAsync(p => p.Id == eventProject.ProjectId); if (project == null) throw Oops.Bah($"关联基础项目不存在: {item.ProjectId}"); // 校验组别 // 略,假设前端传参正确,或需根据 GroupId 查 if (item.Persons == null || !item.Persons.Any()) throw Oops.Bah($"项目[{project.Name}]未填写参赛人信息"); var regItemId = _idGenerator.NewLong(); var regItem = new EventRegistrationItem { Id = regItemId, RegistrationId = registrationId, ProjectId = item.ProjectId, ProjectName = project.Name, GroupName = item.GroupName, UnitPrice = eventProject.Price / 100m, PersonCount = item.Persons.Count, TotalPrice = (eventProject.Price / 100m) * item.Persons.Count, CreatedUserId = userId, CreatedTime = DateTime.Now }; regItems.Add(regItem); totalAmount += regItem.TotalPrice; totalPerson += regItem.PersonCount; // 添加参赛人员 foreach (var pInput in item.Persons) { persons.Add(new EventRegistrationPerson { Id = _idGenerator.NewLong(), RegistrationId = registrationId, RegistrationItemId = regItemId, ProjectId = item.ProjectId, PersonType = PersonType.Athlete, // 运动员 Name = pInput.Name, Gender = pInput.Gender, IdCard = pInput.IdCard, BirthDate = pInput.BirthDate, Phone = pInput.Phone, GroupName = item.GroupName, ProjectName = project.Name, CreatedUserId = userId, CreatedTime = DateTime.Now }); } } registration.TotalAmount = totalAmount; registration.TotalPersonCount = totalPerson; registration.OrderNo = DateTime.Now.ToString("yyyyMMddHHmmss") + new Random().Next(1000, 9999); // 简易订单号 await _registrationRepository.InsertAsync(registration); await _registrationItemRepository.InsertRangeAsync(regItems); await _personRepository.InsertRangeAsync(persons); return registrationId; } /// /// 获取支付信息 /// [HttpGet("payment-info/{registrationId}")] public async Task GetPaymentInfo(long registrationId) { var userId = GetCurrentUserId(); var reg = await _registrationRepository.GetFirstAsync(r => r.Id == registrationId && r.CreatedUserId == userId); if (reg == null) throw Oops.Bah("报名记录不存在"); return new PaymentInfoDto { RegistrationId = reg.Id, Amount = reg.TotalAmount, PaymentMethod = reg.PaymentMethod, BankAccountName = "上海跃动文化科技有限公司", BankAccountNo = "456498465198456261", BankName = "招商银行", // 示例 WechatPayParams = new { appId = "wx8888888888888888", timeStamp = DateTimeOffset.Now.ToUnixTimeSeconds().ToString(), nonceStr = Guid.NewGuid().ToString("N"), package = "prepay_id=wx...", signType = "MD5", paySign = "..." } // 模拟 }; } /// /// 上传支付凭证(线下支付) /// [HttpPost("upload-voucher/{registrationId}")] public async Task UploadPaymentVoucher(long registrationId, [FromBody] UploadVoucherInput input) { var userId = GetCurrentUserId(); var reg = await _registrationRepository.GetFirstAsync(r => r.Id == registrationId && r.CreatedUserId == userId); if (reg == null) throw Oops.Bah("报名记录不存在"); reg.PaymentMethod = 1; // 线下 reg.PaymentProofUrl = input.VoucherUrl; reg.RegistrationStatus = 1; // 待审核 reg.UpdatedTime = DateTime.Now; reg.UpdatedUserId = userId; return await _registrationRepository.UpdateAsync(reg); } /// /// 更新参赛人信息 /// [HttpPost("update-player")] public async Task UpdateRegistrationPlayer([FromBody] UpdatePlayerInput input) { var userId = GetCurrentUserId(); var person = await _personRepository.GetFirstAsync(p => p.Id == input.PersonId); if (person == null) throw Oops.Bah("参赛人不存在"); // 校验权限:确保是该用户创建的报名 var reg = await _registrationRepository.GetFirstAsync(r => r.Id == person.RegistrationId); if (reg == null || reg.CreatedUserId != userId) throw Oops.Bah("无权操作"); person.Name = input.Name; person.IdCard = input.IdCard; person.Gender = input.Gender; person.Phone = input.Phone; person.UpdatedTime = DateTime.Now; person.UpdatedUserId = userId; return await _personRepository.UpdateAsync(person); } /// /// 获取我的已报赛事列表 /// /// [HttpGet("my-events")] public async Task> GetMyEventList([FromQuery] int page = 1, [FromQuery] int pageSize = 10) { var userId = GetCurrentUserId(); var query = _registrationRepository.AsQueryable() .LeftJoin((r, e) => r.EventId == e.Id) .Where(r => r.CreatedUserId == userId && !r.DeleteMark) .OrderByDescending(r => r.RegistrationTime) .Select((r, e) => new MyEventListDto { RegistrationId = r.Id, EventId = r.EventId, EventName = e.EventName, CoverImage = e.CoverImage, RegistrationStatus = r.RegistrationStatus, PaymentStatus = r.PaymentStatus, RegistrationTime = r.RegistrationTime, ProjectCount = SqlFunc.Subqueryable().Where(i => i.RegistrationId == r.Id).Count() }); return await query.ToPagedListAsync(new Pagination { PageNo = page, PageSize = pageSize }); } /// /// 获取报名详情(包含参赛人) /// [HttpGet("my-registration/{registrationId}")] public async Task GetRegistrationDetail(long registrationId) { var userId = GetCurrentUserId(); var reg = await _registrationRepository.AsQueryable() .LeftJoin((r, e) => r.EventId == e.Id) .Where(r => r.Id == registrationId && r.CreatedUserId == userId) .Select((r, e) => new { r.Id, r.EventId, e.EventName, e.CoverImage, r.RegistrationStatus, r.PaymentStatus, r.TotalAmount, r.OrderNo, r.RegistrationTime, r.ReviewRemark, // 为了计算 EventStatus RegStart = e.RegistrationStartTime, RegEnd = e.RegistrationEndTime, MatchStart = e.MatchStartTime, MatchEnd = e.MatchEndTime }) .FirstAsync(); if (reg == null) throw Oops.Bah("报名记录不存在"); // 计算 EventStatus EventStatus status = EventStatus.Enrolling; var now = DateTime.Now; if (now >= reg.RegStart && now <= reg.RegEnd) status = EventStatus.Enrolling; else if (now >= reg.MatchStart && now <= reg.MatchEnd) status = EventStatus.Ongoing; else if (now > reg.MatchEnd) status = EventStatus.Ended; // 获取 Items 和 Persons var items = await _registrationItemRepository.AsQueryable() .Where(i => i.RegistrationId == registrationId) .ToListAsync(); var persons = await _personRepository.AsQueryable() .Where(p => p.RegistrationId == registrationId) .ToListAsync(); return new RegistrationDetailDto { RegistrationId = reg.Id, EventId = reg.EventId, EventName = reg.EventName, CoverImage = reg.CoverImage, EventStatus = status, RegistrationStatus = reg.RegistrationStatus, PaymentStatus = reg.PaymentStatus, TotalAmount = reg.TotalAmount, OrderNo = reg.OrderNo, RegistrationTime = reg.RegistrationTime, ReviewRemark = reg.ReviewRemark, Items = items.Select(i => new RegistrationItemDto { Id = i.Id, ProjectName = i.ProjectName, GroupName = i.GroupName, Price = i.UnitPrice, // 单价 Persons = persons.Where(p => p.RegistrationItemId == i.Id).Select(p => new RegistrationPersonDto { Id = p.Id, Name = p.Name, IdCard = p.IdCard, Gender = p.Gender, Phone = p.Phone }).ToList() }).ToList() }; } /// /// 查询报名状态 /// /// /// [HttpGet("status/{registrationId}")] public async Task GetRegistrationStatus(long registrationId) { var userId = GetCurrentUserId(); var reg = await _registrationRepository.GetFirstAsync(r => r.Id == registrationId && r.CreatedUserId == userId); if (reg == null) throw Oops.Bah("报名记录不存在"); return new RegistrationStatusDto { RegistrationId = reg.Id, RegistrationStatus = reg.RegistrationStatus, PaymentStatus = reg.PaymentStatus, RejectReason = reg.ReviewRemark }; } /// /// 取消报名 /// /// /// [HttpPost("cancel/{registrationId}")] public async Task CancelRegistration(long registrationId) { var userId = GetCurrentUserId(); var reg = await _registrationRepository.GetFirstAsync(r => r.Id == registrationId && r.CreatedUserId == userId); if (reg == null) throw Oops.Bah("报名记录不存在"); // 校验状态:只有待提交、待审核、未支付状态可直接取消 // 已支付状态需走退款流程(暂不实现) if (reg.PaymentStatus == 1) throw Oops.Bah("已支付订单请联系客服退款"); if (reg.RegistrationStatus == 4) throw Oops.Bah("订单已取消,无需重复操作"); reg.RegistrationStatus = 4; // 4=已取消 reg.UpdatedTime = DateTime.Now; reg.UpdatedUserId = userId; return await _registrationRepository.UpdateAsync(reg); } private EventStatus CalculateEventStatus(EventInfo e) { var now = DateTime.Now; if (now >= e.RegistrationStartTime && now <= e.RegistrationEndTime) return EventStatus.Enrolling; // 报名中 if (now >= e.MatchStartTime && now <= e.MatchEndTime) return EventStatus.Ongoing; // 比赛中 if (now > e.MatchEndTime) return EventStatus.Ended; // 已结束 return EventStatus.Enrolling; // 默认? 或者新增未开始状态 } }