diff --git a/YD_YiLu.Commons/Dto/DeviceSync/DeviceSyncUserPageRequestDto.cs b/YD_YiLu.Commons/Dto/DeviceSync/DeviceSyncUserPageRequestDto.cs index fbe79ea..dcab30a 100644 --- a/YD_YiLu.Commons/Dto/DeviceSync/DeviceSyncUserPageRequestDto.cs +++ b/YD_YiLu.Commons/Dto/DeviceSync/DeviceSyncUserPageRequestDto.cs @@ -86,4 +86,5 @@ namespace YD_YiLu.Commons.Dto.DeviceSync public string BestResult { get; set; } public string LastResult { get; set; } } + } diff --git a/YD_YiLu.Commons/Dto/DeviceSync/DeviceVideoUploadRequestDto.cs b/YD_YiLu.Commons/Dto/DeviceSync/DeviceVideoUploadRequestDto.cs new file mode 100644 index 0000000..c20238d --- /dev/null +++ b/YD_YiLu.Commons/Dto/DeviceSync/DeviceVideoUploadRequestDto.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Http; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace YD_YiLu.Commons.Dto.DeviceSync +{ + public class DeviceVideoUploadRequestDto + { + public string DeviceSerial { get; set; } + + public int DeviceType { get; set; } + + public string VideoSerial { get; set; } + + public long Timestamp { get; set; } + + /// + /// 视频文件(form-data) + /// + public IFormFile File { get; set; } + } +} diff --git a/YD_YiLu/Context/SmartSportsContext.cs b/YD_YiLu/Context/SmartSportsContext.cs index f03947a..e9f3d99 100644 --- a/YD_YiLu/Context/SmartSportsContext.cs +++ b/YD_YiLu/Context/SmartSportsContext.cs @@ -26,11 +26,13 @@ namespace YD_YiLu.Api.Context public DbSet XW_ProjectMode { get; set; } public DbSet XW_TestingProject { get; set; } - public DbSet YL_Device { get; set; } - public DbSet YL_Train { get; set; } public DbSet XW_SportsTestData { get; set; } public DbSet XW_TrainStudents { get; set; } public DbSet XW_TrainSetting { get; set; } public DbSet XW_TrainViolationsBO { get; set; } + public DbSet YL_Device { get; set; } + public DbSet YL_Train { get; set; } + public DbSet YL_DeviceVideo { get; set; } + public DbSet YL_AppVersion { get; set; } } } diff --git a/YD_YiLu/Controllers/DeviceSyncController.cs b/YD_YiLu/Controllers/DeviceSyncController.cs index cf31eb9..2ebc9e3 100644 --- a/YD_YiLu/Controllers/DeviceSyncController.cs +++ b/YD_YiLu/Controllers/DeviceSyncController.cs @@ -138,5 +138,17 @@ namespace YD_YiLu.Api.Controllers var res = await _deviceSyncService.MyRecordAsync(dto); return res; } + + /// + /// 视频回放上传 + /// + /// + /// + [HttpPost("device/sync/video/upload")] + public async Task UploadVideo([FromForm] DeviceVideoUploadRequestDto dto) + { + var res = await _deviceSyncService.DeviceVideoUploadAsync(dto); + return res; + } } } diff --git a/YD_YiLu/Services/Impl/DeviceSyncService.cs b/YD_YiLu/Services/Impl/DeviceSyncService.cs index ad35029..73132bf 100644 --- a/YD_YiLu/Services/Impl/DeviceSyncService.cs +++ b/YD_YiLu/Services/Impl/DeviceSyncService.cs @@ -3,6 +3,7 @@ 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.Utils; @@ -387,86 +388,87 @@ namespace YD_YiLu.Api.Services.Impl }).ToList(); } - - public async Task> LastGradeAsync(DeviceSyncLastGradeRequestDto dto) + public async Task> LastGradeAsync(DeviceSyncLastGradeRequestDto dto) { - if (dto == null || dto.UserSerials == null || dto.UserSerials.Count == 0) - return new List(); + if (dto?.UserSerials == null || dto.UserSerials.Count == 0) + return new List(); - // 去掉空白、重复序列号 - var orderedUnique = dto.UserSerials + var serials = dto.UserSerials .Where(x => !string.IsNullOrWhiteSpace(x)) .Select(x => x.Trim()) .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); - if (!orderedUnique.Any()) return new List(); - - // 查询学生信息 var students = await _context.Student - .Where(s => orderedUnique.Contains(s.StudentNo)) + .Where(s => serials.Contains(s.StudentNo)) .ToListAsync(); - S_Student MatchStudent(string serial) => - students.FirstOrDefault(s => string.Equals(s.StudentNo, serial, StringComparison.OrdinalIgnoreCase)); - - // 获取学生最近一次训练记录 var studentSerials = students.Select(s => s.StudentNo).ToList(); - var trainRows = await _context.YL_Train + + var trains = await _context.YL_Train .Where(t => studentSerials.Contains(t.UserSerial)) - .GroupBy(t => t.UserSerial) - .Select(g => g - .OrderByDescending(t => t.EndTime) - .FirstOrDefault()) .ToListAsync(); - var trainMap = trainRows.ToDictionary(t => t.UserSerial, t => t); - - var result = new List(); - - foreach (var serial in orderedUnique) - { - var st = MatchStudent(serial); - if (st == null) - { - result.Add(new DeviceSyncLastGradeItemDto + // 👉 人 + 课程 聚合 + var trainGroup = trains + .GroupBy(t => new { t.UserSerial, t.CourseId }) + .ToDictionary( + g => (g.Key.UserSerial, g.Key.CourseId), + g => new { - UserSerial = serial, - UserId = 0, - Nickname = "", - OrganizationId = 0, - OrganizationName = "", - Gender = 0, - FacePictureUrl = "", - Score = "0", - UnStandardCounts = 0, - StartTime = "", - EndTime = "", - CourseId = 0, - CourseName = "" + Best = g.Max(x => x.Score ?? 0), + Last = g.OrderByDescending(x => x.EndTime).FirstOrDefault()?.Score ?? 0 }); - continue; + + // 👉 课程枚举(必须全返回) + 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?.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" + }); + } } - trainMap.TryGetValue(serial, out var train); - - result.Add(new DeviceSyncLastGradeItemDto - { - UserSerial = st.StudentNo, - UserId = st.Id, - TrainId = train?.Id ?? 0, - Nickname = st.StudentName, - OrganizationId = st.ClassId ?? 0, - OrganizationName = st.ClassName ?? "", - Gender = st.Sex, - FacePictureUrl = st.Photo ?? "", - Score = train?.Score?.ToString("0") ?? "0", - UnStandardCounts = train?.UnStandardCounts ?? 0, - StartTime = train?.StartTime.ToString("yyyy-MM-dd HH:mm:ss") ?? "", - EndTime = train?.EndTime.ToString("yyyy-MM-dd HH:mm:ss") ?? "", - CourseId = train?.CourseId ?? 0, - CourseName = Util.GetEnumDescription(train?.CourseId) - }); + result.Add(user); } return result; @@ -679,16 +681,108 @@ namespace YD_YiLu.Api.Services.Impl 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 = dto.VersionNumber, - NeedForcedUpdating = 0, - UpdateUrl = null, - HardwareType = dto.HardwareType, - ApplyName = dto.ApplyName + 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); @@ -736,5 +830,44 @@ namespace YD_YiLu.Api.Services.Impl }; } + 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; + } } } diff --git a/YD_YiLu/Services/Interface/IDeviceSyncService.cs b/YD_YiLu/Services/Interface/IDeviceSyncService.cs index 7a3218f..82d2ca7 100644 --- a/YD_YiLu/Services/Interface/IDeviceSyncService.cs +++ b/YD_YiLu/Services/Interface/IDeviceSyncService.cs @@ -41,7 +41,7 @@ namespace YD_YiLu.Api.Services.Interface /// /// 人员上次成绩(按人员序列号批量查询) /// - Task> LastGradeAsync(DeviceSyncLastGradeRequestDto dto); + Task> LastGradeAsync(DeviceSyncLastGradeRequestDto dto); /// /// 查询运动记录 @@ -72,5 +72,10 @@ namespace YD_YiLu.Api.Services.Interface /// 获取人员详情(壹动站协议) /// Task GetUserDetailAsync(string deviceSerial, int userId); + + /// + /// 视频回放上传 + /// + Task DeviceVideoUploadAsync(DeviceVideoUploadRequestDto dto); } } diff --git a/YD_YiLu/SmartSportsEntitys/YL_AppVersion.cs b/YD_YiLu/SmartSportsEntitys/YL_AppVersion.cs new file mode 100644 index 0000000..a7f1e64 --- /dev/null +++ b/YD_YiLu/SmartSportsEntitys/YL_AppVersion.cs @@ -0,0 +1,126 @@ +using Microsoft.EntityFrameworkCore; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace YD_YiLu.Api.SmartSportsEntitys +{ + /// + /// 应用版本表(设备升级控制) + /// + [Table("YL_AppVersion")] + public class YL_AppVersion + { + /// + /// 主键ID + /// + [Key] + public long Id { get; set; } + + /// + /// 应用名称(壹动站 / 壹体机等) + /// + [Required] + [MaxLength(50)] + public string ApplyName { get; set; } + + /// + /// 设备类型 + /// + public int DeviceType { get; set; } + + /// + /// 硬件类型(如 LZ) + /// + [MaxLength(20)] + public string HardwareType { get; set; } + + /// + /// 操作系统(android) + /// + [MaxLength(20)] + public string OperatingSystem { get; set; } + + /// + /// 版本号(如 1.0.1) + /// + [Required] + [MaxLength(20)] + public string VersionNumber { get; set; } + + /// + /// 版本Code(用于排序,安卓强烈建议使用) + /// + public int VersionCode { get; set; } + + /// + /// 是否强制更新 0=否 1=是 + /// + public int NeedForcedUpdating { get; set; } + + /// + /// 更新包下载地址 + /// + [Required] + [MaxLength(500)] + public string UpdateUrl { get; set; } + + /// + /// 是否为最新版本 0=否 1=是 + /// + public int IsLatest { get; set; } + + /// + /// 发布状态 0=未发布 1=已发布 + /// + public int PublishStatus { get; set; } + + /// + /// 最小支持版本(低于该版本必须升级) + /// + [MaxLength(20)] + public string? MinSupportVersion { get; set; } + + /// + /// 更新说明 + /// + [MaxLength(500)] + public string? UpdateContent { get; set; } + + /// + /// 下载包大小(KB) + /// + public long? FileSize { get; set; } + + /// + /// 下载MD5(用于校验) + /// + [MaxLength(64)] + public string? FileMd5 { get; set; } + + /// + /// 灰度发布比例(0-100) + /// + public int GrayPercent { get; set; } + + /// + /// 状态 0=禁用 1=启用 + /// + public int Status { get; set; } + + /// + /// 创建时间 + /// + public DateTime CreateTime { get; set; } + + /// + /// 更新时间 + /// + public DateTime? UpdateTime { get; set; } + + /// + /// 备注 + /// + [MaxLength(500)] + public string? Remark { get; set; } + } +} diff --git a/YD_YiLu/SmartSportsEntitys/YL_DeviceVideo.cs b/YD_YiLu/SmartSportsEntitys/YL_DeviceVideo.cs new file mode 100644 index 0000000..1f6a801 --- /dev/null +++ b/YD_YiLu/SmartSportsEntitys/YL_DeviceVideo.cs @@ -0,0 +1,81 @@ +using Microsoft.EntityFrameworkCore; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace YD_YiLu.Api.SmartSportsEntitys +{ + /// + /// 视频回放表(训练视频上传) + /// + public class YL_DeviceVideo + { + /// + /// 主键ID + /// + [Key] + public long Id { get; set; } + + /// + /// 设备序列号 + /// + [Required] + [MaxLength(64)] + public string DeviceSerial { get; set; } + + /// + /// 设备类型 + /// + public int DeviceType { get; set; } + + /// + /// 训练序列号(一次训练唯一标识) + /// + [Required] + [MaxLength(64)] + public string VideoSerial { get; set; } + + /// + /// 视频访问地址(上传后返回) + /// + [Required] + [MaxLength(500)] + public string VideoUrl { get; set; } + + /// + /// 文件名称 + /// + [MaxLength(200)] + public string? FileName { get; set; } + + /// + /// 文件大小(KB) + /// + public long? FileSize { get; set; } + + /// + /// 视频时长(秒) + /// + public int? Duration { get; set; } + + /// + /// 时间戳(客户端上传) + /// + public long Timestamp { get; set; } + + /// + /// 上传时间 + /// + public DateTime CreateTime { get; set; } + + /// + /// 状态 0=无效 1=有效 + /// + public int Status { get; set; } + + /// + /// 备注 + /// + [MaxLength(500)] + public string? Remark { get; set; } + } +}