ltx 48151578a1 feat: 运动馆班级批量删除与菜单权限登记
新增 SportsHall/BatchDeleteClasses 接口:校验批量数量、本校运动馆班级归属及空班条件,在可串行化事务中清理班级和教练关联,并发送物联网删除同步。

学生批量删除增加 BatchDelete 动作权限校验;按实际页面菜单匹配权限,兼容生产库中重复的学生管理菜单。运动馆班级删除由已授权角色使用,不硬编码学校角色。

新增幂等菜单权限脚本,仅向学生管理和运动馆班级管理的有效页面菜单登记批量删除动作,不自动给角色授权。QA 与生产库已核验菜单动作;代码上线仍需独立部署。

验证:Debug/Release 构建通过,SQL 在 QA 重跑无重复写入。
2026-10-15 14:51:56 +08:00

1003 lines
42 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using AutoMapper;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json;
using OfficeOpenXml.FormulaParsing.Excel.Functions.DateTime;
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using VOL.Business.IServices;
using VOL.Business.IServices.Norm;
using VOL.Business.IServices.School;
using VOL.Business.IServices.SportsHall;
using VOL.Business.Services.School;
using VOL.Core.CacheManager;
using VOL.Core.Extensions.AutofacManager;
using VOL.Core.ManageUser;
using VOL.Core.Utilities;
using VOL.Entity.DomainModels;
using VOL.Entity.Enum;
using VOL.Model;
using VOL.Model.IOT.Request;
using VOL.Model.School.Request;
using VOL.Model.School.Response;
using VOL.Model.SportsHall;
using VOL.System.IRepositories;
using VOL.System.Repositories;
namespace VOL.Business.Services.SportsHall
{
/// <summary>
/// 运动馆
/// </summary>
public class SportsHallService : ISportsHallService, IDependency
{
#region 初始化
private readonly IS_ClassRepository _classRepository;
private readonly IS_GradeRepository _gradeRepository;
private readonly IS_ClassAssocTeacherRepository _classAssocTeacherRepository;
private readonly IS_StudentRepository _studentRepository;
private readonly IotDataSyncService _iotDataSyncService;
IN_HealthStandardsRepository _healthStandardsRepository;
IN_SportsTestResultRepository _sportsTestResultRepository;
[ActivatorUtilitiesConstructor]
public SportsHallService(IS_ClassRepository classRepository,
IS_GradeRepository gradeRepository,
IS_ClassAssocTeacherRepository classAssocTeacherRepository,
IS_StudentRepository studentRepository,
IotDataSyncService iotDataSyncService,
IN_HealthStandardsRepository healthStandardsRepository,
IN_SportsTestResultRepository sportsTestResultRepository)
{
_classRepository = classRepository;
_gradeRepository = gradeRepository;
_classAssocTeacherRepository = classAssocTeacherRepository;
_studentRepository = studentRepository;
_iotDataSyncService = iotDataSyncService;
_healthStandardsRepository = healthStandardsRepository;
_sportsTestResultRepository = sportsTestResultRepository;
}
#endregion
/// <summary>
/// 新增班级
/// </summary>
/// <param name="paramDto"></param>
/// <returns></returns>
public async Task AddClass(AddSportsHallClassParam paramDto)
{
var tenantId = UserContext.Current.TenantId;
var grade = await _gradeRepository.FindAsIQueryable(x => x.GradeName == "运动馆").Select(x => new { x.Id, x.GradeName }).FirstAsync();
var exist = await _classRepository.ExistsAsync(x => x.SchoolCode == tenantId && x.ClassName == paramDto.ClassName && x.GradeId == grade.Id);
if (exist)
throw new Exception("班级名称已存在");
var classEntity = new S_Class()
{
GradeId = grade.Id,
GradeName = grade.GradeName,
ClassName = paramDto.ClassName,
Creator = UserContext.Current.UserId,
SchoolCode = UserContext.Current.TenantId,
CreateDate = DateTime.Now
};
using (var transaction = _classRepository.DbContext.Database.BeginTransaction())
{
try
{
await _classRepository.AddAsync(classEntity);
await _classRepository.SaveChangesAsync();
int classId = classEntity.Id;
var classAssocTeacherList = new List<S_ClassAssocTeacher>();
paramDto.TeacherIds.ForEach(x =>
{
classAssocTeacherList.Add(new S_ClassAssocTeacher()
{
ClassId = classId,
TeacherId = x,
Creator = UserContext.Current.UserId,
CreateDate = DateTime.Now,
Modifier = UserContext.Current.UserId,
ModifyDate = DateTime.Now,
});
});
await _classAssocTeacherRepository.AddRangeAsync(classAssocTeacherList);
var students = await _studentRepository.FindAsync(x => paramDto.studentNoList.Contains(x.StudentNo));
foreach (var student in students)
{
student.ClassId = classId;
int index = paramDto.studentNoList.IndexOf(student.StudentNo);
student.OrderNo = index >= 0 ? index + 1 : 0;
}
_studentRepository.UpdateRange(students);
await _studentRepository.SaveChangesAsync();
// 提交事务
transaction.Commit();
}
catch (Exception ex)
{
// 发生错误,回滚事务
transaction.Rollback();
Console.WriteLine("事务回滚:" + ex.Message);
}
}
}
/// <summary>
/// 更新班级
/// </summary>
/// <param name="paramDto"></param>
/// <returns></returns>
public async Task ModifyClass(ModifySportsHallClassParam paramDto)
{
var tenantId = UserContext.Current.TenantId;
var classModel = await _classRepository.FindAsyncFirst(x => x.Id == paramDto.ClassId);
var grade = await _gradeRepository.FindAsIQueryable(x => x.GradeName == "运动馆").Select(x => new { x.Id, x.GradeName }).FirstAsync();
if (classModel == null)
throw new ArgumentNullException("未找到要更新得数据");
var exist = await _classRepository.ExistsAsync(
x => x.SchoolCode == tenantId
&& x.ClassName == paramDto.ClassName
&& x.GradeId == grade.Id
&& x.Id != paramDto.ClassId);
if (exist)
throw new Exception("班级名称已存在");
classModel.ClassName = paramDto.ClassName;
using (var transaction = _classRepository.DbContext.Database.BeginTransaction())
{
try
{
_classRepository.Update(classModel);
var teacherList = await _classAssocTeacherRepository.FindAsync(x => x.ClassId == paramDto.ClassId);
_classAssocTeacherRepository.DbContext.RemoveRange(teacherList);
var classAssocTeacherList = new List<S_ClassAssocTeacher>();
paramDto.TeacherIds.ForEach(x =>
{
classAssocTeacherList.Add(new S_ClassAssocTeacher()
{
ClassId = paramDto.ClassId,
TeacherId = x,
Creator = UserContext.Current.UserId,
CreateDate = DateTime.Now,
Modifier = UserContext.Current.UserId,
ModifyDate = DateTime.Now,
});
});
await _classAssocTeacherRepository.AddRangeAsync(classAssocTeacherList);
var oldStudents = await _studentRepository.FindAsync(x => x.ClassId == classModel.Id);
var newStudents = await _studentRepository.FindAsync(x => paramDto.studentNoList.Contains(x.StudentNo));
var allStudents = oldStudents.UnionBy(newStudents, s => s.Id).ToList();
foreach (var student in allStudents)
{
if (paramDto.studentNoList.Contains(student.StudentNo))
{
student.ClassId = classModel.Id;
student.OrderNo = paramDto.studentNoList.IndexOf(student.StudentNo) + 1;
}
else
{
student.ClassId = 0;
student.OrderNo = 0;
}
}
_studentRepository.UpdateRange(allStudents);
await _classRepository.SaveChangesAsync();
transaction.Commit();
}
catch (Exception ex)
{
// 发生错误,回滚事务
transaction.Rollback();
Console.WriteLine("事务回滚:" + ex.Message);
}
}
}
public async Task<int> BatchDeleteClasses(List<int> ids)
{
var user = UserContext.Current;
var classIds = ids?.Where(id => id > 0).Distinct().ToList() ?? new List<int>();
if (classIds.Count == 0 || classIds.Count > 100)
throw new ArgumentException("请选择1至100个班级");
if (user.RoleId != 1)
{
var menuIds = await _classRepository.DbContext.Set<Sys_Menu>()
.Where(menu => menu.Url == "/sportsHallClass" && menu.ParentId > 0)
.Select(menu => menu.Menu_Id)
.ToListAsync();
if (!user.Permissions.Any(permission =>
menuIds.Contains(permission.Menu_Id) && permission.UserAuthArr.Contains("BatchDelete")))
throw new Exception("没有批量删除运动馆班级的权限");
}
var gradeId = await _gradeRepository.FindAsIQueryable(grade => grade.GradeName == "运动馆")
.Select(grade => grade.Id)
.FirstOrDefaultAsync();
if (gradeId == 0)
throw new Exception("未找到运动馆年级");
await using var transaction = await _classRepository.DbContext.Database.BeginTransactionAsync(IsolationLevel.Serializable);
var classes = await _classRepository.DbContext.Set<S_Class>()
.Where(item => item.SchoolCode == user.TenantId && item.GradeId == gradeId && classIds.Contains(item.Id))
.ToListAsync();
if (classes.Count != classIds.Count)
throw new Exception("部分班级不存在或不属于当前学校,请刷新列表后重试");
var occupiedClassIds = await _classRepository.DbContext.Set<S_Student>()
.Where(student => classIds.Contains(student.ClassId))
.Select(student => student.ClassId)
.Distinct()
.ToListAsync();
if (occupiedClassIds.Count > 0)
{
var names = classes.Where(item => occupiedClassIds.Contains(item.Id)).Select(item => item.ClassName);
throw new Exception($"班级 {string.Join("、", names)} 仍有学生,请先调整学生后再删除");
}
var teacherAssociations = await _classRepository.DbContext.Set<S_ClassAssocTeacher>()
.Where(item => classIds.Contains(item.ClassId))
.ToListAsync();
_classRepository.DbContext.RemoveRange(teacherAssociations);
_classRepository.DbContext.RemoveRange(classes);
await _classRepository.SaveChangesAsync();
await transaction.CommitAsync();
foreach (var item in classes)
{
await _iotDataSyncService.DataSyncCallBack(new DataSyncCallBackParam
{
EventType = EventType.Delete.GetDisplayName(),
DataType = IOTDataSyncType.Class.GetDisplayName(),
Json = JsonConvert.SerializeObject(item)
});
}
return classes.Count;
}
/// <summary>
/// 导入运动馆学生体测成绩
/// </summary>
/// <param name="file"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public async Task SportsHallImportStudentsTestData(IFormFile file)
{
await ImportStudentsTestData(file, false);
}
/// <summary>
/// 导入体测成绩管理学生体测成绩
/// </summary>
/// <param name="file"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public async Task SummaryOfGradesImportStudentsTestData(IFormFile file)
{
await ImportStudentsTestData(file, true);
}
private async Task ImportStudentsTestData(IFormFile file, bool isDisplay)
{
if (file == null || file.Length <= 0)
throw new Exception("操作失败");
List<SportsHallImportStudentsTestDataParam> dataObjects;
using (var fileStream = file.OpenReadStream())
{
dataObjects = Tool.ConvertExcelToList<SportsHallImportStudentsTestDataParam>(fileStream);
}
var studentNos = dataObjects.Select(x => x.StudentNo).Distinct().ToList();
// 用一个查询一次性获取所有学生信息
var students = await (from s in _studentRepository.DbContext.Set<S_Student>()
join c in _studentRepository.DbContext.Set<S_Class>() on s.ClassId equals c.Id
join g in _studentRepository.DbContext.Set<S_Grade>() on c.GradeId equals g.Id
where c.SchoolCode.Equals(UserContext.Current.TenantId)
select new
{
s.Id,
s.Age,
s.ClassId,
c.ClassName,
s.StudentName,
GradeId = g.Id,
g.GradeName,
s.Sex,
s.StudentNo
}).ToListAsync();
// 将学生信息存入字典,快速查找
var studentsDict = students.ToDictionary(x => x.StudentNo);
var entityList = new List<N_SportsTestValue>();
foreach (var data in dataObjects)
{
if (!studentsDict.TryGetValue(data.StudentNo, out var student))
{
throw new Exception($"未找到此编号为{data.StudentNo}的学生");
}
var timeNow = data.ScoreTime;
// 为每个学生添加多条体测数据
if (data.BMIValue > 0)
entityList.Add(await AddSportsHallEnumStudentsTestData(data, SportsTestItemType.BMI, student, data.GradeId, timeNow, isDisplay));
//if (data.Sit_And_ReachValue > 0)
entityList.Add(await AddSportsHallEnumStudentsTestData(data, SportsTestItemType.Sit_And_Reach, student, data.GradeId, timeNow, isDisplay));
if (data.VitalCapacityValue > 0)
entityList.Add(await AddSportsHallEnumStudentsTestData(data, SportsTestItemType.VitalCapacity, student, data.GradeId, timeNow, isDisplay));
if (data.MeterRun_50Value > 0)
entityList.Add(await AddSportsHallEnumStudentsTestData(data, SportsTestItemType.MeterRun_50, student, data.GradeId, timeNow, isDisplay));
if (data.OneMinuteJumpRopeValue > 0)
entityList.Add(await AddSportsHallEnumStudentsTestData(data, SportsTestItemType.OneMinuteJumpRope, student, data.GradeId, timeNow, isDisplay));
if (data.One_Minute_Sit_UpValue > 0)
entityList.Add(await AddSportsHallEnumStudentsTestData(data, SportsTestItemType.One_Minute_Sit_Up, student, data.GradeId, timeNow, isDisplay));
if (data.ShuttleRun_50x8Value > 0)
entityList.Add(await AddSportsHallEnumStudentsTestData(data, SportsTestItemType.ShuttleRun_50x8, student, data.GradeId, timeNow, isDisplay));
if (data.StandingLongJumpValue > 0)
entityList.Add(await AddSportsHallEnumStudentsTestData(data, SportsTestItemType.StandingLongJump, student, data.GradeId, timeNow, isDisplay));
if (data.MeterRun_800Value > 0)
entityList.Add(await AddSportsHallEnumStudentsTestData(data, SportsTestItemType.MeterRun_800, student, data.GradeId, timeNow, isDisplay));
if (data.MeterRun_1000Value > 0)
entityList.Add(await AddSportsHallEnumStudentsTestData(data, SportsTestItemType.MeterRun_1000, student, data.GradeId, timeNow, isDisplay));
if (data.Pull_UpValue > 0)
entityList.Add(await AddSportsHallEnumStudentsTestData(data, SportsTestItemType.Pull_Up, student, data.GradeId, timeNow, isDisplay));
}
// 批量添加数据
await _sportsTestResultRepository.AddRangeAsync(entityList);
// 一次性保存所有修改
await _sportsTestResultRepository.SaveChangesAsync();
}
public async Task<N_SportsTestValue> AddSportsHallEnumStudentsTestData(SportsHallImportStudentsTestDataParam data, SportsTestItemType type, dynamic student, int gradeId, DateTime timeNow, bool isDisplay = false)
{
var entity = new N_SportsTestValue
{
TeacherId = 0,
TeacherName = "",
DataSource = DataSource.Im,
ScoreTime = timeNow,
GradeId = gradeId,
ClassId = student.ClassId,
GradeName = student.GradeName,
ClassName = student.ClassName,
SchoolCode = UserContext.Current.TenantId,
Sex = student.Sex,
StudentNo = data.StudentNo,
StudentName = student.StudentName,
Creator = UserContext.Current.UserId,
CreateDate = timeNow,
IsDisplay = isDisplay
};
SexType sexType = student.Sex;
string sex = sexType == SexType.Male ? "男" : "女";
string typeStr = type.ToString();
// 根据测试项目类型设置对应的值
if (type == SportsTestItemType.BMI)
{
entity.Value = data.BMIValue;
// 根据测试项目查找标准
var standard = await _healthStandardsRepository.FindFirstAsync(x =>
x.CategoryEnum.Equals(typeStr) &&
x.GradeId == gradeId &&
x.Sex == sexType &&
data.BMIValue >= x.MinValue &&
data.BMIValue < x.MaxValue
);
if (standard != null)
{
entity.Score = standard.Score;
entity.Rank = standard.Rank;
}
}
else if (type == SportsTestItemType.Sit_And_Reach)
{
entity.Value = data.Sit_And_ReachValue;
// 根据测试项目查找标准
var standard = await _healthStandardsRepository.FindFirstAsync(x =>
x.CategoryEnum.Equals(typeStr) &&
x.GradeId == gradeId &&
x.Sex == sexType &&
data.Sit_And_ReachValue >= x.MinValue &&
data.Sit_And_ReachValue < x.MaxValue
);
if (standard != null)
{
entity.Score = standard.Score;
entity.Rank = standard.Rank;
}
}
else if (type == SportsTestItemType.VitalCapacity)
{
entity.Value = data.VitalCapacityValue;
// 根据测试项目查找标准
var standard = await _healthStandardsRepository.FindFirstAsync(x =>
x.CategoryEnum.Equals(typeStr) &&
x.GradeId == gradeId &&
x.Sex == sexType &&
data.VitalCapacityValue >= x.MinValue &&
data.VitalCapacityValue < x.MaxValue
);
if (standard != null)
{
entity.Score = standard.Score;
entity.Rank = standard.Rank;
}
}
else if (type == SportsTestItemType.MeterRun_50)
{
entity.Value = data.MeterRun_50Value;
// 根据测试项目查找标准
var standard = await _healthStandardsRepository.FindFirstAsync(x =>
x.CategoryEnum.Equals(typeStr) &&
x.GradeId == gradeId &&
x.Sex == sexType &&
data.MeterRun_50Value >= x.MinValue &&
data.MeterRun_50Value < x.MaxValue
);
if (standard != null)
{
entity.Score = standard.Score;
entity.Rank = standard.Rank;
}
}
else if (type == SportsTestItemType.OneMinuteJumpRope)
{
entity.Value = data.OneMinuteJumpRopeValue;
// 根据测试项目查找标准
var standard = await _healthStandardsRepository.FindFirstAsync(x =>
x.CategoryEnum.Equals(typeStr) &&
x.GradeId == gradeId &&
x.Sex == sexType &&
data.OneMinuteJumpRopeValue >= x.MinValue &&
data.OneMinuteJumpRopeValue < x.MaxValue
);
if (standard != null)
{
entity.Score = standard.Score;
entity.Rank = standard.Rank;
}
}
else if (type == SportsTestItemType.One_Minute_Sit_Up)
{
entity.Value = data.One_Minute_Sit_UpValue;
// 根据测试项目查找标准
var standard = await _healthStandardsRepository.FindFirstAsync(x =>
x.CategoryEnum.Equals(typeStr) &&
x.GradeId == gradeId &&
x.Sex == sexType &&
data.One_Minute_Sit_UpValue >= x.MinValue &&
data.One_Minute_Sit_UpValue < x.MaxValue
);
if (standard != null)
{
entity.Score = standard.Score;
entity.Rank = standard.Rank;
}
}
else if (type == SportsTestItemType.ShuttleRun_50x8)
{
entity.Value = data.ShuttleRun_50x8Value;
// 根据测试项目查找标准
var standard = await _healthStandardsRepository.FindFirstAsync(x =>
x.CategoryEnum.Equals(typeStr) &&
x.GradeId == gradeId &&
x.Sex == sexType &&
data.ShuttleRun_50x8Value >= x.MinValue &&
data.ShuttleRun_50x8Value < x.MaxValue
);
if (standard != null)
{
entity.Score = standard.Score;
entity.Rank = standard.Rank;
}
}
else if (type == SportsTestItemType.StandingLongJump)
{
entity.Value = data.StandingLongJumpValue;
// 根据测试项目查找标准
var standard = await _healthStandardsRepository.FindFirstAsync(x =>
x.CategoryEnum.Equals(typeStr) &&
x.GradeId == gradeId &&
x.Sex == sexType &&
data.StandingLongJumpValue >= x.MinValue &&
data.StandingLongJumpValue < x.MaxValue
);
if (standard != null)
{
entity.Score = standard.Score;
entity.Rank = standard.Rank;
}
}
else if (type == SportsTestItemType.MeterRun_800)
{
entity.Value = data.MeterRun_800Value;
// 根据测试项目查找标准
var standard = await _healthStandardsRepository.FindFirstAsync(x =>
x.CategoryEnum.Equals(typeStr) &&
x.GradeId == gradeId &&
x.Sex == sexType &&
data.MeterRun_800Value >= x.MinValue &&
data.MeterRun_800Value < x.MaxValue
);
if (standard != null)
{
entity.Score = standard.Score;
entity.Rank = standard.Rank;
}
}
else if (type == SportsTestItemType.MeterRun_1000)
{
entity.Value = data.MeterRun_1000Value;
// 根据测试项目查找标准
var standard = await _healthStandardsRepository.FindFirstAsync(x =>
x.CategoryEnum.Equals(typeStr) &&
x.GradeId == gradeId &&
x.Sex == sexType &&
data.MeterRun_1000Value >= x.MinValue &&
data.MeterRun_1000Value < x.MaxValue
);
if (standard != null)
{
entity.Score = standard.Score;
entity.Rank = standard.Rank;
}
}
else if (type == SportsTestItemType.Pull_Up)
{
entity.Value = data.Pull_UpValue;
// 根据测试项目查找标准
var standard = await _healthStandardsRepository.FindFirstAsync(x =>
x.CategoryEnum.Equals(typeStr) &&
x.GradeId == gradeId &&
x.Sex == sexType &&
data.Pull_UpValue >= x.MinValue &&
data.Pull_UpValue < x.MaxValue
);
if (standard != null)
{
entity.Score = standard.Score;
entity.Rank = standard.Rank;
}
}
entity.CategoryEnum = type.ToString();
entity.CategoryValue = (int)type;
return entity;
}
/// <summary>
/// 运动馆体测成绩分页列表
/// </summary>
/// <param name="paramDto"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public async Task<PageDataDto<SportsHallStudentsTestModel>> GetSportsHallStudentsTestPageList(SportsHallStudentsTestParam paramDto)
{
var result = new PageDataDto<SportsHallStudentsTestModel>();
// 基础查询(带租户过滤)
var baseQuery = _sportsTestResultRepository.DbContext.Set<N_SportsTestValue>()
.Where(s => s.SchoolCode == UserContext.Current.TenantId);
// 公共条件过滤(时间、班级、性别、等级)
if (paramDto.StartTime.HasValue)
baseQuery = baseQuery.Where(x => x.CreateDate >= paramDto.StartTime.Value);
if (paramDto.EndTime.HasValue)
baseQuery = baseQuery.Where(x => x.CreateDate <= paramDto.EndTime.Value);
if (paramDto.ClassId.HasValue)
baseQuery = baseQuery.Where(x => x.ClassId == paramDto.ClassId.Value);
if (paramDto.Sex.HasValue)
baseQuery = baseQuery.Where(x => (int)x.Sex == paramDto.Sex);
if (!string.IsNullOrWhiteSpace(paramDto.ResultLevel))
baseQuery = baseQuery.Where(x => x.Rank == paramDto.ResultLevel);
// Step 1: 获取每个学生最新的 ScoreTime(子查询)
var latestScoreTimeQuery = baseQuery
.GroupBy(x => x.StudentNo)
.Select(g => new { StudentNo = g.Key, MaxScoreTime = g.Max(x => x.ScoreTime) });
// Step 2: 关联原表,只保留每个学生最新测试时间的记录
var latestRecordsQuery = from record in baseQuery
join latest in latestScoreTimeQuery
on new { record.StudentNo, record.ScoreTime }
equals new { latest.StudentNo, ScoreTime = latest.MaxScoreTime }
select record;
// Step 3: 获取符合条件的学生总数(去重)
result.Total = await latestRecordsQuery
.Select(x => x.StudentNo)
.Distinct()
.CountAsync();
// Step 4: 分页获取当前页的学生学号列表(先按最新测试时间倒序排)
var pagedStudentNos = await latestRecordsQuery
.OrderByDescending(x => x.ScoreTime) // 按最新测试时间整体倒序
.Select(x => x.StudentNo)
.Distinct()
.Skip((paramDto.PageIndex - 1) * paramDto.PageSize)
.Take(paramDto.PageSize)
.ToListAsync();
if (!pagedStudentNos.Any())
{
result.Datas = new List<SportsHallStudentsTestModel>();
return result;
}
// Step 5: 获取当前页学生的最新测试所有项目记录(包含 pivot 需要的数据)
var pageRecords = await (from record in latestRecordsQuery
where pagedStudentNos.Contains(record.StudentNo)
select new
{
record.Id,
record.StudentNo,
record.StudentName,
record.Sex,
record.ClassName,
record.GradeName,
record.ScoreTime,
record.IsDisplay,
record.CategoryValue,
record.Value
}).ToListAsync();
// Step 6: 在内存中完成 pivot(因为同一学生同一测试时间的项目数量有限,性能可接受)
var grouped = pageRecords
.GroupBy(x => new
{
x.StudentNo,
x.StudentName,
x.Sex,
x.ClassName,
x.GradeName,
x.ScoreTime,
x.IsDisplay
})
.Select(g => new SportsHallStudentsTestModel
{
Id = g.Max(x => x.Id), // 取组内最大 Id(或任意一个,仅作标识)
StudentNo = g.Key.StudentNo,
StudentName = g.Key.StudentName,
SexName = g.Key.Sex.GetDescription(),
GradeAndClass = $"{g.Key.GradeName} {g.Key.ClassName}",
ScoreTime = g.Key.ScoreTime,
IsDisplay = g.Key.IsDisplay,
// 各项指标:根据 CategoryValue 匹配,使用 Max(如果同一个 CategoryValue 有多条,取最大值)
BMIValue = g.Where(x => x.CategoryValue == (int)SportsTestItemType.BMI)
.Max(x => (float?)x.Value) ?? 0,
VitalCapacityValue = g.Where(x => x.CategoryValue == (int)SportsTestItemType.VitalCapacity)
.Max(x => (float?)x.Value) ?? 0,
Sit_And_ReachValue = g.Where(x => x.CategoryValue == (int)SportsTestItemType.Sit_And_Reach)
.Max(x => (float?)x.Value) ?? 0,
OneMinuteJumpRopeValue = g.Where(x => x.CategoryValue == (int)SportsTestItemType.OneMinuteJumpRope)
.Max(x => (int?)x.Value) ?? 0,
One_Minute_Sit_UpValue = g.Where(x => x.CategoryValue == (int)SportsTestItemType.One_Minute_Sit_Up)
.Max(x => (int?)x.Value) ?? 0,
StandingLongJumpValue = g.Where(x => x.CategoryValue == (int)SportsTestItemType.StandingLongJump)
.Max(x => (float?)x.Value) ?? 0
})
.OrderByDescending(x => x.ScoreTime) // 按测试时间倒序
.ToList();
result.Datas = grouped;
return result;
}
/// <summary>
/// 体测成绩管理分页列表(全部体测项目)
/// </summary>
/// <param name="paramDto"></param>
/// <returns></returns>
public async Task<PageDataDto<SummaryOfGradesStudentsTestModel>> GetSummaryOfGradesStudentsTestPageList(SportsHallStudentsTestParam paramDto)
{
var result = new PageDataDto<SummaryOfGradesStudentsTestModel>();
paramDto.PageIndex = paramDto.PageIndex <= 0 ? 1 : paramDto.PageIndex;
paramDto.PageSize = paramDto.PageSize <= 0 ? 10 : paramDto.PageSize;
var baseQuery = _sportsTestResultRepository.DbContext.Set<N_SportsTestValue>()
.Where(s => s.SchoolCode == UserContext.Current.TenantId);
if (paramDto.StartTime.HasValue)
baseQuery = baseQuery.Where(x => x.CreateDate >= paramDto.StartTime.Value);
if (paramDto.EndTime.HasValue)
baseQuery = baseQuery.Where(x => x.CreateDate <= paramDto.EndTime.Value);
if (paramDto.ClassId.HasValue)
baseQuery = baseQuery.Where(x => x.ClassId == paramDto.ClassId.Value);
if (paramDto.Sex.HasValue)
baseQuery = baseQuery.Where(x => (int)x.Sex == paramDto.Sex);
if (!string.IsNullOrWhiteSpace(paramDto.ResultLevel))
baseQuery = baseQuery.Where(x => x.Rank == paramDto.ResultLevel);
var filteredQuery = baseQuery;
if (paramDto.ItemType.HasValue && paramDto.ItemType.Value > 0)
filteredQuery = filteredQuery.Where(x => x.CategoryValue == paramDto.ItemType.Value);
var latestScoreTimeQuery = filteredQuery
.GroupBy(x => x.StudentNo)
.Select(g => new { StudentNo = g.Key, MaxScoreTime = g.Max(x => x.ScoreTime) });
var latestRecordsQuery = from record in filteredQuery
join latest in latestScoreTimeQuery
on new { record.StudentNo, record.ScoreTime }
equals new { latest.StudentNo, ScoreTime = latest.MaxScoreTime }
select record;
result.Total = await latestRecordsQuery
.Select(x => x.StudentNo)
.Distinct()
.CountAsync();
var pagedStudentKeys = await latestRecordsQuery
.OrderByDescending(x => x.ScoreTime)
.Select(x => new { x.StudentNo, x.ScoreTime })
.Distinct()
.Skip((paramDto.PageIndex - 1) * paramDto.PageSize)
.Take(paramDto.PageSize)
.ToListAsync();
if (!pagedStudentKeys.Any())
{
result.Datas = new List<SummaryOfGradesStudentsTestModel>();
return result;
}
var pagedStudentNos = pagedStudentKeys.Select(x => x.StudentNo).Distinct().ToList();
var pageRecords = await baseQuery
.Where(x => pagedStudentNos.Contains(x.StudentNo))
.Select(x => new
{
x.Id,
x.StudentNo,
x.StudentName,
x.Sex,
x.ClassName,
x.GradeName,
x.ScoreTime,
x.IsDisplay,
x.CategoryValue,
x.CategoryEnum,
x.Value,
x.Score,
x.Rank
})
.ToListAsync();
pageRecords = pageRecords
.Where(x => pagedStudentKeys.Any(k => k.StudentNo == x.StudentNo && k.ScoreTime == x.ScoreTime))
.ToList();
var sportsTestItemTypes = Enum.GetValues(typeof(SportsTestItemType))
.Cast<SportsTestItemType>()
.OrderBy(x => (int)x)
.ToList();
result.Datas = pageRecords
.GroupBy(x => new
{
x.StudentNo,
x.StudentName,
x.Sex,
x.ClassName,
x.GradeName,
x.ScoreTime,
x.IsDisplay
})
.Select(g =>
{
var records = g
.GroupBy(x => x.CategoryValue)
.ToDictionary(x => x.Key, x => x.OrderByDescending(r => r.Id).First());
return new SummaryOfGradesStudentsTestModel
{
Id = g.Max(x => x.Id),
StudentNo = g.Key.StudentNo,
StudentName = g.Key.StudentName,
SexName = g.Key.Sex.GetDescription(),
GradeAndClass = $"{g.Key.GradeName} {g.Key.ClassName}",
ScoreTime = g.Key.ScoreTime,
IsDisplay = g.Key.IsDisplay,
Items = sportsTestItemTypes.Select(itemType =>
{
records.TryGetValue((int)itemType, out var record);
return new SummaryOfGradesSportsTestItemModel
{
ItemType = (int)itemType,
ItemCode = itemType.ToString(),
ItemName = itemType.GetDisplayName(),
Unit = itemType.GetDisplayDescription(),
Value = record?.Value,
Score = record?.Score,
Rank = record?.Rank ?? string.Empty
};
}).ToList()
};
})
.OrderByDescending(x => x.ScoreTime)
.ToList();
return result;
}
/// <summary>
/// 成绩推送
/// </summary>
/// <param name="paramDto"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public async Task ScorePush(SportsHallStudentsTestParam paramDto)
{
var sql = new StringBuilder();
sql.Append("UPDATE N_SportsTestValue SET IsDisplay = 1 WHERE SchoolCode = {0} ");
var parameters = new List<object>
{
UserContext.Current.TenantId
};
if (paramDto.StartTime.HasValue)
{
sql.Append(" AND CreateDate >= {1} ");
parameters.Add(paramDto.StartTime.Value);
}
if (paramDto.EndTime.HasValue)
{
sql.Append(" AND CreateDate <= {2} ");
parameters.Add(paramDto.EndTime.Value);
}
if (paramDto.ClassId > 0)
{
sql.Append(" AND ClassId = {3} ");
parameters.Add(paramDto.ClassId.Value);
}
if (paramDto.Sex > 0)
{
sql.Append(" AND Sex = {4} ");
parameters.Add(paramDto.Sex.Value);
}
if (!string.IsNullOrWhiteSpace(paramDto.ResultLevel))
{
sql.Append(" AND Rank = {5} ");
parameters.Add(paramDto.ResultLevel);
}
await _sportsTestResultRepository.DbContext.Database
.ExecuteSqlRawAsync(sql.ToString(), parameters.ToArray());
}
/// <summary>
/// 成绩修成
/// </summary>
/// <param name="paramDto"></param>
/// <returns></returns>
public async Task ScoreUpdate(SportsHallStudentsTestDataUpdateParam paramDto)
{
// 获取该学生每个项目的最新一条记录
var list = await _sportsTestResultRepository.DbContext.Set<N_SportsTestValue>()
.Where(s => s.SchoolCode == UserContext.Current.TenantId && s.StudentNo == paramDto.StudentNo)
.GroupBy(x => x.CategoryValue)
.Select(g => g.OrderByDescending(x => x.ScoreTime).FirstOrDefault())
.ToListAsync();
if (list == null || !list.Any())
throw new Exception($"未找到要修改得数据");
foreach (var entity in list)
{
switch (entity.CategoryValue)
{
case (int)SportsTestItemType.BMI:
entity.Value = paramDto.BMIValue;
break;
case (int)SportsTestItemType.VitalCapacity:
entity.Value = paramDto.VitalCapacityValue;
break;
case (int)SportsTestItemType.Sit_And_Reach:
entity.Value = paramDto.Sit_And_ReachValue;
break;
case (int)SportsTestItemType.OneMinuteJumpRope:
entity.Value = paramDto.OneMinuteJumpRopeValue;
break;
case (int)SportsTestItemType.One_Minute_Sit_Up:
entity.Value = paramDto.One_Minute_Sit_UpValue;
break;
case (int)SportsTestItemType.StandingLongJump:
entity.Value = paramDto.StandingLongJumpValue;
break;
}
}
_sportsTestResultRepository.UpdateRange(list);
await _sportsTestResultRepository.DbContext.SaveChangesAsync();
}
}
}