☰
商城小程序源码实战:mofunShop-v2从环境搭建到支付回调全链路
2026/10/7 3:18:11 网站建设 项目流程

简介:商城小程序mofunShop-v2源代码是一套面向微信等平台的电商小程序源码套件,适合具备JavaScript、Vue.js、Node.js与MySQL基础的开发者进行二次开发,帮助快速搭建功能完善的线上购物平台。资源包共120个文件,约767KB,以json配置、js逻辑、wxss样式与wxml结构文件为主,另含jpg、png图片素材,覆盖页面搭建、交互实现与视觉资源等环节。已有184人学习下载,可作为小程序商城项目的入门参考。源码涵盖用户界面、商品展示、搜索筛选、购物车、订单管理、支付对接、后台管理、推广营销与在线客服等核心模块,并集成微信支付、支付宝支付等主流方式。开发者可借此理解电商小程序的整体架构与模块划分,对照实现登录注册、商品分类浏览、订单流转与营销工具等常见功能,同时注意源码维护更新与平台接口兼容,遵守当地法规,降低法律风险。

1. 商城小程序 mofunShop-v2 源代码:从拿到包到跑通第一个订单

你从朋友那儿拷来一个压缩包,名字叫 mofunShop-v2,解压后一堆目录,README 只有三行。你搜「商城小程序 源代码」,出来的全是广告和下载站,没人告诉你这套东西到底怎么跑起来。我去年帮一个做社区团购的团队落地过这套代码,从环境搭建到第一笔订单走通,前后踩了七八个坑。这篇文章就是那次过程的复盘——mofunShop-v2 是一套基于微信小程序的商城前端加后端服务的完整源码,覆盖商品、购物车、订单、支付回调这些核心链路。适合想拿现成代码二次开发的小团队,也适合想研究小程序商城架构的开发者。下面按「先看懂结构、再跑通环境、最后调通支付」的顺序讲,每一步都有可复现的命令和参数。

2. mofunShop-v2 的目录结构与技术栈拆解

拿到源码第一步不是急着npm install,而是先花二十分钟把目录结构和依赖关系摸清楚。mofunShop-v2 的代码组织方式决定了你后面改一个功能要动几个文件,也决定了你遇到报错时该去哪个目录翻日志。

2.1 前端小程序目录:pages 与 components 的职责边界

解压后根目录下通常有两个主文件夹:miniprogram和server。miniprogram是小程序前端,用微信原生框架写的,没有用 uni-app 或 taro 这类跨端方案。进去之后你会看到:

miniprogram/ ├── app.js ├── app.json ├── app.wxss ├── pages/ │ ├── index/ # 首页,商品推荐流 │ ├── category/ # 分类页 │ ├── cart/ # 购物车 │ ├── order/ # 订单列表与详情 │ ├── pay/ # 支付中间页 │ └── user/ # 个人中心 ├── components/ │ ├── goods-card/ # 商品卡片组件 │ ├── price/ # 价格格式化组件 │ └── empty/ # 空状态占位 ├── utils/ │ ├── request.js # 网络请求封装 │ └── auth.js # 登录态管理 └── config/ └── api.js # 后端接口地址配置

pages下每个目录是一个页面,四件套.js/.json/.wxml/.wxss齐全。components放的是跨页面复用的 UI 块,比如goods-card在首页、分类页、搜索结果页都会用到。utils/request.js是整个前端最关键的封装,它统一处理了 token 注入、错误码拦截和 loading 状态。你后面调接口地址、改超时时间,都从这里入手。

config/api.js里通常长这样:

// config/api.js const BASE_URL = 'http://localhost:3000/api'; // 本地开发地址 // const BASE_URL = 'https://your-domain.com/api'; // 线上地址 module.exports = { BASE_URL, GOODS_LIST: `${BASE_URL}/goods/list`, GOODS_DETAIL: `${BASE_URL}/goods/detail`, CART_ADD: `${BASE_URL}/cart/add`, ORDER_CREATE: `${BASE_URL}/order/create`, PAY_PREPAY: `${BASE_URL}/pay/prepay`, };

这个文件是你切换环境的唯一入口。本地调试时把BASE_URL指向localhost,但要注意微信开发者工具默认不允许请求http://localhost,需要在工具右上角「详情」→「本地设置」里勾选「不校验合法域名」。这一步不勾,后面所有接口都会报request:fail url not in domain list。

2.2 后端服务目录:路由、控制器与数据模型

