Files
CheckInApp/backend/workers/check_in_worker.py
T
8a12744 fdc725b893 refactor: v2
backend & frontend
2026-01-01 18:38:21 +08:00

276 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import requests
import json
import time
import os
import logging
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from typing import Dict, Any
from backend.config import settings
from backend.workers.email_notifier import send_success_notification, send_failure_notification
logger = logging.getLogger(__name__)
# Chrome 配置路径 - 从设置中读取
CHROME_BINARY_PATH = settings.CHROME_BINARY_PATH
CHROMEDRIVER_PATH = settings.CHROMEDRIVER_PATH
def get_live_x_api_payload(auth_token: str) -> str:
"""
启动一个临时的无头浏览器会话,获取新鲜的 x-api-request-payload
Args:
auth_token: 用户的 Authorization Token
Returns:
x-api-request-payload 值,失败返回 None
"""
logger.info("正在启动临时浏览器会话以监听网络日志...")
# 根据配置创建 Service
if CHROMEDRIVER_PATH:
service = Service(executable_path=CHROMEDRIVER_PATH)
else:
service = Service() # 使用 Selenium Manager 自动管理
chrome_options = Options()
# 如果配置了 Chrome 路径,则使用配置的路径
if CHROME_BINARY_PATH:
chrome_options.binary_location = CHROME_BINARY_PATH
# 开启性能日志记录功能
logging_prefs = {'performance': 'ALL'}
chrome_options.set_capability('goog:loggingPrefs', logging_prefs)
# Headless 模式配置
user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36"
chrome_options.add_argument(f'user-agent={user_agent}')
chrome_options.add_argument("--headless")
chrome_options.add_argument("--no-sandbox")
chrome_options.add_argument("--disable-dev-shm-usage")
chrome_options.add_argument("--window-size=1920,1080")
chrome_options.add_argument('--ignore-certificate-errors')
chrome_options.add_experimental_option("excludeSwitches", ["enable-automation"])
driver = webdriver.Chrome(service=service, options=chrome_options)
payload_signature = None
try:
# 导航到同源空白页,用于设置 Cookie
driver.get("https://i.jielong.com/my-class")
# 注入长期 Token
driver.add_cookie({
'name': 'token',
'value': auth_token,
'domain': '.jielong.com'
})
# 导航到触发 API 的页面
driver.get("https://i.jielong.com/my-form")
# 等待并捕获 x-api-request-payload
max_wait_time = 20 # 最多等待20秒
start_time = time.time()
found = False
while time.time() - start_time < max_wait_time:
logs = driver.get_log('performance')
for entry in logs:
log = json.loads(entry['message'])['message']
if log['method'] == 'Network.requestWillBeSent':
headers = log.get('params', {}).get('request', {}).get('headers', {})
headers_lower = {k.lower(): v for k, v in headers.items()}
if 'x-api-request-payload' in headers_lower:
payload_signature = headers_lower['x-api-request-payload']
logger.info("成功通过网络日志捕获到现场的 x-api-request-payload")
found = True
break
if found:
break
time.sleep(1)
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}")
debug_screenshot = os.path.join(settings.BASE_DIR, 'payload_debug.png')
driver.save_screenshot(debug_screenshot)
finally:
driver.quit()
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: 错误信息
"""
# 从 payload_config 中提取 Signature 用于日志
try:
payload_dict = json.loads(task.payload_config) if task.payload_config else {}
signature = payload_dict.get('Signature', 'Unknown')
except:
signature = 'Unknown'
logger.info(f"Selenium打卡: 正在为任务 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
}
# 获取 x-api-request-payload
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:
# 使用任务的 payload_config(从模板生成的完整配置,包含 ThreadId)
payload = json.loads(task.payload_config) if task.payload_config else {}
if not payload.get('ThreadId'):
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}")
# 判断响应内容(参考 V1 实现逻辑)
# 使用用户账户的邮箱,而不是任务的邮箱
email = task.user.email if task.user else None
# 情况1: 明确包含"打卡成功" → 成功
if "打卡成功" in response_text:
logger.info(f"✅ 检测到成功关键字 '打卡成功',打卡成功")
if email:
send_success_notification(email)
return {
"success": True,
"status": "success",
"response_text": response_text,
"error_message": ""
}
# 情况2: 不在打卡时间范围 → 标记为时间范围外
# 支持多种匹配方式:直接文本匹配、JSON Data 字段、Description 字段
elif ("不在打卡时间范围" in response_text or
"不在打卡时间" in response_text or
'"Data":"不在打卡时间范围"' in response_text or
'"Description":"不在打卡时间范围"' in response_text):
logger.warning(f"⏰ 检测到'不在打卡时间范围',打卡时间不符")
return {
"success": False,
"status": "out_of_time",
"response_text": response_text,
"error_message": "不在打卡时间范围内"
}
# 情况3: Token 失效的特征标识 → 失败
elif ("登录" in response_text):
logger.warning(f"⚠️ 检测到登录失败关键字,Token 可能已失效")
if email:
send_failure_notification(email)
return {
"success": False,
"status": "failure",
"response_text": response_text,
"error_message": "Token 已失效,需要重新授权"
}
# 情况4: 其他响应 → 需要人工确认(标记为异常)
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)
}