6 Commits

Author SHA1 Message Date
18338781771 4ec46704b2 imagick 2026-01-15 19:09:35 +08:00
18338781771 80e1a170e5 上传 2026-01-15 18:44:07 +08:00
18338781771 f2196809ea 图片上传 2026-01-15 18:04:39 +08:00
18338781771 05675471ab 上传图片 2026-01-15 18:03:14 +08:00
18338781771 c77da51775 图片压缩 2026-01-15 17:55:51 +08:00
18338781771 3d51a841cf 上传 2026-01-15 17:30:11 +08:00
94 changed files with 4408 additions and 5545 deletions
@@ -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;
// }
}
+3 -3
View File
@@ -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());
}
}
}
+61 -23
View File
@@ -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;
}
}
+6 -96
View File
@@ -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());
}
}
}
+1 -8
View File
@@ -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;
// }
}
+25 -34
View File
@@ -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;
}
-17
View File
@@ -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;
}
+1 -1
View File
@@ -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'];
/**
* 不允许自动填充模型字段
-123
View File
@@ -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);
}
}
-108
View File
@@ -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);
}
}
-58
View File
@@ -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');
}
}
+1 -8
View File
@@ -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);
}
}
-49
View File
@@ -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 = [];
}
-49
View File
@@ -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 = [];
}
-49
View File
@@ -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 = [];
}
-14
View File
@@ -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上存储的对象名称
+1 -45
View File
@@ -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 -82
View File
@@ -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;
}
}
+124
View File
@@ -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 表里存的是 '&#xE53D;' 或 '&#xe53d;'
*/
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) {
// 从 '&#xE53D;' 提取 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);
}
}
}
+8 -8
View File
@@ -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>
+2 -2
View File
@@ -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>
@@ -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' => [
]
];
}
@@ -4,7 +4,7 @@ namespace app\common\validate;
use think\Validate;
class WordpressVisitorValidate extends Validate
class ApprovalRecordsValidate extends Validate
{
// 规则
protected $rule = [
+35
View File
@@ -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']
];
}
+29
View File
@@ -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' => [
]
];
}
+176
View File
@@ -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' => [
]
];
}
+29
View File
@@ -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' => [
]
];
}
+209
View File
@@ -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';
// }
// }
// }
}
+29
View File
@@ -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' => [
]
];
}
+29
View File
@@ -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' => [
]
];
}
+29
View File
@@ -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' => [
]
];
}
+1 -1
View File
@@ -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');
-35
View File
@@ -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']);
}
}
});
}
}
}
+83
View File
@@ -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;
}
}
+51
View File
@@ -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);
}
}
+131
View File
@@ -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);
}
}
+222
View File
@@ -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);
}
}
+61
View File
@@ -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);
}
}
+95
View File
@@ -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);
}
}
+57
View File
@@ -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);
}
}
+51
View File
@@ -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);
}
}
-128
View File
@@ -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(),
]);
}
}
}
}
+68
View File
@@ -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);
}
}
+9 -8
View File
@@ -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')
// ]
// ]
];
-47
View File
@@ -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']);
});
});
-122
View File
@@ -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`;
-18
View File
@@ -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='二手车审核记录表';
-16
View File
@@ -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='二手车常见问题表';
+94
View File
@@ -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='审批讨论';
+174
View File
@@ -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;