Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4ec46704b2 | |||
| 80e1a170e5 | |||
| f2196809ea | |||
| 05675471ab | |||
| c77da51775 | |||
| 3d51a841cf |
@@ -1,56 +0,0 @@
|
||||
<?php
|
||||
namespace app\admin\controller;
|
||||
|
||||
use app\common\controller\BaseController;
|
||||
use app\common\traits\crud\CrudControllerTrait;
|
||||
use app\common\service\WordpressSiteService;
|
||||
use support\Request;
|
||||
|
||||
class WordpressSiteController extends BaseController
|
||||
{
|
||||
use CrudControllerTrait;
|
||||
|
||||
protected $service;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->service = new WordpressSiteService();
|
||||
}
|
||||
|
||||
//推送,在wp站点上创建产品
|
||||
public function createProduct(Request $request){
|
||||
try {
|
||||
$domain = $request->get('domain');
|
||||
$id = $request->get('id');
|
||||
$res = $this->service->createProduct($domain,$id);
|
||||
return $this->success($res, '推送成功');
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage().'-'.$e->getLine().'-'.$e->getFile());
|
||||
}
|
||||
}
|
||||
|
||||
public function uploadImageToWp(Request $request){
|
||||
try {
|
||||
$domain = $request->get('domain');
|
||||
$res = $this->service->uploadImageToWp($domain);
|
||||
return $this->success($res, '推送成功');
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表页数据格式化
|
||||
*
|
||||
* @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;
|
||||
// }
|
||||
}
|
||||
@@ -67,14 +67,14 @@ class AuthController extends BaseController
|
||||
try {
|
||||
$data = $request->all();
|
||||
|
||||
$data['login_type'] = $request->login_type;
|
||||
if($data['login_type'] == 'feishu' && (empty($data['feishu_code']) || empty($data['app_id']))){
|
||||
if(empty($data['feishu_code']) || empty($data['app_id'])){
|
||||
throw new BusinessException('参数错误');
|
||||
}
|
||||
|
||||
$result = (new AuthService())->login($data);
|
||||
return $this->success($result, '登录成功');
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
return $this->error($e->getMessage().$e->getLine().$e->getFile());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\controller;
|
||||
|
||||
use app\common\model\CorpFeishuApp;
|
||||
use app\common\service\AuthService;
|
||||
use app\common\validate\UserValidate;
|
||||
use support\exception\BusinessException;
|
||||
use support\Request;
|
||||
|
||||
class PresetController extends BaseController
|
||||
{
|
||||
|
||||
/**
|
||||
* 不需要头信息的方法
|
||||
*/
|
||||
protected $noNeedHeaderData = ['corps'];
|
||||
|
||||
protected $noNeedLogin = ['corps'];
|
||||
|
||||
|
||||
// 调试
|
||||
public function corps(Request $request)
|
||||
{
|
||||
try {
|
||||
$fsAppInfo = CorpFeishuApp::where('status', 1)->select(['id','name','app_id','r_url','m_url','state'])->get();
|
||||
return $this->success($fsAppInfo);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,10 +89,12 @@ class UploadController
|
||||
$originalName = $file->getUploadName();
|
||||
|
||||
$ossservice = new OssService();
|
||||
$mimes = $ossservice->getMimeTypes(['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'zip', 'rar']);
|
||||
|
||||
$result = Storage::adapter('oss')
|
||||
->path(config('oss.oss_main_dir') . $sceneUrl . date('Ymd'))
|
||||
->size(1024*1024*100)
|
||||
->size(1024*1024*50)
|
||||
->extYes($mimes)
|
||||
->upload($file);
|
||||
|
||||
|
||||
@@ -133,7 +135,7 @@ class UploadController
|
||||
'image/webp',
|
||||
];
|
||||
|
||||
$mime = $file->getMimeType();
|
||||
$mime = $file->getUploadMimeType();
|
||||
if (!in_array($mime, $allowMime, true)) {
|
||||
return $this->error('不支持的图片类型,支持jpeg、png、webp');
|
||||
}
|
||||
@@ -144,18 +146,29 @@ class UploadController
|
||||
mkdir($tmpDir, 0755, true);
|
||||
}
|
||||
|
||||
$tmpPath = $tmpDir . '/' . uniqid('img_', true);
|
||||
// 保留原始文件扩展名
|
||||
$originalName = $file->getUploadName();
|
||||
$extension = pathinfo($originalName, PATHINFO_EXTENSION);
|
||||
$tmpFileName = uniqid('img_', true) . ($extension ? '.' . $extension : '');
|
||||
$tmpPath = $tmpDir . '/' . $tmpFileName;
|
||||
$file->move($tmpPath);
|
||||
|
||||
// 压缩处理
|
||||
$finalPath = $this->compressImage($tmpPath, $mime);
|
||||
$finalPath = $this->compressImage($tmpPath, $originalName, $mime);
|
||||
|
||||
$result = Storage::adapter('oss')
|
||||
->path(config('oss.oss_main_dir') . $sceneUrl . date('Ymd'))
|
||||
->size(1024*1024*30)
|
||||
->extYes(['image/jpeg','image/png'])
|
||||
->extNo(['image/gif'])
|
||||
->upload($file);
|
||||
// $result = Storage::adapter('oss')
|
||||
// ->path(config('oss.oss_main_dir') . $sceneUrl . date('Ymd'))
|
||||
// ->size(1024*1024*30)
|
||||
//// ->extYes(['image/jpeg','image/png'])
|
||||
//// ->extNo(['image/gif'])
|
||||
// ->uploadFile($finalPath);
|
||||
|
||||
$param = [
|
||||
'scene' => $scene,
|
||||
'path' => $finalPath,
|
||||
'original_name' => $originalName
|
||||
];
|
||||
$result = (new UploadService())->uploadLocalFile($param);
|
||||
|
||||
// 清理临时文件
|
||||
@unlink($tmpPath);
|
||||
@@ -163,12 +176,12 @@ class UploadController
|
||||
@unlink($finalPath);
|
||||
}
|
||||
|
||||
$ossservice = new OssService();
|
||||
//$ossservice = new OssService();
|
||||
|
||||
$data = [
|
||||
'href' => $result->file_name,
|
||||
'href' => $result['file_name'],
|
||||
'alt' => '',
|
||||
'url' => $ossservice->generatePrivateUrl($result->file_name)
|
||||
'url' => $result['file_url']
|
||||
];
|
||||
|
||||
// 记录文件信息到数据库
|
||||
@@ -185,10 +198,22 @@ class UploadController
|
||||
}
|
||||
}
|
||||
|
||||
protected function compressImage(string $srcPath, string $mime): string
|
||||
protected function compressImage(string $srcPath, string $originalName, string $mime): string
|
||||
{
|
||||
// 不支持 imagick 直接返回原图
|
||||
if (!extension_loaded('imagick')) {
|
||||
// imagick 未安装时,确保返回的路径有扩展名
|
||||
$extension = pathinfo($srcPath, PATHINFO_EXTENSION);
|
||||
if (!$extension) {
|
||||
// 如果临时文件没有扩展名,从原始文件名获取
|
||||
$originalExtension = pathinfo($originalName, PATHINFO_EXTENSION);
|
||||
if ($originalExtension) {
|
||||
$newPath = $srcPath . '.' . $originalExtension;
|
||||
if (file_exists($srcPath)) {
|
||||
rename($srcPath, $newPath);
|
||||
return $newPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $srcPath;
|
||||
}
|
||||
|
||||
@@ -197,26 +222,28 @@ class UploadController
|
||||
$image->readImage($srcPath);
|
||||
|
||||
// 防止超大图
|
||||
$maxWidth = 2000;
|
||||
$maxHeight = 2000;
|
||||
$maxWidth = 1280;
|
||||
$maxHeight = 1280;
|
||||
|
||||
$image->thumbnailImage($maxWidth, $maxHeight, true);
|
||||
|
||||
// 自动方向修正(拍照图必备)
|
||||
if (method_exists($image, 'autoOrient')) {
|
||||
$image->autoOrient();
|
||||
}
|
||||
|
||||
// PNG / JPEG → WebP
|
||||
// 根据 MIME 决定输出格式
|
||||
if (in_array($mime, ['image/png', 'image/jpeg'], true)) {
|
||||
$image->setImageFormat('webp');
|
||||
$image->setImageCompressionQuality(80);
|
||||
|
||||
$dstPath = $srcPath . '.webp';
|
||||
// 保留原文件名 + 新扩展名
|
||||
$pathInfo = pathinfo($originalName);
|
||||
$dstName = $pathInfo['filename'] . '.webp';
|
||||
$dstPath = dirname($srcPath) . '/' . $dstName;
|
||||
|
||||
$image->writeImage($dstPath);
|
||||
} else {
|
||||
// 其他类型直接原样
|
||||
$dstPath = $srcPath;
|
||||
$dstPath = $srcPath; // GIF 或其他类型直接原样
|
||||
}
|
||||
|
||||
$image->clear();
|
||||
@@ -225,7 +252,18 @@ class UploadController
|
||||
return $dstPath;
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
// 压缩失败直接返回原图
|
||||
// 压缩失败时,确保返回的路径有扩展名
|
||||
$extension = pathinfo($srcPath, PATHINFO_EXTENSION);
|
||||
if (!$extension) {
|
||||
$originalExtension = pathinfo($originalName, PATHINFO_EXTENSION);
|
||||
if ($originalExtension) {
|
||||
$newPath = $srcPath . '.' . $originalExtension;
|
||||
if (file_exists($srcPath)) {
|
||||
rename($srcPath, $newPath);
|
||||
return $newPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $srcPath;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
namespace app\common\controller;
|
||||
|
||||
use app\common\controller\BaseController;
|
||||
use app\common\model\UsedCar;
|
||||
use app\common\model\UsedCarPdfLog;
|
||||
use app\common\model\User;
|
||||
@@ -8,17 +9,14 @@ use app\common\service\OssService;
|
||||
use app\common\service\SparkService;
|
||||
use app\common\traits\crud\CrudControllerTrait;
|
||||
use app\common\service\UsedCarService;
|
||||
use app\common\utils\crawl\service\service\UsedCarCrawlSingle;
|
||||
use app\common\utils\crawl\service\UnCodeFont;
|
||||
use app\common\utils\pdf\service\MpdfDriver;
|
||||
use app\queue\utils\XunfeiFanyi;
|
||||
use support\exception\BusinessException;
|
||||
use support\Log;
|
||||
use support\Request;
|
||||
|
||||
class UsedCarController extends BaseController
|
||||
{
|
||||
use CrudControllerTrait;
|
||||
use UnCodeFont;
|
||||
|
||||
protected $service;
|
||||
|
||||
@@ -37,7 +35,6 @@ class UsedCarController extends BaseController
|
||||
'RUB' => '₽',
|
||||
'AED' => 'د.إ',
|
||||
'EUR' => '€',
|
||||
'CNY' => '¥',
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
@@ -45,20 +42,6 @@ class UsedCarController extends BaseController
|
||||
$this->service = new UsedCarService();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新状态
|
||||
* @return \support\Response
|
||||
*/
|
||||
public function status(Request $request){
|
||||
try {
|
||||
$data = $request->post();
|
||||
$res = $this->service->serviceUpdateStatus($request, $data);
|
||||
return $this->success([], '更新成功');
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表页数据格式化
|
||||
*
|
||||
@@ -100,10 +83,8 @@ class UsedCarController extends BaseController
|
||||
$images = explode(',', $data['images']);
|
||||
$ossService = new OssService();
|
||||
$images_pull = [];
|
||||
//图片压缩参数 resize,w_1200:最大宽度1200px,quality,Q_50:图片质量如压缩到50%,format,webp:自动转 WebP
|
||||
$process = 'image/resize,w_800/quality,Q_40/format,webp';
|
||||
foreach ($images as $image){
|
||||
$images_pull[] = $ossService->generatePrivateUrlResize($image,3600,$process);
|
||||
$images_pull[] = $ossService->generatePrivateUrl($image);
|
||||
}
|
||||
$data['images_pull'] = $images_pull;
|
||||
|
||||
@@ -131,69 +112,6 @@ class UsedCarController extends BaseController
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移动端链接抓取
|
||||
* @param Request $request
|
||||
*/
|
||||
public function crawlMobileCar(Request $request)
|
||||
{
|
||||
try {
|
||||
$urlShare = $request->get('url');
|
||||
|
||||
$finalUrl = (new UsedCarCrawlSingle())->getFinaUrl($urlShare);
|
||||
|
||||
$request->setGet('url', $finalUrl);
|
||||
$data = $this->crawlCar($request);
|
||||
return $data;
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function crawlCar(Request $request)
|
||||
{
|
||||
try {
|
||||
$url = $request->get('url');
|
||||
if (empty($url)){
|
||||
throw new BusinessException('url不能为空');
|
||||
}
|
||||
//$info = (new UsedCarCrawlSingle())->crawlCar($url);
|
||||
$info = $this->service->selectCraw($url);
|
||||
$data = [
|
||||
'user_id' => $request->user['id'],
|
||||
'product_name' => $info['title'],
|
||||
'price' => $info['price']<200 ? ((float)$info['price'])*10000 : $info['price'],
|
||||
'source_place' => $info['source_place'],
|
||||
'displacement' => $info['capacity']??'', //排量
|
||||
'mileage' => ((float)$info['usage_mileage'])*10000, //里程
|
||||
'power' => $info['horsepower']??'', //马力
|
||||
'body_color' => $info['body_color'],
|
||||
'images' => $info['image'],
|
||||
'image_pull' => $info['image_pull'],
|
||||
'condition_desc' => $info['condition_desc'],
|
||||
'attachment' => '',
|
||||
'source_url' => $url,
|
||||
'crawl_car_id' => $info['car_id'],
|
||||
'crawl_title' => $info['car_name'],
|
||||
'crawl_brand_id' => $info['brand_id'],
|
||||
'crawl_brand' => $info['brand_name'],
|
||||
'crawl_series_id' => $info['series_id'],
|
||||
'crawl_series' => $info['series_name'],
|
||||
'used_car_id' => $info['used_car_id'],
|
||||
'crawl_year' => $info['year'],
|
||||
'plate_time' => $info['plate_time'],
|
||||
];
|
||||
|
||||
return $this->success($data);
|
||||
} catch (\Throwable $e) {
|
||||
if (str_contains($e->getMessage(), '404 Not Found')){
|
||||
return $this->error('车辆已下架或找不到页面');
|
||||
}
|
||||
Log::error('crawlCar fail',['param' => $request->get('url'),'message' => $e->getMessage().$e->getFile().$e->getLine()]);
|
||||
return $this->error($e->getMessage().$e->getFile().$e->getLine());
|
||||
}
|
||||
}
|
||||
|
||||
public function crawlKeyword(Request $request){
|
||||
try {
|
||||
// $keyword = $request->get('keyword');
|
||||
@@ -214,7 +132,6 @@ class UsedCarController extends BaseController
|
||||
$price = $request->get('price');
|
||||
$lang = $request->get('lang');
|
||||
$currency = $request->get('currency');
|
||||
$trade = $request->get('trade_term');
|
||||
if(empty($price)){
|
||||
throw new BusinessException("请输入生成pdf中的价格");
|
||||
}
|
||||
@@ -256,10 +173,8 @@ class UsedCarController extends BaseController
|
||||
|
||||
$field = $this->field($lang);//字段相关外文
|
||||
$field['currency'] = $this->currency[$currency];
|
||||
$data['product_name'] = $data['crawl_brand'].' '.$data['crawl_series'].' '. $data['crawl_year'] .'款 '.$data['crawl_title'];
|
||||
$data = $this->fanyi($data, $lang);//车辆字段翻译外文
|
||||
$contactField = $this->contact($lang); //名片相关外文
|
||||
$data['trade'] = $trade;
|
||||
$pdf = (new MpdfDriver())->exportUsedCarPdf($field,$data,$user,$contactField,$contact,$lang);
|
||||
UsedCar::where('id', $data['id'])->update(['pdf_src' => $pdf]);
|
||||
|
||||
@@ -288,13 +203,13 @@ class UsedCarController extends BaseController
|
||||
|
||||
return response()->download(
|
||||
$filePath,
|
||||
$raw['product_name']. '-' .$raw['code'].'-'.$raw['id'].'.pdf' // 下载时显示的文件名
|
||||
$raw['product_name'].'-'.$raw['id'].'.pdf' // 下载时显示的文件名
|
||||
);
|
||||
}
|
||||
|
||||
public function fanyi($data, $to){
|
||||
foreach ($data as $k => $v) {
|
||||
if (in_array($k, ['product_name', 'source_place', 'body_color', 'condition_desc', 'displacement']) && !empty(trim($v))) {
|
||||
if (in_array($k, ['product_name', 'source_place', 'body_color', 'condition_desc']) && !empty(trim($v))) {
|
||||
// $xunfei = new XunfeiFanyi();
|
||||
// $data[$k] = $xunfei->fanyi($v, $from = 'cn', $to);
|
||||
|
||||
@@ -331,7 +246,6 @@ class UsedCarController extends BaseController
|
||||
'created_at' => '创建时间',
|
||||
'condition_desc' => '车况介绍',
|
||||
'images' => '产品图',
|
||||
'trade' => '贸易条款',
|
||||
],
|
||||
|
||||
'en' => [
|
||||
@@ -345,7 +259,6 @@ class UsedCarController extends BaseController
|
||||
'created_at' => 'Created At',
|
||||
'condition_desc' => 'Vehicle Condition',
|
||||
'images' => 'Product image',
|
||||
'trade' => 'Trade terms',
|
||||
],
|
||||
|
||||
'ru' => [
|
||||
@@ -359,7 +272,6 @@ class UsedCarController extends BaseController
|
||||
'created_at' => 'Дата создания',
|
||||
'condition_desc' => 'Состояние автомобиля',
|
||||
'images' => 'Фотографии',
|
||||
'trade' => 'Условия торговли',
|
||||
],
|
||||
|
||||
'es' => [
|
||||
@@ -373,7 +285,6 @@ class UsedCarController extends BaseController
|
||||
'created_at' => 'Fecha de creación',
|
||||
'condition_desc' => 'Estado del vehículo',
|
||||
'images' => 'Imagen del producto',
|
||||
'trade' => 'Условия торговли',
|
||||
],
|
||||
|
||||
'ar' => [
|
||||
@@ -387,7 +298,6 @@ class UsedCarController extends BaseController
|
||||
'created_at' => 'تاريخ الإنشاء',
|
||||
'condition_desc' => 'حالة السيارة',
|
||||
'images' => 'خريطة المنتج',
|
||||
'trade' => 'شروط التجارة',
|
||||
],
|
||||
];
|
||||
return $field[$to];
|
||||
@@ -493,7 +403,7 @@ class UsedCarController extends BaseController
|
||||
|
||||
return $this->success([
|
||||
'pdf_url' => $url,
|
||||
'file_name' => $raw['product_name']. '-' .$raw['code']. '-' . $raw['id'] . '.pdf'
|
||||
'file_name' => $raw['product_name'] . '-' . $raw['id'] . '.pdf'
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
|
||||
@@ -1,425 +0,0 @@
|
||||
<?php
|
||||
namespace app\common\controller;
|
||||
|
||||
use app\common\controller\BaseController;
|
||||
use app\common\traits\crud\CrudControllerTrait;
|
||||
use app\common\service\UsedCarDetailService;
|
||||
use app\common\service\OssService;
|
||||
use support\Request;
|
||||
use support\exception\BusinessException;
|
||||
use support\Log;
|
||||
|
||||
class UsedCarDetailController extends BaseController
|
||||
{
|
||||
use CrudControllerTrait;
|
||||
|
||||
protected $service;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->service = new UsedCarDetailService();
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表页数据格式化
|
||||
*
|
||||
* @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 (!empty($data['images'])) {
|
||||
$images = is_array($data['images']) ? $data['images'] : explode(',', $data['images']);
|
||||
if ($images) {
|
||||
$ossService = new OssService();
|
||||
$images_pull = [];
|
||||
foreach ($images as $image) {
|
||||
$images_pull[] = $ossService->generatePrivateUrl($image);
|
||||
}
|
||||
$data['images_pull'] = $images_pull;
|
||||
} else {
|
||||
$data['images_pull'] = [];
|
||||
}
|
||||
}
|
||||
|
||||
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 (!empty($data['images'])) {
|
||||
$images = is_array($data['images']) ? $data['images'] : explode(',', $data['images']);
|
||||
if ($images) {
|
||||
$ossService = new OssService();
|
||||
$images_pull = [];
|
||||
$process = 'image/resize,w_800/quality,Q_40/format,webp';
|
||||
foreach ($images as $image) {
|
||||
$images_pull[] = $ossService->generatePrivateUrlResize($image, 3600, $process);
|
||||
}
|
||||
$data['images_pull'] = $images_pull;
|
||||
} else {
|
||||
$data['images_pull'] = [];
|
||||
}
|
||||
} else {
|
||||
$data['images_pull'] = [];
|
||||
}
|
||||
|
||||
if (!empty($data['intro_video'])){
|
||||
$videos = explode(',', $data['intro_video']);
|
||||
$ossService = new OssService();
|
||||
$videos_pull = [];
|
||||
foreach ($videos as $video){
|
||||
$videos_pull[] = $ossService->generatePrivateUrl($video);
|
||||
}
|
||||
$data['video_pull'] = $videos_pull;
|
||||
|
||||
}else{
|
||||
$data['video_pull'] = [];
|
||||
}
|
||||
|
||||
// 处理介绍图片
|
||||
if (!empty($data['intro_images'])) {
|
||||
$introImages = is_array($data['intro_images']) ? $data['intro_images'] : explode(',', $data['intro_images']);
|
||||
if ($introImages) {
|
||||
$ossService = new OssService();
|
||||
$intro_images_pull = [];
|
||||
foreach ($introImages as $image) {
|
||||
$intro_images_pull[] = $ossService->generatePrivateUrl($image);
|
||||
}
|
||||
$data['intro_images_pull'] = $intro_images_pull;
|
||||
} else {
|
||||
$data['intro_images_pull'] = [];
|
||||
}
|
||||
} else {
|
||||
$data['intro_images_pull'] = [];
|
||||
}
|
||||
|
||||
// 处理附件
|
||||
if (!empty($data['attachment'])) {
|
||||
$attachments = is_array($data['attachment']) ? $data['attachment'] : json_decode($data['attachment'], true);
|
||||
if ($attachments) {
|
||||
$ossService = new OssService();
|
||||
$attachment_pull = [];
|
||||
foreach ($attachments as $attachment) {
|
||||
$attachment_pull[] = [
|
||||
'name' => $attachment['name'] ?? '',
|
||||
'src' => $attachment['src'] ?? '',
|
||||
'src_pull' => $ossService->generatePrivateUrl($attachment['src'] ?? ''),
|
||||
];
|
||||
}
|
||||
$data['attachment_pull'] = $attachment_pull;
|
||||
} else {
|
||||
$data['attachment_pull'] = [];
|
||||
}
|
||||
} else {
|
||||
$data['attachment_pull'] = [];
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新状态
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \support\Response
|
||||
*/
|
||||
public function status(Request $request)
|
||||
{
|
||||
try {
|
||||
$data = $request->post();
|
||||
$res = $this->service->serviceUpdateStatus($request, $data);
|
||||
return $this->success([], '更新成功');
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存FAQ
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \support\Response
|
||||
*/
|
||||
public function saveFaqs(Request $request)
|
||||
{
|
||||
try {
|
||||
$carId = $request->post('id');
|
||||
$faqs = $request->post('faqs', []);
|
||||
|
||||
if (empty($carId)) {
|
||||
throw new BusinessException('车辆ID不能为空');
|
||||
}
|
||||
|
||||
$this->service->saveFaqs($carId, $faqs);
|
||||
|
||||
return $this->success([], '保存成功');
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建车辆(含FAQ)
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \support\Response
|
||||
*/
|
||||
public function storeWithFaqs(Request $request)
|
||||
{
|
||||
try {
|
||||
$data = $request->post();
|
||||
$faqs = $data['faqs'] ?? [];
|
||||
|
||||
// 先创建车辆
|
||||
$result = $this->service->serviceStore($request,$data);
|
||||
|
||||
// 再保存FAQ
|
||||
if (!empty($faqs) && isset($result['id'])) {
|
||||
$this->service->saveFaqs($result['id'], $faqs);
|
||||
}
|
||||
|
||||
return $this->success($result, '创建成功');
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新车辆(含FAQ)
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \support\Response
|
||||
*/
|
||||
public function updateWithFaqs(Request $request)
|
||||
{
|
||||
try {
|
||||
$data = $request->post();
|
||||
$faqs = $data['faqs'] ?? [];
|
||||
$carId = $data['id'] ?? null;
|
||||
unset($data['faqs']);
|
||||
|
||||
// 先更新车辆
|
||||
$result = $this->service->serviceUpdate($request,$data);
|
||||
|
||||
// 再更新FAQ
|
||||
if (!empty($faqs) && $carId) {
|
||||
$this->service->saveFaqs($carId, $faqs);
|
||||
}
|
||||
|
||||
return $this->success($result, '更新成功');
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交审核
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \support\Response
|
||||
*/
|
||||
public function submitAudit(Request $request)
|
||||
{
|
||||
try {
|
||||
$carId = $request->post('id');
|
||||
|
||||
if (empty($carId)) {
|
||||
throw new BusinessException('车辆ID不能为空');
|
||||
}
|
||||
|
||||
$this->service->submitAudit($request, $carId);
|
||||
|
||||
return $this->success([], '提交审核成功');
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 审核车辆
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \support\Response
|
||||
*/
|
||||
public function audit(Request $request)
|
||||
{
|
||||
try {
|
||||
$data = $request->post();
|
||||
|
||||
$result = $this->service->audit($request, $data);
|
||||
|
||||
$statusText = $data['audit_status'] == 1 ? '通过' : '拒绝';
|
||||
$message = "审核{$statusText}成功";
|
||||
|
||||
// 审核通过时,提示推送任务已加入队列
|
||||
if ($data['audit_status'] == 1 && $result['push_queued']) {
|
||||
$message .= ',WordPress推送任务已加入队列,将在后台处理';
|
||||
}
|
||||
|
||||
return $this->success($result, $message);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取审核记录
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \support\Response
|
||||
*/
|
||||
public function auditRecords(Request $request)
|
||||
{
|
||||
try {
|
||||
$carId = $request->get('id');
|
||||
|
||||
if (empty($carId)) {
|
||||
throw new BusinessException('车辆ID不能为空');
|
||||
}
|
||||
|
||||
$records = $this->service->getAuditRecords($carId);
|
||||
|
||||
return $this->success($records);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取审核统计
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \support\Response
|
||||
*/
|
||||
public function auditStats(Request $request)
|
||||
{
|
||||
try {
|
||||
$stats = $this->service->getAuditStats($request);
|
||||
|
||||
return $this->success($stats);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存懂车帝二手车数据
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \support\Response
|
||||
*/
|
||||
public function crawlStore(Request $request)
|
||||
{
|
||||
try {
|
||||
$params = $request->post();
|
||||
$userId = $request->user['id'] ?? 0;
|
||||
$result = $this->service->crawlStore($params, $userId);
|
||||
return $this->success($result, '保存成功,WordPress推送任务已加入队列,将在后台处理');
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 抓取懂车帝二手车数据
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \support\Response
|
||||
*/
|
||||
public function crawl(Request $request)
|
||||
{
|
||||
try {
|
||||
$url = $request->post('url');
|
||||
|
||||
if (empty($url)) {
|
||||
throw new BusinessException('链接不能为空');
|
||||
}
|
||||
|
||||
// 验证是否是懂车帝链接
|
||||
if (!str_contains($url, 'dongchedi.com')) {
|
||||
throw new BusinessException('只支持懂车帝二手车链接');
|
||||
}
|
||||
|
||||
$userId = $request->user['id'] ?? 0;
|
||||
if (empty($userId)) {
|
||||
throw new BusinessException('用户未登录');
|
||||
}
|
||||
|
||||
$result = $this->service->crawl($url, $userId);
|
||||
if(!empty($result['images'])){
|
||||
$images = explode(',',$result['images']);
|
||||
$ossService = new OssService();
|
||||
$images_pull = [];
|
||||
foreach ($images as $image){
|
||||
$images_pull[] = $ossService->generatePrivateUrl($image);
|
||||
}
|
||||
$result['images_pull'] = $images_pull;
|
||||
}else{
|
||||
$result['images_pull'] = [];
|
||||
}
|
||||
|
||||
return $this->success($result, '抓取成功');
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('抓取懂车帝二手车失败', [
|
||||
'url' => $request->post('url'),
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新推送失败的车辆到WordPress
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \support\Response
|
||||
*/
|
||||
public function retryPush(Request $request)
|
||||
{
|
||||
try {
|
||||
$carId = $request->post('id');
|
||||
|
||||
if (empty($carId)) {
|
||||
throw new BusinessException('车辆ID不能为空');
|
||||
}
|
||||
|
||||
$count = $this->service->retryPush($carId);
|
||||
|
||||
if ($count === 0) {
|
||||
return $this->error('车辆不存在或状态不是推送失败');
|
||||
}
|
||||
|
||||
return $this->success(['count' => $count], 'WordPress推送任务已重新加入队列');
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('重新推送失败', [
|
||||
'car_id' => $request->post('id'),
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,16 +89,9 @@ class UserController extends BaseController
|
||||
|
||||
$ossservice = new OssService();
|
||||
$data['avatar_pull'] = $data['avatar'];
|
||||
if(!str_contains($data['avatar'], 'http') && !str_contains($data['avatar'], 'www.') && !empty($data['avatar'])){
|
||||
if(!str_contains($data['avatar'], 'http') && !str_contains($data['avatar'], 'www.') && !empty($data['user']['avatar'])){
|
||||
$data['avatar_pull'] = $ossservice->generatePrivateUrl($data['avatar']);
|
||||
}
|
||||
|
||||
if (!empty($data['social_code'])){
|
||||
$data['social_code_pull'] = $ossservice->generatePrivateUrl($data['social_code']);
|
||||
}
|
||||
elseif (isset($data['social_code'])){
|
||||
$data['social_code_pull'] = '';
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
<?php
|
||||
namespace app\common\controller;
|
||||
|
||||
use app\common\controller\BaseController;
|
||||
use app\common\model\User;
|
||||
use app\common\model\UserGroupMembers;
|
||||
use app\common\traits\crud\CrudControllerTrait;
|
||||
use app\common\service\WordpressVisitorService;
|
||||
use support\Request;
|
||||
|
||||
class WordpressVisitorController extends BaseController
|
||||
{
|
||||
use CrudControllerTrait;
|
||||
|
||||
protected $noNeedLogin = ['whatsApp'];
|
||||
|
||||
protected $noNeedHeaderData = ['whatsApp'];
|
||||
|
||||
protected $service;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->service = new WordpressVisitorService();
|
||||
}
|
||||
|
||||
public function whatsApp(Request $request){
|
||||
try {
|
||||
$url = $this->service->whatsApp($request);
|
||||
return $this->success(['url' => $url]);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表页数据格式化
|
||||
*
|
||||
* @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;
|
||||
// }
|
||||
}
|
||||
@@ -23,10 +23,10 @@ class AuthLogic
|
||||
public function uPwdLogin(array $data): array
|
||||
{
|
||||
$user = $this->validateUser($data['account'], $data['password']);
|
||||
//$system = $this->validateSystem($data['system_code']);
|
||||
//$this->validateSystemPermission($user, $system);
|
||||
$system = $this->validateSystem($data['system_code']);
|
||||
$this->validateSystemPermission($user, $system);
|
||||
|
||||
return $this->generateTokensAndReturn($user, $data);
|
||||
return $this->generateTokensAndReturn($user, $system, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -37,14 +37,14 @@ class AuthLogic
|
||||
{
|
||||
$feishuUserInfo = $this->getFeishuUserInfo($data);
|
||||
return $feishuUserInfo;
|
||||
// $user = $this->getOrCreateUser($feishuUserInfo);
|
||||
//
|
||||
// $system = $this->validateSystem($data['system_code']);
|
||||
// if ($user->isLogin) {
|
||||
// $this->validateSystemPermission($user, $system);
|
||||
// }
|
||||
//
|
||||
// return $this->generateTokensAndReturn($user, $system, $data);
|
||||
$user = $this->getOrCreateUser($feishuUserInfo);
|
||||
|
||||
$system = $this->validateSystem($data['system_code']);
|
||||
if ($user->isLogin) {
|
||||
$this->validateSystemPermission($user, $system);
|
||||
}
|
||||
|
||||
return $this->generateTokensAndReturn($user, $system, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -168,34 +168,28 @@ class AuthLogic
|
||||
/**
|
||||
* 生成并记录token,返回包含token的响应数据
|
||||
*/
|
||||
private function generateTokensAndReturn(User $user, array $data): array
|
||||
private function generateTokensAndReturn(User $user, Systems $system, array $data): array
|
||||
{
|
||||
// $refreshToken = $this->generateRefreshToken($user->id, $data['system_code']);
|
||||
$payload = $this->buildJwtPayload($user, $system);
|
||||
$token = $this->generateAccessToken($payload);
|
||||
$refreshToken = $this->generateRefreshToken($user->id, $data['system_code']);
|
||||
$homeUrl = $this->getHome($system->id, $payload);
|
||||
|
||||
// 记录 token 到 Redis 和 MySQL
|
||||
$this->recordUserToken($user->id, $token, $data['system_code']);
|
||||
|
||||
return [
|
||||
'union_id' => $user->union_id,
|
||||
'role' => $user->role,
|
||||
'role_wp' => $user->role_wp,
|
||||
'name' => $user->name,
|
||||
'access_token' => $token,
|
||||
'refresh_token' => $refreshToken,
|
||||
'expires_in' => config('jwt.jwt_expiration'),
|
||||
'system_code' => $data['system_code'],
|
||||
'corp_abbr' => $data['corp_abbr'],
|
||||
'login_type' => $data['login_type'],
|
||||
'home' => $this->getHomePage($user),
|
||||
'home_url' => $homeUrl,
|
||||
// 'payload' => $payload,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认页
|
||||
*/
|
||||
private function getHomePage(User $user)
|
||||
{
|
||||
if($user->role>0){
|
||||
return '/admin/publish/publish_list';
|
||||
}else{
|
||||
return '/admin/publish/publish_my_listpro';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取飞书用户信息
|
||||
*/
|
||||
@@ -227,15 +221,12 @@ class AuthLogic
|
||||
throw new BusinessException('授权失败,请重试');
|
||||
}
|
||||
|
||||
$user = User::firstOrCreate(['union_id'=>$userInfo['data']['union_id']],[
|
||||
User::firstOrCreate(['union_id'=>$userInfo['data']['union_id']],[
|
||||
'union_id'=>$userInfo['data']['union_id'],
|
||||
'name'=>$userInfo['data']['name'],
|
||||
'app_id'=>$data['app_id'],
|
||||
'avatar'=>$userInfo['data']['avatar_url']??'',
|
||||
]);
|
||||
$data['role'] = $user['role'];
|
||||
$data['role_wp'] = $user['role_wp'];
|
||||
$data['home'] =$this->getHomePage($user);
|
||||
return $userInfo['data'] + $data;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\logic;
|
||||
|
||||
use app\common\traits\crud\CrudLogicTrait;
|
||||
use app\common\model\UsedCarDetail;
|
||||
|
||||
class UsedCarDetailLogic
|
||||
{
|
||||
use CrudLogicTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = UsedCarDetail::class;
|
||||
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\logic;
|
||||
|
||||
use app\common\traits\crud\CrudLogicTrait;
|
||||
use app\common\model\WordpressVisitor;
|
||||
|
||||
class WordpressVisitorLogic
|
||||
{
|
||||
use CrudLogicTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = WordpressVisitor::class;
|
||||
|
||||
}
|
||||
@@ -40,7 +40,7 @@ class UsedCar extends Model
|
||||
/**
|
||||
* 允许自动填充模型字段
|
||||
*/
|
||||
protected $fillable = ['id','code', 'user_id', 'role', 'product_name', 'vin', 'price', 'source_place', 'plate_place', 'plate_time', 'displacement','mileage','power', 'body_color', 'images', 'condition_desc', 'attachment', 'pdf_src', 'source_url', 'crawl_title', 'crawl_brand', 'crawl_brand_id', 'crawl_series', 'crawl_series_id', 'crawl_year', 'crawl_msrp', 'crawl_car_id', 'status', 'sort', 'created_at', 'updated_at'];
|
||||
protected $fillable = ['id','code', 'user_id', 'product_name', 'vin', 'price', 'source_place', 'plate_place', 'plate_time', 'displacement','mileage','power', 'body_color', 'images', 'condition_desc', 'attachment', 'crawl_title', 'crawl_brand', 'crawl_brand_id', 'crawl_series', 'crawl_series_id', 'crawl_year', 'crawl_msrp', 'crawl_car_id', 'status', 'sort', 'created_at', 'updated_at'];
|
||||
|
||||
/**
|
||||
* 不允许自动填充模型字段
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use support\Model;
|
||||
|
||||
class UsedCarDetail extends Model
|
||||
{
|
||||
/**
|
||||
* The connection name for the model.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $connection = 'mysql';
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'used_car_detail';
|
||||
|
||||
/**
|
||||
* 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', 'user_id', 'role_wp', 'product_name', 'vin', 'price', 'source_place', 'plate_place', 'plate_time', 'displacement', 'mileage', 'power', 'body_color', 'images', 'condition_desc', 'attachment', 'pdf_src', 'source_url', 'crawl_title', 'crawl_brand', 'crawl_brand_id', 'crawl_series', 'crawl_series_id', 'crawl_year', 'crawl_msrp', 'crawl_car_id', 'status', 'sort', 'simple_desc', 'intro_images', 'intro_text', 'intro_video', 'official_guide_price', 'manufacturer', 'level', 'energy_type', 'listing_time', 'engine', 'electric_motor', 'nedc_range', 'cltc_range', 'fast_charge_platform', 'charging_time', 'fast_charge_percent', 'max_power_kw', 'max_torque_nm', 'transmission', 'dimensions', 'body_structure', 'max_speed', 'acceleration_100', 'nedc_fuel_consumption', 'power_consumption_100', 'equivalent_fuel_consumption', 'maintenance_cost_60k', 'body_length', 'body_width', 'body_height', 'wheelbase', 'door_count', 'door_open_type', 'seat_count', 'transmission_desc', 'gear_count', 'transmission_type', 'drive_mode', 'front_suspension', 'rear_suspension', 'steering_type', 'has_abs', 'has_ebd', 'has_bra', 'has_tcs', 'has_esp', 'has_active_brake', 'has_lane_assist', 'has_lka', 'has_lane_centering', 'has_fatigue_remind', 'front_brake_type', 'rear_brake_type', 'parking_brake_type', 'front_tire_spec', 'rear_tire_spec', 'created_at', 'updated_at'];
|
||||
|
||||
/**
|
||||
* 不允许自动填充模型字段
|
||||
*/
|
||||
protected $guarded = [];
|
||||
|
||||
/**
|
||||
* JSON 类型字段自动转换
|
||||
*/
|
||||
protected $casts = [
|
||||
// 'images' => 'array',
|
||||
// 'attachment' => 'array',
|
||||
// 'intro_images' => 'array',
|
||||
];
|
||||
|
||||
/**
|
||||
* 关联 FAQ
|
||||
*/
|
||||
public function faqs()
|
||||
{
|
||||
return $this->hasMany(UsedCarFaq::class, 'car_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联用户
|
||||
*/
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(\app\common\model\User::class, 'user_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联审核记录
|
||||
*/
|
||||
public function auditRecords()
|
||||
{
|
||||
return $this->hasMany(UsedCarDetailAudit::class, 'car_id', 'id')->orderBy('created_at', 'desc');
|
||||
}
|
||||
|
||||
/**
|
||||
* 最新审核记录
|
||||
*/
|
||||
public function latestAudit()
|
||||
{
|
||||
return $this->hasOne(UsedCarDetailAudit::class, 'car_id', 'id')->orderBy('created_at', 'desc');
|
||||
}
|
||||
|
||||
/**
|
||||
* 审核状态文本
|
||||
*/
|
||||
public static $auditStatusText = [
|
||||
0 => '待审核',
|
||||
1 => '审核通过',
|
||||
2 => '审核拒绝',
|
||||
];
|
||||
|
||||
/**
|
||||
* 作用域:待审核
|
||||
*/
|
||||
public function scopePending($query)
|
||||
{
|
||||
return $query->where('status', 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 作用域:已通过
|
||||
*/
|
||||
public function scopeApproved($query)
|
||||
{
|
||||
return $query->where('status', 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 作用域:已拒绝
|
||||
*/
|
||||
public function scopeRejected($query)
|
||||
{
|
||||
return $query->where('status', 2);
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use support\Model;
|
||||
|
||||
class UsedCarDetailAudit extends Model
|
||||
{
|
||||
/**
|
||||
* The connection name for the model.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $connection = 'mysql';
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'used_car_detail_audit';
|
||||
|
||||
/**
|
||||
* 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', 'car_id', 'auditor_id', 'audit_status', 'audit_reason',
|
||||
'before_status', 'after_status', 'created_at', 'updated_at'
|
||||
];
|
||||
|
||||
/**
|
||||
* 不允许自动填充模型字段
|
||||
*/
|
||||
protected $guarded = [];
|
||||
|
||||
/**
|
||||
* 审核状态文本
|
||||
*/
|
||||
public static $statusText = [
|
||||
0 => '待审核',
|
||||
1 => '审核通过',
|
||||
2 => '审核拒绝',
|
||||
];
|
||||
|
||||
/**
|
||||
* 关联车辆
|
||||
*/
|
||||
public function car()
|
||||
{
|
||||
return $this->belongsTo(UsedCarDetail::class, 'car_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联审核人
|
||||
*/
|
||||
public function auditor()
|
||||
{
|
||||
return $this->belongsTo(\app\common\model\User::class, 'auditor_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取审核状态文本
|
||||
*/
|
||||
public function getAuditStatusTextAttribute()
|
||||
{
|
||||
return self::$statusText[$this->audit_status] ?? '未知';
|
||||
}
|
||||
|
||||
/**
|
||||
* 作用域:通过审核
|
||||
*/
|
||||
public function scopeApproved($query)
|
||||
{
|
||||
return $query->where('audit_status', 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 作用域:拒绝审核
|
||||
*/
|
||||
public function scopeRejected($query)
|
||||
{
|
||||
return $query->where('audit_status', 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 作用域:待审核
|
||||
*/
|
||||
public function scopePending($query)
|
||||
{
|
||||
return $query->where('audit_status', 0);
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use support\Model;
|
||||
|
||||
class UsedCarFaq extends Model
|
||||
{
|
||||
/**
|
||||
* The connection name for the model.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $connection = 'mysql';
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'used_car_faq';
|
||||
|
||||
/**
|
||||
* 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', 'car_id', 'question', 'answer', 'sort', 'status', 'created_at', 'updated_at'
|
||||
];
|
||||
|
||||
/**
|
||||
* 不允许自动填充模型字段
|
||||
*/
|
||||
protected $guarded = [];
|
||||
|
||||
/**
|
||||
* 关联车辆详情
|
||||
*/
|
||||
public function car()
|
||||
{
|
||||
return $this->belongsTo(UsedCarDetail::class, 'car_id', 'id');
|
||||
}
|
||||
}
|
||||
@@ -40,17 +40,10 @@ class User extends Model
|
||||
/**
|
||||
* 允许自动填充模型字段
|
||||
*/
|
||||
protected $fillable = ['id', 'union_id', 'app_id', 'name', 'name_en', 'avatar', 'phone', 'email', 'whatsapp', 'social', 'social_code', 'status', 'role'];
|
||||
protected $fillable = ['id', 'union_id', 'app_id', 'name', 'avatar', 'phone', 'email', 'whatsapp', 'social', 'social_code'];
|
||||
|
||||
/**
|
||||
* 不允许自动填充模型字段
|
||||
*/
|
||||
protected $guarded = [];
|
||||
|
||||
// 账号查询的作用域
|
||||
public function scopeByAccount($query, $account)
|
||||
{
|
||||
return $query->Where('phone', $account)
|
||||
->orWhere('email', $account);
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use support\Model;
|
||||
|
||||
class WordpressSite extends Model
|
||||
{
|
||||
/**
|
||||
* The connection name for the model.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $connection = 'mysql';
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'wordpress_site';
|
||||
|
||||
/**
|
||||
* 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', 'site_name', 'site_url', 'tpl_type', 'admin_url', 'api_base_url', 'auth_type', 'api_user', 'api_secret', 'exchange_rate', 'categories', 'status', 'sort', 'last_push_at', 'remark', 'locale', 'last_uid', 'created_at', 'updated_at'];
|
||||
|
||||
/**
|
||||
* 不允许自动填充模型字段
|
||||
*/
|
||||
protected $guarded = [];
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use support\Model;
|
||||
|
||||
class WordpressUsedCar extends Model
|
||||
{
|
||||
/**
|
||||
* The connection name for the model.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $connection = 'mysql';
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'wordpress_used_car';
|
||||
|
||||
/**
|
||||
* 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', 'wp_site_id', 'used_car_id', 'wp_post_id', 'status', 'request_payload', 'response_body', 'error_message', 'retry_count', 'pushed_at', 'created_at', 'updated_at'];
|
||||
|
||||
/**
|
||||
* 不允许自动填充模型字段
|
||||
*/
|
||||
protected $guarded = [];
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use support\Model;
|
||||
|
||||
class WordpressVisitor extends Model
|
||||
{
|
||||
/**
|
||||
* The connection name for the model.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $connection = 'mysql';
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'wordpress_visitor';
|
||||
|
||||
/**
|
||||
* 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', 'user_agent', 'accept_language', 'ip', 'md5_unique', 'user_id', 'created_at', 'updated_at'];
|
||||
|
||||
/**
|
||||
* 不允许自动填充模型字段
|
||||
*/
|
||||
protected $guarded = [];
|
||||
}
|
||||
@@ -57,20 +57,6 @@ class OssService
|
||||
return $this->ossClient->signUrl($this->bucket, $objectKey, $expireTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成图片的私有访问 URL
|
||||
* @param string $objectKey
|
||||
* @return string
|
||||
*/
|
||||
public function generatePrivateUrlResize($objectKey, $expireTime = 3600,$process = 'x-oss-process='.'image/resize,w_1200/quality,Q_50/format,webp')
|
||||
{
|
||||
// 生成 OSS 私有 URL(有效期为 3600 秒)
|
||||
return $this->ossClient->signUrl($this->bucket, $objectKey, $expireTime,'GET',
|
||||
[
|
||||
'x-oss-process' => $process
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除 OSS 上的文件
|
||||
* @param string $objectKey OSS上存储的对象名称
|
||||
|
||||
@@ -46,8 +46,7 @@ class UploadService
|
||||
$extensionFromPath = pathinfo($basename, PATHINFO_EXTENSION);
|
||||
$extension = $extensionFromName ?: $extensionFromPath;
|
||||
|
||||
// 获取 MIME 类型,如果函数不可用则使用扩展名判断
|
||||
$mime = $this->getMimeType($filePath, $extension);
|
||||
$mime = mime_content_type($filePath) ?: 'application/octet-stream';
|
||||
$size = filesize($filePath) ?: 0;
|
||||
|
||||
$sceneUrl = $scene ? ($scene . '/') : '';
|
||||
@@ -117,49 +116,6 @@ class UploadService
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件的 MIME 类型
|
||||
* 优先使用 mime_content_type(),不可用时根据扩展名判断
|
||||
*
|
||||
* @param string $filePath 文件路径
|
||||
* @param string $extension 文件扩展名
|
||||
* @return string
|
||||
*/
|
||||
private function getMimeType(string $filePath, string $extension = ''): string
|
||||
{
|
||||
// 优先使用系统函数
|
||||
if (function_exists('mime_content_type')) {
|
||||
$mime = mime_content_type($filePath);
|
||||
if ($mime !== false && $mime !== '') {
|
||||
return $mime;
|
||||
}
|
||||
}
|
||||
|
||||
// 降级:根据扩展名判断
|
||||
$extension = strtolower(ltrim($extension, '.'));
|
||||
$mimeTypes = [
|
||||
'jpg' => 'image/jpeg',
|
||||
'jpeg' => 'image/jpeg',
|
||||
'png' => 'image/png',
|
||||
'gif' => 'image/gif',
|
||||
'webp' => 'image/webp',
|
||||
'bmp' => 'image/bmp',
|
||||
'svg' => 'image/svg+xml',
|
||||
'pdf' => 'application/pdf',
|
||||
'doc' => 'application/msword',
|
||||
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'xls' => 'application/vnd.ms-excel',
|
||||
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'zip' => 'application/zip',
|
||||
'rar' => 'application/x-rar-compressed',
|
||||
'mp4' => 'video/mp4',
|
||||
'mp3' => 'audio/mpeg',
|
||||
'txt' => 'text/plain',
|
||||
];
|
||||
|
||||
return $mimeTypes[$extension] ?? 'application/octet-stream';
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除 OSS 上的文件
|
||||
*
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,12 +2,10 @@
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
use app\common\utils\crawl\service\UsedCarCrawlSingle;
|
||||
use app\common\validate\UsedCarValidate;
|
||||
use app\common\traits\crud\CrudServiceTrait;
|
||||
use app\common\model\UsedCar;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use support\exception\BusinessException;
|
||||
use support\Log;
|
||||
use support\Request;
|
||||
@@ -53,18 +51,10 @@ class UsedCarService
|
||||
$config['order']['id'] = 'desc';
|
||||
break;
|
||||
case 'index':
|
||||
$config['where']['role'] = 2;
|
||||
$config['where']['status'] = 1;
|
||||
if (isset($p['status']) && $p['status']=='archive') {
|
||||
$config['where']['status'] = ['in', [2, 3]];
|
||||
}elseif (isset($p['status'])) {
|
||||
$config['where']['status'] = $p['status'];
|
||||
}
|
||||
|
||||
if (isset($p['scene']) && $p['scene'] == 'self') {
|
||||
$config['where']['user_id'] = $request->user['id'];
|
||||
unset($config['where']['role']);
|
||||
unset( $config['where']['status']);
|
||||
}
|
||||
|
||||
// 关键字
|
||||
@@ -79,6 +69,7 @@ class UsedCarService
|
||||
break;
|
||||
case 'show':
|
||||
$config['where']['id'] = $p['id'];
|
||||
$config['where']['status'] = 1;
|
||||
break;
|
||||
case 'store':
|
||||
break;
|
||||
@@ -111,21 +102,8 @@ class UsedCarService
|
||||
$data['attachment'] = '';
|
||||
}
|
||||
$data['user_id'] = $request->user['id'];
|
||||
$user = User::where('id', $request->user['id'])->first();
|
||||
$data['role'] = $user['role'];
|
||||
if(!empty($data['source_url'])){
|
||||
$used = UsedCar::where('source_url', $data['source_url']);
|
||||
if ($data['role'] == 1) {
|
||||
$used = $used->where('user_id', $data['user_id']);
|
||||
}
|
||||
$count = $used->count();
|
||||
if ($count) {
|
||||
throw new BusinessException('该车源已收录');
|
||||
}
|
||||
}
|
||||
$data['code'] = $this->getcode($request->user['id']);
|
||||
$data['mileage'] = str_replace(['K','M','k','m'], '', $data['mileage']);
|
||||
$data['power'] = str_replace(['P','S','p','s','马力'], '', $data['power']);
|
||||
return $data;
|
||||
}
|
||||
|
||||
@@ -146,21 +124,9 @@ class UsedCarService
|
||||
$data['attachment'] = '';
|
||||
}
|
||||
$data['mileage'] = str_replace(['K','M','k','m'], '', $data['mileage']);
|
||||
$data['power'] = str_replace(['P','S','p','s','马力'], '', $data['power']);
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function serviceUpdateStatus(Request $request, array $data)
|
||||
{
|
||||
if (empty($data['id'])) {
|
||||
throw new BusinessException('id不能为空');
|
||||
}
|
||||
if (!isset($data['status'])) {
|
||||
throw new BusinessException('status错误');
|
||||
}
|
||||
return UsedCar::where('id', $data['id'])->update(['status' => $data['status']]);
|
||||
}
|
||||
|
||||
public function getcode($userId){
|
||||
$userCount = Redis::get('used_car_count_'.$userId, 0);
|
||||
$todayStart = strtotime('today');
|
||||
@@ -262,7 +228,7 @@ class UsedCarService
|
||||
// $crawlSeries = $crawlService->brandSeries($keyword);
|
||||
|
||||
if (empty($seriesId)) {
|
||||
throw new \Exception('抓取失败,请使用更精确的关键词(品牌+车系)');
|
||||
throw new \Exception('抓取失败,请使用更精确的关键词');
|
||||
}
|
||||
|
||||
// $brandSerieses = [];
|
||||
@@ -424,50 +390,4 @@ class UsedCarService
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择车辆抓取方式
|
||||
* @param string $url
|
||||
*/
|
||||
public function selectCraw(string $url){
|
||||
$usedCarCrawl = new UsedCarCrawlSingle();
|
||||
switch (true){
|
||||
case str_contains($url,'dongchedi.com'): //懂车帝pc端
|
||||
$info = $usedCarCrawl->crawlCar($url);
|
||||
break;
|
||||
case str_contains($url,'dcdapp.com'): //懂车帝移动端
|
||||
$finalUrl = $usedCarCrawl->getPcUrl($url); //获取pc端页面链接
|
||||
if(empty($finalUrl)){
|
||||
throw new \Exception('链接解析错误');
|
||||
}
|
||||
$info = $usedCarCrawl->crawlCar($finalUrl);
|
||||
break;
|
||||
case str_contains($url,'t.zijieimg.com'): //懂车帝移动端分享链接
|
||||
$finalUrl = $usedCarCrawl->getFinaUrl($url); //解析抖音短链,并获取pc端页面链接
|
||||
if(empty($finalUrl)){
|
||||
throw new \Exception('链接解析错误');
|
||||
}
|
||||
$info = $usedCarCrawl->crawlCar($finalUrl);
|
||||
break;
|
||||
case str_contains($url,'www.che168.com/dealer'): //汽车之家
|
||||
$info = $usedCarCrawl->crawlChe168Car($url);
|
||||
break;
|
||||
case str_contains($url,'www.che168.com/home?infoid='): //汽车之家pc端链接转换
|
||||
$url = $usedCarCrawl->getChe168UrlByPc($url);
|
||||
$info = $usedCarCrawl->crawlChe168Car($url);
|
||||
break;
|
||||
case str_contains($url,'wap.che168.com/dealer'): //汽车之家移动端链接
|
||||
$finalUrl = $usedCarCrawl->getChe168UrlByMobile($url);
|
||||
$info = $usedCarCrawl->crawlChe168Car($finalUrl);
|
||||
break;
|
||||
case str_contains($url,'comm.app.autohome.com.cn'): //汽车之家移动端链接
|
||||
$finalUrl = $usedCarCrawl->get168FinaUrl($url);
|
||||
$info = $usedCarCrawl->crawlChe168Car($finalUrl);
|
||||
break;
|
||||
default:
|
||||
Log::info('未知链接:'.$url);
|
||||
throw new \Exception('加紧开发中');
|
||||
}
|
||||
return $info;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
use app\common\validate\UserGroupMembersValidate;
|
||||
use app\common\traits\crud\CrudServiceTrait;
|
||||
use app\common\model\UserGroupMembers;
|
||||
use app\common\model\UserGroup;
|
||||
use support\Request;
|
||||
|
||||
class UserGroupMembersService
|
||||
{
|
||||
|
||||
use CrudServiceTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = UserGroupMembers::class;
|
||||
|
||||
/**
|
||||
* 验证器
|
||||
*/
|
||||
protected $validator = UserGroupMembersValidate::class;
|
||||
|
||||
/**
|
||||
* 获取验证器
|
||||
*/
|
||||
public function getValidator()
|
||||
{
|
||||
return new $this->validator;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
use app\common\logic\UserGroupLogic;
|
||||
use app\common\logic\UserGroupMembersLogic;
|
||||
use app\common\validate\UserGroupValidate;
|
||||
use app\common\traits\crud\CrudServiceTrait;
|
||||
use app\common\model\UserGroup;
|
||||
use support\Request;
|
||||
|
||||
class UserGroupService
|
||||
{
|
||||
|
||||
use CrudServiceTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = UserGroup::class;
|
||||
|
||||
/**
|
||||
* 验证器
|
||||
*/
|
||||
protected $validator = UserGroupValidate::class;
|
||||
|
||||
/**
|
||||
* 获取验证器
|
||||
*/
|
||||
public function getValidator()
|
||||
{
|
||||
return new $this->validator;
|
||||
}
|
||||
|
||||
|
||||
protected function buildQueryConfig(Request $request, string $scene = 'default'): array
|
||||
{
|
||||
$p = $request->all();
|
||||
|
||||
$config = [];
|
||||
$config['model'] = $this->modelClass;
|
||||
switch ($scene) {
|
||||
case 'default':
|
||||
$config['where']['status'] = 1;
|
||||
$config['order']['sort'] = 'desc';
|
||||
$config['order']['id'] = 'desc';
|
||||
|
||||
$config['with'] = ['groupMembers', 'groupMembers.user'];
|
||||
break;
|
||||
case 'index':
|
||||
$config['where']['status'] = 1;
|
||||
$config['order']['sort'] = 'desc';
|
||||
$config['order']['id'] = 'desc';
|
||||
|
||||
$config['with'] = ['groupMembers', 'groupMembers.user'];
|
||||
break;
|
||||
case 'show':
|
||||
$config['where']['id'] = $p['id'];
|
||||
$config['where']['status'] = 1;
|
||||
break;
|
||||
case 'store':
|
||||
break;
|
||||
case 'update':
|
||||
$config['where']['id'] = $p['id'];
|
||||
break;
|
||||
case 'destroy':
|
||||
$config['where']['id'] = $p['id'];
|
||||
break;
|
||||
case 'userGroup':
|
||||
$config['where']['status'] = 1;
|
||||
$config['order']['sort'] = 'desc';
|
||||
$config['order']['id'] = 'desc';
|
||||
$config['fields'] = ['id', 'title'];
|
||||
break;
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
public function serviceStore(Request $request, array $data = [], string $scene = 'store')
|
||||
{
|
||||
$data = $this->preProcess(ucfirst($scene), $data, $request);
|
||||
$created = (new UserGroupLogic())->createGroup($data);
|
||||
return $this->postProcess(ucfirst($scene), $created);
|
||||
}
|
||||
|
||||
public function serviceUpdate(Request $request, array $data = [], string $scene = 'update')
|
||||
{
|
||||
$data = $this->preProcess(ucfirst($scene), $data, $request);
|
||||
$config = $this->buildQueryConfig($request, $scene);
|
||||
$userGroup = $this->logicShow($config);
|
||||
$updated = (new UserGroupLogic())->updateGroup($userGroup, $data);
|
||||
return $this->postProcess(ucfirst($scene), $updated);
|
||||
}
|
||||
|
||||
public function serviceDestroy(Request $request, string $scene = 'destroy')
|
||||
{
|
||||
$config = $this->buildQueryConfig($request, $scene);
|
||||
$deleted = $this->logicDeleteBy($config);
|
||||
(new UserGroupMembersLogic())->deleteUserGroupMembers($config['where']['id']);
|
||||
return $this->postProcess(ucfirst($scene), $deleted);
|
||||
}
|
||||
|
||||
|
||||
public function userGroup(Request $request, string $scene = 'userGroup')
|
||||
{
|
||||
$config = $this->buildQueryConfig($request, $scene);
|
||||
$result = $this->logicListAll($config);
|
||||
return $this->postProcess(ucfirst($scene), $result);
|
||||
}
|
||||
|
||||
public function userGroupScene(Request $request, string $scene = 'userGroup')
|
||||
{
|
||||
$data = (new \app\common\service\SysDictService())->pluckKeyValueByCode('user_group_scene');
|
||||
$result = [];
|
||||
foreach($data as $key => $value) {
|
||||
$result[] = [
|
||||
'name' => $key,
|
||||
'id' => $value,
|
||||
];
|
||||
}
|
||||
return $this->postProcess(ucfirst($scene), $result);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,101 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
use app\common\model\User;
|
||||
use app\common\model\UserGroupMembers;
|
||||
use app\common\validate\WordpressVisitorValidate;
|
||||
use app\common\traits\crud\CrudServiceTrait;
|
||||
use app\common\model\WordpressVisitor;
|
||||
use support\exception\BusinessException;
|
||||
use support\Redis;
|
||||
use support\Request;
|
||||
|
||||
class WordpressVisitorService
|
||||
{
|
||||
|
||||
use CrudServiceTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = WordpressVisitor::class;
|
||||
|
||||
/**
|
||||
* 验证器
|
||||
*/
|
||||
protected $validator = WordpressVisitorValidate::class;
|
||||
|
||||
/**
|
||||
* 获取验证器
|
||||
*/
|
||||
public function getValidator()
|
||||
{
|
||||
return new $this->validator;
|
||||
}
|
||||
|
||||
public function whatsApp(Request $request){
|
||||
// $userAgent = $_SERVER['HTTP_USER_AGENT'];
|
||||
// $acceptLanguage = $_SERVER['HTTP_ACCEPT_LANGUAGE'];
|
||||
|
||||
$userAgent = $request->header('User-Agent','');
|
||||
$acceptLanguage = $request->header('Accept-Language','');
|
||||
$referer = $request->header('referer');
|
||||
$ip = $request->getRealIp();
|
||||
$md5Hash = md5($userAgent . '-' . $acceptLanguage . '-' . $ip);
|
||||
$visitor = WordpressVisitor::where('md5_unique', $md5Hash)->first();
|
||||
if (!empty($visitor)) {
|
||||
$visitor->updated_at = time();
|
||||
$visitor->save();
|
||||
$user = User::where('id',$visitor['user_id'])->first();
|
||||
return $this->url($user['whatsapp'], $referer);
|
||||
}
|
||||
|
||||
$lastId = Redis::get('wp_whatsApp_user');
|
||||
if($lastId){
|
||||
$last = WordpressVisitor::orderBy('id','desc')->first();
|
||||
$lastId = $last['id']??0;
|
||||
}
|
||||
|
||||
$userIds = User::where('role',1)
|
||||
->where('whatsapp','!=','')
|
||||
->orderBy('id','asc')
|
||||
->get()
|
||||
->toArray();
|
||||
if(empty($userIds)){
|
||||
throw new BusinessException('没有符合条件的whatsApp~');
|
||||
}
|
||||
$userIds = array_column($userIds, 'id');
|
||||
$userIds = array_values($userIds);
|
||||
$id = 0;
|
||||
foreach ($userIds as $userId) {
|
||||
if($userId < $lastId){
|
||||
$id = $userId;
|
||||
}
|
||||
}
|
||||
if(empty($id)){
|
||||
$id = array_shift($userIds);
|
||||
}
|
||||
Redis::set('wp_whatsApp_user',$id);
|
||||
|
||||
$data = [
|
||||
'user_agent' => $userAgent,
|
||||
'accept_language' => $acceptLanguage,
|
||||
'ip' => $ip,
|
||||
'md5_unique' => $md5Hash,
|
||||
'user_id' => $id,
|
||||
];
|
||||
WordpressVisitor::create($data);
|
||||
$user = User::where('id',$id)->first();
|
||||
//return $user['phone'];
|
||||
return $this->url($user['whatsapp'], $referer);
|
||||
}
|
||||
|
||||
public function url($phone,$referer){
|
||||
$url = "https://wa.me/" . $phone;
|
||||
if(!empty($referer)){
|
||||
$url = $url . "?text=来自%20" . $referer;
|
||||
}
|
||||
return $url;
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
<?php
|
||||
namespace app\common\utils\crawl\service;
|
||||
use support\Db;
|
||||
|
||||
trait UnCodeFont{
|
||||
/**
|
||||
* 是否包含 PUA 字符(U+E000~U+F8FF)
|
||||
*/
|
||||
function needFontDecode(string $text): bool
|
||||
{
|
||||
if ($text === '') return false;
|
||||
$chars = preg_split('//u', $text, -1, PREG_SPLIT_NO_EMPTY);
|
||||
if (!$chars) return false;
|
||||
|
||||
foreach ($chars as $ch) {
|
||||
$u = mb_convert_encoding($ch, 'UCS-4BE', 'UTF-8');
|
||||
if ($u === false || strlen($u) !== 4) continue;
|
||||
$cp = unpack('N', $u)[1];
|
||||
if ($cp >= 0xE000 && $cp <= 0xF8FF) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取某个 font_hash 的映射表,返回 [ 'e53d' => '中', ... ]
|
||||
* code 表里存的是 '' 或 ''
|
||||
*/
|
||||
function loadFontMap(string $fontHash): array
|
||||
{
|
||||
$rows = Db::table('font_map')
|
||||
->select('code', 'char')
|
||||
->where('font_hash', $fontHash)
|
||||
->get()
|
||||
->toArray();
|
||||
$rows = array_map(function($item) {
|
||||
return (array) $item;
|
||||
}, $rows);
|
||||
|
||||
$map = [];
|
||||
foreach ($rows as $r) {
|
||||
// 从 '' 提取 e53d
|
||||
if (preg_match('/&#x([0-9a-fA-F]+);/i', $r['code'], $m)) {
|
||||
$map[strtolower($m[1])] = $r['char'];
|
||||
}
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将包含 PUA 字符的字符串,按 font_map 替换
|
||||
*/
|
||||
function decodeObfuscatedText(string $text, array $fontMap): string
|
||||
{
|
||||
$fontHash = '77fd388510a974babdf4837b26d2d33f'; // 你当前字体 md5(font_map.sql 里那一份)
|
||||
$fontMap = $this->loadFontMap($fontHash);
|
||||
// 按 Unicode 字符切分
|
||||
$chars = preg_split('//u', $text, -1, PREG_SPLIT_NO_EMPTY);
|
||||
if (!$chars) return $text;
|
||||
|
||||
$out = '';
|
||||
|
||||
foreach ($chars as $ch) {
|
||||
// 获取码点(十六进制),如 e53d
|
||||
$hex = $this->unicodeCodePointHex($ch);
|
||||
if ($hex !== null) {
|
||||
$key = strtolower($hex);
|
||||
// 只替换命中的(一般就是 E***)
|
||||
if (isset($fontMap[$key])) {
|
||||
$out .= $fontMap[$key];
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// 没命中:原样保留(比如 '.' 这种正常字符)
|
||||
$out .= $ch;
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个 Unicode 字符的码点十六进制(不带 0x),失败返回 null
|
||||
* 例:'' -> 'e53d'
|
||||
*/
|
||||
function unicodeCodePointHex(string $ch): ?string
|
||||
{
|
||||
if ($ch === '') return null;
|
||||
$u = mb_convert_encoding($ch, 'UCS-4BE', 'UTF-8');
|
||||
if ($u === false || strlen($u) !== 4) return null;
|
||||
$code = unpack('N', $u)[1]; // 32-bit
|
||||
return dechex($code);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\utils\crawl\service\service;
|
||||
namespace app\common\utils\crawl\service;
|
||||
|
||||
use app\common\model\UsedcarCrawlResultsImages;
|
||||
use app\common\service\UploadService;
|
||||
|
||||
@@ -1,768 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\utils\crawl\service;
|
||||
|
||||
use app\common\model\UsedcarCrawlResultsImages;
|
||||
use app\common\service\UploadService;
|
||||
use app\common\traits\crud\CrudServiceTrait;
|
||||
use app\common\utils\crawl\service\UnCodeFont;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use QL\QueryList;
|
||||
use support\exception\BusinessException;
|
||||
use support\Log;
|
||||
use Throwable;
|
||||
|
||||
class UsedCarCrawlSingle
|
||||
{
|
||||
use CrudServiceTrait;
|
||||
use UnCodeFont;
|
||||
|
||||
// 默认请求头
|
||||
private const DEFAULT_HEADERS = [
|
||||
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
'Accept-Language' => 'zh-CN,zh;q=0.9',
|
||||
'Referer' => 'https://www.dongchedi.com/',
|
||||
];
|
||||
|
||||
/**
|
||||
* 获取网页HTML内容
|
||||
*
|
||||
* @param string $url
|
||||
* @return string
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
public function fetchHtml(string $url): string
|
||||
{
|
||||
$client = new Client([
|
||||
'verify' => false,
|
||||
]);
|
||||
$response = $client->request('GET', $url, [
|
||||
'headers' => self::DEFAULT_HEADERS,
|
||||
'timeout' => 10,
|
||||
]);
|
||||
return (string)$response->getBody();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取懂车帝移动端分享短链最终url
|
||||
*
|
||||
* @param string $url
|
||||
* @return string
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
public function getFinaUrl(string $url): string
|
||||
{
|
||||
$client = new Client([
|
||||
'verify' => false,
|
||||
]);
|
||||
$response = $client->get($url, [
|
||||
'allow_redirects' => ['track_redirects' => true],
|
||||
]);
|
||||
|
||||
$history = $response->getHeader('X-Guzzle-Redirect-History');
|
||||
$finalUrl = end($history);
|
||||
|
||||
$pcUrl = $this->getPcUrl($finalUrl);
|
||||
return $pcUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从移动端url获取sku_id
|
||||
* @param string $url
|
||||
* @return string|null
|
||||
*/
|
||||
public function getPcUrl(string $url): ?string
|
||||
{
|
||||
$query = parse_url($url, PHP_URL_QUERY);
|
||||
if (!$query) {
|
||||
return null;
|
||||
}
|
||||
|
||||
parse_str($query, $params);
|
||||
|
||||
return !empty($params['sku_id']) ? 'https://www.dongchedi.com/usedcar/'.$params['sku_id'] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 抓取懂车帝二手车信息
|
||||
* @param string $url
|
||||
* @return array
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
public function crawlCar(string $url){
|
||||
$html = $this->fetchHtml($url);
|
||||
if ($html === '' || str_contains($html, '404 Not Found')) {
|
||||
throw new BusinessException("页面内容为空: {$url}");
|
||||
}
|
||||
|
||||
$info = $this->parseUsedCarDetail($html, $url);
|
||||
|
||||
// $insertId = $this->saveUsedCarBasicInfo($info);
|
||||
|
||||
// 下载图片 + 上传 + 入库(图片失败不影响主记录)
|
||||
$imagesArray = json_decode($info['images'] ?? '[]', true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
$imagesArray = [];
|
||||
}
|
||||
|
||||
Log::info('抓取到的图片数量', [
|
||||
'url' => $url,
|
||||
'images_count' => count($imagesArray),
|
||||
'images' => $imagesArray,
|
||||
]);
|
||||
|
||||
if (!empty($imagesArray)) {
|
||||
$saveDir = public_path('uploads/usedcar/' . $info['used_car_id']);
|
||||
$image = $this->uploadAndRemove($imagesArray, $saveDir);
|
||||
if ($image !== false) {
|
||||
$info['image'] = $image['image'];
|
||||
$info['image_pull'] = $image['image_pull'];
|
||||
Log::info('图片上传成功', [
|
||||
'uploaded_count' => count($image['image']),
|
||||
]);
|
||||
} else {
|
||||
$info['image'] = [];
|
||||
$info['image_pull'] = [];
|
||||
Log::warning('图片上传失败', ['url' => $url]);
|
||||
}
|
||||
} else {
|
||||
$info['image'] = [];
|
||||
$info['image_pull'] = [];
|
||||
}
|
||||
|
||||
// 继续派发配置页任务
|
||||
// if (!empty($info['car_id_url'])) {
|
||||
// $this->pushUrl($info['car_id_url'], 'config', $insertId);
|
||||
// }
|
||||
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过汽车之家分享链接获取pc端地址
|
||||
*
|
||||
* @param string $url
|
||||
* @return string
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
public function get168FinaUrl(string $url): string
|
||||
{
|
||||
$client = new Client([
|
||||
'verify' => false,
|
||||
'timeout' => 10,
|
||||
'headers' => [
|
||||
'User-Agent' => 'Mozilla/5.0 (Linux; Android 13; Mobile)'
|
||||
]
|
||||
]);
|
||||
|
||||
$response = $client->get($url);
|
||||
$html = (string) $response->getBody();
|
||||
|
||||
preg_match(
|
||||
'/var\s+oUrl\s*=\s*[\'"]([^\'"]+che168\.com[^\'"]+)[\'"]/',
|
||||
$html,
|
||||
$m
|
||||
);
|
||||
|
||||
if(empty($m[1])){ //如果没有获取到移动端地址
|
||||
throw new \Exception('168移动端地址解析失败');
|
||||
}
|
||||
|
||||
$pcUrl = $this->getChe168UrlByMobile($m[1]);
|
||||
return $pcUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据移动端url获取汽车之家pc端url
|
||||
*
|
||||
* @param string $url
|
||||
* @return string
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
public function getChe168UrlByMobile(string $url): string
|
||||
{
|
||||
|
||||
// 解析 URL获取车辆id
|
||||
$parsedUrl = parse_url($url);
|
||||
$path = $parsedUrl['path']; // "/dealer/162138/56723550.html"
|
||||
// 使用 pathinfo 获取文件名
|
||||
$pathParts = explode('/', $path);
|
||||
$filename = end($pathParts); // "56723550.html"
|
||||
$carId = pathinfo($filename, PATHINFO_FILENAME); // "56723550"
|
||||
|
||||
$pcUrl = 'https://www.che168.com/dealer/111/' . $carId . '.html?pvareaid=0';
|
||||
return $pcUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据汽车之家pc端url转换为另一种页面的汽车之家pc端url
|
||||
*
|
||||
* @param string $url
|
||||
* @return string
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
public function getChe168UrlByPc(string $url): string
|
||||
{
|
||||
// 解析URL获取查询参数部分
|
||||
$queryString = parse_url($url, PHP_URL_QUERY);
|
||||
// 将查询参数解析为数组
|
||||
parse_str($queryString, $params);
|
||||
|
||||
// 获取infoid
|
||||
$carId = $params['infoid'] ?? null;
|
||||
|
||||
$pcUrl = 'https://www.che168.com/dealer/111/' . $carId . '.html?pvareaid=0';
|
||||
|
||||
return $pcUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 抓取汽车之家数据
|
||||
* @param Request $request
|
||||
*/
|
||||
public function crawlChe168Car(string $url)
|
||||
{
|
||||
// 解析 URL获取车辆id
|
||||
$parsedUrl = parse_url($url);
|
||||
$path = $parsedUrl['path']; // "/dealer/162138/56723550.html"
|
||||
// 使用 pathinfo 获取文件名
|
||||
$pathParts = explode('/', $path);
|
||||
$filename = end($pathParts); // "56723550.html"
|
||||
$carId = pathinfo($filename, PATHINFO_FILENAME); // "56723550"
|
||||
|
||||
// 初始化 Guzzle 客户端
|
||||
$client = new Client([
|
||||
'headers' => [
|
||||
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
|
||||
'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||||
'Referer' => 'https://www.che168.com/',
|
||||
],
|
||||
'verify' => false,
|
||||
'timeout' => 10,
|
||||
]);
|
||||
|
||||
|
||||
// 请求 HTML
|
||||
$response = $client->get($url);
|
||||
$html = (string)$response->getBody();
|
||||
file_put_contents(runtime_path('craw.html'), $html);
|
||||
|
||||
// 检查是否获取到 HTML
|
||||
if (empty($html)) {
|
||||
throw new \Exception('无法获取页面 HTML');
|
||||
}
|
||||
|
||||
// che168 页面为 gb2312,Linux 默认 UTF-8,直接解析会导致 DOM 解析异常。统一转 UTF-8 再解析。
|
||||
$html = $this->ensureUtf8ForChe168($html);
|
||||
|
||||
// 使用 QueryList 解析 HTML
|
||||
$ql = QueryList::html($html);
|
||||
|
||||
// 解析车辆列表,每辆车可能在 li.cards-li 或类似类名里
|
||||
$cars = $ql->find('div#nav1 ul.basic-item-ul li')->map(function($item) {
|
||||
$attribute = [];
|
||||
$field = $item->find('.item-name')->text();
|
||||
$value = $item->text();
|
||||
$title = ['product_name' => '产品名称','plate_time' => '上牌时间', 'mileage' => '表显里程', 'capacity' => '排 量', 'source_place'=> '所 在 地', 'body_color'=>'车身颜色','condition_desc'=>'车况介绍',];
|
||||
$title_key = array_flip($title);
|
||||
if(in_array($field,$title)){
|
||||
$attribute[$title_key[$field]] = str_replace($field, '', $value);
|
||||
}
|
||||
|
||||
return $attribute;
|
||||
})->all();
|
||||
|
||||
$cars = array_filter($cars);
|
||||
$cars = array_merge(...$cars);
|
||||
$cars['product_name'] = $cars['car_name'] = $ql->find('div.car-box .car-brand-name')->text();
|
||||
$cars['price'] = $ql->find('div.goodstartmoney')->text();
|
||||
if(empty($cars['price'])){
|
||||
$cars['price'] = $ql->find('span#overlayPrice')->text();
|
||||
}
|
||||
// $cars['price'] = explode('万', $cars['price'])[0]??0;
|
||||
$cars['price'] = str_replace(['¥', '¥', '万'], '', $cars['price']);
|
||||
$cars['price'] = is_numeric($cars['price'])?$cars['price']*10000:$cars['price'];
|
||||
//$cars['price'] = $ql->find('#overlayPrice')->text();
|
||||
$cars['title'] = $cars['product_name'];
|
||||
$cars['condition_desc'] = $ql->find('div.leave-message-box #messageBox')->text();
|
||||
$cars['usage_mileage'] = str_replace('万公里', '', $cars['mileage']);
|
||||
|
||||
//品牌与车系
|
||||
$links = $ql->find('.bread-crumbs a')->map(function ($a) {
|
||||
return trim($a->text());
|
||||
})->all();
|
||||
$cars['brand_name'] = !empty($links[3]) ? str_replace('二手','',$links[3]): null;
|
||||
$cars['brand_id'] = 0;
|
||||
$cars['series_name'] = !empty($links[4]) ?str_replace('二手','',$links[4]): null;
|
||||
$cars['series_id'] = 0;
|
||||
$cars['car_id'] = 0;
|
||||
$info = explode(' ',$cars['product_name']);
|
||||
$info = array_filter($info, function($item) {
|
||||
return str_contains($item, '款');
|
||||
});
|
||||
$year = end($info);
|
||||
$cars['year'] = !empty($year) ? str_replace('款', '', $year):'';
|
||||
|
||||
// 图片:主用轮播 .swiper-container2,备用 #pic_li(懒加载 data-original)
|
||||
$images = $this->collectChe168Images($ql, '.swiper-container2 .swiper-slide .jiaodianphotoclick img', false);
|
||||
if (empty($images)) {
|
||||
$images = $this->collectChe168Images($ql, '#pic_li img', true);
|
||||
}
|
||||
$images = array_values(array_unique($images));
|
||||
|
||||
if (empty($images)) {
|
||||
Log::warning("crawlChe168Car:未找到任何车辆图片,URL: {$url}");
|
||||
}
|
||||
|
||||
$saveDir = public_path('uploads/usedcar/' . $carId);
|
||||
$cars['used_car_id'] = $carId;
|
||||
$image = $this->uploadAndRemove($images, $saveDir, 'https://www.che168.com/');
|
||||
$cars['image'] = $image['image'];
|
||||
$cars['image_pull'] = $image['image_pull'];
|
||||
|
||||
return $cars;
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载 -> 上传 -> 删除本地
|
||||
*
|
||||
* @param array $images 原始相册URL列表
|
||||
* @param string $path 本地保存目录
|
||||
*/
|
||||
public function uploadAndRemove(array $images, string $path, string $referer = '')
|
||||
{
|
||||
if (empty($images)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 下载图片
|
||||
$filePatch = $this->downloadImageAlbum($images, $path,$referer);
|
||||
if (empty($filePatch)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$uploadService = new UploadService();
|
||||
|
||||
$image = [];
|
||||
$image_pull = [];
|
||||
foreach ($filePatch as $url => $filePath) {
|
||||
if ($filePath === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// 上传云端
|
||||
$param = [
|
||||
'path' => $filePath,
|
||||
'scene' => 'usedcar_crawl',
|
||||
'category' => 'usedcar',
|
||||
];
|
||||
|
||||
$result = $uploadService->uploadLocalFile($param);
|
||||
if (empty($result['file_name'])) {
|
||||
// 上传失败则跳过,但不抛异常影响主流程
|
||||
continue;
|
||||
}
|
||||
|
||||
// 保存图片信息
|
||||
// UsedcarCrawlResultsImages::create([
|
||||
// 'uc_id' => $id,
|
||||
// 'used_car_id' => $usedCarId,
|
||||
// 'original_name'=> $result['original_name'] ?? '',
|
||||
// 'file_path' => $result['file_name'],
|
||||
// ]);
|
||||
$image[] = $result['file_name'];
|
||||
$image_pull[] = $result['file_url'];
|
||||
|
||||
} catch (Throwable $e) {
|
||||
Log::error('crawlUsedCarImage'.$e->getMessage().'|'.$e->getLine().$e->getFile());
|
||||
// 单张图失败不影响其他
|
||||
// 可以按需写日志
|
||||
} finally {
|
||||
// 删除本地文件
|
||||
if (is_file($filePath)) {
|
||||
@unlink($filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 删除目录
|
||||
rmdir($path);
|
||||
|
||||
return ['image' => $image, 'image_pull' => $image_pull];
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量下载相册
|
||||
*
|
||||
* @param array $urls 图片URL数组
|
||||
* @param string $saveDir 保存目录
|
||||
* @return array [原始URL => 保存路径或null]
|
||||
*/
|
||||
public function downloadImageAlbum(array $urls, string $saveDir, string $referer): array
|
||||
{
|
||||
$result = [];
|
||||
$successCount = 0;
|
||||
$failCount = 0;
|
||||
|
||||
foreach ($urls as $url) {
|
||||
$url = trim((string)$url);
|
||||
if ($url === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strpos($url, '//') === 0) {
|
||||
$url = 'https:' . $url;
|
||||
}
|
||||
|
||||
try {
|
||||
$path = $this->downloadImage($url, $saveDir, $referer);
|
||||
if ($path !== null) {
|
||||
$successCount++;
|
||||
} else {
|
||||
$failCount++;
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$failCount++;
|
||||
Log::error('下载图片失败', [
|
||||
'url' => $url,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
$path = null;
|
||||
}
|
||||
|
||||
$result[$url] = $path;
|
||||
}
|
||||
|
||||
Log::info('图片下载完成', [
|
||||
'total' => count($urls),
|
||||
'success' => $successCount,
|
||||
'fail' => $failCount,
|
||||
]);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 下载单张图片
|
||||
*/
|
||||
public function downloadImage(string $url, string $saveDir, string $referer, ?string $filename = null, array $headers = []): ?string
|
||||
{
|
||||
if (!is_dir($saveDir)) {
|
||||
if (!@mkdir($saveDir, 0777, true) && !is_dir($saveDir)) {
|
||||
// echo "[Image] 创建目录失败:{$saveDir}\n";
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if ($filename === null) {
|
||||
$path = parse_url($url, PHP_URL_PATH);
|
||||
$ext = pathinfo($path ?? '', PATHINFO_EXTENSION);
|
||||
|
||||
if ($ext === '') {
|
||||
$ext = 'jpg';
|
||||
}
|
||||
|
||||
$filename = md5($url) . '.' . $ext;
|
||||
}
|
||||
|
||||
$savePath = rtrim($saveDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $filename;
|
||||
|
||||
$referer = empty($referer) ? self::DEFAULT_HEADERS['Referer'] : $referer;
|
||||
$defaultHeaders = [
|
||||
'User-Agent: ' . self::DEFAULT_HEADERS['User-Agent'],
|
||||
'Accept: image/avif,image/webp,image/apng,image/*,*/*;q=0.8',
|
||||
// 'Referer: ' . self::DEFAULT_HEADERS['Referer'],
|
||||
'Referer: ' . $referer,
|
||||
];
|
||||
|
||||
foreach ($headers as $h) {
|
||||
$defaultHeaders[] = $h;
|
||||
}
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_CONNECTTIMEOUT => 5,
|
||||
CURLOPT_TIMEOUT => 15,
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
CURLOPT_SSL_VERIFYHOST => false,
|
||||
CURLOPT_HTTPHEADER => $defaultHeaders,
|
||||
]);
|
||||
|
||||
$data = curl_exec($ch);
|
||||
$errNo = curl_errno($ch);
|
||||
$code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($errNo !== 0 || $code !== 200 || $data === false || $data === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$bytes = @file_put_contents($savePath, $data);
|
||||
if ($bytes === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $savePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析二手车详情页信息
|
||||
*
|
||||
* @param string $html
|
||||
* @param string $url
|
||||
* @return array
|
||||
* @throws BusinessException
|
||||
*/
|
||||
public function parseUsedCarDetail(string $html = '', string $url = ''): array
|
||||
{
|
||||
try {
|
||||
$ql = QueryList::html($html);
|
||||
} catch (Throwable $e) {
|
||||
throw new BusinessException('解析 HTML 失败(详情页): ' . $e->getMessage());
|
||||
}
|
||||
|
||||
$title = trim($ql->find('h1')->text());
|
||||
$price = trim($ql->find('.head-info_price-wrap__Y4bxi > p')->text());
|
||||
if(empty($price)){
|
||||
$price = trim($ql->find('span.tw-text-color-red-500')->text());
|
||||
}
|
||||
$pageText = $ql->find('body')->text();
|
||||
$jsonText = $ql->find('#__NEXT_DATA__')->text();
|
||||
|
||||
if ($jsonText === '') {
|
||||
throw new BusinessException('详情页未找到 __NEXT_DATA__ JSON');
|
||||
}
|
||||
|
||||
$jsonArray = $this->decodeJson($jsonText, '详情页 __NEXT_DATA__');
|
||||
|
||||
// 车辆相册
|
||||
$images = $jsonArray['props']['pageProps']['skuDetail']['head_images'] ?? [];
|
||||
|
||||
// 车辆ID / 年份
|
||||
$carInfo = $jsonArray['props']['pageProps']['skuDetail']['car_info'] ?? [];
|
||||
$carId = $carInfo['car_id'] ?? 0;
|
||||
$year = (int)($carInfo['year'] ?? 0);
|
||||
|
||||
// 车辆ID URL
|
||||
$carIdUrl = $carId ? ('https://www.dongchedi.com/auto/params-carIds-' . $carId) : '';
|
||||
|
||||
// 能源类型(两个字段择一)
|
||||
$overview = $jsonArray['props']['pageProps']['skuDetail']['car_config_overview'] ?? [];
|
||||
$fuelForm = $overview['power']['fuel_form'] ?? '';
|
||||
$horsepower = $overview['power']['horsepower'] ?? ''; //马力
|
||||
$capacity = $overview['power']['capacity'] ?? ''; //排量
|
||||
$newFuelForm = $overview['new_energy_power']['fuel_form'] ?? '';
|
||||
$resultFuelForm = $fuelForm === '' ? $newFuelForm : $fuelForm;
|
||||
|
||||
// 使用里程
|
||||
$importantText = $jsonArray['props']['pageProps']['skuDetail']['important_text'] ?? '';
|
||||
$importantTextArray = $importantText ? explode(' | ', $importantText) : [];
|
||||
$usageMileage = '';
|
||||
if (isset($importantTextArray[1])) {
|
||||
$usageMileage = str_replace('万公里', '', $importantTextArray[1]);
|
||||
}
|
||||
|
||||
// 车辆名称 品牌、车系、车型
|
||||
$brandName = $carInfo['brand_name'] ?? '';
|
||||
$brand_id = $carInfo['brand_id'] ?? '';
|
||||
$seriesName = $carInfo['series_name'] ?? '';
|
||||
$series_id = $carInfo['series_id'] ?? '';
|
||||
$carName = $carInfo['car_name'] ?? '';
|
||||
|
||||
$bodyColor = $carInfo['body_color'] ?? ''; //颜色
|
||||
|
||||
// 商家信息
|
||||
$shopInfo = $jsonArray['props']['pageProps']['skuDetail']['shop_info'] ?? [];
|
||||
$formatShopInfo = [
|
||||
'shop_id' => $shopInfo['shop_id'] ?? 0,
|
||||
'shop_name' => $shopInfo['shop_short_name'] ?? '',
|
||||
'shop_address' => $shopInfo['shop_address'] ?? '',
|
||||
];
|
||||
|
||||
// 简介参数
|
||||
$otherParams = $jsonArray['props']['pageProps']['skuDetail']['other_params'] ?? [];
|
||||
$displacement = $this->getParamValue($otherParams, '排量');
|
||||
$gearbox = $this->getParamValue($otherParams, '变速箱');
|
||||
$sourcePlace = $this->getParamValue($otherParams, '车源地');
|
||||
$plateTime = $this->getParamValue($otherParams, '上牌时间');
|
||||
|
||||
$formatCarInfo = [
|
||||
'displacement' => $displacement,
|
||||
'gearbox' => $gearbox,
|
||||
];
|
||||
|
||||
// 提取新车指导价 / 比新车省金额
|
||||
$newPrice = $this->extractPatternValue($pageText, '/新车指导价[::]\s*([^\r\n万]*万)/u');
|
||||
$priceDifference = $this->extractPatternValue($pageText, '/比新车省[::]\s*([^\r\n万]*万)/u');
|
||||
|
||||
// 提取车况介绍
|
||||
$conditionDesc = '';
|
||||
if ($pageText && preg_match('/车况介绍\s*(.*?)\s*推荐理由/su', $pageText, $m)) {
|
||||
$conditionDesc = trim($m[1]);
|
||||
}
|
||||
|
||||
$price = str_replace('一口价', '', $price);
|
||||
if ($this->needFontDecode($price)) {
|
||||
$price = $this->decodeObfuscatedText($price, []);
|
||||
}
|
||||
if ($this->needFontDecode($usageMileage)) {
|
||||
$price = $this->decodeObfuscatedText($usageMileage, []);
|
||||
}
|
||||
return [
|
||||
'used_car_id' => $this->extractIdFromUrl($url),
|
||||
'car_id' => $carId,
|
||||
'url' => $url,
|
||||
'title' => $title,
|
||||
'price' => str_replace('万', '', $this->decodePrice($price)),
|
||||
'new_price' => str_replace('万', '', $newPrice),
|
||||
'price_difference' => str_replace('万', '', $priceDifference),
|
||||
'condition_desc' => $conditionDesc,
|
||||
'images' => json_encode($images, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||
'car_id_url' => $carIdUrl,
|
||||
'used_detail' => $jsonText,
|
||||
'format_shop_info' => json_encode($formatShopInfo, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||
'fuel_form' => $resultFuelForm,
|
||||
'usage_mileage' => $usageMileage,
|
||||
'brand_name' => $brandName,
|
||||
'series_name' => $seriesName,
|
||||
'car_name' => $carName,
|
||||
'year' => $year,
|
||||
'format_car_info' => json_encode($formatCarInfo, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||
'body_color' => $bodyColor,
|
||||
'horsepower' => $horsepower,
|
||||
'capacity' => $capacity,
|
||||
'source_place' => $sourcePlace,
|
||||
'brand_id' => $brand_id,
|
||||
'series_id' => $series_id,
|
||||
'plate_time' => $plateTime,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 解码懂车帝价格
|
||||
*/
|
||||
protected function decodePrice(string $str): string
|
||||
{
|
||||
$map = [
|
||||
"\u{E53D}" => '1',
|
||||
"\u{E3F0}" => '2',
|
||||
"\u{E422}" => '3',
|
||||
"\u{E42C}" => '4',
|
||||
"\u{E49C}" => '5',
|
||||
"\u{E42B}" => '6',
|
||||
"\u{E4FE}" => '7',
|
||||
"\u{E548}" => '8',
|
||||
"\u{E4C8}" => '9',
|
||||
"\u{E453}" => '0',
|
||||
"\u{E45F}" => '万',
|
||||
];
|
||||
|
||||
return strtr($str, $map);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从URL中提取ID
|
||||
*/
|
||||
protected function extractIdFromUrl(string $url)
|
||||
{
|
||||
if (preg_match('/\/(\d+)$/', $url, $matches)) {
|
||||
return (int)$matches[1];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取符合正则表达式的值(只保留数字和小数点)
|
||||
*/
|
||||
protected function extractPatternValue(string $text, string $pattern): string
|
||||
{
|
||||
$value = '';
|
||||
if ($text && preg_match($pattern, $text, $m)) {
|
||||
$value = trim(preg_replace('/[^0-9.]/', '', $m[1]));
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从参数列表中获取指定名称的值
|
||||
*/
|
||||
public function getParamValue(array $params, string $name): ?string
|
||||
{
|
||||
foreach ($params as $item) {
|
||||
if (($item['name'] ?? null) === $name) {
|
||||
return $item['value'] ?? null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 che168 页面上收集图片 URL
|
||||
*
|
||||
* @param \QL\QueryList $ql
|
||||
* @param string $selector 选择器
|
||||
* @param bool $preferDataOriginal 为 true 时优先用 data-original(#pic_li 懒加载)
|
||||
* @return array<string>
|
||||
*/
|
||||
protected function collectChe168Images($ql, string $selector, bool $preferDataOriginal): array
|
||||
{
|
||||
return $ql->find($selector)->map(function ($img) use ($preferDataOriginal) {
|
||||
$src = $preferDataOriginal ? ($img->attr('data-original') ?: $img->attr('src')) : ($img->attr('src') ?: $img->attr('data-original'));
|
||||
if (!$src || $src === '' || str_contains($src, 'default-che168')) {
|
||||
return null;
|
||||
}
|
||||
if (str_starts_with($src, '//')) {
|
||||
$src = 'https:' . $src;
|
||||
}
|
||||
return $src;
|
||||
})->filter()->values()->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 che168 页面 (gb2312/gbk) 转为 UTF-8,避免 Linux 下 DOM 解析异常
|
||||
*
|
||||
* @param string $html
|
||||
* @return string
|
||||
*/
|
||||
protected function ensureUtf8ForChe168(string $html): string
|
||||
{
|
||||
if (!function_exists('mb_convert_encoding')) {
|
||||
return $html;
|
||||
}
|
||||
$isGb = stripos($html, 'charset=gb2312') !== false || stripos($html, 'charset=gbk') !== false;
|
||||
if (!$isGb) {
|
||||
return $html;
|
||||
}
|
||||
$out = @mb_convert_encoding($html, 'UTF-8', 'GB2312');
|
||||
if ($out === false || $out === '') {
|
||||
$out = @mb_convert_encoding($html, 'UTF-8', 'GBK');
|
||||
}
|
||||
if ($out !== false && $out !== '') {
|
||||
$html = $out;
|
||||
$html = preg_replace('/charset\s*=\s*["\']?gb(2312|k)["\']?/i', 'charset="utf-8"', $html);
|
||||
}
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全解 JSON
|
||||
*
|
||||
* @param string $json
|
||||
* @param string $context
|
||||
* @return array
|
||||
* @throws BusinessException
|
||||
*/
|
||||
protected function decodeJson(string $json, string $context = 'JSON'): array
|
||||
{
|
||||
$data = json_decode($json, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE || !is_array($data)) {
|
||||
throw new BusinessException($context . ' 解析失败:' . json_last_error_msg());
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,681 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\utils\crawl\service;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use QL\QueryList;
|
||||
use support\Redis;
|
||||
use support\Db;
|
||||
use support\exception\BusinessException;
|
||||
|
||||
class UsedCarCrawl_bak
|
||||
{
|
||||
|
||||
/**
|
||||
* 插入队列url
|
||||
*
|
||||
* $scene 二手信息 usedcar 配置信息 config
|
||||
*/
|
||||
public function iUrl(string $url = '', string $scene = 'usedcar', int $id = 0)
|
||||
{
|
||||
return Redis::lpush('usedcar:urls', json_encode([
|
||||
'scene' => $scene,
|
||||
'url' => $url,
|
||||
'id' => $id
|
||||
], JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取队列url
|
||||
*
|
||||
* $scene 二手信息 usedcar 配置信息 config
|
||||
*/
|
||||
public function rUrl()
|
||||
{
|
||||
return json_decode(Redis::rPop('usedcar:urls'), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行抓取
|
||||
*/
|
||||
public function doData()
|
||||
{
|
||||
try {
|
||||
$raw = $this->rUrl();
|
||||
|
||||
if (!$raw) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$html = $this->getHtml($raw['url']);
|
||||
|
||||
switch ($raw['scene']) {
|
||||
case 'usedcar':
|
||||
$info = $this->getUsedDetail($html, $raw['url']);
|
||||
$id = $this->saveUsedcarBasicToDb($info);
|
||||
$this->iUrl($info['car_id_url'], 'config', $id);
|
||||
break;
|
||||
|
||||
case 'config':
|
||||
$data = $this->getConfigDetail($html);
|
||||
$this->saveUsedcarConfigToDb($data, $raw['id']);
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (\Throwable $th) {
|
||||
throw new BusinessException($th->getMessage().'|'.$th->getLine().'|'.$th->getFile());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取html
|
||||
*/
|
||||
public function getHtml($url)
|
||||
{
|
||||
$client = new Client();
|
||||
$res = $client->request('GET', $url, [
|
||||
'headers' => [
|
||||
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
|
||||
. 'AppleWebKit/537.36 (KHTML, like Gecko) '
|
||||
. 'Chrome/120.0.0.0 Safari/537.36',
|
||||
'Accept-Language' => 'zh-CN,zh;q=0.9',
|
||||
'Referer' => 'https://www.dongchedi.com/'
|
||||
]
|
||||
]);
|
||||
return (string)$res->getBody();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取二手信息
|
||||
*/
|
||||
public function getUsedDetail(string $html = '', string $url = '')
|
||||
{
|
||||
$ql = QueryList::html($html);
|
||||
|
||||
$title = $ql->find('h1')->text();
|
||||
$price = $ql->find('.head-info_price-wrap__Y4bxi > p')->text();
|
||||
$pageText = $ql->find('body')->text();
|
||||
$jsonText = $ql->find('#__NEXT_DATA__')->text();
|
||||
|
||||
// 新车指导价
|
||||
$newPrice = '';
|
||||
if ($pageText) {
|
||||
if (preg_match('/新车指导价[::]\s*([^\r\n万]*万)/u', $pageText, $m)) {
|
||||
$newPrice = trim(preg_replace('/[^0-9.]/', '', $m[1]));
|
||||
}
|
||||
}
|
||||
|
||||
// 比新车省
|
||||
$priceDifference = '';
|
||||
if ($pageText) {
|
||||
if (preg_match('/比新车省[::]\s*([^\r\n万]*万)/u', $pageText, $m)) {
|
||||
$priceDifference = trim(preg_replace('/[^0-9.]/', '', $m[1]));
|
||||
}
|
||||
}
|
||||
|
||||
// 车况介绍
|
||||
$conditionDesc = '';
|
||||
if ($pageText) {
|
||||
if (preg_match('/车况介绍\s*(.*?)\s*推荐理由/su', $pageText, $m)) {
|
||||
$conditionDesc = trim($m[1]);
|
||||
}
|
||||
}
|
||||
|
||||
$jsonArray = json_decode($jsonText, true);
|
||||
|
||||
// 车辆相册
|
||||
$images = $jsonArray['props']['pageProps']['skuDetail']['head_images'] ?? [];
|
||||
|
||||
// 车辆ID
|
||||
$carId = $jsonArray['props']['pageProps']['skuDetail']['car_info']['car_id'] ?? 0;
|
||||
|
||||
// 车辆年份
|
||||
$year = $jsonArray['props']['pageProps']['skuDetail']['car_info']['year'] ?? 0;
|
||||
|
||||
// 车辆ID URL
|
||||
$carIdUrl = $carId ? ('https://www.dongchedi.com/auto/params-carIds-' . $carId) : '';
|
||||
|
||||
// 能源类型
|
||||
$fuelForm = $jsonArray['props']['pageProps']['skuDetail']['car_config_overview']['power']['fuel_form'] ?? '';
|
||||
$newFuelForm = $jsonArray['props']['pageProps']['skuDetail']['car_config_overview']['new_energy_power']['fuel_form'] ?? '';
|
||||
$resultFuelForm = $fuelForm === '' ? $newFuelForm : $fuelForm;
|
||||
|
||||
// 使用里程
|
||||
$importantText = $jsonArray['props']['pageProps']['skuDetail']['important_text'] ?? '';
|
||||
$importantTextArray = explode(' | ', $importantText);
|
||||
$usageMileage = str_replace('万公里', '', isset($importantTextArray[1]) ? $importantTextArray[1] : '');
|
||||
|
||||
// 车辆名称 品牌、车系、车型
|
||||
$brandName = $jsonArray['props']['pageProps']['skuDetail']['car_info']['brand_name'] ?? '';
|
||||
$seriesName = $jsonArray['props']['pageProps']['skuDetail']['car_info']['series_name'] ?? '';
|
||||
$carName = $jsonArray['props']['pageProps']['skuDetail']['car_info']['car_name'] ?? '';
|
||||
|
||||
// 商家信息
|
||||
$shopInfo = $jsonArray['props']['pageProps']['skuDetail']['shop_info'] ?? [];
|
||||
$formatShopInfo = [
|
||||
'shop_id' => $shopInfo['shop_id'] ?? 0,
|
||||
'shop_name' => $shopInfo['shop_short_name'] ?? '',
|
||||
'shop_address' => $shopInfo['shop_address'] ?? ''
|
||||
];
|
||||
|
||||
// 简介参数
|
||||
$otherParams = $jsonArray['props']['pageProps']['skuDetail']['other_params'] ?? [];
|
||||
// 排量
|
||||
$displacement = $this->getOtherParamValue($otherParams, '排量');
|
||||
// 变速箱
|
||||
$gearbox = $this->getOtherParamValue($otherParams, '变速箱');
|
||||
// 格式化车辆信息
|
||||
$formatCarInfo = [
|
||||
'displacement' => $displacement,
|
||||
'gearbox' => $gearbox
|
||||
];
|
||||
|
||||
// 整理车辆信息
|
||||
$info['used_car_id'] = $this->getLastIdFromUrl($url);
|
||||
$info['car_id'] = $carId;
|
||||
$info['url'] = $url;
|
||||
$info['title'] = $title;
|
||||
$info['price'] = str_replace('万', '', $this->decodeDongchediPrice($price));
|
||||
$info['new_price'] = str_replace('万', '', $newPrice);
|
||||
$info['price_difference'] = str_replace('万', '', $priceDifference);
|
||||
$info['condition_desc'] = $conditionDesc;
|
||||
$info['images'] = json_encode($images);
|
||||
$info['car_id_url'] = $carIdUrl;
|
||||
$info['used_detail'] = $jsonText;
|
||||
$info['format_shop_info'] = json_encode($formatShopInfo);
|
||||
$info['fuel_form'] = $resultFuelForm;
|
||||
$info['classify_energy'] = $this->detectEnergyType($resultFuelForm);
|
||||
$info['usage_mileage'] = $usageMileage;
|
||||
$info['brand_name'] = $brandName;
|
||||
$info['series_name'] = $seriesName;
|
||||
$info['car_name'] = $carName;
|
||||
$info['year'] = $year;
|
||||
$info['format_car_info'] = json_encode($formatCarInfo);
|
||||
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置信息
|
||||
*/
|
||||
public function getConfigDetail(string $html = '')
|
||||
{
|
||||
$ql = QueryList::html($html);
|
||||
|
||||
$jsonText = $ql->find('#__NEXT_DATA__')->text();
|
||||
|
||||
$raw = json_decode($jsonText, true);
|
||||
$properties = $raw['props']['pageProps']['rawData']['properties'] ?? [];
|
||||
$carInfo = $raw['props']['pageProps']['rawData']['car_info'][0] ?? [];
|
||||
|
||||
$valuesMap = $this->buildValuesMapFromCarInfo($carInfo);
|
||||
$groups = $this->buildCarSpecGroups($properties, $valuesMap);
|
||||
|
||||
$data['format_car_info'] = [
|
||||
'market_time' => $this->getFieldValue($groups, 'market_time'), // 上市时间
|
||||
'driver_form' => $this->getFieldValue($groups, 'driver_form'), // 驱动形式
|
||||
'seat_count' => $this->getFieldValue($groups, 'seat_count'), // 座位数
|
||||
'engine_description' => $this->getFieldValue($groups, 'engine_description'), // 发动机
|
||||
'engine_max_power_horsepower' => $this->getFieldValue($groups, 'engine_max_power_horsepower'), // 发动机最大功率
|
||||
'electric_description' => $this->getFieldValue($groups, 'electric_description'), //电动机
|
||||
'battery_capacity' => $this->getFieldValue($groups, 'battery_capacity'), //电池容量
|
||||
'battery_type' => $this->getFieldValue($groups, 'battery_type'), // 电池类型
|
||||
'recharge_mileage' => $this->getFieldValue($groups, 'recharge_mileage') // 续航里程
|
||||
];
|
||||
$data['groups'] = $groups;
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从车辆配置 JSON 数据中获取能源类型
|
||||
*
|
||||
* @param array $data JSON 数据
|
||||
* @return string|null 能源类型,如 '油电混合'、'纯电动'、
|
||||
*/
|
||||
public function getFuelFormByConfigInfo(array $data)
|
||||
{
|
||||
$fuelForm = null;
|
||||
foreach ($data[0]['items'] as $item) {
|
||||
if ($item['key'] === 'fuel_form') {
|
||||
$fuelForm = $item['value'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $fuelForm;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从车辆配置 JSON 数据中根据字段 key 获取对应的 value
|
||||
*
|
||||
* @param string|array $data JSON 字符串 或 已解析的数组
|
||||
* @param string $field 要查找的字段 key(如 'fuel_form')
|
||||
* @return string|null 找到则返回 value,否则返回 null
|
||||
*/
|
||||
public function getFieldValue($data, string $field)
|
||||
{
|
||||
// 如果传入的是 JSON 字符串,先解码
|
||||
if (is_string($data)) {
|
||||
$data = json_decode($data, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
return null; // JSON 格式错误
|
||||
}
|
||||
}
|
||||
|
||||
// 遍历第一层(假设结构是 [ { "items": [...] } ])
|
||||
foreach ($data as $group) {
|
||||
if (isset($group['items']) && is_array($group['items'])) {
|
||||
foreach ($group['items'] as $item) {
|
||||
if (isset($item['key']) && $item['key'] === $field) {
|
||||
return $item['value'] ?? null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null; // 未找到
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 other_params 数组中根据 name 获取对应的 value
|
||||
*
|
||||
* @param array $otherParams 格式为 [ ['name' => 'xxx', 'value' => 'yyy'], ... ]
|
||||
* @param string $name 要查找的字段名称(如 '上牌地')
|
||||
* @return string|null 找到则返回 value,否则返回 null
|
||||
*/
|
||||
public function getOtherParamValue(array $otherParams, string $name): ?string
|
||||
{
|
||||
foreach ($otherParams as $item) {
|
||||
if (isset($item['name']) && $item['name'] === $name) {
|
||||
return $item['value'] ?? null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据给定字符串判断车辆能源类型
|
||||
*
|
||||
* 返回:
|
||||
* - RE 新能源(纯电、插电混动、增程、氢燃料等)
|
||||
* - CE 传统能源(汽油、柴油、天然气、非插电混动等)
|
||||
* - UNKNOWN 无法判断
|
||||
*
|
||||
* @param string $text 例如:'2022款 某某牌 2.0L 油电混合 双擎'
|
||||
* @return string
|
||||
*/
|
||||
public function detectEnergyType(string $text): string
|
||||
{
|
||||
if (function_exists('mb_internal_encoding')) {
|
||||
mb_internal_encoding('UTF-8');
|
||||
}
|
||||
|
||||
// 明确定义的新能源关键字(纯电 / 插电 / 增程 / 氢 / 太阳能 等)
|
||||
$newEnergyKeywords = [
|
||||
// 纯电
|
||||
'纯电', '纯电动', '电动车', 'BEV',
|
||||
// 插电式混合
|
||||
'插电混动', '插电式混合', '插电式', '插电', 'PHEV',
|
||||
// 增程式
|
||||
'增程式', '增程', 'REEV',
|
||||
// 氢能 / 燃料电池
|
||||
'氢能', '氢燃料', '燃料电池', 'FCEV',
|
||||
// 其它
|
||||
'太阳能',
|
||||
];
|
||||
|
||||
// 传统能源关键字(包括燃油/燃气)
|
||||
$traditionalKeywords = [
|
||||
'汽油',
|
||||
'柴油',
|
||||
'天然气', 'LNG', 'CNG',
|
||||
];
|
||||
|
||||
// 普通混动关键字(按你的要求,统一归为传统能源)
|
||||
$hybridKeywords = [
|
||||
'油电混合', '油电混动', '油电',
|
||||
'混动', // 不带“插电”的混动
|
||||
'双擎', // 丰田常见表达
|
||||
'HEV',
|
||||
];
|
||||
|
||||
// 辅助:不区分大小写的包含判断,优先 mb_stripos
|
||||
$contains = function (string $haystack, string $needle): bool {
|
||||
if ($needle === '') {
|
||||
return false;
|
||||
}
|
||||
if (function_exists('mb_stripos')) {
|
||||
return mb_stripos($haystack, $needle) !== false;
|
||||
}
|
||||
return stripos($haystack, $needle) !== false;
|
||||
};
|
||||
|
||||
// 1. 先判断“明确新能源”关键词(插电、增程、氢、纯电等)
|
||||
foreach ($newEnergyKeywords as $kw) {
|
||||
if ($contains($text, $kw)) {
|
||||
return 'RE';
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 再判断“普通油电混动”——按照你的业务,一律归为传统能源
|
||||
foreach ($hybridKeywords as $kw) {
|
||||
if ($contains($text, $kw)) {
|
||||
return 'CE';
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 再判断传统能源(纯燃油/气)
|
||||
foreach ($traditionalKeywords as $kw) {
|
||||
if ($contains($text, $kw)) {
|
||||
return 'CE';
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 都没匹配上
|
||||
return 'UNKNOWN';
|
||||
}
|
||||
|
||||
/**
|
||||
* 二手信息入库
|
||||
*/
|
||||
public function saveUsedcarBasicToDb(array $data = [])
|
||||
{
|
||||
$data['created_at'] = time();
|
||||
$data['updated_at'] = time();
|
||||
return Db::table('usedcar_crawl_results')->insertGetId($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新信息入库
|
||||
*/
|
||||
public function saveUsedcarConfigToDb(array $data = [], int $id = 0)
|
||||
{
|
||||
$info = Db::table('usedcar_crawl_results')->find($id);
|
||||
|
||||
if (!$info) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$groups = $data['groups'];
|
||||
$formatCarInfo = array_merge($data['format_car_info'], json_decode($info->format_car_info, true));
|
||||
|
||||
return Db::table('usedcar_crawl_results')
|
||||
->where('id', $id)
|
||||
->update([
|
||||
'format_car_info' => json_encode($formatCarInfo, JSON_UNESCAPED_UNICODE),
|
||||
'config_detail' => json_encode($groups, JSON_UNESCAPED_UNICODE),
|
||||
'updated_at' => time()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 把 car_info 扁平成一个 [key => ['value' => xxx]] 的 map
|
||||
*
|
||||
* @param array $carInfo rawData['car_info'][0]
|
||||
* @return array
|
||||
*/
|
||||
public function buildValuesMapFromCarInfo(array $carInfo): array
|
||||
{
|
||||
$valuesMap = [];
|
||||
|
||||
// 1) car_info 顶层的一些字段(official_price、series_name 等)
|
||||
foreach ($carInfo as $k => $v) {
|
||||
if ($k === 'info' || is_array($v)) {
|
||||
continue;
|
||||
}
|
||||
$valuesMap[$k] = ['value' => $v];
|
||||
}
|
||||
|
||||
// 2) info 里的配置项
|
||||
if (!empty($carInfo['info']) && is_array($carInfo['info'])) {
|
||||
foreach ($carInfo['info'] as $k => $v) {
|
||||
if (is_array($v) && array_key_exists('value', $v)) {
|
||||
$valuesMap[$k] = $v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $valuesMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 type 重新分组:
|
||||
* - type=0,2 为分组(左侧分类)
|
||||
* - 其它 type 都视为该分组下的字段
|
||||
*
|
||||
* @param array $properties rawData['properties']
|
||||
* @param array $valuesMap 来自 buildValuesMapFromCarInfo()
|
||||
* @return array
|
||||
*/
|
||||
public function buildCarSpecGroups(array $properties, array $valuesMap): array
|
||||
{
|
||||
$groups = [];
|
||||
$currentKey = null; // 当前分组 key
|
||||
|
||||
foreach ($properties as $item) {
|
||||
$type = (int)($item['type'] ?? 1);
|
||||
$key = $item['key'] ?? '';
|
||||
$text = $item['text'] ?? '';
|
||||
$subList = $item['sub_list'] ?? null;
|
||||
|
||||
// ---------- 1) 分类字段:type = 0 / 2 ----------
|
||||
if (in_array($type, [0, 2], true)) {
|
||||
$currentKey = $key ?: ($text ?: ('group_' . count($groups)));
|
||||
|
||||
if (!isset($groups[$currentKey])) {
|
||||
$groups[$currentKey] = [
|
||||
'group_key' => $currentKey,
|
||||
'group_text' => $text,
|
||||
'group_type' => $type,
|
||||
'items' => [],
|
||||
];
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// ---------- 2) 普通字段:挂到最近的分类下 ----------
|
||||
if ($currentKey === null) {
|
||||
// 如果一开始就没有遇到分类,给一个默认分组
|
||||
$currentKey = '_default';
|
||||
$groups[$currentKey] = [
|
||||
'group_key' => '_default',
|
||||
'group_text' => '其他',
|
||||
'group_type' => -1,
|
||||
'items' => [],
|
||||
];
|
||||
}
|
||||
|
||||
// 取 value
|
||||
$value = '';
|
||||
if ($key && isset($valuesMap[$key])) {
|
||||
$value = trim((string)($valuesMap[$key]['value'] ?? ''));
|
||||
}
|
||||
|
||||
$node = [
|
||||
'key' => $key,
|
||||
'text' => $text,
|
||||
'value' => $value,
|
||||
];
|
||||
|
||||
// ---------- 3) sub_list 子项(有就当成 children) ----------
|
||||
if (is_array($subList) && !empty($subList)) {
|
||||
$children = [];
|
||||
foreach ($subList as $sub) {
|
||||
$subKey = $sub['key'] ?? '';
|
||||
$subText = $sub['text'] ?? '';
|
||||
$subValue = '';
|
||||
|
||||
if ($subKey && isset($valuesMap[$subKey])) {
|
||||
$subValue = trim((string)($valuesMap[$subKey]['value'] ?? ''));
|
||||
}
|
||||
|
||||
$children[] = [
|
||||
'key' => $subKey,
|
||||
'text' => $subText,
|
||||
'value' => $subValue,
|
||||
];
|
||||
}
|
||||
|
||||
$node['children'] = $children;
|
||||
}
|
||||
|
||||
$groups[$currentKey]['items'][] = $node;
|
||||
}
|
||||
|
||||
// 你可以按需返回关联数组或索引数组,这里转成索引数组方便前端遍历
|
||||
return array_values($groups);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转义价格
|
||||
*/
|
||||
protected function decodeDongchediPrice(string $str): string
|
||||
{
|
||||
// 映射表:PUA 字符 => 数字 / 万
|
||||
$map = [
|
||||
"\u{E53D}" => '1',
|
||||
"\u{E3F0}" => '2',
|
||||
"\u{E422}" => '3',
|
||||
"\u{E42C}" => '4',
|
||||
"\u{E49C}" => '5',
|
||||
"\u{E42B}" => '6',
|
||||
"\u{E4FE}" => '7',
|
||||
"\u{E548}" => '8',
|
||||
"\u{E4C8}" => '9',
|
||||
"\u{E453}" => '0',
|
||||
"\u{E45F}" => '万',
|
||||
];
|
||||
|
||||
return strtr($str, $map);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取URL最后ID
|
||||
*/
|
||||
protected function getLastIdFromUrl($url)
|
||||
{
|
||||
// 方法1:使用正则表达式提取最后的数字ID
|
||||
if (preg_match('/\/(\d+)$/', $url, $matches)) {
|
||||
return $matches[1];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载单张图片
|
||||
*
|
||||
* @param string $url 图片完整 URL
|
||||
* @param string $saveDir 保存目录(如 /www/usedcar_images/2025-11-20)
|
||||
* @param string|null $filename 指定文件名(可选,不含路径),为空则自动生成
|
||||
* @param array $headers 额外 HTTP 头(可选)
|
||||
*
|
||||
* @return string|null 成功返回保存路径,失败返回 null
|
||||
*/
|
||||
protected function downloadImage(string $url, string $saveDir, ?string $filename = null, array $headers = []): ?string
|
||||
{
|
||||
// 1. 目录检查 & 创建
|
||||
if (!is_dir($saveDir)) {
|
||||
if (!mkdir($saveDir, 0777, true) && !is_dir($saveDir)) {
|
||||
echo "[Image] 创建目录失败:{$saveDir}\n";
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 自动生成文件名(基于 URL md5 + 原扩展名)
|
||||
if ($filename === null) {
|
||||
$path = parse_url($url, PHP_URL_PATH); // /xxx/xxx.jpg
|
||||
$ext = pathinfo($path, PATHINFO_EXTENSION); // jpg 或 webp 之类
|
||||
|
||||
if ($ext === '') {
|
||||
$ext = 'jpg'; // 没有扩展名时兜底
|
||||
}
|
||||
|
||||
$filename = md5($url) . '.' . $ext;
|
||||
}
|
||||
|
||||
$savePath = rtrim($saveDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $filename;
|
||||
|
||||
// 3. 组装 HTTP 头
|
||||
$defaultHeaders = [
|
||||
'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
|
||||
. 'AppleWebKit/537.36 (KHTML, like Gecko) '
|
||||
. 'Chrome/120.0.0.0 Safari/537.36',
|
||||
'Accept: image/avif,image/webp,image/apng,image/*,*/*;q=0.8',
|
||||
'Referer: https://www.dongchedi.com/', // 很多图床需要 Referer
|
||||
];
|
||||
|
||||
foreach ($headers as $h) {
|
||||
$defaultHeaders[] = $h;
|
||||
}
|
||||
|
||||
// 4. 使用 cURL 下载
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_CONNECTTIMEOUT => 5,
|
||||
CURLOPT_TIMEOUT => 15,
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
CURLOPT_SSL_VERIFYHOST => false,
|
||||
CURLOPT_HTTPHEADER => $defaultHeaders,
|
||||
]);
|
||||
|
||||
$data = curl_exec($ch);
|
||||
$errNo = curl_errno($ch);
|
||||
$errMsg = curl_error($ch);
|
||||
$code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($errNo !== 0) {
|
||||
echo "[Image] 下载失败(cURL):{$url} | {$errNo} {$errMsg}\n";
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($code !== 200 || $data === false || $data === '') {
|
||||
echo "[Image] 下载失败(HTTP {$code}):{$url}\n";
|
||||
return null;
|
||||
}
|
||||
|
||||
// 5. 写入本地文件
|
||||
$bytes = file_put_contents($savePath, $data);
|
||||
if ($bytes === false) {
|
||||
echo "[Image] 保存失败:{$savePath}\n";
|
||||
return null;
|
||||
}
|
||||
|
||||
echo "[Image] 下载成功:{$url} => {$savePath}\n";
|
||||
|
||||
return $savePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量下载相册
|
||||
*
|
||||
* @param array $urls 图片 URL 数组
|
||||
* @param string $saveDir 保存目录
|
||||
*
|
||||
* @return array [原始 URL => 保存路径或 null]
|
||||
*/
|
||||
protected function downloadImageAlbum(array $urls, string $saveDir): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
foreach ($urls as $url) {
|
||||
$url = trim($url);
|
||||
if ($url === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 如果存在 // 开头的相对协议,补全
|
||||
if (strpos($url, '//') === 0) {
|
||||
$url = 'https:' . $url;
|
||||
}
|
||||
|
||||
$path = $this->downloadImage($url, $saveDir);
|
||||
$result[$url] = $path;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,798 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\utils\crawl\service;
|
||||
|
||||
use app\common\model\UsedcarCrawlResultsImages;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use QL\QueryList;
|
||||
use support\Redis;
|
||||
use support\Db;
|
||||
use support\exception\BusinessException;
|
||||
use Throwable;
|
||||
|
||||
class UsedCarCrawl_bak2
|
||||
{
|
||||
// 默认请求头
|
||||
private const DEFAULT_HEADERS = [
|
||||
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
'Accept-Language' => 'zh-CN,zh;q=0.9',
|
||||
'Referer' => 'https://www.dongchedi.com/'
|
||||
];
|
||||
|
||||
// Redis队列键名
|
||||
private const REDIS_QUEUE_KEY = 'usedcar:urls';
|
||||
|
||||
// 能源类型常量
|
||||
public const ENERGY_TYPE_RENEWABLE = 'RE';
|
||||
public const ENERGY_TYPE_CONVENTIONAL = 'CE';
|
||||
public const ENERGY_TYPE_UNKNOWN = 'UNKNOWN';
|
||||
|
||||
/**
|
||||
* 插入队列url
|
||||
*
|
||||
* @param string $url URL地址
|
||||
* @param string $scene 场景:usedcar(二手信息) config(配置信息)
|
||||
* @param int $id 记录ID
|
||||
* @return int
|
||||
*/
|
||||
public function pushUrl(string $url = '', string $scene = 'usedcar', int $id = 0): int
|
||||
{
|
||||
$data = [
|
||||
'scene' => $scene,
|
||||
'url' => $url,
|
||||
'id' => $id
|
||||
];
|
||||
|
||||
return Redis::lpush(self::REDIS_QUEUE_KEY, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取队列url
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function popUrl(): ?array
|
||||
{
|
||||
$data = Redis::rPop(self::REDIS_QUEUE_KEY);
|
||||
if (!$data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return json_decode($data, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行抓取任务
|
||||
*
|
||||
* @return bool
|
||||
* @throws BusinessException
|
||||
*/
|
||||
public function execute(): bool
|
||||
{
|
||||
try {
|
||||
$task = $this->popUrl();
|
||||
|
||||
if (!$task) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$html = $this->fetchHtml($task['url']);
|
||||
|
||||
switch ($task['scene']) {
|
||||
case 'usedcar':
|
||||
$info = $this->parseUsedCarDetail($html, $task['url']);
|
||||
$id = $this->saveUsedCarBasicInfo($info);
|
||||
$this->uploadAndRemove(json_decode($info['images'], true), public_path('uploads/usedcar/' . $id), $info['used_car_id'], $id);
|
||||
$this->pushUrl($info['car_id_url'], 'config', $id);
|
||||
break;
|
||||
|
||||
case 'config':
|
||||
$data = $this->parseConfigDetail($html);
|
||||
$this->saveUsedCarConfigInfo($data, $task['id']);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new BusinessException("未知的任务场景: " . $task['scene']);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (GuzzleException $e) {
|
||||
throw new BusinessException("网络请求失败: " . $e->getMessage());
|
||||
} catch (Throwable $th) {
|
||||
throw new BusinessException("执行任务出错: " . $th->getMessage() . ' | 行号: ' . $th->getLine() . ' | 文件: ' . $th->getFile());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取网页HTML内容
|
||||
*
|
||||
* @param string $url
|
||||
* @return string
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
public function fetchHtml(string $url): string
|
||||
{
|
||||
$client = new Client();
|
||||
$response = $client->request('GET', $url, [
|
||||
'headers' => self::DEFAULT_HEADERS
|
||||
]);
|
||||
return (string)$response->getBody();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析二手车详情页信息
|
||||
*
|
||||
* @param string $html
|
||||
* @param string $url
|
||||
* @return array
|
||||
*/
|
||||
public function parseUsedCarDetail(string $html = '', string $url = ''): array
|
||||
{
|
||||
$ql = QueryList::html($html);
|
||||
|
||||
$title = $ql->find('h1')->text();
|
||||
$price = $ql->find('.head-info_price-wrap__Y4bxi > p')->text();
|
||||
$pageText = $ql->find('body')->text();
|
||||
$jsonText = $ql->find('#__NEXT_DATA__')->text();
|
||||
|
||||
// 解析JSON数据
|
||||
$jsonArray = json_decode($jsonText, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
throw new BusinessException("JSON解析失败: " . json_last_error_msg());
|
||||
}
|
||||
|
||||
// 车辆相册
|
||||
$images = $jsonArray['props']['pageProps']['skuDetail']['head_images'] ?? [];
|
||||
|
||||
// 车辆ID
|
||||
$carId = $jsonArray['props']['pageProps']['skuDetail']['car_info']['car_id'] ?? 0;
|
||||
|
||||
// 车辆年份
|
||||
$year = $jsonArray['props']['pageProps']['skuDetail']['car_info']['year'] ?? 0;
|
||||
|
||||
// 车辆ID URL
|
||||
$carIdUrl = $carId ? ('https://www.dongchedi.com/auto/params-carIds-' . $carId) : '';
|
||||
|
||||
// 能源类型
|
||||
$fuelForm = $jsonArray['props']['pageProps']['skuDetail']['car_config_overview']['power']['fuel_form'] ?? '';
|
||||
$newFuelForm = $jsonArray['props']['pageProps']['skuDetail']['car_config_overview']['new_energy_power']['fuel_form'] ?? '';
|
||||
$resultFuelForm = $fuelForm === '' ? $newFuelForm : $fuelForm;
|
||||
|
||||
// 使用里程
|
||||
$importantText = $jsonArray['props']['pageProps']['skuDetail']['important_text'] ?? '';
|
||||
$importantTextArray = explode(' | ', $importantText);
|
||||
$usageMileage = str_replace('万公里', '', isset($importantTextArray[1]) ? $importantTextArray[1] : '');
|
||||
|
||||
// 车辆名称 品牌、车系、车型
|
||||
$brandName = $jsonArray['props']['pageProps']['skuDetail']['car_info']['brand_name'] ?? '';
|
||||
$seriesName = $jsonArray['props']['pageProps']['skuDetail']['car_info']['series_name'] ?? '';
|
||||
$carName = $jsonArray['props']['pageProps']['skuDetail']['car_info']['car_name'] ?? '';
|
||||
|
||||
// 商家信息
|
||||
$shopInfo = $jsonArray['props']['pageProps']['skuDetail']['shop_info'] ?? [];
|
||||
$formatShopInfo = [
|
||||
'shop_id' => $shopInfo['shop_id'] ?? 0,
|
||||
'shop_name' => $shopInfo['shop_short_name'] ?? '',
|
||||
'shop_address' => $shopInfo['shop_address'] ?? ''
|
||||
];
|
||||
|
||||
// 简介参数
|
||||
$otherParams = $jsonArray['props']['pageProps']['skuDetail']['other_params'] ?? [];
|
||||
// 排量
|
||||
$displacement = $this->getParamValue($otherParams, '排量');
|
||||
// 变速箱
|
||||
$gearbox = $this->getParamValue($otherParams, '变速箱');
|
||||
// 格式化车辆信息
|
||||
$formatCarInfo = [
|
||||
'displacement' => $displacement,
|
||||
'gearbox' => $gearbox
|
||||
];
|
||||
|
||||
// 提取新车指导价
|
||||
$newPrice = $this->extractPatternValue($pageText, '/新车指导价[::]\s*([^\r\n万]*万)/u');
|
||||
|
||||
// 提取比新车省金额
|
||||
$priceDifference = $this->extractPatternValue($pageText, '/比新车省[::]\s*([^\r\n万]*万)/u');
|
||||
|
||||
// 提取车况介绍
|
||||
$conditionDesc = '';
|
||||
if ($pageText) {
|
||||
if (preg_match('/车况介绍\s*(.*?)\s*推荐理由/su', $pageText, $m)) {
|
||||
$conditionDesc = trim($m[1]);
|
||||
}
|
||||
}
|
||||
|
||||
// 整理车辆信息
|
||||
return [
|
||||
'used_car_id' => $this->extractIdFromUrl($url),
|
||||
'car_id' => $carId,
|
||||
'url' => $url,
|
||||
'title' => $title,
|
||||
'price' => str_replace('万', '', $this->decodePrice($price)),
|
||||
'new_price' => str_replace('万', '', $newPrice),
|
||||
'price_difference' => str_replace('万', '', $priceDifference),
|
||||
'condition_desc' => $conditionDesc,
|
||||
'images' => json_encode($images, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||
'car_id_url' => $carIdUrl,
|
||||
'used_detail' => $jsonText,
|
||||
'format_shop_info' => json_encode($formatShopInfo, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||
'fuel_form' => $resultFuelForm,
|
||||
'classify_energy' => $this->detectEnergyType($resultFuelForm),
|
||||
'usage_mileage' => $usageMileage,
|
||||
'brand_name' => $brandName,
|
||||
'series_name' => $seriesName,
|
||||
'car_name' => $carName,
|
||||
'year' => $year,
|
||||
'format_car_info' => json_encode($formatCarInfo, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析配置详情页信息
|
||||
*
|
||||
* @param string $html
|
||||
* @return array
|
||||
*/
|
||||
public function parseConfigDetail(string $html = ''): array
|
||||
{
|
||||
$ql = QueryList::html($html);
|
||||
$jsonText = $ql->find('#__NEXT_DATA__')->text();
|
||||
|
||||
$raw = json_decode($jsonText, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
throw new BusinessException("配置页面JSON解析失败: " . json_last_error_msg());
|
||||
}
|
||||
|
||||
$properties = $raw['props']['pageProps']['rawData']['properties'] ?? [];
|
||||
$carInfo = $raw['props']['pageProps']['rawData']['car_info'][0] ?? [];
|
||||
|
||||
$valuesMap = $this->buildValuesMap($carInfo);
|
||||
$groups = $this->buildCarSpecGroups($properties, $valuesMap);
|
||||
|
||||
return [
|
||||
'format_car_info' => [
|
||||
'market_time' => $this->getFieldValue($groups, 'market_time'), // 上市时间
|
||||
'driver_form' => $this->getFieldValue($groups, 'driver_form'), // 驱动形式
|
||||
'seat_count' => $this->getFieldValue($groups, 'seat_count'), // 座位数
|
||||
'engine_description' => $this->getFieldValue($groups, 'engine_description'), // 发动机
|
||||
'engine_max_power_horsepower' => $this->getFieldValue($groups, 'engine_max_power_horsepower'), // 发动机最大功率
|
||||
'electric_description' => $this->getFieldValue($groups, 'electric_description'), //电动机
|
||||
'battery_capacity' => $this->getFieldValue($groups, 'battery_capacity'), //电池容量
|
||||
'battery_type' => $this->getFieldValue($groups, 'battery_type'), // 电池类型
|
||||
'recharge_mileage' => $this->getFieldValue($groups, 'recharge_mileage') // 续航里程
|
||||
],
|
||||
'groups' => $groups
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 从车辆配置中获取能源类型
|
||||
*
|
||||
* @param array $data JSON数据
|
||||
* @return string|null 能源类型
|
||||
*/
|
||||
public function getFuelFormByConfig(array $data): ?string
|
||||
{
|
||||
if (empty($data[0]['items'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach ($data[0]['items'] as $item) {
|
||||
if (isset($item['key']) && $item['key'] === 'fuel_form') {
|
||||
return $item['value'] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从数据中根据字段key获取值
|
||||
*
|
||||
* @param mixed $data 数据
|
||||
* @param string $field 字段名
|
||||
* @return string|null
|
||||
*/
|
||||
public function getFieldValue($data, string $field): ?string
|
||||
{
|
||||
// 如果传入的是 JSON 字符串,先解码
|
||||
if (is_string($data)) {
|
||||
$data = json_decode($data, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果不是数组,直接返回null
|
||||
if (!is_array($data)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 遍历寻找对应字段
|
||||
foreach ($data as $group) {
|
||||
if (isset($group['items']) && is_array($group['items'])) {
|
||||
foreach ($group['items'] as $item) {
|
||||
if (isset($item['key']) && $item['key'] === $field) {
|
||||
return $item['value'] ?? null;
|
||||
}
|
||||
|
||||
// 检查子元素
|
||||
if (isset($item['children']) && is_array($item['children'])) {
|
||||
foreach ($item['children'] as $child) {
|
||||
if (isset($child['key']) && $child['key'] === $field) {
|
||||
return $child['value'] ?? null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从参数列表中获取指定名称的值
|
||||
*
|
||||
* @param array $params 参数列表
|
||||
* @param string $name 名称
|
||||
* @return string|null
|
||||
*/
|
||||
public function getParamValue(array $params, string $name): ?string
|
||||
{
|
||||
foreach ($params as $item) {
|
||||
if (isset($item['name']) && $item['name'] === $name) {
|
||||
return $item['value'] ?? null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断车辆能源类型
|
||||
*
|
||||
* @param string $text 描述文本
|
||||
* @return string 能源类型标识
|
||||
*/
|
||||
public function detectEnergyType(string $text): string
|
||||
{
|
||||
if (function_exists('mb_internal_encoding')) {
|
||||
mb_internal_encoding('UTF-8');
|
||||
}
|
||||
|
||||
// 明确定义的新能源关键字
|
||||
$newEnergyKeywords = [
|
||||
// 纯电
|
||||
'纯电', '纯电动', '电动车', 'BEV',
|
||||
// 插电式混合
|
||||
'插电混动', '插电式混合', '插电式', '插电', 'PHEV',
|
||||
// 增程式
|
||||
'增程式', '增程', 'REEV',
|
||||
// 氢能 / 燃料电池
|
||||
'氢能', '氢燃料', '燃料电池', 'FCEV',
|
||||
// 其它
|
||||
'太阳能',
|
||||
];
|
||||
|
||||
// 传统能源关键字
|
||||
$traditionalKeywords = [
|
||||
'汽油',
|
||||
'柴油',
|
||||
'天然气', 'LNG', 'CNG',
|
||||
];
|
||||
|
||||
// 普通混动关键字(归为传统能源)
|
||||
$hybridKeywords = [
|
||||
'油电混合', '油电混动', '油电',
|
||||
'混动', // 不带"插电"的混动
|
||||
'双擎', // 丰田常见表达
|
||||
'HEV',
|
||||
];
|
||||
|
||||
// 辅助函数:不区分大小写的包含判断
|
||||
$contains = function (string $haystack, string $needle): bool {
|
||||
if ($needle === '') {
|
||||
return false;
|
||||
}
|
||||
if (function_exists('mb_stripos')) {
|
||||
return mb_stripos($haystack, $needle) !== false;
|
||||
}
|
||||
return stripos($haystack, $needle) !== false;
|
||||
};
|
||||
|
||||
// 1. 先判断"明确新能源"关键词
|
||||
foreach ($newEnergyKeywords as $kw) {
|
||||
if ($contains($text, $kw)) {
|
||||
return self::ENERGY_TYPE_RENEWABLE;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 再判断"普通油电混动"
|
||||
foreach ($hybridKeywords as $kw) {
|
||||
if ($contains($text, $kw)) {
|
||||
return self::ENERGY_TYPE_CONVENTIONAL;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 再判断传统能源
|
||||
foreach ($traditionalKeywords as $kw) {
|
||||
if ($contains($text, $kw)) {
|
||||
return self::ENERGY_TYPE_CONVENTIONAL;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 都没匹配上
|
||||
return self::ENERGY_TYPE_UNKNOWN;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存二手车基本信息到数据库
|
||||
*
|
||||
* @param array $data 数据
|
||||
* @return int 插入记录ID
|
||||
*/
|
||||
public function saveUsedCarBasicInfo(array $data): int
|
||||
{
|
||||
$data['created_at'] = time();
|
||||
$data['updated_at'] = time();
|
||||
return Db::table('usedcar_crawl_results')->insertGetId($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新配置信息到数据库
|
||||
*
|
||||
* @param array $data 配置数据
|
||||
* @param int $id 记录ID
|
||||
* @return bool|int
|
||||
*/
|
||||
public function saveUsedCarConfigInfo(array $data, int $id)
|
||||
{
|
||||
$record = Db::table('usedcar_crawl_results')->find($id);
|
||||
|
||||
if (!$record) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$groups = $data['groups'];
|
||||
$formatCarInfo = array_merge(
|
||||
$data['format_car_info'],
|
||||
json_decode($record->format_car_info, true)
|
||||
);
|
||||
|
||||
return Db::table('usedcar_crawl_results')
|
||||
->where('id', $id)
|
||||
->update([
|
||||
'format_car_info' => json_encode($formatCarInfo, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||
'config_detail' => json_encode($groups, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||
'updated_at' => time()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建值映射表
|
||||
*
|
||||
* @param array $carInfo 车辆信息
|
||||
* @return array
|
||||
*/
|
||||
public function buildValuesMap(array $carInfo): array
|
||||
{
|
||||
$valuesMap = [];
|
||||
|
||||
// 处理顶层字段
|
||||
foreach ($carInfo as $k => $v) {
|
||||
if ($k === 'info' || is_array($v)) {
|
||||
continue;
|
||||
}
|
||||
$valuesMap[$k] = ['value' => $v];
|
||||
}
|
||||
|
||||
// 处理info中的配置项
|
||||
if (!empty($carInfo['info']) && is_array($carInfo['info'])) {
|
||||
foreach ($carInfo['info'] as $k => $v) {
|
||||
if (is_array($v) && array_key_exists('value', $v)) {
|
||||
$valuesMap[$k] = $v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $valuesMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建车辆规格分组
|
||||
*
|
||||
* @param array $properties 属性列表
|
||||
* @param array $valuesMap 值映射表
|
||||
* @return array
|
||||
*/
|
||||
public function buildCarSpecGroups(array $properties, array $valuesMap): array
|
||||
{
|
||||
$groups = [];
|
||||
$currentKey = null;
|
||||
|
||||
foreach ($properties as $item) {
|
||||
$type = (int)($item['type'] ?? 1);
|
||||
$key = $item['key'] ?? '';
|
||||
$text = $item['text'] ?? '';
|
||||
$subList = $item['sub_list'] ?? null;
|
||||
|
||||
// 分类字段:type = 0 / 2
|
||||
if (in_array($type, [0, 2], true)) {
|
||||
$currentKey = $key ?: ($text ?: ('group_' . count($groups)));
|
||||
|
||||
if (!isset($groups[$currentKey])) {
|
||||
$groups[$currentKey] = [
|
||||
'group_key' => $currentKey,
|
||||
'group_text' => $text,
|
||||
'group_type' => $type,
|
||||
'items' => [],
|
||||
];
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// 普通字段:挂到最近的分类下
|
||||
if ($currentKey === null) {
|
||||
// 如果一开始就没有遇到分类,给一个默认分组
|
||||
$currentKey = '_default';
|
||||
$groups[$currentKey] = [
|
||||
'group_key' => '_default',
|
||||
'group_text' => '其他',
|
||||
'group_type' => -1,
|
||||
'items' => [],
|
||||
];
|
||||
}
|
||||
|
||||
// 取 value
|
||||
$value = '';
|
||||
if ($key && isset($valuesMap[$key])) {
|
||||
$value = trim((string)($valuesMap[$key]['value'] ?? ''));
|
||||
}
|
||||
|
||||
$node = [
|
||||
'key' => $key,
|
||||
'text' => $text,
|
||||
'value' => $value,
|
||||
];
|
||||
|
||||
// 处理子项
|
||||
if (is_array($subList) && !empty($subList)) {
|
||||
$children = [];
|
||||
foreach ($subList as $sub) {
|
||||
$subKey = $sub['key'] ?? '';
|
||||
$subText = $sub['text'] ?? '';
|
||||
$subValue = '';
|
||||
|
||||
if ($subKey && isset($valuesMap[$subKey])) {
|
||||
$subValue = trim((string)($valuesMap[$subKey]['value'] ?? ''));
|
||||
}
|
||||
|
||||
$children[] = [
|
||||
'key' => $subKey,
|
||||
'text' => $subText,
|
||||
'value' => $subValue,
|
||||
];
|
||||
}
|
||||
|
||||
$node['children'] = $children;
|
||||
}
|
||||
|
||||
$groups[$currentKey]['items'][] = $node;
|
||||
}
|
||||
|
||||
// 转成索引数组方便前端遍历
|
||||
return array_values($groups);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解码懂车帝价格
|
||||
*
|
||||
* @param string $str 加密字符串
|
||||
* @return string 解码后的字符串
|
||||
*/
|
||||
protected function decodePrice(string $str): string
|
||||
{
|
||||
// 映射表:PUA 字符 => 数字 / 万
|
||||
$map = [
|
||||
"\u{E53D}" => '1',
|
||||
"\u{E3F0}" => '2',
|
||||
"\u{E422}" => '3',
|
||||
"\u{E42C}" => '4',
|
||||
"\u{E49C}" => '5',
|
||||
"\u{E42B}" => '6',
|
||||
"\u{E4FE}" => '7',
|
||||
"\u{E548}" => '8',
|
||||
"\u{E4C8}" => '9',
|
||||
"\u{E453}" => '0',
|
||||
"\u{E45F}" => '万',
|
||||
];
|
||||
|
||||
return strtr($str, $map);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从URL中提取ID
|
||||
*
|
||||
* @param string $url URL
|
||||
* @return int|false
|
||||
*/
|
||||
protected function extractIdFromUrl(string $url)
|
||||
{
|
||||
if (preg_match('/\/(\d+)$/', $url, $matches)) {
|
||||
return (int)$matches[1];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取符合正则表达式的值
|
||||
*
|
||||
* @param string $text 文本
|
||||
* @param string $pattern 正则表达式
|
||||
* @return string
|
||||
*/
|
||||
protected function extractPatternValue(string $text, string $pattern): string
|
||||
{
|
||||
$value = '';
|
||||
if ($text) {
|
||||
if (preg_match($pattern, $text, $m)) {
|
||||
$value = trim(preg_replace('/[^0-9.]/', '', $m[1]));
|
||||
}
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载单张图片
|
||||
*
|
||||
* @param string $url 图片完整URL
|
||||
* @param string $saveDir 保存目录
|
||||
* @param string|null $filename 指定文件名
|
||||
* @param array $headers 额外HTTP头
|
||||
* @return string|null 成功返回保存路径,失败返回null
|
||||
*/
|
||||
public function downloadImage(string $url, string $saveDir, ?string $filename = null, array $headers = []): ?string
|
||||
{
|
||||
// 目录检查 & 创建
|
||||
if (!is_dir($saveDir)) {
|
||||
if (!mkdir($saveDir, 0777, true) && !is_dir($saveDir)) {
|
||||
echo "[Image] 创建目录失败:{$saveDir}\n";
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 自动生成文件名
|
||||
if ($filename === null) {
|
||||
$path = parse_url($url, PHP_URL_PATH);
|
||||
$ext = pathinfo($path, PATHINFO_EXTENSION);
|
||||
|
||||
if ($ext === '') {
|
||||
$ext = 'jpg';
|
||||
}
|
||||
|
||||
$filename = md5($url) . '.' . $ext;
|
||||
}
|
||||
|
||||
$savePath = rtrim($saveDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $filename;
|
||||
|
||||
// 组装HTTP头
|
||||
$defaultHeaders = [
|
||||
'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
'Accept: image/avif,image/webp,image/apng,image/*,*/*;q=0.8',
|
||||
'Referer: https://www.dongchedi.com/',
|
||||
];
|
||||
|
||||
foreach ($headers as $h) {
|
||||
$defaultHeaders[] = $h;
|
||||
}
|
||||
|
||||
// 使用cURL下载
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_CONNECTTIMEOUT => 5,
|
||||
CURLOPT_TIMEOUT => 15,
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
CURLOPT_SSL_VERIFYHOST => false,
|
||||
CURLOPT_HTTPHEADER => $defaultHeaders,
|
||||
]);
|
||||
|
||||
$data = curl_exec($ch);
|
||||
$errNo = curl_errno($ch);
|
||||
$errMsg = curl_error($ch);
|
||||
$code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($errNo !== 0) {
|
||||
// echo "[Image] 下载失败(cURL):{$url} | {$errNo} {$errMsg}\n";
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($code !== 200 || $data === false || $data === '') {
|
||||
// echo "[Image] 下载失败(HTTP {$code}):{$url}\n";
|
||||
return null;
|
||||
}
|
||||
|
||||
// 写入本地文件
|
||||
$bytes = file_put_contents($savePath, $data);
|
||||
if ($bytes === false) {
|
||||
// echo "[Image] 保存失败:{$savePath}\n";
|
||||
return null;
|
||||
}
|
||||
|
||||
// echo "[Image] 下载成功:{$url} => {$savePath}\n";
|
||||
|
||||
return $savePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量下载相册
|
||||
*
|
||||
* @param array $urls 图片URL数组
|
||||
* @param string $saveDir 保存目录
|
||||
* @return array [原始URL => 保存路径或null]
|
||||
*/
|
||||
public function downloadImageAlbum(array $urls, string $saveDir): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
foreach ($urls as $url) {
|
||||
$url = trim($url);
|
||||
if ($url === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 补全协议
|
||||
if (strpos($url, '//') === 0) {
|
||||
$url = 'https:' . $url;
|
||||
}
|
||||
|
||||
$path = $this->downloadImage($url, $saveDir);
|
||||
$result[$url] = $path;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
public function uploadAndRemove($images, $path, $usedCarId, $id)
|
||||
{
|
||||
// 下载图片
|
||||
$filePatch = $this->downloadImageAlbum($images, $path);
|
||||
|
||||
// 检查图片
|
||||
if (empty($filePatch)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 上传图片
|
||||
foreach ($filePatch as $url => $path) {
|
||||
if ($path === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 上传云端
|
||||
$param = [
|
||||
'path' => $path,
|
||||
'scene' => 'usedcar_crawl',
|
||||
'category' => 'usedcar'
|
||||
];
|
||||
$up = new \app\common\service\UploadService();
|
||||
$result = $up->uploadLocalFile($param);
|
||||
|
||||
// 保存图片信息
|
||||
UsedcarCrawlResultsImages::create([
|
||||
'uc_id' => $id,
|
||||
'used_car_id' => $usedCarId,
|
||||
'original_name' => $result['original_name'],
|
||||
'file_path' => $result['file_name'],
|
||||
]);
|
||||
|
||||
// 删除本地文件
|
||||
@unlink($path);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,7 +119,7 @@ class MpdfDriver
|
||||
public function exportUsedCarPdf($field,$car,$user,$contactField,$contact,$lang)
|
||||
{
|
||||
/*$car = [
|
||||
'product_name' => '奥迪Q3 2013款 35 TFSI 舒适型',
|
||||
'title' => '奥迪Q3 2013款 35 TFSI 舒适型',
|
||||
'vin' => '',
|
||||
'price' => 450000,
|
||||
'source_city' => '北京',
|
||||
@@ -154,15 +154,15 @@ class MpdfDriver
|
||||
'default_font' => 'sun-exta'
|
||||
]);
|
||||
|
||||
// $mpdf->SetProtection(//限制篡改或盗用
|
||||
// ['print'], // 允许的权限 copy:允许复制、print:允许打印
|
||||
// '', // 用户密码(可留空)
|
||||
// $car['code'] // 拥有者密码(一定要设置)
|
||||
// );
|
||||
$mpdf->SetProtection(//限制篡改或盗用
|
||||
['print'], // 允许的权限 copy:允许复制、print:允许打印
|
||||
'', // 用户密码(可留空)
|
||||
$car['code'] // 拥有者密码(一定要设置)
|
||||
);
|
||||
|
||||
//设置水印
|
||||
$mpdf->SetWatermarkText(
|
||||
$user['name_en'] ,
|
||||
$user['name'] .' '. $contact,
|
||||
0.08
|
||||
);
|
||||
$mpdf->showWatermarkText = true;
|
||||
@@ -186,7 +186,7 @@ class MpdfDriver
|
||||
$pdf = $mpdf->Output('车辆详情.pdf', 'S');
|
||||
$dir = runtime_path('pdf');
|
||||
if (!is_dir($dir)) mkdir($dir, 0777, true);
|
||||
$pdfName = 'invoice-' . $car['code'].'-'.$car['id'] . '-' . $lang . '-' .substr(time(),4,6). '.pdf';
|
||||
$pdfName = 'invoice-' . date('YmdHis').'-'.$car['id'] . '-' . $lang . '.pdf';
|
||||
$path = $dir . '/' . $pdfName;
|
||||
file_put_contents($path, $pdf);
|
||||
|
||||
|
||||
@@ -59,10 +59,10 @@
|
||||
<div class="row"><span class="label"><?= $contactField['name'] ?>:</span><?= $user['name'] ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($user['email'])): ?>
|
||||
<div class="row"><span class="label"><?= $contactField['email'] ?>:</span><?= $user['email'] ?></div>
|
||||
<div class="row"><span class="label"><?= $contactField['name'] ?>:</span><?= $user['email'] ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($user['phone'])): ?>
|
||||
<div class="row"><span class="label"><?= $contactField['phone'] ?>:</span><?= $user['phone'] ?></div>
|
||||
<div class="row"><span class="label"><?= $contactField['name'] ?>:</span><?= $user['phone'] ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($user['whatsapp'])): ?>
|
||||
<div class="row"><span class="label">WhatsApp:</span><?= $user['whatsapp'] ?></div>
|
||||
|
||||
@@ -74,8 +74,8 @@
|
||||
<td class="price"><?= $field['currency'].' ' ?><?= number_format($car['price'], 2) ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label"><?= $field['trade'] ?></td>
|
||||
<td><?= $car['trade'] ?></td>
|
||||
<td class="label"><?= $field['source_place'] ?></td>
|
||||
<td><?= $car['source_place'] ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label"><?= $field['plate_time'] ?></td>
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class WordpressSiteValidate extends Validate
|
||||
class ApprovalFlowsValidate extends Validate
|
||||
{
|
||||
// 规则
|
||||
protected $rule = [
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class ApprovalInstancesValidate extends Validate
|
||||
{
|
||||
// 规则
|
||||
protected $rule = [
|
||||
|
||||
];
|
||||
|
||||
// 提示
|
||||
protected $message = [
|
||||
|
||||
];
|
||||
|
||||
// 场景
|
||||
protected $scene = [
|
||||
'store' => [
|
||||
|
||||
],
|
||||
|
||||
'update' => [
|
||||
|
||||
]
|
||||
];
|
||||
}
|
||||
+1
-1
@@ -4,7 +4,7 @@ namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class WordpressVisitorValidate extends Validate
|
||||
class ApprovalRecordsValidate extends Validate
|
||||
{
|
||||
// 规则
|
||||
protected $rule = [
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class CorpValidate extends Validate
|
||||
{
|
||||
protected $rule = [
|
||||
'id' => 'require|number',
|
||||
'name' => 'require|max:255',
|
||||
'abbr' => 'require|in:hesu,xinyouhe,paika,simate,mbaf',
|
||||
'tenant_key' => 'string',
|
||||
'sort' => 'number',
|
||||
'status' => 'require|in:0,1'
|
||||
];
|
||||
|
||||
protected $message = [
|
||||
'id.require' => 'ID不能为空',
|
||||
'id.number' => 'ID必须是数字',
|
||||
'name.require' => '企业名称不能为空',
|
||||
'name.max' => '企业名称长度不能超过255个字符',
|
||||
'abbr.require' => '企业缩写不能为空',
|
||||
'abbr.in' => '企业缩写必须是 hesu,xinyouhe,paika,simate,mbaf 中的一个',
|
||||
'tenant_key.string' => '飞书企业标识必须是字符串',
|
||||
'sort.number' => '排序值必须是数字',
|
||||
'status.require' => '状态不能为空',
|
||||
'status.in' => '状态必须是0或1'
|
||||
];
|
||||
|
||||
protected $scene = [
|
||||
'store' => ['name', 'abbr', 'tenant_key', 'sort', 'status'],
|
||||
'update' => ['id', 'name', 'abbr', 'tenant_key', 'sort', 'status']
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class CountryValidate extends Validate
|
||||
{
|
||||
// 规则
|
||||
protected $rule = [
|
||||
|
||||
];
|
||||
|
||||
// 提示
|
||||
protected $message = [
|
||||
|
||||
];
|
||||
|
||||
// 场景
|
||||
protected $scene = [
|
||||
'store' => [
|
||||
|
||||
],
|
||||
|
||||
'update' => [
|
||||
|
||||
]
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class FinPaymentApplyValidate extends Validate
|
||||
{
|
||||
// 规则
|
||||
protected $rule = [
|
||||
|
||||
];
|
||||
|
||||
// 提示
|
||||
protected $message = [
|
||||
|
||||
];
|
||||
|
||||
// 场景
|
||||
protected $scene = [
|
||||
'store' => [
|
||||
|
||||
],
|
||||
|
||||
'update' => [
|
||||
|
||||
]
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class FinReceiptHisValidate extends Validate
|
||||
{
|
||||
// 规则
|
||||
protected $rule = [
|
||||
|
||||
];
|
||||
|
||||
// 提示
|
||||
protected $message = [
|
||||
|
||||
];
|
||||
|
||||
// 场景
|
||||
protected $scene = [
|
||||
'store' => [
|
||||
|
||||
],
|
||||
|
||||
'update' => [
|
||||
|
||||
]
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class FinReceiptValidate extends Validate
|
||||
{
|
||||
// 规则
|
||||
protected $rule = [
|
||||
// 'payment_apply_id' => 'integer|gt:0',
|
||||
// 'country_id' => 'require|integer|gt:0',
|
||||
// 'remitter' => 'require',
|
||||
// 'account_id' => 'require|integer|gt:0',
|
||||
// 'currency_id' => 'require|integer|gt:0',
|
||||
// 'amount' => 'require|gt:0',
|
||||
// 'exchange_rate' => 'require|gt:0',
|
||||
// 'arrived_at' => 'require',
|
||||
// 'is_domestic_return' => 'require|integer',
|
||||
];
|
||||
|
||||
// 提示
|
||||
protected $message = [
|
||||
// 'payment_apply_id.integer' => '付款申请ID必须是整数',
|
||||
// 'payment_apply_id.gt' => '付款申请ID必须大于0',
|
||||
// 'country_id.require' => '付款来源国家ID不能为空',
|
||||
// 'country_id.integer' => '付款来源国家ID必须是整数',
|
||||
// 'country_id.gt' => '付款来源国家ID必须大于0',
|
||||
// 'remitter.require' => '汇款人不能为空',
|
||||
// 'account_id.require' => '收款账号ID不能为空',
|
||||
// 'account_id.integer' => '收款账号ID必须是整数',
|
||||
// 'account_id.gt' => '收款账号ID必须大于0',
|
||||
// 'currency_id.require' => '收款币种ID不能为空',
|
||||
// 'currency_id.integer' => '收款币种ID必须是整数',
|
||||
// 'currency_id.gt' => '收款币种ID必须大于0',
|
||||
// 'amount.require' => '收款金额不能为空',
|
||||
// 'amount.gt' => '收款金额必须大于0',
|
||||
// 'exchange_rate.require' => '实际汇率不能为空',
|
||||
// 'exchange_rate.gt' => '实际汇率必须大于0',
|
||||
// 'arrived_at.require' => '到账时间不能为空',
|
||||
// 'arrived_at.gt' => '到账时间必须大于0',
|
||||
// 'is_domestic_return.require' => '是否回转内地不能为空',
|
||||
// 'is_domestic_return.integer' => '是否回转内地必须是整数',
|
||||
];
|
||||
|
||||
// 场景
|
||||
protected $scene = [
|
||||
'store' => [
|
||||
// 'payment_apply_id', 'country_id', 'remitter', 'account_id', 'currency_id', 'amount', 'exchange_rate', 'arrived_at'
|
||||
],
|
||||
|
||||
'update' => [
|
||||
// 'payment_apply_id'
|
||||
]
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class FinanceReconDubaiFilesValidate extends Validate
|
||||
{
|
||||
// 规则
|
||||
protected $rule = [
|
||||
|
||||
];
|
||||
|
||||
// 提示
|
||||
protected $message = [
|
||||
|
||||
];
|
||||
|
||||
// 场景
|
||||
protected $scene = [
|
||||
'store' => [
|
||||
|
||||
],
|
||||
|
||||
'update' => [
|
||||
|
||||
]
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
use app\common\model\InquiryLog;
|
||||
|
||||
class InquiryLogValidate extends Validate
|
||||
{
|
||||
// 规则
|
||||
protected $rule = [
|
||||
'id' => 'require|integer|gt:0',
|
||||
'order_no' => 'require|alphaDash|length:36',
|
||||
'enter_user_id'=> 'integer|egt:0',
|
||||
'recipient' => 'integer|egt:0',
|
||||
'quality' => 'require|in:1,2,3',
|
||||
'channel' => 'integer|between:0,99',
|
||||
'email' => 'email|max:255|checkEmailContactUnique',
|
||||
'tel' => 'max:255|checkTelContactUnique',
|
||||
'whatsapp' => 'max:255|checkWhatsappContactUnique',
|
||||
'country' => 'integer|egt:0|requireWith:contacts',
|
||||
'contacts' => 'require|max:255|requireWithout:email,tel,whatsapp|checkContactUnique',
|
||||
'content' => 'string',
|
||||
'status' => 'in:0,1,2',
|
||||
'r_time' => 'integer|egt:0',
|
||||
't_time' => 'integer|egt:0'
|
||||
];
|
||||
|
||||
// 提示
|
||||
protected $message = [
|
||||
'id.require' => 'ID不能为空',
|
||||
'id.integer' => 'ID必须是整数',
|
||||
'id.gt' => 'ID必须大于0',
|
||||
'order_no.require' => '订单号不能为空',
|
||||
'order_no.alphaDash'=> '订单号只能包含字母、数字和下划线',
|
||||
'order_no.length' => '订单号长度必须为36位',
|
||||
'enter_user_id.integer' => '录入人ID必须是整数',
|
||||
'enter_user_id.egt' => '录入人ID不能为负数',
|
||||
'recipient.integer' => '接单人ID必须是整数',
|
||||
'recipient.egt' => '接单人ID不能为负数',
|
||||
'quality.require' => '询单等级不能为空',
|
||||
'quality.in' => '询单等级只能是1、2或3',
|
||||
'channel.integer' => '渠道编号必须是整数',
|
||||
'channel.between' => '渠道编号范围0-99',
|
||||
'email.email' => '邮箱格式不正确',
|
||||
'email.max' => '邮箱长度不能超过255个字符',
|
||||
'tel.max' => '电话长度不能超过255个字符',
|
||||
'whatsapp.max' => 'WhatsApp长度不能超过255个字符',
|
||||
'country.integer' => '国家代码必须是整数',
|
||||
'country.egt' => '国家代码不能为负数',
|
||||
'contacts.require' => '联系人不能为空',
|
||||
'contacts.max' => '联系人长度不能超过255个字符',
|
||||
'content.string' => '询单内容必须是字符串',
|
||||
'status.in' => '状态值只能是0、1或2',
|
||||
'r_time.integer' => '发布时间必须是整数',
|
||||
'r_time.egt' => '发布时间不能为负数',
|
||||
't_time.integer' => '跟踪时间必须是整数',
|
||||
't_time.egt' => '跟踪时间不能为负数',
|
||||
'email.checkEmailContactUnique' => '邮箱联系方式已存在',
|
||||
'tel.checkTelContactUnique' => '电话联系方式已存在',
|
||||
'whatsapp.checkWhatsappContactUnique' => 'WhatsApp联系方式已存在',
|
||||
'contacts.requireWithout' => '至少需要提供一种联系方式',
|
||||
'contacts.checkContactUnique' => '联系人已存在',
|
||||
'country.requireWith' => '国家必须与联系人一起提供'
|
||||
];
|
||||
|
||||
// 场景
|
||||
protected $scene = [
|
||||
'store' => [
|
||||
'enter_user_id', 'quality', 'channel', 'email', 'tel', 'whatsapp', 'country', 'contacts', 'content', 'recipient'
|
||||
],
|
||||
'update' => [
|
||||
'quality', 'channel', 'email', 'tel', 'whatsapp', 'country', 'contacts', 'content', 'recipient'
|
||||
],
|
||||
'release' => [
|
||||
'status', 'r_time'
|
||||
],
|
||||
'allocation' => [
|
||||
'status', 't_time'
|
||||
]
|
||||
];
|
||||
|
||||
|
||||
/**
|
||||
* 验证联系方式唯一性
|
||||
*/
|
||||
protected function checkContactUnique($value, $rule, $data, $field, $title)
|
||||
{
|
||||
// 如果是更新操作且联系人未修改,跳过检查
|
||||
if ($this->currentScene == 'update') {
|
||||
$original = InquiryLog::find($data['id']);
|
||||
if ($original && $original->contacts == $value && $original->country == ($data['country'] ?? 0)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($data['email']) && empty($data['tel']) && empty($data['whatsapp'])) {
|
||||
$query = InquiryLog::where('contacts', trim($value))
|
||||
->where('country', $data['country']);
|
||||
|
||||
if (!empty($data['id'])) {
|
||||
$query->where('id', '<>', $data['id']);
|
||||
}
|
||||
|
||||
return !$query->exists();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证邮箱联系方式唯一性
|
||||
*/
|
||||
protected function checkEmailContactUnique($value, $rule, $data, $field, $title)
|
||||
{
|
||||
if (empty($value)) return true;
|
||||
|
||||
// 如果是更新操作且邮箱未修改,跳过检查
|
||||
if ($this->currentScene == 'update' && !empty($data['id'])) {
|
||||
$original = InquiryLog::find($data['id']);
|
||||
if ($original && $original->email == $value) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
$query = InquiryLog::where('email', trim($value));
|
||||
if (!empty($data['id'])) {
|
||||
$query->where('id', '<>', $data['id']);
|
||||
}
|
||||
return !$query->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证电话联系方式唯一性
|
||||
*/
|
||||
protected function checkTelContactUnique($value, $rule, $data, $field, $title)
|
||||
{
|
||||
if (empty($value)) return true;
|
||||
|
||||
// 如果是更新操作且电话未修改,跳过检查
|
||||
if ($this->currentScene == 'update' && !empty($data['id'])) {
|
||||
$original = InquiryLog::find($data['id']);
|
||||
if ($original && $original->tel == $value) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
$query = InquiryLog::where('tel', trim($value));
|
||||
if (!empty($data['id'])) {
|
||||
$query->where('id', '<>', $data['id']);
|
||||
}
|
||||
return !$query->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证WhatsApp联系方式唯一性
|
||||
*/
|
||||
protected function checkWhatsappContactUnique($value, $rule, $data, $field, $title)
|
||||
{
|
||||
if (empty($value)) return true;
|
||||
|
||||
// 如果是更新操作且WhatsApp未修改,跳过检查
|
||||
if ($this->currentScene == 'update' && !empty($data['id'])) {
|
||||
$original = InquiryLog::find($data['id']);
|
||||
if ($original && $original->whatsapp == $value) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
$query = InquiryLog::where('whatsapp', trim($value));
|
||||
if (!empty($data['id'])) {
|
||||
$query->where('id', '<>', $data['id']);
|
||||
}
|
||||
return !$query->exists();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class InquiryTransmitConfigValidate extends Validate
|
||||
{
|
||||
// 规则
|
||||
protected $rule = [
|
||||
|
||||
];
|
||||
|
||||
// 提示
|
||||
protected $message = [
|
||||
|
||||
];
|
||||
|
||||
// 场景
|
||||
protected $scene = [
|
||||
'store' => [
|
||||
|
||||
],
|
||||
|
||||
'update' => [
|
||||
|
||||
]
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class InquiryTransmitValidate extends Validate
|
||||
{
|
||||
// 规则
|
||||
protected $rule = [
|
||||
|
||||
];
|
||||
|
||||
// 提示
|
||||
protected $message = [
|
||||
|
||||
];
|
||||
|
||||
// 场景
|
||||
protected $scene = [
|
||||
'store' => [
|
||||
|
||||
],
|
||||
|
||||
'update' => [
|
||||
|
||||
]
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class InvItemValidate extends Validate
|
||||
{
|
||||
// 规则
|
||||
protected $rule = [
|
||||
|
||||
];
|
||||
|
||||
// 提示
|
||||
protected $message = [
|
||||
|
||||
];
|
||||
|
||||
// 场景
|
||||
protected $scene = [
|
||||
'store' => [
|
||||
|
||||
],
|
||||
|
||||
'update' => [
|
||||
|
||||
]
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class InvRequisitionApproveValidate extends Validate
|
||||
{
|
||||
// 规则
|
||||
protected $rule = [
|
||||
|
||||
];
|
||||
|
||||
// 提示
|
||||
protected $message = [
|
||||
|
||||
];
|
||||
|
||||
// 场景
|
||||
protected $scene = [
|
||||
'store' => [
|
||||
|
||||
],
|
||||
|
||||
'update' => [
|
||||
|
||||
]
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class InvRequisitionAttachmentValidate extends Validate
|
||||
{
|
||||
// 规则
|
||||
protected $rule = [
|
||||
|
||||
];
|
||||
|
||||
// 提示
|
||||
protected $message = [
|
||||
|
||||
];
|
||||
|
||||
// 场景
|
||||
protected $scene = [
|
||||
'store' => [
|
||||
|
||||
],
|
||||
|
||||
'update' => [
|
||||
|
||||
]
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class InvRequisitionItemReturnAttachmentValidate extends Validate
|
||||
{
|
||||
// 规则
|
||||
protected $rule = [
|
||||
|
||||
];
|
||||
|
||||
// 提示
|
||||
protected $message = [
|
||||
|
||||
];
|
||||
|
||||
// 场景
|
||||
protected $scene = [
|
||||
'store' => [
|
||||
|
||||
],
|
||||
|
||||
'update' => [
|
||||
|
||||
]
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class InvRequisitionItemReturnValidate extends Validate
|
||||
{
|
||||
// 规则
|
||||
protected $rule = [
|
||||
|
||||
];
|
||||
|
||||
// 提示
|
||||
protected $message = [
|
||||
|
||||
];
|
||||
|
||||
// 场景
|
||||
protected $scene = [
|
||||
'store' => [
|
||||
|
||||
],
|
||||
|
||||
'update' => [
|
||||
|
||||
]
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class InvRequisitionItemValidate extends Validate
|
||||
{
|
||||
// 规则
|
||||
protected $rule = [
|
||||
|
||||
];
|
||||
|
||||
// 提示
|
||||
protected $message = [
|
||||
|
||||
];
|
||||
|
||||
// 场景
|
||||
protected $scene = [
|
||||
'store' => [
|
||||
|
||||
],
|
||||
|
||||
'update' => [
|
||||
|
||||
]
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class InvRequisitionValidate extends Validate
|
||||
{
|
||||
// 规则
|
||||
protected $rule = [
|
||||
'applicant_id' => 'number',
|
||||
'borrow_reason' => 'require|max:255',
|
||||
'use_start_date' => 'require|date',
|
||||
'use_end_date' => 'date',
|
||||
'items' => 'require|array',
|
||||
'items.*.item_id' => 'require|number',
|
||||
'items.*.request_qty' => 'require|number|gt:0',
|
||||
];
|
||||
|
||||
// 提示
|
||||
protected $message = [
|
||||
'applicant_id.number' => '申请人格式错误',
|
||||
'borrow_reason.require' => '领用事由不能为空',
|
||||
'borrow_reason.max' => '领用事由最多不能超过255个字符',
|
||||
'use_start_date.require' => '使用开始日期不能为空',
|
||||
'use_start_date.date' => '使用开始日期格式错误',
|
||||
'use_end_date.date' => '预计归还日期格式错误',
|
||||
'items.require' => '申请物品明细不能为空',
|
||||
'items.array' => '申请物品明细格式错误',
|
||||
'items.*.item_id.require' => '物品ID不能为空',
|
||||
'items.*.item_id.number' => '物品ID格式错误',
|
||||
'items.*.request_qty.require' => '申请数量不能为空',
|
||||
'items.*.request_qty.number' => '申请数量格式错误',
|
||||
'items.*.request_qty.gt' => '申请数量必须大于0',
|
||||
];
|
||||
|
||||
// 场景
|
||||
protected $scene = [
|
||||
'store' => [
|
||||
|
||||
],
|
||||
|
||||
'update' => [
|
||||
|
||||
],
|
||||
|
||||
'apply' => [
|
||||
'applicant_id',
|
||||
'borrow_reason',
|
||||
'use_start_date',
|
||||
'use_end_date',
|
||||
'items'
|
||||
]
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class MessageNotificationsValidate extends Validate
|
||||
{
|
||||
// 规则
|
||||
protected $rule = [
|
||||
|
||||
];
|
||||
|
||||
// 提示
|
||||
protected $message = [
|
||||
|
||||
];
|
||||
|
||||
// 场景
|
||||
protected $scene = [
|
||||
'store' => [
|
||||
|
||||
],
|
||||
|
||||
'update' => [
|
||||
|
||||
]
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class MessageUserStatusValidate extends Validate
|
||||
{
|
||||
// 规则
|
||||
protected $rule = [
|
||||
|
||||
];
|
||||
|
||||
// 提示
|
||||
protected $message = [
|
||||
|
||||
];
|
||||
|
||||
// 场景
|
||||
protected $scene = [
|
||||
'store' => [
|
||||
|
||||
],
|
||||
|
||||
'update' => [
|
||||
|
||||
]
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class PurchaseOrderItemsValidate extends Validate
|
||||
{
|
||||
// 规则
|
||||
protected $rule = [
|
||||
|
||||
];
|
||||
|
||||
// 提示
|
||||
protected $message = [
|
||||
|
||||
];
|
||||
|
||||
// 场景
|
||||
protected $scene = [
|
||||
'store' => [
|
||||
|
||||
],
|
||||
|
||||
'update' => [
|
||||
|
||||
]
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class PurchaseOrdersValidate extends Validate
|
||||
{
|
||||
// 规则
|
||||
protected $rule = [
|
||||
|
||||
];
|
||||
|
||||
// 提示
|
||||
protected $message = [
|
||||
|
||||
];
|
||||
|
||||
// 场景
|
||||
protected $scene = [
|
||||
'store' => [
|
||||
|
||||
],
|
||||
|
||||
'update' => [
|
||||
|
||||
]
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class StockOrderAccountsValidate extends Validate
|
||||
{
|
||||
// 规则
|
||||
protected $rule = [
|
||||
|
||||
];
|
||||
|
||||
// 提示
|
||||
protected $message = [
|
||||
|
||||
];
|
||||
|
||||
// 场景
|
||||
protected $scene = [
|
||||
'store' => [
|
||||
|
||||
],
|
||||
|
||||
'update' => [
|
||||
|
||||
]
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class StockOrderItemOptionsValidate extends Validate
|
||||
{
|
||||
// 规则
|
||||
protected $rule = [
|
||||
|
||||
];
|
||||
|
||||
// 提示
|
||||
protected $message = [
|
||||
|
||||
];
|
||||
|
||||
// 场景
|
||||
protected $scene = [
|
||||
'store' => [
|
||||
|
||||
],
|
||||
|
||||
'update' => [
|
||||
|
||||
]
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class StockOrderItemsValidate extends Validate
|
||||
{
|
||||
// 规则
|
||||
protected $rule = [
|
||||
|
||||
];
|
||||
|
||||
// 提示
|
||||
protected $message = [
|
||||
|
||||
];
|
||||
|
||||
// 场景
|
||||
protected $scene = [
|
||||
'store' => [
|
||||
|
||||
],
|
||||
|
||||
'update' => [
|
||||
|
||||
]
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class StockOrdersFilesValidate extends Validate
|
||||
{
|
||||
// 规则
|
||||
protected $rule = [
|
||||
|
||||
];
|
||||
|
||||
// 提示
|
||||
protected $message = [
|
||||
|
||||
];
|
||||
|
||||
// 场景
|
||||
protected $scene = [
|
||||
'store' => [
|
||||
|
||||
],
|
||||
|
||||
'update' => [
|
||||
|
||||
]
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class StockOrdersValidate extends Validate
|
||||
{
|
||||
/**
|
||||
* 验证规则
|
||||
*/
|
||||
protected $rule = [
|
||||
'id' => 'number|egt:0',
|
||||
'order_no' => 'require|max:40|unique:stock_orders',
|
||||
'pi_no' => 'require|max:64',
|
||||
'biz_date' => 'require',
|
||||
'salesperson_id' => 'number|egt:0',
|
||||
'salesperson_snapshot' => 'max:64',
|
||||
'clearance_country_id' => 'require|number|egt:0',
|
||||
'clearance_country_snapshot' => 'require|max:64',
|
||||
'customer_source_id' => 'require|number|egt:0',
|
||||
'customer_source_snapshot' => 'require|max:64',
|
||||
'customer_id' => 'require|number|egt:0',
|
||||
'customer_snapshot' => 'require|max:128',
|
||||
'customer_country_id' => 'require|number|egt:0',
|
||||
'customer_country_snapshot' => 'require|max:64',
|
||||
'declarant_name' => 'require|max:64',
|
||||
'eta_ship_date' => 'number|egt:0',
|
||||
'shipping_method_id' => 'require|number',
|
||||
'shipping_method_snapshot' => 'require|max:32',
|
||||
'cif_total_amount' => 'require|float|egt:0',
|
||||
'domestic_port_id' => 'require|number|egt:0',
|
||||
'domestic_port_snapshot' => 'require|max:64',
|
||||
'trade_term_id' => 'require|number|egt:0',
|
||||
'trade_term_snapshot' => 'require|max:64',
|
||||
'clearance_port_id' => 'require|number|egt:0',
|
||||
'clearance_port_snapshot' => 'require|max:64',
|
||||
'latest_arrival_date' => 'number|egt:0',
|
||||
'contract_exchange_rate' => 'require|float|egt:0',
|
||||
'settlement_currency' => 'require|length:3|alpha',
|
||||
'remark' => 'max:255',
|
||||
'contract' => 'max:255',
|
||||
'is_use' => 'in:0,1',
|
||||
'object_id' => 'number|egt:0',
|
||||
'is_final' => 'in:0,1',
|
||||
'approval_status' => 'in:0,1,2,3',
|
||||
'status' => 'in:0,1',
|
||||
'file_url' => 'max:255',
|
||||
'pick_up_date' => 'number|egt:0',
|
||||
'collect_all_date' => 'number|egt:0',
|
||||
'created_at' => 'number|egt:0',
|
||||
'updated_at' => 'number|egt:0',
|
||||
'deleted_at' => 'number|egt:0',
|
||||
];
|
||||
|
||||
/**
|
||||
* 错误提示信息
|
||||
*/
|
||||
protected $message = [
|
||||
'order_no.require' => '备货单编号不能为空',
|
||||
'order_no.max' => '备货单编号长度不能超过40个字符',
|
||||
'order_no.unique' => '备货单编号已存在',
|
||||
'pi_no.require' => 'PI号不能为空',
|
||||
'pi_no.max' => 'PI号长度不能超过64个字符',
|
||||
'biz_date.require' => '单据日期不能为空',
|
||||
'clearance_country_id.require' => '清关目的国ID不能为空',
|
||||
'clearance_country_snapshot.require' => '清关目的国名称不能为空',
|
||||
'customer_source_id.require' => '客户来源ID不能为空',
|
||||
'customer_source_snapshot.require' => '客户来源名称不能为空',
|
||||
'customer_id.require' => '客户ID不能为空',
|
||||
'customer_snapshot.require' => '客户名称不能为空',
|
||||
'customer_country_id.require' => '客户所在国ID不能为空',
|
||||
'customer_country_snapshot.require' => '客户所在国名称不能为空',
|
||||
'declarant_name.require' => '清关人不能为空',
|
||||
'shipping_method_id.require' => '出运方式ID不能为空',
|
||||
'shipping_method_snapshot.require' => '出运方式不能为空',
|
||||
'cif_total_amount.require' => '合计到岸价不能为空',
|
||||
'cif_total_amount.float' => '合计到岸价必须为数字',
|
||||
'cif_total_amount.egt' => '合计到岸价不能为负数',
|
||||
'domestic_port_id.require' => '国内收货港ID不能为空',
|
||||
'domestic_port_snapshot.require' => '国内收货港名称不能为空',
|
||||
'trade_term_id.require' => '贸易条款ID不能为空',
|
||||
'trade_term_snapshot.require' => '贸易条款名称不能为空',
|
||||
'clearance_port_id.require' => '清关目的港ID不能为空',
|
||||
'clearance_port_snapshot.require' => '清关目的港名称不能为空',
|
||||
'contract_exchange_rate.require' => '合同汇率不能为空',
|
||||
'contract_exchange_rate.float' => '合同汇率必须为数字',
|
||||
'contract_exchange_rate.egt' => '合同汇率不能为负数',
|
||||
'settlement_currency.require' => '结算币种不能为空',
|
||||
'settlement_currency.length' => '结算币种必须为3个字符',
|
||||
'settlement_currency.alpha' => '结算币种必须为字母',
|
||||
'approval_status.in' => '审批状态值不正确',
|
||||
'status.in' => '状态值不正确',
|
||||
'is_final.in' => '是否终版值不正确',
|
||||
'is_use.in' => '是否开过终版值不正确',
|
||||
];
|
||||
|
||||
/**
|
||||
* 验证场景
|
||||
*/
|
||||
protected $scene = [
|
||||
// 创建场景
|
||||
'store' => [
|
||||
'pi_no', 'biz_date',
|
||||
'clearance_country_id', 'clearance_country_snapshot', 'customer_source_id',
|
||||
'customer_source_snapshot', 'customer_id', 'customer_snapshot',
|
||||
'customer_country_id', 'customer_country_snapshot', 'declarant_name',
|
||||
'shipping_method_id', 'shipping_method_snapshot',
|
||||
'domestic_port_id', 'domestic_port_snapshot',
|
||||
'trade_term_id', 'trade_term_snapshot', 'clearance_port_id',
|
||||
'clearance_port_snapshot', 'contract_exchange_rate',
|
||||
'settlement_currency'
|
||||
],
|
||||
|
||||
// 更新场景
|
||||
'update' => [
|
||||
'pi_no', 'biz_date',
|
||||
'clearance_country_id', 'clearance_country_snapshot', 'customer_source_id',
|
||||
'customer_source_snapshot', 'customer_id', 'customer_snapshot',
|
||||
'customer_country_id', 'customer_country_snapshot', 'declarant_name',
|
||||
'shipping_method_id', 'shipping_method_snapshot',
|
||||
'domestic_port_id', 'domestic_port_snapshot',
|
||||
'trade_term_id', 'trade_term_snapshot', 'clearance_port_id',
|
||||
'clearance_port_snapshot', 'contract_exchange_rate',
|
||||
'settlement_currency'
|
||||
],
|
||||
|
||||
// 提交审批场景
|
||||
'submit' => [
|
||||
'id' => 'require|number|egt:1',
|
||||
'status' => 'require|in:1'
|
||||
],
|
||||
|
||||
// 审批场景
|
||||
'approve' => [
|
||||
'id' => 'require|number|egt:1',
|
||||
'approval_status' => 'require|in:1,2,3'
|
||||
],
|
||||
|
||||
// 状态更新场景
|
||||
'status' => [
|
||||
'id' => 'require|number|egt:1',
|
||||
'status' => 'require|in:0,1'
|
||||
],
|
||||
|
||||
// 简略验证场景(用于快速验证)
|
||||
'simple' => [
|
||||
'order_no', 'biz_date', 'cif_total_amount', 'settlement_currency'
|
||||
]
|
||||
];
|
||||
|
||||
/**
|
||||
* 自定义验证规则 - 验证时间戳是否合理
|
||||
*/
|
||||
protected function checkTimestamp($value, $rule, $data = [])
|
||||
{
|
||||
if ($value === 0 || $value === '0') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 时间戳应该在1970年到现在+1年的范围内
|
||||
$minTimestamp = 0;
|
||||
$maxTimestamp = time() + (365 * 24 * 60 * 60);
|
||||
|
||||
return $value >= $minTimestamp && $value <= $maxTimestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义验证规则 - 验证金额格式
|
||||
*/
|
||||
protected function checkMoney($value, $rule, $data = [])
|
||||
{
|
||||
if (!is_numeric($value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 验证小数点位数不超过2位
|
||||
if (strpos($value, '.') !== false) {
|
||||
$decimalPart = explode('.', $value)[1];
|
||||
if (strlen($decimalPart) > 2) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 场景验证前回调 - 添加自定义规则
|
||||
*/
|
||||
// protected function sceneBefore($scene)
|
||||
// {
|
||||
// // 为时间戳字段添加自定义验证
|
||||
// $timestampFields = ['biz_date', 'eta_ship_date', 'latest_arrival_date', 'pick_up_date', 'collect_all_date', 'created_at', 'updated_at', 'deleted_at'];
|
||||
// foreach ($timestampFields as $field) {
|
||||
// if (isset($this->scene[$scene][$field])) {
|
||||
// $this->rule[$field] .= '|checkTimestamp';
|
||||
// }
|
||||
// }
|
||||
|
||||
// // 为金额字段添加自定义验证
|
||||
// $moneyFields = ['cif_total_amount', 'contract_exchange_rate'];
|
||||
// foreach ($moneyFields as $field) {
|
||||
// if (isset($this->scene[$scene][$field])) {
|
||||
// $this->rule[$field] .= '|checkMoney';
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class SysOpLogValidate extends Validate
|
||||
{
|
||||
// 规则
|
||||
protected $rule = [
|
||||
|
||||
];
|
||||
|
||||
// 提示
|
||||
protected $message = [
|
||||
|
||||
];
|
||||
|
||||
// 场景
|
||||
protected $scene = [
|
||||
'store' => [
|
||||
|
||||
],
|
||||
|
||||
'update' => [
|
||||
|
||||
]
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class SysUpdateLogValidate extends Validate
|
||||
{
|
||||
// 规则
|
||||
protected $rule = [
|
||||
|
||||
];
|
||||
|
||||
// 提示
|
||||
protected $message = [
|
||||
|
||||
];
|
||||
|
||||
// 场景
|
||||
protected $scene = [
|
||||
'store' => [
|
||||
|
||||
],
|
||||
|
||||
'update' => [
|
||||
|
||||
]
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class SystemsValidate extends Validate
|
||||
{
|
||||
// 规则
|
||||
protected $rule = [
|
||||
|
||||
];
|
||||
|
||||
// 提示
|
||||
protected $message = [
|
||||
|
||||
];
|
||||
|
||||
// 场景
|
||||
protected $scene = [
|
||||
'store' => [
|
||||
|
||||
],
|
||||
|
||||
'update' => [
|
||||
|
||||
]
|
||||
];
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class UsedCarDetailValidate extends Validate
|
||||
{
|
||||
// 规则
|
||||
protected $rule = [
|
||||
'id' => 'requireWith:id',
|
||||
'product_name' => 'require|max:255',
|
||||
'price' => 'require|float|>=:0',
|
||||
'user_id' => 'require|integer',
|
||||
'role' => 'integer|in:1,2,3,4',
|
||||
];
|
||||
|
||||
// 提示
|
||||
protected $message = [
|
||||
'product_name.require' => '产品名称不能为空',
|
||||
'product_name.max' => '产品名称最多255个字符',
|
||||
'price.require' => '车辆价格不能为空',
|
||||
'price.float' => '车辆价格格式不正确',
|
||||
'price.>=' => '车辆价格不能为负数',
|
||||
'role.in' => '角色值不正确',
|
||||
];
|
||||
|
||||
// 场景
|
||||
protected $scene = [
|
||||
'store' => ['product_name', 'price', 'user_id', 'role'],
|
||||
'update' => ['id', 'product_name', 'price'],
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class UserGroupMembersValidate extends Validate
|
||||
{
|
||||
// 规则
|
||||
protected $rule = [
|
||||
|
||||
];
|
||||
|
||||
// 提示
|
||||
protected $message = [
|
||||
|
||||
];
|
||||
|
||||
// 场景
|
||||
protected $scene = [
|
||||
'store' => [
|
||||
|
||||
],
|
||||
|
||||
'update' => [
|
||||
|
||||
]
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class UserGroupValidate extends Validate
|
||||
{
|
||||
// 规则
|
||||
protected $rule = [
|
||||
|
||||
];
|
||||
|
||||
// 提示
|
||||
protected $message = [
|
||||
|
||||
];
|
||||
|
||||
// 场景
|
||||
protected $scene = [
|
||||
'store' => [
|
||||
|
||||
],
|
||||
|
||||
'update' => [
|
||||
|
||||
]
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class VehDictI18nValidate extends Validate
|
||||
{
|
||||
// 规则
|
||||
protected $rule = [
|
||||
|
||||
];
|
||||
|
||||
// 提示
|
||||
protected $message = [
|
||||
|
||||
];
|
||||
|
||||
// 场景
|
||||
protected $scene = [
|
||||
'store' => [
|
||||
|
||||
],
|
||||
|
||||
'update' => [
|
||||
|
||||
]
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class VehDictMappingValidate extends Validate
|
||||
{
|
||||
// 规则
|
||||
protected $rule = [
|
||||
|
||||
];
|
||||
|
||||
// 提示
|
||||
protected $message = [
|
||||
|
||||
];
|
||||
|
||||
// 场景
|
||||
protected $scene = [
|
||||
'store' => [
|
||||
|
||||
],
|
||||
|
||||
'update' => [
|
||||
|
||||
]
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class VehMoneyValueValidate extends Validate
|
||||
{
|
||||
// 规则
|
||||
protected $rule = [
|
||||
|
||||
];
|
||||
|
||||
// 提示
|
||||
protected $message = [
|
||||
|
||||
];
|
||||
|
||||
// 场景
|
||||
protected $scene = [
|
||||
'store' => [
|
||||
|
||||
],
|
||||
|
||||
'update' => [
|
||||
|
||||
]
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class VehVehicleListingValidate extends Validate
|
||||
{
|
||||
// 规则
|
||||
protected $rule = [
|
||||
|
||||
];
|
||||
|
||||
// 提示
|
||||
protected $message = [
|
||||
|
||||
];
|
||||
|
||||
// 场景
|
||||
protected $scene = [
|
||||
'store' => [
|
||||
|
||||
],
|
||||
|
||||
'update' => [
|
||||
|
||||
]
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class VehVehicleSkuValidate extends Validate
|
||||
{
|
||||
/**
|
||||
* 规则定义
|
||||
*/
|
||||
protected $rule = [
|
||||
'sku_code' => 'max:40|alphaNum',
|
||||
'fingerprint' => 'length:64|alphaNum',
|
||||
'brand_code' => 'require|max:32',
|
||||
'brand_name' => 'require|max:64',
|
||||
'series_code' => 'max:64',
|
||||
'series_name' => 'max:128',
|
||||
'model_code' => 'max:64',
|
||||
'model_name' => 'require|max:128',
|
||||
'model_year' => 'require|number|between:1900,2100',
|
||||
'trim_code' => 'max:64',
|
||||
'trim_level_code' => 'max:64',
|
||||
'edition_code' => 'max:64',
|
||||
'trim' => 'max:128',
|
||||
'trim_level' => 'max:64',
|
||||
'edition_name' => 'max:64',
|
||||
'body_type_code' => 'max:32',
|
||||
'drive_type_code' => 'max:16',
|
||||
'motor_layout_code' => 'max:32',
|
||||
'fuel_type_code' => 'max:16',
|
||||
'engine_disp_l' => 'float|between:0,99.9',
|
||||
'engine_turbo' => 'in:0,1',
|
||||
'engine_power_kw' => 'number|between:0,2000',
|
||||
'battery_kwh' => 'float|between:0,999.99',
|
||||
'transmission_code' => 'max:16',
|
||||
'gears' => 'number|between:1,20',
|
||||
'range_km' => 'number|between:0,2000',
|
||||
'range_std_code' => 'max:16',
|
||||
'doors' => 'number|between:2,10',
|
||||
'seats' => 'number|between:1,20',
|
||||
'weight_kg' => 'number|between:0,10000',
|
||||
'dimensions_mm' => 'array',
|
||||
'color_exterior_code'=> 'max:32',
|
||||
'color_exterior_text'=> 'max:64',
|
||||
'color_interior_code'=> 'max:32',
|
||||
'color_interior_text'=> 'max:64',
|
||||
'options_official' => 'array',
|
||||
'features' => 'array',
|
||||
'emission_std_code' => 'max:16',
|
||||
'origin_country' => 'alpha|length:2',
|
||||
'currency' => 'require|alpha|length:3',
|
||||
'msrp' => 'float|between:0,9999999999.99',
|
||||
'is_active' => 'in:0,1',
|
||||
'corp_id' => 'require|number',
|
||||
'remark' => 'max:255',
|
||||
'created_by' => 'number',
|
||||
'updated_by' => 'number',
|
||||
'created_at' => 'require|number',
|
||||
'updated_at' => 'require|number',
|
||||
];
|
||||
|
||||
/**
|
||||
* 字段别名
|
||||
*/
|
||||
protected $field = [
|
||||
'sku_code' => 'SKU编码',
|
||||
'fingerprint' => '规格指纹',
|
||||
'brand_code' => '品牌代码',
|
||||
'brand_name' => '品牌名称',
|
||||
'series_code' => '车系代码',
|
||||
'series_name' => '车系名称',
|
||||
'model_code' => '车型代码',
|
||||
'model_name' => '车型名称',
|
||||
'model_year' => '年款',
|
||||
'trim_code' => '版型代码',
|
||||
'trim_level_code' => '配置等级代码',
|
||||
'edition_code' => '特别版代码',
|
||||
'trim' => '版型/细分配置',
|
||||
'trim_level' => '配置等级',
|
||||
'edition_name' => '特别版名称',
|
||||
'body_type_code' => '车身类型',
|
||||
'drive_type_code' => '驱动方式',
|
||||
'motor_layout_code' => '电机布置',
|
||||
'fuel_type_code' => '能源类型',
|
||||
'engine_disp_l' => '排量(L)',
|
||||
'engine_turbo' => '是否涡轮',
|
||||
'engine_power_kw' => '最大功率(kW)',
|
||||
'battery_kwh' => '电池容量(kWh)',
|
||||
'transmission_code' => '变速箱代码',
|
||||
'gears' => '挡位数',
|
||||
'range_km' => '续航里程',
|
||||
'range_std_code' => '续航标准',
|
||||
'doors' => '门数',
|
||||
'seats' => '座位数',
|
||||
'weight_kg' => '整备质量',
|
||||
'dimensions_mm' => '车身尺寸',
|
||||
'color_exterior_code'=> '外观色代码',
|
||||
'color_exterior_text'=> '外观色名称',
|
||||
'color_interior_code'=> '内饰色代码',
|
||||
'color_interior_text'=> '内饰色名称',
|
||||
'options_official' => '官方选装',
|
||||
'features' => '功能配置',
|
||||
'emission_std_code' => '排放标准',
|
||||
'origin_country' => '原产国',
|
||||
'currency' => '币种',
|
||||
'msrp' => '建议零售价',
|
||||
'is_active' => '是否启用',
|
||||
'corp_id' => '公司ID',
|
||||
'remark' => '备注',
|
||||
'created_by' => '创建人',
|
||||
'updated_by' => '更新人',
|
||||
'created_at' => '创建时间',
|
||||
'updated_at' => '更新时间',
|
||||
];
|
||||
|
||||
/**
|
||||
* 场景定义
|
||||
*/
|
||||
protected $scene = [
|
||||
'store' => [
|
||||
'brand_code', 'brand_name', 'model_year','currency'
|
||||
],
|
||||
'update' => [
|
||||
'id', 'brand_code','brand_name', 'model_year','currency'
|
||||
],
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class VehVehicleUnitValidate extends Validate
|
||||
{
|
||||
// 规则
|
||||
protected $rule = [
|
||||
|
||||
];
|
||||
|
||||
// 提示
|
||||
protected $message = [
|
||||
|
||||
];
|
||||
|
||||
// 场景
|
||||
protected $scene = [
|
||||
'store' => [
|
||||
|
||||
],
|
||||
|
||||
'update' => [
|
||||
|
||||
]
|
||||
];
|
||||
}
|
||||
@@ -159,7 +159,7 @@ class IndexController
|
||||
|
||||
public function test(Request $request)
|
||||
{
|
||||
$service = new \app\common\utils\crawl\service\service\UsedCarCrawl();
|
||||
$service = new \app\common\utils\crawl\service\UsedCarCrawl();
|
||||
|
||||
$result = $service->pushUrl('https://www.dongchedi.com/usedcar/21631642');
|
||||
|
||||
|
||||
@@ -3,9 +3,6 @@
|
||||
namespace app\process;
|
||||
|
||||
use app\common\model\UsedCar;
|
||||
use app\common\model\WordpressSite;
|
||||
use app\common\model\WordpressUsedCar;
|
||||
use app\common\service\WordpressSiteService;
|
||||
use support\Db;
|
||||
use support\Log;
|
||||
use Workerman\Crontab\Crontab;
|
||||
@@ -29,38 +26,6 @@ class Task
|
||||
Log::info('send follow end',[time()]);
|
||||
});
|
||||
|
||||
// 每三个小时自动归档
|
||||
new Crontab('0 25 */3 * * *', function(){
|
||||
|
||||
Db::table('used_car')
|
||||
->where('status', 1)
|
||||
->where('created_at', '<', time() - (86400*15))
|
||||
->update(['status' => 3]);
|
||||
|
||||
});
|
||||
|
||||
if (getenv('ENVIRONMENT') === 'master') {
|
||||
// 每三个小时自动归档
|
||||
new Crontab('0 0 3 * * *', function(){
|
||||
|
||||
$sites = WordpressSite::where('status', 1)
|
||||
->get();
|
||||
foreach ($sites as $site) {
|
||||
$used_car_id = WordpressUsedCar::where('wp_site_id', $site['id'])
|
||||
->where('status', 1)
|
||||
->max('used_car_id');
|
||||
$used_cars = Db::table('used_car')
|
||||
->where('id', '>', $used_car_id)
|
||||
->get();
|
||||
foreach ($used_cars as $car) {
|
||||
WordpressSiteService::createProduct($site['site_url'], $car['id']);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace app\queue\redis;
|
||||
|
||||
use support\exception\BusinessException;
|
||||
use Webman\RedisQueue\Consumer;
|
||||
|
||||
class ApprovalCallback implements Consumer
|
||||
{
|
||||
// 要消费的队列名
|
||||
public $queue = 'approval-callback';
|
||||
|
||||
// 连接名,对应 plugin/webman/redis-queue/redis.php 里的连接`
|
||||
public $connection = 'default';
|
||||
|
||||
// 消费
|
||||
public function consume($data)
|
||||
{
|
||||
try {
|
||||
match ($data['flow_type']) {
|
||||
'stock_orders' => $this->approvalStockOrders($data),
|
||||
'purchase_orders' => $this->approvalPurchaseOrders($data),
|
||||
'stock_orders_nullify' => '',
|
||||
default => throw new BusinessException('未知的审批类型,无法执行回调操作')
|
||||
};
|
||||
return true;
|
||||
} catch (\Throwable $th) {
|
||||
flg('审批回调异常:'.$th->getFile()."|".$th->getLine()."|".$th->getMessage(), false);
|
||||
throw new BusinessException($th->getFile()."|".$th->getLine()."|".$th->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 消费失败回调
|
||||
/*
|
||||
$package = [
|
||||
'id' => 1357277951, // 消息ID
|
||||
'time' => 1709170510, // 消息时间
|
||||
'delay' => 0, // 延迟时间
|
||||
'attempts' => 2, // 消费次数
|
||||
'queue' => 'send-mail', // 队列名
|
||||
'data' => ['to' => 'tom@gmail.com', 'content' => 'hello'], // 消息内容
|
||||
'max_attempts' => 5, // 最大重试次数
|
||||
'error' => '错误信息' // 错误信息
|
||||
]
|
||||
*/
|
||||
public function onConsumeFailure(\Throwable $e, $package)
|
||||
{
|
||||
echo "consume failure\n";
|
||||
echo $e->getMessage() . "\n";
|
||||
// 无需反序列化
|
||||
var_export($package);
|
||||
}
|
||||
|
||||
/**
|
||||
* 备货单处理
|
||||
*/
|
||||
private function approvalStockOrders($data)
|
||||
{
|
||||
// 生成PDF文件
|
||||
$service = new \app\crm\service\StockOrdersService();
|
||||
$result = $service->createdPdfFile($data['biz_id']);
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 采购单处理
|
||||
*/
|
||||
private function approvalPurchaseOrders($data)
|
||||
{
|
||||
$udata['id'] = $data['biz_id'];
|
||||
if ($data['status'] === 'approve') {
|
||||
$udata['status'] = 2;
|
||||
}
|
||||
|
||||
if ($data['status'] === 'reject') {
|
||||
$udata['status'] = 3;
|
||||
}
|
||||
|
||||
$service = new \app\common\service\PurchaseOrdersService();
|
||||
$result = $service->updateStatus($udata);
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace app\queue\redis;
|
||||
|
||||
use Webman\RedisQueue\Consumer;
|
||||
use app\inquiry\service\InquiryLogService;
|
||||
use app\common\utils\email\MailSend;
|
||||
|
||||
class EmailMessage implements Consumer
|
||||
{
|
||||
// 要消费的队列名
|
||||
public $queue = 'email-message';
|
||||
|
||||
// 连接名,对应 plugin/webman/redis-queue/redis.php 里的连接`
|
||||
public $connection = 'default';
|
||||
|
||||
// 消费
|
||||
public function consume($data)
|
||||
{
|
||||
try {
|
||||
// 询单信息
|
||||
$inquiryLogService = new InquiryLogService();
|
||||
$content = $inquiryLogService->getContent($data['id']);
|
||||
|
||||
MailSend::send($data['email'], $data['title'], replaceTpl($content));
|
||||
} catch (\Throwable $th) {
|
||||
throw $th;
|
||||
}
|
||||
}
|
||||
|
||||
// 消费失败回调
|
||||
/*
|
||||
$package = [
|
||||
'id' => 1357277951, // 消息ID
|
||||
'time' => 1709170510, // 消息时间
|
||||
'delay' => 0, // 延迟时间
|
||||
'attempts' => 2, // 消费次数
|
||||
'queue' => 'send-mail', // 队列名
|
||||
'data' => ['to' => 'tom@gmail.com', 'content' => 'hello'], // 消息内容
|
||||
'max_attempts' => 5, // 最大重试次数
|
||||
'error' => '错误信息' // 错误信息
|
||||
]
|
||||
*/
|
||||
public function onConsumeFailure(\Throwable $e, $package)
|
||||
{
|
||||
echo "consume failure\n";
|
||||
echo $e->getMessage() . "\n";
|
||||
// 无需反序列化
|
||||
var_export($package);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
namespace app\queue\redis;
|
||||
|
||||
use app\common\model\CorpFeishuApp;
|
||||
use app\common\model\CrmCustomer;
|
||||
use Webman\RedisQueue\Consumer;
|
||||
use app\common\model\User;
|
||||
use app\common\utils\FeishuMessage as FeishuMsg;
|
||||
use support\exception\BusinessException;
|
||||
use app\inquiry\service\InquiryLogService;
|
||||
|
||||
class FeishuMessage implements Consumer
|
||||
{
|
||||
// 要消费的队列名
|
||||
public $queue = 'feishu-message';
|
||||
|
||||
// 连接名,对应 plugin/webman/redis-queue/redis.php 里的连接`
|
||||
public $connection = 'default';
|
||||
|
||||
// 消费
|
||||
|
||||
/**
|
||||
* @param string $category 分类:crm | inquiry
|
||||
* @param string $scene 场景:group-send | group-update | person-send | person-update
|
||||
* @param string $id 数据ID:询单ID | 客户ID
|
||||
* @param string $receive_type 接收者类型:group | person
|
||||
*/
|
||||
public function consume($data)
|
||||
{
|
||||
try {
|
||||
if ($data['category'] === 'crm') {
|
||||
// 客户线索
|
||||
$crmCustomer = new CrmCustomer();
|
||||
$info = $crmCustomer->where('id', $data['id'])->first();
|
||||
|
||||
// 跟单人
|
||||
$reInfo = User::where('id', $info->user_id)->first();
|
||||
|
||||
// 飞书应用
|
||||
$feishuApp = new CorpFeishuApp();
|
||||
$feishuAppInfo = $feishuApp->where('corp_id', $reInfo->corp_id)->where('system_id', 1)->first();
|
||||
|
||||
// 推送数据
|
||||
$options = [
|
||||
'contacts' => $info->contacts,
|
||||
'follow_last' => $info->follow_last ? date('Y-m-d H:i', $info->follow_last) : '',
|
||||
'follow_next' => $info->follow_next ? date('Y-m-d H:i', $info->follow_next) : '',
|
||||
|
||||
'app_id' => $feishuAppInfo->app_id,
|
||||
'app_secret' => $feishuAppInfo->app_secret,
|
||||
'state' => $feishuAppInfo->state,
|
||||
'msg_type' => 'interactive',
|
||||
'receive_id_type' => 'open_id',
|
||||
'receive_id' => $reInfo->open_id
|
||||
];
|
||||
}
|
||||
|
||||
if ($data['category'] === 'inquiry') {
|
||||
// 询单信息
|
||||
$inquiryLogService = new InquiryLogService();
|
||||
$info = $inquiryLogService->getInquiryLogBy($data['id']);
|
||||
|
||||
// 接受者处理
|
||||
if ($data['receive_type'] == 'group') {
|
||||
$appId = 'cli_a6183b882cb8500d';
|
||||
$appSecret = 'VqYib48u4EtDIa6nG9qH3f0fYCA1bCvs';
|
||||
$state = 'hesu';
|
||||
$receiveId = 'oc_a52d9f89ad76735279088c78beade8dc';
|
||||
$receiveIdType = 'chat_id';
|
||||
} else {
|
||||
// 跟单人
|
||||
$reInfo = User::where('id', $info->recipient)->first();
|
||||
// 飞书应用
|
||||
$feishuApp = new CorpFeishuApp();
|
||||
$feishuAppInfo = $feishuApp->where('corp_id', $reInfo->corp_id)->where('system_id', 1)->first();
|
||||
// 推送信息
|
||||
$appId = $feishuAppInfo->app_id;
|
||||
$appSecret = $feishuAppInfo->app_secret;
|
||||
$state = $feishuAppInfo->state;
|
||||
$receiveId = $reInfo->open_id;
|
||||
$receiveIdType = 'open_id';
|
||||
}
|
||||
|
||||
// 推送数据
|
||||
$options = [
|
||||
'order_no' => $info->order_no,
|
||||
'quality' => $inquiryLogService->qualityName($info->quality),
|
||||
'channel' => $info->channelInfo ? $info->channelInfo->name : '',
|
||||
'country' => $info->countryInfo ? $info->countryInfo->cn_addr : '',
|
||||
'enter_user' => $info->enter ? $info->enter->name : '',
|
||||
'r_time' => $info->r_time ? date('Y-m-d H:i:s', $info->r_time) : '',
|
||||
't_time' => $info->t_time ? date('Y-m-d H:i:s', $info->t_time) : '',
|
||||
|
||||
'app_id' => $appId,
|
||||
'app_secret' => $appSecret,
|
||||
'state' => $state,
|
||||
'msg_type' => 'interactive',
|
||||
'receive_id_type' => $receiveIdType,
|
||||
'receive_id' => $receiveId
|
||||
];
|
||||
}
|
||||
|
||||
$feishuMsg = new FeishuMsg($options);
|
||||
$feishuMsg->messageSend($data['scene'], $options);
|
||||
} catch (\Throwable $th) {
|
||||
throw $th;
|
||||
}
|
||||
}
|
||||
|
||||
// 消费失败回调
|
||||
/*
|
||||
$package = [
|
||||
'id' => 1357277951, // 消息ID
|
||||
'time' => 1709170510, // 消息时间
|
||||
'delay' => 0, // 延迟时间
|
||||
'attempts' => 2, // 消费次数
|
||||
'queue' => 'send-mail', // 队列名
|
||||
'data' => ['to' => 'tom@gmail.com', 'content' => 'hello'], // 消息内容
|
||||
'max_attempts' => 5, // 最大重试次数
|
||||
'error' => '错误信息' // 错误信息
|
||||
]
|
||||
*/
|
||||
public function onConsumeFailure(\Throwable $e, $package)
|
||||
{
|
||||
echo "consume failure\n";
|
||||
echo $e->getMessage() . "\n";
|
||||
// 无需反序列化
|
||||
var_export($package);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
<?php
|
||||
|
||||
namespace app\queue\redis;
|
||||
|
||||
use app\inquiry\service\InquiryLogService;
|
||||
use app\common\model\CrmCustomer;
|
||||
use app\common\model\InquiryLog;
|
||||
use Webman\RedisQueue\Consumer;
|
||||
use app\common\model\User;
|
||||
use app\common\utils\FeishuMessage;
|
||||
use support\exception\BusinessException;
|
||||
use support\Log;
|
||||
use support\Model;
|
||||
|
||||
class InquiryToCrmNew implements Consumer
|
||||
{
|
||||
// 要消费的队列名
|
||||
public $queue = 'inquiry-to-crm-new';
|
||||
|
||||
// 连接名,对应 plugin/webman/redis-queue/redis.php 里的连接`
|
||||
public $connection = 'default';
|
||||
|
||||
// 消费
|
||||
public function consume($data)
|
||||
{
|
||||
|
||||
/*
|
||||
询单 与 CRM 之间的同步场景描述
|
||||
|
||||
添加(询单)
|
||||
添加 不发 不分 (不同步)1
|
||||
添加 发了 不分 (要同步)2
|
||||
添加 不发 分了 (要同步)3
|
||||
添加 不发 直分 (要同步)4
|
||||
|
||||
更新(询单)
|
||||
没发 没分 更新 (不同步)5
|
||||
没发 分过 更新 (要同步)6
|
||||
发过 没分 更新 (要同步)7
|
||||
发过 分过 更新 (要同步)8
|
||||
*/
|
||||
|
||||
try {
|
||||
// 获取询盘信息
|
||||
$info = InquiryLog::with(['channelInfo', 'countryInfo', 'enter'])->where('id', $data['id'])->first();
|
||||
|
||||
// 无论添加 或 更新,询单必然要存在
|
||||
if (!$info) {
|
||||
throw new BusinessException('询盘不存在:' . $data['id']);
|
||||
}
|
||||
|
||||
if ($info->status == 0 && $info->recipient == 0) {
|
||||
// 如果询单未发、未分,则不与crm同步,不做任何处理
|
||||
return true;
|
||||
}
|
||||
|
||||
// 更改库信息
|
||||
$this->syncCustomer($info);
|
||||
} catch (\Throwable $th) {
|
||||
throw $th;
|
||||
}
|
||||
}
|
||||
|
||||
public function pushWs($title, $content, $recipient = [])
|
||||
{
|
||||
$wsdata = [
|
||||
'sender_id' => 0,
|
||||
'sender_type' => 'system',
|
||||
'title' => $title,
|
||||
'content' => $content,
|
||||
'scene' => 1,
|
||||
'recipient' => $recipient
|
||||
];
|
||||
// 异步推送
|
||||
\Webman\RedisQueue\Redis::send('notice', $wsdata);
|
||||
}
|
||||
|
||||
public function pushEmail($id, $title, $email)
|
||||
{
|
||||
$options = [
|
||||
'id' => $id,
|
||||
'email' => $email,
|
||||
'title' => $title,
|
||||
];
|
||||
\Webman\RedisQueue\Redis::send('email-message', $options);
|
||||
}
|
||||
|
||||
public function syncCustomer(Model $info)
|
||||
{
|
||||
$uInfo = $this->recipientInfo($info);
|
||||
$crmCustomerInfo = CrmCustomer::where('sn', $info->order_no)->first();
|
||||
if ($crmCustomerInfo) {
|
||||
|
||||
// 消息推送
|
||||
if ($info->status == 2 && empty($crmCustomerInfo->user_id)) {
|
||||
// $notice = '分配';
|
||||
$title = '[询单·分配] 新的客户线索【' . $info->order_no . '】,请及时处理';
|
||||
$content = '[询单·分配] 新的客户线索【' . $info->order_no . '】,请及时处理';
|
||||
$recipient = [$info->recipient];
|
||||
// $this->pushWs($title, $content, $recipient);
|
||||
$this->pushEmail($info->id, $title, $uInfo['uinfo']['email']);
|
||||
} else {
|
||||
if ($info->status == 2 && $crmCustomerInfo->user_id == $info->recipient) {
|
||||
// $notice = '更新';
|
||||
$title = '[询单·更新] 客户线索' . $info->order_no . '有新的调整,请及时处理';
|
||||
$content = '[询单·更新] 客户线索' . $info->order_no . '有新的调整,请及时处理';
|
||||
$recipient = [$info->recipient];
|
||||
// $this->pushWs($title, $content, $recipient);
|
||||
$this->pushEmail($info->id, $title, $uInfo['uinfo']['email']);
|
||||
} else {
|
||||
// $notice = '重新分配';
|
||||
$title = '[询单·分配] 新的客户线索【' . $info->order_no . '】,请及时处理';
|
||||
$content = '[询单·分配] 新的客户线索【' . $info->order_no . '】,请及时处理';
|
||||
$recipient = [$info->recipient];
|
||||
// $this->pushWs($title, $content, $recipient);
|
||||
$this->pushEmail($info->id, $title, $uInfo['uinfo']['email']);
|
||||
}
|
||||
}
|
||||
|
||||
$udata = [
|
||||
'corp_id' => $uInfo['corpId'],
|
||||
'user_id' => $info->recipient,
|
||||
'channel_id' => $info->channel,
|
||||
'country_id' => $info->country,
|
||||
'magnitude' => $info->quality,
|
||||
'email' => $info->email,
|
||||
'phone' => $info->tel,
|
||||
'WhatsApp' => $info->whatsapp,
|
||||
'contacts' => $info->contacts,
|
||||
'desc_collect' => $info->content,
|
||||
];
|
||||
if (!$crmCustomerInfo->update($udata)) {
|
||||
throw new BusinessException('同步失败,更新客户线索失败');
|
||||
}
|
||||
} else {
|
||||
$crmCustomer = new CrmCustomer();
|
||||
$crmCustomer->sn = $info->order_no;
|
||||
$crmCustomer->corp_id = $uInfo['corpId'];
|
||||
$crmCustomer->user_id = $uInfo['recipient'];
|
||||
$crmCustomer->type = 1;
|
||||
$crmCustomer->channel_id = $info->channel;
|
||||
$crmCustomer->country_id = $info->country;
|
||||
$crmCustomer->magnitude = $info->quality;
|
||||
$crmCustomer->email = $info->email;
|
||||
$crmCustomer->phone = $info->tel;
|
||||
$crmCustomer->WhatsApp = $info->whatsapp;
|
||||
$crmCustomer->contacts = $info->contacts;
|
||||
$crmCustomer->desc_collect = $info->content;
|
||||
$crmCustomer->enter_user_id = $info->enter_user_id;
|
||||
$crmCustomer->obtain = time();
|
||||
if(empty($uInfo['recipient'])){
|
||||
$crmCustomer->deleted_at = time();
|
||||
}
|
||||
// 修改同步CRM状态
|
||||
$info->sync_crm = 1;
|
||||
// 保存
|
||||
if (!$crmCustomer->save() || !$info->save()) {
|
||||
throw new BusinessException('同步失败,创建客户线索失败');
|
||||
}
|
||||
|
||||
// 消息推送
|
||||
if ($info->status == 1) {
|
||||
// $notice = '发布';
|
||||
$title = '[询单·公海] 新的客户线索【' . $info->order_no . '】发布';
|
||||
$content = '[询单·公海] 新的客户线索【' . $info->order_no . '】发布';
|
||||
$recipient = [$info->recipient];
|
||||
// $this->pushWs($title, $content);
|
||||
$this->pushEmail($info->id, $title, $uInfo['uinfo']['email']);
|
||||
}
|
||||
|
||||
if ($info->status == 2) {
|
||||
// $notice = '分配';
|
||||
$title = '[询单·分配] 新的客户线索【' . $info->order_no . '】,请及时处理';
|
||||
$content = '[询单·分配] 新的客户线索【' . $info->order_no . '】,请及时处理';
|
||||
$recipient = [$info->recipient];
|
||||
// $this->pushWs($title, $content, $recipient);
|
||||
$this->pushEmail($info->id, $title, $uInfo['uinfo']['email']);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function recipientInfo(Model $info)
|
||||
{
|
||||
$recipient = 0;
|
||||
$corpId = 0;
|
||||
$uinfo = null;
|
||||
if (!empty($info->recipient)) {
|
||||
$recipient = $info->recipient;
|
||||
$uinfo = User::where('id', $info->recipient)->first();
|
||||
if ($uinfo) {
|
||||
$corpId = $uinfo->corp_id;
|
||||
}
|
||||
}
|
||||
return [
|
||||
'recipient' => $recipient,
|
||||
'corpId' => $corpId,
|
||||
'uinfo' => $uinfo
|
||||
];
|
||||
}
|
||||
|
||||
// 消费失败回调
|
||||
/*
|
||||
$package = [
|
||||
'id' => 1357277951, // 消息ID
|
||||
'time' => 1709170510, // 消息时间
|
||||
'delay' => 0, // 延迟时间
|
||||
'attempts' => 2, // 消费次数
|
||||
'queue' => 'send-mail', // 队列名
|
||||
'data' => ['to' => 'tom@gmail.com', 'content' => 'hello'], // 消息内容
|
||||
'max_attempts' => 5, // 最大重试次数
|
||||
'error' => '错误信息' // 错误信息
|
||||
]
|
||||
*/
|
||||
public function onConsumeFailure(\Throwable $e, $package)
|
||||
{
|
||||
echo "consume failure\n";
|
||||
echo $e->getMessage() . "\n";
|
||||
// 无需反序列化
|
||||
var_export($package);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace app\queue\redis;
|
||||
|
||||
use support\exception\BusinessException;
|
||||
use Webman\RedisQueue\Consumer;
|
||||
|
||||
class Notice implements Consumer
|
||||
{
|
||||
// 要消费的队列名
|
||||
public $queue = 'notice';
|
||||
|
||||
// 连接名,对应 plugin/webman/redis-queue/redis.php 里的连接`
|
||||
public $connection = 'default';
|
||||
|
||||
// 消费
|
||||
public function consume($data)
|
||||
{
|
||||
try {
|
||||
// 应用消息
|
||||
$service = new \app\common\service\MessageNotificationsService();
|
||||
$service->pushNotice($data);
|
||||
|
||||
// 邮件消息
|
||||
if (isset($data['email']) && $data['email'] === 1) {
|
||||
$emails = $service->getUserEmailsById($data['recipient']);
|
||||
foreach ($emails as $email) {
|
||||
if (empty($email)) {
|
||||
continue;
|
||||
}
|
||||
\app\common\utils\email\MailSend::send($email, $data['title'], $data['content']);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} catch (\Throwable $th) {
|
||||
flg("消息推送异常:".$th->getFile()."|".$th->getLine()."|".$th->getMessage());
|
||||
throw new BusinessException($th->getFile()."|".$th->getLine()."|".$th->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 消费失败回调
|
||||
/*
|
||||
$package = [
|
||||
'id' => 1357277951, // 消息ID
|
||||
'time' => 1709170510, // 消息时间
|
||||
'delay' => 0, // 延迟时间
|
||||
'attempts' => 2, // 消费次数
|
||||
'queue' => 'send-mail', // 队列名
|
||||
'data' => ['to' => 'tom@gmail.com', 'content' => 'hello'], // 消息内容
|
||||
'max_attempts' => 5, // 最大重试次数
|
||||
'error' => '错误信息' // 错误信息
|
||||
]
|
||||
*/
|
||||
public function onConsumeFailure(\Throwable $e, $package)
|
||||
{
|
||||
echo "consume failure\n";
|
||||
echo $e->getMessage() . "\n";
|
||||
// 无需反序列化
|
||||
var_export($package);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace app\queue\redis;
|
||||
|
||||
use support\Db;
|
||||
use support\exception\BusinessException;
|
||||
use Webman\RedisQueue\Consumer;
|
||||
|
||||
class PermAgt implements Consumer
|
||||
{
|
||||
// 要消费的队列名
|
||||
public $queue = 'perm-agt';
|
||||
|
||||
// 连接名,对应 plugin/webman/redis-queue/redis.php 里的连接`
|
||||
public $connection = 'default';
|
||||
|
||||
// 消费
|
||||
public function consume($data)
|
||||
{
|
||||
try {
|
||||
|
||||
$corpId = $data['corp_id'];
|
||||
$userId = $data['user_id'];
|
||||
|
||||
if ($corpId == 2) {
|
||||
$roles = [
|
||||
[
|
||||
'user_id' => $userId,
|
||||
'role_id' => 1
|
||||
],
|
||||
[
|
||||
'user_id' => $userId,
|
||||
'role_id' => 2
|
||||
],
|
||||
];
|
||||
$systems = [
|
||||
[
|
||||
'user_id' => $userId,
|
||||
'system_id' => 1
|
||||
],
|
||||
[
|
||||
'user_id' => $userId,
|
||||
'system_id' => 2
|
||||
],
|
||||
[
|
||||
'user_id' => $userId,
|
||||
'system_id' => 3
|
||||
],
|
||||
];
|
||||
} else {
|
||||
$roles = [
|
||||
'user_id' => $userId,
|
||||
'role_id' => 2
|
||||
];
|
||||
$systems = [
|
||||
[
|
||||
'user_id' => $userId,
|
||||
'system_id' => 2
|
||||
],
|
||||
[
|
||||
'user_id' => $userId,
|
||||
'system_id' => 3
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
Db::table('user_roles')->insert($roles);
|
||||
Db::table('user_systems')->insert($systems);
|
||||
return true;
|
||||
} catch (\Throwable $th) {
|
||||
throw new BusinessException($th->getFile()."|".$th->getLine()."|".$th->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 消费失败回调
|
||||
/*
|
||||
$package = [
|
||||
'id' => 1357277951, // 消息ID
|
||||
'time' => 1709170510, // 消息时间
|
||||
'delay' => 0, // 延迟时间
|
||||
'attempts' => 2, // 消费次数
|
||||
'queue' => 'send-mail', // 队列名
|
||||
'data' => ['to' => 'tom@gmail.com', 'content' => 'hello'], // 消息内容
|
||||
'max_attempts' => 5, // 最大重试次数
|
||||
'error' => '错误信息' // 错误信息
|
||||
]
|
||||
*/
|
||||
public function onConsumeFailure(\Throwable $e, $package)
|
||||
{
|
||||
echo "consume failure\n";
|
||||
echo $e->getMessage() . "\n";
|
||||
// 无需反序列化
|
||||
var_export($package);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace app\queue\redis;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\RequestException;
|
||||
use support\exception\BusinessException;
|
||||
use Webman\RedisQueue\Consumer;
|
||||
|
||||
class Server implements Consumer
|
||||
{
|
||||
// 要消费的队列名
|
||||
public $queue = 'server';
|
||||
|
||||
// 连接名,对应 plugin/webman/redis-queue/redis.php 里的连接`
|
||||
public $connection = 'default';
|
||||
|
||||
// 消费
|
||||
public function consume($data)
|
||||
{
|
||||
try {
|
||||
$source = $data['source'] === 'development' ? '-- 开发环境 --' : '** 生产环境 **';
|
||||
$webhookUrl = 'https://open.feishu.cn/open-apis/bot/v2/hook/806c2e9d-6ca2-4345-a459-69bb2651706b';
|
||||
$message = "====== ======\n";
|
||||
$message .= $data['content']."\n";
|
||||
$message .= "时间:".date('Y-m-d H:i:s', time())."\n";
|
||||
$message .= "来源:".$source."\n";
|
||||
$message .= "====== ======";
|
||||
sendFeishuTextMessage($webhookUrl, $message);
|
||||
return true;
|
||||
} catch (\Throwable $th) {
|
||||
var_dump('4 :'.$th->getFile()."|".$th->getLine()."|".$th->getMessage());
|
||||
throw new BusinessException($th->getFile()."|".$th->getLine()."|".$th->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 消费失败回调
|
||||
/*
|
||||
$package = [
|
||||
'id' => 1357277951, // 消息ID
|
||||
'time' => 1709170510, // 消息时间
|
||||
'delay' => 0, // 延迟时间
|
||||
'attempts' => 2, // 消费次数
|
||||
'queue' => 'send-mail', // 队列名
|
||||
'data' => ['to' => 'tom@gmail.com', 'content' => 'hello'], // 消息内容
|
||||
'max_attempts' => 5, // 最大重试次数
|
||||
'error' => '错误信息' // 错误信息
|
||||
]
|
||||
*/
|
||||
public function onConsumeFailure(\Throwable $e, $package)
|
||||
{
|
||||
echo "consume failure\n";
|
||||
echo $e->getMessage() . "\n";
|
||||
// 无需反序列化
|
||||
var_export($package);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace app\queue\redis;
|
||||
|
||||
use support\exception\BusinessException;
|
||||
use Webman\RedisQueue\Consumer;
|
||||
|
||||
class StockOrderPdf implements Consumer
|
||||
{
|
||||
// 要消费的队列名
|
||||
public $queue = 'stock-order-pdf';
|
||||
|
||||
// 连接名,对应 plugin/webman/redis-queue/redis.php 里的连接`
|
||||
public $connection = 'default';
|
||||
|
||||
// 消费
|
||||
public function consume($data)
|
||||
{
|
||||
try {
|
||||
|
||||
$service = new \app\crm\service\StockOrdersService();
|
||||
$service->createdPdfFile($data['id']);
|
||||
|
||||
return true;
|
||||
} catch (\Throwable $th) {
|
||||
var_dump('4 :'.$th->getFile()."|".$th->getLine()."|".$th->getMessage());
|
||||
throw new BusinessException($th->getFile()."|".$th->getLine()."|".$th->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 消费失败回调
|
||||
/*
|
||||
$package = [
|
||||
'id' => 1357277951, // 消息ID
|
||||
'time' => 1709170510, // 消息时间
|
||||
'delay' => 0, // 延迟时间
|
||||
'attempts' => 2, // 消费次数
|
||||
'queue' => 'send-mail', // 队列名
|
||||
'data' => ['to' => 'tom@gmail.com', 'content' => 'hello'], // 消息内容
|
||||
'max_attempts' => 5, // 最大重试次数
|
||||
'error' => '错误信息' // 错误信息
|
||||
]
|
||||
*/
|
||||
public function onConsumeFailure(\Throwable $e, $package)
|
||||
{
|
||||
echo "consume failure\n";
|
||||
echo $e->getMessage() . "\n";
|
||||
// 无需反序列化
|
||||
var_export($package);
|
||||
}
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace app\queue\redis;
|
||||
|
||||
use Webman\RedisQueue\Consumer;
|
||||
use support\Log;
|
||||
use app\common\model\WordpressSite;
|
||||
use app\common\model\WordpressUsedCar;
|
||||
use app\common\model\UsedCarDetail;
|
||||
use app\common\service\WordpressSiteService;
|
||||
|
||||
class WordPressPush implements Consumer
|
||||
{
|
||||
// 要消费的队列名
|
||||
public $queue = 'wordpress-push';
|
||||
|
||||
// 连接名
|
||||
public $connection = 'default';
|
||||
|
||||
/**
|
||||
* 消费 WordPress 推送任务
|
||||
* @param array $data
|
||||
* - car_id: 车辆ID
|
||||
* - site_id: WordPress站点ID(可选,默认使用启用的站点)
|
||||
*/
|
||||
public function consume($data)
|
||||
{
|
||||
$carId = $data['car_id'] ?? null;
|
||||
$siteId = $data['site_id'] ?? null;
|
||||
|
||||
if (empty($carId)) {
|
||||
Log::error('WordPress推送:车辆ID为空', ['data' => $data]);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取车辆信息
|
||||
$car = UsedCarDetail::find($carId);
|
||||
if (!$car) {
|
||||
Log::warning('WordPress推送:车辆不存在', ['car_id' => $carId]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取WordPress站点
|
||||
if ($siteId) {
|
||||
$site = WordpressSite::find($siteId);
|
||||
} else {
|
||||
$site = WordpressSite::where('status', 1)->first();
|
||||
}
|
||||
|
||||
if (!$site) {
|
||||
$msg = '没有启用的WordPress站点';
|
||||
Log::warning('WordPress推送失败:' . $msg, ['car_id' => $carId]);
|
||||
// 推送失败,更新车辆状态为5
|
||||
$car->update(['status' => 5]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否已推送过
|
||||
$existing = WordpressUsedCar::where('wp_site_id', $site->id)
|
||||
->where('used_car_id', $carId)
|
||||
->where('status', 1)
|
||||
->first();
|
||||
|
||||
if ($existing) {
|
||||
Log::info('WordPress推送:车辆已推送过,跳过', [
|
||||
'car_id' => $carId,
|
||||
'wp_product_id' => $existing->wp_product_id
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 执行推送
|
||||
$wpService = new WordpressSiteService();
|
||||
$result = $wpService->createProductForNewSite($site, $carId);
|
||||
|
||||
// 推送成功(没有抛出异常),更新车辆状态为4
|
||||
$car->update(['status' => 4]);
|
||||
Log::info('WordPress推送成功,车辆状态已更新为推送成功', [
|
||||
'car_id' => $carId,
|
||||
'wp_post_id' => $result['result']['id'] ?? null,
|
||||
]);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('WordPress推送异常', [
|
||||
'car_id' => $carId,
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
// 推送失败,更新车辆状态为5
|
||||
try {
|
||||
if ($car) {
|
||||
$car->update(['status' => 5]);
|
||||
}
|
||||
} catch (\Throwable $updateError) {
|
||||
Log::error('更新车辆状态失败', [
|
||||
'car_id' => $carId,
|
||||
'error' => $updateError->getMessage(),
|
||||
]);
|
||||
}
|
||||
// 不再重新抛出异常,让队列正常处理下一条
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 消费失败回调
|
||||
*/
|
||||
public function onConsumeFailure(\Throwable $e, $package)
|
||||
{
|
||||
Log::error('WordPress推送消费失败', [
|
||||
'error' => $e->getMessage(),
|
||||
'package' => $package,
|
||||
]);
|
||||
|
||||
// 获取车辆ID并更新状态
|
||||
$carId = $package['data']['car_id'] ?? null;
|
||||
if ($carId) {
|
||||
try {
|
||||
UsedCarDetail::where('id', $carId)->update(['status' => 5]);
|
||||
} catch (\Throwable $updateError) {
|
||||
Log::error('推送失败后更新车辆状态异常', [
|
||||
'car_id' => $carId,
|
||||
'error' => $updateError->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace app\queue\redis;
|
||||
|
||||
use app\common\model\UserSystems;
|
||||
use Webman\RedisQueue\Consumer;
|
||||
|
||||
class WsMsg implements Consumer
|
||||
{
|
||||
// 要消费的队列名
|
||||
public $queue = 'ws-msg';
|
||||
|
||||
// 连接名,对应 plugin/webman/redis-queue/redis.php 里的连接`
|
||||
public $connection = 'default';
|
||||
|
||||
// 消费
|
||||
public function consume($data)
|
||||
{
|
||||
try {
|
||||
$ly = [
|
||||
1 => 186,
|
||||
3 => 226,
|
||||
];
|
||||
|
||||
if (isset($data['is_first']) && $data['is_first'] === true) {
|
||||
$num = (new \app\common\utils\approval\service\ApprovalFlowService())->getInitTodoCount($data['user_id']);
|
||||
} else {
|
||||
$num = $data['num'];
|
||||
}
|
||||
|
||||
if ($data['event'] === 'approval.todo.counts') {
|
||||
$systemIds = UserSystems::where('user_id', $data['user_id'])->pluck('system_id')->toArray();
|
||||
if (count($systemIds) > 0) {
|
||||
foreach ($systemIds as $systemId) {
|
||||
if (isset($ly[$systemId])) {
|
||||
$chain = \app\common\service\MenuBadgeService::buildBadgeChain(menuId: $ly[$systemId], leafCount: $num);
|
||||
(new \app\common\utils\WsEvents())->pushWebsocket('approval.todo.counts', $data['user_id'], $chain);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $th) {
|
||||
dpr($th);
|
||||
throw $th;
|
||||
}
|
||||
}
|
||||
|
||||
// 消费失败回调
|
||||
/*
|
||||
$package = [
|
||||
'id' => 1357277951, // 消息ID
|
||||
'time' => 1709170510, // 消息时间
|
||||
'delay' => 0, // 延迟时间
|
||||
'attempts' => 2, // 消费次数
|
||||
'queue' => 'send-mail', // 队列名
|
||||
'data' => ['to' => 'tom@gmail.com', 'content' => 'hello'], // 消息内容
|
||||
'max_attempts' => 5, // 最大重试次数
|
||||
'error' => '错误信息' // 错误信息
|
||||
]
|
||||
*/
|
||||
public function onConsumeFailure(\Throwable $e, $package)
|
||||
{
|
||||
echo "consume failure\n";
|
||||
echo $e->getMessage() . "\n";
|
||||
// 无需反序列化
|
||||
var_export($package);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
<?php
|
||||
return [
|
||||
'consumer' => [
|
||||
'handler' => Webman\RedisQueue\Process\Consumer::class,
|
||||
'count' => 1, // 消费者进程数量
|
||||
'constructor' => [
|
||||
// 消费者类目录
|
||||
'consumer_dir' => app_path() . '/queue/redis',
|
||||
]
|
||||
]
|
||||
// 'consumer' => [
|
||||
// 'handler' => Webman\RedisQueue\Process\Consumer::class,
|
||||
// 'count' => getenv('REDIS_QUEUE_PROCESS_COUNT'), // 可以设置多进程同时消费
|
||||
// 'constructor' => [
|
||||
// // 消费者类目录
|
||||
// 'consumer_dir' => app_path() . '/queue/redis',
|
||||
// 'consumer_dir' => app_path() . getenv('REDIS_QUEUE_CONSUMER_DIR')
|
||||
// ]
|
||||
// ]
|
||||
];
|
||||
@@ -41,9 +41,6 @@ Route::any('/upload/delete', [app\common\controller\UploadController::class, 'de
|
||||
|
||||
// 公共
|
||||
Route::group('/common', function () {
|
||||
Route::group('/preset', function () {
|
||||
Route::any('/corps', [app\common\controller\PresetController::class, 'corps']);
|
||||
});
|
||||
Route::group('/auth', function () {
|
||||
Route::any('/login', [app\common\controller\AuthController::class, 'login']);
|
||||
Route::any('/feishu/scope', [app\common\controller\AuthController::class, 'feishuScope']);
|
||||
@@ -67,36 +64,8 @@ Route::group('/common', function () {
|
||||
Route::any('/show', [app\common\controller\UsedCarController::class, 'show']);
|
||||
Route::any('/store', [app\common\controller\UsedCarController::class, 'store']);
|
||||
Route::any('/update', [app\common\controller\UsedCarController::class, 'update']);
|
||||
Route::any('/update/status', [app\common\controller\UsedCarController::class, 'status']);
|
||||
Route::any('/destroy', [app\common\controller\UsedCarController::class, 'destroy']);
|
||||
Route::any('/crawl', [app\common\controller\UsedCarController::class, 'crawlKeyword']);
|
||||
Route::any('/url_crawl', [app\common\controller\UsedCarController::class, 'crawlCar']);
|
||||
Route::any('/mobile/url_crawl', [app\common\controller\UsedCarController::class, 'crawlMobileCar']);
|
||||
});
|
||||
|
||||
// 二手车详情(扩展版)
|
||||
Route::group('/usedcardetail', function () {
|
||||
Route::any('/list', [app\common\controller\UsedCarDetailController::class, 'index']);
|
||||
Route::any('/show', [app\common\controller\UsedCarDetailController::class, 'show']);
|
||||
Route::any('/store', [app\common\controller\UsedCarDetailController::class, 'storeWithFaqs']);
|
||||
Route::any('/update', [app\common\controller\UsedCarDetailController::class, 'updateWithFaqs']);
|
||||
Route::any('/update/status', [app\common\controller\UsedCarDetailController::class, 'status']);
|
||||
Route::any('/destroy', [app\common\controller\UsedCarDetailController::class, 'destroy']);
|
||||
Route::any('/store/faqs', [app\common\controller\UsedCarDetailController::class, 'storeWithFaqs']);
|
||||
Route::any('/update/faqs', [app\common\controller\UsedCarDetailController::class, 'updateWithFaqs']);
|
||||
Route::any('/save/faqs', [app\common\controller\UsedCarDetailController::class, 'saveFaqs']);
|
||||
|
||||
// 审核相关
|
||||
Route::any('/audit', [app\common\controller\UsedCarDetailController::class, 'audit']);
|
||||
Route::any('/audit/records', [app\common\controller\UsedCarDetailController::class, 'auditRecords']);
|
||||
Route::any('/audit/stats', [app\common\controller\UsedCarDetailController::class, 'auditStats']);
|
||||
|
||||
// 抓取相关
|
||||
Route::any('/crawl', [app\common\controller\UsedCarDetailController::class, 'crawl']);
|
||||
Route::any('/crawl/store', [app\common\controller\UsedCarDetailController::class, 'crawlStore']);
|
||||
|
||||
// 重新推送
|
||||
Route::any('/retry/push', [app\common\controller\UsedCarDetailController::class, 'retryPush']);
|
||||
});
|
||||
|
||||
Route::group('/user', function () {
|
||||
@@ -104,21 +73,5 @@ Route::group('/common', function () {
|
||||
Route::any('/edit', [app\common\controller\UserController::class, 'edit']);
|
||||
});
|
||||
|
||||
//分配whatsApp
|
||||
Route::group('/wordpress', function () {
|
||||
Route::any('/whatsApp', [app\common\controller\WordpressVisitorController::class, 'whatsApp']);
|
||||
});
|
||||
});
|
||||
|
||||
Route::group('/admin', function () {
|
||||
//仪表盘路由规则
|
||||
Route::group('/wp', function () {
|
||||
Route::any('/list', [app\admin\controller\WordpressSiteController::class, 'index']);
|
||||
Route::any('/show', [app\admin\controller\WordpressSiteController::class, 'show']);
|
||||
Route::any('/store', [app\admin\controller\WordpressSiteController::class, 'store']);
|
||||
Route::any('/update', [app\admin\controller\WordpressSiteController::class, 'update']);
|
||||
Route::any('/destroy', [app\admin\controller\WordpressSiteController::class, 'destroy']);
|
||||
Route::any('/send', [app\admin\controller\WordpressSiteController::class, 'createProduct']);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,122 +0,0 @@
|
||||
-- 二手车主表(扩展版)
|
||||
CREATE TABLE `used_car_detail` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
|
||||
-- 基本信息(继承自 used_car)
|
||||
`code` varchar(50) DEFAULT NULL COMMENT '车辆编号',
|
||||
`user_id` bigint(20) unsigned NOT NULL COMMENT '用户ID',
|
||||
`role_wp` tinyint(1) NOT NULL DEFAULT '2' COMMENT 'wp数据角色:1:业务,2:外部供应商,3:管理员',
|
||||
`product_name` varchar(255) NOT NULL COMMENT '产品名称',
|
||||
`vin` varchar(50) DEFAULT NULL COMMENT '车架号',
|
||||
`price` decimal(10,2) unsigned NOT NULL DEFAULT '0.00' COMMENT '车辆价格',
|
||||
`source_place` varchar(100) DEFAULT NULL COMMENT '来源地',
|
||||
`plate_place` varchar(100) DEFAULT NULL COMMENT '上牌地',
|
||||
`plate_time` varchar(20) DEFAULT NULL COMMENT '上牌时间',
|
||||
`displacement` varchar(50) DEFAULT NULL COMMENT '排量',
|
||||
`mileage` varchar(50) DEFAULT NULL COMMENT '里程',
|
||||
`power` varchar(50) DEFAULT NULL COMMENT '马力',
|
||||
`body_color` varchar(50) DEFAULT NULL COMMENT '车身颜色',
|
||||
`images` text COMMENT '车辆图片JSON',
|
||||
`condition_desc` text COMMENT '车况描述',
|
||||
`attachment` text COMMENT '附件JSON',
|
||||
`pdf_src` varchar(255) DEFAULT NULL COMMENT 'PDF文件路径',
|
||||
`source_url` varchar(500) DEFAULT NULL COMMENT '来源链接',
|
||||
`crawl_title` varchar(255) DEFAULT NULL COMMENT '爬取标题',
|
||||
`crawl_brand` varchar(100) DEFAULT NULL COMMENT '爬取品牌',
|
||||
`crawl_brand_id` varchar(50) DEFAULT NULL COMMENT '爬取品牌ID',
|
||||
`crawl_series` varchar(100) DEFAULT NULL COMMENT '爬取车系',
|
||||
`crawl_series_id` varchar(50) DEFAULT NULL COMMENT '爬取车系ID',
|
||||
`crawl_year` varchar(10) DEFAULT NULL COMMENT '爬取年份',
|
||||
`crawl_msrp` decimal(10,2) unsigned DEFAULT NULL COMMENT '爬取厂商指导价',
|
||||
`crawl_car_id` varchar(50) DEFAULT NULL COMMENT '爬取车型ID',
|
||||
`status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '审核状态:0=待审核,1=审核通过,2=审核拒绝',
|
||||
`sort` int(11) NOT NULL DEFAULT '0' COMMENT '排序',
|
||||
|
||||
-- 车辆信息简单描述(新增)
|
||||
`simple_desc` text COMMENT '车辆信息简单描述',
|
||||
|
||||
-- 车辆介绍(新增)
|
||||
`intro_images` text COMMENT '车辆介绍图片JSON',
|
||||
`intro_text` text COMMENT '车辆介绍文本',
|
||||
`intro_video` varchar(500) DEFAULT NULL COMMENT '车辆介绍视频URL',
|
||||
|
||||
-- 1. 基本信息(新增)
|
||||
`official_guide_price` decimal(10,2) unsigned DEFAULT NULL COMMENT '官方指导价',
|
||||
`manufacturer` varchar(100) DEFAULT NULL COMMENT '厂商',
|
||||
`level` varchar(50) DEFAULT NULL COMMENT '级别',
|
||||
`energy_type` varchar(50) DEFAULT NULL COMMENT '能源类型',
|
||||
`listing_time` varchar(20) DEFAULT NULL COMMENT '上市时间',
|
||||
`engine` varchar(255) DEFAULT NULL COMMENT '发动机',
|
||||
`electric_motor` varchar(255) DEFAULT NULL COMMENT '电动机',
|
||||
`nedc_range` int(11) unsigned DEFAULT NULL COMMENT '纯电续航里程(km)工信部',
|
||||
`cltc_range` int(11) unsigned DEFAULT NULL COMMENT '纯电续航里程(km)CLTC',
|
||||
`fast_charge_platform` varchar(100) DEFAULT NULL COMMENT '高压快充平台',
|
||||
`charging_time` decimal(5,2) unsigned DEFAULT NULL COMMENT '充电时间(小时)',
|
||||
`fast_charge_percent` tinyint(3) unsigned DEFAULT NULL COMMENT '快充电量(%)',
|
||||
`max_power_kw` int(11) unsigned DEFAULT NULL COMMENT '最大功率(kW)',
|
||||
`max_torque_nm` int(11) unsigned DEFAULT NULL COMMENT '最大扭矩(N·m)',
|
||||
`transmission` varchar(100) DEFAULT NULL COMMENT '变速箱',
|
||||
`dimensions` varchar(50) DEFAULT NULL COMMENT '长x宽x高(mm)',
|
||||
`body_structure` varchar(50) DEFAULT NULL COMMENT '车身结构',
|
||||
`max_speed` int(11) unsigned DEFAULT NULL COMMENT '最高车速(km/h)',
|
||||
`acceleration_100` decimal(4,2) unsigned DEFAULT NULL COMMENT '官方百公里加速时间(s)',
|
||||
`nedc_fuel_consumption` decimal(5,2) unsigned DEFAULT NULL COMMENT 'NEDC综合油耗(L/100km)',
|
||||
`power_consumption_100` decimal(5,2) unsigned DEFAULT NULL COMMENT '百公里耗电量(kWh/100km)',
|
||||
`equivalent_fuel_consumption` decimal(5,2) unsigned DEFAULT NULL COMMENT '电能当量燃料消耗量(L/100km)',
|
||||
`maintenance_cost_60k` decimal(10,2) unsigned DEFAULT NULL COMMENT '6万公里保养总成本预估',
|
||||
|
||||
-- 2. 车身信息(新增)
|
||||
`body_length` int(11) unsigned DEFAULT NULL COMMENT '长(mm)',
|
||||
`body_width` int(11) unsigned DEFAULT NULL COMMENT '宽(mm)',
|
||||
`body_height` int(11) unsigned DEFAULT NULL COMMENT '高(mm)',
|
||||
`wheelbase` int(11) unsigned DEFAULT NULL COMMENT '轴距(mm)',
|
||||
`door_count` tinyint(3) unsigned DEFAULT NULL COMMENT '车门数(个)',
|
||||
`door_open_type` varchar(50) DEFAULT NULL COMMENT '车门开启方式',
|
||||
`seat_count` tinyint(3) unsigned DEFAULT NULL COMMENT '座位数(个)',
|
||||
|
||||
-- 3. 变速箱(新增)
|
||||
`transmission_desc` varchar(255) DEFAULT NULL COMMENT '变速箱描述',
|
||||
`gear_count` tinyint(3) unsigned DEFAULT NULL COMMENT '挡位数',
|
||||
`transmission_type` varchar(50) DEFAULT NULL COMMENT '变速箱类型',
|
||||
|
||||
-- 4. 底盘/转向(新增)
|
||||
`drive_mode` varchar(50) DEFAULT NULL COMMENT '驱动方式',
|
||||
`front_suspension` varchar(100) DEFAULT NULL COMMENT '前悬挂形式',
|
||||
`rear_suspension` varchar(100) DEFAULT NULL COMMENT '后悬挂形式',
|
||||
`steering_type` varchar(50) DEFAULT NULL COMMENT '转向类型',
|
||||
|
||||
-- 5. 主动安全(新增)
|
||||
`has_abs` varchar(20) DEFAULT NULL COMMENT 'ABS防抱死',
|
||||
`has_ebd` varchar(20) DEFAULT NULL COMMENT '制动力分配(EBD/CBC等)',
|
||||
`has_bra` varchar(20) DEFAULT NULL COMMENT '刹车辅助(EBA/BA等)',
|
||||
`has_tcs` varchar(20) DEFAULT NULL COMMENT '牵引力控制(TCS/ASR等)',
|
||||
`has_esp` varchar(20) DEFAULT NULL COMMENT '车身稳定系统(ESP/DSC等)',
|
||||
`has_active_brake` varchar(20) DEFAULT NULL COMMENT '主动刹车',
|
||||
`has_lane_assist` varchar(20) DEFAULT NULL COMMENT '并线辅助',
|
||||
`has_lka` varchar(20) DEFAULT NULL COMMENT '车道保持辅助系统',
|
||||
`has_lane_centering` varchar(20) DEFAULT NULL COMMENT '车道居中保持',
|
||||
`has_fatigue_remind` varchar(20) DEFAULT NULL COMMENT '疲劳驾驶提示',
|
||||
|
||||
-- 6. 车轮/制动(新增)
|
||||
`front_brake_type` varchar(50) DEFAULT NULL COMMENT '前制动器类型',
|
||||
`rear_brake_type` varchar(50) DEFAULT NULL COMMENT '后制动器类型',
|
||||
`parking_brake_type` varchar(50) DEFAULT NULL COMMENT '驻车制动类型',
|
||||
`front_tire_spec` varchar(50) DEFAULT NULL COMMENT '前轮胎规格尺寸',
|
||||
`rear_tire_spec` varchar(50) DEFAULT NULL COMMENT '后轮胎规格尺寸',
|
||||
|
||||
`created_at` timestamp NULL DEFAULT NULL,
|
||||
`updated_at` timestamp NULL DEFAULT NULL,
|
||||
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_code` (`code`),
|
||||
KEY `idx_user_id` (`user_id`),
|
||||
KEY `idx_role_wp` (`role_wp`),
|
||||
KEY `idx_status` (`status`),
|
||||
KEY `idx_brand_series` (`crawl_brand_id`, `crawl_series_id`),
|
||||
KEY `idx_price` (`price`),
|
||||
KEY `idx_created_at` (`created_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='二手车详情表(扩展版)';
|
||||
|
||||
|
||||
ALTER TABLE `useds`.`user`
|
||||
ADD COLUMN `role_wp` TINYINT (2) DEFAULT 1 NULL COMMENT 'wp数据角色:1:业务,2:外部供应商,3:管理员' AFTER `role`;
|
||||
@@ -1,18 +0,0 @@
|
||||
-- 二手车审核记录表
|
||||
CREATE TABLE `used_car_detail_audit` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`car_id` bigint(20) unsigned NOT NULL COMMENT '关联used_car_detail.id',
|
||||
`auditor_id` bigint(20) unsigned NOT NULL COMMENT '审核人ID',
|
||||
`audit_status` tinyint(1) NOT NULL COMMENT '审核状态:0=待审核,1=通过,2=拒绝',
|
||||
`audit_reason` varchar(500) DEFAULT NULL COMMENT '审核理由/拒绝原因',
|
||||
`before_status` tinyint(1) DEFAULT NULL COMMENT '审核前状态',
|
||||
`after_status` tinyint(1) DEFAULT NULL COMMENT '审核后状态',
|
||||
`created_at` timestamp NULL DEFAULT NULL COMMENT '审核时间',
|
||||
`updated_at` timestamp NULL DEFAULT NULL,
|
||||
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_car_id` (`car_id`),
|
||||
KEY `idx_auditor_id` (`auditor_id`),
|
||||
KEY `idx_audit_status` (`audit_status`),
|
||||
KEY `idx_created_at` (`created_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='二手车审核记录表';
|
||||
@@ -1,16 +0,0 @@
|
||||
-- 二手车FAQ关联表
|
||||
CREATE TABLE `used_car_faq` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`car_id` bigint(20) unsigned NOT NULL COMMENT '关联used_car_detail.id',
|
||||
`question` varchar(500) NOT NULL COMMENT '问题',
|
||||
`answer` text NOT NULL COMMENT '答案',
|
||||
`sort` int(11) NOT NULL DEFAULT '0' COMMENT '排序',
|
||||
`status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '状态:0=禁用,1=启用',
|
||||
`created_at` timestamp NULL DEFAULT NULL,
|
||||
`updated_at` timestamp NULL DEFAULT NULL,
|
||||
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_car_id` (`car_id`),
|
||||
KEY `idx_status` (`status`),
|
||||
KEY `idx_sort` (`sort`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='二手车常见问题表';
|
||||
@@ -0,0 +1,94 @@
|
||||
CREATE TABLE `approval_flows` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(255) NOT NULL COMMENT '流程名称',
|
||||
`type` varchar(100) NOT NULL COMMENT '业务类型标识,如 leave, reimbursement',
|
||||
`steps` json NOT NULL COMMENT '审批步骤配置',
|
||||
`is_active` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否启用',
|
||||
`timeout_hours` int(11) NOT NULL DEFAULT '0' COMMENT '实例级超时时间(小时),0为不超时',
|
||||
`timeout_action` varchar(20) NOT NULL DEFAULT 'auto_reject' COMMENT 'auto_approve | auto_reject',
|
||||
`created_at` timestamp NULL DEFAULT NULL,
|
||||
`updated_at` timestamp NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_flow_type` (`type`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COMMENT='审批流程定义';
|
||||
|
||||
CREATE TABLE `approval_instances` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`flow_type` varchar(100) NOT NULL COMMENT '关联 approval_flows.type',
|
||||
`biz_id` varchar(255) NOT NULL COMMENT '业务ID',
|
||||
`biz_type` varchar(100) NOT NULL COMMENT '业务标识/表名',
|
||||
`form_data` json NOT NULL COMMENT '提交表单快照',
|
||||
`initiator_id` bigint(20) unsigned NOT NULL COMMENT '发起人',
|
||||
`status` varchar(20) NOT NULL DEFAULT 'processing' COMMENT 'processing|approved|rejected|revoked|timed_out',
|
||||
`current_step` int(11) NOT NULL DEFAULT '0' COMMENT '当前步骤索引',
|
||||
`round` int(11) NOT NULL DEFAULT '1' COMMENT '当前轮次:1=首次,重提+1',
|
||||
`finished_at` timestamp NULL DEFAULT NULL,
|
||||
`deadline_at` timestamp NULL DEFAULT NULL,
|
||||
`created_at` timestamp NULL DEFAULT NULL,
|
||||
`updated_at` timestamp NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_flow_type` (`flow_type`),
|
||||
KEY `idx_biz` (`biz_type`,`biz_id`),
|
||||
KEY `idx_initiator` (`initiator_id`),
|
||||
KEY `idx_status_updated` (`status`,`updated_at`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=68 DEFAULT CHARSET=utf8mb4 COMMENT='审批实例';
|
||||
|
||||
CREATE TABLE `approval_records` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`instance_id` bigint(20) unsigned NOT NULL COMMENT 'approval_instances.id',
|
||||
`approver_id` bigint(20) unsigned NOT NULL COMMENT '审批人',
|
||||
`step_index` int(11) NOT NULL COMMENT '步骤索引',
|
||||
`round` int(11) NOT NULL DEFAULT '1' COMMENT '所属轮次',
|
||||
`action` varchar(20) DEFAULT NULL COMMENT 'approve|reject,待办时为NULL',
|
||||
`status` varchar(20) NOT NULL DEFAULT 'pending' COMMENT 'pending|approved|rejected|skipped|timeout',
|
||||
`comment` text COMMENT '审批意见',
|
||||
`approved_at` timestamp NULL DEFAULT NULL COMMENT '审批时间/关闭时间',
|
||||
`created_at` timestamp NULL DEFAULT NULL,
|
||||
`updated_at` timestamp NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_instance_step_approver` (`instance_id`,`step_index`,`approver_id`),
|
||||
KEY `idx_instance_step_status` (`instance_id`,`step_index`,`status`),
|
||||
KEY `idx_approver` (`approver_id`),
|
||||
KEY `idx_instance_step_round` (`instance_id`,`step_index`,`round`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=105 DEFAULT CHARSET=utf8mb4 COMMENT='审批记录';
|
||||
|
||||
CREATE TABLE `approval_transfers` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`instance_id` bigint(20) unsigned NOT NULL,
|
||||
`step_index` int(11) NOT NULL,
|
||||
`record_id` bigint(20) unsigned NOT NULL COMMENT '转交后仍然有效的记录ID(若合并则为目标记录ID)',
|
||||
`from_user_id` bigint(20) unsigned NOT NULL,
|
||||
`to_user_id` bigint(20) unsigned NOT NULL,
|
||||
`reason` text,
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_record` (`record_id`),
|
||||
KEY `idx_instance_step` (`instance_id`,`step_index`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='审批转交日志';
|
||||
|
||||
CREATE TABLE `approval_ccs` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`instance_id` bigint(20) unsigned NOT NULL,
|
||||
`user_id` bigint(20) unsigned NOT NULL,
|
||||
`notified_at` timestamp NULL DEFAULT NULL,
|
||||
`read_at` timestamp NULL DEFAULT NULL,
|
||||
`created_at` timestamp NULL DEFAULT NULL,
|
||||
`updated_at` timestamp NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_instance_user` (`instance_id`,`user_id`),
|
||||
KEY `idx_user` (`user_id`),
|
||||
KEY `idx_instance` (`instance_id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COMMENT='审批抄送';
|
||||
|
||||
CREATE TABLE `approval_discussions` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`instance_id` bigint(20) unsigned NOT NULL,
|
||||
`user_id` bigint(20) unsigned NOT NULL,
|
||||
`is_internal` tinyint(1) NOT NULL DEFAULT '0' COMMENT '0=公开,1=仅审批人可见',
|
||||
`content` text NOT NULL,
|
||||
`attachments` json DEFAULT NULL,
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_instance_id` (`instance_id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='审批讨论';
|
||||
@@ -0,0 +1,174 @@
|
||||
SET NAMES utf8mb4;
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
|
||||
-- 1. 物品主数据表:增加 company_id
|
||||
DROP TABLE IF EXISTS inv_item;
|
||||
CREATE TABLE inv_item (
|
||||
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
company_id BIGINT(20) UNSIGNED DEFAULT NULL COMMENT '物品所属公司ID',
|
||||
item_code VARCHAR(64) NOT NULL COMMENT '物品编号(公司内部编码)',
|
||||
item_name VARCHAR(128) NOT NULL COMMENT '物品名称',
|
||||
category_id BIGINT(20) UNSIGNED DEFAULT NULL COMMENT '物品分类ID(可选,指向分类表)',
|
||||
spec_model VARCHAR(128) DEFAULT NULL COMMENT '规格型号',
|
||||
unit VARCHAR(32) DEFAULT '件' COMMENT '计量单位,如 件/盒/包',
|
||||
is_consumable TINYINT(1) DEFAULT 1 COMMENT '是否耗材:1耗材(不需归还),0非耗材(需归还)',
|
||||
stock_qty INT(11) DEFAULT 0 COMMENT '当前库存数量',
|
||||
manage_user_id BIGINT(20) UNSIGNED DEFAULT NULL COMMENT '物品管理员用户ID',
|
||||
location VARCHAR(255) DEFAULT NULL COMMENT '存放位置说明',
|
||||
remark VARCHAR(255) DEFAULT NULL COMMENT '备注',
|
||||
created_at INT(11) NOT NULL DEFAULT 0 COMMENT '创建时间时间戳',
|
||||
updated_at INT(11) NOT NULL DEFAULT 0 COMMENT '更新时间时间戳',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_item_code (item_code),
|
||||
KEY idx_company (company_id),
|
||||
KEY idx_category (category_id),
|
||||
KEY idx_manage_user (manage_user_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='物品主数据表';
|
||||
|
||||
|
||||
-- 2. 物品领用/借用申请单(主表)
|
||||
DROP TABLE IF EXISTS inv_requisition;
|
||||
CREATE TABLE inv_requisition (
|
||||
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
order_no VARCHAR(64) NOT NULL COMMENT '申请单号',
|
||||
|
||||
applicant_company_id BIGINT(20) UNSIGNED DEFAULT NULL COMMENT '申请人所在公司ID(可选)',
|
||||
dept_id BIGINT(20) UNSIGNED DEFAULT NULL COMMENT '申请人部门ID',
|
||||
applicant_id BIGINT(20) UNSIGNED NOT NULL COMMENT '申请人用户ID',
|
||||
applicant_name VARCHAR(64) DEFAULT NULL COMMENT '申请人姓名快照',
|
||||
leader_id BIGINT(20) UNSIGNED DEFAULT NULL COMMENT '直属上级用户ID',
|
||||
manager_id BIGINT(20) UNSIGNED DEFAULT NULL COMMENT '物品管理员用户ID(主负责人)',
|
||||
|
||||
borrow_reason VARCHAR(255) NOT NULL COMMENT '领用/借用事由',
|
||||
use_start_date DATE NOT NULL COMMENT '借用/预计使用开始日期',
|
||||
use_end_date DATE DEFAULT NULL COMMENT '预计归还日期(耗材可为空)',
|
||||
|
||||
-- 0草稿 10待直属上级审批 20直属上级通过待物品管理员审批
|
||||
-- 30全部通过 40已驳回 50已取消
|
||||
status TINYINT(3) NOT NULL DEFAULT 10 COMMENT '审批状态',
|
||||
|
||||
is_returned TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否已全部归还(针对需要归还的物品):0否 1是',
|
||||
|
||||
remark VARCHAR(255) DEFAULT NULL COMMENT '备注',
|
||||
created_at INT(11) NOT NULL DEFAULT 0 COMMENT '创建时间时间戳',
|
||||
updated_at INT(11) NOT NULL DEFAULT 0 COMMENT '更新时间时间戳',
|
||||
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_order_no (order_no),
|
||||
KEY idx_applicant (applicant_id),
|
||||
KEY idx_status (status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='物品领用/借用申请单';
|
||||
|
||||
|
||||
-- 3. 申请单明细表:支持部分、多次归还
|
||||
DROP TABLE IF EXISTS inv_requisition_item;
|
||||
CREATE TABLE inv_requisition_item (
|
||||
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
requisition_id BIGINT(20) UNSIGNED NOT NULL COMMENT '申请单ID',
|
||||
item_id BIGINT(20) UNSIGNED NOT NULL COMMENT '物品ID',
|
||||
|
||||
item_name_snap VARCHAR(128) DEFAULT NULL COMMENT '物品名称快照',
|
||||
spec_model_snap VARCHAR(128) DEFAULT NULL COMMENT '规格型号快照',
|
||||
unit_snap VARCHAR(32) DEFAULT NULL COMMENT '单位快照',
|
||||
is_consumable_snap TINYINT(1) DEFAULT NULL COMMENT '是否耗材快照:1耗材 0非耗材',
|
||||
|
||||
request_qty INT(11) NOT NULL DEFAULT 1 COMMENT '申请数量',
|
||||
approved_qty INT(11) DEFAULT NULL COMMENT '实批数量(可与申请数量不同)',
|
||||
returned_qty INT(11) NOT NULL DEFAULT 0 COMMENT '已归还数量(累加)',
|
||||
|
||||
-- 对于非耗材:returned_qty >= approved_qty 时置为1
|
||||
is_finished TINYINT(1) NOT NULL DEFAULT 0 COMMENT '该物品是否已完成归还:0否 1是',
|
||||
|
||||
remark VARCHAR(255) DEFAULT NULL COMMENT '行备注',
|
||||
created_at INT(11) NOT NULL DEFAULT 0 COMMENT '创建时间时间戳',
|
||||
updated_at INT(11) NOT NULL DEFAULT 0 COMMENT '更新时间时间戳',
|
||||
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_req (requisition_id),
|
||||
KEY idx_item (item_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='物品领用申请明细表';
|
||||
|
||||
|
||||
-- 4. 审批记录表(同上一版,略微无改动)
|
||||
DROP TABLE IF EXISTS inv_requisition_approve;
|
||||
CREATE TABLE inv_requisition_approve (
|
||||
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
requisition_id BIGINT(20) UNSIGNED NOT NULL COMMENT '申请单ID',
|
||||
node_type VARCHAR(32) NOT NULL COMMENT '审批节点类型:leader=直属上级,manager=物品管理员,other=其他',
|
||||
approver_id BIGINT(20) UNSIGNED NOT NULL COMMENT '审批人用户ID',
|
||||
approver_name VARCHAR(64) DEFAULT NULL COMMENT '审批人姓名快照',
|
||||
action TINYINT(3) NOT NULL COMMENT '审批动作:1通过 2驳回',
|
||||
comment VARCHAR(255) DEFAULT NULL COMMENT '审批意见',
|
||||
action_at INT(11) NOT NULL DEFAULT 0 COMMENT '审批时间时间戳',
|
||||
|
||||
created_at INT(11) NOT NULL DEFAULT 0 COMMENT '创建时间时间戳',
|
||||
updated_at INT(11) NOT NULL DEFAULT 0 COMMENT '更新时间时间戳',
|
||||
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_req (requisition_id),
|
||||
KEY idx_node (node_type),
|
||||
KEY idx_approver (approver_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='物品领用申请审批记录表';
|
||||
|
||||
|
||||
-- 5. 申请单附件表(申请时上传)
|
||||
DROP TABLE IF EXISTS inv_requisition_attachment;
|
||||
CREATE TABLE inv_requisition_attachment (
|
||||
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
requisition_id BIGINT(20) UNSIGNED NOT NULL COMMENT '申请单ID',
|
||||
file_name VARCHAR(255) NOT NULL COMMENT '文件原始名称',
|
||||
file_path VARCHAR(255) NOT NULL COMMENT '文件存储路径/URL',
|
||||
file_size INT(11) DEFAULT NULL COMMENT '文件大小(字节)',
|
||||
created_at INT(11) NOT NULL DEFAULT 0 COMMENT '创建时间时间戳',
|
||||
updated_at INT(11) NOT NULL DEFAULT 0 COMMENT '更新时间时间戳',
|
||||
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_req (requisition_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='物品领用申请附件表';
|
||||
|
||||
|
||||
/* ================= 归 还 相 关 表 ================= */
|
||||
|
||||
-- 6. 归还记录表:一次归还动作(可归还一个明细的一部分)
|
||||
DROP TABLE IF EXISTS inv_requisition_item_return;
|
||||
CREATE TABLE inv_requisition_item_return (
|
||||
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
requisition_id BIGINT(20) UNSIGNED NOT NULL COMMENT '申请单ID',
|
||||
requisition_item_id BIGINT(20) UNSIGNED NOT NULL COMMENT '申请单明细ID',
|
||||
item_id BIGINT(20) UNSIGNED NOT NULL COMMENT '物品ID',
|
||||
|
||||
return_qty INT(11) NOT NULL DEFAULT 0 COMMENT '本次归还数量',
|
||||
handler_id BIGINT(20) UNSIGNED NOT NULL COMMENT '办理归还的人(通常是物品管理员)',
|
||||
handler_name VARCHAR(64) DEFAULT NULL COMMENT '办理人姓名快照',
|
||||
|
||||
return_status TINYINT(3) NOT NULL DEFAULT 1 COMMENT '归还状态:1正常归还 2损坏 3其他',
|
||||
remark VARCHAR(255) DEFAULT NULL COMMENT '归还备注',
|
||||
|
||||
return_at INT(11) NOT NULL DEFAULT 0 COMMENT '归还时间时间戳',
|
||||
created_at INT(11) NOT NULL DEFAULT 0 COMMENT '创建时间时间戳',
|
||||
updated_at INT(11) NOT NULL DEFAULT 0 COMMENT '更新时间时间戳',
|
||||
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_req (requisition_id),
|
||||
KEY idx_req_item (requisition_item_id),
|
||||
KEY idx_item (item_id),
|
||||
KEY idx_handler (handler_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='物品归还记录表(支持多次、部分归还)';
|
||||
|
||||
|
||||
-- 7. 归还附件表:归还时拍照留底
|
||||
DROP TABLE IF EXISTS inv_requisition_item_return_attachment;
|
||||
CREATE TABLE inv_requisition_item_return_attachment (
|
||||
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
return_id BIGINT(20) UNSIGNED NOT NULL COMMENT '归还记录ID',
|
||||
file_name VARCHAR(255) NOT NULL COMMENT '文件原始名称',
|
||||
file_path VARCHAR(255) NOT NULL COMMENT '文件存储路径/URL(通常为照片)',
|
||||
file_size INT(11) DEFAULT NULL COMMENT '文件大小(字节)',
|
||||
created_at INT(11) NOT NULL DEFAULT 0 COMMENT '创建时间时间戳',
|
||||
updated_at INT(11) NOT NULL DEFAULT 0 COMMENT '更新时间时间戳',
|
||||
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_return (return_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='物品归还记录附件表(归还拍照留底)';
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
Reference in New Issue
Block a user