Files
yunzerwebsiteallinone/go/controllers/app_approval.go
T
2026-10-16 23:46:49 +08:00

860 lines
26 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"
"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)})
}