111
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
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<MattressComfortOutput> 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<WeatherSample?> 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);
|
||||
}
|
||||
Reference in New Issue
Block a user