package controllers import ( "encoding/json" "io" "strconv" "strings" "time" "server/models" "server/pkg/identity" "server/pkg/jwtutil" "server/services" approvalEngine "server/services/approval" "github.com/beego/beego/v2/client/orm" beego "github.com/beego/beego/v2/server/web" ) // BackendApprovalRuntimeController Web 后台「审批中心」运行时接口。 // // 与移动端 /api/oa/approval/* 共用同一套审批引擎与视图函数,差别只在鉴权: // 移动端走 oaClaims(backend/app/platform 三类 token),后台只认 backend token。 // // 接口清单: // // GET /backend/approval/instance/mine 我发起的审批 // GET /backend/approval/instance/todo 待我审批(handled=1 返回我已处理) // GET /backend/approval/instance/detail/:id 审批详情(含流程时间线) // POST /backend/approval/instance/approve 通过 / 驳回 // POST /backend/approval/instance/withdraw/:id 发起人撤回 type BackendApprovalRuntimeController struct { beego.Controller } func (c *BackendApprovalRuntimeController) fail(httpStatus, bizCode int, msg string) { jsonErr(&c.Controller, httpStatus, bizCode, msg) } func (c *BackendApprovalRuntimeController) ok(data interface{}) { c.Data["json"] = map[string]interface{}{"code": 200, "msg": "ok", "data": data} _ = c.ServeJSON() } func (c *BackendApprovalRuntimeController) idParam() uint64 { id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64) if err != nil { return 0 } return id } // approvalRuntimeUID 当前登录用户在租户侧的 UID(审批人以 yz_system_tenant_user.id 记录) func approvalRuntimeUID(claims *jwtutil.Claims) uint64 { return services.ResolveTenantUID(uint64(claims.TenantId), uint64(claims.UserID)) } // ============================ 我发起的 ============================ // InstanceMine GET /backend/approval/instance/mine?page=&pageSize=&keyword=&status= // 我发起的审批实例列表。 func (c *BackendApprovalRuntimeController) InstanceMine() { claims, err := requireBackend(&c.Controller) if err != nil { code := identity.Status(err) c.fail(code, code, err.Error()) return } tid := uint64(claims.TenantId) uid := approvalRuntimeUID(claims) page, _ := c.GetInt("page", 1) pageSize, _ := c.GetInt("pageSize", 10) if page < 1 { page = 1 } if pageSize < 1 || pageSize > 100 { pageSize = 10 } qs := models.Orm.QueryTable(new(models.ApprovalInstance)). Filter("scope", models.ApprovalScopeTenant). Filter("tid", tid). Filter("submit_user_id", uid). Filter("is_deleted", 0) if keyword := strings.TrimSpace(c.GetString("keyword")); keyword != "" { qs = qs.Filter("biz_title__icontains", keyword) } if status, err := c.GetInt("status"); err == nil { qs = qs.Filter("status", status) } total, _ := qs.Count() var rows []models.ApprovalInstance _, err = qs.OrderBy("-id").Limit(pageSize, (page-1)*pageSize).All(&rows) if err != nil && err != orm.ErrNoRows { c.fail(500, 500, "查询失败") return } list := make([]map[string]interface{}, 0, len(rows)) for _, row := range rows { view := oaApprovalInstanceView(row) view["flow_name"] = approvalRuntimeFlowName(tid, row.FlowID) list = append(list, view) } c.ok(map[string]interface{}{"list": list, "total": total, "page": page, "pageSize": pageSize}) } // ============================ 待我审批 ============================ // InstanceTodo GET /backend/approval/instance/todo?handled=0|1 // 我作为审批人的任务列表,默认取待办;handled=1 时返回我已处理过的任务。 func (c *BackendApprovalRuntimeController) InstanceTodo() { claims, err := requireBackend(&c.Controller) if err != nil { code := identity.Status(err) c.fail(code, code, err.Error()) return } tid := uint64(claims.TenantId) uid := approvalRuntimeUID(claims) page, _ := c.GetInt("page", 1) pageSize, _ := c.GetInt("pageSize", 10) if page < 1 { page = 1 } if pageSize < 1 || pageSize > 100 { pageSize = 10 } handled, _ := c.GetInt("handled", 0) qs := models.Orm.QueryTable(new(models.ApprovalTask)). Filter("scope", models.ApprovalScopeTenant). Filter("tid", tid). Filter("approver_id", uid). Filter("is_deleted", 0) if handled == 1 { qs = qs.Filter("status__in", models.ApprovalTaskApproved, models.ApprovalTaskRejected, models.ApprovalTaskSkipped, models.ApprovalTaskTimeout) } else { qs = qs.Filter("status", models.ApprovalTaskPending) } total, _ := qs.Count() var tasks []models.ApprovalTask _, err = qs.OrderBy("-id").Limit(pageSize, (page-1)*pageSize).All(&tasks) if err != nil && err != orm.ErrNoRows { c.fail(500, 500, "查询失败") return } list := make([]map[string]interface{}, 0, len(tasks)) for _, t := range tasks { var inst models.ApprovalInstance if models.Orm.QueryTable(new(models.ApprovalInstance)). Filter("id", t.InstanceID).Filter("is_deleted", 0).One(&inst) != nil { continue } // 待办列表只保留仍在审批中的实例,已结束的实例不再展示为待办 if handled == 0 && inst.Status != models.ApprovalInstanceRunning { continue } taskView := oaApprovalTaskView(t, true, oaApprovalNodeTypes(inst)[t.NodeKey]) taskView["instance"] = oaApprovalInstanceView(inst) list = append(list, taskView) } c.ok(map[string]interface{}{"list": list, "total": total, "page": page, "pageSize": pageSize}) } // ============================ 详情 ============================ // InstanceDetail GET /backend/approval/instance/detail/:id // 返回实例信息 + 首尾齐备的流程时间线(复用移动端同一套视图)。 func (c *BackendApprovalRuntimeController) InstanceDetail() { claims, err := requireBackend(&c.Controller) if err != nil { code := identity.Status(err) c.fail(code, code, err.Error()) return } tid := uint64(claims.TenantId) uid := approvalRuntimeUID(claims) id := c.idParam() if id == 0 { c.fail(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.fail(404, 404, "审批不存在") return } tasks := oaApprovalTasksByInstance(inst.ID) timeline := oaApprovalFlowTimeline(inst, tasks) // 当前用户可操作的任务(存在即说明本人是当前审批人) var currentTaskID uint64 canApprove := false for _, t := range tasks { if t.ApproverID == uid && t.Status == models.ApprovalTaskPending { canApprove = true currentTaskID = t.ID break } } c.ok(map[string]interface{}{ "item": oaApprovalInstanceView(inst), "timeline": timeline, "flow_name": approvalRuntimeFlowName(tid, inst.FlowID), "can_approve": canApprove, "can_withdraw": inst.SubmitUserID == uid && inst.Status == models.ApprovalInstanceRunning, "current_task_id": currentTaskID, }) } // ============================ 审批动作 ============================ type approvalRuntimeApprovePayload struct { TaskID uint64 `json:"task_id"` Pass bool `json:"pass"` Comment string `json:"comment"` } // Approve POST /backend/approval/instance/approve {task_id, pass, comment} func (c *BackendApprovalRuntimeController) Approve() { claims, err := requireBackend(&c.Controller) if err != nil { code := identity.Status(err) c.fail(code, code, err.Error()) return } tid := uint64(claims.TenantId) uid := approvalRuntimeUID(claims) var p approvalRuntimeApprovePayload raw, err := io.ReadAll(c.Ctx.Request.Body) if len(raw) > 0 && (err != nil || json.Unmarshal(raw, &p) != nil) { c.fail(400, 400, "参数错误") return } if p.TaskID == 0 { c.fail(400, 400, "缺少审批任务ID") 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.fail(404, 404, "任务不存在") return } if task.ApproverID != uid { c.fail(403, 403, "您不是该任务的审批人") return } res, err := approvalEngine.Approve(task.ID, approvalEngine.ApproveInput{ ApproverID: uid, Pass: p.Pass, Comment: strings.TrimSpace(p.Comment), }) if err != nil { c.fail(400, 400, err.Error()) return } c.ok(map[string]interface{}{ "instance_id": res.InstanceID, "instance_stat": res.InstanceStat, "finished": res.Finished, "message": res.Message, }) } // Withdraw POST /backend/approval/instance/withdraw/:id 发起人撤回(仅审批中) func (c *BackendApprovalRuntimeController) Withdraw() { claims, err := requireBackend(&c.Controller) if err != nil { code := identity.Status(err) c.fail(code, code, err.Error()) return } tid := uint64(claims.TenantId) uid := approvalRuntimeUID(claims) id := c.idParam() if id == 0 { c.fail(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.fail(404, 404, "审批不存在") return } if inst.SubmitUserID != uid { c.fail(403, 403, "仅发起人可撤回") return } if inst.Status != models.ApprovalInstanceRunning { c.fail(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) c.ok(nil) } // approvalRuntimeFlowName 取流程名称(供列表/详情展示),流程被删除时返回空串。 func approvalRuntimeFlowName(tid, flowID uint64) string { if flowID == 0 { return "" } var f models.TenantApprovalFlow if err := models.Orm.QueryTable(new(models.TenantApprovalFlow)). Filter("id", flowID).Filter("tid", tid).One(&f); err != nil { return "" } return f.FlowName }