This commit is contained in:
mzl
2025-12-12 09:47:34 +00:00
parent 909f169aae
commit f99b2a84ca
6 changed files with 404 additions and 120 deletions
+93 -35
View File
@@ -8,7 +8,7 @@ class FbLeadHeaderMapper
{
/**
* Excel 原始表头 => DB 字段名
* 注意:key 用的是“原始表头字符串”(包括下划线)
* 注意:这里的 key 会经过 normalizeHeader() 处理后再匹配
*/
protected array $headerMap = [
// ===== 基础公共字段 =====
@@ -33,37 +33,53 @@ class FbLeadHeaderMapper
'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', // 这里也归到车型/版本字段
'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',
// ===== 数量相关:建议归到 car_quantity_text(避免 1-2/3-10/10+)=====
// 你之前 car_quantity 用 int 时会截断,这里建议后续统一改成 car_quantity_text
'how_many_cars_would_you_like_to_buy' => 'car_quantity_text',
'_how_many_cars_would_you_like_to_buy' => 'car_quantity_text',
'how_many_cars_do_you_buy' => 'car_quantity_text',
'how_many_cars_do_you_want_to_purchase' => 'car_quantity_text',
'how_many_cars_do_you_want_to_buy' => 'car_quantity_text',
'what_quantity_do_you_want_to_buy' => 'car_quantity_text',
'what_quantity_do_you_wantv_to_buy' => 'car_quantity_text', // wantv 拼写兼容
// ===== 颜色相关:归到 favorite_color =====
'what_color_do_you_like?' => 'favorite_color',
'which_color_would_you_like_to_purchase?' => 'favorite_color',
'what_color_do_you_like' => 'favorite_color',
'which_color_would_you_like_to_purchase' => 'favorite_color',
'what_color_car_do_you_plan_to_buy' => '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',
'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',
'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
{
@@ -76,12 +92,11 @@ class FbLeadHeaderMapper
public function mapHeader(string $rawHeader): ?string
{
$key = $this->normalizeHeader($rawHeader);
return $this->headerMap[$key] ?? null;
}
/**
* 校验一整行表头:
* 旧的严格校验:保留(有时你可能还想用来排查某个表单的字段)
* - 有任何不认识的表头,就抛 BusinessException
*/
public function validateHeaders(array $rawHeaders): void
@@ -104,42 +119,74 @@ class FbLeadHeaderMapper
if (!empty($unsupported)) {
$msg = 'Excel 表头包含未兼容字段:' . implode('、', $unsupported)
. '。请检查这些字段是否需要加入映射配置。';
. '。请检查这些字段是否需要加入映射配置。';
throw new BusinessException($msg);
}
}
/**
* 根据第一行表头,构建 “列索引 => DB 字段” 映射
* ✅ 宽松模式:根据第一行表头,构建映射
*
* 返回结构:
* [
* 'colToField' => [ 'A' => 'email', 'B' => 'phone', ... ],
* 'unknownCols' => [ 'C' => 'some_question?', ... ], // 未映射字段(后续可进富文本/extra_data)
* ]
*/
public function buildColumnFieldMap(array $rawHeaders): array
{
// 先做一遍校验(发现问题直接抛异常)
$this->validateHeaders($rawHeaders);
$colToField = [];
$colToField = [];
$unknownCols = [];
foreach ($rawHeaders as $index => $header) {
$key = $this->normalizeHeader((string)$header);
$raw = (string)$header;
$key = $this->normalizeHeader($raw);
if ($key === '') {
continue;
}
$field = $this->headerMap[$key] ?? null;
if ($field === null) {
continue; // 理论上不会到这儿,双保险
// 宽松:记录未知列(保留原始表头用于展示)
$unknownCols[$index] = trim($raw) !== '' ? trim($raw) : $key;
continue;
}
$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 完全一致
* 如果后面再出现“有空格/大小写”的版本,我们可以在这里做 strtolower / 替换空格 为 _
* - 去 BOM
* - trim
* - ✅ 统一处理尾部多余的 ? / _(解决 xxx?_ 与 xxx?)
*
* 注意:为了避免误伤中文表头,这里默认不做 strtolower / 空格替换。
* 若你后续想增强,可在注释处开启。
*/
protected function normalizeHeader(string $header): string
{
@@ -149,6 +196,17 @@ class FbLeadHeaderMapper
// 去掉前后空白
$header = trim($header);
if ($header === '') {
return '';
}
// 可选增强(默认不开启,避免影响中文):
// $header = mb_strtolower($header);
// $header = preg_replace('/\s+/', '_', $header);
// ✅ 去掉末尾连续的 "_" / "?"
$header = rtrim($header, "_?");
return $header;
}
}