860 lines
26 KiB
Go
860 lines
26 KiB
Go
package controllers
|
||
|
||
import (
|
||
"encoding/json"
|
||
"io"
|
||
"sort"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"server/models"
|
||
"server/pkg/jwtutil"
|
||
"server/services"
|
||
approvalEngine "server/services/approval"
|
||
|
||
"github.com/beego/beego/v2/client/orm"
|
||
)
|
||
|
||
// 移动端审批接口,挂在 AppOaController 上。
|
||
//
|
||
// 设计说明:
|
||
// - 数据按 JWT 的 tid + user 双重隔离:发起列表仅看本人发起的审批,
|
||
// 待办列表仅看本人作为审批人的待办;
|
||
// - 复用 services/approval 引擎的 Submit / Approve,保证与 Web 端行为一致;
|
||
// - 单据类型(biz_type)取审批流 yz_backend_approval_flow.biz_type;
|
||
// 移动端不开放自由单据类型,仅允许本租户已配置的分类。
|
||
//
|
||
// 路由前缀:/api/oa/approval/*
|
||
//
|
||
// 接口清单:
|
||
// GET /api/oa/approval/list 我发起的审批实例
|
||
// GET /api/oa/approval/todo 我的待办(作为审批人)
|
||
// GET /api/oa/approval/detail/:id 审批详情(含任务时间线)
|
||
// POST /api/oa/approval/submit 提交审批
|
||
// POST /api/oa/approval/approve 通过/驳回审批任务
|
||
// POST /api/oa/approval/withdraw/:id 发起人撤回审批
|
||
// GET /api/oa/approval/flows 可发起的流程清单(按分类)
|
||
// GET /api/oa/approval/categories 单据类型(biz_type)下拉
|
||
|
||
// ============================ 入参 ============================
|
||
|
||
// 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"` // 表单数据
|
||
}
|
||
|
||
// oaApprovalApprovePayload 审批入参
|
||
type oaApprovalApprovePayload struct {
|
||
TaskID uint64 `json:"task_id"` // 待审批任务ID(必填)
|
||
Pass bool `json:"pass"` // true 通过 false 驳回
|
||
Comment string `json:"comment"` // 审批意见
|
||
}
|
||
|
||
// ============================ 工具 ============================
|
||
|
||
// oaApprovalIDParam 解析 :id 路由参数
|
||
func oaApprovalIDParam(c *AppOaController) uint64 {
|
||
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||
if err != nil {
|
||
return 0
|
||
}
|
||
return id
|
||
}
|
||
|
||
// oaApprovalClaims 解析并校验 JWT
|
||
func oaApprovalClaims(c *AppOaController) (*jwtutil.Claims, bool) {
|
||
claims, err := c.oaClaims()
|
||
if err != nil {
|
||
c.oaErr(401, 401, "未登录或无权限")
|
||
return nil, false
|
||
}
|
||
return claims, true
|
||
}
|
||
|
||
// oaApprovalEmployee 通过 JWT 找到当前登录用户对应的员工记录(用于取部门 ID)
|
||
func oaApprovalEmployee(c *AppOaController, claims *jwtutil.Claims) *models.BackendEmployee {
|
||
emp, ok := c.oaMyEmployee(claims)
|
||
if !ok {
|
||
return nil
|
||
}
|
||
return emp
|
||
}
|
||
|
||
// oaApprovalSubmitUID 通过 JWT 拿到租户侧的 UID(SystemTenantUser.id),
|
||
// 审批引擎 SubmitInput.SubmitUID 用的是租户侧用户 ID 而非平台 AdminUser.ID。
|
||
func oaApprovalSubmitUID(claims *jwtutil.Claims) uint64 {
|
||
return services.ResolveTenantUID(uint64(claims.TenantId), uint64(claims.UserID))
|
||
}
|
||
|
||
// oaApprovalReadJSON 读取并解析 JSON 请求体,失败时输出错误响应
|
||
func oaApprovalReadJSON(c *AppOaController, dst interface{}) bool {
|
||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||
if err != nil {
|
||
c.oaErr(400, 400, "参数错误")
|
||
return false
|
||
}
|
||
if len(raw) == 0 {
|
||
return true
|
||
}
|
||
if err := json.Unmarshal(raw, dst); err != nil {
|
||
c.oaErr(400, 400, "参数错误: "+err.Error())
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
// oaApprovalUserName 取得发起人姓名(优先员工表,否则使用 JWT 用户名)
|
||
func oaApprovalUserName(c *AppOaController, claims *jwtutil.Claims) string {
|
||
if emp := oaApprovalEmployee(c, claims); emp != nil && emp.Name != "" {
|
||
return emp.Name
|
||
}
|
||
return strings.TrimSpace(claims.Username)
|
||
}
|
||
|
||
// ============================ 实例组装 ============================
|
||
|
||
// oaApprovalStatusText / oaApprovalStatusTag 状态展示文案/Tag 类型(uview-plus)
|
||
func oaApprovalStatusText(status int8) string {
|
||
switch status {
|
||
case models.ApprovalInstanceDraft:
|
||
return "草稿"
|
||
case models.ApprovalInstanceRunning:
|
||
return "审批中"
|
||
case models.ApprovalInstanceApproved:
|
||
return "已通过"
|
||
case models.ApprovalInstanceRejected:
|
||
return "已驳回"
|
||
case models.ApprovalInstanceWithdrawn:
|
||
return "已撤回"
|
||
case models.ApprovalInstanceCanceled:
|
||
return "已取消"
|
||
default:
|
||
return "未知"
|
||
}
|
||
}
|
||
|
||
// oaApprovalStatusTagType 给前端 uview-plus u-tag 用的类型映射
|
||
func oaApprovalStatusTagType(status int8) string {
|
||
switch status {
|
||
case models.ApprovalInstanceRunning:
|
||
return "warning"
|
||
case models.ApprovalInstanceApproved:
|
||
return "success"
|
||
case models.ApprovalInstanceRejected:
|
||
return "error"
|
||
case models.ApprovalInstanceWithdrawn, models.ApprovalInstanceCanceled:
|
||
return "info"
|
||
default:
|
||
return "info"
|
||
}
|
||
}
|
||
|
||
// oaApprovalInstanceView 把 ApprovalInstance 组装为移动端列表/详情通用的视图结构
|
||
func oaApprovalInstanceView(inst models.ApprovalInstance) map[string]interface{} {
|
||
submitTime := ""
|
||
if inst.SubmitTime != nil {
|
||
submitTime = inst.SubmitTime.Format("2006-01-02 15:04:05")
|
||
}
|
||
finishTime := ""
|
||
if inst.FinishTime != nil {
|
||
finishTime = inst.FinishTime.Format("2006-01-02 15:04:05")
|
||
}
|
||
createTime := ""
|
||
if !inst.CreateTime.IsZero() {
|
||
createTime = inst.CreateTime.Format("2006-01-02 15:04:05")
|
||
}
|
||
return map[string]interface{}{
|
||
"id": inst.ID,
|
||
"biz_type": inst.BizType,
|
||
"flow_id": inst.FlowID,
|
||
"biz_id": inst.BizID,
|
||
"biz_title": inst.BizTitle,
|
||
"submit_user_id": inst.SubmitUserID,
|
||
"submit_user_name": inst.SubmitUserName,
|
||
"submit_dept_id": inst.SubmitDeptID,
|
||
"current_node": inst.CurrentNode,
|
||
"status": inst.Status,
|
||
"status_label": oaApprovalStatusText(inst.Status),
|
||
"status_tag": oaApprovalStatusTagType(inst.Status),
|
||
"submit_time": submitTime,
|
||
"finish_time": finishTime,
|
||
"create_time": createTime,
|
||
}
|
||
}
|
||
|
||
// oaApprovalBizTypes 返回本租户已配置(启用 + 未删除)的 biz_type 列表
|
||
func oaApprovalBizTypes(tid uint64) []map[string]interface{} {
|
||
var flows []models.TenantApprovalFlow
|
||
_, _ = models.Orm.QueryTable(new(models.TenantApprovalFlow)).
|
||
Filter("tid", tid).Filter("is_deleted", 0).Filter("status", 1).
|
||
OrderBy("module_code", "id").All(&flows)
|
||
|
||
seen := map[string]bool{}
|
||
list := make([]map[string]interface{}, 0, len(flows))
|
||
for _, f := range flows {
|
||
biz := strings.TrimSpace(f.BizType)
|
||
if biz == "" {
|
||
biz = strings.TrimSpace(f.ModuleCode)
|
||
}
|
||
if biz == "" || seen[biz] {
|
||
continue
|
||
}
|
||
seen[biz] = true
|
||
list = append(list, map[string]interface{}{
|
||
"value": biz,
|
||
"label": biz,
|
||
"module_code": f.ModuleCode,
|
||
})
|
||
}
|
||
sort.Slice(list, func(i, j int) bool {
|
||
return list[i]["value"].(string) < list[j]["value"].(string)
|
||
})
|
||
return list
|
||
}
|
||
|
||
// ============================ 任务时间线 ============================
|
||
|
||
// oaApprovalTasksByInstance 取出某实例下的全部任务(按 sort / id 升序),
|
||
// 同时按 (instance_id, node_key) 找当前节点的当前审批人。
|
||
func oaApprovalTasksByInstance(instanceID uint64) []models.ApprovalTask {
|
||
var tasks []models.ApprovalTask
|
||
_, _ = models.Orm.QueryTable(new(models.ApprovalTask)).
|
||
Filter("instance_id", instanceID).Filter("is_deleted", 0).
|
||
OrderBy("sort", "id").All(&tasks)
|
||
return tasks
|
||
}
|
||
|
||
// oaApprovalTaskText / oaApprovalTaskTag 任务级状态文案/Tag 类型
|
||
func oaApprovalTaskText(status int8) string {
|
||
switch status {
|
||
case models.ApprovalTaskPending:
|
||
return "待审批"
|
||
case models.ApprovalTaskApproved:
|
||
return "已通过"
|
||
case models.ApprovalTaskRejected:
|
||
return "已驳回"
|
||
case models.ApprovalTaskReturned:
|
||
return "已退回"
|
||
case models.ApprovalTaskSkipped:
|
||
return "已跳过"
|
||
case models.ApprovalTaskTimeout:
|
||
return "已超时"
|
||
case models.ApprovalTaskNotified:
|
||
return "已通知"
|
||
default:
|
||
return "未知"
|
||
}
|
||
}
|
||
|
||
func oaApprovalTaskTag(status int8) string {
|
||
switch status {
|
||
case models.ApprovalTaskPending:
|
||
return "warning"
|
||
case models.ApprovalTaskApproved:
|
||
return "success"
|
||
case models.ApprovalTaskRejected:
|
||
return "error"
|
||
case models.ApprovalTaskReturned, models.ApprovalTaskSkipped, models.ApprovalTaskTimeout:
|
||
return "info"
|
||
default:
|
||
return "info"
|
||
}
|
||
}
|
||
|
||
// 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")
|
||
}
|
||
deadline := ""
|
||
if t.Deadline != nil {
|
||
deadline = t.Deadline.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
|
||
}
|
||
|
||
// ============================ 路由方法 ============================
|
||
|
||
// ApprovalCategories GET /api/oa/approval/categories
|
||
// 返回本租户可用的 biz_type 下拉项,移动端「发起审批」选择单据类型使用。
|
||
func (c *AppOaController) ApprovalCategories() {
|
||
claims, ok := oaApprovalClaims(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
c.oaOk(map[string]interface{}{
|
||
"list": oaApprovalBizTypes(uint64(claims.TenantId)),
|
||
"total": len(oaApprovalBizTypes(uint64(claims.TenantId))),
|
||
})
|
||
}
|
||
|
||
// ApprovalFlows GET /api/oa/approval/flows
|
||
// 按 biz_type 返回本租户可发起的流程(含流程名 + 节点名列表),
|
||
// 用于「选择流程」下拉。biz_type 为空时返回全部。
|
||
func (c *AppOaController) ApprovalFlows() {
|
||
claims, ok := oaApprovalClaims(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
tid := uint64(claims.TenantId)
|
||
biz := strings.TrimSpace(c.GetString("biz_type"))
|
||
|
||
qs := models.Orm.QueryTable(new(models.TenantApprovalFlow)).
|
||
Filter("tid", tid).Filter("is_deleted", 0).Filter("status", 1)
|
||
if biz != "" {
|
||
qs = qs.Filter("biz_type", biz)
|
||
}
|
||
var flows []models.TenantApprovalFlow
|
||
_, _ = qs.OrderBy("is_default", "id").All(&flows)
|
||
|
||
list := make([]map[string]interface{}, 0, len(flows))
|
||
for _, f := range flows {
|
||
var nodes []models.TenantApprovalNode
|
||
_, _ = models.Orm.QueryTable(new(models.TenantApprovalNode)).
|
||
Filter("flow_id", f.ID).Filter("is_deleted", 0).
|
||
OrderBy("sort", "id").All(&nodes)
|
||
|
||
nodeNames := make([]string, 0, len(nodes))
|
||
for _, n := range nodes {
|
||
if n.NodeType == models.ApprovalNodeApproval {
|
||
nodeNames = append(nodeNames, n.NodeName)
|
||
}
|
||
}
|
||
list = append(list, map[string]interface{}{
|
||
"id": f.ID,
|
||
"flow_code": f.FlowCode,
|
||
"flow_name": f.FlowName,
|
||
"biz_type": f.BizType,
|
||
"module_code": f.ModuleCode,
|
||
"is_default": f.IsDefault,
|
||
"nodes": nodeNames,
|
||
})
|
||
}
|
||
c.oaOk(map[string]interface{}{"list": list, "total": len(list)})
|
||
}
|
||
|
||
// ApprovalList GET /api/oa/approval/list
|
||
// 我发起的审批实例(移动端「我的发起」列表)。
|
||
// 支持 keyword / status / biz_type 过滤。
|
||
func (c *AppOaController) ApprovalList() {
|
||
claims, ok := oaApprovalClaims(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
tid := uint64(claims.TenantId)
|
||
uid := oaApprovalSubmitUID(claims)
|
||
|
||
page, _ := c.GetInt("page", 1)
|
||
pageSize, _ := c.GetInt("pageSize", 20)
|
||
if page < 1 {
|
||
page = 1
|
||
}
|
||
if pageSize < 1 || pageSize > 100 {
|
||
pageSize = 20
|
||
}
|
||
keyword := strings.TrimSpace(c.GetString("keyword"))
|
||
statusStr := strings.TrimSpace(c.GetString("status"))
|
||
bizType := strings.TrimSpace(c.GetString("biz_type"))
|
||
|
||
qs := models.Orm.QueryTable(new(models.ApprovalInstance)).
|
||
Filter("scope", models.ApprovalScopeTenant).
|
||
Filter("tid", tid).
|
||
Filter("submit_user_id", uid).
|
||
Filter("is_deleted", 0)
|
||
if bizType != "" {
|
||
qs = qs.Filter("biz_type", bizType)
|
||
}
|
||
if statusStr != "" {
|
||
if s, err := strconv.Atoi(statusStr); err == nil {
|
||
qs = qs.Filter("status", int8(s))
|
||
}
|
||
}
|
||
|
||
total, _ := qs.Count()
|
||
var rows []models.ApprovalInstance
|
||
_, err := qs.OrderBy("-id").
|
||
Limit(pageSize).Offset((page - 1) * pageSize).All(&rows)
|
||
if err != nil && err != orm.ErrNoRows {
|
||
c.oaErr(500, 500, "查询失败")
|
||
return
|
||
}
|
||
|
||
list := make([]map[string]interface{}, 0, len(rows))
|
||
for _, inst := range rows {
|
||
item := oaApprovalInstanceView(inst)
|
||
if keyword != "" {
|
||
if !strings.Contains(strings.ToLower(inst.BizTitle), strings.ToLower(keyword)) {
|
||
continue
|
||
}
|
||
}
|
||
list = append(list, item)
|
||
}
|
||
|
||
c.oaOk(map[string]interface{}{
|
||
"list": list,
|
||
"total": total,
|
||
"page": page,
|
||
"pageSize": pageSize,
|
||
})
|
||
}
|
||
|
||
// ApprovalTodo GET /api/oa/approval/todo
|
||
// 我的待办(作为审批人)。返回任务列表 + 关联实例基本信息,
|
||
// 移动端「待我审批」直接展示。
|
||
func (c *AppOaController) ApprovalTodo() {
|
||
claims, ok := oaApprovalClaims(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
tid := uint64(claims.TenantId)
|
||
uid := oaApprovalSubmitUID(claims)
|
||
|
||
page, _ := c.GetInt("page", 1)
|
||
pageSize, _ := c.GetInt("pageSize", 20)
|
||
if page < 1 {
|
||
page = 1
|
||
}
|
||
if pageSize < 1 || pageSize > 100 {
|
||
pageSize = 20
|
||
}
|
||
|
||
qs := models.Orm.QueryTable(new(models.ApprovalTask)).
|
||
Filter("scope", models.ApprovalScopeTenant).
|
||
Filter("tid", tid).
|
||
Filter("approver_id", uid).
|
||
Filter("status", models.ApprovalTaskPending).
|
||
Filter("is_deleted", 0)
|
||
total, _ := qs.Count()
|
||
|
||
var tasks []models.ApprovalTask
|
||
_, err := qs.OrderBy("-id").
|
||
Limit(pageSize).Offset((page - 1) * pageSize).All(&tasks)
|
||
if err != nil && err != orm.ErrNoRows {
|
||
c.oaErr(500, 500, "查询失败")
|
||
return
|
||
}
|
||
|
||
list := make([]map[string]interface{}, 0, len(tasks))
|
||
for _, t := range tasks {
|
||
// 关联查询实例,校验实例仍在审批中(防止实例已结束但任务未及时同步)
|
||
var inst models.ApprovalInstance
|
||
if err := models.Orm.QueryTable(new(models.ApprovalInstance)).
|
||
Filter("id", t.InstanceID).Filter("is_deleted", 0).One(&inst); err != nil {
|
||
continue
|
||
}
|
||
if inst.Status != models.ApprovalInstanceRunning {
|
||
continue
|
||
}
|
||
view := oaApprovalTaskView(t, true, oaApprovalNodeTypes(inst)[t.NodeKey])
|
||
view["instance"] = oaApprovalInstanceView(inst)
|
||
list = append(list, view)
|
||
}
|
||
|
||
c.oaOk(map[string]interface{}{
|
||
"list": list,
|
||
"total": total,
|
||
"page": page,
|
||
"pageSize": pageSize,
|
||
})
|
||
}
|
||
|
||
// ApprovalDetail GET /api/oa/approval/detail/:id
|
||
// 审批详情:实例基本信息 + 任务时间线 + 当前节点可执行动作。
|
||
func (c *AppOaController) ApprovalDetail() {
|
||
claims, ok := oaApprovalClaims(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
tid := uint64(claims.TenantId)
|
||
uid := oaApprovalSubmitUID(claims)
|
||
|
||
id := oaApprovalIDParam(c)
|
||
if id == 0 {
|
||
c.oaErr(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.oaErr(404, 404, "审批不存在")
|
||
return
|
||
}
|
||
|
||
// 权限:仅发起人或当前节点的审批人可查看
|
||
// 两者独立判断:同一个用户既可能是发起人也可能是审批人(如自审批场景)
|
||
isSubmitter := inst.SubmitUserID == uid
|
||
isApprover := false
|
||
var currentTaskID uint64
|
||
{
|
||
var t models.ApprovalTask
|
||
err := models.Orm.QueryTable(new(models.ApprovalTask)).
|
||
Filter("instance_id", inst.ID).
|
||
Filter("approver_id", uid).
|
||
Filter("status", models.ApprovalTaskPending).
|
||
Filter("is_deleted", 0).One(&t)
|
||
if err == nil {
|
||
isApprover = true
|
||
currentTaskID = t.ID
|
||
}
|
||
}
|
||
if !isSubmitter && !isApprover {
|
||
c.oaErr(403, 403, "无权查看该审批")
|
||
return
|
||
}
|
||
|
||
tasks := oaApprovalTasksByInstance(inst.ID)
|
||
timeline := oaApprovalFlowTimeline(inst, tasks)
|
||
|
||
c.oaOk(map[string]interface{}{
|
||
"item": oaApprovalInstanceView(inst),
|
||
"timeline": timeline,
|
||
"can_approve": isApprover,
|
||
"can_withdraw": isSubmitter && inst.Status == models.ApprovalInstanceRunning,
|
||
"current_task_id": currentTaskID,
|
||
})
|
||
}
|
||
|
||
// ApprovalSubmit POST /api/oa/approval/submit
|
||
// 提交审批(发起)。引擎内部完成:创建实例 → 找首个审批节点 → 生成任务。
|
||
func (c *AppOaController) ApprovalSubmit() {
|
||
claims, ok := oaApprovalClaims(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
|
||
var p oaApprovalSubmitPayload
|
||
if !oaApprovalReadJSON(c, &p) {
|
||
return
|
||
}
|
||
if strings.TrimSpace(p.BizType) == "" {
|
||
c.oaErr(400, 400, "请选择单据类型")
|
||
return
|
||
}
|
||
if strings.TrimSpace(p.BizTitle) == "" {
|
||
c.oaErr(400, 400, "请填写单据标题")
|
||
return
|
||
}
|
||
|
||
tid := uint64(claims.TenantId)
|
||
uid := oaApprovalSubmitUID(claims)
|
||
|
||
// 发起人所在部门(用于审批引擎按部门主管匹配审批人)
|
||
var deptID uint64
|
||
var tu models.SystemTenantUser
|
||
if models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||
Filter("uid", uid).Filter("tid", tid).
|
||
Filter("delete_time__isnull", true).One(&tu) == nil {
|
||
deptID = tu.OrgID
|
||
}
|
||
|
||
in := approvalEngine.SubmitInput{
|
||
Scope: models.ApprovalScopeTenant,
|
||
Tid: tid,
|
||
BizType: strings.TrimSpace(p.BizType),
|
||
FlowCode: strings.TrimSpace(p.FlowCode),
|
||
FlowID: p.FlowID,
|
||
BizID: p.BizID,
|
||
BizTitle: strings.TrimSpace(p.BizTitle),
|
||
SubmitUID: uid,
|
||
SubmitName: oaApprovalUserName(c, claims),
|
||
DeptID: deptID,
|
||
FormData: p.FormData,
|
||
}
|
||
|
||
instID, err := approvalEngine.Submit(in)
|
||
if err != nil {
|
||
// ErrNoFlow / ErrNoApprover 等业务错误统一以 400 返回
|
||
c.oaErr(400, 400, err.Error())
|
||
return
|
||
}
|
||
|
||
var inst models.ApprovalInstance
|
||
_ = models.Orm.QueryTable(new(models.ApprovalInstance)).
|
||
Filter("id", instID).One(&inst)
|
||
|
||
c.oaOk(map[string]interface{}{
|
||
"id": instID,
|
||
"item": oaApprovalInstanceView(inst),
|
||
})
|
||
}
|
||
|
||
// ApprovalApprove POST /api/oa/approval/approve
|
||
// 审批人通过/驳回一条任务。
|
||
func (c *AppOaController) ApprovalApprove() {
|
||
claims, ok := oaApprovalClaims(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
tid := uint64(claims.TenantId)
|
||
uid := oaApprovalSubmitUID(claims)
|
||
|
||
var p oaApprovalApprovePayload
|
||
if !oaApprovalReadJSON(c, &p) {
|
||
return
|
||
}
|
||
if p.TaskID == 0 {
|
||
// 发起人视角在「我的发起」详情里也会看到「通过」按钮(操作入口对齐 Web 端),
|
||
// 但发起人不是当前审批人,没有可处理的 task:给一个明确的提示而不是含糊的「缺少任务ID」。
|
||
c.oaErr(403, 403, "您不是当前审批人,无法通过/驳回该审批")
|
||
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.oaErr(404, 404, "任务不存在")
|
||
return
|
||
}
|
||
if task.ApproverID != uid {
|
||
c.oaErr(403, 403, "您无权处理该任务")
|
||
return
|
||
}
|
||
if task.Status != models.ApprovalTaskPending {
|
||
c.oaErr(400, 400, "该任务已处理")
|
||
return
|
||
}
|
||
|
||
res, err := approvalEngine.Approve(p.TaskID, approvalEngine.ApproveInput{
|
||
ApproverID: uid,
|
||
Pass: p.Pass,
|
||
Comment: strings.TrimSpace(p.Comment),
|
||
})
|
||
if err != nil {
|
||
c.oaErr(500, 500, err.Error())
|
||
return
|
||
}
|
||
|
||
var inst models.ApprovalInstance
|
||
_ = models.Orm.QueryTable(new(models.ApprovalInstance)).
|
||
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),
|
||
})
|
||
}
|
||
|
||
// ApprovalWithdraw POST /api/oa/approval/withdraw/:id
|
||
// 发起人撤回(仅审批中允许)。
|
||
func (c *AppOaController) ApprovalWithdraw() {
|
||
claims, ok := oaApprovalClaims(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
tid := uint64(claims.TenantId)
|
||
uid := oaApprovalSubmitUID(claims)
|
||
|
||
id := oaApprovalIDParam(c)
|
||
if id == 0 {
|
||
c.oaErr(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.oaErr(404, 404, "审批不存在")
|
||
return
|
||
}
|
||
if inst.SubmitUserID != uid {
|
||
c.oaErr(403, 403, "仅发起人可撤回")
|
||
return
|
||
}
|
||
if inst.Status != models.ApprovalInstanceRunning {
|
||
c.oaErr(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)
|
||
|
||
var updated models.ApprovalInstance
|
||
_ = models.Orm.QueryTable(new(models.ApprovalInstance)).
|
||
Filter("id", id).One(&updated)
|
||
c.oaOk(map[string]interface{}{"item": oaApprovalInstanceView(updated)})
|
||
}
|