ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于C#与NetCore的微信小程序商城开发实战与避坑指南

基于C#与NetCore的微信小程序商城开发实战与避坑指南 简介这份资源是一套基于原生微信小程序与.NET CoreC#构建的商用级多店铺商城系统源码适合需要快速搭建小程序电商平台的中高级开发人员、独立开发者或中小企业技术团队参考。项目覆盖多店铺商城、三级分销、微信小程序端、管理后台、物流配送、优惠券、积分、促销及插件管理等完整业务模块后台采用C#语言开发整体架构清晰可直接作为二次开发或学习模板。压缩包共1962个文件大小约10.33MB主要包含789个C#源码文件、218个JS文件、147个cshtml视图文件、43个wxml与40个wxss小程序前端文件以及SQL、配置、DLL等支持文件可覆盖后端逻辑、前端页面、数据库脚本和部署配置。已有1278人学习下载。通过源码可掌握订单处理、用户/商品管理、插件管理、导出管理等核心实现并理解小程序商城前后端对接方式适合用于毕业设计、商业项目起步或技术栈迁移参考。1. 拿到这套 C# 小程序商城源码先想清楚它替你解决了什么你手上这份「基于 C# 的小程序商城原生微信小程序 NetCore 技术构建.zip」拆开看其实就是两条技术线前端是微信官方原生小程序语法WXML / WXSS / JS后端是 .NET Core 的 Web API。它解决的问题很具体——你的团队有 C# 存量经验或者公司服务器已经是 Windows SQL Server 的体系你不想为一个小程序商城再引入一套 Java 或 Node 技术栈那么用 NetCore 撑起商品、订单、支付、登录这一整套 REST API小程序端不借助 uni-app 这类跨端框架直接用原生语法对接调试链最短出问题也好定位。这套方案适合三类人一是 C# 后端工程师想接私活或做公司内部商城需要一套能讲清楚前后端数据契约的参考实现二是已经有 Web 端商城、想快速开出微信小程序入口的团队后端接口可以复用三是想脱离第三方 SaaS 商城模板、把交易数据握在自己手里的开发者。很多人纠结「现在新项目是不是应该上 uni-app Vue3」但如果你后端是 C#原生小程序反而是更稳的选择——你不需要维护两层语法映射微信开发者工具里看到的就是微信真正在跑的东西。接下来我从架构选型、后端接口、小程序对接、支付与库存的坑一路讲到上线前的验证手段。2. 架构与选型为什么是原生微信小程序 NetCore这六个决定不能拍脑袋2.1 用 NetCore 而不是 .NET Framework不只是跨平台问题很多 C# 老手看到「小程序商城」第一反应是用 WebForm 或 MVC 5 快速套页面但小程序后端根本没有页面只有 API所以这个选择基本没有悬念.NET Core 3.1 或 .NET 6/7/8 的 Web API 项目。NetCore 在这里的优势有三个层面。第一是部署形态小程序后端通常跑在云服务器 Linux 上Core 可以发布成单个 exe 或单文件包不需要在服务器上装完整 .NET Framework 和 IIS第二是性能Core 的 Kestrel 服务器在裸 API 场景下的吞吐量比 Framework 的 IIS 托管高不少商城在下单峰值时 API 就是生命线第三是依赖注入和中间件管线已经是内置一等公民后面做登录鉴权过滤器、接口耗时日志都比你自己在 Framework 里造轮子顺手。// Program.cs 最小骨架.NET 6/7/8 风格 var builder WebApplication.CreateBuilder(args); // 小程序端请求来自微信客户端跨域不是主要矛盾但开发调试时用得上 builder.Services.AddCors(options { options.AddPolicy(MiniApp, policy { policy.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader(); }); }); builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); var app builder.Build(); app.UseCors(MiniApp); app.UseAuthorization(); app.MapControllers(); app.Run();这段代码里你真正要关注的是 CORS 策略。小程序端的 wx.request 不受浏览器同源策略限制所以 CORS 不是给小程序用的是给 Swagger 调试页和内部管理后台用的。生产环境如果只有小程序访问AllowAnyOrigin 没问题但如果后续你要开放 H5 端就得收紧成白名单域名。另外 Swagger 建议只在 Development 环境暴露发布到生产前要么注释掉要么用app.Environment.IsDevelopment()包一层否则你的接口结构等于公开给所有人看。2.2 数据模型商品、SKU、订单、支付回调四张表怎么设计才够用商城系统的数据模型比普通 CRUD 复杂在「SKU 维度」和「订单状态机」上。商品是抽象概念SKU 才是用户真正下单的东西——一件衣服有红色/M 码和黑色/L 码两个 SKU价格和库存都挂在 SKU 上而不是挂在商品上。这个设计如果一开始做错后面加购物车、下单、库存扣减全部要返工。-- 商品表只放公共属性 CREATE TABLE Products ( Id BIGINT IDENTITY(1,1) PRIMARY KEY, Title NVARCHAR(100) NOT NULL, Description NVARCHAR(MAX), MainImageUrl NVARCHAR(500), Status INT NOT NULL DEFAULT 1, -- 1上架 0下架 CreatedAt DATETIME2 NOT NULL DEFAULT GETDATE() ); -- SKU 表价格、库存、规格维度都在这 CREATE TABLE ProductSkus ( Id BIGINT IDENTITY(1,1) PRIMARY KEY, ProductId BIGINT NOT NULL, SpecJson NVARCHAR(200) NOT NULL, -- 如 [{key:颜色,value:红},{key:尺码,value:M}] Price DECIMAL(10,2) NOT NULL, -- 实付价单位元 Stock INT NOT NULL DEFAULT 0, Version INT NOT NULL DEFAULT 0, -- 乐观锁版本号扣库存用 FOREIGN KEY (ProductId) REFERENCES Products(Id) ); -- 订单主表一个订单可能包含多个 SKU CREATE TABLE Orders ( Id BIGINT IDENTITY(1,1) PRIMARY KEY, OrderNo VARCHAR(32) NOT NULL UNIQUE, -- 业务订单号展示给用户 UserId BIGINT NOT NULL, TotalAmount DECIMAL(10,2) NOT NULL, Status INT NOT NULL DEFAULT 0, -- 0待支付 1已支付 2已发货 3已完成 4已取消 CreatedAt DATETIME2 NOT NULL DEFAULT GETDATE(), PaidAt DATETIME2 NULL ); -- 订单明细表记录下单那一刻的商品快照 CREATE TABLE OrderItems ( Id BIGINT IDENTITY(1,1) PRIMARY KEY, OrderId BIGINT NOT NULL, SkuId BIGINT NOT NULL, ProductTitle NVARCHAR(100) NOT NULL, -- 冗余商品标题防止商品改名影响历史订单 SpecJson NVARCHAR(200) NOT NULL, UnitPrice DECIMAL(10,2) NOT NULL, Quantity INT NOT NULL ); -- 支付回调表微信支付回调必须落库防止丢单 CREATE TABLE PaymentCallbacks ( Id BIGINT IDENTITY(1,1) PRIMARY KEY, OrderNo VARCHAR(32) NOT NULL, TransactionId VARCHAR(64) NOT NULL, -- 微信支付单号 RawBody NVARCHAR(MAX), -- 完整回调报文排障用 Processed INT NOT NULL DEFAULT 0, -- 0未处理 1已处理 CreatedAt DATETIME2 NOT NULL DEFAULT GETDATE() );四张表的核心逻辑在三处。第一OrderItems冗余了商品标题和规格快照这是必须的——商品可以改价、改名、下架但历史订单的结算记录不能被篡改。第二ProductSkus里的Version字段是乐观锁扣库存时用UPDATE ProductSkus SET Stock Stock - 1, Version Version 1 WHERE Id skuId AND Stock 0这是防止超卖的最简单手段后面避坑章节我会展开讲。第三支付回调单独建表先存原始报文再异步处理——微信回调可能重复推送你不落库直接改订单状态收到两次回调就会出现重复发货或状态错乱。2.3 后端项目分层Controller 薄、Service 厚、Repository 看情况拿到源码后先看它的项目结构常见的合理拆法是一个 Web API 启动项目一个 Application 层放业务逻辑和服务接口一个 Domain/Entities 层放 EF Core 实体一个 Infrastructure 层放数据库访问。如果源码里所有代码挤在一个 Controller 里你要有心理准备这属于「demo 级工程」而不是「商城级工程」。但也不必迷信分层小程序商城这种体量三层足够不要让泛型仓储和 UnitOfWork 这些模式把简单事搞复杂。/Shop.Api - Controllers、Program.cs、appsettings.json /Shop.Application - Services、Dtos、Interfaces /Shop.Domain - Entities、Enums /Shop.Infrastructure - DbContext、Migrations我个人做这个体量项目时Controller 里只做三件事取参数、调 Service、返回统一响应体。统一响应体长这样{ code: 0, msg: ok, data: { } }。code 为 0 表示成功非 0 是业务错误码。这里的「0 成功」约定要全项目统一不要在登录接口返回success: true在商品接口又返回status: 1小程序端封装的请求函数会因为你接口风格不统一而写出一堆 if else。3. 后端实战从空项目到能用的登录、商品与下单接口3.1 微信登录code2Session 换 openid再签发你自己的 JWT小程序商城没有账号密码体系用户的身份来源是微信。整个过程是小程序端wx.login()拿到临时 code把这个 code 传给你的后端后端拿着 code、小程序 appid、appsecret 去请求微信的https://api.weixin.qq.com/sns/jscode2session接口换回 openid 和 session_key之后你不再依赖微信自己用 JWT 给这个小程序用户签发登录态。// WeChatAuthService.cs 核心代码 public async Taskstring Code2SessionAsync(string code) { var appid _configuration[WeChat:AppId]; var secret _configuration[WeChat:AppSecret]; var url $https://api.weixin.qq.com/sns/jscode2session?appid{appid}secret{secret}js_code{code}grant_typeauthorization_code; using var http new HttpClient(); var json await http.GetStringAsync(url); var result JsonSerializer.DeserializeCode2SessionResult(json); if (result.ErrCode ! 0) { // 常见错误码40029 code 无效45011 接口调用太频繁 throw new BusinessException($微信登录失败: {result.ErrCode} {result.ErrMsg}); } // 用 openid 查用户没有就自动注册 var user await _userRepo.GetByOpenIdAsync(result.OpenId); if (user null) { user new User { OpenId result.OpenId, Nickname 微信用户, CreatedAt DateTime.Now }; await _userRepo.AddAsync(user); } // 签发 JWT注意 secret 必须足够长至少 32 字节 var token GenerateJwt(user.Id, user.OpenId); return token; } private string GenerateJwt(long userId, string openId) { var key new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration[Jwt:Secret])); var creds new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var claims new[] { new Claim(ClaimTypes.NameIdentifier, userId.ToString()), new Claim(openid, openId) }; var token new JwtSecurityToken( issuer: _configuration[Jwt:Issuer], audience: _configuration[Jwt:Audience], claims: claims, expires: DateTime.Now.AddDays(7), signingCredentials: creds); return new JwtSecurityTokenHandler().WriteToken(token); }这段代码有两个参数值得你改。expires我一般设为 7 天小程序商城不是后台管理系统用户每天打开的频率高7 天登录态可以减少刷新次数。小程序端 wx.login 的 code 有效期为 5 分钟且只能使用一次所以Code2SessionAsync被调用后如果失败了前端要重新wx.login()拿新 code。还要注意 appsecret 绝不能被小程序端拿到——你所有敏感配置只放在后端小程序端拿 code 换 token 的接口要多留一条验签逻辑至少不能让陌生人随便刷这个接口消耗你的微信接口配额。3.2 手机号快速验证组件前后端如何配合拿到用户手机号小程序商城在下单、发货通知、售后环节都需要手机号。微信提供了一个「手机号快速验证组件」它的核心思路是用户在前端点击授权后微信返回一个code注意不是手机号本身后端拿这个 code 调用微信接口或结合 session_key 解密才能得到真实手机号。这个 code 是一次性的而且有时效你不能存下来复用。// 小程序前端 wxml 中的按钮写法 button open-typegetPhoneNumber bindgetphonenumberonGetPhoneNumber获取手机号/button// 小程序前端 js 部分 async onGetPhoneNumber(e) { if (!e.detail.code) { wx.showToast({ title: 你取消了授权, icon: none }); return; } // 把手机号 code 传给后端注意这个 code 不是 wx.login 的 code const res await wx.request({ url: https://api.yourdomain.com/api/user/phone, method: POST, data: { phoneCode: e.detail.code }, header: { Authorization: Bearer ${wx.getStorageSync(token)} } }); if (res.data.code 0) { wx.setStorageSync(userPhone, res.data.data.phone); } }// 后端接收手机号 code 并换取手机号 [HttpPost(api/user/phone)] [Authorize] public async TaskIActionResult BindPhone([FromBody] BindPhoneRequest req) { var openId User.FindFirst(openid)?.Value; // 从缓存里拿 session_key登录时存下来的 var sessionKey await _redis.GetAsync($session:{openId}); var url $https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token{await GetAccessTokenAsync()}; var body JsonSerializer.Serialize(new { code req.PhoneCode }); // 调微信接口返回的 phone_info 里就是真实手机号 var result await _httpClient.PostAsync(url, new StringContent(body, Encoding.UTF8, application/json)); var phone result.phone_info.phoneNumber; await _userRepo.UpdatePhoneAsync(openId, phone); return Ok(new { phone }); }这里最大的坑是老版本的「微信小程序登录获取手机号」用的是getPhoneNumber返回encryptedDataiv要靠 session_key 解密新版本微信已经收敛为code换取接口的模式不再推荐自己解密。你拿到源码后先看它用的是哪一种如果是encryptedData解密的老方案建议升级成新接口因为老方案在 2023 年后新注册的小程序已经陆续收紧了权限。还有一点手机号是敏感信息后端返回手机号时在小程序端展示要做脱敏比如138****1234。3.3 商品列表与下单接口分页参数和幂等键是检验源码质量的试金石商品列表没什么玄学但分页参数设计能看出源码是不是能直接生产用。我要求商品列表接口至少支持page和pageSize返回里带total排序按created_at desc或销量倒序。如果源码里的商品接口是GetAllProducts()一次性把全表返回它在 demo 阶段能跑上线后商品过千就会卡。下单接口是整套系统的核心它要解决的问题是「前端多点了一次提交按钮会不会生成两笔订单」。常见做法是前端生成一个clientTokenUUID后端在创建订单前先看这个 token 有没有被用过。[HttpPost(api/order/create)] [Authorize] public async TaskIActionResult CreateOrder([FromBody] CreateOrderRequest req) { // 幂等校验同一个 clientToken 只允许创建一个订单 var exist await _orderRepo.GetByClientTokenAsync(req.ClientToken); if (exist ! null) return Ok(exist); // 直接返回已有订单不报错 // 1. 锁定/校验库存 foreach (var item in req.Items) { var effected await _db.ProductSkus .Where(s s.Id item.SkuId s.Stock item.Quantity) .ExecuteUpdateAsync(s s.SetProperty(p p.Stock, p p.Stock - item.Quantity)); if (effected 0) throw new BusinessException($SKU {item.SkuId} 库存不足); } // 2. 创建订单主记录 明细 var orderNo GenerateOrderNo(); // 如 20250101120000123 随机数 var order new Order { OrderNo orderNo, UserId CurrentUserId, TotalAmount req.Items.Sum(i i.Price * i.Quantity), Status OrderStatus.PendingPayment, ClientToken req.ClientToken }; _db.Orders.Add(order); var detailItems req.Items.Select(i new OrderItem { ... }).ToList(); _db.OrderItems.AddRange(detailItems); // 3. 事务提交库存扣减和订单创建必须同生共死 await _db.SaveChangesAsync(); return Ok(new { orderNo order.OrderNo, payParams await PrepareWeChatPayAsync(order) }); }代码里的关键参数有三个。ClientToken就是幂等键前端每次进入结算页生成一个 UUID提交时带上来后端查重。ExecuteUpdateAsync是 EF Core 7 的原子更新写法它生成的 SQL 是UPDATE ... WHERE SkuId ... AND Stock ...并发下不会超卖。GenerateOrderNo()我建议用DateTime.Now.ToString(yyyyMMddHHmmss) Random.Shared.Next(1000, 9999)别用数据库自增 Id 当订单号——订单号要展示给用户太长且暴露销量信息也别用 Guid用户没法念。4. 原生小程序端请求层、登录态与商城页面的最小实现4.1 wx.request 封装统一处理 token、错误码和登录过期原生小程序没有 axios所有请求都走wx.request如果你每个页面都写一遍wx.request后面前端会重到没法维护。我拿到源码会第一时间看它有没有做请求层封装——一个独立的request.js里面统一处理四件事拼接 baseURL、自动带 Authorization 头、业务错误码提示、401 时自动跳登录。// utils/request.js const BASE_URL https://api.yourdomain.com; function request(path, method GET, data {}) { return new Promise((resolve, reject) { const token wx.getStorageSync(token); wx.request({ url: ${BASE_URL}${path}, method, data, header: { Content-Type: application/json, Authorization: token ? Bearer ${token} : }, success(res) { const body res.data; // 后端统一返回 { code, msg, data } if (body.code 0) { resolve(body.data); } else if (body.code 401) { // 登录过期清掉本地 token跳登录页 wx.removeStorageSync(token); wx.navigateTo({ url: /pages/login/login }); reject(body); } else { wx.showToast({ title: body.msg || 请求失败, icon: none }); reject(body); } }, fail(err) { // 网络不通或域名不在白名单这里最容易踩坑 wx.showToast({ title: 网络异常请检查域名配置, icon: none }); reject(err); } }); }); } module.exports { request, get: (p) request(p, GET), post: (p, d) request(p, POST, d) };这段封装里BASE_URL在生产环境必须是你在微信公众平台「开发管理 - 服务器域名」里配置过的 HTTPS 域名而且不能带路径前缀https://api.yourdomain.com这个域名下所有路径都被允许。fail回调里提示「网络异常」太笼统真实开发时 90% 的原因是开发工具里没勾选「不校验合法域名」、正式版本域名没备案、或 SSL 证书链不完整。你调试时勾选不校验域名能通真机预览时又失败先查这三项。4.2 自定义顶部导航iPhone 刘海屏适配不能写死高度小程序商城页面通常要自定义顶部导航栏因为系统导航栏无法放搜索框和胶囊按钮。这里有个老生常谈但依然有人翻车的点导航栏高度不是写死 44px 或 64px「微信小程序顶部导航栏高度」由状态栏高度wx.getSystemInfoSync().statusBarHeight 导航栏本身高度组成不同机型差异很大。// app.js 或导航组件中 const sysInfo wx.getWindowInfo ? wx.getWindowInfo() : wx.getSystemInfoSync(); const statusBarHeight sysInfo.statusBarHeight; // 状态栏高度如 20/44/47 const navbarHeight statusBarHeight 44; // 44 是导航栏内容区大致高度 // 在自定义导航组件里这样用 this.setData({ statusBarHeight, navbarHeight, capsulePosition: wx.getMenuButtonBoundingClientRect() // 胶囊按钮位置用于对齐右侧 });关键在wx.getMenuButtonBoundingClientRect()这个方法——它返回胶囊按钮就是右上角那三个点的准确位置你的自定义导航按钮和标题要和胶囊对齐就必须动态获取。iOS 常见的状态栏高度是 47px灵动岛机型更离谱安卓主流是 20-30px你写死任何一个都会在某类机型上出现标题偏上或按钮重叠。这套参数在真机预览前一定要拿 iPhone 14/15 系列和一台老安卓对比测一下。4.3 商品详情页与 SKU 选择让前端联动后端库存而不是各说各话商城前端最复杂的交互不是列表页而是商品详情页的 SKU 选择——颜色、尺码、套餐多维交叉选完要实时显示对应价格和库存。小程序端的常见做法是后端把 SKU 列表一次性返回前端用「规格矩阵」做联动计算。// 商品详情页的核心数据结构 data: { product: {}, skuList: [ { id: 1, specJson: [{key:颜色, value:红}, {key:尺码, value:M}], price: 99.0, stock: 20 }, { id: 2, specJson: [{key:颜色, value:红}, {key:尺码, value:L}], price: 99.0, stock: 0 }, { id: 3, specJson: [{key:颜色, value:黑}, {key:尺码, value:M}], price: 109.0, stock: 15 } ], selectedSpec: {}, // 用户点选的 { 颜色: 红, 尺码: M } currentSku: null // 匹配到的完整 SKU } // 当用户点击任一规格值时重新匹配 SKU matchSku() { const { skuList, selectedSpec } this.data; // 后端返回的 specJson 已经结构化为对象数组这里用 find 做全匹配 const matched skuList.find(sku { const specObj sku.specJson.reduce((acc, item) { acc[item.key] item.value; return acc; }, {}); return Object.keys(selectedSpec).every(k selectedSpec[k] specObj[k]); }); this.setData({ currentSku: matched || null }); }前端逻辑里有个必踩的坑库存为 0 的 SKU 你能匹配到 matchSku但用户不能点击「立即购买」按钮。所以currentSku匹配后还要判断stock 0库存不足时前端直接置灰按钮别让用户走到下单接口才被告知失败。还要注意selectedSpec初始为空对象时every([])会返回 true会匹配到第一个 SKU需要加一个「是否已选全所有规格维度」的判断通常是比较Object.keys(selectedSpec).length 规格维度数。5. 实战避坑支付调不起来、库存超卖、真机白屏这三关最常翻车5.1 支付调不起来报错requestPayment:fail的排查顺序现象开发工具里点「立即支付」弹不起微信支付面板真机预览同样不行控制台报错requestPayment:fail。原因排查按顺序来。第一下单接口返回的payParams必须是timeStamp、nonceStr、package、signType、paySign五个字段缺一个或字段名对不上比如把package传成packages微信直接拒调。第二paySign的签名算法官方要求用 HMAC-SHA256但很多老代码用了 MD5商户平台如果没开启 MD5 权限就会失败。第三最常见的坑是你拿的是小程序 appid 去调微信支付统一下单但商户号mch_id和小程序 appid 没有在微信商户平台完成绑定授权。第四开发工具默认模拟器对支付支持不完整一定要真机预览测。解决先打开微信开发者工具的「真机调试」用vConsole打印wx.requestPayment的入参逐一核对字段名是否严格一致再检查商户平台「产品中心 - AppID 账号管理」里有没有绑定你这个小程序的 appid最后确认统一下单接口传的body里没有特殊字符——商品名里如果有 emoji 或超长文本某些支付通道会报签名错误。5.2 支付回调重复推送同一笔订单被标记两次已支付现象用户支付成功后订单状态变成已支付但后台日志发现回调接口被微信调用了两次微信官方机制就是会重复通知最多 24 小时内多次如果代码里没有幂等保护会把订单状态从已支付又改成已发货或重复加积分。原因微信支付回调的「通知」不是一次性的接收方没有返回成功应答{code:SUCCESS}时微信会重试。而很多初版代码是「收到回调 - 直接改订单状态 - 返回给微信 success」看起来逻辑没问题但两条请求并发到达时两个线程同时读到「待支付」的订单都执行了更新可能造成状态覆盖。解决两步走。第一回调处理加锁或依赖数据库状态流转校验比如UPDATE Orders SET Status 1 WHERE OrderNo orderNo AND Status 0受影响行数为 0 说明已被处理过直接忽略。第二先写PaymentCallbacks表再处理业务处理完把Processed置 1重复回调先查这张表已经处理过就不再走业务逻辑但依然要返回微信 success——否则微信会一直重试。我见过的生产事故里还有一种是回调里直接用了context.Request.Body没重置流位置导致验签时读不到原始报文这是 .NET 里读 request body 的经典坑。5.3 库存超卖为什么加了Stock 0判断还是不顶用现象秒杀活动时100 件商品卖出了 130 单数据库里库存变成负数。原因很多人写的扣库存代码是「先 SELECT 查库存判断大于 0再 UPDATE 减库存」。这在并发下是废的——两个请求同时查到库存是 1都判定可以卖各自执行 UPDATE最终库存变成 -1。这是典型的「检查-然后-执行」竞态。解决把检查和扣减合并成一条原子的 UPDATE 语句也就是第 2 章建表时我加的Version乐观锁或Stock 0条件。正确写法是UPDATE ProductSkus SET Stock Stock - quantity, Version Version 1 WHERE Id skuId AND Stock quantity AND Version version;执行后判断受影响行数0 就说明库存被别人抢先改了。如果你用 EF Core就写ExecuteUpdateAsync带条件第 3 章下单接口里已经演示了。还有一个附加坑如果商家在管理后台改过 SKU 价格或库存Version可能会被你手动更新绕过所以所有更新入口都要带 Version 校验不能只靠下单接口。再补一个注意点秒杀场景建议加上 Redis 预扣库存但如果你第一版没有 Redis依赖数据库的原子更新已经能防超卖只是数据库压力会大一些别为了防超卖把架构搞复杂。5.4 真机白屏或请求失败域名白名单和 HTTPS 证书是头号嫌疑人现象微信开发者工具里一切正常点「预览」用手机扫码打开所有数据加载不出来页面白屏或转圈。原因小程序正式环境真机预览、体验版、正式版只允许请求「服务器域名」白名单里的 HTTPS 地址。开发者工具的「不校验合法域名」只对工具内的模拟器生效手机上一律走严格校验。如果你配置的域名没有 ICP 备案、证书链不完整缺中间证书、或域名是 IP 地址微信不允许用 IP 配置都会被拦截。解决在微信公众平台「开发 - 开发管理 - 开发设置 - 服务器域名」里把https://api.yourdomain.com加进 request 合法域名。注意域名不能带端口必须 443、不能是 IP、必须有有效的 HTTPS 证书。还有一个隐藏坑如果你用了http://开头的本地联调地址真机预览肯定不行公司内网环境可以在开发者工具里临时勾选「不校验合法域名」调试但提交体验版前必须切回正式域名。另外小程序发布前有一个「业务域名」配置那是给 web-view 内嵌网页用的和接口请求域名不是一回事别再混在一起花一下午排查。5.5 图片裂了防盗链和小程序域名的爱恨情仇现象商城后台传的商品图在 PC 管理端显示正常小程序商品列表里一部分图片加载不出来WXSS 背景图直接消失控制台报downloadFile:fail或 403。原因很多图片服务器开启了防盗链只允许特定 Referer 来源访问。小程序的image组件加载图片时不带Referer或者 Referer 是https://servicewechat.com图片服务器上的防盗链规则不认识这个域名就返回 403。还有一种是图片 URL 是 HTTP 的小程序强制要求业务域名 HTTPS直接拦截。解决两个层面一是图片服务器关掉防盗链或者把servicewechat.com和你的小程序域名加入白名单二是后端在上传图片时做一层代理或转存——把图片第三方 URL 下载后重新上传到你自己的云存储/OSS小程序端永远只加载自己域名下的图片。做代理时要注意小程序对并发下载有限制图片懒加载用wx.lazyLoad或image组件的lazy-load属性首页最多预加载首屏 10 张左右一次性加载 50 张大图会明显卡顿。我见过一套源码图片直接用外链上架第二天运营发现朋友圈分享出去的图全裂最后老老实实加了图片转存逻辑。6. 上线前最后一步接口防刷、慢查询定位与发布检查表这一章不讲新功能讲我怎么验证一套商城源码能不能扛住真实流量。拿到任何一套 C# 小程序商城源码我会先做三件事压测它的底线。第一件事是给关键接口加耗时记录。在 .NET Core 里动手最快的是一个中间件也常叫过滤器// 耗时日志中间件每个 API 请求记录状态码、耗时、用户标识 public class RequestLoggingMiddleware { private readonly RequestDelegate _next; private readonly ILoggerRequestLoggingMiddleware _logger; public RequestLoggingMiddleware(RequestDelegate next, ILoggerRequestLoggingMiddleware logger) { _next next; _logger logger; } public async Task InvokeAsync(HttpContext context) { var sw Stopwatch.StartNew(); var userId context.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? anonymous; await _next(context); sw.Stop(); if (sw.ElapsedMilliseconds 500) // 超过 500ms 的请求重点观察 { _logger.LogWarning( Slow request: {Method} {Path} cost {Ms}ms, user{UserId}, status{StatusCode}, context.Request.Method, context.Request.Path, sw.ElapsedMilliseconds, userId, context.Response.StatusCode); } } }把这个中间件注册进管线后dotnet run跑起来用 wrk 或 Postman 的 Collection Runner 压一下商品列表接口如果 QPS 到不了 200 或 P95 延迟超过 300ms先看是不是数据库查询缺索引。商城商品列表最常见的慢查询就是WHERE status 1 ORDER BY created_at DESC LIMIT 20在Status、CreatedAt上建联合索引能带来数量级提升。第二件事是防刷。商城接口最容易被打的是登录接口拿 code 换 token、短信验证码接口、商品详情接口。登录接口刷爆会消耗你的微信 API 配额短信接口被刷会产生真金白银的费用。我一般加三个手段按用户 openid 限频已经登录的用户按 user id、按 IP 限频、下单接口做验签参数服务端生成签名前端带上校验失败直接拒绝。这些在 .NET Core 里可以写成一个RateLimitFilter或直接用 ASP.NET Core 内置的速率限制中间件AddRateLimiter固定窗口限流对商城足够了不需要引入 Redis 做分布式计数——除非你有多台服务器。第三件事是发布检查表。我会按这个顺序过一遍appsettings.json里的小程序 appid/secret 有没有换成生产环境的值JWT 密钥是不是足够长的随机字符串数据库连接字符串是不是指向生产库Swagger 有没有在非 Development 环境禁用日志有没有配到文件或独立日志服务不让Console.WriteLine裸奔到生产HTTPS 证书在微信后台的 request 合法域名是否配置成功支付回调地址是不是外网可访问的 HTTPS 域名。我的习惯是任何商城源码到手后先跑一次「下单-支付-回调-发货」全链路手动测试再把这个流程写成一份 checkmark 列表。特别是支付回调我强烈建议你在本地用 Postman 模拟微信回调报文往本地回调地址打一次先验证验签不通过时后端返回什么——很多源码在这个环节会直接 500 而不是返回微信需要的非成功应答真上线时微信就会无限重试。这行日志就是你上线前最后的后悔药。希望这套从架构选型到避坑验证的路线能帮你把这份 C# 小程序商城源码真正落到能交付的状态。本文还有配套的精品资源点击获取
返回列表