上传视频接口

This commit is contained in:
tanglong 2026-04-13 09:59:12 +08:00
parent 020145b6d5
commit 7b4409de71
8 changed files with 455 additions and 70 deletions

View File

@ -86,4 +86,5 @@ namespace YD_YiLu.Commons.Dto.DeviceSync
public string BestResult { get; set; }
public string LastResult { get; set; }
}
}

View File

@ -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; }
/// <summary>
/// 视频文件(form-data)
/// </summary>
public IFormFile File { get; set; }
}
}

View File

@ -26,11 +26,13 @@ namespace YD_YiLu.Api.Context
public DbSet<XW_ProjectMode> XW_ProjectMode { get; set; }
public DbSet<XW_TestingProject> XW_TestingProject { get; set; }
public DbSet<YL_Device> YL_Device { get; set; }
public DbSet<YL_Train> YL_Train { get; set; }
public DbSet<XW_SportsTestData> XW_SportsTestData { get; set; }
public DbSet<XW_TrainStudents> XW_TrainStudents { get; set; }
public DbSet<XW_TrainSetting> XW_TrainSetting { get; set; }
public DbSet<XW_TrainViolationsBO> XW_TrainViolationsBO { get; set; }
public DbSet<YL_Device> YL_Device { get; set; }
public DbSet<YL_Train> YL_Train { get; set; }
public DbSet<YL_DeviceVideo> YL_DeviceVideo { get; set; }
public DbSet<YL_AppVersion> YL_AppVersion { get; set; }
}
}

View File

@ -138,5 +138,17 @@ namespace YD_YiLu.Api.Controllers
var res = await _deviceSyncService.MyRecordAsync(dto);
return res;
}
/// <summary>
/// 视频回放上传
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
[HttpPost("device/sync/video/upload")]
public async Task<bool> UploadVideo([FromForm] DeviceVideoUploadRequestDto dto)
{
var res = await _deviceSyncService.DeviceVideoUploadAsync(dto);
return res;
}
}
}

View File

@ -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<List<DeviceSyncLastGradeItemDto>> LastGradeAsync(DeviceSyncLastGradeRequestDto dto)
public async Task<List<DeviceSyncUserDto>> LastGradeAsync(DeviceSyncLastGradeRequestDto dto)
{
if (dto == null || dto.UserSerials == null || dto.UserSerials.Count == 0)
return new List<DeviceSyncLastGradeItemDto>();
if (dto?.UserSerials == null || dto.UserSerials.Count == 0)
return new List<DeviceSyncUserDto>();
// 去掉空白、重复序列号
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<DeviceSyncLastGradeItemDto>();
// 查询学生信息
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<DeviceSyncLastGradeItemDto>();
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<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?.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<CourseType>(train?.CourseId)
});
result.Add(user);
}
return result;
@ -679,16 +681,108 @@ namespace YD_YiLu.Api.Services.Impl
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 = 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
};
}
/// <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);
@ -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;
}
}
}

View File

@ -41,7 +41,7 @@ namespace YD_YiLu.Api.Services.Interface
/// <summary>
/// 人员上次成绩(按人员序列号批量查询)
/// </summary>
Task<List<DeviceSyncLastGradeItemDto>> LastGradeAsync(DeviceSyncLastGradeRequestDto dto);
Task<List<DeviceSyncUserDto>> LastGradeAsync(DeviceSyncLastGradeRequestDto dto);
/// <summary>
/// 查询运动记录
@ -72,5 +72,10 @@ namespace YD_YiLu.Api.Services.Interface
/// 获取人员详情(壹动站协议)
/// </summary>
Task<DeviceSyncUserDto> GetUserDetailAsync(string deviceSerial, int userId);
/// <summary>
/// 视频回放上传
/// </summary>
Task<bool> DeviceVideoUploadAsync(DeviceVideoUploadRequestDto dto);
}
}

View File

