// Package approval 提供自定义审批流的运行时引擎。 // // 设计说明: // - 配置侧(flow/node)与运行时(instance/task)分离:配置由租户/平台编辑, // 运行时实例创建后不可编辑,节点信息以任务表快照为准,避免改配置影响在途单据; // - 租户侧与平台侧共用本引擎,通过 scope 区分数据来源: // 租户侧审批人取租户用户/租户角色/组织主管/职位, // 平台侧审批人取平台用户/平台角色(平台账号不归属租户); // - 当期只实现顺序链流转(按 sort 升序),prev_keys / next_keys / condition_expr // 为条件分支预留,后续扩展无需改表。 package approval import ( "encoding/json" "fmt" "sort" "strconv" "strings" "time" "server/models" "github.com/beego/beego/v2/client/orm" ) // Approver 展开后的单个审批人 type Approver struct { ID uint64 Name string } // SubmitInput 提交审批入参 type SubmitInput struct { Scope int8 // 1租户侧 2平台侧 Tid uint64 // 租户ID(租户侧=本租户) BizTid uint64 // 业务来源租户(平台侧必填) BizType string FlowCode string // 按审批流编号发起(优先于 BizType 默认流) BizID uint64 BizTitle string SubmitUID uint64 // 发起人ID(租户侧为 uid,平台侧为 AdminUser.id) SubmitName string DeptID uint64 // 发起人部门 FlowID uint64 // 指定流程;0 表示按 FlowCode/BizType 取流程 FormData map[string]interface{} // 发起表单数据,供条件分支求值 } // Node 流程节点(两侧统一结构,承载图连线) type Node struct { ID uint64 NodeKey string NodeName string NodeType int8 ApproverType int8 ApproverIDs string MultiMode int8 AllowReturn int8 TimeoutHours int Sort int PrevKeys string // 前置节点 node_key 列表(逗号分隔) NextKeys string // 后继节点 node_key 列表(逗号分隔) ConditionExpr string // 作为条件网关某出边目标时的进入条件 } // Flow 流程定义(两侧统一结构,含图索引) type Flow struct { ID uint64 BizType string FlowName string IsDefault int8 Status int8 Nodes []Node ByKey map[string]*Node StartKey string } // ErrNoFlow 表示未找到可用流程 type ErrNoFlow struct { BizType string } func (e *ErrNoFlow) Error() string { return "未找到适用于该单据的审批流程" } // ErrNoApprover 表示节点未能解析出任何审批人 type ErrNoApprover struct { NodeName string NodeKey string } func (e *ErrNoApprover) Error() string { return fmt.Sprintf("节点「%s」未解析到有效审批人,请检查审批人配置", e.NodeName) } // ============================ 配置读取 ============================ // tenantFlows 查询租户侧流程列表(可按 biz_type 过滤) func tenantFlows(tid uint64, bizType string) ([]models.TenantApprovalFlow, error) { qs := models.Orm.QueryTable(new(models.TenantApprovalFlow)). Filter("tid", tid). Filter("is_deleted", 0) if bizType != "" { qs = qs.Filter("biz_type", bizType) } var rows []models.TenantApprovalFlow _, err := qs.OrderBy("-id").All(&rows) return rows, err } // tenantFlowNodes 查询租户侧流程的全部节点(含开始/审批/条件/结束,携带图连线) func tenantFlowNodes(flowID uint64) ([]Node, error) { var rows []models.TenantApprovalNode if _, err := models.Orm.QueryTable(new(models.TenantApprovalNode)). Filter("flow_id", flowID). Filter("is_deleted", 0). OrderBy("sort", "id"). All(&rows); err != nil && err != orm.ErrNoRows { return nil, err } out := make([]Node, 0, len(rows)) for _, r := range rows { out = append(out, Node{ ID: r.ID, NodeKey: r.NodeKey, NodeName: r.NodeName, NodeType: r.NodeType, ApproverType: r.ApproverType, ApproverIDs: r.ApproverIDs, MultiMode: r.MultiMode, AllowReturn: r.AllowReturn, TimeoutHours: r.TimeoutHours, Sort: r.Sort, PrevKeys: r.PrevKeys, NextKeys: r.NextKeys, ConditionExpr: r.ConditionExpr, }) } return out, nil } // platformFlows 查询平台侧流程列表 func platformFlows(bizType string) ([]models.PlatformApprovalFlow, error) { qs := models.Orm.QueryTable(new(models.PlatformApprovalFlow)).Filter("is_deleted", 0) if bizType != "" { qs = qs.Filter("biz_type", bizType) } var rows []models.PlatformApprovalFlow _, err := qs.OrderBy("-id").All(&rows) return rows, err } // platformFlowNodes 查询平台侧流程的全部节点(含图连线) func platformFlowNodes(flowID uint64) ([]Node, error) { var rows []models.PlatformApprovalNode if _, err := models.Orm.QueryTable(new(models.PlatformApprovalNode)). Filter("flow_id", flowID). Filter("is_deleted", 0). OrderBy("sort", "id"). All(&rows); err != nil && err != orm.ErrNoRows { return nil, err } out := make([]Node, 0, len(rows)) for _, r := range rows { out = append(out, Node{ ID: r.ID, NodeKey: r.NodeKey, NodeName: r.NodeName, NodeType: r.NodeType, ApproverType: r.ApproverType, ApproverIDs: r.ApproverIDs, MultiMode: r.MultiMode, AllowReturn: r.AllowReturn, TimeoutHours: r.TimeoutHours, Sort: r.Sort, PrevKeys: r.PrevKeys, NextKeys: r.NextKeys, ConditionExpr: r.ConditionExpr, }) } return out, nil } // LoadFlow 按 scope 加载流程(flowID=0 时取该单据类型的默认启用流程) func LoadFlow(scope int8, tid uint64, bizType string, flowID uint64) (*Flow, error) { if scope == models.ApprovalScopePlatform { var candidates []models.PlatformApprovalFlow var err error if flowID > 0 { var one models.PlatformApprovalFlow if e := models.Orm.QueryTable(new(models.PlatformApprovalFlow)). Filter("id", flowID).Filter("is_deleted", 0).One(&one); e != nil { return nil, &ErrNoFlow{BizType: bizType} } candidates = []models.PlatformApprovalFlow{one} } else { candidates, err = platformFlows(bizType) } if err != nil || len(candidates) == 0 { return nil, &ErrNoFlow{BizType: bizType} } return buildPlatformFlow(pickPlatformFlow(candidates)) } var candidates []models.TenantApprovalFlow var err error if flowID > 0 { var one models.TenantApprovalFlow if e := models.Orm.QueryTable(new(models.TenantApprovalFlow)). Filter("id", flowID).Filter("tid", tid).Filter("is_deleted", 0).One(&one); e != nil { return nil, &ErrNoFlow{BizType: bizType} } candidates = []models.TenantApprovalFlow{one} } else { candidates, err = tenantFlows(tid, bizType) } if err != nil || len(candidates) == 0 { return nil, &ErrNoFlow{BizType: bizType} } return buildTenantFlow(pickTenantFlow(candidates)) } // LoadFlowByCode 按审批流编号加载流程(供各程序按编号发起) func LoadFlowByCode(scope int8, tid uint64, code string) (*Flow, error) { code = strings.TrimSpace(code) if code == "" { return nil, &ErrNoFlow{BizType: code} } if scope == models.ApprovalScopePlatform { var one models.PlatformApprovalFlow if err := models.Orm.QueryTable(new(models.PlatformApprovalFlow)). Filter("flow_code", code).Filter("is_deleted", 0).One(&one); err != nil { return nil, &ErrNoFlow{BizType: code} } return buildPlatformFlow(one) } var one models.TenantApprovalFlow if err := models.Orm.QueryTable(new(models.TenantApprovalFlow)). Filter("flow_code", code).Filter("tid", tid).Filter("is_deleted", 0).One(&one); err != nil { return nil, &ErrNoFlow{BizType: code} } return buildTenantFlow(one) } // buildTenantFlow / buildPlatformFlow 装配租户/平台流程(含图索引) func buildTenantFlow(f models.TenantApprovalFlow) (*Flow, error) { nodes, err := tenantFlowNodes(f.ID) if err != nil { return nil, err } if len(nodes) == 0 { return nil, &ErrNoFlow{BizType: f.BizType} } return assembleFlow(f.ID, f.BizType, f.FlowName, nodes), nil } func buildPlatformFlow(f models.PlatformApprovalFlow) (*Flow, error) { nodes, err := platformFlowNodes(f.ID) if err != nil { return nil, err } if len(nodes) == 0 { return nil, &ErrNoFlow{BizType: f.BizType} } return assembleFlow(f.ID, f.BizType, f.FlowName, nodes), nil } // assembleFlow 构建图索引 ByKey 与入口 StartKey(优先开始类型节点,否则取无前置节点) func assembleFlow(id uint64, bizType, name string, nodes []Node) *Flow { f := &Flow{ID: id, BizType: bizType, FlowName: name, Nodes: nodes, ByKey: make(map[string]*Node, len(nodes))} for i := range f.Nodes { f.ByKey[f.Nodes[i].NodeKey] = &f.Nodes[i] } for i := range f.Nodes { if f.Nodes[i].NodeType == models.ApprovalNodeStart { f.StartKey = f.Nodes[i].NodeKey break } } if f.StartKey == "" { for i := range f.Nodes { if strings.TrimSpace(f.Nodes[i].PrevKeys) == "" { f.StartKey = f.Nodes[i].NodeKey break } } } if f.StartKey == "" && len(f.Nodes) > 0 { f.StartKey = f.Nodes[0].NodeKey } return f } // pickTenantFlow 在候选流程中择优:优先「默认 + 启用」,其次任一启用,最后首条。 func pickTenantFlow(rows []models.TenantApprovalFlow) models.TenantApprovalFlow { for _, r := range rows { if r.IsDefault == 1 && r.Status == 1 { return r } } for _, r := range rows { if r.Status == 1 { return r } } return rows[0] } // pickPlatformFlow 同 pickTenantFlow,平台侧口径。 func pickPlatformFlow(rows []models.PlatformApprovalFlow) models.PlatformApprovalFlow { for _, r := range rows { if r.IsDefault == 1 && r.Status == 1 { return r } } for _, r := range rows { if r.Status == 1 { return r } } return rows[0] } // ============================ 审批人解析 ============================ // parseIDs 解析逗号分隔的 ID 列表 func parseIDs(raw string) []uint64 { out := make([]uint64, 0, 4) for _, part := range strings.Split(raw, ",") { part = strings.TrimSpace(part) if part == "" { continue } if v, err := strconv.ParseUint(part, 10, 64); err == nil && v > 0 { out = append(out, v) } } return out } // resolveApprovers 按 scope 解析节点审批人。 // 发起人相关类型需要 SubmitInput 上下文。 func resolveApprovers(scope int8, node Node, in SubmitInput) ([]Approver, error) { ids := parseIDs(node.ApproverIDs) switch node.ApproverType { case models.ApprovalApproverUser: if scope == models.ApprovalScopePlatform { return platformUsers(ids), nil } return tenantUsers(uint64(in.Tid), ids), nil case models.ApprovalApproverRole: if scope == models.ApprovalScopePlatform { return platformRoleUsers(ids), nil } return tenantRoleUsers(uint64(in.Tid), ids), nil case models.ApprovalApproverInitiator: if in.SubmitUID == 0 { return nil, nil } return []Approver{{ID: in.SubmitUID, Name: in.SubmitName}}, nil case models.ApprovalApproverOrgLeader: // 指定部门的负责人;approver_ids 存组织ID if len(ids) == 0 { return nil, nil } return orgLeaders(in.Tid, ids), nil case models.ApprovalApproverInitLeader: // 发起人所在部门负责人 if in.DeptID == 0 { return nil, nil } return orgLeaders(in.Tid, []uint64{in.DeptID}), nil case models.ApprovalApproverPosition: // 指定职位(approver_ids 存 BackendPosition.id),按员工职位名匹配 if len(ids) == 0 { return nil, nil } return positionUsers(uint64(in.Tid), ids), nil } return nil, nil } // resolveNotifyTargets 解析通知节点的通知对象(支持多类型)。 // approver_ids 兼容两种存储格式: // 1. 旧格式:纯数字 CSV —— 单类型,配合 node.ApproverType(历史数据); // 2. 多选格式:"类型:ID" CSV(如 "1:52,5:0")—— 类型取值同 approver_type, // 自动定位类型(5发起人本人 / 6发起人主管)无需指定对象,ID 占位 0。 // 各类型的解析结果按 ID 去重合并。 func resolveNotifyTargets(scope int8, node Node, in SubmitInput) ([]Approver, error) { if !strings.Contains(node.ApproverIDs, ":") { return resolveApprovers(scope, node, in) } typeOrder := make([]int8, 0, 4) idsByType := make(map[int8][]uint64) for _, part := range strings.Split(node.ApproverIDs, ",") { part = strings.TrimSpace(part) if part == "" { continue } kv := strings.SplitN(part, ":", 2) t, err := strconv.Atoi(strings.TrimSpace(kv[0])) if err != nil || t < int(models.ApprovalApproverUser) || t > int(models.ApprovalApproverInitLeader) { continue } at := int8(t) if _, ok := idsByType[at]; !ok { typeOrder = append(typeOrder, at) } if len(kv) == 2 { if id, err := strconv.ParseUint(strings.TrimSpace(kv[1]), 10, 64); err == nil && id > 0 { idsByType[at] = append(idsByType[at], id) } } } merged := make([]Approver, 0, 8) for _, t := range typeOrder { sub := Node{ApproverType: t, ApproverIDs: joinApproverIDs(idsByType[t])} approvers, err := resolveApprovers(scope, sub, in) if err != nil { return nil, err } merged = append(merged, approvers...) } return dedupeApprovers(merged), nil } // joinApproverIDs 把 ID 列表拼成 CSV(与 parseIDs 互逆) func joinApproverIDs(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, ",") } // tenantUsers 按 uid 取租户用户 func tenantUsers(tid uint64, uids []uint64) []Approver { if len(uids) == 0 { return nil } var rows []models.SystemTenantUser if _, err := models.Orm.QueryTable(new(models.SystemTenantUser)). Filter("tid", tid). Filter("uid__in", uids). Filter("delete_time__isnull", true). All(&rows); err != nil { return nil } out := make([]Approver, 0, len(rows)) for _, r := range rows { out = append(out, Approver{ID: r.Uid, Name: firstNonEmptyPtr(r.Name, r.Account)}) } return out } // tenantRoleUsers 按租户角色取全部成员 func tenantRoleUsers(tid uint64, roleIDs []uint64) []Approver { if len(roleIDs) == 0 { return nil } var rows []models.SystemTenantUser if _, err := models.Orm.QueryTable(new(models.SystemTenantUser)). Filter("tid", tid). Filter("group_id__in", roleIDs). Filter("delete_time__isnull", true). All(&rows); err != nil { return nil } out := make([]Approver, 0, len(rows)) for _, r := range rows { out = append(out, Approver{ID: r.Uid, Name: firstNonEmptyPtr(r.Name, r.Account)}) } return out } // platformUsers 按 id 取平台用户 func platformUsers(ids []uint64) []Approver { if len(ids) == 0 { return nil } var rows []models.AdminUser if _, err := models.Orm.QueryTable(new(models.AdminUser)). Filter("id__in", ids). Filter("delete_time__isnull", true). All(&rows); err != nil { return nil } out := make([]Approver, 0, len(rows)) for _, r := range rows { out = append(out, Approver{ID: r.ID, Name: approverName(r.Name, r.Account)}) } return out } // platformRoleUsers 按平台角色取全部成员(AdminRole cid=1 为平台角色) func platformRoleUsers(roleIDs []uint64) []Approver { if len(roleIDs) == 0 { return nil } var users []models.AdminUser if _, err := models.Orm.QueryTable(new(models.AdminUser)). Filter("role_id__in", roleIDs). Filter("delete_time__isnull", true). All(&users); err != nil { return nil } out := make([]Approver, 0, len(users)) for _, r := range users { out = append(out, Approver{ID: r.ID, Name: approverName(r.Name, r.Account)}) } return out } // orgLeaders 取组织负责人 func orgLeaders(tid uint64, orgIDs []uint64) []Approver { var orgs []models.BackendOrganization if _, err := models.Orm.QueryTable(new(models.BackendOrganization)). Filter("id__in", orgIDs). Filter("delete_time__isnull", true). All(&orgs); err != nil { return nil } // 负责人可能指向员工ID,也可能指向租户用户uid,这里统一再查一次租户用户 leaderIDs := make([]uint64, 0, len(orgs)) for _, o := range orgs { if o.LeaderID != nil && *o.LeaderID > 0 { leaderIDs = append(leaderIDs, *o.LeaderID) } } if len(leaderIDs) == 0 { return nil } return tenantUsers(tid, leaderIDs) } // positionUsers 按职位取员工:职位ID → 职位名 → 员工(职位字段) → 登录账号 → 租户用户 func positionUsers(tid uint64, positionIDs []uint64) []Approver { var positions []models.BackendPosition if _, err := models.Orm.QueryTable(new(models.BackendPosition)). Filter("id__in", positionIDs). Filter("delete_time__isnull", true). All(&positions); err != nil { return nil } names := make([]string, 0, len(positions)) for _, p := range positions { if strings.TrimSpace(p.PositionName) != "" { names = append(names, p.PositionName) } } if len(names) == 0 { return nil } var employees []models.BackendEmployee if _, err := models.Orm.QueryTable(new(models.BackendEmployee)). Filter("tid", tid). Filter("position__in", names). Filter("delete_time__isnull", true). All(&employees); err != nil { return nil } accounts := make([]string, 0, len(employees)) for _, e := range employees { if strings.TrimSpace(e.Account) != "" { accounts = append(accounts, e.Account) } } if len(accounts) == 0 { return nil } var users []models.SystemTenantUser if _, err := models.Orm.QueryTable(new(models.SystemTenantUser)). Filter("tid", tid). Filter("account__in", accounts). Filter("delete_time__isnull", true). All(&users); err != nil { return nil } out := make([]Approver, 0, len(users)) for _, u := range users { out = append(out, Approver{ID: u.Uid, Name: firstNonEmptyPtr(u.Name, u.Account)}) } return out } // approverName 优先取姓名字段,回退到账号(平台侧 Account 为普通字符串) func approverName(name *string, account string) string { if s := firstNonEmptyPtr(name); s != "" { return s } return strings.TrimSpace(account) } // firstNonEmptyPtr 取第一个非空字符串指针的值 func firstNonEmptyPtr(vals ...*string) string { for _, v := range vals { if v != nil && strings.TrimSpace(*v) != "" { return strings.TrimSpace(*v) } } return "" } // dedupeApprovers 去重并保持顺序 func dedupeApprovers(in []Approver) []Approver { seen := make(map[uint64]bool, len(in)) out := make([]Approver, 0, len(in)) for _, a := range in { if a.ID == 0 || seen[a.ID] { continue } seen[a.ID] = true out = append(out, a) } return out } // ============================ 运行时:发起前的可用流查询 ============================ // AvailableFlow 业务侧选择用的轻量结构 type AvailableFlow struct { FlowID uint64 `json:"flow_id"` FlowCode string `json:"flow_code"` FlowName string `json:"flow_name"` BizType string `json:"biz_type"` } // ListAvailableFlows 按 bizType + 用户部门 过滤“当前可用”审批流。 // 直接查询 flow 表中 biz_type 匹配且 status=1 未删除的流程。 // 租户侧额外按 apply_dept_ids 部门过滤。 // 平台侧(scope=2)忽略部门过滤(因平台 flow 无 apply_dept_ids)。 func ListAvailableFlows(scope int8, tid uint64, bizType string, userOrgID uint64) ([]AvailableFlow, error) { // 租户侧预计算一次当前用户组织的“祖先链”(含自身),选中上级部门或公司均命中 var path []uint64 if scope == models.ApprovalScopeTenant && userOrgID > 0 { path = buildOrgPath(tid, userOrgID) } out := make([]AvailableFlow, 0) if scope == models.ApprovalScopePlatform { var rows []models.PlatformApprovalFlow _, _ = models.Orm.QueryTable(new(models.PlatformApprovalFlow)). Filter("biz_type", bizType).Filter("is_deleted", 0).Filter("status", 1). OrderBy("id").All(&rows) for _, f := range rows { out = append(out, AvailableFlow{FlowID: f.ID, FlowCode: f.FlowCode, FlowName: f.FlowName, BizType: f.BizType}) } return out, nil } var rows []models.TenantApprovalFlow _, _ = models.Orm.QueryTable(new(models.TenantApprovalFlow)). Filter("tid", tid).Filter("biz_type", bizType).Filter("is_deleted", 0).Filter("status", 1). OrderBy("id").All(&rows) for _, f := range rows { if !deptMatch(f.ApplyDeptIDs, path) { continue } out = append(out, AvailableFlow{FlowID: f.ID, FlowCode: f.FlowCode, FlowName: f.FlowName, BizType: f.BizType}) } return out, nil } // deptMatch csv 空视为全公司通用;否则与 path (包含自身与祖先部门ID) 任一命中即命中。 func deptMatch(csv string, path []uint64) bool { csv = strings.TrimSpace(csv) if csv == "" { return true } if len(path) == 0 { return false } inPath := make(map[uint64]bool, len(path)) for _, id := range path { inPath[id] = true } for _, part := range strings.Split(csv, ",") { if v, err := strconv.ParseUint(strings.TrimSpace(part), 10, 64); err == nil && inPath[v] { return true } } return false } // buildOrgPath 从当前部门沿 parent_id 向上追到根,返回含自身的 ID 链(环保护) func buildOrgPath(tid, orgID uint64) []uint64 { if orgID == 0 { return nil } var all []models.BackendOrganization _, _ = models.Orm.QueryTable(new(models.BackendOrganization)). Filter("tid", tid).Filter("delete_time__isnull", true). All(&all, "id", "parent_id") parent := make(map[uint64]uint64, len(all)) for _, o := range all { parent[o.ID] = o.ParentID } out := []uint64{orgID} seen := map[uint64]bool{orgID: true} cur := orgID for { p, ok := parent[cur] if !ok || p == 0 || seen[p] { break } out = append(out, p) seen[p] = true cur = p } return out } // ============================ 运行时:提交 ============================ // Submit 提交单据进入审批,创建实例并按图展开首个可执行节点的任务。 // 优先按 FlowCode 命中流程;无审批节点时直接置为已通过。 func Submit(in SubmitInput) (uint64, error) { var ( flow *Flow err error ) if strings.TrimSpace(in.FlowCode) != "" { flow, err = LoadFlowByCode(in.Scope, in.Tid, in.FlowCode) } else { flow, err = LoadFlow(in.Scope, in.Tid, in.BizType, in.FlowID) } if err != nil { return 0, err } now := time.Now() inst := models.ApprovalInstance{ Scope: in.Scope, Tid: in.Tid, BizTid: in.BizTid, FlowID: flow.ID, BizType: in.BizType, BizID: in.BizID, BizTitle: in.BizTitle, SubmitUserID: in.SubmitUID, SubmitUserName: in.SubmitName, SubmitDeptID: in.DeptID, SubmitTime: &now, Status: models.ApprovalInstanceRunning, } if len(in.FormData) > 0 { if b, e := json.Marshal(in.FormData); e == nil { s := string(b) inst.FormData = &s } } instID, err := models.Orm.Insert(&inst) if err != nil { return 0, err } start := flow.ByKey[flow.StartKey] hasApproval, firstApprovalName, firstApprovalKey := anyApprovalNode(flow) notifyFn := func(n *Node) { fireNotifyTasks(uint64(instID), in, n) } node, approvers := nextExecutable(flow, in.FormData, start, in, notifyFn) if node == nil { // 无可执行审批节点 if hasApproval { // 配了审批节点但均无有效审批人:视为配置错误,回滚实例 _, _ = models.Orm.QueryTable(new(models.ApprovalInstance)). Filter("id", instID).Update(orm.Params{"is_deleted": 1, "status": models.ApprovalInstanceCanceled}) return 0, &ErrNoApprover{NodeName: firstApprovalName, NodeKey: firstApprovalKey} } finishInstance(uint64(instID), models.ApprovalInstanceApproved) return uint64(instID), nil } if err := createTasks(uint64(instID), in, *node, approvers); err != nil { return uint64(instID), err } if _, err := models.Orm.QueryTable(new(models.ApprovalInstance)). Filter("id", instID).Update(orm.Params{"current_node": node.NodeKey}); err != nil { return uint64(instID), err } return uint64(instID), nil } // anyApprovalNode 判断流程是否含审批节点,并返回首个审批节点信息 func anyApprovalNode(flow *Flow) (bool, string, string) { for i := range flow.Nodes { if flow.Nodes[i].NodeType == models.ApprovalNodeApproval { return true, flow.Nodes[i].NodeName, flow.Nodes[i].NodeKey } } return false, "", "" } // advanceFrom 从 cur 沿出边前进,遇条件网关求值选路,返回首个审批节点(nil 表示到终点)。 // onNotify 可选回调:当路径经过通知节点时触发,由调用方负责创建通知任务。 func advanceFrom(flow *Flow, formData map[string]interface{}, cur *Node, onNotify ...func(*Node)) *Node { visited := make(map[string]bool) node := pickNext(flow, formData, cur) for node != nil { if visited[node.NodeKey] { return nil // 环保护 } visited[node.NodeKey] = true switch node.NodeType { case models.ApprovalNodeApproval: return node case models.ApprovalNodeEnd: return nil case models.ApprovalNodeNotify: if len(onNotify) > 0 && onNotify[0] != nil { onNotify[0](node) } node = pickNext(flow, formData, node) default: // start / condition:继续向后 node = pickNext(flow, formData, node) } } return nil } // pickNext 取节点的下一个目标:普通节点取唯一出边;条件网关按各目标 condition_expr 互斥选路,空条件为默认分支 func pickNext(flow *Flow, formData map[string]interface{}, node *Node) *Node { if node == nil { return nil } outs := splitKeys(node.NextKeys) if len(outs) == 0 { return nil } if node.NodeType != models.ApprovalNodeCondition { return flow.ByKey[outs[0]] } var def *Node for _, k := range outs { t := flow.ByKey[k] if t == nil { continue } if strings.TrimSpace(t.ConditionExpr) == "" { if def == nil { def = t } continue } if evalCondition(t.ConditionExpr, formData) { return t } } return def } // nextExecutable 沿图找到下一个「有有效审批人」的审批节点;无人审批节点自动跳过 // nextExecutable 获取下一个可执行审批节点及其审批人(跳过通知节点并通过 onNotify 回调触发) func nextExecutable(flow *Flow, formData map[string]interface{}, cur *Node, in SubmitInput, onNotify ...func(*Node)) (*Node, []Approver) { node := advanceFrom(flow, formData, cur, onNotify...) visited := make(map[string]bool) for node != nil { if visited[node.NodeKey] { break } visited[node.NodeKey] = true approvers, err := resolveApprovers(in.Scope, *node, in) if err == nil { approvers = dedupeApprovers(approvers) if len(approvers) > 0 { return node, approvers } } node = advanceFrom(flow, formData, node, onNotify...) } return nil, nil } // splitKeys 拆分逗号分隔的 node_key 列表 func splitKeys(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 } // instanceFormData 解析实例的表单快照 func instanceFormData(inst *models.ApprovalInstance) map[string]interface{} { if inst == nil || inst.FormData == nil || strings.TrimSpace(*inst.FormData) == "" { return nil } m := make(map[string]interface{}) if err := json.Unmarshal([]byte(*inst.FormData), &m); err != nil { return nil } return m } // submitFromInstance 由实例重建提交上下文(供推进时解析审批人) func submitFromInstance(inst *models.ApprovalInstance, formData map[string]interface{}) SubmitInput { return SubmitInput{ Scope: inst.Scope, Tid: inst.Tid, BizTid: inst.BizTid, BizType: inst.BizType, BizID: inst.BizID, SubmitUID: inst.SubmitUserID, SubmitName: inst.SubmitUserName, DeptID: inst.SubmitDeptID, FormData: formData, } } // createTasks 为一个节点展开审批任务 func createTasks(instID uint64, in SubmitInput, node Node, approvers []Approver) error { timeout := node.TimeoutHours if timeout <= 0 { timeout = 48 } deadline := time.Now().Add(time.Duration(timeout) * time.Hour) multiMode := node.MultiMode if multiMode != models.ApprovalMultiAll { multiMode = models.ApprovalMultiAny } tasks := make([]models.ApprovalTask, 0, len(approvers)) for _, a := range approvers { tasks = append(tasks, models.ApprovalTask{ InstanceID: instID, Scope: in.Scope, Tid: in.Tid, BizType: in.BizType, BizID: in.BizID, NodeKey: node.NodeKey, NodeName: node.NodeName, Sort: node.Sort, ApproverID: a.ID, ApproverName: a.Name, MultiMode: multiMode, Status: models.ApprovalTaskPending, Deadline: &deadline, }) } _, err := models.Orm.InsertMulti(len(tasks), tasks) return err } // fireNotifyTasks 为通知节点创建已读通知任务(不阻塞流程) func fireNotifyTasks(instID uint64, in SubmitInput, notifyNode *Node) { recipients, _ := resolveNotifyTargets(in.Scope, *notifyNode, in) recipients = dedupeApprovers(recipients) if len(recipients) == 0 { return } now := time.Now() tasks := make([]models.ApprovalTask, 0, len(recipients)) for _, r := range recipients { tasks = append(tasks, models.ApprovalTask{ InstanceID: instID, Scope: in.Scope, Tid: in.Tid, BizType: in.BizType, BizID: in.BizID, NodeKey: notifyNode.NodeKey, NodeName: notifyNode.NodeName, Sort: notifyNode.Sort, ApproverID: r.ID, ApproverName: r.Name, Status: models.ApprovalTaskNotified, FinishTime: &now, }) } _, _ = models.Orm.InsertMulti(len(tasks), tasks) } // finishInstance 结束实例 func finishInstance(instID uint64, status int8) { now := time.Now() _, _ = models.Orm.QueryTable(new(models.ApprovalInstance)). Filter("id", instID).Update(orm.Params{ "status": status, "finish_time": now, "update_time": now, }) } // ============================ 运行时:审批 ============================ // ApproveInput 审批入参 type ApproveInput struct { ApproverID uint64 // 审批人ID Pass bool Comment string // BizTid 平台侧操作时按业务租户校验/展示用(可留空) BizTid uint64 } // ApproveResult 审批结果 type ApproveResult struct { InstanceID uint64 InstanceStat int8 // 实例新状态 CurrentNode string // 新当前节点(完成时为空) Finished bool Message string } // Approve 处理一条审批任务:按或签/会签推进到下一节点或结束实例。 func Approve(taskID uint64, in ApproveInput) (*ApproveResult, error) { var task models.ApprovalTask if err := models.Orm.QueryTable(new(models.ApprovalTask)). Filter("id", taskID). Filter("is_deleted", 0). One(&task); err != nil { return nil, err } if task.Status != models.ApprovalTaskPending { return nil, fmt.Errorf("该审批任务已处理") } if task.ApproverID != in.ApproverID { return nil, fmt.Errorf("您无权审批该任务") } var inst models.ApprovalInstance if err := models.Orm.QueryTable(new(models.ApprovalInstance)). Filter("id", task.InstanceID). Filter("is_deleted", 0). One(&inst); err != nil { return nil, err } if inst.Status != models.ApprovalInstanceRunning { return nil, fmt.Errorf("该单据当前状态不可审批") } now := time.Now() // 驳回:整个实例结束(与退回上一节点区分) if !in.Pass { _, err := models.Orm.QueryTable(new(models.ApprovalTask)). Filter("id", taskID). Update(orm.Params{ "status": models.ApprovalTaskRejected, "action": models.ApprovalActionReject, "comment": in.Comment, "finish_time": now, "update_time": now, }) if err != nil { return nil, err } // 同节点其余待办一并置为跳过,避免留下永远待办的僵尸任务 _, _ = models.Orm.QueryTable(new(models.ApprovalTask)). Filter("instance_id", task.InstanceID). Filter("node_key", task.NodeKey). Filter("status", models.ApprovalTaskPending). Filter("id__neq", taskID). Update(orm.Params{"status": models.ApprovalTaskSkipped, "update_time": now}) finishInstance(task.InstanceID, models.ApprovalInstanceRejected) return &ApproveResult{ InstanceID: task.InstanceID, InstanceStat: models.ApprovalInstanceRejected, Finished: true, Message: "已驳回", }, nil } // 通过:更新当前任务 _, err := models.Orm.QueryTable(new(models.ApprovalTask)). Filter("id", taskID). Update(orm.Params{ "status": models.ApprovalTaskApproved, "action": models.ApprovalActionApprove, "comment": in.Comment, "finish_time": now, "update_time": now, }) if err != nil { return nil, err } // 或签:任一通过即推进,会签:需全部通过 if task.MultiMode == models.ApprovalMultiAny { _, _ = models.Orm.QueryTable(new(models.ApprovalTask)). Filter("instance_id", task.InstanceID). Filter("node_key", task.NodeKey). Filter("status", models.ApprovalTaskPending). Update(orm.Params{"status": models.ApprovalTaskSkipped, "update_time": now}) } else { remaining, _ := models.Orm.QueryTable(new(models.ApprovalTask)). Filter("instance_id", task.InstanceID). Filter("node_key", task.NodeKey). Filter("status", models.ApprovalTaskPending). Count() if remaining > 0 { return &ApproveResult{ InstanceID: task.InstanceID, InstanceStat: models.ApprovalInstanceRunning, CurrentNode: task.NodeKey, Message: fmt.Sprintf("已通过,等待其余 %d 位审批人", remaining), }, nil } } // 当前节点完成 → 推进下一节点 return advance(task.InstanceID, task.NodeKey, in) } // advance 沿图推进到下一个可执行审批节点;无后继则结束实例 func advance(instID uint64, currentKey string, in ApproveInput) (*ApproveResult, error) { var inst models.ApprovalInstance if err := models.Orm.QueryTable(new(models.ApprovalInstance)). Filter("id", instID).Filter("is_deleted", 0).One(&inst); err != nil { return nil, err } flow, err := LoadFlow(inst.Scope, inst.Tid, inst.BizType, inst.FlowID) if err != nil { // 配置被删除时保守处理:按已完成结束 finishInstance(instID, models.ApprovalInstanceApproved) return &ApproveResult{ InstanceID: instID, InstanceStat: models.ApprovalInstanceApproved, Finished: true, Message: "审批已通过", }, nil } formData := instanceFormData(&inst) si := submitFromInstance(&inst, formData) cur := flow.ByKey[currentKey] notifyFn := func(n *Node) { fireNotifyTasks(instID, si, n) } node, approvers := nextExecutable(flow, formData, cur, si, notifyFn) if node == nil { finishInstance(instID, models.ApprovalInstanceApproved) return &ApproveResult{ InstanceID: instID, InstanceStat: models.ApprovalInstanceApproved, Finished: true, Message: "审批已全部通过", }, nil } if err := createTasks(instID, si, *node, approvers); err != nil { return nil, err } now := time.Now() if _, err := models.Orm.QueryTable(new(models.ApprovalInstance)). Filter("id", instID).Update(orm.Params{"current_node": node.NodeKey, "update_time": now}); err != nil { return nil, err } return &ApproveResult{ InstanceID: instID, InstanceStat: models.ApprovalInstanceRunning, CurrentNode: node.NodeKey, Message: "已流转至「" + node.NodeName + "」", }, nil } // ============================ 条件表达式求值 ============================ // evalCondition 求值条件表达式:支持 == != > >= < <= 与 && ||(不含括号)。空表达式恒真。 func evalCondition(expr string, data map[string]interface{}) bool { expr = strings.TrimSpace(expr) if expr == "" { return true } for _, orPart := range strings.Split(expr, "||") { if evalAnd(orPart, data) { return true } } return false } func evalAnd(part string, data map[string]interface{}) bool { for _, atom := range strings.Split(part, "&&") { if !evalAtom(atom, data) { return false } } return true } var condOps = []string{">=", "<=", "!=", "==", ">", "<"} func evalAtom(s string, data map[string]interface{}) bool { s = strings.TrimSpace(s) if s == "" { return true } for _, op := range condOps { idx := strings.Index(s, op) if idx <= 0 { continue } lv := resolveCondVal(strings.TrimSpace(s[:idx]), data) rv := parseCondLiteral(strings.TrimSpace(s[idx+len(op):])) return compareCond(lv, op, rv) } return false } // resolveCondVal 取字段值:支持点路径 a.b.c func resolveCondVal(key string, data map[string]interface{}) interface{} { if data == nil { return nil } if v, ok := data[key]; ok { return v } var cur interface{} = data for _, seg := range strings.Split(key, ".") { m, ok := cur.(map[string]interface{}) if !ok { return nil } v, ok := m[seg] if !ok { return nil } cur = v } return cur } func parseCondLiteral(raw string) interface{} { raw = strings.TrimSpace(raw) if len(raw) >= 2 { if (raw[0] == '"' || raw[0] == '\'') && raw[len(raw)-1] == raw[0] { return raw[1 : len(raw)-1] } } if n, err := strconv.ParseFloat(raw, 64); err == nil { return n } return raw } func toFloat(v interface{}) (float64, bool) { switch x := v.(type) { case float64: return x, true case float32: return float64(x), true case int: return float64(x), true case int64: return float64(x), true case uint64: return float64(x), true case json.Number: if f, e := x.Float64(); e == nil { return f, true } case string: if f, e := strconv.ParseFloat(strings.TrimSpace(x), 64); e == nil { return f, true } } return 0, false } func compareCond(l interface{}, op string, r interface{}) bool { lf, lok := toFloat(l) rf, rok := toFloat(r) if lok && rok { switch op { case ">": return lf > rf case "<": return lf < rf case ">=": return lf >= rf case "<=": return lf <= rf case "==": return lf == rf case "!=": return lf != rf } } ls := condString(l) rs := condString(r) switch op { case "==": return ls == rs case "!=": return ls != rs } return false } func condString(v interface{}) string { if v == nil { return "" } switch x := v.(type) { case string: return x case bool: if x { return "true" } return "false" case float64: return strconv.FormatFloat(x, 'f', -1, 64) default: return fmt.Sprintf("%v", v) } } // SortNodes 按 sort 升序排序(导出供配置端校验使用) func SortNodes(nodes []Node) { sort.SliceStable(nodes, func(i, j int) bool { return nodes[i].Sort < nodes[j].Sort }) }