Files
yunzerwebsiteallinone/go/controllers/backend_approval_runtime.go
T
2026-10-19 01:13:17 +08:00

347 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}