using Microsoft.EntityFrameworkCore; using VOL.Commons; using VOL.Entity.DomainModels.XinWei; using YD_YiLu.Api.Context; using YD_YiLu.Api.Services.Interface; using YD_YiLu.Api.SmartSportsEntitys; using YD_YiLu.Api.Utilities; using YD_YiLu.Commons; using YD_YiLu.Commons.Dto.DeviceSync; using YD_YiLu.Commons.Dto.Open; using YD_YiLu.Commons.Utils; namespace YD_YiLu.Api.Services.Impl { /// /// 壹动站/壹体屏/活力课间屏 设备同步服务实现 /// public class DeviceSyncService : IDeviceSyncService { private readonly SmartSportsContext _context; public DeviceSyncService(SmartSportsContext context) { _context = context; } public async Task GetOrgIdByDeviceAsync(string deviceSerial) { var device = await _context.YL_Device.FirstOrDefaultAsync(x => x.SerialNumber == deviceSerial); if (device?.OrgId != null) return device.OrgId; else return 0; } public async Task UserPageAsync(DeviceSyncUserPageRequestDto dto) { var orgId = await GetOrgIdByDeviceAsync(dto.DeviceSerial); if (!orgId.HasValue) return new DeviceSyncUserPageResponseDto(); string schoolCode = await _context.School.Where(x => x.Id == orgId).Select(x => x.SchoolCode).FirstAsync(); var query = _context.Student.Where(x => x.SchoolCode == schoolCode); //if (dto.UserType == 1) query = query.Where(x => false); //else if (dto.UserType == 2) { } //else if (dto.UserType == 3) query = query.Where(x => false); var total = await query.CountAsync(); var pageIndex = dto.PageIndex < 1 ? 1 : dto.PageIndex; var fullSync = dto.FullSync || dto.PageSize <= 0; var pageSize = fullSync ? total : (dto.PageSize <= 0 ? 10 : dto.PageSize); var listQuery = query.OrderBy(x => x.Id); if (!fullSync) listQuery = (IOrderedQueryable)listQuery.Skip((pageIndex - 1) * pageSize).Take(pageSize); var list = await listQuery .Select(s => new DeviceSyncUserDto { UserId = s.Id, UserType = 2, Username = s.StudentNo, Nickname = s.StudentName, GradeName = "", OrganizationId = s.ClassId ?? 0, OrganizationName = s.ClassName ?? "", FacePictureUrl = s.Photo ?? "", Identity = "学生", Gender = s.Sex == 1 ? "男" : "女", Birthday = s.Birthday.HasValue ? s.Birthday.Value.ToString("yyyy-MM-dd") : "", UserSerial = s.StudentNo, }) .ToListAsync(); foreach (var u in list) { var c = await _context.Class.FindAsync(u.OrganizationId); if (c != null) { u.OrganizationName = FormatOrganizationName(c, u.OrganizationName); u.GradeName = c.GradeName ?? ""; } } return new DeviceSyncUserPageResponseDto { SyncId = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), Current = fullSync ? 1 : pageIndex, Pages = fullSync ? 1 : (total == 0 ? 0 : (int)Math.Ceiling(total / (double)Math.Max(pageSize, 1))), Total = total, UserList = list }; } public async Task UserSyncAsync(string deviceSerial, int deviceType, int userType) { return await UserPageAsync(new DeviceSyncUserPageRequestDto { DeviceSerial = deviceSerial, DeviceType = deviceType, UserType = userType, PageIndex = 1, FullSync = true, PageSize = 0 }); } public async Task> OrganizationAsync(DeviceSyncOrganizationRequestDto dto) { var orgId = await GetOrgIdByDeviceAsync(dto.DeviceSerial); if (!orgId.HasValue) return new List(); string schoolCode = await _context.School.Where(x => x.Id == orgId).Select(x => x.SchoolCode).FirstAsync(); var grades = await ( from g in _context.Grade join sg in _context.SchoolAssocGrade on g.Id equals sg.GradeId where sg.SchoolCode == schoolCode orderby g.Id select new { g.Id, g.GradeName } ).ToListAsync(); var classes = await _context.Class.Where(x => x.SchoolCode == schoolCode).ToListAsync(); var result = new List(); foreach (var g in grades) { var gradeClasses = classes.Where(x => x.GradeId == g.Id).ToList(); result.Add(new DeviceSyncOrganizationDto { OrganizationId = g.Id, ParentId = 0, ParentName = null, OrganizationName = g.GradeName, OrganizationType = "年级", Children = gradeClasses.Select(c => new DeviceSyncOrganizationDto { OrganizationId = c.Id, ParentId = g.Id, ParentName = g.GradeName, OrganizationName = FormatOrganizationName(c, c.ClassName ?? ""), OrganizationType = "班级", ChildrenIds = new List() }).ToList(), ChildrenIds = gradeClasses.Select(x => x.Id).ToList() }); } return result; } public async Task> TrainPushAsync(List list) { if (list == null || !list.Any()) return new List(); var result = new List(); // 👉 1. 设备校验(可选,看你业务) var orgId = await GetOrgIdByDeviceAsync(list.First().DeviceSerial); if (!orgId.HasValue) return result; // 👉 2. 批量查已存在的 TrainSerial(防重复) var normalizedItems = list .Where(x => x != null && !string.IsNullOrWhiteSpace(x.DeviceSerial) && !string.IsNullOrWhiteSpace(x.UserSerial)) .Select(x => { var trainSerial = NormalizeTrainSerial(x.TrainSerial, x); var videoSerial = NormalizeSerial(x.VideoSerial, trainSerial); return new { Item = x, TrainSerial = trainSerial, VideoSerial = videoSerial }; }) .ToList(); if (!normalizedItems.Any()) return result; var trainSerials = normalizedItems.Select(x => x.TrainSerial).Distinct().ToList(); var existSerials = await _context.YL_Train .Where(x => trainSerials.Contains(x.TrainSerial)) .Select(x => x.TrainSerial) .ToListAsync(); var seenSerials = new HashSet(existSerials, StringComparer.OrdinalIgnoreCase); var newEntities = new List(); foreach (var normalized in normalizedItems) { var item = normalized.Item; // 👉 3. 唯一校验(跳过已存在) if (!seenSerials.Add(normalized.TrainSerial)) continue; var score = CalculateScore(item); if (!score.HasValue || score.Value <= 0) continue; var entity = new YL_Train { DeviceSerial = item.DeviceSerial, DeviceType = item.DeviceType, UserSerial = item.UserSerial, TrainSerial = normalized.TrainSerial, VideoSerial = normalized.VideoSerial, TrainExam = item.TrainExam, QuizId = item.QuizId, StandardCounts = item.StandardCounts, UnStandardCounts = item.UnStandardCounts, TimeKeeping = item.TimeKeeping, Duration = item.Duration, Distance = item.Distance, CourseId = item.CourseId, StartTime = Util.ParseDateTime(item.StartTime), EndTime = Util.ParseDateTime(item.EndTime), // 👉 成绩计算(建议统一规则) Score = score, IsValid = 1, // 默认有效 UploadTime = DateTime.Now, CreateTime = DateTime.Now }; newEntities.Add(entity); } // 👉 4. 批量入库(关键优化点) if (newEntities.Any()) { //var sportsTestValueEntities = await AddSportsTestValueEntity(orgId.Value, newEntities); _context.YL_Train.AddRange(newEntities); //if (sportsTestValueEntities.Any()) // _context.SportsTestValue.AddRange(sportsTestValueEntities); await _context.SaveChangesAsync(); } // 👉 5. 返回结果 result = newEntities.Select(x => new DeviceSyncTrainPushResponseItem { TrainId = x.Id, UserSerial = x.UserSerial }).ToList(); return result; } private double? CalculateScore(DeviceSyncTrainPushRequestDto item) { // 👉 根据课目类型决定成绩字段 // 这里给你一个通用策略(你可以按课目细化) // 计数类(跳绳/仰卧起坐) if (item.StandardCounts > 0) return item.StandardCounts; // 计时类(悬垂) if (item.TimeKeeping.HasValue) return item.TimeKeeping; // 距离类(跳远) if (item.Distance.HasValue) return item.Distance; return null; } private async Task> AddSportsTestValueEntity(int orgId, List trains) { var sportsTestValueEntities = new List(); if (trains == null || !trains.Any()) return sportsTestValueEntities; var userSerials = trains .Select(x => x.UserSerial) .Where(x => !string.IsNullOrWhiteSpace(x)) .Distinct() .ToList(); var students = await _context.Student .Where(x => userSerials.Contains(x.StudentNo)) .ToListAsync(); var classIds = students .Where(x => x.ClassId.HasValue) .Select(x => x.ClassId.Value) .Distinct() .ToList(); var classes = await _context.Class .Where(x => classIds.Contains(x.Id)) .ToListAsync(); var healthStandards = await _context.HealthStandards.ToListAsync(); var semesterDto = new SemesterDto(); var nowTime = DateTime.Now; var studentMap = students .GroupBy(x => x.StudentNo) .ToDictionary(x => x.Key, x => x.First()); var classMap = classes.ToDictionary(x => x.Id, x => x); foreach (var train in trains) { var value = ResolveSportsTestValue(train); if (value == 0) continue; if (!studentMap.TryGetValue(train.UserSerial, out var student)) continue; S_Class? classInfo = null; if (student.ClassId.HasValue) classMap.TryGetValue(student.ClassId.Value, out classInfo); var category = ResolveSportsTestCategory(train.CourseId); var standard = healthStandards.FirstOrDefault(x => x.CategoryEnum == category.CategoryEnum && x.GradeId == (classInfo?.GradeId ?? 0) && x.Sex == student.Sex && value >= x.MinValue && value < x.MaxValue); var sportsTestValueEntity = new N_SportsTestValue { SchoolCode = student.SchoolCode, Sex = student.Sex, DataSource = DataSource.YL, CategoryValue = category.CategoryValue, CategoryEnum = category.CategoryEnum, GradeId = classInfo?.GradeId ?? 0, GradeName = classInfo?.GradeName ?? "", ClassId = student.ClassId ?? 0, ClassName = classInfo?.ClassName ?? student.ClassName ?? "", TeacherId = 0, TeacherName = "", StudentNo = student.StudentNo, StudentName = student.StudentName, Value = value, MotionDuration = train.Duration > 0 ? (int)Math.Ceiling(train.Duration / 60.0) : 0, Consumption = 0, Strength = 0, Score = standard?.Score ?? 0, Rank = standard?.Rank, RankEnum = standard?.Score.GetRank() ?? 0, AdditionalScore = 0, WrongResults = train.UnStandardCounts, Creator = orgId, CreateDate = nowTime, ScoreTime = train.EndTime, Year = semesterDto.Year, Semester = semesterDto.Semester, IsDisplay = true }; sportsTestValueEntities.Add(sportsTestValueEntity); } return sportsTestValueEntities; } private static (int CategoryValue, string CategoryEnum) ResolveSportsTestCategory(int courseId) { var categoryEnum = courseId switch { 3 => nameof(SportsTestItemType.Pull_Up), 7 => nameof(SportsTestItemType.StandingLongJump), 8 => nameof(SportsTestItemType.OneMinuteJumpRope), 23 => nameof(SportsTestItemType.Sit_And_Reach), 1001 => nameof(SportsTestItemType.BMI), 1002 => nameof(SportsTestItemType.VitalCapacity), _ => Enum.GetName(typeof(CourseType), courseId) ?? courseId.ToString() }; return (courseId, categoryEnum); } private static float ResolveSportsTestValue(YL_Train train) { if (train.CourseId == (int)CourseType.StandingLongJump && train.Distance.HasValue) return (float)train.Distance.Value; if (train.TimeKeeping.HasValue && train.TimeKeeping.Value > 0) return (float)train.TimeKeeping.Value; if (train.StandardCounts > 0) return (float)train.StandardCounts; if (train.Distance.HasValue) return (float)train.Distance.Value; return train.Score.HasValue ? (float)train.Score.Value : 0; } public async Task TrainDetailsAsync(int trainId) { // 1. 获取训练记录 var train = await _context.YL_Train.FirstOrDefaultAsync(t => t.Id == trainId); if (train == null) return null; // 2. 获取学生信息 var student = await _context.Student.FirstOrDefaultAsync(s => s.StudentNo == train.UserSerial); if (student == null) return null; // 3. 最近10次训练记录(CountList) var recentTrains = await _context.YL_Train .Where(t => t.UserSerial == train.UserSerial && t.CourseId == train.CourseId) .OrderByDescending(t => t.StartTime) .Take(10) .Select(t => new CountListItem { TrainId = t.Id, Score = (t.Score ?? 0).ToString("0"), // 先拿数值 Time = t.StartTime.ToString("yyyy-MM-dd HH:mm:ss") }) .AsNoTracking() // 可选,提高性能 .ToListAsync(); // 在内存中格式化 var recentTrainsDto = recentTrains .Select(t => new CountListItem { TrainId = t.TrainId, Score = t.Score, // 格式化整数 Time = t.Time }) .ToList(); // 4. 近10日最佳成绩(DailyList) var tenDaysAgo = DateTime.Now.Date.AddDays(-10); var dailyScores = await _context.YL_Train .Where(t => t.UserSerial == train.UserSerial && t.CourseId == train.CourseId && t.StartTime >= tenDaysAgo) .GroupBy(t => t.StartTime.Date) .Select(g => new { Date = g.Key, MaxScore = g.Max(t => t.Score) ?? 0 }) .OrderByDescending(g => g.Date) .ToListAsync(); // 在内存中格式化 var dailyListDto = dailyScores .Select(d => new DailyListItem { Score = d.MaxScore.ToString("0"), Time = d.Date.ToString("yyyy-MM-dd 00:00:00") }) .ToList(); // 5. 课程名称(枚举描述) string courseName = Util.GetEnumDescription(train.CourseId); var trainClass = await _context.Class.FindAsync(student.ClassId); // 6. 返回 DTO return new DeviceSyncTrainDetailsDto { TrainId = train.Id, UserSerial = train.UserSerial, UserType = 2, // 学生 Nickname = student.StudentName, FacePictureUrl = student.Photo ?? "", Identity = "学生", OrganizationId = (int)student.ClassId, OrganizationName = FormatOrganizationName(trainClass, student.ClassName), Gender = student.Sex == 1 ? "男" : "女", Score = train.Score?.ToString("0") ?? "0", UnStandardCounts = train.UnStandardCounts, CourseId = train.CourseId, CourseName = courseName, TrainType = "自由练习", VideoUrl = train.VideoSerial, StartTime = train.StartTime.ToString("yyyy-MM-dd HH:mm:ss"), EndTime = train.EndTime.ToString("yyyy-MM-dd HH:mm:ss"), CountList = recentTrains, DailyList = dailyListDto, KeyframeList = new List() // 暂时为空 }; } public async Task> GradeOrderAsync(DeviceSyncGradeOrderRequestDto dto) { // 1️⃣ 获取组织ID var orgId = await GetOrgIdByDeviceAsync(dto.DeviceSerial); if (!orgId.HasValue) return new List(); // 2️⃣ 获取学校编码 string schoolCode = await _context.School .Where(x => x.Id == orgId) .Select(x => x.SchoolCode) .FirstAsync(); var (start, end) = GetDateRange(dto.DateType, dto.QueryStartTime, dto.QueryEndTime); // 4️⃣ 主查询(YL_Train + Student) var query = from t in _context.YL_Train join s in _context.Student on t.UserSerial equals s.StudentNo where s.SchoolCode == schoolCode select new { t, s }; // 5️⃣ 条件过滤 if (dto.CourseId > 0) query = query.Where(x => x.t.CourseId == dto.CourseId.Value); if (dto.Gender > 0) query = query.Where(x => x.s.Sex == dto.Gender.Value); if (dto.OrganizeId != null && dto.OrganizeId.Count > 0 && dto.OrganizeId[0] > 0) query = query.Where(x => dto.OrganizeId.Contains(x.s.ClassId ?? 0)); if (start.HasValue) query = query.Where(x => x.t.StartTime >= start.Value); if (end.HasValue) query = query.Where(x => x.t.StartTime < end.Value.AddDays(1)); // 6️⃣ 分组求总成绩并排序 var grouped = await query .GroupBy(x => new { x.s.Id, x.s.StudentName, x.s.ClassId, x.s.ClassName, x.s.Sex, x.t.CourseId }) .Select(g => new { Key = g.Key, Score = g.Sum(x => x.t.Score ?? 0) }) .OrderByDescending(x => x.Score) .Skip((dto.Page - 1) * dto.Size) .Take(dto.Size) .ToListAsync(); // ✅ 确保在此处完成 // 7️⃣ 返回结果 var classIds = grouped .Select(x => x.Key.ClassId ?? 0) .Where(x => x > 0) .Distinct() .ToList(); var classMap = await _context.Class .Where(x => classIds.Contains(x.Id)) .ToDictionaryAsync(x => x.Id, x => new { x.GradeName, x.ClassName }); return grouped.Select(x => new DeviceSyncGradeOrderItemDto { UserId = x.Key.Id, Nickname = x.Key.StudentName, OrganizationId = x.Key.ClassId ?? 0, OrganizationName = classMap.TryGetValue(x.Key.ClassId ?? 0, out var classInfo) ? $"{classInfo.GradeName ?? ""}{classInfo.ClassName ?? ""}" : x.Key.ClassName ?? "", Gender = x.Key.Sex, Score = Math.Round(x.Score).ToString("0"), CourseId = x.Key.CourseId, CourseName = Util.GetEnumDescription(x.Key.CourseId) }).ToList(); } public async Task> OrganizationGradeOrderAsync(DeviceSyncGradeOrderRequestDto dto) { var orgId = await GetOrgIdByDeviceAsync(dto.DeviceSerial); if (!orgId.HasValue) return new List(); string schoolCode = await _context.School .Where(x => x.Id == orgId) .Select(x => x.SchoolCode) .FirstAsync(); var (start, end) = GetDateRange(dto.DateType, dto.QueryStartTime, dto.QueryEndTime); var query = from t in _context.YL_Train join s in _context.Student on t.UserSerial equals s.StudentNo where s.SchoolCode == schoolCode && t.Score.HasValue && t.Score.Value > 0 select new { t, s }; if (dto.CourseId.HasValue) query = query.Where(x => x.t.CourseId == dto.CourseId.Value); if (dto.Gender > 0) query = query.Where(x => x.s.Sex == dto.Gender.Value); if (dto.OrganizeId != null && dto.OrganizeId.Count > 0 && dto.OrganizeId[0] > 0) query = query.Where(x => dto.OrganizeId.Contains(x.s.ClassId ?? 0)); if (start.HasValue) query = query.Where(x => x.t.StartTime >= start.Value); if (end.HasValue) query = query.Where(x => x.t.StartTime < end.Value.AddDays(1)); var page = dto.Page < 1 ? 1 : dto.Page; var size = dto.Size < 1 ? 10 : dto.Size; var grouped = await query .GroupBy(x => new { x.s.ClassId, x.s.ClassName, x.t.CourseId }) .Select(g => new { g.Key.ClassId, g.Key.ClassName, g.Key.CourseId, SumScore = g.Sum(x => x.t.Score ?? 0), AvgScore = g.Average(x => x.t.Score ?? 0), SumCount = g.Count() }) .OrderByDescending(x => x.SumScore) .Skip((page - 1) * size) .Take(size) .ToListAsync(); var organizationClassIds = grouped .Select(x => x.ClassId ?? 0) .Where(x => x > 0) .Distinct() .ToList(); var organizationClassMap = await _context.Class .Where(x => organizationClassIds.Contains(x.Id)) .ToDictionaryAsync(x => x.Id); return grouped.Select(x => new DeviceSyncOrganizationGradeOrderItemDto { OrganizationName = organizationClassMap.TryGetValue(x.ClassId ?? 0, out var classInfo) ? FormatOrganizationName(classInfo, x.ClassName ?? "") : x.ClassName ?? "", SumScore = Math.Round(x.SumScore).ToString("0"), AvgScore = x.AvgScore.ToString("0.##"), SumCount = x.SumCount, CourseId = x.CourseId, CourseName = Util.GetEnumDescription(x.CourseId) }).ToList(); } public async Task BestScoreAsync(DeviceSyncBestScoreRequestDto dto) { var orgId = await GetOrgIdByDeviceAsync(dto.DeviceSerial); if (!orgId.HasValue) return null; string schoolCode = await _context.School .Where(x => x.Id == orgId) .Select(x => x.SchoolCode) .FirstAsync(); var currentStudent = await _context.Student .FirstOrDefaultAsync(x => x.SchoolCode == schoolCode && x.StudentNo == dto.UserSerial); if (currentStudent == null) return null; var currentClass = currentStudent.ClassId.HasValue ? await _context.Class.FirstOrDefaultAsync(x => x.Id == currentStudent.ClassId.Value) : null; var query = from t in _context.YL_Train join s in _context.Student on t.UserSerial equals s.StudentNo where s.SchoolCode == schoolCode && t.CourseId == dto.CourseId && s.Sex == dto.Gender && t.Score.HasValue && t.Score.Value > 0 select new { t, s }; if (dto.Range == 1) { query = query.Where(x => x.s.ClassId == currentStudent.ClassId); } else if (dto.Range == 2 && currentClass != null) { var classIds = await _context.Class .Where(x => x.SchoolCode == schoolCode && x.GradeId == currentClass.GradeId) .Select(x => x.Id) .ToListAsync(); query = query.Where(x => classIds.Contains(x.s.ClassId ?? 0)); } var best = await query .OrderByDescending(x => x.t.Score) .ThenBy(x => x.t.StartTime) .FirstOrDefaultAsync(); if (best == null) return null; var bestClass = best.s.ClassId.HasValue ? await _context.Class.FirstOrDefaultAsync(x => x.Id == best.s.ClassId.Value) : null; return new DeviceSyncBestScoreDto { UserSerial = best.s.StudentNo, Nickname = best.s.StudentName, OrganizationId = best.s.ClassId ?? 0, OrganizationName = FormatOrganizationName(bestClass, best.s.ClassName ?? ""), Gender = best.s.Sex, Score = best.t.Score?.ToString("0.##") ?? "0", CourseId = best.t.CourseId, CourseName = Util.GetEnumDescription(best.t.CourseId) }; } public async Task> LastGradeAsync(DeviceSyncLastGradeRequestDto dto) { if (dto?.UserSerials == null || dto.UserSerials.Count == 0) return new List(); var serials = dto.UserSerials .Where(x => !string.IsNullOrWhiteSpace(x)) .Select(x => x.Trim()) .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); var students = await _context.Student .Where(s => serials.Contains(s.StudentNo)) .ToListAsync(); var studentSerials = students.Select(s => s.StudentNo).ToList(); var lastGradeClassIds = students .Select(x => x.ClassId ?? 0) .Where(x => x > 0) .Distinct() .ToList(); var lastGradeClassMap = await _context.Class .Where(x => lastGradeClassIds.Contains(x.Id)) .ToDictionaryAsync(x => x.Id); var trains = await _context.YL_Train .Where(t => studentSerials.Contains(t.UserSerial)) .ToListAsync(); // 👉 人 + 课程 聚合 var trainGroup = trains .GroupBy(t => new { t.UserSerial, t.CourseId }) .ToDictionary( g => (g.Key.UserSerial, g.Key.CourseId), g => new { Best = g.Max(x => x.Score ?? 0), Last = g.OrderByDescending(x => x.EndTime).FirstOrDefault()?.Score ?? 0 }); // 👉 课程枚举(必须全返回) var courseIds = new List { 2, 7, 8, 9, 10, 11 }; var result = new List(); foreach (var serial in serials) { var st = students.FirstOrDefault(s => s.StudentNo == serial); var user = new DeviceSyncUserDto { UserId = st?.Id ?? 0, UserType = 0, // 你自己定义 Username = st?.StudentNo ?? serial, Nickname = st?.StudentName ?? "", OrganizationId = st?.ClassId ?? 0, OrganizationName = st != null && lastGradeClassMap.TryGetValue(st.ClassId ?? 0, out var lastGradeClass) ? FormatOrganizationName(lastGradeClass, st.ClassName ?? "") : st?.ClassName ?? "", GradeName = "", // 没有就空 FacePictureUrl = st?.Photo ?? "", Identity = "", // 可选 Gender = st == null ? "" : (st.Sex == 1 ? "男" : "女"), Birthday = st?.Birthday?.ToString("yyyy-MM-dd") ?? "", UserSerial = serial }; // 👉 构造 trainLimitValues foreach (var cid in courseIds) { if (trainGroup.TryGetValue((serial, cid), out var val)) { user.TrainLimitValues.Add(new TrainLimitValueDto { CourseId = cid, BestResult = Math.Round(val.Best).ToString("0"), LastResult = Math.Round(val.Last).ToString("0") }); } else { user.TrainLimitValues.Add(new TrainLimitValueDto { CourseId = cid, BestResult = "0", LastResult = "0" }); } } result.Add(user); } return result; } public async Task> RecordAppAsync(DeviceSyncRecordAppRequestDto dto) { var orgId = await GetOrgIdByDeviceAsync(dto.DeviceSerial); if (!orgId.HasValue) return new List(); // 👉 1. 学校编码 string schoolCode = await _context.School .Where(x => x.Id == orgId) .Select(x => x.SchoolCode) .FirstAsync(); // 👉 2. 时间范围 var (start, end) = GetDateRange(dto.DateType, dto.QueryStartTime, dto.QueryEndTime); // 👉 3. 主查询(YL_Train + Student) var query = from t in _context.YL_Train join s in _context.Student on t.UserSerial equals s.StudentNo where s.SchoolCode == schoolCode select new { t, s }; // 👉 4. 条件过滤 if (dto.CourseId > 0) query = query.Where(x => x.t.CourseId == dto.CourseId.Value); if (dto.Gender > 0) query = query.Where(x => x.s.Sex == dto.Gender.Value); if (!string.IsNullOrEmpty(dto.UserSerial)) query = query.Where(x => x.t.UserSerial == dto.UserSerial); if (!string.IsNullOrEmpty(dto.UserName)) query = query.Where(x => x.s.StudentName.Contains(dto.UserName)); if (dto.OrganizeId != null && dto.OrganizeId.Count > 0) query = query.Where(x => dto.OrganizeId.Contains(x.s.ClassId ?? 0)); if (start.HasValue) query = query.Where(x => x.t.StartTime >= start.Value); if (end.HasValue) query = query.Where(x => x.t.StartTime < end.Value.AddDays(1)); // 👉 5. 分页 + 排序 var list = await query .OrderByDescending(x => x.t.StartTime) .Skip((dto.Page - 1) * dto.Size) .Take(dto.Size) .Select(x => new DeviceSyncRecordAppItemDto { TrainId = x.t.Id, Nickname = x.s.StudentName, OrganizationId = x.s.ClassId ?? 0, OrganizationName = x.s.ClassName, Gender = x.s.Sex, FacePictureUrl = x.s.Photo, Score = x.t.Score.HasValue ? x.t.Score.Value.ToString("0.##") : "0", StartTime = x.t.StartTime.ToString("yyyy-MM-dd HH:mm:ss"), EndTime = x.t.EndTime.ToString("yyyy-MM-dd HH:mm:ss"), CourseId = x.t.CourseId, CourseName = Util.GetEnumDescription(x.t.CourseId), // 👉 建议后面接字典表 }) .ToListAsync(); var recordClassIds = list .Select(x => x.OrganizationId) .Where(x => x > 0) .Distinct() .ToList(); var recordClassMap = await _context.Class .Where(x => recordClassIds.Contains(x.Id)) .ToDictionaryAsync(x => x.Id); foreach (var item in list) { if (recordClassMap.TryGetValue(item.OrganizationId, out var recordClass)) item.OrganizationName = FormatOrganizationName(recordClass, item.OrganizationName); } return list; } public async Task MyRecordAsync(DeviceSyncMyRecordRequestDto dto) { var result = new DeviceSyncMyRecordResponseDto(); var orgId = await GetOrgIdByDeviceAsync(dto.DeviceSerial); if (!orgId.HasValue) return result; // 👉 1. 查学生 string schoolCode = await _context.School .Where(x => x.Id == orgId) .Select(x => x.SchoolCode) .FirstAsync(); var student = await _context.Student .FirstOrDefaultAsync(x => x.SchoolCode == schoolCode && x.StudentNo == dto.UserSerial); if (student == null) return result; var c = await _context.Class.FindAsync(student.ClassId); result.UserSync = new UserSyncDto { UserId = student.Id, UserType = 2, Nickname = student.StudentName, OrganizationId = student.ClassId ?? 0, OrganizationName = FormatOrganizationName(c, ""), FacePictureUrl = student.Photo, Identity = "学生", Gender = student.Sex == 1 ? "男" : "女", Birthday = student.Birthday?.ToString("yyyy-MM-dd") ?? "", UserSerial = student.StudentNo, Height = "0", Weight = "0" }; // 👉 2. 时间范围 var (start, end) = GetDateRange(dto.DateType, dto.QueryStartTime, dto.QueryEndTime); result.UserSync.Identity = "学生"; result.UserSync.Gender = student.Sex == 1 ? "男" : "女"; var query = _context.YL_Train .Where(x => x.UserSerial == dto.UserSerial && x.DeviceSerial == dto.DeviceSerial && x.DeviceType == dto.DeviceType && x.Score.HasValue && x.Score.Value > 0); if (start.HasValue) query = query.Where(x => x.StartTime >= start.Value); if (end.HasValue) query = query.Where(x => x.StartTime < end.Value.AddDays(1)); if (dto.CourseIdList != null && dto.CourseIdList.Count > 0) query = query.Where(x => dto.CourseIdList.Contains(x.CourseId)); else if (dto.CourseType > 0) query = query.Where(x => x.CourseId == dto.CourseType); var trains = await query .Select(x => new { x.CourseId, x.StartTime, x.Duration, x.Score }) .ToListAsync(); if (!trains.Any()) return result; // 👉 3. 统计 var trainDays = trains.Select(x => x.StartTime.Date).Distinct().Count(); var totalSeconds = trains.Sum(x => x.Duration); var trainHours = (totalSeconds / 3600.0).ToString("0.00"); // 👉 4. gradeList(按天统计) var gradeList = trains .GroupBy(x => x.CourseId) .Select(g => new GradeListItem { CourseId = g.Key, CourseName = Util.GetEnumDescription(g.Key), BestGrade = (g.Max(x => x.Score) ?? 0).ToString("0.##"), AvgGrade = (g.Average(x => x.Score) ?? 0).ToString("0.##") }).ToList(); // 👉 5. graphTrainList(课目统计) var graphList = trains .GroupBy(x => x.CourseId) .Select(g => new GraphTrainListItem { CourseId = g.Key, CourseName = Util.GetEnumDescription(g.Key), Time = g.Sum(x => x.Duration), Count = g.Count() }).ToList(); // 👉 6. courseBestList(最好/平均/最差) var courseBestList = trains .Where(x => x.Score.HasValue) .GroupBy(x => x.CourseId) .Select(g => new CourseBestListItem { CourseId = g.Key, CourseName = Util.GetEnumDescription(g.Key), BestGrade = g.Max(x => x.Score).ToString(), AvgGrade = g.Average(x => x.Score).ToString(), WorstGrade = g.Min(x => x.Score).ToString() }).ToList(); // 👉 7. 返回 result = new DeviceSyncMyRecordResponseDto { UserSync = new UserSyncDto { UserId = student.Id, UserType = 2, Nickname = student.StudentName, OrganizationId = student.ClassId ?? 0, OrganizationName = FormatOrganizationName(c, ""), FacePictureUrl = student.Photo, Identity = "学生", Gender = student.Sex == 1 ? "男" : "女", Birthday = student.Birthday?.ToString("yyyy-MM-dd") ?? "", UserSerial = student.StudentNo, Height = "", Weight = "" }, TrainCourseSum = graphList.Count, TrainDays = trainDays, TrainHours = trainHours, TrainCounts = trains.Count, GradeList = gradeList, GraphTrainList = graphList, //CourseBestList = courseBestList }; result.UserSync.Identity = "学生"; result.UserSync.Gender = student.Sex == 1 ? "男" : "女"; result.UserSync.Height = "0"; result.UserSync.Weight = "0"; return result; } public async Task YtpMyRecordAsync(DeviceSyncYtpMyRecordRequestDto dto) { var result = new DeviceSyncYtpMyRecordResponseDto(); var orgId = await GetOrgIdByDeviceAsync(dto.DeviceSerial); if (!orgId.HasValue) return result; var schoolCode = await _context.School .Where(x => x.Id == orgId) .Select(x => x.SchoolCode) .FirstAsync(); var student = await _context.Student .FirstOrDefaultAsync(x => x.SchoolCode == schoolCode && x.StudentNo == dto.UserSerial); if (student == null) return result; var studentClass = await _context.Class.FindAsync(student.ClassId); result.UserSync = new UserSyncDto { UserId = student.Id, UserType = 2, Nickname = student.StudentName, OrganizationId = student.ClassId ?? 0, OrganizationName = FormatOrganizationName(studentClass, student.ClassName ?? ""), FacePictureUrl = student.Photo ?? "", Identity = "学生", Gender = student.Sex == 1 ? "男" : "女", Birthday = student.Birthday?.ToString("yyyy-MM-dd") ?? "", UserSerial = student.StudentNo, Height = "0", Weight = "0" }; result.UserSync.Identity = "学生"; result.UserSync.Gender = student.Sex == 1 ? "男" : "女"; var (start, end) = GetDateRange(dto.DateType, dto.QueryStartTime, dto.QueryEndTime); var query = _context.YL_Train .AsNoTracking() .Where(x => x.UserSerial == dto.UserSerial && x.DeviceSerial == dto.DeviceSerial && x.DeviceType == dto.DeviceType); if (start.HasValue) query = query.Where(x => x.StartTime >= start.Value); if (end.HasValue) query = query.Where(x => x.StartTime < end.Value.AddDays(1)); var trains = await query .Select(x => new { x.CourseId, x.StartTime, x.Duration }) .ToListAsync(); if (!trains.Any()) { var today = DateTime.Now.Date; result.GradeList = Enumerable.Range(0, 7) .Select(i => today.AddDays(i - 6)) .Select(day => new YtpGradeListItem { Time = day.ToString("yyyy-MM-dd"), Duration = "0", Count = "0" }) .ToList(); return result; } var totalSeconds = trains.Sum(x => x.Duration); result.TrainCourseSum = trains.Select(x => x.CourseId).Distinct().Count(); result.TrainDays = trains.Select(x => x.StartTime.Date).Distinct().Count(); result.TrainHours = (totalSeconds / 3600.0).ToString("0.00"); result.TrainCounts = trains.Count; var graphEnd = end?.Date ?? DateTime.Now.Date; var graphStart = graphEnd.AddDays(-6); result.GradeList = Enumerable.Range(0, 7) .Select(i => graphStart.AddDays(i)) .Select(day => { var dayTrains = trains.Where(x => x.StartTime.Date == day).ToList(); return new YtpGradeListItem { Time = day.ToString("yyyy-MM-dd"), Duration = dayTrains.Sum(x => x.Duration).ToString(), Count = dayTrains.Count.ToString() }; }) .ToList(); result.GraphTrainList = trains .GroupBy(x => x.CourseId) .Select(g => new YtpGraphTrainListItem { CourseId = g.Key, CourseName = Util.GetEnumDescription(g.Key), Time = g.Sum(x => x.Duration).ToString(), Count = g.Count() }) .OrderByDescending(x => int.TryParse(x.Time, out var seconds) ? seconds : 0) .ToList(); return result; } public async Task TeacherLoginAsync(TeacherLoginRequestDto dto) { var pwdHash = MD5Helper.MD5Encrypt32(dto.Password); var user = await _context.Sys_User.FirstOrDefaultAsync(x => x.UserName == dto.Username && x.UserPwd == pwdHash); if (user == null) return null; var school = await _context.School.FirstOrDefaultAsync(x => x.SchoolCode == user.SchoolCode); return new TeacherLoginResponseDto { UserId = user.User_Id, Nickname = user.UserTrueName ?? user.UserName, Username = user.UserName, OrganizationId = school?.Id ?? 0, OrganizationName = school?.SchoolName ?? user.SchoolName ?? "", SerialNumber = user.UserName, Classification = 1, Gender = user.Gender }; } /// /// 新增app版本 /// /// /// public async Task AppVersionAddAsync(AppVersionAddRequestDto dto) { // 1️⃣ 如果设为最新版本,取消旧版本 if (dto.IsLatest == 1) { var oldList = await _context.Set() .Where(x => x.ApplyName == dto.ApplyName && x.DeviceType == dto.DeviceType && x.HardwareType == dto.HardwareType && x.OperatingSystem == dto.OperatingSystem && x.Status == 1) .ToListAsync(); foreach (var item in oldList) { item.IsLatest = 0; } } // 2️⃣ 新版本 var entity = new YL_AppVersion { ApplyName = dto.ApplyName, DeviceType = dto.DeviceType, HardwareType = dto.HardwareType, OperatingSystem = dto.OperatingSystem, VersionNumber = dto.VersionNumber, VersionCode = dto.VersionCode, UpdateUrl = dto.UpdateUrl, NeedForcedUpdating = dto.NeedForcedUpdating, IsLatest = dto.IsLatest, Status = 1, CreateTime = DateTime.Now }; _context.Set().Add(entity); await _context.SaveChangesAsync(); return entity.Id; } public async Task AppVerifyAsync(AppVerifyRequestDto dto) { // 1️⃣ 查询最新版本 var latest = await _context.Set() .Where(x => x.ApplyName == dto.ApplyName && x.DeviceType == dto.DeviceType && x.HardwareType == dto.HardwareType && x.OperatingSystem == dto.OperatingSystem && x.Status == 1 && x.PublishStatus == 1) .OrderByDescending(x => x.IsLatest) .ThenByDescending(x => x.VersionCode) .FirstOrDefaultAsync(); // 2️⃣ 没有配置版本 → 不更新 if (latest == null) { return new AppVerifyResponseDto { VersionNumber = dto.VersionNumber, NeedForcedUpdating = 0, UpdateUrl = null, HardwareType = dto.HardwareType, ApplyName = dto.ApplyName }; } // 3️⃣ 版本比较(必须用 VersionCode) int currentVersionCode = ParseVersionCode(dto.VersionNumber); bool needUpdate = latest.VersionCode > currentVersionCode; if (!needUpdate) { return new AppVerifyResponseDto { VersionNumber = dto.VersionNumber, NeedForcedUpdating = 0, UpdateUrl = null, HardwareType = dto.HardwareType, ApplyName = dto.ApplyName }; } // 4️⃣ 是否强制升级 bool needForce = latest.NeedForcedUpdating == 1; // 最低版本限制(强制升级) if (!string.IsNullOrEmpty(latest.MinSupportVersion)) { if (CompareVersion(dto.VersionNumber, latest.MinSupportVersion) < 0) { needForce = true; } } // 5️⃣ 返回结果 return new AppVerifyResponseDto { VersionNumber = latest.VersionNumber, NeedForcedUpdating = needForce ? 1 : 0, UpdateUrl = latest.UpdateUrl, HardwareType = latest.HardwareType, ApplyName = latest.ApplyName }; } /// /// 视频回放上传 /// /// /// public async Task DeviceVideoUploadAsync(DeviceVideoUploadRequestDto dto) { if (dto.File == null || dto.File.Length == 0) return false; // 1️⃣ 生成OSS路径(按我之前给你的规范) var path = $"Upload/device/{dto.DeviceType}/{dto.DeviceSerial}/video/{dto.VideoSerial}/"; // 2️⃣ 上传到OSS var url = ALiYunOss.Upload(dto.File, path, dto.VideoSerial); if (string.IsNullOrEmpty(url)) return false; // 3️⃣ 入库 var entity = new YL_DeviceVideo { DeviceSerial = dto.DeviceSerial, DeviceType = dto.DeviceType, VideoSerial = dto.VideoSerial, VideoUrl = url, Timestamp = dto.Timestamp, CreateTime = DateTime.Now, Status = 1 }; _context.Set().Add(entity); await _context.SaveChangesAsync(); return true; } public async Task GetUserDetailAsync(string deviceSerial, int userId) { var orgId = await GetOrgIdByDeviceAsync(deviceSerial); if (!orgId.HasValue) return null; string schoolCode = await _context.School.Where(x => x.Id == orgId).Select(x => x.SchoolCode).FirstAsync(); var s = await _context.Student.FirstOrDefaultAsync(x => x.Id == userId && x.SchoolCode == schoolCode); if (s == null) return null; var dto = new DeviceSyncUserDto { UserId = s.Id, UserType = 2, Username = s.StudentNo, Nickname = s.StudentName, OrganizationId = s.ClassId ?? 0, OrganizationName = s.ClassName ?? "", GradeName = "", FacePictureUrl = s.Photo ?? "", Identity = "学生", Gender = s.Sex == 1 ? "男" : "女", Birthday = s.Birthday.HasValue ? s.Birthday.Value.ToString("yyyy-MM-dd") : "", UserSerial = s.StudentNo }; var c = await _context.Class.FindAsync(dto.OrganizationId); if (c != null) { dto.OrganizationName = FormatOrganizationName(c, dto.OrganizationName); dto.GradeName = c.GradeName ?? ""; } return dto; } private static string NormalizeTrainSerial(string serial, DeviceSyncTrainPushRequestDto item) { if (!string.IsNullOrWhiteSpace(serial)) return NormalizeSerial(serial, "AUTO-" + MD5Helper.MD5Encrypt32(serial)); var seed = string.Join("|", new[] { item.DeviceSerial, item.UserSerial, item.CourseId.ToString(), item.TrainExam.ToString(), item.StartTime, item.EndTime, item.Duration.ToString(), item.StandardCounts.ToString(), item.UnStandardCounts.ToString(), item.Distance?.ToString() ?? "", item.TimeKeeping?.ToString() ?? "" }); return "AUTO-" + MD5Helper.MD5Encrypt32(seed); } private static string NormalizeSerial(string serial, string fallback) { var value = string.IsNullOrWhiteSpace(serial) ? fallback : serial.Trim(); if (value.Length <= 64) return value; return "AUTO-" + MD5Helper.MD5Encrypt32(value); } private static (DateTime? start, DateTime? end) GetDateRange(int dateType, string queryStart, string queryEnd) { var today = DateTime.Now.Date; var weekStart = today.AddDays(-(((int)today.DayOfWeek + 6) % 7)); var monthStart = new DateTime(today.Year, today.Month, 1); (DateTime? start, DateTime? end) result = dateType switch { 0 => ((DateTime?)null, (DateTime?)null), 1 => (today, today), 2 => (weekStart, today), 3 => (monthStart, today), 4 => (today.Month >= 9 ? new DateTime(today.Year, 9, 1) : new DateTime(today.Year - 1, 9, 1), today), 5 => (DateTime.TryParse(queryStart, out var s) ? s : (DateTime?)null, DateTime.TryParse(queryEnd, out var e) ? e : (DateTime?)null), 6 => (weekStart.AddDays(-7), weekStart.AddDays(-1)), 7 => (monthStart.AddMonths(-1), monthStart.AddDays(-1)), 8 => (today.AddMonths(-3), today), 9 => (new DateTime(today.Year, 1, 1), today), _ => ((DateTime?)null, (DateTime?)null) }; if (result.start.HasValue && result.end.HasValue && result.start.Value > result.end.Value) result = (result.end, result.start); return result; } private static string FormatOrganizationName(S_Class classInfo, string fallback = "") { if (classInfo == null) return fallback ?? ""; var name = $"{classInfo.GradeName ?? ""}{classInfo.ClassName ?? ""}"; return string.IsNullOrWhiteSpace(name) ? fallback ?? "" : name; } private int CompareVersion(string v1, string v2) { if (string.IsNullOrEmpty(v1) || string.IsNullOrEmpty(v2)) return 0; var arr1 = v1.Split('.'); var arr2 = v2.Split('.'); int len = Math.Max(arr1.Length, arr2.Length); for (int i = 0; i < len; i++) { int num1 = i < arr1.Length ? int.Parse(arr1[i]) : 0; int num2 = i < arr2.Length ? int.Parse(arr2[i]) : 0; if (num1 > num2) return 1; if (num1 < num2) return -1; } return 0; } private int ParseVersionCode(string version) { if (string.IsNullOrEmpty(version)) return 0; var parts = version.Split('.'); int code = 0; for (int i = 0; i < parts.Length; i++) { if (int.TryParse(parts[i], out int num)) { code = code * 100 + num; } } return code; } } }