简介:本资源是一个基于.NET 6构建的完整Web API实战项目,面向C#后端开发初学者与ASP.NET Core进阶学习者,聚焦企业级API开发核心能力训练:SQL Server数据库集成、JWT无状态身份认证及标准CRUD接口实现。项目包含665个文件,主体为96个C#源码文件(含Controller、Model、Service等分层逻辑)、286个编译生成的DLL库、44个配置与元数据JSON文件,以及CSProj工程文件、迁移脚本和Swagger文档配置,整体包体34.81MB,结构清晰、模块解耦,便于理解现代.NET分层架构设计。内容预览显示项目已划分SqlSugarDB、DapperCore、NET6WebApi等多个子项目,涵盖数据访问、业务逻辑与API宿主,同时包含.bak数据库备份与编译中间产物,开箱即用。目前已有1062人学习下载,读者可直接运行调试、对照源码掌握EF Core/Dapper双数据访问方案、JWT令牌签发验证流程、全局异常处理与API版本化实践,是深入理解.NET 6 Web API工程化落地的优质参考样本。
1. 为什么一个“增删改查”接口要专门用 .NET 6 + SQL Server + JWT 重做一遍?
不是所有 CRUD 都叫“能上线的 CRUD”。我带过三届毕设,看过上百份 WebAPI 课设——八成卡在「登录后拿不到用户身份」「删不掉数据但也不报错」「Postman 调通了,前端一连就 401」。问题不在语法,而在整套链路没闭环:数据库连接池没配、JWT 密钥硬编码进代码、SQL Server 时间戳字段没处理、EF Core 迁移脚本一跑就丢表……这套方案不是教你怎么写GetById(int id),而是把从dotnet new webapi开始,到docker run -p 5000:80 api-image跑通带权限校验的完整增删改查,拆成可验证、可答辩、可部署的六个模块。适合正在赶毕设/课设、需要交一份“看起来像真做过项目”的同学,也适合刚转 .NET 的开发者补全生产级 API 的落地细节。它不讲 JWT 原理,但告诉你AddJwtBearer()里哪三个参数改错会导致前端永远收不到 token;不展开 SQL Server 的事务隔离级别,但明确写出TransactionScope在删除关联数据时怎么避免外键冲突。
2. 技术选型与环境准备:为什么是 .NET 6、SQL Server、JWT 而不是其他组合?
2.1 为什么锁定 .NET 6 而非 .NET 8 或 .NET Core 3.1?
.NET 6 是 LTS(长期支持)版本,也是首个真正统一开发体验的版本:单文件发布、最小 API 模式、原生 AOT 编译支持(虽本项目未启用,但为后续扩展留出余地)。更重要的是,它对 SQL Server 的 EF Core 6 支持最成熟——比如HasIndex(e => e.Email).IsUnique()在迁移中能正确生成唯一约束,而早期版本常静默失败;又如DateTimeOffset类型在 SQL Server 中映射为datetimeoffset(7),精度可控,避免 .NET Core 3.1 中因时区转换导致的DateTime值偏移。某高校课程设计明确要求“使用 LTS 版本”,.NET 6 正好卡在这个平衡点:比 .NET 5 更稳,比 .NET 8 更少踩新特性坑。如果你用 .NET 8,WebApplication.CreateBuilder()的配置方式已有细微变化(如AddControllersAsServices()已废弃),反而增加理解成本。
2.2 为什么选 SQL Server 而非 SQLite 或 PostgreSQL?
本方案面向的是企业级课设/毕设常见场景:需体现“真实数据库管理能力”。SQL Server 提供图形化 SSMS 工具,学生可直观看到表结构、索引、执行计划;其 T-SQL 语法(如MERGE、OUTPUT子句)在课程教学中覆盖度高;且与 Windows 环境天然兼容——本地开发用 LocalDB(轻量版 SQL Server),部署时无缝切换至远程 SQL Server 实例,无需改连接字符串逻辑。对比 SQLite:虽免安装,但不支持存储过程、行级安全策略,无法体现“数据库权限设计”这一关键得分点;PostgreSQL 虽开源强大,但 Windows 下安装配置复杂,学生常卡在pg_hba.conf权限或psql命令行导入失败。本方案中所有 SQL 脚本(建库、建表、初始化数据)均按 SQL Server 2019+ 语法编写,含CREATE SEQUENCE用于主键生成,ADD CONSTRAINT显式声明外键,全部可直接粘贴进 SSMS 执行。
2.3 为什么用 JWT 而非 Session 或 Cookie 认证?
课设答辩时,导师常问:“前后端分离怎么鉴权?”JWT 是标准答案。它无状态、可跨域、易调试(token 可直接在 jwt.io 解码),且 .NET 6 内置支持完善。关键在于——它强制你思考安全边界:密钥不能硬编码、token 过期时间必须可配、刷新机制需设计。本方案采用对称密钥(HMAC-SHA256),因课设场景无需公私钥体系;密钥通过IConfiguration读取,支持从appsettings.json、环境变量、Azure Key Vault(预留扩展点)注入;token payload 中明确包含UserId、Role、jti(防重放),避免常见漏洞。注意:不推荐在课设中用Microsoft.AspNetCore.Identity全家桶——它抽象层太厚,学生容易陷入UserManager和SignInManager的调用迷宫,反而说不清“token 到底在哪一步生成”。
提示:本方案所有技术选型均以“可解释、可演示、可截图答辩”为第一原则。比如 SQL Server 的“活动监视器”能实时展示连接数,JWT 的 token 可用 Postman 的 “Authorization → Bearer Token” 直接粘贴测试——这些细节,都是答辩时让导师点头的关键帧。
3. 总体架构设计:分层清晰、职责分明的六模块结构
3.1 整体分层:Controller → Service → Repository → Entity → DTO → Auth
本方案采用经典六层结构,但去掉过度设计的IUnitOfWork和IGenericRepository——课设阶段,过度抽象反而增加理解负担。实际分层如下:
- Controller 层:仅负责 HTTP 协议适配(接收请求、返回状态码、绑定模型),不做业务逻辑。所有 Action 方法标注
[Authorize]或[AllowAnonymous],[ApiController]启用自动模型验证。 - Service 层:核心业务逻辑所在。例如
UserService.RegisterAsync()封装密码哈希、邮箱去重、角色分配;OrderService.DeleteAsync()先检查订单状态再软删除。每个 Service 依赖IRepository<T>,但 Repository 接口由 Service 定义(而非全局定义),避免“为抽象而抽象”。 - Repository 层:基于 EF Core 的
DbContext封装。不暴露DbSet<T>,只提供GetByIdAsync()、ListAsync()、AddAsync()等方法。关键点:所有查询方法默认.AsNoTracking()(提高读性能),写操作显式调用SaveChangesAsync()并捕获DbUpdateConcurrencyException。 - Entity 层:纯 C# 类,对应数据库表。关键设计:
Id为主键(int或Guid),CreatedAt/UpdatedAt为DateTimeOffset,IsDeleted用于软删除(配合全局查询过滤器)。 - DTO 层:
UserCreateDto、UserResponseDto等,用于 Controller 与 Service 间数据传递。严格区分输入(含[Required]验证)和输出(不含敏感字段如密码哈希)。 - Auth 层:独立
JwtService类,封装GenerateToken()、ValidateToken()、RefreshToken()。GenerateToken()中SecurityTokenDescriptor的Subject设置为new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, user.Email) }),确保User.Identity.Name可用。
3.2 数据库实体设计:兼顾范式与课设实用性
以User和Post为例(典型一对多关系),实体类设计直击课设高频需求:
// Entities/User.cs public class User { public int Id { get; set; } [Required, EmailAddress, MaxLength(256)] public string Email { get; set; } = null!; [Required, MinLength(6)] public string PasswordHash { get; set; } = null!; public string FullName { get; set; } = null!; public bool IsActive { get; set; } = true; public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset? UpdatedAt { get; set; } public bool IsDeleted { get; set; } = false; // 导航属性 public ICollection<Post> Posts { get; set; } = new List<Post>(); } // Entities/Post.cs public class Post { public int Id { get; set; } [Required, MaxLength(100)] public string Title { get; set; } = null!; [Required] public string Content { get; set; } = null!; public int UserId { get; set; } // 外键 public User User { get; set; } = null!; // 导航属性 public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset? UpdatedAt { get; set; } public bool IsDeleted { get; set; } = false; }设计说明:
Email加[EmailAddress]验证,PasswordHash不加[Required](因注册时传明文,Service 层哈希后赋值),避免模型绑定失败;CreatedAt默认DateTimeOffset.UtcNow,EF Core 迁移时生成GETUTCDATE()默认值,解决本地时区偏差;IsDeleted配合DbContext.OnModelCreating()中的全局查询过滤器:modelBuilder.Entity<User>().HasQueryFilter(u => !u.IsDeleted),实现软删除——这是课设中体现“数据安全意识”的加分项;UserId与User导航属性并存,既满足外键约束,又支持.Include(u => u.Posts)预加载。
3.3 JWT 认证流程图解:从登录到接口调用的完整链路
认证不是黑匣子。本方案将 JWT 流程拆解为可验证的四步:
登录请求:
POST /api/auth/login,Body 为{ "email": "a@b.com", "password": "123" }
→ Controller 调用AuthService.LoginAsync()→ Service 校验邮箱密码 → 成功则调用JwtService.GenerateToken(user)生成 token。Token 生成:
JwtService.GenerateToken()构造SecurityTokenDescriptor:var tokenDescriptor = new SecurityTokenDescriptor { Subject = new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, user.Email), new Claim(ClaimTypes.Role, user.Role), // 如 "Admin" new Claim("UserId", user.Id.ToString()), // 自定义声明,Service 层常用 new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) // jti 防重放 }), Expires = DateTime.UtcNow.AddMinutes(_jwtSettings.ExpiryInMinutes), // 从配置读取 SigningCredentials = new SigningCredentials( new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSettings.Key)), SecurityAlgorithms.HmacSha256Signature) };关键点:
Expires必须动态读取配置(非硬编码),jti每次登录唯一,UserId声明便于后续接口直接获取 ID。响应返回:Controller 返回
Ok(new { token, expiresIn = _jwtSettings.ExpiryInMinutes * 60 }),前端存入 localStorage。受保护接口调用:
GET /api/posts请求头Authorization: Bearer <token>
→[Authorize]特性触发JwtBearerHandler→ 验证签名、过期、jti(需自行实现黑名单,本方案用内存缓存ConcurrentDictionary<string, bool>存已注销 token)→ 验证通过后,HttpContext.User可通过User.FindFirstValue(ClaimTypes.Name)获取邮箱。
注意:课设中务必演示
jti黑名单机制。可在AuthService.LogoutAsync()中将当前 token 的jti加入缓存,并在JwtBearerOptions.Events.OnTokenValidated中检查——这是体现“安全深度”的硬核细节。
4. 核心模块实现:从数据库迁移、JWT 配置到增删改查接口
4.1 SQL Server 连接与 EF Core 迁移:零错误建库脚本
连接字符串必须支持开发与部署切换。appsettings.json中配置:
{ "ConnectionStrings": { "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=ApiDb;Trusted_Connection=true;MultipleActiveResultSets=true" }, "JwtSettings": { "Key": "your-32-byte-secret-key-here-12345678901234567890123456789012", "ExpiryInMinutes": 60 } }关键点:
MultipleActiveResultSets=true启用 MARS,避免DataReader未关闭导致的并发异常;Trusted_Connection=true本地开发用 Windows 认证,部署时改为User ID=sa;Password=xxx;- JWT 密钥长度必须为 32 字节(256 位),否则
SymmetricSecurityKey构造失败——这是学生最常翻车点。
EF Core 迁移命令(在项目根目录执行):
# 初始化迁移(生成第一个迁移文件) dotnet ef migrations add InitialCreate --project .\Api.csproj --startup-project .\Api.csproj # 应用迁移(创建数据库和表) dotnet ef database update --project .\Api.csproj --startup-project .\Api.csproj生成的迁移文件InitialCreate.cs中,Up(MigrationBuilder migrationBuilder)方法会自动添加:
migrationBuilder.CreateTable( name: "Users", columns: table => new { Id = table.Column<int>(type: "int", nullable: false) .Annotation("SqlServer:Identity", "1, 1"), Email = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false), PasswordHash = table.Column<string>(type: "nvarchar(max)", nullable: false), FullName = table.Column<string>(type: "nvarchar(max)", nullable: true), IsActive = table.Column<bool>(type: "bit", nullable: false, defaultValue: true), CreatedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false, defaultValueSql: "GETUTCDATE()"), UpdatedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: true), IsDeleted = table.Column<bool>(type: "bit", nullable: false, defaultValue: false) }, constraints: table => { table.PrimaryKey("PK_Users", x => x.Id); table.UniqueConstraint("AK_Users_Email", x => x.Email); // 唯一索引 });设计说明:defaultValueSql: "GETUTCDATE()"确保数据库层面生成时间,避免应用服务器时区差异;UniqueConstraint显式声明邮箱唯一,比[Index]特性更可靠。
4.2 JWT 认证中间件配置:三处关键代码
在Program.cs中配置 JWT,共三处不可省略:
// 1. 注册服务(在 builder.Services 里) builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = false, // 课设通常不验 Issuer ValidateAudience = false, // 同上 ValidateLifetime = true, // 必须为 true ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey( Encoding.UTF8.GetBytes(builder.Configuration["JwtSettings:Key"]!)), ClockSkew = TimeSpan.Zero // 严格校验过期,避免宽容导致安全隐患 }; // 2. 添加事件:验证通过后检查 jti 是否在黑名单 options.Events = new JwtBearerEvents { OnTokenValidated = context => { var jti = context.Principal?.FindFirstValue(JwtRegisteredClaimNames.Jti); if (jti != null && context.HttpContext.RequestServices .GetRequiredService<IJwtBlacklistService>() .IsBlacklisted(jti)) { context.Fail("Token has been revoked"); } return Task.CompletedTask; } }; }); // 3. 启用认证中间件(在 app.UseAuthentication() 之后) app.UseAuthentication(); app.UseAuthorization();避坑 / 常见问题 / 排查
现象:前端调用/api/posts返回 401,但 token 在 jwt.io 解码正常。
原因:ClockSkew默认为 5 分钟,若服务器时间与客户端偏差过大,或 token 刚生成就立即使用(毫秒级偏差),可能被判定为“尚未生效”。
解决:设ClockSkew = TimeSpan.Zero,并确保服务器时间同步(Windows 下运行w32tm /resync)。
现象:HttpContext.User.Identity.IsAuthenticated始终为false。
原因:UseAuthentication()调用位置错误,必须在UseRouting()之后、UseEndpoints()之前。
解决:严格按顺序:UseRouting()→UseAuthentication()→UseAuthorization()→UseEndpoints()。
现象:User.FindFirstValue(ClaimTypes.Name)返回 null。
原因:Subject中未添加ClaimTypes.Name声明,或ClaimTypes.Name拼写错误(如写成"name"字符串)。
解决:Subject必须用ClaimTypes.Name常量,且ClaimsIdentity构造时传入数组。
现象:dotnet ef database update报错 “The underlying provider does not support the 'GetDatabaseName' method”。
原因:DbContext构造函数中未正确注入DbContextOptions,或OnConfiguring覆盖了依赖注入配置。
解决:删除OnConfiguring,在Startup.cs或Program.cs中用options.UseSqlServer(connectionString)配置。
现象:POST /api/users创建用户后,Email字段在数据库中为 NULL。
原因:DTO 中Email属性未加[Required],或Controller的[FromBody]绑定失败后静默忽略。
解决:DTO 必须加完整验证特性,Controller 方法添加if (!ModelState.IsValid) return BadRequest(ModelState);。
5. 接口实现与测试:覆盖增删改查全场景的 Controller 代码
5.1 用户注册与登录接口:带密码哈希与邮箱验证
AuthController.cs实现:
[ApiController] [Route("api/[controller]")] public class AuthController : ControllerBase { private readonly IUserService _userService; private readonly IJwtService _jwtService; public AuthController(IUserService userService, IJwtService jwtService) { _userService = userService; _jwtService = jwtService; } [HttpPost("register")] [AllowAnonymous] public async Task<ActionResult> Register([FromBody] UserRegisterDto dto) { if (!ModelState.IsValid) return BadRequest(ModelState); try { var user = await _userService.RegisterAsync(dto); var token = _jwtService.GenerateToken(user); return Ok(new { token, expiresIn = _jwtService.GetExpirySeconds() }); } catch (InvalidOperationException ex) when (ex.Message.Contains("email")) { return BadRequest(new { message = "Email already exists" }); } catch (Exception ex) { // 生产环境应记录日志,课设可简化 return StatusCode(500, new { message = "Registration failed" }); } } [HttpPost("login")] [AllowAnonymous] public async Task<ActionResult> Login([FromBody] UserLoginDto dto) { if (!ModelState.IsValid) return BadRequest(ModelState); var user = await _userService.ValidateCredentialsAsync(dto.Email, dto.Password); if (user == null) return Unauthorized(new { message = "Invalid email or password" }); var token = _jwtService.GenerateToken(user); return Ok(new { token, expiresIn = _jwtService.GetExpirySeconds() }); } }关键设计点:
RegisterAsync()内部调用PasswordHasher<User>.HashPassword(),而非BCrypt.Net等第三方库,减少依赖;ValidateCredentialsAsync()使用PasswordHasher<User>.VerifyHashedPassword()校验,避免明文比对;- 异常处理区分业务异常(邮箱重复)与系统异常,返回不同状态码和消息,方便前端提示。
5.2 文章增删改查接口:带软删除与权限控制
PostsController.cs实现(管理员可删他人文章,普通用户只能删自己):
[ApiController] [Route("api/[controller]")] [Authorize] // 全局授权 public class PostsController : ControllerBase { private readonly IPostService _postService; public PostsController(IPostService postService) { _postService = postService; } [HttpGet] public async Task<ActionResult<IEnumerable<PostResponseDto>>> GetAll() { var posts = await _postService.GetAllAsync(); return Ok(posts); } [HttpGet("{id:int}")] public async Task<ActionResult<PostResponseDto>> GetById(int id) { var post = await _postService.GetByIdAsync(id); if (post == null) return NotFound(); return Ok(post); } [HttpPost] public async Task<ActionResult<PostResponseDto>> Create([FromBody] PostCreateDto dto) { // 从 JWT 中提取 UserId var userId = int.Parse(User.FindFirstValue("UserId")!); var post = await _postService.CreateAsync(dto, userId); return CreatedAtAction(nameof(GetById), new { id = post.Id }, post); } [HttpPut("{id:int}")] public async Task<IActionResult> Update(int id, [FromBody] PostUpdateDto dto) { var userId = int.Parse(User.FindFirstValue("UserId")!); var success = await _postService.UpdateAsync(id, dto, userId); if (!success) return Forbid(); // 无权修改他人文章 return NoContent(); } [HttpDelete("{id:int}")] public async Task<IActionResult> Delete(int id) { var userId = int.Parse(User.FindFirstValue("UserId")!); var role = User.FindFirstValue(ClaimTypes.Role); var success = await _postService.DeleteAsync(id, userId, role == "Admin"); if (!success) return Forbid(); return NoContent(); } }设计说明:
CreateAsync()和UpdateAsync()的userId参数来自 JWT 声明,而非请求 Body,防止前端伪造;DeleteAsync()接收isAdmin标志,Service 层据此决定是否跳过用户 ID 校验;CreatedAtAction()返回 201 状态码及 Location 头,符合 REST 规范,Postman 可直接点击跳转。
5.3 Service 层关键逻辑:软删除与并发控制
PostService.cs中DeleteAsync实现:
public async Task<bool> DeleteAsync(int id, int userId, bool isAdmin) { var post = await _context.Posts .FirstOrDefaultAsync(p => p.Id == id && !p.IsDeleted); if (post == null) return false; // 权限校验 if (!isAdmin && post.UserId != userId) return false; // 软删除:更新 IsDeleted 和 UpdatedAt post.IsDeleted = true; post.UpdatedAt = DateTimeOffset.UtcNow; try { await _context.SaveChangesAsync(); return true; } catch (DbUpdateConcurrencyException) { // 处理并发:检查记录是否已被删除 var entry = _context.Entry(post); if (entry.State == EntityState.Detached) { // 记录已不存在,视为删除成功 return true; } throw; } }为什么不用物理删除?
课设答辩时,导师会问“如何保证数据可追溯?”。软删除 +UpdatedAt字段,配合数据库审计日志(SSMS 中可右键表 → “查看更改跟踪”),能完整回答此问题。物理删除则无法回溯。
6. 验收与答辩:用这五步让导师当场打满分
6.1 本地全流程验收清单:每一步都可截图演示
别只说“功能正常”,要让导师亲眼看到每个环节的输出。按顺序执行以下五步,每步截图保存(建议用 Snipaste 截图,带窗口标题栏):
| 步骤 | 操作 | 预期结果 | 截图重点 |
|---|---|---|---|
| 1. 数据库验证 | 打开 SSMS → 连接(localdb)\mssqllocaldb→ 展开ApiDb→ 查看Users表数据 | 显示Id,Email,PasswordHash(哈希值非明文)、CreatedAt(UTC 时间) | 表数据网格 + 列名,突出IsDeleted = 0 |
| 2. 注册登录验证 | Postman 发送POST /api/auth/register→POST /api/auth/login | 登录响应中token字段存在,expiresIn为3600 | Postman 响应 JSON,高亮token和expiresIn |
| 3. Token 解码验证 | 将上步 token 粘贴到 https://jwt.io | Header 显示HS256,Payload 显示name,role,UserId,jti,exp(时间戳转为北京时间) | jwt.io 界面,Payload 区域,标出jti和exp |
| 4. 受保护接口验证 | PostmanGET /api/posts,Headers 加Authorization: Bearer <token> | 返回200 OK及文章列表 JSON | Postman 请求头 + 响应状态码 + JSON 数据 |
| 5. 权限拦截验证 | 修改 Postman 中 token 的jti值(任意改一个字符)→ 再发GET /api/posts | 返回401 Unauthorized,响应体含message: "Invalid token" | Postman 响应状态码 + 错误消息 |
提示:答辩前务必用另一台电脑(或虚拟机)重装一遍环境,从
dotnet new webapi开始,走完上述五步。很多同学答辩时发现“本地能跑,换台电脑就崩”,根源是appsettings.json里写了绝对路径或硬编码密钥。
6.2 答辩话术设计:用三个问题预判导师关注点
导师不会逐行看代码,但会问本质问题。提前准备好这三问的回答,能极大提升专业感:
Q1:“JWT 密钥怎么保证安全?如果代码泄露,密钥不就暴露了吗?”
A:密钥绝不提交到 Git。我们在.gitignore中加入appsettings.Production.json,生产环境密钥通过 Azure Key Vault 或 Docker 环境变量注入。课设中,我们演示了两种方式:开发用appsettings.Development.json(密钥明文,但 .gitignore 已排除),部署时用--environment Production启动,密钥从环境变量JWT_KEY读取。代码中builder.Configuration["JwtSettings:Key"]会自动优先读取环境变量。
Q2:“软删除后,统计文章总数时,会不会把已删文章也算进去?”
A:不会。我们在PostService.GetAllAsync()中使用await _context.Posts.Where(p => !p.IsDeleted).ToListAsync(),EF Core 生成的 SQL 自动加上WHERE [IsDeleted] = 0。更重要的是,我们在DbContext.OnModelCreating()中配置了全局查询过滤器:modelBuilder.Entity<Post>().HasQueryFilter(p => !p.IsDeleted),这样所有DbSet<Post>查询都默认过滤已删数据,避免遗漏。
Q3:“如果两个用户同时编辑同一篇文章,怎么保证数据不被覆盖?”
A:我们实现了乐观并发控制。在Post实体中添加public byte[] RowVersion { get; set; }属性,并在OnModelCreating中配置.IsRowVersion()。更新时,EF Core 自动生成WHERE [RowVersion] = @p0条件,若数据库中RowVersion已变(被他人更新),则抛出DbUpdateConcurrencyException。Service 层捕获此异常,返回409 Conflict,前端可提示“内容已被他人修改,请刷新后重试”。
6.3 代码质量自检:答辩前必做的三件事
别让低级错误毁掉努力。每次答辩前,强制执行:
删掉所有
Console.WriteLine和Debug.WriteLine:课设中常见“调试语句忘删”,导致接口响应体混入乱码。用 VS 的“查找全部”搜索WriteLine,确认只在Program.cs的builder.Host.ConfigureLogging中有日志配置。检查
appsettings.json的ConnectionStrings和JwtSettings是否为空:用 Notepad++ 打开,确认DefaultConnection值非空字符串,Key长度为 32 字符(可用在线工具 https://www.browserling.com/tools/text-to-hex 验证)。用
dotnet publish -c Release -r win-x64打包,然后在干净 Windows 机器上运行Api.exe:这是终极考验。若失败,90% 是Microsoft.Data.SqlClient未正确打包(.NET 6 默认不包含该 NuGet,需在.csproj中显式添加<PackageReference Include="Microsoft.Data.SqlClient" Version="5.1.5" />)。
从那以后我每次交课设,都强制走一遍这三步:先本地五步验收截图,再模拟导师问三个问题,最后 clean machine 打包验证。不是为了应付,而是让“能跑”变成“经得起推敲”。希望帮到你。
本文还有配套的精品资源,点击获取