优化报销功能
This commit is contained in:
1 parent
395c7f717e
commit
4306cbd74b
19 files changed
+2059
-534
No files matched your search
+176
-43
@@ -41,19 +41,19 @@ import (
|
||||
|
||||
// oaApprovalSubmitPayload 提交审批入参
|
||||
type oaApprovalSubmitPayload struct {
|
||||
BizType string `json:"biz_type"` // 单据类型(必填)
|
||||
FlowCode string `json:"flow_code"` // 指定流程(可选;留空取该 biz_type 默认流)
|
||||
FlowID uint64 `json:"flow_id"` // 指定流程ID(可选)
|
||||
BizTitle string `json:"biz_title"` // 单据标题(必填)
|
||||
BizID uint64 `json:"biz_id"` // 关联业务单据ID(可选)
|
||||
FormData map[string]interface{} `json:"form_data"` // 表单数据
|
||||
BizType string `json:"biz_type"` // 单据类型(必填)
|
||||
FlowCode string `json:"flow_code"` // 指定流程(可选;留空取该 biz_type 默认流)
|
||||
FlowID uint64 `json:"flow_id"` // 指定流程ID(可选)
|
||||
BizTitle string `json:"biz_title"` // 单据标题(必填)
|
||||
BizID uint64 `json:"biz_id"` // 关联业务单据ID(可选)
|
||||
FormData map[string]interface{} `json:"form_data"` // 表单数据
|
||||
}
|
||||
|
||||
// oaApprovalApprovePayload 审批入参
|
||||
type oaApprovalApprovePayload struct {
|
||||
TaskID uint64 `json:"task_id"` // 待审批任务ID(必填)
|
||||
Pass bool `json:"pass"` // true 通过 false 驳回
|
||||
Comment string `json:"comment"` // 审批意见
|
||||
TaskID uint64 `json:"task_id"` // 待审批任务ID(必填)
|
||||
Pass bool `json:"pass"` // true 通过 false 驳回
|
||||
Comment string `json:"comment"` // 审批意见
|
||||
}
|
||||
|
||||
// ============================ 工具 ============================
|
||||
@@ -267,8 +267,9 @@ func oaApprovalTaskTag(status int8) string {
|
||||
}
|
||||
}
|
||||
|
||||
// oaApprovalTaskView 任务视图(移动端时间线用)
|
||||
func oaApprovalTaskView(t models.ApprovalTask, isCurrent bool) map[string]interface{} {
|
||||
// oaApprovalTaskView 任务视图(移动端流程时间线用)
|
||||
// nodeType 来自流程定义(开始/审批/通知/结束),取不到时按审批节点处理。
|
||||
func oaApprovalTaskView(t models.ApprovalTask, isCurrent bool, nodeType int8) map[string]interface{} {
|
||||
finishTime := ""
|
||||
if t.FinishTime != nil {
|
||||
finishTime = t.FinishTime.Format("2006-01-02 15:04:05")
|
||||
@@ -277,23 +278,158 @@ func oaApprovalTaskView(t models.ApprovalTask, isCurrent bool) map[string]interf
|
||||
if t.Deadline != nil {
|
||||
deadline = t.Deadline.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"id": t.ID,
|
||||
"node_key": t.NodeKey,
|
||||
"node_name": t.NodeName,
|
||||
"approver_id": t.ApproverID,
|
||||
"approver_name": t.ApproverName,
|
||||
"multi_mode": t.MultiMode,
|
||||
"status": t.Status,
|
||||
"status_label": oaApprovalTaskText(t.Status),
|
||||
"status_tag": oaApprovalTaskTag(t.Status),
|
||||
"action": t.Action,
|
||||
"comment": t.Comment,
|
||||
"is_current": isCurrent,
|
||||
"deadline": deadline,
|
||||
"finish_time": finishTime,
|
||||
"create_time": t.CreateTime.Format("2006-01-02 15:04:05"),
|
||||
if nodeType == 0 {
|
||||
nodeType = models.ApprovalNodeApproval
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"id": t.ID,
|
||||
"node_key": t.NodeKey,
|
||||
"node_name": t.NodeName,
|
||||
"node_type": nodeType,
|
||||
"approver_id": t.ApproverID,
|
||||
"approver_name": t.ApproverName,
|
||||
"approver_sub": oaApprovalUserOrgName(t.Tid, t.ApproverID),
|
||||
"multi_mode": t.MultiMode,
|
||||
"status": t.Status,
|
||||
"status_label": oaApprovalTaskText(t.Status),
|
||||
"status_tag": oaApprovalTaskTag(t.Status),
|
||||
"action": t.Action,
|
||||
"comment": t.Comment,
|
||||
"is_current": isCurrent,
|
||||
"deadline": deadline,
|
||||
"finish_time": finishTime,
|
||||
"create_time": t.CreateTime.Format("2006-01-02 15:04:05"),
|
||||
}
|
||||
}
|
||||
|
||||
// oaApprovalUserOrgName 审批人所属部门名,用作流程节点副标题(如「财务部」),取不到返回空串。
|
||||
func oaApprovalUserOrgName(tid, uid uint64) string {
|
||||
if tid == 0 || uid == 0 {
|
||||
return ""
|
||||
}
|
||||
var tu models.SystemTenantUser
|
||||
if err := models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||
Filter("tid", tid).Filter("uid", uid).
|
||||
Filter("delete_time__isnull", true).One(&tu); err != nil {
|
||||
return ""
|
||||
}
|
||||
if tu.OrgID == 0 {
|
||||
return ""
|
||||
}
|
||||
var org models.BackendOrganization
|
||||
if err := models.Orm.QueryTable(new(models.BackendOrganization)).
|
||||
Filter("id", tu.OrgID).One(&org); err != nil {
|
||||
return ""
|
||||
}
|
||||
return org.OrgName
|
||||
}
|
||||
|
||||
// 时间线虚拟节点的 node_key:用于标识流程的开始 / 结束,前端据此区分渲染。
|
||||
const (
|
||||
oaApprovalStartKey = "__start__"
|
||||
oaApprovalEndKey = "__end__"
|
||||
)
|
||||
|
||||
// oaApprovalFlowTimeline 组装移动端「审批流程」时间线:
|
||||
// 首尾固定为提交申请(开始)与流程结束(结束),中间是流程产生的节点任务,
|
||||
// 让发起人一眼看出流程当前走到哪、是否已经走完。
|
||||
func oaApprovalFlowTimeline(inst models.ApprovalInstance, tasks []models.ApprovalTask) []map[string]interface{} {
|
||||
nodeTypes := oaApprovalNodeTypes(inst)
|
||||
timeline := make([]map[string]interface{}, 0, len(tasks)+2)
|
||||
timeline = append(timeline, oaApprovalStartNode(inst))
|
||||
for _, t := range tasks {
|
||||
timeline = append(timeline, oaApprovalTaskView(t, t.NodeKey == inst.CurrentNode, nodeTypes[t.NodeKey]))
|
||||
}
|
||||
timeline = append(timeline, oaApprovalEndNode(inst))
|
||||
return timeline
|
||||
}
|
||||
|
||||
// oaApprovalNodeTypes 读流程定义,取各节点类型(开始/审批/通知/结束)。
|
||||
// 流程被删除或改版时返回空表,调用方按审批节点兜底。
|
||||
func oaApprovalNodeTypes(inst models.ApprovalInstance) map[string]int8 {
|
||||
out := make(map[string]int8)
|
||||
flow, err := approvalEngine.LoadFlow(inst.Scope, inst.Tid, inst.BizType, inst.FlowID)
|
||||
if err != nil {
|
||||
return out
|
||||
}
|
||||
for i := range flow.Nodes {
|
||||
out[flow.Nodes[i].NodeKey] = flow.Nodes[i].NodeType
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// oaApprovalStartNode 虚拟开始节点:即发起人提交动作。
|
||||
func oaApprovalStartNode(inst models.ApprovalInstance) map[string]interface{} {
|
||||
timeStr := ""
|
||||
if inst.SubmitTime != nil {
|
||||
timeStr = inst.SubmitTime.Format("2006-01-02 15:04:05")
|
||||
} else if !inst.CreateTime.IsZero() {
|
||||
timeStr = inst.CreateTime.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"id": 0,
|
||||
"node_key": oaApprovalStartKey,
|
||||
"node_name": "提交申请",
|
||||
"node_type": models.ApprovalNodeStart,
|
||||
"approver_id": inst.SubmitUserID,
|
||||
"approver_name": inst.SubmitUserName,
|
||||
"approver_sub": oaApprovalUserOrgName(inst.Tid, inst.SubmitUserID),
|
||||
"multi_mode": 1,
|
||||
"status": models.ApprovalTaskApproved,
|
||||
"status_label": "已提交",
|
||||
"status_tag": "success",
|
||||
"action": 0,
|
||||
"comment": "",
|
||||
"is_current": false,
|
||||
"deadline": "",
|
||||
"finish_time": timeStr,
|
||||
"create_time": timeStr,
|
||||
}
|
||||
}
|
||||
|
||||
// oaApprovalEndNode 虚拟结束节点:实例运行中为「待完成」,结束后展示最终结果与完成时间。
|
||||
func oaApprovalEndNode(inst models.ApprovalInstance) map[string]interface{} {
|
||||
finishTime := ""
|
||||
if inst.FinishTime != nil {
|
||||
finishTime = inst.FinishTime.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
end := map[string]interface{}{
|
||||
"id": 0,
|
||||
"node_key": oaApprovalEndKey,
|
||||
"node_name": "流程结束",
|
||||
"node_type": models.ApprovalNodeEnd,
|
||||
"approver_id": 0,
|
||||
"approver_name": "",
|
||||
"multi_mode": 1,
|
||||
"status": models.ApprovalTaskPending,
|
||||
"status_label": "待完成",
|
||||
"status_tag": "info",
|
||||
"action": 0,
|
||||
"comment": "",
|
||||
"is_current": false,
|
||||
"deadline": "",
|
||||
"finish_time": finishTime,
|
||||
"create_time": finishTime,
|
||||
}
|
||||
switch inst.Status {
|
||||
case models.ApprovalInstanceApproved:
|
||||
end["status"] = models.ApprovalTaskApproved
|
||||
end["status_label"] = "已通过"
|
||||
end["status_tag"] = "success"
|
||||
case models.ApprovalInstanceRejected:
|
||||
end["status"] = models.ApprovalTaskRejected
|
||||
end["status_label"] = "已驳回"
|
||||
end["status_tag"] = "error"
|
||||
case models.ApprovalInstanceWithdrawn:
|
||||
end["status"] = models.ApprovalTaskSkipped
|
||||
end["status_label"] = "已撤回"
|
||||
end["status_tag"] = "info"
|
||||
case models.ApprovalInstanceCanceled:
|
||||
end["status"] = models.ApprovalTaskSkipped
|
||||
end["status_label"] = "已取消"
|
||||
end["status_tag"] = "info"
|
||||
}
|
||||
return end
|
||||
}
|
||||
|
||||
// ============================ 路由方法 ============================
|
||||
@@ -468,7 +604,7 @@ func (c *AppOaController) ApprovalTodo() {
|
||||
if inst.Status != models.ApprovalInstanceRunning {
|
||||
continue
|
||||
}
|
||||
view := oaApprovalTaskView(t, true)
|
||||
view := oaApprovalTaskView(t, true, oaApprovalNodeTypes(inst)[t.NodeKey])
|
||||
view["instance"] = oaApprovalInstanceView(inst)
|
||||
list = append(list, view)
|
||||
}
|
||||
@@ -529,16 +665,13 @@ func (c *AppOaController) ApprovalDetail() {
|
||||
}
|
||||
|
||||
tasks := oaApprovalTasksByInstance(inst.ID)
|
||||
timeline := make([]map[string]interface{}, 0, len(tasks))
|
||||
for _, t := range tasks {
|
||||
timeline = append(timeline, oaApprovalTaskView(t, t.NodeKey == inst.CurrentNode))
|
||||
}
|
||||
timeline := oaApprovalFlowTimeline(inst, tasks)
|
||||
|
||||
c.oaOk(map[string]interface{}{
|
||||
"item": oaApprovalInstanceView(inst),
|
||||
"timeline": timeline,
|
||||
"can_approve": isApprover,
|
||||
"can_withdraw": isSubmitter && inst.Status == models.ApprovalInstanceRunning,
|
||||
"item": oaApprovalInstanceView(inst),
|
||||
"timeline": timeline,
|
||||
"can_approve": isApprover,
|
||||
"can_withdraw": isSubmitter && inst.Status == models.ApprovalInstanceRunning,
|
||||
"current_task_id": currentTaskID,
|
||||
})
|
||||
}
|
||||
@@ -602,8 +735,8 @@ func (c *AppOaController) ApprovalSubmit() {
|
||||
Filter("id", instID).One(&inst)
|
||||
|
||||
c.oaOk(map[string]interface{}{
|
||||
"id": instID,
|
||||
"item": oaApprovalInstanceView(inst),
|
||||
"id": instID,
|
||||
"item": oaApprovalInstanceView(inst),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -662,11 +795,11 @@ func (c *AppOaController) ApprovalApprove() {
|
||||
Filter("id", res.InstanceID).One(&inst)
|
||||
|
||||
c.oaOk(map[string]interface{}{
|
||||
"finished": res.Finished,
|
||||
"instance_stat": res.InstanceStat,
|
||||
"current_node": res.CurrentNode,
|
||||
"message": res.Message,
|
||||
"item": oaApprovalInstanceView(inst),
|
||||
"finished": res.Finished,
|
||||
"instance_stat": res.InstanceStat,
|
||||
"current_node": res.CurrentNode,
|
||||
"message": res.Message,
|
||||
"item": oaApprovalInstanceView(inst),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -256,7 +256,8 @@ func (c *AppOaController) ExpenseSubmit() {
|
||||
c.oaOk(nil)
|
||||
}
|
||||
|
||||
// ExpenseStatus POST /api/oa/expense/status/:id 手动确认更改单据状态(body: {status})。
|
||||
// ExpenseStatus POST /api/oa/expense/status/:id 报销人确认款项到账(body: {status:5})。
|
||||
// 仅限「已通过」的单据确认收款,其它状态由审批引擎推进。
|
||||
func (c *AppOaController) ExpenseStatus() {
|
||||
claims, err := c.oaClaims()
|
||||
if err != nil {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -1001,16 +1001,14 @@ type reimbursementStatusPayload struct {
|
||||
Status int8 `json:"status"`
|
||||
}
|
||||
|
||||
var manualConfirmStatuses = map[int8]bool{
|
||||
0: true, // 草稿
|
||||
1: true, // 审批中
|
||||
2: true, // 已通过
|
||||
3: true, // 已驳回
|
||||
4: true, // 已撤回
|
||||
5: true, // 已打款
|
||||
}
|
||||
// 报销单状态(0草稿 1审批中 2已通过 3已驳回 4已撤回 5已打款)
|
||||
const (
|
||||
reimbursementStatusApproved int8 = 2 // 已通过
|
||||
reimbursementStatusPaid int8 = 5 // 已打款
|
||||
)
|
||||
|
||||
// UpdateStatus POST /backend/reimbursements/:id/status — 手动确认更改单据状态
|
||||
// UpdateStatus POST /backend/reimbursements/:id/status — 报销人确认款项到账(置为「已打款」)。
|
||||
// 其它状态均由审批引擎/流程动作推进,不再支持手工变更。
|
||||
func (c *BackendReimburseController) UpdateStatus() {
|
||||
claims, ok := c.claims()
|
||||
if !ok {
|
||||
@@ -1040,27 +1038,33 @@ func (c *BackendReimburseController) UpdateStatus() {
|
||||
c.reply(200, "success", map[string]interface{}{"status": newStatus})
|
||||
}
|
||||
|
||||
// reimburseUpdateStatusCore 手动确认更改单据状态共享逻辑。
|
||||
// reimburseUpdateStatusCore 报销人确认款项到账共享逻辑。
|
||||
//
|
||||
// 规则:审批通过后,报销金额是否到账由报销人本人确认;确认后单据置为「已打款」。
|
||||
// 草稿/审批中/已通过/已驳回/已撤回等状态一律由流程动作与审批引擎推进,不再支持手工修改。
|
||||
// 返回 (生效状态, HTTP 状态码, 错误信息)。
|
||||
func reimburseUpdateStatusCore(claims *jwtutil.Claims, id uint64, status int8) (int8, int, string) {
|
||||
if !manualConfirmStatuses[status] {
|
||||
return 0, 400, "不支持的状态值"
|
||||
if status != reimbursementStatusPaid {
|
||||
return 0, 400, "仅支持确认收款,其它状态由审批流程自动推进"
|
||||
}
|
||||
row, err := findOwnedReimbursement(claims, id)
|
||||
if err != nil {
|
||||
return 0, 404, "报销单不存在"
|
||||
}
|
||||
if row.Status == status {
|
||||
if row.Status == reimbursementStatusPaid {
|
||||
return row.Status, 200, ""
|
||||
}
|
||||
if row.Status != reimbursementStatusApproved {
|
||||
return 0, 400, "仅审批通过的报销单可以确认到账"
|
||||
}
|
||||
now := time.Now()
|
||||
_, err = models.Orm.QueryTable(new(models.BackendReimbursement)).Filter("id", id).Update(orm.Params{
|
||||
"status": status, "update_time": now,
|
||||
"status": reimbursementStatusPaid, "update_time": now,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, 500, "更新状态失败"
|
||||
}
|
||||
return status, 200, ""
|
||||
return reimbursementStatusPaid, 200, ""
|
||||
}
|
||||
|
||||
// Records GET /backend/reimbursements/:id/records
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
-- ============================================================
|
||||
-- OA 审批中心 —— 菜单配置(yz_system_menu)
|
||||
-- 幂等脚本:按 path 判重,重复执行不会产生重复菜单
|
||||
-- 执行:mysql -h <host> -P <port> -u <user> -p <db> < oa_approvalcenter_menus.sql
|
||||
--
|
||||
-- 对应页面:
|
||||
-- /apps/oa/approvalcenter/mine 我的发起(我提交的单据)
|
||||
-- /apps/oa/approvalcenter/list 审批列表(待我审批 / 我已处理)
|
||||
-- /apps/oa/approvalcenter/settings 审批流设置(复用系统管理的编排能力)
|
||||
-- 注意:目录本身依赖 OA 应用根菜单 '/apps/oa',缺失时脚本不会写入任何数据。
|
||||
-- ============================================================
|
||||
|
||||
SET @oa_id := (
|
||||
SELECT id FROM yz_system_menu
|
||||
WHERE path = '/apps/oa' AND delete_time IS NULL
|
||||
LIMIT 1
|
||||
);
|
||||
|
||||
-- ---------- 审批中心(目录) ----------
|
||||
INSERT INTO yz_system_menu
|
||||
(pid, title, path, component_path, icon, sort, status, is_visible, views, type, creater, remark)
|
||||
SELECT @oa_id, '审批中心', '/apps/oa/approvalcenter', NULL,
|
||||
'Stamp', 99, 1, 1, '[2]', 1, 'system', '发起、处理与配置 OA 审批流程'
|
||||
FROM DUAL
|
||||
WHERE @oa_id IS NOT NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM yz_system_menu WHERE path = '/apps/oa/approvalcenter' AND delete_time IS NULL) x);
|
||||
|
||||
SET @center_id := (
|
||||
SELECT id FROM yz_system_menu
|
||||
WHERE path = '/apps/oa/approvalcenter' AND delete_time IS NULL
|
||||
LIMIT 1
|
||||
);
|
||||
|
||||
-- ---------- 我的发起 ----------
|
||||
INSERT INTO yz_system_menu
|
||||
(pid, title, path, component_path, icon, sort, status, is_visible, views, type, creater, remark)
|
||||
SELECT @center_id, '我的发起', '/apps/oa/approvalcenter/mine', 'apps/oa/approvalcenter/mine.vue',
|
||||
'Promotion', 1, 1, 1, '[2]', 2, 'system', '我发起的审批单据与当前进度,支持撤回'
|
||||
FROM DUAL
|
||||
WHERE @center_id IS NOT NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM yz_system_menu WHERE path = '/apps/oa/approvalcenter/mine' AND delete_time IS NULL) x);
|
||||
|
||||
-- ---------- 审批列表 ----------
|
||||
INSERT INTO yz_system_menu
|
||||
(pid, title, path, component_path, icon, sort, status, is_visible, views, type, creater, remark)
|
||||
SELECT @center_id, '审批列表', '/apps/oa/approvalcenter/list', 'apps/oa/approvalcenter/list.vue',
|
||||
'Tickets', 2, 1, 1, '[2]', 2, 'system', '待我审批 / 我已处理的任务,可直接通过或驳回'
|
||||
FROM DUAL
|
||||
WHERE @center_id IS NOT NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM yz_system_menu WHERE path = '/apps/oa/approvalcenter/list' AND delete_time IS NULL) x);
|
||||
|
||||
-- ---------- 审批流设置 ----------
|
||||
INSERT INTO yz_system_menu
|
||||
(pid, title, path, component_path, icon, sort, status, is_visible, views, type, creater, remark)
|
||||
SELECT @center_id, '审批流设置', '/apps/oa/approvalcenter/settings', 'apps/oa/approvalcenter/settings.vue',
|
||||
'Setting', 3, 1, 1, '[2]', 2, 'system', '按功能模块编排审批流程(复用系统管理 → 审批流配置)'
|
||||
FROM DUAL
|
||||
WHERE @center_id IS NOT NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM yz_system_menu WHERE path = '/apps/oa/approvalcenter/settings' AND delete_time IS NULL) x);
|
||||
|
||||
-- ---------- 结果校验 ----------
|
||||
SELECT id, pid, title, path, component_path, type, sort
|
||||
FROM yz_system_menu
|
||||
WHERE delete_time IS NULL AND path LIKE '/apps/oa/approvalcenter%'
|
||||
ORDER BY pid, sort;
|
||||
@@ -33,6 +33,13 @@ func RegisterApprovalRoutes() {
|
||||
beego.Router("/backend/approval/flow/delete/:id", &controllers.BackendApprovalController{}, "delete:FlowDelete")
|
||||
beego.Router("/backend/approval/flow/pick", &controllers.BackendApprovalController{}, "get:Pick")
|
||||
beego.Router("/backend/approval/flow/available", &controllers.BackendApprovalController{}, "get:AvailableFlows")
|
||||
|
||||
// 审批中心运行时(我发起 / 待我审批 / 详情 / 通过驳回 / 撤回)
|
||||
beego.Router("/backend/approval/instance/mine", &controllers.BackendApprovalRuntimeController{}, "get:InstanceMine")
|
||||
beego.Router("/backend/approval/instance/todo", &controllers.BackendApprovalRuntimeController{}, "get:InstanceTodo")
|
||||
beego.Router("/backend/approval/instance/detail/:id", &controllers.BackendApprovalRuntimeController{}, "get:InstanceDetail")
|
||||
beego.Router("/backend/approval/instance/approve", &controllers.BackendApprovalRuntimeController{}, "post:Approve")
|
||||
beego.Router("/backend/approval/instance/withdraw/:id", &controllers.BackendApprovalRuntimeController{}, "post:Withdraw")
|
||||
}
|
||||
|
||||
// RegisterProductRoutes 注册租户端「产品购买」路由。
|
||||
|
||||
Reference in new issue
Block a user