开发文档

鲜花交易系统与鲜花交易平台(PHP / ThinkPHP 版)的研发记录:功能上线、架构演进与实施经验,按发布日期持续更新。

一套代码,三类用户:厂商端、平台端、租户端怎么分离

三种身份,三种后台

先分清三类用户:

身份是谁关心什么
厂商(vendor)卖这套软件的我们授权、客户上线情况、版本分发
平台(platform)买了 SaaS 的运营公司租户开通、平台级配置、跨租户报表
租户(tenant)平台下的商户自己的商品、订单、会员、财务

三者共用同一套业务能力(商品、订单、结算),但入口与可见范围完全不同。最常见的事故是:租户超管登录后,在菜单里看到了"租户管理"甚至"授权中心"。

为什么不拆成三个代码库

拆库看起来干净,但代价是:

  • 商品、订单、结算这些核心逻辑要复制三份,改一次要改三处;
  • 权限模型要维护三套;
  • 数据库结构要处理三套演进。

所以选择一份代码 + 三个入口 + 一层闸门:

  • 后端按 Host 解析当前身份与租户,注入上下文;
  • 前端按身份构建不同的产物(平台端 / 租户端各自一套 dist);
  • 所有平台专属接口加统一闸门,租户上下文调用直接拒绝。

入口与构建产物分离

前端视图目录按三份组织:

  • common:登录、个人中心、错误页等共用页面;
  • platform:平台运营专属(租户管理、平台配置);
  • tenant:商户业务(商品、订单、库存、财务)。

构建时按入口产出两套静态资源,分别部署到平台域名与租户域名。这样即使租户端被反编译,也拿不到平台端的页面代码——物理分离比运行时判断更可靠。

租户端还支持自有域名:客户希望用自己的域名对外,所以租户解析不能只靠子域,还要支持"域名 → 租户"的映射表。

闸门:默认拒绝

平台专属功能在三个层面各拦一次:

  1. 菜单装配:按身份过滤,租户上下文不出现平台菜单;
  2. 接口中间件:命中平台专属路由且当前是租户上下文 → 直接返回无权限;
  3. 页面守卫:前端路由守卫再判一次,防止直接输 URL。

为什么三层都要做?因为前两层都可能被绕过或漏配:菜单靠数据(漏插一条就露出来)、接口靠注解(漏标一个就放行)。三层叠加后,漏一个也不至于越权。

一个真实教训

上线初期,租户超管确实能看到平台功能页面。根因是"菜单可见性只按角色权限判断,没判断身份"——租户超管在自己的租户里是超级管理员,天然拥有全部角色权限,于是平台菜单也被判定为可见。

修法就是上面那套闸门:把"身份"作为独立维度参与判断,而不是复用角色权限。这条经验后来也用在授权受限模式上(见另一篇)。

一套代码的边界在哪

一份代码服务三类用户,代价是复杂性集中在"上下文解析"上:每个请求都要知道"我现在是谁、属于哪个租户"。

我们的约束是:

  • 上下文由中间件统一注入,业务代码不自己解析;
  • 数据访问层统一加租户条件,禁止业务代码手写租户过滤(否则迟早漏一处);
  • 平台跨租户查询走只读视图,不允许平台端直接改租户数据。

第三条很关键:允许平台端随意改租户数据,等于给了一个绕过租户隔离的后门,一旦出问题就是数据归属纠纷。

小结

三端分离的本质不是三套界面,而是三种可见范围。入口、构建产物、权限闸门、数据访问四个层面同时收口,才能做到"共用一套业务逻辑,却互不越界"。

本文属于「开发文档」系列,记录鲜花交易系统(PHP + ThinkPHP 8 + MySQL 8)的实际研发进展。想了解功能清单与报价,可前往系统报价或联系我们。