feat(rbac): F3 前端 RBAC 按钮级控制

后端:
- 新增 GET /user/permissions 端点返回当前用户权限码
- Login 响应附带 permissions 数组
- collectUserPermissionCodes 用 raw SQL JOIN 避免 GORM many2many 表名问题
- admin 用户返回 ['*'] 通配符

前端:
- usePermission hook (hasPermission/hasAnyPermission/hasAllPermissions)
- <HasPermission> 组件 (permission/anyOf/allOf + fallback)
- zustand store 新增 permissions 字段
- Sidebar 菜单递归过滤 (42 菜单项全部标注 permission)
- ServerManager: 添加/导入/批量删除按钮按权限显隐
- ServerTableRow: 终端/编辑/文件/删除按权限显隐
- SystemUserTab: 新建/导入/编辑/删除按钮按权限显隐
- MainLayout: 页面刷新自动拉取权限
- Login: 登录时存储 permissions

验证:
- admin: permissions=['*'] 通配符
- viewer: 7 个只读权限 (dashboard:view/server:view/...)
- viewer 无 server:create/delete
- 10/10 回归测试通过
- npm run build 零错误
This commit is contained in:
attacker committed 2026-06-25 22:51:25 +08:00
1 parent 77c234b060
commit be2a377f01
30 files changed
+2328 -462

No files matched your search

