first init

This commit is contained in:
xingc
2025-02-18 14:26:47 +08:00
commit 48a02974fc
41 changed files with 2297 additions and 0 deletions

13
core/routers/__init__.py Normal file
View File

@@ -0,0 +1,13 @@
# -*- coding = utf-8 -*-
# @Time : 2025/2/13 下午3:48
# @File : __init__.py.py
# @Software : PyCharm
# @Author : xingc
# @Desc :
from .auth.routes import auth_router
from .card.routes import card_router
from .im.routes import im_router
from .file.routes import file_router
from .user.routes import user_router
routers = [auth_router, card_router, im_router, file_router, user_router]

View File

@@ -0,0 +1,6 @@
# -*- coding = utf-8 -*-
# @Time : 2025/2/13 下午3:48
# @File : __init__.py.py
# @Software : PyCharm
# @Author : xingc
# @Desc :

View File

@@ -0,0 +1,72 @@
# -*- coding = utf-8 -*-
# @Time : 2025/2/13 下午5:02
# @File : routes.py
# @Software : PyCharm
# @Author : xingc
# @Desc :
import logging
import httpx
from fastapi import APIRouter, HTTPException, Depends
import redis.asyncio as aioredis
from sqlalchemy.ext.asyncio import AsyncSession
from . import schema
from config import settings
from core.db.engine import get_async_session, get_cache
from core.db.models import User
from core.db.crud import CRUD
logger = logging.getLogger(__name__)
auth_router = APIRouter(prefix='/auth', tags=['认证模块'])
async def get_wechat_session(code: str):
url = "https://api.weixin.qq.com/sns/jscode2session"
params = {
"appid": settings.APPID,
"secret": settings.APPSECRET,
"js_code": code,
"grant_type": "authorization_code",
}
async with httpx.AsyncClient() as client:
response = await client.get(url, params=params)
if response.status_code != 200:
raise HTTPException(status_code=400, detail="Failed to fetch session from WeChat")
data = response.json()
if "errcode" in data:
logger.warning(f"WeChat session error: {data['errcode']}")
raise HTTPException(status_code=400, detail=data["errmsg"])
return data
@auth_router.post('/wechat_login')
async def wechat_login(
payload: schema.WechatLoginIn,
db_session: AsyncSession = Depends(get_async_session),
redis_conn: aioredis.Redis = Depends(get_cache)
):
# 获取微信 session
wechat_session = await get_wechat_session(payload.code)
logger.info(f"WeChat session: {wechat_session}")
openid = wechat_session.get("openid")
if openid:
raise HTTPException(status_code=400, detail="Invalid WeChat session")
db_crud = CRUD(db_session, User)
user = await db_crud.get(filters={'openid': openid})
if user is None:
# 新用户注册
if not payload.encrypted_data or not payload.iv:
raise HTTPException(400, detail="需要用户授权信息")
# user = await db_crud.create(
# data={
# 'wechat_openid': openid,
# 'wechat_union_id': union_id,
# },
# filters={'union_id': union_id}
# )
return {'data': {'openid': openid}}

View File

@@ -0,0 +1,16 @@
# -*- coding = utf-8 -*-
# @Time : 2025/2/17 下午2:30
# @File : schema.py
# @Software : PyCharm
# @Author : xingc
# @Desc :
from typing import Optional
from pydantic import BaseModel, constr
class WechatLoginIn(BaseModel):
code: str
phone: Optional[constr(pattern='^1[0-9]{10}$')] = None
encrypted_data: Optional[str] = None
iv: Optional[str] = None

View File

@@ -0,0 +1,9 @@
# -*- coding = utf-8 -*-
# @Time : 2025/2/17 下午8:14
# @File : services.py
# @Software : PyCharm
# @Author : xingc
# @Desc :
from fastapi.security import OAuth2PasswordBearer
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

View File

@@ -0,0 +1,6 @@
# -*- coding = utf-8 -*-
# @Time : 2025/2/13 下午3:59
# @File : __init__.py.py
# @Software : PyCharm
# @Author : xingc
# @Desc :

220
core/routers/card/routes.py Normal file
View File

