☰
基于.Net 5.0与Uni-App的小程序商城源码:可视化布局与微信支付实践
2026/10/28 16:25:09 网站建设 项目流程

简介:这套基于.Net 5.0与Uni-App开发的小程序商城系统源码,定位为可投入二次开发的电商中台项目,适合有一定前后端基础的技术团队、独立开发者及SaaS服务商,用于快速搭建具备可视化布局的多平台商城。系统前后端分离、支持分布式部署,一套代码可发布微信小程序、H5、Android、iOS、支付宝小程序、字节跳动小程序、QQ小程序等10个平台;后台覆盖商品管理(单/多规格SKU)、订单售后物流、会员分销、代理、团购秒杀拼团、直播优惠券及自定义表单等完整电商业务链。资源为zip压缩包,共1983个文件,以C#服务端源码、Vue页面、HTML模板和JavaScript脚本为主,另含SQL数据库脚本、Dockerfile部署配置、PSD设计稿等,整体33.38MB,目录结构清晰,便于按模块查阅。已有675人学习,尤其适合需要借鉴商业级商城设计、梳理多端项目架构或扩展分销代理玩法的开发者。

1. 为什么这套.Net 5.0 + Uni-App商城源码值得下:可视化布局不是噱头

很多小程序商城项目都声称支持可视化布局,但打开后台会发现,大部分只是改改主题色、换换轮播图,页面结构仍然是写死的。这份基于.Net 5.0和Uni-App开发的小程序商城系统源码,把首页拆成轮播、导航宫格、商品列表、优惠券位等组件,后台拖拽排序后保存成JSON,小程序端启动时拉取JSON动态渲染,改版不用发版;同时把微信登录、手机号授权、支付回调这些高频接口按当前规范走通。适合有.Net后端基础、想在微信小程序里快速起商城并保留定制能力的开发者,也适合被SaaS模板锁死、想拿源码掌控业务逻辑的团队。下面从工程骨架开始拆。

2. 先看骨架:.Net 5.0 API、Uni-App前端与装修数据的流向

2.1 为什么是.Net 5.0而不是3.1:统一平台决定API走向

.Net 5.0在这套源码里不是单纯追新,它解决了两个现实问题。第一个是运行时统一,之前.Net Core 3.1和.NET Framework两套代码在商城这种需要对接微信支付、短信服务、物流接口的场景里经常要写两遍客户端,5.0把底子统一后,原来Framework里的加密库、绘图相关API可以直接搬过来用。第二个是EF Core 5.0的查询能力提升,过滤Include和全局查询过滤器对商城特别实用,后面说商品列表接口时会再展开。

工程结构上,这套源码的后端是标准的Controller、Service、Repository三层,Controller只做参数校验和结果包装,业务规则全部在Service层,数据库访问收敛在Repository。我第一次拆这个工程时最满意的是它把返回结构统一成了固定格式,而不是直接抛裸对象,这让Uni-App端处理错误和登录过期轻松很多。源码里对外的返回包装类长这样:

public class ApiResult<T> { public int Code { get; set; } public string Msg { get; set; } public T Data { get; set; } public static ApiResult<T> Ok(T data) => new ApiResult<T> { Code = 0, Msg = "ok", Data = data }; public static ApiResult<T> Fail(string msg) => new ApiResult<T> { Code = -1, Msg = msg }; }

Code这里约定0表示成功,-1是业务失败,-401是未登录或token过期。前端Uni-App的request封装里统一判断res.data.code,只有0才进业务成功回调,其他情况弹出Msg并做对应的跳转。这个包装类看着简单,但它决定了整个前后端联调的节奏——如果每个接口各自返回裸数据,后期加统一埋点和异常上报会非常痛苦。

数据访问层用的EF Core,连接字符串放在appsettings.json里,源码默认用的是SQL Server。如果要换MySQL,需要做两处改动:在ConfigureServices里把UseSqlServer替换成UseMySql,同时检查所有涉及RowVersion或Guid主键的字段映射。我实际迁移过一次,订单表的并发控制字段类型差异是主要坑点,其他部分EF Core的LINQ查询基本能直接平移。

2.2 最关键的几张表:商品、订单、装修配置

