feat(jd): 添加京东相关路由及苹果权益充值功能

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

View File

@@ -0,0 +1,11 @@
from fastapi import APIRouter
def router() -> APIRouter:
router = APIRouter(prefix="/jd-anti-risk-spider")
from apps.jd.router import app_store, payment
router.include_router(app_store.router)
router.include_router(payment.router)
return router

216
apps/jd/router/app_store.py Normal file
View File

@@ -0,0 +1,216 @@
from typing import Any
import traceback
from fastapi import APIRouter
from fastapi.concurrency import run_in_threadpool
from apps.jd.schemas.models import (
AppStoreRequest,
AppleStoreRequestCategoryEnum,
PlatPayResponseData,
QueryCardRequest,
QueryCardResponseData,
)
from apps.jd.services.app_store import AppStoreSpider
from apps.jd.services.ctrip import XiechengCardSpider
from apps.shared.proxy_pool.proxy_pool import ProxyPoolFactory
from core.config import ProxyPoolType
from core.responses import ApiResponse, BusinessCode, error, success
from observability.logging import LoggerAdapter, get_logger_with_trace
router = APIRouter(prefix="/jd", tags=["苹果权益充值"])
logger: LoggerAdapter = get_logger_with_trace(__name__)
@router.post("/app/store", response_model=ApiResponse[PlatPayResponseData])
async def app_store(
request_data: AppStoreRequest,
) -> ApiResponse[PlatPayResponseData] | ApiResponse[Any]:
"""苹果权益充值"""
# 接收参数
order_num = request_data.order_num
cookies = request_data.cookies.strip()
res = {}
try:
match request_data.category:
case AppleStoreRequestCategoryEnum.Apple:
return error(code=BusinessCode.NOT_IMPLEMENTED)
# app_store_ = AppStoreSpider(
# cookies=cookies,
# face_price=request_data.face_price,
# order_num=order_num,
# )
# code, res, msg = await run_in_threadpool(app_store_.run)
# logger.info(msg=f"订单ID{order_num},最终返回日志:{res}")
# return success(data=res, message=msg)
case AppleStoreRequestCategoryEnum.CTrip:
# 10010164405229636
# 20010164405699325
# 30010164405273963
# 50010164404344795
# 20010155862962901
# 30010155867355510
# 50010155863307550
# 100010155867606890
# 100010164403489305
# 100010157256668464
# 50010157256668463
# 30010157256668462
# 20010157256668461
# 10010157256668460
skus = {
10.0: "10186761790338",
20.0: "10157199199546",
100.0: "10157256668460",
200.0: "10155862962901",
300.0: "10164405273963",
500.0: "10164404344795",
1000.0: "10155867606890",
102.0: "10157256668460",
308.0: "10155867355510",
520: "10155863307550",
1026: "10155867606890",
}
if request_data.face_price not in skus:
return error(
code=BusinessCode.JD_ORDER_FACE_PRICE_ERR,
data=PlatPayResponseData(
**{
"deeplink": "",
"order_id": "",
"pay_id": "",
"remark": "不支持的充值面额",
"face_price": 0,
}
),
message="不支持的充值金额",
)
try:
c_trip_spider = XiechengCardSpider(
cookies=cookies,
order_num=order_num,
sku_id=skus[request_data.face_price],
)
code, res = await run_in_threadpool(c_trip_spider.run)
if code == BusinessCode.SUCCESS:
return success(data=res)
return error(code=code, data=res, message="请求完成")
except Exception as e:
logger.error(f"请求失败:{traceback.format_exc()}")
return error(
code=BusinessCode.INTERNAL_ERROR,
data={
"deeplink": "",
"order_id": "",
"pay_id": "",
"remark": str(e),
},
)
case AppleStoreRequestCategoryEnum.Walmart:
skus = {
100.0: "10140177420168",
200.0: "10140177579817",
300.0: "10140177668026",
500.0: "10140177784201",
1000.0: "10140177806944",
}
if request_data.face_price not in skus:
return error(
code=BusinessCode.JD_ORDER_FACE_PRICE_ERR,
data={},
message="不支持的充值金额",
)
try:
code = BusinessCode.INTERNAL_ERROR
for i in range(3):
c_trip_spider = XiechengCardSpider(
cookies=cookies,
order_num=order_num,
sku_id=skus[request_data.face_price],
)
code, res = await run_in_threadpool(c_trip_spider.run)
if code == BusinessCode.JD_ORDER_RISK_ERR:
proxy_pool = ProxyPoolFactory.get_proxy_pool(
ProxyPoolType.EXPIRING, expire_time=60
)
proxy = proxy_pool.get_proxy(order_id=order_num)
if proxy:
proxy_pool.remove_invalid_proxy(proxy)
break
if code == BusinessCode.SUCCESS:
return success(data=res)
return error(code=code, data=res, message="请求完成")
except Exception as e:
logger.error(f"请求失败:{traceback.format_exc()}")
return error(
code=BusinessCode.INTERNAL_ERROR,
data={
"deeplink": "",
"order_id": "",
"pay_id": "",
"remark": str(e),
},
)
except Exception as e:
logger.error(f"请求失败:{traceback.format_exc()}")
return error(
code=BusinessCode.INTERNAL_ERROR,
data={
"deeplink": "",
"order_id": "",
"pay_id": "",
"remark": str(e),
},
message="请求完成",
)
@router.post("/query/card", response_model=ApiResponse[QueryCardResponseData])
async def query_card(request_data: QueryCardRequest):
"""查卡密"""
cookies = request_data.cookies.strip()
# 打印参数日志
logger.info(f"订单:{request_data}")
try:
match request_data.category:
case AppleStoreRequestCategoryEnum.Apple:
app_store_ = AppStoreSpider(
cookies=cookies, order_num=request_data.jd_order_id
)
code, res = await run_in_threadpool(
app_store_.query_card, request_data.jd_order_id
)
logger.info(
f"订单号:{request_data.order_id}订单ID{request_data.jd_order_id},最终返回日志:{res}"
)
if code == BusinessCode.SUCCESS:
return success(data=res)
return error(code=code, data=res, message="查询完成")
case (
AppleStoreRequestCategoryEnum.CTrip
| AppleStoreRequestCategoryEnum.Walmart
):
c_trip_spider = XiechengCardSpider(
cookies=cookies,
order_num=request_data.jd_order_id,
sku_id=request_data.order_id,
)
code, res = await run_in_threadpool(
c_trip_spider.get_locdetails, request_data.jd_order_id
)
logger.info(
f"订单号:{request_data.order_id}订单ID{request_data.jd_order_id},最终返回日志:{res}"
)
if code == BusinessCode.SUCCESS:
return success(data=res)
return error(code=code, data=res, message="查询完成")
return error(code=BusinessCode.JD_ORDER_RISK_ERR, data=None, message="查询失败")
except Exception as e:
logger.error(f"查询卡密失败: {traceback.format_exc()}")
return error(code=BusinessCode.INTERNAL_ERROR, data=None, message="查询异常")

41
apps/jd/router/auth.py Normal file
View File

@@ -0,0 +1,41 @@
from fastapi import APIRouter
from apps.jd.schemas.models import SmsCodeRequest, SmsLoginRequest
from apps.jd.services.login import LoginSpider
from observability.logging import get_logger_with_trace
router = APIRouter(prefix="/jd/sms", tags=["短信认证"])
logger = get_logger_with_trace(__name__)
@router.post("/code")
async def get_code(request_data: SmsCodeRequest):
"""发送短信验证码"""
phone = request_data.phone
res = LoginSpider(phone_=phone).run_send_code()
logger.info(f"发送验证码返回:{res}")
return res
@router.post("/login")
async def sms_login(request_data: SmsLoginRequest):
"""短信登录"""
phone = request_data.phone
ck = request_data.ck
code = request_data.code
s_token = request_data.s_token
jd_risk_token_id = request_data.jd_risk_token_id
rsa_modulus = request_data.rsa_modulus
res = LoginSpider(phone_=phone).run_get_ck(
ck=ck,
code=code,
s_token=s_token,
jd_risk_token_id=jd_risk_token_id,
rsa_modulus=rsa_modulus,
)
logger.info(f"短信登录返回:{res}")
return res

65
apps/jd/router/goods.py Normal file
View File

@@ -0,0 +1,65 @@
from logging import LoggerAdapter
import traceback
from fastapi import APIRouter, HTTPException
from apps.jd.schemas.models import GoodsStoreRequest, SkuRequest
from apps.jd.services.game_area import GameArea
from apps.jd.services.goods_apple_card import GoodsAppleCard
from core.responses import BusinessCode
from observability.logging import get_logger_with_trace
router = APIRouter(prefix="/jd", tags=["商品管理"])
logger: LoggerAdapter = get_logger_with_trace(__name__)
@router.post("/sku")
async def get_sku_details(request_data: SkuRequest):
"""获取sku参数"""
cookies = request_data.cookies.strip()
sku_id = request_data.sku_id
details = GameArea.get_details(cookies, sku_id)
data = details.get("result", {})
item = {"code": 100 if data else 110, "data": data}
return item
@router.post("/goods/store")
async def goods_store(request_data: GoodsStoreRequest):
"""账号类下单"""
# 参数
face_price = request_data.face_price
order_num = request_data.order_num
cookies = request_data.cookies.strip()
brand_id = request_data.brand_id
sku_id = request_data.sku_id
username = request_data.username
game_srv = request_data.gamesrv
game_area_ = request_data.gamearea
recharge_type = request_data.recharge_type
# 打印参数日志
logger.info(f"订单ID{order_num}cookies{cookies}")
logger.info(f"订单ID{order_num}card_pwd{face_price}")
try:
app_store__ = GoodsAppleCard(
cookies=cookies,
face_price=face_price,
order_num=order_num,
sku_id=sku_id,
brand_id=brand_id,
username=username,
game_srv=game_srv,
game_area=game_area_,
recharge_type=recharge_type,
)
code, res = app_store__.run()
logger.info(f"订单ID{order_num}goods_store最终返回日志{res}")
if code == BusinessCode.SUCCESS:
return res
except Exception as e:
logger.error("请求失败" + traceback.format_exc(), exc_info=True)
raise HTTPException(status_code=500, detail="请求失败")

25
apps/jd/router/order.py Normal file
View File

@@ -0,0 +1,25 @@
from fastapi import APIRouter
from apps.jd.schemas.models import DeleteOrderRequest
from apps.jd.services.delete import DeleteOrder
from observability.logging import LoggerAdapter, get_logger_with_trace
from core.responses import error
router = APIRouter(prefix="/api/v1/jd", tags=["订单管理"])
logger: LoggerAdapter = get_logger_with_trace(__name__)
@router.post("/delete_order")
async def delete_order(request_data: DeleteOrderRequest):
"""删除订单"""
# 接收参数
cookie = request_data.cookie.strip()
order_id = request_data.order_id
delete_res = DeleteOrder(cookie=cookie, order_id=order_id).run()
if isinstance(delete_res.get("body"), bool):
code = 2000
else:
code = 2001
return error(code=code, data={}, message="请求成功") # type: ignore

96
apps/jd/router/payment.py Normal file
View File

@@ -0,0 +1,96 @@
import traceback
from fastapi import APIRouter, HTTPException
from starlette.concurrency import run_in_threadpool
from apps.jd.schemas.models import (
PlatPayRequest,
PlatPayResponseData,
RefreshPaymentRequest,
)
from apps.jd.services.app_store import AppStoreSpider
from apps.jd.services.ctrip import XiechengCardSpider
from core.responses import ApiResponse, BusinessCode, success, error
from observability.logging import get_logger_with_trace
router = APIRouter(prefix="/order", tags=["支付"])
logger = get_logger_with_trace(__name__)
# @router.post("/plat_pay_channel", response_model=ApiResponse)
# async def plat_pay_channel(request_data: PlatPayRequest):
# """获取微信app端支付参数"""
# # 接收参数
# order_id = request_data.order_id
# jd_order_id = request_data.jd_order_id
# face_price = float(request_data.face_price)
# pay_id = request_data.pay_id
# cookies = request_data.cookies.strip()
# # 打印日志
# logger.info(
# f"获取微信app端支付参数 cookie{cookies} order_id{order_id} face_price{face_price} pay_id{pay_id}"
# )
# try:
# apple_store_spider = AppStoreSpider(
# cookies=cookies,
# order_num=order_id,
# face_price=face_price,
# )
# code, deeplink = await run_in_threadpool(
# apple_store_spider.ios_pay, jd_order_id, pay_id
# )
# data = PlatPayResponseData(
# deeplink=deeplink, order_id=order_id, pay_id=pay_id, face_price=face_price
# )
# return success(code=getattr(code, "value", code), data=data, msg="请求成功")
# except Exception as e:
# logger.error("请求失败" + traceback.format_exc(), exc_info=True)
# raise HTTPException(status_code=500, detail="请求失败")
@router.post("/refresh-payment", response_model=ApiResponse[PlatPayResponseData])
async def refresh_payment(request_data: RefreshPaymentRequest):
"""获取微信app端支付参数"""
# 打印日志
logger.info(
f"获取微信app端支付参数 cookie{request_data.cookies} order_id{request_data.order_id} pay_id{request_data.pay_id} user_order_id{request_data.user_order_id}"
)
# data = {
# "deeplink": "weixin://wap/pay?prepayid%3Dwx11141602253864f3a03c1c654daa820001&package=3012187903&noncestr=1760163364&sign=b8e94df5e28a10e0938d84e322161882",
# "order_id": "339328433580",
# "pay_id": "652e4edb825348e78f692c17ef1bc8b9",
# "face_price": 0
# }
# return my_json(code=100, data=data, msg="请求完成")
res = {}
try:
for i in range(3):
c_trip_spider = XiechengCardSpider(
cookies=request_data.cookies,
order_num=request_data.user_order_id,
sku_id="",
)
code, res = await run_in_threadpool(
c_trip_spider.refresh_payment_url,
request_data.pay_id,
request_data.order_id,
)
if code == BusinessCode.JD_ORDER_RISK_ERR:
continue
if code == BusinessCode.SUCCESS:
return success(data=res)
return error(
code=BusinessCode.JD_ORDER_RISK_ERR,
data=PlatPayResponseData(**{
"deeplink": "",
"order_id": "",
"pay_id": "",
"remark": res.get("remark"),
}),
)
except Exception as e:
logger.error(f"请求失败:{traceback.format_exc()}")
raise HTTPException(status_code=500, detail="请求失败")

110
apps/jd/schemas/models.py Normal file
View File

@@ -0,0 +1,110 @@
from enum import Enum
from pydantic import BaseModel, Field
class AppleStoreRequestCategoryEnum(Enum):
Apple = "apple"
CTrip = "cTrip"
Walmart = "walmart"
class AppStoreRequest(BaseModel):
"""苹果权益充值请求"""
category: AppleStoreRequestCategoryEnum = Field(..., description="分类")
face_price: float = Field(..., description="面值")
order_num: str = Field(..., description="订单号")
cookies: str = Field(..., description="登录cookies")
class QueryCardRequest(BaseModel):
"""查卡密请求"""
category: AppleStoreRequestCategoryEnum = Field(..., description="分类")
order_id: str = Field(..., description="订单号")
jd_order_id: str = Field(..., description="京东订单号")
cookies: str = Field(..., description="登录cookies")
class SkuRequest(BaseModel):
"""获取SKU参数请求"""
cookies: str = Field(..., description="登录cookies")
sku_id: str = Field(..., description="SKU ID")
class GoodsStoreRequest(BaseModel):
"""账号类下单请求"""
face_price: float = Field(..., description="面值")
order_num: str = Field(..., description="订单号")
cookies: str = Field(..., description="登录cookies")
brand_id: str = Field(..., description="品牌ID")
sku_id: str = Field(..., description="SKU ID")
username: str | None = Field(None, description="用户名")
gamesrv: str | None = Field(None, description="游戏服务器")
gamearea: str | None = Field(None, description="游戏区域")
recharge_type: int = Field(
0, description="充值类型 1username账号充值2游戏区服充值"
)
class PlatPayRequest(BaseModel):
"""支付渠道请求"""
order_id: str = Field(..., description="订单ID")
jd_order_id: str = Field(..., description="京东订单ID")
face_price: float = Field(..., description="面值")
pay_id: str = Field(..., description="支付ID")
cookies: str = Field(..., description="登录cookies")
class RefreshPaymentRequest(BaseModel):
"""刷新支付请求"""
user_order_id: str = Field(..., description="用户订单ID")
pay_id: str = Field(..., description="支付ID")
cookies: str = Field(..., description="登录cookies")
order_id: int = Field(..., description="订单ID")
class DeleteOrderRequest(BaseModel):
"""删除订单请求"""
cookie: str = Field(..., description="登录cookies")
order_id: str = Field(..., description="订单ID")
class SmsCodeRequest(BaseModel):
"""发送短信验证码请求"""
phone: str = Field(..., description="手机号")
class SmsLoginRequest(BaseModel):
"""短信登录请求"""
phone: str = Field(..., description="手机号")
ck: str = Field(..., description="cookies")
code: str = Field(..., description="验证码")
s_token: str = Field(..., description="s_token")
jd_risk_token_id: str = Field(..., description="京东风控token ID")
rsa_modulus: str = Field(..., description="RSA模数")
class PlatPayResponseData(BaseModel):
deeplink: str = Field("", description="微信唤起链接或支付信息")
order_id: str = Field("", description="订单号")
remark: str | None = Field(None, description="备注")
pay_id: str = Field("", description="支付ID")
face_price: float = Field(0.0, description="面值")
class QueryCardResponseData(BaseModel):
"""查卡密响应数据"""
order_status: str = Field(..., description="订单状态")
card_num: str = Field(..., description="卡号")
card_pwd: str = Field(..., description="卡密")
remark: str = Field(default="", description="备注")

