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