diff --git a/YD_Event.Application/Client/RefereeController.cs b/YD_Event.Application/Client/RefereeController.cs index 384770b..95bf0f8 100644 --- a/YD_Event.Application/Client/RefereeController.cs +++ b/YD_Event.Application/Client/RefereeController.cs @@ -13,18 +13,27 @@ public class RefereeController : IDynamicApiController private readonly ISqlSugarRepository _userRepository; private readonly ISqlSugarRepository _eventRepository; private readonly ISqlSugarRepository _applicationRepository; + private readonly ISqlSugarRepository _refereeRepository; + private readonly ISqlSugarRepository _eventRefereeRepository; private readonly AuthManager _authManager; + private readonly IIdGenerator _idGenerator; public RefereeController( ISqlSugarRepository userRepository, ISqlSugarRepository eventRepository, ISqlSugarRepository applicationRepository, - AuthManager authManager) + ISqlSugarRepository refereeRepository, + ISqlSugarRepository eventRefereeRepository, + AuthManager authManager, + IIdGenerator idGenerator) { _userRepository = userRepository; _eventRepository = eventRepository; _applicationRepository = applicationRepository; + _refereeRepository = refereeRepository; + _eventRefereeRepository = eventRefereeRepository; _authManager = authManager; + _idGenerator = idGenerator; } [HttpGet("applications")] @@ -80,15 +89,36 @@ public class RefereeController : IDynamicApiController x.EventId == input.EventId && x.RefereeUserId == userId && x.ApplicationStatus != 4); if (exists) throw Oops.Bah("已报名执裁当前赛事"); - await _applicationRepository.InsertAsync(new RefereeApplication + var referee = await _refereeRepository.GetFirstAsync(x => x.AppUserId == userId); + if (referee == null) throw Oops.Bah("裁判档案不存在,请先完善裁判资料"); + + var application = new RefereeApplication { + Id = _idGenerator.NextId(), EventId = input.EventId, RefereeUserId = userId, - ApplicationStatus = 1, + // 小程序报名无需后台审批,提交成功即成为赛事裁判。 + ApplicationStatus = 2, Status = AvailabilityStatus.Enable, CreatedUserId = userId, CreatedTime = DateTime.Now - }); + }; + await _applicationRepository.InsertAsync(application); + + if (!await _eventRefereeRepository.IsAnyAsync(x => x.EventId == input.EventId && x.RefereeId == referee.Id)) + { + await _eventRefereeRepository.InsertAsync(new EventReferee + { + Id = _idGenerator.NextId(), + EventId = input.EventId, + RefereeId = referee.Id, + RefereeApplicationId = application.Id, + SourceType = 2, + Status = AvailabilityStatus.Enable, + CreatedUserId = userId, + CreatedTime = DateTime.Now + }); + } return new { success = true }; } @@ -104,9 +134,9 @@ public class RefereeController : IDynamicApiController private static string ApplicationStatusText(int status) => status switch { - 2 => "已确认执裁", + 2 => "报名成功", 3 => "报名未通过", 4 => "已取消", - _ => "已报名执裁" + _ => "待审批" }; } diff --git a/YD_Event.Application/Event/Dtos/JudgeAssignmentDtos.cs b/YD_Event.Application/Event/Dtos/JudgeAssignmentDtos.cs index fe5c182..e901664 100644 --- a/YD_Event.Application/Event/Dtos/JudgeAssignmentDtos.cs +++ b/YD_Event.Application/Event/Dtos/JudgeAssignmentDtos.cs @@ -12,6 +12,16 @@ public class RefereeApplicationOutput public int Roles { get; set; } public List RoleNames { get; set; } = new(); public DateTime CreatedTime { get; set; } + public int ApplicationStatus { get; set; } + public string ApplicationStatusName { get; set; } = null!; + public string? Remark { get; set; } +} + +public class EventRefereeOutput : RefereeApplicationOutput +{ + public long EventRefereeId { get; set; } + public int SourceType { get; set; } + public string SourceTypeName { get; set; } = null!; } public class JudgeAssignmentBoardOutput @@ -41,6 +51,7 @@ public class JudgeAssignmentSessionOutput public class JudgeAssignmentItemOutput { + public long EventRefereeId { get; set; } public string JudgeRole { get; set; } = null!; public string JudgeRoleName { get; set; } = null!; public long RefereeId { get; set; } @@ -58,6 +69,7 @@ public class SaveJudgeAssignmentsInput public class SaveJudgeAssignmentItem { public string JudgeRole { get; set; } = null!; + public long EventRefereeId { get; set; } public long RefereeId { get; set; } } diff --git a/YD_Event.Application/Event/JudgeAssignmentService.cs b/YD_Event.Application/Event/JudgeAssignmentService.cs index 828b7e0..e3f33d7 100644 --- a/YD_Event.Application/Event/JudgeAssignmentService.cs +++ b/YD_Event.Application/Event/JudgeAssignmentService.cs @@ -6,9 +6,8 @@ namespace YD_Event.Application.Event; /// 赛事裁判报名与分配。 public class JudgeAssignmentService : IDynamicApiController, ITransient { - private readonly ISqlSugarRepository _applicationRepository; + private readonly ISqlSugarRepository _eventRefereeRepository; private readonly ISqlSugarRepository _refereeRepository; - private readonly ISqlSugarRepository _appUserRepository; private readonly ISqlSugarRepository _sessionRepository; private readonly ISqlSugarRepository _venueRepository; private readonly ISqlSugarRepository _eventProjectRepository; @@ -19,48 +18,33 @@ public class JudgeAssignmentService : IDynamicApiController, ITransient private readonly IIdGenerator _idGenerator; private readonly AuthManager _authManager; - public JudgeAssignmentService(ISqlSugarRepository applicationRepository, + public JudgeAssignmentService(ISqlSugarRepository eventRefereeRepository, ISqlSugarRepository refereeRepository, - ISqlSugarRepository appUserRepository, ISqlSugarRepository sessionRepository, ISqlSugarRepository venueRepository, ISqlSugarRepository eventProjectRepository, ISqlSugarRepository projectRepository, ISqlSugarRepository ruleRepository, ISqlSugarRepository assignmentRepository, ISqlSugarRepository eventRepository, IIdGenerator idGenerator, AuthManager authManager) { - _applicationRepository = applicationRepository; _refereeRepository = refereeRepository; _appUserRepository = appUserRepository; + _eventRefereeRepository = eventRefereeRepository; + _refereeRepository = refereeRepository; _sessionRepository = sessionRepository; _venueRepository = venueRepository; _eventProjectRepository = eventProjectRepository; _projectRepository = projectRepository; _ruleRepository = ruleRepository; _assignmentRepository = assignmentRepository; _eventRepository = eventRepository; _idGenerator = idGenerator; _authManager = authManager; } - [HttpGet("applications/{eventId}"), DisplayName("赛事裁判报名列表")] - public async Task> GetApplications(long eventId) + [HttpGet("event-referees/{eventId}"), DisplayName("获取赛事裁判列表")] + public async Task> GetEventReferees(long eventId) { - var applications = await _applicationRepository.AsQueryable() - .Where(x => x.EventId == eventId && (x.ApplicationStatus == 1 || x.ApplicationStatus == 2)) + var relations = await _eventRefereeRepository.AsQueryable() + // 赛事裁判目前没有“禁用”业务,是否有效统一由软删除标记控制。 + // 同时兼容早期迁移中被错误写成 Status=1 的存量记录。 + .Where(x => x.EventId == eventId) .OrderByDescending(x => x.CreatedTime).ToListAsync(); - var users = await _appUserRepository.AsQueryable().Where(x => applications.Select(a => a.RefereeUserId).Contains(x.Id)).ToListAsync(); - var mobiles = users.Select(x => x.Mobile).Distinct().ToList(); - var referees = await _refereeRepository.AsQueryable().Where(x => mobiles.Contains(x.Mobile) && x.AuditStatus == AuditStatus.Pass).ToListAsync(); - return (from app in applications join user in users on app.RefereeUserId equals user.Id join referee in referees on user.Mobile equals referee.Mobile - select new RefereeApplicationOutput { ApplicationId = app.Id, RefereeId = referee.Id, Name = referee.Name, - RegionName = $"{referee.ProvinceName}/{referee.CityName}/{referee.DistrictName}", LevelName = referee.Level.Description(), - CertificateUrl = referee.CertificateUrl, Mobile = referee.Mobile, Roles = (int)referee.Roles, - RoleNames = Enum.GetValues().Where(r => r != RefereeRole.None && referee.Roles.HasFlag(r)).Select(r => r.Description()).ToList(), CreatedTime = app.CreatedTime }).ToList(); - } - - [HttpDelete("applications"), DisplayName("移除裁判报名"), UnitOfWork] - public async Task DeleteApplication([FromBody] KeyDto input) - { - var app = await _applicationRepository.GetByIdAsync(input.Id); - if (app == null) throw Oops.Bah("裁判报名不存在"); - var user = await _appUserRepository.GetByIdAsync(app.RefereeUserId); - var referee = user == null ? null : await _refereeRepository.GetFirstAsync(x => x.Mobile == user.Mobile && x.AuditStatus == AuditStatus.Pass); - if (referee != null && await _assignmentRepository.IsAnyAsync(x => x.EventId == app.EventId && x.RefereeId == referee.Id && x.AssignmentStatus < 9)) - throw Oops.Bah("该裁判已存在分配记录,不能移除报名"); - app.ApplicationStatus = 4; app.UpdatedUserId = _authManager.UserId; app.UpdatedTime = DateTime.Now; - await _applicationRepository.UpdateAsync(app); + var referees = await _refereeRepository.AsQueryable() + .Where(x => relations.Select(r => r.RefereeId).Contains(x.Id)).ToListAsync(); + return relations.Select(relation => ToEventRefereeOutput(relation, + referees.FirstOrDefault(x => x.Id == relation.RefereeId))).ToList(); } [HttpGet("board/{eventId}"), DisplayName("获取裁判分配看板")] @@ -82,7 +66,7 @@ public class JudgeAssignmentService : IDynamicApiController, ITransient var group = new JudgeAssignmentProjectOutput { EventProjectId = ep.Id, ProjectName = project.Name, RuleName = rule?.RuleName ?? "未配置计分规则", JudgeRoleSchemaJson = rule?.JudgeRoleSchemaJson ?? "[]" }; foreach (var session in sessions.Where(x => x.EventProjectId == ep.Id).OrderBy(x => venueMap.GetValueOrDefault(x.VenueId)?.Sort)) { - var items = assignments.Where(x => x.SessionId == session.Id).Select(x => { var referee = referees.FirstOrDefault(r => r.Id == x.RefereeId); return new JudgeAssignmentItemOutput { JudgeRole = x.JudgeRole, JudgeRoleName = x.JudgeRole, RefereeId = x.RefereeId, RefereeName = referee?.Name ?? "-", Mobile = referee?.Mobile ?? "" }; }).ToList(); + var items = assignments.Where(x => x.SessionId == session.Id).Select(x => { var referee = referees.FirstOrDefault(r => r.Id == x.RefereeId); return new JudgeAssignmentItemOutput { EventRefereeId = x.EventRefereeId, JudgeRole = x.JudgeRole, JudgeRoleName = x.JudgeRole, RefereeId = x.RefereeId, RefereeName = referee?.Name ?? "-", Mobile = referee?.Mobile ?? "" }; }).ToList(); group.Sessions.Add(new JudgeAssignmentSessionOutput { SessionId = session.Id, SessionName = session.SessionName ?? session.SessionNo, VenueName = venueMap.GetValueOrDefault(session.VenueId)?.VenueName ?? "-", Assigned = items.Count > 0, Assignments = items }); } @@ -92,10 +76,10 @@ public class JudgeAssignmentService : IDynamicApiController, ITransient } [HttpGet("candidates/{eventId}/{sessionId}"), DisplayName("获取可分配裁判")] - public async Task> GetCandidates(long eventId, long sessionId) + public async Task> GetCandidates(long eventId, long sessionId) { if (!await _sessionRepository.IsAnyAsync(x => x.Id == sessionId && x.EventId == eventId)) throw Oops.Bah("场次不存在"); - return await GetApplications(eventId); + return await GetEventReferees(eventId); } [HttpPost("save"), DisplayName("保存裁判分配"), UnitOfWork] @@ -105,25 +89,36 @@ public class JudgeAssignmentService : IDynamicApiController, ITransient if (session == null || session.SessionStatus == 90) throw Oops.Bah("场次不存在或已取消"); if (await _assignmentRepository.IsAnyAsync(x => x.SessionId == input.SessionId && x.AssignmentStatus == 1)) throw Oops.Bah("裁判分配已提交,不能再次修改"); - if (input.Assignments.Any(x => string.IsNullOrWhiteSpace(x.JudgeRole) || x.RefereeId <= 0)) throw Oops.Bah("裁判岗位未完整分配"); + if (input.Assignments.Any(x => string.IsNullOrWhiteSpace(x.JudgeRole) || (x.EventRefereeId <= 0 && x.RefereeId <= 0))) + throw Oops.Bah("裁判岗位未完整分配"); var rule = await _ruleRepository.GetByIdAsync(session.ScoreRuleId); var requiredRoles = JArray.Parse(rule?.JudgeRoleSchemaJson ?? "[]").SelectMany(x => x["slots"] ?? new JArray()).Select(x => x["code"]?.ToString()).Where(x => !string.IsNullOrWhiteSpace(x)).ToList(); if (requiredRoles.Count == 0 || requiredRoles.Except(input.Assignments.Select(x => x.JudgeRole)).Any() || input.Assignments.Any(x => !requiredRoles.Contains(x.JudgeRole))) throw Oops.Bah("请完成计分规则要求的全部裁判岗位分配"); if (input.Assignments.GroupBy(x => x.JudgeRole).Any(x => x.Count() > 1)) throw Oops.Bah("同一岗位只能分配一名裁判"); - if (input.Assignments.GroupBy(x => x.RefereeId).Any(x => x.Count() > 1)) throw Oops.Bah("同一场次一名裁判只能分配一个岗位"); - var candidates = await GetApplications(input.EventId); + if (input.Assignments.GroupBy(x => x.EventRefereeId > 0 ? x.EventRefereeId : x.RefereeId).Any(x => x.Count() > 1)) + throw Oops.Bah("同一场次一名裁判只能分配一个岗位"); + var candidates = await GetEventReferees(input.EventId); foreach (var item in input.Assignments) { - var candidate = candidates.FirstOrDefault(x => x.RefereeId == item.RefereeId); - if (candidate == null) throw Oops.Bah("存在未报名或未审核通过的裁判"); + var candidate = item.EventRefereeId > 0 + ? candidates.FirstOrDefault(x => x.EventRefereeId == item.EventRefereeId) + : candidates.FirstOrDefault(x => x.RefereeId == item.RefereeId); + if (candidate == null) throw Oops.Bah("存在尚未关联到当前赛事的裁判"); + item.EventRefereeId = candidate.EventRefereeId; + item.RefereeId = candidate.RefereeId; var requiredFlag = RoleFlag(item.JudgeRole); if (requiredFlag != 0 && (candidate.Roles & requiredFlag) == 0) throw Oops.Bah($"裁判{candidate.Name}不具备岗位{item.JudgeRole}资格"); } await _assignmentRepository.SoftDeleteAsync(x => x.SessionId == input.SessionId && x.AssignmentStatus == 0); var now = DateTime.Now; + var refereeIds = input.Assignments.Select(x => x.RefereeId).Distinct().ToList(); + var refereeUsers = (await _refereeRepository.AsQueryable() + .Where(x => refereeIds.Contains(x.Id)).ToListAsync()) + .ToDictionary(x => x.Id, x => x.AppUserId); await _assignmentRepository.InsertRangeAsync(input.Assignments.Select(x => new EventJudgeAssignment { Id = _idGenerator.NextId(), EventId = input.EventId, - SessionId = input.SessionId, RefereeId = x.RefereeId, JudgeRole = x.JudgeRole, AssignmentStatus = 0, + SessionId = input.SessionId, EventRefereeId = x.EventRefereeId, RefereeId = x.RefereeId, + JudgeUserId = refereeUsers.GetValueOrDefault(x.RefereeId), JudgeRole = x.JudgeRole, AssignmentStatus = 0, Status = AvailabilityStatus.Enable, CreatedUserId = _authManager.UserId, CreatedTime = now }).ToList()); } @@ -139,13 +134,6 @@ public class JudgeAssignmentService : IDynamicApiController, ITransient sessions.ForEach(x => { x.SessionStatus = 10; x.Version++; x.UpdatedUserId = _authManager.UserId; x.UpdatedTime = DateTime.Now; }); await _sessionRepository.UpdateRangeAsync(sessions); - var assignedRefereeIds = assignments.Select(x => x.RefereeId).Distinct().ToList(); - var assignedMobiles = await _refereeRepository.AsQueryable().Where(x => assignedRefereeIds.Contains(x.Id)).Select(x => x.Mobile).ToListAsync(); - var assignedUserIds = await _appUserRepository.AsQueryable().Where(x => assignedMobiles.Contains(x.Mobile)).Select(x => x.Id).ToListAsync(); - var applications = await _applicationRepository.AsQueryable() - .Where(x => x.EventId == input.EventId && assignedUserIds.Contains(x.RefereeUserId) && x.ApplicationStatus == 1).ToListAsync(); - applications.ForEach(x => { x.ApplicationStatus = 2; x.UpdatedUserId = _authManager.UserId; x.UpdatedTime = DateTime.Now; }); - if (applications.Count > 0) await _applicationRepository.UpdateRangeAsync(applications); } [HttpPost("registration-code/{eventId}"), DisplayName("生成裁判报名码")] @@ -162,6 +150,32 @@ public class JudgeAssignmentService : IDynamicApiController, ITransient return entity.RefereeRegistrationCode; } + private static EventRefereeOutput ToEventRefereeOutput(EventReferee? relation, + YD_Event.Core.Entities.Referee? referee) + { + var roles = referee?.Roles ?? RefereeRole.None; + return new EventRefereeOutput + { + EventRefereeId = relation?.Id ?? 0, + ApplicationId = relation?.RefereeApplicationId ?? 0, + RefereeId = referee?.Id ?? relation?.RefereeId ?? 0, + Name = referee?.Name ?? "-", + RegionName = referee == null ? "-" : $"{referee.ProvinceName}/{referee.CityName}/{referee.DistrictName}", + LevelName = referee?.Level.Description() ?? "-", + CertificateUrl = referee?.CertificateUrl ?? string.Empty, + Mobile = referee?.Mobile ?? string.Empty, + Roles = (int)roles, + RoleNames = Enum.GetValues() + .Where(role => role != RefereeRole.None && roles.HasFlag(role)) + .Select(role => role.Description()).ToList(), + CreatedTime = relation?.CreatedTime ?? referee?.CreatedTime ?? DateTime.MinValue, + ApplicationStatus = 2, + ApplicationStatusName = "已通过", + SourceType = relation?.SourceType ?? 1, + SourceTypeName = relation?.SourceType == 2 ? "小程序报名" : "后台添加" + }; + } + private static int RoleFlag(string role) => role switch { "T" => (int)RefereeRole.Chief, diff --git a/YD_Event.Application/Referee/Dtos/RefereeDtos.cs b/YD_Event.Application/Referee/Dtos/RefereeDtos.cs index 2554000..bd581c4 100644 --- a/YD_Event.Application/Referee/Dtos/RefereeDtos.cs +++ b/YD_Event.Application/Referee/Dtos/RefereeDtos.cs @@ -53,6 +53,7 @@ public class UpdateRefereeInput : SaveRefereeInput public class RefereeOutput { public long Id { get; set; } + public long? AppUserId { get; set; } public string Name { get; set; } = null!; public string Mobile { get; set; } = null!; public string ProvinceCode { get; set; } = null!; diff --git a/YD_Event.Application/Referee/RefereeService.cs b/YD_Event.Application/Referee/RefereeService.cs index 2bc4a26..e8e3745 100644 --- a/YD_Event.Application/Referee/RefereeService.cs +++ b/YD_Event.Application/Referee/RefereeService.cs @@ -27,6 +27,7 @@ public class RefereeService : BaseService, ITran } [DisplayName("裁判分页查询")] + [HttpGet("page")] public async Task> Page([FromQuery] RefereeQueryInput input) { var query = _repository.AsQueryable() @@ -48,13 +49,17 @@ public class RefereeService : BaseService, ITran } [DisplayName("新增裁判")] + [HttpPost("add")] public async Task Add([FromBody] SaveRefereeInput input) { await EnsureMobileAvailable(input.Mobile, null); await VerifyCode(input.Mobile, input.VerificationCode); + var appUser = await FindAppUserAsync(input.Mobile); var entity = input.Adapt(); entity.Id = _idGenerator.NextId(); - entity.AuditStatus = AuditStatus.PendingAudit; + entity.AppUserId = appUser?.Id; + // 后台管理员直接维护的裁判档案无需再次审批。 + entity.AuditStatus = AuditStatus.Pass; entity.Status = AvailabilityStatus.Enable; entity.CreatedUserId = _authManager.UserId; entity.CreatedTime = DateTime.Now; @@ -63,6 +68,7 @@ public class RefereeService : BaseService, ITran } [DisplayName("修改裁判")] + [HttpPut("update")] public async Task Update([FromBody] UpdateRefereeInput input) { var entity = await _repository.GetByIdAsync(input.Id); @@ -73,7 +79,9 @@ public class RefereeService : BaseService, ITran await VerifyCode(input.Mobile, input.VerificationCode); await _cache.RemoveAsync($"sms:code:{input.Mobile}"); } + var appUser = await FindAppUserAsync(input.Mobile); input.Adapt(entity); + entity.AppUserId = appUser?.Id; entity.UpdatedUserId = _authManager.UserId; entity.UpdatedTime = DateTime.Now; await _repository.UpdateAsync(entity); @@ -95,10 +103,21 @@ public class RefereeService : BaseService, ITran private async Task EnsureMobileAvailable(string mobile, long? exceptId) { - if (await _repository.IsAnyAsync(x => x.Mobile == mobile && (!exceptId.HasValue || x.Id != exceptId.Value))) + // 不要把 Nullable.HasValue 分支写进 SqlSugar 表达式;空值时会生成 “<>” 附近的非法 SQL。 + var refereeQuery = _repository.AsQueryable().Where(x => x.Mobile == mobile); + if (exceptId.HasValue) + refereeQuery = refereeQuery.Where(x => x.Id != exceptId.Value); + + if (await refereeQuery.AnyAsync()) throw Oops.Bah("该手机号已存在于裁判库"); - if (await _userRepository.IsAnyAsync(x => x.Mobile == mobile)) - throw Oops.Bah("该手机号已绑定现有账号"); + } + + /// + /// 手机号已注册小程序账号时自动建立关联,未注册时允许裁判档案独立存在。 + /// + private Task FindAppUserAsync(string mobile) + { + return _userRepository.AsQueryable().Where(x => x.Mobile == mobile).FirstAsync(); } private async Task VerifyCode(string mobile, string? code) @@ -110,7 +129,7 @@ public class RefereeService : BaseService, ITran private static RefereeOutput ToOutput(YD_Event.Core.Entities.Referee x) => new() { - Id = x.Id, Name = x.Name, Mobile = x.Mobile, + Id = x.Id, AppUserId = x.AppUserId, Name = x.Name, Mobile = x.Mobile, ProvinceCode = x.ProvinceCode, ProvinceName = x.ProvinceName, CityCode = x.CityCode, CityName = x.CityName, DistrictCode = x.DistrictCode, DistrictName = x.DistrictName, diff --git a/YD_Event.Application/Unit/Dtos/UnitAccountDtos.cs b/YD_Event.Application/Unit/Dtos/UnitAccountDtos.cs index 203a269..e31e424 100644 --- a/YD_Event.Application/Unit/Dtos/UnitAccountDtos.cs +++ b/YD_Event.Application/Unit/Dtos/UnitAccountDtos.cs @@ -6,7 +6,7 @@ namespace YD_Event.Application.Unit.Dtos; public class UnitAccountQueryInput : Pagination { /// - /// 所属平台(0-报名平台 1-打分系统) + /// 所属平台(1-报名平台 2-打分系统) /// [Required(ErrorMessage = "请选择所属平台")] public UnitAccountPlatform Platform { get; set; } @@ -38,7 +38,7 @@ public class UnitAccountQueryInput : Pagination public class AddUnitAccountInput { /// - /// 所属平台(0-报名平台 1-打分系统) + /// 所属平台(1-报名平台 2-打分系统) /// [Required(ErrorMessage = "请选择所属平台")] public UnitAccountPlatform Platform { get; set; } @@ -154,7 +154,7 @@ public class UnitAccountOutput /// /// 所属平台 /// - public UnitAccountPlatform? Platform { get; set; } + public UnitAccountPlatform Platform { get; set; } /// /// 状态(0-启用 1-禁用) diff --git a/YD_Event.Application/Unit/UnitAccountService.cs b/YD_Event.Application/Unit/UnitAccountService.cs index 3a57874..5ecb090 100644 --- a/YD_Event.Application/Unit/UnitAccountService.cs +++ b/YD_Event.Application/Unit/UnitAccountService.cs @@ -35,6 +35,7 @@ public class UnitAccountService : BaseService, ITransient [HttpGet("page")] public async Task> Page([FromQuery] UnitAccountQueryInput input) { + EnsureUnitPlatform(input.Platform); return await _repository.AsQueryable() .Where(x => x.UnitPlatform == input.Platform) .WhereIF(!string.IsNullOrWhiteSpace(input.UnitName), x => x.UnitName.Contains(input.UnitName)) @@ -85,6 +86,7 @@ public class UnitAccountService : BaseService, ITransient [UnitOfWork] public async Task Add(AddUnitAccountInput input) { + EnsureUnitPlatform(input.Platform); // 账号是全局登录标识,需在整张 SysUser 表内查重 if (await _repository.IsAnyAsync(x => x.Account == input.Account)) { @@ -191,10 +193,16 @@ public class UnitAccountService : BaseService, ITransient { var user = await _repository.GetByIdAsync(id); if (user == null) throw Oops.Bah("单位账号不存在"); - if (user.UnitPlatform == null) throw Oops.Bah("该账号不是单位账号,请在系统用户中管理"); + if (user.UnitPlatform == UnitAccountPlatform.EventSystem) throw Oops.Bah("该账号不是单位账号,请在系统用户中管理"); return user; } + private static void EnsureUnitPlatform(UnitAccountPlatform platform) + { + if (platform is not UnitAccountPlatform.Registration and not UnitAccountPlatform.Scoring) + throw Oops.Bah("所属平台只能选择报名平台或打分系统"); + } + /// /// 平台对应的角色 /// diff --git a/YD_Event.Application/YD_Event.Application.xml b/YD_Event.Application/YD_Event.Application.xml index 2a744c8..b9eb8bd 100644 --- a/YD_Event.Application/YD_Event.Application.xml +++ b/YD_Event.Application/YD_Event.Application.xml @@ -12476,6 +12476,11 @@ 裁判库管理。 + + + 手机号已注册小程序账号时自动建立关联,未注册时允许裁判档案独立存在。 + + 退款配置DTO @@ -13135,7 +13140,7 @@ - 所属平台(0-报名平台 1-打分系统) + 所属平台(1-报名平台 2-打分系统) @@ -13165,7 +13170,7 @@ - 所属平台(0-报名平台 1-打分系统) + 所属平台(1-报名平台 2-打分系统) diff --git a/YD_Event.Core/Entities/CheckinRecord.cs b/YD_Event.Core/Entities/CheckinRecord.cs index 859713c..23b7e20 100644 --- a/YD_Event.Core/Entities/CheckinRecord.cs +++ b/YD_Event.Core/Entities/CheckinRecord.cs @@ -1,7 +1,7 @@ namespace YD_Event.Core.Entities; /// -/// 检录人员状态记录表(每个秩序册列在单次检录中的检录状态) +/// 检录人员状态记录表 /// [SugarTable("CheckinRecord")] public class CheckinRecord : Entity, ISoftDelete, ICreatedUserId, ICreatedTime, IUpdatedUserId, IUpdatedTime diff --git a/YD_Event.Core/Entities/CheckinTask.cs b/YD_Event.Core/Entities/CheckinTask.cs index 3bac70c..7ef15be 100644 --- a/YD_Event.Core/Entities/CheckinTask.cs +++ b/YD_Event.Core/Entities/CheckinTask.cs @@ -4,8 +4,6 @@ namespace YD_Event.Core.Entities; /// /// 赛事检录任务表 -/// 一个检录员对应一条任务,记录总检录次数以及当前检录轮次、组别; -/// Current 系列字段为现场进度快照,切换检次或重新绑定检录员时会被重置 /// [SugarTable("CheckinTask")] public class CheckinTask : Entity, ISoftDelete, ICreatedUserId, ICreatedTime, IUpdatedUserId, IUpdatedTime, IAvailability diff --git a/YD_Event.Core/Entities/EventJudgeAssignment.cs b/YD_Event.Core/Entities/EventJudgeAssignment.cs index 614cc55..8a8bcf5 100644 --- a/YD_Event.Core/Entities/EventJudgeAssignment.cs +++ b/YD_Event.Core/Entities/EventJudgeAssignment.cs @@ -3,7 +3,7 @@ using YD_Event.Core.Enum; namespace YD_Event.Core.Entities; /// -/// 赛事裁判分配表(按场次分配裁判席位) +/// 赛事裁判分配表 /// [SugarTable("EventJudgeAssignment")] public class EventJudgeAssignment : Entity, ISoftDelete, ICreatedUserId, ICreatedTime, IUpdatedUserId, IUpdatedTime, IAvailability @@ -19,7 +19,12 @@ public class EventJudgeAssignment : Entity, ISoftDelete, ICreatedUserId, I public long SessionId { get; set; } /// - /// 关联的裁判档案ID + /// 关联的赛事裁判记录ID + /// + public long EventRefereeId { get; set; } + + /// + /// 关联的裁判档案ID(兼容历史查询的冗余字段) /// public long RefereeId { get; set; } diff --git a/YD_Event.Core/Entities/EventReferee.cs b/YD_Event.Core/Entities/EventReferee.cs new file mode 100644 index 0000000..f4b476f --- /dev/null +++ b/YD_Event.Core/Entities/EventReferee.cs @@ -0,0 +1,63 @@ +using YD_Event.Core.Enum; + +namespace YD_Event.Core.Entities; + +/// +/// 赛事裁判表 +/// +[SugarTable("EventReferee")] +public class EventReferee : Entity, ISoftDelete, ICreatedUserId, ICreatedTime, IUpdatedUserId, IUpdatedTime, IAvailability +{ + /// + /// 关联的赛事ID + /// + public long EventId { get; set; } + + /// + /// 关联的裁判档案ID + /// + public long RefereeId { get; set; } + + /// + /// 关联的裁判报名记录ID(非小程序报名时为空) + /// + [SugarColumn(IsNullable = true)] + public long? RefereeApplicationId { get; set; } + + /// + /// 关联来源:1-后台添加,2-小程序报名 + /// + public int SourceType { get; set; } = 1; + + /// + /// 可用状态 + /// + public AvailabilityStatus Status { get; set; } = AvailabilityStatus.Enable; + + /// + /// 标记删除 + /// + public bool DeleteMark { get; set; } + + /// + /// 创建人 + /// + public long CreatedUserId { get; set; } + + /// + /// 创建时间 + /// + public DateTime CreatedTime { get; set; } + + /// + /// 更新人 + /// + [SugarColumn(IsNullable = true)] + public long? UpdatedUserId { get; set; } + + /// + /// 更新时间 + /// + [SugarColumn(IsNullable = true)] + public DateTime? UpdatedTime { get; set; } +} diff --git a/YD_Event.Core/Entities/EventSession.cs b/YD_Event.Core/Entities/EventSession.cs index bdae054..945e001 100644 --- a/YD_Event.Core/Entities/EventSession.cs +++ b/YD_Event.Core/Entities/EventSession.cs @@ -3,7 +3,7 @@ using YD_Event.Core.Enum; namespace YD_Event.Core.Entities; /// -/// 赛事比赛场次表(编排确认后生成的正式场次) +/// 赛事比赛场次表 /// [SugarTable("EventSession")] public class EventSession : Entity, ISoftDelete, ICreatedUserId, ICreatedTime, IUpdatedUserId, IUpdatedTime, IAvailability diff --git a/YD_Event.Core/Entities/EventSessionEntry.cs b/YD_Event.Core/Entities/EventSessionEntry.cs index 6bc545c..b2f0d2a 100644 --- a/YD_Event.Core/Entities/EventSessionEntry.cs +++ b/YD_Event.Core/Entities/EventSessionEntry.cs @@ -3,7 +3,7 @@ using YD_Event.Core.Enum; namespace YD_Event.Core.Entities; /// -/// 赛事场次出场记录表(场次内每个参赛单元的出场信息) +/// 赛事场次出场记录表 /// [SugarTable("EventSessionEntry")] public class EventSessionEntry : Entity, ISoftDelete, ICreatedUserId, ICreatedTime, IUpdatedUserId, IUpdatedTime, IAvailability diff --git a/YD_Event.Core/Entities/Referee.cs b/YD_Event.Core/Entities/Referee.cs index 89f45c8..8084fec 100644 --- a/YD_Event.Core/Entities/Referee.cs +++ b/YD_Event.Core/Entities/Referee.cs @@ -15,11 +15,17 @@ public class Referee : Entity, ISoftDelete, ICreatedUserId, ICreatedTime, public string Name { get; set; } = null!; /// - /// 联系电话(未删除数据中唯一,作为裁判与小程序账号的关联依据) + /// 联系电话(未删除数据中唯一) /// [SugarColumn(Length = 16)] public string Mobile { get; set; } = null!; + /// + /// 关联的小程序用户ID(未关联小程序账号时为空) + /// + [SugarColumn(IsNullable = true, ColumnDescription = "关联的小程序用户ID")] + public long? AppUserId { get; set; } + /// /// 所属省份编码 /// diff --git a/YD_Event.Core/Entities/RefereeApplication.cs b/YD_Event.Core/Entities/RefereeApplication.cs index 8a75031..7c8c551 100644 --- a/YD_Event.Core/Entities/RefereeApplication.cs +++ b/YD_Event.Core/Entities/RefereeApplication.cs @@ -19,7 +19,7 @@ public class RefereeApplication : Entity, ISoftDelete, ICreatedUserId, ICr public long RefereeUserId { get; set; } /// - /// 报名状态:1-已报名执裁,2-已确认执裁,3-报名未通过,4-已取消 + /// 报名状态:1-处理中(历史数据),2-报名成功,3-报名失败,4-已取消 /// public int ApplicationStatus { get; set; } = 1; diff --git a/YD_Event.Core/Entities/SysUser.cs b/YD_Event.Core/Entities/SysUser.cs index 1b6457e..75bbedf 100644 --- a/YD_Event.Core/Entities/SysUser.cs +++ b/YD_Event.Core/Entities/SysUser.cs @@ -97,15 +97,16 @@ public class SysUser : Entity, ISoftDelete, ICreatedUserId, ICreatedTime, public DateTime? LockExpired { get; set; } /// - /// 单位名称(单位账号专用) + /// 单位名称 /// [SugarColumn(Length = 128)] public string? UnitName { get; set; } /// - /// 所属平台(null 表示普通系统用户,非单位管理创建的账号) + /// 所属平台 /// - public UnitAccountPlatform? UnitPlatform { get; set; } + [SugarColumn(ColumnDescription = "所属平台")] + public UnitAccountPlatform UnitPlatform { get; set; } = UnitAccountPlatform.EventSystem; /// /// 标记删除 @@ -131,4 +132,4 @@ public class SysUser : Entity, ISoftDelete, ICreatedUserId, ICreatedTime, /// 更新时间 /// public DateTime? UpdatedTime { get; set; } -} \ No newline at end of file +} diff --git a/YD_Event.Core/Entities/SysUserRole.cs b/YD_Event.Core/Entities/SysUserRole.cs index 6bae193..222d7e1 100644 --- a/YD_Event.Core/Entities/SysUserRole.cs +++ b/YD_Event.Core/Entities/SysUserRole.cs @@ -1,14 +1,17 @@ namespace YD_Event.Core.Entities; +/// +/// 系统用户角色关联表 +/// public class SysUserRole : Entity, ISoftDelete, ICreatedUserId, ICreatedTime, IUpdatedUserId, IUpdatedTime { /// - /// 用户id + /// 关联的系统用户ID /// public long UserId { get; set; } /// - /// 角色id + /// 关联的系统角色ID /// public long RoleId { get; set; } @@ -36,4 +39,4 @@ public class SysUserRole : Entity, ISoftDelete, ICreatedUserId, ICreatedTi /// 更新时间 /// public DateTime? UpdatedTime { get; set; } -} \ No newline at end of file +} diff --git a/YD_Event.Core/Enum/UnitAccountPlatform.cs b/YD_Event.Core/Enum/UnitAccountPlatform.cs index 71aa182..838c526 100644 --- a/YD_Event.Core/Enum/UnitAccountPlatform.cs +++ b/YD_Event.Core/Enum/UnitAccountPlatform.cs @@ -3,19 +3,25 @@ using System.ComponentModel; namespace YD_Event.Core.Enum; /// -/// 单位账号所属平台 +/// 用户所属平台 /// public enum UnitAccountPlatform { + /// + /// 赛事系统用户 + /// + [Description("赛事系统用户")] + EventSystem = 0, + /// /// 报名平台 /// [Description("报名平台")] - Registration = 0, + Registration = 1, /// /// 打分系统 /// [Description("打分系统")] - Scoring = 1 + Scoring = 2 } diff --git a/YD_Event.Core/YD_Event.Core.xml b/YD_Event.Core/YD_Event.Core.xml index 43806be..8e821c0 100644 --- a/YD_Event.Core/YD_Event.Core.xml +++ b/YD_Event.Core/YD_Event.Core.xml @@ -1000,19 +1000,203 @@ - 单次检录人员状态记录。 + 检录人员状态记录表 + + + + + 关联的检录任务ID + + + + + 检次(与检录任务的 CheckinNo 一致) + + + + + 秩序册列标识 + + + + + 秩序册列的出场顺序 + + + + + 参赛组别ID(不限定组别时为空) + + + + + 关联的报名人员ID + + + + + 执行检录的检录员用户ID + + + + + 是否已检录 + + + + + 检录时间(取消检录时置空) + + + + + 标记删除 + + + + + 创建人 + + + + + 创建时间 + + + + + 更新人 + + + + + 更新时间 - 赛事检录任务,记录总检录次数以及当前检录轮次、组别。 + 赛事检录任务表 + + + + + 关联的赛事ID - 该检录员负责的检次:1-一检,2-二检,3-三检。 + + 该检录员负责的检次:1-一检,2-二检,3-三检 + + + + + 关联的检录员用户ID(尚未绑定检录员时为空) + + + + + 赛事的检录总次数 + + + + + 当前检录轮次(与 CheckinNo 保持一致) + + + + + 当前秩序册列标识 + + + + + 当前秩序册列的出场顺序 + + + + + 当前秩序册列名称 + + + + + 当前检录的赛事项目ID + + + + + 当前秩序册列所在场地 + + + + + 检次名称(由 CheckinNo 转换而来,如一检、二检) + + + + + 当前检录的参赛组别ID(不限定组别时为空) + + + + + 当前检录的参赛组别名称 + + + + + 当前组别的顺序 + + + + + 本次检录的组别总数 + + + + + 下一个待检录组别名称 + + + + + 检录场地 + - 0-未开始,1-进行中,2-已结束。 + + 任务状态:0-未开始,1-进行中,2-已结束 + + + + + 可用状态 + + + + + 标记删除 + + + + + 创建人 + + + + + 创建时间 + + + + + 更新人 + + + + + 更新时间 + @@ -1330,7 +1514,9 @@ - 裁判报名邀请码。 + + 裁判报名邀请码 + @@ -1512,8 +1698,85 @@ 更新时间 + + + 赛事裁判分配表 + + + + + 关联的赛事ID + + + + + 关联的比赛场次ID + + + + + 关联的赛事裁判记录ID + + + + + 关联的裁判档案ID(兼容历史查询的冗余字段) + + + + + 关联的小程序用户ID(裁判尚未注册小程序账号时为空) + + + + + 裁判席位编码(取自计分规则的裁判席位配置,如 T、DP1、P) + + - 0草稿,1已提交,2已替换,9停用。 + + 分配状态:0-草稿,1-已提交,2-已替换,9-已停用 + + + + + 提交时间 + + + + + 备注 + + + + + 可用状态 + + + + + 标记删除 + + + + + 创建人 + + + + + 创建时间 + + + + + 更新人 + + + + + 更新时间 + @@ -1780,6 +2043,61 @@ 更新时间 + + + 赛事裁判表 + + + + + 关联的赛事ID + + + + + 关联的裁判档案ID + + + + + 关联的裁判报名记录ID(非小程序报名时为空) + + + + + 关联来源:1-后台添加,2-小程序报名 + + + + + 可用状态 + + + + + 标记删除 + + + + + 创建人 + + + + + 创建时间 + + + + + 更新人 + + + + + 更新时间 + + 赛事报名表 @@ -2485,8 +2803,180 @@ 更新时间 + + + 赛事比赛场次表 + + + + + 关联的赛事ID + + + + + 关联的赛事项目ID + + + + + 关联的赛事项目组别ID(本场次为多个组别混合时为空) + + + + + 关联的比赛场地ID + + + + + 关联的计分规则ID(取自项目配置的计分规则) + + + + + 场次编号(同一赛事内未删除数据中唯一) + + + + + 场次名称 + + + + + 场次开始时间 + + + + + 场次结束时间 + + - 0草稿,10就绪,30进行中,60结束,90取消。 + + 场次状态:0-草稿,10-就绪,30-进行中,60-已结束,90-已取消 + + + + + 版本号(场次内容变更时递增) + + + + + 可用状态 + + + + + 标记删除 + + + + + 创建人 + + + + + 创建时间 + + + + + 更新人 + + + + + 更新时间 + + + + + 赛事场次出场记录表 + + + + + 关联的比赛场次ID + + + + + 关联的赛事参赛记录ID(未匹配到参赛记录时为空) + + + + + 关联的报名项目明细ID + + + + + 出场编号(项目序号+场地序号+出场顺序) + + + + + 出场顺序(同一场次内未删除数据中唯一) + + + + + 出场状态:0-待出场(其余状态由打分系统流转时写入) + + + + + 参赛人员姓名 + + + + + 参赛队伍名称(个人项目时为空) + + + + + 参赛组别名称 + + + + + 版本号(出场信息变更时递增) + + + + + 可用状态 + + + + + 标记删除 + + + + + 创建人 + + + + + 创建时间 + + + + + 更新人 + + + + + 更新时间 + @@ -2548,6 +3038,61 @@ 更新时间 + + + 赛事比赛场地表 + + + + + 关联的赛事ID + + + + + 场地编码(同一赛事内未删除数据中唯一,如 V01) + + + + + 场地名称 + + + + + 可用状态 + + + + + 排序 + + + + + 标记删除 + + + + + 创建人 + + + + + 创建时间 + + + + + 更新人 + + + + + 更新时间 + + 可用状态 @@ -3130,7 +3675,9 @@ - 计分规则。 + + 计分规则ID(未配置计分规则时为空,赛事编排确认前必须配置) + @@ -3238,15 +3785,164 @@ - 裁判档案。 + + 裁判档案表 + + + + + 裁判姓名 + + + + + 联系电话(未删除数据中唯一) + + + + + 关联的小程序用户ID(未关联小程序账号时为空) + + + + + 所属省份编码 + + + + + 所属省份名称 + + + + + 所属城市编码 + + + + + 所属城市名称 + + + + + 所属区县编码 + + + + + 所属区县名称 + + + + + 裁判等级:1-国际级,2-国家级,3-一级,4-二级,5-三级 + + + + + 裁判证书图片URL + + + + + 可执裁角色(位标记,可多选) + + + + + 审核状态 + + + + + 审核备注 + + + + + 可用状态 + + + + + 标记删除 + + + + + 创建人 + + + + + 创建时间 + + + + + 更新人 + + + + + 更新时间 + - 裁判报名执裁记录。 + 裁判报名执裁记录表 + + + + + 关联的赛事ID + + + + + 报名执裁的小程序用户ID(对应 AppUser.Id,账户角色为裁判) - 1-已报名,2-已确认,3-已拒绝,4-已取消。 + + 报名状态:1-处理中(历史数据),2-报名成功,3-报名失败,4-已取消 + + + + + 备注 + + + + + 可用状态 + + + + + 标记删除 + + + + + 创建人 + + + + + 创建时间 + + + + + 更新人 + + + + + 更新时间 + @@ -3429,7 +4125,64 @@ - 项目计分规则。 + + 项目计分规则表 + + + + + 规则编码(未删除数据中唯一,如 ROPE_PRESCRIBED、FREESTYLE_SINGLE) + + + + + 规则名称 + + + + + 裁判席位配置(JSON格式,按裁判组描述该规则需要的裁判角色席位) + + + + + 计分参数配置(JSON格式,规则的扣分项、权重等扩展参数) + + + + + 可用状态 + + + + + 排序 + + + + + 标记删除 + + + + + 创建人 + + + + + 创建时间 + + + + + 更新人 + + + + + 更新时间 + @@ -4128,12 +4881,12 @@ - 单位名称(单位账号专用) + 单位名称 - 所属平台(null 表示普通系统用户,非单位管理创建的账号) + 所属平台 @@ -4161,14 +4914,19 @@ 更新时间 + + + 系统用户角色关联表 + + - 用户id + 关联的系统用户ID - 角色id + 关联的系统角色ID @@ -4720,7 +5478,23 @@ - 小程序账户角色。与参赛人员的 PersonType 分离,避免混淆业务身份。 + 小程序账户角色 + 与报名人员类型 PersonType 相互独立,避免混淆业务身份 + + + + + 参赛者 + + + + + 裁判 + + + + + 检录员 @@ -4908,6 +5682,72 @@ 团队花样 + + + 裁判等级 + + + + + 国际级裁判员 + + + + + 国家级裁判员 + + + + + 一级裁判员 + + + + + 二级裁判员 + + + + + 三级裁判员 + + + + + 裁判可执裁的角色 + 位标记枚举,一名裁判可同时具备多个执裁角色,按位组合存储 + + + + + 无(未选择任何执裁角色) + + + + + 主裁 + + + + + 表现裁判 + + + + + 难度裁判 + + + + + 创意编排裁判(仅规定赛使用) + + + + + 完成质量裁判(仅规定赛使用) + + 报名方式 @@ -4930,7 +5770,12 @@ - 单位账号所属平台 + 用户所属平台 + + + + + 赛事系统用户 diff --git a/二期需求/赛事二期四模块开发设计说明书(含数据库变更).docx b/二期需求/赛事二期四模块开发设计说明书(含数据库变更).docx index fec7d3b..495a8ed 100644 Binary files a/二期需求/赛事二期四模块开发设计说明书(含数据库变更).docx and b/二期需求/赛事二期四模块开发设计说明书(含数据库变更).docx differ diff --git a/二期需求/赛事二期四模块需求规格说明书.docx b/二期需求/赛事二期四模块需求规格说明书.docx index 310803e..21132ff 100644 Binary files a/二期需求/赛事二期四模块需求规格说明书.docx and b/二期需求/赛事二期四模块需求规格说明书.docx differ