Parcourir la source

feat(crm): 联系人、产品、报价与合同域接入数据权限过滤

补全 PR3 Phase 2.4–2.7,列表 apply_data_scope、详情/CUD assert_row_in_data_scope,合同/报价主表支持 owner_user_id。

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu il y a 4 mois
Parent
commit
bf73b6b2d8

+ 78 - 16
backend/app/api/crm.py

@@ -17,6 +17,8 @@ from app.core.tenant_context import get_tenant_no
 from app.crud.crm.data_scope_helpers import (
     crm_user_info,
     ensure_crm_create_by_row_in_scope,
+    ensure_crm_contact_row_in_scope,
+    ensure_crm_customer_linked_row_in_scope,
     ensure_crm_owner_row_in_scope,
 )
 from app.utils.crm_export_columns import (
@@ -329,6 +331,8 @@ async def category_tree(
     """获取产品分类树"""
     try:
         tenant_no = get_tenant_no(db, _user)
+        user_info = crm_user_info(_user)
+        user_info = crm_user_info(_user)
         payload = body or {}
         category_name = payload.get("categoryName") or payload.get("category_name")
         status = payload.get("status")
@@ -337,6 +341,7 @@ async def category_tree(
             tenant_no,
             category_name=category_name,
             status=status,
+            user_info=user_info,
         )
         return success_response(rows)
     except ValueError as e:
@@ -516,6 +521,7 @@ async def category_add(
     """新增产品分类"""
     try:
         tenant_no = get_tenant_no(db, _user)
+        user_info = crm_user_info(_user)
         user_id = _user.get("user_id", "") or _user.get("id", "")
         data = body.model_dump(exclude_unset=True)
         obj = create_category(db, tenant_no=tenant_no, user_id=user_id, **data)
@@ -537,7 +543,7 @@ async def category_update(
         user_id = _user.get("user_id", "") or _user.get("id", "")
         data = body.model_dump(exclude_unset=True)
         category_id = data.pop("id")
-        obj = update_category(db, category_id, user_id=user_id, **data)
+        obj = update_category(db, category_id, user_id=user_id, user_info=user_info, **data)
         return success_response(_category_row_to_dict(obj), message="更新成功")
     except ValueError as e:
         return error_response(400, str(e))
@@ -553,7 +559,7 @@ async def category_delete(
 ):
     """删除产品分类"""
     try:
-        soft_delete_category(db, body.id)
+        soft_delete_category(db, body.id, user_info=user_info)
         return success_response(None, message="删除成功")
     except ValueError as e:
         return error_response(400, str(e))
@@ -573,6 +579,7 @@ async def product_list(
     """分页查询产品列表"""
     try:
         tenant_no = get_tenant_no(db, _user)
+        user_info = crm_user_info(_user)
         rows, total = list_products(
             db,
             tenant_no,
@@ -581,6 +588,7 @@ async def product_list(
             keyword=body.keyword,
             category_id=body.category_id,
             status=body.status,
+            user_info=user_info,
         )
         tp = total_pages(total, body.pageSize)
         return success_response(
@@ -606,9 +614,12 @@ async def product_detail(
 ):
     """获取产品详情"""
     try:
+        tenant_no = get_tenant_no(db, _user)
+        user_info = crm_user_info(_user)
         obj = get_product(db, body.id)
         if not obj:
             return error_response(404, "产品不存在")
+        ensure_crm_create_by_row_in_scope(db, obj, user_info, tenant_no, message="无权访问该产品")
         return success_response(_product_row_to_dict(obj))
     except ValueError as e:
         return error_response(400, str(e))
@@ -625,6 +636,7 @@ async def product_add(
     """新增产品"""
     try:
         tenant_no = get_tenant_no(db, _user)
+        user_info = crm_user_info(_user)
         user_id = _user.get("user_id", "") or _user.get("id", "")
         obj = create_product(
             db,
@@ -650,7 +662,9 @@ async def product_update(
         user_id = _user.get("user_id", "") or _user.get("id", "")
         data = body.model_dump(exclude_unset=True)
         product_id = data.pop("id")
-        obj = update_product(db, product_id, user_id=user_id, **data)
+        tenant_no = get_tenant_no(db, _user)
+        user_info = crm_user_info(_user)
+        obj = update_product(db, product_id, user_id=user_id, user_info=user_info, **data)
         return success_response(_product_row_to_dict(obj), message="更新成功")
     except ValueError as e:
         return error_response(400, str(e))
@@ -666,6 +680,8 @@ async def product_delete(
 ):
     """删除产品(支持单条 body.id 和批量 body.ids)"""
     try:
+        tenant_no = get_tenant_no(db, _user)
+        user_info = crm_user_info(_user)
         ids_to_delete: List[str] = []
         if body.ids:
             ids_to_delete = body.ids
@@ -675,7 +691,7 @@ async def product_delete(
             return error_response(400, "请传入 id 或 ids")
 
         for pid in ids_to_delete:
-            soft_delete_product(db, pid)
+            soft_delete_product(db, pid, user_info=user_info)
 
         return success_response(None, message="删除成功")
     except ValueError as e:
@@ -692,10 +708,12 @@ async def product_update_status(
 ):
     """批量上架/下架"""
     try:
+        tenant_no = get_tenant_no(db, _user)
+        user_info = crm_user_info(_user)
         user_id = _user.get("user_id", "") or _user.get("id", "")
         status_label = "上架" if body.status == "on" else "下架"
         for pid in body.ids:
-            update_product(db, pid, user_id=user_id, status=body.status)
+            update_product(db, pid, user_id=user_id, user_info=user_info, status=body.status)
         return success_response(None, message=f"批量{status_label}成功")
     except ValueError as e:
         return error_response(400, str(e))
@@ -712,11 +730,13 @@ async def product_options(
     """获取产品下拉选项"""
     try:
         tenant_no = get_tenant_no(db, _user)
+        user_info = crm_user_info(_user)
         options = list_product_options(
             db,
             tenant_no,
             keyword=body.keyword,
             category_id=body.category_id,
+            user_info=user_info,
         )
         return success_response(options)
     except ValueError as e:
@@ -739,6 +759,7 @@ async def product_import(
         from openpyxl import load_workbook
 
         tenant_no = get_tenant_no(db, _user)
+        user_info = crm_user_info(_user)
         user_id = _user.get("user_id", "") or _user.get("id", "")
 
         content = await file.read()
@@ -833,6 +854,7 @@ async def product_export(
     """Excel 导出产品列表"""
     try:
         tenant_no = get_tenant_no(db, _user)
+        user_info = crm_user_info(_user)
         rows, _ = list_products(
             db,
             tenant_no,
@@ -1424,10 +1446,11 @@ async def contact_list(
     """联系人分页列表"""
     try:
         tenant_no = get_tenant_no(db, _user)
+        user_info = crm_user_info(_user)
         page = body.page if body else 1
         page_size = body.pageSize if body else 10
         rows, total = list_contacts(
-            db, tenant_no, page=page, page_size=page_size,
+            db, tenant_no, page=page, page_size=page_size, user_info=user_info,
             keyword=body.keyword if body else None,
             contact_name=body.contact_name if body else None,
             mobile=body.mobile if body else None,
@@ -1461,9 +1484,12 @@ async def contact_detail(
         contact_id = body.get("id")
         if not contact_id:
             return error_response(400, "缺少联系人ID")
+        tenant_no = get_tenant_no(db, _user)
+        user_info = crm_user_info(_user)
         row = get_contact(db, contact_id)
         if not row:
             return error_response(404, "联系人不存在")
+        ensure_crm_contact_row_in_scope(db, row, user_info, tenant_no, message="无权访问该联系人")
         return success_response(contact_row_to_dict(row))
     except ValueError as e:
         return error_response(400, str(e))
@@ -1480,10 +1506,11 @@ async def contact_by_customer(
     """按客户ID查询联系人"""
     try:
         tenant_no = get_tenant_no(db, _user)
+        user_info = crm_user_info(_user)
         customer_id = body.get("customerId", "")
         if not customer_id:
             return error_response(400, "缺少客户ID")
-        rows = list_contacts_by_customer(db, tenant_no, customer_id)
+        rows = list_contacts_by_customer(db, tenant_no, customer_id, user_info=user_info)
         return success_response(rows)
     except ValueError as e:
         return error_response(400, str(e))
@@ -1500,9 +1527,11 @@ async def contact_add(
     """新增联系人"""
     try:
         tenant_no = get_tenant_no(db, _user)
+        user_info = crm_user_info(_user)
         user_id = _user.get("user_id", "") or _user.get("id", "")
+        user_info = crm_user_info(_user)
         obj = create_contact(
-            db, tenant_no=tenant_no, user_id=user_id,
+            db, tenant_no=tenant_no, user_id=user_id, user_info=user_info,
             **body.model_dump(exclude_unset=True),
         )
         return success_response(contact_row_to_dict(obj), message="创建成功")
@@ -1523,7 +1552,9 @@ async def contact_update(
         user_id = _user.get("user_id", "") or _user.get("id", "")
         data = body.model_dump(exclude_unset=True)
         contact_id = data.pop("id")
-        obj = update_contact(db, contact_id, user_id=user_id, **data)
+        tenant_no = get_tenant_no(db, _user)
+        user_info = crm_user_info(_user)
+        obj = update_contact(db, contact_id, user_id=user_id, tenant_no=tenant_no, user_info=user_info, **data)
         return success_response(contact_row_to_dict(obj), message="更新成功")
     except ValueError as e:
         return error_response(400, str(e))
@@ -1539,8 +1570,11 @@ async def contact_delete(
 ):
     """删除联系人"""
     try:
+        tenant_no = get_tenant_no(db, _user)
+        user_info = crm_user_info(_user)
+        user_id = _user.get("user_id", "") or _user.get("id", "")
         for cid in body.ids:
-            soft_delete_contact(db, cid)
+            soft_delete_contact(db, cid, user_id=user_id, tenant_no=tenant_no, user_info=user_info)
         return success_response(None, message="删除成功")
     except ValueError as e:
         return error_response(400, str(e))
@@ -1560,7 +1594,9 @@ async def contact_set_primary(
         if not contact_id:
             return error_response(400, "缺少联系人ID")
         user_id = _user.get("user_id", "") or _user.get("id", "")
-        obj = set_primary_contact(db, contact_id, user_id=user_id)
+        tenant_no = get_tenant_no(db, _user)
+        user_info = crm_user_info(_user)
+        obj = set_primary_contact(db, contact_id, user_id=user_id, tenant_no=tenant_no, user_info=user_info)
         return success_response(contact_row_to_dict(obj), message="设置成功")
     except ValueError as e:
         return error_response(400, str(e))
@@ -1577,8 +1613,9 @@ async def contact_export(
     """导出联系人列表"""
     try:
         tenant_no = get_tenant_no(db, _user)
+        user_info = crm_user_info(_user)
         rows, _ = list_contacts(
-            db, tenant_no, page=1, page_size=10000,
+            db, tenant_no, page=1, page_size=10000, user_info=user_info,
             keyword=body.keyword if body else None,
             customer_id=body.customer_id if body else None,
         )
@@ -1606,10 +1643,11 @@ async def contact_decision(
     """决策人联系人列表"""
     try:
         tenant_no = get_tenant_no(db, _user)
+        user_info = crm_user_info(_user)
         page = body.page if body else 1
         page_size = body.pageSize if body else 10
         rows, total = list_contacts(
-            db, tenant_no, page=page, page_size=page_size,
+            db, tenant_no, page=page, page_size=page_size, user_info=user_info,
             is_decision=True,
             keyword=body.keyword if body else None,
         )
@@ -1636,10 +1674,11 @@ async def contact_primary(
     """主联系人列表"""
     try:
         tenant_no = get_tenant_no(db, _user)
+        user_info = crm_user_info(_user)
         page = body.page if body else 1
         page_size = body.pageSize if body else 10
         rows, total = list_contacts(
-            db, tenant_no, page=page, page_size=page_size,
+            db, tenant_no, page=page, page_size=page_size, user_info=user_info,
             is_primary=True,
             keyword=body.keyword if body else None,
         )
@@ -1666,10 +1705,11 @@ async def contact_birthday(
     """本月生日联系人列表"""
     try:
         tenant_no = get_tenant_no(db, _user)
+        user_info = crm_user_info(_user)
         page = body.page if body else 1
         page_size = body.pageSize if body else 10
         rows, total = list_contacts(
-            db, tenant_no, page=page, page_size=page_size,
+            db, tenant_no, page=page, page_size=page_size, user_info=user_info,
             birthday_month=datetime.now().month,
             keyword=body.keyword if body else None,
         )
@@ -3044,10 +3084,12 @@ async def supplier_info_list(
     """分页查询供方列表 — Java: CrmSupplierInfoController.list"""
     try:
         tenant_no = get_tenant_no(db, _user)
+        user_info = crm_user_info(_user)
         list_body = body or SupplierInfoListBody()
         rows, total = list_supplier_info(
             db,
             tenant_no,
+            user_info=user_info,
             page=list_body.page,
             page_size=list_body.pageSize,
             supplier_name=list_body.supplier_name,
@@ -3077,10 +3119,12 @@ async def supplier_info_list_all(
     """供方全量列表(下拉)— Java: CrmSupplierInfoController.listAll"""
     try:
         tenant_no = get_tenant_no(db, _user)
+        user_info = crm_user_info(_user)
         list_body = body or SupplierInfoListAllBody()
         rows = list_all_supplier_info(
             db,
             tenant_no,
+            user_info=user_info,
             supplier_name=list_body.supplier_name,
             status=list_body.status,
         )
@@ -3100,9 +3144,13 @@ async def supplier_info_detail(
     """供方详情 — Java: CrmSupplierInfoController.getInfo"""
     try:
         tenant_no = get_tenant_no(db, _user)
+        user_info = crm_user_info(_user)
         obj = get_supplier_info(db, body.id, tenant_no=tenant_no)
         if not obj:
             return error_response(404, "供方信息不存在")
+        ensure_crm_create_by_row_in_scope(
+            db, obj, user_info, tenant_no, message="无权访问该供方信息"
+        )
         return success_response(supplier_info_row_to_dict(obj))
     except ValueError as e:
         return error_response(400, str(e))
@@ -3119,6 +3167,7 @@ async def supplier_info_add(
     """新增供方 — Java: CrmSupplierInfoController.add"""
     try:
         tenant_no = get_tenant_no(db, _user)
+        user_info = crm_user_info(_user)
         user_id = _user.get("user_id", "") or _user.get("id", "")
         dept_id = _user.get("dept_id", "") or _user.get("create_dept_id", "") or ""
         obj = create_supplier_info(
@@ -3144,6 +3193,7 @@ async def supplier_info_update(
     """修改供方 — Java: CrmSupplierInfoController.edit"""
     try:
         tenant_no = get_tenant_no(db, _user)
+        user_info = crm_user_info(_user)
         user_id = _user.get("user_id", "") or _user.get("id", "")
         data = body.model_dump(exclude_unset=True)
         supplier_id = data.pop("id")
@@ -3152,6 +3202,7 @@ async def supplier_info_update(
             supplier_id,
             tenant_no=tenant_no,
             user_id=user_id,
+            user_info=user_info,
             **data,
         )
         return success_response(supplier_info_row_to_dict(obj), message="修改成功")
@@ -3170,6 +3221,7 @@ async def supplier_info_delete(
     """删除供方 — Java: CrmSupplierInfoController.remove"""
     try:
         tenant_no = get_tenant_no(db, _user)
+        user_info = crm_user_info(_user)
         user_id = _user.get("user_id", "") or _user.get("id", "")
         ids_to_delete: List[str] = []
         if body.ids:
@@ -3198,10 +3250,12 @@ async def supplier_info_export(
     """导出供方列表 — Java: CrmSupplierInfoController.export"""
     try:
         tenant_no = get_tenant_no(db, _user)
+        user_info = crm_user_info(_user)
         list_body = body or SupplierInfoListBody()
         rows, _ = list_supplier_info(
             db,
             tenant_no,
+            user_info=user_info,
             page=1,
             page_size=10000,
             supplier_name=list_body.supplier_name,
@@ -3231,7 +3285,10 @@ async def _invoice_info_by_customer_impl(
     db: Session,
 ):
     tenant_no = get_tenant_no(db, _user)
-    rows = list_invoice_info_by_customer(db, tenant_no, body.customer_id)
+    user_info = crm_user_info(_user)
+    rows = list_invoice_info_by_customer(
+        db, tenant_no, body.customer_id, user_info=user_info
+    )
     return success_response(rows)
 
 
@@ -3274,11 +3331,13 @@ async def invoice_info_add(
     """新增开票信息"""
     try:
         tenant_no = get_tenant_no(db, _user)
+        user_info = crm_user_info(_user)
         user_id = _user.get("user_id", "") or _user.get("id", "")
         obj = create_invoice_info(
             db,
             tenant_no=tenant_no,
             user_id=user_id,
+            user_info=user_info,
             **body.model_dump(exclude_unset=True),
         )
         return success_response(invoice_info_row_to_dict(obj), message="创建成功")
@@ -3297,6 +3356,7 @@ async def invoice_info_update(
     """更新开票信息"""
     try:
         tenant_no = get_tenant_no(db, _user)
+        user_info = crm_user_info(_user)
         user_id = _user.get("user_id", "") or _user.get("id", "")
         data = body.model_dump(exclude_unset=True)
         invoice_id = data.pop("id")
@@ -3305,6 +3365,7 @@ async def invoice_info_update(
             invoice_id,
             tenant_no=tenant_no,
             user_id=user_id,
+            user_info=user_info,
             **data,
         )
         return success_response(invoice_info_row_to_dict(obj), message="更新成功")
@@ -3346,6 +3407,7 @@ async def invoice_info_set_default(
     """设置默认开票信息"""
     try:
         tenant_no = get_tenant_no(db, _user)
+        user_info = crm_user_info(_user)
         user_id = _user.get("user_id", "") or _user.get("id", "")
         obj = set_default_invoice_info(
             db,

+ 45 - 1
backend/app/api/crm_contract_routes.py

@@ -11,6 +11,7 @@ from app.core.database import get_db
 from app.core.menu_guard import require_admin_or_super_admin
 from app.core.response import error_response, success_response
 from app.core.tenant_context import get_tenant_no
+from app.crud.crm.data_scope_helpers import crm_user_info
 from app.crud.crm.contract import (
     batch_assign_owner,
     batch_submit_contract_approval,
@@ -99,6 +100,7 @@ def register_contract_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             user_id = _user.get("user_id", "") or _user.get("id", "")
             query = ContractListQuery(
                 pageNum=pageNum,
@@ -131,6 +133,7 @@ def register_contract_routes(router):
             rows, total = list_contracts(
                 db,
                 tenant_no,
+                user_info=user_info,
                 page=query.pageNum,
                 page_size=query.pageSize,
                 filters=filters,
@@ -160,6 +163,7 @@ def register_contract_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             user_id = _user.get("user_id", "") or _user.get("id", "")
             dept_id = _user.get("dept_id", "") or _user.get("create_dept_id", "") or ""
             detail = create_contract(
@@ -183,6 +187,7 @@ def register_contract_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             user_id = _user.get("user_id", "") or _user.get("id", "")
             dept_id = _user.get("dept_id", "") or _user.get("create_dept_id", "") or ""
             data = body.model_dump(by_alias=True)
@@ -194,6 +199,7 @@ def register_contract_routes(router):
                 data=data,
                 user_id=user_id,
                 dept_id=dept_id,
+                user_info=user_info,
             )
             return success_response(detail, message="修改成功")
         except ValueError as e:
@@ -209,6 +215,7 @@ def register_contract_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             id_list = [x.strip() for x in ids.split(",") if x.strip()]
             soft_delete_contracts(db, id_list, tenant_no=tenant_no)
             return success_response(None, message="删除成功")
@@ -225,6 +232,7 @@ def register_contract_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             user_id = _user.get("user_id", "") or _user.get("id", "")
             dept_id = _user.get("dept_id", "") or _user.get("create_dept_id", "") or ""
             submit_contract_approval(
@@ -235,6 +243,7 @@ def register_contract_routes(router):
                 remark=body.remark,
                 user_id=user_id,
                 dept_id=dept_id,
+                user_info=user_info,
             )
             return success_response(None, message="提交成功")
         except ValueError as e:
@@ -250,6 +259,7 @@ def register_contract_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             user_id = _user.get("user_id", "") or _user.get("id", "")
             dept_id = _user.get("dept_id", "") or _user.get("create_dept_id", "") or ""
             count = batch_submit_contract_approval(
@@ -260,6 +270,7 @@ def register_contract_routes(router):
                 remark=body.remark,
                 user_id=user_id,
                 dept_id=dept_id,
+                user_info=user_info,
             )
             return success_response(count, message="批量提交完成")
         except ValueError as e:
@@ -275,6 +286,7 @@ def register_contract_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             user_id = _user.get("user_id", "") or _user.get("id", "")
             withdraw_contract_approval(
                 db,
@@ -282,6 +294,7 @@ def register_contract_routes(router):
                 tenant_no=tenant_no,
                 remark=body.remark,
                 user_id=user_id,
+                user_info=user_info,
             )
             return success_response(None, message="撤回成功")
         except ValueError as e:
@@ -297,6 +310,7 @@ def register_contract_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             user_id = _user.get("user_id", "") or _user.get("id", "")
             start_execute(db, body.id, tenant_no=tenant_no, remark=body.remark, user_id=user_id)
             return success_response(None, message="操作成功")
@@ -313,6 +327,7 @@ def register_contract_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             user_id = _user.get("user_id", "") or _user.get("id", "")
             finish_contract(db, body.id, tenant_no=tenant_no, remark=body.remark, user_id=user_id)
             return success_response(None, message="操作成功")
@@ -329,6 +344,7 @@ def register_contract_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             user_id = _user.get("user_id", "") or _user.get("id", "")
             invalid_contract(
                 db,
@@ -337,6 +353,7 @@ def register_contract_routes(router):
                 invalid_reason=body.invalidReason,
                 remark=body.remark,
                 user_id=user_id,
+                user_info=user_info,
             )
             return success_response(None, message="作废成功")
         except ValueError as e:
@@ -352,12 +369,14 @@ def register_contract_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             save_template(
                 db,
                 body.id,
                 body.templateContent,
                 tenant_no=tenant_no,
                 settlement_clause_drafts=body.settlementClauseDrafts,
+                user_info=user_info,
             )
             return success_response(None, message="保存成功")
         except ValueError as e:
@@ -373,6 +392,7 @@ def register_contract_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             content = get_template(db, contract_id, tenant_no=tenant_no)
             return success_response(content or "", message="获取模板成功")
         except ValueError as e:
@@ -388,6 +408,7 @@ def register_contract_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             upload_file(db, body.id, body.fileUrl, tenant_no=tenant_no)
             return success_response(None, message="上传成功")
         except ValueError as e:
@@ -403,9 +424,11 @@ def register_contract_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             if body.attachments:
                 upload_attachments_with_category(
-                    db, body.id, body.attachments, tenant_no=tenant_no
+                    db, body.id, body.attachments, tenant_no=tenant_no,
+                user_info=user_info,
                 )
             elif body.ossIds:
                 upload_attachments(db, body.id, body.ossIds, tenant_no=tenant_no)
@@ -425,6 +448,7 @@ def register_contract_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             list_body = body or ContractExportBody()
             filters = list_body.model_dump(
                 by_alias=True,
@@ -456,6 +480,7 @@ def register_contract_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             req = ContractStatisticsOverviewQuery(
                 ownerUserId=ownerUserId,
                 beginTime=beginTime,
@@ -467,6 +492,7 @@ def register_contract_routes(router):
                 owner_user_id=req.ownerUserId,
                 begin_time=req.beginTime,
                 end_time=req.endTime,
+                user_info=user_info,
             )
             return success_response(data)
         except ValueError as e:
@@ -481,6 +507,7 @@ def register_contract_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             return success_response(statistics_by_status(db, tenant_no))
         except ValueError as e:
             return error_response(400, str(e))
@@ -494,6 +521,7 @@ def register_contract_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             return success_response(statistics_by_type(db, tenant_no))
         except ValueError as e:
             return error_response(400, str(e))
@@ -510,6 +538,7 @@ def register_contract_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             req = ContractStatisticsTrendQuery(type=type, beginTime=beginTime, endTime=endTime)
             data = statistics_trend(
                 db,
@@ -517,6 +546,7 @@ def register_contract_routes(router):
                 period_type=req.type or "month",
                 begin_time=req.beginTime,
                 end_time=req.endTime,
+                user_info=user_info,
             )
             return success_response(data)
         except ValueError as e:
@@ -532,6 +562,7 @@ def register_contract_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             rows = query_expiring(db, tenant_no, days=days)
             return success_response(rows)
         except ValueError as e:
@@ -548,6 +579,7 @@ def register_contract_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             user_id = _user.get("user_id", "") or _user.get("id", "")
             dept_id = _user.get("dept_id", "") or _user.get("create_dept_id", "") or ""
             new_id = renew_contract(
@@ -573,6 +605,7 @@ def register_contract_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             user_id = _user.get("user_id", "") or _user.get("id", "")
             dept_id = _user.get("dept_id", "") or _user.get("create_dept_id", "") or ""
             contract_id = create_from_quote(
@@ -598,6 +631,7 @@ def register_contract_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             user_id = _user.get("user_id", "") or _user.get("id", "")
             dept_id = _user.get("dept_id", "") or _user.get("create_dept_id", "") or ""
             contract_id = create_from_opportunity(
@@ -622,6 +656,7 @@ def register_contract_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             user_id = _user.get("user_id", "") or _user.get("id", "")
             sign_contract(
                 db,
@@ -631,6 +666,7 @@ def register_contract_routes(router):
                 contract_file=body.contractFile,
                 remark=body.remark or "",
                 user_id=user_id,
+                user_info=user_info,
             )
             return success_response(None, message="签订成功")
         except ValueError as e:
@@ -646,6 +682,7 @@ def register_contract_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             user_id = _user.get("user_id", "") or _user.get("id", "")
             count = batch_assign_owner(
                 db,
@@ -654,6 +691,7 @@ def register_contract_routes(router):
                 owner_user_id=body.ownerUserId,
                 remark=body.remark or "",
                 user_id=user_id,
+                user_info=user_info,
             )
             return success_response(count, message="分配成功")
         except ValueError as e:
@@ -669,6 +707,7 @@ def register_contract_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             file_path = download_contract_pdf(db, contract_id, tenant_no=tenant_no)
             return success_response(file_path)
         except ValueError as e:
@@ -684,6 +723,7 @@ def register_contract_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             user_id = _user.get("user_id", "") or _user.get("id", "")
             approve_contract_by_contract_id(
                 db,
@@ -693,6 +733,7 @@ def register_contract_routes(router):
                 attachments=body.attachments or "",
                 remark=body.remark or "",
                 user_id=user_id,
+                user_info=user_info,
             )
             db.commit()
             return success_response(None, message="审批成功")
@@ -709,6 +750,7 @@ def register_contract_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             rows = list_contract_approvals(db, contract_id, tenant_no=tenant_no)
             return success_response(rows)
         except ValueError as e:
@@ -724,12 +766,14 @@ def register_contract_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             user_id = _user.get("user_id", "") or _user.get("id", "")
             detail = get_contract_detail(
                 db,
                 contract_id,
                 tenant_no=tenant_no,
                 current_user_id=user_id,
+                user_info=user_info,
             )
             return success_response(detail)
         except ValueError as e:

+ 28 - 3
backend/app/api/crm_contract_sub_routes.py

@@ -11,6 +11,7 @@ from app.core.database import get_db
 from app.core.menu_guard import require_admin_or_super_admin
 from app.core.response import error_response, success_response
 from app.core.tenant_context import get_tenant_no
+from app.crud.crm.data_scope_helpers import crm_user_info
 from app.crud.crm.contract import (
     create_contract_change,
     create_contract_template,
@@ -82,6 +83,7 @@ def register_contract_sub_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             query = ContractChangeListQuery(
                 pageNum=pageNum,
                 pageSize=pageSize,
@@ -96,6 +98,7 @@ def register_contract_sub_routes(router):
             rows, total = list_contract_changes(
                 db,
                 tenant_no,
+                user_info=user_info,
                 page=query.pageNum,
                 page_size=query.pageSize,
                 filters=query.model_dump(by_alias=True, exclude={"pageNum", "pageSize"}, exclude_none=True),
@@ -123,7 +126,8 @@ def register_contract_sub_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
-            detail = get_contract_change_detail(db, change_id, tenant_no=tenant_no)
+            user_info = crm_user_info(_user)
+            detail = get_contract_change_detail(db, change_id, tenant_no=tenant_no, user_info=user_info)
             return success_response(detail)
         except ValueError as e:
             return error_response(400, str(e))
@@ -138,6 +142,7 @@ def register_contract_sub_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             user_id = _user.get("user_id", "") or _user.get("id", "")
             dept_id = _user.get("dept_id", "") or _user.get("create_dept_id", "") or ""
             detail = create_contract_change(
@@ -161,6 +166,7 @@ def register_contract_sub_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             user_id = _user.get("user_id", "") or _user.get("id", "")
             data = body.model_dump(by_alias=True)
             change_id = data.pop("id")
@@ -170,6 +176,7 @@ def register_contract_sub_routes(router):
                 tenant_no=tenant_no,
                 data=data,
                 user_id=user_id,
+                user_info=user_info,
             )
             return success_response(detail, message="修改成功")
         except ValueError as e:
@@ -185,8 +192,9 @@ def register_contract_sub_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             id_list = [x.strip() for x in ids.split(",") if x.strip()]
-            soft_delete_contract_changes(db, id_list, tenant_no=tenant_no)
+            soft_delete_contract_changes(db, id_list, tenant_no=tenant_no, user_info=user_info)
             return success_response(None, message="删除成功")
         except ValueError as e:
             return error_response(400, str(e))
@@ -201,6 +209,7 @@ def register_contract_sub_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             user_id = _user.get("user_id", "") or _user.get("id", "")
             submit_change_approval(
                 db,
@@ -224,6 +233,7 @@ def register_contract_sub_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             user_id = _user.get("user_id", "") or _user.get("id", "")
             withdraw_change(db, change_id, tenant_no=tenant_no, user_id=user_id)
             return success_response(None, message="撤回成功")
@@ -240,6 +250,7 @@ def register_contract_sub_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             user_id = _user.get("user_id", "") or _user.get("id", "")
             execute_change(db, change_id, tenant_no=tenant_no, user_id=user_id)
             return success_response(None, message="执行成功")
@@ -256,6 +267,7 @@ def register_contract_sub_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             list_body = body or ContractChangeExportBody()
             filters = list_body.model_dump(
                 by_alias=True,
@@ -293,6 +305,7 @@ def register_contract_sub_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             query = ContractTemplateListQuery(
                 pageNum=pageNum,
                 pageSize=pageSize,
@@ -306,6 +319,7 @@ def register_contract_sub_routes(router):
             rows, total = list_contract_templates(
                 db,
                 tenant_no,
+                user_info=user_info,
                 page=query.pageNum,
                 page_size=query.pageSize,
                 filters=query.model_dump(by_alias=True, exclude={"pageNum", "pageSize"}, exclude_none=True),
@@ -333,6 +347,7 @@ def register_contract_sub_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             detail = get_contract_template_detail(db, template_id, tenant_no=tenant_no)
             return success_response(detail)
         except ValueError as e:
@@ -348,6 +363,7 @@ def register_contract_sub_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             user_id = _user.get("user_id", "") or _user.get("id", "")
             dept_id = _user.get("dept_id", "") or _user.get("create_dept_id", "") or ""
             detail = create_contract_template(
@@ -371,6 +387,7 @@ def register_contract_sub_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             user_id = _user.get("user_id", "") or _user.get("id", "")
             data = body.model_dump(by_alias=True)
             template_id = data.pop("id")
@@ -380,6 +397,7 @@ def register_contract_sub_routes(router):
                 tenant_no=tenant_no,
                 data=data,
                 user_id=user_id,
+                user_info=user_info,
             )
             return success_response(detail, message="修改成功")
         except ValueError as e:
@@ -395,8 +413,9 @@ def register_contract_sub_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             id_list = [x.strip() for x in ids.split(",") if x.strip()]
-            soft_delete_contract_templates(db, id_list, tenant_no=tenant_no)
+            soft_delete_contract_templates(db, id_list, tenant_no=tenant_no, user_info=user_info)
             return success_response(None, message="删除成功")
         except ValueError as e:
             return error_response(400, str(e))
@@ -411,6 +430,7 @@ def register_contract_sub_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             user_id = _user.get("user_id", "") or _user.get("id", "")
             toggle_contract_template_active(
                 db, template_id, tenant_no=tenant_no, is_active=1, user_id=user_id
@@ -429,6 +449,7 @@ def register_contract_sub_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             user_id = _user.get("user_id", "") or _user.get("id", "")
             toggle_contract_template_active(
                 db, template_id, tenant_no=tenant_no, is_active=0, user_id=user_id
@@ -447,6 +468,7 @@ def register_contract_sub_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             list_body = body or ContractTemplateExportBody()
             filters = list_body.model_dump(
                 by_alias=True,
@@ -485,6 +507,7 @@ def register_contract_sub_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             query = ContractApprovalListQuery(
                 pageNum=pageNum,
                 pageSize=pageSize,
@@ -526,6 +549,7 @@ def register_contract_sub_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             detail = get_contract_approval_detail(db, approval_id, tenant_no=tenant_no)
             return success_response(detail)
         except ValueError as e:
@@ -595,6 +619,7 @@ def register_contract_sub_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             list_body = body or ContractApprovalExportBody()
             filters = list_body.model_dump(
                 by_alias=True,

+ 44 - 1
backend/app/api/crm_quote_routes.py

@@ -10,6 +10,7 @@ from app.core.database import get_db
 from app.core.menu_guard import require_admin_or_super_admin
 from app.core.response import error_response, success_response
 from app.core.tenant_context import get_tenant_no
+from app.crud.crm.data_scope_helpers import crm_user_info
 from app.crud.crm.quote import (
     accept_quote,
     create_new_version,
@@ -88,11 +89,13 @@ def register_quote_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             list_body = body or QuoteListBody()
             user_id = _user.get("user_id", "") or _user.get("id", "")
             rows, total = list_quotes(
                 db,
                 tenant_no,
+                user_info=user_info,
                 page=list_body.page,
                 page_size=list_body.pageSize,
                 filters=list_body.model_dump(by_alias=True, exclude={"page", "pageSize"}),
@@ -122,7 +125,8 @@ def register_quote_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
-            detail = get_quote_detail(db, body.id, tenant_no=tenant_no)
+            user_info = crm_user_info(_user)
+            detail = get_quote_detail(db, body.id, tenant_no=tenant_no, user_info=user_info)
             return success_response(detail)
         except ValueError as e:
             return error_response(400, str(e))
@@ -137,6 +141,7 @@ def register_quote_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             user_id = _user.get("user_id", "") or _user.get("id", "")
             dept_id = _user.get("dept_id", "") or _user.get("create_dept_id", "") or ""
             detail = create_quote(
@@ -160,6 +165,7 @@ def register_quote_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             user_id = _user.get("user_id", "") or _user.get("id", "")
             dept_id = _user.get("dept_id", "") or _user.get("create_dept_id", "") or ""
             data = body.model_dump(by_alias=True)
@@ -171,6 +177,7 @@ def register_quote_routes(router):
                 data=data,
                 user_id=user_id,
                 dept_id=dept_id,
+                user_info=user_info,
             )
             return success_response(detail, message="修改成功")
         except ValueError as e:
@@ -186,6 +193,7 @@ def register_quote_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             soft_delete_quotes(db, body.ids, tenant_no=tenant_no)
             return success_response(None, message="删除成功")
         except ValueError as e:
@@ -201,12 +209,14 @@ def register_quote_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             submit_quote(
                 db,
                 body.id,
                 tenant_no=tenant_no,
                 approver_ids=body.approverIds,
                 remark=body.remark,
+                user_info=user_info,
             )
             return success_response(None, message="提交成功")
         except ValueError as e:
@@ -222,6 +232,7 @@ def register_quote_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             save_template(db, body.id, body.templateContent, tenant_no=tenant_no)
             return success_response(None, message="保存成功")
         except ValueError as e:
@@ -237,6 +248,7 @@ def register_quote_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             content = get_template(db, body.id, tenant_no=tenant_no)
             return success_response(content or "")
         except ValueError as e:
@@ -252,6 +264,7 @@ def register_quote_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             pdf_url = generate_pdf(db, body.id, tenant_no=tenant_no)
             return success_response(pdf_url)
         except ValueError as e:
@@ -267,12 +280,14 @@ def register_quote_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             link = generate_share(
                 db,
                 body.id,
                 tenant_no=tenant_no,
                 validity_days=body.validityDays,
                 password=body.password or "",
+                user_info=user_info,
             )
             return success_response(link)
         except ValueError as e:
@@ -288,6 +303,7 @@ def register_quote_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             accept_quote(db, body.id, tenant_no=tenant_no, remark=body.remark)
             return success_response(None, message="操作成功")
         except ValueError as e:
@@ -303,12 +319,14 @@ def register_quote_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             reject_quote(
                 db,
                 body.id,
                 tenant_no=tenant_no,
                 reject_reason=body.rejectReason,
                 remark=body.remark,
+                user_info=user_info,
             )
             return success_response(None, message="操作成功")
         except ValueError as e:
@@ -324,6 +342,7 @@ def register_quote_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             user_id = _user.get("user_id", "") or _user.get("id", "")
             dept_id = _user.get("dept_id", "") or _user.get("create_dept_id", "") or ""
             contract_id = to_contract(
@@ -337,6 +356,7 @@ def register_quote_routes(router):
                 end_date=body.endDate,
                 user_id=user_id,
                 dept_id=dept_id,
+                user_info=user_info,
             )
             return success_response(contract_id)
         except ValueError as e:
@@ -352,6 +372,7 @@ def register_quote_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             user_id = _user.get("user_id", "") or _user.get("id", "")
             dept_id = _user.get("dept_id", "") or _user.get("create_dept_id", "") or ""
             new_id = create_new_version(
@@ -362,6 +383,7 @@ def register_quote_routes(router):
                 remark=body.remark,
                 user_id=user_id,
                 dept_id=dept_id,
+                user_info=user_info,
             )
             return success_response(new_id)
         except ValueError as e:
@@ -377,6 +399,7 @@ def register_quote_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             rows = query_versions(db, body.quoteNo, tenant_no=tenant_no)
             return success_response(rows)
         except ValueError as e:
@@ -392,6 +415,7 @@ def register_quote_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             invalid_quote(db, body.id, tenant_no=tenant_no, remark=body.remark)
             return success_response(None, message="作废成功")
         except ValueError as e:
@@ -407,10 +431,12 @@ def register_quote_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             list_body = body or QuoteListBody()
             rows, _ = list_quotes(
                 db,
                 tenant_no,
+                user_info=user_info,
                 page=1,
                 page_size=10000,
                 filters=list_body.model_dump(by_alias=True, exclude={"page", "pageSize"}),
@@ -437,6 +463,7 @@ def register_quote_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             req = body or QuoteStatisticsOverviewBody()
             data = statistics_overview(
                 db,
@@ -444,6 +471,7 @@ def register_quote_routes(router):
                 owner_user_id=req.ownerUserId,
                 begin_time=req.beginTime,
                 end_time=req.endTime,
+                user_info=user_info,
             )
             return success_response(data)
         except ValueError as e:
@@ -458,6 +486,7 @@ def register_quote_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             return success_response(statistics_status(db, tenant_no))
         except ValueError as e:
             return error_response(400, str(e))
@@ -472,6 +501,7 @@ def register_quote_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             req = body or QuoteStatisticsTrendBody()
             data = statistics_trend(
                 db,
@@ -479,6 +509,7 @@ def register_quote_routes(router):
                 period_type=req.type or "month",
                 begin_time=req.beginTime,
                 end_time=req.endTime,
+                user_info=user_info,
             )
             return success_response(data)
         except ValueError as e:
@@ -494,6 +525,7 @@ def register_quote_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             rows = list_quote_products(db, body.quoteId, tenant_no=tenant_no)
             return success_response(rows)
         except ValueError as e:
@@ -509,6 +541,7 @@ def register_quote_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             user_id = _user.get("user_id", "") or _user.get("id", "")
             dept_id = _user.get("dept_id", "") or _user.get("create_dept_id", "") or ""
             obj = insert_quote_product(
@@ -534,6 +567,7 @@ def register_quote_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             user_id = _user.get("user_id", "") or _user.get("id", "")
             data = body.model_dump(by_alias=True)
             line_id = data.pop("id")
@@ -543,6 +577,7 @@ def register_quote_routes(router):
                 tenant_no=tenant_no,
                 data=data,
                 user_id=user_id,
+                user_info=user_info,
             )
             from app.crud.crm.quote_product import product_row_to_dict
 
@@ -560,6 +595,7 @@ def register_quote_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             soft_delete_quote_products(db, body.ids, tenant_no=tenant_no)
             return success_response(None, message="删除成功")
         except ValueError as e:
@@ -575,6 +611,7 @@ def register_quote_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             rows = list_quote_approvals(db, body.quoteId, tenant_no=tenant_no)
             return success_response(rows)
         except ValueError as e:
@@ -590,6 +627,7 @@ def register_quote_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             user_id = _user.get("user_id", "") or _user.get("id", "")
             approve_quote(
                 db,
@@ -599,6 +637,7 @@ def register_quote_routes(router):
                 approval_opinion=body.approvalOpinion or "",
                 remark=body.remark or "",
                 user_id=user_id,
+                user_info=user_info,
             )
             return success_response(None, message="审批成功")
         except ValueError as e:
@@ -614,6 +653,7 @@ def register_quote_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             user_id = _user.get("user_id", "") or _user.get("id", "")
             withdraw_approval(
                 db,
@@ -621,6 +661,7 @@ def register_quote_routes(router):
                 tenant_no=tenant_no,
                 remark=body.remark or "",
                 user_id=user_id,
+                user_info=user_info,
             )
             return success_response(None, message="撤回成功")
         except ValueError as e:
@@ -651,6 +692,7 @@ def register_quote_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             rows = list_share_logs(db, body.quoteId, tenant_no=tenant_no)
             return success_response(rows)
         except ValueError as e:
@@ -666,6 +708,7 @@ def register_quote_routes(router):
     ):
         try:
             tenant_no = get_tenant_no(db, _user)
+            user_info = crm_user_info(_user)
             stats = share_log_statistics(db, body.quoteId, tenant_no=tenant_no)
             return success_response(stats)
         except ValueError as e:

+ 56 - 4
backend/app/crud/crm/contact.py

@@ -9,6 +9,11 @@ from sqlalchemy import func, or_
 from sqlalchemy.orm import Session
 
 from app.core.utils import generate_uuid
+from app.crud.crm.data_scope_helpers import (
+    apply_crm_create_by_scope,
+    ensure_crm_contact_row_in_scope,
+    ensure_crm_owner_row_in_scope,
+)
 from app.models.crm_models import CrmContact
 
 
@@ -86,11 +91,14 @@ def list_contacts(
     is_decision: bool = None,
     birthday_month: int = None,
     role: str = None,
+    user_info=None,
 ) -> Tuple[List[Dict[str, Any]], int]:
     q = db.query(CrmContact).filter(
         CrmContact.tenant_no == tenant_no,
         CrmContact.del_flag == 0,
     )
+    if user_info:
+        q = apply_crm_create_by_scope(q, CrmContact, user_info, db, tenant_no)
 
     kw = (keyword or "").strip()
     if kw:
@@ -146,7 +154,18 @@ def list_contacts_by_customer(
     db: Session,
     tenant_no: str,
     customer_id: str,
+    *,
+    user_info=None,
 ) -> List[Dict[str, Any]]:
+    if user_info:
+        from app.crud.crm.customer import get_customer
+
+        customer = get_customer(db, customer_id)
+        if not customer or customer.tenant_no != tenant_no:
+            raise ValueError("客户不存在")
+        ensure_crm_owner_row_in_scope(
+            db, customer, user_info, tenant_no, message="无权访问该客户"
+        )
     rows = (
         db.query(CrmContact)
         .filter(
@@ -177,14 +196,22 @@ def create_contact(
     tenant_no: str,
     user_id: str = None,
     dept_id: str = "",
+    user_info=None,
     **kwargs,
 ) -> CrmContact:
     customer_id = kwargs.get("customer_id")
     customer_name = kwargs.get("customer_name")
-    if customer_id and not customer_name:
+    if customer_id:
         from app.crud.crm.customer import get_customer
+
         cust = get_customer(db, customer_id)
-        if cust:
+        if user_info:
+            if not cust or cust.tenant_no != tenant_no:
+                raise ValueError("客户不存在")
+            ensure_crm_owner_row_in_scope(
+                db, cust, user_info, tenant_no, message="无权访问该客户"
+            )
+        if cust and not customer_name:
             customer_name = cust.customer_name
 
     obj = CrmContact(
@@ -229,11 +256,16 @@ def update_contact(
     contact_id: str,
     *,
     user_id: str = None,
+    tenant_no: str = None,
+    user_info=None,
     **kwargs,
 ) -> CrmContact:
     obj = get_contact(db, contact_id)
     if not obj:
         raise ValueError("联系人不存在")
+    if tenant_no and obj.tenant_no != tenant_no:
+        raise ValueError("联系人不存在")
+    ensure_crm_contact_row_in_scope(db, obj, user_info, obj.tenant_no)
 
     _updatable = (
         "contact_name", "gender", "mobile", "telephone", "email",
@@ -260,10 +292,20 @@ def update_contact(
     return obj
 
 
-def set_primary_contact(db: Session, contact_id: str, user_id: str = None) -> CrmContact:
+def set_primary_contact(
+    db: Session,
+    contact_id: str,
+    user_id: str = None,
+    *,
+    tenant_no: str = None,
+    user_info=None,
+) -> CrmContact:
     obj = get_contact(db, contact_id)
     if not obj:
         raise ValueError("联系人不存在")
+    if tenant_no and obj.tenant_no != tenant_no:
+        raise ValueError("联系人不存在")
+    ensure_crm_contact_row_in_scope(db, obj, user_info, obj.tenant_no)
 
     db.query(CrmContact).filter(
         CrmContact.customer_id == obj.customer_id,
@@ -279,10 +321,20 @@ def set_primary_contact(db: Session, contact_id: str, user_id: str = None) -> Cr
     return obj
 
 
-def soft_delete_contact(db: Session, contact_id: str, user_id: str = None) -> bool:
+def soft_delete_contact(
+    db: Session,
+    contact_id: str,
+    user_id: str = None,
+    *,
+    tenant_no: str = None,
+    user_info=None,
+) -> bool:
     obj = get_contact(db, contact_id)
     if not obj:
         raise ValueError("联系人不存在")
+    if tenant_no and obj.tenant_no != tenant_no:
+        raise ValueError("联系人不存在")
+    ensure_crm_contact_row_in_scope(db, obj, user_info, obj.tenant_no)
     customer_id = obj.customer_id
     obj.del_flag = 1
     obj.update_by = user_id

+ 103 - 26
backend/app/crud/crm/contract.py

@@ -13,6 +13,7 @@ from sqlalchemy import func, or_
 from sqlalchemy.orm import Query, Session
 
 from app.core.utils import generate_uuid
+from app.crud.crm.data_scope_helpers import apply_crm_owner_scope, ensure_crm_owner_row_in_scope, apply_crm_create_by_scope, ensure_crm_create_by_row_in_scope
 from app.crud.crm.contact import get_contact
 from app.crud.crm.contract_approval import (
     can_user_approve,
@@ -42,6 +43,25 @@ from app.models.crm_models import (
 from app.models.models import SysUser
 
 _FMT = "%Y-%m-%d %H:%M:%S"
+
+def _ensure_contract_in_scope(db: Session, row: CrmContract, user_info, tenant_no: str) -> None:
+    ensure_crm_owner_row_in_scope(db, row, user_info, tenant_no, message="无权访问该合同")
+
+
+def _get_contract_row_in_scope(
+    db: Session,
+    contract_id: str,
+    *,
+    tenant_no: str,
+    user_info=None,
+) -> CrmContract:
+    row = get_contract_row(db, contract_id, tenant_no=tenant_no)
+    if not row:
+        raise ValueError("合同不存在")
+    _ensure_contract_in_scope(db, row, user_info, tenant_no)
+    return row
+
+
 _DATE_FMT = "%Y-%m-%d"
 
 
@@ -374,10 +394,13 @@ def list_contracts(
     page_size: int = 10,
     filters: dict = None,
     current_user_id: str = None,
+    user_info=None,
 ) -> Tuple[List[Dict[str, Any]], int]:
     """Java: queryPageList"""
     q = db.query(CrmContract)
     q = _build_contract_filters(db, q, filters or {}, tenant_no=tenant_no)
+    if user_info:
+        q = apply_crm_owner_scope(q, CrmContract, user_info, db, tenant_no)
     total = q.order_by(None).with_entities(func.count(CrmContract.id)).scalar() or 0
     page = max(1, page)
     page_size = min(max(1, page_size), 100)
@@ -403,6 +426,7 @@ def get_contract(
     *,
     tenant_no: str,
     current_user_id: str = None,
+    user_info=None,
 ) -> Optional[Dict[str, Any]]:
     """Java: queryById"""
     row = (
@@ -416,6 +440,7 @@ def get_contract(
     )
     if not row:
         return None
+    _ensure_contract_in_scope(db, row, user_info, tenant_no)
 
     detail = _row_to_dict(row)
     detail["createByName"] = _resolve_user_display_name(db, detail.get("createBy"))
@@ -506,12 +531,14 @@ def get_contract_detail(
     *,
     tenant_no: str,
     current_user_id: str = None,
+    user_info=None,
 ) -> Dict[str, Any]:
     detail = get_contract(
         db,
         contract_id,
         tenant_no=tenant_no,
         current_user_id=current_user_id,
+        user_info=user_info,
     )
     if not detail:
         raise ValueError("合同不存在")
@@ -957,9 +984,10 @@ def update_contract(
     data: dict,
     user_id: str = None,
     dept_id: str = "",
+    user_info=None,
 ) -> Dict[str, Any]:
     """Java: updateByBo"""
-    old = get_contract_row(db, contract_id, tenant_no=tenant_no)
+    old = _get_contract_row_in_scope(db, contract_id, tenant_no=tenant_no, user_info=user_info)
     if not old:
         raise ValueError("合同不存在")
     if old.status != "draft":
@@ -1026,11 +1054,12 @@ def soft_delete_contracts(
     *,
     tenant_no: str,
     validate: bool = True,
+    user_info=None,
 ) -> int:
     """Java: deleteWithValidByIds — 软删除"""
     count = 0
     for cid in contract_ids:
-        entity = get_contract_row(db, cid, tenant_no=tenant_no)
+        entity = _get_contract_row_in_scope(db, cid, tenant_no=tenant_no, user_info=user_info)
         if not entity:
             continue
         if validate and entity.status != "draft":
@@ -1116,9 +1145,10 @@ def submit_contract_approval(
     remark: str = "",
     user_id: str = None,
     dept_id: str = "",
+    user_info=None,
 ) -> bool:
     """Java: submitApproval"""
-    entity = get_contract_row(db, contract_id, tenant_no=tenant_no)
+    entity = _get_contract_row_in_scope(db, contract_id, tenant_no=tenant_no, user_info=user_info)
     if not entity:
         raise ValueError("合同不存在")
     if entity.status != "draft":
@@ -1158,6 +1188,7 @@ def batch_submit_contract_approval(
     remark: str = "",
     user_id: str = None,
     dept_id: str = "",
+    user_info=None,
 ) -> int:
     """Java: batchSubmitApproval"""
     success = 0
@@ -1185,9 +1216,10 @@ def withdraw_contract_approval(
     tenant_no: str,
     remark: str = "",
     user_id: str = None,
+    user_info=None,
 ) -> bool:
     """Java: withdrawApproval"""
-    entity = get_contract_row(db, contract_id, tenant_no=tenant_no)
+    entity = _get_contract_row_in_scope(db, contract_id, tenant_no=tenant_no, user_info=user_info)
     if not entity:
         raise ValueError("合同不存在")
     if entity.status != "approving":
@@ -1211,8 +1243,9 @@ def start_execute(
     tenant_no: str,
     remark: str = "",
     user_id: str = None,
+    user_info=None,
 ) -> bool:
-    entity = get_contract_row(db, contract_id, tenant_no=tenant_no)
+    entity = _get_contract_row_in_scope(db, contract_id, tenant_no=tenant_no, user_info=user_info)
     if not entity:
         raise ValueError("合同不存在")
     if entity.status != "approved":
@@ -1233,8 +1266,9 @@ def finish_contract(
     tenant_no: str,
     remark: str = "",
     user_id: str = None,
+    user_info=None,
 ) -> bool:
-    entity = get_contract_row(db, contract_id, tenant_no=tenant_no)
+    entity = _get_contract_row_in_scope(db, contract_id, tenant_no=tenant_no, user_info=user_info)
     if not entity:
         raise ValueError("合同不存在")
     if entity.status != "executing":
@@ -1256,10 +1290,11 @@ def invalid_contract(
     invalid_reason: str,
     remark: str = "",
     user_id: str = None,
+    user_info=None,
 ) -> bool:
     if not (invalid_reason or "").strip():
         raise ValueError("作废原因不能为空")
-    entity = get_contract_row(db, contract_id, tenant_no=tenant_no)
+    entity = _get_contract_row_in_scope(db, contract_id, tenant_no=tenant_no, user_info=user_info)
     if not entity:
         raise ValueError("合同不存在")
     if entity.status == "finished":
@@ -1282,8 +1317,9 @@ def save_template(
     *,
     tenant_no: str,
     settlement_clause_drafts: str = None,
+    user_info=None,
 ) -> bool:
-    entity = get_contract_row(db, contract_id, tenant_no=tenant_no)
+    entity = _get_contract_row_in_scope(db, contract_id, tenant_no=tenant_no, user_info=user_info)
     if not entity:
         raise ValueError("合同不存在")
 
@@ -1305,7 +1341,7 @@ def save_template(
 
 
 def get_template(db: Session, contract_id: str, *, tenant_no: str) -> str:
-    entity = get_contract_row(db, contract_id, tenant_no=tenant_no)
+    entity = _get_contract_row_in_scope(db, contract_id, tenant_no=tenant_no, user_info=user_info)
     if not entity:
         raise ValueError("合同不存在")
     return entity.template_content or ""
@@ -1317,8 +1353,9 @@ def upload_file(
     file_url: str,
     *,
     tenant_no: str,
+    user_info=None,
 ) -> bool:
-    entity = get_contract_row(db, contract_id, tenant_no=tenant_no)
+    entity = _get_contract_row_in_scope(db, contract_id, tenant_no=tenant_no, user_info=user_info)
     if not entity:
         raise ValueError("合同不存在")
     entity.contract_file = file_url or ""
@@ -1333,10 +1370,11 @@ def upload_attachments(
     oss_ids: str,
     *,
     tenant_no: str,
+    user_info=None,
 ) -> bool:
     if not (oss_ids or "").strip():
         raise ValueError("附件ossId不能为空")
-    entity = get_contract_row(db, contract_id, tenant_no=tenant_no)
+    entity = _get_contract_row_in_scope(db, contract_id, tenant_no=tenant_no, user_info=user_info)
     if not entity:
         raise ValueError("合同不存在")
 
@@ -1357,10 +1395,11 @@ def upload_attachments_with_category(
     attachments: List[dict],
     *,
     tenant_no: str,
+    user_info=None,
 ) -> bool:
     if not attachments:
         raise ValueError("附件列表不能为空")
-    entity = get_contract_row(db, contract_id, tenant_no=tenant_no)
+    entity = _get_contract_row_in_scope(db, contract_id, tenant_no=tenant_no, user_info=user_info)
     if not entity:
         raise ValueError("合同不存在")
 
@@ -1404,6 +1443,7 @@ def statistics_overview(
     owner_user_id: str = None,
     begin_time: str = None,
     end_time: str = None,
+    user_info=None,
 ) -> Dict[str, Any]:
     q = db.query(CrmContract).filter(CrmContract.tenant_no == tenant_no, CrmContract.del_flag == 0)
     if owner_user_id:
@@ -1416,6 +1456,8 @@ def statistics_overview(
         q = q.filter(CrmContract.create_time >= begin_dt)
     elif end_dt:
         q = q.filter(CrmContract.create_time <= end_dt)
+    if user_info:
+        q = apply_crm_owner_scope(q, CrmContract, user_info, db, tenant_no)
 
     contracts = q.all()
     total_count = len(contracts)
@@ -1457,12 +1499,11 @@ def statistics_overview(
     }
 
 
-def statistics_by_status(db: Session, tenant_no: str) -> List[Dict[str, Any]]:
-    contracts = (
-        db.query(CrmContract)
-        .filter(CrmContract.tenant_no == tenant_no, CrmContract.del_flag == 0)
-        .all()
-    )
+def statistics_by_status(db: Session, tenant_no: str, *, user_info=None) -> List[Dict[str, Any]]:
+    q = db.query(CrmContract).filter(CrmContract.tenant_no == tenant_no, CrmContract.del_flag == 0)
+    if user_info:
+        q = apply_crm_owner_scope(q, CrmContract, user_info, db, tenant_no)
+    contracts = q.all()
     total_count = len(contracts)
     total_amount = sum(_decimal_value(c.amount) for c in contracts)
 
@@ -1492,12 +1533,11 @@ def statistics_by_status(db: Session, tenant_no: str) -> List[Dict[str, Any]]:
     return result
 
 
-def statistics_by_type(db: Session, tenant_no: str) -> List[Dict[str, Any]]:
-    contracts = (
-        db.query(CrmContract)
-        .filter(CrmContract.tenant_no == tenant_no, CrmContract.del_flag == 0)
-        .all()
-    )
+def statistics_by_type(db: Session, tenant_no: str, *, user_info=None) -> List[Dict[str, Any]]:
+    q = db.query(CrmContract).filter(CrmContract.tenant_no == tenant_no, CrmContract.del_flag == 0)
+    if user_info:
+        q = apply_crm_owner_scope(q, CrmContract, user_info, db, tenant_no)
+    contracts = q.all()
     total_count = len(contracts)
     type_map = (
         ("sale", "销售合同"),
@@ -1544,12 +1584,15 @@ def statistics_trend(
     period_type: str = "month",
     begin_time: str = None,
     end_time: str = None,
+    user_info=None,
 ) -> List[Dict[str, Any]]:
     q = db.query(CrmContract).filter(CrmContract.tenant_no == tenant_no, CrmContract.del_flag == 0)
     begin_dt = _parse_optional_datetime(begin_time)
     end_dt = _parse_optional_datetime(end_time)
     if begin_dt and end_dt:
         q = q.filter(CrmContract.create_time >= begin_dt, CrmContract.create_time <= end_dt)
+    if user_info:
+        q = apply_crm_owner_scope(q, CrmContract, user_info, db, tenant_no)
     contracts = q.all()
     if not contracts:
         return []
@@ -1585,6 +1628,7 @@ def query_expiring(
     tenant_no: str,
     *,
     days: int = 30,
+    user_info=None,
 ) -> List[Dict[str, Any]]:
     if not days or days <= 0:
         days = 30
@@ -1642,6 +1686,7 @@ def create_from_quote(
     data: dict,
     user_id: str = None,
     dept_id: str = "",
+    user_info=None,
 ) -> str:
     """Java: createFromQuote — 返回合同 id"""
     quote = get_quote(db, quote_id, tenant_no=tenant_no)
@@ -1700,6 +1745,7 @@ def renew_contract(
     data: dict,
     user_id: str = None,
     dept_id: str = "",
+    user_info=None,
 ) -> str:
     """Java: renewContract — 基础续签"""
     parent = get_contract_row(db, parent_contract_id, tenant_no=tenant_no)
@@ -1750,6 +1796,7 @@ def export_contracts(
     tenant_no: str,
     *,
     filters: dict = None,
+    user_info=None,
 ) -> List[Dict[str, Any]]:
     """Java: queryList for export"""
     q = db.query(CrmContract)
@@ -1790,11 +1837,14 @@ def list_contract_changes(
     page: int = 1,
     page_size: int = 10,
     filters: dict = None,
+    user_info=None,
 ) -> Tuple[List[Dict[str, Any]], int]:
     q = db.query(CrmContractChange).filter(
         CrmContractChange.tenant_no == tenant_no,
         CrmContractChange.del_flag == 0,
     )
+    if user_info:
+        q = apply_crm_create_by_scope(q, CrmContractChange, user_info, db, tenant_no)
     filters = filters or {}
     contract_id = (filters.get("contractId") or filters.get("contract_id") or "").strip()
     if contract_id:
@@ -1814,6 +1864,7 @@ def get_contract_change_detail(
     change_id: str,
     *,
     tenant_no: str,
+    user_info=None,
 ) -> Dict[str, Any]:
     row = (
         db.query(CrmContractChange)
@@ -1826,6 +1877,9 @@ def get_contract_change_detail(
     )
     if not row:
         raise ValueError("合同变更记录不存在")
+    ensure_crm_create_by_row_in_scope(
+        db, row, user_info, tenant_no, message="无权访问该合同变更"
+    )
     return _change_row_to_dict(row)
 
 
@@ -1861,11 +1915,14 @@ def list_contract_templates(
     page: int = 1,
     page_size: int = 10,
     filters: dict = None,
+    user_info=None,
 ) -> Tuple[List[Dict[str, Any]], int]:
     q = db.query(CrmContractTemplate).filter(
         CrmContractTemplate.tenant_no == tenant_no,
         CrmContractTemplate.del_flag == 0,
     )
+    if user_info:
+        q = apply_crm_create_by_scope(q, CrmContractTemplate, user_info, db, tenant_no)
     filters = filters or {}
     template_no = (filters.get("templateNo") or filters.get("template_no") or "").strip()
     if template_no:
@@ -1937,6 +1994,7 @@ def create_from_opportunity(
     data: dict,
     user_id: str = None,
     dept_id: str = "",
+    user_info=None,
 ) -> str:
     """Java: createFromOpportunity — 返回合同 id"""
     opp_row = (
@@ -1990,9 +2048,10 @@ def sign_contract(
     contract_file: str,
     remark: str = "",
     user_id: str = None,
+    user_info=None,
 ) -> bool:
     """Java: signContract"""
-    entity = get_contract_row(db, contract_id, tenant_no=tenant_no)
+    entity = _get_contract_row_in_scope(db, contract_id, tenant_no=tenant_no, user_info=user_info)
     if not entity:
         raise ValueError("合同不存在")
     if entity.status != "approved":
@@ -2019,6 +2078,7 @@ def batch_assign_owner(
     owner_user_id: str,
     remark: str = "",
     user_id: str = None,
+    user_info=None,
 ) -> int:
     """Java: batchAssignOwner"""
     if not contract_ids:
@@ -2030,7 +2090,7 @@ def batch_assign_owner(
     success = 0
     owner_name = _resolve_user_display_name(db, owner_id)
     for cid in contract_ids:
-        entity = get_contract_row(db, cid, tenant_no=tenant_no)
+        entity = _get_contract_row_in_scope(db, cid, tenant_no=tenant_no, user_info=user_info)
         if not entity:
             continue
         entity.owner_user_id = owner_id
@@ -2049,6 +2109,7 @@ def download_contract_pdf(
     contract_id: str,
     *,
     tenant_no: str,
+    user_info=None,
 ) -> str:
     """Java: downloadContractPdf — 返回合同文件 URL/路径"""
     detail = get_contract_detail(db, contract_id, tenant_no=tenant_no)
@@ -2177,10 +2238,14 @@ def update_contract_change(
     tenant_no: str,
     data: dict,
     user_id: str = None,
+    user_info=None,
 ) -> Dict[str, Any]:
     entity = _get_change_row(db, change_id, tenant_no=tenant_no)
     if not entity:
         raise ValueError("合同变更记录不存在")
+    ensure_crm_create_by_row_in_scope(
+        db, entity, user_info, tenant_no, message="无权访问该合同变更"
+    )
     if entity.approval_status != "pending":
         raise ValueError("只能修改待审批状态的变更记录")
 
@@ -2210,12 +2275,16 @@ def soft_delete_contract_changes(
     *,
     tenant_no: str,
     validate: bool = True,
+    user_info=None,
 ) -> int:
     count = 0
     for cid in change_ids:
         entity = _get_change_row(db, cid, tenant_no=tenant_no)
         if not entity:
             continue
+        ensure_crm_create_by_row_in_scope(
+            db, entity, user_info, tenant_no, message="无权访问该合同变更"
+        )
         if validate and entity.approval_status != "pending":
             raise ValueError("只能删除待审批状态的变更记录")
         entity.del_flag = 1
@@ -2346,6 +2415,7 @@ def update_contract_template(
     tenant_no: str,
     data: dict,
     user_id: str = None,
+    user_info=None,
 ) -> Dict[str, Any]:
     entity = (
         db.query(CrmContractTemplate)
@@ -2358,6 +2428,9 @@ def update_contract_template(
     )
     if not entity:
         raise ValueError("合同模板不存在")
+    ensure_crm_create_by_row_in_scope(
+        db, entity, user_info, tenant_no, message="无权访问该合同模板"
+    )
 
     field_map = (
         (("template_no", "templateNo"), "template_no"),
@@ -2398,6 +2471,7 @@ def soft_delete_contract_templates(
     *,
     tenant_no: str,
     validate: bool = True,
+    user_info=None,
 ) -> int:
     count = 0
     for tid in template_ids:
@@ -2412,6 +2486,9 @@ def soft_delete_contract_templates(
         )
         if not entity:
             continue
+        ensure_crm_create_by_row_in_scope(
+            db, entity, user_info, tenant_no, message="无权访问该合同模板"
+        )
         if validate and (entity.usage_count or 0) > 0:
             raise ValueError(f"模板【{entity.template_name}】已被使用,不能删除")
         entity.del_flag = 1

+ 5 - 0
backend/app/crud/crm/contract_product.py

@@ -76,7 +76,12 @@ def list_contract_products(
     *,
     tenant_no: str = None,
     include_price_tiers: bool = False,
+    user_info=None,
 ) -> List[Dict[str, Any]]:
+    if tenant_no and user_info:
+        from app.crud.crm.contract import _get_contract_row_in_scope
+
+        _get_contract_row_in_scope(db, contract_id, tenant_no=tenant_no, user_info=user_info)
     q = db.query(CrmContractProduct).filter(
         CrmContractProduct.contract_id == contract_id,
         CrmContractProduct.del_flag == 0,

+ 48 - 0
backend/app/crud/crm/data_scope_helpers.py

@@ -52,3 +52,51 @@ def ensure_crm_create_by_row_in_scope(
         return
     if not row_in_data_scope(db, row, user_info, tenant_no):
         raise ValueError(message)
+
+
+def ensure_crm_contact_row_in_scope(
+    db: Session, row, user_info, tenant_no: str, *, message: str = "无权访问该联系人"
+) -> None:
+    """联系人:create_by 在范围内,或关联客户在 owner 范围内。"""
+    if not user_info or row is None:
+        return
+    if row_in_data_scope(db, row, user_info, tenant_no):
+        return
+    customer_id = getattr(row, "customer_id", None) or ""
+    if customer_id:
+        from app.crud.crm.customer import get_customer
+
+        customer = get_customer(db, customer_id)
+        if customer and customer.tenant_no == tenant_no:
+            if row_in_data_scope(
+                db, customer, user_info, tenant_no, owner_field=CRM_OWNER_FIELD
+            ):
+                return
+    raise ValueError(message)
+
+
+def ensure_crm_customer_linked_row_in_scope(
+    db: Session,
+    row,
+    user_info,
+    tenant_no: str,
+    *,
+    customer_id: str = None,
+    message: str = "无权访问该数据",
+) -> None:
+    """客户子表(开票信息等):先校验关联客户 owner scope,否则校验 create_by。"""
+    if not user_info or row is None:
+        return
+    cid = customer_id or getattr(row, "customer_id", None) or ""
+    if cid:
+        from app.crud.crm.customer import get_customer
+
+        customer = get_customer(db, cid)
+        if customer and customer.tenant_no == tenant_no:
+            if row_in_data_scope(
+                db, customer, user_info, tenant_no, owner_field=CRM_OWNER_FIELD
+            ):
+                return
+    if row_in_data_scope(db, row, user_info, tenant_no):
+        return
+    raise ValueError(message)

+ 46 - 1
backend/app/crud/crm/invoice_info.py

@@ -7,6 +7,10 @@ from typing import Any, Dict, List, Optional
 from sqlalchemy.orm import Session
 
 from app.core.utils import generate_uuid
+from app.crud.crm.data_scope_helpers import (
+    ensure_crm_customer_linked_row_in_scope,
+    ensure_crm_owner_row_in_scope,
+)
 from app.models.crm_models import CrmInvoiceInfo
 
 
@@ -39,7 +43,18 @@ def list_invoice_info_by_customer(
     db: Session,
     tenant_no: str,
     customer_id: str,
+    *,
+    user_info=None,
 ) -> List[Dict[str, Any]]:
+    if user_info:
+        from app.crud.crm.customer import get_customer
+
+        customer = get_customer(db, customer_id)
+        if not customer or customer.tenant_no != tenant_no:
+            raise ValueError("客户不存在")
+        ensure_crm_owner_row_in_scope(
+            db, customer, user_info, tenant_no, message="无权访问该客户"
+        )
     rows = (
         db.query(CrmInvoiceInfo)
         .filter(
@@ -87,11 +102,21 @@ def create_invoice_info(
     tenant_no: str,
     user_id: str = None,
     dept_id: str = "",
+    user_info=None,
     **kwargs,
 ) -> CrmInvoiceInfo:
     customer_id = kwargs.get("customer_id")
     if not customer_id:
         raise ValueError("缺少客户ID")
+    if user_info:
+        from app.crud.crm.customer import get_customer
+
+        customer = get_customer(db, customer_id)
+        if not customer or customer.tenant_no != tenant_no:
+            raise ValueError("客户不存在")
+        ensure_crm_owner_row_in_scope(
+            db, customer, user_info, tenant_no, message="无权访问该客户"
+        )
 
     company_name = (kwargs.get("company_name") or "").strip()
     if not company_name:
@@ -132,6 +157,7 @@ def update_invoice_info(
     *,
     tenant_no: str = None,
     user_id: str = None,
+    user_info=None,
     **kwargs,
 ) -> CrmInvoiceInfo:
     obj = get_invoice_info(db, invoice_id)
@@ -140,6 +166,9 @@ def update_invoice_info(
 
     if tenant_no and obj.tenant_no != tenant_no:
         raise ValueError("开票信息不存在")
+    ensure_crm_customer_linked_row_in_scope(
+        db, obj, user_info, obj.tenant_no, message="无权访问该开票信息"
+    )
 
     _updatable = (
         "company_name",
@@ -181,12 +210,16 @@ def set_default_invoice_info(
     *,
     tenant_no: str,
     user_id: str = None,
+    user_info=None,
 ) -> CrmInvoiceInfo:
     obj = get_invoice_info(db, invoice_id)
     if not obj or obj.tenant_no != tenant_no:
         raise ValueError("开票信息不存在")
     if obj.customer_id != customer_id:
         raise ValueError("开票信息与客户不匹配")
+    ensure_crm_customer_linked_row_in_scope(
+        db, obj, user_info, tenant_no, message="无权访问该开票信息"
+    )
 
     _clear_default_for_customer(db, tenant_no, customer_id, exclude_id=invoice_id)
     obj.is_default = "1"
@@ -197,10 +230,22 @@ def set_default_invoice_info(
     return obj
 
 
-def soft_delete_invoice_info(db: Session, invoice_id: str, user_id: str = None) -> bool:
+def soft_delete_invoice_info(
+    db: Session,
+    invoice_id: str,
+    user_id: str = None,
+    *,
+    tenant_no: str = None,
+    user_info=None,
+) -> bool:
     obj = get_invoice_info(db, invoice_id)
     if not obj:
         raise ValueError("开票信息不存在")
+    if tenant_no and obj.tenant_no != tenant_no:
+        raise ValueError("开票信息不存在")
+    ensure_crm_customer_linked_row_in_scope(
+        db, obj, user_info, obj.tenant_no, message="无权访问该开票信息"
+    )
     obj.del_flag = 1
     obj.update_by = user_id
     obj.update_time = datetime.now()

+ 32 - 3
backend/app/crud/crm/product.py

@@ -10,6 +10,10 @@ from sqlalchemy import and_, func, or_
 from sqlalchemy.orm import Session
 
 from app.core.utils import generate_uuid
+from app.crud.crm.data_scope_helpers import (
+    apply_crm_create_by_scope,
+    ensure_crm_create_by_row_in_scope,
+)
 from app.models.crm_models import CrmProduct, CrmProductCategory, SysOss
 
 
@@ -48,12 +52,15 @@ def list_categories(
     *,
     category_name: Optional[str] = None,
     status: Optional[str] = None,
+    user_info=None,
 ) -> List[Dict[str, Any]]:
     """获取分类(扁平列表,支持名称模糊与状态筛选)"""
     q = db.query(CrmProductCategory).filter(
         CrmProductCategory.tenant_no == tenant_no,
         CrmProductCategory.del_flag == 0,
     )
+    if user_info:
+        q = apply_crm_create_by_scope(q, CrmProductCategory, user_info, db, tenant_no)
     name_kw = (category_name or "").strip()
     if name_kw:
         q = q.filter(CrmProductCategory.category_name.like(f"%{name_kw}%"))
@@ -99,6 +106,7 @@ def list_category_tree(
     *,
     category_name: Optional[str] = None,
     status: Optional[str] = None,
+    user_info=None,
 ) -> List[Dict[str, Any]]:
     """获取产品分类树(嵌套 children,供树形表格/树选择器使用)"""
     name_kw = (category_name or "").strip()
@@ -106,7 +114,7 @@ def list_category_tree(
     has_filter = bool(name_kw) or bool(status_val)
 
     if not has_filter:
-        flat = list_categories(db, tenant_no)
+        flat = list_categories(db, tenant_no, user_info=user_info)
         return _build_category_tree(flat)
 
     matched = list_categories(
@@ -114,6 +122,7 @@ def list_category_tree(
         tenant_no,
         category_name=category_name,
         status=status,
+        user_info=user_info,
     )
     if not matched:
         return []
@@ -178,12 +187,16 @@ def update_category(
     category_id: str,
     *,
     user_id: str = None,
+    user_info=None,
     **kwargs,
 ) -> CrmProductCategory:
     """更新分类"""
     obj = get_category(db, category_id)
     if not obj:
         raise ValueError("分类不存在")
+    ensure_crm_create_by_row_in_scope(
+        db, obj, user_info, obj.tenant_no, message="无权访问该分类"
+    )
 
     # 逐字段更新
     for field in ("category_name", "category_code", "status", "order_num", "remark"):
@@ -208,11 +221,14 @@ def update_category(
     return obj
 
 
-def soft_delete_category(db: Session, category_id: str) -> None:
+def soft_delete_category(db: Session, category_id: str, *, user_info=None) -> None:
     """软删除分类"""
     obj = get_category(db, category_id)
     if not obj:
         raise ValueError("分类不存在")
+    ensure_crm_create_by_row_in_scope(
+        db, obj, user_info, obj.tenant_no, message="无权访问该分类"
+    )
 
     # 检查是否有子分类
     has_children = (
@@ -303,12 +319,15 @@ def list_products(
     keyword: str = None,
     category_id: str = None,
     status: str = None,
+    user_info=None,
 ) -> Tuple[List[Dict[str, Any]], int]:
     """分页查询产品列表"""
     q = db.query(CrmProduct).filter(
         CrmProduct.tenant_no == tenant_no,
         CrmProduct.del_flag == 0,
     )
+    if user_info:
+        q = apply_crm_create_by_scope(q, CrmProduct, user_info, db, tenant_no)
 
     # keyword 筛选
     kw = (keyword or "").strip()
@@ -482,12 +501,16 @@ def update_product(
     product_id: str,
     *,
     user_id: str = None,
+    user_info=None,
     **kwargs,
 ) -> CrmProduct:
     """更新产品"""
     obj = get_product(db, product_id)
     if not obj:
         raise ValueError("产品不存在")
+    ensure_crm_create_by_row_in_scope(
+        db, obj, user_info, obj.tenant_no, message="无权访问该产品"
+    )
 
     # 逐字段更新
     for field in (
@@ -535,11 +558,14 @@ def update_product(
     return obj
 
 
-def soft_delete_product(db: Session, product_id: str) -> None:
+def soft_delete_product(db: Session, product_id: str, *, user_info=None) -> None:
     """软删除产品"""
     obj = get_product(db, product_id)
     if not obj:
         raise ValueError("产品不存在")
+    ensure_crm_create_by_row_in_scope(
+        db, obj, user_info, obj.tenant_no, message="无权访问该产品"
+    )
     obj.del_flag = 1
     db.commit()
 
@@ -584,6 +610,7 @@ def list_product_options(
     *,
     keyword: str = None,
     category_id: str = None,
+    user_info=None,
 ) -> List[Dict[str, Any]]:
     """获取产品下拉选项(不分页,返回全量)"""
     q = db.query(CrmProduct).filter(
@@ -591,6 +618,8 @@ def list_product_options(
         CrmProduct.del_flag == 0,
         CrmProduct.status == "on",
     )
+    if user_info:
+        q = apply_crm_create_by_scope(q, CrmProduct, user_info, db, tenant_no)
     if keyword:
         like = f"%{keyword}%"
         q = q.filter(or_(CrmProduct.product_name.like(like), CrmProduct.product_no.like(like)))

+ 62 - 26
backend/app/crud/crm/quote.py

@@ -31,10 +31,28 @@ from app.crud.crm.supplier_info import get_supplier_info
 from app.models.crm_models import CrmContract, CrmQuote, CrmQuoteApproval
 from app.models.models import SysUser
 from app.services.crm_quote_pdf import generate_quote_pdf
+from app.crud.crm.data_scope_helpers import apply_crm_owner_scope, ensure_crm_owner_row_in_scope
 
 _FMT = "%Y-%m-%d %H:%M:%S"
 _DATE_FMT = "%Y-%m-%d"
 
+def _ensure_quote_in_scope(db: Session, row: CrmQuote, user_info, tenant_no: str) -> None:
+    ensure_crm_owner_row_in_scope(db, row, user_info, tenant_no, message="无权访问该报价单")
+
+
+def _get_quote_in_scope(
+    db: Session,
+    quote_id: str,
+    *,
+    tenant_no: str,
+    user_info=None,
+) -> CrmQuote:
+    obj = get_quote(db, quote_id, tenant_no=tenant_no)
+    if not obj:
+        raise ValueError("报价单不存在")
+    _ensure_quote_in_scope(db, obj, user_info, tenant_no)
+    return obj
+
 
 def total_pages(total: int, page_size: int) -> int:
     if page_size <= 0:
@@ -467,11 +485,13 @@ def get_quote_detail(
     quote_id: str,
     *,
     tenant_no: str = None,
+    user_info=None,
 ) -> Dict[str, Any]:
     """Java: queryById"""
     obj = get_quote(db, quote_id, tenant_no=tenant_no)
     if not obj:
         raise ValueError("报价单不存在")
+    _ensure_quote_in_scope(db, obj, user_info, tenant_no or obj.tenant_no)
 
     detail = _row_to_dict(obj)
     detail["productList"] = list_quote_products(db, quote_id, tenant_no=tenant_no or obj.tenant_no)
@@ -513,10 +533,13 @@ def list_quotes(
     page_size: int = 10,
     filters: dict = None,
     current_user_id: str = None,
+    user_info=None,
 ) -> Tuple[List[Dict[str, Any]], int]:
     """Java: queryPageList"""
     q = db.query(CrmQuote)
     q = _build_quote_filters(db, q, filters or {}, tenant_no=tenant_no)
+    if user_info:
+        q = apply_crm_owner_scope(q, CrmQuote, user_info, db, tenant_no)
     total = q.order_by(None).with_entities(func.count(CrmQuote.id)).scalar() or 0
     page = max(1, page)
     page_size = min(max(1, page_size), 100)
@@ -645,9 +668,10 @@ def update_quote(
     data: dict,
     user_id: str = None,
     dept_id: str = "",
+    user_info=None,
 ) -> Dict[str, Any]:
     """Java: updateByBo"""
-    old = get_quote(db, quote_id, tenant_no=tenant_no)
+    old = _get_quote_in_scope(db, quote_id, tenant_no=tenant_no, user_info=user_info)
     if not old:
         raise ValueError("报价单不存在")
     if old.status != "draft":
@@ -769,11 +793,12 @@ def soft_delete_quotes(
     *,
     tenant_no: str,
     validate: bool = True,
+    user_info=None,
 ) -> int:
     """Java: deleteWithValidByIds"""
     count = 0
     for qid in quote_ids:
-        entity = get_quote(db, qid, tenant_no=tenant_no)
+        entity = _get_quote_in_scope(db, qid, tenant_no=tenant_no, user_info=user_info)
         if not entity:
             continue
         if validate:
@@ -797,9 +822,10 @@ def submit_quote(
     remark: str = "",
     user_id: str = None,
     dept_id: str = "",
+    user_info=None,
 ) -> bool:
     """Java: submitQuote"""
-    entity = get_quote(db, quote_id, tenant_no=tenant_no)
+    entity = _get_quote_in_scope(db, quote_id, tenant_no=tenant_no, user_info=user_info)
     if not entity:
         raise ValueError("报价单不存在")
     if entity.status != "draft":
@@ -837,9 +863,10 @@ def save_template(
     template_content: str,
     *,
     tenant_no: str,
+    user_info=None,
 ) -> bool:
     """Java: saveTemplate"""
-    entity = get_quote(db, quote_id, tenant_no=tenant_no)
+    entity = _get_quote_in_scope(db, quote_id, tenant_no=tenant_no, user_info=user_info)
     if not entity:
         raise ValueError("报价单不存在")
     entity.template_content = template_content
@@ -853,9 +880,10 @@ def get_template(
     quote_id: str,
     *,
     tenant_no: str,
+    user_info=None,
 ) -> str:
     """Java: getTemplate"""
-    entity = get_quote(db, quote_id, tenant_no=tenant_no)
+    entity = _get_quote_in_scope(db, quote_id, tenant_no=tenant_no, user_info=user_info)
     if not entity:
         raise ValueError("报价单不存在")
     return entity.template_content or ""
@@ -866,9 +894,10 @@ def generate_pdf(
     quote_id: str,
     *,
     tenant_no: str,
+    user_info=None,
 ) -> str:
     """Java: generatePdf"""
-    entity = get_quote(db, quote_id, tenant_no=tenant_no)
+    entity = _get_quote_in_scope(db, quote_id, tenant_no=tenant_no, user_info=user_info)
     if not entity:
         raise ValueError("报价单不存在")
     detail = get_quote_detail(db, quote_id, tenant_no=tenant_no)
@@ -887,9 +916,10 @@ def generate_share(
     tenant_no: str,
     validity_days: int = None,
     password: str = "",
+    user_info=None,
 ) -> str:
     """Java: generateShare"""
-    entity = get_quote(db, quote_id, tenant_no=tenant_no)
+    entity = _get_quote_in_scope(db, quote_id, tenant_no=tenant_no, user_info=user_info)
     if not entity:
         raise ValueError("报价单不存在")
 
@@ -966,9 +996,10 @@ def accept_quote(
     *,
     tenant_no: str,
     remark: str = "",
+    user_info=None,
 ) -> bool:
     """Java: acceptQuote"""
-    entity = get_quote(db, quote_id, tenant_no=tenant_no)
+    entity = _get_quote_in_scope(db, quote_id, tenant_no=tenant_no, user_info=user_info)
     if not entity:
         raise ValueError("报价单不存在")
     entity.status = "accepted"
@@ -986,9 +1017,10 @@ def reject_quote(
     tenant_no: str,
     reject_reason: str = "",
     remark: str = "",
+    user_info=None,
 ) -> bool:
     """Java: rejectQuote"""
-    entity = get_quote(db, quote_id, tenant_no=tenant_no)
+    entity = _get_quote_in_scope(db, quote_id, tenant_no=tenant_no, user_info=user_info)
     if not entity:
         raise ValueError("报价单不存在")
     entity.status = "rejected"
@@ -1012,6 +1044,7 @@ def to_contract(
     end_date: date = None,
     user_id: str = None,
     dept_id: str = "",
+    user_info=None,
 ) -> str:
     """Java: toContract — 返回 contractId"""
     sign_date = _parse_optional_date(sign_date)
@@ -1075,9 +1108,10 @@ def create_new_version(
     remark: str = "",
     user_id: str = None,
     dept_id: str = "",
+    user_info=None,
 ) -> str:
     """Java: createNewVersion — 返回新报价单 id"""
-    old = get_quote(db, quote_id, tenant_no=tenant_no)
+    old = _get_quote_in_scope(db, quote_id, tenant_no=tenant_no, user_info=user_info)
     if not old:
         raise ValueError("报价单不存在")
 
@@ -1150,6 +1184,7 @@ def query_versions(
     quote_no: str,
     *,
     tenant_no: str,
+    user_info=None,
 ) -> List[Dict[str, Any]]:
     """Java: queryVersions"""
     rows = (
@@ -1172,9 +1207,10 @@ def withdraw_quote_approval(
     tenant_no: str,
     remark: str = "",
     user_id: str = None,
+    user_info=None,
 ) -> bool:
     """Java: CrmQuoteServiceImpl.withdrawApproval(校验创建人)"""
-    entity = get_quote(db, quote_id, tenant_no=tenant_no)
+    entity = _get_quote_in_scope(db, quote_id, tenant_no=tenant_no, user_info=user_info)
     if not entity:
         raise ValueError("报价单不存在")
     if entity.status != "submitted":
@@ -1197,9 +1233,10 @@ def invalid_quote(
     *,
     tenant_no: str,
     remark: str = "",
+    user_info=None,
 ) -> bool:
     """Java: invalidQuote"""
-    entity = get_quote(db, quote_id, tenant_no=tenant_no)
+    entity = _get_quote_in_scope(db, quote_id, tenant_no=tenant_no, user_info=user_info)
     if not entity:
         raise ValueError("报价单不存在")
     if entity.status == "invalid":
@@ -1221,6 +1258,7 @@ def statistics_overview(
     owner_user_id: str = None,
     begin_time: str = None,
     end_time: str = None,
+    user_info=None,
 ) -> Dict[str, Any]:
     """Java: statisticsOverview"""
     q = db.query(CrmQuote).filter(CrmQuote.tenant_no == tenant_no, CrmQuote.del_flag == 0)
@@ -1232,6 +1270,8 @@ def statistics_overview(
         q = q.filter(CrmQuote.create_time >= begin_dt)
     if end_dt:
         q = q.filter(CrmQuote.create_time <= end_dt)
+    if user_info:
+        q = apply_crm_owner_scope(q, CrmQuote, user_info, db, tenant_no)
 
     all_quotes = q.all()
     total_count = len(all_quotes)
@@ -1273,28 +1313,21 @@ def statistics_overview(
 def statistics_status(
     db: Session,
     tenant_no: str,
+    user_info=None,
 ) -> List[Dict[str, Any]]:
     """Java: statisticsStatus"""
     status_array = ("draft", "submitted", "accepted", "rejected", "expired", "contract")
     status_names = ("草稿", "已提交", "已接受", "已拒绝", "已过期", "已转合同")
 
-    total = (
-        db.query(CrmQuote)
-        .filter(CrmQuote.tenant_no == tenant_no, CrmQuote.del_flag == 0)
-        .count()
-    )
+    base_q = db.query(CrmQuote).filter(CrmQuote.tenant_no == tenant_no, CrmQuote.del_flag == 0)
+    if user_info:
+        base_q = apply_crm_owner_scope(base_q, CrmQuote, user_info, db, tenant_no)
+    total = base_q.count()
 
     result = []
     for status, status_name in zip(status_array, status_names):
-        rows = (
-            db.query(CrmQuote)
-            .filter(
-                CrmQuote.tenant_no == tenant_no,
-                CrmQuote.del_flag == 0,
-                CrmQuote.status == status,
-            )
-            .all()
-        )
+        q = base_q.filter(CrmQuote.status == status)
+        rows = q.all()
         count = len(rows)
         amount = sum(_decimal_value(r.final_amount) for r in rows)
         percent = (count * 100.0 / total) if total else 0.0
@@ -1326,6 +1359,7 @@ def statistics_trend(
     period_type: str = "month",
     begin_time: str = None,
     end_time: str = None,
+    user_info=None,
 ) -> List[Dict[str, Any]]:
     """Java: statisticsTrend"""
     q = db.query(CrmQuote).filter(CrmQuote.tenant_no == tenant_no, CrmQuote.del_flag == 0)
@@ -1335,6 +1369,8 @@ def statistics_trend(
         q = q.filter(CrmQuote.create_time >= begin_dt)
     if end_dt:
         q = q.filter(CrmQuote.create_time <= end_dt)
+    if user_info:
+        q = apply_crm_owner_scope(q, CrmQuote, user_info, db, tenant_no)
 
     all_quotes = q.all()
     if not all_quotes:

+ 5 - 0
backend/app/crud/crm/quote_approval.py

@@ -44,8 +44,13 @@ def list_quote_approvals(
     quote_id: str,
     *,
     tenant_no: str = None,
+    user_info=None,
 ) -> List[Dict[str, Any]]:
     """Java: queryListByQuoteId(含审批人昵称)"""
+    if tenant_no and user_info:
+        from app.crud.crm.quote import _get_quote_in_scope
+
+        _get_quote_in_scope(db, quote_id, tenant_no=tenant_no, user_info=user_info)
     q = db.query(CrmQuoteApproval).filter(
         CrmQuoteApproval.quote_id == quote_id,
         CrmQuoteApproval.del_flag == 0,

+ 18 - 0
backend/app/crud/crm/quote_product.py

@@ -13,6 +13,13 @@ from app.models.crm_models import CrmQuoteProduct
 
 _FMT = "%Y-%m-%d %H:%M:%S"
 
+def _ensure_quote_parent_scope(db: Session, quote_id: str, tenant_no: str, user_info) -> None:
+    if user_info:
+        from app.crud.crm.quote import _get_quote_in_scope
+
+        _get_quote_in_scope(db, quote_id, tenant_no=tenant_no, user_info=user_info)
+
+
 
 def _decimal_value(value: Any, default: Decimal = Decimal("0")) -> Decimal:
     if value is None:
@@ -76,8 +83,11 @@ def list_quote_products(
     quote_id: str,
     *,
     tenant_no: str = None,
+    user_info=None,
 ) -> List[Dict[str, Any]]:
     """Java: queryListByQuoteId"""
+    if tenant_no and user_info:
+        _ensure_quote_parent_scope(db, quote_id, tenant_no, user_info)
     q = db.query(CrmQuoteProduct).filter(
         CrmQuoteProduct.quote_id == quote_id,
         CrmQuoteProduct.del_flag == 0,
@@ -104,8 +114,10 @@ def insert_quote_product(
     data: dict,
     user_id: str = None,
     dept_id: str = "",
+    user_info=None,
 ) -> CrmQuoteProduct:
     """Java: insertByBo"""
+    _ensure_quote_parent_scope(db, quote_id, tenant_no, user_info)
     price = _decimal_value(_get_field(data, "price"), Decimal("0"))
     quantity = _decimal_value(_get_field(data, "quantity"), Decimal("0"))
     discount = _decimal_value(_get_field(data, "discount"), Decimal("0"))
@@ -169,11 +181,14 @@ def update_quote_product(
     *,
     data: dict,
     user_id: str = None,
+    user_info=None,
 ) -> CrmQuoteProduct:
     """Java: updateByBo"""
     entity = get_quote_product(db, product_line_id)
     if not entity:
         raise ValueError("报价产品明细不存在")
+    if user_info:
+        _ensure_quote_parent_scope(db, entity.quote_id, entity.tenant_no, user_info)
 
     if "product_id" in data or "productId" in data:
         entity.product_id = (_get_field(data, "product_id", "productId") or entity.product_id or "").strip()
@@ -217,6 +232,7 @@ def soft_delete_quote_products(
     ids: List[str],
     *,
     validate_min_one: bool = True,
+    user_info=None,
 ) -> int:
     """Java: deleteWithValidByIds"""
     count = 0
@@ -224,6 +240,8 @@ def soft_delete_quote_products(
         entity = get_quote_product(db, pid)
         if not entity:
             continue
+        if user_info:
+            _ensure_quote_parent_scope(db, entity.quote_id, entity.tenant_no, user_info)
         if validate_min_one:
             sibling_count = (
                 db.query(CrmQuoteProduct)

+ 10 - 1
backend/app/crud/crm/quote_share_log.py

@@ -7,7 +7,6 @@ from typing import Any, Dict, List, Optional
 
 from sqlalchemy.orm import Session
 
-from app.core.utils import generate_uuid
 from app.models.crm_models import CrmQuoteShareLog
 
 _FMT = "%Y-%m-%d %H:%M:%S"
@@ -78,8 +77,13 @@ def list_share_logs(
     quote_id: str,
     *,
     tenant_no: str = None,
+    user_info=None,
 ) -> List[Dict[str, Any]]:
     """Java: queryLogsByQuoteId"""
+    if tenant_no and user_info:
+        from app.crud.crm.quote import _get_quote_in_scope
+
+        _get_quote_in_scope(db, quote_id, tenant_no=tenant_no, user_info=user_info)
     q = db.query(CrmQuoteShareLog).filter(
         CrmQuoteShareLog.quote_id == quote_id,
         CrmQuoteShareLog.del_flag == 0,
@@ -95,8 +99,13 @@ def share_log_statistics(
     quote_id: str,
     *,
     tenant_no: str = None,
+    user_info=None,
 ) -> Dict[str, Any]:
     """Java: statisticsVisits"""
+    if tenant_no and user_info:
+        from app.crud.crm.quote import _get_quote_in_scope
+
+        _get_quote_in_scope(db, quote_id, tenant_no=tenant_no, user_info=user_info)
     q = db.query(CrmQuoteShareLog).filter(
         CrmQuoteShareLog.quote_id == quote_id,
         CrmQuoteShareLog.del_flag == 0,

+ 18 - 0
backend/app/crud/crm/supplier_info.py

@@ -8,6 +8,10 @@ from typing import Any, Dict, List, Optional, Tuple
 from sqlalchemy.orm import Session
 
 from app.core.utils import generate_uuid
+from app.crud.crm.data_scope_helpers import (
+    apply_crm_create_by_scope,
+    ensure_crm_create_by_row_in_scope,
+)
 from app.models.crm_models import CrmSupplierInfo
 
 _FMT = "%Y-%m-%d %H:%M:%S"
@@ -74,12 +78,15 @@ def list_supplier_info(
     page_size: int = 10,
     supplier_name: str = None,
     status: str = None,
+    user_info=None,
 ) -> Tuple[List[Dict[str, Any]], int]:
     """Java: queryPageList"""
     q = db.query(CrmSupplierInfo).filter(
         CrmSupplierInfo.tenant_no == tenant_no,
         CrmSupplierInfo.del_flag == 0,
     )
+    if user_info:
+        q = apply_crm_create_by_scope(q, CrmSupplierInfo, user_info, db, tenant_no)
     q = _apply_list_filters(q, supplier_name=supplier_name, status=status)
     total = q.count()
     rows = (
@@ -97,12 +104,15 @@ def list_all_supplier_info(
     *,
     supplier_name: str = None,
     status: str = None,
+    user_info=None,
 ) -> List[Dict[str, Any]]:
     """Java: queryList(不分页,报价单下拉)"""
     q = db.query(CrmSupplierInfo).filter(
         CrmSupplierInfo.tenant_no == tenant_no,
         CrmSupplierInfo.del_flag == 0,
     )
+    if user_info:
+        q = apply_crm_create_by_scope(q, CrmSupplierInfo, user_info, db, tenant_no)
     q = _apply_list_filters(q, supplier_name=supplier_name, status=status)
     rows = q.order_by(CrmSupplierInfo.create_time.desc()).all()
     return [_row_to_dict(r) for r in rows]
@@ -168,12 +178,16 @@ def update_supplier_info(
     *,
     tenant_no: str = None,
     user_id: str = None,
+    user_info=None,
     **kwargs,
 ) -> CrmSupplierInfo:
     """Java: updateByBo"""
     obj = get_supplier_info(db, supplier_id, tenant_no=tenant_no)
     if not obj:
         raise ValueError("供方信息不存在")
+    ensure_crm_create_by_row_in_scope(
+        db, obj, user_info, obj.tenant_no, message="无权访问该供方信息"
+    )
 
     if "supplier_name" in kwargs and kwargs["supplier_name"] is not None:
         name = (kwargs["supplier_name"] or "").strip()
@@ -200,11 +214,15 @@ def soft_delete_supplier_info(
     *,
     tenant_no: str = None,
     user_id: str = None,
+    user_info=None,
 ) -> bool:
     """Java: deleteWithValidByIds(门户软删)"""
     obj = get_supplier_info(db, supplier_id, tenant_no=tenant_no)
     if not obj:
         raise ValueError("供方信息不存在")
+    ensure_crm_create_by_row_in_scope(
+        db, obj, user_info, obj.tenant_no, message="无权访问该供方信息"
+    )
     obj.del_flag = 1
     obj.update_by = user_id
     obj.update_time = datetime.now()