简介:Niushop企业版V4多商户商城源码是一套以PHP为主的开源多商户电商系统,采用插件化架构,融合分销、团购、直播、秒杀、优惠券、砍价、DIY自定义页面等50余种营销能力,覆盖微信小程序、H5、PC、APP等多终端商城场景,适合需要快速搭建多商户平台或进行二次开发的团队与企业。压缩包为zip格式,约33.28MB,共7302个文件;其中4536个php文件构成后端业务核心,946个png与550个html提供页面与视觉素材,281个js、194个css实现前端交互,另有48个sql数据库脚本和134个md文档,便于部署与代码阅读,整体目录结构按模块划分,清晰易查。包内还额外包含商家手机管理端源码与完整前端工程,可直接部署为含后台管理、商家入驻、营销插件、多端前台的城市版商城;涉及的订单、会员、分销、秒杀等模块均可在此基础上继续扩展。目前已有2615人浏览学习,对正在选型开源商城或计划深度二开的开发者来说,这是一份代码完整、可运行、易扩展的参考工程。
1. 微信小程序商城不是套个模板就行:多商户源码的取舍与边界
做多商户商城,最容易翻车的不是页面,而是「商户、平台、用户」三种身份在一个订单里怎么算钱。微信小程序只是一个前端壳,真正的复杂度在商品归属、订单拆单、佣金分摊和结算对账。这套 niushop 企业版 v4 多商户商城源码,把后台管理、接口层、小程序端三块都摊开了,适合两类人:一类是接私活或外包的 PHP 开发者,拿它做 B2B2C 平台交付;一类是运营方想快速把平台跑起来,再让技术团队做二次开发。先说清边界:它不是零代码搭建工具,部署和对接微信生态还需要一点运维底子。
2. 部署安装与初始化:从环境检测到伪静态的一小时实录
这套源码解压后第一眼是典型的 PHP MVC 布局,后台入口一般在/admin,前端接口走public目录的入口文件。我实际部署过几套,真正折磨人的不是安装向导,而是环境差半级带来的各种玄学报错。这一章按我自己的操作顺序来:先说环境选型,再走部署步骤,最后把伪静态规则单独拎出来讲,因为它在 Nginx 下最容易出问题。
2.1 运行环境选型:为什么 PHP 7.1 + MySQL 5.7 + Redis 是舒适区
这套源码的底层是 ThinkPHP 框架,PHP 版本太新反而容易踩坑。PHP 7.4 之后,老代码里常见的each()、create_function()会被移除,安装向导的环境检测页面会直接标红。我一般建议 PHP 装 7.1 到 7.3 之间,别图新。MySQL 用 5.7 最稳,MariaDB 10.2 以上也能跑,但要注意字符集统一用 utf8mb4,否则商品标题里带特殊符号会存入失败。
Redis 是硬依赖,不是可选项。验证码、登录 token、购物车、商品详情缓存全往 Redis 写。如果 Redis 没装或者没启动,最典型的表现是后台验证码图片不显示、小程序端登录一直转圈。所以部署前先确认三件事:PHP 版本、MySQL 版本、Redis 可用。
| 组件 | 建议版本 | 说明 |
|---|---|---|
| PHP | 7.1 ~ 7.3 | 必须开启 fileinfo、curl、gd、pdo_mysql、redis 扩展 |
| MySQL | 5.7 | 事务与 InnoDB 是硬要求,MyISAM 别用 |
| Redis | 4.0 以上 | 存验证码、token、缓存,建议设 maxmemory 256mb |
| Web 服务器 | Nginx 或 Apache | Nginx 需配伪静态,Apache 需开 mod_rewrite |
小经验:PHP 7.1 的openssl扩展必须单独确认开启,微信支付回调验签和 code2Session 都依赖它。用一行命令就能查:
php -m | grep -E "fileinfo|curl|gd|pdo_mysql|redis|openssl"正常输出会列出这六个扩展名,缺哪个就装哪个。这里过滤出的每一项都对应源码里一个具体功能模块:fileinfo管图片上传的二进制识别,curl管微信接口请求,gd管验证码图片生成,pdo_mysql管数据库连接,redis管缓存,openssl管支付验签。缺任何一个,安装向导都会在环境检测步骤卡住。
2.2 部署三步走:解压、绑定、装库
部署流程不复杂,但顺序错了会绕远路。我习惯按三步走:
第一步,把源码解压到站点根目录,确认目录结构里有app、addon、thinkphp、public这几个核心目录。app里是业务代码,addon里是插件机制,public是 Web 对外目录。
第二步,把域名根目录绑定到public,不是绑定到源码根目录。这一步很多人搞反,导致后台能打开但首页样式全丢。绑定后访问域名,会自动跳转到安装向导页面。
第三步,填写数据库信息和管理员账号。数据库建议提前建好并授权,账号不要用 root,给业务库单独建一个只读写的账号,权限最小化。安装完成后系统会生成一个锁定文件,同时提示你删除install目录,这个动作别跳过——不删的话,别人访问/install就能重装并把管理员密码改掉。
安装向导页在 Nginx 下有几个页面会白屏,原因多半是伪静态没配。Nginx 的推荐配置如下:
server { listen 80; server_name yourdomain.com; root /data/www/niushop/public; index index.php index.html; location / { if (!-e $request_filename) { rewrite ^(.*)$ /index.php?s=$1 last; } } location ~ \.php$ { fastcgi_pass unix:/tmp/php-cgi.sock; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } }这段配置的核心是两个location块。第一个location /里的if判断请求的文件是否存在,不存在就重写到index.php,这就是 ThinkPHP 的路由入口;第二个location ~ \.php$负责把 PHP 文件交给 FastCGI 进程处理。fastcgi_pass后面的路径要跟你实际 PHP-FPM 的 socket 路径一致,可以用php-fpm -i | grep listen查到。常见错误是 socket 路径填错,表现为访问任何 PHP 页面都返回 502。
2.3 安装后的第一件事:验证后台与小程序的连通性
安装完成、能登录后台,只算成功了一半。我习惯立刻做一轮连通性验证,免得后面排查时不知道是哪一层出了问题。
第一步,登录后台,打开「系统设置 → 站点设置」,把站点的 URL 改成当前域名,不带结尾斜杠。这个字段会写进生成静态页和分享链接的逻辑里,填错的话小程序端分享出去的卡片会打不开。
第二步,打开「微信管理 → 公众号/小程序设置」,确认 AppID 和 AppSecret 的保存按钮点了没。源码里前端申请一个授权,后台如果没保存密钥,回调会报「AppSecret 错误」。
第三步,用开发者工具把小程序端编译一遍,看控制台有没有request域名校验的报错。这一步能提前暴露域名和证书问题,比等真机预览白屏再查省事得多。
这三步走完,这套基础就稳了。接下来才值得去碰多商户的核心数据流。
3. 多商户核心链路:商品、订单、结算的数据流与后台配置
如果只把后台所有菜单点一遍,你会觉得这套系统功能很全;但真正决定平台能不能良性运转的,是商品归属、订单拆分、佣金计算、结算对账这一条链路。这一章我按数据流顺序拆:先看库表结构,再看订单状态流转,最后看结算规则。
3.1 平台、商户、会员的三级结构:先搞清楚一张订单属于谁
在多商户系统里,每一件商品头上都顶着「商户 ID」。订单主表里也有两个关键角色:下单的会员 ID 和接收订单的商户 ID。理不清这个归属关系,后面分账一定乱。
常见的库表结构里,涉及核心数据的表大概包括这些:会员表存买家信息,商户表存店铺信息,商品表通过seller_id指向商户表,订单主表同时记录会员和商户两侧的 ID。举一个实际对账时最常用的查询——按商户维度汇总订单金额:
SELECT o.seller_id, s.shop_name, SUM(o.pay_price) AS total_pay, SUM(o.platform_commission) AS platform_income, SUM(o.seller_income) AS seller_income, COUNT(o.order_id) AS order_count FROM order_main o LEFT JOIN shop s ON o.seller_id = s.shop_id WHERE o.order_status = 3 AND o.pay_time BETWEEN '2024-01-01 00:00:00' AND '2024-01-31 23:59:59' GROUP BY o.seller_id, s.shop_name ORDER BY total_pay DESC;这个 SQL 里有几个字段名在不同的表里可能略不一样,但查询思路是通用的:pay_price是用户实际支付金额,platform_commission是平台抽走的佣金,seller_income是商户最终收入。三者要满足「支付金额 = 佣金 + 商户收入」的等式,如果有订单违反这个等式,说明佣金配置或者退款冲抵逻辑出了问题。我在做数据校验时,第一件事就是跑这个汇总,核对等式两边是否相等。
3.2 订单状态流转与库存扣减:锁库存和支付回调的时序
多商户商城的订单状态,和单商户最大的差异在于「拆单」。用户在购物车里选了 A 店和 B 店的商品,提交订单时系统会生成对应数量的子订单,每个子订单归属各自的商户。拆单逻辑在主订单表和子订单表之间完成,主订单管支付和物流维度,子订单管商户维度和售后维度。
状态流转主线是:待付款 → 已付款待发货 → 已发货待收货 → 已完成。支线有取消和退款。这里面最容易出 bug 的是锁库存和支付回调的先后顺序。
我见过一套跑得好好的系统,突然出现「支付成功但商户端不显示订单」的情况。查到最后是库存扣减接口里的一个参数问题:支付回调回来时,系统把订单状态改成了已付款,但回调处理时它把「支付成功的订单列表」误处理成了「待付款订单列表」,导致商户后台漏单。解决方式是检查回调里查订单的查询条件,确保order_status过滤的是支付成功对应的状态。
支付回调的验签逻辑不能省。微信支付回调拿到的数据要先校验签名,再用商户号验证。以下是一段典型的回调处理骨架:
$postData = file_get_contents('php://input'); $result = json_decode($postData, true); if ($result['return_code'] === 'SUCCESS') { // 先验签,验签通过后再处理业务 if (WxPay::verifyNotifySign($result)) { $orderNo = $result['out_trade_no']; $order = (new OrderModel())->where('order_no', $orderNo)->find(); if ($order && $order['order_status'] == 0) { $order->order_status = 1; $order->pay_time = time(); $order->save(); // 扣减库存的操作放在这里,不要放在下单时 } } }这里有个关键设计:库存扣减放在支付回调里,而不是用户点击下单时。原因很简单,未支付订单的库存如果先扣掉,会导致超卖假象和恶意占库存。但放在回调里就要求回调接口必须幂等——同一笔回调通知可能到达多次,处理前一定要判断订单状态已经是已付款就直接返回成功,避免重复扣减库存。out_trade_no就是主订单号,回调里用它匹配订单。
3.3 结算规则:平台抽佣、商户货款与退款冲抵
多商户平台的收入来源是抽佣。这套源码的后台里,佣金比例通常按商品类目或商户等级配置,比如数码 5%、服饰 8%。结算逻辑则是:订单完成后,经过一个售后期(比如 7 天),系统把商户货款结算到可提现金额,平台佣金则进入平台的收入账户。
最容易出问题的场景是退款。一个订单支付了 100 元,平台按 10% 抽佣,但后来用户退款了 50 元,那佣金应该按 50 元重新计算,还是维持原来的 10 元?正确做法是:退款订单按退款后的实际留存金额冲抵佣金,平台收入应当减少到 5 元,商户可结算金额同样按退款后的金额计算。但有些版本在退款时不自动生成绩联的结算冲抵记录,导致月底对账时平台收入虚高。
我当时排查一个对不上账的问题,最后定位到是售后单关闭后,系统没有把退款金额从结算单里扣回来。解决办法是在售后完成的回调里,增加一条结算冲抵记录。以下是结账单更新逻辑的参考写法:
$settlement = SettlementModel::where('order_id', $orderId)->find(); if ($settlement && $settlement['status'] == 0) { // 退款冲抵:用退款金额反向更新待结算金额 $settlement->settle_amount = $settlement->settle_amount - $refundAmount; $settlement->refund_amount += $refundAmount; if ($settlement->settle_amount < 0) { $settlement->settle_amount = 0; } $settlement->save(); }settle_amount是商户实际能拿到的钱,refund_amount是这笔结算单里累计的退款额。更新的前提是status == 0,也就是结算单还没被平台确认打款;一旦结算状态变成已打款,退款只能走线下转账,系统里不会自动冲抵,这个前后逻辑要在后台配置里区分清楚。每次上线新商户,我都会先问一句:退款是走系统冲抵还是线下处理,然后对应的配置走一遍。
4. 微信小程序端对接:从 AppID 到登录鉴权的四个关键步
多商户源码一般会附带小程序端代码目录,这是它能快速上线微信小程序商城的核心。但小程序端拿到手,不是填个 AppID 就能跑的。这一章按对接顺序写:先看代码结构,再配域名和证书,然后打通登录鉴权,最后验支付。
4.1 小程序端目录结构:先分清「页面」和「接口层」
解压后的代码里,小程序端通常是独立目录,里面有pages、utils、api这几个核心目录。pages放的是页面级的.wxml和.js,api目录里集中封装了所有请求接口,utils里放工具函数和请求基类。
第一件要改的事,不是在 AppID 那里改,而是在api目录下的请求工具里改baseUrl。源码里默认的baseUrl往往是作者的测试域名,比如https://example.com/api,如果你不改,小程序发出去的每个请求都会打到一个不存在的服务器上。我一般会把baseUrl改成https://你的域名/api,同时确认后台管理里配置的接口地址和这是一致的。
// api/request.js 片段 const BASE_URL = 'https://yourdomain.com/api'; function request(path, method = 'GET', data = {}) { return new Promise((resolve, reject) => { wx.request({ url: `${BASE_URL}${path}`, method: method, data: data, header: { 'content-type': 'application/json', 'token': wx.getStorageSync('token') || '' }, success: (res) => { if (res.data.code === 1) { resolve(res.data.data); } else { wx.showToast({ title: res.data.msg, icon: 'none' }); reject(res.data); } }, fail: reject }); }); }这段代码每次请求都会从本地存储里取 token 放到请求头,后端接口通过 token 识别当前用户。code === 1是这套系统的统一成功标记,其他状态码都按失败处理。求职信式的错误提示在开发调试时很管用,但上线前要把wx.showToast的信息换成用户能懂的话,不然接口报错信息会把系统细节暴露给用户。
4.2 合法域名、SSL 证书与业务域名:真机白屏的九成原因
开发工具里跑得好好的小程序,一上真机就白屏,绝大多数是合法域名没配或者证书链不完整。小程序后台的「开发管理 → 开发设置 → 服务器域名」里,需要配置以下三类:
request合法域名:所有接口的https://你的域名uploadFile合法域名:图片上传接口所在域名downloadFile合法域名:文件下载和预览接口所在域名
这三项都必须是 HTTPS,并且证书链要完整。我遇到过一次接口能通但图片不显示的怪问题,查到最后是证书只装了一半,中间证书没部署,手机端校验严格直接拦截了。Linux 下用一行命令检查证书链完整性:
openssl s_client -connect yourdomain.com:443 -servername yourdomain.com输出里的Verify return code如果是0,说明证书链完整;如果出现unable to verify the first certificate,就是中间证书缺失,需要在服务器上把 CA 证书一并配置。这个小检查我每次上线前都会跑一遍,比在手机端反复清缓存调试快得多。
4.3 登录鉴权:wx.login 到 code2Session 的完整链路
小程序端没有传统的密码登录,用的是微信的静默登录体系。在小程序端调用wx.login拿到临时 code,后端用这个 code 去微信接口换取 openid 和 session_key,然后生成自己的登录态。
小程序端典型的wx.login调用:
wx.login({ success: async (res) => { if (res.code) { const data = await request('/login/code2session', 'POST', { code: res.code }); wx.setStorageSync('token', data.token); wx.setStorageSync('userInfo', data.userInfo); } } });后端接住 code 之后,用 code 去微信的jscode2session接口换身份信息:
$appId = '你的AppID'; $appSecret = '你的AppSecret'; $url = "https://api.weixin.qq.com/sns/jscode2session" . "?appid={$appId}&secret={$appSecret}&js_code={$code}&grant_type=authorization_code"; $result = file_get_contents($url); $info = json_decode($result, true); // $info['openid'] 是用户唯一标识 // $info['session_key'] 用于手机号解密数据拿到 openid 之后,源码的处理逻辑一般是先去会员表里查这个 openid 是否已存在,不存在就创建一个新会员,存在就刷新登录 token。session_key是敏感数据,不能返回给前端,它只用于后端解密手机号等微信加密数据。所以在这段代码后面,接口返回的应该是系统自己的 token 和用户基本信息,而不是 openid 或 session_key 本身。
4.4 支付与手机号授权:解密流程的边界问题
支付这块,小程序端调用wx.requestPayment之前,需要先让后端生成支付参数。后端返回的参数包含timeStamp、nonceStr、package、signType、paySign这几个字段,直接透传给wx.requestPayment即可。常见的问题是package字段拼错,正确格式是prepay_id=xxxxx,很多新手会把prepay_id=前缀漏掉,导致拉起收银台时报「支付参数错误」。
手机号快速验证是这个源码里比较老逻辑的典型场景。新版小程序要用button的open-type="getPhoneNumber"拿到encryptedData和iv传给后端,后端用session_key解密出手机号。如果解密失败,先看 session_key 是否过期——小程序端必须重新wx.login刷新 session_key 之后再做解密,否则会频繁报「解密失败」。
$decryptedData = ''; $errCode = openssl_decrypt( base64_decode($encryptedData), 'AES-128-CBC', $sessionKey, OPENSSL_RAW_DATA, base64_decode($iv) );这段是手机号解密的 PHP 核心逻辑,参数分别是微信加密数据、加密算法、会话密钥、原始输出格式和初始向量。$sessionKey必须与登录时拿到的一致,如果前后端任何一侧重新刷了登录态,旧 session_key 就会立刻失效。调试解密问题时,先把 session_key 的生成和存储时间打出来看,八成是刷新时序的问题,不是算法问题。
5. 常见问题排查与避坑:五个真实翻车记录
这一章写我在这套源码上真正踩过的坑,按「现象 → 原因 → 解决」的方式记录,每一条都花过不少时间排查。
5.1 安装向导卡在环境检测:PHP 扩展缺装的假象
现象:环境检测页面有一项红色 ×,提示curl 扩展未启用,但 PHP 命令行里php -m明明能查到这个扩展。
原因:PHP-FPM 和 PHP CLI 加载的配置文件不是同一份。CLI 走的是/etc/php/7.2/cli/php.ini,FPM 走的是/etc/php/7.2/fpm/php.ini,你只在命令行装好了扩展,FPM 那边没启用。
解决:找到 FPM 的 php.ini,确认extension=curl.so这行没被注释,或者用php -i | grep loaded对比 CLI 与 FPM 的加载路径。改完配置记得systemctl restart php7.2-fpm,重启后重新跑安装向导。我后来养成一个习惯:检查扩展一律看 FPM 的进程信息,而不是只看命令行结果。
5.2 后台验证码不显示:Redis 没启动的伪装
现象:后台登录页的验证码图片一直加载不出来,或者显示一个裂图图标。浏览器控制台看不出任何报错。
原因:验证码的生成和存储不落 MySQL,而是写进 Redis。Redis 没启动,写入失败,验证码就生成不出来。
解决:执行redis-cli ping,如果返回PONG说明正常;如果返回连接失败,启动 Redis 服务并确认密码参数填对了。这套源码的配置里就算没设密码也要留空字符串,不能随便填一个占位符。检查完 Redis 再刷新验证码,通常立刻恢复。
5.3 小程序请求全部失败:HTTPS 证书链不完整
现象:开发工具里预览一切正常,用真机扫码打开,所有接口都报request:fail,页面数据完全空白。
原因:开发工具不会严格校验证书链,但真机上的微信客户端会校验整个信任链。服务器只部署了域名证书,缺了中间证书。
解决:用上文的openssl s_client命令检查Verify return code。如果证书链不完整,把 CA 证书追加到服务器证书后面,形成一个完整链文件重新配置一遍。这个问题的隐蔽性在于,本地 curl 测试可能正常,只有移动端才拦截。
5.4 商户结算单金额对不上:退款订单重复记账
现象:月底对账时,平台后台的「佣金收入」和每个商户的「打款总额」加总后,比实收金额多了几千块。差异额全部来自退款订单。
原因:订单退款后生成了一条退款流水,但结算单里的待结算金额没有同步冲抵。等结算周期到了,系统按原订单金额生成结算单,商户实际拿到的退款后的钱,账却按退款前的记录,差额就悬空了。
解决:检查退款完成的通知回调,确认是否有更新对应结算单的逻辑。如果没有,参考 3.3 小节的上半段方式补上冲抵逻辑。我在排查这类问题时,先用 SQL 把所有已退款但结算单金额未变的订单捞出来:
SELECT o.order_no, r.refund_amount, s.settle_amount FROM order_main o JOIN order_refund r ON o.order_id = r.order_id JOIN settlement s ON o.order_id = s.order_id WHERE r.refund_status = 4 AND s.refund_amount = 0;这条 SQL 里的refund_status = 4表示退款完成,settlement.refund_amount = 0表示结算单里没有冲抵记录。这两列一旦同时满足,就是一笔铁定的重复记账订单。修复后,我每次上线前都要跑这条 SQL 确认结果集为空,再看结算汇总。
5.5 商品详情页打开 404:伪静态规则失效
现象:首页和后台能正常打开,但点击任意商品进入详情页就 404,URL 是正常的goods/detail?id=1形式。
原因:Nginx 配置里把伪静态规则删掉了,或者把location /的 rewrite 状态写成了注释。没有伪静态规则,ThinkPHP 的路由就不能正常解析 URL。
解决:把 2.2 小节的 Nginx 伪静态规则重新加回去,确认root指向public目录后再nginx -t测试配置、nginx -s reload重载。加完之后随手访问一个商品详情页验证。这个问题容易在服务器重启、迁移环境后出现,也算老生常谈了。
6. 上线前必做的一步:结算对账脚本与缓存预热
这章说一个我每次上线前都强制走一遍的操作,两个小脚本,能省掉上线后一半的麻烦。
第一个是对账脚本。多商户平台最怕的不是功能缺失,而是钱对不上。我把 5.4 小节的对账 SQL 做成了定时任务,每天凌晨跑一遍,把结果输出成 CSV 发到运营邮箱。一旦发现支付金额 = 佣金 + 商户收入等式被破坏,邮件里会列出所有异常订单号,运营可以直接拿到订单去查问题。
# crontab 每天凌晨 2 点执行对账脚本 0 2 * * * php /data/www/niushop/cli/check_settlement.php >> /data/logs/settlement.log 2>&1这份脚本从数据库里把已支付未结算的订单拉出来,逐单计算平台佣金和商户收入,如果差值超过 0.01 元,就写入异常列表并把订单号追加到日志里。这样做的好处是时效性——正常情况下每个工作日早上排查完前一天的异常,不会把问题拖到月底。
第二个是商品详情的缓存预热。商城系统流量上来后,数据库压力首先出现在商品详情页。这套源码里商品详情是有缓存位的,但第一次访问某商品时往往没有缓存,要回源查库。我的做法是用脚本把热销商品的详情主动写进缓存:
// 初始化热销商品缓存 $hotGoods = (new GoodsModel())->getHotGoods(100); foreach ($hotGoods as $goods) { $cacheKey = 'goods_detail_' . $goods['goods_id']; $detail = (new GoodsModel())->getDetail($goods['goods_id']); cache($cacheKey, $detail, 3600); }细看这段代码:getHotGoods(100)从商品表按销量筛选前 100 个商品,然后逐个调用getDetail取详细信息,最后用一个小时的过期时间写入缓存。这样用户访问热销商品时直接命中缓存,不会回源查库。实际使用中,缓存过期时间我会调到 30 到 60 分钟之间,太短起不到缓存效果,太长会导致库存和价格更新不及时。
从那以后,我每次给商户做上线复盘,都强制把这两件事走完一遍:先跑一次对账脚本确保净额一致,再预热一遍热销商品列表。这个习惯救了我好几次——有一回上线当晚就发现某个类目的佣金比例配置错了,对账邮件第二天一早把问题摆到桌面上,商户还没发现,我们就已经修复了。希望这些实操细节能帮你少踩几个坑,也祝你的项目上线顺顺利利。
本文还有配套的精品资源,点击获取