2026-08-18 11:49:48 +08:00

1971 lines
90 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 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.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<PageDataDto<StudentPageListModel>> GetStudentPageList(StudentPageListParam paramDto)
{
var res = new PageDataDto<StudentPageListModel>();
var classIds = new List<int>();
var isTeacher = (UserContext.Current.RoleId == 3);
if (isTeacher)
{
var teacher = await _studentRepository.DbContext.Set<S_Teacher>().FirstOrDefaultAsync(x => x.SchoolCode == UserContext.Current.TenantId && x.TeacherPhoneNo == UserContext.Current.UserInfo.PhoneNo);
var teacherClassList = await (
from at in _studentRepository.DbContext.Set<S_ClassAssocTeacher>()
join c in _studentRepository.DbContext.Set<S_Class>() 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<S_Student>()
join c in _studentRepository.DbContext.Set<S_Class>() 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<List<StudentPageListModel>> GetStudentList(StudentExportParam paramDto)
{
var classIds = new List<int>();
var isTeacher = (UserContext.Current.RoleId == 3);
if (isTeacher)
{
var teacher = await _studentRepository.DbContext.Set<S_Teacher>().FirstOrDefaultAsync(x => x.SchoolCode == UserContext.Current.TenantId && x.TeacherPhoneNo == UserContext.Current.UserInfo.PhoneNo);
var teacherClassList = await (
from at in _studentRepository.DbContext.Set<S_ClassAssocTeacher>()
join c in _studentRepository.DbContext.Set<S_Class>() 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<S_Student>()
join c in _studentRepository.DbContext.Set<S_Class>() 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;
}
/// <summary>
/// 获取场馆学员
/// </summary>
/// <param name="paramDto"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public async Task<List<StudentPageListModel>> GetStudentListBySportsHall(StudentExportParam paramDto)
{
var query = from s in _studentRepository.DbContext.Set<S_Student>()
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<S_Student>(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<S_Student>() { studentEntity })
//}));
await _iotDataSyncService.DataSyncCallBack(new DataSyncCallBackParam
{
EventType = EventType.Add.GetDisplayName(),
DataType = IOTDataSyncType.Student.GetDisplayName(),
Json = JsonConvert.SerializeObject(new List<S_Student>() { 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<int> BatchDeleteStudents(List<int> ids)
{
if (UserContext.Current.RoleId != 1 && UserContext.Current.RoleId != 2)
throw new Exception("仅管理员可以批量删除学生");
var studentIds = ids?.Where(x => x > 0).Distinct().ToList() ?? new List<int>();
if (studentIds.Count == 0)
throw new ArgumentException("请至少选择一名学生");
var students = await _studentRepository.FindAsIQueryable(x =>
x.SchoolCode == UserContext.Current.TenantId && studentIds.Contains(x.Id))
.ToListAsync();
if (students.Count != studentIds.Count)
throw new Exception("部分学生不存在或不属于当前学校,请刷新列表后重新选择");
var activeBatchId = await _studentRepository.DbContext.Set<S_StudentPromotionBatch>()
.Where(x => x.SchoolCode == UserContext.Current.TenantId && x.Status == 1)
.OrderByDescending(x => x.Id)
.Select(x => (int?)x.Id)
.FirstOrDefaultAsync();
if (activeBatchId.HasValue)
{
var promotionStudentCount = await _studentRepository.DbContext.Set<S_StudentPromotionDetail>()
.CountAsync(x => x.BatchId == activeBatchId.Value && studentIds.Contains(x.StudentId));
if (promotionStudentCount > 0)
throw new Exception($"所选学生中有{promotionStudentCount}人属于当前可撤回的升学记录,请先撤回升学后再删除");
}
_studentRepository.DbContext.RemoveRange(students);
await _studentRepository.SaveChangesAsync();
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<int> 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<ImportStudentParam> dataObjects;
try
{
using var fileStream = file.OpenReadStream();
dataObjects = Tool.ConvertExcelToList<ImportStudentParam>(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<string>();
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<S_Student>();
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;
}
/// <summary>
/// 上传学生头像
/// </summary>
public async Task<string> 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<S_Student>()
.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<BatchUploadStudentPhotoResult> 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<string>(StringComparer.OrdinalIgnoreCase);
var updatedStudents = new List<S_Student>();
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<bool> 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;
}
/// <summary>
/// 学生数据统计
/// </summary>
/// <param name="paramDto"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public async Task<StudentDataStatsModel> StudentWholeDataStats(StudentDataStatsParam paramDto)
{
var res = new StudentDataStatsModel();
var tenantId = UserContext.Current.TenantId;
if (string.IsNullOrEmpty(paramDto.StudentNo))
{
paramDto.StudentNo = _studentRepository.DbContext.Set<S_Student>().First().StudentNo;
}
var student = await (from s in _studentRepository.DbContext.Set<S_Student>()
join c in _studentRepository.DbContext.Set<S_Class>() 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<I_TrainingData>()
join s in _studentRepository.DbContext.Set<I_TrainRanking>() 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<S_GradeAssocCategory>()
join n in _sportsTestResultRepository.DbContext.Set<N_SportsTestCategory>()
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;
}
/// <summary>
/// 各项目平均成绩
/// </summary>
/// <param name="paramDto"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public async Task<Dictionary<string, float>> 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;
}
);
}
/// <summary>
/// 各项目成绩对比
/// </summary>
/// <param name="paramDto"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public async Task<Dictionary<string, float>> 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);
}
/// <summary>
/// 成绩趋势
/// </summary>
/// <param name="paramDto"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public async Task<VariousSportsProportion> 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;
}
/// <summary>
/// 训练记录
/// </summary>
/// <param name="paramDto"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public async Task<PageDataDto<StudentTrainingRecordsModel>> TrainingRecords(StudentTrainingRecordsParam paramDto)
{
var res = new PageDataDto<StudentTrainingRecordsModel>();
var tenantId = UserContext.Current.TenantId;
if (string.IsNullOrEmpty(paramDto.StudentNo))
{
paramDto.StudentNo = _studentRepository.DbContext.Set<S_Student>().First(s => s.SchoolCode == tenantId).StudentNo;
}
// 查询 I_TrainingData 表
var iotQuery = from student in _studentRepository.DbContext.Set<I_TrainRanking>()
join trainingData in _studentRepository.DbContext.Set<I_TrainingData>()
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<Ai_SportsTestData>()
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;
}
/// <summary>
/// 体测记录
/// </summary>
/// <param name="paramDto"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public async Task<PageDataDto<StudentTestRecordsModel>> TestRecords(StudentTrainingRecordsParam paramDto)
{
var res = new PageDataDto<StudentTestRecordsModel>();
var tenantId = UserContext.Current.TenantId;
if (string.IsNullOrEmpty(paramDto.StudentNo))
{
paramDto.StudentNo = this._studentRepository.DbContext.Set<S_Student>().First().StudentNo;
}
// 初始化基础查询条件
Expression<Func<SportsTestValueModel, bool>> 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<StudentTrainingRecordsModel> BuildClassRoomModeQuery(string tenantId, string studentNo, StudentTrainingRecordsParam paramDto)
{
var query = from td in _studentRepository.DbContext.Set<I_TrainingData>()
join tr in _studentRepository.DbContext.Set<I_TrainRanking>() 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<StudentTrainingRecordsModel> BuildFreeModeQuery(string tenantId, string studentNo, StudentTrainingRecordsParam paramDto)
{
var query = from a in _studentRepository.DbContext.Set<Ai_TrainingData>()
join n in _studentRepository.DbContext.Set<N_SportsTestValue>() 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<StudentTrainingRecordsModel> ApplyFilters(IQueryable<StudentTrainingRecordsModel> 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<StudentTestRecordsModel> BuildTestRoomModeQuery(string tenantId, string studentNo, StudentTrainingRecordsParam paramDto)
{
var query = from a in _studentRepository.DbContext.Set<I_SportsTestData>()
join r in _studentRepository.DbContext.Set<N_SportsTestValue>() 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<StudentTestRecordsModel> BuildTestFreeModeQuery(string tenantId, string studentNo, StudentTrainingRecordsParam paramDto)
{
var query = from a in _studentRepository.DbContext.Set<Ai_TrainingData>()
join n in _studentRepository.DbContext.Set<N_SportsTestValue>() 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<StudentTestRecordsModel> TestApplyFilters(IQueryable<StudentTestRecordsModel> 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);
}
/// <summary>
/// 学生名单列表
/// </summary>
/// <param name="paramDto"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public async Task<List<StudentNamePageListModel>> 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<Ai_SportsTestData>().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;
}
/// <summary>
/// 更换班级
/// </summary>
/// <param name="studentNoList"></param>
/// <param name="classId"></param>
/// <returns></returns>
public async Task ReplaceClasses(List<string> 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<StudentPromotionStatusModel> GetPromotionStatus()
{
var school = await GetPromotionSchool();
var academicYear = GetAcademicYear(DateTime.Now);
var latestBatch = await _studentRepository.DbContext.Set<S_StudentPromotionBatch>()
.Where(x => x.SchoolCode == school.SchoolCode)
.OrderByDescending(x => x.Id)
.FirstOrDefaultAsync();
var currentBatchExists = await _studentRepository.DbContext.Set<S_StudentPromotionBatch>()
.AnyAsync(x => x.SchoolCode == school.SchoolCode &&
x.AcademicYear == academicYear &&
x.Status == 1);
var gradeIds = new List<int>();
string gradeConfigurationReason = null;
try
{
gradeIds = await GetPromotionGradeIds(school);
var existingGradeCount = await _studentRepository.DbContext.Set<S_Grade>()
.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<S_Class>().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<StudentPromotionStatusModel> 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<S_StudentPromotionBatch>()
.AnyAsync(x => x.SchoolCode == school.SchoolCode &&
x.AcademicYear == academicYear &&
x.Status == 1);
if (hasPromoted)
throw new Exception($"{academicYear}学年已经执行过升学,请勿重复操作");
var grades = await _studentRepository.DbContext.Set<S_Grade>()
.Where(x => gradeIds.Contains(x.Id))
.OrderBy(x => x.Id)
.ToListAsync();
var classes = await _studentRepository.DbContext.Set<S_Class>()
.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<int, S_Class>();
var classesToCreate = new List<S_Class>();
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<S_ClassAssocTeacher>()
.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<S_StudentPromotionBatch>().AddAsync(batch);
await _studentRepository.SaveChangesAsync();
if (classesToCreate.Count > 0)
{
classesToCreate.ForEach(x => x.Remarks = $"一键升学自动创建,批次Id:{batch.Id}");
await _studentRepository.DbContext.Set<S_Class>().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<S_StudentPromotionBatch>().Update(batch);
foreach (var association in teacherAssociations)
{
var sourceClass = classById[association.ClassId];
if (sourceClass.GradeId == highestGradeId)
{
_studentRepository.DbContext.Set<S_ClassAssocTeacher>().Remove(association);
continue;
}
association.ClassId = targetClasses[sourceClass.Id].Id;
association.Modifier = UserContext.Current.UserId;
association.ModifyDate = now;
_studentRepository.DbContext.Set<S_ClassAssocTeacher>().Update(association);
}
var details = new List<S_StudentPromotionDetail>();
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<S_StudentPromotionDetail>().AddRangeAsync(details);
_studentRepository.UpdateRange(students);
await _studentRepository.SaveChangesAsync();
await transaction.CommitAsync();
}
catch
{
await transaction.RollbackAsync();
throw;
}
}
return await GetPromotionStatus();
}
public async Task<StudentPromotionStatusModel> UndoPromotion()
{
var school = await GetPromotionSchool();
var batch = await _studentRepository.DbContext.Set<S_StudentPromotionBatch>()
.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<S_StudentPromotionDetail>()
.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<TeacherPromotionSnapshotItem>()
: JsonConvert.DeserializeObject<List<TeacherPromotionSnapshotItem>>(batch.TeacherSnapshot)
?? new List<TeacherPromotionSnapshotItem>();
var shiftedAssociationIds = teacherSnapshots
.Where(x => x.TargetClassId > 0)
.Select(x => x.AssociationId)
.ToList();
var currentTeacherAssociations = await _studentRepository.DbContext.Set<S_ClassAssocTeacher>()
.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;
using (var transaction = await _studentRepository.DbContext.Database.BeginTransactionAsync())
{
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<S_ClassAssocTeacher>().Update(association);
}
else
{
await _studentRepository.DbContext.Set<S_ClassAssocTeacher>().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<S_StudentPromotionBatch>().Update(batch);
await _studentRepository.SaveChangesAsync();
await transaction.CommitAsync();
}
catch
{
await transaction.RollbackAsync();
throw;
}
}
return await GetPromotionStatus();
}
private async Task<S_School> GetPromotionSchool()
{
if (UserContext.Current.RoleId != 2)
throw new Exception("仅学校管理员可以执行一键升学");
var school = await _studentRepository.DbContext.Set<S_School>()
.FirstOrDefaultAsync(x => x.SchoolCode == UserContext.Current.TenantId && x.SchoolType == SchoolType.School);
if (school == null)
throw new Exception("未找到当前学校信息");
return school;
}
private async Task<List<int>> GetPromotionGradeIds(S_School school)
{
var natureGradeIds = await _studentRepository.DbContext.Set<S_SchoolNatureAssocGrade>()
.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<S_SchoolAssocGrade>()
.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<string> GetSchoolNatureName(int natureId)
{
return await _studentRepository.DbContext.Set<S_SchoolNature>()
.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; }
}
}
}