1971 lines
90 KiB
C#
1971 lines
90 KiB
C#
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; }
|
||
}
|
||
}
|
||
}
|