permissions.js 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. /**
  2. * 角色→权限映射表(《角色权限与菜单归类整合方案》B-4)
  3. * 每个角色映射一组权限标识,支持通配符 '*' 和前缀通配 'prefix:*'
  4. *
  5. * 平台角色: admin / operator / finance
  6. * 入驻角色: butler / knowledge_admin / teacher / nutritionist / supplier / activity_manager
  7. * 遗留角色: article_manager / activity_admin / supplier_admin(历史数据兼容)
  8. */
  9. const ROLE_PERMISSIONS = {
  10. // 超管:全部权限
  11. admin: ['*'],
  12. // 运营:审核全量、活动全量、任务全量、家庭/商城/知识/测评只读
  13. operator: ['dashboard', 'audit:*', 'activity:*', 'task:*', 'assessment:read', 'family:read', 'commerce:read', 'articles:read', 'knowledge:read'],
  14. // 财务:财务/退款/提现、订单只读、CF值、商城营销、业务管理
  15. finance: ['dashboard', 'finance:*', 'audit:withdraw', 'audit:refund', 'commerce:read', 'commerce:orders:read', 'biz:*', 'cf-value:*'],
  16. // 管家:家庭客服 + 规划师 + 营养师权限并集(getEffectivePermissions 中自动并入 teacher/nutritionist)
  17. butler: ['dashboard', 'family:*', 'task:*', 'reward:*', 'service:*', 'growth:*', 'biz:*',
  18. 'assessment:dan', 'health:*', 'diet:*', 'report:audit'],
  19. // 知识管理员
  20. knowledge_admin: ['dashboard', 'articles:*', 'knowledge:*'],
  21. // 成长规划师
  22. teacher: ['dashboard', 'service:*', 'growth:*', 'biz:*', 'assessment:dan', 'family:bound', 'messages'],
  23. // 营养师
  24. nutritionist: ['dashboard', 'health:*', 'diet:*', 'report:audit', 'service:family', 'biz:orders', 'messages'],
  25. // 供应商(仅自己的商品/库存/订单/结算)+ 业务管理
  26. supplier: ['dashboard', 'commerce:self', 'inventory:self', 'order:self', 'settlement:self', 'supply:member', 'biz:*'],
  27. // 活动管理员(仅自己的活动)+ 业务管理
  28. activity_manager: ['dashboard', 'activity:self', 'activity:registration', 'biz:*'],
  29. // ---- 遗留角色兼容(与后端 AdminRoles.normalize 对应)----
  30. article_manager: ['dashboard', 'articles:*', 'knowledge:*'],
  31. activity_admin: ['dashboard', 'activity:self', 'activity:registration'],
  32. supplier_admin: ['dashboard', 'commerce:self', 'inventory:self', 'order:self', 'settlement:self', 'supply:member'],
  33. }
  34. /**
  35. * 遗留角色 → 新角色归一化(与后端 AdminRoles.normalize 保持一致)
  36. */
  37. function normalizeRole(role) {
  38. if (!role) return role
  39. if (role === 'activity_admin') return 'activity_manager'
  40. if (role === 'article_admin' || role === 'article_manager') return 'knowledge_admin'
  41. if (role === 'supplier_admin') return 'supplier'
  42. return role
  43. }
  44. /**
  45. * 获取用户有效权限列表
  46. * butler 单向兼容:分配 butler 时自动附带 teacher 与 nutritionist 的权限(权限求并集,角色记录仍为 butler)
  47. * @param {string[]} roles - 用户拥有的角色列表
  48. * @returns {string[]} - 去重后的权限标识列表
  49. */
  50. export function getEffectivePermissions(roles) {
  51. if (!roles || roles.length === 0) return []
  52. const permSet = new Set()
  53. const normalizedRoles = []
  54. for (let i = 0; i < roles.length; i++) {
  55. const normalized = normalizeRole(roles[i])
  56. normalizedRoles.push(normalized)
  57. }
  58. for (const role of normalizedRoles) {
  59. const perms = ROLE_PERMISSIONS[role]
  60. if (perms) {
  61. for (const p of perms) {
  62. permSet.add(p)
  63. }
  64. }
  65. }
  66. // B-2 单向兼容:butler 附带 teacher + nutritionist 权限并集
  67. if (normalizedRoles.indexOf('butler') >= 0) {
  68. const union = [].concat(ROLE_PERMISSIONS.teacher || [], ROLE_PERMISSIONS.nutritionist || [])
  69. for (const p of union) {
  70. permSet.add(p)
  71. }
  72. }
  73. return Array.from(permSet)
  74. }
  75. /**
  76. * 判断是否拥有指定权限
  77. * @param {string[]} userPerms - 用户的有效权限列表
  78. * @param {string} required - 需要的权限标识(支持 'prefix:*' 通配、逗号分隔多值任一匹配)
  79. * @returns {boolean}
  80. *
  81. * 匹配规则:
  82. * - '*' → 全部放行
  83. * - required 含逗号 → 任一命中即通过(如 'family:read,family:bound')
  84. * - 精确匹配
  85. * - required 为单段(组级权限如 'audit')→ 拥有 'audit:*' 或任何 'audit:xxx' 即通过
  86. * - required 为 'p:*'(模块通配)→ 拥有 'p:*' 或任何 'p:xxx' 即通过(模块只读/自管可见)
  87. * - required 为 'p:sub' → 拥有 'p:*'、'p:read'(模块只读)或任何以 'p:sub:' 开头的更具体权限即通过
  88. */
  89. export function hasPermission(userPerms, required) {
  90. if (!userPerms || !required) return false
  91. if (userPerms.indexOf('*') >= 0) return true
  92. // 逗号分隔多值:任一命中即通过
  93. if (required.indexOf(',') >= 0) {
  94. const parts = required.split(',')
  95. for (let i = 0; i < parts.length; i++) {
  96. if (matchSingle(userPerms, parts[i].trim())) return true
  97. }
  98. return false
  99. }
  100. return matchSingle(userPerms, required)
  101. }
  102. function matchSingle(userPerms, required) {
  103. if (!required) return false
  104. // 精确匹配
  105. if (userPerms.indexOf(required) >= 0) return true
  106. const colonIndex = required.indexOf(':')
  107. if (colonIndex < 0) {
  108. // 组级权限 'audit':拥有 'audit:*' 或任何 'audit:xxx'
  109. if (userPerms.indexOf(required + ':*') >= 0) return true
  110. for (let i = 0; i < userPerms.length; i++) {
  111. if (userPerms[i].indexOf(required + ':') === 0) return true
  112. }
  113. return false
  114. }
  115. const prefix = required.substring(0, colonIndex)
  116. if (colonIndex === required.length - 2 && required.charAt(required.length - 1) === '*') {
  117. // 模块通配 'p:*':拥有 'p:*' 或任何 'p:xxx'
  118. if (userPerms.indexOf(prefix + ':*') >= 0) return true
  119. for (let i = 0; i < userPerms.length; i++) {
  120. if (userPerms[i].indexOf(prefix + ':') === 0) return true
  121. }
  122. return false
  123. }
  124. // 具体权限 'p:sub'
  125. // 1) 拥有模块通配 'p:*'
  126. if (userPerms.indexOf(prefix + ':*') >= 0) return true
  127. for (let i = 0; i < userPerms.length; i++) {
  128. const perm = userPerms[i]
  129. // 2) 模块只读 'p:read' 可见模块内页面(数据安全由后端行级隔离保证)
  130. if (perm === prefix + ':read') return true
  131. // 3) 拥有更具体权限 'p:sub:xxx' 蕴含粗粒度 'p:sub'(如 finance:cf-value:read → cf-value:manage 场景由通配覆盖)
  132. if (perm.indexOf(required + ':') === 0) return true
  133. }
  134. return false
  135. }
  136. /**
  137. * 入驻角色(登录后默认首页为个人版看板)
  138. */
  139. export const RESIDENT_ROLES = ['butler', 'knowledge_admin', 'teacher', 'nutritionist', 'supplier', 'activity_manager']
  140. /**
  141. * 入驻角色默认首页(登录后/访问 /dashboard 时跳转到个人工作台,B-6)
  142. * 平台角色(admin/operator/finance)返回 '/' 即平台首页
  143. */
  144. const ROLE_HOME_PAGES = {
  145. butler: '/my-families',
  146. teacher: '/teacher-dashboard',
  147. nutritionist: '/health-reports',
  148. supplier: '/product-manage',
  149. activity_manager: '/activities',
  150. knowledge_admin: '/article-manage',
  151. }
  152. export function getRoleHomePage(roles) {
  153. if (!roles || roles.length === 0) return '/'
  154. let home = null
  155. for (let i = 0; i < roles.length; i++) {
  156. const normalized = normalizeRole(roles[i])
  157. // 平台角色优先:拥有平台角色则进平台首页
  158. if (normalized === 'admin' || normalized === 'operator' || normalized === 'finance') {
  159. return '/'
  160. }
  161. if (!home && ROLE_HOME_PAGES[normalized]) {
  162. home = ROLE_HOME_PAGES[normalized]
  163. }
  164. }
  165. return home || '/'
  166. }
  167. /**
  168. * 判断角色列表是否为入驻角色(非平台角色 admin/operator/finance)
  169. */
  170. export function isResident(roles) {
  171. if (!roles || roles.length === 0) return false
  172. for (let i = 0; i < roles.length; i++) {
  173. const normalized = normalizeRole(roles[i])
  174. if (normalized === 'admin' || normalized === 'operator' || normalized === 'finance') {
  175. return false
  176. }
  177. if (RESIDENT_ROLES.indexOf(normalized) >= 0) {
  178. return true
  179. }
  180. }
  181. return false
  182. }
  183. /**
  184. * 获取角色中文名
  185. */
  186. export function getRoleLabel(role) {
  187. const labels = {
  188. admin: '超级管理员',
  189. operator: '运营',
  190. finance: '财务',
  191. butler: '管家',
  192. knowledge_admin: '知识管理员',
  193. teacher: '成长规划师',
  194. nutritionist: '营养师',
  195. supplier: '供应商',
  196. activity_manager: '活动管理员',
  197. // 遗留角色
  198. article_manager: '知识管理员',
  199. activity_admin: '活动管理员',
  200. supplier_admin: '供应商',
  201. }
  202. return labels[role] || role
  203. }