fix(card): 修复cic搜卡失败,增加响应data解密处理

This commit is contained in:
xingc
2025-07-21 14:58:39 +08:00
parent 57d71af146
commit 071ad07979
3 changed files with 79 additions and 13 deletions

View File

@@ -23,10 +23,17 @@ class SearchCardRetry(ScraperError):
"""搜索重试""" """搜索重试"""
pass pass
class SearchCardRetryFail(ScraperError): class SearchCardRetryFail(ScraperError):
"""搜索重试失败""" """搜索重试失败"""
pass pass
class SearchCardHandlerFail(ScraperError):
"""搜索处理失败"""
pass
class WechatApiError(CardBookException): class WechatApiError(CardBookException):
"""微信开放服务调用api错误""" """微信开放服务调用api错误"""
pass pass

View File

@@ -6,15 +6,18 @@
# @Desc : # @Desc :
import logging import logging
import re import re
from typing import Any
import ujson
from curl_cffi import requests, CurlError from curl_cffi import requests, CurlError
from fake_useragent import UserAgent from fake_useragent import UserAgent
from parsel import Selector from parsel import Selector
from simple_spider_tool import jsonpath, regx_match, format_json from simple_spider_tool import jsonpath, regx_match, format_json
from config import settings from config import settings
from core.base.exceptions import SearchCardNotFound, SearchCardRetry, SearchCardRetryFail from core.base.exceptions import SearchCardRetry, SearchCardRetryFail, SearchCardHandlerFail
from core.utils.helper import async_retry from core.utils.helper import async_retry
from core.utils.helper import decrypt_aes_cbc
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -182,9 +185,18 @@ class SiteApi:
await self.session.close() await self.session.close()
class SiteDecrypt:
@staticmethod
def cic(ciphertext_b64: str, iv_str):
key_str = '3bd48ea5e910b195843941351be7cbae'
decrypted_text = decrypt_aes_cbc(ciphertext_b64, key_str, iv_str)
return decrypted_text
class CardScraper: class CardScraper:
def __init__(self): def __init__(self):
self.api = SiteApi() self.api = SiteApi()
self.decrypt = SiteDecrypt()
self._handlers = None self._handlers = None
@property @property
@@ -213,7 +225,7 @@ class CardScraper:
for info_element in info_elements: for info_element in info_elements:
name = info_element.xpath('./dt[1]').xpath('string()').get() or info_element.xpath( name = info_element.xpath('./dt[1]').xpath('string()').get() or info_element.xpath(
'./dt/template/@id').get() or '' './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('./dd[1]').xpath('string()').get() value = info_element.xpath('./dd[1]').xpath('string()').get()
info[name.strip()] = value.strip() info[name.strip()] = value.strip()
@@ -225,7 +237,7 @@ class CardScraper:
} for picture_element in picture_elements } for picture_element in picture_elements
] ]
item = dict(cert_number=None) item: dict[str, Any] = dict(cert_number=None)
# 评级卡号 # 评级卡号
item['cert_number'] = info['cert_number'] item['cert_number'] = info['cert_number']
# 年份 # 年份
@@ -264,7 +276,7 @@ class CardScraper:
return return
result = r.json() result = r.json()
item = dict(cert_number=None) item: dict[str, Any] = dict(cert_number=None)
# 评级卡号 # 评级卡号
item['cert_number'] = result.get('item_id') item['cert_number'] = result.get('item_id')
# 年份 # 年份
@@ -290,7 +302,7 @@ class CardScraper:
'raw_data': result, 'raw_data': result,
} }
async def cgc(self, cert_number: str | int): async def cgc(self, cert_number: str | int) -> dict | None:
"""CGC""" """CGC"""
r = await self.api.cgc(cert_number) r = await self.api.cgc(cert_number)
html = Selector(r.text) html = Selector(r.text)
@@ -305,7 +317,7 @@ class CardScraper:
value = info_element.xpath('./dd[1]').xpath('string()').get() value = info_element.xpath('./dd[1]').xpath('string()').get()
info[name.strip()] = value.strip() info[name.strip()] = value.strip()
item = dict(cert_number=None) item: dict[str, Any] = dict(cert_number=None)
# 评级卡号 # 评级卡号
item['cert_number'] = info.get('cert') item['cert_number'] = info.get('cert')
# 年份 # 年份
@@ -344,7 +356,7 @@ class CardScraper:
attrs = jsonpath(result, '$.data.attr', first=True) attrs = jsonpath(result, '$.data.attr', first=True)
attrs_string = ';'.join(attrs) attrs_string = ';'.join(attrs)
item = dict(cert_number=None) item: dict[str, Any] = dict(cert_number=None)
# 评级卡号 # 评级卡号
item['cert_number'] = jsonpath(result, '$.data.ratingCode', first=True) item['cert_number'] = jsonpath(result, '$.data.ratingCode', first=True)
# 年份 # 年份
@@ -380,7 +392,7 @@ class CardScraper:
if result.get('code') != '200' or not result['data']: if result.get('code') != '200' or not result['data']:
return return
item = dict(cert_number=None) item: dict[str, Any] = dict(cert_number=None)
# 评级卡号 # 评级卡号
item['cert_number'] = jsonpath(result, '$.data.num', first=True) item['cert_number'] = jsonpath(result, '$.data.num', first=True)
# 年份 # 年份
@@ -418,7 +430,7 @@ class CardScraper:
details = jsonpath(result, '$.info.details', first=True) details = jsonpath(result, '$.info.details', first=True)
item = dict(cert_number=None) item: dict[str, Any] = dict(cert_number=None)
# 评级卡号 # 评级卡号
item['cert_number'] = jsonpath(details, '$.grade[?(@.title =="证书编号")].val', first=True) item['cert_number'] = jsonpath(details, '$.grade[?(@.title =="证书编号")].val', first=True)
# 年份 # 年份
@@ -455,9 +467,19 @@ class CardScraper:
if result.get('code') != 200: if result.get('code') != 200:
return return
details = jsonpath(result, '$.data.obj_order_rating_goods', first=True) ciphertext = result.get('data')
iv_str = result.get('iv')
try:
plaintext_str = self.decrypt.cic(ciphertext, iv_str)
item = dict(cert_number=None) plaintext = ujson.loads(plaintext_str)
except Exception as e:
logger.error(e)
raise SearchCardHandlerFail('请联系客服反馈搜卡异常')
details = jsonpath(plaintext, '$.obj_order_rating_goods', first=True)
item: dict[str, Any] = dict(cert_number=None)
# 评级卡号 # 评级卡号
item['cert_number'] = details.get('cert_no') item['cert_number'] = details.get('cert_no')
# 年份 # 年份
@@ -465,7 +487,7 @@ class CardScraper:
# 卡片品牌 # 卡片品牌
item['card_brand'] = jsonpath(details, '$.obj_brand.title', first=True) item['card_brand'] = jsonpath(details, '$.obj_brand.title', first=True)
# 卡片评分 # 卡片评分
item['card_score'] = details.get('score') item['card_score'] = details.get('score')
# 卡片名称 # 卡片名称
item['card_name'] = details.get('goods_name') item['card_name'] = details.get('goods_name')
# 卡片编号 # 卡片编号

