소스 검색

feat:费控管理

alphah 2 주 전
부모
커밋
d877fe0551
23개의 변경된 파일2840개의 추가작업 그리고 1099개의 파일을 삭제
  1. 16 9
      backend/app/plugin/module_payment/__init__.py
  2. 9 0
      backend/app/plugin/module_payment/department/service.py
  3. 10 1
      backend/app/plugin/module_payment/employee/service.py
  4. 13 4
      backend/app/plugin/module_payment/expense/__init__.py
  5. 214 9
      backend/app/plugin/module_payment/expense/institution/controller.py
  6. 4 2
      backend/app/plugin/module_payment/expense/institution/enums.py
  7. 91 0
      backend/app/plugin/module_payment/expense/institution/scope_sync.py
  8. 618 50
      backend/app/plugin/module_payment/expense/institution/service.py
  9. 118 147
      backend/app/plugin/module_payment/expense/quota/controller.py
  10. 326 354
      backend/app/plugin/module_payment/expense/quota/service.py
  11. 118 118
      backend/app/plugin/module_payment/expense/rule/controller.py
  12. 290 288
      backend/app/plugin/module_payment/expense/rule/service.py
  13. 36 29
      backend/tests/conftest.py
  14. 315 0
      backend/tests/test_institution.py
  15. 81 6
      frontend/src/api/module_payment/institution.ts
  16. 2 2
      frontend/src/views/module_payment/employee/components/EmployeeForm.vue
  17. 85 47
      frontend/src/views/module_payment/institution/components/InstitutionDetail.vue
  18. 62 13
      frontend/src/views/module_payment/institution/components/InstitutionForm.vue
  19. 38 0
      frontend/src/views/module_payment/institution/components/QuotaDetailDialog.vue
  20. 148 0
      frontend/src/views/module_payment/institution/components/QuotaList.vue
  21. 38 0
      frontend/src/views/module_payment/institution/components/RuleDetailDialog.vue
  22. 154 0
      frontend/src/views/module_payment/institution/components/RuleList.vue
  23. 54 20
      frontend/src/views/module_payment/institution/index.vue

+ 16 - 9
backend/app/plugin/module_payment/__init__.py

@@ -1,9 +1,16 @@
-from .employee import *
-from .enterprise import *
-from .expense import *
-from .notification import *
-from .points import *
-from .department import *
-from .openapi import *
-
-__all__ = ["employee", "enterprise", "expense", "notification", "points", "department", "openapi"]
+"""
+plugin/module_payment - 延迟导入,避免模块级全量 import 触发生产依赖链。
+"""
+import importlib
+
+_MODULES = ["employee", "enterprise", "expense", "notification", "points", "department", "openapi"]
+
+
+def __getattr__(name):
+    if name in _MODULES:
+        return importlib.import_module(f".{name}", __package__)
+    raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
+
+
+def __dir__():
+    return list(_MODULES)

+ 9 - 0
backend/app/plugin/module_payment/department/service.py

@@ -93,6 +93,15 @@ class DepartmentService:
     async def delete_department_service(
     async def delete_department_service(
         cls, auth: AuthSchema, department_id: str, enterprise_id: str
         cls, auth: AuthSchema, department_id: str, enterprise_id: str
     ) -> DepartmentOperationOutSchema:
     ) -> DepartmentOperationOutSchema:
+        """删除部门(停用联动:从所有引用该部门的费控制度中移除)"""
+        # 停用联动:从所有引用该部门的费控制度中移除
+        try:
+            from app.plugin.module_payment.expense.institution.scope_sync import remove_department_from_institution_scopes
+            await remove_department_from_institution_scopes(
+                auth=auth, enterprise_id=enterprise_id, department_id=department_id
+            )
+        except Exception as e:
+            log.warning(f"从制度移除停用部门失败(不影响主体操作): {e}")
         return DepartmentOperationOutSchema(
         return DepartmentOperationOutSchema(
             department_id=department_id,
             department_id=department_id,
             department_name=""
             department_name=""

+ 10 - 1
backend/app/plugin/module_payment/employee/service.py

@@ -92,7 +92,7 @@ class EmployeeService:
         )
         )
 
 
         create_data_dict = result_data.model_dump()
         create_data_dict = result_data.model_dump()
-        create_data_dict.update(data.model_dump())
+        create_data_dict.update(data.model_dump(exclude_none=True))
 
 
         # 自动创建系统用户记录
         # 自动创建系统用户记录
         user_crud = UserCRUD(auth)  
         user_crud = UserCRUD(auth)  
@@ -231,6 +231,15 @@ class EmployeeService:
         if not employee:
         if not employee:
             raise CustomException(msg=f"员工 {employee_id} 不存在")
             raise CustomException(msg=f"员工 {employee_id} 不存在")
 
 
+        # 解约联动:从所有引用该员工的费控制度中移除
+        try:
+            from app.plugin.module_payment.expense.institution.scope_sync import remove_employee_from_institution_scopes
+            await remove_employee_from_institution_scopes(
+                auth=auth, enterprise_id=enterprise_id, employee_id=employee_id
+            )
+        except Exception as e:
+            log.warning(f"从制度移除解约员工失败(不影响主体操作): {e}")
+
         # 先删除关联的用户
         # 先删除关联的用户
         if employee.user_id:
         if employee.user_id:
             user_service = UserCRUD(auth)
             user_service = UserCRUD(auth)

+ 13 - 4
backend/app/plugin/module_payment/expense/__init__.py

@@ -1,5 +1,14 @@
-from .institution import *
-from .rule import *
-from .quota import *
+"""延迟导入"""
+import importlib
 
 
-__all__ = ["institution", "rule", "quota"]
+_MODULES = ["institution", "rule", "quota"]
+
+
+def __getattr__(name):
+    if name in _MODULES:
+        return importlib.import_module(f".{name}", __package__)
+    raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
+
+
+def __dir__():
+    return list(_MODULES)

+ 214 - 9
backend/app/plugin/module_payment/expense/institution/controller.py

@@ -1,4 +1,5 @@
 from typing import Annotated
 from typing import Annotated
+import uuid
 
 
 from fastapi import APIRouter, Depends, Path, Query
 from fastapi import APIRouter, Depends, Path, Query
 from fastapi.responses import JSONResponse
 from fastapi.responses import JSONResponse
@@ -10,7 +11,7 @@ from app.core.logger import log
 from app.core.router_class import OperationLogRoute
 from app.core.router_class import OperationLogRoute
 from app.plugin.module_payment.expense.institution.schema import InstitutionListOutSchema
 from app.plugin.module_payment.expense.institution.schema import InstitutionListOutSchema
 
 
