聊聊如何设计一款灵活配置、方便管理的工单系统

原创
2020/08/20 18:58
阅读数 1.4K

简单聊聊

在我们平时的工作中,会有很多时候需要跟其他同事进行协同办公,甚至需要跨部门合作,在这种情况下,我们就需要同事之间的沟通交流,而沟通过程中一句不合适的话,都会造成争吵,跨部门合作的时候,更加麻烦,因为对彼此都不熟悉,因此很难进行很好的沟通交流。

其实很多时候,我们的工作都是流程化,上一位同事处理完,下一位同事接着进行处理,在这种场景需求下就诞生了工单系统,将一个任务处理过程中,所涉及到的人,按照优先级排列,然后依次进行处理,处理过程中可进行注解,方便下一个处理人对之前处理结果的熟悉,并且处理完成后通过邮件或者其他通讯方式通知下一个人处理,依次类推,当处理完成后,通知工单申请人,工单已全部处理完成。

其实目前有一些现成的工单系统了,有开源的也有收费的,但是普遍的功能比较单一,不方便管理,当然肯定有我不知道的非常流弊的工单系统,只是在说我已知的,如果冒犯到你的话,深感抱歉。

本篇文章就给大家详细的介绍一下如何设计一个灵活,易维护管理的工单系统,在介绍此工单系统之前,首先给大家推荐一个还是不错的工单系统,供大家参考,学习。

Demo: ferry 工单系统(点击进入演示Demo)

Github: https://github.com/lanyulei/ferry

Gitee: https://gitee.com/yllan/ferry

核心功能介绍

单点登陆

目前大部分公司都会安装部署一套单点登录系统,方便维护管理员工的账号信息,同时实现多系统的用户登陆,例如Ldap,因此支持主流的单点登录系统也是很有必要的。

权限管理

对于一个工单系统来说,因为涉及的人会非常多,审批阶段会有各层次的领导进行审阅;处理阶段会有各部门的人进行处理。因此对于权限的管理是要非常的严格。

流程设计

在工单系统中流程设计应该算是最繁琐的地方了,要定义好每个节点的关联关系、处理人以及各种属性数据,如果没有一个好的流程设计工具,对于工单系统的用户体验来说,是非常差的。而且对于后续的维护也是非常的不易。

之前就有朋友说,他们的工单系统是写死的,每个流程,每个节点,每个处理人都是在代码里定义好的,这样的工单系统是非常不易用的,只要有人员变动,流程变动,都需要重新修改代码进行编译上线,因此好的流程设计工具,对一个好的工单系统来说,是至关重要的。

模版设计

我们在提交工单的时候,需要写很多的描述数据,例如申请服务器,就需要写服务器的配置信息,像是CPU、内存、磁盘等信息,这些需要写的描述数据,并非是一成不变的,会根据场景的不同而进行适当的修改,因此我们就需要有一个可视化的表单设计工具,来进行表单模版的维护管理,通过将表单模版与流程的关联从而实现一条完整的工作流。

多样化的工单列表

我们工单系统内的工单,根据工单归属的分类,可以大概的分为:我创建的工单、我待办的工单、我相关的工单以及所有工单。我们根据这4类工单归属类型进行数据的分类展示,方便他人的使用及查看。

任务管理

在一个工单流程中,通常很多操作都是可以自动完成的,比如申请数据库账号,可以让用户填写好数据后,DBA审批通过,则自动去创建账号,并进行邮件或者其他通讯工具进行通知。这种就能节省很多人为操作,使这个人可以处理更多其他的工作。毕竟20世纪最贵的成本是人力呢。

更多其他功能点

  • 加签: 临时将工单转交给他人处理,处理完成后自动返回给原来的处理人。
  • 会签: 当一个节点出现多个处理人的时候,则需要所有的处理人都处理完成才可进行下一步。
  • 催办: 催促当前节点的处理人尽快处理,当然需要设置时间间隔的。
  • 结单: 有的时候我们会提错单子,因为可联系管理员进行手动结单。
  • 转交: 在流程中,某一节点的处理人临时有时间,商议后,可转交给他人。
  • 主动接单: 当一个节点出现多个处理人的时候,配置主动接单后,需这几个人进行抢单处理。
  • 等等,还有很多扩展的功能,有兴趣的人,可以加群一起聊聊:1127401830 。

