多角色账号按 All > College > Class > Self 自动取最高权限。

学院管理员限制在所属学院。
辅导员通过稳定账号 ID 绑定行政班,避免重名串班。
教师只能访问本人档案、授课课程和所授课学生。
学生只能访问本人档案及所在班级课程。
教师/学生角色会自动校验并绑定工号或学号档案。
超级管理员可在用户页面调整角色、学院、工号/学号,并预览生效后的数据范围。
This commit is contained in:
2026-07-24 14:54:30 +08:00 Unverified
parent 0b463fa4f2
commit bcc4d33bd5
26 changed files with 2333 additions and 88 deletions
@@ -72,6 +72,14 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
.WithMany()
.HasForeignKey(x => x.MajorId)
.OnDelete(DeleteBehavior.Restrict);
builder.Entity<AdministrativeClass>(entity =>
{
entity.HasIndex(x => x.CounselorUserId);
entity.HasOne(x => x.CounselorUser)
.WithMany()
.HasForeignKey(x => x.CounselorUserId)
.OnDelete(DeleteBehavior.SetNull);
});
builder.Entity<Building>()
.HasOne(x => x.Campus)
@@ -419,6 +419,85 @@ public sealed class DatabaseInitializer(
});
await db.SaveChangesAsync();
}
await SeedDevelopmentUsersAsync(computerCollege.Id);
}
private async Task SeedDevelopmentUsersAsync(Guid collegeId)
{
var definitions = new[]
{
new DevelopmentUser(
"academic", "校级教务员", "Academic@123456",
null, null, SystemRoles.AcademicAdmin),
new DevelopmentUser(
"college", "计算机学院教务员", "College@123456",
"A2026001", collegeId, SystemRoles.CollegeAdmin),
new DevelopmentUser(
"counselor", "陈老师", "Counselor@123456",
"C2026001", collegeId, SystemRoles.Counselor),
new DevelopmentUser(
"teacher", "陈明远", "Teacher@123456",
"T2026001", collegeId, SystemRoles.Teacher),
new DevelopmentUser(
"student", "周启航", "Student@123456",
"202601001", collegeId, SystemRoles.Student),
new DevelopmentUser(
"leader", "教学分管领导", "Leader@123456",
null, null, SystemRoles.Leader)
};
foreach (var definition in definitions)
{
var user = await userManager.FindByNameAsync(definition.UserName);
if (user is null)
{
user = new ApplicationUser
{
UserName = definition.UserName,
DisplayName = definition.DisplayName,
StaffNumber = definition.StaffNumber,
CollegeId = definition.CollegeId,
LockoutEnabled = true,
IsEnabled = true
};
EnsureSucceeded(
await userManager.CreateAsync(user, definition.Password),
$"创建开发账号 {definition.UserName}");
}
if (!await userManager.IsInRoleAsync(user, definition.Role))
{
EnsureSucceeded(
await userManager.AddToRoleAsync(user, definition.Role),
$"授予开发账号 {definition.UserName} 角色");
}
if (definition.Role == SystemRoles.Teacher)
{
var teacher = await db.Teachers.SingleAsync(
x => x.TeacherNumber == definition.StaffNumber);
if (!teacher.UserId.HasValue) teacher.UserId = user.Id;
}
if (definition.Role == SystemRoles.Student)
{
var student = await db.Students.SingleAsync(
x => x.StudentNumber == definition.StaffNumber);
if (!student.UserId.HasValue) student.UserId = user.Id;
}
if (definition.Role == SystemRoles.Counselor)
{
var classes = await db.AdministrativeClasses
.Where(x =>
x.CounselorUserId == null &&
x.CounselorName == definition.DisplayName)
.ToListAsync();
foreach (var administrativeClass in classes)
administrativeClass.CounselorUserId = user.Id;
}
}
await db.SaveChangesAsync();
}
private static void EnsureSucceeded(IdentityResult result, string action)
@@ -431,4 +510,12 @@ public sealed class DatabaseInitializer(
throw new InvalidOperationException(
$"{action}失败:{string.Join("", result.Errors.Select(x => x.Description))}");
}
private sealed record DevelopmentUser(
string UserName,
string DisplayName,
string Password,
string? StaffNumber,
Guid? CollegeId,
string Role);
}
@@ -10,6 +10,7 @@ public sealed class DevelopmentSqliteMigrator(
private const string CurriculumPlansMigration = "20260724_02_curriculum_plans";
private const string TeachingTasksMigration = "20260724_03_teaching_tasks";
private const string SchedulesMigration = "20260724_04_schedules";
private const string ClassCounselorMigration = "20260724_05_class_counselor";
public async Task MigrateAsync(CancellationToken cancellationToken = default)
{
@@ -43,6 +44,20 @@ public sealed class DevelopmentSqliteMigrator(
SchedulesMigration,
SchedulesStatements,
cancellationToken);
var counselorColumnExists = await db.Database
.SqlQueryRaw<int>(
"""
SELECT COUNT(*) AS "Value"
FROM pragma_table_info('AdministrativeClasses')
WHERE name = 'CounselorUserId'
""")
.AnyAsync(value => value > 0, cancellationToken);
await ApplyMigrationAsync(
ClassCounselorMigration,
counselorColumnExists
? ClassCounselorStatements.Skip(1)
: ClassCounselorStatements,
cancellationToken);
}
private async Task ApplyMigrationAsync(
@@ -399,4 +414,16 @@ public sealed class DevelopmentSqliteMigrator(
ON "ScheduleEntries" ("ClassroomId");
"""
];
private static readonly string[] ClassCounselorStatements =
[
"""
ALTER TABLE "AdministrativeClasses"
ADD COLUMN "CounselorUserId" TEXT NULL;
""",
"""
CREATE INDEX IF NOT EXISTS "IX_AdministrativeClasses_CounselorUserId"
ON "AdministrativeClasses" ("CounselorUserId");
"""
];
}
@@ -0,0 +1,50 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
{
/// <inheritdoc />
public partial class ClassCounselorAssignment : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<Guid>(
name: "CounselorUserId",
table: "AdministrativeClasses",
type: "char(36)",
nullable: true);
migrationBuilder.CreateIndex(
name: "IX_AdministrativeClasses_CounselorUserId",
table: "AdministrativeClasses",
column: "CounselorUserId");
migrationBuilder.AddForeignKey(
name: "FK_AdministrativeClasses_AspNetUsers_CounselorUserId",
table: "AdministrativeClasses",
column: "CounselorUserId",
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_AdministrativeClasses_AspNetUsers_CounselorUserId",
table: "AdministrativeClasses");
migrationBuilder.DropIndex(
name: "IX_AdministrativeClasses_CounselorUserId",
table: "AdministrativeClasses");
migrationBuilder.DropColumn(
name: "CounselorUserId",
table: "AdministrativeClasses");
}
}
}
@@ -89,6 +89,9 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
b.Property<string>("CounselorName")
.HasColumnType("longtext");
b.Property<Guid?>("CounselorUserId")
.HasColumnType("char(36)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
@@ -117,6 +120,8 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
b.HasIndex("Code")
.IsUnique();
b.HasIndex("CounselorUserId");
b.HasIndex("MajorId");
b.HasIndex("IsEnabled", "SortOrder");
@@ -1154,12 +1159,19 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.AdministrativeClass", b =>
{
b.HasOne("Jiaowu.Api.Domain.Identity.ApplicationUser", "CounselorUser")
.WithMany()
.HasForeignKey("CounselorUserId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("Jiaowu.Api.Domain.Academic.Major", "Major")
.WithMany()
.HasForeignKey("MajorId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("CounselorUser");
b.Navigation("Major");
});