347 lines
10 KiB
Go
347 lines
10 KiB
Go
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
|
||
}
|