生鲜商城微信小程序源码解析:从全局配置到购物车订单支付实战
2026/9/15 11:11:31 网站建设 项目流程

简介:这套源码是2022年最新的我厨蔬菜生鲜商城小程序完整版,面向希望搭建在线生鲜购物平台的开发者或中小商家,整合了前端用户界面与后台管理功能,可支撑完整的电商交易流程。资源包共46个文件,大小121KB,以wxml、js、wxss、json等小程序核心代码为主,另有png、gif图片素材,分别承担页面结构、交互逻辑、样式布局、数据配置与视觉展示等功能,项目内含app.json、app.js、api、pages、utils、images等目录,便于按模块理解和二次开发。目前已有270人学习下载。通过学习可掌握生鲜商城小程序从商品展示、分类搜索、购物车、订单管理、支付接口到促销活动、后台管理等核心模块的实现思路,同时可参考其目录结构快速复现可运行的商城实例,是入门小程序电商开发与进行项目改造的实用参考资料。

1. 生鲜商城小程序的源码格局与上手路线

拆过几套电商类微信小程序源码的人,打开weixin_wochu-master这类目录时通常不会急着看代码,而是先看app.json里注册了哪些页面、pages目录下文件夹的命名是否规范。这套我厨蔬菜生鲜商城小程序源码属于典型的原生微信小程序项目结构,前端基于 WXML/WXSS/JS,后端依赖可配置的 API 接口,没有引入 vue 或 react 那套运行时,对小程序开发者来说反而更容易直接改造成自己的生鲜商城小程序。它的完整之处在于商品展示、购物车、订单、支付、会员、促销等电商闭环模块在 pages 层都有对应实现,适合想快速搭建一个可用原型、或者需要一份能跑通交易流程的参考项目的人。对于刚接触微信小程序商城开发的从业者,这套源码的价值在于能直接看到页面生命周期、全局状态、本地存储和微信支付回调如何在一个真实业务里串联,而不是零散的功能片段。

2. 小程序骨架:从 app.json 到页面注册

2.1 全局配置里的商城骨架

拿到源码后第一个要打开的文件就是app.json。它决定了小程序商城包含哪些页面、底部导航长什么样、窗口风格如何。常见的生鲜商城会把首页、分类、购物车、我的这四个入口放在tabBar里,这套源码也是这么设计的。看一个精简版的app.json配置:

{ "pages": [ "pages/index/index", "pages/category/category", "pages/cart/cart", "pages/user/user", "pages/goods/detail", "pages/order/confirm", "pages/order/list" ], "window": { "navigationBarBackgroundColor": "#07c160", "navigationBarTitleText": "我厨生鲜", "navigationBarTextStyle": "white", "backgroundColor": "#f6f6f6" }, "tabBar": { "color": "#999", "selectedColor": "#07c160", "list": [ { "pagePath": "pages/index/index", "text": "首页" }, { "pagePath": "pages/category/category", "text": "分类" }, { "pagePath": "pages/cart/cart", "text": "购物车" }, { "pagePath": "pages/user/user", "text": "我的" } ] }, "style": "v2", "sitemapLocation": "sitemap.json" }

这段配置说明了几件事:小程序的页面路由是声明式的,新增一个业务页面必须在pages数组里注册,不然wx.navigateTo跳转时会报 “page not found”。tabBar里的pagePath只能是注册过的页面,而且不能设置成navigationStyle: custom,否则底部导航会浮在自定义导航上面。生鲜商城的主题色通常是绿色,所以navigationBarBackgroundColor用了接近生鲜行业的绿。后续如果要把这套源码的皮肤改成其他品牌色,只需要统一替换这几个颜色值,不需要改动业务逻辑。

2.2 页面生命周期与全局数据共享

每个页面的Page()构造器里,生命周期顺序直接影响商城数据的加载时机。生鲜商城的首页需要同时拿到轮播图、推荐商品、公告信息,这些请求如果都堆在onLoad里,用户会看到白屏时间变长。常见做法是onLoad只做初始化参数读取,onShow里再刷新购物车角标,因为每次从分类页切回首页时onShow都会触发。

看一段典型页面逻辑:

