山东省继续医学教育公需科目学习平台sdcme.net.cn自动化学习脚本python版

由 GWBLog 发布

        最近一段时间以来,sdcme.net.cn山东省继续医学教育服务平台开始了学习,但是网站三天两头维护,不维护的时间还卡的要死。

        本人实在无奈,委托AI帮我写了这段自动化学习脚本。需要的朋友可以自取。但是关于动态验证码的识别接口为本人自建,服务器性能有限,故不能随时免费提供。需要的可以联系我索要源码。下方为自动化学习脚本源代码(不带记录核查功能)记录核查版本的已经封存,需要的可以联系。

MAX_CONCURRENT_USERS = 25
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
山东医学教育平台自动化学习系统
"""

import os
import json
import time
import random
import string
import logging
import sqlite3
import threading
from datetime import datetime
from typing import Dict, List, Optional, Tuple
from urllib.parse import quote

import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

# ==================== 配置区 ====================
class Config:
    BASE_URL = "https://course.sdcme.net.cn"

    # --- OCR 接口配置区 ---
    OCR_URL = "http://您自己的ocr识别接口"
    # (注:卡密和账号名全部改为动态传入,不在代码里写死任何校验信息)
    # ----------------------

    HEARTBEAT_INTERVAL = 60
    ROLL_CALL_DELAY = (5.0, 15.0)
    REQUEST_TIMEOUT = 60
    MAX_RETRIES = 5
    RETRY_DELAY = 5
    MIN_WAIT_TIME = 180

    BASE_DIR = os.path.dirname(os.path.abspath(__file__))
    SKELETON_FILE = os.path.join(BASE_DIR, "global_skeleton.json")
    COURSE_LEARN_ORDER = ['18', '12', '13'] 

SKELETON_LOCK = threading.Lock()

# ==================== 日志配置(双通道分离) ====================
logger = logging.getLogger('sdcme_learner')
logger.setLevel(logging.DEBUG) 
logger.handlers.clear()

console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
console_formatter = logging.Formatter('%(asctime)s [%(levelname)s] %(message)s', datefmt='%H:%M:%S')
console_handler.setFormatter(console_formatter)
logger.addHandler(console_handler)

log_file_path = os.path.join(Config.BASE_DIR, 'sdcme.log')
file_handler = logging.FileHandler(log_file_path, encoding='utf-8', mode='a')
file_handler.setLevel(logging.DEBUG)
file_formatter = logging.Formatter('%(asctime)s [%(levelname)s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
file_handler.setFormatter(file_formatter)
logger.addHandler(file_handler)

logging.getLogger('urllib3').setLevel(logging.CRITICAL)

# ==================== 数据库管理器 ====================
class DBManager:
    def __init__(self, username: str):
        self.db_file = os.path.join(Config.BASE_DIR, f"{username}_sdcme.db")
        self.conn = sqlite3.connect(self.db_file, check_same_thread=False)
        try:
            self.conn.execute('PRAGMA journal_mode=DELETE;')
        except:
            pass
        self._init_tables()

    def _init_tables(self):
        cursor = self.conn.cursor()
        cursor.execute('''CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password TEXT NOT NULL, cookies TEXT, cookies_expire DATETIME, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP)''')
        cursor.execute('''CREATE TABLE IF NOT EXISTS global_courses (course_id TEXT PRIMARY KEY, title TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP)''')
        cursor.execute('''CREATE TABLE IF NOT EXISTS global_videos (video_id TEXT PRIMARY KEY, course_id TEXT, title TEXT, duration INTEGER, sort_index INTEGER, FOREIGN KEY(course_id) REFERENCES global_courses(course_id))''')
        cursor.execute('''CREATE TABLE IF NOT EXISTS video_progress (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, video_id TEXT NOT NULL, course_id TEXT, progress INTEGER DEFAULT 0, has_checkin BOOLEAN DEFAULT 0, has_anomaly BOOLEAN DEFAULT 0, status TEXT DEFAULT 'pending', updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY(user_id) REFERENCES users(id), FOREIGN KEY(video_id) REFERENCES global_videos(video_id), UNIQUE(user_id, video_id))''')
        cursor.execute('''CREATE TABLE IF NOT EXISTS heartbeat_trace (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, video_id TEXT, request TEXT, response TEXT, has_anomaly BOOLEAN DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY(user_id) REFERENCES users(id))''')
        self.conn.commit()

    def add_user(self, username: str, password: str) -> int:
        cursor = self.conn.cursor()
        try:
            cursor.execute('INSERT INTO users (username, password) VALUES (?, ?)', (username, password))
            self.conn.commit()
            return cursor.lastrowid
        except sqlite3.IntegrityError:
            cursor.execute('SELECT id FROM users WHERE username = ?', (username,))
            return cursor.fetchone()[0]

    def get_user(self, username: str) -> Optional[Dict]:
        cursor = self.conn.cursor()
        cursor.execute('SELECT * FROM users WHERE username = ?', (username,))
        result = cursor.fetchone()
        if result:
            return {'id': result[0], 'username': result[1], 'password': result[2], 'cookies': result[3]}
        return None

    def update_user_cookies(self, user_id: int, cookies: str, expire_hours: int = 24):
        expire_time = datetime.now().timestamp() + expire_hours * 3600
        cursor = self.conn.cursor()
        cursor.execute('UPDATE users SET cookies = ?, cookies_expire = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', (cookies, expire_time, user_id))
        self.conn.commit()

    def check_global_skeleton_exists(self) -> bool:
        cursor = self.conn.cursor()
        cursor.execute('SELECT COUNT(*) FROM global_videos')
        return cursor.fetchone()[0] > 0

    def save_global_skeleton(self, course_id: str, course_title: str, videos: List[Dict]):
        cursor = self.conn.cursor()
        cursor.execute('INSERT OR IGNORE INTO global_courses (course_id, title) VALUES (?, ?)', (course_id, course_title))
        for idx, v in enumerate(videos):
            cursor.execute('INSERT OR IGNORE INTO global_videos (video_id, course_id, title, duration, sort_index) VALUES (?, ?, ?, ?, ?)', 
                           (v['videoId'], course_id, v['title'], v['duration'], idx))
        self.conn.commit()

    def get_all_global_video_ids(self) -> List[str]:
        cursor = self.conn.cursor()
        cursor.execute('SELECT video_id FROM global_videos')
        return [row[0] for row in cursor.fetchall()]

    def get_all_global_videos_info(self) -> List[Tuple[str, str]]:
        cursor = self.conn.cursor()
        cursor.execute('SELECT video_id, course_id FROM global_videos')
        return cursor.fetchall()

    def save_video_progress(self, user_id: int, video_id: str, course_id: str, progress: int):
        status = 'completed' if progress >= 100 else 'pending'
        cursor = self.conn.cursor()
        cursor.execute('''INSERT OR REPLACE INTO video_progress (user_id, video_id, course_id, progress, status, updated_at) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)''', (user_id, video_id, course_id, progress, status))
        self.conn.commit()

    def get_pending_tasks_ordered(self, user_id: int, order_list: List[str]) -> List[Dict]:
        cursor = self.conn.cursor()
        cursor.execute('''
            SELECT p.video_id, p.course_id, p.progress, v.title, v.duration, v.sort_index
            FROM video_progress p
            JOIN global_videos v ON p.video_id = v.video_id
            WHERE p.user_id = ? AND p.progress < 100
        ''', (user_id,))

        tasks = []
        for row in cursor.fetchall():
            tasks.append({
                'video_id': row[0], 'course_id': str(row[1]), 'progress': row[2],
                'title': row[3], 'duration': row[4], 'sort_index': row[5]
            })

        def sort_key(task):
            try:
                c_order = order_list.index(task['course_id'])
            except ValueError:
                c_order = 9999
            return (c_order, task['sort_index'])

        tasks.sort(key=sort_key)
        return tasks

    def get_user_videos(self, user_id: int) -> List[Dict]:
        cursor = self.conn.cursor()
        cursor.execute('SELECT * FROM video_progress WHERE user_id = ?', (user_id,))
        results = []
        for row in cursor.fetchall():
            results.append({'video_id': row[2], 'title': row[3], 'duration': row[7], 'progress': row[6]})
        return results

    def save_heartbeat(self, user_id: int, video_id: str, request: Dict, response: Dict):
        cursor = self.conn.cursor()
        cursor.execute('INSERT INTO heartbeat_trace (user_id, video_id, request, response, has_anomaly) VALUES (?, ?, ?, ?, ?)', (user_id, video_id, json.dumps(request), json.dumps(response), False))
        self.conn.commit()

def generate_session_id(length: int = 8) -> str:
    return ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(length))

def get_timestamp() -> int:
    return int(time.time() * 1000)

# ==================== 学习系统类 ====================
class SDCMEEAutoLearn:
    def __init__(self, username: str, password: str, token: str):
        self.username = username
        self.password = password
        self.token = token  # 接收调度器传来的专属卡密
        self.db = DBManager(self.username)
        self.user_id = self.db.add_user(username, password)
        self.has_local_cookies = False
        self.session = self._create_session()
        self.course_tree_cached = False
        logger.info(f"[{self.username}] 系统初始化完成,专属数据库 {self.username}_sdcme.db 就绪。")

    def _create_session(self) -> requests.Session:
        session = requests.Session()
        session.headers.update({
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36",
            "Referer": f"{Config.BASE_URL}/"
        })

        retry_strategy = Retry(total=Config.MAX_RETRIES, backoff_factor=1, status_forcelist=[429, 500, 502, 503, 504])
        adapter = HTTPAdapter(max_retries=retry_strategy)
        session.mount("http://", adapter)
        session.mount("https://", adapter)

        user = self.db.get_user(self.username)
        if user and user.get('cookies'):
            try:
                cookies_dict = json.loads(user['cookies'])
                if cookies_dict and isinstance(cookies_dict, dict):
                    for name, value in cookies_dict.items():
                        session.cookies.set(name, value, domain=".sdcme.net.cn")
                    self.has_local_cookies = True
                    logger.info(f"[{self.username}] 已从专属数据库加载历史Cookie。")
            except Exception as e:
                logger.warning(f"[{self.username}] 历史Cookie解析失败: {e}")
        return session

    def _save_cookies(self):
        try:
            cookies_dict = self.session.cookies.get_dict()
            self.db.update_user_cookies(self.user_id, json.dumps(cookies_dict))
        except Exception as e:
            logger.error(f"[{self.username}] 保存Cookie失败: {e}")

    def _check_login_status(self) -> bool:
        if not self.has_local_cookies:
            return False
        logger.info(f"[{self.username}] 正在验证本地凭证是否过期...")
        try:
            req_start = time.time()
            resp = self.session.get(f"{Config.BASE_URL}/", timeout=Config.REQUEST_TIMEOUT, allow_redirects=False)
            cost = round(time.time() - req_start, 2)
            if resp.status_code == 200 and "登录" not in resp.text[:2000]:
                logger.info(f"[{self.username}] 凭证有效校验完成 (耗时: {cost}s)")
                return True
            return False
        except Exception:
            return False

    def _retry_on_failure(self, func, *args, **kwargs):
        for attempt in range(Config.MAX_RETRIES):
            try:
                return func(*args, **kwargs)
            except (requests.exceptions.RequestException, requests.exceptions.JSONDecodeError) as e:
                wait_time = min(Config.RETRY_DELAY * (attempt + 1) * 5, Config.MIN_WAIT_TIME)

                err_str = str(e)
                if "Timeout" in err_str or "timed out" in err_str:
                    clean_msg = "网络连接超时,平台服务器卡顿"
                elif "10061" in err_str or "积极拒绝" in err_str:
                    clean_msg = "平台服务器宕机或拒绝连接"
                elif "502" in err_str or "503" in err_str or "504" in err_str:
                    clean_msg = f"平台网关异常 ({err_str[:15]}...)"
                elif "Expecting value" in err_str:
                    clean_msg = "接口返回了空白或无效数据"
                else:
                    clean_msg = "网络请求波动"

                logger.warning(f"[{self.username}] ⚡ {clean_msg},等待 {wait_time}秒后重试...")
                logger.debug(f"[{self.username}] 详细报错追踪: {err_str}", exc_info=True)

                time.sleep(wait_time)

        raise Exception(f"[{self.username}] 多次重试后仍然失败,终止当前请求。")

    def login(self) -> bool:
        logger.info(f"[{self.username}] 开始执行账号密码登录流程...")
        for attempt in range(Config.MAX_RETRIES):
            try:
                session_id = generate_session_id()
                captcha_url = f"{Config.BASE_URL}/login/captcha?sessionId={session_id}&t={get_timestamp()}"
                response = self.session.get(captcha_url, timeout=Config.REQUEST_TIMEOUT)

                # 【核心修复】:对中文账号进行安全的 URL 编码,防止 Header 报 latin-1 错误!
                ocr_headers = {
                    "X-Token": self.token,
                    "X-User-ID": quote(self.username)  
                }

                files = {'file': ('captcha.gif', response.content, 'image/gif')}

                try:
                    ocr_resp = requests.post(Config.OCR_URL, files=files, headers=ocr_headers, timeout=Config.REQUEST_TIMEOUT)
                    if ocr_resp.status_code == 200:
                        captcha_code = ocr_resp.json().get("result", "")
                    else:
                        logger.warning(f"[{self.username}] OCR拦截或失败 | 状态码: {ocr_resp.status_code} | 详情: {ocr_resp.text}")
                        time.sleep(Config.RETRY_DELAY)
                        continue
                except Exception as e:
                    logger.error(f"[{self.username}] OCR网络请求异常: {e}")
                    time.sleep(Config.RETRY_DELAY)
                    continue

                if not captcha_code:
                    time.sleep(Config.RETRY_DELAY)
                    continue

                logger.info(f"[{self.username}] 登录验证码识别结果: 【{captcha_code}】")

                login_data = {'username': self.username, 'password': self.password, 'captchaCode': captcha_code, 'captchaSessionId': session_id}
                headers = {'X-Requested-With': 'XMLHttpRequest', 'Content-Type': 'application/json'}

                req_start = time.time()
                response = self.session.post(f"{Config.BASE_URL}/login", json=login_data, headers=headers, timeout=Config.REQUEST_TIMEOUT)
                cost = round(time.time() - req_start, 2)
                result = response.json()

                if result.get('success') or result.get('code') == 200:
                    self._save_cookies()
                    logger.info(f"[{self.username}] ✓ 登录成功并更新本地Cookie库!(耗时: {cost}s)")
                    return True
                time.sleep(Config.RETRY_DELAY)
            except Exception as e:
                pass
        return False

    def confirm_agreement(self) -> bool:
        try:
            self.session.get(f"{Config.BASE_URL}/agreement", timeout=Config.REQUEST_TIMEOUT)
            return True
        except:
            return False

    def get_course_categories(self) -> List[Dict]:
        def do_req():
            response = self.session.get(f"{Config.BASE_URL}/courses", timeout=Config.REQUEST_TIMEOUT)
            response.raise_for_status()
            return response.json().get('data', [])
        return self._retry_on_failure(do_req)

    def get_course_videos(self, course_id: str) -> List[Dict]:
        def do_req():
            response = self.session.get(f"{Config.BASE_URL}/coursewares/course/{course_id}", timeout=Config.REQUEST_TIMEOUT)
            response.raise_for_status()
            return response.json().get('data', [])
        return self._retry_on_failure(do_req)

    def get_batch_progress(self, video_ids: List[str]) -> Dict:
        if not video_ids: 
            return {}

        chunk_size = 30 
        all_progress_data = {}

        for i in range(0, len(video_ids), chunk_size):
            chunk = video_ids[i:i + chunk_size]

            def do_req():
                url = f"{Config.BASE_URL}/video/play/progress/batch?videoIds={','.join(chunk)}"
                response = self.session.get(url, timeout=Config.REQUEST_TIMEOUT)
                response.raise_for_status()
                return response.json()

            try:
                result = self._retry_on_failure(do_req)
                chunk_data = result.get('data', {}) if isinstance(result, dict) else {}
                if isinstance(chunk_data, dict):
                    all_progress_data.update(chunk_data)
            except Exception as e:
                logger.error(f"[{self.username}] 批量获取进度(部分批次)失败,将跳过本批次。")
                logger.debug(f"[{self.username}] 批次获取失败详情: {e}", exc_info=True)

        return {'data': all_progress_data}

    def load_or_fetch_skeleton(self):
        if self.db.check_global_skeleton_exists():
            return

        with SKELETON_LOCK:
            if os.path.exists(Config.SKELETON_FILE):
                try:
                    with open(Config.SKELETON_FILE, 'r', encoding='utf-8') as f:
                        skeleton_data = json.load(f)
                    logger.info(f"[{self.username}] 🌍 检测到共享 JSON 图纸,直接复用本地公共骨架...")
                except Exception:
                    skeleton_data = []
            else:
                logger.info(f"[{self.username}] ✨ 暂无公共图纸!作为开拓者,开始向云端拉取全量课程骨架...")
                courses = self.get_course_categories()
                skeleton_data = []
                total_saved = 0
                for course in courses:
                    course_id = str(course.get('id', ''))
                    course_title = course.get('title', '未知')
                    videos = self.get_course_videos(course_id)
                    if not videos: continue

                    v_list = []
                    for v in videos:
                        if v.get('fileName'):
                            v_list.append({'videoId': v.get('fileName'), 'title': v.get('title', v.get('name')), 'duration': v.get('duration', 0)})
                    skeleton_data.append({'course_id': course_id, 'course_title': course_title, 'videos': v_list})
                    total_saved += len(v_list)

                with open(Config.SKELETON_FILE, 'w', encoding='utf-8') as f:
                    json.dump(skeleton_data, f, ensure_ascii=False, indent=2)
                logger.info(f"[{self.username}] 🌍 共享图纸建立完成!(共 {total_saved} 个视频)")

        for course_data in skeleton_data:
            self.db.save_global_skeleton(course_data['course_id'], course_data['course_title'], course_data['videos'])

    def sync_global_and_personal_progress(self):
        self.confirm_agreement()
        self.load_or_fetch_skeleton()

        global_video_ids = self.db.get_all_global_video_ids()
        if not global_video_ids:
            return

        logger.info(f"[{self.username}] 正在向云端发包对账最新进度 (分批请求)...")
        req_start = time.time()
        progress_map = self.get_batch_progress(global_video_ids)
        cost = round(time.time() - req_start, 2)

        actual_progress_map = progress_map.get('data', progress_map) if isinstance(progress_map, dict) else {}

        if not actual_progress_map:
            logger.warning(f"[{self.username}] 警告:未获取到有效的进度字典。")
            return

        global_v_data = self.db.get_all_global_videos_info()

        for row in global_v_data:
            vid, cid = row[0], row[1]
            prog = actual_progress_map.get(vid, 0)
            self.db.save_video_progress(self.user_id, vid, cid, prog)

        logger.info(f"[{self.username}] ✓ 个人独立进度同步完成!(接口耗时: {cost}s)")

    def _build_play_referer(self, video_id: str, title: str) -> str:
        encoded_title = quote(title, safe='')
        return f"https://course.sdcme.net.cn/play/index.html?vid={video_id}&title={encoded_title}"

    def init_play_session(self, video_id: str, title: str) -> bool:
        referer = self._build_play_referer(video_id, title)
        self.session.headers.update({'Referer': referer})

        def do_init():
            self.session.get(f"{Config.BASE_URL}/playapi/me", timeout=Config.REQUEST_TIMEOUT).raise_for_status()
            self.session.get(f"{Config.BASE_URL}/playapi/vod/playauth", params={'videoId': video_id}, timeout=Config.REQUEST_TIMEOUT).raise_for_status()
            self.session.get(f"{Config.BASE_URL}/playapi/vod/progress", params={'videoId': video_id}, timeout=Config.REQUEST_TIMEOUT).raise_for_status()
            return True
        try:
            return self._retry_on_failure(do_init)
        except Exception:
            return False

    def send_heartbeat(self, video_id: str, progress_seconds: int, duration_seconds: int) -> Dict:
        def do_req():
            payload = {'videoId': video_id, 'progressSeconds': progress_seconds, 'durationSeconds': duration_seconds}
            response = self.session.post(f"{Config.BASE_URL}/playapi/vod/progress", json=payload, timeout=Config.REQUEST_TIMEOUT)
            response.raise_for_status()
            return payload, response.json()

        payload, result = self._retry_on_failure(do_req)
        self.db.save_heartbeat(self.user_id, video_id, payload, result)
        return result

    def handle_roll_call(self, video_id: str, command_id: str) -> bool:
        delay = random.uniform(*Config.ROLL_CALL_DELAY)
        logger.info(f"[{self.username}] 模拟点名反应延迟: {delay:.1f}s")
        time.sleep(delay)
        def do_req():
            response = self.session.post(f"{Config.BASE_URL}/playapi/vod/roll-call/checkin", json={'videoId': video_id, 'commandId': command_id}, timeout=Config.REQUEST_TIMEOUT)
            return response.json()
        try:
            req_start = time.time()
            result = self._retry_on_failure(do_req)
            cost = round(time.time() - req_start, 2)
            res_bool = bool(result.get('checkedIn'))
            logger.info(f"[{self.username}] 点名提交反馈: {res_bool} (耗时: {cost}s)")
            return res_bool
        except:
            return False

    def play_video(self, video: Dict) -> bool:
        video_id, title, duration = video['video_id'], video['title'], video['duration']
        logger.info(f"[{self.username}] ▶ 开始学习: [课程:{video['course_id']}] {title} ({duration}秒)")

        if not self.init_play_session(video_id, title): return False
        current_seconds = video.get('progress', 0) * duration // 100

        while current_seconds < duration:
            next_seconds = min(current_seconds + Config.HEARTBEAT_INTERVAL, duration)
            try:
                req_start = time.time()
                result = self.send_heartbeat(video_id, next_seconds, duration)
                cost = round(time.time() - req_start, 2)

                if result.get('command') == 'ROLL_CALL':
                    logger.info(f"[{self.username}] >>> 捕获到随机点名指令 (ROLL_CALL) <<<")
                    if not self.handle_roll_call(video_id, result.get('commandId')): return False

                progress_percent = int(next_seconds * 100 / duration)
                logger.info(f"[{self.username}] 进度: {progress_percent}% ({next_seconds}/{duration}秒) - 心跳发送成功 (耗时: {cost}s)")

                self.db.save_video_progress(self.user_id, video_id, video['course_id'], progress_percent)

                current_seconds = next_seconds
                if current_seconds < duration:
                    time.sleep(Config.HEARTBEAT_INTERVAL)
            except Exception as e:
                logger.warning(f"[{self.username}] ⚠ 视频 [{title}] 学习意外中断。")
                logger.debug(f"[{self.username}] 播放异常拦截: {e}", exc_info=True)
                return False

        logger.info(f"[{self.username}] ✓ 单个视频学习完成: {title}")
        self.db.save_video_progress(self.user_id, video_id, video['course_id'], 100)
        return True

    def _execute_learning_tasks(self, tasks: List[Dict]):
        for db_task in tasks:
            success = self.play_video(db_task)
            if not success:
                logger.info(f"[{self.username}] ⚠ 本轮播放被中断,准备重新对账。")
                break 

    def run(self):
        logger.info(f"[{self.username}] === 山东医学教育平台自动化学习系统启动 ===")

        if self._check_login_status():
            logger.info(f"[{self.username}] ✓ 检测到有效Cookie,免密登录成功!")
        else:
            logger.info(f"[{self.username}] ⚠ 本地凭证无效或缺失,回退至账号密码登录...")
            if not self.login():
                logger.error(f"[{self.username}] ✗ 登录彻底失败,中断执行。")
                return

        loop_counter = 1
        while True:
            logger.info(f"[{self.username}] === [步骤 1] 获取/更新全量课程进度至专属数据库 (第 {loop_counter} 轮) ===")
            self.sync_global_and_personal_progress()

            pending_tasks = self.db.get_pending_tasks_ordered(self.user_id, Config.COURSE_LEARN_ORDER)

            if not pending_tasks:
                logger.info(f"[{self.username}] 🎉 恭喜!专属数据库内校验所有视频进度已 100%!")
                logger.info(f"[{self.username}] >>> 学习圆满完成,安全退出。 <<<")
                break

            logger.info(f"[{self.username}] === [步骤 2] 按指定顺序执行学习任务 (剩余 {len(pending_tasks)} 个未满进度视频) ===")
            self._execute_learning_tasks(pending_tasks)

            logger.info(f"[{self.username}] === [休整] 准备重新对账 ===")
            loop_counter += 1
            time.sleep(3)

# ==================== 主入口 ====================
def main():
    import sys
    if len(sys.argv) < 4:
        print("使用方法: python ceshi.py <用户名> <密码> <卡密>")
        return
    learner = SDCMEEAutoLearn(sys.argv[1], sys.argv[2], sys.argv[3])
    learner.run()

if __name__ == '__main__':
    main()

1条评论

  1. 184585333
    184585333 · 1 个月前
    https://26bbqypx.mh.chaoxing.com/

发表评论