Merge branch 'develop'
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\logic;
|
||||
|
||||
use app\common\traits\crud\CrudLogicTrait;
|
||||
use app\common\model\Brand;
|
||||
|
||||
class BrandLogic
|
||||
{
|
||||
use CrudLogicTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = Brand::class;
|
||||
|
||||
public function getList()
|
||||
{
|
||||
return Brand::where('status', 1)->orderBy('sort','desc')->get(['id', 'brand_code', 'brand_name', 'logo', 'description']);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\logic;
|
||||
|
||||
use app\common\traits\crud\CrudLogicTrait;
|
||||
use app\common\model\BrandSeries;
|
||||
|
||||
class BrandSeriesLogic
|
||||
{
|
||||
use CrudLogicTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = BrandSeries::class;
|
||||
|
||||
public function getList($where)
|
||||
{
|
||||
return BrandSeries::where('status', 1)->where($where)->orderBy('sort','desc')->get(['id', 'series_code', 'series_name', 'description']);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\logic;
|
||||
|
||||
/**
|
||||
* Fingerprint
|
||||
*
|
||||
* 目标:
|
||||
* - 默认“强制所有字段参与计算”,保证相同记录生成同一指纹
|
||||
* - 通过统一规范化策略,避免无意义差异(空白、大小写、JSON键序)导致指纹抖动
|
||||
* - 仅排除技术字段:id、fingerprint(可通过参数自行增删)
|
||||
*
|
||||
* 用法:
|
||||
* $hash = Fingerprint::makeFromArray($payload); // 创建前
|
||||
* $hash = Fingerprint::makeFromModel($skuModel); // 已有模型
|
||||
* $hash = Fingerprint::makeFromArray($payload, ['exclude'=>['msrp']]); // 可选排除
|
||||
*/
|
||||
class Fingerprint
|
||||
{
|
||||
/** 你的 veh_vehicle_sku 所有列(保持与表结构一致,顺序无关但建议完整) */
|
||||
public const COLS = [
|
||||
'id','sku_code','fingerprint',
|
||||
'brand_code','brand_name','series_code','series_name',
|
||||
'model_code','model_name','model_year',
|
||||
'trim_code','trim_level_code','edition_code',
|
||||
'trim','trim_level','edition_name',
|
||||
'body_type_code','drive_type_code','motor_layout_code','fuel_type_code',
|
||||
'engine_disp_l','engine_turbo','engine_power_kw','battery_kwh',
|
||||
'transmission_code','gears','range_km','range_std_code',
|
||||
'doors','seats','weight_kg','dimensions_mm',
|
||||
'color_exterior_code','color_exterior_text',
|
||||
'color_interior_code','color_interior_text',
|
||||
'options_official','features','emission_std_code',
|
||||
'origin_country','currency','msrp',
|
||||
'is_active','corp_id','remark',
|
||||
'created_by','updated_by','created_at','updated_at',
|
||||
];
|
||||
|
||||
/** 默认剔除:技术/循环依赖字段 */
|
||||
public const EXCLUDE_DEFAULT = [
|
||||
'id','sku_code','fingerprint', 'dimensions_mm', 'options_official', 'features',
|
||||
'origin_country','currency','msrp',
|
||||
'is_active','corp_id','remark',
|
||||
'created_by','updated_by','created_at','updated_at'
|
||||
];
|
||||
|
||||
/** 识别为“code类”的字段模式(统一大写) */
|
||||
private const CODE_FIELD_REGEX = '/(_code|_country|^currency)$/i';
|
||||
|
||||
/** 识别为 JSON 字段 */
|
||||
private const JSON_FIELDS = ['dimensions_mm','options_official','features'];
|
||||
|
||||
/**
|
||||
* 从 Eloquent 模型生成
|
||||
*/
|
||||
public static function makeFromModel($model, array $opts = []): string
|
||||
{
|
||||
return self::makeFromArray($model->toArray(), $opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从数组生成指纹
|
||||
* @param array $row 一般为 $_POST 合并后的业务数据或 $model->toArray()
|
||||
* @param array $opts ['exclude'=>[], 'extra'=>[], 'salt'=>string]
|
||||
* - exclude: 额外排除的字段(默认仅排除 id,fingerprint)
|
||||
* - extra : 额外拼入的键值(例如上下文、租户策略),会参与计算
|
||||
* - salt : 额外加盐字符串
|
||||
*/
|
||||
public static function makeFromArray(array $row, array $opts = []): string
|
||||
{
|
||||
$exclude = array_values(array_unique(array_merge(self::EXCLUDE_DEFAULT, $opts['exclude'] ?? [])));
|
||||
$extra = $opts['extra'] ?? [];
|
||||
$salt = $opts['salt'] ?? '';
|
||||
|
||||
// 1) 统一抽取“表中定义的字段” + extra,缺失字段也要纳入(值按 NULL 处理)
|
||||
$data = [];
|
||||
foreach (self::COLS as $col) {
|
||||
if (in_array($col, $exclude, true)) continue;
|
||||
$data[$col] = array_key_exists($col, $row) ? $row[$col] : null;
|
||||
}
|
||||
// extra 也参与
|
||||
foreach ($extra as $k => $v) {
|
||||
if (!is_string($k) || $k === '') continue;
|
||||
$data['__extra__'.$k] = $v;
|
||||
}
|
||||
|
||||
// 2) 规范化(按字段特征)
|
||||
$canon = [];
|
||||
foreach ($data as $k => $v) {
|
||||
$canon[$k] = self::normalize($k, $v);
|
||||
}
|
||||
|
||||
// 3) 键名排序,确保顺序无关
|
||||
ksort($canon, SORT_STRING);
|
||||
|
||||
// 4) 拼成稳定串: key=value | key=value...
|
||||
$parts = [];
|
||||
foreach ($canon as $k => $v) {
|
||||
// 为空也必须占位,避免 key 消失影响其他键的意义
|
||||
$parts[] = $k.'='.$v;
|
||||
}
|
||||
if ($salt !== '') {
|
||||
$parts[] = '__salt__='.$salt;
|
||||
}
|
||||
$plain = implode('|', $parts);
|
||||
|
||||
// 5) sha256
|
||||
return hash('sha256', $plain);
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化单个字段
|
||||
* - NULL -> 特殊占位 "∅"(避免与空串混淆)
|
||||
* - 布尔/整数/浮点 -> 统一数字字符串
|
||||
* - code类字段 -> 去首尾空白 & 转大写
|
||||
* - 普通字符串 -> trim & 压缩连续空白为单空格
|
||||
* - JSON字段 -> 解析后做“键排序 + 值规范化 + 索引数组可排序”,再无空白编码
|
||||
*/
|
||||
private static function normalize(string $field, $value): string
|
||||
{
|
||||
if ($value === null) return '∅';
|
||||
|
||||
// 数值类型
|
||||
if (is_int($value)) return (string)$value;
|
||||
if (is_bool($value)) return $value ? '1' : '0';
|
||||
|
||||
if (is_float($value) || (is_string($value) && is_numeric($value) && self::looksLikeNumberField($field))) {
|
||||
// 统一浮点格式,最多 6 位小数并去尾零
|
||||
$num = (float)$value;
|
||||
$s = number_format($num, 6, '.', '');
|
||||
$s = rtrim(rtrim($s, '0'), '.');
|
||||
return $s === '' ? '0' : $s;
|
||||
}
|
||||
|
||||
// JSON 字段
|
||||
if (in_array($field, self::JSON_FIELDS, true)) {
|
||||
return self::normalizeJson($value);
|
||||
}
|
||||
|
||||
// 字符串
|
||||
if (is_string($value)) {
|
||||
$s = trim($value);
|
||||
// code类:统一大写
|
||||
if (preg_match(self::CODE_FIELD_REGEX, $field)) {
|
||||
$s = mb_strtoupper($s, 'UTF-8');
|
||||
} else {
|
||||
// 普通文本:压缩连续空白
|
||||
$s = preg_replace('/\s+/u', ' ', $s);
|
||||
}
|
||||
return $s === '' ? '' : $s;
|
||||
}
|
||||
|
||||
// 数组(极少出现,按 JSON 处理)
|
||||
if (is_array($value)) {
|
||||
return self::normalizeJson($value);
|
||||
}
|
||||
|
||||
// 其他类型(对象等):转 JSON 后处理
|
||||
return self::normalizeJson($value);
|
||||
}
|
||||
|
||||
/** 判断这个字段是否“看起来像数值字段” */
|
||||
private static function looksLikeNumberField(string $field): bool
|
||||
{
|
||||
static $numKeys = [
|
||||
'engine_disp_l','engine_power_kw','battery_kwh','gears',
|
||||
'range_km','doors','seats','weight_kg','msrp','model_year',
|
||||
'created_at','updated_at','corp_id','is_active'
|
||||
];
|
||||
return in_array($field, $numKeys, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON 规范化:
|
||||
* - 字符串:若能 json_decode 则按 JSON;否则当普通字符串再规范化
|
||||
* - 对象:键名排序;值递归规范化
|
||||
* - 索引数组:元素递归规范化后,按字符串值排序(视为集合,避免顺序抖动)
|
||||
* - 最终 json_encode(JSON_UNESCAPED_UNICODE|UNESCAPED_SLASHES)
|
||||
*/
|
||||
private static function normalizeJson($val): string
|
||||
{
|
||||
// 已是数组/对象
|
||||
if (is_array($val)) {
|
||||
$normalized = self::canonArray($val);
|
||||
return json_encode($normalized, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
// 字符串尝试解析
|
||||
if (is_string($val)) {
|
||||
$val = trim($val);
|
||||
if ($val === '') return '""';
|
||||
$decoded = json_decode($val, true);
|
||||
if (json_last_error() === JSON_ERROR_NONE) {
|
||||
$normalized = self::canonArray($decoded);
|
||||
return json_encode($normalized, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
// 非 JSON 字符串,按普通字符串规范化
|
||||
$s = preg_replace('/\s+/u', ' ', $val);
|
||||
return json_encode($s, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
// 其他类型:直接编码
|
||||
return json_encode($val, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
/** 递归规范化数组/对象 */
|
||||
private static function canonArray($arr)
|
||||
{
|
||||
if (!is_array($arr)) return $arr;
|
||||
|
||||
// 关联数组(对象)
|
||||
if (self::isAssoc($arr)) {
|
||||
$out = [];
|
||||
ksort($arr, SORT_STRING);
|
||||
foreach ($arr as $k => $v) {
|
||||
$out[(string)$k] = self::canonValue($v);
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
// 索引数组(当集合处理:元素字符串化后排序)
|
||||
$tmp = [];
|
||||
foreach ($arr as $v) {
|
||||
$tmp[] = self::canonValue($v);
|
||||
}
|
||||
// 将元素转为字符串再排序,确保稳定
|
||||
usort($tmp, function($a, $b) {
|
||||
$sa = is_scalar($a) ? (string)$a : json_encode($a, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
|
||||
$sb = is_scalar($b) ? (string)$b : json_encode($b, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
|
||||
return strcmp($sa, $sb);
|
||||
});
|
||||
return $tmp;
|
||||
}
|
||||
|
||||
private static function canonValue($v)
|
||||
{
|
||||
if (is_array($v)) return self::canonArray($v);
|
||||
if (is_bool($v)) return $v ? 1 : 0;
|
||||
if (is_int($v)) return $v;
|
||||
if (is_float($v)) {
|
||||
$s = number_format($v, 6, '.', '');
|
||||
$s = rtrim(rtrim($s, '0'), '.');
|
||||
return $s === '' ? '0' : $s;
|
||||
}
|
||||
if (is_string($v)) {
|
||||
$s = trim($v);
|
||||
$s = preg_replace('/\s+/u', ' ', $s);
|
||||
return $s;
|
||||
}
|
||||
return $v;
|
||||
}
|
||||
|
||||
private static function isAssoc(array $arr): bool
|
||||
{
|
||||
// 有任意一个键不是 0..n 的递增整数,就视为关联数组
|
||||
$i = 0;
|
||||
foreach ($arr as $k => $_) {
|
||||
if ($k !== $i++) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
namespace app\common\logic;
|
||||
|
||||
final class ModelNaming
|
||||
{
|
||||
/** 生成稳定的 model_code(只用 *_code 与关键数字) */
|
||||
public static function buildModelCode(array $s): string
|
||||
{
|
||||
$brand = self::tok($s['brand_code'] ?? '');
|
||||
$series = self::tok($s['series_code'] ?? '');
|
||||
$fuel = self::tok($s['fuel_type_code'] ?? 'UNK');
|
||||
|
||||
$parts = [$brand, $series];
|
||||
$key = [];
|
||||
|
||||
switch ($fuel) {
|
||||
case 'EV':
|
||||
$key[] = 'EV';
|
||||
if (!empty($s['range_km'])) {
|
||||
$key[] = (string) (round((int)$s['range_km'] / 10) * 10); // 到十位
|
||||
} elseif (!empty($s['battery_kwh'])) {
|
||||
$key[] = self::numToken($s['battery_kwh']) . 'KWH';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'GAS':
|
||||
if (!empty($s['engine_disp_l'])) $key[] = self::dispToken($s['engine_disp_l']); // 1.5L -> 15L
|
||||
if (!empty($s['engine_turbo']) && (int)$s['engine_turbo'] === 1) $key[] = 'T';
|
||||
if (!empty($s['transmission_code'])) $key[] = self::tok($s['transmission_code']);
|
||||
break;
|
||||
|
||||
case 'HEV':
|
||||
case 'PHEV':
|
||||
case 'EREV':
|
||||
$key[] = $fuel;
|
||||
if (!empty($s['engine_disp_l'])) $key[] = self::dispToken($s['engine_disp_l']);
|
||||
if (!empty($s['motor_layout_code'])) $key[] = self::tok($s['motor_layout_code']); // 如 DM-I/DM-P/AWD
|
||||
break;
|
||||
|
||||
default:
|
||||
$key[] = $fuel;
|
||||
}
|
||||
|
||||
if (!empty($s['drive_type_code'])) $key[] = self::tok($s['drive_type_code']);
|
||||
|
||||
$parts[] = implode('_', array_filter($key));
|
||||
return strtoupper(implode('_', array_filter($parts)));
|
||||
}
|
||||
|
||||
/** 生成中文可读的 model_name(走快照中文,不依赖 i18n) */
|
||||
public static function buildModelName(array $s): string
|
||||
{
|
||||
$seriesName = trim((string)($s['series_name'] ?? $s['series_code'] ?? ''));
|
||||
$yearPart = !empty($s['model_year']) ? ($s['model_year'] . '款 ') : '';
|
||||
|
||||
$fuel = strtoupper($s['fuel_type_code'] ?? '');
|
||||
$tail = '';
|
||||
|
||||
if ($fuel === 'EV') {
|
||||
$range = !empty($s['range_km']) ? (int)$s['range_km'] . 'KM ' : '';
|
||||
$drive = trim((string)($s['drive_type'] ?? $s['drive_type_code'] ?? ''));
|
||||
$tail = "EV {$range}{$drive}";
|
||||
} elseif ($fuel === 'GAS') {
|
||||
$disp = (!empty($s['engine_disp_l']))
|
||||
? rtrim(rtrim(number_format((float)$s['engine_disp_l'], 1, '.', ''), '0'), '.') . 'L '
|
||||
: '';
|
||||
$trans = trim((string)($s['transmission'] ?? $s['transmission_code'] ?? ''));
|
||||
$tail = "{$disp}{$trans}";
|
||||
} else {
|
||||
$tail = trim((string)($s['fuel_type'] ?? $fuel));
|
||||
}
|
||||
|
||||
$extras = [];
|
||||
foreach (['trim','trim_level','edition_name'] as $k) {
|
||||
if (!empty($s[$k])) $extras[] = trim((string)$s[$k]);
|
||||
}
|
||||
$extraStr = $extras ? ' ' . implode(' ', $extras) : '';
|
||||
|
||||
return trim($seriesName . ' ' . $yearPart . $tail . $extraStr);
|
||||
}
|
||||
|
||||
/* ===== Helpers ===== */
|
||||
|
||||
private static function tok($v): string
|
||||
{
|
||||
$v = strtoupper((string)$v);
|
||||
return preg_replace('/[^A-Z0-9_]/', '', $v ?? '');
|
||||
}
|
||||
|
||||
private static function numToken($v): string
|
||||
{
|
||||
return preg_replace('/[^0-9]/', '', (string)$v);
|
||||
}
|
||||
|
||||
/** 1.5L -> 15L;2.0L -> 20L(便于 code 稳定检索) */
|
||||
private static function dispToken($l): string
|
||||
{
|
||||
$x = (float)$l;
|
||||
return (string)round($x * 10) . 'L';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\logic;
|
||||
|
||||
use app\common\traits\crud\CrudLogicTrait;
|
||||
use app\common\model\StockOrderAccounts;
|
||||
|
||||
class StockOrderAccountsLogic
|
||||
{
|
||||
use CrudLogicTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = StockOrderAccounts::class;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\logic;
|
||||
|
||||
use app\common\traits\crud\CrudLogicTrait;
|
||||
use app\common\model\StockOrderItemOptions;
|
||||
|
||||
class StockOrderItemOptionsLogic
|
||||
{
|
||||
use CrudLogicTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = StockOrderItemOptions::class;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\logic;
|
||||
|
||||
use app\common\traits\crud\CrudLogicTrait;
|
||||
use app\common\model\StockOrderItems;
|
||||
|
||||
class StockOrderItemsLogic
|
||||
{
|
||||
use CrudLogicTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = StockOrderItems::class;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\logic;
|
||||
|
||||
use app\common\traits\crud\CrudLogicTrait;
|
||||
use app\common\model\StockOrders;
|
||||
|
||||
class StockOrdersLogic
|
||||
{
|
||||
use CrudLogicTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = StockOrders::class;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\logic;
|
||||
|
||||
use app\common\traits\crud\CrudLogicTrait;
|
||||
use app\common\model\SysAccount;
|
||||
|
||||
class SysAccountLogic
|
||||
{
|
||||
use CrudLogicTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = SysAccount::class;
|
||||
|
||||
public function getList()
|
||||
{
|
||||
return SysAccount::where('status', 1)->orderBy('sort','desc')->get(['id', 'account_name', 'currency_id', 'account_number', 'bank_name', 'bank_branch', 'description']);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\logic;
|
||||
|
||||
use app\common\model\SysCurrency;
|
||||
use app\common\traits\crud\CrudLogicTrait;
|
||||
|
||||
class SysCurrencyLogic
|
||||
{
|
||||
use CrudLogicTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = SysCurrency::class;
|
||||
|
||||
public function getList()
|
||||
{
|
||||
return SysCurrency::where('status', 1)->get(['id', 'country_id', 'currency_code', 'currency_name', 'currency_symbol', 'exchange_rate', 'float_percent_up', 'float_percent_down']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\logic;
|
||||
|
||||
use app\common\traits\crud\CrudLogicTrait;
|
||||
use app\common\model\SysDictItem;
|
||||
|
||||
class SysDictItemLogic
|
||||
{
|
||||
use CrudLogicTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = SysDictItem::class;
|
||||
|
||||
public function getList($where)
|
||||
{
|
||||
return SysDictItem::where('status', 1)->where($where)->orderBy('sort','desc')->get(['id', 'item_label', 'item_value', 'remark', 'dict_id']);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\logic;
|
||||
|
||||
use app\common\traits\crud\CrudLogicTrait;
|
||||
use app\common\model\SysDict;
|
||||
|
||||
class SysDictLogic
|
||||
{
|
||||
use CrudLogicTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = SysDict::class;
|
||||
|
||||
public function getDictByCode($code)
|
||||
{
|
||||
return SysDict::where('status', 1)->where('code', $code)->orderBy('sort','desc')->first(['id']);
|
||||
}
|
||||
|
||||
public function getList()
|
||||
{
|
||||
return SysDict::where('status', 1)->get(['id', 'dict_name', 'dict_code', 'description']);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\logic;
|
||||
|
||||
use app\common\traits\crud\CrudLogicTrait;
|
||||
use app\common\model\SysPort;
|
||||
|
||||
class SysPortLogic
|
||||
{
|
||||
use CrudLogicTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = SysPort::class;
|
||||
|
||||
public function getList($where)
|
||||
{
|
||||
return SysPort::where('status', 1)->where($where)->orderBy('sort','desc')->get(['id', 'port_code', 'port_name', 'country_id', 'description']);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\logic;
|
||||
|
||||
use app\common\traits\crud\CrudLogicTrait;
|
||||
use app\common\model\UploadedFiles;
|
||||
|
||||
class UploadedFilesLogic
|
||||
{
|
||||
use CrudLogicTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = UploadedFiles::class;
|
||||
|
||||
/**
|
||||
* 根据分区获取文件列表
|
||||
*
|
||||
* @param string $partition
|
||||
* @param int $limit
|
||||
* @param int $page
|
||||
* @return mixed
|
||||
*/
|
||||
public function getFilesByPartition(string $partition, int $limit = 15, int $page = 1)
|
||||
{
|
||||
$config = [
|
||||
'model' => $this->modelClass,
|
||||
'where' => ['partition' => $partition],
|
||||
'order' => ['created_at' => 'desc', 'id' => 'desc']
|
||||
];
|
||||
|
||||
return $this->logicList($config, $limit, $page);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据分类获取文件列表
|
||||
*
|
||||
* @param string $category
|
||||
* @param int $limit
|
||||
* @param int $page
|
||||
* @return mixed
|
||||
*/
|
||||
public function getFilesByCategory(string $category, int $limit = 15, int $page = 1)
|
||||
{
|
||||
$config = [
|
||||
'model' => $this->modelClass,
|
||||
'where' => ['category' => $category],
|
||||
'order' => ['created_at' => 'desc', 'id' => 'desc']
|
||||
];
|
||||
|
||||
return $this->logicList($config, $limit, $page);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据文件类型获取文件列表
|
||||
*
|
||||
* @param string $fileType
|
||||
* @param int $limit
|
||||
* @param int $page
|
||||
* @return mixed
|
||||
*/
|
||||
public function getFilesByType(string $fileType, int $limit = 15, int $page = 1)
|
||||
{
|
||||
$config = [
|
||||
'model' => $this->modelClass,
|
||||
'where' => ['file_type' => $fileType],
|
||||
'order' => ['created_at' => 'desc', 'id' => 'desc']
|
||||
];
|
||||
|
||||
return $this->logicList($config, $limit, $page);
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索文件
|
||||
*
|
||||
* @param string $keyword
|
||||
* @param int $limit
|
||||
* @param int $page
|
||||
* @return mixed
|
||||
*/
|
||||
public function searchFiles(string $keyword, int $limit = 15, int $page = 1)
|
||||
{
|
||||
$config = [
|
||||
'model' => $this->modelClass,
|
||||
'where' => [
|
||||
'or' => [
|
||||
['file_name', 'like', '%' . $keyword . '%'],
|
||||
['original_name', 'like', '%' . $keyword . '%'],
|
||||
['category', 'like', '%' . $keyword . '%']
|
||||
]
|
||||
],
|
||||
'order' => ['created_at' => 'desc', 'id' => 'desc']
|
||||
];
|
||||
|
||||
return $this->logicList($config, $limit, $page);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件统计信息
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getFileStatistics(): array
|
||||
{
|
||||
$model = new $this->modelClass();
|
||||
|
||||
$totalFiles = $model->count();
|
||||
$totalSize = $model->sum('file_size');
|
||||
|
||||
$partitionStats = $model->selectRaw('partition, COUNT(*) as count, SUM(file_size) as size')
|
||||
->groupBy('partition')
|
||||
->get()
|
||||
->toArray();
|
||||
|
||||
$categoryStats = $model->selectRaw('category, COUNT(*) as count, SUM(file_size) as size')
|
||||
->groupBy('category')
|
||||
->get()
|
||||
->toArray();
|
||||
|
||||
$typeStats = $model->selectRaw('file_type, COUNT(*) as count, SUM(file_size) as size')
|
||||
->groupBy('file_type')
|
||||
->get()
|
||||
->toArray();
|
||||
|
||||
return [
|
||||
'total_files' => $totalFiles,
|
||||
'total_size' => $totalSize,
|
||||
'partition_stats' => $partitionStats,
|
||||
'category_stats' => $categoryStats,
|
||||
'type_stats' => $typeStats
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期文件
|
||||
*
|
||||
* @param int $days 保留天数
|
||||
* @return int 清理的文件数量
|
||||
*/
|
||||
public function cleanExpiredFiles(int $days = 30): int
|
||||
{
|
||||
$expiredTime = time() - ($days * 24 * 60 * 60);
|
||||
|
||||
$config = [
|
||||
'model' => $this->modelClass,
|
||||
'where' => [
|
||||
['created_at', '<', $expiredTime]
|
||||
]
|
||||
];
|
||||
|
||||
$files = $this->logicListAll($config);
|
||||
$count = 0;
|
||||
|
||||
foreach ($files as $file) {
|
||||
// 删除物理文件
|
||||
$fullPath = public_path() . $file->file_path;
|
||||
if (file_exists($fullPath)) {
|
||||
unlink($fullPath);
|
||||
}
|
||||
|
||||
// 删除数据库记录
|
||||
$this->logicDeleteBy(['where' => ['id' => $file->id]]);
|
||||
$count++;
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\logic;
|
||||
|
||||
use app\common\traits\crud\CrudLogicTrait;
|
||||
use app\common\model\VehColor;
|
||||
|
||||
class VehColorLogic
|
||||
{
|
||||
use CrudLogicTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = VehColor::class;
|
||||
|
||||
public function getList($where)
|
||||
{
|
||||
return VehColor::where('status', 1)->where($where)->orderBy('sort','desc')->get(['id', 'color_code', 'color_text']);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\logic;
|
||||
|
||||
use app\common\traits\crud\CrudLogicTrait;
|
||||
use app\common\model\VehDictI18n;
|
||||
|
||||
class VehDictI18nLogic
|
||||
{
|
||||
use CrudLogicTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = VehDictI18n::class;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\logic;
|
||||
|
||||
use app\common\traits\crud\CrudLogicTrait;
|
||||
use app\common\model\VehDictMapping;
|
||||
|
||||
class VehDictMappingLogic
|
||||
{
|
||||
use CrudLogicTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = VehDictMapping::class;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\logic;
|
||||
|
||||
use app\common\traits\crud\CrudLogicTrait;
|
||||
use app\common\model\VehModification;
|
||||
|
||||
class VehModificationLogic
|
||||
{
|
||||
use CrudLogicTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = VehModification::class;
|
||||
|
||||
public function getList()
|
||||
{
|
||||
return VehModification::where('status', 1)->orderBy('sort','desc')->get(['id', 'code', 'name', 'type', 'price']);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\logic;
|
||||
|
||||
use app\common\traits\crud\CrudLogicTrait;
|
||||
use app\common\model\VehMoneyValue;
|
||||
|
||||
class VehMoneyValueLogic
|
||||
{
|
||||
use CrudLogicTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = VehMoneyValue::class;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\logic;
|
||||
|
||||
use app\common\traits\crud\CrudLogicTrait;
|
||||
use app\common\model\VehVehicleListing;
|
||||
|
||||
class VehVehicleListingLogic
|
||||
{
|
||||
use CrudLogicTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = VehVehicleListing::class;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\logic;
|
||||
|
||||
use app\common\traits\crud\CrudLogicTrait;
|
||||
use app\common\model\VehVehicleSku;
|
||||
|
||||
class VehVehicleSkuLogic
|
||||
{
|
||||
use CrudLogicTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = VehVehicleSku::class;
|
||||
|
||||
|
||||
/**
|
||||
* 指纹 = “稳定可复现的事实串”做 sha256
|
||||
* - 仅纳入稳定维度:品牌/车系/年款/能源/驱动/变速/车身 + 关键数值
|
||||
* - 不纳入营销名(trim/edition)、颜色文本等
|
||||
* - corp 维度:若你未来希望租户内去重,可传 $scopeCorp=true
|
||||
*/
|
||||
public static function fingerprintMake(array $sku, bool $scopeCorp = false): string
|
||||
{
|
||||
$norm = function ($v) {
|
||||
if ($v === null || $v === '') return '';
|
||||
if (is_float($v)) return rtrim(rtrim(number_format($v, 3, '.', ''), '0'), '.'); // 规范小数
|
||||
return (string)$v;
|
||||
};
|
||||
|
||||
$parts = [
|
||||
$scopeCorp ? ($sku['corp_id'] ?? 0) : '',
|
||||
|
||||
strtoupper($sku['brand_code'] ?? ''),
|
||||
strtoupper($sku['series_code'] ?? ''),
|
||||
(int)($sku['model_year'] ?? 0),
|
||||
strtoupper($sku['fuel_type_code'] ?? ''),
|
||||
strtoupper($sku['drive_type_code']?? ''),
|
||||
strtoupper($sku['transmission_code'] ?? ''),
|
||||
strtoupper($sku['body_type_code'] ?? ''),
|
||||
|
||||
$norm($sku['engine_disp_l'] ?? null),
|
||||
(int)($sku['engine_turbo'] ?? 0),
|
||||
$norm($sku['battery_kwh'] ?? null),
|
||||
(int)($sku['range_km'] ?? 0),
|
||||
|
||||
strtoupper($sku['range_std_code'] ?? ''),
|
||||
strtoupper($sku['emission_std_code'] ?? ''),
|
||||
];
|
||||
|
||||
$plain = implode('|', $parts);
|
||||
return hash('sha256', $plain);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\logic;
|
||||
|
||||
use app\common\traits\crud\CrudLogicTrait;
|
||||
use app\common\model\VehVehicleUnit;
|
||||
|
||||
class VehVehicleUnitLogic
|
||||
{
|
||||
use CrudLogicTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = VehVehicleUnit::class;
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user