Files
yunzerwebsiteallinone/go/services/approval/engine.go
T
2026-10-31 19:17:37 +08:00

1471 lines
43 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 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"
"server/services/erp"
"server/services/wechatmini"
"github.com/beego/beego/v2/client/orm"
"github.com/beego/beego/v2/core/logs"
)
// 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)
if err != nil {
return err
}
// 租户侧审批人同时推小程序订阅消息(授权过才有一次发送机会,失败不影响流程)
if in.Scope == models.ApprovalScopeTenant {
for _, a := range approvers {
approverID, nodeName := a.ID, node.NodeName
go func(uid uint64, name string) {
res := wechatmini.PushTenantNotice(uid, "您有新的审批待办", "审批事项:"+name+",请及时处理。")
if res.Sent > 0 {
logs.Info("[wechat-mini] 审批待办已推送订阅消息: user=%d sent=%d", uid, res.Sent)
}
for _, e := range res.Errors {
logs.Info("[wechat-mini] 订阅消息推送失败: user=%d err=%v", uid, e)
}
}(approverID, nodeName)
}
}
return nil
}
// 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,
})
SyncBizResult(instID, status)
}
// 报销单状态(与 yz_backend_reimbursements.status / 前端字典 oa_reimburse_status 一致)
const (
reimburseStatusApproved int8 = 2 // 已通过
reimburseStatusRejected int8 = 3 // 已驳回
reimburseStatusWithdrawn int8 = 4 // 已撤回
)
// BizTypeLegacyOA 早期报销单提交进审批引擎时写入的 biz_type(按模块 code 记录)。
// 与 ApprovalBizReimburse 一并识别,保证按老写法产生的在途/历史单据也能正常回写。
const BizTypeLegacyOA = "oa"
// IsReimburseBizType 判断审批实例是否属于 OA 报销单
func IsReimburseBizType(bizType string) bool {
switch strings.TrimSpace(bizType) {
case models.ApprovalBizReimburse, BizTypeLegacyOA:
return true
}
return false
}
// SyncBizResult 把审批实例的最终结果回写到对应业务单据,保证「审批中心」与业务列表状态一致。
//
// 目前覆盖 OA 报销单(biz_type=reimburse / 老写法 oa):按实例状态同步
// yz_backend_reimbursements.status,并补一条审批记录供业务详情页时间线展示。
// 撤回入口(不走 finishInstance)也会显式调用本函数,重复调用按最终态幂等。
func SyncBizResult(instID uint64, status int8) {
if instID == 0 {
return
}
var inst models.ApprovalInstance
if err := models.Orm.QueryTable(new(models.ApprovalInstance)).
Filter("id", instID).One(&inst); err != nil {
return
}
// 无业务单据ID的实例(纯手工审批)不回写任何业务表
if inst.BizID == 0 {
return
}
// 进销存单据:biz_type 形如 erp_purchase_in / erp_sales_out,终态回写单据状态,
// 「已生效」单据的效果(库存流水/往来账)在回写时同事务执行。
if strings.HasPrefix(inst.BizType, "erp_") {
erp.SyncErpDocResult(inst.BizType, inst.BizID, bizTenantID(&inst), status)
return
}
if !IsReimburseBizType(inst.BizType) {
return
}
syncReimburseResult(&inst, status)
}
// bizTenantID 业务单据所属租户:平台侧实例的 tid 恒为 0,来源租户记在 biz_tid。
func bizTenantID(inst *models.ApprovalInstance) uint64 {
if inst.Scope == models.ApprovalScopePlatform && inst.BizTid != 0 {
return inst.BizTid
}
return inst.Tid
}
// syncReimburseResult 回写报销单状态:审批通过/驳回/撤回分别对应业务状态 2/3/4。
func syncReimburseResult(inst *models.ApprovalInstance, status int8) {
var bizStatus, action int8
switch status {
case models.ApprovalInstanceApproved:
bizStatus, action = reimburseStatusApproved, models.ApprovalActionApprove
case models.ApprovalInstanceRejected:
bizStatus, action = reimburseStatusRejected, models.ApprovalActionReject
case models.ApprovalInstanceWithdrawn:
bizStatus, action = reimburseStatusWithdrawn, models.ApprovalActionWithdraw
default:
return
}
tid := bizTenantID(inst)
if tid == 0 {
return
}
var row models.BackendReimbursement
if err := models.Orm.QueryTable(new(models.BackendReimbursement)).
Filter("id", inst.BizID).Filter("tid", tid).
Filter("is_deleted", 0).One(&row); err != nil {
return
}
// 已是目标状态说明同步过,避免重复插入审批记录
if row.Status == bizStatus {
return
}
now := time.Now()
if _, err := models.Orm.QueryTable(new(models.BackendReimbursement)).
Filter("id", inst.BizID).Filter("tid", tid).
Update(orm.Params{"status": bizStatus, "update_time": now}); err != nil {
return
}
// 业务详情页的时间线读 yz_backend_approval_records,这里补记一条审批结果
actorID, actorName, comment := lastApprovalActor(inst)
_, _ = models.Orm.Insert(&models.BackendApprovalRecord{
Tid: tid, UID: approvalUint64Ptr(actorID), ReimbursementID: inst.BizID,
ApproverID: actorID, ApproverName: actorName, Action: action,
Comment: approvalStringPtr(comment),
})
}
// lastApprovalActor 取实例上最近一次通过/驳回的审批人(撤回等场景回退到发起人)。
func lastApprovalActor(inst *models.ApprovalInstance) (uint64, string, string) {
var task models.ApprovalTask
if err := models.Orm.QueryTable(new(models.ApprovalTask)).
Filter("instance_id", inst.ID).
Filter("status__in", models.ApprovalTaskApproved, models.ApprovalTaskRejected).
OrderBy("-finish_time", "-id").One(&task); err == nil {
return task.ApproverID, task.ApproverName, task.Comment
}
return inst.SubmitUserID, inst.SubmitUserName, ""
}
func approvalUint64Ptr(v uint64) *uint64 {
if v == 0 {
return nil
}
return &v
}
func approvalStringPtr(s string) *string {
if s == "" {
return nil
}
return &s
}
// ============================ 运行时:审批 ============================
// 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 })
}