组织架构与权限体系 --- 资深前端视角教学手册
本文档基于 newhome-web 实际源码逐行阅读而成,不臆测每一行业务含义。所有路径均可在仓库内
read_file验证。阅读对象:刚接手该项目、要理解"组织架构 / 角色 / 岗位 / 权限"如何前后端协同工作的前端工程师。
一、给你的总览(30 秒读完一张图)
#mermaid-svg-mINaBeldTIoTcwif{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-mINaBeldTIoTcwif .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-mINaBeldTIoTcwif .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-mINaBeldTIoTcwif .error-icon{fill:#552222;}#mermaid-svg-mINaBeldTIoTcwif .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-mINaBeldTIoTcwif .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-mINaBeldTIoTcwif .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-mINaBeldTIoTcwif .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-mINaBeldTIoTcwif .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-mINaBeldTIoTcwif .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-mINaBeldTIoTcwif .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-mINaBeldTIoTcwif .marker{fill:#333333;stroke:#333333;}#mermaid-svg-mINaBeldTIoTcwif .marker.cross{stroke:#333333;}#mermaid-svg-mINaBeldTIoTcwif svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-mINaBeldTIoTcwif p{margin:0;}#mermaid-svg-mINaBeldTIoTcwif .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-mINaBeldTIoTcwif .cluster-label text{fill:#333;}#mermaid-svg-mINaBeldTIoTcwif .cluster-label span{color:#333;}#mermaid-svg-mINaBeldTIoTcwif .cluster-label span p{background-color:transparent;}#mermaid-svg-mINaBeldTIoTcwif .label text,#mermaid-svg-mINaBeldTIoTcwif span{fill:#333;color:#333;}#mermaid-svg-mINaBeldTIoTcwif .node rect,#mermaid-svg-mINaBeldTIoTcwif .node circle,#mermaid-svg-mINaBeldTIoTcwif .node ellipse,#mermaid-svg-mINaBeldTIoTcwif .node polygon,#mermaid-svg-mINaBeldTIoTcwif .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-mINaBeldTIoTcwif .rough-node .label text,#mermaid-svg-mINaBeldTIoTcwif .node .label text,#mermaid-svg-mINaBeldTIoTcwif .image-shape .label,#mermaid-svg-mINaBeldTIoTcwif .icon-shape .label{text-anchor:middle;}#mermaid-svg-mINaBeldTIoTcwif .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-mINaBeldTIoTcwif .rough-node .label,#mermaid-svg-mINaBeldTIoTcwif .node .label,#mermaid-svg-mINaBeldTIoTcwif .image-shape .label,#mermaid-svg-mINaBeldTIoTcwif .icon-shape .label{text-align:center;}#mermaid-svg-mINaBeldTIoTcwif .node.clickable{cursor:pointer;}#mermaid-svg-mINaBeldTIoTcwif .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-mINaBeldTIoTcwif .arrowheadPath{fill:#333333;}#mermaid-svg-mINaBeldTIoTcwif .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-mINaBeldTIoTcwif .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-mINaBeldTIoTcwif .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-mINaBeldTIoTcwif .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-mINaBeldTIoTcwif .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-mINaBeldTIoTcwif .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-mINaBeldTIoTcwif .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-mINaBeldTIoTcwif .cluster text{fill:#333;}#mermaid-svg-mINaBeldTIoTcwif .cluster span{color:#333;}#mermaid-svg-mINaBeldTIoTcwif div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-mINaBeldTIoTcwif .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-mINaBeldTIoTcwif rect.text{fill:none;stroke-width:0;}#mermaid-svg-mINaBeldTIoTcwif .icon-shape,#mermaid-svg-mINaBeldTIoTcwif .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-mINaBeldTIoTcwif .icon-shape p,#mermaid-svg-mINaBeldTIoTcwif .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-mINaBeldTIoTcwif .icon-shape .label rect,#mermaid-svg-mINaBeldTIoTcwif .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-mINaBeldTIoTcwif .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-mINaBeldTIoTcwif .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-mINaBeldTIoTcwif :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} editPositionUser
updateOrgPosition permissionOperations
保存 roleId
request.ts 自动注入
后端按 roleId 收数据范围
登录时拿到
positionIdList 多对多
getUserPermissionsByStaffId
enrich 聚合
useCanEdit / canDo
员工档案 home_org_user 姓名/手机/工号/入职 (组织架构-人员管理创建)
后台账号 nbstaffs 登录身份 (账号管理创建)
岗位 home_org_position 绑菜单+按钮权限 (岗位管理授权)
菜单项 (含 operation 两位字符串) (后端权限字典)
localStorage.roleId
所有 HTTP 请求
后端范围过滤
角色 nbroles 岗位合集 + 数据范围标签
公司/区域 home_org_company
职能中心 home_org_institution
部门 home_org_dept (数据存在/树不渲染)
登录后 app.tsx getInitialState
currentUser.permissions currentUser.buttonPermissions
菜单 + 按钮
一句话点题:
- 权限绑在「岗位」上 。员工档案 ↔ 岗位是实线(真正的权限来源),角色只是"身份标签 + 数据范围"。
- 菜单权限表名我没在前端代码里看到实证------图上只写"菜单项",不写表名,避免幻觉。
- 角色 = 身份的"虚"路径 :菜单配置权已被收回,仅靠
request.ts把roleId透传到后端参与"能看哪些公司/项目"的范围收口。
二、推荐切入路径(按这个顺序读,阻力最小)
| 顺序 | 文件 | 一句话目标 |
|---|---|---|
| ① | src/ctx/organization.ts |
看清全局状态到底是干啥的 |
| ② | src/layouts/AreaLayoutOrganization/index.tsx |
左侧树的容器,明白谁是 Provider |
| ③ | src/components/AreaSelectorOrganization/index.tsx |
"只读版"左侧树,看清 orgLevel 和数据形状 |
| ④ | src/pages/organization/orgManagement/component/AreaSelectorOrganization/index.tsx |
"可编辑版"左侧树(CRUD 全在树上) |
| ⑤ | src/pages/organization/BasicInfo/index.tsx |
整页布局,知道 Tab 是怎么组合子页面的 |
| ⑥ | src/pages/organization/orgManagement/index.tsx |
"组织架构"Tab 的页面 --- 人员分配 + 导入 |
| ⑦ | src/pages/organization/orgManagement/component/OrgTableByLevel.tsx |
看表格按 orgLevel 怎么切不同的查询条件 |
| ⑧ | src/pages/organization/positionManagement/index.tsx |
权限的核心!所有菜单/按钮权限都靠这里配置 |
| ⑨ | src/pages/accountManagement/role/index.tsx |
看角色怎么挂岗位(一行关键注释:权限配置已迁移到岗位管理) |
| ⑩ | src/hooks/useCanEdit.ts + src/hooks/useButtonPermission.ts |
前端是怎么把 currentUser.permissions 翻译成显隐的 |
| ⑪ | src/app.tsx |
看 getInitialState 如何把后端返回的扁平权限构建成树,再喂给菜单和按钮 |
千万不要 先去看
accountManagement/account/账号列表------它和"权限如何生效"关系不大,反而会混淆你。
三、组织架构数据模型(前后端共享一个心智模型)
3.1 三层节点:company → institution → position
数据形状(见 src/@types/API.d.ts:52-67):
ts
export interface OrganizationMenu {
id: number
name: string
children: OrganizationMenu[] // 子公司(公司下面)
institutionList: OrganizationMenu[] // 职能中心(公司下面)
deptVoList: OrganizationMenu[] // 部门(前端树未渲染,仅作数据承载)
positionVoList: OrganizationMenu[] // 岗位(挂在职能中心下面)
orgLevel: string // 'company' | 'institution' | 'position' | 'dept'
companyId: string
parentId?: number
institutionId?: number
deptId?: number
}
注意
deptVoList在前端树里并不渲染 ------本项目组织树只展示公司 / 职能中心 / 岗位三级,部门只是个数据兜底。comments at orgManagement/.../AreaSelectorOrganization里也写了"虽然业务上说只要中心和岗位,但保留部门逻辑以防万一"。
3.2 关键枚举 orgLevel
教学要点:所有"按层级不同做不同事"的代码,全部 switch (item.orgLevel):
company(公司/区域)---organizationType再细分 1=区域 2=公司institution(职能中心,原"机构")---type再细分 1=集团级 2=公司级 3=项目级position(岗位)--- 叶子节点
教学提示:观察
getTagName(item)在两个AreaSelectorOrganization文件里几乎一致------这种视觉规则要抽公共组件才优雅,目前只是巧合一致,将来 BUG 多从这里出。
3.3 路由层级(看清再下手)
.umirc.ts:556-590:
ts
// 旧版扁平页面(保留作兼容)
'/organization/orgManagement_2' → orgManagement/index
'/organization/companyManagement' → companyManagement/index
'/organization/deptManagement' → deptManagement/index
'/organization/positionManagement' → positionManagement/index ← 权限核心
'/organization/institutionManagement' → institutionManagement/index
// 新版带 layout
'/organization' ← AreaLayoutOrganization
├── /organization/orgManagement → BasicInfo/index ← 主入口(Tab 页)
├── /organization/basicInfo → BasicInfo/index
├── /organization/projectManagement → projectManagement/index
└── /organization/userManagement → userManagement/index
教学要点:BasicInfo/index.tsx 是两套路由都会指向的"主 Tab 页" ,里面包含"组织架构 / 关联社区"两个 Tab;orgManagement_2 是无 Tab 的扁平后备路径。建议新代码统一走 BasicInfo 这条线。
四、Context 怎么传组织状态
src/ctx/organization.ts 整文件 14 行:
ts
import { createContainer } from 'unstated-next'
const OrganizationContext = createContainer(() => {
const [organization, setOrganization] = useState<any>({})
return { organization, setOrganization }
})
- Provider 出现的位置只有两处 :
src/layouts/AreaLayoutOrganization/index.tsx--- 给了所有人(包括AreaSelectorOrganization)。src/pages/organization/BasicInfo/index.tsx--- 又包了一层!这是历史包袱(带 layout 的路由之外还有旧路径orgManagement_2也指向BasicInfo,没 layout 给 Context)。
- 读取方式
OrganizationContext.useContainer()。 - 作用:仅承担"当前选中的组织节点 + 它的关键信息"(id / orgLevel / companyId / institutionId),不是真"全局组织数据"。
反模式点拨:很多 dev 看到 Context 以为里面缓存了组织树数据------其实完全没缓存 ,树数据每次走 react-query(见
useQuery({ queryKey: ['/home_org_company/getCompanyTreeWithDeptAndPositionByUserId', userId] }))。Context 只负责"你点的是哪一个"。
五、左侧组织树:可编辑版 vs 只读版(这是组织的"灵魂 UI")
5.1 只读版:src/components/AreaSelectorOrganization/index.tsx
- 数据源:
getOrgTreeWithDeptAndPositionByUserId({ userId }) - 用
defaultExpandAll全展开;选中节点的key来自${orgLevel}_${id}_${name}_${level}_${parentId|companyId|institutionId}拼接。 - 唯一的副作用:父节点(
parentId == 0)渲染时自动setOrganization(treeNode.info)--- 等同于"页面进来默认选中根公司"。
5.2 可编辑版:src/pages/organization/orgManagement/component/AreaSelectorOrganization/index.tsx
这是组织模块的核心交互组件,CRUD 全在这个文件里。读它时把目光集中在 4 个动词 + 1 个特殊动作:
| 动词 | 触发 | 关键 API |
|---|---|---|
handleAdd(e, item) |
选中态出现 + 号 | addOrgCompany / addOrgInstitution |
handleEdit(e, item) |
选中态出现 ✎ | updateOrgCompany / updateOrgInstitution |
handleDelete(e, item) |
选中态出现 🗑 + Popconfirm | deleteOrgCompany / deleteOrgInstitution |
handlePositionAssign(e, item) |
仅 institution 节点 + 号 | getOrgPositionList + setOrgEditInstitutionPosition |
Modal 是一个,但内部根据 organizationType 决定是新增公司 / 区域 / 职能中心(数字 1/2/3 字典式读法)。
一个不容易读出来的设计点:"sessionStorage 层级标识"
文件里有这段被注释掉的 useEffect(treeData 变化时通过 sessionStorage 的 marker 试图恢复选中):
ts
const STORAGE_KEY = 'orgTree_selected_marker'
const buildMarker = (info) => ({ orgLevel, id, companyId?, institutionId? })
const matchByMarker = (info, marker) => { /* 按 orgLevel + id + 父子ID 比对 */ }
目前是注释掉的 ------只在 unload / pagehide 里清理。教学意义:可以告诉新人,"这里曾经想做'刷新保留选中',但作者主动放弃了,可能因为后端 id 重用问题或复杂度"。
标题中的 Tag 颜色(小心误导)
ts
const getTagColor = (level) => {
switch (level) {
case 'company': return 'blue'
case 'institution': return 'blue'
case 'position': return 'blue'
default: return 'blue'
}
}
全部
blue!这是 dead code 或者将来扩展用的------读别人代码时不要被分支结构迷惑,每个 case 都返回相同值说明目前只有视觉等级(文字"公司"/"职能中心"),颜色并未真正差异化。这是很好的"找无效代码"练习素材。
5.3 表格按层级变化:OrgTableByLevel.tsx
ts
if (organization.orgLevel === 'institution') {
queryParams.institutionId = organization.id
if (selectedPositionId) queryParams.positionId = selectedPositionId
} else if (organization.orgLevel === 'position') {
queryParams.positionId = organization.id
} else if (organization.orgLevel === 'dept') {
queryParams.deptId = organization.id
}
教学要点:前端按层级把不同 id 塞进不同字段 ,但表格只能看到 institution + position 两层(公司节点不查)。dept 分支保留是为了"接口适配不重构、不就现在删"。
六、核心问题:权限到底绑在哪?------ 岗位
这是整个体系的关键,请把这一节读三遍。
6.1 把代码里"权限配置已迁移到岗位管理"找出来
打开 src/pages/accountManagement/role/index.tsx,搜"权限配置已迁移到岗位管理"------你会发现 整个权限树 UI 都被注释掉了:
ts
// ========== 以下权限配置相关代码已迁移到岗位管理 ==========
// import { getUserPermissionsByRoleId } from '@/services/api'
// const RoleForm = () => <TreeSelect treeCheckable ... />
// const permissionToTreeData = () => ...
// ========== 权限配置相关代码结束 ==========
新增/编辑角色的 handleSubmit 里只剩:
ts
const submitData = {
name: values.name,
positionIdList: values.positionIdList || [], // 关键:只传岗位列表
developerId: localStorage.getItem('developerId'),
// permissionOperations, // 权限配置已迁移到岗位管理
}
历史已经选择了"岗位绑权限,角色 = 岗位集合"的模型。后续如果你被业务方问"权限配在哪",永远回答:"权限配在岗位里,角色选择岗位就拿到了所有权限"。别说不清。
6.2 角色页面表单里只能"多选岗位"
tsx
<FormItem label="岗位" name="positionIdList" rules={[{ required: true }]}>
<Select allowClear mode="multiple" maxTagCount={3} placeholder="请选择岗位">
{positionList.map(item => <Option key={item.id} value={item.id}>{item.name}</Option>)}
</Select>
</FormItem>
接口:POST /nbroles/save,body 含 positionIdList: number[](api.ts:701-710)。
反过来查:GET /nbroles/selectPositionListByRoleId?staffRoleId= 在编辑时回显已选岗位。
6.3 权限"真正的归宿":src/pages/organization/positionManagement/index.tsx
这是全项目最复杂的页面,承担三种职责:
- 岗位的 CRUD(新增/修改/删除);
- 菜单权限的可视化配置(左侧岗位 + 右侧模块 Tab + 操作 checkbox 树);
- 按钮级权限的可视化配置(每个菜单节点下挂按钮集合);
- 保存时一次性回传给
updateOrgPosition({ permissionOperations: [...] })。
读这页时按这段代码顺序读即可:
ts
// 1) 左侧:搜索岗位、点选
loadPositionList -> getOrgPositionList
// 2) 右侧:加载当前岗位的菜单 + 按钮权限
loadPositionPermissions(positionId)
-> getMenuByPositionId(positionId) // API:POST /home_org_position/getPermissionsByPositionId
// 3) 渲染:根据 currentUser.permissions(前端已聚合的可视范围)建树
buildPermissionTree(perms) -> 按 pid=0 找根模块 -> 渲染 Tab
// 4) 勾选/取消:togglePermission(menuId, 'edit'|'view', childrenIds)
togglePermission 会级联父 ↔ 子
// 5) 保存:组 permissionOperations + buttonPermissions 推回后端
handleSavePermissions
6.3.1 极重要的"两位 operation"
src/utils/utils.ts:104-110 与 src/utils/permission.ts:189-200:
ts
export const hasOperationPermission = (permission, operationType) => {
// operation 格式: 第一位-编辑权限, 第二位-查看权限
const i = operationType === 'edit' ? 0 : 1
return permission.operation[i] === '1'
}
Permission.operation = '11' | '10' | '01' | '00':
11--- 可编辑、可查看01--- 仅查看10--- 仅编辑(罕见场景)00--- 无权(前端树里根本不出现)
6.3.2 父节点会自动回填 "01"
ts
const parentIds = getParentPermissionIds(permissionId, currentUser.permissions)
parentIds.forEach(parentId => {
if (!permissionsConfig[parentId] && !addedParentIds.has(parentId)) {
addedParentIds.add(parentId)
finalOperations.push({ permissionId: parentId, operation: '01' }) // 默认给查看权限
}
})
教学要点:勾选子节点时,父节点会被自动加 01(只看)。这是 UX 细节:"你点子菜单,前面的菜单路径也亮起来"。
6.3.3 切换模块的"未保存提示"
ts
Modal.confirm({
title: '权限未保存',
content: '当前模块权限已更改,是否保留改动再切换?',
okText: '保留改动并切换',
cancelText: '放弃并切换',
onOk: // 更新 moduleSnapshotsRef,但不发请求------真正的"保存"必须点顶部"保存授权"
onCancel: // 回滚到快照
})
教学要点:用 ref 维护"每个模块的已保存快照" ,纯前端 diff;真实的 commit 由"保存授权"按钮统一走 updateOrgPosition 一次写回。这种"区分模块、回滚快照、不抢用户主动保存意图"的设计是值得借鉴的。
七、前端怎么把权限翻译成显隐(菜单 + 路由 + 按钮)
7.1 启动时:app.tsx 的 getInitialState
ts
permissions = await getUserPermissionsByStaffId(staffId)
const resp = await getPermissionConfig() // 全量菜单
allPermissions = Array.isArray(resp) ? resp : (resp.data ?? [])
parentIdSet = new Set() /* 来自 allPermissions 的 pid 集合 */
permissions.map(p => ({
...p,
isParentOrigin: parentIdSet.has(p.id),
isLeafNode: !parentIdSet.has(p.id),
}))
currentUser.permissions = enriched
currentUser.buttonPermissions = buildButtonPermissionConfigFromPermissions(enriched)
menuData = getMenuDataFromPermissions(currentUser.permissions)
要点:
- 后端只返回"该用户有权的菜单" (
getPermissionsByUserId),但getPermissionConfig返回的是"全量菜单"------前端用它反推isParentOrigin/isLeafNode,从而正确分组父子。 buttonPermissions是按页面path聚合的Record<path, Record<action, boolean>>,源头来自后端Permission.buttonPermissions字段。getMenuDataFromPermissions调arrayToTree,然后有一步markDisabledWhenNoChildren--- 当一个原本是父节点 但因为没权限被过滤成"空 children"时,菜单项disabled=true selectable=false,避免点了进死页面。
7.1·补 深度拆解:两个接口分别返回了什么?为什么要"反推 isLeafNode"?
这是新人最容易卡住的地方------"明明
getUserPermissionsByStaffId已经拿到菜单了,为啥还要多调一次getPermissionConfig?"
7.1·补.1 两个接口返回的内容完全不同
| 接口 | 路径 | 返回 | 用途 |
|---|---|---|---|
getUserPermissionsByStaffId(staffId) |
GET /nbroles/getPermissionsByUserId?userId= |
当前用户有权的菜单(只下发有权的) | 渲染菜单 + 鉴权 |
getPermissionConfig() |
GET /nbroles/getPermissions |
全量菜单字典(所有用户可能用到的) | 反推"谁曾经是父节点" |
两者不能互相替代,原因用一个具体 BUG 案例说清楚 ↓
7.1·补.2 一个真实的孤儿节点 BUG
全量菜单字典长这样:
系统管理 (id=1, pid=0)
├── 组织管理 (id=10, pid=1)
│ └── 人员管理 (id=100, pid=10) ← 叶子
└── 账号管理 (id=20, pid=1)
└── 账号列表 (id=200, pid=20) ← 叶子
当前用户只对"人员管理"有查看权,其他都没权。
后端 getUserPermissionsByStaffId 只会返回 1 条:
ts
[{ id: 100, pid: 10, name: '人员管理', operation: '01' }]
⚠️ 问题来了 :id=100 的 pid=10 在返回数组里找不到父节点(id=10 没被返回)!
看 utils/utils.ts 的 arrayToTree ------当 parentId 找不到父节点时,节点会被当成"根节点"(tree.push(node))。
结果 :菜单里直接挂出一个孤儿节点"人员管理",没有"系统管理 / 组织管理"做面包屑。
7.1·补.3 "反推 isLeafNode" 的真意
根本问题:你没法通过"用户有权的菜单"反推谁是父节点、谁是叶子节点------因为父节点本身可能没权(被过滤了)。
解决办法 :用"全量菜单字典 "里"所有被人引用过的 pid"来反推:
ts
// app.tsx:152-170
const parentIdSet = new Set<number | string>()
allPermissions.forEach((p: any) => {
if (p && (p.pid || p.pid === 0)) {
parentIdSet.add(p.pid) // 收集所有"当过爹"的 id
}
})
// 给当前用户有权的菜单打标
const enriched = permissions.map(permission => ({
...permission,
isParentOrigin: parentIdSet.has(permission.id), // 这个 id 曾经是别人父节点吗?
isLeafNode: !parentIdSet.has(permission.id), // 它本身是叶子节点吗?
}))
举例(接上面 BUG 案例):
id=1(系统管理):在全量字典里被人pid=1引过 →parentIdSet.has(1) === true→isLeafNode: falseid=10(组织管理):在全量字典里被人pid=10引过 →parentIdSet.has(10) === true→isLeafNode: falseid=100(人员管理):没人 把它当pid→isParentOrigin: false→isLeafNode: true✅
"反推"的真意:用"别人都引用过谁"这条信息,反过来标记"谁是父谁是叶"。
7.1·补.4 isLeafNode 后续被三处使用
| 使用点 | 文件:行 | 怎么用 | 作用 |
|---|---|---|---|
ProLayout component 字段 |
app.tsx:91 |
item.component = item.isLeafNode |
父节点 = 容器(不可点)、叶子 = 页面(可点) |
| 禁用空 children 的父菜单 | app.tsx:102-105 markDisabledWhenNoChildren |
如果"原本是父节点但有权限过滤后 没子项" → disabled=true selectable=false |
防止点了父节点跳死页 |
| 路由级 edit 权限匹配 | utils/utils.ts:32-35 havePermission |
if (!permission.isLeafNode) return false |
父节点的 operation 不参与路由编辑权匹配------这是关键! |
第 3 点尤其重要:useCanEdit 走的是"找到匹配当前 path 的 permission 项,再用 operation[0] 判断 edit 位"。但 havePermission 故意排除父节点参与匹配------否则点"系统管理"父菜单时也会被判为有 edit 位,逻辑就乱了。
7.1·补.5 没了 getPermissionConfig 会爆什么
如果完全用当前用户权限自己推父节点 (不调 getPermissionConfig),看 app.tsx:165-167 的降级逻辑:
ts
permissions.forEach((p: any) => {
if (p && (p.pid || p.pid === 0)) parentIdSet.add(p.pid)
})
反例 1 :用户只有"人员管理"(pid=10),parentIdSet 只有 {10},id=10 标成"当过爹"------但 id=10 不在当前数组里,根本不会被打标。结果:树里仍然出现孤儿。
反例 2 :用户只有"组织管理"(id=10),parentIdSet={10},然后 permissions.map 标 id=10.isLeafNode = !true = false ✅------这次凑对了,但只是巧合。
原作者完全知道 这个降级方案有缺陷,注释里写明:"降级用当前权限推断(可能把无权限的父节点误判为叶子) "。所以
getPermissionConfig是关键依赖,不是冗余调用。
7.1·补.6 性能取舍
多调一次 getPermissionConfig 看起来浪费,但只在登录初始化时调一次 ,然后缓存到 currentUser.permissions 全局共享。从实际体验看:
- 单次登录多 1 个 HTTP 请求(< 100ms)
- 换来
isLeafNode100% 正确 - 换来
markDisabledWhenNoChildren100% 正确
结论 :这个性能 trade-off 是值得的。如果未来 getPermissionConfig 变得超级慢(> 1s),可以考虑:
- 让后端在
getUserPermissionsByStaffId里直接带一个isParentOrigin字段回来 - 或前端用 Web Worker 缓存全量字典到 IndexedDB
但目前 100ms 量级不值得优化。
7.2 菜单渲染:src/layouts/BasicLayout/index.tsx
menuDataRender={(menuData) => filterWelcomeMenus(initialState?.menuData || menuData)} ------ 菜单本身就是 currentUser.permissions 转换出来的,没权限的菜单根本不会出现。
7.3 路由权限判定:src/utils/utils.ts 的 havePermission
ts
export const havePermission = (permissions, path) => {
// 1. 精确匹配 → 2. 前缀匹配(父菜单的 path 也能访问子路由)
// 注意:isLeafNode=false 的不参与编辑权限匹配(!)
}
7.4 页面级"能不能进入":useCanEdit
ts
export default function useCanEdit() {
const { initialState } = useModel('@@initialState')
const currentUser = initialState?.currentUser
const pathname = location.pathname
const permission = currentUser && havePermission(currentUser.permissions, pathname)
return permission && permission.operation.startsWith('1') // 第一位是 '1' = 有编辑权
}
重要 :这只判断"当前路径是否有 edit 位"。查看权限(
01)没有 propagate 给路由级------也就是说你进入页面的判断只看"是否拿到了这个页面",不看具体操作;要具体到按钮是否可点,看下一节。
7.5 按钮级:useButtonPermission(最值得带新人读的一段)
src/hooks/useButtonPermission.ts:
ts
const buttonPermissionConfig = useMemo((): ButtonPermissionConfig | undefined => {
const raw = currentUser?.buttonPermissions
if (!raw) return undefined // ① 没下发 → undefined,降级放行
try {
const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw
if (!parsed || Object.keys(parsed).length === 0) return undefined // ② 空配置 → undefined,也放行
return parsed
} catch { return undefined }
}, [currentUser?.buttonPermissions])
const pageConfig = getButtonPermissionPageConfig(buttonPermissionConfig, resolvedPath)
// getButtonPermissionPageConfig 支持"详情页继承入口菜单"
// 例:/workOrder/add 没配 → 回退找 /workOrder 的配置
canDo(action) 的逻辑:
| 场景 | 结果 |
|---|---|
buttonPermissionConfig === undefined(后端没下发) |
✅ true(降级放行) |
buttonPermissionConfig 存在,当前 pageConfig === {}(没配这个页面的按钮) |
✅ true(向后兼容放行) |
pageConfig[action] === undefined(配了页面但没配这个 action) |
❌ false(默认禁用) |
pageConfig[action] === true |
✅ true |
pageConfig[action] === false |
❌ false |
教学要点(极其容易被新人忽略):
后端没下发 → 降级放行;下发了但这个 action 没配 → 默认禁用 。这两条策略方向相反!
意图:初次接入权限系统时不要破坏老页面 ,但新页面必须显式声明禁用------这是一种"白名单在新,黑名单在旧"的渐进式策略。读这代码能学到"权限策略要可演进"的设计。
八、把链路串起来(从登录到点击按钮)
后端服务 newhome-web 用户 后端服务 newhome-web 用户 #mermaid-svg-M9HChKNqZXTunCWO{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-M9HChKNqZXTunCWO .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-M9HChKNqZXTunCWO .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-M9HChKNqZXTunCWO .error-icon{fill:#552222;}#mermaid-svg-M9HChKNqZXTunCWO .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-M9HChKNqZXTunCWO .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-M9HChKNqZXTunCWO .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-M9HChKNqZXTunCWO .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-M9HChKNqZXTunCWO .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-M9HChKNqZXTunCWO .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-M9HChKNqZXTunCWO .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-M9HChKNqZXTunCWO .marker{fill:#333333;stroke:#333333;}#mermaid-svg-M9HChKNqZXTunCWO .marker.cross{stroke:#333333;}#mermaid-svg-M9HChKNqZXTunCWO svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-M9HChKNqZXTunCWO p{margin:0;}#mermaid-svg-M9HChKNqZXTunCWO .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-M9HChKNqZXTunCWO text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-M9HChKNqZXTunCWO .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-M9HChKNqZXTunCWO .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-M9HChKNqZXTunCWO .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-M9HChKNqZXTunCWO .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-M9HChKNqZXTunCWO #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-M9HChKNqZXTunCWO .sequenceNumber{fill:white;}#mermaid-svg-M9HChKNqZXTunCWO #sequencenumber{fill:#333;}#mermaid-svg-M9HChKNqZXTunCWO #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-M9HChKNqZXTunCWO .messageText{fill:#333;stroke:none;}#mermaid-svg-M9HChKNqZXTunCWO .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-M9HChKNqZXTunCWO .labelText,#mermaid-svg-M9HChKNqZXTunCWO .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-M9HChKNqZXTunCWO .loopText,#mermaid-svg-M9HChKNqZXTunCWO .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-M9HChKNqZXTunCWO .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-M9HChKNqZXTunCWO .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-M9HChKNqZXTunCWO .noteText,#mermaid-svg-M9HChKNqZXTunCWO .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-M9HChKNqZXTunCWO .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-M9HChKNqZXTunCWO .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-M9HChKNqZXTunCWO .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-M9HChKNqZXTunCWO .actorPopupMenu{position:absolute;}#mermaid-svg-M9HChKNqZXTunCWO .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-M9HChKNqZXTunCWO .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-M9HChKNqZXTunCWO .actor-man circle,#mermaid-svg-M9HChKNqZXTunCWO line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-M9HChKNqZXTunCWO :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ① 登录后初始化 (app.tsx getInitialState) ② 用户进岗位管理配置权限 ③ 给账号分配角色 打开 /newHomeGET /staffs/info (queryCurrent)CurrentUser(含 roleId / developerId)getUserPermissionsByStaffId(userId)当前用户有权菜单(含 operation, buttonPermissions)getPermissionConfig()全量菜单(用来反推 isLeafNode)enrich + arrayToTree + buildButtonPermissionConfigFromPermissionsProLayout 渲染菜单树、ProTable/Form 渲染按钮(根据 canDo)进入 /organization/positionManagementPOST /home_org_position/getPermissionsByPositionId当前岗位已配菜单(operation, buttonPermissions)勾选/取消若干菜单 + 按钮父节点自动补 '01',纯前端聚合点"保存授权"POST /home_org_position/updatePosition (含 permissionOperations)进入 /account-management/role → 编辑角色GET /nbroles/selectPositionListByRoleId?staffRoleId=已选岗位多选岗位后提交POST /nbroles/save { positionIdList: ... }
九、API 清单(直接拷贝到你的笔记里)
前缀:MANAGEMENT、SUMMARY 两组(见 src/services/api.ts 顶部常量)。
| 用途 | 接口 | 在哪里 |
|---|---|---|
| 当前用户 | GET /staffs/info |
queryCurrent |
| 用户全量菜单权限 | POST /nbroles/getPermissionsByUserId?userId= |
getUserPermissionsByStaffId |
| 全量权限配置(菜单字典) | GET /nbroles/getPermissions |
getPermissionConfig |
| 角色增改 | POST /nbroles/save |
addRole / updateRole |
| 角色关联岗位查询 | GET /nbroles/selectPositionListByRoleId?staffRoleId= |
getPositionListByRoleId |
| 岗位绑定菜单权限 | POST /home_org_position/getPermissionsByPositionId?positionId= |
getMenuByPositionId |
| 岗位增改(含权限) | POST /home_org_position/updatePosition (addPosition) |
updateOrgPosition / addOrgPosition |
| 组织树(按用户权限过滤) | POST /home_org_company/getCompanyTreeWithDeptAndPositionByUserId |
getOrgTreeWithDeptAndPositionByUserIdData |
| 公司增删改 | POST /home_org_company/addCompany / updateCompany / deleteCompany |
树组件 CRUD |
| 职能中心增删改 | POST /home_org_institution/addInstitution / updateInstitution / deleteInstitution |
树组件 CRUD |
| 职能中心绑定岗位 | POST /home_org_position/editInstitutionPosition |
setOrgEditInstitutionPosition |
| 部门绑定岗位 | POST /home_org_position/editDeptPosition |
setOrgEditDeptPosition |
| 用户列表 | POST /home_org_user/selectUserPage |
getOrgUserPage |
| 岗位绑定员工 | POST /home_org_user/editPositionUser |
setOrgEditPositionUser |
十、踩坑点与扩展建议
10.1 已知踩坑
Color 永远返回 'blue'(getTagColor)--- 看着像 Bug 但不是,影响视觉一致性。新人常自做主张改七色,结果立刻撞上"和原型稿对不齐"。STORAGE_KEY = 'orgTree_selected_marker'--- 写着但useEffect恢复逻辑被注释。读代码别去"修复它",先确认后端 ID 是否复用。useCanEdit只看 edit 位 --- 如果业务需要"查看权也可见菜单项但禁用",不要去改 hook 的语义,更好的做法是在按钮级别控制。buttonPermissions没配置时降级放行 --- 新业务上线时必须主动配置 action=false,否则所有按钮依然可点!新人常踩。- 职能中心的
type编辑时禁用 --- 前端已加form.setFieldsValue({ type: undefined }),但需要在 FormItem 上挂disabled,审 PR 时重点扫这个。
10.2 你可能要做的事(决策路径)
| 需求 | 改哪里 |
|---|---|
| 给某个菜单加按钮 | src/config/button-permissions.ts 加 actions,并在所有用到该 action 的按钮上加 disabled={!canDo('xxx')} |
| 给某页面设置可见性 | 改后端 getUserPermissionsByUserId 下发的菜单(不要前端瞎挡;前端依赖后端) |
| 数据范围按"城市/项目"再收口 | 走 getUserTreePermission({ developerId }),由"角色绑城市"再次下发 |
| 角色想重新支持权限树 | 别!还原回滚成本极高;建议保留"角色 = 岗位集合"的现行架构 |
| 让你新引入页面默认不显按钮 | 业务完成后,在 button-permissions.ts 给该页面 actions: [] 而不是删引用 |
十一、给新人的 3 分钟快问快答
Q1:组织架构有哪几级?
答:公司(区域)/ 职能中心 / 岗位。部门作为数据存在但树不渲染。
Q2:权限绑在角色还是岗位?
答:岗位 。角色只是"岗位合集"。代码里有 // 权限配置已迁移到岗位管理 注释可佐证。
Q3:一个员工如何拿到权限?
答:分 5 步,强顺序依赖:
- 岗位授权 (岗位管理):给"客服专员"岗位配菜单 + 按钮权限 → 落
permissionOperations - 人员建档 (人员管理):创建员工档案
home_org_user - 开通账号 (账号管理):敲手机号 + 自动回填 → 落
nbstaffs - 人员分配 (组织架构):把员工绑到岗位 → 写
user ↔ position关联 - 员工登录 :
getPermissionsByStaffId(userId)反查岗位→菜单,下发currentUser.permissions和currentUser.buttonPermissions→ 渲染时由useCanEdit/useButtonPermission决策
⚠️ 关键纠正 :之前我写过"员工 → 分配角色(账号管理里)→ 角色绑了一组岗位"------这个说法不准确 。代码里
account/index.tsx:330-345的saveUser根本没有 roleId 字段入参 ,角色是登录后由后端下发的;前端没有"先选角色再选岗位"的链路。真正的权限路径是"员工 ↔ 岗位",不是"员工 → 角色 → 岗位"。完整流程见 §12。
Q4:菜单不显示,是前端的 bug 还是后端没下发?
答:99% 是后端没下发。getMenuDataFromPermissions(currentUser.permissions) 只画已有的;前端不做"看不见的放出来"。
Q5:按钮不显示,怎么调?
答:先去 岗位管理 → 该岗位 → 按钮权限 Tab 勾上对应 action;如果 buttonPermissions 整个字段没下发(canDo 看到 undefined),降级放行------所以真正出现按钮不显示的常见原因是 canDo === false ,意味着后端在 buttonPermissions 里写死了 false。这时要么改岗位,要么扩展 BUTTON_PERMISSIONS 字典。
Q6:组织树一直加载不上来?
答:八成是 getOrgTreeWithDeptAndPositionByUserId 用了 enabled: !!orgUserId,先确认 initialState.currentUser.orgUserVo.id 在登录后被注入。
十一·补、半小时速读:员工 / 账号 / 岗位 的关系速查
这一节先于 §12,因为它澄清 3 个最容易混淆的概念,不然后面的 5 步流程你会看得很糊涂。
11·补.1 三张表,两条线,绝不能合并
#mermaid-svg-TRatc5ToHQ3rVwTv{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-TRatc5ToHQ3rVwTv .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-TRatc5ToHQ3rVwTv .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-TRatc5ToHQ3rVwTv .error-icon{fill:#552222;}#mermaid-svg-TRatc5ToHQ3rVwTv .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-TRatc5ToHQ3rVwTv .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-TRatc5ToHQ3rVwTv .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-TRatc5ToHQ3rVwTv .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-TRatc5ToHQ3rVwTv .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-TRatc5ToHQ3rVwTv .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-TRatc5ToHQ3rVwTv .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-TRatc5ToHQ3rVwTv .marker{fill:#333333;stroke:#333333;}#mermaid-svg-TRatc5ToHQ3rVwTv .marker.cross{stroke:#333333;}#mermaid-svg-TRatc5ToHQ3rVwTv svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-TRatc5ToHQ3rVwTv p{margin:0;}#mermaid-svg-TRatc5ToHQ3rVwTv .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-TRatc5ToHQ3rVwTv .cluster-label text{fill:#333;}#mermaid-svg-TRatc5ToHQ3rVwTv .cluster-label span{color:#333;}#mermaid-svg-TRatc5ToHQ3rVwTv .cluster-label span p{background-color:transparent;}#mermaid-svg-TRatc5ToHQ3rVwTv .label text,#mermaid-svg-TRatc5ToHQ3rVwTv span{fill:#333;color:#333;}#mermaid-svg-TRatc5ToHQ3rVwTv .node rect,#mermaid-svg-TRatc5ToHQ3rVwTv .node circle,#mermaid-svg-TRatc5ToHQ3rVwTv .node ellipse,#mermaid-svg-TRatc5ToHQ3rVwTv .node polygon,#mermaid-svg-TRatc5ToHQ3rVwTv .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-TRatc5ToHQ3rVwTv .rough-node .label text,#mermaid-svg-TRatc5ToHQ3rVwTv .node .label text,#mermaid-svg-TRatc5ToHQ3rVwTv .image-shape .label,#mermaid-svg-TRatc5ToHQ3rVwTv .icon-shape .label{text-anchor:middle;}#mermaid-svg-TRatc5ToHQ3rVwTv .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-TRatc5ToHQ3rVwTv .rough-node .label,#mermaid-svg-TRatc5ToHQ3rVwTv .node .label,#mermaid-svg-TRatc5ToHQ3rVwTv .image-shape .label,#mermaid-svg-TRatc5ToHQ3rVwTv .icon-shape .label{text-align:center;}#mermaid-svg-TRatc5ToHQ3rVwTv .node.clickable{cursor:pointer;}#mermaid-svg-TRatc5ToHQ3rVwTv .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-TRatc5ToHQ3rVwTv .arrowheadPath{fill:#333333;}#mermaid-svg-TRatc5ToHQ3rVwTv .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-TRatc5ToHQ3rVwTv .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-TRatc5ToHQ3rVwTv .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-TRatc5ToHQ3rVwTv .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-TRatc5ToHQ3rVwTv .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-TRatc5ToHQ3rVwTv .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-TRatc5ToHQ3rVwTv .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-TRatc5ToHQ3rVwTv .cluster text{fill:#333;}#mermaid-svg-TRatc5ToHQ3rVwTv .cluster span{color:#333;}#mermaid-svg-TRatc5ToHQ3rVwTv div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-TRatc5ToHQ3rVwTv .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-TRatc5ToHQ3rVwTv rect.text{fill:none;stroke-width:0;}#mermaid-svg-TRatc5ToHQ3rVwTv .icon-shape,#mermaid-svg-TRatc5ToHQ3rVwTv .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-TRatc5ToHQ3rVwTv .icon-shape p,#mermaid-svg-TRatc5ToHQ3rVwTv .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-TRatc5ToHQ3rVwTv .icon-shape .label rect,#mermaid-svg-TRatc5ToHQ3rVwTv .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-TRatc5ToHQ3rVwTv .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-TRatc5ToHQ3rVwTv .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-TRatc5ToHQ3rVwTv :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 权限线
登录账号线
员工档案线
靠手机号关联
走 request.ts
roleId 透传
走 editPositionUser
home_org_user
姓名/手机/工号/入职
字段: employmentStatus=0 离职
字段: deptNames / positionNames
nbstaffs
token / password / roleId
字段: isDeleted 禁用
字段: staffIsDeleted 后台禁用
home_org_position
岗位
关联表: user ↔ position
关联表: position ↔ menu
关键代码实证 (src/pages/organization/orgManagement/component/OrgTableByLevel.tsx:108-120)------员工表格里同时出现"后台账号 / APP 账号"两列:
ts
{ title: '后台账号', dataIndex: 'staffIsDeleted' /* 来自 nbstaffs */ }
{ title: 'APP账号', dataIndex: 'isDeleted' /* 也来自 nbstaffs,但分两个字段控制两端禁用 */ }
如果员工 = 账号,就不会有这两列 。所以这 3 张表在数据层就是分开的。
11·补.2 三大常见误区
| 误区 | 真相 |
|---|---|
| "员工 = 账号,离职 = 删除" | 离职只动 home_org_user.employmentStatus=0 ,账号 nbstaffs 保留(作为登录历史 + 审计需要) |
| "创建员工时直接开账号" | 不行 。原因:超管账号 id=1 必须能独立保护(看 models/account.ts:42 的 filter(item => item.id !== 1));批量入职时档案可分批,账号必须后开 |
| "调岗要重置账号" | 不需要 。改的是 user ↔ position 关联表,账号 nbstaffs 完全不动 |
11·补.3 回答"②新增账号是不是多余的"
不是多余,是业务上必须拆开。 4 个反例证明合并会出 BUG:
- 同号复用场景:员工离职 → 再入职,手机号相同 → 旧账号是激活还是重建?拆开后可以"激活旧账号"。
- 超管账号保护 :
id=1的系统账号必须能从员工列表里隐藏------合并后无法区分。 - 多端禁用独立 :
isDeleted(APP 端)和staffIsDeleted(后台端)互不影响------合并后只能同生共死。 - 审计合规 :离职员工如果在
home_org_user删了,但登录日志/工单历史里要查"他当时用谁的身份登录的"------必须有独立的nbstaffs记录可查。
优化方向 (如果你觉得流程长):后端可以加一个 "一键开通"接口 /home_org_user/createUserWithAccount,把 ② ③ 合并成一次提交。但 ④ 岗位授权省不掉------因为权限是按岗位来的,不存在"按个人授权"。
11·补.4 登录 vs 权限 vs 员工档案------三件事必须分清
这一节专门回答"做了 1+3 步,没做 2,可以登录吗"这类问题。
nbstaffs表才是登录凭证,和员工档案完全分离。
11·补.4.1 关键源码证据
ts
// src/services/api.ts:55-60
export function accountLogin(data: any) {
return request(SUMMARY + '/nbstaffs/login', { method: 'POST', data })
}
// src/pages/accountManagement/account/index.tsx:108
const obj = {
...values,
name: userInfo?.name || '', // 姓名从 home_org_user 带
username: userInfo?.phone || '', // 登录名 = 手机号(关键!)
}
saveUser(obj) // → POST /nbstaffs/save,落 nbstaffs 表
ts
// src/pages/organization/userManagement/index.tsx:294-300
const handleSubmit = (values: TableRow) => {
let data = { ...values, institutionId, companyId }
addOrgUser(data) // → POST /home_org_user/addUser,**只落 home_org_user**
}
两个表的"分界线":
| 动作 | 调用 | 落库 | 用途 |
|---|---|---|---|
| 新增员工 | addOrgUser |
home_org_user |
员工档案(姓名/手机/工号/入职) |
| 新增账号 | saveUser |
nbstaffs |
员工登录凭证(手机号+密码) |
| 登录 | accountLogin |
读 nbstaffs |
验证手机号+密码 |
11·补.4.2 四种状态组合的真相
| 已做步骤 | 后台能登录? | APP 能登录? | 能看到菜单? |
|---|---|---|---|
| 仅 ① 新增员工 | ❌ nbstaffs 没记录 |
❌ 同左 | ❌ |
| 仅 ② 新增账号 | ✅ 凭证已建 | ✅ 凭证已建 | ❌ 没岗位 = 权限空 |
| ① + ② | ✅ 凭证已建 | ✅ 凭证已建 | ❌ 没岗位 = 权限空 |
| ① + ② + ③ 人员分配 | ✅ 凭证已建 | ✅ 凭证已建 | ❌ 岗位没授权 = 空菜单 |
| ①+②+③+④ 全部完成 | ✅ | ✅ | ✅ |
| 仅 ④ 岗位授权 | ❌ | ❌ | ❌ 没人绑岗位 = 无意义 |
教学要点:"能登录" 和 "能看到菜单" 是两件事 。前者由
nbstaffs决定,后者由getPermissionsByStaffId聚合岗位+菜单决定。
11·补.4.3 重要概念纠偏:APP 账号 ≠ 后台账号
看 OrgTableByLevel.tsx:170-190 员工表格:
ts
{ title: '后台账号', dataIndex: 'staffIsDeleted', valueEnum: { 0: '启用', 1: '禁用' } }
{ title: 'APP账号', dataIndex: 'isDeleted', valueEnum: { 0: '启用', 1: '禁用' } }
⚠️ 新手最常误解 :以为有"两套账号"------其实只有一张
nbstaffs表 。两列只是两个独立的状态开关:
staffIsDeleted = 1→ 后台端被禁isDeleted = 1→ APP 端被禁- 两个都
0→ 两端都启用也就是说,禁了后台不影响 APP (
OrgTableByLevel.tsx:215-220的"账号管理"下拉菜单可以独立切换两端)。注:
OrgTableByLevel.tsx:75-80注释里写"APP 用 isDeleted、后台用 staffIsDeleted",但第二行被作者注释废除 ("以上约定废除"),实际两列都通过isDeleted字段控制 ------也就是说两端的禁用状态目前是绑定的。
11·补.4.4 "导入员工"也只导入档案
api.ts:2601 的 importUserList(批量导入)也只写 home_org_user,不创建 nbstaffs。也就是说:
- 一次导入 100 个员工 → 他们全部没有登录凭证
- 需要后续在"账号管理"逐个"新增账号"才能登录
11·补.4.5 为什么不能"新增员工时自动开账号"?
理论上 ① 完成后顺手调 saveUser 创建 nbstaffs 记录,技术上不难。但作者故意没做,因为:
- 超管账号保护 :
id=1的系统账号必须能独立于员工档案存在------合并后无法区分 - 多端禁用独立 :员工档案不该背负"是否禁用后台/APP"的状态------那应该挂在
nbstaffs上 - 审计合规 :员工离职后
home_org_user.employmentStatus=0但nbstaffs必须保留以追溯登录历史 - 不同时机:HR 建员工档案 ≠ 立刻给登录凭证(可能等入职当天再开)
结论 :nbstaffs 是登录凭证,home_org_user 是员工档案------永远不应该合并。
十二、新员工从 0 到能登录能操作的完整流程(案例讲解)
这一节用一个真实新人入职的过程把上面分散的概念串起来。流程每一步都标注了"打开哪个页面 / 调哪个接口 / 关键代码在哪"。
12.1 场景:富国物业新来一位客服"王小二",要开通后台账号
假设:
- 公司:富国物业有限公司(
companyId = 10) - 职能中心:客户服务中心(
institutionId = 102) - 已有岗位:客服主管、客服专员、客服助理
我们要让她:① 录入员工档案 → ② 开通后台账号 → ③ 分配岗位 → ④ 岗位拿到应有权限 → ⑤ 正常登录干活。
12.1·补 流程顺序总图(有强先后依赖)
⚠️ 顺序不是 ①②③④⑤ ,而是 ④ → ① → ② → ③ → ⑤。文档下面的小节按"自然阅读顺序"展开,但实际执行必须按这个总图来:
#mermaid-svg-1RxISHEY0dYO0Q4X{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-1RxISHEY0dYO0Q4X .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-1RxISHEY0dYO0Q4X .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-1RxISHEY0dYO0Q4X .error-icon{fill:#552222;}#mermaid-svg-1RxISHEY0dYO0Q4X .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-1RxISHEY0dYO0Q4X .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-1RxISHEY0dYO0Q4X .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-1RxISHEY0dYO0Q4X .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-1RxISHEY0dYO0Q4X .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-1RxISHEY0dYO0Q4X .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-1RxISHEY0dYO0Q4X .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-1RxISHEY0dYO0Q4X .marker{fill:#333333;stroke:#333333;}#mermaid-svg-1RxISHEY0dYO0Q4X .marker.cross{stroke:#333333;}#mermaid-svg-1RxISHEY0dYO0Q4X svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-1RxISHEY0dYO0Q4X p{margin:0;}#mermaid-svg-1RxISHEY0dYO0Q4X .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-1RxISHEY0dYO0Q4X .cluster-label text{fill:#333;}#mermaid-svg-1RxISHEY0dYO0Q4X .cluster-label span{color:#333;}#mermaid-svg-1RxISHEY0dYO0Q4X .cluster-label span p{background-color:transparent;}#mermaid-svg-1RxISHEY0dYO0Q4X .label text,#mermaid-svg-1RxISHEY0dYO0Q4X span{fill:#333;color:#333;}#mermaid-svg-1RxISHEY0dYO0Q4X .node rect,#mermaid-svg-1RxISHEY0dYO0Q4X .node circle,#mermaid-svg-1RxISHEY0dYO0Q4X .node ellipse,#mermaid-svg-1RxISHEY0dYO0Q4X .node polygon,#mermaid-svg-1RxISHEY0dYO0Q4X .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-1RxISHEY0dYO0Q4X .rough-node .label text,#mermaid-svg-1RxISHEY0dYO0Q4X .node .label text,#mermaid-svg-1RxISHEY0dYO0Q4X .image-shape .label,#mermaid-svg-1RxISHEY0dYO0Q4X .icon-shape .label{text-anchor:middle;}#mermaid-svg-1RxISHEY0dYO0Q4X .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-1RxISHEY0dYO0Q4X .rough-node .label,#mermaid-svg-1RxISHEY0dYO0Q4X .node .label,#mermaid-svg-1RxISHEY0dYO0Q4X .image-shape .label,#mermaid-svg-1RxISHEY0dYO0Q4X .icon-shape .label{text-align:center;}#mermaid-svg-1RxISHEY0dYO0Q4X .node.clickable{cursor:pointer;}#mermaid-svg-1RxISHEY0dYO0Q4X .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-1RxISHEY0dYO0Q4X .arrowheadPath{fill:#333333;}#mermaid-svg-1RxISHEY0dYO0Q4X .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-1RxISHEY0dYO0Q4X .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-1RxISHEY0dYO0Q4X .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1RxISHEY0dYO0Q4X .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-1RxISHEY0dYO0Q4X .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1RxISHEY0dYO0Q4X .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-1RxISHEY0dYO0Q4X .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-1RxISHEY0dYO0Q4X .cluster text{fill:#333;}#mermaid-svg-1RxISHEY0dYO0Q4X .cluster span{color:#333;}#mermaid-svg-1RxISHEY0dYO0Q4X div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-1RxISHEY0dYO0Q4X .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-1RxISHEY0dYO0Q4X rect.text{fill:none;stroke-width:0;}#mermaid-svg-1RxISHEY0dYO0Q4X .icon-shape,#mermaid-svg-1RxISHEY0dYO0Q4X .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1RxISHEY0dYO0Q4X .icon-shape p,#mermaid-svg-1RxISHEY0dYO0Q4X .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-1RxISHEY0dYO0Q4X .icon-shape .label rect,#mermaid-svg-1RxISHEY0dYO0Q4X .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1RxISHEY0dYO0Q4X .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-1RxISHEY0dYO0Q4X .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-1RxISHEY0dYO0Q4X :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ④ 岗位管理 给 客服专员 岗位授权 updateOrgPosition
① 人员管理 创建王小二档案 addOrgUser
② 账号管理 敲手机号带出员工 saveUser / nbstaffs
③ 组织架构-人员分配 把王小二绑到客服专员 editPositionUser
⑤ 王小二登录 getPermissionsByStaffId 下发权限
| 步骤 | 顺序 | 这一步失败会怎样 |
|---|---|---|
| ④ 岗位授权 | 最先做 | 后面 ③ 绑的岗位没菜单,员工登录后啥也干不了 |
| ① 人员建档 | 第二 | 后面 ② 敲手机号带不出员工(提示"未查询到该手机号的员工信息") |
| ② 开通账号 | 第三 | 员工登录不了(没 token) |
| ③ 人员分配 | 第四 | 员工登录后看不到任何菜单(currentUser.permissions = []) |
| ⑤ 登录验证 | 最后 | --- |
反例(顺序错了会怎样):
- 跳过 ④ 直接 ① → ② → ③ → ⑤:王小二登录后看到的是空菜单 ,因为
getPermissionsByStaffId拿不到任何菜单 - 跳过 ② 直接 ① → ③ → ⑤:王小二能登录但账号不存在 ,
queryCurrent会 401 - 跳过 ① 直接 ②:敲手机号后带不出任何信息,表单里的姓名/公司/岗位全是空
教学要点:哪怕同一个人同一份资料,所有步骤都不能省、不能错。这是 4 张独立表的代价。
12.2 第一步:先有"员工"档案(人员管理)
打开路径:组织架构 → 关联社区 / 项目 一栏的子页面 /organization/userManagement (基于 AreaLayoutOrganization layout),或老路径 orgManagement/index。
#mermaid-svg-R2P8nZMJy1DRmXVe{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-R2P8nZMJy1DRmXVe .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-R2P8nZMJy1DRmXVe .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-R2P8nZMJy1DRmXVe .error-icon{fill:#552222;}#mermaid-svg-R2P8nZMJy1DRmXVe .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-R2P8nZMJy1DRmXVe .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-R2P8nZMJy1DRmXVe .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-R2P8nZMJy1DRmXVe .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-R2P8nZMJy1DRmXVe .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-R2P8nZMJy1DRmXVe .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-R2P8nZMJy1DRmXVe .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-R2P8nZMJy1DRmXVe .marker{fill:#333333;stroke:#333333;}#mermaid-svg-R2P8nZMJy1DRmXVe .marker.cross{stroke:#333333;}#mermaid-svg-R2P8nZMJy1DRmXVe svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-R2P8nZMJy1DRmXVe p{margin:0;}#mermaid-svg-R2P8nZMJy1DRmXVe .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-R2P8nZMJy1DRmXVe .cluster-label text{fill:#333;}#mermaid-svg-R2P8nZMJy1DRmXVe .cluster-label span{color:#333;}#mermaid-svg-R2P8nZMJy1DRmXVe .cluster-label span p{background-color:transparent;}#mermaid-svg-R2P8nZMJy1DRmXVe .label text,#mermaid-svg-R2P8nZMJy1DRmXVe span{fill:#333;color:#333;}#mermaid-svg-R2P8nZMJy1DRmXVe .node rect,#mermaid-svg-R2P8nZMJy1DRmXVe .node circle,#mermaid-svg-R2P8nZMJy1DRmXVe .node ellipse,#mermaid-svg-R2P8nZMJy1DRmXVe .node polygon,#mermaid-svg-R2P8nZMJy1DRmXVe .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-R2P8nZMJy1DRmXVe .rough-node .label text,#mermaid-svg-R2P8nZMJy1DRmXVe .node .label text,#mermaid-svg-R2P8nZMJy1DRmXVe .image-shape .label,#mermaid-svg-R2P8nZMJy1DRmXVe .icon-shape .label{text-anchor:middle;}#mermaid-svg-R2P8nZMJy1DRmXVe .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-R2P8nZMJy1DRmXVe .rough-node .label,#mermaid-svg-R2P8nZMJy1DRmXVe .node .label,#mermaid-svg-R2P8nZMJy1DRmXVe .image-shape .label,#mermaid-svg-R2P8nZMJy1DRmXVe .icon-shape .label{text-align:center;}#mermaid-svg-R2P8nZMJy1DRmXVe .node.clickable{cursor:pointer;}#mermaid-svg-R2P8nZMJy1DRmXVe .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-R2P8nZMJy1DRmXVe .arrowheadPath{fill:#333333;}#mermaid-svg-R2P8nZMJy1DRmXVe .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-R2P8nZMJy1DRmXVe .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-R2P8nZMJy1DRmXVe .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-R2P8nZMJy1DRmXVe .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-R2P8nZMJy1DRmXVe .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-R2P8nZMJy1DRmXVe .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-R2P8nZMJy1DRmXVe .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-R2P8nZMJy1DRmXVe .cluster text{fill:#333;}#mermaid-svg-R2P8nZMJy1DRmXVe .cluster span{color:#333;}#mermaid-svg-R2P8nZMJy1DRmXVe div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-R2P8nZMJy1DRmXVe .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-R2P8nZMJy1DRmXVe rect.text{fill:none;stroke-width:0;}#mermaid-svg-R2P8nZMJy1DRmXVe .icon-shape,#mermaid-svg-R2P8nZMJy1DRmXVe .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-R2P8nZMJy1DRmXVe .icon-shape p,#mermaid-svg-R2P8nZMJy1DRmXVe .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-R2P8nZMJy1DRmXVe .icon-shape .label rect,#mermaid-svg-R2P8nZMJy1DRmXVe .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-R2P8nZMJy1DRmXVe .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-R2P8nZMJy1DRmXVe .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-R2P8nZMJy1DRmXVe :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 左侧树选公司或职能中心
点 新增员工
填名字+手机+身份证+工号+入职时间
提交 addOrgUser
后端落库 home_org_user 关联 companyId + institutionId
关键代码 src/pages/organization/userManagement/index.tsx:266-300:
ts
// 关键:选中的组织节点决定了"员工落到哪里"
let data = {
...requestParams,
institutionId: organizationItem?.id, // 选的是职能中心 → 这里有值
companyId: organizationItem?.companyId, // 选的是公司 → 落到公司下
}
addOrgUser(data)
⚠️ 这一步只创建"员工"档案,没有账号、没有岗位、没有任何权限 。表里能搜到她,但她登录不上系统。
12.3 第二步:给"员工"开"后台账号"
打开路径:账号管理 /account-management/user (注意是 user 不是 account ------ 这是路由的命名遗留,对应文件是 src/pages/accountManagement/account/index.tsx)。
⚠️ 地址常见坑:
- 路由表
.umirc.ts:193-202写的是/account-management/user和/account-management/role(连字符)- 但源码目录用的是
accountManagement(驼峰 )------ 国际化 key 也是驼峰menu.systemManage.accountManagement- 别手敲
/accountManagement/...,会被 404
- 如果登录后菜单栏看不到"账号管理",说明你后端
getUserPermissionsByStaffId下发的permissions数组里没有 path 为/account-management/...的菜单项------这页要靠后端给菜单权,不是前端能解决
#mermaid-svg-tYUOYBkQoXi3Z5o5{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-tYUOYBkQoXi3Z5o5 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-tYUOYBkQoXi3Z5o5 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-tYUOYBkQoXi3Z5o5 .error-icon{fill:#552222;}#mermaid-svg-tYUOYBkQoXi3Z5o5 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-tYUOYBkQoXi3Z5o5 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-tYUOYBkQoXi3Z5o5 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-tYUOYBkQoXi3Z5o5 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-tYUOYBkQoXi3Z5o5 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-tYUOYBkQoXi3Z5o5 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-tYUOYBkQoXi3Z5o5 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-tYUOYBkQoXi3Z5o5 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-tYUOYBkQoXi3Z5o5 .marker.cross{stroke:#333333;}#mermaid-svg-tYUOYBkQoXi3Z5o5 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-tYUOYBkQoXi3Z5o5 p{margin:0;}#mermaid-svg-tYUOYBkQoXi3Z5o5 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-tYUOYBkQoXi3Z5o5 .cluster-label text{fill:#333;}#mermaid-svg-tYUOYBkQoXi3Z5o5 .cluster-label span{color:#333;}#mermaid-svg-tYUOYBkQoXi3Z5o5 .cluster-label span p{background-color:transparent;}#mermaid-svg-tYUOYBkQoXi3Z5o5 .label text,#mermaid-svg-tYUOYBkQoXi3Z5o5 span{fill:#333;color:#333;}#mermaid-svg-tYUOYBkQoXi3Z5o5 .node rect,#mermaid-svg-tYUOYBkQoXi3Z5o5 .node circle,#mermaid-svg-tYUOYBkQoXi3Z5o5 .node ellipse,#mermaid-svg-tYUOYBkQoXi3Z5o5 .node polygon,#mermaid-svg-tYUOYBkQoXi3Z5o5 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-tYUOYBkQoXi3Z5o5 .rough-node .label text,#mermaid-svg-tYUOYBkQoXi3Z5o5 .node .label text,#mermaid-svg-tYUOYBkQoXi3Z5o5 .image-shape .label,#mermaid-svg-tYUOYBkQoXi3Z5o5 .icon-shape .label{text-anchor:middle;}#mermaid-svg-tYUOYBkQoXi3Z5o5 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-tYUOYBkQoXi3Z5o5 .rough-node .label,#mermaid-svg-tYUOYBkQoXi3Z5o5 .node .label,#mermaid-svg-tYUOYBkQoXi3Z5o5 .image-shape .label,#mermaid-svg-tYUOYBkQoXi3Z5o5 .icon-shape .label{text-align:center;}#mermaid-svg-tYUOYBkQoXi3Z5o5 .node.clickable{cursor:pointer;}#mermaid-svg-tYUOYBkQoXi3Z5o5 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-tYUOYBkQoXi3Z5o5 .arrowheadPath{fill:#333333;}#mermaid-svg-tYUOYBkQoXi3Z5o5 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-tYUOYBkQoXi3Z5o5 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-tYUOYBkQoXi3Z5o5 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-tYUOYBkQoXi3Z5o5 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-tYUOYBkQoXi3Z5o5 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-tYUOYBkQoXi3Z5o5 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-tYUOYBkQoXi3Z5o5 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-tYUOYBkQoXi3Z5o5 .cluster text{fill:#333;}#mermaid-svg-tYUOYBkQoXi3Z5o5 .cluster span{color:#333;}#mermaid-svg-tYUOYBkQoXi3Z5o5 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-tYUOYBkQoXi3Z5o5 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-tYUOYBkQoXi3Z5o5 rect.text{fill:none;stroke-width:0;}#mermaid-svg-tYUOYBkQoXi3Z5o5 .icon-shape,#mermaid-svg-tYUOYBkQoXi3Z5o5 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-tYUOYBkQoXi3Z5o5 .icon-shape p,#mermaid-svg-tYUOYBkQoXi3Z5o5 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-tYUOYBkQoXi3Z5o5 .icon-shape .label rect,#mermaid-svg-tYUOYBkQoXi3Z5o5 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-tYUOYBkQoXi3Z5o5 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-tYUOYBkQoXi3Z5o5 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-tYUOYBkQoXi3Z5o5 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
点 新增账号
填手机号 失焦
getUserDetailinfoByPhone 反向查组织树里有没有这个员工
找到了?
表单自动显示 姓名+公司+中心+部门+岗位
提示 未查询到该手机号的员工信息
saveUser nbstaffs/save
关键代码 account/index.tsx:330-345:
ts
const handleSubmit = async (values: any) => {
if (curUser) {
// 修改
params = { id: curUser.id, preRoleId: curUser.roleId, ...values } // 仍然保留 preRoleId 字段
} else {
params = { ...values }
}
saveUser(params)
}
"账号"的字段语义:
nbstaffs(后台账号)= 一个能登录后台的登录账号记录- 一个员工(
home_org_user)有或没有 后台账号;一个后台账号不一定 关联角色(账号表里preRoleId字段保留向旧模型兼容) - 表单顶部那张被注释掉的"角色下拉"------确实被废弃了 。新流程是:账号创建后,通过
staffRoleId字段(在请求里依然会被 axios 加进 params)兜底 走老路径兼容,但真正决定权限的是用户登录后调getUserPermissionsByUserId拉下来的permissions列表 ------这列表来自员工当前绑定的所有岗位汇总出来的菜单。
12.4 第三步:把员工分配到具体岗位
打开路径:组织架构 → 组织架构 Tab → 左侧树选某职能中心 → 右侧岗位按钮组里选岗位 → 顶部"人员分配"按钮。
#mermaid-svg-jd3dVWnZznneqUz7{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-jd3dVWnZznneqUz7 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-jd3dVWnZznneqUz7 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-jd3dVWnZznneqUz7 .error-icon{fill:#552222;}#mermaid-svg-jd3dVWnZznneqUz7 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-jd3dVWnZznneqUz7 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-jd3dVWnZznneqUz7 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-jd3dVWnZznneqUz7 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-jd3dVWnZznneqUz7 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-jd3dVWnZznneqUz7 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-jd3dVWnZznneqUz7 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-jd3dVWnZznneqUz7 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-jd3dVWnZznneqUz7 .marker.cross{stroke:#333333;}#mermaid-svg-jd3dVWnZznneqUz7 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-jd3dVWnZznneqUz7 p{margin:0;}#mermaid-svg-jd3dVWnZznneqUz7 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-jd3dVWnZznneqUz7 .cluster-label text{fill:#333;}#mermaid-svg-jd3dVWnZznneqUz7 .cluster-label span{color:#333;}#mermaid-svg-jd3dVWnZznneqUz7 .cluster-label span p{background-color:transparent;}#mermaid-svg-jd3dVWnZznneqUz7 .label text,#mermaid-svg-jd3dVWnZznneqUz7 span{fill:#333;color:#333;}#mermaid-svg-jd3dVWnZznneqUz7 .node rect,#mermaid-svg-jd3dVWnZznneqUz7 .node circle,#mermaid-svg-jd3dVWnZznneqUz7 .node ellipse,#mermaid-svg-jd3dVWnZznneqUz7 .node polygon,#mermaid-svg-jd3dVWnZznneqUz7 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-jd3dVWnZznneqUz7 .rough-node .label text,#mermaid-svg-jd3dVWnZznneqUz7 .node .label text,#mermaid-svg-jd3dVWnZznneqUz7 .image-shape .label,#mermaid-svg-jd3dVWnZznneqUz7 .icon-shape .label{text-anchor:middle;}#mermaid-svg-jd3dVWnZznneqUz7 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-jd3dVWnZznneqUz7 .rough-node .label,#mermaid-svg-jd3dVWnZznneqUz7 .node .label,#mermaid-svg-jd3dVWnZznneqUz7 .image-shape .label,#mermaid-svg-jd3dVWnZznneqUz7 .icon-shape .label{text-align:center;}#mermaid-svg-jd3dVWnZznneqUz7 .node.clickable{cursor:pointer;}#mermaid-svg-jd3dVWnZznneqUz7 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-jd3dVWnZznneqUz7 .arrowheadPath{fill:#333333;}#mermaid-svg-jd3dVWnZznneqUz7 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-jd3dVWnZznneqUz7 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-jd3dVWnZznneqUz7 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-jd3dVWnZznneqUz7 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-jd3dVWnZznneqUz7 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-jd3dVWnZznneqUz7 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-jd3dVWnZznneqUz7 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-jd3dVWnZznneqUz7 .cluster text{fill:#333;}#mermaid-svg-jd3dVWnZznneqUz7 .cluster span{color:#333;}#mermaid-svg-jd3dVWnZznneqUz7 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-jd3dVWnZznneqUz7 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-jd3dVWnZznneqUz7 rect.text{fill:none;stroke-width:0;}#mermaid-svg-jd3dVWnZznneqUz7 .icon-shape,#mermaid-svg-jd3dVWnZznneqUz7 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-jd3dVWnZznneqUz7 .icon-shape p,#mermaid-svg-jd3dVWnZznneqUz7 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-jd3dVWnZznneqUz7 .icon-shape .label rect,#mermaid-svg-jd3dVWnZznneqUz7 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-jd3dVWnZznneqUz7 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-jd3dVWnZznneqUz7 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-jd3dVWnZznneqUz7 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 左侧树选职能中心 比如客户服务中心
右侧岗位按钮组点 客服专员
顶部 人员分配 按钮启用
弹窗 Transfer 穿梭框
左侧未选 getOrgUserList selected=0
右侧已选 getOrgUserList selected=1
拖动王小二到右侧
确认 setOrgEditPositionUser
后端写关系 user_id X position_id Y 走 home_org_user/editPositionUser
关键代码 orgManagement/index.tsx:185-220:
ts
// 同一接口、两个 selected 标记,组装穿梭框数据
const unselected = await getOrgUserList({ institutionId, positionId, selected: 0, employmentStatus: 1 })
const selected = await getOrgUserList({ institutionId, positionId, selected: 1, employmentStatus: 1 })
await setOrgEditPositionUser({
institutionId,
positionId,
userIdList: userTargetKeys, // 选中的 userId 列表,整体覆盖
})
走完这步王小二才"挂上"客服专员这个岗位 。但她还是没有菜单权限 ------因为
getPermissionsByUserId要先知道"客服专员绑了哪些菜单"。
12.5 第四步(管理员先于她做):给"客服专员"这个岗位授权
打开路径:组织管理 → 岗位管理 /organization/positionManagement。
#mermaid-svg-3Wo1OnxM8pIifhPQ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-3Wo1OnxM8pIifhPQ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-3Wo1OnxM8pIifhPQ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-3Wo1OnxM8pIifhPQ .error-icon{fill:#552222;}#mermaid-svg-3Wo1OnxM8pIifhPQ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-3Wo1OnxM8pIifhPQ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-3Wo1OnxM8pIifhPQ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-3Wo1OnxM8pIifhPQ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-3Wo1OnxM8pIifhPQ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-3Wo1OnxM8pIifhPQ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-3Wo1OnxM8pIifhPQ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-3Wo1OnxM8pIifhPQ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-3Wo1OnxM8pIifhPQ .marker.cross{stroke:#333333;}#mermaid-svg-3Wo1OnxM8pIifhPQ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-3Wo1OnxM8pIifhPQ p{margin:0;}#mermaid-svg-3Wo1OnxM8pIifhPQ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-3Wo1OnxM8pIifhPQ .cluster-label text{fill:#333;}#mermaid-svg-3Wo1OnxM8pIifhPQ .cluster-label span{color:#333;}#mermaid-svg-3Wo1OnxM8pIifhPQ .cluster-label span p{background-color:transparent;}#mermaid-svg-3Wo1OnxM8pIifhPQ .label text,#mermaid-svg-3Wo1OnxM8pIifhPQ span{fill:#333;color:#333;}#mermaid-svg-3Wo1OnxM8pIifhPQ .node rect,#mermaid-svg-3Wo1OnxM8pIifhPQ .node circle,#mermaid-svg-3Wo1OnxM8pIifhPQ .node ellipse,#mermaid-svg-3Wo1OnxM8pIifhPQ .node polygon,#mermaid-svg-3Wo1OnxM8pIifhPQ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-3Wo1OnxM8pIifhPQ .rough-node .label text,#mermaid-svg-3Wo1OnxM8pIifhPQ .node .label text,#mermaid-svg-3Wo1OnxM8pIifhPQ .image-shape .label,#mermaid-svg-3Wo1OnxM8pIifhPQ .icon-shape .label{text-anchor:middle;}#mermaid-svg-3Wo1OnxM8pIifhPQ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-3Wo1OnxM8pIifhPQ .rough-node .label,#mermaid-svg-3Wo1OnxM8pIifhPQ .node .label,#mermaid-svg-3Wo1OnxM8pIifhPQ .image-shape .label,#mermaid-svg-3Wo1OnxM8pIifhPQ .icon-shape .label{text-align:center;}#mermaid-svg-3Wo1OnxM8pIifhPQ .node.clickable{cursor:pointer;}#mermaid-svg-3Wo1OnxM8pIifhPQ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-3Wo1OnxM8pIifhPQ .arrowheadPath{fill:#333333;}#mermaid-svg-3Wo1OnxM8pIifhPQ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-3Wo1OnxM8pIifhPQ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-3Wo1OnxM8pIifhPQ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3Wo1OnxM8pIifhPQ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-3Wo1OnxM8pIifhPQ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3Wo1OnxM8pIifhPQ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-3Wo1OnxM8pIifhPQ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-3Wo1OnxM8pIifhPQ .cluster text{fill:#333;}#mermaid-svg-3Wo1OnxM8pIifhPQ .cluster span{color:#333;}#mermaid-svg-3Wo1OnxM8pIifhPQ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-3Wo1OnxM8pIifhPQ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-3Wo1OnxM8pIifhPQ rect.text{fill:none;stroke-width:0;}#mermaid-svg-3Wo1OnxM8pIifhPQ .icon-shape,#mermaid-svg-3Wo1OnxM8pIifhPQ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3Wo1OnxM8pIifhPQ .icon-shape p,#mermaid-svg-3Wo1OnxM8pIifhPQ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-3Wo1OnxM8pIifhPQ .icon-shape .label rect,#mermaid-svg-3Wo1OnxM8pIifhPQ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3Wo1OnxM8pIifhPQ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-3Wo1OnxM8pIifhPQ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-3Wo1OnxM8pIifhPQ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 左侧选 客服专员
右侧模块 Tab 选 工单管理
勾选/取消菜单权限 + 按钮权限
点 保存授权
updateOrgPosition 带 permissionOperations + buttonPermissions
后端写 home_org_position_permission 表
这是最关键 的一步。这里配的 permissionOperations(每个 menuId 对应一个两位 operation 字符串)会直接影响接下来所有挂这个岗位的员工登录后能看见的菜单。
12.6 第五步:王小二重新登录,权限生效
后端 newhome-web 王小二 后端 newhome-web 王小二 #mermaid-svg-fTiNQHIHgXcfYUII{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-fTiNQHIHgXcfYUII .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-fTiNQHIHgXcfYUII .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-fTiNQHIHgXcfYUII .error-icon{fill:#552222;}#mermaid-svg-fTiNQHIHgXcfYUII .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-fTiNQHIHgXcfYUII .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-fTiNQHIHgXcfYUII .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-fTiNQHIHgXcfYUII .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-fTiNQHIHgXcfYUII .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-fTiNQHIHgXcfYUII .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-fTiNQHIHgXcfYUII .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-fTiNQHIHgXcfYUII .marker{fill:#333333;stroke:#333333;}#mermaid-svg-fTiNQHIHgXcfYUII .marker.cross{stroke:#333333;}#mermaid-svg-fTiNQHIHgXcfYUII svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-fTiNQHIHgXcfYUII p{margin:0;}#mermaid-svg-fTiNQHIHgXcfYUII .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-fTiNQHIHgXcfYUII text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-fTiNQHIHgXcfYUII .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-fTiNQHIHgXcfYUII .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-fTiNQHIHgXcfYUII .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-fTiNQHIHgXcfYUII .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-fTiNQHIHgXcfYUII #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-fTiNQHIHgXcfYUII .sequenceNumber{fill:white;}#mermaid-svg-fTiNQHIHgXcfYUII #sequencenumber{fill:#333;}#mermaid-svg-fTiNQHIHgXcfYUII #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-fTiNQHIHgXcfYUII .messageText{fill:#333;stroke:none;}#mermaid-svg-fTiNQHIHgXcfYUII .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-fTiNQHIHgXcfYUII .labelText,#mermaid-svg-fTiNQHIHgXcfYUII .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-fTiNQHIHgXcfYUII .loopText,#mermaid-svg-fTiNQHIHgXcfYUII .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-fTiNQHIHgXcfYUII .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-fTiNQHIHgXcfYUII .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-fTiNQHIHgXcfYUII .noteText,#mermaid-svg-fTiNQHIHgXcfYUII .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-fTiNQHIHgXcfYUII .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-fTiNQHIHgXcfYUII .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-fTiNQHIHgXcfYUII .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-fTiNQHIHgXcfYUII .actorPopupMenu{position:absolute;}#mermaid-svg-fTiNQHIHgXcfYUII .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-fTiNQHIHgXcfYUII .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-fTiNQHIHgXcfYUII .actor-man circle,#mermaid-svg-fTiNQHIHgXcfYUII line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-fTiNQHIHgXcfYUII :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 后端逻辑(推测) 1) 查 user-position 表拿 positionIdList 2) 查 position-permission 拿 menus 3) 合并 + 算最终 operation 登录 /user/login (拿 token)跳到 /newHomeGET /staffs/info (queryCurrent)CurrentUser { id, roleId, orgUserVo }getUserPermissionsByStaffId(userId)Permission\[\] (含 operation, buttonPermissions)enrich + 渲染菜单 + 渲染按钮开始用客服专员能看能点的功能
这里有一个隐性约定 :只要
getUserPermissionsByStaffId的后端实现是"按staffId反查岗位再查菜单",那么王小二的权限就完全由"客服专员"这个岗位的授权决定 。后端如果哪天改成"按 roleId 直接查菜单",那角色就复活了------所以这是一个后端实现选择,前端不需要操心。
12.7 流程一览图(正确顺序:④ → ① → ② → ③ → ⑤)
#mermaid-svg-GYfm4U8kbpGgHUqg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-GYfm4U8kbpGgHUqg .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-GYfm4U8kbpGgHUqg .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-GYfm4U8kbpGgHUqg .error-icon{fill:#552222;}#mermaid-svg-GYfm4U8kbpGgHUqg .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-GYfm4U8kbpGgHUqg .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-GYfm4U8kbpGgHUqg .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-GYfm4U8kbpGgHUqg .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-GYfm4U8kbpGgHUqg .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-GYfm4U8kbpGgHUqg .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-GYfm4U8kbpGgHUqg .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-GYfm4U8kbpGgHUqg .marker{fill:#333333;stroke:#333333;}#mermaid-svg-GYfm4U8kbpGgHUqg .marker.cross{stroke:#333333;}#mermaid-svg-GYfm4U8kbpGgHUqg svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-GYfm4U8kbpGgHUqg p{margin:0;}#mermaid-svg-GYfm4U8kbpGgHUqg .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-GYfm4U8kbpGgHUqg .cluster-label text{fill:#333;}#mermaid-svg-GYfm4U8kbpGgHUqg .cluster-label span{color:#333;}#mermaid-svg-GYfm4U8kbpGgHUqg .cluster-label span p{background-color:transparent;}#mermaid-svg-GYfm4U8kbpGgHUqg .label text,#mermaid-svg-GYfm4U8kbpGgHUqg span{fill:#333;color:#333;}#mermaid-svg-GYfm4U8kbpGgHUqg .node rect,#mermaid-svg-GYfm4U8kbpGgHUqg .node circle,#mermaid-svg-GYfm4U8kbpGgHUqg .node ellipse,#mermaid-svg-GYfm4U8kbpGgHUqg .node polygon,#mermaid-svg-GYfm4U8kbpGgHUqg .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-GYfm4U8kbpGgHUqg .rough-node .label text,#mermaid-svg-GYfm4U8kbpGgHUqg .node .label text,#mermaid-svg-GYfm4U8kbpGgHUqg .image-shape .label,#mermaid-svg-GYfm4U8kbpGgHUqg .icon-shape .label{text-anchor:middle;}#mermaid-svg-GYfm4U8kbpGgHUqg .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-GYfm4U8kbpGgHUqg .rough-node .label,#mermaid-svg-GYfm4U8kbpGgHUqg .node .label,#mermaid-svg-GYfm4U8kbpGgHUqg .image-shape .label,#mermaid-svg-GYfm4U8kbpGgHUqg .icon-shape .label{text-align:center;}#mermaid-svg-GYfm4U8kbpGgHUqg .node.clickable{cursor:pointer;}#mermaid-svg-GYfm4U8kbpGgHUqg .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-GYfm4U8kbpGgHUqg .arrowheadPath{fill:#333333;}#mermaid-svg-GYfm4U8kbpGgHUqg .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-GYfm4U8kbpGgHUqg .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-GYfm4U8kbpGgHUqg .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GYfm4U8kbpGgHUqg .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-GYfm4U8kbpGgHUqg .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GYfm4U8kbpGgHUqg .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-GYfm4U8kbpGgHUqg .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-GYfm4U8kbpGgHUqg .cluster text{fill:#333;}#mermaid-svg-GYfm4U8kbpGgHUqg .cluster span{color:#333;}#mermaid-svg-GYfm4U8kbpGgHUqg div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-GYfm4U8kbpGgHUqg .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-GYfm4U8kbpGgHUqg rect.text{fill:none;stroke-width:0;}#mermaid-svg-GYfm4U8kbpGgHUqg .icon-shape,#mermaid-svg-GYfm4U8kbpGgHUqg .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GYfm4U8kbpGgHUqg .icon-shape p,#mermaid-svg-GYfm4U8kbpGgHUqg .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-GYfm4U8kbpGgHUqg .icon-shape .label rect,#mermaid-svg-GYfm4U8kbpGgHUqg .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GYfm4U8kbpGgHUqg .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-GYfm4U8kbpGgHUqg .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-GYfm4U8kbpGgHUqg :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 先于
后端实现 getPermissionsByStaffId
4️⃣ 岗位管理 给 客服专员 配菜单+按钮 updateOrgPosition
1️⃣ 人员管理 创建王小二档案 addOrgUser
2️⃣ 账号管理 敲手机号 → 带出员工 saveUser / nbstaffs
3️⃣ 组织架构-人员分配 把王小二绑到 客服专员 editPositionUser
5️⃣ 王小二登录 getPermissionsByStaffId 下发权限
⚠️ 4️⃣ 必须先于 3️⃣ 做,否则后面所有步骤都白做------"客服专员"岗位没菜单,王小二绑上去也是空权限。
十三、还剩"角色"吗?------它还存在,但只是历史壳子
你的提问非常关键。正确答案:角色(nbroles)依然在数据库里、菜单里、甚至后端请求里,但权限分配层面它已经被"架空"了。
13.1 角色还活着的 3 个证据
| 证据 | 代码位置 | 含义 |
|---|---|---|
| 路由菜单仍有"角色管理"页 | src/pages/accountManagement/role/index.tsx |
让你能"建角色、配角色名" |
登录后存 roleId 全局 |
src/app.tsx:140 |
localStorage.setItem('roleId', '' + currentUser.roleId) |
所有请求自动带 roleId |
src/utils/request.ts:73-92 |
接口侧按角色收口数据范围 |
13.2 角色失去的 1 个能力
| 失去的能力 | 代码证据 |
|---|---|
| "角色直接配菜单权限" | accountManagement/role/index.tsx 里 permissionOperations 全部被注释,仅剩 positionIdList 一个字段 |
所以更准确的描述 :角色 = 岗位合集 + 数据范围归属。岗位决定"能做什么菜单/按钮";角色决定"登录身份 + 接口能拉哪些范围的数据"。
13.3 request.ts 自动注入 roleId 是关键
ts
// src/utils/request.ts:73-92
newOptions.params.roleId = roleId
newOptions.data.roleId = roleId
⚠️ 注意:所有请求都带 roleId,但后端具体拿它干啥要看后端实现 。从代码里能看出它至少 参与了"接口能看哪些公司/项目"的数据范围收口。它不是权限分配的核心,权限分配由 permissions(来自岗位)决定。
教学要点:很多新人误以为"我没给账号分配角色 = 没权限",但其实:
- 账号 = 登录身份(必须创建)
- 员工 = 档案(必须创建)
- 岗位绑员工 = 拿到操作菜单的能力(必须做)
- 角色 = 身份 + 数据范围(建议做,不做也能用,只是接口能拉的范围可能不对)
13.4 部门(home_org_dept)也只剩数据壳
| 是否还在用 | 在哪用 | 是否影响 UI |
|---|---|---|
| 数据层 | home_org_dept 表 + 后端 API |
仅在员工表的 deptNames 字段展示 |
| 前端树 | ❌ 不渲染 | 完全没有"部门管理"页功能入口(路由仍在但没被任何菜单引用) |
| 业务 | 工单流程配置里"指定部门" | parameterManagement/addProcess 里 deptIds 字段 |
教学要点:保留部门主要是为了工单/审批流程的"审批节点"配置 。组织树不展示不代表业务没用,别误删后端表。
13.5 终极模型图(修订版)
#mermaid-svg-LKeCeoVpATWUdjei{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-LKeCeoVpATWUdjei .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-LKeCeoVpATWUdjei .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-LKeCeoVpATWUdjei .error-icon{fill:#552222;}#mermaid-svg-LKeCeoVpATWUdjei .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-LKeCeoVpATWUdjei .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-LKeCeoVpATWUdjei .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-LKeCeoVpATWUdjei .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-LKeCeoVpATWUdjei .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-LKeCeoVpATWUdjei .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-LKeCeoVpATWUdjei .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-LKeCeoVpATWUdjei .marker{fill:#333333;stroke:#333333;}#mermaid-svg-LKeCeoVpATWUdjei .marker.cross{stroke:#333333;}#mermaid-svg-LKeCeoVpATWUdjei svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-LKeCeoVpATWUdjei p{margin:0;}#mermaid-svg-LKeCeoVpATWUdjei .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-LKeCeoVpATWUdjei .cluster-label text{fill:#333;}#mermaid-svg-LKeCeoVpATWUdjei .cluster-label span{color:#333;}#mermaid-svg-LKeCeoVpATWUdjei .cluster-label span p{background-color:transparent;}#mermaid-svg-LKeCeoVpATWUdjei .label text,#mermaid-svg-LKeCeoVpATWUdjei span{fill:#333;color:#333;}#mermaid-svg-LKeCeoVpATWUdjei .node rect,#mermaid-svg-LKeCeoVpATWUdjei .node circle,#mermaid-svg-LKeCeoVpATWUdjei .node ellipse,#mermaid-svg-LKeCeoVpATWUdjei .node polygon,#mermaid-svg-LKeCeoVpATWUdjei .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-LKeCeoVpATWUdjei .rough-node .label text,#mermaid-svg-LKeCeoVpATWUdjei .node .label text,#mermaid-svg-LKeCeoVpATWUdjei .image-shape .label,#mermaid-svg-LKeCeoVpATWUdjei .icon-shape .label{text-anchor:middle;}#mermaid-svg-LKeCeoVpATWUdjei .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-LKeCeoVpATWUdjei .rough-node .label,#mermaid-svg-LKeCeoVpATWUdjei .node .label,#mermaid-svg-LKeCeoVpATWUdjei .image-shape .label,#mermaid-svg-LKeCeoVpATWUdjei .icon-shape .label{text-align:center;}#mermaid-svg-LKeCeoVpATWUdjei .node.clickable{cursor:pointer;}#mermaid-svg-LKeCeoVpATWUdjei .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-LKeCeoVpATWUdjei .arrowheadPath{fill:#333333;}#mermaid-svg-LKeCeoVpATWUdjei .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-LKeCeoVpATWUdjei .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-LKeCeoVpATWUdjei .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LKeCeoVpATWUdjei .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-LKeCeoVpATWUdjei .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LKeCeoVpATWUdjei .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-LKeCeoVpATWUdjei .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-LKeCeoVpATWUdjei .cluster text{fill:#333;}#mermaid-svg-LKeCeoVpATWUdjei .cluster span{color:#333;}#mermaid-svg-LKeCeoVpATWUdjei div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-LKeCeoVpATWUdjei .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-LKeCeoVpATWUdjei rect.text{fill:none;stroke-width:0;}#mermaid-svg-LKeCeoVpATWUdjei .icon-shape,#mermaid-svg-LKeCeoVpATWUdjei .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LKeCeoVpATWUdjei .icon-shape p,#mermaid-svg-LKeCeoVpATWUdjei .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-LKeCeoVpATWUdjei .icon-shape .label rect,#mermaid-svg-LKeCeoVpATWUdjei .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LKeCeoVpATWUdjei .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-LKeCeoVpATWUdjei .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-LKeCeoVpATWUdjei :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 被分配
挂
多对多
多对多 via editPositionUser
多对多
登录
roleId 透传
菜单+按钮下发
render
员工 home_org_user 姓名/手机/工号/入职时间
后台账号 nbstaffs 登录身份
角色 nbroles 岗位合集 + 数据范围
岗位 home_org_position 绑菜单+按钮权限
权限 home_org_permission operation 两位
getInitialState
页面 + 按钮
关键交叉:
- 员工→岗位的边(实线):权限的真路径
- 账号→角色→岗位的边(虚线):身份的虚路径,已经不能配菜单了,只参与数据范围
十四、给"删除角色/删除部门"这种想改架构的同事的话
- 不要从数据库 DELETE 角色表 :仍有
preRoleId、roleId字段散落在nbstaffs、request.ts里,盲目删会引发全站请求异常。 - 不要重写回"角色绑菜单" :代码里的大段注释就是前人踩过的坑------
permissionOperations散在角色配置时维护成本极高。 - 想统一"权限 vs 范围" :建议推动让
roleId完全不参与权限,仅做"登录身份 + 数据范围"。这样能彻底把"权限"和"范围"两件事解耦。 - 想给部门加 UI :先看
OrgTableByLevel里保留的deptVoList/deptId分支,数据通道已通 ------补一个/organization/deptManagement页面 + 改OrganizationMenu渲染就行,不需要动权限层。
十五、结语(来自资深前辈的几句话)
- 这个项目的权限模型设计是清晰、收敛、易维护的------比"角色直接绑菜单"少了一层耦合。多花一天把这个链路摸透,后面写所有页面就都能提前判断"我这个按钮需不需要 canDo 兜底"。
- 当前体系下,最危险的修改动作是
getPermissionConfig接口结构变动 ------它驱动了isLeafNode推断,进而影响markDisabledWhenNoChildren,进而影响菜单项能否点击。改它之前,先画一张表把"原本是父节点、被过滤后变叶子"的所有 path 列出来。完整原理见 §7.1·补。 - 接手新业务时,先打开
positionManagement/index.tsx看完整个permissionOperations的组装逻辑,你将获得这个项目"权限语义"的 80% 真相。 - "角色是否被取消"的标准答案 :从用户可见的权限配置视角看------是的,角色被取消了 ;从接口的数据范围视角看------没有,roleId 还在透传 。一句话:角色 = 身份的"标签",岗位 = 权限的"载体"。新员工流程请严格按 §12 的 5 步走。