后端: - 新增 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 零错误
346 lines
8.9 KiB
Go
346 lines
8.9 KiB
Go
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").
|
||
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)
|
||
}
|
||
|
||
var total int64
|
||
db.Count(&total)
|
||
|
||
var processes []model.ApprovalProcess
|
||
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,
|
||
Message: "success",
|
||
Data: model.ListResponse{
|
||
List: processes,
|
||
Total: total,
|
||
},
|
||
})
|
||
}
|
||
|
||
// GetProcess 获取审批流程详情 // Logan.Li
|
||
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").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: "流程不存在",
|
||
})
|
||
return
|
||
}
|
||
|
||
c.JSON(http.StatusOK, model.Response{
|
||
Code: 200,
|
||
Message: "success",
|
||
Data: process,
|
||
})
|
||
}
|
||
|
||
// CreateProcess 创建审批流程 — 使用审批流程配置 (sys_approval_workflows) // Logan.Li
|
||
func (h *ApprovalHandler) CreateProcess(c *gin.Context) {
|
||
var req model.CreateApprovalProcessRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
c.JSON(http.StatusBadRequest, model.Response{
|
||
Code: 400,
|
||
Message: "请求参数错误: " + err.Error(),
|
||
})
|
||
return
|
||
}
|
||
|
||
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: "模板不存在"})
|
||
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: userID,
|
||
FormData: req.FormData,
|
||
Status: "pending",
|
||
CurrentStep: 1,
|
||
TotalSteps: totalSteps,
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
// 创建审批步骤(仅 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: "申请已提交",
|
||
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:
|
||
return uint(val)
|
||
case uint:
|
||
return val
|
||
case int:
|
||
return uint(val)
|
||
default:
|
||
return 0
|
||
}
|
||
}
|
||
|
||
// ApproveProcess 审批通过 // Logan.Li
|
||
func (h *ApprovalHandler) ApproveProcess(c *gin.Context) {
|
||
id, _ := strconv.Atoi(c.Param("id"))
|
||
|
||
var req model.ApproveRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
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: "流程不存在"})
|
||
return
|
||
}
|
||
|
||
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: "审批通过",
|
||
Data: process,
|
||
})
|
||
}
|
||
|
||
// RejectProcess 审批拒绝 // Logan.Li
|
||
func (h *ApprovalHandler) RejectProcess(c *gin.Context) {
|
||
id, _ := strconv.Atoi(c.Param("id"))
|
||
|
||
var req model.ApproveRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
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: "流程不存在"})
|
||
return
|
||
}
|
||
|
||
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: "已拒绝",
|
||
Data: process,
|
||
})
|
||
}
|