恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
.NET三层架构教学案例:基于3D彩票规则的WinForms实训项目
首页
资讯中心
/
.NET三层架构教学案例:基于3D彩票规则的WinForms实训项目
.NET三层架构教学案例:基于3D彩票规则的WinForms实训项目
发布时间:2026/10/13 7:11:05
简介本资源是一套基于.NET框架开发的中国福利彩票3D游戏管理系统源码面向.NET初学者、中级开发者及对彩票业务系统感兴趣的技术人员用于学习彩票销售、开奖查询、数据管理等核心业务逻辑的工程化实现。压缩包共65个文件7.67MB含22个C#业务逻辑文件如History.cs、Find.cs、AddNumber.cs、7个.resx本地化资源、3个.config配置文件、1个.accdb本地数据库、1个.sln解决方案及配套PDF说明文档完整覆盖UI层、数据访问层与业务规则层。已有2394人学习下载适合通过真实项目理解ASP.NET窗体应用、ADO.NET数据库交互、随机数生成机制与WinForms界面设计。源码结构清晰含多模块设计器文件与历史记录管理类附《源码必读.pdf》指导入门是深入掌握.NET桌面应用开发与行业系统建模的优质实践材料。1. 这不是彩票系统源码而是.NET平台下典型的三层架构教学案例为什么“中国福利彩票3D”常被用作实训项目名称你搜到“.net中国福利彩票3D源码”大概率不会拿到能联网出号、对接官方接口、参与真实销售的系统——那根本不可能开源也不符合国家对彩票发行管理的严格规范。真实情况是这是一个在高校计算机课程、.NET初学者训练营、企业内部技术练兵中高频出现的教学型Demo项目。它用“3D彩票”这个大众熟悉、规则清晰百位/十位/个位三位数字000–999、业务逻辑边界明确模拟选号、开奖、比对、计奖的场景承载了.NET桌面开发WinForms/WPF、数据访问ADO.NET Entity Framework、分层架构UI/BLL/DAL、基础算法随机数生成与校验、奖金计算逻辑等一整套入门到进阶的技术栈落地。它不解决真实彩票运营问题但能扎实锤炼一个开发者对.NET生态的工程化理解。适合刚学完C#语法、想把零散知识点串成可运行项目的在校生或需要快速验证某项技术点比如WinForms绑定DataGridView、SQL Server本地数据库建模的一线工程师。别把它当生产系统复刻要把它当一块“技术试金石”。2. 从零搭建这个教学项目用Visual Studio 2022 .NET 6 创建标准三层结构这个项目的核心价值不在“彩票”而在“结构”。我们不依赖任何第三方模板或压缩包全程手搭确保每层职责清晰、耦合可控。以下步骤基于 Visual Studio 2022v17.4和 .NET SDK 6.0LTS这是当前企业级教学最稳的组合。2.1 创建解决方案与三个核心项目打开 Visual Studio新建“空白解决方案”命名为Lottery3DDemo。然后依次添加三个类库Class Library项目Lottery3D.Data数据访问层DAL负责所有与数据库交互的代码Lottery3D.Business业务逻辑层BLL封装选号规则、开奖逻辑、中奖判定、奖金计算等纯业务代码Lottery3D.UI用户界面层UIWinForms 窗体应用只负责展示和用户操作绝不直接调用数据库。提示务必选择.NET 6.0作为各项目的 Target Framework。避免混用 .NET Framework 和 .NET Core/.NET 5这是后续引用和 NuGet 包兼容性的最大雷区。2.2 设计数据库模型并生成实体类我们使用轻量级 SQL Server LocalDB随 VS 自动安装作为本地数据源。在Lottery3D.Data项目中创建Models文件夹添加以下两个实体类// Models/DrawResult.cs namespace Lottery3D.Data.Models; public class DrawResult { public int Id { get; set; } public string DrawDate { get; set; } DateTime.Now.ToString(yyyyMMdd); // 格式20240520 public string WinningNumber { get; set; } 000; // 格式XXX如123 public DateTime CreatedAt { get; set; } DateTime.Now; }// Models/PlayerBet.cs namespace Lottery3D.Data.Models; public class PlayerBet { public int Id { get; set; } public string PlayerName { get; set; } 匿名; public string BetNumber { get; set; } 000; // 用户投注号码 public decimal BetAmount { get; set; } 2.0m; // 单注金额单位元 public bool IsWinning { get; set; } public decimal Prize { get; set; } 0.0m; public DateTime BetTime { get; set; } DateTime.Now; }接着在Lottery3D.Data中安装 NuGet 包Microsoft.EntityFrameworkCore.SqlServerv6.0.28Microsoft.EntityFrameworkCore.Toolsv6.0.28注意版本号必须与项目目标框架 .NET 6.0 对齐。高版本 EF Core如 v7.x/v8.x在 .NET 6 项目中会引发运行时异常这是新手最常翻车的第一步。2.3 实现数据访问层DAL用 EF Core 封装增删查在Lottery3D.Data中创建Contexts文件夹添加LotteryDbContext.cs// Contexts/LotteryDbContext.cs using Microsoft.EntityFrameworkCore; using Lottery3D.Data.Models; namespace Lottery3D.Data.Contexts; public class LotteryDbContext : DbContext { public LotteryDbContext(DbContextOptionsLotteryDbContext options) : base(options) { } public DbSetDrawResult DrawResults { get; set; } public DbSetPlayerBet PlayerBets { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.EntityDrawResult().HasKey(x x.Id); modelBuilder.EntityPlayerBet().HasKey(x x.Id); // 可添加索引提升查询性能 modelBuilder.EntityPlayerBet().HasIndex(x x.BetTime); } }再创建Repositories文件夹添加通用仓储基类BaseRepository.cs和具体实现DrawResultRepository.cs// Repositories/BaseRepository.cs using Microsoft.EntityFrameworkCore; using System.Linq.Expressions; namespace Lottery3D.Data.Repositories; public class BaseRepositoryT where T : class { protected readonly LotteryDbContext _context; protected readonly DbSetT _dbSet; public BaseRepository(LotteryDbContext context) { _context context; _dbSet context.SetT(); } public virtual async TaskIEnumerableT GetAllAsync() await _dbSet.ToListAsync(); public virtual async TaskT? GetByIdAsync(int id) await _dbSet.FindAsync(id); public virtual async Task AddAsync(T entity) await _dbSet.AddAsync(entity); public virtual async Task SaveChangesAsync() await _context.SaveChangesAsync(); }// Repositories/DrawResultRepository.cs using Lottery3D.Data.Models; namespace Lottery3D.Data.Repositories; public class DrawResultRepository : BaseRepositoryDrawResult { public DrawResultRepository(LotteryDbContext context) : base(context) { } // 专用方法获取最新一期开奖号 public async TaskDrawResult? GetLatestAsync() await _dbSet.OrderByDescending(x x.Id).FirstOrDefaultAsync(); }逻辑说明BaseRepository提供了 CRUD 基础能力而DrawResultRepository在其上叠加了业务语义方法如GetLatestAsync。这种分层让 BLL 层调用时语义清晰_drawRepo.GetLatestAsync()比_context.DrawResults.OrderByDescending(...)直观得多也便于未来替换底层存储如从 SQL Server 换成 SQLite时只改 DAL 层。2.4 注册依赖注入并初始化数据库回到Lottery3D.UI项目WinForms 应用在Program.cs中配置 DI 容器// Lottery3D.UI/Program.cs using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Lottery3D.Data.Contexts; using Lottery3D.Data.Repositories; using Microsoft.EntityFrameworkCore; var host Host.CreateDefaultBuilder(args) .ConfigureServices((context, services) { // 注册 DbContext使用 LocalDB services.AddDbContextLotteryDbContext(options options.UseSqlServer(Data Source(localdb)\MSSQLLocalDB;Initial CatalogLottery3D;Integrated Securitytrue;)); // 注册仓储 services.AddScopedDrawResultRepository(); services.AddScopedPlayerBetRepository(); // 同理实现 // 注册窗体关键让窗体也能接收依赖 services.AddTransientMainForm(); }) .Build(); ApplicationConfiguration.Initialize(); Application.Run(host.Services.GetRequiredServiceMainForm());首次运行前需在Package Manager Console设置默认项目为Lottery3D.Data执行Add-Migration InitialCreate -Context LotteryDbContext Update-Database -Context LotteryDbContext这将自动创建Lottery3D数据库及两张表。注意LocalDB 实例名(localdb)\MSSQLLocalDB是 VS 默认值若你的环境不同如用 SQL Server Express请同步修改连接字符串。3. 业务逻辑层BLL设计把“3D规则”变成可测试、可复用的方法BLL 是这个教学项目真正的“大脑”。它不关心界面长什么样也不管数据存哪只专注回答“用户投了123本期开的是456该中几等奖奖金多少”——这就是它的全部使命。我们用纯 C# 类实现不依赖 UI 或 DAL 的具体类型保证单元测试友好性。3.1 定义业务契约与输入输出模型在Lottery3D.Business项目中创建Contracts文件夹定义清晰的入参与返回结构// Contracts/DrawRequest.cs namespace Lottery3D.Business.Contracts; public class DrawRequest { public string DrawDate { get; set; } DateTime.Now.ToString(yyyyMMdd); } // Contracts/DrawResultDto.cs namespace Lottery3D.Business.Contracts; public class DrawResultDto { public int Id { get; set; } public string DrawDate { get; set; } string.Empty; public string WinningNumber { get; set; } 000; public DateTime CreatedAt { get; set; } } // Contracts/BetResultDto.cs namespace Lottery3D.Business.Contracts; public class BetResultDto { public bool IsWinning { get; set; } public int PrizeLevel { get; set; } // 1直选, 2组三, 3组六, 0未中 public decimal PrizeAmount { get; set; } public string WinningRule { get; set; } string.Empty; }说明DTOData Transfer Object隔离了 BLL 与 UI 层的数据契约。UI 层传入DrawRequestBLL 返回DrawResultDto或BetResultDto双方都不直接暴露Data.Models.DrawResult这样的实体类。这是解耦的关键一步也是很多新手忽略的“玄学”细节——看似多写几个类实则为后期扩展如加字段、改格式省下大量重构时间。3.2 实现核心开奖与比对逻辑创建Services文件夹添加LotteryService.cs// Services/LotteryService.cs using Lottery3D.Business.Contracts; using Lottery3D.Data.Models; using Lottery3D.Data.Repositories; using System.Security.Cryptography; namespace Lottery3D.Business.Services; public class LotteryService { private readonly DrawResultRepository _drawRepo; private readonly PlayerBetRepository _betRepo; public LotteryService(DrawResultRepository drawRepo, PlayerBetRepository betRepo) { _drawRepo drawRepo; _betRepo betRepo; } // 模拟开奖生成三位随机数000-999 public DrawResultDto GenerateDrawResult(DrawRequest request) { var random RandomNumberGenerator.Create(); var buffer new byte[3]; random.GetBytes(buffer); var num (buffer[0] % 10) * 100 (buffer[1] % 10) * 10 (buffer[2] % 10); var winningNum num.ToString(D3); // 格式化为000-999 var result new DrawResult { DrawDate request.DrawDate, WinningNumber winningNum }; // 保存到数据库调用 DAL _drawRepo.AddAsync(result).Wait(); // 此处为简化实际应异步传递 _drawRepo.SaveChangesAsync().Wait(); return new DrawResultDto { Id result.Id, DrawDate result.DrawDate, WinningNumber result.WinningNumber, CreatedAt result.CreatedAt }; } // 判定投注结果 public BetResultDto EvaluateBet(string playerBet, string winningNumber) { if (playerBet.Length ! 3 || winningNumber.Length ! 3) return new BetResultDto { PrizeLevel 0, PrizeAmount 0, WinningRule 号码格式错误 }; // 直选完全一致 if (playerBet winningNumber) return new BetResultDto { IsWinning true, PrizeLevel 1, PrizeAmount 1040.0m, // 直选奖金1040元 WinningRule 直选 }; // 组三三位中有两位相同如112, 343 var chars playerBet.OrderBy(c c).ToArray(); if (chars[0] chars[1] || chars[1] chars[2] || chars[0] chars[2]) { // 检查是否为组三三个数字中恰有两个相同 var distinctCount playerBet.Distinct().Count(); if (distinctCount 2) return new BetResultDto { IsWinning true, PrizeLevel 2, PrizeAmount 346.0m, // 组三奖金346元 WinningRule 组三 }; } // 组六三位全不同如123, 456 if (playerBet.Distinct().Count() 3) { var winChars winningNumber.OrderBy(c c).ToArray(); var playChars playerBet.OrderBy(c c).ToArray(); if (playChars.SequenceEqual(winChars)) return new BetResultDto { IsWinning true, PrizeLevel 3, PrizeAmount 173.0m, // 组六奖金173元 WinningRule 组六 }; } return new BetResultDto { PrizeLevel 0, PrizeAmount 0, WinningRule 未中奖 }; } }参数说明EvaluateBet方法接受两个字符串参数内部做字符级比对不依赖数据库。这意味着你可以脱离 UI 和数据库直接对这个方法写单元测试例如Assert.AreEqual(1, service.EvaluateBet(123, 123).PrizeLevel)验证所有规则分支。这才是“业务逻辑可测试”的真正含义。3.3 在 BLL 层注册服务并注入到 UI在Lottery3D.Business项目中添加DependencyInjection.cs// DependencyInjection.cs using Microsoft.Extensions.DependencyInjection; using Lottery3D.Business.Services; namespace Lottery3D.Business; public static class DependencyInjection { public static IServiceCollection AddBusinessServices(this IServiceCollection services) { services.AddScopedLotteryService(); return services; } }然后回到Lottery3D.UI/Program.cs在ConfigureServices中追加// Program.cs 内 ConfigureServices 块中追加 services.AddBusinessServices(); // ← 新增这一行现在MainForm构造函数就能通过构造函数注入LotteryService了// Lottery3D.UI/MainForm.cs public partial class MainForm : Form { private readonly LotteryService _lotteryService; public MainForm(LotteryService lotteryService) { InitializeComponent(); _lotteryService lotteryService; } }4. WinForms 界面层UI实现用最小代码完成交互闭环UI 层的目标是“把事情做对”而不是“做得炫酷”。我们用最朴素的控件组合完成“输入投注号 → 点击开奖 → 显示结果 → 记录到数据库”的完整链路。重点在于如何让 UI 层干净地调用 BLL又不污染 BLL4.1 设计主窗体布局与控件命名在Lottery3D.UI中添加MainForm.csWinForms 窗体拖入以下控件并设置Name属性命名规范是后续代码可读性的基础控件类型Name 属性Text/LabelLabellblBetNumber请输入投注号码三位数字TextBoxtxtBetNumber空ButtonbtnDraw开 奖LabellblResult空DataGridViewdgvHistory空注意txtBetNumber只接受数字输入我们在KeyPress事件中强制过滤// MainForm.cs private void txtBetNumber_KeyPress(object sender, KeyPressEventArgs e) { if (!char.IsControl(e.KeyChar) !char.IsDigit(e.KeyChar)) { e.Handled true; // 拦截非数字输入 } if (txtBetNumber.Text.Length 3 !char.IsControl(e.KeyChar)) { e.Handled true; // 限制最多3位 } }4.2 实现“开奖”按钮点击逻辑串联 UI→BLL→DAL双击btnDraw编写事件处理private async void btnDraw_Click(object sender, EventArgs e) { var betInput txtBetNumber.Text.Trim(); if (string.IsNullOrEmpty(betInput) || betInput.Length ! 3) { MessageBox.Show(请输入三位有效数字, 输入错误, MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } try { // Step 1: 调用 BLL 生成本期开奖号 var drawReq new DrawRequest { DrawDate DateTime.Now.ToString(yyyyMMdd) }; var drawResult _lotteryService.GenerateDrawResult(drawReq); // Step 2: 调用 BLL 判定投注结果 var betResult _lotteryService.EvaluateBet(betInput, drawResult.WinningNumber); // Step 3: 更新 UI 显示 lblResult.Text $本期开奖号{drawResult.WinningNumber}\n $您的投注{betInput}\n $中奖等级{betResult.WinningRule}{betResult.PrizeLevel}等奖\n $奖金¥{betResult.PrizeAmount:F2}; // Step 4: 保存投注记录到数据库调用 DAL var playerBet new PlayerBet { PlayerName 测试用户, BetNumber betInput, BetAmount 2.0m, IsWinning betResult.IsWinning, Prize betResult.PrizeAmount }; await _playerBetRepo.AddAsync(playerBet); await _playerBetRepo.SaveChangesAsync(); // Step 5: 刷新历史记录表格 await LoadHistoryAsync(); } catch (Exception ex) { MessageBox.Show($操作失败{ex.Message}, 系统错误, MessageBoxButtons.OK, MessageBoxIcon.Error); } }逻辑说明这段代码清晰体现了“分层调用”的价值。UI 层只做三件事1收集用户输入2调用 BLL 方法获得结果3把结果渲染到界面上。它不关心GenerateDrawResult怎么生成随机数也不管EvaluateBet怎么判断组三更不碰AddAsync怎么写 SQL——这些都交给对应层去管。这种“各司其职”正是大型项目可维护性的根基。4.3 加载历史投注记录到 DataGridViewdgvHistory需要绑定数据源。我们用BindingSource实现自动刷新// MainForm.cs 成员变量 private BindingSource _historyBindingSource new BindingSource(); // 在 InitializeComponent() 后添加 private void InitializeDataGridView() { dgvHistory.AutoGenerateColumns false; dgvHistory.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName BetNumber, HeaderText 投注号码, Width 80 }); dgvHistory.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName IsWinning, HeaderText 是否中奖, Width 80, CellTemplate new DataGridViewCheckBoxCell() }); dgvHistory.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName Prize, HeaderText 奖金元, Width 100, DefaultCellStyle new DataGridViewCellStyle { Format C2 } }); dgvHistory.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName BetTime, HeaderText 投注时间, Width 150, DefaultCellStyle new DataGridViewCellStyle { Format yyyy-MM-dd HH:mm:ss } }); dgvHistory.DataSource _historyBindingSource; } // 异步加载历史数据 private async Task LoadHistoryAsync() { try { var history await _playerBetRepo.GetAllAsync(); _historyBindingSource.DataSource history.ToList(); } catch (Exception ex) { MessageBox.Show($加载历史失败{ex.Message}); } }关键点BindingSource是 WinForms 数据绑定的“中间人”。我们把ListPlayerBet赋给它dgvHistory就能自动显示、排序、甚至支持编辑后回写本例未启用。比起手动循环Rows.Add(...)这种方式更符合现代 WinForms 开发范式且易于扩展比如加搜索框只需BindingSource.Filter BetNumber LIKE 12%。5. 避坑指南那些让新手调试到凌晨三点的典型问题这个项目看似简单但每个环节都有“看似合理、实则致命”的陷阱。以下是我在多个教学现场反复看到的 4 个高频翻车点按现象→原因→解决给出血泪经验。5.1 现象点击“开奖”按钮后程序卡死或报System.InvalidOperationException: Synchronous operations are disallowed.原因EF Core 6 默认禁止在异步上下文中执行同步数据库操作如.Result、.Wait()。而 WinForms 事件处理器是同步的若在GenerateDrawResult中用了_drawRepo.AddAsync(...).Wait()就会触发此异常。解决方案一推荐将事件处理器改为async void btnDraw_Click内部所有await调用保持异步链路方案二在 BLL 方法中提供同步重载如GenerateDrawResultSync但仅限教学演示生产环境严禁。5.2 现象dgvHistory显示为空或列头显示Property1,Property2而非自定义标题原因AutoGenerateColumns true时DataGridView 会根据ListT的属性名自动生成列但PlayerBet的IsWinning是布尔值默认生成DataGridViewCheckBoxColumn失败同时未手动设置DataPropertyName导致绑定失效。解决务必设置AutoGenerateColumns false手动添加DataGridViewTextBoxColumn并精确指定DataPropertyName对于bool类型用DataGridViewCheckBoxColumn并设置DataPropertyName见 4.3 节代码。5.3 现象LocalDB 连接失败报错A network-related or instance-specific error occurred...原因Visual Studio 安装时未勾选 “SQL Server Express LocalDB”或 LocalDB 实例未启动。解决打开命令提示符运行sqllocaldb info查看实例列表若无(localdb)\MSSQLLocalDB运行sqllocaldb create MSSQLLocalDB在 VS 中通过“SQL Server 对象资源管理器”连接(localdb)\MSSQLLocalDB确认能连通后再运行项目。5.4 现象txtBetNumber输入 001EvaluateBet判定为未中奖但开奖号也是 001原因字符串比较时001 001为 true但若winningNumber是1未格式化为三位则比较失败。根源在GenerateDrawResult中num.ToString(D3)缺失导致1被存为1而非001。解决严格使用ToString(D3)格式化所有三位数字在EvaluateBet开头增加日志Console.WriteLine($DEBUG: player{playerBet}, win{winningNumber});第一时间定位字符串实际值。注意以上四条每一条都来自真实教学现场的“崩溃瞬间”。它们不涉及高深算法却足以让一个认真写代码的人怀疑人生。记住在 .NET 分层架构里90% 的问题出在“层与层之间怎么传数据”上而不是“某一层内部怎么写”。6. 进阶技巧用单元测试固化业务规则让“3D逻辑”不再靠人肉验证写完功能只是开始让逻辑长期可靠得靠自动化测试。这个项目最大的教学价值恰恰在于它提供了绝佳的单元测试切入点——所有核心规则直选/组三/组六判定都封装在LotteryService.EvaluateBet中且不依赖任何外部资源数据库、UI。这意味着我们可以用xUnit或NUnit几行代码就覆盖全部分支。6.1 为 BLL 添加单元测试项目在解决方案中右键 → “添加” → “新项目”选择“xUnit Test Project (.NET 6)”命名为Lottery3D.Business.Tests。然后添加对Lottery3D.Business项目的项目引用。安装 NuGet 包xunit、xunit.runner.visualstudiov2.4.5。6.2 编写覆盖全部规则的测试用例在测试项目中创建LotteryServiceTests.cs// Lottery3D.Business.Tests/LotteryServiceTests.cs using Xunit; using Lottery3D.Business.Services; using Lottery3D.Business.Contracts; namespace Lottery3D.Business.Tests; public class LotteryServiceTests { [Fact] public void EvaluateBet_ReturnsStraightWin_WhenNumbersMatchExactly() { // Arrange var service new LotteryService(null, null); // 临时绕过依赖因测试不涉及 DAL // Act var result service.EvaluateBet(123, 123); // Assert Assert.True(result.IsWinning); Assert.Equal(1, result.PrizeLevel); Assert.Equal(直选, result.WinningRule); Assert.Equal(1040.0m, result.PrizeAmount); } [Fact] public void EvaluateBet_ReturnsGroupThree_WhenTwoDigitsSame() { var service new LotteryService(null, null); var result service.EvaluateBet(112, 211); // 组三112 vs 211 → 排序后都是112 Assert.True(result.IsWinning); Assert.Equal(2, result.PrizeLevel); Assert.Equal(组三, result.WinningRule); } [Fact] public void EvaluateBet_ReturnsGroupSix_WhenAllDigitsDifferent() { var service new LotteryService(null, null); var result service.EvaluateBet(123, 321); // 组六123 vs 321 → 排序后都是123 Assert.True(result.IsWinning); Assert.Equal(3, result.PrizeLevel); Assert.Equal(组六, result.WinningRule); } [Fact] public void EvaluateBet_ReturnsNoWin_WhenNoMatch() { var service new LotteryService(null, null); var result service.EvaluateBet(123, 456); Assert.False(result.IsWinning); Assert.Equal(0, result.PrizeLevel); Assert.Equal(未中奖, result.WinningRule); } }说明这里故意传入null作为仓储依赖因为测试目标是纯业务逻辑而非数据库读写。LotteryService的构造函数允许null需在 BLL 项目中将仓储参数改为可空引用类型或添加空检查这是测试友好的设计体现。运行这些测试你会看到绿色对勾——这意味着“3D规则”从此有了机器可验证的“后悔药”哪怕未来有人改了EvaluateBet的内部实现只要测试不红规则就依然正确。6.3 测试驱动开发TDD的实践建议如果你正带学生或带新人强烈建议反向操作先写测试再写实现。例如让学生先看到EvaluateBet_ReturnsGroupThree_WhenTwoDigitsSame这个测试名然后问“要让这个测试通过EvaluateBet方法里至少要写哪几行代码”——答案自然引向Distinct().Count() 2和字符排序比对。这种“以终为始”的方式比直接讲“组三定义”更能建立深刻认知。我带过的某高校实训班要求学生提交代码前必须保证dotnet test全部通过。结果发现他们对“三位数字排列组合”的理解深度远超只做 UI 界面拖拽的学生。技术成长的分水岭往往就藏在“是否愿意为一行业务逻辑写三行测试”这个选择里。希望帮到你。本文还有配套的精品资源点击获取