Files
huangjin fe3ad20fe2 'init'
2026-06-29 17:34:59 +08:00

788 lines
25 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 工作流模块 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("钉钉:拒绝后无其他待审批人需通知")
}
}()
}