亮点-组织架构与权限体系

组织架构与权限体系 --- 资深前端视角教学手册

本文档基于 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 出现的位置只有两处 :
    1. src/layouts/AreaLayoutOrganization/index.tsx --- 给了所有人(包括 AreaSelectorOrganization)。
    2. 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

这是全项目最复杂的页面,承担三种职责:

  1. 岗位的 CRUD(新增/修改/删除);
  2. 菜单权限的可视化配置(左侧岗位 + 右侧模块 Tab + 操作 checkbox 树);
  3. 按钮级权限的可视化配置(每个菜单节点下挂按钮集合);
  4. 保存时一次性回传给 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)

要点:

  1. 后端只返回"该用户有权的菜单" (getPermissionsByUserId),但 getPermissionConfig 返回的是"全量菜单"------前端用它反推 isParentOrigin/isLeafNode,从而正确分组父子。
  2. buttonPermissions 是按页面 path 聚合的 Record<path, Record<action, boolean>>,源头来自后端 Permission.buttonPermissions 字段。
  3. 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: false
  • id=10(组织管理):在全量字典里被人 pid=10 引过 → parentIdSet.has(10) === true → isLeafNode: false
  • id=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)
  • 换来 isLeafNode 100% 正确
  • 换来 markDisabledWhenNoChildren 100% 正确

结论 :这个性能 trade-off 是值得的。如果未来 getPermissionConfig 变得超级慢(> 1s),可以考虑:

  1. 让后端在 getUserPermissionsByStaffId 里直接带一个 isParentOrigin 字段回来
  2. 或前端用 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 已知踩坑

  1. Color 永远返回 'blue' (getTagColor)--- 看着像 Bug 但不是,影响视觉一致性。新人常自做主张改七色,结果立刻撞上"和原型稿对不齐"。
  2. STORAGE_KEY = 'orgTree_selected_marker' --- 写着但 useEffect 恢复逻辑被注释。读代码别去"修复它",先确认后端 ID 是否复用。
  3. useCanEdit 只看 edit 位 --- 如果业务需要"查看权也可见菜单项但禁用",不要去改 hook 的语义,更好的做法是在按钮级别控制。
  4. buttonPermissions 没配置时降级放行 --- 新业务上线时必须主动配置 action=false,否则所有按钮依然可点!新人常踩。
  5. 职能中心的 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 步,强顺序依赖:

  1. 岗位授权 (岗位管理):给"客服专员"岗位配菜单 + 按钮权限 → 落 permissionOperations
  2. 人员建档 (人员管理):创建员工档案 home_org_user
  3. 开通账号 (账号管理):敲手机号 + 自动回填 → 落 nbstaffs
  4. 人员分配 (组织架构):把员工绑到岗位 → 写 user ↔ position 关联
  5. 员工登录 :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:

  1. 同号复用场景:员工离职 → 再入职,手机号相同 → 旧账号是激活还是重建?拆开后可以"激活旧账号"。
  2. 超管账号保护 :id=1 的系统账号必须能从员工列表里隐藏------合并后无法区分。
  3. 多端禁用独立 :isDeleted(APP 端)和 staffIsDeleted(后台端)互不影响------合并后只能同生共死。
  4. 审计合规 :离职员工如果在 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 记录,技术上不难。但作者故意没做,因为:

  1. 超管账号保护 :id=1 的系统账号必须能独立于员工档案存在------合并后无法区分
  2. 多端禁用独立 :员工档案不该背负"是否禁用后台/APP"的状态------那应该挂在 nbstaffs 上
  3. 审计合规 :员工离职后 home_org_user.employmentStatus=0 但 nbstaffs 必须保留以追溯登录历史
  4. 不同时机: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
页面 + 按钮

关键交叉:

  • 员工→岗位的边(实线):权限的真路径
  • 账号→角色→岗位的边(虚线):身份的虚路径,已经不能配菜单了,只参与数据范围

十四、给"删除角色/删除部门"这种想改架构的同事的话

  1. 不要从数据库 DELETE 角色表 :仍有 preRoleId、roleId 字段散落在 nbstaffs、request.ts 里,盲目删会引发全站请求异常。
  2. 不要重写回"角色绑菜单" :代码里的大段注释就是前人踩过的坑------permissionOperations 散在角色配置时维护成本极高。
  3. 想统一"权限 vs 范围" :建议推动让 roleId 完全不参与权限,仅做"登录身份 + 数据范围"。这样能彻底把"权限"和"范围"两件事解耦。
  4. 想给部门加 UI :先看 OrgTableByLevel 里保留的 deptVoList/deptId 分支,数据通道已通 ------补一个 /organization/deptManagement 页面 + 改 OrganizationMenu 渲染就行,不需要动权限层。

十五、结语(来自资深前辈的几句话)

  • 这个项目的权限模型设计是清晰、收敛、易维护的------比"角色直接绑菜单"少了一层耦合。多花一天把这个链路摸透,后面写所有页面就都能提前判断"我这个按钮需不需要 canDo 兜底"。
  • 当前体系下,最危险的修改动作是 getPermissionConfig 接口结构变动 ------它驱动了 isLeafNode 推断,进而影响 markDisabledWhenNoChildren,进而影响菜单项能否点击。改它之前,先画一张表把"原本是父节点、被过滤后变叶子"的所有 path 列出来。完整原理见 §7.1·补。
  • 接手新业务时,先打开 positionManagement/index.tsx 看完整个 permissionOperations 的组装逻辑,你将获得这个项目"权限语义"的 80% 真相。
  • "角色是否被取消"的标准答案 :从用户可见的权限配置视角看------是的,角色被取消了 ;从接口的数据范围视角看------没有,roleId 还在透传 。一句话:角色 = 身份的"标签",岗位 = 权限的"载体"。新员工流程请严格按 §12 的 5 步走。
相关推荐
磐链科技1 小时前
DApp技术架构全面解析:从区块链底层、前后端开发到链上数据交互体系设计
架构·区块链
咖啡八杯2 小时前
异常体系设计:三类策略(i18n code、message、受检异常)
java·架构·代码规范
海宇服务2 小时前
零信任架构实战:基于海宇公安二要素认证即时版构建自动化跨境个人额度申报实名网关
运维·人工智能·架构·自动化
言之。2 小时前
Agent 与沙箱:架构与关系解析
架构
淡海水2 小时前
03-03-架构篇-SGen GC架构解析
unity·架构·c#·gc·sgen
java_nnnn2 小时前
Redis 初识:从单体架构到分布式架构的演进
redis·分布式·架构
硅基手札3 小时前
【Linux 内核专栏 18】进程间通信 IPC(下)— Socket·Netlink·fd 三件套·futex·线程同步
linux·运维·驱动开发·单片机·嵌入式硬件·mcu·架构
安易算力12 小时前
昇腾CANN算子开发与MindSpore部署调优实践:从Ascend C到模型推理全链路
网络·架构
小马不起床13 小时前
零基础玩转 FastAPI_11:前端后台管理页面
前端·状态模式·fastapi