简介:这是一套基于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.sql"ShopDB.sql脚本内已预置CREATE DATABASE ShopDB ON (FILENAME='...')语句,路径指向项目根目录下的Database\ShopDB.mdf。若你解压路径含中文或空格(如D:\我的项目\ShopSystem),请先将整个文件夹移至纯英文路径(如D:\ShopSystem),否则SQLCMD会报路径解析错误——这是血泪经验,90%的“数据库连接失败”源于此。
2.4 解决方案文件结构与核心项目依赖关系
| 项目名称 | 类型 | 关键职责 | 依赖项 |
|---|---|---|---|
ShopClient | WinForms Application | 主程序入口,所有UI窗体(LoginForm、MainForm、AdminForm等) | ShopBusiness,ShopDataAccess |
ShopBusiness | Class Library | 业务逻辑层:UserService、ProductService、OrderService,含事务控制与状态校验 | ShopDataAccess |
ShopDataAccess | Class Library | 数据访问层:ProductDAL、UserDAL、OrderDAL,封装SqlCommand与SqlDataReader | System.Data.SqlClient |
ShopModel | Class 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. 严格比对哈希值,且检查用户状态(isLocked=0) 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。
登录成功后,系统不使用Session(WinForms无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)采用存储过程分页,避免内存中List<Product>.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表示查全部。前端分页控件(NumericUpDown+Button)触发时,仅刷新DataGridView,不重建窗体——这避免了每次翻页都重连数据库。
注意:首次加载商品时,
ProductDAL会检查_cachedProducts静态字典是否为空,若空则执行SP并缓存结果(有效期5分钟)。缓存键为$"All_{pageSize}",避免不同分页参数污染。这是WinForms下最轻量的缓存实践,无需引入Redis或MemoryCache。
3.3 购物车与订单生成:事务边界与并发控制
购物车数据暂存于内存(List<CartItem>),但提交订单时必须保证“库存扣减+订单插入+购物车清空”原子性:
// ShopBusiness/OrderService.cs public bool CreateOrder(List<CartItem> 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 Stock=Stock-@qty WHERE ID=@id) 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,则超卖风险仍存在(两个请求同时读到Stock=10,各自扣减5,最终Stock=0但卖出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的数据源是List<CartItem>,但未调用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.OfType<ProductListForm>().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 name="ShopDB" connectionString="Data Source=(localdb)\mssqllocaldb;Initial Catalog=ShopDB;Integrated Security=true;" providerName="System.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 List<Product> ReadProductsFromExcel(string filePath) { var products = new List<Product>(); 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中启用“输出窗口”(Ctrl+Alt+O),筛选[OrderStatus]关键字,即可看到完整状态流。比在代码里插10个Console.WriteLine更干净,且发布版自动忽略。
5.3 日志追踪:用NLog实现操作日志与错误日志分离
源码包已集成NLog,配置文件NLog.config定义两个目标:
| 目标名称 | 输出位置 | 内容类型 | 示例场景 |
|---|---|---|---|
fileError | Logs/error.log | Level >= Error | SqlException、空引用异常 |
fileOperation | Logs/operation.log | Level = Info | “管理员A删除商品ID=123”、“用户B下单ID=456” |
在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>节已设置archiveEvery="Day",日志按天滚动,避免单文件过大。部署时只需确保Logs文件夹有写入权限。
从那以后我每次接手新项目,都会先检查NLog.config的fileOperation目标是否启用——没有操作日志的系统,等于没有行车记录仪。线上出问题时,5分钟内就能定位到是哪个操作触发了连锁故障。希望帮到你。
本文还有配套的精品资源,点击获取