Sfoglia il codice sorgente

feat: 更新apikey

husenlin 1 mese fa
parent
commit
edd4797068

+ 136 - 0
backend/app/core/apikey.py

@@ -0,0 +1,136 @@
+import time
+from typing import Optional, TypeVar, Generic, Type
+
+from fastapi import Request, Depends, HTTPException
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from app.core.dependencies import db_getter
+from app.api.v1.module_system.auth.schema import AuthSchema
+from app.plugin.module_payment.apikey.service import TenantApiKeyService
+from app.plugin.module_payment.apikey.schema import ApiKeyPayload
+
+
+T = TypeVar("T")
+
+
+class TenantApiKeyAuth(Generic[T]):
+    """
+    租户API Key认证
+    """
+
+    def __init__(self, data_type: Type[T], auto_error: bool = True):
+        self.data_type: Type[T] = data_type
+        self.auto_error: bool = auto_error
+
+    async def __call__(
+        self,
+        request: Request,
+        db: AsyncSession = Depends(db_getter)
+    ) -> Optional[ApiKeyPayload[T]]:
+        """
+        验证API Key
+        """
+        # 记录请求开始时间
+        request.state.start_time = time.time()
+
+        # 获取 Authorization 头
+        authorization = request.headers.get("Authorization", None)
+
+        if not authorization:
+            if self.auto_error:
+                raise HTTPException(status_code=401, detail="Authorization header required")
+            return None
+
+        # 获取 Signature 头
+        signature = request.headers.get("Signature", None)
+
+        if not signature:
+            if self.auto_error:
+                raise HTTPException(status_code=401, detail="Signature header required")
+            return None
+
+        # ========================= 验证 ApiKey =========================
+
+        # 检查是否为ApiKey认证
+        if not authorization.startswith("ApiKey "):
+            if self.auto_error:
+                raise HTTPException(status_code=401, detail="Invalid authorization format")
+            return None
+
+        # 提取API Key和签名
+        api_key = authorization[7:]
+        # 创建AuthSchema对象
+        temp_auth = AuthSchema(user=None, db=db, tenant_id=1, check_data_scope=False)
+
+        # 验证API Key
+        api_key_obj = await TenantApiKeyService.validate_api_key(temp_auth, api_key)
+        if not api_key_obj:
+            # await TenantApiKeyService.log_api_call(
+            #     auth=temp_auth,
+            #     api_key_id=None,
+            #     tenant_id=temp_auth.tenant_id,
+            #     endpoint=str(request.url.path),
+            #     method=request.method,
+            #     request_ip=request.client.host if request.client else "unknown",
+            #     request_data=None,
+            #     response_code=401,
+            #     start_time=request.state.start_time,
+            # )
+
+            if self.auto_error:
+                raise HTTPException(status_code=401, detail="Invalid API Key")
+            return None
+
+        # ========================= 验证 Signature =========================
+
+        # 获取请求数据
+        try:
+            request_data = await request.json()
+        except:
+            request_data = {}
+
+        if not TenantApiKeyService.verify_signature(api_key_obj.api_secret, request_data, signature):
+            # await TenantApiKeyService.log_api_call(
+            #     auth=temp_auth,
+            #     api_key_id=api_key_obj.id,
+            #     tenant_id=api_key_obj.tenant_id,
+            #     endpoint=str(request.url.path),
+            #     method=request.method,
+            #     request_ip=request.client.host if request.client else "unknown",
+            #     request_data=request_data,
+            #     response_code=401,
+            #     start_time=request.state.start_time,
+            # )
+
+            if self.auto_error:
+                raise HTTPException(status_code=401, detail="Invalid Signature")
+            return None
+
+        await TenantApiKeyService.log_api_call(
+            auth=temp_auth,
+            api_key_id=api_key_obj.id,
+            tenant_id=api_key_obj.tenant_id,
+            endpoint=str(request.url.path),
+            method=request.method,
+            request_ip=request.client.host if request.client else "unknown",
+            request_data=None,  # 避免记录敏感数据
+            response_code=200,
+            start_time=request.state.start_time,
+        )
+
+        # 更新最后使用时间
+        await TenantApiKeyService.update_last_used(temp_auth, api_key_obj.id)
+
+        # 将API Key对象存储到请求状态
+        # request.state.api_key = api_key_obj
+        # request.state.tenant_id = api_key_obj.tenant_id
+
+        parsed_data = self.data_type(**request_data) if isinstance(request_data, dict) else request_data
+
+        return ApiKeyPayload(
+            api_key=api_key_obj.api_key,
+            api_secret=api_key_obj.api_secret,
+            tenant_id=api_key_obj.tenant_id,
+            auth=AuthSchema(user=None, db=db, tenant_id=api_key_obj.tenant_id, check_data_scope=False),
+            data=parsed_data,
+        )

