
其实后端程序员也可以快速构建出拥有漂亮界面的UI。
本文将从零开始,使用Blazor Server和.NET 9构建一个功能完善的学生成绩管理系统。我们将采用分层架构设计,结合最新的Bootstrap Blazor组件库,实现从数据建模到部署上线的完整开发流程。通过本文,您将掌握Blazor Server的核心开发技巧、EF Core 9.0的数据操作方法以及企业级应用的最佳实践。
学生成绩管理系统需满足两类核心用户的需求:
角色 | 核心功能 | 权限控制 |
|---|---|---|
教师 | 成绩录入/修改、课程管理、统计分析 | 全部功能访问权限 |
学生 | 成绩查询、个人信息查看 | 仅查看本人数据 |
系统采用模块化设计,包含以下关键模块:
技术领域 | 选型 | 优势 |
|---|---|---|
前端框架 | Blazor Server (.NET 9) | 共享C#代码,实时双向绑定 |
UI组件库 | Bootstrap Blazor v9.7.0 | 企业级UI组件,丰富的数据表格功能 |
数据访问 | EF Core 9.0 | 强大的ORM,支持Code First开发 |
身份认证 | ASP.NET Core Identity | 完善的用户管理与授权机制 |
数据库 | SQL Server LocalDB | 开发便捷,生产环境可无缝迁移至SQL Server |
采用Clean Architecture思想,实现关注点分离:
StudentGradeManagement/
├── Application/ # 应用服务层(业务逻辑)
├── Domain/ # 领域层(实体与接口)
├── Infrastructure/ # 基础设施层(数据访问、外部服务)
└── Web/ # 表示层(Blazor UI)各层职责:
核心实体关系模型:
erDiagram
Student ||--o{ Grade : "has"
Course ||--o{ Grade : "has"
StudentCourse }|--|| Student : "enrolls"
StudentCourse }|--|| Course : "includes"
Student {
int StudentId PK
string Name
string StudentNumber UK
string Class
string Email
}
Course {
int CourseId PK
string CourseName
string CourseCode UK
int Credits
}
Grade {
int GradeId PK
int StudentId FK
int CourseId FK
decimal Score
string Remark
DateTime CreatedAt
}
StudentCourse {
int StudentId PK,FK
int CourseId PK,FK
}按照分层架构重构项目结构:
# 创建核心项目
dotnet new classlib -n Domain
dotnet new classlib -n Application
dotnet new classlib -n Infrastructure
# 添加项目引用
dotnet add Web reference Application
dotnet add Application reference Domain
dotnet add Infrastructure reference Domain
dotnet add Web reference Infrastructure在Web项目中安装关键NuGet包:
# Bootstrap Blazor组件库
dotnet add package BootstrapBlazor --version 9.7.0
# EF Core 9.0 SQL Server提供器
dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 9.0.0
# 身份认证
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore --version 9.0.0
# Excel处理
dotnet add package MiniExcel --version 1.41.2Student.cs(领域层):
using System.ComponentModel.DataAnnotations;
namespace Domain.Entities;
public class Student
{
public int StudentId { get; set; }
[Required(ErrorMessage = "姓名不能为空")]
[MaxLength(50)]
public string Name { get; set; } = string.Empty;
[Required]
[RegularExpression(@"^\d{10}$", ErrorMessage = "学号必须为10位数字")]
public string StudentNumber { get; set; } = string.Empty;
[MaxLength(20)]
public string Class { get; set; } = string.Empty;
[EmailAddress]
public string? Email { get; set; }
public ICollection<Grade> Grades { get; set; } = new List<Grade>();
public ICollection<StudentCourse> StudentCourses { get; set; } = new List<StudentCourse>();
}Course.cs(领域层):
using System.ComponentModel.DataAnnotations;
namespace Domain.Entities;
public class Course
{
public int CourseId { get; set; }
[Required]
[MaxLength(100)]
public string CourseName { get; set; } = string.Empty;
[Required]
[MaxLength(20)]
public string CourseCode { get; set; } = string.Empty;
public int Credits { get; set; }
public ICollection<Grade> Grades { get; set; } = new List<Grade>();
public ICollection<StudentCourse> StudentCourses { get; set; } = new List<StudentCourse>();
}Grade.cs(领域层):
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Domain.Entities;
public class Grade
{
public int GradeId { get; set; }
[ForeignKey("Student")]
public int StudentId { get; set; }
[ForeignKey("Course")]
public int CourseId { get; set; }
[Range(0, 100, ErrorMessage = "成绩必须在0-100之间")]
public decimal Score { get; set; }
[MaxLength(500)]
public string? Remark { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.Now;
// 导航属性
public Student Student { get; set; } = null!;
public Course Course { get; set; } = null!;
}AppDbContext.cs(基础设施层):
using Domain.Entities;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
namespace Infrastructure.Data;
public class AppDbContext : IdentityDbContext<IdentityUser>
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
public DbSet<Student> Students => Set<Student>();
public DbSet<Course> Courses => Set<Course>();
public DbSet<Grade> Grades => Set<Grade>();
public DbSet<StudentCourse> StudentCourses => Set<StudentCourse>();
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
// 配置多对多关系
builder.Entity<StudentCourse>()
.HasKey(sc => new { sc.StudentId, sc.CourseId });
// 设置索引
builder.Entity<Student>()
.HasIndex(s => s.StudentNumber)
.IsUnique();
builder.Entity<Course>()
.HasIndex(c => c.CourseCode)
.IsUnique();
// 种子数据
builder.Entity<Course>().HasData(
new Course { CourseId = 1, CourseName = "高等数学", CourseCode = "MATH101", Credits = 4 },
new Course { CourseId = 2, CourseName = "大学物理", CourseCode = "PHYS102", Credits = 3 },
new Course { CourseId = 3, CourseName = "计算机导论", CourseCode = "CS103", Credits = 4 }
);
}
}StudentService.cs(应用层):
using Domain.Entities;
using Domain.Repositories;
using Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
namespace Application.Services;
public class StudentService : IStudentService
{
private readonly AppDbContext _context;
public StudentService(AppDbContext context)
{
_context = context;
}
public async Task<List<Student>> GetAllStudentsAsync()
{
return await _context.Students
.Include(s => s.Grades)
.ThenInclude(g => g.Course)
.ToListAsync();
}
public async Task<Student?> GetStudentByIdAsync(int id)
{
return await _context.Students
.Include(s => s.Grades)
.ThenInclude(g => g.Course)
.FirstOrDefaultAsync(s => s.StudentId == id);
}
public async Task AddStudentAsync(Student student)
{
_context.Students.Add(student);
await _context.SaveChangesAsync();
}
public async Task UpdateStudentAsync(Student student)
{
_context.Entry(student).State = EntityState.Modified;
await _context.SaveChangesAsync();
}
public async Task DeleteStudentAsync(int id)
{
var student = await _context.Students.FindAsync(id);
if (student != null)
{
_context.Students.Remove(student);
await _context.SaveChangesAsync();
}
}
// 成绩统计方法
public async Task<GradeStatistics> GetGradeStatisticsAsync(int courseId)
{
var grades = await _context.Grades
.Where(g => g.CourseId == courseId)
.Select(g => g.Score)
.ToListAsync();
return new GradeStatistics
{
Average = grades.Any() ? grades.Average() : 0,
Highest = grades.Any() ? grades.Max() : 0,
Lowest = grades.Any() ? grades.Min() : 0,
Count = grades.Count,
PassRate = grades.Any() ?
(decimal)grades.Count(g => g >= 60) / grades.Count * 100 : 0
};
}
}学生列表页面(Pages/Students.razor):
@page "/students"
@inject IStudentService StudentService
@inject NavigationManager NavManager
@inject ILogger<Students> Logger
@inject ToastService ToastService
<PageTitle>学生管理</PageTitle>
<Card Title="学生信息管理" Description="查看和管理所有学生信息">
<Table TItem="Student"
Items="@students"
ShowToolbar="true"
IsStriped="true"
IsBordered="true"
OnSaveAsync="OnSaveAsync"
OnDeleteAsync="OnDeleteAsync"
AutoGenerateColumns="true">
<TableColumns>
<TableColumn @bind-Field="@context.StudentId" IsVisible="false" />
<TableColumn @bind-Field="@context.Name" />
<TableColumn @bind-Field="@context.StudentNumber" />
<TableColumn @bind-Field="@context.Class" />
<TableColumn @bind-Field="@context.Email" />
<TableColumn Title="操作">
<Button Icon="fa-solid fa-edit"
Size="ButtonSize.ExtraSmall"
OnClick="() => EditStudent(context.StudentId)">
</Button>
<Button Icon="fa-solid fa-graduation-cap"
Size="ButtonSize.ExtraSmall"
OnClick="() => ViewGrades(context.StudentId)">
</Button>
</TableColumn>
</TableColumns>
</Table>
</Card>
@code {
private List<Student> students = new();
protected override async Task OnInitializedAsync()
{
await LoadStudents();
}
private async Task LoadStudents()
{
try
{
students = await StudentService.GetAllStudentsAsync();
}
catch (Exception ex)
{
Logger.LogError(ex, "加载学生数据失败");
await ToastService.ShowError("加载数据失败", "错误");
}
}
private async Task<bool> OnSaveAsync(Student student)
{
try
{
if (student.StudentId == 0)
{
await StudentService.AddStudentAsync(student);
await ToastService.ShowSuccess("添加成功", "提示");
}
else
{
await StudentService.UpdateStudentAsync(student);
await ToastService.ShowSuccess("更新成功", "提示");
}
await LoadStudents();
return true;
}
catch (Exception ex)
{
Logger.LogError(ex, "保存学生数据失败");
await ToastService.ShowError($"保存失败: {ex.Message}", "错误");
return false;
}
}
private async Task<bool> OnDeleteAsync(IEnumerable<Student> students)
{
try
{
foreach (var student in students)
{
await StudentService.DeleteStudentAsync(student.StudentId);
}
await LoadStudents();
await ToastService.ShowSuccess("删除成功", "提示");
return true;
}
catch (Exception ex)
{
Logger.LogError(ex, "删除学生数据失败");
await ToastService.ShowError($"删除失败: {ex.Message}", "错误");
return false;
}
}
private void EditStudent(int id)
{
NavManager.NavigateTo($"/students/edit/{id}");
}
private void ViewGrades(int id)
{
NavManager.NavigateTo($"/students/{id}/grades");
}
}成绩分析页面(Pages/GradeAnalysis.razor):
@page "/grade-analysis"
@inject IGradeService GradeService
@inject ICourseService CourseService
@inject ILogger<GradeAnalysis> Logger
@using Blazor.ECharts.Options.Series
@using Blazor.ECharts.Options.XAxis
@using Blazor.ECharts.Options.YAxis
<PageTitle>成绩分析</PageTitle>
<Card Title="成绩统计分析" Description="分析各课程成绩分布情况">
<div class="row">
<div class="col-md-3">
<Select @bind-Value="selectedCourseId"
Placeholder="选择课程"
OnSelectedItemChanged="OnCourseChanged">
@foreach (var course in courses)
{
<SelectItem Value="@course.CourseId">@course.CourseName</SelectItem>
}
</Select>
</div>
</div>
@if (statistics != null)
{
<div class="row mt-4">
<div class="col-md-6">
<Card Title="成绩分布">
<ECharts @ref="_chart"
Options="_chartOptions"
Style="height: 400px;"></ECharts>
</Card>
</div>
<div class="col-md-6">
<Card Title="统计指标">
<ul class="list-group">
<li class="list-group-item">
<strong>平均分:</strong> @statistics.Average.ToString("F1")
</li>
<li class="list-group-item">
<strong>最高分:</strong> @statistics.Highest
</li>
<li class="list-group-item">
<strong>最低分:</strong> @statistics.Lowest
</li>
<li class="list-group-item">
<strong>及格率:</strong> @statistics.PassRate.ToString("F1")%
</li>
<li class="list-group-item">
<strong>参考人数:</strong> @statistics.Count
</li>
</ul>
</Card>
</div>
</div>
}
</Card>
@code {
private List<Course> courses = new();
private int selectedCourseId = 0;
private GradeStatistics? statistics;
private ECharts? _chart;
private EChartsOptions _chartOptions = new();
protected override async Task OnInitializedAsync()
{
courses = await CourseService.GetAllCoursesAsync();
if (courses.Any())
{
selectedCourseId = courses.First().CourseId;
await OnCourseChanged(selectedCourseId);
}
}
private async Task OnCourseChanged(int courseId)
{
if (courseId == 0) return;
try
{
statistics = await GradeService.GetGradeStatisticsAsync(courseId);
var distribution = await GradeService.GetGradeDistributionAsync(courseId);
// 配置图表
_chartOptions = new EChartsOptions
{
Title = new Title { Text = "成绩分布直方图" },
Tooltip = new Tooltip { Trigger = "axis", AxisPointer = new AxisPointer { Type = "shadow" } },
XAxis = new XAxis
{
Type = AxisType.Category,
Data = new List<string> { "0-59", "60-69", "70-79", "80-89", "90-100" }
},
YAxis = new YAxis { Type = AxisType.Value },
Series = new List<ISeries>
{
new BarSeries
{
Name = "学生人数",
Data = new List<double>
{
distribution.Fail,
distribution.Pass,
distribution.Good,
distribution.Excellent,
distribution.Expert
},
ItemStyle = new ItemStyle
{
Color = new JsFunction("function(params) { return params.dataIndex === 0 ? '#ff4d4f' : '#52c41a'; }")
}
}
}
};
StateHasChanged();
}
catch (Exception ex)
{
Logger.LogError(ex, "加载成绩统计失败");
}
}
}Program.cs配置:
var builder = WebApplication.CreateBuilder(args);
// 添加数据库上下文
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
// 添加身份认证
builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<AppDbContext>();
// 添加授权策略
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("TeacherOnly", policy => policy.RequireRole("Teacher"));
options.AddPolicy("StudentOnly", policy => policy.RequireRole("Student"));
});
// 添加服务
builder.Services.AddScoped<IStudentService, StudentService>();
builder.Services.AddScoped<ICourseService, CourseService>();
builder.Services.AddScoped<IGradeService, GradeService>();
// 添加Blazor组件
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
// 添加Bootstrap Blazor组件
builder.Services.AddBootstrapBlazor();
// 添加ECharts
builder.Services.AddECharts();
var app = builder.Build();
// 配置中间件
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error", createScopeForErrors: true);
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
// 初始化角色和管理员用户
using (var scope = app.Services.CreateScope())
{
var services = scope.ServiceProvider;
try
{
await SeedData.Initialize(services);
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred seeding the DB.");
}
}
app.Run();.NET 9引入的动态适应应用大小(DATAS) 垃圾回收机制可显著减少内存占用:
// Program.cs中配置
builder.Services.Configure<GCSettings>(settings =>
{
settings.LatencyMode = GCLatencyMode.SustainedLowLatency;
});使用ShouldRender方法减少不必要的渲染:
protected override bool ShouldRender()
{
// 仅当数据实际更改时才重新渲染
return _dataHasChanged;
}实现虚拟滚动加载大量数据:
<Virtualize Items="@students" Context="student" ItemsProvider="LoadStudents">
<div class="student-item">
@student.Name - @student.StudentNumber
</div>
<LoadingTemplate>
<div class="text-center py-4">加载中...</div>
</LoadingTemplate>
</Virtualize>
@code {
private async ValueTask<ItemsProviderResult<Student>> LoadStudents(ItemsProviderRequest request)
{
var result = await StudentService.GetStudentsAsync(
request.StartIndex, request.Count);
return new ItemsProviderResult<Student>(
result.Items, result.TotalCount);
}
}本文构建的学生成绩管理系统实现了以下核心价值:
通过本文的指导,您已掌握使用Blazor和.NET 9构建企业级Web应用的核心技能。这个成绩管理系统不仅满足了教学管理的基本需求,更为您提供了一个可扩展的架构基础,助力您在.NET生态系统中进一步探索和创新。
StudentGradeManagement/
├── Application/
│ ├── Dtos/
│ ├── Interfaces/
│ └── Services/
├── Domain/
│ ├── Entities/
│ └── Interfaces/
├── Infrastructure/
│ ├── Data/
│ └── Migrations/
├── Web/
│ ├── Components/
│ ├── Pages/
│ ├── Properties/
│ ├── Services/
│ ├── Shared/
│ ├── wwwroot/
│ ├── App.razor
│ ├── Program.cs
│ └── ...
├── StudentGradeManagement.sln
└── README.md原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。