怎么用 这是格式与深度的参考,不是让你照抄的模板。数字全部是示例,直接抄进简历一问就穿——必须换成你自己的真实数据,而且要能说清是怎么测出来的。下面每个模块都有「数字是怎么测的」和「面试追问」两段,写不出那两段的模块就不要往简历上写

面试题库(电商 / 短视频 / 社区 / 本地生活 / 旅游 / 企业服务 多业务场景)

实习级这一档是干什么的 多租户数据隔离方案、权限模型设计、工作流引擎这些核心台面,实习生大概率碰不到。这一档收的是租户与成员管理里实习生真的会被分派、但仍然有明确技术含量的活。下面三个模块彼此独立——只做过一个就只写一个。
怎么讲才不显得 low 「我做了个邀请成员,发个链接过去点了就加入」和「席位是租户级配额,管理员买了 10 个席位可以发 15 份邀请,如果 3 个人同时点激活就会超出配额;而且邀请令牌不能用自增标识,那样能被枚举出别人的邀请」——同一件事,后者面试官会顺着追问。这一档的破解办法是每件事都问一句「这是全局的还是每个租户一份的」:初始化数据、席位配额、通知模板,答案都是每租户一份,而漏掉租户维度的地方就是数据串租户的地方
三条自检 一、能说出不这么做会怎样(开通步骤多不做可重入会留下半套数据、席位不做并发控制会超配额、模板没有回退链渲染失败就发不出通知);二、能说出你踩过的具体坑;三、能说出量级(初始化多少步、多少租户、每天多少封邀请与通知)。三条都有就能写。
项目背景设定 面向企业的 SaaS 产品后端,Spring Boot + MySQL + Redis + 消息队列,多租户共享库、按租户标识隔离。我负责的是试用开通、成员邀请、通知模板这三块「在主业务功能之外、但每个新客户都必然会走一遍」的功能。
为什么这三块值得写 它们共同的技术主题是多租户下的「每租户一份」与「一次性动作被重复触发」:开通要创建一整套初始数据且步骤可能部分失败、席位是租户级配额而激活是并发的、通知模板需要租户覆盖与回退链。这三类问题在单租户或单人测试时都不出现——它们只在真实的并发、失败、以及租户数量增长之后暴露。