View File

@@ -0,0 +1,773 @@
import base64
import hashlib
import json
import platform
import random
import re
import time
import traceback
import uuid
from urllib import parse
from curl_cffi import requests
from fake_useragent import UserAgent
from tenacity import retry, stop_after_attempt, wait_exponential
from apps.jd.schemas.models import QueryCardResponseData
from apps.jd.services.jstk import NormalJsTk, TxSMJsTk
from apps.jd.services.utils import gen_cipher_ep, get_pay_sign, get_sign
from apps.shared.proxy_pool.proxy_pool import ProxyPoolFactory
from core.config import ProxyPoolType, settings
from core.responses import BusinessCode
from observability.logging import LoggerAdapter, get_logger_with_trace
#
logger: LoggerAdapter = get_logger_with_trace(__name__)
class AppStoreSpider:
def __init__(
self,
cookies,
order_num,
face_price: float = 0,
user_client: str = UserAgent().random,
):
self.SKU_MA = {
"10.00": 10022039398507,
"50.00": 11170365589,
"100.00": 11183343342,
"200.00": 11183368356,
"500.00": 11183445154,
"1000.00": 10066407336810,
"68.00": 10023403480808,
"00.00": 10026503885976,
}
self.__proxy_pool = ProxyPoolFactory.get_proxy_pool(settings.proxy_type)
self.order_num = order_num
self.cookies = cookies
self.face_price = face_price
self.__user_client = user_client
self.md5_key = "e7c398ffcb2d4824b4d0a703e38eb0bb"
self.time_stamp = int(time.time() * 1000)
self.submit_order_url = "https://api.m.jd.com/appstore/submitorder"
self.ticket_url = "https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkcaptcha"
self.jd_api = "https://api.m.jd.com/api"
self.action_url = "https://api.m.jd.com/client.action"
self.check_captcha_url = (
"https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkcaptcha"
)
self.headers = {
"content-type": "application/x-www-form-urlencoded",
"cookie": self.cookies,
"origin": "https://txsm-m.jd.com",
"referer": "https://txsm-m.jd.com/",
"user-agent": self.get_user_agent(),
}
self.current_os = platform.system()
self.js = None
self.__js_tk = TxSMJsTk()
self.__js_tk.generate_token(self.get_user_agent(), cookies)
expiring_pool = ProxyPoolFactory.get_proxy_pool(
ProxyPoolType.EXPIRING, expire_time=60
)
self.proxy = expiring_pool.get_proxy(order_id=uuid.uuid4().hex)
# self.init_js()
def h5st(self, pt_pin: str, app_id: str, user_agent: str, data: dict):
logger.info(f"h5st: {pt_pin}, {app_id}, {user_agent}, {data}")
response = requests.post(
"http://152.136.211.112:9001/h5st",
json={
"appCode": "Z6eXo8Dl",
"pin": pt_pin,
"ua": user_agent,
"body": data,
"appId": app_id,
},
)
return response.json().get("body", {}).get("h5st", {}).get("h5st", "")
def get_user_agent(self) -> str:
if "iPhone" in self.__user_client:
return "Mozilla/5.0 (iPhone; CPU iPhone OS 17_7_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.4 Mobile/15E148 Safari/604.1"
return f"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/{random.randint(120, 140)}.0.0.0"
def get_eid_token(self):
return self.__js_tk.get_token()
def format_number(self, number):
if not number:
number = 0
number = float(number)
formatted_number = "{:.2f}".format(number)
return formatted_number
def get_s(self):
use_bean = 0
buy_num = 1
face_price = self.face_price
type = 1
brand_id = 999440
eid = self.__js_tk.get_eid()
pay_mode = 0
coupon_ids = ""
sku_id = self.SKU_MA.setdefault(
self.format_number(self.face_price), 10026503885976
)
total_price = self.format_number(self.face_price)
order_source = 2
order_source_type = 2
s = f"{use_bean}{buy_num}{face_price}{type}{brand_id}{eid}{pay_mode}{coupon_ids}{sku_id}{total_price}{order_source}{order_source_type}"
return s
def get_u(self):
u = f"{self.time_stamp}{self.md5_key}"
return u
def get_enc_str(self):
s = self.get_s()
u = self.get_u()
_str = f"{s}{u}"
enc_str = hashlib.md5(_str.encode()).hexdigest()
return enc_str
def get_body(self, enc_str):
item = {
"useBean": 0,
"buyNum": 1,
"facePrice": self.face_price,
"type": 1,
"brandId": 999440,
"eid": self.__js_tk.get_eid(),
"payMode": "0",
"couponIds": "",
"skuId": self.SKU_MA.setdefault(
self.format_number(self.face_price), 10026503885976
),
"totalPrice": self.format_number(self.face_price),
"orderSource": 2,
"orderSourceType": 2,
"t": self.time_stamp,
"channelSource": "txzs",
"encStr": enc_str,
"babelChannel": "ttt35",
}
json_string = json.dumps(item)
encrypted_message = (
base64.b64encode(json_string.encode("utf-8")).decode("utf-8").rstrip("=")
)
return encrypted_message
def submit_order(self, body):
data = {
"appid": "txsm-m",
"client": "iPhone",
"functionId": "appstore_order_submit_new",
"uuid": uuid.uuid4().hex.replace("-", ""),
"osVersion": "16.6",
"screen": "1170.000046491623*2532.0001006126404",
"t": self.time_stamp,
"loginType": "2",
"x-api-eid-token": self.__js_tk.get_eid(),
"body": body,
}
proxy = self.__proxy_pool.get_proxy(order_id=self.order_num)
if proxy:
response = requests.post(
url=self.submit_order_url,
headers=self.headers,
data=data,
proxies={"http": proxy, "https": proxy},
)
else:
response = requests.post(
url=self.submit_order_url, headers=self.headers, data=data
)
return response.json()
@retry(
stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=15)
)
def get_ticket_res(self):
headers = {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
"Accept-Language": "zh-CN,zh;q=0.9",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"Pragma": "no-cache",
"Upgrade-Insecure-Requests": "1",
"User-Agent": self.get_user_agent(),
}
url = "http://ticket_slide_server:99/api/TX?aid=2093769752&host=https://t.captcha.qq.com&ip="
proxy = self.__proxy_pool.get_proxy(order_id=self.order_num)
if proxy:
response = requests.get(
url,
headers=headers,
verify=False,
proxies={"http": proxy, "https": proxy},
)
else:
response = requests.get(url, headers=headers, verify=False)
return response.json()
def decrypt_card_info(self, message):
key = "2E1ZMAF88CCE5EBE551FR3E9AA6FF322"
card_info = self.js.call("decryptDes", message, key)
card_info = json.loads(card_info)
return card_info[0]
def get_card_res(self, order_id: str):
"""
获取订单卡号/卡密详情
:param order_id: 订单号
:return: 卡号/卡密详情
"""
user_agent = self.get_user_agent()
headers = {
"accept": "application/json, text/plain, */*",
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
"content-type": "application/x-www-form-urlencoded",
"origin": "https://recharge.m.jd.com",
"priority": "u=1, i",
"referer": "https://recharge.m.jd.com/",
"user-agent": user_agent,
"x-referer-page": "https://recharge.m.jd.com/orderDetail",
"x-rp-client": "h5_1.0.0",
"cookie": self.cookies,
}
token_service = NormalJsTk()
token_service.generate_token(user_agent, self.cookies)
data = {
"appid": "tsw-m",
"functionId": "getGPOrderDetail",
"t": self.time_stamp,
"body": '{"appKey":"android","source":41,"orderId":"%s","version":"1.10","rechargeversion":"12.8","moduleName":"JDReactVirtualRecharge","apiVersion":"new"}'
% (order_id),
"uuid": "1736139761886772644158",
"screen": "2560.5*1600.5",
"x-api-eid-token": token_service.get_token(),
}
cookie_dict: dict[str, str] = {
cookie.split("=")[0].strip(): cookie.split("=")[1].strip()
for cookie in self.cookies.split(";")
}
h5st = self.h5st(cookie_dict.get("pt_pin", ""), "8e94a", user_agent, data)
data["h5st"] = h5st
response = requests.post(self.jd_api, headers=headers, data=data)
return response.json()
def get_pay_res(self, order_id):
headers = {
"accept": "*/*",
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
"content-type": "application/json;charset=UTF-8",
"origin": "https://trade.m.jd.com",
"priority": "u=1, i",
"referer": "https://trade.m.jd.com/",
"sec-ch-ua": '"Chromium";v="130", "Microsoft Edge";v="130", "Not?A_Brand";v="99"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"Windows"',
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-site",
"user-agent": self.get_user_agent(),
"x-referer-page": "https://trade.m.jd.com/order/orderlist_jdm.shtml",
"x-rp-client": "h5_1.0.0",
"cookie": self.cookies,
}
params = {
"t": f"{self.time_stamp}",
"loginType": "2",
"loginWQBiz": "golden-trade",
"appid": "m_core",
"client": "MacIntel",
"clientVersion": "",
"build": "",
"osVersion": "null",
"screen": "1920*1080",
"networkType": "4g",
"partner": "",
"forcebot": "",
"d_brand": "",
"d_model": "",
"lang": "zh-CN",
"scope": "",
"sdkVersion": "",
"openudid": "",
"uuid": uuid.uuid4().hex.replace("-", ""),
"x-api-eid-token": self.__js_tk.get_token(),
"functionId": "pay_info_m",
"body": '{"appType":3,"bizType":"2","deviceUUId":"","platform":3,"sceneval":"2","source":"m_inner_myJd.orderFloor_orderlist","systemBaseInfo":"{\\"pixelRatio\\":2,\\"screenWidth\\":1920,\\"screenHeight\\":1080,\\"windowWidth\\":1920,\\"windowHeight\\":959,\\"statusBarHeight\\":null,\\"safeArea\\":{\\"bottom\\":0,\\"height\\":0,\\"left\\":0,\\"right\\":0,\\"top\\":0,\\"width\\":0},\\"bluetoothEnabled\\":false,\\"locationEnabled\\":false,\\"wifiEnabled\\":false,\\"deviceOrientation\\":\\"landscape\\",\\"benchmarkLevel\\":-1,\\"brand\\":\\"\\",\\"model\\":\\"\\",\\"system\\":null,\\"platform\\":\\"MacIntel\\",\\"SDKVersion\\":\\"\\",\\"enableDebug\\":false,\\"language\\":\\"zh-CN\\",\\"version\\":\\"\\",\\"theme\\":\\"light\\",\\"fontSizeSetting\\":null,\\"albumAuthorized\\":false,\\"cameraAuthorized\\":false,\\"locationAuthorized\\":false,\\"microphoneAuthorized\\":false,\\"notificationAuthorized\\":false,\\"notificationAlertAuthorized\\":false,\\"notificationBadgeAuthorized\\":false,\\"notificationSoundAuthorized\\":false,\\"phoneCalendarAuthorized\\":false,\\"locationReducedAccuracy\\":false,\\"environment\\":\\"\\"}","orderId":"%s","origin":10,"tenantCode":"jgm","bizModelCode":"2","bizModeClientType":"M","bizModeFramework":"Taro","externalLoginType":1,"token":"3852b12f8c4d869b7ed3e2b3c68c9436","appId":"m91d27dbf599dff74"}'
% order_id,
"h5st": "20250513003554680%3Bg9gwwg93pzmtgwp3%3B9b070%3Btk03wac841c0518nh3UsKG1ttEEV8mnlrHkEhynQ-S2x3PbyeW6t_Cn0S29bQlTaYFu10XQnqBTZqpZVg7yWDGL1-Ien%3B57ceff3efad640ff671169173de4393cea87add0aeafeb28fe45d8ed0885f34d%3B4.2%3B1747067754680%3Be2dbf31f18c0566b03779cdfb7daf1889536ac5980ca54061cc5d12b276f6fcb74aa49e97fb21499ccffae2bb79d16ba8b664ce42ae53fce6b12c709789cf1eeb1e039f7a491fa6c0bb41380e593285cdb5cbaca0dd43586ff7937f44c9f6f0a1104467ba19dc3d8a5081e3bf7da385fee7ad469c515d6cd459c9efc82eb331e899e6c338ec36e3cfcf4466efd3d4bb31f752f3730e1343447f0524cfabc0dd22d9dbc853d384e6f6969ac7eb9339b72b5ff17147816fa422a79b375f28edbb490089ee8dd463b06339ba1536ba3e4ad90d82913ad4bd6049ff583666f24d648bb47c6d2f65380c4bda6ac0eea24fc622fa95763f9352247d21980f0b8dcdc54d7a2d18b84dad43660f88fcfb8c8613d86daa87f58addf1a60f3df6e5626753bf497864d03408578ff78bd5007b1bf86f0747b05681c58b32d27ae7b2de07059",
}
proxy = self.__proxy_pool.get_proxy(order_id=self.order_num)
if proxy:
response = requests.get(
self.action_url,
headers=headers,
params=params,
proxies={"http": proxy, "https": proxy},
)
else:
response = requests.get(self.action_url, headers=headers, params=params)
return response.json()
def plat_pay_channel_res(self, pay_id):
headers = {
"accept": "application/json, text/plain, */*",
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
"content-type": "application/x-www-form-urlencoded",
"origin": "https://mpay.m.jd.com",
"priority": "u=1, i",
"referer": "https://mpay.m.jd.com/",
"sec-ch-ua": '"Chromium";v="130", "Microsoft Edge";v="130", "Not?A_Brand";v="99"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"Windows"',
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-site",
"user-agent": self.get_user_agent(),
"x-referer-page": "https://mpay.m.jd.com/mpay.623f9498223cf9b9de9f.html",
"x-rp-client": "h5_1.0.0",
"cookie": self.cookies,
}
params = {"functionId": "platPayChannel", "appid": "mcashier", "scval": "mpay"}
data = {
"body": '{"appId":"m_D1vmUq63","payId":"%s","source":"mcashier","origin":"h5","mcashierTraceId":1729842386189}'
% (pay_id),
"x-api-eid-token": self.__js_tk.get_token(),
"h5st": "20241025154626341;0587023779148689;303a7;tk03w7da11b8b18nOQ6HZGNLj6DdtLQBS695YHMu7RyONolcwWCRc8ihMUs5ITCem6HIGhdYo_DpJ62yYLkdrxIBxE0N;ab8a935407baae929b0d3e267f67693f9c00a5d876ad149c10e904371f87726f;3.1;1729842386341;24c9ee85e67cf80746dd82817ecbeafc7a829b35c7f446a4c7d476cc9faa1d8834a93323ad7bce9bef1bba682b93d2e3694e425ff68d304875c1ae9e2ae398cfd94e4ff03cd3bdd9f0f600a0d75c92d537baaa944d39072a92db7dc20c99e7f80889e289e78a1f8f93c57f8471890c464b78b61e9b3bbffea712e6d6c671ad12",
}
proxy = self.__proxy_pool.get_proxy(order_id=self.order_num)
if proxy:
response = requests.post(
self.action_url,
headers=headers,
params=params,
data=data,
proxies={"http": proxy, "https": proxy},
)
else:
response = requests.post(
self.action_url, headers=headers, params=params, data=data
)
return response.json()
def plat_wx_pay_res(self, pay_id):
headers = {
"accept": "application/json, text/plain, */*",
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
"content-type": "application/x-www-form-urlencoded",
"origin": "https://mpay.m.jd.com",
"priority": "u=1, i",
"referer": "https://mpay.m.jd.com/",
"sec-ch-ua": '"Chromium";v="130", "Microsoft Edge";v="130", "Not?A_Brand";v="99"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"Windows"',
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-site",
"user-agent": self.get_user_agent(),
"x-referer-page": "https://mpay.m.jd.com/mpay.623f9498223cf9b9de9f.html",
"x-rp-client": "h5_1.0.0",
"cookie": self.cookies,
}
params = {"functionId": "platWapWXPay", "appid": "mcashier", "scval": "mpay"}
data = {
"body": '{"appId":"m_D1vmUq63","payId":"%s","eid":"PBZHV4O4RF5SAA7QGYPZEPRPAYOCCF3WTUQYMWEFASLCJNYX2HWO7C35L5TYQUL66FGXVVMXDWKTBEEE24LW42XEWM","source":"mcashier","origin":"h5","mcashierTraceId":1729837716957}'
% pay_id,
"x-api-eid-token": "jdd03PBZHV4O4RF5SAA7QGYPZEPRPAYOCCF3WTUQYMWEFASLCJNYX2HWO7C35L5TYQUL66FGXVVMXDWKTBEEE24LW42XEWMAAAAMSYJMLE3IAAAAAC5OTMMGGUM5SYIX",
# "h5st": "20241025142845015;0587023779148689;303a7;tk03w7da11b8b18nOQ6HZGNLj6DdtLQBS695YHMu7RyONolcwWCRc8ihMUs5ITCem6HIGhdYo_DpJ62yYLkdrxIBxE0N;48aee025d65fab059f98c546832f7a1d7bca99dba6bc0df9bd6328b9817cf394;3.1;1729837725015;24c9ee85e67cf80746dd82817ecbeafc7a829b35c7f446a4c7d476cc9faa1d8834a93323ad7bce9bef1bba682b93d2e3694e425ff68d304875c1ae9e2ae398cfd94e4ff03cd3bdd9f0f600a0d75c92d537baaa944d39072a92db7dc20c99e7f80889e289e78a1f8f93c57f8471890c464b78b61e9b3bbffea712e6d6c671ad12"
}
proxy = self.__proxy_pool.get_proxy(order_id=self.order_num)
if proxy:
response = requests.post(
self.action_url,
headers=headers,
params=params,
data=data,
proxies={"http": proxy, "https": proxy},
)
else:
response = requests.post(
self.action_url, headers=headers, params=params, data=data
)
return response.json()
def get_deep_link_res(self, mweb_url):
headers = {
"Host": "wx.tenpay.com",
"sec-ch-ua-platform": '"Windows"',
"User-Agent": self.get_user_agent(),
"sec-ch-ua": '"Chromium";v="130", "Microsoft Edge";v="130", "Not?A_Brand";v="99"',
"sec-ch-ua-mobile": "?0",
"Accept": "*/*",
"Sec-Fetch-Site": "same-origin",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Dest": "empty",
"Referer": mweb_url,
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
}
prepay_id = re.search(r"prepay_id=(wx\w+)", mweb_url).group(1)
package = re.search(r"package=(\d+)", mweb_url).group(1)
ticket_res = self.get_ticket_res()
logger.info(f"订单号:{self.order_num}获取ticket返回{ticket_res}")
ticket = ticket_res["ticket"]
randstr = ticket_res["randstr"]
params = {
"ticket": ticket,
"randstr": randstr,
"prepayid": prepay_id,
"package": package,
}
proxy = self.__proxy_pool.get_proxy(order_id=self.order_num)
if proxy:
response = requests.get(
self.check_captcha_url,
headers=headers,
params=params,
proxies={"http": proxy, "https": proxy},
)
else:
response = requests.get(
self.check_captcha_url, headers=headers, params=params
)
return response.json()
def get_card_secret(self, jd_order_num):
card_res = self.get_card_res(jd_order_num)
logger.info(f"获取卡密信息返回:{card_res}")
if not card_res:
return 110, card_res
if card_res.get("code") != "0":
return 110, card_res
card_info = card_res.get("result").get("cardInfos")
if not card_info:
return 110, card_res
card_info = self.decrypt_card_info(card_info)
return 100, card_info
def query_card(self, jd_order_num):
"""
查询订单状态和卡密信息
:param jd_order_num: 订单号
:return: code=100成功包含order_status, card_num, card_pwd字段
"""
try:
card_res = self.get_card_res(jd_order_num)
logger.info(f"获取卡密信息返回:{card_res}")
if not card_res:
return BusinessCode.JD_ORDER_NORMAL_ERR, QueryCardResponseData(
order_status="", card_num="", card_pwd="", remark=str(card_res)
)
# 判断没有登录的场景
if card_res.get("code") == "20001":
return BusinessCode.JD_ORDER_CK_ERR, QueryCardResponseData(
order_status="", card_num="", card_pwd="", remark=str(card_res)
)
if card_res.get("code") != "0":
return BusinessCode.JD_ORDER_NORMAL_ERR, QueryCardResponseData(
order_status="", card_num="", card_pwd="", remark=str(card_res)
)
card_info_list = card_res.get("result", {}).get("cardInfos", [])
if not card_info_list:
return BusinessCode.JD_ORDER_NORMAL_ERR, QueryCardResponseData(
order_status="获取失败",
card_num="",
card_pwd="",
remark=str(card_res),
)
try:
card_info = self.decrypt_card_info(card_info_list)
return BusinessCode.SUCCESS, QueryCardResponseData(
order_status=card_info.get("status", "成功"),
card_num=card_info.get("cardNo", ""),
card_pwd=card_info.get("cardPwd", ""),
remark=str(card_res),
)
except Exception as e:
logger.error(f"解密卡密信息失败: {e}")
return BusinessCode.JD_ORDER_NORMAL_ERR, QueryCardResponseData(
order_status="解密失败", card_num="", card_pwd=""
)
except Exception as e:
logger.error(f"查询卡密信息异常: {e}")
return BusinessCode.JD_ORDER_NORMAL_ERR, QueryCardResponseData(
order_status="查询失败", card_num="", card_pwd=""
)
def run(self) -> tuple[BusinessCode, dict, str]:
# 获取加密参数
try:
enc_str = self.get_enc_str()
except KeyError as e:
return BusinessCode.JD_ORDER_FACE_PRICE_ERR, {}, "充值面值有误"
# 获取请求body
body = self.get_body(enc_str)
# 提交预付款订单
order_res = self.submit_order(body)
# 提交预付款订单
logger.info(
f"订单号:{self.order_num}app_store提交预付款订单返回{order_res}"
)
if order_res.get("code") == 300:
return BusinessCode.JD_ORDER_CK_ERR, {}, str(order_res)
if order_res.get("code") != 200:
return BusinessCode.JD_ORDER_NORMAL_ERR, {}, str(order_res)
# 提交预付款订单
# # 获取支付信息
pay_res = self.get_pay_res(order_res["data"])
logger.info(f"订单号:{self.order_num}app_store获取支付信息返回{pay_res}")
# pay_res = {'body': {'payId': '0d02ea36cf7946a89ed6f51898d2d2a3', 'url': 'https://mpay.m.jd.com/mpay.25f73d1adb414ba7e0e3.html?appId=m_D1vmUq63&payId=0d02ea36cf7946a89ed6f51898d2d2a3&orderId=314518542769&tId=mpay'}, 'code': '0', 'message': 'success', 'timestamp': 1747127287841}
if pay_res.get("code") != "0":
return BusinessCode.JD_ORDER_NORMAL_ERR, {}, str(pay_res)
# 获取支付信息
pay_id = pay_res["body"]["payId"]
# 获取微信支付信息
pay_channel_res = self.plat_pay_channel_res(pay_id)
logger.info(
f"订单号:{self.order_num}app_store请求微信渠道返回{pay_channel_res}"
)
if pay_channel_res.get("code") != "0":
return BusinessCode.JD_ORDER_NORMAL_ERR, {}, str(pay_channel_res)
if "iPhone" in self.get_user_agent():
code, msg = self.ios_pay(pay_channel_res["orderId"], pay_id)
data = {}
else:
code, data, msg = self.web_pay(pay_id)
if code != BusinessCode.SUCCESS:
return code, data, msg
return (
code,
{
"deeplink": msg,
"order_id": pay_channel_res["orderId"],
"pay_id": pay_id,
"face_price": self.face_price,
},
"请求成功",
)
def web_pay(self, pay_id: str) -> tuple[BusinessCode, dict, str]:
# 获取微信支付信息
wx_pay_res = self.plat_wx_pay_res(pay_id)
logger.info(
f"订单号:{self.order_num}app_store获取微信支付信息返回{wx_pay_res}"
)
if wx_pay_res.get("code") != "0":
return BusinessCode.JD_ORDER_NORMAL_ERR, {}, str(wx_pay_res)
if wx_pay_res.get("errorCode") == "-1":
wx_pay_res["order_id"] = self.order_num
wx_pay_res["pay_id"] = pay_id
wx_pay_res["face_price"] = self.face_price
return BusinessCode.JD_ORDER_NORMAL_ERR, {}, str(wx_pay_res)
mweb_url = wx_pay_res["payInfo"]["mweb_url"]
# 获取支付链接信息
deep_link_res = self.get_deep_link_res(mweb_url)
logger.info(
f"订单号:{self.order_num}app_store获取支付链接deep_link信息返回{deep_link_res}"
)
if deep_link_res.get("retcode") != 1:
return BusinessCode.JD_ORDER_NORMAL_ERR, {}, str(deep_link_res)
return BusinessCode.SUCCESS, {}, deep_link_res["deeplink"]
def ios_pay(self, order_id: str, pay_id: str) -> tuple[BusinessCode, str]:
headers = {
"Host": "api.m.jd.com",
"charset": "UTF-8",
"user-agent": "okhttp/3.12.1;jdmall;android;version/11.1.0;build/98139",
"cache-control": "no-cache",
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
"cookie": self.cookies,
}
url = "https://api.m.jd.com/client.action"
func = "platPayChannel"
version = "11.1.0"
uuid_ = uuid.uuid4().hex.replace("-", "")
ts = int(time.time() * 1000)
ep = gen_cipher_ep(uuid_, ts)
pay_sign = get_pay_sign(order_id=order_id, face_price=self.face_price)
body = (
'{"appId":"jd_android_app4","client":"android","fk_aid":"%s","fk_appId":"com.jingdong.app.mall","fk_latitude":"NfUaIOrNMes=","fk_longtitude":"NfUaIOrNMes=","fk_terminalType":"02","fk_traceIp":"26.26.26.1","hasCyberMoneyPay":"0","hasHuaweiPay":"0","hasOCPay":"0","hasUPPay":"0","orderId":"%s","orderPrice":"%s","orderType":"37","orderTypeCode":"0","origin":"native","payId":"%s","paySign":"%s","paySourceId":"2","payablePrice":"%s","sdkToken":"jdd016CSEHGQ3IPOGEXVOBUPDLTBSKZZUMESDDOP4PPRC2E2R7CC2K4LXQN63E6A3P3Y76GN4M5TMPIZGOWYQJG4MO6ET75VQFVQL2MIYRFQ01234567","source":"jdapp","style":"normal","supportNFC":"1"}'
% (
uuid_,
order_id,
self.face_price,
pay_id,
pay_sign,
self.face_price,
)
)
formatted_params = get_sign(func, body, uuid_, version)
params = {
"functionId": func,
"clientVersion": version,
"build": "98139",
"client": "android",
"partner": "wandoujia",
# "eid": "eidAf760812200s2xOZPVA0sThGrvhABq1zrPMTmUOM1tv8FFg1FjE8yRJtYdV/UFhJuIkVZbrk/xl XPeoFQTNgMiYJpXeeyACCVPbt0/3R7R3Gd 8y",
"sdkVersion": "28",
"lang": "zh_CN",
"harmonyOs": "0",
"networkType": "wifi",
# "uts": "0f31TVRjBSsqndu4/jgUPz6uymy50MQJ8fZr7wet7pLPYx9jEXMpd8VCD64sq/eBbsH5zZBXnKZMkN1vxnjOrpfx7GiQBINsuAELLpjOiZsCHkTDoRW/d9talOxyn2bo1YZLq8uq5Kdx/Fd7diA023Qwr+5V5TzeOnca3cc6QzMFh8p+DS2gR6Bimgz0BiNqbeN1q1NA9rJ/t5QOAIH9EA==",
"uemps": "0-0",
"ext": '{"prstate":"0","pvcStu":"1"}',
"ef": "1",
"ep": ep,
"st": formatted_params["st"],
"sign": formatted_params["sign"],
"sv": formatted_params["sv"],
}
data = {"body": body, "": ""}
proxy = self.__proxy_pool.get_proxy(order_id=self.order_num)
if proxy:
response = requests.post(
url=url,
params=params,
headers=headers,
data=data,
proxies={"http": proxy, "https": proxy},
)
else:
response = requests.post(url=url, params=params, headers=headers, data=data)
client_res = response.json()
logger.info(f"订单id{self.order_num}获取支付参数res返回{client_res}")
# 不支持该支付类型
if client_res.get("errorCode") == "-2":
return BusinessCode.JD_ORDER_TYPE_NOT_SUPPORTED_ERR, "订单类型不支持"
if client_res.get("errorCode") == "3":
return BusinessCode.JD_ORDER_CK_ERR, "ck失效"
# 订单失效
if client_res.get("errorCode") == "-3":
return BusinessCode.JD_ORDER_EXPIRED_ERR, "订单失效"
# 订单与下单账号不同
if client_res.get("errorCode") == "-5":
return BusinessCode.JD_ORDER_NORMAL_ERR, "订单与下单账号不匹配"
# 订单已取消
if client_res.get("errorCode") == "-100":
return BusinessCode.JD_ORDER_EXPIRED_ERR, "订单已支付完成或已取消"
# 订单已支付完成或已取消
if client_res.get("mcashierConfirmInfo", {}).get("errorCode", "") == "-100":
return BusinessCode.JD_ORDER_EXPIRED_ERR, "订单已支付完成或已取消"
# 获取微信支付参数
url = "https://api.m.jd.com/client.action"
headers = {
"Host": "api.m.jd.com",
"charset": "UTF-8",
"user-agent": self.get_user_agent(),
"cache-control": "no-cache",
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
"cookie": self.cookies,
}
uuid_ = uuid.uuid4().hex.replace("-", "")
ts = int(time.time() * 1000)
ep = gen_cipher_ep(uuid_, ts)
pay_sign = get_pay_sign(order_id=order_id, face_price=self.face_price)
sdk_token = "jdd01XYE6SS5ZXXG7F74OZC3PKG5LWF7W3GTVLTSUH56A2YAUXOT6NEEX4TRT3I3XDDYO7NQSIV4BSK3XGKWMROTBRKO4ENETDD4IEHFWZHA01234567"
body = (
'{"appId":"jd_android_app4","backUrl":"","client":"android","orderId":"%s","orderPrice":"%s","orderType":"37","orderTypeCode":"0","origin":"native","payId":"%s","paySign":"%s","sdkToken":"%s","source":"jdapp"}'
% (order_id, self.face_price, pay_id, pay_sign, sdk_token)
)
func = "platWXPay"
version = "11.0.0"
formatted_params = get_sign(func, body, uuid_, version)
params = {
"functionId": "platWXPay",
"clientVersion": "11.0.0",
# "build": "97235",
"client": "android",
"partner": "huawei",
# "eid": "eidAd2c08121b3s9eOeub59TR2G1LS3GtRpmFHjnr/5tBkXKUdbZOahaF5ejedAIDQFfwVV2GDYUzftxnEQRjdhD5bpPNR4B0qK9tPJ 5liDjiHAhDR3",
"sdkVersion": "28",
"lang": "zh_CN",
"harmonyOs": "0",
"networkType": "wifi",
# "uts": "0f31TVRjBSsqndu4/jgUPz6uymy50MQJVJk5AARu8sQVESbyLWRZZNMf5XbN+023PgE2PL4I0aLSvISDbN3u1a1oIB1KTvzrqacX+46wkkSjXxvvYKogR9YCbfnMf2pdC5H/VDcJc2u6uHTxPVwvhLoJ8vX8cN45ZijAbikou9B5o2KTvMTzCfrSYgi3+mls/cA+6k+Ao5sFIIdtimZ6bw==",
"uemps": "0-0",
"ext": '{"prstate":"0","pvcStu":"1"}',
"ef": "1",
"ep": ep,
"st": formatted_params["st"],
"sign": formatted_params["sign"],
"sv": formatted_params["sv"],
}
data = {
"body": body,
}
pay_channel_res = None
for i in range(3):
try:
proxy = self.__proxy_pool.get_proxy(order_id=self.order_num)
if proxy:
response = requests.post(
url=url,
params=params,
headers=headers,
data=data,
proxies={"http": proxy, "https": proxy},
timeout=2,
)
else:
response = requests.post(
url=url, params=params, headers=headers, data=data, timeout=2
)
pay_channel_res = response.json()
logger.info(
f"订单id{self.order_num},获取微信支付参数返回:{pay_channel_res}"
)
break
except Exception as e:
logger.error(traceback.format_exc())
continue
if not pay_channel_res:
return BusinessCode.JD_ORDER_NORMAL_ERR, "爬虫异常"
# 当前支付方式不可用,建议选择其他支付方式
if pay_channel_res.get("errorCode") == "-1":
return BusinessCode.INTERNAL_ERROR, "爬虫异常"
# 加密算法失效
if pay_channel_res.get("code") == "600":
return BusinessCode.INTERNAL_ERROR, "加密算法异常"
# 不支持该支付类型
if pay_channel_res.get("errorCode") == "-2":
return BusinessCode.JD_ORDER_TYPE_NOT_SUPPORTED_ERR, "订单类型不支持"
# ck失效
if pay_channel_res.get("errorCode") == "3":
return BusinessCode.JD_ORDER_CK_ERR, "ck失效"
# 订单失效
if pay_channel_res.get("errorCode") == "-3":
return BusinessCode.JD_ORDER_EXPIRED_ERR, "订单失效"
# 订单与下单账号不同
if pay_channel_res.get("errorCode") == "-5":
return BusinessCode.JD_ORDER_NORMAL_ERR, "订单与下单账号不匹配"
# 订单已取消
if pay_channel_res.get("errorCode") == "-100":
return BusinessCode.JD_ORDER_EXPIRED_ERR, "订单已支付完成或已取消"
# 订单已支付完成或已取消
if (
pay_channel_res.get("mcashierConfirmInfo", {}).get("errorCode", "")
== "-100"
):
return BusinessCode.JD_ORDER_EXPIRED_ERR, "订单已支付完成或已取消"
# 获取数据
pay_info = pay_channel_res["payInfo"]
# url转码
wx_pay_info: str = (
f"weixin://app/wxe75a2e68877315fb/pay/?nonceStr={pay_info['nonceStr']}&package={parse.quote(pay_info['package'])}&partnerId={pay_info['partnerId']}&prepayId={pay_info['prepayId']}&timeStamp={pay_info['timeStamp']}&sign={pay_info['sign']}&timestamp={pay_info['timeStamp']}"
)
logger.info(f"获取微信app端支付参数返回{wx_pay_info}")
return BusinessCode.SUCCESS, wx_pay_info

