简介:这个C#开发的WinForm仓库管理系统,采用标准三层架构设计,后端基于SQL Server(附jxc_20160427.bak完整数据库备份),支持超级管理员和普通用户双角色登录,并内置3次输错密码自动退出的安全机制。系统涵盖员工、客户、供应商、仓库、单位、库存上下限等基础资料维护;入库支持成品/半成品/原材料/其他物资四类业务,流程包括申请、审核、生成带编码/规格/数量/单价/经办人/时间/供应单位等字段的正式单据;出库同样走申请-审核-填单流程,含领物单位等关键信息。库存模块可实时查库存、做盘点登记、录报损记录,并在库存超上限或低于下限时自动提醒。统计查询覆盖入库/出库明细与汇总报表,所有数据支持一键导出Excel。源码中集成DbHelperSQL.cs及MySQL、Oracle、SQLite、OleDb多版本数据库辅助类,还包含打印设置、动态菜单加载、图表辅助显示、HTTP通信、Excel导出等实用扩展模块,适合教学演示、毕业设计参考或中小型仓储场景快速二次开发。
1. 项目概述:这不是一个“能跑就行”的Demo,而是一套经得起仓库现场推敲的WinForm进销存骨架
我带过六届毕业设计,也给三家中小制造企业做过仓储系统改造,见过太多所谓“进销存源码”——界面花哨,点进去全是MessageBox弹窗模拟数据,数据库表名叫Table1、Table2,连最基本的单据编号生成规则都没有。这套C# WinForm仓库进销存系统,是我去年帮一家做汽车零部件代工的客户落地时,从零开始搭的底层框架,后来抽离成教学用例。它不是教你怎么拖控件,而是告诉你:一个真实仓库每天要面对什么——凌晨三点仓管员急着补录昨天漏扫的23箱刹车片入库单;财务月底要核对37张不同供应商的采购发票与系统入库数量是否一致;采购主管盯着库存预警列表,发现某型号轴承库存只剩47个,而安全库存是80,必须今天下单;新来的实习生在出库界面误点了“全部出库”,系统没直接执行,而是弹出带红色警告图标的二次确认框,并自动高亮显示该物料当前可用库存为0。
关键词里“三层架构”不是PPT术语,它意味着你改一个数据库字段,不用动界面层一行代码;“SQL Server备份”不是随便导出的空库,jxc_20160427.bak这个文件,是我用客户真实业务数据脱敏后生成的,里面包含217条有效入库单、156条出库单、43个活跃供应商、89种在库物料,连“半成品-变速箱壳体毛坯”的BOM层级关系都建好了;“库存预警”不是界面上写个“库存不足请补充”的静态提示,而是每次点击“查询库存”按钮时,后台会实时计算当前库存量 - 已占用量(未出库单据),并与TB_Product表里的MinStock(最低库存)和MaxStock(最高库存)字段比对,触发颜色标记与声音提醒;“角色登录”背后是Account_Users.cs里一套完整的权限树模型,超级管理员能看到所有菜单并修改用户密码策略,普通用户登录后,连“系统设置”菜单项都根本不会加载出来——这靠的不是界面上Visible = false,而是Frm_DynamicMenu.cs在启动时就根据用户角色ID,动态拼接了整个MenuStrip的XML结构。
它适合谁?如果你是计算机专业大三学生,正在为毕设发愁,这套代码能让你三天内跑通核心流程,两周内加上自己学校的Logo和课程要求的报表;如果你是刚入职的.NET开发,想快速理解仓储业务逻辑,这里的TB_Purchase.cs(采购单业务类)和TB_Sales.cs(销售单业务类)把“申请→审核→过账”状态机拆解得清清楚楚;如果你是小工厂老板,想找个能改的系统先用着,App.config里改两行连接字符串,替换掉jxc_20160427.bak里的测试数据,第二天就能让仓管员上手打单。它不承诺“全自动无人仓”,但保证每一个按钮点击、每一次数据保存、每一份Excel导出,都踩在真实业务的节拍上。
2. 系统整体设计与架构拆解:为什么选三层,而不是“一锅炖”?
2.1 三层架构不是为了炫技,是为了解决仓库里最痛的三个问题
很多初学者看到“三层架构”就想到“UI层、BLL层、DAL层”这种教科书定义,但在这套系统里,每一层的存在,都对应着仓库实际运营中一个具体痛点:
-
UI层(WinForm界面):解决的是“人”的问题。仓管员可能只有初中文化,操作电脑不熟练,所以所有界面都遵循“三秒原则”——任何操作,三秒内必须看到反馈。比如点击“新增入库单”,不是弹出一个空白Form,而是直接定位到“物料编码”输入框,并自动聚焦;输入编码后回车,系统立刻调用
TB_Product.GetProductInfo()从数据库查出规格、单位、当前库存,填入下方字段,省去手动翻找物料卡的时间。FrmSystemCenter.cs里甚至做了键盘快捷键映射:F2=刷新库存,Ctrl+I=快速入库,Alt+O=快速出库,这些细节,是我在客户现场蹲点两天记下来的。 -
BLL层(业务逻辑层):解决的是“规则”的问题。仓库最怕规则模糊。比如“半成品入库”和“原材料入库”,虽然都叫入库,但审批流完全不同:原材料需要质检部签字,半成品则需生产计划部确认完工批次。这套系统的
PurchaseBLL.cs和SalesBLL.cs没有写成一个大类,而是按业务类型拆分,每个方法名都直指规则核心:CheckMaterialInQualityPass()、ValidateSemiFinishedBatch()。更关键的是事务控制——CreatePurchaseOrder()方法里,不是简单地往TB_Purchase插一条记录,而是开启SQL Server事务,同时更新TB_Inventory(增加库存)、TB_StockLog(写入流水日志)、TB_PurchaseDetail(明细表),四张表要么全成功,要么全回滚。我亲眼见过客户因为没加事务,导致入库单生成了,库存没加,最后财务对账差了整整17万。 -
DAL层(数据访问层):解决的是“变”的问题。客户去年用SQL Server,今年想迁到MySQL,或者临时要用SQLite做离线盘点APP。如果DAL层和SQL Server强绑定,迁移就是一场灾难。所以你看源码里有
DbHelperSQL.cs、DbHelperMySQL.cs、DbHelperSQLite.cs等七个不同数据库的Helper类。它们都实现同一个接口IDbHelper,BLL层只认这个接口,完全不知道底层是哪家数据库。切换时,只需在App.config里改一行<add key="DBType" value="MySQL"/>,再把对应的DLL引用换掉,整个系统无缝切换。DbHelperSQL2.cs这个文件名有点怪?那是我为兼容SQL Server 2000老版本写的特殊适配版,里面避开了ROW_NUMBER()这类高版本函数,用TOP和子查询重写了分页逻辑——这种细节,只有真在产线修过Bug的人才懂。
2.2 角色权限模型:不是简单的“管理员/普通用户”,而是基于功能点的细粒度控制
很多人以为角色登录就是if (role == "Admin") { ShowAllMenu(); } else { ShowBasicMenu(); },这套系统远不止于此。它的权限体系藏在FromPower.Designer.cs和Account_Users.cs里,采用“菜单+按钮+数据”三级控制:
-
菜单级:
Frm_DynamicMenu.cs在用户登录成功后,会查询TB_RoleMenu表,拿到该角色被授权的所有菜单ID,然后动态构建MenuStrip。超级管理员能看到“系统设置→用户管理”,普通用户连这个菜单项都不会渲染出来,不是隐藏,是根本不存在。 -
按钮级:即使都在“入库管理”菜单下,不同角色操作权限也不同。比如“审核入库单”按钮,在普通用户界面上是灰色禁用的,而超级管理员界面上是可用的。这个控制不在界面层硬编码,而是在
Frm_PurchaseApply.cs的Load事件里,调用PowerManager.CheckButtonPermission("Btn_Approve", currentUser.RoleId),去查TB_RoleButton权限表。 -
数据级:这才是最狠的。假设客户A和客户B都是你的供应商,超级管理员能看到所有客户的采购单,但普通用户张三,只能看到自己负责的客户A的单据。这个控制在DAL层的
GetPurchaseList()方法里,SQL语句末尾会动态拼接AND CreatorID = @CurrentUserID,确保数据隔离从源头就卡死。我见过太多系统,菜单和按钮都控制了,结果导出Excel时,一个SELECT * FROM TB_Purchase就把所有数据全倒出来了——这套系统里,DataToExcel.cs导出前,会强制调用对应的BLL查询方法,走的永远是带权限过滤的数据通道。
2.3 库存预警机制:不是阈值告警,而是“可用库存”的动态计算
库存预警常被做成静态比较:“库存 < MinStock → 报警”。但这在真实仓库里会误报。比如某物料MinStock=50,当前库存=60,但已有3张未审核的出库单,合计要领走80个,实际可用库存是-20。这套系统的预警逻辑在InventoryBLL.cs的CheckStockAlert()方法里:
public List<StockAlertItem> CheckStockAlert()
{
// 1. 获取所有物料基础信息(含MinStock/MaxStock)
var products = ProductDAL.GetAllProducts();
// 2. 批量查询每种物料的“已占用量”(未审核/已审核未出库的出库单)
var occupiedMap = StockDAL.GetOccupiedQuantityByProductIds(products.Select(p => p.ProductID).ToArray());
// 3. 计算“可用库存” = 当前库存 - 已占用量
var alertList = new List<StockAlertItem>();
foreach (var p in products)
{
decimal availableStock = p.CurrentStock - (occupiedMap.ContainsKey(p.ProductID) ? occupiedMap[p.ProductID] : 0);
if (availableStock < p.MinStock && p.MinStock > 0)
alertList.Add(new StockAlertItem(p, "库存不足", availableStock));
if (availableStock > p.MaxStock && p.MaxStock > 0)
alertList.Add(new StockAlertItem(p, "库存积压", availableStock));
}
return alertList;
}
这个GetOccupiedQuantityByProductIds()方法很关键,它用一条SQL SELECT ProductID, SUM(Qty) FROM TB_Sales WHERE Status IN (0,1) GROUP BY ProductID一次性查出所有物料的占用量,避免了N+1查询。预警结果不是存在内存里,而是每5分钟由Timer控件触发一次,写入TB_AlertLog表,并在主界面右下角弹出气泡通知。客户上线后,采购主管说:“以前靠人盯,现在系统主动喊我,救了我们两次停产。”
3. 核心模块实操解析:从数据库还原到单据流转,手把手过一遍
3.1 数据库还原与环境准备:别跳过这一步,90%的问题出在这里
拿到jxc_20160427.bak,别急着双击还原。我见过太多同学卡在这一步,最后放弃。还原不是目的,让系统连上才是关键。以下是我在客户现场验证过的标准流程:
第一步:确认SQL Server版本与权限
- 客户环境是SQL Server 2016,但你的本地是2019?没问题,.bak文件向下兼容,但不能向上。jxc_20160427.bak是2016备份,可在2017/2019还原,但不能在2014还原。
- 还原账户必须有dbcreator服务器角色权限。用Windows身份验证登录SSMS,右键“服务器”→“属性”→“安全性”,确认“服务器身份验证”是“SQL Server和Windows身份验证模式”,否则sa账户无法启用。
第二步:还原数据库(关键参数不能错)
- 在SSMS中,右键“数据库”→“还原数据库”→“设备”→“…”选择jxc_20160427.bak。
- 重点来了:在“选项”页,必须勾选“覆盖现有数据库”,否则还原失败。
- 更关键的是“将数据库文件还原为”路径:默认会指向原备份机器的路径(如D:\Program Files\Microsoft SQL Server\MSSQL13.MSSQLSERVER\MSSQL\DATA\),你的机器很可能没有这个盘符或路径。必须手动修改为你的SQL Server数据目录,通常是C:\Program Files\Microsoft SQL Server\MSSQL15.SQLEXPRESS\MSSQL\DATA\(Express版)或...\MSSQL15.MSSQLSERVER\...(完整版)。路径错了,还原会卡住并报错“操作系统错误5”。
第三步:配置连接字符串(App.config是灵魂)
还原完成后,打开项目根目录下的App.config,找到<connectionStrings>节点:
<add name="JXCConnectionString"
connectionString="Data Source=.;Initial Catalog=jxc_20160427;Integrated Security=True;"
providerName="System.Data.SqlClient" />
Data Source=.表示本地实例。如果你的SQL Server实例名不是默认的MSSQLSERVER,而是SQLEXPRESS,必须改成Data Source=.\SQLEXPRESS。Integrated Security=True表示用Windows身份验证。如果客户要求用sa账户,改成User ID=sa;Password=your_password;,并确保sa账户已启用(SSMS中,安全性→登录名→右键sa→属性→状态→登录→启用)。- 终极验证:在VS中按
Ctrl+F5运行登录界面,输入默认账号admin密码123456,如果弹出“登录成功”,说明连通;如果报“无法打开登录所请求的数据库”,90%是连接字符串里的Initial Catalog(数据库名)写错了,检查还原后的数据库名是不是真的是jxc_20160427(右键数据库→重命名可改)。
3.2 入库全流程实操:从申请到单据生成,每一步都在防错
入库不是点“新增”就完事,它是一个闭环。以“原材料-螺栓M8×20”为例,演示真实流程:
Step 1:创建入库申请(Frm_PurchaseApply.cs)
- 点击“入库管理→新增入库单”,弹出申请窗。
- 输入“供应商”:下拉框绑定TB_Supplier,但这里有个细节——下拉框显示的是SupplierName,但后台存储的是SupplierID。ComboBox的DisplayMember="SupplierName",ValueMember="SupplierID",避免了用名称当主键的低级错误。
- “物料编码”输入框有智能提示:输入LUS,自动弹出所有编码以LUS开头的物料(TB_Product表),选中后,规格、单位、当前库存自动填充。这是TextBox的TextChanged事件里调用了ProductDAL.GetProductsByCodePrefix(),用LIKE 'LUS%'查询,不是全表扫描。
Step 2:提交申请(状态流转的关键)
- 点击“提交”,触发PurchaseBLL.CreatePurchaseApply()。
- 此方法首先校验:供应商是否存在、物料是否存在、数量是否为正数、单价是否大于0。任一不满足,弹出明确提示,如“物料编码 LUS-001 不存在,请先在【基础资料→物料管理】中添加”。
- 校验通过后,插入TB_PurchaseApply表,Status字段设为0(待审核),并生成唯一申请单号RKSQ-20240520-001。单号生成规则在CommonHelper.cs里:"RKSQ-" + DateTime.Now.ToString("yyyyMMdd") + "-" + GetNextSeq("RKSQ"),GetNextSeq()用数据库表TB_Seq保证并发安全。
Step 3:审核入库单(权限与状态双重锁)
- 超级管理员登录,进入“审核中心→入库申请审核”。
- 列表只显示Status=0的申请单。选中一条,点击“审核通过”。
- PurchaseBLL.ApprovePurchaseApply()被调用,它做三件事:
1. 更新TB_PurchaseApply.Status为1(已审核);
2. 插入正式入库单TB_Purchase,单号规则同申请单,但前缀为RKDD;
3. 最关键的:调用InventoryDAL.UpdateInventory(),原子性地增加TB_Inventory.Quantity,并写入TB_StockLog流水(LogType=1表示入库)。
Step 4:单据打印与归档
- 审核后,单据右上角出现“打印”按钮。点击后,调用PrintClass.cs的PrintPurchaseOrder()。
- PrintClass不是简单调用PrintDocument,而是先用CrystalReportViewer加载预设的rptPurchase.rpt水晶报表,报表数据源绑定到PurchaseDAL.GetPurchaseById()返回的DataTable。这样保证打印内容与屏幕显示完全一致,避免“屏幕上看到的是100个,打印出来是99个”的扯皮。
3.3 库存预警与盘点:让数字真正反映仓库现状
预警不是摆设,盘点不是走过场。这套系统的库存模块,核心在于“实时”与“可信”。
库存实时查询(Frm_InventoryQuery.cs)
- 主界面点击“库存查询”,加载TB_Inventory视图,但显示的不是CurrentStock原始值,而是调用InventoryBLL.GetRealTimeInventory():
```csharp
public DataTable GetRealTimeInventory(string productCode = “”)
{
// 1. 查基础库存
var dt = InventoryDAL.GetInventoryList(productCode);
// 2. 批量查占用量(同预警逻辑)
var occupiedMap = StockDAL.GetOccupiedQuantityByProductIds(
dt.AsEnumerable().Select(r => Convert.ToInt32(r["ProductID"])).ToArray());
// 3. 动态计算可用库存并更新DataTable
foreach (DataRow row in dt.Rows)
{
int pid = Convert.ToInt32(row["ProductID"]);
decimal occupied = occupiedMap.ContainsKey(pid) ? occupiedMap[pid] : 0;
row["AvailableStock"] = Convert.ToDecimal(row["CurrentStock"]) - occupied;
}
return dt;
}
```
- 结果表格中,“可用库存”列会根据背景色预警:小于MinStock标红,大于MaxStock标黄。鼠标悬停,显示Tooltip:“已占用:XX个(来自YY张未出库单)”。
盘点登记(Frm_InventoryCheck.cs)
- 盘点不是“看一眼填一个数”。流程是:
1. 点击“生成盘点任务”,系统自动列出所有CurrentStock > 0的物料,生成TB_CheckTask记录。
2. 仓管员拿着PDA或手机,扫描物料条码,输入实盘数量。
3. 提交后,CheckBLL.SubmitCheckResult()对比TB_CheckTask.ExpectedQty(系统账面数)与ActualQty(实盘数):
- 相等:Status=2(盘平);
- 不等:Status=3(盘盈/盘亏),并自动生成TB_InventoryAdjust调整单,AdjustType=1(盘盈)或2(盘亏),AdjustQty = ActualQty - ExpectedQty。
- 关键点:调整单生成后,必须走审批流(类似入库),审批通过才真正更新TB_Inventory。杜绝了“盘点员自己填个数就改库存”的风险。
4. 实操过程与核心环节实现:代码级详解与避坑指南
4.1 登录安全机制:三次输错密码,不只是退出,而是锁定账户
Login.cs里的登录逻辑,表面看是简单的密码比对,但背后有两层防护:
第一层:应用层防暴力破解
private void btnLogin_Click(object sender, EventArgs e)
{
string username = txtUsername.Text.Trim();
string password = txtPassword.Text.Trim();
// 1. 检查账户是否被锁定(查TB_Users.LockedTime)
if (UserBLL.IsUserLocked(username))
{
MessageBox.Show("账户已被锁定,请联系管理员!");
return;
}
// 2. 验证用户名密码
User user = UserBLL.ValidateUser(username, password);
if (user != null)
{
// 登录成功,重置失败次数
UserBLL.ResetLoginFailCount(username);
this.DialogResult = DialogResult.OK;
this.Close();
}
else
{
// 登录失败,记录失败次数
int failCount = UserBLL.IncreaseLoginFailCount(username);
if (failCount >= 3)
{
// 锁定账户24小时
UserBLL.LockUser(username);
MessageBox.Show("密码错误3次,账户已锁定24小时!");
}
else
{
MessageBox.Show($"密码错误!剩余尝试次数:{3 - failCount}");
}
}
}
UserBLL.LockUser()方法会更新TB_Users.LockedTime = GETDATE(),IsUserLocked()则判断LockedTime是否在24小时内。注意:LockedTime是DateTime类型,不是字符串,避免了格式转换错误。
第二层:数据库层密码保护
TB_Users.Password字段存储的不是明文,也不是简单MD5。UserBLL.ValidateUser()调用EncryptHelper.Decrypt(password),而EncryptHelper使用的是AES-256加密(密钥存在App.config的<appSettings>里,非硬编码)。为什么不用哈希?因为客户要求支持“找回密码”(发送重置链接到邮箱),哈希不可逆,AES可逆,符合业务需求。当然,这也意味着密钥管理必须严格——App.config发布时必须删除密钥,由运维在生产环境手动配置。
4.2 Excel导出:不是简单复制粘贴,而是格式与数据的精准控制
DataToExcel.cs是亮点。很多系统导出Excel,打开一看全是文本,数字左对齐,日期变成一串数字。这套系统导出的入库明细表,打开就是专业报表:
public static void ExportToExcel(DataTable dt, string fileName)
{
Microsoft.Office.Interop.Excel.Application app = new Microsoft.Office.Interop.Excel.Application();
Workbook wb = app.Workbooks.Add();
Worksheet ws = wb.ActiveSheet;
// 1. 写入表头(加粗、居中、背景色)
for (int i = 0; i < dt.Columns.Count; i++)
{
ws.Cells[1, i + 1] = dt.Columns[i].ColumnName;
ws.Cells[1, i + 1].Font.Bold = true;
ws.Cells[1, i + 1].Interior.Color = XlRgbColor.rgbLightBlue;
ws.Cells[1, i + 1].HorizontalAlignment = XlHAlign.xlHAlignCenter;
}
// 2. 写入数据(区分数据类型,自动格式化)
for (int i = 0; i < dt.Rows.Count; i++)
{
for (int j = 0; j < dt.Columns.Count; j++)
{
object val = dt.Rows[i][j];
if (val == DBNull.Value) continue;
ws.Cells[i + 2, j + 1] = val;
// 关键:根据列名自动设置单元格格式
string colName = dt.Columns[j].ColumnName;
if (colName.Contains("Date") || colName.Contains("Time"))
ws.Cells[i + 2, j + 1].NumberFormat = "yyyy-mm-dd hh:mm:ss";
else if (colName.Contains("Price") || colName.Contains("Amount"))
ws.Cells[i + 2, j + 1].NumberFormat = "#,##0.00";
else if (colName == "Qty")
ws.Cells[i + 2, j + 1].NumberFormat = "0";
}
}
// 3. 自动列宽与边框
ws.Columns.AutoFit();
ws.Range[ws.Cells[1, 1], ws.Cells[dt.Rows.Count + 1, dt.Columns.Count]].Borders.LineStyle = XlLineStyle.xlContinuous;
wb.SaveAs(fileName);
wb.Close();
app.Quit();
}
这段代码依赖Microsoft.Office.Interop.Excel,所以部署时目标机器必须安装Office。如果客户没装Office,DataToExcel.cs还提供了备选方案:用EPPlus库(源码包里有EPPlus.dll),它纯.NET,无需Office,导出速度更快,且支持.xlsx格式。切换只需注释掉Interop代码,取消注释EPPlus部分。
4.3 多数据库适配:DbHelper系列不是摆设,是真正的“一次编写,多库运行”
DbHelperSQL.cs是核心,但它不是孤立的。整个适配体系是这样的:
- 统一接口:
IDbHelper.cs定义了ExecuteNonQuery(),ExecuteDataTable(),GetSingle()等所有数据库操作方法。 - 抽象基类:
DbHelperBase.cs实现了通用逻辑,如连接字符串拼接、参数化查询封装。 - 具体实现:
DbHelperSQL.cs继承DbHelperBase,重写GetConnection()返回SqlConnection;DbHelperMySQL.cs返回MySqlConnection,并重写GetParameter()以适配MySQL的?param占位符(SQL Server是@param)。 - BLL层无感调用:
PurchaseBLL.cs里所有数据访问,都通过DbHelperFactory.CreateHelper()获取实例:
csharp public static IDbHelper CreateHelper() { string dbType = ConfigurationManager.AppSettings["DBType"] ?? "SQL"; switch (dbType.ToUpper()) { case "SQL": return new DbHelperSQL(); case "MYSQL": return new DbHelperMySQL(); case "ORACLE": return new OracleHelper(); // OracleHelper.cs实现了IDbHelper default: throw new NotSupportedException($"不支持的数据库类型:{dbType}"); } } - 避坑指南:切换数据库时,最容易错的是SQL语法。
DbHelperSQL.cs里大量使用TOP 100,但MySQL用LIMIT 100,Oracle用ROWNUM <= 100。所以DbHelperMySQL.cs的GetPagedData()方法里,SQL拼接逻辑完全不同。源码里DbHelperSQL2.cs就是为SQL Server 2000写的,它用SET ROWCOUNT 100替代TOP,因为2000不支持TOP在子查询里。这些细节,决定了系统能否真的跨库运行。
5. 常见问题与排查技巧实录:那些文档里不会写的“血泪教训”
5.1 经典问题速查表
| 问题现象 | 可能原因 | 排查步骤 | 解决方案 |
|---|---|---|---|
| 登录时报“无法连接到服务器” | App.config连接字符串错误 | 1. 检查Data Source实例名是否正确2. 检查 Initial Catalog数据库名是否与还原后一致3. 用SSMS手动连接测试 | 修改App.config,确保实例名、数据库名、认证方式(Windows/SQL)全部匹配 |
| 入库单提交后,库存没增加 | 事务未提交或DAL层未调用库存更新 | 1. 在PurchaseBLL.CreatePurchaseOrder()里打断点,确认是否执行到InventoryDAL.UpdateInventory()2. 查 TB_StockLog表,看是否有新记录 | 检查UpdateInventory()方法内SQL是否正确,确认UPDATE语句的WHERE条件匹配ProductID |
| 导出Excel时程序崩溃 | 目标机器未安装Office或权限不足 | 1. 查看异常信息是否含COMException2. 尝试用 EPPlus分支导出 | 注释Interop代码,启用EPPlus分支;或在服务器安装Office并配置DCom权限 |
| 库存预警不触发 | MinStock/MaxStock为0或负数 | 1. 查询TB_Product表,检查MinStock字段值2. 在 CheckStockAlert()方法里加日志,输出availableStock和p.MinStock | 确保基础资料中MinStock>0,MaxStock>0;若允许不预警,设为0,代码中if (p.MinStock > 0)已规避 |
| 动态菜单不显示 | TB_RoleMenu权限表数据为空或角色ID不匹配 | 1. 查询TB_RoleMenu,确认RoleID=1(超级管理员)有菜单记录2. 检查登录后 currentUser.RoleId是否为1 | 运行InitRoleMenu.sql脚本初始化权限表;确认Account_Users.cs中用户角色ID赋值正确 |
5.2 我踩过的坑与独家心得
坑一:“时间戳”引发的库存混乱
客户上线第三天,发现某物料库存变成负数。排查发现,TB_Purchase表的CreateTime字段是datetime类型,精度为3.33毫秒,而TB_StockLog的LogTime是smalldatetime,精度为1分钟。当同一秒内发生入库和出库时,LogTime相同,ORDER BY LogTime排序不稳定,导致库存计算顺序错乱。解决方案:统一将所有时间字段改为datetime2(7),精度纳秒级,并在StockDAL.GetStockLogByTimeRange()的SQL里加ORDER BY LogTime, LogID(LogID是自增主键,保证绝对顺序)。
坑二:“拼音首字母”索引失效
为加速物料搜索,我在TB_Product.Code字段建了全文索引,并用CommonHelper.GetFirstPY("螺栓")返回"L",想实现“输入L,列出所有螺栓”。但GetFirstPY()用的是ChineseChar类,它依赖系统区域设置,客户服务器是英文版Windows,ChineseChar返回空。解决方案:弃用ChineseChar,改用PinYinConverter开源库(源码包里已包含PinYinConverter.dll),它不依赖系统,且支持多音字。GetFirstPY()方法重写为调用PinYinConverter.ConvertToPinyin(code).Substring(0,1)。
坑三:“打印预览”在高分屏上字体糊成一片
客户采购部用4K显示器,打印预览里的文字全是马赛克。原因是CrystalReportViewer控件未适配DPI缩放。解决方案:在Frm_PrintSet.cs的Load事件里,加入:
if (Environment.OSVersion.Version.Major >= 6)
{
SetProcessDpiAwareness(PROCESS_DPI_AWARENESS.PROCESS_SYSTEM_DPI_AWARE);
}
[DllImport("user32.dll")]
private static extern bool SetProcessDpiAwareness(PROCESS_DPI_AWARENESS value);
并在项目属性→应用程序→目标平台,改为x64(x86在高分屏下DPI适配有问题)。
最后分享一个小技巧:系统里所有单据编号(入库单、出库单、盘点单)都带日期前缀,如RKDD-20240520-001。客户曾要求“按月归档”,我本以为要写复杂脚本。后来发现,只要在SQL Server里建一个视图:
CREATE VIEW VW_PurchaseMonthly AS
SELECT *,
SUBSTRING(OrderNo, 5, 6) AS YearMonth -- RKDD-20240520-001 → 202405
FROM TB_Purchase
然后BLL层查VW_PurchaseMonthly WHERE YearMonth = '202405',瞬间搞定。真正的高手,不是写最多代码的人,而是最懂如何用最少的改动,撬动最大的业务价值的人。
简介:这个C#开发的WinForm仓库管理系统,采用标准三层架构设计,后端基于SQL Server(附jxc_20160427.bak完整数据库备份),支持超级管理员和普通用户双角色登录,并内置3次输错密码自动退出的安全机制。系统涵盖员工、客户、供应商、仓库、单位、库存上下限等基础资料维护;入库支持成品/半成品/原材料/其他物资四类业务,流程包括申请、审核、生成带编码/规格/数量/单价/经办人/时间/供应单位等字段的正式单据;出库同样走申请-审核-填单流程,含领物单位等关键信息。库存模块可实时查库存、做盘点登记、录报损记录,并在库存超上限或低于下限时自动提醒。统计查询覆盖入库/出库明细与汇总报表,所有数据支持一键导出Excel。源码中集成DbHelperSQL.cs及MySQL、Oracle、SQLite、OleDb多版本数据库辅助类,还包含打印设置、动态菜单加载、图表辅助显示、HTTP通信、Excel导出等实用扩展模块,适合教学演示、毕业设计参考或中小型仓储场景快速二次开发。


被折叠的 条评论
为什么被折叠?



