DSmall v6.1.5 多商户商城源码拆解:PHP B2B2C 部署与二次开发指南

发布时间:2026/10/13 23:16:52
DSmall v6.1.5 多商户商城源码拆解:PHP B2B2C 部署与二次开发指南
简介DSmall v6.1.5 是一套基于 PHPMySQL 的多商户 B2B2C 开源商城源码面向需要搭建多店铺电商平台或进行二次开发的开发者、企业及电商创业者。系统采用 ThinkPHP 后端框架H5 端基于 Vue.js 实现前后端分离整体按 B/S 架构组织适合学习电商项目分层、接口设计及多商户交易场景。资源共包含 13466 个文件压缩包约 65.95MB以 11208 个 PHP 业务文件为主体搭配 898 个 HTML 页面、226 个 JS、81 个 CSS 与图片素材另有 65 个 Markdown 文档、64 个 JSON 配置和 4 个 SQL 数据库脚本目录完整可按模块检索。已有 1030 人浏览学习。获取后既能获得完整源码与数据库脚本也可借助页面文件、配置文件与部署相关脚本快速搭建环境作为 ThinkPHPVue 前后端分离商城项目的实战参考并在此基础上扩展多商户功能。1. DSmall v6.1.5 多商户商城源码PHP 场景下 B2B2C 复现的一次完整拆解绝大多数把多商户商城当单商户商城来部署的人最后都会在结算对账这一步翻车。DSmall v6.1.5 是一套基于 PHP 的多商户 B2B2C 开源商城源码和普通 B2C 单店系统最大的区别是它把平台、商户、会员三层角色关系和资金分账逻辑都写进了同一条订单链路里。它面向的不是只做展示的站点而是需要真正经营、招商、抽佣和提现的商城平台。适合三类人要快速搭建多商户平台的创业者、接外包需要拿源码二次开发的 PHP 工程师以及拿真实商城结构做课程设计的学生。后面所有内容都围绕一件事怎么把这套源码跑起来改得动上线前不踩大坑。2. 先看架构再动手DSmall 的模块划分、目录结构与核心表关系拿到源码先花 20 分钟读目录比直接打开控制器找代码有用得多。这套系统不是单入口平铺结构而是按平台端、商户端、前端商城三套界面分开组织的。我见过不少开发者把前台模板改到 admin 目录里改完怎么刷新都不生效就是因为没搞懂目录归属。这一章把源码骨架拆清楚后面改哪一行才知道去哪个文件。2.1 平台、商户、会员三层权限的设计逻辑B2B2C 在账务上天然拆成三个角色平台运营方、入驻商户、普通消费者。对应到权限设计就是三套独立后台加一个前台商城。平台运营方负责审核商户、发布平台活动、处理全局配置入驻商户只能管理自己的商品、订单和结算单消费者只关心浏览、下单、支付和售后。DSmall 的核心权限做法是给所有数据都打上 store_id 标记。商品表有 store_id订单表有 store_id结算表有 store_id甚至优惠券也有 store_id。查询的时候中间件会强制附加当前商户的 store_id 条件这样商户 A 就算手工拼接 URL也看不到商户 B 的订单。平台管理员则可以跨 store 查询因为后台登录态里保存的是 admin_id 而不是 store_id。这里涉及的表主要有四个ds_admin 管理员表ds_role 角色表ds_store 商户表ds_user 用户表。平台后台的菜单权限用 ds_role 里的 rules 字段控制存的是菜单 id 序列商户端员工账号则通过 store_id 挂到指定商户下面员工离职时在商户后台禁用账号即可不用动平台管理员权限。三层登录态也是独立存储的。平台后台登录后写入 admin_id商户端登录后写入 store_id用户中心登录后写入 user_id。三个会话互不干扰这一点在改登录逻辑时要特别注意很多人把商户登录判断写成 user 登录判断导致商户进后台直接被跳到用户中心。2.2 从代码目录看懂请求链路入口文件到控制器再到视图先看整体目录结构这是源码的第一层骨架dsmall/ ├── application/ │ ├── home/ // 前台商城模块 │ ├── admin/ // 平台运营后台 │ ├── shop/ // 商户端后台 │ └── api/ // 移动端接口模块 ├── public/ │ ├── index.php // 唯一入口文件 │ └── uploads/ // 商品图片、头像上传目录 ├── runtime/ // 运行缓存、日志、模板编译 ├── config/ // 全局配置目录 ├── extend/ // 第三方扩展类库 └── data/ // 初始 SQL 脚本与备份public 目录是 Web 根目录入口文件只有 public/index.php 一个。常见做法是把站点根目录指向 public这样 application、runtime、data 都不会暴露到 URL 上。如果你用的是虚拟主机没法改根目录至少要把 data 目录和 SQL 文件用访问规则挡掉否则数据库结构直接暴露给对方。请求链路大致是这个顺序浏览器请求http://域名/index.php?s/home/goods/index框架先解析 s 参数定位到 application/home/controller/GoodsController.php然后调用 index 方法方法里通过模型查询数据库最后把数据渲染到 application/home/view/goods/index.html。这里 s 参数是 PathInfo 模式的传参方式前半段是模块名后半段是控制器名和方法名。前台、平台后台、商户端的模板是分开的。修改商城页面要去找 home/view 下面对应的控制器目录修改平台管理页面要找 admin/view商户后台则是 shop/view。很多新手把前台商品列表样式改了不生效然后发现改的是 admin/view/goods 下的文件这种错位问题看目录结构就能避免。runtime 目录是三套模块共用的但子目录一般按模块区分。改了模板不生效时优先清理 runtime/home/temp 下的编译文件而不是盲目清空整个 runtime清多了可能会把后台在线配置项也重置掉。2.3 核心表关系goods、store、order、user 之间怎么关联B2B2C 的表关系核心是商品和商户挂着订单和用户挂着订单明细里又保留一份商品快照。直接看几个关键表的字段关系SELECT g.goods_id, g.goods_name, g.goods_price, s.store_id, s.store_name, u.user_id, u.nickname FROM ds_goods g LEFT JOIN ds_store s ON g.store_id s.store_id LEFT JOIN ds_user u ON g.user_id u.user_id WHERE g.goods_state 1这段 SQL 是前台商品列表最常用的关联方式。goods_state 字段控制商品上下架状态1 表示上架0 表示下架10 表示待审核。LEFT JOIN 的原因是商品可能被平台下架但商户信息还在用户字段可能是空因为平台自营商品没有创建者。核心表关系可以整理成一张表表名关键字段作用说明ds_storestore_id, commission_rate商户主表存商户名、佣金比例、保证金ds_goodsgoods_id, store_id, goods_state商品主表通过 store_id 归属商户ds_useruser_id, nickname消费者账号ds_orderorder_id, order_sn, user_id, store_id订单主表跨商户订单会被拆成多个子订单ds_order_goodsorder_goods_id, order_id, goods_id, store_id订单明细保留商品快照ds_settlementid, order_id, store_id, amount, commission结算表记录每笔订单商户应收与平台佣金订单是这套关系里比较复杂的一张表。用户一次下单购买了两个不同商户的商品系统会在生成订单时拆分成两个子订单每个子订单带不同的 store_id但 order_sn 会共享同一个父订单编号。查询商户订单时要用 store_id 过滤而不是用 order_sn 去猜归属。修改表结构时要注意表前缀 ds_ 在配置文件中是可改的数据库迁移脚本和代码里的表名要保持一致。如果手工加了新表一定要同步更新 data 目录下的初始化 SQL否则安装向导建表时不会带上你的新表上线后才发现生产库缺少字段那时候补数据就麻烦多了。3. 部署与二次开发从本地跑起来到第一个商城促销标记部署的核心原则是先跑起来再动手改代码。很多人一拿到源码就开始改路由、改控制器改到一半发现环境都没跑通调试起来分不清是代码问题还是环境问题。这一章从零开始把环境、部署、后台权限和一个小钩子插件完整走一遍。3.1 环境要求与快捷部署步骤先看一组我能确认兼容性比较好的环境参数组件建议版本说明PHP7.0 7.4推荐 7.2兼容性最稳MySQL5.6 及以上推荐 5.7事务支持完整Web 服务器Nginx 或 Apache需要开启 PathInfo 支持Redis可选用于会话和缓存加速PHP 8.0 以上跑这套源码需要额外处理兼容问题主要是部分内置函数用法变了不建议在没测试的情况下直接上生产。MySQL 8.0 也可以跑但排序规则建议用 utf8mb4_unicode_ci避免 emoji 写入报错。快捷部署步骤如下# 解压源码到站点目录 unzip dsmall_v6.1.5.zip -d /var/www/dsmall cd /var/www/dsmall # 修改数据库连接配置 vim application/database.php # 导入初始数据库 mysql -uroot -p dsmall data/dsmall_v6.1.5.sql # 设置 runtime 和 uploads 目录写权限 chmod -R 755 runtime chmod -R 755 public/uploadsdatabase.php 里主要改四个值数据库主机、数据库名、用户名、密码。如果跑在本地虚拟机里主机建议写 127.0.0.1 而不要写 localhost因为 PHP 在某些环境下解析 localhost 会走 IPv6 导致连接超时。Nginx 下的伪静态配置是关键一步路径不对会直接白屏server { listen 80; server_name shop.example.com; root /var/www/dsmall/public; index index.php; location / { if (!-e $request_filename) { rewrite ^(.*)$ /index.php?s$1 last; } } location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } location ~* ^/(data|runtime)/ { deny all; } }这段配置有两个作用。第一个是把不存在的文件路径转换成 index.php 的 s 参数让/index.php?s/home/goods/detailid1这样的地址可以直接对应到控制器第二个是把 data 和 runtime 目录挡掉防止别人下载 SQL 备份或者查看日志文件。3.2 后台权限与商户端上架流程部署完成后访问/index.php?s/admin/login/index使用初始管理员账号登录首次登录后必须改密码。平台后台的菜单通常包含管理员管理、商户管理、商品审核、订单管理、结算管理、营销设置这几个大块。商户端上架流程是这样的平台后台先添加一个商户账号设置商户名称、联系人、手机号、佣金比例。商户账号拿到手后访问/index.php?s/shop/login/index登录在商户后台发布商品。商品发布时需要选择分类、填写名称、价格、库存、主图、详情提交后进入审核状态。商品状态的控制逻辑通常集中在商品模型里// 商品状态流转0下架1上架10待审核 public function changeGoodsState(int $goodsId, int $state, bool $isAdmin): bool { if ($isAdmin) { // 平台管理员直接上下架 return $this-where(goods_id, $goodsId)-update([goods_state $state]); } // 商户提交商品时置为待审核 return $this-where(goods_id, $goodsId)-update([goods_state 10]); }这段代码里的 isAdmin 参数来自会话中的 admin_id 判断。平台管理员可以跳过审核直接把商品下架商户端则只能把商品提交成待审核状态。实际项目里建议在控制器里先做权限校验再调用这个模型方法不要把 isAdmin 直接从客户端参数里去取否则商户可以通过伪造参数绕过审核。3.3 用钩子机制加一个简单的促销标记很多二次开发需求并不需要改动原有控制器的逻辑比如要给商品列表加一个“今日秒杀”标签。DSmall 这类 PHP 系统通常都保留了钩子扩展点可以通过行为绑定来插入自定义逻辑。第一步是在钩子配置里注册行为// config/tags.php return [ // 应用初始化 app_init [], // 商品列表过滤钩子传入商品列表数组 goods_list_filter [ app\\common\\behavior\\GoodsPromTag, ], ];第二步是实现行为类// application/common/behavior/GoodsPromTag.php namespace app\common\behavior; class GoodsPromTag { // 参数 $goodsList 是引用传值可以原地修改数据 public function run($goodsList): void { foreach ($goodsList as $item) { // prom_type1 表示秒杀活动2 表示限时折扣 $item[prom_tag] $item[prom_type] 1 ? 今日秒杀 : ; } } }钩子机制的关键在于控制器里必须有一处Hook::listen(goods_list_filter, $goodsList)的调用行为类才会被触发。如果没有这行调用配置写了也不会执行。钩子传参的变量名要和控制器里 listen 的第二个参数一致这里使用引用传值是为了避免 return 后还要手动赋值。这样加促销标记的好处是核心控制器不动后续要移除这个功能只需要把 tags.php 里的行为类注释掉缓存清理一下就可以回滚。如果直接改控制器里的 while 循环上线后出问题你就得在业务代码里反复找那段临时逻辑非常痛苦。4. 支付、结算与分账B2B2C 比单商户多出来的那部分财务逻辑单商户商城的支付逻辑很简单用户下单、付款、商户收款。多商户商城多了一个平台角色钱不能直接到商户口袋要先经过平台再按结算周期分给商户。这就带来了一套额外表结构和流程。这一章把支付、抽佣、退款三个环节完整拆开讲。4.1 支付流程与回调处理支付流程通常是这样用户在前台创建订单点击支付时生成一笔支付单支付单里记录了订单号 order_sn、金额 amount、支付渠道 payment_code。用户跳转到第三方支付平台完成付款支付平台异步回调商户接口回调地址指向/index.php?s/api/payment/notify。回调处理是所有支付流程里最容易出错的地方我给一个通用伪代码// 支付回调处理 public function notify() { $params $_POST; // 第一步验签签名不对直接返回失败 if (!checkSign($params, $this-config[pay_key])) { return fail; } $orderNo $params[out_trade_no]; $payAmount $params[total_fee]; // 第二步防止重复回调幂等判断 $order $orderModel-where(order_sn, $orderNo)-find(); if ($order[pay_status] 1) { return success; } // 第三步核对金额金额不一致不能入账 if (abs($order[order_amount] - $payAmount) 0.01) { Log::error(支付金额不匹配 . $orderNo); return fail; } // 第四步更新订单状态并触发商户结算事件 $orderModel-where(order_sn, $orderNo)-update([pay_status 1]); Hook::listen(order_paid, $orderNo); return success; }这段逻辑里的核心是幂等判断和金额核对。第三方支付平台在网络异常时会重复推送回调如果回调处理里没有pay_status 1的判断订单状态会被反复更新甚至可能触发两次结算事件。金额核对用的是差值比较浮点误差允许 0.01 元以内更大差异直接拒绝回调后续通过人工对账处理。4.2 商户结算与平台抽佣计算订单支付成功不等于商户马上能收到钱一般要等订单完成或确认收货后才进入可结算状态。DSmall 的结算设计里平台抽佣比例在商户表里单独配置不同商户可以使用不同费率。订单完成时触发结算计算// 订单完成生成结算明细 $orderAmount $order[order_amount]; $rate $store[commission_rate]; // 例如 0.05 表示平台抽 5% $platformCommission round($orderAmount * $rate, 2); $storeIncome $orderAmount - $platformCommission; $settlementModel-insert([ order_id $order[order_id], store_id $store[store_id], amount $storeIncome, // 商户实际应收 commission $platformCommission, // 平台佣金 status 0, // 0待结算1已结算 ]);这里容易踩的坑是用浮点数直接做减法。PHP 里0.1 0.7的结果不一定是 0.8用 round 收成两位小数可以避免大多数问题但更稳妥的方式是数据库里的金额字段统一用 DECIMAL(10,2)查询出来先转成字符串再计算或者直接用整数字段存分单位。结算状态流转需要配合商户提现功能。商户申请提现时系统先把待结算状态改成冻结然后生成提现单平台打款后记录打款凭证再改成已结算。如果直接删除待结算记录来抵扣退款后续对账和对账单会非常难查。4.3 退款场景下的资金流向退款在 B2B2C 里是最容易出财务问题的场景因为订单对应的结算可能已经完成钱已经在商户手里了。通常的处理方式是优先从该商户的待结算金额里扣减待结算不够再扣保证金。$refundAmount 200.00; // 查询该商户未结算金额总和 $pending $settlementModel -where(store_id, $storeId) -where(status, 0) -sum(amount); if ($pending $refundAmount) { // 从待结算金额中扣减 $settlementModel -where(store_id, $storeId) -where(status, 0) -decrement(amount, $refundAmount); } else { // 待结算不足差额从保证金扣 $deductFromDeposit $refundAmount - $pending; $storeModel-where(store_id, $storeId) -decrement(deposit, $deductFromDeposit); // 待结算金额全部置为 0 $settlementModel -where(store_id, $storeId) -where(status, 0) -update([amount 0]); }这段代码的逻辑是把待结算资金当作一个蓄水池退款先从池子里扣池子空了再从商户保证金里扣。如果订单已经结算完成status 已经是 1那就需要生成一条负数的结算记录或者在做提现时直接扣减后续结算款。实际项目里建议加一张 refund_record 表记录退款来源是待结算抵扣还是保证金抵扣方便财务审计。我在给几个外包项目做代码审查时发现很多所谓结算金额对不上问题都出在退款单没有关联原始结算单号导致一张订单退款两次。退款零钱计算可以直接套用这一节的处理逻辑但记得把订单状态改成已退款防止用户再次申请售后。5. 避坑指南DSmall 二次开发中的常见问题与排查这一章整理的是我实际拆过、部署过这套源码后遇到的高频问题。每一条都按现象、原因、解决三步写可以直接当成排查手册用。5.1 修改模板不生效缓存层忘记清理现象改了 front 端的 HTML 模板刷新页面还是老样式甚至 F12 里看到的 HTML 还是旧内容。原因这套系统的模板是编译执行的第一次访问时会生成编译文件存到 runtime 目录后续访问直接读编译结果。如果只改了模板没清编译缓存系统不会自动感知模板文件变更。解决删除对应模块的缓存目录。# 清理前台模块编译缓存 rm -rf runtime/home/temp/* # 如果改的是商户端模板 rm -rf runtime/shop/temp/* # 再重启 PHP-FPM service php-fpm restart如果清理之后还是旧页面检查 Nginx 是否开了 fastcgi_cache 或者页面静态化。这类缓存问题在服务器上比本地虚拟机更容易发生本地一般改完立即生效生产环境多了一层 Nginx 缓存容易让人误以为代码没改对。5.2 商品图片不显示URL 生成与伪静态冲突现象后台商品图片上传成功但前台商品详情页所有图片都是 404或者图片路径变成了 index.php 重写后的地址。原因图片地址是用 uploads 目录相对路径拼的正常情况下直接访问/uploads/goods/xxx.jpg就可以了。但我的伪静态规则里写了if (!-e $request_filename)这个判断是判断文件是否存在正常情况下真实文件不会走重写但如果目录权限不对PHP 无法判断 uploads 下文件存在就会把图片路径当成路由去解析。解决先确认 uploads 目录存在且权限为 755 或 775再在 Nginx 配置里显式增加一条静态目录排除规则。location ^~ /uploads/ { expires 7d; access_log off; }放在伪静态规则之前让 uploads 请求直接命中真实目录不进入路由重写判断。这个坑在本地开发时几乎不会出现因为本地目录权限默认都能读上线到服务器后我用普通用户跑 PHP 时经常遇到改完目录属主后重启一下 PHP-FPM 就好。5.3 结算金额对不上时间戳与查询条件不一致现象后台结算列表显示的月份汇总金额和明细订单加起来差了十几块钱。原因问题不在计算逻辑而在查询条件。结算单里的完成时间是订单确认收货时间而我排查时习惯用支付时间去筛订单。有些订单是上个月支付、这个月才确认收货结算归属月份自然不一样。后台列表筛选项和 SQL 里的时间字段不一致对账就对不平。解决统一结算查询的时间字段全部使用完成时间。SELECT store_id, SUM(commission) AS total_commission FROM ds_settlement WHERE finish_time 2025-03-01 AND finish_time 2025-04-01 GROUP BY store_id;排查时先用这个 SQL 和后台列表核对如果还是对不上再检查是否有重复的结算记录。重复结算多见于手动运行了两次订单完成事件可以通过SELECT order_id, COUNT(*) FROM ds_settlement GROUP BY order_id HAVING COUNT(*) 1查出来。5.4 插件安装后白屏权限校验与路由缓存现象后台装了一个新插件点击菜单直接白屏调试日志没有输出甚至控制台也没有报错。原因白屏大概率是路由缓存和权限节点没有同步。插件安装时往数据库插入了新菜单和权限节点但运行中的路由缓存还在内存里控制器方法被权限中间件拦截后抛出异常异常信息又被把错误显示开关关闭了所以页面直接空白。解决先清掉路由缓存再检查插件行为是否注册进 Composer 的自动加载。php think clear php think optimize:route如果清路由缓存后还是空白把配置文件里app_debug true临时打开刷新页面看具体报错。最常见的是插件目录里的类命名空间和文件路径对不上尤其自研插件容易写错 extends 路径。修好后记得把 debug 改回去。5.5 并发下单库存超卖锁的实现位置现象秒杀活动一开商品库存变成负数订单量超过库存量。原因现场代码用的是“先查询库存再扣减并判断”的流程在高并发下两个请求同时读到库存为 1然后分别执行扣减库存变成 -1。数据库层面没有约束应用层面也没有加锁。解决用数据库原子更新替代「读-判-写」三步操作。// 原子扣减库存只有库存充足时才返回影响行数 $result $goodsModel-where(goods_id, $goodsId) -where(stock, , 0) -setDec(stock, 1); if ($result 0) { throw new \Exception(库存不足); } // 扣减成功后写入订单 $orderModel-insert($orderData);setDec 在底层会生成一条UPDATE ds_goods SET stock stock - 1 WHERE goods_id ? AND stock 0的 SQL由数据库行锁保证同一时间只有一个请求能成功。这样一个商品同一秒来 100 个请求也只有 1 个能扣成功其余 99 个都会收到库存不足的异常。注意 setDec 方法返回的是影响行数不是扣减后的库存值别用返回值去展示最新库存。6. 上线前最后一小时一条命令跑完核心闭环回归代码改完、配置调完最怕的就是上线后用户下不了单或者下单了结算对不上。我每次交付这套商城源码前都会强制跑一遍核心闭环回归把本来需要手工点十几页的流程压缩成一条命令。6.1 一键回归脚本脚本做的事情很直接清缓存、登录后台、登录商户端、创建一个测试商品、模拟前端下单、触发支付回调、检查结算单金额。#!/bin/bash # dsmall_check.sh - 核心闭环回归脚本 set -e PHP/usr/bin/php BASE_URLhttp://127.0.0.1 COOKIE_FILE/tmp/dsmall_cookie.txt echo [1/6] 清理缓存 $PHP think clear echo [2/6] 平台后台登录 curl -s -c $COOKIE_FILE -X POST $BASE_URL/index.php?s/admin/login/index \ -d usernameadminpasswordtest123 -o /dev/null echo [3/6] 商户端创建测试商品 curl -s -b $COOKIE_FILE -X POST $BASE_URL/index.php?s/shop/goods/add \ -d goods_name回归测试商品goods_price0.01stock10 -o /dev/null echo [4/6] 模拟前台用户下单 ORDER_NO$(curl -s -b $COOKIE_FILE -X POST $BASE_URL/index.php?s/home/order/create | jq -r .data.order_sn) echo [5/6] 模拟支付回调 curl -s -X POST $BASE_URL/index.php?s/api/payment/notify \ -d out_trade_no$ORDER_NOtotal_fee0.01signtest -o /dev/null echo [6/6] 校验结算记录 $PHP think check --order$ORDER_NO --expect0.0095这段脚本里的订单金额 0.01 元佣金比例 5%所以期望商户结算金额是 0.0095 元。check 命令会去 ds_settlement 表里查这笔订单的记录实际金额和期望值的差值超过 0.01 就返回非零退出码脚本随即失败提醒你结算逻辑有变动。6.2 从一次结算对不上说起有一次我改完佣金计算函数单测数据全绿却忘了商户表里那个测试商户的佣金比例被手动改成了 6%回归脚本一跑立刻暴露了这个问题。从那以后我每次交付代码前都强制走一遍这个闭环回归脚本输出的每一行都仔细看一遍不是只看最后有没有成功。数据库可以回滚配置可以重来但线上用户走一遍完整流程发现结算金额不对那就不是几分钟能收拾的事了。希望帮到你。本文还有配套的精品资源点击获取