Files
fdesk/app/common/logic/SkuCodeBuilder.php
T
2025-09-16 10:53:57 +08:00

233 lines
7.7 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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;
}
}