+193 -57
View File
@@ -1,26 +1,38 @@
package controller
import (
"encoding/json"
"net/http"
"strconv"
"time"
"ezops/model"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type flowStep struct {
Name string `json:"name"`
ApproverRole string `json:"approver_role"`
StepType string `json:"step_type"` // audit, execute
}
// ListProcesses 获取审批流程列表 // Logan.Li
func (h *ApprovalHandler) ListProcesses(c *gin.Context) {
var query model.PageQuery
c.ShouldBindQuery(&query)
db := h.db.Model(&model.ApprovalProcess{}).Preload("Template").Preload("ApplicantUser")
db := h.db.Model(&model.ApprovalProcess{}).
Preload("Template").
Preload("ApplicantUser").
Preload("Steps", func(db *gorm.DB) *gorm.DB {
return db.Order("step ASC")
})
// 状态过滤
if status := c.Query("status"); status != "" {
db = db.Where("status = ?", status)
}
// 按模板类别过滤
if category := c.Query("category"); category != "" {
db = db.Joins("JOIN sys_approval_templates ON sys_approval_templates.id = approval_processes.template_id").
Where("sys_approval_templates.category = ?", category)
@@ -30,10 +42,14 @@ func (h *ApprovalHandler) ListProcesses(c *gin.Context) {
db.Count(&total)
var processes []model.ApprovalProcess
db.Order("created_at DESC").
Offset(query.GetOffset()).
Limit(query.GetLimit()).
Find(&processes)
db.Order("created_at DESC").Offset(query.GetOffset()).Limit(query.GetLimit()).Find(&processes)
// Preload approver user for each step
for i := range processes {
for j := range processes[i].Steps {
h.db.Preload("ApproverUser").First(&processes[i].Steps[j], processes[i].Steps[j].ID)
}
}
c.JSON(http.StatusOK, model.Response{
Code: 200,
@@ -50,7 +66,10 @@ func (h *ApprovalHandler) GetProcess(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var process model.ApprovalProcess
if err := h.db.Preload("Template").Preload("ApplicantUser").Preload("Steps.ApproverUser").First(&process, id).Error; err != nil {
if err := h.db.Preload("Template").Preload("ApplicantUser").
Preload("Steps.ApproverUser").Preload("Steps", func(db *gorm.DB) *gorm.DB {
return db.Order("step ASC")
}).First(&process, id).Error; err != nil {
c.JSON(http.StatusNotFound, model.Response{
Code: 404,
Message: "流程不存在",
@@ -65,7 +84,7 @@ func (h *ApprovalHandler) GetProcess(c *gin.Context) {
})
}
// CreateProcess 创建审批流程 // Logan.Li
// CreateProcess 创建审批流程 — 使用审批流程配置 (sys_approval_workflows) // Logan.Li
func (h *ApprovalHandler) CreateProcess(c *gin.Context) {
var req model.CreateApprovalProcessRequest
if err := c.ShouldBindJSON(&req); err != nil {
@@ -76,46 +95,119 @@ func (h *ApprovalHandler) CreateProcess(c *gin.Context) {
return
}
// 获取当前用户 ID
userID, _ := c.Get("user_id")
userIDVal, _ := c.Get("userID")
userID := toUint(userIDVal)
// 获取模板
var template model.ApprovalTemplate
if err := h.db.First(&template, req.TemplateID).Error; err != nil {
c.JSON(http.StatusNotFound, model.Response{
Code: 404,
Message: "模板不存在",
})
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "模板不存在"})
return
}
// 查找该分类下的默认/活跃审批流程配置
var workflow model.ApprovalWorkflow
if err := h.db.Where("category = ? AND is_active = ?", template.Category, true).
Order("is_default DESC").First(&workflow).Error; err != nil {
// 无工作流配置,回退到模板 flow_config
}
// 获取工作流步骤(跳过 submit 类型)
var workflowSteps []model.ApprovalWorkflowStep
stepCount := 1
var approvalSteps []struct {
name string
stepType string
approverIDs []uint
}
if workflow.ID > 0 {
h.db.Where("workflow_id = ?", workflow.ID).Order("step_number ASC").Find(&workflowSteps)
for _, ws := range workflowSteps {
if ws.StepType == "submit" {
continue // 提交步骤由申请人完成,不生成审批节点
}
stepCount++
approvalSteps = append(approvalSteps, struct {
name string
stepType string
approverIDs []uint
}{ws.Name, ws.StepType, ParseApproverIDs(ws.ApproverIDs)})
}
}
totalSteps := len(approvalSteps)
if totalSteps == 0 {
totalSteps = 1
}
process := model.ApprovalProcess{
TemplateID: req.TemplateID,
Title: req.Title,
Applicant: toUint(userID),
Applicant: userID,
FormData: req.FormData,
Status: "pending",
CurrentStep: 0,
TotalSteps: 1, // 简化处理
CurrentStep: 1,
TotalSteps: totalSteps,
}
if err := h.db.Create(&process).Error; err != nil {
c.JSON(http.StatusInternalServerError, model.Response{
Code: 500,
Message: "创建失败: " + err.Error(),
})
tx := h.db.Begin()
if err := tx.Create(&process).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "创建失败: " + err.Error()})
return
}
h.db.Preload("Template").First(&process, process.ID)
// 创建审批步骤(仅 audit + execute)
for i, as := range approvalSteps {
stepType := as.stepType
if stepType == "" {
// 最后一步默认执行,其余审核
if i == len(approvalSteps)-1 {
stepType = "execute"
} else {
stepType = "audit"
}
}
approverID := uint(1) // 默认管理员
if len(as.approverIDs) > 0 {
approverID = as.approverIDs[0]
}
step := model.ApprovalStep{
ProcessID: process.ID,
Step: i + 1,
StepType: stepType,
Approver: approverID,
Status: "pending",
Comment: as.name, // 存步骤名称
}
if err := tx.Create(&step).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "创建步骤失败"})
return
}
}
tx.Commit()
// 重新加载完整数据
h.db.Preload("Template").Preload("ApplicantUser").Preload("Steps.ApproverUser").First(&process, process.ID)
c.JSON(http.StatusOK, model.Response{
Code: 200,
Message: "创建成功",
Message: "申请已提交",
Data: process,
})
}
// ParseApproverIDs 解析审批人JSON数组
func ParseApproverIDs(raw string) []uint {
if raw == "" {
return nil
}
var ids []uint
if err := json.Unmarshal([]byte(raw), &ids); err != nil {
return nil
}
return ids
}
func toUint(v interface{}) uint {
switch val := v.(type) {
case float64:
@@ -135,32 +227,63 @@ func (h *ApprovalHandler) ApproveProcess(c *gin.Context) {
var req model.ApproveRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, model.Response{
Code: 400,
Message: "请求参数错误",
})
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "请求参数错误"})
return
}
userIDVal, _ := c.Get("userID")
approverID := toUint(userIDVal)
var process model.ApprovalProcess
if err := h.db.First(&process, id).Error; err != nil {
c.JSON(http.StatusNotFound, model.Response{
Code: 404,
Message: "流程不存在",
})
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "流程不存在"})
return
}
process.Status = "approved"
if err := h.db.Save(&process).Error; err != nil {
c.JSON(http.StatusInternalServerError, model.Response{
Code: 500,
Message: "审批失败",
})
if process.Status != "pending" {
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "该申请已处理"})
return
}
now := time.Now()
// 查找当前步骤
var currentStep model.ApprovalStep
if err := h.db.Where("process_id = ? AND step = ?", process.ID, process.CurrentStep).First(&currentStep).Error; err != nil {
// 无步骤记录(旧数据兼容):直接通过
h.db.Model(&process).Updates(map[string]interface{}{
"status": "approved",
"approved_at": &now,
})
h.db.Preload("Template").Preload("ApplicantUser").Preload("Steps.ApproverUser").First(&process, process.ID)
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "审批通过", Data: process})
return
}
tx := h.db.Begin()
// 更新当前步骤为通过
tx.Model(&currentStep).Updates(map[string]interface{}{
"status": "approved",
"approver": approverID,
"comment": req.Comment,
"approved_at": &now,
})
// 推进到下一步 或 标记全部通过
if process.CurrentStep >= process.TotalSteps {
tx.Model(&process).Updates(map[string]interface{}{
"status": "approved",
"approved_at": &now,
})
} else {
tx.Model(&process).Update("current_step", process.CurrentStep+1)
}
tx.Commit()
h.db.Preload("Template").Preload("ApplicantUser").Preload("Steps.ApproverUser").First(&process, process.ID)
c.JSON(http.StatusOK, model.Response{
Code: 200,
Message: "审批通过",
@@ -174,33 +297,46 @@ func (h *ApprovalHandler) RejectProcess(c *gin.Context) {
var req model.ApproveRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, model.Response{
Code: 400,
Message: "请求参数错误",
})
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "请求参数错误"})
return
}
userIDVal, _ := c.Get("userID")
approverID := toUint(userIDVal)
var process model.ApprovalProcess
if err := h.db.First(&process, id).Error; err != nil {
c.JSON(http.StatusNotFound, model.Response{
Code: 404,
Message: "流程不存在",
})
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "流程不存在"})
return
}
process.Status = "rejected"
process.RejectReason = req.Comment
if err := h.db.Save(&process).Error; err != nil {
c.JSON(http.StatusInternalServerError, model.Response{
Code: 500,
Message: "拒绝失败",
})
if process.Status != "pending" {
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "该申请已处理"})
return
}
now := time.Now()
tx := h.db.Begin()
// 更新当前步骤为拒绝
tx.Model(&model.ApprovalStep{}).Where("process_id = ? AND step = ?", process.ID, process.CurrentStep).
Updates(map[string]interface{}{
"status": "rejected",
"approver": approverID,
"comment": req.Comment,
"approved_at": &now,
})
tx.Model(&process).Updates(map[string]interface{}{
"status": "rejected",
"rejected_at": &now,
"reject_reason": req.Comment,
})
tx.Commit()
h.db.Preload("Template").Preload("ApplicantUser").Preload("Steps.ApproverUser").First(&process, process.ID)
c.JSON(http.StatusOK, model.Response{
Code: 200,
Message: "已拒绝",
+318
View File
@@ -0,0 +1,318 @@
package controller
import (
"encoding/json"
"net/http"
"strconv"
"ezops/model"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// WorkflowHandler 审批流程配置处理器
type WorkflowHandler struct {
db *gorm.DB
}
// NewWorkflowHandler 创建处理器
func NewWorkflowHandler(db *gorm.DB) *WorkflowHandler {
return &WorkflowHandler{db: db}
}
// ListWorkflows 获取审批流程列表
func (h *WorkflowHandler) ListWorkflows(c *gin.Context) {
category := c.Query("category")
db := h.db.Model(&model.ApprovalWorkflow{})
if category != "" {
db = db.Where("category = ?", category)
}
var total int64
db.Count(&total)
var workflows []model.ApprovalWorkflow
db.Order("id ASC").Find(&workflows)
// 预加载步骤
for i := range workflows {
var steps []model.ApprovalWorkflowStep
h.db.Where("workflow_id = ?", workflows[i].ID).Order("step_number ASC").Find(&steps)
if steps == nil {
steps = []model.ApprovalWorkflowStep{}
}
workflows[i].Steps = steps
}
c.JSON(http.StatusOK, model.Response{
Code: 200,
Message: "success",
Data: model.ListResponse{
List: workflows,
Total: total,
},
})
}
// GetWorkflow 获取审批流程详情
func (h *WorkflowHandler) GetWorkflow(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var workflow model.ApprovalWorkflow
if err := h.db.First(&workflow, id).Error; err != nil {
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "流程不存在"})
return
}
var steps []model.ApprovalWorkflowStep
h.db.Where("workflow_id = ?", workflow.ID).Order("step_number ASC").Find(&steps)
if steps == nil {
steps = []model.ApprovalWorkflowStep{}
}
workflow.Steps = steps
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "success", Data: workflow})
}
// CreateWorkflow 创建审批流程
func (h *WorkflowHandler) CreateWorkflow(c *gin.Context) {
var req model.CreateWorkflowRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "参数错误: " + err.Error()})
return
}
if len(req.Steps) == 0 {
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "至少需要一个步骤"})
return
}
cat := req.Category
if cat == "" {
cat = "resource"
}
workflow := model.ApprovalWorkflow{
Name: req.Name,
Description: req.Description,
Category: cat,
IsActive: true,
}
tx := h.db.Begin()
if err := tx.Create(&workflow).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "创建失败"})
return
}
for _, s := range req.Steps {
step := model.ApprovalWorkflowStep{
WorkflowID: workflow.ID,
StepNumber: s.StepNumber,
Name: s.Name,
StepType: s.StepType,
ApproverIDs: s.ApproverIDs,
IsRequired: s.IsRequired,
TimeoutHours: s.TimeoutHours,
AutoApprove: s.AutoApproveTimeout,
}
if step.TimeoutHours == 0 {
step.TimeoutHours = 24
}
if err := tx.Create(&step).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "创建步骤失败"})
return
}
}
tx.Commit()
// Reload with steps
h.db.First(&workflow, workflow.ID)
var steps []model.ApprovalWorkflowStep
h.db.Where("workflow_id = ?", workflow.ID).Order("step_number ASC").Find(&steps)
workflow.Steps = steps
c.JSON(http.StatusCreated, model.Response{Code: 201, Message: "创建成功", Data: workflow})
}
// UpdateWorkflow 更新审批流程
func (h *WorkflowHandler) UpdateWorkflow(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var workflow model.ApprovalWorkflow
if err := h.db.First(&workflow, id).Error; err != nil {
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "流程不存在"})
return
}
var req model.UpdateWorkflowRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "参数错误: " + err.Error()})
return
}
tx := h.db.Begin()
// 更新基本字段
updates := map[string]interface{}{}
if req.Name != nil {
updates["name"] = *req.Name
}
if req.Description != nil {
updates["description"] = *req.Description
}
if req.Category != nil {
updates["category"] = *req.Category
}
if req.IsActive != nil {
updates["is_active"] = *req.IsActive
}
if req.IsDefault != nil {
// 设为默认时,先取消同 category 其他默认
if *req.IsDefault {
tx.Model(&model.ApprovalWorkflow{}).
Where("category = ? AND is_default = ?", workflow.Category, true).
Update("is_default", false)
}
updates["is_default"] = *req.IsDefault
}
if len(updates) > 0 {
if err := tx.Model(&workflow).Updates(updates).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "更新失败"})
return
}
}
// 更新步骤(先删后建)
if req.Steps != nil {
tx.Where("workflow_id = ?", workflow.ID).Delete(&model.ApprovalWorkflowStep{})
for _, s := range req.Steps {
step := model.ApprovalWorkflowStep{
WorkflowID: workflow.ID,
StepNumber: s.StepNumber,
Name: s.Name,
StepType: s.StepType,
ApproverIDs: s.ApproverIDs,
IsRequired: s.IsRequired,
TimeoutHours: s.TimeoutHours,
AutoApprove: s.AutoApproveTimeout,
}
if step.TimeoutHours == 0 {
step.TimeoutHours = 24
}
if err := tx.Create(&step).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "更新步骤失败"})
return
}
}
}
tx.Commit()
// Reload
h.db.First(&workflow, id)
var steps []model.ApprovalWorkflowStep
h.db.Where("workflow_id = ?", workflow.ID).Order("step_number ASC").Find(&steps)
workflow.Steps = steps
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "更新成功", Data: workflow})
}
// DeleteWorkflow 删除审批流程
func (h *WorkflowHandler) DeleteWorkflow(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
tx := h.db.Begin()
tx.Where("workflow_id = ?", id).Delete(&model.ApprovalWorkflowStep{})
if err := tx.Delete(&model.ApprovalWorkflow{}, id).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "删除失败"})
return
}
tx.Commit()
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "删除成功"})
}
// SetDefaultWorkflow 设为默认流程
func (h *WorkflowHandler) SetDefaultWorkflow(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var workflow model.ApprovalWorkflow
if err := h.db.First(&workflow, id).Error; err != nil {
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "流程不存在"})
return
}
tx := h.db.Begin()
// 取消同 category 的其他默认
tx.Model(&model.ApprovalWorkflow{}).
Where("category = ? AND is_default = ?", workflow.Category, true).
Update("is_default", false)
// 设置当前为默认
tx.Model(&workflow).Update("is_default", true)
tx.Commit()
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "已设为默认"})
}
// GetWorkflowSteps 获取流程步骤(用于申请页加载)
func (h *WorkflowHandler) GetWorkflowSteps(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var workflow model.ApprovalWorkflow
if err := h.db.First(&workflow, id).Error; err != nil {
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "流程不存在"})
return
}
var steps []model.ApprovalWorkflowStep
h.db.Where("workflow_id = ?", workflow.ID).Order("step_number ASC").Find(&steps)
if steps == nil {
steps = []model.ApprovalWorkflowStep{}
}
// Parse approver_ids from JSON string to arrays for the frontend
type stepResp struct {
ID uint `json:"id"`
StepNumber int `json:"step_number"`
Name string `json:"name"`
StepType string `json:"step_type"`
ApproverIDs []int `json:"approver_ids"`
IsRequired bool `json:"is_required"`
TimeoutHours int `json:"timeout_hours"`
AutoApproveTimeout bool `json:"auto_approve_on_timeout"`
}
result := make([]stepResp, 0, len(steps))
for _, s := range steps {
var ids []int
if s.ApproverIDs != "" {
json.Unmarshal([]byte(s.ApproverIDs), &ids)
}
if ids == nil {
ids = []int{}
}
result = append(result, stepResp{
ID: s.ID,
StepNumber: s.StepNumber,
Name: s.Name,
StepType: s.StepType,
ApproverIDs: ids,
IsRequired: s.IsRequired,
TimeoutHours: s.TimeoutHours,
AutoApproveTimeout: s.AutoApprove,
})
}
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "success", Data: result})
}
+6
View File
@@ -143,6 +143,12 @@ func (h *UserHandler) DeleteRole(c *gin.Context) {
return
}
// 系统默认角色禁止删除
if role.IsSystem {
c.JSON(http.StatusForbidden, model.Response{Code: 403, Message: "系统默认角色不可删除"})
return
}
// 清空权限关联后再删除(避免 join 表残留)
h.db.Model(&role).Association("Permissions").Clear()
if err := h.db.Delete(&role).Error; err != nil {
+74 -4
View File
@@ -219,13 +219,17 @@ func (h *UserHandler) Login(c *gin.Context) {
h.recordLoginLog(c, user.Username, "success", "", "password")
// 聚合用户权限码(前端 RBAC 按钮级控制)
permissions := collectUserPermissionCodes(h.db, user.ID, user.IsAdmin)
c.JSON(http.StatusOK, model.Response{
Code: 200,
Message: "登录成功",
Data: model.LoginResponse{
Token: token,
ExpiresIn: h.cfg.JWT.ExpireHour * 3600,
User: &user,
Data: gin.H{
"token": token,
"expires_in": h.cfg.JWT.ExpireHour * 3600,
"user": &user,
"permissions": permissions,
},
})
}
@@ -321,6 +325,72 @@ func (h *UserHandler) GetProfile(c *gin.Context) {
})
}
// GetMyPermissions 获取当前用户的权限码列表(前端 RBAC 按钮级控制使用)
// GET /api/v1/user/permissions
// 返回扁平化的权限 code 数组(聚合用户所有角色的权限)。is_admin=true 返回 ["*"] 通配符。
func (h *UserHandler) GetMyPermissions(c *gin.Context) {
userID := middleware.GetUserID(c)
isAdmin := middleware.GetIsAdmin(c)
// 管理员返回通配符
if isAdmin {
c.JSON(http.StatusOK, model.Response{
Code: 200,
Message: "success",
Data: map[string]any{
"permissions": []string{"*"},
"is_admin": true,
},
})
return
}
codes := collectUserPermissionCodes(h.db, userID, false)
c.JSON(http.StatusOK, model.Response{
Code: 200,
Message: "success",
Data: map[string]any{
"permissions": codes,
"is_admin": false,
},
})
}
// collectUserPermissionCodes 聚合用户的所有权限 code(用于 login 响应附加)
// 容错设计:用 raw SQL 避免 GORM many2many 关联表名不一致问题
func collectUserPermissionCodes(db *gorm.DB, userID uint, isAdmin bool) []string {
if isAdmin {
return []string{"*"}
}
// raw SQL: user → iam_user_roles → iam_roles → role_permissions → iam_permissions
// 逐层 JOIN,任一层缺失自然返回空
type permRow struct {
Code string
}
var rows []permRow
err := db.Raw(`
SELECT DISTINCT p.code
FROM iam_user_roles ur
JOIN iam_roles r ON r.id = ur.role_id AND r.status = 1
JOIN role_permissions rp ON rp.role_id = r.id
JOIN iam_permissions p ON p.id = rp.permission_id AND p.status = 1
WHERE ur.user_id = ?
`, userID).Scan(&rows).Error
if err != nil || len(rows) == 0 {
return []string{}
}
permSet := make(map[string]struct{}, len(rows))
for _, row := range rows {
permSet[row.Code] = struct{}{}
}
codes := make([]string, 0, len(permSet))
for code := range permSet {
codes = append(codes, code)
}
return codes
}
// UpdateProfile 更新当前用户信息
// @Summary 更新当前用户信息
// @Description 更新当前登录用户的信息
+1
View File
@@ -57,6 +57,7 @@ type ApprovalStep struct {
ID uint `gorm:"primarykey" json:"id"`
ProcessID uint `json:"process_id"`
Step int `json:"step"`
StepType string `gorm:"size:32;default:audit" json:"step_type"` // audit, execute
Approver uint `json:"approver"`
Status string `gorm:"size:32;default:pending" json:"status"` // pending, approved, rejected
Comment string `gorm:"size:500" json:"comment"`
+68
View File
@@ -0,0 +1,68 @@
package model
import "time"
// ApprovalWorkflow 审批流程定义
type ApprovalWorkflow struct {
ID uint `gorm:"primarykey" json:"id"`
Name string `gorm:"size:128;not null" json:"name"`
Description string `gorm:"size:500" json:"description"`
Category string `gorm:"size:32;default:resource" json:"category"` // resource, app, permission
IsDefault bool `gorm:"default:false" json:"is_default"`
IsActive bool `gorm:"default:true" json:"is_active"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Steps []ApprovalWorkflowStep `gorm:"foreignKey:WorkflowID" json:"steps,omitempty"`
}
func (ApprovalWorkflow) TableName() string {
return "sys_approval_workflows"
}
// ApprovalWorkflowStep 审批流程步骤
type ApprovalWorkflowStep struct {
ID uint `gorm:"primarykey" json:"id"`
WorkflowID uint `json:"workflow_id"`
StepNumber int `gorm:"not null" json:"step_number"`
Name string `gorm:"size:128;not null" json:"name"`
StepType string `gorm:"size:32;not null" json:"step_type"` // submit, audit, execute
ApproverIDs string `gorm:"type:text" json:"approver_ids"` // JSON array of user IDs
IsRequired bool `gorm:"default:true" json:"is_required"`
TimeoutHours int `gorm:"default:24" json:"timeout_hours"`
AutoApprove bool `gorm:"default:false" json:"auto_approve_on_timeout"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (ApprovalWorkflowStep) TableName() string {
return "sys_approval_workflow_steps"
}
// CreateWorkflowRequest 创建审批流程请求
type CreateWorkflowRequest struct {
Name string `json:"name" binding:"required"`
Description string `json:"description"`
Category string `json:"category"`
Steps []CreateWorkflowStepReq `json:"steps"`
}
type CreateWorkflowStepReq struct {
StepNumber int `json:"step_number"`
Name string `json:"name" binding:"required"`
StepType string `json:"step_type" binding:"required,oneof=submit audit execute"`
ApproverIDs string `json:"approver_ids"`
IsRequired bool `json:"is_required"`
TimeoutHours int `json:"timeout_hours"`
AutoApproveTimeout bool `json:"auto_approve_on_timeout"`
}
// UpdateWorkflowRequest 更新审批流程请求
type UpdateWorkflowRequest struct {
Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"`
Category *string `json:"category,omitempty"`
IsActive *bool `json:"is_active,omitempty"`
IsDefault *bool `json:"is_default,omitempty"`
Steps []CreateWorkflowStepReq `json:"steps,omitempty"`
}
+2 -1
View File
@@ -22,7 +22,7 @@ type User struct {
UpdatedAt time.Time `json:"updated_at"`
// 关联
Roles []Role `gorm:"many2many:user_roles;" json:"roles,omitempty"`
Roles []Role `gorm:"many2many:iam_user_roles;" json:"roles,omitempty"`
}
// TableName 表名
@@ -37,6 +37,7 @@ type Role struct {
Code string `gorm:"size:64;uniqueIndex;not null" json:"code"`
Description string `gorm:"size:255" json:"description"`
Status int `gorm:"default:1" json:"status"`
IsSystem bool `gorm:"default:false" json:"is_system"` // 系统默认角色,禁止删除
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
+2
View File
@@ -134,6 +134,8 @@ func AutoMigrate(db *gorm.DB) error {
&model.AlertUpgradeRecord{}, // 告警升级记录 // Logan.Li
&model.AlertSilence{}, // 告警静默 // Logan.Li
&model.HealthProbe{}, // 健康探针配置 // Logan.Li
&model.ApprovalWorkflow{}, // 审批流程定义 // Logan.Li
&model.ApprovalWorkflowStep{}, // 审批流程步骤 // Logan.Li
}
var lastErr error
+14
View File
@@ -143,6 +143,7 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
user := authorized.Group("/user")
{
user.GET("/profile", authHandler.GetProfile)
user.GET("/permissions", authHandler.GetMyPermissions)
user.PUT("/profile", authHandler.UpdateProfile)
user.POST("/change-password", authHandler.ChangePassword)
user.GET("/roles", authHandler.ListRoles)
@@ -680,6 +681,19 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
approvals.POST("/processes/:id/reject", approvalHandler.RejectProcess)
}
// 审批流程配置(工作流定义)
workflowHandler := controller.NewWorkflowHandler(db)
workflows := authorized.Group("/approval-workflows")
{
workflows.GET("", workflowHandler.ListWorkflows)
workflows.POST("", workflowHandler.CreateWorkflow)
workflows.GET("/:id", workflowHandler.GetWorkflow)
workflows.PUT("/:id", workflowHandler.UpdateWorkflow)
workflows.DELETE("/:id", workflowHandler.DeleteWorkflow)
workflows.PUT("/:id/default", workflowHandler.SetDefaultWorkflow)
workflows.GET("/:id/steps", workflowHandler.GetWorkflowSteps)
}
// 导航路由
navHandler := controller.NewNavigationHandler(db)
navigations := authorized.Group("/navigations")
+78 -23
View File
@@ -82,7 +82,7 @@ func SeedData(db *gorm.DB) {
{"key": "purpose", "label": "用途说明", "type": "textarea", "required": true}
]
}`,
FlowConfig: `[{"name":"直接主管","approver_role":"team_lead"},{"name":"运维负责人","approver_role":"ops_manager"}]`,
FlowConfig: `[{"name":"直接主管","approver_role":"team_lead","step_type":"audit"},{"name":"运维负责人","approver_role":"ops_manager","step_type":"audit"},{"name":"运维执行","approver_role":"ops_manager","step_type":"execute"}]`,
Enabled: true,
},
{
@@ -106,7 +106,7 @@ func SeedData(db *gorm.DB) {
{"key": "remark", "label": "备注", "type": "textarea", "required": false}
]
}`,
FlowConfig: `[{"name":"技术负责人","approver_role":"tech_lead"},{"name":"运维负责人","approver_role":"ops_manager"}]`,
FlowConfig: `[{"name":"技术负责人","approver_role":"tech_lead","step_type":"audit"},{"name":"运维负责人","approver_role":"ops_manager","step_type":"audit"},{"name":"部署执行","approver_role":"ops_manager","step_type":"execute"}]`,
Enabled: true,
},
{
@@ -125,7 +125,7 @@ func SeedData(db *gorm.DB) {
{"key": "dept", "label": "所属部门", "type": "text", "placeholder": "如 基础架构部", "required": true}
]
}`,
FlowConfig: `[{"name":"直接主管","approver_role":"team_lead"},{"name":"安全负责人","approver_role":"security_lead"}]`,
FlowConfig: `[{"name":"直接主管","approver_role":"team_lead","step_type":"audit"},{"name":"安全负责人","approver_role":"security_lead","step_type":"audit"},{"name":"权限开通","approver_role":"ops_manager","step_type":"execute"}]`,
Enabled: true,
},
}
@@ -182,6 +182,11 @@ func SeedData(db *gorm.DB) {
{Name: "LDAP配置", Code: "system:ldap", Type: "menu", Sort: 83, Status: 1},
// 仪表盘
{Name: "仪表盘", Code: "dashboard:view", Type: "menu", Sort: 90, Status: 1},
// 安全审计
{Name: "终端审计", Code: "audit:terminal", Type: "menu", Sort: 91, Status: 1},
{Name: "操作审计", Code: "audit:operations", Type: "menu", Sort: 92, Status: 1},
{Name: "登录日志", Code: "audit:login-logs", Type: "menu", Sort: 93, Status: 1},
{Name: "会话阻断", Code: "audit:kill-session", Type: "button", Sort: 94, Status: 1},
}
db.Create(&perms)
zap.L().Info("seeded default permissions")
@@ -192,13 +197,16 @@ func SeedData(db *gorm.DB) {
db.Model(&model.Role{}).Count(&roleCount)
if roleCount == 0 {
roles := []model.Role{
{Name: "管理员", Code: "admin", Description: "系统管理员,拥有全部权限", Status: 1},
{Name: "运维工程师", Code: "ops", Description: "日常运维管理", Status: 1},
{Name: "开发工程师", Code: "developer", Description: "开发人员,只读权限", Status: 1},
{Name: "只读用户", Code: "viewer", Description: "仅查看权限", Status: 1},
{Name: "超级管理员", Code: "super_admin", Description: "系统最高权限,管理所有资源和配置", Status: 1, IsSystem: true},
{Name: "Kubernetes管理员", Code: "k8s_admin", Description: "管理K8s集群、节点、工作负载和存储", Status: 1, IsSystem: true},
{Name: "资源管理员", Code: "resource_admin", Description: "管理服务器、云账号、域名和云资源", Status: 1, IsSystem: true},
{Name: "监控管理员", Code: "monitor_admin", Description: "管理告警、监控指标和采集Agent", Status: 1, IsSystem: true},
{Name: "安全审计员", Code: "security_auditor", Description: "审计终端会话、操作日志和登录记录", Status: 1, IsSystem: true},
{Name: "发布管理员", Code: "release_admin", Description: "管理流水线、应用发布和审批流程", Status: 1, IsSystem: true},
{Name: "只读用户", Code: "viewer", Description: "仅查看仪表盘和资源概览,无操作权限", Status: 1, IsSystem: true},
}
db.Create(&roles)
zap.L().Info("seeded default roles")
zap.L().Info("seeded 7 system default roles")
// 为各角色分配权限
var allPerms []model.Permission
@@ -206,37 +214,71 @@ func SeedData(db *gorm.DB) {
for _, role := range roles {
var permIds []uint
switch role.Code {
case "admin":
case "super_admin":
for _, p := range allPerms {
permIds = append(permIds, p.ID)
}
case "ops":
case "k8s_admin":
k8sPrefixes := []string{"k8s:", "server:webshell", "dashboard:view", "deploy:"}
for _, p := range allPerms {
if !strings.HasPrefix(p.Code, "system:") {
permIds = append(permIds, p.ID)
for _, pre := range k8sPrefixes {
if strings.HasPrefix(p.Code, pre) {
permIds = append(permIds, p.ID)
break
}
}
}
case "developer":
devPerms := map[string]bool{
"dashboard:view": true, "server:view": true, "cloud:account": true,
"cloud:domain": true, "cloud:billing": true,
"k8s:cluster": true, "k8s:nodes": true, "k8s:workloads": true, "k8s:topology": true,
"monitor:alert": true, "deploy:pipeline": true, "deploy:release": true,
"approval:apply": true, "aiops:assistant": true, "aiops:analysis": true, "aiops:forecast": true,
}
case "resource_admin":
resPrefixes := []string{"server:", "cloud:", "dashboard:view", "k8s:topology"}
for _, p := range allPerms {
if devPerms[p.Code] {
permIds = append(permIds, p.ID)
for _, pre := range resPrefixes {
if strings.HasPrefix(p.Code, pre) {
permIds = append(permIds, p.ID)
break
}
}
}
case "monitor_admin":
monPrefixes := []string{"monitor:", "agent:", "dashboard:view", "server:view"}
for _, p := range allPerms {
for _, pre := range monPrefixes {
if strings.HasPrefix(p.Code, pre) {
permIds = append(permIds, p.ID)
break
}
}
}
case "security_auditor":
auditPrefixes := []string{"audit:", "dashboard:view", "server:view", "system:audit"}
for _, p := range allPerms {
for _, pre := range auditPrefixes {
if strings.HasPrefix(p.Code, pre) {
permIds = append(permIds, p.ID)
break
}
}
}
case "release_admin":
relPrefixes := []string{"deploy:", "approval:", "dashboard:view", "server:view", "k8s:cluster", "k8s:workloads"}
for _, p := range allPerms {
for _, pre := range relPrefixes {
if strings.HasPrefix(p.Code, pre) {
permIds = append(permIds, p.ID)
break
}
}
}
case "viewer":
for _, p := range allPerms {
if p.Code == "dashboard:view" || p.Code == "server:view" || p.Code == "monitor:alert" {
if p.Code == "dashboard:view" || p.Code == "server:view" || p.Code == "monitor:alert" ||
p.Code == "k8s:cluster" || p.Code == "k8s:topology" || p.Code == "cloud:billing" ||
p.Code == "deploy:pipeline" {
permIds = append(permIds, p.ID)
}
}
}
if len(permIds) > 0 {
permIds = dedupeUint(permIds)
var rolePerms []model.Permission
db.Where("id IN ?", permIds).Find(&rolePerms)
db.Model(&role).Association("Permissions").Append(rolePerms)
@@ -650,3 +692,16 @@ func SeedData(db *gorm.DB) {
ti := toolinstaller.New(db)
ti.SeedTools()
}
// dedupeUint 去重 uint 切片
func dedupeUint(slice []uint) []uint {
seen := make(map[uint]bool)
result := make([]uint, 0, len(slice))
for _, v := range slice {
if !seen[v] {
seen[v] = true
result = append(result, v)
}
}
return result
}