diff --git a/core/routers/card/routes.py b/core/routers/card/routes.py index 0ecb830..4c505f0 100644 --- a/core/routers/card/routes.py +++ b/core/routers/card/routes.py @@ -29,7 +29,7 @@ logger = logging.getLogger(__name__) 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( cert_number: str = Query(pattern='[a-zA-Z0-9]+'), platform: base_schema.SupportPlatformType = Query(...), @@ -190,9 +190,9 @@ async def created_card( 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': - # 卡片库查询 - card: Card = await card_crud.get(filters={'cert_number': payload.cert_number}) if card is None: # 卡片缓存库转正至卡片库, 重新查询保障数据准确性 cache_card_crud = CRUD(db_session, SearchCardCache) @@ -232,14 +232,15 @@ async def created_card( ) 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} - ) + 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 = { @@ -250,8 +251,6 @@ async def created_card( data=record, filters=record ) - # if result is False: - # raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='卡片已存入') return {'data': card} diff --git a/core/routers/card/schema.py b/core/routers/card/schema.py index 31dea82..91754f6 100644 --- a/core/routers/card/schema.py +++ b/core/routers/card/schema.py @@ -38,9 +38,18 @@ class Card(BaseModel): card_spare_picture: Optional[dict] = CardPicture() 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): category: CardCategoryType + source: PlatformSourceType + diy_source: Optional[str] = None reward: Optional[bool] = False @@ -49,14 +58,20 @@ class UpdateCard(BaseModel): display_price: condecimal(max_digits=10, decimal_places=2) +class SearchCardOut(Card): + pass + + class CardVisitOut(Card, UpdateCard): source: PlatformSourceType class CardOut(CardVisitOut): id: Optional[int] = None + card_year: str | None purchase_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): diff --git a/core/routers/card/scraper.py b/core/routers/card/scraper.py index df78a39..978fd92 100644 --- a/core/routers/card/scraper.py +++ b/core/routers/card/scraper.py @@ -85,13 +85,13 @@ class SiteApi: @async_retry(exceptions=(SearchCardRetry,)) async def psa(self, cert_number: str | int): """PSA""" - url = f'https://www.psacard.com/cert/{cert_number}' + url = f'https://www.psacard.com/cert/{cert_number}/psa' headers = { # 'X-Requested-With': 'XMLHttpRequest', 'Origin': 'https://www.psacard.com', '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...' if chck_string in r.text: raise SearchCardRetry(chck_string) @@ -191,16 +191,16 @@ class CardScraper: """PSA""" r = await self.api.psa(cert_number) 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 - 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() 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 = 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() picture_elements = info_elements.xpath('.//img') info['pictures'] = [ @@ -212,35 +212,29 @@ class CardScraper: 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_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) # 签字评分 auto_score = info.get('autograph_grade') or '' 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_type'] = info.get('sport') - images_id = html.xpath( - '//script[contains(text(), "getPSACertImages")]/text()' - ).re_first(r'getPSACertImages\(\'(\d+)\'\);') + item['card_type'] = info.get('category') + # 卡片正、反图片(远程) + item['card_spare_picture'] = { + 'front_picture': html.xpath('//div[contains(@class, "flex")]//span/noscript/img[@alt="left"]/@src').get(), + 'reverse_picture': html.xpath('//div[contains(@class, "flex")]//span/noscript/img[@alt="right"]/@src').get() + } - if images_id: - r = await self.api.psa_images(cert_number, images_id) - result = r.json() - # 卡片正、反图片(远程) - item['card_spare_picture'] = { - 'front_picture': jsonpath(result, '$.PSACertImages[0].OriginalImageUrl', first=True), - 'reverse_picture': jsonpath(result, '$.PSACertImages[1].OriginalImageUrl', first=True) - } # 来源 item['source'] = 'psa' return { @@ -368,7 +362,7 @@ class CardScraper: """重庆藏卡""" r = await self.api.ccg(cert_number) result = r.json() - if result.get('code') != '200': + if result.get('code') != '200' or not result['data']: return item = dict(cert_number=None)