简介:这是一套基于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 库存表必须有的字段与预警阈值设置
库存表设计上最常见的错误是只有「数量」一个字段,这对普通小商店够用,对酒店不够。酒店需要应对部门领用、保质期预警、批次管理,建议按下面的结构来。
| 字段名 | 类型 | 说明 |
|---|---|---|
| StockId | int | 主键 |
| GoodsId | int | 商品档案外键 |
| BatchNo | nvarchar(50) | 批次号,餐饮食材默认按入库日期生成 |
| Quantity | decimal(18,3) | 当前可用数量,带 3 位小数处理按重量计价的食材 |
| SafetyStock | decimal(18,3) | 安全库存下限 |
| ExpireDate | datetime | 有效期,客房和餐饮必备 |
| Location | nvarchar(20) | 库位编码,例如 DRY-01、COLD-02 |
| LastInPrice | decimal(18,2) | 最近一次入库单价 |
其中Quantity用decimal(18,3)是故意的。酒店餐饮经常按公斤进货,按克领用,如果字段设计成int,0.5 公斤的食材会被记成 0 或 1,月底盘点差异会非常离谱。ExpireDate是酒店区别于普通进销存的特有字段,食材过期不能销售,这个字段直接支撑预警功能。
预警不能只写在数据库层,还需要在 C# 端做一个定时检查。常见做法是系统冷启动后或每日定时跑一次CheckStockWarning(),把低于SafetyStock、距离ExpireDate小于 N 天的商品写入提醒表,或直接在 WinForms 主界面弹一个消息列表。
2.3 C# 实体类与建表脚本对照
在 C# 端,实体类一般按表一一对应,属性名与字段名保持一致。下面这段代码是Stock表的实体类,注意数值类型用decimal而不是double:
public 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 name="HotelERP" connectionString="Data Source=.;Initial Catalog=HotelERP;User ID=sa;Password=123456;MultipleActiveResultSets=true" providerName="System.Data.SqlClient" /> </connectionStrings>Data Source=.;表示本机 SQL Server 默认实例;Initial Catalog是数据库名;User ID和Password要按你本机的 SQL Server 登录账号改。MultipleActiveResultSets=true允许同一连接上存在多个 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); // OperationType=1 入库 } 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表示账多实少,走盘亏,流水类型为 4;diff < 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 上线前的验证清单
系统上线前建议人工跑一遍下面五项检查,结果记录在部署文档里:新建一张采购入库单,确认库存增加、流水出现OperationType=1的记录;再做一张出库单,确认库存减少且不会扣成负数;做一次盘点,确认盘盈盘亏金额计算正确;用两个终端同时扣减同一商品,确认后提交的一方不会覆盖先提交的一方;最后检查系统登录账号的数据库权限,正式环境只授予db_datareader和db_datawriter,不授予db_owner,把 SQL 注入和误删表的损失降到最低。
最后确认日切时间的设置。酒店的营业日和自然日不同步,凌晨 0 点到 6 点产生的销售记录通常归属前一天。日切时间存放在系统配置表SysConfig的DayCutOffTime字段中,默认值建议 04:00,修改后重启服务生效。这个细节直接影响营收报表的统计口径,验收时务必单独测一遍跨日订单的归属日期。
本文还有配套的精品资源,点击获取