本文目录导读:

- 轮询分配(Round-Robin)
- 最少工单分配(Least-Busy / Load Balancing)
- 技能匹配分配(Skill-Based)
- 基于权重的随机分配(Weighted Random)
- 高级:基于规则引擎(Rule Engine)
- 如何集成到现有 PHP 项目?
- 需要注意的关键点
- 推荐的开源库
在PHP项目中实现工单的自动指派,通常需要根据业务规则(如轮询、技能匹配、负载均衡等)来设计算法。
以下是几种常见的自动指派策略及其 PHP 实现思路:
轮询分配(Round-Robin)
适用场景: 团队人员能力均等,希望每个人的工单数量大致平均。
逻辑: 记录上一次被指派的人员ID,下次分配给下一个人。
PHP 实现示例:
<?php
class TicketAssigner {
private $db;
public function __construct($db) {
$this->db = $db;
}
/**
* 获取下一个应该被指派的人员ID(轮询)
* @param int $teamId 团队ID
* @return int 人员ID
*/
public function getNextAgentRoundRobin($teamId) {
// 1. 获取该团队所有在线/可用的客服
$agents = $this->db->query(
"SELECT id FROM users
WHERE team_id = ? AND status = 'online'
ORDER BY id ASC",
[$teamId]
);
if (empty($agents)) {
throw new Exception("没有可用客服");
}
// 2. 获取上次分配的最后一个人(存储于 team_config 表)
$lastAgentId = $this->db->query(
"SELECT last_assigned_agent_id FROM team_config WHERE team_id = ?",
[$teamId]
)[0]['last_assigned_agent_id'] ?? 0;
// 3. 找到下一个
$nextAgent = null;
foreach ($agents as $agent) {
if ($agent['id'] > $lastAgentId) {
$nextAgent = $agent;
break;
}
}
// 如果到最后了,从头开始
if (!$nextAgent) {
$nextAgent = $agents[0];
}
// 4. 更新最后指派记录
$this->db->execute(
"UPDATE team_config SET last_assigned_agent_id = ? WHERE team_id = ?",
[$nextAgent['id'], $teamId]
);
return $nextAgent['id'];
}
}
最少工单分配(Least-Busy / Load Balancing)
适用场景: 人员处理速度不同,希望照顾处理慢的人。
逻辑: 查询当前每人未关闭工单数,指派给最少的人。
SQL 实现:
SELECT
u.id,
u.name,
COUNT(t.id) AS current_ticket_count
FROM users u
LEFT JOIN tickets t
ON t.assignee_id = u.id
AND t.status NOT IN ('closed', 'resolved')
WHERE u.team_id = ? AND u.status = 'active'
GROUP BY u.id
ORDER BY current_ticket_count ASC
LIMIT 1;
PHP 使用:
$agent = $db->query($sql, [$teamId]); $assignToId = $agent[0]['id'];
技能匹配分配(Skill-Based)
适用场景: 工单有分类(技术、财务、售后),客服有专长。
数据结构设计:
users_skills表:user_id,skill_id(对应工单分类ID)tickets表:category_id
算法步骤:
- 获取工单的
category_id。 - 查询所有拥有该技能且在线的人员。
- 在这些人员中,再使用轮询或最少工单逻辑。
$categoryId = $ticketData['category_id'];
$agents = $db->query(
"SELECT u.id, COUNT(t.id) as cnt
FROM users u
JOIN users_skills us ON us.user_id = u.id AND us.skill_id = ?
LEFT JOIN tickets t ON t.assignee_id = u.id AND t.status NOT IN ('closed')
WHERE u.status = 'online'
GROUP BY u.id
ORDER BY cnt ASC
LIMIT 1",
[$categoryId]
);
基于权重的随机分配(Weighted Random)
适用场景: 需要给老员工或高级客服更多工单,但仍保留随机性。
权重存储: users.assignment_weight (整数,越大被分配概率越高)
算法:
function weightedRandomSelect($agents) {
$totalWeight = array_sum(array_column($agents, 'weight'));
$random = mt_rand(1, $totalWeight);
foreach ($agents as $agent) {
$random -= $agent['weight'];
if ($random <= 0) {
return $agent['id'];
}
}
}
高级:基于规则引擎(Rule Engine)
对于复杂业务(如“VIP客户优先分配给组长”、“8小时未响应自动转派”),建议引入规则引擎。
伪代码示例:
$rules = [
['condition' => $ticket['priority'] === 'urgent' && $ticket['customer_level'] === 'vip',
'action' => 'assignToGroupLeader'],
['condition' => $ticket['category'] === 'technical',
'action' => 'assignBySkill'],
['condition' => true, // 默认
'action' => 'roundRobin']
];
foreach ($rules as $rule) {
if ($rule['condition']) {
$assigner->{$rule['action']}($ticket);
break;
}
}
如何集成到现有 PHP 项目?
方案 A:创建定时任务(Cron Job)
- 每分钟执行一个 PHP 脚本,查询是否有未指派的工单。
- 调用上述分配函数,执行
UPDATE tickets SET assignee_id = ?。
方案 B:实时分配(在工单创建时)
- 在
TicketController::store()方法中,调用分配函数,直接将assignee_id写入数据库。
// 工单创建后 $assigner = new TicketAssigner($db); $agentId = $assigner->getLeastBusyAgent($teamId); $ticket->assignee_id = $agentId; $ticket->save();
方案 C:使用消息队列(如 Redis / RabbitMQ)
- 适合高并发场景,将待分配工单推入队列,Worker 进程异步处理分配。
需要注意的关键点
-
避免死锁/竞争:在高并发下,两个请求同时查到同一个人是“最少工单”,导致分配重叠。解决方案:
- 使用数据库行锁
SELECT ... FOR UPDATE。 - 或者在分配后立刻检查并回滚(乐观锁)。
- 使用数据库行锁
-
分配记录:建议记录
ticket_assign_log表,记录谁、什么时间、通过什么规则分配,方便回溯。 -
手动干预:自动分配后,应保留管理员手动改派的功能,并设置自动分配优先级低于手动指定。
-
休假/离线状态:分配前必须检查客服的在线状态,离线人员应跳过。
推荐的开源库
如果不想从零实现,可以考虑:
- PHP-DI / Workflow:结合规则引擎。
- Laravel 队列 + 事件:如果使用 Laravel,可以用
Event::listen('ticket.created')触发分配逻辑。
最简单的起步是“最少工单 + 技能匹配”,用 SQL 一次查询即可解决;如果团队规模大且规则复杂,再引入轮询或权重随机,关键是保证公平与可用性之间的平衡。