From 7b4409de71733ea00192ae79074cae8eee1023a0 Mon Sep 17 00:00:00 2001
From: tanglong <842690096@qq.com>
Date: Mon, 13 Apr 2026 09:59:12 +0800
Subject: [PATCH] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E8=A7=86=E9=A2=91=E6=8E=A5?=
=?UTF-8?q?=E5=8F=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../DeviceSyncUserPageRequestDto.cs | 1 +
.../DeviceSync/DeviceVideoUploadRequestDto.cs | 25 ++
YD_YiLu/Context/SmartSportsContext.cs | 6 +-
YD_YiLu/Controllers/DeviceSyncController.cs | 12 +
YD_YiLu/Services/Impl/DeviceSyncService.cs | 267 +++++++++++++-----
.../Services/Interface/IDeviceSyncService.cs | 7 +-
YD_YiLu/SmartSportsEntitys/YL_AppVersion.cs | 126 +++++++++
YD_YiLu/SmartSportsEntitys/YL_DeviceVideo.cs | 81 ++++++
8 files changed, 455 insertions(+), 70 deletions(-)
create mode 100644 YD_YiLu.Commons/Dto/DeviceSync/DeviceVideoUploadRequestDto.cs
create mode 100644 YD_YiLu/SmartSportsEntitys/YL_AppVersion.cs
create mode 100644 YD_YiLu/SmartSportsEntitys/YL_DeviceVideo.cs
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; }
+ }
+}