☰
基于C#的微信小程序商城实战:前后端分离与并发扣库存方案
2026/10/26 22:47:44 网站建设 项目流程

简介:一套基于C#的微信小程序在线购物商城毕业设计源码,面向计算机相关专业学生、微信小程序初学者及需要快速搭建商城原型的开发者。项目以C#/ASP.NET作为后端服务,配合微信小程序前端,涵盖商品展示、购物车、订单管理、支付接口、后台管理等功能模块,既能作为毕业设计参考,也能用于理解前后端分离与小程序的完整开发流程。压缩包共1930个文件,大小26.9MB,主要包括480个cs源文件、125个aspx页面、140个js脚本、24个wxss样式、23个wxml模板,以及sql数据库脚本、75个dll依赖库和大量png/jpg/gif界面素材,目录结构覆盖前端、后端、数据库与配置文档。已有772人学习浏览。资源还包含upload_json.ashx、admin_ajax.ashx、payinfo.ashx等接口示例,对学习小程序登录鉴权、文件上传、在线支付对接及ASP.NET Web服务开发有直接参考价值,部署前可按内置sln工程和配置文件调整连接字符串并导入mdf/ldf数据库。

1. 基于C#的微信小程序在线购物商城:这套源码解决什么问题、适合谁上手

拿到这个 zip,先别急着解压。标题里最误导人的一点是:微信小程序的页面里不可能跑 C#。小程序端能执行的是 WXML、WXSS 和 JavaScript,C# 在这套方案里真正的位置是服务端,负责商品、订单、登录、支付这些接口。所以「基于C#的微信小程序在线购物商城」本质是一个前后端分离的电商工程:C# 写 Web API,小程序原生前端消费这些接口。对已经有 .NET 基础、想快速出一个能下单的微信商城 MVP,或者做管理系统、上位机出身、第一次接触小程序电商的团队,这套源码的价值在于不用从零设计登录体系和接口协议,跑通后端,前端对着接口调,一两天就能出一个能下单的版本。

2. 先拆边界再看代码:C# 负责后端接口,小程序负责页面与交互

2.1 小程序页面里为什么不会有 C#:前后端技术栈的第一课

很多第一次接触小程序的人会打开压缩包找 .cs 文件放在小程序目录里的证据,找不到就怀疑源码不完整。其实微信小程序的前端三件套是固定的:WXML 负责结构,WXSS 负责样式,JavaScript/TypeScript 负责交互逻辑。C# 只能跑在 .NET 运行时上,而小程序运行在微信客户端提供的 WebView 和 JS 引擎里,两边没有任何直接执行 C# 的通道。

所以这套工程的正确打开方式是看两个并行的目录:一个 ASP.NET Core Web API 项目,一个原生小程序项目。C# 站在服务端输出 JSON,小程序端通过 wx.request 把这些 JSON 变成页面上的商品卡片、购物车数字和订单状态。如果团队里有人问「C# 写的页面在哪」,答案是把接口跑起来,然后在开发者工具里看到的页面就是前端工程。理解这条边界,后续所有排错都会顺很多:页面布局问题去查 WXML,接口报错去查 C# 端日志,这两件事可以并行排查。

2.2 后端接口清单:登录、商品、订单、支付回调六类核心端点

一个能跑通购买闭环的商城后端,最少需要下面这些接口。我见过有些新手上来就建二十几张表,结果购物车、优惠券、积分全做成服务端表,连加个购物车都要登录,把简单的 MVP 撑成巨石。正确做法是先列最小闭环,不够再加。

端点方法职责关键入参
/api/auth/loginPOST用 wx.login 的 code 换登录态code
/api/productsGET商品分页列表,支持分类筛选page, pageSize, categoryId
/api/products/{id}GET商品详情,含图文描述与 SKUid
/api/ordersPOST创建订单,扣库存items, addressId
/api/orders/{id}GET查询订单状态与明细id
/api/pay/notifyPOST微信支付结果回调微信支付 XML

