feat: 用药打卡全面重构 - 按次追踪 + 独立打卡页 + 任务区汇总

- 后端用药提醒改为按次粒度(每个服药时间独立判断)
- 新增 per-dose 打卡/取消打卡 API
- 新增 MedicationCheckInPage 独立打卡页(每药每时间单独toggle)
- 用药列表删底部弹窗(编辑/停药),点药品直接进该药打卡页
- 任务区用药汇总为一行(过期待服已服)
- 支持隔天(EveryOtherDay)/每周频率判断
- 删历史对话功能(ConversationItem + conversationListProvider + UI)
This commit is contained in:
MingNian
2026-06-08 16:21:13 +08:00
parent fc7150ad60
commit 16d1d3d305
17 changed files with 519 additions and 373 deletions

View File

@@ -108,32 +108,141 @@ public static class MedicationEndpoints
var userId = GetUserId(http);
var beijingNow = DateTime.UtcNow.AddHours(8);
var now = TimeOnly.FromDateTime(beijingNow);
var windowFuture = now.AddHours(2); // 接下来2小时
var windowPast = now.AddHours(-1); // 过去1小时内过了还没吃就该提醒
var today = DateOnly.FromDateTime(beijingNow);
var todayUtc = DateTime.SpecifyKind(beijingNow.Date, DateTimeKind.Utc);
// 只查活跃且在有效期内的药
var meds = await db.Medications
.Where(m => m.UserId == userId && m.IsActive && m.TimeOfDay != null)
.Where(m => m.UserId == userId && m.IsActive && m.TimeOfDay != null
&& m.StartDate <= today && (m.EndDate == null || m.EndDate >= today))
.ToListAsync(ct);
// 同时包含未来2小时内 + 过去1小时内防止过了时间就不提醒
var due = meds.Where(m => m.TimeOfDay!.Any(t =>
InTimeWindow(t, now, windowFuture) || InTimeWindow(t, windowPast, now)
)).ToList();
// 查询今天所有服药记录
var logs = await db.MedicationLogs
.Where(l => l.UserId == userId && l.ConfirmedAt >= todayUtc)
.ToListAsync(ct);
var todayStart = DateTime.SpecifyKind(beijingNow.Date, DateTimeKind.Utc);
var dueMeds = new List<object>();
foreach (var m in due)
// 按次生成提醒
var reminders = new List<object>();
foreach (var m in meds)
{
var logged = await db.MedicationLogs.AnyAsync(l =>
l.MedicationId == m.Id
&& l.CreatedAt >= todayStart
&& l.Status == MedicationLogStatus.Taken, ct);
if (!logged)
dueMeds.Add(new { m.Id, m.Name, m.Dosage, m.TimeOfDay });
// 处理隔天/每周的逻辑
var dosesToday = GetDosesForToday(m, today);
if (!dosesToday) continue;
foreach (var t in m.TimeOfDay!)
{
var scheduledTime = t;
var doseTimeBeijing = today.ToDateTime(scheduledTime);
// 查该药该时间是否已打卡
var alreadyLogged = logs.Any(l =>
l.MedicationId == m.Id && l.ScheduledTime == scheduledTime);
string status;
if (alreadyLogged)
{
var log = logs.First(l => l.MedicationId == m.Id && l.ScheduledTime == scheduledTime);
status = log.Status == MedicationLogStatus.Taken ? "taken" : "skipped";
}
else if (scheduledTime <= now.AddMinutes(-30))
{
status = "overdue"; // 过了30分钟还没吃
}
else if (scheduledTime <= now.AddHours(3))
{
status = "upcoming"; // 未来3小时内
}
else
{
continue; // 太远,不显示
}
reminders.Add(new
{
m.Id, m.Name, m.Dosage,
scheduledTime = scheduledTime.ToString("HH:mm"),
status,
});
}
}
return Results.Ok(new { code = 0, data = dueMeds, message = (string?)null });
// overdue 在前upcoming 在后taken 在最后
reminders = reminders.OrderBy(r =>
((dynamic)r).status == "overdue" ? 0 :
((dynamic)r).status == "upcoming" ? 1 : 2
).ToList();
return Results.Ok(new { code = 0, data = reminders, message = (string?)null });
});
// 按次打卡
group.MapPost("/{id:guid}/confirm-dose", async (Guid id, HttpContext http, AppDbContext db, CancellationToken ct) =>
{
var userId = GetUserId(http);
var med = await db.Medications.FirstOrDefaultAsync(m => m.Id == id && m.UserId == userId, ct);
if (med == null) return Results.Ok(new { code = 404, message = "药品不存在" });
using var reader = new System.IO.StreamReader(http.Request.Body);
var body = await reader.ReadToEndAsync(ct);
var json = System.Text.Json.JsonDocument.Parse(body);
var timeStr = json.RootElement.GetProperty("scheduledTime").GetString()!;
var statusStr = json.RootElement.TryGetProperty("status", out var st) ? st.GetString() : "taken";
var scheduledTime = TimeOnly.Parse(timeStr);
var status = statusStr == "taken" ? MedicationLogStatus.Taken : MedicationLogStatus.Skipped;
// 防止重复打卡
var todayUtc = DateTime.SpecifyKind(DateTime.UtcNow.AddHours(8).Date, DateTimeKind.Utc);
var existing = await db.MedicationLogs.AnyAsync(l =>
l.MedicationId == id && l.UserId == userId
&& l.ScheduledTime == scheduledTime && l.ConfirmedAt >= todayUtc, ct);
if (existing) return Results.Ok(new { code = 0, data = new { success = false, message = "已打卡" }, message = (string?)null });
db.MedicationLogs.Add(new MedicationLog
{
Id = Guid.NewGuid(), MedicationId = id, UserId = userId,
Status = status, ScheduledTime = scheduledTime, ConfirmedAt = DateTime.UtcNow,
});
await db.SaveChangesAsync(ct);
return Results.Ok(new { code = 0, data = new { success = true }, message = (string?)null });
});
// 取消打卡(删掉对应 log
group.MapDelete("/{id:guid}/confirm-dose/{time}", async (Guid id, string time, HttpContext http, AppDbContext db, CancellationToken ct) =>
{
var userId = GetUserId(http);
var scheduledTime = TimeOnly.Parse(time);
var todayUtc = DateTime.SpecifyKind(DateTime.UtcNow.AddHours(8).Date, DateTimeKind.Utc);
var log = await db.MedicationLogs
.Where(l => l.MedicationId == id && l.UserId == userId
&& l.ScheduledTime == scheduledTime && l.ConfirmedAt >= todayUtc)
.FirstOrDefaultAsync(ct);
if (log != null) { db.MedicationLogs.Remove(log); await db.SaveChangesAsync(ct); }
return Results.Ok(new { code = 0, data = new { success = true }, message = (string?)null });
});
}
/// <summary>
/// 判断今天是否该吃药(处理隔天/每周频率)
/// </summary>
private static bool GetDosesForToday(Medication m, DateOnly today)
{
if (m.Frequency == MedicationFrequency.Daily) return true;
if (m.Frequency == MedicationFrequency.TwiceDaily) return true;
if (m.Frequency == MedicationFrequency.ThreeTimesDaily) return true;
if (m.Frequency == MedicationFrequency.AsNeeded) return true;
var startDate = m.StartDate ?? today;
if (m.Frequency == MedicationFrequency.EveryOtherDay)
{
var daysSinceStart = today.DayNumber - startDate.DayNumber;
return daysSinceStart % 2 == 0;
}
if (m.Frequency == MedicationFrequency.Weekly)
{
return today.DayOfWeek == startDate.DayOfWeek;
}
return true;
}
private static bool InTimeWindow(TimeOnly t, TimeOnly start, TimeOnly end) =>