Files
Evergreen-Health/Evergreen.Health.Application/Health/Integrations/Ib43/Ib43DataRepository.cs
T
2026-11-12 15:45:52 +08:00

136 lines
7.7 KiB
C#

using Evergreen.Health.Application.Health.Entities;
using Furion.FriendlyException;
using SqlSugar;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
namespace Evergreen.Health.Application.Health.Integrations.Ib43;
public sealed class Ib43DataRepository(ISqlSugarClient db)
{
public static string DeviceKey(string productKey, string serialNumber) =>
Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(productKey + "/" + serialNumber)));
public Task<bool> UserEnabled(long userId) => db.Queryable<HealthUser>()
.AnyAsync(u => u.Id == userId && !u.IsDelete && u.Status == HealthUserStatus.Enabled);
public async Task<bool> AdminEnabled(long userId) => await db.Queryable<SysUser>()
.Where(u => u.Id == userId && u.AccountType == AccountTypeEnum.SuperAdmin && u.Status == StatusEnum.Enable).CountAsync() > 0;
public Task<List<HealthIb43Binding>> Bindings(long userId) => db.Queryable<HealthIb43Binding>()
.Where(b => b.UserId == userId).OrderBy(b => b.StartedAtMs, OrderByType.Desc).ToListAsync();
public Task<List<HealthIb43Binding>> ActiveBindings(string productKey, string serialNumber)
{
var key = DeviceKey(productKey, serialNumber);
return db.Queryable<HealthIb43Binding>().Where(b => b.DeviceKey == key && b.EndedAtMs == null).ToListAsync();
}
public async Task<HealthIb43Binding> OwnedBinding(long userId, string id) =>
await db.Queryable<HealthIb43Binding>().Where(b => b.Id == id && b.UserId == userId).FirstAsync()
?? throw new AppFriendlyException("床位绑定不存在或无权访问", null) { StatusCode = 404 };
public Task<List<HealthIb43AutomationExecution>> AutomationHistory(long userId, string bindingId) =>
db.Queryable<HealthIb43AutomationExecution>().Where(row => row.UserId == userId && row.BindingId == bindingId)
.OrderBy(row => row.TriggeredAtMs, OrderByType.Desc).Take(20).ToListAsync();
public async Task<string?> BeginAutomation(string bindingId, long userId, string action, string target, long triggeredAtMs, CancellationToken token)
{
token.ThrowIfCancellationRequested();
if (!await UserEnabled(userId) || !await db.Queryable<HealthIb43Binding>().AnyAsync(b => b.Id == bindingId &&
b.UserId == userId && b.Simulation && b.EndedAtMs == null && b.StartedAtMs <= triggeredAtMs)) return null;
var record = new HealthIb43AutomationExecution
{ BindingId = bindingId, UserId = userId, ActionKey = action, TargetLabel = target, TriggeredAtMs = triggeredAtMs };
token.ThrowIfCancellationRequested();
await db.Insertable(record).ExecuteCommandAsync();
return record.Id;
}
public Task<int> CompleteAutomation(string id, string result, long completedAtMs) => db.Updateable<HealthIb43AutomationExecution>()
.SetColumns(row => new HealthIb43AutomationExecution { Result = result, CompletedAtMs = completedAtMs })
.Where(row => row.Id == id && row.CompletedAtMs == null).ExecuteCommandAsync();
public async Task Assign(HealthIb43Binding binding)
{
if (await db.Queryable<HealthIb43Binding>().AnyAsync(b => b.ActiveSlot == binding.ActiveSlot))
throw new AppFriendlyException("该侧床位已分配,请先解绑", null) { StatusCode = 409 };
try { await db.Insertable(binding).ExecuteCommandAsync(); }
catch
{
if (await db.Queryable<HealthIb43Binding>().AnyAsync(b => b.ActiveSlot == binding.ActiveSlot))
throw new AppFriendlyException("该侧床位已分配,请先解绑", null) { StatusCode = 409 };
throw;
}
}
public async Task Release(string id, long adminId, long endedAtMs)
{
var count = await db.Updateable<HealthIb43Binding>()
.SetColumns(b => new HealthIb43Binding { ActiveSlot = null, EndedAtMs = endedAtMs, ReleasedBy = adminId })
.Where(b => b.Id == id && b.EndedAtMs == null).ExecuteCommandAsync();
if (count == 0) throw new AppFriendlyException("绑定不存在或已解绑", null) { StatusCode = 404 };
}
public async Task<Ib43SavedReport> Save(string reportId, Ib43Binding device, Ib43Report report)
{
if (!report.Accepted || report.Readings.Count == 0) return new(0, []);
var matched = 0;
var activeBeds = new List<Ib43SavedBed>();
var key = DeviceKey(device.ProductKey, device.SerialNumber);
var received = report.ReceivedAt.ToUnixTimeMilliseconds();
// 按接收时刻归属;数据库重试期间发生解绑也不转移给新用户。
var bindings = await db.Queryable<HealthIb43Binding>().Where(b => b.DeviceKey == key &&
b.Simulation == device.Simulation && b.StartedAtMs <= received &&
(b.EndedAtMs == null || b.EndedAtMs > received)).ToListAsync();
foreach (var binding in bindings)
{
var readings = report.Readings.Where(p => p.Key.StartsWith(binding.Side + ".", StringComparison.Ordinal))
.ToDictionary(p => p.Key[(binding.Side.Length + 1)..], p => p.Value);
if (readings.Count == 0) continue;
matched++;
if (binding.EndedAtMs == null) activeBeds.Add(new(binding.Id, binding.Side));
if (await db.Queryable<HealthIb43Observation>().AnyAsync(o => o.ReportId == reportId && o.BindingId == binding.Id)) continue;
var observation = new HealthIb43Observation
{
ReportId = reportId, BindingId = binding.Id, UserId = binding.UserId, ReceivedAtMs = received,
HasHeartRate = readings.ContainsKey("heartRate"), HasRespiration = readings.ContainsKey("respiration"),
HasPresence = readings.ContainsKey("presence"), HasSnoring = readings.ContainsKey("snoring"),
ReadingsJson = JsonSerializer.Serialize(readings)
};
try { await db.Insertable(observation).ExecuteCommandAsync(); }
catch
{
// 同一进程处理的报告重试幂等;没有设备序号,不能对 MQTT 重投宣称去重。
if (!await db.Queryable<HealthIb43Observation>().AnyAsync(o => o.ReportId == reportId && o.BindingId == binding.Id)) throw;
}
}
return new(matched, activeBeds);
}
public async Task<Dictionary<string, Ib43Reading>> Latest(long userId, string bindingId)
{
var output = new Dictionary<string, Ib43Reading>();
foreach (var metric in new[] { "heartRate", "respiration", "presence", "snoring" })
{
var query = db.Queryable<HealthIb43Observation>().Where(o => o.UserId == userId && o.BindingId == bindingId);
query = metric switch
{
"heartRate" => query.Where(o => o.HasHeartRate),
"respiration" => query.Where(o => o.HasRespiration),
"presence" => query.Where(o => o.HasPresence),
_ => query.Where(o => o.HasSnoring)
};
var row = await query.OrderBy(o => o.ReceivedAtMs, OrderByType.Desc).OrderBy(o => o.Id, OrderByType.Desc).FirstAsync();
if (row != null) output[metric] = JsonSerializer.Deserialize<Dictionary<string, Ib43Reading>>(row.ReadingsJson)[metric];
}
return output;
}
public Task<List<HealthIb43Observation>> History(long userId, string bindingId, long fromMs, long toMs, long? beforeId, int take)
=> db.Queryable<HealthIb43Observation>().Where(o => o.UserId == userId && o.BindingId == bindingId &&
o.ReceivedAtMs >= fromMs && o.ReceivedAtMs < toMs)
.WhereIF(beforeId.HasValue, o => o.Id < beforeId.Value)
.OrderBy(o => o.Id, OrderByType.Desc).Take(take).ToListAsync();
}