Merge branch 'develop'

This commit is contained in:
mzl
2025-09-16 10:53:57 +08:00
parent b399f85707
commit 62c32d25ff
210 changed files with 15388 additions and 95 deletions
+103
View File
@@ -3,6 +3,15 @@
namespace app\common\controller;
use app\admin\service\RoleService;
use app\common\service\BrandSeriesService;
use app\common\service\BrandService;
use app\common\service\SysAccountService;
use app\common\service\SysCurrencyService;
use app\common\service\SysDictItemService;
use app\common\service\SysDictService;
use app\common\service\SysPortService;
use app\common\service\VehColorService;
use app\common\service\VehModificationService;
use support\Request;
use app\common\service\CorpService;
use app\common\service\CorpFeishuAppService;
@@ -76,6 +85,100 @@ class PresetController extends BaseController
}
}
public function currency()
{
try {
$result = (new SysCurrencyService)->getList();
return $this->success($result, '获取货币列表');
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
public function brand()
{
try {
$result = (new BrandService)->getList();
return $this->success($result, '获取品牌列表');
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
public function series(Request $request)
{
try {
$params = $request->all();
$result = (new BrandSeriesService)->getList($params);
return $this->success($result, '获取车系列表');
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
public function account()
{
try {
$result = (new SysAccountService)->getList();
return $this->success($result, '获取账号列表');
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
public function dict()
{
try {
$result = (new SysDictService)->getList();
return $this->success($result, '获取字典列表');
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
public function dictItem(Request $request)
{
try {
$params = $request->all();
$result = (new SysDictItemService)->getList($params);
return $this->success($result, '获取字典项列表');
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
public function port(Request $request)
{
try {
$params = $request->all();
$result = (new SysPortService)->getList($params);
return $this->success($result, '获取港口列表');
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
public function color(Request $request)
{
try {
$params = $request->all();
$result = (new VehColorService)->getList($params);
return $this->success($result, '获取颜色列表');
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
public function modification()
{
try {
$result = (new VehModificationService)->getList();
return $this->success($result, '获取货币列表');
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
public function cacheClear()
{
+174 -3
View File
@@ -3,8 +3,12 @@
namespace app\common\controller;
use app\common\service\OssService;
use app\common\service\UploadedFilesService;
use app\common\service\UploadService;
use app\common\traits\ResponseTrait;
use support\Log;
use support\Request;
use support\exception\BusinessException;
use Shopwwi\WebmanFilesystem\Facade\Storage;
class UploadController
@@ -22,15 +26,41 @@ class UploadController
$scene = $request->input('scene', 'image');
$file = $request->file('file');
if ($file && $file->isValid()) {
$pathUrl = '/uploads/'.$scene.'/'.date('Ymd').'/'.md5($file->getUploadName() . time()).'.'.$file->getUploadExtension();
$fileName = md5($file->getUploadName() . time()).'.'.$file->getUploadExtension();
$pathUrl = '/uploads/'.$scene.'/'.date('Ymd').'/'.$fileName;
$fullPathUrl = public_path() . $pathUrl;
$file->move($fullPathUrl);
$data = [
'domain' => getenv('MAIN_DOMAIN'),
'file_url' => $pathUrl,
'full_url' => getenv('MAIN_DOMAIN') . $pathUrl,
'scene' => $scene,
];
// 记录文件信息到数据库
try {
$uploadedFilesService = new UploadedFilesService();
$partition = $scene;
$category = $request->input('category', 'general');
$fileInfo = [
'file_name' => basename($fileName),
'file_path' => $pathUrl,
'file_size' => $file->getSize(),
'file_type' => $file->getUploadMimeType(),
'original_name' => $file->getUploadName(),
];
$recordResult = $uploadedFilesService->recordFileInfo($fileInfo, $request, $partition, $category);
$data['file_id'] = $recordResult['id'];
$data['partition'] = $partition;
$data['category'] = $category;
} catch (\Throwable $e) {
// 记录失败不影响上传结果,只记录日志
Log::error('记录文件信息失败: ' . $e->getMessage());
}
return $this->success($data, 'upload success');
}
return $this->error('file not found');
@@ -43,15 +73,22 @@ class UploadController
public function uploadFile(Request $request)
{
try {
$scene = $request->input('scene');
$file = $request->file('file');
if (!$file || !$file->isValid()) {
return $this->error('上传失败');
}
if (empty($scene)) {
// return $this->error('上传场景不能为空');
$sceneUrl = '';
}else{
$sceneUrl = $scene . '/';
}
$originalName = $file->getUploadName();
$result = Storage::adapter('oss')
->path('vehicle_album/'.date('Ymd'))
->path('x_server/' . $sceneUrl . date('Ymd'))
->size(1024*1024*5)
->upload($file);
@@ -63,6 +100,11 @@ class UploadController
'file_url' => $ossservice->generatePrivateUrl($result->file_name)
];
// 记录文件信息到数据库
$category = $request->input('category', 'general');
$res = $this->recordFileInfo($result,$request,$file,$scene,$category);
$data = array_merge($data, $res);
return $this->success($data);
}catch (\Throwable $e){
return $this->error($e->getMessage());
@@ -73,14 +115,21 @@ class UploadController
public function uploadImage(Request $request)
{
try {
$scene = $request->input('scene');
$file = $request->file('image');
if (!$file || !$file->isValid()) {
return $this->error('上传失败');
}
if (empty($scene)) {
// return $this->error('上传场景不能为空');
$sceneUrl = '';
}else{
$sceneUrl = $scene . '/';
}
$result = Storage::adapter('oss')
->path('vehicle_album/'.date('Ymd'))
->path('x_server/' . $sceneUrl . date('Ymd'))
->size(1024*1024*5)
->extYes(['image/jpeg','image/png'])
->extNo(['image/gif'])
@@ -94,6 +143,11 @@ class UploadController
'url' => $ossservice->generatePrivateUrl($result->file_name)
];
// 记录文件信息到数据库
$category = $request->input('category', 'general');
$res = $this->recordFileInfo($result,$request,$file,$scene,$category);
$data = array_merge($data, $res);
return json(['errno' => 0, 'data' => $data]);
// return $this->success($data);
} catch (\Throwable $e) {
@@ -102,6 +156,123 @@ class UploadController
}
}
public function recordFileInfo($result, $request, $file, $scene, $category)
{
try {
$uploadedFilesService = new UploadedFilesService();
$partition = $scene;
$fileInfo = [
'file_name' => basename($result->file_name),
'file_path' => $result->file_name, // OSS存储路径
'file_size' => $file->getSize(),
'file_type' => $file->getUploadMimeType(),
'original_name' => $file->getUploadName(),
];
$recordResult = $uploadedFilesService->recordFileInfo($fileInfo, $request, $partition, $category);
$data = [];
$data['file_id'] = $recordResult['id'];
$data['partition'] = $partition;
$data['category'] = $category;
return $data;
} catch (\Throwable $e) {
// 记录失败不影响上传结果,只记录日志
Log::error('记录文件信息失败: ' . $e->getMessage());
return [];
}
}
/**
* 获取文件信息
*
* @param Request $request
* @return \support\Response
*/
public function getFileInfo(Request $request)
{
try {
$fileId = $request->input('id');
if (!$fileId) {
return $this->error('文件ID不能为空');
}
$uploadedFilesService = new UploadedFilesService();
$fileInfo = $uploadedFilesService->getFileInfo($fileId);
if (!$fileInfo) {
return $this->error('文件不存在');
}
return $this->success($fileInfo);
} catch (\Throwable $e) {
return $this->error('获取文件信息失败: ' . $e->getMessage());
}
}
/**
* 删除文件
*
* @param Request $request
* @return \support\Response
*/
public function deleteFile(Request $request)
{
try {
$fileId = $request->input('id');
$path = $request->input('path');
$uploadedFilesService = new UploadedFilesService();
if(!empty($path) && empty($fileId)){
$where = ['file_path' => $path];
$file = $uploadedFilesService->getFile($where);
$fileId = $file->id;
}elseif(empty($path) && !empty($fileId)){
$where = ['id' => $fileId];
$file = $uploadedFilesService->getFile($where);
$path = $file->file_path;
}
if (!$fileId) {
return $this->error('文件ID与路径不能为空');
}
// 删除 OSS 上的文件
$ossService = new OssService();
$ossService->deleteFile($path);
$result = $uploadedFilesService->deleteFile($fileId);
if ($result) {
return $this->success([], '文件删除成功');
} else {
return $this->error('文件删除失败');
}
} catch (\Throwable $e) {
return $this->error('文件删除失败: ' . $e->getMessage());
}
}
/**
* 获取分区配置
*
* @param Request $request
* @return \support\Response
*/
public function getPartitionConfig(Request $request)
{
try {
$uploadedFilesService = new UploadedFilesService();
$config = $uploadedFilesService->getPartitionConfig();
return $this->success($config);
} catch (\Throwable $e) {
return $this->error('获取分区配置失败: ' . $e->getMessage());
}
}
public function proxy(Request $request)
{
$filePath = $request->get('file'); // 原OSS文件路径
@@ -0,0 +1,56 @@
<?php
namespace app\common\controller;
use app\common\model\User;
use app\common\traits\crud\CrudControllerTrait;
use app\admin\service\VehColorService;
class VehColorController extends BaseController
{
use CrudControllerTrait;
protected $service;
public function __construct()
{
$this->service = new VehColorService();
}
/**
* 列表页数据格式化
*
* @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']);
if (isset($data['created_by']) && $data['created_by'])
$data['created_by_name'] = User::where('id', $data['created_by'])->value('name');
if (isset($data['updated_by']) && $data['updated_by'])
$data['updated_by_name'] = User::where('id', $data['updated_by'])->value('name');
return $data;
}
/**
* 数据格式化
*
* @param array $data
* @return array
*/
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']);
if (isset($data['created_by']) && $data['created_by'])
$data['created_by_name'] = User::where('id', $data['created_by'])->value('name');
if (isset($data['updated_by']) && $data['updated_by'])
$data['updated_by_name'] = User::where('id', $data['updated_by'])->value('name');
return $data;
}
}
@@ -0,0 +1,33 @@
<?php
namespace app\common\controller;
use app\common\controller\BaseController;
use app\common\traits\crud\CrudControllerTrait;
use app\common\service\VehDictI18nService;
class VehDictI18nController extends BaseController
{
use CrudControllerTrait;
protected $service;
public function __construct()
{
$this->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;
// }
}
@@ -0,0 +1,33 @@
<?php
namespace app\common\controller;
use app\common\controller\BaseController;
use app\common\traits\crud\CrudControllerTrait;
use app\common\service\VehDictMappingService;
class VehDictMappingController extends BaseController
{
use CrudControllerTrait;
protected $service;
public function __construct()
{
$this->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;
// }
}
@@ -0,0 +1,33 @@
<?php
namespace app\common\controller;
use app\common\controller\BaseController;
use app\common\traits\crud\CrudControllerTrait;
use app\common\service\VehMoneyValueService;
class VehMoneyValueController extends BaseController
{
use CrudControllerTrait;
protected $service;
public function __construct()
{
$this->service = new VehMoneyValueService();
}
/**
* 列表页数据格式化
*
* @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;
// }
}
@@ -0,0 +1,33 @@
<?php
namespace app\common\controller;
use app\common\controller\BaseController;
use app\common\traits\crud\CrudControllerTrait;
use app\common\service\VehVehicleListingService;
class VehVehicleListingController extends BaseController
{
use CrudControllerTrait;
protected $service;
public function __construct()
{
$this->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;
// }
}
@@ -0,0 +1,204 @@
<?php
namespace app\common\controller;
use app\common\controller\BaseController;
use app\common\traits\crud\CrudControllerTrait;
use app\common\service\VehVehicleSkuService;
use support\exception\BusinessException;
use support\Request;
class VehVehicleSkuController extends BaseController
{
use CrudControllerTrait;
protected $service;
public function __construct()
{
$this->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 fields_desc(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',
];
}
}
@@ -0,0 +1,33 @@
<?php
namespace app\common\controller;
use app\common\controller\BaseController;
use app\common\traits\crud\CrudControllerTrait;
use app\common\service\VehVehicleUnitService;
class VehVehicleUnitController extends BaseController
{
use CrudControllerTrait;
protected $service;
public function __construct()
{
$this->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;
// }
}
+22
View File
@@ -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']);
}
}
+22
View File
@@ -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']);
}
}
+262
View File
@@ -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;
}
}
+101
View File
@@ -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';
}
}
+233
View File
@@ -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;
}
+17
View File
@@ -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;
}
+17
View File
@@ -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;
}
+22
View File
@@ -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']);
}
}
+21
View File
@@ -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']);
}
}
+22
View File
@@ -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']);
}
}
+27
View File
@@ -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']);
}
}
+22
View File
@@ -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']);
}
}
+169
View File
@@ -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;
}
}
+22
View File
@@ -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']);
}
}
+17
View File
@@ -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;
}
+17
View File
@@ -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;
}
+22
View File
@@ -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']);
}
}
+17
View File
@@ -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;
}
+55
View File
@@ -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);
}
}
+17
View File
@@ -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;
}
+49
View File
@@ -0,0 +1,49 @@
<?php
namespace app\common\model;
use support\Model;
class Brand extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'brand';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
protected $dateFormat = 'U';
/**
* 允许自动填充模型字段
*/
protected $fillable = ['id', 'brand_code', 'brand_name', 'logo', 'country', 'description', 'status', 'sort', 'created_at', 'updated_at'];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
}
+49
View File
@@ -0,0 +1,49 @@
<?php
namespace app\common\model;
use support\Model;
class BrandSeries extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'brand_series';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
protected $dateFormat = 'U';
/**
* 允许自动填充模型字段
*/
protected $fillable = ['id', 'brand_id', 'series_code', 'series_name', 'description', 'status', 'sort', 'created_at', 'updated_at'];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
}
+58
View File
@@ -0,0 +1,58 @@
<?php
namespace app\common\model;
use support\Model;
class StockOrderAccounts extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'stock_order_accounts';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
protected $dateFormat = 'U';
/**
* 允许自动填充模型字段
*/
protected $fillable = ['id', 'order_id', 'seq_no', 'remit_country_id', 'remit_country_name_snapshot', 'remit_date', 'currency_code', 'currency_name_snapshot', 'account_id', 'account_name_snapshot', 'remitter_name', 'amount', 'created_at', 'updated_at'];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
/**
* 关联主订单
*/
public function order()
{
return $this->belongsTo(StockOrders::class, 'order_id', 'id');
}
}
@@ -0,0 +1,58 @@
<?php
namespace app\common\model;
use support\Model;
class StockOrderItemOptions extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'stock_order_item_options';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
protected $dateFormat = 'U';
/**
* 允许自动填充模型字段
*/
protected $fillable = ['id', 'order_item_id', 'option_id', 'option_name_snapshot', 'option_price', 'created_at', 'updated_at'];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
/**
* 关联订单项
*/
public function item()
{
return $this->belongsTo(StockOrderItems::class, 'order_item_id', 'id');
}
}
+99
View File
@@ -0,0 +1,99 @@
<?php
namespace app\common\model;
use support\Model;
use app\common\traits\HasMoneyJournal;
class StockOrderItems extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'stock_order_items';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
protected $dateFormat = 'U';
/**
* 允许自动填充模型字段
*/
protected $fillable = [
'id', 'order_id', 'product_code', 'sku_id', 'sku_snapshot',
'model_snapshot', 'quantity', 'color_interior_code', 'color_interior_snapshot',
'color_exterior_code', 'color_exterior_snapshot', 'remark', 'vin', 'created_at', 'updated_at',
'mileage', 'unique_id'
];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
/* ========= 关系 ========= */
use HasMoneyJournal;
// 实现抽象方法
protected static function moneyObjectScene(): string
{
return 'stock_order_items';
}
// 便捷访问器(最新一条的原币金额)
public function getUnitPriceAmountAttribute() { return $this->moneyLatest('unit_price')?->amount; }
public function getHandlingFeeAmountAttribute() { return $this->moneyLatest('handling_fee')?->amount; }
public function getFobTransportFeeAmountAttribute() { return $this->moneyLatest('fob_transport_fee')?->amount; }
public function getPortMiscFeeAmountAttribute() { return $this->moneyLatest('port_misc_fee')?->amount; }
public function getRetrofitFeeAmountAttribute() { return $this->moneyLatest('retrofit_fee')?->amount; }
public function getTotalPriceAmountAttribute() { return $this->moneyLatest('total_price')?->amount; }
/**
* 关联主订单
*/
public function order()
{
return $this->belongsTo(StockOrders::class, 'order_id', 'id');
}
/**
* 关联订单项选项
*/
public function options()
{
return $this->hasMany(StockOrderItemOptions::class, 'order_item_id', 'id');
}
/**
* 关联货币价值信息(一个订单项可以有多个货币价值记录)
*/
public function moneyValues()
{
return $this->hasMany(VehMoneyValue::class, 'object_id', 'id')
->where('object_scene', 'stock_order_items'); // 场景标识区分不同对象类型
}
}
+98
View File
@@ -0,0 +1,98 @@
<?php
namespace app\common\model;
use support\Model;
use app\common\traits\HasMoneyJournal;
class StockOrders extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'stock_orders';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
protected $dateFormat = 'U';
/**
* 允许自动填充模型字段
*/
protected $fillable = [
'id', 'order_no', 'pi_no', 'biz_date', 'salesperson_id', 'salesperson_snapshot',
'clearance_country_id', 'clearance_country_snapshot', 'ali_order_no',
'customer_source_id', 'customer_source_snapshot', 'customer_id', 'customer_snapshot',
'customer_country_id', 'customer_country_snapshot', 'declarant_name', 'eta_ship_date',
'shipping_method_id', 'shipping_method_snapshot', 'cif_total_amount', 'domestic_port_id', 'domestic_port_snapshot',
'trade_term_id', 'trade_term_snapshot', 'clearance_port_id', 'clearance_port_snapshot',
'latest_arrival_date', 'contract_exchange_rate', 'settlement_currency', 'status',
'remark', 'created_at', 'updated_at', 'deleted_at',
'object_id', 'contract', 'is_use', 'is_final', 'approval_status', 'file_url', 'pick_up_date', 'collect_all_date', 'profit_total'
];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
use HasMoneyJournal;
// 实现抽象方法
protected static function moneyObjectScene(): string
{
return 'stock_orders';
}
/**
* 关联订单项
*/
public function items()
{
return $this->hasMany(StockOrderItems::class, 'order_id', 'id');
}
/**
* 关联订单账户信息
*/
public function accounts()
{
return $this->hasMany(StockOrderAccounts::class, 'order_id', 'id');
}
/**
* 关联货币价值信息(一个订单可以有多个货币价值记录)
*/
public function moneyValues()
{
return $this->hasMany(VehMoneyValue::class, 'object_id', 'id')->where('object_scene', 'stock_orders');
}
public function salesperson()
{
return $this->hasOne(User::class, 'id', 'salesperson_id');
}
}
+49
View File
@@ -0,0 +1,49 @@
<?php
namespace app\common\model;
use support\Model;
class SysAccount extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'sys_account';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
protected $dateFormat = 'U';
/**
* 允许自动填充模型字段
*/
protected $fillable = ['id', 'corp_id', 'currency_id', 'account_name', 'account_number', 'account_holder', 'bank_name', 'bank_branch', 'daily_limit', 'single_limit', 'description', 'status', 'sort', 'created_at', 'updated_at'];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
}
+49
View File
@@ -0,0 +1,49 @@
<?php
namespace app\common\model;
use support\Model;
class SysCurrency extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'sys_currency';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
protected $dateFormat = 'U';
/**
* 允许自动填充模型字段
*/
protected $fillable = ['id', 'country_id', 'currency_code', 'currency_name', 'currency_symbol', 'exchange_rate', 'base_currency', 'float_percent_up', 'float_percent_down', 'status', 'last_updated', 'created_at', 'updated_at'];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
}
@@ -0,0 +1,49 @@
<?php
namespace app\common\model;
use support\Model;
class SysCurrencyTranslation extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'sys_currency_translation';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
protected $dateFormat = 'U';
/**
* 允许自动填充模型字段
*/
protected $fillable = ['id', 'currency_id', 'locale', 'currency_name', 'created_at', 'updated_at'];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
}
+93
View File
@@ -0,0 +1,93 @@
<?php
namespace app\common\model;
use Illuminate\Database\Eloquent\SoftDeletes;
use support\Model;
class SysDict extends Model
{
use SoftDeletes;
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'sys_dict';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
protected $dateFormat = 'U';
/**
* 允许自动填充模型字段
*/
protected $fillable = ['id', 'category_id', 'dict_name', 'dict_code', 'description', 'status', 'created_at', 'updated_at', 'deleted_at'];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
// 定义与 SysDictItem 的一对多关系
public function sysDictItem()
{
return $this->hasMany(SysDictItem::class, 'dict_id', 'id');
}
public function category()
{
return $this->belongsTo(SysDictCategory::class, 'category_id', 'id');
}
/**
* 一个字典有多个字典项
*/
public function items()
{
return $this->hasMany(SysDictItem::class, 'dict_id', 'id');
}
/**
* 通过 dict_code 快速获取所有 item_value
*/
public function scopeGetItemValuesByCode($query, $dictCode)
{
return $query->where('dict_code', $dictCode)
->with('items') // 预加载关联项
->first()?->items->pluck('item_value'); // 返回 item_value 数组
}
/**
* 通过 dict_code 获取完整字典项(含 label + value)
*/
public function scopeGetItemsByCode($query, $dictCode)
{
return $query->where('dict_code', $dictCode)
->with('items')
->first()?->items;
}
}
+58
View File
@@ -0,0 +1,58 @@
<?php
namespace app\common\model;
use support\Model;
class SysDictCategory extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'sys_dict_category';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
protected $dateFormat = 'U';
/**
* 允许自动填充模型字段
*/
protected $fillable = ['id', 'category_name', 'sort', 'status', 'created_at', 'updated_at'];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
public function dicts()
{
return $this->hasMany(SysDict::class, 'category_id', 'id')
->where('status', 1)
->whereNull('deleted_at')
->orderBy('id', 'asc');
}
}
+67
View File
@@ -0,0 +1,67 @@
<?php
namespace app\common\model;
use Illuminate\Database\Eloquent\SoftDeletes;
use support\Model;
class SysDictItem extends Model
{
use SoftDeletes;
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'sys_dict_item';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
protected $dateFormat = 'U';
/**
* 允许自动填充模型字段
*/
protected $fillable = ['id', 'dict_id', 'item_label', 'item_value', 'sort', 'status', 'remark', 'created_at', 'updated_at', 'deleted_at'];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
// 定义反向多对一关联
public function sysDict()
{
return $this->belongsTo(SysDict::class, 'dict_id', 'id');
}
/**
* 字典项属于一个字典
*/
public function dict()
{
return $this->belongsTo(SysDict::class, 'dict_id', 'id');
}
}
@@ -0,0 +1,49 @@
<?php
namespace app\common\model;
use support\Model;
class SysDictItemTranslation extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'sys_dict_translation';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
protected $dateFormat = 'U';
/**
* 允许自动填充模型字段
*/
protected $fillable = ['id', 'dict_id', 'remark', 'locale', 'dict_name', 'description', 'created_at', 'updated_at'];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
}
+49
View File
@@ -0,0 +1,49 @@
<?php
namespace app\common\model;
use support\Model;
class SysDictTranslation extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'sys_dict_translation';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
protected $dateFormat = 'U';
/**
* 允许自动填充模型字段
*/
protected $fillable = ['id', 'dict_id', 'locale', 'dict_name', 'description', 'created_at', 'updated_at'];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
}
+49
View File
@@ -0,0 +1,49 @@
<?php
namespace app\common\model;
use support\Model;
class SysPort extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'sys_port';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
protected $dateFormat = 'U';
/**
* 允许自动填充模型字段
*/
protected $fillable = ['id', 'port_code', 'port_name', 'country_id', 'lat', 'lng', 'description', 'status', 'sort', 'created_at', 'updated_at'];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
}
+49
View File
@@ -0,0 +1,49 @@
<?php
namespace app\common\model;
use support\Model;
class SysPortTranslation extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'sys_port_translation';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
protected $dateFormat = 'U';
/**
* 允许自动填充模型字段
*/
protected $fillable = ['id', 'port_id', 'locale', 'port_name', 'description', 'created_at', 'updated_at'];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
}
+63
View File
@@ -0,0 +1,63 @@
<?php
namespace app\common\model;
use support\Model;
class UploadedFiles extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'uploaded_files';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
protected $dateFormat = 'U';
/**
* 允许自动填充模型字段
*/
protected $fillable = [
'id',
'file_name',
'file_path',
'file_size',
'file_type',
'category',
'original_name',
'extension',
'partition',
'upload_ip',
'user_agent',
'created_at',
'updated_at'
];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
}
+49
View File
@@ -0,0 +1,49 @@
<?php
namespace app\common\model;
use support\Model;
class VehColor extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'veh_color';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
protected $dateFormat = 'U';
/**
* 允许自动填充模型字段
*/
protected $fillable = ['id', 'color_code', 'color_text', 'type', 'hex_code', 'rgb_code', 'status', 'sort', 'created_at', 'updated_by', 'created_at', 'updated_at'];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
}
+49
View File
@@ -0,0 +1,49 @@
<?php
namespace app\common\model;
use support\Model;
class VehColorTranslation extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'veh_color_translation';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
protected $dateFormat = 'U';
/**
* 允许自动填充模型字段
*/
protected $fillable = ['id', 'color_id', 'locale', 'color_text', 'created_at', 'updated_at'];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
}
+49
View File
@@ -0,0 +1,49 @@
<?php
namespace app\common\model;
use support\Model;
class VehDictI18n extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'veh_dict_i18n';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
protected $dateFormat = 'U';
/**
* 允许自动填充模型字段
*/
protected $fillable = ['id', 'dict', 'code', 'locale', 'label', 'status', 'remark', 'created_by', 'updated_by', 'created_at', 'updated_at'];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
}
+52
View File
@@ -0,0 +1,52 @@
<?php
namespace app\common\model;
use support\Model;
class VehDictMapping extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'veh_dict_mapping';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
protected $dateFormat = 'U';
protected $casts = [
'required' => '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'
];
}
+49
View File
@@ -0,0 +1,49 @@
<?php
namespace app\common\model;
use support\Model;
class VehModification extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'veh_modification';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
protected $dateFormat = 'U';
/**
* 允许自动填充模型字段
*/
protected $fillable = ['id', 'code', 'name', 'type', 'description', 'price', 'status', 'sort', 'created_at', 'updated_at'];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
}
+87
View File
@@ -0,0 +1,87 @@
<?php
namespace app\common\model;
use support\Model;
class VehMoneyValue extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'veh_money_value';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
protected $dateFormat = 'U';
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
protected $fillable = [
'object_scene','object_id','field','currency','amount','exchange_rate',
'remark','created_at','updated_at'
];
protected $casts = [
'amount' => 'string',
'exchange_rate' => 'string',
];
/** 按本行汇率换到结算币;无汇率返回 null */
public function amountInSettlement(int $scale = 2): ?string
{
if ($this->exchange_rate === null) return null;
return (string) bcmul((string)$this->amount, (string)$this->exchange_rate, $scale);
}
/**
* 关联订单
*/
public function order()
{
return $this->belongsTo(StockOrders::class, 'object_id', 'id');
}
/**
* 关联订单项(一个货币价值记录可能关联到订单项)
*/
public function orderItem()
{
return $this->belongsTo(StockOrderItems::class, 'object_id', 'id');
}
/**
* 限定查询特定对象类型的货币价值记录
*/
public function scopeOfScene($query, $scene)
{
return $query->where('object_scene', $scene);
}
}
+49
View File
@@ -0,0 +1,49 @@
<?php
namespace app\common\model;
use support\Model;
class VehMoneyValue_bak extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'veh_money_value';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
protected $dateFormat = 'U';
/**
* 允许自动填充模型字段
*/
protected $fillable = ['id', 'currency', 'amount', 'exchange_rate', 'remark', 'created_at', 'updated_at'];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
}
+49
View File
@@ -0,0 +1,49 @@
<?php
namespace app\common\model;
use support\Model;
class VehVehicleListing extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'veh_vehicle_listing';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
protected $dateFormat = 'U';
/**
* 允许自动填充模型字段
*/
protected $fillable = ['id', 'sku_id', 'unit_id', 'title', 'sub_title', 'lang', 'market', 'channel', 'currency', 'list_price', 'promo_price', 'vat_included', 'lead_time_days', 'cover_image', 'media', 'attrs_override', 'status', 'visible_from', 'visible_to', 'corp_id', 'sort', 'remark', 'created_by', 'updated_by', 'created_at', 'updated_at'];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
}
+81
View File
@@ -0,0 +1,81 @@
<?php
namespace app\common\model;
use support\Model;
class VehVehicleSku extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'veh_vehicle_sku';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
protected $dateFormat = 'U';
protected $casts = [
'options_official' => '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'
];
}
+49
View File
@@ -0,0 +1,49 @@
<?php
namespace app\common\model;
use support\Model;
class VehVehicleUnit extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'veh_vehicle_unit';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
protected $dateFormat = 'U';
/**
* 允许自动填充模型字段
*/
protected $fillable = ['id', 'sku_id', 'vin', 'engine_no', 'license_plate', 'first_reg_date', 'ownership_count', 'odometer', 'odometer_unit', 'condition_grade', 'accident_record', 'paint_count', 'owner_type', 'import_status', 'location_id', 'location_text', 'images', 'docs', 'stock_status', 'cost_total', 'currency', 'is_active', 'corp_id', 'remark', 'created_by', 'updated_by', 'created_at', 'updated_at'];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
}
+34
View File
@@ -0,0 +1,34 @@
<?php
namespace app\common\service;
use app\admin\validate\BrandSeriesValidate;
use app\common\logic\BrandSeriesLogic;
use app\common\traits\crud\CrudServiceTrait;
use app\common\model\BrandSeries;
use support\exception\BusinessException;
use support\Request;
class BrandSeriesService
{
protected $logic;
public function __construct()
{
$this->logic = (new BrandSeriesLogic());
}
/**
* 获取系统list
*/
public function getList($params)
{
if (empty($params['brand_id']))
throw new BusinessException('品牌id不能为空');
$where = [];
if(isset($params['brand_id']) && $params['brand_id'])
$where[] = ['brand_id', '=', $params['brand_id']];
return $this->logic->getList($where);
}
}
+24
View File
@@ -0,0 +1,24 @@
<?php
namespace app\common\service;
use app\common\logic\BrandLogic;
class BrandService
{
protected $logic;
public function __construct()
{
$this->logic = (new BrandLogic());
}
/**
* 获取系统list
*/
public function getList()
{
return $this->logic->getList();
}
}
@@ -273,4 +273,14 @@ class MessageNotificationsService
{
return User::where('status', 1)->pluck('id')->toArray();
}
public function getUserEmailsById(array $con = []): array
{
$user = User::where('status', 1);
if (count($con) > 0) {
return $user->whereIn('id', $con)->where('email', '!=', '')->pluck('email')->toArray();
}
return $user->where('email', '!=', '')->pluck('email')->toArray();
}
}
+20
View File
@@ -56,4 +56,24 @@ class OssService
// 生成 OSS 私有 URL(有效期为 3600 秒)
return $this->ossClient->signUrl($this->bucket, $objectKey, $expireTime);
}
/**
* 删除 OSS 上的文件
* @param string $objectKey OSS上存储的对象名称
* @return bool 删除是否成功
*/
public function deleteFile($objectKey)
{
try {
// 删除 OSS 上的文件
$this->ossClient->deleteObject($this->bucket, $objectKey);
return true;
} catch (OssException $e) {
// 错误处理
throw new \RuntimeException('OSS Delete Error: ' . $e->getMessage());
} catch (\Throwable $th) {
// 错误处理
throw new BusinessException('Delete Error: ' . $th->getMessage());
}
}
}
@@ -0,0 +1,31 @@
<?php
namespace app\common\service;
use app\common\validate\StockOrderAccountsValidate;
use app\common\traits\crud\CrudServiceTrait;
use app\common\model\StockOrderAccounts;
class StockOrderAccountsService
{
use CrudServiceTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = StockOrderAccounts::class;
/**
* 验证器
*/
protected $validator = StockOrderAccountsValidate::class;
/**
* 获取验证器
*/
public function getValidator()
{
return new $this->validator;
}
}
@@ -0,0 +1,31 @@
<?php
namespace app\common\service;
use app\common\validate\StockOrderItemOptionsValidate;
use app\common\traits\crud\CrudServiceTrait;
use app\common\model\StockOrderItemOptions;
class StockOrderItemOptionsService
{
use CrudServiceTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = StockOrderItemOptions::class;
/**
* 验证器
*/
protected $validator = StockOrderItemOptionsValidate::class;
/**
* 获取验证器
*/
public function getValidator()
{
return new $this->validator;
}
}
@@ -0,0 +1,31 @@
<?php
namespace app\common\service;
use app\common\validate\StockOrderItemsValidate;
use app\common\traits\crud\CrudServiceTrait;
use app\common\model\StockOrderItems;
class StockOrderItemsService
{
use CrudServiceTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = StockOrderItems::class;
/**
* 验证器
*/
protected $validator = StockOrderItemsValidate::class;
/**
* 获取验证器
*/
public function getValidator()
{
return new $this->validator;
}
}
+742
View File
@@ -0,0 +1,742 @@
<?php
namespace app\common\service;
use app\common\model\StockOrderAccounts;
use app\common\model\StockOrderItemOptions;
use app\common\model\StockOrderItems;
use app\common\validate\StockOrdersValidate;
use app\common\traits\crud\CrudServiceTrait;
use app\common\model\StockOrders;
use app\common\model\SysCurrency;
use support\Request;
use app\common\model\User;
use support\Db;
use support\Model;
use app\admin\model\ApprovalInstances;
use app\admin\model\ApprovalRecords;
class StockOrdersService
{
use CrudServiceTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = StockOrders::class;
/**
* 验证器
*/
protected $validator = StockOrdersValidate::class;
/**
* 获取验证器
*/
public function getValidator()
{
return new $this->validator;
}
protected function buildQueryConfig(Request $request, string $scene = 'index'): array
{
$p = $request->all();
$config = [];
$config['model'] = $this->modelClass;
switch ($scene) {
case 'index':
// ====== ====== 筛选 ====== ======
// 业务id
if (isset($p['salesperson_id']) && $p['salesperson_id']) {
$config['where']['salesperson_id'] = $p['salesperson_id'];
}
// 初、终版
$config['where']['is_final'] = $p['is_final'] ?? 0;
// 关键字
if (isset($p['keyword']) && $p['keyword']) {
$k = '%' . $p['keyword'] . '%';
foreach (['pi_no', 'order_no', 'customer_snapshot'] as $col) {
$config['orWhere'][] = [$col, 'like', $k];
}
}
$config['order']['id'] = 'desc';
// $config['where']['salesperson_id'] = $request->user->uid;
break;
case 'print':
case 'approval':
$config['where']['id'] = $p['id'];
break;
case 'detail':
case 'show':
$config['where']['id'] = $p['id'];
$config['with'][] = 'items';
$config['with'][] = 'items.options';
$config['with'][] = 'items.moneyValues';
$config['with'][] = 'accounts';
$config['with'][] = 'moneyValues';
$config['with'][] = 'salesperson';
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)
{
$userId = $request->user->uid;
$data['order_no'] = generateStockNo();
$data['biz_date'] = strtotime($data['biz_date']);
$data['eta_ship_date'] = strtotime($data['eta_ship_date']);
$data['latest_arrival_date'] = strtotime($data['latest_arrival_date']);
$data['salesperson_id'] = $userId;
$data['salesperson_snapshot'] = User::where('id', $userId)->value('name') ?? '';
$data['approval_status'] = $data['approval_status'] == 1 ? 1 : 0;
// if ($data['is_final'] == 1) {
$data['pick_up_date'] = $data['pick_up_date'] ? strtotime($data['pick_up_date']) : 0;
$data['collect_all_date'] = $data['collect_all_date'] ? strtotime($data['collect_all_date']) : 0;
// }
return $data;
}
protected function afterServiceStore($res)
{
// 处理原单
if ($res->is_final == 1 && $res->object_id > 0) {
$order = StockOrders::find($res->object_id);
$order->is_use = 1;
$order->object_id = $res->id;
if (!$order->save()) {
flg('备货单【'.$order->order_no.'】创建终版成功,但更新原单失败');
}
}
// 启动审批
if ($res->approval_status == 1) {
$this->startApproval($res);
}
return $res;
}
/**
* 启动审批
*/
public function startApproval($item)
{
// 启动审批
$appro = new \app\common\utils\approval\service\ApprovalFlowService();
$appro->start('stock_orders', 'stock_orders', $item->id, $item->toArray(), $item->salesperson_id);
// 异步生成pdf文件
\Webman\RedisQueue\Redis::send('stock-order-pdf', ['id'=>$item->id]);
}
/**
* 创建PDF文件
*/
public function createdPdfFile($id)
{
$config = [];
$config['model'] = $this->modelClass;
$config['where']['id'] = $id;
$config['with'][] = 'items';
$config['with'][] = 'items.options';
$config['with'][] = 'items.moneyValues';
$config['with'][] = 'accounts';
$config['with'][] = 'moneyValues';
$config['with'][] = 'salesperson';
$item = $this->logicShow($config);
$pdfData = $this->pdfData($item);
// 渲染模板为最终HTML
$tplPath = app_path('common/utils/pdf/template/stockOrder.stub');
$html = \app\common\utils\pdf\service\PdfHelper::render($tplPath, ['data' => $pdfData]);
// 生成PDF
$driver = new \app\common\utils\pdf\service\MpdfDriver();
$pdfBin = $driver->htmlToPdf($html);
// 存储
$dir = public_path('pdf').'/';
$fileName = ($pdfData['order_no'] ?? date('YmdHis')) . '.pdf';
// 本地保存
\app\common\utils\pdf\service\PdfStorage::saveBinary(
$pdfBin,
$dir,
$fileName
);
// 上传云端
$param = [
'path' => $dir . $fileName,
'scene' => 'stock_orders',
'category' => 'pdf',
'original_name' => $fileName
];
$up = new \app\common\service\UploadService();
$res = $up->uploadLocalFile($param);
@unlink($dir . $fileName);
// 更新文件地址
$item->file_url = $res['file_name'];
$item->save();
return $res;
}
public function pdfData($item)
{
$vehList = [];
$index = 0;
$totalProfit = 0; // 总利润
// $priceFields = [
// 'official_price', // '官方指导价',
// 'discount_price', // '优惠',
// 'listing_price', // '二手挂牌价价',
// 'reduction_price', // '优惠',
// 'handling_fee', // '手续费',
// 'retrofit_fee', // '改装费',
// 'fob_transport_fee', // 'FOB运输费',
// 'port_misc_fee', // '港杂',
// 'ocean_freight', // '海运费',
// 'unit_price', // '单价',
// 'total_price', // '总价',
// // 'profit', // '利润'
// ];
foreach ($item->items as $k => $v) {
$currencyInfo = SysCurrency::where('currency_code', $item->settlement_currency)->first(['currency_symbol', 'currency_name'])->toArray();
// 存在则数量加1
if (count($vehList) > 0 && isset($vehList[$v->unique_id])) {
$vehList[$v->unique_id]['num'] += 1;
$vehList[$v->unique_id]['total_price']['currency_info'] = $currencyInfo['currency_symbol'].number_format($vehList[$v->unique_id]['total_price']['amount']*$vehList[$v->unique_id]['num'], 2, '.', ',');
$vehList[$v->unique_id]['profit']['currency_info'] = $currencyInfo['currency_symbol'].number_format($vehList[$v->unique_id]['profit']['amount']*$vehList[$v->unique_id]['num'], 2, '.', ',');
continue;
}
$index += 1;
// 处理金额 (每次循环重新初始化)
$mv = [];
foreach ($v->moneyValues as $money) {
$mv[$money->field] = [
'currency' => $money->currency,
'amount' => $money->amount,
'rate' => $money->exchange_rate,
'currency_info' => SysCurrency::where('currency_code', $money->currency)->value('currency_symbol').number_format($money->amount, 2, '.', ',')
];
}
// 处理改装 (每次循环重新初始化)
$mfy = '';
foreach ($v->options as $my) {
$mfy .= $my->option_name_snapshot.' ';
}
// 计算单项利润并累加到总利润
$itemProfit = $this->calculateProfit($mv, $item);
$totalProfit += $itemProfit;
// 处理结果集
$vehListItem = [
'index' => $index,
'model_snapshot' => $v->model_snapshot,
'color_interior_snapshot' => $v->color_interior_snapshot,
'color_exterior_snapshot' => $v->color_exterior_snapshot,
'num' => 1,
'modify' => $mfy,
'mileage' => $v->mileage
];
$vehList[$v->unique_id] = $vehListItem + $mv;
}
$mvs = [];
foreach ($item->moneyValues as $moneys) {
$mvs[$moneys->field] = [
'currency' => $moneys->currency,
'amount' => $moneys->amount,
'rate' => $moneys->exchange_rate,
'currency_info' => SysCurrency::where('currency_code', $moneys->currency)->value('currency_symbol').number_format($moneys->amount, 2, '.', ','),
];
}
$accList = [];
foreach ($item->accounts as $k => $v) {
$accList[] = [
'index' => '收款'.($k + 1),
'remit_country_name_snapshot' => $v->remit_country_name_snapshot,
'remit_date' => date('Y-m-d', $v->remit_date),
'currency_name_snapshot' => $v->currency_name_snapshot,
'account_name_snapshot' => $v->account_name_snapshot,
'remitter_name' => $v->remitter_name,
'amount' => $v->amount,
];
}
$veh = [
'order_no' => $item->order_no,
'salesperson_snapshot' => $item->salesperson_snapshot,
'sign_img_url' => (!empty($item->salesperson->sign_img)) ?
(new \app\common\service\OssService)->generatePrivateUrl($item->salesperson->sign_img) : '',
'created_at' => date('Y-m-d', strtotime($item->created_at)),
'pi_no' => $item->pi_no,
'biz_date' => date('Y-m-d', $item->biz_date),
'clearance_country_snapshot' => $item->clearance_country_snapshot,
'customer_source_snapshot' => $item->customer_source_snapshot,
'customer_snapshot' => $item->customer_snapshot,
'customer_country_snapshot' => $item->customer_country_snapshot,
// 订单项
'veh_list' => array_values($vehList), // 重新索引数组
'contract_exchange_rate' => $item->contract_exchange_rate,
'cif_total_amount' => $mvs['cif_total_amount'],
'profit_total' => $mvs['profit_total'],
'declarant_name' => $item->declarant_name,
'eta_ship_date' => date('Y-m-d', $item->eta_ship_date),
'shipping_method_snapshot' => $item->shipping_method_snapshot,
'domestic_port_snapshot' => $item->domestic_port_snapshot,
'trade_term_snapshot' => $item->trade_term_snapshot,
'clearance_port_snapshot' => $item->clearance_port_snapshot,
'latest_arrival_date' => date('Y-m-d', $item->latest_arrival_date),
'pick_up_date' => date('Y-m-d', $item->pick_up_date),
'collect_all_date' => date('Y-m-d', $item->collect_all_date),
'is_final' => $item->is_final,
// 账户
'account_list' => $accList,
'acc_index' => count($accList)+1,
'currency_symbol' => $item->settlement_currency,
'sign_imgs' => $this->queryAppro($item->id),
'contract_url' => (!empty($item->contract)) ? (new \app\common\service\OssService)->generatePrivateUrl($item->contract) : ''
];
return $veh;
}
public function queryAppro($bizId)
{
$instances = ApprovalInstances::where('biz_id', $bizId)->where('biz_type', 'stock_orders')->first();
if (!$instances) {
return [];
}
$records = ApprovalRecords::where('instance_id', $instances->id)->where('action', 'approve')->orderBy('step_index', 'asc')->pluck('approver_id')->toArray();
if (count($records) == 0) {
return [];
}
$signImgs = [];
foreach ($records as $k => $v) {
$signImg = User::where('id', $v)->value('sign_img');
$signImgs[$k] = (!empty($signImg))?(new \app\common\service\OssService)->generatePrivateUrl($signImg):'';
}
return $signImgs;
}
/**
* 打印数据
*/
public function servicePrint(Request $request)
{
$config = $this->buildQueryConfig($request, 'show');
$item = $this->logicShow($config);
return $this->postProcess('Print', $item);
}
public function serviceDetail(Request $request)
{
$config = $this->buildQueryConfig($request, 'detail');
$item = $this->logicShow($config);
$pdfData = $this->pdfData($item);
return $this->postProcess('Detail', $pdfData);
}
/**
* 审批
*/
public function serviceApproval(Request $request)
{
$config = $this->buildQueryConfig($request, 'approval');
$this->logicUpdateBy($config, [
'approval_status' => 1
]);
$item = $this->logicShow($config);
$this->startApproval($item);
return $this->postProcess('Approval', $item);
}
/**
* 创建
*
* @param Request $request
* @param array $data
* @return mixed
*/
public function serviceStore(Request $request, array $data = [])
{
// 关联表较多,增加事务处理
return Db::transaction(function () use ($request, $data) {
$data = $this->preProcess('Store', $data, $request);
$dl = $this->getDataLimitFromRequest($request);
$created = $this->logicCreate($data, $dl);
// 处理车辆和账户信息
$this->processOrderItems($created, $data);
return $this->postProcess('Store', $created);
});
}
protected function onlyFillableData(Model|string $model, array $data, array $extraWhitelist = []): array
{
// 兼容传类名或实例
$m = \is_string($model) ? new $model : $model;
// 1) 优先使用 fillable
$keys = $m->getFillable();
// 2) 若没配置 fillable(常见:用 guarded),回退到完整表字段,避免误伤
if (empty($keys)) {
$keys = $m->getConnection()
->getSchemaBuilder()
->getColumnListing($m->getTable());
}
// 3) 允许额外白名单(如虚拟字段、JSON映射字段)
if ($extraWhitelist) {
$keys = array_merge($keys, $extraWhitelist);
}
return array_intersect_key($data, array_flip($keys));
}
/**
* 更新前处理
*
* @param array $data
* @param Request $request
* @return array
*/
public function beforeServiceUpdate($data, Request $request)
{
$data['biz_date'] = strtotime($data['biz_date']);
$data['eta_ship_date'] = strtotime($data['eta_ship_date']);
$data['latest_arrival_date'] = strtotime($data['latest_arrival_date']);
$data['approval_status'] = $data['approval_status'] == 1 ? 1 : 0;
$data['pick_up_date'] = $data['pick_up_date'] ? strtotime($data['pick_up_date']) : 0;
$data['collect_all_date'] = $data['collect_all_date'] ? strtotime($data['collect_all_date']) : 0;
return $data;
}
/**
* 更新后处理
*
* @param mixed $res
* @return mixed
*/
protected function afterServiceUpdate($res)
{
// 启动审批
if ($res->approval_status == 1) {
$this->startApproval($res);
}
return $res;
}
/**
* 更新
*
* @param Request $request
* @param array $data
* @return mixed
*/
public function serviceUpdate(Request $request, array $data = [])
{
// 关联表较多,增加事务处理
return Db::transaction(function () use ($request, $data) {
$data = $this->preProcess('Update', $data, $request);
$config = $this->buildQueryConfig($request, 'update');
// 过滤掉非库字段
$dbData = $this->onlyFillableData($config['model'], $data);
// 更新主表
$updated = $this->logicUpdateBy($config, $dbData);
// 获取更新后的数据
$order = $this->logicShow($config);
// 处理车辆和账户信息
$this->processOrderItems($order, $data, true);
return $this->postProcess('Update', $order);
});
}
/**
* 处理订单项(车辆和账户)
* @param StockOrders $order 订单对象
* @param array $data 数据
* @param bool $isUpdate 是否为更新操作
*/
private function processOrderItems($order, array $data, bool $isUpdate = false): void
{
// 如果是更新操作,先删除原有的关联数据
if ($isUpdate) {
$this->clearExistingOrderItems($order->id);
}
// 处理车辆项
$this->processVehicleItems($order, $data['veh_list'] ?? []);
// 处理账户项
$this->processAccountItems($order, $data['account_list'] ?? []);
}
/**
* (更新时)清除已存在的订单项
* @param int $orderId 订单ID
*/
private function clearExistingOrderItems(int $orderId): void
{
// 删除车辆改装项
StockOrderItemOptions::whereHas('item', function ($query) use ($orderId) {
$query->where('order_id', $orderId);
})->delete();
// 删除车辆项及其相关金额记录
$orderItems = StockOrderItems::where('order_id', $orderId)->get();
foreach ($orderItems as $item) {
// 删除车辆项的金额记录
Db::table('veh_money_value')
->where('object_scene', 'stock_order_items')
->where('object_id', $item->id)
->delete();
}
// 删除车辆项
StockOrderItems::where('order_id', $orderId)->delete();
// 删除订单本身的金额记录
Db::table('veh_money_value')
->where('object_scene', 'stock_orders')
->where('object_id', $orderId)
->delete();
// 删除账户项
StockOrderAccounts::where('order_id', $orderId)->delete();
}
/**
* 处理车辆项
*/
private function processVehicleItems($order, array $vehList): void
{
$cifTotalAmount = 0.00;
// $cifTotalAmountRate = 0;
// $cifTotalAmountCurrency = 'CNY';
$profitTotalAmount = 0.00;
foreach ($vehList as $veh) {
$veh['order_id'] = $order->id;
$veh['unique_id'] = $this->generateVehicleUniqueId($veh);
$item = StockOrderItems::create($veh);
// 处理金额信息
$this->processVehiclePrices($item, $veh);
// 计算利润
$profit = $this->calculateProfit($veh, $order);
$item->addMoney('profit', $order->settlement_currency, $profit, $order->contract_exchange_rate, '利润');
// 累计到岸价
$cifTotalAmount += $veh['total_price']['amount'] ?? 0;
$profitTotalAmount += $profit;
// if (isset($veh['retrofit_fee']['rate'])) {
// $cifTotalAmountRate = $veh['retrofit_fee']['rate'];
// }
// if (isset($veh['retrofit_fee']['currency'])) {
// $cifTotalAmountCurrency = $veh['retrofit_fee']['currency'];
// }
// 处理改装项
$this->processModificationItems($item->id, $veh['modify_list'] ?? []);
}
// 保存合计到岸价
$order->addMoney('cif_total_amount', $order->settlement_currency, $cifTotalAmount, $order->contract_exchange_rate, '合计到岸价');
// 利润
$order->addMoney('profit_total', $order->settlement_currency, $profitTotalAmount, $order->contract_exchange_rate, '总利润');
}
/**
* 生成车辆唯一标识
*/
private function generateVehicleUniqueId(array $veh): string
{
$uniqueIdStr = "";
$uniqueIdStr .= $veh['sku_id'] ?? '';
$uniqueIdStr .= $veh['model_snapshot'] ?? '';
$uniqueIdStr .= $veh['color_interior_code'] ?? '';
$uniqueIdStr .= $veh['color_exterior_code'] ?? '';
$uniqueIdStr .= $veh['mileage'] ?? '';
$uniqueIdStr .= json_encode($veh['modify_list'] ?? []);
return md5($uniqueIdStr);
}
/**
* 处理车辆价格信息
*/
private function processVehiclePrices($item, array $veh): void
{
// 基础费用
$priceFields = [
'unit_price' => '单价',
'total_price' => '总价',
'handling_fee' => '手续费',
'fob_transport_fee' => 'FOB运输费',
'port_misc_fee' => '港杂',
'retrofit_fee' => '改装费',
'ocean_freight' => '海运费'
];
foreach ($priceFields as $field => $description) {
if (!empty($veh[$field]['amount'])) {
$item->addMoney(
$field,
$veh[$field]['currency'],
$veh[$field]['amount'],
$veh[$field]['rate'],
$description
);
}
}
// 根据里程处理新车或二手车价格
if (!empty($veh['mileage']) && $veh['mileage'] > 0) {
// 二手车
if (!empty($veh['listing_price']['amount'])) {
$item->addMoney(
'listing_price',
$veh['listing_price']['currency'],
$veh['listing_price']['amount'],
$veh['listing_price']['rate'],
'二手挂牌价'
);
}
if (!empty($veh['reduction_price']['amount'])) {
$item->addMoney(
'reduction_price',
$veh['reduction_price']['currency'],
$veh['reduction_price']['amount'],
$veh['reduction_price']['rate'],
'二手减价'
);
}
} else {
// 新车
if (!empty($veh['official_price']['amount'])) {
$item->addMoney(
'official_price',
$veh['official_price']['currency'],
$veh['official_price']['amount'],
$veh['official_price']['rate'],
'官方指导价'
);
}
if (!empty($veh['discount_price']['amount'])) {
$item->addMoney(
'discount_price',
$veh['discount_price']['currency'],
$veh['discount_price']['amount'],
$veh['discount_price']['rate'],
'优惠价'
);
}
}
}
/**
* 计算利润
*/
private function calculateProfit(array $veh, $order): float
{
$unitPrice = (float)($veh['unit_price']['amount'] ?? 0);
if (!empty($veh['mileage']) && $veh['mileage'] > 0) {
// 二手车
$ttPrice = ($veh['listing_price']['amount'] ?? 0) / ($order->contract_exchange_rate ?: 1);
$ddPrice = $veh['reduction_price']['amount'] ?? 0;
} else {
// 新车
$ttPrice = ($veh['official_price']['amount'] ?? 0) / ($order->contract_exchange_rate ?: 1);
$ddPrice = $veh['discount_price']['amount'] ?? 0;
}
$handlingFee = (float)($veh['handling_fee']['amount'] ?? 0);
$fobTransportFee = (float)($veh['fob_transport_fee']['amount'] ?? 0);
$portMiscFee = (float)($veh['port_misc_fee']['amount'] ?? 0);
$retrofitFee = (float)($veh['retrofit_fee']['amount'] ?? 0);
$oceanFreight = (float)($veh['ocean_freight']['amount'] ?? 0);
return $unitPrice - $ttPrice - $ddPrice - $handlingFee - $fobTransportFee - $portMiscFee - $retrofitFee - $oceanFreight;
}
/**
* 处理改装项
*/
private function processModificationItems(int $orderItemId, array $modifyList): void
{
if (empty($modifyList)) {
return;
}
foreach ($modifyList as $modify) {
$modify['order_item_id'] = $orderItemId;
StockOrderItemOptions::create($modify);
}
}
/**
* 处理账户项
*/
private function processAccountItems($order, array $accountList): void
{
if (empty($accountList)) {
return;
}
foreach ($accountList as $account) {
$account['order_id'] = $order->id;
if (!empty($account['remit_date'])) {
$account['remit_date'] = strtotime($account['remit_date']);
}
StockOrderAccounts::create($account);
}
}
}
@@ -0,0 +1,435 @@
<?php
namespace app\common\service;
use app\common\model\StockOrderAccounts;
use app\common\model\StockOrderItemOptions;
use app\common\model\StockOrderItems;
use app\common\validate\StockOrdersValidate;
use app\common\traits\crud\CrudServiceTrait;
use app\common\model\StockOrders;
use app\common\model\SysCurrency;
use support\Request;
use app\common\model\User;
use support\Db;
class StockOrdersService_bak
{
use CrudServiceTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = StockOrders::class;
/**
* 验证器
*/
protected $validator = StockOrdersValidate::class;
/**
* 获取验证器
*/
public function getValidator()
{
return new $this->validator;
}
protected function buildQueryConfig(Request $request, string $scene = 'index'): array
{
$p = $request->all();
$config = [];
$config['model'] = $this->modelClass;
switch ($scene) {
case 'index':
// ====== ====== 筛选 ====== ======
// 业务id
if (isset($p['salesperson_id']) && $p['salesperson_id']) {
$config['where']['salesperson_id'] = $p['salesperson_id'];
}
// 初、终版
$config['where']['is_final'] = $p['is_final'] ?? 0;
// 关键字
if (isset($p['keyword']) && $p['keyword']) {
$k = '%' . $p['keyword'] . '%';
foreach (['pi_no', 'order_no', 'customer_snapshot'] as $col) {
$config['orWhere'][] = [$col, 'like', $k];
}
}
$config['order']['id'] = 'desc';
// $config['dataLimit']['type'] = 'relation';
// $config['dataLimit']['userId'] = $request->user->uid;
// $config['dataLimit']['userField'] = 'salesperson_id';
break;
case 'print':
$config['where']['id'] = $p['id'];
break;
case 'show':
$config['where']['id'] = $p['id'];
$config['with'][] = 'items';
$config['with'][] = 'items.options';
$config['with'][] = 'items.moneyValues';
$config['with'][] = 'accounts';
$config['with'][] = 'moneyValues';
$config['with'][] = 'salesperson';
break;
case 'store':
break;
case 'update':
$config['where']['id'] = $p['id'];
break;
case 'destroy':
$config['where']['id'] = $p['id'];
break;
}
return $config;
}
public function serviceStore(Request $request, array $data = [])
{
// 关联表较多,增加事务处理
return Db::transaction(function () use ($request, $data) {
$data = $this->preProcess('Store', $data, $request);
$dl = $this->getDataLimitFromRequest($request);
$created = $this->logicCreate($data, $dl); // 表1
// 车辆
$cifTotalAmount = 0;
$cifTotalAmountRate = 0;
$cifTotalAmountCurrency = 'CNY';
// $profit = 0;
foreach ($data['veh_list'] as $veh) {
$veh['order_id'] = $created->id;
// 唯一标识
$uniqueIdStr = "";
$uniqueIdStr .= $veh['sku_id'];
$uniqueIdStr .= $veh['model_snapshot'];
$uniqueIdStr .= $veh['color_interior_code'];
$uniqueIdStr .= $veh['color_exterior_code'];
$uniqueIdStr .= $veh['mileage'];
$uniqueIdStr .= json($veh['modify_list']);
$veh['unique_id'] = md5($uniqueIdStr);
$item = StockOrderItems::create($veh); // 表2
// 金额 // 表3
// $item->addMoney('official_price', $veh['official_price']['currency'], $veh['official_price']['amount'], $veh['official_price']['rate'], '官方指导价');
// $item->addMoney('discount_price', $veh['discount_price']['currency'], $veh['discount_price']['amount'], $veh['discount_price']['rate'], '优惠价');
if ($veh['unit_price']['amount']) {
$item->addMoney('unit_price', $veh['unit_price']['currency'], $veh['unit_price']['amount'], $veh['unit_price']['rate'], '单价');
}
if ($veh['total_price']['amount']) {
$item->addMoney('total_price', $veh['total_price']['currency'], $veh['total_price']['amount'], $veh['total_price']['rate'], '总价');
}
if ($veh['handling_fee']['amount']) {
$item->addMoney('handling_fee', $veh['handling_fee']['currency'], $veh['handling_fee']['amount'], $veh['handling_fee']['rate'], '手续费');
}
if ($veh['fob_transport_fee']['amount']) {
$item->addMoney('fob_transport_fee', $veh['fob_transport_fee']['currency'], $veh['fob_transport_fee']['amount'], $veh['fob_transport_fee']['rate'], 'FOB运输费');
}
if ($veh['port_misc_fee']['amount']) {
$item->addMoney('port_misc_fee', $veh['port_misc_fee']['currency'], $veh['port_misc_fee']['amount'], $veh['port_misc_fee']['rate'], '港杂');
}
if ($veh['retrofit_fee']['amount']) {
$item->addMoney('retrofit_fee', $veh['retrofit_fee']['currency'], $veh['retrofit_fee']['amount'], $veh['retrofit_fee']['rate'], '改装费');
}
// 二手车 or 新车
if ($veh['mileage'] > 0) {
if ($veh['listing_price']['amount']) {
$item->addMoney('listing_price', $veh['listing_price']['currency'], $veh['listing_price']['amount'], $veh['listing_price']['rate'], '二手挂牌价');
}
if ($veh['reduction_price']['amount']) {
$item->addMoney('reduction_price', $veh['reduction_price']['currency'], $veh['reduction_price']['amount'], $veh['reduction_price']['rate'], '二手减价');
}
$ttPrice = ($veh['listing_price']['amount'] / $created->contract_exchange_rate);
$ddPrice = $veh['reduction_price']['amount'];
} else {
if ($veh['official_price']['amount']) {
$item->addMoney('official_price', $veh['official_price']['currency'], $veh['official_price']['amount'], $veh['official_price']['rate'], '官方指导价');
}
if ($veh['discount_price']['amount'] ) {
$item->addMoney('discount_price', $veh['discount_price']['currency'], $veh['discount_price']['amount'], $veh['discount_price']['rate'], '优惠价');
}
$ttPrice = ($veh['official_price']['amount'] / $created->contract_exchange_rate);
$ddPrice = $veh['discount_price']['amount'];
}
// 利润
$profit = (float)$veh['unit_price']['amount'] - (float)$ttPrice - (float)$ddPrice - (float)$veh['handling_fee']['amount'] - (float)$veh['fob_transport_fee']['amount'] - (float)$veh['port_misc_fee']['amount'] - (float)$veh['retrofit_fee']['amount'];
$item->addMoney('profit', $veh['unit_price']['currency'], $profit, $veh['unit_price']['rate'], '利润');
// cif_total_amount
$cifTotalAmount += $veh['total_price']['amount'];
$cifTotalAmountRate = $veh['retrofit_fee']['rate'];
$cifTotalAmountCurrency = $veh['retrofit_fee']['currency'];
// 改装
if (count($veh['modify_list']) > 0) {
foreach ($veh['modify_list'] as $modify) {
$modify['order_item_id'] = $item->id;
StockOrderItemOptions::create($modify); // 表4
}
}
}
// 计算&&插入 表1
$created->addMoney('cif_total_amount', $cifTotalAmountCurrency, $cifTotalAmount, $cifTotalAmountRate, '合计到岸价');
// 账户
if (count($data['account_list']) > 0) {
foreach ($data['account_list'] as $account) {
$account['order_id'] = $created->id;
$account['remit_date'] = strtotime($account['remit_date']);
StockOrderAccounts::create($account); // 表5
}
}
return $this->postProcess('Store', $created);
});
}
public function beforeServiceStore($data, Request $request)
{
$userId = $request->user->uid;
$data['order_no'] = generateStockNo();
$data['biz_date'] = strtotime($data['biz_date']);
$data['eta_ship_date'] = strtotime($data['eta_ship_date']);
$data['latest_arrival_date'] = strtotime($data['latest_arrival_date']);
$data['salesperson_id'] = $userId;
$data['salesperson_snapshot'] = User::where('id', $userId)->value('name') ?? '';
return $data;
}
protected function afterServiceStore($res)
{
// 处理原单
if ($res->is_final == 1 && $res->object_id > 0) {
$order = StockOrders::find($res->object_id);
$order->is_use = 1;
$order->object_id = $res->id;
if (!$order->save()) {
flg('备货单【'.$order->order_no.'】创建终版成功,但更新原单失败');
}
}
// 启动审批
$appro = new \app\common\utils\approval\service\ApprovalFlowService();
$result = $appro->start('stock_orders', 'stock_orders', $res->id, $res->toArray(), $res->salesperson_id);
var_dump($result);
// 异步生成pdf文件
\Webman\RedisQueue\Redis::send('stock-order-pdf', ['id'=>$res->id]);
return $res;
}
public function createdPdfFile($id)
{
$config = [];
$config['model'] = $this->modelClass;
$config['where']['id'] = $id;
$config['with'][] = 'items';
$config['with'][] = 'items.options';
$config['with'][] = 'items.moneyValues';
$config['with'][] = 'accounts';
$config['with'][] = 'moneyValues';
$config['with'][] = 'salesperson';
$item = $this->logicShow($config);
$pdfData = $this->pdfData($item);
// 渲染模板为最终HTML
$tplPath = app_path('common/utils/pdf/template/stockOrder.stub');
$html = \app\common\utils\pdf\service\PdfHelper::render($tplPath, ['data' => $pdfData]);
// 生成PDF
$driver = new \app\common\utils\pdf\service\MpdfDriver();
$pdfBin = $driver->htmlToPdf($html);
// 存储
$dir = public_path('pdf').'/';
$fileName = ($pdfData['order_no'] ?? date('YmdHis')) . '.pdf';
// 本地保存
\app\common\utils\pdf\service\PdfStorage::saveBinary(
$pdfBin,
$dir,
$fileName
);
// // 在线预览
// return new \support\Response(200, [
// 'Content-Type' => 'application/pdf',
// 'Content-Disposition' => 'inline; filename="stock-order.pdf"',
// ], $pdfBin);
// $url = [
// 'file_name' => $fileName,
// 'file_url' => getenv('MAIN_DOMAIN').'/pdf/'.$fileName
// ];
// 上传云端
$param = [
'path' => $dir . $fileName,
'scene' => 'stock_orders',
'category' => 'pdf',
'original_name' => $fileName
];
$up = new UploadService();
$res = $up->uploadLocalFile($param);
@unlink($dir . $fileName);
// 更新文件地址
$item->file_url = $res['file_name'];
$item->save();
return $res;
}
public function servicePrint(Request $request)
{
$config = $this->buildQueryConfig($request, 'show');
$item = $this->logicShow($config);
return $this->postProcess('Print', $item);
}
public function pdfData($item)
{
$vehList = [];
$index = 0;
foreach ($item->items as $k => $v) {
// 存在则数量加1
if (count($vehList) > 0 && isset($vehList[$v->unique_id])) {
$vehList[$v->unique_id]['num'] += 1;
$vehList[$v->unique_id]['total_price'] += $vehList[$v->unique_id]['unit_price'];
$vehList[$v->unique_id]['total_price_text'] = $vehList[$v->unique_id]['total_price_symbol'].number_format($vehList[$v->unique_id]['total_price'], 2, '.', ',');
continue;
}
$index += 1;
// 处理金额
$mv = [];
foreach ($v->moneyValues as $money) {
$mv[$money->field] = [
'currency' => $money->currency,
'amount' => $money->amount,
'rate' => $money->exchange_rate,
'currency_symbol' => SysCurrency::where('currency_code', $money->currency)->value('currency_symbol'),
];
}
// 处理改装
$mfy = '';
foreach ($v->options as $my) {
$mfy .= $my->option_name_snapshot.' ';
}
// 处理结果集
$vehList[$v->unique_id] = [
'index' => $index,
'model_snapshot' => $v->model_snapshot,
'color_interior_snapshot' => $v->color_interior_snapshot,
'color_exterior_snapshot' => $v->color_exterior_snapshot,
'num' => 1,
'modify' => $mfy,
'mileage' => $v->mileage,
'unit_price' => $mv['unit_price']['amount'],
'total_price' => $mv['total_price']['amount'],
'unit_price_symbol' => $mv['unit_price']['currency_symbol'],
'total_price_symbol' => $mv['total_price']['currency_symbol'],
'unit_price_text' => $mv['unit_price']['currency_symbol'].number_format($mv['unit_price']['amount'], 2, '.', ','),
'total_price_text' => $mv['total_price']['currency_symbol'].number_format($mv['total_price']['amount'], 2, '.', ',')
];
// 根据里程处理 新车or二手车
if ($v->mileage > 0) {
if (isset($mv['reduction_price'])) {
$vehList[$v->unique_id]['reduction_price'] = $mv['reduction_price']['amount'];
$vehList[$v->unique_id]['reduction_price_symbol'] = $mv['reduction_price']['currency_symbol'];
$vehList[$v->unique_id]['reduction_price_text'] = $mv['reduction_price']['currency_symbol'].number_format($mv['reduction_price']['amount'], 2, '.', ',');
}
$vehList[$v->unique_id]['listing_price'] = $mv['listing_price']['amount'];
$vehList[$v->unique_id]['listing_price_symbol'] = $mv['listing_price']['currency_symbol'];
$vehList[$v->unique_id]['listing_price_text'] = $mv['listing_price']['currency_symbol'].number_format($mv['listing_price']['amount'], 2, '.', ',');
} else {
if (isset($mv['discount_price'])) {
$vehList[$v->unique_id]['discount_price'] = $mv['discount_price']['amount'];
$vehList[$v->unique_id]['discount_price_symbol'] = $mv['discount_price']['currency_symbol'];
$vehList[$v->unique_id]['discount_price_text'] = $mv['discount_price']['currency_symbol'].number_format($mv['discount_price']['amount'], 2, '.', ',');
}
$vehList[$v->unique_id]['official_price'] = $mv['official_price']['amount'];
$vehList[$v->unique_id]['official_price_symbol'] = $mv['official_price']['currency_symbol'];
$vehList[$v->unique_id]['official_price_text'] = $mv['official_price']['currency_symbol'].number_format($mv['official_price']['amount'], 2, '.', ',');
}
}
$accList = [];
$accIndex = 1;
foreach ($item->accounts as $k => $v) {
$accList[] = [
'index' => '收款'.$k + 1,
'remit_country_name_snapshot' => $v->remit_country_name_snapshot,
'remit_date' => date('Y-m-d', $v->remit_date),
'currency_name_snapshot' => $v->currency_name_snapshot,
'account_name_snapshot' => $v->account_name_snapshot,
'remitter_name' => $v->remitter_name,
'amount' => $v->amount,
];
$accIndex += 1;
}
$cifTotalAmount = 0.00;
if ($item->moneyValues) {
$cifTotalAmountSysboml = SysCurrency::where('currency_code', $item->moneyValues->currency)->value('currency_symbol');
$cifTotalAmount = $cifTotalAmountSysboml.sprintf("%.2f", $item->moneyValues->amount, $item->moneyValues->exchange_rate);
}
$veh = [
'order_no' => $item->order_no,
'salesperson_snapshot' => $item->salesperson_snapshot,
// 'sign_img' => $item->salesperson->sign_img,
'sign_img_url' => (!empty($item->salesperson->sign_img))?(new \app\common\service\OssService)->generatePrivateUrl($item->salesperson->sign_img):'',
'created_at' => date('Y-m-d', strtotime($item->created_at)),
'pi_no' => $item->pi_no,
'biz_date' => date('Y-m-d', $item->biz_date),
'clearance_country_snapshot' => $item->clearance_country_snapshot,
'customer_source_snapshot' => $item->customer_source_snapshot,
'customer_snapshot' => $item->customer_snapshot,
'customer_country_snapshot' => $item->customer_country_snapshot,
// 订单项
'veh_list' => $vehList,
'contract_exchange_rate' => $item->contract_exchange_rate,
'cif_total_amount' => $item->cif_total_amount,
'declarant_name' => $item->declarant_name,
'eta_ship_date' => date('Y-m-d', $item->eta_ship_date),
'shipping_method_snapshot' => $item->shipping_method_snapshot,
'domestic_port_snapshot' => $item->domestic_port_snapshot,
'trade_term_snapshot' => $item->trade_term_snapshot,
'clearance_port_snapshot' => $item->clearance_port_snapshot,
'latest_arrival_date' => date('Y-m-d', $item->latest_arrival_date),
// 账户
'account_list' => $accList,
'acc_index' => $accIndex,
'cif_total_amount' => $cifTotalAmount
];
return $veh;
}
}
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace app\common\service;
use app\admin\validate\SysAccountValidate;
use app\common\logic\SysAccountLogic;
use app\common\traits\crud\CrudServiceTrait;
use app\common\model\SysAccount;
use support\Request;
class SysAccountService
{
protected $logic;
public function __construct()
{
$this->logic = (new SysAccountLogic());
}
/**
* 获取系统list
*/
public function getList()
{
return $this->logic->getList();
}
}
+25
View File
@@ -0,0 +1,25 @@
<?php
namespace app\common\service;
use app\common\logic\SysCurrencyLogic;
use app\common\logic\SystemsLogic;
class SysCurrencyService
{
protected $logic;
public function __construct()
{
$this->logic = (new SysCurrencyLogic());
}
/**
* 获取系统list
*/
public function getList()
{
return $this->logic->getList();
}
}
+42
View File
@@ -0,0 +1,42 @@
<?php
namespace app\common\service;
use app\admin\validate\SysDictItemValidate;
use app\common\logic\SysDictItemLogic;
use app\common\logic\SysDictLogic;
use app\common\traits\crud\CrudServiceTrait;
use app\common\model\SysDictItem;
use support\exception\BusinessException;
use support\Request;
class SysDictItemService
{
protected $logic;
public function __construct()
{
$this->logic = (new SysDictItemLogic());
}
/**
* 获取系统list
*/
public function getList($params)
{
if (empty($params['dict_id']) && empty($params['dict_code']))
throw new BusinessException('未明确要查询的字典组');
$where = [];
if(isset($params['dict_id']) && $params['dict_id'])
$where[] = ['dict_id', '=', $params['dict_id']];
if(isset($params['dict_code']) && $params['dict_code']){
$dict = (new SysDictLogic)->getDictByCode($params['dict_code']);
if (empty($dict['id']))
throw new BusinessException('字典组code错误');
$where[] = ['dict_id', '=', $dict['id']];
}
return $this->logic->getList($where);
}
}
+50
View File
@@ -0,0 +1,50 @@
<?php
namespace app\common\service;
use app\admin\validate\SysDictValidate;
use app\common\logic\SysDictLogic;
use app\common\traits\crud\CrudServiceTrait;
use app\common\model\SysDict;
use support\Request;
class SysDictService
{
protected $logic;
public function __construct()
{
$this->logic = (new SysDictLogic());
}
/**
* 获取系统list
*/
public function getList()
{
return $this->logic->getList();
}
public static function pluckValuesByCode(string $dictCode): \Illuminate\Support\Collection
{
return SysDict::where('dict_code', $dictCode)
->where('status', 1)
->first()?->items()
->where('status', 1)
->orderBy('sort')
->pluck('item_value') ?? collect();
}
public static function pluckKeyValueByCode(string $dictCode): \Illuminate\Support\Collection
{
return SysDict::where('dict_code', $dictCode)
->where('status', 1)
->first()?->items()
->where('status', 1)
->orderBy('sort')
->pluck('item_value', 'item_label') ?? collect();
}
}
+36
View File
@@ -0,0 +1,36 @@
<?php
namespace app\common\service;
use app\admin\validate\SysPortValidate;
use app\common\logic\SysPortLogic;
use app\common\traits\crud\CrudServiceTrait;
use app\common\model\SysPort;
use support\Request;
class SysPortService
{
protected $logic;
public function __construct()
{
$this->logic = (new SysPortLogic());
}
/**
* 获取系统list
*/
public function getList($params)
{
$where = [];
if(isset($params['scene']) && $params['scene']) {
if($params['scene'] == 'domestic'){
$where['country_id'] = 45;
}else{
$where['country_id'] = ['<>', 45];
}
}
return $this->logic->getList($where);
}
}
+177
View File
@@ -0,0 +1,177 @@
<?php
namespace app\common\service;
use support\exception\BusinessException;
use support\Log;
use support\Request;
class UploadService
{
/**
* 从服务器本地路径上传文件到 OSS
*
* @param Request $request
* - path: 本地文件绝对路径
* - scene: 业务分区/场景
* - category: 文件分类(可选)
* - original_name: 原始文件名(可选)
*/
public function uploadLocalFile($param)
{
try {
$filePath = $param['path'];
$scene = $param['scene'];
$category = $param['category']??'general';
$providedOriginalName = $param['original_name']??'';
if (empty($filePath)) {
Log::error('本地文件路径不能为空',$param);
throw new BusinessException('本地文件路径不能为空');
}
if (!file_exists($filePath) || !is_readable($filePath)) {
Log::error('本地文件不存在或不可读',$param);
throw new BusinessException('本地文件不存在或不可读');
}
if (empty($scene)) {
Log::error('上传场景不能为空',$param);
throw new BusinessException('上传场景不能为空');
}
$basename = basename($filePath);
$originalName = $providedOriginalName ?: $basename;
$extensionFromName = pathinfo($originalName, PATHINFO_EXTENSION);
$extensionFromPath = pathinfo($basename, PATHINFO_EXTENSION);
$extension = $extensionFromName ?: $extensionFromPath;
$mime = mime_content_type($filePath) ?: 'application/octet-stream';
$size = filesize($filePath) ?: 0;
$sceneUrl = $scene ? ($scene . '/') : '';
$dir = 'x_server/' . $sceneUrl . date('Ymd') . '/';
// 使用传入的 original_name 命名(缺少扩展名时补上推断的扩展名),并做简单安全处理
$safeBaseName = preg_replace('/[\\\\\/]+/', '_', $originalName);
if ($extension && strtolower(pathinfo($safeBaseName, PATHINFO_EXTENSION)) !== strtolower($extension)) {
// 若提供的名称没有扩展名或扩展名不一致,则以推断扩展名为准
$safeBaseName = pathinfo($safeBaseName, PATHINFO_FILENAME) . '.' . $extension;
} elseif (!$extension && !pathinfo($safeBaseName, PATHINFO_EXTENSION)) {
// 实在没有扩展名信息时,保持原名
}
// 重名检测:同一目录、同名且同类型则在文件名后追加序号
$uploadedFilesService = new UploadedFilesService();
$candidateName = $safeBaseName;
$nameBody = pathinfo($safeBaseName, PATHINFO_FILENAME);
$nameExt = pathinfo($safeBaseName, PATHINFO_EXTENSION);
$counter = 1;
while (true) {
$candidatePath = $dir . $candidateName;
$existing = $uploadedFilesService->findFile(['file_path' => $candidatePath]);
if (!$existing) {
break;
}
// 若存在同路径同名,且类型也相同,则追加序号
if (isset($existing->file_type) && strtolower($existing->file_type) === strtolower($mime)) {
$suffix = ' (' . $counter . ')';
$candidateName = $nameBody . $suffix . ($nameExt !== '' ? ('.' . $nameExt) : '');
$counter++;
continue;
}
// 类型不同则允许同名,结束循环
break;
}
$objectKey = $dir . $candidateName;
$ossService = new OssService();
$ossService->uploadImage($filePath, $objectKey);
$privateUrl = $ossService->generatePrivateUrl($objectKey);
// 记录文件信息到数据库
$uploadedFilesService = new UploadedFilesService();
$fileInfo = [
'file_name' => basename($objectKey),
'file_path' => $objectKey,
'file_size' => $size,
'file_type' => $mime,
'original_name' => $originalName,
];
$recordResult = $uploadedFilesService->recordFileInfo($fileInfo, null, $scene, $category);
$data = [
'original_name' => $originalName,
'file_name' => $objectKey,
'file_url' => $privateUrl,
'file_id' => $recordResult['id'] ?? null,
'partition' => $scene,
'category' => $category,
];
return $data;
} catch (\Throwable $e) {
Log::error($e->getMessage(),$param);
throw new BusinessException($e->getMessage());
}
}
/**
* 删除 OSS 上的文件
*
* @param array $param
* - object_key: OSS 对象键名-oss路径(必需)
* - file_id: 数据库文件记录ID(可选,用于删除数据库记录)
* @return bool 删除是否成功
*
* 示例
* $uploadService = new UploadService();
*
* 只删除 OSS 文件
* $uploadService->deleteOssFile([
* 'object_key' => 'vehicle_album/documents/20241201/abc123.pdf'
* ]);
*
* 同时删除 OSS 文件和数据库记录
* $uploadService->deleteOssFile([
* 'object_key' => 'vehicle_album/images/20241201/abc123.jpg',
* 'file_id' => 123
* ]);
*/
public function deleteOssFile($objectKey,$fileId = null)
{
try {
if (empty($objectKey)) {
Log::error('OSS对象键名不能为空', $objectKey);
throw new BusinessException('OSS对象键名不能为空');
}
$uploadedFilesService = new UploadedFilesService();
// 如果提供了文件ID,同时删除数据库记录
if ($fileId) {
$uploadedFilesService->deleteFile($fileId);
}else{
$uploadedFilesService = new UploadedFilesService();
if (empty($fileId)) {
$where = ['file_path' => $objectKey];
$file = $uploadedFilesService->getFile($where);
$fileId = $file->id;
}
if(!empty($fileId)){
$uploadedFilesService->deleteFile($fileId);
}
}
// 删除 OSS 上的文件
$ossService = new OssService();
$ossService->deleteFile($objectKey);
Log::info('OSS文件删除成功', ['object_key' => $objectKey, 'file_id' => $fileId]);
return true;
} catch (\Throwable $e) {
Log::error('删除OSS文件失败: ' . $e->getMessage());
throw new BusinessException('删除OSS文件失败: ' . $e->getMessage());
}
}
}
+238
View File
@@ -0,0 +1,238 @@
<?php
namespace app\common\service;
use app\common\validate\UploadedFilesValidate;
use app\common\traits\crud\CrudServiceTrait;
use app\common\model\UploadedFiles;
use support\Request;
use support\exception\BusinessException;
class UploadedFilesService
{
use CrudServiceTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = UploadedFiles::class;
/**
* 文件上传分区配置
*/
protected array $uploadPartitions = [
'image' => [
'path' => 'images',
'max_size' => 5 * 1024 * 1024, // 5MB
'allowed_types' => ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
'extensions' => ['jpg', 'jpeg', 'png', 'gif', 'webp']
],
'document' => [
'path' => 'documents',
'max_size' => 10 * 1024 * 1024, // 10MB
'allowed_types' => ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
'extensions' => ['pdf', 'doc', 'docx']
],
'audio' => [
'path' => 'audios',
'max_size' => 20 * 1024 * 1024, // 20MB
'allowed_types' => ['audio/mp3', 'audio/wav', 'audio/ogg'],
'extensions' => ['mp3', 'wav', 'ogg']
],
'other' => [
'path' => 'others',
'max_size' => 5 * 1024 * 1024, // 5MB
'allowed_types' => [],
'extensions' => []
]
];
/**
* 记录文件信息到数据库
*
* @param array $fileInfo 文件信息
* @param Request|null $request 请求对象(可选)
* @param string $partition 分区名称
* @param string $category 文件分类
* @return array
* @throws BusinessException
*/
public function recordFileInfo(array $fileInfo, ?Request $request = null, string $partition = 'other', string $category = 'general'): array
{
// 验证必需的文件信息
$requiredFields = ['file_name', 'file_path', 'file_size', 'file_type'];
foreach ($requiredFields as $field) {
if (!isset($fileInfo[$field])) {
throw new BusinessException("缺少必需的文件信息字段: {$field}");
}
}
// 获取文件扩展名
$fileExtension = pathinfo($fileInfo['file_name'], PATHINFO_EXTENSION);
// 记录到数据库
$uploadIp = $request ? $request->getRealIp() : '0.0.0.0';
$userAgent = $request ? $request->header('User-Agent', '') : '';
$fileData = [
'file_name' => $fileInfo['file_name'],
'file_path' => $fileInfo['file_path'],
'file_size' => $fileInfo['file_size'],
'file_type' => $fileInfo['file_type'],
'category' => $category,
'original_name' => $fileInfo['original_name'] ?? '',
'extension' => $fileExtension,
'partition' => $partition,
'upload_ip' => $uploadIp,
'user_agent' => $userAgent,
];
$uploadedFile = $this->logicCreate($fileData);
return [
'id' => $uploadedFile->id,
'file_name' => $fileInfo['file_name'],
'file_path' => $fileInfo['file_path'],
'file_url' => getenv('MAIN_DOMAIN') . $fileInfo['file_path'],
'file_size' => $fileInfo['file_size'],
'file_type' => $fileInfo['file_type'],
'category' => $category,
'partition' => $partition,
'original_name' => $fileInfo['original_name'] ?? '',
'extension' => $fileExtension,
];
}
/**
* 获取文件信息
*
* @param int $fileId
* @return array|null
*/
public function getFileInfo(int $fileId): ?array
{
$file = $this->logicShow(['where' => ['id' => $fileId]]);
if (!$file) {
return null;
}
return [
'id' => $file->id,
'file_name' => $file->file_name,
'file_path' => $file->file_path,
'file_url' => getenv('MAIN_DOMAIN') . $file->file_path,
'file_size' => $file->file_size,
'file_type' => $file->file_type,
'category' => $file->category,
'partition' => $file->partition ?? 'other',
'original_name' => $file->original_name ?? '',
'extension' => $file->extension ?? '',
'created_at' => $file->created_at,
];
}
/**
* 获取文件
*
* @param string $path
* @return bool|\Illuminate\Database\Eloquent\Model
*/
public function getFile(array $where)
{
$config['model'] = $this->modelClass;
$config['where'] = $where;
$file = $this->logicShow($config);
if (!$file) {
return false;
}
return $file;
}
/**
* 获取文件
*
* @param string $path
* @return bool|\Illuminate\Database\Eloquent\Model
*/
public function findFile(array $where)
{
$file = UploadedFiles::where(['file_path' => $where['file_path']]);
if (!$file) {
return false;
}
return $file;
}
/**
* 删除文件
*
* @param int $fileId
* @return bool
*/
public function deleteFile(int $fileId): bool
{
$config['model'] = $this->modelClass;
$config['where'] = ['id' => $fileId];
$file = $this->logicShow($config);
if (!$file) {
return false;
}
// 删除数据库记录
$config =[
'model' => $this->modelClass,
'where' => ['id' => $fileId],
];
return $this->logicDeleteBy($config);
}
/**
* 获取分区配置
*
* @return array
*/
public function getPartitionConfig(): array
{
return $this->uploadPartitions;
}
/**
* 生成文件名
*
* @param $file
* @return string
*/
protected function generateFileName($file): string
{
$extension = $file->getUploadExtension();
$timestamp = time();
$random = substr(md5(uniqid()), 0, 8);
return $timestamp . '_' . $random . '.' . $extension;
}
/**
* 格式化文件大小
*
* @param int $bytes
* @return string
*/
protected function formatFileSize(int $bytes): string
{
$units = ['B', 'KB', 'MB', 'GB'];
$bytes = max($bytes, 0);
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
$bytes /= pow(1024, $pow);
return round($bytes, 2) . ' ' . $units[$pow];
}
}
+6 -1
View File
@@ -52,6 +52,9 @@ class UserService
'sn' => $user->sn,
'corp_id' => $user->corp_id,
'corp_name' => $corp_name,
'report_to' => $user->report_to,
'sign_img' => $user->sign_img,
'sign_img_url' => (!empty($user->sign_img))?(new \app\common\service\OssService)->generatePrivateUrl($user->sign_img):'',
],
// 'menu' => $menuTree,
// 'permissions' => $permissionCodes
@@ -84,7 +87,9 @@ class UserService
'email' => $user->email,
'sex' => $user->sex,
'status' => $user->status,
'roles' => $user->roles->pluck('name')
'roles' => $user->roles->pluck('name'),
'sign_img' => $user->sign_img,
'sign_img_url' => (!empty($user->sign_img))?$ossservice->generatePrivateUrl($user->sign_img):'',
];
}
+33
View File
@@ -0,0 +1,33 @@
<?php
namespace app\common\service;
use app\admin\validate\VehColorValidate;
use app\common\logic\VehColorLogic;
use app\common\traits\crud\CrudServiceTrait;
use app\common\model\VehColor;
use support\exception\BusinessException;
class VehColorService
{
protected $logic;
public function __construct()
{
$this->logic = (new VehColorLogic());
}
/**
* 获取系统list
*/
public function getList($params)
{
if (empty($params['type']))
throw new BusinessException('类型不能为空');
$where = [];
if(isset($params['type']) && $params['type'])
$where[] = ['type', '=', $params['type']];
return $this->logic->getList($where);
}
}
+75
View File
@@ -0,0 +1,75 @@
<?php
namespace app\common\service;
use app\common\validate\VehDictI18nValidate;
use app\common\traits\crud\CrudServiceTrait;
use app\common\model\VehDictI18n;
class VehDictI18nService
{
use CrudServiceTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = VehDictI18n::class;
/**
* 验证器
*/
protected $validator = VehDictI18nValidate::class;
/**
* 获取验证器
*/
public function getValidator()
{
return new $this->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}");
}
}
@@ -0,0 +1,57 @@
<?php
namespace app\common\service;
use app\common\validate\VehDictMappingValidate;
use app\common\traits\crud\CrudServiceTrait;
use app\common\model\VehDictMapping;
class VehDictMappingService
{
use CrudServiceTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = VehDictMapping::class;
/**
* 验证器
*/
protected $validator = VehDictMappingValidate::class;
/**
* 获取验证器
*/
public function getValidator()
{
return new $this->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]);
}
}
@@ -0,0 +1,28 @@
<?php
namespace app\common\service;
use app\admin\validate\VehModificationValidate;
use app\common\logic\VehModificationLogic;
use app\common\traits\crud\CrudServiceTrait;
use app\common\model\VehModification;
use support\Request;
class VehModificationService
{
protected $logic;
public function __construct()
{
$this->logic = (new VehModificationLogic());
}
/**
* 获取系统list
*/
public function getList()
{
return $this->logic->getList();
}
}
@@ -0,0 +1,31 @@
<?php
namespace app\common\service;
use app\common\validate\VehMoneyValueValidate;
use app\common\traits\crud\CrudServiceTrait;
use app\common\model\VehMoneyValue;
class VehMoneyValueService
{
use CrudServiceTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = VehMoneyValue::class;
/**
* 验证器
*/
protected $validator = VehMoneyValueValidate::class;
/**
* 获取验证器
*/
public function getValidator()
{
return new $this->validator;
}
}
@@ -0,0 +1,31 @@
<?php
namespace app\common\service;
use app\common\validate\VehVehicleListingValidate;
use app\common\traits\crud\CrudServiceTrait;
use app\common\model\VehVehicleListing;
class VehVehicleListingService
{
use CrudServiceTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = VehVehicleListing::class;
/**
* 验证器
*/
protected $validator = VehVehicleListingValidate::class;
/**
* 获取验证器
*/
public function getValidator()
{
return new $this->validator;
}
}
+164
View File
@@ -0,0 +1,164 @@
<?php
namespace app\common\service;
use app\common\logic\Fingerprint;
use app\common\logic\ModelNaming;
use app\common\logic\SkuCodeBuilder;
use app\common\validate\VehVehicleSkuValidate;
use app\common\traits\crud\CrudServiceTrait;
use app\common\model\VehVehicleSku;
use support\Request;
class VehVehicleSkuService
{
use CrudServiceTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = VehVehicleSku::class;
/**
* 验证器
*/
protected $validator = VehVehicleSkuValidate::class;
/**
* 获取验证器
*/
public function getValidator()
{
return new $this->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']['id'] = 'desc';
break;
case 'index':
$config['where']['is_active'] = 1;
$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);
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)
{
unset($data['created_at']);
unset($data['updated_at']);
// $data = $this->backfill($data);
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;
}
}
@@ -0,0 +1,31 @@
<?php
namespace app\common\service;
use app\common\validate\VehVehicleUnitValidate;
use app\common\traits\crud\CrudServiceTrait;
use app\common\model\VehVehicleUnit;
class VehVehicleUnitService
{
use CrudServiceTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = VehVehicleUnit::class;
/**
* 验证器
*/
protected $validator = VehVehicleUnitValidate::class;
/**
* 获取验证器
*/
public function getValidator()
{
return new $this->validator;
}
}
+1
View File
@@ -50,6 +50,7 @@ trait DataTransformer
return [
'error' => true,
'message' => $e->getMessage(),
// 'message' => $e->getMessage()."\n".$e->getLine()."\n".$e->getFile(),
'code' => $e->getCode(),
];
}
+85
View File
@@ -0,0 +1,85 @@
<?php
namespace app\common\traits;
use app\common\model\VehMoneyValue;
use support\Db;
use Illuminate\Database\Eloquent\Relations\HasMany;
trait HasMoneyJournal
{
/**
* 【抽象方法】必须由使用该 trait 的模型实现
* 返回当前模型对应的金额“业务场景”标识
*
* @return string
*/
abstract protected static function moneyObjectScene(): string;
/** 所有关联金额记录(append-only 全部历史) */
public function moneyJournal(): HasMany
{
return $this->hasMany(VehMoneyValue::class, 'object_id')
->where('object_scene', static::moneyObjectScene());
}
/** 取某费项的“最新记录”(按时间/id 倒序) */
public function moneyLatest(string $field): ?VehMoneyValue
{
return $this->moneyJournal()
->where('field', $field)
->orderByDesc('created_at')
->orderByDesc('id')
->first();
}
/** 追加一行金额 */
public function addMoney(string $field, string $currency, $amount, ?string $rate = null, ?string $remark = null): VehMoneyValue
{
$now = time();
/** @var VehMoneyValue $row */
$row = $this->moneyJournal()->create([
'object_scene' => static::moneyObjectScene(),
'object_id' => $this->id,
'field' => $field,
'currency' => strtoupper($currency),
'amount' => (string)$amount,
'exchange_rate' => $rate,
'remark' => $remark,
'created_at' => $now,
'updated_at' => $now,
]);
return $row;
}
/** 批量追加:['unit_price'=>['currency'=>'USD','amount'=>'2199.00','rate'=>'7.2','remark'=>'...'], ...] */
public function addMoneyBatch(array $payload): array
{
return Db::transaction(function () use ($payload) {
$out = [];
foreach ($payload as $field => $cfg) {
$out[$field] = $this->addMoney(
$field,
$cfg['currency'] ?? 'CNY',
$cfg['amount'] ?? '0.00',
$cfg['rate'] ?? null,
$cfg['remark'] ?? null
);
}
return $out;
});
}
/** 把若干费项按各自汇率换到结算币并求和(无 rate 的跳过) */
public function sumToSettlement(array $fields, int $scale = 2): string
{
$sum = '0.00';
foreach ($fields as $f) {
$row = $this->moneyLatest($f);
if ($row) {
$v = $row->amountInSettlement($scale);
if ($v !== null) $sum = bcadd($sum, $v, $scale);
}
}
return $sum;
}
}
+194
View File
@@ -0,0 +1,194 @@
<?php
// app/service/ResourceService.php
namespace app\common\utils;
use support\Db;
class MigrationResources
{
/**
* 获取完整的资源树结构(不区分系统)
*
* @param array $conditions 筛选条件
* @return array
*/
public function getFullResourceTree($table = 'resources', array $conditions = []): array
{
// 查询所有资源数据
$query = Db::table($table)->where($conditions);
// 按排序字段排序
$resources = $query->orderBy('sort')->orderBy('id')->get();
// var_dump($resources->count());
// 转换为数组
$resourceList = [];
foreach ($resources as $resource) {
$resourceList[] = (array) $resource;
}
// 构建树状结构
return $this->buildTree($resourceList);
}
/**
* 构建树状结构
*
* @param array $resources 资源列表
* @param int $parentId 父级ID
* @return array
*/
private function buildTree(array $resources, int $parentId = 0): array
{
$tree = [];
foreach ($resources as $resource) {
if ($resource['parent_id'] == $parentId) {
// 递归获取子节点
$children = $this->buildTree($resources, $resource['id']);
// 如果有子节点,则添加到children字段
if (!empty($children)) {
$resource['children'] = $children;
}
$tree[] = $resource;
}
}
return $tree;
}
/**
* 使用引用方式构建树(性能优化版)
*
* @param array $conditions 筛选条件
* @return array
*/
public function getFullResourceTreeOptimized($table = 'resources', array $conditions = []): array
{
// 查询所有资源数据
$query = Db::table($table)->where($conditions);
// 按排序字段排序
$resources = $query->orderBy('sort')->orderBy('id')->get();
// 构建ID映射和引用关系
$resourceMap = [];
$tree = [];
// 初始化所有节点
foreach ($resources as $resource) {
$resourceArray = (array) $resource;
$resourceMap[$resourceArray['id']] = $resourceArray;
}
// 构建父子关系
foreach ($resourceMap as $id => &$node) {
if ($node['parent_id'] == 0) {
// 根节点
$tree[] = &$node;
} else {
// 子节点
if (isset($resourceMap[$node['parent_id']])) {
if (!isset($resourceMap[$node['parent_id']]['children'])) {
$resourceMap[$node['parent_id']]['children'] = [];
}
$resourceMap[$node['parent_id']]['children'][] = &$node;
} else {
// 父节点不存在,作为根节点处理
$tree[] = &$node;
}
}
}
unset($node);
return $tree;
}
/**
* 获取扁平化的资源列表
*
* @param array $conditions 筛选条件
* @return array
*/
public function getResourceList($table = 'resources', array $conditions = []): array
{
$query = Db::table($table)->where($conditions);
// 按排序字段排序
$resources = $query->orderBy('sort')->orderBy('id')->get();
// 转换为数组
$resourceList = [];
foreach ($resources as $resource) {
$resourceList[] = (array) $resource;
}
return $resourceList;
}
/**
* 递归插入资源树
*/
private function insertResourceTree($table, $tree, $parentId, $keys = [])
{
$keys = ['system_id','name','desc','type','code','parent_id','url','icon','open_type','link',
'is_hide','is_hide_tab','no_auth','is_home','instance','keep_alive','sort','status'];
foreach ($tree as $item) {
// 保存子节点
$children = $item['children'] ?? [];
// 过滤字段
$filteredItem = [];
foreach ($item as $k => $v) {
if (in_array($k, $keys)) {
$filteredItem[$k] = $v;
}
}
// 设置parent_id
$filteredItem['parent_id'] = $parentId;
// 插入并获取新ID
$newId = Db::table($table)->insertGetId($filteredItem);
echo "插入节点: {$filteredItem['name']}, 新ID: $newId\n";
// 递归处理子节点
if (!empty($children)) {
$this->insertResourceTree($table, $children, $newId, $keys);
}
}
}
public function do(string $from = 'resources_20250916_bak', string $to = 'resources_yuanshi', array $where = [])
{
try {
// 开启事务
Db::beginTransaction();
// 查询迁移资源的树状结果集
$list = $this->getFullResourceTreeOptimized($from, $where);
// 入库字段
$keys = ['system_id','name','desc','type','code','parent_id','url','icon','open_type','link',
'is_hide','is_hide_tab','no_auth','is_home','instance','keep_alive','sort','status'];
// 使用递归处理树结构
$this->insertResourceTree($to, $list, 0, $keys);
// 提交事务
Db::commit();
echo "数据插入成功";
} catch (\Exception $e) {
// 回滚事务
Db::rollBack();
echo "数据插入失败: " . $e->getMessage() . " 在文件 " . $e->getFile() . " 第 " . $e->getLine() . " 行";
}
}
}
@@ -0,0 +1,49 @@
<?php
namespace app\common\utils\approval\model;
use support\Model;
class ApprovalCcs extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'approval_ccs';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
protected $dateFormat = 'U';
/**
* 允许自动填充模型字段
*/
protected $fillable = ['id', 'instance_id', 'user_id', 'notified_at', 'read_at', 'created_at', 'updated_at'];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
}
@@ -0,0 +1,49 @@
<?php
namespace app\common\utils\approval\model;
use support\Model;
class ApprovalFlows extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'approval_flows';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
protected $dateFormat = 'U';
/**
* 允许自动填充模型字段
*/
protected $fillable = ['id', 'name', 'type', 'steps', 'is_active', 'timeout_hours', 'timeout_action', 'created_at', 'updated_at'];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
}
@@ -0,0 +1,49 @@
<?php
namespace app\common\utils\approval\model;
use support\Model;
class ApprovalInstances extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'approval_instances';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
protected $dateFormat = 'U';
/**
* 允许自动填充模型字段
*/
protected $fillable = ['id', 'flow_type', 'biz_id', 'biz_type', 'form_data', 'initiator_id', 'status', 'current_step', 'finished_at', 'deadline_at', 'created_at', 'updated_at'];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
}
@@ -0,0 +1,49 @@
<?php
namespace app\common\utils\approval\model;
use support\Model;
class ApprovalRecords extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'approval_records';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
protected $dateFormat = 'U';
/**
* 允许自动填充模型字段
*/
protected $fillable = ['id', 'instance_id', 'approver_id', 'step_index', 'action', 'comment', 'approved_at', 'created_at', 'updated_at'];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,76 @@
<?php
// app/controller/PdfController.php
namespace app\controller;
use support\Request;
use support\Response;
use support\View;
use app\common\utils\pdf\service\MpdfDriver;
use app\common\utils\pdf\service\PdfHelper;
class PdfController
{
// ① 从“现有 HTML 文件 + 外链CSS”生成并在线预览
public function previewFromFile(Request $request): Response
{
// 你的模板路径(示例)
$tpl = base_path('resource/templates/stock-order.html');
// 可选:把 {{变量}} 替换
$html = PdfHelper::loadTemplate($tpl, [
'title' => '备货单',
'order_no' => 'SO' . date('YmdHis'),
'customer' => 'Magic Bridge Auto FZE',
]);
$driver = new MpdfDriver();
$pdf = $driver->htmlToPdf($html, [
// 若 HTML 里用到相对资源(img/css),给个基准目录
'asset_base' => public_path('assets/pdf'),
// 也可以单独传 CSS 文件(推荐把 CSS 放在 public/assets/pdf)
'css_files' => [
public_path('assets/pdf/stock-order.css'),
],
'header_html' => '<div style="text-align:right;color:#888;font-size:10px">Company • Stock Order</div>',
'footer_html' => '<div style="text-align:center;color:#888;font-size:10px">{PAGENO} / {nbpg}</div>',
'watermark_text' => '', // 'DRAFT'
]);
return new Response(200, [
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'inline; filename="stock-order.pdf"',
], $pdf);
}
// ② 直接下载
public function download(Request $request): Response
{
$html = '<h1>发票</h1><p>客户:永生花</p>';
$pdf = (new MpdfDriver())->htmlToPdf($html);
return new Response(200, [
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'attachment; filename="invoice.pdf"',
], $pdf);
}
// ③ 保存到本地(或上传 OSS 前先落盘)
public function save(Request $request): Response
{
$tpl = base_path('resource/templates/invoice.html');
$html = PdfHelper::loadTemplate($tpl, [
'invoice_no' => 'INV' . date('YmdHis'),
'amount' => '12345.67',
]);
$pdf = (new MpdfDriver())->htmlToPdf($html, [
'css_files' => [ public_path('assets/pdf/invoice.css') ],
'asset_base'=> public_path('assets/pdf'),
]);
$dir = runtime_path('pdf');
if (!is_dir($dir)) mkdir($dir, 0777, true);
$path = $dir . '/invoice-' . date('YmdHis') . '.pdf';
file_put_contents($path, $pdf);
return json(['path' => $path]);
}
}
+118
View File
@@ -0,0 +1,118 @@
<?php
// app/service/pdf/MpdfDriver.php
namespace app\common\utils\pdf\service;
use Mpdf\Mpdf;
use Mpdf\Config\ConfigVariables;
use Mpdf\Config\FontVariables;
use Mpdf\HTMLParserMode;
class MpdfDriver
{
public function htmlToPdf(string $html, array $options = []): string
{
$opts = array_merge([
'format' => 'A4',
'orientation' => 'L',
'margin_top' => 12,
'margin_bottom' => 12,
'margin_left' => 10,
'margin_right' => 10,
'tempDir' => runtime_path('mpdf'),
'header_html' => '',
'footer_html' => '',
'watermark_text'=> '',
'css_files' => [],
'default_font' => 'notosanssc',
'font_dirs' => [public_path('fonts')],
'font_data' => [
'notosanssc' => ['R'=>'NotoSansSC-Regular.ttf','B'=>'NotoSansSC-Bold.ttf'],
],
'asset_base' => '',
'enable_html5_parser' => true,
], $options);
$defaultConfig = (new ConfigVariables())->getDefaults();
$defaultFontConfig = (new FontVariables())->getDefaults();
$fontDirs = array_merge($defaultConfig['fontDir'], $opts['font_dirs']);
$fontData = array_merge($defaultFontConfig['fontdata'], $opts['font_data']);
$mpdf = new Mpdf([
'format' => $opts['format'],
'orientation' => $opts['orientation'],
'margin_left' => $opts['margin_left'],
'margin_right' => $opts['margin_right'],
'margin_top' => $opts['margin_top'],
'margin_bottom' => $opts['margin_bottom'],
'tempDir' => $opts['tempDir'],
'fontDir' => $fontDirs,
'fontdata' => $fontData,
'default_font' => $opts['default_font'],
'enable_html5_parser' => $opts['enable_html5_parser'],
]);
if ($opts['header_html']) $mpdf->SetHTMLHeader($opts['header_html']);
if ($opts['footer_html']) $mpdf->SetHTMLFooter($opts['footer_html']);
if (!empty($opts['watermark_text'])) {
$mpdf->SetWatermarkText($opts['watermark_text']);
$mpdf->showWatermarkText = true;
$mpdf->watermarkTextAlpha = 0.08;
}
// 资源绝对化(如果你有相对路径的图片等)
$html = PdfHelper::absolutizeAssetUrls($html, $opts['asset_base']);
// === 关键改动:把 <style>…</style> 抽出来,作为 HEADER_CSS 写入 ===
[$inlineCss, $cleanHtml] = $this->extractCssAndCleanHtml($html);
// 先写外部 css_files
if (!empty($opts['css_files'])) {
foreach ($opts['css_files'] as $cssFile) {
if (is_file($cssFile)) {
$mpdf->WriteHTML(file_get_contents($cssFile), HTMLParserMode::HEADER_CSS);
}
}
}
// 再写内联 <style> 的 CSS
if ($inlineCss !== '') {
$mpdf->WriteHTML($inlineCss, HTMLParserMode::HEADER_CSS);
}
// 最后写 HTML 正文(已剔除 <style>,避免任何情况下把 CSS 当文字打印)
$mpdf->WriteHTML($cleanHtml, HTMLParserMode::HTML_BODY);
return $mpdf->Output('', \Mpdf\Output\Destination::STRING_RETURN);
}
/**
* 把所有 <style>...</style> 的内容拼成一段 CSS,并把这些 <style> 标签从 HTML 中移除
* @return array{0:string,1:string} [css, htmlWithoutStyle]
*/
private function extractCssAndCleanHtml(string $html): array
{
$cssParts = [];
// 抓取所有 <style ...>...</style>,DOTALL 支持跨行
if (preg_match_all('/<style\b[^>]*>(.*?)<\/style>/is', $html, $m)) {
foreach ($m[1] as $block) {
// 去掉 HTML 注释,以防注释把 CSS 包起来
$block = preg_replace('/<!--|-->/', '', $block);
$cssParts[] = trim($block);
}
// 从 HTML 删除所有 <style> 块
$html = preg_replace('/<style\b[^>]*>.*?<\/style>/is', '', $html);
}
$css = implode("\n\n", $cssParts);
// 额外保险:把 <head> 也清理掉,防止某些解析器把 head 里残余文本当正文
$html = preg_replace('/<head\b[^>]*>.*?<\/head>/is', '', $html);
// 确保剩余 HTML 有 <body>,没有就包一层
if (!preg_match('/<body\b/i', $html)) {
$html = '<body>' . $html . '</body>';
}
return [$css, $html];
}
}
@@ -0,0 +1,80 @@
<?php
// app/service/pdf/PdfHelper.php
namespace app\common\utils\pdf\service;
class PdfHelper
{
/** 从文件加载 HTML(支持占位符 {{key}} 简单替换) */
public static function loadTemplate(string $file, array $vars = []): string
{
if (!is_file($file)) {
throw new \RuntimeException("Template not found: {$file}");
}
$html = file_get_contents($file);
if ($vars) {
$search = [];
$replace = [];
foreach ($vars as $k => $v) {
$search[] = '{{' . $k . '}}';
$replace[] = htmlspecialchars((string)$v, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
$html = str_replace($search, $replace, $html);
}
return $html;
}
/**
* 把 HTML 里的相对资源路径(img/link/script href/src)改成绝对路径
* 仅处理以 ./ ../ 或非 http(s) 的相对量;base 传 public/assets/pdf 等目录
*/
public static function absolutizeAssetUrls(string $html, string $baseDir): string
{
$baseDir = rtrim($baseDir, DIRECTORY_SEPARATOR);
// img src
$html = preg_replace_callback('#(<img[^>]+src=["\'])([^"\']+)(["\'])#i', function($m) use ($baseDir){
$u = $m[2];
if (preg_match('#^https?://#i', $u) || strpos($u, 'data:') === 0) return $m[0];
$abs = realpath($baseDir . DIRECTORY_SEPARATOR . ltrim($u, '/')) ?: ($baseDir . '/' . ltrim($u, '/'));
return $m[1] . $abs . $m[3];
}, $html);
// link href(如果你在 HTML 里写了 <link>,通常建议走 css_files 方式,这里也兼容)
$html = preg_replace_callback('#(<link[^>]+href=["\'])([^"\']+)(["\'])#i', function($m) use ($baseDir){
$u = $m[2];
if (preg_match('#^https?://#i', $u)) return $m[0];
$abs = realpath($baseDir . DIRECTORY_SEPARATOR . ltrim($u, '/')) ?: ($baseDir . '/' . ltrim($u, '/'));
return $m[1] . $abs . $m[3];
}, $html);
return $html;
}
/**
* 渲染一个原生 PHP 模板为字符串
* @param string $file 模板路径(如 resource/templates/stock-order.php)
* @param array $vars 将在模板中以变量名存在(如 $data)
* @return string 完整 HTML
*/
public static function render(string $file, array $vars = []): string
{
if (!is_file($file)) {
throw new \RuntimeException("Template not found: {$file}");
}
// 将键名导出为同名变量,例如 'data' => [...] 变为 $data
extract($vars, EXTR_OVERWRITE);
// 开启缓冲,将 include 的输出抓取为字符串
ob_start();
ob_implicit_flush(false);
try {
/** @noinspection PhpIncludeInspection */
include $file;
} catch (\Throwable $e) {
// 清理缓冲并抛出更清晰的异常
ob_end_clean();
throw $e;
}
return ob_get_clean();
}
}
@@ -0,0 +1,38 @@
<?php
namespace app\common\utils\pdf\service;
final class PdfStorage
{
/**
* @param string $pdfBin PDF 二进制
* @param string|null $dir 保存目录,默认 runtime/pdf
* @param string|null $name 文件名(不含目录),默认:stock-order-YYYYmmdd-His-uniqid.pdf
* @return string 绝对路径
*/
public static function saveBinary(string $pdfBin, ?string $dir = null, ?string $name = null): string
{
$dir = $dir ?: runtime_path('pdf');
if (!is_dir($dir)) {
if (!@mkdir($dir, 0775, true) && !is_dir($dir)) {
throw new \RuntimeException("Failed to create dir: {$dir}");
}
}
// 生成安全文件名
$name = $name ?: ('stock-order-' . date('Ymd-His') . '-' . substr(uniqid('', true), -6) . '.pdf');
$name = preg_replace('/[^a-zA-Z0-9_\-.]/', '_', $name); // 简易净化
$path = rtrim($dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $name;
// 先写入临时文件,再原子重命名,避免并发/写入中被读取
$tmp = $path . '.part';
if (file_put_contents($tmp, $pdfBin, LOCK_EX) === false) {
throw new \RuntimeException("Failed to write temp file: {$tmp}");
}
if (!@rename($tmp, $path)) {
@unlink($tmp);
throw new \RuntimeException("Failed to move temp to: {$path}");
}
@chmod($path, 0664);
return $path;
}
}
@@ -0,0 +1,319 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>合塑实业-汽车事业部-备货单(客户下定)</title>
<style>
/* 横版:A4 297mm,左右各 12mm => 可用宽度 273mm */
@page { margin: 12mm; }
body {
font-family: 'notosanssc', 'DejaVu Sans', Arial, sans-serif;
font-size: 10pt;
line-height: 1.3;
color: #000;
background: #fff;
}
.container {
width: 273mm; /* 横向版心 */
margin: 0 auto;
background: #fff;
}
.header {
text-align: center;
margin-bottom: 10pt;
padding-bottom: 6pt;
}
.title {
font-size: 14pt;
font-weight: bold;
margin-bottom: 6pt;
}
.sub-title {
font-size: 9pt;
color: #000;
margin: 0 6pt;
}
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 10pt;
font-size: 9pt;
table-layout: fixed; /* 横向固定布局,配合 colgroup 更稳 */
}
th, td {
border: 0.5pt solid #000;
padding: 6pt 6pt;
text-align: center;
vertical-align: middle;
word-break: break-all;
}
th {
background-color: #f0f0f0;
font-weight: bold;
color: #000;
}
/* 跨页友好 */
thead { display: table-header-group; }
tfoot { display: table-row-group; }
tr, img { page-break-inside: avoid; }
/* 信息类表格左对齐 */
.info-table td,
.logistics-table td { text-align: left; }
.info-label { font-weight: bold; color: #000; }
.info-value { color: #000; }
.summary-row {
background-color: #f0f0f0;
font-weight: bold;
}
.payment-title {
background-color: #f0f0f0;
font-weight: bold;
text-align: center;
vertical-align: middle;
white-space: nowrap;
}
.nowrap { white-space: nowrap; }
</style>
</head>
<body>
<div class="container">
<!-- 头部 -->
<div class="header">
<h1 class="title">合塑实业-汽车事业部-备货单(客户下定)</h1>
<div>
<span class="sub-title">备货单号: <?php echo htmlspecialchars($data['order_no'] ?? 'N/A'); ?></span>
<span class="sub-title">业务员: <?php echo htmlspecialchars($data['salesperson_snapshot'] ?? ' - '); ?></span>
</div>
</div>
<!-- 基本信息 -->
<table class="info-table">
<colgroup>
<col style="width:18%">
<col style="width:15%">
<col style="width:14%">
<col style="width:14%">
<col style="width:19%">
<col style="width:20%">
</colgroup>
<tbody>
<tr>
<td><span class="info-label">阿里订单号/PI号:</span> <span class="info-value"><?php echo htmlspecialchars($data['pi_no'] ?? ' - '); ?></span></td>
<td><span class="info-label">日期:</span> <span class="info-value nowrap"><?php echo htmlspecialchars($data['biz_date'] ?? ' - '); ?></span></td>
<td><span class="info-label">关目的国:</span> <span class="info-value"><?php echo htmlspecialchars($data['clearance_country_snapshot'] ?? ' - '); ?></span></td>
<td><span class="info-label">客户来源:</span> <span class="info-value"><?php echo htmlspecialchars($data['customer_source_snapshot'] ?? ' - '); ?></span></td>
<td><span class="info-label">客户名称:</span> <span class="info-value"><?php echo htmlspecialchars($data['customer_snapshot'] ?? ' - '); ?></span></td>
<td><span class="info-label">客户所在国:</span> <span class="info-value"><?php echo htmlspecialchars($data['customer_country_snapshot'] ?? ' - '); ?></span></td>
</tr>
</tbody>
</table>
<!-- 产品列表 -->
<table class="product-table">
<colgroup>
<col style="width:6%">
<col style="width:34%">
<col style="width:6%">
<col style="width:6%">
<col style="width:6%">
<col style="width:14%">
<col style="width:8%">
<col style="width:8%">
<col style="width:6%">
<col style="width:6%">
</colgroup>
<thead>
<tr>
<th>编号</th>
<th>车型</th>
<th>数量/台</th>
<th>颜色/外</th>
<th>内饰</th>
<th>改装</th>
<th class="nowrap">官方指导价/二手挂牌价</th>
<th class="nowrap">优惠</th>
<th class="nowrap">单价</th>
<th class="nowrap">总价</th>
<th class="nowrap">利润</th>
</tr>
</thead>
<tbody>
<?php foreach (($data['veh_list'] ?? []) as $vehicle): ?>
<tr>
<td><?php echo htmlspecialchars($vehicle['index'] ?? ' - '); ?></td>
<td>
<?php echo htmlspecialchars($vehicle['model_snapshot'] ?? ' - '); ?>
<?php if ($vehicle['mileage'] > 0): ?>
<span class="nowrap">(<?php echo htmlspecialchars($vehicle['mileage']).'km'; ?>)</span>
<?php endif; ?>
</td>
<td><?php echo htmlspecialchars($vehicle['num'] ?? ' - '); ?></td>
<td><?php echo htmlspecialchars($vehicle['color_exterior_snapshot'] ?? ' - '); ?></td>
<td><?php echo htmlspecialchars($vehicle['color_interior_snapshot'] ?? ' - '); ?></td>
<td><?php echo htmlspecialchars($vehicle['modify'] ?? ' - '); ?></td>
<?php if ($vehicle['mileage'] > 0): ?>
<td class="nowrap"><?php echo htmlspecialchars($vehicle['listing_price_text'] ?? ' - '); ?></td>
<td class="nowrap"><?php echo htmlspecialchars($vehicle['reduction_price_text'] ?? ' - '); ?></td>
<?php else: ?>
<td class="nowrap"><?php echo htmlspecialchars($vehicle['official_price_text'] ?? ' - '); ?></td>
<td class="nowrap"><?php echo htmlspecialchars($vehicle['discount_price_text'] ?? ' - '); ?></td>
<?php endif; ?>
<td class="nowrap"><?php echo htmlspecialchars($vehicle['unit_price_text'] ?? ' - '); ?></td>
<td class="nowrap"><?php echo htmlspecialchars($vehicle['total_price_text'] ?? ' - '); ?></td>
<td class="nowrap"><?php echo htmlspecialchars($vehicle['profit_text'] ?? ' - '); ?></td>
</tr>
<?php endforeach; ?>
<tr class="summary-row">
<td colspan="6" style="text-align:left;">
<span class="info-label">合同汇率:</span> <span class="info-value"><?php echo htmlspecialchars($data['contract_exchange_rate'] ?? ' - '); ?></span>
</td>
<td colspan="5" style="text-align:left;">
<span class="info-label">合计到岸价:</span> <span class="info-value"><?php echo htmlspecialchars($data['cif_total_amount'] ?? ' - '); ?></span>
</td>
</tr>
</tbody>
</table>
<!-- 物流信息 -->
<table class="logistics-table">
<colgroup>
<col style="width:16%">
<col style="width:18%">
<col style="width:12%">
<col style="width:18%">
<col style="width:22%">
<col style="width:14%">
</colgroup>
<tbody>
<tr>
<td><span class="info-label">清关人:</span> <span class="info-value"><?php echo htmlspecialchars($data['declarant_name'] ?? ' - '); ?></span></td>
<?php if ($data['is_final'] == 1): ?>
<td><span class="info-label">全款收齐日:</span> <span class="info-value nowrap"><?php echo htmlspecialchars($data['collect_all_date'] ?? ' - '); ?></span></td>
<?php else: ?>
<td><span class="info-label">预计发运日期:</span> <span class="info-value nowrap"><?php echo htmlspecialchars($data['eta_ship_date'] ?? ' - '); ?></span></td>
<?php endif; ?>
<td><span class="info-label">出运方式:</span> <span class="info-value"><?php echo htmlspecialchars($data['shipping_method_snapshot'] ?? ' - '); ?></span></td>
<td><span class="info-label">国内收货港:</span> <span class="info-value"><?php echo htmlspecialchars($data['domestic_port_snapshot'] ?? ' - '); ?></span></td>
<td><span class="info-label">贸易条款/清关目的港:</span> <span class="info-value"><?php echo htmlspecialchars($data['trade_term_snapshot'] ?? ' - '); ?>/<?php echo htmlspecialchars($data['clearance_port_snapshot'] ?? ' - '); ?></span></td>
<?php if ($data['is_final'] == 1): ?>
<td><span class="info-label">提车日期:</span> <span class="info-value nowrap"><?php echo htmlspecialchars($data['pick_up_date'] ?? ' - '); ?></span></td>
<?php else: ?>
<td><span class="info-label">最迟到港时间:</span> <span class="info-value nowrap"><?php echo htmlspecialchars($data['latest_arrival_date'] ?? ' - '); ?></span></td>
<?php endif; ?>
</tr>
</tbody>
</table>
<!-- 计划收款信息 -->
<table class="payment-table">
<colgroup>
<col style="width:12%">
<col style="width:10%">
<col style="width:14%">
<col style="width:12%">
<col style="width:12%">
<col style="width:20%">
<col style="width:10%">
</colgroup>
<thead>
<tr>
<th style="width:24mm" rowspan="<?php echo htmlspecialchars($data['acc_index']); ?>">计划收款信息</th>
<th>序号</th>
<th>汇款国</th>
<th>日期</th>
<th>币种</th>
<th>账户</th>
<th>汇款人</th>
<th>金额</th>
</tr>
</thead>
<tbody>
<?php foreach (($data['account_list'] ?? []) as $account): ?>
<tr>
<td class="nowrap"><?php echo htmlspecialchars($account['index'] ?? ' - '); ?></td>
<td><?php echo htmlspecialchars($account['remit_country_name_snapshot'] ?? ' - '); ?></td>
<td class="nowrap"><?php echo htmlspecialchars($account['remit_date'] ?? ' - '); ?></td>
<td class="nowrap"><?php echo htmlspecialchars($account['currency_name_snapshot'] ?? ' - '); ?></td>
<td class="nowrap"><?php echo htmlspecialchars($account['account_name_snapshot'] ?? ' - '); ?></td>
<td style="text-align:left;"><?php echo htmlspecialchars($account['remitter_name'] ?? ' - '); ?></td>
<td class="nowrap"><?php echo htmlspecialchars($account['amount'] ?? ' - '); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<!-- PDF 底部签名区 -->
<div style="margin-top: 6mm; font-size: 12px;">
<!-- 左侧:业务员签字 / 备发货负责人签字 -->
<div style="width: 50%; float: left; text-align: left;">
<!-- 行距通过 margin-bottom 控制;行内元素通过固定行高和 vertical-align: middle 垂直居中 -->
<div style="height: 14mm; line-height: 14mm; margin-bottom: 5mm;">
<span>业务员签字:</span>
<?php if (!empty($data['sign_img_url'])): ?>
<img src="<?php echo htmlspecialchars($data['sign_img_url']); ?>" style="height: 12mm; vertical-align: middle;">
<?php else: ?>
<span style="display:inline-block; width:21mm; height:12mm; vertical-align: middle; border-bottom:0.2mm solid #000;"></span>
<?php endif; ?>
</div>
<div style="height: 14mm; line-height: 14mm;">
<span>备发货负责人签字:</span>
<?php if (!empty($data['sign_img_url'])): ?>
<img src="<?php echo htmlspecialchars($data['sign_img_url']); ?>" style="height: 12mm; vertical-align: middle;">
<?php else: ?>
<span style="display:inline-block; width:21mm; height:12mm; vertical-align: middle; border-bottom:0.2mm solid #000;"></span>
<?php endif; ?>
</div>
</div>
<!-- 右侧:组长签字 / 日期 -->
<div style="width: 50%; float: right; text-align: left;">
<div style="height: 14mm; line-height: 14mm; margin-bottom: 5mm;">
<span>组长签字:</span>
<?php if (!empty($data['sign_img_url'])): ?>
<img src="<?php echo htmlspecialchars($data['sign_img_url']); ?>" style="height: 12mm; vertical-align: middle;">
<?php else: ?>
<span style="display:inline-block; width:21mm; height:12mm; vertical-align: middle; border-bottom:0.2mm solid #000;"></span>
<?php endif; ?>
</div>
<div style="height: 14mm; line-height: 14mm;">
<span>日期:</span>
<?php
$_dt = $data['signed_date'] ?? ($data['created_at'] ?? '');
if (is_numeric($_dt)) {
echo htmlspecialchars(date('Y-m-d', (int)$_dt));
} elseif (!empty($_dt)) {
$t = strtotime($_dt);
echo $t ? htmlspecialchars(date('Y-m-d', $t)) : ' - ';
} else {
echo ' - ';
}
?>
</div>
</div>
<div style="clear: both;"></div>
</div>
</div>
</body>
</html>
@@ -0,0 +1,319 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>合塑实业-汽车事业部-备货单(客户下定)</title>
<style>
/* 横版:A4 297mm,左右各 12mm => 可用宽度 273mm */
@page { margin: 12mm; }
body {
font-family: 'notosanssc', 'DejaVu Sans', Arial, sans-serif;
font-size: 10pt;
line-height: 1.3;
color: #000;
background: #fff;
}
.container {
width: 273mm; /* 横向版心 */
margin: 0 auto;
background: #fff;
}
.header {
text-align: center;
margin-bottom: 10pt;
padding-bottom: 6pt;
}
.title {
font-size: 14pt;
font-weight: bold;
margin-bottom: 6pt;
}
.sub-title {
font-size: 9pt;
color: #000;
margin: 0 6pt;
}
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 10pt;
font-size: 9pt;
table-layout: fixed; /* 横向固定布局,配合 colgroup 更稳 */
}
th, td {
border: 0.5pt solid #000;
padding: 6pt 6pt;
text-align: center;
vertical-align: middle;
word-break: break-all;
}
th {
background-color: #f0f0f0;
font-weight: bold;
color: #000;
}
/* 跨页友好 */
thead { display: table-header-group; }
tfoot { display: table-row-group; }
tr, img { page-break-inside: avoid; }
/* 信息类表格左对齐 */
.info-table td,
.logistics-table td { text-align: left; }
.info-label { font-weight: bold; color: #000; }
.info-value { color: #000; }
.summary-row {
background-color: #f0f0f0;
font-weight: bold;
}
.payment-title {
background-color: #f0f0f0;
font-weight: bold;
text-align: center;
vertical-align: middle;
white-space: nowrap;
}
.nowrap { white-space: nowrap; }
</style>
</head>
<body>
<div class="container">
<!-- 头部 -->
<div class="header">
<h1 class="title">合塑实业-汽车事业部-备货单(客户下定)</h1>
<div>
<span class="sub-title">备货单号: <?php echo htmlspecialchars($data['order_no'] ?? 'N/A'); ?></span>
<span class="sub-title">业务员: <?php echo htmlspecialchars($data['salesperson_snapshot'] ?? ' - '); ?></span>
</div>
</div>
<!-- 基本信息 -->
<table class="info-table">
<colgroup>
<col style="width:18%">
<col style="width:15%">
<col style="width:14%">
<col style="width:14%">
<col style="width:19%">
<col style="width:20%">
</colgroup>
<tbody>
<tr>
<td><span class="info-label">阿里订单号/PI号:</span> <span class="info-value"><?php echo htmlspecialchars($data['pi_no'] ?? ' - '); ?></span></td>
<td><span class="info-label">日期:</span> <span class="info-value nowrap"><?php echo htmlspecialchars($data['biz_date'] ?? ' - '); ?></span></td>
<td><span class="info-label">关目的国:</span> <span class="info-value"><?php echo htmlspecialchars($data['clearance_country_snapshot'] ?? ' - '); ?></span></td>
<td><span class="info-label">客户来源:</span> <span class="info-value"><?php echo htmlspecialchars($data['customer_source_snapshot'] ?? ' - '); ?></span></td>
<td><span class="info-label">客户名称:</span> <span class="info-value"><?php echo htmlspecialchars($data['customer_snapshot'] ?? ' - '); ?></span></td>
<td><span class="info-label">客户所在国:</span> <span class="info-value"><?php echo htmlspecialchars($data['customer_country_snapshot'] ?? ' - '); ?></span></td>
</tr>
</tbody>
</table>
<!-- 产品列表 -->
<table class="product-table">
<colgroup>
<col style="width:6%">
<col style="width:34%">
<col style="width:6%">
<col style="width:6%">
<col style="width:6%">
<col style="width:14%">
<col style="width:8%">
<col style="width:8%">
<col style="width:6%">
<col style="width:6%">
</colgroup>
<thead>
<tr>
<th>编号</th>
<th>车型</th>
<th>数量/台</th>
<th>颜色/外</th>
<th>内饰</th>
<th>改装</th>
<th class="nowrap">官方指导价/二手挂牌价</th>
<th class="nowrap">优惠</th>
<th class="nowrap">单价</th>
<th class="nowrap">总价</th>
<th class="nowrap">利润</th>
</tr>
</thead>
<tbody>
<?php foreach (($data['veh_list'] ?? []) as $vehicle): ?>
<tr>
<td><?php echo htmlspecialchars($vehicle['index'] ?? ' - '); ?></td>
<td>
<?php echo htmlspecialchars($vehicle['model_snapshot'] ?? ' - '); ?>
<?php if ($vehicle['mileage'] > 0): ?>
<span class="nowrap">(<?php echo htmlspecialchars($vehicle['mileage']).'km'; ?>)</span>
<?php endif; ?>
</td>
<td><?php echo htmlspecialchars($vehicle['num'] ?? ' - '); ?></td>
<td><?php echo htmlspecialchars($vehicle['color_exterior_snapshot'] ?? ' - '); ?></td>
<td><?php echo htmlspecialchars($vehicle['color_interior_snapshot'] ?? ' - '); ?></td>
<td><?php echo htmlspecialchars($vehicle['modify'] ?? ' - '); ?></td>
<?php if ($vehicle['mileage'] > 0): ?>
<td class="nowrap"><?php echo htmlspecialchars($vehicle['listing_price']['currency_info'] ?? ' - '); ?></td>
<td class="nowrap"><?php echo htmlspecialchars($vehicle['reduction_price']['currency_info'] ?? ' - '); ?></td>
<?php else: ?>
<td class="nowrap"><?php echo htmlspecialchars($vehicle['official_price']['currency_info'] ?? ' - '); ?></td>
<td class="nowrap"><?php echo htmlspecialchars($vehicle['discount_price']['currency_info'] ?? ' - '); ?></td>
<?php endif; ?>
<td class="nowrap"><?php echo htmlspecialchars($vehicle['unit_price']['currency_info'] ?? ' - '); ?></td>
<td class="nowrap"><?php echo htmlspecialchars($vehicle['total_price']['currency_info'] ?? ' - '); ?></td>
<td class="nowrap"><?php echo htmlspecialchars($vehicle['profit']['currency_info'] ?? ' - '); ?></td>
</tr>
<?php endforeach; ?>
<tr class="summary-row">
<td colspan="6" style="text-align:left;">
<span class="info-label">合同汇率:</span> <span class="info-value"><?php echo htmlspecialchars($data['contract_exchange_rate'] ?? ' - '); ?></span>
</td>
<td colspan="5" style="text-align:left;">
<span class="info-label">合计到岸价:</span> <span class="info-value"><?php echo htmlspecialchars($data['cif_total_amount']['currency_info'] ?? ' - '); ?></span>
</td>
</tr>
</tbody>
</table>
<!-- 物流信息 -->
<table class="logistics-table">
<colgroup>
<col style="width:16%">
<col style="width:18%">
<col style="width:12%">
<col style="width:18%">
<col style="width:22%">
<col style="width:14%">
</colgroup>
<tbody>
<tr>
<td><span class="info-label">清关人:</span> <span class="info-value"><?php echo htmlspecialchars($data['declarant_name'] ?? ' - '); ?></span></td>
<?php if ($data['is_final'] == 1): ?>
<td><span class="info-label">全款收齐日:</span> <span class="info-value nowrap"><?php echo htmlspecialchars($data['collect_all_date'] ?? ' - '); ?></span></td>
<?php else: ?>
<td><span class="info-label">预计发运日期:</span> <span class="info-value nowrap"><?php echo htmlspecialchars($data['eta_ship_date'] ?? ' - '); ?></span></td>
<?php endif; ?>
<td><span class="info-label">出运方式:</span> <span class="info-value"><?php echo htmlspecialchars($data['shipping_method_snapshot'] ?? ' - '); ?></span></td>
<td><span class="info-label">国内收货港:</span> <span class="info-value"><?php echo htmlspecialchars($data['domestic_port_snapshot'] ?? ' - '); ?></span></td>
<td><span class="info-label">贸易条款/清关目的港:</span> <span class="info-value"><?php echo htmlspecialchars($data['trade_term_snapshot'] ?? ' - '); ?>/<?php echo htmlspecialchars($data['clearance_port_snapshot'] ?? ' - '); ?></span></td>
<?php if ($data['is_final'] == 1): ?>
<td><span class="info-label">提车日期:</span> <span class="info-value nowrap"><?php echo htmlspecialchars($data['pick_up_date'] ?? ' - '); ?></span></td>
<?php else: ?>
<td><span class="info-label">最迟到港时间:</span> <span class="info-value nowrap"><?php echo htmlspecialchars($data['latest_arrival_date'] ?? ' - '); ?></span></td>
<?php endif; ?>
</tr>
</tbody>
</table>
<!-- 计划收款信息 -->
<table class="payment-table">
<colgroup>
<col style="width:12%">
<col style="width:10%">
<col style="width:14%">
<col style="width:12%">
<col style="width:12%">
<col style="width:20%">
<col style="width:10%">
</colgroup>
<thead>
<tr>
<th style="width:24mm" rowspan="<?php echo htmlspecialchars($data['acc_index']); ?>">计划收款信息</th>
<th>序号</th>
<th>汇款国</th>
<th>日期</th>
<th>币种</th>
<th>账户</th>
<th>汇款人</th>
<th>金额</th>
</tr>
</thead>
<tbody>
<?php foreach (($data['account_list'] ?? []) as $account): ?>
<tr>
<td class="nowrap"><?php echo htmlspecialchars($account['index'] ?? ' - '); ?></td>
<td><?php echo htmlspecialchars($account['remit_country_name_snapshot'] ?? ' - '); ?></td>
<td class="nowrap"><?php echo htmlspecialchars($account['remit_date'] ?? ' - '); ?></td>
<td class="nowrap"><?php echo htmlspecialchars($account['currency_name_snapshot'] ?? ' - '); ?></td>
<td class="nowrap"><?php echo htmlspecialchars($account['account_name_snapshot'] ?? ' - '); ?></td>
<td style="text-align:left;"><?php echo htmlspecialchars($account['remitter_name'] ?? ' - '); ?></td>
<td class="nowrap"><?php echo htmlspecialchars($account['amount'] ?? ' - '); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<!-- PDF 底部签名区 -->
<div style="margin-top: 6mm; font-size: 12px;">
<!-- 左侧:业务员签字 / 备发货负责人签字 -->
<div style="width: 50%; float: left; text-align: left;">
<!-- 行距通过 margin-bottom 控制;行内元素通过固定行高和 vertical-align: middle 垂直居中 -->
<div style="height: 14mm; line-height: 14mm; margin-bottom: 5mm;">
<span>业务员签字:</span>
<?php if (!empty($data['sign_img_url'])): ?>
<img src="<?php echo htmlspecialchars($data['sign_img_url']); ?>" style="height: 12mm; vertical-align: middle;">
<?php else: ?>
<span style="display:inline-block; width:21mm; height:12mm; vertical-align: middle; border-bottom:0.2mm solid #000;"></span>
<?php endif; ?>
</div>
<div style="height: 14mm; line-height: 14mm;">
<span>备发货负责人签字:</span>
<?php if (!empty($data['sign_imgs'][1])): ?>
<img src="<?php echo htmlspecialchars($data['sign_imgs'][1]); ?>" style="height: 12mm; vertical-align: middle;">
<?php else: ?>
<span style="display:inline-block; width:21mm; height:12mm; vertical-align: middle; border-bottom:0.2mm solid #000;"></span>
<?php endif; ?>
</div>
</div>
<!-- 右侧:组长签字 / 日期 -->
<div style="width: 50%; float: right; text-align: left;">
<div style="height: 14mm; line-height: 14mm; margin-bottom: 5mm;">
<span>组长签字:</span>
<?php if (!empty($data['sign_imgs'][0])): ?>
<img src="<?php echo htmlspecialchars($data['sign_imgs'][0]); ?>" style="height: 12mm; vertical-align: middle;">
<?php else: ?>
<span style="display:inline-block; width:21mm; height:12mm; vertical-align: middle; border-bottom:0.2mm solid #000;"></span>
<?php endif; ?>
</div>
<div style="height: 14mm; line-height: 14mm;">
<span>日期:</span>
<?php
$_dt = $data['signed_date'] ?? ($data['created_at'] ?? '');
if (is_numeric($_dt)) {
echo htmlspecialchars(date('Y-m-d', (int)$_dt));
} elseif (!empty($_dt)) {
$t = strtotime($_dt);
echo $t ? htmlspecialchars(date('Y-m-d', $t)) : ' - ';
} else {
echo ' - ';
}
?>
</div>
</div>
<div style="clear: both;"></div>
</div>
</div>
</body>
</html>
@@ -0,0 +1,255 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>合塑实业-汽车事业部-备货单(客户下定)</title>
<style>
/* 横版:A4 297mm,左右各 12mm => 可用宽度 273mm */
@page { margin: 12mm; }
body {
font-family: 'notosanssc', 'DejaVu Sans', Arial, sans-serif;
font-size: 10pt;
line-height: 1.3;
color: #000;
background: #fff;
}
.container {
width: 273mm; /* 横向版心 */
margin: 0 auto;
background: #fff;
}
.header {
text-align: center;
margin-bottom: 10pt;
padding-bottom: 6pt;
}
.title {
font-size: 14pt;
font-weight: bold;
margin-bottom: 6pt;
}
.sub-title {
font-size: 9pt;
color: #000;
margin: 0 6pt;
}
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 10pt;
font-size: 9pt;
table-layout: fixed; /* 横向固定布局,配合 colgroup 更稳 */
}
th, td {
border: 0.5pt solid #000;
padding: 6pt 6pt;
text-align: center;
vertical-align: middle;
word-break: break-all;
}
th {
background-color: #f0f0f0;
font-weight: bold;
color: #000;
}
/* 跨页友好 */
thead { display: table-header-group; }
tfoot { display: table-row-group; }
tr, img { page-break-inside: avoid; }
/* 信息类表格左对齐 */
.info-table td,
.logistics-table td { text-align: left; }
.info-label { font-weight: bold; color: #000; }
.info-value { color: #000; }
.summary-row {
background-color: #f0f0f0;
font-weight: bold;
}
.payment-title {
background-color: #f0f0f0;
font-weight: bold;
text-align: center;
vertical-align: middle;
white-space: nowrap;
}
.nowrap { white-space: nowrap; }
</style>
</head>
<body>
<div class="container">
<!-- 头部 -->
<div class="header">
<h1 class="title">合塑实业-汽车事业部-备货单(客户下定)</h1>
<div>
<span class="sub-title">备货单号: <?php echo htmlspecialchars($data['order_no'] ?? 'N/A'); ?></span>
<span class="sub-title">业务员: <?php echo htmlspecialchars($data['salesperson_snapshot'] ?? ' - '); ?></span>
</div>
</div>
<!-- 基本信息 -->
<table class="info-table">
<colgroup>
<col style="width:18%">
<col style="width:15%">
<col style="width:14%">
<col style="width:14%">
<col style="width:19%">
<col style="width:20%">
</colgroup>
<tbody>
<tr>
<td><span class="info-label">阿里订单号/PI号:</span> <span class="info-value"><?php echo htmlspecialchars($data['pi_no'] ?? ' - '); ?></span></td>
<td><span class="info-label">日期:</span> <span class="info-value nowrap"><?php echo htmlspecialchars($data['biz_date'] ?? ' - '); ?></span></td>
<td><span class="info-label">关目的国:</span> <span class="info-value"><?php echo htmlspecialchars($data['clearance_country_snapshot'] ?? ' - '); ?></span></td>
<td><span class="info-label">客户来源:</span> <span class="info-value"><?php echo htmlspecialchars($data['customer_source_snapshot'] ?? ' - '); ?></span></td>
<td><span class="info-label">客户名称:</span> <span class="info-value"><?php echo htmlspecialchars($data['customer_snapshot'] ?? ' - '); ?></span></td>
<td><span class="info-label">客户所在国:</span> <span class="info-value"><?php echo htmlspecialchars($data['customer_country_snapshot'] ?? ' - '); ?></span></td>
</tr>
</tbody>
</table>
<!-- 产品列表 -->
<table class="product-table">
<colgroup>
<col style="width:6%">
<col style="width:34%">
<col style="width:6%">
<col style="width:6%">
<col style="width:6%">
<col style="width:14%">
<col style="width:8%">
<col style="width:8%">
<col style="width:6%">
<col style="width:6%">
</colgroup>
<thead>
<tr>
<th>编号</th>
<th>车型</th>
<th>数量/台</th>
<th>颜色/外</th>
<th>内饰</th>
<th>改装</th>
<th class="nowrap">单价</th>
<th class="nowrap">总价</th>
<th class="nowrap">官方指导价/二手挂牌价</th>
<th class="nowrap">优惠/二手减价</th>
</tr>
</thead>
<tbody>
<?php foreach (($data['veh_list'] ?? []) as $vehicle): ?>
<tr>
<td><?php echo htmlspecialchars($vehicle['index'] ?? ' - '); ?></td>
<td><?php echo htmlspecialchars($vehicle['model_snapshot'] ?? ' - '); ?></td>
<td><?php echo htmlspecialchars($vehicle['num'] ?? ' - '); ?></td>
<td><?php echo htmlspecialchars($vehicle['color_exterior_snapshot'] ?? ' - '); ?></td>
<td><?php echo htmlspecialchars($vehicle['color_interior_snapshot'] ?? ' - '); ?></td>
<td><?php echo htmlspecialchars($vehicle['modify'] ?? ' - '); ?></td>
<td class="nowrap"><?php echo htmlspecialchars($vehicle['unit_price_text'] ?? ' - '); ?></td>
<td class="nowrap"><?php echo htmlspecialchars($vehicle['total_price_text'] ?? ' - '); ?></td>
<?php if ($vehicle['mileage'] > 0): ?>
<td class="nowrap"><?php echo htmlspecialchars($vehicle['listing_price'] ?? ' - '); ?></td>
<td class="nowrap"><?php echo htmlspecialchars($vehicle['reduction_price'] ?? ' - '); ?></td>
<?php else: ?>
<td class="nowrap"><?php echo htmlspecialchars($vehicle['official_price'] ?? ' - '); ?></td>
<td class="nowrap"><?php echo htmlspecialchars($vehicle['discount_price'] ?? ' - '); ?></td>
<?php endif; ?>
</tr>
<?php endforeach; ?>
<tr class="summary-row">
<td colspan="6" style="text-align:left;">
<span class="info-label">合同汇率:</span> <span class="info-value"><?php echo htmlspecialchars($data['contract_exchange_rate'] ?? ' - '); ?></span>
</td>
<td colspan="4" style="text-align:left;">
<span class="info-label">合计到岸价:</span> <span class="info-value"><?php echo htmlspecialchars($data['cif_total_amount'] ?? ' - '); ?></span>
</td>
</tr>
</tbody>
</table>
<!-- 物流信息 -->
<table class="logistics-table">
<colgroup>
<col style="width:16%">
<col style="width:18%">
<col style="width:12%">
<col style="width:18%">
<col style="width:22%">
<col style="width:14%">
</colgroup>
<tbody>
<tr>
<td><span class="info-label">清关人:</span> <span class="info-value"><?php echo htmlspecialchars($data['declarant_name'] ?? ' - '); ?></span></td>
<td><span class="info-label">预计发运日期:</span> <span class="info-value nowrap"><?php echo htmlspecialchars($data['eta_ship_date'] ?? ' - '); ?></span></td>
<td><span class="info-label">出运方式:</span> <span class="info-value"><?php echo htmlspecialchars($data['shipping_method_snapshot'] ?? ' - '); ?></span></td>
<td><span class="info-label">国内收货港:</span> <span class="info-value"><?php echo htmlspecialchars($data['domestic_port_snapshot'] ?? ' - '); ?></span></td>
<td><span class="info-label">贸易条款/清关目的港:</span> <span class="info-value"><?php echo htmlspecialchars($data['trade_term_snapshot'] ?? ' - '); ?>/<?php echo htmlspecialchars($data['clearance_port_snapshot'] ?? ' - '); ?></span></td>
<td><span class="info-label">最迟到港时间:</span> <span class="info-value nowrap"><?php echo htmlspecialchars($data['latest_arrival_date'] ?? ' - '); ?></span></td>
</tr>
</tbody>
</table>
<!-- 计划收款信息 -->
<table class="payment-table">
<colgroup>
<col style="width:12%">
<col style="width:10%">
<col style="width:14%">
<col style="width:12%">
<col style="width:12%">
<col style="width:20%">
<col style="width:10%">
</colgroup>
<thead>
<tr>
<th style="width:24mm" rowspan="<?php echo htmlspecialchars($data['acc_index']); ?>">计划收款信息</th>
<th>序号</th>
<th>汇款国</th>
<th>日期</th>
<th>币种</th>
<th>账户</th>
<th>汇款人</th>
<th>金额</th>
</tr>
</thead>
<tbody>
<?php foreach (($data['account_list'] ?? []) as $account): ?>
<tr>
<td class="nowrap"><?php echo htmlspecialchars($account['index'] ?? ' - '); ?></td>
<td><?php echo htmlspecialchars($account['remit_country_name_snapshot'] ?? ' - '); ?></td>
<td class="nowrap"><?php echo htmlspecialchars($account['remit_date'] ?? ' - '); ?></td>
<td class="nowrap"><?php echo htmlspecialchars($account['currency_name_snapshot'] ?? ' - '); ?></td>
<td class="nowrap"><?php echo htmlspecialchars($account['account_name_snapshot'] ?? ' - '); ?></td>
<td style="text-align:left;"><?php echo htmlspecialchars($account['remitter_name'] ?? ' - '); ?></td>
<td class="nowrap"><?php echo htmlspecialchars($account['amount'] ?? ' - '); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<div class="" style="text-align:right;">
<div style="padding-top: 6mm; padding-bottom: 6mm; display: table; margin: 0 0 0 auto;">
<div style="display: table-cell; vertical-align: middle;">
<span>下单人:</span>
<img src="<?php echo htmlspecialchars($data['sign_img_url'] ?? ' - '); ?>" style="width: 21mm; height: 12mm; vertical-align: middle;">
</div>
</div>
<div>下单时间:<?php echo htmlspecialchars($data['created_at'] ?? ' - '); ?></div>
</div>
</div>
</body>
</html>
+4 -4
View File
@@ -156,11 +156,11 @@ class Generate
protected function deleteFileIfExists($path)
{
if (file_exists($path)) {
"文件: <comment>{$path}</comment> 已存在,核对后手动删除。\n";
return;
// "文件: <comment>{$path}</comment> 已存在,核对后手动删除。\n";
// return;
// unlink($path);
// echo "删除已存在文件: <comment>{$path}</comment> \n";
unlink($path);
echo "删除已存在文件: <comment>{$path}</comment> \n";
}
}

Some files were not shown because too many files have changed in this diff Show More