556 lines
19 KiB
Go
556 lines
19 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"
|
||
)
|
||
|
||
// 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 {
|
||
jsonErr(&c.Controller, 401, 401, 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 {
|
||
jsonErr(&c.Controller, 401, 401, 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 {
|
||
jsonErr(&c.Controller, 401, 401, 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 {
|
||
jsonErr(&c.Controller, 401, 401, 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 {
|
||
jsonErr(&c.Controller, 401, 401, 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()
|
||
}
|
||
|
||
// Options GET /backend/approval/options
|
||
// 编排界面数据源:当前租户的功能模块 + 审批人候选(用户 / 角色 / 部门 / 职位)。
|
||
func (c *BackendApprovalController) Options() {
|
||
claims, err := requireBackend(&c.Controller)
|
||
if err != nil {
|
||
jsonErr(&c.Controller, 401, 401, 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 {
|
||
jsonErr(&c.Controller, 401, 401, 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
|
||
}
|