修复数据

This commit is contained in:
2026-07-25 22:14:40 +08:00 Unverified
parent d42ec74822
commit aafc8c537e
3 changed files with 93 additions and 29 deletions
@@ -4,6 +4,7 @@ using Jiaowu.Api.Domain.Identity;
using Jiaowu.Api.Domain.System; using Jiaowu.Api.Domain.System;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Jiaowu.Api.Infrastructure.Persistence; namespace Jiaowu.Api.Infrastructure.Persistence;
@@ -90,6 +91,18 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
Set<GraduationClearanceRecord>(); Set<GraduationClearanceRecord>();
public DbSet<AuditLog> AuditLogs => Set<AuditLog>(); public DbSet<AuditLog> AuditLogs => Set<AuditLog>();
protected override void ConfigureConventions(
ModelConfigurationBuilder configurationBuilder)
{
base.ConfigureConventions(configurationBuilder);
// Connector/NET returns MySQL DATE values as DateTime. An explicit
// provider conversion prevents EF from asking the reader for DateOnly.
configurationBuilder.Properties<DateOnly>()
.HaveConversion<DateOnlyDateTimeConverter>()
.HaveColumnType("date");
}
protected override void OnModelCreating(ModelBuilder builder) protected override void OnModelCreating(ModelBuilder builder)
{ {
base.OnModelCreating(builder); base.OnModelCreating(builder);
@@ -904,3 +917,8 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
}); });
} }
} }
public sealed class DateOnlyDateTimeConverter()
: ValueConverter<DateOnly, DateTime>(
date => date.ToDateTime(TimeOnly.MinValue),
value => DateOnly.FromDateTime(value));
@@ -8,6 +8,8 @@ public sealed class DemoDataSeeder(
AppDbContext db, AppDbContext db,
ILogger<DemoDataSeeder> logger) ILogger<DemoDataSeeder> logger)
{ {
// MySql.EntityFrameworkCore 10 cannot type-map parameterized primitive
// collections, so membership in definition/ID sets is checked after loading.
private const int Grade = 2026; private const int Grade = 2026;
private const int ClassesPerMajor = 2; private const int ClassesPerMajor = 2;
private const int StudentsPerClass = 35; private const int StudentsPerClass = 35;
@@ -176,14 +178,18 @@ public sealed class DemoDataSeeder(
.SelectMany(x => x.Majors) .SelectMany(x => x.Majors)
.Select(x => x.Code) .Select(x => x.Code)
.ToHashSet(StringComparer.OrdinalIgnoreCase); .ToHashSet(StringComparer.OrdinalIgnoreCase);
var majors = await db.Majors var majors = (await db.Majors
.OrderBy(x => x.Code)
.ToListAsync(cancellationToken))
.Where(x => targetMajorCodes.Contains(x.Code)) .Where(x => targetMajorCodes.Contains(x.Code))
.ToList();
var majorIds = majors.Select(x => x.Id).ToHashSet();
var existingClasses = (await db.AdministrativeClasses
.Where(x => x.Grade == Grade)
.OrderBy(x => x.Code) .OrderBy(x => x.Code)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken))
var existingClasses = await db.AdministrativeClasses .Where(x => majorIds.Contains(x.MajorId))
.Where(x => x.Grade == Grade && targetMajorCodes.Contains(x.Major!.Code)) .ToList();
.OrderBy(x => x.Code)
.ToListAsync(cancellationToken);
var existingCodes = (await db.AdministrativeClasses var existingCodes = (await db.AdministrativeClasses
.Select(x => x.Code) .Select(x => x.Code)
.ToListAsync(cancellationToken)) .ToListAsync(cancellationToken))
@@ -218,13 +224,16 @@ public sealed class DemoDataSeeder(
var collegeCodes = CollegeDefinitions var collegeCodes = CollegeDefinitions
.Select(x => x.Code) .Select(x => x.Code)
.ToHashSet(StringComparer.OrdinalIgnoreCase); .ToHashSet(StringComparer.OrdinalIgnoreCase);
var colleges = await db.Colleges var colleges = (await db.Colleges
.Where(x => collegeCodes.Contains(x.Code))
.OrderBy(x => x.Code) .OrderBy(x => x.Code)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken))
var existingTeachers = await db.Teachers .Where(x => collegeCodes.Contains(x.Code))
.Where(x => colleges.Select(c => c.Id).Contains(x.CollegeId)) .ToList();
.ToListAsync(cancellationToken); var collegeIds = colleges.Select(x => x.Id).ToHashSet();
var existingTeachers = (await db.Teachers
.ToListAsync(cancellationToken))
.Where(x => collegeIds.Contains(x.CollegeId))
.ToList();
var existingNumbers = (await db.Teachers var existingNumbers = (await db.Teachers
.Select(x => x.TeacherNumber) .Select(x => x.TeacherNumber)
.ToListAsync(cancellationToken)) .ToListAsync(cancellationToken))
@@ -264,14 +273,23 @@ public sealed class DemoDataSeeder(
.SelectMany(x => x.Majors) .SelectMany(x => x.Majors)
.Select(x => x.Code) .Select(x => x.Code)
.ToHashSet(StringComparer.OrdinalIgnoreCase); .ToHashSet(StringComparer.OrdinalIgnoreCase);
var classes = await db.AdministrativeClasses var targetMajorIds = (await db.Majors
.Where(x => x.Grade == Grade && targetMajorCodes.Contains(x.Major!.Code)) .Select(x => new { x.Id, x.Code })
.ToListAsync(cancellationToken))
.Where(x => targetMajorCodes.Contains(x.Code))
.Select(x => x.Id)
.ToHashSet();
var classes = (await db.AdministrativeClasses
.Where(x => x.Grade == Grade)
.OrderBy(x => x.Code) .OrderBy(x => x.Code)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken))
.Where(x => targetMajorIds.Contains(x.MajorId))
.ToList();
var classIds = classes.Select(x => x.Id).ToHashSet(); var classIds = classes.Select(x => x.Id).ToHashSet();
var existingStudents = await db.Students var existingStudents = (await db.Students
.ToListAsync(cancellationToken))
.Where(x => classIds.Contains(x.AdministrativeClassId)) .Where(x => classIds.Contains(x.AdministrativeClassId))
.ToListAsync(cancellationToken); .ToList();
var existingNumbers = (await db.Students var existingNumbers = (await db.Students
.Select(x => x.StudentNumber) .Select(x => x.StudentNumber)
.ToListAsync(cancellationToken)) .ToListAsync(cancellationToken))
@@ -404,19 +422,23 @@ public sealed class DemoDataSeeder(
var collegeCodes = CollegeDefinitions var collegeCodes = CollegeDefinitions
.Select(x => x.Code) .Select(x => x.Code)
.ToHashSet(StringComparer.OrdinalIgnoreCase); .ToHashSet(StringComparer.OrdinalIgnoreCase);
var colleges = await db.Colleges var colleges = (await db.Colleges
.OrderBy(x => x.Code)
.ToListAsync(cancellationToken))
.Where(x => collegeCodes.Contains(x.Code)) .Where(x => collegeCodes.Contains(x.Code))
.OrderBy(x => x.Code) .ToList();
.ToListAsync(cancellationToken);
var collegeIds = colleges.Select(x => x.Id).ToHashSet(); var collegeIds = colleges.Select(x => x.Id).ToHashSet();
var teachers = await db.Teachers var teachers = (await db.Teachers
.Where(x => collegeIds.Contains(x.CollegeId) && x.Status == TeacherStatus.Active) .Where(x => x.Status == TeacherStatus.Active)
.OrderBy(x => x.TeacherNumber) .OrderBy(x => x.TeacherNumber)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken))
var courses = await db.Courses
.Where(x => collegeIds.Contains(x.CollegeId)) .Where(x => collegeIds.Contains(x.CollegeId))
.ToList();
var courses = (await db.Courses
.OrderBy(x => x.Code) .OrderBy(x => x.Code)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken))
.Where(x => collegeIds.Contains(x.CollegeId))
.ToList();
var publicCourses = courses var publicCourses = courses
.Where(x => x.Nature is CourseNature.GeneralRequired or CourseNature.GeneralElective) .Where(x => x.Nature is CourseNature.GeneralRequired or CourseNature.GeneralElective)
.ToList(); .ToList();
+28 -4
View File
@@ -13,10 +13,7 @@ public sealed class MySqlMigrationTests
[Fact] [Fact]
public void Production_migration_is_discoverable_and_generates_mysql_sql() public void Production_migration_is_discoverable_and_generates_mysql_sql()
{ {
var options = new DbContextOptionsBuilder<AppDbContext>() var options = CreateMySqlOptions();
.UseMySQL(
"Server=localhost;Database=jiaowu;User=__test__;Password=__not_used__;")
.Options;
using var db = new AppDbContext(options); using var db = new AppDbContext(options);
Assert.Contains(LatestMigration, db.Database.GetMigrations()); Assert.Contains(LatestMigration, db.Database.GetMigrations());
@@ -33,4 +30,31 @@ public sealed class MySqlMigrationTests
Assert.DoesNotContain("0001-01-01", script); Assert.DoesNotContain("0001-01-01", script);
Assert.DoesNotContain("0000-00-00", script); Assert.DoesNotContain("0000-00-00", script);
} }
[Fact]
public void MySql_date_properties_use_datetime_provider_conversion()
{
using var db = new AppDbContext(CreateMySqlOptions());
var dateProperties = db.Model.GetEntityTypes()
.SelectMany(x => x.GetProperties())
.Where(x => x.ClrType == typeof(DateOnly) ||
x.ClrType == typeof(DateOnly?))
.ToList();
Assert.NotEmpty(dateProperties);
Assert.All(dateProperties, property =>
{
Assert.Equal("date", property.GetColumnType());
Assert.Equal(
typeof(DateTime),
property.GetTypeMapping().Converter?.ProviderClrType);
});
}
private static DbContextOptions<AppDbContext> CreateMySqlOptions() =>
new DbContextOptionsBuilder<AppDbContext>()
.UseMySQL(
"Server=localhost;Database=jiaowu;User=__test__;Password=__not_used__;")
.Options;
} }