@@ -0,0 +1,220 @@
# -*- coding = utf-8 -*-
# @Time : 2025/2/13 下午4:51
# @File : routes.py
# @Software : PyCharm
# @Author : xingc
# @Desc :
import logging
from typing import Literal, List
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 . 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
logger = logging.getLogger(__name__)
card_router = APIRouter(prefix='/card', tags=['卡册模块'])
@card_router.get('/search', response_model=base_schema.BaseResponse[schema.CardOut])
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 = f'card_book:card_search:{platform.value}:{cert_number}'
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': 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',
response_model=base_schema.BaseResponse[
base_schema.Paginated[
List[schema.CardOut]
]
],
)
async def get_me_all_card(
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,
]
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(UserCollection, Card)
.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': [record.dict() for record in records.scalars().all()],
'total': total.scalar(),
'page': page,
'page_size': page_size,
},
}
@card_router.get(
'/visit/{user_id}/all',
response_model=base_schema.BaseResponse[
base_schema.Paginated[
List[schema.CardOut]
]
],
)
async def get_visit_user_all_card(
user_id: str,
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
]
total_stmt = (
select(func.count()).select_from(UserCollection).filter(*conditions)
)
total = await db_session.execute(total_stmt)
stmt = (
select(UserCollection, Card)
.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': [record.dict() for record in records.scalars().all()],
'total': total.scalar(),
'page': page,
'page_size': page_size,
},
}
@card_router.post('/create', response_model=base_schema.BaseResponse[schema.CardOut])
async def created_card(
payload: schema.Card,
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}
)
return {'data': card}
@card_router.post('/{card_id}/update/')
async def update_card_price_info(
card_id: int,
payload: schema.UpdateCard,
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}
)
return {'message': 'success' if result is not None else 'fail'}
@card_router.post('/{card_id}/display/')
async def display_card_status(
card_id: int,
status: bool,
db_session: AsyncSession = Depends(get_async_session)
):
db_crud = CRUD(db_session, UserCollection)
result = await db_crud.update(
data={
'displayed': status
},
filters={'card_id': card_id, 'user_id': None}
)
return {'message': 'success' if result is not None else 'fail'}
@card_router.delete('/delete/', response_model=base_schema.BaseResponse)
async def delete_card(
payload: schema.DeleteCardIn,
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})
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})
return {'message': 'success' if state else 'fail'}

View File

@@ -0,0 +1,41 @@
# -*- coding = utf-8 -*-
# @Time : 2025/2/13 下午5:01
# @File : schema.py
# @Software : PyCharm
# @Author : xingc
# @Desc :
from typing import Optional, Literal
from pydantic import BaseModel, conint
from core.base.schema import SupportPlatformType, PlatformSourceType
class Card(BaseModel):
cert_number: str
card_year: conint(gt=0, le=2099)
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_spare_picture: Optional[dict] = {}
source: SupportPlatformType
class UpdateCard(BaseModel):
purchase_price: float
display_price: float
class CardOut(Card, UpdateCard):
card_year: int
source: PlatformSourceType
purchase_price: Optional[float] = None
display_price: Optional[float] = None
class DeleteCardIn(BaseModel):
card_ids: list[int]

View File

