233 lines
7.7 KiB
PHP
233 lines
7.7 KiB
PHP
<?php
|
||
namespace app\common\logic;
|
||
|
||
use app\common\model\VehVehicleSku;
|
||
|
||
/**
|
||
* SKU 生成器
|
||
* - 输入: 车辆规格数组(以 *_code 为主)
|
||
* - 输出: 稳定、人读友好的 sku_code (<= 40 默认)
|
||
*/
|
||
final class SkuCodeBuilder
|
||
{
|
||
/** 压缩映射(可按需扩充) */
|
||
private const ABBR = [
|
||
// level
|
||
'FLAGSHIP' => 'FLG',
|
||
'PREMIUM' => 'PRM',
|
||
'STANDARD' => 'STD',
|
||
'DELUXE' => 'DLX',
|
||
// edition
|
||
'HONOR' => 'HNR',
|
||
'CHAMPION' => 'CHP',
|
||
'ANNIV' => 'ANV',
|
||
// trans
|
||
'1SPEED' => '1SP',
|
||
// drive/fuel/body 等一般已短,不必压缩
|
||
];
|
||
|
||
/** 入口:生成 sku_code */
|
||
public static function build(array $s, array $opts = []): string
|
||
{
|
||
$maxLen = $opts['max_len'] ?? 40;
|
||
$ensureUnique = $opts['ensure_unique'] ?? true;
|
||
|
||
$tokens = [
|
||
'brand' => self::tok($s['brand_code'] ?? ''),
|
||
'series' => self::tok($s['series_code'] ?? ''),
|
||
'year' => isset($s['model_year']) ? (string)intval($s['model_year']) : '',
|
||
'fuel' => self::tok($s['fuel_type_code'] ?? ''),
|
||
'drive' => self::tok($s['drive_type_code'] ?? ''),
|
||
'spec' => self::buildSpec($s, self::tok($s['fuel_type_code'] ?? '')),
|
||
'level' => self::abbr(self::tok($s['trim_level_code'] ?? '')),
|
||
'edition' => self::abbr(self::tok($s['edition_code'] ?? '')),
|
||
];
|
||
|
||
// 过滤空段并按既定顺序拼接
|
||
$order = ['brand','series','year','fuel','drive','spec','level','edition'];
|
||
$parts = [];
|
||
foreach ($order as $k) {
|
||
if (!empty($tokens[$k])) $parts[$k] = $tokens[$k];
|
||
}
|
||
|
||
$sku = self::normalizeSku(implode('-', $parts));
|
||
|
||
if (strlen($sku) > $maxLen) {
|
||
$sku = self::shrink($parts, $maxLen); // 传入“带键名的 parts”
|
||
}
|
||
|
||
if ($ensureUnique) {
|
||
$sku = self::ensureUnique($sku, $maxLen);
|
||
}
|
||
return $sku;
|
||
}
|
||
|
||
/** 构造规格位(SPEC),按能源类型差异化 */
|
||
private static function buildSpec(array $s, string $fuel): string
|
||
{
|
||
$fuel = strtoupper($fuel);
|
||
if ($fuel === 'EV') {
|
||
if (!empty($s['range_km'])) {
|
||
$r = (int)$s['range_km'];
|
||
$r10 = (int)round($r / 10) * 10; // 到 10 位
|
||
return (string)$r10;
|
||
}
|
||
if (!empty($s['battery_kwh'])) {
|
||
return self::numToken($s['battery_kwh']) . 'KWH';
|
||
}
|
||
return '';
|
||
}
|
||
|
||
if ($fuel === 'GAS') {
|
||
$bits = [];
|
||
if (!empty($s['engine_disp_l'])) {
|
||
$bits[] = self::dispToken($s['engine_disp_l']); // 1.5L -> 15L
|
||
}
|
||
if (!empty($s['engine_turbo']) && intval($s['engine_turbo']) === 1) {
|
||
$bits[] = 'T';
|
||
}
|
||
if (!empty($s['transmission_code'])) {
|
||
$bits[] = self::abbr(self::tok($s['transmission_code']));
|
||
}
|
||
return implode('', $bits); // 如 15LTCVT
|
||
}
|
||
|
||
// HEV/PHEV/EREV 等
|
||
$bits = [];
|
||
if (!empty($s['engine_disp_l'])) {
|
||
$bits[] = self::dispToken($s['engine_disp_l']); // 可空
|
||
}
|
||
if (!empty($s['motor_layout_code'])) {
|
||
$bits[] = self::tok($s['motor_layout_code']); // 如 AWD / DM-I / DM-P
|
||
}
|
||
return implode('', $bits);
|
||
}
|
||
|
||
/** 冲突检测并追加短校验(全局唯一) */
|
||
private static function ensureUnique(string $sku, int $maxLen): string
|
||
{
|
||
if (!VehVehicleSku::where('sku_code', $sku)->exists()) {
|
||
return $sku;
|
||
}
|
||
// 用 crc32 生成 5~6 位 base36 校验(稳定且短)
|
||
$crc = self::base36Crc($sku);
|
||
$suffix = '-X' . $crc;
|
||
|
||
if (strlen($sku . $suffix) > $maxLen) {
|
||
$sku = substr($sku, 0, max(0, $maxLen - strlen($suffix)));
|
||
$sku = rtrim($sku, '-'); // 避免尾部 '-'
|
||
}
|
||
$sku .= $suffix;
|
||
|
||
// 若极端情况下仍冲突,追加递增计数
|
||
$i = 2;
|
||
$candidate = $sku;
|
||
while (VehVehicleSku::where('sku_code', $candidate)->exists()) {
|
||
$extra = '-'.$i;
|
||
$cut = max(0, $maxLen - strlen($extra));
|
||
$candidate = substr($sku, 0, $cut) . $extra;
|
||
$i++;
|
||
}
|
||
return $candidate;
|
||
}
|
||
|
||
/** 把 token 数组尽量压到 maxLen(逐步压缩策略) */
|
||
private static function shrink(array $partsByKey, int $maxLen): string
|
||
{
|
||
// 1) 先对 series/level/edition 做激进压缩
|
||
$p = $partsByKey;
|
||
foreach (['series','level','edition'] as $k) {
|
||
if (!empty($p[$k])) $p[$k] = self::heavyAbbr($p[$k]);
|
||
}
|
||
$sku = self::normalizeSku(implode('-', $p));
|
||
if (strlen($sku) <= $maxLen) return $sku;
|
||
|
||
// 2) 依次移除不太关键的信息:edition → level(若存在)
|
||
if (!empty($p['edition'])) { unset($p['edition']); }
|
||
$sku = self::normalizeSku(implode('-', $p));
|
||
if (strlen($sku) <= $maxLen) return $sku;
|
||
|
||
if (!empty($p['level'])) { unset($p['level']); }
|
||
$sku = self::normalizeSku(implode('-', $p));
|
||
if (strlen($sku) <= $maxLen) return $sku;
|
||
|
||
// 3) 压缩 spec(如去 KM/KWH,仅保留核心)
|
||
if (!empty($p['spec'])) { $p['spec'] = self::compactSpec($p['spec']); }
|
||
$sku = self::normalizeSku(implode('-', $p));
|
||
if (strlen($sku) <= $maxLen) return $sku;
|
||
|
||
// 4) 仍超长:从右向左截断次要段(spec/drive/year),保留前三段(brand/series/fuel)优先
|
||
$dropOrder = ['spec','drive','year'];
|
||
$q = $p;
|
||
foreach ($dropOrder as $k) {
|
||
if (!empty($q[$k])) { unset($q[$k]); }
|
||
$sku = self::normalizeSku(implode('-', $q));
|
||
if (strlen($sku) <= $maxLen) return $sku;
|
||
}
|
||
|
||
// 5) 最后兜底:硬截到 maxLen,后续 ensureUnique 会追加 -X 校验并避免冲突
|
||
return substr(self::normalizeSku(implode('-', $p)), 0, $maxLen);
|
||
}
|
||
|
||
/* ===== Helpers ===== */
|
||
|
||
private static function tok($v): string
|
||
{
|
||
$v = strtoupper((string)$v);
|
||
return preg_replace('/[^A-Z0-9_]/', '', $v ?? '');
|
||
}
|
||
|
||
private static function abbr(string $t): string
|
||
{
|
||
return self::ABBR[$t] ?? $t;
|
||
}
|
||
|
||
private static function heavyAbbr(string $t): string
|
||
{
|
||
// 更激进:去除元音,连字符化
|
||
$t = str_replace(['STANDARD','FLAGSHIP','PREMIUM','DELUXE'], ['STD','FLG','PRM','DLX'], $t);
|
||
$t = preg_replace('/[AEIOU]/', '', $t);
|
||
return $t;
|
||
}
|
||
|
||
private static function compactSpec(string $s): string
|
||
{
|
||
// 规则:去 KM/KWH 等单位,只保留数字与核心字母
|
||
$s = strtoupper($s);
|
||
$s = str_replace(['KM','KWH'], '', $s);
|
||
return $s;
|
||
}
|
||
|
||
private static function numToken($v): string
|
||
{
|
||
return preg_replace('/[^0-9]/', '', (string)$v);
|
||
}
|
||
|
||
/** 1.5L -> 15L;2.0L -> 20L */
|
||
private static function dispToken($l): string
|
||
{
|
||
$x = (float)$l;
|
||
return (string)round($x * 10) . 'L';
|
||
}
|
||
|
||
private static function base36Crc(string $s): string
|
||
{
|
||
$crc = sprintf('%u', crc32($s)); // 无符号
|
||
$n = (int)$crc;
|
||
$chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||
$res = '';
|
||
do {
|
||
$res = $chars[$n % 36] . $res;
|
||
$n = intdiv($n, 36);
|
||
} while ($n > 0);
|
||
return substr($res, -6); // 取后6位足够
|
||
}
|
||
|
||
private static function normalizeSku(string $s): string
|
||
{
|
||
// 连续的 '-' 合并,首尾修剪
|
||
$s = preg_replace('/-+/', '-', $s);
|
||
$s = trim($s, '-');
|
||
return $s;
|
||
}
|
||
} |