817
apps/jd/services/ctrip.py Normal file
View File

@@ -0,0 +1,817 @@
import hashlib
import json
from logging import LoggerAdapter
import platform
import re
import time
import fake_useragent
from curl_cffi import ProxySpec, requests
from apps.jd.schemas.models import QueryCardResponseData
from apps.shared.proxy_pool.proxy_pool import ProxyPoolFactory
from core.config import ProxyPoolType
from core.exceptions import JDServiceException
from core.responses import BusinessCode
from observability.logging import get_logger_with_trace
logger: LoggerAdapter = get_logger_with_trace(__name__)
class XiechengCardSpider:
"""
携程
100元10140177420168
200元10148161391225
300元10148163028307
500元10148178960836
1000元10148179392280
"""
def __init__(self, cookies, order_num, sku_id):
self.eid = None
self.x_token = None
self._session = requests.Session()
self._order_num = order_num
self.cookies = cookies
self.time_stamp = int(time.time() * 1000)
self.current_os = platform.system()
self.sku_id = sku_id
self.submit_order_url = "https://api.m.jd.com/appstore/submitorder"
self.ticket_url = "https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkcaptcha"
self.jd_api = "https://api.m.jd.com/api"
self.action_url = "https://api.m.jd.com/client.action"
self.check_captcha_url = (
"https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkcaptcha"
)
self._expiring_pool = ProxyPoolFactory.get_proxy_pool(
ProxyPoolType.EXPIRING, expire_time=60
)
self.user_agent = fake_useragent.FakeUserAgent().chrome
def _get_proxy(self):
proxy = self._expiring_pool.get_proxy(order_id=self._order_num)
return ProxySpec(all=proxy) if proxy else None
# def get_ticket_res(self):
# for i in range(1):
# try:
# headers = {
# "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
# "Accept-Language": "zh-CN,zh;q=0.9",
# "Cache-Control": "no-cache",
# "Connection": "keep-alive",
# "Pragma": "no-cache",
# "Upgrade-Insecure-Requests": "1",
# "User-Agent": self.user_agent,
# }
# url = f"http://119.23.175.61:8877/captcha/tx?aid=2093769752&ip={self.proxy}&host=https://t.captcha.qq.com"
# logger.info(f"获取ticket请求代理{self.proxy}")
# logger.info(f"获取ticket请求{url}")
# response = self._session.get(
# url, headers=headers, verify=False, timeout=60
# )
# logger.info(f"获取ticket结果{response.text}")
# if response.json().get("msg_code") != 200:
# continue
# if response.json().get("result", {}).get(
# "ticket"
# ) and response.json().get("result", {}).get("randstr"):
# return {
# "ticket": response.json().get("result", {}).get("ticket"),
# "randstr": response.json().get("result", {}).get("randstr"),
# }
# except Exception as e:
# logger.error(e)
# raise Exception("获取ticket失败")
def get_pay_res(self, order_id):
headers = {
"accept": "*/*",
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
"content-type": "application/json;charset=UTF-8",
"origin": "https://trade.m.jd.com",
"priority": "u=1, i",
"referer": "https://trade.m.jd.com/",
"sec-ch-ua": '"Chromium";v="130", "Microsoft Edge";v="130", "Not?A_Brand";v="99"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"Windows"',
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-site",
"user-agent": self.user_agent,
"x-referer-page": "https://trade.m.jd.com/order/orderlist_jdm.shtml",
"x-rp-client": "h5_1.0.0",
"cookie": self.cookies,
}
body = (
'{"appType":3,"bizType":"2","deviceUUId":"","platform":3,"sceneval":"2","source":"m_inner_myJd.orderFloor_orderlist","systemBaseInfo":"{\\"pixelRatio\\":1.25,\\"screenWidth\\":2048,\\"screenHeight\\":1152,\\"windowWidth\\":414,\\"windowHeight\\":1034,\\"statusBarHeight\\":null,\\"safeArea\\":{\\"bottom\\":0,\\"height\\":0,\\"left\\":0,\\"right\\":0,\\"top\\":0,\\"width\\":0},\\"bluetoothEnabled\\":false,\\"locationEnabled\\":false,\\"wifiEnabled\\":false,\\"deviceOrientation\\":\\"landscape\\",\\"benchmarkLevel\\":-1,\\"brand\\":\\"\\",\\"model\\":\\"\\",\\"system\\":null,\\"platform\\":\\"Win32\\",\\"SDKVersion\\":\\"\\",\\"enableDebug\\":false,\\"language\\":\\"zh-CN\\",\\"version\\":\\"\\",\\"theme\\":\\"light\\",\\"fontSizeSetting\\":null,\\"albumAuthorized\\":false,\\"cameraAuthorized\\":false,\\"locationAuthorized\\":false,\\"microphoneAuthorized\\":false,\\"notificationAuthorized\\":false,\\"notificationAlertAuthorized\\":false,\\"notificationBadgeAuthorized\\":false,\\"notificationSoundAuthorized\\":false,\\"phoneCalendarAuthorized\\":false,\\"locationReducedAccuracy\\":false,\\"environment\\":\\"\\"}","orderId":"%s","origin":10,"tenantCode":"jgm","bizModelCode":"2","bizModeClientType":"M","bizModeFramework":"Taro","externalLoginType":1,"token":"3852b12f8c4d869b7ed3e2b3c68c9436","appId":"m91d27dbf599dff74"}'
% order_id
)
encrypt_body = self.sha256_hash(body)
timestamp = f"{int(time.time() * 1000)}"
object_id = "9b070"
function_id = "pay_info_m"
h5st = self.get_h5st(timestamp, encrypt_body, object_id, function_id)
params = {
"t": f"{self.time_stamp}",
"loginType": "2",
"loginWQBiz": "golden-trade",
"appid": "m_core",
"client": "Win32",
"clientVersion": "",
"build": "",
"osVersion": "null",
"screen": "2048*1152",
"networkType": "4g",
"partner": "",
"forcebot": "",
"d_brand": "",
"d_model": "",
"lang": "zh-CN",
"scope": "",
"sdkVersion": "",
"openudid": "",
"uuid": "17295878824571442265187",
"x-api-eid-token": self.x_token,
"functionId": "pay_info_m",
"body": body,
"h5st": h5st,
}
proxy = self._get_proxy()
response = self._session.get(
self.action_url,
headers=headers,
params=params,
verify=False,
proxies=proxy,
)
return response.json()
def plat_pay_channel_res(self, pay_id):
headers = {
"accept": "application/json, text/plain, */*",
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
"content-type": "application/x-www-form-urlencoded",
"origin": "https://mpay.m.jd.com",
"priority": "u=1, i",
"referer": "https://mpay.m.jd.com/",
"sec-ch-ua": '"Chromium";v="130", "Microsoft Edge";v="130", "Not?A_Brand";v="99"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"Windows"',
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-site",
"user-agent": self.user_agent,
"x-referer-page": "https://mpay.m.jd.com/mpay.623f9498223cf9b9de9f.html",
"x-rp-client": "h5_1.0.0",
"cookie": self.cookies,
}
params = {"functionId": "platPayChannel", "appid": "mcashier", "scval": "mpay"}
body = (
'{"appId":"m_D1vmUq63","payId":"%s","source":"mcashier","origin":"h5","mcashierTraceId":1729842386189}'
% pay_id
)
encrypt_body = self.sha256_hash(body)
timestamp = f"{int(time.time() * 1000)}"
object_id = "303a7"
function_id = "plat_pay_channel"
proxy = self._get_proxy()
h5st = self.get_h5st(timestamp, encrypt_body, object_id, function_id)
data = {"body": body, "x-api-eid-token": self.x_token, "h5st": h5st}
response = self._session.post(
self.action_url,
headers=headers,
params=params,
data=data,
verify=False,
proxies=proxy,
)
return response.json()
def get_locdetails(self, order_id):
url = "https://api.m.jd.com/"
data = {
"appid": "loc",
"loginType": "2",
"cthr": "1",
"loginWQBiz": "locdetails",
"t": f"{int(time.time() * 1000)}",
"functionId": "queryLocOrderDetail",
"body": '{"version":"1.0.0","source":"","requestId":1757908327795,"orderId":"%s","oldHttp2Color":true}'
% order_id,
}
headers = {
"accept": "application/json, text/plain, */*",
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
"cache-control": "no-cache",
"content-type": "application/x-www-form-urlencoded",
"origin": "https://locdetails.jd.com",
"pragma": "no-cache",
"priority": "u=1, i",
"referer": "https://locdetails.jd.com/",
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-site",
"user-agent": self.user_agent,
"x-referer-page": "https://locdetails.jd.com/h5/index.html",
"x-rp-client": "h5_1.0.0",
"cookie": self.cookies,
}
# proxy = self._get_proxy()
response = self._session.post(
url,
headers=headers,
data=data,
verify=False,
# proxies={
# "http": proxy,
# "https": proxy,
# },
)
res = response.json()
if res.get("code") == 20001:
return BusinessCode.JD_ORDER_CK_ERR, QueryCardResponseData(
order_status="",
card_num="",
card_pwd="",
remark=str(res),
)
if res and res.get("code") == 0:
logger.info(f"请求结果 {response.json()}")
data = res.get("data", {})
order_status_data = data.get("orderStatusData", {})
shop_sku_info_list = data.get("shopSkuInfoList", [])
card_num = ""
card_pwd = ""
if shop_sku_info_list and len(shop_sku_info_list) > 0:
sku_infos = shop_sku_info_list[0].get("skuInfos", [])
if sku_infos and len(sku_infos) > 0:
code_info = sku_infos[0].get("codeInfo", [])
if code_info and len(code_info) > 0:
card_num = code_info[0].get("cardNum", "")
card_pwd = code_info[0].get("pwdNum", "")
return BusinessCode.SUCCESS, QueryCardResponseData(
order_status=order_status_data.get("statusName", ""),
card_num=card_num,
card_pwd=card_pwd,
)
else:
return BusinessCode.JD_ORDER_NORMAL_ERR, QueryCardResponseData(
order_status="", card_num="", card_pwd="", remark=str(res)
)
def plat_wx_pay_res(self, pay_id):
headers = {
"accept": "application/json, text/plain, */*",
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
"content-type": "application/x-www-form-urlencoded",
"origin": "https://mpay.m.jd.com",
"priority": "u=1, i",
"referer": "https://mpay.m.jd.com/",
"sec-ch-ua": '"Chromium";v="130", "Microsoft Edge";v="130", "Not?A_Brand";v="99"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"Windows"',
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-site",
"user-agent": self.user_agent,
"x-referer-page": "https://mpay.m.jd.com/mpay.623f9498223cf9b9de9f.html",
"x-rp-client": "h5_1.0.0",
"cookie": self.cookies,
}
params = {"functionId": "platWapWXPay", "appid": "mcashier", "scval": "mpay"}
body = (
'{"appId":"m_D1vmUq63","payId":"%s","eid":"%s","source":"mcashier","origin":"h5","mcashierTraceId":1729837716957}'
% (pay_id, self.eid)
)
encrypt_body = self.sha256_hash(body)
timestamp = f"{int(time.time() * 1000)}"
object_id = "303a7"
function_id = "plat_wx_pay"
h5st = self.get_h5st(timestamp, encrypt_body, object_id, function_id)
data = {"body": body, "x-api-eid-token": self.x_token, "h5st": h5st}
proxy = self._get_proxy()
response = self._session.post(
self.action_url,
headers=headers,
params=params,
data=data,
verify=False,
proxies=proxy,
)
return response.json()
# def get_deep_link_res(self, mweb_url):
# headers = {
# "Host": "wx.tenpay.com",
# "sec-ch-ua-platform": '"Windows"',
# "User-Agent": self.user_agent,
# "sec-ch-ua": '"Chromium";v="130", "Microsoft Edge";v="130", "Not?A_Brand";v="99"',
# "sec-ch-ua-mobile": "?0",
# "Accept": "*/*",
# "Sec-Fetch-Site": "same-origin",
# "Sec-Fetch-Mode": "cors",
# "Sec-Fetch-Dest": "empty",
# "Referer": mweb_url,
# "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
# }
# prepay_id = re.search(r"prepay_id=(wx\w+)", mweb_url).group(1)
# package = re.search(r"package=(\d+)", mweb_url).group(1)
# ticket_res = self.get_ticket_res()
# logger.info(f"订单号:{self._order_num}获取ticket返回{ticket_res}")
# ticket = ticket_res["ticket"]
# randstr = ticket_res["randstr"]
# params = {
# "ticket": ticket,
# "randstr": randstr,
# "prepayid": prepay_id,
# "package": package,
# }
# proxy = self._get_proxy()
# response = self._session.get(
# self.check_captcha_url,
# headers=headers,
# params=params,
# verify=False,
# proxies=proxy,
# )
# return response.json()
@staticmethod
def sha256_hash(text):
# 创建 SHA-256 哈希对象
sha256 = hashlib.sha256()
# 更新哈希对象(输入必须为字节类型)
sha256.update(text.encode("utf-8"))
# 返回 16 进制格式的哈希值
return sha256.hexdigest()
def _get_pt_pin(self, cookie_str):
cookies = {}
for item in cookie_str.strip(";").split(";"):
if "=" in item:
key, value = item.strip().split("=", 1)
cookies[key] = value
pt_pin = cookies.get("pt_pin")
return pt_pin
def get_h5st(self, timestamp, body, object_id, function_id):
sua = "Windows NT 10.0; Win64; x64"
pt_pin = self._get_pt_pin(self.cookies)
params_str = f"ai={object_id}&sua={sua}&pin={pt_pin}&appid=m_core&functionId={function_id}&body={body}&client=Win32&clientVersion=2.5.2&t={timestamp}"
h5st = requests.get(f"http://127.0.0.1:8887/jd/h5st?{params_str}").text
return h5st
def get_x_token(self):
proxy = None
_proxy = self._get_proxy()
if _proxy:
proxy = _proxy.get("all")
data = {"type": "1", "str": self.user_agent}
if proxy:
data["proxy"] = proxy
response = requests.post(
"http://127.0.0.1:8887/api/stash/algorithm",
data=data,
)
logger.info(f"获取x-api-eid-token返回{response.text}")
res = response.json()
token = res["data"]["token"]
eid = res["data"]["eid"]
return token, eid
def get_current_order(self, retry_count: int = 3):
if not retry_count:
return JDServiceException(BusinessCode.JD_ORDER_RISK_ERR)
body = (
'{"deviceUUID":"6785593751540242498","appId":"wxae3e8056daea8727","appVersion":"2.5.2","tenantCode":"jgm","bizModelCode":"3","bizModeClientType":"M","token":"3852b12f8c4d869b7ed3e2b3c68c9436","externalLoginType":1,"referer":"https://item.m.jd.com/","resetGsd":true,"useBestCoupon":"1","locationId":"1-72-2819-0","packageStyle":true,"sceneval":"2","balanceCommonOrderForm":{"supportTransport":false,"action":1,"overseaMerge":false,"international":false,"netBuySourceType":0,"appVersion":"2.5.2","tradeShort":false},"balanceDeviceInfo":{"resolution":"2048*1152"},"cartParam":{"skuItem":{"skuId":"%s","num":"1","orderCashBack":false,"extFlag":{}}}}'
% self.sku_id
)
encrypt_body = self.sha256_hash(body)
timestamp = f"{int(time.time() * 1000)}"
object_id = "bd265"
function_id = "balance_getCurrentOrder_m"
h5st = self.get_h5st(timestamp, encrypt_body, object_id, function_id)
headers = {
"accept": "*/*",
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
"cache-control": "no-cache",
"content-type": "application/x-www-form-urlencoded",
"origin": "https://trade.m.jd.com",
"pragma": "no-cache",
"priority": "u=1, i",
"referer": "https://trade.m.jd.com/",
"sec-ch-ua": '"Chromium";v="140", "Not=A?Brand";v="24", "Microsoft Edge";v="140"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"Windows"',
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-site",
"user-agent": self.user_agent,
"x-referer-page": "https://trade.m.jd.com/pay",
"x-rp-client": "h5_1.0.0",
"cookie": self.cookies,
}
url = "https://api.m.jd.com/client.action"
data = {
"t": timestamp,
"body": body,
"h5st": h5st,
"scval": self.sku_id,
"client": "Win32",
"clientVersion": "2.5.2",
"osVersion": "other",
"screen": "2048*1152",
"networkType": "false",
"d_brand": "",
"d_model": "",
"lang": "zh-CN",
"sdkVersion": "2.5.2",
"appid": "m_core",
"openudid": "",
"x-api-eid-token": self.x_token,
"functionId": "balance_getCurrentOrder_m",
"uuid": "17573259367521124301157",
"loginType": "2",
"xAPIScval3": "unknown",
}
proxy = self._get_proxy()
response = self._session.post(
url,
headers=headers,
data=data,
impersonate="chrome",
verify=False,
proxies=proxy,
)
if (
response.ok
and response.json()
and response.json().get("body", {}).get("errorCode") == "601"
):
return self.get_current_order(retry_count - 1)
def submit_order(self):
body = {
"deviceUUID": "6279258698405299736",
"appId": "wxae3e8056daea8727",
"tenantCode": "jgm",
"bizModelCode": "3",
"bizModeClientType": "M",
"token": "3852b12f8c4d869b7ed3e2b3c68c9436",
"externalLoginType": 1,
"appVersion": "2.5.2",
"referer": "https://item.m.jd.com/",
"checkPayPassport": False,
"checkpwdV2": False,
"isEncryptionMobile": True,
"outStockVendorIdList": [12514479],
"mainSkuIdList": [int(self.sku_id)],
"balanceDataServerSkuVOList": [
{
"id": int(self.sku_id),
"jdPrice": "499.00",
"buyNum": 1,
"firstCategoryId": 4938,
"secondCategoryId": 11760,
"thirdCategoryId": 22501,
"promoId": 305236894242,
"venderId": 12514479,
"type": 1,
}
],
"balanceTableWareVoList": [{}],
"cashierDeskBackUrl": f"https://trade.m.jd.com/buy/done.shtml?dealId=%24%7BorderId%7D&sceneval=2&fromPay=1&ptag=7039.27.14&gift_skuid={self.sku_id}&gift_venderid=12514479&gift_cid=22501&normal=1",
"payType": "4",
"subPayType": "",
"licenseList": [],
"balanceCommonOrderForm": {
"action": 1,
"overseaMerge": False,
"international": False,
"netBuySourceType": 0,
"appVersion": "2.5.2",
"supportTransport": False,
"tradeShort": False,
"useChannelFlag": "10000000",
"hasSingleOrderGovSubsidy": False,
"unionPayCouponEffective": False,
"oldAgeStyle": False,
"balanceRefreshByAction": "1",
"supportUserPrivacy": True,
"userPrivacyChecked": True,
},
"balanceExt": {
"baiDuJumpButtonSwitch": False,
"bubbleTips": {"enable": True, "num": 1, "time": 3, "useNum": 0},
"cashierDeskEnable": True,
"cashierPayFlag": "0",
"checkIdInfo": False,
"couponRedText": "",
"hasBackupStorage": False,
"hasCwbg": False,
"hasFreightInsurance": False,
"isInternational": False,
"isSupportTranport": False,
"jdCombineType": 0,
"knowledgeServiceStatus": 0,
"noAddressMatchDegradeSwitch": True,
"overseaMerge": False,
"plusFloorStr": '{"addressType":0,"area":"19-1601-3633-63243","freightInfoList":[],"invoiceType":1,"payWay":4,"plusPromotionRequest":{"couponTotalAmount":0.00,"fareCouponTotalAmount":0.00,"officialDiscountTotalDiscount":0.00,"promotionReduceTotalAmount":0.00,"promotionTotalPrice":499.00,"redBagTotalAmount":0.00,"totalCashGiftDiscount":0,"totalCashGiftNew":0,"totalCashGiftOld":0,"totalParallelDiscount":0.00},"plusStatus":"203","requestConditionList":[105,106,305,529,523,521,522,351],"skuInfoRequestList":[{"col_type":"0","companyType":0,"factoryShip":0,"firstCategory":4938,"isCanUseDongCoupon":1,"isCanUseJingCoupon":1,"isJxzy":0,"isLoc":1,"isOverseaPurchase":0,"jdPrice":"499.00","number":"1","secondCategory":11760,"shopId":12204479,"skuExtension":{"fields":{"fare":"2849262","saler":"","saleAttributes":"[{\\"saleName\\":\\"颜色\\",\\"dim\\":1},{\\"saleName\\":\\"尺码\\",\\"dim\\":2}]","maxBuyNum":"19","is7ToReturn":"0","features":"consumptionVAT:0,inputVAT:0,outputVAT:0","vender_attribute":"","isHitGovSubsidy":"0","selectedGBCZGovConsumerCoupon":"false","yn":"1","vender_name":"易点生活电子商务有限公司","product_id":"10028314345641","sale_atts":"","warranty":"","timeliness_id":"0","sku_name":"【谨防刷单诈骗】沃尔玛大卖场卡500元 官方卡密 卡号8688 不支持山姆 本店不刷单 谨防诈骗 不支持退换","supply_unit":"","model":"","shopId":"12204479","cn_sku_name":"【谨防刷单诈骗】沃尔玛大卖场卡500元 官方卡密 卡号8688 不支持山姆 本店不刷单 谨防诈骗 不支持退换","height":"0","MN":"19","first_buyer_post":"4541","unLimit_cid":"22501","isCanVAT":"0","pay_first":"1","tsfw":"p8,","ms":"0","weight":"0","sku_id":"%s","tax":"consumptionVAT:0,inputVAT:0,outputVAT:0","shop_name":"沃尔玛礼品卡专卖店","product_name":"【谨防刷单诈骗】沃尔玛大卖场卡500元 官方卡密 卡号8688 不支持山姆","brand_id":"250870","shop_id":"12204479","size":"不支持退换","brandId":"250870","col_type":"0","color":"本店不刷单 谨防诈骗","cn_color":"本店不刷单 谨防诈骗","isLOC":"2","img_dfs_url":"jfs/t1/225028/24/16896/94540/6667fb55Fad484540/4069fd317318e9da.jpg","outer_id":"DSwemmck0500","platform":"1","vender_id":"12514479","jc_buyer":"","category_id":"22501","fxg":"0","isQdh":"0","venderAttribute":"","sku_tag":"0","category_id1":"4938","sku_mark":"0","template_type_attributes":"[{\\"attrid\\":\\"1001050620\\",\\"dim\\":1,\\"saleName\\":\\"颜色\\",\\"saleValue\\":\\"本店不刷单 谨防诈骗\\",\\"sequenceNo\\":1,\\"valueId\\":\\"2916524754\\"},{\\"attrid\\":\\"1001051741\\",\\"dim\\":2,\\"saleName\\":\\"尺码\\",\\"saleValue\\":\\"不支持退换\\",\\"sequenceNo\\":1,\\"valueId\\":\\"3924738280\\"}]","category_id2":"11760","sale_template_id":"POP_MODEL","jc_saler":"","day_limited_sales":"19","length":"0","locGroupId":"-100","vender_col_type":"0","sku_status":"1","allnum":"0","containsGovConsumerCoupon":"false","width":"0"},"fresh":false,"num":1,"parallelPromo":false,"samShop":false,"selfSupport":false,"shoppingMalls":false,"skuPriceAfterSinglePromotion":"499.00","skuUuid":"1012_F2t2t3H1499005262095200256","spuId":"-1","storeId":"-1","vendor":"12514479","vendorType":2},"skuId":%s,"skuMark":"","spuId":"-1","thirdCategory":22501,"uuid":"1012_F2t2t3H1499005262095200256","venderId":12514479,"vender_bizid":",popsop,","vender_col_type":"0","vendorType":0}],"totalPrice":"499.00"}'
% (self.sku_id, self.sku_id),
"selectedCouponNum": 0,
"sellLargeDay": "7",
"supportPaymentSkuList": [self.sku_id],
"useBestCoupon": True,
},
"actualPayment": "499.00",
"sendGift": {},
"dsList": [
{"paramName": "report_time", "paramVal": ""},
{"paramName": "deal_id", "paramVal": ""},
{"paramName": "buyer_uin"},
{"paramName": "pin", "paramVal": ""},
{
"paramName": "cookie_pprd_p",
"paramVal": "UUID.17512753463451844059233-LOGID.1751857899901.1014192400",
},
{
"paramName": "cookie_pprd_s",
"paramVal": "76161171.17512753463451844059233.1751275346.1751856681.1751857862.11",
},
{"paramName": "cookie_pprd_t"},
{"paramName": "ip", "paramVal": ""},
{"paramName": "visitkey", "paramVal": "6279258698405299736"},
{"paramName": "gen_entrance", "paramVal": ""},
{"paramName": "deal_src", "paramVal": "7"},
{"paramName": "item_type", "paramVal": "1"},
{"paramName": "fav_unixtime", "paramVal": ""},
{"paramName": "pay_type", "paramVal": "0"},
{"paramName": "ab_test", "paramVal": ""},
{"paramName": "serilize_type", "paramVal": "0"},
{"paramName": "property1", "paramVal": "0"},
{"paramName": "property2", "paramVal": "0"},
{"paramName": "property3", "paramVal": "0"},
{"paramName": "property4", "paramVal": "0"},
{"paramName": "seller_uin", "paramVal": "0"},
{"paramName": "pp_item_id", "paramVal": ""},
{"paramName": "openid"},
{"paramName": "orderprice", "paramVal": ""},
{"paramName": "actiontype", "paramVal": ""},
{"paramName": "extinfo", "paramVal": ""},
{"paramName": "ext1", "paramVal": self.sku_id},
{"paramName": "ext2", "paramVal": ""},
{"paramName": "ext3", "paramVal": ""},
{"paramName": "ext4", "paramVal": ""},
{"paramName": "ext5", "paramVal": ""},
{"paramName": "ext6"},
{"paramName": "ext7", "paramVal": ""},
{"paramName": "ext8", "paramVal": "0"},
{"paramName": "ext9", "paramVal": "0|0|0|0|0||0|0"},
{"paramName": "ext10", "paramVal": "|||"},
{
"paramName": "ext11",
"paramVal": "http://wq.jd.com/wxapp/pages/pay/index/index",
},
{"paramName": "ext12", "paramVal": "1"},
{"paramName": "ext13", "paramVal": ""},
{"paramName": "ext14", "paramVal": ""},
{"paramName": "ext15", "paramVal": ""},
{"paramName": "ext16", "paramVal": ""},
{
"paramName": "ext17",
"paramVal": "76161171%7Ciosapp%7Ct_335139774%7Cappshare%7CCopyURL_shareidde4fa5e467a1fbe323b52ed0fa61777f5ce3e1fe17503293501852_shangxiang_none%7C1751857899902",
},
{"paramName": "ext18"},
{"paramName": "ext19", "paramVal": ""},
{"paramName": "ext20"},
{
"paramName": "fpa",
"paramVal": "7f07f4e1-6ac2-2918-ff9a-18320538d8b2-1743129927",
},
{
"paramName": "fpb",
"paramVal": "BApXSBoHW4fJA8VrxmIXHrE1TcUW-_mloBgFTnghK9xJ1MvE2-4G2",
},
{"paramName": "ext21", "paramVal": ""},
{"paramName": "ext22", "paramVal": ""},
{"paramName": "ext23", "paramVal": "NULL"},
{"paramName": "ext24", "paramVal": "NULL"},
{"paramName": "ext25", "paramVal": "NULL"},
{"paramName": "ext26", "paramVal": "NULL"},
{"paramName": "ext27", "paramVal": "NULL"},
{"paramName": "ext28", "paramVal": "NULL"},
{"paramName": "ext29", "paramVal": "NULL"},
{"paramName": "ext30", "paramVal": "NULL"},
{"paramName": "ext31", "paramVal": "NULL"},
{"paramName": "ext32", "paramVal": "NULL"},
{"paramName": "ext33", "paramVal": "NULL"},
{"paramName": "ext34", "paramVal": "NULL"},
{"paramName": "ext35", "paramVal": "NULL"},
{"paramName": "ext36", "paramVal": "NULL"},
{"paramName": "ext37", "paramVal": "NULL"},
{"paramName": "ext38", "paramVal": "NULL"},
{"paramName": "dt", "paramVal": ""},
],
"govUseGisLocation": False,
"packageStyle": True,
"sceneval": "2",
"balanceDeviceInfo": {"resolution": "2048*1152"},
"balanceId": "5351641558031892481751857904612",
}
encrypt_body = self.sha256_hash(json.dumps(body))
timestamp = f"{int(time.time() * 1000)}"
object_id = "cc85b"
function_id = "balance_submitOrder_m"
h5st = self.get_h5st(timestamp, encrypt_body, object_id, function_id)
headers = {
"accept": "application/json, text/plain, */*",
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
"cache-control": "no-cache",
"content-type": "application/x-www-form-urlencoded",
"origin": "https://trade.m.jd.com",
"pragma": "no-cache",
"priority": "u=1, i",
"referer": "https://trade.m.jd.com/",
"sec-ch-ua": '"Chromium";v="140", "Not=A?Brand";v="24", "Microsoft Edge";v="140"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"Windows"',
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-site",
"user-agent": self.user_agent,
"x-referer-page": "https://trade.m.jd.com/pay",
"x-rp-client": "h5_1.0.0",
"cookie": self.cookies,
}
data = {
"scval": self.sku_id,
"body": json.dumps(body),
"t": timestamp,
"h5st": h5st,
"appid": "m_core",
"client": "Win32",
"clientVersion": "2.5.2",
"d_brand": "",
"d_model": "",
"functionId": "balance_submitOrder_m",
"lang": "zh-CN",
"loginType": "2",
"networkType": "false",
"osVersion": "",
"screen": "1152*2048",
"sdkVersion": "",
"uuid": "17573259367521124301157",
"x-api-eid-token": self.x_token,
"xAPIScval2": "wx",
"xAPIScval3": "unknown",
}
url = "https://api.m.jd.com/client.action"
proxy = self._get_proxy()
response = self._session.post(
url,
headers=headers,
data=data,
verify=False,
proxies=proxy,
)
logger.info(
f"订单号:{self._order_num}app_store提交订单返回{response.json()}"
)
return response.json()
def run(self):
self.x_token, self.eid = self.get_x_token()
# 提交预付款订单
self.get_current_order()
order_res = self.submit_order()
logger.info(
f"订单号:{self._order_num}app_store提交预付款订单返回{order_res}"
)
# 火爆
if order_res.get("body", {}).get("errorCode") == "7201":
return BusinessCode.JD_ORDER_RISK_ERR, {
"deeplink": "",
"order_id": "",
"pay_id": "",
"remark": str(order_res),
}
# 未登录
if order_res.get("body", {}).get("errorCode") == "302":
return BusinessCode.JD_ORDER_CK_ERR, {
"deeplink": "",
"order_id": "",
"pay_id": "",
"remark": order_res.get("body", {}).get("errorReason"),
}
# 无货
if order_res.get("body", {}).get("errorCode") == "722":
return BusinessCode.JD_ORDER_STOCK_ERR, {
"deeplink": "",
"order_id": "",
"pay_id": "",
"remark": order_res.get("body", {})
.get("submitOrderPromptVO", {})
.get("title"),
}
# 火爆
if order_res.get("body", {}).get("errorCode") == "601":
return BusinessCode.JD_ORDER_RISK_ERR, {
"deeplink": "",
"order_id": "",
"pay_id": "",
"remark": str(order_res),
}
if order_res.get("code") != "0":
return BusinessCode.JD_ORDER_NORMAL_ERR, {
"deeplink": "",
"order_id": "",
"pay_id": "",
"remark": str(order_res),
}
if order_res.get("body", {}).get("errorCode") == "601":
return BusinessCode.JD_ORDER_NORMAL_ERR, {
"deeplink": "",
"order_id": "",
"pay_id": "",
"remark": str(order_res),
}
order_id = order_res.get("body", {}).get("order", {}).get("orderId")
# 获取支付信息
pay_res = self.get_pay_res(order_id)
logger.info(f"订单号:{self._order_num}app_store获取支付信息返回{pay_res}")
if (
order_res.get("body", {}).get("errorCode") == "302"
and order_res.get("body", {}).get("errorReason") == "未登录"
):
return BusinessCode.JD_ORDER_CK_ERR, {
"deeplink": "",
"order_id": "",
"pay_id": "",
"remark": str(order_res),
}
if pay_res.get("code") != "0":
return BusinessCode.JD_ORDER_NORMAL_ERR, {
"deeplink": "",
"order_id": "",
"pay_id": "",
"remark": str(pay_res),
}
pay_id = pay_res["body"]["payId"]
# 获取微信支付信息
return self.refresh_payment_url(pay_id, order_id)
def refresh_payment_url(self, pay_id: str, order_id: int):
logger.info(f"订单号:{pay_id}app_store刷新支付链接 {order_id}")
pay_channel_res = self.plat_pay_channel_res(pay_id)
logger.info(
f"订单号:{self._order_num}app_store请求微信渠道返回{pay_channel_res}"
)
if pay_channel_res.get("code") == "601":
return BusinessCode.JD_ORDER_RISK_ERR, {
"deeplink": "",
"order_id": "",
"pay_id": "",
"remark": str(pay_channel_res),
}
if pay_channel_res.get("code") != "0":
return BusinessCode.JD_ORDER_NORMAL_ERR, {
"deeplink": "",
"order_id": "",
"pay_id": "",
"remark": str(pay_channel_res),
}
wx_pay_res = self.plat_wx_pay_res(pay_id)
logger.info(
f"订单号:{self._order_num}app_store获取微信支付信息返回{wx_pay_res}"
)
if wx_pay_res.get("code") != "0":
return BusinessCode.JD_ORDER_NORMAL_ERR, {
"deeplink": "",
"order_id": "",
"pay_id": "",
"remark": str(wx_pay_res),
}
if wx_pay_res.get("errorCode") == "-1":
return BusinessCode.JD_ORDER_CK_ERR, {
"deeplink": "",
"order_id": "",
"pay_id": "",
"remark": str(wx_pay_res),
}
return BusinessCode.SUCCESS, {
"deeplink": wx_pay_res.get("payInfo", {}).get("mweb_url"),
"order_id": str(order_id),
"pay_id": str(pay_id),
}

