飞书人事沙箱的设计思考

4,447 阅读20分钟

我们来自字节跳动飞书商业应用研发部(Lark Business Applications),目前我们在北京、深圳、上海、武汉、杭州、成都、广州、三亚都设立了办公区域。我们关注的产品领域主要在企业经验管理软件上,包括飞书 OKR、飞书绩效、飞书招聘、飞书人事等 HCM 领域系统,也包括飞书审批、OA、法务、财务、采购、差旅与报销等系统。欢迎各位加入我们。

本文作者:飞书商业应用研发部 陕欢铭

欢迎大家关注飞书技术,每周定期更新飞书技术团队技术干货内容,想看什么内容,欢迎大家评论区留言~

产品介绍

背景

企业应用,尤其人事领域应用有复杂度高、配置难度大、影响面广的特点,在实施初始化配置验证、重大配置变更、上下游系统集成、演示培训等场景中,为避免影响线上业务,通常需要有独立且与生产隔离的空间进行操作。

在自动化沙箱投入使用之前,上述场景中,我们通常为客户额外开通线上租户,作为其“沙盒”;客户可将该租户与生产租户搭配使用,但仍遇到突出问题如:

  • 实施配置及验证时,手动迁移配置项效率低工作量大且易出错。

客户采购后初期实施,或业务开展过程中出现组织架构调整等,为避免影响线上,实施/客户需要在沙盒租户中先配置,验证通过后再将配置项迁移至生产租户,当前迁移工具能力有限,仍需有较大手动工作量。

  • 系统 集成 、重大配置变更及验证时,沙盒无数据、或数据与生产不一致、数据未 脱敏 等,开发/测试/验证效率低,有风险。
  • 客户有自研的上下游系统及外部供应商,「飞书招聘」只是其中一环,集成过程需要开发测试,即使给客户开通单独的沙盒租户,但沙盒租户中没有业务数据及配置项,无法满足客户诉求。
  • 「飞书人事」的沙盒租户一般在实施初期使用,手工导入人员及组织架构数据;一方面,业务正常开展后沙盒中的配置项与业务数据与线上未同步,配置变更时需要两边比对调整,且业务数据太旧参考性差,只能跑通配置逻辑,难以准确测试;另一方面,客户不希望开发人员看到真实的业务数据,但沙盒租户中的数据并未脱敏处理,有泄露敏感信息的风险。

目标

解决实施和客户在配置、验证飞书人事时的痛点问题,帮助客户自动化迁移配置(尽量避免手动迁移、重复操作),以提升效率、降低风险。

步骤场景核心能力
本期客户初始化配置、重大配置变更及验证、系统集成具备产品化的沙箱能力
1、从生产创建沙箱,并同步「飞书人事」配置项;
2、沙箱管理;
支持将变更配置项由沙箱->生产:
1、沙箱->生产的变更部署;
2、配置项优先级如「飞书人事」流程设置、权限设置、入职设置、合同设置等;
后续系统集成、演示培训支持从生产创建沙箱并同步业务数据
1、业务数据脱敏规则、数据同步范围控制的管理;
2、从生产创建沙箱并同步配置项+业务数据;
沙箱接入更多应用,进行横向拓展

功能演示

场景图示
管理沙箱
从左侧菜单的「沙箱管理」进入。包括 新建沙箱、进入沙箱、删除 等功能。没有做重置功能,沙箱租户与正式租户的差异较大时,只能删除重建。删除不彻底(没有删除租户在飞书人事的业务数据和元数据、主数据)。
查看详情
在「沙箱管理」的沙箱列表页中点击任意一个沙箱的名称,进入沙箱详情页。
创建沙箱
点击「新建沙箱」按钮,弹出弹窗,创建时可以指定沙箱名称、描述、沙箱管理员等信息。
点击「确定」后进入沙箱创建阶段,页面左下角出现开通进度。无论沙箱创建成功还是失败,该进度都会消失,开通成功的沙箱会出现在沙箱列表里。
一个正式租户最多只能有5个沙箱(不包括已删除的),同时只能开通一个沙箱。
进入沙箱
点击「进入沙箱」,会在当前浏览器中打开新tab,默认进入沙箱租户的「角色配置」页面。
沙箱租户顶部有个banner,与正式租户区分。
查看变更项
当接入沙箱的业务配置有变更时,沙箱顶部banner右侧的「部署变更项」会变成可点击,用户点击后进入变更项列表页。
用户可以在该页面选择要部署的变更项、处理部署错误,并提交部署。
部署变更项
点击「部署」按钮后会出现弹窗,确认无误后即开始部署,前端跳转到其他页面,并弹出「查看部署记录」的tips。
查看部署记录
在正式租户的「沙箱管理」里也可以查看部署记录。
点击「查看详情」,可以看到本次部署涉及到的变更项以及部署方式。

