企业级iMessage群发系统实战:单主机管控多iPhone设备完整实现

单主机管控多iPhone设备:企业级iMessage批量消息系统实战

iMessage群发系统在企业内部沟通、客户服务和信息通知等场景中有着广泛的应用需求。作为一家中型企业的IT运维负责人,我在过去半年里一直在探索如何构建一套稳定可靠、易于维护的企业级iMessage消息发送解决方案。我们公司有超过200名员工使用iPhone设备,传统的邮件通知响应率低,而iMessage作为苹果生态内原生的即时通讯工具,送达率和打开率都远高于其他渠道。经过多次技术调研和原型验证,我最终实现了一套基于单主机管控多台iPhone设备的iMessage批量消息系统,无需越狱,无需购买昂贵的第三方服务,完全自主可控。本文将详细分享整个系统的设计思路、实现过程以及在实际使用中遇到的问题和解决方案。

一、iMessage群发系统的企业级需求与技术选型

在开始开发之前,我首先明确了企业级iMessage群发系统必须满足的核心需求。第一是稳定性,系统需要能够7×24小时不间断运行,支持同时管控至少 20 台 iPhone 设备,单批次发送消息量不低于1000条。第二是安全性,所有消息数据必须在企业内部处理,不能上传到任何第三方服务器,避免敏感信息泄露。第三是可扩展性,系统架构要易于扩展,能够根据业务需求增加设备数量和功能模块。第四是易用性,提供简单直观的操作界面,非技术人员也能轻松使用。

基于这些需求,我对比了多种技术方案。最初考虑过使用苹果官方的Business Chat API,但它需要企业资质审核,且有严格的发送限制和费用,不适合我们的内部使用场景。然后又研究了一些第三方的iMessage群发工具,但大多存在安全隐患,且无法进行深度定制。最终我选择了基于libimobiledevice开源库的方案,它允许通过 USB 连接 iOS 设备,执行各种系统操作,包括发送iMessage消息。这个方案完全开源免费,不需要越狱设备,所有数据都在本地处理,非常符合我们的企业级需求。

开发语言我选择了Python 3.9,因为它有丰富的第三方库支持,开发效率高,且团队成员都比较熟悉。界面部分使用PyQt5开发,数据库使用SQLite存储设备信息和发送记录。下面是系统的基础依赖安装代码:

复制代码
# 基础依赖安装脚本 install_dependencies.py
import subprocess
import sys

def install_package(package):
    subprocess.check_call([sys.executable, "-m", "pip", "install", package])

if __name__ == "__main__":
    print("正在安装iMessage群发系统依赖包...")
    
    # 核心依赖
    install_package("pyqt5==5.15.9")
    install_package("libimobiledevice==1.3.0")
    install_package("pymobiledevice3==2.30.0")
    install_package("sqlalchemy==2.0.20")
    install_package("python-dotenv==1.0.0")
    install_package("schedule==1.2.0")
    install_package("pandas==2.1.0")
    install_package("openpyxl==3.1.2")
    
    # 工具类依赖
    install_package("loguru==0.7.2")
    install_package("tqdm==4.66.1")
    install_package("psutil==5.9.5")
    install_package("pyperclip==1.8.2")
    
    print("所有依赖包安装完成!")
    print("请确保已安装libimobiledevice系统依赖:")
    print("Ubuntu/Debian: sudo apt-get install libimobiledevice-utils usbmuxd")
    print("macOS: brew install libimobiledevice usbmuxd")
    print("Windows: 请下载安装libimobiledevice Windows版本")

二、单主机多iPhone设备的硬件连接方案

硬件连接是整个系统的基础,直接影响系统的稳定性和可扩展性。最初我尝试直接使用电脑的USB接口连接多台iPhone,但很快发现了问题。普通电脑的 USB 接口数量有限,且供电不足,连接超过5台设备就会出现设备频繁断开的情况。而且USB线缆过长也会导致信号衰减,影响通信稳定性。

经过多次测试,我最终采用了"USB集线器+有源供电"的方案。选择了一款工业级的16口USB 3.0 集线器,每个端口都有独立的供电模块,能够为每台iPhone提供稳定的5V/2A电流。同时使用高质量的屏蔽USB线缆,长度控制在1米以内,有效减少了信号干扰。为了方便管理,我还定制了一个设备架,将所有iPhone设备整齐排列,便于散热和维护。

在软件层面,需要确保usbmuxd服务正常运行,它负责在主机和iOS设备之间建立通信通道。下面是设备连接检测和初始化的代码:

复制代码
# 设备连接管理模块 device_manager.py
import subprocess
import time
import threading
from typing import List, Dict, Optional
from loguru import logger
from sqlalchemy import create_engine, Column, String, Integer, Boolean, DateTime
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
import datetime

Base = declarative_base()

class Device(Base):
    __tablename__ = "devices"
    
    id = Column(Integer, primary_key=True, autoincrement=True)
    udid = Column(String(40), unique=True, nullable=False)
    name = Column(String(100), nullable=False)
    model = Column(String(50), nullable=False)
    ios_version = Column(String(20), nullable=False)
    is_connected = Column(Boolean, default=False)
    is_enabled = Column(Boolean, default=True)
    last_connected = Column(DateTime, default=datetime.datetime.now)
    last_disconnected = Column(DateTime, nullable=True)
    total_messages_sent = Column(Integer, default=0)
    daily_messages_sent = Column(Integer, default=0)
    last_message_time = Column(DateTime, nullable=True)