View File

@@ -4,10 +4,12 @@
# @Software : PyCharm # @Software : PyCharm
# @Author : xingc # @Author : xingc
# @Desc : # @Desc :
import asyncio import base64
import functools import functools
import random import random
import string import string
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
from datetime import datetime from datetime import datetime
from typing import Callable from typing import Callable
@@ -73,3 +75,38 @@ def format_date(date: datetime | None) -> str | None:
return date return date
else: else:
return date.strftime('%Y-%m-%d %H:%M:%S') return date.strftime('%Y-%m-%d %H:%M:%S')
def decrypt_aes_cbc(ciphertext_b64, key_str, iv_str):
"""
等效于 Node.js CryptoJS 的解密实现
参数:
ciphertext_b64: Base64编码的密文
key_str: 密钥字符串(UTF-8编码)
iv_str: 初始化向量字符串(UTF-8编码)
返回:
解密后的明文(UTF-8字符串)
"""
# 将密钥和IV从UTF-8字符串转换为bytes
key = key_str.encode('utf-8')
iv = iv_str.encode('utf-8')
# 确保密钥和IV长度正确
if len(key) not in [16, 24, 32]:
raise ValueError("密钥长度必须是16(AES-128), 24(AES-192)或32(AES-256)字节")
if len(iv) != 16:
raise ValueError("IV长度必须是16字节")
# 解码Base64密文
ciphertext = base64.b64decode(ciphertext_b64)
# 创建解密器
cipher = AES.new(key, AES.MODE_CBC, iv)
# 解密并去除PKCS7填充
decrypted = unpad(cipher.decrypt(ciphertext), AES.block_size)
return decrypted.decode('utf-8')