商城系统表很多,但这个源码里真正决定业务边界的是三张表:商品表、订单表和装修配置表。商品表采用SPU和SKU分离设计,SPU存商品标题、详情、主图,SKU存规格组合、价格、库存,多规格商品比如“颜色+尺码”在SKU层展开,前端商品详情页的价格区间和规格选中状态都由SKU列表驱动。订单表的状态流转用枚举固定:pending、paid、shipped、completed、closed,退款单单独建表,不污染主订单状态。

装修配置表最容易被人忽视,但它是可视化布局的地基,建表语句如下:

CREATE TABLE page_decor ( id INT PRIMARY KEY AUTO_INCREMENT, page_key VARCHAR(64) NOT NULL COMMENT '页面标识,如 home、category', schema_json TEXT NOT NULL COMMENT '可视化布局组件JSON,含组件类型、排序、props', version INT DEFAULT 1 COMMENT '乐观锁版本号', updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_page_key (page_key) );

page_key对应一个页面,schema_json存的是后台拖拽配置产生的完整组件描述,version字段解决多人同时编辑首页互相覆盖的问题——保存时带上version,更新语句里带where version=旧版本号,受影响行数为0就提示用户刷新重试。这个设计不算复杂,但很多商城源码把这部分直接写死在前端配置里,后台一发布整个首页结构就不可控了。在真实运营场景里,运营改首页的频率远高于开发发版的频率,把页面结构落库是最低成本的解耦方式。

2.3 数据流串一遍:从装修JSON到小程序渲染

完整的渲染链路是这样的:运营在后台管理端拖拽组件、填写图片地址和跳转链接,保存时前端把组件数组序列化成JSON写入page_decor表;小程序端进入首页时发起请求获取装修配置,后端读出schema_json原样返回;Uni-App的首页拿到components数组后逐项渲染,根据type字段命中对应组件。整个链路里后端只负责存取JSON,不做任何模板解析,这意味着任何页面结构调整都不需要小程序发版。

装修配置JSON的结构大致如下:

{ "components": [ { "type": "banner", "props": { "images": ["https://cdn.example.com/banner1.jpg"], "interval": 4000 } }, { "type": "nav-grid", "props": { "items": [ { "icon": "https://cdn.example.com/icon1.png", "url": "/pages/goods/list?cate=1" } ] } }, { "type": "goods-list", "props": { "source": "manual", "goodsIds": [101, 102, 103] } } ] }

这个结构明确了可视化布局的边界:banner是轮播图,nav-grid是宫格导航,goods-list是商品列表,source字段决定商品来源是手动指定还是按分类自动拉取。后端读库时不需要理解这些字段的含义,前端渲染层是唯一需要消费props的地方。首页标题也可以放在这个JSON的顶层,前端渲染完组件后调用uni.setNavigationBarTitle动态设置,而不是写死在pages.json里——同一个页面在不同活动下可以展示不同标题。这一条数据流看懂之后,后面讲后端接口和前端组件时就不会迷路。

3. 后端落地:JWT鉴权、商品接口与微信支付回调的写法

3.1 微信登录换openid:code换openid的接口与session_key处理原则

小程序登录的第一步永远是uni.login拿到临时code,然后由前端把code发给自己的后端,由后端用appid和secret去微信接口换openid和session_key。这个过程里最核心的原则是:code只能用一次且五分钟有效,session_key只能留在后端,绝不能下发到小程序端,否则等于把解密用户敏感数据的能力交给了前端。

后端换openid的核心方法如下:

public async Task<WxSession> Code2SessionAsync(string code) { var url = $"https://api.weixin.qq.com/sns/jscode2session?appid={_appId}&secret={_appSecret}&js_code={code}&grant_type=authorization_code"; using var http = new HttpClient(); var resp = await http.GetStringAsync(url); var json = JsonDocument.Parse(resp); if (json.RootElement.TryGetProperty("errcode", out var err)) { throw new WxApiException($"jscode2session失败: {err.GetInt32()} {json.RootElement.GetProperty("errmsg").GetString()}"); } return new WxSession( json.RootElement.GetProperty("openid").GetString(), json.RootElement.GetProperty("session_key").GetString()); }