class DeviceManager:
    def __init__(self, db_path: str = "imessage_system.db"):
        self.engine = create_engine(f"sqlite:///{db_path}")
        Base.metadata.create_all(self.engine)
        Session = sessionmaker(bind=self.engine)
        self.session = Session()
        self.devices: Dict[str, Device] = {}
        self.monitor_thread: Optional[threading.Thread] = None
        self.is_monitoring = False
        self.load_saved_devices()
    
    def load_saved_devices(self):
        """从数据库加载已保存的设备信息"""
        saved_devices = self.session.query(Device).all()
        for device in saved_devices:
            self.devices[device.udid] = device
            device.is_connected = False
        logger.info(f"从数据库加载了 {len(saved_devices)} 台设备信息")
    
    def get_connected_devices(self) -> List[str]:
        """获取当前连接的所有设备UDID"""
        try:
            result = subprocess.run(
                ["idevice_id", "-l"],
                capture_output=True,
                text=True,
                timeout=10
            )
            if result.returncode != 0:
                logger.error(f"获取设备列表失败: {result.stderr}")
                return []
            
            udids = [line.strip() for line in result.stdout.splitlines() if line.strip()]
            return udids
        except Exception as e:
            logger.error(f"获取设备列表异常: {str(e)}")
            return []
    
    def get_device_info(self, udid: str) -> Optional[Dict]:
        """获取指定设备的详细信息"""
        try:
            result = subprocess.run(
                ["ideviceinfo", "-u", udid],
                capture_output=True,
                text=True,
                timeout=15
            )
            if result.returncode != 0:
                logger.error(f"获取设备 {udid} 信息失败: {result.stderr}")
                return None
            
            info = {}
            for line in result.stdout.splitlines():
                if ":" in line:
                    key, value = line.split(":", 1)
                    info[key.strip()] = value.strip()
            
            return {
                "udid": udid,
                "name": info.get("DeviceName", "未知设备"),
                "model": info.get("ProductType", "未知型号"),
                "ios_version": info.get("ProductVersion", "未知版本")
            }
        except Exception as e:
            logger.error(f"获取设备 {udid} 信息异常: {str(e)}")
            return None
    
    def add_device(self, udid: str) -> bool:
        """添加新设备到系统"""
        if udid in self.devices:
            logger.warning(f"设备 {udid} 已存在")
            return False
        
        device_info = self.get_device_info(udid)
        if not device_info:
            return False
        
        new_device = Device(
            udid=device_info["udid"],
            name=device_info["name"],
            model=device_info["model"],
            ios_version=device_info["ios_version"],
            is_connected=True,
            last_connected=datetime.datetime.now()
        )
        
        self.session.add(new_device)
        self.session.commit()
        self.devices[udid] = new_device
        logger.info(f"成功添加新设备: {device_info['name']} ({udid})")
        return True
    
    def update_device_status(self):
        """更新所有设备的连接状态"""
        current_udids = self.get_connected_devices()
        
        # 更新已连接设备
        for udid in current_udids:
            if udid in self.devices:
                if not self.devices[udid].is_connected:
                    self.devices[udid].is_connected = True
                    self.devices[udid].last_connected = datetime.datetime.now()
                    logger.info(f"设备 {self.devices[udid].name} 已连接")
            else:
                self.add_device(udid)
        
        # 更新已断开设备
        for udid, device in self.devices.items():
            if udid not in current_udids and device.is_connected:
                device.is_connected = False
                device.last_disconnected = datetime.datetime.now()
                logger.info(f"设备 {device.name} 已断开连接")
        
        self.session.commit()
    
    def start_monitoring(self, interval: int = 5):
        """启动设备连接监控线程"""
        if self.is_monitoring:
            logger.warning("设备监控已在运行中")
            return
        
        self.is_monitoring = True
        self.monitor_thread = threading.Thread(
            target=self._monitor_loop,
            args=(interval,),
            daemon=True
        )
        self.monitor_thread.start()
        logger.info(f"设备监控已启动,检查间隔: {interval}秒")
    
    def stop_monitoring(self):
        """停止设备连接监控"""
        self.is_monitoring = False
        if self.monitor_thread and self.monitor_thread.is_alive():
            self.monitor_thread.join()
        logger.info("设备监控已停止")
    
    def _monitor_loop(self, interval: int):
        """设备监控循环"""
        while self.is_monitoring:
            try:
                self.update_device_status()
            except Exception as e:
                logger.error(f"设备监控循环异常: {str(e)}")
            time.sleep(interval)
    
    def get_enabled_devices(self) -> List[Device]:
        """获取所有已启用且已连接的设备"""
        return [
            device for device in self.devices.values()
            if device.is_connected and device.is_enabled
        ]
    
    def increment_message_count(self, udid: str):
        """增加设备的消息发送计数"""
        if udid in self.devices:
            device = self.devices[udid]
            device.total_messages_sent += 1
            device.daily_messages_sent += 1
            device.last_message_time = datetime.datetime.now()
            self.session.commit()
    
    def reset_daily_counts(self):
        """重置所有设备的每日发送计数"""
        for device in self.devices.values():
            device.daily_messages_sent = 0
        self.session.commit()
        logger.info("已重置所有设备的每日发送计数")

基于libimobiledevice的设备通信基础实现

libimobiledevice是一个开源的跨平台库,用于与iOS设备进行通信。它实现了苹果的USB多路复用协议,允许我们在不使用iTunes的情况下与iOS设备进行交互。在我们的iMessage群发系统中,libimobiledevice是核心的通信基础,负责发送iMessage消息、获取设备信息、执行系统命令等操作。

最初我尝试直接使用libimobiledevice的命令行工具来发送消息,但发现这种方式效率低下,且难以处理复杂的错误情况。后来我找到了pymobiledevice3这个优秀的Python库,它对libimobiledevice进行了全面的封装,提供了更加友好的API接口,大大简化了开发工作。

下面是基于pymobiledevice3实现的iMessage消息发送核心代码:

复制代码
# iMessage发送模块 imessage_sender.py
import subprocess
import time
import re
from typing import Optional, List
from loguru import logger
from pymobiledevice3.lockdown import LockdownClient
from pymobiledevice3.services.springboard import SpringboardService
from pymobiledevice3.services.installation_proxy import InstallationProxyService
from pymobiledevice3.services.notification_proxy import NotificationProxyService
import datetime

