using System.Drawing; using YD_Event.Application.Auth; using YD_Event.Application.Common.Excel; using YD_Event.Application.OrderBook.Dtos; using YD_Event.Core.Domain.Export; namespace YD_Event.Application.OrderBook; /// /// 秩序册导入接口(下载模板 / 同步解析 / 确认落库) /// [ApiDescriptionSettings("打分系统", Name = "order-book")] [Route("api/orderbook")] [Authorize] public class OrderBookService : IDynamicApiController, ITransient { private const string EntrySheetName = "出场秩序"; private const string RefereeSheetName = "裁判名单"; private const string DefaultGroupName = "公开组"; private static readonly TimeSpan BatchTtl = TimeSpan.FromMinutes(30); private readonly ISqlSugarRepository _eventRepository; private readonly ISqlSugarRepository _projectRepository; private readonly ISqlSugarRepository _eventProjectRepository; private readonly ISqlSugarRepository _groupRepository; private readonly ISqlSugarRepository _venueRepository; private readonly ISqlSugarRepository _sessionRepository; private readonly ISqlSugarRepository _entryRepository; private readonly ISqlSugarRepository _refereeRepository; private readonly ISqlSugarRepository _eventRefereeRepository; private readonly ISqlSugarRepository _appUserRepository; private readonly IEasyCachingProvider _cache; private readonly IIdGenerator _idGenerator; private readonly AuthManager _authManager; public OrderBookService( ISqlSugarRepository eventRepository, ISqlSugarRepository projectRepository, ISqlSugarRepository eventProjectRepository, ISqlSugarRepository groupRepository, ISqlSugarRepository venueRepository, ISqlSugarRepository sessionRepository, ISqlSugarRepository entryRepository, ISqlSugarRepository refereeRepository, ISqlSugarRepository eventRefereeRepository, ISqlSugarRepository appUserRepository, IEasyCachingProvider cache, IIdGenerator idGenerator, AuthManager authManager) { _eventRepository = eventRepository; _projectRepository = projectRepository; _eventProjectRepository = eventProjectRepository; _groupRepository = groupRepository; _venueRepository = venueRepository; _sessionRepository = sessionRepository; _entryRepository = entryRepository; _refereeRepository = refereeRepository; _eventRefereeRepository = eventRefereeRepository; _appUserRepository = appUserRepository; _cache = cache; _idGenerator = idGenerator; _authManager = authManager; } #region 下载模板 /// /// 下载秩序册导入模板 /// /// /// [DisplayName("下载秩序册导入模板")] [HttpGet("template")] public IActionResult DownloadTemplate([FromQuery] string? projectMode) { var bytes = new OrderBookTemplateExporter().ExportAsync().GetAwaiter().GetResult(); return new FileContentResult(bytes, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") { FileDownloadName = "秩序册导入模板.xlsx" }; } #endregion #region 同步解析 /// /// 上传并同步解析秩序册(不产生业务数据,结果缓存待确认) /// /// /// /// /// [DisplayName("解析秩序册")] [HttpPost("import")] public async Task Import(long? eventId, string? importMode, IFormFile file) { if (file == null || file.Length == 0) throw Oops.Bah("请上传秩序册文件"); var ext = Path.GetExtension(file.FileName)?.ToLowerInvariant(); if (ext is not ".xlsx" and not ".xls") throw Oops.Bah("仅支持 .xls/.xlsx 格式的秩序册文件"); if (eventId.HasValue && eventId.Value > 0 && !await _eventRepository.IsAnyAsync(e => e.Id == eventId.Value && !e.DeleteMark)) throw Oops.Bah("赛事不存在或已删除"); var mode = string.IsNullOrWhiteSpace(importMode) ? "append" : importMode.Trim().ToLowerInvariant(); if (mode != "append" && mode != "overwrite") throw Oops.Bah("导入模式只能为 append(追加)或 overwrite(覆盖)"); List entryRows; List refereeRows; try { using var stream = file.OpenReadStream(); // NPOI 统一读取,WorkbookFactory 按文件头自动识别 .xls/.xlsx using var workbook = NpoiExcelReader.CreateWorkbook(stream); entryRows = NpoiExcelReader.ReadSheet(workbook, EntrySheetName); refereeRows = NpoiExcelReader.ReadSheet(workbook, RefereeSheetName); } catch (Exception ex) { throw Oops.Bah($"秩序册解析失败,请确认包含「{EntrySheetName}」「{RefereeSheetName}」两个工作表:{ex.Message}"); } var issues = new List(); var draftEntries = ParseEntryRows(entryRows, issues); var draftReferees = ParseRefereeRows(refereeRows, issues); // 同场次出场编号重复校验 var duplicateEntryNos = draftEntries .Where(e => !string.IsNullOrWhiteSpace(e.EntryNo)) .GroupBy(e => $"{e.VenueName}|{e.ProjectName}|{e.GroupName}|{e.EntryNo}") .Where(g => g.Count() > 1) .SelectMany(g => g.Skip(1)) .ToList(); foreach (var dup in duplicateEntryNos) { issues.Add(new OrderBookParseIssue { Row = dup.Row, Column = "出场编号", Message = $"同一场次内出场编号「{dup.EntryNo}」重复", Level = "error" }); } var errorCount = issues.Count(i => i.Level == "error"); var warningCount = issues.Count(i => i.Level == "warning"); var draft = new OrderBookDraft { BatchId = _idGenerator.NextId(), CreatedUserId = _authManager.UserId, HasErrors = errorCount > 0, EventId = eventId, ImportMode = mode, CreatedAt = DateTime.Now, Entries = draftEntries.Where(e => duplicateEntryNos.All(d => d.Row != e.Row)).ToList(), Referees = draftReferees }; var preview = BuildPreview(draft); // 重传覆盖:同一赛事存在旧的未确认批次时移除,始终以最新一次上传为准 if (eventId.HasValue) { var currentKey = CurrentKey(eventId.Value, _authManager.UserId); var oldBatch = await _cache.GetAsync(currentKey); if (oldBatch.HasValue) await _cache.RemoveAsync(BatchKey(oldBatch.Value)); await _cache.SetAsync(currentKey, draft.BatchId, BatchTtl); } await _cache.SetAsync(BatchKey(draft.BatchId), draft, BatchTtl); return new OrderBookImportOutput { BatchId = draft.BatchId, Status = errorCount > 0 ? 20 : 10, TotalRows = entryRows.Count + refereeRows.Count, ValidRows = draft.Entries.Count + draft.Referees.Count, ErrorCount = errorCount, WarningCount = warningCount, Preview = preview, Errors = issues }; } private List ParseEntryRows(List rows, List issues) { var result = new List(); for (var i = 0; i < rows.Count; i++) { var row = rows[i]; var rowNo = i + 2; // 表头占第 1 行 // 整行为空直接跳过 if (IsAllBlank(row.VenueName, row.ProjectName, row.GroupName, row.EntryNo, row.ParticipantName, row.UnitName, row.Mobile, row.IdCard, row.Remark)) continue; var venue = Trim(row.VenueName); var project = Trim(row.ProjectName); var participant = Trim(row.ParticipantName); if (string.IsNullOrWhiteSpace(venue)) issues.Add(Issue(rowNo, "场地名称", "场地名称为必填项")); if (string.IsNullOrWhiteSpace(project)) issues.Add(Issue(rowNo, "项目名称", "项目名称为必填项")); if (string.IsNullOrWhiteSpace(participant)) issues.Add(Issue(rowNo, "姓名/队名", "姓名/队名为必填项")); var mobile = Trim(row.Mobile); if (!string.IsNullOrWhiteSpace(mobile) && !Regex.IsMatch(mobile, @"^1[3-9]\d{9}$")) issues.Add(Issue(rowNo, "联系电话", $"联系电话「{mobile}」格式不正确", "warning")); result.Add(new OrderBookDraftEntry { Row = rowNo, VenueName = venue, ProjectName = project, GroupName = string.IsNullOrWhiteSpace(Trim(row.GroupName)) ? DefaultGroupName : Trim(row.GroupName)!, EntryNo = Trim(row.EntryNo), ParticipantName = participant, UnitName = Trim(row.UnitName), Mobile = mobile }); } return result; } private List ParseRefereeRows(List rows, List issues) { var result = new List(); var seenMobiles = new HashSet(); for (var i = 0; i < rows.Count; i++) { var row = rows[i]; var rowNo = i + 2; if (IsAllBlank(row.Name, row.Mobile, row.RoleText, row.LevelText, row.VenueName, row.ProjectName)) continue; var name = Trim(row.Name); var mobile = Trim(row.Mobile); if (string.IsNullOrWhiteSpace(name)) issues.Add(Issue(rowNo, "姓名", "裁判姓名为必填项")); if (string.IsNullOrWhiteSpace(mobile)) issues.Add(Issue(rowNo, "手机号", "裁判手机号为必填项")); else if (!Regex.IsMatch(mobile, @"^1[3-9]\d{9}$")) issues.Add(Issue(rowNo, "手机号", $"裁判手机号「{mobile}」格式不正确")); else if (!seenMobiles.Add(mobile)) issues.Add(Issue(rowNo, "手机号", $"裁判手机号「{mobile}」重复,已自动合并", "warning")); if (string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(mobile)) continue; // 同手机号只保留第一条 if (seenMobiles.Contains(mobile) && result.Any(r => r.Mobile == mobile)) continue; result.Add(new OrderBookDraftReferee { Row = rowNo, Name = name!, Mobile = mobile!, RoleText = Trim(row.RoleText), LevelText = Trim(row.LevelText), VenueName = Trim(row.VenueName), ProjectName = Trim(row.ProjectName) }); } return result; } private static OrderBookPreview BuildPreview(OrderBookDraft draft) { return new OrderBookPreview { VenueCount = draft.Entries.Select(e => e.VenueName).Where(v => !string.IsNullOrWhiteSpace(v)).Distinct().Count(), SessionCount = draft.Entries .Select(e => $"{e.ProjectName}|{e.GroupName}|{e.VenueName}") .Distinct().Count(), EntryCount = draft.Entries.Count, RefereeCount = draft.Referees.Select(r => r.Mobile).Distinct().Count() }; } #endregion #region 确认落库 /// /// 确认导入:按解析结果生成场地、场次、出场及裁判名单(幂等,同一批次只生成一次) /// /// /// [UnitOfWork] [DisplayName("确认秩序册导入")] [HttpPost("confirm")] public async Task Confirm([FromBody] OrderBookConfirmInput input) { var cached = await _cache.GetAsync(BatchKey(input.BatchId)); if (!cached.HasValue || cached.Value == null) throw Oops.Bah("导入批次不存在或已过期,请重新上传秩序册"); var draft = cached.Value; var currentUserId = _authManager.UserId; if (draft.CreatedUserId != currentUserId) throw Oops.Bah("无权确认其他用户创建的导入批次"); if (draft.HasErrors) throw Oops.Bah("导入文件仍存在校验错误,请修正后重新上传"); var confirmedCache = await _cache.GetAsync(ConfirmedKey(input.BatchId)); if (confirmedCache.HasValue && confirmedCache.Value) throw Oops.Bah("该批次已确认,请勿重复提交"); if (!draft.Entries.Any()) throw Oops.Bah("秩序册没有可导入的出场数据"); if (!draft.EventId.HasValue || draft.EventId.Value <= 0) throw Oops.Bah("请先创建赛事并配置项目评分规则,再导入秩序册"); var eventId = draft.EventId.Value; var eventInfo = await _eventRepository.GetFirstAsync(e => e.Id == eventId && !e.DeleteMark) ?? throw Oops.Bah("赛事不存在或已删除"); var now = DateTime.Now; // 覆盖模式:先软删该赛事既有编排(场地/场次/出场),与编排确认逻辑保持一致 if (draft.ImportMode == "overwrite") await SoftDeleteArrangementAsync(eventId); // 1. 准备项目 / 赛事项目 / 组别映射,并校验评分规则 var projectContext = await EnsureProjectsAsync(draft, eventId, currentUserId, now); // 2. 准备场地 var venueContext = await EnsureVenuesAsync(draft, eventId, currentUserId, now); // 3. 生成场次 var sessionContext = await EnsureSessionsAsync(draft, eventId, projectContext, venueContext, currentUserId, now); // 4. 生成出场 var entryCount = await CreateEntriesAsync(draft, projectContext, venueContext, sessionContext, currentUserId, now); // 5. 裁判名单 + 可选打分账号 var refereeCount = await EnsureRefereesAsync(draft, eventId, input, currentUserId, now); // 幂等标记 + 清理批次缓存 await _cache.SetAsync(ConfirmedKey(input.BatchId), true, BatchTtl); await _cache.RemoveAsync(BatchKey(input.BatchId)); if (await _cache.GetAsync(CurrentKey(eventId, currentUserId)) is { HasValue: true } current && current.Value == input.BatchId) await _cache.RemoveAsync(CurrentKey(eventId, currentUserId)); return new OrderBookConfirmOutput { OrderBookVersion = 1, VenueCount = venueContext.Count, SessionCount = sessionContext.Count, EntryCount = entryCount, RefereeCount = refereeCount }; } private async Task SoftDeleteArrangementAsync(long eventId) { var sessionIds = await _sessionRepository.AsQueryable() .Where(s => !s.DeleteMark && s.EventId == eventId) .Select(s => s.Id).ToListAsync(); if (sessionIds.Any()) { await _entryRepository.SoftDeleteAsync(e => sessionIds.Contains(e.SessionId)); await _sessionRepository.SoftDeleteAsync(s => sessionIds.Contains(s.Id)); } await _venueRepository.SoftDeleteAsync(v => v.EventId == eventId); } // 项目(含赛事项目、组别)建档,返回 (项目名,组别名) -> 上下文 的映射 private async Task> EnsureProjectsAsync( OrderBookDraft draft, long eventId, long userId, DateTime now) { var projectNames = draft.Entries.Select(e => e.ProjectName).Where(n => !string.IsNullOrWhiteSpace(n)).Distinct().ToList(); var result = new Dictionary(); var existProjects = await _projectRepository.AsQueryable() .Where(p => !p.DeleteMark && projectNames.Contains(p.Name)).ToListAsync(); var existEventProjects = await _eventProjectRepository.AsQueryable() .Where(ep => !ep.DeleteMark && ep.EventId == eventId).ToListAsync(); var existGroups = await _groupRepository.AsQueryable() .Where(g => !g.DeleteMark && g.EventId == eventId).ToListAsync(); var maxSort = existProjects.Count == 0 ? 0 : existProjects.Max(p => p.SortIndex); foreach (var projectName in projectNames) { var project = existProjects.FirstOrDefault(p => p.Name == projectName); if (project == null) { project = new Core.Entities.Project { Id = _idGenerator.NextId(), Name = projectName, ProjectType = 0, SortIndex = ++maxSort, Status = AvailabilityStatus.Enable, CreatedUserId = userId, CreatedTime = now }; await _projectRepository.InsertAsync(project); existProjects.Add(project); } var eventProject = existEventProjects.FirstOrDefault(ep => ep.ProjectId == project.Id); if (eventProject == null) { eventProject = new EventProject { Id = _idGenerator.NextId(), EventId = eventId, ProjectId = project.Id, GenderType = Gender.Unknown, CreatedUserId = userId, CreatedTime = now }; await _eventProjectRepository.InsertAsync(eventProject); existEventProjects.Add(eventProject); } var groupNames = draft.Entries.Where(e => e.ProjectName == projectName).Select(e => e.GroupName).Distinct().ToList(); var groupMap = new Dictionary(); foreach (var groupName in groupNames) { if (groupName == DefaultGroupName) { groupMap[groupName] = null; continue; } var group = existGroups.FirstOrDefault(g => g.EventProjectId == eventProject.Id && g.GroupName == groupName); if (group == null) { group = new EventProjectGroup { Id = _idGenerator.NextId(), EventId = eventId, EventProjectId = eventProject.Id, ProjectId = project.Id, ProjectGroupId = 0, GroupName = groupName, BirthDateStart = new DateTime(1900, 1, 1), BirthDateEnd = now.AddYears(1), Gender = Gender.Unknown, CreatedUserId = userId, CreatedTime = now }; await _groupRepository.InsertAsync(group); existGroups.Add(group); } groupMap[groupName] = group.Id; } result[projectName] = new OrderBookProjectContext { Project = project, EventProjectId = eventProject.Id, GroupMap = groupMap }; } // 评分规则必须先配置,否则无法生成场次(与赛事编排确认一致) var missingRule = result.Values.FirstOrDefault(p => p.Project.ScoreRuleId == null || p.Project.ScoreRuleId <= 0); if (missingRule != null) throw Oops.Bah($"项目【{missingRule.Project.Name}】尚未配置评分规则,请先在评分规则中配置后再导入"); return result; } // 场地建档(追加模式按名称复用),返回场地名 -> 场地上下文 的映射 private async Task> EnsureVenuesAsync( OrderBookDraft draft, long eventId, long userId, DateTime now) { var venueNames = draft.Entries.Select(e => e.VenueName).Where(n => !string.IsNullOrWhiteSpace(n)).Distinct().ToList(); var existVenues = await _venueRepository.AsQueryable() .Where(v => !v.DeleteMark && v.EventId == eventId).OrderBy(v => v.Sort).ToListAsync(); var result = new Dictionary(); foreach (var name in venueNames) { var venue = existVenues.FirstOrDefault(v => v.VenueName == name); int venueIndex; if (venue == null) { venueIndex = existVenues.Count + 1; venue = new EventVenue { Id = _idGenerator.NextId(), EventId = eventId, VenueCode = $"V{venueIndex:00}", VenueName = name, Sort = venueIndex, Status = AvailabilityStatus.Enable, CreatedUserId = userId, CreatedTime = now }; await _venueRepository.InsertAsync(venue); existVenues.Add(venue); } else { venueIndex = venue.Sort <= 0 ? existVenues.IndexOf(venue) + 1 : venue.Sort; } result[name] = new OrderBookVenueContext { Venue = venue, Index = venueIndex }; } return result; } // 按 项目×组别×场地 生成场次,返回组合键 -> 场次 的映射 private async Task> EnsureSessionsAsync( OrderBookDraft draft, long eventId, Dictionary projectContext, Dictionary venueContext, long userId, DateTime now) { var combos = draft.Entries .Select(e => (e.ProjectName, e.GroupName, e.VenueName)) .Distinct() .ToList(); var result = new Dictionary(); var sessionSort = 0; foreach (var combo in combos) { var pc = projectContext[combo.ProjectName]; var vc = venueContext[combo.VenueName]; var groupId = pc.GroupMap.GetValueOrDefault(combo.GroupName); var exist = await _sessionRepository.GetFirstAsync(s => !s.DeleteMark && s.EventId == eventId && s.EventProjectId == pc.EventProjectId && s.VenueId == vc.Venue.Id && (groupId == null ? s.EventProjectGroupId == null : s.EventProjectGroupId == groupId)); if (exist != null) { result[SessionKey(combo.ProjectName, combo.GroupName, combo.VenueName)] = exist; continue; } sessionSort++; var session = new EventSession { Id = _idGenerator.NextId(), EventId = eventId, EventProjectId = pc.EventProjectId, EventProjectGroupId = groupId, VenueId = vc.Venue.Id, ScoreRuleId = pc.Project.ScoreRuleId!.Value, SessionNo = $"EP{pc.EventProjectId}-V{vc.Index:00}-{sessionSort:00}", SessionName = $"{combo.ProjectName}-{combo.GroupName}-{combo.VenueName}", SessionStatus = 0, Version = 1, Status = AvailabilityStatus.Enable, CreatedUserId = userId, CreatedTime = now }; await _sessionRepository.InsertAsync(session); result[SessionKey(combo.ProjectName, combo.GroupName, combo.VenueName)] = session; } return result; } // 生成出场记录,返回数量 private async Task CreateEntriesAsync( OrderBookDraft draft, Dictionary projectContext, Dictionary venueContext, Dictionary sessionContext, long userId, DateTime now) { // 统计每个场次已有的最大出场顺序,追加模式在此基础上递增 var sessionIds = sessionContext.Values.Select(s => s.Id).ToList(); var existOrders = await _entryRepository.AsQueryable() .Where(e => !e.DeleteMark && sessionIds.Contains(e.SessionId)) .GroupBy(e => e.SessionId) .Select(e => new { SessionId = e.SessionId, MaxOrder = SqlFunc.AggregateMax(e.PlayOrder) }) .ToListAsync(); var orderCounter = existOrders.ToDictionary(x => x.SessionId, x => x.MaxOrder); var usedEntryNos = (await _entryRepository.AsQueryable() .Where(e => !e.DeleteMark && sessionIds.Contains(e.SessionId)) .Select(e => new { e.SessionId, e.EntryNo }).ToListAsync()) .GroupBy(x => x.SessionId) .ToDictionary(g => g.Key, g => g.Select(x => x.EntryNo).ToHashSet()); var toInsert = new List(); foreach (var d in draft.Entries) { var session = sessionContext[SessionKey(d.ProjectName, d.GroupName, d.VenueName)]; var pc = projectContext[d.ProjectName]; var vc = venueContext[d.VenueName]; orderCounter.TryGetValue(session.Id, out var order); order++; orderCounter[session.Id] = order; var entryNo = !string.IsNullOrWhiteSpace(d.EntryNo) ? d.EntryNo! : $"{pc.Project.SortIndex:000}{vc.Index:00}{order:000}"; if (!usedEntryNos.TryGetValue(session.Id, out var noSet)) { noSet = new HashSet(); usedEntryNos[session.Id] = noSet; } // 自动编号撞号时追加顺序位,保证唯一 while (!noSet.Add(entryNo)) entryNo = $"{entryNo}-{order}"; toInsert.Add(new EventSessionEntry { Id = _idGenerator.NextId(), SessionId = session.Id, ParticipationRecordId = null, RegistrationItemId = null, SourceType = 1, // 秩序册导入 EntryNo = entryNo, PlayOrder = order, EntryStatus = 0, IsRematch = false, IsVoided = false, ParticipantName = d.ParticipantName, TeamName = null, GroupName = d.GroupName, Version = 1, Status = AvailabilityStatus.Enable, CreatedUserId = userId, CreatedTime = now }); } if (toInsert.Any()) await _entryRepository.InsertRangeAsync(toInsert); return toInsert.Count; } // 裁判档案 / 赛事裁判关系 / 可选打分账号,返回纳入的裁判数量 private async Task EnsureRefereesAsync( OrderBookDraft draft, long eventId, OrderBookConfirmInput input, long userId, DateTime now) { if (!draft.Referees.Any()) return 0; string? password = null; if (input.GenerateJudgeAccount) { password = input.DefaultPassword?.Trim(); if (string.IsNullOrWhiteSpace(password)) throw Oops.Bah("生成裁判账号时必须设置初始密码"); if (password.Length is < 6 or > 32) throw Oops.Bah("裁判初始密码长度必须为6-32位"); } var mobiles = draft.Referees.Select(r => r.Mobile).Distinct().ToList(); var existReferees = await _refereeRepository.AsQueryable().Where(r => !r.DeleteMark && mobiles.Contains(r.Mobile)).ToListAsync(); var existEventReferees = await _eventRefereeRepository.AsQueryable() .Where(er => !er.DeleteMark && er.EventId == eventId).ToListAsync(); var existAppUsers = await _appUserRepository.AsQueryable().Where(u => !u.DeleteMark && mobiles.Contains(u.Mobile)).ToListAsync(); var newEventReferees = new List(); var count = 0; foreach (var d in draft.Referees) { var importedRoles = ParseRefereeRoles(d.RoleText); var referee = existReferees.FirstOrDefault(r => r.Mobile == d.Mobile); if (referee == null) { referee = new Core.Entities.Referee { Id = _idGenerator.NextId(), Name = d.Name, Mobile = d.Mobile, AppUserId = null, ProvinceCode = string.Empty, ProvinceName = string.Empty, CityCode = string.Empty, CityName = string.Empty, DistrictCode = string.Empty, DistrictName = string.Empty, Level = ParseRefereeLevel(d.LevelText), CertificateUrl = string.Empty, Roles = importedRoles, AuditStatus = AuditStatus.Pass, AuditUserId = userId, AuditTime = now, Status = AvailabilityStatus.Enable, CreatedUserId = userId, CreatedTime = now }; await _refereeRepository.InsertAsync(referee); existReferees.Add(referee); } else if (importedRoles != RefereeRole.None && !referee.Roles.HasFlag(importedRoles)) { referee.Roles |= importedRoles; referee.UpdatedUserId = userId; referee.UpdatedTime = now; await _refereeRepository.UpdateAsync(referee); } if (input.GenerateJudgeAccount) { var appUser = existAppUsers.FirstOrDefault(u => u.Mobile == d.Mobile); if (appUser == null) { var appUserId = _idGenerator.NextId(); appUser = new Core.Entities.AppUser { Id = appUserId, PersonType = PersonType.User, MiniProgramRole = MiniProgramRole.Referee, Name = d.Name, Mobile = d.Mobile, Gender = Gender.Unknown, Password = MD5Encryption.Encrypt($"{_idGenerator.Encode(appUserId)}{password!}"), AuditStatus = AuditStatus.Pass, AuditUserId = userId, AuditTime = now, Status = AvailabilityStatus.Enable, CreatedUserId = userId, CreatedTime = now }; await _appUserRepository.InsertAsync(appUser); existAppUsers.Add(appUser); referee.AppUserId = appUser.Id; referee.UpdatedUserId = userId; referee.UpdatedTime = now; await _refereeRepository.UpdateAsync(referee); } else if (referee.AppUserId == null) { referee.AppUserId = appUser.Id; referee.UpdatedUserId = userId; referee.UpdatedTime = now; await _refereeRepository.UpdateAsync(referee); } } if (existEventReferees.All(er => er.RefereeId != referee.Id)) { newEventReferees.Add(new EventReferee { Id = _idGenerator.NextId(), EventId = eventId, RefereeId = referee.Id, RefereeApplicationId = null, SourceType = 3, // 秩序册导入 Status = AvailabilityStatus.Enable, CreatedUserId = userId, CreatedTime = now }); } count++; } if (newEventReferees.Any()) await _eventRefereeRepository.InsertRangeAsync(newEventReferees); return count; } #endregion #region 辅助 private static string BatchKey(long batchId) => $"orderbook:batch:{batchId}"; private static string CurrentKey(long eventId, long userId) => $"orderbook:current:{eventId}:{userId}"; private static string ConfirmedKey(long batchId) => $"orderbook:confirmed:{batchId}"; private static string SessionKey(string project, string group, string venue) => $"{project}|{group}|{venue}"; private static OrderBookParseIssue Issue(int row, string column, string message, string level = "error") => new() { Row = row, Column = column, Message = message, Level = level }; private static string? Trim(string? value) => string.IsNullOrWhiteSpace(value) ? null : value.Trim(); private static bool IsAllBlank(params string?[] values) => values.All(string.IsNullOrWhiteSpace); private static RefereeLevel ParseRefereeLevel(string? text) { return text switch { "国际级" => RefereeLevel.International, "国家级" => RefereeLevel.National, "一级" => RefereeLevel.LevelOne, "二级" => RefereeLevel.LevelTwo, "三级" => RefereeLevel.LevelThree, _ => RefereeLevel.LevelThree }; } private static RefereeRole ParseRefereeRoles(string? text) { if (string.IsNullOrWhiteSpace(text)) return RefereeRole.None; var roles = RefereeRole.None; if (text.Contains("主裁", StringComparison.OrdinalIgnoreCase)) roles |= RefereeRole.Chief; if (text.Contains("表现", StringComparison.OrdinalIgnoreCase)) roles |= RefereeRole.Performance; if (text.Contains("难度", StringComparison.OrdinalIgnoreCase)) roles |= RefereeRole.Difficulty; if (text.Contains("创意", StringComparison.OrdinalIgnoreCase)) roles |= RefereeRole.Choreography; if (text.Contains("完成质量", StringComparison.OrdinalIgnoreCase)) roles |= RefereeRole.CompletionQuality; return roles; } private sealed class OrderBookProjectContext { public Core.Entities.Project Project { get; set; } = null!; public long EventProjectId { get; set; } public Dictionary GroupMap { get; set; } = new(); } private sealed class OrderBookVenueContext { public EventVenue Venue { get; set; } = null!; public int Index { get; set; } } #endregion } /// /// 秩序册导入模板导出器:出场秩序 + 裁判名单 两个工作表 /// internal sealed class OrderBookTemplateExporter : ExcelExportBase { public override Task ExportAsync() { var headerColor = Color.FromArgb(217, 225, 242); #region 出场秩序 CreateWorksheet("出场秩序"); var entryHeaders = new[] { "序号", "场地名称", "项目名称", "组别名称", "出场编号", "姓名/队名", "单位", "联系电话", "证件号", "备注" }; SetMergeTitle("秩序册 - 出场秩序(带*为必填,组别留空默认归入公开组,出场编号留空自动生成)", 1, 1, 1, entryHeaders.Length, headerColor, 12); SetHeaders(entryHeaders, 2, headerColor); var sample = new[] { "1", "1号场地", "男子单人", "公开组", "", "张三", "某某单位", "13800000000", "", "" }; for (var c = 0; c < sample.Length; c++) Worksheet.Cells[3, c + 1].Value = sample[c]; SetDataRowStyle(3, entryHeaders.Length); SetColumnWidths(new double[] { 6, 14, 16, 12, 12, 14, 20, 14, 20, 16 }); FreezePanes(3, 1); #endregion #region 裁判名单 CreateWorksheet("裁判名单"); var refereeHeaders = new[] { "序号", "姓名", "手机号", "裁判角色", "裁判等级", "负责场地", "负责项目" }; SetMergeTitle("秩序册 - 裁判名单(姓名、手机号必填,无可不填本工作表)", 1, 1, 1, refereeHeaders.Length, headerColor, 12); SetHeaders(refereeHeaders, 2, headerColor); var refereeSample = new[] { "1", "李四", "13900000000", "主裁", "一级", "1号场地", "男子单人" }; for (var c = 0; c < refereeSample.Length; c++) Worksheet.Cells[3, c + 1].Value = refereeSample[c]; SetDataRowStyle(3, refereeHeaders.Length); SetColumnWidths(new double[] { 6, 12, 14, 14, 12, 14, 16 }); FreezePanes(3, 1); #endregion return GetAsByteArrayAsync(); } }