mirror of
https://github.com/Cccc-owo/CheckInApp.git
synced 2026-06-17 14:06:28 +00:00
d811c20932
BREAKING CHANGE: backend now requires Python 3.12 or newer.
315 lines
11 KiB
Python
315 lines
11 KiB
Python
from __future__ import annotations
|
||
|
||
import json
|
||
import logging
|
||
import requests
|
||
import time
|
||
from typing import Dict, Any
|
||
|
||
from playwright.sync_api import sync_playwright
|
||
|
||
from backend.config import settings
|
||
from backend.workers.browser_automation import (
|
||
PlaywrightLaunchConfig,
|
||
extract_payload_header,
|
||
save_page_debug_artifacts,
|
||
)
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
BASE_DIR = settings.BASE_DIR
|
||
DEBUG_SCREENSHOT_PATH = BASE_DIR / "payload_debug.png"
|
||
DEBUG_PAGE_SOURCE_PATH = BASE_DIR / "payload_debug_page_source.html"
|
||
|
||
|
||
def get_browser_config() -> PlaywrightLaunchConfig:
|
||
"""获取 Playwright 浏览器配置(从 settings 读取)"""
|
||
return PlaywrightLaunchConfig(executable_path=settings.BROWSER_EXECUTABLE_PATH)
|
||
|
||
|
||
def get_live_x_api_payload(auth_token: str) -> str | None:
|
||
"""
|
||
启动一个临时的无头浏览器会话,获取新鲜的 x-api-request-payload
|
||
|
||
Args:
|
||
auth_token: 用户的 Authorization Token
|
||
|
||
Returns:
|
||
x-api-request-payload 值,失败返回 None
|
||
"""
|
||
logger.info("正在启动临时 Playwright 会话以监听网络请求...")
|
||
|
||
browser = None
|
||
context = None
|
||
page = None
|
||
payload_signature = None
|
||
|
||
playwright = None
|
||
try:
|
||
browser_config = get_browser_config()
|
||
|
||
playwright = sync_playwright().start()
|
||
browser = playwright.chromium.launch(**browser_config.to_launch_kwargs())
|
||
context = browser.new_context(**browser_config.to_context_kwargs())
|
||
page = context.new_page()
|
||
|
||
def on_request(request) -> None:
|
||
nonlocal payload_signature
|
||
if payload_signature:
|
||
return
|
||
|
||
payload = extract_payload_header(request.headers)
|
||
if payload:
|
||
payload_signature = payload
|
||
logger.info("成功通过 Playwright 捕获到现场的 x-api-request-payload!")
|
||
|
||
page.on("request", on_request)
|
||
|
||
page.goto("https://i.jielong.com/my-class", wait_until="domcontentloaded", timeout=60000)
|
||
context.add_cookies(
|
||
[
|
||
{
|
||
"name": "token",
|
||
"value": auth_token,
|
||
"domain": ".jielong.com",
|
||
"path": "/",
|
||
}
|
||
]
|
||
)
|
||
|
||
page.goto("https://i.jielong.com/my-form", wait_until="domcontentloaded", timeout=60000)
|
||
|
||
max_wait_time = 20
|
||
start_time = time.time()
|
||
while time.time() - start_time < max_wait_time:
|
||
if payload_signature:
|
||
break
|
||
page.wait_for_timeout(500)
|
||
|
||
if not payload_signature:
|
||
raise Exception(
|
||
f"在 {max_wait_time} 秒内未能通过网络请求捕获到 x-api-request-payload。"
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"获取现场 x-api-request-payload 时失败: {e}")
|
||
try:
|
||
if page:
|
||
save_page_debug_artifacts(page, DEBUG_SCREENSHOT_PATH, DEBUG_PAGE_SOURCE_PATH)
|
||
except Exception as screenshot_error:
|
||
logger.warning(f"保存调试截图失败: {screenshot_error}")
|
||
|
||
finally:
|
||
if page:
|
||
try:
|
||
page.close()
|
||
except Exception:
|
||
pass
|
||
if context:
|
||
try:
|
||
context.close()
|
||
except Exception:
|
||
pass
|
||
if browser:
|
||
try:
|
||
browser.close()
|
||
except Exception as e:
|
||
logger.warning(f"关闭 Playwright 浏览器时出现警告: {e}")
|
||
if playwright:
|
||
try:
|
||
playwright.stop()
|
||
except Exception as e:
|
||
logger.warning(f"关闭 Playwright runtime 时出现警告: {e}")
|
||
|
||
return payload_signature
|
||
|
||
|
||
def perform_check_in(task, user_token: str) -> Dict[str, Any]:
|
||
"""
|
||
执行打卡任务
|
||
|
||
Args:
|
||
task: CheckInTask 对象,包含打卡任务配置
|
||
user_token: 用户的 Authorization Token(从 task.user.authorization 获取)
|
||
|
||
Returns:
|
||
打卡结果字典:
|
||
- success: 是否成功
|
||
- status: 状态 (success/failure)
|
||
- response_text: 响应文本
|
||
- error_message: 错误信息
|
||
"""
|
||
from backend.utils.json_helpers import safe_parse_payload, extract_signature
|
||
|
||
payload_dict = safe_parse_payload(task.payload_config)
|
||
signature = extract_signature(task.payload_config) or "Unknown"
|
||
|
||
logger.info(f"Playwright打卡: 正在为任务 ID: {task.id} (Signature: {signature}) 执行打卡...")
|
||
|
||
if not user_token:
|
||
error_msg = f"任务 ID: {task.id} (Signature: {signature}) 的 Token 为空,跳过。"
|
||
logger.error(error_msg)
|
||
return {
|
||
"success": False,
|
||
"status": "failure",
|
||
"response_text": "",
|
||
"error_message": error_msg,
|
||
}
|
||
|
||
payload_signature = get_live_x_api_payload(user_token)
|
||
if not payload_signature:
|
||
error_msg = f"任务 ID: {task.id} (Signature: {signature}) 未能获取到现场签名,打卡中止。"
|
||
logger.error(error_msg)
|
||
return {
|
||
"success": False,
|
||
"status": "failure",
|
||
"response_text": "",
|
||
"error_message": error_msg,
|
||
}
|
||
|
||
try:
|
||
from backend.utils.json_helpers import safe_parse_payload, extract_thread_id
|
||
|
||
payload = safe_parse_payload(task.payload_config)
|
||
thread_id = extract_thread_id(task.payload_config)
|
||
|
||
if not thread_id:
|
||
error_msg = f"任务 ID: {task.id} 的 payload_config 缺少 ThreadId"
|
||
logger.error(error_msg)
|
||
return {
|
||
"success": False,
|
||
"status": "failure",
|
||
"response_text": "",
|
||
"error_message": error_msg,
|
||
}
|
||
|
||
headers = {
|
||
"User-Agent": "Mozilla%2f5.0+(Linux%3b+Android+16%3b+wv)+AppleWebKit%2f537.36+(KHTML%2c+like+Gecko)+Chrome%2f142.0.0.0+Safari%2f537.36+QQ%2f9.2.30.31620+QQ%2fMiniApp",
|
||
"Accept-Encoding": "gzip",
|
||
"Content-Type": "application/json",
|
||
"authorization": f"Bearer {user_token}",
|
||
"x-api-request-referer": "https://appservice.qq.com/1110276759",
|
||
"x-api-request-payload": payload_signature,
|
||
"referer": "https://appservice.qq.com/1110276759/8.10.1.7/page-frame.html",
|
||
"platform": "qq",
|
||
"x-api-request-mode": "cors",
|
||
}
|
||
|
||
url = "https://api.jielong.com/api/CheckIn/EditRecord"
|
||
|
||
payload_json = json.dumps(payload, ensure_ascii=False)
|
||
logger.info(f"📤 打卡请求详情 - 任务 ID: {task.id} (Signature: {signature})")
|
||
logger.info(f"📍 URL: {url}")
|
||
logger.info(f"📦 Payload: {payload_json}")
|
||
logger.info(f"🔑 x-api-request-payload: {payload_signature[:50]}...")
|
||
|
||
response = requests.post(url, data=payload_json, headers=headers)
|
||
response.raise_for_status()
|
||
response_text = response.text
|
||
|
||
logger.info(
|
||
f"✉️ 任务 ID: {task.id} (Signature: {signature}) 打卡请求完成!响应: {response_text}"
|
||
)
|
||
|
||
if "打卡成功" in response_text:
|
||
logger.info(f"✅ 检测到成功关键字 '打卡成功',打卡成功")
|
||
if task.user and task.user.email:
|
||
try:
|
||
from backend.services.email_service import EmailService
|
||
|
||
task_info = {
|
||
"thread_id": payload.get("ThreadId", "未知"),
|
||
"name": getattr(task, "name", "打卡任务"),
|
||
}
|
||
EmailService.notify_check_in_result(task.user, task_info, True, "打卡成功")
|
||
except Exception as e:
|
||
logger.error(f"发送打卡成功邮件失败: {e}")
|
||
|
||
return {
|
||
"success": True,
|
||
"status": "success",
|
||
"response_text": response_text,
|
||
"error_message": "",
|
||
}
|
||
|
||
elif (
|
||
"已被提交" in response_text
|
||
or "已经打卡" in response_text
|
||
or "重复提交" in response_text
|
||
):
|
||
logger.info(f"✅ 检测到'已被提交',本次打卡已完成(重复提交,不发送邮件)")
|
||
return {
|
||
"success": True,
|
||
"status": "success",
|
||
"response_text": response_text,
|
||
"error_message": "",
|
||
}
|
||
|
||
elif "不在打卡时间范围" in response_text or "不在打卡时间" in response_text:
|
||
logger.warning(f"⏰ 检测到'不在打卡时间范围',打卡时间不符")
|
||
return {
|
||
"success": False,
|
||
"status": "out_of_time",
|
||
"response_text": response_text,
|
||
"error_message": "不在打卡时间范围内",
|
||
}
|
||
|
||
elif (
|
||
"登录" in response_text
|
||
or "授权" in response_text
|
||
or "未登录" in response_text
|
||
or "token" in response_text.lower()
|
||
or "Unauthorized" in response_text
|
||
or response.status_code == 401
|
||
):
|
||
logger.warning(f"⚠️ 检测到Token失效特征,Token 可能已失效")
|
||
if task.user and task.user.email:
|
||
try:
|
||
from backend.services.email_service import EmailService
|
||
from backend.utils.json_helpers import build_task_info
|
||
|
||
task_info = build_task_info(task)
|
||
|
||
EmailService.notify_check_in_result(
|
||
task.user, task_info, False, "Token 已失效,需要重新授权"
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"发送打卡失败邮件失败: {e}")
|
||
|
||
return {
|
||
"success": False,
|
||
"status": "token_expired",
|
||
"response_text": response_text,
|
||
"error_message": "Token 已失效,需要重新授权",
|
||
}
|
||
|
||
else:
|
||
logger.warning(f"⚠️ 未识别的响应内容,请检查: {response_text[:200]}...")
|
||
return {
|
||
"success": False,
|
||
"status": "unknown",
|
||
"response_text": response_text,
|
||
"error_message": "未识别的响应,请人工确认",
|
||
}
|
||
|
||
except requests.exceptions.RequestException as e:
|
||
error_msg = f"为任务 ID: {task.id} (Signature: {signature}) 打卡时请求失败: {e}"
|
||
logger.error(error_msg)
|
||
|
||
response_text = ""
|
||
if e.response is not None:
|
||
response_text = e.response.text
|
||
logger.error(f"响应状态码: {e.response.status_code}, 响应内容: {response_text}")
|
||
|
||
return {
|
||
"success": False,
|
||
"status": "failure",
|
||
"response_text": response_text,
|
||
"error_message": str(e),
|
||
}
|
||
|
||
except Exception as e:
|
||
error_msg = f"为任务 ID: {task.id} (Signature: {signature}) 打卡时发生未知错误: {e}"
|
||
logger.error(error_msg)
|
||
return {"success": False, "status": "failure", "response_text": "", "error_message": str(e)}
|