feat(routers): 完善auth、card、file、user相关接口开发

This commit is contained in:
xingc
2025-02-22 11:38:56 +08:00
parent 02ddc81c51
commit 1b2f9c7539
11 changed files with 573 additions and 123 deletions

View File

@@ -4,8 +4,9 @@
# @Software : PyCharm
# @Author : xingc
# @Desc :
import re
import logging
from typing import Literal, List
from typing import List, Literal
import ujson
from fastapi import APIRouter, Depends, Query, HTTPException, status
@@ -13,19 +14,23 @@ import redis.asyncio as aioredis
from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession
from . import schema
from .scraper import card_scraper
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
from core.routers.auth.schema import LoginUser
from core.routers.auth.services import get_current_user
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', response_model=base_schema.BaseResponse[schema.CardOut])
@card_router.get('/search', summary='搜索卡片', response_model=base_schema.BaseResponse[schema.Card])
async def get_card_search(
cert_number: str = Query(pattern='[a-zA-Z0-9]+'),
platform: base_schema.SupportPlatformType = Query(...),
@@ -36,7 +41,7 @@ async def get_card_search(
logger.info(platform)
result = None
# 缓存
cache_keyname = f'card_book:card_search:{platform.value}:{cert_number}'
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)
@@ -63,8 +68,9 @@ async def get_card_search(
**scraper_result,
'source': result.get('source') or platform.value,
},
filters={'cert_number': cert_number, 'source': 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}
@@ -72,6 +78,7 @@ async def get_card_search(
@card_router.get(
'/me/all',
summary='卡片列表(所有者)',
response_model=base_schema.BaseResponse[
base_schema.Paginated[
List[schema.CardOut]
@@ -79,14 +86,15 @@ async def get_card_search(
],
)
async def get_me_all_card(
login_user: LoginUser = Depends(get_current_user),
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)
):
user_id = None
# 组装查询条件
conditions = [
UserCollection.user_id == user_id,
UserCollection.user_id == login_user.user_id,
]
if category in ['anime', 'sports', 'other']:
conditions.append(Card.category == category)
@@ -94,14 +102,16 @@ async def get_me_all_card(
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(UserCollection, Card)
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)
@@ -110,7 +120,12 @@ async def get_me_all_card(
return {
'data': {
'list': [record.dict() for record in records.scalars().all()],
'list': [
{
**card.dict(),
**user_collection.dict()
} for card, user_collection in records
],
'total': total.scalar(),
'page': page,
'page_size': page_size,
@@ -120,28 +135,34 @@ async def get_me_all_card(
@card_router.get(
'/visit/{user_id}/all',
summary='卡片列表(访问者)',
response_model=base_schema.BaseResponse[
base_schema.Paginated[
List[schema.CardOut]
List[schema.CardVisitOut]
]
],
)
async def get_visit_user_all_card(
user_id: str,
user_id: int,
login_user: LoginUser = Depends(get_current_user),
page: int = Query(default=1, gt=0),
page_size: int = Query(default=15, gt=0, le=200),
# category: Literal['displayed'] = Query(default='displayed'),
db_session: AsyncSession = Depends(get_async_session)
):
# 组装查询条件
conditions = [
UserCollection.user_id == user_id, UserCollection.displayed is True
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(UserCollection, Card)
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)
@@ -150,7 +171,12 @@ async def get_visit_user_all_card(
return {
'data': {
'list': [record.dict() for record in records.scalars().all()],
'list': [
{
**card.dict(),
**user_collection.dict()
} for card, user_collection in records
],
'total': total.scalar(),
'page': page,
'page_size': page_size,
@@ -158,63 +184,139 @@ async def get_visit_user_all_card(
}
@card_router.post('/create', response_model=base_schema.BaseResponse[schema.CardOut])
@card_router.post('/create', summary='新增卡片', response_model=base_schema.BaseResponse[schema.CardOut])
async def created_card(
payload: schema.Card,
payload: schema.CardIn,
login_user: LoginUser = Depends(get_current_user),
db_session: AsyncSession = Depends(get_async_session)
):
db_crud = CRUD(db_session, Card)
card: Card = await db_crud.create(
data={
**payload.dict(exclude_none=True),
'create_user_id': None,
'source': payload.source.value,
},
filters={'cert_number': payload.cert_number, 'source': payload.source.value}
card_crud = CRUD(db_session, Card)
if payload.source.value != 'other':
# 卡片库查询
card: Card = await card_crud.get(filters={'cert_number': payload.cert_number})
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:
card: Card = await card_crud.create(
data={
**payload.dict(exclude_none=True),
'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
)
# if result is False:
# raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='卡片已存入')
return {'data': card}
@card_router.post('/{card_id}/update/')
@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),
db_session: AsyncSession = Depends(get_async_session),
):
db_crud = CRUD(db_session, UserCollection)
result = await db_crud.update(
data=payload.dict(exclude_none=True),
filters={'card_id': card_id, 'user_id': None}
filters={'card_id': card_id, 'user_id': login_user.user_id}
)
return {'message': 'success' if result is not None else 'fail'}
return {'message': 'ok' if result is not None else 'fail'}
@card_router.post('/{card_id}/display/')
@card_router.post('/{card_id}/display/', summary='卡片展示/隐藏', response_model=base_schema.BaseResponse)
async def display_card_status(
card_id: int,
status: bool,
action: Literal['displayed', 'undisplayed'],
login_user: LoginUser = Depends(get_current_user),
db_session: AsyncSession = Depends(get_async_session)
):
db_crud = CRUD(db_session, UserCollection)
result = await db_crud.update(
data={
'displayed': status
'displayed': (action == 'displayed'),
},
filters={'card_id': card_id, 'user_id': None}
filters={'card_id': card_id, 'user_id': login_user.user_id}
)
return {'message': 'success' if result is not None else 'fail'}
return {'message': 'ok' if result is not None else 'fail'}
@card_router.delete('/delete/', response_model=base_schema.BaseResponse)
@card_router.delete('/delete/', summary='卡片删除(批量)', response_model=base_schema.BaseResponse)
async def delete_card(
payload: schema.DeleteCardIn,
login_user: LoginUser = Depends(get_current_user),
db_session: AsyncSession = Depends(get_async_session)
):
db_crud = CRUD(db_session, Card)
state = await db_crud.delete(filters={'card_id_in': payload.card_ids})
# 删除关联关系
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 False:
db_crud = CRUD(db_session, UserCollection)
state = await db_crud.delete(filters={'card_id_in': payload.card_ids, 'user_id': None})
if state is True:
# 删除原始卡片(自定义)
db_crud = CRUD(db_session, Card)
await db_crud.delete(
filters={
'card_id_in': payload.card_ids,
'user_id': login_user.user_id,
'source': 'OTHER'
}
)
return {'message': 'success' if state else 'fail'}
return {'message': 'ok' if state else 'fail'}

View File

@@ -4,37 +4,59 @@
# @Software : PyCharm
# @Author : xingc
# @Desc :
from typing import Optional, Literal
import re
from typing import Optional, Dict
from pydantic import BaseModel, conint
from pydantic import BaseModel, constr, condecimal, field_validator
from core.base.schema import SupportPlatformType, PlatformSourceType
from core.base.schema import SupportPlatformType, PlatformSourceType, CardCategoryType
class CardPrice(BaseModel):
front_picture: Optional[str] = None
reverse_picture: Optional[str] = None
# 自定义校验规则
@field_validator('front_picture', 'reverse_picture')
def check_length(cls, value):
if value is not None:
if not re.match(r'^http.*\.jpg$', value):
raise ValueError('链接不合法')
return value
class Card(BaseModel):
cert_number: str
card_year: conint(gt=0, le=2099)
card_year: Optional[constr(pattern=r'^\d{4}([-~]\d{1,4})?$')]
card_brand: Optional[str] = None
card_score: Optional[float] = None
auto_score: Optional[float] = None
card_name: str
card_number: Optional[str] = None
card_type: Optional[str] = None
card_picture: Optional[dict] = {}
card_picture: Optional[CardPrice] = {}
card_spare_picture: Optional[dict] = {}
source: SupportPlatformType
class CardIn(Card):
category: CardCategoryType
reward: Optional[bool] = False
class UpdateCard(BaseModel):
purchase_price: float
display_price: float
purchase_price: condecimal(max_digits=10, decimal_places=2)
display_price: condecimal(max_digits=10, decimal_places=2)
class CardOut(Card, UpdateCard):
card_year: int
class CardVisitOut(Card, UpdateCard):
source: PlatformSourceType
purchase_price: Optional[float] = None
display_price: Optional[float] = None
class CardOut(CardVisitOut):
id: Optional[int] = None
purchase_price: Optional[condecimal(max_digits=10, decimal_places=2)] = None
display_price: Optional[condecimal(max_digits=10, decimal_places=2)] = None
class DeleteCardIn(BaseModel):

View File

@@ -5,6 +5,7 @@
# @Author : xingc
# @Desc :
import logging
import re
from curl_cffi import requests
from fake_useragent import UserAgent
@@ -181,13 +182,14 @@ class CardScraper:
"""PSA"""
r = await self.api.psa(cert_number)
html = Selector(r.text)
if html.xpath('//div[contains(@class, "alert-danger")]/p[contains(text(), "not found")]').get() is not None:
if html.xpath('//div[contains(@class, "alert-danger")]/p[contains(string(), "not found")]').get() is not None:
return
info_elements = html.xpath('//main[@id="mainContent"]/div/table//tr')
info_elements = html.xpath('//main[@id="mainContent"]/div/table[contains(@class, "text-medium")]//tr')
info = dict()
for info_element in info_elements:
name = info_element.xpath('./th[1]').xpath('string()').get()
name = re.sub(r'\(|\)', '', name)
name = name.lower().replace(' ', '_').replace('/', '_')
value = info_element.xpath('./td[1]').xpath('string()').get()
info[name.strip()] = value.strip()
@@ -234,7 +236,6 @@ class CardScraper:
item['source'] = 'psa'
return {
'cert_number': item['cert_number'],
'card_number': cert_number,
'data': item,
'raw_data': info,
}
@@ -267,7 +268,6 @@ class CardScraper:
item['source'] = 'bgs'
return {
'cert_number': item['cert_number'],
'card_number': cert_number,
'data': item,
'raw_data': result,
}
@@ -312,7 +312,6 @@ class CardScraper:
item['source'] = 'cgc'
return {
'cert_number': item['cert_number'],
'card_number': cert_number,
'data': item,
'raw_data': info,
}
@@ -352,7 +351,6 @@ class CardScraper:
item['source'] = 'gbtc'
return {
'cert_number': item['cert_number'],
'card_number': cert_number,
'data': item,
'raw_data': result.get('data'),
}
@@ -389,7 +387,6 @@ class CardScraper:
item['source'] = 'ccg'
return {
'cert_number': item['cert_number'],
'card_number': cert_number,
'data': item,
'raw_data': result.get('data'),
}
@@ -429,7 +426,6 @@ class CardScraper:
item['source'] = 'bctc'
return {
'cert_number': item['cert_number'],
'card_number': cert_number,
'data': item,
'raw_data': result.get('info'),
}
@@ -450,7 +446,7 @@ card_scraper = CardScraper()
async def main():
import time
started = time.time()
print(format_json(await card_scraper.choice('psa', '83610272')))
print(format_json(await card_scraper.choice('psa', '1')))
print(time.time() - started)
# print(format_json(await card_scraper.choice('psa', '83610272')))
# print(format_json(await card_scraper.psa('83610272')))