模块一:试用租户开通与初始化

  1. 试用租户开通与初始化(多步初始化编排为可重入任务 + 步骤级幂等与断点续做 + 初始化数据模板化 + 试用到期的自动降级)★★★
    简历这样写 试用租户开通与初始化(Spring Boot + 任务编排 + 步骤幂等 + 定时扫描):开通需创建十余项初始数据(租户记录、默认角色与权限、组织结构、示例数据、通知配置等),原实现为单个事务内串行执行,任一步失败即整体回滚但外部调用无法回滚,导致部分租户处于半初始化状态且无法重试;改为拆分为可重入的步骤任务并记录每步完成状态,失败后从断点续做而非整体重来,每步各自幂等;初始数据抽为可配置模板(按行业提供不同预置内容),新增预置项不再改代码;同一企业重复提交开通改为按唯一标识幂等返回已有租户,不再产生重复租户;试用期到期原无任何处理,新增到期前提醒与到期后自动降级(限制写入但保留数据与导出能力)。改造后开通失败可自动恢复,人工介入的开通工单大幅减少。
    展开完整拆解
    为什么要这么设计

    开通一个试用租户,需要做的事比我最初预想的多得多:建租户记录、建默认角色和权限、建一套组织结构、灌一批示例数据、初始化通知配置、调计费系统登记、发欢迎邮件——十几项。我第一版把它们全部放在一个事务里串行执行。四个问题。

    一是事务里有外部调用,回滚回滚不掉。调计费系统登记、发邮件这些是外部动作。某一步失败,本地数据库回滚了,但计费系统那边已经登记了、邮件已经发出去了用户收到了「开通成功」的邮件,登录进来发现什么都没有。

    二是失败之后无法重试。整体回滚意味着「什么都没留下」听起来很干净,但实际上外部调用留下了痕迹,而本地又没有任何记录说明「这个租户尝试开通过」用户重新提交,唯一性校验又撞上了外部系统里已存在的记录——彻底卡住,只能人工介入。

    三是初始数据写死在代码里。示例数据、默认角色名称、预置的通知配置,全部是代码里的常量。运营想给不同行业的客户配不同的示例数据,每次都要改代码发版。

    四是试用到期完全没处理。试用期字段存了,但没有任何逻辑检查它结果是一批试用租户过期几个月了还在正常使用全部功能——而这直接影响转付费的动力。这和「到期这件事没有任何用户操作会触发它」是同一类问题。

    所以四个改动:拆成可重入的步骤任务并记录每步完成状态、失败从断点续做每步各自幂等初始数据抽成可配置模板同一企业重复提交幂等返回已有租户,另外补了到期前提醒与到期后自动降级

    这个模块最想说的一句话是:一个包含外部调用的多步流程,用「一个大事务 + 全部回滚」来保证一致性是行不通的,因为你回滚不了外部系统。正确的思路是把它当成一个可以「做到一半、下次接着做」的任务——记录每一步的完成状态、每一步都幂等、失败了从断点继续我最初想用事务解决所有问题,是因为我把它当成了一次数据库写入,而它其实是一个流程。

    整体链路
    提交开通(入口幂等) │ ├─ 按企业唯一标识(统一社会信用代码等)查重 │ 已存在 → 幂等返回已有租户,不新建 │ 不做的后果:同一企业重复提交产生多个租户,数据分散 │ └─ 创建「开通任务」记录 → 返回任务号 → 异步执行 同步执行的问题:十几步耗时长,接口超时且中断无记录 任务编排(拆成可重入的步骤) │ ├─ 步骤清单显式声明并记录状态 │ 建租户记录 → 建默认角色与权限 → 建组织结构 │ → 灌示例数据 → 初始化通知配置 → 登记计费 │ → 发欢迎邮件 → 标记开通完成 │ ├─ 每步执行前先查该步是否已完成(已完成则跳过) │ 这是「断点续做」的实现方式 │ ├─ 每步内部各自幂等(重复执行不产生重复数据) │ 靠唯一约束或先查后写 + 唯一索引兜底 │ ├─ 每步失败 → 记录失败原因 → 有限重试(退避)→ 仍失败则告警 │ 不要整体回滚 —— 外部调用回滚不了,回滚只会丢掉已完成的部分 │ └─ 支持人工触发重跑(从失败步骤继续) 没有这个入口的话,个例失败只能等发版修 为什么不能用一个大事务 │ ├─ 事务里有外部调用(计费登记 · 发邮件) │ 本地回滚了,外部已经产生了副作用 │ 用户收到「开通成功」邮件,登录进来什么都没有 │ ├─ 整体回滚后本地没有任何记录说明「尝试开通过」 │ 用户重新提交 → 又撞上外部系统里已存在的记录 → 彻底卡住 │ └─ 十几步放一个事务里,事务持续时间过长,占用连接与锁 初始数据模板化 │ ├─ 示例数据 · 默认角色名 · 预置通知配置 → 抽成配置模板 │ ├─ 按行业提供不同模板(零售 · 制造 · 服务业的示例数据不同) │ ├─ 模板变更不影响已开通租户(只对新开通生效) │ 影响已有租户的后果:他们自己改过的配置被覆盖 │ └─ 模板本身要有版本号并记录到租户上 排查时要知道「这个租户是用哪个版本的模板初始化的」 试用到期(没有用户操作会触发它) │ ├─ 定时任务扫描即将到期与已到期的试用租户 │ ├─ 到期前分级提醒(提前较长时间 · 临近 · 到期当天) │ 提醒要发给管理员,并在产品内顶部横幅提示 │ ├─ 到期后自动降级而不是停用 │ 限制写入与新增,但保留登录 · 查看 · 导出 │ 直接停用的后果:客户数据拿不出来,转化机会也没了 │ └─ 转付费后恢复要幂等(可能被多次触发) 可观测 ├─ 开通任务的成功率与各步骤失败分布 ├─ 停在中间状态的任务数(应长期为零) └─ 即将到期与已到期未处理的租户数
    分步拆解
    1. 入口先按企业唯一标识查重并幂等返回。不做的后果是同一企业重复提交产生多个租户,数据分散在几个租户下,后续合并极其麻烦。
    2. 开通必须异步执行并返回任务号。十几步耗时长,同步执行会接口超时,而超时后中断没有任何记录。
    3. 步骤清单要显式声明并逐步记录完成状态。这是「断点续做」的基础——没有状态记录就不知道从哪继续。
    4. 每步执行前先检查是否已完成,已完成则跳过。这样重跑任务是安全的,不会重复创建数据。
    5. 每步内部还要各自幂等。因为「检查已完成」和「执行」之间可能有并发,靠唯一约束兜底最可靠。
    6. 不要整体回滚。事务里有外部调用(计费登记、发邮件),本地回滚了外部已经产生副作用——用户收到「开通成功」邮件、登录进来什么都没有。
    7. 失败要记录原因并有限重试(退避),仍失败则告警。无限重试会积压,而没有告警就没人知道有租户卡住了。
    8. 必须有人工触发重跑的入口(从失败步骤继续)。没有这个入口的话个例失败只能等下次发版修——而客户在等着用。
    9. 初始数据要抽成可配置模板。写死在代码里的话,运营想给不同行业配不同示例数据每次都要改代码发版。
    10. 模板变更只对新开通生效,不影响已有租户。影响已有租户的后果是他们自己改过的配置被覆盖——这是很严重的数据事故。
    11. 模板要有版本号并记录到租户上。排查问题时要知道「这个租户是用哪个版本的模板初始化的」。
    12. 试用到期必须靠定时任务扫描。存了字段却不检查的话,一批试用租户过期几个月还在正常使用全部功能——直接影响转付费动力。
    13. 到期前要分级提醒,并发给管理员加产品内横幅。只发一次邮件容易被忽略,而产品内横幅是他每天都会看到的。
    14. 到期后自动降级而不是停用。限制写入但保留登录、查看、导出。直接停用的后果是客户数据拿不出来,转化机会也一起没了——而我们的目标是让他转付费,不是惩罚他。
    15. 转付费后的恢复要幂等。可能被多次触发(重试、人工操作),不幂等会重复调整配额或重复发通知。
    16. 要监控「停在中间状态的任务数」。这个数字应长期为零,不为零就说明有租户卡在半初始化状态而没人发现。
    关键决策与取舍

    放弃「一个大事务 + 全部回滚」,改成「可重入的步骤任务」。大事务的吸引力在于「要么全成功要么什么都没发生」,但这个承诺在有外部调用时根本兑现不了——你回滚不了别人的系统而且整体回滚之后本地没有任何记录说明「尝试开通过」,用户重新提交又会撞上外部系统里已存在的记录,彻底卡住。可重入任务的代价是要维护步骤状态、每步都要幂等、可能存在「做到一半」的中间状态但中间状态是可恢复的,而「半个回滚」是不可恢复的。判据是「这个流程里有没有你无法回滚的动作」:有,就不能靠事务。

    初始数据模板变更只对新租户生效,不追溯已有租户。追溯能让所有租户都拿到最新的预置内容,但它会覆盖客户自己改过的配置——那是数据事故判据是「这份数据在创建之后是否属于客户」:属于客户的东西我们不能单方面改。如果确实需要给已有租户补充新的预置项,那应该是一个显式的、可选择的「导入」动作,而不是模板变更的自动效果。

    到期后选降级而不是停用。停用最能推动转化(用不了就得付钱),但它把客户的数据也一起锁住了——而数据是客户的,扣着不给违背基本的信任;而且客户拿不到数据的第一反应是投诉和差评,转化机会反而没了降级(限制写入、保留查看与导出)的判据是:我们的目标是让他转付费,不是惩罚他。

    踩过的坑一:事务里有外部调用,回滚之后外部副作用留下了。用户收到了「开通成功」的欢迎邮件,登录进来发现什么都没有,他的第一反应是「你们系统坏了」。教训是:事务边界要和「可回滚的范围」对齐——把不可回滚的动作放进事务,事务的保证就是假的。我当时的思路是「用事务把所有步骤包起来最安全」,完全没有区分哪些步骤是可回滚的。

    踩过的坑二:整体回滚导致用户彻底卡住,只能人工介入。本地什么都没留,但外部系统已经有记录了,用户重新提交时唯一性校验直接失败教训是:失败之后「什么都不留」并不等于「回到了初始状态」——只有当所有副作用都可回滚时这句话才成立。更实用的做法是留下失败记录,让它成为一个可以继续推进的状态。

    踩过的坑三:试用到期字段存了但没有任何逻辑检查它。一批租户过期几个月还在用全部功能。这和资质有效期那类问题完全同源:到点该变的状态,没有任何用户操作会触发它我后来把这一条固化成了自检:凡是数据里有「到期时间」「有效期」这类字段,就必须回答「谁来检查它、检查到了做什么」。

    没做的部分:没做开通流程的可视化编排(让运营自己配步骤顺序)。它听起来灵活,但步骤之间有依赖(角色必须在组织结构之前建),让非开发配置顺序风险很高我做的是「步骤清单在代码里显式声明、但每步的内容用模板配置」——把变化频繁的部分开放,把有依赖关系的部分固定。

    数字是怎么测的

    核心指标是「需要人工介入的开通工单数」。它直接对应「失败无法重试」这个原始问题。报法是改造前后的对比,并说明统计区间与总开通量(分母很重要,开通量本身在增长)。

    「停在中间状态的任务数」要作为长期监控指标报出来。它应该长期为零;报法是「上线后 N 天内该指标为零,期间有 M 次步骤失败均被自动重试恢复」——后半句才是这个设计真正的价值。

    断点续做要用断言型用例:让第五步失败,断言前四步的数据保留、重跑时从第五步开始、前四步不被重复执行「前四步不被重复执行」这一条最关键,要靠数据条数断言。

    步骤幂等:对每一步单独重复执行,断言不产生重复数据。这是一组机械但必要的用例。

    入口幂等:并发提交同一企业标识的开通请求,断言只产生一个租户、其余请求返回同一个租户。要真并发测。

    模板不追溯的验证:修改初始数据模板后,断言已开通租户的配置不变、新开通的租户使用新模板。这条防的是数据事故。

    到期处理:用可控时间点断言到期前触发提醒、到期后限制写入但仍可登录查看导出;转付费后恢复且重复触发不产生副作用。

    不要报什么:不要报「开通成功率 100%」——外部系统会不可用。该报的是「人工介入工单数下降与总开通量」「中间状态任务数为零且失败被自动恢复的次数」「第 N 步失败后重跑不重复前面步骤」「模板变更不影响已有租户」这几件可核对的事。

    面试追问
    Q:开通租户这么多步骤,用一个事务包起来保证原子性不是最简单最安全吗? A:我第一版就是这么做的,而这个「安全」是假的——因为事务里有外部调用,你回滚不了别人的系统。具体的事故是:某一步失败,本地数据库回滚了,但计费系统那边已经登记了、欢迎邮件已经发出去了用户收到「开通成功」的邮件,登录进来发现什么都没有,他的第一反应是「你们系统坏了」。更麻烦的是第二个后果:整体回滚之后,本地没有任何记录说明「这个企业尝试开通过」,用户重新提交时,唯一性校验又撞上外部系统里已存在的记录——彻底卡住,只能人工介入所以「失败之后什么都不留」并不等于「回到了初始状态」,只有当所有副作用都可回滚时这句话才成立。正确的思路是把它当成一个流程而不是一次写入:拆成显式声明的步骤清单、逐步记录完成状态、每步执行前先检查是否已完成(已完成则跳过)、每步内部各自幂等,失败了从断点续做而不是整体重来,并且提供人工触发重跑的入口。代价是会存在「做到一半」的中间状态——但中间状态是可恢复的,而「半个回滚」是不可恢复的我的判据是:这个流程里有没有你无法回滚的动作?有,就不能靠事务,而要靠可重入。另外十几步放一个事务里事务持续时间也过长,会长时间占用连接和锁。
    Q:初始化数据的模板改了,为什么不同步给已经开通的租户?他们不是也能受益吗? A:因为那些数据在创建之后就属于客户了,我们不能单方面改。举个具体的例子:模板里有一个默认角色叫「普通成员」,客户开通之后把它改名成了自己公司的叫法、还调整了权限。如果模板更新时追溯覆盖,他的改动就被冲掉了——这是数据事故,而且很难发现(他可能过几天才注意到权限不对)。示例数据同理,客户可能已经在示例数据基础上改成了自己的真实数据。所以我的判据是「这份数据在创建之后是否属于客户」:属于客户的,模板变更只对新开通生效。那如果确实需要给已有租户补充新的预置项怎么办?我的答案是做成一个显式的、由客户自己选择的「导入」动作——比如「我们新增了一批行业模板,是否导入」,而不是让它成为模板变更的自动效果。这样主动权在客户手上。另外还有一个配套的设计:模板本身要有版本号并记录到租户上——排查问题时要能知道「这个租户是用哪个版本的模板初始化的」,否则客户说「我这里默认角色和文档不一样」,你根本查不出来是模板变过还是他自己改过。这一条其实和退改规则快照是同一个思路:凡是「在某个时刻生成、之后归属于用户」的数据,都要记录当时用的是哪个版本的规则或模板。