-from .service import InstitutionService
+from .service import InstitutionService, InstitutionScopeService, IssueruleService
 
 
 from alipay.aop.api.domain.AlipayEbppInvoiceInstitutionCreateModel import (
 from alipay.aop.api.domain.AlipayEbppInvoiceInstitutionCreateModel import (
     AlipayEbppInvoiceInstitutionCreateModel,
     AlipayEbppInvoiceInstitutionCreateModel,
@@ -41,17 +42,71 @@ InstitutionRouter = APIRouter(
 @InstitutionRouter.post(
 @InstitutionRouter.post(
     "",
     "",
     summary="创建费控制度",
     summary="创建费控制度",
-    description="创建费控制度 (alipay.ebpp.invoice.institution.create)",
-    # response_model=ResponseSchema[AlipayEbppInvoiceInstitutionCreateResponse],
+    description="创建费控制度。支持串联调用:创建制度→设置成员→创建发放规则",
 )
 )
 async def create_institution_controller(
 async def create_institution_controller(
     data: dict,
     data: dict,
     auth: Annotated[AuthSchema, Depends(AuthPermission(["module_payment:expense:institution:create"]))],
     auth: Annotated[AuthSchema, Depends(AuthPermission(["module_payment:expense:institution:create"]))],
 ) -> JSONResponse:
 ) -> JSONResponse:
-    """创建费控制度"""
+    """创建费控制度(含完整串联流程)"""
     institution_create_model = AlipayEbppInvoiceInstitutionCreateModel.from_alipay_dict(data)
     institution_create_model = AlipayEbppInvoiceInstitutionCreateModel.from_alipay_dict(data)
-    result = await InstitutionService.create_institution_service(auth=auth, data=institution_create_model)
-    log.info(f"创建费控制度成功: {institution_create_model.institution_name}, institution_id={result.institution_id}")
+    enterprise_id = data.get("enterprise_id", "")
+
+    # 解析适用成员数据
+    scope_data = None
+    adapter_type = data.get("applicable_scope")
+    if adapter_type and adapter_type != "NONE":
+        scope_data = {
+            "adapter_type": adapter_type,
+            "owner_type": data.get("scope_owner_type", "EMPLOYEE"),
+            "add_owner_id_list": data.get("scope_owner_id_list"),
+        }
+
+    # 解析发放规则数据
+    issuerule_data = None
+    if data.get("grant_mode") == "period":
+        period_type_raw = data.get("period_type", "monthly")
+        # 映射前端period_type到支付宝枚举
+        ISSUE_TYPE_MAP = {
+            "daily": "ISSUE_DAY",
+            "weekly": "ISSUE_WEEK",
+            "monthly": "ISSUE_MONTH",
+            "quarterly": "ISSUE_QUARTER",
+            "yearly": "ISSUE_YEAR",
+        }
+        issue_type = ISSUE_TYPE_MAP.get(period_type_raw, "ISSUE_MONTH")
+        amount = data.get("amount", 0)
+
+        # 有效时间配置
+        effective_time_type = data.get("effective_time_type", "unlimited")
+        if effective_time_type == "unlimited":
+            effective_period = '{"all": true}'
+        elif effective_time_type == "workday":
+            workday_start = data.get("workday_start_time", "00:00")
+            workday_end = data.get("workday_end_time", "23:59")
+            effective_period = f'{{"regular":{{"workday":[["{workday_start}","{workday_end}"]]}}}}'
+        else:
+            effective_period = '{"all": true}'
+
+        issuerule_data = {
+            "quota_type": "CAP",
+            "issue_type": issue_type,
+            "issue_amount_value": str(amount),
+            "issue_rule_name": data.get("name", "") + "-发放规则",
+            "effective_period": effective_period,
+            "invalid_mode": 1 if data.get("effective_time_type") == "unlimited" else 0,
+            "share_mode": 0,
+            "outer_source_id": data.get("outer_source_id") or str(uuid.uuid4()),
+        }
+
+    result = await InstitutionService.create_institution_full_flow(
+        auth=auth,
+        institution_model=institution_create_model,
+        enterprise_id=enterprise_id,
+        scope_data=scope_data,
+        issuerule_data=issuerule_data,
+    )
+    log.info(f"创建费控制度成功: institution_id={result.get('institution_id')}")
     return SuccessResponse(data=result, msg="创建费控制度成功")
     return SuccessResponse(data=result, msg="创建费控制度成功")
 
 
 
 
@@ -87,11 +142,34 @@ async def list_institution_controller(
     return SuccessResponse(data=result, msg="查询费控制度列表成功")
     return SuccessResponse(data=result, msg="查询费控制度列表成功")
 
 
 
 
+@InstitutionRouter.get(
+    "/{institution_id}",
+    summary="查询费控制度详情",
+    description="查询费控制度详情 (alipay.ebpp.invoice.institution.detailinfo.query),失败时降级到本地DB",
+)
+async def detail_institution_controller(
+    institution_id: Annotated[str, Path(description="制度ID")],
+    auth: Annotated[AuthSchema, Depends(AuthPermission(["module_payment:expense:institution:detail"]))],
+    enterprise_id: Annotated[str | None, Query(description="企业ID")] = None,
+) -> JSONResponse:
+    """查询费控制度详情"""
+    if not enterprise_id:
+        return SuccessResponse(data=None, msg="企业ID不能为空")
+
+    result = await InstitutionService.detailinfo_query_service(
+        auth=auth,
+        institution_id=institution_id,
+        enterprise_id=enterprise_id,
+    )
+    if result is None:
+        return SuccessResponse(data=None, msg="制度不存在")
+    return SuccessResponse(data=result, msg="查询费控制度详情成功")
+
+
 @InstitutionRouter.delete(
 @InstitutionRouter.delete(
     "",
     "",
     summary="删除费控制度",
     summary="删除费控制度",
     description="删除费控制度 (alipay.ebpp.invoice.institution.delete)",
     description="删除费控制度 (alipay.ebpp.invoice.institution.delete)",
-    # response_model=ResponseSchema[AlipayEbppInvoiceInstitutionDeleteResponse],
 )
 )
 async def delete_institution_controller(
 async def delete_institution_controller(
     data: dict,
     data: dict,
@@ -108,7 +186,6 @@ async def delete_institution_controller(
     "/modify",
     "/modify",
     summary="编辑费控制度",
     summary="编辑费控制度",
     description="编辑费控制度 (alipay.ebpp.invoice.institution.modify)",
     description="编辑费控制度 (alipay.ebpp.invoice.institution.modify)",
-    # response_model=ResponseSchema[AlipayEbppInvoiceInstitutionModifyResponse],
 )
 )
 async def modify_institution_controller(
 async def modify_institution_controller(
     data: dict,
     data: dict,
@@ -118,4 +195,132 @@ async def modify_institution_controller(
     institution_modify_model = AlipayEbppInvoiceInstitutionModifyModel(**data)
     institution_modify_model = AlipayEbppInvoiceInstitutionModifyModel(**data)
     result = await InstitutionService.modify_institution_service(auth=auth, data=institution_modify_model)
     result = await InstitutionService.modify_institution_service(auth=auth, data=institution_modify_model)
     log.info(f"编辑费控制度成功: institution_id={institution_modify_model.institution_id}")
     log.info(f"编辑费控制度成功: institution_id={institution_modify_model.institution_id}")
-    return SuccessResponse(data=result, msg="编辑费控制度成功")
+    return SuccessResponse(data=result, msg="编辑费控制度成功")
+
+
+# ========== 制度成员范围管理 ==========
+
+
+@InstitutionRouter.get(
+    "/{institution_id}/scope",
+    summary="查询制度成员范围",
+    description="查询制度下成员范围 (alipay.ebpp.invoice.institution.scopepageinfo.query)",
+)
+async def list_scope_controller(
+    institution_id: Annotated[str, Path(description="制度ID")],
+    auth: Annotated[AuthSchema, Depends(AuthPermission(["module_payment:expense:institution:scope:list"]))],
+    enterprise_id: Annotated[str | None, Query(description="企业ID")] = None,
+    owner_type: Annotated[str | None, Query(description="适配ID类型")] = None,
+    page_num: Annotated[int, Query(description="页码")] = 1,
+    page_size: Annotated[int, Query(description="每页条数")] = 20,
+) -> JSONResponse:
+    """查询制度成员"""
+    result = await InstitutionScopeService.scopepageinfo_query_service(
+        auth=auth,
+        institution_id=institution_id,
+        enterprise_id=enterprise_id,
+        page_num=page_num,
+        page_size=page_size,
+        owner_type=owner_type,
+    )
+    return SuccessResponse(data=result, msg="查询成功")
+
+
+@InstitutionRouter.post(
+    "/{institution_id}/scope",
+    summary="设置制度成员范围",
+    description="设置/修改制度成员范围 (alipay.ebpp.invoice.institution.scope.modify)",
+)
+async def modify_scope_controller(
+    institution_id: Annotated[str, Path(description="制度ID")],
+    data: dict,
+    auth: Annotated[AuthSchema, Depends(AuthPermission(["module_payment:expense:institution:scope:modify"]))],
+) -> JSONResponse:
+    """设置制度成员"""
+    result = await InstitutionScopeService.scope_modify_service(
+        auth=auth,
+        institution_id=institution_id,
+        data=data,
+    )
+    log.info(f"设置制度成员成功: institution_id={institution_id}, adapter_type={data.get('adapter_type')}")
+    return SuccessResponse(data=result, msg="设置成功")
+
+
+# ========== 自动额度发放规则管理 ==========
+
+
+@InstitutionRouter.post(
+    "/{institution_id}/issuerule",
+    summary="创建自动发放规则",
+    description="创建自动额度发放规则 (alipay.ebpp.invoice.issuerule.create)",
+)
+async def create_issuerule_controller(
+    institution_id: Annotated[str, Path(description="制度ID")],
+    data: dict,
+    auth: Annotated[AuthSchema, Depends(AuthPermission(["module_payment:expense:institution:issuerule:create"]))],
+) -> JSONResponse:
+    """创建自动发放规则"""
+    result = await IssueruleService.create_issuerule_service(
+        auth=auth,
+        institution_id=institution_id,
+        enterprise_id=data.get("enterprise_id", ""),
+        quota_type=data.get("quota_type", "CAP"),
+        issue_type=data.get("issue_type", "ISSUE_MONTH"),
+        issue_amount_value=data.get("issue_amount_value", "0"),
+        outer_source_id=data.get("outer_source_id"),
+        issue_rule_name=data.get("issue_rule_name"),
+        effective_period=data.get("effective_period"),
+        invalid_mode=data.get("invalid_mode"),
+        share_mode=data.get("share_mode"),
+    )
+    log.info(f"创建自动发放规则成功: institution_id={institution_id}")
+    return SuccessResponse(data=result, msg="创建自动发放规则成功")
+
+
+@InstitutionRouter.put(
+    "/{institution_id}/issuerule/{issue_rule_id}",
+    summary="编辑自动发放规则",
+    description="编辑自动额度发放规则 (alipay.ebpp.invoice.issuerule.modify)",
+)
+async def modify_issuerule_controller(
+    institution_id: Annotated[str, Path(description="制度ID")],
+    issue_rule_id: Annotated[str, Path(description="发放规则ID")],
+    data: dict,
+    auth: Annotated[AuthSchema, Depends(AuthPermission(["module_payment:expense:institution:issuerule:modify"]))],
+) -> JSONResponse:
+    result = await IssueruleService.modify_issuerule_service(
+        auth=auth,
+        institution_id=institution_id,
+        issue_rule_id=issue_rule_id,
+        enterprise_id=data.get("enterprise_id", ""),
+        quota_type=data.get("quota_type"),
+        issue_type=data.get("issue_type"),
+        issue_amount_value=data.get("issue_amount_value"),
+        issue_rule_name=data.get("issue_rule_name"),
+        effective=data.get("effective"),
+        effective_period=data.get("effective_period"),
+        invalid_mode=data.get("invalid_mode"),
+        share_mode=data.get("share_mode"),
+    )
+    log.info(f"编辑自动发放规则成功: issue_rule_id={issue_rule_id}")
+    return SuccessResponse(data=result, msg="编辑自动发放规则成功")
+
+
+@InstitutionRouter.delete(
+    "/{institution_id}/issuerule",
+    summary="删除自动发放规则",
+    description="删除自动额度发放规则 (alipay.ebpp.invoice.issuerule.delete)",
+)
+async def delete_issuerule_controller(
+    institution_id: Annotated[str, Path(description="制度ID")],
+    data: dict,
+    auth: Annotated[AuthSchema, Depends(AuthPermission(["module_payment:expense:institution:issuerule:delete"]))],
+) -> JSONResponse:
+    result = await IssueruleService.delete_issuerule_service(
+        auth=auth,
+        institution_id=institution_id,
+        issue_rule_id_list=data.get("issue_rule_id_list", []),
+        enterprise_id=data.get("enterprise_id", ""),
+    )
+    log.info(f"删除自动发放规则成功: institution_id={institution_id}")
+    return SuccessResponse(data=result, msg="删除自动发放规则成功")

+ 4 - 2
backend/app/plugin/module_payment/expense/institution/enums.py

@@ -2,8 +2,10 @@ import enum
 
 
 
 
 class InstitutionStatusEnum(str, enum.Enum):
 class InstitutionStatusEnum(str, enum.Enum):
-    INSTITUTION_CREATE = "INSTITUTION_CREATE"
-    INSTITUTION_DELETE = "INSTITUTION_DELETE"
+    INSTITUTION_CREATE = "INSTITUTION_CREATE"          # 已创建
+    INSTITUTION_EFFECTIVE = "INSTITUTION_EFFECTIVE"    # 已启用
+    INSTITUTION_INVALID = "INSTITUTION_INVALID"        # 已停用
+    INSTITUTION_DELETE = "INSTITUTION_DELETE"          # 已删除
 
 
 
 
 class ExpenseTypeEnum(str, enum.Enum):
 class ExpenseTypeEnum(str, enum.Enum):

+ 91 - 0
backend/app/plugin/module_payment/expense/institution/scope_sync.py

@@ -0,0 +1,91 @@
+"""
+费控制度成员联动同步工具
+
+用于部门停用/员工解约时自动移除相关制度中的成员引用。
+"""
+
+from app.api.v1.module_system.auth.schema import AuthSchema
+from app.core.alipay import AlipayClient
+from app.core.logger import log
+from app.plugin.module_payment.expense.institution.crud import InstitutionCRUD
+
+
+async def remove_department_from_institution_scopes(
+    auth: AuthSchema,
+    enterprise_id: str,
+    department_id: str,
+) -> None:
+    """
+    当部门被停用时,扫描所有引用该部门的制度,移除该部门
+
+    此方法被 department/service.py 的停用方法调用
+    """
+    try:
+        crud = InstitutionCRUD(auth)
+        # 查所有该企业下的有效制度
+        institutions = await crud.list(
+            search={"enterprise_id": enterprise_id, "status__ne": "INSTITUTION_DELETE"},
+            order_by=[{"id": "desc"}],
+        )
+        if not institutions:
+            return
+
+        for inst in institutions:
+            inst_id = inst.institution_id
+            if not inst_id:
+                continue
+
+            from .service import InstitutionScopeService
+
+            await InstitutionScopeService.scope_modify_service(
+                auth=auth,
+                institution_id=inst_id,
+                data={
+                    "enterprise_id": enterprise_id,
+                    "adapter_type": "EMPLOYEE_DEPARTMENT",
+                    "delete_owner_id_list": [department_id],
+                },
+            )
+            log.info(f"已从制度 {inst_id} 中移除停用部门 {department_id}")
+    except Exception as e:
+        log.error(f"移除部门失败(不影响主体操作): {e}")
+
+
+async def remove_employee_from_institution_scopes(
+    auth: AuthSchema,
+    enterprise_id: str,
+    employee_id: str,
+) -> None:
+    """
+    当员工被解约时,扫描所有按员工模式引用该员工的制度,移除该员工
+
+    此方法被 employee/service.py 的删除方法调用
+    """
+    try:
+        crud = InstitutionCRUD(auth)
+        institutions = await crud.list(
+            search={"enterprise_id": enterprise_id, "status__ne": "INSTITUTION_DELETE"},
+            order_by=[{"id": "desc"}],
+        )
+        if not institutions:
+            return
+
+        for inst in institutions:
+            inst_id = inst.institution_id
+            if not inst_id:
+                continue
+
+            from .service import InstitutionScopeService
+
+            await InstitutionScopeService.scope_modify_service(
+                auth=auth,
+                institution_id=inst_id,
+                data={
+                    "enterprise_id": enterprise_id,
+                    "adapter_type": "EMPLOYEE_SELECT",
+                    "delete_owner_id_list": [employee_id],
+                },
+            )
+            log.info(f"已从制度 {inst_id} 中移除解约员工 {employee_id}")
+    except Exception as e:
+        log.error(f"移除员工失败(不影响主体操作): {e}")

+ 618 - 50
backend/app/plugin/module_payment/expense/institution/service.py

@@ -1,3 +1,5 @@
+import asyncio
+
 from app.api.v1.module_system.auth.schema import AuthSchema
 from app.api.v1.module_system.auth.schema import AuthSchema
 from app.core.alipay import AlipayClient
 from app.core.alipay import AlipayClient
 from app.core.exceptions import CustomException
 from app.core.exceptions import CustomException
@@ -25,6 +27,15 @@ from alipay.aop.api.domain.AlipayEbppInvoiceInstitutionPageinfoQueryModel import
 from alipay.aop.api.response.AlipayEbppInvoiceInstitutionPageinfoQueryResponse import (
 from alipay.aop.api.response.AlipayEbppInvoiceInstitutionPageinfoQueryResponse import (
     AlipayEbppInvoiceInstitutionPageinfoQueryResponse,
     AlipayEbppInvoiceInstitutionPageinfoQueryResponse,
 )
 )
+from alipay.aop.api.request.AlipayEbppInvoiceInstitutionDetailinfoQueryRequest import (
+    AlipayEbppInvoiceInstitutionDetailinfoQueryRequest,
+)
+from alipay.aop.api.domain.AlipayEbppInvoiceInstitutionDetailinfoQueryModel import (
+    AlipayEbppInvoiceInstitutionDetailinfoQueryModel,
+)
+from alipay.aop.api.response.AlipayEbppInvoiceInstitutionDetailinfoQueryResponse import (
+    AlipayEbppInvoiceInstitutionDetailinfoQueryResponse,
+)
 from alipay.aop.api.request.AlipayEbppInvoiceInstitutionDeleteRequest import (
 from alipay.aop.api.request.AlipayEbppInvoiceInstitutionDeleteRequest import (
     AlipayEbppInvoiceInstitutionDeleteRequest,
     AlipayEbppInvoiceInstitutionDeleteRequest,
 )
 )
@@ -48,28 +59,29 @@ from alipay.aop.api.response.AlipayEbppInvoiceInstitutionModifyResponse import (
 class InstitutionService:
 class InstitutionService:
     """费控制度服务层"""
     """费控制度服务层"""
 
 
+    @classmethod
+    def _execute_alipay(cls, request):
+        """同步执行支付宝调用(通过线程池避免阻塞事件循环)"""
+        client = AlipayClient.get_client()
+        return client.execute(request)
+
     @classmethod
     @classmethod
     async def create_institution_service(
     async def create_institution_service(
         cls, auth: AuthSchema, data: AlipayEbppInvoiceInstitutionCreateModel
         cls, auth: AuthSchema, data: AlipayEbppInvoiceInstitutionCreateModel
     ) -> AlipayEbppInvoiceInstitutionCreateResponse:
     ) -> AlipayEbppInvoiceInstitutionCreateResponse:
         """
         """
-        创建费控制度
-
+        创建费控制度(仅调 institution.create,不包含串联流程)
         调用: alipay.ebpp.invoice.institution.create
         调用: alipay.ebpp.invoice.institution.create
         """
         """
-
         if data.enterprise_id is None:
         if data.enterprise_id is None:
             raise CustomException(msg="创建费控制度失败: 企业ID不能为空")
             raise CustomException(msg="创建费控制度失败: 企业ID不能为空")
-        if data.outer_source_id:
-            data.outer_source_id = None
 
 
         data.currency = 'CNY'
         data.currency = 'CNY'
 
 
         request = AlipayEbppInvoiceInstitutionCreateRequest()
         request = AlipayEbppInvoiceInstitutionCreateRequest()
         request.biz_model = data
         request.biz_model = data
 
 
-        client = AlipayClient.get_client()
-        response = client.execute(request)
+        response = await asyncio.to_thread(cls._execute_alipay, request)
 
 
         if not response:
         if not response:
             raise CustomException(msg="创建费控制度失败: 无响应")
             raise CustomException(msg="创建费控制度失败: 无响应")
@@ -81,30 +93,214 @@ class InstitutionService:
             log.error(f"支付宝接口调用失败: {result.code} - {result.msg}")
             log.error(f"支付宝接口调用失败: {result.code} - {result.msg}")
             raise CustomException(msg=f"创建费控制度失败: {result.msg}")
             raise CustomException(msg=f"创建费控制度失败: {result.msg}")
 
 
-        # 构建创建数据
+        return result
+
+    @classmethod
+    async def create_institution_full_flow(
+        cls,
+        auth: AuthSchema,
+        institution_model: AlipayEbppInvoiceInstitutionCreateModel,
+        enterprise_id: str,
+        scope_data: dict | None = None,
+        issuerule_data: dict | None = None,
+    ) -> dict:
+        """
+        创建费控制度(完整串联流程)
+
+        流程:
+        1. institution.create → 获取 institution_id
+        2. scope.modify ← 如有适用成员数据(scope_data)
+        3. issuerule.create ← 如为"按固定周期发放"(issuerule_data)
+        4. 保存到本地DB
+        """
+        # 第1步:创建制度
+        institution_result = await cls.create_institution_service(auth=auth, data=institution_model)
+        institution_id = institution_result.institution_id
+
+        try:
+            # 第2步:设置适用成员(如有)
+            scope_modified = False
+            if scope_data and scope_data.get("adapter_type") and scope_data.get("adapter_type") != "NONE":
+                await InstitutionScopeService.scope_modify_service(
+                    auth=auth,
+                    institution_id=institution_id,
+                    data={
+                        "enterprise_id": enterprise_id,
+                        "adapter_type": scope_data["adapter_type"],
+                        "owner_type": scope_data.get("owner_type"),
+                        "add_owner_id_list": scope_data.get("add_owner_id_list"),
+                    },
+                )
+                scope_modified = True
+                log.info(f"成员设置成功: institution_id={institution_id}")
+
+            # 第3步:创建自动发放规则(如为"按固定周期发放")
+            issue_rule_id = None
+            if issuerule_data:
+                issuerule_result = await IssueruleService.create_issuerule_service(
+                    auth=auth,
+                    institution_id=institution_id,
+                    enterprise_id=enterprise_id,
+                    quota_type=issuerule_data.get("quota_type", "CAP"),
+                    issue_type=issuerule_data.get("issue_type", "ISSUE_MONTH"),
+                    issue_amount_value=issuerule_data.get("issue_amount_value", "0"),
+                    outer_source_id=issuerule_data.get("outer_source_id"),
+                    issue_rule_name=issuerule_data.get("issue_rule_name"),
+                    effective_period=issuerule_data.get("effective_period"),
+                    invalid_mode=issuerule_data.get("invalid_mode", 0),
+                    share_mode=issuerule_data.get("share_mode", 0),
+                )
+                issue_rule_id = issuerule_result.get("issue_rule_id")
+                log.info(f"发放规则创建成功: institution_id={institution_id}, issue_rule_id={issue_rule_id}")
+
+        except Exception as e:
+            # 子步骤失败:删除已创建的支付宝制度(补偿事务)
+            log.error(f"创建串联流程失败: {e},开始回滚 institution_id={institution_id}")
+            try:
+                from alipay.aop.api.request.AlipayEbppInvoiceInstitutionDeleteRequest import (
+                    AlipayEbppInvoiceInstitutionDeleteRequest,
+                )
+                from alipay.aop.api.domain.AlipayEbppInvoiceInstitutionDeleteModel import (
+                    AlipayEbppInvoiceInstitutionDeleteModel,
+                )
+                rollback_model = AlipayEbppInvoiceInstitutionDeleteModel()
+                rollback_model.institution_id = institution_id
+                rollback_model.enterprise_id = enterprise_id
+                req = AlipayEbppInvoiceInstitutionDeleteRequest()
+                req.biz_model = rollback_model
+                await asyncio.to_thread(cls._execute_alipay, req)
+                log.info(f"回滚成功: 已删除 institution_id={institution_id}")
+            except Exception as rollback_err:
+                log.error(f"回滚失败: {rollback_err}")
+
+            raise
+
+        # 第4步:保存到本地DB
         create_data = InstitutionCreateSchema(
         create_data = InstitutionCreateSchema(
-            enterprise_id=data.enterprise_id,
-            institution_id=result.institution_id,
-            institution_name=getattr(data, 'institution_name', None),
-            institution_desc=getattr(data, 'institution_desc', None),
-            scene_type=getattr(data, 'scene_type', None),
-            expense_type=getattr(data, 'expense_type', None),
-            expense_sub_type=getattr(data, 'expense_sub_type', None),
-            status=InstitutionStatusEnum.INSTITUTION_CREATE.value,  # 初始状态
-            effective=getattr(data, 'effective', None),
-            effective_start_date=getattr(data, 'effective_start_date', None),
-            effective_end_date=getattr(data, 'effective_end_date', None),
-            consult_mode=getattr(data, 'consult_mode', None),
-            multi_employee_share_mode=getattr(data, 'multi_employee_share_mode', None),
-            currency=getattr(data, 'currency', None)
+            enterprise_id=enterprise_id,
+            institution_id=institution_id,
+            institution_name=getattr(institution_model, 'institution_name', None),
+            institution_desc=getattr(institution_model, 'institution_desc', None),
+            scene_type=getattr(institution_model, 'scene_type', None),
+            expense_type=getattr(institution_model, 'expense_type', None),
+            expense_sub_type=getattr(institution_model, 'expense_sub_type', None),
+            status=InstitutionStatusEnum.INSTITUTION_CREATE.value,
+            effective=getattr(institution_model, 'effective', None),
+            effective_start_date=getattr(institution_model, 'effective_start_date', None),
+            effective_end_date=getattr(institution_model, 'effective_end_date', None),
+            consult_mode=getattr(institution_model, 'consult_mode', None),
+            multi_employee_share_mode=getattr(institution_model, 'multi_employee_share_mode', None),
+            currency=getattr(institution_model, 'currency', None)
         )
         )
 
 
-        # 转换为字典并添加额外字段
         create_data_dict = create_data.model_dump(exclude_unset=True)
         create_data_dict = create_data.model_dump(exclude_unset=True)
+        if issue_rule_id:
+            create_data_dict["issue_rule_id"] = issue_rule_id
         crud = InstitutionCRUD(auth)
         crud = InstitutionCRUD(auth)
         await crud.create(create_data_dict)
         await crud.create(create_data_dict)
 
 
-        return result
+        return {
+            "institution_id": institution_id,
+            "scope_modified": scope_modified,
+            "issue_rule_id": issue_rule_id,
+        }
+
+    @classmethod
+    async def pageinfo_query_service(
+        cls,
+        auth: AuthSchema,
+        enterprise_id: str,
+        page_no: int = 1,
+        page_size: int = 20,
+        institution_name: str | None = None,
+    ) -> dict:
+        """
+        从支付宝查询费控制度列表
+        调用: alipay.ebpp.invoice.institution.pageinfo.query
+        失败时降级到本地DB
+        """
+        try:
+            model = AlipayEbppInvoiceInstitutionPageinfoQueryModel()
+            model.enterprise_id = enterprise_id
+            model.page_num = page_no
+            model.page_size = page_size
+            if institution_name:
+                model.institution_name = institution_name
+
+            req = AlipayEbppInvoiceInstitutionPageinfoQueryRequest()
+            req.biz_model = model
+
+            response = await asyncio.to_thread(cls._execute_alipay, req)
+
+            if response:
+                result = AlipayEbppInvoiceInstitutionPageinfoQueryResponse()
+                result.parse_response_content(response)
+
+                if result.is_success():
+                    return {
+                        "page_no": getattr(result, 'page_num', page_no) or page_no,
+                        "page_size": getattr(result, 'page_size', page_size) or page_size,
+                        "total": getattr(result, 'total_page_count', 0) or 0,
+                        "list": getattr(result, 'institution_list', []) or [],
+                    }
+
+            log.warning("支付宝 pageinfo.query 失败,降级到本地DB")
+        except Exception as e:
+            log.warning(f"支付宝 pageinfo.query 异常: {e},降级到本地DB")
+
+        # 降级:查本地DB
+        crud = InstitutionCRUD(auth)
+        search = {"enterprise_id": enterprise_id}
+        if institution_name:
+            search["institution_name"] = institution_name
+        offset = (page_no - 1) * page_size
+        return await crud.page(
+            offset=offset,
+            limit=page_size,
+            order_by=[{"id": "desc"}],
+            search=search,
+            out_schema=InstitutionListOutSchema,
+        )
+
+    @classmethod
+    async def detailinfo_query_service(
+        cls,
+        auth: AuthSchema,
+        institution_id: str,
+        enterprise_id: str,
+    ) -> dict | None:
+        """
+        从支付宝查询费控制度详情
+        调用: alipay.ebpp.invoice.institution.detailinfo.query
+        失败时降级到本地DB
+        """
+        try:
+            model = AlipayEbppInvoiceInstitutionDetailinfoQueryModel()
+            model.institution_id = institution_id
+            model.enterprise_id = enterprise_id
+
+            req = AlipayEbppInvoiceInstitutionDetailinfoQueryRequest()
+            req.biz_model = model
+
+            response = await asyncio.to_thread(cls._execute_alipay, req)
+
+            if response:
+                result = AlipayEbppInvoiceInstitutionDetailinfoQueryResponse()
+                result.parse_response_content(response)
+
+                if result.is_success():
+                    return result.to_alipay_dict()
+
+            log.warning("支付宝 detailinfo.query 失败,降级到本地DB")
+        except Exception as e:
+            log.warning(f"支付宝 detailinfo.query 异常: {e},降级到本地DB")
+
+        # 降级:查本地DB
+        crud = InstitutionCRUD(auth)
+        obj = await crud.get(institution_id=institution_id, enterprise_id=enterprise_id)
+        if obj:
+            return InstitutionListOutSchema.model_validate(obj).model_dump()
+        return None
 
 
     @classmethod
     @classmethod
     async def list_service(
     async def list_service(
@@ -116,7 +312,21 @@ class InstitutionService:
     ) -> dict:
     ) -> dict:
         """
         """
         查询费控制度列表
         查询费控制度列表
+        优先调支付宝,失败降级到本地DB
         """
         """
+        enterprise_id = (search or {}).get("enterprise_id", "")
+        institution_name = (search or {}).get("name") or (search or {}).get("institution_name")
+
+        if enterprise_id:
+            return await cls.pageinfo_query_service(
+                auth=auth,
+                enterprise_id=enterprise_id,
+                page_no=page_no,
+                page_size=page_size,
+                institution_name=institution_name,
+            )
+
+        # 无 enterprise_id 时直接查本地
         crud = InstitutionCRUD(auth)
         crud = InstitutionCRUD(auth)
         offset = (page_no - 1) * page_size
         offset = (page_no - 1) * page_size
         return await crud.page(
         return await crud.page(
@@ -133,14 +343,12 @@ class InstitutionService:
     ) -> AlipayEbppInvoiceInstitutionDeleteResponse:
     ) -> AlipayEbppInvoiceInstitutionDeleteResponse:
         """
         """
         删除费控制度
         删除费控制度
-
         调用: alipay.ebpp.invoice.institution.delete
         调用: alipay.ebpp.invoice.institution.delete
         """
         """
         request = AlipayEbppInvoiceInstitutionDeleteRequest()
         request = AlipayEbppInvoiceInstitutionDeleteRequest()
         request.biz_model = data
         request.biz_model = data
 
 
-        client = AlipayClient.get_client()
-        response = client.execute(request)
+        response = await asyncio.to_thread(cls._execute_alipay, request)
 
 
         if not response:
         if not response:
             raise CustomException(msg="删除费控制度失败: 无响应")
             raise CustomException(msg="删除费控制度失败: 无响应")
@@ -152,6 +360,18 @@ class InstitutionService:
             log.error(f"支付宝接口调用失败: {result.code} - {result.msg}")
             log.error(f"支付宝接口调用失败: {result.code} - {result.msg}")
             raise CustomException(msg=f"删除费控制度失败: {result.msg}")
             raise CustomException(msg=f"删除费控制度失败: {result.msg}")
 
 
+        # 同步删除本地记录
+        try:
+            crud = InstitutionCRUD(auth)
+            institution_id = getattr(data, 'institution_id', None)
+            if institution_id:
+                obj = await crud.get(institution_id=institution_id)
+                if obj:
+                    await crud.delete(ids=[obj.id])
+                    log.info(f"已删除本地记录: institution_id={institution_id}")
+        except Exception as e:
+            log.warning(f"删除本地记录失败(不影响支付宝侧): {e}")
+
         return result
         return result
 
 
     @classmethod
     @classmethod
@@ -160,7 +380,6 @@ class InstitutionService:
     ) -> AlipayEbppInvoiceInstitutionModifyResponse:
     ) -> AlipayEbppInvoiceInstitutionModifyResponse:
         """
         """
         编辑费控制度
         编辑费控制度
-
         调用: alipay.ebpp.invoice.institution.modify
         调用: alipay.ebpp.invoice.institution.modify
         """
         """
         if data.institution_id is None:
         if data.institution_id is None:
@@ -169,8 +388,7 @@ class InstitutionService:
         request = AlipayEbppInvoiceInstitutionModifyRequest()
         request = AlipayEbppInvoiceInstitutionModifyRequest()
         request.biz_model = data
         request.biz_model = data
 
 
-        client = AlipayClient.get_client()
-        response = client.execute(request)
+        response = await asyncio.to_thread(cls._execute_alipay, request)
 
 
         if not response:
         if not response:
             raise CustomException(msg="编辑费控制度失败: 无响应")
             raise CustomException(msg="编辑费控制度失败: 无响应")
@@ -182,23 +400,373 @@ class InstitutionService:
             log.error(f"支付宝接口调用失败: {result.code} - {result.msg}")
             log.error(f"支付宝接口调用失败: {result.code} - {result.msg}")
             raise CustomException(msg=f"编辑费控制度失败: {result.msg}")
             raise CustomException(msg=f"编辑费控制度失败: {result.msg}")
 
 
-        # 更新本地数据库状态
-        # crud = InstitutionCRUD(auth)
-        # update_data = {}
-        # if hasattr(data, 'institution_name') and data.institution_name:
-        #     update_data['institution_name'] = data.institution_name
-        # if hasattr(data, 'institution_desc') and data.institution_desc:
-        #     update_data['institution_desc'] = data.institution_desc
-        # if hasattr(data, 'effective') and data.effective is not None:
-        #     update_data['effective'] = data.effective
-        # if hasattr(data, 'effective_start_date') and data.effective_start_date:
-        #     update_data['effective_start_date'] = data.effective_start_date
-        # if hasattr(data, 'effective_end_date') and data.effective_end_date:
-        #     update_data['effective_end_date'] = data.effective_end_date
-        # if hasattr(data, 'consult_mode') and data.consult_mode:
-        #     update_data['consult_mode'] = data.consult_mode
-
-        # if update_data:
-        #     await crud.update({'institution_id': data.institution_id}, update_data)
-
-        return result
+        # 同步更新本地数据库状态
+        try:
+            crud = InstitutionCRUD(auth)
+            institution_id = getattr(data, 'institution_id', None)
+            if institution_id:
+                update_data = {}
+                if hasattr(data, 'institution_name') and data.institution_name:
+                    update_data['institution_name'] = data.institution_name
+                if hasattr(data, 'institution_desc') and data.institution_desc:
+                    update_data['institution_desc'] = data.institution_desc
+                if hasattr(data, 'effective') and data.effective is not None:
+                    update_data['effective'] = data.effective
+                    update_data['status'] = (
+                        InstitutionStatusEnum.INSTITUTION_EFFECTIVE.value
+                        if data.effective == "1"
+                        else InstitutionStatusEnum.INSTITUTION_INVALID.value
+                    )
+                if hasattr(data, 'effective_start_date') and data.effective_start_date:
+                    update_data['effective_start_date'] = data.effective_start_date
+                if hasattr(data, 'effective_end_date') and data.effective_end_date:
+                    update_data['effective_end_date'] = data.effective_end_date
+
+                if update_data:
+                    await crud.update_by_institution_id(institution_id, update_data)
+                    log.info(f"已更新本地记录: institution_id={institution_id}")
+        except Exception as e:
+            log.warning(f"更新本地记录失败(不影响支付宝侧): {e}")
+
+        return result
+
+
+class InstitutionScopeService:
+    """费控制度成员范围服务层"""
+
+    @classmethod
+    def _execute_alipay(cls, request):
+        """同步执行支付宝调用"""
+        client = AlipayClient.get_client()
+        return client.execute(request)
+
+    @classmethod
+    async def scope_modify_service(
+        cls,
+        auth: AuthSchema,
+        institution_id: str,
+        data: dict,
+    ) -> dict:
+        """
+        设置/修改制度成员范围
+        调用: alipay.ebpp.invoice.institution.scope.modify
+        """
+        try:
+            from alipay.aop.api.request.AlipayEbppInvoiceInstitutionScopeModifyRequest import (
+                AlipayEbppInvoiceInstitutionScopeModifyRequest,
+            )
+            from alipay.aop.api.domain.AlipayEbppInvoiceInstitutionScopeModifyModel import (
+                AlipayEbppInvoiceInstitutionScopeModifyModel,
+            )
+            from alipay.aop.api.response.AlipayEbppInvoiceInstitutionScopeModifyResponse import (
+                AlipayEbppInvoiceInstitutionScopeModifyResponse,
+            )
+        except ImportError:
+            raise CustomException(msg="支付宝SDK未正确安装(alipay-ebpp-invoice-institution-scope-modify)")
+
+        model = AlipayEbppInvoiceInstitutionScopeModifyModel()
+        model.institution_id = institution_id
+        model.enterprise_id = data.get("enterprise_id", "")
+        model.adapter_type = data.get("adapter_type", "EMPLOYEE_ALL")
+
+        if data.get("owner_type"):
+            model.owner_type = data["owner_type"]
+        if data.get("add_owner_id_list"):
+            model.add_owner_id_list = data["add_owner_id_list"]
+        if data.get("delete_owner_id_list"):
+            model.delete_owner_id_list = data["delete_owner_id_list"]
+
+        request = AlipayEbppInvoiceInstitutionScopeModifyRequest()
+        request.biz_model = model
+
+        response = await asyncio.to_thread(cls._execute_alipay, request)
+        if not response:
+            raise CustomException(msg="设置制度成员失败: 无响应")
+
+        result = AlipayEbppInvoiceInstitutionScopeModifyResponse()
+        result.parse_response_content(response)
+        if not result.is_success():
+            log.error(f"设置制度成员失败: {result.code} - {result.msg}")
+            raise CustomException(msg=f"设置制度成员失败: {result.msg}")
+
+        return {"result": True}
+
+    @classmethod
+    async def scopepageinfo_query_service(
+        cls,
+        auth: AuthSchema,
+        institution_id: str,
+        enterprise_id: str | None = None,
+        page_num: int = 1,
+        page_size: int = 20,
+        owner_type: str | None = None,
+    ) -> dict:
+        """
+        查询制度成员范围
+        调用: alipay.ebpp.invoice.institution.scopepageinfo.query
+        """
+        try:
+            from alipay.aop.api.request.AlipayEbppInvoiceInstitutionScopepageinfoQueryRequest import (
+                AlipayEbppInvoiceInstitutionScopepageinfoQueryRequest,
+            )
+            from alipay.aop.api.domain.AlipayEbppInvoiceInstitutionScopepageinfoQueryModel import (
+                AlipayEbppInvoiceInstitutionScopepageinfoQueryModel,
+            )
+            from alipay.aop.api.response.AlipayEbppInvoiceInstitutionScopepageinfoQueryResponse import (
+                AlipayEbppInvoiceInstitutionScopepageinfoQueryResponse,
+            )
+        except ImportError:
+            raise CustomException(msg="支付宝SDK未正确安装(alipay-ebpp-invoice-institution-scopepageinfo-query)")
+
+        model = AlipayEbppInvoiceInstitutionScopepageinfoQueryModel()
+        model.institution_id = institution_id
+        model.page_num = page_num
+        model.page_size = page_size
+        if enterprise_id:
+            model.enterprise_id = enterprise_id
+        if owner_type:
+            model.owner_type = owner_type
+
+        request = AlipayEbppInvoiceInstitutionScopepageinfoQueryRequest()
+        request.biz_model = model
+
+        response = await asyncio.to_thread(cls._execute_alipay, request)
+        if not response:
+            raise CustomException(msg="查询制度成员失败: 无响应")
+
+        result = AlipayEbppInvoiceInstitutionScopepageinfoQueryResponse()
+        result.parse_response_content(response)
+        if not result.is_success():
+            log.error(f"查询制度成员失败: {result.code} - {result.msg}")
+            raise CustomException(msg=f"查询制度成员失败: {result.msg}")
+
+        return {
+            "page_num": getattr(result, 'page_num', page_num) or page_num,
+            "page_size": getattr(result, 'page_size', page_size) or page_size,
+            "total_page_count": getattr(result, 'total_page_count', 0) or 0,
+            "adapter_type": getattr(result, 'adapter_type', None),
+            "owner_id_list": getattr(result, 'owner_id_list', []) or [],
+            "owner_open_id_list": getattr(result, 'onwer_open_id_list', []) or [],
+            "scope_info_list": [
+                {
+                    "adapter_type": getattr(result, 'adapter_type', None),
+                    "owner_id_list": getattr(result, 'owner_id_list', []) or [],
+                    "owner_open_id_list": getattr(result, 'onwer_open_id_list', []) or [],
+                }
+            ] if getattr(result, 'adapter_type', None) else [],
+        }
+
+
+class IssueruleService:
+    """自动额度发放规则服务层"""
+
+    ISSUE_TYPE_MAP = {
+        "daily": "ISSUE_DAY",
+        "weekly": "ISSUE_WEEK",
+        "monthly": "ISSUE_MONTH",
+        "quarterly": "ISSUE_QUARTER",
+        "yearly": "ISSUE_YEAR",
+    }
+
+    @classmethod
+    def _execute_alipay(cls, request):
+        client = AlipayClient.get_client()
+        return client.execute(request)
+
+    @classmethod
+    async def create_issuerule_service(
+        cls,
+        auth: AuthSchema,
+        institution_id: str,
+        enterprise_id: str,
+        quota_type: str,
+        issue_type: str,
+        issue_amount_value: str,
+        outer_source_id: str | None = None,
+        issue_rule_name: str | None = None,
+        effective_period: str | None = None,
+        invalid_mode: int | None = None,
+        share_mode: int | None = None,
+    ) -> dict:
+        """
+        创建自动额度发放规则
+        调用: alipay.ebpp.invoice.issuerule.create
+        """
+        try:
+            from alipay.aop.api.request.AlipayEbppInvoiceIssueruleCreateRequest import (
+                AlipayEbppInvoiceIssueruleCreateRequest,
+            )
+            from alipay.aop.api.domain.AlipayEbppInvoiceIssueruleCreateModel import (
+                AlipayEbppInvoiceIssueruleCreateModel,
+            )
+            from alipay.aop.api.response.AlipayEbppInvoiceIssueruleCreateResponse import (
+                AlipayEbppInvoiceIssueruleCreateResponse,
+            )
+        except ImportError:
+            raise CustomException(msg="支付宝SDK未正确安装(alipay-ebpp-invoice-issuerule-create)")
+
+        # 参数约束校验
+        if quota_type == "CAP" and invalid_mode is not None and invalid_mode != 1:
+            raise CustomException(msg="余额类型(CP)的发放规则必须为可累计(invalid_mode=1)")
+        if quota_type == "COUNT" and share_mode is not None and share_mode != 0:
+            raise CustomException(msg="次卡类型(COUNT)的发放规则不可转赠(share_mode=0)")
+
+        model = AlipayEbppInvoiceIssueruleCreateModel()
+        model.target_type = "INSTITUTION"
+        model.target_id = institution_id
+        model.quota_type = quota_type
+        model.issue_type = issue_type
+        model.issue_amount_value = issue_amount_value
+        model.enterprise_id = enterprise_id
+
+        if outer_source_id:
+            model.outer_source_id = outer_source_id
+        if issue_rule_name:
+            model.issue_rule_name = issue_rule_name
+        if effective_period:
+            model.effective_period = effective_period
+        if invalid_mode is not None:
+            model.invalid_mode = invalid_mode
+        if share_mode is not None:
+            model.share_mode = share_mode
+
+        request = AlipayEbppInvoiceIssueruleCreateRequest()
+        request.biz_model = model
+
+        response = await asyncio.to_thread(cls._execute_alipay, request)
+
+        if not response:
+            raise CustomException(msg="创建发放规则失败: 无响应")
+
+        result = AlipayEbppInvoiceIssueruleCreateResponse()
+        result.parse_response_content(response)
+
+        if not result.is_success():
+            log.error(f"创建发放规则失败: {result.code} - {result.msg}")
+            raise CustomException(msg=f"创建发放规则失败: {result.msg}")
+
+        return {
+            "issue_rule_id": getattr(result, 'issue_rule_id', None),
+        }
+
+    @classmethod
+    async def modify_issuerule_service(
+        cls,
+        auth: AuthSchema,
+        institution_id: str,
+        issue_rule_id: str,
+        enterprise_id: str,
+        quota_type: str | None = None,
+        issue_type: str | None = None,
+        issue_amount_value: str | None = None,
+        issue_rule_name: str | None = None,
+        effective: str | None = None,
+        effective_period: str | None = None,
+        invalid_mode: int | None = None,
+        share_mode: int | None = None,
+    ) -> dict:
+        """
+        编辑自动额度发放规则
+        调用: alipay.ebpp.invoice.issuerule.modify
+        """
+        try:
+            from alipay.aop.api.request.AlipayEbppInvoiceIssueruleModifyRequest import (
+                AlipayEbppInvoiceIssueruleModifyRequest,
+            )
+            from alipay.aop.api.domain.AlipayEbppInvoiceIssueruleModifyModel import (
+                AlipayEbppInvoiceIssueruleModifyModel,
+            )
+            from alipay.aop.api.response.AlipayEbppInvoiceIssueruleModifyResponse import (
+                AlipayEbppInvoiceIssueruleModifyResponse,
+            )
+        except ImportError:
+            raise CustomException(msg="支付宝SDK未正确安装(alipay-ebpp-invoice-issuerule-modify)")
+
+        model = AlipayEbppInvoiceIssueruleModifyModel()
+        model.target_type = "INSTITUTION"
+        model.target_id = institution_id
+        model.issue_rule_id = issue_rule_id
+        model.action = "MODIFY_BASIC_INFO"
+        model.enterprise_id = enterprise_id
+
+        if issue_rule_name:
+            model.issue_rule_name = issue_rule_name
+        if quota_type:
+            model.quota_type = quota_type
+        if issue_type:
+            model.issue_type = issue_type
+        if issue_amount_value:
+            model.issue_amount_value = issue_amount_value
+        if effective is not None:
+            model.effective = effective
+        if effective_period:
+            model.effective_period = effective_period
+        if invalid_mode is not None:
+            model.invalid_mode = invalid_mode
+        if share_mode is not None:
+            model.share_mode = share_mode
+
+        request = AlipayEbppInvoiceIssueruleModifyRequest()
+        request.biz_model = model
+
+        response = await asyncio.to_thread(cls._execute_alipay, request)
+
+        if not response:
+            raise CustomException(msg="编辑发放规则失败: 无响应")
+
+        result = AlipayEbppInvoiceIssueruleModifyResponse()
+        result.parse_response_content(response)
+
+        if not result.is_success():
+            log.error(f"编辑发放规则失败: {result.code} - {result.msg}")
+            raise CustomException(msg=f"编辑发放规则失败: {result.msg}")
+
+        return {"result": True}
+
+    @classmethod
+    async def delete_issuerule_service(
+        cls,
+        auth: AuthSchema,
+        institution_id: str,
+        issue_rule_id_list: list[str],
+        enterprise_id: str,
+    ) -> dict:
+        """
+        删除自动额度发放规则
+        调用: alipay.ebpp.invoice.issuerule.delete
+        """
+        try:
+            from alipay.aop.api.request.AlipayEbppInvoiceIssueruleDeleteRequest import (
+                AlipayEbppInvoiceIssueruleDeleteRequest,
+            )
+            from alipay.aop.api.domain.AlipayEbppInvoiceIssueruleDeleteModel import (
+                AlipayEbppInvoiceIssueruleDeleteModel,
+            )
+            from alipay.aop.api.response.AlipayEbppInvoiceIssueruleDeleteResponse import (
+                AlipayEbppInvoiceIssueruleDeleteResponse,
+            )
+        except ImportError:
+            raise CustomException(msg="支付宝SDK未正确安装(alipay-ebpp-invoice-issuerule-delete)")
+
+        model = AlipayEbppInvoiceIssueruleDeleteModel()
+        model.target_type = "INSTITUTION"
+        model.target_id = institution_id
+        model.issue_rule_id_list = issue_rule_id_list
+        model.enterprise_id = enterprise_id
+
+        request = AlipayEbppInvoiceIssueruleDeleteRequest()
+        request.biz_model = model
+
+        response = await asyncio.to_thread(cls._execute_alipay, request)
+
+        if not response:
+            raise CustomException(msg="删除发放规则失败: 无响应")
+
+        result = AlipayEbppInvoiceIssueruleDeleteResponse()
+        result.parse_response_content(response)
+
+        if not result.is_success():
+            log.error(f"删除发放规则失败: {result.code} - {result.msg}")
+            raise CustomException(msg=f"删除发放规则失败: {result.msg}")
+
+        return {"result": True}

+ 118 - 147
backend/app/plugin/module_payment/expense/quota/controller.py

@@ -1,27 +1,26 @@
-# from typing import Annotated
+from typing import Annotated
 
 
 from fastapi import APIRouter, Depends, Path, Query
 from fastapi import APIRouter, Depends, Path, Query
-# from fastapi.responses import JSONResponse
+from fastapi.responses import JSONResponse
 
 
-# from app.api.v1.module_system.auth.schema import AuthSchema
-# from app.common.response import ResponseSchema, SuccessResponse
-# from app.core.dependencies import AuthPermission
-# from app.core.logger import log
+from app.api.v1.module_system.auth.schema import AuthSchema
+from app.common.response import ResponseSchema, SuccessResponse
+from app.core.dependencies import AuthPermission
+from app.core.logger import log
 from app.core.router_class import OperationLogRoute
 from app.core.router_class import OperationLogRoute
 
 
-# from .schema import (
-#     ExpenseQuotaCreateSchema,
-#     ExpenseQuotaDeleteSchema,
-#     ExpenseQuotaModifySchema,
-#     ExpenseQuotaQueryOutSchema,
-#     ExpenseQuotaQuerySchema,
-#     QuotaCreateSchema,
-#     QuotaListOutSchema,
-#     QuotaOperationOutSchema,
-#     QuotaOutSchema,
-#     QuotaUpdateSchema,
-# )
-# from .service import QuotaService
+from .schema import (
+    ExpenseQuotaCreateSchema,
+    ExpenseQuotaDeleteSchema,
+    ExpenseQuotaModifySchema,
+    ExpenseQuotaQuerySchema,
+    QuotaCreateSchema,
+    QuotaListOutSchema,
+    QuotaOperationOutSchema,
+    QuotaOutSchema,
+    QuotaUpdateSchema,
+)
+from .service import QuotaService
 
 
 QuotaRouter = APIRouter(
 QuotaRouter = APIRouter(
     route_class=OperationLogRoute,
     route_class=OperationLogRoute,
@@ -30,131 +29,103 @@ QuotaRouter = APIRouter(
 )
 )
 
 
 
 
-# @QuotaRouter.post(
-#     "/expense/create",
-#     summary="创建余额/点券",
-#     description="创建余额或点券 (alipay.ebpp.invoice.expensecontrol.quota.create)",
-#     response_model=ResponseSchema[QuotaOperationOutSchema],
-# )
-# async def create_expense_quota_controller(
-#     data: ExpenseQuotaCreateSchema,
-#     auth: Annotated[AuthSchema, Depends(AuthPermission(["module_payment:expense:quota:expense:create"]))],
-# ) -> JSONResponse:
-#     result = await QuotaService.create_expense_quota_service(auth=auth, data=data)
-#     log.info(f"创建余额/点券成功: out_biz_no={result.out_biz_no}")
-#     return SuccessResponse(data=result, msg="创建余额/点券成功")
-
-
-# @QuotaRouter.post(
-#     "/expense/query",
-#     summary="查询余额/点券",
-#     description="查询余额或点券 (alipay.ebpp.invoice.expensecontrol.quota.query)",
-#     response_model=ResponseSchema[ExpenseQuotaQueryOutSchema],
-# )
-# async def query_expense_quota_controller(
-#     data: ExpenseQuotaQuerySchema,
-#     auth: Annotated[AuthSchema, Depends(AuthPermission(["module_payment:expense:quota:expense:query"]))],
-# ) -> JSONResponse:
-#     result = await QuotaService.query_expense_quota_service(auth=auth, data=data)
-#     log.info(f"查询余额/点券成功")
-#     return SuccessResponse(data=result, msg="查询余额/点券成功")
-
-
-# @QuotaRouter.put(
-#     "/expense/{out_biz_no}",
-#     summary="修改余额/点券",
-#     description="修改余额或点券 (alipay.ebpp.invoice.expensecontrol.quota.modify)",
-#     response_model=ResponseSchema[QuotaOperationOutSchema],
-# )
-# async def modify_expense_quota_controller(
-#     out_biz_no: Annotated[str, Path(description="外部业务编号")],
-#     data: ExpenseQuotaModifySchema,
-#     auth: Annotated[AuthSchema, Depends(AuthPermission(["module_payment:expense:quota:expense:modify"]))],
-# ) -> JSONResponse:
-#     result = await QuotaService.modify_expense_quota_service(auth=auth, out_biz_no=out_biz_no, data=data)
-#     log.info(f"修改余额/点券成功: {out_biz_no}")
-#     return SuccessResponse(data=result, msg="修改余额/点券成功")
-
-
-# @QuotaRouter.delete(
-#     "/expense/{out_biz_no}",
-#     summary="删除额度",
-#     description="删除额度 (alipay.ebpp.invoice.expensecontrol.quota.delete)",
-#     response_model=ResponseSchema[QuotaOperationOutSchema],
-# )
-# async def delete_expense_quota_controller(
-#     out_biz_no: Annotated[str, Path(description="外部业务编号")],
-#     data: ExpenseQuotaDeleteSchema,
-#     auth: Annotated[AuthSchema, Depends(AuthPermission(["module_payment:expense:quota:expense:delete"]))],
-# ) -> JSONResponse:
-#     result = await QuotaService.delete_expense_quota_service(auth=auth, out_biz_no=out_biz_no, data=data)
-#     log.info(f"删除额度成功: {out_biz_no}")
-#     return SuccessResponse(data=result, msg="删除额度成功")
-
-
-# @QuotaRouter.post(
-#     "",
-#     summary="创建额度",
-#     description="创建/发放额度",
-#     response_model=ResponseSchema[QuotaOperationOutSchema],
-# )
-# async def create_quota_controller(
-#     data: QuotaCreateSchema,
-#     auth: Annotated[AuthSchema, Depends(AuthPermission(["module_payment:expense:quota:create"]))],
-# ) -> JSONResponse:
-#     result = await QuotaService.create_quota_service(auth=auth, data=data)
-#     log.info(f"创建额度成功: out_biz_no={result.out_biz_no}")
-#     return SuccessResponse(data=result, msg="创建额度成功")
-
-
-# @QuotaRouter.get(
-#     "",
-#     summary="查询额度列表",
-#     description="分页查询额度列表",
-#     response_model=ResponseSchema[QuotaListOutSchema],
-# )
-# async def list_quota_controller(
-#     auth: Annotated[AuthSchema, Depends(AuthPermission(["module_payment:expense:quota:list"]))],
-#     page_no: Annotated[int, Query(description="页码")] = 1,
-#     page_size: Annotated[int, Query(description="每页数量")] = 20,
-#     employee_id: Annotated[str | None, Query(description="员工ID")] = None,
-#     institution_id: Annotated[str | None, Query(description="制度ID")] = None,
-# ) -> JSONResponse:
-#     search = {}
-#     if employee_id:
-#         search["employee_id"] = employee_id
-#     if institution_id:
-#         search["institution_id"] = institution_id
-
-#     result = await QuotaService.list_service(
-#         auth=auth, page_no=page_no, page_size=page_size, search=search
-#     )
-#     return SuccessResponse(data=result, msg="查询额度列表成功")
-
-
-# @QuotaRouter.get(
-#     "/{out_biz_no}",
-#     summary="查询额度详情",
-#     response_model=ResponseSchema[QuotaOutSchema],
-# )
-# async def get_detail_controller(
-#     out_biz_no: Annotated[str, Path(description="外部业务编号")],
-#     auth: Annotated[AuthSchema, Depends(AuthPermission(["module_payment:expense:quota:detail"]))],
-# ) -> JSONResponse:
-#     result = await QuotaService.detail_service(auth=auth, out_biz_no=out_biz_no)
-#     return SuccessResponse(data=result, msg="查询额度详情成功")
-
-
-# @QuotaRouter.put(
-#     "/{out_biz_no}",
-#     summary="更新额度",
-#     response_model=ResponseSchema[QuotaOperationOutSchema],
-# )
-# async def update_controller(
-#     out_biz_no: Annotated[str, Path(description="外部业务编号")],
-#     data: QuotaUpdateSchema,
-#     auth: Annotated[AuthSchema, Depends(AuthPermission(["module_payment:expense:quota:update"]))],
-# ) -> JSONResponse:
-#     result = await QuotaService.update_service(auth=auth, out_biz_no=out_biz_no, data=data)
-#     log.info(f"更新额度成功: {out_biz_no}")
-#     return SuccessResponse(data=result, msg="更新额度成功")
+@QuotaRouter.post(
+    "/expense/create",
+    summary="创建余额/点券",
+    description="创建余额或点券 (alipay.ebpp.invoice.expensecontrol.quota.create)",
+    response_model=ResponseSchema[QuotaOperationOutSchema],
+)
+async def create_expense_quota_controller(
+    data: ExpenseQuotaCreateSchema,
+    auth: Annotated[AuthSchema, Depends(AuthPermission(["module_payment:expense:quota:expense:create"]))],
+) -> JSONResponse:
+    result = await QuotaService.create_expense_quota_service(auth=auth, data=data)
+    log.info(f"创建余额/点券成功: out_biz_no={result.out_biz_no}")
+    return SuccessResponse(data=result, msg="创建余额/点券成功")
+
+
+@QuotaRouter.post(
+    "/expense/query",
+    summary="查询余额/点券",
+    description="查询余额或点券 (alipay.ebpp.invoice.expensecontrol.quota.query)",
+    response_model=ResponseSchema[ExpenseQuotaQuerySchema],
+)
+async def query_expense_quota_controller(
+    data: ExpenseQuotaQuerySchema,
+    auth: Annotated[AuthSchema, Depends(AuthPermission(["module_payment:expense:quota:expense:query"]))],
+) -> JSONResponse:
+    result = await QuotaService.query_expense_quota_service(auth=auth, data=data)
+    log.info(f"查询余额/点券成功")
+    return SuccessResponse(data=result, msg="查询余额/点券成功")
+
+
+@QuotaRouter.put(
+    "/expense/{out_biz_no}",
+    summary="修改余额/点券",
+    description="修改余额或点券 (alipay.ebpp.invoice.expensecontrol.quota.modify)",
+    response_model=ResponseSchema[QuotaOperationOutSchema],
+)
+async def modify_expense_quota_controller(
+    out_biz_no: Annotated[str, Path(description="外部业务编号")],
+    data: ExpenseQuotaModifySchema,
+    auth: Annotated[AuthSchema, Depends(AuthPermission(["module_payment:expense:quota:expense:modify"]))],
+) -> JSONResponse:
+    result = await QuotaService.modify_expense_quota_service(auth=auth, out_biz_no=out_biz_no, data=data)
+    log.info(f"修改余额/点券成功: {out_biz_no}")
+    return SuccessResponse(data=result, msg="修改余额/点券成功")
+
+
+@QuotaRouter.delete(
+    "/expense/{out_biz_no}",
+    summary="删除额度",
+    description="删除额度 (alipay.ebpp.invoice.expensecontrol.quota.delete)",
+    response_model=ResponseSchema[QuotaOperationOutSchema],
+)
+async def delete_expense_quota_controller(
+    out_biz_no: Annotated[str, Path(description="外部业务编号")],
+    data: ExpenseQuotaDeleteSchema,
+    auth: Annotated[AuthSchema, Depends(AuthPermission(["module_payment:expense:quota:expense:delete"]))],
+) -> JSONResponse:
+    result = await QuotaService.delete_expense_quota_service(auth=auth, out_biz_no=out_biz_no, data=data)
+    log.info(f"删除额度成功: {out_biz_no}")
+    return SuccessResponse(data=result, msg="删除额度成功")
+
+
+@QuotaRouter.post(
+    "",
+    summary="创建额度",
+    description="创建额度",
+    response_model=ResponseSchema[QuotaOperationOutSchema],
+)
+async def create_quota_controller(
+    data: QuotaCreateSchema,
+    auth: Annotated[AuthSchema, Depends(AuthPermission(["module_payment:expense:quota:create"]))],
+) -> JSONResponse:
+    result = await QuotaService.create_quota_service(auth=auth, data=data)
+    log.info(f"创建额度成功: out_biz_no={result.out_biz_no}")
+    return SuccessResponse(data=result, msg="创建额度成功")
+
+
+@QuotaRouter.get(
+    "",
+    summary="查询额度列表",
+    description="分页查询额度列表",
+    response_model=ResponseSchema[QuotaListOutSchema],
+)
+async def list_quota_controller(
+    auth: Annotated[AuthSchema, Depends(AuthPermission(["module_payment:expense:quota:list"]))],
+    page_no: Annotated[int, Query(description="页码")] = 1,
+    page_size: Annotated[int, Query(description="每页数量")] = 20,
+    institution_id: Annotated[str | None, Query(description="制度ID")] = None,
+    employee_id: Annotated[str | None, Query(description="员工ID")] = None,
+) -> JSONResponse:
+    search = {}
+    if institution_id:
+        search["institution_id"] = institution_id
+    if employee_id:
+        search["employee_id"] = employee_id
+
+    result = await QuotaService.list_service(
+        auth=auth, page_no=page_no, page_size=page_size, search=search
+    )
+    return SuccessResponse(data=result, msg="查询额度列表成功")

+ 326 - 354
backend/app/plugin/module_payment/expense/quota/service.py

@@ -1,354 +1,326 @@
-# from app.api.v1.module_system.auth.schema import AuthSchema
-# from app.core.alipay import AlipayClient
-# from app.core.exceptions import CustomException
-# from app.core.logger import log
-# from app.utils.snowflake import get_snowflake_id
-
-# from .enums import QuotaStatusEnum
-# from .schema import (
-#     ExpenseQuotaCreateSchema,
-#     ExpenseQuotaDeleteSchema,
-#     ExpenseQuotaModifySchema,
-#     ExpenseQuotaQueryOutSchema,
-#     ExpenseQuotaQuerySchema,
-#     QuotaCreateSchema,
-#     QuotaDetailInfoSchema,
-#     QuotaListOutSchema,
-#     QuotaOperationOutSchema,
-#     QuotaOutSchema,
-#     QuotaUpdateSchema,
-# )
-
-
-# class QuotaService:
-#     """额度服务层"""
-
-#     @classmethod
-#     async def create_expense_quota_service(
-#         cls, auth: AuthSchema, data: ExpenseQuotaCreateSchema
-#     ) -> QuotaOperationOutSchema:
-#         """
-#         创建余额/点券
-
-#         调用: alipay.ebpp.invoice.expensecontrol.quota.create
-#         """
-#         crud = QuotaCRUD(auth)
-
-#         out_biz_no = data.outer_source_id or str(get_snowflake_id())
-
-#         try:
-#             from alipay.aop.api.request.AlipayEbppInvoiceExpensecontrolQuotaCreateRequest import (
-#                 AlipayEbppInvoiceExpensecontrolQuotaCreateRequest,
-#             )
-#             from alipay.aop.api.domain.AlipayEbppInvoiceExpensecontrolQuotaCreateModel import (
-#                 AlipayEbppInvoiceExpensecontrolQuotaCreateModel,
-#             )
-#             from alipay.aop.api.domain.IssueQuotaTarget import (
-#                 IssueQuotaTarget,
-#             )
-#             from alipay.aop.api.response.AlipayEbppInvoiceExpensecontrolQuotaCreateResponse import (
-#                 AlipayEbppInvoiceExpensecontrolQuotaCreateResponse,
-#             )
-#         except ImportError:
-#             raise CustomException(msg="支付宝SDK未正确安装,请检查alipay-sdk-python依赖")
-
-#         model = AlipayEbppInvoiceExpensecontrolQuotaCreateModel()
-#         model.target_type = data.target_type
-#         model.target_id = data.target_id
-#         model.enterprise_id = data.enterprise_id
-#         model.outer_source_id = out_biz_no
-#         model.quota_type = data.quota_type or "CAP"
-#         model.share_mode = data.share_mode or "0"
-
-#         if data.effective_start_date:
-#             model.effective_start_date = data.effective_start_date.strftime("%Y-%m-%d %H:%M:%S")
-#         if data.effective_end_date:
-#             model.effective_end_date = data.effective_end_date.strftime("%Y-%m-%d %H:%M:%S")
-#         if data.issue_name:
-#             model.issue_name = data.issue_name
-#         if data.issue_desc:
-#             model.issue_desc = data.issue_desc
-#         if data.issue_quota_target_list:
-#             target_list = []
-#             for item in data.issue_quota_target_list:
-#                 target = IssueQuotaTarget()
-#                 target.owner_type = item.owner_type
-#                 target.owner_id = item.owner_id
-#                 target.quota = item.quota
-#                 if item.amount is not None:
-#                     target.amount = item.amount
-#                 target_list.append(target)
-#             model.issue_quota_target_list = target_list
-
-#         request = AlipayEbppInvoiceExpensecontrolQuotaCreateRequest()
-#         request.biz_model = model
-
-#         client = AlipayClient.get_client()
-#         response = client.execute(request)
-
-#         if not response:
-#             raise CustomException(msg="创建余额/点券失败: 无响应")
-
-#         result = AlipayEbppInvoiceExpensecontrolQuotaCreateResponse()
-#         result.parse_response_content(response)
-
-#         if not result.is_success():
-#             log.error(f"支付宝接口调用失败: {result.code} - {result.msg}")
-#             raise CustomException(msg=f"创建余额/点券失败: {result.msg}")
-
-#         quota_data = data.model_dump(exclude_none=True)
-#         quota_data["out_biz_no"] = out_biz_no
-#         quota_data["status"] = QuotaStatusEnum.QUOTA_ACTIVE.value
-
-#         if result.quota_id:
-#             quota_data["quota_id"] = result.quota_id
-
-#         quota = await crud.create(quota_data)
-#         if not quota:
-#             raise CustomException(msg="创建额度记录失败")
-
-#         return QuotaOperationOutSchema(out_biz_no=out_biz_no, quota_id=result.quota_id)
-
-#     @classmethod
-#     async def create_quota_service(
-#         cls, auth: AuthSchema, data: QuotaCreateSchema
-#     ) -> QuotaOperationOutSchema:
-#         """
-#         创建额度
-
-#         调用: alipay.ebpp.invoice.expensecontrol.quota.create
-#         """
-#         crud = QuotaCRUD(auth)
-#         out_biz_no = str(get_snowflake_id())
-
-#         quota_data = data.model_dump(exclude_none=True)
-#         quota_data["out_biz_no"] = out_biz_no
-#         quota_data["status"] = QuotaStatusEnum.QUOTA_ACTIVE.value
-#         if quota_data.get("available_amount") is None:
-#             quota_data["available_amount"] = quota_data.get("total_amount", 0)
-
-#         quota = await crud.create(quota_data)
-#         if not quota:
-#             raise CustomException(msg="创建额度记录失败")
-
-#         return QuotaOperationOutSchema(out_biz_no=out_biz_no, quota_id=quota.quota_id)
-
-#     @classmethod
-#     async def query_expense_quota_service(
-#         cls, auth: AuthSchema, data: ExpenseQuotaQuerySchema
-#     ) -> ExpenseQuotaQueryOutSchema:
-#         """
-#         查询余额/点券
-
-#         调用: alipay.ebpp.invoice.expensecontrol.quota.query
-#         """
-#         try:
-#             from alipay.aop.api.request.AlipayEbppInvoiceExpensecontrolQuotaQueryRequest import (
-#                 AlipayEbppInvoiceExpensecontrolQuotaQueryRequest,
-#             )
-#             from alipay.aop.api.domain.AlipayEbppInvoiceExpensecontrolQuotaQueryModel import (
-#                 AlipayEbppInvoiceExpensecontrolQuotaQueryModel,
-#             )
-#             from alipay.aop.api.response.AlipayEbppInvoiceExpensecontrolQuotaQueryResponse import (
-#                 AlipayEbppInvoiceExpensecontrolQuotaQueryResponse,
-#             )
-#         except ImportError:
-#             raise CustomException(msg="支付宝SDK未正确安装,请检查alipay-sdk-python依赖")
-
-#         model = AlipayEbppInvoiceExpensecontrolQuotaQueryModel()
-#         model.owner_type = data.owner_type
-#         model.page_size = data.page_size
-#         model.page_num = data.page_num
-
-#         if data.target_type:
-#             model.target_type = data.target_type
-#         if data.target_id:
-#             model.target_id = data.target_id
-#         if data.owner_id:
-#             model.owner_id = data.owner_id
-#         if data.owner_open_id:
-#             model.owner_open_id = data.owner_open_id
-#         if data.enterprise_id:
-#             model.enterprise_id = data.enterprise_id
-#         if data.quota_id_list:
-#             model.quota_id_list = data.quota_id_list
-#         if data.quota_type:
-#             model.quota_type = data.quota_type
-
-#         request = AlipayEbppInvoiceExpensecontrolQuotaQueryRequest()
-#         request.biz_model = model
-
-#         client = AlipayClient.get_client()
-#         response = client.execute(request)
-
-#         if not response:
-#             raise CustomException(msg="查询余额/点券失败: 无响应")
-
-#         result = AlipayEbppInvoiceExpensecontrolQuotaQueryResponse()
-#         result.parse_response_content(response)
-
-#         if not result.is_success():
-#             log.error(f"支付宝接口调用失败: {result.code} - {result.msg}")
-#             raise CustomException(msg=f"查询余额/点券失败: {result.msg}")
-
-#         return ExpenseQuotaQueryOutSchema(
-#             page_num=result.page_num or data.page_num,
-#             page_size=result.page_size or data.page_size,
-#             total_page_count=result.total_page_count or 0,
-#         )
-
-#     @classmethod
-#     async def modify_expense_quota_service(
-#         cls, auth: AuthSchema, out_biz_no: str, data: ExpenseQuotaModifySchema
-#     ) -> QuotaOperationOutSchema:
-#         """
-#         修改余额/点券
-
-#         调用: alipay.ebpp.invoice.expensecontrol.quota.modify
-#         """
-#         crud = QuotaCRUD(auth)
-#         quota = await crud.get_by_out_biz_no(out_biz_no)
-
-#         if not quota:
-#             raise CustomException(msg="额度不存在")
-
-#         try:
-#             from alipay.aop.api.request.AlipayEbppInvoiceExpensecontrolQuotaModifyRequest import (
-#                 AlipayEbppInvoiceExpensecontrolQuotaModifyRequest,
-#             )
-#             from alipay.aop.api.domain.AlipayEbppInvoiceExpensecontrolQuotaModifyModel import (
-#                 AlipayEbppInvoiceExpensecontrolQuotaModifyModel,
-#             )
-#             from alipay.aop.api.response.AlipayEbppInvoiceExpensecontrolQuotaModifyResponse import (
-#                 AlipayEbppInvoiceExpensecontrolQuotaModifyResponse,
-#             )
-#         except ImportError:
-#             raise CustomException(msg="支付宝SDK未正确安装,请检查alipay-sdk-python依赖")
-
-#         model = AlipayEbppInvoiceExpensecontrolQuotaModifyModel()
-#         model.quota_id = data.quota_id
-#         model.action = data.action
-#         model.outer_source_id = data.outer_source_id
-#         model.enterprise_id = data.enterprise_id
-
-#         if data.amount is not None:
-#             model.amount = str(data.amount)
-#         if data.share_mode:
-#             model.share_mode = data.share_mode
-
-#         request = AlipayEbppInvoiceExpensecontrolQuotaModifyRequest()
-#         request.biz_model = model
-
-#         client = AlipayClient.get_client()
-#         response = client.execute(request)
-
-#         if not response:
-#             raise CustomException(msg="修改余额/点券失败: 无响应")
-
-#         result = AlipayEbppInvoiceExpensecontrolQuotaModifyResponse()
-#         result.parse_response_content(response)
-
-#         if not result.is_success():
-#             log.error(f"支付宝接口调用失败: {result.code} - {result.msg}")
-#             raise CustomException(msg=f"修改余额/点券失败: {result.msg}")
-
-#         return QuotaOperationOutSchema(
-#             out_biz_no=out_biz_no,
-#             quota_id=data.quota_id,
-#             result=result.success,
-#         )
-
-#     @classmethod
-#     async def delete_expense_quota_service(
-#         cls, auth: AuthSchema, out_biz_no: str, data: ExpenseQuotaDeleteSchema
-#     ) -> QuotaOperationOutSchema:
-#         """
-#         删除额度
-
-#         调用: alipay.ebpp.invoice.expensecontrol.quota.delete
-#         """
-#         crud = QuotaCRUD(auth)
-#         quota = await crud.get_by_out_biz_no(out_biz_no)
-
-#         if not quota:
-#             raise CustomException(msg="额度不存在")
-
-#         try:
-#             from alipay.aop.api.request.AlipayEbppInvoiceExpensecontrolQuotaDeleteRequest import (
-#                 AlipayEbppInvoiceExpensecontrolQuotaDeleteRequest,
-#             )
-#             from alipay.aop.api.domain.AlipayEbppInvoiceExpensecontrolQuotaDeleteModel import (
-#                 AlipayEbppInvoiceExpensecontrolQuotaDeleteModel,
-#             )
-#             from alipay.aop.api.response.AlipayEbppInvoiceExpensecontrolQuotaDeleteResponse import (
-#                 AlipayEbppInvoiceExpensecontrolQuotaDeleteResponse,
-#             )
-#         except ImportError:
-#             raise CustomException(msg="支付宝SDK未正确安装,请检查alipay-sdk-python依赖")
-
-#         model = AlipayEbppInvoiceExpensecontrolQuotaDeleteModel()
-#         model.enterprise_id = data.enterprise_id
-
-#         if data.quota_id:
-#             model.quota_id = data.quota_id
-#         if data.issue_batch_id:
-#             model.issue_batch_id = data.issue_batch_id
-
-#         request = AlipayEbppInvoiceExpensecontrolQuotaDeleteRequest()
-#         request.biz_model = model
-
-#         client = AlipayClient.get_client()
-#         response = client.execute(request)
-
-#         if not response:
-#             raise CustomException(msg="删除额度失败: 无响应")
-
-#         result = AlipayEbppInvoiceExpensecontrolQuotaDeleteResponse()
-#         result.parse_response_content(response)
-
-#         if not result.is_success():
-#             log.error(f"支付宝接口调用失败: {result.code} - {result.msg}")
-#             raise CustomException(msg=f"删除额度失败: {result.msg}")
-
-#         await crud.delete(id=quota.id)
-
-#         return QuotaOperationOutSchema(out_biz_no=out_biz_no)
-
-#     @classmethod
-#     async def list_service(
-#         cls,
-#         auth: AuthSchema,
-#         page_no: int = 1,
-#         page_size: int = 20,
-#         search: dict | None = None,
-#     ) -> dict:
-#         crud = QuotaCRUD(auth)
-#         offset = (page_no - 1) * page_size
-#         return await crud.page(
-#             offset=offset,
-#             limit=page_size,
-#             order_by=[{"id": "desc"}],
-#             search=search or {},
-#             out_schema=QuotaListOutSchema,
-#         )
-
-#     @classmethod
-#     async def detail_service(
-#         cls, auth: AuthSchema, out_biz_no: str
-#     ) -> QuotaOutSchema:
-#         pass
-
-#     @classmethod
-#     async def update_service(
-#         cls, auth: AuthSchema, out_biz_no: str, data: QuotaUpdateSchema
-#     ) -> QuotaOperationOutSchema:
-#         # quota = await crud.get_by_out_biz_no(out_biz_no)
-#         # if not quota:
-#         #     raise CustomException(msg="额度不存在")
-
-#         # update_data = data.model_dump(exclude_unset=True)
-#         # if update_data:
-#         #     await crud.update(id=quota.id, data=update_data)
-
-#         # return QuotaOperationOutSchema(out_biz_no=out_biz_no, quota_id=quota.quota_id)
-#         pass
+from app.api.v1.module_system.auth.schema import AuthSchema
+from app.core.alipay import AlipayClient
+from app.core.exceptions import CustomException
+from app.core.logger import log
+from app.utils.snowflake import get_snowflake_id
+
+from .enums import QuotaStatusEnum
+from .schema import (
+    ExpenseQuotaCreateSchema,
+    ExpenseQuotaDeleteSchema,
+    ExpenseQuotaModifySchema,
+    ExpenseQuotaQueryOutSchema,
+    ExpenseQuotaQuerySchema,
+    QuotaCreateSchema,
+    QuotaDetailInfoSchema,
+    QuotaListOutSchema,
+    QuotaOperationOutSchema,
+    QuotaOutSchema,
+    QuotaUpdateSchema,
+)
+from .crud import QuotaCRUD
+
+
+class QuotaService:
+    """额度服务层"""
+
+    @classmethod
+    async def create_expense_quota_service(
+        cls, auth: AuthSchema, data: ExpenseQuotaCreateSchema
+    ) -> QuotaOperationOutSchema:
+        """
+        创建余额/点券
+        调用: alipay.ebpp.invoice.expensecontrol.quota.create
+        """
+        crud = QuotaCRUD(auth)
+
+        out_biz_no = data.outer_source_id or str(get_snowflake_id())
+
+        try:
+            from alipay.aop.api.request.AlipayEbppInvoiceExpensecontrolQuotaCreateRequest import (
+                AlipayEbppInvoiceExpensecontrolQuotaCreateRequest,
+            )
+            from alipay.aop.api.domain.AlipayEbppInvoiceExpensecontrolQuotaCreateModel import (
+                AlipayEbppInvoiceExpensecontrolQuotaCreateModel,
+            )
+            from alipay.aop.api.domain.IssueQuotaTarget import (
+                IssueQuotaTarget,
+            )
+            from alipay.aop.api.response.AlipayEbppInvoiceExpensecontrolQuotaCreateResponse import (
+                AlipayEbppInvoiceExpensecontrolQuotaCreateResponse,
+            )
+        except ImportError:
+            raise CustomException(msg="支付宝SDK未正确安装,请检查alipay-sdk-python依赖")
+
+        model = AlipayEbppInvoiceExpensecontrolQuotaCreateModel()
+        model.target_type = data.target_type
+        model.target_id = data.target_id
+        model.enterprise_id = data.enterprise_id
+        model.outer_source_id = out_biz_no
+        model.quota_type = data.quota_type or "CAP"
+        model.share_mode = data.share_mode or "0"
+
+        if data.effective_start_date:
+            model.effective_start_date = data.effective_start_date.strftime("%Y-%m-%d %H:%M:%S")
+        if data.effective_end_date:
+            model.effective_end_date = data.effective_end_date.strftime("%Y-%m-%d %H:%M:%S")
+        if data.issue_name:
+            model.issue_name = data.issue_name
+        if data.issue_desc:
+            model.issue_desc = data.issue_desc
+        if data.issue_quota_target_list:
+            target_list = []
+            for item in data.issue_quota_target_list:
+                target = IssueQuotaTarget()
+                target.owner_type = item.owner_type
+                target.owner_id = item.owner_id
+                target.quota = item.quota
+                if item.amount is not None:
+                    target.amount = item.amount
+                target_list.append(target)
+            model.issue_quota_target_list = target_list
+
+        request = AlipayEbppInvoiceExpensecontrolQuotaCreateRequest()
+        request.biz_model = model
+
+        client = AlipayClient.get_client()
+        response = client.execute(request)
+
+        if not response:
+            raise CustomException(msg="创建余额/点券失败: 无响应")
+
+        result = AlipayEbppInvoiceExpensecontrolQuotaCreateResponse()
+        result.parse_response_content(response)
+
+        if not result.is_success():
+            log.error(f"支付宝接口调用失败: {result.code} - {result.msg}")
+            raise CustomException(msg=f"创建余额/点券失败: {result.msg}")
+
+        quota_data = data.model_dump(exclude_none=True)
+        quota_data["out_biz_no"] = out_biz_no
+        quota_data["status"] = QuotaStatusEnum.QUOTA_ACTIVE.value
+
+        if result.quota_id:
+            quota_data["quota_id"] = result.quota_id
+
+        quota = await crud.create(quota_data)
+        if not quota:
+            raise CustomException(msg="创建额度记录失败")
+
+        return QuotaOperationOutSchema(out_biz_no=out_biz_no, quota_id=result.quota_id)
+
+    @classmethod
+    async def create_quota_service(
+        cls, auth: AuthSchema, data: QuotaCreateSchema
+    ) -> QuotaOperationOutSchema:
+        """创建额度"""
+        crud = QuotaCRUD(auth)
+        out_biz_no = str(get_snowflake_id())
+
+        quota_data = data.model_dump(exclude_none=True)
+        quota_data["out_biz_no"] = out_biz_no
+        quota_data["status"] = QuotaStatusEnum.QUOTA_ACTIVE.value
+        if quota_data.get("available_amount") is None:
+            quota_data["available_amount"] = quota_data.get("total_amount", 0)
+
+        quota = await crud.create(quota_data)
+        if not quota:
+            raise CustomException(msg="创建额度记录失败")
+
+        return QuotaOperationOutSchema(out_biz_no=out_biz_no, quota_id=quota.quota_id)
+
+    @classmethod
+    async def query_expense_quota_service(
+        cls, auth: AuthSchema, data: ExpenseQuotaQuerySchema
+    ) -> ExpenseQuotaQueryOutSchema:
+        """
+        查询余额/点券
+        调用: alipay.ebpp.invoice.expensecontrol.quota.query
+        """
+        try:
+            from alipay.aop.api.request.AlipayEbppInvoiceExpensecontrolQuotaQueryRequest import (
+                AlipayEbppInvoiceExpensecontrolQuotaQueryRequest,
+            )
+            from alipay.aop.api.domain.AlipayEbppInvoiceExpensecontrolQuotaQueryModel import (
+                AlipayEbppInvoiceExpensecontrolQuotaQueryModel,
+            )
+            from alipay.aop.api.response.AlipayEbppInvoiceExpensecontrolQuotaQueryResponse import (
+                AlipayEbppInvoiceExpensecontrolQuotaQueryResponse,
+            )
+        except ImportError:
+            raise CustomException(msg="支付宝SDK未正确安装,请检查alipay-sdk-python依赖")
+
+        model = AlipayEbppInvoiceExpensecontrolQuotaQueryModel()
+        model.owner_type = data.owner_type
+        model.page_size = data.page_size
+        model.page_num = data.page_num
+
+        if data.target_type:
+            model.target_type = data.target_type
+        if data.target_id:
+            model.target_id = data.target_id
+        if data.owner_id:
+            model.owner_id = data.owner_id
+        if data.owner_open_id:
+            model.owner_open_id = data.owner_open_id
+        if data.enterprise_id:
+            model.enterprise_id = data.enterprise_id
+        if data.quota_id_list:
+            model.quota_id_list = data.quota_id_list
+        if data.quota_type:
+            model.quota_type = data.quota_type
+
+        request = AlipayEbppInvoiceExpensecontrolQuotaQueryRequest()
+        request.biz_model = model
+
+        client = AlipayClient.get_client()
+        response = client.execute(request)
+
+        if not response:
+            raise CustomException(msg="查询余额/点券失败: 无响应")
+
+        result = AlipayEbppInvoiceExpensecontrolQuotaQueryResponse()
+        result.parse_response_content(response)
+
+        if not result.is_success():
+            log.error(f"支付宝接口调用失败: {result.code} - {result.msg}")
+            raise CustomException(msg=f"查询余额/点券失败: {result.msg}")
+
+        return ExpenseQuotaQueryOutSchema(
+            page_num=result.page_num or data.page_num,
+            page_size=result.page_size or data.page_size,
+            total_page_count=result.total_page_count or 0,
+        )
+
+    @classmethod
+    async def modify_expense_quota_service(
+        cls, auth: AuthSchema, out_biz_no: str, data: ExpenseQuotaModifySchema
+    ) -> QuotaOperationOutSchema:
+        """
+        修改余额/点券
+        调用: alipay.ebpp.invoice.expensecontrol.quota.modify
+        """
+        crud = QuotaCRUD(auth)
+        quota = await crud.get_by_out_biz_no(out_biz_no)
+
+        if not quota:
+            raise CustomException(msg="额度不存在")
+
+        try:
+            from alipay.aop.api.request.AlipayEbppInvoiceExpensecontrolQuotaModifyRequest import (
+                AlipayEbppInvoiceExpensecontrolQuotaModifyRequest,
+            )
+            from alipay.aop.api.domain.AlipayEbppInvoiceExpensecontrolQuotaModifyModel import (
+                AlipayEbppInvoiceExpensecontrolQuotaModifyModel,
+            )
+            from alipay.aop.api.response.AlipayEbppInvoiceExpensecontrolQuotaModifyResponse import (
+                AlipayEbppInvoiceExpensecontrolQuotaModifyResponse,
+            )
+        except ImportError:
+            raise CustomException(msg="支付宝SDK未正确安装,请检查alipay-sdk-python依赖")
+
+        model = AlipayEbppInvoiceExpensecontrolQuotaModifyModel()
+        model.quota_id = data.quota_id
+        model.action = data.action
+        model.outer_source_id = data.outer_source_id
+        model.enterprise_id = data.enterprise_id
+
+        if data.amount is not None:
+            model.amount = str(data.amount)
+        if data.share_mode:
+            model.share_mode = data.share_mode
+
+        request = AlipayEbppInvoiceExpensecontrolQuotaModifyRequest()
+        request.biz_model = model
+
+        client = AlipayClient.get_client()
+        response = client.execute(request)
+
+        if not response:
+            raise CustomException(msg="修改余额/点券失败: 无响应")
+
+        result = AlipayEbppInvoiceExpensecontrolQuotaModifyResponse()
+        result.parse_response_content(response)
+
+        if not result.is_success():
+            log.error(f"支付宝接口调用失败: {result.code} - {result.msg}")
+            raise CustomException(msg=f"修改余额/点券失败: {result.msg}")
+
+        return QuotaOperationOutSchema(
+            out_biz_no=out_biz_no,
+            quota_id=data.quota_id,
+            result=result.success,
+        )
+
+    @classmethod
+    async def delete_expense_quota_service(
+        cls, auth: AuthSchema, out_biz_no: str, data: ExpenseQuotaDeleteSchema
+    ) -> QuotaOperationOutSchema:
+        """
+        删除额度
+        调用: alipay.ebpp.invoice.expensecontrol.quota.delete
+        """
+        crud = QuotaCRUD(auth)
+        quota = await crud.get_by_out_biz_no(out_biz_no)
+
+        if not quota:
+            raise CustomException(msg="额度不存在")
+
+        try:
+            from alipay.aop.api.request.AlipayEbppInvoiceExpensecontrolQuotaDeleteRequest import (
+                AlipayEbppInvoiceExpensecontrolQuotaDeleteRequest,
+            )
+            from alipay.aop.api.domain.AlipayEbppInvoiceExpensecontrolQuotaDeleteModel import (
+                AlipayEbppInvoiceExpensecontrolQuotaDeleteModel,
+            )
+            from alipay.aop.api.response.AlipayEbppInvoiceExpensecontrolQuotaDeleteResponse import (
+                AlipayEbppInvoiceExpensecontrolQuotaDeleteResponse,
+            )
+        except ImportError:
+            raise CustomException(msg="支付宝SDK未正确安装,请检查alipay-sdk-python依赖")
+
+        model = AlipayEbppInvoiceExpensecontrolQuotaDeleteModel()
+        model.enterprise_id = data.enterprise_id
+
+        if data.quota_id:
+            model.quota_id = data.quota_id
+        if data.issue_batch_id:
+            model.issue_batch_id = data.issue_batch_id
+
+        request = AlipayEbppInvoiceExpensecontrolQuotaDeleteRequest()
+        request.biz_model = model
+
+        client = AlipayClient.get_client()
+        response = client.execute(request)
+
+        if not response:
+            raise CustomException(msg="删除额度失败: 无响应")
+
+        result = AlipayEbppInvoiceExpensecontrolQuotaDeleteResponse()
+        result.parse_response_content(response)
+
+        if not result.is_success():
+            log.error(f"支付宝接口调用失败: {result.code} - {result.msg}")
+            raise CustomException(msg=f"删除额度失败: {result.msg}")
+
+        await crud.delete(id=quota.id)
+
+        return QuotaOperationOutSchema(out_biz_no=out_biz_no)
+
+    @classmethod
+    async def list_service(
+        cls,
+        auth: AuthSchema,
+        page_no: int = 1,
+        page_size: int = 20,
+        search: dict | None = None,
+    ) -> dict:
+        crud = QuotaCRUD(auth)
+        offset = (page_no - 1) * page_size
+        return await crud.page(
+            offset=offset,
+            limit=page_size,
+            order_by=[{"id": "desc"}],
+            search=search or {},
+            out_schema=QuotaListOutSchema,
+        )

+ 118 - 118
backend/app/plugin/module_payment/expense/rule/controller.py

@@ -11,15 +11,15 @@ from app.core.router_class import OperationLogRoute
 
 
 from .schema import (
 from .schema import (
     ExpenseRuleCreateSchema,
     ExpenseRuleCreateSchema,
-    # ExpenseRuleDeleteSchema,
-    # ExpenseRuleModifySchema,
-    # RuleCreateSchema,
-    # RuleListOutSchema,
-    # RuleOperationOutSchema,
-    # RuleOutSchema,
-    # RuleUpdateSchema,
+    ExpenseRuleDeleteSchema,
+    ExpenseRuleModifySchema,
+    RuleCreateSchema,
+    RuleListOutSchema,
+    RuleOperationOutSchema,
+    RuleOutSchema,
+    RuleUpdateSchema,
 )
 )
-# from .service import RuleService
+from .service import RuleService
 
 
 RuleRouter = APIRouter(
 RuleRouter = APIRouter(
     route_class=OperationLogRoute,
     route_class=OperationLogRoute,
@@ -28,113 +28,113 @@ RuleRouter = APIRouter(
 )
 )
 
 
 
 
-# @RuleRouter.post(
-#     "/expense/create",
-#     summary="创建费控使用规则",
-#     description="创建费控使用规则 (alipay.ebpp.invoice.institution.expenserule.create)",
-#     # response_model=ResponseSchema[RuleOperationOutSchema],
-# )
-# async def create_expense_rule_controller(
-#     data: ExpenseRuleCreateSchema,
-#     auth: Annotated[AuthSchema, Depends(AuthPermission(["module_payment:expense:rule:expense:create"]))],
-# ) -> JSONResponse:
-#     result = await RuleService.create_expense_rule_service(auth=auth, data=data)
-#     log.info(f"创建费控使用规则成功: out_biz_no={result.out_biz_no}")
-#     return SuccessResponse(data=result, msg="创建费控使用规则成功")
-
-
-# @RuleRouter.post(
-#     "",
-#     summary="创建使用规则",
-#     description="创建使用规则",
-#     response_model=ResponseSchema[RuleOperationOutSchema],
-# )
-# async def create_rule_controller(
-#     data: RuleCreateSchema,
-#     auth: Annotated[AuthSchema, Depends(AuthPermission(["module_payment:expense:rule:create"]))],
-# ) -> JSONResponse:
-#     result = await RuleService.create_rule_service(auth=auth, data=data)
-#     log.info(f"创建使用规则成功: out_biz_no={result.out_biz_no}")
-#     return SuccessResponse(data=result, msg="创建使用规则成功")
-
-
-# @RuleRouter.put(
-#     "/expense/{out_biz_no}",
-#     summary="编辑使用规则",
-#     description="编辑使用规则 (alipay.ebpp.invoice.institution.expenserule.modify)",
-#     # response_model=ResponseSchema[RuleOperationOutSchema],
-# )
-# async def modify_expense_rule_controller(
-#     out_biz_no: Annotated[str, Path(description="外部业务编号")],
-#     data: ExpenseRuleModifySchema,
-#     auth: Annotated[AuthSchema, Depends(AuthPermission(["module_payment:expense:rule:expense:modify"]))],
-# ) -> JSONResponse:
-#     result = await RuleService.modify_expense_rule_service(auth=auth, out_biz_no=out_biz_no, data=data)
-#     log.info(f"编辑使用规则成功: {out_biz_no}")
-#     return SuccessResponse(data=result, msg="编辑使用规则成功")
-
-
-# @RuleRouter.delete(
-#     "/expense/{out_biz_no}",
-#     summary="删除使用规则",
-#     description="删除使用规则 (alipay.ebpp.invoice.institution.expenserule.delete)",
-#     # response_model=ResponseSchema[RuleOperationOutSchema],
-# )
-# async def delete_expense_rule_controller(
-#     out_biz_no: Annotated[str, Path(description="外部业务编号")],
-#     data: ExpenseRuleDeleteSchema,
-#     auth: Annotated[AuthSchema, Depends(AuthPermission(["module_payment:expense:rule:expense:delete"]))],
-# ) -> JSONResponse:
-#     result = await RuleService.delete_expense_rule_service(auth=auth, out_biz_no=out_biz_no, data=data)
-#     log.info(f"删除使用规则成功: {out_biz_no}")
-#     return SuccessResponse(data=result, msg="删除使用规则成功")
-
-
-# @RuleRouter.get(
-#     "",
-#     summary="查询使用规则列表",
-#     description="分页查询使用规则列表",
-#     # response_model=ResponseSchema[RuleListOutSchema],
-# )
-# async def list_rule_controller(
-#     auth: Annotated[AuthSchema, Depends(AuthPermission(["module_payment:expense:rule:list"]))],
-#     page_no: Annotated[int, Query(description="页码")] = 1,
-#     page_size: Annotated[int, Query(description="每页数量")] = 20,
-#     institution_id: Annotated[str | None, Query(description="制度ID")] = None,
-# ) -> JSONResponse:
-#     search = {}
-#     if institution_id:
-#         search["institution_id"] = institution_id
-
-#     result = await RuleService.list_service(
-#         auth=auth, page_no=page_no, page_size=page_size, search=search
-#     )
-#     return SuccessResponse(data=result, msg="查询使用规则列表成功")
-
-
-# @RuleRouter.get(
-#     "/{out_biz_no}",
-#     summary="查询使用规则详情",
-#     # response_model=ResponseSchema[RuleOutSchema],
-# )
-# async def get_detail_controller(
-#     out_biz_no: Annotated[str, Path(description="外部业务编号")],
-#     auth: Annotated[AuthSchema, Depends(AuthPermission(["module_payment:expense:rule:detail"]))],
-# ) -> JSONResponse:
-#     result = await RuleService.detail_service(auth=auth, out_biz_no=out_biz_no)
-#     return SuccessResponse(data=result, msg="查询使用规则详情成功")
-
-
-# @RuleRouter.put(
-#     "/{out_biz_no}",
-#     summary="更新使用规则",
-#     # response_model=ResponseSchema[RuleOperationOutSchema],
-# )
-# async def update_controller(
-#     out_biz_no: Annotated[str, Path(description="外部业务编号")],
-#     data: RuleUpdateSchema,
-#     auth: Annotated[AuthSchema, Depends(AuthPermission(["module_payment:expense:rule:update"]))],
-# ) -> JSONResponse:
-#     result = await RuleService.update_service(auth=auth, out_biz_no=out_biz_no, data=data)
-#     log.info(f"更新使用规则成功: {out_biz_no}")
-#     return SuccessResponse(data=result, msg="更新使用规则成功")
+@RuleRouter.post(
+    "/expense/create",
+    summary="创建费控使用规则",
+    description="创建费控使用规则 (alipay.ebpp.invoice.institution.expenserule.create)",
+    response_model=ResponseSchema[RuleOperationOutSchema],
+)
+async def create_expense_rule_controller(
+    data: ExpenseRuleCreateSchema,
+    auth: Annotated[AuthSchema, Depends(AuthPermission(["module_payment:expense:rule:expense:create"]))],
+) -> JSONResponse:
+    result = await RuleService.create_expense_rule_service(auth=auth, data=data)
+    log.info(f"创建费控使用规则成功: out_biz_no={result.out_biz_no}")
+    return SuccessResponse(data=result, msg="创建费控使用规则成功")
+
+
+@RuleRouter.post(
+    "",
+    summary="创建使用规则",
+    description="创建使用规则",
+    response_model=ResponseSchema[RuleOperationOutSchema],
+)
+async def create_rule_controller(
+    data: RuleCreateSchema,
+    auth: Annotated[AuthSchema, Depends(AuthPermission(["module_payment:expense:rule:create"]))],
+) -> JSONResponse:
+    result = await RuleService.create_rule_service(auth=auth, data=data)
+    log.info(f"创建使用规则成功: out_biz_no={result.out_biz_no}")
+    return SuccessResponse(data=result, msg="创建使用规则成功")
+
+
+@RuleRouter.put(
+    "/expense/{out_biz_no}",
+    summary="编辑使用规则",
+    description="编辑使用规则 (alipay.ebpp.invoice.institution.expenserule.modify)",
+    response_model=ResponseSchema[RuleOperationOutSchema],
+)
+async def modify_expense_rule_controller(
+    out_biz_no: Annotated[str, Path(description="外部业务编号")],
+    data: ExpenseRuleModifySchema,
+    auth: Annotated[AuthSchema, Depends(AuthPermission(["module_payment:expense:rule:expense:modify"]))],
+) -> JSONResponse:
+    result = await RuleService.modify_expense_rule_service(auth=auth, out_biz_no=out_biz_no, data=data)
+    log.info(f"编辑使用规则成功: {out_biz_no}")
+    return SuccessResponse(data=result, msg="编辑使用规则成功")
+
+
+@RuleRouter.delete(
+    "/expense/{out_biz_no}",
+    summary="删除使用规则",
+    description="删除使用规则 (alipay.ebpp.invoice.institution.expenserule.delete)",
+    response_model=ResponseSchema[RuleOperationOutSchema],
+)
+async def delete_expense_rule_controller(
+    out_biz_no: Annotated[str, Path(description="外部业务编号")],
+    data: ExpenseRuleDeleteSchema,
+    auth: Annotated[AuthSchema, Depends(AuthPermission(["module_payment:expense:rule:expense:delete"]))],
+) -> JSONResponse:
+    result = await RuleService.delete_expense_rule_service(auth=auth, out_biz_no=out_biz_no, data=data)
+    log.info(f"删除使用规则成功: {out_biz_no}")
+    return SuccessResponse(data=result, msg="删除使用规则成功")
+
+
+@RuleRouter.get(
+    "",
+    summary="查询使用规则列表",
+    description="分页查询使用规则列表",
+    response_model=ResponseSchema[RuleListOutSchema],
+)
+async def list_rule_controller(
+    auth: Annotated[AuthSchema, Depends(AuthPermission(["module_payment:expense:rule:list"]))],
+    page_no: Annotated[int, Query(description="页码")] = 1,
+    page_size: Annotated[int, Query(description="每页数量")] = 20,
+    institution_id: Annotated[str | None, Query(description="制度ID")] = None,
+) -> JSONResponse:
+    search = {}
+    if institution_id:
+        search["institution_id"] = institution_id
+
+    result = await RuleService.list_service(
+        auth=auth, page_no=page_no, page_size=page_size, search=search
+    )
+    return SuccessResponse(data=result, msg="查询使用规则列表成功")
+
+
+@RuleRouter.get(
+    "/{out_biz_no}",
+    summary="查询使用规则详情",
+    response_model=ResponseSchema[RuleOutSchema],
+)
+async def get_detail_controller(
+    out_biz_no: Annotated[str, Path(description="外部业务编号")],
+    auth: Annotated[AuthSchema, Depends(AuthPermission(["module_payment:expense:rule:detail"]))],
+) -> JSONResponse:
+    result = await RuleService.detail_service(auth=auth, out_biz_no=out_biz_no)
+    return SuccessResponse(data=result, msg="查询使用规则详情成功")
+
+
+@RuleRouter.put(
+    "/{out_biz_no}",
+    summary="更新使用规则",
+    response_model=ResponseSchema[RuleOperationOutSchema],
+)
+async def update_controller(
+    out_biz_no: Annotated[str, Path(description="外部业务编号")],
+    data: RuleUpdateSchema,
+    auth: Annotated[AuthSchema, Depends(AuthPermission(["module_payment:expense:rule:update"]))],
+) -> JSONResponse:
+    result = await RuleService.update_service(auth=auth, out_biz_no=out_biz_no, data=data)
+    log.info(f"更新使用规则成功: {out_biz_no}")
+    return SuccessResponse(data=result, msg="更新使用规则成功")

+ 290 - 288
backend/app/plugin/module_payment/expense/rule/service.py

@@ -1,288 +1,290 @@
-# from app.api.v1.module_system.auth.schema import AuthSchema
-# from app.core.alipay import AlipayClient
-# from app.core.exceptions import CustomException
-# from app.core.logger import log
-# from app.utils.snowflake import get_snowflake_id
-
-# from .enums import RuleStatusEnum
-# from .schema import (
-#     ExpenseRuleCreateSchema,
-
-# )
-
-# from alipay.aop.api.request.AlipayEbppInvoiceInstitutionExpenseruleCreateRequest import (
-#     AlipayEbppInvoiceInstitutionExpenseruleCreateRequest,
-# )
-# from alipay.aop.api.domain.AlipayEbppInvoiceInstitutionExpenseruleCreateModel import (
-#     AlipayEbppInvoiceInstitutionExpenseruleCreateModel,
-# )
-# from alipay.aop.api.domain.StandardConditionInfo import (
-#     StandardConditionInfo,
-# )
-# from alipay.aop.api.response.AlipayEbppInvoiceInstitutionExpenseruleCreateResponse import (
-#     AlipayEbppInvoiceInstitutionExpenseruleCreateResponse,
-# )
-
-
-# class RuleService:
-#     """使用规则服务层"""
-
-#     @classmethod
-#     async def create_expense_rule_service(
-#         cls, auth: AuthSchema, data: ExpenseRuleCreateSchema
-#     ) -> RuleOperationOutSchema:
-#         """
-#         创建费控使用规则
-
-#         调用: alipay.ebpp.invoice.institution.expenserule.create
-#         """
-#         crud = RuleCRUD(auth)
-
-#         out_biz_no = data.outer_source_id or str(get_snowflake_id())
-
-#         model = AlipayEbppInvoiceInstitutionExpenseruleCreateModel()
-#         model.institution_id = data.institution_id
-#         model.standard_name = data.standard_name
-#         model.expense_type_sub_category = data.expense_type_sub_category
-#         model.outer_source_id = out_biz_no
-
-#         if data.standard_condition_info_list:
-#             condition_list = []
-#             for item in data.standard_condition_info_list:
-#                 condition = StandardConditionInfo()
-#                 condition.rule_factor = item.rule_factor
-#                 condition.rule_value = item.rule_value
-#                 if item.rule_operator:
-#                     condition.rule_operator = item.rule_operator
-#                 if item.rule_name:
-#                     condition.rule_name = item.rule_name
-#                 condition_list.append(condition)
-#             model.standard_condition_info_list = condition_list
-
-#         if data.enterprise_id:
-#             model.enterprise_id = data.enterprise_id
-#         if data.open_rule_id:
-#             model.open_rule_id = data.open_rule_id
-#         if data.payment_policy:
-#             model.payment_policy = data.payment_policy
-#         if data.consume_mode:
-#             model.consume_mode = data.consume_mode
-
-#         request = AlipayEbppInvoiceInstitutionExpenseruleCreateRequest()
-#         request.biz_model = model
-
-#         client = AlipayClient.get_client()
-#         response = client.execute(request)
-
-#         if not response:
-#             raise CustomException(msg="创建费控使用规则失败: 无响应")
-
-#         result = AlipayEbppInvoiceInstitutionExpenseruleCreateResponse()
-#         result.parse_response_content(response)
-
-#         if not result.is_success():
-#             log.error(f"支付宝接口调用失败: {result.code} - {result.msg}")
-#             raise CustomException(msg=f"创建费控使用规则失败: {result.msg}")
-
-#         rule_data = data.model_dump(exclude_none=True)
-#         rule_data["out_biz_no"] = out_biz_no
-#         rule_data["name"] = data.standard_name
-#         rule_data["status"] = RuleStatusEnum.RULE_ACTIVE.value
-
-#         if result.rule_id:
-#             rule_data["rule_id"] = result.rule_id
-
-#         rule = await crud.create(rule_data)
-#         if not rule:
-#             raise CustomException(msg="创建使用规则记录失败")
-
-#         return RuleOperationOutSchema(out_biz_no=out_biz_no, rule_id=result.rule_id)
-
-#     @classmethod
-#     async def create_rule_service(
-#         cls, auth: AuthSchema, data: RuleCreateSchema
-#     ) -> RuleOperationOutSchema:
-#         """
-#         创建使用规则 (本地)
-#         """
-#         crud = RuleCRUD(auth)
-#         out_biz_no = str(get_snowflake_id())
-
-#         rule_data = data.model_dump(exclude_none=True)
-#         rule_data["out_biz_no"] = out_biz_no
-
-#         rule = await crud.create(rule_data)
-#         if not rule:
-#             raise CustomException(msg="创建使用规则记录失败")
-
-#         return RuleOperationOutSchema(out_biz_no=out_biz_no, rule_id=rule.rule_id)
-
-#     @classmethod
-#     async def modify_expense_rule_service(
-#         cls, auth: AuthSchema, out_biz_no: str, data: ExpenseRuleModifySchema
-#     ) -> RuleOperationOutSchema:
-#         """
-#         编辑使用规则
-
-#         调用: alipay.ebpp.invoice.institution.expenserule.modify
-#         """
-#         crud = RuleCRUD(auth)
-#         rule = await crud.get_by_out_biz_no(out_biz_no)
-
-#         if not rule:
-#             raise CustomException(msg="使用规则不存在")
-
-#         try:
-#             from alipay.aop.api.request.AlipayEbppInvoiceInstitutionExpenseruleModifyRequest import (
-#                 AlipayEbppInvoiceInstitutionExpenseruleModifyRequest,
-#             )
-#             from alipay.aop.api.domain.AlipayEbppInvoiceInstitutionExpenseruleModifyModel import (
-#                 AlipayEbppInvoiceInstitutionExpenseruleModifyModel,
-#             )
-#             from alipay.aop.api.response.AlipayEbppInvoiceInstitutionExpenseruleModifyResponse import (
-#                 AlipayEbppInvoiceInstitutionExpenseruleModifyResponse,
-#             )
-#         except ImportError:
-#             raise CustomException(msg="支付宝SDK未正确安装,请检查alipay-sdk-python依赖")
-
-#         model = AlipayEbppInvoiceInstitutionExpenseruleModifyModel()
-#         model.institution_id = data.institution_id
-#         model.standard_id = data.standard_id
-#         model.action = data.action
-#         model.enterprise_id = data.enterprise_id
-
-#         if data.standard_name:
-#             model.standard_name = data.standard_name
-#         if data.standard_desc:
-#             model.standard_desc = data.standard_desc
-#         if data.open_rule_id:
-#             model.open_rule_id = data.open_rule_id
-#         if data.payment_policy:
-#             model.payment_policy = data.payment_policy
-#         if data.consume_mode:
-#             model.consume_mode = data.consume_mode
-
-#         request = AlipayEbppInvoiceInstitutionExpenseruleModifyRequest()
-#         request.biz_model = model
-
-#         client = AlipayClient.get_client()
-#         response = client.execute(request)
-
-#         if not response:
-#             raise CustomException(msg="编辑使用规则失败: 无响应")
-
-#         result = AlipayEbppInvoiceInstitutionExpenseruleModifyResponse()
-#         result.parse_response_content(response)
-
-#         if not result.is_success():
-#             log.error(f"支付宝接口调用失败: {result.code} - {result.msg}")
-#             raise CustomException(msg=f"编辑使用规则失败: {result.msg}")
-
-#         update_data = data.model_dump(exclude_unset=True, exclude_none=True)
-#         exclude_fields = ["institution_id", "standard_id", "action", "enterprise_id"]
-#         for field in exclude_fields:
-#             update_data.pop(field, None)
-
-#         if update_data:
-#             await crud.update(id=rule.id, data=update_data)
-
-#         return RuleOperationOutSchema(out_biz_no=out_biz_no, rule_id=data.standard_id)
-
-#     @classmethod
-#     async def delete_expense_rule_service(
-#         cls, auth: AuthSchema, out_biz_no: str, data: ExpenseRuleDeleteSchema
-#     ) -> RuleOperationOutSchema:
-#         """
-#         删除使用规则
-
-#         调用: alipay.ebpp.invoice.institution.expenserule.delete
-#         """
-#         crud = RuleCRUD(auth)
-#         rule = await crud.get_by_out_biz_no(out_biz_no)
-
-#         if not rule:
-#             raise CustomException(msg="使用规则不存在")
-
-#         try:
-#             from alipay.aop.api.request.AlipayEbppInvoiceInstitutionExpenseruleDeleteRequest import (
-#                 AlipayEbppInvoiceInstitutionExpenseruleDeleteRequest,
-#             )
-#             from alipay.aop.api.domain.AlipayEbppInvoiceInstitutionExpenseruleDeleteModel import (
-#                 AlipayEbppInvoiceInstitutionExpenseruleDeleteModel,
-#             )
-#             from alipay.aop.api.response.AlipayEbppInvoiceInstitutionExpenseruleDeleteResponse import (
-#                 AlipayEbppInvoiceInstitutionExpenseruleDeleteResponse,
-#             )
-#         except ImportError:
-#             raise CustomException(msg="支付宝SDK未正确安装,请检查alipay-sdk-python依赖")
-
-#         model = AlipayEbppInvoiceInstitutionExpenseruleDeleteModel()
-#         model.institution_id = data.institution_id
-#         model.standard_id_list = data.standard_id_list
-#         model.enterprise_id = data.enterprise_id
-
-#         request = AlipayEbppInvoiceInstitutionExpenseruleDeleteRequest()
-#         request.biz_model = model
-
-#         client = AlipayClient.get_client()
-#         response = client.execute(request)
-
-#         if not response:
-#             raise CustomException(msg="删除使用规则失败: 无响应")
-
-#         result = AlipayEbppInvoiceInstitutionExpenseruleDeleteResponse()
-#         result.parse_response_content(response)
-
-#         if not result.is_success():
-#             log.error(f"支付宝接口调用失败: {result.code} - {result.msg}")
-#             raise CustomException(msg=f"删除使用规则失败: {result.msg}")
-
-#         await crud.delete(id=rule.id)
-
-#         return RuleOperationOutSchema(
-#             out_biz_no=out_biz_no,
-#             result=result.result,
-#         )
-
-#     @classmethod
-#     async def list_service(
-#         cls,
-#         auth: AuthSchema,
-#         page_no: int = 1,
-#         page_size: int = 20,
-#         search: dict | None = None,
-#     ) -> dict:
-#         crud = RuleCRUD(auth)
-#         offset = (page_no - 1) * page_size
-#         return await crud.page(
-#             offset=offset,
-#             limit=page_size,
-#             order_by=[{"id": "desc"}],
-#             search=search or {},
-#             out_schema=RuleListOutSchema,
-#         )
-
-#     @classmethod
-#     async def detail_service(
-#         cls, auth: AuthSchema, out_biz_no: str
-#     ) -> RuleOutSchema:
-#         crud = RuleCRUD(auth)
-#         rule = await crud.get_by_out_biz_no(out_biz_no)
-#         if not rule:
-#             raise CustomException(msg="使用规则不存在")
-#         return RuleOutSchema.model_validate(rule)
-
-#     @classmethod
-#     async def update_service(
-#         cls, auth: AuthSchema, out_biz_no: str, data: RuleUpdateSchema
-#     ) -> RuleOperationOutSchema:
-#         crud = RuleCRUD(auth)
-#         rule = await crud.get_by_out_biz_no(out_biz_no)
-#         if not rule:
-#             raise CustomException(msg="使用规则不存在")
-
-#         update_data = data.model_dump(exclude_unset=True)
-#         if update_data:
-#             await crud.update(id=rule.id, data=update_data)
-
-#         return RuleOperationOutSchema(out_biz_no=out_biz_no, rule_id=rule.rule_id)
+from app.api.v1.module_system.auth.schema import AuthSchema
+from app.core.alipay import AlipayClient
+from app.core.exceptions import CustomException
+from app.core.logger import log
+from app.utils.snowflake import get_snowflake_id
+
+from .enums import RuleStatusEnum
+from .schema import (
+    ExpenseRuleCreateSchema,
+    ExpenseRuleDeleteSchema,
+    ExpenseRuleModifySchema,
+    RuleCreateSchema,
+    RuleListOutSchema,
+    RuleOperationOutSchema,
+    RuleOutSchema,
+    RuleUpdateSchema,
+)
+from .crud import RuleCRUD
+
+from alipay.aop.api.request.AlipayEbppInvoiceInstitutionExpenseruleCreateRequest import (
+    AlipayEbppInvoiceInstitutionExpenseruleCreateRequest,
+)
+from alipay.aop.api.domain.AlipayEbppInvoiceInstitutionExpenseruleCreateModel import (
+    AlipayEbppInvoiceInstitutionExpenseruleCreateModel,
+)
+from alipay.aop.api.domain.StandardConditionInfo import (
+    StandardConditionInfo,
+)
+from alipay.aop.api.response.AlipayEbppInvoiceInstitutionExpenseruleCreateResponse import (
+    AlipayEbppInvoiceInstitutionExpenseruleCreateResponse,
+)
+
+
+class RuleService:
+    """使用规则服务层"""
+
+    @classmethod
+    async def create_expense_rule_service(
+        cls, auth: AuthSchema, data: ExpenseRuleCreateSchema
+    ) -> RuleOperationOutSchema:
+        """
+        创建费控使用规则
+        调用: alipay.ebpp.invoice.institution.expenserule.create
+        """
+        crud = RuleCRUD(auth)
+
+        out_biz_no = data.outer_source_id or str(get_snowflake_id())
+
+        model = AlipayEbppInvoiceInstitutionExpenseruleCreateModel()
+        model.institution_id = data.institution_id
+        model.standard_name = data.standard_name
+        model.expense_type_sub_category = data.expense_type_sub_category
+        model.outer_source_id = out_biz_no
+
+        if data.standard_condition_info_list:
+            condition_list = []
+            for item in data.standard_condition_info_list:
+                condition = StandardConditionInfo()
+                condition.rule_factor = item.rule_factor
+                condition.rule_value = item.rule_value
+                if item.rule_operator:
+                    condition.rule_operator = item.rule_operator
+                if item.rule_name:
+                    condition.rule_name = item.rule_name
+                condition_list.append(condition)
+            model.standard_condition_info_list = condition_list
+
+        if data.enterprise_id:
+            model.enterprise_id = data.enterprise_id
+        if data.open_rule_id:
+            model.open_rule_id = data.open_rule_id
+        if data.payment_policy:
+            model.payment_policy = data.payment_policy
+        if data.consume_mode:
+            model.consume_mode = data.consume_mode
+
+        request = AlipayEbppInvoiceInstitutionExpenseruleCreateRequest()
+        request.biz_model = model
+
+        client = AlipayClient.get_client()
+        response = client.execute(request)
+
+        if not response:
+            raise CustomException(msg="创建费控使用规则失败: 无响应")
+
+        result = AlipayEbppInvoiceInstitutionExpenseruleCreateResponse()
+        result.parse_response_content(response)
+
+        if not result.is_success():
+            log.error(f"支付宝接口调用失败: {result.code} - {result.msg}")
+            raise CustomException(msg=f"创建费控使用规则失败: {result.msg}")
+
+        rule_data = data.model_dump(exclude_none=True)
+        rule_data["out_biz_no"] = out_biz_no
+        rule_data["name"] = data.standard_name
+        rule_data["status"] = RuleStatusEnum.RULE_ACTIVE.value
+
+        if result.standard_id:
+            rule_data["standard_id"] = result.standard_id
+
+        rule = await crud.create(rule_data)
+        if not rule:
+            raise CustomException(msg="创建使用规则记录失败")
+
+        return RuleOperationOutSchema(out_biz_no=out_biz_no, rule_id=result.standard_id)
+
+    @classmethod
+    async def create_rule_service(
+        cls, auth: AuthSchema, data: RuleCreateSchema
+    ) -> RuleOperationOutSchema:
+        """创建使用规则"""
+        crud = RuleCRUD(auth)
+        out_biz_no = str(get_snowflake_id())
+
+        rule_data = data.model_dump(exclude_none=True)
+        rule_data["out_biz_no"] = out_biz_no
+
+        rule = await crud.create(rule_data)
+        if not rule:
+            raise CustomException(msg="创建使用规则记录失败")
+
+        return RuleOperationOutSchema(out_biz_no=out_biz_no, rule_id=rule.rule_id)
+
+    @classmethod
+    async def modify_expense_rule_service(
+        cls, auth: AuthSchema, out_biz_no: str, data: ExpenseRuleModifySchema
+    ) -> RuleOperationOutSchema:
+        """
+        编辑使用规则
+        调用: alipay.ebpp.invoice.institution.expenserule.modify
+        """
+        crud = RuleCRUD(auth)
+        rule = await crud.get_by_out_biz_no(out_biz_no)
+
+        if not rule:
+            raise CustomException(msg="使用规则不存在")
+
+        try:
+            from alipay.aop.api.request.AlipayEbppInvoiceInstitutionExpenseruleModifyRequest import (
+                AlipayEbppInvoiceInstitutionExpenseruleModifyRequest,
+            )
+            from alipay.aop.api.domain.AlipayEbppInvoiceInstitutionExpenseruleModifyModel import (
+                AlipayEbppInvoiceInstitutionExpenseruleModifyModel,
+            )
+            from alipay.aop.api.response.AlipayEbppInvoiceInstitutionExpenseruleModifyResponse import (
+                AlipayEbppInvoiceInstitutionExpenseruleModifyResponse,
+            )
+        except ImportError:
+            raise CustomException(msg="支付宝SDK未正确安装,请检查alipay-sdk-python依赖")
+
+        model = AlipayEbppInvoiceInstitutionExpenseruleModifyModel()
+        model.institution_id = data.institution_id
+        model.standard_id = data.standard_id
+        model.action = data.action
+        model.enterprise_id = data.enterprise_id
+
+        if data.standard_name:
+            model.standard_name = data.standard_name
+        if data.standard_desc:
+            model.standard_desc = data.standard_desc
+        if data.open_rule_id:
+            model.open_rule_id = data.open_rule_id
+        if data.payment_policy:
+            model.payment_policy = data.payment_policy
+        if data.consume_mode:
+            model.consume_mode = data.consume_mode
+
+        request = AlipayEbppInvoiceInstitutionExpenseruleModifyRequest()
+        request.biz_model = model
+
+        client = AlipayClient.get_client()
+        response = client.execute(request)
+
+        if not response:
+            raise CustomException(msg="编辑使用规则失败: 无响应")
+
+        result = AlipayEbppInvoiceInstitutionExpenseruleModifyResponse()
+        result.parse_response_content(response)
+
+        if not result.is_success():
+            log.error(f"支付宝接口调用失败: {result.code} - {result.msg}")
+            raise CustomException(msg=f"编辑使用规则失败: {result.msg}")
+
+        update_data = data.model_dump(exclude_unset=True, exclude_none=True)
+        exclude_fields = ["institution_id", "standard_id", "action", "enterprise_id"]
+        for field in exclude_fields:
+            update_data.pop(field, None)
+
+        if update_data:
+            await crud.update(id=rule.id, data=update_data)
+
+        return RuleOperationOutSchema(out_biz_no=out_biz_no, rule_id=data.standard_id)
+
+    @classmethod
+    async def delete_expense_rule_service(
+        cls, auth: AuthSchema, out_biz_no: str, data: ExpenseRuleDeleteSchema
+    ) -> RuleOperationOutSchema:
+        """
+        删除使用规则
+        调用: alipay.ebpp.invoice.institution.expenserule.delete
+        """
+        crud = RuleCRUD(auth)
+        rule = await crud.get_by_out_biz_no(out_biz_no)
+
+        if not rule:
+            raise CustomException(msg="使用规则不存在")
+
+        try:
+            from alipay.aop.api.request.AlipayEbppInvoiceInstitutionExpenseruleDeleteRequest import (
+                AlipayEbppInvoiceInstitutionExpenseruleDeleteRequest,
+            )
+            from alipay.aop.api.domain.AlipayEbppInvoiceInstitutionExpenseruleDeleteModel import (
+                AlipayEbppInvoiceInstitutionExpenseruleDeleteModel,
+            )
+            from alipay.aop.api.response.AlipayEbppInvoiceInstitutionExpenseruleDeleteResponse import (
+                AlipayEbppInvoiceInstitutionExpenseruleDeleteResponse,
+            )
+        except ImportError:
+            raise CustomException(msg="支付宝SDK未正确安装,请检查alipay-sdk-python依赖")
+
+        model = AlipayEbppInvoiceInstitutionExpenseruleDeleteModel()
+        model.institution_id = data.institution_id
+        model.standard_id_list = data.standard_id_list
+        model.enterprise_id = data.enterprise_id
+
+        request = AlipayEbppInvoiceInstitutionExpenseruleDeleteRequest()
+        request.biz_model = model
+
+        client = AlipayClient.get_client()
+        response = client.execute(request)
+
+        if not response:
+            raise CustomException(msg="删除使用规则失败: 无响应")
+
+        result = AlipayEbppInvoiceInstitutionExpenseruleDeleteResponse()
+        result.parse_response_content(response)
+
+        if not result.is_success():
+            log.error(f"支付宝接口调用失败: {result.code} - {result.msg}")
+            raise CustomException(msg=f"删除使用规则失败: {result.msg}")
+
+        await crud.delete(id=rule.id)
+
+        return RuleOperationOutSchema(
+            out_biz_no=out_biz_no,
+            result=result.result,
+        )
+
+    @classmethod
+    async def list_service(
+        cls,
+        auth: AuthSchema,
+        page_no: int = 1,
+        page_size: int = 20,
+        search: dict | None = None,
+    ) -> dict:
+        crud = RuleCRUD(auth)
+        offset = (page_no - 1) * page_size
+        return await crud.page(
+            offset=offset,
+            limit=page_size,
+            order_by=[{"id": "desc"}],
+            search=search or {},
+            out_schema=RuleListOutSchema,
+        )
+
+    @classmethod
+    async def detail_service(
+        cls, auth: AuthSchema, out_biz_no: str
+    ) -> RuleOutSchema:
+        crud = RuleCRUD(auth)
+        rule = await crud.get_by_out_biz_no(out_biz_no)
+        if not rule:
+            raise CustomException(msg="使用规则不存在")
+        return RuleOutSchema.model_validate(rule)
+
+    @classmethod
+    async def update_service(
+        cls, auth: AuthSchema, out_biz_no: str, data: RuleUpdateSchema
+    ) -> RuleOperationOutSchema:
+        crud = RuleCRUD(auth)
+        rule = await crud.get_by_out_biz_no(out_biz_no)
+        if not rule:
+            raise CustomException(msg="使用规则不存在")
+
+        update_data = data.model_dump(exclude_unset=True)
+        if update_data:
+            await crud.update(id=rule.id, data=update_data)
+
+        return RuleOperationOutSchema(out_biz_no=out_biz_no, rule_id=rule.rule_id)

+ 36 - 29
backend/tests/conftest.py

@@ -1,3 +1,4 @@
+"""pytest 共享配置与 fixture。"""
 import os
 import os
 import sys
 import sys
 
 
@@ -10,32 +11,38 @@ os.environ["DATABASE_TYPE"] = "sqlite"
 os.environ["DATABASE_NAME"] = os.path.join(_ROOT, "pytest_fastapiadmin")
 os.environ["DATABASE_NAME"] = os.path.join(_ROOT, "pytest_fastapiadmin")
 
 
 import pytest
 import pytest
-from fastapi.testclient import TestClient
-from sqlalchemy.ext.asyncio import AsyncSession
-
-from main import create_app
-from app.core.database import async_engine
-from app.core.base_model import MappedBase
-from app.plugin.module_payment.enterprise.model import EnterpriseModel
-
-
-# @pytest.fixture(scope="session")
-# def test_client():
-#     """创建测试客户端"""
-#     app = create_app()
-#     with TestClient(app) as client:
-#         yield client
-#
-#
-# @pytest.fixture(scope="session", autouse=True)
-# def setup_database():
-#     """初始化测试数据库表结构"""
-#     import asyncio
-#
-#     async def _setup():
-#         async with async_engine.begin() as coon:
-#             await coon.run_sync(MappedBase.metadata.create_all)
-
-    # asyncio.get_event_loop().run_until_complete(_setup())
-    # yield
-    # asyncio.get_event_loop().run_until_complete(async_engine.dispose())
+
+
+@pytest.fixture(scope="session")
+def test_client():
+    """
+    lazy import:仅在 fixture 求值时载入应用模块,避免顶层导入触发缺失的生产依赖。
+    如果依赖不完整,跳过该 fixture 的用例(而非整个 pytest 进程)。
+    """
+    try:
+        from fastapi.testclient import TestClient
+        from main import create_app
+        app = create_app()
+        with TestClient(app) as client:
+            yield client
+    except ImportError as e:
+        pytest.skip(f"测试客户端不可用(缺失依赖: {e})")
+
+
+@pytest.fixture(scope="session")
+def setup_database():
+    """初始化测试数据库表结构(lazy import)。"""
+    try:
+        import asyncio
+        from app.core.database import async_engine
+        from app.core.base_model import MappedBase
+
+        async def _setup():
+            async with async_engine.begin() as conn:
+                await conn.run_sync(MappedBase.metadata.create_all)
+
+        asyncio.run(_setup())
+        yield
+        asyncio.run(async_engine.dispose())
+    except ImportError as e:
+        pytest.skip(f"数据库 fixture 不可用(缺失依赖: {e})")

+ 315 - 0
backend/tests/test_institution.py

@@ -0,0 +1,315 @@
+"""
+费控制度模块专题测试。
+
+解决策略:
+  1. 函数内 lazy import 避免模块级依赖
+  2. patch.object 对已导入类的实例方法打桩
+  3. patch.object 对导入后的模块全局变量打桩(绕过 @patch 字符串路径限制)
+"""
+
+import json
+from unittest.mock import AsyncMock, MagicMock, patch
+
+import pytest
+
+pytestmark = pytest.mark.asyncio
+
+
+# ====================== 测试数据 ======================
+
+MOCK_INSTITUTION_ID = "inst_202605150001"
+MOCK_ENTERPRISE_ID = "ent_202605150001"
+MOCK_ISSUE_RULE_ID = "rule_202605150001"
+
+
+@pytest.fixture
+def mock_auth():
+    """模拟认证上下文"""
+    auth = MagicMock()
+    auth.user.id = 1
+    auth.tenant_id = "tenant_001"
+    auth.db = AsyncMock()
+    return auth
+
+
+# ====================== 费控制度 Service 测试 ======================
+
+
+class TestInstitutionCreate:
+    """测试创建费控制度的串联流程"""
+
+    async def test_create_full_flow_success(self, mock_auth):
+        """正向场景:完整的创建流程(create + scope + issuerule + 本地保存)"""
+        import app.plugin.module_payment.expense.institution.service as svc
+
+        inst = svc.InstitutionService
+        scope_svc = svc.InstitutionScopeService
+        issuerule_svc = svc.IssueruleService
+
+        with (
+            patch.object(inst, "create_institution_service") as mock_create_inst,
+            patch.object(scope_svc, "scope_modify_service") as mock_scope,
+            patch.object(issuerule_svc, "create_issuerule_service") as mock_issuerule,
+            patch.object(svc, "InstitutionCRUD") as mock_crud_cls,
+        ):
+            mock_create_inst.return_value = MagicMock(institution_id=MOCK_INSTITUTION_ID)
+            mock_scope.return_value = {"result": True}
+            mock_issuerule.return_value = {"issue_rule_id": MOCK_ISSUE_RULE_ID}
+            mock_crud_instance = AsyncMock()
+            mock_crud_cls.return_value = mock_crud_instance
+
+            from alipay.aop.api.domain.AlipayEbppInvoiceInstitutionCreateModel import (
+                AlipayEbppInvoiceInstitutionCreateModel,
+            )
+
+            model = AlipayEbppInvoiceInstitutionCreateModel()
+            model.enterprise_id = MOCK_ENTERPRISE_ID
+            model.institution_name = "测试制度"
+            model.effective = "1"
+
+            result = await inst.create_institution_full_flow(
+                auth=mock_auth,
+                institution_model=model,
+                enterprise_id=MOCK_ENTERPRISE_ID,
+                scope_data={"adapter_type": "EMPLOYEE_ALL"},
+                issuerule_data={
+                    "quota_type": "CAP",
+                    "issue_type": "ISSUE_MONTH",
+                    "issue_amount_value": "1000",
+                    "issue_rule_name": "测试制度-发放规则",
+                },
+            )
+
+            assert result["institution_id"] == MOCK_INSTITUTION_ID
+            assert result["scope_modified"] is True
+            assert result["issue_rule_id"] == MOCK_ISSUE_RULE_ID
+            mock_create_inst.assert_called_once()
+            mock_scope.assert_called_once()
+            mock_issuerule.assert_called_once()
+            mock_crud_instance.create.assert_called_once()
+
+    async def test_create_without_scope_and_issuerule(self, mock_auth):
+        """边界场景:不传 scope 和 issuerule 时只调 create"""
+        import app.plugin.module_payment.expense.institution.service as svc
+
+        inst = svc.InstitutionService
+        scope_svc = svc.InstitutionScopeService
+        issuerule_svc = svc.IssueruleService
+
+        with (
+            patch.object(inst, "create_institution_service") as mock_create_inst,
+            patch.object(scope_svc, "scope_modify_service") as mock_scope,
+            patch.object(issuerule_svc, "create_issuerule_service") as mock_issuerule,
+            patch.object(svc, "InstitutionCRUD") as mock_crud_cls,
+        ):
+            mock_create_inst.return_value = MagicMock(institution_id=MOCK_INSTITUTION_ID)
+            mock_crud_instance = AsyncMock()
+            mock_crud_cls.return_value = mock_crud_instance
+
+            from alipay.aop.api.domain.AlipayEbppInvoiceInstitutionCreateModel import (
+                AlipayEbppInvoiceInstitutionCreateModel,
+            )
+
+            model = AlipayEbppInvoiceInstitutionCreateModel()
+            model.enterprise_id = MOCK_ENTERPRISE_ID
+            model.institution_name = "无成员无规则制度"
+
+            result = await inst.create_institution_full_flow(
+                auth=mock_auth,
+                institution_model=model,
+                enterprise_id=MOCK_ENTERPRISE_ID,
+                scope_data=None,
+                issuerule_data=None,
+            )
+
+            assert result["institution_id"] == MOCK_INSTITUTION_ID
+            assert result["scope_modified"] is False
+            assert result["issue_rule_id"] is None
+            mock_scope.assert_not_called()
+            mock_issuerule.assert_not_called()
+
+    async def test_create_rollback_on_failure(self, mock_auth):
+        """异常场景:scope 失败后触发回滚(删除已创建的制度)"""
+        import app.plugin.module_payment.expense.institution.service as svc
+
+        inst = svc.InstitutionService
+        scope_svc = svc.InstitutionScopeService
+
+        with (
+            patch.object(inst, "create_institution_service") as mock_create_inst,
+            patch.object(scope_svc, "scope_modify_service") as mock_scope,
+        ):
+            mock_create_inst.return_value = MagicMock(institution_id=MOCK_INSTITUTION_ID)
+            mock_scope.side_effect = Exception("支付宝接口异常")
+
+            with patch("asyncio.to_thread") as mock_to_thread:
+                from alipay.aop.api.domain.AlipayEbppInvoiceInstitutionCreateModel import (
+                    AlipayEbppInvoiceInstitutionCreateModel,
+                )
+
+                model = AlipayEbppInvoiceInstitutionCreateModel()
+                model.enterprise_id = MOCK_ENTERPRISE_ID
+                model.institution_name = "测试回滚制度"
+
+                with pytest.raises(Exception, match="支付宝接口异常"):
+                    await inst.create_institution_full_flow(
+                        auth=mock_auth,
+                        institution_model=model,
+                        enterprise_id=MOCK_ENTERPRISE_ID,
+                        scope_data={"adapter_type": "EMPLOYEE_ALL"},
+                        issuerule_data=None,
+                    )
+
+                assert mock_to_thread.called, "回滚时应该调用了 asyncio.to_thread"
+
+
+class TestInstitutionDelete:
+    """测试删除费控制度"""
+
+    async def test_delete_sync_local_db(self, mock_auth):
+        """正向场景:删除成功后同步删除本地记录"""
+        import app.plugin.module_payment.expense.institution.service as svc
+        from alipay.aop.api.domain.AlipayEbppInvoiceInstitutionDeleteModel import (
+            AlipayEbppInvoiceInstitutionDeleteModel,
+        )
+
+        inst = svc.InstitutionService
+
+        with (
+            patch.object(inst, "_execute_alipay") as mock_execute,
+            patch.object(svc, "InstitutionCRUD") as mock_crud_cls,
+        ):
+            mock_execute.return_value = json.dumps({
+                "alipay_ebpp_invoice_institution_delete_response": {
+                    "code": "10000", "msg": "Success",
+                }
+            })
+            mock_obj = MagicMock()
+            mock_obj.id = 1
+            mock_crud_instance = AsyncMock()
+            mock_crud_instance.get = AsyncMock(return_value=mock_obj)
+            mock_crud_cls.return_value = mock_crud_instance
+
+            delete_model = AlipayEbppInvoiceInstitutionDeleteModel()
+            delete_model.institution_id = MOCK_INSTITUTION_ID
+            delete_model.enterprise_id = MOCK_ENTERPRISE_ID
+
+            result = await inst.delete_institution_service(auth=mock_auth, data=delete_model)
+
+            assert result is not None
+            mock_crud_instance.get.assert_called_with(institution_id=MOCK_INSTITUTION_ID)
+            mock_crud_instance.delete.assert_called_with(ids=[mock_obj.id])
+
+
+class TestIssueruleConstraint:
+    """测试自动发放规则的参数约束"""
+
+    async def test_cap_must_be_accumulative(self, mock_auth):
+        """余额类型(CAP)必须为可累计(invalid_mode=1)"""
+        from app.core.exceptions import CustomException
+        import app.plugin.module_payment.expense.institution.service as svc
+
+        with patch.object(svc.IssueruleService, "_execute_alipay") as mock_execute:
+            with pytest.raises(CustomException, match="余额类型.*必须为可累计"):
+                await svc.IssueruleService.create_issuerule_service(
+                    auth=mock_auth,
+                    institution_id=MOCK_INSTITUTION_ID,
+                    enterprise_id=MOCK_ENTERPRISE_ID,
+                    quota_type="CAP",
+                    issue_type="ISSUE_MONTH",
+                    issue_amount_value="500",
+                    invalid_mode=0,
+                )
+            mock_execute.assert_not_called()
+
+    async def test_count_cannot_transfer(self, mock_auth):
+        """次卡类型(COUNT)不可转赠(share_mode=0)"""
+        from app.core.exceptions import CustomException
+        import app.plugin.module_payment.expense.institution.service as svc
+
+        with patch.object(svc.IssueruleService, "_execute_alipay") as mock_execute:
+            with pytest.raises(CustomException, match="次卡类型.*不可转赠"):
+                await svc.IssueruleService.create_issuerule_service(
+                    auth=mock_auth,
+                    institution_id=MOCK_INSTITUTION_ID,
+                    enterprise_id=MOCK_ENTERPRISE_ID,
+                    quota_type="COUNT",
+                    issue_type="ISSUE_MONTH",
+                    issue_amount_value="10",
+                    share_mode=1,
+                )
+            mock_execute.assert_not_called()
+
+
+# ====================== Scope Sync 测试 ======================
+
+
+class TestScopeSync:
+    """测试员工/部门停用联动"""
+
+    async def test_remove_employee(self, mock_auth):
+        """解约员工时:从所有引用该员工的制度中移除"""
+        import app.plugin.module_payment.expense.institution.scope_sync as sync
+        import app.plugin.module_payment.expense.institution.service as svc
+
+        with (
+            patch.object(sync, "InstitutionCRUD") as mock_crud_cls,
+            patch.object(svc.InstitutionScopeService, "scope_modify_service") as mock_scope_modify,
+        ):
+            mock_inst_1 = MagicMock()
+            mock_inst_1.institution_id = "inst_001"
+            mock_inst_2 = MagicMock()
+            mock_inst_2.institution_id = "inst_002"
+
+            mock_crud_instance = AsyncMock()
+            mock_crud_instance.list = AsyncMock(return_value=[mock_inst_1, mock_inst_2])
+            mock_crud_cls.return_value = mock_crud_instance
+
+            await sync.remove_employee_from_institution_scopes(
+                auth=mock_auth,
+                enterprise_id=MOCK_ENTERPRISE_ID,
+                employee_id="emp_001",
+            )
+
+            assert mock_scope_modify.call_count == 2
+            mock_scope_modify.assert_any_call(
+                auth=mock_auth,
+                institution_id="inst_001",
+                data={
+                    "enterprise_id": MOCK_ENTERPRISE_ID,
+                    "adapter_type": "EMPLOYEE_SELECT",
+                    "delete_owner_id_list": ["emp_001"],
+                },
+            )
+
+    async def test_remove_department(self, mock_auth):
+        """停用部门时:从所有引用该部门的制度中移除"""
+        import app.plugin.module_payment.expense.institution.scope_sync as sync
+        import app.plugin.module_payment.expense.institution.service as svc
+
+        with (
+            patch.object(sync, "InstitutionCRUD") as mock_crud_cls,
+            patch.object(svc.InstitutionScopeService, "scope_modify_service") as mock_scope_modify,
+        ):
+            mock_inst = MagicMock()
+            mock_inst.institution_id = "inst_001"
+
+            mock_crud_instance = AsyncMock()
+            mock_crud_instance.list = AsyncMock(return_value=[mock_inst])
+            mock_crud_cls.return_value = mock_crud_instance
+
+            await sync.remove_department_from_institution_scopes(
+                auth=mock_auth,
+                enterprise_id=MOCK_ENTERPRISE_ID,
+                department_id="dept_001",
+            )
+
+            mock_scope_modify.assert_called_once_with(
+                auth=mock_auth,
+                institution_id="inst_001",
+                data={
+                    "enterprise_id": MOCK_ENTERPRISE_ID,
+                    "adapter_type": "EMPLOYEE_DEPARTMENT",
+                    "delete_owner_id_list": ["dept_001"],
+                },
+            )

+ 81 - 6
frontend/src/api/module_payment/institution.ts

@@ -11,10 +11,11 @@ const InstitutionAPI = {
     });
     });
   },
   },
 
 
-  detailInstitution(institutionId: string) {
+  detailInstitution(institutionId: string, enterpriseId?: string) {
     return request<ApiResponse<InstitutionDetail>>({
     return request<ApiResponse<InstitutionDetail>>({
       url: `${API_PATH}/${institutionId}`,
       url: `${API_PATH}/${institutionId}`,
       method: "get",
       method: "get",
+      params: { enterprise_id: enterpriseId },
     });
     });
   },
   },
 
 
@@ -26,18 +27,30 @@ const InstitutionAPI = {
     });
     });
   },
   },
 
 
