feat: 引入 EF Core Migrations + 后台任务/校验修复 + 前端四态

后端
- 改用 EF Core Migrations(InitialCreate),Program.cs 用 MigrateAsync + AUTO_MIGRATE 开关 + 显式 MigrationsAssembly;移除 EnsureCreated、删除手写 DatabaseSchemaMigrator
- 修复后台任务永久卡 Processing:三个队列对超时且达上限的任务原子标记 Failed
- 修复报告重试失效:基础设施异常向上抛激活 RetryAsync,停机取消不计失败
- 修复 AI 用药天数 off-by-one(结束日为包含式)
- AI 报告日志不再记录 VLM 健康正文
- 新增统一输入校验(ValidationException + 中间件映射 400):血压/心率/血糖/血氧/体重范围、药名/日期/服药时间去重、饮食热量与评分、运动时长上限
- 删除健康数据 AI 录入的影子直写路径,统一走 Service 校验

前端
- 新增 AppErrorState / AppFutureView,用药列表、服药打卡、运动计划、随访列表改为 加载/失败/空/数据 四态,失败可重试

瘦身
- 删除过时的 DataSeeder / DevDataSeeder 及调用
- 删除依赖实时服务的 ai_agent_tests 集成测试
This commit is contained in:
MingNian
2026-06-21 21:04:40 +08:00
parent aa44d6f0f0
commit b57d0d16f4
30 changed files with 4377 additions and 752 deletions

View File

@@ -1,3 +1,4 @@
using Health.Domain;
using Health.Domain.Entities;
using Health.Domain.Enums;
@@ -5,6 +6,7 @@ namespace Health.Application.Diets;
public sealed class DietService(IDietRepository diets) : IDietService
{
private const int MaxCalories = 20000; // 单条记录/单项热量上限kcal
private readonly IDietRepository _diets = diets;
public async Task<IReadOnlyList<DietRecordDto>> ListAsync(Guid userId, string? date, string? mealType, CancellationToken ct)
@@ -18,6 +20,15 @@ public sealed class DietService(IDietRepository diets) : IDietService
public async Task<Guid> CreateAsync(Guid userId, DietRecordCreateRequest request, CancellationToken ct)
{
ValidateCalories(request.TotalCalories, "总热量");
ValidateScore(request.HealthScore);
foreach (var item in request.FoodItems)
{
if (string.IsNullOrWhiteSpace(item.Name))
throw new ValidationException("食物名称不能为空");
ValidateCalories(item.Calories, $"食物「{item.Name.Trim()}」热量");
}
var record = new DietRecord
{
Id = Guid.NewGuid(),
@@ -34,7 +45,7 @@ public sealed class DietService(IDietRepository diets) : IDietService
record.FoodItems.Add(new DietFoodItem
{
Id = Guid.NewGuid(),
Name = item.Name,
Name = item.Name.Trim(),
Portion = item.Portion,
Calories = item.Calories,
SortOrder = item.SortOrder,
@@ -61,12 +72,27 @@ public sealed class DietService(IDietRepository diets) : IDietService
var record = await _diets.GetOwnedAsync(userId, recordId, ct);
if (record == null) return false;
ValidateCalories(request.TotalCalories, "总热量");
ValidateScore(request.HealthScore);
if (request.TotalCalories.HasValue) record.TotalCalories = request.TotalCalories.Value;
if (request.HealthScore.HasValue) record.HealthScore = request.HealthScore.Value;
await _diets.SaveChangesAsync(ct);
return true;
}
private static void ValidateCalories(int? calories, string field)
{
if (calories.HasValue && (calories.Value < 0 || calories.Value > MaxCalories))
throw new ValidationException($"{field}应在 0~{MaxCalories} kcal 之间,当前值 {calories.Value} 不合理");
}
private static void ValidateScore(int? score)
{
if (score.HasValue && (score.Value < 0 || score.Value > 100))
throw new ValidationException($"健康评分应在 0~100 之间,当前值 {score.Value} 不合理");
}
private static DietRecordDto ToDto(DietRecord record) => new(
record.Id,
record.MealType.ToString(),