调课、停课、补课申请

This commit is contained in:
2026-07-25 15:36:14 +08:00 Unverified
parent 9685435da9
commit 13b61f191a
11 changed files with 1162 additions and 1 deletions
@@ -0,0 +1,440 @@
using System.ComponentModel.DataAnnotations;
using Jiaowu.Api.Domain.Academic;
using Jiaowu.Api.Domain.Identity;
using Jiaowu.Api.Infrastructure.Auth;
using Jiaowu.Api.Infrastructure.Persistence;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace Jiaowu.Api.Controllers;
[ApiController]
[Authorize]
[Route("api/course-adjustments")]
public sealed class CourseAdjustmentsController(
AppDbContext db,
ICurrentUserDataScope currentUserDataScope) : ControllerBase
{
private const string Applicants =
SystemRoles.SuperAdmin + "," +
SystemRoles.AcademicAdmin + "," +
SystemRoles.CollegeAdmin + "," +
SystemRoles.Teacher;
private const string Reviewers =
SystemRoles.SuperAdmin + "," +
SystemRoles.AcademicAdmin + "," +
SystemRoles.CollegeAdmin;
// ═══════════════ My adjustments ═══════════════
[HttpGet("mine")]
[Authorize(Roles = Applicants)]
public async Task<ActionResult> GetMine(
Guid? academicTermId,
CourseAdjustmentStatus? status,
CancellationToken cancellationToken)
{
var userId = currentUserDataScope.Current.UserId;
var source = db.CourseAdjustments.AsNoTracking()
.Where(x => x.ApplicantUserId == userId);
if (academicTermId.HasValue)
source = source.Where(x =>
x.TeachingTask!.AcademicTermId == academicTermId);
if (status.HasValue)
source = source.Where(x => x.Status == status);
return Ok(await source
.OrderByDescending(x => x.CreatedAt)
.Select(AdjustmentProjection())
.ToListAsync(cancellationToken));
}
// ═══════════════ Pending reviews ═══════════════
[HttpGet("pending-reviews")]
[Authorize(Roles = Reviewers)]
public async Task<ActionResult> GetPendingReviews(
Guid? academicTermId,
CancellationToken cancellationToken)
{
var scope = currentUserDataScope.Current;
var source = db.CourseAdjustments.AsNoTracking()
.Where(x => x.Status == CourseAdjustmentStatus.Submitted);
if (scope.Scope == DataScope.College)
source = source.Where(x =>
x.TeachingTask!.Course!.CollegeId == scope.RestrictedCollegeId);
if (academicTermId.HasValue)
source = source.Where(x =>
x.TeachingTask!.AcademicTermId == academicTermId);
return Ok(await source
.OrderByDescending(x => x.SubmittedAt)
.Select(AdjustmentProjection())
.ToListAsync(cancellationToken));
}
// ═══════════════ Detail ═══════════════
[HttpGet("{id:guid}")]
[Authorize(Roles = Applicants)]
public async Task<ActionResult> GetDetail(Guid id, CancellationToken cancellationToken)
{
var adj = await db.CourseAdjustments.AsNoTracking()
.Where(x => x.Id == id)
.Select(AdjustmentProjection())
.FirstOrDefaultAsync(cancellationToken);
return adj is null ? NotFound() : Ok(adj);
}
// ═══════════════ Create ═══════════════
[HttpPost]
[Authorize(Roles = Applicants)]
public async Task<ActionResult> Create(
CourseAdjustmentRequest request,
CancellationToken cancellationToken)
{
var userId = currentUserDataScope.Current.UserId;
var validation = await ValidateRequestAsync(request, cancellationToken);
if (validation is not null) return validation;
var adj = new CourseAdjustment
{
TeachingTaskId = request.TeachingTaskId,
Type = request.Type,
ApplicantUserId = userId,
Reason = request.Reason.Trim(),
TargetDate = request.TargetDate,
DayOfWeek = request.DayOfWeek,
StartPeriod = request.StartPeriod,
PeriodCount = request.PeriodCount,
ClassroomId = request.ClassroomId,
SubstituteTeacherId = request.SubstituteTeacherId,
CancelWeek = request.CancelWeek,
CancelDate = request.CancelDate
};
if (request.Submit)
{
adj.Status = CourseAdjustmentStatus.Submitted;
adj.SubmittedAt = DateTime.UtcNow;
}
db.CourseAdjustments.Add(adj);
await db.SaveChangesAsync(cancellationToken);
if (request.Submit)
await NotifyReviewersAsync(adj, cancellationToken);
return Created(string.Empty, new { adj.Id });
}
// ═══════════════ Submit ═══════════════
[HttpPost("{id:guid}/submit")]
[Authorize(Roles = Applicants)]
public async Task<ActionResult> Submit(Guid id, CancellationToken cancellationToken)
{
var userId = currentUserDataScope.Current.UserId;
var adj = await db.CourseAdjustments
.Include(x => x.TeachingTask)
.ThenInclude(x => x!.Course)
.FirstOrDefaultAsync(x =>
x.Id == id && x.ApplicantUserId == userId, cancellationToken);
if (adj is null) return NotFound();
if (adj.Status != CourseAdjustmentStatus.Draft)
return ConflictProblem("只有草稿可以提交。");
adj.Status = CourseAdjustmentStatus.Submitted;
adj.SubmittedAt = DateTime.UtcNow;
await db.SaveChangesAsync(cancellationToken);
await NotifyReviewersAsync(adj, cancellationToken);
return NoContent();
}
// ═══════════════ Approve ═══════════════
[HttpPost("{id:guid}/approve")]
[Authorize(Roles = Reviewers)]
public async Task<ActionResult> Approve(Guid id, CancellationToken cancellationToken)
{
var adj = await db.CourseAdjustments
.Include(x => x.TeachingTask)
.ThenInclude(x => x!.Course)
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
if (adj is null) return NotFound();
var scope = currentUserDataScope.Current;
if (scope.Scope == DataScope.College &&
adj.TeachingTask!.Course!.CollegeId != scope.RestrictedCollegeId)
return ConflictProblem("只能审核本学院课程的调停课申请。");
if (adj.Status != CourseAdjustmentStatus.Submitted)
return ConflictProblem("只有待审核的申请可以审批。");
adj.Status = CourseAdjustmentStatus.Approved;
adj.ReviewedAt = DateTime.UtcNow;
adj.ReviewedByUserId = currentUserDataScope.Current.UserId;
await db.SaveChangesAsync(cancellationToken);
await NotifyApplicantAsync(adj, "已通过", cancellationToken);
return NoContent();
}
// ═══════════════ Reject ═══════════════
[HttpPost("{id:guid}/reject")]
[Authorize(Roles = Reviewers)]
public async Task<ActionResult> Reject(
Guid id,
RejectionRequest request,
CancellationToken cancellationToken)
{
var adj = await db.CourseAdjustments
.Include(x => x.TeachingTask)
.ThenInclude(x => x!.Course)
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
if (adj is null) return NotFound();
var scope = currentUserDataScope.Current;
if (scope.Scope == DataScope.College &&
adj.TeachingTask!.Course!.CollegeId != scope.RestrictedCollegeId)
return ConflictProblem("只能审核本学院课程的调停课申请。");
if (adj.Status != CourseAdjustmentStatus.Submitted)
return ConflictProblem("只有待审核的申请可以退回。");
adj.Status = CourseAdjustmentStatus.Rejected;
adj.ReviewComment = request.Comment?.Trim();
adj.ReviewedAt = DateTime.UtcNow;
adj.ReviewedByUserId = currentUserDataScope.Current.UserId;
await db.SaveChangesAsync(cancellationToken);
await NotifyApplicantAsync(adj,
$"已退回" + (adj.ReviewComment is not null ? $"{adj.ReviewComment}" : ""),
cancellationToken);
return NoContent();
}
// ═══════════════ Notifications ═══════════════
[HttpGet("notifications")]
[Authorize]
public async Task<ActionResult> GetNotifications(
bool? unreadOnly,
CancellationToken cancellationToken)
{
var userId = currentUserDataScope.Current.UserId;
var source = db.Notifications.AsNoTracking()
.Where(x => x.UserId == userId);
if (unreadOnly == true)
source = source.Where(x => !x.IsRead);
return Ok(new
{
Items = await source.OrderByDescending(x => x.CreatedAt)
.Take(50)
.Select(x => new
{
x.Id, x.Title, x.Content, x.IsRead, x.LinkUrl, x.CreatedAt
})
.ToListAsync(cancellationToken),
UnreadCount = await db.Notifications
.CountAsync(x => x.UserId == userId && !x.IsRead, cancellationToken)
});
}
[HttpPost("notifications/{id:guid}/read")]
[Authorize]
public async Task<ActionResult> MarkRead(Guid id, CancellationToken cancellationToken)
{
var userId = currentUserDataScope.Current.UserId;
var n = await db.Notifications
.FirstOrDefaultAsync(x => x.Id == id && x.UserId == userId, cancellationToken);
if (n is null) return NotFound();
n.IsRead = true;
await db.SaveChangesAsync(cancellationToken);
return NoContent();
}
[HttpPost("notifications/read-all")]
[Authorize]
public async Task<ActionResult> MarkAllRead(CancellationToken cancellationToken)
{
var userId = currentUserDataScope.Current.UserId;
await db.Notifications
.Where(x => x.UserId == userId && !x.IsRead)
.ExecuteUpdateAsync(s => s.SetProperty(x => x.IsRead, true),
cancellationToken);
return NoContent();
}
// ═══════════════ Helpers ═══════════════
private async Task NotifyReviewersAsync(
CourseAdjustment adj, CancellationToken ct)
{
var typeLabel = adj.Type switch
{
CourseAdjustmentType.Reschedule => "调课",
CourseAdjustmentType.Cancel => "停课",
CourseAdjustmentType.Makeup => "补课",
CourseAdjustmentType.Substitute => "代课",
_ => "调停课"
};
var taskInfo = adj.TeachingTask is not null
? $"{adj.TeachingTask.Course!.Name}({adj.TeachingTask.TaskNumber})"
: "";
// Notify CollegeAdmin and AcademicAdmin of the course's college
var collegeId = adj.TeachingTask?.Course?.CollegeId;
if (!collegeId.HasValue) return;
var reviewerUserIds = await db.Users
.Join(db.UserRoles, u => u.Id, ur => ur.UserId, (u, ur) => new { u.Id, ur.RoleId })
.Join(db.Roles, x => x.RoleId, r => r.Id, (x, r) => new { x.Id, RoleName = r.Name! })
.Where(x =>
(x.RoleName == SystemRoles.AcademicAdmin) ||
(x.RoleName == SystemRoles.CollegeAdmin &&
db.Teachers.Any(t =>
t.UserId == x.Id && t.CollegeId == collegeId)))
.Select(x => x.Id)
.Distinct()
.ToListAsync(ct);
foreach (var reviewerId in reviewerUserIds)
{
db.Notifications.Add(new Notification
{
UserId = reviewerId,
Title = $"新的{typeLabel}申请",
Content = $"{taskInfo} 提交了{typeLabel}申请,请及时审核。",
LinkUrl = "/course-adjustments"
});
}
await db.SaveChangesAsync(ct);
}
private async Task NotifyApplicantAsync(
CourseAdjustment adj, string result, CancellationToken ct)
{
var typeLabel = adj.Type switch
{
CourseAdjustmentType.Reschedule => "调课",
CourseAdjustmentType.Cancel => "停课",
CourseAdjustmentType.Makeup => "补课",
CourseAdjustmentType.Substitute => "代课",
_ => "调停课"
};
db.Notifications.Add(new Notification
{
UserId = adj.ApplicantUserId,
Title = $"{typeLabel}申请{result}",
Content = adj.ReviewComment is not null
? $"您的{typeLabel}申请{result}。审核意见:{adj.ReviewComment}"
: $"您的{typeLabel}申请{result}。",
LinkUrl = "/course-adjustments"
});
await db.SaveChangesAsync(ct);
}
private async Task<ActionResult?> ValidateRequestAsync(
CourseAdjustmentRequest request,
CancellationToken cancellationToken)
{
var task = await db.TeachingTasks.AsNoTracking()
.Include(x => x.Course)
.FirstOrDefaultAsync(x =>
x.Id == request.TeachingTaskId &&
x.Status == TeachingTaskStatus.Published,
cancellationToken);
if (task is null) return ValidationProblem("教学班不存在或未发布。");
switch (request.Type)
{
case CourseAdjustmentType.Reschedule:
case CourseAdjustmentType.Makeup:
if (!request.DayOfWeek.HasValue || request.DayOfWeek is < 1 or > 7)
return ValidationProblem("请选择有效的上课日。");
if (!request.StartPeriod.HasValue || request.StartPeriod < 1)
return ValidationProblem("请选择起始节次。");
if (!request.PeriodCount.HasValue || request.PeriodCount < 1)
return ValidationProblem("请选择持续节数。");
if (request.Type == CourseAdjustmentType.Makeup &&
!request.TargetDate.HasValue)
return ValidationProblem("补课必须指定日期。");
break;
case CourseAdjustmentType.Substitute:
if (!request.SubstituteTeacherId.HasValue)
return ValidationProblem("请选择代课教师。");
if (!await db.Teachers.AnyAsync(x =>
x.Id == request.SubstituteTeacherId &&
x.Status == TeacherStatus.Active, cancellationToken))
return ValidationProblem("代课教师不存在或已离职。");
break;
case CourseAdjustmentType.Cancel:
if (!request.CancelWeek.HasValue && !request.CancelDate.HasValue)
return ValidationProblem("停课需指定周次或日期。");
break;
}
return null;
}
private static System.Linq.Expressions.Expression<
Func<CourseAdjustment, object>> AdjustmentProjection() => x => new
{
x.Id,
x.TeachingTaskId,
x.TeachingTask!.TaskNumber,
TaskName = x.TeachingTask.Name,
CourseCode = x.TeachingTask.Course!.Code,
CourseName = x.TeachingTask.Course.Name,
CollegeName = x.TeachingTask.Course.College!.Name,
TeacherNames = x.TeachingTask.Teachers
.OrderByDescending(t => t.IsPrimary)
.Select(t => t.Teacher!.Name),
x.Type,
x.Status,
x.Reason,
x.ReviewComment,
x.TargetDate,
x.DayOfWeek,
x.StartPeriod,
x.PeriodCount,
ClassroomName = x.Classroom != null ? x.Classroom.Name : null,
BuildingName = x.Classroom != null ? x.Classroom.Building!.Name : null,
SubstituteTeacherName = x.SubstituteTeacher != null ? x.SubstituteTeacher.Name : null,
x.CancelWeek,
x.CancelDate,
x.ApplicantUserId,
x.SubmittedAt,
x.ReviewedAt,
x.CreatedAt
};
private ActionResult ConflictProblem(string detail) =>
Conflict(new ProblemDetails
{
Title = "无法完成操作",
Detail = detail,
Status = StatusCodes.Status409Conflict
});
}
// ═══════════════ Request records ═══════════════
public sealed record CourseAdjustmentRequest(
Guid TeachingTaskId,
CourseAdjustmentType Type,
[Required, MaxLength(500)] string Reason,
bool Submit,
DateOnly? TargetDate,
[Range(1, 7)] int? DayOfWeek,
[Range(1, 30)] int? StartPeriod,
[Range(1, 6)] int? PeriodCount,
Guid? ClassroomId,
Guid? SubstituteTeacherId,
int? CancelWeek,
DateOnly? CancelDate);
public sealed record RejectionRequest(
[MaxLength(500)] string? Comment);
@@ -0,0 +1,50 @@
using Jiaowu.Api.Domain.Common;
namespace Jiaowu.Api.Domain.Academic;
public sealed class CourseAdjustment : EntityBase
{
public Guid TeachingTaskId { get; set; }
public TeachingTask? TeachingTask { get; set; }
public CourseAdjustmentType Type { get; set; }
public CourseAdjustmentStatus Status { get; set; } = CourseAdjustmentStatus.Draft;
public Guid ApplicantUserId { get; set; }
// For Reschedule / Makeup
public DateOnly? TargetDate { get; set; }
public int? DayOfWeek { get; set; }
public int? StartPeriod { get; set; }
public int? PeriodCount { get; set; }
public Guid? ClassroomId { get; set; }
public Classroom? Classroom { get; set; }
// For Substitute
public Guid? SubstituteTeacherId { get; set; }
public Teacher? SubstituteTeacher { get; set; }
// For Cancel
public int? CancelWeek { get; set; }
public DateOnly? CancelDate { get; set; }
public string Reason { get; set; } = "";
public string? ReviewComment { get; set; }
public DateTime? SubmittedAt { get; set; }
public DateTime? ReviewedAt { get; set; }
public Guid? ReviewedByUserId { get; set; }
}
public enum CourseAdjustmentType
{
Reschedule = 1,
Cancel = 2,
Makeup = 3,
Substitute = 4
}
public enum CourseAdjustmentStatus
{
Draft = 1,
Submitted = 2,
Approved = 3,
Rejected = 4
}
@@ -0,0 +1,12 @@
using Jiaowu.Api.Domain.Common;
namespace Jiaowu.Api.Domain.Academic;
public sealed class Notification : EntityBase
{
public Guid UserId { get; set; }
public required string Title { get; set; }
public required string Content { get; set; }
public bool IsRead { get; set; }
public string? LinkUrl { get; set; }
}
@@ -55,6 +55,8 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
public DbSet<ExamSession> ExamSessions => Set<ExamSession>();
public DbSet<ExamSessionInvigilator> ExamSessionInvigilators =>
Set<ExamSessionInvigilator>();
public DbSet<CourseAdjustment> CourseAdjustments => Set<CourseAdjustment>();
public DbSet<Notification> Notifications => Set<Notification>();
public DbSet<StudentStatusChange> StudentStatusChanges =>
Set<StudentStatusChange>();
public DbSet<GraduationAuditBatch> GraduationAuditBatches =>
@@ -668,6 +670,30 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
.HasForeignKey(x => x.StudentId).OnDelete(DeleteBehavior.Restrict);
});
builder.Entity<CourseAdjustment>(entity =>
{
entity.Property(x => x.Reason).HasMaxLength(500);
entity.Property(x => x.ReviewComment).HasMaxLength(500);
entity.HasIndex(x => new { x.TeachingTaskId, x.Status });
entity.HasIndex(x => x.ApplicantUserId);
entity.HasIndex(x => new { x.Status, x.CreatedAt });
entity.HasOne(x => x.TeachingTask).WithMany()
.HasForeignKey(x => x.TeachingTaskId).OnDelete(DeleteBehavior.Restrict);
entity.HasOne(x => x.Classroom).WithMany()
.HasForeignKey(x => x.ClassroomId).OnDelete(DeleteBehavior.SetNull);
entity.HasOne(x => x.SubstituteTeacher).WithMany()
.HasForeignKey(x => x.SubstituteTeacherId).OnDelete(DeleteBehavior.SetNull);
});
builder.Entity<Notification>(entity =>
{
entity.Property(x => x.Title).HasMaxLength(200);
entity.Property(x => x.Content).HasMaxLength(1000);
entity.Property(x => x.LinkUrl).HasMaxLength(300);
entity.HasIndex(x => new { x.UserId, x.IsRead });
entity.HasIndex(x => x.CreatedAt);
});
builder.Entity<AuditLog>(entity =>
{
entity.Property(x => x.Method).HasMaxLength(10);
@@ -36,6 +36,8 @@ public sealed class DevelopmentSqliteMigrator(
"20260725_20_exam_scheduling_optimization";
private const string RetakeEnrollmentMigration =
"20260725_21_retake_enrollment";
private const string CourseAdjustmentsMigration =
"20260725_22_course_adjustments";
public async Task MigrateAsync(CancellationToken cancellationToken = default)
{
@@ -240,6 +242,19 @@ public sealed class DevelopmentSqliteMigrator(
RetakeEnrollmentMigration,
retakeEnrollmentExists ? [] : RetakeEnrollmentStatements,
cancellationToken);
var courseAdjustmentsExist = await db.Database
.SqlQueryRaw<int>(
"""
SELECT COUNT(*) AS "Value"
FROM sqlite_master
WHERE type = 'table' AND name = 'CourseAdjustments'
""")
.AnyAsync(value => value > 0, cancellationToken);
await ApplyMigrationAsync(
CourseAdjustmentsMigration,
courseAdjustmentsExist ? [] : CourseAdjustmentsStatements,
cancellationToken);
}
private async Task ApplyMigrationAsync(
@@ -1484,4 +1499,58 @@ public sealed class DevelopmentSqliteMigrator(
ADD COLUMN "EnrollmentType" INTEGER NOT NULL DEFAULT 1;
"""
];
private static readonly string[] CourseAdjustmentsStatements =
[
"""
CREATE TABLE "CourseAdjustments" (
"Id" TEXT NOT NULL CONSTRAINT "PK_CourseAdjustments" PRIMARY KEY,
"TeachingTaskId" TEXT NOT NULL,
"Type" INTEGER NOT NULL,
"Status" INTEGER NOT NULL,
"ApplicantUserId" TEXT NOT NULL,
"TargetDate" TEXT NULL,
"DayOfWeek" INTEGER NULL,
"StartPeriod" INTEGER NULL,
"PeriodCount" INTEGER NULL,
"ClassroomId" TEXT NULL,
"SubstituteTeacherId" TEXT NULL,
"CancelWeek" INTEGER NULL,
"CancelDate" TEXT NULL,
"Reason" TEXT NOT NULL,
"ReviewComment" TEXT NULL,
"SubmittedAt" TEXT NULL,
"ReviewedAt" TEXT NULL,
"ReviewedByUserId" TEXT NULL,
"CreatedAt" TEXT NOT NULL,
"UpdatedAt" TEXT NOT NULL,
CONSTRAINT "FK_CourseAdjustments_TeachingTasks"
FOREIGN KEY ("TeachingTaskId") REFERENCES "TeachingTasks" ("Id")
ON DELETE RESTRICT,
CONSTRAINT "FK_CourseAdjustments_Classrooms"
FOREIGN KEY ("ClassroomId") REFERENCES "Classrooms" ("Id")
ON DELETE SET NULL,
CONSTRAINT "FK_CourseAdjustments_Teachers"
FOREIGN KEY ("SubstituteTeacherId") REFERENCES "Teachers" ("Id")
ON DELETE SET NULL
);
""",
"""CREATE INDEX "IX_CourseAdjustments_TeachingTaskId_Status" ON "CourseAdjustments" ("TeachingTaskId", "Status");""",
"""CREATE INDEX "IX_CourseAdjustments_ApplicantUserId" ON "CourseAdjustments" ("ApplicantUserId");""",
"""CREATE INDEX "IX_CourseAdjustments_Status_CreatedAt" ON "CourseAdjustments" ("Status", "CreatedAt");""",
"""
CREATE TABLE "Notifications" (
"Id" TEXT NOT NULL CONSTRAINT "PK_Notifications" PRIMARY KEY,
"UserId" TEXT NOT NULL,
"Title" TEXT NOT NULL,
"Content" TEXT NOT NULL,
"IsRead" INTEGER NOT NULL,
"LinkUrl" TEXT NULL,
"CreatedAt" TEXT NOT NULL,
"UpdatedAt" TEXT NOT NULL
);
""",
"""CREATE INDEX "IX_Notifications_UserId_IsRead" ON "Notifications" ("UserId", "IsRead");""",
"""CREATE INDEX "IX_Notifications_CreatedAt" ON "Notifications" ("CreatedAt");"""
];
}
@@ -0,0 +1,125 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
{
/// <inheritdoc />
public partial class CourseAdjustments : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "CourseAdjustments",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false),
TeachingTaskId = table.Column<Guid>(type: "char(36)", nullable: false),
Type = table.Column<int>(type: "int", nullable: false),
Status = table.Column<int>(type: "int", nullable: false),
ApplicantUserId = table.Column<Guid>(type: "char(36)", nullable: false),
TargetDate = table.Column<DateOnly>(type: "date", nullable: true),
DayOfWeek = table.Column<int>(type: "int", nullable: true),
StartPeriod = table.Column<int>(type: "int", nullable: true),
PeriodCount = table.Column<int>(type: "int", nullable: true),
ClassroomId = table.Column<Guid>(type: "char(36)", nullable: true),
SubstituteTeacherId = table.Column<Guid>(type: "char(36)", nullable: true),
CancelWeek = table.Column<int>(type: "int", nullable: true),
CancelDate = table.Column<DateOnly>(type: "date", nullable: true),
Reason = table.Column<string>(type: "varchar(500)", maxLength: 500, nullable: false),
ReviewComment = table.Column<string>(type: "varchar(500)", maxLength: 500, nullable: true),
SubmittedAt = table.Column<DateTime>(type: "datetime(6)", nullable: true),
ReviewedAt = table.Column<DateTime>(type: "datetime(6)", nullable: true),
ReviewedByUserId = table.Column<Guid>(type: "char(36)", nullable: true),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_CourseAdjustments", x => x.Id);
table.ForeignKey(
name: "FK_CourseAdjustments_Classrooms_ClassroomId",
column: x => x.ClassroomId,
principalTable: "Classrooms",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
table.ForeignKey(
name: "FK_CourseAdjustments_Teachers_SubstituteTeacherId",
column: x => x.SubstituteTeacherId,
principalTable: "Teachers",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
table.ForeignKey(
name: "FK_CourseAdjustments_TeachingTasks_TeachingTaskId",
column: x => x.TeachingTaskId,
principalTable: "TeachingTasks",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
})
.Annotation("MySQL:Charset", "utf8mb4");
migrationBuilder.CreateTable(
name: "Notifications",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false),
UserId = table.Column<Guid>(type: "char(36)", nullable: false),
Title = table.Column<string>(type: "varchar(200)", maxLength: 200, nullable: false),
Content = table.Column<string>(type: "varchar(1000)", maxLength: 1000, nullable: false),
IsRead = table.Column<bool>(type: "tinyint(1)", nullable: false),
LinkUrl = table.Column<string>(type: "varchar(300)", maxLength: 300, nullable: true),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Notifications", x => x.Id);
})
.Annotation("MySQL:Charset", "utf8mb4");
migrationBuilder.CreateIndex(
name: "IX_CourseAdjustments_ApplicantUserId",
table: "CourseAdjustments",
column: "ApplicantUserId");
migrationBuilder.CreateIndex(
name: "IX_CourseAdjustments_ClassroomId",
table: "CourseAdjustments",
column: "ClassroomId");
migrationBuilder.CreateIndex(
name: "IX_CourseAdjustments_Status_CreatedAt",
table: "CourseAdjustments",
columns: new[] { "Status", "CreatedAt" });
migrationBuilder.CreateIndex(
name: "IX_CourseAdjustments_SubstituteTeacherId",
table: "CourseAdjustments",
column: "SubstituteTeacherId");
migrationBuilder.CreateIndex(
name: "IX_CourseAdjustments_TeachingTaskId_Status",
table: "CourseAdjustments",
columns: new[] { "TeachingTaskId", "Status" });
migrationBuilder.CreateIndex(
name: "IX_Notifications_CreatedAt",
table: "Notifications",
column: "CreatedAt");
migrationBuilder.CreateIndex(
name: "IX_Notifications_UserId_IsRead",
table: "Notifications",
columns: new[] { "UserId", "IsRead" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(name: "Notifications");
migrationBuilder.DropTable(name: "CourseAdjustments");
}
}
}