这里有两个容易过度设计的地方。第一个是购物车接口,前期完全可以在小程序本地用 Storage 存,结算时把选中的商品一次性提交给 /api/orders,服务端不需要维护购物车表。第二个是支付,真正开发时先从「模拟支付」开始:创建订单后标记为已支付,等整个链路通了再接入微信支付商户号和回调验签。把这个最小集合稳定跑起来,再谈优惠券、秒杀、多商户这些增量需求。

2.3 登录态设计:用 code 换 JWT,而不是把 openid 直接抛给前端

小程序登录是最容易被写坏的一环。常见错误是后端直接把 openid 返回给前端,前端拿着 openid 当身份凭证,这在安全上等于把门钥匙挂在门口。openid 是用户在微信体系里的唯一标识,一旦泄露,别人可以伪造请求直接操作这个用户的订单。

正确流程是:前端 wx.login 拿到临时 code,POST 给后端;后端拿这个 code 去微信的 jscode2session 接口换取 openid 和 session_key;然后用 openid 找到或创建用户记录,签发一个自己的 JWT 返回给小程序。之后前端所有请求带 Authorization 头,后端从 JWT 里解析出用户 ID。session_key 绝不能出服务端,因为它能解密用户的手机号等敏感信息。还有一个细节:微信的 code 五分钟内有效且只能用一次,所以登录接口要做幂等,两次用同一个 code 调用,第二次必然报错,前端发起登录前要避免重复点击。

3. 把 C# 后端跑起来:商品接口、下单扣库存与密钥配置的落地写法

3.1 最小可运行后端:商品列表与详情接口的一个 Controller

后端我用的是 ASP.NET Core Web API + EF Core,这个组合在 .NET 生态里最常见,也是这套源码大概率采用的路线。先贴一个商品列表接口的完整写法,注意它不是教科书式的三层架构,而是把 Controller 和 DbContext 直接对接,MVP 阶段够用。

