mirror of
https://github.com/Cccc-owo/CheckInApp.git
synced 2026-06-17 05:56:29 +00:00
5430dc03f4
- Fix emailing. - Updated manage.sh to enhance command handling and service management for backend and frontend. - Introduced utility functions for better code organization and readability. - Added support for checking Node.js version and ensuring the virtual environment is set up. - Implemented improved logging with color-coded output for better visibility. - Created a new nginx.conf.example file for easy Nginx configuration setup for the application.
286 lines
11 KiB
Python
286 lines
11 KiB
Python
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: 已经提交过了(重复提交)→ 视为成功,但不发送邮件
|
||
# 匹配 "已被提交" 或 "已经打卡"
|
||
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": ""
|
||
}
|
||
|
||
# 情况3: 不在打卡时间范围 → 标记为时间范围外
|
||
# 匹配 Data 或 Description 中的内容
|
||
elif ("不在打卡时间范围" in response_text or
|
||
"不在打卡时间" in response_text):
|
||
logger.warning(f"⏰ 检测到'不在打卡时间范围',打卡时间不符")
|
||
return {
|
||
"success": False,
|
||
"status": "out_of_time",
|
||
"response_text": response_text,
|
||
"error_message": "不在打卡时间范围内"
|
||
}
|
||
|
||
# 情况4: 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 已失效,需要重新授权"
|
||
}
|
||
|
||
# 情况5: 其他响应 → 需要人工确认(标记为异常)
|
||
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)
|
||
}
|