Files
yunzerwebsiteallinone/go/controllers/platform_approval.go
T
2026-10-08 15:30:37 +08:00

691 lines
23 KiB
Go
Raw 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.
package controllers
import (
"encoding/json"
"fmt"
"io"
"strconv"
"strings"
"time"
"server/models"
"server/services"
approvalEngine "server/services/approval"
"github.com/beego/beego/v2/client/orm"
beego "github.com/beego/beego/v2/server/web"
)
// PlatformApprovalController 平台端审批流配置(流程头 + 节点编排)。
//
// 当期面向「域名申请」这类平台侧管控场景,按 module_code + biz_type 联合唯一组织流程;
// 平台账号不归属租户,故流程与节点均无 tid,审批人取平台用户 / 平台角色。
type PlatformApprovalController struct {
beego.Controller
}
// ---- 配置侧 JSON 结构 ----
// approvalNodePayload 单个节点(前端画布节点)。
// node_type:1开始 2审批 3结束 4判断 5通知;pos_x/pos_y 为画布坐标回显;
// condition_expr 为该节点作为「判断节点出边目标」时的进入条件(由 edges 反推落库)。
type approvalNodePayload struct {
NodeKey string `json:"node_key"`
NodeName string `json:"node_name"`
NodeType int8 `json:"node_type"` // 1开始 2审批 3结束 4判断 5通知
ApproverType int8 `json:"approver_type"` // 1指定用户 2指定角色 ...
ApproverIDs []uint64 `json:"approver_ids"`
// NotifyTargets 通知节点多类型存储:"类型:ID" 列表(如 ["1:52","5:0"]),
// 类型取值同 approver_type;自动定位类型(5/6)ID 占位 0。
// 仅通知节点使用;空列表时按 approver_type + approver_ids 旧格式处理。
NotifyTargets []string `json:"notify_targets"`
MultiMode int8 `json:"multi_mode"` // 1或签 2会签
AllowReturn int8 `json:"allow_return"` // 是否允许退回
TimeoutHours int `json:"timeout_hours"` // 超时小时
Sort int `json:"sort"` // 顺序(越小越靠前)
PosX int `json:"pos_x"` // 画布 X 坐标
PosY int `json:"pos_y"` // 画布 Y 坐标
ConditionExpr string `json:"condition_expr"`
}
// approvalEdge 画布连线:from→to,condition 为该出边的分支条件(落到 to 节点的 condition_expr)
type approvalEdge struct {
From string `json:"from"`
To string `json:"to"`
Condition string `json:"condition"`
}
// approvalFlowSave 保存流程(图:节点 + 连线,节点全量覆盖)
type approvalFlowSave struct {
ID uint64 `json:"id"`
ModuleCode string `json:"module_code"`
BizType string `json:"biz_type"`
FlowName string `json:"flow_name"`
Description string `json:"description"`
IsDefault int8 `json:"is_default"`
Status int8 `json:"status"`
ApplyDeptIDs []uint64 `json:"apply_dept_ids"` // 仅租户侧使用,空=全公司通用
Nodes []approvalNodePayload `json:"nodes"`
Edges []approvalEdge `json:"edges"`
}
// approvalFlowView 流程 + 节点 + 连线(详情返回)
type approvalFlowView struct {
ID uint64 `json:"id"`
ModuleCode string `json:"module_code"`
BizType string `json:"biz_type"`
FlowCode string `json:"flow_code"`
FlowName string `json:"flow_name"`
Description string `json:"description"`
IsDefault int8 `json:"is_default"`
Status int8 `json:"status"`
NodeCount int `json:"node_count"`
CreateTime string `json:"create_time"`
ApplyDeptIDs []uint64 `json:"apply_dept_ids"` // 仅租户侧回显
Nodes []approvalNodePayload `json:"nodes"`
Edges []approvalEdge `json:"edges"`
}
// readApprovalBody 读取并解析 JSON 请求体
func (c *PlatformApprovalController) readApprovalBody(dst interface{}) error {
raw, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil {
return fmt.Errorf("参数错误")
}
if err := json.Unmarshal(raw, dst); err != nil {
return fmt.Errorf("参数错误: %s", err.Error())
}
return nil
}
// FlowList GET /platform/approval/flow/list?biz_type=domain
// 平台端流程列表;可按单据类型过滤,附带节点数。
func (c *PlatformApprovalController) FlowList() {
if _, err := requirePlatform(&c.Controller); err != nil {
jsonErr(&c.Controller, 401, 401, err.Error())
return
}
bizType := strings.TrimSpace(c.GetString("biz_type"))
if bizType == "" {
bizType = strings.TrimSpace(c.GetString("module")) // 兼容:分类即业务类型
}
qs := models.Orm.QueryTable(new(models.PlatformApprovalFlow)).
Filter("is_deleted", 0)
if bizType != "" {
qs = qs.Filter("biz_type", bizType)
}
var rows []models.PlatformApprovalFlow
_, _ = qs.OrderBy("-is_default", "id").All(&rows)
list := make([]approvalFlowView, 0, len(rows))
for _, r := range rows {
cnt, _ := models.Orm.QueryTable(new(models.PlatformApprovalNode)).
Filter("flow_id", r.ID).Filter("is_deleted", 0).Count()
list = append(list, approvalFlowView{
ID: r.ID,
ModuleCode: r.ModuleCode,
BizType: r.BizType,
FlowCode: r.FlowCode,
FlowName: r.FlowName,
Description: r.Description,
IsDefault: r.IsDefault,
Status: r.Status,
NodeCount: int(cnt),
CreateTime: r.CreateTime.Format("2006-01-02 15:04:05"),
})
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "ok", "data": list}
_ = c.ServeJSON()
}
// FlowDetail GET /platform/approval/flow/detail/:id
// 返回流程及其全部节点(按 sort 升序),供编排界面回显。
func (c *PlatformApprovalController) FlowDetail() {
if _, err := requirePlatform(&c.Controller); err != nil {
jsonErr(&c.Controller, 401, 401, err.Error())
return
}
id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
var f models.PlatformApprovalFlow
if err := models.Orm.QueryTable(new(models.PlatformApprovalFlow)).
Filter("id", id).Filter("is_deleted", 0).One(&f); err != nil {
jsonErr(&c.Controller, 404, 404, "流程不存在")
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "ok", "data": c.flowToView(f)}
_ = c.ServeJSON()
}
// flowToView 组装流程视图(含节点与连线)
func (c *PlatformApprovalController) flowToView(f models.PlatformApprovalFlow) approvalFlowView {
var nodes []models.PlatformApprovalNode
_, _ = models.Orm.QueryTable(new(models.PlatformApprovalNode)).
Filter("flow_id", f.ID).Filter("is_deleted", 0).
OrderBy("sort", "id").All(&nodes)
view := approvalFlowView{
ID: f.ID,
ModuleCode: f.ModuleCode,
BizType: f.BizType,
FlowCode: f.FlowCode,
FlowName: f.FlowName,
Description: f.Description,
IsDefault: f.IsDefault,
Status: f.Status,
NodeCount: len(nodes),
CreateTime: f.CreateTime.Format("2006-01-02 15:04:05"),
Nodes: make([]approvalNodePayload, 0, len(nodes)),
}
edgeNodes := make([]flowEdgeNode, 0, len(nodes))
for _, n := range nodes {
view.Nodes = append(view.Nodes, approvalNodePayload{
NodeKey: n.NodeKey,
NodeName: n.NodeName,
NodeType: n.NodeType,
ApproverType: n.ApproverType,
ApproverIDs: parseIDSlice(n.ApproverIDs),
MultiMode: n.MultiMode,
AllowReturn: n.AllowReturn,
TimeoutHours: n.TimeoutHours,
Sort: n.Sort,
PosX: n.PosX,
PosY: n.PosY,
ConditionExpr: n.ConditionExpr,
})
edgeNodes = append(edgeNodes, flowEdgeNode{NodeKey: n.NodeKey, NextKeys: n.NextKeys, Condition: n.ConditionExpr})
}
view.Edges = rebuildEdges(edgeNodes)
return view
}
// FlowSave POST /platform/approval/flow/save
// 新建或更新流程;节点采用「全量覆盖」语义(先软删旧节点,再按传入顺序重建)。
func (c *PlatformApprovalController) FlowSave() {
if _, err := requirePlatform(&c.Controller); err != nil {
jsonErr(&c.Controller, 401, 401, err.Error())
return
}
var p approvalFlowSave
if err := c.readApprovalBody(&p); err != nil {
jsonErr(&c.Controller, 400, 400, err.Error())
return
}
if strings.TrimSpace(p.FlowName) == "" {
jsonErr(&c.Controller, 400, 400, "流程名称不能为空")
return
}
// 分类(module_code)与单据类型(biz_type)对齐:二者留空时互补,至少其一非空
if strings.TrimSpace(p.ModuleCode) == "" {
p.ModuleCode = p.BizType
}
if strings.TrimSpace(p.BizType) == "" {
p.BizType = p.ModuleCode
}
if strings.TrimSpace(p.BizType) == "" {
jsonErr(&c.Controller, 400, 400, "请选择所属分类")
return
}
if err := validateApprovalNodes(p.Nodes); err != nil {
jsonErr(&c.Controller, 400, 400, err.Error())
return
}
if err := validateApprovalGraph(p.Nodes, p.Edges); err != nil {
jsonErr(&c.Controller, 400, 400, err.Error())
return
}
g := buildGraph(p.Nodes, p.Edges)
now := time.Now()
o := models.Orm
var flowID uint64
var flowCode string
if p.ID > 0 {
var exist models.PlatformApprovalFlow
if err := o.QueryTable(new(models.PlatformApprovalFlow)).
Filter("id", p.ID).Filter("is_deleted", 0).One(&exist); err != nil {
jsonErr(&c.Controller, 404, 404, "流程不存在")
return
}
flowID = p.ID
flowCode = exist.FlowCode
_, err := o.QueryTable(new(models.PlatformApprovalFlow)).Filter("id", flowID).Update(orm.Params{
"module_code": p.ModuleCode,
"biz_type": p.BizType,
"flow_name": p.FlowName,
"description": p.Description,
"is_default": p.IsDefault,
"status": p.Status,
"update_time": now,
})
if err != nil {
jsonErr(&c.Controller, 500, 500, "保存失败: "+err.Error())
return
}
} else {
row := models.PlatformApprovalFlow{
ModuleCode: p.ModuleCode,
BizType: p.BizType,
FlowName: p.FlowName,
Description: p.Description,
IsDefault: p.IsDefault,
Status: p.Status,
CreateTime: now,
}
id, err := o.Insert(&row)
if err != nil {
jsonErr(&c.Controller, 500, 500, "保存失败: "+err.Error())
return
}
flowID = uint64(id)
flowCode = genApprovalFlowCode(flowID)
_, _ = o.QueryTable(new(models.PlatformApprovalFlow)).Filter("id", flowID).
Update(orm.Params{"flow_code": flowCode})
}
// 默认流程唯一:同 biz_type 下只允许一条 is_default=1
if p.IsDefault == 1 {
_, _ = o.QueryTable(new(models.PlatformApprovalFlow)).
Filter("biz_type", p.BizType).Filter("is_deleted", 0).
Exclude("id", flowID).Update(orm.Params{"is_default": 0, "update_time": now})
}
// 覆盖节点:软删旧节点后按 sort 重建
_, _ = o.QueryTable(new(models.PlatformApprovalNode)).
Filter("flow_id", flowID).Filter("is_deleted", 0).
Update(orm.Params{"is_deleted": 1, "delete_time": now})
for i, nd := range p.Nodes {
sort := nd.Sort
if sort == 0 {
sort = i + 1
}
nodeKey := strings.TrimSpace(nd.NodeKey)
if nodeKey == "" {
nodeKey = fmt.Sprintf("n%d", sort)
}
nodeType := nd.NodeType
if nodeType == 0 {
nodeType = models.ApprovalNodeApproval
}
multi := nd.MultiMode
if multi != models.ApprovalMultiAll {
multi = models.ApprovalMultiAny
}
timeout := nd.TimeoutHours
if timeout <= 0 {
timeout = 48
}
node := models.PlatformApprovalNode{
FlowID: flowID,
NodeKey: nodeKey,
NodeName: strings.TrimSpace(nd.NodeName),
NodeType: nodeType,
ApproverType: nd.ApproverType,
ApproverIDs: joinIDs(nd.ApproverIDs),
MultiMode: multi,
AllowReturn: nd.AllowReturn,
TimeoutHours: timeout,
Sort: sort,
PosX: nd.PosX,
PosY: nd.PosY,
PrevKeys: strings.Join(g.prev[nodeKey], ","),
NextKeys: strings.Join(g.next[nodeKey], ","),
ConditionExpr: g.cond[nodeKey],
CreateTime: now,
}
if _, err := o.Insert(&node); err != nil {
jsonErr(&c.Controller, 500, 500, "保存节点失败: "+err.Error())
return
}
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "保存成功", "data": map[string]interface{}{"id": flowID, "flow_code": flowCode}}
_ = c.ServeJSON()
}
// Pick GET /platform/approval/flow/pick
// 轻量清单:按分类返回启用流程的 {flow_code, flow_name},供其它功能“选/填编号调用”。
func (c *PlatformApprovalController) Pick() {
if _, err := requirePlatform(&c.Controller); err != nil {
jsonErr(&c.Controller, 401, 401, err.Error())
return
}
module := strings.TrimSpace(c.GetString("module"))
if module == "" {
module = strings.TrimSpace(c.GetString("biz_type"))
}
qs := models.Orm.QueryTable(new(models.PlatformApprovalFlow)).
Filter("is_deleted", 0).Filter("status", 1)
if module != "" {
qs = qs.Filter("biz_type", module)
}
var rows []models.PlatformApprovalFlow
_, _ = qs.OrderBy("biz_type", "id").All(&rows)
list := make([]map[string]interface{}, 0, len(rows))
for _, r := range rows {
list = append(list, map[string]interface{}{
"id": r.ID,
"module_code": r.ModuleCode,
"biz_type": r.BizType,
"flow_code": r.FlowCode,
"flow_name": r.FlowName,
})
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "ok", "data": list}
_ = c.ServeJSON()
}
// FlowDelete DELETE /platform/approval/flow/delete/:id
// 软删流程及其节点。存在在途实例时禁止删除,避免审批中断。
func (c *PlatformApprovalController) FlowDelete() {
if _, err := requirePlatform(&c.Controller); err != nil {
jsonErr(&c.Controller, 401, 401, err.Error())
return
}
id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
var f models.PlatformApprovalFlow
if err := models.Orm.QueryTable(new(models.PlatformApprovalFlow)).
Filter("id", id).Filter("is_deleted", 0).One(&f); err != nil {
jsonErr(&c.Controller, 404, 404, "流程不存在")
return
}
running, _ := models.Orm.QueryTable(new(models.ApprovalInstance)).
Filter("flow_id", id).Filter("scope", models.ApprovalScopePlatform).
Filter("status", models.ApprovalInstanceRunning).Filter("is_deleted", 0).Count()
if running > 0 {
jsonErr(&c.Controller, 400, 400, fmt.Sprintf("该流程还有 %d 条在途审批,暂不能删除", running))
return
}
now := time.Now()
_, _ = models.Orm.QueryTable(new(models.PlatformApprovalFlow)).Filter("id", id).
Update(orm.Params{"is_deleted": 1, "delete_time": now, "update_time": now})
_, _ = models.Orm.QueryTable(new(models.PlatformApprovalNode)).Filter("flow_id", id).
Update(orm.Params{"is_deleted": 1, "delete_time": now})
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "已删除"}
_ = c.ServeJSON()
}
// Options GET /platform/approval/options
// 编排界面「选审批人」用:返回平台角色(cid=1)与平台用户列表。
func (c *PlatformApprovalController) Options() {
if _, err := requirePlatform(&c.Controller); err != nil {
jsonErr(&c.Controller, 401, 401, err.Error())
return
}
var roles []models.AdminRole
_, _ = models.Orm.QueryTable(new(models.AdminRole)).
Filter("cid", 1).Filter("status", 1).Filter("delete_time__isnull", true).
OrderBy("id").All(&roles)
roleList := make([]map[string]interface{}, 0, len(roles))
for _, r := range roles {
roleList = append(roleList, map[string]interface{}{"id": r.ID, "name": r.Name})
}
var users []models.AdminUser
_, _ = models.Orm.QueryTable(new(models.AdminUser)).
Filter("status", 1).Filter("delete_time__isnull", true).
OrderBy("id").All(&users)
userList := make([]map[string]interface{}, 0, len(users))
for _, u := range users {
userList = append(userList, map[string]interface{}{
"id": u.ID,
"name": approverDisplayName(u.Name, u.Account),
"role": u.RoleID,
})
}
// 分类(复用功能模块):Web 可见、启用、未删除
var mods []models.SystemModules
_, _ = models.Orm.QueryTable(new(models.SystemModules)).
Filter("delete_time__isnull", true).Filter("status", 1).Filter("is_show", 1).
OrderBy("sort", "id").All(&mods)
mods = services.FilterModulesByPlatform(mods, models.PlatformWeb)
moduleList := make([]map[string]interface{}, 0, len(mods))
for _, m := range mods {
moduleList = append(moduleList, map[string]interface{}{
"code": m.Code, "name": m.Name, "enabled": true,
})
}
c.Data["json"] = map[string]interface{}{
"code": 200, "msg": "ok",
"data": map[string]interface{}{"roles": roleList, "users": userList, "modules": moduleList},
}
_ = c.ServeJSON()
}
// BizTypes GET /platform/approval/biz-types
// 可绑定业务类型字典(当期平台侧为「域名申请」),供配置界面顶级下拉。
func (c *PlatformApprovalController) BizTypes() {
if _, err := requirePlatform(&c.Controller); err != nil {
jsonErr(&c.Controller, 401, 401, err.Error())
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "ok", "data": []map[string]interface{}{
{"value": models.ApprovalBizDomain, "label": "域名申请"},
}}
_ = c.ServeJSON()
}
// AvailableFlows GET /platform/approval/flow/available?biz_type=domain
// 业务侧“发起前拉可用流”:返回当前 biz_type 启用的平台流程列表。
// 平台无部门概念,userOrgID 固定为 0(引擎内平台侧不做部门过滤)。
func (c *PlatformApprovalController) AvailableFlows() {
if _, err := requirePlatform(&c.Controller); err != nil {
jsonErr(&c.Controller, 401, 401, err.Error())
return
}
bizType := strings.TrimSpace(c.GetString("biz_type"))
if bizType == "" {
jsonErr(&c.Controller, 400, 400, "缺少 biz_type 参数")
return
}
list, err := approvalEngine.ListAvailableFlows(models.ApprovalScopePlatform, 0, bizType, 0)
if err != nil {
jsonErr(&c.Controller, 500, 500, "查询失败: "+err.Error())
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "ok", "data": list}
_ = c.ServeJSON()
}
// ============================ 辅助函数 ============================
// approverDisplayName 优先姓名,回退账号(AdminUser.Account 为普通字符串)
func approverDisplayName(name *string, account string) string {
if name != nil && strings.TrimSpace(*name) != "" {
return strings.TrimSpace(*name)
}
return strings.TrimSpace(account)
}
// validateApprovalNodes 校验审批节点:必须选定审批人来源且非空(开始/结束/判断/通知跳过)
func validateApprovalNodes(nodes []approvalNodePayload) error {
for i, nd := range nodes {
if nd.NodeType == models.ApprovalNodeStart || nd.NodeType == models.ApprovalNodeEnd ||
nd.NodeType == models.ApprovalNodeCondition {
continue
}
if strings.TrimSpace(nd.NodeName) == "" {
return fmt.Errorf("第 %d 个节点名称不能为空", i+1)
}
if nd.ApproverType != models.ApprovalApproverUser && nd.ApproverType != models.ApprovalApproverRole {
return fmt.Errorf("节点「%s」审批人类型非法(仅支持指定用户/指定角色)", nd.NodeName)
}
if len(nd.ApproverIDs) == 0 {
return fmt.Errorf("节点「%s」未选择审批人", nd.NodeName)
}
}
return nil
}
// joinIDs 将 ID 数组拼成逗号分隔串(存库口径)
func joinIDs(ids []uint64) string {
parts := make([]string, 0, len(ids))
for _, id := range ids {
if id > 0 {
parts = append(parts, strconv.FormatUint(id, 10))
}
}
return strings.Join(parts, ",")
}
// parseIDSlice 解析逗号分隔 ID 串为数组
func parseIDSlice(raw string) []uint64 {
raw = strings.TrimSpace(raw)
if raw == "" {
return nil
}
out := make([]uint64, 0)
for _, p := range strings.Split(raw, ",") {
if v, err := strconv.ParseUint(strings.TrimSpace(p), 10, 64); err == nil && v > 0 {
out = append(out, v)
}
}
return out
}
// ============================ 图(连线)辅助 ============================
// flowEdgeNode 连线重建输入:节点标识 + 出边(next_keys) + 本节点进入条件
type flowEdgeNode struct {
NodeKey string
NextKeys string
Condition string
}
// splitKeyList 拆分逗号分隔 node_key 列表
func splitKeyList(raw string) []string {
out := make([]string, 0, 4)
for _, p := range strings.Split(raw, ",") {
if s := strings.TrimSpace(p); s != "" {
out = append(out, s)
}
}
return out
}
// containsStr 列表是否包含某字符串
func containsStr(list []string, s string) bool {
for _, x := range list {
if x == s {
return true
}
}
return false
}
// approvalGraph 由画布连线反推的邻接与条件
type approvalGraph struct {
next map[string][]string
prev map[string][]string
cond map[string]string
}
// buildGraph 由 edges 反推每个节点的 prev_keys/next_keys 与进入条件(条件落到目标节点)
func buildGraph(nodes []approvalNodePayload, edges []approvalEdge) approvalGraph {
g := approvalGraph{next: map[string][]string{}, prev: map[string][]string{}, cond: map[string]string{}}
for _, e := range edges {
f := strings.TrimSpace(e.From)
t := strings.TrimSpace(e.To)
if f == "" || t == "" || f == t {
continue
}
if !containsStr(g.next[f], t) {
g.next[f] = append(g.next[f], t)
}
if !containsStr(g.prev[t], f) {
g.prev[t] = append(g.prev[t], f)
}
if c := strings.TrimSpace(e.Condition); c != "" {
g.cond[t] = c
}
}
return g
}
// rebuildEdges 由节点 next_keys 邻接重建画布连线;连线条件取目标节点 condition_expr
func rebuildEdges(nodes []flowEdgeNode) []approvalEdge {
condByKey := make(map[string]string, len(nodes))
for _, n := range nodes {
condByKey[n.NodeKey] = n.Condition
}
edges := make([]approvalEdge, 0)
for _, n := range nodes {
for _, to := range splitKeyList(n.NextKeys) {
edges = append(edges, approvalEdge{From: n.NodeKey, To: to, Condition: condByKey[to]})
}
}
return edges
}
// validateApprovalGraph 校验画布图的完整性:单一开始、审批节点≥1、判断节点出边≥2、全部可达、连线端点有效
func validateApprovalGraph(nodes []approvalNodePayload, edges []approvalEdge) error {
if len(nodes) == 0 {
return fmt.Errorf("流程至少需包含开始与审批节点")
}
keySet := map[string]bool{}
var startCount, approvalCount int
var condKeys []string
var startKey string
for _, n := range nodes {
k := strings.TrimSpace(n.NodeKey)
if k == "" {
return fmt.Errorf("存在缺少标识的节点,请重新连线后再保存")
}
if keySet[k] {
return fmt.Errorf("节点标识重复:%s", k)
}
keySet[k] = true
switch n.NodeType {
case models.ApprovalNodeStart:
startCount++
startKey = k
case models.ApprovalNodeApproval:
approvalCount++
case models.ApprovalNodeCondition:
condKeys = append(condKeys, k)
}
}
if startCount != 1 {
return fmt.Errorf("流程需有且仅有一个「开始」节点")
}
if approvalCount < 1 {
return fmt.Errorf("流程需至少包含一个「审批」节点")
}
nextAdj := map[string][]string{}
for _, e := range edges {
f := strings.TrimSpace(e.From)
t := strings.TrimSpace(e.To)
if !keySet[f] || !keySet[t] {
return fmt.Errorf("存在连向未知节点的连线,请检查画布")
}
nextAdj[f] = append(nextAdj[f], t)
}
for _, k := range condKeys {
if len(nextAdj[k]) < 2 {
return fmt.Errorf("判断节点需至少两条分支连线")
}
}
visited := map[string]bool{}
queue := []string{startKey}
for len(queue) > 0 {
cur := queue[0]
queue = queue[1:]
if visited[cur] {
continue
}
visited[cur] = true
queue = append(queue, nextAdj[cur]...)
}
for _, n := range nodes {
if !visited[strings.TrimSpace(n.NodeKey)] {
return fmt.Errorf("节点「%s」未接入流程,请检查连线", strings.TrimSpace(n.NodeName))
}
}
return nil
}
// genApprovalFlowCode 由自增 id 生成审批流编号(全局唯一、供各程序按编号发起)
func genApprovalFlowCode(id uint64) string {
return fmt.Sprintf("FL%06d", id)
}