388 lines
12 KiB
Go
388 lines
12 KiB
Go
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")
|
|
}
|