using AutoMapper; using Castle.DynamicProxy.Generators; using Google.Protobuf.Collections; 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.IO.Compression; using System.Linq; using System.Linq.Expressions; using System.Text; using System.Threading.Tasks; using VOL.Business.IRepositories; using VOL.Business.IServices.Norm; using VOL.Business.IServices.School; using VOL.Core.CacheManager; using VOL.Core.Extensions; using VOL.Core.Extensions.AutofacManager; using VOL.Core.ManageUser; using VOL.Core.Utilities; using VOL.Entity.DomainModels; using VOL.Entity.DomainModels.Business.People; using VOL.Entity.Enum; using VOL.Model; using VOL.Model.Ai; using VOL.Model.IOT.Request; using VOL.Model.School.Request; using VOL.Model.School.Response; using VOL.System.IRepositories; using VOL.System.Repositories; using static Microsoft.EntityFrameworkCore.DbLoggerCategory; namespace VOL.Business.Services.School { public class S_StudentService : IS_StudentService, IDependency { #region 初始化 private readonly IMapper _mapper; private readonly ICacheService _cacheService; private readonly ICacheQueryService _cacheQueryService; private readonly IotDataSyncService _iotDataSyncService; private readonly IS_StudentRepository _studentRepository; private readonly IS_GradeRepository _gradeRepository; private readonly IS_ClassRepository _classRepository; private readonly IN_SportsTestResultRepository _sportsTestResultRepository; private readonly IN_SportsTestCategoryRepository _sportsTestCategoryRepository; [ActivatorUtilitiesConstructor] public S_StudentService(IMapper mapper, ICacheService cacheService, ICacheQueryService cacheQueryService, IS_StudentRepository studentRepository, IS_GradeRepository gradeRepository, IS_ClassRepository classRepository, IN_SportsTestResultRepository sportsTestResultRepository, IN_SportsTestCategoryRepository sportsTestCategoryRepository, IotDataSyncService iotDataSyncService) { _mapper = mapper; _cacheService = cacheService; _cacheQueryService = cacheQueryService; _studentRepository = studentRepository; _gradeRepository = gradeRepository; _classRepository = classRepository; _sportsTestResultRepository = sportsTestResultRepository; _sportsTestCategoryRepository = sportsTestCategoryRepository; _iotDataSyncService = iotDataSyncService; } #endregion public async Task> GetStudentPageList(StudentPageListParam paramDto) { var res = new PageDataDto(); var classIds = new List(); var isTeacher = (UserContext.Current.RoleId == 3); if (isTeacher) { var teacher = await _studentRepository.DbContext.Set().FirstOrDefaultAsync(x => x.SchoolCode == UserContext.Current.TenantId && x.TeacherPhoneNo == UserContext.Current.UserInfo.PhoneNo); var teacherClassList = await ( from at in _studentRepository.DbContext.Set() join c in _studentRepository.DbContext.Set() on at.ClassId equals c.Id where at.TeacherId == teacher.Id select c).ToListAsync(); classIds = teacherClassList.Select(c => c.Id).Distinct().ToList(); } var query = from s in _studentRepository.DbContext.Set() join c in _studentRepository.DbContext.Set() on s.ClassId equals c.Id into sc from c in sc.DefaultIfEmpty() where s.SchoolCode.Equals(UserContext.Current.TenantId) && s.StudentStatus == StudentStatus.Normal && (!isTeacher || classIds.Contains(c.Id)) select new StudentPageListModel() { Id = s.Id, Age = s.Age, ClassId = s.ClassId, ClassName = c.ClassName, GradeName = c.GradeName, HeartRateFrontNo = s.HeartRateFrontNo, HeartRateId = s.HeartRateId, Photo = s.Photo, RopeSkipQRCode = s.RopeSkipQRCode, SchoolRollNo = s.SchoolRollNo, Sex = s.Sex, StudentName = s.StudentName, StudentNo = s.StudentNo, OrderNo = s.OrderNo, StudentStatus = s.StudentStatus }; if (!string.IsNullOrWhiteSpace(paramDto.StudentName)) { query = query.Where(x => x.StudentName.Contains(paramDto.StudentName)); } if (!string.IsNullOrWhiteSpace(paramDto.StudentNo)) { query = query.Where(x => x.StudentNo.Contains(paramDto.StudentNo)); } if (paramDto.Sex > 0) { query = query.Where(x => x.Sex == paramDto.Sex); } if (paramDto.ClassId > 0) { query = query.Where(x => x.ClassId == paramDto.ClassId); } if (paramDto.StudentStatus > 0) { query = query.Where(x => x.StudentStatus == paramDto.StudentStatus); } res.Total = await query.CountAsync(); var list = await query .OrderBy(x => x.Id) .Skip((paramDto.PageIndex - 1) * paramDto.PageSize) .Take(paramDto.PageSize) .ToListAsync(); res.Datas = list; return res; } public async Task> GetStudentList(StudentExportParam paramDto) { var classIds = new List(); var isTeacher = (UserContext.Current.RoleId == 3); if (isTeacher) { var teacher = await _studentRepository.DbContext.Set().FirstOrDefaultAsync(x => x.SchoolCode == UserContext.Current.TenantId && x.TeacherPhoneNo == UserContext.Current.UserInfo.PhoneNo); var teacherClassList = await ( from at in _studentRepository.DbContext.Set() join c in _studentRepository.DbContext.Set() on at.ClassId equals c.Id where at.TeacherId == teacher.Id select c).ToListAsync(); classIds = teacherClassList.Select(c => c.Id).Distinct().ToList(); } var query = from s in _studentRepository.DbContext.Set() join c in _studentRepository.DbContext.Set() on s.ClassId equals c.Id where c.SchoolCode.Equals(UserContext.Current.TenantId) && s.StudentStatus == StudentStatus.Normal && s.StudentStatus == StudentStatus.Normal && (!isTeacher || classIds.Contains(c.Id)) select new StudentPageListModel() { Id = s.Id, Age = s.Age, ClassId = s.ClassId, ClassName = c.ClassName, GradeName = c.GradeName, HeartRateFrontNo = s.HeartRateFrontNo, HeartRateId = s.HeartRateId, Photo = s.Photo, RopeSkipQRCode = s.RopeSkipQRCode, SchoolRollNo = s.SchoolRollNo, Sex = s.Sex, StudentName = s.StudentName, StudentNo = s.StudentNo, StudentStatus = s.StudentStatus }; if (!string.IsNullOrWhiteSpace(paramDto.StudentName)) { query = query.Where(x => x.StudentName.Contains(paramDto.StudentName)); } if (!string.IsNullOrWhiteSpace(paramDto.StudentNo)) { query = query.Where(x => x.StudentNo.Contains(paramDto.StudentNo)); } if (paramDto.Sex > 0) { query = query.Where(x => x.Sex == paramDto.Sex); } if (paramDto.ClassId > 0) { query = query.Where(x => x.ClassId == paramDto.ClassId); } if (paramDto.StudentStatus > 0) { query = query.Where(x => x.StudentStatus == paramDto.StudentStatus); } var list = await query.OrderBy(x => x.Id).ToListAsync(); return list; } /// /// 获取场馆学员 /// /// /// /// public async Task> GetStudentListBySportsHall(StudentExportParam paramDto) { var query = from s in _studentRepository.DbContext.Set() where s.SchoolCode.Equals(UserContext.Current.TenantId) && s.StudentStatus == StudentStatus.Normal && s.StudentStatus == StudentStatus.Normal select new StudentPageListModel() { Id = s.Id, Age = s.Age, ClassId = s.ClassId, ClassName = s.ClassName, GradeName = "运动馆", HeartRateFrontNo = s.HeartRateFrontNo, HeartRateId = s.HeartRateId, Photo = s.Photo, RopeSkipQRCode = s.RopeSkipQRCode, SchoolRollNo = s.SchoolRollNo, Sex = s.Sex, StudentName = s.StudentName, StudentNo = s.StudentNo, StudentStatus = s.StudentStatus, OrderNo = s.OrderNo, }; if (!string.IsNullOrWhiteSpace(paramDto.StudentName)) { query = query.Where(x => x.StudentName.Contains(paramDto.StudentName)); } if (!string.IsNullOrWhiteSpace(paramDto.StudentNo)) { query = query.Where(x => x.StudentNo.Contains(paramDto.StudentNo)); } if (paramDto.Sex > 0) { query = query.Where(x => x.Sex == paramDto.Sex); } if (paramDto.ClassId > 0) { query = query.Where(x => x.ClassId == paramDto.ClassId); } if (paramDto.StudentStatus > 0) { query = query.Where(x => x.StudentStatus == paramDto.StudentStatus); } var list = await query.OrderBy(x => x.OrderNo).ToListAsync(); return list; } public async Task AddStudent(AddStudentParam paramDto) { if (paramDto == null || string.IsNullOrWhiteSpace(paramDto.StudentNo)) throw new ArgumentNullException("学生学号必填"); var tenantId = UserContext.Current.TenantId; paramDto.StudentNo = paramDto.StudentNo.Trim(); var stuExists = await _studentRepository.ExistsAsync(x => x.SchoolCode == tenantId && x.StudentNo == paramDto.StudentNo); if (stuExists) throw new Exception($"学号{paramDto.StudentNo}的学生已存在!"); var studentEntity = _mapper.Map(paramDto); studentEntity.StudentStatus = StudentStatus.Normal; studentEntity.SchoolCode = tenantId; studentEntity.Creator = UserContext.Current.UserId; studentEntity.CreateDate = DateTime.Now; await _studentRepository.AddAsync(studentEntity); await _studentRepository.SaveChangesAsync(); //调用回调函数,同步数据到IOT //_ = Task.Run(() => _iotDataSyncService.DataSyncCallBack(new DataSyncCallBackParam //{ // EventType = EventType.Add.GetDisplayName(), // DataType = IOTDataSyncType.Student.GetDisplayName(), // Json = JsonConvert.SerializeObject(new List() { studentEntity }) //})); await _iotDataSyncService.DataSyncCallBack(new DataSyncCallBackParam { EventType = EventType.Add.GetDisplayName(), DataType = IOTDataSyncType.Student.GetDisplayName(), Json = JsonConvert.SerializeObject(new List() { studentEntity }) }); } public async Task ModifyStudent(AddStudentParam paramDto) { if (paramDto == null || string.IsNullOrWhiteSpace(paramDto.StudentNo)) { throw new ArgumentNullException("学生学号必填"); } var tenantId = UserContext.Current.TenantId; paramDto.StudentNo = paramDto.StudentNo.Trim(); var student = await _studentRepository.FindAsyncFirst(x => x.Id == paramDto.Id && x.SchoolCode == tenantId); if (student == null) throw new ArgumentNullException("未找到要更新的数据"); var stuExists = await _studentRepository.ExistsAsync(x => x.SchoolCode == tenantId && x.StudentNo == paramDto.StudentNo && x.Id != student.Id); if (stuExists) throw new Exception($"学号{paramDto.StudentNo}的学生已存在!"); student.StudentName = paramDto.StudentName; // 学生名称 student.ClassId = paramDto.ClassId; // 班级 student.Sex = paramDto.Sex; // 性别 student.Age = paramDto.Age; // 年龄 student.Birthday = paramDto.Birthday; // 出生日期 student.IDCard = paramDto.IDCard; // 身份证 student.Photo = paramDto.Photo; // 学生照片 student.HeartRateFrontNo = paramDto.HeartRateFrontNo; // 心率设备正面编号 student.HeartRateId = paramDto.HeartRateId; // 心率设备ID student.RunChipNo = paramDto.RunChipNo; // 中长跑芯片编号 student.RopeSkipNo = paramDto.RopeSkipQRCode; // 跳绳编号 student.OrderNo = 1; student.Creator = UserContext.Current.UserId; student.Modifier = UserContext.Current.UserId; student.ModifyDate = DateTime.Now; student.StudentStatus = StudentStatus.Normal; _studentRepository.Update(student); await _studentRepository.SaveChangesAsync(); //调用回调函数,同步数据到IOT //_ = Task.Run(() => _iotDataSyncService.DataSyncCallBack(new DataSyncCallBackParam //{ // EventType = EventType.Update.GetDisplayName(), // DataType = IOTDataSyncType.Student.GetDisplayName(), // Json = JsonConvert.SerializeObject(student) //})); await _iotDataSyncService.DataSyncCallBack(new DataSyncCallBackParam { EventType = EventType.Update.GetDisplayName(), DataType = IOTDataSyncType.Student.GetDisplayName(), Json = JsonConvert.SerializeObject(student) }); } public async Task UpdateStudentStatus(UpdateStudentStatusParam paramDto) { var student = await _studentRepository.FindAsyncFirst(x => x.StudentNo == paramDto.StudentNo); if (student == null) throw new ArgumentNullException("未找到要更新的数据"); student.StudentStatus = paramDto.Status; _studentRepository.Update(student); await _studentRepository.SaveChangesAsync(); //调用回调函数,同步数据到IOT _ = Task.Run(() => _iotDataSyncService.DataSyncCallBack(new DataSyncCallBackParam { EventType = EventType.Update.GetDisplayName(), DataType = IOTDataSyncType.Student.GetDisplayName(), Json = JsonConvert.SerializeObject(student) })); } public async Task UpdateStudentPwd(UpdateStudentPwdParam paramDto) { var student = await _studentRepository.FindAsyncFirst(x => x.StudentNo == paramDto.StudentNo); if (student == null) throw new ArgumentNullException("未找到要更新的数据"); student.AppletPwd = paramDto.StudentNo; _studentRepository.Update(student); await _studentRepository.SaveChangesAsync(); //调用回调函数,同步数据到IOT //_ = Task.Run(() => _iotDataSyncService.DataSyncCallBack(new DataSyncCallBackParam //{ // EventType = EventType.Update.GetDisplayName(), // DataType = IOTDataSyncType.Student.GetDisplayName(), // Json = JsonConvert.SerializeObject(student) //})); await _iotDataSyncService.DataSyncCallBack(new DataSyncCallBackParam { EventType = EventType.Update.GetDisplayName(), DataType = IOTDataSyncType.Student.GetDisplayName(), Json = JsonConvert.SerializeObject(student) }); } public async Task BatchDeleteStudents(List ids) { if (UserContext.Current.RoleId != 1) { var menuIds = await _studentRepository.DbContext.Set() .Where(menu => menu.Url == "/studentsManage" && menu.ParentId > 0) .Select(menu => menu.Menu_Id) .ToListAsync(); if (!UserContext.Current.Permissions.Any(permission => menuIds.Contains(permission.Menu_Id) && permission.UserAuthArr.Contains("BatchDelete"))) throw new Exception("没有批量删除学生的权限"); } var studentIds = ids?.Where(x => x > 0).Distinct().ToList() ?? new List(); if (studentIds.Count == 0) throw new ArgumentException("请至少选择一名学生"); await using var transaction = await _studentRepository.DbContext.Database.BeginTransactionAsync(IsolationLevel.Serializable); var activeBatchIds = await _studentRepository.DbContext.Set() .Where(x => x.SchoolCode == UserContext.Current.TenantId && x.Status == 1) .Select(x => x.Id) .ToListAsync(); var promotionDetails = await _studentRepository.DbContext.Set() .Where(x => activeBatchIds.Contains(x.BatchId) && studentIds.Contains(x.StudentId)) .ToListAsync(); var students = await _studentRepository.FindAsIQueryable(x => x.SchoolCode == UserContext.Current.TenantId && studentIds.Contains(x.Id)) .ToListAsync(); if (students.Count != studentIds.Count) throw new Exception("部分学生不存在或不属于当前学校,请刷新列表后重新选择"); _studentRepository.DbContext.RemoveRange(promotionDetails); _studentRepository.DbContext.RemoveRange(students); await _studentRepository.SaveChangesAsync(); await transaction.CommitAsync(); await _iotDataSyncService.DataSyncCallBack(new DataSyncCallBackParam { EventType = EventType.Delete.GetDisplayName(), DataType = IOTDataSyncType.Student.GetDisplayName(), Json = JsonConvert.SerializeObject(students) }); return students.Count; } public async Task ChangeClasses(string studentNo, int classId) { var student = await _studentRepository.FindAsyncFirst(x => x.StudentNo == studentNo); if (student == null) throw new ArgumentNullException("未找到更新的数据"); var classModel = await _classRepository.FindAsyncFirst(x => x.Id == student.ClassId); var sportsTestResults = await _sportsTestResultRepository.FindAsIQueryable(x => x.ClassId == student.ClassId && x.StudentNo == studentNo).ToListAsync(); using (var transaction = _studentRepository.DbContext.Database.BeginTransaction()) { try { student.ClassId = classId; student.ClassName = classModel.ClassName; student.Modifier = UserContext.Current.UserId; student.ModifyDate = DateTime.Now; _studentRepository.Update(student); foreach (var item in sportsTestResults) { item.ClassId = classId; item.ClassName = classModel.ClassName; item.Modifier = UserContext.Current.UserId; item.ModifyDate = DateTime.Now; } _sportsTestResultRepository.UpdateRange(sportsTestResults); await _studentRepository.SaveChangesAsync(); // 提交事务 transaction.Commit(); } catch (Exception ex) { // 发生错误,回滚事务 transaction.Rollback(); throw new Exception("操作失败"); } } } public async Task ImportStudents(IFormFile file) { const long maxFileSize = 10 * 1024 * 1024; const int maxRowCount = 10000; if (file == null || file.Length == 0) throw new Exception("请选择需要导入的Excel文件"); if (file.Length > maxFileSize) throw new Exception("Excel文件大小不能超过10MB"); var extension = Path.GetExtension(file.FileName); if (!string.Equals(extension, ".xls", StringComparison.OrdinalIgnoreCase) && !string.Equals(extension, ".xlsx", StringComparison.OrdinalIgnoreCase)) throw new Exception("仅支持.xls或.xlsx格式的Excel文件"); List dataObjects; try { using var fileStream = file.OpenReadStream(); dataObjects = Tool.ConvertExcelToList(fileStream); } catch { throw new Exception("Excel文件解析失败,请使用系统下载的标准模板并检查文件内容"); } if (dataObjects == null || dataObjects.Count == 0) throw new Exception("Excel文件中没有可导入的学生数据"); if (dataObjects.Count > maxRowCount) throw new Exception($"一次最多导入{maxRowCount}名学生,当前文件包含{dataObjects.Count}行数据"); var validationErrors = new List(); for (var index = 0; index < dataObjects.Count; index++) { var data = dataObjects[index]; var rowNumber = index + 2; data.StudentNo = data.StudentNo?.Trim(); data.StudentName = data.StudentName?.Trim(); data.GradeName = data.GradeName?.Trim(); data.ClassName = data.ClassName?.Trim(); data.Sex = data.Sex?.Trim(); data.IDCard = data.IDCard?.Trim(); data.HeartRateFrontNo = data.HeartRateFrontNo?.Trim(); data.HeartRateId = data.HeartRateId?.Trim(); data.RunChipNo = data.RunChipNo?.Trim(); data.RopeSkipQRCode = data.RopeSkipQRCode?.Trim(); if (string.IsNullOrWhiteSpace(data.StudentNo)) validationErrors.Add($"第{rowNumber}行:学号不能为空"); if (string.IsNullOrWhiteSpace(data.StudentName)) validationErrors.Add($"第{rowNumber}行:姓名不能为空"); if (string.IsNullOrWhiteSpace(data.GradeName)) validationErrors.Add($"第{rowNumber}行:年级不能为空"); if (string.IsNullOrWhiteSpace(data.ClassName)) validationErrors.Add($"第{rowNumber}行:班级不能为空"); if (data.Age <= 0 || data.Age > 100) validationErrors.Add($"第{rowNumber}行:年龄必须为1至100之间的整数"); if (data.Sex != "男" && data.Sex != "女") validationErrors.Add($"第{rowNumber}行:性别只能填写“男”或“女”"); } var duplicateStudentNos = dataObjects .Where(x => !string.IsNullOrWhiteSpace(x.StudentNo)) .Select((data, index) => new { data.StudentNo, RowNumber = index + 2 }) .GroupBy(x => x.StudentNo, StringComparer.OrdinalIgnoreCase) .Where(group => group.Count() > 1); foreach (var duplicate in duplicateStudentNos) { validationErrors.Add( $"学号{duplicate.Key}在文件中重复,位于第{string.Join("、", duplicate.Select(x => x.RowNumber))}行"); } if (validationErrors.Count > 0) throw new Exception($"导入数据校验失败:{string.Join(";", validationErrors.Take(10))}" + (validationErrors.Count > 10 ? $";另有{validationErrors.Count - 10}项错误" : string.Empty)); var tenantId = UserContext.Current.TenantId; var gradeNames = dataObjects.Select(x => x.GradeName).Distinct().ToList(); var gradeList = await _gradeRepository.FindAsync(x => gradeNames.Contains(x.GradeName)); var gradeByName = gradeList .GroupBy(x => x.GradeName, StringComparer.OrdinalIgnoreCase) .ToDictionary(group => group.Key, group => group.First(), StringComparer.OrdinalIgnoreCase); var gradeIds = gradeList.Select(x => x.Id).ToList(); var classList = await _classRepository.FindAsync( x => gradeIds.Contains(x.GradeId) && x.SchoolCode == tenantId); var classByGradeAndName = classList .GroupBy(x => $"{x.GradeId}\u001f{x.ClassName}", StringComparer.OrdinalIgnoreCase) .ToDictionary(group => group.Key, group => group.First(), StringComparer.OrdinalIgnoreCase); var studentNos = dataObjects.Select(x => x.StudentNo).ToList(); var existingStudents = await _studentRepository.FindAsync( x => x.SchoolCode == tenantId && studentNos.Contains(x.StudentNo)); var existingStudentNos = existingStudents .Select(x => x.StudentNo) .ToHashSet(StringComparer.OrdinalIgnoreCase); var students = new List(); for (var index = 0; index < dataObjects.Count; index++) { var data = dataObjects[index]; var rowNumber = index + 2; if (existingStudentNos.Contains(data.StudentNo)) { validationErrors.Add($"第{rowNumber}行:学号{data.StudentNo}的学生已存在"); continue; } if (!gradeByName.TryGetValue(data.GradeName, out var grade)) { validationErrors.Add($"第{rowNumber}行:未找到年级“{data.GradeName}”"); continue; } if (!classByGradeAndName.TryGetValue($"{grade.Id}\u001f{data.ClassName}", out var studentClass)) { validationErrors.Add($"第{rowNumber}行:年级“{data.GradeName}”下未找到班级“{data.ClassName}”"); continue; } students.Add(new S_Student { SchoolCode = tenantId, StudentName = data.StudentName, StudentNo = data.StudentNo, ClassId = studentClass.Id, ClassName = studentClass.ClassName, Age = data.Age, IDCard = data.IDCard, Birthday = data.Birthday, HeartRateFrontNo = data.HeartRateFrontNo, HeartRateId = data.HeartRateId, RopeSkipQRCode = data.RopeSkipQRCode, Sex = data.Sex == "男" ? SexType.Male : SexType.Female, RunChipNo = data.RunChipNo, StudentStatus = StudentStatus.Normal, Creator = UserContext.Current.UserId, CreateDate = DateTime.Now }); } if (validationErrors.Count > 0) throw new Exception($"导入数据校验失败:{string.Join(";", validationErrors.Take(10))}" + (validationErrors.Count > 10 ? $";另有{validationErrors.Count - 10}项错误" : string.Empty)); await _studentRepository.AddRangeAsync(students); await _studentRepository.SaveChangesAsync(); //调用回调函数,同步数据到IOT //_ = Task.Run(() => _iotDataSyncService.DataSyncCallBack(new DataSyncCallBackParam //{ // EventType = EventType.Add.GetDisplayName(), // DataType = IOTDataSyncType.Student.GetDisplayName(), // Json = JsonConvert.SerializeObject(entitys) //})); await _iotDataSyncService.DataSyncCallBack(new DataSyncCallBackParam { EventType = EventType.Add.GetDisplayName(), DataType = IOTDataSyncType.Student.GetDisplayName(), Json = JsonConvert.SerializeObject(students) }); return students.Count; } /// /// 上传学生头像 /// public async Task UploadPhoto(IFormFile file, int gradeId, int classId, string studentNo, string? studentName) { const long maxFileSize = 2 * 1024 * 1024; if (file == null || file.Length == 0) throw new Exception("请选择需要上传的人脸照片"); if (file.Length > maxFileSize) throw new Exception("单张人脸照片大小不能超过2MB"); if (string.IsNullOrWhiteSpace(studentNo)) throw new Exception("学生学号不能为空"); // 编辑时上传组件可能只传文件和学号,此时使用当前学校已有的学生姓名。 // 新增学生尚未入库,仍需要客户端显式传入姓名。 if (string.IsNullOrWhiteSpace(studentName)) { studentName = await _studentRepository.DbContext.Set() .AsNoTracking() .Where(x => x.SchoolCode == UserContext.Current.TenantId && x.StudentNo == studentNo) .Select(x => x.StudentName) .FirstOrDefaultAsync(); } if (string.IsNullOrWhiteSpace(studentName)) throw new Exception("学生姓名不能为空"); var extension = Path.GetExtension(file.FileName); if (!IsSupportedPhotoExtension(extension)) throw new Exception("人脸照片仅支持.jpg、.jpeg或.png格式"); if (!await HasValidPhotoSignature(file, extension)) throw new Exception("图片内容与文件格式不一致或文件已损坏"); var url = ALiYunOss.Upload(file, $"Upload/{UserContext.Current.TenantId}/Photo/Student/{gradeId}/{classId}/", studentNo); var faceEntityWithRequest = new FaceEntityWithDto() { EntityId = studentNo, SchoolCode = UserContext.Current.TenantId, IsStudent = true }; await ALiYunFace.DeleteFaceEntityWith(faceEntityWithRequest); var faceEntityResult = await ALiYunFace.FaceEntityWith(faceEntityWithRequest); if (!faceEntityResult) throw new Exception("人脸库学生信息创建失败"); var faceResult = await ALiYunFace.FaceWith(new FaceWithDto() { SchoolCode = UserContext.Current.TenantId, Name = studentName, IsStudent = true, EntityId = studentNo, ImageUrl = url }); if (!faceResult) throw new Exception("人脸照片注册失败,请更换清晰的正面照片后重试"); return url; } public async Task BatchUploadPhoto(IFormFile zipFile) { const long maxZipFileSize = 100 * 1024 * 1024; const long maxUncompressedSize = 500 * 1024 * 1024; const int maxPhotoCount = 2000; const int maxReturnedErrors = 30; if (zipFile == null || zipFile.Length == 0) throw new Exception("请选择需要导入的ZIP压缩包"); if (zipFile.Length > maxZipFileSize) throw new Exception("ZIP压缩包大小不能超过100MB"); if (!string.Equals(Path.GetExtension(zipFile.FileName), ".zip", StringComparison.OrdinalIgnoreCase)) throw new Exception("批量导入仅支持.zip格式的压缩包"); try { using var zipStream = zipFile.OpenReadStream(); using var archive = new ZipArchive(zipStream, ZipArchiveMode.Read, false); var entries = archive.Entries .Where(entry => !string.IsNullOrWhiteSpace(entry.Name)) .Where(entry => !entry.FullName.StartsWith("__MACOSX/", StringComparison.OrdinalIgnoreCase)) .Where(entry => !string.Equals(entry.Name, ".DS_Store", StringComparison.OrdinalIgnoreCase)) .ToList(); if (entries.Count == 0) throw new Exception("ZIP压缩包中没有可导入的图片"); if (entries.Count > maxPhotoCount) throw new Exception($"一次最多导入{maxPhotoCount}张人脸照片,当前压缩包包含{entries.Count}个文件"); if (entries.Sum(entry => entry.Length) > maxUncompressedSize) throw new Exception("ZIP解压后的文件总大小不能超过500MB"); var result = new BatchUploadStudentPhotoResult { TotalCount = entries.Count }; void AddFailure(string message) { result.FailedCount++; if (result.Errors.Count < maxReturnedErrors) result.Errors.Add(message); } var validEntries = entries .Where(entry => IsSupportedPhotoExtension(Path.GetExtension(entry.Name))) .ToList(); var studentNos = validEntries .Select(entry => Path.GetFileNameWithoutExtension(entry.Name).Trim()) .Where(studentNo => !string.IsNullOrWhiteSpace(studentNo)) .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); var tenantId = UserContext.Current.TenantId; var students = await _studentRepository.FindAsync( x => x.SchoolCode == tenantId && studentNos.Contains(x.StudentNo)); var studentByNo = students .GroupBy(x => x.StudentNo, StringComparer.OrdinalIgnoreCase) .ToDictionary(group => group.Key, group => group.First(), StringComparer.OrdinalIgnoreCase); var classIds = students.Select(x => x.ClassId).Distinct().ToList(); var classes = await _classRepository.FindAsync( x => x.SchoolCode == tenantId && classIds.Contains(x.Id)); var classById = classes.ToDictionary(x => x.Id); var processedStudentNos = new HashSet(StringComparer.OrdinalIgnoreCase); var updatedStudents = new List(); foreach (var entry in entries) { var extension = Path.GetExtension(entry.Name); if (!IsSupportedPhotoExtension(extension)) { AddFailure($"{entry.Name}:仅支持.jpg、.jpeg或.png格式"); continue; } if (entry.Length == 0) { AddFailure($"{entry.Name}:图片文件为空"); continue; } if (entry.Length > 2 * 1024 * 1024) { AddFailure($"{entry.Name}:单张图片不能超过2MB"); continue; } var studentNo = Path.GetFileNameWithoutExtension(entry.Name).Trim(); if (string.IsNullOrWhiteSpace(studentNo)) { AddFailure($"{entry.Name}:文件名必须为学生学号"); continue; } if (!processedStudentNos.Add(studentNo)) { AddFailure($"{entry.Name}:学号{studentNo}在压缩包中存在重复照片"); continue; } if (!studentByNo.TryGetValue(studentNo, out var student)) { AddFailure($"{entry.Name}:当前学校未找到学号为{studentNo}的学生"); continue; } try { await using var entryStream = entry.Open(); await using var photoStream = new MemoryStream(); await entryStream.CopyToAsync(photoStream); photoStream.Position = 0; var formFile = new FormFile(photoStream, 0, photoStream.Length, "file", entry.Name); classById.TryGetValue(student.ClassId, out var studentClass); var url = await UploadPhoto( formFile, studentClass?.GradeId ?? 0, student.ClassId, student.StudentNo, student.StudentName); student.Photo = url; updatedStudents.Add(student); result.SuccessCount++; } catch (Exception ex) { AddFailure($"{entry.Name}:{ex.Message}"); } } if (updatedStudents.Count > 0) { _studentRepository.UpdateRange(updatedStudents); await _studentRepository.SaveChangesAsync(); } return result; } catch (InvalidDataException) { throw new Exception("ZIP压缩包已损坏、被加密或格式不正确"); } } private static bool IsSupportedPhotoExtension(string extension) { return string.Equals(extension, ".jpg", StringComparison.OrdinalIgnoreCase) || string.Equals(extension, ".jpeg", StringComparison.OrdinalIgnoreCase) || string.Equals(extension, ".png", StringComparison.OrdinalIgnoreCase); } private static async Task HasValidPhotoSignature(IFormFile file, string extension) { var header = new byte[8]; await using var stream = file.OpenReadStream(); var bytesRead = await stream.ReadAsync(header, 0, header.Length); if (string.Equals(extension, ".png", StringComparison.OrdinalIgnoreCase)) { var pngSignature = new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }; return bytesRead >= pngSignature.Length && header.Take(pngSignature.Length).SequenceEqual(pngSignature); } return bytesRead >= 3 && header[0] == 0xFF && header[1] == 0xD8 && header[2] == 0xFF; } /// /// 学生数据统计 /// /// /// /// public async Task StudentWholeDataStats(StudentDataStatsParam paramDto) { var res = new StudentDataStatsModel(); var tenantId = UserContext.Current.TenantId; if (string.IsNullOrEmpty(paramDto.StudentNo)) { paramDto.StudentNo = _studentRepository.DbContext.Set().First().StudentNo; } var student = await (from s in _studentRepository.DbContext.Set() join c in _studentRepository.DbContext.Set() on s.ClassId equals c.Id where s.StudentNo == paramDto.StudentNo && s.SchoolCode.Equals(UserContext.Current.TenantId) select new StudentPageListModel() { StudentName = s.StudentName, GradeId = c.GradeId, ClassId = s.ClassId, ClassName = c.ClassName, GradeName = c.GradeName, Photo = s.Photo, Age = s.Age, Sex = s.Sex }).FirstOrDefaultAsync(); if (student == null) throw new ArgumentNullException("未找到学生数据"); res.StudentName = student.StudentName; res.ClassName = student.ClassName; res.GradeName = student.GradeName; res.Photo = student.Photo; res.Sex = student.Sex.Description(); res.Age = student.Age; var heartRateData = await (from t in _studentRepository.DbContext.Set() join s in _studentRepository.DbContext.Set() on t.Id equals s.TrainingDataId where t.SchoolCode.Equals(UserContext.Current.TenantId) && t.ItemType == (int)TrainingItemType.HeartRate && s.StudentNo == paramDto.StudentNo select new { t.Id, s.HighHeartRate }) .OrderByDescending(x => x.Id) .Take(5) .ToListAsync(); var keys = new[] { "一", "二", "三", "四", "五" }; if (heartRateData.Count == 0) { res.HeartRateData = keys.ToDictionary(key => key, key => 0); } else { res.HeartRateData = heartRateData .Select((x, index) => new { Key = keys[index], Value = x.HighHeartRate ?? 0 }) .ToDictionary(x => x.Key, x => x.Value); } // 从缓存中获取数据 var sportsTestResults = await _cacheQueryService.GetSportsTestDataCacheAsync(x => x.SchoolCode.Equals(tenantId) && x.StudentNo == paramDto.StudentNo, paramDto.Mode); var categoryList = await (from s in _sportsTestResultRepository.DbContext.Set() join n in _sportsTestResultRepository.DbContext.Set() on s.CategoryValue equals n.CategoryValue where s.GradeId == student.GradeId select new { n.CategoryValue, n.CategoryName }).ToListAsync(); foreach (var category in categoryList) { var categoryResults = sportsTestResults.Where(x => x.CategoryValue == category.CategoryValue).ToList(); var maxValue = categoryResults.Any() ? categoryResults.Max(x => x.Score + x.AdditionalScore) : 0f; var lastValue = categoryResults.OrderByDescending(x => x.ScoreTime).Select(x => x.Score + x.AdditionalScore).FirstOrDefault(); var resultContrast = new ResultContrast { MaxValue = maxValue, LastValue = lastValue }; res.TestResultCompare.AxisX.Add(category.CategoryName); res.TestResultCompare.AxisY.Add(resultContrast); } return res; } /// /// 各项目平均成绩 /// /// /// /// public async Task> TestResultAvg(StudentCategoryParam paramDto) { var sportsTestResults = await _cacheQueryService.GetSportsTestDataCacheAsync(x => x.StudentNo == paramDto.StudentNo && x.SchoolCode.Equals(UserContext.Current.TenantId) && x.CategoryValue == paramDto.CategoryValue , paramDto.Mode); var now = DateTime.Now; var currentQuarter = (now.Month - 1) / 3 + 1; var currentYear = now.Year; // 处理跨年情况 var quarters = new[] { new { Label = "本季度", Quarter = currentQuarter, Year = currentYear }, new { Label = "上季度", Quarter = currentQuarter - 1 == 0 ? 4 : currentQuarter - 1, Year = currentQuarter == 1 ? currentYear - 1 : currentYear }, new { Label = "前季度", Quarter = currentQuarter - 2 == 0 ? 4 : currentQuarter - 2 == -1 ? 3 : currentQuarter - 2, Year = currentQuarter <= 2 ? currentYear - 1 : currentYear } }; return quarters.ToDictionary( q => q.Label, q => { var filteredResults = sportsTestResults .Where(x => x.ScoreTime >= Tool.GetQuarterStartDate(q.Quarter, q.Year) && x.ScoreTime <= Tool.GetQuarterEndDate(q.Quarter, q.Year)) .Select(x => x.Score + x.AdditionalScore) .ToList(); return filteredResults.Any() ? (float)Math.Round(filteredResults.Average(), 2) : 0; } ); } /// /// 各项目成绩对比 /// /// /// /// public async Task> TestResultContrast(StudentCategoryParam paramDto) { // 获取指定班级和类别的测试结果 var sportsTestResults = await _cacheQueryService.GetSportsTestDataCacheAsync(x => x.ClassId == paramDto.ClassId && x.SchoolCode.Equals(UserContext.Current.TenantId) && x.CategoryValue == paramDto.CategoryValue , paramDto.Mode); // 提取班级所有成绩 var filteredResults = sportsTestResults .Select(x => x.Score + x.AdditionalScore) .ToList(); // 提取个人成绩 var personalScore = sportsTestResults .Where(x => x.StudentNo == paramDto.StudentNo) .Select(x => x.Score + x.AdditionalScore) .FirstOrDefault(); var quarters = new[] { new { Label = "个人成绩", Value = personalScore }, new { Label = "班级平均分", Value = filteredResults.Any() ? (float)Math.Round(filteredResults.Average(),2) : 0f }, new { Label = "班级中位数", Value = filteredResults.Any() ? (float)Math.Round(filteredResults.Median(),2) : 0f }, new { Label = "班级最高分", Value = filteredResults.Any() ? (float)Math.Round(filteredResults.Max(),2) : 0f } }; return quarters.ToDictionary(q => q.Label, q => q.Value); } /// /// 成绩趋势 /// /// /// /// public async Task ResultTrends(StudentResultTrendsParam paramDto) { var sportsTestResults = await _cacheQueryService.GetSportsTestDataCacheAsync(x => x.StudentNo == paramDto.StudentNo && x.SchoolCode.Equals(UserContext.Current.TenantId) && x.CategoryValue == paramDto.CategoryValue , paramDto.Mode); var result = new VariousSportsProportion(); DateTime currentDate = DateTime.Now; switch (paramDto.CycleTime) { case CycleTimeEnum.InThePastWeek: // 近一周:按天展示 var pastWeekResults = sportsTestResults .Where(x => x.ScoreTime >= currentDate.AddDays(-7)) .GroupBy(x => x.ScoreTime.Date); foreach (var group in pastWeekResults) { result.AxisX.Add(group.Key.ToString("yyyy-MM-dd")); var score = (group.Sum(x => x.Score) + group.Sum(x => x.AdditionalScore)) / (group.Select(x => x.StudentNo).Distinct().Count()); result.AxisY.Add((float)Math.Round(score)); } break; case CycleTimeEnum.InThePastTwoWeeks: // 近两周:按天展示 var pastTwoWeeksResults = sportsTestResults .Where(x => x.ScoreTime >= currentDate.AddDays(-14)) .GroupBy(x => x.ScoreTime.Date); foreach (var group in pastTwoWeeksResults) { result.AxisX.Add(group.Key.ToString("yyyy-MM-dd")); var score = (group.Sum(x => x.Score) + group.Sum(x => x.AdditionalScore)) / (group.Select(x => x.StudentNo).Distinct().Count()); result.AxisY.Add((float)Math.Round(score)); } break; case CycleTimeEnum.InThePastMonth: // 近一月:每5天一个阶段展示 var pastMonthResults = sportsTestResults .Where(x => x.ScoreTime >= currentDate.AddDays(-30)) .GroupBy(x => (currentDate - x.ScoreTime).Days / 5); foreach (var group in pastMonthResults) { var minDate = group.Min(r => r.ScoreTime).ToString("yyyy-MM-dd"); var maxDate = group.Max(r => r.ScoreTime).ToString("yyyy-MM-dd"); result.AxisX.Add($"{minDate} - {maxDate}"); var score = (group.Sum(x => x.Score) + group.Sum(x => x.AdditionalScore)) / (group.Select(x => x.StudentNo).Distinct().Count()); result.AxisY.Add((float)Math.Round(score)); } break; case CycleTimeEnum.InThePastYear: // 近一年:按月展示 var pastYearResults = sportsTestResults .Where(x => x.ScoreTime >= currentDate.AddYears(-1)) .GroupBy(x => new { x.ScoreTime.Year, x.ScoreTime.Month }); foreach (var group in pastYearResults) { result.AxisX.Add($"{group.Key.Year}-{group.Key.Month:D2}"); var score = (group.Sum(x => x.Score) + group.Sum(x => x.AdditionalScore)) / (group.Select(x => x.StudentNo).Distinct().Count()); result.AxisY.Add((float)Math.Round(score)); } break; default: // 默认处理,如果没有匹配的情况 break; } return result; } /// /// 训练记录 /// /// /// /// public async Task> TrainingRecords(StudentTrainingRecordsParam paramDto) { var res = new PageDataDto(); var tenantId = UserContext.Current.TenantId; if (string.IsNullOrEmpty(paramDto.StudentNo)) { paramDto.StudentNo = _studentRepository.DbContext.Set().First(s => s.SchoolCode == tenantId).StudentNo; } // 查询 I_TrainingData 表 var iotQuery = from student in _studentRepository.DbContext.Set() join trainingData in _studentRepository.DbContext.Set() on student.TrainingDataId equals trainingData.Id where trainingData.IsDisplay && trainingData.SchoolCode == tenantId select new StudentTrainingRecordsModel { StudentNo = student.StudentNo, // 训练模式 TrainType = trainingData.ModelType ?? 0, //项目模式 ModelName = trainingData.ModelName, CategoryValue = (TrainingItemType)trainingData.ItemType, // 成绩 Result = (float?)trainingData.ClassNumAvg, ResultLevel = trainingData.ClassScore, // 训练时间 ScoreTime = trainingData.InsertTime, }; // 查询 Ai_SportsTestData 表 var aiQuery = from trainingData in _studentRepository.DbContext.Set() where trainingData.IsDisplay && trainingData.SchoolCode == tenantId select new StudentTrainingRecordsModel { StudentNo = trainingData.StudentNo, TrainType = (int)trainingData.ModeType, ModelName = trainingData.ModelName, CategoryValue = (TrainingItemType)trainingData.CategoryValue, Result = (float?)trainingData.Value, ResultLevel = (int?)trainingData.RankEnum, ScoreTime = trainingData.ScoreTime }; // 合并两个查询结果 var combinedQuery = iotQuery.Concat(aiQuery); // 根据学号过滤 if (!string.IsNullOrEmpty(paramDto.StudentNo)) { combinedQuery = combinedQuery.Where(x => x.StudentNo == paramDto.StudentNo); } // 根据开始时间过滤 if (paramDto.StartTime.HasValue) { combinedQuery = combinedQuery.Where(x => x.ScoreTime >= paramDto.StartTime); } // 根据结束时间过滤 if (paramDto.EndTime.HasValue) { combinedQuery = combinedQuery.Where(x => x.ScoreTime <= paramDto.EndTime); } // 根据项目类型过滤 if (paramDto.CategoryValue > 0) { combinedQuery = combinedQuery.Where(x => x.CategoryValue == (TrainingItemType)paramDto.CategoryValue); } // 根据成绩等级过滤 if (paramDto.Rank > 0) { combinedQuery = combinedQuery.Where(x => x.ResultLevel == (int)paramDto.Rank); } // 获取总数 res.Total = await combinedQuery.CountAsync(); // 分页和排序 var list = await combinedQuery .OrderBy(c => c.ScoreTime) // 按训练时间排序 .Skip((paramDto.PageIndex - 1) * paramDto.PageSize) .Take(paramDto.PageSize) .ToListAsync(); foreach (var item in list) { // 项目 item.CategoryName = item.CategoryValue.GetDisplayName(); var grade = ((AchievementRank)(item.ResultLevel.HasValue && item.ResultLevel.Value > 0 ? item.ResultLevel.Value : 4)); var gradeName = grade.GetDescription(); // 等级 item.Grade = gradeName; var trainName = ((Ai_ModeEnum)(item.TrainType)).GetDescription(); // 训练模式 item.TrainName = trainName; } res.Datas = list; return res; } /// /// 体测记录 /// /// /// /// public async Task> TestRecords(StudentTrainingRecordsParam paramDto) { var res = new PageDataDto(); var tenantId = UserContext.Current.TenantId; if (string.IsNullOrEmpty(paramDto.StudentNo)) { paramDto.StudentNo = this._studentRepository.DbContext.Set().First().StudentNo; } // 初始化基础查询条件 Expression> query = (x => x.SchoolCode == tenantId); if (!string.IsNullOrEmpty(paramDto.StudentNo)) { query = query.And(x => x.StudentNo == paramDto.StudentNo); } if (paramDto.StartTime.HasValue) { query = query.And(x => x.ScoreTime >= paramDto.StartTime); } if (paramDto.EndTime.HasValue) { query = query.And(x => x.ScoreTime <= paramDto.StartTime); } if (paramDto.CategoryValue > 0) { query = query.And(x => x.CategoryValue == paramDto.CategoryValue); } if (paramDto.Rank > 0) { query = query.And(x => x.RankEnum == paramDto.Rank); } var sportsTestResults = await _cacheQueryService.GetSportsTestDataCacheAsync(query.Compile(), paramDto.TrainType); // 获取总数 res.Total = sportsTestResults.Count(); // 分页和排序 res.Datas = sportsTestResults .OrderBy(c => c.ScoreTime) // 按训练时间排序 .Select(c => new StudentTestRecordsModel { TrainName = c.ModeType.GetDescription(), CategoryValue = (SportsTestItemType)c.CategoryValue, CategoryName = ((SportsTestItemType)c.CategoryValue).GetDisplayName(), Result = c.Value, Score = (c.Score + c.AdditionalScore), Rank = c.Rank, ScoreTime = c.ScoreTime }) .Skip((paramDto.PageIndex - 1) * paramDto.PageSize) .Take(paramDto.PageSize) .ToList(); return res; } // 构建课堂模式查询 private IQueryable BuildClassRoomModeQuery(string tenantId, string studentNo, StudentTrainingRecordsParam paramDto) { var query = from td in _studentRepository.DbContext.Set() join tr in _studentRepository.DbContext.Set() on td.Id equals tr.TrainingDataId into ranking from r in ranking.DefaultIfEmpty() where td.SchoolCode == tenantId && r.StudentNo == studentNo select new StudentTrainingRecordsModel { TrainName = "课堂模式", ModelName = td.ModelName, CategoryValue = (TrainingItemType)td.ItemType, CategoryName = ((TrainingItemType)td.ItemType).GetDescription(), Result = r.JumpValue, ResultLevel = r.ResultLevel, ScoreTime = td.InsertTime }; return ApplyFilters(query, paramDto); } // 构建自由模式查询 private IQueryable BuildFreeModeQuery(string tenantId, string studentNo, StudentTrainingRecordsParam paramDto) { var query = from a in _studentRepository.DbContext.Set() join n in _studentRepository.DbContext.Set() on a.StartTime equals n.ScoreTime where a.SchoolCode == tenantId && a.StudentNo == studentNo select new StudentTrainingRecordsModel { TrainName = "自由模式", ScoreTime = a.StartTime, CategoryValue = a.CategoryValue, CategoryName = ((TrainingItemType)Convert.ToInt32(a.CategoryValue)).GetDescription(), ModelName = "/", Grade = n.Rank }; return ApplyFilters(query, paramDto); } // 通用过滤逻辑 private IQueryable ApplyFilters(IQueryable query, StudentTrainingRecordsParam paramDto) { return query .WhereIF(paramDto.StartTime.HasValue, x => x.ScoreTime >= paramDto.StartTime) .WhereIF(paramDto.EndTime.HasValue, x => x.ScoreTime <= paramDto.EndTime) .WhereIF(paramDto.CategoryValue > 0, x => x.CategoryValue == (TrainingItemType)paramDto.CategoryValue) .WhereIF(paramDto.Rank > 0, x => x.ResultLevel == (int)paramDto.Rank); } // 构建体测课堂模式查询 private IQueryable BuildTestRoomModeQuery(string tenantId, string studentNo, StudentTrainingRecordsParam paramDto) { var query = from a in _studentRepository.DbContext.Set() join r in _studentRepository.DbContext.Set() on a.ScoreTime equals r.ScoreTime where a.SchoolCode == tenantId && r.StudentNo == studentNo select new StudentTestRecordsModel { TrainName = "课堂模式", CategoryValue = (SportsTestItemType)r.CategoryValue, CategoryName = ((SportsTestItemType)r.CategoryValue).GetDescription(), Result = r.Value, Rank = r.Rank, Score = r.Score, ScoreTime = r.ScoreTime }; return TestApplyFilters(query, paramDto); } private IQueryable BuildTestFreeModeQuery(string tenantId, string studentNo, StudentTrainingRecordsParam paramDto) { var query = from a in _studentRepository.DbContext.Set() join n in _studentRepository.DbContext.Set() on a.StartTime equals n.ScoreTime where a.SchoolCode == tenantId && a.StudentNo == studentNo select new StudentTestRecordsModel { TrainName = "自由模式", ScoreTime = a.StartTime, CategoryValue = (SportsTestItemType)a.CategoryValue, CategoryName = ((TrainingItemType)Convert.ToInt32(a.CategoryValue)).GetDescription(), Rank = n.Rank, Score = n.Score, Result = n.Value }; return TestApplyFilters(query, paramDto); } // 通用过滤逻辑 private IQueryable TestApplyFilters(IQueryable query, StudentTrainingRecordsParam paramDto) { return query .WhereIF(paramDto.StartTime.HasValue, x => x.ScoreTime >= paramDto.StartTime) .WhereIF(paramDto.EndTime.HasValue, x => x.ScoreTime <= paramDto.EndTime) .WhereIF(paramDto.CategoryValue > 0, x => x.CategoryValue == (SportsTestItemType)paramDto.CategoryValue) .WhereIF(paramDto.Rank > 0, x => x.Rank == paramDto.RankStr); } /// /// 学生名单列表 /// /// /// /// public async Task> GetStudentNamePageList(StudentNamePageListParam paramDto) { // 查询学生信息 var students = await _studentRepository.FindAsIQueryable(x => x.ClassId == paramDto.ClassId) .Select(x => new StudentNamePageListModel() { StudentNo = x.StudentNo, StudentName = x.StudentName, Photo = x.Photo }) .ToListAsync(); // 查询两个数据源的成绩并合并 var combinedResults = await _sportsTestResultRepository.FindAsIQueryable(x => x.ClassId == paramDto.ClassId && x.SchoolCode.Equals(UserContext.Current.TenantId) ) .Select(x => new { x.StudentNo, x.Score }) .Concat( _sportsTestResultRepository.DbContext.Set().Where(x => x.ClassId == paramDto.ClassId && x.SchoolCode.Equals(UserContext.Current.TenantId) ) .Select(x => new { x.StudentNo, x.Score }) ) .ToListAsync(); // 使用字典来存储平均成绩,便于后续查找 var averageScoresDict = combinedResults .GroupBy(x => x.StudentNo) .ToDictionary(g => g.Key, g => g.Average(x => x.Score)); // 遍历学生信息,为每个学生分配 Rank students.ForEach(student => { if (averageScoresDict.TryGetValue(student.StudentNo, out var averageScore)) { student.Rank = averageScore.GetRank(); // 有平均成绩的情况 } else { student.Rank = 0.GetRank(); // 没有成绩时默认处理 } }); return students; } /// /// 更换班级 /// /// /// /// public async Task ReplaceClasses(List studentNoList, int classId) { var students = await _studentRepository.FindAsync(x => studentNoList.Contains(x.StudentNo)); if (students == null || students.Count == 0) throw new ArgumentNullException("未找到更新的数据"); foreach (var student in students) { student.ClassId = classId; student.Modifier = UserContext.Current.UserId; student.ModifyDate = DateTime.Now; } _studentRepository.UpdateRange(students); await _studentRepository.SaveChangesAsync(); } public async Task GetPromotionStatus() { var school = await GetPromotionSchool(); var academicYear = GetAcademicYear(DateTime.Now); var latestBatch = await _studentRepository.DbContext.Set() .Where(x => x.SchoolCode == school.SchoolCode) .OrderByDescending(x => x.Id) .FirstOrDefaultAsync(); var currentBatchExists = await _studentRepository.DbContext.Set() .AnyAsync(x => x.SchoolCode == school.SchoolCode && x.AcademicYear == academicYear && x.Status == 1); var gradeIds = new List(); string gradeConfigurationReason = null; try { gradeIds = await GetPromotionGradeIds(school); var existingGradeCount = await _studentRepository.DbContext.Set() .CountAsync(x => gradeIds.Contains(x.Id)); if (existingGradeCount != gradeIds.Count) gradeConfigurationReason = $"年级基础数据不完整,应存在{gradeIds.Count}个年级,实际存在{existingGradeCount}个"; } catch (InvalidOperationException ex) { gradeConfigurationReason = ex.Message; } var studentCount = 0; if (string.IsNullOrWhiteSpace(gradeConfigurationReason)) { studentCount = await _studentRepository.FindAsIQueryable(x => x.SchoolCode == school.SchoolCode && x.StudentStatus == StudentStatus.Normal) .Join(_studentRepository.DbContext.Set().Where(x => gradeIds.Contains(x.GradeId)), student => student.ClassId, schoolClass => schoolClass.Id, (student, schoolClass) => student.Id) .CountAsync(); } var cannotPromoteReason = currentBatchExists ? $"{academicYear}学年已经执行过一键升学,如需重新操作请先撤回升学" : gradeConfigurationReason ?? (studentCount == 0 ? "当前学校没有可升学的在读学生" : null); var cannotUndoReason = latestBatch == null ? "当前学校没有升学记录" : latestBatch.Status != 1 ? "最近一次升学已经撤回,没有可撤回的升学记录" : null; return new StudentPromotionStatusModel { SchoolNature = latestBatch?.SchoolNatureName ?? await GetSchoolNatureName(school.SchoolNatureId), AcademicYear = academicYear, CanPromote = string.IsNullOrWhiteSpace(cannotPromoteReason), CannotPromoteReason = cannotPromoteReason, CanUndo = latestBatch?.Status == 1, CannotUndoReason = cannotUndoReason, StudentCount = studentCount, PromotedCount = latestBatch?.PromotedCount ?? 0, GraduatedCount = latestBatch?.GraduatedCount ?? 0, PromotionTime = latestBatch?.CreateDate }; } public async Task PromoteStudents() { var school = await GetPromotionSchool(); var academicYear = GetAcademicYear(DateTime.Now); var gradeIds = await GetPromotionGradeIds(school); var natureName = await GetSchoolNatureName(school.SchoolNatureId); var hasPromoted = await _studentRepository.DbContext.Set() .AnyAsync(x => x.SchoolCode == school.SchoolCode && x.AcademicYear == academicYear && x.Status == 1); if (hasPromoted) throw new Exception($"{academicYear}学年已经执行过升学,请勿重复操作"); var grades = await _studentRepository.DbContext.Set() .Where(x => gradeIds.Contains(x.Id)) .OrderBy(x => x.Id) .ToListAsync(); var classes = await _studentRepository.DbContext.Set() .Where(x => x.SchoolCode == school.SchoolCode && gradeIds.Contains(x.GradeId)) .ToListAsync(); var classIds = classes.Select(x => x.Id).ToList(); var students = await _studentRepository.FindAsIQueryable(x => x.SchoolCode == school.SchoolCode && x.StudentStatus == StudentStatus.Normal && classIds.Contains(x.ClassId)) .ToListAsync(); if (students.Count == 0) throw new Exception("当前学校没有可升学的在读学生"); var classById = classes.ToDictionary(x => x.Id); var gradeIndex = grades.Select((grade, index) => new { grade.Id, Index = index }) .ToDictionary(x => x.Id, x => x.Index); var targetClasses = new Dictionary(); var classesToCreate = new List(); var classLookup = classes .GroupBy(x => $"{x.GradeId}|{GetClassKey(x.ClassName, x.GradeName).ToUpperInvariant()}") .ToDictionary(x => x.Key, x => x.First()); var now = DateTime.Now; foreach (var sourceClassId in classes.Select(x => x.Id).Distinct()) { var sourceClass = classById[sourceClassId]; var sourceGradeIndex = gradeIndex[sourceClass.GradeId]; if (sourceGradeIndex == grades.Count - 1) continue; var targetGrade = grades[sourceGradeIndex + 1]; var classKey = GetClassKey(sourceClass.ClassName, sourceClass.GradeName); var lookupKey = $"{targetGrade.Id}|{classKey.ToUpperInvariant()}"; if (!classLookup.TryGetValue(lookupKey, out var targetClass)) { var sourceClassName = (sourceClass.ClassName ?? string.Empty).Trim(); var sourceGradeName = (sourceClass.GradeName ?? string.Empty).Trim(); var targetClassName = !string.IsNullOrEmpty(sourceGradeName) && sourceClassName.StartsWith(sourceGradeName, StringComparison.OrdinalIgnoreCase) ? $"{targetGrade.GradeName}{classKey}" : sourceClassName; targetClass = new S_Class { SchoolCode = school.SchoolCode, GradeId = targetGrade.Id, GradeName = targetGrade.GradeName, ClassName = targetClassName, Creator = UserContext.Current.UserId, CreateDate = now }; classesToCreate.Add(targetClass); classLookup[lookupKey] = targetClass; } targetClasses[sourceClassId] = targetClass; } var highestGradeId = grades.Last().Id; var teacherAssociations = await _studentRepository.DbContext.Set() .Where(x => classIds.Contains(x.ClassId)) .ToListAsync(); var batch = new S_StudentPromotionBatch { SchoolCode = school.SchoolCode, SchoolNatureId = school.SchoolNatureId, SchoolNatureName = natureName, AcademicYear = academicYear, Status = 1, PromotedCount = students.Count(x => classById[x.ClassId].GradeId != highestGradeId), GraduatedCount = students.Count(x => classById[x.ClassId].GradeId == highestGradeId), OperatorId = UserContext.Current.UserId, CreateDate = now }; using (var transaction = await _studentRepository.DbContext.Database.BeginTransactionAsync()) { try { await _studentRepository.DbContext.Set().AddAsync(batch); await _studentRepository.SaveChangesAsync(); if (classesToCreate.Count > 0) { classesToCreate.ForEach(x => x.Remarks = $"一键升学自动创建,批次Id:{batch.Id}"); await _studentRepository.DbContext.Set().AddRangeAsync(classesToCreate); await _studentRepository.SaveChangesAsync(); } var teacherSnapshots = teacherAssociations.Select(association => { var sourceClass = classById[association.ClassId]; var isGraduating = sourceClass.GradeId == highestGradeId; return new TeacherPromotionSnapshotItem { AssociationId = association.Id, TeacherId = association.TeacherId, OriginalClassId = association.ClassId, TargetClassId = isGraduating ? 0 : targetClasses[sourceClass.Id].Id, Creator = association.Creator, CreateDate = association.CreateDate, Modifier = association.Modifier, ModifyDate = association.ModifyDate }; }).ToList(); batch.TeacherSnapshot = JsonConvert.SerializeObject(teacherSnapshots); _studentRepository.DbContext.Set().Update(batch); foreach (var association in teacherAssociations) { var sourceClass = classById[association.ClassId]; if (sourceClass.GradeId == highestGradeId) { _studentRepository.DbContext.Set().Remove(association); continue; } association.ClassId = targetClasses[sourceClass.Id].Id; association.Modifier = UserContext.Current.UserId; association.ModifyDate = now; _studentRepository.DbContext.Set().Update(association); } var details = new List(); foreach (var student in students) { var sourceClass = classById[student.ClassId]; var isGraduating = sourceClass.GradeId == highestGradeId; var targetClass = isGraduating ? sourceClass : targetClasses[sourceClass.Id]; var targetStatus = isGraduating ? StudentStatus.Grad : student.StudentStatus; details.Add(new S_StudentPromotionDetail { BatchId = batch.Id, StudentId = student.Id, OriginalClassId = student.ClassId, OriginalClassName = student.ClassName, OriginalStatus = student.StudentStatus, OriginalAge = student.Age, TargetClassId = targetClass.Id, TargetClassName = targetClass.ClassName, TargetStatus = targetStatus, TargetAge = student.Age + 1 }); student.ClassId = targetClass.Id; student.ClassName = targetClass.ClassName; student.StudentStatus = targetStatus; student.Age += 1; student.Modifier = UserContext.Current.UserId; student.ModifyDate = now; } await _studentRepository.DbContext.Set().AddRangeAsync(details); _studentRepository.UpdateRange(students); await _studentRepository.SaveChangesAsync(); await transaction.CommitAsync(); } catch { await transaction.RollbackAsync(); throw; } } return await GetPromotionStatus(); } public async Task UndoPromotion() { var school = await GetPromotionSchool(); await using (var transaction = await _studentRepository.DbContext.Database.BeginTransactionAsync(IsolationLevel.Serializable)) { var batch = await _studentRepository.DbContext.Set() .Where(x => x.SchoolCode == school.SchoolCode) .OrderByDescending(x => x.Id) .FirstOrDefaultAsync(); if (batch == null || batch.Status != 1) throw new Exception("当前学校没有可撤回的升学记录"); var details = await _studentRepository.DbContext.Set() .Where(x => x.BatchId == batch.Id) .ToListAsync(); var studentIds = details.Select(x => x.StudentId).ToList(); var students = await _studentRepository.FindAsIQueryable(x => studentIds.Contains(x.Id)).ToListAsync(); var studentById = students.ToDictionary(x => x.Id); var teacherSnapshots = string.IsNullOrWhiteSpace(batch.TeacherSnapshot) ? new List() : JsonConvert.DeserializeObject>(batch.TeacherSnapshot) ?? new List(); var shiftedAssociationIds = teacherSnapshots .Where(x => x.TargetClassId > 0) .Select(x => x.AssociationId) .ToList(); var currentTeacherAssociations = await _studentRepository.DbContext.Set() .Where(x => shiftedAssociationIds.Contains(x.Id)) .ToListAsync(); var currentTeacherAssociationById = currentTeacherAssociations.ToDictionary(x => x.Id); var changedCount = details.Count(detail => !studentById.TryGetValue(detail.StudentId, out var student) || student.ClassId != detail.TargetClassId || student.StudentStatus != detail.TargetStatus || student.Age != detail.TargetAge); if (changedCount > 0) throw new Exception($"升学后已有{changedCount}名学生的班级、状态或年龄被修改,无法自动撤回"); var changedTeacherCount = teacherSnapshots.Count(snapshot => snapshot.TargetClassId > 0 && (!currentTeacherAssociationById.TryGetValue(snapshot.AssociationId, out var association) || association.TeacherId != snapshot.TeacherId || association.ClassId != snapshot.TargetClassId)); if (changedTeacherCount > 0) throw new Exception($"升学后已有{changedTeacherCount}条教师班级关系被修改,无法自动撤回"); var now = DateTime.Now; try { foreach (var detail in details) { var student = studentById[detail.StudentId]; student.ClassId = detail.OriginalClassId; student.ClassName = detail.OriginalClassName; student.StudentStatus = detail.OriginalStatus; student.Age = detail.OriginalAge; student.Modifier = UserContext.Current.UserId; student.ModifyDate = now; } foreach (var snapshot in teacherSnapshots) { if (snapshot.TargetClassId > 0) { var association = currentTeacherAssociationById[snapshot.AssociationId]; association.ClassId = snapshot.OriginalClassId; association.Modifier = snapshot.Modifier; association.ModifyDate = snapshot.ModifyDate; _studentRepository.DbContext.Set().Update(association); } else { await _studentRepository.DbContext.Set().AddAsync(new S_ClassAssocTeacher { ClassId = snapshot.OriginalClassId, TeacherId = snapshot.TeacherId, Creator = snapshot.Creator, CreateDate = snapshot.CreateDate, Modifier = snapshot.Modifier, ModifyDate = snapshot.ModifyDate }); } } batch.Status = 2; batch.RevokeOperatorId = UserContext.Current.UserId; batch.RevokeDate = now; _studentRepository.UpdateRange(students); _studentRepository.DbContext.Set().Update(batch); await _studentRepository.SaveChangesAsync(); await transaction.CommitAsync(); } catch { await transaction.RollbackAsync(); throw; } } return await GetPromotionStatus(); } private async Task GetPromotionSchool() { if (UserContext.Current.RoleId != 2) throw new Exception("仅学校管理员可以执行一键升学"); var school = await _studentRepository.DbContext.Set() .FirstOrDefaultAsync(x => x.SchoolCode == UserContext.Current.TenantId && x.SchoolType == SchoolType.School); if (school == null) throw new Exception("未找到当前学校信息"); return school; } private async Task> GetPromotionGradeIds(S_School school) { var natureGradeIds = await _studentRepository.DbContext.Set() .Where(x => x.NatureId == school.SchoolNatureId) .OrderBy(x => x.GradeId) .Select(x => x.GradeId) .Distinct() .ToListAsync(); if (natureGradeIds.Count == 0) throw new InvalidOperationException("当前学校性质未配置年级,无法执行一键升学"); var schoolGradeIds = await _studentRepository.DbContext.Set() .Where(x => x.SchoolCode == school.SchoolCode) .Select(x => x.GradeId) .Distinct() .ToListAsync(); var gradeIds = natureGradeIds.Where(schoolGradeIds.Contains).ToList(); if (gradeIds.Count != natureGradeIds.Count) throw new InvalidOperationException($"当前学校年级配置不完整,应配置{natureGradeIds.Count}个年级,实际配置{gradeIds.Count}个"); return gradeIds; } private async Task GetSchoolNatureName(int natureId) { return await _studentRepository.DbContext.Set() .Where(x => x.Id == natureId) .Select(x => x.NatureName) .FirstOrDefaultAsync() ?? "未知"; } private static string GetAcademicYear(DateTime date) { var startYear = date.Month >= 7 ? date.Year : date.Year - 1; return $"{startYear}-{startYear + 1}"; } private static string GetClassKey(string className, string gradeName) { var value = (className ?? string.Empty).Trim(); var grade = (gradeName ?? string.Empty).Trim(); if (!string.IsNullOrEmpty(grade) && value.StartsWith(grade, StringComparison.OrdinalIgnoreCase)) value = value.Substring(grade.Length).Trim(); return value; } private class TeacherPromotionSnapshotItem { public int AssociationId { get; set; } public int TeacherId { get; set; } public int OriginalClassId { get; set; } public int TargetClassId { get; set; } public int Creator { get; set; } public DateTime? CreateDate { get; set; } public int Modifier { get; set; } public DateTime? ModifyDate { get; set; } } } }