简介:面向希望研究微信小程序商店实现方式的开发者与学习者,这份完整代码包提供了一套可运行的商店类项目源码,覆盖页面展示、交互逻辑、配置与静态资源等环节,适合用来理解小程序商店从界面到数据组织的常见做法,也可作为课程设计或二次开发的参考底稿。压缩包共57个文件,约131KB,以17个png图片、9个js脚本、9个json配置、8个wxml模板与8个wxss样式为主,另含说明文档,能够呈现页面结构、样式组织、业务逻辑与资源配置之间的对应关系。已有4042人学习下载,说明其在同类小程序项目资料中具有较高关注度。读者可从中获得较完整的目录组织样本、页面模板与样式写法、配置项示例以及商店功能拆分思路,便于对照自身项目查漏补缺,并在理解现有代码结构的基础上进行功能扩展与调试实践。
1. 从一家小店的需求说起:微信小程序商店完整代码到底解决什么问题
前阵子有个朋友找我,说他盘下了街角一家卖文创和零食的小店,想做个线上入口,预算不够定制开发,问我手上有没有能直接改成他自己店的微信小程序商店完整代码。我第一反应是劝他别从零写:商品、购物车、订单、地址、支付回调这套东西,一个人从头撸一遍少说三周,中间还得踩库存超卖、金额对不上、支付回调丢失这些坑。他这种情况最合适的是一份结构清楚的完整代码,买卖链路的骨架已经搭好,他只需要换数据、换配色、换商品,一周内就能上线收第一笔钱。这篇文章就按这个前提,把这份代码从目录结构、运行步骤讲到核心链路和二次开发,新手照着能跑起来,熟手能直接看明白哪里该动刀。
2. 项目结构与技术选型:一个商店小程序的四层骨架
拿到一份完整代码,我先看的不是首页长什么样,而是目录。目录决定了我改东西的时候会不会牵一发动全身。商店类小程序的功能听着多,拆开来其实就四层:页面层(列表、详情、购物车、订单)、状态层(购物车、登录态、地址)、服务层(商品、订单、支付)、数据层(商品、SKU、订单、用户、地址)。这四层分清楚了,剩下的都是体力活。
2.1 从买家动线反推功能清单
别一上来就翻代码,先按买家的动线走一遍,你会得到一张天然的功能清单:进店看首页banner和推荐位 → 逛分类或搜索 → 点进商品详情选规格 → 加购物车 → 结算时选地址、看运费 → 付款 → 在“我的”里查订单、看物流、申请售后。这条动线对应到代码里就是七个页面加三个云函数(或后端接口)。
我一般会拿这张动线表去对代码,缺哪个页面一眼就看出来。这份代码的页面划分基本贴合这条动线,首页做了轮播 + 分类入口 + 商品瀑布流,分类页做了左侧一级分类、右侧二级分类的联动,详情页把规格选择(SKU)放在弹层里,购物车和结算页做了勾选和小计,订单页按待付款、待发货、待收货、已完成分了 tab。这套划分是电商小程序的通用解法,二次开发时你加功能也基本是往这几个位置塞。
值得单独说的是“我的”页面。很多玩具项目把这里做成一个静态头像加几个死链接,实际上这里承担了订单入口、地址管理、客服、优惠券四个高频路径,是留存的关键。这份代码里“我的”页做了登录态判断,未登录时展示登录按钮,登录后拉取订单数量角标,这个细节决定了用户付款完能不能顺利找到自己的订单。
2.2 技术选型:原生小程序 + 云开发,够用且省事
技术栈这块,这份代码走的是原生小程序(WXML + WXSS + JavaScript)配云开发的路子,没有引 uni-app、Taro 这类跨端框架,也没有单独起一台 Node 服务器。选型理由很实在:商店类小程序 90% 的交互是列表、表单、弹层,原生写法性能最好、调试链路最短,而云开发把数据库、云函数、文件存储、支付回调的部署问题一次性抹平了,一个人维护完全撑得住。
原生写法的代价是跨端能力弱,如果你后面要同时上其他平台,这套代码需要重写视图层。但对“开一家自己的店”这个目标来说,先跑起来比先想清楚五年规划重要得多。我把目录结构贴出来,你对着看会更快:
store-miniprogram/ ├── app.js # 全局登录态、云开发初始化 ├── app.json # 页面注册、tabBar、分包配置 ├── app.wxss # 全局样式变量(主题色、间距) ├── config/ │ └── index.js # 环境 ID、分页大小、包邮门槛等全局配置 ├── pages/ │ ├── index/ # 首页:轮播 + 分类入口 + 商品流 │ ├── category/ # 分类:左右联动 │ ├── detail/ # 商品详情:SKU 弹层、加购、立即购买 │ ├── cart/ # 购物车:勾选、改数量、小计 │ ├── confirm/ # 结算:地址、运费、备注 │ ├── order/ # 订单列表 + 详情 │ └── mine/ # 个人中心:地址、客服、订单入口 ├── components/ │ ├── goods-card/ # 商品卡片(列表复用) │ ├── sku-popup/ # 规格选择弹层 │ └── empty/ # 空状态占位 ├── utils/ │ ├── cart.js # 购物车本地存储与合并 │ ├── request.js # 云函数调用封装 + 统一错误提示 │ └── format.js # 金额分转元、时间格式化 └── cloudfunctions/ ├── login/ # 换取 openid、初始化用户记录 ├── getGoods/ # 商品列表分页、详情 ├── createOrder/ # 下单事务:扣库存 + 建订单 └── payNotify/ # 支付回调、订单状态流转这个结构的两个关键点:一是config/index.js把环境 ID、分页大小、包邮门槛这些“会变的东西”集中了,换店的时候只改一个文件;二是云函数按业务动作拆,getGoods只读、createOrder只写、payNotify只处理状态机,职责边界清楚,出问题时排查范围小。
2.3 数据模型:五张集合撑起一个商店
商店类应用的复杂度八成在数据模型上。这份代码用了五张核心集合,我整理成表,你改字段的时候照着来:
| 集合 | 关键字段 | 说明 |
|---|---|---|
| goods | title, cover, images, categoryId, minPrice, soldCount, status | 商品主体,只存展示信息,价格取 SKU 最低价 |
| sku | goodsId, title, price, stock, specs | 规格与库存,价格单位统一用“分” |
| order | orderNo, openid, items, total, addressId, status, createdAt | 订单快照,items 里存下单时的价格和标题 |
| address | openid, name, phone, region, detail, isDefault | 收货地址,按 openid 隔离 |
| user | openid, nickname, avatar, createdAt | 用户表,登录时写入 |
两个设计细节值得抄:价格全部用整数“分”存,展示时再除 100,浮点数算钱迟早出玄学问题;订单里的 items 存的是下单瞬间的价格和标题快照,不是引用商品表,否则商家改价之后历史订单金额就全乱了,这是血泪经验。
3. 跑起来:从导入工程到首单成交的完整步骤
代码能跑和代码能卖货是两件事。这一章按我实际交付的顺序走一遍:先把工程导入、依赖装好,再把云端数据初始化,然后改全局配置,最后用一笔真实下单验证整条链路。
3.1 导入工程:appid、基础库与依赖安装
先把项目目录整个拖进开发者工具,选择“小程序”项目类型。appid 这一栏,如果你只是本地跑通,用自己的测试号就行;要真机预览和调支付,必须用已认证的小程序 appid,否则支付相关的能力全部调不通。基础库版本建议选 2.20 以上,云开发和组件特性都在这个版本之后才稳定。
导入之后先看两处:app.json里的pages数组有没有把七个页面都注册上,project.config.json里的cloudfunctionRoot有没有指向cloudfunctions目录。后者没配的话,云函数目录在工具里不会显示成云图标,右键也没有“上传并部署”的选项,新手最容易卡在这。
依赖安装分两块,命令不复杂,但位置要对:
# 1. 项目根目录:安装前端依赖(如果有 npm 包,比如轻量 UI 组件) npm install # 2. 云函数目录:每个云函数是独立的 Node 包,要单独装 cd cloudfunctions/createOrder && npm install cd ../getGoods && npm install cd ../login && npm install cd ../payNotify && npm install这里有个容易翻车的点:云函数在本地装了node_modules也没用,上传时默认只传代码不传依赖,必须在开发者工具里右键云函数选择“上传并部署:云端安装依赖”。我第一次部署时忘了勾这个,调用一直报Cannot find module 'wx-server-sdk',查了半小时才发现是依赖没上云。
3.2 云端初始化:集合、权限与种子数据
云开发控制台里要手动做三件事。第一,建五张集合:goods、sku、order、address、user,名字必须和代码里一致,大小写也别错。第二,配权限:goods 和 sku 设为“所有用户可读,仅创建者可写”,order、address、user 设为“仅创建者可读写”。权限配错的后果很直接——商品列表拉不出来,或者用户能看到别人的订单地址。
第三,灌种子数据,不然首页是空的,你连样式都调不了。最省事的做法是从控制台的“导入”功能上传一份 JSON,格式按集合字段来:
{ "title": "手冲咖啡挂耳装 10 片", "cover": "cloud://xxx/goods/cover-01.jpg", "images": ["cloud://xxx/goods/detail-01.jpg"], "categoryId": "drink", "minPrice": 3900, "soldCount": 128, "status": "ON" }minPrice单位是分,3900 就是 39 元;cover用云存储的 fileID,直接粘贴控制台里复制出来的那串就行;status用ON和OFF控制上下架,下架商品在列表云函数里会被过滤掉。灌数据时注意 goods 和 sku 的goodsId要能对上,详情页是靠这个字段去查规格的,对不上就会出现“商品有详情但选不了规格”的情况。
3.3 全局配置:一个文件改完整家店
config/index.js是这份代码里我最喜欢的部分,换店基本上只动这里:
// config/index.js module.exports = { envId: 'store-xxxxxx', // 云开发环境 ID,控制台左上角可复制 shopName: '某创意商店', currency: 'CNY', pageSize: 10, // 列表分页大小,云函数单次上限 20 freeShippingThreshold: 4990, // 满 49.90 元包邮,单位分 shippingFee: 800, // 不满门槛收 8 元运费 themeColor: '#2b2b2b', // 主色,app.wxss 里以变量引用 orderTimeoutMinutes: 30 // 未付款订单自动关闭时间 }每个参数都直接影响业务:pageSize设太大首屏会卡,设太小用户滑两下就到底,10 到 15 是手感比较好的区间;freeShippingThreshold和shippingFee在结算页和云函数里各算一次,前端算给用户看,云函数算给订单用,两边必须读同一个配置,否则会出现“页面显示包邮、下单却扣了运费”的对账事故;themeColor在app.wxss里定义成 CSS 变量,改一个色值全站的按钮、价格、标签一起变,不用满项目搜十六进制。
改完配置记得清一次编译缓存(工具里的“清缓存 → 全部清除”),config是模块级缓存,热重载有时读的还是旧值。
3.4 验证顺序:别跳过“首单”这一步
功能验证我一般按“读 → 写 → 钱”三段走,不按页面顺序走。第一段验证读链路:首页列表能出来、下拉能加载第二页、分类切换有数据、详情页规格能选。第二段验证写链路:加购物车、改数量、勾选、删除,退出小程序再进来购物车还在(说明本地存储生效)。第三段才是钱:选地址、提交订单、调起支付、付款成功后订单状态变成“待发货”。
第三段最容易出问题。测试时不要用真实金额,把商品价格临时改成 0.01 元跑一遍,确认订单能在数据库里查到、状态是PAID、库存减了对应数量。跑通这一遍,你才算真正拿到了这份完整代码的使用权,否则它只是一个能点开的 Demo。
4. 核心链路拆解:购物车到支付成功的四段关键代码
这一章是整份代码里最值得逐行看的部分。商店小程序的坑几乎全在“状态”上:购物车在本地还是云端、库存什么时候扣、金额谁来算、支付回调丢了怎么办。四个问题对应四段代码。
4.1 商品列表:分页加载与云函数的游标设计
列表页是所有流量的入口,写法上分“一次性拉全量”和“分页拉取”两种。商品超过 50 条就必须分页,否则首屏 JSON 能到几百 KB,低端机上白屏两秒。这份代码用onReachBottom配合云函数的 skip/limit 做分页:
// cloudfunctions/getGoods/index.js const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) const db = cloud.database() exports.main = async (event) => { const { categoryId = '', page = 0, pageSize = 10 } = event const where = { status: 'ON' } // 只查上架商品 if (categoryId) where.categoryId = categoryId const list = await db.collection('goods') .where(where) .orderBy('soldCount', 'desc') // 按销量排,避免新品永远沉底 .skip(page * pageSize) // 页码从 0 开始,前端别传 1 .limit(pageSize) .get() return { code: 0, data: { list: list.data, hasMore: list.data.length === pageSize } } }page从 0 开始,前端传 1 会导致第一页数据被跳过,这是最常见的 off-by-one。hasMore用“本次返回条数是否等于 pageSize”判断,比再查一次总数便宜得多——商店列表不需要精确总数,用户只关心还能不能往下滑。orderBy选销量而不是创建时间,是因为商店首页的核心指标是转化,新品要靠运营位曝光,不靠列表自然排序。
前端的配合代码很简单,但有个细节:loading锁必须加,否则用户手快连续上拉会触发两次请求,第二页数据被追加两遍,列表里出现重复商品。这类重复在测试时很难复现,上线后被用户截图反馈就很尴尬。
4.2 购物车:本地优先 + 登录后合并
购物车要不要存服务端?我的答案是:未登录存本地,登录后合并上云。理由是加购是高频操作,每次都打网络请求会让手感变粘,而用户换设备时又确实需要购物车跟着走。这份代码的utils/cart.js就是干这个的:
// utils/cart.js const KEY = 'cart_v1' const MAX = 99 function readLocal() { return wx.getStorageSync(KEY) || [] // [{ skuId, goodsId, count, checked }] } function writeLocal(list) { wx.setStorageSync(KEY, list) return list } function add(item) { const list = readLocal() const hit = list.find(i => i.skuId === item.skuId) if (hit) { hit.count = Math.min(MAX, hit.count + item.count) // 同一 SKU 累加,封顶 99 hit.checked = true } else { list.push({ ...item, checked: true }) } return writeLocal(list) } // 登录后合并:数量取两者最大值,而不是相加 function mergeWithServer(serverList) { const local = readLocal() const map = new Map(serverList.map(i => [i.skuId, i])) local.forEach(i => { const s = map.get(i.skuId) if (s) s.count = Math.min(MAX, Math.max(s.count, i.count)) else map.set(i.skuId, i) }) return writeLocal([...map.values()]) }skuId是购物车的唯一键,不是goodsId——同一件衣服的 M 码和 L 码必须能同时存在购物车里,用商品 ID 做键会互相覆盖,这是新手最容易写错的一行。合并策略选“取最大值”而不是“相加”,是因为用户可能在手机上加过一次、平板上又加过一次,相加会变成 2 件,用户会觉得这程序有毛病。checked字段存在本地,是为了让用户勾选状态在切页面回来之后还保留。
4.3 下单:事务里扣库存,订单号自己生成
下单是整个项目里唯一必须用事务的地方。库存扣减和订单创建要么都成功要么都失败,中间断一个就会出现“钱付了没订单”或者“订单有了没扣库存”的经典事故:
// cloudfunctions/createOrder/index.js const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) const db = cloud.database() exports.main = async (event) => { const { OPENID } = cloud.getWXContext() const { items, addressId } = event // items: [{ skuId, goodsId, count }] const transaction = await db.startTransaction() try { let total = 0 const snapshots = [] for (const item of items) { // 事务内读取,拿到的是当前真实库存 const skuRes = await transaction.collection('sku').doc(item.skuId).get() const sku = skuRes.data if (!sku || sku.stock < item.count) { throw new Error(`库存不足: ${sku && sku.title}`) } // 用 inc 原子递减,不写死具体数值,避免并发覆盖 await transaction.collection('sku').doc(item.skuId).update({ data: { stock: db.command.inc(-item.count) } }) total += sku.price * item.count // 金额在服务端重算,前端传的价格一律不信 snapshots.push({ ...item, price: sku.price, title: sku.title }) } const orderNo = genOrderNo(OPENID.slice(-4)) await transaction.collection('order').add({ data: { orderNo, openid: OPENID, items: snapshots, total, addressId, status: 'UNPAID', createdAt: db.serverDate() } }) await transaction.commit() return { code: 0, data: { orderNo, total } } } catch (e) { await transaction.rollback() return { code: 4001, msg: e.message } } } function genOrderNo(suffix) { const t = new Date() const pad = n => String(n).padStart(2, '0') const stamp = `${t.getFullYear()}${pad(t.getMonth() + 1)}${pad(t.getDate())}${pad(t.getHours())}${pad(t.getMinutes())}${pad(t.getSeconds())}` return `S${stamp}${suffix}${Math.floor(Math.random() * 90 + 10)}` }三个点必须讲清楚。第一,db.command.inc(-item.count)是原子操作,把“读出库存 → 减一 → 写回”三步合成一步,两个用户同时下单也不会互相覆盖;如果你写成先get再update({ stock: sku.stock - count }),超卖就是必然的。第二,total必须在云函数里用 SKU 的真实价格重算,前端传来的金额只能用于展示,信它等于把收银台交给顾客。第三,订单号用“时间戳 + openid 后四位 + 随机数”拼,不用自增 ID,自增 ID 会暴露你一天的订单量,属于商业信息泄露。
事务提交之后,未付款订单要靠定时触发器在orderTimeoutMinutes之后关单并回滚库存,这个逻辑别忘写,否则库存会被一堆废单占住。
4.4 支付回调与订单状态机:前端的 success 不算数
支付这块最典型的翻车是:用户付款成功了,订单还显示“待付款”。原因基本都是把wx.requestPayment的 success 回调当成支付成功的依据。实际上前端回调只能说明支付流程走完了,真正的结果要以服务端收到的支付通知为准:
// cloudfunctions/payNotify/index.js const STATUS_FLOW = { UNPAID: ['PAID', 'CANCELED'], PAID: ['SHIPPED', 'REFUNDING'], SHIPPED: ['DONE', 'REFUNDING'], DONE: [] } function canTransit(from, to) { return (STATUS_FLOW[from] || []).includes(to) } exports.main = async (event) => { const { orderNo, tradeState } = event const res = await db.collection('order').where({ orderNo }).get() const order = res.data[0] if (!order) return { code: 404 } // 幂等:支付平台可能重复通知,已处理过的直接返回成功 if (order.status !== 'UNPAID') return { code: 0 } const next = tradeState === 'SUCCESS' ? 'PAID' : 'CANCELED' if (!canTransit(order.status, next)) return { code: 409 } await db.collection('order').doc(order._id).update({ data: { status: next, paidAt: db.serverDate() } }) return { code: 0 } }STATUS_FLOW这张表把订单的合法流转路径写死了,UNPAID只能到PAID或CANCELED,跳状态的请求一律拒绝。这比满屏的if (status === 'PAID')好维护得多,新增“退款中”状态时只改表不改逻辑。幂等判断放在最前面,是因为支付平台的通知机制本身就允许重复推送,不做幂等就会出现一单发两次货。
前端这边补一个兜底:支付回调页面onShow时主动查一次订单状态,查到PAID就跳成功页。服务端通知偶尔会延迟几秒,用户盯着“待付款”三个字会以为钱丢了,主动查一次能极大降低客诉。
5. 避坑与排查:商店小程序最容易翻车的五个地方
下面这几条都是我在真实项目里踩过的,每条按“现象 → 原因 → 解决”写,你遇到对应现象直接对号入座。
商品图片在开发者工具里正常,真机全白。现象是列表能出来、文字正常,但所有图都是空白占位。原因通常是图片域名没配到小程序的合法域名列表里,开发者工具默认勾了“不校验合法域名”,所以本地看不出问题。解决方式有两种:用云存储的直接走cloud://的 fileID 让 image 组件自动解析,或者把图片域名加到小程序后台的 downloadFile 合法域名里。上线前务必在真机上关掉“不校验域名”跑一遍。
库存超卖,两个用户同时下单都成功了。现象是后台库存显示负数,或者明明只剩 1 件却卖出 2 件。原因基本都是先查后扣的非原子写法,两个请求读到同一个库存值。解决办法是把扣减放进事务,用inc原子操作,并在事务内先判断库存够不够。压测的时候可以用两台设备同时点“立即购买”来复现,不要只靠单机测试。
结算页显示 39 元,付款时变成 47 元。现象是用户在小程序里看到的价格和实际支付金额不一致。原因通常是运费、优惠在前后端各算了一套逻辑,两边阈值或取整方式不同。解决办法是把算钱这件事收敛到一个云函数里,前端只负责展示返回结果;运费门槛、包邮规则从同一个 config 读,绝不在页面里硬编码数字。
支付成功但订单一直是待付款。现象是用户明确说扣款了,订单列表里还是“待付款”,过半小时被自动关单。原因要么是回调地址没配、云函数没部署,要么是回调处理逻辑里没做幂等,第一次通知报错后再也没重试。排查顺序是:先看云函数日志有没有收到通知,再看订单状态字段有没有变,最后看状态机是不是拒绝了流转。补一个前端主动查询能救急,但根子还是在回调上。
主包超过 2MB,上传时报错。现象是编译正常,点上传提示包体积超限。原因是商品占位图、mock 数据、没用到的组件全塞在主包里。解决办法是把订单、个人中心这类非首屏页面拆进分包,图片全部走云存储不打包,app.json里配好subPackages和preloadRule。分包之后首屏加载反而更快,算是顺带的收益。
6. 二次开发:把通用商店改成“你自己那家店”
改这份代码,我建议按“先换皮、再换数据、最后加功能”的顺序推,顺序反了会做大量无用功。
换皮最快,只动app.wxss里的几个变量和config/index.js的themeColor、shopName,十分钟就能让整个小程序看起来是另一家店。接着换数据:把 goods、sku 两张表清空,用商家后台或者控制台导入自己的商品,categoryId按自己的品类重新规划,注意同步改分类页的左侧一级分类数组。
真正需要动脑的是加功能。以最常见的“优惠券”为例,改造路径是这样:新建coupon集合存券模板,userCoupon存领取记录;在createOrder云函数里加一段券校验和金额扣减,注意优惠后的金额不能小于 0,且要和运费计算顺序说清楚(一般先算商品优惠再算运费门槛);结算页加一个券选择弹层。整个改动只碰三个文件加一张表,这就是前面把算钱收敛到云函数的好处。
加 SKU 规格(比如从“只有颜色”扩到“颜色 + 尺码”)时,sku表的specs字段从对象改成数组,详情页的规格弹层要重写成二维组合选择,同时createOrder里按skuId查库存的逻辑不用动——只要你的购物车和订单都以skuId为唯一键,上层怎么改都不影响下层。
上线前我一般跑一张自查表:真机关掉域名校验能正常展示、弱网下加购不重复、0.01 元商品能完整下单、未付款订单 30 分钟后自动关单并回滚库存、主包体积小于 1.5MB、订单列表能下拉刷新。这六项过了,才算可以交给商家。
从那以后我每次接这类商店项目,都强制自己先用 0.01 元跑一遍完整下单链路,再动任何业务代码——这个习惯帮我挡掉过至少三次“上线后才发现支付回调没通”的事故。希望帮到你。
本文还有配套的精品资源,点击获取