模块二:成员邀请与激活

  1. 成员邀请与激活(令牌随机不可枚举且单次使用 + 席位配额的并发占用控制 + 已有账号与跨租户身份的处理 + 邀请撤回与失效联动)★★★
    简历这样写 成员邀请与激活(Spring Boot + Redis 原子操作 + 随机令牌 + 状态机):邀请链接原使用可枚举的自增标识,改为足够长度的随机令牌并只存其哈希,配合有效期与单次使用(激活即失效,避免链接被转发后重复加入);席位为租户级配额,管理员可发出多于剩余席位的邀请,原实现「先查剩余再插入成员」在并发激活时会超出配额,改为激活时原子占用席位,失败则明确提示席位不足而非静默入库;补充已有账号与跨租户身份的处理(被邀请邮箱已注册则关联已有账号而非重复建号,同一人可属于多个租户,登录后需选择当前租户);邀请支持撤回并即时失效,成员被移除时其未使用邀请一并失效;邮箱比较统一做规范化(大小写与首尾空白),避免同一人被邀请两次。改造后席位超配与重复账号问题不再出现。
    展开完整拆解
    为什么要这么设计

    邀请成员这个功能第一版做得很快:管理员填邮箱,系统插一条邀请记录,把记录的自增标识拼到链接里发出去,对方点开链接就加入租户四个问题,其中两个是安全和正确性问题。

    一是邀请链接可以被枚举。我用的是邀请记录的自增标识。把链接里的数字改一改就能拿到别人的邀请——而邀请一旦被使用,那个人就加入了别的公司的租户,能看到里面的数据。这是安全同学在评审时指出的,幸好没上线。

    二是席位会超出配额。席位是租户级的配额(比如买了 10 个)。管理员可以发出多于剩余席位的邀请(这本身是合理的,因为不是每个人都会激活)。我的实现是激活时「先查剩余席位、够就插入成员」。三个人同时点激活,三个请求都查到「还剩 1 个」,然后都插入成功——租户变成了 12 个成员而只买了 10 个席位。

    三是被邀请人已有账号的情况完全没考虑。SaaS 产品里同一个人可能同时属于多个租户(比如外部顾问)。我的实现是激活时直接建一个新账号——结果是同一个邮箱有了两个账号,两边的密码、设置、历史都不通。用户很困惑。

    四是邀请撤不回来。管理员发错了邮箱、或者对方还没入职就先离职了,邀请链接依然有效更严重的是成员被移除之后,他之前收到的那封邀请邮件还能用——点一下又回来了。

    所以四个改动:令牌改成足够长度的随机值并只存哈希、加有效期与单次使用席位改成激活时原子占用处理已有账号与跨租户身份支持撤回并与成员移除联动失效,另外补了邮箱规范化比较

    这个模块最想说的一句话是:凡是「发给用户、由用户点回来」的链接,都要当成不可信输入来设计——它会被转发、被截图、被枚举、被重复点击。我最初把邀请链接当成了「一个指向数据库记录的引用」,而它实际上是一个凭据凭据的四个基本要求是:不可猜测、有有效期、只能用一次、可以被撤销——我一个都没做。

    整体链路
    令牌设计(邀请链接是凭据,不是记录引用) │ ├─ 足够长度的随机值,不使用自增标识或可推导的值 │ 自增标识的后果:改一改数字就能拿到别人的邀请 │ 而使用了别人的邀请就会加入别的公司租户、看到其数据 │ ├─ 库里只存令牌的哈希,不存原文 │ 拖库时无法直接得到可用的链接 │ ├─ 有效期(较短,过期需重新邀请) │ ├─ 单次使用:激活成功即失效 │ 不做的后果:链接被转发后可以被多人重复使用 │ └─ 可撤销:撤回后立即失效 凭据的四个基本要求:不可猜 · 有期限 · 用一次 · 可撤销 发出邀请 │ ├─ 邮箱规范化后比较(大小写统一 · 去首尾空白) │ 不规范化的后果:同一人被邀请两次,占两份记录 │ ├─ 已是本租户成员 → 直接提示,不发邀请 │ ├─ 已有未过期邀请 → 提示已邀请,可选择重发(复用或换新令牌) │ ├─ 允许发出多于剩余席位的邀请(合理,因为不是每个人都会激活) │ 但要提示「当前剩余席位 N,已发出待激活邀请 M」 │ 不提示的后果:管理员不知道会超,激活时才发现 │ └─ 记录邀请人 · 时间 · 预分配角色 激活(席位并发是这里的核心问题) │ ├─ 校验令牌:存在 · 未过期 · 未使用 · 未撤回 │ ├─ 原子占用席位(关键一步) │ 原实现「先查剩余席位、够就插入成员」 │ 三人同时激活 → 都查到「还剩 1 个」 → 都插入成功 → 超配额 │ 改为原子扣减(计数原子操作 或 数据库唯一约束 + 条件更新) │ ├─ 占用失败 → 明确提示「席位不足,请联系管理员」 │ 静默入库或静默失败都不行 —— 前者超配额,后者用户不知道为什么 │ ├─ 账号处理分三种情况 │ 邮箱未注册 → 引导注册并关联到该租户 │ 邮箱已注册 → 关联已有账号,不重复建号 │ 已是其他租户成员 → 追加租户归属,登录后可切换 │ ├─ 分配预设角色 → 标记令牌已使用 → 通知邀请人 │ └─ 全流程幂等:重复点击链接不产生两个成员 用户会因为页面没反应而重复点 跨租户身份(SaaS 特有) ├─ 同一账号可属于多个租户,登录后需选择或记住上次的租户 ├─ 所有数据访问都必须带当前租户上下文 └─ 漏掉租户上下文的地方就是数据串租户的地方 撤回与联动失效 ├─ 撤回邀请 → 令牌立即失效(撤回原因留档) ├─ 成员被移除 → 其名下未使用的邀请一并失效 │ 不联动的后果:他点一下旧邀请邮件又回来了 ├─ 邀请人离职 → 其发出的待激活邀请要复核(可配置策略) └─ 席位缩减 → 超出部分不自动踢人,提示管理员处理 可观测 ├─ 邀请发出 · 激活 · 过期 · 撤回的漏斗 ├─ 激活失败原因分布(席位不足 · 过期 · 已使用) └─ 席位使用数与配额的对账(应始终不超)
    分步拆解
    1. 邀请令牌必须是足够长度的随机值,不能用自增标识。自增标识的后果是改一改数字就能拿到别人的邀请,用了它就会加入别的公司租户、看到其数据。
    2. 库里只存令牌哈希,不存原文。拖库时无法直接得到可用的链接。
    3. 令牌要有有效期。长期有效的链接会在邮箱里躺很久,而它的持有者可能已经变了(共享邮箱、离职交接)。
    4. 令牌必须单次使用,激活成功即失效。不做的后果是链接被转发后可以被多人重复使用。
    5. 邮箱要规范化后比较(大小写统一、去首尾空白)。不规范化的后果是同一人被邀请两次,占两份记录。
    6. 已是本租户成员的直接提示,不发邀请。避免无意义的邮件和困惑。
    7. 允许发出多于剩余席位的邀请,但要提示当前席位状况。「剩余 N 个席位,已发出 M 份待激活邀请」——不提示的话管理员不知道会超,激活时才发现。
    8. 激活时必须原子占用席位,不能「先查再插」。原实现三个人同时激活,三个请求都查到「还剩 1 个」然后都插入成功,租户变成 12 个成员而只买了 10 个席位。
    9. 席位占用失败要明确提示「席位不足,请联系管理员」。静默入库会超配额,静默失败则用户不知道为什么加不进去。
    10. 被邀请邮箱已注册时要关联已有账号,不能重复建号。重复建号的后果是同一邮箱两个账号,密码、设置、历史都不通,用户很困惑。
    11. 要支持同一账号属于多个租户。SaaS 里外部顾问、集团多子公司都是真实场景,登录后需要选择或记住上次的租户。
    12. 所有数据访问都必须带当前租户上下文。漏掉租户上下文的地方就是数据串租户的地方——这是多租户里最严重的一类问题。
    13. 激活全流程要幂等。用户会因为页面没反应而重复点击链接,不幂等会产生两个成员或占两个席位。
    14. 邀请要支持撤回并立即失效,撤回原因留档。管理员会发错邮箱。
    15. 成员被移除时,其名下未使用的邀请要一并失效。不联动的后果是他点一下旧邀请邮件又回来了——这是一个真实的权限漏洞。
    16. 席位缩减时不要自动踢人,提示管理员处理。自动踢人会踢掉正在工作的成员,而「踢谁」是管理员的决定不是系统的。
    17. 要对账「席位使用数与配额」。这个数字应始终不超,一旦超了就说明并发控制有漏洞。
    18. 激活失败原因要分类统计(席位不足、过期、已使用)。「席位不足」占比高说明要提醒管理员扩容,「过期」占比高说明有效期设短了。
    关键决策与取舍

    席位并发选「原子占用」而不是「加分布式锁」。分布式锁也能解决,但它的粒度是整个租户的激活操作(同一租户的激活会串行),而且要处理锁超时、锁失效这些复杂情况。原子占用(计数的原子操作,或数据库层的条件更新加唯一约束)更简单也更可靠。判据是「这个并发冲突的范围有多大」——只是一个计数的争用,用原子操作就够,不需要上锁。

    允许发出超过剩余席位的邀请,而不是发邀请时就占席位。发邀请时占席位能彻底避免超配额,但实际情况是很多邀请永远不会被激活(人换了、忘了、不需要了),提前占席位会白白浪费配额,管理员还要手动去清理。所以我选择「发邀请不占、激活时才占」,代价是激活时可能失败缓解办法是发邀请时明确提示席位状况——让管理员知道会不会超。判据是「哪种失败对用户的伤害更小」:白占席位是持续的浪费,激活失败是一次性的、且有明确提示和解决路径。

    同一账号可属于多个租户,而不是「一个邮箱一个租户」。后者实现简单得多(不需要租户切换、不需要处理归属追加)。但外部顾问、集团多子公司、服务商这些场景是真实存在的,强制一个邮箱一个租户会逼用户注册小号——而小号带来的问题(找不回、权限混乱)比支持多租户归属更麻烦。代价是所有数据访问都必须带租户上下文,漏一处就是数据串租户,所以这一条必须收敛到统一的上下文获取入口,不能靠每个开发记得传。

    踩过的坑一:邀请链接用了自增标识。是安全同学在设计评审时指出的——改一改链接里的数字就能拿到别人的邀请,用了它就会加入别的公司的租户、看到里面的数据教训是:我把邀请链接当成了「一个指向数据库记录的引用」,而它实际上是一个凭据凭据的四个基本要求是不可猜测、有有效期、只能用一次、可以被撤销——我一个都没做。幸好是在评审阶段被发现的。

    踩过的坑二:席位「先查再插」,并发激活导致超配额。三个人同时点激活,都查到「还剩 1 个」然后都成功了教训是:任何「检查配额然后消耗配额」的逻辑,检查和消耗必须是一个原子操作——这一类问题(库存、席位、优惠券、限流)的形态完全一样,而「先查再写」在单人测试时永远正确。

    踩过的坑三:成员被移除后,他手里的旧邀请邮件还能用,点一下就回来了。是一个客户的管理员发现并反馈的——他移除了一个离职员工,几天后发现这个人又出现在成员列表里教训是:撤销权限的操作要把「所有能重新获得权限的路径」都一起关掉——移除成员只关了「现有身份」,没关「邀请这条入口」。这和「拉黑要双向、要覆盖所有入口」是同一类思维:一个权限相关的状态变更,要列出它的全部影响面。

    没做的部分:没做基于企业域名的自动加入(同域名邮箱注册即自动加入该租户)。它能大幅降低邀请成本,但风险很高——共享邮箱域名(公共邮箱后缀)、外包人员用了企业邮箱、以及域名所有权验证都要处理。我提了方案但结论是「必须先有域名所有权验证能力,否则这个功能会成为一个越权入口」。

    数字是怎么测的

    席位并发必须真并发压测:剩余 1 个席位,并发发起 5 个激活请求,断言只有 1 个成功、其余 4 个收到「席位不足」的明确提示,且最终成员数不超过配额这是这个模块最重要的一条用例,顺序调用测不出来。

    席位对账要作为长期监控指标:报「席位使用数与配额的对账差异长期为零」。一旦不为零就说明并发控制有漏洞,这个监控比一次性测试更有价值。

    令牌安全性可以报设计参数而不是测试结果:令牌长度、随机来源、存储方式(只存哈希)、有效期。这些是可核对的事实;同时断言「用被撤回或已使用的令牌激活会被拒绝」。

    单次使用:同一令牌并发激活两次,断言只成功一次;成功后再次访问链接,断言提示已使用。

    账号处理三种情况各写一条用例:未注册邮箱、已注册邮箱、已是其他租户成员,断言分别是引导注册、关联已有账号、追加租户归属而不重复建号第二条是踩坑的直接回归。

    联动失效:移除成员后,断言其名下未使用的邀请全部失效、用旧链接无法重新加入。这条是权限漏洞的回归。

    邮箱规范化:用大小写不同、带首尾空白的同一邮箱重复邀请,断言被识别为同一人。

    漏斗数据:报邀请发出、激活、过期、撤回的转化,并按失败原因分类。这些数字用于调整有效期和席位提醒策略。

    不要报什么:不要报「邀请激活率提升」——它主要取决于客户内部的推动力度。该报的是「剩余 1 席并发 5 个激活只成功 1 个」「席位对账差异长期为零」「令牌长度与存储方式及撤回后即失效」「三种账号情况分别正确处理」「移除成员后旧邀请失效」这几件可核对的事。

    面试追问
    Q:邀请链接直接把邀请记录的 ID 放进去有什么问题? A:问题很严重——把链接里的数字改一改就能拿到别人的邀请,而使用了别人的邀请就会加入别的公司的租户、看到里面的数据。这是安全同学在设计评审时指出的,幸好没上线。根本原因是我把邀请链接当成了「一个指向数据库记录的引用」,而它实际上是一个凭据。凭据有四个基本要求,我当时一个都没做到:不可猜测(改成足够长度的随机值,而不是自增标识或任何可推导的值)、有有效期(长期有效的链接会在邮箱里躺很久,而它的持有者可能已经变了——共享邮箱、离职交接)、只能用一次(激活成功即失效,否则链接被转发后可以被多人重复使用)、可以被撤销(管理员会发错邮箱)。另外我还做了一件事:库里只存令牌的哈希不存原文,这样拖库时也拿不到可用的链接。而这个模块里最严重的一个漏洞其实是撤销相关的:成员被移除后,他手里的旧邀请邮件还能用,点一下就又回来了——是一个客户的管理员反馈的,他移除了离职员工,几天后发现这个人又出现在成员列表里。修法是成员被移除时,其名下未使用的邀请一并失效。教训是:撤销权限的操作要把「所有能重新获得权限的路径」都一起关掉——我只关了「现有身份」,没关「邀请这条入口」。这和「拉黑要覆盖所有入口」是同一类思维。
    Q:席位超配额那个问题,你为什么不在发邀请的时候就把席位占掉? A:发邀请就占席位确实能彻底避免超配额,我考虑过,但它有一个更常见的坏处:很多邀请永远不会被激活。人换了、忘了点、不需要了——这些邀请如果提前占了席位,配额就被白白浪费,管理员还要手动去清理过期邀请才能释放。而管理员通常不知道要去清理。所以我选择「发邀请不占席位、激活时才占」,代价是激活时可能失败。判据是「哪种失败对用户的伤害更小」:白占席位是持续的浪费而且不易察觉;激活失败是一次性的、有明确提示(「席位不足,请联系管理员」)、也有清晰的解决路径。缓解办法是发邀请时明确提示席位状况——「当前剩余 N 个席位,已发出 M 份待激活邀请」,让管理员自己判断会不会超。然后核心的技术问题是激活时的并发。我原来的实现是「先查剩余席位、够就插入成员」,三个人同时点激活,三个请求都查到「还剩 1 个」,然后都插入成功——租户变成 12 个成员而只买了 10 个席位。修法是原子占用席位(计数的原子操作,或数据库层的条件更新加唯一约束),占用失败就明确拒绝。我没有用分布式锁,因为锁的粒度会让同一租户的激活操作串行,还要处理锁超时失效这些复杂情况——而这里的争用只是一个计数,用原子操作就够。更一般的教训是:任何「检查配额然后消耗配额」的逻辑,检查和消耗必须是一个原子操作——库存、席位、优惠券、限流的形态完全一样,而「先查再写」在单人测试时永远正确。