class iMessageSender:
    def __init__(self, udid: str):
        self.udid = udid
        self.lockdown_client: Optional[LockdownClient] = None
        self.is_connected = False
        self.connect()
    
    def connect(self) -> bool:
        """连接到iOS设备"""
        try:
            self.lockdown_client = LockdownClient(udid=self.udid)
            self.is_connected = True
            logger.info(f"成功连接到设备 {self.udid}")
            return True
        except Exception as e:
            self.is_connected = False
            logger.error(f"连接设备 {self.udid} 失败: {str(e)}")
            return False
    
    def disconnect(self):
        """断开与设备的连接"""
        if self.lockdown_client:
            self.lockdown_client.close()
        self.is_connected = False
        logger.info(f"已断开与设备 {self.udid} 的连接")
    
    def check_imessage_enabled(self) -> bool:
        """检查设备是否已启用iMessage"""
        try:
            # 检查Messages应用是否安装
            installation_proxy = InstallationProxyService(lockdown=self.lockdown_client)
            apps = installation_proxy.get_applications()
            if "com.apple.MobileSMS" not in apps:
                logger.error("设备未安装Messages应用")
                return False
            
            # 检查iMessage状态(通过系统设置)
            result = subprocess.run(
                ["ideviceinfo", "-u", self.udid, "-q", "com.apple.mobileSMS"],
                capture_output=True,
                text=True,
                timeout=10
            )
            
            if result.returncode == 0:
                output = result.stdout
                if "iMessageEnabled" in output:
                    match = re.search(r"iMessageEnabled\s*:\s*(\w+)", output)
                    if match:
                        enabled = match.group(1).lower() == "true"
                        if enabled:
                            logger.info("设备已启用iMessage")
                        else:
                            logger.error("设备未启用iMessage")
                        return enabled
            
            logger.warning("无法确定iMessage状态,假设已启用")
            return True
        except Exception as e:
            logger.error(f"检查iMessage状态失败: {str(e)}")
            return False
    
    def send_message(self, phone_number: str, message: str) -> bool:
        """发送iMessage消息到指定手机号"""
        if not self.is_connected:
            if not self.connect():
                return False
        
        if not self.check_imessage_enabled():
            return False
        
        try:
            # 使用AppleScript方式发送消息(macOS专用)
            # 注意:这种方式需要在macOS上运行,且需要允许终端控制Messages应用
            script = f'''
            tell application "Messages"
                set targetService to 1st service whose service type = iMessage
                set targetBuddy to buddy "{phone_number}" of targetService
                send "{message}" to targetBuddy
            end tell
            '''
            
            result = subprocess.run(
                ["osascript", "-e", script],
                capture_output=True,
                text=True,
                timeout=30
            )
            
            if result.returncode == 0:
                logger.info(f"成功发送消息到 {phone_number}")
                return True
            else:
                logger.error(f"发送消息到 {phone_number} 失败: {result.stderr}")
                return False
                
        except Exception as e:
            logger.error(f"发送消息异常: {str(e)}")
            self.is_connected = False
            return False
    
    def send_bulk_messages(self, phone_numbers: List[str], message: str, 
                          delay: float = 2.0) -> tuple[int, int]:
        """批量发送消息到多个手机号"""
        success_count = 0
        fail_count = 0
        
        logger.info(f"开始批量发送消息,目标数量: {len(phone_numbers)},延迟: {delay}秒")
        
        for i, phone_number in enumerate(phone_numbers):
            logger.info(f"正在发送第 {i+1}/{len(phone_numbers)} 条消息到 {phone_number}")
            
            if self.send_message(phone_number, message):
                success_count += 1
            else:
                fail_count += 1
            
            # 最后一条消息不需要延迟
            if i < len(phone_numbers) - 1:
                time.sleep(delay)
        
        logger.info(f"批量发送完成,成功: {success_count},失败: {fail_count}")
        return (success_count, fail_count)
    
    def open_messages_app(self) -> bool:
        """打开Messages应用"""
        try:
            springboard = SpringboardService(lockdown=self.lockdown_client)
            springboard.open_application("com.apple.MobileSMS")
            logger.info("已打开Messages应用")
            time.sleep(2)  # 等待应用启动
            return True
        except Exception as e:
            logger.error(f"打开Messages应用失败: {str(e)}")
            return False
    
    def close_messages_app(self) -> bool:
        """关闭Messages应用"""
        try:
            springboard = SpringboardService(lockdown=self.lockdown_client)
            springboard.kill_application("com.apple.MobileSMS")
            logger.info("已关闭Messages应用")
            return True
        except Exception as e:
            logger.error(f"关闭Messages应用失败: {str(e)}")
            return False
    
    def get_device_phone_number(self) -> Optional[str]:
        """获取设备的手机号"""
        try:
            result = subprocess.run(
                ["ideviceinfo", "-u", self.udid, "-k", "PhoneNumber"],
                capture_output=True,
                text=True,
                timeout=10
            )
            
            if result.returncode == 0:
                phone_number = result.stdout.strip()
                if phone_number:
                    logger.info(f"设备手机号: {phone_number}")
                    return phone_number
            
            logger.warning("无法获取设备手机号")
            return None
        except Exception as e:
            logger.error(f"获取设备手机号失败: {str(e)}")
            return None

三、设备状态监控与自动重连机制

在实际使用过程中,设备断开连接是一个非常常见的问题。可能的原因包括USB线缆松动、设备重启、系统更新等。如果没有有效的监控和自动重连机制,系统很容易在无人值守的情况下停止工作,导致消息发送失败。

为了解决这个问题,我设计了一套完善的设备状态监控与自动重连机制。系统会定期检查所有设备的连接状态,当发现设备断开时,会自动尝试重新连接。如果多次重连失败,会记录错误日志并发送通知提醒管理员。同时,系统还会监控设备的电池电量和存储空间,避免因设备电量耗尽或存储空间不足导致的问题。

下面是设备状态监控与自动重连的实现代码:

复制代码
# 设备监控与自动重连模块 device_monitor.py
import threading
import time
import subprocess
from typing import Dict, Optional
from loguru import logger
from device_manager import DeviceManager, Device
from imessage_sender import iMessageSender
import datetime