237
apps/jd/services/delete.py Normal file
View File

@@ -0,0 +1,237 @@
import hashlib
import json
import re
import time
import execjs
from curl_cffi import requests
from observability.logging import get_logger_with_trace
logger = get_logger_with_trace(__name__)
class DeleteOrder:
def __init__(self, cookie, order_id):
self.cookie = cookie
self.order_id = order_id
self.timestamp = int(time.time() * 1000)
self.h5st_ctx = self.get_js_obj("h5st4.2.js")
self.ai = "8108f"
self.version = "4.2"
self.p1 = None
self.fp = None
self.tk = None
self.mode = None
self.rd = None
self.sign = None
self.timestamp_sha256 = None
self.t_sha256 = None
self.t_send_data = None
def get_js_obj(self, js):
with open(f"./js/{js}", "r", encoding="utf-8") as file:
js_code = file.read()
return execjs.compile(js_code, cwd="./node_modules")
def encrypt_body(self):
key = "e7c398ffcb2d4824b4d0a703e38eb0bb"
str_to_hash = f"{self.order_id}1sx{self.timestamp}{key}"
enc_str = hashlib.md5(str_to_hash.encode("utf-8")).hexdigest()
body = {
"orderId": self.order_id,
"source": "1",
"channelSource": "sx",
"t": self.timestamp,
"encStr": enc_str,
}
return json.dumps(body)
def cancel_order(self):
body = self.encrypt_body()
url = "https://api.m.jd.com/api"
headers = {
"accept": "application/json, text/plain, */*",
"accept-language": "zh-CN,zh;q=0.9",
"cache-control": "no-cache",
"content-type": "application/x-www-form-urlencoded",
"origin": "https://recharge.m.jd.com",
"pragma": "no-cache",
"priority": "u=1, i",
"referer": "https://recharge.m.jd.com/",
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-site",
"user-agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1",
"x-referer-page": "https://recharge.m.jd.com/orderDetail",
"x-rp-client": "h5_1.0.0",
"cookie": self.cookie,
}
data = {
"appid": "tsw-m",
"functionId": "huafei_orderCancel",
"t": f"{self.timestamp}",
"body": body,
"client": "iPhone",
"uuid": "1735283651510338525254",
"osVersion": "16.6",
"screen": "828.0000123381615*1792.0000267028809",
"x-api-eid-token": "jdd03MOMPSVKGBFF6WCM3KNQK34LGPGSCNKB2WACDOVKFUNXQWAWDEVXSHMGQEQLJ6EUKKXZ7ARQA4CPF6EMRRUP5P7ETLEAAAAMUC37NLWQAAAAADDQSI4QCFE6GAAX",
}
response = requests.post(url, headers=headers, data=data)
print(response.text)
def get_fp(self):
self.fp = self.h5st_ctx.call("iC")
def get_env(self):
ctx = self.get_js_obj("h5st4.2.js")
env = ctx.call("expandParams", self.fp, self.p1)
return env
def request_algo(self):
env = self.get_env()
url = "https://cactus.jd.com/request_algo"
params = {"g_ty": "ajax"}
timestamp_ms = int(time.time() * 1000)
data = {
"version": "4.2",
"fp": self.fp,
"appId": "8108f",
"timestamp": timestamp_ms,
"platform": "web",
"expandParams": env,
"fv": "h5_npm_v4.2.0",
}
headers = {
"accept": "application/json",
"accept-language": "zh-CN,zh;q=0.9",
"cache-control": "no-cache",
"content-type": "application/json",
"origin": "https://txsm-m.jd.com",
"pragma": "no-cache",
"referer": "https://txsm-m.jd.com/",
"sec-ch-ua": '"Google Chrome";v="123", "Not:A-Brand";v="8", "Chromium";v="123"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"Windows"',
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-site",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
}
data = json.dumps(data, separators=(",", ":"))
response = requests.post(url, headers=headers, params=params, data=data)
return response.json().get("data")
def get_algo(self):
algo_data = self.request_algo()
print(algo_data)
algo = algo_data["result"]["algo"]
self.tk = algo_data["result"]["tk"]
self.mode = re.findall(r"algo\.(.*)\(", algo)[0]
self.rd = re.findall(r"rd='(.*)';", algo)[0]
def get_p1(self):
self.p1 = re.findall(r"pin=(.*);", self.cookie)[0]
def get_tm(self, timestamp):
import datetime
# 将13位时间戳转换为datetime对象
dt = datetime.datetime.fromtimestamp(timestamp / 1000)
year = dt.year
month = str(dt.month).zfill(2)
day = str(dt.day).zfill(2)
hour = str(dt.hour).zfill(2)
minute = str(dt.minute).zfill(2)
second = str(dt.second).zfill(2)
# 将微秒转换为毫秒
microsecond = dt.microsecond // 1000
return f"{year}{month}{day}{hour}{minute}{second}{microsecond}"
def get_oe(self, ts):
oe = self.h5st_ctx.call(
"test", self.mode, self.tk, self.fp, ts, self.ai, self.rd
)
return oe
def get_body(self):
self.t_send_data = int(time.time() * 1000)
self.t_sha256 = int(time.time() * 1000)
self.timestamp_sha256 = self.get_tm(self.t_sha256)
ts = self.timestamp_sha256 + "74"
oe = self.get_oe(ts)
body_hex = self.h5st_ctx.call("encrypt_body", self.order_id, "delete")
t_string = f"{oe}appid:m_core&body:{body_hex}&client:Win32&clientVersion:&functionId:order_recycle_m&t:{self.t_send_data}{oe}"
self.sign = self.h5st_ctx.call("__genSign", t_string)
body = self.h5st_ctx.call("build_params", self.order_id, "delete")
return body
def get_h5st(self):
h5st = f"{self.timestamp_sha256};{self.fp};{self.ai};{self.tk};{self.sign};{self.version};{self.t_sha256}"
return h5st
def request_jd_recycle(self, body, h5st):
headers = {
"accept": "*/*",
"accept-language": "zh-CN,zh;q=0.9",
"cache-control": "no-cache",
"content-type": "application/x-www-form-urlencoded",
"origin": "https://trade.m.jd.com",
"pragma": "no-cache",
"referer": "https://trade.m.jd.com/",
"sec-ch-ua": '"Google Chrome";v="123", "Not:A-Brand";v="8", "Chromium";v="123"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"Windows"',
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-site",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
"x-referer-page": "https://trade.m.jd.com/order/orderlist_jdm.shtml",
"x-rp-client": "h5_1.0.0",
"cookie": self.cookie,
}
url = "https://api.m.jd.com/client.action"
data = {
"t": f"{self.t_send_data}",
"loginType": "2",
"loginWQBiz": "golden-trade",
"appid": "m_core",
"client": "Win32",
"clientVersion": "",
"build": "",
"osVersion": "null",
"screen": "1440*900",
"networkType": "4g",
"partner": "",
"forcebot": "",
"d_brand": "",
"d_model": "",
"lang": "zh-CN",
"scope": "",
"sdkVersion": "",
"openudid": "",
"uuid": "1658065822",
"x-api-eid-token": "jdd03EZ4U2HD6OVXEWMTFQVIXNMASQWBFCYSPX37R7H6QIQDPWYYPIESUHF2YNEGTGXKWHGCH5VEWPMZA4PWPPKX6ZJOKWIAAAAMOR6CEGEIAAAAADWOVM6433SKKPAX",
"functionId": "order_recycle_m",
"body": body,
"h5st": h5st,
}
response = requests.post(url, headers=headers, data=data)
print(response.text)
def recycle_order(self):
self.get_p1()
self.get_fp()
self.get_algo()
body = self.get_body()
h5st = self.get_h5st()
self.request_jd_recycle(body=body, h5st=h5st)
def run(self):
# 取消订单
self.cancel_order()
# 删除订单
self.recycle_order()

