
简介这是一份微信小程序商城系统完整源码面向小程序开发者、电商创业团队及计算机专业学生可直接运行或在此基础上进行二次开发。包内含商品展示、购物车、地址管理、结算下单、搜索等核心功能模块前端页面与后台逻辑分层清晰适合用于快速搭建线上商城原型或作为课程设计与毕业设计参考。压缩包共198个文件主要由46个JS逻辑文件、38个JSON配置、38个WXSS样式、37个WXML页面结构以及37个PNG图片资源组成还附带LICENSE与MD说明文档整体仅249KB结构紧凑方便下载与部署调试。目前已有5000余人学习下载整体反馈良好。通过阅读源码可以掌握小程序组件化开发思路、微信支付接口对接流程、数据缓存与请求封装技巧是一份兼具教学与实战价值的小程序商城入门资料。1. 一套能直接对照业务链路的商城源码我拆过不少小程序项目大多数标着“完整商城系统”的源码包解压后全是云开发模板和登录逻辑真正常用的业务代码反而翻不到。这套资源比较“反直觉”——它没有堆复杂框架核心价值全在goods.js、cart.js、checkout.js、addressAdd.js和showdown.js这批模块文件里几乎就是一个小程序商城从商品展示、搜索、购物车、结算到地址管理的完整链路。适合两类人一是刚接手微信小程序商城开发、想找一套简洁可改造的代码骨架二是已经在写业务、但卡在富文本渲染、结算参数透传这类细节上的熟手。解压后直接看pages目录下和根目录的utils目录就能对上我下面讲的分析路线。2. goods.js 的商品数据流与搜索模块选型2.1 从页面结构反推商品模块的分层先把项目里和商品相关的模块理清楚goods.js管商品列表与详情search.js管关键词检索cart.js管加购checkout.js管结算确认。这种分层在微信小程序里属于比较标准的“页面 服务”结构——页面文件负责交互业务逻辑集中写在utils和services层方便调试也方便后续接接口。我一般拿到这类源码习惯先看goods.js里的Page({})生命周期重点看onLoad和onShow里做了什么。// 商品列表页的简化版加载逻辑 Page({ data: { goodsList: [], // 商品列表 pageNum: 1, // 当前页 pageSize: 10, // 每页条数 hasMore: true, // 是否还有下一页 loading: false // 防重复请求 }, onLoad: function (options) { this.loadGoods(true); }, loadGoods: function (reset) { if (this.data.loading || (!reset !this.data.hasMore)) return; this.setData({ loading: true }); const pageNum reset ? 1 : this.data.pageNum; const params { pageNum: pageNum, pageSize: this.data.pageSize }; wx.request({ url: app.globalData.apiBase /api/goods/list, data: params, success: (res) { const list res.data.data; if (reset) { this.setData({ goodsList: list }); } else { this.setData({ goodsList: this.data.goodsList.concat(list) }); } this.setData({ pageNum: pageNum 1, hasMore: list.length this.data.pageSize, loading: false }); }, fail: () this.setData({ loading: false }) }); }, onReachBottom: function () { this.loadGoods(false); } });这段代码核心逻辑在loadGoods的reset参数下拉刷新时传true重置到第一页触底加载时传false往后追加。参数pageNum和pageSize是后端分页的标准字段接口返回字段data.data这种多层结构要从后端响应体里对出来。2.2 search.js 与商品过滤条件的组合方式搜索模块这块要看的重点不是wx.request本身而是它怎么把搜索关键词和过滤条件拼进请求参数。常见做法是维护一个filter对象搜索时把关键词、分类ID、排序规则合并到请求里。// 搜索页组合请求参数的逻辑 const buildSearchParams (keyword, categoryId, sortType) { return { keyword: keyword.trim() || , // 去掉首尾空格 categoryId: categoryId || 0, // 0 表示全部分类 sortType: sortType || default, // default / price_asc / price_desc / sales pageNum: 1, pageSize: 20 }; }; // 在页面里发起搜索请求 searchGoods() { const params buildSearchParams( this.data.keyword, this.data.selectedCategoryId, this.data.sortType ); wx.request({ url: app.globalData.apiBase /api/goods/search, data: params, success: (res) { this.setData({ goodsList: res.data.data.list }); this.setData({ totalCount: res.data.data.total }); } }); }参数keyword用trim()处理是为了避免用户输入空格导致搜索不到商品这一常见的坑sortType建议和后端约定好白名单只允许固定几个值防止非法排序参数穿透到 SQL 层。如果你把这个模块接到自己的后端可以把sortType的映射关系做成一张常量表放在config.js里前端只传语义值后端负责翻译成具体排序字段。2.3 商品列表渲染的性能取舍商品列表的wxml层一般会用到wx:for和wx:key这个源码里也不例外。这里有个值得注意的点goods.js里把商品图片 URL 做了 ** 域名白名单 ** 处理开发阶段容易忽略真机预览时图片全部加载不出来多半是没在小程序后台配置合法域名。另外如果是长列表超过 30 条建议用recycle-view或virtual-list组件做节点回收普通wx:for在低端安卓机上滑到后面会很吃力。商品卡片一般包含title、price、image、sales这四类字段。价格建议后端返回“分”单位前端再转“元”避免浮点运算导致价格显示错乱。商品详情的富文本内容则先落到detail字段交给showdown.js做渲染这部分我在第 4 章展开讲。3. cart 与 checkout购物车状态管理和结算接口对接3.1 购物车模块的本地存储策略购物车在移动端和小程序里最常见的设计是“本地暂存 服务端同步”。这套源码里的cart.js用的是本地缓存方案把购物车数据存放在wx.setStorageSync里这种做法的优点是减少对服务端压力缺点是换设备或者清缓存后购物车记录丢失适合学习和小规模运营场景。看cart.js源码时重点观察加购时如何判断商品是否已经存在// 加入购物车 addToCart(product) { const cart wx.getStorageSync(cart) || []; const idx cart.findIndex(item item.goodsId product.goodsId); if (idx -1) { // 已存在则数量 1 cart[idx].count 1; cart[idx].selected true; } else { // 不存在则新插入默认选中 product.count 1; product.selected true; cart.push(product); } wx.setStorageSync(cart, cart); this.setData({ cartCount: this.calcTotalCount(cart) }); }这里判断用的是goodsId而不是id或productId字段命名要和商品接口对齐。一个非常容易踩的坑是findIndex在低版本基础库上的兼容性真机调试时如果老机型报错建议改为for循环加标志位。购物车徽标的角标数用calcTotalCount计算这里的“总数量”是数量之和不是商品种类数运营角度两者都有用但 UI 上要确定口径。3.2 购物车的选中状态与全选逻辑全选和单选这组联动逻辑写不好很影响体验。源码里如果直接维护一个布尔值allSelected那么在“部分选中”和“全部选中”之间切换时要小心边界条件。// 单选 toggleItem(index) { const cart this.data.cart; cart[index].selected !cart[index].selected; const allSelected cart.every(item item.selected); this.setData({ cart: cart, allSelected: allSelected }); }, // 全选 toggleAll() { const allSelected !this.data.allSelected; const cart this.data.cart.map(item { item.selected allSelected; return item; }); this.setData({ cart: cart, allSelected: allSelected }); }这段逻辑在数据量小的时候没问题但如果购物车里放了几百个商品every和map每次都遍历全量数组会有一定性能损耗。常见优化是把selectedCount和totalPrice做成计算属性在cart变化时统一算一次或者在操作单个商品时用增量更新只改那一个商品的选中状态。还有一点容易踩的坑是cart[index]这种索引修改方式触发了setData但如果商品的selected字段后台上没有返回every会把undefined判为false所以加购写入缓存时一定要初始化为布尔值。3.3 checkout.js 的结算流程与参数透传结算页通常从购物车跳转过来携带一组选中商品 IDcheckout.js再根据这些 ID 从缓存或服务端重新拉取商品信息。这里要做的是“以服务端价格为准”不能直接用前端缓存里的价格参与计算避免被篡改。// 结算页加载确认数据 onLoad(options) { const goodsIds JSON.parse(options.goodsIds || []); wx.request({ url: app.globalData.apiBase /api/order/confirm, method: POST, data: { goodsIds: goodsIds, // 商品ID数组 addressId: 0 // 0 表示未选择地址 }, success: (res) { const data res.data.data; this.setData({ goodsList: data.goodsList, // 服务端返回的商品清单 totalPrice: data.totalPrice, // 服务端计算的总价单位分 freight: data.freight, // 运费 couponList: data.couponList // 可用优惠券 }); } }); }注意这里的POST请求体里放了goodsIds数组小程序wx.request的data会自动序列化为 JSON。如果你后端是 Java 或 PHP 接口要注意框架接收数组参数的键名格式有的要求传goodsIds1,2,3这种字符串然后用逗号拆。这个差异是前后端联调最常见的返工点。结算流程里另一个高发问题是“重复提交”。用户连续点两次“立即支付”可能生成两个订单。常见解决办法是提交前做一个submitting标志位拦截submitOrder() { if (this.data.submitting) return; this.setData({ submitting: true }); wx.request({ url: app.globalData.apiBase /api/order/create, method: POST, data: this.buildOrderPayload(), success: (res) { // 跳转支付页 wx.navigateTo({ url: /pages/pay/pay?orderId res.data.data.orderId }); }, complete: () this.setData({ submitting: false }) }); }submitting标志位在complete回调里复位这样即使请求失败也能继续提交不会出现“请求失败后一直点不了”的情况。3.4 订单状态流转的枚举设计checkout.js和后续的订单列表页如果依赖后端返回的订单状态字段建议把状态和文字描述的映射关系集中放到一个orderStatus.js常量文件里。比如0-待支付、1-已支付待发货、2-已发货、3-已完成、4-已取消、5-退款中。这样页面里直接用状态码查描述避免后端改文案后前端要到每个页面去替换。源码里如果只有订单状态数字没有文案映射这一块值得自己补上。4. addressAdd 地址表单校验与 Markdown 富文本渲染链路4.1 地址模块的前端校验细节addressAdd.js是收货地址新增/编辑页面。这个页面在很多商城里被忽略但它的校验逻辑其实很有讲究。表单里常见的字段有收货人、手机号、省市区、详细地址、默认地址开关。校验集中在手机号和地址完整性这两块。// 地址表单校验 validateForm() { const { name, phone, region, detail } this.data.form; if (!name.trim()) { wx.showToast({ title: 请填写收货人, icon: none }); return false; } const phoneReg /^1[3-9]\d{9}$/; if (!phoneReg.test(phone)) { wx.showToast({ title: 请填写正确的手机号, icon: none }); return false; } if (!region.province || !region.city || !region.county) { wx.showToast({ title: 请选择省市区, icon: none }); return false; } if (detail.trim().length 5) { wx.showToast({ title: 详细地址过短, icon: none }); return false; } return true; }正则^1[3-9]\d{9}$目前覆盖了主流号段但物联网卡、虚拟运营商的规则会变更稳妥的做法是结合后端sdk做短信验证。省市区字段建议用微信自带picker的moderegion它返回的是province/city/county数组结构后端如果习惯存行政编码需要自己额外做一层映射。需要提醒的是“详细地址过短”这种校验容易误伤有些用户的具体门牌号确实很短校验逻辑最好只检查必填不要限制最少字符数地址真实性由快递验证环节兜底。4.2 showdown.js 与商品详情的富文本渲染链路商品详情如果后台录入的是 Markdown 格式小程序原生rich-text组件是不认识 Markdown 语法的所以这里引入了showdown.js来做 Markdown - HTML 转换。再配合html2json.js和htmlparser.js把 HTML 字符串转换成rich-text需要的节点数组。这条链路是这套源码里最有学习价值的模块组合。// Markdown - 小程序 rich-text 可渲染的节点 const showdown require(../../utils/showdown.js); const html2json require(../../utils/html2json.js); function markdownToRichText(markdownText) { // step1: markdown 转 html const converter new showdown.Converter({ tables: true, // 支持表格 strikethrough: true, // 支持删除线 tasklists: true // 支持任务列表 }); const html converter.makeHtml(markdownText); // step2: html 字符串转 json 节点数组 const richTextNodes html2json(html); return richTextNodes; }这段代码有两个关键参数值得注意showdown.Converter的配置项tables和tasklists很多后台编辑器的 Markdown 内容包含表格或任务列表不开启这几个选项内容会直接丢失。另外html2json转换的节点后续要直接传给rich-text组件的nodes属性如果渲染出来没有样式是正常的——rich-text的节点标签默认不带 CSS需要在小程序wxss里给rich-text内部的img、p、div选择器加样式。4.3 渲染链路里最容易出问题的三个点第 1 个坑是图片宽度。后台 Markdown 里的图片通常没有限制宽度在 PC 端显示正常但在小程序里如果图片宽度超出了rich-text容器的宽度会把页面撑破出现横向滚动条。解决办法是在html2json解析时对img节点统一给出style: max-width:100%;height:auto;。第 2 个坑是wxDiscode.js。这个文件是微信小程序富文本解析的一个辅助模块负责把 HTML 中的nbsp;这类实体编码转换成空格。如果html2json解析出来的文本里出现大量乱码符号多半是wxDiscode没有被正确调用。第 3 个坑是代码块precode。rich-text对pre标签的默认样式处理很弱如果商品详情里经常有代码片段建议在转换前给pre标签加背景色和内边距的样式否则展示效果非常差。// 给解析后的节点统一补齐样式 function normalizeRichTextNodes(nodes) { nodes.forEach(node { if (node.name img) { node.attrs.style max-width:100%;height:auto;; } if (node.name pre) { node.attrs.style background:#f6f6f6;padding:12rpx;border-radius:8rpx;overflow-x:auto;; } }); return nodes; }这段补齐样式的逻辑是实际开发中必须补的一步源码里如果没写就自己加上。另外这条渲染链路只适用于展示 Markdown如果后台内容是 HTML 富文本编辑器直接生成的格式标签完全不同需要单独走htmlparser.js解析不要混用两种格式。5. 源码解压后的快速验证与改动技巧5.1 压缩包校验与解压注意事项拿到zip包先校验完整性Windows 下右键属性可以看到文件大小解析前确认不是“损坏压缩包”。解压工具建议用 7-Zip 或 WinRAR 的最新版本老版本的解压工具对中文文件名支持不好源码里如果目录名带中文解压后会出现乱码路径微信开发者工具直接打不开项目。如果解压时遇到invalid zip archive: could not find eocd这类报错先用压缩软件自带的“修复压缩文件”功能处理一次再不行就重新下载。5.2 在微信开发者工具里跑起来的三个步骤拿到源码后在开发者工具里按下面的顺序操作能避开大部分“运行不起来”的问题。# step1: 确认项目目录结构 ls # 正常应该看到 app.js、app.json、pages 目录、utils 目录 # step2: 编译前先检查 appid # 在 project.config.json 里替换成自己的 appid # 如果你的 appid 没有开通相应接口权限直接用测试号 # step3: 打开安全域名校验开关 # 开发阶段建议在“详情-本地设置”里勾选“不校验合法域名”第 2 步切测试号在个人开发者账号下很实用没有企业资质也能调通页面逻辑。第 3 步勾选“不校验合法域名”后wx.request才能访问http://开头的本地开发环境接口不然代码里所有请求都会被拦截页面看着是空壳。5.3 页面加载动画“修改刚进入的加载页面”的默认改动源码包里的“加载中”状态如果不好看可以换成本地图片资源占位。常见做法是在app.js或首页的onLoad里控制loading状态开源码里如果找不到可以在pages/index/index.js里加一个定时器实现 1.5 秒的加载过渡效果。要改动的时候看wxml里有没有wx:if{{loading}}这种条件渲染图片资源替换完成后重新编译即可看到效果。如果想直接调整顶部导航栏的高度适配问题部分机型刘海屏会遮挡标题可以通过wx.getSystemInfoSync().statusBarHeight拿到状态栏高度再给自定义导航栏的容器padding-top做动态适配。5.4 如何快速验证这套源码的完整性拿到源码之后我一般会按照功能链路过一遍。前端验证重点看这几个页面的跳转关系首页 - 商品列表 - 商品详情 - 购物车 - 结算 - 地址选择 - 提交订单。如果源码里没有后端支撑那么所有wx.request的接口都是 mock 状态可以先用 Charles 或 Burp Suite 的方式抓包看请求参数结构自己写个简单的服务端返回假数据即可跑通全流程。这里注意抓包的是微信开发者工具里的网络请求不是小程序线上环境避免被微信的网络安全策略挡掉。5.5 给二次开发者的模块增补建议这套源码的用处不只是“能跑”更大的价值在于你能看到一个小程序商城的代码边界在哪里。建议做三件事一是把utils里各模块的依赖关系画成一张简单的逻辑清单搞清楚哪些是公共库、哪些是业务库二是把goods.js与search.js里的接口参数整理成一份接口文档后续接后端时直接对着改 URL三是把cart.js的本地缓存逻辑抽成独立的cartService.js后续做登录态同步时只需要替换这一个文件。要特别注意“微信支付 v3 对接”这一环节——源码里通常只留了支付按钮的跳转和回调实际签名与调起支付需要后端的配合如果后端没有完成prepay_id的生成前端是调不起支付弹窗的。这一步卡住了先检查后端返回的timeStamp、nonceStr、package参数是否正确而不是盯前端代码。本文还有配套的精品资源点击获取