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 System.Security.Claims; using Furion.FriendlyException; using EasyCaching.Core; using ProjectEntity = YD_Event.Core.Entities.Project; namespace YD_Event.Application.Client; /// /// 小程序-个人中心接口 /// [ApiDescriptionSettings("微信小程序接口")] [Authorize] public class MyController : IDynamicApiController { private readonly ISqlSugarRepository _appUserRepository; private readonly ISqlSugarRepository _teamRepository; private readonly ISqlSugarRepository _teamMemberRepository; private readonly ISqlSugarRepository _certificateRepository; private readonly ISqlSugarRepository _pointsRecordRepository; private readonly ISqlSugarRepository _orderRepository; private readonly ISqlSugarRepository _registrationRepository; private readonly ISqlSugarRepository _registrationItemRepository; private readonly ISqlSugarRepository _eventRepository; private readonly ISqlSugarRepository _merchantCertificationRepository; private readonly IHttpContextAccessor _httpContextAccessor; private readonly IEasyCachingProvider _cachingProvider; public MyController( ISqlSugarRepository appUserRepository, ISqlSugarRepository teamRepository, ISqlSugarRepository teamMemberRepository, ISqlSugarRepository certificateRepository, ISqlSugarRepository pointsRecordRepository, ISqlSugarRepository orderRepository, ISqlSugarRepository registrationRepository, ISqlSugarRepository registrationItemRepository, ISqlSugarRepository eventRepository, ISqlSugarRepository merchantCertificationRepository, IHttpContextAccessor httpContextAccessor, IEasyCachingProvider cachingProvider) { _appUserRepository = appUserRepository; _teamRepository = teamRepository; _teamMemberRepository = teamMemberRepository; _certificateRepository = certificateRepository; _pointsRecordRepository = pointsRecordRepository; _orderRepository = orderRepository; _registrationRepository = registrationRepository; _registrationItemRepository = registrationItemRepository; _eventRepository = eventRepository; _merchantCertificationRepository = merchantCertificationRepository; _httpContextAccessor = httpContextAccessor; _cachingProvider = cachingProvider; } 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("profile")] public async Task GetProfile() { var userId = GetCurrentUserId(); var user = await _appUserRepository.GetFirstAsync(u => u.Id == userId); if (user == null) throw Oops.Bah("用户信息不存在"); // 统计证书数量 var certCount = await _certificateRepository.CountAsync(c => c.UserId == userId && c.CertificateStatus == 2); // 小程序端只查看个人积分:UserId = 当前用户且 MerchantCertificationId = null var pointsIn = await _pointsRecordRepository.AsQueryable().Where(p => p.UserId == userId && p.MerchantCertificationId == null && p.Type == PointsOperationType.Acquire).SumAsync(p => p.Points); var pointsOut = await _pointsRecordRepository.AsQueryable().Where(p => p.UserId == userId && p.MerchantCertificationId == null && p.Type == PointsOperationType.Consume).SumAsync(p => p.Points); var pointsBalance = (int)(pointsIn - pointsOut); // 认证状态: 0-待提交, 1-已认证, 2-审核中, 3-失败 int authStatus = (int)user.AuditStatus; // 获取单位名称 string? merchantName = null; if (user.MerchantCertificationId.HasValue) { var merchantCert = await _merchantCertificationRepository.GetFirstAsync(m => m.Id == user.MerchantCertificationId.Value); merchantName = merchantCert?.MerchantName; } return new MyProfileDto { NickName = user.NickName ?? "微信用户", Avatar = user.Avatar ?? "", Mobile = MaskMobile(user.Mobile), Points = pointsBalance, CertificateCount = certCount, AuthStatus = authStatus, MerchantName = merchantName, Gender = (int?)user.Gender, BirthDate = user.BirthDate }; } /// /// 获取实名认证信息 /// /// [HttpGet("auth")] public async Task GetAuthInfo() { var userId = GetCurrentUserId(); var user = await _appUserRepository.GetFirstAsync(u => u.Id == userId); if (user == null) throw Oops.Bah("用户信息不存在"); int authType = 1; // 默认身份证认证 if (!string.IsNullOrEmpty(user.PassportPhotoUrl)) { authType = 2; } return new MyAuthInfoDto { AuthType = authType, Name = user.Name, Mobile = user.Mobile, Gender = (int?)user.Gender, BirthDate = user.BirthDate, IdCardNumber = user.IdCardNumber, IdCardImageUrl = user.IdCardImageUrl, IdCardBackImageUrl = user.IdCardBackImageUrl, PassportPhotoUrl = user.PassportPhotoUrl, AuditStatus = user.AuditStatus, AuditRemark = user.AuditRemark }; } /// /// 提交实名认证 /// /// /// [HttpPost("auth")] public async Task SubmitAuth([FromBody] SubmitAuthInput input) { var userId = GetCurrentUserId(); var user = await _appUserRepository.GetFirstAsync(u => u.Id == userId); if (user == null) throw Oops.Bah("用户信息不存在"); if (user.AuditStatus == AuditStatus.PendingAudit) throw Oops.Bah("审核中,请勿重复提交"); // 验证必填字段 if (input.AuthType == 1) { 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) { if (string.IsNullOrEmpty(input.PassportPhotoUrl)) throw Oops.Bah("护照图片不能为空"); if (!input.Gender.HasValue) throw Oops.Bah("性别不能为空"); if (!input.BirthDate.HasValue) throw Oops.Bah("出生日期不能为空"); } else { throw Oops.Bah("不支持的认证类型"); } 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; if (input.AuthType == 1) { user.IdCardNumber = input.IdCardNumber; user.IdCardImageUrl = input.IdCardImageUrl; user.IdCardBackImageUrl = input.IdCardBackImageUrl; user.PassportPhotoUrl = null; // 清空护照图片 user.Gender = (Gender)input.Gender.Value; user.BirthDate = input.BirthDate.Value; } else { user.PassportPhotoUrl = input.PassportPhotoUrl; user.IdCardImageUrl = null; // 清空身份证图片 user.IdCardBackImageUrl = null; // 清空身份证背面图片 user.Gender = (Gender)input.Gender.Value; user.BirthDate = input.BirthDate.Value; } user.AuditStatus = AuditStatus.PendingAudit; user.UpdatedTime = DateTime.Now; return await _appUserRepository.UpdateAsync(user); } /// /// 我的团队列表 /// /// [HttpGet("teams")] public async Task> GetTeams([FromQuery] MyPageInput input) { var userId = GetCurrentUserId(); var myTeamIds = await _teamMemberRepository.AsQueryable() .Where(tm => tm.UserId == userId && !tm.DeleteMark) .Select(tm => tm.TeamId) .ToListAsync(); var query = _teamRepository.AsQueryable() .Where(t => (t.CreatedUserId == userId || myTeamIds.Contains(t.Id)) && !t.DeleteMark); var teams = await query.ToPagedListAsync(input); var teamIds = teams.Rows.Select(t => t.Id).ToList(); var teamMembers = await _teamMemberRepository.AsQueryable() .Where(tm => teamIds.Contains(tm.TeamId) && !tm.DeleteMark) .ToListAsync(); var memberCounts = teamMembers .Where(tm => tm.PersonType == PersonType.Athlete) .GroupBy(tm => tm.TeamId) .ToDictionary(g => g.Key, g => g.Count()); var result = new PageResult { PageNo = teams.PageNo, PageSize = teams.PageSize, Total = teams.Total, Pages = teams.Pages, Rows = teams.Rows.Select(t => new MyTeamDto { Id = t.Id, Name = t.Name, IsLeader = t.CreatedUserId == userId, MemberCount = memberCounts.TryGetValue(t.Id, out var count) ? count : 0 }).ToList() }; return result; } /// /// 团队详情 /// /// 团队ID /// [HttpGet("teams/{id}")] public async Task GetTeamDetail(long id) { var userId = GetCurrentUserId(); var team = await _teamRepository.GetFirstAsync(t => t.Id == id && !t.DeleteMark); if (team == null) throw Oops.Bah("团队不存在"); var teamMembers = await _teamMemberRepository.AsQueryable() .Where(tm => tm.TeamId == id && !tm.DeleteMark) .ToListAsync(); if (!teamMembers.Any(tm => tm.UserId == userId) && team.CreatedUserId != userId) { throw Oops.Bah("无权访问该团队"); } var userIds = teamMembers.Select(tm => tm.UserId).ToList(); var users = await _appUserRepository.AsQueryable() .Where(u => userIds.Contains(u.Id)) .ToListAsync(); var userDict = users.ToDictionary(u => u.Id); var members = teamMembers.Select(tm => { userDict.TryGetValue(tm.UserId, out var user); int? age = null; if (user?.BirthDate.HasValue == true) { var today = DateTime.Today; age = today.Year - user.BirthDate.Value.Year; if (user.BirthDate.Value.Date > today.AddYears(-age.Value)) age--; } return new MyTeamMemberDto { Name = user?.Name ?? "未知", Gender = (int)(user?.Gender ?? 0), Age = age, PersonType = (int)tm.PersonType, Mobile = user?.Mobile != null ? MaskMobile(user.Mobile) : null }; }).ToList(); var leader = members.FirstOrDefault(m => m.PersonType == 3); var coach = members.FirstOrDefault(m => m.PersonType == 2); return new MyTeamDetailDto { Id = team.Id, Name = team.Name, Leader = leader, Coach = coach, CreatedTime = team.CreatedTime, MemberCount = members.Count(m => m.PersonType == 1), Members = members }; } /// /// 我的证书总览 /// /// [HttpGet("certificates/overview")] public async Task GetCertificatesOverview() { var userId = GetCurrentUserId(); var count = await _certificateRepository.CountAsync(c => c.UserId == userId && c.CertificateStatus == 2 && !c.DeleteMark); return new MyCertificateOverviewDto { TotalCount = count }; } /// /// 我的证书列表 /// /// [HttpGet("certificates")] public async Task> GetCertificates([FromQuery] MyCertificatePageInput input) { var userId = GetCurrentUserId(); // 先查询原始数据,然后在内存中转换证书名称 var query = _certificateRepository.AsQueryable() .LeftJoin((c, e) => c.EventId == e.Id) .LeftJoin((c, e, ep) => c.ProjectId == ep.Id) .LeftJoin((c, e, ep, p) => ep.ProjectId == p.Id) .Where((c, e, ep, p) => c.UserId == userId && c.CertificateStatus == 2 && !c.DeleteMark); // 根据类型筛选 if (input.Type.HasValue) { if (input.Type == 1) { // 个人证书:没有单位认证ID query = query.Where((c, e, ep, p) => !c.MerchantCertificationId.HasValue); } else if (input.Type == 2) { // 团队证书:有单位认证ID query = query.Where((c, e, ep, p) => c.MerchantCertificationId.HasValue); } } var resultQuery = query.Select((c, e, ep, p) => new { Id = c.Id, EventName = e.EventName, ProjectName = p.Name, GroupName = c.GroupName, CertificateType = c.CertificateType, CertificateUrl = c.CertificateUrl, IssuedTime = c.IssuedTime }) .OrderByDescending(c => c.IssuedTime); var pagedResult = await resultQuery.ToPagedListAsync(input); // 在内存中转换证书名称 var result = new PageResult { PageNo = pagedResult.PageNo, PageSize = pagedResult.PageSize, Total = pagedResult.Total, Pages = pagedResult.Pages, Rows = pagedResult.Rows.Select(x => new MyCertificateDto { Id = x.Id, EventName = x.EventName, ProjectName = x.ProjectName, GroupName = x.GroupName, CertificateName = GetCertificateName(x.CertificateType), CertificateUrl = x.CertificateUrl, IssuedTime = x.IssuedTime }).ToList() }; return result; } private string GetCertificateName(int certificateType) { return certificateType switch { 1 => "冠军证书", 2 => "亚军证书", 3 => "季军证书", 4 => "优胜证书", 5 => "参与证书", _ => "荣誉证书" }; } /// /// 我的积分总览 /// /// [HttpGet("points/overview")] public async Task GetPointsOverview() { var userId = GetCurrentUserId(); // 小程序端只查看个人积分:UserId = 当前用户且 MerchantCertificationId = null var pointsIn = await _pointsRecordRepository.AsQueryable().Where(p => p.UserId == userId && p.MerchantCertificationId == null && p.Type == PointsOperationType.Acquire).SumAsync(p => p.Points); var pointsOut = await _pointsRecordRepository.AsQueryable().Where(p => p.UserId == userId && p.MerchantCertificationId == null && p.Type == PointsOperationType.Consume).SumAsync(p => p.Points); var pointsBalance = (int)(pointsIn - pointsOut); return new MyPointsOverviewDto { TotalPoints = pointsBalance }; } /// /// 我的积分记录 /// /// [HttpGet("points")] public async Task> GetPoints([FromQuery] MyPointsPageInput input) { var userId = GetCurrentUserId(); var query = _pointsRecordRepository.AsQueryable() .LeftJoin((p, e) => p.EventId == e.Id) .LeftJoin((p, e, ep) => p.ProjectId == ep.Id) .LeftJoin((p, e, ep, pr) => ep.ProjectId == pr.Id) .LeftJoin((p, e, ep, pr, pg) => p.ProjectGroupId == pg.Id) .LeftJoin((p, e, ep, pr, pg, r) => p.ResultId == r.Id && !r.DeleteMark) // 小程序端只查看个人积分:UserId = 当前用户且 MerchantCertificationId = null .Where((p, e, ep, pr, pg, r) => p.UserId == userId && p.MerchantCertificationId == null && !p.DeleteMark); if (input.Type.HasValue) { var typeFilter = (PointsOperationType)input.Type.Value; query = query.Where((p, e, ep, pr, pg, r) => p.Type == typeFilter); } // 如果前端指定了 MerchantCertificationId,优先使用前端的(保持向后兼容) if (input.MerchantCertificationId.HasValue) { query = query.Where((p, e, ep, pr, pg, r) => p.MerchantCertificationId == input.MerchantCertificationId.Value); } var resultQuery = query.Select((p, e, ep, pr, pg, r) => new MyPointsRecordDto { Id = p.Id, Title = p.Remark ?? (p.Type == PointsOperationType.Acquire ? "获取积分" : "消耗积分"), EventName = e.EventName, ProjectName = pr.Name, GroupName = pg.GroupName, Points = p.Points, Type = (int)p.Type, CreatedTime = p.CreatedTime, Rank = r.Rank > 0 ? r.Rank : null // 从成绩记录中获取名次 }) .OrderByDescending(p => p.CreatedTime); return await resultQuery.ToPagedListAsync(input); } /// /// 我的订单列表 /// /// [HttpGet("orders")] public async Task> GetOrders([FromQuery] MyPageInput input) { var userId = GetCurrentUserId(); var query = _orderRepository.AsQueryable() .LeftJoin((o, e) => o.EventId == e.Id) .Where((o, e) => o.CreatedUserId == userId && !o.DeleteMark) .Select((o, e) => new MyOrderDto { Id = o.Id, OrderNo = o.OrderNo, EventName = e.EventName, ActualAmount = o.ActualAmount, RefundAmount = o.OrderType == 2 ? o.ActualAmount : null, OrderStatus = o.OrderStatus, PaymentTime = o.PaymentTime, CreatedTime = o.CreatedTime }) .OrderByDescending(o => o.CreatedTime); return await query.ToPagedListAsync(input); } /// /// 订单详情 /// /// 订单ID /// [HttpGet("orders/{id}")] public async Task GetOrderDetail(long id) { var userId = GetCurrentUserId(); // 1. 先查询订单 var order = await _orderRepository.GetFirstAsync(o => o.Id == id && o.CreatedUserId == userId && !o.DeleteMark); if (order == null) throw Oops.Bah("订单不存在"); // 2. 查询关联的报名信息 EventRegistration? registration = null; if (order.RegistrationId > 0) { registration = await _registrationRepository.GetFirstAsync(r => r.Id == order.RegistrationId && !r.DeleteMark); } // 3. 获取赛事名称 var eventName = await _eventRepository.AsQueryable() .Where(e => e.Id == order.EventId) .Select(e => e.EventName) .FirstAsync(); // 4. 获取报名项目列表(如果有关联的报名记录) List items = new List(); if (order.RegistrationId > 0) { items = await _registrationItemRepository.AsQueryable() .Where(i => i.RegistrationId == order.RegistrationId && !i.DeleteMark) .Select(i => new MyOrderItemDto { ProjectName = i.ProjectName, GroupName = i.GroupName, Fee = i.TotalPrice }) .ToListAsync(); } return new MyOrderDetailDto { Id = order.Id, OrderNo = order.OrderNo, EventName = eventName, ActualAmount = order.ActualAmount, RefundAmount = order.OrderType == 2 ? order.ActualAmount : null, RefundTime = order.OrderType == 2 ? order.PaymentTime : null, OrderStatus = order.OrderStatus, PaymentTime = order.PaymentTime, PaymentMethod = registration?.PaymentMethod ?? 0, Items = items }; } /// /// 我的参赛记录 /// /// [HttpGet("registrations")] [DisplayName("我的参赛记录")] public async Task> GetRegistrations([FromQuery] MyPageInput input) { var userId = GetCurrentUserId(); // 1. 先分页查询报名记录 var regQuery = _registrationRepository.AsQueryable() .Where(r => r.CreatedUserId == userId && !r.DeleteMark) .OrderByDescending(r => r.RegistrationTime); var pagedRegs = await regQuery.ToPagedListAsync(input); // 2. 获取所有赛事ID,然后批量查询赛事信息 var eventIds = pagedRegs.Rows.Select(r => r.EventId).Distinct().ToList(); var eventList = await _eventRepository.AsQueryable() .Where(e => eventIds.Contains(e.Id)) .ToListAsync(); var eventsDict = eventList.ToDictionary(e => e.Id, e => e.EventName); // 3. 构建 DTO 列表 var result = new PageResult { PageNo = pagedRegs.PageNo, PageSize = pagedRegs.PageSize, Total = pagedRegs.Total, Pages = pagedRegs.Pages, Rows = pagedRegs.Rows.Select(r => new MyRegistrationDto { Id = r.Id, EventName = eventsDict.ContainsKey(r.EventId) ? eventsDict[r.EventId] : "", TeamName = r.TeamName, Status = r.PaymentStatus, RegistrationTime = r.RegistrationTime, ProjectNames = "" }).ToList() }; // 4. 填充项目名称 if (result.Rows.Any()) { var registrationIds = result.Rows.Select(r => r.Id).ToList(); var items = await _registrationItemRepository.AsQueryable() .Where(i => registrationIds.Contains(i.RegistrationId) && !i.DeleteMark) .ToListAsync(); var itemsByRegistrationId = items.GroupBy(i => i.RegistrationId) .ToDictionary(g => g.Key, g => string.Join("、", g.Select(i => i.ProjectName))); foreach (var row in result.Rows) { if (itemsByRegistrationId.TryGetValue(row.Id, out var projectNames)) { row.ProjectNames = projectNames; } } } return result; } private string MaskMobile(string mobile) { if (string.IsNullOrEmpty(mobile) || mobile.Length < 7) return mobile; return mobile.Substring(0, 3) + "****" + mobile.Substring(7); } }