feat: 工单执行人直接执行(自动通过) + 审批时间线 + 执行结果展示
后端: - 创建工单时检测是否有审核步骤,无审核步骤直接设为 reviewing - ExecuteTicket 允许 reviewing 状态,自动通过 execute 审批记录再执行 - 验证执行人权限(非执行人返回 403) - 列表 API 返回 has_audit_step/is_executor/is_approver 元信息 - 详情 API 返回 approvals 带 approver_name/actual_approver_name 前端: - 按钮权限精确化:通过/拒绝只对审核人显示,执行只对执行人显示 - 详情弹框加载完整审批数据,展示操作时间线 - 时间线:提交人→每个步骤的指派人+实际操作人+操作时间+备注 - 执行结果文本格式 fallback 显示 - 失败工单显示失败原因
This commit is contained in:
1 parent
cdb52aa4a9
commit
b4c982d565
2 files changed
+262
-16
No files matched your search
@@ -101,9 +101,34 @@ func ListTickets(c *gin.Context) {
|
||||
|
||||
var tickets []model.Ticket
|
||||
query.Offset((page - 1) * limit).Limit(limit).Find(&tickets)
|
||||
|
||||
|
||||
// Enrich tickets with has_audit_step info
|
||||
type TicketWithMeta struct {
|
||||
model.Ticket
|
||||
HasAuditStep bool `json:"has_audit_step"`
|
||||
HasExecuteStep bool `json:"has_execute_step"`
|
||||
IsExecutor bool `json:"is_executor"`
|
||||
IsApprover bool `json:"is_approver"`
|
||||
}
|
||||
var result []TicketWithMeta
|
||||
for _, t := range tickets {
|
||||
var auditCount, execCount int64
|
||||
model.DB.Model(&model.TicketApproval{}).Where("ticket_id = ? AND step_type = 'audit'", t.ID).Count(&auditCount)
|
||||
model.DB.Model(&model.TicketApproval{}).Where("ticket_id = ? AND step_type = 'execute'", t.ID).Count(&execCount)
|
||||
var isExec, isAppr int64
|
||||
model.DB.Model(&model.TicketApproval{}).Where("ticket_id = ? AND step_type = 'execute' AND approver_id = ?", t.ID, userID).Count(&isExec)
|
||||
model.DB.Model(&model.TicketApproval{}).Where("ticket_id = ? AND step_type != 'execute' AND step_type != 'submit' AND approver_id = ?", t.ID, userID).Count(&isAppr)
|
||||
result = append(result, TicketWithMeta{
|
||||
Ticket: t,
|
||||
HasAuditStep: auditCount > 0,
|
||||
HasExecuteStep: execCount > 0,
|
||||
IsExecutor: isExec > 0,
|
||||
IsApprover: isAppr > 0,
|
||||
})
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"items": tickets,
|
||||
"items": result,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": limit,
|
||||
@@ -119,9 +144,31 @@ func GetTicket(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Get approvals
|
||||
// Get approvals — enrich with user display names
|
||||
var approvals []model.TicketApproval
|
||||
model.DB.Where("ticket_id = ?", id).Order("step_number").Find(&approvals)
|
||||
model.DB.Where("ticket_id = ?", id).Order("step_number, id").Find(&approvals)
|
||||
type ApprovalWithNames struct {
|
||||
model.TicketApproval
|
||||
ApproverName string `json:"approver_name"`
|
||||
ActualApproverName string `json:"actual_approver_name"`
|
||||
}
|
||||
var approvalsResult []ApprovalWithNames
|
||||
for _, a := range approvals {
|
||||
item := ApprovalWithNames{TicketApproval: a}
|
||||
if a.ApproverID != nil {
|
||||
var u model.User
|
||||
if model.DB.First(&u, *a.ApproverID).Error == nil {
|
||||
item.ApproverName = u.DisplayName
|
||||
}
|
||||
}
|
||||
if a.ActualApproverID != nil {
|
||||
var u model.User
|
||||
if model.DB.First(&u, *a.ActualApproverID).Error == nil {
|
||||
item.ActualApproverName = u.DisplayName
|
||||
}
|
||||
}
|
||||
approvalsResult = append(approvalsResult, item)
|
||||
}
|
||||
|
||||
// Get SQL items
|
||||
var sqlItems []model.TicketSQLItem
|
||||
@@ -132,7 +179,7 @@ func GetTicket(c *gin.Context) {
|
||||
// Marshal ticket to JSON then unmarshal to map for flattening
|
||||
ticketBytes, _ := json.Marshal(ticket)
|
||||
json.Unmarshal(ticketBytes, &result)
|
||||
result["approvals"] = approvals
|
||||
result["approvals"] = approvalsResult
|
||||
result["sql_items"] = sqlItems
|
||||
|
||||
// Add applicant_name and instance_name for frontend
|
||||
@@ -213,6 +260,7 @@ func CreateTicket(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Create approval records based on workflow or default to admin
|
||||
hasAuditStep := false
|
||||
if input.WorkflowID != nil && *input.WorkflowID > 0 {
|
||||
// Load workflow steps
|
||||
var wfSteps []model.WorkflowStep
|
||||
@@ -221,6 +269,9 @@ func CreateTicket(c *gin.Context) {
|
||||
if ws.StepType == "submit" {
|
||||
continue // submit step needs no approver
|
||||
}
|
||||
if ws.StepType != "execute" {
|
||||
hasAuditStep = true
|
||||
}
|
||||
// Parse approver_ids from JSON
|
||||
var approverIDs []int
|
||||
if ws.ApproverIDs != nil && *ws.ApproverIDs != "" {
|
||||
@@ -283,7 +334,14 @@ func CreateTicket(c *gin.Context) {
|
||||
model.DB.Create(&approval)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// If workflow has no audit step (only submit + execute), skip to reviewing
|
||||
if !hasAuditStep {
|
||||
reviewing := "reviewing"
|
||||
model.DB.Model(&ticket).Update("status", reviewing)
|
||||
ticket.Status = &reviewing
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, ticket)
|
||||
}
|
||||
|
||||
@@ -369,11 +427,57 @@ func ExecuteTicket(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check ticket status
|
||||
if ticket.Status != nil && *ticket.Status != "approved" {
|
||||
// Check ticket status — allow approved OR reviewing (will auto-approve execute step)
|
||||
if ticket.Status != nil && *ticket.Status != "approved" && *ticket.Status != "reviewing" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "Ticket must be approved before execution"})
|
||||
return
|
||||
}
|
||||
|
||||
// Auto-approve any pending execute-type approval for the current user
|
||||
userID, _, _ := middleware.GetCurrentUser(c)
|
||||
|
||||
// Verify current user is an executor for this ticket
|
||||
var executorCheck int64
|
||||
model.DB.Model(&model.TicketApproval{}).
|
||||
Where("ticket_id = ? AND step_type = 'execute' AND (approver_id = ? OR approver_id IS NULL)", id, userID).
|
||||
Count(&executorCheck)
|
||||
if executorCheck == 0 {
|
||||
c.JSON(http.StatusForbidden, gin.H{"detail": "你不是此工单的执行人"})
|
||||
return
|
||||
}
|
||||
|
||||
var pendingExec model.TicketApproval
|
||||
if err := model.DB.Where("ticket_id = ? AND step_type = 'execute' AND action IS NULL", id).First(&pendingExec).Error; err == nil {
|
||||
// Auto-approve the execute step
|
||||
now := time.Now()
|
||||
action := "approve"
|
||||
comment := "自动通过(执行触发)"
|
||||
pendingExec.Action = &action
|
||||
pendingExec.Comment = &comment
|
||||
pendingExec.ActualApproverID = &userID
|
||||
pendingExec.CompletedAt = &now
|
||||
model.DB.Save(&pendingExec)
|
||||
}
|
||||
|
||||
// Check if all audit steps are approved
|
||||
var pendingAudit int64
|
||||
model.DB.Model(&model.TicketApproval{}).
|
||||
Where("ticket_id = ? AND step_type = 'audit' AND action IS NULL", id).
|
||||
Count(&pendingAudit)
|
||||
if pendingAudit > 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "还有未完成的审核步骤"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check for rejected audit steps
|
||||
var rejectedAudit int64
|
||||
model.DB.Model(&model.TicketApproval{}).
|
||||
Where("ticket_id = ? AND step_type = 'audit' AND action = 'reject'", id).
|
||||
Count(&rejectedAudit)
|
||||
if rejectedAudit > 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "审核已被拒绝,无法执行"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get instance info
|
||||
var instance model.Instance
|
||||
|
||||
Reference in new issue
Block a user