Files
kami_spider_monorepo/apps/jd/services/jstk.py
danial 6c768b6e7b feat(jd): 添加京东相关路由及苹果权益充值功能
- 新增jd模块基础路由,整合app_store和payment子路由
- 实现苹果权益充值接口,支持苹果、携程及沃尔玛多个渠道
- 实现卡号密码查询接口,支持不同类别订单查询
- 新增短信认证相关接口,实现短信验证码发送及短信登录
- 新增商品管理接口,支持SKU详情查询及账号类下单功能
- 新增订单管理接口,实现订单删除功能
- 实现支付相关接口,增加刷新支付参数功能
- 定义完整请求及响应数据模型,确保接口数据规范
- 编写AppStoreSpider类,封装苹果应用内订单处理逻辑
- 引入多种代理池及请求重试机制,增强接口稳定性
- 添加详细日志记录,便于请求追踪与错误排查
2025-11-03 19:35:39 +08:00

691 lines
25 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import hashlib
import json
import random
import re
import time
import urllib
from abc import ABC, abstractmethod
from urllib.parse import urlparse
from curl_cffi import requests
class JsTkBase(ABC):
def __init__(self):
self._eid = ""
self._token = ""
def _get_sign(self, text: str) -> str:
text_bytes = text.encode("utf-8")
md5_str = hashlib.md5(text_bytes).hexdigest()
return md5_str
def get_token(self):
return self._token
def get_eid(self):
return self._eid
def _td_encrypt(self, e):
"""
将输入 e 按照给定的JS逻辑进行 JSON化 -> URL编码 -> 自定义Base64编码 -> 加斜杠
Args:
e: 输入的数据 (可以是字典、列表、字符串、数字等会被JSON序列化)
Returns:
str: 编码后的字符串,末尾带 "/"
"""
# 1. 自定义字母表,对应 Base64 的 64 个字符
# JS中的 'u' 是 0-63 的索引对应的字符
custom_alphabet = (
"23IL<N01c7KvwZO56RSTAfghiFyzWJqVabGH4PQdopUrsCuX*xeBjkltDEmn89.-"
)
# JS代码中索引 64 被用于填充字符。虽然字母表里没有 '=',
# 但标准Base64用 '=' 填充且JS代码逻辑中使用了 64推测 64 对应填充字符 '='
padding_char = "="
# 2. 将输入 e 转换为 JSON 字符串
s = json.dumps(e, separators=(",", ":"))
# 3. 对 JSON 字符串进行 URL 编码
# encodeURIComponent 编码所有字符,除了 ASCII 字母、数字、- _ . ! ~ * ' ( )
# Python 的 urllib.parse.quote 需要指定 safe 字符集来模拟
s_encoded = urllib.parse.quote(
s,
safe="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.!~*'()",
)
# 获取 URL 编码后字符串的字节表示,因为 Base64 编码是基于字节的
s_bytes = s_encoded.encode("utf-8")
# 4. 执行自定义的 Base64 编码
encoded_string = ""
d = 0 # 字节索引
s_len = len(s_bytes)
while d < s_len:
# 取 3 个字节(可能不足 3 个在末尾)
byte1 = s_bytes[d]
d += 1
byte2 = s_bytes[d] if d < s_len else None
d += 1
byte3 = s_bytes[d] if d < s_len else None
d += 1
# 将 3 个字节24 位)分成 4 组,每组 6 位
# 索引 1: byte1 的前 6 位
idx1 = (byte1 >> 2) & 63
# 索引 2: byte1 的后 2 位 + byte2 的前 4 位
idx2 = (byte1 & 3) << 4
if byte2 is not None:
idx2 |= (byte2 >> 4) & 15
# else: 如果 byte2 不存在,这里只使用了 byte1 的后 2 位,高位补 0
# 索引 3: byte2 的后 4 位 + byte3 的前 2 位
idx3 = 64 # 默认填充
if byte2 is not None:
idx3 = (byte2 & 15) << 2
if byte3 is not None:
idx3 |= (byte3 >> 6) & 3
# else: 如果 byte3 不存在,只使用了 byte2 的后 4 位,高位补 0
# else: 如果 byte2 不存在,保持填充状态 64
# 索引 4: byte3 的后 6 位
idx4 = 64 # 默认填充
if byte3 is not None:
idx4 = byte3 & 63
# else: 如果 byte3 不存在,保持填充状态 64
# 根据索引查找字符或使用填充符
encoded_string += custom_alphabet[idx1]
encoded_string += custom_alphabet[idx2]
if idx3 == 64:
encoded_string += padding_char
else:
encoded_string += custom_alphabet[idx3]
if idx4 == 64:
encoded_string += padding_char
else:
encoded_string += custom_alphabet[idx4]
# 5. 在末尾添加 "/"
return encoded_string + "/"
def _request(self, headers: dict, data: dict):
response = requests.post(
"https://jra.jd.com/jsTk.do", headers=headers, data=data
)
result = response.json()
self._token = result.get("data", {}).get("token")
self._eid = result.get("data", {}).get("eid")
@abstractmethod
def generate_token(self, user_agent: str, cookie: str = "") -> str:
pass
class NormalJsTk(JsTkBase):
def __init__(self):
super().__init__()
self.__biz_id = "gold_m"
self.__o = "m.jd.com/"
def generate_token(self, user_agent: str, cookie: str = ""):
ctime = int(time.time() * 1000)
app_version = user_agent.replace("Mozilla/", "")
d_param = {
"ts": {
"deviceTime": ctime,
"deviceEndTime": ctime + random.randint(50, 150),
},
"ca": {
"tdHash": "af7c21fe564a8cf177fcc77ce8847eb0",
"contextName": "webgl,experimental-webgl",
"webglversion": "WebGL 1.0 (OpenGL ES 2.0 Chromium)",
"shadingLV": "WebGL GLSL ES 1.0 (OpenGL ES GLSL ES 1.0 Chromium)",
"vendor": "WebKit",
"renderer": "WebKit WebGL",
"extensions": [
"ANGLE_instanced_arrays",
"EXT_blend_minmax",
"EXT_clip_control",
"EXT_color_buffer_half_float",
"EXT_depth_clamp",
"EXT_disjoint_timer_query",
"EXT_float_blend",
"EXT_frag_depth",
"EXT_polygon_offset_clamp",
"EXT_shader_texture_lod",
"EXT_texture_compression_bptc",
"EXT_texture_compression_rgtc",
"EXT_texture_filter_anisotropic",
"EXT_texture_mirror_clamp_to_edge",
"EXT_sRGB",
"KHR_parallel_shader_compile",
"OES_element_index_uint",
"OES_fbo_render_mipmap",
"OES_standard_derivatives",
"OES_texture_float",
"OES_texture_float_linear",
"OES_texture_half_float",
"OES_texture_half_float_linear",
"OES_vertex_array_object",
"WEBGL_blend_func_extended",
"WEBGL_color_buffer_float",
"WEBGL_compressed_texture_astc",
"WEBGL_compressed_texture_etc",
"WEBGL_compressed_texture_etc1",
"WEBGL_compressed_texture_pvrtc",
"WEBGL_compressed_texture_s3tc",
"WEBGL_compressed_texture_s3tc_srgb",
"WEBGL_debug_renderer_info",
"WEBGL_debug_shaders",
"WEBGL_depth_texture",
"WEBGL_draw_buffers",
"WEBGL_lose_context",
"WEBGL_multi_draw",
"WEBGL_polygon_mode",
],
"wuv": "Google Inc. (Apple)",
"wur": "ANGLE (Apple, ANGLE Metal Renderer: Apple M4, Unspecified Version)",
},
"m": {"compatMode": "CSS1Compat"},
"n": {
"vendorSub": "",
"productSub": "20030107",
"vendor": "Google Inc.",
"maxTouchPoints": 0,
"doNotTrack": "1",
"pdfViewerEnabled": True,
"hardwareConcurrency": 10,
"cookieEnabled": True,
"appCodeName": "Mozilla",
"appName": "Netscape",
"appVersion": app_version,
"platform": "MacIntel",
"product": "Gecko",
"userAgent": user_agent,
"language": "zh-CN",
"onLine": True,
"webdriver": False,
"javaEnabled": False,
"deprecatedRunAdAuctionEnforcesKAnonymity": False,
"deviceMemory": 8,
"enumerationOrder": [
"vendorSub",
"productSub",
"vendor",
"maxTouchPoints",
"scheduling",
"userActivation",
"doNotTrack",
"geolocation",
"connection",
"plugins",
"mimeTypes",
"pdfViewerEnabled",
"webkitTemporaryStorage",
"webkitPersistentStorage",
"windowControlsOverlay",
"hardwareConcurrency",
"cookieEnabled",
"appCodeName",
"appName",
"appVersion",
"platform",
"product",
"userAgent",
"language",
"languages",
"onLine",
"webdriver",
"getGamepads",
"javaEnabled",
"sendBeacon",
"vibrate",
"deprecatedRunAdAuctionEnforcesKAnonymity",
"protectedAudience",
"bluetooth",
"storageBuckets",
"clipboard",
"credentials",
"keyboard",
"managed",
"mediaDevices",
"storage",
"serviceWorker",
"virtualKeyboard",
"wakeLock",
"deviceMemory",
"userAgentData",
"login",
"ink",
"mediaCapabilities",
"devicePosture",
"hid",
"locks",
"gpu",
"mediaSession",
"permissions",
"presentation",
"serial",
"usb",
"xr",
"adAuctionComponents",
"runAdAuction",
"canLoadAdAuctionFencedFrame",
"canShare",
"share",
"clearAppBadge",
"getBattery",
"getUserMedia",
"requestMIDIAccess",
"requestMediaKeySystemAccess",
"setAppBadge",
"webkitGetUserMedia",
"clearOriginJoinedAdInterestGroups",
"createAuctionNonce",
"joinAdInterestGroup",
"leaveAdInterestGroup",
"updateAdInterestGroups",
"deprecatedReplaceInURN",
"deprecatedURNToURL",
"getInstalledRelatedApps",
"getInterestGroupAdAuctionData",
"registerProtocolHandler",
"unregisterProtocolHandler",
],
},
"p": [
{"name": "PDF Viewer"},
{"name": "Chrome PDF Viewer"},
{"name": "Chromium PDF Viewer"},
{"name": "Microsoft Edge PDF Viewer"},
{"name": "WebKit built-in PDF"},
],
"w": {"devicePixelRatio": 2, "screenTop": 0, "screenLeft": 0},
"s": {
"availHeight": 1080,
"availWidth": 1920,
"colorDepth": 30,
"height": 1080,
"width": 1920,
"pixelDepth": 30,
},
"sc": {},
"ss": {
"cookie": True,
"localStorage": True,
"sessionStorage": True,
"globalStorage": False,
"indexedDB": True,
},
"tz": -480,
"lil": "",
"wil": "",
"wi": {
"ow": 1920,
"oh": 1080,
"iw": 274,
"ih": 959,
"etn": "[object External]",
},
}
headers = {
"accept": "*/*",
"accept-language": "zh-CN,zh;q=0.9",
"cache-control": "no-cache",
"content-type": "application/x-www-form-urlencoded;charset=UTF-8",
"origin": "https://shop.m.jd.com",
"pragma": "no-cache",
"priority": "u=1, i",
"referer": "https://shop.m.jd.com/",
"sec-ch-ua-mobile": "?1",
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-site",
"user-agent": user_agent,
}
data = {
"d": self._td_encrypt(d_param),
}
a_param = {
"pin": "",
"oid": "",
"bizId": self.__biz_id,
"fc": self._eid if self._eid else "",
"mode": "strict",
"p": "s",
"fp": "6d2c8c60af349b1a87ddd3b065194ca4",
"ctype": "1",
"v": "4.2.8.0",
"pv": "02_mt_5LXK_60653970589",
"f": "3",
"s": self._get_sign(data["d"] + "_*_UYBN6YGTNO6DHPVB"),
"o": self.__o,
"qs": "",
"jsTk": "",
"qi": "",
}
data["a"] = self._td_encrypt(a_param)
response = self._request(headers, data)
print(response)
class TxSMJsTk(JsTkBase):
def __init__(self):
super().__init__()
self.__url = "https://txsm-m.jd.com/?babelChannel=ttt35"
self.__biz_id = "jdtxsm"
self.__jd_risk_token_id = ""
self.__jd_jr_td_risk_pin = ""
self.__o = "txsm-m.jd.com/"
def get_jd_risk_token_id(self):
return self.__jd_risk_token_id
def get_jd_jr_td_risk_pin(self):
return self.__jd_jr_td_risk_pin
def generate_risk_pin(self, user_agent: str, cookie: str):
headers = {
"accept": "*/*",
"accept-language": "zh-CN,zh;q=0.9",
"cookie": cookie,
"user-agent": user_agent,
}
response = requests.get("https://gia.jd.com/m.html", headers=headers)
text = response.text.replace(" ", "")
token = re.findall(r"jd_risk_token_id='(.*?)';", text)
if token:
self.__jd_risk_token_id = token[0]
# 正则表达式解析后面
token = re.findall(r"jd_jr_td_risk_pin='(.*?)';", text)
if token:
self.__jd_jr_td_risk_pin = token[0]
def generate_token(self, user_agent: str, cookie: str = ""):
self.generate_risk_pin(user_agent, cookie)
ctime = int(time.time() * 1000)
app_version = user_agent.replace("Mozilla/", "")
d_param = {
"ts": {
"deviceTime": ctime,
"deviceEndTime": ctime + random.randint(50, 150),
},
"ca": {
"tdHash": "af7c21fe564a8cf177fcc77ce8847eb0",
"contextName": "webgl,experimental-webgl",
"webglversion": "WebGL 1.0 (OpenGL ES 2.0 Chromium)",
"shadingLV": "WebGL GLSL ES 1.0 (OpenGL ES GLSL ES 1.0 Chromium)",
"vendor": "WebKit",
"renderer": "WebKit WebGL",
"extensions": [
"ANGLE_instanced_arrays",
"EXT_blend_minmax",
"EXT_clip_control",
"EXT_color_buffer_half_float",
"EXT_depth_clamp",
"EXT_disjoint_timer_query",
"EXT_float_blend",
"EXT_frag_depth",
"EXT_polygon_offset_clamp",
"EXT_shader_texture_lod",
"EXT_texture_compression_bptc",
"EXT_texture_compression_rgtc",
"EXT_texture_filter_anisotropic",
"EXT_texture_mirror_clamp_to_edge",
"EXT_sRGB",
"KHR_parallel_shader_compile",
"OES_element_index_uint",
"OES_fbo_render_mipmap",
"OES_standard_derivatives",
"OES_texture_float",
"OES_texture_float_linear",
"OES_texture_half_float",
"OES_texture_half_float_linear",
"OES_vertex_array_object",
"WEBGL_blend_func_extended",
"WEBGL_color_buffer_float",
"WEBGL_compressed_texture_astc",
"WEBGL_compressed_texture_etc",
"WEBGL_compressed_texture_etc1",
"WEBGL_compressed_texture_pvrtc",
"WEBGL_compressed_texture_s3tc",
"WEBGL_compressed_texture_s3tc_srgb",
"WEBGL_debug_renderer_info",
"WEBGL_debug_shaders",
"WEBGL_depth_texture",
"WEBGL_draw_buffers",
"WEBGL_lose_context",
"WEBGL_multi_draw",
"WEBGL_polygon_mode",
],
"wuv": "Google Inc. (Apple)",
"wur": "ANGLE (Apple, ANGLE Metal Renderer: Apple M4, Unspecified Version)",
},
"m": {"compatMode": "CSS1Compat"},
"n": {
"vendorSub": "",
"productSub": "20030107",
"vendor": "Google Inc.",
"maxTouchPoints": 0,
"doNotTrack": "1",
"pdfViewerEnabled": True,
"hardwareConcurrency": 10,
"cookieEnabled": True,
"appCodeName": "Mozilla",
"appName": "Netscape",
"appVersion": app_version,
"platform": "MacIntel",
"product": "Gecko",
"userAgent": user_agent,
"language": "zh-CN",
"onLine": True,
"webdriver": False,
"javaEnabled": False,
"deprecatedRunAdAuctionEnforcesKAnonymity": False,
"deviceMemory": 8,
"enumerationOrder": [
"vendorSub",
"productSub",
"vendor",
"maxTouchPoints",
"scheduling",
"userActivation",
"doNotTrack",
"geolocation",
"connection",
"plugins",
"mimeTypes",
"pdfViewerEnabled",
"webkitTemporaryStorage",
"webkitPersistentStorage",
"windowControlsOverlay",
"hardwareConcurrency",
"cookieEnabled",
"appCodeName",
"appName",
"appVersion",
"platform",
"product",
"userAgent",
"language",
"languages",
"onLine",
"webdriver",
"getGamepads",
"javaEnabled",
"sendBeacon",
"vibrate",
"deprecatedRunAdAuctionEnforcesKAnonymity",
"protectedAudience",
"bluetooth",
"storageBuckets",
"clipboard",
"credentials",
"keyboard",
"managed",
"mediaDevices",
"storage",
"serviceWorker",
"virtualKeyboard",
"wakeLock",
"deviceMemory",
"userAgentData",
"login",
"ink",
"mediaCapabilities",
"devicePosture",
"hid",
"locks",
"gpu",
"mediaSession",
"permissions",
"presentation",
"serial",
"usb",
"xr",
"adAuctionComponents",
"runAdAuction",
"canLoadAdAuctionFencedFrame",
"canShare",
"share",
"clearAppBadge",
"getBattery",
"getUserMedia",
"requestMIDIAccess",
"requestMediaKeySystemAccess",
"setAppBadge",
"webkitGetUserMedia",
"clearOriginJoinedAdInterestGroups",
"createAuctionNonce",
"joinAdInterestGroup",
"leaveAdInterestGroup",
"updateAdInterestGroups",
"deprecatedReplaceInURN",
"deprecatedURNToURL",
"getInstalledRelatedApps",
"getInterestGroupAdAuctionData",
"registerProtocolHandler",
"unregisterProtocolHandler",
],
},
"p": [
{"name": "PDF Viewer"},
{"name": "Chrome PDF Viewer"},
{"name": "Chromium PDF Viewer"},
{"name": "Microsoft Edge PDF Viewer"},
{"name": "WebKit built-in PDF"},
],
"w": {"devicePixelRatio": 2, "screenTop": 0, "screenLeft": 0},
"s": {
"availHeight": 1080,
"availWidth": 1920,
"colorDepth": 30,
"height": 1080,
"width": 1920,
"pixelDepth": 30,
},
"sc": {
"ActiveBorder": "rgb(0, 0, 0)",
"ActiveCaption": "rgb(0, 0, 0)",
"AppWorkspace": "rgb(255, 255, 255)",
"Background": "rgb(255, 255, 255)",
"ButtonFace": "rgb(239, 239, 239)",
"ButtonHighlight": "rgb(239, 239, 239)",
"ButtonShadow": "rgb(239, 239, 239)",
"ButtonText": "rgb(0, 0, 0)",
"CaptionText": "rgb(0, 0, 0)",
"GrayText": "rgb(128, 128, 128)",
"Highlight": "rgba(128, 188, 254, 0.6)",
"HighlightText": "rgb(0, 0, 0)",
"InactiveBorder": "rgb(0, 0, 0)",
"InactiveCaption": "rgb(255, 255, 255)",
"InactiveCaptionText": "rgb(128, 128, 128)",
"InfoBackground": "rgb(255, 255, 255)",
"InfoText": "rgb(0, 0, 0)",
"Menu": "rgb(255, 255, 255)",
"MenuText": "rgb(0, 0, 0)",
"Scrollbar": "rgb(255, 255, 255)",
"ThreeDDarkShadow": "rgb(0, 0, 0)",
"ThreeDFace": "rgb(239, 239, 239)",
"ThreeDHighlight": "rgb(0, 0, 0)",
"ThreeDLightShadow": "rgb(0, 0, 0)",
"ThreeDShadow": "rgb(0, 0, 0)",
"Window": "rgb(255, 255, 255)",
"WindowFrame": "rgb(0, 0, 0)",
"WindowText": "rgb(0, 0, 0)",
},
"ss": {
"cookie": True,
"localStorage": True,
"sessionStorage": True,
"globalStorage": False,
"indexedDB": True,
},
"tz": -480,
"lil": "",
"wil": "",
"wi": {
"ow": 1920,
"oh": 1080,
"iw": 397,
"ih": 959,
"etn": "[object External]",
},
}
headers = {
"accept": "*/*",
"accept-language": "zh-CN,zh;q=0.9",
"cache-control": "no-cache",
"content-type": "application/x-www-form-urlencoded;charset=UTF-8",
"origin": "https://shop.m.jd.com",
"pragma": "no-cache",
"priority": "u=1, i",
"referer": "https://shop.m.jd.com/",
"sec-ch-ua-mobile": "?1",
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-site",
"user-agent": user_agent,
}
data = {
"d": self._td_encrypt(d_param),
}
if not self.__jd_jr_td_risk_pin and self._token:
self.__jd_jr_td_risk_pin = self._token
a_param = {
"pin": self.__jd_jr_td_risk_pin if self.__jd_jr_td_risk_pin else "",
"oid": "",
"bizId": self.__biz_id,
"fc": self._eid if self._eid else "",
"mode": "strict",
"p": "s",
"fp": "6d2c8c60af349b1a87ddd3b065194ca4",
"ctype": "1",
"v": "4.2.8.0",
"pv": "02_mt_5LXK_60653970589",
"f": "3",
"s": self._get_sign(data["d"] + "_*_UYBN6YGTNO6DHPVB"),
"o": self.__o,
"qs": urlparse(self.__url).query,
"jsTk": "",
"qi": "",
}
data["a"] = self._td_encrypt(a_param)
self._request(headers, data)