# -*- coding = utf-8 -*- # @Time : 2025/2/13 下午4:51 # @File : routes.py # @Software : PyCharm # @Author : xingc # @Desc : import logging from typing import List, Literal import ujson from fastapi import APIRouter, Depends, Query, HTTPException, status import redis.asyncio as aioredis from sqlalchemy import select, func from sqlalchemy.ext.asyncio import AsyncSession from config import settings from core.base import schema as base_schema from core.db.crud import CRUD from core.db.engine import get_async_session, get_cache from core.db.models import Card, SearchCardCache, UserCollection, User from core.routers.auth.schema import LoginUser from core.routers.auth.services import get_current_user_form_http from core.utils.helper import generate_keyname, check_picture_for_empty from . import schema from .scraper import card_scraper logger = logging.getLogger(__name__) card_router = APIRouter(prefix='/card', tags=['卡册模块']) @card_router.get('/search', summary='搜索卡片', response_model=base_schema.BaseResponse[schema.SearchCardOut]) async def get_card_search( cert_number: str = Query(pattern='[a-zA-Z0-9]+'), platform: base_schema.SupportPlatformType = Query(...), db_session: AsyncSession = Depends(get_async_session), redis_conn: aioredis.Redis = Depends(get_cache) ): logger.info(f'Get card search request: {cert_number}') logger.info(platform) result = None # 缓存 cache_keyname = generate_keyname(f'card_search:{platform.value}:{cert_number}', use_today=True) result_str = await redis_conn.get(cache_keyname) if result_str is not None: result = ujson.decode(result_str) return {'data': result} db_crud = CRUD(db_session, SearchCardCache) # 缓存库 if result is None: card = await db_crud.get(filters={'cert_number': cert_number, 'source': platform.value}) if card is not None: result = card.data # 采集 if result is None: scraper_result = await card_scraper.choice(platform.value, cert_number) if scraper_result is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail='相关卡片信息未找到' ) result = scraper_result['data'] await db_crud.create( data={ **scraper_result, 'source': result.get('source') or platform.value, }, filters={'cert_number': scraper_result['cert_number'], 'source': platform.value} ) await redis_conn.set(cache_keyname, ujson.encode(result), ex=settings.REDIS_CACHE_EXPIRATION_SECONDS) return {'data': result} @card_router.get( '/me/all', summary='卡片列表(所有者)', response_model=base_schema.BaseResponse[ base_schema.Paginated[ List[schema.CardOut] ] ], ) async def get_me_all_card( login_user: LoginUser = Depends(get_current_user_form_http), page: int = Query(default=1, gt=0), page_size: int = Query(default=15, gt=0, le=200), category: Literal['all', 'anime', 'sports', 'other', 'displayed'] = Query(default='all'), db_session: AsyncSession = Depends(get_async_session) ): # 组装查询条件 conditions = [ UserCollection.user_id == login_user.user_id, ] if category in ['anime', 'sports', 'other']: conditions.append(Card.category == category) if category == 'displayed': conditions.append(UserCollection.displayed is True) # 记录总数查询 total_stmt = ( select(func.count()).select_from(UserCollection).filter( *conditions ) ) total = await db_session.execute(total_stmt) # 分页记录查询 stmt = ( select(Card, UserCollection) .join(Card, UserCollection.card_id == Card.id) .where(*conditions) .order_by(UserCollection.created_at.desc()).limit(page_size).offset((page - 1) * page_size) ) records = await db_session.execute(stmt) return { 'data': { 'list': [ { **card.dict(), **user_collection.dict(exclude={'id'}) } for card, user_collection in records ], 'total': total.scalar(), 'page': page, 'page_size': page_size, }, } @card_router.get( '/visit/{user_id}/all', summary='卡片列表(访问者)', response_model=base_schema.BaseResponse[ base_schema.Paginated[ List[schema.CardVisitOut] ] ], ) async def get_visit_user_all_card( user_id: int, login_user: LoginUser = Depends(get_current_user_form_http), page: int = Query(default=1, gt=0), page_size: int = Query(default=15, gt=0, le=200), db_session: AsyncSession = Depends(get_async_session) ): # 组装查询条件 conditions = [ UserCollection.user_id == user_id, UserCollection.displayed == True ] # 记录总数查询 total_stmt = ( select(func.count()).select_from(UserCollection).filter(*conditions) ) total = await db_session.execute(total_stmt) # 分页记录查询 stmt = ( select(Card, UserCollection) .join(Card, UserCollection.card_id == Card.id) .where(*conditions) .order_by(UserCollection.created_at.desc()).limit(page_size).offset((page - 1) * page_size) ) records = await db_session.execute(stmt) return { 'data': { 'list': [ { **card.dict(), **user_collection.dict() } for card, user_collection in records ], 'total': total.scalar(), 'page': page, 'page_size': page_size, }, } @card_router.post('/create', summary='新增卡片', response_model=base_schema.BaseResponse[schema.CardOut]) async def created_card( payload: schema.CardIn, login_user: LoginUser = Depends(get_current_user_form_http), db_session: AsyncSession = Depends(get_async_session) ): card_crud = CRUD(db_session, Card) # 卡片库查询 card: Card = await card_crud.get(filters={'cert_number': payload.cert_number}) if payload.source.value != 'other': if card is None: # 卡片缓存库转正至卡片库, 重新查询保障数据准确性 cache_card_crud = CRUD(db_session, SearchCardCache) cache_card = await cache_card_crud.get( filters={'cert_number': payload.cert_number, 'source': payload.source.value} ) card = await card_crud.create( data={ **cache_card.data, 'category': payload.category, }, filters={ 'cert_number': payload.cert_number, 'source': payload.source.value } ) # 校验是否需要更新图片 card_picture_empty = check_picture_for_empty(card.card_picture) if all([ check_picture_for_empty(payload.card_picture.dict()) is False, # 创建时用户有提交上传图片链接 check_picture_for_empty(card.card_spare_picture), # 采集原始来源无图片 check_picture_for_empty(card.card_picture, or_=True), # 无本地化图片 ]): # 补充有效值 card_picture = {} if card_picture_empty else card.card_picture.copy() for key, value in payload.card_picture.dict(exclude_none=True).items(): if card_picture.get(key) is None: card_picture[key] = value await card_crud.update( data={ 'card_picture': card_picture, 'perfect_user_id': login_user.user_id, }, filters={'id': card.id} ) else: if card is None: card: Card = await card_crud.create( data={ **payload.dict(exclude_none=True, exclude={'reward'}), 'create_user_id': login_user.user_id, 'source': payload.source.value, }, filters={'cert_number': payload.cert_number, 'source': payload.source.value} ) collection_crud = CRUD(db_session, UserCollection) record = { 'user_id': login_user.user_id, 'card_id': card.id } result = await collection_crud.create( data=record, filters=record ) return {'data': card} @card_router.post( '/{card_id}/update/', summary='卡片更新(会员功能,入手价值、展示价值)', response_model=base_schema.BaseResponse ) async def update_card_price_info( card_id: int, payload: schema.UpdateCard, login_user: LoginUser = Depends(get_current_user_form_http), db_session: AsyncSession = Depends(get_async_session), ): db_crud = CRUD(db_session, UserCollection) user = await db_crud.get(model=User, filters={'user_id': login_user.user_id}) if user.is_vip() is False: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='会员未开通或者已过期') result = await db_crud.update( data=payload.dict(exclude_none=True), filters={'card_id': card_id, 'user_id': login_user.user_id} ) return {'message': 'ok' if result is not None else 'fail'} @card_router.post('/display/', summary='卡片展示/隐藏', response_model=base_schema.BaseResponse) async def display_card_status( action: Literal['displayed', 'undisplayed'], payload: schema.DisplayCardIn, login_user: LoginUser = Depends(get_current_user_form_http), db_session: AsyncSession = Depends(get_async_session) ): db_crud = CRUD(db_session, UserCollection) result = await db_crud.updates( data={ 'displayed': (action == 'displayed'), }, filters={'card_id__in': payload.card_ids, 'user_id': login_user.user_id} ) return {'message': 'ok' if result is not None else 'fail'} @card_router.delete('/delete/', summary='卡片删除(批量)', response_model=base_schema.BaseResponse) async def delete_card( payload: schema.DeleteCardIn, login_user: LoginUser = Depends(get_current_user_form_http), db_session: AsyncSession = Depends(get_async_session) ): # 删除关联关系 db_crud = CRUD(db_session, UserCollection) state = await db_crud.delete( filters={ 'card_id__in': payload.card_ids, 'user_id': login_user.user_id } ) if state is True: # 删除原始卡片(自定义) db_crud = CRUD(db_session, Card) await db_crud.delete( filters={ 'id__in': payload.card_ids, 'user_id': login_user.user_id, 'source': 'OTHER' } ) return {'message': 'ok' if state else 'fail'}