diff --git a/video-gen-api/app/api/v1/__init__.py b/video-gen-api/app/api/v1/__init__.py index 3aa88f8b..cdbe67ce 100644 --- a/video-gen-api/app/api/v1/__init__.py +++ b/video-gen-api/app/api/v1/__init__.py @@ -16,6 +16,8 @@ from app.api.v1.video_engines import router as video_engines_router from app.api.v1.image_engines import router as image_engines_router from app.api.v1.generation_ai import router as generation_ai_router from app.api.v1.test import router as test_router +from app.api.v1.user_oauth import router as user_oauth_router +from app.api.v1.user_oauth_app import router as user_oauth_app_router api_router = APIRouter() api_router.include_router(auth_router) @@ -34,3 +36,5 @@ api_router.include_router(video_engines_router) api_router.include_router(image_engines_router) api_router.include_router(generation_ai_router) api_router.include_router(test_router) +api_router.include_router(user_oauth_router) +api_router.include_router(user_oauth_app_router) diff --git a/video-gen-api/app/api/v1/user_oauth_app.py b/video-gen-api/app/api/v1/user_oauth_app.py new file mode 100644 index 00000000..73f745a3 --- /dev/null +++ b/video-gen-api/app/api/v1/user_oauth_app.py @@ -0,0 +1,96 @@ +from fastapi import APIRouter, Depends, HTTPException, Query, status +from sqlalchemy.ext.asyncio import AsyncSession + +from app.dependencies import get_admin_user, get_db +from app.models.user import User +from app.schemas.user_oauth_app import UserOAuthAppCreate, UserOAuthAppOut, UserOAuthAppUpdate +from app.services.user_oauth_app_service import ( + create_user_oauth_app, + delete_user_oauth_app, + get_user_oauth_app_by_id, + list_user_oauth_apps, + update_user_oauth_app, +) + +router = APIRouter(prefix="/user-oauth-apps", tags=["oauth"]) + + +@router.get("/list", summary="获取用户授权应用列表") +async def list_apps( + page: int = Query(1, ge=1), + page_size: int = Query(20, ge=1, le=100), + open_type: int | None = Query(None, ge=1, le=10, description="开户方式"), + status: int | None = Query(None, ge=1, le=2, description="应用状态,1=正常,2=禁用"), + admin: User = Depends(get_admin_user), + db: AsyncSession = Depends(get_db), + app_id: str | None = Query(None, max_length=255, description="应用id"), +): + result = await list_user_oauth_apps(db, page, page_size, open_type, status, admin.id, app_id) + return { + "total": result["total"], + "page": result["page"], + "page_size": result["page_size"], + "items": [UserOAuthAppOut.model_validate(item) for item in result["items"]], + } + + +@router.post("/create", summary="创建用户授权应用", response_model=UserOAuthAppOut, status_code=status.HTTP_201_CREATED) +async def create_app( + req: UserOAuthAppCreate, + admin: User = Depends(get_admin_user), + db: AsyncSession = Depends(get_db), +): + try: + app = await create_user_oauth_app(db, req.app_id, req.secret, req.open_type, admin.id) + return app + except ValueError as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e), + ) + + +@router.get("/read/{id}", summary="获取用户授权应用详情", response_model=UserOAuthAppOut) +async def get_app( + id: str, + admin: User = Depends(get_admin_user), + db: AsyncSession = Depends(get_db), +): + app = await get_user_oauth_app_by_id(db, id) + if not app: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="应用不存在", + ) + return app + + +@router.post("/update/{id}", summary="更新用户授权应用", response_model=UserOAuthAppOut) +async def update_app( + id: str, + req: UserOAuthAppUpdate, + admin: User = Depends(get_admin_user), + db: AsyncSession = Depends(get_db), +): + app = await update_user_oauth_app(db, id, req.secret, req.open_type, req.status, admin.id) + if not app: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="应用不存在", + ) + return app + + +@router.get("/delete/{id}", summary="删除用户授权应用") +async def delete_app( + id: str, + admin: User = Depends(get_admin_user), + db: AsyncSession = Depends(get_db), +): + success = await delete_user_oauth_app(db, id, admin.id) + if not success: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="应用不存在", + ) + return {"message": "删除成功"} \ No newline at end of file diff --git a/video-gen-api/app/models/__init__.py b/video-gen-api/app/models/__init__.py index e26d99f1..0e056fc3 100644 --- a/video-gen-api/app/models/__init__.py +++ b/video-gen-api/app/models/__init__.py @@ -21,6 +21,9 @@ from app.models.chat_provider_call_log import ChatProviderCallLog from app.models.generated_resource import GeneratedResource from app.models.user_resource_month_stat import UserResourceMonthStat from app.models.user_resource_total_stat import UserResourceTotalStat +from app.models.user_oauth import UserOAuth +from app.models.user_oauth_account import UserOAuthAccount +from app.models.user_oauth_app import UserOAuthApp __all__ = [ "Base", "TimestampMixin", "SoftDeleteMixin", "engine", "async_session", @@ -31,4 +34,5 @@ __all__ = [ "MenuConfig", "RechargePackage", "OperationLog", "ChatGenerationTask", "ChatGenerationTaskEvent", "ChatProviderCallLog", "GeneratedResource", "UserResourceMonthStat", "UserResourceTotalStat", + "UserOAuth", "UserOAuthAccount", "UserOAuthApp", ] diff --git a/video-gen-api/app/models/user_oauth_app.py b/video-gen-api/app/models/user_oauth_app.py new file mode 100644 index 00000000..f2246144 --- /dev/null +++ b/video-gen-api/app/models/user_oauth_app.py @@ -0,0 +1,27 @@ +from sqlalchemy import BigInteger, ForeignKey, String +from sqlalchemy.orm import Mapped, mapped_column + +from app.models.base import Base, TimestampMixin, SoftDeleteMixin + + +class UserOAuthApp(Base, TimestampMixin, SoftDeleteMixin): + __tablename__ = "user_oauth_app" + + id: Mapped[str] = mapped_column( + String(32), primary_key=True, comment="主键" + ) + app_id: Mapped[str] = mapped_column( + String(64), unique=True, nullable=False, index=True, comment="应用id" + ) + secret: Mapped[str] = mapped_column( + String(256), nullable=False, comment="应用密钥" + ) + status: Mapped[int] = mapped_column( + BigInteger, nullable=False, default=1, comment="状态,1=正常,2=禁用" + ) + open_type: Mapped[int] = mapped_column( + BigInteger, nullable=False, index=True, comment="开户方式(1=千川,2=广告,3=本地推,4=星图,5=快手代理商,6=巨量星图,7=巨量服务单,8=腾讯服务单,9=腾讯营销K2,10=腾讯营销K3)" + ) + create_by: Mapped[str | None] = mapped_column( + String(32), ForeignKey("users.id", ondelete="SET NULL"), nullable=True, comment="创建者" + ) \ No newline at end of file diff --git a/video-gen-api/app/schemas/user_oauth_app.py b/video-gen-api/app/schemas/user_oauth_app.py new file mode 100644 index 00000000..534eddba --- /dev/null +++ b/video-gen-api/app/schemas/user_oauth_app.py @@ -0,0 +1,38 @@ +from pydantic import BaseModel, Field + +from app.schemas.common import NaiveDatetime + + +class UserOAuthAppCreate(BaseModel): + app_id: str = Field(..., max_length=64, description="应用id") + secret: str = Field(..., max_length=256, description="应用密钥") + open_type: int = Field( + ..., + ge=1, + le=10, + description="开户方式(1=千川,2=广告,3=本地推,4=星图,5=快手代理商,6=巨量星图,7=巨量服务单,8=腾讯服务单,9=腾讯营销K2,10=腾讯营销K3)", + ) + + +class UserOAuthAppUpdate(BaseModel): + secret: str | None = Field(None, max_length=256, description="应用密钥") + open_type: int | None = Field( + None, + ge=1, + le=10, + description="开户方式(1=千川,2=广告,3=本地推,4=星图,5=快手代理商,6=巨量星图,7=巨量服务单,8=腾讯服务单,9=腾讯营销K2,10=腾讯营销K3)", + ) + status: int | None = Field(None, ge=1, le=2, description="应用状态(1=正常,2=禁用)") + + +class UserOAuthAppOut(BaseModel): + id: str = Field(..., description="主键") + app_id: str = Field(..., description="应用id") + secret: str = Field(..., description="应用密钥") + status: int = Field(..., description="状态,1=正常,2=禁用") + open_type: int = Field(..., description="开户方式") + create_by: str | None = Field(None, description="创建者") + created_at: NaiveDatetime = Field(..., description="创建时间") + updated_at: NaiveDatetime = Field(..., description="更新时间") + + model_config = {"from_attributes": True} \ No newline at end of file diff --git a/video-gen-api/app/services/user_oauth_app_service.py b/video-gen-api/app/services/user_oauth_app_service.py new file mode 100644 index 00000000..2da4dd1e --- /dev/null +++ b/video-gen-api/app/services/user_oauth_app_service.py @@ -0,0 +1,123 @@ +from sqlalchemy import func, select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.models.user_oauth_app import UserOAuthApp +from app.utils.id_gen import generate_id + + +async def list_user_oauth_apps( + db: AsyncSession, + page: int = 1, + page_size: int = 20, + open_type: int | None = None, + status: int | None = None, + create_by: str | None = None, + app_id: str | None = None, +) -> dict: + query = select(UserOAuthApp).where(UserOAuthApp.deleted_at.is_(None)).order_by(UserOAuthApp.created_at.desc()) + + if open_type is not None: + query = query.where(UserOAuthApp.open_type == open_type) + + if status is not None: + query = query.where(UserOAuthApp.status == status) + + if create_by is not None: + query = query.where(UserOAuthApp.create_by == create_by) + + if app_id is not None: + query = query.where(UserOAuthApp.app_id.like(f"%{app_id}%")) + + total_result = await db.execute(select(func.count(UserOAuthApp.id)).where(UserOAuthApp.deleted_at.is_(None))) + total = total_result.scalar() or 0 + + result = await db.execute(query.offset((page - 1) * page_size).limit(page_size)) + items = result.scalars().all() + + return { + "total": total, + "page": page, + "page_size": page_size, + "items": items, + } + + +async def get_user_oauth_app_by_id(db: AsyncSession, id: str) -> UserOAuthApp | None: + result = await db.execute( + select(UserOAuthApp).where(UserOAuthApp.id == id, UserOAuthApp.deleted_at.is_(None)).limit(1) + ) + return result.scalar_one_or_none() + + +async def get_user_oauth_app_by_app_id(db: AsyncSession, app_id: str) -> UserOAuthApp | None: + result = await db.execute( + select(UserOAuthApp).where(UserOAuthApp.app_id == app_id, UserOAuthApp.deleted_at.is_(None)).limit(1) + ) + return result.scalar_one_or_none() + + +async def create_user_oauth_app( + db: AsyncSession, + app_id: str, + secret: str, + open_type: int, + create_by: str | None = None, +) -> UserOAuthApp: + existing = await get_user_oauth_app_by_app_id(db, app_id) + if existing: + raise ValueError("应用id已存在") + + app = UserOAuthApp( + id=generate_id(), + app_id=app_id, + secret=secret, + open_type=open_type, + create_by=create_by, + ) + db.add(app) + await db.flush() + return app + + +async def update_user_oauth_app( + db: AsyncSession, + id: str, + secret: str | None = None, + open_type: int | None = None, + status: int | None = None, + create_by: str | None = None, +) -> UserOAuthApp | None: + app = await get_user_oauth_app_by_id(db, id) + if not app: + return None + + if secret is not None: + app.secret = secret + if open_type is not None: + app.open_type = open_type + if status is not None: + app.status = status + if create_by is not None: + app.create_by = create_by + + await db.flush() + return app + + +async def delete_user_oauth_app(db: AsyncSession, id: str, create_by: str | None = None) -> bool: + app = await get_user_oauth_app_by_id(db, id) + if not app: + return False + + app.deleted_at = func.now() + if create_by is not None: + app.create_by = create_by + await db.flush() + return True + + +async def get_apps_by_open_type(db: AsyncSession, open_type: int) -> list[UserOAuthApp]: + result = await db.execute( + select(UserOAuthApp).where(UserOAuthApp.open_type == open_type, UserOAuthApp.deleted_at.is_(None)) + ) + return result.scalars().all() \ No newline at end of file