以上就是大概总结的一些核心的功能,当然还有很多细小的划分。更精细的设计就需要大家去认真构思了。毕竟一篇文章如果说考虑到方方面面的话,那是不现实的,得需要码多少字呢。

数据库设计

其实开发一个系统,最耗费时间的不是敲代码的部分。程序设计,数据结构设计才是最繁琐的,要考虑到各种场景、各种关联。

所以这里就不藏私了,直接给大家看下我的数据结构设计:

用户/权限/菜单/系统配置

image

工单系统

image

核心功能代码

在此将核心功能中代码给大家展示一下,但是肯定不会全部的(因为实在是太多了,贴不完呢),有兴趣的可以去Github上Clone下来详细的研究一下。

Github: https://github.com/lanyulei/ferry

Gitee: https://gitee.com/yllan/ferry

Ldap登陆验证

ldap算是比较常用的一个单点登陆系统了,跟同事、同学、朋友聊天的过程中,能感觉大部分都是在使用ldap。当然有人会说ldap是一个协议,这些就不用纠结这些细节了,你也可以说,使用了ldap协议单点登录系统。

集成ldap登陆验证后,在用户验证通过会,会将数据保存至本地数据库一份,方便维护管理。

连接ldap:

package ldap

import (
	"crypto/tls"
	"errors"
	"ferry/pkg/logger"
	"fmt"
	"time"

	"github.com/spf13/viper"

	"github.com/go-ldap/ldap/v3"
)

/*
  @Author : lanyulei
*/

var conn *ldap.Conn

// ldap连接
func ldapConnection() (err error) {
	var ldapConn = fmt.Sprintf("%v:%v", viper.GetString("settings.ldap.host"), viper.GetString("settings.ldap.port"))

	conn, err = ldap.Dial(
		"tcp",
		ldapConn,
	)
	if err != nil {
		err = errors.New(fmt.Sprintf("无法连接到ldap服务器,%v", err))
		logger.Error(err)
		return
	}

	if viper.GetBool("settings.ldap.tls") {
		err = conn.StartTLS(&tls.Config{
			InsecureSkipVerify: true,
		})
		if err != nil {
			err = errors.New(fmt.Sprintf("升级到加密方式失败,%v", err))
			logger.Error(err)
			return
		}
	}

	//设置超时时间
	conn.SetTimeout(5 * time.Second)

	return
}

搜索ldap用户信息:

package ldap

import (
	"encoding/json"
	"errors"
	"ferry/global/orm"
	"ferry/models/system"
	"ferry/pkg/logger"
	"fmt"

	"github.com/go-ldap/ldap/v3"
	"github.com/spf13/viper"
)

/*
  @Author : lanyulei
*/

func getLdapFields() (ldapFields []map[string]string, err error) {
	var (
		settingsValue system.Settings
		contentList   []map[string]string
	)

	err = orm.Eloquent.Model(&settingsValue).Where("classify = 2").Find(&settingsValue).Error
	if err != nil {
		return
	}

	err = json.Unmarshal(settingsValue.Content, &contentList)
	if err != nil {
		return
	}

	for _, v := range contentList {
		if v["ldap_field_name"] != "" {
			ldapFields = append(ldapFields, v)
		}
	}
	return
}

