派卡社媒脚本

This commit is contained in:
mzl
2025-12-10 17:23:59 +08:00
parent c85f07cff4
commit 460de92912
6 changed files with 823 additions and 0 deletions
+154
View File
@@ -0,0 +1,154 @@
<?php
namespace app\inquery\utils;
use support\exception\BusinessException;
class FbLeadHeaderMapper
{
/**
* Excel 原始表头 => DB 字段名
* 注意:key 用的是“原始表头字符串”(包括下划线)
*/
protected array $headerMap = [
// ===== 基础公共字段 =====
'id' => 'fb_lead_id',
'created_time' => 'fb_created_time',
'ad_id' => 'ad_id',
'ad_name' => 'ad_name',
'adset_id' => 'adset_id',
'adset_name' => 'adset_name',
'campaign_id' => 'campaign_id',
'campaign_name' => 'campaign_name',
'form_id' => 'form_id',
'form_name' => 'form_name',
'is_organic' => 'is_organic',
'platform' => 'platform',
'full_name' => 'full_name',
'phone' => 'phone',
'phone_number' => 'phone', // 兼容写法
'email' => 'email',
'lead_status' => 'lead_status',
'inbox_url' => 'inbox_url',
// ===== 车型相关:全部归到 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_model_do_you_want_to_purchase?' => 'car_model',
'which_model_would_you_like_to_purchase?' => 'car_model',
'which_version_do_you_want_to_buy?' => 'car_model', // 这里也归到车型/版本字段
// ===== 数量相关:全部归到 car_quantity =====
'how_many_cars_would_you_like_to_buy?_' => 'car_quantity',
'how_many_cars_would_you_like_to_buy?' => 'car_quantity',
'_how_many_cars_would_you_like_to_buy?' => 'car_quantity',
'how_many_cars_do_you_buy?' => 'car_quantity',
'how_many_cars_do_you_want_to_purchase?' => 'car_quantity',
'how_many_cars_do_you_want_to_buy?' => 'car_quantity',
'what_quantity_do_you_want_to_buy?' => 'car_quantity',
// ===== 颜色相关:归到 favorite_color =====
'what_color_do_you_like?' => 'favorite_color',
'which_color_would_you_like_to_purchase?' => 'favorite_color',
// ===== 用途相关:归到 usage_type =====
'are_you_using_it_for_business_or_personal_use?' => 'usage_type',
'business_use_or_personal_use?' => 'usage_type',
'personal_use_or_business_use?' => 'usage_type',
// ===== 座位数相关 =====
'do_you_want_to_buy_six_seats_or_seven_seats?' => 'seat_preference',
];
/**
* 返回所有支持的 Excel 表头
*/
public function getSupportedHeaders(): array
{
return array_keys($this->headerMap);
}
/**
* 单个表头映射:原始表头 -> DB 字段名
*/
public function mapHeader(string $rawHeader): ?string
{
$key = $this->normalizeHeader($rawHeader);
return $this->headerMap[$key] ?? null;
}
/**
* 校验一整行表头:
* - 有任何不认识的表头,就抛 BusinessException
*/
public function validateHeaders(array $rawHeaders): void
{
$unsupported = [];
foreach ($rawHeaders as $header) {
$header = (string)$header;
$key = $this->normalizeHeader($header);
// 允许空表头(多余空列)
if ($key === '') {
continue;
}
if (!isset($this->headerMap[$key])) {
$unsupported[] = $header;
}
}
if (!empty($unsupported)) {
$msg = 'Excel 表头包含未兼容字段:' . implode('、', $unsupported)
. '。请检查这些字段是否需要加入映射配置。';
throw new BusinessException($msg);
}
}
/**
* 根据第一行表头,构建 “列索引 => DB 字段” 映射
*/
public function buildColumnFieldMap(array $rawHeaders): array
{
// 先做一遍校验(发现问题直接抛异常)
$this->validateHeaders($rawHeaders);
$colToField = [];
foreach ($rawHeaders as $index => $header) {
$key = $this->normalizeHeader((string)$header);
if ($key === '') {
continue;
}
$field = $this->headerMap[$key] ?? null;
if ($field === null) {
continue; // 理论上不会到这儿,双保险
}
$colToField[$index] = $field;
}
return $colToField;
}
/**
* 表头标准化:
* 目前只去 BOM + trim,保持和 headerMap 中 key 完全一致
* 如果后面再出现“有空格/大小写”的版本,我们可以在这里做 strtolower / 替换空格 为 _
*/
protected function normalizeHeader(string $header): string
{
// 去掉 UTF-8 BOM
$header = preg_replace('/^\xEF\xBB\xBF/', '', $header);
// 去掉前后空白
$header = trim($header);
return $header;
}
}
+58
View File
@@ -0,0 +1,58 @@
<?php
namespace app\inquery\utils;
use support\Db;
class PhoneCountryHelper
{
/**
* 根据电话/WhatsApp 号码识别国家 ID
* 号码格式可以是:+996557660304、996557660304、00996557660304 等
*/
public static function detectCountryId(?string $rawPhone): ?int
{
if (!$rawPhone) {
return null;
}
// 1. 只保留数字和+
$phone = preg_replace('/[^\d\+]/', '', $rawPhone);
if ($phone === '' || $phone === null) {
return null;
}
// 2. 统一前缀:00 开头 -> +
if (str_starts_with($phone, '00')) {
$phone = '+' . substr($phone, 2);
} elseif ($phone[0] !== '+') {
// 没有 +,默认补一个(大多数线索都是国际号)
$phone = '+' . $phone;
}
$digits = ltrim($phone, '+');
if ($digits === '') {
return null;
}
// 3. 国家区号最长 3 位,从长到短匹配
for ($len = 3; $len >= 1; $len--) {
$code = substr($digits, 0, $len);
if ($code === '') {
continue;
}
$country = Db::table('country')
->where('phone_code', $code)
->first();
if ($country) {
return (int)$country->id;
}
}
// 没找到
return null;
}
}
+46
View File
@@ -0,0 +1,46 @@
<?php
namespace app\inquery\utils;
use support\Redis;
use support\exception\BusinessException;
class RoundRobinAssigner
{
/**
* Redis key 前缀
* 你可以按业务分组:比如 "fb_inquiry", "website_inquiry" 等
*/
protected const REDIS_KEY_PREFIX = 'inquiry:assign:round_robin:';
/**
* 从一组业务员 ID 中按轮询规则返回“下一个业务员 ID”
*
* @param int[] $userIds 业务员 ID 列表(有顺序)
* @param string $groupKey 分配分组标识(同一个分组共享一个轮询指针)
*
* @return int 下一位业务员 ID
* @throws BusinessException
*/
public static function nextUserId(array $userIds, string $groupKey = 'default'): int
{
// 校验
$userIds = array_values(array_filter(array_map('intval', $userIds), fn($v) => $v > 0));
if (empty($userIds)) {
throw new BusinessException('轮询分配业务员失败:业务员列表不能为空');
}
$count = count($userIds);
// Redis key:同一个 groupKey 共用一条轮询链
$key = self::REDIS_KEY_PREFIX . $groupKey;
// 使用 INCR 做自增计数,天然是原子操作,适合并发环境
// 第一次调用 INCR 返回 1,我们用 (value - 1) 来做索引,从 0 开始
$val = Redis::incr($key); // 如果不存在,Redis 会先置为 0 再加 1
$index = ($val - 1) % $count;
return $userIds[$index];
}
}