成绩通知:仅在某门课程成绩正式发布后自动发送,按成绩单学生名单通知,不展示具体分数;发布状态与通知同一次提交。
手动发信:移除“成绩通知”选项,只能发送普通通知,原有角色与范围权限保持不变。 选课通知:管理员关闭一轮选课时,自动向每位参与学生发送一条汇总通知,包含最终选中课程及候补未成功课程;候补失效、轮次关闭和通知生成保持原子性。 关闭选课界面会明确提示发送通知,并显示实际通知人数。
This commit is contained in:
@@ -77,6 +77,7 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
|
||||
public DbSet<EvaluationRecord> EvaluationRecords => Set<EvaluationRecord>();
|
||||
public DbSet<EvaluationScore> EvaluationScores => Set<EvaluationScore>();
|
||||
public DbSet<Notification> Notifications => Set<Notification>();
|
||||
public DbSet<MessageDispatch> MessageDispatches => Set<MessageDispatch>();
|
||||
public DbSet<StudentStatusChange> StudentStatusChanges =>
|
||||
Set<StudentStatusChange>();
|
||||
public DbSet<GraduationAuditBatch> GraduationAuditBatches =>
|
||||
@@ -919,7 +920,23 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
|
||||
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 => new { x.UserId, x.Category, x.CreatedAt });
|
||||
entity.HasIndex(x => x.MessageDispatchId);
|
||||
entity.HasIndex(x => x.CreatedAt);
|
||||
entity.HasOne(x => x.MessageDispatch)
|
||||
.WithMany(x => x.Notifications)
|
||||
.HasForeignKey(x => x.MessageDispatchId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
builder.Entity<MessageDispatch>(entity =>
|
||||
{
|
||||
entity.Property(x => x.SenderName).HasMaxLength(100);
|
||||
entity.Property(x => x.Title).HasMaxLength(200);
|
||||
entity.Property(x => x.Content).HasMaxLength(1000);
|
||||
entity.Property(x => x.AudienceName).HasMaxLength(200);
|
||||
entity.Property(x => x.LinkUrl).HasMaxLength(300);
|
||||
entity.HasIndex(x => new { x.SenderUserId, x.CreatedAt });
|
||||
});
|
||||
|
||||
builder.Entity<AuditLog>(entity =>
|
||||
|
||||
@@ -54,6 +54,8 @@ public sealed class DevelopmentSqliteMigrator(
|
||||
"20260726_29_personal_calendar_subscription";
|
||||
private const string OfficialDocumentsMigration =
|
||||
"20260726_30_official_documents";
|
||||
private const string UnifiedMessageCenterMigration =
|
||||
"20260726_31_unified_message_center";
|
||||
|
||||
public async Task MigrateAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -391,6 +393,19 @@ public sealed class DevelopmentSqliteMigrator(
|
||||
OfficialDocumentsMigration,
|
||||
officialDocumentsExist ? [] : OfficialDocumentStatements,
|
||||
cancellationToken);
|
||||
|
||||
var messageDispatchesExist = await db.Database
|
||||
.SqlQueryRaw<int>(
|
||||
"""
|
||||
SELECT COUNT(*) AS "Value"
|
||||
FROM sqlite_master
|
||||
WHERE type = 'table' AND name = 'MessageDispatches'
|
||||
""")
|
||||
.AnyAsync(value => value > 0, cancellationToken);
|
||||
await ApplyMigrationAsync(
|
||||
UnifiedMessageCenterMigration,
|
||||
messageDispatchesExist ? [] : UnifiedMessageCenterStatements,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private async Task ApplyMigrationAsync(
|
||||
@@ -1876,4 +1891,30 @@ public sealed class DevelopmentSqliteMigrator(
|
||||
"""CREATE INDEX "IX_MakeupExamAutoJobs_MakeupExamPlanId_CreatedAt" ON "MakeupExamAutoJobs" ("MakeupExamPlanId", "CreatedAt");""",
|
||||
"""CREATE INDEX "IX_MakeupExamAutoJobs_Status_CreatedAt" ON "MakeupExamAutoJobs" ("Status", "CreatedAt");""",
|
||||
];
|
||||
|
||||
private static readonly string[] UnifiedMessageCenterStatements =
|
||||
[
|
||||
"""
|
||||
CREATE TABLE "MessageDispatches" (
|
||||
"Id" TEXT NOT NULL CONSTRAINT "PK_MessageDispatches" PRIMARY KEY,
|
||||
"SenderUserId" TEXT NOT NULL,
|
||||
"SenderName" TEXT NOT NULL,
|
||||
"Title" TEXT NOT NULL,
|
||||
"Content" TEXT NOT NULL,
|
||||
"Category" INTEGER NOT NULL,
|
||||
"AudienceType" INTEGER NOT NULL,
|
||||
"AudienceId" TEXT NULL,
|
||||
"AudienceName" TEXT NOT NULL,
|
||||
"RecipientCount" INTEGER NOT NULL,
|
||||
"LinkUrl" TEXT NULL,
|
||||
"CreatedAt" TEXT NOT NULL,
|
||||
"UpdatedAt" TEXT NOT NULL
|
||||
);
|
||||
""",
|
||||
"""CREATE INDEX "IX_MessageDispatches_SenderUserId_CreatedAt" ON "MessageDispatches" ("SenderUserId", "CreatedAt");""",
|
||||
"""ALTER TABLE "Notifications" ADD COLUMN "Category" INTEGER NOT NULL DEFAULT 1;""",
|
||||
"""ALTER TABLE "Notifications" ADD COLUMN "MessageDispatchId" TEXT NULL REFERENCES "MessageDispatches" ("Id") ON DELETE CASCADE;""",
|
||||
"""CREATE INDEX "IX_Notifications_MessageDispatchId" ON "Notifications" ("MessageDispatchId");""",
|
||||
"""CREATE INDEX "IX_Notifications_UserId_Category_CreatedAt" ON "Notifications" ("UserId", "Category", "CreatedAt");"""
|
||||
];
|
||||
}
|
||||
|
||||
+4662
File diff suppressed because it is too large
Load Diff
+102
@@ -0,0 +1,102 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class UnifiedMessageCenter : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "Category",
|
||||
table: "Notifications",
|
||||
type: "int",
|
||||
nullable: false,
|
||||
defaultValue: 1);
|
||||
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "MessageDispatchId",
|
||||
table: "Notifications",
|
||||
type: "char(36)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "MessageDispatches",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "char(36)", nullable: false),
|
||||
SenderUserId = table.Column<Guid>(type: "char(36)", nullable: false),
|
||||
SenderName = table.Column<string>(type: "varchar(100)", maxLength: 100, nullable: false),
|
||||
Title = table.Column<string>(type: "varchar(200)", maxLength: 200, nullable: false),
|
||||
Content = table.Column<string>(type: "varchar(1000)", maxLength: 1000, nullable: false),
|
||||
Category = table.Column<int>(type: "int", nullable: false),
|
||||
AudienceType = table.Column<int>(type: "int", nullable: false),
|
||||
AudienceId = table.Column<Guid>(type: "char(36)", nullable: true),
|
||||
AudienceName = table.Column<string>(type: "varchar(200)", maxLength: 200, nullable: false),
|
||||
RecipientCount = table.Column<int>(type: "int", 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_MessageDispatches", x => x.Id);
|
||||
})
|
||||
.Annotation("MySQL:Charset", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Notifications_MessageDispatchId",
|
||||
table: "Notifications",
|
||||
column: "MessageDispatchId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Notifications_UserId_Category_CreatedAt",
|
||||
table: "Notifications",
|
||||
columns: new[] { "UserId", "Category", "CreatedAt" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_MessageDispatches_SenderUserId_CreatedAt",
|
||||
table: "MessageDispatches",
|
||||
columns: new[] { "SenderUserId", "CreatedAt" });
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Notifications_MessageDispatches_MessageDispatchId",
|
||||
table: "Notifications",
|
||||
column: "MessageDispatchId",
|
||||
principalTable: "MessageDispatches",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Notifications_MessageDispatches_MessageDispatchId",
|
||||
table: "Notifications");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "MessageDispatches");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Notifications_MessageDispatchId",
|
||||
table: "Notifications");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Notifications_UserId_Category_CreatedAt",
|
||||
table: "Notifications");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Category",
|
||||
table: "Notifications");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "MessageDispatchId",
|
||||
table: "Notifications");
|
||||
}
|
||||
}
|
||||
}
|
||||
+83
@@ -2210,12 +2210,73 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||
b.ToTable("MakeupExamSessionInvigilators");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.MessageDispatch", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<Guid?>("AudienceId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<string>("AudienceName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("varchar(200)");
|
||||
|
||||
b.Property<int>("AudienceType")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("Category")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("varchar(1000)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("LinkUrl")
|
||||
.HasMaxLength(300)
|
||||
.HasColumnType("varchar(300)");
|
||||
|
||||
b.Property<int>("RecipientCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("SenderName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("varchar(100)");
|
||||
|
||||
b.Property<Guid>("SenderUserId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("varchar(200)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SenderUserId", "CreatedAt");
|
||||
|
||||
b.ToTable("MessageDispatches");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Notification", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<int>("Category")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
@@ -2231,6 +2292,9 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||
.HasMaxLength(300)
|
||||
.HasColumnType("varchar(300)");
|
||||
|
||||
b.Property<Guid?>("MessageDispatchId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
@@ -2246,8 +2310,12 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||
|
||||
b.HasIndex("CreatedAt");
|
||||
|
||||
b.HasIndex("MessageDispatchId");
|
||||
|
||||
b.HasIndex("UserId", "IsRead");
|
||||
|
||||
b.HasIndex("UserId", "Category", "CreatedAt");
|
||||
|
||||
b.ToTable("Notifications");
|
||||
});
|
||||
|
||||
@@ -4076,6 +4144,16 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||
b.Navigation("Teacher");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.Notification", b =>
|
||||
{
|
||||
b.HasOne("Jiaowu.Api.Domain.Academic.MessageDispatch", "MessageDispatch")
|
||||
.WithMany("Notifications")
|
||||
.HasForeignKey("MessageDispatchId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.Navigation("MessageDispatch");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.OfficialDocument", b =>
|
||||
{
|
||||
b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationUser", "InvalidatedByUser")
|
||||
@@ -4549,6 +4627,11 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||
b.Navigation("Invigilators");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.MessageDispatch", b =>
|
||||
{
|
||||
b.Navigation("Notifications");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.OfficialDocument", b =>
|
||||
{
|
||||
b.Navigation("Downloads");
|
||||
|
||||
@@ -75,11 +75,11 @@ public sealed class WarningCheckWorker(IServiceScopeFactory scopeFactory, ILogge
|
||||
{
|
||||
var si = await db.Students.Where(x => x.Id == w.StudentId).Select(x => new { x.UserId, x.Name, x.AdministrativeClassId }).FirstAsync(ct);
|
||||
if (rule.NotifyStudent && si.UserId.HasValue)
|
||||
await NotificationService.SendAsync(db, si.UserId.Value, "学业预警", w.Detail, "/warnings", cancellationToken: ct);
|
||||
await NotificationService.SendAsync(db, si.UserId.Value, "学业预警", w.Detail, "/warnings", ct, NotificationCategory.Warning);
|
||||
if (rule.NotifyCounselor)
|
||||
{
|
||||
var cid = await db.AdministrativeClasses.Where(c => c.Id == si.AdministrativeClassId && c.CounselorUserId != null).Select(c => c.CounselorUserId!.Value).FirstOrDefaultAsync(ct);
|
||||
if (cid != default) await NotificationService.SendAsync(db, cid, "学生学业预警", $"{si.Name}:{w.Detail}", "/warnings", cancellationToken: ct);
|
||||
if (cid != default) await NotificationService.SendAsync(db, cid, "学生学业预警", $"{si.Name}:{w.Detail}", "/warnings", ct, NotificationCategory.Warning);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user