func searchRequest(username string) (userInfo *ldap.Entry, err error) {
	var (
		ldapFields       []map[string]string
		cur              *ldap.SearchResult
		ldapFieldsFilter = []string{
			"dn",
		}
	)
	ldapFields, err = getLdapFields()
	for _, v := range ldapFields {
		ldapFieldsFilter = append(ldapFieldsFilter, v["ldap_field_name"])
	}
	// 用来获取查询权限的用户。如果 ldap 禁止了匿名查询,那我们就需要先用这个帐户 bind 以下才能开始查询
	if !viper.GetBool("settings.ldap.anonymousQuery") {
		err = conn.Bind(
			fmt.Sprintf("cn=%v,%v",
				viper.GetString("settings.ldap.bindUser"),
				viper.GetString("settings.ldap.baseDn")),
			viper.GetString("settings.ldap.bindPwd"))
		if err != nil {
			logger.Error("用户或密码错误。", err)
			return
		}
	}

	sql := ldap.NewSearchRequest(
		viper.GetString("settings.ldap.baseDn"),
		ldap.ScopeWholeSubtree,
		ldap.DerefAlways,
		0,
		0,
		false,
		fmt.Sprintf("(cn=%s)", username),
		ldapFieldsFilter,
		nil)

	if cur, err = conn.Search(sql); err != nil {
		err = errors.New(fmt.Sprintf("在Ldap搜索用户失败, %v", err))
		logger.Error(err)
		return
	}

	if len(cur.Entries) == 0 {
		err = errors.New("未查询到对应的用户信息。")
		logger.Error(err)
		return
	}

	userInfo = cur.Entries[0]

	return
}

映射本地数据库字段:

package ldap

import (
	"ferry/models/system"

	"github.com/go-ldap/ldap/v3"
)

/*
  @Author : lanyulei
*/

func LdapFieldsMap(ldapUserInfo *ldap.Entry) (userInfo system.SysUser, err error) {
	var (
		ldapFields []map[string]string
	)

	ldapFields, err = getLdapFields()
	if err != nil {
		return
	}

	for _, v := range ldapFields {
		switch v["local_field_name"] {
		case "nick_name":
			userInfo.NickName = ldapUserInfo.GetAttributeValue(v["ldap_field_name"])
		case "phone":
			userInfo.Phone = ldapUserInfo.GetAttributeValue(v["ldap_field_name"])
		case "avatar":
			userInfo.Avatar = ldapUserInfo.GetAttributeValue(v["ldap_field_name"])
		case "sex":
			userInfo.Sex = ldapUserInfo.GetAttributeValue(v["ldap_field_name"])
		case "email":
			userInfo.Email = ldapUserInfo.GetAttributeValue(v["ldap_field_name"])
		case "remark":
			userInfo.Remark = ldapUserInfo.GetAttributeValue(v["ldap_field_name"])
		}
	}

	return
}

ldap用户登陆:

package ldap

import (
	"fmt"

	"github.com/go-ldap/ldap/v3"
)

/*
  @Author : lanyulei
*/

func LdapLogin(username string, password string) (userInfo *ldap.Entry, err error) {
	err = ldapConnection()
	if err != nil {
		return
	}
	defer conn.Close()

	userInfo, err = searchRequest(username)
	if err != nil {
		return
	}

	err = conn.Bind(userInfo.DN, password)
	if err != nil {
		return nil, fmt.Errorf("用户或密码不正确。")
	}

	return
}

工单流转

在一个工单系统中,最核心的部分就是工单流转,要兼容各种处理人信息,并且根据节点之间的关联进行节点的流转,因此,在工单流转这一部分我们需要非常认真对待,因为如果你不认真对待,可能会造成工单流转到一个非此流程相关的人名下,对于用户体验是非常差的,甚至会丧失用户对系统的信用不再使用此系统。所以这段是重中之重。需认真对待。

package service

import (
	"encoding/json"
	"errors"
	"ferry/global/orm"
	"ferry/models/base"
	"ferry/models/process"
	"ferry/models/system"
	"ferry/pkg/notify"
	"ferry/tools"
	"fmt"
	"reflect"
	"time"

	"github.com/jinzhu/gorm"

	"github.com/gin-gonic/gin"
)

/*
  @Author : lanyulei
  @Desc : 处理工单
*/

/*
    -- 节点 --
	start: 开始节点
	userTask: 审批节点
	receiveTask: 处理节点
	scriptTask: 任务节点
	end: 结束节点
    -- 网关 --
    exclusiveGateway: 排他网关
    parallelGateway: 并行网关
    inclusiveGateway: 包容网关
*/

type Handle struct {
	cirHistoryList   []process.CirculationHistory
	workOrderId      int
	updateValue      map[string]interface{}
	stateValue       map[string]interface{}
	targetStateValue map[string]interface{}
	workOrderData    [][]byte
	workOrderDetails process.WorkOrderInfo
	endHistory       bool
	flowProperties   int
	circulationValue string
	processState     ProcessState
	tx               *gorm.DB
}

