This commit is contained in:
tanglong 2025-07-15 17:57:47 +08:00
parent 43f076f872
commit d73d342ab0
10 changed files with 154 additions and 57 deletions

View File

@ -95,7 +95,7 @@ namespace YD_AllHeartRates.Api.Entitys
[Display(Name = "错误个数")]
[Comment("错误个数")]
[Column(TypeName = "int")]
public int? ErrorNumber { get; set; }
public int ErrorNumber { get; set; }
/// <summary>
///正确个数[敏捷:正确个数]
@ -103,7 +103,7 @@ namespace YD_AllHeartRates.Api.Entitys
[Display(Name = "正确个数")]
[Comment("正确个数")]
[Column(TypeName = "int")]
public int? JumpValue { get; set; }
public int JumpValue { get; set; }
/// <summary>
///电量

View File

@ -29,13 +29,13 @@ namespace YD_AllHeartRates.Api.Mqtt
private readonly UserContext _userContext;
private readonly SmartSportsContext _smartSportsContext;
private readonly ICaching _caching;
private readonly IServiceScopeFactory _scopeFactory;
private readonly List<StudentDto> _studentList = new();
private readonly List<HeartRateData> _pendingHeartRates = new();
private readonly Dictionary<string, JumpRopeData> _jumpDailyMap = new();
private DateTime _lastHeartRateSaveTime = DateTime.UtcNow;
private DateTime _lastJumpRopeSaveTime = DateTime.UtcNow;
private DateTime _lastHeartRateSaveTime = DateTime.Now;
private DateTime _lastJumpRopeSaveTime = DateTime.Now;
private readonly Channel<MqttMessage> _queue = Channel.CreateUnbounded<MqttMessage>();
@ -46,7 +46,8 @@ namespace YD_AllHeartRates.Api.Mqtt
ILogger<MqttBackgroundService> log,
UserContext userContext,
SmartSportsContext smartSportsContext,
ICaching caching
ICaching caching,
IServiceScopeFactory scopeFactory
)
{
_client = client;
@ -54,6 +55,7 @@ namespace YD_AllHeartRates.Api.Mqtt
_userContext = userContext;
_smartSportsContext = smartSportsContext;
_caching = caching;
_scopeFactory = scopeFactory;
_studentList = (from d in _smartSportsContext.Device
join s in _smartSportsContext.Student on d.StudentNo equals s.StudentNo
@ -93,7 +95,7 @@ namespace YD_AllHeartRates.Api.Mqtt
{
Topic = e.ApplicationMessage.Topic,
Payload = e.ApplicationMessage.Payload == null ? string.Empty : Encoding.UTF8.GetString(e.ApplicationMessage.Payload),
ReceivedAt = DateTime.UtcNow
ReceivedAt = DateTime.Now
};
return _queue.Writer.WriteAsync(msg, stoppingToken).AsTask();
};
@ -172,17 +174,17 @@ namespace YD_AllHeartRates.Api.Mqtt
if (string.IsNullOrWhiteSpace(ble.RawData)) continue;
var student = _studentList.FirstOrDefault(x => x.JumpRopeId == ble.BleName);
if (student == null || student.GradeId == 0 || student.ClassId == 0) continue;
if (student == null || student?.GradeId == 0 || student?.ClassId == 0) continue;
var data = ParseHexData(ble.RawData);
if (data == null) continue;
int mfIndex = Array.IndexOf(data, new byte[] { 0xFF, 0x04, 0xFF, 0xCF });
int mfIndex = IndexOfSequence(data, new byte[] { 0xFF, 0x04, 0xFF, 0xCF });
if (mfIndex < 0 || data.Length < mfIndex + 10) continue;
int jumpCount = data[mfIndex + 5] + (data[mfIndex + 6] << 8);
int jumpCount = data[mfIndex + 5];
int errorCount = data[mfIndex + 7];
int battery = data[mfIndex + 9];
int battery = data[mfIndex + 10];
var jumpData = new JumpRopeData
{
@ -208,61 +210,89 @@ namespace YD_AllHeartRates.Api.Mqtt
// await _wsSender.SendToSchoolAsync(student.SchoolCode, "jumpRope", _jumpDailyMap.Values.ToList());
//}
string key = $"{jumpData.StudentNo}_{jumpData.ScoreTime:yyyyMMdd}";
if (_jumpDailyMap.ContainsKey(key))
{
_jumpDailyMap[key].JumpValue += jumpData.JumpValue;
_jumpDailyMap[key].ErrorNumber += jumpData.ErrorNumber;
_jumpDailyMap[key].QuantityOfElectricity = jumpData.QuantityOfElectricity;
_jumpDailyMap[key].ScoreTime = jumpData.ScoreTime;
}
else
{
_jumpDailyMap[key] = jumpData;
}
var studentSetKey = $"school:{student.SchoolCode}:students";
var jumpKey = $"jumpRope:{student.StudentNo}";
_caching.AddObject(jumpKey, jumpData, 60);
var jumpKey = $"jumpRope:active:{student.StudentNo}:{DateTime.Now:yyyyMMdd}";
var rawKey = $"jumpRope:raw:{student.StudentNo}:{DateTime.Now:yyyyMMdd}";
RedisHelper.SAdd(studentSetKey, student.StudentNo);
// 当前缓存的总值(当天跳绳数据)
var totalData = _caching.Get<JumpRopeData>(jumpKey);
// 初始化总值(首次记录)
if (totalData == null)
{
totalData = new JumpRopeData
{
StudentNo = jumpData.StudentNo,
JumpValue = 0,
ErrorNumber = 0,
QuantityOfElectricity = jumpData.QuantityOfElectricity,
ScoreTime = jumpData.ScoreTime
};
}
// 上一条原始累计值(用于计算差值)
var lastRaw = _caching.Get<JumpRopeData>(rawKey);
// 差值计算
int deltaJump = jumpData.JumpValue;
int deltaError = jumpData.ErrorNumber;
if (lastRaw != null)
{
deltaJump = jumpData.JumpValue >= lastRaw.JumpValue
? jumpData.JumpValue - lastRaw.JumpValue
: jumpData.JumpValue;
deltaError = jumpData.ErrorNumber >= lastRaw.ErrorNumber
? jumpData.ErrorNumber - lastRaw.ErrorNumber
: jumpData.ErrorNumber;
}
// 汇总
totalData.JumpValue += deltaJump;
totalData.ErrorNumber += deltaError;
totalData.QuantityOfElectricity = jumpData.QuantityOfElectricity;
totalData.ScoreTime = jumpData.ScoreTime;
// 写入缓存(总值 + 原始值)
_caching.AddObject(jumpKey, totalData, 60 * 24);
_caching.AddObject(rawKey, jumpData, 60 * 24);
// 记录活跃学生编号(便于定时入库)
//RedisHelper.SAdd($"jumpRope:active:{DateTime.Now:yyyyMMdd}", student.StudentNo);
//var studentSetKey = $"school:{student.SchoolCode}:students";
//var jumpKey = $"jumpRope:{student.StudentNo}";
//_caching.AddObject(jumpKey, jumpData, 60);
//RedisHelper.SAdd(studentSetKey, student.StudentNo);
}
}
// 心率每分钟保存一次
if ((DateTime.UtcNow - _lastHeartRateSaveTime).TotalSeconds >= 60 && _pendingHeartRates.Any())
if ((DateTime.Now - _lastHeartRateSaveTime).TotalSeconds >= 60 && _pendingHeartRates.Any())
{
_userContext.HeartRateData.AddRange(_pendingHeartRates);
await _userContext.SaveChangesAsync();
_pendingHeartRates.Clear();
_lastHeartRateSaveTime = DateTime.UtcNow;
_lastHeartRateSaveTime = DateTime.Now;
}
_pendingHeartRates.AddRange(heartRateEntities);
// 跳绳每日更新保存
if ((DateTime.UtcNow - _lastJumpRopeSaveTime).TotalSeconds >= 60 && _jumpDailyMap.Any())
// 每隔 60 秒执行一次
_ = Task.Run(async () =>
{
foreach (var data in _jumpDailyMap.Values)
while (true)
{
var exist = await _userContext.JumpRopeData
.FirstOrDefaultAsync(x => x.StudentNo == data.StudentNo && x.ScoreTime.Date == data.ScoreTime.Date);
if (exist != null)
{
exist.JumpValue = data.JumpValue;
exist.ErrorNumber = data.ErrorNumber;
exist.QuantityOfElectricity = data.QuantityOfElectricity;
exist.ScoreTime = data.ScoreTime;
}
else
{
_userContext.JumpRopeData.Add(data);
}
await SyncTodayJumpDataToDbAsync();
await Task.Delay(10000); // 每 10 秒检查一次
}
await _userContext.SaveChangesAsync();
_lastJumpRopeSaveTime = DateTime.UtcNow;
}
});
}
}
private byte[]? ParseHexData(string hex)
{
try
@ -297,5 +327,75 @@ namespace YD_AllHeartRates.Api.Mqtt
}
if (buffer.Count > 0) yield return buffer;
}
public static int IndexOfSequence(byte[] buffer, byte[] pattern)
{
if (pattern.Length == 0 || buffer.Length < pattern.Length)
return -1;
for (int i = 0; i <= buffer.Length - pattern.Length; i++)
{
bool matched = true;
for (int j = 0; j < pattern.Length; j++)
{
if (buffer[i + j] != pattern[j])
{
matched = false;
break;
}
}
if (matched)
return i;
}
return -1;
}
public async Task SyncTodayJumpDataToDbAsync()
{
try
{
if ((DateTime.Now - _lastJumpRopeSaveTime).TotalSeconds < 60)
return;
var today = DateTime.Today;
var dateStr = today.ToString("yyyyMMdd");
var studentNos = RedisHelper.SMembers($"jumpRope:active:{dateStr}");
if (studentNos == null || studentNos.Length == 0)
return;
using var scope = _scopeFactory.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<UserContext>();
foreach (var studentNo in studentNos)
{
var cacheKey = $"jumpRope:active:{studentNo}:{dateStr}";
var data = _caching.Get<JumpRopeData>(cacheKey);
if (data == null || string.IsNullOrWhiteSpace(data.StudentNo)) continue;
var exist = await dbContext.JumpRopeData
.FirstOrDefaultAsync(x => x.StudentNo == data.StudentNo && x.ScoreTime.Date == today);
if (exist != null)
{
exist.JumpValue = data.JumpValue;
exist.ErrorNumber = data.ErrorNumber;
exist.QuantityOfElectricity = data.QuantityOfElectricity;
exist.ScoreTime = data.ScoreTime;
}
else
{
dbContext.JumpRopeData.Add(data);
}
}
await dbContext.SaveChangesAsync();
_lastJumpRopeSaveTime = DateTime.Now;
}
catch (Exception ex)
{
}
}
}
}

