每个做大的SaaS产品都会撞上同一堵墙:客户提了个专属需求,你做了,代码库里就多了一段只为一家租户跑的逻辑。一年后,这样的"特例"攒了十几个。代码里到处是判断租户ID的if语句,测试套件要模拟各种客户专属分支,唯一清楚哪段代码归哪个客户的资深工程师成了重构工作的瓶颈。

有没有更好的办法?有。而且不用放弃按客户定制的灵活性。关键是把"能做什么"的代码和"选哪个做"的数据彻底分开。这篇文章讲怎么做、数据存哪儿、以及一个致命的安全陷阱——如果你让数据变成代码,就会掉下悬崖。

打开网易新闻 查看精彩图片

先说什么不能做。几种常见方案各有硬伤:

打开网易新闻 查看精彩图片

第一,给每个客户单独部署实例。这等于把运营面也分叉了:N套数据库、N组后台任务、N条部署流水线、N个版本的bug修复要滚动。两三个客户还能撑,到十个就崩。

第二,后端写条件代码——if tenant_id == "acme": ...。第一天便宜,第六个月就扛不住。每个开发者都得摸清客户版图才能安全改代码,每次重构风险跟租户分支数成正比,客户专属逻辑像毛细血管一样蔓延。

第三,构建时注入代码。靠配置生成不同二进制文件,运营成本和单独实例一样高,还得享受"调试行为取决于编译时flag"的额外乐趣。别这么干。

能扩展的模式是:一个代码库、一个运行集群、一条部署流水线——让租户专属行为活在代码查询的数据里。说白了,这就是多租户架构。

具体怎么做?先定位系统里行为能按租户变化的地方:折扣引擎、审批流、导出格式、通知规则。这些叫扩展点。在每个扩展点,代码定义一小套已知行为,租户数据负责选哪个行为、传什么参数。

典型的实现是类层次结构。比如一个CustomRule基类,约定两个方法——applies?(context)判断是否适用,apply(context)执行逻辑。然后是一组具体实现:

class CustomRule:
def applies(self, context) -> bool: ...
def apply(self, context) -> None: ...

打开网易新闻 查看精彩图片

class PercentageDiscountRule(CustomRule):
def __init__(self, percent, min_order):
self.percent = percent
self.min_order = min_order
def applies(self, context):
return context.order_total >= self.min_order
def apply(self, context):
context.discount += context.order_total * (self.percent / 100)

class FirstPurchaseDiscountRule(CustomRule): ...

代码只定义可能性,数据决定选哪个。租户配置表里存的是规则类型和参数,比如{"rule_type": "PercentageDiscountRule", "percent": 10, "min_order": 100}。运行时加载、实例化、执行。

数据存在哪?数据库就行,但要考虑缓存和版本控制。规则变更需要审计日志,回滚要快速。别把规则塞进应用配置——那是代码,不是数据。

最后那个安全悬崖:如果让数据变成代码,你就完了。永远不要eval()租户输入,不要动态加载租户上传的模块,不要把规则写成自由形式的脚本。数据只能是数据,结构化的、可验证的、沙箱化的。一旦数据能执行任意逻辑,多租户的安全模型就塌了。

这套模式的核心就一句话:代码是通用的,数据是专属的。守住这条线,一个代码库服务一千个客户,不是神话。