@ -0,0 +1,126 @@
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace YD_YiLu.Api.SmartSportsEntitys
{
/// <summary>
/// 应用版本表(设备升级控制)
/// </summary>
[Table("YL_AppVersion")]
public class YL_AppVersion
{
/// <summary>
/// 主键ID
/// </summary>
[Key]
public long Id { get; set; }
/// <summary>
/// 应用名称(壹动站 / 壹体机等)
/// </summary>
[Required]
[MaxLength(50)]
public string ApplyName { get; set; }
/// <summary>
/// 设备类型
/// </summary>
public int DeviceType { get; set; }
/// <summary>
/// 硬件类型(如 LZ)
/// </summary>
[MaxLength(20)]
public string HardwareType { get; set; }
/// <summary>
/// 操作系统(android)
/// </summary>
[MaxLength(20)]
public string OperatingSystem { get; set; }
/// <summary>
/// 版本号(如 1.0.1)
/// </summary>
[Required]
[MaxLength(20)]
public string VersionNumber { get; set; }
/// <summary>
/// 版本Code(用于排序,安卓强烈建议使用)
/// </summary>
public int VersionCode { get; set; }
/// <summary>
/// 是否强制更新 0=否 1=是
/// </summary>
public int NeedForcedUpdating { get; set; }
/// <summary>
/// 更新包下载地址
/// </summary>
[Required]
[MaxLength(500)]
public string UpdateUrl { get; set; }
/// <summary>
/// 是否为最新版本 0=否 1=是
/// </summary>
public int IsLatest { get; set; }
/// <summary>
/// 发布状态 0=未发布 1=已发布
/// </summary>
public int PublishStatus { get; set; }
/// <summary>
/// 最小支持版本(低于该版本必须升级)
/// </summary>
[MaxLength(20)]
public string? MinSupportVersion { get; set; }
/// <summary>
/// 更新说明
/// </summary>
[MaxLength(500)]
public string? UpdateContent { get; set; }
/// <summary>
/// 下载包大小(KB)
/// </summary>
public long? FileSize { get; set; }
/// <summary>
/// 下载MD5(用于校验)
/// </summary>
[MaxLength(64)]
public string? FileMd5 { get; set; }
/// <summary>
/// 灰度发布比例(0-100)
/// </summary>
public int GrayPercent { get; set; }
/// <summary>
/// 状态 0=禁用 1=启用
/// </summary>
public int Status { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreateTime { get; set; }
/// <summary>
/// 更新时间
/// </summary>
public DateTime? UpdateTime { get; set; }
/// <summary>
/// 备注
/// </summary>
[MaxLength(500)]
public string? Remark { get; set; }
}
}

View File

@ -0,0 +1,81 @@
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace YD_YiLu.Api.SmartSportsEntitys
{
/// <summary>
/// 视频回放表(训练视频上传)
/// </summary>
public class YL_DeviceVideo
{
/// <summary>
/// 主键ID
/// </summary>
[Key]
public long Id { get; set; }
/// <summary>
/// 设备序列号
/// </summary>
[Required]
[MaxLength(64)]
public string DeviceSerial { get; set; }
/// <summary>
/// 设备类型
/// </summary>
public int DeviceType { get; set; }
/// <summary>
/// 训练序列号(一次训练唯一标识)
/// </summary>
[Required]
[MaxLength(64)]
public string VideoSerial { get; set; }
/// <summary>
/// 视频访问地址(上传后返回)
/// </summary>
[Required]
[MaxLength(500)]
public string VideoUrl { get; set; }
/// <summary>
/// 文件名称
/// </summary>
[MaxLength(200)]
public string? FileName { get; set; }
/// <summary>
/// 文件大小(KB)
/// </summary>
public long? FileSize { get; set; }
/// <summary>
/// 视频时长(秒)
/// </summary>
public int? Duration { get; set; }
/// <summary>
/// 时间戳(客户端上传)
/// </summary>
public long Timestamp { get; set; }
/// <summary>
/// 上传时间
/// </summary>
public DateTime CreateTime { get; set; }
/// <summary>
/// 状态 0=无效 1=有效
/// </summary>
public int Status { get; set; }
/// <summary>
/// 备注
/// </summary>
[MaxLength(500)]
public string? Remark { get; set; }
}
}