+ 36 - 16
backend/app/core/exceptions.py

@@ -77,8 +77,12 @@ def handle_exception(app: FastAPI) -> None:
         返回:
         - JSONResponse: 包含错误信息的 JSON 响应。
         """
-        log.error(
-            f"[自定义异常] {request.method} {request.url.path} | 错误码: {exc.code} | 错误信息: {exc.msg} | 详情: {exc.data}"
+        log.opt(exception=False).error(
+            "[自定义异常] {} {} | 错误码: {} | 错误信息: {}",
+            request.method,
+            request.url.path,
+            exc.code,
+            exc.msg
         )
         return ErrorResponse(
             msg=exc.msg,
@@ -99,10 +103,13 @@ def handle_exception(app: FastAPI) -> None:
         返回:
         - JSONResponse: 包含错误信息的 JSON 响应。
         """
-        log.error(
-            f"[HTTP异常] {request.method} {request.url.path} | 状态码: {exc.status_code} | 错误信息: {exc.detail}"
+        log.opt(exception=False).error(
+            "[HTTP异常] {} {} | 错误信息: {}",
+            request.method,
+            request.url.path,
+            str(exc)
         )
-        return ErrorResponse(msg=exc.detail, status_code=exc.status_code)
+        return ErrorResponse(msg=exc.detail or "请求错误", status_code=exc.status_code)
 
     @app.exception_handler(RequestValidationError)
     async def ValidationExceptionHandler(
@@ -134,8 +141,11 @@ def handle_exception(app: FastAPI) -> None:
                 if "," in msg
                 else msg.replace("Value error", "").strip()
             )
-        log.error(
-            f"[参数验证异常] {request.method} {request.url.path} | 错误信息: {msg} | 原始错误: {exc.errors()}"
+        log.opt(exception=False).error(
+            "[参数验证异常] {} {} | 错误信息: {}",
+            request.method,
+            request.url.path,
+            str(exc),
         )
         return ErrorResponse(
             msg=str(msg),
@@ -157,8 +167,10 @@ def handle_exception(app: FastAPI) -> None:
         返回:
         - JSONResponse: 包含错误信息的 JSON 响应。
         """
-        log.error(
-            f"[响应验证异常] {request.method} {request.url.path} | 错误信息: 响应数据格式错误 | 详情: {exc.errors()}"
+        log.opt(exception=True).error(
+            "[响应验证异常] {} {} | 错误信息: 响应数据格式错误",
+            request.method,
+            request.url.path,
         )
         return ErrorResponse(
             msg="服务器响应格式错误",
@@ -182,8 +194,11 @@ def handle_exception(app: FastAPI) -> None:
         exc_type = type(exc).__name__
 
         # 对于生产环境,返回通用错误消息
-        log.error(
-            f"[数据库异常] {request.method} {request.url.path} | 错误类型: {exc_type} | 错误详情: {exc!s}"
+        log.opt(exception=True).error(
+            "[数据库异常] {} {} | 错误类型: {}",
+            request.method,
+            request.url.path,
+            exc_type
         )
         return ErrorResponse(
             msg=f"{error_msg}: {exc_type}",
@@ -203,7 +218,7 @@ def handle_exception(app: FastAPI) -> None:
         返回:
         - JSONResponse: 包含错误信息的 JSON 响应。
         """
-        log.error(f"[值异常] {request.method} {request.url.path} | 错误信息: {exc!s}")
+        log.opt(exception=False).error("[值异常] {} {} | 错误信息: {}", request.method, request.url.path, str(exc))
         return ErrorResponse(msg=str(exc), status_code=status.HTTP_400_BAD_REQUEST)
 
     @app.exception_handler(FieldValidationError)
@@ -220,8 +235,9 @@ def handle_exception(app: FastAPI) -> None:
         返回:
         - JSONResponse: 包含错误信息的 JSON 响应。
         """
-        log.error(f"[字段验证异常] {request.method} {request.url.path} | 错误信息: {exc.message}")
-        return ErrorResponse(msg=exc.message, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
+        log.opt(exception=False).error("[字段验证异常] {} {} | 错误信息: {}",
+                                       request.method, request.url.path, exc.message)
+        return ErrorResponse(msg=exc.message or "字段验证异常", status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
 
     @app.exception_handler(Exception)
     async def AllExceptionHandler(request: Request, exc: Exception) -> JSONResponse:
@@ -236,8 +252,12 @@ def handle_exception(app: FastAPI) -> None:
         - JSONResponse: 包含错误信息的 JSON 响应。
         """
         exc_type = type(exc).__name__
-        log.error(
-            f"[未捕获异常] {request.method} {request.url.path} | 错误类型: {exc_type} | 错误详情: {exc!s}"
+        log.opt(exception=False).error(
+            "[未捕获异常] {} {} | 错误类型: {} | 错误信息: {}",
+            request.method,
+            request.url.path,
+            exc_type,
+            str(exc)
         )
         # 对于未捕获的异常,返回通用错误信息
         return ErrorResponse(

+ 0 - 132
backend/app/core/tenant_api_auth.py

@@ -1,132 +0,0 @@
-import time
-from typing import Optional, Dict, Any, Callable, Awaitable
-
-from fastapi import Request, Response, Depends, HTTPException, security
-from sqlalchemy.ext.asyncio import AsyncSession
-
-from app.core.dependencies import db_getter
-from app.api.v1.module_system.auth.schema import AuthSchema
-from app.plugin.module_payment.apikey.service import TenantApiKeyService
-from app.plugin.module_payment.apikey.model import TenantApiKeyModel
-
-
-class TenantApiKeyAuth:
-    """
-    租户API Key认证
-    """
-    def __init__(self, auto_error: bool = True):
-        self.auto_error = auto_error
-
-    async def __call__(self, request: Request, db: AsyncSession = Depends(db_getter)) -> Optional[TenantApiKeyModel]:
-        """
-        验证API Key
-        """
-        # 记录请求开始时间
-        request.state.start_time = time.time()
-        
-        # 获取Authorization头
-        authorization = request.headers.get("Authorization", None)
-        
-        if not authorization:
-            if self.auto_error:
-                raise HTTPException(status_code=401, detail="Authorization header required")
-            return None
-        
-        # 检查是否为ApiKey认证
-        if not authorization.startswith("ApiKey "):
-            if self.auto_error:
-                raise HTTPException(status_code=401, detail="Invalid authorization format")
-            return None
-        
-        # 提取API Key和签名
-        auth_str = authorization[7:]
-        if ":" in auth_str:
-            api_key, signature = auth_str.split(":", 1)
-        else:
-            api_key, signature = auth_str, None
-        
-        # 创建AuthSchema对象
-        temp_auth = AuthSchema(
-            user=None,
-            check_data_scope=False,
-            db=db,
-            tenant_id=0
-        )
-        
-        # 验证API Key
-        api_key_obj = await TenantApiKeyService.validate_api_key(temp_auth, api_key)
-        if not api_key_obj:
-            # 记录失败日志
-            await TenantApiKeyService.log_api_call(
-                auth=temp_auth,
-                api_key_id=None,
-                tenant_id=0,
-                endpoint=str(request.url.path),
-                method=request.method,
-                request_ip=request.client.host if request.client else "unknown",
-                request_data=None,
-                response_code=401,
-                start_time=request.state.start_time,
-            )
-            if self.auto_error:
-                raise HTTPException(status_code=401, detail="Invalid API Key")
-            return None
-        
-        # 验证签名(如果提供)
-        if signature:
-            # 获取请求数据
-            try:
-                request_data = await request.json()
-            except Exception:
-                request_data = {}
-            
-            if not TenantApiKeyService.verify_signature(api_key_obj.api_secret, request_data, signature):
-                # 记录失败日志
-                await TenantApiKeyService.log_api_call(
-                    auth=temp_auth,
-                    api_key_id=api_key_obj.id,
-                    tenant_id=api_key_obj.tenant_id,
-                    endpoint=str(request.url.path),
-                    method=request.method,
-                    request_ip=request.client.host if request.client else "unknown",
-                    request_data=request_data,
-                    response_code=401,
-                    start_time=request.state.start_time,
-                )
-                if self.auto_error:
-                    raise HTTPException(status_code=401, detail="Invalid Signature")
-                return None
-        
-        # 记录成功日志
-        await TenantApiKeyService.log_api_call(
-            auth=temp_auth,
-            api_key_id=api_key_obj.id,
-            tenant_id=api_key_obj.tenant_id,
-            endpoint=str(request.url.path),
-            method=request.method,
-            request_ip=request.client.host if request.client else "unknown",
-            request_data=None,  # 避免记录敏感数据
-            response_code=200,
-            start_time=request.state.start_time,
-        )
-        
-        # 更新最后使用时间
-        await TenantApiKeyService.update_last_used(temp_auth, api_key_obj.id)
-        
-        # 将API Key对象存储到请求状态
-        request.state.api_key = api_key_obj
-        request.state.tenant_id = api_key_obj.tenant_id
-        
-        return api_key_obj
-
-
-async def get_tenant_from_api_key(
-    request: Request,
-    api_key: Optional[TenantApiKeyModel] = Depends(TenantApiKeyAuth(auto_error=False)),
-) -> Optional[int]:
-    """
-    从API Key获取租户ID
-    """
-    if api_key:
-        return api_key.tenant_id
-    return None

+ 2 - 1
backend/app/plugin/module_payment/__init__.py

@@ -4,5 +4,6 @@ from .expense import *
 from .notification import *
 from .points import *
 from .department import *
+from .openapi import *
 
-__all__ = ["employee", "enterprise", "expense", "notification", "points", "department"]
+__all__ = ["employee", "enterprise", "expense", "notification", "points", "department", "openapi"]

+ 1 - 1
backend/app/plugin/module_payment/account/schema.py

@@ -105,7 +105,7 @@ class AccountTransferSchema(BaseModel):
     """资金专户转账请求"""
 
     account_book_id: str = Field(description="付款方资金专户号")
-    enterprise_id: Optional[str] = Field(description="企业ID")
+    enterprise_id: Optional[str] = Field(default=None, description="企业ID")
     out_biz_no: Optional[str] = Field(default=None, description="商家侧订单号")
     # 转账总金额,单位为元,精确到小数点后两位
     amount: Decimal = Field(max_digits=10, decimal_places=2, gt=0, description="转账金额")

+ 11 - 28
backend/app/plugin/module_payment/apikey/controller.py

@@ -1,8 +1,9 @@
 from typing import Annotated
-from fastapi import APIRouter, Depends, HTTPException, Path
+from fastapi import APIRouter, Depends, Path
 from fastapi.responses import JSONResponse
 
 from app.api.v1.module_system.auth.schema import AuthSchema
+from app.core.router_class import OperationLogRoute
 from app.plugin.module_payment.apikey.schema import (
     TenantApiKeyCreate,
     TenantApiKeyResponse,
@@ -11,12 +12,12 @@ from app.plugin.module_payment.apikey.schema import (
     ApiKeyQueryParam,
 )
 from app.plugin.module_payment.apikey.service import TenantApiKeyService
-from app.common.response import ResponseSchema, SuccessResponse
+from app.common.response import ResponseSchema, SuccessResponse, ErrorResponse
 from app.core.base_params import PaginationQueryParam
 from app.core.dependencies import AuthPermission
 from app.core.logger import log
 
-ApiKeyRouter = APIRouter(prefix="/api-key", tags=["租户API Key管理"])
+ApiKeyRouter = APIRouter(route_class=OperationLogRoute, prefix="/api-key", tags=["API Key管理"])
 
 
 @ApiKeyRouter.post(
@@ -29,7 +30,7 @@ async def create_api_key_controller(
     data: TenantApiKeyCreate,
     auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:tenant:api-key:create"]))],
 ) -> JSONResponse:
-    tenant_id = data.tenant_id or auth.tenant_id
+    tenant_id = auth.tenant_id
     api_key_obj = await TenantApiKeyService.create_api_key(
         auth=auth,
         tenant_id=tenant_id,
@@ -45,7 +46,8 @@ async def create_api_key_controller(
             status=api_key_obj.status,
             expired_at=api_key_obj.expired_at,
             created_time=api_key_obj.created_time,
-        )
+        ),
+        msg="创建APIKey成功"
     )
 
 
@@ -60,15 +62,12 @@ async def get_api_key_list_controller(
     search: Annotated[ApiKeyQueryParam, Depends()],
     auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:tenant:api-key:query"]))],
 ) -> JSONResponse:
-    target_tenant_id = search.tenant_id or auth.tenant_id
     result_dict = await TenantApiKeyService.get_api_key_page_service(
         auth=auth,
         page_no=page.page_no,
         page_size=page.page_size,
-        tenant_id=target_tenant_id,
-        status=search.status,
+        search=search
     )
-    log.info("查询API Key成功")
     return SuccessResponse(data=result_dict)
 
 
@@ -83,21 +82,7 @@ async def update_api_key_status_controller(
     data: TenantApiKeyUpdate,
     auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:tenant:api-key:update"]))],
 ) -> JSONResponse:
-    api_key_obj = await TenantApiKeyService.update_api_key_status(auth=auth, api_key_id=api_key_id, status=data.status)
-    if not api_key_obj:
-        raise HTTPException(status_code=404, detail="API Key不存在")
-    log.info(f"更新API Key状态成功: ID={api_key_id}, 状态={data.status}")
-    return SuccessResponse(
-        data=TenantApiKeyListResponse(
-            id=api_key_obj.id,
-            api_key=api_key_obj.api_key,
-            status=api_key_obj.status,
-            expired_at=api_key_obj.expired_at,
-            last_used_at=api_key_obj.last_used_at,
-            created_time=api_key_obj.created_time,
-            description=api_key_obj.description,
-        )
-    )
+    return ErrorResponse(msg="暂不支持操作")
 
 
 @ApiKeyRouter.delete(
@@ -110,8 +95,6 @@ async def delete_api_key_controller(
     api_key_id: Annotated[int, Path(description="API Key ID")],
     auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:tenant:api-key:delete"]))],
 ) -> JSONResponse:
-    success = await TenantApiKeyService.delete_api_key(auth=auth, api_key_id=api_key_id)
-    if not success:
-        raise HTTPException(status_code=404, detail="API Key不存在")
+    await TenantApiKeyService.delete_api_key(auth=auth, api_key_id=api_key_id)
     log.info(f"删除API Key成功: ID={api_key_id}")
-    return SuccessResponse(data={"success": True})
+    return SuccessResponse(msg="删除APIKey成功")

+ 4 - 46
backend/app/plugin/module_payment/apikey/crud.py

@@ -29,7 +29,7 @@ class TenantApiKeyCRUD(CRUDBase[TenantApiKeyModel, TenantApiKeyCreate, TenantApi
             preload=[],
             api_key=api_key,
             status="0",
-            expired_at=("None", None),
+            # expired_at=("None", None),
         )
 
     async def create_crud(
@@ -59,30 +59,8 @@ class TenantApiKeyCRUD(CRUDBase[TenantApiKeyModel, TenantApiKeyCreate, TenantApi
             api_key_obj.last_used_at = datetime.now()
             await self.auth.db.flush()
 
-    async def delete_crud(self, api_key_id: int) -> bool:
-        try:
-            await self.delete(ids=[api_key_id])
-            return True
-        except Exception:
-            return False
-
-    async def get_list_crud(
-        self,
-        tenant_id: int | None = None,
-        status: str | None = None,
-        skip: int = 0,
-        limit: int = 100,
-    ) -> Sequence[TenantApiKeyModel]:
-        search: dict[str, Any] = {}
-        if tenant_id:
-            search["tenant_id"] = tenant_id
-        if status:
-            search["status"] = status
-        return await self.list(
-            search=search if search else None,
-            order_by=[{"created_time": "desc"}],
-        )
-
+    async def delete_crud(self, api_key_id: int) -> None:
+        await self.delete(ids=[api_key_id])
 
 class TenantApiLogCRUD(CRUDBase[TenantApiLogModel, TenantApiLogCreate, TenantApiLogCreate]):
     """租户API调用日志数据层"""
@@ -112,24 +90,4 @@ class TenantApiLogCRUD(CRUDBase[TenantApiLogModel, TenantApiLogCreate, TenantApi
             response_code=response_code,
             response_time=response_time,
         )
-        return await self.create(data=data, skip_tenant_id=True)
-
-    async def get_list_crud(
-        self,
-        tenant_id: int | None = None,
-        api_key_id: int | None = None,
-        endpoint: str | None = None,
-        skip: int = 0,
-        limit: int = 100,
-    ) -> Sequence[TenantApiLogModel]:
-        search: dict[str, Any] = {}
-        if tenant_id:
-            search["tenant_id"] = tenant_id
-        if api_key_id:
-            search["api_key_id"] = api_key_id
-        if endpoint:
-            search["endpoint"] = ("like", endpoint)
-        return await self.list(
-            search=search if search else None,
-            order_by=[{"created_time": "desc"}],
-        )
+        return await self.create(data=data, skip_tenant_id=True)

+ 2 - 0
backend/app/plugin/module_payment/apikey/model.py

@@ -4,6 +4,7 @@ from typing import Optional
 from sqlalchemy import String, DateTime, ForeignKey, Text, Integer
 from sqlalchemy.orm import Mapped, mapped_column
 
+from app.common.enums import PermissionFilterStrategy
 from app.core.base_model import ModelMixin, TenantMixin
 
 
@@ -13,6 +14,7 @@ class TenantApiKeyModel(ModelMixin, TenantMixin):
     """
 
     __tablename__ = "sys_tenant_api_key"
+    __permission_strategy__ = PermissionFilterStrategy.ENTERPRISE_BASED
 
     # 核心字段
     api_key: Mapped[str] = mapped_column(

+ 0 - 52
backend/app/plugin/module_payment/apikey/openapi_controller.py

@@ -1,52 +0,0 @@
-from typing import Annotated
-
-from agno.agent import AgentSession
-from fastapi import APIRouter, Depends, HTTPException
-from fastapi.responses import JSONResponse
-
-from app.core.dependencies import db_getter
-from app.plugin.module_payment.apikey.model import TenantApiKeyModel
-from app.common.response import ResponseSchema, SuccessResponse
-from app.core.logger import log
-from app.core.tenant_api_auth import TenantApiKeyAuth
-from app.plugin.module_payment.account.schema import AccountTransferSchema
-from app.plugin.module_payment.account.service import AccountService
-from app.api.v1.module_system.auth.schema import AuthSchema
-
-from sqlalchemy.ext.asyncio import AsyncSession
-
-
-OpenapiRouter = APIRouter(
-    prefix="/openapi",
-    tags=["开放接口"],
-)
-
-
-@OpenapiRouter.post(
-    "/account/transfer",
-    summary="租户资金专户转账",
-    description="从资金专户转账到支付宝账户/银行卡/资金专户(支持API Key认证)",
-    response_model=ResponseSchema[dict],
-)
-async def tenant_transfer_controller(
-    db: Annotated[AsyncSession, Depends(db_getter)],
-    data: AccountTransferSchema,
-    api_key: TenantApiKeyModel = Depends(TenantApiKeyAuth()),
-) -> JSONResponse:
-    """
-    租户资金专户转账接口
-
-    支持通过API Key进行认证,适用于平台租户调用
-    """
-    # 构建认证信息
-    auth = AuthSchema(
-        user=None,
-        check_data_scope=False,
-        tenant_id=api_key.tenant_id,
-        db=db,
-    )
-
-    # 执行转账
-    result = await AccountService.transfer_service(auth=auth, data=data)
-    log.info(f"租户资金专户转账发起成功: 企业: {data.enterprise_id}, 金额: {data.amount}")
-    return SuccessResponse(data=result, msg="")

+ 16 - 5
backend/app/plugin/module_payment/apikey/schema.py

@@ -1,8 +1,11 @@
 from datetime import datetime
-from typing import Optional
-
 from fastapi import Query
+from typing import Generic, TypeVar, Optional
 from pydantic import BaseModel, Field
+from app.api.v1.module_system.auth.schema import AuthSchema
+
+
+T = TypeVar("T")
 
 
 class TenantApiKeyCreate(BaseModel):
@@ -75,8 +78,16 @@ class ApiKeyQueryParam:
 
     def __init__(
         self,
-        tenant_id: int | None = Query(None, description="租户ID,默认使用当前用户的租户ID"),
+        # tenant_id: int | None = Query(None, description="租户ID,默认使用当前用户的租户ID"),
         status: str | None = Query(None, description="状态:0-正常,1-禁用"),
     ) -> None:
-        self.tenant_id = tenant_id
-        self.status = status
+        # self.tenant_id = tenant_id
+        self.status = status
+
+
+class ApiKeyPayload(BaseModel, Generic[T]):
+    api_key: str = Field(description="API Key")
+    api_secret: str = Field(description="API Secret")
+    tenant_id: int = Field(description="租户ID")
+    auth: AuthSchema = Field(description="认证")
+    data: Optional[T] = Field(default=None, description="业务数据载荷")

+ 9 - 13
backend/app/plugin/module_payment/apikey/service.py

@@ -8,7 +8,7 @@ from typing import Optional
 from app.api.v1.module_system.auth.schema import AuthSchema
 from app.plugin.module_payment.apikey.crud import TenantApiKeyCRUD, TenantApiLogCRUD
 from app.plugin.module_payment.apikey.model import TenantApiKeyModel
-from app.plugin.module_payment.apikey.schema import TenantApiKeyListResponse
+from app.plugin.module_payment.apikey.schema import TenantApiKeyListResponse, ApiKeyQueryParam
 
 
 class TenantApiKeyService:
@@ -18,7 +18,8 @@ class TenantApiKeyService:
     def generate_api_key(tenant_id: int) -> tuple[str, str]:
         random_part = secrets.token_hex(32)
         timestamp = str(int(time.time()))
-        api_key = f"TENANT_{tenant_id}_{timestamp}_{random_part[:16]}"
+        secure_number = secrets.randbelow(9000) + 1000
+        api_key = f"{secure_number}{tenant_id}{timestamp}{random_part[:16]}"
         api_secret = secrets.token_hex(64)
         return api_key, api_secret
 
@@ -63,20 +64,15 @@ class TenantApiKeyService:
         auth: AuthSchema,
         page_no: int,
         page_size: int,
-        tenant_id: Optional[int] = None,
-        status: Optional[str] = None,
+        search: Optional[ApiKeyQueryParam] = None,
+        order_by: Optional[list[dict[str, str]]] = None,
     ) -> dict:
         offset = (page_no - 1) * page_size
-        search = {}
-        if tenant_id:
-            search["tenant_id"] = tenant_id
-        if status:
-            search["status"] = status
         return await TenantApiKeyCRUD(auth).page(
             offset=offset,
             limit=page_size,
-            order_by=[{"created_time": "desc"}],
-            search=search if search else {},
+            order_by=order_by or [{"created_time": "desc"}],
+            search=search.__dict__ if search else {},
             out_schema=TenantApiKeyListResponse,
         )
 
@@ -89,8 +85,8 @@ class TenantApiKeyService:
         return await TenantApiKeyCRUD(auth).update_status_crud(api_key_id=api_key_id, status=status)
 
     @staticmethod
-    async def delete_api_key(auth: AuthSchema, api_key_id: int) -> bool:
-        return await TenantApiKeyCRUD(auth).delete_crud(api_key_id=api_key_id)
+    async def delete_api_key(auth: AuthSchema, api_key_id: int) -> None:
+        await TenantApiKeyCRUD(auth).delete_crud(api_key_id=api_key_id)
 
     @staticmethod
     async def log_api_call(

+ 0 - 0
backend/app/plugin/module_payment/openapi/__init__.py


+ 47 - 0
backend/app/plugin/module_payment/openapi/controller.py

@@ -0,0 +1,47 @@
+from typing import Annotated
+
+from fastapi import APIRouter, Depends
+from starlette.responses import JSONResponse
+
+from app.core.dependencies import db_getter
+from app.core.router_class import OperationLogRoute
+from app.common.response import ResponseSchema, SuccessResponse, ErrorResponse
+from app.core.apikey import TenantApiKeyAuth
+from app.plugin.module_payment.account import AccountService
+from app.plugin.module_payment.account.schema import AccountTransferSchema
+from app.api.v1.module_system.auth.schema import AuthSchema
+
+from sqlalchemy.ext.asyncio import AsyncSession
+from app.core.logger import log
+from app.plugin.module_payment.apikey.schema import ApiKeyPayload
+
+OpenapiRouter = APIRouter(
+    route_class=OperationLogRoute,
+    prefix="/openapi",
+    tags=["开放接口"],
+)
+
+
+@OpenapiRouter.post(
+    "/account/transfer",
+    summary="资金专户转账",
+    description="从资金专户转账到支付宝账户/银行卡/资金专户",
+    response_model=ResponseSchema[dict],
+)
+async def openapi_transfer_controller(
+    apikey: Annotated[ApiKeyPayload[AccountTransferSchema], Depends(TenantApiKeyAuth(AccountTransferSchema))],
+) -> JSONResponse:
+    """
+    租户资金专户转账接口
+
+    支持通过API Key进行认证,适用于平台租户调用
+    """
+    auth = apikey.auth
+    transfer_data = apikey.data
+    if transfer_data is None:
+        return ErrorResponse(msg="缺少必需参数")
+
+    # 执行转账
+    result = await AccountService.transfer_service(auth=auth, data=transfer_data)
+    log.info(f"租户资金专户转账发起成功: 企业: {transfer_data.enterprise_id}, 金额: {transfer_data.amount}")
+    return SuccessResponse(data=result, msg="转账申请已提交")

+ 0 - 0
backend/app/plugin/module_payment/openapi/schema.py