// 时间格式化
func fmtDuration(d time.Duration) string {
	d = d.Round(time.Minute)
	h := d / time.Hour
	d -= h * time.Hour
	m := d / time.Minute
	return fmt.Sprintf("%02d小时 %02d分钟", h, m)
}

// 会签
func (h *Handle) Countersign(c *gin.Context) (err error) {
	var (
		stateList       []map[string]interface{}
		stateIdMap      map[string]interface{}
		currentState    map[string]interface{}
		cirHistoryCount int
	)

	err = json.Unmarshal(h.workOrderDetails.State, &stateList)
	if err != nil {
		return
	}

	stateIdMap = make(map[string]interface{})
	for _, v := range stateList {
		stateIdMap[v["id"].(string)] = v["label"]
		if v["id"].(string) == h.stateValue["id"].(string) {
			currentState = v
		}
	}
	for _, cirHistoryValue := range h.cirHistoryList {
		if _, ok := stateIdMap[cirHistoryValue.Source]; !ok {
			break
		}
		for _, processor := range currentState["processor"].([]interface{}) {
			if cirHistoryValue.ProcessorId != tools.GetUserId(c) &&
				cirHistoryValue.Source == currentState["id"].(string) &&
				cirHistoryValue.ProcessorId == int(processor.(float64)) {
				cirHistoryCount += 1
			}
		}
	}
	if cirHistoryCount == len(currentState["processor"].([]interface{}))-1 {
		h.endHistory = true
		err = h.circulation()
		if err != nil {
			return
		}
	}
	return
}

// 工单跳转
func (h *Handle) circulation() (err error) {
	var (
		stateValue []byte
	)

	stateList := make([]interface{}, 0)
	for _, v := range h.updateValue["state"].([]map[string]interface{}) {
		stateList = append(stateList, v)
	}
	err = GetVariableValue(stateList, h.workOrderDetails.Creator)
	if err != nil {
		return
	}

	stateValue, err = json.Marshal(h.updateValue["state"])
	if err != nil {
		return
	}

	err = h.tx.Model(&process.WorkOrderInfo{}).
		Where("id = ?", h.workOrderId).
		Updates(map[string]interface{}{
			"state":          stateValue,
			"related_person": h.updateValue["related_person"],
		}).Error
	if err != nil {
		h.tx.Rollback()
		return
	}
	return
}

// 条件判断
func (h *Handle) ConditionalJudgment(condExpr map[string]interface{}) (result bool, err error) {
	var (
		condExprOk    bool
		condExprValue interface{}
	)

	defer func() {
		if r := recover(); r != nil {
			switch e := r.(type) {
			case string:
				err = errors.New(e)
			case error:
				err = e
			default:
				err = errors.New("未知错误")
			}
			return
		}
	}()

	for _, data := range h.workOrderData {
		var formData map[string]interface{}
		err = json.Unmarshal(data, &formData)
		if err != nil {
			return
		}
		if condExprValue, condExprOk = formData[condExpr["key"].(string)]; condExprOk {
			break
		}
	}

	if condExprValue == nil {
		err = errors.New("未查询到对应的表单数据。")
		return
	}

	// todo 待优化
	switch reflect.TypeOf(condExprValue).String() {
	case "string":
		switch condExpr["sign"] {
		case "==":
			if condExprValue.(string) == condExpr["value"].(string) {
				result = true
			}
		case "!=":
			if condExprValue.(string) != condExpr["value"].(string) {
				result = true
			}
		case ">":
			if condExprValue.(string) > condExpr["value"].(string) {
				result = true
			}
		case ">=":
			if condExprValue.(string) >= condExpr["value"].(string) {
				result = true
			}
		case "<":
			if condExprValue.(string) < condExpr["value"].(string) {
				result = true
			}
		case "<=":
			if condExprValue.(string) <= condExpr["value"].(string) {
				result = true
			}
		default:
			err = errors.New("目前仅支持6种常规判断类型,包括(等于、不等于、大于、大于等于、小于、小于等于)")
		}
	default:
		err = errors.New("条件判断目前仅支持字符串、整型。")
	}

	return
}

