136 lines
7.7 KiB
C#
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();
|
|
}
|