478 lines
19 KiB
C#
478 lines
19 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using YD_Event.Application.Event.Dtos;
|
|
using YD_Event.Core.Entities;
|
|
using YD_Event.Core.Enum;
|
|
using Easy.Core;
|
|
using SqlSugar;
|
|
using Mapster;
|
|
using YD_Event.Application.Auth;
|
|
using Furion.DynamicApiController;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using YD_Event.Application.Common;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using System.ComponentModel;
|
|
using Furion.FriendlyException;
|
|
using Furion.DatabaseAccessor;
|
|
using Furion.DependencyInjection;
|
|
using YD_Event.Core.Infrastructure.WeChat;
|
|
|
|
namespace YD_Event.Application.Event;
|
|
|
|
/// <summary>
|
|
/// 用户端参赛记录管理
|
|
/// </summary>
|
|
public class UserEventRecordService : IDynamicApiController, ITransient
|
|
{
|
|
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 ISqlSugarRepository<WeChatPayConfig> _weChatPayConfigRepository;
|
|
private readonly ISqlSugarRepository<YD_Event.Core.Entities.Merchant> _merchantRepository;
|
|
private readonly ISqlSugarRepository<EventInfo> _eventInfoRepository;
|
|
private readonly ISqlSugarClient _db;
|
|
private readonly AuthManager _authManager;
|
|
private readonly IIdGenerator _idGenerator;
|
|
private readonly FileService _fileService;
|
|
private readonly WeChatPayService _weChatPayService;
|
|
|
|
public UserEventRecordService(
|
|
ISqlSugarRepository<EventRegistration> registrationRepository,
|
|
ISqlSugarRepository<EventRegistrationPerson> registrationPersonRepository,
|
|
ISqlSugarRepository<YD_Event.Core.Entities.AppUser> userRepository,
|
|
ISqlSugarRepository<EventCertificate> certificateRepository,
|
|
ISqlSugarRepository<WeChatPayConfig> weChatPayConfigRepository,
|
|
ISqlSugarRepository<YD_Event.Core.Entities.Merchant> merchantRepository,
|
|
ISqlSugarRepository<EventInfo> eventInfoRepository,
|
|
ISqlSugarClient db,
|
|
AuthManager authManager,
|
|
IIdGenerator idGenerator,
|
|
FileService fileService,
|
|
WeChatPayService weChatPayService)
|
|
{
|
|
_registrationRepository = registrationRepository;
|
|
_registrationPersonRepository = registrationPersonRepository;
|
|
_userRepository = userRepository;
|
|
_certificateRepository = certificateRepository;
|
|
_weChatPayConfigRepository = weChatPayConfigRepository;
|
|
_merchantRepository = merchantRepository;
|
|
_db = db;
|
|
_authManager = authManager;
|
|
_idGenerator = idGenerator;
|
|
_fileService = fileService;
|
|
_weChatPayService = weChatPayService;
|
|
_eventInfoRepository = eventInfoRepository;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 1. 列表查询
|
|
/// </summary>
|
|
[DisplayName("参赛记录列表查询")]
|
|
[HttpGet("list")]
|
|
public async Task<PageResult<RegistrationItemDTO>> GetRegistrationList([FromQuery] RegistrationListQueryInput input)
|
|
{
|
|
var userId = _authManager.UserId;
|
|
|
|
var list = await _registrationRepository.AsQueryable()
|
|
.InnerJoin<EventInfo>((r, e) => r.EventId == e.Id)
|
|
.LeftJoin<EventFeeConfig>((r, e, fc) => e.Id == fc.EventId)
|
|
.Where((r, e, fc) => r.CreatedUserId == userId && !r.DeleteMark)
|
|
.WhereIF(input.EventId.HasValue, (r, e, fc) => r.EventId == input.EventId)
|
|
.WhereIF(!string.IsNullOrWhiteSpace(input.Keyword), (r, e, fc) => r.TeamName.Contains(input.Keyword) || e.EventName.Contains(input.Keyword))
|
|
.OrderByDescending((r, e, fc) => r.CreatedTime)
|
|
.Select((r, e, fc) => new
|
|
{
|
|
r.Id, r.EventId, e.EventName, r.TeamName, r.PaymentStatus, r.MaterialUrl, r.PaymentProofUrl,
|
|
r.CreatedTime, r.UpdatedTime, r.TotalPersonCount, e.MaterialUploadEndTime,
|
|
e.CoverImage, e.EventType, e.EventStatus, e.RegistrationStartTime, e.RegistrationEndTime,
|
|
e.MatchStartTime, e.MatchEndTime,
|
|
fc.ChargeMode
|
|
})
|
|
.ToPagedListAsync(input);
|
|
|
|
var resultItems = list.Rows.Select(x => {
|
|
// 计算综合状态
|
|
int statusValue = 0;
|
|
if (x.PaymentStatus == 4)
|
|
statusValue = 2; // 已取消
|
|
else if (x.PaymentStatus == 0)
|
|
statusValue = 0; // 待付款
|
|
else if (x.PaymentStatus == 1 && string.IsNullOrEmpty(x.MaterialUrl))
|
|
statusValue = 1; // 已支付
|
|
else if (x.PaymentStatus == 1 && !string.IsNullOrEmpty(x.MaterialUrl))
|
|
statusValue = 4; // 已完成/已报名
|
|
else if (x.PaymentStatus == 2)
|
|
statusValue = 1; // 审核拒绝
|
|
else if (x.PaymentStatus == 3)
|
|
statusValue = 3; // 已退款
|
|
|
|
return new RegistrationItemDTO
|
|
{
|
|
RegistrationId = x.Id,
|
|
EventName = x.EventName,
|
|
CoverImage = x.CoverImage,
|
|
EventType = (int?)x.EventType,
|
|
ChargeMode = (int?)x.ChargeMode,
|
|
EventStatus = (int?)x.EventStatus,
|
|
RegistrationStartTime = x.RegistrationStartTime,
|
|
RegistrationEndTime = x.RegistrationEndTime,
|
|
MaterialDeadline = x.MaterialUploadEndTime,
|
|
MatchStartTime = x.MatchStartTime,
|
|
MatchEndTime = x.MatchEndTime,
|
|
TeamName = x.TeamName,
|
|
Status = statusValue,
|
|
CreateTime = x.CreatedTime,
|
|
UpdateTime = x.UpdatedTime,
|
|
PayStatus = x.PaymentStatus,
|
|
CertStatus = 0, // 未颁发
|
|
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>
|
|
[DisplayName("取消报名")]
|
|
[HttpPost("cancel/{registrationId}")]
|
|
[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("报名记录不存在");
|
|
|
|
if (registration.PaymentStatus == 4) throw Oops.Bah("订单已取消");
|
|
|
|
if (registration.PaymentStatus == 0) throw Oops.Bah("订单未支付");
|
|
|
|
registration.PaymentStatus = 4; // 已取消
|
|
registration.UpdatedUserId = userId;
|
|
registration.UpdatedTime = DateTime.Now;
|
|
await _registrationRepository.UpdateAsync(registration);
|
|
|
|
return new ResponseDTO<bool> { Code = 200, Success = true, Data = true, Message = "取消成功" };
|
|
}
|
|
|
|
/// <summary>
|
|
/// 3. 上传素材
|
|
/// </summary>
|
|
[DisplayName("上传素材")]
|
|
[HttpPost("material/{registrationId}")]
|
|
[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);
|
|
var item = uploadResult.FirstOrDefault();
|
|
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);
|
|
}
|
|
|
|
return new ResponseDTO<List<MaterialDTO>> { Code = 200, Success = true, Data = result, Message = "上传成功" };
|
|
}
|
|
|
|
/// <summary>
|
|
/// 4. 上传付款凭证
|
|
/// </summary>
|
|
[DisplayName("上传付款凭证")]
|
|
[HttpPost("voucher/{registrationId}")]
|
|
[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 = 0;
|
|
registration.UpdatedTime = DateTime.Now;
|
|
await _registrationRepository.UpdateAsync(registration);
|
|
|
|
return new ResponseDTO<VoucherDTO>
|
|
{
|
|
Code = 200, Success = true, Message = "上传成功",
|
|
Data = new VoucherDTO
|
|
{
|
|
VoucherId = _idGenerator.NewLong().ToString(),
|
|
OssUrl = fileInfo.Url,
|
|
AuditStatus = "待审核"
|
|
}
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// 5.1 查看队员列表
|
|
/// </summary>
|
|
[DisplayName("查看队员列表")]
|
|
[HttpGet("members/{registrationId}")]
|
|
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 = "", // 暂无
|
|
Role = p.PersonType == PersonType.Leader ? "队长" : "队员", // 假设
|
|
InviteStatus = "已加入",
|
|
JoinTime = p.CreatedTime
|
|
}).ToList();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 5.2 更换队员
|
|
/// </summary>
|
|
[DisplayName("更换队员")]
|
|
[HttpPost("members/replace/{registrationId}/{userId}")]
|
|
[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("队员不存在");
|
|
|
|
// 被替换人 role ≠ 队长
|
|
if (person.PersonType == PersonType.Leader) throw Oops.Bah("不能替换队长");
|
|
|
|
// 检查新队员
|
|
var newUser = await _userRepository.GetByIdAsync(input.NewUserId);
|
|
if (newUser == null) throw Oops.Bah("新队员不存在");
|
|
|
|
long oldUserId = person.Id;
|
|
|
|
// 更新信息
|
|
person.Name = newUser.Name;
|
|
person.IdCard = newUser.IdCardNumber;
|
|
person.Phone = newUser.Mobile;
|
|
person.UpdatedTime = DateTime.Now;
|
|
await _registrationPersonRepository.UpdateAsync(person);
|
|
|
|
return new ResponseDTO<bool> { Code = 200, Success = true, Data = true, Message = "更换成功" };
|
|
}
|
|
|
|
/// <summary>
|
|
/// 6. 查看证书
|
|
/// </summary>
|
|
[DisplayName("查看证书")]
|
|
[HttpGet("certificate/{registrationId}")]
|
|
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
|
|
}
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// 7. 取消报名预览
|
|
/// </summary>
|
|
[DisplayName("取消报名预览")]
|
|
[HttpGet("cancel/preview/{registrationId}")]
|
|
public async Task<CancelPreviewOutput> CancelPreview([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 eventInfo = await _db.Queryable<EventInfo>().InSingleAsync(registration.EventId);
|
|
|
|
// 获取项目列表
|
|
var items = await _db.Queryable<EventRegistrationItem>()
|
|
.Where(x => x.RegistrationId == registrationId)
|
|
.ToListAsync();
|
|
|
|
// 获取队员列表
|
|
var persons = await _registrationPersonRepository.GetListAsync(x => x.RegistrationId == registrationId);
|
|
|
|
// 计算可退款金额(简化逻辑,实际需根据退款规则计算)
|
|
var refundableAmount = registration.TotalAmount;
|
|
|
|
var output = new CancelPreviewOutput
|
|
{
|
|
RegistrationId = registration.Id,
|
|
EventName = eventInfo?.EventName ?? "",
|
|
TeamName = registration.TeamName ?? "",
|
|
ChargeMode = registration.TotalAmount > 0 ? 1 : 0,
|
|
ActualPaidAmount = registration.TotalAmount,
|
|
RefundableAmount = refundableAmount
|
|
};
|
|
|
|
// 添加项目
|
|
foreach (var item in items)
|
|
{
|
|
output.Projects.Add(new CancelPreviewProjectDto
|
|
{
|
|
ProjectId = item.ProjectId,
|
|
ProjectName = item.ProjectName ?? "",
|
|
ProjectType = item.PersonCount > 1 ? "团队" : "个人",
|
|
PersonCount = item.PersonCount,
|
|
UnitPrice = item.UnitPrice
|
|
});
|
|
}
|
|
|
|
// 添加队员
|
|
foreach (var person in persons)
|
|
{
|
|
output.Members.Add(new CancelPreviewMemberDto
|
|
{
|
|
MemberId = person.Id,
|
|
Name = person.Name ?? "",
|
|
IsLeader = person.PersonType == PersonType.Leader
|
|
});
|
|
}
|
|
|
|
return output;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 8. 执行取消报名
|
|
/// </summary>
|
|
[DisplayName("执行取消报名")]
|
|
[HttpPost("cancel/execute")]
|
|
[UnitOfWork]
|
|
public async Task<CancelExecuteOutput> CancelExecute([FromBody] CancelExecuteInput input)
|
|
{
|
|
var userId = _authManager.UserId;
|
|
var registration = await _registrationRepository.GetFirstAsync(x => x.Id == input.RegistrationId && x.CreatedUserId == userId);
|
|
if (registration == null) throw Oops.Bah("报名记录不存在");
|
|
|
|
// 检查状态
|
|
if (registration.PaymentStatus == 4) throw Oops.Bah("订单已取消");
|
|
if (registration.PaymentStatus != 1) throw Oops.Bah("未支付的订单无法退款");
|
|
|
|
var now = DateTime.Now;
|
|
var output = new CancelExecuteOutput
|
|
{
|
|
Success = true,
|
|
Message = "取消报名成功",
|
|
CancelTime = now,
|
|
RefundAmount = registration.TotalAmount
|
|
};
|
|
|
|
// 更新报名状态
|
|
registration.PaymentStatus = 3; // 已退款
|
|
registration.UpdatedUserId = userId;
|
|
registration.UpdatedTime = now;
|
|
registration.RegistrationRemark = input.CancelReason;
|
|
await _registrationRepository.UpdateAsync(registration);
|
|
|
|
// 如果有支付,执行退款
|
|
if (registration.TotalAmount > 0 && !string.IsNullOrEmpty(registration.OrderNo))
|
|
{
|
|
var eventInfo = await _eventInfoRepository.GetFirstAsync(e=>e.Id == registration.EventId);
|
|
// 查找商户配置
|
|
var merchant = await _merchantRepository.GetFirstAsync(x => x.Id == eventInfo.MerchantId);
|
|
|
|
if (merchant != null)
|
|
{
|
|
var wechatConfig = await _weChatPayConfigRepository.GetFirstAsync(x => x.MerchantId == merchant.Id);
|
|
|
|
if (wechatConfig != null)
|
|
{
|
|
// 生成退款单号
|
|
var outRefundNo = $"REF{DateTime.Now:yyyyMMddHHmmss}{new Random().Next(1000, 9999)}";
|
|
output.RefundNo = outRefundNo;
|
|
|
|
// 调用微信退款服务
|
|
var refundRequest = new Core.Infrastructure.WeChat.Dto.CreateRefundRequest
|
|
{
|
|
OutTradeNo = registration.OrderNo,
|
|
OutRefundNo = outRefundNo,
|
|
RefundAmount = AmountHelper.YuanToFen(registration.TotalAmount), // 转分
|
|
TotalAmount = AmountHelper.YuanToFen(registration.TotalAmount),
|
|
Reason = input.CancelReason
|
|
};
|
|
|
|
var refundResult = await _weChatPayService.CreateRefundAsync(wechatConfig, refundRequest);
|
|
if (!refundResult.Success)
|
|
{
|
|
output.Success = false;
|
|
output.Message = refundResult.Message;
|
|
output.RefundAmount = 0;
|
|
output.RefundNo = null;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return output;
|
|
}
|
|
}
|