从零开始使用Blazor和.NET 9构建学生成绩管理系统

举报
码事漫谈 发表于 2025/07/20 19:58:34 2025/07/20
【摘要】 其实后端程序员也可以快速构建出拥有漂亮界面的UI。本文将从零开始,使用Blazor Server和**. Net 9**构建一个功能完善的学生成绩管理系统。我们将采用分层架构设计,结合最新的Bootstrap Blazor组件库,实现从数据建模到部署上线的完整开发流程。通过本文,您将掌握Blazor Server的核心开发技巧、EF Core 9.0的数据操作方法以及企业级应用的最佳实践。 ...

image.png

其实后端程序员也可以快速构建出拥有漂亮界面的UI。

本文将从零开始,使用Blazor Server和**. Net 9**构建一个功能完善的学生成绩管理系统。我们将采用分层架构设计,结合最新的Bootstrap Blazor组件库,实现从数据建模到部署上线的完整开发流程。通过本文,您将掌握Blazor Server的核心开发技巧、EF Core 9.0的数据操作方法以及企业级应用的最佳实践。

一、需求分析

1.1 用户角色与功能需求

学生成绩管理系统需满足两类核心用户的需求:

角色 核心功能 权限控制
教师 成绩录入/修改、课程管理、统计分析 全部功能访问权限
学生 成绩查询、个人信息查看 仅查看本人数据

1.2 功能模块划分

系统采用模块化设计,包含以下关键模块:

  1. 学生管理:基础信息CRUD,支持批量导入导出
  2. 课程管理:课程信息维护与学生选课管理
  3. 成绩管理:成绩录入、修改、查询与统计分析
  4. 用户认证:基于角色的身份验证与授权

1.3 非功能需求

  • 性能:支持1000+学生数据的高效查询
  • 安全:密码加密存储,操作日志记录
  • 易用性:响应式设计,适配桌面/平板设备
  • 可扩展性:模块化架构,支持功能横向扩展

二、架构设计

2.1 技术栈选型

技术领域 选型 优势
前端框架 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

2.2 分层架构设计

采用Clean Architecture思想,实现关注点分离:

StudentGradeManagement/
├── Application/          # 应用服务层(业务逻辑)
├── Domain/               # 领域层(实体与接口)
├── Infrastructure/       # 基础设施层(数据访问、外部服务)
└── Web/                  # 表示层(Blazor UI

各层职责

  • 领域层:定义核心实体(Student, Course, Grade)和仓储接口
  • 应用层:实现业务逻辑,协调领域对象
  • 基础设施层:提供EF Core数据访问实现、文件存储等
  • 表示层:Blazor组件与页面,处理用户交互

2.3 数据库设计

核心实体关系模型:

Parse error on line 28: ...eTime CreatedAt } StudentCo ----------------------^ Expecting 'ATTRIBUTE_WORD', got 'BLOCK_STOP'

三、环境搭建

3.1 开发环境准备

  1. 安装. Net 9 SDK
    从微软官网下载并安装. Net 9 SDK,验证安装:

    dotnet --version  # 应显示9.0.x
    
  2. 配置Visual Studio 2022
    安装ASP. Net和Web开发工作负载,确保勾选:

    • . Net 9 SDK
    • Blazor Server开发工具
    • SQL Server LocalDB
  3. 创建Blazor Server项目
    使用以下命令创建项目:

    dotnet new blazorserver -n StudentGradeManagement --framework net9.0
    cd StudentGradeManagement
    

3.2 项目结构调整

按照分层架构重构项目结构:

# 创建核心项目
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

3.3 安装必要依赖

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.2

四、核心代码实现

4.1 领域模型设计

Student.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!;
}

4.2 数据访问层实现

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 }
        );
    }
}

4.3 业务服务实现

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
        };
    }
}

4.4 Blazor页面实现

学生列表页面(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");
    }
}

4.5 成绩统计图表实现

成绩分析页面(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, "加载成绩统计失败");
        }
    }
}

4.6 身份认证与授权

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();

五、性能优化策略

5.1 服务器GC优化

. Net 9引入的动态适应应用大小(DATAS) 垃圾回收机制可显著减少内存占用:

// Program.cs中配置
builder.Services.Configure<GCSettings>(settings =>
{
    settings.LatencyMode = GCLatencyMode.SustainedLowLatency;
});

5.2 组件渲染优化

使用ShouldRender方法减少不必要的渲染:

protected override bool ShouldRender()
{
    // 仅当数据实际更改时才重新渲染
    return _dataHasChanged;
}

5.3 数据加载优化

实现虚拟滚动加载大量数据:

<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);
    }
}

六、部署指南

6.1 Windows环境部署(IIS)

  1. 发布应用

    dotnet publish -c Release -o ./publish
    
  2. 配置IIS

    • 安装. Net 9 Hosting Bundle
    • 创建应用池(无托管代码)
    • 设置物理路径指向publish文件夹
  3. 配置web.config

    <?xml version="1.0" encoding="utf-8"?>
    <configuration>
      <location path="." inheritInChildApplications="false">
        <system.webServer>
          <handlers>
            <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
          </handlers>
          <aspNetCore processPath=".\StudentGradeManagement.exe" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" hostingModel="inprocess" />
        </system.webServer>
      </location>
    </configuration>
    

6.2 Linux环境部署(Nginx)

  1. 安装依赖

    sudo apt-get update
    sudo apt-get install -y aspnetcore-runtime-9.0 nginx
    
  2. 创建系统服务

    # /etc/systemd/system/student-grade.service
    [Unit]
    Description=Student Grade Management System
    After=network.target
    
    [Service]
    WorkingDirectory=/var/www/student-grade
    ExecStart=/usr/bin/dotnet StudentGradeManagement.dll
    Restart=always
    RestartSec=10
    User=www-data
    Environment=ASPNETCORE_ENVIRONMENT=Production
    Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false
    
    [Install]
    WantedBy=multi-user.target
    
  3. 配置Nginx

    server {
        listen 80;
        server_name your-domain.com;
    
        location / {
            proxy_pass http://localhost:5000;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection keep-alive;
            proxy_set_header Host $host;
            proxy_cache_bypass $http_upgrade;
        }
    }
    

七、总结与扩展

7.1 项目回顾

本文构建的学生成绩管理系统实现了以下核心价值:

  • 技术先进性:基于. Net 9和Blazor Server,采用分层架构设计
  • 功能完整性:覆盖学生管理、课程管理、成绩管理全流程
  • 性能优化:通过DATAS GC、组件渲染控制提升系统响应速度
  • 安全可靠:基于ASP. Net Core Identity的身份认证与授权

7.2 功能扩展建议

  1. 移动应用:使用Blazor Hybrid开发跨平台移动应用
  2. 高级报表:集成RDLCSyncfusion ReportViewer生成复杂报表
  3. AI分析:添加机器学习模型预测学生成绩趋势
  4. 通知系统:集成SignalR实现成绩更新实时推送

通过本文的指导,您已掌握使用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
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。