C#酒店进销存系统实战:数据模型、事务处理与并发防超卖

发布时间:2026/9/14 14:51:16
C#酒店进销存系统实战:数据模型、事务处理与并发防超卖
简介这是一套基于C#的酒店进销存管理系统完整源码包覆盖采购、付款收款、仓库分店管理、商品入库与销售等核心模块既适合计算机专业学生作为毕业设计参考也适合C#开发人员学习业务与代码实现还可供中小型酒店、餐饮企业用作项目原型。压缩包共954个文件大小约5.47MB主体为aspx页面、cs后台逻辑代码、gif/jpg界面截图与操作演示图以及mdf/ldf/db数据库文件并附有css/js前端脚本、项目解决方案和少量文档说明目录结构清晰便于按模块查看和二次开发。目前已有129人学习下载尤其适合需要可直接运行的完整项目用于论文支撑或功能扩展的读者。通过源码可掌握酒店进销存中商品流转、账款登记、库存变动等场景的编码思路所附图片与资料也有助于毕业设计答辩、项目汇报和文档撰写时快速梳理流程。1. 酒店进销存系统先分清「进、销、存」三个动作再动代码拿到一个「酒店进销存系统.zip」的时候很多人第一反应是解压后找.sln双击运行。真正决定这套 C# 项目能不能跑起来、跑起来能不能用的不是编译而是对「酒店」和「进销存」的理解。酒店不像普通零售店只有一个仓库客房部管一次性耗材餐饮部管食材保质期工程部管维修配件规则不同往往共用一张库存表。进销存三字对应三个独立动作进货、销售、存货每个动作都要留下可追溯的单据和流水。这篇内容沿这条线把这类 C# 进销存项目从数据模型、运行配置、核心编码到边界处理讲清楚适合刚入手源码包准备改造的初级开发也适合要接手维护或做二次开发的工程师。2. 酒店进销存系统的数据模型与库存字段设计2.1 核心表结构入库单、出库单与库存表的关联方式进销存系统的数据模型核心不是「库存表」本身而是单据。一套能通过月底盘点账实相符检查的系统至少包含这几张表Goods商品档案、Supplier供应商档案、PurchaseOrder采购入库主单、PurchaseDetail入库明细、OutStockOrder出库主单、OutStockDetail出库明细、Stock库存表、InventoryLog库存流水、StockCheck盘点单。这是一个典型的「单据—明细—库存—流水」四层结构。单据保存业务事实库存表保存当前结余流水记录每一次变动。设计上有一条纪律业务操作不直接改库存表而是先写流水再更新库存。这样任何数据对不上都能通过流水反推回去而不是对着一个孤立的库存数字猜。采购入库主单建议字段包括OrderNo单号、SupplierId、OrderDate、TotalAmount、Status草稿/已过账/已作废、CreateBy、CreateTime。明细表则用OrderId关联主单记录GoodsId、Quantity、UnitPrice、Amount。Status字段很重要很多教学项目漏掉它导致入库单录错了只能删数据而正规流程应该是「先保存成草稿审核后再过账」。2.1.1 为什么库存表不和单据放在一张表里有人问为什么不能直接在Goods表里加一个StockQuantity字段每次进出货直接UPDATE。这样做的后果是无法回答「这批货是什么时候进的」「上次盘点差异是多少」「哪个批次已经过期」。单据和库存分开后InventoryLog流水表负责记录时间和操作人Stock表只保留当前结余查询性能更好审计路径也完整。2.2 库存表必须有的字段与预警阈值设置库存表设计上最常见的错误是只有「数量」一个字段这对普通小商店够用对酒店不够。酒店需要应对部门领用、保质期预警、批次管理建议按下面的结构来。字段名类型说明StockIdint主键GoodsIdint商品档案外键BatchNonvarchar(50)批次号餐饮食材默认按入库日期生成Quantitydecimal(18,3)当前可用数量带 3 位小数处理按重量计价的食材SafetyStockdecimal(18,3)安全库存下限ExpireDatedatetime有效期客房和餐饮必备Locationnvarchar(20)库位编码例如 DRY-01、COLD-02LastInPricedecimal(18,2)最近一次入库单价其中Quantity用decimal(18,3)是故意的。酒店餐饮经常按公斤进货按克领用如果字段设计成int0.5 公斤的食材会被记成 0 或 1月底盘点差异会非常离谱。ExpireDate是酒店区别于普通进销存的特有字段食材过期不能销售这个字段直接支撑预警功能。预警不能只写在数据库层还需要在 C# 端做一个定时检查。常见做法是系统冷启动后或每日定时跑一次CheckStockWarning()把低于SafetyStock、距离ExpireDate小于 N 天的商品写入提醒表或直接在 WinForms 主界面弹一个消息列表。2.3 C# 实体类与建表脚本对照在 C# 端实体类一般按表一一对应属性名与字段名保持一致。下面这段代码是Stock表的实体类注意数值类型用decimal而不是doublepublic class Stock { public int StockId { get; set; } public int GoodsId { get; set; } public string BatchNo { get; set; } public decimal Quantity { get; set; } public decimal SafetyStock { get; set; } public DateTime? ExpireDate { get; set; } public string Location { get; set; } public decimal LastInPrice { get; set; } }对应的建表脚本通常放在项目的Database目录下文件名常见为InitDB.sql或hotel_erp.sql。脚本里除了建表还要给InventoryLog建组合索引否则单据量上来后按商品查流水会越来越慢CREATE NONCLUSTERED INDEX IX_InventoryLog_Goods ON InventoryLog(GoodsId, OperationType, CreateTime DESC);OperationType用整数表示操作类型1 入库、2 出库领用/销售、3 盘盈、4 盘亏、5 报损。后续用这个字段做流水汇总时一次GROUP BY OperationType就能看到全部出入库构成不需要在 C# 里根据中文名称做字符串匹配。3. 拿到酒店进销存系统 zip 压缩包后的运行准备3.1 解压后先看目录结构不要急着双击 .sln这类「源码资料打包下载」的压缩包解压后通常是一棵约定俗成的目录树HotelERP.sln解决方案文件HotelERP.Model实体层放上面讲到的 POCO 类HotelERP.DAL数据访问层放 SqlHelper 或 Entity Framework 的 DbContextHotelERP.BLL业务逻辑层放入库、出库、盘点等流程代码HotelERP.UI界面层WinForms 的窗体或 ASP.NET 页面Database/数据库脚本或 .mdf 文件使用说明.doc或README.txt记录版本、环境要求和部署步骤先花五分钟把目录结构和README扫一遍比直接打开.sln编译完报错再回头排查要省时间。三层结构里有一个容易踩的坑旧项目的目标框架是 .NET Framework 4.0 或 4.5当前开发机只装了 .NET 6/8 SDK双击.sln会提示「不受支持的格式」或「需要安装 .NET Framework 4.x」。这时先确认README里写的环境要求别急着换框架——老项目里可能用了DataAdapter、DataSet这类只在 .NET Framework 下表现正常的 API盲目升级工作量大且没必要。3.2 数据库脚本与连接字符串配置源码跑不起来十有八九是数据库没就位。先执行Database/InitDB.sql或者用 SQL Server Management Studio 附加Database/HotelERP.mdf。附加成功后确认登录账号有读写权限然后改连接字符串。WinForms 项目的连接字符串在App.config中Web 项目在Web.config中connectionStrings add nameHotelERP connectionStringData Source.;Initial CatalogHotelERP;User IDsa;Password123456;MultipleActiveResultSetstrue providerNameSystem.Data.SqlClient / /connectionStringsData Source.;表示本机 SQL Server 默认实例Initial Catalog是数据库名User ID和Password要按你本机的 SQL Server 登录账号改。MultipleActiveResultSetstrue允许同一连接上存在多个 DataReader在 WinForms 的列表刷新场景里能省掉很多「连接正被占用」的报错。如果本机装的是 SQL Server Express连接字符串要写成Data Source.\SQLEXPRESS这是最常见的启动失败原因。3.3 编译报错与运行问题的排查顺序编译报错集中在那几类缺少 NuGet 包引用、目标框架不一致、代码里用了旧 API。先在 Visual Studio 菜单里选「生成 → 重新生成解决方案」看「错误列表」窗口最前面的几个错误。如果报找不到SqlConnection说明缺System.Data.SqlClient包在 NuGet 包管理器里搜索安装即可如果报ConfigurationManager不存在需要在项目引用里加System.Configuration。提示很多老项目的.csproj里引用了本地packages文件夹的 DLL换机器后这些引用全部变黄。不要逐个手动加用 NuGet 还原功能批量处理更快。运行时报错按这个顺序排查程序目录下有没有log.txt或logs/目录 → Windows 事件查看器里 Application 日志的 .NET Runtime 异常 → SQL Server 错误日志。多数最终都指向连接字符串写错或登录名无权访问 HotelERP 库。排查时把异常信息里的InnerException展开重点看 Message 里Login failed for user还是Cannot open database——前者是账号问题后者是数据库名或实例名问题。4. 用 C# 实现进销存核心操作的代码路径4.1 采购入库先写明细分录再更新库存同一事务内完成采购入库是进销存里最典型的写操作。只更新Stock.Quantity单据作废时会查不到来源只写流水库存表会账实不符。正确路径是把「插入入库主单 插入入库明细 更新库存 写流水」放进同一个事务。下面是用 ADO.NET 配合TransactionScope的简化写法核心是保证多步写操作要么全部成功要么全部回滚using (var scope new TransactionScope()) { int orderId InsertPurchaseOrder(po); // 写采购主单返回自增ID foreach (var item in po.Items) { InsertPurchaseDetail(orderId, item); // 写采购明细 UpdateStockIn(item.GoodsId, item.BatchNo, item.Quantity, item.UnitPrice); WriteInventoryLog(item.GoodsId, 1, item.Quantity, po.OrderNo); // OperationType1 入库 } scope.Complete(); // 所有操作成功提交事务 }参数说明po是采购入库的业务对象po.Items是明细列表UpdateStockIn内部执行INSERT INTO Stock ...或UPDATE Stock SET Quantity Quantity qty判断依据是BatchNo是否已存在WriteInventoryLog的第三个参数是流水的OperationType入库固定传 1。scope.Complete()必须在所有步骤都成功执行后调用只要任何一个方法抛出异常事务自动回滚不会出现「单据有了库存没加」的脏数据。不要在 UI 层写这些逻辑。把事务放在 BLL 层UI 只调用一个purchaseService.CreateOrder(po)方法。这样后期换界面不用动业务代码写单元测试也方便。4.2 出库扣减用 UPDATE 条件判断实现原子扣减出库扣减比入库容易翻车。新手常犯的错误是先用SELECT查出Quantity在 C# 里判断够不够再执行UPDATE。这样操作在单用户环境没问题两个终端同时出库时会出现超卖——双方都读到库存充足后提交的一方把数量扣成负数。正确做法是在UPDATE语句里直接带数量条件让数据库负责校验UPDATE Stock SET Quantity Quantity - quantity WHERE GoodsId goodsId AND BatchNo batchNo AND Quantity quantity;如果ExecuteNonQuery()返回 0说明库存不足或批次不存在事务回滚界面提示「库存不足」。这段 SQL 是出库环节的原子操作参数goodsId、batchNo、quantity分别对应商品、批次和本次扣减数量不需要应用层加锁。要注意的是 C# 端在调用时必须把OrderNo也传进WriteInventoryLog否则流水表里查不到这笔出库对应的单据。4.3 盘点、报损与库存流水的一致性盘点场景里盘点单记录实盘数系统根据「账存数 − 实盘数」自动生成盘盈或盘亏流水再更新库存decimal diff bookQty - realQty; // 正数为盘亏 if (diff ! 0) { WriteInventoryLog(goodsId, diff 0 ? 4 : 3, Math.Abs(diff), checkNo); UpdateStockQty(goodsId, realQty); }diff 0表示账多实少走盘亏流水类型为 4diff 0表示盘盈流水类型为 3。checkNo是盘点单号通过它可以追溯到某次盘点的误差明细。报损单走类似路径OperationType为 5同时要记录报损原因、申请人和审批人形成采购入库到报损出库的闭环。每次盘点结束后把差异清单导出给财务复核而不是直接改库存表数据。5. 并发下单、数据归档与上线前自我检查5.1 并发场景下的库存防超卖酒店前台的迷你吧销售、餐厅食材领用高峰期并发量不低。防超卖的核心原则是「把扣减逻辑下推到数据库」不要在应用层做先查再扣。除了 4.2 里的条件UPDATE另一个方案是在存储过程或查询里加锁提示SELECT Quantity FROM Stock WITH (UPDLOCK, ROWLOCK) WHERE GoodsId goodsId; -- 业务侧判断足够后执行 UPDATEUPDLOCK让 SQL Server 在 SELECT 阶段就锁定该行阻止其他事务同时读到相同数量ROWLOCK控制锁粒度在单行避免锁扩散到整张库存表。这个写法适合教学项目学习和改造生产环境更推荐把整个出库流程封装成存储过程由数据库统一保证一致性C# 端只负责传参和接收结果。5.2 月末结账与流水归档酒店通常按自然月结账。结账后把已完结单据从主流水表复制到归档表例如InventoryLog_2024_01再删除原表中三个月前的数据。归档前必须确认当月所有盘点差异已处理否则库存结余和财务对不上结算单又已锁定查账会非常被动。归档代码常用批次迁移单表流水超过 50 万行后按CreateTime分批DELETE避免单次长事务锁表过久。5.3 上线前的验证清单系统上线前建议人工跑一遍下面五项检查结果记录在部署文档里新建一张采购入库单确认库存增加、流水出现OperationType1的记录再做一张出库单确认库存减少且不会扣成负数做一次盘点确认盘盈盘亏金额计算正确用两个终端同时扣减同一商品确认后提交的一方不会覆盖先提交的一方最后检查系统登录账号的数据库权限正式环境只授予db_datareader和db_datawriter不授予db_owner把 SQL 注入和误删表的损失降到最低。最后确认日切时间的设置。酒店的营业日和自然日不同步凌晨 0 点到 6 点产生的销售记录通常归属前一天。日切时间存放在系统配置表SysConfig的DayCutOffTime字段中默认值建议 04:00修改后重启服务生效。这个细节直接影响营收报表的统计口径验收时务必单独测一遍跨日订单的归属日期。本文还有配套的精品资源点击获取