.NET商城源码解析:ASP.NET MVC三层架构打造完整交易闭环
简介这是一份基于.NET框架开发的网上商城源代码项目面向正在学习ASP.NET Web Forms与VB.NET的初中级开发者可用来理解电商平台从页面呈现、业务逻辑到配置管理的完整设计思路。压缩包共151个文件约2.24MB包含aspx页面、ascx用户控件、vb后台代码、resx资源文件、css样式及jpg/gif图片素材同时附有Web.config配置文件、数据库文件mdf/ldf和部署说明txt目录结构清晰适合本地还原、断点调试与二次扩展。已有128人浏览学习。通过Global.asax可观察全局事件处理与站点初始化结合首页、文章列表、产品列表、折扣商品等模块能掌握内容管理、商品展示、促销位配置等典型电商功能资源文件与样式表则展示了多语言界面和个性化外观的实现方式。整体代码量轻巧适合初学者通读也能为需要搭建小型网上商城的开发者提供可运行的参考范例。1. 一套能直接落地的.NET网上商城从商品上架到订单闭环的完整链路某公司接到一个内部电商项目预算紧、周期短最后直接拿一套带源代码的.NET商城做二次开发三周上线。这不是教学用的小Demo而是一个具备商品管理、购物车、下单、库存扣减、后台订单处理的完整交易闭环。基于.NET平台的ASP.NET MVC框架搭配SQL Server数据库经典三层架构每一层边界清楚改起来不绕。对想学.NET Web开发的人来说它是一份能看到完整电商业务逻辑的样板工程——从数据库表设计到前端页面渲染所有代码都在眼前适合用来跑通流程、理解电商核心模型也适合做毕业设计或企业内部系统的起点。这套源码解决的不是“能运行”而是“能按真实业务去改”。2. 技术栈与工程结构三层架构下每个文件都在哪2.1 技术选型与分层设计这套商城选型属于.NET平台上最稳妥的组合ASP.NET MVC做Web层SQL Server做数据存储数据访问用ADO.NET封装。没有引入过于复杂的ORM框架而是用SqlHelper这种轻量封装原因很简单——商城业务中复杂联表查询和存储过程很多直接写SQL更可控。这不是技术落后而是一种务实的取舍。逻辑上严格按三层架构划分表示层Views和Controllers、业务层BLL、数据访问层DAL再加上Model实体层和Common通用工具层。某团队接手后第一件事就是把这几个项目分层看清楚然后发现换数据库、换页面模板都很容易因为依赖方向是单向的Controllers调用BLLBLL调用DALDAL只认Model和配置文件。ShopSolution.sln ├── Web # 表示层控制器、视图、脚本、样式 │ ├── Controllers # 商品、购物车、订单、用户、后台管理 │ ├── Views # Razor视图模板 │ ├── Scripts # jQuery相关 │ └── Content # CSS与图片资源 ├── BLL # 业务逻辑层订单规则、库存校验、会员计算 ├── DAL # 数据访问层SqlHelper、各实体Repository ├── Model # 数据库实体映射Product、Order、User等 └── Common # 通用类分页组件、加密工具、常量定义2.2 目录结构逐层拆解Model层是数据库表的直接映射。商城核心表有这几张Product商品、Category分类、Sku库存单位、Cart购物车、Order订单、OrderItem订单明细、User用户。对应Model里的每个类字段名和数据库列名保持一致比如Product类里有ProductId、ProductName、Price、Stock、ImageUrl、Status这些属性没有额外包装简单直接。DAL层是数据库操作的核心。SqlHelper封装了连接管理和ExecuteDataTable、ExecuteNonQuery、ExecuteScalar三个基础方法所有复杂查询都以参数化SQL或存储过程调用。针对每个实体都有一个Repository类比如ProductDal里写分页查询、按分类查询、上下架操作OrderDal里写创建订单、更新状态、查询订单列表。这一层只做和数据打交道的事不掺业务判断。BLL层体现业务规则。创建订单时不能只往表里插一条记录要先检查库存、计算总价、验证用户状态这些规则全部放在OrderManager里。商品列表页的分页计算、购物车中商品数量上限、会员折扣计算也在BLL层处理。BLL向上给Controller提供服务向下调用DAL中间可以加缓存或事务但对外接口保持不变。2.3 关键配置文件Web.config是整个项目的命脉。连接字符串决定数据库指向权限配置决定后台管理是否开放Session状态配置决定购物车保存方式。常见做法是把数据库连接写在connectionStrings节点里部署时只需改这一处connectionStrings add nameShopDb connectionStringData Sourcelocalhost;Initial CatalogShopDB;User Idsa;Passwordyour_password;MultipleActiveResultSetsTrue providerNameSystem.Data.SqlClient / /connectionStrings参数说明Data Source指定数据库服务器地址本机调试用localhost或一个点号Initial Catalog是数据库名必须和附加数据库的名称一致User Id和Password看SQL Server实例的认证模式如果用Windows认证就改成Integrated SecurityTrue。MultipleActiveResultSetsTrue这个参数容易被忽略开启后同一个连接可以同时执行多个查询商城页面经常一个请求查多张表没有这个配置可能偶发连接错误。部署时还需要在system.web节点里设置编译模式和Session超时时间商城场景一般会设成40分钟和浏览器会话保持一致。3. 核心模块拆解一条请求从商品浏览到订单生成的完整链路3.1 商品列表的分页查询商品列表是所有商城的流量入口分页做得不好直接拖垮数据库。这里的实现是每次只加载当前页需要的数据而不是一次性读全表后再在内存里截取。DAL层的ProductDal里有一个SearchProducts方法接收关键词、分类Id、页码和每页条数拼出动态SQL。public DataTable SearchProducts(string keyword, int categoryId, int pageIndex, int pageSize, out int totalCount) { // 拼接SQL片段参数全部走SqlParameter避免注入 string where WHERE Status 1 ; ListSqlParameter paras new ListSqlParameter(); if (!string.IsNullOrEmpty(keyword)) { where AND ProductName LIKE kw ; paras.Add(new SqlParameter(kw, % keyword %)); } if (categoryId 0) { where AND CategoryId cid ; paras.Add(new SqlParameter(cid, categoryId)); } // 先查总记录数再查当前页数据 string countSql SELECT COUNT(1) FROM Product where; totalCount Convert.ToInt32(SqlHelper.ExecuteScalar(countSql, paras.ToArray())); string pageSql SELECT * FROM ( SELECT ROW_NUMBER() OVER (ORDER BY ProductId DESC) AS RowNum, * FROM Product where ) AS T WHERE RowNum BETWEEN start AND end; paras.Add(new SqlParameter(start, (pageIndex - 1) * pageSize 1)); paras.Add(new SqlParameter(end, pageIndex * pageSize)); return SqlHelper.ExecuteDataTable(pageSql, paras.ToArray()); }逻辑说明先用COUNT查总条数得到totalCount后用于前端翻页组件的总页数计算再用ROW_NUMBER排序并取指定区间的数据。这里的ORDER BY ProductId DESC保证新上架商品排在前面。分页参数start和end的计算方式是常见写法第一页取1到20第二页取21到40。需要重点注意的是要控制一个查询最多允许翻多少页我一般会在BLL层做保护防止攻击者用pageIndex99999这种参数把数据库拖垮。这套代码没有使用存储过程来做分页。每页数据量在几十条以内的场景下性能完全可以接受。如果商品量超过百万级再改成存储过程或引入缓存也不迟。3.2 购物车的入库与会话管理购物车设计有意思的地方在于它没有用数据库表去实时记录而是利用Session。商城浏览行为的特点是用户可能没有登录就在逛这时候购物车数据放Session里最自然。代码里CartController的Add方法接收商品Id和数量先把Session中的购物车取出来再决定是新增还是累加。public ActionResult Add(int productId, int quantity) { // 从Session取当前购物车没有则新建 ListCartItem cart Session[Cart] as ListCartItem; if (cart null) { cart new ListCartItem(); } CartItem existing cart.FirstOrDefault(c c.ProductId productId); if (existing ! null) { // 同商品数量累加同时判断是否超过库存上限 if (existing.Quantity quantity 999) { return Json(new { ok false, msg 单品数量超出上限 }); } existing.Quantity quantity; } else { // 从商品表取基础信息快照价格避免下单前价格变动 Product product ProductManager.GetProductById(productId); if (product null || product.Status ! 1) { return Json(new { ok false, msg 商品不可购买 }); } cart.Add(new CartItem { ProductId productId, ProductName product.ProductName, Price product.Price, Quantity quantity }); } Session[Cart] cart; return Json(new { ok true, count cart.Sum(c c.Quantity) }); }逻辑说明购物车里的CartItem是Model层的一个轻量类包含ProductId、ProductName、Price、Quantity四个属性。Price在这里做快照用户在浏览时看到的价格会被记录在购物车条目里下单时以这个价格为准不受后台改价影响——这是商城常见做法否则用户加购后看到的价格和结算价不一致会引起客诉。Session存储的缺点是服务器重启后购物车数据丢失但对该规模的项目无伤大雅。如果上线后访问量上来再换Redis或者数据库持久化方案即可。3.3 下单事务扣库存与生成订单下单是整个商城最核心的链路必须保证数据一致性。OrderManager的CreateOrder方法用SqlTransaction包裹所有写操作生成订单主表记录、写订单明细、扣减库存任何一个步骤失败就整体回滚。业务规则都放在进入事务前的校验环节比如购物车不能为空、用户收货地址必须填写、库存是否充足。public bool CreateOrder(int userId, int addressId, ListCartItem cartItems, out string orderNo) { using (SqlConnection conn new SqlConnection(_connString)) { conn.Open(); SqlTransaction tran conn.BeginTransaction(); try { orderNo DateTime.Now.ToString(yyyyMMddHHmmss) new Random().Next(1000, 9999); decimal totalAmount 0; // 第一遍先算总价并校验所有商品库存 foreach (var item in cartItems) { int stock (int)SqlHelper.ExecuteScalar( SELECT Stock FROM Product WHERE ProductIdpid AND Status1, new SqlParameter(pid, item.ProductId), tran); if (stock item.Quantity) { tran.Rollback(); orderNo null; return false; // 库存不足直接失败 } totalAmount item.Price * item.Quantity; } // 写订单主表 string sqlOrder INSERT INTO [Order] (OrderNo, UserId, AddressId, TotalAmount, Status, CreateTime) VALUES (orderNo, uid, addr, amount, 0, GETDATE()); SqlHelper.ExecuteNonQuery(sqlOrder, new SqlParameter(orderNo, orderNo), new SqlParameter(uid, userId), new SqlParameter(addr, addressId), new SqlParameter(amount, totalAmount), tran); // 写明细并扣库存 foreach (var item in cartItems) { string sqlItem INSERT INTO OrderItem (OrderNo, ProductId, ProductName, Price, Quantity) VALUES (orderNo, pid, pname, price, qty); SqlHelper.ExecuteNonQuery(sqlItem, new SqlParameter(orderNo, orderNo), new SqlParameter(pid, item.ProductId), new SqlParameter(pname, item.ProductName), new SqlParameter(price, item.Price), new SqlParameter(qty, item.Quantity), tran); string sqlStock UPDATE Product SET Stock Stock - qty WHERE ProductId pid; SqlHelper.ExecuteNonQuery(sqlStock, new SqlParameter(qty, item.Quantity), new SqlParameter(pid, item.ProductId), tran); } tran.Commit(); return true; } catch { tran.Rollback(); throw; } } }逻辑说明订单表里的Status字段记录订单状态0代表待支付1代表已支付待发货后续靠后台更新这个值。事务保证了一个关键事实——库存扣减和订单生成必须同时成功或同时失败否则会出现扣了库存没订单、或者有订单没扣库存的脏数据。这里用到的随机数生成订单号是个常见方案但不适合高并发场景。如果后续订单量上来订单号可以改成时间戳加用户Id加随机数甚至可以引入雪花算法。直接拼接的订单号在并发量极小的情况下不会冲突。库存校验后实际上并发时仍可能超卖严格做法是用UPDATE Product SET Stock Stock - qty WHERE ProductId pid AND Stock qty判断受影响行数代码里的方式在低并发下够用但二次开发时一定要改掉。这是我在实际项目中踩过的坑——上线初期没人发现问题直到某次促销活动多个用户同时下单同一商品才发现并发超卖。4. 本地跑通指南从数据库脚本到浏览器出页面4.1 数据库脚本初始化整个工程根目录下一般会放database文件夹里面是建库脚本和示例数据脚本。跑通流程第一步不是打开Visual Studio编译而是先把数据库准备好。打开SQL Server Management Studio连上本地实例执行脚本文件夹里的create_database.sql脚本会自动创建数据库和各张表同时插入一批测试用的商品分类和用户数据。通过脚本建库而不是附加MDF文件有个好处SQL Server版本兼容性问题会少很多。脚本里每次建表前都判断IF OBJECT_ID(Product) IS NOT NULL DROP TABLE Product这意味着重复执行不会出错对反复初始化很友好。4.2 修改Web.config连接字符串数据库建好后打开Web项目根目录下的Web.config把连接字符串改成当前环境对应的账号密码。这里有个选择题集成认证最省事但部署到IIS时要额外配置应用程序池的标识SQL Server账号密码方式调试期更直观。建议本地开发直接用SQL Server账号连部署时再决定是否切换。connectionStrings add nameShopDb connectionStringData Source.;Initial CatalogShopDB;User Idsa;Password123456;MultipleActiveResultSetsTrue providerNameSystem.Data.SqlClient / /connectionStrings参数说明Data Source写一个点号代表本机默认实例如果装了命名实例要写成服务器名\实例名。User Id和Password换成你自己SQL Server的账号。如果连接报错先用SQL Server Management Studio试一下这个账号能不能连上能连上再检查Web.config这是排查连接问题最有效的方法。4.3 在Visual Studio里启动项目用Visual Studio打开解决方案文件不用额外安装任何第三方包因为依赖都是.NET Framework自带的程序集这一点非常省心。右键Web项目设置为启动项目直接按F5跑起来。首次启动会编译整个解决方案耗时取决于机器性能。浏览器会自动打开首页如果看到商品列表就说明数据库连接正常。接着可以点开几个商品加入购物车再到购物车页面点击结算填一个测试收货地址走完一笔测试订单这样整个链路就通了。测试订单生成后打开后台管理页面用脚本里预设的管理员账号登录在订单管理里能看到这笔订单状态是待支付。4.4 部署到IIS的额外配置本地跑通只是第一步真正上线要部署到IIS。有几个配置是必经之路。Web项目发布需要右键选择“发布”输出到本地文件夹然后在IIS里创建网站物理路径指向发布文件夹应用程序池选.NET Framework v4.0经典模式或集成模式然后给IIS_IUSRS用户分配读取权限。垃圾回收和Session配置也可以在IIS里调。最大的坑是如果Web.config里没有设置authentication modeForms访问后台地址会直接报401因为控制器上贴了Authorize特性。system.web authentication modeForms forms loginUrl~/Account/Login timeout40 / /authentication /system.web参数说明Forms认证是后台管理访问控制的核心loginUrl指定未登录用户跳转的登录页timeout指定登录状态的保持时间超时后需要重新登录。如果部署后点后台链接总是跳回登录页先检查这个配置再看浏览器是否禁用了Cookie。IIS下如果遇到404错误多半是路由没生效需要在站点处理程序映射里确认ExtensionlessUrlHandler-Integrated-4.0是启用状态。5. 常见问题排查部署和运行期最常踩的五个坑5.1 数据库附加失败报错提示版本不兼容现象直接双击或附加数据库文件SQL Server弹窗报错说数据库文件版本高于当前实例版本。原因这套商城的数据库文件可能是用高版本SQL Server导出备份的低版本实例挂载不上。我遇到过某团队用SQL Server 2016的实例去挂2019生成的库直接翻车。解决优先用脚本建库而非附加文件。脚本建库不依赖版本只要不是语法完全不兼容的写法。如果只有MDF文件可以先用文本编辑器打开看字段定义手动创建表再导数据。血泪经验一切以脚本为基准附加文件省时间但版本兼容问题可能会浪费整个下午。5.2 IIS部署后访问页面提示“拒绝访问”现象网站能启动但打开页面时提示找不到资源或权限拒绝后台管理页尤其明显。原因IIS默认应用程序池身份不一定是管理员对发布文件夹没有写权限。这套商城有日志写入和图片上传功能涉及文件写操作时会触发权限错误。解决给发布目录分配IIS_IUSRS用户的修改权限或者把应用程序池的“加载用户配置文件”设为True。某公司第一次上线时图省事直接把应用程序池标识改成LocalSystem权限是够了但出了事定位问题特别麻烦后来还是规规矩矩给目录配权限。如果是在程序里写日志报错再检查日志目录是否存在Web应用经常犯的错是目录还没创建就写入。5.3 商品列表中文显示乱码现象数据库里的中文正常页面上却显示问号或菱形乱码。原因页面编码和数据库编码不一致。数据库表用GBK存中文页面用UTF-8渲染或者反过来。这套商城建表脚本里字段类型是nvarchar这种类型对编码兼容性比较友好但Web.config里没设置请求编码时表单提交的中文还是可能乱码。解决在Web.config的system.web节点下加globalization requestEncodingutf-8 responseEncodingutf-8 fileEncodingutf-8 /统一改成UTF-8。如果数据库里已经存了乱码数据改配置只能解决新增数据存量数据需要写SQL清洗。我一般处理这类问题的顺序是先看数据库字段存的字节再看HTTP响应头里的Charset最后才改配置。5.4 用户登录后很快掉线购物车经常清空现象登录状态保持不住加购几件商品后过一会儿再打开购物车就空了。原因Forms认证超时时间设置太短或者Session超时时间更短。这套商城的购物车存在Session里Session默认超时只有20分钟一旦超时就丢。用户如果中途去吃个饭回来购物车就没了体验很差。解决在Web.config里把Session超时调大到40分钟以上并且保证Forms认证的timeout大于Session超时。另一个隐蔽问题是IIS应用程序池的“闲置超时”默认20分钟没有访问时工作进程被回收Session数据存在内存里会全部丢失。解决方法是把应用程序池的闲置超时改成0永不回收或改成指定时间。5.5 后台管理页面上传商品图片失败现象图片选择后点击上传提示“操作失败”或“找不到路径”图片没有出现在服务器目录里。原因上传目录在IIS下没有写入权限。更隐蔽的一个原因是默认的临时上传目录被系统保护或者改过站点路径但代码里还指向旧路径。解决先检查Uploads目录是否存在不存在先手动建出来再检查写入权限。如果上传目录用的是相对路径确认发布后的实际目录结构是否和开发环境一致很多开发者本地跑通发布时虚拟目录层级变了相对路径就找不到目标。上传模块里建议用Server.MapPath(~/Uploads/)拿到绝对路径再存这样不会因为虚拟目录问题而踩坑。还有一点图片文件重名会互相覆盖二次开发时最好在文件名里拼上时间戳或GUID。6. 二次开发技巧把通用商城改成自己业务的起点接手这套源码后二次开发的思路决定了后续扩展的成本。建议第一步不是改代码而是先把业务跑通。然后按自己的业务场景做减法或加法。以给业务增加一个“订单备注”字段为例操作路径非常清晰先在数据库Order表加Remark列再在Model层给Order实体加Remark属性然后在DAL的Insert语句和查询映射里加上这个字段最后在Views的订单编辑页加一个输入框。这四步串联起来就是一次完整的“数据库-后端-前端”改版走完一遍就理解了整套代码的耦合点在哪里。如果要做前端改造比如替换过时的UI风格优先动Views文件夹里的Razor模板和Content文件夹下的CSS文件不要动控制器和业务层。商城页面的头部、尾部公共布局放在Layout.cshtml里改这一个文件就能全局生效。我一般会建议先替换首页模板跑通静态资源路径再逐步替换内页。如果引入Bootstrap一类的框架要注意原有脚本里是否依赖了jQuery旧版本的方法可能需要同步升级。对性能有要求的场景在商品列表页加缓存收益最明显。把BLL层的GetProductById方法返回结果缓存在内存里设置一分钟过期商品价格变动时最多延迟一分钟生效这是性价比最高的改动。如果缓存命中率上来了数据库压力能明显下降。具体实现时可以引用System.Runtime.Caching命名空间用MemoryCache类做进程内缓存。整个二次开发过程中最值钱的一个习惯是每次改动都从业务流程梳理开始先确认影响哪些表、哪些页面再动手写代码。某团队当初接这套商城时前端同事直接改了商品详情的界面却不知道下单逻辑里引用了该页面的商品价格快照字段结果页面显示的新价格和订单里的旧价格不一致被用户投诉了几天。从那以后我每次接手这类含事务和快照逻辑的电商项目都会强制自己先列出所有涉及金额的数据字段清单再让前后端同事对齐核对一遍才动工。这套.NET商城虽然代码风格传统但胜在结构清晰只要遵循三层架构的方向去改就不会越改越乱。希望帮到你。本文还有配套的精品资源点击获取