From 949f389854fedc5a07ec9fff1ab1265fc2f5f863 Mon Sep 17 00:00:00 2001 From: biss Date: Mon, 10 Aug 2026 19:04:35 +0800 Subject: [PATCH] =?UTF-8?q?=E6=97=B6=E9=97=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Persistence/AppDbContext.cs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/Jiaowu.Api/Infrastructure/Persistence/AppDbContext.cs b/src/Jiaowu.Api/Infrastructure/Persistence/AppDbContext.cs index ee18562..eb62157 100644 --- a/src/Jiaowu.Api/Infrastructure/Persistence/AppDbContext.cs +++ b/src/Jiaowu.Api/Infrastructure/Persistence/AppDbContext.cs @@ -166,6 +166,14 @@ public sealed class AppDbContext(DbContextOptions options) configurationBuilder.Properties() .HaveConversion() .HaveColumnType("time"); + + // MySQL DATETIME has no offset or DateTimeKind. All system timestamps + // are persisted as UTC, so restore that contract when materializing + // them. System.Text.Json will then emit the trailing "Z", allowing + // browsers to convert timestamps to the viewer's local time correctly. + configurationBuilder.Properties() + .HaveConversion() + .HaveColumnType("datetime(6)"); } protected override void OnModelCreating(ModelBuilder builder) @@ -1086,6 +1094,10 @@ public sealed class AppDbContext(DbContextOptions options) }); builder.Entity(entity => { + // Exam slot times are China-local wall-clock times, not instants. + // Keep their existing API representation offset-free. + entity.Property(x => x.StartsAt).HasConversion(); + entity.Property(x => x.EndsAt).HasConversion(); entity.Property(x => x.Notes).HasMaxLength(500); entity.HasIndex(x => new { x.ExamPlanId, x.StartsAt }); entity.HasIndex(x => x.TeachingTaskId); @@ -1112,6 +1124,8 @@ public sealed class AppDbContext(DbContextOptions options) builder.Entity(entity => { entity.ToTable("ExamRooms"); + entity.Property(x => x.StartsAt).HasConversion(); + entity.Property(x => x.EndsAt).HasConversion(); entity.HasIndex(x => new { x.ExamPlanId, x.StartsAt }) .HasDatabaseName("IX_ExamRooms_Plan_Time"); entity.HasIndex(x => new @@ -1200,6 +1214,9 @@ public sealed class AppDbContext(DbContextOptions options) }); builder.Entity(entity => { + // Makeup-exam slot times follow the same wall-clock convention. + entity.Property(x => x.StartsAt).HasConversion(); + entity.Property(x => x.EndsAt).HasConversion(); entity.Property(x => x.Notes).HasMaxLength(500); entity.HasIndex(x => new { x.MakeupExamPlanId, x.StartsAt }); entity.HasIndex(x => x.TeachingTaskId); @@ -1646,3 +1663,15 @@ public sealed class TimeOnlyTimeSpanConverter() : ValueConverter( time => time.ToTimeSpan(), value => TimeOnly.FromTimeSpan(value)); + +public sealed class UtcDateTimeConverter() + : ValueConverter( + value => value.Kind == DateTimeKind.Local + ? value.ToUniversalTime() + : DateTime.SpecifyKind(value, DateTimeKind.Utc), + value => DateTime.SpecifyKind(value, DateTimeKind.Utc)); + +public sealed class UnspecifiedDateTimeConverter() + : ValueConverter( + value => DateTime.SpecifyKind(value, DateTimeKind.Unspecified), + value => DateTime.SpecifyKind(value, DateTimeKind.Unspecified));