using Evergreen.Health.Application.Health.Entities; using Evergreen.Health.Application.Health.Models; using System.Text.Json; namespace Evergreen.Health.Application.Health.Services; // 重庆演示场景:BMI 只给出可手动修改的软硬起点,不是医学或人体工学诊断。 public sealed class MattressComfortService : ITransient { // 演示使用 Open-Meteo 接口;正式商用前应确认许可或替换为有授权的天气源。 private const string WeatherUrl = "https://api.open-meteo.com/v1/forecast?latitude=29.56&longitude=106.55¤t=temperature_2m%2Capparent_temperature&timezone=Asia%2FShanghai"; private static readonly HttpClient WeatherClient = new() { Timeout = TimeSpan.FromSeconds(6) }; private static readonly SemaphoreSlim WeatherLock = new(1, 1); private static WeatherSample? CachedWeather; public async Task GetAsync(HealthProfile? profile, CancellationToken cancellationToken = default) { decimal? bmi = null; if (profile?.HeightCm > 0 && profile.WeightKg > 0) { var height = profile.HeightCm.Value / 100m; bmi = Math.Round(profile.WeightKg.Value / (height * height), 1); } var firmness = bmi switch { < 18.5m => "很软", < 24m => "稍软", < 28m => "稍硬", < 32m => "很硬", >= 32m => "最硬", _ => null }; var chinaNow = DateTimeOffset.UtcNow.ToOffset(TimeSpan.FromHours(8)); var winter = chinaNow.Month is 12 or 1 or 2; var weather = await ReadWeatherAsync(cancellationToken); var heating = 0; if (winter && weather?.FeelsLikeCelsius is double feelsLike) heating = feelsLike < 8 ? 2 : feelsLike < 16 ? 1 : 0; var weatherReason = !winter ? "重庆非冬季,床垫加热关闭" : weather == null ? "重庆天气暂不可用,为安全起见不自动加热" : heating == 0 ? "重庆处于冬季,但当前体感温度较高,不自动加热" : $"重庆冬季体感 {weather.FeelsLikeCelsius:0.#}℃,建议加热 {heating} 档"; return new MattressComfortOutput { Bmi = bmi, Firmness = firmness, HeatingLevel = heating, IsWinter = winter, OutdoorTemperatureCelsius = weather?.TemperatureCelsius, FeelsLikeCelsius = weather?.FeelsLikeCelsius, WeatherAvailable = weather != null, WeatherObservedAt = weather?.ObservedAt, WeatherReason = weatherReason, FirmnessReason = firmness == null ? "健康档案尚无身高和体重,暂不自动调节软硬度" : $"按 BMI {bmi:0.0} 给出“{firmness}”演示起点,可在设备详情手动调整" }; } public static void ApplyTo(PersonalizedSceneConfig config, MattressComfortOutput recommendation) { config.MattressHeatingLevel = recommendation.HeatingLevel; config.MattressLeftFirmness = recommendation.Firmness == null ? 0 : FirmnessLevel(recommendation.Firmness); config.MattressRightFirmness = config.MattressLeftFirmness; } public static int FirmnessLevel(string firmness) => firmness switch { "最软" => 1, "很软" => 2, "稍软" => 3, "稍硬" => 4, "很硬" => 5, "最硬" => 6, _ => 0 }; public static string? FirmnessName(int level) => level switch { 1 => "最软", 2 => "很软", 3 => "稍软", 4 => "稍硬", 5 => "很硬", 6 => "最硬", _ => null }; private static async Task ReadWeatherAsync(CancellationToken cancellationToken) { if (CachedWeather is { } cached && DateTimeOffset.UtcNow - cached.FetchedAt < TimeSpan.FromMinutes(30)) return cached; await WeatherLock.WaitAsync(cancellationToken); try { if (CachedWeather is { } fresh && DateTimeOffset.UtcNow - fresh.FetchedAt < TimeSpan.FromMinutes(30)) return fresh; using var response = await WeatherClient.GetAsync(WeatherUrl, cancellationToken); response.EnsureSuccessStatusCode(); await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken); using var json = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken); var current = json.RootElement.GetProperty("current"); var temperature = current.GetProperty("temperature_2m").GetDouble(); var feelsLike = current.GetProperty("apparent_temperature").GetDouble(); var observedAt = current.GetProperty("time").GetString(); CachedWeather = new WeatherSample(temperature, feelsLike, observedAt, DateTimeOffset.UtcNow); return CachedWeather; } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { throw; } catch { // 天气失败时不使用过期值自动开启加热,冬季也安全关闭。 return null; } finally { WeatherLock.Release(); } } private sealed record WeatherSample( double TemperatureCelsius, double FeelsLikeCelsius, string? ObservedAt, DateTimeOffset FetchedAt); }