691 lines
23 KiB
Go
691 lines
23 KiB
Go
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)
|
||
}
|