feat: 医生端Web后台 + SignalR实时问诊
- 新增 doctor_web/ React前端 (dashboard/患者/问诊/报告/随访) - 后端新增 doctor_endpoints (14个医生API) + ConsultationHub (SignalR) - Flutter端 SignalR 替换轮询实现实时聊天
This commit is contained in:
502
backend/src/Health.WebApi/Endpoints/doctor_endpoints.cs
Normal file
502
backend/src/Health.WebApi/Endpoints/doctor_endpoints.cs
Normal file
@@ -0,0 +1,502 @@
|
||||
using Health.Domain.Entities;
|
||||
using Health.Domain.Enums;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Health.WebApi.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// 医生端 API(开发阶段 hardcode 王建国为当前医生)
|
||||
/// </summary>
|
||||
public static class DoctorEndpoints
|
||||
{
|
||||
private static async Task<Doctor> GetDoctor(AppDbContext db)
|
||||
{
|
||||
return await db.Doctors.FirstAsync(d => d.Name == "王建国" && d.IsActive);
|
||||
}
|
||||
|
||||
public static void MapDoctorEndpoints(this WebApplication app)
|
||||
{
|
||||
var group = app.MapGroup("/api/doctor");
|
||||
|
||||
// ===== 工作台 =====
|
||||
group.MapGet("/dashboard", async (AppDbContext db) =>
|
||||
{
|
||||
var doctor = await GetDoctor(db);
|
||||
var totalPatients = await db.Users.CountAsync();
|
||||
var activeConsultations = await db.Consultations.CountAsync(c => c.DoctorId == doctor.Id && c.Status != ConsultationStatus.Closed);
|
||||
var pendingReports = await db.Reports.CountAsync(r => r.Status == ReportStatus.PendingDoctor);
|
||||
var todayStart = DateTime.UtcNow.Date;
|
||||
var todayEnd = todayStart.AddDays(1);
|
||||
var todayFollowUps = await db.FollowUps.CountAsync(f => f.ScheduledAt >= todayStart && f.ScheduledAt < todayEnd && f.Status == FollowUpStatus.Upcoming);
|
||||
|
||||
return Results.Ok(new
|
||||
{
|
||||
code = 0,
|
||||
data = new
|
||||
{
|
||||
doctorName = doctor.Name,
|
||||
doctorTitle = doctor.Title,
|
||||
doctorDepartment = doctor.Department,
|
||||
totalPatients,
|
||||
activeConsultations,
|
||||
pendingReports,
|
||||
todayFollowUps
|
||||
},
|
||||
message = (string?)null
|
||||
});
|
||||
});
|
||||
|
||||
// ===== 患者列表 =====
|
||||
group.MapGet("/patients", async (string? search, AppDbContext db) =>
|
||||
{
|
||||
var query = db.Users.AsQueryable();
|
||||
if (!string.IsNullOrWhiteSpace(search))
|
||||
{
|
||||
query = query.Where(u =>
|
||||
(u.Name != null && u.Name.Contains(search)) ||
|
||||
u.Phone.Contains(search));
|
||||
}
|
||||
var patients = await query.OrderByDescending(u => u.CreatedAt).Select(u => new
|
||||
{
|
||||
u.Id,
|
||||
u.Phone,
|
||||
u.Name,
|
||||
u.Gender,
|
||||
u.BirthDate,
|
||||
hasArchive = u.HealthArchive != null
|
||||
}).ToListAsync();
|
||||
|
||||
return Results.Ok(new { code = 0, data = patients, message = (string?)null });
|
||||
});
|
||||
|
||||
// ===== 患者详情 =====
|
||||
group.MapGet("/patients/{id:guid}", async (Guid id, AppDbContext db) =>
|
||||
{
|
||||
var user = await db.Users
|
||||
.Include(u => u.HealthArchive)
|
||||
.FirstOrDefaultAsync(u => u.Id == id);
|
||||
if (user == null)
|
||||
return Results.Ok(new { code = 404, data = (object?)null, message = "患者不存在" });
|
||||
|
||||
// 最新健康指标
|
||||
var latestRecords = await db.HealthRecords
|
||||
.Where(r => r.UserId == id)
|
||||
.GroupBy(r => r.MetricType)
|
||||
.Select(g => g.OrderByDescending(r => r.RecordedAt).First())
|
||||
.ToListAsync();
|
||||
|
||||
// 最近30天健康数据(用于趋势图)
|
||||
var thirtyDaysAgo = DateTime.UtcNow.AddDays(-30);
|
||||
var trendRecords = await db.HealthRecords
|
||||
.Where(r => r.UserId == id && r.RecordedAt >= thirtyDaysAgo)
|
||||
.OrderBy(r => r.RecordedAt)
|
||||
.Select(r => new
|
||||
{
|
||||
r.Id,
|
||||
MetricType = r.MetricType.ToString(),
|
||||
r.Systolic,
|
||||
r.Diastolic,
|
||||
r.Value,
|
||||
r.Unit,
|
||||
r.RecordedAt,
|
||||
r.IsAbnormal
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
// 用药
|
||||
var medications = await db.Medications
|
||||
.Where(m => m.UserId == id && m.IsActive)
|
||||
.OrderByDescending(m => m.StartDate)
|
||||
.Select(m => new { m.Id, m.Name, m.Dosage, Frequency = m.Frequency.ToString(), m.TimeOfDay, m.StartDate, m.EndDate })
|
||||
.ToListAsync();
|
||||
|
||||
// 最近饮食
|
||||
var dietRecords = await db.DietRecords
|
||||
.Where(d => d.UserId == id)
|
||||
.OrderByDescending(d => d.RecordedAt)
|
||||
.Take(7)
|
||||
.Include(d => d.FoodItems)
|
||||
.ToListAsync();
|
||||
|
||||
// 运动计划
|
||||
var exercisePlans = await db.ExercisePlans
|
||||
.Where(p => p.UserId == id)
|
||||
.OrderByDescending(p => p.WeekStartDate)
|
||||
.Take(1)
|
||||
.Include(p => p.Items)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
// 报告
|
||||
var reports = await db.Reports
|
||||
.Where(r => r.UserId == id)
|
||||
.OrderByDescending(r => r.CreatedAt)
|
||||
.Select(r => new
|
||||
{
|
||||
r.Id,
|
||||
r.FileUrl,
|
||||
FileType = r.FileType.ToString(),
|
||||
Category = r.Category.ToString(),
|
||||
Status = r.Status.ToString(),
|
||||
r.AiSummary,
|
||||
r.AiIndicators,
|
||||
r.DoctorComment,
|
||||
r.DoctorName,
|
||||
r.ReviewedAt,
|
||||
r.CreatedAt
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
// 随访
|
||||
var followUps = await db.FollowUps
|
||||
.Where(f => f.UserId == id)
|
||||
.OrderByDescending(f => f.ScheduledAt)
|
||||
.Select(f => new
|
||||
{
|
||||
f.Id,
|
||||
f.Title,
|
||||
f.DoctorName,
|
||||
f.Department,
|
||||
f.ScheduledAt,
|
||||
f.Notes,
|
||||
Status = f.Status.ToString(),
|
||||
f.CreatedAt
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
return Results.Ok(new
|
||||
{
|
||||
code = 0,
|
||||
data = new
|
||||
{
|
||||
profile = new
|
||||
{
|
||||
user.Id,
|
||||
user.Phone,
|
||||
user.Name,
|
||||
user.Gender,
|
||||
BirthDate = user.BirthDate?.ToString("yyyy-MM-dd"),
|
||||
user.AvatarUrl
|
||||
},
|
||||
archive = user.HealthArchive == null ? null : new
|
||||
{
|
||||
user.HealthArchive.Diagnosis,
|
||||
user.HealthArchive.SurgeryType,
|
||||
SurgeryDate = user.HealthArchive.SurgeryDate?.ToString("yyyy-MM-dd"),
|
||||
user.HealthArchive.Allergies,
|
||||
user.HealthArchive.DietRestrictions,
|
||||
user.HealthArchive.ChronicDiseases,
|
||||
user.HealthArchive.FamilyHistory
|
||||
},
|
||||
latestRecords,
|
||||
trendRecords,
|
||||
medications,
|
||||
dietRecords = dietRecords.Select(d => new
|
||||
{
|
||||
d.Id,
|
||||
MealType = d.MealType.ToString(),
|
||||
d.TotalCalories,
|
||||
d.HealthScore,
|
||||
d.RecordedAt,
|
||||
foods = d.FoodItems.Select(f => new { f.Name, f.Portion, f.Calories, f.Warning })
|
||||
}),
|
||||
exercisePlan = exercisePlans == null ? null : new
|
||||
{
|
||||
exercisePlans.WeekStartDate,
|
||||
items = exercisePlans.Items.Select(i => new
|
||||
{
|
||||
i.DayOfWeek,
|
||||
i.ExerciseType,
|
||||
i.DurationMinutes,
|
||||
i.IsCompleted,
|
||||
i.IsRestDay,
|
||||
i.CompletedAt
|
||||
})
|
||||
},
|
||||
reports,
|
||||
followUps
|
||||
},
|
||||
message = (string?)null
|
||||
});
|
||||
});
|
||||
|
||||
// ===== 问诊列表 =====
|
||||
group.MapGet("/consultations", async (AppDbContext db) =>
|
||||
{
|
||||
var doctor = await GetDoctor(db);
|
||||
var consultations = await db.Consultations
|
||||
.Where(c => c.DoctorId == doctor.Id)
|
||||
.OrderByDescending(c => c.CreatedAt)
|
||||
.Select(c => new
|
||||
{
|
||||
c.Id,
|
||||
c.UserId,
|
||||
PatientName = c.User.Name,
|
||||
PatientPhone = c.User.Phone,
|
||||
Status = c.Status.ToString(),
|
||||
c.CreatedAt,
|
||||
c.ClosedAt,
|
||||
LastMessage = c.Messages.OrderByDescending(m => m.CreatedAt).Select(m => new { m.Content, m.SenderType, m.CreatedAt }).FirstOrDefault()
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
return Results.Ok(new { code = 0, data = consultations, message = (string?)null });
|
||||
});
|
||||
|
||||
// ===== 问诊消息 =====
|
||||
group.MapGet("/consultations/{id:guid}/messages", async (Guid id, AppDbContext db) =>
|
||||
{
|
||||
var messages = await db.ConsultationMessages
|
||||
.Where(m => m.ConsultationId == id)
|
||||
.OrderBy(m => m.CreatedAt)
|
||||
.Select(m => new
|
||||
{
|
||||
m.Id,
|
||||
SenderType = m.SenderType.ToString(),
|
||||
m.SenderName,
|
||||
m.Content,
|
||||
m.CreatedAt
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
var consultation = await db.Consultations.FirstOrDefaultAsync(c => c.Id == id);
|
||||
|
||||
return Results.Ok(new
|
||||
{
|
||||
code = 0,
|
||||
data = new
|
||||
{
|
||||
status = consultation?.Status.ToString() ?? "Closed",
|
||||
messages
|
||||
},
|
||||
message = (string?)null
|
||||
});
|
||||
});
|
||||
|
||||
// ===== 医生发送消息(同时通过 SignalR 广播)=====
|
||||
group.MapPost("/consultations/{id:guid}/messages", async (Guid id, HttpContext http, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var doctor = await GetDoctor(db);
|
||||
var consultation = await db.Consultations.FirstOrDefaultAsync(c => c.Id == id, ct);
|
||||
if (consultation == null)
|
||||
return Results.Ok(new { code = 404, data = (object?)null, message = "问诊不存在" });
|
||||
|
||||
// 读请求 body
|
||||
using var reader = new StreamReader(http.Request.Body);
|
||||
var body = await reader.ReadToEndAsync(ct);
|
||||
var json = System.Text.Json.JsonDocument.Parse(body);
|
||||
var content = json.RootElement.GetProperty("content").GetString() ?? "";
|
||||
|
||||
// 保存消息
|
||||
var msg = new ConsultationMessage
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
ConsultationId = id,
|
||||
SenderType = ConsultationSenderType.Doctor,
|
||||
SenderName = $"医生 · {doctor.Name}",
|
||||
Content = content,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
db.ConsultationMessages.Add(msg);
|
||||
|
||||
// 更新问诊状态
|
||||
if (consultation.Status == ConsultationStatus.AiTalking || consultation.Status == ConsultationStatus.WaitingDoctor)
|
||||
{
|
||||
consultation.Status = ConsultationStatus.DoctorReplied;
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync(ct);
|
||||
|
||||
// 通过 SignalR 广播(如果 Hub 已注册)
|
||||
var hubContext = http.RequestServices.GetService<Microsoft.AspNetCore.SignalR.IHubContext<Hubs.ConsultationHub>>();
|
||||
if (hubContext != null)
|
||||
{
|
||||
await hubContext.Clients.Group(id.ToString()).SendAsync("ReceiveMessage", new
|
||||
{
|
||||
msg.Id,
|
||||
consultationId = id.ToString(),
|
||||
senderType = "Doctor",
|
||||
senderName = msg.SenderName,
|
||||
content,
|
||||
msg.CreatedAt
|
||||
});
|
||||
}
|
||||
|
||||
return Results.Ok(new { code = 0, data = new { msg.Id }, message = (string?)null });
|
||||
});
|
||||
|
||||
// ===== 报告列表 =====
|
||||
group.MapGet("/reports", async (string? status, AppDbContext db) =>
|
||||
{
|
||||
var query = db.Reports.AsQueryable();
|
||||
if (!string.IsNullOrWhiteSpace(status) && Enum.TryParse<ReportStatus>(status, out var s))
|
||||
query = query.Where(r => r.Status == s);
|
||||
|
||||
var reports = await query
|
||||
.OrderByDescending(r => r.CreatedAt)
|
||||
.Select(r => new
|
||||
{
|
||||
r.Id,
|
||||
r.UserId,
|
||||
PatientName = r.User.Name,
|
||||
r.FileUrl,
|
||||
FileType = r.FileType.ToString(),
|
||||
Category = r.Category.ToString(),
|
||||
Status = r.Status.ToString(),
|
||||
r.AiSummary,
|
||||
r.DoctorComment,
|
||||
r.ReviewedAt,
|
||||
r.CreatedAt
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
return Results.Ok(new { code = 0, data = reports, message = (string?)null });
|
||||
});
|
||||
|
||||
// ===== 报告详情 =====
|
||||
group.MapGet("/reports/{id:guid}", async (Guid id, AppDbContext db) =>
|
||||
{
|
||||
var report = await db.Reports
|
||||
.Where(r => r.Id == id)
|
||||
.Select(r => new
|
||||
{
|
||||
r.Id,
|
||||
r.UserId,
|
||||
PatientName = r.User.Name,
|
||||
r.FileUrl,
|
||||
FileType = r.FileType.ToString(),
|
||||
Category = r.Category.ToString(),
|
||||
Status = r.Status.ToString(),
|
||||
r.AiSummary,
|
||||
r.AiIndicators,
|
||||
r.DoctorComment,
|
||||
r.DoctorName,
|
||||
r.ReviewedAt,
|
||||
r.CreatedAt
|
||||
})
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (report == null)
|
||||
return Results.Ok(new { code = 404, data = (object?)null, message = "报告不存在" });
|
||||
|
||||
return Results.Ok(new { code = 0, data = report, message = (string?)null });
|
||||
});
|
||||
|
||||
// ===== 审阅报告 =====
|
||||
group.MapPost("/reports/{id:guid}/review", async (Guid id, HttpContext http, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var doctor = await GetDoctor(db);
|
||||
var report = await db.Reports.FirstOrDefaultAsync(r => r.Id == id, ct);
|
||||
if (report == null)
|
||||
return Results.Ok(new { code = 404, data = (object?)null, message = "报告不存在" });
|
||||
|
||||
using var reader = new StreamReader(http.Request.Body);
|
||||
var body = await reader.ReadToEndAsync(ct);
|
||||
var json = System.Text.Json.JsonDocument.Parse(body);
|
||||
var comment = json.RootElement.GetProperty("comment").GetString() ?? "";
|
||||
|
||||
report.DoctorComment = comment;
|
||||
report.DoctorName = doctor.Name;
|
||||
report.ReviewedAt = DateTime.UtcNow;
|
||||
report.Status = ReportStatus.DoctorReviewed;
|
||||
|
||||
await db.SaveChangesAsync(ct);
|
||||
|
||||
return Results.Ok(new { code = 0, data = new { success = true }, message = (string?)null });
|
||||
});
|
||||
|
||||
// ===== 随访列表 =====
|
||||
group.MapGet("/follow-ups", async (string? status, string? patientId, AppDbContext db) =>
|
||||
{
|
||||
var query = db.FollowUps.AsQueryable();
|
||||
if (!string.IsNullOrWhiteSpace(status) && Enum.TryParse<FollowUpStatus>(status, out var s))
|
||||
query = query.Where(f => f.Status == s);
|
||||
if (Guid.TryParse(patientId, out var pid))
|
||||
query = query.Where(f => f.UserId == pid);
|
||||
|
||||
var followUps = await query
|
||||
.OrderByDescending(f => f.ScheduledAt)
|
||||
.Select(f => new
|
||||
{
|
||||
f.Id,
|
||||
f.UserId,
|
||||
PatientName = f.User.Name,
|
||||
f.Title,
|
||||
f.DoctorName,
|
||||
f.Department,
|
||||
f.ScheduledAt,
|
||||
f.Notes,
|
||||
Status = f.Status.ToString(),
|
||||
f.CreatedAt
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
return Results.Ok(new { code = 0, data = followUps, message = (string?)null });
|
||||
});
|
||||
|
||||
// ===== 创建随访 =====
|
||||
group.MapPost("/follow-ups", async (HttpContext http, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var doctor = await GetDoctor(db);
|
||||
using var reader = new StreamReader(http.Request.Body);
|
||||
var body = await reader.ReadToEndAsync(ct);
|
||||
var json = System.Text.Json.JsonDocument.Parse(body);
|
||||
|
||||
var followUp = new FollowUp
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = Guid.Parse(json.RootElement.GetProperty("userId").GetString()!),
|
||||
Title = json.RootElement.GetProperty("title").GetString() ?? "",
|
||||
DoctorName = doctor.Name,
|
||||
Department = doctor.Department,
|
||||
ScheduledAt = DateTime.Parse(json.RootElement.GetProperty("scheduledAt").GetString()!),
|
||||
Notes = json.RootElement.TryGetProperty("notes", out var n) ? n.GetString() : null,
|
||||
Status = FollowUpStatus.Upcoming,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
db.FollowUps.Add(followUp);
|
||||
await db.SaveChangesAsync(ct);
|
||||
|
||||
return Results.Ok(new { code = 0, data = new { followUp.Id }, message = (string?)null });
|
||||
});
|
||||
|
||||
// ===== 更新随访 =====
|
||||
group.MapPut("/follow-ups/{id:guid}", async (Guid id, HttpContext http, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var followUp = await db.FollowUps.FirstOrDefaultAsync(f => f.Id == id, ct);
|
||||
if (followUp == null)
|
||||
return Results.Ok(new { code = 404, data = (object?)null, message = "随访不存在" });
|
||||
|
||||
using var reader = new StreamReader(http.Request.Body);
|
||||
var body = await reader.ReadToEndAsync(ct);
|
||||
var json = System.Text.Json.JsonDocument.Parse(body);
|
||||
|
||||
if (json.RootElement.TryGetProperty("title", out var title)) followUp.Title = title.GetString() ?? followUp.Title;
|
||||
if (json.RootElement.TryGetProperty("scheduledAt", out var sat)) followUp.ScheduledAt = DateTime.Parse(sat.GetString()!);
|
||||
if (json.RootElement.TryGetProperty("notes", out var notes)) followUp.Notes = notes.GetString();
|
||||
if (json.RootElement.TryGetProperty("status", out var st) && Enum.TryParse<FollowUpStatus>(st.GetString(), out var fs))
|
||||
followUp.Status = fs;
|
||||
|
||||
await db.SaveChangesAsync(ct);
|
||||
return Results.Ok(new { code = 0, data = new { success = true }, message = (string?)null });
|
||||
});
|
||||
|
||||
// ===== 删除随访 =====
|
||||
group.MapDelete("/follow-ups/{id:guid}", async (Guid id, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var followUp = await db.FollowUps.FirstOrDefaultAsync(f => f.Id == id, ct);
|
||||
if (followUp == null)
|
||||
return Results.Ok(new { code = 404, data = (object?)null, message = "随访不存在" });
|
||||
|
||||
db.FollowUps.Remove(followUp);
|
||||
await db.SaveChangesAsync(ct);
|
||||
return Results.Ok(new { code = 0, data = new { success = true }, message = (string?)null });
|
||||
});
|
||||
|
||||
// ===== 患者列表(简版,给随访选患者用)=====
|
||||
group.MapGet("/patients-simple", async (AppDbContext db) =>
|
||||
{
|
||||
var patients = await db.Users.OrderBy(u => u.Name).Select(u => new { u.Id, u.Name, u.Phone }).ToListAsync();
|
||||
return Results.Ok(new { code = 0, data = patients, message = (string?)null });
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
global using Health.Domain.Entities;
|
||||
global using Health.Domain.Enums;
|
||||
global using Health.Infrastructure.Data;
|
||||
global using Microsoft.AspNetCore.SignalR;
|
||||
global using Microsoft.EntityFrameworkCore;
|
||||
global using System.Text.Json;
|
||||
|
||||
36
backend/src/Health.WebApi/Hubs/ConsultationHub.cs
Normal file
36
backend/src/Health.WebApi/Hubs/ConsultationHub.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace Health.WebApi.Hubs;
|
||||
|
||||
/// <summary>
|
||||
/// 问诊实时通信 Hub(开发阶段跳过认证)
|
||||
/// </summary>
|
||||
public sealed class ConsultationHub : Hub
|
||||
{
|
||||
public async Task JoinConsultation(string consultationId)
|
||||
{
|
||||
await Groups.AddToGroupAsync(Context.ConnectionId, consultationId);
|
||||
}
|
||||
|
||||
public async Task LeaveConsultation(string consultationId)
|
||||
{
|
||||
await Groups.RemoveFromGroupAsync(Context.ConnectionId, consultationId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发送消息到问诊房间
|
||||
/// </summary>
|
||||
public async Task SendMessage(string consultationId, string senderType, string? senderName, string content)
|
||||
{
|
||||
// 广播给组内所有人(包括自己)
|
||||
await Clients.Group(consultationId).SendAsync("ReceiveMessage", new
|
||||
{
|
||||
id = Guid.NewGuid(),
|
||||
consultationId,
|
||||
senderType,
|
||||
senderName,
|
||||
content,
|
||||
createdAt = DateTime.UtcNow
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -87,14 +87,16 @@ builder.Services.AddHostedService<CleanupService>();
|
||||
// ---- OpenAPI ----
|
||||
builder.Services.AddOpenApi();
|
||||
|
||||
// ---- CORS ----
|
||||
// ---- SignalR ----
|
||||
builder.Services.AddSignalR();
|
||||
|
||||
// ---- CORS(SignalR 需要明确 origins + AllowCredentials)----
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddDefaultPolicy(policy =>
|
||||
{
|
||||
policy.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader();
|
||||
policy.SetIsOriginAllowed(_ => true).AllowAnyMethod().AllowAnyHeader().AllowCredentials();
|
||||
});
|
||||
// 生产环境:policy.WithOrigins("https://yourdomain.com").AllowAnyMethod().AllowAnyHeader();
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
@@ -129,5 +131,9 @@ app.MapUserEndpoints();
|
||||
app.MapAiChatEndpoints();
|
||||
app.MapFileEndpoints();
|
||||
app.MapCalendarEndpoints();
|
||||
app.MapDoctorEndpoints();
|
||||
|
||||
// SignalR Hub
|
||||
app.MapHub<Health.WebApi.Hubs.ConsultationHub>("/hubs/consultation");
|
||||
|
||||
app.Run();
|
||||
|
||||
Reference in New Issue
Block a user