马梓彭 99a1857088 完善二期接口
补齐接口文档和上线SQL
优化底层保存逻辑
2026-08-30 21:46:05 +08:00

1017 lines
39 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Microsoft.AspNetCore.Mvc;
using YD_Event.Application.Client.Dtos;
using YD_Event.Application.Common;
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 SkiaSharp;
using ProjectEntity = YD_Event.Core.Entities.Project;
namespace YD_Event.Application.Client;
/// <summary>
/// 小程序-个人中心接口
/// </summary>
[ApiDescriptionSettings("微信小程序接口")]
[Authorize]
public class MyController : IDynamicApiController
{
private readonly ISqlSugarRepository<YD_Event.Core.Entities.AppUser> _appUserRepository;
private readonly ISqlSugarRepository<YD_Event.Core.Entities.Team> _teamRepository;
private readonly ISqlSugarRepository<TeamMember> _teamMemberRepository;
private readonly ISqlSugarRepository<EventCertificate> _certificateRepository;
private readonly ISqlSugarRepository<PointsRecord> _pointsRecordRepository;
private readonly ISqlSugarRepository<YD_Event.Core.Entities.Order> _orderRepository;
private readonly ISqlSugarRepository<EventRegistration> _registrationRepository;
private readonly ISqlSugarRepository<EventRegistrationItem> _registrationItemRepository;
private readonly ISqlSugarRepository<EventInfo> _eventRepository;
private readonly ISqlSugarRepository<MerchantCertification> _merchantCertificationRepository;
private readonly ISqlSugarClient _db;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IEasyCachingProvider _cachingProvider;
private readonly FileService _fileService;
public MyController(
ISqlSugarRepository<YD_Event.Core.Entities.AppUser> appUserRepository,
ISqlSugarRepository<YD_Event.Core.Entities.Team> teamRepository,
ISqlSugarRepository<TeamMember> teamMemberRepository,
ISqlSugarRepository<EventCertificate> certificateRepository,
ISqlSugarRepository<PointsRecord> pointsRecordRepository,
ISqlSugarRepository<YD_Event.Core.Entities.Order> orderRepository,
ISqlSugarRepository<EventRegistration> registrationRepository,
ISqlSugarRepository<EventRegistrationItem> registrationItemRepository,
ISqlSugarRepository<EventInfo> eventRepository,
ISqlSugarRepository<MerchantCertification> merchantCertificationRepository,
ISqlSugarClient db,
IHttpContextAccessor httpContextAccessor,
IEasyCachingProvider cachingProvider,
FileService fileService)
{
_appUserRepository = appUserRepository;
_teamRepository = teamRepository;
_teamMemberRepository = teamMemberRepository;
_certificateRepository = certificateRepository;
_pointsRecordRepository = pointsRecordRepository;
_orderRepository = orderRepository;
_registrationRepository = registrationRepository;
_registrationItemRepository = registrationItemRepository;
_eventRepository = eventRepository;
_merchantCertificationRepository = merchantCertificationRepository;
_db = db;
_httpContextAccessor = httpContextAccessor;
_cachingProvider = cachingProvider;
_fileService = fileService;
}
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;
}
/// <summary>
/// 获取个人主页信息
/// </summary>
/// <returns></returns>
[HttpGet("profile")]
public async Task<MyProfileDto> 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
{
Role = user.MiniProgramRole switch
{
MiniProgramRole.Referee => "referee",
MiniProgramRole.Checker => "checker",
_ => "participant"
},
RoleType = user.MiniProgramRole is MiniProgramRole.Referee or MiniProgramRole.Checker
? (int)user.MiniProgramRole
: (int)MiniProgramRole.Participant,
RoleName = user.MiniProgramRole switch
{
MiniProgramRole.Referee => "裁判",
MiniProgramRole.Checker => "检录",
_ => user.PersonType == PersonType.Coach ? "教练" : "运动员"
},
PersonType = (int)user.PersonType,
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
};
}
/// <summary>
/// 获取实名认证信息
/// </summary>
/// <returns></returns>
[HttpGet("auth")]
public async Task<MyAuthInfoDto> GetAuthInfo()
{
var userId = GetCurrentUserId();
var user = await _appUserRepository.GetFirstAsync(u => u.Id == userId);
if (user == null) throw Oops.Bah("用户信息不存在");
int authType = user.AuthType;
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,
PassportNumber = user.PassportNumber,
PassportPhotoUrl = user.PassportPhotoUrl,
HouseholdPhotoUrl = user.HouseholdPhotoUrl,
AuditStatus = user.AuditStatus,
AuditRemark = user.AuditRemark
};
}
/// <summary>
/// 提交实名认证
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[HttpPost("auth")]
public async Task<bool> 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("身份证背面图片不能为空");
// 身份证号查重(排除被单位删除的影子账号:无单位关联且无密码)
var duplicateIdCard = await _appUserRepository.AsQueryable()
.Where(u => u.IdCardNumber == input.IdCardNumber && u.Id != userId && !u.DeleteMark && u.PersonType == PersonType.Athlete
&& !(u.MerchantCertificationId == null && u.Password == null))
.FirstAsync();
if (duplicateIdCard != null) throw Oops.Bah("该身份证号已被其他用户使用");
}
else if (input.AuthType == 2)
{
if (string.IsNullOrEmpty(input.PassportPhotoUrl)) throw Oops.Bah("护照图片不能为空");
if (string.IsNullOrEmpty(input.PassportNumber)) throw Oops.Bah("护照号码不能为空");
if (!input.Gender.HasValue) throw Oops.Bah("性别不能为空");
if (!input.BirthDate.HasValue) throw Oops.Bah("出生日期不能为空");
// 护照号查重(排除被单位删除的影子账号:无单位关联且无密码)
var duplicatePassport = await _appUserRepository.AsQueryable()
.Where(u => u.PassportNumber == input.PassportNumber && u.Id != userId && !u.DeleteMark && u.PersonType == PersonType.Athlete
&& !(u.MerchantCertificationId == null && u.Password == null))
.FirstAsync();
if (duplicatePassport != null) throw Oops.Bah("该护照号码已被其他用户使用");
}
else if (input.AuthType == 3)
{
if (string.IsNullOrEmpty(input.HouseholdPhotoUrl)) throw Oops.Bah("户口本图片不能为空");
if (string.IsNullOrEmpty(input.IdCardNumber)) throw Oops.Bah("身份证号不能为空");
// 身份证号查重(排除被单位删除的影子账号:无单位关联且无密码)
var duplicateIdCard = await _appUserRepository.AsQueryable()
.Where(u => u.IdCardNumber == input.IdCardNumber && u.Id != userId && !u.DeleteMark && u.PersonType == PersonType.Athlete
&& !(u.MerchantCertificationId == null && u.Password == null))
.FirstAsync();
if (duplicateIdCard != null) 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<string>(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;
user.AuthType = input.AuthType;
if (input.AuthType == 1)
{
user.IdCardNumber = input.IdCardNumber;
user.IdCardImageUrl = input.IdCardImageUrl;
user.IdCardBackImageUrl = input.IdCardBackImageUrl;
user.PassportPhotoUrl = null;
user.PassportNumber = null;
user.HouseholdPhotoUrl = null;
user.Gender = (Gender)input.Gender.Value;
user.BirthDate = input.BirthDate.Value;
}
else if (input.AuthType == 2)
{
user.PassportNumber = input.PassportNumber;
user.PassportPhotoUrl = input.PassportPhotoUrl;
user.IdCardImageUrl = null;
user.IdCardBackImageUrl = null;
user.IdCardNumber = null;
user.HouseholdPhotoUrl = null;
user.Gender = (Gender)input.Gender.Value;
user.BirthDate = input.BirthDate.Value;
}
else if (input.AuthType == 3)
{
// AuthType == 3:户口本认证
user.IdCardNumber = input.IdCardNumber;
user.HouseholdPhotoUrl = input.HouseholdPhotoUrl;
user.IdCardImageUrl = null;
user.IdCardBackImageUrl = null;
user.PassportPhotoUrl = null;
user.PassportNumber = null;
if (input.Gender.HasValue)
{
user.Gender = (Gender)input.Gender.Value;
}
if (input.BirthDate.HasValue)
{
user.BirthDate = input.BirthDate.Value;
}
}
else
{
// AuthType == 0 或其他值:未认证,不修改任何认证相关字段
user.AuthType = 0;
user.IdCardNumber = null;
user.IdCardImageUrl = null;
user.IdCardBackImageUrl = null;
user.PassportPhotoUrl = null;
user.PassportNumber = null;
user.HouseholdPhotoUrl = null;
}
user.AuditStatus = AuditStatus.PendingAudit;
user.AuditRemark = null;
user.AuditUserId = null;
user.AuditTime = null;
user.UpdatedTime = DateTime.Now;
return await _appUserRepository.UpdateAsync(user);
}
/// <summary>
/// 我的团队列表
/// </summary>
/// <returns></returns>
[HttpGet("teams")]
public async Task<PageResult<MyTeamDto>> 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<MyTeamDto>
{
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;
}
/// <summary>
/// 团队详情
/// </summary>
/// <param name="id">团队ID</param>
/// <returns></returns>
[HttpGet("teams/{id}")]
public async Task<MyTeamDetailDto> 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
};
}
/// <summary>
/// 我的证书总览
/// </summary>
/// <returns></returns>
[HttpGet("certificates/overview")]
public async Task<MyCertificateOverviewDto> GetCertificatesOverview()
{
var userId = GetCurrentUserId();
var count = await _certificateRepository.CountAsync(c => c.UserId == userId && c.CertificateStatus == 2 && !c.DeleteMark);
return new MyCertificateOverviewDto
{
TotalCount = count
};
}
/// <summary>
/// 我的证书列表
/// </summary>
/// <returns></returns>
[HttpGet("certificates")]
public async Task<PageResult<MyCertificateDto>> GetCertificates([FromQuery] MyCertificatePageInput input)
{
var userId = GetCurrentUserId();
// 先查询原始数据,然后在内存中转换证书名称
var query = _certificateRepository.AsQueryable()
.LeftJoin<EventInfo>((c, e) => c.EventId == e.Id)
.LeftJoin<EventProject>((c, e, ep) => c.ProjectId == ep.Id)
.LeftJoin<ProjectEntity>((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<MyCertificateDto>
{
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 => "参与证书",
_ => "荣誉证书"
};
}
/// <summary>
/// 我的积分总览
/// </summary>
/// <returns></returns>
[HttpGet("points/overview")]
public async Task<MyPointsOverviewDto> 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
};
}
/// <summary>
/// 我的积分记录
/// </summary>
/// <returns></returns>
[HttpGet("points")]
public async Task<PageResult<MyPointsRecordDto>> GetPoints([FromQuery] MyPointsPageInput input)
{
var userId = GetCurrentUserId();
var query = _pointsRecordRepository.AsQueryable()
.LeftJoin<EventInfo>((p, e) => p.EventId == e.Id)
.LeftJoin<EventProject>((p, e, ep) => p.ProjectId == ep.Id)
.LeftJoin<ProjectEntity>((p, e, ep, pr) => ep.ProjectId == pr.Id)
.LeftJoin<EventProjectGroup>((p, e, ep, pr, pg) => p.ProjectGroupId == pg.Id)
.LeftJoin<EventResult>((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);
}
/// <summary>
/// 我的订单列表
/// </summary>
/// <returns></returns>
[HttpGet("orders")]
public async Task<PageResult<MyOrderDto>> GetOrders([FromQuery] MyPageInput input)
{
var userId = GetCurrentUserId();
// 首先获取用户自己创建的订单ID列表
var userOrderIds = await _orderRepository.AsQueryable()
.Where(o => o.CreatedUserId == userId && !o.DeleteMark)
.Select(o => o.Id)
.ToListAsync();
// 查询订单:包括用户自己创建的订单,以及关联到用户订单的退款订单
var orders = await _orderRepository.AsQueryable()
.LeftJoin<EventInfo>((o, e) => o.EventId == e.Id)
.Where((o, e) =>
!o.DeleteMark &&
o.OrderStatus != OrderStatus.Cancelled &&
(o.CreatedUserId == userId ||
(o.OrderType == 2 && o.RelatedOrderId.HasValue && userOrderIds.Contains(o.RelatedOrderId.Value)))
)
.Select((o, e) => new
{
o.Id,
o.OrderNo,
EventName = e.EventName,
o.ActualAmount,
o.OrderType,
o.RelatedOrderId,
o.OrderStatus,
o.PaymentTime,
o.CreatedTime
})
.OrderByDescending(o => o.CreatedTime)
.ToPagedListAsync(input);
// 处理退款订单的金额显示
var resultList = new List<MyOrderDto>();
foreach (var order in orders.Rows)
{
var dto = new MyOrderDto
{
Id = order.Id,
OrderNo = order.OrderNo,
EventName = order.EventName,
ActualAmount = order.ActualAmount,
RefundAmount = order.OrderType == 2 ? order.ActualAmount : null,
OrderStatus = order.OrderStatus,
PaymentTime = order.PaymentTime,
CreatedTime = order.CreatedTime
};
// 如果是退款订单,查找关联的原始订单,显示原始支付金额
if (order.OrderType == 2 && order.RelatedOrderId > 0)
{
var originalOrder = await _orderRepository.GetByIdAsync(order.RelatedOrderId);
if (originalOrder != null)
{
dto.ActualAmount = originalOrder.ActualAmount;
}
}
resultList.Add(dto);
}
return new PageResult<MyOrderDto>
{
PageNo = orders.PageNo,
PageSize = orders.PageSize,
Total = orders.Total,
Rows = resultList
};
}
/// <summary>
/// 订单详情
/// </summary>
/// <param name="id">订单ID</param>
/// <returns></returns>
[HttpGet("orders/{id}")]
public async Task<MyOrderDetailDto> GetOrderDetail(long id)
{
var userId = GetCurrentUserId();
// 1. 先查询订单
var order = await _orderRepository.GetFirstAsync(o => o.Id == id && !o.DeleteMark);
if (order == null) throw Oops.Bah("订单不存在");
// 2. 查询关联的报名信息,包括软删除的
EventRegistration? registration = null;
if (order.RegistrationId > 0)
{
registration = await _db.Queryable<EventRegistration>()
.ClearFilter()
.Where(r => r.Id == order.RegistrationId)
.FirstAsync();
}
// 3. 获取赛事信息和名称
var eventQuery = await _eventRepository.AsQueryable()
.Where(e => e.Id == order.EventId)
.Select(e => new { e.EventName, e.ChargeMode })
.FirstAsync();
var eventName = eventQuery.EventName;
var chargeMode = eventQuery.ChargeMode;
// 4. 获取项目列表
List<MyOrderItemDto> items = new List<MyOrderItemDto>();
if (order.RegistrationId > 0)
{
if (order.OrderType == 2)
{
// 退款订单:通过 RefundOrderId 字段精准找到对应的软删除记录
var refundOrderId = order.Id;
// 先找被软删除的人员中 RefundOrderId 等于当前退款订单ID的
var deletedPersons = await _db.Queryable<EventRegistrationPerson>()
.ClearFilter()
.Where(p => p.RegistrationId == order.RegistrationId &&
p.DeleteMark == true &&
p.RefundOrderId == refundOrderId)
.Select(p => p.RegistrationItemId)
.ToListAsync();
if (deletedPersons.Any())
{
// 找出这些人员关联的项目(去重)
var distinctItemIds = deletedPersons.Distinct().ToList();
items = await _db.Queryable<EventRegistrationItem>()
.ClearFilter()
.Where(i => distinctItemIds.Contains(i.Id))
.Select(i => new MyOrderItemDto
{
ProjectName = i.ProjectName,
GroupName = i.GroupName,
Fee = i.TotalPrice
})
.ToListAsync();
}
else
{
// 如果没有找到匹配的人员,再试试找项目的 RefundOrderId 等于当前退款订单ID的
items = await _db.Queryable<EventRegistrationItem>()
.ClearFilter()
.Where(i => i.RegistrationId == order.RegistrationId &&
i.DeleteMark == true &&
i.RefundOrderId == refundOrderId)
.Select(i => new MyOrderItemDto
{
ProjectName = i.ProjectName,
GroupName = i.GroupName,
Fee = i.TotalPrice
})
.ToListAsync();
}
}
else
{
// 其他订单:显示全部的项目
items = await _db.Queryable<EventRegistrationItem>()
.ClearFilter()
.Where(i => i.RegistrationId == order.RegistrationId)
.Select(i => new MyOrderItemDto
{
ProjectName = i.ProjectName,
GroupName = i.GroupName,
Fee = i.TotalPrice
})
.ToListAsync();
}
}
// 如果是退款订单,查找关联的原始订单,显示原始支付金额
decimal displayAmount = order.ActualAmount;
if (order.OrderType == 2 && order.RelatedOrderId > 0)
{
var originalOrder = await _orderRepository.GetByIdAsync(order.RelatedOrderId);
if (originalOrder != null)
{
displayAmount = originalOrder.ActualAmount;
}
}
return new MyOrderDetailDto
{
Id = order.Id,
OrderNo = order.OrderNo,
EventName = eventName,
ActualAmount = displayAmount,
RefundAmount = order.OrderType == 2 ? order.ActualAmount : null,
RefundTime = order.OrderType == 2 ? order.PaymentTime : null,
OrderStatus = order.OrderStatus,
PaymentTime = order.PaymentTime,
PaymentMethod = registration?.PaymentMethod ?? 0,
ChargeMode = chargeMode,
Items = items
};
}
/// <summary>
/// 我的参赛记录
/// </summary>
/// <returns></returns>
[HttpGet("registrations")]
[DisplayName("我的参赛记录")]
public async Task<PageResult<MyRegistrationDto>> GetRegistrations([FromQuery] MyPageInput input)
{
var userId = GetCurrentUserId();
// 1. 先分页查询报名记录
var regQuery = _registrationRepository.AsQueryable()
.Where(r => r.CreatedUserId == userId && !r.DeleteMark &&
((r.PaymentMethod == 2 && r.PaymentStatus == 1) || (r.PaymentMethod == 1 && (r.PaymentStatus == 0 || r.PaymentStatus == 1))))
.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<MyRegistrationDto>
{
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);
}
/// <summary>
/// 更新头像 - 上传图片文件
/// </summary>
/// <param name="file">头像图片文件</param>
/// <returns>更新后的头像信息</returns>
[HttpPost("avatar/upload")]
[DisplayName("上传头像")]
public async Task<UpdateAvatarOutput> UpdateAvatarByFile(IFormFile file)
{
if (file == null || file.Length == 0)
{
throw Oops.Bah("请上传头像图片");
}
// 验证文件类型
var allowedExtensions = new[] { ".jpg", ".jpeg", ".png", ".gif", ".bmp" };
var extension = Path.GetExtension(file.FileName).ToLower();
if (!allowedExtensions.Contains(extension))
{
throw Oops.Bah("仅支持 JPG、JPEG、PNG、GIF、BMP 格式的图片");
}
// 验证文件大小 (最大 5MB)
if (file.Length > 5 * 1024 * 1024)
{
throw Oops.Bah("图片大小不能超过 5MB");
}
// 处理图片
using var processedStream = await ProcessAvatarImage(file.OpenReadStream());
// 上传处理后的图片
var now = DateTime.Now;
var fileName = $"avatar/{now.Year}/{now.Month:D2}/{now.Day:D2}/{Yitter.IdGenerator.YitIdHelper.NextId()}.jpg";
var avatarUrl = await _fileService.UploadFileStreamAsync(fileName, processedStream);
// 更新用户头像
var userId = GetCurrentUserId();
var user = await _appUserRepository.GetFirstAsync(u => u.Id == userId);
if (user == null)
{
throw Oops.Bah("用户信息不存在");
}
user.Avatar = avatarUrl;
user.UpdatedTime = DateTime.Now;
await _appUserRepository.UpdateAsync(user);
return new UpdateAvatarOutput
{
AvatarUrl = avatarUrl
};
}
/// <summary>
/// 更新头像 - 使用图片URL
/// </summary>
/// <param name="input">头像URL输入</param>
/// <returns>更新后的头像信息</returns>
[HttpPost("avatar/url")]
[DisplayName("更新头像URL")]
public async Task<UpdateAvatarOutput> UpdateAvatarByUrl([FromBody] UpdateAvatarUrlInput input)
{
// 验证URL格式
if (!Uri.TryCreate(input.AvatarUrl, UriKind.Absolute, out _))
{
throw Oops.Bah("头像URL格式不正确");
}
// 更新用户头像
var userId = GetCurrentUserId();
var user = await _appUserRepository.GetFirstAsync(u => u.Id == userId);
if (user == null)
{
throw Oops.Bah("用户信息不存在");
}
user.Avatar = input.AvatarUrl;
user.UpdatedTime = DateTime.Now;
await _appUserRepository.UpdateAsync(user);
return new UpdateAvatarOutput
{
AvatarUrl = input.AvatarUrl
};
}
/// <summary>
/// 处理头像图片 - 压缩和格式转换
/// </summary>
/// <param name="inputStream">原始图片流</param>
/// <returns>处理后的图片流</returns>
private async Task<Stream> ProcessAvatarImage(Stream inputStream)
{
// 将流复制到内存流,以便可以多次读取
var memoryStream = new MemoryStream();
await inputStream.CopyToAsync(memoryStream);
memoryStream.Position = 0;
// 使用 SkiaSharp 处理图片
using var originalBitmap = SKBitmap.Decode(memoryStream);
if (originalBitmap == null)
{
throw Oops.Bah("图片解码失败,请检查图片格式");
}
// 计算缩放后的尺寸(最大 200x200)
const int maxSize = 200;
int width = originalBitmap.Width;
int height = originalBitmap.Height;
if (width > maxSize || height > maxSize)
{
if (width > height)
{
height = (int)(height * (double)maxSize / width);
width = maxSize;
}
else
{
width = (int)(width * (double)maxSize / height);
height = maxSize;
}
}
// 创建缩放后的图片
using var scaledBitmap = originalBitmap.Resize(new SKImageInfo(width, height), SKFilterQuality.High);
// 裁剪为正方形(从中心裁剪)
int cropSize = Math.Min(width, height);
int cropX = (width - cropSize) / 2;
int cropY = (height - cropSize) / 2;
using var croppedBitmap = new SKBitmap(cropSize, cropSize);
using var canvas = new SKCanvas(croppedBitmap);
canvas.Clear(SKColors.Transparent);
canvas.DrawBitmap(scaledBitmap, new SKRect(cropX, cropY, cropX + cropSize, cropY + cropSize), new SKRect(0, 0, cropSize, cropSize));
// 转换为 JPG 格式并压缩
var outputStream = new MemoryStream();
using var image = SKImage.FromBitmap(croppedBitmap);
using var data = image.Encode(SKEncodedImageFormat.Jpeg, 80); // 80% 质量
data.SaveTo(outputStream);
outputStream.Position = 0;
return outputStream;
}
}