修复Mysql
This commit is contained in:
@@ -168,7 +168,7 @@ public sealed class ExamArrangementService(AppDbContext db)
|
||||
.ToHashSet();
|
||||
|
||||
if (occupiedRoomIds.Count > 0)
|
||||
query = query.Where(x => !occupiedRoomIds.Contains(x.Id));
|
||||
query = query.WhereNotIn(occupiedRoomIds, x => x.Id);
|
||||
|
||||
// Exclude classrooms occupied by DB sessions not yet tracked in memory
|
||||
var dbOccupiedRooms = await db.ExamSessions.AsNoTracking()
|
||||
@@ -181,7 +181,7 @@ public sealed class ExamArrangementService(AppDbContext db)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (dbOccupiedRooms.Count > 0)
|
||||
query = query.Where(x => !dbOccupiedRooms.Contains(x.Id));
|
||||
query = query.WhereNotIn(dbOccupiedRooms, x => x.Id);
|
||||
|
||||
return await query
|
||||
.OrderBy(x => x.Capacity)
|
||||
@@ -213,8 +213,8 @@ public sealed class ExamArrangementService(AppDbContext db)
|
||||
foreach (var id in excludeTeacherIds) busyTeacherIds.Add(id);
|
||||
|
||||
return await db.Teachers.AsNoTracking()
|
||||
.Where(x => x.Status == TeacherStatus.Active &&
|
||||
!busyTeacherIds.Contains(x.Id))
|
||||
.Where(x => x.Status == TeacherStatus.Active)
|
||||
.WhereNotIn(busyTeacherIds, x => x.Id)
|
||||
.OrderBy(x => Guid.NewGuid())
|
||||
.Take(needed)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
@@ -164,7 +164,7 @@ public sealed class MakeupExamArrangementService(AppDbContext db)
|
||||
.ToHashSet();
|
||||
|
||||
if (occupiedRoomIds.Count > 0)
|
||||
query = query.Where(x => !occupiedRoomIds.Contains(x.Id));
|
||||
query = query.WhereNotIn(occupiedRoomIds, x => x.Id);
|
||||
|
||||
var dbOccupiedRooms = await db.MakeupExamSessions.AsNoTracking()
|
||||
.Where(x => x.MakeupExamPlanId == session.MakeupExamPlanId &&
|
||||
@@ -176,7 +176,7 @@ public sealed class MakeupExamArrangementService(AppDbContext db)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (dbOccupiedRooms.Count > 0)
|
||||
query = query.Where(x => !dbOccupiedRooms.Contains(x.Id));
|
||||
query = query.WhereNotIn(dbOccupiedRooms, x => x.Id);
|
||||
|
||||
return await query
|
||||
.OrderBy(x => x.Capacity)
|
||||
@@ -208,8 +208,8 @@ public sealed class MakeupExamArrangementService(AppDbContext db)
|
||||
foreach (var id in excludeTeacherIds) busyTeacherIds.Add(id);
|
||||
|
||||
return await db.Teachers.AsNoTracking()
|
||||
.Where(x => x.Status == TeacherStatus.Active &&
|
||||
!busyTeacherIds.Contains(x.Id))
|
||||
.Where(x => x.Status == TeacherStatus.Active)
|
||||
.WhereNotIn(busyTeacherIds, x => x.Id)
|
||||
.OrderBy(x => Guid.NewGuid())
|
||||
.Take(needed)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
@@ -32,8 +32,8 @@ public sealed class MakeupExamEligibilityService(AppDbContext db)
|
||||
// 1. Approved deferred exams (highest priority)
|
||||
var deferredStudents = await db.DeferredExams.AsNoTracking()
|
||||
.Where(x => x.TeachingTaskId == teachingTaskId &&
|
||||
x.Status == ApprovalStatus.Approved &&
|
||||
!enrolledSet.Contains(x.StudentId))
|
||||
x.Status == ApprovalStatus.Approved)
|
||||
.WhereNotIn(enrolledSet, x => x.StudentId)
|
||||
.Select(x => new
|
||||
{
|
||||
x.StudentId,
|
||||
@@ -58,8 +58,8 @@ public sealed class MakeupExamEligibilityService(AppDbContext db)
|
||||
// 2. Absent students
|
||||
var absentStudents = await db.GradeRecords.AsNoTracking()
|
||||
.Where(x => x.GradeSheetId == gradeSheet.Id &&
|
||||
x.ExamStatus == GradeExamStatus.Absent &&
|
||||
!enrolledSet.Contains(x.StudentId))
|
||||
x.ExamStatus == GradeExamStatus.Absent)
|
||||
.WhereNotIn(enrolledSet, x => x.StudentId)
|
||||
.Select(x => new
|
||||
{
|
||||
x.StudentId,
|
||||
@@ -85,8 +85,8 @@ public sealed class MakeupExamEligibilityService(AppDbContext db)
|
||||
var failedStudents = await db.GradeRecords.AsNoTracking()
|
||||
.Where(x => x.GradeSheetId == gradeSheet.Id &&
|
||||
x.ExamStatus == GradeExamStatus.Normal &&
|
||||
x.TotalScore < 60 &&
|
||||
!enrolledSet.Contains(x.StudentId))
|
||||
x.TotalScore < 60)
|
||||
.WhereNotIn(enrolledSet, x => x.StudentId)
|
||||
.Select(x => new
|
||||
{
|
||||
x.StudentId,
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace Jiaowu.Api.Infrastructure.Persistence;
|
||||
|
||||
public static class QueryableCollectionExtensions
|
||||
{
|
||||
public static IQueryable<TEntity> WhereIn<TEntity, TValue>(
|
||||
this IQueryable<TEntity> source,
|
||||
IEnumerable<TValue> values,
|
||||
Expression<Func<TEntity, TValue>> valueSelector)
|
||||
{
|
||||
var predicate = BuildPredicate(values, valueSelector, Expression.OrElse, false);
|
||||
return source.Where(predicate);
|
||||
}
|
||||
|
||||
public static IQueryable<TEntity> WhereNotIn<TEntity, TValue>(
|
||||
this IQueryable<TEntity> source,
|
||||
IEnumerable<TValue> values,
|
||||
Expression<Func<TEntity, TValue>> valueSelector)
|
||||
{
|
||||
var predicate = BuildPredicate(values, valueSelector, Expression.AndAlso, true);
|
||||
return source.Where(predicate);
|
||||
}
|
||||
|
||||
private static Expression<Func<TEntity, bool>> BuildPredicate<TEntity, TValue>(
|
||||
IEnumerable<TValue> values,
|
||||
Expression<Func<TEntity, TValue>> valueSelector,
|
||||
Func<Expression, Expression, BinaryExpression> combine,
|
||||
bool negate)
|
||||
{
|
||||
Expression? body = null;
|
||||
foreach (var value in values.Distinct())
|
||||
{
|
||||
Expression comparison = Expression.Equal(
|
||||
valueSelector.Body,
|
||||
Expression.Constant(value, typeof(TValue)));
|
||||
if (negate)
|
||||
comparison = Expression.Not(comparison);
|
||||
body = body is null ? comparison : combine(body, comparison);
|
||||
}
|
||||
|
||||
body ??= Expression.Constant(negate);
|
||||
return Expression.Lambda<Func<TEntity, bool>>(
|
||||
body,
|
||||
valueSelector.Parameters);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using System.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
|
||||
namespace Jiaowu.Api.Infrastructure.Persistence;
|
||||
|
||||
public static class RetriableTransactionExtensions
|
||||
{
|
||||
public static Task<TResult> ExecuteInRetriableTransactionAsync<TResult>(
|
||||
this DbContext db,
|
||||
Func<IDbContextTransaction, Task<TResult>> operation,
|
||||
CancellationToken cancellationToken,
|
||||
IsolationLevel? isolationLevel = null)
|
||||
{
|
||||
var strategy = db.Database.CreateExecutionStrategy();
|
||||
return strategy.ExecuteAsync(async () =>
|
||||
{
|
||||
await using var transaction = isolationLevel.HasValue
|
||||
? await db.Database.BeginTransactionAsync(
|
||||
isolationLevel.Value,
|
||||
cancellationToken)
|
||||
: await db.Database.BeginTransactionAsync(cancellationToken);
|
||||
return await operation(transaction);
|
||||
});
|
||||
}
|
||||
|
||||
public static Task ExecuteInRetriableTransactionAsync(
|
||||
this DbContext db,
|
||||
Func<IDbContextTransaction, Task> operation,
|
||||
CancellationToken cancellationToken,
|
||||
IsolationLevel? isolationLevel = null)
|
||||
{
|
||||
var strategy = db.Database.CreateExecutionStrategy();
|
||||
return strategy.ExecuteAsync(async () =>
|
||||
{
|
||||
await using var transaction = isolationLevel.HasValue
|
||||
? await db.Database.BeginTransactionAsync(
|
||||
isolationLevel.Value,
|
||||
cancellationToken)
|
||||
: await db.Database.BeginTransactionAsync(cancellationToken);
|
||||
await operation(transaction);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -38,8 +38,9 @@ public sealed class AutomaticScheduleGenerator(AppDbContext db)
|
||||
.ThenByDescending(x => x.Capacity)
|
||||
.ThenBy(x => x.TaskNumber)
|
||||
.ToListAsync(cancellationToken);
|
||||
var taskIds = tasks.Select(task => task.Id).ToArray();
|
||||
var constraints = await db.TeachingTaskScheduleConstraints.AsNoTracking()
|
||||
.Where(x => tasks.Select(task => task.Id).Contains(x.TeachingTaskId))
|
||||
.WhereIn(taskIds, x => x.TeachingTaskId)
|
||||
.Include(x => x.AllowedClassrooms)
|
||||
.ToDictionaryAsync(x => x.TeachingTaskId, cancellationToken);
|
||||
var classrooms = await db.Classrooms.AsNoTracking()
|
||||
|
||||
@@ -169,10 +169,7 @@ public sealed class AutomaticScheduleJobProcessor(
|
||||
job.CompletedTasks = result.CompletedTasks;
|
||||
job.MessagesJson = JsonSerializer.Serialize(result.Messages);
|
||||
job.CompletedAt = DateTime.UtcNow;
|
||||
await using var transaction =
|
||||
await db.Database.BeginTransactionAsync(stoppingToken);
|
||||
await db.SaveChangesAsync(stoppingToken);
|
||||
await transaction.CommitAsync(stoppingToken);
|
||||
|
||||
logger.LogInformation(
|
||||
"Automatic schedule job {JobId} completed with {CreatedEntries} entries.",
|
||||
|
||||
@@ -141,35 +141,46 @@ public sealed class SchedulePublishJobProcessor(
|
||||
ReportProgress,
|
||||
stoppingToken);
|
||||
|
||||
await using var transaction =
|
||||
await db.Database.BeginTransactionAsync(stoppingToken);
|
||||
if (plan.Status != SchedulePlanStatus.Draft)
|
||||
throw new SchedulePublishValidationException(
|
||||
"排课草稿状态已发生变化,请刷新后重试。");
|
||||
var publishedPlanId = plan.Id;
|
||||
await db.ExecuteInRetriableTransactionAsync(
|
||||
async transaction =>
|
||||
{
|
||||
db.ChangeTracker.Clear();
|
||||
var publishJob = await db.SchedulePublishJobs
|
||||
.FirstAsync(x => x.Id == jobId, stoppingToken);
|
||||
var publishPlan = await db.SchedulePlans
|
||||
.FirstAsync(x => x.Id == publishedPlanId, stoppingToken);
|
||||
if (publishPlan.Status != SchedulePlanStatus.Draft)
|
||||
{
|
||||
throw new SchedulePublishValidationException(
|
||||
"排课草稿状态已发生变化,请刷新后重试。");
|
||||
}
|
||||
|
||||
var previous = await db.SchedulePlans
|
||||
.Where(x =>
|
||||
x.Id != plan.Id &&
|
||||
x.AcademicTermId == plan.AcademicTermId &&
|
||||
x.Status == SchedulePlanStatus.Published)
|
||||
.ToListAsync(stoppingToken);
|
||||
foreach (var oldPlan in previous)
|
||||
oldPlan.Status = SchedulePlanStatus.Archived;
|
||||
var previous = await db.SchedulePlans
|
||||
.Where(x =>
|
||||
x.Id != publishPlan.Id &&
|
||||
x.AcademicTermId == publishPlan.AcademicTermId &&
|
||||
x.Status == SchedulePlanStatus.Published)
|
||||
.ToListAsync(stoppingToken);
|
||||
foreach (var oldPlan in previous)
|
||||
oldPlan.Status = SchedulePlanStatus.Archived;
|
||||
|
||||
plan.Status = SchedulePlanStatus.Published;
|
||||
plan.PublishedAt = DateTime.UtcNow;
|
||||
job.Status = SchedulePublishJobStatus.Succeeded;
|
||||
job.ActiveAcademicTermId = null;
|
||||
job.CompletedSteps = job.TotalSteps;
|
||||
job.CurrentStep = "课表已发布";
|
||||
job.CompletedAt = DateTime.UtcNow;
|
||||
await db.SaveChangesAsync(stoppingToken);
|
||||
await transaction.CommitAsync(stoppingToken);
|
||||
publishPlan.Status = SchedulePlanStatus.Published;
|
||||
publishPlan.PublishedAt = DateTime.UtcNow;
|
||||
publishJob.Status = SchedulePublishJobStatus.Succeeded;
|
||||
publishJob.ActiveAcademicTermId = null;
|
||||
publishJob.CompletedSteps = publishJob.TotalSteps;
|
||||
publishJob.CurrentStep = "课表已发布";
|
||||
publishJob.CompletedAt = DateTime.UtcNow;
|
||||
await db.SaveChangesAsync(stoppingToken);
|
||||
await transaction.CommitAsync(stoppingToken);
|
||||
},
|
||||
stoppingToken);
|
||||
|
||||
logger.LogInformation(
|
||||
"Schedule publish job {JobId} published plan {SchedulePlanId}.",
|
||||
job.Id,
|
||||
plan.Id);
|
||||
jobId,
|
||||
publishedPlanId);
|
||||
}
|
||||
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
@@ -245,7 +256,7 @@ public sealed class SchedulePlanPublisher(AppDbContext db)
|
||||
|
||||
var taskIds = plan.Entries.Select(x => x.TeachingTaskId).Distinct().ToList();
|
||||
var constraints = await db.TeachingTaskScheduleConstraints.AsNoTracking()
|
||||
.Where(x => taskIds.Contains(x.TeachingTaskId))
|
||||
.WhereIn(taskIds, x => x.TeachingTaskId)
|
||||
.Include(x => x.AllowedClassrooms)
|
||||
.ToDictionaryAsync(x => x.TeachingTaskId, cancellationToken);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user