竞品对比

北森

提供了 2 种沙箱租户,如

  • 测试租户:主要用于实施、配置变更测试和验证
  • 沙箱环境租户:主要用于需求开发、测试,提升开发效率
功能图示
支持从生产复制至沙箱:
* 测试租户:全部元数据配置的同步,不含业务数据
* 沙箱环境租户:全部元数据+业务脱敏数据的同步
沙箱 初始化
仅支持将元数据配置项从沙箱迁移回生产,不支持迁移业务数据填写 打包 信息(创建部署任务)添加 元数据 配置(选择变更项)打包 并执行部署

aPaaS

aPaaS的沙箱为开发者提供了aPaaS应用的开发环境。主要用于修改、测试应用,可在完成测试后再发布:

未开启沙箱,可在生产环境中修改;开启沙箱后,只能在开发环境中修改应用,在生产环境中可以查看已发布的应用配置,但不能修改。

功能图示
支持从生产复制至沙箱,可复制应用的全部配置项/元数据启用开发环境(创建沙箱)
支持从沙箱迁移到生产,可迁移全部配置项/元数据沙箱内变更发布(部署)

系统介绍

设计思路

在收到产品需求后,我们首先应该进行需求分析,了解需求背后用户的真实诉求。进而从用户诉求出发,进行系统设计和模块划分,并以此来指导后续的开发工作。

用户诉求

根据上一节的「产品介绍」,实施和客户是沙箱的主要用户(前期主要是实施),他们对沙箱的诉求如下:

模块功能描述是否是痛点
沙箱管理> 正式租户中创建沙箱从正式租户创建一个沙箱,并将正式租户的基础数据和业务配置数据复制到沙箱中创建沙箱涉及到复制多个业务方的数据,以及元数据、根部门、第一人等基础数据的创建,这些节点之间可能有先后依赖顺序,这就引入了沙箱创建流程的编排和调度
沙箱列表在沙箱列表中可以查看已创建的沙箱,并能进入或删除沙箱
沙箱详情查看沙箱的详情,并能添加/移除沙箱管理员
部署记录查看正式租户下所有沙箱的部署记录,包括部署时间和成功失败状态等
部署详情查看某个部署记录的详情信息,及本次部署了哪些沙箱中的变更配置项到正式租户
沙箱重置由于用户既能在沙箱又能在正式租户中操作,所以在沙箱使用过一段时间后,其中的业务数据可能跟正式环境相差越来越大,在部署到正式环境时就会遇到各种各样难以避免和解决的问题
所以重置沙箱的目的就是将沙箱中的各种数据强制跟正式环境对齐(相当于git中rebase master分支),以避免上述问题

对实施来说不是痛点需求,可以通过删除过期沙箱再创建新沙箱来解决
对业务方来说,每个业务方都要实现重置逻辑,ROI不高
变更项部署> 沙箱租户中变更项列表用户在沙箱租户中操作了业务配置,如新增角色时,要有地方能看到用户操作的变更项,因此需要给用户提供变更项列表页面
选择变更项用户在沙箱中操作产生变更项后,希望能将某些(而不是全部)变更项部署到正式租户,这时就要提供让用户选择其期望部署的变更项的功能

某些变更项A可能会跟其他变更项B关联,用户在选中A时要自动选中B并一起部署(如沙箱中的BPM流程a引用了在沙箱中新增的角色b,如果用户希望部署a,则系统要自动选中b并一起部署,否则a部署到线上后可能会因为缺少角色b而无法使用)
预部署检查在实际部署前,需要将用户选中的变更项提交给各业务方进行预部署检查

