ARTICLE DETAIL

资讯详情

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

C#与原生微信小程序搭建商城:登录支付、接口联调与避坑指南

C#与原生微信小程序搭建商城:登录支付、接口联调与避坑指南 简介这是一套基于C#与NetCore构建的小程序商城系统前端采用原生微信小程序后端由C#语言实现面向需要快速搭建多店铺电商平台的技术团队与.NET开发者。系统整合完整商城业务闭环覆盖多店铺管理、三级分销、促销、优惠券、积分、物流配送及插件化管理模块同时提供小程序端与管理后台整体达到商用标准。压缩包共1962个文件大小约10.33MB以cs、cshtml、js、json等源代码与配置资源为主配套wxml、wxss小程序前端文件以及sql、xml数据库与部署配置目录结构清晰。订单、用户、商品、插件管理、导出管理等核心业务均有对应控制器与处理服务可从源码层面理解小程序与NetCore前后端交互方式、商城权限体系及营销模块的落地细节也能作为直接运行的基础工程进行二次开发与功能扩展。目前已有1278人学习下载适合需要完整商城参考项目或希望快速迭代上线的团队与个人开发者。1. 这套 zip 到底装了什么C# 与原生小程序搭出来的商城骨架收到「基于C#的小程序商城原生微信小程序NetCore技术构建.zip」这类压缩包时先别急着解压跑起来。它本质上是一条完整的业务链路微信小程序负责商品展示、购物车、下单支付这些用户能摸到的界面NetCore WebApi 负责商品库存、订单状态、用户身份这类业务逻辑和数据落库。两端通过 HTTPS 上的 JSON 通信登录态靠微信的 code2Session 换取 openid再在服务端签发 JWT 维护会话。对于手里只有 C# 技术栈、又想快速上线微信小程序商城的团队这套组合比换 Java 或 Node 后端的学习成本低得多也比用第三方 SaaS 商城多一层可控性。适合的人能写 .NET 接口、愿意啃一遍小程序生命周期但不打算引入跨端框架的开发者。2. 拆开工程包原生小程序与 NetCore 的项目边界2.1 前端 pages 目录与后端 Controllers 的一一对应关系一个规范的小程序商城工程前端目录基本长这样pages下按业务模块分子目录utils里放 request 封装和公共方法static或images存静态资源。后端 NetCore 项目则按经典三层拆Controllers暴露 REST 接口Services写业务逻辑Repositories或DbContext直接操纵数据库。解压后先别急着跑第一件事是核对前端每个wx.request的 URL 前缀和后端 Controller 的路由能不能对得上。// utils/request.js 前端请求封装 const BASE_URL https://api.yourdomain.com; // 正式环境 // const BASE_URL http://127.0.0.1:5000; // 本地联调时用但小程序真机不能访问 localhost function request(path, method, data, header {}) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL path, method: method || GET, data: data || {}, header: { Content-Type: application/json, Authorization: wx.getStorageSync(token) ? Bearer wx.getStorageSync(token) : , ...header }, success: (res) { if (res.statusCode 200) { if (res.data.code 0) { resolve(res.data.data); } else { wx.showToast({ title: res.data.msg, icon: none }); reject(res.data); } } else if (res.statusCode 401) { // token 过期或无效跳回登录页 wx.removeStorageSync(token); wx.reLaunch({ url: /pages/login/login }); reject(res); } else { wx.showToast({ title: 服务器开小差了, icon: none }); reject(res); } }, fail: (err) { reject(err); } }); }); }这段封装解决的是小程序端最基础的三个问题BASE_URL 环境切换、Authorization 头的统一注入、状态码 401 时的登录态失效跳转。注意小程序真机调试时不能访问127.0.0.1必须用局域网 IP 或者已备案且配好 HTTPS 的域名开发者工具里勾选了「不校验合法域名」才能本地联调但这只是开发期的救命手段上线前一定要关掉。res.data.code 0是前后端约定好的业务码0 代表成功非 0 是业务错误HTTP 层只表达传输状态业务错误码统一放 body 里。后端对应这个请求的 Controller 大致长这样路由命名建议直接按微信小程序的习惯走 RESTful 风格省得前端维护一份 URL 映射表。// Controllers/ProductController.cs [ApiController] [Route(api/[controller])] public class ProductController : ControllerBase { private readonly IProductService _productService; public ProductController(IProductService productService) { _productService productService; } [HttpGet(list)] public async TaskIActionResult GetList(int page 1, int pageSize 20, int? categoryId null) { var items await _productService.GetPagedListAsync(page, pageSize, categoryId); return Ok(new { code 0, data items }); } [HttpGet({id})] public async TaskIActionResult GetDetail(int id) { var item await _productService.GetDetailAsync(id); if (item null) return NotFound(new { code 404, msg 商品不存在 }); return Ok(new { code 0, data item }); } }[ApiController]特性会自动做模型校验和参数绑定[Route(api/[controller])]这种占位符路由让控制器名直接映射进 URLapi/product/list对应前端请求路径。GetPagedListAsync里的分页参数page和pageSize要在前端 request 里用小程序的data传过来后端做了默认值兜底但前端不应依赖因为一旦漏传用户会直接看到第 1 页第 20 条之后乱掉。控制器不写业务逻辑只做参数接收和结果包装所有规则放在 Service 层这样后续加缓存、加消息队列都不需要动接口签名。2.2 为什么选原生微信小程序而不上 uni-app 或 Taro可能你拿到这套源码时会疑惑都 2025 年了为什么不直接上 uni-app 一份代码双端复用答案很现实商城类小程序对微信生态的依赖极重原生框架能拿到最完整的 API 能力和最少的兼容层损耗。原生wx.login、wx.getPhoneNumber、wx.requestPayment这些接口在跨端框架里往往要包一层桥接桥接层一旦跟不上微信的更新节奏支付和手机号这类核心链路的稳定性就要打折扣。C# 后端开发者通常不是专职前端少学一套 Vue 语法和框架编译规则直接写 WXML 反而更容易定位问题。另外原生小程序在「微信小程序单选框」「微信小程序顶部导航栏高度」这类细节上可以直接用官方组件和wx.getSystemInfoSync()拿真实值不需要通过框架的抽象层猜。虽然 uni-app 也能编译到微信端但 H5 端和小程序端的渲染差异会让你在调试「原生微信小程序NetCore技术构建」这套架构时多出一层不确定因素。如果你确定未来还要做支付宝小程序或 H5再考虑跨端如果只做微信生态原生就是成本最低的选型。2.3 NetCore 后端的模块划分从商品到订单的状态流转后端建议按商城业务域拆模块而不是按技术层拆。商品域、库存域、购物车域、订单域、支付域各自独立域之间只通过接口交互。这种划分在工程里落地时一般用文件夹结构体现配合AddScoped注册每个域的 Service数据库上下文统一在启动项目里配置。// Program.cs NetCore 6 最小 API 托管方式 var builder WebApplication.CreateBuilder(args); builder.Services.AddControllers().AddNewtonsoftJson(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddDbContextMallDbContext(options options.UseSqlServer(builder.Configuration.GetConnectionString(Default))); builder.Services.AddScopedIProductService, ProductService(); builder.Services.AddScopedIOrderService, OrderService(); builder.Services.AddScopedICartService, CartService(); builder.Services.AddScopedIWeChatService, WeChatService(); builder.Services.AddScopedIPaymentService, PaymentService(); var app builder.Build(); if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); app.Run();依赖注入在这里管住了全局任何 Controller 的构造函数里声明IProductService容器就自动把实现类塞进来要换数据库或加中间件只需改到这里不用改业务代码。UseAuthentication和UseAuthorization的顺序不能颠倒前者识别「你是谁」后者决定「你能不能做」顺序反了在后续做 JWT 鉴权时会出现 401 和 403 混在一起的诡异现象。数据库先选 SQL Server 还是 MySQL 取决于团队但连接字符串要单独放在appsettings.json里并且上线后用环境变量覆盖千万别把带口令的连接串提交进 Git 历史。3. 登录与手机号链路NetCore 后端如何接管微信身份3.1 小程序 wx.login 到 JWT 签发code2Session 的两次网络请求商城小程序的核心前提是知道「这个人是谁」。微信小程序登录获取手机号之前必须先用wx.login()拿到临时 code再把 code 发到自己的 NetCore 后端。后端拿着 code 去调微信的https://api.weixin.qq.com/sns/jscode2session换回openid、session_key和unionid。这里有个常见的认知错误session_key 的有效期微信不直接给出但 code 只能一次性使用且 5 分钟后过期所以后端必须在收到 code 后立刻发起换 session 的请求不能缓存 code 供后续重试。// Services/WeChatService.cs public async TaskWeChatSession Code2SessionAsync(string code) { var appId _config[WeChat:AppId]; var appSecret _config[WeChat:AppSecret]; var url $https://api.weixin.qq.com/sns/jscode2session?appid{appId}secret{appSecret}js_code{code}grant_typeauthorization_code; using var httpClient _httpClientFactory.CreateClient(WeChat); var response await httpClient.GetStringAsync(url); var result JsonSerializer.DeserializeJsonElement(response); if (result.TryGetProperty(errcode, out var errcode) errcode.GetInt32() ! 0) { // 记录微信原始错误码和消息便于排查 var errmsg result.GetProperty(errmsg).GetString(); throw new WeChatApiException($code2session failed: {errcode} {errmsg}); } var session new WeChatSession { OpenId result.GetProperty(openid).GetString(), SessionKey result.GetProperty(session_key).GetString(), UnionId result.TryGetProperty(unionid, out var u) ? u.GetString() : null }; return session; }这里必须用IHttpClientFactory来管 HttpClient 的生命周期而不是在方法里new HttpClient()。原因HttpClient 的 socket 资源不会随对象释放而立即回收高并发下会出现端口耗尽这在 .NET Core 的异步场景里是高频故障。code2session 接口不需要静态 Token每次带 AppId 和 AppSecret 请求即可AppSecret 绝不能出现在小程序前端代码里。拿到 openid 后检查用户表里是否已有该 openid有就直接发 JWT没有就先创建默认用户再发。JWT 的有效期建议 2 小时刷新令牌另发一个 7 天的 refresh_token避免用户每两小时重新登录一次但 refresh_token 需要落库存状态以备吊销。// Services/AuthService.cs 签发 JWT 的核心参数 private string GenerateJwtToken(string openId, string userId) { var claims new ListClaim { new Claim(ClaimTypes.NameIdentifier, userId), new Claim(openid, openId) }; var key new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config[Jwt:SecretKey])); var creds new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var token new JwtSecurityToken( issuer: _config[Jwt:Issuer], audience: _config[Jwt:Audience], claims: claims, expires: DateTime.Now.AddHours(2), signingCredentials: creds); return new JwtSecurityTokenHandler().WriteToken(token); }JWT 的 SecretKey 至少 32 字节放appsettings.json的Jwt:SecretKey节点上线后通过环境变量注入。Issuer 和 Audience 是签发方和接收方标识小程序端不需要关心这两个值但后端在Program.cs配置AddAuthentication().AddJwtBearer()时必须把验证参数里的ValidateIssuer、ValidateAudience、ValidateLifetime全部设为 true否则任意拿一个同 SecretKey 签名的 token 就能冒充整个系统的所有用户这个坑一旦踩上账号体系直接失去意义。3.2 获取用户手机号加密数据解密与偏移量的算法细节用户在小程序端点击「获取手机号」按钮后微信返回code这个 code 换手机号的方式在 2023 年后改成了动态令牌模式后端拿这个 code 调用https://api.weixin.qq.com/wxa/business/getuserphonenumber接口传入access_token和 code直接拿手机号不再需要自己解密encryptedData。这一点很多老教程还停留在自己解密 session_key 的阶段需要注意区分新接口一次调用就能拿 phone_info不需要小程序端把 encryptedData 和 iv 传过来这大幅减少了传输链路里的数据暴露。// Services/PhoneService.cs public async TaskPhoneInfo GetPhoneNumberAsync(string phoneCode) { var accessToken await GetAccessTokenAsync(); // 全局缓存提前几分钟刷新 var requestBody new { code phoneCode }; var requestUrl $https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token{accessToken}; using var httpClient _httpClientFactory.CreateClient(WeChat); var json JsonSerializer.Serialize(requestBody); var content new StringContent(json, Encoding.UTF8, application/json); var response await httpClient.PostAsync(requestUrl, content); var result await response.Content.ReadAsStringAsync(); var doc JsonDocument.Parse(result); if (doc.RootElement.GetProperty(errcode).GetInt32() ! 0) throw new WeChatApiException(getphone failed); return doc.RootElement.GetProperty(phone_info).DeserializePhoneInfo(); }GetAccessTokenAsync必须做缓存微信的 access_token 有效期 7200 秒且每日获取次数有限接口频率限制在文档写明每次调 getuserphonenumber 都去拿新 token 会很快触发频率限制。常见做法是放到内存缓存或 Redis过期前 5 分钟主动刷新。手机号属于敏感个人信息后端拿到后要做脱敏存储比如只存138****1234完整号码单独加密放在另一列或另一张表并在日志里禁止打印完整手机号。商城场景下手机号通常用于登录后的二次身份绑定不一定要和 openid 一一对应需要自己设计用户表结构来存多个手机号或换绑历史。3.3 NetCore 过滤器实现登录态校验ActionFilter 与 AuthorizationFilter 的取舍鉴权不能在每个 Controller 里复制粘贴校验代码社区的标准做法是用 AuthorizationFilter 或自定义中间件统一拦。.NETCore 过滤器里IAsyncAuthorizationFilter在 Controller Action 执行之前运行适合做 token 校验和用户信息填充IAsyncActionFilter在 Action 执行前后包一层适合做参数日志和性能埋点。对于商城这种绝大多数接口都需要登录态的场景不如直接做全局过滤器再在匿名接口上打[AllowAnonymous]。// Filters/JwtAuthorizeFilter.cs public class JwtAuthorizeFilter : IAsyncAuthorizationFilter { private readonly ITokenValidator _validator; public JwtAuthorizeFilter(ITokenValidator validator) { _validator validator; } public async Task OnAuthorizationAsync(AuthorizationFilterContext context) { var hasAllowAnonymous context.ActionDescriptor.EndpointMetadata .Any(e e is AllowAnonymousAttribute); if (hasAllowAnonymous) return; // 登录接口、支付回调不能拦 var authHeader context.HttpContext.Request.Headers[Authorization].ToString(); if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith(Bearer )) { context.Result new UnauthorizedResult(); return; } var token authHeader.Substring(Bearer .Length).Trim(); var principal await _validator.ValidateTokenAsync(token); if (principal null) { context.Result new UnauthorizedResult(); return; } context.HttpContext.User principal; // 后续 Action 里用 User.Identity.Name 取用户 } }注意检查 AllowAnonymous 的逻辑必须在 token 校验之前否则微信支付回调这类需要匿名访问的接口会被误伤成 401。支付回调尤其特殊微信服务器的回调不带你的 JWT只带签名所以整个回调接口都不能过自定义鉴权过滤器改为回调内部自己验签。Validator 内部解析 JWT 时要处理 token 过期、签名错误、用户被禁用三种异常并且分别记录不同日志级别。这里「返回 401 而不是 403」是一个重要的语义区分401 表示未认证前端收到后跳登录页403 表示已认证但无权限前端需要提示「无权限」而不是重新登录。4. 小程序商城的页面与接口联调从商品列表到支付回调4.1 顶部导航栏与页面布局拿到真实高度再渲染商城首页的布局比普通页面更敏感因为顶部往往有搜索框、分类 tab、轮播图任何像素偏移都会让用户觉得「这个商城是坏的」。原生小程序里wx.getSystemInfoSync()可以拿到状态栏高度但胶囊按钮的位置和高度在不同机型尤其是 iPhone 刘海屏和 Android 全面屏上差异明显。「微信小程序顶部导航栏高度」这个问题的标准解法是自定义导航栏用胶囊按钮的top和height算出导航栏总高度而不是硬编码 44px 或 64px。// utils/navigation.js function getNavBarHeight() { const win wx.getWindowInfo ? wx.getWindowInfo() : wx.getSystemInfoSync(); const capsule wx.getMenuButtonBoundingClientRect(); if (!capsule || !capsule.height) return { statusBarHeight: 20, navBarHeight: 44 }; const statusBarHeight win.statusBarHeight || 20; const navBarHeight (capsule.top - statusBarHeight) * 2 capsule.height; return { statusBarHeight, navBarHeight }; }wx.getMenuButtonBoundingClientRect()是拿胶囊按钮位置的关键 API(capsule.top - statusBarHeight) * 2 capsule.height这个公式算出的是导航栏从状态栏底部到导航栏底部的总高因为胶囊按钮垂直居中于导航栏所以顶部留白的两倍加胶囊自身高度就是导航栏完整高度。这个值在页面onLoad里取一次存入全局即可设计稿里不要写死。首页 swiper 的高度如果用比例算通常用750 / 设计稿轮播图宽度 * 设计稿轮播图高度得到 rpx 转 px 后的值但要注意rpx转px在不同宽度设备上会变直接image的modewidthFix更省心。4.2 购物车与订单的数据流本地缓存、服务端同步和状态机购物车在商城里的实现有两条路线纯服务端保存每次增删都请求接口或本地缓存 服务端校验。纯本地缓存的隐患是换设备或清缓存后购物车丢失用户会直接骂人纯服务端的隐患是网络慢时用户频繁点击加购会感觉到卡。折中做法是本地用wx.setStorageSync存一份购物车数据的快照进入购物车页面时调后端/api/cart/sync做合并以服务端数据为准把本地差异提交上去。合并逻辑要考虑同一个 SKU 的数量叠加而不是直接把本地覆盖服务端否则会出现用户 A 在 Web 端加购的商品被小程序端的空购物车冲掉。// Services/CartService.cs public async TaskCartMergeResult MergeCartAsync(string userId, ListCartItemDto localItems) { var serverItems await _db.CartItems.Where(c c.UserId userId).ToListAsync(); foreach (var local in localItems) { var existing serverItems.FirstOrDefault(s s.SkuId local.SkuId); if (existing ! null) { existing.Quantity Math.Max(existing.Quantity, local.Quantity); // 取较大值不简单相加 } else { _db.CartItems.Add(new CartItem { UserId userId, SkuId local.SkuId, Quantity local.Quantity }); } } await _db.SaveChangesAsync(); return new CartMergeResult { Items await GetCartListAsync(userId) }; }合并策略里的Math.Max是故意设计的用户在购物车页面把数量改成 1但本地缓存里是 3如果直接覆盖会把用户的修改吞掉如果相加则会莫名多出数量。取较大值是一个折中方案真实项目里应该在用户的每次数量变更操作时立即同步服务端让本地只作为弱网时的临时存储。订单状态机则严格很多待付款 → 已付款 → 待发货 → 已发货 → 已完成 / 已取消每个状态流转都要有对应的操作接口不允许直接改状态字段。Enum用 int 落库但接口里用字符串枚举名称传输避免前端看到魔法数字 3 还要去猜是什么状态。4.3 微信支付与回调验签NetCore 接口中的签名防重支付这块最容易出事故的环节不在「调起支付」而在「回调通知」。小程序端wx.requestPayment成功后后端收到微信支付的异步通知必须做两件事验签和幂等处理。验签用微信支付平台证书按文档用Wechatpay-Serial、Wechatpay-Signature、Wechatpay-Timestamp、Wechatpay-Nonce四个头构造验签串用证书公钥做 RSA-SHA256 验签。验签失败直接返回{ code: FAIL, message: 签名错误 }微信会停止重试但不影响已支付成功的订单只是后台不会自动更新状态。// Controllers/PaymentController.cs [HttpPost(callback)] [AllowAnonymous] public async TaskIActionResult PayCallback() { var headers Request.Headers; var body await new StreamReader(Request.Body).ReadToEndAsync(); var isValid _wechatPayService.VerifyCallbackSignature(headers, body); if (!isValid) return Ok(new { code FAIL, message 签名错误 }); var result JsonSerializer.DeserializePayCallbackDto(body); if (result.event_type ! TRANSACTION.SUCCESS) return Ok(new { code FAIL, message 忽略非支付事件 }); var outTradeNo result.resource.OutTradeNo; var transactionId result.resource.TransactionId; var amount result.resource.Amount.Total; // 幂等检查订单状态是否已经是已支付是则直接返回成功不再处理 var order await _db.Orders.FirstOrDefaultAsync(o o.OrderNo outTradeNo); if (order null) return Ok(new { code FAIL, message 订单不存在 }); if (order.Status OrderStatus.Paid) return Ok(new { code SUCCESS, message 成功 }); order.Status OrderStatus.Paid; order.TransactionId transactionId; order.PaidAmount amount; await _db.SaveChangesAsync(); // 通知业务模块扣库存、发消息、记流水 await _orderService.AfterPaidAsync(order.Id); return Ok(new { code SUCCESS, message 成功 }); }幂等这一步是血泪教训微信支付回调在网络抖动或服务重启时可能发送多次如果不做状态判断就会出现订单状态被覆盖为已支付后再次触发扣库存库存瞬间变负。TRANSACTION.SUCCESS是微信支付 v3 版本的事件类型老的 v2 版本用的是SUCCESS字符串字段结构也不一样实现前先确认你集成的是 v3 还是 v2。回调响应要在拿到结果后立即返回不能把业务处理放在异步线程里再返回 SUCCESS否则微信认为没收到回执继续重试直到几小时后服务端才真正处理完用户已经发起退款。回调接口里可以用中间件记录完整请求头和 body排查问题时这是唯一线索。5. 避坑与排查这套商城方案最常见的 5 个翻车现场5.1 AppSecret 泄漏在小程序端代码里被反编译现象上线后微信公众平台安全体检提示 AppSecret 疑似泄漏或者有外部用户调用你的 code2session 接口刷出大量 openid。 原因开发时图省事把 AppSecret 直接写在app.js里当全局变量小程序代码包解包后字符串一搜就能看到AppSecret 一旦泄露任何人都能拿它换 session_key进而伪造身份。 解决后端所有请求微信接口的数据都只存appsettings.json并部署时用环境变量覆盖前端只存 AppId。一个合理的习惯是每周轮换一次 AppSecret并对 code2session 接口做调用频率限制比如同一个 IP 每分钟最多 100 次这两个动作同时做能把风险压到可接受范围。5.2 code2Session 返回 40029 或 45011 错误吗现象测试登录一切正常上线后大量用户登录报errcode 40029invalid code偶发45011api minute-quota limit hit。 原因40029 通常是 code 使用过一次后又被重复提交或者前端在短时间把两个不同 code 发到了后端可能是点击登录时按钮没禁点用户连点了两次。45011 出现在高并发冷启动时后端大量请求同时打向微信的 code2session 接口触发分钟级频率上限。 解决前端提交登录请求前加防抖5 秒内只允许发送一次后端收到 code 后用 Redis 做一次性校验同一个 code 第二次出现直接返回客户端错误并将 code2session 请求串行化或用内存队列削峰不要每来一个请求就立刻向外发。真实打开率高的商城上午十点和晚上八点各有一个秒杀峰最好提前把 access_token 也预热好。5.3 获取手机号接口解密后手机号少一位现象调用getuserphonenumber返回成功但拿到的手机号中间缺了数字或者格式变成86 138...的开头。 原因新接口返回的phone_info里purePhoneNumber才是纯 11 位号码phoneNumber字段带国家区号直接把phoneNumber拿来存库会出现脏数据。还有一个隐蔽坑接口返回 JSON 中手机号字段名拼写是purePhoneNumber有的老代码用purephonenumber去取JObject 索引大小写不敏感可能没事但用 System.Text.Json 默认大小写敏感就直接取不到。 解决反序列化时明确指定[JsonPropertyName(purePhoneNumber)]存库前用正则^1[3-9]\d{9}$校验不通过直接拒绝存入并打印告警日志。入库后再次查询返回给前端前做脱敏处理不要在接口响应里返回完整手机号。5.4 支付成功但订单还是待付款现象用户微信支付弹窗显示已扣款但小程序订单详情页状态还是「待付款」用户开始投诉。 原因支付回调没有正确返回 SUCCESS 给微信或者回调处理抛了异常没捕获。异常可能来自数据库写入失败字段长度不够、外键约束也可能来自 AfterPaidAsync 里发优惠券时外部接口超时。 解决回调接口最外层套 try-catch只要验签通过就返回 SUCCESS把订单更新放进一个可靠队列或单独补偿任务去处理数据库操作拆小事务订单状态更新先 Commit再发券扣库存后者失败不能回滚前者而是记录失败标记由定时任务重试。按这个思路支付成功的订单状态最迟 30 秒内会正确不需要等用户投诉再来修。5.5 真机请求全部失败但开发者工具正常现象开发者工具里所有接口都能通预览到真机上全部 request 失败network 面板显示request:fail url not in domain list。 原因小程序真机强制校验合法域名开发者工具里「不校验合法域名」的开关只对工具内调试生效真机不会理会。如果你用的域名没备案、没配 HTTPS、或没在小程序后台 request 合法域名列表里都会直接拦截NetCore 默认没有启用 HTTPS 重定向时也会在真机上被拦。 解决上线前把https://api.yourdomain.com加入微信公众平台的「服务器域名」配置并保证证书链完整NetCore 的UseHttpsRedirection确保用户访问 http 时 302 跳 https。本地联调时如果一定要真机预览用内网穿透工具把本机 5000 端口映射成临时 https 域名再暂时加入合法域名列表但这只是权宜之计正式版本必须走正式域名。6. 进阶技巧把库存并发压下来和把工程化做上去6.1 Redis 缓存与防超卖一锁二查三更新商城的高并发瓶颈几乎都死在库存上秒杀场景下数据库行锁扛不住。常见做法是商品详情和库存数量放 Redis用 String 类型存 SKU 剩余量下单时用 Lua 脚本做原子扣减扣成功才允许创建订单扣失败直接提示库存不足。Lua 脚本能保证检查和扣减两步的原子性避免两个线程同时读到库存 1 后都下单成功。-- lua/stock_deduct.lua local key KEYS[1] -- 库存 key形如 sku:stock:{skuId} local quantity tonumber(ARGV[1]) local current tonumber(redis.call(GET, key) or 0) if current quantity then return -1 end redis.call(DECRBY, key, quantity) return 1NetCore 侧调用这个脚本时用StackExchange.Redis的ScriptEvaluate方法把 SKU ID 作为 KEYS扣减数量作为 ARGV返回值是 1 才继续走创建订单流程。数据库里的库存表和 Redis 之间需要一个对账任务每五分钟把所有 SKU 的 Redis 剩余量回写数据库同时记录扣减流水。这个方案的前提是 Redis 不能丢数据生产环境必须开启 AOF 持久化否则 Redis 重启后库存恢复到满量用户用的优惠券也全作废运营那边会以为系统出了灵异事件。6.2 环境隔离与配置管理appsettings 多环境自动切换NetCore 在ASPNETCORE_ENVIRONMENT环境变量下会自动加载appsettings.{Environment}.json这套机制在小程序商城的多人协作里很有用。本地开发用Development联调用Staging连测试数据库生产用Production。小程序端则用wx.getAccountInfoSync().miniProgram.envVersion判断当前运行环境是开发版、体验版还是正式版动态切 BASE_URL这样测试体验版时不会把数据写到生产库。// appsettings.Production.json 关键配置 { ConnectionStrings: { Default: Server10.0.0.5;DatabaseMallProd;User Idappuser;Password********;TrustServerCertificateTrue }, WeChat: { AppId: wx1234567890, AppSecret: 从环境变量注入不要写这里 }, Jwt: { SecretKey: 从环境变量注入至少32位随机串, Issuer: MallServer, Audience: MallMiniProgram }, Redis: { ConnectionString: 10.0.0.6:6379,password********,abortConnectfalse }, Aes: { Key: 从环境变量注入, Iv: 从环境变量注入 } }生产环境的密码全部由 CI/CD 流水线的环境变量注入不落appsettings.json也不打进去镜像。我见过一个团队把生产数据库密码提交到 Git 私有仓库后面仓库权限泄露导致整个库被脱机加密勒索这就是把 AppSecret 写进代码的放大版。CI 流水线至少做四件事dotnet build -c Release、dotnet test、dotnet publish、用Dockerfile构建镜像推到私有仓库。小程序端虽不能直接走这些构建产物但可以用微信 CI 机器人做预览版上传和自动化测试把人工发版这一步省掉。最后说一个我自己踩过的坑有一版上线把WeChat:AppSecret误写进appsettings.Development.json并提交了仓库当时没事三个月后收到安全告警才发现。从那以后所有秘钥一律要求裸奔式管理——代码里只留占位符部署环境里必须给到完整值少一个环境变量服务就起不来而不是启动后再偷偷读一个默认值。这看起来很麻烦但它能逼着团队把敏感配置的入口收窄到一个可控的点上。小程序商城的链路本身就长前端、网关、后端、DB、Redis、微信开放平台、支付平台每一环都可能出问题配置管理整齐一点排查问题时就能少一个变量。希望帮到你。本文还有配套的精品资源点击获取
返回列表