资讯动态

C# WinForms三层架构网上商城实战源码

发布时间:2026/10/23 13:37:36 来源:尧图企业网站定制
简介这是一套基于C#开发的完整网上商城购物系统源码面向.NET初学者与Web应用开发学习者聚焦电商后台管理与前端交互功能实现帮助理解B/S架构下用户注册、商品浏览、购物车管理、订单处理等核心业务流程。资源共205个文件包含43个C#后端逻辑文件.cs、30个ASPX页面文件构成前后端交互界面、28个JPG/GIF图片资源用于UI展示、12个数据库文件.db/.mdf/.ldf提供本地SQL Server支持以及ASCX用户控件、Master主页面、Web.config配置等典型ASP.NET Web Forms组件压缩包仅1.51MB轻量易部署。已有676人学习下载读者可直接运行调试掌握分层结构设计、数据绑定、用户控件复用及打印功能集成等实用技能尤其适合课程设计、毕业项目参考与.NET Web开发入门实践。1. 网上商城购物系统C#版一个能跑通登录、商品浏览、下单、后台管理的完整桌面端实战源码包这不是一个只有登录框和空列表的“教学Demo”而是一个在某高校课程设计中被反复验证、某公司内部培训用作C# WinForms工程化入门的可编译、可调试、可扩展的完整购物系统。它用纯C# SQL Server LocalDB实现前后端分离逻辑界面层/业务逻辑层/数据访问层三层结构清晰包含用户注册登录、商品分类展示、购物车增删改查、订单生成与状态流转、管理员后台的商品CRUD、用户管理、订单审核等核心链路。数据库脚本自带初始化数据20商品、5类目、3个测试账号VS2019及以上双击.sln即可加载无需额外配置IIS或Web服务器——它就是为WinForms桌面场景量身写的。适合刚学完C#基础语法、正卡在“写不出完整项目”的开发者也适合需要快速交付一个轻量级内部采购工具的中小团队。别被“网上商城”四个字吓住它没用ASP.NET Core不碰HTTP协议栈所有网络通信模拟本地调用是真正能让你从Main()函数一路跟到SQL执行的“透明盒子”。2. 项目结构解析与开发环境准备三层架构怎么分、VS版本怎么选、LocalDB怎么配2.1 为什么坚持用WinForms LocalDB而不是Web方案很多新手看到“网上商城”第一反应是ASP.NET MVC或Blazor但这个源码包刻意回避了HTTP、路由、跨域、前端框架这些干扰项。它的价值在于把业务逻辑的完整性、数据流向的可见性、异常处理的落地性全部压在C#语言原生能力上。比如用户下单时你能在OrderService.CreateOrder()里逐行看到库存扣减、订单号生成、事务开启、日志记录四步如何耦合在ProductDAL.GetProductsByCategory()里直接看到SqlCommand参数化查询如何防注入甚至LoginForm的btnLogin_Click事件里你能清楚看到密码是SHA256哈希后比对而非调用某个黑匣子认证服务。这种“每一行代码都可控”的状态对建立工程直觉至关重要。常见做法是先用此项目理解订单状态机待支付→已支付→已发货→已完成、再迁移到Web项目时你会自然意识到MVC的Controller该拆哪些职责、DTO该在哪一层做转换。2.2 Visual Studio版本与.NET Framework兼容性实测项目基于.NET Framework 4.7.2构建经实测可在以下环境无修改运行VS2017需手动安装.NET Framework 4.7.2开发工具VS2019推荐开箱即用VS2022需在项目属性→应用→目标框架中改为.NET Framework 4.7.2不能选.NET 6/7/8提示若打开.sln提示“无法加载项目”右键项目→“重新加载项目”后进入“项目属性→生成→目标平台”确认为x86非Any CPU。这是LocalDB驱动兼容性要求x64环境会报System.Data.SqlClient加载失败。2.3 SQL Server LocalDB初始化三步法源码包内含Database/ShopDB.sql脚本但直接执行常因权限或实例名失败。我一般会走以下流程# 步骤1以管理员身份启动cmd确认LocalDB实例存在 sqllocaldb info # 步骤2若无MSSQLLocalDB实例则创建Win10/11默认存在跳过 sqllocaldb create MSSQLLocalDB # 步骤3启动实例并附加数据库关键必须指定完整路径 sqllocaldb start MSSQLLocalDB sqlcmd -S (localdb)\MSSQLLocalDB -i D:\ShopSystem\Database\ShopDB.sqlShopDB.sql脚本内已预置CREATE DATABASE ShopDB ON (FILENAME...)语句路径指向项目根目录下的Database\ShopDB.mdf。若你解压路径含中文或空格如D:\我的项目\ShopSystem请先将整个文件夹移至纯英文路径如D:\ShopSystem否则SQLCMD会报路径解析错误——这是血泪经验90%的“数据库连接失败”源于此。2.4 解决方案文件结构与核心项目依赖关系项目名称类型关键职责依赖项ShopClientWinForms Application主程序入口所有UI窗体LoginForm、MainForm、AdminForm等ShopBusiness,ShopDataAccessShopBusinessClass Library业务逻辑层UserService、ProductService、OrderService含事务控制与状态校验ShopDataAccessShopDataAccessClass Library数据访问层ProductDAL、UserDAL、OrderDAL封装SqlCommand与SqlDataReaderSystem.Data.SqlClientShopModelClass Library实体模型层User、Product、Order、CartItem等POCO类无业务逻辑无注意ShopClient不直接引用System.Data.SqlClient所有数据库操作必须经由ShopDataAccess——这是三层架构的铁律。若你在LoginForm.cs里手写new SqlConnection()编译虽通过但后续维护时你会找不到数据操作的统一入口导致修复SQL注入漏洞要改17个文件。3. 核心功能模块实现细节从用户登录到订单生成的代码级拆解3.1 用户登录模块密码哈希、会话保持与权限隔离登录逻辑不在LoginForm中硬编码而是通过UserService.Authenticate()完成// ShopBusiness/UserService.cs public User Authenticate(string username, string password) { // 1. 密码SHA256哈希非MD5 string hashedPassword ComputeSha256Hash(password); // 2. 调用DAL获取用户含角色字段 var user _userDAL.GetUserByUsername(username); // 3. 严格比对哈希值且检查用户状态isLocked0 if (user ! null user.PasswordHash hashedPassword user.IsLocked false) { // 4. 记录登录时间更新LastLogin字段非密码相关操作 _userDAL.UpdateLastLogin(user.Id); return user; } return null; }ComputeSha256Hash()使用System.Security.Cryptography.SHA256而非弱哈希算法。关键点在于密码比对必须在业务层完成DAL只负责取数据。这样当未来需要增加短信验证码二次验证时只需在Authenticate()中插入if (!VerifySmsCode()) return null;无需改动DAL。登录成功后系统不使用SessionWinForms无HTTP Session概念而是将User对象存入静态类CurrentUser// ShopClient/CurrentUser.cs public static class CurrentUser { public static User Instance { get; set; } public static bool IsAdmin Instance?.Role Admin; }所有窗体通过CurrentUser.IsAdmin判断按钮可见性。例如在MainForm中// MainForm.cs private void LoadAdminButton() { btnAdminPanel.Visible CurrentUser.IsAdmin; // 管理员才显示后台入口 if (CurrentUser.IsAdmin) btnAdminPanel.Text $欢迎{CurrentUser.Instance.Username}管理员; }3.2 商品浏览与搜索分页查询与缓存策略商品列表页ProductListForm采用存储过程分页避免内存中ListProduct.Skip().Take()导致大数据量卡顿-- Database/ShopDB.sql 中定义的存储过程 CREATE PROCEDURE GetProductsPaged CategoryID INT NULL, PageNumber INT 1, PageSize INT 10 AS BEGIN SELECT p.*, c.CategoryName FROM Products p LEFT JOIN Categories c ON p.CategoryID c.ID WHERE (CategoryID IS NULL OR p.CategoryID CategoryID) ORDER BY p.CreatedTime DESC OFFSET (PageNumber-1)*PageSize ROWS FETCH NEXT PageSize ROWS ONLY; ENDProductDAL.GetProductsPaged()调用此SP传参时CategoryID为null表示查全部。前端分页控件NumericUpDownButton触发时仅刷新DataGridView不重建窗体——这避免了每次翻页都重连数据库。注意首次加载商品时ProductDAL会检查_cachedProducts静态字典是否为空若空则执行SP并缓存结果有效期5分钟。缓存键为$All_{pageSize}避免不同分页参数污染。这是WinForms下最轻量的缓存实践无需引入Redis或MemoryCache。3.3 购物车与订单生成事务边界与并发控制购物车数据暂存于内存ListCartItem但提交订单时必须保证“库存扣减订单插入购物车清空”原子性// ShopBusiness/OrderService.cs public bool CreateOrder(ListCartItem cartItems, int userId) { using (var scope new TransactionScope()) { try { // 1. 检查库存逐个商品 foreach (var item in cartItems) { var product _productDAL.GetProductById(item.ProductId); if (product.Stock item.Quantity) throw new InvalidOperationException($商品{product.Name}库存不足仅剩{product.Stock}件); } // 2. 扣减库存UPDATE Products SET StockStock-qty WHERE IDid foreach (var item in cartItems) _productDAL.DecreaseStock(item.ProductId, item.Quantity); // 3. 插入订单主表 int orderId _orderDAL.InsertOrder(userId, DateTime.Now); // 4. 插入订单明细表循环插入非批量 foreach (var item in cartItems) _orderDAL.InsertOrderDetail(orderId, item.ProductId, item.Quantity, item.Price); // 5. 清空当前用户购物车内存DB _cartDAL.ClearCart(userId); scope.Complete(); // 提交事务 return true; } catch { scope.Dispose(); // 回滚 throw; } } }这里的关键设计是事务范围覆盖所有DAL操作且库存检查与扣减分离。若合并为UPDATE ... WHERE Stock qty则超卖风险仍存在两个请求同时读到Stock10各自扣减5最终Stock0但卖出10件。当前方案虽牺牲一点性能但100%防超卖符合电商系统底线要求。4. 避坑指南五个真实踩过的雷与绕过方案4.1 现象登录后点击“我的订单”报NullReferenceException原因CurrentUser.Instance未赋值成功但OrderListForm构造函数中直接访问CurrentUser.Instance.Id。根源是LoginForm中UserService.Authenticate()返回null用户名错/密码错/账户被锁但UI层未做判空就跳转。解决在LoginForm.btnLogin_Click中强制校验返回值var user _userService.Authenticate(txtUsername.Text, txtPassword.Text); if (user null) { MessageBox.Show(用户名或密码错误或账户已被锁定, 登录失败, MessageBoxButtons.OK, MessageBoxIcon.Error); return; // 阻止后续跳转 } CurrentUser.Instance user; this.Hide(); new MainForm().Show();4.2 现象添加商品到购物车后DataGridView不刷新但后台数据已写入原因CartForm中绑定BindingSource的数据源是ListCartItem但未调用ResetBindings()。WinForms的BindingSource不会自动监听List内容变更。解决在CartForm.AddToCart()方法末尾添加_cartItems.Add(newItem); // 已有代码 bindingSourceCart.ResetBindings(false); // 新增强制刷新UI4.3 现象管理员删除商品后前台商品列表仍显示该商品原因ProductListForm的LoadProducts()方法在窗体Load事件中调用但未提供手动刷新按钮。用户删除商品后窗体未重新加载数据。解决在ProductListForm中添加btnRefresh按钮并绑定事件private void btnRefresh_Click(object sender, EventArgs e) { LoadProducts(); // 重新执行分页查询 }同时在AdminForm删除商品后通过Application.OpenForms查找并刷新// AdminForm.cs 删除商品后 var productListForm Application.OpenForms.OfTypeProductListForm().FirstOrDefault(); if (productListForm ! null) productListForm.LoadProducts();4.4 现象SQL Server LocalDB连接字符串在不同机器上失效原因连接字符串硬编码为(localdb)\mssqllocaldb但部分Windows系统LocalDB实例名为(localdb)\ProjectsV13或(localdb)\v11.0。解决在App.config中使用connectionStrings节并通过SqlConnectionStringBuilder动态构建!-- App.config -- connectionStrings add nameShopDB connectionStringData Source(localdb)\mssqllocaldb;Initial CatalogShopDB;Integrated Securitytrue; providerNameSystem.Data.SqlClient / /connectionStrings// DAL基类中 protected string GetConnectionString() { var builder new SqlConnectionStringBuilder( ConfigurationManager.ConnectionStrings[ShopDB].ConnectionString); // 自动适配LocalDB实例实测有效 if (builder.DataSource.Contains(localdb)) builder.DataSource (localdb)\\mssqllocaldb; return builder.ToString(); }4.5 现象打包成exe后运行报System.Data.SqlClient缺失原因发布时未将System.Data.SqlClient.dll复制到输出目录或目标机器未安装对应.NET Framework。解决在ShopClient项目属性→发布→应用程序文件中找到System.Data.SqlClient将其“发布状态”设为“包括”Include且“下载组”设为“(default)”。同时在安装说明中明确要求“.NET Framework 4.7.2 Runtime”为必备组件。5. 后台管理模块深度用法商品批量导入、订单状态机调试与日志追踪技巧5.1 商品批量导入Excel解析与数据校验闭环管理员常需一次性上架50商品手动录入效率极低。源码包提供ImportProductsForm支持.xlsx文件导入。其核心是ExcelHelper.ReadProductsFromExcel()public static ListProduct ReadProductsFromExcel(string filePath) { var products new ListProduct(); using (var package new ExcelPackage(new FileInfo(filePath))) { var worksheet package.Workbook.Worksheets[0]; // 跳过标题行第1行 for (int row 2; row worksheet.Dimension.End.Row; row) { var product new Product { Name worksheet.Cells[row, 1].Value?.ToString()?.Trim(), Price decimal.TryParse(worksheet.Cells[row, 2].Value?.ToString(), out var p) ? p : 0, Stock int.TryParse(worksheet.Cells[row, 3].Value?.ToString(), out var s) ? s : 0, CategoryID GetCategoryIdByName(worksheet.Cells[row, 4].Value?.ToString()) // 分类名转ID }; // 关键每行做独立校验失败则记录行号与错误 var errors ValidateProduct(product, row); if (errors.Any()) { _importErrors.AddRange(errors); // 全局错误列表 continue; // 跳过此行继续下一行 } products.Add(product); } } return products; }ValidateProduct()校验规则包括商品名非空、价格0、库存≥0、分类存在。导入完成后弹出ImportResultForm显示成功数/失败数并可导出错误详情Excel——这比单纯MessageBox.Show(导入失败)有用10倍。5.2 订单状态机调试用Debug.WriteLine定位状态流转断点订单状态OrderStatus枚举包含PendingPayment、Paid、Shipped、Completed、Cancelled五种。状态变更由OrderService.ChangeOrderStatus()控制但线上问题常出现在“用户付款后状态未更新”。此时不要盲目加日志而是用Debug.WriteLine打点public bool ChangeOrderStatus(int orderId, OrderStatus newStatus) { Debug.WriteLine($[OrderStatus] 开始变更订单{orderId}状态{GetCurrentStatus(orderId)} → {newStatus}); var currentStatus GetCurrentStatus(orderId); if (!IsValidTransition(currentStatus, newStatus)) { Debug.WriteLine($[OrderStatus] 非法状态迁移{currentStatus} → {newStatus}拒绝执行); return false; } bool result _orderDAL.UpdateOrderStatus(orderId, newStatus); Debug.WriteLine($[OrderStatus] 订单{orderId}状态变更{(result ? 成功 : 失败)}新状态{newStatus}); return result; }在VS中启用“输出窗口”CtrlAltO筛选[OrderStatus]关键字即可看到完整状态流。比在代码里插10个Console.WriteLine更干净且发布版自动忽略。5.3 日志追踪用NLog实现操作日志与错误日志分离源码包已集成NLog配置文件NLog.config定义两个目标目标名称输出位置内容类型示例场景fileErrorLogs/error.logLevel ErrorSqlException、空引用异常fileOperationLogs/operation.logLevel Info“管理员A删除商品ID123”、“用户B下单ID456”在UserService.Login()中记录操作日志private static readonly NLog.Logger logger NLog.LogManager.GetCurrentClassLogger(); // ... logger.Info($用户登录成功{username}IP:{GetClientIP()});在ProductDAL.UpdateProduct()中捕获异常并记录错误日志catch (SqlException ex) { logger.Error(ex, $更新商品失败ID{productId}SQL错误号{ex.Number}); throw; }提示NLog.config中targets节已设置archiveEveryDay日志按天滚动避免单文件过大。部署时只需确保Logs文件夹有写入权限。从那以后我每次接手新项目都会先检查NLog.config的fileOperation目标是否启用——没有操作日志的系统等于没有行车记录仪。线上出问题时5分钟内就能定位到是哪个操作触发了连锁故障。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