feat: 询盘导入改为公平分配——按近10天窗口实际分到数排序(三脚本共用)

新增 InquiryFairAssigner:统计 crm_customer(合塑谷歌/导入/未删/近10天)
中名单内每人的已有条数,按 少者优先 -> 最久未分到优先 -> 名单顺序 排序,
三个导入命令分配前统一调用并打印排序结果(含各人窗口内条数)。
解决:日询盘量少于人数时固定只分给名单前几人的问题;
名单增删(离职/暂停回归)自适应,跨脚本/跨天共享,无额外状态。

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
mzl
2026-08-20 15:40:45 +08:00
parent 89b9162f09
commit 6fa80336a6
4 changed files with 90 additions and 9 deletions
@@ -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 = [];
+66
View File
@@ -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];
}
}