Files
yunzerwebsiteallinone/go/controllers/backend_approval.go
T
2026-10-19 01:13:17 +08:00

632 lines
22 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"
"server/pkg/identity"
approvalEngine "server/services/approval"
"github.com/beego/beego/v2/client/orm"
beego "github.com/beego/beego/v2/server/web"
)
// BackendApprovalController 租户端审批流配置(流程头 + 节点编排)。
//
// 与平台端差异:
// - 数据按登录租户隔离(tid 取自 JWT,写入/查询均带 tid);
// - 「功能」下拉取当前租户已开通的模块(SystemModules),流程 module_code / biz_type 均绑定模块 code,
// 实现「选功能 → 命名 → 把审批流引用到该功能」;
// - 审批人取本租户主体:租户用户 / 租户角色 / 部门主管 / 职位 / 发起人 / 发起人主管(引擎支持 6 种)。
type BackendApprovalController struct {
beego.Controller
}
// readTenantBody 读取并解析 JSON 请求体
func (c *BackendApprovalController) readTenantBody(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
}
// tenantFlowList GET /backend/approval/flow/list?module=oa
// 当前租户的流程列表;按分类(module_code)过滤,附带节点数。
func (c *BackendApprovalController) FlowList() {
claims, err := requireBackend(&c.Controller)
if err != nil {
code := identity.Status(err)
jsonErr(&c.Controller, code, code, err.Error())
return
}
tid := uint64(claims.TenantId)
module := strings.TrimSpace(c.GetString("module"))
qs := models.Orm.QueryTable(new(models.TenantApprovalFlow)).
Filter("tid", tid).Filter("is_deleted", 0)
if module != "" {
qs = qs.Filter("module_code", module)
}
var rows []models.TenantApprovalFlow
_, _ = qs.OrderBy("-is_default", "id").All(&rows)
list := make([]approvalFlowView, 0, len(rows))
for _, r := range rows {
cnt, _ := models.Orm.QueryTable(new(models.TenantApprovalNode)).
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"),
ApplyDeptIDs: parseIDSlice(r.ApplyDeptIDs),
})
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "ok", "data": list}
_ = c.ServeJSON()
}
// FlowDetail GET /backend/approval/flow/detail/:id
// 返回本租户某流程及其全部节点(按 sort 升序),供编排界面回显。
func (c *BackendApprovalController) FlowDetail() {
claims, err := requireBackend(&c.Controller)
if err != nil {
code := identity.Status(err)
jsonErr(&c.Controller, code, code, err.Error())
return
}
tid := uint64(claims.TenantId)
id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
var f models.TenantApprovalFlow
if err := models.Orm.QueryTable(new(models.TenantApprovalFlow)).
Filter("id", id).Filter("tid", tid).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": tenantFlowToView(f)}
_ = c.ServeJSON()
}
// tenantFlowToView 组装租户流程视图(含节点与连线)
func tenantFlowToView(f models.TenantApprovalFlow) approvalFlowView {
var nodes []models.TenantApprovalNode
_, _ = models.Orm.QueryTable(new(models.TenantApprovalNode)).
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"),
ApplyDeptIDs: parseIDSlice(f.ApplyDeptIDs),
Nodes: make([]approvalNodePayload, 0, len(nodes)),
}
edgeNodes := make([]flowEdgeNode, 0, len(nodes))
for _, n := range nodes {
approverIDs := parseIDSlice(n.ApproverIDs)
// 通知节点多类型格式:"类型:ID" CSV,原样下发给前端回显
var notifyTargets []string
if n.NodeType == models.ApprovalNodeNotify && strings.Contains(n.ApproverIDs, ":") {
for _, part := range strings.Split(n.ApproverIDs, ",") {
if trimmed := strings.TrimSpace(part); trimmed != "" {
notifyTargets = append(notifyTargets, trimmed)
}
}
}
view.Nodes = append(view.Nodes, approvalNodePayload{
NodeKey: n.NodeKey,
NodeName: n.NodeName,
NodeType: n.NodeType,
ApproverType: n.ApproverType,
ApproverIDs: approverIDs,
NotifyTargets: notifyTargets,
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 /backend/approval/flow/save
// 新建或更新本租户流程;节点采用「全量覆盖」语义(先软删旧节点,再按传入顺序重建)。
func (c *BackendApprovalController) FlowSave() {
claims, err := requireBackend(&c.Controller)
if err != nil {
code := identity.Status(err)
jsonErr(&c.Controller, code, code, err.Error())
return
}
tid := uint64(claims.TenantId)
var p approvalFlowSave
if err := c.readTenantBody(&p); err != nil {
jsonErr(&c.Controller, 400, 400, err.Error())
return
}
if strings.TrimSpace(p.FlowName) == "" {
jsonErr(&c.Controller, 400, 400, "流程名称不能为空")
return
}
if strings.TrimSpace(p.ModuleCode) == "" {
jsonErr(&c.Controller, 400, 400, "请选择要配置的分类")
return
}
// 分类与单据类型对齐:运行期按 biz_type=分类 code 命中默认流
if strings.TrimSpace(p.BizType) == "" {
p.BizType = p.ModuleCode
}
if err := validateTenantApprovalNodes(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()
// 适用部门写入前校验:需全部属于当前租户(空列表=全公司通用,直接放行)
if len(p.ApplyDeptIDs) > 0 {
count, _ := models.Orm.QueryTable(new(models.BackendOrganization)).
Filter("tid", tid).Filter("id__in", p.ApplyDeptIDs).Filter("delete_time__isnull", true).
Count()
if int(count) != len(p.ApplyDeptIDs) {
jsonErr(&c.Controller, 400, 400, "适用部门包含非本租户组织,请重新选择")
return
}
}
applyDeptCSV := joinIDs(p.ApplyDeptIDs)
o := models.Orm
var flowID uint64
var flowCode string
if p.ID > 0 {
var exist models.TenantApprovalFlow
if err := o.QueryTable(new(models.TenantApprovalFlow)).
Filter("id", p.ID).Filter("tid", tid).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.TenantApprovalFlow)).
Filter("id", flowID).Filter("tid", tid).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,
"apply_dept_ids": applyDeptCSV,
"update_time": now,
})
if err != nil {
jsonErr(&c.Controller, 500, 500, "保存失败: "+err.Error())
return
}
} else {
row := models.TenantApprovalFlow{
Tid: tid,
ModuleCode: p.ModuleCode,
BizType: p.BizType,
FlowName: p.FlowName,
Description: p.Description,
IsDefault: p.IsDefault,
Status: p.Status,
ApplyDeptIDs: applyDeptCSV,
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.TenantApprovalFlow)).Filter("id", flowID).Filter("tid", tid).
Update(orm.Params{"flow_code": flowCode})
}
// 默认流程唯一:同租户同单据类型下只允许一条 is_default=1
if p.IsDefault == 1 {
_, _ = o.QueryTable(new(models.TenantApprovalFlow)).
Filter("tid", tid).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.TenantApprovalNode)).
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
}
// 通知节点多类型:以 "类型:ID" CSV 落库到 approver_ids,引擎按类型分组解析
approverCSV := joinIDs(nd.ApproverIDs)
if nodeType == models.ApprovalNodeNotify && len(nd.NotifyTargets) > 0 {
approverCSV = strings.Join(nd.NotifyTargets, ",")
}
node := models.TenantApprovalNode{
FlowID: flowID,
Tid: tid,
NodeKey: nodeKey,
NodeName: strings.TrimSpace(nd.NodeName),
NodeType: nodeType,
ApproverType: nd.ApproverType,
ApproverIDs: approverCSV,
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 /backend/approval/flow/pick
// 轻量清单:按分类返回本租户启用流程的 {flow_code, flow_name},供其它功能“选/填编号调用”。
func (c *BackendApprovalController) Pick() {
claims, err := requireBackend(&c.Controller)
if err != nil {
code := identity.Status(err)
jsonErr(&c.Controller, code, code, err.Error())
return
}
tid := uint64(claims.TenantId)
module := strings.TrimSpace(c.GetString("module"))
qs := models.Orm.QueryTable(new(models.TenantApprovalFlow)).
Filter("tid", tid).Filter("is_deleted", 0).Filter("status", 1)
if module != "" {
qs = qs.Filter("module_code", module)
}
var rows []models.TenantApprovalFlow
_, _ = qs.OrderBy("module_code", "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,
"flow_code": r.FlowCode,
"flow_name": r.FlowName,
})
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "ok", "data": list}
_ = c.ServeJSON()
}
// FlowDelete DELETE /backend/approval/flow/delete/:id
// 软删本租户流程及其节点。存在在途实例时禁止删除,避免审批中断。
func (c *BackendApprovalController) FlowDelete() {
claims, err := requireBackend(&c.Controller)
if err != nil {
code := identity.Status(err)
jsonErr(&c.Controller, code, code, err.Error())
return
}
tid := uint64(claims.TenantId)
id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
var f models.TenantApprovalFlow
if err := models.Orm.QueryTable(new(models.TenantApprovalFlow)).
Filter("id", id).Filter("tid", tid).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.ApprovalScopeTenant).
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.TenantApprovalFlow)).Filter("id", id).Filter("tid", tid).
Update(orm.Params{"is_deleted": 1, "delete_time": now, "update_time": now})
_, _ = models.Orm.QueryTable(new(models.TenantApprovalNode)).Filter("flow_id", id).Filter("tid", tid).
Update(orm.Params{"is_deleted": 1, "delete_time": now})
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "已删除"}
_ = c.ServeJSON()
}
// FlowPerm POST /backend/approval/flow/perm
// 审批中心「审批权限」设置:只更新流程权限相关字段(适用部门 / 是否默认 / 启用状态),
// 不动节点编排。设为默认流时自动取消同分类(biz_type)下其它流程的默认标记。
func (c *BackendApprovalController) FlowPerm() {
claims, err := requireBackend(&c.Controller)
if err != nil {
code := identity.Status(err)
jsonErr(&c.Controller, code, code, err.Error())
return
}
tid := uint64(claims.TenantId)
var payload struct {
ID uint64 `json:"id"`
ApplyDeptIDs []uint64 `json:"apply_dept_ids"`
IsDefault int8 `json:"is_default"`
Status int8 `json:"status"`
}
if err := c.readTenantBody(&payload); err != nil {
jsonErr(&c.Controller, 400, 400, err.Error())
return
}
if payload.ID == 0 {
jsonErr(&c.Controller, 400, 400, "无效ID")
return
}
var f models.TenantApprovalFlow
if err := models.Orm.QueryTable(new(models.TenantApprovalFlow)).
Filter("id", payload.ID).Filter("tid", tid).Filter("is_deleted", 0).One(&f); err != nil {
jsonErr(&c.Controller, 404, 404, "流程不存在")
return
}
now := time.Now()
params := orm.Params{
"apply_dept_ids": joinUintCSV(payload.ApplyDeptIDs),
"is_default": payload.IsDefault,
"status": payload.Status,
"update_time": now,
}
if _, err := models.Orm.QueryTable(new(models.TenantApprovalFlow)).
Filter("id", payload.ID).Filter("tid", tid).Update(params); err != nil {
jsonErr(&c.Controller, 500, 500, "保存失败")
return
}
// 设为默认流:取消同分类其它流程的默认标记,保证每个分类只有一条默认流
if payload.IsDefault == 1 {
models.Orm.QueryTable(new(models.TenantApprovalFlow)).
Filter("tid", tid).Filter("biz_type", f.BizType).
Filter("id__ne", payload.ID).Filter("is_deleted", 0).
Update(orm.Params{"is_default": 0, "update_time": now})
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "保存成功"}
_ = c.ServeJSON()
}
// joinUintCSV 把 ID 列表拼成逗号分隔串(与 parseIDSlice 互逆)。
func joinUintCSV(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, ",")
}
// Options GET /backend/approval/options
// 编排界面数据源:当前租户的功能模块 + 审批人候选(用户 / 角色 / 部门 / 职位)。
func (c *BackendApprovalController) Options() {
claims, err := requireBackend(&c.Controller)
if err != nil {
code := identity.Status(err)
jsonErr(&c.Controller, code, code, err.Error())
return
}
tid := uint64(claims.TenantId)
// 功能模块:状态可用、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)
allowed := services.GetTenantModuleCodes(tid)
moduleList := make([]map[string]interface{}, 0, len(mods))
for _, m := range mods {
enabled := true
if len(allowed) > 0 {
enabled = allowed[services.NormalizeModuleCode(m.Code)]
}
moduleList = append(moduleList, map[string]interface{}{
"code": m.Code, "name": m.Name, "enabled": enabled,
})
}
// 租户用户
var tu []models.SystemTenantUser
_, _ = models.Orm.QueryTable(new(models.SystemTenantUser)).
Filter("tid", tid).Filter("status", 1).Filter("delete_time__isnull", true).
OrderBy("id").All(&tu)
userList := make([]map[string]interface{}, 0, len(tu))
for _, u := range tu {
userList = append(userList, map[string]interface{}{
"id": u.Uid, "name": firstNonEmptyStr(u.Name, u.Account),
})
}
// 租户角色(cid=2,含平台预置 tenant_id=0 与本租户自定义)
var ar []models.AdminRole
_, _ = models.Orm.QueryTable(new(models.AdminRole)).
Filter("cid", 2).Filter("tenant_id__in", []uint64{0, tid}).
Filter("status", 1).Filter("delete_time__isnull", true).
OrderBy("id").All(&ar)
roleList := make([]map[string]interface{}, 0, len(ar))
for _, r := range ar {
roleList = append(roleList, map[string]interface{}{"id": r.ID, "name": r.Name})
}
// 部门 / 职位
var og []models.BackendOrganization
_, _ = models.Orm.QueryTable(new(models.BackendOrganization)).
Filter("tid", tid).Filter("status", 1).Filter("delete_time__isnull", true).
OrderBy("sort", "id").All(&og)
orgList := make([]map[string]interface{}, 0, len(og))
for _, o := range og {
orgList = append(orgList, map[string]interface{}{
"id": o.ID, "name": o.OrgName,
"parent_id": o.ParentID, "is_company": o.IsCompany,
})
}
var ps []models.BackendPosition
_, _ = models.Orm.QueryTable(new(models.BackendPosition)).
Filter("tid", tid).Filter("status", 1).Filter("delete_time__isnull", true).
OrderBy("sort", "id").All(&ps)
posList := make([]map[string]interface{}, 0, len(ps))
for _, p := range ps {
posList = append(posList, map[string]interface{}{"id": p.ID, "name": p.PositionName})
}
c.Data["json"] = map[string]interface{}{
"code": 200, "msg": "ok",
"data": map[string]interface{}{
"modules": moduleList,
"users": userList,
"roles": roleList,
"orgs": orgList,
"positions": posList,
},
}
_ = c.ServeJSON()
}
// AvailableFlows GET /backend/approval/flow/available?biz_type=reimburse
// 业务侧“发起前拉可用流”:基于当前登录用户的部门,过滤启用的流程。
// 前端基于返回结果判定:空则提示配置;一条自动带入;多条下拉选。
func (c *BackendApprovalController) AvailableFlows() {
claims, err := requireBackend(&c.Controller)
if err != nil {
code := identity.Status(err)
jsonErr(&c.Controller, code, code, err.Error())
return
}
tid := uint64(claims.TenantId)
bizType := strings.TrimSpace(c.GetString("biz_type"))
if bizType == "" {
jsonErr(&c.Controller, 400, 400, "缺少 biz_type 参数")
return
}
// 取当前登录用户的部门(未分配时 OrgID=0,仅命中“全公司通用”流)
var tu models.SystemTenantUser
var userOrgID uint64
if e := models.Orm.QueryTable(new(models.SystemTenantUser)).
Filter("tid", tid).Filter("uid", claims.UserID).Filter("delete_time__isnull", true).
One(&tu); e == nil {
userOrgID = tu.OrgID
}
list, err := approvalEngine.ListAvailableFlows(models.ApprovalScopeTenant, tid, bizType, userOrgID)
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()
}
// ============================ 辅助函数 ============================
// firstNonEmptyStr 取字符串指针/普通字符串中第一个非空值
func firstNonEmptyStr(vals ...*string) string {
for _, v := range vals {
if v != nil && strings.TrimSpace(*v) != "" {
return strings.TrimSpace(*v)
}
}
return ""
}
// validateTenantApprovalNodes 校验租户节点:审批/条件节点须合法选择审批人来源。
// 租户侧支持 6 种审批人类型:1用户 2角色 3部门主管 4职位 5发起人本人 6发起人主管。
func validateTenantApprovalNodes(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)
}
// 通知节点多类型格式:校验每条 "类型:ID",自动定位类型(5/6)无需对象
if nd.NodeType == models.ApprovalNodeNotify && len(nd.NotifyTargets) > 0 {
for _, tg := range nd.NotifyTargets {
kv := strings.SplitN(strings.TrimSpace(tg), ":", 2)
if len(kv) != 2 {
return fmt.Errorf("节点「%s」通知对象格式非法", nd.NodeName)
}
t, err := strconv.Atoi(strings.TrimSpace(kv[0]))
if err != nil || t < int(models.ApprovalApproverUser) || t > int(models.ApprovalApproverInitLeader) {
return fmt.Errorf("节点「%s」通知类型非法", nd.NodeName)
}
if t != int(models.ApprovalApproverInitiator) && t != int(models.ApprovalApproverInitLeader) {
id, err := strconv.ParseUint(strings.TrimSpace(kv[1]), 10, 64)
if err != nil || id == 0 {
return fmt.Errorf("节点「%s」未选择通知对象", nd.NodeName)
}
}
}
continue
}
if nd.ApproverType < models.ApprovalApproverUser || nd.ApproverType > models.ApprovalApproverInitLeader {
return fmt.Errorf("节点「%s」审批人类型非法", nd.NodeName)
}
// 发起人相关类型无需指定对象;其余必须选择非空来源
needIDs := nd.ApproverType != models.ApprovalApproverInitiator &&
nd.ApproverType != models.ApprovalApproverInitLeader
if needIDs && len(nd.ApproverIDs) == 0 {
return fmt.Errorf("节点「%s」未选择审批人", nd.NodeName)
}
}
return nil
}