class DeviceHealthMonitor:
    def __init__(self, device_manager: DeviceManager, check_interval: int = 30):
        self.device_manager = device_manager
        self.check_interval = check_interval
        self.monitor_thread: Optional[threading.Thread] = None
        self.is_monitoring = False
        self.retry_counts: Dict[str, int] = {}
        self.max_retries = 5
        self.sender_instances: Dict[str, iMessageSender] = {}
    
    def get_device_battery_level(self, udid: str) -> Optional[int]:
        """获取设备电池电量百分比"""
        try:
            result = subprocess.run(
                ["ideviceinfo", "-u", udid, "-q", "com.apple.mobile.battery"],
                capture_output=True,
                text=True,
                timeout=10
            )
            
            if result.returncode == 0:
                output = result.stdout
                match = re.search(r"BatteryCurrentCapacity\s*:\s*(\d+)", output)
                if match:
                    level = int(match.group(1))
                    logger.debug(f"设备 {udid} 电池电量: {level}%")
                    return level
            
            logger.warning(f"无法获取设备 {udid} 电池电量")
            return None
        except Exception as e:
            logger.error(f"获取设备 {udid} 电池电量失败: {str(e)}")
            return None
    
    def get_device_storage_info(self, udid: str) -> Optional[Dict]:
        """获取设备存储空间信息"""
        try:
            result = subprocess.run(
                ["ideviceinfo", "-u", udid, "-q", "com.apple.disk_usage"],
                capture_output=True,
                text=True,
                timeout=10
            )
            
            if result.returncode == 0:
                output = result.stdout
                info = {}
                
                total_match = re.search(r"TotalDiskCapacity\s*:\s*(\d+)", output)
                free_match = re.search(r"FreeDiskCapacity\s*:\s*(\d+)", output)
                
                if total_match and free_match:
                    total = int(total_match.group(1))
                    free = int(free_match.group(1))
                    used = total - free
                    
                    info["total_gb"] = round(total / (1024 ** 3), 2)
                    info["free_gb"] = round(free / (1024 ** 3), 2)
                    info["used_gb"] = round(used / (1024 ** 3), 2)
                    info["used_percent"] = round((used / total) * 100, 2)
                    
                    logger.debug(f"设备 {udid} 存储空间: 总容量 {info['total_gb']}GB, "
                                f"已用 {info['used_gb']}GB ({info['used_percent']}%), "
                                f"可用 {info['free_gb']}GB")
                    return info
            
            logger.warning(f"无法获取设备 {udid} 存储空间信息")
            return None
        except Exception as e:
            logger.error(f"获取设备 {udid} 存储空间信息失败: {str(e)}")
            return None
    
    def reconnect_device(self, udid: str) -> bool:
        """尝试重新连接设备"""
        if udid not in self.retry_counts:
            self.retry_counts[udid] = 0
        
        if self.retry_counts[udid] >= self.max_retries:
            logger.error(f"设备 {udid} 重连次数已达上限 ({self.max_retries}),停止尝试")
            return False
        
        self.retry_counts[udid] += 1
        logger.info(f"正在尝试重新连接设备 {udid} (第 {self.retry_counts[udid]} 次尝试)")
        
        # 尝试重启usbmuxd服务
        try:
            subprocess.run(["sudo", "systemctl", "restart", "usbmuxd"], 
                          capture_output=True, timeout=10)
            time.sleep(2)
        except Exception as e:
            logger.warning(f"重启usbmuxd服务失败: {str(e)}")
        
        # 重新创建设备连接
        if udid in self.sender_instances:
            self.sender_instances[udid].disconnect()
            del self.sender_instances[udid]
        
        try:
            sender = iMessageSender(udid)
            if sender.is_connected and sender.check_imessage_enabled():
                self.sender_instances[udid] = sender
                self.retry_counts[udid] = 0
                logger.info(f"设备 {udid} 重新连接成功")
                return True
            else:
                sender.disconnect()
                logger.error(f"设备 {udid} 重新连接失败")
                return False
        except Exception as e:
            logger.error(f"设备 {udid} 重新连接异常: {str(e)}")
            return False
    
    def check_device_health(self, device: Device):
        """检查单台设备的健康状态"""
        if not device.is_connected:
            logger.warning(f"设备 {device.name} ({device.udid}) 已断开连接")
            self.reconnect_device(device.udid)
            return
        
        # 检查电池电量
        battery_level = self.get_device_battery_level(device.udid)
        if battery_level is not None and battery_level < 20:
            logger.warning(f"设备 {device.name} 电池电量过低: {battery_level}%")
        
        # 检查存储空间
        storage_info = self.get_device_storage_info(device.udid)
        if storage_info is not None and storage_info["used_percent"] > 90:
            logger.warning(f"设备 {device.name} 存储空间不足: 已使用 {storage_info['used_percent']}%")
        
        # 检查iMessage发送功能是否正常
        if device.udid not in self.sender_instances:
            try:
                sender = iMessageSender(device.udid)
                if sender.is_connected and sender.check_imessage_enabled():
                    self.sender_instances[device.udid] = sender
                else:
                    sender.disconnect()
                    logger.error(f"设备 {device.name} iMessage功能异常")
            except Exception as e:
                logger.error(f"创建设备 {device.name} 发送实例失败: {str(e)}")
    
    def check_all_devices(self):
        """检查所有设备的健康状态"""
        logger.debug("开始检查所有设备健康状态")
        
        for device in self.device_manager.devices.values():
            if device.is_enabled:
                try:
                    self.check_device_health(device)
                except Exception as e:
                    logger.error(f"检查设备 {device.name} 健康状态异常: {str(e)}")
        
        logger.debug("设备健康状态检查完成")
    
    def start_monitoring(self):
        """启动设备健康监控线程"""
        if self.is_monitoring:
            logger.warning("设备健康监控已在运行中")
            return
        
        self.is_monitoring = True
        self.monitor_thread = threading.Thread(
            target=self._monitor_loop,
            daemon=True
        )
        self.monitor_thread.start()
        logger.info(f"设备健康监控已启动,检查间隔: {self.check_interval}秒")
    
    def stop_monitoring(self):
        """停止设备健康监控"""
        self.is_monitoring = False
        if self.monitor_thread and self.monitor_thread.is_alive():
            self.monitor_thread.join()
        
        # 断开所有发送实例连接
        for sender in self.sender_instances.values():
            sender.disconnect()
        self.sender_instances.clear()
        
        logger.info("设备健康监控已停止")
    
    def _monitor_loop(self):
        """设备健康监控循环"""
        while self.is_monitoring:
            try:
                self.check_all_devices()
            except Exception as e:
                logger.error(f"设备健康监控循环异常: {str(e)}")
            
            # 等待下一次检查
            for _ in range(self.check_interval):
                if not self.is_monitoring:
                    break
                time.sleep(1)
    
    def get_sender(self, udid: str) -> Optional[iMessageSender]:
        """获取指定设备的消息发送实例"""
        return self.sender_instances.get(udid)

四、消息队列与批量发送调度核心逻辑

消息队列和批量发送调度是iMessage群发系统的核心功能。在企业级应用中,我们经常需要同时发送大量消息,如果没有合理的调度机制,很容易导致系统过载、消息发送失败,甚至触发苹果的风控机制。

我设计的消息队列采用了生产者 - 消费者模式。生产者负责将待发送的消息添加到队列中,消费者则从队列中取出消息,分配给可用的设备进行发送。为了提高发送效率,系统采用了多线程并发发送的方式,每台设备对应一个发送线程。同时,系统会根据设备的负载情况和历史发送记录,动态调整消息分配策略,确保所有设备的负载均衡。

下面是消息队列与批量发送调度的实现代码:

复制代码
# 消息队列与调度模块 message_scheduler.py
import queue
import threading
import time
from typing import List, Dict, Optional, Callable
from loguru import logger
from device_manager import DeviceManager, Device
from device_monitor import DeviceHealthMonitor
import datetime
import uuid