// 并行网关,确认其他节点是否完成
func (h *Handle) completeAllParallel(c *gin.Context, target string) (statusOk bool, err error) {
	var (
		stateList []map[string]interface{}
	)

	err = json.Unmarshal(h.workOrderDetails.State, &stateList)
	if err != nil {
		err = fmt.Errorf("反序列化失败,%v", err.Error())
		return
	}

continueHistoryTag:
	for _, v := range h.cirHistoryList {
		status := false
		for i, s := range stateList {
			if v.Source == s["id"].(string) && v.Target == target {
				status = true
				stateList = append(stateList[:i], stateList[i+1:]...)
				continue continueHistoryTag
			}
		}
		if !status {
			break
		}
	}

	if len(stateList) == 1 && stateList[0]["id"].(string) == h.stateValue["id"] {
		statusOk = true
	}

	return
}

func (h *Handle) commonProcessing(c *gin.Context) (err error) {
	// 如果是拒绝的流转则直接跳转
	if h.flowProperties == 0 {
		err = h.circulation()
		if err != nil {
			err = fmt.Errorf("工单跳转失败,%v", err.Error())
		}
		return
	}

	// 会签
	if h.stateValue["assignValue"] != nil && len(h.stateValue["assignValue"].([]interface{})) > 1 {
		if isCounterSign, ok := h.stateValue["isCounterSign"]; ok {
			if isCounterSign.(bool) {
				h.endHistory = false
				err = h.Countersign(c)
				if err != nil {
					return
				}
			} else {
				err = h.circulation()
				if err != nil {
					return
				}
			}
		} else {
			err = h.circulation()
			if err != nil {
				return
			}
		}
	} else {
		err = h.circulation()
		if err != nil {
			return
		}
	}
	return
}