业务方应该检查这些变更项是否能正常部署到正式租户,如进行重名校验,或者检查变更项是否引用了在沙箱中创建的部门、员工等正式租户没有的数据,并告知用户进行相应的处理,例如放弃部署、用正式租户中的数据来替换沙箱的数据(即依赖项替换)
查看部署差异在预部署检查通过以后,提供一个预览页面给用户查看当前将要部署到正式租户的变更项,用来作最后的确认
部署变更项用户确认无误后,即可开始部署沙箱中的变更项到正式租户

在部署变更项时,如果变更项A关联了变更项B,就需要B在A之前部署,否则A部署后可能无法正常使用(如沙箱中的BPM流程a引用了沙箱中的角色b,则应该先部署角色b再部署流程a),这就引入了沙箱部署流程的编排和调度

问题分析

由于飞书人事下有 权限、文件模板、BPM流程、入职流程、人员档案配置(自定义字段)、转调离配置 等众多业务模块,各业务的实现细节、数据存储方式均有不同,所以在业务方接入人事沙箱时,应该对那些功能由沙箱的业务中台做、哪些功能由业务方来做,有个基本的判断。而且业务中台有必要对业务方提供一些接入规范和建议,来降低业务方的接入成本。

  • 问题一:沙箱中的业务数据如何与正式租户中的数据建立对应关系?

    •   创建沙箱时,要把正式租户中的业务数据往沙箱中复制,首先要将沙箱和正式租户的数据建立对应关系,才能在之后进行数据变更对比、变更项部署等操作。

      方案描述优点缺点
      建立映射表,保存沙箱和正式租户中数据的映射关系在这种方案下,沙箱和正式租户中的业务数据可以随意使用各自的唯一 ID 标识,但是在把数据从正式租户复制到沙箱、或者从沙箱迁移到正式租户时,要在某个地方建立数据的映射关系;业务方在对比、部署变更项前先查找映射关系,才能确定沙箱中的某条数据跟正式中的数据对应沙箱中的数据唯一标识可以随意生成,无需跟正式租户保持一致数据唯一标识的映射关系,有一定的开发和维护成本
      业务方保证沙箱中的数据唯一标识与正式租户中一致这种方案,只要沙箱和正式租户数据的唯一标识相同,就认为它们是同一条业务数据有几种方式:
      1. 以 api name、domain key 等字段作为数据的唯一标识,这些字段能天然保证租户内唯一、跨租户可以不唯一
      2. 业务方用 wk_id + tenant_id 作为联合唯一索引,如果沙箱和正式租户数据的 wk_id 相同,就认为是同一条数据
      省去了维护和查找映射关系的成本,更加简单、不易出错某些业务方可能之前以 wk_id 作为唯一索引,需要修改表结构和相关的DML逻辑
    •   结论:对于绝大多数业务方来说,方案二的开发和维护成本都更小,故目前所有业务方都采用方案二。

  • 问题二:业务如何判断沙箱中的数据是否有变更?

    • 方案描述优点缺点
      对比沙箱和正式租户中数据的更新时间只要沙箱中的数据的更新时间晚于正式租户,就认为该数据在沙箱中有变更,可以将变更部署到正式实现简单,不易出错没有记录历史数据,不支持回滚可能会出现用户在沙箱中修改了某条数据后,又在正式环境修改了该数据的其他字段,这时沙箱租户数据的更新时间小于正式租户,不会认为是变更项> 以上两条均非痛点,实施可以接受
      模拟git的工作方式进行数据对比和合并正式租户,相当于master分支创建沙箱,相当于从master创建一个新分支在正式租户或沙箱中的每一次操作,都相当于在对应分支中的一个commit获取变更项相当于对比沙箱分支和master的差异部署变更项相当于将沙箱分支合并到master可以支持细粒度的数据差异对比,并支持回滚(reset)、重置(rebase)等功能对于人事业务来说,用户操作和数据变更比较频繁,会产生很多提交记录,无法保证性能实现复杂,ROI很低
      •   结论:采用方案一。
  • 问题三:变更项的相关信息如何记录?

    • 方案描述优点缺点
      admin中台记录变更项最后一次部署成功的时间,变更项的具体数据由业务方返回变更项部署成功后会有回调,这时中台记录「业务方+已部署的变更项ID+最近部署时间」;业务方通过接口从中台获取自己已部署的变更项ID+时间列表,自己判断是否有新的变更(根据每个变更项的更新时间是否晚于上次部署时间)
      业务方自己提供变更项详细信息,自己记录依赖关系,并通过变更项列表接口返回给中台,在前端展示
      业务方无需自己存储变更项的部署记录业务方要请求中台的接口获取变更项的最新的部署时间,相当于获取变更项的逻辑没有收敛在自己的服务内,反而依赖了admin中台的外部接口,架构上不合理
      业务作为黑盒,自己记录自己的变更项、部署时间和部署状态业务自己记录自己内部的变更项、部署状态,实现方式由业务方自己定,与中台约定统一的接口格式返回数据中台无需感知业务,变更项记录、部署完全由业务方自己实现业务方为了记录变更项要增加额外的存储和逻辑,且无法被其他业务复用
      业务方统一上报变更项、依赖关系到中台业务每次变更都上报到中台,由中台统一存储(中台不感知业务逻辑)格式统一,有统一的收口,中台可以直接使用这些变更项给前端展示,无需业务方提供变更项列表接口业务每次修改数据都需要进行上报相关的改动和依赖的其他数据,对业务的侵入较大,而且不灵活
    •   结论:目前各业务都采用方案一,总的开发成本相对来说比较小;但从架构合理性的角度来看,应该采用方案二。

