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:
@@ -0,0 +1,64 @@
|
||||
namespace Health.Infrastructure.AI.AgentHandlers;
|
||||
|
||||
/// <summary>
|
||||
/// 共享工具处理器——被多个 Agent 引用的通用工具
|
||||
/// </summary>
|
||||
public static class CommonAgentHandler
|
||||
{
|
||||
public static readonly ToolDefinition QueryHealthRecordsTool = new()
|
||||
{
|
||||
Function = new()
|
||||
{
|
||||
Name = "query_health_records", Description = "查询近期健康数据",
|
||||
Parameters = new { type = "object", properties = new { type = new { type = "string" }, days = new { type = "integer" } } }
|
||||
}
|
||||
};
|
||||
|
||||
public static readonly ToolDefinition CheckArchiveTool = new()
|
||||
{
|
||||
Function = new() { Name = "check_archive", Description = "查询患者健康档案", Parameters = new { type = "object", properties = new { } } }
|
||||
};
|
||||
|
||||
public static List<ToolDefinition> Tools => [QueryHealthRecordsTool, CheckArchiveTool];
|
||||
|
||||
public static async Task<object> Execute(string toolName, JsonElement args, AppDbContext db, Guid userId)
|
||||
{
|
||||
return toolName switch
|
||||
{
|
||||
"query_health_records" => await ExecuteQueryHealthRecords(db, userId, args),
|
||||
"check_archive" => await ExecuteCheckArchive(db, userId),
|
||||
_ => new { success = false, message = $"未知工具: {toolName}" }
|
||||
};
|
||||
}
|
||||
|
||||
private static async Task<object> ExecuteQueryHealthRecords(AppDbContext db, Guid userId, JsonElement args)
|
||||
{
|
||||
var type = args.TryGetProperty("type", out var t) ? t.GetString() : null;
|
||||
var days = args.TryGetProperty("days", out var d) ? d.GetInt32() : 7;
|
||||
|
||||
var query = db.HealthRecords.Where(r => r.UserId == userId);
|
||||
if (!string.IsNullOrEmpty(type) && Enum.TryParse<HealthMetricType>(type, ignoreCase: true, out var mt))
|
||||
query = query.Where(r => r.MetricType == mt);
|
||||
|
||||
query = query.Where(r => r.RecordedAt >= DateTime.UtcNow.AddDays(-days));
|
||||
|
||||
var records = await query.OrderByDescending(r => r.RecordedAt).Take(30).Select(r => new
|
||||
{
|
||||
r.Id, Type = r.MetricType.ToString(), r.Systolic, r.Diastolic, r.Value, r.Unit, r.IsAbnormal, r.RecordedAt,
|
||||
}).ToListAsync();
|
||||
|
||||
return new { count = records.Count, records };
|
||||
}
|
||||
|
||||
private static async Task<object> ExecuteCheckArchive(AppDbContext db, Guid userId)
|
||||
{
|
||||
var archive = await db.HealthArchives.FirstOrDefaultAsync(a => a.UserId == userId);
|
||||
if (archive == null) return new { found = false };
|
||||
return new
|
||||
{
|
||||
found = true, archive.Diagnosis, archive.SurgeryType,
|
||||
SurgeryDate = archive.SurgeryDate?.ToString("yyyy-MM-dd"),
|
||||
archive.Allergies, archive.DietRestrictions, archive.ChronicDiseases, archive.FamilyHistory,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace Health.Infrastructure.AI.AgentHandlers;
|
||||
|
||||
/// <summary>
|
||||
/// AI 问诊 Agent 工具处理器——转医生
|
||||
/// </summary>
|
||||
public static class ConsultationAgentHandler
|
||||
{
|
||||
public static readonly ToolDefinition RequestDoctorTool = new()
|
||||
{
|
||||
Function = new()
|
||||
{
|
||||
Name = "request_doctor", Description = "请求转接真人医生",
|
||||
Parameters = new { type = "object", properties = new { reason = new { type = "string" }, urgency_level = new { type = "string" } } }
|
||||
}
|
||||
};
|
||||
|
||||
public static List<ToolDefinition> Tools => [CommonAgentHandler.QueryHealthRecordsTool, CommonAgentHandler.CheckArchiveTool, RequestDoctorTool];
|
||||
|
||||
public static Task<object> Execute(string toolName, JsonElement args, AppDbContext db, Guid userId)
|
||||
{
|
||||
return toolName switch
|
||||
{
|
||||
"query_health_records" or "check_archive" => CommonAgentHandler.Execute(toolName, args, db, userId),
|
||||
_ => Task.FromResult<object>(new { success = false, message = $"未知工具: {toolName}" })
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
namespace Health.Infrastructure.AI.AgentHandlers;
|
||||
|
||||
/// <summary>
|
||||
/// 拍饮食 Agent 工具处理器——食物估算
|
||||
/// </summary>
|
||||
public static class DietAgentHandler
|
||||
{
|
||||
public static readonly ToolDefinition EstimateFoodTool = new()
|
||||
{
|
||||
Function = new() { Name = "estimate_food_text", Description = "根据文字描述估算食物份量和热量", Parameters = new { type = "object", properties = new { text = new { type = "string" } }, required = new[] { "text" } } }
|
||||
};
|
||||
|
||||
public static List<ToolDefinition> Tools => [EstimateFoodTool, CommonAgentHandler.CheckArchiveTool];
|
||||
|
||||
public static Task<object> Execute(string toolName, JsonElement args, AppDbContext db, Guid userId)
|
||||
{
|
||||
return toolName switch
|
||||
{
|
||||
"check_archive" => CommonAgentHandler.Execute(toolName, args, db, userId),
|
||||
_ => Task.FromResult<object>(new { success = false, message = $"未知工具: {toolName}" })
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
namespace Health.Infrastructure.AI.AgentHandlers;
|
||||
|
||||
/// <summary>
|
||||
/// 运动计划 Agent 工具处理器
|
||||
/// </summary>
|
||||
public static class ExerciseAgentHandler
|
||||
{
|
||||
public static readonly ToolDefinition ManageExerciseTool = new()
|
||||
{
|
||||
Function = new()
|
||||
{
|
||||
Name = "manage_exercise", Description = "运动计划管理",
|
||||
Parameters = new { type = "object", properties = new { action = new { type = "string" } }, required = new[] { "action" } }
|
||||
}
|
||||
};
|
||||
|
||||
public static List<ToolDefinition> Tools => [ManageExerciseTool];
|
||||
|
||||
public static async Task<object> Execute(string toolName, JsonElement args, AppDbContext db, Guid userId)
|
||||
{
|
||||
return toolName switch
|
||||
{
|
||||
"manage_exercise" => await ExecuteManageExercise(db, userId, args),
|
||||
_ => new { success = false, message = $"未知工具: {toolName}" }
|
||||
};
|
||||
}
|
||||
|
||||
private static async Task<object> ExecuteManageExercise(AppDbContext db, Guid userId, JsonElement args)
|
||||
{
|
||||
var action = args.TryGetProperty("action", out var a) ? a.GetString()! : "query";
|
||||
switch (action)
|
||||
{
|
||||
case "create":
|
||||
var weekStart = args.TryGetProperty("week_start_date", out var wsd) ? DateOnly.Parse(wsd.GetString()!) : DateOnly.FromDateTime(DateTime.Now);
|
||||
var plan = new ExercisePlan { Id = Guid.NewGuid(), UserId = userId, WeekStartDate = weekStart };
|
||||
if (args.TryGetProperty("items", out var items))
|
||||
{
|
||||
foreach (var item in items.EnumerateArray())
|
||||
{
|
||||
plan.Items.Add(new ExercisePlanItem
|
||||
{
|
||||
Id = Guid.NewGuid(), DayOfWeek = item.GetProperty("day_of_week").GetInt32(),
|
||||
ExerciseType = item.GetProperty("exercise_type").GetString() ?? "散步",
|
||||
DurationMinutes = item.GetProperty("duration_minutes").GetInt32(),
|
||||
IsRestDay = item.TryGetProperty("is_rest_day", out var rd) && rd.GetBoolean(),
|
||||
});
|
||||
}
|
||||
}
|
||||
db.ExercisePlans.Add(plan);
|
||||
await db.SaveChangesAsync();
|
||||
return new { success = true, plan_id = plan.Id };
|
||||
|
||||
case "checkin":
|
||||
var itemId = args.TryGetProperty("item_id", out var iid) ? iid.GetGuid() : Guid.Empty;
|
||||
var exerciseItem = await db.ExercisePlanItems.FindAsync([itemId]);
|
||||
if (exerciseItem == null) return new { success = false, message = "条目不存在" };
|
||||
exerciseItem.IsCompleted = true;
|
||||
exerciseItem.CompletedAt = DateTime.UtcNow;
|
||||
await db.SaveChangesAsync();
|
||||
return new { success = true };
|
||||
|
||||
default:
|
||||
var existingPlan = await db.ExercisePlans.Where(p => p.UserId == userId)
|
||||
.OrderByDescending(p => p.WeekStartDate).FirstOrDefaultAsync();
|
||||
if (existingPlan == null) return new { found = false };
|
||||
var exerciseItems = await db.ExercisePlanItems.Where(i => i.PlanId == existingPlan.Id).OrderBy(i => i.DayOfWeek).ToListAsync();
|
||||
return new { found = true, plan_id = existingPlan.Id, items = exerciseItems.Select(i => new { i.Id, i.DayOfWeek, i.ExerciseType, i.DurationMinutes, i.IsCompleted }) };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
namespace Health.Infrastructure.AI.AgentHandlers;
|
||||
|
||||
/// <summary>
|
||||
/// 记数据 Agent 工具处理器——录入健康指标
|
||||
/// </summary>
|
||||
public static class HealthDataAgentHandler
|
||||
{
|
||||
public static readonly ToolDefinition RecordHealthDataTool = new()
|
||||
{
|
||||
Function = new()
|
||||
{
|
||||
Name = "record_health_data", Description = "记录健康数据(血压/心率/血糖/血氧/体重)",
|
||||
Parameters = new { type = "object", properties = new { type = new { type = "string" }, systolic = new { type = "integer" }, diastolic = new { type = "integer" }, heart_rate = new { type = "number" }, glucose = new { type = "number" }, spo2 = new { type = "number" }, weight = new { type = "number" } }, required = new[] { "type" } }
|
||||
}
|
||||
};
|
||||
|
||||
public static List<ToolDefinition> Tools => [RecordHealthDataTool, CommonAgentHandler.QueryHealthRecordsTool];
|
||||
|
||||
public static async Task<object> Execute(string toolName, JsonElement args, AppDbContext db, Guid userId)
|
||||
{
|
||||
return toolName switch
|
||||
{
|
||||
"record_health_data" => await ExecuteRecordHealthData(db, userId, args),
|
||||
"query_health_records" => await CommonAgentHandler.Execute(toolName, args, db, userId),
|
||||
_ => new { success = false, message = $"未知工具: {toolName}" }
|
||||
};
|
||||
}
|
||||
|
||||
private static async Task<object> ExecuteRecordHealthData(AppDbContext db, Guid userId, JsonElement args)
|
||||
{
|
||||
var type = args.TryGetProperty("type", out var t) ? t.GetString()! : "";
|
||||
var record = new HealthRecord
|
||||
{
|
||||
Id = Guid.NewGuid(), UserId = userId, Source = HealthRecordSource.AiEntry,
|
||||
RecordedAt = args.TryGetProperty("recorded_at", out var ra) && ra.TryGetDateTime(out var dt) ? dt : DateTime.UtcNow,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
};
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case "blood_pressure":
|
||||
record.MetricType = HealthMetricType.BloodPressure;
|
||||
record.Systolic = args.TryGetProperty("systolic", out var s) ? s.GetInt32() : null;
|
||||
record.Diastolic = args.TryGetProperty("diastolic", out var d) ? d.GetInt32() : null;
|
||||
record.Unit = "mmHg";
|
||||
record.IsAbnormal = record.Systolic >= 140 || record.Diastolic >= 90 || record.Systolic <= 89 || record.Diastolic <= 59;
|
||||
break;
|
||||
case "heart_rate":
|
||||
record.MetricType = HealthMetricType.HeartRate;
|
||||
record.Value = args.TryGetProperty("heart_rate", out var hr) ? hr.GetDecimal() : null;
|
||||
record.Unit = "次/分";
|
||||
record.IsAbnormal = record.Value > 100 || record.Value < 60;
|
||||
break;
|
||||
case "glucose":
|
||||
record.MetricType = HealthMetricType.Glucose;
|
||||
record.Value = args.TryGetProperty("glucose", out var g) ? g.GetDecimal() : null;
|
||||
record.Unit = "mmol/L";
|
||||
record.IsAbnormal = record.Value >= 7.0m || record.Value <= 3.8m;
|
||||
break;
|
||||
case "spo2":
|
||||
record.MetricType = HealthMetricType.SpO2;
|
||||
record.Value = args.TryGetProperty("spo2", out var o) ? o.GetDecimal() : null;
|
||||
record.Unit = "%";
|
||||
record.IsAbnormal = record.Value <= 94;
|
||||
break;
|
||||
case "weight":
|
||||
record.MetricType = HealthMetricType.Weight;
|
||||
record.Value = args.TryGetProperty("weight", out var w) ? w.GetDecimal() : null;
|
||||
record.Unit = "kg";
|
||||
break;
|
||||
default:
|
||||
return new { success = false, message = $"未知指标类型: {type}" };
|
||||
}
|
||||
|
||||
db.HealthRecords.Add(record);
|
||||
await db.SaveChangesAsync();
|
||||
return new { success = true, record_id = record.Id, type = record.MetricType.ToString() };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
namespace Health.Infrastructure.AI.AgentHandlers;
|
||||
|
||||
/// <summary>
|
||||
/// 药管家 Agent 工具处理器——用药管理
|
||||
/// </summary>
|
||||
public static class MedicationAgentHandler
|
||||
{
|
||||
public static readonly ToolDefinition ManageMedicationTool = new()
|
||||
{
|
||||
Function = new()
|
||||
{
|
||||
Name = "manage_medication", Description = "用药管理",
|
||||
Parameters = new { type = "object", properties = new { action = new { type = "string" }, name = new { type = "string" }, dosage = new { type = "string" } }, required = new[] { "action" } }
|
||||
}
|
||||
};
|
||||
|
||||
public static List<ToolDefinition> Tools => [ManageMedicationTool, CommonAgentHandler.CheckArchiveTool];
|
||||
|
||||
public static async Task<object> Execute(string toolName, JsonElement args, AppDbContext db, Guid userId)
|
||||
{
|
||||
return toolName switch
|
||||
{
|
||||
"manage_medication" => await ExecuteManageMedication(db, userId, args),
|
||||
"check_archive" => await CommonAgentHandler.Execute(toolName, args, db, userId),
|
||||
_ => new { success = false, message = $"未知工具: {toolName}" }
|
||||
};
|
||||
}
|
||||
|
||||
private static async Task<object> ExecuteManageMedication(AppDbContext db, Guid userId, JsonElement args)
|
||||
{
|
||||
var action = args.TryGetProperty("action", out var a) ? a.GetString()! : "query";
|
||||
return action switch
|
||||
{
|
||||
"create" => await CreateMedication(db, userId, args),
|
||||
"query" => await QueryMedications(db, userId),
|
||||
"confirm" => await ConfirmMedication(db, userId, args),
|
||||
_ => new { success = false, message = $"未知操作: {action}" }
|
||||
};
|
||||
}
|
||||
|
||||
private static async Task<object> CreateMedication(AppDbContext db, Guid userId, JsonElement args)
|
||||
{
|
||||
var med = new Medication
|
||||
{
|
||||
Id = Guid.NewGuid(), UserId = userId,
|
||||
Name = args.TryGetProperty("name", out var n) ? n.GetString()! : "",
|
||||
Dosage = args.TryGetProperty("dosage", out var dg) ? dg.GetString() : null,
|
||||
Source = MedicationSource.AiEntry, IsActive = true,
|
||||
};
|
||||
db.Medications.Add(med);
|
||||
await db.SaveChangesAsync();
|
||||
return new { success = true, medication_id = med.Id, med.Name };
|
||||
}
|
||||
|
||||
private static async Task<object> QueryMedications(AppDbContext db, Guid userId)
|
||||
{
|
||||
var meds = await db.Medications.Where(m => m.UserId == userId && m.IsActive)
|
||||
.Select(m => new { m.Id, m.Name, m.Dosage, m.TimeOfDay }).ToListAsync();
|
||||
return new { count = meds.Count, medications = meds };
|
||||
}
|
||||
|
||||
private static async Task<object> ConfirmMedication(AppDbContext db, Guid userId, JsonElement args)
|
||||
{
|
||||
var medId = args.TryGetProperty("medication_id", out var mid) ? mid.GetGuid() : Guid.Empty;
|
||||
db.MedicationLogs.Add(new MedicationLog
|
||||
{
|
||||
Id = Guid.NewGuid(), MedicationId = medId, UserId = userId,
|
||||
Status = MedicationLogStatus.Taken, ScheduledTime = TimeOnly.FromDateTime(DateTime.Now), ConfirmedAt = DateTime.UtcNow,
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
return new { success = true };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
namespace Health.Infrastructure.AI.AgentHandlers;
|
||||
|
||||
/// <summary>
|
||||
/// 看报告 Agent 工具处理器——报告分析
|
||||
/// </summary>
|
||||
public static class ReportAgentHandler
|
||||
{
|
||||
public static readonly ToolDefinition AnalyzeReportTool = new()
|
||||
{
|
||||
Function = new() { Name = "analyze_report", Description = "分析报告图片", Parameters = new { type = "object", properties = new { image_url = new { type = "string" } }, required = new[] { "image_url" } } }
|
||||
};
|
||||
|
||||
public static List<ToolDefinition> Tools => [AnalyzeReportTool, CommonAgentHandler.QueryHealthRecordsTool];
|
||||
|
||||
public static Task<object> Execute(string toolName, JsonElement args, AppDbContext db, Guid userId)
|
||||
{
|
||||
return toolName switch
|
||||
{
|
||||
"query_health_records" => CommonAgentHandler.Execute(toolName, args, db, userId),
|
||||
_ => Task.FromResult<object>(new { success = false, message = $"未知工具: {toolName}" })
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
global using Health.Domain.Entities;
|
||||
global using Health.Domain.Enums;
|
||||
global using Health.Infrastructure.Data;
|
||||
global using Microsoft.EntityFrameworkCore;
|
||||
global using System.Text;
|
||||
global using System.Text.Json;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Drawing;
|
||||
using System.Drawing.Imaging;
|
||||
using Health.Infrastructure.AI;
|
||||
using Health.Infrastructure.AI.AgentHandlers;
|
||||
|
||||
namespace Health.WebApi.Endpoints;
|
||||
|
||||
@@ -121,7 +122,6 @@ public static class AiChatEndpoints
|
||||
|
||||
if (choice.FinishReason == "stop")
|
||||
{
|
||||
// 流式输出最终回复(带上完整的 tool call 历史,方便 LLM 利用工具结果生成回复)
|
||||
await foreach (var chunk in llmClient.ChatStreamAsync(messages, tools: null, ct: ct))
|
||||
{
|
||||
try
|
||||
@@ -141,7 +141,6 @@ public static class AiChatEndpoints
|
||||
}
|
||||
else if (choice.FinishReason == "tool_calls" && choice.Message?.ToolCalls != null)
|
||||
{
|
||||
// 一条 assistant 消息包含所有 tool calls(符合 OpenAI 协议)
|
||||
messages.Add(new ChatMessage
|
||||
{
|
||||
Role = "assistant",
|
||||
@@ -265,7 +264,6 @@ public static class AiChatEndpoints
|
||||
using (var stream = new FileStream(filePath, FileMode.Create))
|
||||
await file.CopyToAsync(stream, ct);
|
||||
|
||||
// 压缩图片后转 base64(VLM API 有请求体大小限制)
|
||||
var compressedPath = Path.Combine(uploadsDir, $"compressed_{safeName}");
|
||||
CompressImage(filePath, compressedPath, maxWidth: 1280, quality: 75L);
|
||||
var compressedBytes = await File.ReadAllBytesAsync(compressedPath, ct);
|
||||
@@ -273,7 +271,14 @@ public static class AiChatEndpoints
|
||||
imageUrls.Add($"data:image/jpeg;base64,{base64}");
|
||||
}
|
||||
|
||||
var prompt = "精准识别用户提供的食物图片,提取并返回详细信息,包括但不限于食物名称、具体份量及对应热量值。系统应确保识别结果的准确性和清晰度,以便为病人的饮食管理提供可靠数据支持。";
|
||||
var prompt = """
|
||||
识别图片中的食物,以 JSON 数组格式返回,每项包含:
|
||||
- name: 食物名称
|
||||
- portion: 份量描述(如"约1碗"、"约200g")
|
||||
- calories: 估算热量(千卡,整数)
|
||||
只返回 JSON 数组,不要任何其他文字。
|
||||
示例:[{"name":"米饭","portion":"约1碗","calories":150},{"name":"番茄炒蛋","portion":"约1份","calories":200},{...}]
|
||||
""";
|
||||
|
||||
try
|
||||
{
|
||||
@@ -288,6 +293,8 @@ public static class AiChatEndpoints
|
||||
});
|
||||
}
|
||||
|
||||
// ── SSE / 认证辅助 ──
|
||||
|
||||
private static async Task SseWriteAsync(HttpContext http, object data, CancellationToken ct)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(data, JsonOpts);
|
||||
@@ -298,7 +305,6 @@ public static class AiChatEndpoints
|
||||
private static Guid? GetUserId(HttpContext http) =>
|
||||
Guid.TryParse(http.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value, out var id) ? id : null;
|
||||
|
||||
/// 从 query string token 解析用户 ID(浏览器 EventSource 用)
|
||||
private static Guid? GetUserIdFromToken(string? token)
|
||||
{
|
||||
if (string.IsNullOrEmpty(token)) return null;
|
||||
@@ -312,202 +318,39 @@ public static class AiChatEndpoints
|
||||
catch (Exception) { return null; }
|
||||
}
|
||||
|
||||
// ── Agent / Tool 调度 ──
|
||||
|
||||
private static List<ToolDefinition> GetToolsForAgent(AgentType agentType) => agentType switch
|
||||
{
|
||||
AgentType.Health => [RecordHealthDataTool, QueryHealthRecordsTool],
|
||||
AgentType.Medication => [ManageMedicationTool, CheckArchiveTool],
|
||||
AgentType.Diet => [EstimateFoodTool, CheckArchiveTool],
|
||||
AgentType.Consultation => [QueryHealthRecordsTool, CheckArchiveTool, RequestDoctorTool],
|
||||
AgentType.Report => [AnalyzeReportTool, QueryHealthRecordsTool],
|
||||
AgentType.Exercise => [ManageExerciseTool],
|
||||
_ => [QueryHealthRecordsTool, CheckArchiveTool],
|
||||
AgentType.Health => HealthDataAgentHandler.Tools,
|
||||
AgentType.Medication => MedicationAgentHandler.Tools,
|
||||
AgentType.Diet => DietAgentHandler.Tools,
|
||||
AgentType.Consultation => ConsultationAgentHandler.Tools,
|
||||
AgentType.Report => ReportAgentHandler.Tools,
|
||||
AgentType.Exercise => ExerciseAgentHandler.Tools,
|
||||
_ => CommonAgentHandler.Tools,
|
||||
};
|
||||
|
||||
private static async Task<object> ExecuteToolCall(string toolName, string arguments, AppDbContext db, Guid userId)
|
||||
private static Task<object> ExecuteToolCall(string toolName, string arguments, AppDbContext db, Guid userId)
|
||||
{
|
||||
using var jsonDoc = JsonDocument.Parse(arguments);
|
||||
var root = jsonDoc.RootElement;
|
||||
|
||||
return toolName switch
|
||||
{
|
||||
"record_health_data" => await ExecuteRecordHealthData(db, userId, root),
|
||||
"query_health_records" => await ExecuteQueryHealthRecords(db, userId, root),
|
||||
"check_archive" => await ExecuteCheckArchive(db, userId),
|
||||
"manage_medication" => await ExecuteManageMedication(db, userId, root),
|
||||
"manage_exercise" => await ExecuteManageExercise(db, userId, root),
|
||||
_ => new { success = false, message = $"未知工具: {toolName}" }
|
||||
"record_health_data" => HealthDataAgentHandler.Execute(toolName, root, db, userId),
|
||||
"query_health_records" => CommonAgentHandler.Execute(toolName, root, db, userId),
|
||||
"check_archive" => CommonAgentHandler.Execute(toolName, root, db, userId),
|
||||
"manage_medication" => MedicationAgentHandler.Execute(toolName, root, db, userId),
|
||||
"estimate_food_text" => DietAgentHandler.Execute(toolName, root, db, userId),
|
||||
"analyze_report" => ReportAgentHandler.Execute(toolName, root, db, userId),
|
||||
"manage_exercise" => ExerciseAgentHandler.Execute(toolName, root, db, userId),
|
||||
"request_doctor" => ConsultationAgentHandler.Execute(toolName, root, db, userId),
|
||||
_ => Task.FromResult<object>(new { success = false, message = $"未知工具: {toolName}" })
|
||||
};
|
||||
}
|
||||
|
||||
private static async Task<object> ExecuteRecordHealthData(AppDbContext db, Guid userId, JsonElement args)
|
||||
{
|
||||
var type = args.TryGetProperty("type", out var t) ? t.GetString()! : "";
|
||||
var record = new HealthRecord
|
||||
{
|
||||
Id = Guid.NewGuid(), UserId = userId, Source = HealthRecordSource.AiEntry,
|
||||
RecordedAt = args.TryGetProperty("recorded_at", out var ra) && ra.TryGetDateTime(out var dt) ? dt : DateTime.UtcNow,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
};
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case "blood_pressure":
|
||||
record.MetricType = HealthMetricType.BloodPressure;
|
||||
record.Systolic = args.TryGetProperty("systolic", out var s) ? s.GetInt32() : null;
|
||||
record.Diastolic = args.TryGetProperty("diastolic", out var d) ? d.GetInt32() : null;
|
||||
record.Unit = "mmHg";
|
||||
record.IsAbnormal = record.Systolic >= 140 || record.Diastolic >= 90 || record.Systolic <= 89 || record.Diastolic <= 59;
|
||||
break;
|
||||
case "heart_rate":
|
||||
record.MetricType = HealthMetricType.HeartRate;
|
||||
record.Value = args.TryGetProperty("heart_rate", out var hr) ? hr.GetDecimal() : null;
|
||||
record.Unit = "次/分";
|
||||
record.IsAbnormal = record.Value > 100 || record.Value < 60;
|
||||
break;
|
||||
case "glucose":
|
||||
record.MetricType = HealthMetricType.Glucose;
|
||||
record.Value = args.TryGetProperty("glucose", out var g) ? g.GetDecimal() : null;
|
||||
record.Unit = "mmol/L";
|
||||
record.IsAbnormal = record.Value >= 7.0m || record.Value <= 3.8m;
|
||||
break;
|
||||
case "spo2":
|
||||
record.MetricType = HealthMetricType.SpO2;
|
||||
record.Value = args.TryGetProperty("spo2", out var o) ? o.GetDecimal() : null;
|
||||
record.Unit = "%";
|
||||
record.IsAbnormal = record.Value <= 94;
|
||||
break;
|
||||
case "weight":
|
||||
record.MetricType = HealthMetricType.Weight;
|
||||
record.Value = args.TryGetProperty("weight", out var w) ? w.GetDecimal() : null;
|
||||
record.Unit = "kg";
|
||||
break;
|
||||
default:
|
||||
return new { success = false, message = $"未知指标类型: {type}" };
|
||||
}
|
||||
|
||||
db.HealthRecords.Add(record);
|
||||
await db.SaveChangesAsync();
|
||||
return new { success = true, record_id = record.Id, type = record.MetricType.ToString() };
|
||||
}
|
||||
|
||||
private static async Task<object> ExecuteQueryHealthRecords(AppDbContext db, Guid userId, JsonElement args)
|
||||
{
|
||||
var type = args.TryGetProperty("type", out var t) ? t.GetString() : null;
|
||||
var days = args.TryGetProperty("days", out var d) ? d.GetInt32() : 7;
|
||||
|
||||
var query = db.HealthRecords.Where(r => r.UserId == userId);
|
||||
if (!string.IsNullOrEmpty(type) && Enum.TryParse<HealthMetricType>(type, ignoreCase: true, out var mt))
|
||||
query = query.Where(r => r.MetricType == mt);
|
||||
|
||||
query = query.Where(r => r.RecordedAt >= DateTime.UtcNow.AddDays(-days));
|
||||
|
||||
var records = await query.OrderByDescending(r => r.RecordedAt).Take(30).Select(r => new
|
||||
{
|
||||
r.Id, Type = r.MetricType.ToString(), r.Systolic, r.Diastolic, r.Value, r.Unit, r.IsAbnormal, r.RecordedAt,
|
||||
}).ToListAsync();
|
||||
|
||||
return new { count = records.Count, records };
|
||||
}
|
||||
|
||||
private static async Task<object> ExecuteCheckArchive(AppDbContext db, Guid userId)
|
||||
{
|
||||
var archive = await db.HealthArchives.FirstOrDefaultAsync(a => a.UserId == userId);
|
||||
if (archive == null) return new { found = false };
|
||||
return new
|
||||
{
|
||||
found = true, archive.Diagnosis, archive.SurgeryType,
|
||||
SurgeryDate = archive.SurgeryDate?.ToString("yyyy-MM-dd"),
|
||||
archive.Allergies, archive.DietRestrictions, archive.ChronicDiseases, archive.FamilyHistory,
|
||||
};
|
||||
}
|
||||
|
||||
private static async Task<object> ExecuteManageMedication(AppDbContext db, Guid userId, JsonElement args)
|
||||
{
|
||||
var action = args.TryGetProperty("action", out var a) ? a.GetString()! : "query";
|
||||
return action switch
|
||||
{
|
||||
"create" => await CreateMedication(db, userId, args),
|
||||
"query" => await QueryMedications(db, userId),
|
||||
"confirm" => await ConfirmMedication(db, userId, args),
|
||||
_ => new { success = false, message = $"未知操作: {action}" }
|
||||
};
|
||||
}
|
||||
|
||||
private static async Task<object> CreateMedication(AppDbContext db, Guid userId, JsonElement args)
|
||||
{
|
||||
var med = new Medication
|
||||
{
|
||||
Id = Guid.NewGuid(), UserId = userId,
|
||||
Name = args.TryGetProperty("name", out var n) ? n.GetString()! : "",
|
||||
Dosage = args.TryGetProperty("dosage", out var dg) ? dg.GetString() : null,
|
||||
Source = MedicationSource.AiEntry, IsActive = true,
|
||||
};
|
||||
db.Medications.Add(med);
|
||||
await db.SaveChangesAsync();
|
||||
return new { success = true, medication_id = med.Id, med.Name };
|
||||
}
|
||||
|
||||
private static async Task<object> QueryMedications(AppDbContext db, Guid userId)
|
||||
{
|
||||
var meds = await db.Medications.Where(m => m.UserId == userId && m.IsActive)
|
||||
.Select(m => new { m.Id, m.Name, m.Dosage, m.TimeOfDay }).ToListAsync();
|
||||
return new { count = meds.Count, medications = meds };
|
||||
}
|
||||
|
||||
private static async Task<object> ConfirmMedication(AppDbContext db, Guid userId, JsonElement args)
|
||||
{
|
||||
var medId = args.TryGetProperty("medication_id", out var mid) ? mid.GetGuid() : Guid.Empty;
|
||||
db.MedicationLogs.Add(new MedicationLog
|
||||
{
|
||||
Id = Guid.NewGuid(), MedicationId = medId, UserId = userId,
|
||||
Status = MedicationLogStatus.Taken, ScheduledTime = TimeOnly.FromDateTime(DateTime.Now), ConfirmedAt = DateTime.UtcNow,
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
return new { success = true };
|
||||
}
|
||||
|
||||
private static async Task<object> ExecuteManageExercise(AppDbContext db, Guid userId, JsonElement args)
|
||||
{
|
||||
var action = args.TryGetProperty("action", out var a) ? a.GetString()! : "query";
|
||||
switch (action)
|
||||
{
|
||||
case "create":
|
||||
var weekStart = args.TryGetProperty("week_start_date", out var wsd) ? DateOnly.Parse(wsd.GetString()!) : DateOnly.FromDateTime(DateTime.Now);
|
||||
var plan = new ExercisePlan { Id = Guid.NewGuid(), UserId = userId, WeekStartDate = weekStart };
|
||||
if (args.TryGetProperty("items", out var items))
|
||||
{
|
||||
foreach (var item in items.EnumerateArray())
|
||||
{
|
||||
plan.Items.Add(new ExercisePlanItem
|
||||
{
|
||||
Id = Guid.NewGuid(), DayOfWeek = item.GetProperty("day_of_week").GetInt32(),
|
||||
ExerciseType = item.GetProperty("exercise_type").GetString() ?? "散步",
|
||||
DurationMinutes = item.GetProperty("duration_minutes").GetInt32(),
|
||||
IsRestDay = item.TryGetProperty("is_rest_day", out var rd) && rd.GetBoolean(),
|
||||
});
|
||||
}
|
||||
}
|
||||
db.ExercisePlans.Add(plan);
|
||||
await db.SaveChangesAsync();
|
||||
return new { success = true, plan_id = plan.Id };
|
||||
|
||||
case "checkin":
|
||||
var itemId = args.TryGetProperty("item_id", out var iid) ? iid.GetGuid() : Guid.Empty;
|
||||
var exerciseItem = await db.ExercisePlanItems.FindAsync([itemId]);
|
||||
if (exerciseItem == null) return new { success = false, message = "条目不存在" };
|
||||
exerciseItem.IsCompleted = true;
|
||||
exerciseItem.CompletedAt = DateTime.UtcNow;
|
||||
await db.SaveChangesAsync();
|
||||
return new { success = true };
|
||||
|
||||
default: // query
|
||||
var existingPlan = await db.ExercisePlans.Where(p => p.UserId == userId)
|
||||
.OrderByDescending(p => p.WeekStartDate).FirstOrDefaultAsync();
|
||||
if (existingPlan == null) return new { found = false };
|
||||
var exerciseItems = await db.ExercisePlanItems.Where(i => i.PlanId == existingPlan.Id).OrderBy(i => i.DayOfWeek).ToListAsync();
|
||||
return new { found = true, plan_id = existingPlan.Id, items = exerciseItems.Select(i => new { i.Id, i.DayOfWeek, i.ExerciseType, i.DurationMinutes, i.IsCompleted }) };
|
||||
}
|
||||
}
|
||||
// ── 患者上下文构建 ──
|
||||
|
||||
private static async Task<string> BuildPatientContext(AppDbContext db, Guid userId, CancellationToken ct)
|
||||
{
|
||||
@@ -542,61 +385,8 @@ public static class AiChatEndpoints
|
||||
_ => "—"
|
||||
};
|
||||
|
||||
// ---- Tool Definitions ----
|
||||
private static readonly ToolDefinition RecordHealthDataTool = new()
|
||||
{
|
||||
Function = new()
|
||||
{
|
||||
Name = "record_health_data", Description = "记录健康数据(血压/心率/血糖/血氧/体重)",
|
||||
Parameters = new { type = "object", properties = new { type = new { type = "string" }, systolic = new { type = "integer" }, diastolic = new { type = "integer" }, heart_rate = new { type = "number" }, glucose = new { type = "number" }, spo2 = new { type = "number" }, weight = new { type = "number" } }, required = new[] { "type" } }
|
||||
}
|
||||
};
|
||||
private static readonly ToolDefinition QueryHealthRecordsTool = new()
|
||||
{
|
||||
Function = new()
|
||||
{
|
||||
Name = "query_health_records", Description = "查询近期健康数据",
|
||||
Parameters = new { type = "object", properties = new { type = new { type = "string" }, days = new { type = "integer" } } }
|
||||
}
|
||||
};
|
||||
private static readonly ToolDefinition CheckArchiveTool = new()
|
||||
{
|
||||
Function = new() { Name = "check_archive", Description = "查询患者健康档案", Parameters = new { type = "object", properties = new { } } }
|
||||
};
|
||||
private static readonly ToolDefinition ManageMedicationTool = new()
|
||||
{
|
||||
Function = new()
|
||||
{
|
||||
Name = "manage_medication", Description = "用药管理",
|
||||
Parameters = new { type = "object", properties = new { action = new { type = "string" }, name = new { type = "string" }, dosage = new { type = "string" } }, required = new[] { "action" } }
|
||||
}
|
||||
};
|
||||
private static readonly ToolDefinition ManageExerciseTool = new()
|
||||
{
|
||||
Function = new()
|
||||
{
|
||||
Name = "manage_exercise", Description = "运动计划管理",
|
||||
Parameters = new { type = "object", properties = new { action = new { type = "string" } }, required = new[] { "action" } }
|
||||
}
|
||||
};
|
||||
private static readonly ToolDefinition EstimateFoodTool = new()
|
||||
{
|
||||
Function = new() { Name = "estimate_food_text", Description = "根据文字描述估算食物份量和热量", Parameters = new { type = "object", properties = new { text = new { type = "string" } }, required = new[] { "text" } } }
|
||||
};
|
||||
private static readonly ToolDefinition AnalyzeReportTool = new()
|
||||
{
|
||||
Function = new() { Name = "analyze_report", Description = "分析报告图片", Parameters = new { type = "object", properties = new { image_url = new { type = "string" } }, required = new[] { "image_url" } } }
|
||||
};
|
||||
private static readonly ToolDefinition RequestDoctorTool = new()
|
||||
{
|
||||
Function = new()
|
||||
{
|
||||
Name = "request_doctor", Description = "请求转接真人医生",
|
||||
Parameters = new { type = "object", properties = new { reason = new { type = "string" }, urgency_level = new { type = "string" } } }
|
||||
}
|
||||
};
|
||||
// ── 消息类型判断 ──
|
||||
|
||||
/// <summary>根据工具调用结果更新消息类型和元数据</summary>
|
||||
private static void _UpdateMessageTypeAndMetadata(string toolName, object toolResult, ref string messageType, ref Dictionary<string, object> metadata)
|
||||
{
|
||||
switch (toolName)
|
||||
@@ -630,7 +420,8 @@ public static class AiChatEndpoints
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>压缩图片到合理大小供 VLM API 使用</summary>
|
||||
// ── 图片处理 ──
|
||||
|
||||
private static void CompressImage(string inputPath, string outputPath, int maxWidth, long quality)
|
||||
{
|
||||
using var image = Image.FromFile(inputPath);
|
||||
@@ -653,5 +444,4 @@ public static class AiChatEndpoints
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>AI 对话请求</summary>
|
||||
public sealed record ChatRequest(string Message, string? ConversationId);
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
namespace Health.WebApi.Endpoints;
|
||||
|
||||
public static class ConsultationEndpoints
|
||||
{
|
||||
public static void MapConsultationEndpoints(this WebApplication app)
|
||||
{
|
||||
var group = app.MapGroup("/api").RequireAuthorization();
|
||||
|
||||
group.MapGet("/doctors", async (AppDbContext db) =>
|
||||
{
|
||||
var doctors = await db.Doctors.Where(d => d.IsActive).Select(d => new { d.Id, d.Name, d.Title, d.Department, d.Introduction }).ToListAsync();
|
||||
return Results.Ok(new { code = 0, data = doctors, message = (string?)null });
|
||||
});
|
||||
|
||||
group.MapGet("/consultations", async (HttpContext http, AppDbContext db) =>
|
||||
{
|
||||
var userId = GetUserId(http);
|
||||
var consultations = await db.Consultations.Where(c => c.UserId == userId).OrderByDescending(c => c.CreatedAt).ToListAsync();
|
||||
return Results.Ok(new { code = 0, data = consultations, message = (string?)null });
|
||||
});
|
||||
|
||||
group.MapPost("/consultations", async (CreateConsultationRequest req, HttpContext http, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var userId = GetUserId(http);
|
||||
var consultation = new Consultation
|
||||
{
|
||||
Id = Guid.NewGuid(), UserId = userId, DoctorId = req.DoctorId,
|
||||
Status = ConsultationStatus.AiTalking,
|
||||
Month = DateTime.UtcNow.Year * 100 + DateTime.UtcNow.Month,
|
||||
};
|
||||
db.Consultations.Add(consultation);
|
||||
await db.SaveChangesAsync(ct);
|
||||
return Results.Ok(new { code = 0, data = new { consultation.Id }, message = (string?)null });
|
||||
});
|
||||
|
||||
group.MapGet("/consultations/{id:guid}/messages", async (Guid id, string? after, HttpContext http, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var userId = GetUserId(http);
|
||||
var query = db.ConsultationMessages.Where(m => m.ConsultationId == id && m.Consultation.UserId == userId);
|
||||
if (Guid.TryParse(after, out var afterId))
|
||||
query = query.Where(m => m.Id.CompareTo(afterId) > 0);
|
||||
var messages = await query.OrderBy(m => m.CreatedAt).Take(50).Select(m => new { m.Id, SenderType = m.SenderType.ToString(), m.SenderName, m.Content, m.CreatedAt }).ToListAsync(ct);
|
||||
return Results.Ok(new { code = 0, data = messages, message = (string?)null });
|
||||
});
|
||||
|
||||
group.MapPost("/consultations/{id:guid}/messages", async (Guid id, SendMessageRequest req, HttpContext http, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var userId = GetUserId(http);
|
||||
var msg = new ConsultationMessage { Id = Guid.NewGuid(), ConsultationId = id, SenderType = ConsultationSenderType.User, Content = req.Content, SenderName = null, CreatedAt = DateTime.UtcNow };
|
||||
db.ConsultationMessages.Add(msg);
|
||||
await db.SaveChangesAsync(ct);
|
||||
return Results.Ok(new { code = 0, data = new { msg.Id }, message = (string?)null });
|
||||
});
|
||||
|
||||
group.MapGet("/user/consultation-quota", async (HttpContext http, AppDbContext db) =>
|
||||
{
|
||||
var userId = GetUserId(http);
|
||||
var now = DateTime.UtcNow;
|
||||
var currentPeriod = now.Year * 100 + now.Month;
|
||||
var used = await db.Consultations.CountAsync(c => c.UserId == userId && c.Month == currentPeriod);
|
||||
return Results.Ok(new { code = 0, data = new { total = 3, used, remaining = 3 - used }, message = (string?)null });
|
||||
});
|
||||
}
|
||||
|
||||
private static Guid GetUserId(HttpContext http) =>
|
||||
Guid.TryParse(http.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value, out var id) ? id : Guid.Empty;
|
||||
}
|
||||
|
||||
public sealed record CreateConsultationRequest(Guid DoctorId);
|
||||
public sealed record SendMessageRequest(string Content);
|
||||
49
backend/src/Health.WebApi/Endpoints/diet_endpoints.cs
Normal file
49
backend/src/Health.WebApi/Endpoints/diet_endpoints.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
namespace Health.WebApi.Endpoints;
|
||||
|
||||
public static class DietEndpoints
|
||||
{
|
||||
public static void MapDietEndpoints(this WebApplication app)
|
||||
{
|
||||
var group = app.MapGroup("/api/diet-records").RequireAuthorization();
|
||||
|
||||
group.MapGet("/", async (string? date, string? mealType, HttpContext http, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var userId = GetUserId(http);
|
||||
var query = db.DietRecords.Include(d => d.FoodItems).Where(d => d.UserId == userId);
|
||||
if (DateOnly.TryParse(date, out var d)) query = query.Where(r => r.RecordedAt == d);
|
||||
if (Enum.TryParse<MealType>(mealType, ignoreCase: true, out var mt)) query = query.Where(r => r.MealType == mt);
|
||||
var records = await query.OrderByDescending(r => r.RecordedAt).ToListAsync(ct);
|
||||
return Results.Ok(new { code = 0, data = records, message = (string?)null });
|
||||
});
|
||||
|
||||
group.MapPost("/", async (CreateDietRequest req, HttpContext http, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var userId = GetUserId(http);
|
||||
var record = new DietRecord
|
||||
{
|
||||
Id = Guid.NewGuid(), UserId = userId, MealType = req.MealType,
|
||||
TotalCalories = req.TotalCalories, HealthScore = req.HealthScore, RecordedAt = req.RecordedAt ?? DateOnly.FromDateTime(DateTime.Now),
|
||||
};
|
||||
if (req.FoodItems != null)
|
||||
foreach (var fi in req.FoodItems)
|
||||
record.FoodItems.Add(new DietFoodItem { Id = Guid.NewGuid(), Name = fi.Name, Portion = fi.Portion, Calories = fi.Calories, ProteinGrams = fi.ProteinGrams, CarbsGrams = fi.CarbsGrams, FatGrams = fi.FatGrams, Warning = fi.Warning, SortOrder = fi.SortOrder });
|
||||
db.DietRecords.Add(record);
|
||||
await db.SaveChangesAsync(ct);
|
||||
return Results.Ok(new { code = 0, data = new { record.Id }, message = (string?)null });
|
||||
});
|
||||
|
||||
group.MapDelete("/{id:guid}", async (Guid id, HttpContext http, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var userId = GetUserId(http);
|
||||
var record = await db.DietRecords.FirstOrDefaultAsync(r => r.Id == id && r.UserId == userId, ct);
|
||||
if (record != null) { db.DietRecords.Remove(record); await db.SaveChangesAsync(ct); }
|
||||
return Results.Ok(new { code = 0, data = new { success = true }, message = (string?)null });
|
||||
});
|
||||
}
|
||||
|
||||
private static Guid GetUserId(HttpContext http) =>
|
||||
Guid.TryParse(http.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value, out var id) ? id : Guid.Empty;
|
||||
}
|
||||
|
||||
public sealed record CreateDietRequest(MealType MealType, int? TotalCalories, int? HealthScore, DateOnly? RecordedAt, List<FoodItemDto>? FoodItems);
|
||||
public sealed record FoodItemDto(string Name, string? Portion, int? Calories, decimal? ProteinGrams, decimal? CarbsGrams, decimal? FatGrams, string? Warning, int SortOrder);
|
||||
69
backend/src/Health.WebApi/Endpoints/exercise_endpoints.cs
Normal file
69
backend/src/Health.WebApi/Endpoints/exercise_endpoints.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
namespace Health.WebApi.Endpoints;
|
||||
|
||||
public static class ExerciseEndpoints
|
||||
{
|
||||
public static void MapExerciseEndpoints(this WebApplication app)
|
||||
{
|
||||
var group = app.MapGroup("/api/exercise-plans").RequireAuthorization();
|
||||
|
||||
group.MapGet("/current", async (HttpContext http, AppDbContext db) =>
|
||||
{
|
||||
var userId = GetUserId(http);
|
||||
var today = DateOnly.FromDateTime(DateTime.Now);
|
||||
var monday = today.AddDays(-(int)today.DayOfWeek + 1);
|
||||
var plan = await db.ExercisePlans.Include(p => p.Items).FirstOrDefaultAsync(p => p.UserId == userId && p.WeekStartDate == monday);
|
||||
if (plan == null) return Results.Ok(new { code = 0, data = (object?)null, message = (string?)null });
|
||||
return Results.Ok(new
|
||||
{
|
||||
code = 0,
|
||||
data = new
|
||||
{
|
||||
plan.Id, plan.WeekStartDate, plan.CreatedAt, plan.UpdatedAt,
|
||||
items = plan.Items.Select(i => new
|
||||
{
|
||||
i.Id, i.DayOfWeek, i.ExerciseType, i.DurationMinutes,
|
||||
i.IsCompleted, i.CompletedAt, i.IsRestDay
|
||||
})
|
||||
},
|
||||
message = (string?)null
|
||||
});
|
||||
});
|
||||
|
||||
group.MapPost("/", async (CreateExercisePlanRequest req, HttpContext http, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var userId = GetUserId(http);
|
||||
var plan = new ExercisePlan { Id = Guid.NewGuid(), UserId = userId, WeekStartDate = req.WeekStartDate };
|
||||
if (req.Items != null)
|
||||
foreach (var item in req.Items)
|
||||
plan.Items.Add(new ExercisePlanItem { Id = Guid.NewGuid(), DayOfWeek = item.DayOfWeek, ExerciseType = item.ExerciseType, DurationMinutes = item.DurationMinutes, IsRestDay = item.IsRestDay });
|
||||
db.ExercisePlans.Add(plan);
|
||||
await db.SaveChangesAsync(ct);
|
||||
return Results.Ok(new { code = 0, data = new { plan.Id }, message = (string?)null });
|
||||
});
|
||||
|
||||
group.MapPost("/items/{itemId:guid}/checkin", async (Guid itemId, HttpContext http, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var item = await db.ExercisePlanItems.FindAsync([itemId], ct);
|
||||
if (item == null) return Results.Ok(new { code = 40004, message = "不存在" });
|
||||
item.IsCompleted = true; item.CompletedAt = DateTime.UtcNow;
|
||||
await db.SaveChangesAsync(ct);
|
||||
return Results.Ok(new { code = 0, data = new { success = true }, message = (string?)null });
|
||||
});
|
||||
}
|
||||
|
||||
private static Guid GetUserId(HttpContext http) =>
|
||||
Guid.TryParse(http.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value, out var id) ? id : Guid.Empty;
|
||||
}
|
||||
|
||||
public sealed class CreateExercisePlanRequest
|
||||
{
|
||||
public DateOnly WeekStartDate { get; init; }
|
||||
public List<ExerciseItemDto>? Items { get; init; }
|
||||
}
|
||||
public sealed class ExerciseItemDto
|
||||
{
|
||||
public int DayOfWeek { get; init; }
|
||||
public string ExerciseType { get; init; } = "";
|
||||
public int DurationMinutes { get; init; }
|
||||
public bool IsRestDay { get; init; }
|
||||
}
|
||||
30
backend/src/Health.WebApi/Endpoints/file_endpoints.cs
Normal file
30
backend/src/Health.WebApi/Endpoints/file_endpoints.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
namespace Health.WebApi.Endpoints;
|
||||
|
||||
public static class FileEndpoints
|
||||
{
|
||||
public static void MapFileEndpoints(this WebApplication app)
|
||||
{
|
||||
var group = app.MapGroup("/api/files").RequireAuthorization();
|
||||
|
||||
group.MapPost("/upload", async (HttpRequest request) =>
|
||||
{
|
||||
var form = await request.ReadFormAsync();
|
||||
var files = form.Files;
|
||||
var results = new List<object>();
|
||||
var uploadsDir = Path.Combine(Directory.GetCurrentDirectory(), "uploads");
|
||||
Directory.CreateDirectory(uploadsDir);
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
var fileId = Guid.NewGuid().ToString();
|
||||
var ext = Path.GetExtension(file.FileName);
|
||||
var filePath = Path.Combine(uploadsDir, $"{fileId}{ext}");
|
||||
using var stream = new FileStream(filePath, FileMode.Create);
|
||||
await file.CopyToAsync(stream);
|
||||
results.Add(new { id = fileId, name = file.FileName, size = file.Length });
|
||||
}
|
||||
|
||||
return Results.Ok(new { code = 0, data = results, message = (string?)null });
|
||||
});
|
||||
}
|
||||
}
|
||||
91
backend/src/Health.WebApi/Endpoints/medication_endpoints.cs
Normal file
91
backend/src/Health.WebApi/Endpoints/medication_endpoints.cs
Normal file
@@ -0,0 +1,91 @@
|
||||
namespace Health.WebApi.Endpoints;
|
||||
|
||||
public static class MedicationEndpoints
|
||||
{
|
||||
public static void MapMedicationEndpoints(this WebApplication app)
|
||||
{
|
||||
var group = app.MapGroup("/api/medications").RequireAuthorization();
|
||||
|
||||
group.MapGet("/", async (HttpContext http, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var userId = GetUserId(http);
|
||||
var meds = await db.Medications.Where(m => m.UserId == userId).OrderByDescending(m => m.CreatedAt).ToListAsync(ct);
|
||||
return Results.Ok(new { code = 0, data = meds, message = (string?)null });
|
||||
});
|
||||
|
||||
group.MapPost("/", async (CreateMedicationRequest req, HttpContext http, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var userId = GetUserId(http);
|
||||
var med = new Medication
|
||||
{
|
||||
Id = Guid.NewGuid(), UserId = userId, Name = req.Name, Dosage = req.Dosage,
|
||||
Frequency = req.Frequency, TimeOfDay = req.TimeOfDay ?? [],
|
||||
StartDate = req.StartDate, EndDate = req.EndDate, IsActive = true, Source = req.Source,
|
||||
};
|
||||
db.Medications.Add(med);
|
||||
await db.SaveChangesAsync(ct);
|
||||
return Results.Ok(new { code = 0, data = new { med.Id }, message = (string?)null });
|
||||
});
|
||||
|
||||
group.MapPut("/{id:guid}", async (Guid id, CreateMedicationRequest req, 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 = 40004, message = "不存在" });
|
||||
med.Name = req.Name; med.Dosage = req.Dosage; med.Frequency = req.Frequency;
|
||||
med.TimeOfDay = req.TimeOfDay ?? med.TimeOfDay; med.StartDate = req.StartDate; med.EndDate = req.EndDate;
|
||||
med.UpdatedAt = DateTime.UtcNow;
|
||||
await db.SaveChangesAsync(ct);
|
||||
return Results.Ok(new { code = 0, data = new { success = true }, message = (string?)null });
|
||||
});
|
||||
|
||||
group.MapDelete("/{id:guid}", 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) { db.Medications.Remove(med); await db.SaveChangesAsync(ct); }
|
||||
return Results.Ok(new { code = 0, data = new { success = true }, message = (string?)null });
|
||||
});
|
||||
|
||||
group.MapPost("/{id:guid}/confirm", async (Guid id, HttpContext http, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var userId = GetUserId(http);
|
||||
var log = new MedicationLog
|
||||
{
|
||||
Id = Guid.NewGuid(), MedicationId = id, UserId = userId,
|
||||
Status = MedicationLogStatus.Taken, ScheduledTime = TimeOnly.FromDateTime(DateTime.Now), ConfirmedAt = DateTime.UtcNow,
|
||||
};
|
||||
db.MedicationLogs.Add(log);
|
||||
await db.SaveChangesAsync(ct);
|
||||
return Results.Ok(new { code = 0, data = new { success = true }, message = (string?)null });
|
||||
});
|
||||
|
||||
group.MapGet("/reminders", async (HttpContext http, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var userId = GetUserId(http);
|
||||
var now = TimeOnly.FromDateTime(DateTime.Now);
|
||||
var windowEnd = now.AddHours(1);
|
||||
var meds = await db.Medications
|
||||
.Where(m => m.UserId == userId && m.IsActive && m.TimeOfDay != null)
|
||||
.ToListAsync(ct);
|
||||
var due = meds.Where(m => m.TimeOfDay!.Any(t => t >= now && t <= windowEnd)).ToList();
|
||||
|
||||
var today = DateOnly.FromDateTime(DateTime.Now);
|
||||
var dueMeds = new List<object>();
|
||||
foreach (var m in due)
|
||||
{
|
||||
var logged = await db.MedicationLogs.AnyAsync(l =>
|
||||
l.MedicationId == m.Id && l.CreatedAt >= today.ToDateTime(TimeOnly.MinValue) && l.Status == MedicationLogStatus.Taken, ct);
|
||||
if (!logged)
|
||||
dueMeds.Add(new { m.Id, m.Name, m.Dosage, m.TimeOfDay });
|
||||
}
|
||||
|
||||
return Results.Ok(new { code = 0, data = dueMeds, message = (string?)null });
|
||||
});
|
||||
}
|
||||
|
||||
private static Guid GetUserId(HttpContext http) =>
|
||||
Guid.TryParse(http.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value, out var id) ? id : Guid.Empty;
|
||||
}
|
||||
|
||||
public sealed record CreateMedicationRequest(string Name, string? Dosage, MedicationFrequency Frequency, List<TimeOnly>? TimeOfDay, DateOnly? StartDate, DateOnly? EndDate, MedicationSource Source);
|
||||
@@ -1,310 +0,0 @@
|
||||
namespace Health.WebApi.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// 饮食、用药、报告、问诊、运动、文件端点
|
||||
/// </summary>
|
||||
public static class RemainingEndpoints
|
||||
{
|
||||
public static void MapDietEndpoints(this WebApplication app)
|
||||
{
|
||||
var group = app.MapGroup("/api/diet-records").RequireAuthorization();
|
||||
|
||||
group.MapGet("/", async (string? date, string? mealType, HttpContext http, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var userId = GetUserId(http);
|
||||
var query = db.DietRecords.Include(d => d.FoodItems).Where(d => d.UserId == userId);
|
||||
if (DateOnly.TryParse(date, out var d)) query = query.Where(r => r.RecordedAt == d);
|
||||
if (Enum.TryParse<MealType>(mealType, ignoreCase: true, out var mt)) query = query.Where(r => r.MealType == mt);
|
||||
var records = await query.OrderByDescending(r => r.RecordedAt).ToListAsync(ct);
|
||||
return Results.Ok(new { code = 0, data = records, message = (string?)null });
|
||||
});
|
||||
|
||||
group.MapPost("/", async (CreateDietRequest req, HttpContext http, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var userId = GetUserId(http);
|
||||
var record = new DietRecord
|
||||
{
|
||||
Id = Guid.NewGuid(), UserId = userId, MealType = req.MealType,
|
||||
TotalCalories = req.TotalCalories, HealthScore = req.HealthScore, RecordedAt = req.RecordedAt ?? DateOnly.FromDateTime(DateTime.Now),
|
||||
};
|
||||
if (req.FoodItems != null)
|
||||
foreach (var fi in req.FoodItems)
|
||||
record.FoodItems.Add(new DietFoodItem { Id = Guid.NewGuid(), Name = fi.Name, Portion = fi.Portion, Calories = fi.Calories, ProteinGrams = fi.ProteinGrams, CarbsGrams = fi.CarbsGrams, FatGrams = fi.FatGrams, Warning = fi.Warning, SortOrder = fi.SortOrder });
|
||||
db.DietRecords.Add(record);
|
||||
await db.SaveChangesAsync(ct);
|
||||
return Results.Ok(new { code = 0, data = new { record.Id }, message = (string?)null });
|
||||
});
|
||||
|
||||
group.MapDelete("/{id:guid}", async (Guid id, HttpContext http, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var userId = GetUserId(http);
|
||||
var record = await db.DietRecords.FirstOrDefaultAsync(r => r.Id == id && r.UserId == userId, ct);
|
||||
if (record != null) { db.DietRecords.Remove(record); await db.SaveChangesAsync(ct); }
|
||||
return Results.Ok(new { code = 0, data = new { success = true }, message = (string?)null });
|
||||
});
|
||||
}
|
||||
|
||||
public static void MapMedicationEndpoints(this WebApplication app)
|
||||
{
|
||||
var group = app.MapGroup("/api/medications").RequireAuthorization();
|
||||
|
||||
group.MapGet("/", async (HttpContext http, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var userId = GetUserId(http);
|
||||
var meds = await db.Medications.Where(m => m.UserId == userId).OrderByDescending(m => m.CreatedAt).ToListAsync(ct);
|
||||
return Results.Ok(new { code = 0, data = meds, message = (string?)null });
|
||||
});
|
||||
|
||||
group.MapPost("/", async (CreateMedicationRequest req, HttpContext http, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var userId = GetUserId(http);
|
||||
var med = new Medication
|
||||
{
|
||||
Id = Guid.NewGuid(), UserId = userId, Name = req.Name, Dosage = req.Dosage,
|
||||
Frequency = req.Frequency, TimeOfDay = req.TimeOfDay ?? [],
|
||||
StartDate = req.StartDate, EndDate = req.EndDate, IsActive = true, Source = req.Source,
|
||||
};
|
||||
db.Medications.Add(med);
|
||||
await db.SaveChangesAsync(ct);
|
||||
return Results.Ok(new { code = 0, data = new { med.Id }, message = (string?)null });
|
||||
});
|
||||
|
||||
group.MapPut("/{id:guid}", async (Guid id, CreateMedicationRequest req, 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 = 40004, message = "不存在" });
|
||||
med.Name = req.Name; med.Dosage = req.Dosage; med.Frequency = req.Frequency;
|
||||
med.TimeOfDay = req.TimeOfDay ?? med.TimeOfDay; med.StartDate = req.StartDate; med.EndDate = req.EndDate;
|
||||
med.UpdatedAt = DateTime.UtcNow;
|
||||
await db.SaveChangesAsync(ct);
|
||||
return Results.Ok(new { code = 0, data = new { success = true }, message = (string?)null });
|
||||
});
|
||||
|
||||
group.MapDelete("/{id:guid}", 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) { db.Medications.Remove(med); await db.SaveChangesAsync(ct); }
|
||||
return Results.Ok(new { code = 0, data = new { success = true }, message = (string?)null });
|
||||
});
|
||||
|
||||
group.MapPost("/{id:guid}/confirm", async (Guid id, HttpContext http, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var userId = GetUserId(http);
|
||||
var log = new MedicationLog
|
||||
{
|
||||
Id = Guid.NewGuid(), MedicationId = id, UserId = userId,
|
||||
Status = MedicationLogStatus.Taken, ScheduledTime = TimeOnly.FromDateTime(DateTime.Now), ConfirmedAt = DateTime.UtcNow,
|
||||
};
|
||||
db.MedicationLogs.Add(log);
|
||||
await db.SaveChangesAsync(ct);
|
||||
return Results.Ok(new { code = 0, data = new { success = true }, message = (string?)null });
|
||||
});
|
||||
|
||||
// 获取待提醒的用药
|
||||
group.MapGet("/reminders", async (HttpContext http, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var userId = GetUserId(http);
|
||||
var now = TimeOnly.FromDateTime(DateTime.Now);
|
||||
var windowEnd = now.AddHours(1);
|
||||
var meds = await db.Medications
|
||||
.Where(m => m.UserId == userId && m.IsActive && m.TimeOfDay != null)
|
||||
.ToListAsync(ct);
|
||||
var due = meds.Where(m => m.TimeOfDay!.Any(t => t >= now && t <= windowEnd)).ToList();
|
||||
|
||||
// 检查今天是否已打卡
|
||||
var today = DateOnly.FromDateTime(DateTime.Now);
|
||||
var dueMeds = new List<object>();
|
||||
foreach (var m in due)
|
||||
{
|
||||
var logged = await db.MedicationLogs.AnyAsync(l =>
|
||||
l.MedicationId == m.Id && l.CreatedAt >= today.ToDateTime(TimeOnly.MinValue) && l.Status == MedicationLogStatus.Taken, ct);
|
||||
if (!logged)
|
||||
dueMeds.Add(new { m.Id, m.Name, m.Dosage, m.TimeOfDay });
|
||||
}
|
||||
|
||||
return Results.Ok(new { code = 0, data = dueMeds, message = (string?)null });
|
||||
});
|
||||
}
|
||||
|
||||
public static void MapReportEndpoints(this WebApplication app)
|
||||
{
|
||||
var group = app.MapGroup("/api/reports").RequireAuthorization();
|
||||
|
||||
group.MapGet("/", async (HttpContext http, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var userId = GetUserId(http);
|
||||
var reports = await db.Reports.Where(r => r.UserId == userId).OrderByDescending(r => r.CreatedAt).ToListAsync(ct);
|
||||
return Results.Ok(new { code = 0, data = reports, message = (string?)null });
|
||||
});
|
||||
|
||||
group.MapGet("/{id:guid}", async (Guid id, HttpContext http, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var userId = GetUserId(http);
|
||||
var report = await db.Reports.FirstOrDefaultAsync(r => r.Id == id && r.UserId == userId, ct);
|
||||
return report == null ? Results.Ok(new { code = 40004, message = "不存在" }) : Results.Ok(new { code = 0, data = report, message = (string?)null });
|
||||
});
|
||||
}
|
||||
|
||||
public static void MapConsultationEndpoints(this WebApplication app)
|
||||
{
|
||||
var group = app.MapGroup("/api").RequireAuthorization();
|
||||
|
||||
group.MapGet("/doctors", async (AppDbContext db) =>
|
||||
{
|
||||
var doctors = await db.Doctors.Where(d => d.IsActive).Select(d => new { d.Id, d.Name, d.Title, d.Department, d.Introduction }).ToListAsync();
|
||||
return Results.Ok(new { code = 0, data = doctors, message = (string?)null });
|
||||
});
|
||||
|
||||
group.MapGet("/consultations", async (HttpContext http, AppDbContext db) =>
|
||||
{
|
||||
var userId = GetUserId(http);
|
||||
var consultations = await db.Consultations.Where(c => c.UserId == userId).OrderByDescending(c => c.CreatedAt).ToListAsync();
|
||||
return Results.Ok(new { code = 0, data = consultations, message = (string?)null });
|
||||
});
|
||||
|
||||
group.MapPost("/consultations", async (CreateConsultationRequest req, HttpContext http, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var userId = GetUserId(http);
|
||||
var consultation = new Consultation
|
||||
{
|
||||
Id = Guid.NewGuid(), UserId = userId, DoctorId = req.DoctorId,
|
||||
Status = ConsultationStatus.AiTalking,
|
||||
Month = DateTime.UtcNow.Year * 100 + DateTime.UtcNow.Month,
|
||||
};
|
||||
db.Consultations.Add(consultation);
|
||||
await db.SaveChangesAsync(ct);
|
||||
return Results.Ok(new { code = 0, data = new { consultation.Id }, message = (string?)null });
|
||||
});
|
||||
|
||||
group.MapGet("/consultations/{id:guid}/messages", async (Guid id, string? after, HttpContext http, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var userId = GetUserId(http);
|
||||
var query = db.ConsultationMessages.Where(m => m.ConsultationId == id && m.Consultation.UserId == userId);
|
||||
if (Guid.TryParse(after, out var afterId))
|
||||
query = query.Where(m => m.Id.CompareTo(afterId) > 0);
|
||||
var messages = await query.OrderBy(m => m.CreatedAt).Take(50).Select(m => new { m.Id, SenderType = m.SenderType.ToString(), m.SenderName, m.Content, m.CreatedAt }).ToListAsync(ct);
|
||||
return Results.Ok(new { code = 0, data = messages, message = (string?)null });
|
||||
});
|
||||
|
||||
group.MapPost("/consultations/{id:guid}/messages", async (Guid id, SendMessageRequest req, HttpContext http, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var userId = GetUserId(http);
|
||||
var msg = new ConsultationMessage { Id = Guid.NewGuid(), ConsultationId = id, SenderType = ConsultationSenderType.User, Content = req.Content, SenderName = null, CreatedAt = DateTime.UtcNow };
|
||||
db.ConsultationMessages.Add(msg);
|
||||
await db.SaveChangesAsync(ct);
|
||||
return Results.Ok(new { code = 0, data = new { msg.Id }, message = (string?)null });
|
||||
});
|
||||
|
||||
group.MapGet("/user/consultation-quota", async (HttpContext http, AppDbContext db) =>
|
||||
{
|
||||
var userId = GetUserId(http);
|
||||
var now = DateTime.UtcNow;
|
||||
// 用年月组合值避免跨年问题:202601=2026年1月
|
||||
var currentPeriod = now.Year * 100 + now.Month;
|
||||
var used = await db.Consultations.CountAsync(c => c.UserId == userId && c.Month == currentPeriod);
|
||||
return Results.Ok(new { code = 0, data = new { total = 3, used, remaining = 3 - used }, message = (string?)null });
|
||||
});
|
||||
}
|
||||
|
||||
public static void MapExerciseEndpoints(this WebApplication app)
|
||||
{
|
||||
var group = app.MapGroup("/api/exercise-plans").RequireAuthorization();
|
||||
|
||||
group.MapGet("/current", async (HttpContext http, AppDbContext db) =>
|
||||
{
|
||||
var userId = GetUserId(http);
|
||||
var today = DateOnly.FromDateTime(DateTime.Now);
|
||||
var monday = today.AddDays(-(int)today.DayOfWeek + 1);
|
||||
var plan = await db.ExercisePlans.Include(p => p.Items).FirstOrDefaultAsync(p => p.UserId == userId && p.WeekStartDate == monday);
|
||||
if (plan == null) return Results.Ok(new { code = 0, data = (object?)null, message = (string?)null });
|
||||
return Results.Ok(new
|
||||
{
|
||||
code = 0,
|
||||
data = new
|
||||
{
|
||||
plan.Id, plan.WeekStartDate, plan.CreatedAt, plan.UpdatedAt,
|
||||
items = plan.Items.Select(i => new
|
||||
{
|
||||
i.Id, i.DayOfWeek, i.ExerciseType, i.DurationMinutes,
|
||||
i.IsCompleted, i.CompletedAt, i.IsRestDay
|
||||
})
|
||||
},
|
||||
message = (string?)null
|
||||
});
|
||||
});
|
||||
|
||||
group.MapPost("/", async (CreateExercisePlanRequest req, HttpContext http, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var userId = GetUserId(http);
|
||||
var plan = new ExercisePlan { Id = Guid.NewGuid(), UserId = userId, WeekStartDate = req.WeekStartDate };
|
||||
if (req.Items != null)
|
||||
foreach (var item in req.Items)
|
||||
plan.Items.Add(new ExercisePlanItem { Id = Guid.NewGuid(), DayOfWeek = item.DayOfWeek, ExerciseType = item.ExerciseType, DurationMinutes = item.DurationMinutes, IsRestDay = item.IsRestDay });
|
||||
db.ExercisePlans.Add(plan);
|
||||
await db.SaveChangesAsync(ct);
|
||||
return Results.Ok(new { code = 0, data = new { plan.Id }, message = (string?)null });
|
||||
});
|
||||
|
||||
group.MapPost("/items/{itemId:guid}/checkin", async (Guid itemId, HttpContext http, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var item = await db.ExercisePlanItems.FindAsync([itemId], ct);
|
||||
if (item == null) return Results.Ok(new { code = 40004, message = "不存在" });
|
||||
item.IsCompleted = true; item.CompletedAt = DateTime.UtcNow;
|
||||
await db.SaveChangesAsync(ct);
|
||||
return Results.Ok(new { code = 0, data = new { success = true }, message = (string?)null });
|
||||
});
|
||||
}
|
||||
|
||||
public static void MapFileEndpoints(this WebApplication app)
|
||||
{
|
||||
var group = app.MapGroup("/api/files").RequireAuthorization();
|
||||
|
||||
group.MapPost("/upload", async (HttpRequest request) =>
|
||||
{
|
||||
var form = await request.ReadFormAsync();
|
||||
var files = form.Files;
|
||||
var results = new List<object>();
|
||||
var uploadsDir = Path.Combine(Directory.GetCurrentDirectory(), "uploads");
|
||||
Directory.CreateDirectory(uploadsDir);
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
var fileId = Guid.NewGuid().ToString();
|
||||
var ext = Path.GetExtension(file.FileName);
|
||||
var filePath = Path.Combine(uploadsDir, $"{fileId}{ext}");
|
||||
using var stream = new FileStream(filePath, FileMode.Create);
|
||||
await file.CopyToAsync(stream);
|
||||
results.Add(new { id = fileId, name = file.FileName, size = file.Length });
|
||||
}
|
||||
|
||||
return Results.Ok(new { code = 0, data = results, message = (string?)null });
|
||||
});
|
||||
}
|
||||
|
||||
private static Guid GetUserId(HttpContext http) =>
|
||||
Guid.TryParse(http.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value, out var id) ? id : Guid.Empty;
|
||||
}
|
||||
|
||||
// ---- 请求 DTO ----
|
||||
public sealed record CreateDietRequest(MealType MealType, int? TotalCalories, int? HealthScore, DateOnly? RecordedAt, List<FoodItemDto>? FoodItems);
|
||||
public sealed record FoodItemDto(string Name, string? Portion, int? Calories, decimal? ProteinGrams, decimal? CarbsGrams, decimal? FatGrams, string? Warning, int SortOrder);
|
||||
|
||||
public sealed record CreateMedicationRequest(string Name, string? Dosage, MedicationFrequency Frequency, List<TimeOnly>? TimeOfDay, DateOnly? StartDate, DateOnly? EndDate, MedicationSource Source);
|
||||
|
||||
public sealed record CreateConsultationRequest(Guid DoctorId);
|
||||
public sealed record SendMessageRequest(string Content);
|
||||
|
||||
public sealed class CreateExercisePlanRequest
|
||||
{
|
||||
public DateOnly WeekStartDate { get; init; }
|
||||
public List<ExerciseItemDto>? Items { get; init; }
|
||||
}
|
||||
public sealed class ExerciseItemDto
|
||||
{
|
||||
public int DayOfWeek { get; init; }
|
||||
public string ExerciseType { get; init; } = "";
|
||||
public int DurationMinutes { get; init; }
|
||||
public bool IsRestDay { get; init; }
|
||||
}
|
||||
26
backend/src/Health.WebApi/Endpoints/report_endpoints.cs
Normal file
26
backend/src/Health.WebApi/Endpoints/report_endpoints.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
namespace Health.WebApi.Endpoints;
|
||||
|
||||
public static class ReportEndpoints
|
||||
{
|
||||
public static void MapReportEndpoints(this WebApplication app)
|
||||
{
|
||||
var group = app.MapGroup("/api/reports").RequireAuthorization();
|
||||
|
||||
group.MapGet("/", async (HttpContext http, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var userId = GetUserId(http);
|
||||
var reports = await db.Reports.Where(r => r.UserId == userId).OrderByDescending(r => r.CreatedAt).ToListAsync(ct);
|
||||
return Results.Ok(new { code = 0, data = reports, message = (string?)null });
|
||||
});
|
||||
|
||||
group.MapGet("/{id:guid}", async (Guid id, HttpContext http, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var userId = GetUserId(http);
|
||||
var report = await db.Reports.FirstOrDefaultAsync(r => r.Id == id && r.UserId == userId, ct);
|
||||
return report == null ? Results.Ok(new { code = 40004, message = "不存在" }) : Results.Ok(new { code = 0, data = report, message = (string?)null });
|
||||
});
|
||||
}
|
||||
|
||||
private static Guid GetUserId(HttpContext http) =>
|
||||
Guid.TryParse(http.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value, out var id) ? id : Guid.Empty;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
namespace Health.Tests;
|
||||
|
||||
public class UnitTest1
|
||||
{
|
||||
[Fact]
|
||||
public void Test1()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user