func (h *Handle) HandleWorkOrder(
	c *gin.Context,
	workOrderId int,
	tasks []string,
	targetState string,
	sourceState string,
	circulationValue string,
	flowProperties int,
	remarks string,
) (err error) {
	h.workOrderId = workOrderId
	h.flowProperties = flowProperties
	h.endHistory = true

	var (
		execTasks          []string
		relatedPersonList  []int
		cirHistoryValue    []process.CirculationHistory
		cirHistoryData     process.CirculationHistory
		costDurationValue  string
		sourceEdges        []map[string]interface{}
		targetEdges        []map[string]interface{}
		condExprStatus     bool
		relatedPersonValue []byte
		parallelStatusOk   bool
		processInfo        process.Info
		currentUserInfo    system.SysUser
		sendToUserList     []system.SysUser
		noticeList         []int
		sendSubject        string = "您有一条待办工单,请及时处理"
		sendDescription    string = "您有一条待办工单请及时处理,工单描述如下"
	)

	defer func() {
		if r := recover(); r != nil {
			switch e := r.(type) {
			case string:
				err = errors.New(e)
			case error:
				err = e
			default:
				err = errors.New("未知错误")
			}
			return
		}
	}()

	// 获取工单信息
	err = orm.Eloquent.Model(&process.WorkOrderInfo{}).Where("id = ?", workOrderId).Find(&h.workOrderDetails).Error
	if err != nil {
		return
	}

	// 获取流程信息
	err = orm.Eloquent.Model(&process.Info{}).Where("id = ?", h.workOrderDetails.Process).Find(&processInfo).Error
	if err != nil {
		return
	}
	err = json.Unmarshal(processInfo.Structure, &h.processState.Structure)
	if err != nil {
		return
	}

	// 获取当前节点
	h.stateValue, err = h.processState.GetNode(sourceState)
	if err != nil {
		return
	}

	// 目标状态
	h.targetStateValue, err = h.processState.GetNode(targetState)
	if err != nil {
		return
	}

	// 获取工单数据
	err = orm.Eloquent.Model(&process.TplData{}).
		Where("work_order = ?", workOrderId).
		Pluck("form_data", &h.workOrderData).Error
	if err != nil {
		return
	}

	// 根据处理人查询出需要会签的条数
	err = orm.Eloquent.Model(&process.CirculationHistory{}).
		Where("work_order = ?", workOrderId).
		Order("id desc").
		Find(&h.cirHistoryList).Error
	if err != nil {
		return
	}

	err = json.Unmarshal(h.workOrderDetails.RelatedPerson, &relatedPersonList)
	if err != nil {
		return
	}
	relatedPersonStatus := false
	for _, r := range relatedPersonList {
		if r == tools.GetUserId(c) {
			relatedPersonStatus = true
			break
		}
	}
	if !relatedPersonStatus {
		relatedPersonList = append(relatedPersonList, tools.GetUserId(c))
	}

	relatedPersonValue, err = json.Marshal(relatedPersonList)
	if err != nil {
		return
	}

	h.updateValue = map[string]interface{}{
		"related_person": relatedPersonValue,
	}

	// 开启事务
	h.tx = orm.Eloquent.Begin()

	stateValue := map[string]interface{}{
		"label": h.targetStateValue["label"].(string),
		"id":    h.targetStateValue["id"].(string),
	}

	switch h.targetStateValue["clazz"] {
	// 排他网关
	case "exclusiveGateway":
		sourceEdges, err = h.processState.GetEdge(h.targetStateValue["id"].(string), "source")
		if err != nil {
			return
		}
	breakTag:
		for _, edge := range sourceEdges {
			edgeCondExpr := make([]map[string]interface{}, 0)
			err = json.Unmarshal([]byte(edge["conditionExpression"].(string)), &edgeCondExpr)
			if err != nil {
				return
			}
			for _, condExpr := range edgeCondExpr {
				// 条件判断
				condExprStatus, err = h.ConditionalJudgment(condExpr)
				if err != nil {
					return
				}
				if condExprStatus {
					// 进行节点跳转
					h.targetStateValue, err = h.processState.GetNode(edge["target"].(string))
					if err != nil {
						return
					}

					if h.targetStateValue["clazz"] == "userTask" || h.targetStateValue["clazz"] == "receiveTask" {
						if h.targetStateValue["assignValue"] == nil || h.targetStateValue["assignType"] == "" {
							err = errors.New("处理人不能为空")
							return
						}
					}

					h.updateValue["state"] = []map[string]interface{}{{
						"id":             h.targetStateValue["id"].(string),
						"label":          h.targetStateValue["label"],
						"processor":      h.targetStateValue["assignValue"],
						"process_method": h.targetStateValue["assignType"],
					}}
					err = h.commonProcessing(c)
					if err != nil {
						err = fmt.Errorf("流程流程跳转失败,%v", err.Error())
						return
					}

					break breakTag
				}
			}
		}
		if !condExprStatus {
			err = errors.New("所有流转均不符合条件,请确认。")
			return
		}
	// 并行/聚合网关
	case "parallelGateway":
		// 入口,判断
		sourceEdges, err = h.processState.GetEdge(h.targetStateValue["id"].(string), "source")
		if err != nil {
			err = fmt.Errorf("查询流转信息失败,%v", err.Error())
			return
		}

		targetEdges, err = h.processState.GetEdge(h.targetStateValue["id"].(string), "target")
		if err != nil {
			err = fmt.Errorf("查询流转信息失败,%v", err.Error())
			return
		}

		if len(sourceEdges) > 0 {
			h.targetStateValue, err = h.processState.GetNode(sourceEdges[0]["target"].(string))
			if err != nil {
				return
			}
		} else {
			err = errors.New("并行网关流程不正确")
			return
		}

		if len(sourceEdges) > 1 && len(targetEdges) == 1 {
			// 入口
			h.updateValue["state"] = make([]map[string]interface{}, 0)
			for _, edge := range sourceEdges {
				targetStateValue, err := h.processState.GetNode(edge["target"].(string))
				if err != nil {
					return err
				}
				h.updateValue["state"] = append(h.updateValue["state"].([]map[string]interface{}), map[string]interface{}{
					"id":             edge["target"].(string),
					"label":          targetStateValue["label"],
					"processor":      targetStateValue["assignValue"],
					"process_method": targetStateValue["assignType"],
				})
			}
			err = h.circulation()
			if err != nil {
				err = fmt.Errorf("工单跳转失败,%v", err.Error())
				return
			}
		} else if len(sourceEdges) == 1 && len(targetEdges) > 1 {
			// 出口
			parallelStatusOk, err = h.completeAllParallel(c, sourceEdges[0]["target"].(string))
			if err != nil {
				err = fmt.Errorf("并行检测失败,%v", err.Error())
				return
			}
			if parallelStatusOk {
				h.endHistory = true
				endAssignValue, ok := h.targetStateValue["assignValue"]
				if !ok {
					endAssignValue = []int{}
				}

				endAssignType, ok := h.targetStateValue["assignType"]
				if !ok {
					endAssignType = ""
				}

				h.updateValue["state"] = []map[string]interface{}{{
					"id":             h.targetStateValue["id"].(string),
					"label":          h.targetStateValue["label"],
					"processor":      endAssignValue,
					"process_method": endAssignType,
				}}
				err = h.circulation()
				if err != nil {
					err = fmt.Errorf("工单跳转失败,%v", err.Error())
					return
				}
			} else {
				h.endHistory = false
			}

		} else {
			err = errors.New("并行网关流程不正确")
			return
		}
	// 包容网关
	case "inclusiveGateway":
		return
	case "start":
		stateValue["processor"] = []int{h.workOrderDetails.Creator}
		stateValue["process_method"] = "person"
		h.updateValue["state"] = []map[string]interface{}{stateValue}
		err = h.circulation()
		if err != nil {
			return
		}
	case "userTask":
		stateValue["processor"] = h.targetStateValue["assignValue"].([]interface{})
		stateValue["process_method"] = h.targetStateValue["assignType"].(string)
		h.updateValue["state"] = []map[string]interface{}{stateValue}
		err = h.commonProcessing(c)
		if err != nil {
			return
		}
	case "receiveTask":
		stateValue["processor"] = h.targetStateValue["assignValue"].([]interface{})
		stateValue["process_method"] = h.targetStateValue["assignType"].(string)
		h.updateValue["state"] = []map[string]interface{}{stateValue}
		err = h.commonProcessing(c)
		if err != nil {
			return
		}
	case "scriptTask":
		stateValue["processor"] = []int{}
		stateValue["process_method"] = ""
		h.updateValue["state"] = []map[string]interface{}{stateValue}
	case "end":
		stateValue["processor"] = []int{}
		stateValue["process_method"] = ""
		h.updateValue["state"] = []map[string]interface{}{stateValue}
		err = h.circulation()
		if err != nil {
			h.tx.Rollback()
			return
		}
		err = h.tx.Model(&process.WorkOrderInfo{}).
			Where("id = ?", h.workOrderId).
			Update("is_end", 1).Error
		if err != nil {
			h.tx.Rollback()
			return
		}
	}

	// 流转历史写入
	err = orm.Eloquent.Model(&cirHistoryValue).
		Where("work_order = ?", workOrderId).
		Find(&cirHistoryValue).
		Order("create_time desc").Error
	if err != nil {
		h.tx.Rollback()
		return
	}
	for _, t := range cirHistoryValue {
		if t.Source != h.stateValue["id"] {
			costDuration := time.Since(t.CreatedAt.Time)
			costDurationValue = fmtDuration(costDuration)
		}
	}

	// 获取当前用户信息
	err = orm.Eloquent.Model(¤tUserInfo).
		Where("user_id = ?", tools.GetUserId(c)).
		Find(¤tUserInfo).Error
	if err != nil {
		return
	}

	cirHistoryData = process.CirculationHistory{
		Model:        base.Model{},
		Title:        h.workOrderDetails.Title,
		WorkOrder:    h.workOrderDetails.Id,
		State:        h.stateValue["label"].(string),
		Source:       h.stateValue["id"].(string),
		Target:       h.targetStateValue["id"].(string),
		Circulation:  circulationValue,
		Processor:    currentUserInfo.NickName,
		ProcessorId:  tools.GetUserId(c),
		CostDuration: costDurationValue,
		Remarks:      remarks,
	}

	err = h.tx.Create(&cirHistoryData).Error
	if err != nil {
		h.tx.Rollback()
		return
	}

	// 获取流程通知类型列表
	err = json.Unmarshal(processInfo.Notice, ¬iceList)
	if err != nil {
		return
	}

	bodyData := notify.BodyData{
		SendTo: map[string]interface{}{
			"userList": sendToUserList,
		},
		Subject:     sendSubject,
		Description: sendDescription,
		Classify:    noticeList,
		ProcessId:   h.workOrderDetails.Process,
		Id:          h.workOrderDetails.Id,
		Title:       h.workOrderDetails.Title,
		Creator:     currentUserInfo.NickName,
		Priority:    h.workOrderDetails.Priority,
		CreatedAt:   h.workOrderDetails.CreatedAt.Format("2006-01-02 15:04:05"),
	}

	// 判断目标是否是结束节点
	if h.targetStateValue["clazz"] == "end" && h.endHistory == true {
		sendSubject = "您的工单已处理完成"
		sendDescription = "您的工单已处理完成,工单描述如下"
		err = h.tx.Create(&process.CirculationHistory{
			Model:       base.Model{},
			Title:       h.workOrderDetails.Title,
			WorkOrder:   h.workOrderDetails.Id,
			State:       h.targetStateValue["label"].(string),
			Source:      h.targetStateValue["id"].(string),
			Processor:   currentUserInfo.NickName,
			ProcessorId: tools.GetUserId(c),
			Circulation: "结束",
			Remarks:     "工单已结束",
		}).Error
		if err != nil {
			h.tx.Rollback()
			return
		}
		if len(noticeList) > 0 {
			// 查询工单创建人信息
			err = h.tx.Model(&system.SysUser{}).
				Where("user_id = ?", h.workOrderDetails.Creator).
				Find(&sendToUserList).Error
			if err != nil {
				return
			}

			bodyData.SendTo = map[string]interface{}{
				"userList": sendToUserList,
			}
			bodyData.Subject = sendSubject
			bodyData.Description = sendDescription

			// 发送通知
			go func(bodyData notify.BodyData) {
				err = bodyData.SendNotify()
				if err != nil {
					return
				}
			}(bodyData)
		}
	}

	h.tx.Commit() // 提交事务

	// 发送通知
	if len(noticeList) > 0 {
		stateList := make([]interface{}, 0)
		for _, v := range h.updateValue["state"].([]map[string]interface{}) {
			stateList = append(stateList, v)
		}
		sendToUserList, err = GetPrincipalUserInfo(stateList, h.workOrderDetails.Creator)
		if err != nil {
			return
		}

		bodyData.SendTo = map[string]interface{}{
			"userList": sendToUserList,
		}
		bodyData.Subject = sendSubject
		bodyData.Description = sendDescription

		// 发送通知
		go func(bodyData notify.BodyData) {
			err = bodyData.SendNotify()
			if err != nil {
				return
			}
		}(bodyData)
	}

	// 执行流程公共任务及节点任务
	if h.stateValue["task"] != nil {
		for _, task := range h.stateValue["task"].([]interface{}) {
			tasks = append(tasks, task.(string))
		}
	}
continueTag:
	for _, task := range tasks {
		for _, t := range execTasks {
			if t == task {
				continue continueTag
			}
		}
		execTasks = append(execTasks, task)
	}
	go ExecTask(execTasks)

	return
}

ok, anyway.

加上一段在公司经常听到英文作为结束语,如果能实现本文的这些功能,其实一个简单易用,灵活方便维护管理的工单系统就成型了。

在此给大家说声抱歉,无法将完整的代码贴在这里(因为实在是太多了),如果大家还有什么疑问的话,欢迎随时来我的博客留言,我看到会尽快回复,也可以加入我的qq群,一起讨论学习。

blog: 兰玉磊的技术博客

QQ群:1127401830

展开阅读全文
加载中

作者的其它热门文章

打赏
0
1 收藏
分享
打赏
2 评论
1 收藏
0
分享
返回顶部
顶部