package controllers import ( "encoding/json" "io" "strconv" "time" "server/models" "server/pkg/identity" "server/pkg/jwtutil" approvalEngine "server/services/approval" beego "github.com/beego/beego/v2/server/web" ) // ApprovalCenterController 审批中心(OA 审批工作台): // - 待我审批 / 我已审批 / 抄送我的:按当前用户查询运行期任务(yz_approval_task); // - 我发起的:按提交人查询审批实例(yz_approval_instance); // - 通用审批操作:复用运行期引擎 services/approval 的 Approve(或签/会签/多级推进均在引擎内), // 业务侧回调(如报销单状态同步、审批记录时间线)按 biz_type 分发。 // // 目前走引擎的 OA 业务为报销(biz_type=reimburse,回退 oa),后续新业务接入后无需改本控制器。 type ApprovalCenterController struct { beego.Controller } // centerTaskType 任务列表视角:pending-待我审批 done-我已审批 cc-抄送我的 const centerTaskTypeAll = "pending|done|cc" func (c *ApprovalCenterController) centerJSON(httpStatus, bizCode int, msg string, data interface{}) { c.Ctx.Output.SetStatus(httpStatus) resp := map[string]interface{}{"code": bizCode, "msg": msg} if data != nil { resp["data"] = data } c.Data["json"] = resp _ = c.ServeJSON() } // centerClaims 解析 backend 令牌。 func (c *ApprovalCenterController) centerClaims() (*jwtutil.Claims, error) { return requireBackend(&c.Controller) } // centerTaskView 任务行(含实例概要信息),列表与操作返回共用。 type centerTaskView struct { TaskID uint64 `json:"task_id"` NodeName string `json:"node_name"` NodeSort int `json:"node_sort"` MultiMode int8 `json:"multi_mode"` Status int8 `json:"status"` Comment string `json:"comment"` FinishTime string `json:"finish_time"` Deadline string `json:"deadline"` InstanceID uint64 `json:"instance_id"` InstanceStatus int8 `json:"instance_status"` BizType string `json:"biz_type"` BizID uint64 `json:"biz_id"` BizTitle string `json:"biz_title"` FlowName string `json:"flow_name"` SubmitUserID uint64 `json:"submit_user_id"` SubmitUserName string `json:"submit_user_name"` SubmitTime string `json:"submit_time"` CurrentNode string `json:"current_node"` } // TaskList GET /backend/approval/center/task/list?type=pending|done|cc&page=&pageSize= // 我的任务列表。pending=待我审批(status=待办);done=我已审批(通过/驳回); // cc=抄送我的(通知节点生成的已通知任务)。 func (c *ApprovalCenterController) TaskList() { claims, err := c.centerClaims() if err != nil { code := identity.Status(err) c.centerJSON(code, code, err.Error(), nil) return } taskType := c.GetString("type") switch taskType { case "pending", "done", "cc": default: c.centerJSON(400, 400, "type 参数须为 "+centerTaskTypeAll, nil) return } 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.ApprovalTask)). Filter("scope", models.ApprovalScopeTenant). Filter("tid", claims.TenantId). Filter("approver_id", claims.UserID). Filter("is_deleted", 0) switch taskType { case "pending": qs = qs.Filter("status", models.ApprovalTaskPending) case "done": qs = qs.Filter("status__in", []int8{models.ApprovalTaskApproved, models.ApprovalTaskRejected}) case "cc": qs = qs.Filter("status", models.ApprovalTaskNotified) } total, _ := qs.Count() var tasks []models.ApprovalTask if _, err := qs.OrderBy("-id").Limit(pageSize).Offset((page - 1) * pageSize).All(&tasks); err != nil { c.centerJSON(500, 500, "查询任务失败", nil) return } list := make([]centerTaskView, 0, len(tasks)) if len(tasks) > 0 { instMap := c.loadInstanceMap(tasks) for _, t := range tasks { view := centerTaskView{ TaskID: t.ID, NodeName: t.NodeName, NodeSort: t.Sort, MultiMode: t.MultiMode, Status: t.Status, Comment: t.Comment, } if t.FinishTime != nil { view.FinishTime = t.FinishTime.Format("2006-01-02 15:04") } if t.Deadline != nil { view.Deadline = t.Deadline.Format("2006-01-02 15:04") } if inst, ok := instMap[t.InstanceID]; ok { view.InstanceID = inst.ID view.InstanceStatus = inst.Status view.BizType = inst.BizType view.BizID = inst.BizID view.BizTitle = inst.BizTitle view.SubmitUserID = inst.SubmitUserID view.SubmitUserName = inst.SubmitUserName if inst.SubmitTime != nil { view.SubmitTime = inst.SubmitTime.Format("2006-01-02 15:04") } view.CurrentNode = inst.CurrentNode } // 兜底:任务行自带的业务信息(实例被删时仍可展示) if view.BizTitle == "" { view.BizType = t.BizType view.BizID = t.BizID } list = append(list, view) } } c.centerJSON(200, 200, "ok", map[string]interface{}{"list": list, "total": total}) } // loadInstanceMap 批量加载任务关联的实例。 func (c *ApprovalCenterController) loadInstanceMap(tasks []models.ApprovalTask) map[uint64]models.ApprovalInstance { ids := make([]uint64, 0, len(tasks)) seen := make(map[uint64]bool) for _, t := range tasks { if t.InstanceID > 0 && !seen[t.InstanceID] { seen[t.InstanceID] = true ids = append(ids, t.InstanceID) } } instMap := make(map[uint64]models.ApprovalInstance, len(ids)) if len(ids) == 0 { return instMap } var insts []models.ApprovalInstance _, _ = models.Orm.QueryTable(new(models.ApprovalInstance)).Filter("id__in", ids).All(&insts) for _, inst := range insts { instMap[inst.ID] = inst } return instMap } // TaskAct POST /backend/approval/center/task/act // 通用审批操作(body: {task_id, pass, comment})。业务回调按 biz_type 分发: // 报销类(reimburse/oa)同步单据状态并写审批记录;其它业务仅推进引擎。 func (c *ApprovalCenterController) TaskAct() { claims, err := c.centerClaims() if err != nil { code := identity.Status(err) c.centerJSON(code, code, err.Error(), nil) return } var payload struct { TaskID uint64 `json:"task_id"` Pass bool `json:"pass"` Comment string `json:"comment"` } raw, err := io.ReadAll(c.Ctx.Request.Body) if err != nil || json.Unmarshal(raw, &payload) != nil || payload.TaskID == 0 { c.centerJSON(400, 400, "参数错误", nil) return } var task models.ApprovalTask if err := models.Orm.QueryTable(new(models.ApprovalTask)). Filter("id", payload.TaskID). Filter("tid", claims.TenantId). Filter("approver_id", claims.UserID). Filter("is_deleted", 0).One(&task); err != nil { c.centerJSON(404, 404, "审批任务不存在", nil) return } result, err := approvalEngine.Approve(task.ID, approvalEngine.ApproveInput{ ApproverID: uint64(claims.UserID), Pass: payload.Pass, Comment: payload.Comment, }) if err != nil { c.centerJSON(400, 400, err.Error(), nil) return } // OA 报销业务回调:同步单据状态 + 时间线记录 if isReimburseBizType(task.BizType) { applyReimburseApprovalResult(uint64(claims.TenantId), task.BizID, uint64(claims.UserID), claims.Username, task.Sort, payload.Pass, payload.Comment, result.Finished, result.InstanceStat) } msg := result.Message if msg == "" { msg = "操作成功" } c.centerJSON(200, 200, msg, map[string]interface{}{ "finished": result.Finished, }) } // InstanceList GET /backend/approval/center/instance/list?status=&page=&pageSize= // 我发起的审批实例列表(status 可选:1审批中 2通过 3驳回 4撤回)。 func (c *ApprovalCenterController) InstanceList() { claims, err := c.centerClaims() if err != nil { code := identity.Status(err) c.centerJSON(code, code, err.Error(), nil) return } 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", claims.TenantId). Filter("submit_user_id", claims.UserID). Filter("is_deleted", 0) if status, err := c.GetInt8("status"); err == nil && status > 0 { qs = qs.Filter("status", status) } total, _ := qs.Count() var insts []models.ApprovalInstance if _, err := qs.OrderBy("-id").Limit(pageSize).Offset((page - 1) * pageSize).All(&insts); err != nil { c.centerJSON(500, 500, "查询失败", nil) return } flowNames := c.loadFlowNames(insts) list := make([]map[string]interface{}, 0, len(insts)) for _, inst := range insts { row := map[string]interface{}{ "id": inst.ID, "biz_type": inst.BizType, "biz_id": inst.BizID, "biz_title": inst.BizTitle, "status": inst.Status, "current_node": inst.CurrentNode, "submit_time": formatCenterTime(inst.SubmitTime), "finish_time": formatCenterTime(inst.FinishTime), "submit_dept_name": "", } if name, ok := flowNames[inst.FlowID]; ok { row["flow_name"] = name } list = append(list, row) } c.centerJSON(200, 200, "ok", map[string]interface{}{"list": list, "total": total}) } // loadFlowNames 批量加载实例关联的流程名称。 func (c *ApprovalCenterController) loadFlowNames(insts []models.ApprovalInstance) map[uint64]string { ids := make([]uint64, 0, len(insts)) seen := make(map[uint64]bool) for _, inst := range insts { if inst.FlowID > 0 && !seen[inst.FlowID] { seen[inst.FlowID] = true ids = append(ids, inst.FlowID) } } names := make(map[uint64]string, len(ids)) if len(ids) == 0 { return names } var flows []models.TenantApprovalFlow _, _ = models.Orm.QueryTable(new(models.TenantApprovalFlow)).Filter("id__in", ids).All(&flows) for _, f := range flows { names[f.ID] = f.FlowName } return names } // InstanceDetail GET /backend/approval/center/instance/detail/:id // 实例详情:实例信息 + 节点任务时间线(含通知/抄送任务),按 sort 升序。 func (c *ApprovalCenterController) InstanceDetail() { claims, err := c.centerClaims() if err != nil { code := identity.Status(err) c.centerJSON(code, code, err.Error(), nil) return } id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64) if id == 0 { c.centerJSON(400, 400, "无效ID", nil) return } var inst models.ApprovalInstance if err := models.Orm.QueryTable(new(models.ApprovalInstance)). Filter("id", id). Filter("tid", claims.TenantId). Filter("is_deleted", 0).One(&inst); err != nil { c.centerJSON(404, 404, "审批实例不存在", nil) return } var tasks []models.ApprovalTask _, _ = models.Orm.QueryTable(new(models.ApprovalTask)). Filter("instance_id", inst.ID). Filter("is_deleted", 0). OrderBy("sort", "id").All(&tasks) timeline := make([]map[string]interface{}, 0, len(tasks)) for _, t := range tasks { timeline = append(timeline, map[string]interface{}{ "task_id": t.ID, "node_key": t.NodeKey, "node_name": t.NodeName, "node_sort": t.Sort, "approver_id": t.ApproverID, "approver": t.ApproverName, "status": t.Status, "comment": t.Comment, "finish_time": formatCenterTime(t.FinishTime), }) } flowName := "" if name, ok := c.loadFlowNames([]models.ApprovalInstance{inst})[inst.FlowID]; ok { flowName = name } c.centerJSON(200, 200, "ok", map[string]interface{}{ "id": inst.ID, "biz_type": inst.BizType, "biz_id": inst.BizID, "biz_title": inst.BizTitle, "status": inst.Status, "flow_name": flowName, "submit_user_name": inst.SubmitUserName, "submit_time": formatCenterTime(inst.SubmitTime), "finish_time": formatCenterTime(inst.FinishTime), "current_node": inst.CurrentNode, "timeline": timeline, }) } // formatCenterTime 时间指针格式化(空返回空串)。 func formatCenterTime(t *time.Time) string { if t == nil { return "" } return t.Format("2006-01-02 15:04") }