diff --git a/app/common/controller/VehDictI18nController.php b/app/common/controller/VehDictI18nController.php new file mode 100644 index 0000000..6a87c9f --- /dev/null +++ b/app/common/controller/VehDictI18nController.php @@ -0,0 +1,33 @@ +service = new VehDictI18nService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + // protected function indexFormat($data) + // { + // if (isset($data['created_at'])) + // $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + // if (isset($data['updated_at'])) + // $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + // return $data; + // } +} \ No newline at end of file diff --git a/app/common/controller/VehDictMappingController.php b/app/common/controller/VehDictMappingController.php new file mode 100644 index 0000000..d1a1dfe --- /dev/null +++ b/app/common/controller/VehDictMappingController.php @@ -0,0 +1,33 @@ +service = new VehDictMappingService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + // protected function indexFormat($data) + // { + // if (isset($data['created_at'])) + // $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + // if (isset($data['updated_at'])) + // $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + // return $data; + // } +} \ No newline at end of file diff --git a/app/common/controller/VehVehicleListingController.php b/app/common/controller/VehVehicleListingController.php new file mode 100644 index 0000000..59d49aa --- /dev/null +++ b/app/common/controller/VehVehicleListingController.php @@ -0,0 +1,33 @@ +service = new VehVehicleListingService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + // protected function indexFormat($data) + // { + // if (isset($data['created_at'])) + // $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + // if (isset($data['updated_at'])) + // $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + // return $data; + // } +} \ No newline at end of file diff --git a/app/common/controller/VehVehicleSkuController.php b/app/common/controller/VehVehicleSkuController.php new file mode 100644 index 0000000..b99cacb --- /dev/null +++ b/app/common/controller/VehVehicleSkuController.php @@ -0,0 +1,204 @@ +service = new VehVehicleSkuService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + // protected function indexFormat($data) + // { + // if (isset($data['created_at'])) + // $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + // if (isset($data['updated_at'])) + // $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + // return $data; + // } + + protected function showFormat($data) + { + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + return $data; + } + + + public function store(Request $request) + { + try { + $data = $this->validate($this->service->getValidator(), $request->post(), 'store'); + $res = $this->service->serviceStore($request, $data); + return $this->success($res, '创建成功'); + } catch (\PDOException $e) { + $errorCode = $e->getCode(); + $errorMessage = $e->getMessage(); + if ($errorCode == '23000') { + if (strpos($errorMessage, "Duplicate entry") !== false && strpos($errorMessage, "uk_fingerprint") !== false) { + return $this->error("该车辆指纹已存在,请勿重复提交"); + } + // 其他类型的完整性约束错误 + throw new BusinessException("数据完整性错误", 0, $e); + } + // 重新抛出其他类型的异常 + throw $e; + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + public function update(Request $request) + { + try { + $data = $this->validate($this->service->getValidator(), $request->post(), 'update'); + $res = $this->service->serviceUpdate($request, $data); + return $this->success([], '更新成功'); + } catch (\PDOException $e) { + $errorCode = $e->getCode(); + $errorMessage = $e->getMessage(); + if ($errorCode == '23000') { + if (strpos($errorMessage, "Duplicate entry") !== false && strpos($errorMessage, "uk_fingerprint") !== false) { + return $this->error("该车辆指纹已存在,请勿重复提交"); + } + // 其他类型的完整性约束错误 + throw new BusinessException("数据完整性错误", 0, $e); + } + // 重新抛出其他类型的异常 + throw $e; + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + + public function index(Request $request) + { + $data = [ + 'sku_code' , // 'SKU编码,系统生成', + 'fingerprint' , // '规格指纹(sha256)去重', + 'brand_code' , // '品牌代码,如 BYD、HYUNDAI、KIA', + 'brand_name' , // '品牌名称展示,如 比亚迪、现代、起亚', + 'series_code' , // '车系代码,如 ELANTRA、SONGPLUS', + 'series_name' , // '车系名称展示', + 'model_code' , // '车型/配置代码(工厂/内部编码)', + 'model_name' , // '车型/配置名称(可读,如 伊兰特 1.5L、海狮07)', + 'model_year' , // '年款(YYYY)', + 'trim_code' , // '版型代码(TRIM)', + 'trim_level_code' , // '配置等级代码(TRIM_LEVEL)', + 'edition_code' , // '特别版代码(EDITION)', + 'trim' , // '版型/细分配置(如 TOP旗舰版、荣耀版)', + 'trim_level' , // '配置等级(豪华型/旗舰型等,便于筛选)', + 'edition_name' , // '特别版/纪念版(如 荣耀版、致敬版等)', + 'body_type_code' , // '车身类型:SEDAN/SUV/HB/MPV/Pickup', + 'drive_type_code' , // '驱动:FWD/RWD/AWD/4WD', + 'motor_layout_code' , // '电驱/电机布置(MOTOR_LAYOUT):FWD/RWD/AWD/DM-i/DM-p 等', + 'fuel_type_code' , // '能源:GAS/HEV/PHEV/EV/EREV', + 'engine_disp_l' , // '排量(L),如 1.5;纯电为空', + 'engine_turbo' , // '0' COMMENT '是否涡轮(0/1)', + 'engine_power_kw' , // '最大功率(kW)', + 'battery_kwh' , // '电池容量(kWh)', + 'transmission_code' , // '变速箱:MT/AT/CVT/DCT/1SPEED', + 'gears' , // '挡位数(EV多为1)', + 'range_km' , // '续航里程(km)', + 'range_std_code' , // '续航标准:CLTC/WLTP/EPA/NEDC', + 'doors' , // '门数', + 'seats' , // '座位数', + 'weight_kg' , // '整备质量(kg)', + 'dimensions_mm' , // '尺寸(mm):{length,width,height,wheelbase}', + 'color_exterior_code' , // '外观色代码', + 'color_exterior_text' , // '外观色名称', + 'color_interior_code' , // '内饰色代码', + 'color_interior_text' , // '内饰色名称', + 'options_official' , // '官方选装(结构化 JSON)', + 'features' , // '功能/安全/智能配置(结构化 JSON)', + 'emission_std_code' , // '排放标准:CHN6/EURO6/EPA', + 'origin_country' , // '原产国 ISO-3166-1 alpha-2', + 'currency' , // 'CNY' COMMENT '币种 ISO-4217', + 'msrp' , // '建议零售价/参考价', + 'is_active' , // '1' COMMENT '启用(1)/禁用(0)', + 'corp_id' , // '0' COMMENT '租户/公司(全局库可写0)', + 'remark' , // '备注', + 'created_by' , // '0', + 'updated_by' , // '0', + ]; + + + $data = [ + 'fingerprint' , // '规格指纹(sha256)去重', + 'brand_name' , // '品牌名称展示,如 比亚迪、现代、起亚', + 'series_name' , // '车系名称展示', + 'model_name' , // '车型/配置名称(可读,如 伊兰特 1.5L、海狮07)', + 'model_year' , // '年款(YYYY)', + 'trim' , // '版型/细分配置(如 TOP旗舰版、荣耀版)', + 'trim_level' , // '配置等级(豪华型/旗舰型等,便于筛选)', + 'edition_name' , // '特别版/纪念版(如 荣耀版、致敬版等)', + 'engine_disp_l' , // '排量(L),如 1.5;纯电为空', + 'engine_turbo' , // '0' COMMENT '是否涡轮(0/1)', + 'engine_power_kw' , // '最大功率(kW)', + 'battery_kwh' , // '电池容量(kWh)', + 'gears' , // '挡位数(EV多为1)', + 'range_km' , // '续航里程(km)', + 'doors' , // '门数', + 'seats' , // '座位数', + 'weight_kg' , // '整备质量(kg)', + 'dimensions_mm' , // '尺寸(mm):{length,width,height,wheelbase}', + 'color_exterior_text' , // '外观色名称', + 'color_interior_text' , // '内饰色名称', + 'options_official' , // '官方选装(结构化 JSON)', + 'features' , // '功能/安全/智能配置(结构化 JSON)', + 'origin_country' , // '原产国 ISO-3166-1 alpha-2', + 'currency' , // 'CNY' COMMENT '币种 ISO-4217', + 'msrp' , // '建议零售价/参考价', + 'is_active' , // '1' COMMENT '启用(1)/禁用(0)', + 'corp_id' , // '0' COMMENT '租户/公司(全局库可写0)', + 'remark' , // '备注', + 'created_by' , // '0', + 'updated_by' , // '0', + + 'body_type', // '车身类型' + 'drive_type', // '驱动:FWD/RWD/AWD/4WD', + 'motor_layout', // '电机布置(MOTOR_LAYOUT):FWD/RWD/AWD/DM-i/DM-p 等' + 'fuel_type', // '能源:GAS/HEV/PHEV/EV/EREV' + 'transmission', // '变速箱:MT/AT/CVT/DCT/1SPEED' + 'range_std', // '续航标准:CLTC/WLTP/EPA/NEDC' + 'emission_std', // '排放标准:CHN6/EURO6/EPA' + + 'sku_code' , // 'SKU编码,系统生成', + 'brand_code' , // '品牌代码,如 BYD、HYUNDAI、KIA', + 'series_code' , // '车系代码,如 ELANTRA、SONGPLUS', + 'model_code' , // '车型/配置代码(工厂/内部编码)', + 'trim_code' , // '版型代码(TRIM)', + 'trim_level_code' , // '配置等级代码(TRIM_LEVEL)', + 'edition_code' , // '特别版代码(EDITION)', + 'color_exterior_code' , // '外观色代码', + 'color_interior_code' , // '内饰色代码', + + 'body_type_code' , // '车身类型:SEDAN/SUV/HB/MPV/Pickup', + 'drive_type_code' , // '驱动:FWD/RWD/AWD/4WD', + 'motor_layout_code' , // '电驱/电机布置(MOTOR_LAYOUT):FWD/RWD/AWD/DM-i/DM-p 等', + 'fuel_type_code' , // '能源:GAS/HEV/PHEV/EV/EREV', + 'transmission_code' , // '变速箱:MT/AT/CVT/DCT/1SPEED', + 'range_std_code' , // '续航标准:CLTC/WLTP/EPA/NEDC', + 'emission_std_code' , // '排放标准:CHN6/EURO6/EPA', + ]; + } +} \ No newline at end of file diff --git a/app/common/controller/VehVehicleUnitController.php b/app/common/controller/VehVehicleUnitController.php new file mode 100644 index 0000000..3d2e7e2 --- /dev/null +++ b/app/common/controller/VehVehicleUnitController.php @@ -0,0 +1,33 @@ +service = new VehVehicleUnitService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + // protected function indexFormat($data) + // { + // if (isset($data['created_at'])) + // $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + // if (isset($data['updated_at'])) + // $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + // return $data; + // } +} \ No newline at end of file diff --git a/app/common/logic/Fingerprint.php b/app/common/logic/Fingerprint.php new file mode 100644 index 0000000..a066ada --- /dev/null +++ b/app/common/logic/Fingerprint.php @@ -0,0 +1,262 @@ +['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; + } +} \ No newline at end of file diff --git a/app/common/logic/ModelNaming.php b/app/common/logic/ModelNaming.php new file mode 100644 index 0000000..b50a4cf --- /dev/null +++ b/app/common/logic/ModelNaming.php @@ -0,0 +1,101 @@ + 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'; + } +} \ No newline at end of file diff --git a/app/common/logic/SkuCodeBuilder.php b/app/common/logic/SkuCodeBuilder.php new file mode 100644 index 0000000..823b8dc --- /dev/null +++ b/app/common/logic/SkuCodeBuilder.php @@ -0,0 +1,233 @@ + '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; + } +} \ No newline at end of file diff --git a/app/common/logic/VehDictI18nLogic.php b/app/common/logic/VehDictI18nLogic.php new file mode 100644 index 0000000..f25b50b --- /dev/null +++ b/app/common/logic/VehDictI18nLogic.php @@ -0,0 +1,17 @@ + 'integer', + 'enabled' => 'integer', + 'sort_order' => 'integer', + 'created_at' => 'integer', + 'updated_at' => 'integer', + ]; + + protected $fillable = [ + 'field','dict','required','enabled','sort_order', + 'remark','created_at','updated_at' + ]; +} \ No newline at end of file diff --git a/app/common/model/VehVehicleListing.php b/app/common/model/VehVehicleListing.php new file mode 100644 index 0000000..77d238c --- /dev/null +++ b/app/common/model/VehVehicleListing.php @@ -0,0 +1,49 @@ + 'array', + 'features' => 'array', + 'dimensions_mm' => 'array', + 'model_year' => 'integer', + 'engine_turbo' => 'integer', + 'gears' => 'integer', + 'range_km' => 'integer', + 'doors' => 'integer', + 'seats' => 'integer', + 'weight_kg' => 'integer', + 'msrp' => 'float', + 'battery_kwh' => 'float', + 'engine_disp_l' => 'float', + 'is_active' => 'integer', + 'corp_id' => 'integer', + 'created_at' => 'integer', + 'updated_at' => 'integer', + ]; + + protected $fillable = [ + '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', + 'body_type', 'drive_type', 'motor_layout', 'fuel_type', 'transmission', + 'range_std', 'emission_std' + ]; +} \ No newline at end of file diff --git a/app/common/model/VehVehicleUnit.php b/app/common/model/VehVehicleUnit.php new file mode 100644 index 0000000..28e38f1 --- /dev/null +++ b/app/common/model/VehVehicleUnit.php @@ -0,0 +1,49 @@ +validator; + } + + + /** 本进程缓存:[dict][code] => labels array */ + protected static array $labelsCache = []; + + public static function label(string $dict=null, ?string $code=null, string $locale='zh-CN', ?string $fallbackZh=null): ?string + { + if (!$dict || !$code) return $fallbackZh; + + $labels = self::labels($dict, $code); + if (!$labels) return $fallbackZh ?? $code; + + return $labels[$locale] ?? $labels['zh-CN'] ?? $code; + } + + /** 返回 labels 数组,命中缓存 */ + public static function labels(string $dict, string $code): ?array + { + $dict = strtoupper($dict); + $code = (string)$code; + + if (isset(self::$labelsCache[$dict][$code])) { + return self::$labelsCache[$dict][$code]; + } + + $term = DictTerm::where('dict',$dict)->where('code',$code)->first(['labels']); + if (!$term) return null; + + $labels = $term->labels ?? null; + if (!is_array($labels)) return null; + + self::$labelsCache[$dict][$code] = $labels; + return $labels; + } + + /** 校验 code 是否存在于字典(用于必填维度) */ + public static function ensureExists(string $dict, ?string $code, string $fieldNameForError): void + { + if ($code === null || $code === '') { + throw new \InvalidArgumentException("缺少必填字段:{$fieldNameForError}"); + } + $ok = VehDictI18n::where('dict',$dict)->where('code',$code)->where('status', 1)->exists(); + if (!$ok) throw new \InvalidArgumentException("字典不存在或未启用:{$dict}.{$code}"); + } +} \ No newline at end of file diff --git a/app/common/service/VehDictMappingService.php b/app/common/service/VehDictMappingService.php new file mode 100644 index 0000000..67c5288 --- /dev/null +++ b/app/common/service/VehDictMappingService.php @@ -0,0 +1,57 @@ +validator; + } + + + public static function all(): array + { + $rows = VehDictMapping::where('status', 1)->orderBy('sort')->orderBy('id')->get()->toArray(); + $out = []; + foreach ($rows as $r) { + $out[$r['dict']] = [ + 'field' => $r['field'], + 'dict' => $r['dict'], + 'required' => (int)$r['required'] === 1, + ]; + } + return $out; + } + + public static function requiredDicts(): array + { + return array_keys(array_filter(self::all(), fn($x)=>$x['required']===true)); + } + + public static function enabled(string $dict): bool + { + $all = self::all(); + return isset($all[$dict]); + } +} \ No newline at end of file diff --git a/app/common/service/VehVehicleListingService.php b/app/common/service/VehVehicleListingService.php new file mode 100644 index 0000000..3aee146 --- /dev/null +++ b/app/common/service/VehVehicleListingService.php @@ -0,0 +1,31 @@ +validator; + } +} \ No newline at end of file diff --git a/app/common/service/VehVehicleSkuService.php b/app/common/service/VehVehicleSkuService.php new file mode 100644 index 0000000..2cfb588 --- /dev/null +++ b/app/common/service/VehVehicleSkuService.php @@ -0,0 +1,349 @@ +validator; + } + + + // 枚举 + private const MAPS = [ + 'BODY' => [ + 'SEDAN' => '三厢轿车', 'SUV'=>'SUV', 'HB'=>'两厢/掀背', 'MPV'=>'MPV', 'PICKUP'=>'皮卡' + ], + 'DRIVE' => [ + 'FWD'=>'前驱', 'RWD'=>'后驱', 'AWD'=>'四驱(AWD)', '4WD'=>'四驱(4WD)' + ], + 'MOTOR_LAYOUT' => [ + 'FWD'=>'前置单电机', 'RWD'=>'后置单电机', 'AWD'=>'双电机四驱', 'DM-I'=>'DM-i', 'DM-P'=>'DM-p' + ], + 'FUEL' => [ + 'GAS'=>'汽油', 'HEV'=>'油电混动', 'PHEV'=>'插电混动', 'EV'=>'纯电', 'EREV'=>'增程' + ], + 'TRANS' => [ + 'MT'=>'手动', 'AT'=>'自动', 'CVT'=>'无级', 'DCT'=>'双离合', '1SPEED'=>'单速' + ], + 'RANGE_STD' => [ + 'CLTC'=>'CLTC', 'WLTP'=>'WLTP', 'EPA'=>'EPA', 'NEDC'=>'NEDC' + ], + 'EMISSION' => [ + 'CHN6'=>'国六', 'EURO6'=>'欧6', 'EPA'=>'美国EPA' + ], + ]; + + // *_code取中文 + public function label(string $dict, ?string $code): ?string + { + if ($code === null || $code === '') return null; + $dict = strtoupper($dict); + $code = strtoupper($code); + return self::MAPS[$dict][$code] ?? $code; + } + + // 静态映射把 *_code → 中文 + private function backfill(array $d): array + { + $d['body_type'] = $this->label('BODY', $d['body_type_code']); + $d['drive_type'] = $this->label('DRIVE', $d['drive_type_code']); + $d['motor_layout'] = $this->label('MOTOR_LAYOUT', $d['motor_layout_code']); + $d['fuel_type'] = $this->label('FUEL', $d['fuel_type_code']); + $d['transmission'] = $this->label('TRANS', $d['transmission_code']); + $d['range_std'] = $this->label('RANGE_STD', $d['range_std_code']); + $d['emission_std'] = $this->label('EMISSION', $d['emission_std_code']); + return $d; + } + + protected function buildQueryConfig(Request $request, string $scene = 'default'): array + { + $p = $request->all(); + + $config = []; + $config['model'] = $this->modelClass; + switch ($scene) { + case 'default': + $config['where']['is_active'] = 1; + $config['order']['sort'] = 'desc'; + $config['order']['id'] = 'desc'; + break; + case 'index': + $config['where']['is_active'] = 1; + $config['order']['sort'] = 'desc'; + $config['order']['id'] = 'desc'; + break; + case 'show': + $config['where']['id'] = $p['id']; + $config['where']['is_active'] = 1; + break; + case 'store': + break; + case 'update': + $config['where']['id'] = $p['id']; + break; + case 'destroy': + $config['where']['id'] = $p['id']; + break; + } + + return $config; + } + + public function beforeServiceStore($data, Request $request) + { + $data = $this->backfill($data); + // 未提供时自动生成;若你想“强制覆盖人工输入”,可去掉 isset 判断 + if (empty($data['model_code'])) { + $data['model_code'] = ModelNaming::buildModelCode($data); + } + if (empty($data['model_name'])) { + $data['model_name'] = ModelNaming::buildModelName($data); + } + // 填充系统字段 + $data['sku_code'] = SkuCodeBuilder::build($data); + $data['corp_id'] = 0; + $data['created_by'] = $request->user->uid; + $data['updated_by'] = $request->user->uid; + // 指纹 + $data['fingerprint'] = Fingerprint::makeFromArray($data); + return $data; + } + + protected function afterServiceStore($res) + { + + return $res; + } + + + public function beforeServiceUpdate($data, Request $request) + { + $data = $this->backfill($data); + // 未提供时自动生成;若你想“强制覆盖人工输入”,可去掉 isset 判断 + if (empty($data['model_code'])) { + $data['model_code'] = ModelNaming::buildModelCode($data); + } + if (empty($data['model_name'])) { + $data['model_name'] = ModelNaming::buildModelName($data); + } + // 填充系统字段 + $data['updated_by'] = $request->user->uid; + // 指纹 + $data['fingerprint'] = Fingerprint::makeFromArray($data); + return $data; + } + + protected function afterServiceUpdate($res) + { + + return $res; + } + + + /** SKU 字段名 → dict 名(用于 i18n 取词 & 必填校验) */ + protected static array $SKU_FIELD_TO_DICT = [ + 'brand_code' => 'BRAND', + 'series_code' => 'SERIES', + 'model_code' => 'MODEL', + 'trim_code' => 'TRIM', + 'trim_level_code' => 'TRIM_LEVEL', + 'edition_code' => 'EDITION', + 'fuel_type_code' => 'FUEL', + 'drive_type_code' => 'DRIVE', + 'transmission_code' => 'TRANS', + 'motor_layout_code' => 'MOTOR_LAYOUT', + 'body_type_code' => 'BODY', + 'range_std_code' => 'RANGE_STD', + 'emission_std_code' => 'EMISSION', + 'color_exterior_code'=> 'COLOR', + 'color_interior_code'=> 'COLOR', + ]; + + /** 创建 SKU(含指纹与字典校验) */ + public static function create(array $p, int $userId=0, int $corpId=0): VehicleSku + { + // SkuValidator::validateForCreate($p); + + // 必填字典:由映射表控制(一般 BRAND/SERIES/FUEL) + $requiredDicts = MappingService::requiredDicts(); + foreach (self::$SKU_FIELD_TO_DICT as $field => $dict) { + if (!MappingService::enabled($dict)) continue; + $isRequired = in_array($dict, $requiredDicts, true); + if ($isRequired) { + DictService::ensureExists($dict, $p[$field] ?? null, $field); + } + } + + // 填充系统字段 + $now = time(); + $p['sku_code'] = $p['sku_code'] ?? ('SKU'.date('Ymd').'-'.substr(bin2hex(random_bytes(3)),0,6)); + $p['corp_id'] = $p['corp_id'] ?? $corpId; + $p['created_by'] = $p['created_by'] ?? $userId; + $p['updated_by'] = $p['updated_by'] ?? $userId; + $p['created_at'] = $p['created_at'] ?? $now; + $p['updated_at'] = $p['updated_at'] ?? $now; + + // 指纹 + $p['fingerprint'] = Fingerprint::make($p /*, $scopeCorp=true/false*/); + + return DB::connection()->transaction(function () use ($p) { + $sku = new VehicleSku($p); + $sku->save(); + return $sku; + }); + } + + /** 更新 SKU(部分字段;指纹随事实变动而刷新) */ + public static function update(int $id, array $p, int $userId=0): VehicleSku + { + SkuValidator::validateForUpdate($p); + + $sku = VehicleSku::findOrFail($id); + + // 若传入了必填 dict 的 code,需要确认字典存在 + $requiredDicts = MappingService::requiredDicts(); + foreach (self::$SKU_FIELD_TO_DICT as $field => $dict) { + if (!array_key_exists($field, $p)) continue; + if (!MappingService::enabled($dict)) continue; + $isRequired = in_array($dict, $requiredDicts, true); + if ($isRequired) { + DictService::ensureExists($dict, $p[$field], $field); + } else { + // 可选的:若传了 code 则也校验存在 + if (($p[$field] ?? null) !== null && $p[$field] !== '') { + $exists = \app\model\DictTerm::where('dict',$dict)->where('code',$p[$field])->exists(); + if (!$exists) throw new \InvalidArgumentException("字典不存在:{$dict}.{$p[$field]}"); + } + } + } + + // 刷新系统字段 + $p['updated_by'] = $userId; + $p['updated_at'] = time(); + + // 赋值保存 + $sku->fill($p); + + // 只要影响到指纹的事实字段变动,就重算指纹 + $fingerFields = [ + 'brand_code','series_code','model_year','fuel_type_code', + 'drive_type_code','transmission_code','body_type_code', + 'engine_disp_l','engine_turbo','battery_kwh','range_km', + 'range_std_code','emission_std_code', + 'corp_id' + ]; + foreach ($fingerFields as $f) { + if (array_key_exists($f, $p)) { + $sku->fingerprint = Fingerprint::make($sku->toArray()); + break; + } + } + + $sku->save(); + return $sku; + } + + /** 详情(带 i18n label 与标准标题) */ + public static function detail(int $id, string $locale='zh-CN'): array + { + $sku = VehicleSku::findOrFail($id); + $data = $sku->toArray(); + + $labelOf = function(string $field) use ($data, $locale) { + $dict = self::$SKU_FIELD_TO_DICT[$field] ?? null; + if (!$dict) return null; + + $code = $data[$field] ?? null; + // 若是品牌/车系,允许以中文兜底 + $fallback = null; + if ($field === 'brand_code') $fallback = $data['brand_name'] ?? null; + if ($field === 'series_code') $fallback = $data['series_name'] ?? null; + + // 版型/等级/特别版允许中文兜底 + if ($field === 'trim_code') $fallback = $data['trim'] ?? null; + if ($field === 'trim_level_code') $fallback = $data['trim_level'] ?? null; + if ($field === 'edition_code') $fallback = $data['edition_name'] ?? null; + + // 颜色兜底中文 + if ($field === 'color_exterior_code') $fallback = $data['color_exterior_text'] ?? null; + if ($field === 'color_interior_code') $fallback = $data['color_interior_text'] ?? null; + + return \app\service\DictService::label($dict, $code, $locale, $fallback); + }; + + $i18n = [ + 'brand_label' => $labelOf('brand_code'), + 'series_label' => $labelOf('series_code'), + 'model_label' => \app\service\DictService::label('MODEL', $data['model_code'] ?? null, $locale, $data['model_name'] ?? null), + 'trim_label' => $labelOf('trim_code'), + 'trim_level_label' => $labelOf('trim_level_code'), + 'edition_label' => $labelOf('edition_code'), + 'fuel_label' => $labelOf('fuel_type_code'), + 'drive_label' => $labelOf('drive_type_code'), + 'trans_label' => $labelOf('transmission_code'), + 'motor_layout_label' => $labelOf('motor_layout_code'), + 'body_label' => $labelOf('body_type_code'), + 'range_std_label' => $labelOf('range_std_code'), + 'emission_label' => $labelOf('emission_std_code'), + 'color_exterior_label' => $labelOf('color_exterior_code'), + 'color_interior_label' => $labelOf('color_interior_code'), + 'listing_title' => TitleBuilder::build($data, $locale), + ]; + + return ['sku'=>$data, 'i18n'=>$i18n]; + } + + /** 列表查询(支持基础筛选 & i18n 列) */ + public static function list(array $q, string $locale='zh-CN', int $page=1, int $pageSize=20): array + { + $builder = VehicleSku::query(); + + // 常用筛选 + if (!empty($q['brand_code'])) $builder->where('brand_code', $q['brand_code']); + if (!empty($q['series_code'])) $builder->where('series_code', $q['series_code']); + if (!empty($q['fuel_type_code'])) $builder->where('fuel_type_code', $q['fuel_type_code']); + if (!empty($q['model_year'])) $builder->where('model_year', (int)$q['model_year']); + if (isset($q['is_active'])) $builder->where('is_active', (int)$q['is_active']); + + $builder->orderByDesc('created_at'); + + $total = (clone $builder)->count(); + $rows = $builder->forPage($page, $pageSize)->get()->toArray(); + + // 附加 i18n 字段(轻量) + foreach ($rows as &$row) { + $row['brand_label'] = DictService::label('BRAND', $row['brand_code'] ?? null, $locale, $row['brand_name'] ?? null); + $row['series_label'] = DictService::label('SERIES', $row['series_code'] ?? null, $locale, $row['series_name'] ?? null); + $row['fuel_label'] = DictService::label('FUEL', $row['fuel_type_code'] ?? null, $locale, null); + $row['listing_title']= TitleBuilder::build($row, $locale); + } + + return ['total'=>$total, 'list'=>$rows, 'page'=>$page, 'page_size'=>$pageSize]; + } +} \ No newline at end of file diff --git a/app/common/service/VehVehicleUnitService.php b/app/common/service/VehVehicleUnitService.php new file mode 100644 index 0000000..4af3ea4 --- /dev/null +++ b/app/common/service/VehVehicleUnitService.php @@ -0,0 +1,31 @@ +validator; + } +} \ No newline at end of file diff --git a/app/common/utils/scaffold/Generate.php b/app/common/utils/scaffold/Generate.php index eb82025..2ed10a0 100644 --- a/app/common/utils/scaffold/Generate.php +++ b/app/common/utils/scaffold/Generate.php @@ -156,11 +156,11 @@ class Generate protected function deleteFileIfExists($path) { if (file_exists($path)) { - "文件: {$path} 已存在,核对后手动删除。\n"; - return; + // "文件: {$path} 已存在,核对后手动删除。\n"; + // return; - // unlink($path); - // echo "删除已存在文件: {$path} \n"; + unlink($path); + echo "删除已存在文件: {$path} \n"; } } diff --git a/app/common/validate/VehDictI18nValidate.php b/app/common/validate/VehDictI18nValidate.php new file mode 100644 index 0000000..cb65500 --- /dev/null +++ b/app/common/validate/VehDictI18nValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/common/validate/VehDictMappingValidate.php b/app/common/validate/VehDictMappingValidate.php new file mode 100644 index 0000000..1b4bc7a --- /dev/null +++ b/app/common/validate/VehDictMappingValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/common/validate/VehVehicleListingValidate.php b/app/common/validate/VehVehicleListingValidate.php new file mode 100644 index 0000000..ffa9c83 --- /dev/null +++ b/app/common/validate/VehVehicleListingValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/common/validate/VehVehicleSkuValidate.php b/app/common/validate/VehVehicleSkuValidate.php new file mode 100644 index 0000000..7305e71 --- /dev/null +++ b/app/common/validate/VehVehicleSkuValidate.php @@ -0,0 +1,128 @@ + 'max:40|alphaNum', + 'fingerprint' => 'length:64|alphaNum', + 'brand_code' => 'require|max:32', + 'brand_name' => 'require|max:64', + 'series_code' => 'max:64', + 'series_name' => 'max:128', + 'model_code' => 'max:64', + 'model_name' => 'require|max:128', + 'model_year' => 'require|number|between:1900,2100', + 'trim_code' => 'max:64', + 'trim_level_code' => 'max:64', + 'edition_code' => 'max:64', + 'trim' => 'max:128', + 'trim_level' => 'max:64', + 'edition_name' => 'max:64', + 'body_type_code' => 'max:32', + 'drive_type_code' => 'max:16', + 'motor_layout_code' => 'max:32', + 'fuel_type_code' => 'max:16', + 'engine_disp_l' => 'float|between:0,99.9', + 'engine_turbo' => 'in:0,1', + 'engine_power_kw' => 'number|between:0,2000', + 'battery_kwh' => 'float|between:0,999.99', + 'transmission_code' => 'max:16', + 'gears' => 'number|between:1,20', + 'range_km' => 'number|between:0,2000', + 'range_std_code' => 'max:16', + 'doors' => 'number|between:2,10', + 'seats' => 'number|between:1,20', + 'weight_kg' => 'number|between:0,10000', + 'dimensions_mm' => 'array', + 'color_exterior_code'=> 'max:32', + 'color_exterior_text'=> 'max:64', + 'color_interior_code'=> 'max:32', + 'color_interior_text'=> 'max:64', + 'options_official' => 'array', + 'features' => 'array', + 'emission_std_code' => 'max:16', + 'origin_country' => 'alpha|length:2', + 'currency' => 'require|alpha|length:3', + 'msrp' => 'float|between:0,9999999999.99', + 'is_active' => 'in:0,1', + 'corp_id' => 'require|number', + 'remark' => 'max:255', + 'created_by' => 'number', + 'updated_by' => 'number', + 'created_at' => 'require|number', + 'updated_at' => 'require|number', + ]; + + /** + * 字段别名 + */ + protected $field = [ + 'sku_code' => 'SKU编码', + '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' => '排量(L)', + 'engine_turbo' => '是否涡轮', + 'engine_power_kw' => '最大功率(kW)', + 'battery_kwh' => '电池容量(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' => '公司ID', + 'remark' => '备注', + 'created_by' => '创建人', + 'updated_by' => '更新人', + 'created_at' => '创建时间', + 'updated_at' => '更新时间', + ]; + + /** + * 场景定义 + */ + protected $scene = [ + 'store' => [ + 'brand_code', 'brand_name', 'model_year','currency' + ], + 'update' => [ + 'id', 'brand_code','brand_name', 'model_year','currency' + ], + ]; +} \ No newline at end of file diff --git a/app/common/validate/VehVehicleUnitValidate.php b/app/common/validate/VehVehicleUnitValidate.php new file mode 100644 index 0000000..11f91a9 --- /dev/null +++ b/app/common/validate/VehVehicleUnitValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/controller/IndexController.php b/app/controller/IndexController.php index ba87d98..3871c59 100644 --- a/app/controller/IndexController.php +++ b/app/controller/IndexController.php @@ -78,25 +78,25 @@ class IndexController $options = [ 'controller' => [ 'module' => 'common', - 'name' => 'message_notifications' + 'name' => 'veh_dict_i18n' ], 'model' => [ 'module' => 'common', - 'name' => 'message_notifications' + 'name' => 'veh_dict_i18n' ], 'service' => [ 'module' => 'common', - 'name' => 'message_notifications' + 'name' => 'veh_dict_i18n' ], 'logic' => [ 'module' => 'common', - 'name' => 'message_notifications' + 'name' => 'veh_dict_i18n' ], 'validate' => [ 'module' => 'common', - 'name' => 'message_notifications' + 'name' => 'veh_dict_i18n' ], - 'table' => 'message_notifications' + 'table' => 'veh_dict_i18n' ]; $result = (new Generate($options))->doGenerate(); diff --git a/app/functions.php b/app/functions.php index d9bb411..db8cfe7 100644 --- a/app/functions.php +++ b/app/functions.php @@ -304,6 +304,29 @@ if (!function_exists('generateMsgId')) { // 随机部分(3位) $rand = mt_rand(100, 999); + // 拼接:日期时间 + 微秒 + 随机数 + return $date . $micro . $rand; + } +} + +/** + * 生成sku_code + * 格式:年月日时分秒 + 微秒 + 随机数 + * 例如:20250820175530987654321 + * + * @return string + */ +if (!function_exists('skuCode')) { + function skuCode(): string + { + // 时间部分(精确到微秒) + $time = microtime(true); + $micro = sprintf("%06d", ($time - floor($time)) * 1e6); + $date = date('YmdHis', (int)$time); + + // 随机部分(3位) + $rand = mt_rand(100, 999); + // 拼接:日期时间 + 微秒 + 随机数 return $date . $micro . $rand; } diff --git a/app/queue/redis/InquiryToCrm.php b/app/queue/redis/InquiryToCrm.php deleted file mode 100644 index 4d7ca3e..0000000 --- a/app/queue/redis/InquiryToCrm.php +++ /dev/null @@ -1,110 +0,0 @@ -where('id', $data['id'])->where('sync_crm', 0)->first(); - - if ($info) { - // 获取用户信息 - $recipient = 0; - $corpId = 0; - $uinfo = null; - if ($info->recipient) { - $recipient = $info->recipient; - $uinfo = User::where('id', $info->recipient)->first(); - if ($uinfo) { - $corpId = $uinfo->corp_id; - } - } - // 同步客户数据 - $crmCustomer = new CrmCustomer(); - $crmCustomer->sn = $info->order_no; - $crmCustomer->corp_abbr = $corpId; - $crmCustomer->user_id = $recipient; - $crmCustomer->type = 1; - $crmCustomer->channel_id = $info->channel; - $crmCustomer->country_id = $info->country; - $crmCustomer->magnitude = $info->quality; - $crmCustomer->email = $info->email; - $crmCustomer->phone = $info->tel; - $crmCustomer->WhatsApp = $info->whatsapp; - $crmCustomer->contacts = $info->contacts; - $crmCustomer->desc_collect = $info->content; - $crmCustomer->enter_user_id = $info->enter_user_id; - $crmCustomer->obtain = time(); - // 修改同步CRM状态 - $info->sync_crm = 1; - // 保存 - if (!$crmCustomer->save() || !$info->save()) { - throw new BusinessException('同步失败~'); - } - - // // 发送邮件消息 - // if ($uinfo && $uinfo->email) { - // $options = [ - // 'id' => $info->id, - // 'email' => $uinfo->email - // ]; - // // Log::info('发送邮件消息:' . json_encode($options)); - // \Webman\RedisQueue\Redis::send('email-message', $options); - // } - - $wsdata = [ - 'sender_id' => 0, - 'sender_type' => 'system', - 'title' => '有新的客户线索,' . $info->order_no . ',请及时处理', - 'content' => '有新的客户线索,' . $info->order_no . ',请及时处理', - 'scene' => 1, - 'recipient' => [$info->recipient] - ]; - // 异步推送 - \Webman\RedisQueue\Redis::send('notice', $wsdata); - } - } catch (\Throwable $th) { - throw $th; - } - } - - // 消费失败回调 - /* - $package = [ - 'id' => 1357277951, // 消息ID - 'time' => 1709170510, // 消息时间 - 'delay' => 0, // 延迟时间 - 'attempts' => 2, // 消费次数 - 'queue' => 'send-mail', // 队列名 - 'data' => ['to' => 'tom@gmail.com', 'content' => 'hello'], // 消息内容 - 'max_attempts' => 5, // 最大重试次数 - 'error' => '错误信息' // 错误信息 - ] - */ - public function onConsumeFailure(\Throwable $e, $package) - { - echo "consume failure\n"; - echo $e->getMessage() . "\n"; - // 无需反序列化 - var_export($package); - } -} \ No newline at end of file diff --git a/app/queue/redis/InquiryToCrmUpdate.php b/app/queue/redis/InquiryToCrmUpdate.php deleted file mode 100644 index c4e6d02..0000000 --- a/app/queue/redis/InquiryToCrmUpdate.php +++ /dev/null @@ -1,110 +0,0 @@ -where('id', $data['id'])->first(); - - $corpId = 0; - if ($info->recipient) { - $uinfo = User::where('id', $info->recipient)->where('status', 1)->first(); - if ($uinfo) { - $corpId = $uinfo->corp_id; - } - } - - // 获取客户信息 - $crmCustomerInfo = CrmCustomer::where('sn', $info->order_no)->first(); - if ($crmCustomerInfo) { - $udata = [ - 'corp_abbr' => $corpId, - 'user_id' => $info->recipient, - 'channel_id' => $info->channel, - 'country_id' => $info->country, - 'magnitude' => $info->quality, - 'email' => $info->email, - 'phone' => $info->tel, - 'WhatsApp' => $info->whatsapp, - 'contacts' => $info->contacts, - 'desc_collect' => $info->content, - ]; - $crmCustomerInfo->update($udata); - } else { - // 同步客户数据 - $crmCustomer = new CrmCustomer(); - $crmCustomer->sn = $info->order_no; - $crmCustomer->corp_abbr = $corpId; - $crmCustomer->user_id = $info->recipient; - $crmCustomer->type = 1; - $crmCustomer->channel_id = $info->channel; - $crmCustomer->country_id = $info->country; - $crmCustomer->magnitude = $info->quality; - $crmCustomer->email = $info->email; - $crmCustomer->phone = $info->tel; - $crmCustomer->WhatsApp = $info->whatsapp; - $crmCustomer->contacts = $info->contacts; - $crmCustomer->desc_collect = $info->content; - $crmCustomer->enter_user_id = $info->enter_user_id; - // 修改同步CRM状态 - $info->sync_crm = 1; - // 保存 - if (!$crmCustomer->save() || !$info->save()) { - throw new BusinessException('同步失败~'); - } - - // 发送邮件消息 - if ($uinfo && $uinfo->email) { - $options = [ - 'id' => $info->id, - 'email' => $uinfo->email - ]; - \Webman\RedisQueue\Redis::send('email-message', $options); - } - } - } catch (\Throwable $th) { - throw $th; - } - } - - // 消费失败回调 - /* - $package = [ - 'id' => 1357277951, // 消息ID - 'time' => 1709170510, // 消息时间 - 'delay' => 0, // 延迟时间 - 'attempts' => 2, // 消费次数 - 'queue' => 'send-mail', // 队列名 - 'data' => ['to' => 'tom@gmail.com', 'content' => 'hello'], // 消息内容 - 'max_attempts' => 5, // 最大重试次数 - 'error' => '错误信息' // 错误信息 - ] - */ - public function onConsumeFailure(\Throwable $e, $package) - { - echo "consume failure\n"; - echo $e->getMessage() . "\n"; - // 无需反序列化 - var_export($package); - } -} \ No newline at end of file diff --git a/config/route.php b/config/route.php index 6393f2d..b2cb132 100644 --- a/config/route.php +++ b/config/route.php @@ -182,6 +182,18 @@ Route::group('/admin', function () { Route::any('/allocation', [app\crmAdmin\controller\CustomerController::class, 'allocation']); }); }); + + // 车辆 + Route::group('/vehicle', function () { + // 基础信息 + Route::group('/sku', function () { + Route::any('/list', [app\common\controller\VehVehicleSkuController::class, 'index']); + Route::any('/show', [app\common\controller\VehVehicleSkuController::class, 'show']); + Route::any('/store', [app\common\controller\VehVehicleSkuController::class, 'store']); + Route::any('/update', [app\common\controller\VehVehicleSkuController::class, 'update']); + Route::any('/destroy', [app\common\controller\VehVehicleSkuController::class, 'destroy']); + }); + }); });