2026-08-22 20:34:05 +08:00
|
|
|
|
using YD_Event.Application.Auth;
|
|
|
|
|
|
using YD_Event.Application.Scoring.Dtos;
|
|
|
|
|
|
|
|
|
|
|
|
namespace YD_Event.Application.Scoring;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 裁判评分与成绩计算(后台记分台)
|
|
|
|
|
|
/// </summary>
|
2026-08-30 21:46:05 +08:00
|
|
|
|
[ApiDescriptionSettings("打分系统")]
|
2026-09-08 16:27:27 +08:00
|
|
|
|
[Route("api/judgescore")]
|
2026-08-22 20:34:05 +08:00
|
|
|
|
public class JudgeScoreService : IDynamicApiController, ITransient
|
|
|
|
|
|
{
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 修正状态:0-已申请
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private const int CorrectionStatusApplied = 0;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 修正状态:10-已批准
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private const int CorrectionStatusApproved = 10;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 修正状态:20-已拒绝
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private const int CorrectionStatusRejected = 20;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 修正状态:30-裁判修改中
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private const int CorrectionStatusEditing = 30;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 修正状态:40-已完成
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private const int CorrectionStatusCompleted = 40;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 修正状态:90-已取消
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private const int CorrectionStatusCancelled = 90;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 评分单状态:20-已确认
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private const int SheetStatusConfirmed = 20;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 场次状态:60-已结束
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private const int SessionStatusFinished = 60;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 消息类型:1-成绩修正提醒
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private const int MessageTypeCorrection = 1;
|
|
|
|
|
|
|
|
|
|
|
|
private readonly ISqlSugarRepository<EventSession> _sessionRepository;
|
|
|
|
|
|
private readonly ISqlSugarRepository<EventSessionEntry> _entryRepository;
|
|
|
|
|
|
private readonly ISqlSugarRepository<EventVenue> _venueRepository;
|
|
|
|
|
|
private readonly ISqlSugarRepository<YD_Event.Core.Entities.ScoreRule> _ruleRepository;
|
|
|
|
|
|
private readonly ISqlSugarRepository<EventJudgeAssignment> _assignmentRepository;
|
|
|
|
|
|
private readonly ISqlSugarRepository<EventJudgeScoreSheet> _sheetRepository;
|
|
|
|
|
|
private readonly ISqlSugarRepository<EventScoreCorrection> _correctionRepository;
|
|
|
|
|
|
private readonly ISqlSugarRepository<EventJudgeMessage> _messageRepository;
|
2026-09-08 14:07:59 +08:00
|
|
|
|
private readonly ISqlSugarRepository<EventJudgeScoreAttachment> _attachmentRepository;
|
2026-08-22 20:34:05 +08:00
|
|
|
|
private readonly ISqlSugarRepository<YD_Event.Core.Entities.Referee> _refereeRepository;
|
|
|
|
|
|
private readonly ISqlSugarRepository<EventResult> _resultRepository;
|
|
|
|
|
|
private readonly EventScoreCalculationService _calculationService;
|
|
|
|
|
|
private readonly IIdGenerator _idGenerator;
|
|
|
|
|
|
private readonly AuthManager _authManager;
|
|
|
|
|
|
|
|
|
|
|
|
public JudgeScoreService(ISqlSugarRepository<EventSession> sessionRepository,
|
|
|
|
|
|
ISqlSugarRepository<EventSessionEntry> entryRepository, ISqlSugarRepository<EventVenue> venueRepository,
|
|
|
|
|
|
ISqlSugarRepository<YD_Event.Core.Entities.ScoreRule> ruleRepository,
|
|
|
|
|
|
ISqlSugarRepository<EventJudgeAssignment> assignmentRepository,
|
|
|
|
|
|
ISqlSugarRepository<EventJudgeScoreSheet> sheetRepository,
|
|
|
|
|
|
ISqlSugarRepository<EventScoreCorrection> correctionRepository,
|
|
|
|
|
|
ISqlSugarRepository<EventJudgeMessage> messageRepository,
|
2026-09-08 14:07:59 +08:00
|
|
|
|
ISqlSugarRepository<EventJudgeScoreAttachment> attachmentRepository,
|
2026-08-22 20:34:05 +08:00
|
|
|
|
ISqlSugarRepository<YD_Event.Core.Entities.Referee> refereeRepository,
|
|
|
|
|
|
ISqlSugarRepository<EventResult> resultRepository,
|
|
|
|
|
|
EventScoreCalculationService calculationService, IIdGenerator idGenerator, AuthManager authManager)
|
|
|
|
|
|
{
|
|
|
|
|
|
_sessionRepository = sessionRepository; _entryRepository = entryRepository; _venueRepository = venueRepository;
|
|
|
|
|
|
_ruleRepository = ruleRepository; _assignmentRepository = assignmentRepository; _sheetRepository = sheetRepository;
|
|
|
|
|
|
_correctionRepository = correctionRepository; _messageRepository = messageRepository;
|
2026-09-08 14:07:59 +08:00
|
|
|
|
_attachmentRepository = attachmentRepository;
|
2026-08-22 20:34:05 +08:00
|
|
|
|
_refereeRepository = refereeRepository; _resultRepository = resultRepository;
|
|
|
|
|
|
_calculationService = calculationService; _idGenerator = idGenerator; _authManager = authManager;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 获取场次记分台。按当前已提交的评分单实时算分,不写库,可随时刷新查看进度。
|
|
|
|
|
|
/// </summary>
|
2026-09-08 16:27:27 +08:00
|
|
|
|
[HttpGet("board/{sessionId}")]
|
2026-08-22 20:34:05 +08:00
|
|
|
|
[DisplayName("获取场次记分台")]
|
|
|
|
|
|
public async Task<JudgeScoreBoardOutput> GetBoard(long sessionId)
|
|
|
|
|
|
{
|
|
|
|
|
|
var ctx = await _calculationService.LoadContextAsync(sessionId);
|
|
|
|
|
|
var outcomes = await _calculationService.EvaluateSessionAsync(sessionId, ctx);
|
|
|
|
|
|
return await BuildBoardAsync(ctx, outcomes);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 重算整场成绩。所有应到裁判都已提交的出场写入成绩并按规则重排名次,未评满的出场只返回进度。
|
|
|
|
|
|
/// </summary>
|
2026-09-08 16:27:27 +08:00
|
|
|
|
[HttpPost("recalculate/{sessionId}")]
|
2026-08-22 20:34:05 +08:00
|
|
|
|
[DisplayName("重算场次成绩")]
|
|
|
|
|
|
[UnitOfWork]
|
|
|
|
|
|
public async Task<JudgeScoreBoardOutput> Recalculate(long sessionId)
|
|
|
|
|
|
{
|
|
|
|
|
|
var ctx = await _calculationService.LoadContextAsync(sessionId);
|
|
|
|
|
|
var outcomes = await _calculationService.RecalculateSessionAsync(sessionId,
|
|
|
|
|
|
EventScoreCalculationService.SourceTypeNormal, _authManager.UserId, ctx);
|
|
|
|
|
|
return await BuildBoardAsync(ctx, outcomes);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 成绩排名查询。按项目、组别筛选,名次为 0 的(未评满、退赛、取消资格)排在最后。
|
|
|
|
|
|
/// </summary>
|
2026-09-08 16:27:27 +08:00
|
|
|
|
[HttpGet("ranking")]
|
2026-08-22 20:34:05 +08:00
|
|
|
|
[DisplayName("成绩排名查询")]
|
|
|
|
|
|
public async Task<List<JudgeRankingRowOutput>> GetRanking([FromQuery] JudgeRankingQueryInput input)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (input.EventId <= 0) throw Oops.Bah("请指定赛事");
|
|
|
|
|
|
var results = await _resultRepository.AsQueryable()
|
|
|
|
|
|
.Where(x => x.EventId == input.EventId && x.Status == AvailabilityStatus.Enable)
|
|
|
|
|
|
.WhereIF(input.ProjectId.HasValue, x => x.ProjectId == input.ProjectId!.Value)
|
|
|
|
|
|
.WhereIF(!string.IsNullOrWhiteSpace(input.GroupName), x => x.GroupName == input.GroupName)
|
|
|
|
|
|
.WhereIF(input.ResultStatus.HasValue, x => x.ResultStatus == input.ResultStatus!.Value)
|
|
|
|
|
|
.ToListAsync();
|
|
|
|
|
|
|
|
|
|
|
|
return results
|
|
|
|
|
|
.OrderBy(x => x.ProjectId).ThenBy(x => x.GroupName)
|
|
|
|
|
|
// 名次 0 表示尚未参与排名,统一沉到组内末尾,避免占据第一名的位置。
|
|
|
|
|
|
.ThenBy(x => x.Rank == 0 ? int.MaxValue : x.Rank)
|
|
|
|
|
|
.ThenByDescending(x => x.TotalScore ?? 0m).ThenBy(x => x.ParticipantName)
|
|
|
|
|
|
.Select(x => new JudgeRankingRowOutput
|
|
|
|
|
|
{
|
|
|
|
|
|
ResultId = x.Id,
|
|
|
|
|
|
SessionEntryId = x.SessionEntryId,
|
|
|
|
|
|
ProjectId = x.ProjectId,
|
|
|
|
|
|
ProjectTypeName = x.ProjectTypeName ?? string.Empty,
|
|
|
|
|
|
GroupName = x.GroupName ?? string.Empty,
|
|
|
|
|
|
ParticipantName = x.ParticipantName ?? string.Empty,
|
|
|
|
|
|
Rank = x.Rank,
|
|
|
|
|
|
TotalScore = x.Score ?? string.Empty,
|
|
|
|
|
|
DifficultyScore = x.DifficultyScore,
|
|
|
|
|
|
PerformanceScore = x.PerformanceScore,
|
|
|
|
|
|
RequiredScore = x.RequiredScore,
|
|
|
|
|
|
DeductionScore = x.DeductionScore,
|
|
|
|
|
|
IsDisqualified = x.IsDisqualified,
|
|
|
|
|
|
ResultStatus = x.ResultStatus,
|
|
|
|
|
|
ResultStatusName = ResultStatusName(x.ResultStatus),
|
2026-08-30 21:46:05 +08:00
|
|
|
|
ResultVersion = x.ResultVersion,
|
|
|
|
|
|
ReviewUserId = x.ReviewUserId,
|
|
|
|
|
|
ReviewTime = x.ReviewTime
|
2026-08-22 20:34:05 +08:00
|
|
|
|
}).ToList();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 确认场次成绩。仅在全场都已评满时确认,确认后裁判端不能再自行申请修正。
|
|
|
|
|
|
/// </summary>
|
2026-09-08 16:27:27 +08:00
|
|
|
|
[HttpPost("confirm/{sessionId}")]
|
2026-08-22 20:34:05 +08:00
|
|
|
|
[DisplayName("确认场次成绩")]
|
|
|
|
|
|
[UnitOfWork]
|
|
|
|
|
|
public async Task<ConfirmSessionScoreOutput> ConfirmSession(long sessionId)
|
|
|
|
|
|
{
|
|
|
|
|
|
var session = await _sessionRepository.GetFirstAsync(x => x.Id == sessionId);
|
|
|
|
|
|
if (session == null) throw Oops.Bah("比赛场次不存在");
|
|
|
|
|
|
var ctx = await _calculationService.LoadContextAsync(session);
|
|
|
|
|
|
|
|
|
|
|
|
// 先只算不写:确认被拒时不留下任何写库痕迹,也不会把已确认的成绩打回待复核。
|
|
|
|
|
|
var preview = await _calculationService.EvaluateSessionAsync(sessionId, ctx);
|
|
|
|
|
|
var output = new ConfirmSessionScoreOutput
|
|
|
|
|
|
{
|
|
|
|
|
|
PendingEntries = preview.Where(x => !x.IsSkipped && !x.IsCompleted).Select(x => x.EntryNo).ToList()
|
|
|
|
|
|
};
|
|
|
|
|
|
// 有出场没评满就不确认,否则先确认的成绩会挡住后到的评分。
|
|
|
|
|
|
if (output.PendingEntries.Count > 0) return output;
|
|
|
|
|
|
|
|
|
|
|
|
var outcomes = await _calculationService.RecalculateSessionAsync(sessionId,
|
|
|
|
|
|
EventScoreCalculationService.SourceTypeNormal, _authManager.UserId, ctx);
|
|
|
|
|
|
|
|
|
|
|
|
var entryIds = outcomes.Select(x => (long?)x.SessionEntryId).ToList();
|
|
|
|
|
|
var results = await _resultRepository.GetListAsync(x => entryIds.Contains(x.SessionEntryId)
|
|
|
|
|
|
&& x.ResultStatus == EventScoreCalculationService.ResultStatusPendingReview);
|
|
|
|
|
|
var now = DateTime.Now;
|
|
|
|
|
|
foreach (var result in results)
|
|
|
|
|
|
{
|
|
|
|
|
|
result.ResultStatus = EventScoreCalculationService.ResultStatusConfirmed;
|
2026-08-30 21:46:05 +08:00
|
|
|
|
result.ReviewUserId = _authManager.UserId;
|
|
|
|
|
|
result.ReviewTime = now;
|
2026-08-22 20:34:05 +08:00
|
|
|
|
result.UpdatedUserId = _authManager.UserId;
|
|
|
|
|
|
result.UpdatedTime = now;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (results.Count > 0) await _resultRepository.UpdateRangeAsync(results);
|
|
|
|
|
|
output.ConfirmedCount = results.Count;
|
|
|
|
|
|
|
|
|
|
|
|
// 评分单一并置为已确认,裁判端据此禁用修正入口。
|
|
|
|
|
|
var plainIds = outcomes.Select(x => x.SessionEntryId).ToList();
|
|
|
|
|
|
var sheets = await _sheetRepository.GetListAsync(x => plainIds.Contains(x.SessionEntryId)
|
|
|
|
|
|
&& x.IsCurrent && x.SheetStatus == EventScoreCalculationService.SheetStatusSubmitted);
|
|
|
|
|
|
foreach (var sheet in sheets)
|
|
|
|
|
|
{
|
|
|
|
|
|
sheet.SheetStatus = SheetStatusConfirmed;
|
|
|
|
|
|
sheet.UpdatedUserId = _authManager.UserId;
|
|
|
|
|
|
sheet.UpdatedTime = now;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (sheets.Count > 0) await _sheetRepository.UpdateRangeAsync(sheets);
|
|
|
|
|
|
|
|
|
|
|
|
if (session.SessionStatus != SessionStatusFinished)
|
|
|
|
|
|
{
|
|
|
|
|
|
session.SessionStatus = SessionStatusFinished;
|
|
|
|
|
|
session.Version++;
|
|
|
|
|
|
session.EndTime ??= now;
|
|
|
|
|
|
session.UpdatedUserId = _authManager.UserId;
|
|
|
|
|
|
session.UpdatedTime = now;
|
|
|
|
|
|
await _sessionRepository.UpdateAsync(session);
|
|
|
|
|
|
}
|
|
|
|
|
|
output.SessionFinished = true;
|
|
|
|
|
|
return output;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 获取成绩修正申请列表
|
|
|
|
|
|
/// </summary>
|
2026-09-08 16:27:27 +08:00
|
|
|
|
[HttpGet("corrections/{eventId}")]
|
2026-08-22 20:34:05 +08:00
|
|
|
|
[DisplayName("获取成绩修正申请列表")]
|
|
|
|
|
|
public async Task<List<JudgeCorrectionOutput>> GetCorrections(long eventId, [FromQuery] int? correctionStatus = null)
|
|
|
|
|
|
{
|
|
|
|
|
|
var corrections = (await _correctionRepository.AsQueryable()
|
|
|
|
|
|
.Where(x => x.EventId == eventId)
|
|
|
|
|
|
.WhereIF(correctionStatus.HasValue, x => x.CorrectionStatus == correctionStatus!.Value)
|
|
|
|
|
|
.ToListAsync())
|
|
|
|
|
|
.OrderByDescending(x => x.CreatedTime).ToList();
|
|
|
|
|
|
if (corrections.Count == 0) return new List<JudgeCorrectionOutput>();
|
|
|
|
|
|
|
|
|
|
|
|
var sessionIds = corrections.Select(x => x.SessionId).Distinct().ToList();
|
|
|
|
|
|
var sessions = await _sessionRepository.GetListAsync(x => sessionIds.Contains(x.Id));
|
|
|
|
|
|
var ruleIds = sessions.Select(x => x.ScoreRuleId).Distinct().ToList();
|
|
|
|
|
|
var rules = await _ruleRepository.GetListAsync(x => ruleIds.Contains(x.Id));
|
|
|
|
|
|
// 每个场次的小数位取自各自的计分规则,避免跨规则显示口径不一致。
|
|
|
|
|
|
var configs = sessions.GroupBy(x => x.Id).ToDictionary(x => x.Key,
|
|
|
|
|
|
x => ScoreRuleConfigService.Parse(rules.FirstOrDefault(r => r.Id == x.First().ScoreRuleId)?.RuleConfigJson));
|
|
|
|
|
|
|
|
|
|
|
|
var entryIds = corrections.Select(x => x.SessionEntryId).Distinct().ToList();
|
|
|
|
|
|
var entries = await _entryRepository.GetListAsync(x => entryIds.Contains(x.Id));
|
|
|
|
|
|
var sheetIds = corrections.Select(x => x.OriginalScoreSheetId)
|
|
|
|
|
|
.Concat(corrections.Where(x => x.NewScoreSheetId.HasValue).Select(x => x.NewScoreSheetId!.Value))
|
|
|
|
|
|
.Distinct().ToList();
|
|
|
|
|
|
var sheets = await _sheetRepository.GetListAsync(x => sheetIds.Contains(x.Id));
|
|
|
|
|
|
var assignmentIds = corrections.Select(x => x.JudgeAssignmentId).Distinct().ToList();
|
|
|
|
|
|
var assignments = await _assignmentRepository.GetListAsync(x => assignmentIds.Contains(x.Id));
|
|
|
|
|
|
var refereeIds = assignments.Select(x => x.RefereeId).Distinct().ToList();
|
|
|
|
|
|
var referees = await _refereeRepository.GetListAsync(x => refereeIds.Contains(x.Id));
|
|
|
|
|
|
|
|
|
|
|
|
return corrections.Select(correction =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var session = sessions.FirstOrDefault(x => x.Id == correction.SessionId);
|
|
|
|
|
|
var entry = entries.FirstOrDefault(x => x.Id == correction.SessionEntryId);
|
|
|
|
|
|
var original = sheets.FirstOrDefault(x => x.Id == correction.OriginalScoreSheetId);
|
|
|
|
|
|
var updated = correction.NewScoreSheetId.HasValue
|
|
|
|
|
|
? sheets.FirstOrDefault(x => x.Id == correction.NewScoreSheetId!.Value)
|
|
|
|
|
|
: null;
|
|
|
|
|
|
var assignment = assignments.FirstOrDefault(x => x.Id == correction.JudgeAssignmentId);
|
|
|
|
|
|
var referee = assignment == null ? null : referees.FirstOrDefault(x => x.Id == assignment.RefereeId);
|
|
|
|
|
|
var config = configs.TryGetValue(correction.SessionId, out var found) ? found : ScoreRuleConfigService.Parse(null);
|
|
|
|
|
|
return new JudgeCorrectionOutput
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = correction.Id,
|
|
|
|
|
|
SessionId = correction.SessionId,
|
|
|
|
|
|
SessionName = session == null ? string.Empty : session.SessionName ?? session.SessionNo,
|
|
|
|
|
|
SessionEntryId = correction.SessionEntryId,
|
|
|
|
|
|
EntryNo = entry?.EntryNo ?? string.Empty,
|
|
|
|
|
|
ParticipantName = entry == null
|
|
|
|
|
|
? string.Empty
|
|
|
|
|
|
: string.IsNullOrWhiteSpace(entry.TeamName) ? entry.ParticipantName : entry.TeamName!,
|
|
|
|
|
|
OriginalScoreSheetId = correction.OriginalScoreSheetId,
|
|
|
|
|
|
NewScoreSheetId = correction.NewScoreSheetId,
|
|
|
|
|
|
JudgeRole = correction.JudgeRole,
|
|
|
|
|
|
RefereeName = referee?.Name ?? string.Empty,
|
|
|
|
|
|
OriginalScore = original == null ? string.Empty : config.Format(original.TotalScore),
|
|
|
|
|
|
NewScore = updated == null ? null : config.Format(updated.TotalScore),
|
|
|
|
|
|
ApplyReason = correction.ApplyReason,
|
|
|
|
|
|
ApproveRemark = correction.ApproveRemark,
|
|
|
|
|
|
CorrectionStatus = correction.CorrectionStatus,
|
|
|
|
|
|
CorrectionStatusName = CorrectionStatusName(correction.CorrectionStatus),
|
|
|
|
|
|
CreatedTime = correction.CreatedTime,
|
|
|
|
|
|
ApproveTime = correction.ApproveTime,
|
|
|
|
|
|
CompletedTime = correction.CompletedTime
|
|
|
|
|
|
};
|
|
|
|
|
|
}).ToList();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 后台发起成绩修正。发起后该裁判可重新提交这张评分单,原评分在新评分提交时才失效。
|
|
|
|
|
|
/// </summary>
|
2026-09-08 16:27:27 +08:00
|
|
|
|
[HttpPost("corrections/initiate")]
|
2026-08-22 20:34:05 +08:00
|
|
|
|
[DisplayName("发起成绩修正")]
|
|
|
|
|
|
[UnitOfWork]
|
|
|
|
|
|
public async Task<long> InitiateCorrection([FromBody] InitiateJudgeCorrectionInput input)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(input.Reason)) throw Oops.Bah("请填写修正原因");
|
|
|
|
|
|
var sheet = await _sheetRepository.GetFirstAsync(x => x.Id == input.ScoreSheetId);
|
|
|
|
|
|
if (sheet == null) throw Oops.Bah("评分单不存在");
|
|
|
|
|
|
if (!sheet.IsCurrent || sheet.SheetStatus < EventScoreCalculationService.SheetStatusSubmitted)
|
|
|
|
|
|
throw Oops.Bah("该评分单不是当前生效的已提交评分,无需修正");
|
|
|
|
|
|
if (await HasOpenCorrectionAsync(sheet.Id)) throw Oops.Bah("该评分单已有修正流程在处理中");
|
|
|
|
|
|
|
|
|
|
|
|
var now = DateTime.Now;
|
|
|
|
|
|
var correction = new EventScoreCorrection
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = _idGenerator.NextId(),
|
|
|
|
|
|
EventId = sheet.EventId,
|
|
|
|
|
|
SessionId = sheet.SessionId,
|
|
|
|
|
|
SessionEntryId = sheet.SessionEntryId,
|
|
|
|
|
|
OriginalScoreSheetId = sheet.Id,
|
|
|
|
|
|
JudgeAssignmentId = sheet.JudgeAssignmentId,
|
|
|
|
|
|
JudgeRole = sheet.JudgeRole,
|
|
|
|
|
|
ApplyUserId = _authManager.UserId,
|
|
|
|
|
|
ApplyReason = input.Reason.Trim(),
|
|
|
|
|
|
// 后台发起本身即代表已批准,裁判端可直接重新评分。
|
|
|
|
|
|
CorrectionStatus = CorrectionStatusApproved,
|
|
|
|
|
|
ApproveUserId = _authManager.UserId,
|
|
|
|
|
|
ApproveRemark = "赛事管理员发起",
|
|
|
|
|
|
ApproveTime = now,
|
|
|
|
|
|
CreatedUserId = _authManager.UserId,
|
|
|
|
|
|
CreatedTime = now
|
|
|
|
|
|
};
|
|
|
|
|
|
await _correctionRepository.InsertAsync(correction);
|
|
|
|
|
|
await NotifyRefereeAsync(correction, sheet, "成绩修正通知",
|
|
|
|
|
|
$"请对{sheet.JudgeRole}号位的评分重新提交,原因:{correction.ApplyReason}");
|
|
|
|
|
|
return correction.Id;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 审批裁判提交的成绩修正申请
|
|
|
|
|
|
/// </summary>
|
2026-09-08 16:27:27 +08:00
|
|
|
|
[HttpPost("corrections/approve")]
|
2026-08-22 20:34:05 +08:00
|
|
|
|
[DisplayName("审批成绩修正申请")]
|
|
|
|
|
|
[UnitOfWork]
|
|
|
|
|
|
public async Task ApproveCorrection([FromBody] ApproveJudgeCorrectionInput input)
|
|
|
|
|
|
{
|
|
|
|
|
|
var correction = await _correctionRepository.GetFirstAsync(x => x.Id == input.CorrectionId);
|
|
|
|
|
|
if (correction == null) throw Oops.Bah("修正申请不存在");
|
|
|
|
|
|
if (correction.CorrectionStatus != CorrectionStatusApplied)
|
|
|
|
|
|
throw Oops.Bah($"该申请当前为“{CorrectionStatusName(correction.CorrectionStatus)}”,不能重复审批");
|
|
|
|
|
|
var sheet = await _sheetRepository.GetFirstAsync(x => x.Id == correction.OriginalScoreSheetId);
|
|
|
|
|
|
if (sheet == null) throw Oops.Bah("被修正的评分单不存在");
|
|
|
|
|
|
|
|
|
|
|
|
var now = DateTime.Now;
|
|
|
|
|
|
correction.CorrectionStatus = input.Approved ? CorrectionStatusApproved : CorrectionStatusRejected;
|
|
|
|
|
|
correction.ApproveUserId = _authManager.UserId;
|
|
|
|
|
|
correction.ApproveRemark = input.ApproveRemark;
|
|
|
|
|
|
correction.ApproveTime = now;
|
|
|
|
|
|
if (!input.Approved) correction.CompletedTime = now;
|
|
|
|
|
|
correction.UpdatedUserId = _authManager.UserId;
|
|
|
|
|
|
correction.UpdatedTime = now;
|
|
|
|
|
|
await _correctionRepository.UpdateAsync(correction);
|
|
|
|
|
|
|
|
|
|
|
|
await NotifyRefereeAsync(correction, sheet,
|
|
|
|
|
|
input.Approved ? "成绩修正申请已通过" : "成绩修正申请被驳回",
|
|
|
|
|
|
input.Approved
|
|
|
|
|
|
? $"{sheet.JudgeRole}号位的修正申请已通过,请重新提交评分。"
|
|
|
|
|
|
: $"{sheet.JudgeRole}号位的修正申请未通过。{input.ApproveRemark}");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 组装记分台数据,名次与成绩状态取已落库的成绩
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private async Task<JudgeScoreBoardOutput> BuildBoardAsync(ScoringContext ctx, List<EntryCalculationOutcome> outcomes)
|
|
|
|
|
|
{
|
|
|
|
|
|
var venue = await _venueRepository.GetFirstAsync(x => x.Id == ctx.Session.VenueId);
|
|
|
|
|
|
var output = new JudgeScoreBoardOutput
|
|
|
|
|
|
{
|
|
|
|
|
|
SessionId = ctx.Session.Id,
|
|
|
|
|
|
EventId = ctx.Session.EventId,
|
|
|
|
|
|
SessionName = ctx.Session.SessionName ?? ctx.Session.SessionNo,
|
|
|
|
|
|
VenueName = venue?.VenueName ?? string.Empty,
|
|
|
|
|
|
ProjectName = ctx.Project.Name,
|
|
|
|
|
|
RuleName = ctx.Rule.RuleName,
|
|
|
|
|
|
Formula = EntryResultCalculationService.BuildFormula(ctx.Config),
|
|
|
|
|
|
SessionStatus = ctx.Session.SessionStatus,
|
|
|
|
|
|
EntryCount = outcomes.Count,
|
|
|
|
|
|
CompletedCount = outcomes.Count(x => x.IsCompleted)
|
|
|
|
|
|
};
|
|
|
|
|
|
if (outcomes.Count == 0) return output;
|
|
|
|
|
|
|
|
|
|
|
|
var entryIds = outcomes.Select(x => (long?)x.SessionEntryId).ToList();
|
|
|
|
|
|
var results = await _resultRepository.GetListAsync(x => entryIds.Contains(x.SessionEntryId)
|
|
|
|
|
|
&& x.ResultStatus != EventScoreCalculationService.ResultStatusVoided);
|
2026-09-08 14:07:59 +08:00
|
|
|
|
var sheetIds = outcomes.SelectMany(x => x.Aggregate?.Symbols ?? new List<EntrySymbolScore>())
|
|
|
|
|
|
.SelectMany(x => x.Judges).Select(x => x.ScoreSheetId).Distinct().ToList();
|
|
|
|
|
|
var annotationUrls = sheetIds.Count == 0
|
|
|
|
|
|
? new Dictionary<long, string>()
|
|
|
|
|
|
: (await _attachmentRepository.GetListAsync(x => sheetIds.Contains(x.ScoreSheetId)
|
|
|
|
|
|
&& x.IsCurrent && x.AttachmentType == "score_annotation_png"))
|
|
|
|
|
|
.GroupBy(x => x.ScoreSheetId).ToDictionary(x => x.Key, x => x.OrderByDescending(y => y.CreatedTime).First().FileUrl);
|
2026-08-22 20:34:05 +08:00
|
|
|
|
// 一个出场(队伍)的多名运动员共享同一名次,取任意一条即可。
|
|
|
|
|
|
var resultByEntry = results.Where(x => x.SessionEntryId.HasValue)
|
|
|
|
|
|
.GroupBy(x => x.SessionEntryId!.Value).ToDictionary(x => x.Key, x => x.First());
|
|
|
|
|
|
|
|
|
|
|
|
foreach (var outcome in outcomes)
|
|
|
|
|
|
{
|
|
|
|
|
|
resultByEntry.TryGetValue(outcome.SessionEntryId, out var result);
|
|
|
|
|
|
var entry = new JudgeScoreBoardEntryOutput
|
|
|
|
|
|
{
|
|
|
|
|
|
SessionEntryId = outcome.SessionEntryId,
|
|
|
|
|
|
EntryNo = outcome.EntryNo,
|
|
|
|
|
|
ParticipantName = outcome.ParticipantName,
|
|
|
|
|
|
GroupName = outcome.GroupName,
|
|
|
|
|
|
IsCompleted = outcome.IsCompleted,
|
|
|
|
|
|
IsSkipped = outcome.IsSkipped,
|
|
|
|
|
|
SkipReason = outcome.SkipReason,
|
|
|
|
|
|
IsDisqualified = outcome.IsDisqualified,
|
|
|
|
|
|
TotalScore = outcome.IsDisqualified
|
|
|
|
|
|
? "取消资格"
|
|
|
|
|
|
: outcome.IsCompleted ? ctx.Config.Format(outcome.TotalScore) : string.Empty,
|
|
|
|
|
|
Rank = result?.Rank ?? 0,
|
|
|
|
|
|
ResultStatus = result?.ResultStatus ?? 0,
|
|
|
|
|
|
ResultStatusName = ResultStatusName(result?.ResultStatus ?? 0),
|
|
|
|
|
|
Progress = outcome.Roles.Select(role => new JudgeBoardRoleProgressOutput
|
|
|
|
|
|
{
|
|
|
|
|
|
RoleType = role.RoleType, RoleName = role.RoleName, Symbol = role.Symbol,
|
|
|
|
|
|
ExpectedCount = role.ExpectedCount, SubmittedCount = role.SubmittedCount,
|
|
|
|
|
|
PendingJudges = role.PendingJudges
|
|
|
|
|
|
}).ToList()
|
|
|
|
|
|
};
|
|
|
|
|
|
if (outcome.Aggregate != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
entry.Symbols = outcome.Aggregate.Symbols.Select(symbol => new JudgeSymbolScoreOutput
|
|
|
|
|
|
{
|
|
|
|
|
|
Symbol = symbol.Symbol, Label = symbol.Label, RoleType = symbol.RoleType,
|
|
|
|
|
|
Aggregate = symbol.Aggregate, Sign = symbol.Sign, Value = ctx.Config.Format(symbol.Value),
|
|
|
|
|
|
Judges = symbol.Judges.Select(judge => new JudgeBoardSheetScoreOutput
|
|
|
|
|
|
{
|
|
|
|
|
|
ScoreSheetId = judge.ScoreSheetId, RefereeId = judge.RefereeId,
|
|
|
|
|
|
RefereeName = judge.JudgeName ?? string.Empty, JudgeRole = judge.JudgeRole,
|
|
|
|
|
|
JudgeRoleType = judge.JudgeRoleType, Value = ctx.Config.Format(judge.Value),
|
2026-09-08 14:07:59 +08:00
|
|
|
|
IsDisqualified = judge.IsDisqualified,
|
|
|
|
|
|
AnnotationUrl = annotationUrls.GetValueOrDefault(judge.ScoreSheetId)
|
2026-08-22 20:34:05 +08:00
|
|
|
|
}).ToList()
|
|
|
|
|
|
}).ToList();
|
|
|
|
|
|
}
|
|
|
|
|
|
output.Entries.Add(entry);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 出场状态不在计算结果里,单独回填,前端据此区分退赛与未上场。
|
|
|
|
|
|
var plainIds = outcomes.Select(x => x.SessionEntryId).ToList();
|
|
|
|
|
|
var entries = await _entryRepository.GetListAsync(x => plainIds.Contains(x.Id));
|
|
|
|
|
|
foreach (var row in output.Entries)
|
|
|
|
|
|
{
|
|
|
|
|
|
var source = entries.FirstOrDefault(x => x.Id == row.SessionEntryId);
|
|
|
|
|
|
if (source == null) continue;
|
|
|
|
|
|
row.EntryStatus = source.EntryStatus;
|
|
|
|
|
|
row.PlayOrder = source.PlayOrder;
|
|
|
|
|
|
}
|
|
|
|
|
|
return output;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 该评分单是否已有未结束的修正流程
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private async Task<bool> HasOpenCorrectionAsync(long scoreSheetId) =>
|
|
|
|
|
|
await _correctionRepository.IsAnyAsync(x => x.OriginalScoreSheetId == scoreSheetId
|
|
|
|
|
|
&& (x.CorrectionStatus == CorrectionStatusApplied
|
|
|
|
|
|
|| x.CorrectionStatus == CorrectionStatusApproved
|
|
|
|
|
|
|| x.CorrectionStatus == CorrectionStatusEditing));
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 给被修正的裁判发一条站内消息,裁判端在消息列表里看到修正提醒
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private async Task NotifyRefereeAsync(EventScoreCorrection correction, EventJudgeScoreSheet sheet,
|
|
|
|
|
|
string title, string content)
|
|
|
|
|
|
{
|
|
|
|
|
|
var receiverUserId = sheet.JudgeUserId;
|
|
|
|
|
|
if (!receiverUserId.HasValue)
|
|
|
|
|
|
{
|
|
|
|
|
|
var assignment = await _assignmentRepository.GetFirstAsync(x => x.Id == correction.JudgeAssignmentId);
|
|
|
|
|
|
receiverUserId = assignment?.JudgeUserId;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!receiverUserId.HasValue)
|
|
|
|
|
|
{
|
|
|
|
|
|
var referee = await _refereeRepository.GetFirstAsync(x => x.Id == sheet.RefereeId);
|
|
|
|
|
|
receiverUserId = referee?.AppUserId;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 裁判还没绑定小程序账号时消息无处投递,此时只留修正记录,不阻断主流程。
|
|
|
|
|
|
if (!receiverUserId.HasValue) return;
|
|
|
|
|
|
|
|
|
|
|
|
await _messageRepository.InsertAsync(new EventJudgeMessage
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = _idGenerator.NextId(),
|
|
|
|
|
|
EventId = correction.EventId,
|
|
|
|
|
|
ReceiverUserId = receiverUserId.Value,
|
|
|
|
|
|
RefereeId = sheet.RefereeId,
|
|
|
|
|
|
MessageType = MessageTypeCorrection,
|
|
|
|
|
|
Title = title,
|
|
|
|
|
|
Content = content.Length > 512 ? content[..512] : content,
|
|
|
|
|
|
RelationType = nameof(EventScoreCorrection),
|
|
|
|
|
|
RelationId = correction.Id,
|
|
|
|
|
|
IsRead = false,
|
|
|
|
|
|
CreatedUserId = _authManager.UserId,
|
|
|
|
|
|
CreatedTime = DateTime.Now
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 成绩状态名称
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private static string ResultStatusName(int status) => status switch
|
|
|
|
|
|
{
|
|
|
|
|
|
0 => "未生成",
|
|
|
|
|
|
EventScoreCalculationService.ResultStatusPendingReview => "待复核",
|
|
|
|
|
|
EventScoreCalculationService.ResultStatusConfirmed => "已确认",
|
|
|
|
|
|
EventScoreCalculationService.ResultStatusVoided => "已作废",
|
|
|
|
|
|
_ => status.ToString()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 修正状态名称
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private static string CorrectionStatusName(int status) => status switch
|
|
|
|
|
|
{
|
|
|
|
|
|
CorrectionStatusApplied => "已申请",
|
|
|
|
|
|
CorrectionStatusApproved => "已批准",
|
|
|
|
|
|
CorrectionStatusRejected => "已拒绝",
|
|
|
|
|
|
CorrectionStatusEditing => "裁判修改中",
|
|
|
|
|
|
CorrectionStatusCompleted => "已完成",
|
|
|
|
|
|
CorrectionStatusCancelled => "已取消",
|
|
|
|
|
|
_ => status.ToString()
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|