1450 lines
42 KiB
Go
1450 lines
42 KiB
Go
// 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"
|
||
|
||
"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,
|
||
})
|
||
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 })
|
||
}
|