YD_YiLu.Api/YD_YiLu/Services/Impl/DeviceSyncService.cs
2026-07-15 15:38:03 +08:00

1485 lines
57 KiB
C#
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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

using 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
{
/// <summary>
/// 壹动站/壹体屏/活力课间屏 设备同步服务实现
/// </summary>
public class DeviceSyncService : IDeviceSyncService
{
private readonly SmartSportsContext _context;
public DeviceSyncService(SmartSportsContext context)
{
_context = context;
}
public async Task<int?> 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<DeviceSyncUserPageResponseDto> 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<S_Student>)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<DeviceSyncUserPageResponseDto> 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<List<DeviceSyncOrganizationDto>> OrganizationAsync(DeviceSyncOrganizationRequestDto dto)
{
var orgId = await GetOrgIdByDeviceAsync(dto.DeviceSerial);
if (!orgId.HasValue) return new List<DeviceSyncOrganizationDto>();
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<DeviceSyncOrganizationDto>();
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<int>()
}).ToList(),
ChildrenIds = gradeClasses.Select(x => x.Id).ToList()
});
}
return result;
}
public async Task<List<DeviceSyncTrainPushResponseItem>> TrainPushAsync(List<DeviceSyncTrainPushRequestDto> list)
{
if (list == null || !list.Any())
return new List<DeviceSyncTrainPushResponseItem>();
var result = new List<DeviceSyncTrainPushResponseItem>();
// 👉 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<string>(existSerials, StringComparer.OrdinalIgnoreCase);
var newEntities = new List<YL_Train>();
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<List<N_SportsTestValue>> AddSportsTestValueEntity(int orgId, List<YL_Train> trains)
{
var sportsTestValueEntities = new List<N_SportsTestValue>();
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<DeviceSyncTrainDetailsDto> 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<CourseType>(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<KeyframeListItem>() // 暂时为空
};
}
public async Task<List<DeviceSyncGradeOrderItemDto>> GradeOrderAsync(DeviceSyncGradeOrderRequestDto dto)
{
// 1️⃣ 获取组织ID
var orgId = await GetOrgIdByDeviceAsync(dto.DeviceSerial);
if (!orgId.HasValue) return new List<DeviceSyncGradeOrderItemDto>();
// 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<CourseType>(x.Key.CourseId)
}).ToList();
}
public async Task<List<DeviceSyncOrganizationGradeOrderItemDto>> OrganizationGradeOrderAsync(DeviceSyncGradeOrderRequestDto dto)
{
var orgId = await GetOrgIdByDeviceAsync(dto.DeviceSerial);
if (!orgId.HasValue) return new List<DeviceSyncOrganizationGradeOrderItemDto>();
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<CourseType>(x.CourseId)
}).ToList();
}
public async Task<DeviceSyncBestScoreDto> 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<CourseType>(best.t.CourseId)
};
}
public async Task<List<DeviceSyncUserDto>> LastGradeAsync(DeviceSyncLastGradeRequestDto dto)
{
if (dto?.UserSerials == null || dto.UserSerials.Count == 0)
return new List<DeviceSyncUserDto>();
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<int> { 2, 7, 8, 9, 10, 11 };
var result = new List<DeviceSyncUserDto>();
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<List<DeviceSyncRecordAppItemDto>> RecordAppAsync(DeviceSyncRecordAppRequestDto dto)
{
var orgId = await GetOrgIdByDeviceAsync(dto.DeviceSerial);
if (!orgId.HasValue)
return new List<DeviceSyncRecordAppItemDto>();
// 👉 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<CourseType>(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<DeviceSyncMyRecordResponseDto> 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<CourseType>(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<CourseType>(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<CourseType>(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<DeviceSyncYtpMyRecordResponseDto> 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<CourseType>(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<TeacherLoginResponseDto> 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
};
}
/// <summary>
/// 新增app版本
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
public async Task<long> AppVersionAddAsync(AppVersionAddRequestDto dto)
{
// 1️⃣ 如果设为最新版本,取消旧版本
if (dto.IsLatest == 1)
{
var oldList = await _context.Set<YL_AppVersion>()
.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<YL_AppVersion>().Add(entity);
await _context.SaveChangesAsync();
return entity.Id;
}
public async Task<AppVerifyResponseDto> AppVerifyAsync(AppVerifyRequestDto dto)
{
// 1️⃣ 查询最新版本
var latest = await _context.Set<YL_AppVersion>()
.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
};
}
/// <summary>
/// 视频回放上传
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
public async Task<bool> 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<YL_DeviceVideo>().Add(entity);
await _context.SaveChangesAsync();
return true;
}
public async Task<DeviceSyncUserDto> 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;
}
}
}