这段代码里有几个参数要特别注意。appid和secret来自小程序后台,secret在生产环境必须放到配置或环境变量里,不能写死在代码仓库,很多翻车事故就是secret被提交到Git后被人拿去刷接口。http.GetStringAsync这里直接用了默认超时,建议显式设置10秒超时并做重试——微信接口偶尔抖动,商城登录入口一卡用户会直接流失。session_key每次登录都会变化,不要拿它做长期业务标识,业务标识应该用openid或你自己生成的userId。

3.2 JWT鉴权中间件:把用户身份塞进HttpContext

登录成功后后端签发JWT,前端存到storage里,每次请求在Authorization头带上。源码里JWT的签发和验证都集中在Startup里配置,Issuer、Audience、过期时间、签名密钥四项是必填参数,过期时间默认两个小时。中间件负责解析token并把userId和openid塞进HttpContext.Items,Controller里随时取用户身份,不用每个接口重复写解析逻辑。

中间件核心代码如下:

public async Task InvokeAsync(HttpContext context) { if (context.Request.Headers.TryGetValue("Authorization", out var token)) { var rawToken = token.ToString().Replace("Bearer ", ""); var principal = _validator.ValidateToken(rawToken); if (principal != null) { context.Items["UserId"] = principal.FindFirst("uid")?.Value; context.Items["OpenId"] = principal.FindFirst("openid")?.Value; } } await _next(context); }

这个中间件不主动拦截请求,token无效时只是不写入用户身份,由后续Controller根据业务需要决定是否要求登录。这样设计的好处是兼容游客访问商品列表和需要登录的下单接口,Action上标注匿名访问的就不查用户,标注需要认证的才判断context.Items里有没有UserId。小额商城不必上OAuth那套复杂体系,JWT配合过期时间已经够用,重点是token刷新策略要想清楚。

源码里前端request拦截器会在收到401时调refresh接口换新token,后端refresh接口校验旧token里的userId,查询用户仍在有效期就重签一个。如果你不想维护refresh token链路,最简单的替代方案是把过期时间拉长到7天,代价是用户换设备后旧token无法主动失效,二选一看你的安全等级要求。

3.3 微信支付回调:签名验证和订单状态幂等

微信支付的流程是前端先调后端下单接口,后端调微信统一下单接口拿到prepay_id并生成二次签名返回给前端,前端再用wx.requestPayment拉起收银台。支付完成后微信服务器会向配置的NotifyUrl发起异步回调,这笔回调才是订单状态更新的唯一可靠来源。

回调接口的处理有两个硬要求,第一是验签,第二是幂等。验签是为了确认请求确实来自微信而不是伪造的订单通知;幂等是为了防止重复回调导致订单状态被反复修改。用更新语句本身实现幂等是最省事的方案:

[HttpPost("notify")] public async Task<IActionResult> PayNotify() { using var reader = new StreamReader(Request.Body); var body = await reader.ReadToEndAsync(); if (!WxPaySign.Verify(body, _apiKey)) return BadRequest("sign error"); var xml = XDocument.Parse(body); var orderNo = xml.Root.Element("out_trade_no").Value; var txId = xml.Root.Element("transaction_id").Value; int updated = await _orderRepo.MarkPaidAsync(orderNo, txId); if (updated == 0) return Ok(WxPayResult.Fail); return Ok(WxPayResult.Success); }

MarkPaidAsync的SQL模板是update orders set status='paid', transaction_id=@txId where order_no=@orderNo and status='pending',where里的status条件就是幂等关键。第一次回调时status=pending,更新成功受影响行数为1;第二次重复回调时status已经是paid,更新影响0行,直接返回成功给微信服务器就好。这里注意不要用先查后更新的做法,两个回调并发时会同时查到pending状态,导致重复发货。另外回调里不能做耗时操作,发优惠券、计算积分这类逻辑应该丢到消息队列或后台任务里,微信回调通常要求5秒内响应,超时会重试多次。

4. 前端落地:Uni-App登录、商城页面与可视化装修的联动机制

4.1 uni.login与手机号授权:登录code与getPhoneNumber的code别搞混