@@ -0,0 +1,470 @@
# -*- coding = utf-8 -*-
# @Time : 2025/2/14 上午10:34
# @File : scraper.py
# @Software : PyCharm
# @Author : xingc
# @Desc :
import logging
from curl_cffi import requests
from fake_useragent import UserAgent
from parsel import Selector
from simple_spider_tool import jsonpath, regx_match, format_json
from config import settings
from core.base.exceptions import SearchCardNotFound, SearchCardRetry
from core.utils.helper import async_retry
logger = logging.getLogger(__name__)
class SiteApi:
def __init__(self):
self.user_agent = UserAgent(
browsers=['Google', 'Chrome', 'Firefox', 'Edge', 'Safari'],
os=['Windows', 'Mac OS X'],
platforms=['desktop'],
min_version=127.0
)
self.session = requests.AsyncSession()
self.headers = {
'User-Agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 Edg/132.0.0.0",
'Accept-Encoding': "gzip, deflate, br, zstd",
'Sec-Fetch-Site': "same-origin",
'Sec-Fetch-Mode': "cors",
'Sec-Fetch-Dest': "empty",
'Accept-Language': "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
}
async def request(
self,
method,
url,
headers=None,
params=None,
data=None,
json=None,
use_abroad_proxy: bool = False,
**kwargs
):
headers = {
**self.headers,
'User-Agent': self.user_agent.random,
**(headers or {}),
}
# 境外代理
if use_abroad_proxy:
kwargs['proxies'] = {
'http': settings.OVERSEAS_PROXY,
'https': settings.OVERSEAS_PROXY
}
r = await self.session.request(
method, url, headers=headers, params=params, data=data, json=json, **kwargs
)
logger.debug(f'耗时:{url} {r.elapsed}')
return r
async def get(self, url, headers=None, params=None, **kwargs):
return await self.request('GET', url, headers=headers, params=params, **kwargs)
async def post(self, url, headers=None, params=None, data=None, json=None, **kwargs):
return await self.request(
'POST', url, headers=headers, params=params, data=data, json=json, **kwargs
)
@async_retry(exceptions=(SearchCardRetry,))
async def psa(self, cert_number: str | int):
"""PSA"""
url = f'https://www.psacard.com/cert/{cert_number}'
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)
chck_string = 'Just a moment...'
if chck_string in r.text:
raise SearchCardRetry(chck_string)
return r
async def psa_images(self, cert_number: str | int, images_id: str | int):
"""PSA图片"""
url = 'https://www.psacard.com/GetPSACertImages'
headers = {
'X-Requested-With': 'XMLHttpRequest',
'Origin': 'https://www.psacard.com',
'Referer': f'https://www.psacard.com/cert/{cert_number}',
}
payload = {
'certID': images_id
}
return await self.post(url, headers=headers, params=payload, use_abroad_proxy=True)
async def bgs(self, cert_number: str | int):
"""BGS"""
url = 'https://www.beckett.com/api/grading/lookup'
params = {
'category': 'BGS',
'serialNumber': cert_number
}
r = await self.get(url, headers=self.headers, params=params, use_abroad_proxy=True)
return r
async def cgc(self, cert_number: str | int):
"""CGC"""
url = f'https://www.cgccards.com/certlookup/{cert_number}/'
headers = {
'referer': 'https://www.cgccards.com/'
}
return await self.get(url, headers=headers)
async def gbtc(self, cert_number: str | int):
"""北京公博"""
url = 'https://wapi.gongbocoins.com/gbca/orderCoin/getWebsiteRatingInfo'
headers = {
'Content-Type': "application/json;charset=UTF-8",
'origin': 'https://www.gongbocoins.com',
'referer': "https://www.gongbocoins.com"
}
payload = {
'ratingCode': cert_number
}
return await self.post(url, headers=headers, json=payload)
async def bctc(self, cert_number: str | int):
"""南京保粹"""
url = f'https://web-api.baocuicoin.com/Search/index?keyword={cert_number}&code'
headers = {
'origin': 'https://www.baocuicoin.com/',
'referer': "https://www.baocuicoin.com/"
}
r = await self.post(url, headers=headers)
return r
async def ccg(self, cert_number: str | int):
"""重庆藏卡"""
url = 'https://webapi.cangcard.com/webapi/card/num_list'
payload = {
'num': cert_number
}
headers = {
'Content-Type': 'application/json',
'Referer': "https://servicewechat.com/wx0b020dffc7cf7e74/75/page-frame.html",
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 MicroMessenger/7.0.20.1781(0x6700143B) NetType/WIFI MiniProgramEnv/Windows WindowsWechat/WMPF WindowsWechat(0x63090c1f)XWEB/11581'
}
r = await self.post(url, headers=headers, json=payload)
return r
async def close(self):
await self.session.close()
class CardScraper:
def __init__(self):
self.api = SiteApi()
self._handlers = None
@property
def handlers(self):
if self._handlers is None:
self._handlers = {
'psa': self.psa,
'bgs': self.bgs,
'cgc': self.cgc,
'gbtc': self.gbtc,
'ccg': self.ccg,
'bctc': self.bctc,
}
return self._handlers
async def psa(self, cert_number: str | int) -> dict | None:
"""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:
return
info_elements = html.xpath('//main[@id="mainContent"]/div/table//tr')
info = dict()
for info_element in info_elements:
name = info_element.xpath('./th[1]').xpath('string()').get()
name = name.lower().replace(' ', '_').replace('/', '_')
value = info_element.xpath('./td[1]').xpath('string()').get()
info[name.strip()] = value.strip()
picture_elements = info_elements.xpath('.//img')
info['pictures'] = [
{
'name': picture_element.xpath('./@alt').get(),
'value': picture_element.xpath('./@src').get(),
} for picture_element in picture_elements
]
item = dict(cert_number=None)
# 评级卡号
item['cert_number'] = info['certification_number']
# 年份
item['card_year'] = info.get('year')
# 卡片品牌
item['card_brand'] = info.get('brand')
# 卡片评分
card_grade = info.get('card_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_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+)\'\);')
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 {
'cert_number': item['cert_number'],
'card_number': cert_number,
'data': item,
'raw_data': info,
}
async def bgs(self, cert_number: str | int) -> dict | None:
r = await self.api.bgs(cert_number)
if r.status_code != 200:
return
result = r.json()
item = dict(cert_number=None)
# 评级卡号
item['cert_number'] = result.get('item_id')
# 年份
set_name = result.get('set_name') or ''
item['card_year'] = regx_match(r'(\d{,4}).*', set_name, first=True)
# 卡片品牌
item['card_brand'] = result.get('sport_name')
# 卡片评分
item['card_score'] = result.get('final_grade')
# 签字评分
auto_score = result.get('autograph_grade')
item['auto_score'] = auto_score if auto_score != '0.0' else None
# 卡片名称
item['card_name'] = result.get('player_name')
# 卡片编号 无
# 卡片类型 无
# 卡片正、反图片(远程) 无
# 来源
item['source'] = 'bgs'
return {
'cert_number': item['cert_number'],
'card_number': cert_number,
'data': item,
'raw_data': result,
}
async def cgc(self, cert_number: str | int):
"""CGC"""
r = await self.api.cgc(cert_number)
html = Selector(r.text)
if html.xpath('//div[@class="error" and @ng-show]').get() is None:
return
info_elements = html.xpath('//div[@class="content-wrapper"]//dl')
info = dict()
for info_element in info_elements:
name = info_element.xpath('./dt[1]').xpath('string()').get()
name = name.lower().replace('#', '').strip().replace(' ', '_')
value = info_element.xpath('./dd[1]').xpath('string()').get()
info[name.strip()] = value.strip()
item = dict(cert_number=None)
# 评级卡号
item['cert_number'] = info.get('cert')
# 年份
item['card_year'] = info.get('year')
# 卡片品牌
item['card_brand'] = info.get('game')
# 卡片评分
item['card_score'] = regx_match(r'(\d+)', info.get('grade') or '', first=True)
# 签字评分 无
# 卡片名称
item['card_name'] = info.get('card_name')
# 卡片编号
item['card_number'] = info.get('card_number')
# 卡片类型 无
# 卡片正、反图片(远程)
picture_query = '//div[@class="certlookup-images"]/div[@class="certlookup-images-item"][{index}]//img/@src'
item['card_spare_picture'] = {
'front_picture': html.xpath(picture_query.format(index=1)).get(),
'reverse_picture': html.xpath(picture_query.format(index=2)).get()
}
# 来源
item['source'] = 'cgc'
return {
'cert_number': item['cert_number'],
'card_number': cert_number,
'data': item,
'raw_data': info,
}
async def gbtc(self, cert_number: str | int) -> dict | None:
"""北京公博"""
r = await self.api.gbtc(cert_number)
result = r.json()
if result.get('errorCode') != 0:
return
attrs = jsonpath(result, '$.data.attr', first=True)
attrs_string = ';'.join(attrs)
item = dict(cert_number=None)
# 评级卡号
item['cert_number'] = jsonpath(result, '$.data.ratingCode', first=True)
# 年份
item['card_year'] = regx_match(r'年份:(\d{,4})', attrs_string, first=True)
# 卡片品牌
item['card_brand'] = regx_match(r'发行商:(.*?);', attrs_string, first=True)
# 卡片评分
item['card_score'] = jsonpath(result, '$.data.goodsScore', first=True)
# 签字评分
item['auto_score'] = regx_match(r'签字分数:(\d+)', attrs_string, first=True)
# 卡片名称
item['card_name'] = jsonpath(result, '$.data.goodsName', first=True)
# 卡片编号
item['card_number'] = regx_match(r'卡片编码:(.*?);', attrs_string, first=True)
# 卡片类型 无
# 卡片正、反图片(远程)
item['card_spare_picture'] = {
'front_picture': jsonpath(result, '$.data.imgList[0].self', first=True),
'reverse_picture': jsonpath(result, '$.data.imgList[1].self', first=True)
}
# 来源
item['source'] = 'gbtc'
return {
'cert_number': item['cert_number'],
'card_number': cert_number,
'data': item,
'raw_data': result.get('data'),
}
async def ccg(self, cert_number: str | int) -> dict | None:
"""重庆藏卡"""
r = await self.api.ccg(cert_number)
result = r.json()
if result.get('code') != '200':
return
item = dict(cert_number=None)
# 评级卡号
item['cert_number'] = jsonpath(result, '$.data.num', first=True)
# 年份
item['card_year'] = jsonpath(result, '$.data.year', first=True)
# 卡片品牌
content = jsonpath(result, '$.data.content', first=True)
item['card_brand'] = regx_match(r'\d{4}(.*?)\.', content, first=True)
# 卡片评分
item['card_score'] = jsonpath(result, '$.data.zm_num', first=True)
# 签字评分 无
# 卡片名称
item['card_name'] = jsonpath(result, '$.data.name', first=True)
# 卡片类型 无
# 卡片编号
item['card_number'] = content
# 卡片正、反图片(远程)
item['card_spare_picture'] = {
'front_picture': jsonpath(result, '$.data.img_z', first=True),
'reverse_picture': jsonpath(result, '$.data.img_f', first=True)
}
# 来源
item['source'] = 'ccg'
return {
'cert_number': item['cert_number'],
'card_number': cert_number,
'data': item,
'raw_data': result.get('data'),
}
async def bctc(self, cert_number: str | int) -> dict | None:
"""南京保粹"""
r = await self.api.bctc(cert_number)
result = r.json()
if result.get('code') != '200':
return
details = jsonpath(result, '$.info.details', first=True)
item = dict(cert_number=None)
# 评级卡号
item['cert_number'] = jsonpath(details, '$.grade[?(@.title =="证书编号")].val', first=True)
# 年份
year_string = jsonpath(details, '$.grade[?(@.title =="年份")].val', first=True)
item['card_year'] = year = regx_match(r'(\d{,4})', year_string, first=True)
# 卡片品牌
item['card_brand'] = year_string.replace(f'{year}', '').strip() if year_string else None
# 卡片评分
item['card_score'] = jsonpath(details, '$.grade[?(@.title =="分值")].val', first=True)
# 签字评分
item['auto_score'] = jsonpath(details, '$.grade[?(@.title =="签字分值")].val', first=True)
# 卡片名称
item['card_name'] = jsonpath(details, '$.grade[?(@.title =="名称")].val', first=True)
# 卡片编号
item['card_number'] = jsonpath(details, '$.grade[?(@.title =="编号")].val', first=True)
# 卡片类型 无
# 卡片正、反图片(远程)
item['card_spare_picture'] = {
'front_picture': jsonpath(details, '$.info.picurl', first=True),
'reverse_picture': jsonpath(details, '$.info.dt_pics', first=True)
}
# 来源
item['source'] = 'bctc'
return {
'cert_number': item['cert_number'],
'card_number': cert_number,
'data': item,
'raw_data': result.get('info'),
}
async def choice(self, platform: str, cert_number: str | int) -> dict | None:
handler = self.handlers.get(platform.lower())
if handler is None:
raise Exception('平台不支持')
return await handler(cert_number)
async def close(self):
await self.api.close()
card_scraper = CardScraper()
async def main():
import time
started = time.time()
print(format_json(await card_scraper.choice('psa', '83610272')))
print(time.time() - started)
# print(format_json(await card_scraper.choice('psa', '83610272')))
# print(format_json(await card_scraper.psa('83610272')))
# print(format_json(await card_scraper.cgc('6011456021')))
# print(format_json(await card_scraper.bgs('16770585')))
# print(format_json(await card_scraper.gbtc('8110071983')))
# print(format_json(await card_scraper.bctc('E000033353')))
# print(format_json(await card_scraper.ccg('230810139152')))
if __name__ == '__main__':
import asyncio
import sys
if sys.platform == 'win32':
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
asyncio.run(main())

View File

@@ -0,0 +1,18 @@
# -*- coding = utf-8 -*-
# @Time : 2025/2/17 上午1:00
# @File : services.py
# @Software : PyCharm
# @Author : xingc
# @Desc :
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from core.db.models import SearchCardCache
async def get_cache_card_by_number(db_session: AsyncSession, *, cert_number: str) -> SearchCardCache | None:
query = select(SearchCardCache).where(SearchCardCache.cert_number == cert_number)
result = await db_session.execute(query)
card: SearchCardCache | None = result.scalar_one_or_none()
return card

View File

@@ -0,0 +1,6 @@
# -*- coding = utf-8 -*-
# @Time : 2025/2/13 下午4:01
# @File : __init__.py.py
# @Software : PyCharm
# @Author : xingc
# @Desc : 文件存储模块

View File

@@ -0,0 +1,76 @@
# -*- coding = utf-8 -*-
# @Time : 2025/2/13 下午4:50
# @File : routes.py
# @Software : PyCharm
# @Author : xingc
# @Desc :
import hashlib
import logging
import os
from io import BytesIO
from typing import Literal
from fastapi import APIRouter, UploadFile, File, status, HTTPException, Depends
from PIL import Image
from config import settings
from . import schema
from core.base.schema import BaseResponse
logger = logging.getLogger(__name__)
file_router = APIRouter(prefix='/file', tags=['文件存储模块'])
@file_router.post('/{upload_type}/upload', response_model=BaseResponse[schema.FileOut])
async def upload_file(
upload_type: Literal['card', 'chat', 'avatar'],
file: UploadFile = File(...)
):
# 验证文件类型
if file.content_type not in settings.UPLOADED_ALLOWED_MIME_TYPES:
raise HTTPException(
status_code=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
detail=f'Unsupported media type: {file.content_type}'
)
# 验证文件大小
if file.size > settings.UPLOADED_MAX_FILE_SIZE:
raise HTTPException(
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
detail=f'File exceeds maximum size'
)
try:
file_chunks = await file.read()
file_hash = hashlib.sha256(file_chunks).hexdigest()
file_name = f'{file_hash}.jpg'
dirname_args = [settings.UPLOADED_FILES_DIR, upload_type.lower()]
if upload_type.lower() != 'card':
dirname_args.append('test')
save_path = os.path.join(*dirname_args)
save_file_path = os.path.join(*dirname_args, file_name)
if os.path.exists(save_file_path) is False:
# 创建目录
os.makedirs(save_path, exist_ok=True)
img = Image.open(BytesIO(file_chunks))
img = img.convert("RGB")
img.save(save_file_path, format="JPEG")
file_info = {
'filename': file_name,
'content_type': file.content_type,
'size': os.path.getsize(save_path),
'hash': file_hash,
'url': save_file_path
}
return {'data': file_info}
except Exception as e:
logger.error(f'Upload failed: {str(e)}')
logger.exception(e)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f'File upload failed'
)

View File

@@ -0,0 +1,14 @@
# -*- coding = utf-8 -*-
# @Time : 2025/2/13 下午5:01
# @File : schema.py
# @Software : PyCharm
# @Author : xingc
# @Desc :
from pydantic import BaseModel
class FileOut(BaseModel):
filename: str
content_type: str
size: int
hash: str

View File

@@ -0,0 +1,6 @@
# -*- coding = utf-8 -*-
# @Time : 2025/2/13 下午1:03
# @File : __init__.py.py
# @Software : PyCharm
# @Author : xingc
# @Desc :

33
core/routers/im/routes.py Normal file
View File

@@ -0,0 +1,33 @@
# -*- coding = utf-8 -*-
# @Time : 2025/2/13 下午4:53
# @File : routes.py
# @Software : PyCharm
# @Author : xingc
# @Desc :
from typing import Annotated
from fastapi import APIRouter, Query
im_router = APIRouter(prefix='/im', tags=['聊天模块'])
# @im_router.get('/chat/{user_id}/messages/')
# def get_single_chat_messages(
# user_id: int,
# limit: Annotated[int, Query(ge=1, le=200)] = 20,
# cursor: int = -1,
# ):
# pass
#
#
# @im_router.get('/chats/messages/')
# def get_chats_messages(
# limit: Annotated[int, Query(ge=1, le=200)] = 100,
# cursor: int = -1,
# ):
# pass
#
#
# @im_router.websocket('/chat')
# def get_chat():
# pass

View File

@@ -0,0 +1,6 @@
# -*- coding = utf-8 -*-
# @Time : 2025/2/13 下午4:01
# @File : __init__.py.py
# @Software : PyCharm
# @Author : xingc
# @Desc :

View File

@@ -0,0 +1,9 @@
# -*- coding = utf-8 -*-
# @Time : 2025/2/13 下午4:53
# @File : routes.py
# @Software : PyCharm
# @Author : xingc
# @Desc :
from fastapi import APIRouter
user_router = APIRouter(prefix='/user', tags=['用户模块'])