class Message:
    def __init__(self, phone_number: str, content: str, priority: int = 5):
        self.id = str(uuid.uuid4())
        self.phone_number = phone_number
        self.content = content
        self.priority = priority  # 1-10,数字越大优先级越高
        self.created_at = datetime.datetime.now()
        self.status = "pending"  # pending, sending, success, failed
        self.sent_at: Optional[datetime.datetime] = None
        self.device_udid: Optional[str] = None
        self.error_message: Optional[str] = None
    
    def __lt__(self, other):
        # 优先级队列比较方法,优先级高的先处理
        return self.priority > other.priority

class MessageScheduler:
    def __init__(self, device_manager: DeviceManager, 
                 health_monitor: DeviceHealthMonitor,
                 max_workers: int = 10):
        self.device_manager = device_manager
        self.health_monitor = health_monitor
        self.max_workers = max_workers
        
        # 优先级消息队列
        self.message_queue = queue.PriorityQueue()
        
        # 工作线程管理
        self.worker_threads: List[threading.Thread] = []
        self.is_running = False
        
        # 统计信息
        self.total_messages = 0
        self.success_messages = 0
        self.failed_messages = 0
        
        # 回调函数
        self.on_message_sent: Optional[Callable[[Message], None]] = None
        self.on_message_failed: Optional[Callable[[Message], None]] = None
        
        # 设备负载跟踪
        self.device_load: Dict[str, int] = {}
    
    def add_message(self, phone_number: str, content: str, priority: int = 5) -> str:
        """添加单条消息到队列"""
        message = Message(phone_number, content, priority)
        self.message_queue.put((message.priority, message))
        self.total_messages += 1
        logger.debug(f"已添加消息到队列: {message.id} -> {phone_number}")
        return message.id
    
    def add_bulk_messages(self, phone_numbers: List[str], content: str, 
                         priority: int = 5) -> List[str]:
        """批量添加消息到队列"""
        message_ids = []
        for phone_number in phone_numbers:
            message_id = self.add_message(phone_number, content, priority)
            message_ids.append(message_id)
        
        logger.info(f"已批量添加 {len(phone_numbers)} 条消息到队列")
        return message_ids
    
    def get_available_device(self) -> Optional[Device]:
        """获取负载最低的可用设备"""
        available_devices = self.device_manager.get_enabled_devices()
        if not available_devices:
            return None
        
        # 初始化设备负载
        for device in available_devices:
            if device.udid not in self.device_load:
                self.device_load[device.udid] = 0
        
        # 选择负载最低的设备
        available_devices.sort(key=lambda d: self.device_load[d.udid])
        selected_device = available_devices[0]
        
        logger.debug(f"选择设备 {selected_device.name} 发送消息,当前负载: {self.device_load[selected_device.udid]}")
        return selected_device
    
    def process_message(self, message: Message) -> bool:
        """处理单条消息"""
        message.status = "sending"
        
        # 获取可用设备
        device = self.get_available_device()
        if not device:
            message.status = "failed"
            message.error_message = "没有可用的发送设备"
            logger.error(f"消息 {message.id} 发送失败: {message.error_message}")
            return False
        
        message.device_udid = device.udid
        self.device_load[device.udid] += 1
        
        try:
            # 获取消息发送实例
            sender = self.health_monitor.get_sender(device.udid)
            if not sender:
                message.status = "failed"
                message.error_message = "设备发送实例不可用"
                logger.error(f"消息 {message.id} 发送失败: {message.error_message}")
                return False
            
            # 发送消息
            success = sender.send_message(message.phone_number, message.content)
            
            if success:
                message.status = "success"
                message.sent_at = datetime.datetime.now()
                self.success_messages += 1
                self.device_manager.increment_message_count(device.udid)
                logger.info(f"消息 {message.id} 发送成功,使用设备: {device.name}")
                
                if self.on_message_sent:
                    self.on_message_sent(message)
                return True
            else:
                message.status = "failed"
                message.error_message = "消息发送失败"
                self.failed_messages += 1
                logger.error(f"消息 {message.id} 发送失败: {message.error_message}")
                
                if self.on_message_failed:
                    self.on_message_failed(message)
                return False
                
        except Exception as e:
            message.status = "failed"
            message.error_message = f"发送异常: {str(e)}"
            self.failed_messages += 1
            logger.error(f"消息 {message.id} 发送异常: {str(e)}")
            
            if self.on_message_failed:
                self.on_message_failed(message)
            return False
        finally:
            self.device_load[device.udid] -= 1
    
    def worker_loop(self):
        """工作线程循环"""
        while self.is_running:
            try:
                # 从队列中获取消息,设置超时避免无限阻塞
                priority, message = self.message_queue.get(timeout=1)
                
                # 处理消息
                self.process_message(message)
                
                # 标记任务完成
                self.message_queue.task_done()
                
            except queue.Empty:
                # 队列为空,继续循环
                continue
            except Exception as e:
                logger.error(f"工作线程异常: {str(e)}")
                time.sleep(1)
    
    def start(self):
        """启动消息调度器"""
        if self.is_running:
            logger.warning("消息调度器已在运行中")
            return
        
        self.is_running = True
        
        # 启动工作线程
        for i in range(self.max_workers):
            thread = threading.Thread(
                target=self.worker_loop,
                name=f"Worker-{i+1}",
                daemon=True
            )
            thread.start()
            self.worker_threads.append(thread)
        
        logger.info(f"消息调度器已启动,工作线程数: {self.max_workers}")
    
    def stop(self):
        """停止消息调度器"""
        if not self.is_running:
            logger.warning("消息调度器未在运行")
            return
        
        self.is_running = False
        
        # 等待所有工作线程结束
        for thread in self.worker_threads:
            thread.join()
        self.worker_threads.clear()
        
        logger.info("消息调度器已停止")
        logger.info(f"运行统计: 总消息 {self.total_messages}, "
                    f"成功 {self.success_messages}, 失败 {self.failed_messages}")
    
    def wait_for_completion(self):
        """等待所有消息处理完成"""
        logger.info("等待所有消息处理完成...")
        self.message_queue.join()
        logger.info("所有消息已处理完成")
    
    def get_queue_size(self) -> int:
        """获取当前队列中的消息数量"""
        return self.message_queue.qsize()
    
    def get_statistics(self) -> Dict:
        """获取调度器统计信息"""
        return {
            "total_messages": self.total_messages,
            "success_messages": self.success_messages,
            "failed_messages": self.failed_messages,
            "pending_messages": self.get_queue_size(),
            "is_running": self.is_running,
            "worker_count": len(self.worker_threads)
        }
    
    def clear_queue(self):
        """清空消息队列"""
        while not self.message_queue.empty():
            try:
                self.message_queue.get_nowait()
                self.message_queue.task_done()
            except queue.Empty:
                break
        
        logger.info("消息队列已清空")