View File

@@ -0,0 +1,44 @@
import time
from curl_cffi import requests
from observability.logging import get_logger_with_trace
logger = get_logger_with_trace(__name__)
class GameArea:
@staticmethod
def get_details(cookies, sku_id):
headers = {
"accept": "application/json, text/plain, */*",
"accept-language": "zh-CN,zh;q=0.9",
"cache-control": "no-cache",
"origin": "https://recharge.m.jd.com",
"pragma": "no-cache",
"priority": "u=1, i",
"referer": "https://recharge.m.jd.com/",
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-site",
"user-agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1",
"x-referer-page": "https://recharge.m.jd.com/cardSettlement",
"x-rp-client": "h5_1.0.0",
"cookie": cookies,
}
params = {
"appid": "tsw-m",
"functionId": "getGameDetailBySkuId",
"t": f"{int(time.time() * 1000)}",
"body": '{"skuId":"%s","appKey":"apple","source":41}' % sku_id,
"client": "iPhone",
"uuid": "1731377463937218663686",
"osVersion": "16.6",
"screen": "1170.000046491623*2532.0001006126404",
# "h5st": "20241112145441915;l055lflh9lr1i4k8;8e94a;tk03w92d21bda18nCxAp7H5xKn6xhgPjl2DU239CUZMhS1OwR9VyMZc5hQGRyFwDwYV8pE9DyQ7wpjquSVNSz5Kj3B5v;cbe697aecf5adbdd1ee2ddb4a1943f24;4.9;1731394481915;pjbMhjpdAaYR6jkQyLlQF6Ve2roQJrJdJrESJrpjh7Jf6rJdJz1TIipjLDrgJTISJSVS6PYd1jof0bFTKqIfJqoe1rYTImFf1LofzfITJrJdJrEa-OFTGOEjLrJp-jJS5ToeyT4e6nodGSld4j4TJeYe7PYS4bVT6T1fFSYTyjpjxj5PKSEQKeFjLrJp-jJf9HIg3T0UG6VRFuWeDipjxjJOJrpjh7JjbKUXzfUSnWYQfe2XJrJdJ31QHyVT5ipjLDrgJj4f9G1WJrJdJTlPJrpjh7ZMLrJp7rJdJLYOJipjLrpjh7JjJrJdJPYOJipjLrpjh7peLDIj1XETJrpjLrJp-rojxjZe2iFjLrpjLDrg7rJdJbYOJipjLrpjh7Je2rJdJfYOJipjLrpjh7Jf_rJdJjYOJipjLrpjh7Jj2zZf9rIjLDIj6XETJrpjLrJp-rojxj5R0ipjLrpjh7pfLDIj46FjLrpjLDrg7rJdJ7FjLrpjLDrg7rJdJb1OJrpjLrJpwqJdJbFQGakNGipjLDrguqpjhjZVl6VS5C2OqmHXi_1UHCFjLDIj6rEjLrpjLD7NLDIj7qEjLrJp-jpVLf2YLfVTeqZSAGlQLT4U1nojYunjGy1QDqWRLXmXoq5dGy1QDqWRJrJdJnVO4ipjLD7N;949f348da7bbb53fcee1f3a592db0ad3",
"x-api-eid-token": "jdd03MOMPSVKGBFF6WCM3KNQK34LGPGSCNKB2WACDOVKFUNXQWAWDEVXSHMGQEQLJ6EUKKXZ7ARQA4CPF6EMRRUP5P7ETLEAAAAMTD4S5X6IAAAAADZXKZJY7RRZL4AX",
}
jd_api = "https://api.m.jd.com/api"
response = requests.get(jd_api, headers=headers, params=params)
return response.json()

