qq 登录修复
This commit is contained in:
@@ -138,9 +138,16 @@ mkdir -p "$YYB_WORKER_DATA_DIR"
|
|||||||
trap cleanup EXIT INT TERM
|
trap cleanup EXIT INT TERM
|
||||||
|
|
||||||
start_yyb_worker() {
|
start_yyb_worker() {
|
||||||
|
# Worker 跟随 dev.sh 生命周期:发现遗留进程(外部启动)时先停止,再自启,
|
||||||
|
# 确保运行的是当前代码且 Ctrl+C 时一并退出。
|
||||||
if curl -fsS "${DEV_YYB_WORKER_URL}/health" >/dev/null 2>&1; then
|
if curl -fsS "${DEV_YYB_WORKER_URL}/health" >/dev/null 2>&1; then
|
||||||
echo "本地 YYB Worker 已运行: ${DEV_YYB_WORKER_URL}(复用现有进程,如需重启请先停掉该进程)"
|
local leaked
|
||||||
return
|
leaked="$(lsof -ti tcp:"$YYB_WORKER_PORT" -sTCP:LISTEN 2>/dev/null | head -1 || true)"
|
||||||
|
if [ -n "$leaked" ]; then
|
||||||
|
echo "发现遗留 YYB Worker(pid $leaked),正在停止并重新启动..."
|
||||||
|
kill "$leaked" 2>/dev/null || true
|
||||||
|
sleep 1
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
if [ ! -d "$YYB_WORKER_NODE_MODULES/jsdom" ]; then
|
if [ ! -d "$YYB_WORKER_NODE_MODULES/jsdom" ]; then
|
||||||
echo "正在安装本地 YYB Worker Node 依赖..."
|
echo "正在安装本地 YYB Worker Node 依赖..."
|
||||||
|
|||||||
@@ -480,6 +480,8 @@ def main() -> int:
|
|||||||
p.add_argument("--role-name", default=None, help="游戏角色名称;默认使用模板当前角色")
|
p.add_argument("--role-name", default=None, help="游戏角色名称;默认使用模板当前角色")
|
||||||
p.add_argument("--zone-id", default=None, help="游戏区服 ID;默认使用模板当前区服")
|
p.add_argument("--zone-id", default=None, help="游戏区服 ID;默认使用模板当前区服")
|
||||||
p.add_argument("--zone-name", default=None, help="游戏区服名称")
|
p.add_argument("--zone-name", default=None, help="游戏区服名称")
|
||||||
|
p.add_argument("--area", default=None, help="游戏大区 ID(QQ 平台与 zoneid 不同,来自角色查询的 partition_info)")
|
||||||
|
p.add_argument("--partition", default=None, help="游戏分区 ID(QQ 平台可为空)")
|
||||||
p.add_argument("--pf", default=None, help="支付平台标识;由角色选择器按 Android/iOS 写入")
|
p.add_argument("--pf", default=None, help="支付平台标识;由角色选择器按 Android/iOS 写入")
|
||||||
p.add_argument("--output", default=None)
|
p.add_argument("--output", default=None)
|
||||||
p = msub.add_parser("pay", help="mall 下单 -> goods web_save -> 微信支付二维码(纯 Python)")
|
p = msub.add_parser("pay", help="mall 下单 -> goods web_save -> 微信支付二维码(纯 Python)")
|
||||||
@@ -624,8 +626,17 @@ def _apply_card_selection(payload: dict, args) -> None:
|
|||||||
zone_id = getattr(args, "zone_id", None)
|
zone_id = getattr(args, "zone_id", None)
|
||||||
if zone_id:
|
if zone_id:
|
||||||
product["zoneid"] = zone_id
|
product["zoneid"] = zone_id
|
||||||
product["area"] = zone_id
|
|
||||||
payload["zoneid"] = zone_id
|
payload["zoneid"] = zone_id
|
||||||
|
# QQ 平台的大区(area)与 zoneid 不同,来自角色查询的 partition_info;
|
||||||
|
# 必须用真实 area,否则 PlaceOrder 校验角色失败(gamerole err)。
|
||||||
|
area = getattr(args, "area", None)
|
||||||
|
if area:
|
||||||
|
product["area"] = area
|
||||||
|
elif zone_id:
|
||||||
|
product["area"] = zone_id
|
||||||
|
partition = getattr(args, "partition", None)
|
||||||
|
if partition is not None:
|
||||||
|
product["partition"] = partition
|
||||||
zone_name = getattr(args, "zone_name", None)
|
zone_name = getattr(args, "zone_name", None)
|
||||||
if zone_name:
|
if zone_name:
|
||||||
product["zonename"] = zone_name
|
product["zonename"] = zone_name
|
||||||
@@ -696,12 +707,22 @@ def cmd_mall_auto(args) -> int:
|
|||||||
out.write_text(raw, encoding="utf-8")
|
out.write_text(raw, encoding="utf-8")
|
||||||
js = json.loads(raw)
|
js = json.loads(raw)
|
||||||
ret = js.get("result_code")
|
ret = js.get("result_code")
|
||||||
if ret == "0":
|
try:
|
||||||
cr = json.loads(js["data"]["call_reply"])
|
call_reply = json.loads(js["data"]["call_reply"])
|
||||||
print(f"✅ 仅CK全自动下单成功! token={cr['data']['token'][:24]}...")
|
except (KeyError, TypeError, json.JSONDecodeError):
|
||||||
|
call_reply = {}
|
||||||
|
# PlaceOrder 业务失败(如 10014 gamerole err)时外层 result_code 仍可能为 0,
|
||||||
|
# 必须检查 call_reply 的 result_code,避免把失败当成功后再崩溃。
|
||||||
|
inner_ret = call_reply.get("result_code")
|
||||||
|
if ret == "0" and str(inner_ret) in ("0", "None", ""):
|
||||||
|
data = call_reply.get("data") or {}
|
||||||
|
token = str(data.get("token") or "")[:24]
|
||||||
|
print(f"✅ 仅CK全自动下单成功! token={token or '(无 token)'}...")
|
||||||
print(f" 响应已保存 → {out}")
|
print(f" 响应已保存 → {out}")
|
||||||
return 0
|
return 0
|
||||||
print(f"❌ 下单失败 ret={ret} ({js.get('result_info', '')})")
|
detail = js.get("result_info", "") or call_reply.get("result_info", "") or ""
|
||||||
|
print(f"❌ 下单失败 ret={ret} inner={inner_ret} ({detail})")
|
||||||
|
print(f" 响应已保存 → {out}")
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -212,11 +212,16 @@ def save_payment_status(path: Path, document: dict, baseline: dict[str, bool], m
|
|||||||
path.write_text(json.dumps(record, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
path.write_text(json.dumps(record, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
def save_payment_meta(path: Path, order: dict[str, str], baseline: dict[str, bool], document: dict) -> None:
|
def save_payment_meta(path: Path, order: dict[str, str], baseline: dict[str, bool],
|
||||||
|
document: dict, portal_serial_no: str = "") -> None:
|
||||||
"""Persist the order identifiers and pre-payment baseline needed by a later check.
|
"""Persist the order identifiers and pre-payment baseline needed by a later check.
|
||||||
|
|
||||||
``check`` only re-reads this file and never touches web_save, so re-running a
|
``check`` only re-reads this file and never touches web_save, so re-running a
|
||||||
check is side-effect free.
|
check is side-effect free.
|
||||||
|
|
||||||
|
The official order list (GetPrivateDomainOrderList) exposes its own ``order_id``
|
||||||
|
which equals web_save's ``portal_serial_no``; save it under ``order_id`` so the
|
||||||
|
completion check can match precisely instead of guessing by "newly completed".
|
||||||
"""
|
"""
|
||||||
from pyvm.order_status import order_ids
|
from pyvm.order_status import order_ids
|
||||||
|
|
||||||
@@ -225,6 +230,7 @@ def save_payment_meta(path: Path, order: dict[str, str], baseline: dict[str, boo
|
|||||||
"token_id": order.get("token_id", ""),
|
"token_id": order.get("token_id", ""),
|
||||||
"transaction_id": order.get("transaction_id", ""),
|
"transaction_id": order.get("transaction_id", ""),
|
||||||
"out_trade_no": order.get("out_trade_no", ""),
|
"out_trade_no": order.get("out_trade_no", ""),
|
||||||
|
"order_id": portal_serial_no,
|
||||||
},
|
},
|
||||||
"baseline_order_states": baseline,
|
"baseline_order_states": baseline,
|
||||||
"baseline_order_ids": sorted(order_ids(document)),
|
"baseline_order_ids": sorted(order_ids(document)),
|
||||||
@@ -295,7 +301,7 @@ def main() -> int:
|
|||||||
parser.add_argument("--wait", type=int, default=12, help="jsdom 等待 DeviceFP 的秒数")
|
parser.add_argument("--wait", type=int, default=12, help="jsdom 等待 DeviceFP 的秒数")
|
||||||
parser.add_argument("--zone-id", default="1", help="所选游戏区服 ID")
|
parser.add_argument("--zone-id", default="1", help="所选游戏区服 ID")
|
||||||
parser.add_argument("--pf", default="", help="所选 Android/iOS 支付平台标识")
|
parser.add_argument("--pf", default="", help="所选 Android/iOS 支付平台标识")
|
||||||
parser.add_argument("--amount-fen", type=int, required=True, help="所选点券的价格,单位分")
|
parser.add_argument("--amount-fen", type=int, default=0, help="所选点券的价格,单位分(check-only 模式不需要)")
|
||||||
parser.add_argument("--dry-run", action="store_true", help="仅拉取页面并生成 DeviceFP,不上报或创建付款码")
|
parser.add_argument("--dry-run", action="store_true", help="仅拉取页面并生成 DeviceFP,不上报或创建付款码")
|
||||||
parser.add_argument("--payment-timeout", type=float, default=300,
|
parser.add_argument("--payment-timeout", type=float, default=300,
|
||||||
help="付款码生成后等待订单完成的最长秒数")
|
help="付款码生成后等待订单完成的最长秒数")
|
||||||
@@ -315,6 +321,8 @@ def main() -> int:
|
|||||||
if args.check_only:
|
if args.check_only:
|
||||||
out_dir.mkdir(parents=True, exist_ok=True)
|
out_dir.mkdir(parents=True, exist_ok=True)
|
||||||
return cmd_check_only(session_path, out_dir)
|
return cmd_check_only(session_path, out_dir)
|
||||||
|
if args.amount_fen <= 0:
|
||||||
|
raise ValueError("充值金额必须为正数")
|
||||||
|
|
||||||
session = load_json(session_path)
|
session = load_json(session_path)
|
||||||
cookies = dict(session.get("cookies", {}))
|
cookies = dict(session.get("cookies", {}))
|
||||||
@@ -420,6 +428,8 @@ def main() -> int:
|
|||||||
if not sign.startswith("weixin://"):
|
if not sign.startswith("weixin://"):
|
||||||
print("[jsdom-pay] web_save 成功,但响应没有微信付款链接")
|
print("[jsdom-pay] web_save 成功,但响应没有微信付款链接")
|
||||||
return 1
|
return 1
|
||||||
|
info = response_json.get("info", {}) or {}
|
||||||
|
portal_serial_no = str(info.get("portal_serial_no", "") or "")
|
||||||
qr_path = Path(args.qr) if args.qr else out_dir / "wechat-pay.png"
|
qr_path = Path(args.qr) if args.qr else out_dir / "wechat-pay.png"
|
||||||
make_qr(sign, qr_path)
|
make_qr(sign, qr_path)
|
||||||
print("[jsdom-pay] 微信付款码已生成")
|
print("[jsdom-pay] 微信付款码已生成")
|
||||||
@@ -435,7 +445,7 @@ def main() -> int:
|
|||||||
baseline_document = {"list": []}
|
baseline_document = {"list": []}
|
||||||
baseline_states = {}
|
baseline_states = {}
|
||||||
print("[jsdom-pay] 记录付款前订单基线与本次订单标识...")
|
print("[jsdom-pay] 记录付款前订单基线与本次订单标识...")
|
||||||
save_payment_meta(out_dir / "payment-meta.json", order, baseline_states, baseline_document)
|
save_payment_meta(out_dir / "payment-meta.json", order, baseline_states, baseline_document, portal_serial_no)
|
||||||
if args.skip_payment_check:
|
if args.skip_payment_check:
|
||||||
print("[jsdom-pay] 已保存基线;后台将单独执行只读到账检测")
|
print("[jsdom-pay] 已保存基线;后台将单独执行只读到账检测")
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@@ -123,10 +123,17 @@ class Cmall:
|
|||||||
def roles(self, zone_id: str) -> list[dict[str, str]]:
|
def roles(self, zone_id: str) -> list[dict[str, str]]:
|
||||||
response = self.query("15", use_currency_offerid="1", zoneid=zone_id)
|
response = self.query("15", use_currency_offerid="1", zoneid=zone_id)
|
||||||
roles = response.get("role_info") or response.get("role_list") or []
|
roles = response.get("role_info") or response.get("role_list") or []
|
||||||
return [{"role_id": str(item.get("role_id", "")),
|
# QQ 平台和平精英的 area 与 zoneid 不同(如 area=2, zoneid=1);
|
||||||
"name": unquote(str(item.get("role_name", ""))),
|
# PlaceOrder 校验角色时需要该分区信息,随角色一并返回。
|
||||||
"ban_status": str(item.get("ban_status", ""))}
|
partition = response.get("partition_info") or {}
|
||||||
for item in roles if isinstance(item, dict) and item.get("role_id")]
|
return [{
|
||||||
|
"role_id": str(item.get("role_id", "")),
|
||||||
|
"name": unquote(str(item.get("role_name", ""))),
|
||||||
|
"ban_status": str(item.get("ban_status", "")),
|
||||||
|
"area": str(partition.get("area", "")),
|
||||||
|
"partition": str(partition.get("partition", "")),
|
||||||
|
"platid": str(partition.get("platid", "")),
|
||||||
|
} for item in roles if isinstance(item, dict) and item.get("role_id")]
|
||||||
|
|
||||||
|
|
||||||
def choose(label: str, options: list[dict], display) -> dict:
|
def choose(label: str, options: list[dict], display) -> dict:
|
||||||
|
|||||||
@@ -242,6 +242,10 @@ def _payment_flow(job_id: str, selection: dict) -> None:
|
|||||||
"--role-id", str(selection["role_id"]), "--role-name", str(selection["role_name"]),
|
"--role-id", str(selection["role_id"]), "--role-name", str(selection["role_name"]),
|
||||||
"--zone-id", str(selection["zone_id"]), "--zone-name", str(selection["zone_name"]),
|
"--zone-id", str(selection["zone_id"]), "--zone-name", str(selection["zone_name"]),
|
||||||
"--output", str(response)]
|
"--output", str(response)]
|
||||||
|
if selection.get("area"):
|
||||||
|
order_cmd.extend(["--area", str(selection["area"])])
|
||||||
|
if selection.get("partition"):
|
||||||
|
order_cmd.extend(["--partition", str(selection["partition"])])
|
||||||
if selection.get("order_pf"):
|
if selection.get("order_pf"):
|
||||||
order_cmd.extend(["--pf", str(selection["order_pf"])])
|
order_cmd.extend(["--pf", str(selection["order_pf"])])
|
||||||
if not _payment_stage(job_id, order_cmd, "order", "正在创建商城订单", "创建商城订单失败"):
|
if not _payment_stage(job_id, order_cmd, "order", "正在创建商城订单", "创建商城订单失败"):
|
||||||
@@ -413,6 +417,7 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
"product_id": product["product_id"], "offer_id": product["offer_id"],
|
"product_id": product["product_id"], "offer_id": product["offer_id"],
|
||||||
"zone_id": zone["zone_id"], "zone_name": zone["name"],
|
"zone_id": zone["zone_id"], "zone_name": zone["name"],
|
||||||
"role_id": role["role_id"], "role_name": role["name"],
|
"role_id": role["role_id"], "role_name": role["name"],
|
||||||
|
"area": role.get("area", ""), "partition": role.get("partition", ""),
|
||||||
"order_pf": selector.PLATFORMS[body["platform"]]["order_pf"],
|
"order_pf": selector.PLATFORMS[body["platform"]]["order_pf"],
|
||||||
}
|
}
|
||||||
_jobs[job_id]["phase"] = "payment"
|
_jobs[job_id]["phase"] = "payment"
|
||||||
|
|||||||
@@ -22,6 +22,15 @@ def _from_ts(value) -> datetime | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _as_utc(value: datetime | None) -> datetime | None:
|
||||||
|
"""DB 时间列为 naive UTC;输出时补 UTC 时区标记,前端才能按本地时区正确显示。"""
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
if value.tzinfo is None:
|
||||||
|
return value.replace(tzinfo=timezone.utc)
|
||||||
|
return value.astimezone(timezone.utc)
|
||||||
|
|
||||||
|
|
||||||
def cleanup_orphan_yyb_tasks(db: Session, message: str) -> int:
|
def cleanup_orphan_yyb_tasks(db: Session, message: str) -> int:
|
||||||
rows = db.query(YybRechargeTask).filter(
|
rows = db.query(YybRechargeTask).filter(
|
||||||
YybRechargeTask.status.in_(["created", "waiting_login", "ready", "running", "ordering", "waiting_payment"])
|
YybRechargeTask.status.in_(["created", "waiting_login", "ready", "running", "ordering", "waiting_payment"])
|
||||||
@@ -85,10 +94,10 @@ def public_task(task: YybRechargeTask, include_qr: bool = True,
|
|||||||
"role_id": task.role_id, "role_name": task.role_name, "status": task.status,
|
"role_id": task.role_id, "role_name": task.role_name, "status": task.status,
|
||||||
"phase": task.phase, "message": task.message, "result": task.result,
|
"phase": task.phase, "message": task.message, "result": task.result,
|
||||||
"created_by": task.created_by, "created_by_username": creator_username,
|
"created_by": task.created_by, "created_by_username": creator_username,
|
||||||
"created_at": task.created_at, "finished_at": task.finished_at,
|
"created_at": _as_utc(task.created_at), "finished_at": _as_utc(task.finished_at),
|
||||||
"payment_started_at": task.payment_started_at,
|
"payment_started_at": _as_utc(task.payment_started_at),
|
||||||
"payment_qr_created_at": task.payment_qr_created_at,
|
"payment_qr_created_at": _as_utc(task.payment_qr_created_at),
|
||||||
"payment_last_checked_at": task.payment_last_checked_at,
|
"payment_last_checked_at": _as_utc(task.payment_last_checked_at),
|
||||||
}
|
}
|
||||||
if task.result:
|
if task.result:
|
||||||
result["login_qr_mime_type"] = task.result.get("login_qr_mime_type", "image/jpeg")
|
result["login_qr_mime_type"] = task.result.get("login_qr_mime_type", "image/jpeg")
|
||||||
|
|||||||
@@ -309,7 +309,17 @@ export default function YybRechargePage() {
|
|||||||
)}
|
)}
|
||||||
{status === 'ordering' && (
|
{status === 'ordering' && (
|
||||||
<Space direction="vertical" size={10} style={{ width: '100%' }}>
|
<Space direction="vertical" size={10} style={{ width: '100%' }}>
|
||||||
<Spin tip={task.message || '正在生成付款码'}><div style={{ height: 40 }} /></Spin>
|
<Alert type="info" showIcon
|
||||||
|
message="正在生成微信付款码"
|
||||||
|
description="下单与付款码生成约需 30~60 秒,请勿关闭页面;生成后二维码会自动显示。" />
|
||||||
|
<div style={{
|
||||||
|
width: 280, height: 280, border: '2px dashed #d9d9d9', borderRadius: 12,
|
||||||
|
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
|
||||||
|
gap: 12, background: '#fafafa',
|
||||||
|
}}>
|
||||||
|
<Spin size="large" />
|
||||||
|
<Text type="secondary">付款码生成中...</Text>
|
||||||
|
</div>
|
||||||
{showLogs && (
|
{showLogs && (
|
||||||
<Collapse
|
<Collapse
|
||||||
activeKey={logsOpen ? ['logs'] : undefined}
|
activeKey={logsOpen ? ['logs'] : undefined}
|
||||||
|
|||||||
Reference in New Issue
Block a user