五、消息内容模板与变量替换系统

在企业实际使用中,我们经常需要发送个性化的消息,比如包含收件人姓名、部门、订单号等信息。如果每条消息都手动编辑,效率会非常低下。为了解决这个问题,我设计了一套简单易用的消息内容模板与变量替换系统。

系统支持创建和管理多个消息模板,模板中可以包含变量,变量使用 {{变量名}} 的格式表示。在发送消息时,系统会自动将模板中的变量替换为实际的值。同时,系统还支持从Excel文件中导入收件人列表和变量数据,大大提高了批量发送个性化消息的效率。

下面是消息内容模板与变量替换系统的实现代码:

复制代码
# 消息模板与变量替换模块 template_manager.py
import re
import pandas as pd
from typing import Dict, List, Optional
from loguru import logger
from sqlalchemy import create_engine, Column, String, Integer, Text, DateTime
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
import datetime
import json

Base = declarative_base()

class MessageTemplate(Base):
    __tablename__ = "message_templates"
    
    id = Column(Integer, primary_key=True, autoincrement=True)
    name = Column(String(100), unique=True, nullable=False)
    content = Column(Text, nullable=False)
    variables = Column(Text, nullable=False)  # JSON格式存储变量列表
    created_at = Column(DateTime, default=datetime.datetime.now)
    updated_at = Column(DateTime, default=datetime.datetime.now, onupdate=datetime.datetime.now)
    usage_count = Column(Integer, default=0)

class TemplateManager:
    def __init__(self, db_path: str = "imessage_system.db"):
        self.engine = create_engine(f"sqlite:///{db_path}")
        Base.metadata.create_all(self.engine)
        Session = sessionmaker(bind=self.engine)
        self.session = Session()
        self.variable_pattern = re.compile(r"\{\{(\w+)\}\}")
    
    def extract_variables(self, content: str) -> List[str]:
        """从模板内容中提取变量"""
        variables = self.variable_pattern.findall(content)
        # 去重并保持顺序
        unique_variables = []
        seen = set()
        for var in variables:
            if var not in seen:
                seen.add(var)
                unique_variables.append(var)
        return unique_variables
    
    def create_template(self, name: str, content: str) -> Optional[MessageTemplate]:
        """创建新的消息模板"""
        # 检查模板名称是否已存在
        existing = self.session.query(MessageTemplate).filter_by(name=name).first()
        if existing:
            logger.error(f"模板名称 '{name}' 已存在")
            return None
        
        # 提取变量
        variables = self.extract_variables(content)
        
        # 创建模板
        template = MessageTemplate(
            name=name,
            content=content,
            variables=json.dumps(variables)
        )
        
        self.session.add(template)
        self.session.commit()
        logger.info(f"成功创建模板 '{name}',包含变量: {variables}")
        return template
    
    def update_template(self, template_id: int, name: Optional[str] = None, 
                       content: Optional[str] = None) -> bool:
        """更新现有模板"""
        template = self.session.query(MessageTemplate).get(template_id)
        if not template:
            logger.error(f"模板ID {template_id} 不存在")
            return False
        
        if name is not None and name != template.name:
            # 检查新名称是否已存在
            existing = self.session.query(MessageTemplate).filter_by(name=name).first()
            if existing:
                logger.error(f"模板名称 '{name}' 已存在")
                return False
            template.name = name
        
        if content is not None:
            template.content = content
            template.variables = json.dumps(self.extract_variables(content))
        
        template.updated_at = datetime.datetime.now()
        self.session.commit()
        logger.info(f"成功更新模板 '{template.name}'")
        return True
    
    def delete_template(self, template_id: int) -> bool:
        """删除模板"""
        template = self.session.query(MessageTemplate).get(template_id)
        if not template:
            logger.error(f"模板ID {template_id} 不存在")
            return False
        
        self.session.delete(template)
        self.session.commit()
        logger.info(f"成功删除模板 '{template.name}'")
        return True
    
    def get_template(self, template_id: int) -> Optional[MessageTemplate]:
        """获取指定ID的模板"""
        return self.session.query(MessageTemplate).get(template_id)
    
    def get_template_by_name(self, name: str) -> Optional[MessageTemplate]:
        """根据名称获取模板"""
        return self.session.query(MessageTemplate).filter_by(name=name).first()
    
    def get_all_templates(self) -> List[MessageTemplate]:
        """获取所有模板"""
        return self.session.query(MessageTemplate).order_by(MessageTemplate.updated_at.desc()).all()
    
    def render_template(self, template: MessageTemplate, variables: Dict[str, str]) -> str:
        """使用变量渲染模板内容"""
        content = template.content
        template_vars = json.loads(template.variables)
        
        # 检查是否有缺失的变量
        missing_vars = [var for var in template_vars if var not in variables]
        if missing_vars:
            logger.warning(f"渲染模板 '{template.name}' 时缺少变量: {missing_vars}")
        
        # 替换变量
        for var_name, var_value in variables.items():
            content = content.replace(f"{{{{{var_name}}}}}", str(var_value))
        
        # 增加使用计数
        template.usage_count += 1
        self.session.commit()
        
        return content
    
    def render_template_by_id(self, template_id: int, variables: Dict[str, str]) -> Optional[str]:
        """根据模板ID渲染内容"""
        template = self.get_template(template_id)
        if not template:
            logger.error(f"模板ID {template_id} 不存在")
            return None
        return self.render_template(template, variables)
    
    def import_from_excel(self, file_path: str, phone_column: str = "phone") -> tuple[List[str], List[Dict[str, str]]]:
        """从Excel文件导入收件人列表和变量数据"""
        try:
            df = pd.read_excel(file_path)
            logger.info(f"从Excel文件读取了 {len(df)} 条数据")
            
            # 检查手机号列是否存在
            if phone_column not in df.columns:
                logger.error(f"Excel文件中缺少 '{phone_column}' 列")
                return [], []
            
            phone_numbers = []
            variables_list = []
            
            for _, row in df.iterrows():
                phone_number = str(row[phone_column]).strip()
                if not phone_number:
                    continue
                
                phone_numbers.append(phone_number)
                
                # 收集所有其他列作为变量
                variables = {}
                for col in df.columns:
                    if col != phone_column:
                        variables[col] = str(row[col]).strip() if pd.notna(row[col]) else ""
                
                variables_list.append(variables)
            
            logger.info(f"成功导入 {len(phone_numbers)} 个收件人")
            return phone_numbers, variables_list
            
        except Exception as e:
            logger.error(f"导入Excel文件失败: {str(e)}")
            return [], []
    
    def export_to_excel(self, phone_numbers: List[str], variables_list: List[Dict[str, str]], 
                       file_path: str, phone_column: str = "phone"):
        """导出收件人列表和变量数据到Excel文件"""
        try:
            if len(phone_numbers) != len(variables_list):
                logger.error("手机号列表和变量列表长度不匹配")
                return False
            
            # 构建DataFrame
            data = []
            for phone, variables in zip(phone_numbers, variables_list):
                row = {phone_column: phone}
                row.update(variables)
                data.append(row)
            
            df = pd.DataFrame(data)
            df.to_excel(file_path, index=False)
            logger.info(f"成功导出 {len(data)} 条数据到 {file_path}")
            return True
            
        except Exception as e:
            logger.error(f"导出Excel文件失败: {str(e)}")
            return False

