缓存
This commit is contained in:
parent
ee9116aea2
commit
d6d2020f50
Binary file not shown.
@ -3,6 +3,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
using MQTTnet;
|
||||
using MQTTnet.Client;
|
||||
using MQTTnet.Protocol;
|
||||
using System.Reflection.PortableExecutable;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
@ -11,8 +12,10 @@ using YD_AllHeartRates.Api.Context;
|
||||
using YD_AllHeartRates.Api.Entitys;
|
||||
using YD_AllHeartRates.Api.SmartSportsEntitys;
|
||||
using YD_AllHeartRates.Api.Utilities;
|
||||
using YD_AllHeartRates.Api.WebSocket;
|
||||
using YD_AllHeartRates.Commons.Dto.Mqtt;
|
||||
using YD_AllHeartRates.Commons.Dto.Student;
|
||||
using YD_AllHeartRates.Commons.MemoryCaches;
|
||||
|
||||
namespace YD_AllHeartRates.Api.Mqtt
|
||||
{
|
||||
@ -23,34 +26,38 @@ namespace YD_AllHeartRates.Api.Mqtt
|
||||
{
|
||||
private readonly IMqttClient _client;
|
||||
private readonly ILogger _log;
|
||||
private UserContext _userContext;
|
||||
private SmartSportsContext _smartSportsContext;
|
||||
private readonly UserContext _userContext;
|
||||
private readonly SmartSportsContext _smartSportsContext;
|
||||
private readonly ICaching _caching;
|
||||
|
||||
private List<StudentDto> _studentList = new List<StudentDto>();
|
||||
private readonly List<StudentDto> _studentList = new();
|
||||
private readonly List<HeartRateData> _pendingHeartRates = new();
|
||||
private readonly Dictionary<string, JumpRopeData> _jumpDailyMap = new();
|
||||
|
||||
static readonly JsonSerializerOptions _jsonOpt = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
private DateTime _lastHeartRateSaveTime = DateTime.UtcNow;
|
||||
private DateTime _lastJumpRopeSaveTime = DateTime.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
/// <param name="client"></param>
|
||||
/// <param name="log"></param>
|
||||
public MqttBackgroundService(IMqttClient client, ILogger<MqttBackgroundService> log, UserContext userContext, SmartSportsContext smartSportsContext)
|
||||
private readonly Channel<MqttMessage> _queue = Channel.CreateUnbounded<MqttMessage>();
|
||||
|
||||
static readonly JsonSerializerOptions _jsonOpt = new() { PropertyNameCaseInsensitive = true };
|
||||
|
||||
public MqttBackgroundService(
|
||||
IMqttClient client,
|
||||
ILogger<MqttBackgroundService> log,
|
||||
UserContext userContext,
|
||||
SmartSportsContext smartSportsContext,
|
||||
ICaching caching
|
||||
)
|
||||
{
|
||||
_client = client;
|
||||
_log = log;
|
||||
_userContext = userContext;
|
||||
_smartSportsContext = smartSportsContext;
|
||||
_caching = caching;
|
||||
|
||||
_studentList = (from d in _smartSportsContext.Device
|
||||
join s in _smartSportsContext.Student
|
||||
on d.StudentNo equals s.StudentNo
|
||||
join c in _smartSportsContext.Class
|
||||
on s.ClassId equals c.Id
|
||||
//where d.SchoolCode == "202501060001" && s.StudentStatus == 1
|
||||
join s in _smartSportsContext.Student on d.StudentNo equals s.StudentNo
|
||||
join c in _smartSportsContext.Class on s.ClassId equals c.Id
|
||||
where s.StudentStatus == 1
|
||||
select new StudentDto
|
||||
{
|
||||
@ -69,16 +76,8 @@ namespace YD_AllHeartRates.Api.Mqtt
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
private readonly Channel<MqttMessage> _queue = Channel.CreateUnbounded<MqttMessage>();
|
||||
|
||||
/// <summary>
|
||||
/// 消费队列
|
||||
/// </summary>
|
||||
/// <param name="stoppingToken"></param>
|
||||
/// <returns></returns>
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
// 连接 MQTT
|
||||
await _client.ConnectAsync(new MqttClientOptionsBuilder()
|
||||
.WithTcpServer(AppSettings.Mqtt.Host, AppSettings.Mqtt.Port)
|
||||
.Build(), stoppingToken);
|
||||
@ -88,33 +87,25 @@ namespace YD_AllHeartRates.Api.Mqtt
|
||||
.WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce)
|
||||
.Build(), stoppingToken);
|
||||
|
||||
_log.LogInformation("MQTT subscribed: {Topic}", AppSettings.Mqtt.Topic);
|
||||
|
||||
// 消息回调 → 入队
|
||||
_client.ApplicationMessageReceivedAsync += e =>
|
||||
{
|
||||
var msg = new MqttMessage
|
||||
{
|
||||
Topic = e.ApplicationMessage.Topic,
|
||||
Payload = e.ApplicationMessage.Payload == null
|
||||
? string.Empty
|
||||
: Encoding.UTF8.GetString(e.ApplicationMessage.Payload),
|
||||
Payload = e.ApplicationMessage.Payload == null ? string.Empty : Encoding.UTF8.GetString(e.ApplicationMessage.Payload),
|
||||
ReceivedAt = DateTime.UtcNow
|
||||
};
|
||||
return _queue.Writer.WriteAsync(msg, stoppingToken).AsTask();
|
||||
};
|
||||
|
||||
// ✅ 启动消费循环(写入数据库)
|
||||
//await foreach (var msg in _queue.Reader.ReadAllAsync(stoppingToken))
|
||||
await foreach (var batch in ReadBatchesAsync(stoppingToken))
|
||||
{
|
||||
var heartRateEntities = new List<HeartRateData>();
|
||||
var jumpRopeEntities = new List<JumpRopeData>();
|
||||
|
||||
foreach (var msg in batch) // batch 内部还是你业务消息
|
||||
foreach (var msg in batch)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(msg.Payload))
|
||||
continue;
|
||||
if (string.IsNullOrWhiteSpace(msg.Payload)) continue;
|
||||
|
||||
List<BlePayload>? list;
|
||||
try
|
||||
@ -127,59 +118,37 @@ namespace YD_AllHeartRates.Api.Mqtt
|
||||
continue;
|
||||
}
|
||||
|
||||
if (list is null || list.Count == 0)
|
||||
continue;
|
||||
if (list is null || list.Count == 0) continue;
|
||||
|
||||
var heartRateList = list.Where(x => x.BleName.Contains("GTY0")).ToList();
|
||||
var jumpRopeList = list.Where(x => x.BleName.Contains("RS207")).ToList();
|
||||
|
||||
foreach (var ble in heartRateList)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ble.RawData))
|
||||
continue;
|
||||
if (string.IsNullOrWhiteSpace(ble.RawData)) continue;
|
||||
|
||||
var student = _studentList.FirstOrDefault(x => x.HeartRateId == ble.BleName);
|
||||
if (student == null)
|
||||
continue;
|
||||
if (student == null || student.GradeId == 0 || student.ClassId == 0) continue;
|
||||
|
||||
if (student.GradeId == 0 || student.ClassId == 0)
|
||||
continue;
|
||||
var data = ParseHexData(ble.RawData);
|
||||
if (data == null) continue;
|
||||
|
||||
int len = ble.RawData.Length / 2;
|
||||
var data = new byte[len];
|
||||
try
|
||||
{
|
||||
for (int i = 0; i < len; i++)
|
||||
data[i] = Convert.ToByte(ble.RawData.Substring(i * 2, 2), 16);
|
||||
}
|
||||
catch (FormatException ex)
|
||||
{
|
||||
_log.LogWarning(ex, "rawData 非十六进制: {Raw}", ble.RawData);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2) 定位 0xCD 起始字段
|
||||
int cd = Array.IndexOf(data, (byte)0xCD);
|
||||
if (cd < 0 || data.Length < cd + 9)
|
||||
{
|
||||
_log.LogWarning("找不到 0xCD 或字段不足: {Raw}", ble.RawData);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3) 逐字段解析
|
||||
int battery = data[cd + 1]; // 电量 %
|
||||
int heartRate = data[cd + 2]; // 心率 bpm
|
||||
if (cd < 0 || data.Length < cd + 9) continue;
|
||||
|
||||
int battery = data[cd + 1];
|
||||
int heartRate = data[cd + 2];
|
||||
if (heartRate == 0) continue;
|
||||
|
||||
// 4) 写实体
|
||||
heartRateEntities.Add(new HeartRateData
|
||||
|
||||
|
||||
var entity = new HeartRateData
|
||||
{
|
||||
ScoreTime = ble.Timestamp,
|
||||
Code = ble.BleName,
|
||||
GradeId = student.GradeId,
|
||||
GradeName = student.GradeName,
|
||||
ClassId = student?.ClassId ?? 0,
|
||||
ClassId = student.ClassId,
|
||||
ClassName = student.ClassName ?? "",
|
||||
SchoolCode = student.SchoolCode ?? "",
|
||||
Sex = student.Sex,
|
||||
@ -188,92 +157,133 @@ namespace YD_AllHeartRates.Api.Mqtt
|
||||
StudentNo = student.StudentNo,
|
||||
StudentName = student.StudentName,
|
||||
Strength = (int)Math.Round(((double)heartRate / (220 - student.Age)) * 100)
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
heartRateEntities.Add(entity);
|
||||
var heartRateKey = $"heartRate:{student.StudentNo}";
|
||||
_caching.AddObject(heartRateKey, entity, 600); // 10分钟缓存
|
||||
|
||||
// 更新学校学生编号集合(Set)
|
||||
var studentSetKey = $"school:{student.SchoolCode}:students";
|
||||
RedisHelper.SAdd(studentSetKey, student.StudentNo); // 自动去重
|
||||
}
|
||||
|
||||
foreach (var ble in jumpRopeList)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ble.RawData))
|
||||
continue;
|
||||
if (string.IsNullOrWhiteSpace(ble.RawData)) continue;
|
||||
|
||||
var student = _studentList.FirstOrDefault(x => x.JumpRopeId == ble.BleName);
|
||||
if (student == null)
|
||||
continue;
|
||||
if (student == null || student.GradeId == 0 || student.ClassId == 0) continue;
|
||||
|
||||
if (student.GradeId == 0 || student.ClassId == 0)
|
||||
continue;
|
||||
var data = ParseHexData(ble.RawData);
|
||||
if (data == null) continue;
|
||||
|
||||
int len = ble.RawData.Length / 2;
|
||||
var data = new byte[len];
|
||||
try
|
||||
int mfIndex = Array.IndexOf(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 errorCount = data[mfIndex + 7];
|
||||
int battery = data[mfIndex + 9];
|
||||
|
||||
var jumpData = new JumpRopeData
|
||||
{
|
||||
for (int i = 0; i < len; i++)
|
||||
data[i] = Convert.ToByte(ble.RawData.Substring(i * 2, 2), 16);
|
||||
}
|
||||
catch (FormatException ex)
|
||||
{
|
||||
_log.LogWarning(ex, "rawData 非十六进制: {Raw}", ble.RawData);
|
||||
continue;
|
||||
}
|
||||
ScoreTime = ble.Timestamp,
|
||||
Code = ble.BleName,
|
||||
GradeId = student.GradeId,
|
||||
GradeName = student.GradeName,
|
||||
ClassId = student.ClassId,
|
||||
ClassName = student.ClassName ?? "",
|
||||
SchoolCode = student.SchoolCode ?? "",
|
||||
Sex = student.Sex,
|
||||
JumpValue = jumpCount,
|
||||
QuantityOfElectricity = battery,
|
||||
ErrorNumber = errorCount,
|
||||
StudentNo = student.StudentNo,
|
||||
StudentName = student.StudentName
|
||||
};
|
||||
|
||||
int mfIndex = -1;
|
||||
for (int i = 0; i < data.Length - 4; i++)
|
||||
{
|
||||
if (data[i] == 0xFF && data[i + 1] == 0x04 && data[i + 2] == 0xFF && data[i + 3] == 0xCF)
|
||||
{
|
||||
mfIndex = i + 4; // CF 后第一个字节是数据起始位置
|
||||
break;
|
||||
}
|
||||
}
|
||||
//if (!string.IsNullOrWhiteSpace(student.SchoolCode))
|
||||
//{
|
||||
// // WebSocket 推送
|
||||
// await _wsSender.SendToSchoolAsync(student.SchoolCode, "heartRate", heartRateEntities);
|
||||
// await _wsSender.SendToSchoolAsync(student.SchoolCode, "jumpRope", _jumpDailyMap.Values.ToList());
|
||||
//}
|
||||
|
||||
// 确保数据长度足够读取所有字段
|
||||
if (mfIndex >= 0 && data.Length > mfIndex + 5)
|
||||
string key = $"{jumpData.StudentNo}_{jumpData.ScoreTime:yyyyMMdd}";
|
||||
if (_jumpDailyMap.ContainsKey(key))
|
||||
{
|
||||
// 解析跳绳个数,低位 + 高位 << 8
|
||||
int jumpCount = data[mfIndex + 1] + (data[mfIndex + 2] << 8);
|
||||
int errorCount = data[mfIndex + 3];
|
||||
int battery = data[mfIndex + 5];
|
||||
|
||||
// 添加跳绳数据实体
|
||||
jumpRopeEntities.Add(new JumpRopeData
|
||||
{
|
||||
ScoreTime = ble.Timestamp,
|
||||
Code = ble.BleName,
|
||||
GradeId = student.GradeId,
|
||||
GradeName = student.GradeName,
|
||||
ClassId = student?.ClassId ?? 0,
|
||||
ClassName = student.ClassName ?? "",
|
||||
SchoolCode = student.SchoolCode ?? "",
|
||||
Sex = student.Sex,
|
||||
JumpValue = jumpCount,
|
||||
QuantityOfElectricity = battery,
|
||||
ErrorNumber = errorCount,
|
||||
StudentNo = student.StudentNo,
|
||||
StudentName = student.StudentName
|
||||
});
|
||||
_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, 600);
|
||||
|
||||
RedisHelper.SAdd(studentSetKey, student.StudentNo);
|
||||
}
|
||||
}
|
||||
|
||||
// 心率
|
||||
if (heartRateEntities.Count > 0)
|
||||
_userContext.HeartRateData.AddRange(heartRateEntities);
|
||||
// 心率每分钟保存一次
|
||||
if ((DateTime.UtcNow - _lastHeartRateSaveTime).TotalSeconds >= 60 && _pendingHeartRates.Any())
|
||||
{
|
||||
_userContext.HeartRateData.AddRange(_pendingHeartRates);
|
||||
await _userContext.SaveChangesAsync();
|
||||
_pendingHeartRates.Clear();
|
||||
_lastHeartRateSaveTime = DateTime.UtcNow;
|
||||
}
|
||||
_pendingHeartRates.AddRange(heartRateEntities);
|
||||
|
||||
// 跳绳
|
||||
if (jumpRopeEntities.Count > 0)
|
||||
_userContext.JumpRopeData.AddRange(jumpRopeEntities);
|
||||
|
||||
await _userContext.SaveChangesAsync();
|
||||
// 跳绳每日更新保存
|
||||
if ((DateTime.UtcNow - _lastJumpRopeSaveTime).TotalSeconds >= 60 && _jumpDailyMap.Any())
|
||||
{
|
||||
foreach (var data in _jumpDailyMap.Values)
|
||||
{
|
||||
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 _userContext.SaveChangesAsync();
|
||||
_lastJumpRopeSaveTime = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private byte[]? ParseHexData(string hex)
|
||||
{
|
||||
try
|
||||
{
|
||||
int len = hex.Length / 2;
|
||||
var bytes = new byte[len];
|
||||
for (int i = 0; i < len; i++)
|
||||
bytes[i] = Convert.ToByte(hex.Substring(i * 2, 2), 16);
|
||||
return bytes;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async IAsyncEnumerable<List<MqttMessage>> ReadBatchesAsync(
|
||||
[EnumeratorCancellation] CancellationToken ct)
|
||||
[EnumeratorCancellation] CancellationToken ct)
|
||||
{
|
||||
var buffer = new List<MqttMessage>(AppSettings.Mqtt.BatchSize);
|
||||
|
||||
while (await _queue.Reader.WaitToReadAsync(ct))
|
||||
{
|
||||
while (_queue.Reader.TryRead(out var m))
|
||||
@ -286,7 +296,6 @@ namespace YD_AllHeartRates.Api.Mqtt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (buffer.Count > 0) yield return buffer;
|
||||
}
|
||||
}
|
||||
|
@ -2,11 +2,14 @@
|
||||
using AutoMapper;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Drawing;
|
||||
using System.Reflection.PortableExecutable;
|
||||
using YD_AllHeartRates.Api.Context;
|
||||
using YD_AllHeartRates.Api.Entitys;
|
||||
using YD_AllHeartRates.Api.Services.Interface;
|
||||
using YD_AllHeartRates.Api.SmartSportsEntitys;
|
||||
using YD_AllHeartRates.Api.Utilities;
|
||||
using YD_AllHeartRates.Commons.Dto.LargeScreen;
|
||||
using YD_AllHeartRates.Commons.MemoryCaches;
|
||||
|
||||
namespace YD_AllHeartRates.Api.Services.Impl
|
||||
{
|
||||
@ -19,15 +22,18 @@ namespace YD_AllHeartRates.Api.Services.Impl
|
||||
public UserContext _userContext;
|
||||
private readonly LoginContext _loginContext;
|
||||
private string schoolCode;
|
||||
private readonly ICaching _caching;
|
||||
|
||||
/// <summary>
|
||||
/// 构造
|
||||
/// </summary>
|
||||
public LargeScreenService(SmartSportsContext sportsContext, UserContext userContext, LoginContext loginContext)
|
||||
public LargeScreenService(SmartSportsContext sportsContext, UserContext userContext, LoginContext loginContext, ICaching caching)
|
||||
{
|
||||
_sportsContext = sportsContext;
|
||||
_userContext = userContext;
|
||||
_loginContext = loginContext;
|
||||
_caching = caching;
|
||||
|
||||
|
||||
schoolCode = _loginContext.SchoolCode;
|
||||
}
|
||||
@ -138,46 +144,78 @@ namespace YD_AllHeartRates.Api.Services.Impl
|
||||
{
|
||||
var res = new HeartRateDataDto();
|
||||
|
||||
var tenMinutesAgo = DateTime.Now.AddMinutes(-1);
|
||||
var now = DateTime.Now;
|
||||
var tenMinutesAgo = now.AddMinutes(-10);
|
||||
|
||||
var studentList = await _sportsContext.Student.Where(x => x.ClassId == classId && x.SchoolCode == schoolCode).ToListAsync();
|
||||
// 班级学生列表
|
||||
var studentList = await _sportsContext.Student
|
||||
.Where(x => x.ClassId == classId && x.SchoolCode == schoolCode && x.StudentStatus == 1)
|
||||
.ToListAsync();
|
||||
|
||||
var heartRateData = await _userContext.HeartRateData
|
||||
.Where(x => x.SchoolCode == schoolCode && x.ScoreTime >= tenMinutesAgo)
|
||||
.GroupBy(x => x.StudentNo)
|
||||
.Select(g => g.OrderByDescending(x => x.ScoreTime).FirstOrDefault())
|
||||
.ToListAsync();
|
||||
|
||||
var jumpingRopeData = await _userContext.JumpRopeData
|
||||
.Where(x => x.SchoolCode == schoolCode && x.ScoreTime >= tenMinutesAgo)
|
||||
.GroupBy(x => x.StudentNo)
|
||||
.Select(g => g.OrderByDescending(x => x.ScoreTime).FirstOrDefault())
|
||||
.ToListAsync();
|
||||
|
||||
res.WarmUp = studentList.Count == 0 ? 0 : (int)Math.Round(heartRateData.Count(x => x.Strength < 50) * 100.0 / studentList.Count);
|
||||
res.Low = studentList.Count == 0 ? 0 : (int)Math.Round(heartRateData.Count(x => x.Strength >= 50 && x.Strength < 60) * 100.0 / studentList.Count);
|
||||
res.Medium = studentList.Count == 0 ? 0 : (int)Math.Round(heartRateData.Count(x => x.Strength >= 60 && x.Strength < 70) * 100.0 / studentList.Count);
|
||||
res.High = studentList.Count == 0 ? 0 : (int)Math.Round(heartRateData.Count(x => x.Strength >= 70 && x.Strength < 85) * 100.0 / studentList.Count);
|
||||
res.Warning = studentList.Count == 0 ? 0 : (int)Math.Round(heartRateData.Count(x => x.Strength >= 85) * 100.0 / studentList.Count);
|
||||
int warmUp = 0, low = 0, medium = 0, high = 0, warning = 0;
|
||||
|
||||
foreach (var student in studentList)
|
||||
{
|
||||
var heartRate = heartRateData.FirstOrDefault(x => x.StudentNo == student.StudentNo);
|
||||
var jumpingRope = jumpingRopeData.FirstOrDefault(x => x.StudentNo == student.StudentNo);
|
||||
var heartRateKey = $"heartRate:{student.StudentNo}";
|
||||
var jumpRopeKey = $"jumpRope:{student.StudentNo}";
|
||||
|
||||
res.StudentList.Add(new StudentDto()
|
||||
// 先从缓存拿
|
||||
var heartRate = _caching.Get<HeartRateData>(heartRateKey);
|
||||
var jumpRope = _caching.Get<JumpRopeData>(jumpRopeKey);
|
||||
|
||||
// ❗心率缓存未命中 → 单独查数据库
|
||||
if (heartRate == null)
|
||||
{
|
||||
heartRate = await _userContext.HeartRateData
|
||||
.Where(x => x.StudentNo == student.StudentNo && x.ScoreTime >= tenMinutesAgo)
|
||||
.OrderByDescending(x => x.ScoreTime)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (heartRate != null)
|
||||
_caching.AddObject(heartRateKey, heartRate, 600);
|
||||
}
|
||||
|
||||
// ❗跳绳缓存未命中 → 单独查数据库
|
||||
if (jumpRope == null)
|
||||
{
|
||||
jumpRope = await _userContext.JumpRopeData
|
||||
.Where(x => x.StudentNo == student.StudentNo && x.ScoreTime.Date == now.Date)
|
||||
.OrderByDescending(x => x.ScoreTime)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (jumpRope != null)
|
||||
_caching.AddObject(jumpRopeKey, jumpRope, 600);
|
||||
}
|
||||
|
||||
// 心率强度判断
|
||||
int strength = heartRate?.Strength ?? 0;
|
||||
if (strength < 50) warmUp++;
|
||||
else if (strength < 60) low++;
|
||||
else if (strength < 70) medium++;
|
||||
else if (strength < 85) high++;
|
||||
else warning++;
|
||||
|
||||
res.StudentList.Add(new StudentDto
|
||||
{
|
||||
StudentName = student.StudentName,
|
||||
StudentNo = student?.StudentNo ?? "",
|
||||
Photo = student?.Photo ?? "",
|
||||
StudentNo = student.StudentNo,
|
||||
Photo = student.Photo ?? "",
|
||||
Sex = student.Sex,
|
||||
HeartRate = heartRate?.Value ?? 0,
|
||||
JumpingRope = jumpingRope?.JumpValue ?? 0,
|
||||
Strength = heartRate?.Strength ?? 0
|
||||
JumpingRope = jumpRope?.JumpValue ?? 0,
|
||||
Strength = strength
|
||||
});
|
||||
}
|
||||
|
||||
int total = studentList.Count == 0 ? 1 : studentList.Count;
|
||||
res.WarmUp = (int)Math.Round(warmUp * 100.0 / total);
|
||||
res.Low = (int)Math.Round(low * 100.0 / total);
|
||||
res.Medium = (int)Math.Round(medium * 100.0 / total);
|
||||
res.High = (int)Math.Round(high * 100.0 / total);
|
||||
res.Warning = (int)Math.Round(warning * 100.0 / total);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -66,7 +66,7 @@ namespace YD_AllHeartRates.Api.Services.Impl
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
//GTY0|RS207
|
||||
/// <summary>
|
||||
/// 获取个人信息
|
||||
/// </summary>
|
||||
|
@ -27,6 +27,7 @@ using MQTTnet;
|
||||
using YD_AllHeartRates.Api.Mqtt;
|
||||
using YD_AllHeartRates.Commons.MemoryCaches;
|
||||
using YD_AllHeartRates.Api.Middlewares;
|
||||
using YD_AllHeartRates.Api.WebSocket;
|
||||
|
||||
namespace YD_AllHeartRates.Api
|
||||
{
|
||||
@ -235,13 +236,14 @@ namespace YD_AllHeartRates.Api
|
||||
"https://*/404";
|
||||
});
|
||||
services.AddSignalR();
|
||||
services.AddSingleton<IWebSocketSender, SignalRWebSocketSender>();
|
||||
|
||||
services.AddHttpClient();
|
||||
|
||||
services.AddAutoMapper(typeof(MappingProfile));
|
||||
|
||||
services.AddLogging();
|
||||
|
||||
//设置文件上传大小限制
|
||||
//设置文件上传大小限制
|
||||
services.Configure<FormOptions>(x =>
|
||||
{
|
||||
|
25
YD_AllHeartRates.Api/WebSocket/IWebSocketSender.cs
Normal file
25
YD_AllHeartRates.Api/WebSocket/IWebSocketSender.cs
Normal file
@ -0,0 +1,25 @@
|
||||
namespace YD_AllHeartRates.Api.WebSocket
|
||||
{
|
||||
/// <summary>
|
||||
/// WebSocket
|
||||
/// </summary>
|
||||
public interface IWebSocketSender
|
||||
{
|
||||
/// <summary>
|
||||
/// 消息发送
|
||||
/// </summary>
|
||||
/// <param name="channel"></param>
|
||||
/// <param name="data"></param>
|
||||
/// <returns></returns>
|
||||
Task SendAsync(string channel, object data);
|
||||
|
||||
/// <summary>
|
||||
/// 根据学校发送消息
|
||||
/// </summary>
|
||||
/// <param name="schoolCode"></param>
|
||||
/// <param name="channel"></param>
|
||||
/// <param name="data"></param>
|
||||
/// <returns></returns>
|
||||
Task SendToSchoolAsync(string schoolCode, string channel, object data);
|
||||
}
|
||||
}
|
50
YD_AllHeartRates.Api/WebSocket/RealtimeHub.cs
Normal file
50
YD_AllHeartRates.Api/WebSocket/RealtimeHub.cs
Normal file
@ -0,0 +1,50 @@
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace YD_AllHeartRates.Api.WebSocket
|
||||
{
|
||||
/// <summary>
|
||||
/// RealtimeHub
|
||||
/// </summary>
|
||||
public class RealtimeHub : Hub
|
||||
{
|
||||
/// <summary>
|
||||
/// 客户端连接时调用(可选)
|
||||
/// </summary>
|
||||
public override async Task OnConnectedAsync()
|
||||
{
|
||||
var connectionId = Context.ConnectionId;
|
||||
Console.WriteLine($"客户端连接:{connectionId}");
|
||||
|
||||
await base.OnConnectedAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 客户端断开连接时调用(可选)
|
||||
/// </summary>
|
||||
public override async Task OnDisconnectedAsync(Exception? exception)
|
||||
{
|
||||
var connectionId = Context.ConnectionId;
|
||||
Console.WriteLine($"客户端断开连接:{connectionId}");
|
||||
|
||||
await base.OnDisconnectedAsync(exception);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 客户端可调用的方法:加入指定分组(如按学校、班级)
|
||||
/// </summary>
|
||||
public async Task JoinGroup(string groupName)
|
||||
{
|
||||
await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
|
||||
Console.WriteLine($"加入分组:{groupName}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 客户端可调用的方法:离开指定分组
|
||||
/// </summary>
|
||||
public async Task LeaveGroup(string groupName)
|
||||
{
|
||||
await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName);
|
||||
Console.WriteLine($"离开分组:{groupName}");
|
||||
}
|
||||
}
|
||||
}
|
45
YD_AllHeartRates.Api/WebSocket/SignalRWebSocketSender.cs
Normal file
45
YD_AllHeartRates.Api/WebSocket/SignalRWebSocketSender.cs
Normal file
@ -0,0 +1,45 @@
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace YD_AllHeartRates.Api.WebSocket
|
||||
{
|
||||
/// <summary>
|
||||
/// 发送SignalRWebSocket
|
||||
/// </summary>
|
||||
public class SignalRWebSocketSender : IWebSocketSender
|
||||
{
|
||||
private readonly IHubContext<RealtimeHub> _hubContext;
|
||||
|
||||
/// <summary>
|
||||
/// hubContext
|
||||
/// </summary>
|
||||
/// <param name="hubContext"></param>
|
||||
public SignalRWebSocketSender(IHubContext<RealtimeHub> hubContext)
|
||||
{
|
||||
_hubContext = hubContext;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 消息发送
|
||||
/// </summary>
|
||||
/// <param name="channel"></param>
|
||||
/// <param name="data"></param>
|
||||
/// <returns></returns>
|
||||
public async Task SendAsync(string channel, object data)
|
||||
{
|
||||
await _hubContext.Clients.All.SendAsync(channel, data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据学校发送消息
|
||||
/// </summary>
|
||||
/// <param name="schoolCode"></param>
|
||||
/// <param name="channel"></param>
|
||||
/// <param name="data"></param>
|
||||
/// <returns></returns>
|
||||
public async Task SendToSchoolAsync(string schoolCode, string channel, object data)
|
||||
{
|
||||
string groupName = $"school_{schoolCode}";
|
||||
await _hubContext.Clients.Group(groupName).SendAsync(channel, data);
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
Binary file not shown.
@ -1 +1 @@
|
||||
kpAOGOwQr+PqcqSkET2ftxen78nx6+ufMJyhzNQMesU=WWmIfOUbyYPYdKFHlVpzo+vbEnfIsJNTTcG8+oWdtSc=
|
||||
OZ2FMHhzcKLaTsTH79CqMYu8/tuIh7oBcr70vB907z4=WWmIfOUbyYPYdKFHlVpzo+vbEnfIsJNTTcG8+oWdtSc=
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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+c65f0d665eff3d83c1cdb0eaf581369baecbe26a")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+ee9116aea22701bf6d184657d96c4e24b2bc0095")]
|
||||
[assembly: System.Reflection.AssemblyProductAttribute("YD_AllHeartRates.Api")]
|
||||
[assembly: System.Reflection.AssemblyTitleAttribute("YD_AllHeartRates.Api")]
|
||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||
|
@ -1 +1 @@
|
||||
0edc1169727cbac8f4192a409031e62e7df5ab080dd285125c8d176be132b106
|
||||
1c24cdf25780b49d3cd275d101f9545d4bc591310051a502fe2c6f782554626d
|
||||
|
Binary file not shown.
@ -1 +1 @@
|
||||
e1c1286248133e4f873378c81bc1cba408058a1c11789d81f9a4f37908ac6243
|
||||
be24640c820a322f6ed07b42336b9100bbed9b708418756cbe716ef9eb7afbd3
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -426,20 +426,6 @@
|
||||
接受写入数据
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:YD_AllHeartRates.Api.Mqtt.MqttBackgroundService.#ctor(MQTTnet.Client.IMqttClient,Microsoft.Extensions.Logging.ILogger{YD_AllHeartRates.Api.Mqtt.MqttBackgroundService},YD_AllHeartRates.Api.Context.UserContext,YD_AllHeartRates.Api.Context.SmartSportsContext)">
|
||||
<summary>
|
||||
构造函数
|
||||
</summary>
|
||||
<param name="client"></param>
|
||||
<param name="log"></param>
|
||||
</member>
|
||||
<member name="M:YD_AllHeartRates.Api.Mqtt.MqttBackgroundService.ExecuteAsync(System.Threading.CancellationToken)">
|
||||
<summary>
|
||||
消费队列
|
||||
</summary>
|
||||
<param name="stoppingToken"></param>
|
||||
<returns></returns>
|
||||
</member>
|
||||
<member name="T:YD_AllHeartRates.Api.Services.Impl.DeviceService">
|
||||
<summary>
|
||||
服务实现
|
||||
@ -533,7 +519,7 @@
|
||||
服务实现
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:YD_AllHeartRates.Api.Services.Impl.LargeScreenService.#ctor(YD_AllHeartRates.Api.Context.SmartSportsContext,YD_AllHeartRates.Api.Context.UserContext,YD_AllHeartRates.Api.Utilities.LoginContext)">
|
||||
<member name="M:YD_AllHeartRates.Api.Services.Impl.LargeScreenService.#ctor(YD_AllHeartRates.Api.Context.SmartSportsContext,YD_AllHeartRates.Api.Context.UserContext,YD_AllHeartRates.Api.Utilities.LoginContext,YD_AllHeartRates.Commons.MemoryCaches.ICaching)">
|
||||
<summary>
|
||||
构造
|
||||
</summary>
|
||||
@ -1385,5 +1371,80 @@
|
||||
每个UserContext的属性至多读取一次redis或Memory缓存从而提高查询效率
|
||||
</summary>
|
||||
</member>
|
||||
<member name="T:YD_AllHeartRates.Api.WebSocket.IWebSocketSender">
|
||||
<summary>
|
||||
WebSocket
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:YD_AllHeartRates.Api.WebSocket.IWebSocketSender.SendAsync(System.String,System.Object)">
|
||||
<summary>
|
||||
消息发送
|
||||
</summary>
|
||||
<param name="channel"></param>
|
||||
<param name="data"></param>
|
||||
<returns></returns>
|
||||
</member>
|
||||
<member name="M:YD_AllHeartRates.Api.WebSocket.IWebSocketSender.SendToSchoolAsync(System.String,System.String,System.Object)">
|
||||
<summary>
|
||||
根据学校发送消息
|
||||
</summary>
|
||||
<param name="schoolCode"></param>
|
||||
<param name="channel"></param>
|
||||
<param name="data"></param>
|
||||
<returns></returns>
|
||||
</member>
|
||||
<member name="T:YD_AllHeartRates.Api.WebSocket.RealtimeHub">
|
||||
<summary>
|
||||
RealtimeHub
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:YD_AllHeartRates.Api.WebSocket.RealtimeHub.OnConnectedAsync">
|
||||
<summary>
|
||||
客户端连接时调用(可选)
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:YD_AllHeartRates.Api.WebSocket.RealtimeHub.OnDisconnectedAsync(System.Exception)">
|
||||
<summary>
|
||||
客户端断开连接时调用(可选)
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:YD_AllHeartRates.Api.WebSocket.RealtimeHub.JoinGroup(System.String)">
|
||||
<summary>
|
||||
客户端可调用的方法:加入指定分组(如按学校、班级)
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:YD_AllHeartRates.Api.WebSocket.RealtimeHub.LeaveGroup(System.String)">
|
||||
<summary>
|
||||
客户端可调用的方法:离开指定分组
|
||||
</summary>
|
||||
</member>
|
||||
<member name="T:YD_AllHeartRates.Api.WebSocket.SignalRWebSocketSender">
|
||||
<summary>
|
||||
发送SignalRWebSocket
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:YD_AllHeartRates.Api.WebSocket.SignalRWebSocketSender.#ctor(Microsoft.AspNetCore.SignalR.IHubContext{YD_AllHeartRates.Api.WebSocket.RealtimeHub})">
|
||||
<summary>
|
||||
hubContext
|
||||
</summary>
|
||||
<param name="hubContext"></param>
|
||||
</member>
|
||||
<member name="M:YD_AllHeartRates.Api.WebSocket.SignalRWebSocketSender.SendAsync(System.String,System.Object)">
|
||||
<summary>
|
||||
消息发送
|
||||
</summary>
|
||||
<param name="channel"></param>
|
||||
<param name="data"></param>
|
||||
<returns></returns>
|
||||
</member>
|
||||
<member name="M:YD_AllHeartRates.Api.WebSocket.SignalRWebSocketSender.SendToSchoolAsync(System.String,System.String,System.Object)">
|
||||
<summary>
|
||||
根据学校发送消息
|
||||
</summary>
|
||||
<param name="schoolCode"></param>
|
||||
<param name="channel"></param>
|
||||
<param name="data"></param>
|
||||
<returns></returns>
|
||||
</member>
|
||||
</members>
|
||||
</doc>
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -14,7 +14,7 @@ using System.Reflection;
|
||||
[assembly: System.Reflection.AssemblyCompanyAttribute("YD_AllHeartRates.Commons")]
|
||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+580d586b2d20254267b1182a8ceff393c252da81")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+ee9116aea22701bf6d184657d96c4e24b2bc0095")]
|
||||
[assembly: System.Reflection.AssemblyProductAttribute("YD_AllHeartRates.Commons")]
|
||||
[assembly: System.Reflection.AssemblyTitleAttribute("YD_AllHeartRates.Commons")]
|
||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||
|
@ -1 +1 @@
|
||||
35d58a7d90dcca1c52be85a8d3e3e2c8fc425c6c7ec2830346ca890579682685
|
||||
ad77ca7765704675fc50e971094eeabaa926244d017d6f5a36917f704f444668
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user