View File

@ -1,7 +1,6 @@

using AutoMapper;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using OfficeOpenXml.FormulaParsing.Excel.Functions.DateTime;
using System.Drawing;
using System.Reflection.PortableExecutable;
@ -25,7 +24,6 @@ namespace YD_AllHeartRates.Api.Services.Impl
private readonly LoginContext _loginContext;
private string schoolCode;
private readonly ICaching _caching;
private readonly Dictionary<string, JumpRopeData> _jumpDailyMap = new();
/// <summary>
/// 构造
@ -119,7 +117,7 @@ namespace YD_AllHeartRates.Api.Services.Impl
res.HeartRateOnlineCount = onlineHeartRateCount;
// 在线跳绳设备
int onlineJumpRopeCount = RedisHelper.Keys("jumpRope:*").Length;
int onlineJumpRopeCount = RedisHelper.Keys("jumpRope:active:*").Length;
res.JumpingRopeOnLineCount = onlineJumpRopeCount;
// 1. 构建缓存 key
@ -151,15 +149,14 @@ namespace YD_AllHeartRates.Api.Services.Impl
var heartRateKey = $"heartRate:{student.StudentNo}";
//var jumpRopeKey = $"jumpRope:{student.StudentNo}";
string jumpRopeKey = $"{student.StudentNo}_{DateTime.Now:yyyyMMdd}";
//string jumpRopeKey = $"{student.StudentNo}_{DateTime.Now:yyyyMMdd}";
var jumpRopeKey = $"jumpRope:active:{student.StudentNo}:{DateTime.Now:yyyyMMdd}";
// 先从缓存拿
var heartRate = _caching.Get<HeartRateData>(heartRateKey);
//var jumpRope = _caching.Get<JumpRopeData>(jumpRopeKey);
_jumpDailyMap.TryGetValue(jumpRopeKey, out var jumpRope);
var jumpRope = _caching.Get<JumpRopeData>(jumpRopeKey);
// ❗心率缓存未命中 → 单独查数据库
//if (heartRate == null)

View File

@ -14,7 +14,7 @@ using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("YD_AllHeartRates.Api")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+3c399871d7a4fff8b5d368263e3942991a974fdd")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+43f076f872b66673a4a48984e93b5af18678ab90")]
[assembly: System.Reflection.AssemblyProductAttribute("YD_AllHeartRates.Api")]
[assembly: System.Reflection.AssemblyTitleAttribute("YD_AllHeartRates.Api")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]

View File

@ -1 +1 @@
937ead42c43efa52de467b7e6b0b10b17938c2bb3b9c6b465840319e0efb56a9
b0f272ad7220d6b797997e6256efe0155a22b7fdee2e14ca711ed9643b29fa86