feat: 优化web

This commit is contained in:
2026-04-23 18:58:13 +08:00
commit 544a2f3428
160 changed files with 27327 additions and 0 deletions
+24
View File
@@ -0,0 +1,24 @@
package entity
import (
"time"
"gorm.io/gorm"
)
// Dept 部门 iam_dept(根部门 parent_id 为空字符串)
type Dept struct {
ID string `json:"id" gorm:"primaryKey;type:varchar(36);not null"`
TenantID string `json:"tenant_id" gorm:"size:36;not null;index:idx_dept_tenant"`
ParentID string `json:"parent_id" gorm:"size:36;default:'';index:idx_dept_parent"`
DeptName string `json:"dept_name" gorm:"size:128;not null"`
DeptPath string `json:"dept_path" gorm:"type:text"`
LeaderID *string `json:"leader_id" gorm:"size:36"`
SortOrder int `json:"sort_order" gorm:"default:0"`
Status int16 `json:"status" gorm:"default:1"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
}
func (Dept) TableName() string { return "iam_dept" }
+32
View File
@@ -0,0 +1,32 @@
package entity
import (
"time"
"gorm.io/gorm"
)
// PublicOverviewPerms 动态导航中「概览页」类公开权限标识(PRD:所有用户默认可见,需在菜单中配置同名 perms)
const PublicOverviewPerms = "public:overview"
// Menu 菜单 iam_menu(全局,不按租户分表;根节点 parent_id 为空字符串)
type Menu struct {
ID string `json:"id" gorm:"primaryKey;type:varchar(36);not null"`
ParentID string `json:"parent_id" gorm:"size:36;default:'';index:idx_menu_parent"`
MenuName string `json:"menu_name" gorm:"size:128;not null"`
MenuType int16 `json:"menu_type" gorm:"not null"` // 1目录 2菜单 3按钮
Perms string `json:"perms" gorm:"size:128;uniqueIndex"`
Path string `json:"path" gorm:"size:255"`
Component string `json:"component" gorm:"size:255"`
Icon string `json:"icon" gorm:"size:64"`
SortOrder int `json:"sort_order" gorm:"default:0"`
IsVisible bool `json:"is_visible" gorm:"default:true"`
IsBuiltin bool `json:"is_builtin" gorm:"default:false"`
ExternalLink string `json:"external_link" gorm:"size:512"`
Status int16 `json:"status" gorm:"default:1"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
}
func (Menu) TableName() string { return "iam_menu" }
+43
View File
@@ -0,0 +1,43 @@
package entity
import (
"time"
"gorm.io/gorm"
)
// 数据范围并集优先级(数值越大权限越大)
const (
DataScopeSelf int16 = 1
DataScopeDept int16 = 2
DataScopeDeptTree int16 = 3
DataScopeAll int16 = 4
)
// Role 角色 iam_role
type Role struct {
ID string `json:"id" gorm:"primaryKey;type:varchar(36);not null"`
TenantID string `json:"tenant_id" gorm:"size:36;not null;index:idx_role_tenant"`
RoleCode string `json:"role_code" gorm:"size:64;not null"`
RoleName string `json:"role_name" gorm:"size:128;not null"`
DataScope int16 `json:"data_scope" gorm:"default:4"` // 1本人 2本部门 3本部门及子部门 4全部
Description string `json:"description" gorm:"size:512"`
IsBuiltin bool `json:"is_builtin" gorm:"default:false"`
Status int16 `json:"status" gorm:"default:1"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
}
func (Role) TableName() string { return "iam_role" }
// RoleMenu 角色菜单 iam_role_menu
type RoleMenu struct {
ID string `json:"id" gorm:"primaryKey;type:varchar(36);not null"`
RoleID string `json:"role_id" gorm:"size:36;not null;uniqueIndex:uk_role_menu"`
MenuID string `json:"menu_id" gorm:"size:36;not null;uniqueIndex:uk_role_menu"`
CreatedAt time.Time `json:"created_at"`
}
func (RoleMenu) TableName() string { return "iam_role_menu" }
+25
View File
@@ -0,0 +1,25 @@
package entity
import (
"time"
"gorm.io/gorm"
)
// PlatformTenantID 平台租户主键(与初始化数据一致;菜单维护等仅平台租户可操作)
const PlatformTenantID = "00000000-0000-0000-0000-000000000001"
// Tenant 租户 iam_tenant
type Tenant struct {
ID string `json:"id" gorm:"primaryKey;type:varchar(36);not null"`
TenantCode string `json:"tenant_code" gorm:"size:64;uniqueIndex;not null"`
TenantName string `json:"tenant_name" gorm:"size:128;not null"`
AdminUserID *string `json:"admin_user_id" gorm:"size:36"`
Status int16 `json:"status" gorm:"default:1"` // 1 正常 0 冻结 -1 删除(逻辑)
ExpireTime *time.Time `json:"expire_time"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
}
func (Tenant) TableName() string { return "iam_tenant" }
+53
View File
@@ -0,0 +1,53 @@
package entity
import (
"time"
"gorm.io/gorm"
)
// User 用户 iam_user
type User struct {
ID string `json:"id" gorm:"primaryKey;type:varchar(36);not null"`
TenantID string `json:"tenant_id" gorm:"size:36;not null;index:idx_user_tenant"`
DeptID *string `json:"dept_id" gorm:"size:36;index:idx_user_dept"`
UserName string `json:"user_name" gorm:"size:64;not null"`
RealName string `json:"real_name" gorm:"size:64"`
PasswordHash string `json:"-" gorm:"size:255;not null"`
Phone string `json:"phone" gorm:"size:20"`
Email string `json:"email" gorm:"size:128"`
Avatar string `json:"avatar" gorm:"size:512"`
Gender int16 `json:"gender" gorm:"default:0"`
Status int16 `json:"status" gorm:"default:1"`
LoginAttempts int `json:"login_attempts" gorm:"default:0"`
LockedUntil *time.Time `json:"locked_until"`
LastLoginAt *time.Time `json:"last_login_at"`
LastLoginIP string `json:"last_login_ip" gorm:"size:45"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
}
func (User) TableName() string { return "iam_user" }
// UserDept 用户部门关联 iam_user_dept
type UserDept struct {
ID string `json:"id" gorm:"primaryKey;type:varchar(36);not null"`
UserID string `json:"user_id" gorm:"size:36;not null;uniqueIndex:uk_user_dept"`
DeptID string `json:"dept_id" gorm:"size:36;not null;uniqueIndex:uk_user_dept"`
IsPrimary bool `json:"is_primary" gorm:"default:false"`
CreatedAt time.Time `json:"created_at"`
}
func (UserDept) TableName() string { return "iam_user_dept" }
// UserRole 用户角色 iam_user_role
type UserRole struct {
ID string `json:"id" gorm:"primaryKey;type:varchar(36);not null"`
UserID string `json:"user_id" gorm:"size:36;not null;uniqueIndex:uk_user_role"`
RoleID string `json:"role_id" gorm:"size:36;not null;uniqueIndex:uk_user_role"`
CreatedAt time.Time `json:"created_at"`
}
func (UserRole) TableName() string { return "iam_user_role" }
+95
View File
@@ -0,0 +1,95 @@
package handler
import (
"net/http"
"giter.top/smart/internal/iam/service"
"github.com/gin-gonic/gin"
)
type DeptHandler struct {
svc service.DeptService
}
func NewDeptHandler(svc service.DeptService) *DeptHandler {
return &DeptHandler{svc: svc}
}
func (h *DeptHandler) Tree(c *gin.Context) {
tid := headerTenantID(c)
keyword := c.Query("keyword")
var leaderID *string
if s := c.Query("leader_id"); s != "" {
leaderID = &s
}
tree, err := h.svc.Tree(c.Request.Context(), tid, keyword, leaderID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, tree)
}
func (h *DeptHandler) Create(c *gin.Context) {
tid := headerTenantID(c)
var req service.CreateDeptRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
d, err := h.svc.Create(c.Request.Context(), tid, &req)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, d)
}
func (h *DeptHandler) Update(c *gin.Context) {
tid := headerTenantID(c)
id := c.Param("id")
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
var req service.UpdateDeptRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
d, err := h.svc.Update(c.Request.Context(), tid, id, &req)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, d)
}
func (h *DeptHandler) Delete(c *gin.Context) {
tid := headerTenantID(c)
var ids []string
if err := c.ShouldBindJSON(&ids); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := h.svc.Delete(c.Request.Context(), tid, ids); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.Status(http.StatusNoContent)
}
func (h *DeptHandler) Get(c *gin.Context) {
tid := headerTenantID(c)
id := c.Param("id")
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
d, err := h.svc.Get(c.Request.Context(), tid, id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, d)
}
+53
View File
@@ -0,0 +1,53 @@
package handler
import (
"strconv"
authmw "giter.top/smart/internal/auth/middleware"
"giter.top/smart/internal/iam/entity"
"github.com/gin-gonic/gin"
)
func atoiDef(s string, def int) int {
if s == "" {
return def
}
v, err := strconv.Atoi(s)
if err != nil {
return def
}
return v
}
// headerTenantID 当前租户:优先 OAuth2 Bearer 解析结果,其次 X-Tenant-ID,缺省平台租户。
func headerTenantID(c *gin.Context) string {
if v, ok := c.Get(authmw.CtxTenantID); ok {
if s, ok2 := v.(string); ok2 && s != "" {
return s
}
}
s := c.GetHeader("X-Tenant-ID")
if s == "" {
return entity.PlatformTenantID
}
return s
}
// headerUserID 当前用户:优先 OAuth2 opaque access_token 对应用户,其次 X-User-ID。
func headerUserID(c *gin.Context) string {
if v, ok := c.Get(authmw.CtxUserID); ok {
if s, ok2 := v.(string); ok2 && s != "" {
return s
}
}
return c.GetHeader("X-User-ID")
}
// headerGrantorUserID 请求头 X-Grantor-User-ID(授权人,用于防越权校验)
func headerGrantorUserID(c *gin.Context) *string {
s := c.GetHeader("X-Grantor-User-ID")
if s == "" {
return nil
}
return &s
}
+141
View File
@@ -0,0 +1,141 @@
package handler
import (
"errors"
"net/http"
"strconv"
"giter.top/smart/internal/iam/entity"
"giter.top/smart/internal/iam/repository"
"giter.top/smart/internal/iam/service"
"github.com/gin-gonic/gin"
)
type MenuHandler struct {
svc service.MenuService
}
func NewMenuHandler(svc service.MenuService) *MenuHandler {
return &MenuHandler{svc: svc}
}
func isPlatformAdmin(c *gin.Context) bool {
return headerTenantID(c) == entity.PlatformTenantID
}
func (h *MenuHandler) Create(c *gin.Context) {
var req service.CreateMenuRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
m, err := h.svc.Create(c.Request.Context(), &req, isPlatformAdmin(c))
if err != nil {
if errors.Is(err, repository.ErrForbidden) {
c.JSON(http.StatusForbidden, gin.H{"error": "仅平台管理员可维护菜单"})
return
}
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, m)
}
func (h *MenuHandler) Update(c *gin.Context) {
mid := c.Param("id")
if mid == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
var req service.UpdateMenuRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
m, err := h.svc.Update(c.Request.Context(), mid, &req, isPlatformAdmin(c))
if err != nil {
if errors.Is(err, repository.ErrForbidden) {
c.JSON(http.StatusForbidden, gin.H{"error": "仅平台管理员可维护菜单"})
return
}
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, m)
}
func (h *MenuHandler) Delete(c *gin.Context) {
var ids []string
if err := c.ShouldBindJSON(&ids); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := h.svc.Delete(c.Request.Context(), ids, isPlatformAdmin(c)); err != nil {
if errors.Is(err, repository.ErrForbidden) {
c.JSON(http.StatusForbidden, gin.H{"error": "仅平台管理员可维护菜单"})
return
}
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.Status(http.StatusNoContent)
}
func (h *MenuHandler) Get(c *gin.Context) {
mid := c.Param("id")
if mid == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
m, err := h.svc.Get(c.Request.Context(), mid)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, m)
}
func (h *MenuHandler) Tree(c *gin.Context) {
var mt *int16
if s := c.Query("menu_type"); s != "" {
v64, err := strconv.ParseInt(s, 10, 16)
if err == nil {
v := int16(v64)
mt = &v
}
}
tree, err := h.svc.Tree(c.Request.Context(), mt)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, tree)
}
func (h *MenuHandler) Nav(c *gin.Context) {
uid := headerUserID(c)
if uid == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "需要 X-User-ID"})
return
}
tree, err := h.svc.NavForUser(c.Request.Context(), uid)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, tree)
}
func (h *MenuHandler) Perms(c *gin.Context) {
uid := headerUserID(c)
if uid == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "需要 X-User-ID"})
return
}
perms, err := h.svc.PermsForUser(c.Request.Context(), uid)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"perms": perms})
}
+117
View File
@@ -0,0 +1,117 @@
package handler
import (
"net/http"
"giter.top/smart/internal/iam/service"
"github.com/gin-gonic/gin"
)
type RoleHandler struct {
svc service.RoleService
}
func NewRoleHandler(svc service.RoleService) *RoleHandler {
return &RoleHandler{svc: svc}
}
func (h *RoleHandler) Create(c *gin.Context) {
tid := headerTenantID(c)
var req service.CreateRoleRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
role, err := h.svc.Create(c.Request.Context(), tid, &req, headerGrantorUserID(c))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, role)
}
func (h *RoleHandler) Update(c *gin.Context) {
tid := headerTenantID(c)
rid := c.Param("id")
if rid == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
var req service.UpdateRoleRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
r, err := h.svc.Update(c.Request.Context(), tid, rid, &req, headerGrantorUserID(c))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, r)
}
func (h *RoleHandler) Delete(c *gin.Context) {
tid := headerTenantID(c)
var ids []string
if err := c.ShouldBindJSON(&ids); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := h.svc.Delete(c.Request.Context(), tid, ids); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.Status(http.StatusNoContent)
}
func (h *RoleHandler) Get(c *gin.Context) {
tid := headerTenantID(c)
rid := c.Param("id")
if rid == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
r, err := h.svc.Get(c.Request.Context(), tid, rid)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, r)
}
func (h *RoleHandler) List(c *gin.Context) {
tid := headerTenantID(c)
name := c.Query("name")
code := c.Query("code")
page := atoiDef(c.Query("page"), 1)
pageSize := atoiDef(c.Query("page_size"), 10)
resp, err := h.svc.List(c.Request.Context(), tid, name, code, page, pageSize)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, resp)
}
type assignMenusBody struct {
MenuIDs []string `json:"menu_ids"`
}
func (h *RoleHandler) AssignMenus(c *gin.Context) {
tid := headerTenantID(c)
rid := c.Param("id")
if rid == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
var body assignMenusBody
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := h.svc.AssignMenus(c.Request.Context(), tid, rid, body.MenuIDs, headerGrantorUserID(c)); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.Status(http.StatusNoContent)
}
+98
View File
@@ -0,0 +1,98 @@
package handler
import (
"net/http"
"strconv"
"giter.top/smart/internal/iam/service"
"github.com/gin-gonic/gin"
)
type TenantHandler struct {
svc service.TenantService
}
func NewTenantHandler(svc service.TenantService) *TenantHandler {
return &TenantHandler{svc: svc}
}
func (h *TenantHandler) Create(c *gin.Context) {
var req service.CreateTenantRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
t, err := h.svc.Create(c.Request.Context(), &req)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, t)
}
func (h *TenantHandler) Update(c *gin.Context) {
id := c.Param("id")
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
var req service.UpdateTenantRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
t, err := h.svc.Update(c.Request.Context(), id, &req)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, t)
}
func (h *TenantHandler) Delete(c *gin.Context) {
var ids []string
if err := c.ShouldBindJSON(&ids); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := h.svc.Delete(c.Request.Context(), ids); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.Status(http.StatusNoContent)
}
func (h *TenantHandler) Get(c *gin.Context) {
id := c.Param("id")
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
t, err := h.svc.Get(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, t)
}
func (h *TenantHandler) List(c *gin.Context) {
name := c.Query("name")
code := c.Query("code")
var status *int16
if s := c.Query("status"); s != "" {
v64, err := strconv.ParseInt(s, 10, 16)
if err == nil {
v := int16(v64)
status = &v
}
}
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
resp, err := h.svc.List(c.Request.Context(), name, code, status, page, pageSize)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, resp)
}
+123
View File
@@ -0,0 +1,123 @@
package handler
import (
"net/http"
"strconv"
"giter.top/smart/internal/iam/service"
"github.com/gin-gonic/gin"
)
type UserHandler struct {
svc service.UserService
}
func NewUserHandler(svc service.UserService) *UserHandler {
return &UserHandler{svc: svc}
}
func (h *UserHandler) Create(c *gin.Context) {
tid := headerTenantID(c)
var req service.CreateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
u, err := h.svc.Create(c.Request.Context(), tid, &req)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, u)
}
func (h *UserHandler) Update(c *gin.Context) {
tid := headerTenantID(c)
uid := c.Param("id")
if uid == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
var req service.UpdateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
u, err := h.svc.Update(c.Request.Context(), tid, uid, &req)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, u)
}
func (h *UserHandler) Delete(c *gin.Context) {
tid := headerTenantID(c)
var ids []string
if err := c.ShouldBindJSON(&ids); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := h.svc.Delete(c.Request.Context(), tid, ids); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.Status(http.StatusNoContent)
}
func (h *UserHandler) Get(c *gin.Context) {
tid := headerTenantID(c)
uid := c.Param("id")
if uid == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
u, err := h.svc.Get(c.Request.Context(), tid, uid)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, u)
}
func (h *UserHandler) List(c *gin.Context) {
tid := headerTenantID(c)
q := &service.UserListQuery{
Keyword: c.Query("keyword"),
Page: atoiDef(c.Query("page"), 1),
PageSize: atoiDef(c.Query("page_size"), 10),
}
if s := c.Query("dept_id"); s != "" {
q.DeptID = &s
}
if s := c.Query("role_id"); s != "" {
q.RoleID = &s
}
if s := c.Query("status"); s != "" {
v64, err := strconv.ParseInt(s, 10, 16)
if err == nil {
v := int16(v64)
q.Status = &v
}
}
resp, err := h.svc.List(c.Request.Context(), tid, q)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, resp)
}
func (h *UserHandler) DataScope(c *gin.Context) {
uid := headerUserID(c)
if uid == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "需要 X-User-ID"})
return
}
ds, err := h.svc.DataScopeForUser(c.Request.Context(), uid)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data_scope": ds})
}
+87
View File
@@ -0,0 +1,87 @@
package iam
import (
"giter.top/smart/internal/iam/handler"
"github.com/gin-gonic/gin"
)
type IamRoutes struct {
tenantHandler *handler.TenantHandler
deptHandler *handler.DeptHandler
roleHandler *handler.RoleHandler
userHandler *handler.UserHandler
menuHandler *handler.MenuHandler
}
func NewIamRoutes(tenantHandler *handler.TenantHandler, deptHandler *handler.DeptHandler, roleHandler *handler.RoleHandler, userHandler *handler.UserHandler, menuHandler *handler.MenuHandler) *IamRoutes {
return &IamRoutes{
tenantHandler: tenantHandler,
deptHandler: deptHandler,
roleHandler: roleHandler,
userHandler: userHandler,
menuHandler: menuHandler,
}
}
// TODO 添加注册信息
func (s *IamRoutes) Register(engine *gin.Engine, apiGroup *gin.RouterGroup) {
// group :=engine.Group("/iam")
group := apiGroup.Group("/iam")
s.registerTenantRoutes(group)
s.registerDeptRoutes(group)
s.registerRoleRoutes(group)
s.registerUserRoutes(group)
s.registerMenuRoutes(group)
}
func (s *IamRoutes) registerTenantRoutes(group *gin.RouterGroup) {
tg := group.Group("/tenant")
{
tg.POST("/create", s.tenantHandler.Create)
tg.PUT("/update/:id", s.tenantHandler.Update)
tg.DELETE("/delete-batch", s.tenantHandler.Delete)
tg.GET("/get/:id", s.tenantHandler.Get)
tg.GET("/list", s.tenantHandler.List)
}
}
func (s *IamRoutes) registerDeptRoutes(group *gin.RouterGroup) {
dg := group.Group("/dept")
{
dg.POST("/create", s.deptHandler.Create)
dg.PUT("/update/:id", s.deptHandler.Update)
dg.DELETE("/delete-batch", s.deptHandler.Delete)
dg.GET("/get/:id", s.deptHandler.Get)
dg.GET("/tree", s.deptHandler.Tree)
}
}
func (s *IamRoutes) registerRoleRoutes(group *gin.RouterGroup) {
rg := group.Group("/role")
{
rg.POST("/create", s.roleHandler.Create)
rg.PUT("/update/:id", s.roleHandler.Update)
rg.DELETE("/delete-batch", s.roleHandler.Delete)
rg.GET("/get/:id", s.roleHandler.Get)
rg.GET("/list", s.roleHandler.List)
}
}
func (s *IamRoutes) registerUserRoutes(group *gin.RouterGroup) {
ug := group.Group("/user")
{
ug.POST("/create", s.userHandler.Create)
ug.PUT("/update/:id", s.userHandler.Update)
ug.DELETE("/delete-batch", s.userHandler.Delete)
ug.GET("/get/:id", s.userHandler.Get)
ug.GET("/list", s.userHandler.List)
}
}
func (s *IamRoutes) registerMenuRoutes(group *gin.RouterGroup) {
mg := group.Group("/menu")
{
mg.POST("/create", s.menuHandler.Create)
mg.PUT("/update/:id", s.menuHandler.Update)
mg.DELETE("/delete-batch", s.menuHandler.Delete)
mg.GET("/get/:id", s.menuHandler.Get)
mg.GET("/tree", s.menuHandler.Tree)
mg.GET("/nav", s.menuHandler.Nav)
mg.GET("/perms", s.menuHandler.Perms)
}
}
@@ -0,0 +1,93 @@
package repository
import (
"context"
"giter.top/smart/internal/iam/entity"
"gorm.io/gorm"
)
// DeptRepository 部门数据访问
type DeptRepository interface {
Create(ctx context.Context, d *entity.Dept) error
Update(ctx context.Context, d *entity.Dept) error
Delete(ctx context.Context, id string) error
GetByID(ctx context.Context, id string) (*entity.Dept, error)
ListByTenant(ctx context.Context, tenantID string) ([]entity.Dept, error)
CountChildren(ctx context.Context, id string) (int64, error)
ExistsSiblingName(ctx context.Context, tenantID, parentID, name string, excludeID string) (bool, error)
FindRoot(ctx context.Context, tenantID string) (*entity.Dept, error)
UpdatePath(ctx context.Context, id string, path string) error
}
type deptRepository struct {
db *gorm.DB
}
func NewDeptRepository(db *gorm.DB) DeptRepository {
return &deptRepository{db: db}
}
func (r *deptRepository) Create(ctx context.Context, d *entity.Dept) error {
return r.db.WithContext(ctx).Create(d).Error
}
func (r *deptRepository) Update(ctx context.Context, d *entity.Dept) error {
return r.db.WithContext(ctx).Save(d).Error
}
func (r *deptRepository) Delete(ctx context.Context, id string) error {
return r.db.WithContext(ctx).Delete(&entity.Dept{}, "id = ?", id).Error
}
func (r *deptRepository) GetByID(ctx context.Context, id string) (*entity.Dept, error) {
var out entity.Dept
err := r.db.WithContext(ctx).Where("id = ?", id).First(&out).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, ErrNotFound
}
return nil, err
}
return &out, nil
}
func (r *deptRepository) ListByTenant(ctx context.Context, tenantID string) ([]entity.Dept, error) {
var rows []entity.Dept
err := r.db.WithContext(ctx).Where("tenant_id = ?", tenantID).Order("sort_order ASC, created_at ASC").Find(&rows).Error
return rows, err
}
func (r *deptRepository) CountChildren(ctx context.Context, id string) (int64, error) {
var n int64
err := r.db.WithContext(ctx).Model(&entity.Dept{}).Where("parent_id = ?", id).Count(&n).Error
return n, err
}
func (r *deptRepository) ExistsSiblingName(ctx context.Context, tenantID, parentID, name string, excludeID string) (bool, error) {
q := r.db.WithContext(ctx).Model(&entity.Dept{}).Where("tenant_id = ? AND parent_id = ? AND dept_name = ?", tenantID, parentID, name)
if excludeID != "" {
q = q.Where("id <> ?", excludeID)
}
var n int64
err := q.Count(&n).Error
return n > 0, err
}
func (r *deptRepository) FindRoot(ctx context.Context, tenantID string) (*entity.Dept, error) {
var out entity.Dept
err := r.db.WithContext(ctx).
Where("tenant_id = ? AND (parent_id = '' OR parent_id = '0')", tenantID).
First(&out).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, ErrNotFound
}
return nil, err
}
return &out, nil
}
func (r *deptRepository) UpdatePath(ctx context.Context, id string, path string) error {
return r.db.WithContext(ctx).Model(&entity.Dept{}).Where("id = ?", id).Update("dept_path", path).Error
}
+10
View File
@@ -0,0 +1,10 @@
package repository
import "errors"
var (
ErrNotFound = errors.New("not found")
ErrConflict = errors.New("conflict")
ErrInvalidState = errors.New("invalid state")
ErrForbidden = errors.New("forbidden")
)
+111
View File
@@ -0,0 +1,111 @@
package repository
import (
"context"
"giter.top/smart/internal/iam/entity"
"gorm.io/gorm"
)
// MenuRepository 菜单
type MenuRepository interface {
Create(ctx context.Context, m *entity.Menu) error
Update(ctx context.Context, m *entity.Menu) error
Delete(ctx context.Context, id string) error
GetByID(ctx context.Context, id string) (*entity.Menu, error)
ListAll(ctx context.Context) ([]entity.Menu, error)
ListByType(ctx context.Context, menuType *int16) ([]entity.Menu, error)
ExistsPerms(ctx context.Context, perms string, excludeID string) (bool, error)
CountChildren(ctx context.Context, parentID string) (int64, error)
CountRoleRefs(ctx context.Context, menuID string) (int64, error)
ListByPerms(ctx context.Context, perms string) ([]entity.Menu, error)
ListIDsByPermsIn(ctx context.Context, perms []string) ([]string, error)
}
type menuRepository struct {
db *gorm.DB
}
func NewMenuRepository(db *gorm.DB) MenuRepository {
return &menuRepository{db: db}
}
func (r *menuRepository) Create(ctx context.Context, m *entity.Menu) error {
return r.db.WithContext(ctx).Create(m).Error
}
func (r *menuRepository) Update(ctx context.Context, m *entity.Menu) error {
return r.db.WithContext(ctx).Save(m).Error
}
func (r *menuRepository) Delete(ctx context.Context, id string) error {
return r.db.WithContext(ctx).Delete(&entity.Menu{}, "id = ?", id).Error
}
func (r *menuRepository) GetByID(ctx context.Context, id string) (*entity.Menu, error) {
var out entity.Menu
err := r.db.WithContext(ctx).Where("id = ?", id).First(&out).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, ErrNotFound
}
return nil, err
}
return &out, nil
}
func (r *menuRepository) ListAll(ctx context.Context) ([]entity.Menu, error) {
var rows []entity.Menu
err := r.db.WithContext(ctx).Order("sort_order ASC, created_at ASC").Find(&rows).Error
return rows, err
}
func (r *menuRepository) ListByType(ctx context.Context, menuType *int16) ([]entity.Menu, error) {
q := r.db.WithContext(ctx).Model(&entity.Menu{})
if menuType != nil {
q = q.Where("menu_type = ?", *menuType)
}
var rows []entity.Menu
err := q.Order("sort_order ASC, created_at ASC").Find(&rows).Error
return rows, err
}
func (r *menuRepository) ExistsPerms(ctx context.Context, perms string, excludeID string) (bool, error) {
if perms == "" {
return false, nil
}
q := r.db.WithContext(ctx).Model(&entity.Menu{}).Where("perms = ?", perms)
if excludeID != "" {
q = q.Where("id <> ?", excludeID)
}
var n int64
err := q.Count(&n).Error
return n > 0, err
}
func (r *menuRepository) CountChildren(ctx context.Context, parentID string) (int64, error) {
var n int64
err := r.db.WithContext(ctx).Model(&entity.Menu{}).Where("parent_id = ?", parentID).Count(&n).Error
return n, err
}
func (r *menuRepository) CountRoleRefs(ctx context.Context, menuID string) (int64, error) {
var n int64
err := r.db.WithContext(ctx).Model(&entity.RoleMenu{}).Where("menu_id = ?", menuID).Count(&n).Error
return n, err
}
func (r *menuRepository) ListByPerms(ctx context.Context, perms string) ([]entity.Menu, error) {
var rows []entity.Menu
err := r.db.WithContext(ctx).Where("perms = ?", perms).Find(&rows).Error
return rows, err
}
func (r *menuRepository) ListIDsByPermsIn(ctx context.Context, perms []string) ([]string, error) {
if len(perms) == 0 {
return nil, nil
}
var ids []string
err := r.db.WithContext(ctx).Model(&entity.Menu{}).Where("perms IN ?", perms).Pluck("id", &ids).Error
return ids, err
}
+147
View File
@@ -0,0 +1,147 @@
package repository
import (
"context"
"giter.top/smart/internal/iam/entity"
"giter.top/smart/pkg/utils/id"
"gorm.io/gorm"
)
// RoleRepository 角色与角色菜单
type RoleRepository interface {
Create(ctx context.Context, r *entity.Role) error
Update(ctx context.Context, r *entity.Role) error
Delete(ctx context.Context, id string) error
GetByID(ctx context.Context, id string) (*entity.Role, error)
List(ctx context.Context, tenantID string, name, code string, page, pageSize int) ([]entity.Role, int64, error)
ExistsCode(ctx context.Context, tenantID string, code string, excludeID string) (bool, error)
CountUsers(ctx context.Context, roleID string) (int64, error)
ReplaceRoleMenus(ctx context.Context, roleID string, menuIDs []string) error
ListMenuIDsByRole(ctx context.Context, roleID string) ([]string, error)
ListMenuIDsByRoles(ctx context.Context, roleIDs []string) ([]string, error)
ListRolesByUser(ctx context.Context, userID string) ([]entity.Role, error)
}
type roleRepository struct {
db *gorm.DB
}
func NewRoleRepository(db *gorm.DB) RoleRepository {
return &roleRepository{db: db}
}
func (r *roleRepository) Create(ctx context.Context, row *entity.Role) error {
return r.db.WithContext(ctx).Create(row).Error
}
func (r *roleRepository) Update(ctx context.Context, row *entity.Role) error {
return r.db.WithContext(ctx).Save(row).Error
}
func (r *roleRepository) Delete(ctx context.Context, id string) error {
return r.db.WithContext(ctx).Delete(&entity.Role{}, "id = ?", id).Error
}
func (r *roleRepository) GetByID(ctx context.Context, id string) (*entity.Role, error) {
var out entity.Role
err := r.db.WithContext(ctx).Where("id = ?", id).First(&out).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, ErrNotFound
}
return nil, err
}
return &out, nil
}
func (r *roleRepository) List(ctx context.Context, tenantID string, name, code string, page, pageSize int) ([]entity.Role, int64, error) {
q := r.db.WithContext(ctx).Model(&entity.Role{}).Where("tenant_id = ?", tenantID)
if name != "" {
q = q.Where("role_name LIKE ?", "%"+name+"%")
}
if code != "" {
q = q.Where("role_code LIKE ?", "%"+code+"%")
}
var total int64
if err := q.Count(&total).Error; err != nil {
return nil, 0, err
}
if page <= 0 {
page = 1
}
if pageSize <= 0 {
pageSize = 10
}
offset := (page - 1) * pageSize
var rows []entity.Role
err := q.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&rows).Error
return rows, total, err
}
func (r *roleRepository) ExistsCode(ctx context.Context, tenantID string, code string, excludeID string) (bool, error) {
q := r.db.WithContext(ctx).Model(&entity.Role{}).Where("tenant_id = ? AND role_code = ?", tenantID, code)
if excludeID != "" {
q = q.Where("id <> ?", excludeID)
}
var n int64
err := q.Count(&n).Error
return n > 0, err
}
func (r *roleRepository) CountUsers(ctx context.Context, roleID string) (int64, error) {
var n int64
err := r.db.WithContext(ctx).Model(&entity.UserRole{}).Where("role_id = ?", roleID).Count(&n).Error
return n, err
}
func (r *roleRepository) ReplaceRoleMenus(ctx context.Context, roleID string, menuIDs []string) error {
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("role_id = ?", roleID).Delete(&entity.RoleMenu{}).Error; err != nil {
return err
}
for _, mid := range menuIDs {
rm := entity.RoleMenu{ID: id.New(), RoleID: roleID, MenuID: mid}
if err := tx.Create(&rm).Error; err != nil {
return err
}
}
return nil
})
}
func (r *roleRepository) ListMenuIDsByRole(ctx context.Context, roleID string) ([]string, error) {
var ids []string
err := r.db.WithContext(ctx).Model(&entity.RoleMenu{}).Where("role_id = ?", roleID).Pluck("menu_id", &ids).Error
return ids, err
}
func (r *roleRepository) ListMenuIDsByRoles(ctx context.Context, roleIDs []string) ([]string, error) {
if len(roleIDs) == 0 {
return nil, nil
}
var raw []string
err := r.db.WithContext(ctx).Model(&entity.RoleMenu{}).Where("role_id IN ?", roleIDs).Pluck("menu_id", &raw).Error
if err != nil {
return nil, err
}
seen := make(map[string]struct{}, len(raw))
var ids []string
for _, menuID := range raw {
if _, ok := seen[menuID]; ok {
continue
}
seen[menuID] = struct{}{}
ids = append(ids, menuID)
}
return ids, nil
}
func (r *roleRepository) ListRolesByUser(ctx context.Context, userID string) ([]entity.Role, error) {
var roles []entity.Role
err := r.db.WithContext(ctx).Table("iam_role").
Joins("JOIN iam_user_role ur ON ur.role_id = iam_role.id").
Where("ur.user_id = ?", userID).
Find(&roles).Error
return roles, err
}
@@ -0,0 +1,109 @@
package repository
import (
"context"
"giter.top/smart/internal/iam/entity"
"gorm.io/gorm"
)
// TenantRepository 租户数据访问
type TenantRepository interface {
Create(ctx context.Context, t *entity.Tenant) error
Update(ctx context.Context, t *entity.Tenant) error
GetByID(ctx context.Context, id string) (*entity.Tenant, error)
GetByCode(ctx context.Context, code string) (*entity.Tenant, error)
List(ctx context.Context, name, code string, status *int16, page, pageSize int) ([]entity.Tenant, int64, error)
CountUsers(ctx context.Context, tenantID string) (int64, error)
CountDepts(ctx context.Context, tenantID string) (int64, error)
ExistsCode(ctx context.Context, code string, excludeID string) (bool, error)
}
type tenantRepository struct {
db *gorm.DB
}
func NewTenantRepository(db *gorm.DB) TenantRepository {
return &tenantRepository{db: db}
}
func (r *tenantRepository) Create(ctx context.Context, t *entity.Tenant) error {
return r.db.WithContext(ctx).Create(t).Error
}
func (r *tenantRepository) Update(ctx context.Context, t *entity.Tenant) error {
return r.db.WithContext(ctx).Save(t).Error
}
func (r *tenantRepository) GetByID(ctx context.Context, id string) (*entity.Tenant, error) {
var out entity.Tenant
err := r.db.WithContext(ctx).Where("id = ?", id).First(&out).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, ErrNotFound
}
return nil, err
}
return &out, nil
}
func (r *tenantRepository) GetByCode(ctx context.Context, code string) (*entity.Tenant, error) {
var out entity.Tenant
err := r.db.WithContext(ctx).Where("tenant_code = ?", code).First(&out).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, ErrNotFound
}
return nil, err
}
return &out, nil
}
func (r *tenantRepository) List(ctx context.Context, name, code string, status *int16, page, pageSize int) ([]entity.Tenant, int64, error) {
q := r.db.WithContext(ctx).Model(&entity.Tenant{})
if name != "" {
q = q.Where("tenant_name LIKE ?", "%"+name+"%")
}
if code != "" {
q = q.Where("tenant_code LIKE ?", "%"+code+"%")
}
if status != nil {
q = q.Where("status = ?", *status)
}
var total int64
if err := q.Count(&total).Error; err != nil {
return nil, 0, err
}
if page <= 0 {
page = 1
}
if pageSize <= 0 {
pageSize = 10
}
offset := (page - 1) * pageSize
var rows []entity.Tenant
err := q.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&rows).Error
return rows, total, err
}
func (r *tenantRepository) CountUsers(ctx context.Context, tenantID string) (int64, error) {
var n int64
err := r.db.WithContext(ctx).Model(&entity.User{}).Where("tenant_id = ?", tenantID).Count(&n).Error
return n, err
}
func (r *tenantRepository) CountDepts(ctx context.Context, tenantID string) (int64, error) {
var n int64
err := r.db.WithContext(ctx).Model(&entity.Dept{}).Where("tenant_id = ?", tenantID).Count(&n).Error
return n, err
}
func (r *tenantRepository) ExistsCode(ctx context.Context, code string, excludeID string) (bool, error) {
q := r.db.WithContext(ctx).Model(&entity.Tenant{}).Where("tenant_code = ?", code)
if excludeID != "" {
q = q.Where("id <> ?", excludeID)
}
var n int64
err := q.Count(&n).Error
return n > 0, err
}
+179
View File
@@ -0,0 +1,179 @@
package repository
import (
"context"
"giter.top/smart/internal/iam/entity"
"giter.top/smart/pkg/utils/id"
"gorm.io/gorm"
)
// UserRepository 用户数据访问
type UserRepository interface {
Create(ctx context.Context, u *entity.User) error
Update(ctx context.Context, u *entity.User) error
Delete(ctx context.Context, id string) error
GetByID(ctx context.Context, id string) (*entity.User, error)
GetByUserName(ctx context.Context, tenantID string, userName string) (*entity.User, error)
ExistsUserName(ctx context.Context, tenantID string, userName string, excludeID string) (bool, error)
CountByDept(ctx context.Context, deptID string) (int64, error)
List(ctx context.Context, tenantID string, deptID *string, roleID *string, keyword string, status *int16, page, pageSize int) ([]entity.User, int64, error)
ReplaceUserDepts(ctx context.Context, userID string, primaryDept string, deptIDs []string) error
ReplaceUserRoles(ctx context.Context, userID string, roleIDs []string) error
ListRoleIDs(ctx context.Context, userID string) ([]string, error)
ListDeptIDs(ctx context.Context, userID string) ([]string, error)
}
type userRepository struct {
db *gorm.DB
}
func NewUserRepository(db *gorm.DB) UserRepository {
return &userRepository{db: db}
}
func (r *userRepository) Create(ctx context.Context, u *entity.User) error {
return r.db.WithContext(ctx).Create(u).Error
}
func (r *userRepository) Update(ctx context.Context, u *entity.User) error {
return r.db.WithContext(ctx).Save(u).Error
}
func (r *userRepository) Delete(ctx context.Context, id string) error {
return r.db.WithContext(ctx).Delete(&entity.User{}, "id = ?", id).Error
}
func (r *userRepository) GetByID(ctx context.Context, id string) (*entity.User, error) {
var out entity.User
err := r.db.WithContext(ctx).Where("id = ?", id).First(&out).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, ErrNotFound
}
return nil, err
}
return &out, nil
}
func (r *userRepository) GetByUserName(ctx context.Context, tenantID string, userName string) (*entity.User, error) {
var out entity.User
err := r.db.WithContext(ctx).Where("tenant_id = ? AND user_name = ?", tenantID, userName).First(&out).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, ErrNotFound
}
return nil, err
}
return &out, nil
}
func (r *userRepository) ExistsUserName(ctx context.Context, tenantID string, userName string, excludeID string) (bool, error) {
q := r.db.WithContext(ctx).Model(&entity.User{}).Where("tenant_id = ? AND user_name = ?", tenantID, userName)
if excludeID != "" {
q = q.Where("id <> ?", excludeID)
}
var n int64
err := q.Count(&n).Error
return n > 0, err
}
func (r *userRepository) CountByDept(ctx context.Context, deptID string) (int64, error) {
var n int64
err := r.db.WithContext(ctx).Raw(`
SELECT COUNT(*) FROM (
SELECT id FROM iam_user WHERE dept_id = ? AND deleted_at IS NULL
UNION
SELECT user_id FROM iam_user_dept WHERE dept_id = ?
) t`, deptID, deptID).Scan(&n).Error
return n, err
}
func (r *userRepository) List(ctx context.Context, tenantID string, deptID *string, roleID *string, keyword string, status *int16, page, pageSize int) ([]entity.User, int64, error) {
q := r.db.WithContext(ctx).Model(&entity.User{}).Where("tenant_id = ?", tenantID)
if deptID != nil {
d := *deptID
q = q.Where("dept_id = ? OR id IN (SELECT user_id FROM iam_user_dept WHERE dept_id = ?)", d, d)
}
if roleID != nil {
sub := r.db.WithContext(ctx).Model(&entity.UserRole{}).Select("user_id").Where("role_id = ?", *roleID)
q = q.Where("id IN (?)", sub)
}
if keyword != "" {
kw := "%" + keyword + "%"
q = q.Where("user_name LIKE ? OR real_name LIKE ? OR phone LIKE ? OR email LIKE ?", kw, kw, kw, kw)
}
if status != nil {
q = q.Where("status = ?", *status)
}
var total int64
if err := q.Count(&total).Error; err != nil {
return nil, 0, err
}
if page <= 0 {
page = 1
}
if pageSize <= 0 {
pageSize = 10
}
offset := (page - 1) * pageSize
var rows []entity.User
err := q.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&rows).Error
return rows, total, err
}
func (r *userRepository) ReplaceUserDepts(ctx context.Context, userID string, primaryDept string, deptIDs []string) error {
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("user_id = ?", userID).Delete(&entity.UserDept{}).Error; err != nil {
return err
}
seen := map[string]struct{}{}
for _, did := range deptIDs {
if _, ok := seen[did]; ok {
continue
}
seen[did] = struct{}{}
ud := entity.UserDept{
ID: id.New(),
UserID: userID,
DeptID: did,
IsPrimary: did == primaryDept,
}
if err := tx.Create(&ud).Error; err != nil {
return err
}
}
if len(deptIDs) == 0 && primaryDept != "" {
ud := entity.UserDept{ID: id.New(), UserID: userID, DeptID: primaryDept, IsPrimary: true}
return tx.Create(&ud).Error
}
return nil
})
}
func (r *userRepository) ReplaceUserRoles(ctx context.Context, userID string, roleIDs []string) error {
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("user_id = ?", userID).Delete(&entity.UserRole{}).Error; err != nil {
return err
}
for _, rid := range roleIDs {
ur := entity.UserRole{ID: id.New(), UserID: userID, RoleID: rid}
if err := tx.Create(&ur).Error; err != nil {
return err
}
}
return nil
})
}
func (r *userRepository) ListRoleIDs(ctx context.Context, userID string) ([]string, error) {
var ids []string
err := r.db.WithContext(ctx).Model(&entity.UserRole{}).Where("user_id = ?", userID).Pluck("role_id", &ids).Error
return ids, err
}
func (r *userRepository) ListDeptIDs(ctx context.Context, userID string) ([]string, error) {
var ids []string
err := r.db.WithContext(ctx).Model(&entity.UserDept{}).Where("user_id = ?", userID).Pluck("dept_id", &ids).Error
return ids, err
}
+4
View File
@@ -0,0 +1,4 @@
package service
// DefaultTenantAdminRoleCode 新租户初始化时的单位管理员角色编码
const DefaultTenantAdminRoleCode = "tenant_admin"
+326
View File
@@ -0,0 +1,326 @@
package service
import (
"context"
"errors"
"fmt"
"strings"
"giter.top/smart/internal/iam/entity"
"giter.top/smart/internal/iam/repository"
"giter.top/smart/pkg/utils/id"
)
// DeptService 部门
type DeptService interface {
Tree(ctx context.Context, tenantID string, keyword string, leaderID *string) ([]DeptNode, error)
Create(ctx context.Context, tenantID string, req *CreateDeptRequest) (*entity.Dept, error)
Update(ctx context.Context, tenantID string, id string, req *UpdateDeptRequest) (*entity.Dept, error)
Delete(ctx context.Context, tenantID string, ids []string) error
Get(ctx context.Context, tenantID string, id string) (*entity.Dept, error)
}
type CreateDeptRequest struct {
ParentID string `json:"parent_id"`
DeptName string `json:"dept_name" binding:"required,max=128"`
LeaderID *string `json:"leader_id"`
SortOrder int `json:"sort_order"`
}
type UpdateDeptRequest struct {
ParentID *string `json:"parent_id"`
DeptName *string `json:"dept_name" binding:"omitempty,max=128"`
LeaderID *string `json:"leader_id"`
SortOrder *int `json:"sort_order"`
}
// DeptNode 树节点
type DeptNode struct {
entity.Dept
Children []DeptNode `json:"children,omitempty"`
}
type deptService struct {
depts repository.DeptRepository
users repository.UserRepository
}
func NewDeptService(depts repository.DeptRepository, users repository.UserRepository) DeptService {
return &deptService{depts: depts, users: users}
}
func isDeptRoot(d *entity.Dept) bool {
return d.ParentID == "" || d.ParentID == "0"
}
func (s *deptService) Tree(ctx context.Context, tenantID string, keyword string, leaderID *string) ([]DeptNode, error) {
rows, err := s.depts.ListByTenant(ctx, tenantID)
if err != nil {
return nil, err
}
filtered := rows
if keyword != "" || leaderID != nil {
filtered = make([]entity.Dept, 0)
for _, d := range rows {
if keyword != "" && !strings.Contains(d.DeptName, keyword) {
continue
}
if leaderID != nil && (d.LeaderID == nil || *d.LeaderID != *leaderID) {
continue
}
filtered = append(filtered, d)
}
if keyword != "" || leaderID != nil {
filtered = s.includeAncestors(rows, filtered)
}
}
return buildDeptTree(filtered, ""), nil
}
func (s *deptService) includeAncestors(all []entity.Dept, matched []entity.Dept) []entity.Dept {
idSet := map[string]struct{}{}
byID := map[string]entity.Dept{}
for _, d := range all {
byID[d.ID] = d
}
for _, d := range matched {
cur := d
for {
idSet[cur.ID] = struct{}{}
if isDeptRoot(&cur) {
break
}
p, ok := byID[cur.ParentID]
if !ok {
break
}
cur = p
}
}
out := make([]entity.Dept, 0, len(idSet))
for _, d := range all {
if _, ok := idSet[d.ID]; ok {
out = append(out, d)
}
}
return out
}
func buildDeptTree(rows []entity.Dept, parentID string) []DeptNode {
children := map[string][]entity.Dept{}
for _, d := range rows {
pid := d.ParentID
if d.ParentID == "0" {
pid = ""
}
children[pid] = append(children[pid], d)
}
var walk func(pid string) []DeptNode
walk = func(pid string) []DeptNode {
list := children[pid]
out := make([]DeptNode, 0, len(list))
for _, d := range list {
out = append(out, DeptNode{Dept: d, Children: walk(d.ID)})
}
return out
}
return walk(parentID)
}
func (s *deptService) Create(ctx context.Context, tenantID string, req *CreateDeptRequest) (*entity.Dept, error) {
parentKey := req.ParentID
if parentKey == "0" {
parentKey = ""
}
ok, err := s.depts.ExistsSiblingName(ctx, tenantID, parentKey, req.DeptName, "")
if err != nil {
return nil, err
}
if ok {
return nil, fmt.Errorf("同级部门名称已存在")
}
d := &entity.Dept{
ID: id.New(),
TenantID: tenantID,
ParentID: parentKey,
DeptName: req.DeptName,
LeaderID: req.LeaderID,
SortOrder: req.SortOrder,
Status: 1,
}
if err := s.depts.Create(ctx, d); err != nil {
return nil, err
}
path := fmt.Sprintf("/%s/", d.ID)
if parentKey != "" {
p, err := s.depts.GetByID(ctx, parentKey)
if err != nil {
return nil, err
}
if p.TenantID != tenantID {
return nil, fmt.Errorf("父部门不属于当前租户")
}
base := p.DeptPath
if base == "" {
base = fmt.Sprintf("/%s/", p.ID)
}
path = base + fmt.Sprintf("%s/", d.ID)
}
if err := s.depts.UpdatePath(ctx, d.ID, path); err != nil {
return nil, err
}
d.DeptPath = path
return d, nil
}
func (s *deptService) Update(ctx context.Context, tenantID string, id string, req *UpdateDeptRequest) (*entity.Dept, error) {
d, err := s.depts.GetByID(ctx, id)
if err != nil {
if errors.Is(err, repository.ErrNotFound) {
return nil, fmt.Errorf("部门不存在")
}
return nil, err
}
if d.TenantID != tenantID {
return nil, fmt.Errorf("部门不属于当前租户")
}
if isDeptRoot(d) {
if req.ParentID != nil && *req.ParentID != "" && *req.ParentID != "0" {
return nil, fmt.Errorf("根部门禁止移动")
}
if req.DeptName != nil && *req.DeptName != "" && *req.DeptName != d.DeptName {
return nil, fmt.Errorf("根部门禁止重命名")
}
}
curParent := d.ParentID
if curParent == "0" {
curParent = ""
}
var newParentForName string
if req.ParentID != nil {
np := *req.ParentID
if np == "0" {
np = ""
}
newParentForName = np
} else {
newParentForName = curParent
}
if req.DeptName != nil && *req.DeptName != "" {
ok, err := s.depts.ExistsSiblingName(ctx, tenantID, newParentForName, *req.DeptName, id)
if err != nil {
return nil, err
}
if ok {
return nil, fmt.Errorf("同级部门名称已存在")
}
d.DeptName = *req.DeptName
}
if req.ParentID != nil {
npID := *req.ParentID
if npID == "0" {
npID = ""
}
if npID != curParent {
if npID == id {
return nil, fmt.Errorf("不能将部门移动到自身之下")
}
if npID != "" {
if s.isDescendant(ctx, id, npID) {
return nil, fmt.Errorf("禁止移动至子部门(防环)")
}
np, err := s.depts.GetByID(ctx, npID)
if err != nil {
return nil, fmt.Errorf("父部门无效")
}
if np.TenantID != tenantID {
return nil, fmt.Errorf("父部门不属于当前租户")
}
d.ParentID = npID
base := np.DeptPath
if base == "" {
base = fmt.Sprintf("/%s/", np.ID)
}
d.DeptPath = base + fmt.Sprintf("%s/", d.ID)
} else {
d.ParentID = ""
d.DeptPath = fmt.Sprintf("/%s/", d.ID)
}
_ = s.depts.UpdatePath(ctx, d.ID, d.DeptPath)
}
}
if req.LeaderID != nil {
d.LeaderID = req.LeaderID
}
if req.SortOrder != nil {
d.SortOrder = *req.SortOrder
}
if err := s.depts.Update(ctx, d); err != nil {
return nil, err
}
return d, nil
}
func (s *deptService) isDescendant(ctx context.Context, rootID, nodeID string) bool {
if nodeID == rootID {
return true
}
cur, err := s.depts.GetByID(ctx, nodeID)
if err != nil {
return false
}
for i := 0; i < 64 && cur.ParentID != "" && cur.ParentID != "0"; i++ {
if cur.ParentID == rootID {
return true
}
cur, err = s.depts.GetByID(ctx, cur.ParentID)
if err != nil {
return false
}
}
return false
}
func (s *deptService) Delete(ctx context.Context, tenantID string, ids []string) error {
for _, did := range ids {
d, err := s.depts.GetByID(ctx, did)
if err != nil {
return err
}
if d.TenantID != tenantID {
return fmt.Errorf("部门 %s 不属于当前租户", did)
}
if isDeptRoot(d) {
return fmt.Errorf("根部门禁止删除")
}
n, err := s.depts.CountChildren(ctx, did)
if err != nil {
return err
}
if n > 0 {
return fmt.Errorf("部门 %s 存在子部门", did)
}
uc, err := s.users.CountByDept(ctx, did)
if err != nil {
return err
}
if uc > 0 {
return fmt.Errorf("部门 %s 仍存在用户", did)
}
if err := s.depts.Delete(ctx, did); err != nil {
return err
}
}
return nil
}
func (s *deptService) Get(ctx context.Context, tenantID string, id string) (*entity.Dept, error) {
d, err := s.depts.GetByID(ctx, id)
if err != nil {
return nil, err
}
if d.TenantID != tenantID {
return nil, fmt.Errorf("部门不属于当前租户")
}
return d, nil
}
+319
View File
@@ -0,0 +1,319 @@
package service
import (
"context"
"errors"
"fmt"
"sort"
"giter.top/smart/internal/iam/entity"
"giter.top/smart/internal/iam/repository"
"giter.top/smart/pkg/utils/id"
)
// MenuService 菜单(全局资源)
type MenuService interface {
Create(ctx context.Context, req *CreateMenuRequest, isPlatform bool) (*entity.Menu, error)
Update(ctx context.Context, mid string, req *UpdateMenuRequest, isPlatform bool) (*entity.Menu, error)
Delete(ctx context.Context, ids []string, isPlatform bool) error
Get(ctx context.Context, mid string) (*entity.Menu, error)
Tree(ctx context.Context, menuType *int16) ([]MenuNode, error)
NavForUser(ctx context.Context, userID string) ([]MenuNode, error)
PermsForUser(ctx context.Context, userID string) ([]string, error)
}
type CreateMenuRequest struct {
ParentID string `json:"parent_id"`
MenuName string `json:"menu_name" binding:"required,max=128"`
MenuType int16 `json:"menu_type" binding:"required"`
Perms string `json:"perms"`
Path string `json:"path"`
Component string `json:"component"`
Icon string `json:"icon"`
SortOrder int `json:"sort_order"`
IsVisible bool `json:"is_visible"`
IsBuiltin bool `json:"is_builtin"`
ExternalLink string `json:"external_link"`
}
type UpdateMenuRequest struct {
ParentID *string `json:"parent_id"`
MenuName *string `json:"menu_name"`
SortOrder *int `json:"sort_order"`
IsVisible *bool `json:"is_visible"`
Path *string `json:"path"`
Component *string `json:"component"`
Icon *string `json:"icon"`
ExternalLink *string `json:"external_link"`
Status *int16 `json:"status"`
}
// MenuNode 菜单树节点
type MenuNode struct {
entity.Menu
Children []MenuNode `json:"children,omitempty"`
}
type menuService struct {
menus repository.MenuRepository
roles repository.RoleRepository
users repository.UserRepository
}
func NewMenuService(menus repository.MenuRepository, roles repository.RoleRepository, users repository.UserRepository) MenuService {
return &menuService{menus: menus, roles: roles, users: users}
}
func normalizeMenuParent(pid string) string {
if pid == "0" {
return ""
}
return pid
}
func (s *menuService) Create(ctx context.Context, req *CreateMenuRequest, isPlatform bool) (*entity.Menu, error) {
if !isPlatform {
return nil, repository.ErrForbidden
}
if req.Perms != "" {
ok, err := s.menus.ExistsPerms(ctx, req.Perms, "")
if err != nil {
return nil, err
}
if ok {
return nil, fmt.Errorf("权限标识已存在")
}
}
m := &entity.Menu{
ID: id.New(),
ParentID: normalizeMenuParent(req.ParentID),
MenuName: req.MenuName,
MenuType: req.MenuType,
Perms: req.Perms,
Path: req.Path,
Component: req.Component,
Icon: req.Icon,
SortOrder: req.SortOrder,
IsVisible: req.IsVisible,
IsBuiltin: req.IsBuiltin,
ExternalLink: req.ExternalLink,
Status: 1,
}
if err := s.menus.Create(ctx, m); err != nil {
return nil, err
}
return m, nil
}
func (s *menuService) Update(ctx context.Context, mid string, req *UpdateMenuRequest, isPlatform bool) (*entity.Menu, error) {
if !isPlatform {
return nil, repository.ErrForbidden
}
m, err := s.menus.GetByID(ctx, mid)
if err != nil {
if errors.Is(err, repository.ErrNotFound) {
return nil, fmt.Errorf("菜单不存在")
}
return nil, err
}
if m.IsBuiltin {
return nil, fmt.Errorf("系统内置菜单禁止修改")
}
if req.MenuName != nil {
m.MenuName = *req.MenuName
}
if req.SortOrder != nil {
m.SortOrder = *req.SortOrder
}
if req.IsVisible != nil {
m.IsVisible = *req.IsVisible
}
if req.Path != nil {
m.Path = *req.Path
}
if req.Component != nil {
m.Component = *req.Component
}
if req.Icon != nil {
m.Icon = *req.Icon
}
if req.ExternalLink != nil {
m.ExternalLink = *req.ExternalLink
}
if req.Status != nil {
m.Status = *req.Status
}
if err := s.menus.Update(ctx, m); err != nil {
return nil, err
}
return m, nil
}
func (s *menuService) Delete(ctx context.Context, ids []string, isPlatform bool) error {
if !isPlatform {
return repository.ErrForbidden
}
for _, mid := range ids {
m, err := s.menus.GetByID(ctx, mid)
if err != nil {
return err
}
if m.IsBuiltin {
return fmt.Errorf("系统内置菜单禁止删除")
}
n, err := s.menus.CountChildren(ctx, mid)
if err != nil {
return err
}
if n > 0 {
return fmt.Errorf("存在子菜单,无法删除")
}
rn, err := s.menus.CountRoleRefs(ctx, mid)
if err != nil {
return err
}
if rn > 0 {
return fmt.Errorf("菜单仍被角色引用")
}
if err := s.menus.Delete(ctx, mid); err != nil {
return err
}
}
return nil
}
func (s *menuService) Get(ctx context.Context, mid string) (*entity.Menu, error) {
return s.menus.GetByID(ctx, mid)
}
func (s *menuService) Tree(ctx context.Context, menuType *int16) ([]MenuNode, error) {
rows, err := s.menus.ListByType(ctx, menuType)
if err != nil {
return nil, err
}
return buildMenuTreeRows(rows), nil
}
func buildMenuTreeRows(rows []entity.Menu) []MenuNode {
byParent := map[string][]entity.Menu{}
for _, m := range rows {
pid := normalizeMenuParent(m.ParentID)
byParent[pid] = append(byParent[pid], m)
}
for k := range byParent {
sort.Slice(byParent[k], func(i, j int) bool {
if byParent[k][i].SortOrder != byParent[k][j].SortOrder {
return byParent[k][i].SortOrder < byParent[k][j].SortOrder
}
return byParent[k][i].ID < byParent[k][j].ID
})
}
var walk func(pid string) []MenuNode
walk = func(pid string) []MenuNode {
list := byParent[pid]
out := make([]MenuNode, 0, len(list))
for _, m := range list {
out = append(out, MenuNode{Menu: m, Children: walk(m.ID)})
}
return out
}
return walk("")
}
func (s *menuService) NavForUser(ctx context.Context, userID string) ([]MenuNode, error) {
rids, err := s.users.ListRoleIDs(ctx, userID)
if err != nil {
return nil, err
}
menuIDs, err := s.roles.ListMenuIDsByRoles(ctx, rids)
if err != nil {
return nil, err
}
allowed := map[string]struct{}{}
for _, mid := range menuIDs {
allowed[mid] = struct{}{}
}
pub, err := s.menus.ListByPerms(ctx, entity.PublicOverviewPerms)
if err != nil {
return nil, err
}
for _, m := range pub {
allowed[m.ID] = struct{}{}
}
all, err := s.menus.ListAll(ctx)
if err != nil {
return nil, err
}
byID := map[string]entity.Menu{}
for _, m := range all {
byID[m.ID] = m
}
for _, m := range all {
if _, ok := allowed[m.ID]; !ok {
continue
}
cur := m
for {
pid := normalizeMenuParent(cur.ParentID)
if pid == "" {
break
}
p, ok := byID[pid]
if !ok {
break
}
allowed[p.ID] = struct{}{}
cur = p
}
}
filtered := make([]entity.Menu, 0)
for _, m := range all {
if _, ok := allowed[m.ID]; ok && m.Status == 1 && m.IsVisible {
filtered = append(filtered, m)
}
}
tree := buildMenuTreeRows(filtered)
return pruneEmptyDirs(tree), nil
}
func pruneEmptyDirs(nodes []MenuNode) []MenuNode {
out := make([]MenuNode, 0, len(nodes))
for _, n := range nodes {
ch := pruneEmptyDirs(n.Children)
if n.MenuType == 1 && len(ch) == 0 {
continue
}
n.Children = ch
out = append(out, n)
}
return out
}
func (s *menuService) PermsForUser(ctx context.Context, userID string) ([]string, error) {
rids, err := s.users.ListRoleIDs(ctx, userID)
if err != nil {
return nil, err
}
mids, err := s.roles.ListMenuIDsByRoles(ctx, rids)
if err != nil {
return nil, err
}
all, err := s.menus.ListAll(ctx)
if err != nil {
return nil, err
}
idset := map[string]struct{}{}
for _, mid := range mids {
idset[mid] = struct{}{}
}
var perms []string
for _, m := range all {
if _, ok := idset[m.ID]; !ok {
continue
}
if m.Perms != "" {
perms = append(perms, m.Perms)
}
}
return perms, nil
}
+223
View File
@@ -0,0 +1,223 @@
package service
import (
"context"
"errors"
"fmt"
"giter.top/smart/internal/iam/entity"
"giter.top/smart/internal/iam/repository"
"giter.top/smart/pkg/utils/id"
)
// RoleService 角色
type RoleService interface {
Create(ctx context.Context, tenantID string, req *CreateRoleRequest, grantorUserID *string) (*entity.Role, error)
Update(ctx context.Context, tenantID string, rid string, req *UpdateRoleRequest, grantorUserID *string) (*entity.Role, error)
Delete(ctx context.Context, tenantID string, ids []string) error
Get(ctx context.Context, tenantID string, rid string) (*entity.Role, error)
List(ctx context.Context, tenantID string, name, code string, page, pageSize int) (*RoleListResponse, error)
AssignMenus(ctx context.Context, tenantID string, roleID string, menuIDs []string, grantorUserID *string) error
}
type CreateRoleRequest struct {
RoleCode string `json:"role_code" binding:"required,max=64"`
RoleName string `json:"role_name" binding:"required,max=128"`
DataScope int16 `json:"data_scope" binding:"required"`
Description string `json:"description"`
MenuIDs []string `json:"menu_ids"`
}
type UpdateRoleRequest struct {
RoleName *string `json:"role_name"`
DataScope *int16 `json:"data_scope"`
Description *string `json:"description"`
MenuIDs []string `json:"menu_ids"`
}
type RoleListResponse struct {
Items []entity.Role `json:"items"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
TotalPages int `json:"total_pages"`
}
type roleService struct {
roles repository.RoleRepository
users repository.UserRepository
menus repository.MenuRepository
}
func NewRoleService(roles repository.RoleRepository, users repository.UserRepository, menus repository.MenuRepository) RoleService {
return &roleService{roles: roles, users: users, menus: menus}
}
func (s *roleService) grantorMenuSet(ctx context.Context, grantorUserID string) (map[string]struct{}, error) {
rids, err := s.users.ListRoleIDs(ctx, grantorUserID)
if err != nil {
return nil, err
}
ids, err := s.roles.ListMenuIDsByRoles(ctx, rids)
if err != nil {
return nil, err
}
m := make(map[string]struct{}, len(ids))
for _, mid := range ids {
m[mid] = struct{}{}
}
return m, nil
}
func (s *roleService) assertMenuSubset(ctx context.Context, grantorUserID *string, menuIDs []string) error {
if grantorUserID == nil || *grantorUserID == "" {
return nil
}
allowed, err := s.grantorMenuSet(ctx, *grantorUserID)
if err != nil {
return err
}
for _, mid := range menuIDs {
if _, ok := allowed[mid]; !ok {
return fmt.Errorf("防越权: 不能分配自身未拥有的菜单权限 (menu_id=%s)", mid)
}
}
return nil
}
func (s *roleService) Create(ctx context.Context, tenantID string, req *CreateRoleRequest, grantorUserID *string) (*entity.Role, error) {
ok, err := s.roles.ExistsCode(ctx, tenantID, req.RoleCode, "")
if err != nil {
return nil, err
}
if ok {
return nil, fmt.Errorf("角色编码已存在")
}
if err := s.assertMenuSubset(ctx, grantorUserID, req.MenuIDs); err != nil {
return nil, err
}
r := &entity.Role{
ID: id.New(),
TenantID: tenantID,
RoleCode: req.RoleCode,
RoleName: req.RoleName,
DataScope: req.DataScope,
Description: req.Description,
Status: 1,
}
if err := s.roles.Create(ctx, r); err != nil {
return nil, err
}
if len(req.MenuIDs) > 0 {
if err := s.roles.ReplaceRoleMenus(ctx, r.ID, req.MenuIDs); err != nil {
return nil, err
}
}
return r, nil
}
func (s *roleService) Update(ctx context.Context, tenantID string, rid string, req *UpdateRoleRequest, grantorUserID *string) (*entity.Role, error) {
r, err := s.roles.GetByID(ctx, rid)
if err != nil {
if errors.Is(err, repository.ErrNotFound) {
return nil, fmt.Errorf("角色不存在")
}
return nil, err
}
if r.TenantID != tenantID {
return nil, fmt.Errorf("角色不属于当前租户")
}
if r.IsBuiltin {
// 内置角色仅允许改部分字段(MVP:允许改名称与数据范围与菜单需业务再定)
}
if req.RoleName != nil {
r.RoleName = *req.RoleName
}
if req.DataScope != nil {
r.DataScope = *req.DataScope
}
if req.Description != nil {
r.Description = *req.Description
}
if err := s.roles.Update(ctx, r); err != nil {
return nil, err
}
if req.MenuIDs != nil {
if err := s.assertMenuSubset(ctx, grantorUserID, req.MenuIDs); err != nil {
return nil, err
}
if err := s.roles.ReplaceRoleMenus(ctx, r.ID, req.MenuIDs); err != nil {
return nil, err
}
}
return r, nil
}
func (s *roleService) Delete(ctx context.Context, tenantID string, ids []string) error {
for _, rid := range ids {
r, err := s.roles.GetByID(ctx, rid)
if err != nil {
return err
}
if r.TenantID != tenantID {
return fmt.Errorf("角色 %s 不属于当前租户", rid)
}
if r.IsBuiltin {
return fmt.Errorf("内置角色不可删除")
}
n, err := s.roles.CountUsers(ctx, rid)
if err != nil {
return err
}
if n > 0 {
return fmt.Errorf("角色仍被用户使用")
}
if err := s.roles.Delete(ctx, rid); err != nil {
return err
}
}
return nil
}
func (s *roleService) Get(ctx context.Context, tenantID string, rid string) (*entity.Role, error) {
r, err := s.roles.GetByID(ctx, rid)
if err != nil {
return nil, err
}
if r.TenantID != tenantID {
return nil, fmt.Errorf("角色不属于当前租户")
}
return r, nil
}
func (s *roleService) List(ctx context.Context, tenantID string, name, code string, page, pageSize int) (*RoleListResponse, error) {
if page <= 0 {
page = 1
}
if pageSize <= 0 {
pageSize = 10
}
rows, total, err := s.roles.List(ctx, tenantID, name, code, page, pageSize)
if err != nil {
return nil, err
}
tp := int(total) / pageSize
if int(total)%pageSize != 0 {
tp++
}
return &RoleListResponse{Items: rows, Total: total, Page: page, PageSize: pageSize, TotalPages: tp}, nil
}
func (s *roleService) AssignMenus(ctx context.Context, tenantID string, roleID string, menuIDs []string, grantorUserID *string) error {
r, err := s.roles.GetByID(ctx, roleID)
if err != nil {
return err
}
if r.TenantID != tenantID {
return fmt.Errorf("角色不属于当前租户")
}
if err := s.assertMenuSubset(ctx, grantorUserID, menuIDs); err != nil {
return err
}
return s.roles.ReplaceRoleMenus(ctx, roleID, menuIDs)
}
+17
View File
@@ -0,0 +1,17 @@
package service
import "giter.top/smart/internal/iam/entity"
// MergeDataScope 多角色数据范围并集:取最大(PRD:全部 > 本部门及子部门 > 本部门 > 仅本人)
func MergeDataScope(scopes []int16) int16 {
var m int16
for _, s := range scopes {
if s > m {
m = s
}
}
if m == 0 {
return entity.DataScopeSelf
}
return m
}
+261
View File
@@ -0,0 +1,261 @@
package service
import (
"context"
"errors"
"fmt"
"time"
"giter.top/smart/internal/iam/entity"
"giter.top/smart/internal/iam/repository"
"giter.top/smart/pkg/utils/id"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
// TenantService 租户
type TenantService interface {
Create(ctx context.Context, req *CreateTenantRequest) (*entity.Tenant, error)
Update(ctx context.Context, id string, req *UpdateTenantRequest) (*entity.Tenant, error)
Delete(ctx context.Context, ids []string) error
Get(ctx context.Context, id string) (*entity.Tenant, error)
List(ctx context.Context, name, code string, status *int16, page, pageSize int) (*TenantListResponse, error)
}
type CreateTenantRequest struct {
TenantCode string `json:"tenant_code" binding:"required,max=64"`
TenantName string `json:"tenant_name" binding:"required,max=128"`
AdminUserName string `json:"admin_user_name" binding:"required,max=64"`
AdminPassword string `json:"admin_password" binding:"required,min=6,max=64"`
AdminRealName string `json:"admin_real_name" binding:"max=64"`
}
type UpdateTenantRequest struct {
TenantName *string `json:"tenant_name"`
TenantCode *string `json:"tenant_code" binding:"omitempty,max=64"`
Status *int16 `json:"status"`
ExpireTime *string `json:"expire_time"` // RFC3339
}
type TenantListItem struct {
entity.Tenant
UserCount int64 `json:"user_count"`
DeptCount int64 `json:"dept_count"`
}
type TenantListResponse struct {
Items []TenantListItem `json:"items"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
TotalPages int `json:"total_pages"`
}
type tenantService struct {
db *gorm.DB
tenants repository.TenantRepository
depts repository.DeptRepository
users repository.UserRepository
roles repository.RoleRepository
menus repository.MenuRepository
}
func NewTenantService(
db *gorm.DB,
tenants repository.TenantRepository,
depts repository.DeptRepository,
users repository.UserRepository,
roles repository.RoleRepository,
menus repository.MenuRepository,
) TenantService {
return &tenantService{db: db, tenants: tenants, depts: depts, users: users, roles: roles, menus: menus}
}
func (s *tenantService) Create(ctx context.Context, req *CreateTenantRequest) (*entity.Tenant, error) {
ok, err := s.tenants.ExistsCode(ctx, req.TenantCode, "")
if err != nil {
return nil, err
}
if ok {
return nil, fmt.Errorf("租户编码已存在")
}
hash, err := bcrypt.GenerateFromPassword([]byte(req.AdminPassword), bcrypt.DefaultCost)
if err != nil {
return nil, err
}
var out *entity.Tenant
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
t := &entity.Tenant{
ID: id.New(),
TenantCode: req.TenantCode,
TenantName: req.TenantName,
Status: 1,
}
if err := tx.Create(t).Error; err != nil {
return err
}
var ucount int64
if err := tx.Model(&entity.User{}).Where("tenant_id = ? AND user_name = ?", t.ID, req.AdminUserName).Count(&ucount).Error; err != nil {
return err
}
if ucount > 0 {
return fmt.Errorf("管理员账号已存在")
}
root := &entity.Dept{
ID: id.New(),
TenantID: t.ID,
ParentID: "",
DeptName: req.TenantName,
SortOrder: 0,
Status: 1,
}
if err := tx.Create(root).Error; err != nil {
return err
}
path := fmt.Sprintf("/%s/", root.ID)
if err := tx.Model(&entity.Dept{}).Where("id = ?", root.ID).Update("dept_path", path).Error; err != nil {
return err
}
admin := &entity.User{
ID: id.New(),
TenantID: t.ID,
DeptID: &root.ID,
UserName: req.AdminUserName,
RealName: req.AdminRealName,
PasswordHash: string(hash),
Status: 1,
}
if err := tx.Create(admin).Error; err != nil {
return err
}
if err := tx.Create(&entity.UserDept{ID: id.New(), UserID: admin.ID, DeptID: root.ID, IsPrimary: true}).Error; err != nil {
return err
}
role := &entity.Role{
ID: id.New(),
TenantID: t.ID,
RoleCode: DefaultTenantAdminRoleCode,
RoleName: "超级管理员",
DataScope: entity.DataScopeAll,
Description: "租户初始化角色",
IsBuiltin: true,
Status: 1,
}
if err := tx.Create(role).Error; err != nil {
return err
}
var allMenus []entity.Menu
if err := tx.Find(&allMenus).Error; err != nil {
return err
}
for _, m := range allMenus {
if err := tx.Create(&entity.RoleMenu{ID: id.New(), RoleID: role.ID, MenuID: m.ID}).Error; err != nil {
return err
}
}
if err := tx.Create(&entity.UserRole{ID: id.New(), UserID: admin.ID, RoleID: role.ID}).Error; err != nil {
return err
}
aid := admin.ID
if err := tx.Model(t).Update("admin_user_id", aid).Error; err != nil {
return err
}
t.AdminUserID = &aid
out = t
return nil
})
if err != nil {
return nil, err
}
return out, nil
}
func (s *tenantService) Update(ctx context.Context, id string, req *UpdateTenantRequest) (*entity.Tenant, error) {
t, err := s.tenants.GetByID(ctx, id)
if err != nil {
if errors.Is(err, repository.ErrNotFound) {
return nil, fmt.Errorf("租户不存在")
}
return nil, err
}
if req.TenantName != nil && *req.TenantName != "" {
t.TenantName = *req.TenantName
if root, err := s.depts.FindRoot(ctx, t.ID); err == nil {
root.DeptName = *req.TenantName
_ = s.depts.Update(ctx, root)
}
}
if req.TenantCode != nil && *req.TenantCode != "" {
ok, err := s.tenants.ExistsCode(ctx, *req.TenantCode, id)
if err != nil {
return nil, err
}
if ok {
return nil, fmt.Errorf("租户编码已存在")
}
t.TenantCode = *req.TenantCode
}
if req.Status != nil {
t.Status = *req.Status
}
if req.ExpireTime != nil && *req.ExpireTime != "" {
et, err := time.Parse(time.RFC3339, *req.ExpireTime)
if err != nil {
return nil, fmt.Errorf("到期时间格式无效: %w", err)
}
t.ExpireTime = &et
if et.Before(time.Now()) {
t.Status = 0
}
}
if err := s.tenants.Update(ctx, t); err != nil {
return nil, err
}
return t, nil
}
func (s *tenantService) Delete(ctx context.Context, ids []string) error {
for _, tid := range ids {
n, err := s.tenants.CountUsers(ctx, tid)
if err != nil {
return err
}
if n > 0 {
return fmt.Errorf("租户 %s 仍存在用户,无法删除", tid)
}
}
for _, tid := range ids {
if err := s.db.WithContext(ctx).Delete(&entity.Tenant{}, "id = ?", tid).Error; err != nil {
return err
}
}
return nil
}
func (s *tenantService) Get(ctx context.Context, id string) (*entity.Tenant, error) {
return s.tenants.GetByID(ctx, id)
}
func (s *tenantService) List(ctx context.Context, name, code string, status *int16, page, pageSize int) (*TenantListResponse, error) {
if page <= 0 {
page = 1
}
if pageSize <= 0 {
pageSize = 10
}
rows, total, err := s.tenants.List(ctx, name, code, status, page, pageSize)
if err != nil {
return nil, err
}
items := make([]TenantListItem, 0, len(rows))
for _, t := range rows {
uc, _ := s.tenants.CountUsers(ctx, t.ID)
dc, _ := s.tenants.CountDepts(ctx, t.ID)
items = append(items, TenantListItem{Tenant: t, UserCount: uc, DeptCount: dc})
}
tp := int(total) / pageSize
if int(total)%pageSize != 0 {
tp++
}
return &TenantListResponse{Items: items, Total: total, Page: page, PageSize: pageSize, TotalPages: tp}, nil
}
+240
View File
@@ -0,0 +1,240 @@
package service
import (
"context"
"errors"
"fmt"
"giter.top/smart/internal/iam/entity"
"giter.top/smart/internal/iam/repository"
"giter.top/smart/pkg/utils/id"
"golang.org/x/crypto/bcrypt"
)
// UserService 用户
type UserService interface {
Create(ctx context.Context, tenantID string, req *CreateUserRequest) (*entity.User, error)
Update(ctx context.Context, tenantID string, uid string, req *UpdateUserRequest) (*entity.User, error)
Delete(ctx context.Context, tenantID string, ids []string) error
Get(ctx context.Context, tenantID string, uid string) (*entity.User, error)
List(ctx context.Context, tenantID string, q *UserListQuery) (*UserListResponse, error)
DataScopeForUser(ctx context.Context, userID string) (int16, error)
}
type CreateUserRequest struct {
UserName string `json:"user_name" binding:"required,max=64"`
Password string `json:"password" binding:"required,min=6,max=64"`
RealName string `json:"real_name" binding:"max=64"`
Phone string `json:"phone"`
Email string `json:"email"`
DeptID *string `json:"dept_id"`
DeptIDs []string `json:"dept_ids"`
RoleIDs []string `json:"role_ids"`
}
type UpdateUserRequest struct {
RealName *string `json:"real_name"`
Phone *string `json:"phone"`
Email *string `json:"email"`
DeptID *string `json:"dept_id"`
DeptIDs []string `json:"dept_ids"`
RoleIDs []string `json:"role_ids"`
Status *int16 `json:"status"`
Password *string `json:"password"`
}
type UserListQuery struct {
DeptID *string
RoleID *string
Keyword string
Status *int16
Page int
PageSize int
}
type UserListResponse struct {
Items []entity.User `json:"items"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
TotalPages int `json:"total_pages"`
}
type userService struct {
users repository.UserRepository
roles repository.RoleRepository
}
func NewUserService(users repository.UserRepository, roles repository.RoleRepository) UserService {
return &userService{users: users, roles: roles}
}
func (s *userService) Create(ctx context.Context, tenantID string, req *CreateUserRequest) (*entity.User, error) {
ok, err := s.users.ExistsUserName(ctx, tenantID, req.UserName, "")
if err != nil {
return nil, err
}
if ok {
return nil, fmt.Errorf("账号已存在")
}
hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
return nil, err
}
u := &entity.User{
ID: id.New(),
TenantID: tenantID,
UserName: req.UserName,
RealName: req.RealName,
Phone: req.Phone,
Email: req.Email,
PasswordHash: string(hash),
Status: 1,
}
if req.DeptID != nil {
u.DeptID = req.DeptID
}
if err := s.users.Create(ctx, u); err != nil {
return nil, err
}
depts := req.DeptIDs
primary := ""
if req.DeptID != nil {
primary = *req.DeptID
}
if len(depts) == 0 && primary != "" {
depts = []string{primary}
}
if len(depts) > 0 {
if primary == "" {
primary = depts[0]
}
u.DeptID = &primary
_ = s.users.Update(ctx, u)
if err := s.users.ReplaceUserDepts(ctx, u.ID, primary, depts); err != nil {
return nil, err
}
}
if len(req.RoleIDs) > 0 {
if err := s.users.ReplaceUserRoles(ctx, u.ID, req.RoleIDs); err != nil {
return nil, err
}
}
return u, nil
}
func (s *userService) Update(ctx context.Context, tenantID string, uid string, req *UpdateUserRequest) (*entity.User, error) {
u, err := s.users.GetByID(ctx, uid)
if err != nil {
if errors.Is(err, repository.ErrNotFound) {
return nil, fmt.Errorf("用户不存在")
}
return nil, err
}
if u.TenantID != tenantID {
return nil, fmt.Errorf("用户不属于当前租户")
}
if req.RealName != nil {
u.RealName = *req.RealName
}
if req.Phone != nil {
u.Phone = *req.Phone
}
if req.Email != nil {
u.Email = *req.Email
}
if req.Status != nil {
u.Status = *req.Status
}
if req.Password != nil && *req.Password != "" {
hash, err := bcrypt.GenerateFromPassword([]byte(*req.Password), bcrypt.DefaultCost)
if err != nil {
return nil, err
}
u.PasswordHash = string(hash)
}
if err := s.users.Update(ctx, u); err != nil {
return nil, err
}
if req.DeptIDs != nil || req.DeptID != nil {
depts := req.DeptIDs
primary := ""
if req.DeptID != nil {
primary = *req.DeptID
u.DeptID = req.DeptID
_ = s.users.Update(ctx, u)
}
if len(depts) == 0 && primary != "" {
depts = []string{primary}
}
if primary == "" && len(depts) > 0 {
primary = depts[0]
}
if err := s.users.ReplaceUserDepts(ctx, u.ID, primary, depts); err != nil {
return nil, err
}
}
if req.RoleIDs != nil {
if err := s.users.ReplaceUserRoles(ctx, u.ID, req.RoleIDs); err != nil {
return nil, err
}
}
return u, nil
}
func (s *userService) Delete(ctx context.Context, tenantID string, ids []string) error {
for _, uid := range ids {
u, err := s.users.GetByID(ctx, uid)
if err != nil {
return err
}
if u.TenantID != tenantID {
return fmt.Errorf("用户 %s 不属于当前租户", uid)
}
if err := s.users.Delete(ctx, uid); err != nil {
return err
}
}
return nil
}
func (s *userService) Get(ctx context.Context, tenantID string, uid string) (*entity.User, error) {
u, err := s.users.GetByID(ctx, uid)
if err != nil {
return nil, err
}
if u.TenantID != tenantID {
return nil, fmt.Errorf("用户不属于当前租户")
}
return u, nil
}
func (s *userService) List(ctx context.Context, tenantID string, q *UserListQuery) (*UserListResponse, error) {
if q.Page <= 0 {
q.Page = 1
}
if q.PageSize <= 0 {
q.PageSize = 10
}
rows, total, err := s.users.List(ctx, tenantID, q.DeptID, q.RoleID, q.Keyword, q.Status, q.Page, q.PageSize)
if err != nil {
return nil, err
}
tp := int(total) / q.PageSize
if int(total)%q.PageSize != 0 {
tp++
}
return &UserListResponse{Items: rows, Total: total, Page: q.Page, PageSize: q.PageSize, TotalPages: tp}, nil
}
func (s *userService) DataScopeForUser(ctx context.Context, userID string) (int16, error) {
roles, err := s.roles.ListRolesByUser(ctx, userID)
if err != nil {
return 0, err
}
scopes := make([]int16, 0, len(roles))
for _, r := range roles {
scopes = append(scopes, r.DataScope)
}
return MergeDataScope(scopes), nil
}
+45
View File
@@ -0,0 +1,45 @@
package iam
import (
"giter.top/smart/internal/iam/handler"
"giter.top/smart/internal/iam/repository"
"giter.top/smart/internal/iam/service"
"github.com/google/wire"
)
// HandlerProviderSet 处理程序提供者集合
var handlerProviderSet = wire.NewSet(
handler.NewTenantHandler,
handler.NewDeptHandler,
handler.NewRoleHandler,
handler.NewUserHandler,
handler.NewMenuHandler,
)
// ServiceProviderSet 服务提供者集合
var serviceProviderSet = wire.NewSet(
service.NewTenantService,
service.NewDeptService,
service.NewRoleService,
service.NewUserService,
service.NewMenuService,
)
// RepositoryProviderSet 仓库提供者集合
var repositoryProviderSet = wire.NewSet(
repository.NewTenantRepository,
repository.NewDeptRepository,
repository.NewRoleRepository,
repository.NewUserRepository,
repository.NewMenuRepository,
)
var ProviderSet = wire.NewSet(
handlerProviderSet,
serviceProviderSet,
repositoryProviderSet,
// 路由注册
NewIamRoutes,
)