功能边界

根据上述设计思路,可以划分出整个沙箱系统的功能和边界:

whiteboard_exported_image (11).png

角色职责分工
admin中台目前先实现了人事的沙箱,以后会扩展到招聘、OKR,所以由admin中台统一来做沙箱管理
1. 处理用户交互,负责沙箱的统一管理
2. 负责沙箱本身数据的存储、展示、更新
3. 负责触发创建、部署任务
4. 规定各业务接入沙箱所必需实现的接口和其他逻辑
人事沙箱中台1. 中台和飞书人事各业务之间的桥梁
2. 负责飞书人事内部变更项的获取和展示
3. 负责飞书人事沙箱租户的创建和部署流程的编排调度
人事业务方1. 根据沙箱接入规范实现接入沙箱所需要的接口、MQ消费 等逻辑
a. 两个接口:获取变更项、预部署检查
b. 两个MQ:沙箱创建、变更项部署
2. 识别并处理自己业务内部的变更项、关联项、依赖项,完成自己的业务逻辑

逻辑架构

我们作为飞书人事的业务中台,在沙箱这个大系统里起到了承上启下的作用。在业务中台的视角下,沙箱的逻辑架构如下:

流程图 (8).jpg

时序图

新建沙箱

新建沙箱时,要创建一个飞书租户、安装飞书人事应用,并通过人事沙箱中台的流程调度将正式租户的基础数据和业务配置数据复制到沙箱中。

部署变更项

数据模型

流程领域模型

人事沙箱中台的主要工作是对各业务的沙箱创建、部署节点进行流程编排和任务调度,是一个典型的流程调度器。

将整个创建/部署过程抽象为一条流程,每个接入下游抽象为流程中的一个任务,可以同时并发执行的任务包装为一个节点。整个流程的执行过程如下:

流程图 (9).jpg

E-R图

设计上 flow_node 与 flow_task 是一对多关系,实际上目前每个node下只有一个task,也就是说各业务的任务执行一定有先后顺序。

数据表

表名简介作用
upstream_message上游消息表对接收到的上游沙箱创建、部署任务数据进行持久化,用于创建后续流程
flow流程表根据上游消息表中的信息生成的沙箱创建、部署流程
flow_node流程节点表流程中的具体节点
flow_task流程任务表流程节点中的任务
task_payload任务负载表由于沙箱部署任务中的变更项列表可能很大,所以单独创建一个payload表来保存部署任务的负载

沙箱数据结构

依赖项

// 依赖项,树结构
struct DependenceItem {
    // 依赖项无app、无biz
    1: required string                Identifier      // idgen id, domain key, api name, ... 业务自己可唯一标识的字段. 其中 user, employee 需要返回 SaaS user ID
    2: required I18N                  Name            // 依赖项名字
    3: required DependenceType        DependenceType  // 依赖对象类型
    4: optional DependenceItem        ReplacedBy      // 在另一个租户中被替换为其他依赖项
    5: optional list<DependenceItem>  Children        // e.g. 部门树结构,层级展示
    6: optional Attr                  Attr            // 给前端的附属信息
    7: optional map<string, string>   Extra           // 业务方自己存的额外信息,搜索的时候传递给业务方
}

