学生可在“学籍管理 → 电子成绩单与证明”申请成绩单或学籍证明。

服务端根据登录账号绑定本人档案,不接受学生 ID,无法代他人申请。
成绩单仅包含正式发布成绩;无已发布成绩时明确提示。
同类型、同用途 5 分钟内重复申请复用已有有效凭证。
申请后即时生成 PDF,可在本人凭证列表下载、二维码验真。
管理员的下载记录、失效、重签能力保持不变。
This commit is contained in:
2026-07-26 18:24:53 +08:00 Unverified
parent 40b8cb6e3c
commit ecc6546f3a
24 changed files with 7203 additions and 0 deletions
@@ -91,6 +91,9 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
Set<GraduationClearanceItem>();
public DbSet<GraduationClearanceRecord> GraduationClearanceRecords =>
Set<GraduationClearanceRecord>();
public DbSet<OfficialDocument> OfficialDocuments => Set<OfficialDocument>();
public DbSet<OfficialDocumentDownload> OfficialDocumentDownloads =>
Set<OfficialDocumentDownload>();
public DbSet<AuditLog> AuditLogs => Set<AuditLog>();
protected override void ConfigureConventions(
@@ -927,6 +930,54 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
entity.Property(x => x.IpAddress).HasMaxLength(64);
entity.HasIndex(x => x.CreatedAt);
});
builder.Entity<OfficialDocument>(entity =>
{
entity.Property(x => x.DocumentNumber).HasMaxLength(50);
entity.Property(x => x.VerificationCodeHash).HasMaxLength(64);
entity.Property(x => x.Purpose).HasMaxLength(200);
entity.Property(x => x.SnapshotJson).HasColumnType("longtext");
entity.Property(x => x.PdfContent).HasColumnType("longblob");
entity.Property(x => x.PdfSha256).HasMaxLength(64);
entity.Property(x => x.InvalidationReason).HasMaxLength(500);
entity.HasIndex(x => x.DocumentNumber).IsUnique();
entity.HasIndex(x => x.VerificationCodeHash).IsUnique();
entity.HasIndex(x => new { x.StudentId, x.IssuedAt });
entity.HasIndex(x => new { x.Status, x.IssuedAt });
entity.HasOne(x => x.Student)
.WithMany()
.HasForeignKey(x => x.StudentId)
.OnDelete(DeleteBehavior.Restrict);
entity.HasOne(x => x.IssuedByUser)
.WithMany()
.HasForeignKey(x => x.IssuedByUserId)
.OnDelete(DeleteBehavior.Restrict);
entity.HasOne(x => x.InvalidatedByUser)
.WithMany()
.HasForeignKey(x => x.InvalidatedByUserId)
.OnDelete(DeleteBehavior.Restrict);
entity.HasIndex(x => x.ReissuedFromDocumentId).IsUnique();
entity.HasOne(x => x.ReissuedFromDocument)
.WithMany()
.HasForeignKey(x => x.ReissuedFromDocumentId)
.OnDelete(DeleteBehavior.Restrict);
});
builder.Entity<OfficialDocumentDownload>(entity =>
{
entity.Property(x => x.IpAddress).HasMaxLength(64);
entity.Property(x => x.UserAgent).HasMaxLength(500);
entity.HasIndex(x => new { x.OfficialDocumentId, x.CreatedAt });
entity.HasIndex(x => new { x.DownloadedByUserId, x.CreatedAt });
entity.HasOne(x => x.OfficialDocument)
.WithMany(x => x.Downloads)
.HasForeignKey(x => x.OfficialDocumentId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasOne(x => x.DownloadedByUser)
.WithMany()
.HasForeignKey(x => x.DownloadedByUserId)
.OnDelete(DeleteBehavior.Restrict);
});
}
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)