diff --git a/backend/app/api/bx/advance.py b/backend/app/api/bx/advance.py index 9692b0449..a8ad18cb8 100644 --- a/backend/app/api/bx/advance.py +++ b/backend/app/api/bx/advance.py @@ -67,7 +67,7 @@ def list_advances( d0, d1 = bounds filters.append(BxAdvanceApply.create_time >= d0) filters.append(BxAdvanceApply.create_time < d1) - rows, total = AdvanceService.get_paged(db, tenant_no, page=page, page_size=pageSize, filters=filters) + rows, total = AdvanceService.get_paged(db, tenant_no, page=page, page_size=pageSize, filters=filters, user=user) out_rows = [AdvanceApplyOut.model_validate(r) for r in rows] _fill_offset_amounts(db, tenant_no, rows, out_rows) return ApiResponse(data=PagedData(rows=out_rows, total=total, page=page, page_size=pageSize)) @@ -104,7 +104,7 @@ def my_advances( d0, d1 = bounds filters.append(BxAdvanceApply.create_time >= d0) filters.append(BxAdvanceApply.create_time < d1) - rows, total = AdvanceService.get_paged(db, tenant_no, page=page, page_size=pageSize, filters=filters) + rows, total = AdvanceService.get_paged(db, tenant_no, page=page, page_size=pageSize, filters=filters, user=user) out_rows = [AdvanceApplyOut.model_validate(r) for r in rows] _fill_offset_amounts(db, tenant_no, rows, out_rows) return ApiResponse(data=PagedData(rows=out_rows, total=total, page=page, page_size=pageSize)) @@ -116,7 +116,7 @@ def my_advance_stats( user: BxUserContext = Depends(get_bx_current_user), ): tenant_no = _tenant(db, user) - data = AdvanceService.my_apply_stats(db, user.id, tenant_no) + data = AdvanceService.my_apply_stats(db, user, tenant_no) return ApiResponse(data=AdvanceMyStatsOut.model_validate(data)) @@ -126,7 +126,7 @@ def advance_status_counts( user: BxUserContext = Depends(get_bx_current_user), ): tenant_no = _tenant(db, user) - return ApiResponse(data=AdvanceService.global_status_counts(db, tenant_no)) + return ApiResponse(data=AdvanceService.global_status_counts(db, tenant_no, user=user)) @router.get("/pending-approval", response_model=ApiResponse[PagedData[AdvanceApplyOut]]) @@ -198,7 +198,7 @@ def get_advance( user: BxUserContext = Depends(get_bx_current_user), ): tenant_no = _tenant(db, user) - advance = AdvanceService.get_by_id(db, advance_id, tenant_no) + advance = AdvanceService.get_by_id(db, advance_id, tenant_no, user=user) if not advance: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="备用金申请不存在") out = AdvanceApplyOut.model_validate(advance) @@ -214,9 +214,9 @@ def get_approvals( user: BxUserContext = Depends(get_bx_current_user), ): tenant_no = _tenant(db, user) - if not AdvanceService.get_by_id(db, advance_id, tenant_no): + if not AdvanceService.get_by_id(db, advance_id, tenant_no, user=user): raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="备用金申请不存在") - approvals = AdvanceService.get_approvals(db, advance_id, tenant_no) + approvals = AdvanceService.get_approvals(db, advance_id, tenant_no, user=user) return ApiResponse(data=[AdvanceApprovalOut.model_validate(a) for a in approvals]) @@ -227,9 +227,9 @@ def get_approval_history( user: BxUserContext = Depends(get_bx_current_user), ): tenant_no = _tenant(db, user) - if not AdvanceService.get_by_id(db, advance_id, tenant_no): + if not AdvanceService.get_by_id(db, advance_id, tenant_no, user=user): raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="备用金申请不存在") - history = AdvanceService.get_approval_history(db, advance_id, tenant_no) + history = AdvanceService.get_approval_history(db, advance_id, tenant_no, user=user) return ApiResponse(data=[AdvanceApprovalOut.model_validate(a) for a in history]) @@ -243,7 +243,7 @@ def create_advance( data = body.model_dump(exclude_unset=True) eid = data.get("expense_item_id") if eid: - if not ExpenseItemService.get_by_id(db, eid, tenant_no): + if not ExpenseItemService.get_by_id(db, eid, tenant_no, user=user): raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"费用项目 {eid} 不存在") data["applicant_id"] = user.id data["applicant_name"] = user.name @@ -277,7 +277,7 @@ def update_advance( data = body.model_dump(exclude_unset=True) if data.get("expense_item_id"): eid = data["expense_item_id"] - if not ExpenseItemService.get_by_id(db, eid, tenant_no): + if not ExpenseItemService.get_by_id(db, eid, tenant_no, user=user): raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"费用项目 {eid} 不存在") advance = AdvanceService.update(db, advance_id, data, user=user) if not advance: @@ -338,7 +338,7 @@ def delete_advance( user: BxUserContext = Depends(get_bx_current_user), ): tenant_no = _tenant(db, user) - if not AdvanceService.get_by_id(db, advance_id, tenant_no): + if not AdvanceService.get_by_id(db, advance_id, tenant_no, user=user): raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="备用金申请不存在") AdvanceService.soft_delete(db, advance_id, user=user) db.commit() diff --git a/backend/app/api/bx/attachment.py b/backend/app/api/bx/attachment.py index 16e7b6cbb..1e99d7baa 100644 --- a/backend/app/api/bx/attachment.py +++ b/backend/app/api/bx/attachment.py @@ -70,7 +70,7 @@ def download_file( user: BxUserContext = Depends(get_bx_current_user), ): tenant_no = _tenant(db, user) - attachment = AttachmentService.get_by_id(db, attachment_id, tenant_no) + attachment = AttachmentService.get_by_id(db, attachment_id, tenant_no, user=user) if not attachment: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="附件不存在") try: @@ -92,7 +92,7 @@ def delete_attachment( user: BxUserContext = Depends(get_bx_current_user), ): tenant_no = _tenant(db, user) - if not AttachmentService.get_by_id(db, attachment_id, tenant_no): + if not AttachmentService.get_by_id(db, attachment_id, tenant_no, user=user): raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="附件不存在") success = AttachmentService.soft_delete(db, attachment_id, user=user) db.commit() @@ -107,7 +107,7 @@ def preview_attachment( ): """返回图片/附件预览 URL(MinIO 预签名或本地公开地址,对齐门户 storage 能力)。""" tenant_no = _tenant(db, user) - attachment = AttachmentService.get_by_id(db, attachment_id, tenant_no) + attachment = AttachmentService.get_by_id(db, attachment_id, tenant_no, user=user) if not attachment: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="附件不存在") try: @@ -132,7 +132,9 @@ def list_by_biz( ): tenant_no = _tenant(db, user) biz_id_value = str(biz_id).strip() if biz_id else "0" - attachments = AttachmentService.get_by_biz(db, biz_type=biz_type, biz_id=biz_id_value, tenant_no=tenant_no) + attachments = AttachmentService.get_by_biz( + db, biz_type=biz_type, biz_id=biz_id_value, tenant_no=tenant_no, user=user + ) return ApiResponse(data=[AttachmentOut.model_validate(a) for a in attachments]) diff --git a/backend/app/api/bx/expense_item.py b/backend/app/api/bx/expense_item.py index 7bf7a8d17..59fca96d9 100644 --- a/backend/app/api/bx/expense_item.py +++ b/backend/app/api/bx/expense_item.py @@ -19,7 +19,7 @@ def get_expense_item_tree( user: BxUserContext = Depends(get_bx_current_user), ): tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) - tree = ExpenseItemService.get_item_tree(db, tenant_no) + tree = ExpenseItemService.get_item_tree(db, tenant_no, user=user) return ApiResponse(data=[ExpenseItemOut.model_validate(i) for i in tree]) @@ -30,7 +30,7 @@ def get_expense_item( user: BxUserContext = Depends(get_bx_current_user), ): tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) - item = ExpenseItemService.get_by_id(db, item_id, tenant_no) + item = ExpenseItemService.get_by_id(db, item_id, tenant_no, user=user) if not item: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="费用项目不存在") return ApiResponse(data=ExpenseItemOut.model_validate(item)) @@ -80,7 +80,7 @@ def delete_expense_item( user: BxUserContext = Depends(get_bx_current_user), ): tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) - item = ExpenseItemService.get_by_id(db, item_id, tenant_no) + item = ExpenseItemService.get_by_id(db, item_id, tenant_no, user=user) if not item: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="费用项目不存在") ExpenseItemService.soft_delete(db, item_id, user=user) diff --git a/backend/app/api/bx/payment.py b/backend/app/api/bx/payment.py index f3a570922..27de62209 100644 --- a/backend/app/api/bx/payment.py +++ b/backend/app/api/bx/payment.py @@ -70,7 +70,7 @@ def list_pending_payments( d0, d1 = bounds filters.append(BxReimburseApply.create_time >= d0) filters.append(BxReimburseApply.create_time < d1) - rows, total = ReimburseService.get_paged(db, tenant_no, page=page, page_size=pageSize, filters=filters) + rows, total = ReimburseService.get_paged(db, tenant_no, page=page, page_size=pageSize, filters=filters, user=user) outs = ReimburseService.out_rows_with_attachment_counts(db, tenant_no, list(rows)) return ApiResponse( data=PagedData(rows=_wrap_reimburse_rows(outs), total=total, page=page, page_size=pageSize) @@ -83,7 +83,7 @@ def payment_finance_stats( user: BxUserContext = Depends(get_bx_current_user), ): tenant_no = _tenant(db, user) - data = ReimburseService.finance_payment_stats(db, tenant_no) + data = ReimburseService.finance_payment_stats(db, tenant_no, user=user) return ApiResponse(data=PaymentFinanceStatsOut.model_validate(data)) @@ -113,7 +113,7 @@ def list_processed_payments( d0, d1 = bounds filters.append(BxReimburseApply.create_time >= d0) filters.append(BxReimburseApply.create_time < d1) - rows, total = ReimburseService.get_paged(db, tenant_no, page=page, page_size=pageSize, filters=filters) + rows, total = ReimburseService.get_paged(db, tenant_no, page=page, page_size=pageSize, filters=filters, user=user) outs = ReimburseService.out_rows_with_attachment_counts(db, tenant_no, list(rows)) return ApiResponse( data=PagedData(rows=_wrap_reimburse_rows(outs), total=total, page=page, page_size=pageSize) @@ -220,7 +220,7 @@ def list_advance_issues( d0, d1 = bounds filters.append(BxAdvanceApply.create_time >= d0) filters.append(BxAdvanceApply.create_time < d1) - rows, total = AdvanceService.get_paged(db, tenant_no, page=page, page_size=pageSize, filters=filters) + rows, total = AdvanceService.get_paged(db, tenant_no, page=page, page_size=pageSize, filters=filters, user=user) out_rows = [AdvanceIssueOut.model_validate(r) for r in rows] if out_rows: offset_map = AdvanceService.offset_amount_map(db, tenant_no, [r.id for r in rows]) @@ -235,7 +235,7 @@ def advance_issue_stats( user: BxUserContext = Depends(get_bx_current_user), ): tenant_no = _tenant(db, user) - data = AdvanceService.finance_issue_stats(db, tenant_no) + data = AdvanceService.finance_issue_stats(db, tenant_no, user=user) return ApiResponse(data=AdvanceFinanceIssueStatsOut.model_validate(data)) @@ -271,7 +271,7 @@ def export_advance_issues( d0, d1 = bounds filters.append(BxAdvanceApply.create_time >= d0) filters.append(BxAdvanceApply.create_time < d1) - rows, _ = AdvanceService.get_paged(db, tenant_no, page=1, page_size=99999, filters=filters) + rows, _ = AdvanceService.get_paged(db, tenant_no, page=1, page_size=99999, filters=filters, user=user) out_rows = [AdvanceIssueOut.model_validate(r) for r in rows] buf = io.StringIO() @@ -323,7 +323,7 @@ def issue_advance( user: BxUserContext = Depends(get_bx_current_user), ): tenant_no = _tenant(db, user) - issue_atts = AttachmentService.get_by_biz(db, "advance_issue", advance_id, tenant_no) + issue_atts = AttachmentService.get_by_biz(db, "advance_issue", advance_id, tenant_no, user=user) if not issue_atts: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="请至少上传 1 个发放附件") advance = AdvanceService.issue( @@ -346,6 +346,7 @@ def list_advance_offset_records( user: BxUserContext = Depends(get_bx_current_user), ): tenant_no = _tenant(db, user) + AdvanceService.get_by_id(db, advance_id, tenant_no, user=user) rows = ( db.query(BxReimburseOffset, BxReimburseApply) .join( @@ -385,6 +386,7 @@ def list_reimburse_offset_records( user: BxUserContext = Depends(get_bx_current_user), ): tenant_no = _tenant(db, user) + ReimburseService.get_by_id(db, payment_id, tenant_no, user=user) rows = ( db.query(BxReimburseOffset, BxAdvanceApply) .join(BxAdvanceApply, BxAdvanceApply.id == BxReimburseOffset.advance_id) diff --git a/backend/app/api/bx/reimburse.py b/backend/app/api/bx/reimburse.py index 097175f68..eef1770b7 100644 --- a/backend/app/api/bx/reimburse.py +++ b/backend/app/api/bx/reimburse.py @@ -33,7 +33,7 @@ def _tenant(db: Session, user: BxUserContext) -> str: def _build_details_data(db: Session, tenant_no: str, body_details) -> List[dict]: details_data: List[dict] = [] for d in body_details: - item = ExpenseItemService.get_by_id(db, d.expense_item_id, tenant_no) + item = ExpenseItemService.get_by_id(db, d.expense_item_id, tenant_no, user=user) if not item: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, @@ -95,7 +95,7 @@ def list_reimburses( d0, d1 = bounds filters.append(BxReimburseApply.create_time >= d0) filters.append(BxReimburseApply.create_time < d1) - rows, total = ReimburseService.get_paged(db, tenant_no, page=page, page_size=pageSize, filters=filters) + rows, total = ReimburseService.get_paged(db, tenant_no, page=page, page_size=pageSize, filters=filters, user=user) out_rows = ReimburseService.out_rows_with_attachment_counts(db, tenant_no, list(rows)) return ApiResponse(data=PagedData(rows=out_rows, total=total, page=page, page_size=pageSize)) @@ -131,7 +131,7 @@ def my_reimburses( d0, d1 = bounds filters.append(BxReimburseApply.create_time >= d0) filters.append(BxReimburseApply.create_time < d1) - rows, total = ReimburseService.get_paged(db, tenant_no, page=page, page_size=pageSize, filters=filters) + rows, total = ReimburseService.get_paged(db, tenant_no, page=page, page_size=pageSize, filters=filters, user=user) out_rows = ReimburseService.out_rows_with_attachment_counts(db, tenant_no, list(rows)) return ApiResponse(data=PagedData(rows=out_rows, total=total, page=page, page_size=pageSize)) @@ -142,7 +142,7 @@ def my_reimburse_stats( user: BxUserContext = Depends(get_bx_current_user), ): tenant_no = _tenant(db, user) - data = ReimburseService.my_apply_stats(db, user.id, tenant_no) + data = ReimburseService.my_apply_stats(db, user, tenant_no) return ApiResponse(data=ReimburseMyStatsOut.model_validate(data)) @@ -152,7 +152,7 @@ def reimburse_status_counts( user: BxUserContext = Depends(get_bx_current_user), ): tenant_no = _tenant(db, user) - return ApiResponse(data=ReimburseService.global_status_counts(db, tenant_no)) + return ApiResponse(data=ReimburseService.global_status_counts(db, tenant_no, user=user)) @router.get("/pending-approval", response_model=ApiResponse[PagedData[ReimburseApplyOut]]) @@ -224,7 +224,7 @@ def get_reimburse( user: BxUserContext = Depends(get_bx_current_user), ): tenant_no = _tenant(db, user) - reimburse = ReimburseService.get_by_id(db, reimburse_id, tenant_no) + reimburse = ReimburseService.get_by_id(db, reimburse_id, tenant_no, user=user) if not reimburse: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="报销单不存在") out = ReimburseService.build_apply_out_with_details(db, tenant_no, reimburse) @@ -238,9 +238,9 @@ def get_approvals( user: BxUserContext = Depends(get_bx_current_user), ): tenant_no = _tenant(db, user) - if not ReimburseService.get_by_id(db, reimburse_id, tenant_no): + if not ReimburseService.get_by_id(db, reimburse_id, tenant_no, user=user): raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="报销单不存在") - approvals = ReimburseService.get_approvals(db, reimburse_id, tenant_no) + approvals = ReimburseService.get_approvals(db, reimburse_id, tenant_no, user=user) return ApiResponse(data=[ReimburseApprovalOut.model_validate(a) for a in approvals]) @@ -251,9 +251,9 @@ def get_approval_history( user: BxUserContext = Depends(get_bx_current_user), ): tenant_no = _tenant(db, user) - if not ReimburseService.get_by_id(db, reimburse_id, tenant_no): + if not ReimburseService.get_by_id(db, reimburse_id, tenant_no, user=user): raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="报销单不存在") - history = ReimburseService.get_approval_history(db, reimburse_id, tenant_no) + history = ReimburseService.get_approval_history(db, reimburse_id, tenant_no, user=user) return ApiResponse(data=[ReimburseApprovalOut.model_validate(a) for a in history]) @@ -374,7 +374,7 @@ def delete_reimburse( user: BxUserContext = Depends(get_bx_current_user), ): tenant_no = _tenant(db, user) - if not ReimburseService.get_by_id(db, reimburse_id, tenant_no): + if not ReimburseService.get_by_id(db, reimburse_id, tenant_no, user=user): raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="报销单不存在") ReimburseService.soft_delete(db, reimburse_id, user=user) db.commit() diff --git a/backend/app/api/bx/workflow.py b/backend/app/api/bx/workflow.py index d7f1cdf93..562656bbb 100644 --- a/backend/app/api/bx/workflow.py +++ b/backend/app/api/bx/workflow.py @@ -26,7 +26,7 @@ def list_workflows( user: BxUserContext = Depends(get_bx_current_user), ): tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) - rows, total = WorkflowService.get_paged(db, tenant_no, page=page, page_size=pageSize) + rows, total = WorkflowService.get_paged(db, tenant_no, page=page, page_size=pageSize, user=user) out_rows = [WorkflowDefOut.model_validate(r) for r in rows] paged_data = PagedData(rows=out_rows, total=total, page=page, page_size=pageSize) return ApiResponse(data=paged_data) @@ -51,7 +51,7 @@ def get_workflow( user: BxUserContext = Depends(get_bx_current_user), ): tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) - workflow = WorkflowService.get_by_id(db, workflow_id, tenant_no) + workflow = WorkflowService.get_by_id(db, workflow_id, tenant_no, user=user) if not workflow: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="工作流不存在") return ApiResponse(data=WorkflowDefOut.model_validate(workflow)) @@ -107,7 +107,7 @@ def save_workflow_nodes( except ValueError as exc: db.rollback() raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc - workflow = WorkflowService.get_by_id(db, workflow_id, tenant_no) + workflow = WorkflowService.get_by_id(db, workflow_id, tenant_no, user=user) if not workflow: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="工作流不存在") db.commit() @@ -122,7 +122,7 @@ def delete_workflow( user: BxUserContext = Depends(get_bx_current_user), ): tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) - workflow = WorkflowService.get_by_id(db, workflow_id, tenant_no) + workflow = WorkflowService.get_by_id(db, workflow_id, tenant_no, user=user) if not workflow: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="工作流不存在") WorkflowService.soft_delete(db, workflow_id, user=user) diff --git a/backend/app/core/bx_auth.py b/backend/app/core/bx_auth.py index 0f633df5e..08b2d82db 100644 --- a/backend/app/core/bx_auth.py +++ b/backend/app/core/bx_auth.py @@ -27,6 +27,16 @@ class BxUserContext: "tenant_no": self.tenant_no, } + def to_user_info(self) -> Dict[str, Any]: + """与门户 JWT / apply_data_scope 所需的 user_info 字段对齐。""" + return { + "user_id": self.id, + "role": self.role, + "tenant_no": self.tenant_no, + "dept_id": self.dept_id, + "region_id": self.dept_id, + } + def get_bx_current_user( user: dict = Depends(require_admin_or_super_admin), diff --git a/backend/app/services/bx/advance.py b/backend/app/services/bx/advance.py index 9b6d65678..48bcda016 100644 --- a/backend/app/services/bx/advance.py +++ b/backend/app/services/bx/advance.py @@ -18,6 +18,7 @@ from app.models.bx_models import ( ) from app.services.bx.approval_cycle import filter_current_cycle_approvals from app.services.bx.base import BxCrudService +from app.services.bx_data_scope import apply_bx_scope from app.utils.bx_codegen import generate_apply_code from app.utils.bx_date_range import current_month_bounds, parse_create_time_day_bounds @@ -28,8 +29,17 @@ class AdvanceService: return get_tenant_no(db, user.to_tenant_user_dict()) @staticmethod - def get_by_id(db: Session, advance_id: str, tenant_no: str) -> Optional[BxAdvanceApply]: - return BxCrudService.get_by_id(BxAdvanceApply, db, advance_id, tenant_no) + def get_by_id( + db: Session, + advance_id: str, + tenant_no: str, + *, + user: Optional[BxUserContext] = None, + skip_scope: bool = False, + ) -> Optional[BxAdvanceApply]: + return BxCrudService.get_by_id( + BxAdvanceApply, db, advance_id, tenant_no, user=user, skip_scope=skip_scope + ) @staticmethod def get_paged( @@ -38,8 +48,12 @@ class AdvanceService: page: int, page_size: int, filters: Optional[List[Any]] = None, + *, + user: Optional[BxUserContext] = None, ) -> Tuple[Sequence[BxAdvanceApply], int]: - return BxCrudService.get_paged(BxAdvanceApply, db, tenant_no, page, page_size, filters) + return BxCrudService.get_paged( + BxAdvanceApply, db, tenant_no, page, page_size, filters, user=user + ) @staticmethod def create( @@ -74,13 +88,14 @@ class AdvanceService: tenant_no, obj_in, user_id=user.id, + user=user, ) @staticmethod def soft_delete(db: Session, advance_id: str, *, user: BxUserContext) -> bool: tenant_no = AdvanceService._tenant_no(db, user) return BxCrudService.soft_delete( - BxAdvanceApply, db, advance_id, tenant_no, user_id=user.id + BxAdvanceApply, db, advance_id, tenant_no, user_id=user.id, user=user ) @staticmethod @@ -178,6 +193,7 @@ class AdvanceService: if bounds: d0, d1 = bounds query = query.filter(BxAdvanceApply.create_time >= d0, BxAdvanceApply.create_time < d1) + query = apply_bx_scope(query, BxAdvanceApply, user, db, tenant_no) total = query.count() rows = ( query.order_by(BxAdvanceApply.create_time.desc()) @@ -203,7 +219,7 @@ class AdvanceService: @staticmethod def submit(db: Session, apply_id: str, user: BxUserContext) -> Optional[BxAdvanceApply]: tenant_no = AdvanceService._tenant_no(db, user) - apply_obj = AdvanceService.get_by_id(db, apply_id, tenant_no) + apply_obj = AdvanceService.get_by_id(db, apply_id, tenant_no, user=user) if not apply_obj: return None apply_obj.status = "pending" @@ -222,7 +238,7 @@ class AdvanceService: db: Session, apply_id: str, approver: BxUserContext, opinion: str ) -> Optional[BxAdvanceApply]: tenant_no = AdvanceService._tenant_no(db, approver) - apply_obj = AdvanceService.get_by_id(db, apply_id, tenant_no) + apply_obj = AdvanceService.get_by_id(db, apply_id, tenant_no, skip_scope=True) if not apply_obj or not apply_obj.wf_id: return None nodes = AdvanceService._workflow_nodes(db, apply_obj.wf_id, tenant_no) @@ -267,7 +283,7 @@ class AdvanceService: db: Session, apply_id: str, approver: BxUserContext, opinion: str ) -> Optional[BxAdvanceApply]: tenant_no = AdvanceService._tenant_no(db, approver) - apply_obj = AdvanceService.get_by_id(db, apply_id, tenant_no) + apply_obj = AdvanceService.get_by_id(db, apply_id, tenant_no, skip_scope=True) if not apply_obj or not apply_obj.wf_id: return None nodes = AdvanceService._workflow_nodes(db, apply_obj.wf_id, tenant_no) @@ -342,8 +358,10 @@ class AdvanceService: ) @staticmethod - def get_approvals(db: Session, apply_id: str, tenant_no: str) -> List[Dict[str, Any]]: - apply_obj = AdvanceService.get_by_id(db, apply_id, tenant_no) + def get_approvals( + db: Session, apply_id: str, tenant_no: str, *, user: Optional[BxUserContext] = None + ) -> List[Dict[str, Any]]: + apply_obj = AdvanceService.get_by_id(db, apply_id, tenant_no, user=user) if not apply_obj or not apply_obj.wf_id: return [] wf = BxCrudService.get_by_id(BxWorkflowDef, db, apply_obj.wf_id, tenant_no) @@ -376,9 +394,11 @@ class AdvanceService: return result @staticmethod - def get_approval_history(db: Session, apply_id: str, tenant_no: str) -> List[Dict[str, Any]]: + def get_approval_history( + db: Session, apply_id: str, tenant_no: str, *, user: Optional[BxUserContext] = None + ) -> List[Dict[str, Any]]: """返回全部审批动作,按时间正序排列,用于展示完整审批历史。""" - if not AdvanceService.get_by_id(db, apply_id, tenant_no): + if not AdvanceService.get_by_id(db, apply_id, tenant_no, user=user): return [] approvals = ( db.query(BxAdvanceApproval) @@ -393,26 +413,33 @@ class AdvanceService: return [AdvanceService._approval_record_dict(a, apply_id) for a in approvals] @staticmethod - def global_status_counts(db: Session, tenant_no: str) -> Dict[str, int]: - rows = ( - db.query(BxAdvanceApply.status, func.count()) - .filter(BxAdvanceApply.del_flag == 0, BxAdvanceApply.tenant_no == tenant_no) - .group_by(BxAdvanceApply.status) - .all() + def global_status_counts(db: Session, tenant_no: str, *, user: BxUserContext) -> Dict[str, int]: + query = db.query(BxAdvanceApply.status, func.count()).filter( + BxAdvanceApply.del_flag == 0, BxAdvanceApply.tenant_no == tenant_no ) + query = apply_bx_scope(query, BxAdvanceApply, user, db, tenant_no) + rows = query.group_by(BxAdvanceApply.status).all() return {str(s or ""): int(c) for s, c in rows} @staticmethod - def my_apply_stats(db: Session, user_id: str, tenant_no: str) -> dict: - base = and_( + def _my_apply_base(db: Session, user: BxUserContext, tenant_no: str): + query = db.query(BxAdvanceApply).filter( BxAdvanceApply.del_flag == 0, BxAdvanceApply.tenant_no == tenant_no, or_( - BxAdvanceApply.applicant_id == user_id, - BxAdvanceApply.create_by == user_id, + BxAdvanceApply.applicant_id == user.id, + BxAdvanceApply.create_by == user.id, ), ) - total_count = db.query(func.count()).select_from(BxAdvanceApply).filter(base).scalar() or 0 + return apply_bx_scope(query, BxAdvanceApply, user, db, tenant_no) + + @staticmethod + def my_apply_stats(db: Session, user: BxUserContext, tenant_no: str) -> dict: + base_q = AdvanceService._my_apply_base(db, user, tenant_no).subquery() + base = BxAdvanceApply.id.in_(db.query(base_q.c.id)) + total_count = ( + db.query(func.count()).select_from(BxAdvanceApply).filter(base).scalar() or 0 + ) pending = ( db.query(func.count()) .select_from(BxAdvanceApply) @@ -466,9 +493,7 @@ class AdvanceService: db.query(func.coalesce(func.sum(BxReimburseOffset.offset_amount), 0)) .join(BxAdvanceApply, BxReimburseOffset.advance_id == BxAdvanceApply.id) .filter( - BxAdvanceApply.applicant_id == user_id, - BxAdvanceApply.del_flag == 0, - BxAdvanceApply.tenant_no == tenant_no, + BxAdvanceApply.id.in_(db.query(base_q.c.id)), BxReimburseOffset.tenant_no == tenant_no, BxReimburseOffset.del_flag == 0, ) @@ -547,7 +572,7 @@ class AdvanceService: actual_amount: Optional[float] = None, ) -> Optional[BxAdvanceApply]: tenant_no = AdvanceService._tenant_no(db, user) - apply_obj = AdvanceService.get_by_id(db, advance_id, tenant_no) + apply_obj = AdvanceService.get_by_id(db, advance_id, tenant_no, user=user) if not apply_obj: return None apply_obj.status = "issued" @@ -564,8 +589,13 @@ class AdvanceService: return apply_obj @staticmethod - def finance_issue_stats(db: Session, tenant_no: str) -> dict: - base = and_(BxAdvanceApply.del_flag == 0, BxAdvanceApply.tenant_no == tenant_no) + def finance_issue_stats(db: Session, tenant_no: str, *, user: BxUserContext) -> dict: + base_q = ( + db.query(BxAdvanceApply) + .filter(BxAdvanceApply.del_flag == 0, BxAdvanceApply.tenant_no == tenant_no) + ) + base_q = apply_bx_scope(base_q, BxAdvanceApply, user, db, tenant_no).subquery() + base = BxAdvanceApply.id.in_(db.query(base_q.c.id)) pending_f = and_( BxAdvanceApply.status == "approved", or_(BxAdvanceApply.issue_status.is_(None), BxAdvanceApply.issue_status != "issued"), diff --git a/backend/app/services/bx/attachment.py b/backend/app/services/bx/attachment.py index 727a13c72..9126c9ee2 100644 --- a/backend/app/services/bx/attachment.py +++ b/backend/app/services/bx/attachment.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import List, Optional +from typing import List, Optional, Optional from sqlalchemy import case as sql_case, func, or_ from sqlalchemy.orm import Session @@ -9,6 +9,7 @@ from app.core.bx_auth import BxUserContext from app.core.tenant_context import get_tenant_no from app.models.bx_models import BxAttachment from app.services.bx.base import BxCrudService +from app.services.bx_data_scope import assert_bx_attachment_biz_in_scope, assert_bx_biz_in_scope class AttachmentService: @@ -17,8 +18,17 @@ class AttachmentService: return get_tenant_no(db, user.to_tenant_user_dict()) @staticmethod - def get_by_id(db: Session, attachment_id: str, tenant_no: str) -> Optional[BxAttachment]: - return BxCrudService.get_by_id(BxAttachment, db, attachment_id, tenant_no) + def get_by_id( + db: Session, + attachment_id: str, + tenant_no: str, + *, + user: Optional[BxUserContext] = None, + ) -> Optional[BxAttachment]: + row = BxCrudService.get_by_id(BxAttachment, db, attachment_id, tenant_no) + if row and user: + assert_bx_attachment_biz_in_scope(db, row, user, tenant_no) + return row @staticmethod def create( @@ -40,12 +50,18 @@ class AttachmentService: @staticmethod def soft_delete(db: Session, attachment_id: str, *, user: BxUserContext) -> bool: tenant_no = AttachmentService._tenant_no(db, user) + row = AttachmentService.get_by_id(db, attachment_id, tenant_no, user=user) + if not row: + return False return BxCrudService.soft_delete( BxAttachment, db, attachment_id, tenant_no, user_id=user.id ) @staticmethod - def get_by_biz(db: Session, biz_type: str, biz_id: str, tenant_no: str) -> List[BxAttachment]: + def get_by_biz( + db: Session, biz_type: str, biz_id: str, tenant_no: str, *, user: BxUserContext + ) -> List[BxAttachment]: + assert_bx_biz_in_scope(db, biz_type, biz_id, user, tenant_no) query = db.query(BxAttachment).filter( BxAttachment.biz_type == biz_type, BxAttachment.biz_id == biz_id, @@ -64,6 +80,7 @@ class AttachmentService: user: BxUserContext, ) -> int: tenant_no = AttachmentService._tenant_no(db, user) + assert_bx_biz_in_scope(db, biz_type, biz_id, user, tenant_no) if not ids: return 0 count = ( diff --git a/backend/app/services/bx/base.py b/backend/app/services/bx/base.py index 5f3fd880c..6887efdb0 100644 --- a/backend/app/services/bx/base.py +++ b/backend/app/services/bx/base.py @@ -1,22 +1,37 @@ """报销模块 Service 基类 — 租户隔离 + UUID + 软删除。""" from __future__ import annotations -from typing import Any, List, Optional, Sequence, Tuple, Type, TypeVar +from typing import TYPE_CHECKING, Any, List, Optional, Sequence, Tuple, Type, TypeVar from sqlalchemy.orm import Session from app.core.utils import generate_uuid +from app.services.bx_data_scope import apply_bx_scope, assert_bx_row_in_scope + +if TYPE_CHECKING: + from app.core.bx_auth import BxUserContext M = TypeVar("M") class BxCrudService: @staticmethod - def get_by_id(model: Type[M], db: Session, id: str, tenant_no: str) -> Optional[M]: + def get_by_id( + model: Type[M], + db: Session, + id: str, + tenant_no: str, + *, + user: Optional["BxUserContext"] = None, + skip_scope: bool = False, + ) -> Optional[M]: query = db.query(model).filter(model.id == id, model.tenant_no == tenant_no) if hasattr(model, "del_flag"): query = query.filter(model.del_flag == 0) - return query.first() + row = query.first() + if row and user and not skip_scope: + assert_bx_row_in_scope(db, row, model, user, tenant_no) + return row @staticmethod def get_paged( @@ -26,12 +41,16 @@ class BxCrudService: page: int, page_size: int, filters: Optional[List[Any]] = None, + *, + user: Optional["BxUserContext"] = None, ) -> Tuple[Sequence[M], int]: query = db.query(model).filter(model.tenant_no == tenant_no) if hasattr(model, "del_flag"): query = query.filter(model.del_flag == 0) if filters: query = query.filter(*filters) + if user: + query = apply_bx_scope(query, model, user, db, tenant_no) total = query.count() if hasattr(model, "sort_order"): query = query.order_by(model.sort_order.asc(), model.create_time.desc()) @@ -69,8 +88,12 @@ class BxCrudService: obj_in: dict, *, user_id: str, + user: Optional["BxUserContext"] = None, + skip_scope: bool = False, ) -> Optional[M]: - obj = BxCrudService.get_by_id(model, db, id, tenant_no) + obj = BxCrudService.get_by_id( + model, db, id, tenant_no, user=user, skip_scope=skip_scope + ) if not obj: return None for key, value in obj_in.items(): @@ -81,8 +104,19 @@ class BxCrudService: return obj @staticmethod - def soft_delete(model: Type[M], db: Session, id: str, tenant_no: str, *, user_id: str = "") -> bool: - obj = BxCrudService.get_by_id(model, db, id, tenant_no) + def soft_delete( + model: Type[M], + db: Session, + id: str, + tenant_no: str, + *, + user_id: str = "", + user: Optional["BxUserContext"] = None, + skip_scope: bool = False, + ) -> bool: + obj = BxCrudService.get_by_id( + model, db, id, tenant_no, user=user, skip_scope=skip_scope + ) if not obj: return False obj.del_flag = 1 diff --git a/backend/app/services/bx/expense_item.py b/backend/app/services/bx/expense_item.py index e96d4a1a4..471436c53 100644 --- a/backend/app/services/bx/expense_item.py +++ b/backend/app/services/bx/expense_item.py @@ -8,6 +8,7 @@ from app.core.bx_auth import BxUserContext from app.core.tenant_context import get_tenant_no from app.models.bx_models import BxExpenseItem from app.services.bx.base import BxCrudService +from app.services.bx_data_scope import apply_bx_scope class ExpenseItemService: @@ -16,8 +17,17 @@ class ExpenseItemService: return get_tenant_no(db, user.to_tenant_user_dict()) @staticmethod - def get_by_id(db: Session, item_id: str, tenant_no: str) -> Optional[BxExpenseItem]: - return BxCrudService.get_by_id(BxExpenseItem, db, item_id, tenant_no) + def get_by_id( + db: Session, + item_id: str, + tenant_no: str, + *, + user: Optional[BxUserContext] = None, + skip_scope: bool = False, + ) -> Optional[BxExpenseItem]: + return BxCrudService.get_by_id( + BxExpenseItem, db, item_id, tenant_no, user=user, skip_scope=skip_scope + ) @staticmethod def create( @@ -31,7 +41,7 @@ class ExpenseItemService: parent_id = data.get("parent_id") or "0" data["parent_id"] = parent_id if parent_id and parent_id != "0": - parent = ExpenseItemService.get_by_id(db, parent_id, tenant_no) + parent = ExpenseItemService.get_by_id(db, parent_id, tenant_no, user=user) if not parent: raise ValueError("父级费用项目不存在") data.setdefault("level", (parent.level or 1) + 1) @@ -63,7 +73,7 @@ class ExpenseItemService: if parent_id != "0": if parent_id == item_id: raise ValueError("不能将项目设置为自己的父级") - parent = ExpenseItemService.get_by_id(db, parent_id, tenant_no) + parent = ExpenseItemService.get_by_id(db, parent_id, tenant_no, user=user) if not parent: raise ValueError("父级费用项目不存在") return BxCrudService.update( @@ -73,23 +83,25 @@ class ExpenseItemService: tenant_no, data, user_id=user.id, + user=user, ) @staticmethod def soft_delete(db: Session, item_id: str, *, user: BxUserContext) -> bool: tenant_no = ExpenseItemService._tenant_no(db, user) return BxCrudService.soft_delete( - BxExpenseItem, db, item_id, tenant_no, user_id=user.id + BxExpenseItem, db, item_id, tenant_no, user_id=user.id, user=user ) @staticmethod - def get_item_tree(db: Session, tenant_no: str) -> List[BxExpenseItem]: - items = ( - db.query(BxExpenseItem) - .filter(BxExpenseItem.del_flag == 0, BxExpenseItem.tenant_no == tenant_no) - .order_by(BxExpenseItem.sort_order.asc(), BxExpenseItem.create_time.desc()) - .all() + def get_item_tree(db: Session, tenant_no: str, *, user: BxUserContext) -> List[BxExpenseItem]: + query = db.query(BxExpenseItem).filter( + BxExpenseItem.del_flag == 0, BxExpenseItem.tenant_no == tenant_no ) + query = apply_bx_scope(query, BxExpenseItem, user, db, tenant_no) + items = query.order_by( + BxExpenseItem.sort_order.asc(), BxExpenseItem.create_time.desc() + ).all() return ExpenseItemService._build_tree(items) @staticmethod diff --git a/backend/app/services/bx/reimburse.py b/backend/app/services/bx/reimburse.py index c04ad49f0..8b691b96a 100644 --- a/backend/app/services/bx/reimburse.py +++ b/backend/app/services/bx/reimburse.py @@ -23,6 +23,7 @@ from app.schemas.bx.reimburse import ReimburseApplyOut, ReimburseDetailOut from app.services.bx.approval_cycle import filter_current_cycle_approvals from app.services.bx.attachment import AttachmentService from app.services.bx.base import BxCrudService +from app.services.bx_data_scope import apply_bx_scope from app.utils.bx_codegen import ( allocate_reimburse_title_seq, generate_apply_code, @@ -42,8 +43,17 @@ class ReimburseService: return get_tenant_no(db, user.to_tenant_user_dict()) @staticmethod - def get_by_id(db: Session, reimburse_id: str, tenant_no: str) -> Optional[BxReimburseApply]: - return BxCrudService.get_by_id(BxReimburseApply, db, reimburse_id, tenant_no) + def get_by_id( + db: Session, + reimburse_id: str, + tenant_no: str, + *, + user: Optional[BxUserContext] = None, + skip_scope: bool = False, + ) -> Optional[BxReimburseApply]: + return BxCrudService.get_by_id( + BxReimburseApply, db, reimburse_id, tenant_no, user=user, skip_scope=skip_scope + ) @staticmethod def get_paged( @@ -52,14 +62,18 @@ class ReimburseService: page: int, page_size: int, filters: Optional[List[Any]] = None, + *, + user: Optional[BxUserContext] = None, ) -> Tuple[Sequence[BxReimburseApply], int]: - return BxCrudService.get_paged(BxReimburseApply, db, tenant_no, page, page_size, filters) + return BxCrudService.get_paged( + BxReimburseApply, db, tenant_no, page, page_size, filters, user=user + ) @staticmethod def soft_delete(db: Session, reimburse_id: str, *, user: BxUserContext) -> bool: tenant_no = ReimburseService._tenant_no(db, user) return BxCrudService.soft_delete( - BxReimburseApply, db, reimburse_id, tenant_no, user_id=user.id + BxReimburseApply, db, reimburse_id, tenant_no, user_id=user.id, user=user ) @staticmethod @@ -170,6 +184,7 @@ class ReimburseService: if bounds: d0, d1 = bounds query = query.filter(BxReimburseApply.create_time >= d0, BxReimburseApply.create_time < d1) + query = apply_bx_scope(query, BxReimburseApply, user, db, tenant_no) total = query.count() rows = ( query.order_by(BxReimburseApply.create_time.desc()) @@ -182,7 +197,7 @@ class ReimburseService: @staticmethod def submit(db: Session, apply_id: str, user: BxUserContext) -> Optional[BxReimburseApply]: tenant_no = ReimburseService._tenant_no(db, user) - apply_obj = ReimburseService.get_by_id(db, apply_id, tenant_no) + apply_obj = ReimburseService.get_by_id(db, apply_id, tenant_no, user=user) if not apply_obj: return None apply_obj.status = "pending" @@ -201,7 +216,7 @@ class ReimburseService: db: Session, apply_id: str, approver: BxUserContext, opinion: str ) -> Optional[BxReimburseApply]: tenant_no = ReimburseService._tenant_no(db, approver) - apply_obj = ReimburseService.get_by_id(db, apply_id, tenant_no) + apply_obj = ReimburseService.get_by_id(db, apply_id, tenant_no, skip_scope=True) if not apply_obj or not apply_obj.wf_id: return None nodes = ReimburseService._workflow_nodes(db, apply_obj.wf_id, tenant_no) @@ -246,7 +261,7 @@ class ReimburseService: db: Session, apply_id: str, approver: BxUserContext, opinion: str ) -> Optional[BxReimburseApply]: tenant_no = ReimburseService._tenant_no(db, approver) - apply_obj = ReimburseService.get_by_id(db, apply_id, tenant_no) + apply_obj = ReimburseService.get_by_id(db, apply_id, tenant_no, skip_scope=True) if not apply_obj or not apply_obj.wf_id: return None nodes = ReimburseService._workflow_nodes(db, apply_obj.wf_id, tenant_no) @@ -321,8 +336,10 @@ class ReimburseService: ) @staticmethod - def get_approvals(db: Session, apply_id: str, tenant_no: str) -> List[Dict[str, Any]]: - apply_obj = ReimburseService.get_by_id(db, apply_id, tenant_no) + def get_approvals( + db: Session, apply_id: str, tenant_no: str, *, user: Optional[BxUserContext] = None + ) -> List[Dict[str, Any]]: + apply_obj = ReimburseService.get_by_id(db, apply_id, tenant_no, user=user) if not apply_obj or not apply_obj.wf_id: return [] wf = BxCrudService.get_by_id(BxWorkflowDef, db, apply_obj.wf_id, tenant_no) @@ -355,9 +372,11 @@ class ReimburseService: return result @staticmethod - def get_approval_history(db: Session, apply_id: str, tenant_no: str) -> List[Dict[str, Any]]: + def get_approval_history( + db: Session, apply_id: str, tenant_no: str, *, user: Optional[BxUserContext] = None + ) -> List[Dict[str, Any]]: """返回全部审批动作,按时间正序排列,用于展示完整审批历史。""" - if not ReimburseService.get_by_id(db, apply_id, tenant_no): + if not ReimburseService.get_by_id(db, apply_id, tenant_no, user=user): return [] approvals = ( db.query(BxReimburseApproval) @@ -445,7 +464,7 @@ class ReimburseService: user: BxUserContext, ) -> Optional[BxReimburseApply]: tenant_no = ReimburseService._tenant_no(db, user) - apply_obj = ReimburseService.get_by_id(db, apply_id, tenant_no) + apply_obj = ReimburseService.get_by_id(db, apply_id, tenant_no, user=user) if not apply_obj: return None @@ -560,26 +579,33 @@ class ReimburseService: return out @staticmethod - def global_status_counts(db: Session, tenant_no: str) -> Dict[str, int]: - rows = ( - db.query(BxReimburseApply.status, func.count()) - .filter(BxReimburseApply.del_flag == 0, BxReimburseApply.tenant_no == tenant_no) - .group_by(BxReimburseApply.status) - .all() + def global_status_counts(db: Session, tenant_no: str, *, user: BxUserContext) -> Dict[str, int]: + query = db.query(BxReimburseApply.status, func.count()).filter( + BxReimburseApply.del_flag == 0, BxReimburseApply.tenant_no == tenant_no ) + query = apply_bx_scope(query, BxReimburseApply, user, db, tenant_no) + rows = query.group_by(BxReimburseApply.status).all() return {str(s or ""): int(c) for s, c in rows} @staticmethod - def my_apply_stats(db: Session, user_id: str, tenant_no: str) -> dict: - base = and_( + def _my_apply_base(db: Session, user: BxUserContext, tenant_no: str): + query = db.query(BxReimburseApply).filter( BxReimburseApply.del_flag == 0, BxReimburseApply.tenant_no == tenant_no, or_( - BxReimburseApply.applicant_id == user_id, - BxReimburseApply.create_by == user_id, + BxReimburseApply.applicant_id == user.id, + BxReimburseApply.create_by == user.id, ), ) - total_count = db.query(func.count()).select_from(BxReimburseApply).filter(base).scalar() or 0 + return apply_bx_scope(query, BxReimburseApply, user, db, tenant_no) + + @staticmethod + def my_apply_stats(db: Session, user: BxUserContext, tenant_no: str) -> dict: + base_q = ReimburseService._my_apply_base(db, user, tenant_no).subquery() + base = BxReimburseApply.id.in_(db.query(base_q.c.id)) + total_count = ( + db.query(func.count()).select_from(BxReimburseApply).filter(base).scalar() or 0 + ) pending = ( db.query(func.count()) .select_from(BxReimburseApply) @@ -671,7 +697,7 @@ class ReimburseService: actual_amount: Optional[float] = None, ) -> Optional[BxReimburseApply]: tenant_no = ReimburseService._tenant_no(db, user) - apply_obj = ReimburseService.get_by_id(db, apply_id, tenant_no) + apply_obj = ReimburseService.get_by_id(db, apply_id, tenant_no, user=user) if not apply_obj: return None apply_obj.status = "processed" @@ -695,7 +721,7 @@ class ReimburseService: remark: str = "", ) -> Optional[BxReimburseApply]: tenant_no = ReimburseService._tenant_no(db, user) - apply_obj = ReimburseService.get_by_id(db, apply_id, tenant_no) + apply_obj = ReimburseService.get_by_id(db, apply_id, tenant_no, user=user) if not apply_obj: return None apply_obj.status = "offset" @@ -726,7 +752,7 @@ class ReimburseService: remark: str = "", ) -> Optional[BxReimburseApply]: tenant_no = ReimburseService._tenant_no(db, user) - apply_obj = ReimburseService.get_by_id(db, apply_id, tenant_no) + apply_obj = ReimburseService.get_by_id(db, apply_id, tenant_no, user=user) if not apply_obj: return None @@ -772,8 +798,13 @@ class ReimburseService: return apply_obj @staticmethod - def finance_payment_stats(db: Session, tenant_no: str) -> dict: - base = and_(BxReimburseApply.del_flag == 0, BxReimburseApply.tenant_no == tenant_no) + def finance_payment_stats(db: Session, tenant_no: str, *, user: BxUserContext) -> dict: + base_q = ( + db.query(BxReimburseApply) + .filter(BxReimburseApply.del_flag == 0, BxReimburseApply.tenant_no == tenant_no) + ) + base_q = apply_bx_scope(base_q, BxReimburseApply, user, db, tenant_no).subquery() + base = BxReimburseApply.id.in_(db.query(base_q.c.id)) pending = ( db.query(func.count()) .select_from(BxReimburseApply) diff --git a/backend/app/services/bx/workflow.py b/backend/app/services/bx/workflow.py index 832a5e9fc..632b6430d 100644 --- a/backend/app/services/bx/workflow.py +++ b/backend/app/services/bx/workflow.py @@ -29,8 +29,17 @@ class WorkflowService: return get_tenant_no(db, user.to_tenant_user_dict()) @staticmethod - def get_by_id(db: Session, workflow_id: str, tenant_no: str) -> Optional[BxWorkflowDef]: - return BxCrudService.get_by_id(BxWorkflowDef, db, workflow_id, tenant_no) + def get_by_id( + db: Session, + workflow_id: str, + tenant_no: str, + *, + user: Optional[BxUserContext] = None, + skip_scope: bool = False, + ) -> Optional[BxWorkflowDef]: + return BxCrudService.get_by_id( + BxWorkflowDef, db, workflow_id, tenant_no, user=user, skip_scope=skip_scope + ) @staticmethod def get_paged( @@ -38,8 +47,12 @@ class WorkflowService: tenant_no: str, page: int, page_size: int, + *, + user: Optional[BxUserContext] = None, ) -> Tuple[Sequence[BxWorkflowDef], int]: - return BxCrudService.get_paged(BxWorkflowDef, db, tenant_no, page, page_size) + return BxCrudService.get_paged( + BxWorkflowDef, db, tenant_no, page, page_size, user=user + ) @staticmethod def create( @@ -74,13 +87,14 @@ class WorkflowService: tenant_no, obj_in, user_id=user.id, + user=user, ) @staticmethod def soft_delete(db: Session, workflow_id: str, *, user: BxUserContext) -> bool: tenant_no = WorkflowService._tenant_no(db, user) deleted = BxCrudService.soft_delete( - BxWorkflowDef, db, workflow_id, tenant_no, user_id=user.id + BxWorkflowDef, db, workflow_id, tenant_no, user_id=user.id, user=user ) if deleted: db.query(BxWorkflowNode).filter( @@ -142,7 +156,7 @@ class WorkflowService: user: BxUserContext, ) -> List[BxWorkflowNode]: tenant_no = WorkflowService._tenant_no(db, user) - workflow = WorkflowService.get_by_id(db, wf_id, tenant_no) + workflow = WorkflowService.get_by_id(db, wf_id, tenant_no, user=user) if not workflow: raise ValueError("工作流不存在") diff --git a/backend/app/services/bx_data_scope.py b/backend/app/services/bx_data_scope.py new file mode 100644 index 000000000..e0b0c4ed8 --- /dev/null +++ b/backend/app/services/bx_data_scope.py @@ -0,0 +1,76 @@ +"""BX 模块数据权限辅助(create_by / create_dept_id)。""" +from __future__ import annotations + +from typing import TYPE_CHECKING +from sqlalchemy.orm import Session + +from app.core.data_scope import apply_data_scope, assert_row_in_data_scope + +if TYPE_CHECKING: + from app.core.bx_auth import BxUserContext + + +def apply_bx_scope(query, model, user: "BxUserContext", db: Session, tenant_no: str): + """列表/统计查询应用数据权限。""" + return apply_data_scope(query, model, user.to_user_info(), db, tenant_no) + + +def assert_bx_row_in_scope( + db: Session, + row, + model, + user: "BxUserContext", + tenant_no: str, +) -> None: + """详情/改/删前校验行级数据权限。""" + assert_row_in_data_scope(db, row, model, user.to_user_info(), tenant_no) + + +def assert_bx_biz_in_scope( + db: Session, + biz_type: str, + biz_id: str, + user: "BxUserContext", + tenant_no: str, +) -> None: + """附件下载/删除/绑定前校验关联业务单在数据权限内。""" + bid = (biz_id or "").strip() + if not bid or bid == "0": + return + btype = (biz_type or "").strip().lower() + if btype in ("advance", "advance_apply", "advance_issue"): + from app.services.bx.advance import AdvanceService + + row = AdvanceService.get_by_id(db, bid, tenant_no, user=user) + if not row: + raise HTTPException(status_code=404, detail="关联备用金申请不存在") + return + if btype in ("reimburse", "reimburse_apply"): + from app.services.bx.reimburse import ReimburseService + + row = ReimburseService.get_by_id(db, bid, tenant_no, user=user) + if not row: + raise HTTPException(status_code=404, detail="关联报销单不存在") + return + if btype == "workflow": + from app.services.bx.workflow import WorkflowService + + row = WorkflowService.get_by_id(db, bid, tenant_no, user=user) + if not row: + raise HTTPException(status_code=404, detail="关联工作流不存在") + + +def assert_bx_attachment_biz_in_scope( + db: Session, + attachment, + user: "BxUserContext", + tenant_no: str, +) -> None: + """按附件关联业务单校验数据权限。""" + assert_bx_biz_in_scope( + db, + getattr(attachment, "biz_type", None) or "", + getattr(attachment, "biz_id", None) or "", + user, + tenant_no, + )