531aacb57e
CI/CD Pipeline / Validate Code Quality And Tests (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Lint (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
192 lines
6.8 KiB
Python
Executable File
192 lines
6.8 KiB
Python
Executable File
"""Title library CRUD routes."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Optional
|
|
|
|
from app.api.routes._helpers import get_user_plan
|
|
from app.auth import AuthenticatedUser, get_current_user
|
|
from app.dependencies import get_db_session, get_user_repository
|
|
from app.schemas.title_library import (
|
|
CreateTitleLibraryRequest,
|
|
ListTitleLibraryResponse,
|
|
TitleLibraryItemResponse,
|
|
UpdateTitleLibraryRequest,
|
|
)
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, Response, status
|
|
from sqlalchemy.orm import Session
|
|
|
|
from packages.adapters.sqlalchemy_impl.title_library_repository import SQLAlchemyTitleLibraryRepository
|
|
from packages.application.title_library.commands import (
|
|
CreateTitleLibraryCommand,
|
|
PickTitleCommand,
|
|
UpdateTitleLibraryCommand,
|
|
)
|
|
from packages.application.title_library.use_cases import (
|
|
CreateTitleLibraryUseCase,
|
|
DeleteTitleLibraryUseCase,
|
|
GetTitleLibraryUseCase,
|
|
ListTitleLibraryUseCase,
|
|
NotFoundError,
|
|
PickTitleUseCase,
|
|
QuotaExceededError,
|
|
UpdateTitleLibraryUseCase,
|
|
)
|
|
from packages.ports.user_repository import UserRepository
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
def _get_title_repository(session: Session = Depends(get_db_session)) -> SQLAlchemyTitleLibraryRepository:
|
|
return SQLAlchemyTitleLibraryRepository(session)
|
|
|
|
|
|
def _to_response(item) -> TitleLibraryItemResponse:
|
|
return TitleLibraryItemResponse(
|
|
id=item.id,
|
|
user_id=item.user_id,
|
|
name=item.name,
|
|
text=item.text,
|
|
category=item.category,
|
|
description=item.description,
|
|
tags=item.tags,
|
|
usage_count=item.usage_count,
|
|
is_active=item.is_active,
|
|
created_at=item.created_at,
|
|
updated_at=item.updated_at,
|
|
)
|
|
|
|
|
|
@router.get("", response_model=ListTitleLibraryResponse)
|
|
def list_titles(
|
|
category: Optional[str] = Query(None),
|
|
skip: int = Query(0, ge=0),
|
|
limit: int = Query(50, ge=1, le=200),
|
|
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
|
title_repository: SQLAlchemyTitleLibraryRepository = Depends(_get_title_repository),
|
|
) -> ListTitleLibraryResponse:
|
|
user_id = authenticated_user.user.id
|
|
use_case = ListTitleLibraryUseCase(title_repository)
|
|
items = use_case.execute(user_id, category=category, skip=skip, limit=limit)
|
|
total = title_repository.count_by_user(user_id)
|
|
return ListTitleLibraryResponse(
|
|
items=[_to_response(i) for i in items],
|
|
total=total,
|
|
)
|
|
|
|
|
|
@router.post("/pick", response_model=TitleLibraryItemResponse)
|
|
def pick_title(
|
|
category: Optional[str] = Query(None, description="按分类筛选,不填则从全部标题中选"),
|
|
exclude_ids: Optional[str] = Query(
|
|
None,
|
|
description="排除的标题ID(逗号分隔),用于批量生成时避免重复",
|
|
),
|
|
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
|
title_repository: SQLAlchemyTitleLibraryRepository = Depends(_get_title_repository),
|
|
) -> TitleLibraryItemResponse:
|
|
"""智能选择一个标题。
|
|
|
|
策略:优先使用次数少的,从最少的前5个中随机选一个,兼顾公平和多样性。
|
|
"""
|
|
user_id = authenticated_user.user.id
|
|
exclude_list: list[str] = []
|
|
if exclude_ids:
|
|
exclude_list = [t.strip() for t in exclude_ids.split(",") if t.strip()]
|
|
|
|
use_case = PickTitleUseCase(title_repository)
|
|
item = use_case.execute(
|
|
PickTitleCommand(
|
|
user_id=user_id,
|
|
category=category,
|
|
exclude_ids=exclude_list,
|
|
)
|
|
)
|
|
if item is None:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="标题库为空,请先添加标题",
|
|
)
|
|
return _to_response(item)
|
|
|
|
|
|
@router.get("/{title_id}", response_model=TitleLibraryItemResponse)
|
|
def get_title(
|
|
title_id: str,
|
|
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
|
title_repository: SQLAlchemyTitleLibraryRepository = Depends(_get_title_repository),
|
|
) -> TitleLibraryItemResponse:
|
|
user_id = authenticated_user.user.id
|
|
use_case = GetTitleLibraryUseCase(title_repository)
|
|
item = use_case.execute(title_id, user_id)
|
|
if item is None:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Title not found")
|
|
return _to_response(item)
|
|
|
|
|
|
@router.post("", response_model=TitleLibraryItemResponse, status_code=status.HTTP_201_CREATED)
|
|
def create_title(
|
|
request: CreateTitleLibraryRequest,
|
|
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
|
title_repository: SQLAlchemyTitleLibraryRepository = Depends(_get_title_repository),
|
|
user_repository: UserRepository = Depends(get_user_repository),
|
|
) -> TitleLibraryItemResponse:
|
|
user_id = authenticated_user.user.id
|
|
plan_name = get_user_plan(user_id, user_repository)
|
|
command = CreateTitleLibraryCommand(
|
|
user_id=user_id,
|
|
name=request.name,
|
|
text=request.text,
|
|
category=request.category,
|
|
description=request.description,
|
|
tags=request.tags,
|
|
)
|
|
use_case = CreateTitleLibraryUseCase(title_repository)
|
|
try:
|
|
item = use_case.execute(command, plan_name=plan_name)
|
|
except QuotaExceededError as exc:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
|
|
detail=f"标题库配额已满({exc.used}/{exc.limit}),请升级套餐",
|
|
) from exc
|
|
return _to_response(item)
|
|
|
|
|
|
@router.put("/{title_id}", response_model=TitleLibraryItemResponse)
|
|
def update_title(
|
|
title_id: str,
|
|
request: UpdateTitleLibraryRequest,
|
|
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
|
title_repository: SQLAlchemyTitleLibraryRepository = Depends(_get_title_repository),
|
|
) -> TitleLibraryItemResponse:
|
|
user_id = authenticated_user.user.id
|
|
command = UpdateTitleLibraryCommand(
|
|
title_id=title_id,
|
|
user_id=user_id,
|
|
name=request.name,
|
|
text=request.text,
|
|
category=request.category,
|
|
description=request.description,
|
|
tags=request.tags,
|
|
)
|
|
use_case = UpdateTitleLibraryUseCase(title_repository)
|
|
try:
|
|
item = use_case.execute(command)
|
|
except NotFoundError as _e:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Title not found") from _e
|
|
return _to_response(item)
|
|
|
|
|
|
@router.delete("/{title_id}", status_code=status.HTTP_204_NO_CONTENT, response_model=None, response_class=Response)
|
|
def delete_title(
|
|
title_id: str,
|
|
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
|
title_repository: SQLAlchemyTitleLibraryRepository = Depends(_get_title_repository),
|
|
) -> Response:
|
|
user_id = authenticated_user.user.id
|
|
use_case = DeleteTitleLibraryUseCase(title_repository)
|
|
deleted = use_case.execute(title_id, user_id)
|
|
if not deleted:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Title not found")
|
|
return
|