View File

@@ -0,0 +1,145 @@
from curl_cffi import requests
from apps.jd.services.app_store import AppStoreSpider
from observability.logging import get_logger_with_trace
logger = get_logger_with_trace(__name__)
class GoodsAppleCard(AppStoreSpider):
def __init__(
self,
cookies,
order_num,
brand_id,
face_price,
sku_id,
username,
game_srv=None,
game_area=None,
recharge_type=1,
):
super(GoodsAppleCard, self).__init__(cookies, order_num, face_price=face_price)
self.sku_id = sku_id
self.brand_id = brand_id
self.username = username
self.game_srv = game_srv
self.game_area = game_area
self.face_price = face_price
self.recharge_type = recharge_type
def encrypt_username(self, username):
key = "2E1ZMAF88CCE5EBE551FR3E9AA6FF322"
username = self.js.call("encryptDes", username, key)
return username
def submit_gp_order(self):
headers = {
"Host": "api.m.jd.com",
"pragma": "no-cache",
"cache-control": "no-cache",
"user-agent": self.__user_client,
"accept": "application/json, text/plain, */*",
"x-referer-page": "https://recharge.m.jd.com/cardSettlement",
"content-type": "application/x-www-form-urlencoded",
"x-rp-client": "h5_1.0.0",
"origin": "https://recharge.m.jd.com",
"sec-fetch-site": "same-site",
"sec-fetch-mode": "cors",
"sec-fetch-dest": "empty",
"referer": "https://recharge.m.jd.com/",
"accept-language": "zh-CN,zh;q=0.9",
"priority": "u=1, i",
"cookie": self.cookies,
}
username = self.encrypt_username(self.username)
data = {}
if self.recharge_type == 1:
data = {
"appid": "tsw-m",
"functionId": "submitGPOrder",
"t": f"{self.time_stamp}",
"body": '{"skuId":"%s","brandId":"%s","type":2,"buyNum":1,"payMode":"0","totalPrice":"%s","username":"%s","appKey":"apple","source":41,"version":"1.10","orderSource":41}'
% (self.sku_id, self.brand_id, self.face_price, username),
"client": "iPhone",
"uuid": "1731377463937218663686",
"osVersion": "16.6",
"screen": "1170.000046491623*2532.0001006126404",
# "h5st": "20241112141707038;l055lflh9lr1i4k8;8e94a;tk03w92d21bda18nCxAp7H5xKn6xhgPjl2DU239CUZMhS1OwR9VyMZc5hQGRyFwDwYV8pE9DyQ7wpjquSVNSz5Kj3B5v;8effdf684f396d5889d2bde7e769f965;4.9;1731392227038;pjbMhjpdAaYR6jkQyLlQF6Ve2roQJrJdJrESJrpjh7Jf6rJdJz1TIipjLDrgJTISJSVS6PYd1jof0bFTKqIfJqoe1rYTImFf1LofzfITJrJdJrEa-OFTGOEjLrJp-jJS5ToeyT4e6nodGSld4j4TJeYe7PYS4bVT6T1fFSYTyjpjxj5PKSEQKeFjLrJp-jJf9HIg3T0UG6VRFuWeDipjxjJOJrpjh7JjxOYRhiFPyK3Z2f2XJrJdJ31QHyVT5ipjLDrgJj4f9G1WJrJdJTlPJrpjh7ZMLrJp7rJdJLYOJipjLrpjh7JjJrJdJPYOJipjLrpjh7ZeLDIj1XETJrpjLrJp-rojxjZe2iFjLrpjLDrg7rJdJbYOJipjLrpjh75e2rJdJfYOJipjLrpjh7Jf_rJdJjYOJipjLrpjh7Jj2zZf9rIjLDIj6XETJrpjLrJp-rojxj5R0ipjLrpjh7pfLDIj46FjLrpjLDrg7rJdJ7FjLrpjLDrg7rJdJb1OJrpjLrJpwqJdJbFQGakNGipjLDrguqpjhjZVl6VS5C2OqmHXi_1UHCFjLDIj6rEjLrpjLD7NLDIj7qEjLrJp-jpVLf2YLfVTeqZSAGlQLT4U1nojYunjGy1QDqWRLXmXoq5dGy1QDqWRJrJdJnVO4ipjLD7N;204790ef3f0380d87102b52131fd50d7",
"x-api-eid-token": "jdd03MOMPSVKGBFF6WCM3KNQK34LGPGSCNKB2WACDOVKFUNXQWAWDEVXSHMGQEQLJ6EUKKXZ7ARQA4CPF6EMRRUP5P7ETLEAAAAMTDYRX6SIAAAAACB66T3PYQZLCEEX",
}
if self.recharge_type == 2:
data = {
"appid": "tsw-m",
"functionId": "submitGPOrder",
"t": f"{self.time_stamp}",
"body": '{"skuId":"%s","brandId":"%s","type":2,"buyNum":1,"payMode":"0","totalPrice":"%s","gamesrv":"%s","gamearea":"%s","username":"%s","appKey":"apple","source":41,"version":"1.10","orderSource":41}'
% (
self.sku_id,
self.brand_id,
self.face_price,
self.game_srv,
self.game_area,
username,
),
"client": "iPhone",
"uuid": "1731377463937218663686",
"osVersion": "16.6",
"screen": "1170.000046491623*2532.0001006126404",
# "h5st": "20241112172954252;l055lflh9lr1i4k8;8e94a;tk03w92d21bda18nCxAp7H5xKn6xhgPjl2DU239CUZMhS1OwR9VyMZc5hQGRyFwDwYV8pE9DyQ7wpjquSVNSz5Kj3B5v;dcf2dab0edd6fadd62550cc4ca5e28c4;4.9;1731403794252;pjbMhjpdAaYR6jkQyLlQF6Ve2roQJrJdJrESJrpjh7Jf6rJdJz1TIipjLDrgJTISJSVS6PYd1jof0bFTKqIfJqoe1rYTImFf1LofzfITJrJdJrEa-OFTGOEjLrJp-jJS5ToeyT4e6nodGSld4j4TJeYe7PYS4bVT6T1fFSYTyjpjxj5PKSEQKeFjLrJp-jJf9HIg3T0UG6VRFuWeDipjxjJOJrpjh7JjWW1f5LnageHNqSEOJrJdJ31QHyVT5ipjLDrgJj4f9G1WJrJdJTlPJrpjh7ZMLrJp7rJdJLYOJipjLrpjh7JjJrJdJPYOJipjLrpjh7ZeLDIj1XETJrpjLrJp-rojxjZe2iFjLrpjLDrg7rJdJbYOJipjLrpjh75e2rJdJfYOJipjLrpjh7Jf_rJdJjYOJipjLrpjh7Jj2zZf9rIjLDIj6XETJrpjLrJp-rojxj5R0ipjLrpjh7pfLDIj46FjLrpjLDrg7rJdJ7FjLrpjLDrg7rJdJb1OJrpjLrJpwqJdJbFQGakNGipjLDrguqpjhjZVl6VS5C2OqmHXi_1UHCFjLDIj6rEjLrpjLD7NLDIj7qEjLrJp-jpVLf2YLfVTeqZSAGlQLT4U1nojYunjGy1QDqWRLXmXoq5dGy1QDqWRJrJdJnVO4ipjLD7N;651ee3204b43d0d514092fa61f5629f1",
"x-api-eid-token": "jdd03MOMPSVKGBFF6WCM3KNQK34LGPGSCNKB2WACDOVKFUNXQWAWDEVXSHMGQEQLJ6EUKKXZ7ARQA4CPF6EMRRUP5P7ETLEAAAAMTD6ZVZVQAAAAADSCNVUR4IR7W54X",
}
response = requests.post(self.jd_api, headers=headers, data=data)
return response.json()
def run(self):
# 提交预付款订单
gp_order_res = self.submit_gp_order()
# gp_order_res = {'result': {'orderId': 304636667875, 'paySuccessUrl': ''}, 'code': '0'}
logger.info(
f"订单号:{self.order_num},商品下单提交预付款订单返回:{gp_order_res}"
)
if gp_order_res.get("code") != "0":
return 110, gp_order_res
order_id = gp_order_res["result"]["orderId"]
# 获取支付信息
pay_res_ = self.get_pay_res(order_id)
logger.info(f"订单号:{self.order_num},商品下单获取支付信息返回:{pay_res_}")
if pay_res_.get("code") != "0":
return 110, pay_res_
pay_id = pay_res_["body"]["payId"]
# 获取微信支付信息
pay_channel_res = self.plat_pay_channel_res(pay_id)
logger.info(
f"订单号:{self.order_num},商品下单请求微信渠道返回:{pay_channel_res}"
)
if pay_channel_res.get("code") != "0":
return 110, pay_channel_res
wx_pay_res = self.plat_wx_pay_res(pay_id)
logger.info(
f"订单号:{self.order_num},商品下单获取微信支付信息返回:{wx_pay_res}"
)
if wx_pay_res.get("code") != "0":
return 110, wx_pay_res
if wx_pay_res.get("errorCode") == "-1":
wx_pay_res["order_id"] = order_id
wx_pay_res["pay_id"] = pay_id
wx_pay_res["face_price"] = self.face_price
return 110, wx_pay_res
mweb_url = wx_pay_res["payInfo"]["mweb_url"]
# 获取支付链接信息
deep_link_res = self.get_deep_link_res(mweb_url)
logger.info(
f"订单号:{self.order_num}商品下单获取支付链接deep_link信息返回{deep_link_res}"
)
if deep_link_res.get("retcode") != 1:
return 110, deep_link_res
return 100, {
"deeplink": deep_link_res["deeplink"],
"order_id": pay_channel_res["orderId"],
"pay_id": pay_id,
"face_price": self.face_price,
}

