feat(card): 优化卡片新增、修复搜卡改版、卡不存在抛异常 其他卡片支持自定义卡片名 修复psa页面改版 修复ccg卡不存在抛异常

This commit is contained in:
xingc
2025-04-25 17:09:18 +08:00
parent 5dd7910167
commit 0f21491ec2
3 changed files with 44 additions and 36 deletions

View File

@@ -29,7 +29,7 @@ logger = logging.getLogger(__name__)
card_router = APIRouter(prefix='/card', tags=['卡册模块']) card_router = APIRouter(prefix='/card', tags=['卡册模块'])
@card_router.get('/search', summary='搜索卡片', response_model=base_schema.BaseResponse[schema.Card]) @card_router.get('/search', summary='搜索卡片', response_model=base_schema.BaseResponse[schema.SearchCardOut])
async def get_card_search( async def get_card_search(
cert_number: str = Query(pattern='[a-zA-Z0-9]+'), cert_number: str = Query(pattern='[a-zA-Z0-9]+'),
platform: base_schema.SupportPlatformType = Query(...), platform: base_schema.SupportPlatformType = Query(...),
@@ -190,9 +190,9 @@ async def created_card(
db_session: AsyncSession = Depends(get_async_session) db_session: AsyncSession = Depends(get_async_session)
): ):
card_crud = CRUD(db_session, Card) card_crud = CRUD(db_session, Card)
if payload.source.value != 'other':
# 卡片库查询 # 卡片库查询
card: Card = await card_crud.get(filters={'cert_number': payload.cert_number}) card: Card = await card_crud.get(filters={'cert_number': payload.cert_number})
if payload.source.value != 'other':
if card is None: if card is None:
# 卡片缓存库转正至卡片库, 重新查询保障数据准确性 # 卡片缓存库转正至卡片库, 重新查询保障数据准确性
cache_card_crud = CRUD(db_session, SearchCardCache) cache_card_crud = CRUD(db_session, SearchCardCache)
@@ -232,9 +232,10 @@ async def created_card(
) )
else: else:
if card is None:
card: Card = await card_crud.create( card: Card = await card_crud.create(
data={ data={
**payload.dict(exclude_none=True), **payload.dict(exclude_none=True, exclude={'reward'}),
'create_user_id': login_user.user_id, 'create_user_id': login_user.user_id,
'source': payload.source.value, 'source': payload.source.value,
}, },
@@ -250,8 +251,6 @@ async def created_card(
data=record, data=record,
filters=record filters=record
) )
# if result is False:
# raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='卡片已存入')
return {'data': card} return {'data': card}

View File

@@ -38,9 +38,18 @@ class Card(BaseModel):
card_spare_picture: Optional[dict] = CardPicture() card_spare_picture: Optional[dict] = CardPicture()
source: SupportPlatformType source: SupportPlatformType
@field_validator("card_year", mode="before")
@classmethod
def convert_to_str(cls, v):
if isinstance(v, int):
v = str(v)
return v
class CardIn(Card): class CardIn(Card):
category: CardCategoryType category: CardCategoryType
source: PlatformSourceType
diy_source: Optional[str] = None
reward: Optional[bool] = False reward: Optional[bool] = False
@@ -49,14 +58,20 @@ class UpdateCard(BaseModel):
display_price: condecimal(max_digits=10, decimal_places=2) display_price: condecimal(max_digits=10, decimal_places=2)
class SearchCardOut(Card):
pass
class CardVisitOut(Card, UpdateCard): class CardVisitOut(Card, UpdateCard):
source: PlatformSourceType source: PlatformSourceType
class CardOut(CardVisitOut): class CardOut(CardVisitOut):
id: Optional[int] = None id: Optional[int] = None
card_year: str | None
purchase_price: Optional[condecimal(max_digits=10, decimal_places=2)] = None purchase_price: Optional[condecimal(max_digits=10, decimal_places=2)] = None
display_price: Optional[condecimal(max_digits=10, decimal_places=2)] = None display_price: Optional[condecimal(max_digits=10, decimal_places=2)] = None
diy_source: Optional[str] = None
class DeleteCardIn(BaseModel): class DeleteCardIn(BaseModel):

View File

@@ -85,13 +85,13 @@ class SiteApi:
@async_retry(exceptions=(SearchCardRetry,)) @async_retry(exceptions=(SearchCardRetry,))
async def psa(self, cert_number: str | int): async def psa(self, cert_number: str | int):
"""PSA""" """PSA"""
url = f'https://www.psacard.com/cert/{cert_number}' url = f'https://www.psacard.com/cert/{cert_number}/psa'
headers = { headers = {
# 'X-Requested-With': 'XMLHttpRequest', # 'X-Requested-With': 'XMLHttpRequest',
'Origin': 'https://www.psacard.com', 'Origin': 'https://www.psacard.com',
'Referer': f'https://www.psacard.com/cert/{cert_number}', 'Referer': f'https://www.psacard.com/cert/{cert_number}',
} }
r = await self.get(url, headers=headers, use_abroad_proxy=True) r = await self.get(url, headers=headers, impersonate="chrome131", use_abroad_proxy=True)
chck_string = 'Just a moment...' chck_string = 'Just a moment...'
if chck_string in r.text: if chck_string in r.text:
raise SearchCardRetry(chck_string) raise SearchCardRetry(chck_string)
@@ -191,16 +191,16 @@ class CardScraper:
"""PSA""" """PSA"""
r = await self.api.psa(cert_number) r = await self.api.psa(cert_number)
html = Selector(r.text) html = Selector(r.text)
if html.xpath('//div[contains(@class, "alert-danger")]/p[contains(string(), "not found")]').get() is not None: if html.xpath('//div[contains(@class, "wrapper")]//h1/text()').get() is not None:
return return
info_elements = html.xpath('//main[@id="mainContent"]/div/table[contains(@class, "text-medium")]//tr') info_elements = html.xpath('//div[@hidden and @id]/div[contains(@class, "py-3")]')
info = dict() info = dict()
for info_element in info_elements: for info_element in info_elements:
name = info_element.xpath('./th[1]').xpath('string()').get() name = info_element.xpath('./dt[1]').xpath('string()').get() or info_element.xpath('./dt/template/@id').get() or ''
name = re.sub(r'\(|\)', '', name) name = re.sub(r'\(|\)', '', name)
name = name.lower().replace(' ', '_').replace('/', '_') name = name.lower().replace(' ', '_').replace('/', '_')
value = info_element.xpath('./td[1]').xpath('string()').get() value = info_element.xpath('./dd[1]').xpath('string()').get()
info[name.strip()] = value.strip() info[name.strip()] = value.strip()
picture_elements = info_elements.xpath('.//img') picture_elements = info_elements.xpath('.//img')
info['pictures'] = [ info['pictures'] = [
@@ -212,35 +212,29 @@ class CardScraper:
item = dict(cert_number=None) item = dict(cert_number=None)
# 评级卡号 # 评级卡号
item['cert_number'] = info['certification_number'] item['cert_number'] = info['cert_number']
# 年份 # 年份
item['card_year'] = info.get('year') item['card_year'] = info.get('year')
# 卡片品牌 # 卡片品牌
item['card_brand'] = info.get('brand') item['card_brand'] = info.get('brand_title')
# 卡片评分 # 卡片评分
card_grade = info.get('card_grade') or info.get('grade') or '' card_grade = info.get('item_grade') or ''
item['card_score'] = regx_match(r'(\d+)', card_grade, first=True) item['card_score'] = regx_match(r'(\d+)', card_grade, first=True)
# 签字评分 # 签字评分
auto_score = info.get('autograph_grade') or '' auto_score = info.get('autograph_grade') or ''
item['auto_score'] = regx_match(r'(\d+)', auto_score, first=True) item['auto_score'] = regx_match(r'(\d+)', auto_score, first=True)
# 卡片名称 # 卡片名称
item['card_name'] = info.get('player') item['card_name'] = info.get('subject') or info.get('p:16')
# 卡片编号 # 卡片编号
item['card_number'] = info.get('card_number') item['card_number'] = info.get('card_number')
# 卡片类型 # 卡片类型
item['card_type'] = info.get('sport') item['card_type'] = info.get('category')
images_id = html.xpath(
'//script[contains(text(), "getPSACertImages")]/text()'
).re_first(r'getPSACertImages\(\'(\d+)\'\);')
if images_id:
r = await self.api.psa_images(cert_number, images_id)
result = r.json()
# 卡片正、反图片(远程) # 卡片正、反图片(远程)
item['card_spare_picture'] = { item['card_spare_picture'] = {
'front_picture': jsonpath(result, '$.PSACertImages[0].OriginalImageUrl', first=True), 'front_picture': html.xpath('//div[contains(@class, "flex")]//span/noscript/img[@alt="left"]/@src').get(),
'reverse_picture': jsonpath(result, '$.PSACertImages[1].OriginalImageUrl', first=True) 'reverse_picture': html.xpath('//div[contains(@class, "flex")]//span/noscript/img[@alt="right"]/@src').get()
} }
# 来源 # 来源
item['source'] = 'psa' item['source'] = 'psa'
return { return {
@@ -368,7 +362,7 @@ class CardScraper:
"""重庆藏卡""" """重庆藏卡"""
r = await self.api.ccg(cert_number) r = await self.api.ccg(cert_number)
result = r.json() result = r.json()
if result.get('code') != '200': if result.get('code') != '200' or not result['data']:
return return
item = dict(cert_number=None) item = dict(cert_number=None)