refactor: 4层架构重构 + 饮食VLM接入 + 多项修复

- 后端: remaining_endpoints拆分为6个独立文件
- 后端: AI Agent Handler从ai_chat_endpoints抽取为7个独立处理器
- 后端: 食物识别prompt改为输出结构化JSON
- 前端: 饮食识别从Mock替换为真实VLM API调用
- 前端: 首页图片上传URL修复(/api/upload→/api/files/upload)
- 前端: 拍饮食按钮导航到独立DietCapturePage
- 前端: 删除无用agent_bar.dart
- 前端: 修复widget_test.dart过期属性名
- 前端: 恢复ServicePackageCard和详情页
- 新增6份实施文档(情况/问诊/报告/建档/日历/视觉统一)
This commit is contained in:
MingNian
2026-06-03 23:17:37 +08:00
parent 5bd0155e17
commit c2399b952f
33 changed files with 3311 additions and 660 deletions

View File

@@ -0,0 +1,252 @@
# 健康日历 — 实施文档
## 一、现状
`remaining_pages.dart``HealthCalendarPage` 用固定规律模拟事件:
```dart
// 每周日、六 固定显示"用药",每 7 天显示"运动",每月 20 号显示"复查"
if (date.day == 5 || date.day == 12 || date.day == 19 || date.day == 26) events.add('medication');
```
日历格子只显示小圆点,不反映任何真实数据。
## 二、目标
日历每个日期格子上用小圆点标注当天的真实事件:
| 事件类型 | 圆点颜色 | 数据来源 |
|---------|---------|---------|
| 用药计划 | `Color(0xFF8B9CF7)` 紫 | `/api/medications` — TimeOfDay 数组 |
| 运动计划 | `Color(0xFF43A047)` 绿 | `/api/exercise-plans/current` — DayOfWeek |
| 复查随访 | `Color(0xFFF59E0B)` 橙 | 新增 `/api/followups`(或复用现有数据结构) |
| 健康数据记录 | `Color(0xFF4D96FF)` 蓝 | `/api/health-records?date=xxx` — 当天有记录 |
点击某天 → 弹出该日详情(当天记录列表 + 可快速跳转)。
## 三、后端改动
### 3.1 新增日历聚合端点
新建 `Endpoints/calendar_endpoints.cs`
```csharp
public static class CalendarEndpoints
{
public static void MapCalendarEndpoints(this WebApplication app)
{
app.MapGet("/api/calendar", async (
int year, int month,
HttpContext http, AppDbContext db, CancellationToken ct) =>
{
var userId = GetUserId(http);
var start = new DateOnly(year, month, 1);
var end = start.AddMonths(1);
// ① 用药:查询该月所有活跃用药的服药日期
var medications = await db.Medications
.Where(m => m.UserId == userId && m.IsActive)
.Select(m => new { m.Id, m.Name, m.Dosage, m.TimeOfDay })
.ToListAsync(ct);
// 每天都有服药计划Daily频率映射到每一天
// ② 运动:查该月覆盖的运动计划
var plans = await db.ExercisePlans
.Where(p => p.UserId == userId && p.WeekStartDate < end)
.Include(p => p.Items)
.ToListAsync(ct);
// ③ 健康数据:查该月有记录的日期
var healthDates = await db.HealthRecords
.Where(r => r.UserId == userId
&& r.RecordedAt >= start.ToDateTime(TimeOnly.MinValue)
&& r.RecordedAt < end.ToDateTime(TimeOnly.MinValue))
.Select(r => DateOnly.FromDateTime(r.RecordedAt))
.Distinct()
.ToListAsync(ct);
// ④ 复查随访
var followups = await db.FollowUps
.Where(f => f.UserId == userId
&& f.ScheduledAt >= start.ToDateTime(TimeOnly.MinValue)
&& f.ScheduledAt < end.ToDateTime(TimeOnly.MinValue))
.Select(f => new { f.Id, f.Title, f.Department,
Date = DateOnly.FromDateTime(f.ScheduledAt) })
.ToListAsync(ct);
// 按日期聚合
var calendar = new Dictionary<string, object>();
for (var d = start; d < end; d = d.AddDays(1))
{
var key = d.ToString("yyyy-MM-dd");
var events = new List<string>();
// 用药事件
if (medications.Any(m => m.TimeOfDay.Count > 0))
events.Add("medication");
// 运动事件
if (plans.Any(p => p.Items.Any(i => (int)d.DayOfWeek == i.DayOfWeek && !i.IsRestDay)))
events.Add("exercise");
// 复查事件
if (followups.Any(f => f.Date == d))
events.Add("followup");
// 健康数据
if (healthDates.Contains(d))
events.Add("health_record");
if (events.Count > 0)
calendar[key] = new { date = key, events };
}
return Results.Ok(new { code = 0, data = calendar.Values, message = (string?)null });
}).RequireAuthorization();
}
private static Guid GetUserId(HttpContext http) => ...;
}
```
`Program.cs` 注册:
```csharp
app.MapCalendarEndpoints();
```
### 3.2 新增每日详情端点
```csharp
app.MapGet("/api/calendar/{date}", async (
string date, HttpContext http, AppDbContext db, CancellationToken ct) =>
{
var userId = GetUserId(http);
var d = DateOnly.Parse(date);
var dt = d.ToDateTime(TimeOnly.MinValue);
// 当日健康记录
var records = await db.HealthRecords
.Where(r => r.UserId == userId && r.RecordedAt >= dt && r.RecordedAt < dt.AddDays(1))
.OrderByDescending(r => r.RecordedAt)
.Select(r => new { r.Id, Type = r.MetricType.ToString(), r.Systolic, r.Diastolic, r.Value, r.Unit })
.ToListAsync(ct);
// 当日服药打卡
var medLogs = await db.MedicationLogs
.Where(l => l.UserId == userId && l.CreatedAt >= dt && l.CreatedAt < dt.AddDays(1))
.Select(l => new { l.Id, l.Status, MedName = l.Medication.Name })
.ToListAsync(ct);
// 当日随访
var followup = await db.FollowUps
.Where(f => f.UserId == userId && f.ScheduledAt >= dt && f.ScheduledAt < dt.AddDays(1))
.Select(f => new { f.Id, f.Title, f.Department, f.DoctorName, f.Notes })
.FirstOrDefaultAsync(ct);
return Results.Ok(new
{
code = 0,
data = new { records, medicationLogs = medLogs, followup },
message = (string?)null
});
}).RequireAuthorization();
```
## 四、前端改动
### 4.1 数据层
`HealthCalendarPage` 改为 `ConsumerStatefulWidget`
```dart
class _HealthCalendarPageState extends ConsumerState<HealthCalendarPage> {
DateTime _currentMonth = DateTime.now();
Map<String, List<String>> _events = {}; // "2026-06-03" -> ["medication", "exercise"]
@override void initState() {
super.initState();
_loadMonth();
}
Future<void> _loadMonth() async {
final api = ref.read(apiClientProvider);
final res = await api.get('/api/calendar', queryParameters: {
'year': _currentMonth.year,
'month': _currentMonth.month,
});
final list = (res.data['data'] as List?) ?? [];
setState(() {
_events = { for (var item in list)
(item as Map)['date']: List<String>.from(item['events'] ?? [])
};
});
}
}
```
### 4.2 替换 _getEvents
```dart
// 旧(删除):
List<String> _getEvents(DateTime date) {
final events = <String>[];
if (date.day == 5 || date.day == 12 ...) events.add('medication');
...
}
// 新:
List<String> _getEvents(DateTime date) {
final key = '${date.year}-${date.month.toString().padLeft(2,'0')}-${date.day.toString().padLeft(2,'0')}';
return _events[key] ?? [];
}
```
### 4.3 点击日期 → 详情弹窗
```dart
void _onDayTap(int day) {
final date = DateTime(_currentMonth.year, _currentMonth.month, day);
final key = date.toIso8601String().substring(0, 10);
_showDayDetail(key);
}
void _showDayDetail(String date) async {
final api = ref.read(apiClientProvider);
final res = await api.get('/api/calendar/$date');
final data = res.data['data'];
if (!mounted) return;
showModalBottomSheet(
context: context,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (ctx) => _DayDetailSheet(date: date, data: data),
);
}
```
### 4.4 DayDetailSheet 组件
```dart
class _DayDetailSheet extends StatelessWidget {
// 健康数据区:指标名 + 数值
// 用药区:药品名 + 打卡状态
// 复查区:医院/科室/备注
// 空状态:"当天暂无记录"
}
```
## 五、文件改动清单
```
后端:
新建: Endpoints/calendar_endpoints.cs (~90行)
修改: Program.cs (+ app.MapCalendarEndpoints(), 1行)
前端:
修改: pages/remaining_pages.dart (HealthCalendarPage 重写, ~100行改)
```
## 六、不做的事情
- 不替换 Flutter 日历包(继续用自定义 GridView
- 不做月视图周视图切换
- 不做日历同步到系统日历