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 UserEnabled(long userId) => db.Queryable() .AnyAsync(u => u.Id == userId && !u.IsDelete && u.Status == HealthUserStatus.Enabled); public async Task AdminEnabled(long userId) => await db.Queryable() .Where(u => u.Id == userId && u.AccountType == AccountTypeEnum.SuperAdmin && u.Status == StatusEnum.Enable).CountAsync() > 0; public Task> Bindings(long userId) => db.Queryable() .Where(b => b.UserId == userId).OrderBy(b => b.StartedAtMs, OrderByType.Desc).ToListAsync(); public Task> ActiveBindings(string productKey, string serialNumber) { var key = DeviceKey(productKey, serialNumber); return db.Queryable().Where(b => b.DeviceKey == key && b.EndedAtMs == null).ToListAsync(); } public async Task OwnedBinding(long userId, string id) => await db.Queryable().Where(b => b.Id == id && b.UserId == userId).FirstAsync() ?? throw new AppFriendlyException("床位绑定不存在或无权访问", null) { StatusCode = 404 }; public async Task Assign(HealthIb43Binding binding) { if (await db.Queryable().AnyAsync(b => b.ActiveSlot == binding.ActiveSlot)) throw new AppFriendlyException("该侧床位已分配,请先解绑", null) { StatusCode = 409 }; try { await db.Insertable(binding).ExecuteCommandAsync(); } catch { if (await db.Queryable().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() .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 Save(string reportId, Ib43Binding device, Ib43Report report) { if (!report.Accepted || report.Readings.Count == 0) return new(0, []); var matched = 0; var activeBeds = new List(); var key = DeviceKey(device.ProductKey, device.SerialNumber); var received = report.ReceivedAt.ToUnixTimeMilliseconds(); // 按接收时刻归属;数据库重试期间发生解绑也不转移给新用户。 var bindings = await db.Queryable().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().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().AnyAsync(o => o.ReportId == reportId && o.BindingId == binding.Id)) throw; } } return new(matched, activeBeds); } public async Task> Latest(long userId, string bindingId) { var output = new Dictionary(); foreach (var metric in new[] { "heartRate", "respiration", "presence", "snoring" }) { var query = db.Queryable().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>(row.ReadingsJson)[metric]; } return output; } public Task> History(long userId, string bindingId, long fromMs, long toMs, long? beforeId, int take) => db.Queryable().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(); }