241 lines
8.2 KiB
PHP
241 lines
8.2 KiB
PHP
|
|
<?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);
|
|||
|
|
}
|
|||
|
|
}
|