ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C#课程设计实战:WinForm食堂订餐系统从数据库到打包部署

C#课程设计实战:WinForm食堂订餐系统从数据库到打包部署 简介这份C#课程设计学校食堂订餐系统文档面向高校计算机相关专业学生与课程设计开发者提供一套完整的C/S架构订餐系统实现方案。系统采用Socket通信与多线程、多任务设计客户端支持浏览菜品、点菜订餐及评分、管理个人信息、投诉留言服务器端则涵盖顾客、菜单、订单与管理员信息管理并配有需求分析、系统设计、数据库设计与测试评估等完整章节。资源包内共1个doc文件约795KB内容以系统设计说明与实现文档为主结构清晰便于按模块查阅。目前已有794人学习下载适合需要完成课程设计、参考C#与ASP.NET网络编程、Socket通信及模块化开发思路的读者可据此快速理解订餐系统的整体架构与功能划分并借鉴其数据库设计与业务流程组织方式。1. 食堂订餐系统从课程设计到能跑起来的 WinForm 实战每年一到学期中段做 C# 课程设计的同学就开始扎堆找参考。食堂订餐系统这个题目出现频率极高原因也简单业务场景大家都熟需求边界清晰数据库表不多界面用 WinForm 拖一拖就能出效果。但真正动手写的时候翻车点往往不在业务逻辑而在环境配置、数据绑定、并发扣库存这些看起来不起眼的地方。这篇笔记面向正在做 C# 课程设计、需要交出一个能演示、能答辩、代码结构说得清楚的食堂订餐系统的同学。我会按实际开发顺序把数据库设计、WinForm 界面搭建、订单流程、库存并发、打包部署这几个环节拆开讲每一步给出可复现的代码和参数说明。读完你至少能拿到一个结构完整、能跑通下单流程、知道哪里容易出问题的实现方案。2. 数据库与实体层先定表结构再写代码2.1 食堂订餐系统的四张核心表怎么设计食堂订餐系统的业务链条其实很短用户选菜、下单、扣库存、查订单。落到数据库上四张表基本够用——用户表、菜品表、订单主表、订单明细表。很多同学一上来就打开 Visual Studio 拖控件结果写到一半发现字段不够、关联对不上回头改表结构界面绑定全部重来。血泪经验是先把表定死再动界面。用户表存账号、密码哈希、角色学生/管理员、余额。菜品表存菜名、单价、库存、分类、图片路径、上架状态。订单主表存订单号、用户 ID、下单时间、总金额、订单状态。订单明细表存订单号、菜品 ID、数量、单价快照。这里有个容易忽略的点明细表里的单价要单独存一份不能只靠菜品表的当前单价去关联查询。因为菜品调价之后历史订单的金额必须保持不变这是订单类系统的基本要求。CREATE TABLE Users ( UserId INT IDENTITY(1,1) PRIMARY KEY, UserName NVARCHAR(50) NOT NULL UNIQUE, PasswordHash NVARCHAR(128) NOT NULL, Role NVARCHAR(20) NOT NULL DEFAULT Student, Balance DECIMAL(10,2) NOT NULL DEFAULT 0 ); CREATE TABLE Dishes ( DishId INT IDENTITY(1,1) PRIMARY KEY, DishName NVARCHAR(100) NOT NULL, Price DECIMAL(10,2) NOT NULL, Stock INT NOT NULL DEFAULT 0, Category NVARCHAR(50) NULL, ImagePath NVARCHAR(200) NULL, IsActive BIT NOT NULL DEFAULT 1 ); CREATE TABLE Orders ( OrderId INT IDENTITY(1,1) PRIMARY KEY, OrderNo NVARCHAR(32) NOT NULL UNIQUE, UserId INT NOT NULL, OrderTime DATETIME NOT NULL DEFAULT GETDATE(), TotalAmount DECIMAL(10,2) NOT NULL, Status NVARCHAR(20) NOT NULL DEFAULT Pending ); CREATE TABLE OrderItems ( ItemId INT IDENTITY(1,1) PRIMARY KEY, OrderId INT NOT NULL, DishId INT NOT NULL, Quantity INT NOT NULL, UnitPrice DECIMAL(10,2) NOT NULL );上面这段建表脚本可以直接在 SQL Server 里执行。几个参数需要说明PasswordHash给 128 长度是为了容纳 SHA256 加盐后的十六进制字符串OrderNo用 32 位字符串方便放时间戳加随机数Status默认Pending后续流转到Paid、Completed、Cancelled。UnitPrice放在明细表里就是前面说的价格快照答辩时老师问起来这是加分项。2.2 用 Dapper 还是 EF Core课程设计的选型理由课程设计里数据访问层用 Entity Framework Core 还是 Dapper取决于你想展示什么能力。EF Core 的优势是代码量少、迁移方便、LINQ 写起来顺手适合表关系复杂、需要快速出活的场景。Dapper 的优势是 SQL 可控、性能直观、学习成本低适合表不多但你想把 SQL 写清楚的场景。食堂订餐系统四张表我一般推荐 Dapper原因是课程设计答辩时老师经常会问“这条查询对应什么 SQL”用 Dapper 你能直接把 SQL 亮出来逻辑透明。public class DishRepository { private readonly string _connStr; public DishRepository(string connStr) { _connStr connStr; } public ListDish GetActiveDishes() { using (var conn new SqlConnection(_connStr)) { // 只查上架菜品按分类和名称排序 string sql SELECT DishId, DishName, Price, Stock, Category, ImagePath FROM Dishes WHERE IsActive 1 ORDER BY Category, DishName; return conn.QueryDish(sql).ToList(); } } public int ReduceStock(int dishId, int quantity) { using (var conn new SqlConnection(_connStr)) { // 库存充足才扣减返回受影响行数 string sql UPDATE Dishes SET Stock Stock - Qty WHERE DishId DishId AND Stock Qty; return conn.Execute(sql, new { Qty quantity, DishId dishId }); } } }ReduceStock这个方法值得多说两句。它的 WHERE 条件里带了Stock Qty这是防止超卖的第一道防线。如果返回值为 0说明库存不够或者菜品不存在调用方必须处理这个失败情况。很多同学写扣库存直接UPDATE Dishes SET Stock Stock - Qty WHERE DishId DishId并发一上来库存就变负数答辩演示时两个人同时点同一道菜就露馅了。3. WinForm 界面搭建从登录到下单的完整流程3.1 主窗体布局与控件选型WinForm 做订餐系统界面不需要多华丽但要做到分区清晰、操作路径短。我一般把主窗体分成三块顶部是用户信息和余额左侧是菜品分类树或下拉框中间是菜品列表底部是购物车和结算按钮。菜品列表用FlowLayoutPanel动态生成菜品卡片每个卡片是一个UserControl包含图片、名称、价格、库存和一个“加入购物车”按钮。这样比DataGridView更直观演示效果也好。private void LoadDishes(ListDish dishes) { flowDishes.Controls.Clear(); foreach (var dish in dishes) { var card new DishCard(); card.SetData(dish); card.AddToCart (s, e) { // 加入购物车逻辑检查库存 if (dish.Stock 0) { MessageBox.Show(${dish.DishName} 已售罄); return; } _cart.AddOrUpdate(dish); RefreshCartUI(); }; flowDishes.Controls.Add(card); } }这段代码里flowDishes是FlowLayoutPanelDishCard是自定义控件。AddToCart是一个自定义事件把“加入购物车”的动作从卡片内部抛给主窗体处理这样卡片只负责展示业务逻辑集中在主窗体职责清晰。_cart可以用Dictionaryint, CartItem实现key 是 DishIdvalue 包含菜品信息和数量。3.2 购物车与订单提交的事务处理购物车到订单提交这一步是课程设计里最容易出 bug 的地方。核心要求是扣库存、写订单主表、写订单明细表这三件事要么全成功要么全失败。用TransactionScope或者SqlTransaction都可以我一般用SqlTransaction因为控制粒度更细也方便在答辩时讲清楚事务边界。public bool SubmitOrder(int userId, ListCartItem items, out string orderNo) { orderNo GenerateOrderNo(); using (var conn new SqlConnection(_connStr)) { conn.Open(); using (var tran conn.BeginTransaction()) { try { decimal total 0; foreach (var item in items) { // 扣库存失败则回滚 string updateSql UPDATE Dishes SET Stock Stock - Qty WHERE DishId DishId AND Stock Qty; int rows conn.Execute(updateSql, new { Qty item.Quantity, DishId item.DishId }, tran); if (rows 0) { tran.Rollback(); return false; } total item.UnitPrice * item.Quantity; } // 写订单主表 string orderSql INSERT INTO Orders (OrderNo, UserId, TotalAmount, Status) VALUES (OrderNo, UserId, Total, Pending); SELECT CAST(SCOPE_IDENTITY() as int);; int orderId conn.QuerySingleint(orderSql, new { OrderNo orderNo, UserId userId, Total total }, tran); // 写订单明细 string itemSql INSERT INTO OrderItems (OrderId, DishId, Quantity, UnitPrice) VALUES (OrderId, DishId, Qty, Price); foreach (var item in items) { conn.Execute(itemSql, new { OrderId orderId, DishId item.DishId, Qty item.Quantity, Price item.UnitPrice }, tran); } tran.Commit(); return true; } catch { tran.Rollback(); throw; } } } }这段代码的关键点有三个。第一扣库存和写订单在同一个事务里任何一步失败都回滚。第二SCOPE_IDENTITY()拿到刚插入的订单自增 ID用于关联明细。第三GenerateOrderNo()生成订单号建议用DateTime.Now.ToString(yyyyMMddHHmmss) 随机四位数保证唯一性。注意conn.Execute和conn.QuerySingle都传了tran参数这是 Dapper 的事务支持方式漏传的话事务不生效这是常见翻车点。4. 避坑与排查课程设计里最容易翻车的五个地方4.1 现象程序在自己电脑能跑换台电脑就报数据库连接失败原因通常是连接字符串写死了Data Source.;或者Data Sourcelocalhost而答辩教室的机器没装 SQL Server 或者实例名不同。解决方式是把连接字符串放到App.config里并且用.\SQLEXPRESS这种更通用的实例名。如果实在不放心可以改用 LocalDBVisual Studio 自带不需要额外安装。connectionStrings add nameFoodOrderDb connectionStringData Source(LocalDB)\MSSQLLocalDB;AttachDbFilename|DataDirectory|\FoodOrder.mdf;Integrated SecurityTrue providerNameSystem.Data.SqlClient / /connectionStrings4.2 现象点击下单按钮界面卡死几秒才响应原因是数据库操作放在 UI 线程里同步执行网络或磁盘稍慢就会阻塞消息循环。解决方式是用async/await把数据库调用异步化或者至少用Task.Run包一层。WinForm 里更新控件要注意回到 UI 线程用Invoke或者async方法里直接更新await之后默认回到 UI 上下文。4.3 现象两个人同时点最后一份菜库存变成负数原因就是前面说的扣库存 SQL 没加Stock Qty条件或者加了条件但没检查返回值。解决方式是扣库存必须检查受影响行数返回 0 就提示“库存不足”并回滚事务。这个点在答辩时如果被问到并发能答上来是很加分的。4.4 现象订单列表显示的总金额和明细对不上原因是订单主表的TotalAmount是在插入主表时算的但明细表的UnitPrice可能取了菜品当前价格而不是下单时的价格。解决方式是下单时先把菜品价格快照到CartItem里算总价和写明细都用这个快照价不要再去查Dishes表。4.5 现象发布后的程序在别人电脑上提示缺少 DLL原因是用了第三方库比如 Dapper、Newtonsoft.Json但发布时没有把依赖一起打包。解决方式是用 Costura.Fody 把 DLL 嵌入 exe或者在发布配置里选择“独立部署”。课程设计答辩通常要求能直接双击运行Costura.Fody 是最省事的方案装个 NuGet 包编译出来就是一个 exe。5. 打包部署与答辩演示的实用技巧5.1 用 Costura.Fody 把依赖压成一个 exe课程设计答辩现场最尴尬的事情是程序在自己电脑上跑得好好的拷到答辩教室的机器上双击就报错。除了数据库连接问题另一个常见原因就是缺少依赖 DLL。Costura.Fody 解决这个问题的方式很直接——在编译阶段把引用的 DLL 嵌入到主程序集里运行时自动加载。# 在 NuGet 包管理器控制台执行 Install-Package Costura.Fody装完之后不需要改任何代码直接重新编译。编译输出目录里会多出一个Costura文件夹里面是压缩后的依赖。最终bin\Release下的 exe 可以单独拷走运行。注意一点如果用了 LocalDB目标机器仍然需要安装 SQL Server LocalDB 运行时这个没法嵌进去。所以更稳妥的方案是把数据库脚本导出成.sql文件答辩前在目标机器上执行一遍建库建表。5.2 答辩演示的脚本化流程答辩演示最怕现场翻车所以我会提前把演示流程写成一个 checklist每一步都手动走一遍。流程大概是启动程序 → 用学生账号登录 → 浏览菜品 → 加两道菜到购物车 → 提交订单 → 查看订单列表 → 退出登录 → 用管理员账号登录 → 查看所有订单 → 修改订单状态 → 下架一个菜品 → 退出。整个过程控制在三分钟以内重点展示下单事务和库存扣减。演示前需要准备的数据也要提前灌好至少两个用户一个学生一个管理员、六到八个菜品、其中至少一个菜品库存设为 1 用来演示“最后一份”的场景。库存为 1 的菜品在演示时先下单成功再尝试下单就会提示库存不足这个对比效果很直观。5.3 代码结构怎么讲清楚答辩时老师通常会问“你的项目分了几层”。食堂订餐系统不需要过度设计但至少要能说出三层界面层WinForm 窗体、业务层订单提交、库存扣减、数据访问层Repository。如果时间充裕可以把业务层单独抽一个OrderService类把事务逻辑放在里面窗体只负责收集用户输入和展示结果。这样讲起来结构清晰代码也好维护。public class OrderService { private readonly DishRepository _dishRepo; private readonly OrderRepository _orderRepo; public OrderService(string connStr) { _dishRepo new DishRepository(connStr); _orderRepo new OrderRepository(connStr); } public OrderResult Submit(int userId, ListCartItem items) { // 参数校验 if (items null || items.Count 0) return OrderResult.Fail(购物车为空); // 调用仓储层执行事务 return _orderRepo.CreateOrder(userId, items); } }这个OrderService很薄但它把“校验”和“执行”分开了。窗体只调Submit拿到OrderResult之后根据成功或失败更新界面。这样如果老师问“业务逻辑在哪”你可以直接指到OrderService而不是在一堆按钮点击事件里翻找。5.4 一个容易被忽略的细节订单号生成订单号看起来简单但演示时如果两个人同时下单用时间戳生成订单号可能重复。我一般用时间戳 用户ID后两位 随机四位数重复概率极低。如果数据库OrderNo字段加了唯一约束重复插入会抛异常事务回滚用户看到“下单失败请重试”。这个异常处理也要写不然程序直接崩掉。private string GenerateOrderNo(int userId) { // 时间戳精确到秒 用户ID后两位 随机数 string timestamp DateTime.Now.ToString(yyyyMMddHHmmss); string userPart (userId % 100).ToString(D2); string randomPart new Random(Guid.NewGuid().GetHashCode()) .Next(1000, 9999).ToString(); return timestamp userPart randomPart; }用Guid.NewGuid().GetHashCode()做随机种子是为了避免同一秒内多次调用Random产生相同序列。这个细节很小但演示时如果连续快速下单能看出差别。5.5 最后说一个习惯我做课程设计类项目有一个习惯每完成一个功能模块就手动走一遍完整流程确认没有回归问题再继续写下一个模块。食堂订餐系统的模块耦合度不高但订单提交依赖库存扣减库存扣减依赖菜品数据任何一环出问题都会在下单时暴露。与其最后统一调试不如每步都验证。这个习惯让我在答辩前至少能保证主流程是通的剩下的时间用来打磨界面和准备问题。希望帮到你。本文还有配套的精品资源点击获取
返回列表