配置项

// 配置项,树结构
struct ConfigItem {
    1:  required I18N                 Name         (api.json="name", go.tag="json:"name"")                 // 配置项名字, 0<name.len<50
    2:  required bool                 Deployable   (api.json="deployable", go.tag="json:"deployable"")     // 是否可部署。一般情况下叶节点配置项均可部署,非叶节点配置项不可部署。可部署配置项会在前端展示部署方式字段。
    3:  required App                  App          (api.json="app", go.tag="json:"app"")                   // 飞书人事、OKR、绩效等
    4:  optional string               Biz          (api.json="biz", go.tag="json:"biz"")                   // 入职、BPM、流程、文件模板、自助服务、权限角色等。App内唯一,0<Biz.len<36。非叶节点可以不传Biz、Identifier。
    5:  optional string               Identifier   (api.json="identifier", go.tag="json:"identifier"")     // 配置项唯一标识。App内唯一,是否跨租户唯一业务方自己定义。0<Identifier.len<36
    6:  optional string               URI          (api.json="uri", go.tag="json:"uri"")                   // 配置项链接,相对路径
    7:  optional DeployType           DeployType   (api.json="deployType", go.tag="json:"deployType"")     // 变更方式(部署方式)。父子节点变更方式要求一样,暂不支持特殊处理。
    8:  optional User                 Operator     (api.json="operator", go.tag="json:"operator"")         // 变更人
    9:  optional i64                  ChangeAt     (api.json="changeAt", go.tag="json:"changeAt"")         // 最近变更时间,毫秒时间戳。前端展示,时间戳传给前端不需要转为string
    10: optional list<ConfigItem>     Children     (api.json="children", go.tag="json:"children"")         // 配置项子树
    11: optional list<ConfigItem>     Related      (api.json="related", go.tag="json:"related"")           // 配置项依赖的配置项
    12: optional SelectedType         SelectedType (api.json="selectedType", go.tag="json:"selectedType"") // 勾选类型
    13: optional list<DependenceItem> Dependence   (api.json="dependence", go.tag="json:"dependence"")     // 依赖项
    14: optional bool                 Hidden       (api.json="hidden", go.tag="json:"hidden"")             // 暂未使用。是否隐藏配置项,默认情况下配置项都展示给用户看。隐藏的配置项也可以参与联动
    15: optional map<string, string>  Extra        (api.json="extra", go.tag="json:"extra"")               // 业务方自己存的少量额外信息
}

enum DeployType {
    NEW    = 1    // 新增
    UPDATE = 2    // 更新
    DELETE = 3    // 删除
}

enum SelectedType {
    SELECTED     = 1    // 选择
    NOT_SELECTED = 2    // 未选择
    DISCARD      = 3    // 放弃部署,类同未选择
}

后续规划

阶段技术规划产品规划支持场景/收益进展
第一阶段具备沙箱最主要的能力,包括沙箱管理配置项部署1、具备产品化的沙箱能力,包括沙箱创建、沙箱数据初始化、沙箱管理2、同步沙箱租户的配置变更到正式租户客户初始化配置、重大配置变更及验证、系统集成DONE
第二阶段完善沙箱能力,降低业务接入成本联调 环境隔离、提高接入效率1、支持人事领域内的业务接入,如转调离续、基础薪酬、payroll 等等沙箱接入更多应用,进行横向拓展DOING
第三阶段支持沙箱重置、删除1、重置沙箱,强制跟生产环境对齐2、删除沙箱中的业务方数据避免沙箱同步到生产环境出错,避免产生脏数据TODO
第四阶段支持业务数据部署,完善沙箱能力1、支持从生产创建沙箱时同步业务数据,业务数据在脱敏后复制到沙箱1. 2、支持沙箱业务数据变更同步到正式租户系统集成、演示培训TODO
第五阶段支持MG、KA1、根据Base地将用户相关数据分散存储2、支持私有化环境具备沙箱能力数据安全合规,支持跨国企业TODO