+  /** 编辑费控制度: POST /payment/institution/modify */
   updateInstitution(institutionId: string, body: InstitutionForm) {
   updateInstitution(institutionId: string, body: InstitutionForm) {
     return request<ApiResponse>({
     return request<ApiResponse>({
-      url: `${API_PATH}/${institutionId}`,
-      method: "put",
-      data: body,
+      url: `${API_PATH}/modify`,
+      method: "post",
+      data: { institution_id: institutionId, ...body },
     });
     });
   },
   },
 
 
-  deleteInstitution(institutionId: string) {
+  /** 删除费控制度: DELETE /payment/institution (body传参) */
+  deleteInstitution(institutionId: string, enterpriseId?: string) {
     return request<ApiResponse>({
     return request<ApiResponse>({
-      url: `${API_PATH}/${institutionId}`,
+      url: `${API_PATH}`,
       method: "delete",
       method: "delete",
+      data: { institution_id: institutionId, enterprise_id: enterpriseId },
+    });
+  },
+
+  /** 启用/停用费控制度: 通过modify接口仅传effective字段 */
+  modifyEffective(institutionId: string, enterpriseId: string, effective: string) {
+    return request<ApiResponse>({
+      url: `${API_PATH}/modify`,
+      method: "post",
+      data: { institution_id: institutionId, enterprise_id: enterpriseId, effective },
     });
     });
   },
   },
 
 
