前言
在大规模移动端运营、自动化测试、数据采集等场景下,群控手机平台成为了刚需。本文将深入讲解群控平台的技术原理、架构设计,并提供完整的代码实现,帮助读者从零搭建一套生产可用的群控系统。
一、群控平台核心原理
1.1 什么是群控平台?
群控平台是通过一台控制端(PC/服务器)同时控制多台手机设备,执行批量操作的自动化系统。其核心价值在于:
- 提升效率:一台电脑控制成百上千台手机
- 降低成本:减少人工操作,提高自动化程度
- 规模化运营:支持大规模批量操作
1.2 技术架构模式
┌─────────────────────────────────────────────┐
│ Web管理后台(Vue.js) │
│ 设备管理 | 任务管理 | 数据统计 │
└─────────────────┬───────────────────────────┘
│ WebSocket/HTTP
┌─────────────────▼───────────────────────────┐
│ API服务层(FastAPI) │
│ 设备注册 | 任务分发 | 结果收集 │
└─────────────────┬───────────────────────────┘
│
┌─────────────┼─────────────┐
│ │ │
┌───▼───┐ ┌───▼───┐ ┌───▼───┐
│ Redis │ │ MySQL │ │ MQ │
│ 缓存 │ │ 持久化│ │ 队列 │
└───┬───┘ └───────┘ └───┬───┘
│ │
└──────────┬───────────────┘
│
┌──────────▼──────────┐
│ Worker节点集群 │
│ (Python执行器) │
└──────────┬──────────┘
│ ADB/USB/网络
┌──────────▼──────────┐
│ 手机设备集群 │
│ Android/iOS设备 │
└─────────────────────┘
二、技术选型与对比
2.1 自动化框架对比
| 框架 | 语言 | 平台 | 优势 | 劣势 |
|---|---|---|---|---|
| Appium | 多语言 | Android/iOS | 跨平台、生态完善 | 速度较慢 |
| UIAutomator2 | Python | Android | 速度快、API简洁 | 仅支持Android |
| Airtest | Python | Android/iOS | 图像识别、网易开源 | 学习曲线陡 |
| ADB | Shell | Android | 底层控制、灵活 | 功能单一 |
推荐方案: Android使用UIAutomator2,iOS使用WebDriverAgent
2.2 后端框架选型
- FastAPI(推荐):高性能、异步支持、自动API文档
- Flask:轻量级、易上手
- Django:功能全面、适合快速开发
- Go Gin:极致性能、适合高并发场景
三、核心模块实现
3.1 设备管理模块
import uiautomator2 as u2
from typing import Dict, List, Optional
import logging
class DeviceManager:
"""设备管理器"""
def __init__(self):
self.devices: Dict[str, u2.Device] = {}
self.logger = logging.getLogger(__name__)
def connect_device(self, device_id: str) -> Optional[u2.Device]:
"""连接设备"""
try:
device = u2.connect(device_id)
self.devices[device_id] = device
# 获取设备信息
info = device.info
self.logger.info(f"设备连接成功: {device_id}, {info}")
return device
except Exception as e:
self.logger.error(f"设备连接失败: {device_id}, {str(e)}")
return None
def disconnect_device(self, device_id: str):
"""断开设备连接"""
if device_id in self.devices:
del self.devices[device_id]
self.logger.info(f"设备已断开: {device_id}")
def get_device_info(self, device_id: str) -> Dict:
"""获取设备信息"""
device = self.devices.get(device_id)
if device:
return {
'device_id': device_id,
'brand': device.info['brand'],
'model': device.info['model'],
'version': device.info['version'],
'screen_size': device.info['displaySize'],
'status': 'online'
}
return {'status': 'offline'}
def batch_execute(
self,
device_ids: List[str],
script: str
) -> List[Dict]:
"""批量执行脚本"""
results = []
for device_id in device_ids:
device = self.devices.get(device_id)
if device:
try:
result = self._execute_script(device, script)
results.append({
'device_id': device_id,
'status': 'success',
'result': result
})
except Exception as e:
results.append({
'device_id': device_id,
'status': 'failed',
'error': str(e)
})
return results
def _execute_script(
self,
device: u2.Device,
script: str
) -> Dict:
"""执行自动化脚本"""
# 创建执行环境
context = {'d': device, 'result': {}}
# 执行脚本
exec(script, context)
return context.get('result', {})
def get_all_devices(self) -> List[Dict]:
"""获取所有设备列表"""
return [
self.get_device_info(device_id)
for device_id in self.devices.keys()
]
3.2 任务调度模块
from celery import Celery
from celery.result import AsyncResult
from typing import Dict, List
import json
# 初始化Celery
app = Celery(
'group_control',
broker='redis://localhost:6379/0',
backend='redis://localhost:6379/1'
)
# Celery配置
app.conf.update(
task_serializer='json',
accept_content=['json'],
result_serializer='json',
timezone='Asia/Shanghai',
enable_utc=True,
task_track_started=True,
task_time_limit=3600, # 1小时超时
worker_prefetch_multiplier=1,
worker_max_tasks_per_child=100
)
@app.task(bind=True)
def execute_task(
self,
task_id: str,
device_ids: List[str],
script: str
):
"""异步执行任务"""
from device_manager import DeviceManager
from database import save_task_result
device_manager = DeviceManager()
# 更新任务状态
self.update_state(
state='PROGRESS',
meta={'current': 0, 'total': len(device_ids)}
)
results = []
for index, device_id in enumerate(device_ids):
# 执行脚本
result = device_manager.batch_execute([device_id], script)
results.extend(result)
# 更新进度
self.update_state(
state='PROGRESS',
meta={
'current': index + 1,
'total': len(device_ids),
'results': results
}
)
# 保存结果到数据库
save_task_result(task_id, results)
return {
'task_id': task_id,
'status': 'completed',
'results': results
}
class TaskScheduler:
"""任务调度器"""
def __init__(self):
self.celery_app = app
def submit_task(
self,
task_name: str,
device_ids: List[str],
script: str
) -> str:
"""提交任务"""
task_id = self._generate_task_id()
# 异步执行
task = execute_task.delay(
task_id,
device_ids,
script
)
# 保存任务信息到数据库
self._save_task_info(task_id, task_name, task.id)
return task_id
def get_task_status(self, task_id: str) -> Dict:
"""获取任务状态"""
# 从数据库获取Celery任务ID
celery_task_id = self._get_celery_task_id(task_id)
# 查询任务状态
task = AsyncResult(celery_task_id, app=self.celery_app)
if task.state == 'PENDING':
return {
'status': 'pending',
'message': '任务等待中'
}
elif task.state == 'PROGRESS':
return {
'status': 'running',
'current': task.info.get('current', 0),
'total': task.info.get('total', 1),
'results': task.info.get('results', [])
}
elif task.state == 'SUCCESS':
return {
'status': 'completed',
'results': task.result
}
else: # FAILURE
return {
'status': 'failed',
'error': str(task.info)
}
def cancel_task(self, task_id: str):
"""取消任务"""
celery_task_id = self._get_celery_task_id(task_id)
self.celery_app.control.revoke(
celery_task_id,
terminate=True
)
def _generate_task_id(self) -> str:
"""生成任务ID"""
import uuid
return str(uuid.uuid4())
def _save_task_info(
self,
task_id: str,
task_name: str,
celery_task_id: str
):
"""保存任务信息"""
# 实现数据库保存逻辑
pass
def _get_celery_task_id(self, task_id: str) -> str:
"""获取Celery任务ID"""
# 从数据库查询
pass
3.3 API服务模块
from fastapi import FastAPI, WebSocket, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from typing import List, Optional
import asyncio
import json
app = FastAPI(
title="群控平台API",
description="群控手机平台后端服务",
version="1.0.0"
)
# CORS配置
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 请求模型
class DeviceConnectRequest(BaseModel):
device_id: str
class TaskExecuteRequest(BaseModel):
task_name: str
device_ids: List[str]
script: str
# 导入模块
from device_manager import DeviceManager
from task_scheduler import TaskScheduler
device_manager = DeviceManager()
task_scheduler = TaskScheduler()
@app.get("/")
async def root():
"""根路径"""
return {"message": "群控平台API服务运行中"}
@app.get("/devices")
async def get_devices():
"""获取所有设备"""
devices = device_manager.get_all_devices()
return {"devices": devices}
@app.post("/devices/connect")
async def connect_device(request: DeviceConnectRequest):
"""连接设备"""
device = device_manager.connect_device(request.device_id)
if device:
return {
"status": "success",
"message": f"设备 {request.device_id} 连接成功"
}
raise HTTPException(
status_code=400,
detail=f"设备 {request.device_id} 连接失败"
)
@app.delete("/devices/{device_id}")
async def disconnect_device(device_id: str):
"""断开设备连接"""
device_manager.disconnect_device(device_id)
return {"status": "success"}
@app.get("/devices/{device_id}")
async def get_device_info(device_id: str):
"""获取设备信息"""
info = device_manager.get_device_info(device_id)
return info
@app.post("/tasks")
async def execute_task(request: TaskExecuteRequest):
"""执行任务"""
task_id = task_scheduler.submit_task(
request.task_name,
request.device_ids,
request.script
)
return {
"task_id": task_id,
"status": "submitted"
}
@app.get("/tasks/{task_id}")
async def get_task_status(task_id: str):
"""获取任务状态"""
status = task_scheduler.get_task_status(task_id)
return status
@app.delete("/tasks/{task_id}")
async def cancel_task(task_id: str):
"""取消任务"""
task_scheduler.cancel_task(task_id)
return {"status": "cancelled"}
# WebSocket实时通信
@app.websocket("/ws/{device_id}")
async def websocket_endpoint(websocket: WebSocket, device_id: str):
"""WebSocket连接"""
await websocket.accept()
try:
while True:
# 接收前端指令
data = await websocket.receive_text()
command = json.loads(data)
# 执行操作
if command['action'] == 'screenshot':
# 截图
device = device_manager.devices.get(device_id)
if device:
screenshot = device.screenshot()
await websocket.send_json({
'type': 'screenshot',
'data': screenshot
})
elif command['action'] == 'execute':
# 执行脚本
script = command['script']
result = device_manager.batch_execute(
[device_id],
script
)
await websocket.send_json({
'type': 'result',
'data': result
})
except Exception as e:
print(f"WebSocket错误: {str(e)}")
finally:
await websocket.close()
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
3.4 自动化脚本示例
# 示例1:抖音自动点赞
def douyin_auto_like(d):
"""抖音自动点赞脚本"""
# 打开抖音
d.app_start('com.ss.android.ugc.aweme')
d.sleep(3)
# 循环点赞
for i in range(10):
# 双击点赞
d.double_click(540, 960)
d.sleep(1)
# 滑动到下一个视频
d.swipe(540, 1500, 540, 500)
d.sleep(2)
# 返回结果
result['liked_count'] = 10
# 示例2:微信自动加好友
def wechat_auto_add_friend(d):
"""微信自动加好友脚本"""
# 打开微信
d.app_start('com.tencent.mm')
d.sleep(3)
# 点击发现
d(resourceId='com.tencent.mm:id/f3s').click()
d.sleep(1)
# 点击添加朋友
d(text='添加朋友').click()
d.sleep(1)
# 输入微信号
d(resourceId='com.tencent.mm:id/djk').set_text('微信号')
d.sleep(1)
# 点击搜索
d(resourceId='com.tencent.mm:id/djo').click()
d.sleep(2)
# 点击添加到通讯录
d(text='添加到通讯录').click()
d.sleep(1)
# 发送好友申请
d(resourceId='com.tencent.mm:id/djk').set_text('你好,加个好友')
d.sleep(1)
d(text='发送').click()
result['status'] = 'success'
# 示例3:批量发朋友圈
def wechat_moments_batch_post(d):
"""批量发朋友圈脚本"""
import time
contents = [
'今天天气真好!',
'努力工作,快乐生活!',
'分享是一种美德!'
]
for content in contents:
# 打开朋友圈
d.app_start('com.tencent.mm')
d.sleep(2)
# 点击发现
d(resourceId='com.tencent.mm:id/f3s').click()
d.sleep(1)
# 点击朋友圈
d(text='朋友圈').click()
d.sleep(2)
# 点击发布
d(resourceId='com.tencent.mm:id/ky').click()
d.sleep(1)
# 输入内容
d(resourceId='com.tencent.mm:id/b43').set_text(content)
d.sleep(1)
# 发布
d(text='发表').click()
d.sleep(3)
# 返回
d.press('back')
d.sleep(1)
result['posted_count'] = len(contents)
四、前端界面实现
4.1 设备管理页面
<template>
<div class="device-management">
<el-card>
<div slot="header">
<span>设备管理</span>
<el-button
style="float: right"
type="primary"
@click="showConnectDialog = true"
>
添加设备
</el-button>
</div>
<el-table :data="devices" style="width: 100%">
<el-table-column prop="device_id" label="设备ID" width="180" />
<el-table-column prop="brand" label="品牌" width="120" />
<el-table-column prop="model" label="型号" width="150" />
<el-table-column prop="version" label="系统版本" width="120" />
<el-table-column prop="status" label="状态" width="100">
<template slot-scope="scope">
<el-tag :type="scope.row.status === 'online' ? 'success' : 'danger'">
{{ scope.row.status }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作">
<template slot-scope="scope">
<el-button size="mini" @click="viewScreen(scope.row)">投屏</el-button>
<el-button size="mini" @click="executeScript(scope.row)">执行</el-button>
<el-button
size="mini"
type="danger"
@click="disconnectDevice(scope.row)"
>
断开
</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 连接设备对话框 -->
<el-dialog title="连接设备" :visible.sync="showConnectDialog">
<el-form>
<el-form-item label="设备ID">
<el-input v-model="deviceId" placeholder="请输入设备ID或IP地址" />
</el-form-item>
</el-form>
<div slot="footer">
<el-button @click="showConnectDialog = false">取消</el-button>
<el-button type="primary" @click="connectDevice">连接</el-button>
</div>
</el-dialog>
<!-- 实时投屏对话框 -->
<el-dialog
title="设备投屏"
:visible.sync="showScreenDialog"
width="60%"
>
<div class="screen-container">
<img :src="screenImage" style="width: 100%" />
</div>
</el-dialog>
</div>
</template>
<script>
import { getDevices, connectDevice, disconnectDevice } from '@/api/device'
export default {
data() {
return {
devices: [],
showConnectDialog: false,
showScreenDialog: false,
deviceId: '',
screenImage: '',
ws: null
}
},
mounted() {
this.loadDevices()
},
methods: {
async loadDevices() {
const res = await getDevices()
this.devices = res.devices
},
async connectDevice() {
try {
await connectDevice({ device_id: this.deviceId })
this.$message.success('设备连接成功')
this.showConnectDialog = false
this.loadDevices()
} catch (error) {
this.$message.error('设备连接失败')
}
},
async disconnectDevice(device) {
try {
await disconnectDevice(device.device_id)
this.$message.success('设备已断开')
this.loadDevices()
} catch (error) {
this.$message.error('断开失败')
}
},
viewScreen(device) {
this.showScreenDialog = true
this.connectWebSocket(device.device_id)
},
connectWebSocket(deviceId) {
const wsUrl = `ws://localhost:8000/ws/${deviceId}`
this.ws = new WebSocket(wsUrl)
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data)
if (data.type === 'screenshot') {
this.screenImage = data.data
}
}
// 定时请求截图
setInterval(() => {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({ action: 'screenshot' }))
}
}, 1000)
},
executeScript(device) {
this.$router.push({
path: '/script-editor',
query: { device_id: device.device_id }
})
}
}
}
</script>
4.2 任务管理页面
<template>
<div class="task-management">
<el-row :gutter="20">
<el-col :span="16">
<el-card>
<div slot="header">任务列表</div>
<el-table :data="tasks">
<el-table-column prop="task_name" label="任务名称" />
<el-table-column prop="status" label="状态">
<template slot-scope="scope">
<el-tag :type="getStatusType(scope.row.status)">
{{ scope.row.status }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="created_at" label="创建时间" />
<el-table-column label="操作">
<template slot-scope="scope">
<el-button size="mini" @click="viewTask(scope.row)">查看</el-button>
<el-button
size="mini"
type="danger"
@click="cancelTask(scope.row)"
>
取消
</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</el-col>
<el-col :span="8">
<el-card>
<div slot="header">创建任务</div>
<el-form :model="taskForm">
<el-form-item label="任务名称">
<el-input v-model="taskForm.task_name" />
</el-form-item>
<el-form-item label="选择设备">
<el-select
v-model="taskForm.device_ids"
multiple
placeholder="请选择设备"
>
<el-option
v-for="device in onlineDevices"
:key="device.device_id"
:label="device.device_id"
:value="device.device_id"
/>
</el-select>
</el-form-item>
<el-form-item label="脚本内容">
<el-input
v-model="taskForm.script"
type="textarea"
:rows="10"
placeholder="请输入Python脚本"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitTask">提交任务</el-button>
</el-form-item>
</el-form>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script>
import { getTasks, createTask, getTaskStatus, cancelTask } from '@/api/task'
import { getDevices } from '@/api/device'
export default {
data() {
return {
tasks: [],
devices: [],
taskForm: {
task_name: '',
device_ids: [],
script: ''
}
}
},
computed: {
onlineDevices() {
return this.devices.filter(d => d.status === 'online')
}
},
mounted() {
this.loadTasks()
this.loadDevices()
},
methods: {
async loadTasks() {
const res = await getTasks()
this.tasks = res.tasks
},
async loadDevices() {
const res = await getDevices()
this.devices = res.devices
},
async submitTask() {
try {
const res = await createTask(this.taskForm)
this.$message.success('任务提交成功')
// 轮询任务状态
this.pollTaskStatus(res.task_id)
// 重置表单
this.taskForm = {
task_name: '',
device_ids: [],
script: ''
}
} catch (error) {
this.$message.error('任务提交失败')
}
},
async pollTaskStatus(taskId) {
const timer = setInterval(async () => {
const status = await getTaskStatus(taskId)
if (status.status === 'completed' || status.status === 'failed') {
clearInterval(timer)
this.loadTasks()
}
}, 2000)
},
getStatusType(status) {
const types = {
pending: 'info',
running: 'warning',
completed: 'success',
failed: 'danger'
}
return types[status] || 'info'
},
viewTask(task) {
this.$router.push({
path: '/task-detail',
query: { task_id: task.task_id }
})
},
async cancelTask(task) {
try {
await cancelTask(task.task_id)
this.$message.success('任务已取消')
this.loadTasks()
} catch (error) {
this.$message.error('取消失败')
}
}
}
}
</script>
五、性能优化与最佳实践
5.1 性能优化策略
1. 连接池管理
from queue import Queue
import threading
class DevicePool:
"""设备连接池"""
def __init__(self, max_size=100):
self.pool = Queue(maxsize=max_size)
self.lock = threading.Lock()
def get_device(self, device_id):
"""获取设备连接"""
with self.lock:
if not self.pool.empty():
return self.pool.get()
return None
def return_device(self, device):
"""归还设备连接"""
with self.lock:
if not self.pool.full():
self.pool.put(device)
2. 异步并发控制
import asyncio
from typing import List
async def batch_execute_async(
device_ids: List[str],
script: str,
max_concurrent=10
):
"""异步批量执行"""
semaphore = asyncio.Semaphore(max_concurrent)
async def execute_with_limit(device_id):
async with semaphore:
return await execute_on_device(device_id, script)
tasks = [execute_with_limit(did) for did in device_ids]
results = await asyncio.gather(*tasks)
return results
3. 结果缓存
import redis
import json
class ResultCache:
"""结果缓存"""
def __init__(self):
self.redis = redis.Redis(host='localhost', port=6379, db=2)
def get(self, task_id: str):
"""获取缓存结果"""
data = self.redis.get(f'task:result:{task_id}')
return json.loads(data) if data else None
def set(self, task_id: str, result: dict, expire=3600):
"""设置缓存结果"""
self.redis.setex(
f'task:result:{task_id}',
expire,
json.dumps(result)
)
5.2 异常处理
import functools
import time
from typing import Callable
def retry(max_retries=3, delay=1):
"""重试装饰器"""
def decorator(func: Callable):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(max_retries):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_retries - 1:
raise e
time.sleep(delay * (attempt + 1))
return wrapper
return decorator
# 使用示例
@retry(max_retries=3, delay=2)
def connect_device_with_retry(device_id):
"""带重试的设备连接"""
return u2.connect(device_id)
5.3 监控告警
from prometheus_client import Counter, Histogram, Gauge
# 定义指标
task_counter = Counter('tasks_total', 'Total tasks executed')
task_duration = Histogram('task_duration_seconds', 'Task execution duration')
device_gauge = Gauge('devices_online', 'Number of online devices')
# 记录指标
def execute_task_with_metrics(task_id, device_ids, script):
start_time = time.time()
try:
result = execute_task(task_id, device_ids, script)
task_counter.inc()
return result
finally:
duration = time.time() - start_time
task_duration.observe(duration)
六、部署方案
6.1 Docker部署
version: '3.8'
services:
# API服务
api:
build: ./backend
ports:
- "8000:8000"
environment:
- REDIS_URL=redis://redis:6379/0
- MYSQL_URL=mysql://root:password@mysql:3306/group_control
depends_on:
- mysql
- redis
volumes:
- ./backend:/app
command: uvicorn main:app --host 0.0.0.0 --port 8000
# Celery Worker
worker:
build: ./backend
environment:
- REDIS_URL=redis://redis:6379/0
depends_on:
- redis
volumes:
- ./backend:/app
command: celery -A tasks worker --loglevel=info --concurrency=4
# Celery Beat(定时任务)
beat:
build: ./backend
environment:
- REDIS_URL=redis://redis:6379/0
depends_on:
- redis
volumes:
- ./backend:/app
command: celery -A tasks beat --loglevel=info
# MySQL
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: group_control
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
# Redis
redis:
image: redis:latest
ports:
- "6379:6379"
volumes:
- redis_data:/data
# 前端
frontend:
build: ./frontend
ports:
- "80:80"
depends_on:
- api
volumes:
mysql_data:
redis_data:
6.2 启动命令
# 构建镜像
docker-compose build
# 启动服务
docker-compose up -d
# 查看日志
docker-compose logs -f api
# 停止服务
docker-compose down
七、总结与展望
7.1 核心要点
- 架构设计:采用微服务架构,前后端分离,易于扩展
- 技术选型:Python + FastAPI + Celery + Redis,性能与开发效率兼顾
- 自动化框架:UIAutomator2速度快、API简洁,适合Android群控
- 任务调度:Celery实现异步任务分发和进度跟踪
- 实时通信:WebSocket实现设备投屏和实时控制
7.2 扩展方向
- 云手机集成:接入ARM云手机,无需物理设备
- AI识别:集成OCR、图像识别,提升自动化智能化
- 分布式部署:支持多节点部署,提升并发能力
- 协议逆向:研究APP协议,实现更底层的控制
- 风控对抗:研究反检测技术,提升稳定性
7.3 注意事项
⚠️ 法律合规:群控技术应用于合法场景,遵守平台规则
⚠️ 账号安全:使用代理IP、设备指纹等技术,避免账号关联
⚠️ 性能监控:实时监控系统资源,避免过载
⚠️ 异常处理:完善的错误处理和重试机制
八、参考资料
作者介绍:资深Python开发工程师,专注于自动化测试、爬虫开发、群控系统等领域。
文章首发:掘金社区
版权声明:本文为原创文章,转载请注明出处。
如果这篇文章对你有帮助,欢迎点赞、收藏、转发!有问题欢迎在评论区留言讨论。🚀