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:
1 parent
77c234b060
commit
be2a377f01
30 files changed
+2328
-462
No files matched your search
@@ -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(¤tStep).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(¤tStep).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: "已拒绝",
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 更新当前登录用户的信息
|
||||
|
||||
Reference in new issue
Block a user