package controllers import ( "encoding/json" "io" "sort" "strconv" "strings" "time" "server/models" "server/pkg/identity" "server/pkg/jwtutil" "server/services" approvalEngine "server/services/approval" "github.com/beego/beego/v2/client/orm" ) // 移动端审批接口,挂在 AppOaController 上。 // // 设计说明: // - 数据按 JWT 的 tid + user 双重隔离:发起列表仅看本人发起的审批, // 待办列表仅看本人作为审批人的待办; // - 复用 services/approval 引擎的 Submit / Approve,保证与 Web 端行为一致; // - 单据类型(biz_type)取审批流 yz_backend_approval_flow.biz_type; // 移动端不开放自由单据类型,仅允许本租户已配置的分类。 // // 路由前缀:/api/oa/approval/* // // 接口清单: // GET /api/oa/approval/list 我发起的审批实例 // GET /api/oa/approval/todo 我的待办(作为审批人) // GET /api/oa/approval/detail/:id 审批详情(含任务时间线) // POST /api/oa/approval/submit 提交审批 // POST /api/oa/approval/approve 通过/驳回审批任务 // POST /api/oa/approval/withdraw/:id 发起人撤回审批 // GET /api/oa/approval/flows 可发起的流程清单(按分类) // GET /api/oa/approval/categories 单据类型(biz_type)下拉 // ============================ 入参 ============================ // oaApprovalSubmitPayload 提交审批入参 type oaApprovalSubmitPayload struct { BizType string `json:"biz_type"` // 单据类型(必填) FlowCode string `json:"flow_code"` // 指定流程(可选;留空取该 biz_type 默认流) FlowID uint64 `json:"flow_id"` // 指定流程ID(可选) BizTitle string `json:"biz_title"` // 单据标题(必填) BizID uint64 `json:"biz_id"` // 关联业务单据ID(可选) FormData map[string]interface{} `json:"form_data"` // 表单数据 } // oaApprovalApprovePayload 审批入参 type oaApprovalApprovePayload struct { TaskID uint64 `json:"task_id"` // 待审批任务ID(必填) Pass bool `json:"pass"` // true 通过 false 驳回 Comment string `json:"comment"` // 审批意见 } // ============================ 工具 ============================ // oaApprovalIDParam 解析 :id 路由参数 func oaApprovalIDParam(c *AppOaController) uint64 { id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64) if err != nil { return 0 } return id } // oaApprovalClaims 解析并校验 JWT func oaApprovalClaims(c *AppOaController) (*jwtutil.Claims, bool) { claims, err := c.oaClaims() if err != nil { code := identity.Status(err) c.oaErr(code, code, err.Error()) return nil, false } return claims, true } // oaApprovalEmployee 通过 JWT 找到当前登录用户对应的员工记录(用于取部门 ID) func oaApprovalEmployee(c *AppOaController, claims *jwtutil.Claims) *models.BackendEmployee { emp, ok := c.oaMyEmployee(claims) if !ok { return nil } return emp } // oaApprovalSubmitUID 通过 JWT 拿到租户侧的 UID(SystemTenantUser.id), // 审批引擎 SubmitInput.SubmitUID 用的是租户侧用户 ID 而非平台 AdminUser.ID。 func oaApprovalSubmitUID(claims *jwtutil.Claims) uint64 { return services.ResolveTenantUID(uint64(claims.TenantId), uint64(claims.UserID)) } // oaApprovalReadJSON 读取并解析 JSON 请求体,失败时输出错误响应 func oaApprovalReadJSON(c *AppOaController, dst interface{}) bool { raw, err := io.ReadAll(c.Ctx.Request.Body) if err != nil { c.oaErr(400, 400, "参数错误") return false } if len(raw) == 0 { return true } if err := json.Unmarshal(raw, dst); err != nil { c.oaErr(400, 400, "参数错误: "+err.Error()) return false } return true } // oaApprovalUserName 取得发起人姓名(优先员工表,否则使用 JWT 用户名) func oaApprovalUserName(c *AppOaController, claims *jwtutil.Claims) string { if emp := oaApprovalEmployee(c, claims); emp != nil && emp.Name != "" { return emp.Name } return strings.TrimSpace(claims.Username) } // ============================ 实例组装 ============================ // oaApprovalStatusText / oaApprovalStatusTag 状态展示文案/Tag 类型(uview-plus) func oaApprovalStatusText(status int8) string { switch status { case models.ApprovalInstanceDraft: return "草稿" case models.ApprovalInstanceRunning: return "审批中" case models.ApprovalInstanceApproved: return "已通过" case models.ApprovalInstanceRejected: return "已驳回" case models.ApprovalInstanceWithdrawn: return "已撤回" case models.ApprovalInstanceCanceled: return "已取消" default: return "未知" } } // oaApprovalStatusTagType 给前端 uview-plus u-tag 用的类型映射 func oaApprovalStatusTagType(status int8) string { switch status { case models.ApprovalInstanceRunning: return "warning" case models.ApprovalInstanceApproved: return "success" case models.ApprovalInstanceRejected: return "error" case models.ApprovalInstanceWithdrawn, models.ApprovalInstanceCanceled: return "info" default: return "info" } } // oaApprovalInstanceView 把 ApprovalInstance 组装为移动端列表/详情通用的视图结构 func oaApprovalInstanceView(inst models.ApprovalInstance) map[string]interface{} { submitTime := "" if inst.SubmitTime != nil { submitTime = inst.SubmitTime.Format("2006-01-02 15:04:05") } finishTime := "" if inst.FinishTime != nil { finishTime = inst.FinishTime.Format("2006-01-02 15:04:05") } createTime := "" if !inst.CreateTime.IsZero() { createTime = inst.CreateTime.Format("2006-01-02 15:04:05") } return map[string]interface{}{ "id": inst.ID, "biz_type": inst.BizType, "flow_id": inst.FlowID, "biz_id": inst.BizID, "biz_title": inst.BizTitle, "submit_user_id": inst.SubmitUserID, "submit_user_name": inst.SubmitUserName, "submit_dept_id": inst.SubmitDeptID, "current_node": inst.CurrentNode, "status": inst.Status, "status_label": oaApprovalStatusText(inst.Status), "status_tag": oaApprovalStatusTagType(inst.Status), "submit_time": submitTime, "finish_time": finishTime, "create_time": createTime, } } // oaApprovalBizTypes 返回本租户已配置(启用 + 未删除)的 biz_type 列表 func oaApprovalBizTypes(tid uint64) []map[string]interface{} { var flows []models.TenantApprovalFlow _, _ = models.Orm.QueryTable(new(models.TenantApprovalFlow)). Filter("tid", tid).Filter("is_deleted", 0).Filter("status", 1). OrderBy("module_code", "id").All(&flows) seen := map[string]bool{} list := make([]map[string]interface{}, 0, len(flows)) for _, f := range flows { biz := strings.TrimSpace(f.BizType) if biz == "" { biz = strings.TrimSpace(f.ModuleCode) } if biz == "" || seen[biz] { continue } seen[biz] = true list = append(list, map[string]interface{}{ "value": biz, "label": biz, "module_code": f.ModuleCode, }) } sort.Slice(list, func(i, j int) bool { return list[i]["value"].(string) < list[j]["value"].(string) }) return list } // ============================ 任务时间线 ============================ // oaApprovalTasksByInstance 取出某实例下的全部任务(按 sort / id 升序), // 同时按 (instance_id, node_key) 找当前节点的当前审批人。 func oaApprovalTasksByInstance(instanceID uint64) []models.ApprovalTask { var tasks []models.ApprovalTask _, _ = models.Orm.QueryTable(new(models.ApprovalTask)). Filter("instance_id", instanceID).Filter("is_deleted", 0). OrderBy("sort", "id").All(&tasks) return tasks } // oaApprovalTaskText / oaApprovalTaskTag 任务级状态文案/Tag 类型 func oaApprovalTaskText(status int8) string { switch status { case models.ApprovalTaskPending: return "待审批" case models.ApprovalTaskApproved: return "已通过" case models.ApprovalTaskRejected: return "已驳回" case models.ApprovalTaskReturned: return "已退回" case models.ApprovalTaskSkipped: return "已跳过" case models.ApprovalTaskTimeout: return "已超时" case models.ApprovalTaskNotified: return "已通知" default: return "未知" } } func oaApprovalTaskTag(status int8) string { switch status { case models.ApprovalTaskPending: return "warning" case models.ApprovalTaskApproved: return "success" case models.ApprovalTaskRejected: return "error" case models.ApprovalTaskReturned, models.ApprovalTaskSkipped, models.ApprovalTaskTimeout: return "info" default: return "info" } } // oaApprovalTaskView 任务视图(移动端流程时间线用) // nodeType 来自流程定义(开始/审批/通知/结束),取不到时按审批节点处理。 func oaApprovalTaskView(t models.ApprovalTask, isCurrent bool, nodeType int8) map[string]interface{} { finishTime := "" if t.FinishTime != nil { finishTime = t.FinishTime.Format("2006-01-02 15:04:05") } deadline := "" if t.Deadline != nil { deadline = t.Deadline.Format("2006-01-02 15:04:05") } if nodeType == 0 { nodeType = models.ApprovalNodeApproval } return map[string]interface{}{ "id": t.ID, "node_key": t.NodeKey, "node_name": t.NodeName, "node_type": nodeType, "approver_id": t.ApproverID, "approver_name": t.ApproverName, "approver_sub": oaApprovalUserOrgName(t.Tid, t.ApproverID), "multi_mode": t.MultiMode, "status": t.Status, "status_label": oaApprovalTaskText(t.Status), "status_tag": oaApprovalTaskTag(t.Status), "action": t.Action, "comment": t.Comment, "is_current": isCurrent, "deadline": deadline, "finish_time": finishTime, "create_time": t.CreateTime.Format("2006-01-02 15:04:05"), } } // oaApprovalUserOrgName 审批人所属部门名,用作流程节点副标题(如「财务部」),取不到返回空串。 func oaApprovalUserOrgName(tid, uid uint64) string { if tid == 0 || uid == 0 { return "" } var tu models.SystemTenantUser if err := models.Orm.QueryTable(new(models.SystemTenantUser)). Filter("tid", tid).Filter("uid", uid). Filter("delete_time__isnull", true).One(&tu); err != nil { return "" } if tu.OrgID == 0 { return "" } var org models.BackendOrganization if err := models.Orm.QueryTable(new(models.BackendOrganization)). Filter("id", tu.OrgID).One(&org); err != nil { return "" } return org.OrgName } // 时间线虚拟节点的 node_key:用于标识流程的开始 / 结束,前端据此区分渲染。 const ( oaApprovalStartKey = "__start__" oaApprovalEndKey = "__end__" ) // oaApprovalFlowTimeline 组装移动端「审批流程」时间线: // 首尾固定为提交申请(开始)与流程结束(结束),中间是流程产生的节点任务, // 让发起人一眼看出流程当前走到哪、是否已经走完。 func oaApprovalFlowTimeline(inst models.ApprovalInstance, tasks []models.ApprovalTask) []map[string]interface{} { nodeTypes := oaApprovalNodeTypes(inst) timeline := make([]map[string]interface{}, 0, len(tasks)+2) timeline = append(timeline, oaApprovalStartNode(inst)) for _, t := range tasks { timeline = append(timeline, oaApprovalTaskView(t, t.NodeKey == inst.CurrentNode, nodeTypes[t.NodeKey])) } timeline = append(timeline, oaApprovalEndNode(inst)) return timeline } // oaApprovalNodeTypes 读流程定义,取各节点类型(开始/审批/通知/结束)。 // 流程被删除或改版时返回空表,调用方按审批节点兜底。 func oaApprovalNodeTypes(inst models.ApprovalInstance) map[string]int8 { out := make(map[string]int8) flow, err := approvalEngine.LoadFlow(inst.Scope, inst.Tid, inst.BizType, inst.FlowID) if err != nil { return out } for i := range flow.Nodes { out[flow.Nodes[i].NodeKey] = flow.Nodes[i].NodeType } return out } // oaApprovalStartNode 虚拟开始节点:即发起人提交动作。 func oaApprovalStartNode(inst models.ApprovalInstance) map[string]interface{} { timeStr := "" if inst.SubmitTime != nil { timeStr = inst.SubmitTime.Format("2006-01-02 15:04:05") } else if !inst.CreateTime.IsZero() { timeStr = inst.CreateTime.Format("2006-01-02 15:04:05") } return map[string]interface{}{ "id": 0, "node_key": oaApprovalStartKey, "node_name": "提交申请", "node_type": models.ApprovalNodeStart, "approver_id": inst.SubmitUserID, "approver_name": inst.SubmitUserName, "approver_sub": oaApprovalUserOrgName(inst.Tid, inst.SubmitUserID), "multi_mode": 1, "status": models.ApprovalTaskApproved, "status_label": "已提交", "status_tag": "success", "action": 0, "comment": "", "is_current": false, "deadline": "", "finish_time": timeStr, "create_time": timeStr, } } // oaApprovalEndNode 虚拟结束节点:实例运行中为「待完成」,结束后展示最终结果与完成时间。 func oaApprovalEndNode(inst models.ApprovalInstance) map[string]interface{} { finishTime := "" if inst.FinishTime != nil { finishTime = inst.FinishTime.Format("2006-01-02 15:04:05") } end := map[string]interface{}{ "id": 0, "node_key": oaApprovalEndKey, "node_name": "流程结束", "node_type": models.ApprovalNodeEnd, "approver_id": 0, "approver_name": "", "multi_mode": 1, "status": models.ApprovalTaskPending, "status_label": "待完成", "status_tag": "info", "action": 0, "comment": "", "is_current": false, "deadline": "", "finish_time": finishTime, "create_time": finishTime, } switch inst.Status { case models.ApprovalInstanceApproved: end["status"] = models.ApprovalTaskApproved end["status_label"] = "已通过" end["status_tag"] = "success" case models.ApprovalInstanceRejected: end["status"] = models.ApprovalTaskRejected end["status_label"] = "已驳回" end["status_tag"] = "error" case models.ApprovalInstanceWithdrawn: end["status"] = models.ApprovalTaskSkipped end["status_label"] = "已撤回" end["status_tag"] = "info" case models.ApprovalInstanceCanceled: end["status"] = models.ApprovalTaskSkipped end["status_label"] = "已取消" end["status_tag"] = "info" } return end } // ============================ 路由方法 ============================ // ApprovalCategories GET /api/oa/approval/categories // 返回本租户可用的 biz_type 下拉项,移动端「发起审批」选择单据类型使用。 func (c *AppOaController) ApprovalCategories() { claims, ok := oaApprovalClaims(c) if !ok { return } c.oaOk(map[string]interface{}{ "list": oaApprovalBizTypes(uint64(claims.TenantId)), "total": len(oaApprovalBizTypes(uint64(claims.TenantId))), }) } // ApprovalFlows GET /api/oa/approval/flows // 按 biz_type 返回本租户可发起的流程(含流程名 + 节点名列表), // 用于「选择流程」下拉。biz_type 为空时返回全部。 func (c *AppOaController) ApprovalFlows() { claims, ok := oaApprovalClaims(c) if !ok { return } tid := uint64(claims.TenantId) biz := strings.TrimSpace(c.GetString("biz_type")) qs := models.Orm.QueryTable(new(models.TenantApprovalFlow)). Filter("tid", tid).Filter("is_deleted", 0).Filter("status", 1) if biz != "" { qs = qs.Filter("biz_type", biz) } var flows []models.TenantApprovalFlow _, _ = qs.OrderBy("is_default", "id").All(&flows) list := make([]map[string]interface{}, 0, len(flows)) for _, f := range flows { var nodes []models.TenantApprovalNode _, _ = models.Orm.QueryTable(new(models.TenantApprovalNode)). Filter("flow_id", f.ID).Filter("is_deleted", 0). OrderBy("sort", "id").All(&nodes) nodeNames := make([]string, 0, len(nodes)) for _, n := range nodes { if n.NodeType == models.ApprovalNodeApproval { nodeNames = append(nodeNames, n.NodeName) } } list = append(list, map[string]interface{}{ "id": f.ID, "flow_code": f.FlowCode, "flow_name": f.FlowName, "biz_type": f.BizType, "module_code": f.ModuleCode, "is_default": f.IsDefault, "nodes": nodeNames, }) } c.oaOk(map[string]interface{}{"list": list, "total": len(list)}) } // ApprovalList GET /api/oa/approval/list // 我发起的审批实例(移动端「我的发起」列表)。 // 支持 keyword / status / biz_type 过滤。 func (c *AppOaController) ApprovalList() { claims, ok := oaApprovalClaims(c) if !ok { return } tid := uint64(claims.TenantId) uid := oaApprovalSubmitUID(claims) page, _ := c.GetInt("page", 1) pageSize, _ := c.GetInt("pageSize", 20) if page < 1 { page = 1 } if pageSize < 1 || pageSize > 100 { pageSize = 20 } keyword := strings.TrimSpace(c.GetString("keyword")) statusStr := strings.TrimSpace(c.GetString("status")) bizType := strings.TrimSpace(c.GetString("biz_type")) qs := models.Orm.QueryTable(new(models.ApprovalInstance)). Filter("scope", models.ApprovalScopeTenant). Filter("tid", tid). Filter("submit_user_id", uid). Filter("is_deleted", 0) if bizType != "" { qs = qs.Filter("biz_type", bizType) } if statusStr != "" { if s, err := strconv.Atoi(statusStr); err == nil { qs = qs.Filter("status", int8(s)) } } total, _ := qs.Count() var rows []models.ApprovalInstance _, err := qs.OrderBy("-id"). Limit(pageSize).Offset((page - 1) * pageSize).All(&rows) if err != nil && err != orm.ErrNoRows { c.oaErr(500, 500, "查询失败") return } list := make([]map[string]interface{}, 0, len(rows)) for _, inst := range rows { item := oaApprovalInstanceView(inst) if keyword != "" { if !strings.Contains(strings.ToLower(inst.BizTitle), strings.ToLower(keyword)) { continue } } list = append(list, item) } c.oaOk(map[string]interface{}{ "list": list, "total": total, "page": page, "pageSize": pageSize, }) } // ApprovalTodo GET /api/oa/approval/todo // 我的待办(作为审批人)。返回任务列表 + 关联实例基本信息, // 移动端「待我审批」直接展示。 func (c *AppOaController) ApprovalTodo() { claims, ok := oaApprovalClaims(c) if !ok { return } tid := uint64(claims.TenantId) uid := oaApprovalSubmitUID(claims) page, _ := c.GetInt("page", 1) pageSize, _ := c.GetInt("pageSize", 20) if page < 1 { page = 1 } if pageSize < 1 || pageSize > 100 { pageSize = 20 } qs := models.Orm.QueryTable(new(models.ApprovalTask)). Filter("scope", models.ApprovalScopeTenant). Filter("tid", tid). Filter("approver_id", uid). Filter("status", models.ApprovalTaskPending). Filter("is_deleted", 0) total, _ := qs.Count() var tasks []models.ApprovalTask _, err := qs.OrderBy("-id"). Limit(pageSize).Offset((page - 1) * pageSize).All(&tasks) if err != nil && err != orm.ErrNoRows { c.oaErr(500, 500, "查询失败") return } list := make([]map[string]interface{}, 0, len(tasks)) for _, t := range tasks { // 关联查询实例,校验实例仍在审批中(防止实例已结束但任务未及时同步) var inst models.ApprovalInstance if err := models.Orm.QueryTable(new(models.ApprovalInstance)). Filter("id", t.InstanceID).Filter("is_deleted", 0).One(&inst); err != nil { continue } if inst.Status != models.ApprovalInstanceRunning { continue } view := oaApprovalTaskView(t, true, oaApprovalNodeTypes(inst)[t.NodeKey]) view["instance"] = oaApprovalInstanceView(inst) list = append(list, view) } c.oaOk(map[string]interface{}{ "list": list, "total": total, "page": page, "pageSize": pageSize, }) } // ApprovalDetail GET /api/oa/approval/detail/:id // 审批详情:实例基本信息 + 任务时间线 + 当前节点可执行动作。 func (c *AppOaController) ApprovalDetail() { claims, ok := oaApprovalClaims(c) if !ok { return } tid := uint64(claims.TenantId) uid := oaApprovalSubmitUID(claims) id := oaApprovalIDParam(c) if id == 0 { c.oaErr(400, 400, "无效ID") return } var inst models.ApprovalInstance if err := models.Orm.QueryTable(new(models.ApprovalInstance)). Filter("id", id).Filter("tid", tid). Filter("scope", models.ApprovalScopeTenant). Filter("is_deleted", 0).One(&inst); err != nil { c.oaErr(404, 404, "审批不存在") return } // 权限:仅发起人或当前节点的审批人可查看 // 两者独立判断:同一个用户既可能是发起人也可能是审批人(如自审批场景) isSubmitter := inst.SubmitUserID == uid isApprover := false var currentTaskID uint64 { var t models.ApprovalTask err := models.Orm.QueryTable(new(models.ApprovalTask)). Filter("instance_id", inst.ID). Filter("approver_id", uid). Filter("status", models.ApprovalTaskPending). Filter("is_deleted", 0).One(&t) if err == nil { isApprover = true currentTaskID = t.ID } } if !isSubmitter && !isApprover { c.oaErr(403, 403, "无权查看该审批") return } tasks := oaApprovalTasksByInstance(inst.ID) timeline := oaApprovalFlowTimeline(inst, tasks) c.oaOk(map[string]interface{}{ "item": oaApprovalInstanceView(inst), "timeline": timeline, "can_approve": isApprover, "can_withdraw": isSubmitter && inst.Status == models.ApprovalInstanceRunning, "current_task_id": currentTaskID, }) } // ApprovalSubmit POST /api/oa/approval/submit // 提交审批(发起)。引擎内部完成:创建实例 → 找首个审批节点 → 生成任务。 func (c *AppOaController) ApprovalSubmit() { claims, ok := oaApprovalClaims(c) if !ok { return } var p oaApprovalSubmitPayload if !oaApprovalReadJSON(c, &p) { return } if strings.TrimSpace(p.BizType) == "" { c.oaErr(400, 400, "请选择单据类型") return } if strings.TrimSpace(p.BizTitle) == "" { c.oaErr(400, 400, "请填写单据标题") return } tid := uint64(claims.TenantId) uid := oaApprovalSubmitUID(claims) // 发起人所在部门(用于审批引擎按部门主管匹配审批人) var deptID uint64 var tu models.SystemTenantUser if models.Orm.QueryTable(new(models.SystemTenantUser)). Filter("uid", uid).Filter("tid", tid). Filter("delete_time__isnull", true).One(&tu) == nil { deptID = tu.OrgID } in := approvalEngine.SubmitInput{ Scope: models.ApprovalScopeTenant, Tid: tid, BizType: strings.TrimSpace(p.BizType), FlowCode: strings.TrimSpace(p.FlowCode), FlowID: p.FlowID, BizID: p.BizID, BizTitle: strings.TrimSpace(p.BizTitle), SubmitUID: uid, SubmitName: oaApprovalUserName(c, claims), DeptID: deptID, FormData: p.FormData, } instID, err := approvalEngine.Submit(in) if err != nil { // ErrNoFlow / ErrNoApprover 等业务错误统一以 400 返回 c.oaErr(400, 400, err.Error()) return } var inst models.ApprovalInstance _ = models.Orm.QueryTable(new(models.ApprovalInstance)). Filter("id", instID).One(&inst) c.oaOk(map[string]interface{}{ "id": instID, "item": oaApprovalInstanceView(inst), }) } // ApprovalApprove POST /api/oa/approval/approve // 审批人通过/驳回一条任务。 func (c *AppOaController) ApprovalApprove() { claims, ok := oaApprovalClaims(c) if !ok { return } tid := uint64(claims.TenantId) uid := oaApprovalSubmitUID(claims) var p oaApprovalApprovePayload if !oaApprovalReadJSON(c, &p) { return } if p.TaskID == 0 { // 发起人视角在「我的发起」详情里也会看到「通过」按钮(操作入口对齐 Web 端), // 但发起人不是当前审批人,没有可处理的 task:给一个明确的提示而不是含糊的「缺少任务ID」。 c.oaErr(403, 403, "您不是当前审批人,无法通过/驳回该审批") return } // 校验任务归属本租户、当前审批人 var task models.ApprovalTask if err := models.Orm.QueryTable(new(models.ApprovalTask)). Filter("id", p.TaskID). Filter("tid", tid). Filter("scope", models.ApprovalScopeTenant). Filter("is_deleted", 0).One(&task); err != nil { c.oaErr(404, 404, "任务不存在") return } if task.ApproverID != uid { c.oaErr(403, 403, "您无权处理该任务") return } if task.Status != models.ApprovalTaskPending { c.oaErr(400, 400, "该任务已处理") return } res, err := approvalEngine.Approve(p.TaskID, approvalEngine.ApproveInput{ ApproverID: uid, Pass: p.Pass, Comment: strings.TrimSpace(p.Comment), }) if err != nil { c.oaErr(500, 500, err.Error()) return } var inst models.ApprovalInstance _ = models.Orm.QueryTable(new(models.ApprovalInstance)). Filter("id", res.InstanceID).One(&inst) c.oaOk(map[string]interface{}{ "finished": res.Finished, "instance_stat": res.InstanceStat, "current_node": res.CurrentNode, "message": res.Message, "item": oaApprovalInstanceView(inst), }) } // ApprovalWithdraw POST /api/oa/approval/withdraw/:id // 发起人撤回(仅审批中允许)。 func (c *AppOaController) ApprovalWithdraw() { claims, ok := oaApprovalClaims(c) if !ok { return } tid := uint64(claims.TenantId) uid := oaApprovalSubmitUID(claims) id := oaApprovalIDParam(c) if id == 0 { c.oaErr(400, 400, "无效ID") return } var inst models.ApprovalInstance if err := models.Orm.QueryTable(new(models.ApprovalInstance)). Filter("id", id).Filter("tid", tid). Filter("scope", models.ApprovalScopeTenant). Filter("is_deleted", 0).One(&inst); err != nil { c.oaErr(404, 404, "审批不存在") return } if inst.SubmitUserID != uid { c.oaErr(403, 403, "仅发起人可撤回") return } if inst.Status != models.ApprovalInstanceRunning { c.oaErr(400, 400, "当前状态不允许撤回") return } now := time.Now() _, _ = models.Orm.QueryTable(new(models.ApprovalInstance)). Filter("id", id).Update(orm.Params{ "status": models.ApprovalInstanceWithdrawn, "finish_time": now, "update_time": now, }) // 同实例剩余待办置为已跳过 _, _ = models.Orm.QueryTable(new(models.ApprovalTask)). Filter("instance_id", id). Filter("status", models.ApprovalTaskPending). Update(orm.Params{"status": models.ApprovalTaskSkipped, "update_time": now}) // 回写业务单据状态(如报销单置为已撤回),保持业务列表与审批中心一致 approvalEngine.SyncBizResult(id, models.ApprovalInstanceWithdrawn) var updated models.ApprovalInstance _ = models.Orm.QueryTable(new(models.ApprovalInstance)). Filter("id", id).One(&updated) c.oaOk(map[string]interface{}{"item": oaApprovalInstanceView(updated)}) }