C# .NET Core 微信小程序商城实战:支付库存订单闭环
简介:这是一套基于C#与.NET Core构建的商用级微信小程序多店铺商城系统,面向中高级开发者及企业级项目学习者,解决电商类小程序从开发到部署的全栈实践需求。资源包含1962个文件,以789个C#后端逻辑文件(如OrderController.cs、UserController.cs)、218个JS前端交互脚本、188个PNG图标资源、147个CSHTML管理后台视图及63个JSON配置文件为主,辅以WXML/WXSS小程序页面结构与样式,整体压缩包仅10.33MB,轻量但功能完备。已有1278人学习下载,体现了其在实战教学与快速落地中的高参考价值。读者可直接获取含三级分销、多店铺运营、物流对接、优惠券与积分体系、插件化扩展能力的完整解决方案,代码结构清晰、模块职责分明,特别适合用于二次开发、架构学习或毕业设计原型搭建。
1. 这不是“又一个小程序模板”:C# + .NET Core 构建的微信小程序商城,真能跑通服务端闭环?
你搜“小程序商城源码”,90% 是 Vue/UniApp 前端配 Node.js 或 PHP 后端,剩下 10% 里,Java 和 Python 占大头。但如果你手头是 Windows 生产环境、团队主攻 C#、已有 SQL Server 或 Oracle 数据库、甚至要对接金蝶/用友 ERP 接口——那这份基于 C# 的小程序商城(原生微信小程序 + .NET Core)就不是“可选项”,而是“少踩三年坑的刚需”。它不是把 Vue 页面套个 .NET Core API 层,而是从微信登录态校验、支付回调验签、商品 SKU 库存原子扣减、订单状态机流转,到后台管理系统的角色权限 RBAC 控制,全部用 C# 实现,且所有接口都按微信小程序官方文档要求做签名、加密、重放校验。我去年在一家医疗器械经销商落地时,直接复用这套结构,把原有 WinForm 上位机团队无缝拉进小程序开发,3 天就跑通扫码下单 → 支付成功 → 打印发货单全流程。适合:.NET 开发者想快速交付小程序商城、企业内部系统需与现有 C# 生态深度集成、对支付/库存/订单一致性有强事务要求的场景。
2. 为什么选 .NET Core 而不是 ASP.NET Framework?技术栈拆解与模块职责
2.1 核心架构图:三层分离 + 微信特有链路
整个项目采用清晰的分层架构:
- Presentation 层(微信小程序端):原生 WXML + WXSS + JavaScript,无任何框架依赖,所有网络请求走
wx.request,严格遵循微信小程序 HTTPS 强制要求,app.js中统一注入token和openid,避免每个页面重复获取。 - API 层(.NET Core Web API):基于 .NET Core 6.0(非 .NET 5 或 7),原因见下文避坑章节;使用
Microsoft.AspNetCore.Mvc.Core统一路由,所有 Controller 继承自WeChatBaseController,该基类自动完成:Authorization头解析并校验 JWT(Token 由小程序code2Session换取后生成)- 微信支付回调验签(SHA256withRSA + 商户证书)
- 敏感操作日志记录(如库存扣减、退款申请)
- Business & Data Layer(C# 业务逻辑 + EF Core):核心业务封装在
Services文件夹,如IOrderService、IInventoryService;数据访问层使用 EF Core 6.0,不使用 Database-First,全部 Code-First + Migration 管理,实体类中大量使用[Column("xxx")]显式映射字段,规避微信字段名(如open_id)与 C# 命名规范冲突。
提示:项目未使用 DDD 分层,但通过
Domain文件夹隔离了领域模型(如OrderAggregateRoot),避免贫血模型。这是为后续接入 Saga 分布式事务预留的扩展点。
2.2 关键技术选型理由:不是跟风,是填坑
| 技术组件 | 选用版本 | 为什么不用其他方案 | 血泪经验 |
|---|---|---|---|
| .NET Core 版本 | 6.0 LTS | 不选 7.0/8.0:微信支付 SDKWxPaySDK官方仅适配至 .NET Core 6.0,升级后RSACryptoServiceProvider被弃用导致验签失败 | 曾试过手动移植验签逻辑,耗时 2 天仍存在签名长度不一致问题,最终回滚 |
| 数据库 ORM | EF Core 6.0 | 不选 Dapper:库存扣减需强事务 + 并发控制,EF Core 的ExecuteUpdateAsync+RowVersion更易实现乐观并发 | Dapper 手写 SQL 易漏WHERE Version = @oldVersion,线上出现超卖 |
| JWT Token 签发 | Microsoft.IdentityModel.Tokens6.22.0 | 不选System.IdentityModel.Tokens.Jwt旧版:新版支持SecurityTokenDescriptor.SigningCredentials显式指定密钥长度,兼容微信aes-128-cbc加密要求 | 旧版默认 RSA-2048,微信解密失败返回invalid aes key |
| 微信登录态管理 | 自研WeChatSessionManager | 不用 IdentityServer4:微信code2Session返回的是openid+session_key,非标准 OAuth2 流程,IS4 配置复杂且增加单点故障点 | IS4 在高并发下 session_key 解密延迟达 800ms,导致小程序白屏 |
2.3 项目目录结构:一眼看懂谁管什么
/src ├── WeChatMiniProgram.API/ # 主 API 项目(.NET Core Web API) │ ├── Controllers/ │ │ ├── AuthController.cs # 登录、code2Session、token 刷新 │ │ ├── ProductController.cs # 商品列表、详情、SKU 查询 │ │ ├── OrderController.cs # 创建订单、支付回调、订单查询 │ │ └── AdminController.cs # 后台管理接口(需 JWT + 角色校验) │ ├── Services/ │ │ ├── IWeChatAuthService.cs # 微信登录态校验服务 │ │ ├── IOrderService.cs # 订单状态机服务(含支付成功后库存扣减) │ │ └── IInventoryService.cs # 库存服务(支持秒杀场景的 Redis+DB 双写) │ ├── Models/ │ │ ├── WeChatLoginResult.cs # code2Session 返回结构体 │ │ └── OrderAggregateRoot.cs # 订单聚合根(含 Address、Items、PaymentInfo) │ └── Filters/ │ └── WeChatSignatureFilter.cs # 全局过滤器:校验微信回调签名 ├── WeChatMiniProgram.Client/ # 微信小程序源码(已编译为 miniprogram/ 目录) │ ├── pages/ │ │ ├── index/index.wxml # 首页(含商品瀑布流 + 下拉刷新) │ │ ├── product/detail.wxml # 商品详情(含规格选择器 + 加入购物车) │ │ └── order/confirm.wxml # 订单确认页(调用微信 JS-SDK 发起支付) │ └── utils/ │ └── request.js # 封装 wx.request,自动添加 Authorization header └── WeChatMiniProgram.Data/ # EF Core DbContext + Migration ├── ApplicationDbContext.cs └── Migrations/ # 包含 20230515182233_Init.cs 等 7 个迁移脚本注意:WeChatMiniProgram.Client是完整的小程序源码,不是 npm 包或构建产物,可直接导入微信开发者工具运行;WeChatMiniProgram.API需自行发布为 IIS 应用或 Docker 容器。
3. 从零部署:三步跑通本地调试环境(含微信配置)
3.1 后端:.NET Core 项目启动与数据库初始化
确保已安装 .NET SDK 6.0(dotnet --version输出6.0.400或更高)。进入WeChatMiniProgram.API目录:
# 1. 还原 NuGet 包(关键:必须用 .NET 6 SDK) dotnet restore # 2. 执行数据库迁移(首次运行会创建表结构) dotnet ef database update --project ../WeChatMiniProgram.Data/WeChatMiniProgram.Data.csproj # 3. 启动 API(监听 https://localhost:5001) dotnet run逻辑说明:
dotnet ef database update会读取WeChatMiniProgram.Data项目的ApplicationDbContext,执行所有未应用的 Migration 脚本。若报错The specified framework version '6.0' could not be found,请确认全局安装的是 .NET 6 SDK(非仅 Runtime)。
参数说明:
--project参数指定 DbContext 所在项目路径,避免因解决方案多项目导致上下文识别错误;- 若需指定连接字符串,修改
appsettings.Development.json中的"ConnectionStrings": { "DefaultConnection": "Server=.;Database=WeChatMall;Trusted_Connection=true;" },支持 SQL Server、PostgreSQL(需更换 EF Core Provider)。
3.2 前端:微信小程序开发者工具配置要点
将WeChatMiniProgram.Client目录拖入微信开发者工具(必须使用稳定版 v1.05.2305161,不支持 Nightly 版):
- AppID 配置:在
project.config.json中修改"appid": "wx1234567890abcdef"为你自己的小程序 AppID; - 域名配置:在
app.js第 12 行const API_BASE_URL = 'https://localhost:5001/api';——开发阶段允许 localhost,但必须在「开发管理 > 开发者工具」中勾选「不校验合法域名」; - HTTPS 代理:因 API 使用 HTTPS,需在开发者工具设置中开启「启用 HTTPS 代理」,否则
wx.request报request:fail ssl hand shake error; - 登录调试:首次打开
index页面,会自动调用wx.login()获取code,然后 POST 到/api/auth/login,返回access_token存入wx.setStorageSync。
注意:小程序端所有接口请求头自动携带
Authorization: Bearer <token>,由utils/request.js统一封装,无需每个页面手动设置。
3.3 微信侧关键配置:三个地方必须改,否则支付/登录全挂
微信公众平台 → 开发管理 → 开发设置
- 服务器域名:添加
https://your-domain.com(生产环境)或留空(开发环境); request合法域名:添加你的 API 域名(如https://api.yourmall.com);- JS接口安全域名:必须填写小程序前端所在域名(如
https://mp.weixin.qq.com不行,要填你备案的域名)。
- 服务器域名:添加
微信支付商户平台 → 账户中心 → API安全
- 设置 APIv3 密钥(32位随机字符串);
- 上传 API 证书(
apiclient_cert.p12),注意:此证书需从商户平台下载后用密码解压,再转为.pem格式供 .NET Core 使用; - 在
appsettings.json中配置:"WeChatPay": { "MchId": "1900000109", "AppId": "wx8888888888888888", "ApiV3Key": "your-32-char-api-v3-key-here", "CertPath": "./cert/apiclient_cert.pem", "CertPassword": "your-p12-password" }
小程序管理后台 → 功能推广 → 微信支付
- 必须提交「小程序支付」类目(如“电商-综合电商”),否则
wx.requestPayment调用直接失败,错误码10002; - 审核通过前,可用测试号调通流程,但无法真实支付。
- 必须提交「小程序支付」类目(如“电商-综合电商”),否则
4. 避坑:微信小程序 + .NET Core 商城最常翻车的 5 个点
4.1 现象:小程序调用/api/auth/login返回 401,但code确认有效
原因:WeChatSessionManager中GetSessionKeyAsync方法调用微信sns/jscode2session接口时,未正确 urlencodeappid和secret,导致微信返回{"errcode":40013,"errmsg":"invalid appid"},但项目日志未捕获该响应,直接抛出 401。
解决:检查WeChatSessionManager.cs第 47 行,确保HttpClient.PostAsync的 URL 构造使用Uri.EscapeDataString():
var url = $"https://api.weixin.qq.com/sns/jscode2session?" + $"appid={Uri.EscapeDataString(_options.AppId)}&" + $"secret={Uri.EscapeDataString(_options.AppSecret)}&" + $"js_code={code}&grant_type=authorization_code";4.2 现象:支付成功后,订单状态仍是 “待支付”,库存未扣减
原因:微信支付回调地址/api/order/paynotify被配置为 HTTP(非 HTTPS),微信服务器拒绝推送;或回调接口未加[HttpPost]特性,导致路由匹配失败,返回 404。
解决:
- 确保
Startup.cs中app.UseHttpsRedirection()已启用; PayNotifyController.cs必须有[Route("api/[controller]")]和[HttpPost("paynotify")];- 在
appsettings.json中配置WeChatPay:NotifyUrl: "https://your-domain.com/api/order/paynotify",不能带 query string。
4.3 现象:商品列表页下拉刷新后,新数据覆盖旧数据,而非追加
原因:小程序端onPullDownRefresh中调用getProducts时,未重置this.data.products = [],导致setData({ products: [...old, ...new] })逻辑缺失。
解决:修改pages/index/index.js的onPullDownRefresh:
onPullDownRefresh() { this.setData({ products: [] }); // 关键:清空旧数据 this.getProducts().then(() => { wx.stopPullDownRefresh(); }); }4.4 现象:后台管理系统登录后,部分菜单不显示,AdminMenuController返回空数组
原因:AdminController中GetMenus方法使用User.IsInRole("Admin")判断,但 JWT Token 中未包含roleclaim,EF Core 查询时Where(x => x.Role == "Admin")永远为 false。
解决:在AuthController.Login方法中,生成 Token 时显式添加 role:
var claims = new List<Claim> { new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()), new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), new Claim(ClaimTypes.Role, "Admin") // 必须加这行! };4.5 现象:Docker 部署后,dotnet ef database update报错Unable to connect to any of the specified MySQL hosts
原因:Dockerfile中ENTRYPOINT ["dotnet", "WeChatMiniProgram.API.dll"]启动时,EF Core Migration 尚未执行,且容器内 DNS 解析不到宿主机数据库。
解决:
- 方案一(推荐):在
Dockerfile中添加RUN dotnet ef database update --project ../WeChatMiniProgram.Data/; - 方案二:使用
docker-compose.yml定义depends_on+healthcheck,确保数据库就绪后再启动 API; - 方案三:将 Migration 脚本导出为 SQL,在容器启动脚本中
mysql -h db -u root -p$PASS < migration.sql。
5. 进阶技巧:如何让库存扣减真正“不超卖”?Redis + EF Core 双校验实战
微信小程序商城最怕的不是界面卡顿,而是“100 件库存,1000 人同时下单,结果卖出 120 件”。这份源码的IInventoryService.DecreaseStockAsync方法,实现了工业级库存控制,不是简单UPDATE SET stock = stock - 1。
5.1 双校验流程:为什么只靠数据库锁不够?
高并发下,单纯依赖数据库行锁(如SELECT ... FOR UPDATE)会导致:
- 锁等待时间长,请求堆积;
- 若事务未及时提交,锁持有时间不可控;
- Redis 缓存穿透风险(缓存失效瞬间大量请求打穿 DB)。
因此本项目采用“Redis 预减 + DB 终审”两段式校验:
- Redis 预减:用
DECRBY sku:1001:stock 1原子操作,若返回值 ≥ 0,进入下一步;否则直接返回“库存不足”; - DB 终审:查 DB 当前库存,若
stock >= 1,则执行UPDATE ... SET stock = stock - 1 WHERE id = 1001 AND stock >= 1,利用AND stock >= 1作为乐观锁条件; - Redis 回补:若 DB 更新失败(影响行数为 0),则
INCRBY sku:1001:stock 1回退预减量。
5.2 核心代码:InventoryService.cs关键片段
public async Task<bool> DecreaseStockAsync(int skuId, int quantity = 1) { var redisKey = $"sku:{skuId}:stock"; // Step 1: Redis 预减(原子操作) var remaining = await _redisDatabase.DecrementAsync(redisKey, quantity); if (remaining < 0) { // 预减失败,立即回退 await _redisDatabase.IncrementAsync(redisKey, quantity); return false; } // Step 2: DB 终审(乐观锁) using var transaction = await _context.Database.BeginTransactionAsync(); try { var sku = await _context.Skus .FirstOrDefaultAsync(x => x.Id == skuId && x.Stock >= quantity); if (sku == null) { // DB 库存不足,回退 Redis await _redisDatabase.IncrementAsync(redisKey, quantity); await transaction.RollbackAsync(); return false; } sku.Stock -= quantity; _context.Skus.Update(sku); await _context.SaveChangesAsync(); await transaction.CommitAsync(); return true; } catch { await transaction.RollbackAsync(); throw; } }参数说明:
quantity默认为 1,支持批量扣减(如套餐商品);_redisDatabase使用StackExchange.Redis,连接字符串从appsettings.json读取;_context是 EF Core DbContext。
5.3 Redis 缓存策略:不是所有数据都该缓存
| 数据类型 | 是否缓存 | TTL(秒) | 更新时机 | 原因 |
|---|---|---|---|---|
| SKU 库存 | ✅ 强制缓存 | 300(5分钟) | 每次扣减/补货后SET sku:1001:stock <value> | 高频读,低频写,容忍短暂不一致 |
| 商品详情 | ✅ 缓存 | 3600(1小时) | 后台编辑商品后DEL product:1001 | 减轻 DB 压力,详情页加载快 |
| 用户订单列表 | ❌ 不缓存 | — | — | 每个用户数据隔离,缓存 key 爆炸,且需实时性 |
| 微信 session_key | ✅ 缓存 | 7200(2小时) | code2Session成功后SET session:xxx <key> | 微信官方有效期为 2 小时,精准匹配 |
5.4 压测验证:用 wrk 模拟 1000 并发下单
在 Linux 服务器上安装wrk,执行以下命令压测库存接口:
# 模拟 1000 并发,持续 30 秒,POST 到扣减接口 wrk -t12 -c1000 -d30s -H "Authorization: Bearer your-jwt-token" \ -s post-stock.lua http://localhost:5001/api/inventory/decreasestock # post-stock.lua 内容: wrk.method = "POST" wrk.body = '{"skuId":1001,"quantity":1}' wrk.headers["Content-Type"] = "application/json"预期结果:
- 成功率 100%(无超卖);
- 99% 请求延迟 < 200ms;
- Redis
INFO显示instantaneous_ops_per_sec峰值 > 5000。
从那以后我每次上线新商城,都会在预发环境跑一遍这个
wrk脚本,哪怕只是改了个按钮颜色——因为库存逻辑一旦出错,就是真金白银的损失。现在我的习惯是:改完任何和InventoryService相关的代码,第一件事就是删掉本地 Redis 所有sku:*key,再跑三轮压测。希望帮到你。
本文还有配套的精品资源,点击获取