server目录是 Node.js 写的,常见的是 Express 或 Koa 框架。以 Express 为例,结构大致如下:

server/ ├── app.js # 入口,注册中间件和路由 ├── routes/ │ ├── goods.js │ ├── cart.js │ ├── order.js │ └── pay.js ├── controllers/ │ ├── goodsController.js │ ├── orderController.js │ └── payController.js ├── models/ │ ├── goods.js │ ├── order.js │ └── user.js ├── middlewares/ │ ├── auth.js # JWT 鉴权 │ └── errorHandler.js ├── config/ │ └── db.js # 数据库连接配置 └── package.json

routes定义 URL 路径和 HTTP 方法,controllers写业务逻辑,models是数据模型。以订单创建为例,routes/order.js里会有一行router.post('/create', auth, orderController.create),auth中间件校验 JWT,通过后才进create函数。create里做的事包括:校验库存、生成订单号、写入订单表、扣减库存、返回订单 ID。这一串逻辑如果中间任何一步失败,需要回滚,否则会出现「库存扣了但订单没生成」的脏数据。

config/db.js是数据库连接配置,常见的是 MySQL 或 MongoDB。如果是 MySQL,你会看到:

// config/db.js const mysql = require('mysql2'); const pool = mysql.createPool({ host: '127.0.0.1', port: 3306, user: 'root', password: 'your_password', database: 'mofunshop', waitForConnections: true, connectionLimit: 10, queueLimit: 0, }); module.exports = pool;

connectionLimit: 10是连接池大小,本地开发够用,线上如果并发高要调到 50 以上。database名字必须和你导入的 SQL 文件里的库名一致,不一致会报ER_BAD_DB_ERROR。

2.3 数据库表结构与初始化 SQL 的位置

源码包里通常会有一个sql文件夹或根目录下的mofunshop.sql文件。这个文件包含建表语句和初始数据。导入之前先看一眼表数量,mofunShop-v2 一般有这些核心表:

表名用途关键字段
users用户信息openid,nickname,phone
goods商品title,price,stock,cover
categories分类name,parent_id,sort
carts购物车user_id,goods_id,count
orders订单order_no,user_id,total,status
order_items订单明细order_id,goods_id,price,count

导入命令:

# 先创建数据库 mysql -u root -p -e "CREATE DATABASE mofunshop DEFAULT CHARSET utf8mb4;" # 再导入 SQL 文件 mysql -u root -p mofunshop < mofunshop.sql

utf8mb4是必须的,因为商品标题里可能有 emoji 或特殊字符,用utf8会报Incorrect string value。导入完成后用SHOW TABLES;确认表都建好了。

3. 本地跑通 mofunShop-v2 的最小步骤

环境搭建是劝退率最高的环节。我见过太多人卡在npm install报错或者数据库连不上就放弃了。这一章把步骤拆到最细,按顺序执行基本不会出问题。

3.1 后端启动:Node 版本、依赖安装与端口配置

先确认 Node 版本。mofunShop-v2 的package.json里一般会写"engines": { "node": ">=14" },但我实测 Node 16 和 18 都能跑,Node 20 偶尔会有依赖不兼容。用node -v看一下,如果是 20 以上,建议用 nvm 切到 18:

# 安装 nvm(如果还没装) curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash # 切到 Node 18 nvm install 18 nvm use 18

然后进server目录装依赖:

cd server npm install

如果npm install卡住或报ETIMEDOUT,换淘宝源:

npm config set registry https://registry.npmmirror.com npm install

装完之后检查package.json里的scripts:

{ "scripts": { "start": "node app.js", "dev": "nodemon app.js" } }

本地开发用npm run dev,有热重载。启动前确认app.js里的端口:

// app.js const PORT = process.env.PORT || 3000; app.listen(PORT, () => { console.log(`Server running on port ${PORT}`); });

默认 3000,如果被占用会报EADDRINUSE。改端口或者杀掉占用进程:

# 查谁占了 3000 lsof -i :3000 # 杀掉 kill -9 <PID>

启动成功后终端会打印Server running on port 3000和数据库连接成功的日志。如果只看到端口日志但没看到数据库日志,说明config/db.js配置有问题,回去检查用户名密码和库名。

3.2 小程序端配置:appid 替换与开发者工具导入

打开微信开发者工具,选择「导入项目」,目录指向miniprogram文件夹。AppID 填你自己的测试号或正式号。如果你没有 AppID,在开发者工具里选「测试号」也能跑,但测试号不支持支付,后面调支付时需要换成正式号。