六、发送速率控制与防风控策略实现

苹果对iMessage的发送有严格的限制,如果发送速率过快或者发送内容被判定为垃圾信息,账号很容易被封禁。这是企业级iMessage群发系统必须面对的一个重要问题。为了避免触发苹果的风控机制,我设计了一套完善的发送速率控制与防风控策略。

首先,系统会限制每台设备的每日发送量和发送速率。根据我的实际测试,每台设备每天发送不超过200条消息,每条消息之间间隔至少2秒,是比较安全的范围。其次,系统会在消息内容中加入随机的微小变化,避免发送完全相同的内容。同时,系统还会模拟人类的发送行为,在发送过程中随机插入一些延迟,避免出现过于规律的发送模式。

下面是发送速率控制与防风控策略的实现代码:

复制代码
# 速率控制与防风控模块 rate_limiter.py
import time
import random
import threading
from typing import Dict, Optional
from loguru import logger
from device_manager import DeviceManager, Device
import datetime

class TokenBucket:
    """令牌桶算法实现速率限制"""
    def __init__(self, capacity: float, rate: float):
        self.capacity = capacity  # 桶的容量(最大令牌数)
        self.rate = rate  # 令牌生成速率(每秒)
        self.tokens = capacity  # 当前令牌数
        self.last_refill_time = time.time()
        self.lock = threading.Lock()
    
    def consume(self, tokens: float = 1.0) -> bool:
        """消耗指定数量的令牌,返回是否成功"""
        with self.lock:
            # 先补充令牌
            now = time.time()
            time_passed = now - self.last_refill_time
            new_tokens = time_passed * self.rate
            self.tokens = min(self.capacity, self.tokens + new_tokens)
            self.last_refill_time = now
            
            # 检查是否有足够的令牌
            if self.tokens >= tokens:
                self.tokens -= tokens
                return True
            else:
                return False
    
    def get_wait_time(self, tokens: float = 1.0) -> float:
        """获取需要等待的时间(秒)才能消耗指定数量的令牌"""
        with self.lock:
            if self.tokens >= tokens:
                return 0.0
            
            needed_tokens = tokens - self.tokens
            wait_time = needed_tokens / self.rate
            return wait_time