690
apps/jd/services/jstk.py Normal file
View File

@@ -0,0 +1,690 @@
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)

329
apps/jd/services/login.py Normal file
View File

@@ -0,0 +1,329 @@
import base64
import json
import os
import platform
import ddddocr
import execjs
from curl_cffi import requests
from observability.logging import get_logger_with_trace
logger = get_logger_with_trace(__name__)
class LoginSpider:
def __init__(self, phone_):
self.phone = phone_
self.headers = {
"accept": "application/json, text/plain, */*",
"accept-language": "zh-CN,zh;q=0.9",
"cache-control": "no-cache",
"content-type": "application/x-www-form-urlencoded",
"origin": "https://plogin.m.jd.com",
"pragma": "no-cache",
"priority": "u=1, i",
"referer": "https://plogin.m.jd.com/login/login?appid=300&returnurl=https%3A%2F%2Fm.jd.com%2F&source=wq_passport",
"sec-ch-ua": '"Microsoft Edge";v="131", "Chromium";v="131", "Not_A Brand";v="24"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"Windows"',
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-origin",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0",
}
self.cookies = {}
self.current_os = platform.system()
self.js_path = None
self.node_modules_path = None
self.load_directory()
self.slide_ctw = self.init_verify_data_js()
self.user_ctw = self.init_user_encrypt_js()
self.h5st_ctw = self.init_h5st_js()
self.session = requests.Session()
self.ocr = ddddocr.DdddOcr()
self.eid = ""
self.fp = "26114226dc0c6ee7f1eaf3d4abb30cf2"
self.s_token = None
self.rsa_modulus = None
self.api_st = None
self.api_fp = None
self.img1 = None
self.img2 = None
self.vt = None
self.img = None
self.jd_risk_token_id = None
self.jcap_sid = None
self.ck = None
def load_directory(self):
if self.current_os == "Linux":
self.js_path = r"/app/js"
self.node_modules_path = r"/app/node_modules"
else:
self.js_path = "js"
self.node_modules_path = "./node_modules"
def do_execjs(self, path):
return execjs.compile(
open(path, encoding="utf8").read(), cwd=self.node_modules_path
)
def init_verify_data_js(self):
js_path = os.path.join(self.js_path, "modules.js")
return execjs.compile(
open(js_path, encoding="gbk", errors="ignore").read(),
cwd=self.node_modules_path,
)
def init_user_encrypt_js(self):
js_path = os.path.join(self.js_path, "user_encrypt.js")
return execjs.compile(
open(js_path, encoding="gbk", errors="ignore").read(),
cwd=self.node_modules_path,
)
def init_h5st_js(self):
js_path = os.path.join(self.js_path, "h5st-5.0.js")
return execjs.compile(
open(js_path, encoding="gbk", errors="ignore").read(),
cwd=self.node_modules_path,
)
def request_jd_risk_token_id(self):
response = self.session.get(
"https://payrisk.jd.com/m.html", cookies=self.cookies, headers=self.headers
)
return response.text
def build_jcapsid_data(self):
return self.user_ctw.call(
"init", self.phone, self.fp, self.jd_risk_token_id, self.s_token
)
def request_jcapsid(self):
url = "https://plogin.m.jd.com/cgi-bin/mm/jcapsid"
data = self.build_jcapsid_data()
response = self.session.post(url, headers=self.headers, data=data)
print(f"request_jcapsid返回{response.text}")
return response.json()
def request_new_login_entrance(self):
url = "https://plogin.m.jd.com/cgi-bin/mm/new_login_entrance"
params = {
"lang": "chs",
"returnurl": "https://my.m.jd.com/",
"risk_jd\\[eid\\]": self.eid,
"risk_jd\\[fp\\]": self.fp,
}
response = self.session.get(
url, headers=self.headers, cookies=self.cookies, params=params
)
print(response.text)
return response.json()
def get_jcap_sid(self):
jcapsid_res = self.request_jcapsid()
self.jcap_sid = jcapsid_res["jcap_sid"]
def get_login_entrance(self):
login_entrance_res = self.request_new_login_entrance()
self.s_token = login_entrance_res["s_token"]
self.rsa_modulus = login_entrance_res["rsa_modulus"]
def build_fp_data(self):
return self.slide_ctw.call("getFp", self.phone, self.jcap_sid)
def request_data_fp(self):
data = self.build_fp_data()
url = "https://jcap.m.jd.com/cgi-bin/api/fp"
response = self.session.post(url, headers=self.headers, data=data)
print(response.text)
return response.json()
def get_api_params(self):
fp_res = self.request_data_fp()
self.api_st = fp_res["st"]
self.api_fp = fp_res["fp"]
def build_captcha_data(self):
return self.slide_ctw.call("getImage", self.phone, self.jcap_sid, self.api_st)
def request_captcha(self):
url = "https://jcap.m.jd.com/cgi-bin/api/check"
data = self.build_captcha_data()
response = requests.post(url, headers=self.headers, data=data)
print(response.text)
return response.json()
def set_img(self, captcha_res):
self.img = captcha_res["img"]
img_json = json.loads(captcha_res["img"])
self.api_st = captcha_res["st"]
self.img1 = img_json["b1"]
self.img2 = img_json["b2"]
def get_captcha(self):
captcha_res = self.request_captcha()
if captcha_res.get("vt"):
self.vt = captcha_res["vt"]
self.api_st = captcha_res["st"]
return True
self.set_img(captcha_res)
return False
def verify_captcha(self):
bg = self.img1.replace("data:image/jpg;base64,", "")
background = base64.b64decode(bg)
fg = self.img2.replace("data:image/png;base64,", "")
target = base64.b64decode(fg)
result = self.ocr.slide_match(target, background, simple_target=True)
x = round(result["target"][0] * (290 / 275))
url = "https://jcap.m.jd.com/cgi-bin/api/check"
data = self.slide_ctw.call(
"verify", self.phone, self.jcap_sid, self.img, self.api_st, x
)
response = self.session.post(url, headers=self.headers, data=data)
print(response.text)
return response.json()
def check_captcha(self):
captcha_res = self.verify_captcha()
if captcha_res.get("vt"):
self.vt = captcha_res["vt"]
self.api_st = captcha_res["st"]
return True
self.set_img(captcha_res)
return False
def build_send_code_data(self):
return self.user_ctw.call(
"sendMsg",
self.phone,
self.vt,
self.jd_risk_token_id,
self.s_token,
self.rsa_modulus,
)
def send_code(self):
url = "https://plogin.m.jd.com/cgi-bin/mm/dosendlogincode"
data = self.build_send_code_data()
response = self.session.post(url, headers=self.headers, data=data)
print(response.text)
self.ck = self.get_cookie()
return response.json()
def get_h5st(self, code):
return self.h5st_ctw.call("getH5st", code, self.s_token)
def build_verify_data(self, phone_, code, s_token, jd_risk_token_id, rsa_modulus):
h5st = self.get_h5st(code)
return self.user_ctw.call(
"login", phone_, code, s_token, h5st, jd_risk_token_id, rsa_modulus
)
def get_cookie(self):
cookie_dict = self.session.cookies.get_dict()
return "; ".join([f"{key}={value}" for key, value in cookie_dict.items()]) + ";"
def request_sms_login(self, ck, code, s_token, jd_risk_token_id, rsa_modulus):
url = "https://plogin.m.jd.com/cgi-bin/mm/dosmslogin"
h5st = self.get_h5st(code)
data = self.user_ctw.call(
"login", phone, code, s_token, h5st, jd_risk_token_id, rsa_modulus
)
self.headers["cookie"] = ck
response = requests.post(url, headers=self.headers, data=data)
print(response.text)
self.ck = self.get_cookie()
return response.json()
def get_jd_risk_token_id(self):
self.jd_risk_token_id = (
self.request_jd_risk_token_id()
.split("var jd_risk_token_id = ")[1]
.strip(";")
.strip("'")
)
def get_code_res(self):
send_res = self.send_code()
if send_res.get("err_code") == 0:
return 100, {
"ck": self.ck,
"s_token": self.s_token,
"jd_risk_token_id": self.jd_risk_token_id,
"rsa_modulus": self.rsa_modulus,
}
else:
return 101, {
"ck": "",
"s_token": "",
"jd_risk_token_id": "",
"rsa_modulus": "",
}
@staticmethod
def my_response(status_code, data):
return {"code": status_code, "data": data, "msg": "请求成功"}
def run_get_ck(self, ck, code, s_token, jd_risk_token_id, rsa_modulus):
login_res = self.request_sms_login(
ck, code, s_token, jd_risk_token_id, rsa_modulus
)
if login_res.get("err_code") == 0:
data = {"ck": self.ck}
return self.my_response(status_code=100, data=data)
else:
data = {"ck": ""}
return self.my_response(status_code=101, data=data)
def run_send_code(self):
try:
# 获取risk_token_id
self.get_jd_risk_token_id()
# 获取登录所需初始sid
self.get_login_entrance()
self.get_jcap_sid()
# 获取验证码
self.get_api_params()
captcha_status = self.get_captcha()
if not captcha_status:
print("出现滑块验证")
# 验证验证码
check_status = self.check_captcha()
if check_status:
print("滑块验证通过,发送验证码")
status_code, data = self.get_code_res()
return self.my_response(status_code, data)
return None
else:
print("没有出现滑块验证,直接发送验证码")
status_code, data = self.get_code_res()
return self.my_response(status_code, data)
except:
return self.my_response(status_code=111, data={})
if __name__ == "__main__":
phone_ = "13071534209"
res = LoginSpider(
phone_=phone_,
).run_send_code()
print(res)
res = LoginSpider(
phone_=phone_,
).run_get_ck(
ck="jcap_dvzw_fp=7Hq5_QuYK92sDlclFxhJ8m28JaYU1hDYJ-U-gC59LmJRPuo3ERCbiMGzn5vdc3WcTX0ndWGFZNOs3BDenMr7lw==; guid=a5bb2dc650ab23a2cf5cb58afef19b3f35adba9613421b4cba79aa59ea891803; lang=chs; lsid=503346699194megeguncgrf6gvwb9tdujp2md5p27pkt1737276375048; lstoken=vvwcnbi4;",
code="315572",
s_token="vvwcnbi4",
jd_risk_token_id="SH6IDCXBUWZNAV3SJPRGIXLVNHGLZR6AYHJ3244TGU5Z35SFJU76TAQ3OMNTXE7XA3SBRMPVTZJVW",
rsa_modulus="B03744DE9EAB28F6FA6B9C8FB1873CF57D42A2B6D382B79B276C2079A42B24C11D641EA642CF62485A632AE244DE6DE05A92A20EEFE8B6C7743F09FCE0BF78E6D614C115CDAEC2F4825F82E06770A2599D69BBADBE678DD25F2E5B9E2D0E3E15BEB749B436860872D30676794D3C3E8C37B71372DE52F223917FA730EC21F047",
)
print(res)

193
apps/jd/services/utils.py Normal file
View File