@@ -56,6 +69,33 @@ const InstitutionAPI = {
       data: body,
       data: body,
     });
     });
   },
   },
+
+  /** 创建自动发放规则 */
+  createIssuerule(institutionId: string, body: Record<string, unknown>) {
+    return request<ApiResponse>({
+      url: `${API_PATH}/${institutionId}/issuerule`,
+      method: "post",
+      data: body,
+    });
+  },
+
+  /** 编辑自动发放规则 */
+  modifyIssuerule(institutionId: string, issueRuleId: string, body: Record<string, unknown>) {
+    return request<ApiResponse>({
+      url: `${API_PATH}/${institutionId}/issuerule/${issueRuleId}`,
+      method: "put",
+      data: body,
+    });
+  },
+
+  /** 删除自动发放规则 */
+  deleteIssuerule(institutionId: string, issueRuleIdList: string[]) {
+    return request<ApiResponse>({
+      url: `${API_PATH}/${institutionId}/issuerule`,
+      method: "delete",
+      data: { issue_rule_id_list: issueRuleIdList },
+    });
+  },
 };
 };
 
 
 export default InstitutionAPI;
 export default InstitutionAPI;
@@ -64,6 +104,9 @@ export interface InstitutionPageQuery {
   page_no?: number;
   page_no?: number;
   page_size?: number;
   page_size?: number;
   enterprise_id?: string;
   enterprise_id?: string;
+  name?: string;
+  expense_type?: string;
+  status?: string;
 }
 }
 
 
 export interface InstitutionPageResp {
 export interface InstitutionPageResp {
@@ -76,9 +119,14 @@ export interface InstitutionTable {
   institution_id?: string;
   institution_id?: string;
   enterprise_id?: string;
   enterprise_id?: string;
   name?: string;
   name?: string;
+  institution_name?: string;
   expense_type?: string;
   expense_type?: string;
   status: string;
   status: string;
+  effective?: string;
+  effective_start_date?: string;
+  effective_end_date?: string;
   created_time: string;
   created_time: string;
+  updated_time?: string;
 }
 }
 
 
 export interface InstitutionDetail {
 export interface InstitutionDetail {
@@ -87,6 +135,7 @@ export interface InstitutionDetail {
   enterprise_id?: string;
   enterprise_id?: string;
   out_biz_no?: string;
   out_biz_no?: string;
   name?: string;
   name?: string;
+  institution_name?: string;
   expense_type?: string;
   expense_type?: string;
   expense_sub_type?: string;
   expense_sub_type?: string;
   effective?: string;
   effective?: string;
@@ -95,6 +144,10 @@ export interface InstitutionDetail {
   effective_start_date?: string;
   effective_start_date?: string;
   effective_end_date?: string;
   effective_end_date?: string;
   consult_mode?: string;
   consult_mode?: string;
+  grant_mode?: string;
+  applicable_scope?: string;
+  period_type?: string;
+  amount?: number;
   created_time: string;
   created_time: string;
   updated_time: string;
   updated_time: string;
 }
 }
@@ -109,6 +162,28 @@ export interface InstitutionForm {
   effective_start_date?: string;
   effective_start_date?: string;
   effective_end_date?: string;
   effective_end_date?: string;
   consult_mode?: string;
   consult_mode?: string;
+  /** 额度发放: period | manual */
+  grant_mode?: string;
+  /** 适用员工范围: department | employee | all | none */
+  applicable_scope?: string;
+  /** 周期类型: daily | weekly | monthly | quarterly | yearly */
+  period_type?: string;
+  /** 金额 */
+  amount?: number;
+  /** 有效时间类型: unlimited | workday */
+  effective_time_type?: string;
+  /** 单次限额 */
+  single_limit?: number;
+  workday_start_time?: string;
+  workday_end_type?: string;
+  workday_end_time?: string;
+  holiday_start_time?: string;
+  holiday_end_type?: string;
+  holiday_end_time?: string;
+  department_id?: string;
+  employee_ids?: string[];
+  /** 是否长期有效 */
+  is_long_term?: boolean;
 }
 }
 
 
 export interface InstitutionOperation {
 export interface InstitutionOperation {

+ 2 - 2
frontend/src/views/module_payment/employee/components/EmployeeForm.vue

@@ -233,7 +233,7 @@ async function submitForm() {
       employee_cert_type: formData.employee_cert_type || undefined,
       employee_cert_type: formData.employee_cert_type || undefined,
       employee_cert_no: formData.employee_cert_no || undefined,
       employee_cert_no: formData.employee_cert_no || undefined,
       iot_check_type: formData.iot_check_type || undefined,
       iot_check_type: formData.iot_check_type || undefined,
-      department_ids: formData.department_ids || [],
+      department_ids: Array.isArray(formData.department_ids) ? formData.department_ids : [],
       accounting_entity_ids: formData.accounting_entity_ids || [],
       accounting_entity_ids: formData.accounting_entity_ids || [],
       label_names: formData.label_names || [],
       label_names: formData.label_names || [],
       sign_return_url: formData.sign_return_url || undefined,
       sign_return_url: formData.sign_return_url || undefined,
@@ -270,7 +270,7 @@ async function handleSaveAndAddNext() {
       employee_cert_type: formData.employee_cert_type || undefined,
       employee_cert_type: formData.employee_cert_type || undefined,
       employee_cert_no: formData.employee_cert_no || undefined,
       employee_cert_no: formData.employee_cert_no || undefined,
       iot_check_type: formData.iot_check_type || undefined,
       iot_check_type: formData.iot_check_type || undefined,
-      department_ids: formData.department_ids || [],
+      department_ids: Array.isArray(formData.department_ids) ? formData.department_ids : [],
       accounting_entity_ids: formData.accounting_entity_ids || [],
       accounting_entity_ids: formData.accounting_entity_ids || [],
       label_names: formData.label_names || [],
       label_names: formData.label_names || [],
       sign_return_url: formData.sign_return_url || undefined,
       sign_return_url: formData.sign_return_url || undefined,

+ 85 - 47
frontend/src/views/module_payment/institution/components/InstitutionDetail.vue

@@ -1,53 +1,71 @@
 <template>
 <template>
   <div class="institution-detail">
   <div class="institution-detail">
-    <el-descriptions :column="3" border>
-      <el-descriptions-item label="制度ID" :span="2">
-        {{ detailData.institution_id || "-" }}
-      </el-descriptions-item>
-      <el-descriptions-item label="状态" :span="1">
-        <el-tag :type="STATUS_TAG_TYPE[detailData.status]">
-          {{ STATUS_LABEL[detailData.status] || detailData.status }}
-        </el-tag>
-      </el-descriptions-item>
-      <el-descriptions-item label="企业ID" :span="2">
-        {{ detailData.enterprise_id || "-" }}
-      </el-descriptions-item>
-      <el-descriptions-item label="外部业务编号" :span="1">
-        {{ detailData.out_biz_no || "-" }}
-      </el-descriptions-item>
-      <el-descriptions-item label="制度名称" :span="3">
-        {{ detailData.name || "-" }}
-      </el-descriptions-item>
-      <el-descriptions-item label="费用类型" :span="1">
-        {{ EXPENSE_TYPE_LABEL[detailData.expense_type] || detailData.expense_type || "-" }}
-      </el-descriptions-item>
-      <el-descriptions-item label="是否启用" :span="2">
-        {{ detailData.effective === "1" ? "启用" : "停用" }}
-      </el-descriptions-item>
-      <el-descriptions-item label="生效开始时间" :span="1">
-        {{ detailData.effective_start_date || "-" }}
-      </el-descriptions-item>
-      <el-descriptions-item label="生效结束时间" :span="2">
-        {{ detailData.effective_end_date || "-" }}
-      </el-descriptions-item>
-      <el-descriptions-item label="费控咨询模式" :span="3">
-        {{ formatConsultMode(detailData.consult_mode) }}
-      </el-descriptions-item>
-      <el-descriptions-item label="制度描述" :span="3">
-        {{ detailData.institution_desc || "-" }}
-      </el-descriptions-item>
-    </el-descriptions>
+    <el-tabs v-model="activeTab" class="detail-tabs">
+      <el-tab-pane label="基本信息" name="basic">
+        <el-descriptions :column="3" border>
+          <el-descriptions-item label="制度ID" :span="2">
+            {{ detailData.institution_id || "-" }}
+          </el-descriptions-item>
+          <el-descriptions-item label="状态" :span="1">
+            <el-tag :type="STATUS_TAG_TYPE[detailData.status]">
+              {{ STATUS_LABEL[detailData.status] || detailData.status }}
+            </el-tag>
+          </el-descriptions-item>
+          <el-descriptions-item label="企业ID" :span="2">
+            {{ detailData.enterprise_id || "-" }}
+          </el-descriptions-item>
+          <el-descriptions-item label="外部业务编号" :span="1">
+            {{ detailData.out_biz_no || "-" }}
+          </el-descriptions-item>
+          <el-descriptions-item label="制度名称" :span="3">
+            {{ detailData.name || detailData.institution_name || "-" }}
+          </el-descriptions-item>
+          <el-descriptions-item label="费用类型" :span="1">
+            {{ EXPENSE_TYPE_LABEL[detailData.expense_type] || detailData.expense_type || "-" }}
+          </el-descriptions-item>
+          <el-descriptions-item label="是否启用" :span="2">
+            {{ detailData.effective === "1" ? "启用" : "停用" }}
+          </el-descriptions-item>
+          <el-descriptions-item label="生效开始时间" :span="1">
+            {{ detailData.effective_start_date || "-" }}
+          </el-descriptions-item>
+          <el-descriptions-item label="生效结束时间" :span="2">
+            {{ detailData.effective_end_date || "-" }}
+          </el-descriptions-item>
+          <el-descriptions-item label="额度发放方式" :span="1">
+            {{ detailData.grant_mode === "period" ? "按固定周期" : detailData.grant_mode === "manual" ? "手工发放" : "-" }}
+          </el-descriptions-item>
+          <el-descriptions-item label="适用员工范围" :span="2">
+            {{ formatScope(detailData.applicable_scope) }}
+          </el-descriptions-item>
+          <el-descriptions-item label="费控咨询模式" :span="3">
+            {{ formatConsultMode(detailData.consult_mode) }}
+          </el-descriptions-item>
+          <el-descriptions-item label="制度描述" :span="3">
+            {{ detailData.institution_desc || "-" }}
+          </el-descriptions-item>
+        </el-descriptions>
 
 
-    <el-divider content-position="left">时间信息</el-divider>
+        <el-divider content-position="left">时间信息</el-divider>
 
 
-    <el-descriptions :column="3" border>
-      <el-descriptions-item label="创建时间" :span="1">
-        {{ detailData.created_time || "-" }}
-      </el-descriptions-item>
-      <el-descriptions-item label="更新时间" :span="2">
-        {{ detailData.updated_time || "-" }}
-      </el-descriptions-item>
-    </el-descriptions>
+        <el-descriptions :column="3" border>
+          <el-descriptions-item label="创建时间" :span="1">
+            {{ detailData.created_time || "-" }}
+          </el-descriptions-item>
+          <el-descriptions-item label="更新时间" :span="2">
+            {{ detailData.updated_time || "-" }}
+          </el-descriptions-item>
+        </el-descriptions>
+      </el-tab-pane>
+
+      <el-tab-pane label="使用规则" name="rule">
+        <RuleList :institution-id="props.institutionId" />
+      </el-tab-pane>
+
+      <el-tab-pane label="额度管理" name="quota">
+        <QuotaList :institution-id="props.institutionId" />
+      </el-tab-pane>
+    </el-tabs>
   </div>
   </div>
 </template>
 </template>
 
 
@@ -59,6 +77,8 @@ import InstitutionAPI, {
   EXPENSE_TYPE_LABEL,
   EXPENSE_TYPE_LABEL,
   CONSULT_MODE_OPTIONS,
   CONSULT_MODE_OPTIONS,
 } from "@/api/module_payment/institution";
 } from "@/api/module_payment/institution";
+import RuleList from "./RuleList.vue";
+import QuotaList from "./QuotaList.vue";
 import { onMounted, ref } from "vue";
 import { onMounted, ref } from "vue";
 
 
 interface Props {
 interface Props {
@@ -67,6 +87,8 @@ interface Props {
 
 
 const props = defineProps<Props>();
 const props = defineProps<Props>();
 
 
+const activeTab = ref("basic");
+
 const detailData = ref<InstitutionDetail>({
 const detailData = ref<InstitutionDetail>({
   status: "",
   status: "",
   created_time: "",
   created_time: "",
@@ -79,6 +101,16 @@ function formatConsultMode(mode?: string) {
   return option ? option.label : mode;
   return option ? option.label : mode;
 }
 }
 
 
+function formatScope(scope?: string) {
+  const map: Record<string, string> = {
+    department: "按部门",
+    employee: "按员工",
+    all: "全体员工",
+    none: "暂不设置",
+  };
+  return scope ? map[scope] || scope : "-";
+}
+
 async function fetchDetail() {
 async function fetchDetail() {
   if (!props.institutionId) return;
   if (!props.institutionId) return;
   const res = await InstitutionAPI.detailInstitution(props.institutionId);
   const res = await InstitutionAPI.detailInstitution(props.institutionId);
@@ -90,4 +122,10 @@ async function fetchDetail() {
 onMounted(() => {
 onMounted(() => {
   fetchDetail();
   fetchDetail();
 });
 });
-</script>
+</script>
+
+<style scoped>
+.detail-tabs {
+  min-height: 300px;
+}
+</style>

+ 62 - 13
frontend/src/views/module_payment/institution/components/InstitutionForm.vue

@@ -8,6 +8,18 @@
             <el-input v-model="formData.name" placeholder="最多50字" :maxlength="50" />
             <el-input v-model="formData.name" placeholder="最多50字" :maxlength="50" />
           </el-form-item>
           </el-form-item>
         </el-col>
         </el-col>
+        <el-col :span="12">
+          <el-form-item label="费用类型" prop="expense_type">
+            <el-select v-model="formData.expense_type" placeholder="请选择费用类型" style="width: 100%">
+              <el-option
+                v-for="opt in EXPENSE_TYPE_OPTIONS"
+                :key="opt.value"
+                :label="opt.label"
+                :value="opt.value"
+              />
+            </el-select>
+          </el-form-item>
+        </el-col>
       </el-row>
       </el-row>
 
 
       <el-row :gutter="20">
       <el-row :gutter="20">
@@ -179,16 +191,27 @@
             </el-col>
             </el-col>
           </el-row>
           </el-row>
 
 
-          <el-row :gutter="20">
-            <el-col :span="24">
-              <el-form-item label="是否启用" prop="effective">
-                <el-radio-group v-model="formData.effective">
-                  <el-radio value="1">启用</el-radio>
-                  <el-radio value="0">停用</el-radio>
-                </el-radio-group>
-              </el-form-item>
-            </el-col>
-          </el-row>
+      <el-row :gutter="20">
+        <el-col :span="24">
+          <el-form-item label="是否启用" prop="effective">
+            <el-radio-group v-model="formData.effective">
+              <el-radio value="1">启用</el-radio>
+              <el-radio value="0">停用</el-radio>
+            </el-radio-group>
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="20">
+        <el-col :span="24">
+          <el-form-item label="费控咨询模式" prop="consult_mode">
+            <el-radio-group v-model="formData.consult_mode">
+              <el-radio value="0">支付宝内部计算</el-radio>
+              <el-radio value="1">咨询外部服务商</el-radio>
+            </el-radio-group>
+          </el-form-item>
+        </el-col>
+      </el-row>
         </el-col>
         </el-col>
       </el-row>
       </el-row>
     </el-form>
     </el-form>
@@ -298,17 +321,21 @@ watch(
   () => props.institutionId,
   () => props.institutionId,
   async (newVal) => {
   async (newVal) => {
     if (newVal && props.type === "update") {
     if (newVal && props.type === "update") {
-      const res = await InstitutionAPI.detailInstitution(newVal);
+      const res = await InstitutionAPI.detailInstitution(newVal, props.enterpriseId);
       const data = res.data.data as InstitutionDetail;
       const data = res.data.data as InstitutionDetail;
       if (data) {
       if (data) {
         formData.enterprise_id = data.enterprise_id;
         formData.enterprise_id = data.enterprise_id;
-        formData.name = data.name;
+        formData.name = data.name || data.institution_name;
         formData.expense_type = data.expense_type || "GENERAL";
         formData.expense_type = data.expense_type || "GENERAL";
         formData.effective = data.effective || "1";
         formData.effective = data.effective || "1";
         formData.institution_desc = data.institution_desc;
         formData.institution_desc = data.institution_desc;
         formData.effective_start_date = data.effective_start_date;
         formData.effective_start_date = data.effective_start_date;
         formData.effective_end_date = data.effective_end_date;
         formData.effective_end_date = data.effective_end_date;
         formData.consult_mode = data.consult_mode || "0";
         formData.consult_mode = data.consult_mode || "0";
+        formData.grant_mode = data.grant_mode || "period";
+        formData.applicable_scope = data.applicable_scope || "none";
+        formData.period_type = data.period_type || "monthly";
+        formData.amount = data.amount;
       }
       }
     }
     }
   },
   },
@@ -381,6 +408,7 @@ async function submitForm() {
   if (!valid) return;
   if (!valid) return;
 
 
   try {
   try {
+    // 构建通用表单数据
     const submitData: Record<string, unknown> = {
     const submitData: Record<string, unknown> = {
       enterprise_id: formData.enterprise_id,
       enterprise_id: formData.enterprise_id,
       name: formData.name,
       name: formData.name,
@@ -394,6 +422,16 @@ async function submitForm() {
       applicable_scope: formData.applicable_scope,
       applicable_scope: formData.applicable_scope,
     };
     };
 
 
+    // 适用范围参数
+    if (formData.applicable_scope === "department") {
+      submitData.scope_owner_type = "ENTERPRISE_PAY_UID";
+      submitData.scope_owner_id_list = formData.department_id ? [formData.department_id] : undefined;
+    } else if (formData.applicable_scope === "employee") {
+      submitData.scope_owner_type = "PHONE";
+      submitData.scope_owner_id_list = formData.employee_ids;
+    }
+
+    // 固定周期发放参数
     if (formData.grant_mode === "period") {
     if (formData.grant_mode === "period") {
       submitData.period_type = formData.period_type;
       submitData.period_type = formData.period_type;
       submitData.amount = formData.amount;
       submitData.amount = formData.amount;
@@ -413,7 +451,18 @@ async function submitForm() {
     if (props.type === "create") {
     if (props.type === "create") {
       await InstitutionAPI.createInstitution(submitData);
       await InstitutionAPI.createInstitution(submitData);
     } else if (props.type === "update" && props.institutionId) {
     } else if (props.type === "update" && props.institutionId) {
-      await InstitutionAPI.updateInstitution(props.institutionId, submitData);
+      // 编辑时重命名字段以匹配支付宝 AlipayEbppInvoiceInstitutionModifyModel
+      const modifyData: Record<string, unknown> = {
+        institution_id: props.institutionId,
+        enterprise_id: formData.enterprise_id,
+        institution_name: formData.name,
+        institution_desc: formData.institution_desc || undefined,
+        effective: formData.effective,
+        effective_start_date: formData.effective_start_date || undefined,
+        effective_end_date: formData.is_long_term ? undefined : (formData.effective_end_date || undefined),
+        consult_mode: formData.consult_mode || undefined,
+      };
+      await InstitutionAPI.updateInstitution(props.institutionId, modifyData);
     }
     }
     emit("success");
     emit("success");
   } catch (error) {
   } catch (error) {

+ 38 - 0
frontend/src/views/module_payment/institution/components/QuotaDetailDialog.vue

@@ -0,0 +1,38 @@
+<template>
+  <el-dialog
+    v-model="visible"
+    title="额度详情"
+    width="700px"
+    @close="handleClose"
+  >
+    <QuotaDetail :quota-id="quotaId" />
+    <template #footer>
+      <el-button type="primary" @click="visible = false">确定</el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import QuotaDetail from "@/views/module_payment/quota/components/QuotaDetail.vue";
+import { computed } from "vue";
+
+interface Props {
+  visible: boolean;
+  quotaId: string;
+}
+
+const props = defineProps<Props>();
+
+const emit = defineEmits<{
+  (e: "update:visible", value: boolean): void;
+}>();
+
+const visible = computed({
+  get: () => props.visible,
+  set: (val) => emit("update:visible", val),
+});
+
+function handleClose() {
+  visible.value = false;
+}
+</script>

+ 148 - 0
frontend/src/views/module_payment/institution/components/QuotaList.vue

@@ -0,0 +1,148 @@
+<template>
+  <div class="quota-list">
+    <div class="quota-list__toolbar">
+      <el-button
+        v-hasPerm="['module_payment:quota:create']"
+        type="primary"
+        size="small"
+        @click="handleCreate"
+      >
+        发放额度
+      </el-button>
+    </div>
+
+    <el-table v-loading="loading" :data="list" border stripe size="small" style="width: 100%">
+      <template #empty>
+        <el-empty :image-size="60" description="暂无额度数据" />
+      </template>
+      <el-table-column type="index" label="序号" width="60" align="center" />
+      <el-table-column prop="quota_id" label="额度ID" min-width="160" show-overflow-tooltip />
+      <el-table-column prop="employee_id" label="员工ID" min-width="140" show-overflow-tooltip />
+      <el-table-column prop="quota_type" label="额度类型" width="90">
+        <template #default="scope">
+          {{ formatQuotaType(scope.row.quota_type) }}
+        </template>
+      </el-table-column>
+      <el-table-column prop="total_amount" label="总金额" width="100" align="right">
+        <template #default="scope">
+          {{ scope.row.total_amount ? `¥${scope.row.total_amount}` : "-" }}
+        </template>
+      </el-table-column>
+      <el-table-column prop="available_amount" label="可用金额" width="100" align="right">
+        <template #default="scope">
+          {{ scope.row.available_amount ? `¥${scope.row.available_amount}` : "-" }}
+        </template>
+      </el-table-column>
+      <el-table-column prop="status" label="状态" width="90">
+        <template #default="scope">
+          <el-tag :type="STATUS_TAG_TYPE[scope.row.status]" size="small">
+            {{ STATUS_LABEL[scope.row.status] || scope.row.status }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column prop="created_time" label="创建时间" width="160" />
+      <el-table-column label="操作" width="120" align="center" fixed="right">
+        <template #default="scope">
+          <el-button
+            v-hasPerm="['module_payment:quota:detail']"
+            type="text"
+            size="small"
+            @click="handleDetail(scope.row)"
+          >
+            详情
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <div class="quota-list__pagination">
+      <el-pagination
+        v-model:current-page="pageNo"
+        v-model:page-size="pageSize"
+        :total="total"
+        :page-sizes="[5, 10, 20]"
+        layout="total, sizes, prev, pager, next"
+        small
+        @current-change="fetchList"
+        @size-change="fetchList"
+      />
+    </div>
+
+    <QuotaDetailDialog
+      v-model:visible="detailVisible"
+      :quota-id="currentQuotaId"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+import QuotaAPI, {
+  STATUS_TAG_TYPE,
+  STATUS_LABEL,
+  QUOTA_TYPE_OPTIONS,
+} from "@/api/module_payment/quota";
+import QuotaDetailDialog from "./QuotaDetailDialog.vue";
+import { ElMessage } from "element-plus";
+import { onMounted, ref } from "vue";
+
+interface Props {
+  institutionId: string;
+}
+
+const props = defineProps<Props>();
+
+const list = ref<any[]>([]);
+const total = ref(0);
+const pageNo = ref(1);
+const pageSize = ref(10);
+const loading = ref(false);
+
+const detailVisible = ref(false);
+const currentQuotaId = ref("");
+
+async function fetchList() {
+  if (!props.institutionId) return;
+  loading.value = true;
+  try {
+    const res = await QuotaAPI.listQuota({
+      page_no: pageNo.value,
+      page_size: pageSize.value,
+      institution_id: props.institutionId,
+    });
+    list.value = res.data.data?.items || [];
+    total.value = res.data.data?.total || 0;
+  } finally {
+    loading.value = false;
+  }
+}
+
+function formatQuotaType(type?: string) {
+  if (!type) return "-";
+  const option = QUOTA_TYPE_OPTIONS.find((item) => item.value === type);
+  return option ? option.label : type;
+}
+
+function handleCreate() {
+  ElMessage.info("请在独立的「额度管理」页面发放额度");
+}
+
+function handleDetail(row: any) {
+  currentQuotaId.value = row.quota_id;
+  detailVisible.value = true;
+}
+
+onMounted(() => {
+  fetchList();
+});
+</script>
+
+<style scoped>
+.quota-list__toolbar {
+  margin-bottom: 12px;
+}
+.quota-list__pagination {
+  margin-top: 16px;
+  display: flex;
+  justify-content: flex-end;
+}
+</style>

+ 38 - 0
frontend/src/views/module_payment/institution/components/RuleDetailDialog.vue

@@ -0,0 +1,38 @@
+<template>
+  <el-dialog
+    v-model="visible"
+    title="使用规则详情"
+    width="700px"
+    @close="handleClose"
+  >
+    <RuleDetail :rule-id="ruleId" />
+    <template #footer>
+      <el-button type="primary" @click="visible = false">确定</el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import RuleDetail from "@/views/module_payment/rule/components/RuleDetail.vue";
+import { computed } from "vue";
+
+interface Props {
+  visible: boolean;
+  ruleId: string;
+}
+
+const props = defineProps<Props>();
+
+const emit = defineEmits<{
+  (e: "update:visible", value: boolean): void;
+}>();
+
+const visible = computed({
+  get: () => props.visible,
+  set: (val) => emit("update:visible", val),
+});
+
+function handleClose() {
+  visible.value = false;
+}
+</script>

+ 154 - 0
frontend/src/views/module_payment/institution/components/RuleList.vue

@@ -0,0 +1,154 @@
+<template>
+  <div class="rule-list">
+    <div class="rule-list__toolbar">
+      <el-button
+        v-hasPerm="['module_payment:rule:create']"
+        type="primary"
+        size="small"
+        @click="handleCreate"
+      >
+        新增规则
+      </el-button>
+    </div>
+
+    <el-table v-loading="loading" :data="list" border stripe size="small" style="width: 100%">
+      <template #empty>
+        <el-empty :image-size="60" description="暂无使用规则" />
+      </template>
+      <el-table-column type="index" label="序号" width="60" align="center" />
+      <el-table-column prop="rule_id" label="规则ID" min-width="160" show-overflow-tooltip />
+      <el-table-column prop="name" label="规则名称" min-width="140" />
+      <el-table-column prop="max_amount" label="单笔限额" width="100" align="right">
+        <template #default="scope">
+          {{ scope.row.max_amount ? `¥${scope.row.max_amount}` : "-" }}
+        </template>
+      </el-table-column>
+      <el-table-column prop="max_day_amount" label="日限额" width="100" align="right">
+        <template #default="scope">
+          {{ scope.row.max_day_amount ? `¥${scope.row.max_day_amount}` : "-" }}
+        </template>
+      </el-table-column>
+      <el-table-column prop="max_month_amount" label="月限额" width="100" align="right">
+        <template #default="scope">
+          {{ scope.row.max_month_amount ? `¥${scope.row.max_month_amount}` : "-" }}
+        </template>
+      </el-table-column>
+      <el-table-column prop="created_time" label="创建时间" width="160" />
+      <el-table-column label="操作" width="160" align="center" fixed="right">
+        <template #default="scope">
+          <el-button
+            v-hasPerm="['module_payment:rule:detail']"
+            type="text"
+            size="small"
+            @click="handleDetail(scope.row)"
+          >
+            详情
+          </el-button>
+          <el-button
+            v-hasPerm="['module_payment:rule:delete']"
+            type="text"
+            size="small"
+            @click="handleDelete(scope.row)"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <div class="rule-list__pagination">
+      <el-pagination
+        v-model:current-page="pageNo"
+        v-model:page-size="pageSize"
+        :total="total"
+        :page-sizes="[5, 10, 20]"
+        layout="total, sizes, prev, pager, next"
+        small
+        @current-change="fetchList"
+        @size-change="fetchList"
+      />
+    </div>
+
+    <RuleDetailDialog
+      v-model:visible="detailVisible"
+      :rule-id="currentRuleId"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+import RuleAPI from "@/api/module_payment/rule";
+import RuleDetailDialog from "./RuleDetailDialog.vue";
+import { ElMessage, ElMessageBox } from "element-plus";
+import { onMounted, ref } from "vue";
+
+interface Props {
+  institutionId: string;
+}
+
+const props = defineProps<Props>();
+
+const list = ref<any[]>([]);
+const total = ref(0);
+const pageNo = ref(1);
+const pageSize = ref(10);
+const loading = ref(false);
+
+const detailVisible = ref(false);
+const currentRuleId = ref("");
+
+async function fetchList() {
+  if (!props.institutionId) return;
+  loading.value = true;
+  try {
+    const res = await RuleAPI.listRule({
+      page_no: pageNo.value,
+      page_size: pageSize.value,
+      institution_id: props.institutionId,
+    });
+    list.value = res.data.data?.items || [];
+    total.value = res.data.data?.total || 0;
+  } finally {
+    loading.value = false;
+  }
+}
+
+function handleCreate() {
+  ElMessage.info("请在独立的「使用规则」页面添加规则");
+}
+
+function handleDetail(row: any) {
+  currentRuleId.value = row.rule_id;
+  detailVisible.value = true;
+}
+
+async function handleDelete(row: any) {
+  try {
+    await ElMessageBox.confirm("确认删除该使用规则?", "警告", {
+      confirmButtonText: "确定",
+      cancelButtonText: "取消",
+      type: "warning",
+    });
+    await RuleAPI.deleteRule(row.rule_id);
+    ElMessage.success("删除成功");
+    await fetchList();
+  } catch {
+    // cancelled
+  }
+}
+
+onMounted(() => {
+  fetchList();
+});
+</script>
+
+<style scoped>
+.rule-list__toolbar {
+  margin-bottom: 12px;
+}
+.rule-list__pagination {
+  margin-top: 16px;
+  display: flex;
+  justify-content: flex-end;
+}
+</style>

+ 54 - 20
frontend/src/views/module_payment/institution/index.vue

@@ -126,7 +126,15 @@
             >
             >
               <template #default="scope">
               <template #default="scope">
                 <el-button
                 <el-button
-                  v-hasPerm="['module_payment:institution:update']"
+                  v-hasPerm="['module_payment:expense:institution:detail']"
+                  type="text"
+                  size="small"
+                  @click="handleOpenDialog('detail', scope.row.institution_id)"
+                >
+                  详情
+                </el-button>
+                <el-button
+                  v-hasPerm="['module_payment:expense:institution:modify']"
                   type="text"
                   type="text"
                   size="small"
                   size="small"
                   @click="handleOpenDialog('update', scope.row.institution_id)"
                   @click="handleOpenDialog('update', scope.row.institution_id)"
@@ -134,19 +142,28 @@
                   编辑
                   编辑
                 </el-button>
                 </el-button>
                 <el-button
                 <el-button
-                  v-hasPerm="['module_payment:institution:manual_pay']"
+                  v-hasPerm="['module_payment:expense:institution:scope:modify']"
                   type="text"
                   type="text"
                   size="small"
                   size="small"
-                  @click="handleManualPay(scope.row.institution_id)"
+                  @click="handleToggleEffective(scope.row)"
                 >
                 >
-                  手动发钱
+                  {{ scope.row.effective === "1" ? "停用" : "启用" }}
                 </el-button>
                 </el-button>
                 <el-button
                 <el-button
+                  v-hasPerm="['module_payment:expense:institution:scope:modify']"
                   type="text"
                   type="text"
                   size="small"
                   size="small"
-                  @click="handleMore(scope.row)"
+                  @click="handleOpenScopeDialog(scope.row.institution_id)"
                 >
                 >
-                  更多
+                  成员管理
+                </el-button>
+                <el-button
+                  v-hasPerm="['module_payment:expense:institution:delete']"
+                  type="text"
+                  size="small"
+                  @click="handleDelete(scope.row)"
+                >
+                  删除
                 </el-button>
                 </el-button>
               </template>
               </template>
             </el-table-column>
             </el-table-column>
@@ -158,6 +175,7 @@
     <EnhancedDialog
     <EnhancedDialog
       v-model="dialogVisible.visible"
       v-model="dialogVisible.visible"
       :title="dialogVisible.title"
       :title="dialogVisible.title"
+      width="1000px"
       @close="handleCloseDialog"
       @close="handleCloseDialog"
     >
     >
       <template v-if="dialogVisible.type === 'detail'">
       <template v-if="dialogVisible.type === 'detail'">
@@ -346,14 +364,20 @@ function handleFormSuccess() {
   refreshList();
   refreshList();
 }
 }
 
 
-async function handleDelete(institutionId?: string) {
+async function handleDelete(row: any) {
+  const institutionId = row.institution_id;
+  const enterpriseId = row.enterprise_id || enterpriseIdFromUrl.value;
   if (!institutionId) return;
   if (!institutionId) return;
+  if (!enterpriseId) {
+    ElMessage.warning("企业ID不存在,无法删除");
+    return;
+  }
   await loadingExecute({
   await loadingExecute({
     confirmMessage: "确认删除该费控制度?",
     confirmMessage: "确认删除该费控制度?",
     confirmTitle: "警告",
     confirmTitle: "警告",
     confirmType: "warning",
     confirmType: "warning",
     loadingText: "正在删除...",
     loadingText: "正在删除...",
-    action: () => InstitutionAPI.deleteInstitution(institutionId),
+    action: () => InstitutionAPI.deleteInstitution(institutionId, enterpriseId),
     onSuccess: () => {
     onSuccess: () => {
       ElMessage.success("删除成功");
       ElMessage.success("删除成功");
       refreshList();
       refreshList();
@@ -361,6 +385,28 @@ async function handleDelete(institutionId?: string) {
   });
   });
 }
 }
 
 
+async function handleToggleEffective(row: any) {
+  const institutionId = row.institution_id;
+  const enterpriseId = row.enterprise_id || enterpriseIdFromUrl.value;
+  if (!institutionId || !enterpriseId) {
+    ElMessage.warning("制度ID或企业ID不存在");
+    return;
+  }
+  const newEffective = row.effective === "1" ? "0" : "1";
+  const actionText = newEffective === "1" ? "启用" : "停用";
+  await loadingExecute({
+    confirmMessage: `确认${actionText}该制度?`,
+    confirmTitle: "提示",
+    confirmType: "warning",
+    loadingText: `正在${actionText}...`,
+    action: () => InstitutionAPI.modifyEffective(institutionId, enterpriseId, newEffective),
+    onSuccess: () => {
+      ElMessage.success(`${actionText}成功`);
+      refreshList();
+    },
+  });
+}
+
 function handleCategoryChange(categoryKey: string) {
 function handleCategoryChange(categoryKey: string) {
   activeCategory.value = categoryKey;
   activeCategory.value = categoryKey;
   // 切换分类后,重置场景为当前分类的第一个场景
   // 切换分类后,重置场景为当前分类的第一个场景
@@ -377,18 +423,6 @@ function handleSceneChange(sceneValueStr: string) {
 function handleSearch() {
 function handleSearch() {
   refreshList();
   refreshList();
 }
 }
-
-function handleManualPay(institutionId?: string) {
-  if (!institutionId) {
-    ElMessage.warning("制度ID不存在");
-    return;
-  }
-  ElMessage.info(`手动发钱功能:制度ID ${institutionId}`);
-}
-
-function handleMore(row: any) {
-  ElMessage.info(`更多操作:制度名称 ${row.name}`);
-}
 </script>
 </script>
 
 
 <style scoped>
 <style scoped>