sys_post.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622
  1. package service
  2. import (
  3. "cold-delivery/app/admin/model"
  4. "cold-delivery/app/admin/service/dto"
  5. "cold-delivery/common/actions"
  6. cDto "cold-delivery/common/dto"
  7. "cold-delivery/common/global"
  8. "errors"
  9. "fmt"
  10. "strings"
  11. "github.com/casbin/casbin/v2"
  12. "gogs.baozhida.cn/zoie/OAuth-core/sdk"
  13. "gogs.baozhida.cn/zoie/OAuth-core/sdk/config"
  14. "gogs.baozhida.cn/zoie/OAuth-core/service"
  15. "gorm.io/gorm"
  16. )
  17. type SysPost struct {
  18. service.Service
  19. }
  20. // GetPage 分页查询岗位列表
  21. func (e *SysPost) GetPage(c *dto.SysPostGetPageReq, list *[]model.SysPost, count *int64, p *actions.DataPermission) error {
  22. var err error
  23. var data model.SysPost
  24. db := e.Orm.Model(&data).
  25. Scopes(
  26. cDto.MakeCondition(c.GetNeedSearch()),
  27. cDto.Paginate(c.GetPageSize(), c.GetPageIndex()),
  28. )
  29. // 超级管理员(deptId=0)可以看到所有部门的岗位
  30. if p.DeptId != 0 {
  31. db = db.Scopes(actions.Permission(data.TableName(), p))
  32. }
  33. err = db.Find(list).Limit(-1).Offset(-1).
  34. Count(count).Error
  35. if err != nil {
  36. e.Log.Errorf("db error: %s", err)
  37. return global.GetFailedErr
  38. }
  39. return nil
  40. }
  41. // Get 获取单个岗位详情(含权限菜单ID列表)
  42. func (e *SysPost) Get(d *dto.SysPostGetReq, postModel *model.SysPost, p *actions.DataPermission) error {
  43. db := e.Orm
  44. // 超级管理员可以看到所有部门的岗位
  45. if p.DeptId != 0 {
  46. db = db.Scopes(actions.Permission(postModel.TableName(), p))
  47. }
  48. err := db.First(postModel, d.GetId()).Error
  49. if err != nil {
  50. e.Log.Errorf("db error: %s", err)
  51. if errors.Is(err, gorm.ErrRecordNotFound) {
  52. return global.GetNotFoundOrNoPermissionErr
  53. }
  54. return global.GetFailedErr
  55. }
  56. postModel.MenuIds, err = e.GetPostMenuIds(postModel.Id)
  57. if err != nil {
  58. e.Log.Errorf("get menuIds error, %s", err)
  59. return global.GetFailedErr
  60. }
  61. return nil
  62. }
  63. // Insert 创建岗位(含菜单权限分配)
  64. func (e *SysPost) Insert(c *dto.SysPostInsertReq) error {
  65. var err error
  66. var data model.SysPost
  67. tx := e.Orm.Begin()
  68. defer func() {
  69. if err != nil {
  70. tx.Rollback()
  71. } else {
  72. tx.Commit()
  73. }
  74. }()
  75. // 检查岗位编码在同一个部门下是否已存在
  76. var count int64
  77. err = tx.Model(&data).Where("post_code = ? AND dept_id = ?", c.PostCode, c.DeptId).Count(&count).Error
  78. if err != nil {
  79. e.Log.Errorf("db error: %s", err)
  80. return global.CreateFailedErr
  81. }
  82. if count > 0 {
  83. return errors.New("该部门下岗位编码已存在")
  84. }
  85. // 创建岗位
  86. c.Generate(&data)
  87. err = tx.Create(&data).Error
  88. if err != nil {
  89. e.Log.Errorf("db error: %s", err)
  90. return global.CreateFailedErr
  91. }
  92. c.Id = data.Id
  93. // 添加岗位菜单关联
  94. if len(c.MenuIds) > 0 {
  95. var menuList []model.SysMenu
  96. err = tx.Where("id in (?)", c.MenuIds).Find(&menuList).Error
  97. if err != nil {
  98. return global.GetFailedErr
  99. }
  100. if len(menuList) == 0 {
  101. return errors.New("菜单不存在!")
  102. }
  103. var postMenuList []model.SysPostMenu
  104. for _, v := range menuList {
  105. postMenuObj := model.SysPostMenu{
  106. PostId: data.Id,
  107. MenuId: v.Id,
  108. }
  109. postMenuObj.SetCreateBy(c.CreateBy)
  110. postMenuList = append(postMenuList, postMenuObj)
  111. }
  112. err = tx.Save(&postMenuList).Error
  113. if err != nil {
  114. e.Log.Errorf("save menuIds error, %s", err)
  115. return global.UpdateFailedErr
  116. }
  117. // 同步写入 Casbin 规则
  118. err = e.syncCasbinRules(tx, data, menuList)
  119. if err == nil {
  120. cb := sdk.Runtime.GetCasbinKey(config.ApplicationConfig.Host)
  121. if cb != nil {
  122. cb.LoadPolicy()
  123. }
  124. }
  125. if err != nil {
  126. e.Log.Errorf("sync casbin rules error, %s", err)
  127. return global.UpdateFailedErr
  128. }
  129. }
  130. return nil
  131. }
  132. // Update 修改岗位(含菜单权限重新分配)
  133. func (e *SysPost) Update(c *dto.SysPostUpdateReq, p *actions.DataPermission, cb *casbin.SyncedEnforcer) error {
  134. var err error
  135. tx := e.Orm.Begin()
  136. defer func() {
  137. if err != nil {
  138. tx.Rollback()
  139. } else {
  140. tx.Commit()
  141. }
  142. }()
  143. var postModel = model.SysPost{}
  144. // 查询岗位是否存在
  145. db := e.Orm
  146. if p.DeptId != 0 {
  147. db = db.Scopes(actions.Permission(postModel.TableName(), p))
  148. }
  149. err = db.First(&postModel, c.GetId()).Error
  150. if err != nil {
  151. e.Log.Errorf("db error: %s", err)
  152. if errors.Is(err, gorm.ErrRecordNotFound) {
  153. return global.UpdateNotFoundOrNoPermissionErr
  154. }
  155. return global.UpdateFailedErr
  156. }
  157. // 更新岗位基本信息
  158. c.Generate(&postModel)
  159. // deptId 由 Generate 方法从更新请求中设置,允许修改部门
  160. err = tx.Save(&postModel).Error
  161. if err != nil {
  162. if errors.Is(err, model.ErrForbidUpdateSysPost) {
  163. return err
  164. }
  165. e.Log.Errorf("db error: %s", err)
  166. return global.UpdateFailedErr
  167. }
  168. // 清除旧的 Casbin 规则(用旧的 postCode)
  169. _, _ = cb.RemoveFilteredPolicy(0, "role:"+postModel.PostCode)
  170. _, _ = cb.RemoveFilteredPolicy(0, postModel.PostCode) // 清除旧格式规则(无 role: 前缀)
  171. // 删除旧的菜单关联
  172. err = tx.Where("post_id = ?", postModel.Id).Delete(&model.SysPostMenu{}).Error
  173. if err != nil {
  174. e.Log.Errorf("db error: %s", err)
  175. return global.UpdateFailedErr
  176. }
  177. // 重新添加菜单关联
  178. if len(c.MenuIds) > 0 {
  179. var menuList []model.SysMenu
  180. err = e.Orm.Where("id in (?)", c.MenuIds).Find(&menuList).Error
  181. if err != nil {
  182. return global.GetFailedErr
  183. }
  184. if len(menuList) == 0 {
  185. return errors.New("菜单不存在!")
  186. }
  187. var postMenuList []model.SysPostMenu
  188. for _, v := range menuList {
  189. postMenuObj := model.SysPostMenu{
  190. PostId: postModel.Id,
  191. MenuId: v.Id,
  192. }
  193. postMenuObj.SetCreateBy(c.UpdateBy)
  194. postMenuList = append(postMenuList, postMenuObj)
  195. }
  196. err = tx.Save(&postMenuList).Error
  197. if err != nil {
  198. e.Log.Errorf("save menuIds error, %s", err)
  199. return global.UpdateFailedErr
  200. }
  201. // 同步写入 Casbin 规则
  202. err = e.syncCasbinRules(tx, postModel, menuList)
  203. if err == nil {
  204. cb.LoadPolicy()
  205. }
  206. if err != nil {
  207. e.Log.Errorf("sync casbin rules error, %s", err)
  208. return global.UpdateFailedErr
  209. }
  210. }
  211. // 清除缓存
  212. _ = model.DeletePostMenuCache(postModel.Id)
  213. c.Id = postModel.Id
  214. return nil
  215. }
  216. // Remove 删除岗位(逻辑删除)
  217. func (e *SysPost) Remove(c *dto.SysPostDeleteReq, p *actions.DataPermission, cb *casbin.SyncedEnforcer) error {
  218. var err error
  219. tx := e.Orm.Begin()
  220. defer func() {
  221. if err != nil {
  222. tx.Rollback()
  223. } else {
  224. tx.Commit()
  225. }
  226. }()
  227. // 检查是否有用户关联该岗位
  228. var userCount int64
  229. err = tx.Table("sys_user").Where("post_id = ? AND deleted_at IS NULL", c.Id).Count(&userCount).Error
  230. if err != nil {
  231. e.Log.Errorf("db error: %s", err)
  232. return global.DeleteFailedErr
  233. }
  234. if userCount > 0 {
  235. return errors.New(fmt.Sprintf("有 %d 个用户关联了该岗位,禁止删除!", userCount))
  236. }
  237. var postModel model.SysPost
  238. rdb := e.Orm
  239. if p.DeptId != 0 {
  240. rdb = rdb.Scopes(actions.Permission(postModel.TableName(), p))
  241. }
  242. err = rdb.First(&postModel, c.GetId()).Error
  243. if err != nil {
  244. e.Log.Errorf("db error: %s", err)
  245. if errors.Is(err, gorm.ErrRecordNotFound) {
  246. return global.DeleteNotFoundOrNoPermissionErr
  247. }
  248. return global.DeleteFailedErr
  249. }
  250. // GORM 逻辑删除(自动设置 deleted_at)
  251. db := tx.Delete(&postModel)
  252. if err = db.Error; err != nil {
  253. if errors.Is(err, model.ErrForbidDeleteSysPost) {
  254. return err
  255. }
  256. e.Log.Errorf("db error: %s", err)
  257. return global.DeleteFailedErr
  258. }
  259. if db.RowsAffected == 0 {
  260. return global.DeleteNotFoundOrNoPermissionErr
  261. }
  262. // 删除岗位关联的菜单
  263. err = tx.Where("post_id = ?", postModel.Id).Delete(&model.SysPostMenu{}).Error
  264. if err != nil {
  265. return global.UpdateFailedErr
  266. }
  267. // 清除 Casbin 规则
  268. _, _ = cb.RemoveFilteredPolicy(0, "role:"+postModel.PostCode)
  269. _, _ = cb.RemoveFilteredPolicy(0, postModel.PostCode) // 清除旧格式规则(无 role: 前缀)
  270. // 清除缓存
  271. _ = model.DeletePostMenuCache(postModel.Id)
  272. return nil
  273. }
  274. // GetAll 获取所有岗位(下拉列表用)
  275. func (e *SysPost) GetAll(deptId int, list *[]model.SysPost, p *actions.DataPermission) error {
  276. var err error
  277. var data model.SysPost
  278. db := e.Orm.Model(&data)
  279. // 超级管理员可以看到所有部门的岗位
  280. if p.DeptId != 0 {
  281. db = db.Scopes(actions.Permission(data.TableName(), p))
  282. }
  283. if deptId > 0 {
  284. db = db.Where("dept_id = ?", deptId)
  285. }
  286. err = db.Order("sort asc").Find(list).Error
  287. if err != nil {
  288. e.Log.Errorf("db error: %s", err)
  289. return global.GetFailedErr
  290. }
  291. return nil
  292. }
  293. // GetPostMenuIds 获取岗位已分配的菜单ID列表
  294. func (e *SysPost) GetPostMenuIds(postId int) ([]int, error) {
  295. var menuIds []int
  296. err := e.Orm.Model(&model.SysPostMenu{}).Where("post_id = ?", postId).Pluck("menu_id", &menuIds).Error
  297. if err != nil {
  298. return []int{}, global.GetFailedErr
  299. }
  300. return menuIds, nil
  301. }
  302. // GetMenuTree 获取完整菜单树(用于权限分配界面)
  303. func (e *SysPost) GetMenuTree() ([]dto.MenuLabel, error) {
  304. // 直接查询所有菜单,不依赖 SysMenu.GetList 的复杂搜索逻辑
  305. var menuList []model.SysMenu
  306. err := e.Orm.Order("sort asc").Find(&menuList).Error
  307. if err != nil {
  308. e.Log.Errorf("db error: %s", err)
  309. return nil, global.GetFailedErr
  310. }
  311. // 构建菜单树
  312. tree := make([]dto.MenuLabel, 0)
  313. for i := 0; i < len(menuList); i++ {
  314. if menuList[i].ParentId != 0 {
  315. continue
  316. }
  317. label := dto.MenuLabel{}
  318. label.Id = menuList[i].Id
  319. label.Label = menuList[i].Title
  320. buildMenuLabelTree(&menuList, &label)
  321. tree = append(tree, label)
  322. }
  323. return tree, nil
  324. }
  325. // buildMenuLabelTree 递归构建菜单标签树
  326. func buildMenuLabelTree(menuList *[]model.SysMenu, parent *dto.MenuLabel) {
  327. list := *menuList
  328. children := make([]dto.MenuLabel, 0)
  329. for j := 0; j < len(list); j++ {
  330. if parent.Id != list[j].ParentId {
  331. continue
  332. }
  333. child := dto.MenuLabel{}
  334. child.Id = list[j].Id
  335. child.Label = list[j].Title
  336. child.Children = []dto.MenuLabel{}
  337. if list[j].MenuType != "F" {
  338. buildMenuLabelTree(menuList, &child)
  339. }
  340. children = append(children, child)
  341. }
  342. if len(children) > 0 {
  343. parent.Children = children
  344. } else {
  345. parent.Children = nil
  346. }
  347. }
  348. // FilterMenuTree 根据允许的菜单ID过滤菜单树
  349. func (e *SysPost) FilterMenuTree(fullTree []dto.MenuLabel, allowedIds []int) []dto.MenuLabel {
  350. if len(allowedIds) == 0 {
  351. return nil
  352. }
  353. allowedMap := make(map[int]bool)
  354. for _, id := range allowedIds {
  355. allowedMap[id] = true
  356. }
  357. var filter func(nodes []dto.MenuLabel) []dto.MenuLabel
  358. filter = func(nodes []dto.MenuLabel) []dto.MenuLabel {
  359. result := make([]dto.MenuLabel, 0)
  360. for _, node := range nodes {
  361. // Only include if allowed or has allowed children
  362. if allowedMap[node.Id] || hasAllowedChild(node.Children, allowedMap) {
  363. child := dto.MenuLabel{
  364. Id: node.Id,
  365. Label: node.Label,
  366. Children: filter(node.Children),
  367. }
  368. result = append(result, child)
  369. }
  370. }
  371. return result
  372. }
  373. return filter(fullTree)
  374. }
  375. func hasAllowedChild(children []dto.MenuLabel, allowedMap map[int]bool) bool {
  376. for _, c := range children {
  377. if allowedMap[c.Id] || hasAllowedChild(c.Children, allowedMap) {
  378. return true
  379. }
  380. }
  381. return false
  382. }
  383. // syncCasbinRules 同步岗位权限到 Casbin 规则表
  384. func (e *SysPost) syncCasbinRules(tx *gorm.DB, post model.SysPost, menuList []model.SysMenu) error {
  385. // 获取完整的菜单信息(包括父菜单的路由权限)
  386. var allMenus []model.SysMenu
  387. menuIds := make([]int, 0)
  388. for _, m := range menuList {
  389. menuIds = append(menuIds, m.Id)
  390. }
  391. // 递归获取所有父级菜单的权限路径
  392. err := e.resolveMenuPaths(menuIds, &allMenus)
  393. if err != nil {
  394. return err
  395. }
  396. // 为每个菜单路径写入 Casbin 规则
  397. // Casbin 规则格式: p = postCode, domain, path, method
  398. // 在 Casbin 中: sub = postCode, dom = service::api, obj = path, act = method
  399. for _, menu := range allMenus {
  400. if menu.Path == "" {
  401. continue
  402. }
  403. var methods []string
  404. if menu.MenuType == "C" {
  405. methods = []string{"GET"}
  406. } else if menu.MenuType == "M" {
  407. continue
  408. } else if menu.MenuType == "F" {
  409. methods = mapButtonToMethods(menu.Title)
  410. if len(methods) == 0 {
  411. continue
  412. }
  413. }
  414. for _, method := range methods {
  415. // 使用原始 SQL 插入避免与 Casbin adapter 冲突
  416. // 检查是否已存在
  417. var count int64
  418. sub := "role:" + post.PostCode
  419. tx.Table("sys_casbin_rule").
  420. Where("p_type = 'p' AND v0 = ? AND v1 = ? AND v2 = ? AND v3 = ?",
  421. sub, "service::api", menu.Path, method).
  422. Count(&count)
  423. if count == 0 {
  424. err = tx.Exec(
  425. "INSERT INTO sys_casbin_rule (p_type, v0, v1, v2, v3, v4, v5) VALUES ('p', ?, ?, ?, ?, '', '')",
  426. sub, "service::api", menu.Path, method,
  427. ).Error
  428. if err != nil {
  429. return err
  430. }
  431. }
  432. }
  433. }
  434. // 写入 g 规则(Casbin RBAC 角色继承)
  435. sub := "role:" + post.PostCode
  436. var gCount int64
  437. tx.Table("sys_casbin_rule").
  438. Where("p_type = 'g' AND v0 = ? AND v1 = ? AND v2 = ?", sub, sub, "service::api").
  439. Count(&gCount)
  440. if gCount == 0 {
  441. err = tx.Exec(
  442. "INSERT INTO sys_casbin_rule (p_type, v0, v1, v2, v3, v4, v5) VALUES ('g', ?, ?, ?, '', '', '')",
  443. sub, sub, "service::api",
  444. ).Error
  445. if err != nil {
  446. return err
  447. }
  448. }
  449. return nil
  450. }
  451. // mapButtonToMethods 将按钮名称映射为 HTTP 方法
  452. func mapButtonToMethods(title string) []string {
  453. title = strings.TrimSpace(title)
  454. switch {
  455. case strings.Contains(title, "查询"):
  456. return []string{"GET"}
  457. case strings.Contains(title, "新增") || strings.Contains(title, "导入"):
  458. return []string{"POST"}
  459. case strings.Contains(title, "编辑") || strings.Contains(title, "修改"):
  460. return []string{"PUT"}
  461. case strings.Contains(title, "删除"):
  462. return []string{"DELETE"}
  463. case strings.Contains(title, "重置") || strings.Contains(title, "审核"):
  464. return []string{"PUT"}
  465. case strings.Contains(title, "分配"):
  466. return []string{"POST"}
  467. default:
  468. return []string{"GET"}
  469. }
  470. }
  471. // resolveMenuPaths 递归获取菜单及其所有父菜单
  472. func (e *SysPost) resolveMenuPaths(menuIds []int, result *[]model.SysMenu) error {
  473. if len(menuIds) == 0 {
  474. return nil
  475. }
  476. var menus []model.SysMenu
  477. err := e.Orm.Where("id in (?)", menuIds).Find(&menus).Error
  478. if err != nil {
  479. return err
  480. }
  481. parentIds := make([]int, 0)
  482. seen := make(map[int]bool)
  483. for _, m := range *result {
  484. seen[m.Id] = true
  485. }
  486. for _, m := range menus {
  487. if !seen[m.Id] {
  488. *result = append(*result, m)
  489. seen[m.Id] = true
  490. }
  491. if m.ParentId != 0 && !seen[m.ParentId] {
  492. parentIds = append(parentIds, m.ParentId)
  493. }
  494. }
  495. if len(parentIds) > 0 {
  496. return e.resolveMenuPaths(parentIds, result)
  497. }
  498. return nil
  499. }
  500. // GetWithName 根据岗位名称查询
  501. func (e *SysPost) GetWithName(name string, deptId int, postModel *model.SysPost) *SysPost {
  502. db := e.Orm.Where("post_name = ?", name)
  503. if deptId > 0 {
  504. db = db.Where("dept_id = ?", deptId)
  505. }
  506. err := db.First(postModel).Error
  507. if err != nil {
  508. e.Log.Errorf("db error: %s", err)
  509. if errors.Is(err, gorm.ErrRecordNotFound) {
  510. err = global.GetNotFoundOrNoPermissionErr
  511. } else {
  512. err = global.GetFailedErr
  513. }
  514. _ = e.AddError(err)
  515. return e
  516. }
  517. return e
  518. }
  519. // GetPostNamesByIds 根据岗位ID列表获取岗位名称映射
  520. func (e *SysPost) GetPostNamesByIds(ids []int) (map[int]string, error) {
  521. if len(ids) == 0 {
  522. return map[int]string{}, nil
  523. }
  524. var posts []model.SysPost
  525. err := e.Orm.Where("id in (?)", ids).Find(&posts).Error
  526. if err != nil {
  527. e.Log.Errorf("db error: %s", err)
  528. return nil, global.GetFailedErr
  529. }
  530. result := make(map[int]string)
  531. for _, p := range posts {
  532. result[p.Id] = p.PostName
  533. }
  534. return result, nil
  535. }
  536. // GetPostById 通过岗位ID获取菜单权限标识列表
  537. func (e *SysPost) GetPostById(postId int) ([]string, error) {
  538. permissions := make([]string, 0)
  539. postModel := model.SysPost{}
  540. postModel.Id = postId
  541. err := e.Orm.Model(&postModel).First(&postModel).Error
  542. if err != nil {
  543. e.Log.Errorf("db error: %s", err)
  544. if errors.Is(err, gorm.ErrRecordNotFound) {
  545. return nil, global.GetNotFoundOrNoPermissionErr
  546. }
  547. return nil, global.GetFailedErr
  548. }
  549. // 查询岗位关联的菜单权限标识
  550. var menuPerms []string
  551. err = e.Orm.Model(&model.SysMenu{}).
  552. Joins("JOIN sys_post_menu ON sys_post_menu.menu_id = sys_menu.id").
  553. Where("sys_post_menu.post_id = ? AND sys_menu.permission != ''", postId).
  554. Pluck("sys_menu.permission", &menuPerms).Error
  555. if err != nil {
  556. return nil, global.GetFailedErr
  557. }
  558. permissions = append(permissions, menuPerms...)
  559. return permissions, nil
  560. }