diff --git a/Cunkebao/app/workspace/traffic-distribution/[id]/edit/page.tsx b/Cunkebao/app/workspace/traffic-distribution/[id]/edit/page.tsx index 46e586a1..3a055ed0 100644 --- a/Cunkebao/app/workspace/traffic-distribution/[id]/edit/page.tsx +++ b/Cunkebao/app/workspace/traffic-distribution/[id]/edit/page.tsx @@ -194,9 +194,7 @@ export default function EditTrafficDistributionPage({ params }: { params: Promis

编辑流量分发

- + diff --git a/Cunkebao/app/workspace/traffic-distribution/new/components/basic-info-step.tsx b/Cunkebao/app/workspace/traffic-distribution/new/components/basic-info-step.tsx index d18a396d..43852bd4 100644 --- a/Cunkebao/app/workspace/traffic-distribution/new/components/basic-info-step.tsx +++ b/Cunkebao/app/workspace/traffic-distribution/new/components/basic-info-step.tsx @@ -17,7 +17,7 @@ export default function BasicInfoStep({ onNext, initialData = {} }: BasicInfoSte const [formData, setFormData] = useState({ name: initialData.name ?? `流量分发 ${format(new Date(), "yyyyMMdd HHmm")}`, distributeType: initialData.distributeType ?? "1", - maxPerDay: initialData.maxPerDay ?? 50, + maxPerDay: initialData.maxPerDay ?? 100, timeType: initialData.timeType ?? "2", startTime: initialData.startTime ?? "09:00", endTime: initialData.endTime ?? "18:00", @@ -69,7 +69,7 @@ export default function BasicInfoStep({ onNext, initialData = {} }: BasicInfoSte 均分配 (流量将均分配给所有客服) -
+ {/*
+
*/} diff --git a/Cunkebao/app/workspace/traffic-distribution/new/components/target-settings-step.tsx b/Cunkebao/app/workspace/traffic-distribution/new/components/target-settings-step.tsx index 93be1813..e5d1c18f 100644 --- a/Cunkebao/app/workspace/traffic-distribution/new/components/target-settings-step.tsx +++ b/Cunkebao/app/workspace/traffic-distribution/new/components/target-settings-step.tsx @@ -103,18 +103,19 @@ export default function TargetSettingsStep({ onNext, onBack, initialData = {}, s {/* 设备选择弹窗 */} - - 选择设备 -
+ + 选择设备 +
+ {/* 搜索和筛选 */}
setSearch(e.target.value)} - className="flex-1" + className="flex-1 rounded-lg border-gray-200 focus:border-blue-500 focus:ring-2 focus:ring-blue-100" />
-
+ {/* 设备列表 */} +
{loading ? (
加载中...
) : filteredDevices.length === 0 ? ( @@ -132,11 +134,15 @@ export default function TargetSettingsStep({ onNext, onBack, initialData = {}, s filteredDevices.map(device => ( )) )}
-
-
diff --git a/Cunkebao/app/workspace/traffic-distribution/new/components/traffic-pool-step.tsx b/Cunkebao/app/workspace/traffic-distribution/new/components/traffic-pool-step.tsx index de023f3c..be56a8b0 100644 --- a/Cunkebao/app/workspace/traffic-distribution/new/components/traffic-pool-step.tsx +++ b/Cunkebao/app/workspace/traffic-distribution/new/components/traffic-pool-step.tsx @@ -113,25 +113,29 @@ export default function TrafficPoolStep({ onSubmit, onBack, initialData = {}, de
- - 选择流量池 -
+ + 选择流量池 +
+ {/* 搜索栏 */}
setSearchTerm(e.target.value)} - className="pl-10" + className="pl-10 rounded-lg border-gray-200 focus:border-blue-500 focus:ring-2 focus:ring-blue-100" />
-
+ {/* 流量池列表 */} +
{pagedPools.map((pool) => ( - togglePool(pool.label)} >
@@ -143,27 +147,35 @@ export default function TrafficPoolStep({ onSubmit, onBack, initialData = {}, de

{poolDescMap[pool.label] || ""}

-
- {pool.count} 人 - togglePool(pool.label)} - onClick={e => e.stopPropagation()} - /> -
- + {pool.count} 人 + { + e.stopPropagation(); + togglePool(pool.label); + }} + onClick={e => e.stopPropagation()} + /> +
))}
{/* 分页按钮 */} {totalPages > 1 && ( -
+
第 {currentPage} / {totalPages} 页
)} -
-
diff --git a/Cunkebao/app/workspace/traffic-distribution/new/page.tsx b/Cunkebao/app/workspace/traffic-distribution/new/page.tsx index 9446d63a..1db3f045 100644 --- a/Cunkebao/app/workspace/traffic-distribution/new/page.tsx +++ b/Cunkebao/app/workspace/traffic-distribution/new/page.tsx @@ -141,9 +141,6 @@ export default function NewTrafficDistribution() {

新建流量分发

-
diff --git a/Cunkebao/app/workspace/traffic-distribution/page.tsx b/Cunkebao/app/workspace/traffic-distribution/page.tsx index 85775f09..48c858dd 100644 --- a/Cunkebao/app/workspace/traffic-distribution/page.tsx +++ b/Cunkebao/app/workspace/traffic-distribution/page.tsx @@ -240,8 +240,8 @@ export default function TrafficDistributionPage() {

{plan.name}

- - {plan.status === "active" ? "进行中" : "已暂停"} + + {plan.status == 1 ? "进行中" : "已暂停"} {plan.config.pools.map((group, index) => ( @@ -292,8 +292,8 @@ export default function TrafficDistributionPage() {
-
{plan.config.total.dailyAverage}
-
日均分发人数
+
{plan.config.total.totalAccounts}
+
分发账号
{plan.config.total.deviceCount}
@@ -322,7 +322,7 @@ export default function TrafficDistributionPage() {
- 上次执行: {plan.lastUpdated} + 上次执行: {plan.config.lastUpdated}
创建人: {plan.creatorName}
diff --git a/Server/application/api/controller/WebSocketController.php b/Server/application/api/controller/WebSocketController.php index 0119808c..249bfe75 100644 --- a/Server/application/api/controller/WebSocketController.php +++ b/Server/application/api/controller/WebSocketController.php @@ -649,24 +649,24 @@ class WebSocketController extends BaseController // 消息拼接 msgType(1:文本 3:图片 43:视频 47:动图表情包(gif、其他表情包) 49:小程序/其他:图文、文件) // 当前,type 为文本、图片、动图表情包的时候,content为string, 其他情况为对象 {type: 'file/link/...', url: '', title: '', thunmbPath: '', desc: ''} - $result = [ - "cmdType" => "CmdSendMessage", - "content" => $dataArray['content'], - "msgSubType" => 0, - "msgType" => $dataArray['msgType'], - "seq" => time(), - "wechatAccountId" => $dataArray['wechatAccountId'], - "wechatChatroomId" => 0, - "wechatFriendId" => $dataArray['wechatFriendId'], - ]; + $result = [ + "cmdType" => "CmdSendMessage", + "content" => $dataArray['content'], + "msgSubType" => 0, + "msgType" => $dataArray['msgType'], + "seq" => time(), + "wechatAccountId" => $dataArray['wechatAccountId'], + "wechatChatroomId" => 0, + "wechatFriendId" => $dataArray['wechatFriendId'], + ]; - $result = json_encode($result); - $this->client->send($result); - $message = $this->client->receive(); - $message = json_decode($message, 1); - //关闭WS链接 - $this->client->close(); - //Log::write('WS个人消息发送'); + $result = json_encode($result); + $this->client->send($result); + $message = $this->client->receive(); + $message = json_decode($message, 1); + //关闭WS链接 + $this->client->close(); + //Log::write('WS个人消息发送'); return $message; } diff --git a/Server/application/cunkebao/controller/WorkbenchController.php b/Server/application/cunkebao/controller/WorkbenchController.php index 5d4f4309..0fa2b422 100644 --- a/Server/application/cunkebao/controller/WorkbenchController.php +++ b/Server/application/cunkebao/controller/WorkbenchController.php @@ -188,7 +188,7 @@ class WorkbenchController extends Controller $list = Workbench::where($where) ->with($with) - ->field('id,name,type,status,autoStart,userId,createTime,updateTime') + ->field('id,companyId,name,type,status,autoStart,userId,createTime,updateTime') ->order('id', 'desc') ->page($page, $limit) ->select() @@ -260,12 +260,50 @@ class WorkbenchController extends Controller $item->config = $item->trafficConfig; $item->config->devices = json_decode($item->config->devices, true); $item->config->pools = json_decode($item->config->pools, true); + $config_item = Db::name('workbench_traffic_config_item')->where(['workbenchId' => $item->id])->order('id DESC')->find(); + $item->config->lastUpdated = !empty($config_item) ? date('Y-m-d H:i',$config_item['createTime']) : '--'; + + //统计 + $labels = $item->config->pools; + $totalUsers = Db::table('s2_wechat_friend')->alias('wf') + ->join(['s2_company_account' => 'sa'], 'sa.id = wf.accountId', 'left') + ->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left') + ->where([ + ['wf.isDeleted', '=', 0], + ['sa.departmentId', '=', $item->companyId] + ]) + ->whereIn('wa.currentDeviceId', $item->config->devices) + ->field('wf.id,wf.wechatAccountId,wf.wechatId,wf.labels,sa.userName,wa.currentDeviceId as deviceId') + ->where(function ($q) use ($labels) { + foreach ($labels as $label) { + $q->whereOrRaw("JSON_CONTAINS(wf.labels, '\"{$label}\"')"); + } + })->count(); + $totalAccounts = Db::table('s2_company_account') + ->alias('a') + ->where(['a.departmentId' => $item->companyId, 'a.status' => 0]) + ->whereNotLike('a.userName', '%_offline%') + ->whereNotLike('a.userName', '%_delete%') + ->group('a.id') + ->count(); + + $todayStart = strtotime(date('Y-m-d 00:00:00')); + $todayEnd = strtotime(date('Y-m-d 23:59:59')); + $dailyAverage = Db::name('workbench_traffic_config_item') + ->where('workbenchId', $item->id) + ->whereTime('createTime', 'between', [$todayStart, $todayEnd]) + ->count(); + + if($dailyAverage > 0){ + $dailyAverage = $dailyAverage / $totalAccounts; + } + $item->config->total = [ - 'dailyAverage' => 0, + 'dailyAverage' => intval($dailyAverage), + 'totalAccounts' => $totalAccounts, 'deviceCount' => count($item->config->devices), 'poolCount' => count($item->config->pools), - 'dailyAverage' => $item->config->maxPerDay, - 'totalUsers' => $item->config->maxPerDay * count($item->config->devices) * count($item->config->pools) + 'totalUsers' => $totalUsers >> 0 ]; } unset($item->trafficConfig,$item->traffic_config); @@ -1175,7 +1213,7 @@ class WorkbenchController extends Controller foreach ($labels as $label) { $friendCount = Db::table('s2_wechat_friend') ->whereIn('ownerWechatId',$wechatIds) - ->where('labels', 'like', '%'.$label.'%') + ->where('labels', 'like', '%"'.$label.'"%') ->count(); $newLabel[] = [ 'label' => $label, diff --git a/Server/application/job/WorkbenchTrafficDistributeJob.php b/Server/application/job/WorkbenchTrafficDistributeJob.php index 2a6fc621..730f7031 100644 --- a/Server/application/job/WorkbenchTrafficDistributeJob.php +++ b/Server/application/job/WorkbenchTrafficDistributeJob.php @@ -10,6 +10,7 @@ use think\facade\Cache; use app\cunkebao\model\Workbench; use app\cunkebao\model\WorkbenchTrafficConfig; use think\Db; +use app\api\controller\AutomaticAssign; class WorkbenchTrafficDistributeJob { @@ -69,31 +70,83 @@ class WorkbenchTrafficDistributeJob return; } - // 获取账号,userName不包含offline和delete + // 获取当天未超额的可用账号 + $todayStart = strtotime(date('Y-m-d 00:00:00')); + $todayEnd = strtotime(date('Y-m-d 23:59:59')); $accounts = Db::table('s2_company_account') - ->where(['departmentId' => $workbench->companyId, 'status' => 0]) - ->whereNotLike('userName', '%_offline%') - ->whereNotLike('userName', '%_delete%') - ->field('id,userName,realName') + ->alias('a') + ->where(['a.departmentId' => $workbench->companyId, 'a.status' => 0]) + ->whereNotLike('a.userName', '%_offline%') + ->whereNotLike('a.userName', '%_delete%') + ->leftJoin('workbench_traffic_config_item wti', "wti.wechatAccountId = a.id AND wti.workbenchId = {$workbench->id} AND wti.createTime BETWEEN {$todayStart} AND {$todayEnd}") + ->field('a.id,a.userName,a.realName,COUNT(wti.id) as todayCount') + ->group('a.id') + ->having('todayCount <= ' . $config['maxPerDay']) ->select(); $accountNum = count($accounts); - if ($accountNum < 2) { - Log::info("流量分发工作台 {$workbench->id} 账号少于3个"); + if ($accountNum < 1) { + Log::info("流量分发工作台 {$workbench->id} 可分配账号少于1个"); return; } + $automaticAssign = new AutomaticAssign(); + do { + $friends = $this->getFriendsByLabels($workbench, $config, $page, $pageSize); + if (empty($friends) || count($friends) == 0) { + Log::info("流量分发工作台 {$workbench->id} 没有可分配的好友"); + break; + } + $i = 0; + $accountNum = count($accounts); + foreach ($friends as $friend) { + if ($accountNum == 0) { + Log::info("流量分发工作台 {$workbench->id} 所有账号今日分配已满"); + break 2; + } + if ($i >= $accountNum) { + $i = 0; + } + $account = $accounts[$i]; - // 获取可分配好友 - $friends = $this->getFriendsByLabels($workbench, $config, $page, $pageSize); - if (empty($friends) || count($friends) == 0) { - Log::info("流量分发工作台 {$workbench->id} 没有可分配的好友"); - return; - } + // 如果该账号今天分配的记录数加上本次分配的记录数超过最大限制 + if (($account['todayCount'] + $pageSize) >= $config['maxPerDay']) { + // 查询该客服账号当天分配记录数 + $todayCount = Db::name('workbench_traffic_config_item') + ->where('workbenchId', $workbench->id) + ->where('wechatAccountId', $account['id']) + ->whereBetween('createTime', [$todayStart, $todayEnd]) + ->count(); + if ($todayCount >= $config['maxPerDay']) { + unset($accounts[$i]); + $accounts = array_values($accounts); + $accountNum = count($accounts); + $i++; + continue; + } + } - // TODO: 在这里实现分发逻辑 - print_r($friends); - exit; + // 执行切换好友命令 + $automaticAssign->allotWechatFriend([ + 'wechatFriendId' => $friend['id'], + 'toAccountId' => $account['id'] + ], true); + // 写入分配记录表 + Db::name('workbench_traffic_config_item')->insert([ + 'workbenchId' => $workbench->id, + 'deviceId' => $friend['deviceId'], + 'wechatFriendId' => $friend['id'], + 'wechatAccountId' => $account['id'], + 'createTime' => time() + ]); + Log::info("流量分发工作台 {$workbench->id} 好友[{$friend['id']}]分配给客服[{$account['id']}] 成功"); + $i++; + } + break; + $page++; + } while (true); + Log::info("流量分发工作台 {$workbench->id} 执行分发逻辑完成"); } + /** * 检查是否在流量分发时间范围内 * @param WorkbenchAutoLike $config @@ -146,7 +199,7 @@ class WorkbenchTrafficDistributeJob $q->whereOrRaw("JSON_CONTAINS(wf.labels, '\"{$label}\"')"); } }); - $list = $query->page($page, $pageSize)->select(); + $list = $query->page($page, $pageSize)->order('wf.id DESC')->select(); return $list; }