单主机管控多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="