[ApiController] [Route("api/[controller]")] public class ProductsController : ControllerBase { private readonly AppDbContext _db; public ProductsController(AppDbContext db) { _db = db; } // GET /api/products?page=1&pageSize=20&categoryId=2 [HttpGet] public async Task<IActionResult> List(int page = 1, int pageSize = 20, int categoryId = 0) { var query = _db.Products.Where(p => p.Status == 1); // 只查上架商品 if (categoryId > 0) { query = query.Where(p => p.CategoryId == categoryId); } var total = await query.CountAsync(); var items = await query .OrderByDescending(p => p.SortWeight) .Skip((page - 1) * pageSize) .Take(pageSize) .Select(p => new { p.Id, p.Name, p.Price, p.Stock, p.MainImage, p.Sales }) .ToListAsync(); return Ok(new { total, items }); } }

这段代码里有两个参数值得留意。page 和 pageSize 用默认值兜底,前端传-1之类脏数据时不会直接把数据库拖垮,但仍建议在方法开头加一道 pageSize 最大 50 的限制。查询里用了匿名对象,只返回列表页需要的字段,而不是把 Description 这种大段文本整包丢给小程序,列表页的渲染速度会明显更好。Status == 1 的过滤必须放在最前面,否则下架商品会出现在列表里,这是电商后台最容易出现的低级数据事故。

3.2 下单扣库存:事务包裹 + 条件 UPDATE,防止并发超卖

下单接口是整个商城后端技术含量最高的地方,核心难点在库存扣减。网上大量教程写的是先 SELECT 查一次库存,再 UPDATE 减掉数量,这是教科书写法,也是并发下超卖的标准翻车现场。两个用户同时读到库存还有 1 件,分别执行 UPDATE,库存就变成 -1。正确做法是把扣减条件写进 UPDATE 语句里,让数据库帮我们做原子判断。

[HttpPost("api/orders")] public async Task<IActionResult> Create([FromBody] CreateOrderRequest req) { var userId = GetUserIdFromJwt(User); // 从 JWT 里解出当前用户 Id using var tx = await _db.Database.BeginTransactionAsync(); foreach (var item in req.Items) { // 条件更新:库存足够才扣减,影响行数为 0 说明库存不足或商品已下架 var rows = await _db.Database.ExecuteSqlInterpolatedAsync( "UPDATE Products SET Stock = Stock - {0} WHERE Id = {1} AND Stock >= {0}", item.Quantity, item.ProductId ); if (rows == 0) { await tx.RollbackAsync(); return BadRequest($"商品 {item.ProductId} 库存不足"); } } var order = new Order { UserId = userId, TotalAmount = req.Items.Sum(i => i.Price * i.Quantity), Status = 0, // 0 待支付,1 已支付,2 已发货,3 已完成 CreateTime = DateTime.Now }; _db.Orders.Add(order); await _db.SaveChangesAsync(); await tx.CommitAsync(); return Ok(new { order.Id }); }

这段代码里最关键的是ExecuteSqlInterpolatedAsync。注意花括号里的 {0} 不是字符串拼接占位符,EF Core 会把它转成 SQL 参数,能防注入,这一点比拼字符串安全得多。WHERE Stock >= {0}是超卖的防火墙:并发同时执行这条 UPDATE 时,数据库的行锁会让两个请求串行执行,第二个请求发现库存不够,影响行数为 0,直接回滚整个事务。

下单是典型的跨表操作,扣库存和写订单必须在一个事务里。如果扣了库存但订单没写进去,用户付了钱拿不到货;如果订单写了但库存没扣,超卖又会发生。所以事务的边界要包住「所有库存扣减 + 订单插入」这两步,任何一个失败都整体回滚。还有一个实践细节:订单金额不要信任前端传过来的 totalAmount,后端要用商品表里的价格重新计算,否则用户改一下请求体就能用 1 分钱下单。

3.3 配置管理:数据库连接串与微信密钥从哪读、怎么藏

这套源码里最容易被忽略但上线后最容易出事的是配置管理。AppSecret 是微信小程序后端的核心机密,它要是写死在 appsettings.json 里再提交到 git 仓库,等于把商城后台的钥匙公开发布。正确做法是区分环境:开发环境用 user-secrets 或环境变量,生产环境用部署平台的密钥管理服务,appsettings.json 只留占位符。

{ "ConnectionStrings": { "Default": "Server=localhost;Database=MallDb;User Id=sa;Password=YourPassword;TrustServerCertificate=True;" }, "WeChat": { "AppId": "wxxxxxxxxxxxxxxxxx", "AppSecret": "从环境变量读取,不要写在这里" }, "Jwt": { "Key": "至少32位的随机字符串", "ExpireHours": 72 } }

JWT Key 同样敏感,它是服务端签发和验证 token 的种子,泄露后任何人都能伪造登录态。Key 至少 32 位随机,最好用 GUID 拼接再加几段随机字符串,和生产环境的数据库密码同级对待。另外,微信支付商户号相关的 APIv3 Key 和证书文件也是同样的管理标准,这些配置一旦泄露,轻则被刷接口,重则商户资金受损。把这几个值从代码里拆出去,是这套源码上线前必须做的一道手术。

4. 小程序端把购物闭环串起来:登录、商品列表、购物车到下单一气呵成

4.1 request 统一封装:token 注入与 401 跳转一次搞定

小程序端的第一步不是写页面,而是写一个统一的请求封装。所有 wx.request 调用都走同一个函数,token 注入、错误处理、登录过期跳转这些逻辑只用写一遍。否则每个页面都自己拼 header,改一个接口域名要动几十个文件,那是灾难。

const BASE_URL = 'https://api.example.com'; function request(path, method = 'GET', data = {}) { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + path, method: method, data: data, header: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + (wx.getStorageSync('token') || '') }, success: (res) => { if (res.statusCode === 200) { resolve(res.data); } else if (res.statusCode === 401) { // token 失效,清掉本地登录态并跳转登录页 wx.removeStorageSync('token'); wx.navigateTo({ url: '/pages/login/login' }); reject(new Error('登录已过期')); } else { reject(res); } }, fail: (err) => reject(err) }); }); } module.exports = { request, BASE_URL };

BASE_URL 就是第 5 章要讲的合法域名,必须是小程序后台配置过的 HTTPS 地址。这里头 Authorization 头用Bearer前缀是行业习惯,后端 JWT 解析中间件也认这个格式。401 统一处理是个藏得很深的效率点:没有它,每个页面都要写一遍登录过期判断,有了它,所有接口在 token 过期后行为一致,不会出现有的页面跳登录、有的页面白屏的怪象。

登录接口本身也走这个封装,只是登录的时候还没有 token,所以 header 里 token 为空字符串也没关系,后端登录接口根本不校验 Authorization。

function wxLogin() { return new Promise((resolve, reject) => { wx.login({ success: async (res) => { try { const data = await request('/api/auth/login', 'POST', { code: res.code }); wx.setStorageSync('token', data.token); resolve(data); } catch (e) { reject(e); } }, fail: reject }); }); }

这个流程里 wx.login 拿到的 code 是一次性的,后端换完 token 后 code 立刻失效。所以前端不要在登录失败后拿同一个 code 重试,而是重新调用 wx.login 拿新 code。登录成功后的 token 直接存 Storage,后续请求自动带上,用户下次打开小程序不需要重新登录,JWT 的 72 小时有效期在 2.3 节的配置里有体现。

4.2 商品列表数据流与购物车本地缓存:不登录也能加购

商品列表页的数据流很简单:页面 onLoad 里调 /api/products,拿到 total 和 items,渲染到 WXML,触底时 page +1 再拉下一页。这里容易踩的坑是触底加载时把数据覆盖而不是追加,以及没有处理加载中的重复请求。加一个 isLoading 标志位就能解决。

购物车是这个章节里最有设计感的部分。MVP 阶段我强烈建议用本地缓存实现,而不是建表存服务端。购物车本质上是一个「准购物意图」的暂存区,用户加购时往往还没登录,强行要求登录才能加购会流失大量订单。

const CART_KEY = 'cart_items'; function getCart() { return wx.getStorageSync(CART_KEY) || []; } function addToCart(product, skuId, qty = 1) { const cart = getCart(); const index = cart.findIndex(item => item.skuId === skuId); if (index > -1) { cart[index].qty += qty; } else { cart.push({ productId: product.id, skuId: skuId, name: product.name, price: product.price, image: product.mainImage, qty: qty }); } wx.setStorageSync(CART_KEY, cart); wx.showToast({ title: '已加入购物车', icon: 'success' }); } function clearCart() { wx.removeStorageSync(CART_KEY); } module.exports = { getCart, addToCart, clearCart };

这段代码的细节在 skuId 上。同一个商品可能有多个颜色、尺码,价格可能不同,所以购物车项的标识符必须是 SKU 维度而不是商品维度。否则用户加了一件红色 M 码和一件红色 L 码,会被误判成同一件商品直接合并数量。加购时把 name、price、image 冗余存进购物车,是为了结算页不需要再回查商品列表,少一次接口调用。但注意结算提交订单时,后端必须以数据库里的价格为准重算,本地存的价格只做展示用,防止有人篡改 Storage。

4.3 下单页联动:SKU 选择、地址与订单提交的数据拼装

下单页是前端逻辑最密集的页面,需要同时处理三块数据:SKU 选择、收货地址、订单金额汇总。SKU 选择的多规格联动是一个经典难点,比如选了颜色才显示对应的尺码列表,选完才刷新价格。实现思路是先在后端商品详情接口里返回完整的 SKU 树,前端按维度逐步筛选。

// 商品详情的 SKU 结构示意 skus: [ { skuId: 101, color: '黑', size: 'M', price: 199, stock: 10 }, { skuId: 102, color: '黑', size: 'L', price: 199, stock: 5 }, { skuId: 103, color: '白', size: 'M', price: 209, stock: 0 } ]

前端保存用户选中的 color 和 size,然后从 skus 里找匹配项。匹配不到就提示「该规格暂时缺货」,匹配到就显示对应价格和库存。下单页的提交按钮要拼装成后端 CreateOrderRequest 格式:

function submitOrder() { const cart = getCart(); const items = cart.map(item => ({ productId: item.productId, skuId: item.skuId, quantity: item.qty })); request('/api/orders', 'POST', { items, addressId: selectedAddressId }) .then(res => { clearCart(); wx.navigateTo({ url: '/pages/order-detail/order-detail?id=' + res.id }); }) .catch(err => { wx.showToast({ title: err.message || '下单失败', icon: 'none' }); }); }

地址选择这里有个产品决策:是复用微信的收货地址接口,还是自己做地址管理。wx.chooseAddress 可以直接拉起微信原生地址选择器,用户不用在商城里重新录一遍地址,体验好。但注意它返回的地址信息里包含 name、tel、provinceName 这些字段,提交前要做一层字段映射,转成自己后端 OrderAddress 的格式。另外,如果没有用 wx.chooseAddress,而是商城自建地址本,那就要再加 /api/address 的增删改查接口,这属于 MVP 之后的迭代项,第一次跑通时不建议做进闭环里。

5. 避坑:这套商城源码运行中最常见的 6 个故障与排查记录

5.1 真机请求全部失败,开发者工具却正常:合法域名在作怪

现象:开发者工具里接口全部通,一扫码到真机上所有请求全部 fail,控制台报request:fail url not in domain list。原因:开发者工具可以勾选「不校验合法域名」跳过检查,真机没有这个开关,小程序后台只允许请求配置过的 HTTPS 域名。解决:域名备案并上 HTTPS 证书,登录微信公众平台,在小程序后台的「开发管理 - 开发设置 - 服务器域名」里把接口域名加进 request 合法域名列表。如果商品图片也用了独立域名,还要同步加 downloadFile 合法域名。这个配置改动需要几分钟生效,而且开发环境域名和生产环境域名可以同时配,上限各 20 个,够用。

5.2 库存被扣成负数:先查后更的并发漏洞

现象:运营反馈库存明明设置的是 5 件,订单却成交了 8 件,后台库存变成 -3。原因:下单代码写的是「先 SELECT 查询库存,再 UPDATE 扣减」。两个请求同时读到库存 5,各自认为够卖,各自扣减,最终结果就是负数。解决:把扣减条件写进 UPDATE,即UPDATE Products SET Stock = Stock - @qty WHERE Id = @id AND Stock >= @qty,影响行数为 0 就判定库存不足并回滚。这是我在第 3 章代码里强调条件更新的原因,也是这套源码里最值得优先检查的一行 SQL。改完用并发脚本压一下,多线程同时下单同一个商品,库存最终必须等于初始值减去成功订单数。

5.3 支付成功但订单仍是待支付:回调验签与幂等处理

现象:用户微信支付弹窗显示扣款成功,App 里订单状态还是「待支付」,用户反复找客服。原因:最常见的是支付回调没做好验签就直接信任了通知内容,或者回调处理时更新了订单但返回给微信的响应格式不对,微信认为回调失败会重试,但重试的是同一笔订单,代码里没做幂等,第二次处理时把状态搞乱了。解决:回调接口先验证签名,再拿订单号查订单,如果订单已经是「已支付」就直接返回 success,不做任何更新,这就是幂等。处理完返回微信要求的 XML 格式的 success,否则微信会按策略重试。另外,用户支付过程中息屏或直接切走小程序,回调到达时小程序已经被系统杀掉,前端收不到任何通知。处理办法是订单详情页进入时主动查询一次后端订单状态,用后端状态为准,这比依赖前端回调可靠得多。

5.4 商品图在部分机型不显示:图片域名白名单的坑

现象:同一个商品图片,iOS 上显示正常,部分安卓机型上就是空白。原因:商品图片如果是开发调试时直接用的本地路径或 IP 地址,真机上要么无法访问,要么图片域名没进 downloadFile 合法域名白名单。开发者工具里能看到是因为本地资源不受域名校验管制。解决:图片一律上传到云存储或 OSS,使用 HTTPS 正式域名,并在小程序后台把图片域名加进 downloadFile 白名单。还有一个隐藏点:如果图片链接是 http 而不是 https,iOS 的 ATS 策略会直接拦截,安卓表现反而宽松,所以「iOS 不显示安卓正常」这个方向的问题,优先检查协议是不是 https。

5.5 自定义导航栏顶栏错位:胶囊按钮决定导航高度

现象:改用了自定义导航栏后,在 iPhone 14 Pro Max 上标题偏上,在安卓机型上又偏下,和右上角的胶囊按钮重叠。原因:不同机型状态栏高度不一样,iPhone 的刘海屏和安卓挖孔屏差异明显,把导航栏高度写死成 44px 或 64px 必然错位。解决:动态计算导航栏高度,以胶囊按钮的位置为锚点,这是微信官方推荐的适配方式。

const menu = wx.getMenuButtonBoundingClientRect(); const sys = wx.getSystemInfoSync(); const statusBarHeight = sys.statusBarHeight; const navBarHeight = statusBarHeight + (menu.top - statusBarHeight) * 2 + menu.height;

拿到 navBarHeight 后直接设置自定义导航栏的样式高度。这套计算逻辑对胶囊在左侧的安卓和右侧的 iOS 都通用,因为它不关心胶囊靠哪边,只关心它的 top 和 height。适配完要拿几台不同机型的预览截图对比,iPhone SE、iPhone 14 Pro、小米、华为各看一遍,比在模拟器里调半天都有效。

5.6 AppSecret 进仓库被扒:密钥管理翻车现场

现象:上线第二天,后端日志里出现大量非业务的登录请求,第三方在疯狂调登录接口换 token。原因:AppSecret 写死在 appsettings.json 里,提交到了公开仓库,被人用代码扫描工具直接扒走。AppSecret 泄露后攻击者可以模拟合法小程序后端,换取任意用户的 openid 信息,甚至配合 session_key 解密手机号。解决:立刻在微信公众平台重置 AppSecret,然后按第 3.3 节的方式把密钥全部挪到环境变量或密钥管理服务里。给 git 仓库加 .gitignore 过滤掉 appsettings.Production.json 这类敏感配置文件,团队内部用 user-secrets 管理开发环境密钥。这类问题最坑的点在于它不会立刻报错,等你发现时数据已经被刷了一段时间,所以密钥轮换要当成上线前的固定动作,而不是出了事才补救。

6. 上线前必做的验证清单与两个值得马上投入的改造

上线前按照下面的清单过一遍,能拦住绝大多数线上事故。第一项是域名与白名单:request 和 downloadFile 合法域名是否都配了 HTTPS 正式域名。第二项是登录链路:code 换 token 是否幂等,token 过期后前端是否会跳登录。第三项是库存并发:用脚本模拟 10 个线程同时抢最后一件商品,是否只有一个订单成功。第四项是支付回调:回调验签是否正确,重复通知是否幂等,订单状态是否以后端为准。第五项是弱网体验:小程序在 3G 网络下下单,重复点击提交按钮会不会生成多个订单,前端要加提交中禁用。

两个值得马上投入的改造,我排第一的是管理后台。这套源码通常只带小程序端和 API,运营改商品价格、上下架还要直接改数据库,这是不可接受的。用 ASP.NET Core MVC 或 Blazor 写一个简单的后台,复用同一套 DbContext 和 Models,界面只需要商品管理、订单管理、库存调整三个页面,工作量在一到两周,但运营效率的提升是质变的。第二个是订单状态的自动化通知,用户下单、支付成功、发货这三个节点通过微信订阅消息推送给用户,能明显减少「我的订单怎么还没发货」的客服咨询。

我自己第一次接这类商城项目时,最深刻的教训就是过于迷信源码自带的完整性,拿到手就急着改页面,结果线上首单就遇到库存超卖,查了一整天才发现是「先查后更」的老问题。后来学乖了,任何一套源码到手,第一件事永远是读下单和支付的代码,这两块没跑通,页面做得再漂亮都不敢上线。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询