package controllers import ( "fmt" "time" "server/models" "server/pkg/jwtutil" "server/services" approvalEngine "server/services/approval" "github.com/beego/beego/v2/client/orm" ) // reimburse_approval_core.go 报销单「审批人视角」操作共享逻辑。 // // Web 端(/backend/reimbursements/approve|reject|my-approve-task)与 // 小程序端(/api/oa/expense/approve|reject|my-approve-task)共用本文件; // 审批中心 TaskAct 也复用其中的 biz_type 判定与时间线回写。 // isReimburseBizType 判断审批任务/实例是否属于报销类单据。 func isReimburseBizType(bizType string) bool { return approvalEngine.IsReimburseBizType(bizType) } // claimsReimburseUIDs 当前登录用户可能出现在 approver_id 上的两种口径: // 认证中心 identity_id 与本租户 uid(历史上两种写法并存,这里都兼容)。 func claimsReimburseUIDs(claims *jwtutil.Claims) []interface{} { raw := uint64(claims.UserID) out := []interface{}{raw} if uid := services.ResolveTenantUID(uint64(claims.TenantId), raw); uid != raw { out = append(out, uid) } return out } // reimburseMyTaskCore 查当前用户在报销单 id 上的待办审批任务;无待办返回 (nil, nil)。 func reimburseMyTaskCore(claims *jwtutil.Claims, id uint64) (*models.ApprovalTask, error) { tid := uint64(claims.TenantId) var task models.ApprovalTask err := models.Orm.QueryTable(new(models.ApprovalTask)). Filter("scope", models.ApprovalScopeTenant). Filter("tid", tid). Filter("biz_type", models.ApprovalBizReimburse). Filter("biz_id", id). Filter("status", models.ApprovalTaskPending). Filter("is_deleted", 0). Filter("approver_id__in", claimsReimburseUIDs(claims)...). OrderBy("-id").One(&task) if err == orm.ErrNoRows { return nil, nil } if err != nil { return nil, err } return &task, nil } // reimburseActCore 报销单审批操作共享逻辑(pass=true 通过 / false 驳回)。 // // 返回 (httpCode, errMsg, hint):errMsg 非空表示失败;成功时 hint 为提示文案。 // 节点流转交由统一审批引擎,本函数负责定位当前用户的待办任务并补记业务时间线。 func reimburseActCore(claims *jwtutil.Claims, id uint64, pass bool, comment string) (int, string, string) { if _, err := findOwnedReimbursement(claims, id); err != nil { return 404, "报销单不存在", "" } task, err := reimburseMyTaskCore(claims, id) if err != nil { return 500, "查询审批任务失败", "" } if task == nil { return 400, "当前没有待您审批的节点", "" } result, err := approvalEngine.Approve(task.ID, approvalEngine.ApproveInput{ ApproverID: task.ApproverID, Pass: pass, Comment: comment, }) if err != nil { return 400, err.Error(), "" } applyReimburseApprovalResult(uint64(claims.TenantId), id, uint64(claims.UserID), reimburseActorName(uint64(claims.TenantId), claims), task.Sort, pass, comment, result.Finished, result.InstanceStat) hint := result.Message if hint == "" { if pass { hint = "审批通过" } else { hint = "已驳回" } } return 200, "", hint } // applyReimburseApprovalResult 把一次审批动作回写到报销业务侧: // 补记 yz_backend_approval_records 时间线;流程结束时同步单据终态(引擎的 // SyncBizResult 也会做同样的事,这里按终态幂等,避免重复插记录)。 func applyReimburseApprovalResult(tid, bizID, actorID uint64, actorName string, step int, pass bool, comment string, finished bool, instStatus int8) { if tid == 0 || bizID == 0 { return } action := int8(models.ApprovalActionApprove) if !pass { action = models.ApprovalActionReject } var row models.BackendReimbursement if err := models.Orm.QueryTable(new(models.BackendReimbursement)). Filter("id", bizID).Filter("tid", tid).Filter("is_deleted", 0).One(&row); err != nil { return } // 终态同步(审批中→通过/驳回),引擎已同步过则跳过 var bizStatus int8 if finished { switch instStatus { case models.ApprovalInstanceApproved: bizStatus = 2 case models.ApprovalInstanceRejected: bizStatus = 3 } if bizStatus != 0 && row.Status != bizStatus { _, _ = models.Orm.QueryTable(new(models.BackendReimbursement)). Filter("id", bizID).Filter("tid", tid). Update(orm.Params{"status": bizStatus, "current_approver_id": 0, "update_time": time.Now()}) } } // 时间线去重:同一步骤同一动作只记一次 dup := models.Orm.QueryTable(new(models.BackendApprovalRecord)). Filter("tid", tid).Filter("reimbursement_id", bizID). Filter("approver_id", actorID).Filter("action", action). Filter("step", int8(step)).Filter("is_deleted", 0).Exist() if dup { return } var commentPtr *string if comment != "" { commentPtr = &comment } _, _ = models.Orm.Insert(&models.BackendApprovalRecord{ Tid: tid, UID: uint64Ptr(actorID), ReimbursementID: bizID, ApproverID: actorID, ApproverName: fmt.Sprintf("%s", actorName), Action: action, Comment: commentPtr, Step: int8(step), PrevStep: int8(step), }) }