导言:状态机管流转,权限管谁能参与
上一篇我们聊了状态机。状态机解决的是业务怎么流转的问题:一张采购申请从"草稿"到"待审批"再到"已审批",每一步都需要明确的事件触发,状态之间不能乱跳。
但状态机只管"规则",不管"人"。
想象这样一个场景:一个刚入职的实习生,在系统里看到"审批"按钮,顺手把自己提交的采购申请点了通过。这张申请绕过了主管审批、绕过了财务审核,直接生效。状态机并没有报错,因为"审批通过"确实是一个合法的状态迁移事件。
问题出在"人"身上——这个人不该有这个权限。
这就是我们今天要讨论的主题:ERP 系统的权限设计。如果说状态机定义了业务流转的"交通规则",那么权限系统就是"驾照考试"和"道路限行"。没有后者,再好的规则也会被人为绕过。
权限设计为什么比普适系统更复杂
做过普通业务系统的工程师都知道,权限通常就是"谁能登录、谁能管理"。但在 ERP 里,这个问题要复杂得多。
第一,模块跨度大。 一个 ERP 系统里同时存在采购、销售、库存、财务等多个业务域。采购员需要创建采购订单,但不需要看到客户的信用额度;销售人员需要查看客户信息,但不该知道供应商的采购底价。
第二,角色密度高。 同一家公司里,采购员、采购主管、库管员、财务会计、财务经理、销售代表、销售总监……每个角色对同一个功能的操作范围都可能不同。
第三,数据敏感度分层。 客户的联系方式是低敏感数据,几乎所有销售都能看;客户的欠款余额和信用额度是高敏感数据,通常只有风控和财务能看;供应商的结算价格和账期是商业机密,可能只有采购主管以上级别才能接触。
第四,合规与审计要求。 ERP 系统的操作往往直接对应企业的资金进出和资产变动。如果采购员既能下单又能审批自己的订单,出了问题很难追溯责任。审计人员会要求系统证明"谁、在什么时间、对哪张单据做了什么操作"。
所以,ERP 的权限设计不是"加几个白名单"那么简单。它需要同时回答三个问题:
- 功能权限:这个人能不能点这个按钮?
- 数据权限:点了之后,他能看哪些数据?
- 操作权限:他能不能改、能不能删、能不能审批?
RBAC 三层模型:给用户发"门禁卡"
业界最常用的权限模型是 RBAC(Role-Based Access Control,基于角色的访问控制)。
它的核心思想很简单:不直接给用户分配权限,而是给"角色"分配权限,再把用户绑定到角色上。这就像是公司大楼的门禁系统——不是给每个人单独配钥匙,而是按部门发门禁卡,每张卡能开哪些门是预先设定好的。
业务定义:RBAC 是一种将权限与角色关联、再将角色与用户关联的访问控制模型。它的目标是降低权限管理的复杂度:当员工入职、调岗或离职时,只需调整其角色绑定,而不需要逐个修改数百项权限配置。
RBAC 的经典三层结构如下:
graph TD;
A[用户 User] -->|属于| B[角色 Role];
C[用户 User] -->|属于| D[角色 Role];
B -->|拥有| E[权限 Permission];
D -->|拥有| F[权限 Permission];
E -->|控制| G[功能操作];
F -->|控制| H[数据范围];
用户(User):系统的具体使用者,对应一个真实的人或系统账号。
角色(Role):一组权限的集合,对应业务中的一个岗位或职能。例如"采购员"、"销售经理"、"财务会计"。
权限(Permission):对系统资源的操作许可。例如"创建采购订单"、"查看客户信用额度"、"审批付款申请"。
这种分层的好处是:当公司新增一个采购员时,管理员只需要把他加入"采购员"这个角色,他就自动拥有了采购员应有的全部权限。如果公司决定让所有采购员都能查看库存可用量,只需要修改"采购员"角色的权限配置,所有绑定该角色的用户会同时生效。
反过来看,如果没有角色这层抽象,每来一个新人,管理员都要手动勾选几十项权限,既低效又容易出错。
功能权限 vs 数据权限:两个维度,不能混为一谈
在 RBAC 的基础上,ERP 系统通常会把权限再拆成两个维度:功能权限和数据权限。
很多人容易把这两者混为一谈,认为"有权限"就是"什么都能做"。实际上,功能权限决定"能不能操作",数据权限决定"能操作谁的数据"。
| 维度 | 定义 | 示例 |
|---|---|---|
| 功能权限 | 能否执行某个功能操作 | 销售代表能否点击"创建订单"按钮 |
| 数据权限 | 能操作的数据范围 | 销售代表创建订单时,客户下拉框里出现哪些客户 |
用一个更生活化的类比:功能权限像是"你能不能进厨房",数据权限像是"进了厨房你能用哪些食材"。两个维度叠加,才能完整描述一个人的系统行为边界。
功能权限的典型场景:
- 采购员可以"创建采购订单",但不能"审批采购订单"。
- 财务可以"查看付款申请",但不能"创建采购订单"。
- 库管员可以"执行出库操作",但不能"修改销售价格"。
数据权限的典型场景:
- 销售代表 A 只能看到自己负责的客户,看不到同事的客户。
- 华东区经理可以看到华东区所有销售的数据,但看不到华北区的。
- 普通会计只能看到本公司的财务凭证,集团 CFO 可以看到全集团的汇总报表。
在实际系统实现中,这两个维度通常需要联合校验。例如,当用户点击"查看订单详情"时,系统先检查他是否有"查看订单"的功能权限,再检查这张订单是否在他的数据权限范围内。只有两个条件同时满足,请求才能通过。
这里有一个容易踩坑的细节:很多系统在前端做了按钮隐藏,认为"看不到按钮就等于没权限"。但这只是用户体验层面的处理,真正的权限校验必须放在后端。一个稍有技术基础的用户,完全可以通过直接调用 API 来绕过前端限制。所以功能权限和数据权限的校验,必须是后端接口的第一道闸门。
职责分离原则:一个人不能既是裁判又是运动员
权限设计中最重要的一条原则,叫做 SoD(Segregation of Duties,职责分离)。
业务定义:职责分离是一种内控机制,要求将一项完整业务流程中的关键操作拆分给不同角色执行,防止单一个体同时拥有"发起"和"审批"的权力,从而降低舞弊和误操作的风险。
在第 1 篇中,我们讲过采购申请(PR)的审批流程:申请人提交后,需要经过主管审批、财务审批,才能进入采购执行阶段。这个流程之所以要设计多个审批节点,核心目的之一就是职责分离。
如果采购员既能提交 PR 又能审批 PR,他就完全可以给自己买一台不需要的电脑,然后自己点通过。系统里没有任何人能发现这个问题,直到审计时才会暴露。
SoD 在 ERP 系统中最常见的应用规则包括:
- 制单人不能审批自己的单据:采购员创建的采购订单,不能由自己审批。
- 录入人不能复核自己录入的数据:会计录入的凭证,需要由另一位会计或主管复核。
- 审批人不能执行被审批的操作:审批付款申请的人,不能是实际执行付款操作的人。
这些规则听起来是"常识",但在系统实现上需要明确的校验逻辑。以下是一段简化版的伪代码,展示如何在订单审批时检查 SoD:
function canApproveOrder(orderId, userId) {
const order = getOrderById(orderId);
// 检查功能权限:当前用户是否有审批权限
if (!hasPermission(userId, "ORDER_APPROVE")) {
return { allowed: false, reason: "无审批权限" };
}
// 检查职责分离:制单人不能审批自己的订单
if (order.createdBy === userId) {
return { allowed: false, reason: "不能审批自己创建的单据" };
}
// 检查数据权限:只能审批自己数据范围内的订单
if (!inDataScope(userId, order)) {
return { allowed: false, reason: "该订单不在您的数据范围内" };
}
return { allowed: true };
}
这段代码只有 16 行,但它同时覆盖了功能权限、职责分离和数据权限三个层面。在真实的 ERP 系统中,SoD 规则通常会以配置表的形式存在,方便业务人员根据公司制度灵活调整,而不是硬编码在程序里。
权限矩阵:把角色和权限的关系可视化
当角色越来越多、权限越来越细时,用文字描述"谁能做什么"就变得非常低效。这时候,权限矩阵是一个很好的工具。
业务定义:权限矩阵是一张以角色为行、以功能/数据权限为列的二维表格,用于可视化展示每个角色在系统中的完整权限配置。它是需求评审、权限梳理和审计检查的标准交付物。
以下是一个简化版的 ERP 权限矩阵模板,按业务模块拆分为两张表:
采购与销售模块:
| 角色 | 采购模块 | 销售模块 |
|---|---|---|
| 采购员 | 创建/查看 PO | 无 |
| 采购主管 | 审批 PO、查看报表 | 无 |
| 销售代表 | 无 | 创建 SO、查看客户 |
| 销售经理 | 无 | 审批 SO、查看报表 |
| 库管员 | 查看 PO | 查看 SO |
| 财务会计 | 查看 PO | 查看 SO |
| 财务经理 | 查看采购报表 | 查看销售报表 |
库存与财务模块:
| 角色 | 库存模块 | 财务模块 |
|---|---|---|
| 采购员 | 查看库存 | 无 |
| 采购主管 | 查看库存 | 查看应付 |
| 销售代表 | 查看可用库存 | 无 |
| 销售经理 | 查看库存 | 查看应收 |
| 库管员 | 入库/出库/盘点 | 无 |
| 财务会计 | 查看库存报表 | 录入凭证/对账 |
| 财务经理 | 查看库存报表 | 审批付款/查看总账 |
这两张表合起来,就是一张完整的角色权限蓝图。它的价值不在于有多详细,而在于把"谁能做什么"变成了一张可以讨论、可以评审、可以审计的图纸。当业务部门说"这个岗位好像权限不够"或者"这个人不该看到这个数据"时,可以直接在矩阵上定位到对应的格子里讨论。
在系统实现层面,RBAC 的核心数据表通常包含三张主表和两张关联表:
| 表名 | 作用 | 核心字段 |
|---|---|---|
| 用户表 | 存储系统账号 | user_id, username, dept_id, status |
| 角色表 | 存储角色定义 | role_id, role_name, role_type |
| 权限表 | 存储权限定义 | perm_id, perm_code, perm_name, resource_type |
| 用户角色关联表 | 用户与角色的多对多关系 | user_id, role_id |
| 角色权限关联表 | 角色与权限的多对多关系 | role_id, perm_id |
如果还需要支持数据权限,通常会在"角色权限关联表"中增加一个 data_scope 字段,取值可以是"本人"、"本部门"、"本部门及下属"、"全部"等。
常见权限场景的拆解
理论讲完,我们来看三个在真实 ERP 系统中最常见的权限场景。
场景一:跨部门数据隔离
销售部门 A 和销售部门 B 各自负责不同的客户群体。系统需要保证:A 部门的销售代表在查看客户列表时,只能看到 A 部门的客户,看不到 B 部门的。
实现思路:在客户主数据上增加"归属部门"字段。当销售代表查询客户列表时,系统自动在查询条件中追加 WHERE dept_id = 当前用户所属部门。这不是前端隐藏,而是后端查询层面的数据过滤。
如果销售经理需要查看多个部门的数据,可以给他配置"数据权限范围 = 本部门及下属",系统在查询时自动展开其管辖的所有部门 ID。
这个机制的关键在于:数据权限不是前端过滤,而是后端查询条件的自动注入。即使销售代表知道了其他部门客户的编码,直接通过接口查询,系统也会因为他的数据权限范围限制而返回空结果。
场景二:审批权限分级
在第 9 篇中,我们讨论了信用检查失败后的审批流程。不同金额的订单,需要不同级别的人审批。例如:1 万元以下由销售主管审批,1 万到 10 万元由销售经理审批,超过 10 万元需要财务总监审批。
实现思路:这不是传统的"角色绑定权限",而是"条件驱动权限"。系统在判断当前用户是否有审批资格时,除了检查角色,还要检查订单金额是否落在该角色的审批区间内。这种规则通常通过"审批矩阵"或"审批策略表"来实现,而不是写死在代码里。
function getApprovers(orderAmount) {
if (orderAmount < 10000) return ["销售主管"];
if (orderAmount < 100000) return ["销售经理"];
return ["财务总监"];
}
场景三:临时授权与代理
销售经理要休假一周,需要把手头的审批权限临时转给另一位同事。如果系统不支持临时授权,就只能把账号密码告诉对方,或者把对方的角色临时改成"销售经理"——两者都有明显的安全和审计风险。
实现思路:引入"代理授权"机制。销售经理可以在系统中指定一位代理人,设定代理的起止时间。在代理期间,代理人获得被代理角色的部分权限(通常可以限定范围,如"只代理审批权限,不代理查看报表权限")。代理关系到期后自动失效,不需要人工回收。
这在数据模型上通常表现为一张"代理关系表",记录 delegator_id、delegatee_id、start_time、end_time、scope 等字段。系统在校验权限时,先查用户自身的角色,再查其是否处于有效的代理关系中,两者取并集。
代理机制还有一个重要的审计要求:所有通过代理权限执行的操作,在系统日志中必须同时记录"操作人"和"代理人"两个字段。这样事后审计时,可以清楚地区分哪些操作是员工本人执行的,哪些是通过代理权限执行的。
总结:权限是内控的数字化表达
回到文章开头的那个场景:实习生点击了"审批通过",系统应该拒绝他。
这个拒绝的背后,不是一句简单的"你没权限",而是一整套权限设计在起作用:RBAC 模型告诉他没有这个角色的功能权限,SoD 规则告诉他即便有审批角色也不能审批自己的单据,数据权限告诉他这张单据不在他的可见范围内。
权限设计的本质,是把企业的管理制度和风险控制要求,翻译成系统里的校验规则。它不像状态机那样有明显的流转路径,也不像单据那样有具体的业务形态,但它在每一个按钮、每一次查询、每一次状态变更的背后默默工作。
我的判断是:一套好的权限系统,应该像空气一样存在——用户感受不到它的复杂,但缺了它,整个系统就会立刻陷入混乱。
下一篇,我们继续深入产品设计层面,聊聊如何把审批制度产品化:审批流设计。状态机定义了流转规则,权限定义了谁能参与,审批流则把"谁审批、按什么条件审批、审批不通过怎么办"这些业务制度变成可配置的系统能力。
往期回顾
- 业财通识27:状态机设计——用状态驱动业务流转
- 业财通识26:功能模块划分——如何把业务流程变成系统菜单
- 业财通识25:业财对账——打通业务与财务的最后一公里
- 业财通识24:银行对账——银行流水与账本的逐笔核对
- 业财通识23:应付对账——管住每一笔该付的钱
- 业财通识22:应收对账——确保每一笔钱都收回来
- 业财通识21:库存成本计价——FIFO、加权平均与标准成本
- 业财通识20:安全库存——科学补货的数学基础
- 业财通识19:可用库存——为什么账上有货却不能卖
- 业财通识18:调拨——多仓协同的物流调度
- 业财通识17:盘点——账实一致的最后防线
- 业财通识16:出库——从销售发货到领料消耗
- 业财通识15:入库——四种场景下的库存增加
- 业财通识14:应收账款——从开票到回款的风险管控
- 业财通识13:价格策略——多维定价与动态调整
- 业财通识12:一个客户,为什么会有三条记录?——CRM 作为主数据底座的三层模型
- 业财通识11:从开票到收款,企业如何收回每一分钱?
- 业财通识10:当货物发出,系统里发生了什么?
- 业财通识09:订单确认前,系统如何防止坏账风险?
- 业财通识08:企业赚钱的第一步,从"潜在客户"到"销售合同"
- 业财通识07:业财难点之"暂估入账"与冲销
- 业财通识06:什么是采购在途?它对库存预测的价值
- 业财通识05:商品世界的基石——SPU 与 SKU
- 业财通识04:万事俱备,如何优雅地"打款"给供应商?
- 业财通识03:收到供应商账单,能直接付款吗?
- 业财通识02:当货物上门,系统里发生了什么?
- 业财通识01:企业花钱的第一步,从"购物清单"到"法律合同"
关于十三Tech
资深服务端研发工程师、架构师、AI 编程实践者。
希望能和大家一起写出更优雅的代码。
如果正好有需要,可以支持一下
这里放了一个阿里云活动入口。你本来就打算了解这类服务的话,可以从这里进去;如果有推广收益,我会优先用来覆盖服务器、域名和维护成本。
