diff --git a/YD_Event.Application/Client/Dtos/MyDtos.cs b/YD_Event.Application/Client/Dtos/MyDtos.cs new file mode 100644 index 0000000..be45364 --- /dev/null +++ b/YD_Event.Application/Client/Dtos/MyDtos.cs @@ -0,0 +1,124 @@ +using System.ComponentModel.DataAnnotations; +using YD_Event.Core.Enum; +using SqlSugar; + +namespace YD_Event.Application.Client.Dtos; + +/// +/// 通用分页输入 +/// +public class MyPageInput : Pagination +{ +} + +/// +/// 个人中心-首页信息 +/// +public class MyProfileDto +{ + public string NickName { get; set; } + public string Avatar { get; set; } + public string Mobile { get; set; } + public int Points { get; set; } + public int CertificateCount { get; set; } + + /// + /// 实名认证状态:0-未认证,1-已认证,2-审核中 + /// + public int AuthStatus { get; set; } +} + +/// +/// 个人中心-实名认证信息 +/// +public class MyAuthInfoDto +{ + public string Name { get; set; } + public string Mobile { get; set; } + public string IdCardNumber { get; set; } + public string IdCardImageUrl { get; set; } // 正面 + public string IdCardBackImageUrl { get; set; } // 反面 + public AuditStatus AuditStatus { get; set; } + public string? AuditRemark { get; set; } +} + +/// +/// 提交实名认证 +/// +public class SubmitAuthInput +{ + [Required(ErrorMessage = "姓名不能为空")] + public string Name { get; set; } + + [Required(ErrorMessage = "手机号不能为空")] + public string Mobile { get; set; } + + [Required(ErrorMessage = "身份证号不能为空")] + public string IdCardNumber { get; set; } + + public string? IdCardImageUrl { get; set; } + public string? IdCardBackImageUrl { get; set; } +} + +/// +/// 个人中心-我的团队 +/// +public class MyTeamDto +{ + public long Id { get; set; } + public string Name { get; set; } + public DateTime EstablishedDate { get; set; } + public int MemberCount { get; set; } // 队员数量 + public bool IsLeader { get; set; } // 是否队长 +} + +/// +/// 个人中心-我的证书 +/// +public class MyCertificateDto +{ + public long Id { get; set; } + public string EventName { get; set; } // 赛事名称 (需要联表查) + public string ProjectName { get; set; } // 项目名称 + public string CertificateName { get; set; } // 证书名称 + public string CertificateUrl { get; set; } + public DateTime? IssuedTime { get; set; } +} + +/// +/// 个人中心-积分记录 +/// +public class MyPointsRecordDto +{ + public long Id { get; set; } + public string Title { get; set; } // 标题:如 "参加xxx赛事获得" + public decimal Points { get; set; } + public DateTime CreatedTime { get; set; } + public int Type { get; set; } // 1-获取,2-消耗 +} + +/// +/// 个人中心-我的订单 +/// +public class MyOrderDto +{ + public long Id { get; set; } + public string OrderNo { get; set; } + public string EventName { get; set; } + public decimal Amount { get; set; } + public int Status { get; set; } // OrderStatus + public DateTime CreatedTime { get; set; } +} + +/// +/// 个人中心-参赛记录 +/// +public class MyRegistrationDto +{ + public long Id { get; set; } + public string EventName { get; set; } + public string TeamName { get; set; } // 个人赛则为个人 + public int Status { get; set; } // RegistrationStatus + public DateTime RegistrationTime { get; set; } + public string ProjectNames { get; set; } // 报名项目集合 +} diff --git a/YD_Event.Application/Client/MyController.cs b/YD_Event.Application/Client/MyController.cs new file mode 100644 index 0000000..e783d88 --- /dev/null +++ b/YD_Event.Application/Client/MyController.cs @@ -0,0 +1,295 @@ +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; + +namespace YD_Event.Application.Client; + +/// +/// 小程序-个人中心接口 +/// +[ApiDescriptionSettings("微信小程序接口")] +[Route("api/v1/mini-program/my")] +[Authorize(AuthenticationSchemes = "Bearer")] +public class MyController : IDynamicApiController +{ + private readonly ISqlSugarRepository _appUserRepository; + private readonly ISqlSugarRepository _teamRepository; + private readonly ISqlSugarRepository _certificateRepository; + private readonly ISqlSugarRepository _pointsRecordRepository; + private readonly ISqlSugarRepository _orderRepository; + private readonly ISqlSugarRepository _registrationRepository; + private readonly ISqlSugarRepository _eventRepository; + private readonly IHttpContextAccessor _httpContextAccessor; + + public MyController( + ISqlSugarRepository appUserRepository, + ISqlSugarRepository teamRepository, + ISqlSugarRepository certificateRepository, + ISqlSugarRepository pointsRecordRepository, + ISqlSugarRepository orderRepository, + ISqlSugarRepository registrationRepository, + ISqlSugarRepository eventRepository, + IHttpContextAccessor httpContextAccessor) + { + _appUserRepository = appUserRepository; + _teamRepository = teamRepository; + _certificateRepository = certificateRepository; + _pointsRecordRepository = pointsRecordRepository; + _orderRepository = orderRepository; + _registrationRepository = registrationRepository; + _eventRepository = eventRepository; + _httpContextAccessor = httpContextAccessor; + } + + 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(); + // 注意:AuthAccount.Id 和 AppUser.UserId 关联。这里假设登录的是 AuthAccount,需要查 AppUser + // 实际上 AppUser 表的 UserId 字段关联的是 AuthAccount 的 Id。 + // 但 AppUser 本身也有 Id。 + // 业务逻辑通常是:一个 AuthAccount 对应一个 AppUser。 + + var user = await _appUserRepository.GetFirstAsync(u => u.UserId == userId); + if (user == null) throw Oops.Bah("用户信息不存在"); + + // 统计证书数量 + var certCount = await _certificateRepository.CountAsync(c => c.CreatedUserId == userId && c.CertificateStatus == 2); + + // 积分 (需计算积分总和,PointsRecord 表有 Points 字段,Type 1-加 2-减) + // 简单计算:Sum(Type=1) - Sum(Type=2) + var pointsIn = await _pointsRecordRepository.AsQueryable().Where(p => p.UserId == userId && p.Type == PointsOperationType.Acquire).SumAsync(p => p.Points); + var pointsOut = await _pointsRecordRepository.AsQueryable().Where(p => p.UserId == userId && p.Type == PointsOperationType.Consume).SumAsync(p => p.Points); + var pointsBalance = (int)(pointsIn - pointsOut); + + // 认证状态: AuditStatus: 0-待提交, 1-待审核, 2-通过, 3-拒绝. + int authStatus = 0; + if (string.IsNullOrEmpty(user.Name) || string.IsNullOrEmpty(user.IdCardNumber)) + { + authStatus = 0; // 未认证 + } + else if (user.AuditStatus == AuditStatus.Pass) + { + authStatus = 1; // 已认证 + } + else if (user.AuditStatus == AuditStatus.PendingAudit) + { + authStatus = 2; // 审核中 + } + else + { + authStatus = 3; // 失败/其他 + } + + return new MyProfileDto + { + NickName = user.NickName ?? "微信用户", + Avatar = user.Avatar ?? "", + Mobile = MaskMobile(user.Mobile), + Points = pointsBalance, + CertificateCount = certCount, + AuthStatus = authStatus + }; + } + + /// + /// 获取实名认证信息 + /// + /// + [HttpGet("auth")] + public async Task GetAuthInfo() + { + var userId = GetCurrentUserId(); + var user = await _appUserRepository.GetFirstAsync(u => u.UserId == userId); + if (user == null) throw Oops.Bah("用户信息不存在"); + + return new MyAuthInfoDto + { + Name = user.Name, + Mobile = user.Mobile, + IdCardNumber = user.IdCardNumber, // 可以在前端脱敏 + IdCardImageUrl = user.IdCardImageUrl, + 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.UserId == userId); + if (user == null) throw Oops.Bah("用户信息不存在"); + + if (user.AuditStatus == AuditStatus.Pass) throw Oops.Bah("已通过实名认证,无需重复提交"); + if (user.AuditStatus == AuditStatus.PendingAudit) throw Oops.Bah("审核中,请勿重复提交"); + + user.Name = input.Name; + user.Mobile = input.Mobile; + user.IdCardNumber = input.IdCardNumber; + user.IdCardImageUrl = input.IdCardImageUrl; + 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(); + // 逻辑:查找我是创建人(队长) 或 队员的团队 + // 目前 Team 表只有 CreatedUserId,假设 CreatedUserId 就是队长。 + // 如果有 TeamMember 表关联队员,需要联表。目前假设只查我创建的团队(根据原型图"我的团队"通常指我管理的)。 + // 如果原型包含"我加入的",需要查 TeamMember。 + // 鉴于 TeamMember 实体未直接给出,这里先查 CreatedUserId = userId + + var query = _teamRepository.AsQueryable() + .Where(t => t.CreatedUserId == userId && !t.DeleteMark) + .Select(t => new MyTeamDto + { + Id = t.Id, + Name = t.Name, + EstablishedDate = t.EstablishedDate, + IsLeader = true, + MemberCount = 0 // 暂无 Member 表关联,置为 0 或需扩展 + }); + + return await query.ToPagedListAsync(input); + } + + /// + /// 我的证书列表 + /// + /// + [HttpGet("certificates")] + public async Task> GetCertificates([FromQuery] MyPageInput input) + { + var userId = GetCurrentUserId(); + + // 联表查询 EventInfo 获取赛事名称 + var query = _certificateRepository.AsQueryable() + .LeftJoin((c, e) => c.EventId == e.Id) + .Where(c => c.CreatedUserId == userId && c.CertificateStatus == 2 && !c.DeleteMark) + .Select((c, e) => new MyCertificateDto + { + Id = c.Id, + EventName = e.EventName, + ProjectName = c.GroupName, // 暂时用 GroupName 代替 ProjectName,具体看 ProjectId 关联 + CertificateName = "荣誉证书", // 此时可根据 CertificateType 转义 + CertificateUrl = c.CertificateUrl, + IssuedTime = c.IssuedTime + }) + .OrderByDescending(c => c.IssuedTime); + + return await query.ToPagedListAsync(input); + } + + /// + /// 我的积分记录 + /// + /// + [HttpGet("points")] + public async Task> GetPoints([FromQuery] MyPageInput input) + { + var userId = GetCurrentUserId(); + var query = _pointsRecordRepository.AsQueryable() + .Where(p => p.UserId == userId && !p.DeleteMark) + .Select(p => new MyPointsRecordDto + { + Id = p.Id, + Title = p.Remark ?? (p.Type == PointsOperationType.Acquire ? "获取积分" : "消耗积分"), + Points = p.Points, + Type = (int)p.Type, + CreatedTime = p.CreatedTime + }) + .OrderByDescending(p => p.CreatedTime); + + return await query.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 => o.CreatedUserId == userId && !o.DeleteMark) + .Select((o, e) => new MyOrderDto + { + Id = o.Id, + OrderNo = o.OrderNo, + EventName = e.EventName, + Amount = o.ActualAmount, + Status = (int)o.OrderStatus, + CreatedTime = o.CreatedTime + }) + .OrderByDescending(o => o.CreatedTime); + + return await query.ToPagedListAsync(input); + } + + /// + /// 我的参赛记录 + /// + /// + [HttpGet("registrations")] + public async Task> GetRegistrations([FromQuery] MyPageInput input) + { + var userId = GetCurrentUserId(); + var query = _registrationRepository.AsQueryable() + .LeftJoin((r, e) => r.EventId == e.Id) + .Where(r => r.CreatedUserId == userId && !r.DeleteMark) + .Select((r, e) => new MyRegistrationDto + { + Id = r.Id, + EventName = e.EventName, + TeamName = r.TeamName, + Status = r.RegistrationStatus, + RegistrationTime = r.RegistrationTime, + ProjectNames = "" // 暂空,需联表 EventRegistrationItem + }) + .OrderByDescending(r => r.RegistrationTime); + + return await query.ToPagedListAsync(input); + } + + private string MaskMobile(string mobile) + { + if (string.IsNullOrEmpty(mobile) || mobile.Length < 7) return mobile; + return mobile.Substring(0, 3) + "****" + mobile.Substring(7); + } +}