@@ -0,0 +1,193 @@
import ctypes
import hashlib
import json
from urllib.parse import parse_qs
from curl_cffi import requests
from tenacity import retry, stop_after_attempt, wait_exponential
from observability.logging import get_logger_with_trace
logger = get_logger_with_trace(__name__)
def r(data_string):
def int_overflow(val):
maxint = 2147483647
if not -maxint - 1 <= val <= maxint:
val = (val + (maxint + 1)) % (2 * (maxint + 1)) - maxint - 1
return val
def unsigned_right_shitf(n, i):
# 数字小于0则转为32位无符号uint
if n < 0:
n = ctypes.c_uint32(n).value
# 正常位移位数是为正数但是为了兼容js之类的负数就右移变成左移好了
if i < 0:
return -int_overflow(n << abs(i))
# print(n)
return int_overflow(n >> i)
char_list = []
aae = [
"K",
"L",
"M",
"N",
"O",
"P",
"Q",
"R",
"S",
"T",
"A",
"B",
"C",
"D",
"E",
"F",
"G",
"H",
"I",
"J",
"U",
"V",
"W",
"X",
"Y",
"Z",
"a",
"b",
"c",
"d",
"o",
"p",
"q",
"r",
"s",
"t",
"u",
"v",
"w",
"x",
"e",
"f",
"g",
"h",
"i",
"j",
"k",
"l",
"m",
"n",
"y",
"z",
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"+",
"/",
]
b_arr = data_string.encode("utf-8")
for i in range(0, len(b_arr), 3):
b_arr2 = [None for i in range(4)]
b2 = 0
for i2 in range(0, 3):
i3 = i + i2
if i3 <= len(b_arr) - 1:
b_arr2[i2] = b2 | unsigned_right_shitf(
(b_arr[i3] & 255), ((i2 * 2) + 2)
)
b2 = unsigned_right_shitf(
((b_arr[i3] & 255) << (((2 - i2) * 2) + 2)) & 255, 2
)
else:
b_arr2[i2] = b2
b2 = 64
b_arr2[3] = b2
for i4 in range(4):
if b_arr2[i4] <= 63:
char_list.append(aae[b_arr2[i4]])
else:
char_list.append("=")
return "".join(char_list)
def encode_cipher(cipher_dict):
for k, v in cipher_dict.items():
cipher_dict[k] = r(v)
def gen_cipher_ep(uuid, ts):
cipher_dict = {
"d_model": "SM-N9760",
"wifiBssid": "unknown",
"osVersion": "9",
"d_brand": "samsung",
"screen": "960*540",
"uuid": uuid,
"aid": uuid,
}
encode_cipher(cipher_dict)
data_dict = {
"hdid": "JM9F1ywUPwflvMIpYPok0tt5k9kW4ArJEU3lfLhxBqw=",
"ts": ts,
"ridx": -1,
"cipher": cipher_dict,
"ciphertype": 5,
"version": "1.2.0",
"appname": "com.jingdong.app.mall",
}
ep = json.dumps(data_dict, separators=(",", ":"))
return ep
def get_pay_sign(order_id, face_price):
app_id = "jd_android_app4"
pay_app_key = "e53jfgRgd7Hk"
input_str = f"{app_id};{order_id};37;{face_price};{pay_app_key}"
# 获取 MD5 实例
input_bytes = input_str.encode("GBK")
md5_hash = hashlib.md5()
md5_hash.update(input_bytes)
return md5_hash.hexdigest()
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=15))
def get_sign(func, body, uuid_, version):
data = {
"func": func,
"body": body,
"uid": uuid_,
"platform": "android",
"version": version,
}
logger.info(f"请求参数:{data}")
response_jd = requests.post(
url="http://unidbg-boot-server:9999/api/jd/encrypt",
json=data,
headers={"Content-Type": "application/json"},
)
logger.info(f"请求结果:{response_jd.text}")
res = response_jd.json()
params = parse_qs(res["data"])
formatted_params = {key: value[0] for key, value in params.items()}
return formatted_params
def my_json(code, data, msg):
"""返回标准格式的JSON响应"""
# 处理枚举类型
if hasattr(code, "value"):
code = code.value
return {"code": code, "data": data, "msg": msg}

View File

View File

@@ -0,0 +1,442 @@
from collections import Counter
import threading
import time
from abc import ABC, abstractmethod
from typing import Optional, Dict, Tuple
from curl_cffi import ProxySpec, requests
from core.config import ProxyPoolType, settings
from observability.logging import get_logger_with_trace
# from app.config.config import Config
# from app.logging.logger import get_logger
# from app.proxy_pool.enums import ProxyPoolType
# 禁用 HTTPS 请求的警告
logger = get_logger_with_trace(__name__)
class BaseProxyPool(ABC):
"""代理池抽象基类"""
def __init__(self):
self.proxy_timeout = 3 # 代理超时时间,单位秒
self.test_url = "https://www.baidu.com"
self.max_retries = 1 # 最大重试次数
self.lock = threading.Lock() # 添加线程锁以确保并发安全
@abstractmethod
def get_proxy(self, *args, **kwargs) -> Optional[str]:
"""获取代理的抽象方法"""
pass
@abstractmethod
def release_proxy(self, *args, **kwargs):
"""释放代理的抽象方法"""
pass
@abstractmethod
def _get_new_proxy(self) -> Optional[str]:
"""获取新的代理的抽象方法"""
pass
@abstractmethod
def mark_proxy_invalid(self, proxy: str):
"""标记代理为无效的抽象方法"""
pass
@abstractmethod
def remove_invalid_proxy(self, proxy: str):
"""删除指定的无效代理的抽象方法"""
pass
def _validate_proxy_with_auth(self, proxy: str) -> bool:
"""验证带认证信息的代理是否可用
Args:
proxy: 代理地址可能是纯IP地址或带认证信息的完整代理地址
Returns:
bool: 代理是否可用
"""
# 检查是否已经是带认证信息的代理地址
if proxy.startswith("http://") and "@" in proxy:
proxyMeta = ProxySpec(all=proxy)
else:
# 如果是纯IP地址添加认证信息
proxyMeta: ProxySpec = ProxySpec(all="http://%(user)s:%(pass)s@%(host)s" % {
"host": proxy,
"user": settings.proxy_username,
"pass": settings.proxy_password,
})
for attempt in range(self.max_retries):
try:
response = requests.get(
self.test_url,
proxies=proxyMeta,
timeout=self.proxy_timeout,
verify=False,
)
if response.status_code == 200:
return True
logger.warning(
f"带认证的代理验证失败,状态码: {response.status_code},重试次数: {attempt + 1},代理: {proxyMeta}"
)
except Exception as e:
logger.warning(
f"带认证的代理验证出错: {str(e)},代理: {proxyMeta},重试次数: {attempt + 1}"
)
return False
class DefaultProxyPool(BaseProxyPool):
"""默认代理池实现"""
def __init__(self):
super().__init__()
self.order_proxy_map: Dict[str, str] = {} # 订单ID -> 代理地址
self.proxy_order_count_map: Dict[str, int] = {} # 代理地址 -> 使用计数
self.max_orders_per_proxy = 5 # 一个代理最多可在5个订单中使用
def get_proxy(self, order_id: str = "") -> Optional[str]:
"""获取指定订单的代理
Args:
order_id: 订单ID
Returns:
代理地址,格式为 http://user:pass@ip:port如果获取失败则返回None
"""
with self.lock: # 使用线程锁确保并发安全
# 检查是否已有分配的代理
if order_id in self.order_proxy_map:
proxy = self.order_proxy_map[order_id]
if self._validate_proxy_with_auth(proxy):
return proxy
else:
# 代理无效,清理相关映射
self._cleanup_proxy_mapping(proxy)
del self.order_proxy_map[order_id]
# 尝试复用现有代理(使用次数未达上限)
for proxy, count in self.proxy_order_count_map.items():
if count < self.max_orders_per_proxy and self._validate_proxy_with_auth(proxy):
# 复用此代理
self.order_proxy_map[order_id] = proxy
self.proxy_order_count_map[proxy] = count + 1
logger.info(f"订单 {order_id} 复用代理 {proxy},当前使用次数: {count + 1}")
return proxy
# 获取新代理
try:
proxy = self._get_new_proxy()
if proxy is not None:
self.order_proxy_map[order_id] = proxy
self.proxy_order_count_map[proxy] = 1
logger.info(f"订单 {order_id} 获取新代理 {proxy}")
return proxy
except Exception as e:
logger.error(f"获取代理失败: {str(e)}")
return None
def release_proxy(self, order_id: str):
"""释放指定订单的代理
Args:
order_id: 订单ID
"""
with self.lock: # 使用线程锁确保并发安全
if order_id in self.order_proxy_map:
proxy = self.order_proxy_map[order_id]
del self.order_proxy_map[order_id]
# 减少代理使用计数
if proxy in self.proxy_order_count_map:
self.proxy_order_count_map[proxy] -= 1
if self.proxy_order_count_map[proxy] <= 0:
del self.proxy_order_count_map[proxy]
logger.info(f"代理 {proxy} 使用计数归零,已从计数映射中删除")
else:
logger.info(f"订单 {order_id} 释放代理 {proxy},剩余使用次数: {self.proxy_order_count_map[proxy]}")
logger.info(f"订单 {order_id} 已释放代理 {proxy}")
def _cleanup_proxy_mapping(self, proxy: str):
"""清理代理相关的所有映射
Args:
proxy: 要清理的代理地址
"""
if proxy in self.proxy_order_count_map:
del self.proxy_order_count_map[proxy]
logger.info(f"已清理代理 {proxy} 的计数映射")
def mark_proxy_invalid(self, proxy: str):
"""标记代理为无效
Args:
proxy: 要标记为无效的代理地址
"""
self.remove_invalid_proxy(proxy)
def remove_invalid_proxy(self, proxy: str):
"""删除指定的无效代理
Args:
proxy: 要删除的代理地址
"""
with self.lock:
# 查找并删除使用该代理的所有订单映射
orders_to_remove = []
for order_id, proxy_addr in self.order_proxy_map.items():
if proxy_addr == proxy:
orders_to_remove.append(order_id)
# 删除找到的订单映射
for order_id in orders_to_remove:
del self.order_proxy_map[order_id]
logger.info(f"已删除订单 {order_id} 的无效代理 {proxy}")
# 清理代理的计数映射
self._cleanup_proxy_mapping(proxy)
if not orders_to_remove:
logger.warning(f"未找到使用代理 {proxy} 的订单,可能已被删除")
def _get_new_proxy(self) -> Optional[str]:
"""获取新的代理最多尝试3次
Returns:
代理地址,格式为 http://user:pass@ip:port
Raises:
Exception: 连续3次获取代理失败时抛出异常
"""
max_attempts = 2 # 最大尝试次数
for attempt in range(max_attempts):
try:
res = requests.get(
settings.proxy_url,
timeout=self.proxy_timeout,
)
ip = res.text.strip()
if ip and self._validate_proxy_with_auth(ip):
# 构建带认证信息的代理地址
proxyMeta = "http://%(user)s:%(pass)s@%(host)s" % {
"host": ip,
"user": settings.proxy_username,
"pass": settings.proxy_password,
}
return proxyMeta
logger.warning(
f"获取代理失败或代理验证失败,尝试次数: {attempt + 1}/{max_attempts}"
)
except Exception as e:
logger.error(
f"获取代理出错: {str(e)},尝试次数: {attempt + 1}/{max_attempts}"
)
# 所有尝试都失败,抛出异常
raise Exception(f"连续{max_attempts}次获取代理失败")
def get_all_proxies(self) -> Dict[str, str]:
"""获取所有订单的代理映射
Returns:
Dict[str, str]: 订单ID到代理地址的映射
"""
with self.lock: # 使用线程锁确保并发安全
return self.order_proxy_map.copy()
def get_proxy_usage_stats(self) -> Dict[str, int]:
"""获取代理使用统计信息
Returns:
Dict[str, int]: 代理地址到使用次数的映射
"""
with self.lock: # 使用线程锁确保并发安全
return self.proxy_order_count_map.copy()
def set_max_orders_per_proxy(self, max_orders: int):
"""设置一个代理最多可使用的订单数
Args:
max_orders: 最大订单数量
"""
with self.lock:
self.max_orders_per_proxy = max_orders
logger.info(f"代理最大使用次数已设置为: {max_orders}")
class ExpiringProxyPool(BaseProxyPool):
"""带有效期的代理池实现"""
def __init__(self, expire_time: int = 60):
super().__init__()
self.current_proxy: Optional[Tuple[str, float]] = None # 当前代理及其过期时间
self.proxy_order_count_map = Counter() # 代理使用计数
self.expire_time = expire_time # 代理有效期
self.invalid_proxies: set = set() # 存储无效的代理
self.max_orders_per_proxy = 5 # 一个代理最多可在5个订单中使用
def get_proxy(self, order_id: str = "") -> Optional[str]:
"""获取指定订单的代理
Args:
order_id: 订单ID
Returns:
代理地址,格式为 http://user:pass@ip:port如果获取失败则返回None
"""
with self.lock: # 使用线程锁确保并发安全
# 检查当前代理是否有效且未过期
if self.current_proxy:
proxy, expire_time = self.current_proxy
# 如果代理未过期且不在无效列表中,则返回
if (
time.time() < expire_time
and proxy not in self.invalid_proxies
and self._validate_proxy_with_auth(proxy)
and self.proxy_order_count_map[proxy] < self.max_orders_per_proxy
):
self.proxy_order_count_map[proxy] += 1
return proxy
# 获取新代理
try:
proxy = self._get_new_proxy()
if proxy:
# 设置代理过期时间
expire_time = time.time() + self.expire_time
self.current_proxy = (proxy, expire_time)
return proxy
except Exception as e:
logger.error(f"获取代理失败: {str(e)}")
return None
return None
def release_proxy(self, *args, **kwargs):
"""释放代理(此实现不需要)"""
pass
def _get_new_proxy(self) -> Optional[str]:
"""获取新的代理最多尝试3次
Returns:
代理地址,格式为 http://user:pass@ip:port
Raises:
Exception: 连续3次获取代理失败时抛出异常
"""
max_attempts = 2 # 最大尝试次数
logger.info(
f"获取代理{settings.proxy_url} {settings.proxy_username} {settings.proxy_password}"
)
for attempt in range(max_attempts):
try:
res = requests.get(
settings.proxy_url,
timeout=self.proxy_timeout,
)
ip = res.text.strip()
logger.info(f"获取代理:{ip}")
if ip and self._validate_proxy_with_auth(ip):
# 构建带认证信息的代理地址
proxyMeta = "http://%(user)s:%(pass)s@%(host)s" % {
"host": ip,
"user": settings.proxy_username,
"pass": settings.proxy_password,
}
# 检查新获取的代理是否在无效列表中
if proxyMeta not in self.invalid_proxies:
return proxyMeta
logger.warning(
f"获取的代理 {proxyMeta} 在无效列表中,继续尝试获取新代理"
)
logger.warning(
f"获取代理失败或代理验证失败,尝试次数: {attempt + 1}/{max_attempts}"
)
except Exception as e:
logger.error(
f"获取代理出错: {str(e)},尝试次数: {attempt + 1}/{max_attempts}"
)
# 所有尝试都失败,抛出异常
raise Exception(f"连续{max_attempts}次获取代理失败")
def mark_proxy_invalid(self, proxy: str):
"""标记代理为无效
Args:
proxy: 要标记为无效的代理地址
"""
with self.lock:
# 添加到无效代理列表
self.invalid_proxies.add(proxy)
# 如果当前代理被标记为无效,立即清除
if self.current_proxy and self.current_proxy[0] == proxy:
self.current_proxy = None
logger.info(f"代理 {proxy} 已被标记为无效")
def clear_invalid_proxies(self):
"""清除无效代理列表
用于定期清理无效代理列表,避免内存占用过大
"""
with self.lock:
self.invalid_proxies.clear()
logger.info("无效代理列表已清除")
def remove_invalid_proxy(self, proxy: str):
"""删除指定的无效代理
Args:
proxy: 要删除的代理地址
"""
with self.lock:
# 从无效代理列表中删除该代理(如果存在)
if proxy in self.invalid_proxies:
self.invalid_proxies.remove(proxy)
logger.info(f"已从无效代理列表中删除代理 {proxy}")
# 如果当前代理就是要删除的代理,也清除当前代理
if self.current_proxy and self.current_proxy[0] == proxy:
self.current_proxy = None
logger.info(f"已清除当前无效代理 {proxy}")
def set_expire_time(self, seconds: int):
"""设置代理有效期
Args:
seconds: 代理有效期,单位秒
"""
with self.lock:
self.expire_time = seconds
class ProxyPoolFactory:
"""代理池工厂类"""
_instances: Dict[ProxyPoolType, BaseProxyPool] = {}
_lock = threading.Lock()
@classmethod
def get_proxy_pool(
cls, pool_type: ProxyPoolType = ProxyPoolType.DEFAULT, **kwargs
) -> BaseProxyPool:
"""获取代理池实例
Args:
pool_type: 代理池类型
**kwargs: 代理池初始化参数
Returns:
BaseProxyPool: 代理池实例
"""
with cls._lock:
if pool_type not in cls._instances:
if pool_type == ProxyPoolType.DEFAULT:
cls._instances[pool_type] = DefaultProxyPool()
elif pool_type == ProxyPoolType.EXPIRING:
expire_time = kwargs.get("expire_time", 60)
cls._instances[pool_type] = ExpiringProxyPool(
expire_time=expire_time
)
return cls._instances[pool_type]
# 使用示例
if __name__ == "__main__":
# 获取默认代理池
# default_pool = ProxyPoolFactory.get_proxy_pool()
# print(default_pool.get_proxy("test_order"))
# 获取带有效期的代理池
for i in range(10):
expiring_pool = ProxyPoolFactory.get_proxy_pool(
ProxyPoolType.EXPIRING, expire_time=60
)
proxy = expiring_pool.get_proxy(order_id="test_order")
print(proxy)
time.sleep(2)