模块三:通知模板的租户级定制

  1. 通知模板的租户级定制(租户到默认的三层回退链 + 变量白名单与保存前校验 + 渲染失败降级到默认模板 + 内容注入防护与测试发送)★★★
    简历这样写 通知模板租户定制(Spring Boot + 模板引擎受限渲染 + 多层配置回退 + 发送前校验):通知文案原为代码内硬编码,客户要求带上自己的公司称谓与口径只能定制开发,改为模板配置化并支持租户覆盖,取用按租户模板到行业模板到系统默认三层回退;模板变量限定在白名单内并在保存时校验(原先保存不校验,引用了不存在的变量要到真实发送时才报错,且当时通知已经发不出去),变量缺值时按占位策略处理而非渲染出空白或字面量;渲染改为受限模式(仅做变量替换与简单条件,不执行任意表达式)并对变量值做转义,避免用户输入内容被当作模板或标记解析;渲染或发送失败时降级到系统默认模板重试,保证通知能发出而不是整条丢失;提供预览与测试发送(用样例数据渲染,发给操作人自己),让客户改完能自己验证。改造后模板类定制需求不再占用开发排期。
    展开完整拆解
    为什么要这么设计

    通知文案原来是硬编码在代码里的。客户的需求很常见:把「您的审批已通过」改成带自己公司口径的说法、把签名换成自己的公司名、邮件里加上自己的联系方式。每来一个这样的需求就要改代码发版,而且不同客户要求不同,代码里开始出现按租户判断的分支。四个问题。

    一是硬编码加租户分支很快就失控了。几个客户之后,一段文案里出现了好几层条件判断。新增一个客户的定制,要在十几个通知点各加一个分支——而漏改一处就是「有的通知定制了、有的没定制」,客户会认为我们做得很随意。

    二是改成模板配置化之后,出现了新问题:保存时不校验变量。客户在模板里写了一个不存在的变量名(打错字、或者以为有这个字段)。保存成功,但真实发送时渲染报错——而那时通知已经发不出去了更糟的是这个错误只在特定业务事件触发时才暴露,可能是几天之后。

    三是渲染失败导致通知整条丢失。我最初的处理是渲染异常就抛出、消息进死信队列。结果是客户的一批审批通知完全没发出去——而通知丢失的影响远大于「文案不是定制的」。客户宁可收到默认文案,也不愿意收不到通知。

    四是模板渲染有注入风险。模板里会插入用户输入的内容(比如申请单标题)。如果这段内容本身包含模板语法或标记语言的特殊字符,会被当成模板或标记去解析——最轻的表现是渲染出错,严重的会造成邮件内容被篡改。

    所以四个改动:模板配置化并按租户到行业到默认三层回退变量限定白名单并在保存时校验渲染或发送失败降级到默认模板重试渲染用受限模式并对变量值转义,另外补了预览与测试发送

    这个模块最想说的一句话是:把配置能力开放给客户,就必须同时提供「校验」和「兜底」——否则你只是把出错的机会转移给了客户,而后果还是由你承担。模板配置化本身很容易做,难的是保证「客户配错了不会导致通知发不出去」:保存时校验拦住可预见的错误,渲染失败降级兜住不可预见的,两者缺一个,这个功能就会从「解决问题」变成「制造事故」。

    整体链路
    模板取用:三层回退链 │ ├─ 租户自定义模板(该租户 + 该通知类型 + 该渠道) │ ↓ 没有则回退 ├─ 行业预置模板(按租户所属行业) │ ↓ 没有则回退 ├─ 系统默认模板(一定存在,是兜底的基石) │ └─ 回退发生时记录日志(便于排查「为什么用的不是我配的模板」) 客户配了模板但没生效,最常见的原因是渠道或类型没对上 模板结构 │ ├─ 维度:通知类型 × 渠道(邮件 / 短信 / 站内 / 企业办公平台) │ 同一事件不同渠道的文案长度与格式要求完全不同 │ 短信有长度限制、站内可以富一点、邮件有标题与正文 │ ├─ 每个模板含:标题 · 正文 · 可用变量清单 · 启用状态 · 版本 │ └─ 变量白名单由通知类型决定 审批通知有「申请人 · 单号 · 结果」,登录提醒有「时间 · 设备」 不按类型限定的后果:客户在审批模板里用登录相关变量,永远取不到值 保存时校验(拦住可预见的错误) │ ├─ 解析模板中引用的全部变量 → 与白名单比对 │ 不在白名单 → 拒绝保存并指出具体位置 │ 不校验的后果:真实发送时才报错,而那时通知已发不出去 │ 且只在特定业务事件触发时才暴露,可能是几天之后 │ ├─ 长度校验(短信渠道尤其重要,超长会被截断或发送失败) │ 要按变量填入后的估算长度算,不是按模板字符数 │ ├─ 必要信息校验(比如退订说明 · 签名等合规要求项不可删) │ └─ 语法校验(未闭合的标记等) 渲染(受限模式 + 转义) │ ├─ 只支持变量替换与简单条件,不执行任意表达式 │ 开放完整模板引擎的后果:客户配的模板成了可执行入口 │ ├─ 变量值统一转义后再代入 │ 不转义的后果 │ 用户输入的内容含模板语法 → 被当成模板二次解析 │ 含标记语言特殊字符 → 邮件内容结构被破坏 │ ├─ 变量缺值按占位策略处理(给出中性替代文案) │ 直接渲染成空白或字面量的后果:句子不通、暴露变量名 │ └─ 渲染结果再过一次长度与必要项检查 失败降级(兜住不可预见的错误) │ ├─ 租户模板渲染失败 → 用系统默认模板重试 │ 直接失败的后果:整条通知丢失 │ 而通知丢失的影响远大于「文案不是定制的」 │ ├─ 降级发生要告警并通知租户管理员(他的模板有问题) │ 静默降级的后果:他一直以为自己的模板在生效 │ └─ 系统默认模板必须始终可渲染(它是兜底,不能依赖任何可选变量) 预览与测试发送(让客户自己能验证) ├─ 用样例数据实时预览渲染结果 ├─ 测试发送:发给操作人自己,真实走渠道 │ 只做预览不够 —— 渠道本身的格式限制只有真发才看得出来 └─ 测试发送要标记为测试,不计入配额与统计 可观测 ├─ 各租户模板的使用与降级次数 ├─ 保存时校验的拒绝原因分布(哪类错误最多 → 改进编辑器提示) └─ 按渠道的发送成功率(模板变更后异常下降要能发现)
    分步拆解
    1. 先把硬编码文案抽成模板配置。硬编码加租户分支很快就失控——几个客户之后一段文案里有好几层条件判断,新增一个客户的定制要在十几个通知点各加分支,漏改一处就是「有的定制了有的没定制」。
    2. 取用走「租户 → 行业 → 系统默认」三层回退。系统默认必须始终存在,它是整条链的兜底基石。
    3. 回退发生时要记录日志。客户最常问的问题是「我配了模板为什么没生效」,而原因通常是渠道或通知类型没对上——有日志才能快速答复。
    4. 模板维度必须是「通知类型 × 渠道」。同一事件在短信、邮件、站内的长度与格式要求完全不同,用一份模板覆盖多渠道必然有一个渠道不合适。
    5. 变量白名单要按通知类型限定。审批通知有「申请人、单号、结果」,登录提醒有「时间、设备」。不按类型限定的话客户会在审批模板里用登录相关变量,永远取不到值。
    6. 保存时必须解析并校验全部变量引用。不校验的后果是真实发送时才报错,而那时通知已经发不出去,且只在特定业务事件触发时才暴露,可能是几天之后。
    7. 校验失败要指出具体位置。「第几行的某个变量不存在」——只说「模板有误」客户找不到。
    8. 长度校验要按变量填入后的估算长度算,不是按模板字符数。短信渠道尤其重要,超长会被截断或直接发送失败。
    9. 合规必要项(退订说明、签名)不允许删除。这类要求客户可能不知道,系统要挡住。
    10. 渲染必须用受限模式,只支持变量替换与简单条件。开放完整模板引擎的后果是客户配的模板成了一个可执行入口——这是很严重的安全问题。
    11. 变量值必须统一转义后再代入。不转义的话,用户输入的内容如果含模板语法会被二次解析,含标记语言特殊字符会破坏邮件结构。
    12. 变量缺值要按占位策略处理,给中性替代文案。渲染成空白会让句子不通,渲染成变量名字面量会暴露内部字段名。
    13. 渲染结果要再过一次长度与必要项检查。因为实际变量值可能比估算的长。
    14. 渲染失败要降级到系统默认模板重试,不能让通知整条丢失。通知丢失的影响远大于「文案不是定制的」——客户宁可收到默认文案也不愿收不到通知。
    15. 降级要告警并通知租户管理员。静默降级的后果是他一直以为自己的模板在生效。
    16. 系统默认模板不能依赖任何可选变量。它是兜底,必须在任何数据状况下都能渲染成功。
    17. 要提供预览与测试发送两件事,预览不能替代测试发送。渠道本身的格式限制(短信长度、邮件客户端渲染差异)只有真发一次才看得出来。
    18. 测试发送要标记为测试,不计入配额与统计。否则客户多试几次配额就被吃掉了。
    19. 保存时校验的拒绝原因要统计。哪类错误最多,就该在编辑器里加对应的提示或补全——从源头减少错误。
    关键决策与取舍

    渲染选「受限模式」而不是完整模板引擎。完整引擎功能强、客户能做复杂的条件和循环。但客户配的模板是外部输入,开放完整引擎等于开放了一个可执行入口——这个风险不可接受。受限模式的代价是复杂需求做不了(比如按金额区间显示不同文案)。我的处理是把常见的复杂需求归纳成有限的几种条件形式内建支持,而不是开放通用表达式判据是「这个输入是否可信」:不可信,那能力就必须受限。

    渲染失败降级到默认模板,而不是让通知失败。让它失败看起来更「诚实」(不掩盖问题),但结果是客户的一批业务通知完全没发出去——而通知丢失的业务影响远大于「文案不是定制的」降级的代价是可能掩盖问题,所以必须配告警加通知租户管理员——降级本身是兜底,但不能是静默的判据是「哪种失败的业务影响更小」,同时保证失败可见。

    保存时校验 + 渲染失败降级,两者都要有。只做保存校验挡不住不可预见的错误(变量值异常、渠道限制变化);只做降级则会让客户一直配错而不自知这一条我认为是这个模块最核心的设计判断:把配置能力开放给客户,必须同时提供「校验」和「兜底」——否则你只是把出错的机会转移给了客户,而后果还是由你承担。

    三层回退链而不是两层。只做「租户 → 默认」也能跑,但行业模板这一层的价值很大——同行业客户的口径需求相近,预置一份能让大部分客户不用自己配。代价是取用逻辑多一层、排查时要能说清用的是哪一层。所以回退发生要记日志。

    踩过的坑一:模板保存时不校验变量,真实发送时才报错。客户在模板里写了一个不存在的变量名(打错字),保存成功,几天后某个审批事件触发时渲染失败、通知发不出去而客户完全不知道是自己模板的问题,他的反馈是「你们的通知不发了」。教训是:校验的时机应该尽量靠近「用户做出错误动作的那一刻」——保存时校验的成本和发送时校验一样,但前者能让客户立刻改,后者已经造成了业务损失。

    踩过的坑二:渲染异常直接进死信队列,一批通知全丢。我当时的想法是「失败就该暴露出来」。但暴露的方式不该是让业务通知消失——客户是在业务出问题之后(审批没人处理)才发现的。教训是:对于「附属能力失败」的处理,要问「有没有一个可用的降级结果」;有的话就该降级,同时告警让问题可见。「失败就抛出」在核心链路上是对的,在通知这类场景上代价太大。

    踩过的坑三:变量值没有转义,用户输入的标题里含特殊字符导致邮件结构被破坏。表现是邮件排版乱掉、部分内容不显示。教训是:模板渲染里「模板」和「数据」必须严格区分——模板是我们和客户配置的,数据是用户输入的,数据代入模板时必须转义,否则数据就有机会变成模板的一部分。这和防注入是同一个原理。

    没做的部分:没做模板的多语言版本管理。它对有海外团队的客户有价值,但需要在「租户 × 类型 × 渠道」之上再加一个语言维度,回退链会变成四层,复杂度明显上升。取舍依据是当前客户的需求集中在中文口径定制上;我在设计时把语言留成了模板的一个字段,这样将来扩展不需要改表结构。

    数字是怎么测的

    核心价值指标是「模板类定制需求占用的开发排期」。改造前每个定制需求都要改代码发版。报法是「改造前 N 个月内处理了 M 个模板定制需求、平均耗时若干人天;改造后此类需求由客户自助完成」——这个对比很具体。

    回退链的正确性用断言型用例:分别构造「有租户模板」「无租户模板有行业模板」「都没有」三种情况,断言取用的模板正确、且回退时记录了日志。

    保存校验:提交含不存在变量的模板,断言被拒绝且错误信息指出具体位置;提交超长的短信模板,断言被拒绝。

    转义防护:用包含模板语法与标记语言特殊字符的变量值渲染,断言这些内容被当作纯文本呈现、没有被二次解析、邮件结构完整。这条是安全性质的,必须有。

    降级:人为让租户模板渲染失败,断言自动使用系统默认模板发送成功、同时产生了告警与管理员通知「产生告警」这一条最容易漏,而它是降级不变成静默掩盖的关键。

    默认模板的健壮性:用缺失全部可选变量的数据渲染系统默认模板,断言仍能成功渲染。它是兜底,不能自己也失败。

    变量缺值:断言渲染出中性占位文案,而不是空白或变量名字面量。

    测试发送:断言真实走渠道、发给操作人、且不计入配额与统计。

    模板变更后的发送成功率要监控:报「按渠道的发送成功率」,模板变更后异常下降要能发现——这是保存校验之外的第二道观察。

    不要报什么:不要报「模板渲染成功率 100%」——客户会配出各种意外情况。该报的是「模板定制需求从占用开发排期变为客户自助」「三种回退情况取用正确」「特殊字符被当作纯文本呈现」「渲染失败自动降级且产生告警」「默认模板在缺失全部可选变量时仍可渲染」这几件可核对的事。

    面试追问
    Q:通知模板做成可配置,让客户自己改,这不就是加个表存文案吗? A:存文案是最容易的部分,难的是保证「客户配错了不会导致通知发不出去」——而我正是在这里踩了两个坑。第一个坑是保存时不校验变量:客户在模板里写了一个不存在的变量名(打错字),保存成功,几天后某个审批事件触发时渲染失败、通知发不出去。客户完全不知道是自己模板的问题,他的反馈是「你们的通知不发了」。教训是校验的时机应该尽量靠近「用户做出错误动作的那一刻」——保存时校验和发送时校验的实现成本一样,但前者能让客户立刻改,后者已经造成了业务损失。所以现在保存时会解析模板中引用的全部变量与白名单比对,不在白名单就拒绝并指出具体位置(只说「模板有误」客户找不到)。第二个坑是渲染异常直接进死信队列,一批审批通知全丢了。我当时的想法是「失败就该暴露出来」,但暴露的方式不该是让业务通知消失——客户是在审批没人处理之后才发现的。修法是渲染失败降级到系统默认模板重试,同时告警并通知租户管理员——降级是兜底但不能是静默的,否则客户一直以为自己的模板在生效我从这两个坑总结出的核心判断是:把配置能力开放给客户,必须同时提供「校验」和「兜底」——只做校验挡不住不可预见的错误,只做兜底会让客户一直配错而不自知,缺一个这个功能就会从「解决问题」变成「制造事故」,因为你只是把出错的机会转移给了客户,而后果还是由你承担。
    Q:模板渲染为什么要用受限模式?直接用现成的模板引擎不是更省事、功能也更强? A:因为客户配的模板是外部输入,而完整模板引擎通常支持表达式求值甚至方法调用——开放它等于给了外部输入一个可执行入口。这个风险在多租户 SaaS 里不可接受:一个租户的管理员配一段模板,就可能读到不该读的数据或者执行不该执行的逻辑。所以我的做法是只支持变量替换与有限的几种简单条件,不执行任意表达式代价是复杂需求做不了(比如「按金额区间显示不同文案」),我的处理是把常见的复杂需求归纳成有限的几种内建条件形式,而不是开放通用表达式——覆盖大部分场景,同时保持能力边界清晰。判据很简单:这个输入是否可信?不可信,能力就必须受限。另外还有一件同源但独立的事:变量值必须转义后再代入。我踩过这个坑——用户输入的申请单标题里含有标记语言的特殊字符,代入邮件模板后把邮件结构破坏了,表现是排版乱掉、部分内容不显示。教训是模板渲染里「模板」和「数据」必须严格区分:模板是我们和客户配置的,数据是终端用户输入的,数据代入模板时必须转义,否则数据就有机会变成模板的一部分——这和防 SQL 注入、防跨站脚本是同一个原理,只是载体换成了通知模板。还有一个细节:变量缺值时要给中性的占位文案,渲染成空白会让句子不通,渲染成变量名字面量会把内部字段名暴露给终端用户。

没有匹配的内容,换个关键词试试。

项目拆解 · 租户开通与成员管理(后端 · 实习级)· 共 3 个模块 · 数字均为示例,需替换成自己项目的真实数据