Page({ data: { banners: [], goodsList: [], loading: true, cartCount: 0 }, onLoad(options) { this.loadBanners(); this.loadGoods(); }, onShow() { this.setData({ cartCount: wx.getStorageSync('cartCount') || 0 }); }, async loadBanners() { const res = await wx.request({ url: `${this.globalData.apiBase}/banners`, method: 'GET' }); if (res.statusCode === 200) { this.setData({ banners: res.data.data }); } }, loadGoods() { // 实际项目中这里会用 wx.request 请求商品接口 // 源码中这部分是 mock 数据,方便离线开发 } });

这里有几个参数值得注意。this.globalData是在app.js里定义的全局对象,用来存放apiBase、用户登录态、购物车同步状态等跨页面数据。cartCount从本地缓存读取,是为了避免每次进入首页都调用统计接口,减少服务端压力。wx.requesturl在开发阶段需要在小程序开发者工具中勾选“不校验合法域名”,正式上线则必须把域名配置到微信公众平台的白名单里,否则请求直接被拦掉,这是新手最容易卡住的点。

2.3 公共样式与组件化拆分的取舍

app.wxss里定义的公共类决定了整个商城的视觉一致性。生鲜小程序常见的公共样式包括价格文本(.price)、按钮(.btn-primary)、卡片间距、一行两列的商品网格等。源码里的app.wxss我把几个关键片段抽出来看:

page { background-color: #f6f6f6; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica, sans-serif; font-size: 28rpx; color: #333; } .price { color: #ff4d2d; font-weight: 600; display: inline-block; } .price-symbol { font-size: 24rpx; margin-right: 2rpx; } .goods-card { background: #fff; border-radius: 16rpx; padding: 20rpx; margin-bottom: 20rpx; box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04); }

在手机端,rpx是响应式单位,以 750 设计稿为基准,一套样式在不同机型上等比缩放。生鲜商品图多,goods-card的圆角和阴影能提升质感,但阴影不能太重,否则长列表滚动时会掉帧。这套源码没有把所有页面都拆成自定义组件,比如商品卡片在首页、分类页、搜索结果页里各写了一遍模板。如果要做成可维护的项目,我会建议把商品卡抽象成components/goods-card,用properties接收商品对象,这样改价格展示样式时只动一处。不过对于学习源码来说,重复代码反而方便对照着看不同页面的差异。

3. 商品展示与分类搜索:数据流与筛选实现

3.1 商品列表页的数据组织

生鲜商城的商品数据通常包含goods_idnamepriceoriginal_pricestockcategory_idimage_urlis_sale这些字段。源码里的商品列表页pages/category/category使用的数据结构基本一致,但有一个细节值得学习:全部商品会一次性拉取到本地,再通过前端setData筛选,而不是每次切换分类都重新请求接口。这样做的好处是切换分类时没有网络延迟,筛选响应很快,坏处是当商品数据量超过几百条时,小程序setData的性能会明显下降,页面出现卡顿。对于生鲜商城这种 SKU 数量并不夸张的场景,前段筛选是合理的。

看一个商品列表的筛选代码:

const app = getApp(); Page({ data: { categories: [], currentCategoryId: 0, allGoods: [], filteredGoods: [], keyword: '', sortType: 'default', // default | price_asc | price_desc | sales page: 1, pageSize: 10, hasMore: true }, onLoad() { // 从 app.globalData 取出商品数据,实际项目里可能来自 wx.request this.setData({ categories: app.globalData.categories, allGoods: app.globalData.goods }); this.filterGoods(); }, filterGoods() { const { allGoods, currentCategoryId, keyword, sortType } = this.data; let tempList = allGoods.slice(); // 分类筛选 if (currentCategoryId !== 0) { tempList = tempList.filter(item => item.category_id === currentCategoryId); } // 关键词搜索 if (keyword.trim()) { tempList = tempList.filter(item => { return item.name.indexOf(keyword.trim()) !== -1; }); } // 价格排序:用 sort 直接操作副本 if (sortType === 'price_asc') { tempList.sort((a, b) => a.price - b.price); } else if (sortType === 'price_desc') { tempList.sort((a, b) => b.price - a.price); } else if (sortType === 'sales') { tempList.sort((a, b) => b.sales - a.sales); } this.setData({ filteredGoods: tempList, page: 1, hasMore: tempList.length > this.data.pageSize }); }, onSearchInput(e) { this.setData({ keyword: e.detail.value }); this.filterGoods(); }, onCategoryTap(e) { const cid = e.currentTarget.dataset.id; this.setData({ currentCategoryId: cid }); this.filterGoods(); }, loadMore() { // 触底时根据 page 和 pageSize 截取数据,模拟分页加载 } });

这段代码里tempListallGoods.slice()出来的副本,排序用tempList.sort()不会污染原始数据。filterGoods被三个事件共用:搜索框输入、分类点击、排序切换。每个事件只改一个数据源,最后统一调filterGoods,这种写法的核心思路是把筛选逻辑收敛到一处,排查问题时只需要看一个方法。onCategoryTape.currentTarget.dataset.id是 WXML 上通过>onSearchInput(e) { wx.clearTimeout(this.searchTimer); this.searchTimer = setTimeout(() => { this.setData({ keyword: e.detail.value }); this.filterGoods(); }, 300); }, onUnload() { wx.clearTimeout(this.searchTimer); }

3.3 商品图片的加载与缓存优化

生鲜商品的图片一般是白色背景的实拍图,体积较大。小程序 image 组件的lazy-load属性可以延迟加载屏幕外的图片,减少首屏网络请求。源码里如果没有加这个属性,二开时建议加上。另外,商品图片的 URL 要使用供应商提供的 CDN 地址,不要在小程序端传 base64 字符串,因为setData的数据量直接影响渲染性能。如果图片下载失败,要监听binderror事件,替换成默认占位图。

<image class="goods-img" src="{{item.image_url}}" lazy-load="true" mode="aspectFill" binderror="onImageError" >onImageError(e) { const index = e.currentTarget.dataset.index; this.setData({ [`filteredGoods[${index}].image_url`]: '/assets/img/default_goods.png' }); }

mode="aspectFill"会填满整个 image 容器,但裁剪掉图片边缘,适合宽度固定、高度 300rpx 左右的卡片图。如果商品图需要显示完整,比如详情页顶部主图,应该用mode="widthFix"。这就是为什么同一个商品在不同页面会有不同的mode值,取图时不要只顾容器样式,还要考虑图片本身的宽高比。

4. 购物车、订单与支付:状态管理与接口对接

4.1 购物车的本地存储设计

生鲜商城的购物车与普通衣物电商不同,它存在“加购后结算按钮可能因为库存变化而失效”的问题,同时生鲜商品还会有关联优惠(满减、买赠),所以购物车数据结构至少要包含商品 ID、数量、所选规格(如果存在)、单价快照、活动标识。源码里的购物车页面是用wx.setStorageSync把整个购物车数组存到了本地,没有做用户维度隔离。这在单机演示中没什么问题,但真实使用时,用户切换账号后购物车还留在本地,会造成串数据。二开时建议把购物车数据同步到服务端,本地缓存只作为离线兜底。

看看购物车操作的核心代码:

// utils/cart.js const CART_KEY = 'my_cart'; function getCart() { // 本地没有数据时返回空数组 return wx.getStorageSync(CART_KEY) || []; } function addToCart(goods, count) { const cart = getCart(); const index = cart.findIndex(item => item.goods_id === goods.goods_id); if (index > -1) { // 已存在商品,合并数量(要注意库存上限) cart[index].count += count; if (cart[index].count > goods.stock) { cart[index].count = goods.stock; } } else { // 新商品,保存价格快照,避免后续改价影响历史订单 cart.push({ goods_id: goods.goods_id, name: goods.name, price: goods.price, image_url: goods.image_url, count: count, stock: goods.stock, selected: true }); } wx.setStorageSync(CART_KEY, cart); wx.setStorageSync('cartCount', cart.reduce((sum, item) => sum + item.count, 0)); return cart; } function removeFromCart(goodsId) { let cart = getCart(); cart = cart.filter(item => item.goods_id !== goodsId); wx.setStorageSync(CART_KEY, cart); wx.setStorageSync('cartCount', cart.reduce((sum, item) => sum + item.count, 0)); return cart; } module.exports = { getCart, addToCart, removeFromCart };

这段代码里的关键点是“价格快照”。购物车里的price不应该在每次渲染时从商品接口重新读,因为如果用户加购时是 3 元,但结算时活动结束了商品变成了 5 元,此时应该按加购时的价格结算,还是让用户重新确认?代码里item.price存的是加购那一刻的价格,这是电商购物车的标准做法。cartCount单独存一个 key,是为了在 tabBar 上显示角标时不用读整个购物车数组,节省 I/O。不过要注意,cartCountcart是两个独立的缓存,如果在修改购物车时漏掉了cartCount的更新,会出现角标数量不一致。源码里这个模块是跟页面逻辑混在一起的,二开时建议把购物车操作迁到utils/cart.js,统一入口。

4.2 订单流程与状态机

订单模块是生鲜商城最容易出 bug 的地方。源码里的订单状态用了数字枚举,从创建到完成共五个状态。处理多个状态用 switch-case 比用 if 嵌套更直观,而且方便在调试阶段打印状态跳转记录。看一个订单状态机定义:

const ORDER_STATUS = { UNPAID: 0, // 待付款 PAID: 1, // 已付款,待发货 SHIPPED: 2, // 已发货/配送中 COMPLETED: 3, // 已完成(确认收货) CANCELLED: 4 // 已取消 }; function createOrder(cartList, totalPrice, address, remark) { const order = { order_id: generateOrderId(), goods_list: cartList.map(item => ({ goods_id: item.goods_id, name: item.name, price: item.price, count: item.count })), total_price: totalPrice, address: address, remark: remark, status: ORDER_STATUS.UNPAID, create_time: Date.now(), pay_time: null, ship_time: null }; // 源码中这步是把 order 存入服务器,实际可用 wx.request 提交 // 本地演示时可以直接 push 到全局订单数组 return order; } function updateOrderStatus(order, nextStatus) { // 校验状态流转是否合法 const allowTransitions = { [ORDER_STATUS.UNPAID]: [ORDER_STATUS.PAID, ORDER_STATUS.CANCELLED], [ORDER_STATUS.PAID]: [ORDER_STATUS.SHIPPED, ORDER_STATUS.CANCELLED], [ORDER_STATUS.SHIPPED]: [ORDER_STATUS.COMPLETED] }; const allowed = allowTransitions[order.status] || []; if (!allowed.includes(nextStatus)) { console.error(`非法状态流转: ${order.status} -> ${nextStatus}`); return false; } order.status = nextStatus; if (nextStatus === ORDER_STATUS.PAID) { order.pay_time = Date.now(); } else if (nextStatus === ORDER_STATUS.SHIPPED) { order.ship_time = Date.now(); } return true; }

状态机表可以这样理解:

当前状态可流转到触发动作
待付款 (0)已付款 (1)、已取消 (4)用户支付 / 超时未支付主动取消
已付款 (1)已发货 (2)、已取消 (4)商家后台发货 / 用户申请退款
已发货 (2)已完成 (3)用户确认收货 / 超时自动确认
已完成 (3)
已取消 (4)

createOrder里,goods_list是从购物车传入的,但要注意你不能直接用购物车对象,因为后续购物车里的selected属性会变化,订单详情里不应包含购物车状态。正确做法是做一层映射,只拿需要的字段,也就是代码里的map。源码中订单确认页还有一个容易忽略的点:真实下单前必须重新校验库存,不能只用本地缓存里的stock字段判断,因为那个值可能是几小时前获取的。实际项目要在提交订单时给后端传一份商品 ID 和数量,由后端再查一次真实库存并锁定,避免超卖。

4.3 支付接口的准备与回调处理

微信小程序的支付流程是:前端调用wx.requestPayment,拉起微信支付面板,支付成功后微信服务器会先回调开发者后台,后台再通过小程序消息推送或前端轮询更新订单状态。源码里演示支付通常只是模拟了弹窗,没有接真实商户号,但二开时你需要知道wx.requestPayment接收哪些参数。这些参数一般由后端签名生成,前端不能自己构造。

wx.requestPayment({ timeStamp: res.timeStamp, // 支付签名时间戳,单位秒 nonceStr: res.nonceStr, // 随机字符串,不长于32位 package: res.package, // 统一下单接口返回的 prepay_id,格式为 prepay_id=xxx signType: 'MD5', // 签名算法,通常为 MD5 或 HMAC-SHA256 paySign: res.paySign, // 后端二次签名结果 success: (payRes) => { // 支付成功后的逻辑:跳转订单详情或清空购物车中已结算商品 const orderId = this.data.orderId; wx.navigateTo({ url: `/pages/order/detail?order_id=${orderId}` }); }, fail: (err) => { // 用户取消支付时 err.errMsg 通常是 "requestPayment:fail cancel" // 此时订单状态保持为待付款,不需要更新后端 wx.showToast({ title: '支付已取消', icon: 'none' }); } });

这段代码里,success回调只代表前端键盘流程走完了,严谨的业务还要以后端收到微信支付回调为准。所以源码的支付成功处理中,有一个函数wx.request去 POST/order/pay_callback,把微信返回的payRes中的参数转交给后端校验,防止伪造支付成功。真实项目中,后台应使用微信支付证书和回调密文做验签,前端切不可把paySign暴露给与后端无关的人。

5. 从源码到上线:二次开发与常见坑

5.1 动态设置小程序标题的两种姿势

生鲜商城的商品详情页通常需要根据商品名动态设置顶部标题,比如用户从搜索“有机西兰花”进入详情页,标题栏应该显示“有机西兰花”,而不是固定的“商品详情”。源码里用的是wx.setNavigationBarTitle,这个接口会在页面onLoad或拿到商品数据后调用。第二种方式是直接在页面 json 文件里写死navigationBarTitleText,但这样所有商品都共享同一个标题,不符合需求。

// pages/goods/detail.js onLoad(options) { const goodsId = options.goods_id; this.loadGoods(goodsId).then(res => { wx.setNavigationBarTitle({ title: res.data.name }); }); }

注意动态标题的长度限制,微信小程序页面标题最多显示约 10 个汉字,超出部分会截断。生鲜商品名里常有“赠送小葱”“拍下减5元”这类促销后缀,如果直接放到标题栏,会显得很乱,二开时应只截取主要商品名。

5.2 微信小程序单选框与数据绑定

商品详情页如果存在规格,比如“冬瓜 约500g/份”“冬瓜 约1000g/份”,使用的是radio-group或自定义点击高亮。源码里用的是简单的><view class="spec-item {{selectedSpecIndex === index ? 'active' : ''}}" wx:for="{{specList}}" wx:key="id" >onSelectSpec(e) { const index = e.currentTarget.dataset.index; this.setData({ selectedSpecIndex: index, currentPrice: this.data.specList[index].price }); }

active类名控制高亮边框,选中后价格同步更新。这里有一个常见误写:selectedSpecIndex === indexindexwx:for的默认下标,如果循环中有嵌套,需要用wx:for-index="specIndex"自定义下标名,写错后高亮会出现在错误位置。源码里商品详情页的加购按钮会把选中的规格 ID 和数量一起传入购物车,所以购物车中同一商品的规格不同应视为两个独立行,去重时要加上spec_id,否则两种规格会合并成一件。

5.3 商城上线前的域名与备案核查

小程序商城要正式发布,后端接口必须使用 HTTPS 域名,且该域名要在微信公众平台「开发管理-服务器域名」里配置到request合法域名列表中。源码里app.jsapiBase写的是http://localhost:8080,开发时可以用http://127.0.0.1配合“不校验合法域名”调试,但发布的体验版和正式版一律不允许 HTTP。另外,小程序名称与类目审核时会要求提供商家资质,生鲜食品类目还涉及食品经营许可证。如果你在 2023 年之后注册小程序并准备上线,必须完成微信小程序备案,拿到备案号后才能在mp.weixin.qq.com完成发布。备案信息要在小程序后台填写,内容包含主体信息、服务内容、前置审批项,生鲜电商一般属于“网上零售”这类大类,不需要额外前置审批。二开时不需要自己开发备案功能,但要在项目 README 里提醒运营同学提前 1-2 周开始备案流程,否则代码写好了也发不出去。

5.4 用 uniapp 迁移的可行性

这套原生小程序源码如果要用uniapp迁移,重点不是改app.json,而是要把每个.wxml文件转换成vue单文件组件的template部分,把wx.setStorageSync换成uni.setStorageSync,把wx.request换成uni.requestuniapp项目里可以用条件编译#ifdef MP-WEIXIN保持部分微信私有 API 的调用,比如wx.requestPayment在 uniapp 里对应uni.requestPayment,但参数可以通用。迁移的主要收益是以后可以一键发布到支付宝小程序、抖音小程序。不过要注意,生鲜商城中使用了微信open-type="getUserInfo"之类的旧版授权写法,在 uniapp 中需要通过uni.getUserProfile来兼容新版微信规则,这部分改动量不小,建议优先把业务逻辑抽离到common目录,页面留着薄壳再逐页迁移。

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

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

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

立即咨询