fix: audit issues - field mismatches, missing endpoints, data loss
- Report frontends: createdAt→uploadedAt field alignment with backend - Dashboard: fix pending reports endpoint /api/reports/pending - FollowUpListPage: status labels upcoming/cancelled - MedicationController: add PUT/DELETE endpoints + service methods - FollowUpController: add DELETE endpoint, Notes to CreateRequest - Auth: UpdateProfileRequest includes doctor fields - Auth: login restores soft-deleted users instead of crashing
This commit is contained in:
@@ -16,4 +16,5 @@ public record UserProfileResponse(
|
|||||||
|
|
||||||
public record UpdateProfileRequest(
|
public record UpdateProfileRequest(
|
||||||
string? Name, string? Gender, DateOnly? Birthday,
|
string? Name, string? Gender, DateOnly? Birthday,
|
||||||
decimal? HeightCm, decimal? WeightKg, List<string>? MedicalHistory);
|
decimal? HeightCm, decimal? WeightKg, List<string>? MedicalHistory,
|
||||||
|
string? Department, string? Title, string? Introduction, List<string>? Specialty);
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ public class FollowUpService(AppDbContext db)
|
|||||||
.OrderBy(f => f.ScheduledAt)
|
.OrderBy(f => f.ScheduledAt)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
public async Task<FollowUp> AddAsync(Guid patientId, string title, string? description, DateTime scheduledAt, bool reminderEnabled, Guid? doctorId = null)
|
public async Task<FollowUp> AddAsync(Guid patientId, string title, string? description, DateTime scheduledAt, bool reminderEnabled, Guid? doctorId = null, string? notes = null)
|
||||||
{
|
{
|
||||||
var followUp = new FollowUp
|
var followUp = new FollowUp
|
||||||
{
|
{
|
||||||
@@ -30,6 +30,7 @@ public class FollowUpService(AppDbContext db)
|
|||||||
Description = description,
|
Description = description,
|
||||||
ScheduledAt = DateTime.SpecifyKind(scheduledAt, DateTimeKind.Utc),
|
ScheduledAt = DateTime.SpecifyKind(scheduledAt, DateTimeKind.Utc),
|
||||||
ReminderEnabled = reminderEnabled,
|
ReminderEnabled = reminderEnabled,
|
||||||
|
Notes = notes,
|
||||||
};
|
};
|
||||||
db.FollowUps.Add(followUp);
|
db.FollowUps.Add(followUp);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
@@ -59,4 +60,13 @@ public class FollowUpService(AppDbContext db)
|
|||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
return followUp;
|
return followUp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<bool> DeleteAsync(Guid id)
|
||||||
|
{
|
||||||
|
var followUp = await db.FollowUps.FindAsync(id);
|
||||||
|
if (followUp == null) return false;
|
||||||
|
db.FollowUps.Remove(followUp);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,4 +90,34 @@ public class MedicationService(AppDbContext db)
|
|||||||
|
|
||||||
return totalCount > 0 ? Math.Round((decimal)takenCount / totalCount * 100, 1) : 0;
|
return totalCount > 0 ? Math.Round((decimal)takenCount / totalCount * 100, 1) : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<Medication?> UpdateAsync(Guid medicationId, Guid userId, string? drugName,
|
||||||
|
string? dosage, string? frequency, List<string>? timeSlots,
|
||||||
|
DateOnly? startDate, DateOnly? endDate, string? notes, string? status)
|
||||||
|
{
|
||||||
|
var med = await db.Medications.FindAsync(medicationId);
|
||||||
|
if (med == null || med.UserId != userId) return null;
|
||||||
|
|
||||||
|
if (drugName != null) med.DrugName = drugName;
|
||||||
|
if (dosage != null) med.Dosage = dosage;
|
||||||
|
if (frequency != null) med.Frequency = frequency;
|
||||||
|
if (timeSlots != null) med.TimeSlots = timeSlots;
|
||||||
|
if (startDate.HasValue) med.StartDate = startDate.Value;
|
||||||
|
if (endDate.HasValue) med.EndDate = endDate;
|
||||||
|
if (notes != null) med.Notes = notes;
|
||||||
|
if (status != null) med.Status = status;
|
||||||
|
med.UpdatedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
return med;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> DeleteAsync(Guid medicationId, Guid userId)
|
||||||
|
{
|
||||||
|
var med = await db.Medications.FindAsync(medicationId);
|
||||||
|
if (med == null || med.UserId != userId) return false;
|
||||||
|
db.Medications.Remove(med);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,14 +29,28 @@ public class AuthController(
|
|||||||
{
|
{
|
||||||
// Demo: auto-register new users
|
// Demo: auto-register new users
|
||||||
var db = HttpContext.RequestServices.GetRequiredService<Infrastructure.Data.AppDbContext>();
|
var db = HttpContext.RequestServices.GetRequiredService<Infrastructure.Data.AppDbContext>();
|
||||||
user = new User
|
|
||||||
|
// Check if this phone was soft-deleted — restore instead of creating duplicate
|
||||||
|
var deleted = await db.Users.IgnoreQueryFilters()
|
||||||
|
.FirstOrDefaultAsync(u => u.Phone == request.Phone && u.IsDeleted);
|
||||||
|
if (deleted != null)
|
||||||
{
|
{
|
||||||
Phone = request.Phone,
|
deleted.IsDeleted = false;
|
||||||
Name = "用户" + request.Phone[^4..],
|
deleted.DeletedAt = null;
|
||||||
Role = "patient",
|
deleted.UpdatedAt = DateTime.UtcNow;
|
||||||
PasswordHash = AuthService.HashPassword("demo123"),
|
user = deleted;
|
||||||
};
|
}
|
||||||
db.Users.Add(user);
|
else
|
||||||
|
{
|
||||||
|
user = new User
|
||||||
|
{
|
||||||
|
Phone = request.Phone,
|
||||||
|
Name = "用户" + request.Phone[^4..],
|
||||||
|
Role = "patient",
|
||||||
|
PasswordHash = AuthService.HashPassword("demo123"),
|
||||||
|
};
|
||||||
|
db.Users.Add(user);
|
||||||
|
}
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,6 +135,10 @@ public class AuthController(
|
|||||||
if (request.HeightCm.HasValue) user.HeightCm = request.HeightCm;
|
if (request.HeightCm.HasValue) user.HeightCm = request.HeightCm;
|
||||||
if (request.WeightKg.HasValue) user.WeightKg = request.WeightKg;
|
if (request.WeightKg.HasValue) user.WeightKg = request.WeightKg;
|
||||||
if (request.MedicalHistory != null) user.MedicalHistory = request.MedicalHistory;
|
if (request.MedicalHistory != null) user.MedicalHistory = request.MedicalHistory;
|
||||||
|
if (request.Department != null) user.Department = request.Department;
|
||||||
|
if (request.Title != null) user.Title = request.Title;
|
||||||
|
if (request.Introduction != null) user.Introduction = request.Introduction;
|
||||||
|
if (request.Specialty != null) user.Specialty = request.Specialty;
|
||||||
user.UpdatedAt = DateTime.UtcNow;
|
user.UpdatedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|||||||
@@ -55,10 +55,19 @@ public class FollowUpController(FollowUpService followUpService) : ControllerBas
|
|||||||
}
|
}
|
||||||
|
|
||||||
var followUp = await followUpService.AddAsync(patientId, request.Title, request.Description,
|
var followUp = await followUpService.AddAsync(patientId, request.Title, request.Description,
|
||||||
request.ScheduledAt, request.ReminderEnabled, doctorId);
|
request.ScheduledAt, request.ReminderEnabled, doctorId, request.Notes);
|
||||||
return Ok(new { followUp.Id, followUp.Title, followUp.Status });
|
return Ok(new { followUp.Id, followUp.Title, followUp.Status });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpDelete("{id:guid}")]
|
||||||
|
[Authorize(Roles = "doctor")]
|
||||||
|
public async Task<IActionResult> DeleteFollowUp(Guid id)
|
||||||
|
{
|
||||||
|
var ok = await followUpService.DeleteAsync(id);
|
||||||
|
if (!ok) return NotFound(new { message = "复查不存在" });
|
||||||
|
return Ok(new { message = "删除成功" });
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPut("{id:guid}")]
|
[HttpPut("{id:guid}")]
|
||||||
[Authorize(Roles = "doctor")]
|
[Authorize(Roles = "doctor")]
|
||||||
public async Task<IActionResult> UpdateFollowUp(Guid id, [FromBody] FollowUpUpdateRequest request)
|
public async Task<IActionResult> UpdateFollowUp(Guid id, [FromBody] FollowUpUpdateRequest request)
|
||||||
@@ -77,6 +86,7 @@ public class FollowUpCreateRequest
|
|||||||
public DateTime ScheduledAt { get; set; }
|
public DateTime ScheduledAt { get; set; }
|
||||||
public bool ReminderEnabled { get; set; } = true;
|
public bool ReminderEnabled { get; set; } = true;
|
||||||
public Guid? PatientId { get; set; }
|
public Guid? PatientId { get; set; }
|
||||||
|
public string? Notes { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class FollowUpUpdateRequest
|
public class FollowUpUpdateRequest
|
||||||
|
|||||||
@@ -73,8 +73,31 @@ public class MedicationController(MedicationService medicationService) : Control
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPut("{id:guid}")]
|
||||||
|
public async Task<IActionResult> UpdateMedication(Guid id, [FromBody] MedicationUpdateRequest request)
|
||||||
|
{
|
||||||
|
var med = await medicationService.UpdateAsync(id, UserId, request.DrugName, request.Dosage,
|
||||||
|
request.Frequency, request.TimeSlots, request.StartDate, request.EndDate, request.Notes, request.Status);
|
||||||
|
if (med == null) return NotFound(new { message = "药品不存在" });
|
||||||
|
return Ok(new { med.Id, med.DrugName, med.Dosage, med.Status });
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("{id:guid}")]
|
||||||
|
public async Task<IActionResult> DeleteMedication(Guid id)
|
||||||
|
{
|
||||||
|
var ok = await medicationService.DeleteAsync(id, UserId);
|
||||||
|
if (!ok) return NotFound(new { message = "药品不存在" });
|
||||||
|
return Ok(new { message = "删除成功" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public record MedicationCreateRequest(
|
public record MedicationCreateRequest(
|
||||||
string DrugName, string Dosage, string Frequency,
|
string DrugName, string Dosage, string Frequency,
|
||||||
List<string> TimeSlots, DateOnly StartDate, DateOnly? EndDate, string? Notes);
|
List<string> TimeSlots, DateOnly StartDate, DateOnly? EndDate, string? Notes);
|
||||||
|
|
||||||
|
public record MedicationUpdateRequest(
|
||||||
|
string? DrugName, string? Dosage, string? Frequency,
|
||||||
|
List<string>? TimeSlots, DateOnly? StartDate, DateOnly? EndDate,
|
||||||
|
string? Notes, string? Status);
|
||||||
|
|
||||||
public record MarkTakenRequest(string TimeSlot);
|
public record MarkTakenRequest(string TimeSlot);
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export function DashboardPage() {
|
|||||||
const [patients, consultations, reports, followUps] = await Promise.all([
|
const [patients, consultations, reports, followUps] = await Promise.all([
|
||||||
api.get<RawPatient[]>('/api/patients'),
|
api.get<RawPatient[]>('/api/patients'),
|
||||||
api.get<RawConsultation[]>('/api/consultations'),
|
api.get<RawConsultation[]>('/api/consultations'),
|
||||||
api.get<RawReport[]>('/api/reports?status=pending'),
|
api.get<RawReport[]>('/api/reports/pending'),
|
||||||
api.get<RawFollowUp[]>('/api/follow-ups'),
|
api.get<RawFollowUp[]>('/api/follow-ups'),
|
||||||
]);
|
]);
|
||||||
setStats({
|
setStats({
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ export function FollowUpListPage() {
|
|||||||
|
|
||||||
const statusLabel = (s: string) => {
|
const statusLabel = (s: string) => {
|
||||||
switch (s) {
|
switch (s) {
|
||||||
case 'pending': return { text: '待随访', color: '#F59E0B', bg: '#FFF8E6' };
|
case 'upcoming': return { text: '待随访', color: '#F59E0B', bg: '#FFF8E6' };
|
||||||
case 'completed': return { text: '已完成', color: '#20C997', bg: '#E6F9F2' };
|
case 'completed': return { text: '已完成', color: '#20C997', bg: '#E6F9F2' };
|
||||||
case 'missed': return { text: '已错过', color: '#EF4444', bg: '#FEE9E9' };
|
case 'cancelled': return { text: '已取消', color: '#EF4444', bg: '#FEE9E9' };
|
||||||
default: return { text: s, color: '#9BA0B4', bg: '#F5F6F9' };
|
default: return { text: s, color: '#9BA0B4', bg: '#F5F6F9' };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ interface RawReport {
|
|||||||
imageUrls: string[]; status: string; riskLevel?: string;
|
imageUrls: string[]; status: string; riskLevel?: string;
|
||||||
summary?: string; suggestions?: string;
|
summary?: string; suggestions?: string;
|
||||||
patientName?: string; doctorName?: string;
|
patientName?: string; doctorName?: string;
|
||||||
createdAt: string; completedAt?: string;
|
uploadedAt: string; completedAt?: string;
|
||||||
items?: RawItem[];
|
items?: RawItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ export function ReportDetailPage() {
|
|||||||
<div style={{ marginTop: 8, fontSize: 13, color: '#9BA0B4' }}>
|
<div style={{ marginTop: 8, fontSize: 13, color: '#9BA0B4' }}>
|
||||||
患者:{report.patientName || '未知'} |
|
患者:{report.patientName || '未知'} |
|
||||||
分类:{categoryMap[report.category] || report.category} |
|
分类:{categoryMap[report.category] || report.category} |
|
||||||
日期:{report.createdAt?.split('T')[0]}
|
日期:{report.uploadedAt?.split('T')[0]}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span style={{
|
<span style={{
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { api } from '../../services/api-client';
|
|||||||
|
|
||||||
interface RawReportItem {
|
interface RawReportItem {
|
||||||
id: string; patientId: string; patientName?: string;
|
id: string; patientId: string; patientName?: string;
|
||||||
title: string; category: string; status: string; createdAt: string;
|
title: string; category: string; status: string; uploadedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ReportListPage() {
|
export function ReportListPage() {
|
||||||
@@ -52,7 +52,7 @@ export function ReportListPage() {
|
|||||||
{s.text}
|
{s.text}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td style={{ padding: '12px 20px', color: '#9BA0B4' }}>{r.createdAt?.split('T')[0]}</td>
|
<td style={{ padding: '12px 20px', color: '#9BA0B4' }}>{r.uploadedAt?.split('T')[0]}</td>
|
||||||
<td style={{ padding: '12px 20px' }}>
|
<td style={{ padding: '12px 20px' }}>
|
||||||
<Link to={`/reports/${r.id}`} style={{
|
<Link to={`/reports/${r.id}`} style={{
|
||||||
color: '#4F6EF7', fontSize: 12, fontWeight: 600,
|
color: '#4F6EF7', fontSize: 12, fontWeight: 600,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ interface RawReport {
|
|||||||
imageUrls: string[];
|
imageUrls: string[];
|
||||||
status: string;
|
status: string;
|
||||||
result?: string;
|
result?: string;
|
||||||
createdAt: string;
|
uploadedAt: string;
|
||||||
interpretedAt?: string;
|
interpretedAt?: string;
|
||||||
interpretedBy?: string;
|
interpretedBy?: string;
|
||||||
}
|
}
|
||||||
@@ -35,7 +35,7 @@ function mapReport(r: RawReport): Report {
|
|||||||
userId: r.patientId,
|
userId: r.patientId,
|
||||||
title: r.title,
|
title: r.title,
|
||||||
imageUrls: r.imageUrls,
|
imageUrls: r.imageUrls,
|
||||||
uploadAt: r.createdAt,
|
uploadAt: r.uploadedAt,
|
||||||
status: r.status as Report['status'],
|
status: r.status as Report['status'],
|
||||||
category: r.category,
|
category: r.category,
|
||||||
result,
|
result,
|
||||||
|
|||||||
Reference in New Issue
Block a user