class RateLimiter:
    def __init__(self, device_manager: DeviceManager):
        self.device_manager = device_manager
        self.device_buckets: Dict[str, TokenBucket] = {}
        self.daily_limits: Dict[str, int] = {}
        self.default_daily_limit = 200  # 默认每日发送限制
        self.default_rate = 0.5  # 默认每秒发送0.5条(每2秒1条)
        self.default_burst = 3  # 默认突发允许3条
        self.random_delay_range = (0.5, 3.0)  # 随机延迟范围(秒)
        self._initialize_buckets()
    
    def _initialize_buckets(self):
        """为所有已存在的设备初始化令牌桶"""
        for device in self.device_manager.devices.values():
            self.add_device(device.udid)
    
    def add_device(self, udid: str, daily_limit: Optional[int] = None, 
                  rate: Optional[float] = None, burst: Optional[int] = None):
        """为新设备添加速率限制"""
        if udid in self.device_buckets:
            logger.warning(f"设备 {udid} 已存在速率限制")
            return
        
        daily_limit = daily_limit or self.default_daily_limit
        rate = rate or self.default_rate
        burst = burst or self.default_burst
        
        self.device_buckets[udid] = TokenBucket(capacity=burst, rate=rate)
        self.daily_limits[udid] = daily_limit
        logger.info(f"为设备 {udid} 设置速率限制: 每日{daily_limit}条, 速率{rate}条/秒, 突发{burst}条")
    
    def remove_device(self, udid: str):
        """移除设备的速率限制"""
        if udid in self.device_buckets:
            del self.device_buckets[udid]
            del self.daily_limits[udid]
            logger.info(f"已移除设备 {udid} 的速率限制")
    
    def update_device_limit(self, udid: str, daily_limit: Optional[int] = None, 
                           rate: Optional[float] = None, burst: Optional[int] = None):
        """更新设备的速率限制"""
        if udid not in self.device_buckets:
            self.add_device(udid, daily_limit, rate, burst)
            return
        
        if daily_limit is not None:
            self.daily_limits[udid] = daily_limit
        
        if rate is not None or burst is not None:
            current_bucket = self.device_buckets[udid]
            new_rate = rate or current_bucket.rate
            new_burst = burst or current_bucket.capacity
            self.device_buckets[udid] = TokenBucket(capacity=new_burst, rate=new_rate)
        
        logger.info(f"已更新设备 {udid} 的速率限制")
    
    def check_daily_limit(self, udid: str) -> bool:
        """检查设备是否达到每日发送限制"""
        if udid not in self.device_manager.devices:
            return False
        
        device = self.device_manager.devices[udid]
        daily_limit = self.daily_limits.get(udid, self.default_daily_limit)
        
        if device.daily_messages_sent >= daily_limit:
            logger.warning(f"设备 {device.name} 已达到每日发送限制 ({daily_limit}条)")
            return False
        
        return True
    
    def wait_for_send(self, udid: str) -> bool:
        """等待直到可以发送消息,返回是否成功"""
        # 先检查每日限制
        if not self.check_daily_limit(udid):
            return False
        
        # 检查令牌桶
        if udid not in self.device_buckets:
            self.add_device(udid)
        
        bucket = self.device_buckets[udid]
        
        # 等待获取令牌
        wait_time = bucket.get_wait_time()
        if wait_time > 0:
            logger.debug(f"设备 {udid} 需要等待 {wait_time:.2f} 秒才能发送下一条消息")
            time.sleep(wait_time)
        
        # 消耗令牌
        if not bucket.consume():
            logger.error(f"设备 {udid} 无法获取发送令牌")
            return False
        
        # 添加随机延迟,模拟人类行为
        random_delay = random.uniform(*self.random_delay_range)
        logger.debug(f"添加随机延迟: {random_delay:.2f} 秒")
        time.sleep(random_delay)
        
        return True
    
    def add_content_variation(self, content: str) -> str:
        """为消息内容添加随机变化,避免完全相同"""
        # 在消息末尾添加随机的空格或标点
        variations = ["", " ", "  ", ".", "。", "!", "!", "?", "?"]
        variation = random.choice(variations)
        
        # 随机替换一些同义词(简单示例)
        synonyms = {
            "你好": ["您好", "嗨", "哈喽"],
            "谢谢": ["感谢", "多谢", "谢谢啦"],
            "请": ["烦请", "请您", "麻烦您"],
            "通知": ["告知", "提醒", "消息"]
        }
        
        for original, alternatives in synonyms.items():
            if original in content and random.random() < 0.3:
                content = content.replace(original, random.choice(alternatives), 1)
        
        return content + variation
    
    def schedule_send_time(self, base_hour: int = 9, end_hour: int = 18) -> datetime.datetime:
        """调度消息发送时间,只在工作时间发送"""
        now = datetime.datetime.now()
        
        # 如果当前时间在工作时间内,立即发送
        if base_hour <= now.hour < end_hour:
            return now
        
        # 否则,安排到下一个工作日的工作时间
        next_day = now + datetime.timedelta(days=1)
        send_time = next_day.replace(hour=base_hour, minute=0, second=0, microsecond=0)
        
        # 如果是周末,推迟到周一
        if send_time.weekday() >= 5:  # 5=周六, 6=周日
            days_to_monday = 7 - send_time.weekday()
            send_time += datetime.timedelta(days=days_to_monday)
        
        logger.info(f"消息将在 {send_time.strftime('%Y-%m-%d %H:%M:%S')} 发送")
        return send_time
    
    def reset_daily_limits(self):
        """重置所有设备的每日限制计数器"""
        self.device_manager.reset_daily_counts()
        logger.info("已重置所有设备的每日发送计数")
    
    def start_daily_reset_scheduler(self):
        """启动每日重置调度器,每天凌晨0点重置计数"""
        def reset_loop():
            while True:
                now = datetime.datetime.now()
                # 计算到明天0点的时间
                tomorrow = now + datetime.timedelta(days=1)
                midnight = tomorrow.replace(hour=0, minute=0, second=0, microsecond=0)
                sleep_time = (midnight - now).total_seconds()
                
                logger.info(f"将在 {midnight.strftime('%Y-%m-%d %H:%M:%S')} 重置每日计数,"
                            f"等待 {sleep_time/3600:.2f} 小时")
                
                time.sleep(sleep_time)
                self.reset_daily_limits()
        
        thread = threading.Thread(target=reset_loop, daemon=True)
        thread.start()
        logger.info("每日重置调度器已启动")

七、系统日志记录与异常处理机制

完善的日志记录和异常处理机制是企业级系统不可或缺的部分。它不仅可以帮助我们快速定位和解决问题,还可以为系统的优化和改进提供数据支持。在我们的iMessage群发系统中,我使用了loguru库来实现日志记录功能,它提供了简单易用的API和丰富的功能,支持日志分级、格式化、滚动存储等。

系统会记录所有重要的操作和事件,包括设备连接 / 断开、消息发送成功/失败、系统错误等。日志会同时输出到控制台和文件,文件日志按天进行滚动存储,保留最近30天的日志。同时,系统还会对常见的异常情况进行捕获和处理,避免因单个错误导致整个系统崩溃。

下面是系统日志记录与异常处理机制的实现代码:

复制代码
# 日志与异常处理模块 logger_config.py
import os
import sys
import traceback
from loguru import logger
import datetime

def setup_logger(log_dir: str = "logs", retention_days: int = 30):
    """配置系统日志"""
    # 创建日志目录
    if not os.path.exists(log_dir):
        os.makedirs(log_dir)
    
    # 移除默认的日志处理器
    logger.remove()
    
    # 控制台日志处理器
    logger.add(
        sys.stdout,
        format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>",
        level="INFO",
        colorize=True
    )
    
    # 文件日志处理器(INFO级别)
    info_log_path = os.path.join(log_dir, "imessage_system_{time:YYYY-MM-DD}.log")
    logger.add(
        info_log_path,
        format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}",
        level="INFO",
        rotation="00:00",  # 每天午夜滚动
        retention=f"{retention_days} days",
        compression="zip",
        encoding="utf-8"
    )
    
    # 错误日志处理器(ERROR级别)
    error_log_path = os.path.join(log_dir, "imessage_system_error_{time:YYYY-MM-DD}.log")
    logger.add(
        error_log_path,
        format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}\n{exception}",
        level="ERROR",
        rotation="00:00",
        retention=f"{retention_days} days",
        compression="
相关推荐
XIAOHEZIcode2 天前
Linux系统鼠标偏移常见原因以及修复方案
linux·运维·游戏
用户0328472220703 天前
如何搭建本地yum源(上)
运维
大树886 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠6 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质6 天前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
Inhand陈工6 天前
基于台达PLC与映翰通IG502的智慧水产养殖精准投喂与远程运维解决方案
运维·人工智能·物联网·阿里云·信息与通信
酣大智6 天前
ARP代理--工作原理
运维·网络·arp·arp代理
shushangyun_6 天前
2026年快消品B2B系统推荐:支持终端门店订货、促销政策自动化的工具?
java·运维·网络·数据库·人工智能·spring·自动化
施努卡机器视觉6 天前
SNK施努卡侧滑门锁上滑轮总成自动化装配线,从零件到组件,全流程精密制造方案
运维·自动化·制造
2601_961845156 天前
粉笔行测5000题电子版|pdf|解析
pdf·新媒体运营·github·个人开发·内容运营·规格说明书·极限编程