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" ) // PlatformApprovalController 平台端审批流配置(流程头 + 节点编排)。 // // 当期面向「域名申请」这类平台侧管控场景,按 module_code + biz_type 联合唯一组织流程; // 平台账号不归属租户,故流程与节点均无 tid,审批人取平台用户 / 平台角色。 type PlatformApprovalController struct { beego.Controller } // ---- 配置侧 JSON 结构 ---- // approvalNodePayload 单个节点(前端画布节点)。 // node_type:1开始 2审批 3结束 4判断 5通知;pos_x/pos_y 为画布坐标回显; // condition_expr 为该节点作为「判断节点出边目标」时的进入条件(由 edges 反推落库)。 type approvalNodePayload struct { NodeKey string `json:"node_key"` NodeName string `json:"node_name"` NodeType int8 `json:"node_type"` // 1开始 2审批 3结束 4判断 5通知 ApproverType int8 `json:"approver_type"` // 1指定用户 2指定角色 ... ApproverIDs []uint64 `json:"approver_ids"` // NotifyTargets 通知节点多类型存储:"类型:ID" 列表(如 ["1:52","5:0"]), // 类型取值同 approver_type;自动定位类型(5/6)ID 占位 0。 // 仅通知节点使用;空列表时按 approver_type + approver_ids 旧格式处理。 NotifyTargets []string `json:"notify_targets"` MultiMode int8 `json:"multi_mode"` // 1或签 2会签 AllowReturn int8 `json:"allow_return"` // 是否允许退回 TimeoutHours int `json:"timeout_hours"` // 超时小时 Sort int `json:"sort"` // 顺序(越小越靠前) PosX int `json:"pos_x"` // 画布 X 坐标 PosY int `json:"pos_y"` // 画布 Y 坐标 ConditionExpr string `json:"condition_expr"` } // approvalEdge 画布连线:from→to,condition 为该出边的分支条件(落到 to 节点的 condition_expr) type approvalEdge struct { From string `json:"from"` To string `json:"to"` Condition string `json:"condition"` } // approvalFlowSave 保存流程(图:节点 + 连线,节点全量覆盖) type approvalFlowSave struct { ID uint64 `json:"id"` ModuleCode string `json:"module_code"` BizType string `json:"biz_type"` FlowName string `json:"flow_name"` Description string `json:"description"` IsDefault int8 `json:"is_default"` Status int8 `json:"status"` ApplyDeptIDs []uint64 `json:"apply_dept_ids"` // 仅租户侧使用,空=全公司通用 Nodes []approvalNodePayload `json:"nodes"` Edges []approvalEdge `json:"edges"` } // approvalFlowView 流程 + 节点 + 连线(详情返回) type approvalFlowView struct { ID uint64 `json:"id"` ModuleCode string `json:"module_code"` BizType string `json:"biz_type"` FlowCode string `json:"flow_code"` FlowName string `json:"flow_name"` Description string `json:"description"` IsDefault int8 `json:"is_default"` Status int8 `json:"status"` NodeCount int `json:"node_count"` CreateTime string `json:"create_time"` ApplyDeptIDs []uint64 `json:"apply_dept_ids"` // 仅租户侧回显 Nodes []approvalNodePayload `json:"nodes"` Edges []approvalEdge `json:"edges"` } // readApprovalBody 读取并解析 JSON 请求体 func (c *PlatformApprovalController) readApprovalBody(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 } // FlowList GET /platform/approval/flow/list?biz_type=domain // 平台端流程列表;可按单据类型过滤,附带节点数。 func (c *PlatformApprovalController) FlowList() { if _, err := requirePlatform(&c.Controller); err != nil { code := identity.Status(err) jsonErr(&c.Controller, code, code, err.Error()) return } bizType := strings.TrimSpace(c.GetString("biz_type")) if bizType == "" { bizType = strings.TrimSpace(c.GetString("module")) // 兼容:分类即业务类型 } qs := models.Orm.QueryTable(new(models.PlatformApprovalFlow)). Filter("is_deleted", 0) if bizType != "" { qs = qs.Filter("biz_type", bizType) } var rows []models.PlatformApprovalFlow _, _ = qs.OrderBy("-is_default", "id").All(&rows) list := make([]approvalFlowView, 0, len(rows)) for _, r := range rows { cnt, _ := models.Orm.QueryTable(new(models.PlatformApprovalNode)). 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"), }) } c.Data["json"] = map[string]interface{}{"code": 200, "msg": "ok", "data": list} _ = c.ServeJSON() } // FlowDetail GET /platform/approval/flow/detail/:id // 返回流程及其全部节点(按 sort 升序),供编排界面回显。 func (c *PlatformApprovalController) FlowDetail() { if _, err := requirePlatform(&c.Controller); err != nil { code := identity.Status(err) jsonErr(&c.Controller, code, code, err.Error()) return } id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64) var f models.PlatformApprovalFlow if err := models.Orm.QueryTable(new(models.PlatformApprovalFlow)). Filter("id", id).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": c.flowToView(f)} _ = c.ServeJSON() } // flowToView 组装流程视图(含节点与连线) func (c *PlatformApprovalController) flowToView(f models.PlatformApprovalFlow) approvalFlowView { var nodes []models.PlatformApprovalNode _, _ = models.Orm.QueryTable(new(models.PlatformApprovalNode)). 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"), Nodes: make([]approvalNodePayload, 0, len(nodes)), } edgeNodes := make([]flowEdgeNode, 0, len(nodes)) for _, n := range nodes { view.Nodes = append(view.Nodes, approvalNodePayload{ NodeKey: n.NodeKey, NodeName: n.NodeName, NodeType: n.NodeType, ApproverType: n.ApproverType, ApproverIDs: parseIDSlice(n.ApproverIDs), 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 /platform/approval/flow/save // 新建或更新流程;节点采用「全量覆盖」语义(先软删旧节点,再按传入顺序重建)。 func (c *PlatformApprovalController) FlowSave() { if _, err := requirePlatform(&c.Controller); err != nil { code := identity.Status(err) jsonErr(&c.Controller, code, code, err.Error()) return } var p approvalFlowSave if err := c.readApprovalBody(&p); err != nil { jsonErr(&c.Controller, 400, 400, err.Error()) return } if strings.TrimSpace(p.FlowName) == "" { jsonErr(&c.Controller, 400, 400, "流程名称不能为空") return } // 分类(module_code)与单据类型(biz_type)对齐:二者留空时互补,至少其一非空 if strings.TrimSpace(p.ModuleCode) == "" { p.ModuleCode = p.BizType } if strings.TrimSpace(p.BizType) == "" { p.BizType = p.ModuleCode } if strings.TrimSpace(p.BizType) == "" { jsonErr(&c.Controller, 400, 400, "请选择所属分类") return } if err := validateApprovalNodes(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() o := models.Orm var flowID uint64 var flowCode string if p.ID > 0 { var exist models.PlatformApprovalFlow if err := o.QueryTable(new(models.PlatformApprovalFlow)). Filter("id", p.ID).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.PlatformApprovalFlow)).Filter("id", flowID).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, "update_time": now, }) if err != nil { jsonErr(&c.Controller, 500, 500, "保存失败: "+err.Error()) return } } else { row := models.PlatformApprovalFlow{ ModuleCode: p.ModuleCode, BizType: p.BizType, FlowName: p.FlowName, Description: p.Description, IsDefault: p.IsDefault, Status: p.Status, 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.PlatformApprovalFlow)).Filter("id", flowID). Update(orm.Params{"flow_code": flowCode}) } // 默认流程唯一:同 biz_type 下只允许一条 is_default=1 if p.IsDefault == 1 { _, _ = o.QueryTable(new(models.PlatformApprovalFlow)). 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.PlatformApprovalNode)). 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 } node := models.PlatformApprovalNode{ FlowID: flowID, NodeKey: nodeKey, NodeName: strings.TrimSpace(nd.NodeName), NodeType: nodeType, ApproverType: nd.ApproverType, ApproverIDs: joinIDs(nd.ApproverIDs), 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 /platform/approval/flow/pick // 轻量清单:按分类返回启用流程的 {flow_code, flow_name},供其它功能“选/填编号调用”。 func (c *PlatformApprovalController) Pick() { if _, err := requirePlatform(&c.Controller); err != nil { code := identity.Status(err) jsonErr(&c.Controller, code, code, err.Error()) return } module := strings.TrimSpace(c.GetString("module")) if module == "" { module = strings.TrimSpace(c.GetString("biz_type")) } qs := models.Orm.QueryTable(new(models.PlatformApprovalFlow)). Filter("is_deleted", 0).Filter("status", 1) if module != "" { qs = qs.Filter("biz_type", module) } var rows []models.PlatformApprovalFlow _, _ = qs.OrderBy("biz_type", "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, "biz_type": r.BizType, "flow_code": r.FlowCode, "flow_name": r.FlowName, }) } c.Data["json"] = map[string]interface{}{"code": 200, "msg": "ok", "data": list} _ = c.ServeJSON() } // FlowDelete DELETE /platform/approval/flow/delete/:id // 软删流程及其节点。存在在途实例时禁止删除,避免审批中断。 func (c *PlatformApprovalController) FlowDelete() { if _, err := requirePlatform(&c.Controller); err != nil { code := identity.Status(err) jsonErr(&c.Controller, code, code, err.Error()) return } id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64) var f models.PlatformApprovalFlow if err := models.Orm.QueryTable(new(models.PlatformApprovalFlow)). Filter("id", id).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.ApprovalScopePlatform). 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.PlatformApprovalFlow)).Filter("id", id). Update(orm.Params{"is_deleted": 1, "delete_time": now, "update_time": now}) _, _ = models.Orm.QueryTable(new(models.PlatformApprovalNode)).Filter("flow_id", id). Update(orm.Params{"is_deleted": 1, "delete_time": now}) c.Data["json"] = map[string]interface{}{"code": 200, "msg": "已删除"} _ = c.ServeJSON() } // Options GET /platform/approval/options // 编排界面「选审批人」用:返回平台角色(cid=1)与平台用户列表。 func (c *PlatformApprovalController) Options() { if _, err := requirePlatform(&c.Controller); err != nil { code := identity.Status(err) jsonErr(&c.Controller, code, code, err.Error()) return } var roles []models.AdminRole _, _ = models.Orm.QueryTable(new(models.AdminRole)). Filter("cid", 1).Filter("status", 1).Filter("delete_time__isnull", true). OrderBy("id").All(&roles) roleList := make([]map[string]interface{}, 0, len(roles)) for _, r := range roles { roleList = append(roleList, map[string]interface{}{"id": r.ID, "name": r.Name}) } var users []models.AdminUser _, _ = models.Orm.QueryTable(new(models.AdminUser)). Filter("status", 1).Filter("delete_time__isnull", true). OrderBy("id").All(&users) userList := make([]map[string]interface{}, 0, len(users)) for _, u := range users { userList = append(userList, map[string]interface{}{ "id": u.ID, "name": approverDisplayName(u.Name, u.Account), "role": u.RoleID, }) } // 分类(复用功能模块):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) moduleList := make([]map[string]interface{}, 0, len(mods)) for _, m := range mods { moduleList = append(moduleList, map[string]interface{}{ "code": m.Code, "name": m.Name, "enabled": true, }) } c.Data["json"] = map[string]interface{}{ "code": 200, "msg": "ok", "data": map[string]interface{}{"roles": roleList, "users": userList, "modules": moduleList}, } _ = c.ServeJSON() } // BizTypes GET /platform/approval/biz-types // 可绑定业务类型字典(当期平台侧为「域名申请」),供配置界面顶级下拉。 func (c *PlatformApprovalController) BizTypes() { if _, err := requirePlatform(&c.Controller); err != nil { code := identity.Status(err) jsonErr(&c.Controller, code, code, err.Error()) return } c.Data["json"] = map[string]interface{}{"code": 200, "msg": "ok", "data": []map[string]interface{}{ {"value": models.ApprovalBizDomain, "label": "域名申请"}, }} _ = c.ServeJSON() } // AvailableFlows GET /platform/approval/flow/available?biz_type=domain // 业务侧“发起前拉可用流”:返回当前 biz_type 启用的平台流程列表。 // 平台无部门概念,userOrgID 固定为 0(引擎内平台侧不做部门过滤)。 func (c *PlatformApprovalController) AvailableFlows() { if _, err := requirePlatform(&c.Controller); err != nil { code := identity.Status(err) jsonErr(&c.Controller, code, code, err.Error()) return } bizType := strings.TrimSpace(c.GetString("biz_type")) if bizType == "" { jsonErr(&c.Controller, 400, 400, "缺少 biz_type 参数") return } list, err := approvalEngine.ListAvailableFlows(models.ApprovalScopePlatform, 0, bizType, 0) 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() } // ============================ 辅助函数 ============================ // approverDisplayName 优先姓名,回退账号(AdminUser.Account 为普通字符串) func approverDisplayName(name *string, account string) string { if name != nil && strings.TrimSpace(*name) != "" { return strings.TrimSpace(*name) } return strings.TrimSpace(account) } // validateApprovalNodes 校验审批节点:必须选定审批人来源且非空(开始/结束/判断/通知跳过) func validateApprovalNodes(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) } if nd.ApproverType != models.ApprovalApproverUser && nd.ApproverType != models.ApprovalApproverRole { return fmt.Errorf("节点「%s」审批人类型非法(仅支持指定用户/指定角色)", nd.NodeName) } if len(nd.ApproverIDs) == 0 { return fmt.Errorf("节点「%s」未选择审批人", nd.NodeName) } } return nil } // joinIDs 将 ID 数组拼成逗号分隔串(存库口径) func joinIDs(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, ",") } // parseIDSlice 解析逗号分隔 ID 串为数组 func parseIDSlice(raw string) []uint64 { raw = strings.TrimSpace(raw) if raw == "" { return nil } out := make([]uint64, 0) for _, p := range strings.Split(raw, ",") { if v, err := strconv.ParseUint(strings.TrimSpace(p), 10, 64); err == nil && v > 0 { out = append(out, v) } } return out } // ============================ 图(连线)辅助 ============================ // flowEdgeNode 连线重建输入:节点标识 + 出边(next_keys) + 本节点进入条件 type flowEdgeNode struct { NodeKey string NextKeys string Condition string } // splitKeyList 拆分逗号分隔 node_key 列表 func splitKeyList(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 } // containsStr 列表是否包含某字符串 func containsStr(list []string, s string) bool { for _, x := range list { if x == s { return true } } return false } // approvalGraph 由画布连线反推的邻接与条件 type approvalGraph struct { next map[string][]string prev map[string][]string cond map[string]string } // buildGraph 由 edges 反推每个节点的 prev_keys/next_keys 与进入条件(条件落到目标节点) func buildGraph(nodes []approvalNodePayload, edges []approvalEdge) approvalGraph { g := approvalGraph{next: map[string][]string{}, prev: map[string][]string{}, cond: map[string]string{}} for _, e := range edges { f := strings.TrimSpace(e.From) t := strings.TrimSpace(e.To) if f == "" || t == "" || f == t { continue } if !containsStr(g.next[f], t) { g.next[f] = append(g.next[f], t) } if !containsStr(g.prev[t], f) { g.prev[t] = append(g.prev[t], f) } if c := strings.TrimSpace(e.Condition); c != "" { g.cond[t] = c } } return g } // rebuildEdges 由节点 next_keys 邻接重建画布连线;连线条件取目标节点 condition_expr func rebuildEdges(nodes []flowEdgeNode) []approvalEdge { condByKey := make(map[string]string, len(nodes)) for _, n := range nodes { condByKey[n.NodeKey] = n.Condition } edges := make([]approvalEdge, 0) for _, n := range nodes { for _, to := range splitKeyList(n.NextKeys) { edges = append(edges, approvalEdge{From: n.NodeKey, To: to, Condition: condByKey[to]}) } } return edges } // validateApprovalGraph 校验画布图的完整性:单一开始、审批节点≥1、判断节点出边≥2、全部可达、连线端点有效 func validateApprovalGraph(nodes []approvalNodePayload, edges []approvalEdge) error { if len(nodes) == 0 { return fmt.Errorf("流程至少需包含开始与审批节点") } keySet := map[string]bool{} var startCount, approvalCount int var condKeys []string var startKey string for _, n := range nodes { k := strings.TrimSpace(n.NodeKey) if k == "" { return fmt.Errorf("存在缺少标识的节点,请重新连线后再保存") } if keySet[k] { return fmt.Errorf("节点标识重复:%s", k) } keySet[k] = true switch n.NodeType { case models.ApprovalNodeStart: startCount++ startKey = k case models.ApprovalNodeApproval: approvalCount++ case models.ApprovalNodeCondition: condKeys = append(condKeys, k) } } if startCount != 1 { return fmt.Errorf("流程需有且仅有一个「开始」节点") } if approvalCount < 1 { return fmt.Errorf("流程需至少包含一个「审批」节点") } nextAdj := map[string][]string{} for _, e := range edges { f := strings.TrimSpace(e.From) t := strings.TrimSpace(e.To) if !keySet[f] || !keySet[t] { return fmt.Errorf("存在连向未知节点的连线,请检查画布") } nextAdj[f] = append(nextAdj[f], t) } for _, k := range condKeys { if len(nextAdj[k]) < 2 { return fmt.Errorf("判断节点需至少两条分支连线") } } visited := map[string]bool{} queue := []string{startKey} for len(queue) > 0 { cur := queue[0] queue = queue[1:] if visited[cur] { continue } visited[cur] = true queue = append(queue, nextAdj[cur]...) } for _, n := range nodes { if !visited[strings.TrimSpace(n.NodeKey)] { return fmt.Errorf("节点「%s」未接入流程,请检查连线", strings.TrimSpace(n.NodeName)) } } return nil } // genApprovalFlowCode 由自增 id 生成审批流编号(全局唯一、供各程序按编号发起) func genApprovalFlowCode(id uint64) string { return fmt.Sprintf("FL%06d", id) }