删除MiniExcel统一采用NPOI
对接完善打分系统工作台
This commit is contained in:
parent
5dbfa36057
commit
2c972df6d5
196
YD_Event.Application/Common/Excel/NpoiExcelReader.cs
Normal file
196
YD_Event.Application/Common/Excel/NpoiExcelReader.cs
Normal file
@ -0,0 +1,196 @@
|
||||
using System.Globalization;
|
||||
using System.Reflection;
|
||||
using NPOI.SS.UserModel;
|
||||
|
||||
namespace YD_Event.Application.Common.Excel;
|
||||
|
||||
/// <summary>
|
||||
/// Excel 表头列名映射特性,标注属性所对应的表头单元格文本
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
|
||||
public sealed class ExcelHeaderAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// 表头文本
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
public ExcelHeaderAttribute(string name)
|
||||
{
|
||||
Name = name;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 基于 NPOI 的 Excel 读取器,统一支持 .xls(HSSF) 与 .xlsx(XSSF),按表头名称映射为对象
|
||||
/// </summary>
|
||||
public static class NpoiExcelReader
|
||||
{
|
||||
/// <summary>
|
||||
/// 按文件流自动识别格式创建工作簿,调用方无需区分 .xls/.xlsx
|
||||
/// </summary>
|
||||
public static IWorkbook CreateWorkbook(Stream stream)
|
||||
{
|
||||
// 缓冲到内存,避免上传流不可寻址导致 NPOI 无法按文件头识别格式
|
||||
var memoryStream = new MemoryStream();
|
||||
stream.CopyTo(memoryStream);
|
||||
memoryStream.Position = 0;
|
||||
return WorkbookFactory.Create(memoryStream);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取指定名称的工作表
|
||||
/// </summary>
|
||||
public static List<T> ReadSheet<T>(IWorkbook workbook, string sheetName, int headerSearchRows = 10)
|
||||
where T : new()
|
||||
{
|
||||
var sheet = workbook.GetSheet(sheetName)
|
||||
?? throw new InvalidDataException($"缺少工作表:{sheetName}");
|
||||
return ReadSheet<T>(sheet, headerSearchRows);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取指定序号的工作表(序号从0开始)
|
||||
/// </summary>
|
||||
public static List<T> ReadSheet<T>(IWorkbook workbook, int sheetIndex, int headerSearchRows = 10)
|
||||
where T : new()
|
||||
{
|
||||
if (sheetIndex < 0 || sheetIndex >= workbook.NumberOfSheets)
|
||||
throw new InvalidDataException($"工作表序号超出范围:{sheetIndex}");
|
||||
return ReadSheet<T>(workbook.GetSheetAt(sheetIndex), headerSearchRows);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取工作表:在前若干行内定位表头行,按表头名称逐行映射为对象,整行为空则跳过
|
||||
/// </summary>
|
||||
public static List<T> ReadSheet<T>(ISheet sheet, int headerSearchRows = 10) where T : new()
|
||||
{
|
||||
var headerMap = BuildHeaderMap<T>();
|
||||
var formatter = new DataFormatter();
|
||||
|
||||
var headerRow = LocateHeaderRow(sheet, headerMap.Keys, headerSearchRows, formatter)
|
||||
?? throw new InvalidDataException($"工作表「{sheet.SheetName}」缺少表头");
|
||||
|
||||
// 列序号到属性的映射
|
||||
var columnProperties = new Dictionary<int, PropertyInfo>();
|
||||
foreach (var headerCell in headerRow.Cells)
|
||||
{
|
||||
var headerText = formatter.FormatCellValue(headerCell).Trim();
|
||||
if (headerMap.TryGetValue(headerText, out var property))
|
||||
columnProperties[headerCell.ColumnIndex] = property;
|
||||
}
|
||||
|
||||
var result = new List<T>();
|
||||
for (var rowIndex = headerRow.RowNum + 1; rowIndex <= sheet.LastRowNum; rowIndex++)
|
||||
{
|
||||
var row = sheet.GetRow(rowIndex);
|
||||
if (row == null) continue;
|
||||
|
||||
var item = new T();
|
||||
var hasValue = false;
|
||||
foreach (var (columnIndex, property) in columnProperties)
|
||||
{
|
||||
var cell = row.GetCell(columnIndex);
|
||||
// 以单元格原始文本是否为空判断整行是否有数据,避免值类型默认值0误判
|
||||
var rawText = cell == null ? null : formatter.FormatCellValue(cell).Trim();
|
||||
if (!string.IsNullOrWhiteSpace(rawText)) hasValue = true;
|
||||
var value = ConvertCellValue(cell, property.PropertyType, formatter);
|
||||
property.SetValue(item, value);
|
||||
}
|
||||
|
||||
// 整行没有任何有效数据时视为空行跳过
|
||||
if (hasValue) result.Add(item);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建表头文本到属性的映射
|
||||
/// </summary>
|
||||
private static Dictionary<string, PropertyInfo> BuildHeaderMap<T>()
|
||||
{
|
||||
var map = new Dictionary<string, PropertyInfo>();
|
||||
foreach (var property in typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance))
|
||||
{
|
||||
var header = property.GetCustomAttribute<ExcelHeaderAttribute>();
|
||||
if (header != null) map[header.Name] = property;
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在表头搜索范围内,取命中映射表头数量最多的一行作为表头行
|
||||
/// </summary>
|
||||
private static IRow? LocateHeaderRow(ISheet sheet, ICollection<string> headers,
|
||||
int searchRows, DataFormatter formatter)
|
||||
{
|
||||
var lastRow = Math.Min(sheet.LastRowNum, sheet.FirstRowNum + searchRows - 1);
|
||||
IRow? matched = null;
|
||||
var maxHits = 0;
|
||||
for (var index = sheet.FirstRowNum; index <= lastRow; index++)
|
||||
{
|
||||
var row = sheet.GetRow(index);
|
||||
if (row == null) continue;
|
||||
var hits = row.Cells.Count(cell =>
|
||||
headers.Contains(formatter.FormatCellValue(cell).Trim()));
|
||||
if (hits > maxHits)
|
||||
{
|
||||
maxHits = hits;
|
||||
matched = row;
|
||||
}
|
||||
}
|
||||
return maxHits > 0 ? matched : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 单元格值按目标属性类型转换;空白单元格对引用/可空类型返回 null,对非空值类型返回默认值
|
||||
/// </summary>
|
||||
private static object? ConvertCellValue(ICell? cell, Type targetType, DataFormatter formatter)
|
||||
{
|
||||
var underlyingType = Nullable.GetUnderlyingType(targetType) ?? targetType;
|
||||
var text = cell == null ? null : formatter.FormatCellValue(cell).Trim();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
return CreateDefaultValue(targetType);
|
||||
|
||||
if (underlyingType == typeof(string)) return text;
|
||||
|
||||
// 数值、日期型单元格优先取强类型值,避免格式化文本的千分位、小数位影响解析
|
||||
if (cell is { CellType: CellType.Numeric })
|
||||
{
|
||||
if (DateUtil.IsCellDateFormatted(cell) && underlyingType == typeof(DateTime)
|
||||
&& cell.DateCellValue is { } cellDate)
|
||||
return cellDate;
|
||||
|
||||
var number = cell.NumericCellValue;
|
||||
if (underlyingType == typeof(long)) return (long)Math.Round(number);
|
||||
if (underlyingType == typeof(int)) return (int)Math.Round(number);
|
||||
if (underlyingType == typeof(short)) return (short)Math.Round(number);
|
||||
if (underlyingType == typeof(byte)) return (byte)Math.Round(number);
|
||||
if (underlyingType == typeof(decimal)) return (decimal)number;
|
||||
if (underlyingType == typeof(double)) return number;
|
||||
if (underlyingType == typeof(float)) return (float)number;
|
||||
}
|
||||
|
||||
if (underlyingType == typeof(long) && long.TryParse(text, NumberStyles.Any, CultureInfo.InvariantCulture, out var longValue)) return longValue;
|
||||
if (underlyingType == typeof(int) && int.TryParse(text, NumberStyles.Any, CultureInfo.InvariantCulture, out var intValue)) return intValue;
|
||||
if (underlyingType == typeof(decimal) && decimal.TryParse(text, NumberStyles.Any, CultureInfo.InvariantCulture, out var decimalValue)) return decimalValue;
|
||||
if (underlyingType == typeof(double) && double.TryParse(text, NumberStyles.Any, CultureInfo.InvariantCulture, out var doubleValue)) return doubleValue;
|
||||
if (underlyingType == typeof(DateTime) && DateTime.TryParse(text, CultureInfo.CurrentCulture, DateTimeStyles.None, out var dateTimeValue)) return dateTimeValue;
|
||||
if (underlyingType == typeof(bool) && bool.TryParse(text, out var boolValue)) return boolValue;
|
||||
if (underlyingType.IsEnum && Enum.TryParse(underlyingType, text, out var enumValue)) return enumValue;
|
||||
|
||||
// 无法转换时不抛异常中断整表导入:值类型给默认值,引用/可空类型给 null
|
||||
return CreateDefaultValue(targetType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成类型默认值:非空值类型返回 Activator 默认实例,其余返回 null
|
||||
/// </summary>
|
||||
private static object? CreateDefaultValue(Type targetType)
|
||||
{
|
||||
return targetType.IsValueType && Nullable.GetUnderlyingType(targetType) == null
|
||||
? Activator.CreateInstance(targetType)
|
||||
: null;
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using YD_Event.Core.Enum;
|
||||
using MiniExcelLibs.Attributes;
|
||||
using YD_Event.Application.Common.Excel;
|
||||
|
||||
namespace YD_Event.Application.Event.Dtos;
|
||||
|
||||
@ -245,120 +245,60 @@ public class ScoreImportDto
|
||||
/// <summary>
|
||||
/// 报名ID(唯一标识符)
|
||||
/// </summary>
|
||||
[ExcelColumnName("报名ID")]
|
||||
[ExcelHeader("报名ID")]
|
||||
public long? RegistrationItemId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 赛事地址
|
||||
/// </summary>
|
||||
[ExcelColumnName("赛事地址")]
|
||||
[ExcelHeader("赛事地址")]
|
||||
public string EventAddress { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 项目名称
|
||||
/// </summary>
|
||||
[ExcelColumnName("项目")]
|
||||
[ExcelHeader("项目")]
|
||||
public string ProjectName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 组别名称
|
||||
/// </summary>
|
||||
[ExcelColumnName("组别")]
|
||||
[ExcelHeader("组别")]
|
||||
public string GroupName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 姓名
|
||||
/// </summary>
|
||||
[ExcelColumnName("姓名")]
|
||||
[ExcelHeader("姓名")]
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 联系方式
|
||||
/// </summary>
|
||||
[ExcelColumnName("联系方式")]
|
||||
[ExcelHeader("联系方式")]
|
||||
public string Contact { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 单位名称
|
||||
/// </summary>
|
||||
[ExcelColumnName("单位")]
|
||||
[ExcelHeader("单位")]
|
||||
public string UnitName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 成绩
|
||||
/// </summary>
|
||||
[ExcelColumnName("成绩")]
|
||||
[ExcelHeader("成绩")]
|
||||
public string Score { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 名次(数字)
|
||||
/// </summary>
|
||||
[ExcelColumnName("名次(数字)")]
|
||||
[ExcelHeader("名次(数字)")]
|
||||
public int Rank { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 名次(汉字)
|
||||
/// </summary>
|
||||
[ExcelColumnName("名次(汉字)")]
|
||||
[ExcelHeader("名次(汉字)")]
|
||||
public string RankText { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 证书导出DTO
|
||||
/// </summary>
|
||||
public class CertificateExportDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 证书编号
|
||||
/// </summary>
|
||||
[ExcelColumnName("证书编号")]
|
||||
public string CertificateNo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 姓名
|
||||
/// </summary>
|
||||
[ExcelColumnName("姓名")]
|
||||
public string ParticipantName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 项目名称
|
||||
/// </summary>
|
||||
[ExcelColumnName("项目")]
|
||||
public string ProjectName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 组别名称
|
||||
/// </summary>
|
||||
[ExcelColumnName("组别")]
|
||||
public string GroupName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 单位名称
|
||||
/// </summary>
|
||||
[ExcelColumnName("单位")]
|
||||
public string UnitName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 成绩
|
||||
/// </summary>
|
||||
[ExcelColumnName("成绩")]
|
||||
public string Score { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 名次
|
||||
/// </summary>
|
||||
[ExcelColumnName("名次")]
|
||||
public int Rank { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 证书类型
|
||||
/// </summary>
|
||||
[ExcelColumnName("证书类型")]
|
||||
public string CertificateType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 发放时间
|
||||
/// </summary>
|
||||
[ExcelColumnName("发放时间")]
|
||||
public DateTime? IssuedTime { get; set; }
|
||||
}
|
||||
|
||||
@ -2,7 +2,6 @@ using YD_Event.Application.Event.Dtos;
|
||||
using YD_Event.Core.Domain.Export;
|
||||
using YD_Event.Core.Entities;
|
||||
using YD_Event.Core.Extensions;
|
||||
using MiniExcelLibs;
|
||||
using SqlSugar;
|
||||
using OfficeOpenXml;
|
||||
using OfficeOpenXml.Style;
|
||||
|
||||
@ -6,7 +6,7 @@ using YD_Event.Application.Certificate;
|
||||
using YD_Event.Core.Entities;
|
||||
using YD_Event.Core.Enum;
|
||||
using YD_Event.Core.Domain.Export;
|
||||
using MiniExcelLibs;
|
||||
using YD_Event.Application.Common.Excel;
|
||||
using SqlSugar;
|
||||
using OfficeOpenXml;
|
||||
using OfficeOpenXml.Style;
|
||||
@ -524,7 +524,9 @@ public class EventScoreService : BaseService<EventResult>, ITransient
|
||||
var startTime = DateTime.Now;
|
||||
|
||||
using var stream = file.OpenReadStream();
|
||||
var allRows = stream.Query<ScoreImportDto>().ToList();
|
||||
// NPOI 统一读取,自动识别 .xls/.xlsx,按表头特性映射为导入行
|
||||
using var workbook = NpoiExcelReader.CreateWorkbook(stream);
|
||||
var allRows = NpoiExcelReader.ReadSheet<ScoreImportDto>(workbook, 0);
|
||||
_logger.LogInformation("解析Excel完成,共 {RowCount} 条原始记录", allRows.Count);
|
||||
|
||||
// 过滤掉空行(没有报名ID的行)
|
||||
@ -1536,7 +1538,7 @@ public class EventScoreService : BaseService<EventResult>, ITransient
|
||||
SetHeaders(headers, 1, headerColor, 11);
|
||||
|
||||
// 设置第1列为文本格式,防止科学计数法显示
|
||||
// 只设置前20行的格式,避免MiniExcel读取过多空行
|
||||
// 只设置前20行的格式,避免导入读取时遍历过多空行
|
||||
for (int r = 2; r <= 20; r++)
|
||||
{
|
||||
Worksheet.Cells[r, 1].Style.Numberformat.Format = "@";
|
||||
|
||||
@ -3,7 +3,6 @@ using YD_Event.Application.Project.Dtos;
|
||||
using YD_Event.Core.Entities;
|
||||
using YD_Event.Core.Enum;
|
||||
using Easy.Core;
|
||||
using MiniExcelLibs;
|
||||
using SqlSugar;
|
||||
using Mapster;
|
||||
|
||||
|
||||
@ -393,6 +393,21 @@ public class EventScoreCalculationService : ITransient
|
||||
result.UpdatedTime = now;
|
||||
}
|
||||
await _resultRepository.UpdateRangeAsync(results);
|
||||
// 同步作废当前成绩版本(历史版本保留留痕),名次清零不再参与排名
|
||||
var resultIds = results.Select(x => x.Id).ToList();
|
||||
var versions = await _resultVersionRepository.GetListAsync(x =>
|
||||
!x.DeleteMark && resultIds.Contains(x.EventResultId));
|
||||
var currentVersions = versions.Where(version => results.Any(result =>
|
||||
result.Id == version.EventResultId && result.ResultVersion == version.ResultVersion)).ToList();
|
||||
foreach (var version in currentVersions)
|
||||
{
|
||||
version.ResultStatus = ResultStatusVoided;
|
||||
version.Rank = 0;
|
||||
version.UpdatedUserId = operatorUserId ?? 0;
|
||||
version.UpdatedTime = now;
|
||||
}
|
||||
if (currentVersions.Count > 0)
|
||||
await _resultVersionRepository.UpdateRangeAsync(currentVersions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
using MiniExcelLibs.Attributes;
|
||||
using YD_Event.Application.Common.Excel;
|
||||
|
||||
namespace YD_Event.Application.OrderBook.Dtos;
|
||||
|
||||
@ -10,61 +10,61 @@ public class OrderBookEntryRow
|
||||
/// <summary>
|
||||
/// 序号(仅便于人工核对,导入时不参与落库)
|
||||
/// </summary>
|
||||
[ExcelColumnName("序号")]
|
||||
[ExcelHeader("序号")]
|
||||
public string? Seq { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 场地名称(必填)
|
||||
/// </summary>
|
||||
[ExcelColumnName("场地名称")]
|
||||
[ExcelHeader("场地名称")]
|
||||
public string? VenueName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 项目名称(必填,需与赛事已配置项目一致,不存在将自动建档)
|
||||
/// </summary>
|
||||
[ExcelColumnName("项目名称")]
|
||||
[ExcelHeader("项目名称")]
|
||||
public string? ProjectName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 组别名称(可空,空则归入"公开组")
|
||||
/// </summary>
|
||||
[ExcelColumnName("组别名称")]
|
||||
[ExcelHeader("组别名称")]
|
||||
public string? GroupName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 出场编号(可空,空则按同场次顺序自动生成)
|
||||
/// </summary>
|
||||
[ExcelColumnName("出场编号")]
|
||||
[ExcelHeader("出场编号")]
|
||||
public string? EntryNo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 姓名/队名(必填)
|
||||
/// </summary>
|
||||
[ExcelColumnName("姓名/队名")]
|
||||
[ExcelHeader("姓名/队名")]
|
||||
public string? ParticipantName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 所属单位(可空,仅用于核对展示)
|
||||
/// </summary>
|
||||
[ExcelColumnName("单位")]
|
||||
[ExcelHeader("单位")]
|
||||
public string? UnitName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 联系电话(可空)
|
||||
/// </summary>
|
||||
[ExcelColumnName("联系电话")]
|
||||
[ExcelHeader("联系电话")]
|
||||
public string? Mobile { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 证件号(可空)
|
||||
/// </summary>
|
||||
[ExcelColumnName("证件号")]
|
||||
[ExcelHeader("证件号")]
|
||||
public string? IdCard { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 备注(可空)
|
||||
/// </summary>
|
||||
[ExcelColumnName("备注")]
|
||||
[ExcelHeader("备注")]
|
||||
public string? Remark { get; set; }
|
||||
}
|
||||
|
||||
@ -76,43 +76,43 @@ public class OrderBookRefereeRow
|
||||
/// <summary>
|
||||
/// 序号(仅便于人工核对)
|
||||
/// </summary>
|
||||
[ExcelColumnName("序号")]
|
||||
[ExcelHeader("序号")]
|
||||
public string? Seq { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 裁判姓名(必填)
|
||||
/// </summary>
|
||||
[ExcelColumnName("姓名")]
|
||||
[ExcelHeader("姓名")]
|
||||
public string? Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 手机号(必填,用于匹配/创建裁判档案及打分账号)
|
||||
/// </summary>
|
||||
[ExcelColumnName("手机号")]
|
||||
[ExcelHeader("手机号")]
|
||||
public string? Mobile { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 裁判角色(可空,如:主裁/难度裁判/表现裁判)
|
||||
/// </summary>
|
||||
[ExcelColumnName("裁判角色")]
|
||||
[ExcelHeader("裁判角色")]
|
||||
public string? RoleText { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 裁判等级(可空,如:国际级/国家级/一级/二级/三级)
|
||||
/// </summary>
|
||||
[ExcelColumnName("裁判等级")]
|
||||
[ExcelHeader("裁判等级")]
|
||||
public string? LevelText { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 负责场地(可空,对应出场秩序中的场地名称)
|
||||
/// </summary>
|
||||
[ExcelColumnName("负责场地")]
|
||||
[ExcelHeader("负责场地")]
|
||||
public string? VenueName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 负责项目(可空)
|
||||
/// </summary>
|
||||
[ExcelColumnName("负责项目")]
|
||||
[ExcelHeader("负责项目")]
|
||||
public string? ProjectName { get; set; }
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
using MiniExcelLibs;
|
||||
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;
|
||||
|
||||
@ -99,8 +99,8 @@ public class OrderBookService : IDynamicApiController, ITransient
|
||||
throw Oops.Bah("请上传秩序册文件");
|
||||
|
||||
var ext = Path.GetExtension(file.FileName)?.ToLowerInvariant();
|
||||
if (ext != ".xlsx")
|
||||
throw Oops.Bah("仅支持 .xlsx 格式的秩序册文件");
|
||||
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))
|
||||
@ -115,8 +115,10 @@ public class OrderBookService : IDynamicApiController, ITransient
|
||||
try
|
||||
{
|
||||
using var stream = file.OpenReadStream();
|
||||
entryRows = stream.Query<OrderBookEntryRow>(sheetName: EntrySheetName).ToList();
|
||||
refereeRows = stream.Query<OrderBookRefereeRow>(sheetName: RefereeSheetName).ToList();
|
||||
// NPOI 统一读取,WorkbookFactory 按文件头自动识别 .xls/.xlsx
|
||||
using var workbook = NpoiExcelReader.CreateWorkbook(stream);
|
||||
entryRows = NpoiExcelReader.ReadSheet<OrderBookEntryRow>(workbook, EntrySheetName);
|
||||
refereeRows = NpoiExcelReader.ReadSheet<OrderBookRefereeRow>(workbook, RefereeSheetName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@ -88,6 +88,10 @@ public class UnitAccountService : BaseService<SysUser>, ITransient
|
||||
public async Task Add(AddUnitAccountInput input)
|
||||
{
|
||||
EnsureUnitPlatform(input.Platform);
|
||||
// 规范化文本输入,避免首尾空格导致账号查重与登录不一致
|
||||
input.Account = input.Account.Trim();
|
||||
input.UnitName = input.UnitName.Trim();
|
||||
input.ContactName = input.ContactName.Trim();
|
||||
// 账号是全局登录标识,需在整张 SysUser 表内查重
|
||||
if (await _repository.IsAnyAsync(x => x.Account == input.Account))
|
||||
{
|
||||
@ -133,6 +137,10 @@ public class UnitAccountService : BaseService<SysUser>, ITransient
|
||||
public async Task Update(UpdateUnitAccountInput input)
|
||||
{
|
||||
var user = await GetUnitAccountAsync(input.Id);
|
||||
// 规范化文本输入,避免首尾空格导致账号查重与登录不一致
|
||||
input.Account = input.Account.Trim();
|
||||
input.UnitName = input.UnitName.Trim();
|
||||
input.ContactName = input.ContactName.Trim();
|
||||
|
||||
if (await _repository.IsAnyAsync(x => x.Account == input.Account && x.Id != input.Id))
|
||||
{
|
||||
|
||||
@ -73,6 +73,8 @@ public class SysUserService : BaseService<SysUser>, ITransient
|
||||
// 构造基本查询
|
||||
var query = _repository.AsQueryable()
|
||||
.Where(x => x.Id > 1) // 排除超级管理员或特定ID
|
||||
// 权限管理中的“报名平台”只管理后台账号;单位/打分账号由对应平台页签管理。
|
||||
.Where(x => x.UnitPlatform == UnitAccountPlatform.EventSystem)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(dto.Name), x => x.Name.Contains(dto.Name))
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(dto.Account), x => x.Account.Contains(dto.Account))
|
||||
.WhereIF(dto.StartTime.HasValue, x => x.CreatedTime >= dto.StartTime.Value)
|
||||
@ -141,6 +143,9 @@ public class SysUserService : BaseService<SysUser>, ITransient
|
||||
[DisplayName("添加系统用户")]
|
||||
public async Task AddUser(AddSysUserInput dto)
|
||||
{
|
||||
// 规范化文本输入,避免首尾空格导致账号查重与登录不一致
|
||||
dto.Account = dto.Account.Trim();
|
||||
dto.Name = dto.Name.Trim();
|
||||
// 校验账号是否已存在
|
||||
if (await _repository.IsAnyAsync(x => x.Account == dto.Account))
|
||||
throw new InvalidOperationException($"账号 {dto.Account} 已存在");
|
||||
@ -162,6 +167,8 @@ public class SysUserService : BaseService<SysUser>, ITransient
|
||||
user.Id = _idGenerator.NextId();
|
||||
string encode = _idGenerator.Encode(user.Id);
|
||||
user.Password = MD5Encryption.Encrypt(encode + dto.Password);
|
||||
// 权限管理-账号管理只创建报名平台后台账号
|
||||
user.UnitPlatform = UnitAccountPlatform.EventSystem;
|
||||
user.CreatedTime = DateTime.Now; // 确保设置创建时间
|
||||
|
||||
// 处理角色关联
|
||||
@ -191,8 +198,10 @@ public class SysUserService : BaseService<SysUser>, ITransient
|
||||
[UnitOfWork, HttpPut("user")]
|
||||
public async Task UpdateUser(UpdateSysUserInput dto)
|
||||
{
|
||||
var user = await _repository.GetByIdAsync(dto.Id);
|
||||
if (user == null) throw new InvalidOperationException("用户不存在");
|
||||
var user = await GetEventSystemUserAsync(dto.Id);
|
||||
// 规范化文本输入,避免首尾空格导致账号查重与登录不一致
|
||||
dto.Account = dto.Account.Trim();
|
||||
dto.Name = dto.Name.Trim();
|
||||
|
||||
// 校验账号是否重复(排除自己)
|
||||
if (await _repository.IsAnyAsync(x => x.Account == dto.Account && x.Id != dto.Id))
|
||||
@ -253,6 +262,7 @@ public class SysUserService : BaseService<SysUser>, ITransient
|
||||
[HttpDelete]
|
||||
public override async Task Delete(KeyDto dto)
|
||||
{
|
||||
await GetEventSystemUserAsync(dto.Id);
|
||||
await base.Delete(dto);
|
||||
await _easyCachingProvider.RemoveByPrefixAsync(CacheConst.PermissionKey);
|
||||
}
|
||||
@ -286,6 +296,7 @@ public class SysUserService : BaseService<SysUser>, ITransient
|
||||
[HttpGet("detail")]
|
||||
public async Task<UpdateSysUserInput> Detail([FromQuery] long id)
|
||||
{
|
||||
await GetEventSystemUserAsync(id);
|
||||
return await _repository.AsQueryable().Where(x => x.Id == id)
|
||||
.Select(x => new UpdateSysUserInput()
|
||||
{
|
||||
@ -305,6 +316,7 @@ public class SysUserService : BaseService<SysUser>, ITransient
|
||||
[HttpPost("reset")]
|
||||
public async Task Reset(SysUserResetPasswordInput dto)
|
||||
{
|
||||
await GetEventSystemUserAsync(dto.Id);
|
||||
string encrypt = MD5Encryption.Encrypt(_idGenerator.Encode(dto.Id) + dto.Password);
|
||||
await _repository.UpdateAsync(x => new SysUser()
|
||||
{
|
||||
@ -312,6 +324,31 @@ public class SysUserService : BaseService<SysUser>, ITransient
|
||||
}, x => x.Id == dto.Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 修改报名平台后台账号状态。
|
||||
/// </summary>
|
||||
[DisplayName("修改报名平台账号状态")]
|
||||
[HttpPost("setStatus")]
|
||||
public override async Task SetStatus(AvailabilityDto dto)
|
||||
{
|
||||
await GetEventSystemUserAsync(dto.Id);
|
||||
await base.SetStatus(dto);
|
||||
await _easyCachingProvider.RemoveByPrefixAsync(CacheConst.PermissionKey);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验目标账号为报名平台后台账号,拦截超级管理员及其他平台账号,防止跨平台误操作
|
||||
/// </summary>
|
||||
private async Task<SysUser> GetEventSystemUserAsync(long id)
|
||||
{
|
||||
if (id <= 1) throw Oops.Bah("超级管理员账号不允许在此操作");
|
||||
var user = await _repository.GetByIdAsync(id);
|
||||
if (user == null) throw Oops.Bah("报名平台账号不存在");
|
||||
if (user.UnitPlatform != UnitAccountPlatform.EventSystem)
|
||||
throw Oops.Bah("该账号不属于报名平台,请切换到对应平台管理");
|
||||
return user;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前登录用户的信息
|
||||
/// </summary>
|
||||
|
||||
@ -1,5 +1,52 @@
|
||||
namespace YD_Event.Application.Workbench.Dtos;
|
||||
|
||||
/// <summary>
|
||||
/// 工作台赛事选择查询输入
|
||||
/// </summary>
|
||||
public class WorkbenchEventQueryInput : Pagination
|
||||
{
|
||||
/// <summary>
|
||||
/// 赛事名称(可选,模糊匹配)
|
||||
/// </summary>
|
||||
public string? EventName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 数据来源:registration-报名平台(默认),orderbook-秩序册
|
||||
/// </summary>
|
||||
public string SourceMode { get; set; } = "registration";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 工作台赛事选择项
|
||||
/// </summary>
|
||||
public class WorkbenchEventDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 赛事ID
|
||||
/// </summary>
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 赛事名称
|
||||
/// </summary>
|
||||
public string EventName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 赛事状态
|
||||
/// </summary>
|
||||
public int EventStatus { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 比赛开始时间
|
||||
/// </summary>
|
||||
public DateTime? MatchStartTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 比赛结束时间
|
||||
/// </summary>
|
||||
public DateTime? MatchEndTime { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 工作台场次列表查询输入
|
||||
/// </summary>
|
||||
@ -36,6 +83,31 @@ public class WorkbenchSessionQueryInput
|
||||
/// </summary>
|
||||
public class WorkbenchSessionDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 赛事ID
|
||||
/// </summary>
|
||||
public long EventId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 场地ID
|
||||
/// </summary>
|
||||
public long VenueId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 赛事项目ID
|
||||
/// </summary>
|
||||
public long EventProjectId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 项目组别ID(多组别混合场次时为空)
|
||||
/// </summary>
|
||||
public long? EventProjectGroupId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 计分规则编码,用于前端按规则展示评分维度
|
||||
/// </summary>
|
||||
public string ScoreRuleCode { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 场次ID
|
||||
/// </summary>
|
||||
@ -168,6 +240,11 @@ public class WorkbenchEntryDetailDto
|
||||
/// </summary>
|
||||
public string? TeamName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 参赛单位名称
|
||||
/// </summary>
|
||||
public string? UnitName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 组别名称
|
||||
/// </summary>
|
||||
@ -359,6 +436,62 @@ public class WorkbenchJudgeScoreDto
|
||||
/// 是否存在待处理的修正申请
|
||||
/// </summary>
|
||||
public bool HasPendingCorrection { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 评分单的动态评分项明细快照
|
||||
/// </summary>
|
||||
public List<WorkbenchScoreItemDto> ScoreItems { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 工作台评分项详情
|
||||
/// </summary>
|
||||
public class WorkbenchScoreItemDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 评分项编码
|
||||
/// </summary>
|
||||
public string ItemCode { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 评分项名称
|
||||
/// </summary>
|
||||
public string ItemName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 所属科目编码(可选)
|
||||
/// </summary>
|
||||
public string? SubjectCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 评分项类型
|
||||
/// </summary>
|
||||
public int ItemType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 计分维度
|
||||
/// </summary>
|
||||
public int ScoreDimension { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 计数值(计数类评分项,如次数、人数)
|
||||
/// </summary>
|
||||
public decimal? CountValue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 裁判录入值
|
||||
/// </summary>
|
||||
public decimal? InputValue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 小计得分
|
||||
/// </summary>
|
||||
public decimal Subtotal { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 排序号
|
||||
/// </summary>
|
||||
public int Sort { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -490,6 +623,36 @@ public class CurrentScreenCastDto
|
||||
/// </summary>
|
||||
public DateTime CastTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 赛事名称(大屏标题)
|
||||
/// </summary>
|
||||
public string EventName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 项目名称
|
||||
/// </summary>
|
||||
public string ProjectName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 计分规则编码
|
||||
/// </summary>
|
||||
public string ScoreRuleCode { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 组别名称
|
||||
/// </summary>
|
||||
public string GroupName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 场地名称
|
||||
/// </summary>
|
||||
public string VenueName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 场次编号
|
||||
/// </summary>
|
||||
public string SessionNo { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 投屏内容(JSON格式)
|
||||
/// </summary>
|
||||
@ -698,6 +861,8 @@ public class CreateRematchInput
|
||||
/// <summary>
|
||||
/// 重赛原因
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "重赛原因不能为空")]
|
||||
[StringLength(200, MinimumLength = 2, ErrorMessage = "重赛原因长度必须为2-200个字符")]
|
||||
public string RematchReason { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
@ -735,6 +900,8 @@ public class VoidResultInput
|
||||
/// <summary>
|
||||
/// 作废原因
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "作废原因不能为空")]
|
||||
[StringLength(200, MinimumLength = 2, ErrorMessage = "作废原因长度必须为2-200个字符")]
|
||||
public string VoidReason { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
@ -760,7 +927,9 @@ public class BatchConfirmResultInput
|
||||
public long SessionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 成绩ID列表(可选,不传则确认该场次所有待确认成绩)
|
||||
/// 成绩ID列表(必须明确提交选中的成绩)
|
||||
/// </summary>
|
||||
public List<long>? ResultIds { get; set; }
|
||||
[Required(ErrorMessage = "请选择需要确认的成绩")]
|
||||
[MinLength(1, ErrorMessage = "请选择至少一条待确认成绩")]
|
||||
public List<long> ResultIds { get; set; } = new();
|
||||
}
|
||||
|
||||
@ -15,15 +15,25 @@ public class ResultExportQueryInput
|
||||
/// </summary>
|
||||
public long? EventProjectId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 场次ID(可选)
|
||||
/// </summary>
|
||||
public long? SessionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 关键词(可选):姓名、队名或出场编号模糊匹配
|
||||
/// </summary>
|
||||
public string? Keyword { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 组别名称(可选,模糊匹配)
|
||||
/// </summary>
|
||||
public string? GroupName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 成绩状态(可选,默认20-已确认;0-计算中,10-待复核,20-已确认,90-已作废)
|
||||
/// 成绩状态(可选,不传则导出全部状态;0-计算中,10-待复核,20-已确认,30-已投屏,40-已更正,90-已作废)
|
||||
/// </summary>
|
||||
public int? ResultStatus { get; set; } = 20;
|
||||
public int? ResultStatus { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否包含被取消比赛资格(DQ)的记录,默认不包含
|
||||
|
||||
@ -29,9 +29,11 @@ public class WorkbenchController : IDynamicApiController
|
||||
private readonly ISqlSugarRepository<Core.Entities.AppUser> _userRepository;
|
||||
private readonly ISqlSugarRepository<Core.Entities.Referee> _refereeRepository;
|
||||
private readonly ISqlSugarRepository<Core.Entities.Project> _projectRepository;
|
||||
private readonly ISqlSugarRepository<Core.Entities.ScoreRule> _scoreRuleRepository;
|
||||
private readonly ISqlSugarRepository<EventJudgeScoreDetail> _scoreDetailRepository;
|
||||
private readonly ISqlSugarRepository<SysOperationLog> _operationLogRepository;
|
||||
private readonly ISqlSugarRepository<SysUser> _sysUserRepository;
|
||||
private readonly ISqlSugarRepository<MerchantCertification> _merchantCertificationRepository;
|
||||
private readonly AuthManager _authManager;
|
||||
private readonly IIdGenerator _idGenerator;
|
||||
|
||||
@ -53,9 +55,11 @@ public class WorkbenchController : IDynamicApiController
|
||||
ISqlSugarRepository<Core.Entities.AppUser> userRepository,
|
||||
ISqlSugarRepository<Core.Entities.Referee> refereeRepository,
|
||||
ISqlSugarRepository<Core.Entities.Project> projectRepository,
|
||||
ISqlSugarRepository<Core.Entities.ScoreRule> scoreRuleRepository,
|
||||
ISqlSugarRepository<EventJudgeScoreDetail> scoreDetailRepository,
|
||||
ISqlSugarRepository<SysOperationLog> operationLogRepository,
|
||||
ISqlSugarRepository<SysUser> sysUserRepository,
|
||||
ISqlSugarRepository<MerchantCertification> merchantCertificationRepository,
|
||||
AuthManager authManager,
|
||||
IIdGenerator idGenerator)
|
||||
{
|
||||
@ -76,15 +80,45 @@ public class WorkbenchController : IDynamicApiController
|
||||
_userRepository = userRepository;
|
||||
_refereeRepository = refereeRepository;
|
||||
_projectRepository = projectRepository;
|
||||
_scoreRuleRepository = scoreRuleRepository;
|
||||
_scoreDetailRepository = scoreDetailRepository;
|
||||
_operationLogRepository = operationLogRepository;
|
||||
_sysUserRepository = sysUserRepository;
|
||||
_merchantCertificationRepository = merchantCertificationRepository;
|
||||
_authManager = authManager;
|
||||
_idGenerator = idGenerator;
|
||||
}
|
||||
|
||||
#region 场次管理
|
||||
|
||||
/// <summary>
|
||||
/// 获取可进入打分工作台的赛事列表
|
||||
/// </summary>
|
||||
[HttpGet("workbench/events")]
|
||||
public async Task<PageResult<WorkbenchEventDto>> GetEvents([FromQuery] WorkbenchEventQueryInput input)
|
||||
{
|
||||
var mode = input.SourceMode?.Trim().ToLowerInvariant();
|
||||
var query = _eventRepository.AsQueryable()
|
||||
.Where(e => !e.DeleteMark)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.EventName), e => e.EventName.Contains(input.EventName!.Trim()));
|
||||
|
||||
// 报名平台入口只展示进行中/已结束的赛事;秩序册入口不限制赛事状态
|
||||
if (mode == "registration")
|
||||
query = query.Where(e => e.EventStatus == Core.Enum.EventStatus.Ongoing || e.EventStatus == Core.Enum.EventStatus.Ended);
|
||||
else if (mode != "orderbook")
|
||||
throw Oops.Bah("数据来源只能为registration或orderbook");
|
||||
|
||||
return await query.OrderByDescending(e => e.MatchStartTime)
|
||||
.Select(e => new WorkbenchEventDto
|
||||
{
|
||||
Id = e.Id,
|
||||
EventName = e.EventName,
|
||||
EventStatus = (int)e.EventStatus,
|
||||
MatchStartTime = e.MatchStartTime,
|
||||
MatchEndTime = e.MatchEndTime
|
||||
}).ToPagedListAsync(input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取工作台场次列表
|
||||
/// </summary>
|
||||
@ -121,6 +155,7 @@ public class WorkbenchController : IDynamicApiController
|
||||
s.VenueId,
|
||||
s.EventProjectId,
|
||||
s.EventProjectGroupId,
|
||||
s.ScoreRuleId,
|
||||
s.StartTime,
|
||||
s.EndTime,
|
||||
s.SessionStatus,
|
||||
@ -163,6 +198,13 @@ public class WorkbenchController : IDynamicApiController
|
||||
.Select(g => new { g.Id, g.GroupName })
|
||||
.ToListAsync();
|
||||
|
||||
// 批量查出场次绑定的计分规则编码,供前端按规则渲染评分维度
|
||||
var ruleIds = sessions.Select(s => s.ScoreRuleId).Distinct().ToList();
|
||||
var ruleDict = (await _scoreRuleRepository.AsQueryable()
|
||||
.Where(r => !r.DeleteMark && ruleIds.Contains(r.Id))
|
||||
.Select(r => new { r.Id, r.RuleCode })
|
||||
.ToListAsync()).ToDictionary(r => r.Id, r => r.RuleCode);
|
||||
|
||||
var entryCounts = await _entryRepository.AsQueryable()
|
||||
.Where(e => !e.DeleteMark && sessionIds.Contains(e.SessionId))
|
||||
.GroupBy(e => e.SessionId)
|
||||
@ -176,7 +218,9 @@ public class WorkbenchController : IDynamicApiController
|
||||
|
||||
var resultCounts = await _resultRepository.AsQueryable()
|
||||
.InnerJoin<EventSessionEntry>((r, e) => r.SessionEntryId == e.Id)
|
||||
.Where((r, e) => !r.DeleteMark && !e.DeleteMark && sessionIds.Contains(e.SessionId) && r.ResultStatus == 20)
|
||||
.Where((r, e) => !r.DeleteMark && !e.DeleteMark && sessionIds.Contains(e.SessionId) &&
|
||||
// 已确认(20)、已投屏(30)、已更正(40)均计入已完成成绩数
|
||||
(r.ResultStatus == 20 || r.ResultStatus == 30 || r.ResultStatus == 40))
|
||||
.GroupBy((r, e) => e.SessionId)
|
||||
.Select((r, e) => new
|
||||
{
|
||||
@ -195,6 +239,11 @@ public class WorkbenchController : IDynamicApiController
|
||||
return sessions.Select(s => new WorkbenchSessionDto
|
||||
{
|
||||
SessionId = s.Id,
|
||||
EventId = s.EventId,
|
||||
VenueId = s.VenueId,
|
||||
EventProjectId = s.EventProjectId,
|
||||
EventProjectGroupId = s.EventProjectGroupId,
|
||||
ScoreRuleCode = ruleDict.GetValueOrDefault(s.ScoreRuleId, string.Empty),
|
||||
SessionNo = s.SessionNo,
|
||||
SessionName = s.SessionName,
|
||||
EventName = eventDict.GetValueOrDefault(s.EventId, ""),
|
||||
@ -269,10 +318,19 @@ public class WorkbenchController : IDynamicApiController
|
||||
r.ResultVersion,
|
||||
r.ReviewUserId,
|
||||
r.ReviewTime,
|
||||
r.Rank
|
||||
r.Rank,
|
||||
r.MerchantCertificationId
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
// 批量查询成绩关联的参赛单位名称(单位信息存于商户认证表)
|
||||
var merchantIds = results.Where(r => r.MerchantCertificationId.HasValue)
|
||||
.Select(r => r.MerchantCertificationId!.Value).Distinct().ToList();
|
||||
var merchantNames = (await _merchantCertificationRepository.AsQueryable()
|
||||
.Where(m => merchantIds.Contains(m.Id) && !m.DeleteMark)
|
||||
.Select(m => new { m.Id, m.MerchantName }).ToListAsync())
|
||||
.ToDictionary(m => m.Id, m => m.MerchantName);
|
||||
|
||||
if (input.ResultStatus.HasValue)
|
||||
{
|
||||
var filteredEntryIds = results.Where(r => r.ResultStatus == input.ResultStatus.Value).Select(r => r.SessionEntryId).ToHashSet();
|
||||
@ -285,6 +343,25 @@ public class WorkbenchController : IDynamicApiController
|
||||
.OrderBy(s => s.JudgeRole)
|
||||
.ToListAsync();
|
||||
|
||||
// 批量查询当前评分单的动态评分项明细,按评分单分组挂到各裁判评分下
|
||||
var sheetIds = sheets.Select(s => s.Id).ToList();
|
||||
var scoreItems = await _scoreDetailRepository.AsQueryable()
|
||||
.Where(d => !d.DeleteMark && sheetIds.Contains(d.ScoreSheetId))
|
||||
.OrderBy(d => d.Sort)
|
||||
.Select(d => new
|
||||
{
|
||||
d.ScoreSheetId,
|
||||
d.ItemCode,
|
||||
d.ItemName,
|
||||
d.SubjectCode,
|
||||
d.ItemType,
|
||||
d.ScoreDimension,
|
||||
d.CountValue,
|
||||
d.InputValue,
|
||||
d.Subtotal,
|
||||
d.Sort
|
||||
}).ToListAsync();
|
||||
|
||||
var refereeIds = sheets.Select(s => s.RefereeId).Distinct().ToList();
|
||||
var refereeMap = (await _refereeRepository.AsQueryable()
|
||||
.Where(r => refereeIds.Contains(r.Id))
|
||||
@ -312,6 +389,9 @@ public class WorkbenchController : IDynamicApiController
|
||||
PlayOrder = e.PlayOrder,
|
||||
ParticipantName = e.ParticipantName,
|
||||
TeamName = e.TeamName,
|
||||
UnitName = result?.MerchantCertificationId is long merchantId
|
||||
? merchantNames.GetValueOrDefault(merchantId)
|
||||
: null,
|
||||
GroupName = e.GroupName,
|
||||
EntryStatus = e.EntryStatus,
|
||||
EntryStatusName = GetEntryStatusName(e.EntryStatus),
|
||||
@ -350,7 +430,19 @@ public class WorkbenchController : IDynamicApiController
|
||||
TotalScore = sheet.TotalScore,
|
||||
IsDisqualified = sheet.IsDisqualified,
|
||||
SubmittedTime = sheet.SubmittedTime,
|
||||
HasPendingCorrection = pendingCorrectionSet.Contains(sheet.Id)
|
||||
HasPendingCorrection = pendingCorrectionSet.Contains(sheet.Id),
|
||||
ScoreItems = scoreItems.Where(d => d.ScoreSheetId == sheet.Id).Select(d => new WorkbenchScoreItemDto
|
||||
{
|
||||
ItemCode = d.ItemCode,
|
||||
ItemName = d.ItemName,
|
||||
SubjectCode = d.SubjectCode,
|
||||
ItemType = d.ItemType,
|
||||
ScoreDimension = d.ScoreDimension,
|
||||
CountValue = d.CountValue,
|
||||
InputValue = d.InputValue,
|
||||
Subtotal = d.Subtotal,
|
||||
Sort = d.Sort
|
||||
}).ToList()
|
||||
}).ToList()
|
||||
};
|
||||
}).ToList();
|
||||
@ -364,25 +456,55 @@ public class WorkbenchController : IDynamicApiController
|
||||
/// 投出成绩到大屏
|
||||
/// </summary>
|
||||
[HttpPost("workbench/screencast")]
|
||||
[UnitOfWork]
|
||||
public async Task<ResultScreenCastOutput> CastResult([FromBody] ResultScreenCastInput input)
|
||||
{
|
||||
var currentUserId = _authManager.UserId;
|
||||
var now = DateTime.Now;
|
||||
|
||||
// 两种投屏类型都先校验赛事存在且未删除
|
||||
if (input.EventId <= 0 || !await _eventRepository.IsAnyAsync(e => e.Id == input.EventId && !e.DeleteMark))
|
||||
throw Oops.Bah("赛事不存在或已删除");
|
||||
|
||||
if (input.CastType == 1)
|
||||
{
|
||||
if (!input.SessionEntryId.HasValue || !input.EventResultId.HasValue)
|
||||
throw Oops.Bah("投出单个成绩时,必须提供出场记录ID和成绩ID");
|
||||
if (!input.SessionId.HasValue || !input.SessionEntryId.HasValue || !input.EventResultId.HasValue)
|
||||
throw Oops.Bah("投出单个成绩时,必须提供场次ID、出场记录ID和成绩ID");
|
||||
|
||||
var entry = await _entryRepository.GetFirstAsync(e => e.Id == input.SessionEntryId.Value && !e.DeleteMark)
|
||||
?? throw Oops.Bah("出场记录不存在");
|
||||
|
||||
var result = await _resultRepository.GetFirstAsync(r => r.Id == input.EventResultId.Value && !r.DeleteMark)
|
||||
?? throw Oops.Bah("成绩记录不存在");
|
||||
var previousResultStatus = result.ResultStatus;
|
||||
|
||||
var session = await _sessionRepository.GetByIdAsync(entry.SessionId)
|
||||
?? throw Oops.Bah("场次不存在");
|
||||
|
||||
// 投屏前校验归属关系与成绩状态,防止跨赛事/跨场次或过期版本被投出
|
||||
if (session.DeleteMark || session.Id != input.SessionId.Value || session.EventId != input.EventId ||
|
||||
(input.VenueId.HasValue && session.VenueId != input.VenueId.Value))
|
||||
throw Oops.Bah("赛事、场地、场次和出场记录不匹配");
|
||||
if (result.EventId != input.EventId || result.SessionId != session.Id || result.SessionEntryId != entry.Id)
|
||||
throw Oops.Bah("成绩记录与当前赛事出场记录不匹配");
|
||||
if (result.ResultStatus is not (20 or 30 or 40))
|
||||
throw Oops.Bah("只有已确认的有效成绩可以投屏");
|
||||
if (result.IsDisqualified)
|
||||
throw Oops.Bah("DQ成绩不能投屏");
|
||||
if (input.ResultVersion.HasValue && input.ResultVersion.Value != result.ResultVersion)
|
||||
throw Oops.Bah("成绩已产生新版本,请刷新后重新投屏");
|
||||
|
||||
// 该出场应到裁判的评分单必须全部提交,否则成绩未定稿,不允许投屏
|
||||
var expectedJudgeCount = await _assignmentRepository.AsQueryable()
|
||||
.Where(a => !a.DeleteMark && a.SessionId == session.Id && a.Status == Core.Enum.AvailabilityStatus.Enable)
|
||||
.CountAsync();
|
||||
var submittedJudgeCount = await _sheetRepository.AsQueryable()
|
||||
.Where(s => !s.DeleteMark && s.IsCurrent && s.SessionEntryId == entry.Id &&
|
||||
s.SheetStatus >= 10 && s.SheetStatus < 90)
|
||||
.Select(s => s.JudgeAssignmentId).Distinct().CountAsync();
|
||||
if (expectedJudgeCount <= 0 || submittedJudgeCount < expectedJudgeCount)
|
||||
throw Oops.Bah("相关裁判评分单尚未全部提交,不能投屏");
|
||||
|
||||
var payload = new
|
||||
{
|
||||
EntryNo = entry.EntryNo,
|
||||
@ -399,6 +521,7 @@ public class WorkbenchController : IDynamicApiController
|
||||
Rank = result.Rank
|
||||
};
|
||||
|
||||
// 同赛事同场地此前投出中的记录统一置为“已被新投屏替换(20)”,保证大屏只展示最新一条
|
||||
await _screenCastRepository.AsUpdateable()
|
||||
.SetColumns(sc => new EventResultScreenCast { CastStatus = 20 },
|
||||
appendColumnsByDataFilter: true)
|
||||
@ -428,6 +551,14 @@ public class WorkbenchController : IDynamicApiController
|
||||
|
||||
await _screenCastRepository.InsertAsync(screenCast);
|
||||
|
||||
// 投屏成功后成绩置为“已投屏(30)”,同步当前版本状态并记录审计
|
||||
result.ResultStatus = 30;
|
||||
result.UpdatedUserId = currentUserId;
|
||||
result.UpdatedTime = now;
|
||||
await _resultRepository.UpdateAsync(result);
|
||||
await SyncResultVersionStatusAsync(result, 30, currentUserId, now);
|
||||
await WriteAuditAsync("投出单人成绩", input.EventId, result.Id, previousResultStatus, 30, sessionId: session.Id);
|
||||
|
||||
return new ResultScreenCastOutput
|
||||
{
|
||||
ScreenCastId = screenCast.Id,
|
||||
@ -441,6 +572,15 @@ public class WorkbenchController : IDynamicApiController
|
||||
if (!input.EventProjectId.HasValue || !input.EventProjectGroupId.HasValue)
|
||||
throw Oops.Bah("投出排名时,必须提供项目ID和组别ID");
|
||||
|
||||
// 投屏排名前校验项目归属当前赛事、组别归属该项目
|
||||
var eventProject = await _eventProjectRepository.GetFirstAsync(ep =>
|
||||
ep.Id == input.EventProjectId.Value && ep.EventId == input.EventId && !ep.DeleteMark)
|
||||
?? throw Oops.Bah("所选项目不属于当前赛事");
|
||||
_ = eventProject;
|
||||
if (!await _groupRepository.IsAnyAsync(g => g.Id == input.EventProjectGroupId.Value &&
|
||||
g.EventProjectId == input.EventProjectId.Value && !g.DeleteMark))
|
||||
throw Oops.Bah("所选组别不属于当前赛事项目");
|
||||
|
||||
var results = await _resultRepository.AsQueryable()
|
||||
.InnerJoin<EventSessionEntry>((r, e) => r.SessionEntryId == e.Id)
|
||||
.InnerJoin<EventSession>((r, e, s) => e.SessionId == s.Id)
|
||||
@ -451,11 +591,14 @@ public class WorkbenchController : IDynamicApiController
|
||||
e.GroupName == SqlFunc.Subqueryable<EventProjectGroup>()
|
||||
.Where(g => g.Id == input.EventProjectGroupId.Value)
|
||||
.Select(g => g.GroupName)) &&
|
||||
r.ResultStatus >= 20)
|
||||
// 只投屏有效成绩(已确认20/已投屏30/已更正40),排除DQ成绩
|
||||
(r.ResultStatus == 20 || r.ResultStatus == 30 || r.ResultStatus == 40) &&
|
||||
!r.IsDisqualified)
|
||||
.OrderBy((r, e, s) => r.Rank)
|
||||
.Select((r, e, s) => new
|
||||
{
|
||||
r.Id,
|
||||
r.ResultVersion,
|
||||
e.EntryNo,
|
||||
e.ParticipantName,
|
||||
e.TeamName,
|
||||
@ -467,6 +610,9 @@ public class WorkbenchController : IDynamicApiController
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
if (!results.Any())
|
||||
throw Oops.Bah("当前项目组别没有可投屏的已确认成绩");
|
||||
|
||||
var payload = results.Select(r => new
|
||||
{
|
||||
r.EntryNo,
|
||||
@ -479,6 +625,7 @@ public class WorkbenchController : IDynamicApiController
|
||||
r.IsDisqualified
|
||||
}).ToList();
|
||||
|
||||
// 同赛事同场地此前投出中的记录统一置为“已被新投屏替换(20)”,保证大屏只展示最新一条
|
||||
await _screenCastRepository.AsUpdateable()
|
||||
.SetColumns(sc => new EventResultScreenCast { CastStatus = 20 },
|
||||
appendColumnsByDataFilter: true)
|
||||
@ -508,6 +655,34 @@ public class WorkbenchController : IDynamicApiController
|
||||
|
||||
await _screenCastRepository.InsertAsync(screenCast);
|
||||
|
||||
// 组别排名投屏后,组内全部成绩置为“已投屏(30)”
|
||||
var rankedResultIds = results.Select(r => r.Id).ToList();
|
||||
await _resultRepository.AsUpdateable()
|
||||
.SetColumns(r => new EventResult
|
||||
{
|
||||
ResultStatus = 30,
|
||||
UpdatedUserId = currentUserId,
|
||||
UpdatedTime = now
|
||||
}, appendColumnsByDataFilter: true)
|
||||
.Where(r => rankedResultIds.Contains(r.Id))
|
||||
.ExecuteCommandAsync();
|
||||
var rankedVersions = await _resultVersionRepository.AsQueryable()
|
||||
.Where(v => !v.DeleteMark && rankedResultIds.Contains(v.EventResultId))
|
||||
.ToListAsync();
|
||||
// 只同步各成绩的当前版本,历史版本保留原状态留痕
|
||||
var currentRankedVersions = rankedVersions
|
||||
.Where(v => results.Any(r => r.Id == v.EventResultId && r.ResultVersion == v.ResultVersion))
|
||||
.ToList();
|
||||
foreach (var version in currentRankedVersions)
|
||||
{
|
||||
version.ResultStatus = 30;
|
||||
version.UpdatedUserId = currentUserId;
|
||||
version.UpdatedTime = now;
|
||||
}
|
||||
if (currentRankedVersions.Count > 0)
|
||||
await _resultVersionRepository.UpdateRangeAsync(currentRankedVersions);
|
||||
await WriteAuditAsync("投出组别排名", input.EventId, input.EventProjectGroupId.Value, null, 30);
|
||||
|
||||
return new ResultScreenCastOutput
|
||||
{
|
||||
ScreenCastId = screenCast.Id,
|
||||
@ -539,12 +714,15 @@ public class WorkbenchController : IDynamicApiController
|
||||
screenCast.UpdatedTime = DateTime.Now;
|
||||
|
||||
await _screenCastRepository.UpdateAsync(screenCast);
|
||||
await WriteAuditAsync("撤回投屏", screenCast.EventId, screenCast.Id, 0, 10, sessionId: screenCast.SessionId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前投屏内容
|
||||
/// </summary>
|
||||
[HttpGet("workbench/screencast/current")]
|
||||
// 大屏展示端无登录态,允许匿名拉取当前投屏内容
|
||||
[AllowAnonymous]
|
||||
public async Task<CurrentScreenCastDto?> GetCurrentScreenCast([FromQuery] GetCurrentScreenCastInput input)
|
||||
{
|
||||
var query = _screenCastRepository.AsQueryable()
|
||||
@ -560,12 +738,39 @@ public class WorkbenchController : IDynamicApiController
|
||||
if (screenCast == null)
|
||||
return null;
|
||||
|
||||
// 补全赛事、项目、组别、场地、场次与计分规则信息,供大屏标题区展示
|
||||
var eventInfo = await _eventRepository.GetByIdAsync(screenCast.EventId);
|
||||
var venue = screenCast.VenueId.HasValue
|
||||
? await _venueRepository.GetByIdAsync(screenCast.VenueId.Value)
|
||||
: null;
|
||||
var eventProject = screenCast.EventProjectId.HasValue
|
||||
? await _eventProjectRepository.GetByIdAsync(screenCast.EventProjectId.Value)
|
||||
: null;
|
||||
var project = eventProject != null
|
||||
? await _projectRepository.GetByIdAsync(eventProject.ProjectId)
|
||||
: null;
|
||||
var group = screenCast.EventProjectGroupId.HasValue
|
||||
? await _groupRepository.GetByIdAsync(screenCast.EventProjectGroupId.Value)
|
||||
: null;
|
||||
var session = screenCast.SessionId.HasValue
|
||||
? await _sessionRepository.GetByIdAsync(screenCast.SessionId.Value)
|
||||
: null;
|
||||
var rule = session != null
|
||||
? await _scoreRuleRepository.GetByIdAsync(session.ScoreRuleId)
|
||||
: null;
|
||||
|
||||
return new CurrentScreenCastDto
|
||||
{
|
||||
ScreenCastId = screenCast.Id,
|
||||
CastType = screenCast.CastType,
|
||||
CastStatus = screenCast.CastStatus,
|
||||
CastTime = screenCast.CastTime,
|
||||
EventName = eventInfo?.EventName ?? string.Empty,
|
||||
ProjectName = project?.Name ?? string.Empty,
|
||||
ScoreRuleCode = rule?.RuleCode ?? string.Empty,
|
||||
GroupName = group?.GroupName ?? string.Empty,
|
||||
VenueName = venue?.VenueName ?? string.Empty,
|
||||
SessionNo = session?.SessionNo ?? string.Empty,
|
||||
PayloadJson = screenCast.PayloadJson
|
||||
};
|
||||
}
|
||||
@ -692,6 +897,10 @@ public class WorkbenchController : IDynamicApiController
|
||||
Session = new WorkbenchSessionDto
|
||||
{
|
||||
SessionId = session.Id,
|
||||
EventId = session.EventId,
|
||||
VenueId = session.VenueId,
|
||||
EventProjectId = session.EventProjectId,
|
||||
EventProjectGroupId = session.EventProjectGroupId,
|
||||
SessionNo = session.SessionNo,
|
||||
SessionName = session.SessionName,
|
||||
EventName = eventInfo?.EventName ?? "",
|
||||
@ -780,6 +989,7 @@ public class WorkbenchController : IDynamicApiController
|
||||
/// 审批成绩修正申请
|
||||
/// </summary>
|
||||
[HttpPost("workbench/corrections/approve")]
|
||||
[UnitOfWork]
|
||||
public async Task ApproveCorrection([FromBody] ApproveScoreCorrectionInput input)
|
||||
{
|
||||
var currentUserId = _authManager.UserId;
|
||||
@ -802,7 +1012,10 @@ public class WorkbenchController : IDynamicApiController
|
||||
|
||||
if (input.IsApproved)
|
||||
{
|
||||
// 批准修正需向原裁判下发修正任务,原裁判分配已删除时无法继续
|
||||
var assignment = await _assignmentRepository.GetByIdAsync(correction.JudgeAssignmentId);
|
||||
if (assignment == null || assignment.DeleteMark)
|
||||
throw Oops.Bah("原裁判分配不存在,无法下发修正任务");
|
||||
|
||||
var task = new EventJudgeTask
|
||||
{
|
||||
@ -811,11 +1024,11 @@ public class WorkbenchController : IDynamicApiController
|
||||
SessionId = correction.SessionId,
|
||||
SessionEntryId = correction.SessionEntryId,
|
||||
JudgeAssignmentId = correction.JudgeAssignmentId,
|
||||
EventRefereeId = assignment?.EventRefereeId ?? 0,
|
||||
RefereeId = assignment?.RefereeId ?? 0,
|
||||
JudgeUserId = assignment?.JudgeUserId,
|
||||
JudgeRole = assignment?.JudgeRole ?? "",
|
||||
JudgeRoleType = assignment?.JudgeRoleType ?? "",
|
||||
EventRefereeId = assignment.EventRefereeId,
|
||||
RefereeId = assignment.RefereeId,
|
||||
JudgeUserId = assignment.JudgeUserId,
|
||||
JudgeRole = assignment.JudgeRole,
|
||||
JudgeRoleType = assignment.JudgeRoleType,
|
||||
TaskSnapshotJson = "{}",
|
||||
TaskType = 1,
|
||||
TaskStatus = 0,
|
||||
@ -828,7 +1041,7 @@ public class WorkbenchController : IDynamicApiController
|
||||
correction.CorrectionTaskId = task.Id;
|
||||
await _correctionRepository.UpdateAsync(correction);
|
||||
|
||||
if (assignment?.JudgeUserId != null)
|
||||
if (assignment.JudgeUserId != null)
|
||||
{
|
||||
var message = new EventJudgeMessage
|
||||
{
|
||||
@ -849,6 +1062,9 @@ public class WorkbenchController : IDynamicApiController
|
||||
await _messageRepository.InsertAsync(message);
|
||||
}
|
||||
}
|
||||
await WriteAuditAsync(input.IsApproved ? "批准成绩修正" : "驳回成绩修正",
|
||||
correction.EventId, correction.Id, 0, correction.CorrectionStatus,
|
||||
input.IsApproved ? correction.ApplyReason : input.ApproveRemark, correction.SessionId);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@ -859,6 +1075,7 @@ public class WorkbenchController : IDynamicApiController
|
||||
/// 确认成绩
|
||||
/// </summary>
|
||||
[HttpPost("workbench/results/confirm")]
|
||||
[UnitOfWork]
|
||||
public async Task ConfirmResult([FromBody] ConfirmResultInput input)
|
||||
{
|
||||
var currentUserId = _authManager.UserId;
|
||||
@ -873,19 +1090,26 @@ public class WorkbenchController : IDynamicApiController
|
||||
if (result.ResultStatus != 10)
|
||||
throw Oops.Bah("只能确认待复核状态的成绩");
|
||||
|
||||
result.ResultStatus = 20;
|
||||
// 当前版本由成绩修正产生时确认为“已更正(40)”,常规成绩确认为“已确认(20)”
|
||||
var currentVersion = await _resultVersionRepository.GetFirstAsync(v =>
|
||||
!v.DeleteMark && v.EventResultId == result.Id && v.ResultVersion == result.ResultVersion);
|
||||
var confirmedStatus = currentVersion?.ScoreCorrectionId.HasValue == true ? 40 : 20;
|
||||
result.ResultStatus = confirmedStatus;
|
||||
result.ReviewUserId = currentUserId;
|
||||
result.ReviewTime = now;
|
||||
result.UpdatedUserId = currentUserId;
|
||||
result.UpdatedTime = now;
|
||||
|
||||
await _resultRepository.UpdateAsync(result);
|
||||
await SyncResultVersionStatusAsync(result, confirmedStatus, currentUserId, now, currentVersion);
|
||||
await WriteAuditAsync("确认成绩", result.EventId, result.Id, 10, confirmedStatus, sessionId: result.SessionId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量确认成绩
|
||||
/// </summary>
|
||||
[HttpPost("workbench/results/batch-confirm")]
|
||||
[UnitOfWork]
|
||||
public async Task BatchConfirmResults([FromBody] BatchConfirmResultInput input)
|
||||
{
|
||||
var currentUserId = _authManager.UserId;
|
||||
@ -895,36 +1119,61 @@ public class WorkbenchController : IDynamicApiController
|
||||
.InnerJoin<EventSessionEntry>((r, e) => r.SessionEntryId == e.Id)
|
||||
.Where((r, e) => !r.DeleteMark && !e.DeleteMark && e.SessionId == input.SessionId && r.ResultStatus == 10);
|
||||
|
||||
if (input.ResultIds != null && input.ResultIds.Any())
|
||||
{
|
||||
query = query.Where((r, e) => input.ResultIds.Contains(r.Id));
|
||||
}
|
||||
// 必须显式提交选中的成绩,过滤非法Id并去重,防止误确认整场次
|
||||
var requestedIds = input.ResultIds.Where(id => id > 0).Distinct().ToList();
|
||||
if (!requestedIds.Any())
|
||||
throw Oops.Bah("请选择至少一条待确认成绩");
|
||||
query = query.Where((r, e) => requestedIds.Contains(r.Id));
|
||||
|
||||
var results = await query.Select((r, e) => r).ToListAsync();
|
||||
|
||||
if (!results.Any())
|
||||
throw Oops.Bah("没有可确认的成绩");
|
||||
if (results.Count != requestedIds.Count)
|
||||
throw Oops.Bah("选中成绩中包含非当前场次或非待复核数据,请刷新后重试");
|
||||
|
||||
// 批量取出当前版本,逐条同步确认状态(含修正产生的已更正40状态)
|
||||
var resultIds = results.Select(r => r.Id).ToList();
|
||||
var resultVersions = await _resultVersionRepository.AsQueryable()
|
||||
.Where(v => !v.DeleteMark && resultIds.Contains(v.EventResultId))
|
||||
.ToListAsync();
|
||||
var currentResultVersions = new List<EventResultVersion>();
|
||||
foreach (var result in results)
|
||||
{
|
||||
result.ResultStatus = 20;
|
||||
var version = resultVersions.FirstOrDefault(v =>
|
||||
v.EventResultId == result.Id && v.ResultVersion == result.ResultVersion);
|
||||
result.ResultStatus = version?.ScoreCorrectionId.HasValue == true ? 40 : 20;
|
||||
result.ReviewUserId = currentUserId;
|
||||
result.ReviewTime = now;
|
||||
result.UpdatedUserId = currentUserId;
|
||||
result.UpdatedTime = now;
|
||||
if (version != null)
|
||||
{
|
||||
version.ResultStatus = result.ResultStatus;
|
||||
version.UpdatedUserId = currentUserId;
|
||||
version.UpdatedTime = now;
|
||||
currentResultVersions.Add(version);
|
||||
}
|
||||
}
|
||||
|
||||
await _resultRepository.UpdateRangeAsync(results);
|
||||
if (currentResultVersions.Count > 0)
|
||||
await _resultVersionRepository.UpdateRangeAsync(currentResultVersions);
|
||||
await WriteAuditAsync("批量确认成绩", results[0].EventId, input.SessionId, 10,
|
||||
results.Any(r => r.ResultStatus == 40) ? "20/40" : "20",
|
||||
$"成绩数量:{results.Count}", input.SessionId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建重赛
|
||||
/// </summary>
|
||||
[HttpPost("workbench/entries/rematch")]
|
||||
[UnitOfWork]
|
||||
public async Task<CreateRematchOutput> CreateRematch([FromBody] CreateRematchInput input)
|
||||
{
|
||||
var currentUserId = _authManager.UserId;
|
||||
var now = DateTime.Now;
|
||||
var reason = NormalizeReason(input.RematchReason, "重赛原因");
|
||||
|
||||
var originalEntry = await _entryRepository.GetFirstAsync(e => e.Id == input.OriginalSessionEntryId && !e.DeleteMark)
|
||||
?? throw Oops.Bah("原出场记录不存在");
|
||||
@ -932,14 +1181,31 @@ public class WorkbenchController : IDynamicApiController
|
||||
var session = await _sessionRepository.GetByIdAsync(originalEntry.SessionId)
|
||||
?? throw Oops.Bah("场次不存在");
|
||||
|
||||
var maxPlayOrder = await _entryRepository.AsQueryable()
|
||||
.Where(e => !e.DeleteMark && e.SessionId == originalEntry.SessionId)
|
||||
// 已作废的出场不能重复发起重赛,避免生成多条重赛记录
|
||||
if (originalEntry.IsVoided)
|
||||
throw Oops.Bah("该出场记录已作废,不能重复创建重赛");
|
||||
|
||||
// 出场序号只在同组别内连续,重赛出场追加到本组别末尾
|
||||
var groupLastPlayOrder = await _entryRepository.AsQueryable()
|
||||
.Where(e => !e.DeleteMark && e.SessionId == originalEntry.SessionId && e.GroupName == originalEntry.GroupName)
|
||||
.MaxAsync(e => e.PlayOrder);
|
||||
|
||||
var newPlayOrder = maxPlayOrder + 1;
|
||||
var newPlayOrder = groupLastPlayOrder + 1;
|
||||
// 占用该序号及之后的同组别出场倒序后移一位,避免出场序号冲突
|
||||
var laterEntries = await _entryRepository.AsQueryable()
|
||||
.Where(e => !e.DeleteMark && e.SessionId == originalEntry.SessionId && e.PlayOrder >= newPlayOrder)
|
||||
.OrderByDescending(e => e.PlayOrder)
|
||||
.ToListAsync();
|
||||
foreach (var laterEntry in laterEntries)
|
||||
{
|
||||
laterEntry.PlayOrder++;
|
||||
laterEntry.UpdatedUserId = currentUserId;
|
||||
laterEntry.UpdatedTime = now;
|
||||
await _entryRepository.UpdateAsync(laterEntry);
|
||||
}
|
||||
|
||||
originalEntry.IsVoided = true;
|
||||
originalEntry.VoidReason = $"重赛原因:{input.RematchReason}";
|
||||
originalEntry.VoidReason = $"重赛原因:{reason}";
|
||||
originalEntry.UpdatedUserId = currentUserId;
|
||||
originalEntry.UpdatedTime = now;
|
||||
await _entryRepository.UpdateAsync(originalEntry);
|
||||
@ -951,6 +1217,7 @@ public class WorkbenchController : IDynamicApiController
|
||||
result.UpdatedUserId = currentUserId;
|
||||
result.UpdatedTime = now;
|
||||
await _resultRepository.UpdateAsync(result);
|
||||
await SyncResultVersionStatusAsync(result, 90, currentUserId, now);
|
||||
}
|
||||
|
||||
var newEntry = new EventSessionEntry
|
||||
@ -977,6 +1244,7 @@ public class WorkbenchController : IDynamicApiController
|
||||
};
|
||||
|
||||
await _entryRepository.InsertAsync(newEntry);
|
||||
await WriteAuditAsync("创建重赛", session.EventId, originalEntry.Id, "有效", "已作废并创建重赛", reason, session.Id);
|
||||
|
||||
return new CreateRematchOutput
|
||||
{
|
||||
@ -990,16 +1258,22 @@ public class WorkbenchController : IDynamicApiController
|
||||
/// 作废成绩
|
||||
/// </summary>
|
||||
[HttpPost("workbench/results/void")]
|
||||
[UnitOfWork]
|
||||
public async Task VoidResult([FromBody] VoidResultInput input)
|
||||
{
|
||||
var currentUserId = _authManager.UserId;
|
||||
var now = DateTime.Now;
|
||||
var reason = NormalizeReason(input.VoidReason, "作废原因");
|
||||
|
||||
var entry = await _entryRepository.GetFirstAsync(e => e.Id == input.SessionEntryId && !e.DeleteMark)
|
||||
?? throw Oops.Bah("出场记录不存在");
|
||||
|
||||
// 已作废的出场记录直接拦截,避免重复写入作废状态与原因
|
||||
if (entry.IsVoided)
|
||||
throw Oops.Bah("该出场记录已作废");
|
||||
|
||||
entry.IsVoided = true;
|
||||
entry.VoidReason = input.VoidReason;
|
||||
entry.VoidReason = reason;
|
||||
entry.UpdatedUserId = currentUserId;
|
||||
entry.UpdatedTime = now;
|
||||
|
||||
@ -1012,7 +1286,10 @@ public class WorkbenchController : IDynamicApiController
|
||||
result.UpdatedUserId = currentUserId;
|
||||
result.UpdatedTime = now;
|
||||
await _resultRepository.UpdateAsync(result);
|
||||
await SyncResultVersionStatusAsync(result, 90, currentUserId, now);
|
||||
}
|
||||
var voidSession = await _sessionRepository.GetByIdAsync(entry.SessionId);
|
||||
await WriteAuditAsync("作废成绩", voidSession?.EventId ?? 0, entry.Id, "有效", "已作废", reason, entry.SessionId);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@ -1048,6 +1325,19 @@ public class WorkbenchController : IDynamicApiController
|
||||
var eventProjectId = input.EventProjectId.Value;
|
||||
query = query.Where((r, e, s) => s.EventProjectId == eventProjectId);
|
||||
}
|
||||
// 支持按单个场次缩小导出范围
|
||||
if (input.SessionId.HasValue)
|
||||
{
|
||||
var sessionId = input.SessionId.Value;
|
||||
query = query.Where((r, e, s) => e.SessionId == sessionId);
|
||||
}
|
||||
// 关键词匹配出场编号、参赛者姓名或队名
|
||||
if (!string.IsNullOrWhiteSpace(input.Keyword))
|
||||
{
|
||||
var keyword = input.Keyword.Trim();
|
||||
query = query.Where((r, e, s) => e.EntryNo.Contains(keyword) || e.ParticipantName.Contains(keyword) ||
|
||||
(e.TeamName != null && e.TeamName.Contains(keyword)));
|
||||
}
|
||||
|
||||
var flat = await query
|
||||
.OrderBy((r, e, s) => r.Rank)
|
||||
@ -1378,6 +1668,72 @@ public class WorkbenchController : IDynamicApiController
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 规范化重赛/作废原因:去除首尾空格并校验长度为2-200个字符
|
||||
/// </summary>
|
||||
private static string NormalizeReason(string? value, string fieldName)
|
||||
{
|
||||
var reason = value?.Trim() ?? string.Empty;
|
||||
if (reason.Length < 2 || reason.Length > 200)
|
||||
throw Oops.Bah($"{fieldName}长度必须为2-200个字符");
|
||||
return reason;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 同步成绩当前版本的状态,保证 EventResult 与 EventResultVersion 状态一致
|
||||
/// </summary>
|
||||
private async Task SyncResultVersionStatusAsync(EventResult result, int status, long userId,
|
||||
DateTime now, EventResultVersion? version = null)
|
||||
{
|
||||
version ??= await _resultVersionRepository.GetFirstAsync(v =>
|
||||
!v.DeleteMark && v.EventResultId == result.Id && v.ResultVersion == result.ResultVersion);
|
||||
if (version == null) return;
|
||||
version.ResultStatus = status;
|
||||
version.UpdatedUserId = userId;
|
||||
version.UpdatedTime = now;
|
||||
await _resultVersionRepository.UpdateAsync(version);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 写入工作台业务操作审计日志,记录投屏、确认、修正、重赛、作废等关键动作的状态流转
|
||||
/// </summary>
|
||||
private async Task WriteAuditAsync(string action, long eventId, long objectId,
|
||||
object? beforeStatus, object? afterStatus, string? reason = null, long? sessionId = null)
|
||||
{
|
||||
var parameter = System.Text.Json.JsonSerializer.Serialize(new
|
||||
{
|
||||
EventId = eventId,
|
||||
SessionId = sessionId,
|
||||
ObjectId = objectId,
|
||||
BeforeStatus = beforeStatus,
|
||||
AfterStatus = afterStatus,
|
||||
Reason = reason
|
||||
});
|
||||
await _operationLogRepository.InsertAsync(new SysOperationLog
|
||||
{
|
||||
Id = _idGenerator.NextId(),
|
||||
UserId = _authManager.UserId,
|
||||
LogLevel = Microsoft.Extensions.Logging.LogLevel.Information,
|
||||
Description = action,
|
||||
ActionName = action,
|
||||
ControllerName = nameof(WorkbenchController),
|
||||
HttpMethod = "BUSINESS",
|
||||
HttpStatusCode = 200,
|
||||
Message = action,
|
||||
RequestUrl = "/workbench/audit",
|
||||
Parameter = parameter,
|
||||
Response = null,
|
||||
TraceId = string.Empty,
|
||||
ThreadId = Environment.CurrentManagedThreadId,
|
||||
UserAgent = string.Empty,
|
||||
RemoteIp = string.Empty,
|
||||
Location = string.Empty,
|
||||
OsDescription = null,
|
||||
Elapsed = 0,
|
||||
CreatedTime = DateTime.Now
|
||||
});
|
||||
}
|
||||
|
||||
private static string GetSheetStatusName(int status)
|
||||
{
|
||||
return status switch
|
||||
|
||||
@ -36,8 +36,8 @@
|
||||
<ProjectReference Include="..\YD_Event.Core\YD_Event.Core.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MiniExcel" Version="1.42.0" />
|
||||
<PackageReference Include="NPOI" Version="2.7.2" />
|
||||
<PackageReference Include="SKIT.FlurlHttpClient.Wechat.TenpayV3" Version="3.16.0" />
|
||||
<PackageReference Include="EPPlus" Version="7.1.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@ -1890,35 +1890,12 @@
|
||||
"CreatedUserId": 1000,
|
||||
"CreatedTime": "2023-06-01"
|
||||
},
|
||||
{
|
||||
"Id": 1080,
|
||||
"Name": "单位管理",
|
||||
"Type": 1,
|
||||
"Code": "system:unit",
|
||||
"ParentId": 1070,
|
||||
"RouteName": "systemunit",
|
||||
"Path": "/system/unit",
|
||||
"Component": "system/unit/index",
|
||||
"Redirect": "",
|
||||
"Icon": "ele-OfficeBuilding",
|
||||
"IsIframe": "0",
|
||||
"Link": "",
|
||||
"IsVisible": "1",
|
||||
"IsKeepAlive": "1",
|
||||
"IsFixed": "0",
|
||||
"Status": 0,
|
||||
"Sort": 6,
|
||||
"Remark": "报名平台/打分系统账号管理",
|
||||
"DeleteMark": false,
|
||||
"CreatedUserId": 1000,
|
||||
"CreatedTime": "2023-06-01"
|
||||
},
|
||||
{
|
||||
"Id": 1081,
|
||||
"Name": "新增",
|
||||
"Name": "打分账号-新增",
|
||||
"Type": 2,
|
||||
"Code": "system:unit:add",
|
||||
"ParentId": 1080,
|
||||
"ParentId": 1047,
|
||||
"RouteName": "btn_system_unit_add",
|
||||
"Path": "",
|
||||
"Component": "",
|
||||
@ -1930,18 +1907,18 @@
|
||||
"IsKeepAlive": "1",
|
||||
"IsFixed": "0",
|
||||
"Status": 0,
|
||||
"Sort": 1,
|
||||
"Remark": "",
|
||||
"Sort": 11,
|
||||
"Remark": "权限管理-账号管理:打分系统账号新增",
|
||||
"DeleteMark": false,
|
||||
"CreatedUserId": 1000,
|
||||
"CreatedTime": "2023-06-01"
|
||||
},
|
||||
{
|
||||
"Id": 1082,
|
||||
"Name": "编辑",
|
||||
"Name": "打分账号-编辑",
|
||||
"Type": 2,
|
||||
"Code": "system:unit:edit",
|
||||
"ParentId": 1080,
|
||||
"ParentId": 1047,
|
||||
"RouteName": "btn_system_unit_edit",
|
||||
"Path": "",
|
||||
"Component": "",
|
||||
@ -1953,18 +1930,18 @@
|
||||
"IsKeepAlive": "1",
|
||||
"IsFixed": "0",
|
||||
"Status": 0,
|
||||
"Sort": 2,
|
||||
"Remark": "",
|
||||
"Sort": 12,
|
||||
"Remark": "权限管理-账号管理:打分系统账号编辑",
|
||||
"DeleteMark": false,
|
||||
"CreatedUserId": 1000,
|
||||
"CreatedTime": "2023-06-01"
|
||||
},
|
||||
{
|
||||
"Id": 1083,
|
||||
"Name": "删除",
|
||||
"Name": "打分账号-删除",
|
||||
"Type": 2,
|
||||
"Code": "system:unit:delete",
|
||||
"ParentId": 1080,
|
||||
"ParentId": 1047,
|
||||
"RouteName": "btn_system_unit_delete",
|
||||
"Path": "",
|
||||
"Component": "",
|
||||
@ -1976,18 +1953,18 @@
|
||||
"IsKeepAlive": "1",
|
||||
"IsFixed": "0",
|
||||
"Status": 0,
|
||||
"Sort": 3,
|
||||
"Remark": "",
|
||||
"Sort": 13,
|
||||
"Remark": "权限管理-账号管理:打分系统账号删除",
|
||||
"DeleteMark": false,
|
||||
"CreatedUserId": 1000,
|
||||
"CreatedTime": "2023-06-01"
|
||||
},
|
||||
{
|
||||
"Id": 1084,
|
||||
"Name": "启用禁用",
|
||||
"Name": "打分账号-启用禁用",
|
||||
"Type": 2,
|
||||
"Code": "system:unit:status",
|
||||
"ParentId": 1080,
|
||||
"ParentId": 1047,
|
||||
"RouteName": "btn_system_unit_status",
|
||||
"Path": "",
|
||||
"Component": "",
|
||||
@ -1999,18 +1976,18 @@
|
||||
"IsKeepAlive": "1",
|
||||
"IsFixed": "0",
|
||||
"Status": 0,
|
||||
"Sort": 4,
|
||||
"Remark": "",
|
||||
"Sort": 14,
|
||||
"Remark": "权限管理-账号管理:打分系统账号状态",
|
||||
"DeleteMark": false,
|
||||
"CreatedUserId": 1000,
|
||||
"CreatedTime": "2023-06-01"
|
||||
},
|
||||
{
|
||||
"Id": 1085,
|
||||
"Name": "重置密码",
|
||||
"Name": "打分账号-重置密码",
|
||||
"Type": 2,
|
||||
"Code": "system:unit:resetpwd",
|
||||
"ParentId": 1080,
|
||||
"ParentId": 1047,
|
||||
"RouteName": "btn_system_unit_resetpwd",
|
||||
"Path": "",
|
||||
"Component": "",
|
||||
@ -2022,8 +1999,8 @@
|
||||
"IsKeepAlive": "1",
|
||||
"IsFixed": "0",
|
||||
"Status": 0,
|
||||
"Sort": 5,
|
||||
"Remark": "",
|
||||
"Sort": 15,
|
||||
"Remark": "权限管理-账号管理:打分系统账号重置密码",
|
||||
"DeleteMark": false,
|
||||
"CreatedUserId": 1000,
|
||||
"CreatedTime": "2023-06-01"
|
||||
|
||||
@ -679,14 +679,6 @@
|
||||
"CreatedTime": "2023-06-01",
|
||||
"DeleteMark": false
|
||||
},
|
||||
{
|
||||
"Id": 1101,
|
||||
"RoleId": 1000,
|
||||
"MenuId": 1080,
|
||||
"CreatedUserId": 1000,
|
||||
"CreatedTime": "2023-06-01",
|
||||
"DeleteMark": false
|
||||
},
|
||||
{
|
||||
"Id": 1102,
|
||||
"RoleId": 1000,
|
||||
@ -727,4 +719,4 @@
|
||||
"CreatedTime": "2023-06-01",
|
||||
"DeleteMark": false
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
50
二期需求/dbo.sql
50
二期需求/dbo.sql
@ -1,7 +1,7 @@
|
||||
/*
|
||||
========================================================================================
|
||||
YD_Event 二期生产环境部署脚本
|
||||
版本:3.0 | 基线提交:25830581d2add95f2d659e663f3d78b766ce0946
|
||||
版本:3.1 | 基线提交:25830581d2add95f2d659e663f3d78b766ce0946
|
||||
数据库:Microsoft SQL Server 2016+(兼容级别 130+)
|
||||
|
||||
执行示例:
|
||||
@ -31,7 +31,7 @@ SET QUOTED_IDENTIFIER ON;
|
||||
BEGIN TRY
|
||||
BEGIN TRANSACTION;
|
||||
DECLARE @LockResult INT;
|
||||
EXEC @LockResult=sys.sp_getapplock @Resource=N'YD_Event_Phase2_Production_Deployment_v3',@LockMode='Exclusive',@LockOwner='Transaction',@LockTimeout=10000;
|
||||
EXEC @LockResult=sys.sp_getapplock @Resource=N'YD_Event_Phase2_Production_Deployment',@LockMode='Exclusive',@LockOwner='Transaction',@LockTimeout=10000;
|
||||
IF @LockResult<0 THROW 51002,N'无法取得二期部署排他锁,可能有另一个部署正在执行。',1;
|
||||
|
||||
/* ====================================================================================
|
||||
@ -688,10 +688,18 @@ BEGIN TRY
|
||||
IF NOT EXISTS(SELECT 1 FROM sys.indexes WHERE object_id=OBJECT_ID(N'dbo.[EventResultVersion]') AND name=N'UX_EventResultVersion_Result_Version') CREATE UNIQUE NONCLUSTERED INDEX [UX_EventResultVersion_Result_Version] ON dbo.[EventResultVersion]([EventResultId],[ResultVersion]) WHERE [DeleteMark]=0;
|
||||
IF NOT EXISTS(SELECT 1 FROM sys.indexes WHERE object_id=OBJECT_ID(N'dbo.[EventJudgeMessage]') AND name=N'IX_EventJudgeMessage_Receiver_Unread') CREATE NONCLUSTERED INDEX [IX_EventJudgeMessage_Receiver_Unread] ON dbo.[EventJudgeMessage]([ReceiverUserId],[IsRead],[CreatedTime]) WHERE [DeleteMark]=0;
|
||||
IF NOT EXISTS(SELECT 1 FROM sys.indexes WHERE object_id=OBJECT_ID(N'dbo.[EventResultScreenCast]') AND name=N'IX_EventResultScreenCast_Event_Time') CREATE NONCLUSTERED INDEX [IX_EventResultScreenCast_Event_Time] ON dbo.[EventResultScreenCast]([EventId],[CastTime]) WHERE [DeleteMark]=0;
|
||||
/* 同一赛事同一场地只允许保留一条投出中的投屏:先把重复的历史投屏置为“已替换(20)”,再创建唯一筛选索引兜底 */
|
||||
;WITH ActiveCast AS (
|
||||
SELECT Id,ROW_NUMBER() OVER(PARTITION BY EventId,VenueId ORDER BY CastTime DESC,Id DESC) AS rn
|
||||
FROM dbo.EventResultScreenCast WHERE CastStatus=0 AND DeleteMark=0
|
||||
)
|
||||
UPDATE sc SET CastStatus=20,UpdatedUserId=1000,UpdatedTime=SYSUTCDATETIME()
|
||||
FROM dbo.EventResultScreenCast sc JOIN ActiveCast a ON a.Id=sc.Id WHERE a.rn>1;
|
||||
IF NOT EXISTS(SELECT 1 FROM sys.indexes WHERE object_id=OBJECT_ID(N'dbo.[EventResultScreenCast]') AND name=N'UX_EventResultScreenCast_Current') CREATE UNIQUE NONCLUSTERED INDEX [UX_EventResultScreenCast_Current] ON dbo.[EventResultScreenCast]([EventId],[VenueId]) WHERE [CastStatus]=0 AND [DeleteMark]=0;
|
||||
|
||||
/* ====================================================================================
|
||||
[4/9] 插入数据 · 初始化种子(统一“存在则更新、缺失才插入”的幂等写法,可重复执行)
|
||||
顺序:4.1 计分规则(4条) -> 规则Id映射 -> 4.2 规则明细(230条) -> 4.3 角色 -> 4.4 菜单 -> 4.5 角色菜单授权
|
||||
顺序:4.1 计分规则(4条) -> 规则Id映射 -> 4.2 规则明细(230条) -> 4.3 角色 -> 4.4 账号管理按钮 -> 4.5 角色菜单授权
|
||||
==================================================================================== */
|
||||
PRINT N'[4/9] 插入数据:初始化计分规则、规则明细、角色与菜单';
|
||||
/* 4.1 四条内置计分规则(固定Id:3000规定赛/3100车轮跳花样/3200交互绳花样/3300表演赛):先按 RuleCode 更新,再插入缺失,Id冲突则报错 */
|
||||
@ -972,15 +980,23 @@ BEGIN TRY
|
||||
INSERT dbo.SysRole(Id,Name,Code,Status,Sort,Remark,DeleteMark,CreatedUserId,CreatedTime) VALUES(1003,N'打分系统单位',N'scoringunit',0,4,N'打分系统单位账号',0,1000,'2023-06-01');
|
||||
ELSE UPDATE dbo.SysRole SET Name=N'打分系统单位',Code=N'scoringunit',Status=0,Sort=4,Remark=N'打分系统单位账号',DeleteMark=0 WHERE Id=1003;
|
||||
|
||||
/* 4.4 菜单/按钮:1080 单位管理及其5个按钮(1081-1085)、2010 裁判管理;Id被其他编码占用则报错 */
|
||||
/* 4.4 菜单/按钮:账号管理下的打分账号按钮(1081-1085)、2010 裁判管理;不再创建“系统管理-单位管理” */
|
||||
IF NOT EXISTS(SELECT 1 FROM dbo.SysMenu WHERE Id=1047 AND Code=N'permission:account' AND DeleteMark=0)
|
||||
THROW 51006,N'未找到权限管理-账号管理菜单(Id=1047,Code=permission:account)。',1;
|
||||
IF EXISTS(SELECT 1 FROM dbo.SysMenu WHERE Id=1080 AND ISNULL(Code,N'')<>N'system:unit')
|
||||
THROW 51006,N'SysMenu.Id=1080 已被其他菜单占用。',1;
|
||||
/* 兼容曾执行过旧版脚本的环境:停用旧菜单及其角色授权,业务统一归入账号管理。 */
|
||||
UPDATE dbo.SysRoleMenu SET DeleteMark=1,UpdatedUserId=1000,UpdatedTime=SYSUTCDATETIME()
|
||||
WHERE MenuId=1080 AND DeleteMark=0;
|
||||
UPDATE dbo.SysMenu SET IsVisible=0,Status=1,DeleteMark=1,Remark=N'功能已迁移至权限管理-账号管理',UpdatedUserId=1000,UpdatedTime=SYSUTCDATETIME()
|
||||
WHERE Id=1080 AND Code=N'system:unit' AND DeleteMark=0;
|
||||
CREATE TABLE #MenuSeed(Id BIGINT PRIMARY KEY,Name NVARCHAR(32),Type INT,Code NVARCHAR(128),ParentId BIGINT,RouteName NVARCHAR(32),Path NVARCHAR(128),Component NVARCHAR(128),Redirect NVARCHAR(128),Icon NVARCHAR(64),IsIframe BIT,Link NVARCHAR(256),IsVisible BIT,IsKeepAlive BIT,IsFixed BIT,Status INT,Sort INT,Remark NVARCHAR(256),DeleteMark BIT,CreatedUserId BIGINT,CreatedTime DATETIME2(3));
|
||||
INSERT #MenuSeed VALUES
|
||||
(1080,N'单位管理',1,N'system:unit',1070,N'systemunit',N'/system/unit',N'system/unit/index',N'',N'ele-OfficeBuilding',N'0',N'',N'1',N'1',N'0',0,6,N'报名平台/打分系统账号管理',0,1000,N'2023-06-01'),
|
||||
(1081,N'新增',2,N'system:unit:add',1080,N'btn_system_unit_add',N'',N'',N'',N'',N'0',N'',N'0',N'1',N'0',0,1,N'',0,1000,N'2023-06-01'),
|
||||
(1082,N'编辑',2,N'system:unit:edit',1080,N'btn_system_unit_edit',N'',N'',N'',N'',N'0',N'',N'0',N'1',N'0',0,2,N'',0,1000,N'2023-06-01'),
|
||||
(1083,N'删除',2,N'system:unit:delete',1080,N'btn_system_unit_delete',N'',N'',N'',N'',N'0',N'',N'0',N'1',N'0',0,3,N'',0,1000,N'2023-06-01'),
|
||||
(1084,N'启用禁用',2,N'system:unit:status',1080,N'btn_system_unit_status',N'',N'',N'',N'',N'0',N'',N'0',N'1',N'0',0,4,N'',0,1000,N'2023-06-01'),
|
||||
(1085,N'重置密码',2,N'system:unit:resetpwd',1080,N'btn_system_unit_resetpwd',N'',N'',N'',N'',N'0',N'',N'0',N'1',N'0',0,5,N'',0,1000,N'2023-06-01'),
|
||||
(1081,N'打分账号-新增',2,N'system:unit:add',1047,N'btn_system_unit_add',N'',N'',N'',N'',N'0',N'',N'0',N'1',N'0',0,11,N'权限管理-账号管理:打分系统账号新增',0,1000,N'2023-06-01'),
|
||||
(1082,N'打分账号-编辑',2,N'system:unit:edit',1047,N'btn_system_unit_edit',N'',N'',N'',N'',N'0',N'',N'0',N'1',N'0',0,12,N'权限管理-账号管理:打分系统账号编辑',0,1000,N'2023-06-01'),
|
||||
(1083,N'打分账号-删除',2,N'system:unit:delete',1047,N'btn_system_unit_delete',N'',N'',N'',N'',N'0',N'',N'0',N'1',N'0',0,13,N'权限管理-账号管理:打分系统账号删除',0,1000,N'2023-06-01'),
|
||||
(1084,N'打分账号-启用禁用',2,N'system:unit:status',1047,N'btn_system_unit_status',N'',N'',N'',N'',N'0',N'',N'0',N'1',N'0',0,14,N'权限管理-账号管理:打分系统账号状态',0,1000,N'2023-06-01'),
|
||||
(1085,N'打分账号-重置密码',2,N'system:unit:resetpwd',1047,N'btn_system_unit_resetpwd',N'',N'',N'',N'',N'0',N'',N'0',N'1',N'0',0,15,N'权限管理-账号管理:打分系统账号重置密码',0,1000,N'2023-06-01'),
|
||||
(2010,N'裁判管理',1,N'referee',NULL,N'referee',N'/referee',N'match/referee/index',N'',N'ele-UserFilled',N'0',N'',N'1',N'1',N'0',0,245,N'裁判库管理',0,1000,N'2023-06-01');
|
||||
IF EXISTS(SELECT 1 FROM #MenuSeed s JOIN dbo.SysMenu m ON m.Id=s.Id WHERE ISNULL(m.Code,N'')<>ISNULL(s.Code,N'')) THROW 51006,N'二期菜单ID已被其他菜单占用。',1;
|
||||
UPDATE m SET Name=s.Name,Type=s.Type,Code=s.Code,ParentId=s.ParentId,RouteName=s.RouteName,Path=s.Path,Component=s.Component,Redirect=s.Redirect,Icon=s.Icon,IsIframe=s.IsIframe,Link=s.Link,IsVisible=s.IsVisible,IsKeepAlive=s.IsKeepAlive,IsFixed=s.IsFixed,Status=s.Status,Sort=s.Sort,Remark=s.Remark,DeleteMark=0,UpdatedUserId=1000,UpdatedTime=SYSUTCDATETIME()
|
||||
@ -990,9 +1006,9 @@ BEGIN TRY
|
||||
INSERT dbo.SysMenu(Id,Name,Type,Code,ParentId,RouteName,Path,Component,Redirect,Icon,IsIframe,Link,IsVisible,IsKeepAlive,IsFixed,Status,Sort,Remark,DeleteMark,CreatedUserId,CreatedTime)
|
||||
SELECT Id,Name,Type,Code,ParentId,RouteName,Path,Component,Redirect,Icon,IsIframe,Link,IsVisible,IsKeepAlive,IsFixed,Status,Sort,Remark,DeleteMark,CreatedUserId,CreatedTime FROM #MenuSeed s WHERE NOT EXISTS(SELECT 1 FROM dbo.SysMenu m WHERE m.Id=s.Id);
|
||||
|
||||
/* 4.5 角色-菜单授权:把上述菜单授予超管角色 RoleId=1000(关系Id 1101-1110,幂等) */
|
||||
/* 4.5 角色-菜单授权:账号管理入口为一期既有授权,仅补充打分账号按钮与裁判菜单 */
|
||||
DECLARE @RoleMenuSeed TABLE(Id BIGINT PRIMARY KEY,RoleId BIGINT,MenuId BIGINT);
|
||||
INSERT @RoleMenuSeed VALUES(1101,1000,1080),(1102,1000,1081),(1103,1000,1082),(1104,1000,1083),(1105,1000,1084),(1106,1000,1085),(1110,1000,2010);
|
||||
INSERT @RoleMenuSeed VALUES(1102,1000,1081),(1103,1000,1082),(1104,1000,1083),(1105,1000,1084),(1106,1000,1085),(1110,1000,2010);
|
||||
IF EXISTS(SELECT 1 FROM @RoleMenuSeed s JOIN dbo.SysRoleMenu rm ON rm.Id=s.Id WHERE rm.RoleId<>s.RoleId OR rm.MenuId<>s.MenuId) THROW 51007,N'二期角色菜单关系ID已被占用。',1;
|
||||
UPDATE rm SET DeleteMark=0,UpdatedUserId=1000,UpdatedTime=SYSUTCDATETIME() FROM dbo.SysRoleMenu rm JOIN @RoleMenuSeed s ON s.RoleId=rm.RoleId AND s.MenuId=rm.MenuId WHERE rm.DeleteMark<>0;
|
||||
INSERT dbo.SysRoleMenu(Id,RoleId,MenuId,CreatedUserId,CreatedTime,DeleteMark)
|
||||
@ -1003,9 +1019,9 @@ BEGIN TRY
|
||||
1个标量函数 + 3个视图 + 2个存储过程
|
||||
==================================================================================== */
|
||||
PRINT N'[5/9] 可编程对象:创建函数、视图与存储过程';
|
||||
/* 标量函数:成绩状态码 -> 中文名(0草稿/10待确认/20已确认/30已发布/40已投屏/90作废) */
|
||||
/* 标量函数:成绩状态码 -> 中文名(0计算中/10待复核/20已确认/30已投屏/40已更正/90作废) */
|
||||
EXEC(N'CREATE OR ALTER FUNCTION dbo.fn_Phase2_ResultStatusName(@ResultStatus INT)
|
||||
RETURNS NVARCHAR(32) AS BEGIN RETURN CASE @ResultStatus WHEN 0 THEN N''草稿'' WHEN 10 THEN N''待确认'' WHEN 20 THEN N''已确认'' WHEN 30 THEN N''已发布'' WHEN 40 THEN N''已投屏'' WHEN 90 THEN N''已作废'' ELSE N''未知'' END END');
|
||||
RETURNS NVARCHAR(32) AS BEGIN RETURN CASE @ResultStatus WHEN 0 THEN N''计算中'' WHEN 10 THEN N''待复核'' WHEN 20 THEN N''已确认'' WHEN 30 THEN N''已投屏'' WHEN 40 THEN N''已更正'' WHEN 90 THEN N''已作废'' ELSE N''未知'' END END');
|
||||
|
||||
/* 视图:裁判席位分配明细(关联场次/场地/裁判档案) */
|
||||
EXEC(N'CREATE OR ALTER VIEW dbo.vw_Phase2_JudgeAssignment AS
|
||||
@ -1130,7 +1146,11 @@ SELECT r.RuleCode,COUNT(i.Id) AS ItemCount FROM dbo.ScoreRule r LEFT JOIN dbo.Sc
|
||||
(N'CONSTRAINT',N'二期外键',CASE WHEN (SELECT COUNT(*) FROM sys.foreign_keys WHERE name LIKE 'FK[_]%' AND parent_object_id IN(SELECT OBJECT_ID(N'dbo.'+QUOTENAME(Name)) FROM @ExpectedTables))>=20 THEN 1 ELSE 0 END,CONVERT(NVARCHAR(20),(SELECT COUNT(*) FROM sys.foreign_keys WHERE name LIKE 'FK[_]%' AND parent_object_id IN(SELECT OBJECT_ID(N'dbo.'+QUOTENAME(Name)) FROM @ExpectedTables)))),
|
||||
(N'SEED',N'计分规则',CASE WHEN (SELECT COUNT(DISTINCT r.RuleCode) FROM dbo.ScoreRule r JOIN #RuleSeed s ON s.RuleCode=r.RuleCode WHERE r.DeleteMark=0)=4 THEN 1 ELSE 0 END,N'期望4条'),
|
||||
(N'SEED',N'计分规则明细',CASE WHEN (SELECT COUNT(*) FROM #ItemSeed s JOIN #RuleMap m ON m.SourceId=s.SourceRuleId JOIN dbo.ScoreRuleItem i ON i.ScoreRuleId=m.TargetId AND i.JudgeRoleType=s.JudgeRoleType AND i.ItemCode=s.ItemCode AND i.DeleteMark=0)=230 THEN 1 ELSE 0 END,N'期望230条'),
|
||||
(N'SEED',N'角色与菜单',CASE WHEN EXISTS(SELECT 1 FROM dbo.SysRole WHERE Id=1003 AND Code='scoringunit' AND DeleteMark=0) AND (SELECT COUNT(*) FROM dbo.SysMenu WHERE Id IN(1080,1081,1082,1083,1084,1085,2010) AND DeleteMark=0)=7 THEN 1 ELSE 0 END,N'角色1条、菜单7条'),
|
||||
(N'SEED',N'角色与菜单',CASE WHEN EXISTS(SELECT 1 FROM dbo.SysRole WHERE Id=1003 AND Code='scoringunit' AND DeleteMark=0)
|
||||
AND (SELECT COUNT(*) FROM dbo.SysMenu WHERE Id IN(1081,1082,1083,1084,1085,2010) AND DeleteMark=0)=6
|
||||
AND (SELECT COUNT(*) FROM dbo.SysMenu WHERE Id IN(1081,1082,1083,1084,1085) AND ParentId=1047 AND DeleteMark=0)=5
|
||||
AND NOT EXISTS(SELECT 1 FROM dbo.SysMenu WHERE Id=1080 AND DeleteMark=0)
|
||||
THEN 1 ELSE 0 END,N'角色1条、账号管理按钮5条、裁判菜单1条、无单位管理菜单'),
|
||||
(N'PERMISSION',N'最小权限角色',CASE WHEN (SELECT COUNT(*) FROM sys.database_principals WHERE name IN('yd_phase2_app_rw','yd_phase2_app_ro','yd_phase2_executor') AND type='R')=3 THEN 1 ELSE 0 END,N'期望3个数据库角色');
|
||||
|
||||
SELECT N'POSTCHECK' AS [Stage],[Category],[Item],CASE WHEN Passed=1 THEN N'PASS' ELSE N'FAIL' END AS [Result],[Actual] FROM @Validation ORDER BY Passed,[Category],[Item];
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user