This commit is contained in:
hero920103 committed 2026-10-17 11:52:02 +08:00
1 parent 184d5b0403
commit a758966fae
18 files changed
+2002 -19

No files matched your search

+296 -14
View File
@@ -2,6 +2,7 @@ package controllers
import (
"encoding/json"
"errors"
"fmt"
"io"
"path"
@@ -13,6 +14,7 @@ import (
"server/models"
"server/pkg/jwtutil"
"server/services"
"server/services/approval"
"github.com/beego/beego/v2/client/orm"
beego "github.com/beego/beego/v2/server/web"
@@ -355,8 +357,39 @@ func reimburseListData(claims *jwtutil.Claims, page, pageSize int, keyword strin
if pageSize < 1 || pageSize > 100 {
pageSize = 10
}
qs := models.Orm.QueryTable(new(models.BackendReimbursement)).
Filter("tid", claims.TenantId).Filter("user_id", claims.UserID).Filter("is_deleted", 0)
// 待我审批(status=-1 约定值):审批人视角,切换为「我名下运行中实例的待办单据」
// 查询(yz_approval_task.approver_id=当前用户),不再按创建人 user_id 过滤
mineOnly := hasStatus && status == -1
if mineOnly {
hasStatus = false
taskQs := models.Orm.QueryTable(new(models.ApprovalTask)).
Filter("scope", models.ApprovalScopeTenant).
Filter("tid", claims.TenantId).
Filter("approver_id", claims.UserID).
Filter("status", models.ApprovalTaskPending).
Filter("is_deleted", 0).
Filter("biz_type__in", reimburseFallbackBizTypes)
var tasks []models.ApprovalTask
if _, err := taskQs.All(&tasks, "biz_id"); err != nil {
return nil, 500, "查询审批待办失败"
}
ids := make([]uint64, 0, len(tasks))
for _, t := range tasks {
if t.BizID > 0 {
ids = append(ids, t.BizID)
}
}
if len(ids) == 0 {
return map[string]interface{}{"list": []map[string]interface{}{}, "total": 0}, 200, ""
}
qs = models.Orm.QueryTable(new(models.BackendReimbursement)).
Filter("tid", claims.TenantId).Filter("id__in", ids).Filter("is_deleted", 0)
}
if keyword != "" {
qs = qs.Filter("description__icontains", keyword)
}
@@ -827,8 +860,99 @@ func (c *BackendReimburseController) Submit() {
c.reply(200, "success", nil)
}
// reimburseActPayload 审批操作请求体。
type reimburseActPayload struct {
Comment string `json:"comment"`
}
// readReimburseActPayload 读取审批意见(可空)。
func (c *BackendReimburseController) readReimburseActPayload() string {
var payload reimburseActPayload
raw, err := io.ReadAll(c.Ctx.Request.Body)
if err == nil && len(raw) > 0 {
_ = json.Unmarshal(raw, &payload)
}
return strings.TrimSpace(payload.Comment)
}
// Approve POST /backend/reimbursements/approve/:id 通过当前审批节点(body: {comment})。
func (c *BackendReimburseController) Approve() {
claims, ok := c.claims()
if !ok {
c.reply(401, "未登录或无权限", nil)
return
}
id, ok := c.getID()
if !ok {
c.reply(400, "无效ID", nil)
return
}
code, msg, hint := reimburseActCore(claims, id, true, c.readReimburseActPayload())
if msg != "" {
c.reply(code, msg, nil)
return
}
c.reply(200, hint, nil)
}
// Reject POST /backend/reimbursements/reject/:id 驳回报销单(body: {comment})。
func (c *BackendReimburseController) Reject() {
claims, ok := c.claims()
if !ok {
c.reply(401, "未登录或无权限", nil)
return
}
id, ok := c.getID()
if !ok {
c.reply(400, "无效ID", nil)
return
}
code, msg, hint := reimburseActCore(claims, id, false, c.readReimburseActPayload())
if msg != "" {
c.reply(code, msg, nil)
return
}
c.reply(200, hint, nil)
}
// MyApproveTask GET /backend/reimbursements/my-approve-task/:id
// 查当前用户在该单据上的待审批任务:返回 {task_id, node_name},无待办返回 data=null。
func (c *BackendReimburseController) MyApproveTask() {
claims, ok := c.claims()
if !ok {
c.reply(401, "未登录或无权限", nil)
return
}
id, ok := c.getID()
if !ok {
c.reply(400, "无效ID", nil)
return
}
task, err := reimburseMyTaskCore(claims, id)
if err != nil {
c.reply(500, "查询审批任务失败", nil)
return
}
if task == nil {
c.reply(200, "success", nil)
return
}
c.reply(200, "success", map[string]interface{}{
"task_id": task.ID,
"node_name": task.NodeName,
})
}
// reimburseFallbackBizTypes 报销匹配审批流的单据类型回退顺序:
// 先按报销专有流(biz_type=reimburse),未配置时回退到 OA 分类默认流
// (FlowSave 未显式指定单据类型时 biz_type=module_code,即 oa)。
var reimburseFallbackBizTypes = []string{models.ApprovalBizReimburse, "oa"}
// reimburseSubmitCore 提交审批共享逻辑:仅草稿可提交,需租户已配置启用审批流,
// 提交后状态置为审批中(1),并写入 action=1 的审批记录。
// 提交后状态置为审批中(1)并写入 action=1 的审批记录。
// 流程匹配与实例/任务创建走运行期引擎(services/approval):
// 实例落 yz_approval_instance,首节点待办落 yz_approval_task,
// 审批人在「我的待办」里处理(reimburseActCore)。
func reimburseSubmitCore(claims *jwtutil.Claims, id uint64) (int, string) {
row, err := findOwnedReimbursement(claims, id)
if err != nil {
@@ -837,31 +961,189 @@ func reimburseSubmitCore(claims *jwtutil.Claims, id uint64) (int, string) {
if row.Status != 0 {
return 400, "当前状态不可提交"
}
var flow models.BackendApprovalFlow
err = models.Orm.QueryTable(new(models.BackendApprovalFlow)).
Filter("tid", claims.TenantId).Filter("is_active", 1).Filter("is_deleted", 0).
OrderBy("step_order").One(&flow)
if err != nil {
var deptID uint64
if row.DepartmentID != nil {
deptID = *row.DepartmentID
}
// 依次尝试回退类型匹配流程;有部门限制的流程未命中时由引擎继续尝试通用流
var instID uint64
var submitErr error
for _, bizType := range reimburseFallbackBizTypes {
instID, submitErr = approval.Submit(approval.SubmitInput{
Scope: models.ApprovalScopeTenant,
Tid: uint64(claims.TenantId),
BizTid: uint64(claims.TenantId),
BizType: bizType,
BizID: id,
BizTitle: claims.Username + "的报销单",
SubmitUID: uint64(claims.UserID),
SubmitName: claims.Username,
DeptID: deptID,
})
if submitErr == nil {
break
}
var noFlow *approval.ErrNoFlow
if !errors.As(submitErr, &noFlow) {
return 400, submitErr.Error()
}
}
if submitErr != nil {
return 400, "尚未配置审批流程"
}
// 实例可能无审批节点直接通过(如流程只有通知),按实例实际状态落单据状态
var inst models.ApprovalInstance
if err := models.Orm.QueryTable(new(models.ApprovalInstance)).
Filter("id", instID).One(&inst); err != nil {
return 500, "查询审批实例失败"
}
newStatus := int8(1) // 审批中
if inst.Status == models.ApprovalInstanceApproved {
newStatus = 2
}
now := time.Now()
_, err = models.Orm.QueryTable(new(models.BackendReimbursement)).Filter("id", id).Update(orm.Params{
"status": 1, "current_step": flow.StepOrder, "current_approver_id": 0, "update_time": now,
})
if err != nil {
if _, err := models.Orm.QueryTable(new(models.BackendReimbursement)).Filter("id", id).Update(orm.Params{
"status": newStatus, "current_approver_id": 0, "update_time": now,
}); err != nil {
return 500, "提交失败"
}
_, err = models.Orm.Insert(&models.BackendApprovalRecord{
if _, err := models.Orm.Insert(&models.BackendApprovalRecord{
Tid: uint64(claims.TenantId), UID: uint64Ptr(uint64(claims.UserID)), ReimbursementID: id,
ApproverID: uint64(claims.UserID), ApproverName: claims.Username, Action: 1,
Step: flow.StepOrder, PrevStep: 0,
})
if err != nil {
}); err != nil {
return 500, "提交审批记录失败"
}
return 200, ""
}
// findRunningReimburseInstance 按报销单 ID 查运行中的审批实例
// (实例的 biz_type 取发起时命中的类型,按回退顺序逐一查找)。
func findRunningReimburseInstance(tid, id uint64) (*models.ApprovalInstance, error) {
for _, bizType := range reimburseFallbackBizTypes {
var inst models.ApprovalInstance
err := models.Orm.QueryTable(new(models.ApprovalInstance)).
Filter("tid", tid).Filter("biz_type", bizType).Filter("biz_id", id).
Filter("status", models.ApprovalInstanceRunning).Filter("is_deleted", 0).
OrderBy("-id").One(&inst)
if err == nil {
return &inst, nil
}
if err != orm.ErrNoRows {
return nil, err
}
}
return nil, orm.ErrNoRows
}
// reimburseMyTaskCore 查当前用户在指定报销单上的待审批任务(无则返回 nil)。
func reimburseMyTaskCore(claims *jwtutil.Claims, id uint64) (*models.ApprovalTask, error) {
inst, err := findRunningReimburseInstance(uint64(claims.TenantId), id)
if err != nil {
return nil, nil // 无运行中实例:不是「错误」,只是没有待办
}
var task models.ApprovalTask
err = models.Orm.QueryTable(new(models.ApprovalTask)).
Filter("instance_id", inst.ID).
Filter("approver_id", claims.UserID).
Filter("status", models.ApprovalTaskPending).
Filter("is_deleted", 0).
One(&task)
if err == orm.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
return &task, nil
}
// reimburseActCore 审批操作共享逻辑:当前用户必须是该单当前节点的待办审批人,
// 通过/驳回经运行期引擎推进(或签/会签/多级节点均由引擎处理),
// 实例结束时同步报销单状态,并写审批记录保持时间线兼容。
// 返回 (HTTP 状态码, 错误信息, 成功提示),错误信息为空串表示成功。
func reimburseActCore(claims *jwtutil.Claims, id uint64, pass bool, comment string) (int, string, string) {
inst, err := findRunningReimburseInstance(uint64(claims.TenantId), id)
if err != nil {
return 404, "该报销单不在审批中", ""
}
var task models.ApprovalTask
err = models.Orm.QueryTable(new(models.ApprovalTask)).
Filter("instance_id", inst.ID).
Filter("approver_id", claims.UserID).
Filter("status", models.ApprovalTaskPending).
Filter("is_deleted", 0).
One(&task)
if err == orm.ErrNoRows {
return 400, "您没有该单据的待审批任务", ""
}
if err != nil {
return 500, "查询审批任务失败", ""
}
result, err := approval.Approve(task.ID, approval.ApproveInput{
ApproverID: uint64(claims.UserID),
Pass: pass,
Comment: comment,
})
if err != nil {
return 400, err.Error(), ""
}
// 业务回调:写时间线记录 + 实例结束时同步单据状态
applyReimburseApprovalResult(uint64(claims.TenantId), id,
uint64(claims.UserID), claims.Username, task.Sort,
pass, comment, result.Finished, result.InstanceStat)
hint := result.Message
if hint == "" {
hint = "操作成功"
}
return 200, "", hint
}
// isReimburseBizType 业务类型是否归属报销(提交与实例落库的 biz_type 均可能命中)。
func isReimburseBizType(bizType string) bool {
for _, bt := range reimburseFallbackBizTypes {
if bt == bizType {
return true
}
}
return false
}
// applyReimburseApprovalResult 报销业务回调(报销页与审批中心两处审批入口共用):
// - 始终写一条审批记录(action: 2通过 3驳回),与既有时间线保持兼容;
// - 实例结束时同步报销单状态:通过→2,驳回→3。
func applyReimburseApprovalResult(tid, bizID, approverID uint64, approverName string,
taskSort int, pass bool, comment string, finished bool, instStat int8) {
action := int8(models.ApprovalActionApprove)
if !pass {
action = models.ApprovalActionReject
}
if _, err := models.Orm.Insert(&models.BackendApprovalRecord{
Tid: tid, UID: uint64Ptr(approverID), ReimbursementID: bizID,
ApproverID: approverID, ApproverName: approverName, Action: action,
Comment: &comment, Step: int8(taskSort),
}); err != nil {
return
}
if finished {
newStatus := int8(3)
if instStat == models.ApprovalInstanceApproved {
newStatus = 2
}
_, _ = models.Orm.QueryTable(new(models.BackendReimbursement)).Filter("id", bizID).
Update(orm.Params{"status": newStatus, "update_time": time.Now()})
}
}
type reimbursementStatusPayload struct {
Status int8 `json:"status"`
}