导入后第一件事是改project.config.json里的appid:

{ "appid": "wx1234567890abcdef", "projectname": "mofunShop-v2", "setting": { "urlCheck": false, "es6": true, "enhance": true } }

urlCheck: false就是前面说的「不校验合法域名」,本地调试必须关掉。改完保存,工具会重新编译。

然后检查app.js里的globalData:

// app.js App({ globalData: { userInfo: null, token: null, baseUrl: 'http://localhost:3000/api', }, onLaunch() { const token = wx.getStorageSync('token'); if (token) { this.globalData.token = token; } }, });

baseUrl要和config/api.js里的BASE_URL一致。不一致的话,首页商品列表会空白,控制台报request:fail。

3.3 前后端联调:用 curl 验证第一个接口

在打开小程序之前,先用 curl 确认后端接口是通的:

# 测试商品列表接口 curl -s http://localhost:3000/api/goods/list?page=1&size=10 | jq

如果返回 JSON 里有data数组且长度大于 0,说明后端和数据库都正常。如果返回{"code":500,"msg":"..."},看msg内容。常见的是ER_NO_SUCH_TABLE,说明 SQL 没导入成功,重新导一次。

如果 curl 通但小程序里请求失败,检查三个地方:一是开发者工具的「不校验合法域名」有没有勾;二是baseUrl有没有写错;三是后端有没有开 CORS。Express 加 CORS 中间件:

// app.js const cors = require('cors'); app.use(cors());

cors()默认允许所有来源,本地开发够用。线上要配白名单,否则有安全风险。

4. 商品、购物车、订单三条链路的代码走读

跑通环境只是第一步,真正要二次开发,必须理解核心链路的代码怎么走。这一章按用户操作顺序,从浏览商品到下单,把关键函数和参数讲清楚。

4.1 商品列表接口:分页参数与缓存策略

GET /api/goods/list接收page和size两个参数。controllers/goodsController.js里:

// controllers/goodsController.js const pool = require('../config/db'); exports.list = async (req, res) => { const page = parseInt(req.query.page) || 1; const size = parseInt(req.query.size) || 10; const offset = (page - 1) * size; try { const [rows] = await pool.query( 'SELECT id, title, price, cover, stock FROM goods WHERE status = 1 ORDER BY sort DESC LIMIT ? OFFSET ?', [size, offset] ); const [[{ total }]] = await pool.query( 'SELECT COUNT(*) as total FROM goods WHERE status = 1' ); res.json({ code: 0, data: rows, total, page, size }); } catch (err) { res.status(500).json({ code: 500, msg: err.message }); } };

status = 1表示上架商品,sort DESC是排序权重。LIMIT ? OFFSET ?用参数化查询防止 SQL 注入。total用于前端判断是否还有下一页。如果商品数量大,COUNT(*)会慢,可以加缓存:

const NodeCache = require('node-cache'); const cache = new NodeCache({ stdTTL: 60 }); // 缓存 60 秒 exports.list = async (req, res) => { const cacheKey = `goods_list_${req.query.page}_${req.query.size}`; const cached = cache.get(cacheKey); if (cached) return res.json(cached); // ... 查询逻辑 cache.set(cacheKey, result); res.json(result); };

stdTTL: 60表示 60 秒后过期。商品更新频率不高的话,这个缓存能挡掉大部分重复查询。

4.2 购物车增删改:库存校验与并发问题

购物车接口有三个:POST /cart/add、POST /cart/update、POST /cart/remove。以添加为例:

// controllers/cartController.js exports.add = async (req, res) => { const { goodsId, count } = req.body; const userId = req.user.id; const conn = await pool.getConnection(); try { await conn.beginTransaction(); // 查库存 const [[goods]] = await conn.query( 'SELECT stock FROM goods WHERE id = ? FOR UPDATE', [goodsId] ); if (!goods || goods.stock < count) { throw new Error('库存不足'); } // 查购物车是否已有该商品 const [[existing]] = await conn.query( 'SELECT id, count FROM carts WHERE user_id = ? AND goods_id = ?', [userId, goodsId] ); if (existing) { await conn.query( 'UPDATE carts SET count = count + ? WHERE id = ?', [count, existing.id] ); } else { await conn.query( 'INSERT INTO carts (user_id, goods_id, count) VALUES (?, ?, ?)', [userId, goodsId, count] ); } await conn.commit(); res.json({ code: 0, msg: '添加成功' }); } catch (err) { await conn.rollback(); res.status(400).json({ code: 400, msg: err.message }); } finally { conn.release(); } };

