Finish mzl_inquery
This commit is contained in:
@@ -11,6 +11,9 @@ use PhpOffice\PhpSpreadsheet\IOFactory;
|
|||||||
use app\inquiry\utils\FbLeadHeaderMapper;
|
use app\inquiry\utils\FbLeadHeaderMapper;
|
||||||
use support\exception\BusinessException;
|
use support\exception\BusinessException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从Excel文件导入fb_leads
|
||||||
|
*/
|
||||||
class InquiryImportFbCommand extends Command
|
class InquiryImportFbCommand extends Command
|
||||||
{
|
{
|
||||||
protected static $defaultName = 'iq-fb';
|
protected static $defaultName = 'iq-fb';
|
||||||
|
|||||||
@@ -0,0 +1,201 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\inquiry\service;
|
||||||
|
|
||||||
|
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||||
|
use support\Db;
|
||||||
|
use app\inquiry\utils\FbLeadHeaderMapper;
|
||||||
|
use support\exception\BusinessException;
|
||||||
|
use support\Log;
|
||||||
|
|
||||||
|
class FbFileImportService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 导入指定目录下所有 .xlsx 文件
|
||||||
|
*/
|
||||||
|
public function importDir(string $dir, int $channelId): int
|
||||||
|
{
|
||||||
|
if (!is_dir($dir)) {
|
||||||
|
throw new BusinessException("目录不存在:{$dir}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只扫描 .xlsx,之前处理过的 .xlsx.done 不会被匹配到
|
||||||
|
$files = glob($dir . DIRECTORY_SEPARATOR . '*.xlsx');
|
||||||
|
if (!$files) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$count = 0;
|
||||||
|
|
||||||
|
foreach ($files as $filePath) {
|
||||||
|
if (!is_file($filePath)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$doneFlag = $filePath . '.done';
|
||||||
|
if (is_file($doneFlag)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows = $this->importOneFile($filePath, $channelId);
|
||||||
|
$count++;
|
||||||
|
|
||||||
|
// 导入成功后,把文件重命名为 .xlsx.done,避免下次重复导入
|
||||||
|
$newPath = $filePath . '.done';
|
||||||
|
if (!@rename($filePath, $newPath)) {
|
||||||
|
Log::warning("FbFileImportService: 文件导入成功,但重命名失败:{$filePath} -> {$newPath}");
|
||||||
|
} else {
|
||||||
|
Log::info("FbFileImportService: 文件导入并标记完成:{$newPath}(行数:{$rows})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导入单个 Excel 到 fb_leads
|
||||||
|
*/
|
||||||
|
public function importOneFile(string $filePath, int $channelId): int
|
||||||
|
{
|
||||||
|
if (!is_file($filePath)) {
|
||||||
|
throw new BusinessException("Excel 文件不存在:{$filePath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$spreadsheet = IOFactory::load($filePath);
|
||||||
|
$sheet = $spreadsheet->getActiveSheet();
|
||||||
|
$rows = $sheet->toArray(null, true, true, true);
|
||||||
|
|
||||||
|
if (empty($rows)) {
|
||||||
|
throw new BusinessException('Excel 内容为空:' . $filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表头
|
||||||
|
$headerRow = array_shift($rows);
|
||||||
|
|
||||||
|
$mapper = new FbLeadHeaderMapper();
|
||||||
|
$res = $mapper->buildColumnFieldMap($headerRow);
|
||||||
|
$colToField = $res['colToField'];
|
||||||
|
$unknownCols = $res['unknownCols'];
|
||||||
|
|
||||||
|
$insertData = [];
|
||||||
|
$now = time();
|
||||||
|
$sourceFile = basename($filePath);
|
||||||
|
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$item = [];
|
||||||
|
|
||||||
|
foreach ($colToField as $colIndex => $field) {
|
||||||
|
$raw = $row[$colIndex] ?? null;
|
||||||
|
|
||||||
|
switch ($field) {
|
||||||
|
case 'is_organic':
|
||||||
|
$item['is_organic'] = $this->normalizeIsOrganic($raw);
|
||||||
|
break;
|
||||||
|
case 'fb_created_time':
|
||||||
|
// ✅ 方案B:写入 DATETIME 字符串
|
||||||
|
$item['fb_created_time'] = $this->normalizeCreatedTime($raw);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// 小增强:字符串统一 trim(避免 " xxx ")
|
||||||
|
if (is_string($raw)) {
|
||||||
|
$raw = trim($raw);
|
||||||
|
}
|
||||||
|
$item[$field] = $raw;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 未知列收集到 extra_data(宽松模式)
|
||||||
|
$extra = [];
|
||||||
|
foreach ($unknownCols as $colIndex => $rawHeader) {
|
||||||
|
$val = $row[$colIndex] ?? null;
|
||||||
|
if ($val === null || $val === '') continue;
|
||||||
|
$extra[$rawHeader] = is_string($val) ? trim($val) : $val;
|
||||||
|
}
|
||||||
|
$item['extra_data'] = $extra ? json_encode($extra, JSON_UNESCAPED_UNICODE) : null;
|
||||||
|
|
||||||
|
$item['source_file'] = $sourceFile;
|
||||||
|
$item['channel'] = $channelId;
|
||||||
|
|
||||||
|
$item['created_at'] = $now;
|
||||||
|
$item['updated_at'] = $now;
|
||||||
|
$item['sync_status'] = 0;
|
||||||
|
|
||||||
|
$insertData[] = $item;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($insertData) {
|
||||||
|
Db::table('fb_leads')->insert($insertData);
|
||||||
|
}
|
||||||
|
|
||||||
|
return count($insertData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 规范化 is_organic 字段为 0/1
|
||||||
|
*/
|
||||||
|
protected function normalizeIsOrganic($raw): int
|
||||||
|
{
|
||||||
|
if (is_bool($raw)) {
|
||||||
|
return $raw ? 1 : 0;
|
||||||
|
}
|
||||||
|
$str = strtolower(trim((string)$raw));
|
||||||
|
if ($str === '') {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return in_array($str, ['1', 'true', 'yes', 'organic'], true) ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ✅ 方案B:把 FB 创建时间统一转成 MySQL DATETIME(Y-m-d H:i:s)
|
||||||
|
*
|
||||||
|
* 支持:
|
||||||
|
* - "10/13/25" / "12/8/25"
|
||||||
|
* - "2025-12-08 13:22:11"
|
||||||
|
* - Excel 日期序列(数字)
|
||||||
|
*
|
||||||
|
* 解析失败:返回 null(让 DB 用默认值/允许为空)
|
||||||
|
* 注意:如果你的 fb_created_time 列是 NOT NULL 且无默认值,请把 null 改成 '1970-01-01 00:00:00'
|
||||||
|
*/
|
||||||
|
protected function normalizeCreatedTime($raw): ?string
|
||||||
|
{
|
||||||
|
if ($raw === null || $raw === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Excel 日期序列:数字
|
||||||
|
if (is_numeric($raw)) {
|
||||||
|
$n = (float)$raw;
|
||||||
|
// Excel serial date -> unix timestamp
|
||||||
|
$ts = (int)round(($n - 25569) * 86400);
|
||||||
|
if ($ts > 0) {
|
||||||
|
return date('Y-m-d H:i:s', $ts);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$s = trim((string)$raw);
|
||||||
|
if ($s === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1) strtotime 能解析很多格式
|
||||||
|
$ts = strtotime($s);
|
||||||
|
if ($ts !== false) {
|
||||||
|
return date('Y-m-d H:i:s', $ts);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) 兼容 m/d/yy(如 10/13/25)
|
||||||
|
if (preg_match('#^(\d{1,2})/(\d{1,2})/(\d{2})$#', $s, $m)) {
|
||||||
|
$mm = (int)$m[1];
|
||||||
|
$dd = (int)$m[2];
|
||||||
|
$yy = (int)$m[3];
|
||||||
|
$year = 2000 + $yy; // FB 导出通常是 20xx
|
||||||
|
$ts2 = mktime(0, 0, 0, $mm, $dd, $year);
|
||||||
|
return date('Y-m-d H:i:s', $ts2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) 兜底:无法解析
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\inquiry\service;
|
||||||
|
|
||||||
|
use app\inquiry\utils\PhoneCountryHelper;
|
||||||
|
use support\Db;
|
||||||
|
|
||||||
|
class FbLeadSyncService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 同步指定渠道的 fb_leads.sync_status=0 数据到 inquiry_log
|
||||||
|
* 并将对应 fb_leads.sync_status 更新为 1
|
||||||
|
*
|
||||||
|
* @param int $enterUserId 录入人ID
|
||||||
|
* @param int $channelId 渠道ID(既用于 fb_leads.channel,也用于 inquiry_log.channel)
|
||||||
|
* @return int 同步条数
|
||||||
|
*/
|
||||||
|
public function syncToInquiryLog(int $enterUserId, int $channelId): int
|
||||||
|
{
|
||||||
|
$leads = Db::table('fb_leads')
|
||||||
|
->where('sync_status', 0)
|
||||||
|
->where('channel', $channelId)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
if ($leads->isEmpty()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$now = time();
|
||||||
|
|
||||||
|
// 转数组,后面循环更快
|
||||||
|
$leadArrList = [];
|
||||||
|
foreach ($leads as $lead) {
|
||||||
|
$leadArrList[] = (array)$lead;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ① 收集本批手机号(去重)
|
||||||
|
$phoneList = $this->collectPhonesFromLeads($leadArrList);
|
||||||
|
|
||||||
|
// ② 批量查出 inquiry_log 已存在手机号集合(索引加持)
|
||||||
|
$existingTelSet = $this->getExistingTelSet($phoneList);
|
||||||
|
|
||||||
|
$insert = [];
|
||||||
|
$idsToMarkProcessed = []; // 插入成功 or 重复跳过,统一标记已处理
|
||||||
|
$insertCount = 0;
|
||||||
|
|
||||||
|
Db::transaction(function () use (
|
||||||
|
$leadArrList, $existingTelSet, $enterUserId, $channelId, $now,
|
||||||
|
&$insert, &$idsToMarkProcessed, &$insertCount
|
||||||
|
) {
|
||||||
|
foreach ($leadArrList as $lead) {
|
||||||
|
$leadId = (int)($lead['id'] ?? 0);
|
||||||
|
|
||||||
|
$tel = trim((string)($lead['phone'] ?? ''));
|
||||||
|
|
||||||
|
// 如果手机号存在于 inquiry_log,跳过同步(但要标记 fb_leads 已处理)
|
||||||
|
if ($tel !== '' && isset($existingTelSet[$tel])) {
|
||||||
|
if ($leadId > 0) {
|
||||||
|
$idsToMarkProcessed[] = $leadId;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$insert[] = $this->mapLeadToInquiry($lead, $now, $enterUserId, $channelId);
|
||||||
|
|
||||||
|
if ($leadId > 0) {
|
||||||
|
$idsToMarkProcessed[] = $leadId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($insert)) {
|
||||||
|
Db::table('inquiry_log')->insert($insert);
|
||||||
|
$insertCount = count($insert);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($idsToMarkProcessed)) {
|
||||||
|
Db::table('fb_leads')
|
||||||
|
->whereIn('id', $idsToMarkProcessed)
|
||||||
|
->update([
|
||||||
|
'sync_status' => 1,
|
||||||
|
'updated_at' => $now,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return $insertCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从本批 leads 中收集手机号(tel),去重后返回
|
||||||
|
*/
|
||||||
|
protected function collectPhonesFromLeads(array $leadArrList): array
|
||||||
|
{
|
||||||
|
$phones = [];
|
||||||
|
foreach ($leadArrList as $lead) {
|
||||||
|
$tel = trim((string)($lead['phone'] ?? ''));
|
||||||
|
if ($tel !== '') {
|
||||||
|
// 先用原值精确匹配(你后续要做归一化再改这里即可)
|
||||||
|
$phones[$tel] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return array_keys($phones);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量查出 inquiry_log 已存在手机号集合(返回 set:tel => true)
|
||||||
|
* - 自动分块 whereIn,避免 SQL 过长
|
||||||
|
* - 建议给 inquiry_log.tel 加索引 idx_tel
|
||||||
|
*/
|
||||||
|
protected function getExistingTelSet(array $phoneList, int $chunkSize = 800): array
|
||||||
|
{
|
||||||
|
$phoneList = array_values(array_unique(array_filter(array_map('trim', $phoneList))));
|
||||||
|
if (empty($phoneList)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$existingTelSet = [];
|
||||||
|
|
||||||
|
foreach (array_chunk($phoneList, $chunkSize) as $chunk) {
|
||||||
|
$rows = Db::table('inquiry_log')
|
||||||
|
->select('tel')
|
||||||
|
->where('tel', '<>', '')
|
||||||
|
->whereIn('tel', $chunk)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
$t = trim((string)($r->tel ?? ''));
|
||||||
|
if ($t !== '') {
|
||||||
|
$existingTelSet[$t] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $existingTelSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 映射一条 fb_leads -> inquiry_log
|
||||||
|
*/
|
||||||
|
protected function mapLeadToInquiry(array $lead, int $now, int $enterUserId, int $channelId): array
|
||||||
|
{
|
||||||
|
$email = (string)($lead['email'] ?? '');
|
||||||
|
$tel = (string)($lead['phone'] ?? '');
|
||||||
|
$wa = $tel;
|
||||||
|
|
||||||
|
$countryId = PhoneCountryHelper::detectCountryId($tel ?: $wa);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'order_no' => $this->orderNum(),
|
||||||
|
'enter_user_id' => $enterUserId,
|
||||||
|
'recipient' => 0,
|
||||||
|
'quality' => 2,
|
||||||
|
'channel' => $channelId, // 👈 inquiry_log 里同样用这个渠道ID
|
||||||
|
|
||||||
|
'email' => $email,
|
||||||
|
'tel' => $tel,
|
||||||
|
'whatsapp' => $wa,
|
||||||
|
'country' => $countryId ?: 0,
|
||||||
|
'contacts' => (string)($lead['full_name'] ?? ''),
|
||||||
|
|
||||||
|
'content' => $this->buildContentHtml($lead),
|
||||||
|
|
||||||
|
'status' => 0,
|
||||||
|
'r_time' => 0,
|
||||||
|
't_time' => 0,
|
||||||
|
'sync_crm' => 0,
|
||||||
|
|
||||||
|
'created_at' => $now,
|
||||||
|
'updated_at' => $now,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function orderNum(): string
|
||||||
|
{
|
||||||
|
return 'SN' . date('Ymd') . str_pad(mt_rand(1, 99999), 5, '0', STR_PAD_LEFT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 富文本内容拼装 —— 基于你之前的版本
|
||||||
|
*/
|
||||||
|
protected function buildContentHtml(array $lead): string
|
||||||
|
{
|
||||||
|
$e = function ($v) {
|
||||||
|
return htmlspecialchars((string)$v, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||||
|
};
|
||||||
|
|
||||||
|
$lines = [];
|
||||||
|
|
||||||
|
if (!empty($lead['car_model'])) {
|
||||||
|
$lines[] = '<p><strong>意向车型:</strong>' . $e($lead['car_model']) . '</p>';
|
||||||
|
}
|
||||||
|
if (!empty($lead['car_quantity']) || !empty($lead['car_quantity_text'])) {
|
||||||
|
$qty = $lead['car_quantity'] ?? $lead['car_quantity_text'] ?? '';
|
||||||
|
$lines[] = '<p><strong>意向数量:</strong>' . $e($qty) . '</p>';
|
||||||
|
}
|
||||||
|
if (!empty($lead['favorite_color'])) {
|
||||||
|
$lines[] = '<p><strong>意向颜色:</strong>' . $e($lead['favorite_color']) . '</p>';
|
||||||
|
}
|
||||||
|
if (!empty($lead['usage_type'])) {
|
||||||
|
$lines[] = '<p><strong>用途:</strong>' . $e($lead['usage_type']) . '</p>';
|
||||||
|
}
|
||||||
|
if (!empty($lead['seat_preference'])) {
|
||||||
|
$lines[] = '<p><strong>座位数偏好:</strong>' . $e($lead['seat_preference']) . '</p>';
|
||||||
|
}
|
||||||
|
if (!empty($lead['drive_type'])) {
|
||||||
|
$lines[] = '<p><strong>驱动形式偏好:</strong>' . $e($lead['drive_type']) . '</p>';
|
||||||
|
}
|
||||||
|
if (!empty($lead['extra_data'])) {
|
||||||
|
$extra = json_decode((string)$lead['extra_data'], true);
|
||||||
|
if (is_array($extra) && $extra) {
|
||||||
|
$lines[] = '<hr />';
|
||||||
|
$lines[] = '<p><strong>补充问答信息:</strong></p>';
|
||||||
|
foreach ($extra as $k => $v) {
|
||||||
|
$lines[] = '<p>' . $e($k) . ':' . $e($v) . '</p>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$lines[] = '<hr />';
|
||||||
|
$lines[] = '<p><strong>广告与表单信息:</strong></p>';
|
||||||
|
$lines[] = '<p>平台:' . $e($lead['platform'] ?? '') . '</p>';
|
||||||
|
$lines[] = '<p>Campaign:' . $e($lead['campaign_name'] ?? '') . ' (#' . $e($lead['campaign_id'] ?? '') . ')</p>';
|
||||||
|
$lines[] = '<p>Adset:' . $e($lead['adset_name'] ?? '') . ' (#' . $e($lead['adset_id'] ?? '') . ')</p>';
|
||||||
|
$lines[] = '<p>Ad:' . $e($lead['ad_name'] ?? '') . ' (#' . $e($lead['ad_id'] ?? '') . ')</p>';
|
||||||
|
$lines[] = '<p>Form:' . $e($lead['form_name'] ?? '') . ' (#' . $e($lead['form_id'] ?? '') . ')</p>';
|
||||||
|
|
||||||
|
$lines[] = '<hr />';
|
||||||
|
$lines[] = '<p><strong>Meta 信息:</strong></p>';
|
||||||
|
$lines[] = '<p>Lead ID:' . $e($lead['fb_lead_id'] ?? '') . '</p>';
|
||||||
|
$lines[] = '<p>FB 创建时间原始值:' . $e($lead['fb_created_time'] ?? '') . '</p>';
|
||||||
|
|
||||||
|
if (!empty($lead['inbox_url'])) {
|
||||||
|
$url = $e($lead['inbox_url']);
|
||||||
|
$lines[] = '<p>Inbox 链接:<a href="' . $url . '" target="_blank">' . $url . '</a></p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode("\n", $lines);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\inquiry\service;
|
||||||
|
|
||||||
|
use support\Db;
|
||||||
|
use support\Redis;
|
||||||
|
use support\exception\BusinessException;
|
||||||
|
use Webman\RedisQueue\Redis as RedisQueueRedis;
|
||||||
|
|
||||||
|
class InquiryAssignService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 按渠道分配 inquiry_log 中待分配的记录,轮询业务员,并推送到 CRM 队列
|
||||||
|
*
|
||||||
|
* @param int[] $salesUserIds 默认参与轮询的业务员ID列表(该渠道默认池)
|
||||||
|
* @param string $roundRobinKey 默认轮询分组key(Redis记指针)
|
||||||
|
* @param int $channelId 渠道ID(对应 inquiry_log.channel)
|
||||||
|
* @param array $assignRules 渠道内规则:按国家切换业务员池(可选)
|
||||||
|
* @param int $limit 每次最多分配多少条
|
||||||
|
*
|
||||||
|
* @return int 实际分配的条数
|
||||||
|
*/
|
||||||
|
public function assignBatch(
|
||||||
|
array $salesUserIds,
|
||||||
|
string $roundRobinKey,
|
||||||
|
int $channelId,
|
||||||
|
array $assignRules = [],
|
||||||
|
int $limit = 100
|
||||||
|
): int {
|
||||||
|
// 默认池(渠道默认业务员列表)
|
||||||
|
$defaultUserIds = $this->normalizeUserIds($salesUserIds);
|
||||||
|
if (empty($defaultUserIds)) {
|
||||||
|
throw new BusinessException('分配失败:业务员列表为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
$inquiries = Db::table('inquiry_log')
|
||||||
|
->where('status', 0)
|
||||||
|
->where('recipient', 0)
|
||||||
|
->where('channel', $channelId)
|
||||||
|
->orderBy('id')
|
||||||
|
->limit($limit)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
if ($inquiries->isEmpty()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$now = time();
|
||||||
|
$count = 0;
|
||||||
|
|
||||||
|
foreach ($inquiries as $row) {
|
||||||
|
$inquiryId = (int)$row->id;
|
||||||
|
|
||||||
|
// ✅ 关键:按规则选业务员池(命中规则就用规则池,否则用默认池)
|
||||||
|
[$poolUserIds, $rrKey] = $this->pickSalesPoolByRules(
|
||||||
|
(int)($row->country ?? 0),
|
||||||
|
$defaultUserIds,
|
||||||
|
$roundRobinKey,
|
||||||
|
$assignRules
|
||||||
|
);
|
||||||
|
|
||||||
|
// ✅ 用“对应池子自己的 rrKey”轮询拿业务员
|
||||||
|
$recipientUserId = $this->nextRecipientId($poolUserIds, $rrKey);
|
||||||
|
|
||||||
|
Db::transaction(function () use ($inquiryId, $recipientUserId, $now, &$count) {
|
||||||
|
$affected = Db::table('inquiry_log')
|
||||||
|
->where('id', $inquiryId)
|
||||||
|
->update([
|
||||||
|
'recipient' => $recipientUserId,
|
||||||
|
'status' => 2,
|
||||||
|
'r_time' => $now,
|
||||||
|
't_time' => $now,
|
||||||
|
'updated_at' => $now,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($affected === 0) {
|
||||||
|
// 可能被其他进程抢先处理了,直接跳过
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$res = [
|
||||||
|
'id' => $inquiryId,
|
||||||
|
'recipient' => $recipientUserId,
|
||||||
|
'dispatched_at' => $now,
|
||||||
|
'source' => 'fb_leads_inquiry',
|
||||||
|
];
|
||||||
|
|
||||||
|
RedisQueueRedis::send('inquiry-to-crm-new', $res);
|
||||||
|
|
||||||
|
$count++;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return $count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 轮询获取下一个业务员ID
|
||||||
|
*/
|
||||||
|
protected function nextRecipientId(array $userIds, string $groupKey): int
|
||||||
|
{
|
||||||
|
$userIds = $this->normalizeUserIds($userIds);
|
||||||
|
if (empty($userIds)) {
|
||||||
|
throw new BusinessException('轮询分配失败:业务员列表为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
$count = count($userIds);
|
||||||
|
$key = 'inquiry:assign:round_robin:' . $groupKey;
|
||||||
|
|
||||||
|
$val = Redis::incr($key); // 原子自增
|
||||||
|
$index = ($val - 1) % $count;
|
||||||
|
|
||||||
|
return $userIds[$index];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按国家规则挑选“业务员池 + 轮询key”
|
||||||
|
* 命中 rules 则返回规则池,否则返回默认池
|
||||||
|
*
|
||||||
|
* rules 示例:
|
||||||
|
* [
|
||||||
|
* [
|
||||||
|
* 'countries' => [1,2,3],
|
||||||
|
* 'sales_user_ids' => [63,64],
|
||||||
|
* 'round_robin_key' => 'fb_xxx_ru',
|
||||||
|
* ],
|
||||||
|
* ]
|
||||||
|
*
|
||||||
|
* @return array{0:int[], 1:string} [userIds, rrKey]
|
||||||
|
*/
|
||||||
|
protected function pickSalesPoolByRules(
|
||||||
|
int $countryId,
|
||||||
|
array $defaultUserIds,
|
||||||
|
string $defaultRrKey,
|
||||||
|
array $rules
|
||||||
|
): array {
|
||||||
|
foreach ($rules as $rule) {
|
||||||
|
if (!is_array($rule)) continue;
|
||||||
|
|
||||||
|
$countries = $rule['countries'] ?? [];
|
||||||
|
if (!is_array($countries) || empty($countries)) continue;
|
||||||
|
|
||||||
|
if (!in_array($countryId, array_map('intval', $countries), true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$userIds = $this->normalizeUserIds($rule['sales_user_ids'] ?? []);
|
||||||
|
if (!empty($userIds)) {
|
||||||
|
// 规则池必须使用自己的 rrKey,否则会和默认池“抢指针”
|
||||||
|
$rrKey = (string)($rule['round_robin_key'] ?? ($defaultRrKey . '_rule_' . md5(json_encode($countries))));
|
||||||
|
return [$userIds, $rrKey];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [$this->normalizeUserIds($defaultUserIds), $defaultRrKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一清洗业务员ID数组:转 int、去重、过滤 <=0
|
||||||
|
*/
|
||||||
|
protected function normalizeUserIds(array $ids): array
|
||||||
|
{
|
||||||
|
$ids = array_values(array_filter(array_map('intval', $ids), fn($v) => $v > 0));
|
||||||
|
$ids = array_values(array_unique($ids));
|
||||||
|
return $ids;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ class FbLeadHeaderMapper
|
|||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Excel 原始表头 => DB 字段名
|
* Excel 原始表头 => DB 字段名
|
||||||
* 注意:key 用的是“原始表头字符串”(包括下划线)
|
* 注意:这里的 key 会经过 normalizeHeader() 处理后再匹配
|
||||||
*/
|
*/
|
||||||
protected array $headerMap = [
|
protected array $headerMap = [
|
||||||
// ===== 基础公共字段 =====
|
// ===== 基础公共字段 =====
|
||||||
@@ -33,37 +33,53 @@ class FbLeadHeaderMapper
|
|||||||
'inbox_url' => 'inbox_url',
|
'inbox_url' => 'inbox_url',
|
||||||
|
|
||||||
// ===== 车型相关:全部归到 car_model =====
|
// ===== 车型相关:全部归到 car_model =====
|
||||||
'which_model_of_this_car_would_you_like_to_purchase?_' => 'car_model',
|
'which_model_of_this_car_would_you_like_to_purchase' => 'car_model',
|
||||||
'which_model_of_this_car_would_you_like_to_purchase?' => 'car_model',
|
'which_car_model_would_you_like_to_purchase' => 'car_model',
|
||||||
'which_car_model_would_you_like_to_purchase?' => 'car_model',
|
'which_model_do_you_want_to_purchase' => 'car_model',
|
||||||
'which_model_do_you_want_to_purchase?' => 'car_model',
|
'which_model_would_you_like_to_purchase' => 'car_model',
|
||||||
'which_model_would_you_like_to_purchase?' => 'car_model',
|
'which_version_do_you_want_to_buy' => 'car_model', // 这里也归到车型/版本字段
|
||||||
'which_version_do_you_want_to_buy?' => 'car_model', // 这里也归到车型/版本字段
|
|
||||||
|
|
||||||
// ===== 数量相关:全部归到 car_quantity =====
|
// ===== 数量相关:建议归到 car_quantity_text(避免 1-2/3-10/10+)=====
|
||||||
'how_many_cars_would_you_like_to_buy?_' => 'car_quantity',
|
// 你之前 car_quantity 用 int 时会截断,这里建议后续统一改成 car_quantity_text
|
||||||
'how_many_cars_would_you_like_to_buy?' => 'car_quantity',
|
'how_many_cars_would_you_like_to_buy' => 'car_quantity_text',
|
||||||
'_how_many_cars_would_you_like_to_buy?' => 'car_quantity',
|
'_how_many_cars_would_you_like_to_buy' => 'car_quantity_text',
|
||||||
'how_many_cars_do_you_buy?' => 'car_quantity',
|
'how_many_cars_do_you_buy' => 'car_quantity_text',
|
||||||
'how_many_cars_do_you_want_to_purchase?' => 'car_quantity',
|
'how_many_cars_do_you_want_to_purchase' => 'car_quantity_text',
|
||||||
'how_many_cars_do_you_want_to_buy?' => 'car_quantity',
|
'how_many_cars_do_you_want_to_buy' => 'car_quantity_text',
|
||||||
'what_quantity_do_you_want_to_buy?' => 'car_quantity',
|
'what_quantity_do_you_want_to_buy' => 'car_quantity_text',
|
||||||
|
'what_quantity_do_you_wantv_to_buy' => 'car_quantity_text', // wantv 拼写兼容
|
||||||
|
|
||||||
// ===== 颜色相关:归到 favorite_color =====
|
// ===== 颜色相关:归到 favorite_color =====
|
||||||
'what_color_do_you_like?' => 'favorite_color',
|
'what_color_do_you_like' => 'favorite_color',
|
||||||
'which_color_would_you_like_to_purchase?' => 'favorite_color',
|
'which_color_would_you_like_to_purchase' => 'favorite_color',
|
||||||
|
'what_color_car_do_you_plan_to_buy' => 'favorite_color',
|
||||||
|
|
||||||
// ===== 用途相关:归到 usage_type =====
|
// ===== 用途相关:归到 usage_type =====
|
||||||
'are_you_using_it_for_business_or_personal_use?' => 'usage_type',
|
'are_you_using_it_for_business_or_personal_use' => 'usage_type',
|
||||||
'business_use_or_personal_use?' => 'usage_type',
|
'business_use_or_personal_use' => 'usage_type',
|
||||||
'personal_use_or_business_use?' => 'usage_type',
|
'personal_use_or_business_use' => 'usage_type',
|
||||||
|
|
||||||
// ===== 座位数相关 =====
|
// ===== 座位数相关 =====
|
||||||
'do_you_want_to_buy_six_seats_or_seven_seats?' => 'seat_preference',
|
'do_you_want_to_buy_six_seats_or_seven_seats' => 'seat_preference',
|
||||||
|
|
||||||
|
// ===== AVATR 11 =====
|
||||||
|
'which_model_of_the_avatr_11_do_you_want_to_purchase' => 'car_model',
|
||||||
|
'do_you_want_to_buy_a_four-wheel_drive_or_rear-wheel_drive_car' => 'drive_type',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 返回所有支持的 Excel 表头
|
* 宽松模式下仍然强制要求存在的“核心 DB 字段”
|
||||||
|
* 你说的:联系人、电话、邮箱、国家、渠道
|
||||||
|
* - country / channel 不是从 Excel 映射,而是你代码里根据电话识别/配置注入,所以这里不用要求
|
||||||
|
*/
|
||||||
|
protected array $requiredDbFields = [
|
||||||
|
'full_name',
|
||||||
|
'phone',
|
||||||
|
'email',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回所有支持的 Excel 表头(标准化后的 key)
|
||||||
*/
|
*/
|
||||||
public function getSupportedHeaders(): array
|
public function getSupportedHeaders(): array
|
||||||
{
|
{
|
||||||
@@ -76,12 +92,11 @@ class FbLeadHeaderMapper
|
|||||||
public function mapHeader(string $rawHeader): ?string
|
public function mapHeader(string $rawHeader): ?string
|
||||||
{
|
{
|
||||||
$key = $this->normalizeHeader($rawHeader);
|
$key = $this->normalizeHeader($rawHeader);
|
||||||
|
|
||||||
return $this->headerMap[$key] ?? null;
|
return $this->headerMap[$key] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 校验一整行表头:
|
* 旧的严格校验:保留(有时你可能还想用来排查某个表单的字段)
|
||||||
* - 有任何不认识的表头,就抛 BusinessException
|
* - 有任何不认识的表头,就抛 BusinessException
|
||||||
*/
|
*/
|
||||||
public function validateHeaders(array $rawHeaders): void
|
public function validateHeaders(array $rawHeaders): void
|
||||||
@@ -104,42 +119,74 @@ class FbLeadHeaderMapper
|
|||||||
|
|
||||||
if (!empty($unsupported)) {
|
if (!empty($unsupported)) {
|
||||||
$msg = 'Excel 表头包含未兼容字段:' . implode('、', $unsupported)
|
$msg = 'Excel 表头包含未兼容字段:' . implode('、', $unsupported)
|
||||||
. '。请检查这些字段是否需要加入映射配置。';
|
. '。请检查这些字段是否需要加入映射配置。';
|
||||||
throw new BusinessException($msg);
|
throw new BusinessException($msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据第一行表头,构建 “列索引 => DB 字段” 映射
|
* ✅ 宽松模式:根据第一行表头,构建映射
|
||||||
|
*
|
||||||
|
* 返回结构:
|
||||||
|
* [
|
||||||
|
* 'colToField' => [ 'A' => 'email', 'B' => 'phone', ... ],
|
||||||
|
* 'unknownCols' => [ 'C' => 'some_question?', ... ], // 未映射字段(后续可进富文本/extra_data)
|
||||||
|
* ]
|
||||||
*/
|
*/
|
||||||
public function buildColumnFieldMap(array $rawHeaders): array
|
public function buildColumnFieldMap(array $rawHeaders): array
|
||||||
{
|
{
|
||||||
// 先做一遍校验(发现问题直接抛异常)
|
$colToField = [];
|
||||||
$this->validateHeaders($rawHeaders);
|
$unknownCols = [];
|
||||||
|
|
||||||
$colToField = [];
|
|
||||||
|
|
||||||
foreach ($rawHeaders as $index => $header) {
|
foreach ($rawHeaders as $index => $header) {
|
||||||
$key = $this->normalizeHeader((string)$header);
|
$raw = (string)$header;
|
||||||
|
$key = $this->normalizeHeader($raw);
|
||||||
|
|
||||||
if ($key === '') {
|
if ($key === '') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$field = $this->headerMap[$key] ?? null;
|
$field = $this->headerMap[$key] ?? null;
|
||||||
|
|
||||||
if ($field === null) {
|
if ($field === null) {
|
||||||
continue; // 理论上不会到这儿,双保险
|
// 宽松:记录未知列(保留原始表头用于展示)
|
||||||
|
$unknownCols[$index] = trim($raw) !== '' ? trim($raw) : $key;
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$colToField[$index] = $field;
|
$colToField[$index] = $field;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $colToField;
|
// 核心字段校验:缺少就终止(保证你说的“联系人/电话/邮箱没问题”)
|
||||||
|
$this->assertRequiredFields($colToField);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'colToField' => $colToField,
|
||||||
|
'unknownCols' => $unknownCols,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 核心字段校验:要求必须能映射出 requiredDbFields
|
||||||
|
*/
|
||||||
|
protected function assertRequiredFields(array $colToField): void
|
||||||
|
{
|
||||||
|
$fields = array_values($colToField);
|
||||||
|
foreach ($this->requiredDbFields as $req) {
|
||||||
|
if (!in_array($req, $fields, true)) {
|
||||||
|
throw new BusinessException("Excel 表头缺少必要字段映射:{$req}(请补充映射或别名)");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 表头标准化:
|
* 表头标准化:
|
||||||
* 目前只去 BOM + trim,保持和 headerMap 中 key 完全一致
|
* - 去 BOM
|
||||||
* 如果后面再出现“有空格/大小写”的版本,我们可以在这里做 strtolower / 替换空格 为 _
|
* - trim
|
||||||
|
* - ✅ 统一处理尾部多余的 ? / _(解决 xxx?_ 与 xxx?)
|
||||||
|
*
|
||||||
|
* 注意:为了避免误伤中文表头,这里默认不做 strtolower / 空格替换。
|
||||||
|
* 若你后续想增强,可在注释处开启。
|
||||||
*/
|
*/
|
||||||
protected function normalizeHeader(string $header): string
|
protected function normalizeHeader(string $header): string
|
||||||
{
|
{
|
||||||
@@ -149,6 +196,17 @@ class FbLeadHeaderMapper
|
|||||||
// 去掉前后空白
|
// 去掉前后空白
|
||||||
$header = trim($header);
|
$header = trim($header);
|
||||||
|
|
||||||
|
if ($header === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 可选增强(默认不开启,避免影响中文):
|
||||||
|
// $header = mb_strtolower($header);
|
||||||
|
// $header = preg_replace('/\s+/', '_', $header);
|
||||||
|
|
||||||
|
// ✅ 去掉末尾连续的 "_" / "?"
|
||||||
|
$header = rtrim($header, "_?");
|
||||||
|
|
||||||
return $header;
|
return $header;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\process;
|
||||||
|
|
||||||
|
use app\inquiry\service\FbFileImportService;
|
||||||
|
use app\inquiry\service\FbLeadSyncService;
|
||||||
|
use app\inquiry\service\InquiryAssignService;
|
||||||
|
use Workerman\Timer;
|
||||||
|
use support\Log;
|
||||||
|
|
||||||
|
class InquiryAutoProcess
|
||||||
|
{
|
||||||
|
protected int $interval;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $interval 间隔秒数,默认 30 秒
|
||||||
|
*/
|
||||||
|
public function __construct(int $interval = 30)
|
||||||
|
{
|
||||||
|
$this->interval = $interval;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onWorkerStart()
|
||||||
|
{
|
||||||
|
// 每 interval 秒执行一次
|
||||||
|
Timer::add($this->interval, function () {
|
||||||
|
$this->runOnceForAllChannels();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 一轮执行:扫描 config/inquiry_channel.php 中所有渠道,逐个跑一遍流水线
|
||||||
|
*/
|
||||||
|
protected function runOnceForAllChannels()
|
||||||
|
{
|
||||||
|
$configAll = config('inquiry_channel');
|
||||||
|
if (empty($configAll) || !is_array($configAll)) {
|
||||||
|
Log::error('InquiryAutoProcess: config("inquiry_channel") 为空或格式不正确');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($configAll as $channelKey => $cfg) {
|
||||||
|
if (!is_array($cfg)) {
|
||||||
|
Log::error("InquiryAutoProcess: 渠道 {$channelKey} 配置不是数组,已跳过");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->runForChannel($channelKey, $cfg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对单个渠道执行:导入 Excel -> fb_leads -> inquiry_log -> 分配 + 推队列
|
||||||
|
*/
|
||||||
|
protected function runForChannel(string $channelKey, array $cfg)
|
||||||
|
{
|
||||||
|
$watchDir = public_path($cfg['watch_dir'] ?? 'excel');
|
||||||
|
$enterUserId = (int)($cfg['enter_user_id'] ?? 0);
|
||||||
|
$channelId = (int)($cfg['channel_id'] ?? 0);
|
||||||
|
$salesUserIds = $cfg['sales_user_ids'] ?? [];
|
||||||
|
$roundRobinKey = $cfg['round_robin_key'] ?? $channelKey;
|
||||||
|
|
||||||
|
if ($enterUserId <= 0 || $channelId <= 0) {
|
||||||
|
Log::error("InquiryAutoProcess: 渠道 {$channelKey} 配置缺少正确的 enter_user_id 或 channel_id,已跳过");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$importService = new FbFileImportService();
|
||||||
|
$syncService = new FbLeadSyncService();
|
||||||
|
$assignService = new InquiryAssignService();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1) 导入该渠道目录下的 Excel 到 fb_leads(fb_leads.channel = 渠道ID)
|
||||||
|
$filesImported = $importService->importDir($watchDir, $channelId);
|
||||||
|
|
||||||
|
// 2) 只同步该渠道的 fb_leads -> inquiry_log(channel 过滤)
|
||||||
|
$synced = $syncService->syncToInquiryLog($enterUserId, $channelId);
|
||||||
|
|
||||||
|
// 3) 只分配该渠道的 inquiry_log(channel 过滤)
|
||||||
|
// $assigned = $assignService->assignBatch($salesUserIds, $roundRobinKey, $channelId, 100);
|
||||||
|
$assigned = $assignService->assignBatch(
|
||||||
|
$salesUserIds,
|
||||||
|
$roundRobinKey,
|
||||||
|
$channelId,
|
||||||
|
$cfg['assign_rules'] ?? [],
|
||||||
|
100
|
||||||
|
);
|
||||||
|
|
||||||
|
Log::info("InquiryAutoProcess[{$channelKey}] files={$filesImported}, synced={$synced}, assigned={$assigned}");
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Log::error(
|
||||||
|
'InquiryAutoProcess[' . $channelKey . '] error: ' .
|
||||||
|
$e->getMessage() . "\n" . $e->getTraceAsString()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
// 派卡社媒
|
||||||
|
'fb_paika_shemei' => [
|
||||||
|
// 监听目录(相对 public_path)
|
||||||
|
'watch_dir' => 'excel/fb_paika_shemei',
|
||||||
|
|
||||||
|
// 清洗到 inquiry_log 时使用的录入人 & 渠道
|
||||||
|
'enter_user_id' => 186,
|
||||||
|
'channel_id' => 10, // 👈 这里就是你系统里的渠道ID
|
||||||
|
|
||||||
|
// 参与轮询分配的业务员 ID 列表
|
||||||
|
'sales_user_ids' => [
|
||||||
|
63,
|
||||||
|
67,
|
||||||
|
93,
|
||||||
|
108,
|
||||||
|
132,
|
||||||
|
143,
|
||||||
|
145,
|
||||||
|
157,
|
||||||
|
160,
|
||||||
|
168,
|
||||||
|
172,
|
||||||
|
173,
|
||||||
|
191,
|
||||||
|
193,
|
||||||
|
194,
|
||||||
|
195,
|
||||||
|
196,
|
||||||
|
197,
|
||||||
|
200
|
||||||
|
],
|
||||||
|
|
||||||
|
// 轮询用的分组 key(Redis 记指针用)
|
||||||
|
'round_robin_key'=> 'fb_paika_shemei_inquiry',
|
||||||
|
|
||||||
|
// ✅ 渠道内分配规则(按顺序匹配,先命中先用)
|
||||||
|
'assign_rules' => [
|
||||||
|
[
|
||||||
|
'name' => '加纳池',
|
||||||
|
'countries' => [82],
|
||||||
|
'sales_user_ids' => [93],
|
||||||
|
'round_robin_key' => 'fb_paika_shemei_gh',
|
||||||
|
]
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
// 合塑社媒
|
||||||
|
'fb_hesu_shemei' => [
|
||||||
|
'watch_dir' => 'excel/fb_hesu_shemei',
|
||||||
|
'enter_user_id' => 186,
|
||||||
|
'channel_id' => 6,
|
||||||
|
'sales_user_ids' => [
|
||||||
|
22, 23, 34, 44, 49, 71, 72, 73, 96, 104, 125, 135, 155
|
||||||
|
],
|
||||||
|
'round_robin_key'=> 'fb_hesu_shemei_inquiry',
|
||||||
|
// ✅ 渠道内分配规则(按顺序匹配,先命中先用)
|
||||||
|
'assign_rules' => [
|
||||||
|
[
|
||||||
|
'name' => '加纳池',
|
||||||
|
'countries' => [82],
|
||||||
|
'sales_user_ids' => [44],
|
||||||
|
'round_robin_key' => 'fb_hesu_shemei_gh',
|
||||||
|
]
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
@@ -69,4 +69,12 @@ return [
|
|||||||
'checkweb' => [
|
'checkweb' => [
|
||||||
'handler' => app\process\CheckWeb::class
|
'handler' => app\process\CheckWeb::class
|
||||||
],
|
],
|
||||||
|
'inquiry_auto_all' => [
|
||||||
|
'handler' => app\process\InquiryAutoProcess::class,
|
||||||
|
'count' => 1,
|
||||||
|
'reloadable' => true,
|
||||||
|
'constructor' => [
|
||||||
|
'interval' => 30, // 每 30 秒跑一轮所有渠道
|
||||||
|
],
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user