Uni-App端登录分两步。第一步用uni.login拿登录code,这个code调用后端登录接口换业务token;第二步是用户点击手机号授权按钮,拿到另一个code,这个code用于换取手机号。两个code用途完全不同,接口也完全不同,经常有人把getPhoneNumber的code拿去调jscode2session,结果自然报错。

登录代码常规是写成这样:

uni.login({ provider: 'weixin', success: async (loginRes) => { const { data } = await api.post('/auth/login', { code: loginRes.code }); uni.setStorageSync('token', data.token); uni.setStorageSync('userInfo', data.userInfo); } });

按钮绑定用open-type="getPhoneNumber",当前规范是组件只返回一个code,不再直接返回加密的phoneData:

<button open-type="getPhoneNumber" @getphonenumber="onGetPhoneNumber">微信一键登录</button>
onGetPhoneNumber(e) { if (e.detail.errMsg !== 'getPhoneNumber:ok') return; api.post('/user/bind-phone', { code: e.detail.code }) .then((res) => { uni.showToast({ title: '绑定成功', icon: 'success' }); }); }

后端拿到这个code后需要再调微信的接口换取手机号,注意这里换手机号依赖的是小程序全局access_token,和普通登录的session_key不是同一个东西,需要提前在TokenManager里维护和刷新。还有一点,手机号快速验证组件现在必须是企业主体才能调用,个人主体小程序拿不到手机号授权;如果你的主体是个人,就得先降级做openid登录,业务需要绑定手机号时再用短信验证码补。这是当前微信生态里最容易被旧教程误导的地方,很多老文章还在讲解密encryptedData,那套逻辑现在已经走不通了。

注意:个人主体小程序无法调用getPhoneNumber,这是平台侧限制,不是代码能绕过的。

4.2 首页动态渲染:装修JSON如何映射成组件树

前端拿到装修JSON之后,首页不再是写死的模板,而是数据驱动的组件序列。Uni-App的Vue模板里核心思路是遍历components数组,按type命中不同的组件。每个组件拆成独立文件,用条件渲染的方式挂载:

<template> <view class="home-page"> <template v-for="(item, index) in pageConfig.components" :key="index"> <Banner v-if="item.type === 'banner'" :config="item.props" /> <NavGrid v-else-if="item.type === 'nav-grid'" :config="item.props" /> <GoodsList v-else-if="item.type === 'goods-list'" :config="item.props" /> <CouponBar v-else-if="item.type === 'coupon'" :config="item.props" /> </template> </view> </template>

组件类型和props的对应关系如下:

组件类型用途props必填项
banner首页轮播图images数组、interval
nav-grid导航宫格items数组(icon、url)
goods-list商品列表source(manual/category)、goodsIds或categoryId
coupon优惠券入口couponIds数组

如果后来要加直播组件、秒杀组件,渲染层只需要新增一个组件文件并在这段模板里加一行分支,同时在后端装修配置面板里加拖拽项。组件类型多了之后建议把v-if链改成动态组件component :is="map[item.type]",前提是所有组件已注册,否则匹配不到时组件会是undefined,页面直接渲染失败。如果你打开源码发现Uni-App端还是Vue 2的老写法,v-for和v-if同时用会有优先级差异,处理方式是把模板改成v-for包一层view再判断组件类型。实际项目里还要注意props兜底——后端返的JSON里缺少字段时,组件内部要用默认值兜住,比如banner的images为空数组时直接跳过渲染。

这里有个容易被忽略的联动点:装修JSON不只管布局,还管页面级信息。首页标题在pages.json里写固定值,但运营在后台改了首页标题,前端需要在拿到JSON后调用uni.setNavigationBarTitle覆盖掉。页面标题、是否显示导航栏、背景色这些页面级配置,建议都放进schema_json的顶层字段,前端统一处理。

4.3 下单与支付调起:requestPayment的参数一个都不能错

商品详情页、购物车、订单确认页的逻辑和其他商城大同小异,这里只讲最容易出问题的支付环节。下单接口提交商品和收货地址后,后端创建订单返回orderNo,接着调支付预下单接口获取拉起收银台的参数。前端调用wx.requestPayment的代码长这样:

const prepay = async (orderNo) => { const { data } = await api.post('/pay/prepay', { orderNo }); return wx.requestPayment({ timeStamp: data.timeStamp, nonceStr: data.nonceStr, package: data.package, signType: 'RSA', paySign: data.paySign, success: () => refreshOrderStatus(orderNo), fail: (err) => { // 用户取消支付或支付失败时,不要在这里改订单状态 uni.showToast({ title: '支付未完成', icon: 'none' }); } }); };

pay/prepay接口返回的timeStamp是字符串,package是prepay_id=xxx的完整串别拆开,signType在微信支付API v3里固定是RSA,后端签名用的私钥从微信支付商户平台下载的证书里提取。支付成功的判断建议前端success回调和后端回调双确认,因为iOS上存在极端情况——用户指纹支付成功后前端没收到回调,导致页面一直停在未支付状态。正确做法是success里只调订单状态接口,以后端查询结果为准,不要拿回调结果直接改本地缓存订单状态。

5. 部署与打包避坑:从Visual Studio到微信开发者工具的五条血泪记录

5.1 接口全部500:Linux服务器上少了ASP.NET Core运行时

现象:本地Visual Studio调试一切正常,发布到Linux服务器后所有接口直接返回500,看服务器日志也找不到具体异常。

原因:源码用的是framework-dependent发布模式,服务器上只装了.NET Runtime没有装ASP.NET Core Runtime,或者装的是其他大版本。.Net 5.0的运行时和ASP.NET Core Runtime是两个独立安装包,只装前者会让依赖Microsoft.AspNetCore.App的接口全部起不来。

解决:先跑dotnet --info确认版本,再执行sudo apt-get install aspnetcore-runtime-5.0,装完重启进程。如果服务器环境特殊不想装运行时,发布会改用self-contained模式,把整个运行时打进发布目录,代价是发布包体积增大几十兆。还要顺带检查appsettings.json里数据库连接串的密码有没有特殊字符,连接字符串解析失败也表现为500,而且这类问题在Linux上尤其难排查,因为日志级别默认是Information,连接失败的堆栈会被吞掉。

5.2 开发者工具能编译但真机白屏:appid与合法域名是两座山

现象:微信开发者工具里页面正常渲染,但点预览用手机扫码打开后白屏,或者部分图片和接口请求全部失败。

原因:第一层是appid问题,开发者工具里用了测试号,测试号没有真实小程序的合法域名配置,正式版小程序只允许请求已备案的HTTPS域名;第二层是域名配置问题,后台没配置request合法域名,或者开发阶段直接用了IP地址。

解决:在开发者工具项目详情里换成真实小程序的appid,把接口域名在小程序后台的“开发管理-开发设置-服务器域名”里配置request和downloadFile合法域名。开发阶段可以在开发者工具里勾选“不校验合法域名”,但这条只是给本地联调用的,发布前必须取消勾选。图片域名要走downloadFile合法域名,很多人接口通了但图片裂了,问题就出在只配了request没配downloadFile。backgroundAudio等媒体资源也要单独配,这是小程序端最容易漏掉的细节。

5.3 getPhoneNumber拿不到手机号:基础库版本与主体类型是硬条件

现象:点击“微信一键登录”按钮没有反应,或者e.detail里只有errCode没有手机号相关信息,控制台报错信息指向基础库版本。

原因:手机号快速验证组件要求微信基础库版本不低于2.21.2,且小程序主体必须是企业、个体户等非个人类型。个人主体小程序调getPhoneNumber会直接失败,这是平台侧限制,不是代码能绕过的。

解决:在manifest.json的mp-weixin配置里设置最低基础库版本,通常写2.30.0比较稳,同时检查小程序后台账号类型。如果主体暂时无法变更,业务上可以降级处理:用户用openid登录,在需要手机号的环节改用短信验证码,等认证通过后再切换。另外getPhoneNumber按钮必须在用户点击的事件回调里触发,不能写进onLoad里自动执行,违者同样被微信拦截。

5.4 H5端正常但小程序端白屏:条件编译和pages.json路径

现象:同一套Uni-App代码在H5端跑得很流畅,打包成微信小程序后白屏,或者进入某个页面报找不到页面。

原因:代码里用了window或document这类浏览器专有API,H5运行时能容错跳过去,小程序端的JavaScript环境里这些对象根本不存在,代码在启动阶段就抛异常导致白屏。另一个高频原因是pages.json里注册的页面路径和实际文件路径不一致,H5端的路由容错性很强,跳转失败也能降级显示,小程序端则直接报页面不存在。

解决:全项目搜索window和document的使用点,用条件编译包一层:

// #ifdef H5 window.scrollTo(0, 0); // #endif // #ifdef MP-WEIXIN wx.pageScrollTo({ scrollTop: 0 }); // #endif

pages.json里的路径逐条对照文件目录核对,保持字母大小写一致。还有个更隐蔽的坑是用了ES6+的高阶语法,编译目标调成微信开发者工具当前稳定支持的版本,一些新语法在老基础库上表现不一致,打包工具不会报错但运行时行为有差异。

5.5 抓包看参数被“加密”:Content-Type和JSON序列化不一致

现象:前端传参后端接收到null,拿抓包工具看请求体是一串被转义的字符串,跟后端定义的模型完全对不上。

原因:后端接口用的是[FromBody]但前端请求头Content-Type是text/plain,或者后端用了[FromForm]但前端传的是JSON字符串,两边协议没对齐。Uni-App的uni.request默认Content-Type是application/json,但有些封装会在上传接口时改成表单格式,改回来的时候漏了几个接口。

解决:先用抓包工具确认请求头的Content-Type和请求体格式,后端接口如果接收JSON体就统一标注[FromBody],并约定请求头为application/json; charset=utf-8。前端把uni.request做一层统一封装,在header里强制写死Content-Type,不允许单个接口覆盖。开发者工具自带的Network面板足够排查这类问题——请求体里参数是否被引号包裹、字段名和模型属性名大小写是否一致,一眼就能看出来。这类问题排查成本很低,但最容易让人误以为是后端逻辑错误而反复调试。

6. 进阶:把可视化布局引擎拆出来,用JSON Schema约束组件扩展

6.1 现有引擎的扩展点在哪

可视化布局系统最怕的就是运营配置了一个不合法的JSON,前端渲染直接崩掉。这套源码的扩展点其实很清楚:后台管理端拖拽面板、后端page_decor表存JSON不用改表结构、前端渲染层新增组件分支,三端改完才能上一个新组件。问题在于后台保存JSON时没有任何校验,运营在表单里填错一个字段,比如banner的images传成了字符串而不是数组,前端渲染层拿到后遍历字符串会直接报错。

6.2 用JSON Schema把配置错误挡在后台

我的做法是在后端保存接口里加一层JSON Schema校验,让配置错误在后台保存时就暴露,而不是等到小程序端白屏。JSON Schema草案07的写法是声明合法结构,然后后端保存时校验:

{ "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "required": ["components"], "properties": { "components": { "type": "array", "items": { "type": "object", "required": ["type", "props"], "properties": { "type": { "enum": ["banner", "nav-grid", "goods-list", "coupon"] }, "props": { "type": "object" } } } } } }

后端保存前用Newtonsoft.Json.Schema做一次校验,不通过就返回配置错误的具体路径,比如components[0].props.images必须是数组,运营在后台立刻看到提示而不是线上事故。前端渲染层再加一道兜底,遇到未注册的组件类型时跳过而不是抛异常:

const componentMap = { banner: Banner, 'nav-grid': NavGrid, 'goods-list': GoodsList, coupon: CouponBar }; // 渲染前先判断是否存在对应组件 if (componentMap[item.type]) { // 动态渲染 }

这个兜底逻辑同样适用于后台误存了未来版本的新组件类型——旧版本小程序不会崩,只会跳过,等前端发版后自然生效。我最早调试这套商城时就是因为没有校验,运营误配了一个空图片地址的banner,整个首页组件渲染直接崩掉,查了一晚上才发现是JSON里一个字段拼错了。从那以后我每次加组件,都强制先更新JSON Schema再写前端分支,配置错误在后台就被拦住。这个习惯帮我少踩了很多次坑,希望帮到你。

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

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

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

立即咨询