FOR UPDATE是行级锁,防止两个请求同时扣同一件商品的库存。beginTransaction和commit/rollback保证要么全成功要么全失败。这里有个坑:如果carts表没有(user_id, goods_id)的唯一索引,并发添加同一商品会插入两条记录。建表时加上:

ALTER TABLE carts ADD UNIQUE KEY uk_user_goods (user_id, goods_id);

4.3 订单创建与支付回调:状态机与幂等处理

订单创建是整条链路最复杂的部分。POST /order/create接收购物车 ID 列表或直接商品列表,做这几件事:

// controllers/orderController.js exports.create = async (req, res) => { const { items, addressId } = req.body; const userId = req.user.id; const conn = await pool.getConnection(); try { await conn.beginTransaction(); let total = 0; const orderNo = `MO${Date.now()}${Math.floor(Math.random() * 1000)}`; // 校验库存并计算总价 for (const item of items) { const [[goods]] = await conn.query( 'SELECT price, stock FROM goods WHERE id = ? FOR UPDATE', [item.goodsId] ); if (!goods || goods.stock < item.count) { throw new Error(`商品 ${item.goodsId} 库存不足`); } total += goods.price * item.count; } // 写订单 const [orderResult] = await conn.query( 'INSERT INTO orders (order_no, user_id, total, status, address_id) VALUES (?, ?, ?, 0, ?)', [orderNo, userId, total, addressId] ); const orderId = orderResult.insertId; // 写订单明细并扣库存 for (const item of items) { await conn.query( 'INSERT INTO order_items (order_id, goods_id, price, count) VALUES (?, ?, ?, ?)', [orderId, item.goodsId, item.price, item.count] ); await conn.query( 'UPDATE goods SET stock = stock - ? WHERE id = ?', [item.count, item.goodsId] ); } await conn.commit(); res.json({ code: 0, data: { orderId, orderNo, total } }); } catch (err) { await conn.rollback(); res.status(400).json({ code: 400, msg: err.message }); } finally { conn.release(); } };

status: 0表示待支付。支付回调接口POST /pay/notify收到微信支付通知后,把订单状态改成 1(已支付)。这里必须做幂等:微信可能重复通知同一笔订单。做法是在orders表加一个pay_no字段,回调时先查pay_no是否已存在:

exports.notify = async (req, res) => { const { orderNo, payNo, resultCode } = req.body; if (resultCode !== 'SUCCESS') { return res.json({ code: 'FAIL', msg: '支付失败' }); } const conn = await pool.getConnection(); try { await conn.beginTransaction(); const [[order]] = await conn.query( 'SELECT id, status FROM orders WHERE order_no = ? FOR UPDATE', [orderNo] ); if (!order) throw new Error('订单不存在'); if (order.status === 1) { // 已处理过,直接返回成功 await conn.commit(); return res.json({ code: 'SUCCESS' }); } await conn.query( 'UPDATE orders SET status = 1, pay_no = ? WHERE id = ?', [payNo, order.id] ); await conn.commit(); res.json({ code: 'SUCCESS' }); } catch (err) { await conn.rollback(); res.json({ code: 'FAIL', msg: err.message }); } finally { conn.release(); } };

FOR UPDATE锁住订单行,防止两个回调同时进来。order.status === 1判断是幂等核心,重复通知直接返回成功,不重复改状态。

5. 部署与联调中容易翻车的五个坑

这一章记录我实际踩过的坑,每个都按「现象 → 原因 → 解决」写。你遇到问题时可以直接对号入座。

5.1 坑一:小程序请求全部失败,控制台报 url not in domain list

现象:开发者工具里首页空白,Network 面板全是红色,报request:fail url not in domain list。

原因:微信小程序默认只允许请求已备案的 HTTPS 域名,http://localhost不在白名单里。

解决:开发者工具右上角「详情」→「本地设置」→ 勾选「不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书」。勾完重新编译即可。注意这个选项只在开发工具生效,真机预览时需要在微信公众平台配置合法域名,或者用内网穿透工具把本地服务暴露成 HTTPS。

5.2 坑二:数据库连接报 ER_ACCESS_DENIED_ERROR

现象:后端启动时报ER_ACCESS_DENIED_ERROR: Access denied for user 'root'@'localhost'。

原因:config/db.js里的密码和 MySQL 实际密码不一致,或者 MySQL 8 的认证插件变了。

解决:先确认密码。如果 MySQL 8 用的是caching_sha2_password,Node 的mysql2包需要升级到最新版,或者改用户认证方式:

ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'your_password'; FLUSH PRIVILEGES;

改完重启后端。

5.3 坑三:订单创建成功但库存没扣

现象:下单后订单列表有记录,但商品详情页库存没变。

原因:orderController.create里扣库存的UPDATE语句没执行,或者事务没提交。常见的是conn.query写成了pool.query,导致扣库存操作不在同一个事务里。

解决:检查所有数据库操作是否都用conn.query而不是pool.query。pool.query会从连接池另取一个连接,不在当前事务中。改完后用SELECT stock FROM goods WHERE id = ?确认扣减生效。

5.4 坑四:支付回调收不到,订单一直待支付

现象:用户付了钱,但订单状态还是 0。

原因:微信支付回调地址配置错误,或者后端没有正确处理xml格式的请求体。微信支付 v2 的回调是 XML 格式,不是 JSON。

解决:确认pay/notify路由能公网访问,且没有经过需要鉴权的中间件。解析 XML 用xml2js:

const xml2js = require('xml2js'); const parser = new xml2js.Parser({ explicitArray: false }); exports.notify = async (req, res) => { let body = ''; req.on('data', chunk => { body += chunk; }); req.on('end', async () => { const result = await parser.parseStringPromise(body); const { out_trade_no, transaction_id, result_code } = result.xml; // ... 处理逻辑 }); };

注意auth中间件要跳过这个路由,否则微信的请求没有 token 会被拦截。

5.5 坑五:商品图片在真机上不显示

现象:开发者工具里图片正常,真机预览时图片裂开。

原因:图片 URL 是http://开头,真机要求 HTTPS。或者图片存在本地uploads目录,真机访问不到你的localhost。

解决:把图片传到对象存储或 CDN,拿到 HTTPS 地址后更新goods.cover字段。本地开发可以用nginx配一个自签名证书,但真机不信任自签名证书,还是得用正规 HTTPS。最省事的办法是图片走图床,数据库只存 URL。

6. 二次开发前值得做的三件事

源码跑通只是起点,真正要投入二次开发,有三件事我建议你先做,能省掉后面大量返工。

第一件是给orders表加索引。默认建表语句里order_no可能没有唯一索引,user_id也没有普通索引。订单量上来之后,SELECT * FROM orders WHERE user_id = ?会全表扫描。加索引:

ALTER TABLE orders ADD UNIQUE KEY uk_order_no (order_no); ALTER TABLE orders ADD KEY idx_user_status (user_id, status); ALTER TABLE orders ADD KEY idx_created (created_at);

idx_user_status是联合索引,查「某用户的待支付订单」时直接命中。idx_created用于后台按时间筛选。

第二件是把config/api.js里的硬编码地址改成环境变量。小程序没有process.env,但可以用wx.getAccountInfoSync()判断环境:

// config/api.js const accountInfo = wx.getAccountInfoSync(); const env = accountInfo.miniProgram.envVersion; // develop / trial / release const BASE_URL_MAP = { develop: 'http://localhost:3000/api', trial: 'https://test.your-domain.com/api', release: 'https://api.your-domain.com/api', }; const BASE_URL = BASE_URL_MAP[env] || BASE_URL_MAP.develop; module.exports = { BASE_URL };

这样开发、体验、正式三个环境自动切换,不用每次手动改地址。

第三件是给关键接口加日志。middlewares/下新建logger.js:

// middlewares/logger.js module.exports = (req, res, next) => { const start = Date.now(); res.on('finish', () => { const duration = Date.now() - start; console.log(`[${new Date().toISOString()}] ${req.method} ${req.originalUrl} ${res.statusCode} ${duration}ms`); }); next(); };

在app.js里app.use(logger)注册。订单创建和支付回调出问题时,日志能帮你快速定位是请求没到、还是到了但报错。我一般会把日志写到文件里,用winston或pino,按天切割,保留 7 天。

这三件事做完,mofunShop-v2 才算真正能承载业务。我自己的习惯是每接手一套新源码,先跑通、再加索引和日志、最后才动业务逻辑。顺序反了,后面排查问题会非常痛苦。希望帮到你。

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

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

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

立即咨询