// 工作流模块 HTTP Handler package workflow import ( "encoding/json" "fmt" "strconv" "strings" "time" "com.sclktx/m/v2/internal/common/model" "com.sclktx/m/v2/internal/common/response" "com.sclktx/m/v2/internal/common/utils" "com.sclktx/m/v2/internal/pkg/dingtalk" "com.sclktx/m/v2/internal/pkg/wechat/templates" "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" "gorm.io/gorm" ) // Handler 工作流处理器 type Handler struct { db *gorm.DB engine *Engine wxSend func(openid, templateID string, data map[string]string, page string) error dingTalk *dingtalk.Client } // NewHandler 创建工作流处理器 func NewHandler(db *gorm.DB, wxSend func(openid, templateID string, data map[string]string, page string) error, dingTalk *dingtalk.Client) *Handler { return &Handler{ db: db, engine: NewEngine(db), wxSend: wxSend, dingTalk: dingTalk, } } // ==================== 请求/响应结构 ==================== // CreateDefinitionReq 创建流程定义请求 type CreateDefinitionReq struct { ProcessName string `json:"process_name" binding:"required"` ProcessCode string `json:"process_code" binding:"required"` Source string `json:"source"` Description string `json:"description"` RevokeEvents string `json:"revoke_events"` NodesJSON string `json:"nodes_json"` } // UpdateDefinitionReq 更新流程定义请求 type UpdateDefinitionReq struct { ProcessName string `json:"process_name"` Source string `json:"source"` Description string `json:"description"` IsActive *bool `json:"is_active"` RevokeEvents string `json:"revoke_events"` NodesJSON string `json:"nodes_json"` } // StartInstanceReq 启动流程实例请求 type StartInstanceReq struct { ProcessCode string `json:"process_code" binding:"required"` BusinessType string `json:"business_type" binding:"required"` BusinessID uint `json:"business_id" binding:"required"` Variables map[string]interface{} `json:"variables"` } // TaskActionReq 任务操作请求 type TaskActionReq struct { Comment string `json:"comment"` } // ==================== 路由注册 ==================== // RegisterRoutes 注册工作流路由(需要 JWT 认证) func (h *Handler) RegisterRoutes(router *gin.RouterGroup) { // 流程定义(管理员操作) router.GET("/workflow/definitions", h.ListDefinitions) router.GET("/workflow/definitions/:id", h.GetDefinition) router.POST("/workflow/definitions", h.CreateDefinition) router.PUT("/workflow/definitions/:id", h.UpdateDefinition) router.DELETE("/workflow/definitions/:id", h.DeleteDefinition) // 流程实例 router.POST("/workflow/instances", h.StartInstance) router.GET("/workflow/instances", h.ListInstances) router.GET("/workflow/instances/:id", h.GetInstanceDetail) // 任务 router.GET("/workflow/tasks/pending", h.GetPendingTasks) router.POST("/workflow/tasks/:id/approve", h.ApproveTask) router.POST("/workflow/tasks/:id/reject", h.RejectTask) } // ==================== 流程定义管理 ==================== // ListDefinitions 获取流程定义列表 // @Summary 获取流程定义列表 // @Description 获取所有流程定义列表 // @Tags 工作流 // @Accept json // @Produce json // @Security BearerAuth // @Param page query int false "页码" default(1) // @Param page_size query int false "每页条数" default(10) // @Success 200 {object} response.Response "success" // @Router /api/v1/workflow/definitions [get] func (h *Handler) ListDefinitions(c *gin.Context) { page, pageSize := utils.ParsePagination(c) var defs []model.ProcessDefinition var total int64 query := h.db.Model(&model.ProcessDefinition{}) query.Count(&total) query.Offset(utils.GetOffset(page, pageSize)). Limit(pageSize). Order("created_at DESC"). Find(&defs) response.SuccessPage(c, defs, total, page, pageSize) } // GetDefinition 获取流程定义详情 // @Summary 获取流程定义详情 // @Description 根据ID获取流程定义详情 // @Tags 工作流 // @Accept json // @Produce json // @Security BearerAuth // @Param id path uint true "流程定义ID" // @Success 200 {object} response.Response "success" // @Router /api/v1/workflow/definitions/{id} [get] func (h *Handler) GetDefinition(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { response.BadRequest(c, "参数错误") return } var def model.ProcessDefinition if err := h.db.First(&def, id).Error; err != nil { response.NotFound(c, "流程定义不存在") return } response.Success(c, def) } // CreateDefinition 创建流程定义 // @Summary 创建流程定义 // @Description 创建一个新的流程定义(管理员操作) // @Tags 工作流 // @Accept json // @Produce json // @Security BearerAuth // @Param body body CreateDefinitionReq true "创建流程定义请求" // @Success 200 {object} response.Response "success" // @Router /api/v1/workflow/definitions [post] func (h *Handler) CreateDefinition(c *gin.Context) { var req CreateDefinitionReq if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "参数错误") return } // 检查编码唯一性 var existCount int64 h.db.Model(&model.ProcessDefinition{}).Where("process_code = ?", req.ProcessCode).Count(&existCount) if existCount > 0 { response.BadRequest(c, "流程编码已存在") return } def := model.ProcessDefinition{ ProcessName: req.ProcessName, ProcessCode: req.ProcessCode, Source: req.Source, Description: req.Description, IsActive: true, RevokeEvents: req.RevokeEvents, NodesJSON: req.NodesJSON, } if err := h.db.Create(&def).Error; err != nil { response.InternalError(c, "创建失败") return } response.SuccessWithMessage(c, "创建成功", def) } // UpdateDefinition 更新流程定义 // @Summary 更新流程定义 // @Description 更新指定流程定义的信息(管理员操作) // @Tags 工作流 // @Accept json // @Produce json // @Security BearerAuth // @Param id path uint true "流程定义ID" // @Param body body UpdateDefinitionReq true "更新流程定义请求" // @Success 200 {object} response.Response "success" // @Router /api/v1/workflow/definitions/{id} [put] func (h *Handler) UpdateDefinition(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { response.BadRequest(c, "参数错误") return } var def model.ProcessDefinition if err := h.db.First(&def, id).Error; err != nil { response.NotFound(c, "流程定义不存在") return } var req UpdateDefinitionReq if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "参数错误") return } updates := map[string]interface{}{} if req.ProcessName != "" { updates["process_name"] = req.ProcessName } if req.Source != "" { updates["source"] = req.Source } if req.Description != "" { updates["description"] = req.Description } if req.IsActive != nil { updates["is_active"] = *req.IsActive } if req.RevokeEvents != "" { updates["revoke_events"] = req.RevokeEvents } if req.NodesJSON != "" { updates["nodes_json"] = req.NodesJSON } if len(updates) > 0 { if err := h.db.Model(&def).Updates(updates).Error; err != nil { response.InternalError(c, "更新失败") return } } response.SuccessWithMessage(c, "更新成功", nil) } // DeleteDefinition 删除流程定义 // @Summary 删除流程定义 // @Description 删除指定的流程定义(管理员操作,存在运行中的实例时无法删除) // @Tags 工作流 // @Accept json // @Produce json // @Security BearerAuth // @Param id path uint true "流程定义ID" // @Success 200 {object} response.Response "success" // @Router /api/v1/workflow/definitions/{id} [delete] func (h *Handler) DeleteDefinition(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { response.BadRequest(c, "参数错误") return } // 检查是否有运行中的实例 var activeCount int64 h.db.Model(&model.ProcessInstance{}).Where("process_def_id = ? AND status = 0", id).Count(&activeCount) if activeCount > 0 { response.BadRequest(c, "存在运行中的流程实例,无法删除") return } if err := h.db.Delete(&model.ProcessDefinition{}, id).Error; err != nil { response.InternalError(c, "删除失败") return } response.SuccessWithMessage(c, "删除成功", nil) } // ==================== 流程实例管理 ==================== // StartInstance 启动流程实例 // @Summary 启动流程实例 // @Description 根据流程定义启动一个新的流程实例 // @Tags 工作流 // @Accept json // @Produce json // @Security BearerAuth // @Param body body StartInstanceReq true "启动流程实例请求" // @Success 200 {object} response.Response "success" // @Router /api/v1/workflow/instances [post] func (h *Handler) StartInstance(c *gin.Context) { var req StartInstanceReq if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "参数错误") return } // 查找流程定义 var def model.ProcessDefinition if err := h.db.Where("process_code = ?", req.ProcessCode).First(&def).Error; err != nil { response.NotFound(c, "流程定义不存在") return } userID := utils.GetUserID(c) instance, err := h.engine.StartInstance(def.ID, req.BusinessType, req.BusinessID, userID, req.Variables) if err != nil { response.InternalError(c, err.Error()) return } response.SuccessWithMessage(c, "启动成功", instance) } // ListInstances 获取流程实例列表 // @Summary 获取流程实例列表 // @Description 获取所有流程实例列表,可按业务类型和状态筛选 // @Tags 工作流 // @Accept json // @Produce json // @Security BearerAuth // @Param business_type query string false "业务类型" // @Param status query int false "实例状态" // @Param page query int false "页码" default(1) // @Param page_size query int false "每页条数" default(10) // @Success 200 {object} response.Response "success" // @Router /api/v1/workflow/instances [get] func (h *Handler) ListInstances(c *gin.Context) { page, pageSize := utils.ParsePagination(c) businessType := c.Query("business_type") businessIDStr := c.Query("business_id") statusStr := c.Query("status") var instances []model.ProcessInstance var total int64 query := h.db.Model(&model.ProcessInstance{}). Preload("ProcessDef"). Preload("Starter") if businessType != "" { query = query.Where("business_type = ?", businessType) } if businessIDStr != "" { businessID, _ := strconv.ParseUint(businessIDStr, 10, 64) if businessID > 0 { query = query.Where("business_id = ?", businessID) } } if statusStr != "" { status, _ := strconv.Atoi(statusStr) query = query.Where("status = ?", status) } query.Count(&total) query.Offset(utils.GetOffset(page, pageSize)). Limit(pageSize). Order("created_at DESC"). Find(&instances) response.SuccessPage(c, instances, total, page, pageSize) } // GetInstanceDetail 获取流程实例详情(含任务列表) // @Summary 获取流程实例详情 // @Description 获取流程实例详情,包含关联的任务列表 // @Tags 工作流 // @Accept json // @Produce json // @Security BearerAuth // @Param id path uint true "流程实例ID" // @Success 200 {object} response.Response "success" // @Router /api/v1/workflow/instances/{id} [get] func (h *Handler) GetInstanceDetail(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { response.BadRequest(c, "参数错误") return } var instance model.ProcessInstance if err := h.db.Preload("ProcessDef").Preload("Starter").First(&instance, id).Error; err != nil { response.NotFound(c, "流程实例不存在") return } var tasks []model.Task h.db.Where("instance_id = ?", id). Preload("Assignee"). Order("created_at ASC"). Find(&tasks) response.Success(c, gin.H{ "instance": instance, "tasks": tasks, }) } // ==================== 任务管理 ==================== // GetPendingTasks 获取当前用户的待办任务 // @Summary 获取待办任务 // @Description 获取当前用户的待处理流程任务列表 // @Tags 工作流 // @Accept json // @Produce json // @Security BearerAuth // @Success 200 {object} response.Response "success" // @Router /api/v1/workflow/tasks/pending [get] func (h *Handler) GetPendingTasks(c *gin.Context) { userID := utils.GetUserID(c) var tasks []model.Task h.db.Where("assignee_id = ? AND status = 0", userID). Preload("Instance"). Preload("Instance.ProcessDef"). Preload("Assignee"). Order("created_at DESC"). Find(&tasks) response.Success(c, tasks) } // ApproveTask 审批通过任务 // @Summary 审批通过任务 // @Description 审批通过指定的流程任务 // @Tags 工作流 // @Accept json // @Produce json // @Security BearerAuth // @Param id path uint true "任务ID" // @Param body body TaskActionReq true "审批请求" // @Success 200 {object} response.Response "success" // @Router /api/v1/workflow/tasks/{id}/approve [post] func (h *Handler) ApproveTask(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { response.BadRequest(c, "参数错误") return } var req TaskActionReq if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "参数错误") return } userID := utils.GetUserID(c) if err := h.engine.ApproveTask(uint(id), userID, req.Comment); err != nil { response.InternalError(c, err.Error()) return } // 审批完成后发送通知 h.sendAppointmentNotification(uint(id)) h.sendDingTalkAfterApproval(uint(id), req.Comment) response.SuccessWithMessage(c, "审批通过", nil) } // RejectTask 审批拒绝任务 // @Summary 审批拒绝任务 // @Description 拒绝指定的流程任务 // @Tags 工作流 // @Accept json // @Produce json // @Security BearerAuth // @Param id path uint true "任务ID" // @Param body body TaskActionReq true "拒绝请求" // @Success 200 {object} response.Response "success" // @Router /api/v1/workflow/tasks/{id}/reject [post] func (h *Handler) RejectTask(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { response.BadRequest(c, "参数错误") return } var req TaskActionReq if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "参数错误") return } userID := utils.GetUserID(c) if err := h.engine.RejectTask(uint(id), userID, req.Comment); err != nil { response.InternalError(c, err.Error()) return } // 拒绝后发送通知 h.sendAppointmentNotification(uint(id)) h.sendDingTalkAfterRejection(uint(id), req.Comment) response.SuccessWithMessage(c, "已拒绝", nil) } // sendAppointmentNotification 审批结束后发送微信通知给申请人 func (h *Handler) sendAppointmentNotification(taskID uint) { var task model.Task if err := h.db.Preload("Instance").First(&task, taskID).Error; err != nil { return } if task.Instance == nil || task.Instance.BusinessType != "appointment" { return } go func() { var appointment model.Appointment if err := h.db.First(&appointment, task.Instance.BusinessID).Error; err != nil { return } // 只发送已通过或已拒绝的通知 if appointment.Status != 1 && appointment.Status != 2 { return } var creatorUser model.User if err := h.db.First(&creatorUser, appointment.CreatorUserID).Error; err != nil || creatorUser.Openid == "" { return } resultText := "已通过" if appointment.Status == 2 { resultText = "已拒绝" } // 解析 JSON 字段 var employeeInfo map[string]interface{} json.Unmarshal([]byte(appointment.EmployeeInfo), &employeeInfo) var visitorInfo map[string]interface{} json.Unmarshal([]byte(appointment.VisitorInfo), &visitorInfo) company, _ := visitorInfo["company"].(string) employeeName, _ := employeeInfo["name"].(string) var areasInfo []string json.Unmarshal([]byte(appointment.AreasInfo), &areasInfo) areaText := strings.Join(areasInfo, "、") // 格式化时间(微信 thing 类型字段限 20 字) timeStr := "" if !appointment.VisitStartTime.IsZero() { loc, _ := time.LoadLocation("Asia/Shanghai") timeStr = appointment.VisitStartTime.In(loc).Format("01-02 15:04") + "-" + appointment.VisitEndTime.In(loc).Format("15:04") } // TMPL_RESULT: 访客申请结果通知(thing 字段限 20 字) trunc := func(s string) string { runes := []rune(s) if len(runes) > 20 { return string(runes[:20]) } return s } tmplID := templates.TMPL_RESULT if h.wxSend != nil { h.wxSend(creatorUser.Openid, tmplID, map[string]string{ "thing1": trunc(resultText), "thing2": trunc(company), "thing3": trunc(timeStr), "thing8": trunc(employeeName), "thing9": trunc(areaText), }, fmt.Sprintf("/subpackages/appointment/appointment-detail?id=%d", appointment.ID)) } }() } // sendDingTalkAfterApproval 审批通过后发送钉钉消息 func (h *Handler) sendDingTalkAfterApproval(taskID uint, comment string) { if h.dingTalk == nil { return } var task model.Task if err := h.db.Preload("Instance").First(&task, taskID).Error; err != nil { return } if task.Instance == nil || task.Instance.BusinessType != "appointment" || task.Instance.Status != 0 { return } logrus.WithFields(logrus.Fields{ "task_id": taskID, "instance_id": task.InstanceID, }).Info("钉钉:审批通过,开始发送通知") go func() { // 获取审批人信息 var approverUser model.User if task.AssigneeID == nil { return } if err := h.db.First(&approverUser, *task.AssigneeID).Error; err != nil { logrus.WithError(err).WithField("task_id", taskID).Warn("钉钉:查询审批人信息失败") return } // 获取预约信息 var appointment model.Appointment if err := h.db.First(&appointment, task.Instance.BusinessID).Error; err != nil { return } var visitorInfo struct { Name string `json:"name"` Company string `json:"company"` } json.Unmarshal([]byte(appointment.VisitorInfo), &visitorInfo) visitTime := "" if !appointment.VisitStartTime.IsZero() { visitTime = dingtalk.FormatCSTTime(appointment.VisitStartTime, appointment.VisitEndTime) } // 1. 通知被取消的同节点审批人(同一节点非会签模式,其他人审批后当前节点其他人被取消) var cancelledTasks []model.Task if err := h.db.Where("instance_id = ? AND node_id = ? AND status = 3 AND id != ?", task.InstanceID, task.NodeID, task.ID).Find(&cancelledTasks).Error; err == nil && len(cancelledTasks) > 0 { var userIDs []string for _, ct := range cancelledTasks { if ct.AssigneeID != nil { var ud model.UserDepartment if err := h.db.Where("user_id = ?", *ct.AssigneeID).First(&ud).Error; err == nil && ud.DingTalkUserID != "" { userIDs = append(userIDs, ud.DingTalkUserID) } else if err != nil { logrus.WithError(err).WithField("user_id", *ct.AssigneeID).Warn("钉钉:查询同节点取消人 UserDepartment 失败") } else { logrus.WithField("user_id", *ct.AssigneeID).Warn("钉钉:同节点取消人 DingTalkUserID 为空") } } } if len(userIDs) > 0 { title, markdown := dingtalk.BuildSameNodeCancelledMsg(approverUser.RealName, comment, visitTime) logrus.WithFields(logrus.Fields{ "user_ids": userIDs, "title": title, }).Info("钉钉:发送同节点取消通知") if err := h.dingTalk.BatchSendRobotMessage(userIDs, title, markdown); err != nil { logrus.WithError(err).Error("钉钉发送同节点取消通知失败") } } else { logrus.WithField("cancelled_count", len(cancelledTasks)).Info("钉钉:同节点已取消任务无有效钉钉ID") } } else { logrus.WithField("has_cancelled", len(cancelledTasks) > 0).Info("钉钉:无同节点取消任务") } // 2. 通知下一个节点审批人 var pendingTasks []model.Task if err := h.db.Where("instance_id = ? AND status = 0", task.InstanceID).Find(&pendingTasks).Error; err == nil && len(pendingTasks) > 0 { var userIDs []string for _, pt := range pendingTasks { if pt.AssigneeID != nil { var ud model.UserDepartment if err := h.db.Where("user_id = ?", *pt.AssigneeID).First(&ud).Error; err == nil && ud.DingTalkUserID != "" { userIDs = append(userIDs, ud.DingTalkUserID) } else if err != nil { logrus.WithError(err).WithField("user_id", *pt.AssigneeID).Warn("钉钉:查询下一节点审批人 UserDepartment 失败") } else { logrus.WithField("user_id", *pt.AssigneeID).Warn("钉钉:下一节点审批人 DingTalkUserID 为空") } } } if len(userIDs) > 0 { msg := &dingtalk.AppointmentResultMessage{ ApproverName: approverUser.RealName, Action: "通过", Comment: comment, AppointmentID: appointment.ID, VisitTime: visitTime, VisitorName: visitorInfo.Name, } title, markdown := dingtalk.BuildApproveNotificationMsg(msg) logrus.WithFields(logrus.Fields{ "user_ids": userIDs, "title": title, }).Info("钉钉:发送下一节点审批人通知") if err := h.dingTalk.BatchSendRobotMessage(userIDs, title, markdown); err != nil { logrus.WithError(err).Error("钉钉发送下一节点通知失败") } } else { logrus.WithField("pending_count", len(pendingTasks)).Info("钉钉:下一节点无有效钉钉ID的审批人") } } else { logrus.WithField("has_pending", len(pendingTasks) > 0).Info("钉钉:无下一节点待审批任务") } }() } // sendDingTalkAfterRejection 审批拒绝后发送钉钉消息 func (h *Handler) sendDingTalkAfterRejection(taskID uint, comment string) { if h.dingTalk == nil { return } var task model.Task if err := h.db.Preload("Instance").First(&task, taskID).Error; err != nil { return } if task.Instance == nil || task.Instance.BusinessType != "appointment" { return } logrus.WithFields(logrus.Fields{ "task_id": taskID, "instance_id": task.InstanceID, }).Info("钉钉:审批拒绝,开始发送通知") go func() { // 获取拒绝人信息 var rejector model.User if task.AssigneeID == nil { return } if err := h.db.First(&rejector, *task.AssigneeID).Error; err != nil { logrus.WithError(err).WithField("task_id", taskID).Warn("钉钉:查询拒绝人信息失败") return } // 获取预约信息 var appointment model.Appointment if err := h.db.First(&appointment, task.Instance.BusinessID).Error; err != nil { return } var visitorInfo struct { Name string `json:"name"` Company string `json:"company"` } json.Unmarshal([]byte(appointment.VisitorInfo), &visitorInfo) visitTime := "" if !appointment.VisitStartTime.IsZero() { visitTime = dingtalk.FormatCSTTime(appointment.VisitStartTime, appointment.VisitEndTime) } // 查找本实例中其他待审批任务,通知其审批人流程已终止 var otherPendingTasks []model.Task if err := h.db.Where("instance_id = ? AND status = 0 AND id != ?", task.InstanceID, task.ID).Find(&otherPendingTasks).Error; err != nil { return } var userIDs []string for _, pt := range otherPendingTasks { if pt.AssigneeID != nil { var ud model.UserDepartment if err := h.db.Where("user_id = ?", *pt.AssigneeID).First(&ud).Error; err == nil && ud.DingTalkUserID != "" { userIDs = append(userIDs, ud.DingTalkUserID) } else if err != nil { logrus.WithError(err).WithField("user_id", *pt.AssigneeID).Warn("钉钉:查询其他待审批人 UserDepartment 失败") } else { logrus.WithField("user_id", *pt.AssigneeID).Warn("钉钉:其他待审批人 DingTalkUserID 为空") } } } if len(userIDs) > 0 { msg := &dingtalk.AppointmentResultMessage{ ApproverName: rejector.RealName, Action: "拒绝", Comment: comment, AppointmentID: appointment.ID, VisitTime: visitTime, VisitorName: visitorInfo.Name, } title, markdown := dingtalk.BuildRejectNotificationMsg(msg) logrus.WithFields(logrus.Fields{ "user_ids": userIDs, "title": title, }).Info("钉钉:发送拒绝通知给其他待审批人") if err := h.dingTalk.BatchSendRobotMessage(userIDs, title, markdown); err != nil { logrus.WithError(err).Error("钉钉发送拒绝通知失败") } } else { logrus.WithField("other_pending_count", len(otherPendingTasks)).Info("钉钉:拒绝后无其他待审批人需通知") } }() }