优化报销功能
This commit is contained in:
1 parent
395c7f717e
commit
4306cbd74b
19 files changed
+2059
-534
No files matched your search
@@ -0,0 +1,340 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
"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 {
|
||||
c.fail(401, 401, 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 {
|
||||
c.fail(401, 401, 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 {
|
||||
c.fail(401, 401, 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 {
|
||||
c.fail(401, 401, 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 {
|
||||
c.fail(401, 401, 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
|
||||
}
|
||||
Reference in new issue
Block a user