概览 —— 本功能的作用
角色 与 团队 只是用于给人员分类的 信息性标签。真正开放画面与操作访问权限的,只有 级别(user_level)。用户能看到什么、能保存或删除什么,100% 由 级别 决定;角色(“DBA”“DevOps”“安全”)和团队(“基础设施团队”“安全团队”)只是 显示与组织用的标签,不授予任何权限。
这样可以保持权限模型的简单。要给某人管理员权限,就 提升其级别 —— 而不是修改其角色。给用户打上“安全”标签并不会打开安全类画面;其级别必须达到员工(5)或以上。
本文涵盖三部分内容:
- 角色(
infr_role) —— 六个职务标签(DBA、DevOps、安全、管理员、员工、开发者)。 - 团队(
infr_team) —— 组织单位,用于用户归属与资产负责人。 - 负责人选择器 —— 一个合一的下拉框,可指定用户或团队,其唯一事实来源(SOT)是
App\Support\Owners。实例、硬件与存储上的“负责人”字段都使用它。
访问控制 = 仅级别(
user_level)。角色和团队在程序上不开放任何权限。级别体系参见 访问级别(RBAC)。
何时使用
- 新人入职,为其标注职务(角色)与归属(团队)。
- 组织调整 —— 新建团队或更换团队负责人。
- 为服务器、硬件或存储资产指定 负责人(个人或团队),让“这项资产由谁负责”一目了然。
- 审计/交接:查询“这项资产的负责人是谁”。
页面结构
/admin/users(管理 ▸ 用户与角色)分为两个标签页。
| 标签页 | 内容 |
|---|---|
| 用户 | 用户列表(登录 ID · 姓名 · 邮箱 · 角色 · 团队 · 级别 · 状态 · 最近登录)+ 弹窗 CRUD |
| 团队 | 团队列表(名称 · 颜色 · 负责人 · 说明 · 成员数)+ 弹窗 CRUD |
用户编辑弹窗中有级别/角色/团队选择框,其下方是这样一段提示:
级别决定访问权限 — 演示(只读)、员工(操作)、管理员(全部)。角色只是显示标签。
使用方法
角色 —— 职务标签
角色主表共六条记录,由 InfrRoleSeeder 按 key 幂等 upsert。
| key | 标签 | 颜色令牌 | seq |
|---|---|---|---|
dba |
DBA | info | 10 |
devops |
DevOps | accent | 20 |
security |
安全 | crit | 30 |
admin |
管理员 | warn | 40 |
staff |
员工 | muted | 50 |
developer |
开发者 | good | 60 |
- 一个用户 最多只有一个 角色(
users.role_id,可为空)。也可以不设置。 - 角色仅作为标签与颜色出现在顶栏用户下拉框和列表徽标中。
- 显示标签由
App\Support\RolePolicy::roleLabel($roleId)解析,带 60 秒缓存(修改后请调用RolePolicy::flush())。
角色
admin(标签“管理员”)与 管理员级别(10)毫无关系。角色只是标签,因此角色为“管理员”但级别为演示(1)的用户什么都保存不了。反之,级别为 10 的管理员即使角色为空,也拥有全部管理权限。
团队 —— 组织单位
团队可在 团队 标签页自由 CRUD。四个种子团队如下:
| 名称 | 颜色 | 说明 |
|---|---|---|
| 基础设施团队 | accent | 服务器/网络/存储运维 |
| 安全团队 | crit | 检测 · 响应 · 封禁 |
| 开发团队 | good | 服务开发 · 部署 |
| DBA 团队 | info | 数据库运维 · 调优 |
团队编辑字段:
| 字段 | 含义 |
|---|---|
名称(name) |
必填。团队名称 |
颜色(color) |
徽标色调(accent/crit/good/info/muted …) |
负责人(lead_user_id) |
一名用户(可选) |
说明(description) |
自由文本 |
启用(is_active) |
停用的团队不会出现在负责人选择器中 |
- 一个用户 最多只属于一个 团队(
users.team_id,可为空)。 - 删除团队时: 该团队成员的
team_id会被置为null(用户仍在,只是变为未分配)。通过owner_type='team'引用该团队的资产是另一回事,因此资产本身不会被删除。
负责人选择器 —— 用户与团队合一
资产(服务器实例、硬件、存储)都有一个“负责人”字段,可以 是个人,也可以是团队。这个合一选择器的规则位于 App\Support\Owners(SOT)。
存储形态 = 两个列
资产表用两个列保存负责人。
| 列 | 含义 |
|---|---|
owner_id |
用户或团队的整数主键 |
owner_type |
'user' 或 'team' |
前端交换形态 = 单个 ref 字符串
浏览器(表单、筛选)把 owner_type 与 owner_id 作为一个字符串来交换。
user:5 → user id 5
team:3 → team id 3
引导选项(Owners::options())返回两组数据以供 optgroup 渲染(20 秒缓存)。已关闭的用户(is_close=1)与停用的团队(is_active=0)会被排除。
{
"users": [ { "ref": "user:5", "id": 5, "name": "Jane Doe" } ],
"teams": [ { "ref": "team:3", "id": 3, "name": "Infra Team" } ]
}
服务端的拆分/还原
| 辅助方法 | 行为 |
|---|---|
Owners::split($ref) |
"user:5" → ['user', 5]。格式错误 → ['user', 0](= 未分配) |
Owners::ref($ownerId, $ownerType) |
(5, 'user') → "user:5"。ownerId 为空 → null |
Owners::resolveName($ownerId, $ownerType) |
用于卡片/列表的显示名称(没有则为 null,20 秒缓存) |
Owners::flush() |
用户/团队变更后使缓存失效 |
例如实例的保存控制器是这样处理的:
// 负责人 ref("user:5"/"team:3")→ owner_id + owner_type
[$ownerType, $ownerId] = Owners::split($request->input('owner_ref'));
$payload['owner_id'] = $ownerId ?: null;
$payload['owner_type'] = $ownerId ? $ownerType : 'user'; // 未设置时默认为 'user'
按负责人 筛选 时使用同一个 ref —— 列表查询用 split() 拆分 owner 参数(user:5/team:3),再收敛为 WHERE owner_id = ? AND owner_type = ?。
负责人选择器在 实例、硬件 与 存储与文件 各资产画面上被原样复用。把团队指定为负责人,可以让整个团队站在责任线上(任何一人离开都不受影响)。
头像 · 首字母 · 颜色
标签旁显示的头像由 RolePolicy 依据姓名生成。
- 首字母(
initials):中日韩/韩文 → 取首个字符;拉丁文姓名 → 取首词与末词的首字母(2 个字符)。 - 色相(
hue):crc32(name) % 360,得到稳定的 HSL 背景色。 - 级别色调(
tierTone):管理员(≥10)= crit · 员工/操作(≥2)= accent · 演示 = muted。
数据来源
| 表 | 用途 |
|---|---|
infr_role |
角色主表(6 个信息性标签)—— key·name·color·seq·is_active |
infr_team |
团队主表 —— name·color·lead_user_id·description·is_active·seq |
users |
用户 —— user_level(决定访问权限)、role_id(可为空)、team_id(可为空) |
| 资产表 | 负责人以 owner_id + owner_type(user/team)两个列保存(实例/硬件/存储等) |
SOT 辅助类:
App\Support\Owners—— 合一的负责人选择器(options/split/ref/resolveName/flush)。App\Support\RolePolicy—— 级别标签/色调、角色标签(60 秒缓存)、头像首字母/颜色。
访问权限
- 进入整个画面仅限管理员。
/admin/*由分组中间件infr.level:10保护,UserController/TeamController自身也会再次检查会话中的user_level >= 10(RolePolicy::ADMIN)。演示(1)与员工(5)会得到 403。 - 级别阈值(参考):写入 ≥ 2(
RolePolicy::WRITE,拦截演示)· 管理 ≥ 10(ADMIN)· 开发/可观测性 ≥ 9(DEV,含管理员 10)。 - 保存用户时,
user_level被限制为1,5,10之一。 - 每一次用户/团队变更都会审计记录到
infr_audit_log(team.create/team.update/team.delete、用户保存/删除等)。
保护机制(最后一名管理员保护)
| 情形 | 结果 |
|---|---|
| 降级/停用/删除最后一名启用的管理员 | 拒绝 —— “最后一名启用的管理员不能降级、停用或删除。” |
| 降低或停用自己的管理员级别 | 拒绝 —— “不能降级或停用自己的管理员权限。” |
| 删除自己 | 拒绝 —— “不能删除自己。” |
| 登录 ID 已被使用 | 拒绝 —— “该登录 ID 已被使用。” |
排查与常见问题
Q. 我给某人分配了“安全”角色,但安全类画面打不开。 角色只是标签,不开放任何权限。安全类画面(以只读为主)需要的是 级别 —— 请把该用户提升到员工(5)或以上。参见 RBAC 级别。
Q. 负责人下拉框里少了某个用户/团队。
已关闭的用户(is_close=1)与停用的团队(is_active=0)会被排除在选择器之外。请重新启用该用户/团队;若列表内容陈旧,请等待 20 秒缓存过期(Owners::flush() 可立即失效)。
Q. 删除团队后,团队里的人会消失吗?
不会。用户仍在,只是其 team_id 变为 null(未分配)。资产的 owner_type='team' 引用是另一回事,因此资产也不会被删除(只是负责人显示变为空)。
Q. 我保存了负责人,却不显示名称。
resolveName() 有 20 秒缓存。如果刚刚重命名过,稍等片刻就会刷新。另请确认 ref 格式是 user:5/team:3 —— 格式损坏时 split() 会视为未分配(user:0)。
Q. 一个用户可以有两个角色吗?
不可以。role_id 是单一值(可为空)。如果还需要组织层面的分组,请使用团队(同样是单一值)。
相关内容
- 访问级别(RBAC) —— 唯一真正决定访问权限的维度
- 用户与团队控制台 ——
/admin/users的 CRUD 详情 - 实例 —— 使用负责人选择器的旗舰资产画面
- 硬件 —— 物理资产负责人
- 存储与文件 —— 存储资产负责人
- 审计日志 —— 用户/团队/负责人的变更历史
