ThinkPHP多商户需子域名路由、中间件识别商户、分库分表及缓存key加shop_id前缀,避免库存错乱、越权核销等问题;模型须重写initialize()动态选库,Redis缓存强制租户隔离,支付回调须二次验商户状态。

多商户商城在 ThinkPHP 中不是开箱即用的功能,必须通过分库分表、子域名路由绑定、商户上下文隔离三者配合才能稳定运行。直接复用单商户代码改“加个 shop_id 字段”会在线上高并发时出现库存错乱、优惠券越权核销、订单归属错误等隐蔽问题。
ThinkPHP6 多商户子域名路由怎么配
核心是让 www.example.com 走总后台,shop1.example.com 和 shop2.example.com 分别命中不同商户模块。不能只靠 Route::domain() 简单绑定,必须配合中间件做运行时商户识别:
- 在
app/middleware.php中注册一个BindShopMiddleware,从子域名提取前缀(如shop1),查shops表确认存在且启用 - 把商户 ID、数据库连接配置、模板路径写入
think\Container的 request 作用域,后续模型、视图、配置都基于此动态加载 - 避免在控制器里手动
Db::connect($config)切库——这会导致事务失效、日志错乱、Redis key 冲突 - Nginx 配置需开启泛解析:
server_name ~^(?<subdomain>.+)\.example\.com$;</subdomain>,并把$subdomain透传给 PHP 的$_SERVER['HTTP_HOST']
商户数据分库后 Model 怎么自动选库
ThinkPHP 原生的 db()->connect() 不支持运行时切换且无法与模型生命周期对齐。正确做法是重写基类模型的 initialize() 方法:
- 在
app/model/BaseModel.php中,读取中间件注入的shop_id,用哈希算法映射到物理库名(如shop_db_01、shop_db_02) - 调用
$this->connection('shop_db_01')显式指定连接,而不是依赖全局配置 - 所有商户相关模型(
Goods、Order、Coupon)必须继承该基类;总后台使用的模型(如Admin、SystemConfig)则走默认库 - 注意:
belongsTo关联查询不会自动继承连接,必须显式传参:belongsTo('Shop', 'ws_shops', 'shop_id')->connection('system_db')
Redis 缓存怎么避免商户间 key 冲突
直接用 Cache::set('goods_list', $data) 会导致 A 商户刷新缓存后,B 商户读到脏数据。必须强制加入租户标识:
立即学习“PHP免费学习笔记(深入)”;
- 所有缓存 key 前缀统一为
shop:{shop_id}:,例如:shop:123:goods_list、shop:123:cart:uid_456 - 不要依赖 ThinkPHP 的
cache()助手函数——它不支持动态前缀;改用Cache::store('redis')->set("shop:{$shopId}:xxx", $value) - 购物车用 Redis Hash 存储时,field 名也需带商户 ID,否则不同商户的用户可能共用同一 hash key 导致覆盖
- 秒杀库存扣减必须用
DECR+GET原子操作,且 key 必须含shop_id和goods_id,如shop:123:seckill:789:stock
商家入驻审核流程怎么防绕过
前端隐藏“审核通过”按钮或后端只校验 status=1 是常见漏洞。真实系统必须做到三层拦截:
- 路由层:所有
/shop/xxx接口加中间件,检查当前商户status = 2(已审核)且expire_time > now() - 模型层:在
BaseModel::baseQuery()中自动追加where('shop_id', $this->shopId),防止 SQL 注入或关联误查 - 支付回调层:微信/支付宝异步通知中,必须重新查一次商户状态,不能信任请求参数里的
shop_id——攻击者可伪造 - 特别注意:OCR 营业执照识别结果必须落库并人工复核,不能仅靠前端 JS 校验就放行资质上传
最易被忽略的是事务边界——跨库操作(比如总后台冻结商户时要同时更新系统库的 shops 表和商户库的 orders 表)无法用单个数据库事务保证一致性,必须用本地消息表 + 定时补偿,而不是硬写 Db::transaction()。



















