数据库脚本修复
This commit is contained in:
1 parent
b7b7c25d78
commit
14af0910e7
7 files changed
+500
-1
No files matched your search
@@ -0,0 +1,135 @@
|
|||||||
|
using Jiaowu.Api.Contracts;
|
||||||
|
using Jiaowu.Api.Domain.Academic;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using Jiaowu.Api.Domain.Identity;
|
||||||
|
using Jiaowu.Api.Infrastructure.Excel;
|
||||||
|
using Jiaowu.Api.Infrastructure.Files;
|
||||||
|
using Jiaowu.Api.Infrastructure.Persistence;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Jiaowu.Api.Controllers;
|
||||||
|
|
||||||
|
[ApiController, Authorize, Route("api/tuition")]
|
||||||
|
public sealed class TuitionController(AppDbContext db) : ControllerBase
|
||||||
|
{
|
||||||
|
private const string Managers = SystemRoles.SuperAdmin + "," + SystemRoles.AcademicAdmin;
|
||||||
|
|
||||||
|
[HttpGet("terms"), Authorize(Roles = Managers)]
|
||||||
|
public async Task<ActionResult> Terms(CancellationToken ct) => Ok(await db.AcademicTerms.AsNoTracking().OrderByDescending(x => x.StartDate).Select(x => new { x.Id, x.Name, x.IsCurrent, Setting = db.TuitionTermSettings.Where(s => s.AcademicTermId == x.Id).Select(s => new { s.CreditUnitPrice, s.Status, s.CreditsLockedAt }).FirstOrDefault() }).ToListAsync(ct));
|
||||||
|
|
||||||
|
[HttpPut("terms/{termId:guid}/credit-price"), Authorize(Roles = Managers)]
|
||||||
|
public async Task<ActionResult> SetCreditPrice(Guid termId, TuitionPriceRequest request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (request.CreditUnitPrice < 0) return BadRequest(new { message = "每学分金额不能为负数。" });
|
||||||
|
if (!await db.AcademicTerms.AnyAsync(x => x.Id == termId, ct)) return NotFound();
|
||||||
|
var setting = await db.TuitionTermSettings.SingleOrDefaultAsync(x => x.AcademicTermId == termId, ct);
|
||||||
|
if (setting is null) { setting = new TuitionTermSetting { AcademicTermId = termId, CreditUnitPrice = request.CreditUnitPrice }; db.TuitionTermSettings.Add(setting); }
|
||||||
|
else if (setting.Status != TuitionTermStatus.Draft) return Conflict(new { message = "学分费已锁定,不能修改单价。" });
|
||||||
|
else setting.CreditUnitPrice = request.CreditUnitPrice;
|
||||||
|
await db.SaveChangesAsync(ct); return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("terms/{termId:guid}/import-template.xlsx"), Authorize(Roles = Managers)]
|
||||||
|
public IActionResult Template(Guid termId) => File(ExcelWorkbookHelper.Create("学费名单导入", ["学号", "基础学费", "住宿费"], [], ["按学号匹配学生;仅覆盖未缴的基础学费和住宿费。", "已缴项目的差额不会覆盖,将自动记为期末多退少补调整项。"]), ExcelWorkbookHelper.ContentType, DownloadFileName.Template("学费名单", ".xlsx"));
|
||||||
|
|
||||||
|
[HttpPost("terms/{termId:guid}/import"), Authorize(Roles = Managers)]
|
||||||
|
public async Task<ActionResult> Import(Guid termId, IFormFile file, CancellationToken ct)
|
||||||
|
{
|
||||||
|
IReadOnlyList<ExcelRow> rows; try { rows = await ExcelWorkbookHelper.ReadAsync(file, ["学号", "基础学费", "住宿费"], ct); } catch (InvalidDataException ex) { return BadRequest(new { message = ex.Message }); }
|
||||||
|
if (!await db.AcademicTerms.AnyAsync(x => x.Id == termId, ct)) return NotFound();
|
||||||
|
if (rows.Count == 0) return BadRequest(new { message = "Excel 中没有可导入的数据行,请从第 2 行开始填写。" });
|
||||||
|
var errors = new List<string>();
|
||||||
|
var inputs = new List<(int RowNumber, string Number, decimal Base, decimal Accommodation)>();
|
||||||
|
foreach (var row in rows)
|
||||||
|
{
|
||||||
|
var number = row["学号"];
|
||||||
|
if (string.IsNullOrWhiteSpace(number)) { errors.Add($"第 {row.RowNumber} 行:学号不能为空。"); continue; }
|
||||||
|
if (!decimal.TryParse(row["基础学费"], out var baseTuition) || baseTuition < 0)
|
||||||
|
errors.Add($"第 {row.RowNumber} 行:基础学费必须是大于等于 0 的数字。");
|
||||||
|
if (!decimal.TryParse(row["住宿费"], out var accommodation) || accommodation < 0)
|
||||||
|
errors.Add($"第 {row.RowNumber} 行:住宿费必须是大于等于 0 的数字。");
|
||||||
|
if (baseTuition >= 0 && accommodation >= 0) inputs.Add((row.RowNumber, number, baseTuition, accommodation));
|
||||||
|
}
|
||||||
|
if (errors.Count > 0) return BadRequest(new { message = string.Join(";", errors.Take(20)) });
|
||||||
|
if (inputs.GroupBy(x => x.Number).Any(x => x.Count() > 1)) return BadRequest(new { message = "Excel 中存在重复学号,请每名学生只保留一行。" });
|
||||||
|
var students = await db.Students.Where(x => inputs.Select(i => i.Number).Contains(x.StudentNumber)).ToDictionaryAsync(x => x.StudentNumber, ct);
|
||||||
|
var missingNumbers = inputs.Where(x => !students.ContainsKey(x.Number)).Select(x => $"第 {x.RowNumber} 行({x.Number})").Take(20).ToArray();
|
||||||
|
if (missingNumbers.Length > 0) return BadRequest(new { message = $"以下学号未匹配学生:{string.Join("、", missingNumbers)}。" });
|
||||||
|
foreach (var input in inputs)
|
||||||
|
{
|
||||||
|
var bill = await db.TuitionBills.SingleOrDefaultAsync(x => x.AcademicTermId == termId && x.StudentId == students[input.Number].Id, ct);
|
||||||
|
if (bill is null) { bill = new TuitionBill { AcademicTermId = termId, StudentId = students[input.Number].Id, BaseTuition = input.Base, AccommodationFee = input.Accommodation }; db.TuitionBills.Add(bill); continue; }
|
||||||
|
if (bill.BasePaid == 0) bill.BaseTuition = input.Base; else bill.ReconciliationAdjustment += input.Base - bill.BaseTuition;
|
||||||
|
if (bill.AccommodationPaid == 0) bill.AccommodationFee = input.Accommodation; else bill.ReconciliationAdjustment += input.Accommodation - bill.AccommodationFee;
|
||||||
|
}
|
||||||
|
await db.SaveChangesAsync(ct); return Ok(new { imported = inputs.Count });
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("terms/{termId:guid}/lock-credits"), Authorize(Roles = Managers)]
|
||||||
|
public async Task<ActionResult> LockCredits(Guid termId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var setting = await db.TuitionTermSettings.SingleOrDefaultAsync(x => x.AcademicTermId == termId, ct);
|
||||||
|
if (setting is null) return BadRequest(new { message = "请先设定每学分金额。" });
|
||||||
|
if (setting.Status != TuitionTermStatus.Draft) return Conflict(new { message = "学分费已经锁定。" });
|
||||||
|
if (await db.CourseSelectionRounds.AnyAsync(x => x.AcademicTermId == termId && x.Status != CourseSelectionRoundStatus.Closed, ct)) return Conflict(new { message = "本学期仍有未结束的选课轮次,不能锁定学分费。" });
|
||||||
|
var credits = await db.CourseEnrollments.Where(x => x.CourseSelectionOffering!.CourseSelectionRound!.AcademicTermId == termId && x.Status == CourseEnrollmentStatus.Enrolled).GroupBy(x => x.StudentId).Select(x => new { StudentId = x.Key, Credits = x.Sum(e => e.CourseSelectionOffering!.TeachingTask!.Course!.Credits) }).ToListAsync(ct);
|
||||||
|
foreach (var credit in credits) { var bill = await db.TuitionBills.SingleOrDefaultAsync(x => x.AcademicTermId == termId && x.StudentId == credit.StudentId, ct); if (bill is null) { bill = new TuitionBill { AcademicTermId = termId, StudentId = credit.StudentId }; db.TuitionBills.Add(bill); } bill.CreditCount = credit.Credits; bill.CreditFee = credit.Credits * setting.CreditUnitPrice; }
|
||||||
|
setting.Status = TuitionTermStatus.CreditsLocked; setting.CreditsLockedAt = DateTime.UtcNow;
|
||||||
|
await db.SaveChangesAsync(ct); return Ok(new { locked = credits.Count });
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("terms/{termId:guid}/open-payment"), Authorize(Roles = Managers)]
|
||||||
|
public async Task<ActionResult> OpenPayment(Guid termId, CancellationToken ct) { var setting = await db.TuitionTermSettings.SingleOrDefaultAsync(x => x.AcademicTermId == termId, ct); if (setting is null || setting.Status != TuitionTermStatus.CreditsLocked) return Conflict(new { message = "请先锁定学分费。" }); setting.Status = TuitionTermStatus.OpenForPayment; await db.SaveChangesAsync(ct); return NoContent(); }
|
||||||
|
|
||||||
|
[HttpGet("bills"), Authorize(Roles = Managers)]
|
||||||
|
public async Task<ActionResult> Bills(Guid termId, [FromQuery] string? keyword, [FromQuery] int page = 1, [FromQuery] int pageSize = 20, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var source = db.TuitionBills.AsNoTracking().Where(x => x.AcademicTermId == termId);
|
||||||
|
if (!string.IsNullOrWhiteSpace(keyword))
|
||||||
|
{
|
||||||
|
var search = keyword.Trim();
|
||||||
|
source = source.Where(x => x.Student!.StudentNumber.Contains(search) || x.Student.Name.Contains(search));
|
||||||
|
}
|
||||||
|
page = Math.Max(1, page); pageSize = Math.Clamp(pageSize, 1, 100);
|
||||||
|
var total = await source.CountAsync(ct);
|
||||||
|
var items = await source.OrderBy(x => x.Student!.StudentNumber).Skip((page - 1) * pageSize).Take(pageSize)
|
||||||
|
.Select(x => new { x.Id, x.StudentId, x.Student!.StudentNumber, StudentName = x.Student.Name, x.BaseTuition, x.AccommodationFee, x.CreditCount, x.CreditFee, x.ReconciliationAdjustment, x.BasePaid, x.AccommodationPaid, x.CreditPaid, x.AdjustmentPaid })
|
||||||
|
.ToListAsync(ct);
|
||||||
|
return Ok(new PagedResult<object>(items.Cast<object>().ToList(), total, page, pageSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("bills/{billId:guid}/reconcile"), Authorize(Roles = Managers)]
|
||||||
|
public async Task<ActionResult> Reconcile(Guid billId, ReconcileRequest request, CancellationToken ct) { var bill = await db.TuitionBills.FindAsync([billId], ct); if (bill is null) return NotFound(); bill.ReconciliationAdjustment += request.Amount; await db.SaveChangesAsync(ct); return NoContent(); }
|
||||||
|
|
||||||
|
[HttpGet("mine"), Authorize(Roles = SystemRoles.Student)]
|
||||||
|
public async Task<ActionResult> Mine(CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (!Guid.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier), out var userId)) return Forbid();
|
||||||
|
return Ok(await db.TuitionBills.AsNoTracking().Where(x => x.Student!.UserId == userId).OrderByDescending(x => x.AcademicTerm!.StartDate).Select(x => new { x.Id, TermName = x.AcademicTerm!.Name, x.BaseTuition, x.AccommodationFee, x.CreditCount, x.CreditFee, x.ReconciliationAdjustment, x.BasePaid, x.AccommodationPaid, x.CreditPaid, x.AdjustmentPaid, IsPayable = db.TuitionTermSettings.Any(s => s.AcademicTermId == x.AcademicTermId && s.Status == TuitionTermStatus.OpenForPayment) }).ToListAsync(ct));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("bills/{billId:guid}/items/{item}/pay-balance"), Authorize(Roles = SystemRoles.Student)]
|
||||||
|
public async Task<ActionResult> PayBalance(Guid billId, TuitionFeeItem item, CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (!Guid.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier), out var userId)) return Forbid();
|
||||||
|
var bill = await db.TuitionBills.Include(x => x.Student).SingleOrDefaultAsync(x => x.Id == billId && x.Student!.UserId == userId, ct);
|
||||||
|
if (bill is null) return NotFound();
|
||||||
|
if (!await db.TuitionTermSettings.AnyAsync(x => x.AcademicTermId == bill.AcademicTermId && x.Status == TuitionTermStatus.OpenForPayment, ct)) return Conflict(new { message = "当前学期尚未开放缴费。" });
|
||||||
|
var due = Due(bill, item); if (due <= 0) return Conflict(new { message = "该项目没有待缴金额。" });
|
||||||
|
var wallet = await db.CampusWalletAccounts.SingleOrDefaultAsync(x => x.StudentId == bill.StudentId, ct); if (wallet is null || wallet.Balance < due) return Conflict(new { message = "校园余额不足。" });
|
||||||
|
wallet.Balance -= due; AddPaid(bill, item, due); db.TuitionPayments.Add(new TuitionPayment { TuitionBillId = bill.Id, Item = item, Amount = due, Method = TuitionPaymentMethod.Balance, ConfirmedByUserId = userId, ConfirmedAt = DateTime.UtcNow }); await db.SaveChangesAsync(ct); return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("bills/{billId:guid}/items/{item}/confirm-offline"), Authorize(Roles = Managers)]
|
||||||
|
public async Task<ActionResult> ConfirmOffline(Guid billId, TuitionFeeItem item, OfflineTuitionPaymentRequest request, CancellationToken ct)
|
||||||
|
{ var bill = await db.TuitionBills.FindAsync([billId], ct); if (bill is null) return NotFound(); var due = Due(bill, item); if (due <= 0) return Conflict(new { message = "该项目没有待缴金额。" }); AddPaid(bill, item, due); db.TuitionPayments.Add(new TuitionPayment { TuitionBillId = bill.Id, Item = item, Amount = due, Method = TuitionPaymentMethod.Offline, ReferenceNumber = request.ReferenceNumber?.Trim(), ConfirmedByUserId = Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!), ConfirmedAt = DateTime.UtcNow }); await db.SaveChangesAsync(ct); return NoContent(); }
|
||||||
|
|
||||||
|
private static decimal Due(TuitionBill b, TuitionFeeItem item) => item switch { TuitionFeeItem.BaseTuition => b.BaseTuition - b.BasePaid, TuitionFeeItem.AccommodationFee => b.AccommodationFee - b.AccommodationPaid, TuitionFeeItem.CreditFee => b.CreditFee - b.CreditPaid, TuitionFeeItem.ReconciliationAdjustment => b.ReconciliationAdjustment - b.AdjustmentPaid, _ => 0 };
|
||||||
|
private static void AddPaid(TuitionBill b, TuitionFeeItem item, decimal amount) { if (item == TuitionFeeItem.BaseTuition) b.BasePaid += amount; else if (item == TuitionFeeItem.AccommodationFee) b.AccommodationPaid += amount; else if (item == TuitionFeeItem.CreditFee) b.CreditPaid += amount; else b.AdjustmentPaid += amount; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record TuitionPriceRequest(decimal CreditUnitPrice);
|
||||||
|
public sealed record ReconcileRequest(decimal Amount, string? Reason);
|
||||||
|
public sealed record OfflineTuitionPaymentRequest(string? ReferenceNumber);
|
||||||
+74
@@ -0,0 +1,74 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql;
|
||||||
|
|
||||||
|
public partial class CampusRegistrationPayments : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
// MySQL DDL is not transactional. IF NOT EXISTS makes a retry safe when
|
||||||
|
// an earlier attempt created this first table but failed later in the migration.
|
||||||
|
migrationBuilder.Sql("""
|
||||||
|
CREATE TABLE IF NOT EXISTS `CampusRegistrationProjects` (
|
||||||
|
`Id` char(36) NOT NULL, `Code` varchar(40) NOT NULL, `Name` varchar(120) NOT NULL,
|
||||||
|
`Description` varchar(2000) NULL, `CollegeId` char(36) NULL,
|
||||||
|
`RegistrationStartsAt` datetime(6) NOT NULL, `RegistrationEndsAt` datetime(6) NOT NULL,
|
||||||
|
`Capacity` int NOT NULL, `FeeAmount` decimal(10,2) NOT NULL, `RequiresReview` tinyint(1) NOT NULL,
|
||||||
|
`Status` int NOT NULL, `ContactName` varchar(50) NULL, `ContactPhone` varchar(30) NULL,
|
||||||
|
`Notes` varchar(500) NULL, `CreatedByUserId` char(36) NOT NULL,
|
||||||
|
`CreatedAt` datetime(6) NOT NULL, `UpdatedAt` datetime(6) NOT NULL,
|
||||||
|
PRIMARY KEY (`Id`),
|
||||||
|
CONSTRAINT `FK_CRP_College` FOREIGN KEY (`CollegeId`) REFERENCES `Colleges` (`Id`) ON DELETE SET NULL,
|
||||||
|
CONSTRAINT `FK_CRP_Creator` FOREIGN KEY (`CreatedByUserId`) REFERENCES `AspNetUsers` (`Id`) ON DELETE RESTRICT
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
""");
|
||||||
|
migrationBuilder.Sql("""
|
||||||
|
CREATE TABLE IF NOT EXISTS `CampusRegistrationApplications` (
|
||||||
|
`Id` char(36) NOT NULL, `ProjectId` char(36) NOT NULL, `StudentId` char(36) NOT NULL, `Status` int NOT NULL,
|
||||||
|
`ApplicantNote` varchar(1000) NULL, `ReviewedByUserId` char(36) NULL, `ReviewedAt` datetime(6) NULL,
|
||||||
|
`ReviewComment` varchar(500) NULL, `ConfirmedAt` datetime(6) NULL, `CancelledAt` datetime(6) NULL,
|
||||||
|
`CreatedAt` datetime(6) NOT NULL, `UpdatedAt` datetime(6) NOT NULL, PRIMARY KEY (`Id`),
|
||||||
|
CONSTRAINT `FK_CRA_Project` FOREIGN KEY (`ProjectId`) REFERENCES `CampusRegistrationProjects` (`Id`) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT `FK_CRA_Student` FOREIGN KEY (`StudentId`) REFERENCES `Students` (`Id`) ON DELETE RESTRICT,
|
||||||
|
CONSTRAINT `FK_CRA_Reviewer` FOREIGN KEY (`ReviewedByUserId`) REFERENCES `AspNetUsers` (`Id`) ON DELETE SET NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
CREATE TABLE IF NOT EXISTS `CampusPaymentOrders` (
|
||||||
|
`Id` char(36) NOT NULL, `ApplicationId` char(36) NOT NULL, `OrderNumber` varchar(40) NOT NULL,
|
||||||
|
`Amount` decimal(10,2) NOT NULL, `PaymentMethod` int NOT NULL, `Status` int NOT NULL,
|
||||||
|
`ConfirmedByUserId` char(36) NULL, `ConfirmedAt` datetime(6) NULL, `ReferenceNumber` varchar(100) NULL,
|
||||||
|
`Note` varchar(500) NULL, `CreatedAt` datetime(6) NOT NULL, `UpdatedAt` datetime(6) NOT NULL, PRIMARY KEY (`Id`),
|
||||||
|
CONSTRAINT `FK_CPO_Application` FOREIGN KEY (`ApplicationId`) REFERENCES `CampusRegistrationApplications` (`Id`) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT `FK_CPO_Confirmer` FOREIGN KEY (`ConfirmedByUserId`) REFERENCES `AspNetUsers` (`Id`) ON DELETE SET NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
""");
|
||||||
|
|
||||||
|
CreateIndexIfMissing(migrationBuilder, "CampusRegistrationProjects", "IX_CampusRegistrationProjects_Code", "`Code`", unique: true);
|
||||||
|
CreateIndexIfMissing(migrationBuilder, "CampusRegistrationProjects", "IX_CRP_College_Status_Ends", "`CollegeId`, `Status`, `RegistrationEndsAt`");
|
||||||
|
CreateIndexIfMissing(migrationBuilder, "CampusRegistrationProjects", "IX_CampusRegistrationProjects_CreatedByUserId", "`CreatedByUserId`");
|
||||||
|
CreateIndexIfMissing(migrationBuilder, "CampusRegistrationApplications", "IX_CampusRegistrationApplications_ProjectId_StudentId", "`ProjectId`, `StudentId`", unique: true);
|
||||||
|
CreateIndexIfMissing(migrationBuilder, "CampusRegistrationApplications", "IX_CampusRegistrationApplications_ProjectId_Status_CreatedAt", "`ProjectId`, `Status`, `CreatedAt`");
|
||||||
|
CreateIndexIfMissing(migrationBuilder, "CampusRegistrationApplications", "IX_CampusRegistrationApplications_StudentId", "`StudentId`");
|
||||||
|
CreateIndexIfMissing(migrationBuilder, "CampusRegistrationApplications", "IX_CampusRegistrationApplications_ReviewedByUserId", "`ReviewedByUserId`");
|
||||||
|
CreateIndexIfMissing(migrationBuilder, "CampusPaymentOrders", "IX_CampusPaymentOrders_ApplicationId", "`ApplicationId`", unique: true);
|
||||||
|
CreateIndexIfMissing(migrationBuilder, "CampusPaymentOrders", "IX_CampusPaymentOrders_OrderNumber", "`OrderNumber`", unique: true);
|
||||||
|
CreateIndexIfMissing(migrationBuilder, "CampusPaymentOrders", "IX_CampusPaymentOrders_Status_CreatedAt", "`Status`, `CreatedAt`");
|
||||||
|
CreateIndexIfMissing(migrationBuilder, "CampusPaymentOrders", "IX_CampusPaymentOrders_ConfirmedByUserId", "`ConfirmedByUserId`");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CreateIndexIfMissing(MigrationBuilder migrationBuilder, string table, string name, string columns, bool unique = false)
|
||||||
|
{
|
||||||
|
var statement = $"CREATE {(unique ? "UNIQUE " : string.Empty)}INDEX `{name}` ON `{table}` ({columns})";
|
||||||
|
migrationBuilder.Sql($"""
|
||||||
|
SET @index_exists = (SELECT COUNT(*) FROM `INFORMATION_SCHEMA`.`STATISTICS`
|
||||||
|
WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = '{table}' AND `INDEX_NAME` = '{name}');
|
||||||
|
SET @create_index_sql = IF(@index_exists = 0, '{statement}', 'SELECT 1');
|
||||||
|
PREPARE create_index_stmt FROM @create_index_sql;
|
||||||
|
EXECUTE create_index_stmt;
|
||||||
|
DEALLOCATE PREPARE create_index_stmt;
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropTable(name: "CampusPaymentOrders"); migrationBuilder.DropTable(name: "CampusRegistrationApplications"); migrationBuilder.DropTable(name: "CampusRegistrationProjects"); }
|
||||||
|
}
|
||||||
@@ -314,6 +314,36 @@ public sealed class MySqlMigrationTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
<<<<<<< Updated upstream
|
||||||
|
=======
|
||||||
|
public void Campus_registration_migration_uses_mysql_safe_foreign_key_names()
|
||||||
|
{
|
||||||
|
using var db = new AppDbContext(CreateMySqlOptions());
|
||||||
|
|
||||||
|
var script = db.GetService<IMigrator>().GenerateScript(
|
||||||
|
fromMigration: "20260822085356_GradeAnalyticsCommentInteractions",
|
||||||
|
toMigration: "20260822100000_CampusWalletBalances");
|
||||||
|
|
||||||
|
Assert.Contains("CREATE TABLE IF NOT EXISTS `CampusRegistrationProjects`", script);
|
||||||
|
Assert.Contains("CREATE TABLE IF NOT EXISTS `CampusRegistrationApplications`", script);
|
||||||
|
Assert.Contains("CREATE TABLE IF NOT EXISTS `CampusPaymentOrders`", script);
|
||||||
|
Assert.Contains("@create_index_sql", script);
|
||||||
|
Assert.Contains("IX_CRP_College_Status_Ends", script);
|
||||||
|
Assert.DoesNotContain(
|
||||||
|
"IX_CampusRegistrationProjects_CollegeId_Status_RegistrationEndsAt",
|
||||||
|
script);
|
||||||
|
Assert.Contains("CONSTRAINT `FK_CRA_Project`", script);
|
||||||
|
Assert.DoesNotContain(
|
||||||
|
"FK_CampusRegistrationApplications_CampusRegistrationProjects_ProjectId",
|
||||||
|
script);
|
||||||
|
Assert.Contains("CONSTRAINT `FK_CWT_Account`", script);
|
||||||
|
Assert.DoesNotContain(
|
||||||
|
"FK_CampusWalletTransactions_CampusWalletAccounts_WalletAccountId",
|
||||||
|
script);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
>>>>>>> Stashed changes
|
||||||
public void Application_mysql_connections_allow_migration_user_variables()
|
public void Application_mysql_connections_allow_migration_user_variables()
|
||||||
{
|
{
|
||||||
var normalized = MySqlConnectionStringPolicy.ForApplication(
|
var normalized = MySqlConnectionStringPolicy.ForApplication(
|
||||||
|
|||||||
Loaded 3 of 7 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user