YD_Event.Admin/YD_Event.Application/Event/UserEventRecordService.cs

411 lines
17 KiB
C#
Raw Normal View History

2026-03-20 03:15:31 +08:00
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
2026-03-14 05:19:40 +08:00
using YD_Event.Application.Event.Dtos;
using YD_Event.Core.Entities;
using YD_Event.Core.Enum;
using SqlSugar;
using Mapster;
using YD_Event.Application.Auth;
using Furion.DynamicApiController;
using Microsoft.AspNetCore.Mvc;
using YD_Event.Application.File;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Authorization;
using System.ComponentModel;
2026-03-20 03:15:31 +08:00
using Furion.FriendlyException;
using Furion.DatabaseAccessor;
using Furion.DependencyInjection;
2026-03-14 05:19:40 +08:00
namespace YD_Event.Application.Event;
/// <summary>
2026-03-20 03:15:31 +08:00
/// 用户端参赛记录管理
2026-03-14 05:19:40 +08:00
/// </summary>
2026-03-20 03:15:31 +08:00
public class UserEventRecordService : IDynamicApiController, ITransient
2026-03-14 05:19:40 +08:00
{
private readonly ISqlSugarRepository<EventRegistration> _registrationRepository;
private readonly ISqlSugarRepository<EventRegistrationPerson> _registrationPersonRepository;
private readonly ISqlSugarRepository<YD_Event.Core.Entities.AppUser> _userRepository;
private readonly ISqlSugarRepository<EventCertificate> _certificateRepository;
private readonly AuthManager _authManager;
private readonly IIdGenerator _idGenerator;
private readonly FileService _fileService;
2026-03-20 03:15:31 +08:00
public UserEventRecordService(
2026-03-14 05:19:40 +08:00
ISqlSugarRepository<EventRegistration> registrationRepository,
ISqlSugarRepository<EventRegistrationPerson> registrationPersonRepository,
ISqlSugarRepository<YD_Event.Core.Entities.AppUser> userRepository,
ISqlSugarRepository<EventCertificate> certificateRepository,
AuthManager authManager,
IIdGenerator idGenerator,
FileService fileService)
{
_registrationRepository = registrationRepository;
_registrationPersonRepository = registrationPersonRepository;
_userRepository = userRepository;
_certificateRepository = certificateRepository;
_authManager = authManager;
_idGenerator = idGenerator;
_fileService = fileService;
}
/// <summary>
/// 1. 列表查询
/// </summary>
2026-03-17 15:32:01 +08:00
[DisplayName("参赛记录列表查询")]
2026-03-20 03:15:31 +08:00
[HttpGet("list")]
2026-03-14 05:19:40 +08:00
public async Task<PageResult<RegistrationItemDTO>> GetRegistrationList([FromQuery] RegistrationListQueryInput input)
{
var userId = _authManager.UserId;
var query = _registrationRepository.AsQueryable()
.InnerJoin<EventInfo>((r, e) => r.EventId == e.Id)
.Where((r, e) => r.CreatedUserId == userId && !r.DeleteMark)
.WhereIF(input.EventId.HasValue, (r, e) => r.EventId == input.EventId)
.WhereIF(!string.IsNullOrWhiteSpace(input.Keyword), (r, e) => r.TeamName.Contains(input.Keyword) || e.EventName.Contains(input.Keyword))
.OrderByDescending((r, e) => r.CreatedTime)
.Select((r, e) => new RegistrationItemDTO
{
RegistrationId = r.Id,
EventName = e.EventName,
TeamName = r.TeamName,
CreateTime = r.CreatedTime,
UpdateTime = r.UpdatedTime,
PayStatus = r.PaymentStatus == 1 ? "已支付" : "待付款", // 简化映射
CertStatus = "未颁发", // 暂无证书逻辑
HasMaterial = SqlFunc.HasValue(r.MaterialUrl),
HasVoucher = SqlFunc.HasValue(r.PaymentProofUrl),
MemberCount = r.TotalPersonCount,
MaxMember = 10, // 假设
ExpireTime = e.MaterialUploadEndTime
});
// 这里需要先把数据查出来再做复杂的状态转换,因为 SqlSugar Select 中不能写太复杂的 switch
var pagedResult = await query.ToPagedListAsync(input);
// 二次处理状态描述
foreach (var item in pagedResult.Rows)
{
// 状态映射逻辑
// status ∈ (待付款, 待上传素材, 已取消, 已报名, 待审核)
// 假设:RegStatus=4 -> 已取消
// RegStatus=1 -> 待审核
// PayStatus=0 -> 待付款
// PayStatus=1 && !HasMaterial -> 待上传素材
// PayStatus=1 && HasMaterial -> 已报名
// 注意:这里需要再次查询原始状态值才能准确判断,上面的Select只映射了部分。
// 优化:Select时把原始状态值带出来
// 重新查询一遍原始数据比较准确
}
// 重新构建查询以支持复杂逻辑
var list = await _registrationRepository.AsQueryable()
.InnerJoin<EventInfo>((r, e) => r.EventId == e.Id)
.Where((r, e) => r.CreatedUserId == userId && !r.DeleteMark)
.WhereIF(input.EventId.HasValue, (r, e) => r.EventId == input.EventId)
.WhereIF(!string.IsNullOrWhiteSpace(input.Keyword), (r, e) => r.TeamName.Contains(input.Keyword) || e.EventName.Contains(input.Keyword))
.OrderByDescending((r, e) => r.CreatedTime)
.Select((r, e) => new
{
r.Id, r.EventId, e.EventName, r.TeamName, r.RegistrationStatus, r.PaymentStatus, r.MaterialUrl, r.PaymentProofUrl,
r.CreatedTime, r.UpdatedTime, r.TotalPersonCount, e.MaterialUploadEndTime
})
.ToPagedListAsync(input);
var resultItems = list.Rows.Select(x => {
string statusDesc = "未知";
if (x.RegistrationStatus == 4) statusDesc = "已取消";
else if (x.RegistrationStatus == 1) statusDesc = "待审核"; // 假设1是待审核
else if (x.PaymentStatus == 0) statusDesc = "待付款";
else if (x.PaymentStatus == 1 && string.IsNullOrEmpty(x.MaterialUrl)) statusDesc = "待上传素材";
else if (x.PaymentStatus == 1 && !string.IsNullOrEmpty(x.MaterialUrl)) statusDesc = "已报名"; // 或已完成
return new RegistrationItemDTO
{
RegistrationId = x.Id,
EventName = x.EventName,
TeamName = x.TeamName,
Status = statusDesc,
CreateTime = x.CreatedTime,
UpdateTime = x.UpdatedTime,
PayStatus = x.PaymentStatus == 1 ? "已支付" : (x.PaymentStatus == 4 ? "待审核" : "待付款"),
CertStatus = "未颁发",
HasMaterial = !string.IsNullOrEmpty(x.MaterialUrl),
HasVoucher = !string.IsNullOrEmpty(x.PaymentProofUrl),
MemberCount = x.TotalPersonCount,
MaxMember = 10,
ExpireTime = x.MaterialUploadEndTime
};
}).ToList();
return new PageResult<RegistrationItemDTO>
{
PageNo = list.PageNo,
PageSize = list.PageSize,
Total = list.Total,
Pages = list.Pages,
Rows = resultItems
};
}
/// <summary>
/// 2. 取消报名
/// </summary>
2026-03-17 15:32:01 +08:00
[DisplayName("取消报名")]
2026-03-20 03:15:31 +08:00
[HttpPost("cancel/{registrationId}")]
2026-03-14 05:19:40 +08:00
[UnitOfWork]
public async Task<ResponseDTO<bool>> CancelRegistration([FromRoute] long registrationId)
{
var userId = _authManager.UserId;
var registration = await _registrationRepository.GetFirstAsync(x => x.Id == registrationId && x.CreatedUserId == userId);
if (registration == null) throw Oops.Bah("报名记录不存在");
// 仅允许 status ∈ (待付款, 待上传素材) 可取消
// 待付款: PayStatus == 0
// 待上传素材: PayStatus == 1 && !HasMaterial
// 已取消: RegStatus == 4
if (registration.RegistrationStatus == 4) throw Oops.Bah("订单已取消");
bool canCancel = false;
if (registration.PaymentStatus == 0) canCancel = true;
else if (registration.PaymentStatus == 1 && string.IsNullOrEmpty(registration.MaterialUrl)) canCancel = true;
if (!canCancel)
{
// 已付款且已上传素材的报名需走退赛流程
throw Oops.Bah("当前状态无法直接取消,请联系管理员退赛");
}
registration.RegistrationStatus = 4; // 已取消
registration.UpdatedUserId = userId;
registration.UpdatedTime = DateTime.Now;
// registration.CancelReason = "用户主动取消"; // 需扩展字段
await _registrationRepository.UpdateAsync(registration);
// 记录日志 (暂不需要)
/*
await _eventLogRepository.InsertAsync(new EventLog
{
RegistrationId = registration.Id,
LogType = EventLogType.Cancel,
Description = "用户取消报名",
OperatorId = userId,
CreatedTime = DateTime.Now
});
*/
return new ResponseDTO<bool> { Code = 200, Success = true, Data = true, Message = "取消成功" };
}
/// <summary>
/// 3. 上传素材
/// </summary>
2026-03-17 15:32:01 +08:00
[DisplayName("上传素材")]
2026-03-20 03:15:31 +08:00
[HttpPost("material/{registrationId}")]
2026-03-14 05:19:40 +08:00
[UnitOfWork]
public async Task<ResponseDTO<List<MaterialDTO>>> UploadMaterial([FromRoute] long registrationId, [FromForm] UploadMaterialInput input)
{
var userId = _authManager.UserId;
var registration = await _registrationRepository.GetFirstAsync(x => x.Id == registrationId && x.CreatedUserId == userId);
if (registration == null) throw Oops.Bah("报名记录不存在");
// 仅 status = 待上传素材 或 已上传素材 允许再次上传
// 即 PayStatus == 1 (已支付)
if (registration.PaymentStatus != 1) throw Oops.Bah("未支付或状态不正确,无法上传素材");
// 总素材大小累计不超过 100 MB (简化校验,假设单次)
long totalSize = input.Files.Sum(f => f.Length);
if (totalSize > 100 * 1024 * 1024) throw Oops.Bah("素材总大小超过100MB");
var result = new List<MaterialDTO>();
foreach (var file in input.Files)
{
var uploadResult = await _fileService.Upload(file); // 假设 FileService 支持 IFormFile
var item = uploadResult.FirstOrDefault(); // 假设返回 List
if (item != null)
{
result.Add(new MaterialDTO
{
MaterialId = _idGenerator.NewLong().ToString(),
OssUrl = item.Url,
FileName = item.Name,
FileSize = file.Length,
UploadTime = DateTime.Now
});
}
}
if (result.Any())
{
registration.MaterialUrl = result.First().OssUrl; // 更新主表
await _registrationRepository.UpdateAsync(registration);
// 记录日志 (暂不需要)
/*
await _eventLogRepository.InsertAsync(new EventLog
{
RegistrationId = registration.Id,
LogType = EventLogType.UploadMaterial,
Description = $"用户上传素材: {result.Count}个文件",
OperatorId = userId,
CreatedTime = DateTime.Now
});
*/
}
return new ResponseDTO<List<MaterialDTO>> { Code = 200, Success = true, Data = result, Message = "上传成功" };
}
/// <summary>
/// 4. 上传付款凭证
/// </summary>
2026-03-17 15:32:01 +08:00
[DisplayName("上传付款凭证")]
2026-03-20 03:15:31 +08:00
[HttpPost("voucher/{registrationId}")]
2026-03-14 05:19:40 +08:00
[UnitOfWork]
public async Task<ResponseDTO<VoucherDTO>> UploadVoucher([FromRoute] long registrationId, [FromForm] UploadVoucherInput input)
{
var userId = _authManager.UserId;
var registration = await _registrationRepository.GetFirstAsync(x => x.Id == registrationId && x.CreatedUserId == userId);
if (registration == null) throw Oops.Bah("报名记录不存在");
// 仅 status = 待付款 允许上传
if (registration.PaymentStatus != 0) throw Oops.Bah("当前状态不允许上传凭证");
var uploadResult = await _fileService.Upload(input.File);
var fileInfo = uploadResult.First();
registration.PaymentProofUrl = fileInfo.Url;
registration.PaymentStatus = 4; // 假设 4 = 待审核
registration.UpdatedTime = DateTime.Now;
// registration.VoucherUploadTime = DateTime.Now;
await _registrationRepository.UpdateAsync(registration);
// 记录日志 (暂不需要)
/*
await _eventLogRepository.InsertAsync(new EventLog
{
RegistrationId = registration.Id,
LogType = EventLogType.UploadVoucher,
Description = "用户上传付款凭证",
OperatorId = userId,
CreatedTime = DateTime.Now
});
*/
return new ResponseDTO<VoucherDTO>
{
Code = 200, Success = true, Message = "上传成功",
Data = new VoucherDTO
{
VoucherId = _idGenerator.NewLong().ToString(),
OssUrl = fileInfo.Url,
AuditStatus = "待审核"
}
};
}
/// <summary>
/// 5.1 查看队员列表
/// </summary>
2026-03-17 15:32:01 +08:00
[DisplayName("查看队员列表")]
2026-03-20 03:15:31 +08:00
[HttpGet("members/{registrationId}")]
2026-03-14 05:19:40 +08:00
public async Task<List<TeamMemberDTO>> GetTeamMembers([FromRoute] long registrationId)
{
var userId = _authManager.UserId;
var registration = await _registrationRepository.GetFirstAsync(x => x.Id == registrationId && x.CreatedUserId == userId);
if (registration == null) throw Oops.Bah("报名记录不存在");
var persons = await _registrationPersonRepository.GetListAsync(x => x.RegistrationId == registrationId);
// 需要关联 AppUser 获取更多信息 (Email 等)
// 这里简化处理
return persons.Select(p => new TeamMemberDTO
{
UserId = 0, // 需关联 AppUser
RealName = p.Name,
Phone = p.Phone,
Email = "", // 暂无
2026-03-20 02:00:34 +08:00
Role = p.PersonType == PersonType.Leader ? "队长" : "队员", // 假设
2026-03-14 05:19:40 +08:00
InviteStatus = "已加入",
JoinTime = p.CreatedTime
}).ToList();
}
/// <summary>
/// 5.2 更换队员
/// </summary>
2026-03-17 15:32:01 +08:00
[DisplayName("更换队员")]
2026-03-20 03:15:31 +08:00
[HttpPost("members/replace/{registrationId}/{userId}")]
2026-03-14 05:19:40 +08:00
[UnitOfWork]
public async Task<ResponseDTO<bool>> ReplaceMember([FromRoute] long registrationId, [FromRoute] long userId, [FromBody] ReplaceMemberInput input)
{
var currentUserId = _authManager.UserId;
// 校验队长权限: 仅创建者(队长)可操作
var registration = await _registrationRepository.GetFirstAsync(x => x.Id == registrationId && x.CreatedUserId == currentUserId);
if (registration == null) throw Oops.Bah("无权操作或记录不存在");
var person = await _registrationPersonRepository.GetFirstAsync(x => x.RegistrationId == registrationId && x.Id == userId);
if (person == null) throw Oops.Bah("队员不存在");
2026-03-20 02:00:34 +08:00
// 被替换人 role ≠ 队长 (假设 PersonType=Leader 是队长)
if (person.PersonType == PersonType.Leader) throw Oops.Bah("不能替换队长");
2026-03-14 05:19:40 +08:00
// 检查新队员
var newUser = await _userRepository.GetByIdAsync(input.NewUserId);
if (newUser == null) throw Oops.Bah("新队员不存在");
// 检查新队员是否已报名该赛事 (简化检查: 是否在 Person 表中存在该 EventId 的记录)
// var exists = await _registrationPersonRepository.AnyAsync(x => x.EventId == registration.EventId && x.IdCard == newUser.IdCardNumber);
// if (exists) throw Oops.Bah("新队员已报名该赛事");
long oldUserId = person.Id; // 这里应该是 AppUserId,但 Person 表没存 AppUserId,只存了 Name/Phone
// 更新信息
person.Name = newUser.Name;
person.IdCard = newUser.IdCardNumber;
person.Phone = newUser.Mobile;
person.UpdatedTime = DateTime.Now;
await _registrationPersonRepository.UpdateAsync(person);
// 记录日志 (暂不需要)
/*
await _eventLogRepository.InsertAsync(new EventLog
{
RegistrationId = registration.Id,
LogType = EventLogType.ReplaceMember,
Description = $"更换队员: {oldUserId} -> {input.NewUserId}",
OperatorId = currentUserId,
CreatedTime = DateTime.Now
});
*/
return new ResponseDTO<bool> { Code = 200, Success = true, Data = true, Message = "更换成功" };
}
/// <summary>
/// 6. 查看证书
/// </summary>
2026-03-17 15:32:01 +08:00
[DisplayName("查看证书")]
2026-03-20 03:15:31 +08:00
[HttpGet("certificate/{registrationId}")]
2026-03-14 05:19:40 +08:00
public async Task<ResponseDTO<CertificateDTO>> GetCertificate([FromRoute] long registrationId)
{
// 2-已发放
var cert = await _certificateRepository.GetFirstAsync(x => x.RegistrationId == registrationId && x.CertificateStatus == 2);
if (cert == null) return new ResponseDTO<CertificateDTO> { Code = 404, Message = "证书未颁发", Success = false };
return new ResponseDTO<CertificateDTO>
{
Code = 200, Success = true,
Data = new CertificateDTO
{
CertUrl = cert.CertificateUrl + "?sas_token=mock_10min_token", // 模拟带签名 URL
CertNumber = cert.CertificateNo,
IssueTime = cert.IssuedTime ?? DateTime.MinValue,
ExpireTime = null
}
};
}
}