From 76226dc1112a78ca23758c2d0f5366cfa6b2033a Mon Sep 17 00:00:00 2001 From: ltx <1006158920@qq.com> Date: Thu, 5 Nov 2026 17:54:19 +0800 Subject: [PATCH] =?UTF-8?q?update=EF=BC=9A=E7=9B=B4=E4=BC=A0bug=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AppUpdate/AppReleaseUploadInputs.cs | 19 + .../AppUpdate/AppUpdateController.cs | 97 +- YD_Event.Application/YD_Event.Application.xml | 1196 ++++++++++++++--- .../Infrastructure/Aliyun/Oss/OssService.cs | 39 +- 4 files changed, 1156 insertions(+), 195 deletions(-) create mode 100644 YD_Event.Application/AppUpdate/AppReleaseUploadInputs.cs diff --git a/YD_Event.Application/AppUpdate/AppReleaseUploadInputs.cs b/YD_Event.Application/AppUpdate/AppReleaseUploadInputs.cs new file mode 100644 index 0000000..b93469f --- /dev/null +++ b/YD_Event.Application/AppUpdate/AppReleaseUploadInputs.cs @@ -0,0 +1,19 @@ +namespace YD_Event.Application.AppUpdate; + +public sealed class AppReleaseUploadTicketInput +{ + public int VersionCode { get; set; } + public long FileSize { get; set; } +} + +public sealed class CompleteAppReleaseUploadInput +{ + public string ObjectKey { get; set; } = string.Empty; + public long FileSize { get; set; } + public string VersionName { get; set; } = string.Empty; + public int VersionCode { get; set; } + public string CertificateSha256 { get; set; } = string.Empty; + public string NotesZh { get; set; } = string.Empty; + public string? NotesEn { get; set; } + public bool IsMandatory { get; set; } +} diff --git a/YD_Event.Application/AppUpdate/AppUpdateController.cs b/YD_Event.Application/AppUpdate/AppUpdateController.cs index a119ff8..0c73824 100644 --- a/YD_Event.Application/AppUpdate/AppUpdateController.cs +++ b/YD_Event.Application/AppUpdate/AppUpdateController.cs @@ -1,4 +1,5 @@ using System.Security.Cryptography; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using YD_Event.Application.Auth; using YD_Event.Application.Common; @@ -24,18 +25,21 @@ public class AppUpdateController : IDynamicApiController private readonly IIdGenerator _ids; private readonly FileService _files; private readonly OssService _oss; + private readonly ILogger _logger; private readonly IOptionsMonitor _ossOptions; private readonly string _channel; public AppUpdateController(ISqlSugarRepository releases, AuthManager auth, IIdGenerator ids, FileService files, - IOptionsMonitor ossOptions, OssService oss) + IOptionsMonitor ossOptions, OssService oss, + ILogger logger) { _releases = releases; _auth = auth; _ids = ids; _files = files; _oss = oss; + _logger = logger; _ossOptions = ossOptions; _channel = Environment.GetEnvironmentVariable("APP_UPDATE_CHANNEL")?.Trim().ToLowerInvariant() ?? "production"; } @@ -111,6 +115,97 @@ public class AppUpdateController : IDynamicApiController .OrderByDescending(x => x.VersionCode).ToListAsync(); } + /// 签发只允许超级管理员使用的短时效 OSS 直传地址;请求体不包含 APK。 + [HttpPost("admin/upload-ticket")] + public async Task CreateUploadTicket([FromBody] AppReleaseUploadTicketInput input) + { + EnsureAdmin(); + if (!_ossOptions.CurrentValue.Enable) + throw Oops.Bah("裁判端 APK OSS 未启用,请先配置阿里云对象存储"); + if (!Regex.IsMatch(_channel, "^[a-z0-9-]{1,32}$")) + throw Oops.Bah("APP_UPDATE_CHANNEL 配置无效"); + if (input.VersionCode <= 0 || input.FileSize < 1024 || input.FileSize > MaxApkSize) + throw Oops.Bah("APK 文件大小或版本号无效(最大 200MB)"); + if (await _releases.IsAnyAsync(x => x.Channel == _channel && + x.PackageName == PackageName && x.VersionCode == input.VersionCode)) + throw Oops.Bah("该版本号已存在,请使用新的递增版本号"); + + var objectKey = $"{RELEASE_OSS_PREFIX}staging/{_channel}/{_auth.UserId}/{input.VersionCode}/{Guid.NewGuid():N}.apk"; + var expiresAt = DateTime.UtcNow.AddMinutes(30); + var uploadUrl = _oss.PresignPutToBucket(RELEASE_OSS_BUCKET, objectKey, expiresAt); + return new { uploadUrl = uploadUrl.AbsoluteUri, objectKey, expiresAt, contentType = "application/octet-stream" }; + } + + /// 从 OSS 校验直传文件,复制到不可由临时签名覆盖的正式路径,再建立草稿。 + [HttpPost("admin/upload-complete")] + public async Task CompleteUpload([FromBody] CompleteAppReleaseUploadInput input) + { + EnsureAdmin(); + if (!_ossOptions.CurrentValue.Enable) + throw Oops.Bah("裁判端 APK OSS 未启用"); + if (input.FileSize < 1024 || input.FileSize > MaxApkSize || input.VersionCode <= 0 || + string.IsNullOrWhiteSpace(input.VersionName) || input.VersionName.Length > 64 || + string.IsNullOrWhiteSpace(input.NotesZh) || input.NotesZh.Length > 2000 || + (input.NotesEn?.Length ?? 0) > 2000) + throw Oops.Bah("请核对 APK 大小、版本号、版本名和更新说明"); + var fingerprint = input.CertificateSha256?.Trim().Replace(":", "").ToUpperInvariant(); + if (fingerprint == null || !Regex.IsMatch(fingerprint, "^[0-9A-F]{64}$")) + throw Oops.Bah("请填写 64 位的公开签名证书 SHA-256 指纹"); + var expectedPrefix = $"{RELEASE_OSS_PREFIX}staging/{_channel}/{_auth.UserId}/{input.VersionCode}/"; + if (string.IsNullOrWhiteSpace(input.ObjectKey) || + !input.ObjectKey.StartsWith(expectedPrefix, StringComparison.Ordinal) || + !Regex.IsMatch(input.ObjectKey[expectedPrefix.Length..], "^[0-9a-f]{32}\\.apk$")) + throw Oops.Bah("上传凭据不属于当前管理员或版本号"); + + var finalKey = $"{RELEASE_OSS_PREFIX}{_channel}/{Path.GetFileName(input.ObjectKey)}"; + var fileUrl = $"{RELEASE_OSS_DOMAIN}/{finalKey}"; + var existing = await _releases.GetFirstAsync(x => x.Channel == _channel && + x.PackageName == PackageName && x.VersionCode == input.VersionCode); + if (existing != null) + { + if (existing.FileUrl == fileUrl) return existing; + throw Oops.Bah("该版本号已存在,请使用新的递增版本号"); + } + + _oss.CopyObjectWithinBucket(RELEASE_OSS_BUCKET, input.ObjectKey, finalKey); + var sha = await HashAndValidateReleaseAsync(finalKey, input.FileSize); + var release = new JudgeAppRelease + { + Id = _ids.NextId(), Channel = _channel, PackageName = PackageName, + VersionCode = input.VersionCode, VersionName = input.VersionName.Trim(), + NotesZh = input.NotesZh.Trim(), NotesEn = input.NotesEn?.Trim() ?? string.Empty, + IsMandatory = input.IsMandatory, FileSize = input.FileSize, + Sha256 = sha, CertificateSha256 = fingerprint, FileUrl = fileUrl, + ReleaseStatus = 0, CreatedUserId = _auth.UserId, CreatedTime = DateTime.UtcNow + }; + await _releases.InsertAsync(release); + try { _oss.DeleteObjectFromBucket(RELEASE_OSS_BUCKET, input.ObjectKey); } + catch (Exception error) { _logger.LogWarning(error, "无法清理直传暂存对象 {ObjectKey}", input.ObjectKey); } + return release; + } + + private async Task HashAndValidateReleaseAsync(string objectKey, long expectedSize) + { + await using var stream = _oss.GetObjectFromBucket(RELEASE_OSS_BUCKET, objectKey); + using var hash = IncrementalHash.CreateHash(HashAlgorithmName.SHA256); + var buffer = new byte[81920]; + var header = new byte[4]; + var headerLength = 0; + long size = 0; + int read; + while ((read = await stream.ReadAsync(buffer)) > 0) + { + if (size + read > expectedSize) throw Oops.Bah("OSS 文件大小与所选 APK 不一致"); + for (var i = 0; i < read && headerLength < header.Length; i++) header[headerLength++] = buffer[i]; + hash.AppendData(buffer, 0, read); + size += read; + } + if (size != expectedSize || headerLength != 4 || header[0] != 0x50 || + header[1] != 0x4B || header[2] != 0x03 || header[3] != 0x04) + throw Oops.Bah("OSS 文件不完整或不是有效的 APK/ZIP"); + return Convert.ToHexString(hash.GetHashAndReset()); + } + [HttpPost("admin/upload")] [DisplayName("上传裁判端 APK 草稿")] [RequestSizeLimit(MaxApkSize + 1024 * 1024)] diff --git a/YD_Event.Application/YD_Event.Application.xml b/YD_Event.Application/YD_Event.Application.xml index 36cb8ae..02a24d8 100644 --- a/YD_Event.Application/YD_Event.Application.xml +++ b/YD_Event.Application/YD_Event.Application.xml @@ -46,6 +46,24 @@ 违规内容总数 + + 内部 Android 裁判端 APK 发布与升级检查。 + + + 登录前即可查询当前环境的最新 APK。 + + + OSS 已启用时直连阿里云/CDN;配置缺失或不安全时仍使用受控下载接口。 + + + 只允许下载当前环境已发布的版本,沿用现有文件服务流式返回。 + + + 签发只允许超级管理员使用的短时效 OSS 直传地址;请求体不包含 APK。 + + + 从 OSS 校验直传文件,复制到不可由临时签名覆盖的正式路径,再建立草稿。 + 普通用户服务 @@ -1851,9 +1869,80 @@ 主键ID + + + 市民运动会平台报名数据对接服务。 + + + + + 市民运动会报名数据同步。 + + + + + 使用当前环境配置验证平台地址和签名。 + + + + + 只读取本地报名数据,返回不含敏感信息的上传预览。 + + + + + 将预检通过的报名人员上传到市民运动会平台。 + + + + + 市民运动会报名同步请求。 + + + + + 跃动系统赛事 ID。 + + + + + 市民运动会平台中的赛事名称,必须与一网通办申报名称一致。 + + + + + 系列赛场次名称,没有时不传。 + + + + + 市民运动会报名执行同步请求。 + + + + + 必须明确确认后才允许向外部平台写入。 + + + + + 同步预览结果,包含实际待上传的运动员明细。 + + + + + 实际发送给市民运动会平台的运动员数据。 + + 后台检录员及赛事顺序检录管理。 + + + 统一检录管理使用的赛事状态口径。后台状态与比赛时间不一致时按生命周期靠后的状态处理, + 避免状态同步延迟导致比赛中仍可解绑,或赛事结束后账号未及时释放。 + + 小程序检录员按秩序册列进行顺序检录。 面向运动员、教练的检录进度查询与检录投屏。 @@ -1861,18 +1950,68 @@ 按扫码内容定位秩序册列;支持号码布、报名人员ID和姓名。 - + 检录顺序卡控:非一检必须先通过上一检次。 - - 对报名了所选项目组别的人员发送检录通知;单人失败不影响整体流程。 + + + 获取指定检录对象的记录;个人项目同时兼容未包含人员 ID 的历史检录键。 + - + + + 判断单个检录任务中,指定检录对象是否已经通过检录。 + + + + + 判断多个后续检录任务中,指定检录对象是否已经通过检录。 + + + + + 获取旧版检录键。旧版仅包含赛事项目 ID 和报名项目 ID。 + + + + + 将检录记录映射到当前检录键。旧版个人项目记录按保存的报名人员 ID 精确归属, + 防止同一单位账号报名的其他人员被一并判定为已检录。 + + + + + 对报名了所选项目的人员发送检录通知;单人失败不影响整体流程。 + 个人报名:通知运动员本人;单位报名:通知教练和领队,不受项目类型影响。 + 短信内容:您报名的{赛事名称}正在检录,当前检录至{项目名称}第{x}场次,您所在第{y}场次,请提前前往检录区做好准备。 + + + + 个人报名:取 RegistrationItemId 下的运动员及其场次号。 + + + + 单位报名:运动员 → TeamMember → 同团队教练/领队 → AppUser,汇总其队伍涉及的场次。 + + + + 降级路径:从 EventRegistrationPerson 查同报名下教练/领队。 + + + 批量加载 AppUser 并返回字典。 + + 三检通过即为已上场;仅配置两检时由二检标记上场。 上场检次固定为三检;未配置三检但配置了二检时使用二检。 + + 校验赛事状态是否允许检录操作,不允许时抛出业务异常。 + + + 判断赛事是否允许检录:仅比赛中(Ongoing)允许。 + 汇总各检次的检录记录,生成项目组别三态与当前检录、当前比赛信息。 @@ -1891,164 +2030,379 @@ 从秩序册实时生成检录项目、组别和列对象。 + + + 生成检录对象唯一键。团队项目不传人员 ID,个人项目必须传入人员 ID。 + + 秩序册中的一列;团队项目的一列可以包含多名运动员。 + + + 是否为团队项目,用于区分整队检录和个人逐人检录。 + + + + 报名方式:1-个人报名,2-单位报名;无法关联报名记录时为空。 + + + + 成绩查询赛事信息DTO + + - 赛事状态:3-比赛中,4-已结束。成绩查询只开放这两种状态。 + + 赛事状态:3-比赛中,4-已结束。成绩查询只开放这两种状态。 + + + + + 成绩查询输入参数 + - 项目筛选,对应基础项目 Project.Id。 + + 项目筛选,对应基础项目 Project.Id。 + - 组别筛选,成绩表按组别名称存储。 + + 组别筛选,成绩表按组别名称存储。 + - 成绩查询页的项目、组别下拉数据。 + + 成绩查询页的项目、组别下拉数据 + + + + + 成绩查询项目组别筛选DTO + + + + + 裁判打分项DTO + + + + + 客户端成绩列表项DTO + - 难度分。规定赛没有难度分,值为 0 时不进入 JudgeScores。 + + 难度分。规定赛没有难度分,值为 0 时不进入 JudgeScores。 + - 表现分。 + + 表现分 + - 规定分。 + + 规定分 + - 扣分。 + + 扣分 + + + + + 成绩查询输出结果 + - 成绩数据的最后更新时间,页面显示“数据更新:yyyy-MM-dd HH:mm:ss”。 + + 成绩数据的最后更新时间,页面显示“数据更新:yyyy-MM-dd HH:mm:ss”。 + - 实时排名提示,成绩确认前均为参考值。 + + 实时排名提示,成绩确认前均为参考值。 + + + + + 裁判报名输入参数 + - 裁判认证提交参数,对应小程序“裁判认证”页面。 + + 裁判认证提交参数,对应小程序“裁判认证”页面。 + - 手机号。为空时取当前登录账号手机号。 + + 手机号。为空时取当前登录账号手机号 + - 短信验证码。更换手机号时必填。 + + 短信验证码。更换手机号时必填 + - 可执裁角色,位标记多选。 + + 可执裁角色,位标记多选。 + + + + + 裁判认证信息DTO + - 审核通过后才能报名执裁。 + + 审核通过后才能报名执裁 + - 裁判认证页面的角色、级别下拉数据。 + + 裁判认证页面的角色、级别下拉数据 + + + + + 裁判枚举选项DTO + - 赛事裁判报名名额状态,对应原型“裁判名额未满/已满”提示。 + + 赛事裁判报名名额状态,对应原型“裁判名额未满/已满”提示。 + - 名额上限,0 表示不限名额。 + + 名额上限,0 表示不限名额。 + - 当前账号是否已报名该赛事执裁。 + + 当前账号是否已报名该赛事执裁 + + + + + 裁判执裁申请记录DTO + + + + + 裁判执裁申请列表输出 + + + + + 检录运动员信息DTO + + + + + 检录工作台上下文信息DTO + + + + + 当前赛事是否允许检录操作(仅赛事状态为比赛中时为 true) + + + + + 不允许检录时的原因提示 + + + + + 赛事状态枚举值 + + + + + 赛事状态文本 + + + + + 更新检录状态输入参数 + + + + + 秩序册检录列状态DTO + - 是否已上场;三检通过即视为上场。 + + 是否已上场;三检通过即视为上场。 + - 检录员开始检录并通知报名人员的入参。 + + 检录员开始检录并通知报名人员的入参 + + + + 兼容旧客户端;项目级检录时无需传组别。 - 开始检录后的通知结果。 + + 开始检录后的通知结果 + - 本次通知覆盖的秩序册列数量。 + + 本次通知覆盖的秩序册列数量 + - 应通知人数。 + + 应通知人数 + - 短信通知成功人数。 + + 短信通知成功人数 + - 短信通知失败人数(手机号缺失或发送异常)。 + + 短信通知失败人数(手机号缺失或发送异常) + - 扫码检录入参;扫码内容为运动员号码布编号或报名人员ID。 + + 扫码检录入参;扫码内容为运动员号码布编号或报名人员ID。 + - 扫码检录结果。 + + 扫码检录结果 + - 标记秩序册列上场的入参;仅三检检录员可操作。 + + 标记秩序册列上场的入参;仅三检检录员可操作。 + - 面向运动员、教练的检录进度总览。 + + 面向运动员、教练的检录进度总览 + - 服务端当前时间,供小程序顶部时间展示。 + + 服务端当前时间,供小程序顶部时间展示。 + - 当前检录项目(检录中的项目组别),无进行中项目时为空。 + + 当前检录项目(检录中的项目组别),无进行中项目时为空。 + - 当前正在比赛的项目组别(三检已上场的最新一列)。 + + 当前正在比赛的项目组别(三检已上场的最新一列) + - 赛事检录顺序,按秩序册顺序输出三态。 + + 赛事检录顺序,按秩序册顺序输出三态。 + - 我(或我关联的运动员)在本赛事的报名检录明细。 + + 我(或我关联的运动员)在本赛事的报名检录明细 + - 检录进度中的一个项目组别阶段。 + + 检录进度中的一个项目组别阶段 + - 项目-组别拼接名称,用于页面直接展示。 + + 项目-组别拼接名称,用于页面直接展示。 + - 阶段状态:1-检录中,2-即将开始,3-等待中,4-已完成。 + + 阶段状态:1-检录中,2-即将开始,3-等待中,4-已完成。 + - 该阶段正在比赛的队伍名称。 + + 该阶段正在比赛的队伍名称 + - 该阶段正在比赛的单位名称。 + + 该阶段正在比赛的单位名称 + - 报名类型展示文本:个人报名或团队报名。 + + 报名类型展示文本:个人报名或团队报名 + - 该阶段正在比赛的队员名称列表。 + + 该阶段正在比赛的队员名称列表 + - 我的检录明细卡片。 + + 我的检录明细卡片 + - 运动员姓名列表;教练查看时为其带训报名人员。 + + 运动员姓名列表;教练查看时为其带训报名人员。 + - 我的检录状态:0-等待中,1-检录中,2-已检录,3-已上场。 + + 我的检录状态:0-等待中,1-检录中,2-已检录,3-已上场。 + - 各检次通过情况明细。 + + 各检次通过情况明细 + - 页面提示,例如“请前往检录口”。 + + 页面提示,例如“请前往检录口”。 + - 单个检次的通过情况。 + + 单个检次的通过情况 + - 执行本次检录的检录员姓名。 + + 执行本次检录的检录员姓名 + - 检录投屏数据。 + + 检录投屏数据 + - 当前检录项目下待上场的队伍明细,供大屏滚动展示。 + + 当前检录项目下待上场的队伍明细,供大屏滚动展示。 + - 投屏中的一列检录对象。 + + 投屏中的一列检录对象 + @@ -4109,7 +4463,7 @@ 完成退款流程 - + 获取可查询成绩的赛事。只开放比赛中、已结束两种状态。 @@ -4249,6 +4603,12 @@ + + + 裁判账号注册完成后立即建立未提交档案,使其能够在后台裁判管理中显示。 + 已由后台提前建档的裁判按手机号关联账号,并保留原审核状态及认证资料。 + + 重置密码接口 @@ -4295,11 +4655,17 @@ 查询指定赛事的裁判报名名额状态。 - + 报名名额按赛事报名限制中的裁判限报数量控制,为 0 时不限名额。 + + + 查找当前裁判已报名、后台添加或秩序册导入,且执裁时间与目标赛事重叠的赛事。 + 时间区间按左闭右开处理,相邻赛事首尾时间相等时不视为冲突。 + + 微信小程序接口 @@ -4337,6 +4703,11 @@ 附件链接 + + + 需要保留 Excel 物理行号的行模型。 + + Excel 表头列名映射特性,标注属性所对应的表头单元格文本 @@ -4825,6 +5196,11 @@ 编排预览输出 + + + 是否为团队项目。团队项目按报名项目整队检录,个人项目按报名人员逐人检录。 + + 赛事项目ID,关联 EventProject.Id。 @@ -5940,6 +6316,11 @@ 是否需要填写收款人信息(线下支付且已支付时需要) + + + 是否允许部分取消(多团队合并报名仅允许整单取消) + + 取消报名项目详情DTO(包含组别和团队信息) @@ -6302,6 +6683,21 @@ 报名项目(包含队员) + + + 来源团队ID + + + + + 来源团队名称 + + + + + 队伍序号 + + 报名项目ID @@ -6849,7 +7245,7 @@ - 项目下的场次分配列表 + 项目下按场地归并后的裁判分配列表 @@ -6897,19 +7293,24 @@ 执裁该类型所需具备的裁判角色位标记(RefereeRole 枚举值) + + + 该裁判类型下的具体评分分类(如表演赛 A 组: DP+DM(个人绳)、DA(车轮跳) 等) + + - 场次裁判分配情况 + 项目场地裁判分配情况 - 比赛场次ID + 代表场次ID,用于定位同一项目、同一场地的裁判分配单元 - 场次名称 + 分配单元名称(项目名称-场地名称) @@ -6924,7 +7325,7 @@ - 场次内的裁判席位列表 + 该项目场地下共用的裁判席位列表 @@ -6969,7 +7370,7 @@ - 保存场次裁判分配输入 + 保存项目场地裁判分配输入 @@ -6979,12 +7380,12 @@ - 比赛场次ID + 代表场次ID,用于定位同一项目、同一场地的裁判分配单元 - 该场次的裁判分配列表 + 该项目场地下共用的裁判分配列表 @@ -8399,6 +8800,21 @@ 报名项目选择输出 + + + 来源团队ID(报名预览时返回) + + + + + 来源团队名称(报名预览时返回) + + + + + 队伍序号(报名预览时返回) + + 项目ID @@ -8781,7 +9197,7 @@ - 团队ID (必填) + 团队ID(旧版单团队结构,保留兼容) @@ -9384,6 +9800,11 @@ 组别队员分配中的团队信息 + + + 来源团队ID + + 团队序号(例如:1, 2, 3) @@ -9500,7 +9921,7 @@ 随机种子由赛事和赛事项目确定,避免预览、导出与确认结果反复变化。 - + 花样项目以整个组别为不可拆分单元分配场地,优先将大组分配给当前负载最小的场地。 同一场地内的组别串行,且每个组别的所有出场记录连续。 @@ -9746,6 +10167,11 @@ 赛事管理服务 + + + 清除赛事限报与项目相关缓存,确保后台修改后前台立即生效 + + 分页查询赛事 @@ -9872,7 +10298,8 @@ - 保存场次裁判分配。 + 保存场地裁判分配。 + 传入的场次仅用于定位“项目 + 场地”,分配结果会同步到该分配单元下的全部实际场次。 同一裁判类型可分配多名裁判,人数上下限由计分规则的裁判席位配置决定。 场次裁判分配信息。 @@ -10059,6 +10486,12 @@ 从报名项中获取所有人员列表(支持新旧数据结构) + + + 根据已保存的报名明细重建统一报名结构,用于支付确认前再次校验报名限制。 + 同一项目组别的多个报名明细会还原为多个参赛队伍,避免少算队伍数量。 + + 获取人员证件号码(根据认证类型返回身份证号或护照号) @@ -10311,6 +10744,12 @@ 赛事开始时间 应退金额 + + + 判断报名单是否包含多个来源团队。 + 来源团队与同一项目下的一队、二队不是同一概念,以报名项的 TeamId 为准。 + + 统一取消报名预览方法 @@ -11240,6 +11679,9 @@ 一个出场(个人或队伍)的成绩聚合结果 + + 舍入前的因子,仅用于复核;计算使用舍入后的因子。 + 难度分(规定赛不使用,花样赛复用同一套聚合时写入) @@ -11302,7 +11744,13 @@ - 花样赛的扣分因子 M = max(0, 1 - 主裁扣分合计) + 花样赛的扣分因子 M = max(0, 1 - 最终扣分合计)。 + 含表现裁判失误项的规则会将所有表现裁判与主裁的失误次数一起平均,四舍五入取整数后换算,最后与主裁其他扣分合并。 + + + + + 花样赛表现裁判与主裁失误合并计算明细;当前规则不包含两类失误项时为空 @@ -11310,6 +11758,82 @@ 取某个计分符号的分值 + + 保存总分计算前的原始因子,便于复核舍入过程。 + + + 保存总分计算前的原始因子,便于复核舍入过程。 + + + + 花样赛所有表现裁判与主裁失误平均及扣分换算明细 + + + + + 各表现裁判记录的失误次数 + + + + + 主裁记录的失误次数 + + + + + 参与平均的表现裁判人数(主裁不参与该平均) + + + + + 参与最终失误平均的裁判总人数(表现裁判 + 主裁) + + + + + 兼容历史快照的字段;新快照表示所有参与裁判的未取整平均失误数 + + + + + 表现裁判内部平均失误次数 + + + + + 所有表现裁判与主裁失误次数之和,尚未求平均 + + + + + 所有表现裁判与主裁按实际人数求得的未取整平均失误数 + + + + + 四舍五入后用于阶梯扣分的失误次数 + + + + + 主裁评分单中原失误扣分 + + + + + 合并失误次数换算出的新失误扣分 + + + + + 主裁除失误之外的其他扣分 + + + + + 最终扣分 + + 参与排名的一条成绩 @@ -11529,6 +12053,12 @@ 多名裁判之间的聚合方式:avg、sum、max、min + + + 聚合后保留的小数位数。为空时沿用计分规则的 ScoreDecimals; + 个人花样规定元素缺失数按同类型裁判平均后需四舍五入到整数,因此配置为 0。 + + 参与总分时的符号:1-加分,-1-减分 @@ -11674,7 +12204,7 @@ - 规定元素目标完成量(个人花样、车轮跳花样默认 4),只配在缺失数自动项上 + 规定元素目标完成量(个人花样为 4,车轮跳花样为 4 类 × 每类 4 个),只配在缺失数自动项上 @@ -11975,6 +12505,12 @@ 计算结果 + + 裁判任务分页结果;场次范围使用秩序册中的实际场次号。 + + + 仅含当前裁判可访问的出场信息,不含平台最终成绩。 + 裁判端查询的当前场次赛事状态与操作权限。 @@ -12110,6 +12646,11 @@ 我已提交评分的出场数 + + + 场次在赛事编排中的顺序 + + 裁判端出场记录 @@ -12130,6 +12671,16 @@ 出场顺序 + + + 平板端展示的业务场次号;重赛沿用来源出场记录的场次号 + + + + + 秩序册中的全局出场顺序;非秩序册数据为0 + + 参赛者/参赛队名称 @@ -12393,12 +12944,12 @@ - 规定元素目标完成量(个人花样、车轮跳花样为 4) + 规定元素目标完成量(个人花样为 4,车轮跳花样为 16) - 规定元素是否按类别计完成量。true 时该类别录入数量大于 0 即计一类(车轮跳花样), + 规定元素是否按类别计完成量。true 时该类别录入数量大于 0 即计一类(如表演赛), false 时按数量 × 单位分值折算(个人花样 3 级及以上按 1 计、2 级及以下按 0.5 计)。 @@ -12427,6 +12978,9 @@ 裁判端评分表 + + 历史互动配合数据缺少等级时,提示重新核对录入,不能自动推断。 + 出场记录ID @@ -12610,6 +13164,11 @@ 各项评分录入值 + + + 主裁花样赛扣分系数(各扣分项合计值,不是最终得分系数 M) + + 提交评分 @@ -12625,6 +13184,11 @@ 各项评分录入值 + + + 主裁花样赛扣分系数(各扣分项合计值,不是最终得分系数 M) + + 幂等键,取评分表接口返回值。弱网重试时同一幂等键只会提交一次。 @@ -13326,7 +13890,7 @@ 计算成绩的统一入口。persist 为 false 时只算不写,用于记分台预览。 - + 按当前已提交的评分单算出一个出场的成绩与进度(纯内存计算) @@ -13341,6 +13905,11 @@ 作废某个出场已写入的成绩,成绩不再对外展示也不参与排名 + + + 作废指定成绩及其当前版本,历史版本保留留痕。 + + 按项目、组别重排名次。 @@ -13353,11 +13922,16 @@ 取本场裁判的姓名,用于进度提示与成绩明细展示 - + 取各出场对应的运动员名单。集体项目取同一报名项下的全部运动员,个人项目只有一人。 + + + 个人项始终显示出场人快照,只有集体项才使用队名。 + + 计分规则里裁判席位配置(JudgeRoleSchemaJson)的解析工具。 @@ -13448,6 +14022,12 @@ 场次状态。 允许修改返回 ,否则返回 。 + + + 结合赛事状态判断是否允许修改裁判分配。 + 赛事进入比赛中或已结束后,即使场次仍停留在草稿/就绪状态也必须锁定。 + + 判断指定场次状态是否允许评分。 @@ -13455,6 +14035,15 @@ 场次状态。 允许评分返回 ,否则返回 。 + + + 结合赛事状态判断指定场次是否允许评分。 + 已提交裁判分配的场次处于“就绪”状态;赛事进入比赛中后,该状态即可评分。 + + 场次状态。 + 赛事状态。 + 允许评分返回 ,否则返回 。 + 根据场次状态获取赛事阶段编码。 @@ -13462,6 +14051,11 @@ 场次状态。 赛事阶段编码。 + + + 结合赛事状态获取赛事阶段编码。 + + 根据场次状态获取赛事阶段名称。 @@ -13469,6 +14063,11 @@ 场次状态。 赛事阶段名称。 + + + 结合赛事状态获取赛事阶段名称。 + + 获取指定场次状态下禁止评分的提示信息。 @@ -13476,6 +14075,11 @@ 场次状态。 禁止评分的提示信息。 + + + 结合赛事状态获取禁止评分的提示信息。 + + 获取指定场次状态下禁止修改裁判分配的提示信息。 @@ -13483,13 +14087,19 @@ 场次状态。 禁止修改裁判分配的提示信息。 + + + 结合赛事状态获取禁止修改裁判分配的提示信息。 + + 打分系统裁判端评分接口(独立于微信小程序,供裁判端页面调用)。 规定赛计分(totalMode=linear):创意编排裁判创意分取平均记作 T1,完成质量裁判完成分取平均记作 T2, 主裁每项违例从 100 分中扣 5 分记作 T3,总分 = T1 + T2 - T3。 花样赛计分(totalMode=factor):两人车轮跳花样 R = D × (1 + P) × Q × M, - 其中 D = (dA + dB) / 2、P = 表现裁判均值 × 0.05、Q = max(0, 1 - 0.05 × 规定元素缺失类别数)、M = max(0, 1 - 主裁扣分合计); + 其中 D = (dA + dB) / 2、P = 表现裁判均值 × 0.05、Q = max(0, 1 - 0.025 × 规定元素缺失数)、 + M = max(0, 1 - 最终扣分合计);所有表现裁判与主裁的失误次数按实际人数求平均,四舍五入取整后计算阶梯扣分,其他扣分仍取主裁记录; 交互绳花样 R = D × (1 + P) × M,其中 D = dJ × 1.4 + dT、P = 表现裁判均值 × 0.033,不计 Q。 表演赛 R = D × (1 + P) × Q × M,其中 D = (A + B) / 2(A/B 组难度裁判各自取平均)、P = 表现裁判均值 × 0.05、 Q = max(0, 1 - 0.05 × 5 类规定元素缺失数)、M = max(0, 1 - (失误 + 违例))。 @@ -13501,6 +14111,18 @@ 获取我的执裁场次 + + 分页读取当前裁判最近待执裁赛事的任务。 + + + 按出场ID定位本人任务,用于修正通知跳转。 + + + 只沿编排顺序向后查找下一条正常待评分任务。 + + + 一次批量读取任务所需数据,避免对每个场次请求一次出场名单。 + 查询当前场次的赛事阶段与操作权限,供裁判端进入场次或准备评分时刷新界面状态。 @@ -13564,11 +14186,17 @@ 校验当前登录用户的裁判身份并取出裁判档案 - + 校验当前场次状态是否允许评分。 场次状态。 + 赛事状态。 + + + + 获取场次所属赛事,确保评分权限校验与后台展示使用同一赛事状态。 + @@ -13580,11 +14208,17 @@ 加载评分表所需的出场、计分上下文与我的裁判席位 - + 取出场对应的运动员姓名,用于评分表表头展示 + + + 个人项始终显示出场人快照;只有集体项才优先显示队名。 + 兼容历史出场记录中的单位名称字段。 + + 该评分单是否有正在处理中的修正申请 @@ -13610,6 +14244,12 @@ 把录入项转成计算器的入参 + + + 校验主裁端上传的扣分系数。最终得分系数 M 仍由平台按 1-扣分系数计算, + 这里不信任客户端结果,只用于确认页面明细与上传系数属于同一评分快照。 + + 组装合计预览 @@ -13650,6 +14290,11 @@ 评分任务状态:已作废 + + + 消息类型:成绩修正提醒 + + 评分任务类型:成绩修正 @@ -13690,6 +14335,43 @@ 出场状态:退赛 + + + 获取场次在赛事编排中的顺序。自动编排和秩序册导入生成的场次号末段均保存场次顺序; + 速度项目只有项目和场地段,同一项目内按场地顺序排列。 + + + + + 将自动编排任务还原为“项目、场次、场地”的全局顺序。 + 同组同场地的多条记录在数据库中属于同一场次,因此需结合出场顺序恢复原始场次号。 + + + + + 获取自动编排出场记录的实际场次号。 + + + + + 秩序册导入以场次名称作为业务场次号,无法解析时回退到编排序号。 + + + + + 构建裁判端展示用的“场次-场地”,去除秩序册导入时写入场次名的项目和组别信息。 + + + + + 获取用于裁判场次选择的赛事实时状态,避免后台状态同步延迟导致比赛中场次不可见。 + + + + + 兼容历史实体名和当前业务关系名两种成绩修正消息标识。 + + 场次状态名称 @@ -13711,15 +14393,22 @@ 规定赛:T1=3 名创意编排裁判创意分的平均值,T2=3 名完成质量裁判完成分的平均值, T3=主裁违例扣分(每项扣 5 分)合计,总分=T1+T2-T3,均四舍五入保留两位小数。 花样赛:各难度裁判类型先聚合再按难度除数合并为 D,表现裁判平均分乘系数得 P(个人/车轮 0.05、交互绳 0.033), - 规定元素缺失数换算为 Q,主裁扣分换算为 M = max(0, 1 - 扣分),总分 = D × (1 + P) × Q × M(交互绳不计 Q)。 + 规定元素缺失数换算为 Q;扣分中的失误按所有表现裁判与主裁的实际人数求平均,取整后换算,并加上主裁其他扣分, + 得到 M = max(0, 1 - 扣分),总分 = D × (1 + P) × Q × M(交互绳不计 Q)。 排名:总分高者在前,总分相同按规则配置的比较字段依次比较,全部相同则并列,其后名次按 N+M 规则顺延。 - + 把某个出场的各裁判评分单聚合成一条成绩。judgeNames 用于在计算明细里带上裁判姓名,可为空。 + + + 对所有表现裁判与主裁记录的失误次数求平均,并替换扣分因子中的失误部分。 + 以评分项是否同时存在 P/FAILURE_COUNT 和 T/T_FAILURE 作为启用条件,规定赛等不含这些评分项的规则不受影响。 + + 花样赛按乘法公式算总分: @@ -13782,6 +14471,18 @@ 把聚合结果写入成绩对应的分值字段 + + 从已保存的计算快照读取展示值,不能在查询时静默重算历史成绩。 + + + 按角色格式化本人得分,主裁系数保留三位。 + + + 本裁判分值;主裁为本人的系数预览,综合M以成绩快照为准。 + + + 花样得分系数 M;非花样或缺少计算快照时不推断数值。 + 计分规则配置解析服务 @@ -13792,6 +14493,12 @@ 解析计分规则配置,配置缺失或格式不合法时回退到规定赛的默认配置,避免整场比赛因一处配置错误无法计分。 + + 个人花样与同步花样采用相同的表现分计算口径。 + + + 补齐花样赛规则的默认计算参数。解析过程只生成内存配置,不改写数据库。 + 读取参与总分的符号,非法值按加分处理 @@ -13807,6 +14514,11 @@ 读取配置里的数值,非数字等非法值按未配置处理 + + + 读取受限整数配置,非法值按未配置处理。 + + 未配置聚合方式时按规定赛的 T1/T2/T3 兜底 @@ -13837,6 +14549,11 @@ 解析评分项定义。scope 为 personal 或 team,用于取对应的分值上限。 + + + 车轮跳包含四类规定元素,每类最多录入四个,合计规定数量为十六个。 + + 读取扩展配置里的文本,空白按未配置处理 @@ -13866,7 +14583,7 @@ 按计分规则计算一张评分单。 requireAll 为 true(正式提交)时校验必填项与取值范围;为 false(保存草稿)时允许留空,只算已录入的部分。 - 花样赛的表现维度分、失误自动减分、规定元素缺失数一律由服务端联动生成,客户端传来的自动项不参与计算。 + 花样赛的表现维度分、失误联动项、规定元素缺失数一律由服务端生成,客户端传来的自动项不参与计算。 @@ -13877,7 +14594,8 @@ - 表现维度计算。每个维度 = 基准分 + 人工加 - 人工减 - 失误自动减,限制在上下限内, + 表现维度计算。失误在每个维度只扣一次,同时独立提交给综合扣分 M; + 每个维度 = 基准分 + 人工加 - 人工减 - 失误次数,限制在上下限内, 再按各维度权重加权求和得到本裁判的表现分。失误自动减分作为自动项落明细,裁判不可直接修改。 @@ -13890,7 +14608,8 @@ 规定元素完成量与缺失量。 按数量折算时(个人花样)3 级及以上按 1 计、2 级及以下按 0.5 计; - 按类别计数时(车轮跳花样)每个类别只要录入数量大于 0 就计一类。 + 按类别计数时(如表演赛)每个类别只要录入数量大于 0 就计一类; + 车轮跳花样按实际完成数累加,4 类元素每类上限 4 个。 缺失量 = max(0, 目标数 - 完成量),作为自动项落明细,供跨裁判聚合按 Q = 1 - 系数 × 缺失量 计算。 @@ -13932,8 +14651,8 @@ 车轮跳花样:DA×2、DB×2、P×2、T×1;dA=(da1+da2)/2、dB=(db1+db2)/2、D=(dA+dB)/2;
P=average(p1,p2)×0.05;Q=max(0,1-0.05×规定元素缺失类别数);M=max(0,1-(失误+违例+间歇));
总分 R=max(0, D×(1+P)×Q×M)。
- 交互绳花样:DJ×2、DT×2、P×2、T×1;DJ 按全部动作取均值、DT 取最高 20 个动作(分母固定 20);
- dJ=(DJ1+DJ2)/2、dT=(DT1+DT2)/2、D=dJ×1.4+dT;P=average(p1,p2)×0.033;
+ 交互绳花样:DJ、DT、P 各可配置 1~2 名裁判,T 固定 1 名;DJ 按全部动作取均值、DT 取最高 20 个动作(分母固定 20);
+ dJ=DJ 裁判分数平均值、dT=DT 裁判分数平均值、D=dJ×1.4+dT;P=P 裁判分数平均值×0.033;
M=max(0,1-(失误+违例+空间违例));总分 R=max(0, D×(1+P)×M),不计 Q。
@@ -14020,12 +14739,17 @@
- 每缺 1 类规定元素的 Q 扣减系数:Q = max(0, 1 - 0.05 × 缺失类别数) + 每缺 1 类规定元素的 Q 扣减系数(花样跳绳打分.xmind 明确规定 Fq=2.5%=0.025):Q = max(0, 1 - 0.025 × 缺失类别数) - + - 车轮跳花样的规定元素目标类别数(力量和体操、多摇、绳子运用、互动配合) + 车轮跳花样每类规定元素的要求完成数量。 + + + + + 车轮跳花样规定元素应完成总数:4 类 × 每类 4 个。 @@ -14048,6 +14772,17 @@ 难度等级分值表。必须查表取值,不得用浮点公式推导。
+ + + 交互绳跳绳运动员难度等级。间歇单独按0级记录;1~5级均支持减号、标准和加号档。 + 减号按 x-0.25、加号按 x+0.5 换算,分值为 L(x)=1.5^x,保留4位小数。 + + + + + 交互绳摇绳运动员难度等级。1~5级均支持减号、标准和加号档。 + + 表现五维。权重合计必须为 1,页面按此顺序展示。 @@ -14063,14 +14798,19 @@ 车轮跳花样的规定元素类别,只要该类别有动作即计一类完成 + + + 个人花样/同步花样裁判席位配置:DP/DM/DR 难度裁判各 1~2 人、表现裁判 1~2 人、主裁 1 人 + + - 车轮跳花样裁判席位配置:运动员A/B难度裁判各 2 人、表现裁判 2 人、主裁 1 人 + 车轮跳花样裁判席位配置:运动员A/B难度裁判各 1~2 人、表现裁判 1~2 人、主裁 1 人 - 交互绳花样裁判席位配置:跳绳者/摇绳者难度裁判各 2 人、表现裁判 2 人、主裁 1 人 + 交互绳花样裁判席位配置:跳绳者/摇绳者难度裁判各 1~2 人、表现裁判 1~2 人、主裁 1 人 @@ -14107,7 +14847,7 @@ 构造交互绳花样评分项:DJ 按全部动作取均值、DT 取最高 20 个动作,表现与主裁扣分同车轮跳但不计规定元素
- + 构造一个难度裁判类型的等级计数项。 aggregate 为空表示按数量 × 等级分值直接累加;avg 按动作总数取均值;top 取最高 topCount 个动作。 @@ -14260,7 +15000,7 @@ - 规定元素类别项:录入数量大于 0 即计一类完成 + 规定元素类别项:录入数量大于 0 即计一类完成(表演赛使用)。 @@ -14317,7 +15057,7 @@ 裁判席位配置。 - CJ / CQ 不限人数(成绩按平均值计算,标准配置各 3 人写在 RuleConfigJson 里),主裁固定 1 人。 + 主裁固定 1 人,创意编排 CJ 下限 1 人上限 3 人,完成质量 CQ 下限 1 人上限 3 人。 @@ -14522,10 +15262,10 @@ 计分口径(裁判端原型 + 花样跳绳裁判端逻辑与测试验收说明书 6.7):
- 席位:难度A组×3、难度B组×3、表现P×3、主裁T×1。
- A 组裁判一人评 个人绳(DP+DM)、车轮跳(DA)、交互绳(DT_INTERACTION)、旅行跳(DT_TRAVEL) 四个类别,A=average(A1,A2,A3);
- B 组裁判一人评 个人绳(DR)、车轮跳(DB)、交互绳(DJ)、长绳(DL) 四个类别,B=average(B1,B2,B3);
- 总难度 D=(A+B)/2;表现 P=average(p1,p2,p3)×0.05;
+ 席位:难度A组×1~3、难度B组×1~3、表现P×1~3、主裁T×1。
+ A 组裁判一人评 个人绳(DP+DM)、车轮跳(DA)、交互绳(DT_INTERACTION)、旅行跳(DT_TRAVEL) 四个类别,A=average(实际A组裁判);
+ B 组裁判一人评 个人绳(DR)、车轮跳(DB)、交互绳(DJ)、长绳(DL) 四个类别,B=average(实际B组裁判);
+ 总难度 D=(A+B)/2;表现 P=average(实际表现裁判)×0.05;
规定元素 Q=max(0,1-0.05×缺失类别数),5 类为 个人绳、车轮跳、交互绳、长绳(6米以上)、旅行跳;
扣分 M=max(0,1-(失误+违例)),表演赛不记间歇与空间违例;
总分 R=max(0, D×(1+P)×Q×M)。 @@ -14567,6 +15307,12 @@ 规定元素目标类别数:个人绳、车轮跳、交互绳、长绳(6米以上)、旅行跳
+ + + 长绳、旅行跳难度等级按脑图公式 L(x)=1.5^x 换算。 + UnitValue 入库精度为 4 位小数,因此在此按四位小数固化,避免运行期浮点差异。 + + A 组难度裁判要评的类别。 @@ -14585,7 +15331,7 @@ - 表演赛裁判席位配置:A/B 组难度裁判各 3 人、表现裁判 3 人、主裁 1 人 + 表演赛裁判席位配置:A/B 组难度裁判各 1~3 人、表现裁判 1~3 人、主裁 1 人 @@ -15802,54 +16548,47 @@ 秩序册「出场秩序」工作表行模型 - - - 序号(仅便于人工核对,导入时不参与落库) - - - - - 场地名称(必填) - + + 工作表实际行号,不对应Excel列。 - 项目名称(必填,需与赛事已配置项目一致,不存在将自动建档) + 项目名称 + + + + + 计分规则名称 - 组别名称(可空,空则归入"公开组") - - - - - 出场编号(可空,空则按同场次顺序自动生成) + 组别名称 - 姓名/队名(必填) + 运动员姓名;多人项目可使用顿号或逗号分隔 + + + + + 队伍名称(可空) - 所属单位(可空,仅用于核对展示) + 所属单位(可空) - + - 联系电话(可空) + 场次名称 - + - 证件号(可空) - - - - - 备注(可空) + 场地名称 @@ -15857,6 +16596,9 @@ 秩序册「裁判名单」工作表行模型
+ + 工作表实际行号,不对应Excel列。 + 序号(仅便于人工核对) @@ -15872,26 +16614,6 @@ 手机号(必填,用于匹配/创建裁判档案及打分账号) - - - 裁判角色(可空,如:主裁/难度裁判/表现裁判) - - - - - 裁判等级(可空,如:国际级/国家级/一级/二级/三级) - - - - - 负责场地(可空,对应出场秩序中的场地名称) - - - - - 负责项目(可空) - - 秩序册导入解析输出 @@ -15967,6 +16689,9 @@ 秩序册解析问题项 + + 问题所在工作表,便于区分两个工作表的相同行号。 + 所在行号(Excel 物理行号,含表头) @@ -16064,7 +16789,7 @@ - 导入模式:append-追加(默认),overwrite-覆盖该赛事既有编排 + 导入模式:overwrite-覆盖该赛事既有编排(默认),append-追加 @@ -16087,6 +16812,21 @@ 暂存出场行 + + 解析时唯一匹配的平台项目ID。 + + + 解析时唯一匹配的赛事项目ID。 + + + 解析时校验通过的计分规则ID。 + + + 平台项目组别ID。 + + + 赛事项目组别ID。 + Excel 物理行号 @@ -16102,19 +16842,29 @@ 项目名称 + + + 计分规则名称 + + 组别名称 - + - 原始出场编号(可空) + 场次名称 - 姓名/队名 + 姓名 + + + + + 队伍名称 @@ -16122,51 +16872,11 @@ 单位
- - - 联系电话 - - 暂存裁判行 - - - Excel 物理行号 - - - - - 裁判姓名 - - - - - 手机号 - - - - - 裁判角色文本 - - - - - 裁判等级文本 - - - - - 负责场地名称 - - - - - 负责项目名称 - - 秩序册导入接口(下载模板 / 同步解析 / 确认落库) @@ -16195,9 +16905,18 @@ + + 导入匹配只读取已维护的配置,不创建或修复业务数据。 + + + 用真实赛事项目、组别ID区分同名项目;结构化键避免名称含分隔符时碰撞。 + + + 用真实赛事项目、组别ID区分同名项目;结构化键避免名称含分隔符时碰撞。 + - 秩序册导入模板导出器:出场秩序 + 裁判名单 两个工作表 + 秩序册导入模板导出器:出场秩序 + 裁判名单 + 计分规则参考 @@ -17351,7 +18070,7 @@ - 计分规则ID。 + 计分规则ID(速度项目可为空,花样项目必填)。 @@ -18082,9 +18801,11 @@ 发送验证码短信 - + 发送检录通知短信;未配置检录模板时返回 false 由调用方降级处理。 + 模板示例:您报名的${event}正在检录,当前检录至${current},您所在第${order}场次,请提前前往检录区做好准备。单位报名多个场次时 order 可为“1、3、4、5”。 + current 参数格式:{项目名称}第{x}场次。 @@ -19037,6 +19758,9 @@ 工作台出场记录列表输入
+ + 仅获取指定出场记录(供详情按需加载)。 + 场次ID @@ -19062,6 +19786,9 @@ 工作台出场记录详情 + + 所属场次ID。 + 出场记录ID @@ -19077,6 +19804,16 @@ 出场顺序 + + + 该运动员在秩序册预览中的实际场次号 + + + + + 秩序册预览/赛事编排中的全局出场顺序 + + 参赛者姓名 @@ -19127,6 +19864,12 @@ 工作台出场成绩信息 + + 已保存的花样得分系数 M;规定赛为空。 + + + 用于复核的计算快照(包括失误平均和因子舍入过程)。 + 成绩ID @@ -19282,6 +20025,16 @@ 是否存在待处理的修正申请 + + + 修正任务已批准或裁判修改中,原评分仅供追溯,本次提交仍待裁判重新完成。 + + + + + 当前评分单提交的记录画板 PNG 地址 + + 评分单的动态评分项明细快照 @@ -19433,6 +20186,16 @@ 小计得分 + + + 是否由服务端联动生成 + + + + + 联动来源评分项编码 + + 排序号 @@ -19898,6 +20661,12 @@ 是否包含被取消比赛资格(DQ)的记录,默认不包含 + + + 导出出场全名单:无成绩、DQ、退赛和作废记录均保留;忽略成绩状态与关键词筛选。 + 默认关闭,以兼容原有成绩导出调用方。 + + 评分单导出查询输入 @@ -19948,6 +20717,9 @@ 操作人ID(可选) + + 按项目读取成绩概要,分页后才加载裁判提交状态和成绩。 + 打分系统工作台接口(供工作台管理端调用) @@ -20035,6 +20807,9 @@ 导出成绩册(与成绩管理页面筛选条件一致,默认仅导出已确认、未DQ成绩) + + 从出场名单而不是成绩表生成全员成绩册,未评分者也占一行。 + 导出评分单明细(按场次/项目,逐出场逐裁判列出评分项与提交信息) @@ -20045,11 +20820,39 @@ 导出操作审计日志(按赛事ID匹配请求参数,支持时间范围、操作人筛选,最多导出10000条) + + + 获取场次在秩序册/自动编排中的全局顺序。 + 秩序册导入及花样赛自动编排生成的场次号末段均保存该顺序; + 无全局序号的速度项目回退到项目配置及场地顺序。 + + + + + 将自动编排场次中的出场记录还原为秩序册预览的全局顺序。 + 同一组别、同一场地的多条记录共用一个场次,因此需要在场次起始序号上叠加出场序号。 + + + + + 获取自动编排出场记录的实际场次号。 + 花样项目的场次号保存的是本场起始序号;速度项目没有起始序号时按出场顺序计算。 + + + + 秩序册导入以 Excel 场次列作为逻辑场次,无法解析时回退到物理编排序号。 + 规范化重赛/作废原因:去除首尾空格并校验长度为2-200个字符 + + + Resolve the App user that currently owns a judge assignment. Older assignments may not + have JudgeUserId populated, while the referee profile or submitted sheet still has it. + + 同步成绩当前版本的状态,保证 EventResult 与 EventResultVersion 状态一致 @@ -20060,6 +20863,15 @@ 写入工作台业务操作审计日志,记录投屏、确认、修正、重赛、作废等关键动作的状态流转 + + + 按项目分页获取成绩概要。先按秩序册顺序确定页边界,再仅为当前页查询成绩与裁判提交状态; + 评分项和计分板不随五秒轮询返回。 + + + + 点击详情时才加载该出场的裁判评分项和计分板地址。 + 工作台通用列表 Excel 导出器:合并标题 + 表头 + 数据行 diff --git a/YD_Event.Core/Infrastructure/Aliyun/Oss/OssService.cs b/YD_Event.Core/Infrastructure/Aliyun/Oss/OssService.cs index b279f93..0e9e883 100644 --- a/YD_Event.Core/Infrastructure/Aliyun/Oss/OssService.cs +++ b/YD_Event.Core/Infrastructure/Aliyun/Oss/OssService.cs @@ -15,6 +15,7 @@ public class OssService : ISingleton { private readonly OssConnectionOptions _options; private readonly IOss _ossClient; + private readonly IOss _releaseClient; private readonly ILogger _logger; private readonly AsyncPolicy _retryPolicy; @@ -26,6 +27,12 @@ public class OssService : ISingleton if (_options.Enable) { _ossClient = new OssClient(_options.Endpoint, _options.AccessKeyId, _options.AccessKeySecret); + var secureEndpoint = _options.Endpoint.StartsWith("http://", StringComparison.OrdinalIgnoreCase) + ? "https://" + _options.Endpoint[7..] + : _options.Endpoint.StartsWith("https://", StringComparison.OrdinalIgnoreCase) + ? _options.Endpoint + : "https://" + _options.Endpoint; + _releaseClient = new OssClient(secureEndpoint, _options.AccessKeyId, _options.AccessKeySecret); } // 定义重试策略:最多重试3次,每次间隔指数增长 @@ -107,7 +114,7 @@ public class OssService : ISingleton await _retryPolicy.ExecuteAsync(() => Task.Run(() => { stream.Position = 0; - _ossClient.PutObject(bucket, objectName, stream); + _releaseClient.PutObject(bucket, objectName, stream); })); } @@ -207,7 +214,35 @@ public class OssService : ISingleton { if (!_options.Enable) throw new InvalidOperationException("OSS未启用,无法下载裁判端安装包。"); - return _ossClient.GetObject(bucket, objectName).Content; + return _releaseClient.GetObject(bucket, objectName).Content; + } + + /// Issue a short-lived HTTPS PUT URL for browser-to-OSS upload. + public Uri PresignPutToBucket(string bucket, string objectName, DateTime expiresAt) + { + if (!_options.Enable) + throw new InvalidOperationException("OSS未启用,无法签发直传地址。"); + var request = new GeneratePresignedUriRequest(bucket, objectName, SignHttpMethod.Put) + { + Expiration = expiresAt, + ContentType = "application/octet-stream" + }; + return _releaseClient.GeneratePresignedUri(request); + } + + /// Promote a temporary upload without routing the APK through the app server. + public void CopyObjectWithinBucket(string bucket, string sourceKey, string destinationKey) + { + if (!_options.Enable) + throw new InvalidOperationException("OSS未启用,无法完成安装包上传。"); + _releaseClient.CopyObject(new CopyObjectRequest(bucket, sourceKey, bucket, destinationKey)); + } + + public void DeleteObjectFromBucket(string bucket, string objectName) + { + if (!_options.Enable) + throw new InvalidOperationException("OSS未启用,无法清理临时安装包。"); + _releaseClient.DeleteObject(bucket, objectName); } ///