feat: 询盘导入改为公平分配——按近10天窗口实际分到数排序(三脚本共用)
新增 InquiryFairAssigner:统计 crm_customer(合塑谷歌/导入/未删/近10天) 中名单内每人的已有条数,按 少者优先 -> 最久未分到优先 -> 名单顺序 排序, 三个导入命令分配前统一调用并打印排序结果(含各人窗口内条数)。 解决:日询盘量少于人数时固定只分给名单前几人的问题; 名单增删(离职/暂停回归)自适应,跨脚本/跨天共享,无额外状态。 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
namespace app\command;
|
||||
|
||||
use app\common\model\CrmCustomer;
|
||||
use app\common\utils\InquiryFairAssigner;
|
||||
use app\common\utils\email\InquiryDigestMail;
|
||||
use app\common\utils\email\MailSend;
|
||||
use app\crm\model\Country;
|
||||
@@ -324,10 +325,14 @@ class HesuGoogleAiInquiryImportCommand extends Command
|
||||
$output->writeln('<comment>未匹配到国家(将留空): ' . implode('、', array_keys($countryMiss)) . '</comment>');
|
||||
}
|
||||
|
||||
// 轮询分配(按行序 i % n)
|
||||
$n = count($targetUserIds);
|
||||
// 公平分配:按近10天窗口内实际分到数升序(少者优先、最久未分到优先),再按序轮询
|
||||
$fair = InquiryFairAssigner::fairOrder($targetUserIds);
|
||||
$orderedUsers = $fair['ordered'];
|
||||
$output->writeln('<info>分配顺序(近' . $fair['window_days'] . '天已有条数):'
|
||||
. implode('、', array_map(fn($u) => $u . '(' . ($fair['counts'][$u] ?? 0) . ')', $orderedUsers)) . '</info>');
|
||||
$n = count($orderedUsers);
|
||||
foreach ($keep as $i => &$row) {
|
||||
$row['new_user_id'] = $targetUserIds[$i % $n];
|
||||
$row['new_user_id'] = $orderedUsers[$i % $n];
|
||||
}
|
||||
unset($row);
|
||||
$groups = [];
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace app\command;
|
||||
|
||||
use app\common\model\CrmCustomer;
|
||||
use app\common\utils\InquiryFairAssigner;
|
||||
use app\common\utils\email\InquiryDigestMail;
|
||||
use app\common\utils\email\MailSend;
|
||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||
@@ -191,10 +192,14 @@ class HesuGoogleInquiryImportCommand extends Command
|
||||
$output->writeln('<comment> 无联系方式 行' . $row['excel_row'] . ' ' . $row['contacts'] . '</comment>');
|
||||
}
|
||||
|
||||
// 轮询分配(按行序 i % n)
|
||||
$n = count($targetUserIds);
|
||||
// 公平分配:按近10天窗口内实际分到数升序(少者优先、最久未分到优先),再按序轮询
|
||||
$fair = InquiryFairAssigner::fairOrder($targetUserIds);
|
||||
$orderedUsers = $fair['ordered'];
|
||||
$output->writeln('<info>分配顺序(近' . $fair['window_days'] . '天已有条数):'
|
||||
. implode('、', array_map(fn($u) => $u . '(' . ($fair['counts'][$u] ?? 0) . ')', $orderedUsers)) . '</info>');
|
||||
$n = count($orderedUsers);
|
||||
foreach ($keep as $i => &$row) {
|
||||
$row['new_user_id'] = $targetUserIds[$i % $n];
|
||||
$row['new_user_id'] = $orderedUsers[$i % $n];
|
||||
}
|
||||
unset($row);
|
||||
$groups = [];
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace app\command;
|
||||
|
||||
use app\common\model\CrmCustomer;
|
||||
use app\common\service\OssService;
|
||||
use app\common\utils\InquiryFairAssigner;
|
||||
use app\common\utils\email\InquiryDigestMail;
|
||||
use app\common\utils\email\MailSend;
|
||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||
@@ -219,10 +220,14 @@ class HesuGoogleWhatsAppImportCommand extends Command
|
||||
}
|
||||
$output->writeln('<info>截图: 找到 ' . $shotFound . ' / 缺失 ' . $shotMissing . '(目录: ' . $imagesDir . ')</info>');
|
||||
|
||||
// 轮询分配(按行序 i % n)
|
||||
$n = count($targetUserIds);
|
||||
// 公平分配:按近10天窗口内实际分到数升序(少者优先、最久未分到优先),再按序轮询
|
||||
$fair = InquiryFairAssigner::fairOrder($targetUserIds);
|
||||
$orderedUsers = $fair['ordered'];
|
||||
$output->writeln('<info>分配顺序(近' . $fair['window_days'] . '天已有条数):'
|
||||
. implode('、', array_map(fn($u) => $u . '(' . ($fair['counts'][$u] ?? 0) . ')', $orderedUsers)) . '</info>');
|
||||
$n = count($orderedUsers);
|
||||
foreach ($keep as $i => &$row) {
|
||||
$row['new_user_id'] = $targetUserIds[$i % $n];
|
||||
$row['new_user_id'] = $orderedUsers[$i % $n];
|
||||
}
|
||||
unset($row);
|
||||
$groups = [];
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\utils;
|
||||
|
||||
use support\Db;
|
||||
|
||||
/**
|
||||
* 询盘公平分配排序器(合塑谷歌三个导入脚本共用)
|
||||
*
|
||||
* 按"最近窗口期内实际分到的条数"升序排用户:分得少的优先、最久未分到的优先、
|
||||
* 再按传入名单顺序。统计池:crm_customer 中 channel_id=4(合塑谷歌)、type=2(导入)、
|
||||
* 未删除、窗口期内新建的记录。
|
||||
*
|
||||
* 场景自适应:
|
||||
* - 名单增删(离职移除/新人加入):每次执行按当次名单统计,无残留状态;
|
||||
* - 暂停一段时间再恢复:暂停期间窗口计数自然衰减,回归后适度优先拉平,
|
||||
* 不会饿死也不会被灌顶。
|
||||
*/
|
||||
class InquiryFairAssigner
|
||||
{
|
||||
/** 默认窗口期(天) */
|
||||
public const DEFAULT_WINDOW_DAYS = 10;
|
||||
|
||||
/**
|
||||
* 计算公平分配顺序
|
||||
*
|
||||
* @param array $userIds 本次参与分配的 user_id 列表
|
||||
* @param int $windowDays 统计窗口(天)
|
||||
* @return array{ordered: array, counts: array<int,int>, window_days: int}
|
||||
* ordered=排好序的 user_id 列表;counts=窗口内每人已有条数
|
||||
*/
|
||||
public static function fairOrder(array $userIds, int $windowDays = self::DEFAULT_WINDOW_DAYS): array
|
||||
{
|
||||
$since = time() - $windowDays * 86400;
|
||||
$counts = []; $lastIds = [];
|
||||
foreach (Db::table('crm_customer')
|
||||
->where('channel_id', 4)
|
||||
->where('type', 2)
|
||||
->whereNull('deleted_at')
|
||||
->whereIn('user_id', $userIds)
|
||||
->where('created_at', '>=', $since)
|
||||
->groupBy('user_id')
|
||||
->selectRaw('user_id, COUNT(*) AS cnt, MAX(id) AS last_id')
|
||||
->get() as $r) {
|
||||
$uid = (int)$r->user_id;
|
||||
$counts[$uid] = (int)$r->cnt;
|
||||
$lastIds[$uid] = (int)$r->last_id;
|
||||
}
|
||||
|
||||
$pos = array_flip(array_map('intval', $userIds));
|
||||
$ordered = array_map('intval', $userIds);
|
||||
usort($ordered, function ($a, $b) use ($counts, $lastIds, $pos) {
|
||||
$ca = $counts[$a] ?? 0; $cb = $counts[$b] ?? 0;
|
||||
if ($ca !== $cb) {
|
||||
return $ca <=> $cb; // 窗口内分到少的优先
|
||||
}
|
||||
$la = $lastIds[$a] ?? 0; $lb = $lastIds[$b] ?? 0;
|
||||
if ($la !== $lb) {
|
||||
return $la <=> $lb; // 最久未分到的优先
|
||||
}
|
||||
return $pos[$a] <=> $pos[$b]; // 名单顺序(稳定)
|
||||
});
|
||||
|
||||
return ['ordered' => $ordered, 'counts' => $counts, 'window_days' => $windowDays];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user