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);
+ }
+}