diff --git a/.gitignore b/.gitignore
index 92bf9b0..224ad2d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,3 +12,5 @@
/windows.php
/bootstrap.php
/composer.lock
+.env_copy
+/public/excel
\ No newline at end of file
diff --git a/app/command/InquiryAssignInquiryCommand.php b/app/command/InquiryAssignInquiryCommand.php
new file mode 100644
index 0000000..adb8309
--- /dev/null
+++ b/app/command/InquiryAssignInquiryCommand.php
@@ -0,0 +1,161 @@
+ $v > 0
+ ));
+
+ if (empty($userIds)) {
+ throw new BusinessException('SALES_USER_IDS 业务员列表不能为空,请先在命令中配置.');
+ }
+
+ // 取出“待分配”的询盘(你可以根据自己系统调整条件)
+ // 当前策略:status = 0 且 recipient = 0 的视为未分配
+ $inquiries = Db::table('inquiry_log')
+ ->where('enter_user_id', 186)
+ ->where('channel', 10)
+ ->where('status', 0)
+ ->where('recipient', 0)
+ ->where('id', '>', 34426)
+ ->orderBy('id')
+ ->limit(static::BATCH_LIMIT)
+ ->get();
+
+ if ($inquiries->isEmpty()) {
+ $output->writeln('没有需要分配的询盘(status=0 且 recipient=0)');
+ return self::SUCCESS;
+ }
+
+ $now = time();
+ $count = 0;
+
+ foreach ($inquiries as $row) {
+ $inquiryId = (int)$row->id;
+ sleep(1);
+ // 轮询取出一个业务员 ID
+ $recipientUserId = $this->nextRecipientId($userIds, static::ROUND_ROBIN_GROUP_KEY);
+
+ // 对每一条记录单独事务,避免单条异常拖垮全部
+ Db::transaction(function () use ($inquiryId, $recipientUserId, $now, $output, &$count) {
+ // 1)更新 inquiry_log
+ $affected = Db::table('inquiry_log')
+ ->where('id', $inquiryId)
+ ->update([
+ 'recipient' => $recipientUserId,
+ 'status' => 2,
+ 'r_time' => $now,
+ 't_time' => $now,
+ 'updated_at' => $now,
+ ]);
+
+ if ($affected === 0) {
+ // 被其他进程抢先更新也有可能,这里只打印提示不中断整个批次
+ $output->writeln("询盘 {$inquiryId} 未更新(可能已被处理)");
+ return;
+ }
+
+ // 2)构造队列 payload(后续 CRM 消费时至少知道 ID)
+ $res = [
+ 'id' => $inquiryId,
+ 'recipient' => $recipientUserId,
+ 'dispatched_at' => $now,
+ 'source' => 'fb_leads_inquiry',
+ ];
+
+ // 3)推送到 CRM 队列
+ RedisQueueRedis::send('inquiry-to-crm-new', $res);
+
+ $count++;
+ $output->writeln("已分配询盘 {$inquiryId} 给业务员 {$recipientUserId},并推送到队列");
+ });
+ }
+
+ $output->writeln("本次共分配成功 {$count} 条询盘");
+
+ return self::SUCCESS;
+ }
+
+ /**
+ * 轮询获取“下一位业务员 ID”
+ *
+ * @param int[] $userIds
+ * @param string $groupKey
+ * @return int
+ * @throws BusinessException
+ */
+ protected function nextRecipientId(array $userIds, string $groupKey = 'default'): int
+ {
+ $userIds = array_values(array_filter(array_map('intval', $userIds), fn($v) => $v > 0));
+ if (empty($userIds)) {
+ throw new BusinessException('轮询分配失败:业务员列表为空');
+ }
+
+ $count = count($userIds);
+ $key = 'inquiry:assign:round_robin:' . $groupKey;
+
+ // Redis INCR 是原子操作,适合并发下使用
+ $val = Redis::incr($key); // 第一次会从 0 变成 1
+
+ // 换算为从 0 开始的下标
+ $index = ($val - 1) % $count;
+
+ return $userIds[$index];
+ }
+}
diff --git a/app/command/InquiryFbLeadCommand.php b/app/command/InquiryFbLeadCommand.php
new file mode 100644
index 0000000..aec2199
--- /dev/null
+++ b/app/command/InquiryFbLeadCommand.php
@@ -0,0 +1,164 @@
+where('sync_status', 0)
+ ->get();
+
+ if ($leads->isEmpty()) {
+ $output->writeln('没有需要同步的 fb_leads(sync_status = 0)');
+ return self::SUCCESS;
+ }
+
+ $now = time();
+
+ Db::transaction(function () use ($leads, $now, $output) {
+ $insert = [];
+ $ids = [];
+
+ foreach ($leads as $lead) {
+ $leadArr = (array)$lead;
+ $insert[] = $this->mapLeadToInquiry($leadArr, $now);
+ // 假设 fb_leads 的主键字段名为 id
+ $ids[] = $leadArr['id'];
+ }
+
+ if ($insert) {
+ // 1) 写入 inquiry_log
+ Db::table('inquiry_log')->insert($insert);
+
+ // 2) 回写 fb_leads 的同步状态
+ Db::table('fb_leads')
+ ->whereIn('id', $ids)
+ ->update([
+ 'sync_status' => 1,
+ 'updated_at' => $now,
+ ]);
+
+ $output->writeln('已导入 ' . count($insert) . ' 条数据到 inquiry_log,并更新 fb_leads.sync_status = 1');
+ }
+ });
+
+ return self::SUCCESS;
+ }
+
+ /**
+ * 将一条 fb_leads 记录映射为 inquiry_log 记录
+ */
+ protected function mapLeadToInquiry(array $lead, int $now): array
+ {
+ // 联系方式
+ $email = (string)($lead['email'] ?? '');
+ $tel = (string)($lead['phone'] ?? '');
+ $wa = $tel; // 先用电话号码当 WhatsApp
+
+ // 根据电话或 WhatsApp 区号识别国家
+ $countryId = PhoneCountryHelper::detectCountryId($tel ?: $wa);
+
+ // 所有默认值按你的要求设置
+ $data = [
+ 'order_no' => $this->orderNum(),
+ 'enter_user_id' => static::DEFAULT_ENTER_USER_ID,
+ 'recipient' => 0,
+ 'quality' => 2,
+ 'channel' => static::DEFAULT_CHANNEL,
+
+ 'email' => $email,
+ 'tel' => $tel,
+ 'whatsapp' => $wa,
+ 'country' => $countryId ?: 0,
+ 'contacts' => (string)($lead['full_name'] ?? ''),
+
+ 'content' => $this->buildContentHtml($lead),
+
+ 'status' => 0,
+ 'r_time' => 0,
+ 't_time' => 0,
+ 'sync_crm' => 0,
+
+ 'created_at' => $now,
+ 'updated_at' => $now,
+ ];
+
+ return $data;
+ }
+
+ /**
+ * 生成订单号
+ */
+ protected function orderNum(): string
+ {
+ return 'SN' . date('Ymd') . str_pad(mt_rand(1, 99999), 5, '0', STR_PAD_LEFT);
+ }
+
+ /**
+ * 根据 fb_leads 的字段拼出富文本内容
+ */
+ protected function buildContentHtml(array $lead): string
+ {
+ $e = function ($v) {
+ return htmlspecialchars((string)$v, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
+ };
+
+ $lines = [];
+
+ if (!empty($lead['car_model'])) {
+ $lines[] = '
意向车型:' . $e($lead['car_model']) . '
';
+ }
+ if (!empty($lead['car_quantity']) || !empty($lead['car_quantity_text'])) {
+ $qty = $lead['car_quantity'] ?? $lead['car_quantity_text'] ?? '';
+ $lines[] = '意向数量:' . $e($qty) . '
';
+ }
+ if (!empty($lead['favorite_color'])) {
+ $lines[] = '意向颜色:' . $e($lead['favorite_color']) . '
';
+ }
+ if (!empty($lead['usage_type'])) {
+ $lines[] = '用途:' . $e($lead['usage_type']) . '
';
+ }
+ if (!empty($lead['seat_preference'])) {
+ $lines[] = '座位数偏好:' . $e($lead['seat_preference']) . '
';
+ }
+
+ $lines[] = '
';
+ $lines[] = '广告与表单信息:
';
+ $lines[] = '平台:' . $e($lead['platform'] ?? '') . '
';
+ $lines[] = 'Campaign:' . $e($lead['campaign_name'] ?? '') . ' (#' . $e($lead['campaign_id'] ?? '') . ')
';
+ $lines[] = 'Adset:' . $e($lead['adset_name'] ?? '') . ' (#' . $e($lead['adset_id'] ?? '') . ')
';
+ $lines[] = 'Ad:' . $e($lead['ad_name'] ?? '') . ' (#' . $e($lead['ad_id'] ?? '') . ')
';
+ $lines[] = 'Form:' . $e($lead['form_name'] ?? '') . ' (#' . $e($lead['form_id'] ?? '') . ')
';
+
+ $lines[] = '
';
+ $lines[] = 'Meta 信息:
';
+ $lines[] = 'Lead ID:' . $e($lead['fb_lead_id'] ?? '') . '
';
+ $lines[] = 'FB 创建时间原始值:' . $e($lead['fb_created_time'] ?? '') . '
';
+
+ if (!empty($lead['inbox_url'])) {
+ $url = $e($lead['inbox_url']);
+ $lines[] = 'Inbox 链接:' . $url . '
';
+ }
+
+ return implode("\n", $lines);
+ }
+}
diff --git a/app/command/InquiryImportFbCommand.php b/app/command/InquiryImportFbCommand.php
new file mode 100644
index 0000000..3c9a3c0
--- /dev/null
+++ b/app/command/InquiryImportFbCommand.php
@@ -0,0 +1,256 @@
+addOption(
+ 'file',
+ null,
+ InputOption::VALUE_OPTIONAL,
+ '要导入的单个 Excel 文件名(相对 public/excel 的路径,或绝对路径)'
+ )
+ ->addOption(
+ 'dir',
+ null,
+ InputOption::VALUE_OPTIONAL,
+ 'Excel 目录(相对 public/ 的路径)',
+ 'excel' // 默认就是 public/excel
+ );
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $fileOption = $input->getOption('file');
+ $dirOption = $input->getOption('dir') ?? 'excel';
+
+ // 基准目录:public/{$dirOption}
+ $baseDir = rtrim(public_path($dirOption), DIRECTORY_SEPARATOR);
+
+ if ($fileOption) {
+ // 如果传的是绝对路径,直接用;否则认为是相对 baseDir 的文件名
+ if ($this->isAbsolutePath($fileOption)) {
+ $filePath = $fileOption;
+ } else {
+ $filePath = $baseDir . DIRECTORY_SEPARATOR . $fileOption;
+ }
+
+ $output->writeln("开始导入单个文件: {$filePath}");
+
+ $this->importOneFile($filePath, $output);
+ } else {
+ // 不指定 file 时,默认导入目录下所有 .xlsx
+ if (!is_dir($baseDir)) {
+ throw new BusinessException("目录不存在:{$baseDir}");
+ }
+
+ $pattern = $baseDir . DIRECTORY_SEPARATOR . '*.xlsx';
+ $files = glob($pattern);
+
+ if (empty($files)) {
+ $output->writeln("目录 {$baseDir} 下没有找到 .xlsx 文件");
+ return self::SUCCESS;
+ }
+
+ $output->writeln("开始批量导入目录: {$baseDir}");
+ foreach ($files as $filePath) {
+ $output->writeln(" -> 文件:{$filePath}");
+ try {
+ $this->importOneFile($filePath, $output);
+ $output->writeln(" 导入成功");
+ } catch (\Throwable $e) {
+ // 某个文件失败,不影响后面的文件
+ $output->writeln(" 导入失败:{$e->getMessage()}");
+ }
+ }
+ }
+
+ return self::SUCCESS;
+ }
+
+ /**
+ * 导入单个 Excel 文件
+ */
+ protected function importOneFile(string $filePath, OutputInterface $output): void
+ {
+ if (!is_file($filePath)) {
+ throw new BusinessException("文件不存在:{$filePath}");
+ }
+
+ $spreadsheet = IOFactory::load($filePath);
+ $sheet = $spreadsheet->getActiveSheet();
+ $rows = $sheet->toArray(null, true, true, true);
+
+ if (empty($rows)) {
+ throw new BusinessException('Excel 内容为空:' . $filePath);
+ }
+
+ // 取第一行表头
+ $headerRow = array_shift($rows);
+
+ // 表头映射器
+ $mapper = new FbLeadHeaderMapper();
+
+ // 如果有未知表头,这里会直接抛异常中断当前文件
+ $colToField = $mapper->buildColumnFieldMap($headerRow);
+
+ $insertData = [];
+ $now = time();
+
+ foreach ($rows as $row) {
+ $item = [];
+
+ foreach ($colToField as $colIndex => $field) {
+ $raw = $row[$colIndex] ?? null;
+
+ switch ($field) {
+ // 1) is_organic: Excel 里是 true/false,这里转成 0/1
+ case 'is_organic':
+ $item['is_organic'] = $this->normalizeIsOrganic($raw);
+ break;
+
+ // 2) fb_created_time: 统一转成 Y-m-d H:i:s
+ case 'fb_created_time':
+ $item['fb_created_time'] = $this->normalizeFbCreatedTime($raw);
+ break;
+
+ default:
+ if (is_string($raw)) {
+ $raw = trim($raw);
+ }
+ $item[$field] = $raw;
+ break;
+ }
+ }
+
+ // 补充公共字段
+ $item['source_file'] = basename($filePath);
+ $item['created_at'] = $now;
+ $item['updated_at'] = $now;
+
+ // 整行都空就跳过
+ $allNull = true;
+ foreach ($item as $v) {
+ if ($v !== null && $v !== '') {
+ $allNull = false;
+ break;
+ }
+ }
+ if ($allNull) {
+ continue;
+ }
+
+ $insertData[] = $item;
+ }
+
+ if ($insertData) {
+ Db::table('fb_leads')->insert($insertData);
+ } else {
+ $output->writeln(" 文件 {$filePath} 没有有效数据行");
+ }
+ }
+
+ /**
+ * 将 Excel 里的 is_organic 值转换为 0/1
+ */
+ protected function normalizeIsOrganic($value): int
+ {
+ if ($value === null) {
+ return 0;
+ }
+
+ $v = strtolower(trim((string)$value));
+
+ // 认为这些是 “true”
+ $trueValues = ['1', 'true', 'yes', 'y'];
+
+ return in_array($v, $trueValues, true) ? 1 : 0;
+ }
+
+ /**
+ * 统一处理 fb_created_time
+ * Excel 目前是类似 '12/8/25',这里尽量转成 'Y-m-d H:i:s'
+ * 解析失败时返回 null
+ */
+ protected function normalizeFbCreatedTime($value): ?string
+ {
+ if ($value === null || $value === '') {
+ return null;
+ }
+
+ if ($value instanceof \DateTimeInterface) {
+ return $value->format('Y-m-d H:i:s');
+ }
+
+ $v = trim((string)$value);
+
+ $formats = [
+ 'Y-m-d H:i:s',
+ 'Y-m-d',
+ 'n/j/y', // 12/8/25
+ 'n/j/Y',
+ 'd/m/Y',
+ 'd.m.Y',
+ ];
+
+ foreach ($formats as $fmt) {
+ $dt = \DateTime::createFromFormat($fmt, $v);
+ if ($dt !== false) {
+ return $dt->format('Y-m-d H:i:s');
+ }
+ }
+
+ // 纯数字当时间戳试一下
+ if (is_numeric($v) && strlen($v) >= 9 && strlen($v) <= 11) {
+ $ts = (int)$v;
+ if ($ts > 0) {
+ return date('Y-m-d H:i:s', $ts);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * 判断是否为绝对路径(简单兼容 Windows / Linux)
+ */
+ protected function isAbsolutePath(string $path): bool
+ {
+ // Windows: X:\ 或 \\server\share
+ if (preg_match('/^[a-zA-Z]:\\\\/', $path) || str_starts_with($path, '\\\\')) {
+ return true;
+ }
+
+ // *nix: / 开头
+ return str_starts_with($path, '/');
+ }
+}
+
+/*
+
+php webman tb-db
+# 等价于 php webman tb-db --dir=excel
+# 会遍历 public/excel/*.xlsx 逐个导入
+
+# 相对路径(相对 public/excel)
+php webman tb-db --file="BYD tang_Leads_2025-12-04_2025-12-09.xlsx"
+
+# 或给绝对路径
+php webman tb-db --file="/www/wwwroot/xxx/public/excel/BYD tang_Leads_2025-12-04_2025-12-09.xlsx"
+
+*/
\ No newline at end of file
diff --git a/app/command/VehMoneyCommand.php b/app/command/VehMoneyCommand.php
new file mode 100644
index 0000000..0485e09
--- /dev/null
+++ b/app/command/VehMoneyCommand.php
@@ -0,0 +1,29 @@
+service = new UsedcarCrawlResultsService();
+ }
+
+ /**
+ * 列表页数据格式化
+ *
+ * @param array $data
+ * @return array
+ */
+ // protected function indexFormat($data)
+ // {
+ // if (isset($data['created_at']))
+ // $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']);
+ // if (isset($data['updated_at']))
+ // $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']);
+ // return $data;
+ // }
+}
\ No newline at end of file
diff --git a/app/common/controller/UsedcarCrawlResultsImagesController.php b/app/common/controller/UsedcarCrawlResultsImagesController.php
new file mode 100644
index 0000000..5560fda
--- /dev/null
+++ b/app/common/controller/UsedcarCrawlResultsImagesController.php
@@ -0,0 +1,33 @@
+service = new UsedcarCrawlResultsImagesService();
+ }
+
+ /**
+ * 列表页数据格式化
+ *
+ * @param array $data
+ * @return array
+ */
+ // protected function indexFormat($data)
+ // {
+ // if (isset($data['created_at']))
+ // $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']);
+ // if (isset($data['updated_at']))
+ // $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']);
+ // return $data;
+ // }
+}
\ No newline at end of file
diff --git a/app/common/logic/UsedcarCrawlResultsImagesLogic.php b/app/common/logic/UsedcarCrawlResultsImagesLogic.php
new file mode 100644
index 0000000..428bc16
--- /dev/null
+++ b/app/common/logic/UsedcarCrawlResultsImagesLogic.php
@@ -0,0 +1,17 @@
+validator;
+ }
+}
\ No newline at end of file
diff --git a/app/common/service/UsedcarCrawlResultsService.php b/app/common/service/UsedcarCrawlResultsService.php
new file mode 100644
index 0000000..5aa1cdd
--- /dev/null
+++ b/app/common/service/UsedcarCrawlResultsService.php
@@ -0,0 +1,31 @@
+validator;
+ }
+}
\ No newline at end of file
diff --git a/app/common/utils/crawl/service/UsedCarCrawl.php b/app/common/utils/crawl/service/UsedCarCrawl.php
new file mode 100644
index 0000000..7e99090
--- /dev/null
+++ b/app/common/utils/crawl/service/UsedCarCrawl.php
@@ -0,0 +1,857 @@
+ '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
+ * @throws BusinessException
+ */
+ public function popUrl(): ?array
+ {
+ $data = Redis::rPop(self::REDIS_QUEUE_KEY);
+ if (!$data) {
+ return null;
+ }
+
+ $task = json_decode($data, true);
+ if (json_last_error() !== JSON_ERROR_NONE || !is_array($task)) {
+ throw new BusinessException('任务数据解析失败:' . json_last_error_msg());
+ }
+
+ // 基本字段校验
+ if (empty($task['url']) || empty($task['scene'])) {
+ throw new BusinessException('队列任务数据缺少必要字段:url 或 scene');
+ }
+
+ return $task;
+ }
+
+ /**
+ * 执行抓取任务
+ *
+ * @return bool
+ * @throws BusinessException
+ */
+ public function execute(): bool
+ {
+ try {
+ $task = $this->popUrl();
+
+ if (!$task) {
+ // 队列为空,任务结束
+ return false;
+ }
+
+ $url = (string)$task['url'];
+ $scene = (string)$task['scene'];
+ $id = (int)($task['id'] ?? 0);
+
+ $html = $this->fetchHtml($url);
+ if ($html === '') {
+ throw new BusinessException("页面内容为空: {$url}");
+ }
+
+ switch ($scene) {
+ case 'usedcar':
+ $info = $this->parseUsedCarDetail($html, $url);
+ $insertId = $this->saveUsedCarBasicInfo($info);
+
+ // 下载图片 + 上传 + 入库(图片失败不影响主记录)
+ $imagesArray = json_decode($info['images'] ?? '[]', true);
+ if (json_last_error() !== JSON_ERROR_NONE) {
+ $imagesArray = [];
+ }
+ $saveDir = public_path('uploads/usedcar/' . $insertId);
+ $this->uploadAndRemove($imagesArray, $saveDir, $info['used_car_id'], $insertId);
+
+ // 继续派发配置页任务
+ if (!empty($info['car_id_url'])) {
+ $this->pushUrl($info['car_id_url'], 'config', $insertId);
+ }
+ break;
+
+ case 'config':
+ if ($id <= 0) {
+ throw new BusinessException("配置任务缺少主记录ID");
+ }
+
+ $data = $this->parseConfigDetail($html);
+ $this->saveUsedCarConfigInfo($data, $id);
+ break;
+
+ default:
+ throw new BusinessException("未知的任务场景: " . $scene);
+ }
+
+ return true;
+ } catch (GuzzleException $e) {
+ throw new BusinessException("网络请求失败: " . $e->getMessage());
+ } catch (BusinessException $e) {
+ // 业务异常原样抛出,方便上层知道是逻辑问题
+ throw $e;
+ } catch (Throwable $th) {
+ // 兜底异常,避免 worker 直接挂掉
+ 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,
+ 'timeout' => 10,
+ ]);
+
+ return (string)$response->getBody();
+ }
+
+ /**
+ * 解析二手车详情页信息
+ *
+ * @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());
+ $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'] ?? '';
+ $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'] ?? '';
+ $seriesName = $carInfo['series_name'] ?? '';
+ $carName = $carInfo['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 && 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
+ * @throws BusinessException
+ */
+ public function parseConfigDetail(string $html = ''): array
+ {
+ try {
+ $ql = QueryList::html($html);
+ } catch (Throwable $e) {
+ throw new BusinessException('解析 HTML 失败(配置页): ' . $e->getMessage());
+ }
+
+ $jsonText = $ql->find('#__NEXT_DATA__')->text();
+ if ($jsonText === '') {
+ throw new BusinessException('配置页未找到 __NEXT_DATA__ JSON');
+ }
+
+ $raw = $this->decodeJson($jsonText, '配置页 __NEXT_DATA__');
+
+ $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']) || !is_array($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
+ {
+ if (is_string($data)) {
+ $data = json_decode($data, true);
+ if (json_last_error() !== JSON_ERROR_NONE) {
+ return 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 (($item['key'] ?? null) === $field) {
+ return $item['value'] ?? null;
+ }
+
+ if (!empty($item['children']) && is_array($item['children'])) {
+ foreach ($item['children'] as $child) {
+ if (($child['key'] ?? null) === $field) {
+ return $child['value'] ?? null;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * 从参数列表中获取指定名称的值
+ */
+ public function getParamValue(array $params, string $name): ?string
+ {
+ foreach ($params as $item) {
+ if (($item['name'] ?? null) === $name) {
+ return $item['value'] ?? null;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * 判断车辆能源类型
+ */
+ 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;
+ };
+
+ foreach ($newEnergyKeywords as $kw) {
+ if ($contains($text, $kw)) {
+ return self::ENERGY_TYPE_RENEWABLE;
+ }
+ }
+
+ foreach ($hybridKeywords as $kw) {
+ if ($contains($text, $kw)) {
+ return self::ENERGY_TYPE_CONVENTIONAL;
+ }
+ }
+
+ foreach ($traditionalKeywords as $kw) {
+ if ($contains($text, $kw)) {
+ return self::ENERGY_TYPE_CONVENTIONAL;
+ }
+ }
+
+ return self::ENERGY_TYPE_UNKNOWN;
+ }
+
+ /**
+ * 保存二手车基本信息到数据库
+ *
+ * @param array $data 数据
+ * @return int 插入记录ID
+ * @throws BusinessException
+ */
+ public function saveUsedCarBasicInfo(array $data): int
+ {
+ try {
+ $time = time();
+ $data['created_at'] = $data['created_at'] ?? $time;
+ $data['updated_at'] = $time;
+
+ return (int)Db::table('usedcar_crawl_results')->insertGetId($data);
+ } catch (Throwable $e) {
+ throw new BusinessException('保存二手车基础信息失败:' . $e->getMessage());
+ }
+ }
+
+ /**
+ * 更新配置信息到数据库
+ *
+ * @param array $data 配置数据
+ * @param int $id 记录ID
+ * @return bool|int
+ * @throws BusinessException
+ */
+ public function saveUsedCarConfigInfo(array $data, int $id)
+ {
+ try {
+ $record = Db::table('usedcar_crawl_results')->find($id);
+
+ if (!$record) {
+ // 找不到记录就算了,不抛致命错误,视业务情况也可以抛异常
+ return false;
+ }
+
+ $groups = $data['groups'] ?? [];
+ $oldFormat = [];
+
+ if (!empty($record->format_car_info)) {
+ $decoded = json_decode($record->format_car_info, true);
+ if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
+ $oldFormat = $decoded;
+ }
+ }
+
+ $formatCarInfo = array_merge(
+ $oldFormat,
+ $data['format_car_info'] ?? []
+ );
+
+ 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(),
+ ]);
+ } catch (Throwable $e) {
+ throw new BusinessException('保存车辆配置信息失败:' . $e->getMessage());
+ }
+ }
+
+ /**
+ * 构建值映射表
+ */
+ 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;
+ }
+
+ /**
+ * 构建车辆规格分组
+ */
+ 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 = '';
+ 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);
+ }
+
+ /**
+ * 解码懂车帝价格
+ */
+ 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 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;
+
+ $defaultHeaders = [
+ 'User-Agent: ' . self::DEFAULT_HEADERS['User-Agent'],
+ 'Accept: image/avif,image/webp,image/apng,image/*,*/*;q=0.8',
+ 'Referer: ' . self::DEFAULT_HEADERS['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 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((string)$url);
+ if ($url === '') {
+ continue;
+ }
+
+ if (strpos($url, '//') === 0) {
+ $url = 'https:' . $url;
+ }
+
+ try {
+ $path = $this->downloadImage($url, $saveDir);
+ } catch (Throwable $e) {
+ $path = null;
+ }
+
+ $result[$url] = $path;
+ }
+
+ return $result;
+ }
+
+ /**
+ * 下载 -> 上传 -> 删除本地
+ *
+ * @param array $images 原始相册URL列表
+ * @param string $path 本地保存目录
+ * @param int $usedCarId 懂车帝 used_car_id
+ * @param int $id 本地 crawl 记录id
+ * @return bool
+ */
+ public function uploadAndRemove(array $images, string $path, $usedCarId, $id): bool
+ {
+ if (empty($images)) {
+ return false;
+ }
+
+ // 下载图片
+ $filePatch = $this->downloadImageAlbum($images, $path);
+ if (empty($filePatch)) {
+ return false;
+ }
+
+ $uploadService = new UploadService();
+
+ 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'],
+ ]);
+ } catch (Throwable $e) {
+ // 单张图失败不影响其他
+ // 可以按需写日志
+ } finally {
+ // 删除本地文件
+ if (is_file($filePath)) {
+ @unlink($filePath);
+ }
+ }
+ }
+
+ // 删除目录
+ rmdir($path);
+
+ return true;
+ }
+
+ /**
+ * 安全解 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;
+ }
+}
\ No newline at end of file
diff --git a/app/common/utils/crawl/service/UsedCarCrawl_bak.php b/app/common/utils/crawl/service/UsedCarCrawl_bak.php
new file mode 100644
index 0000000..11ea8c8
--- /dev/null
+++ b/app/common/utils/crawl/service/UsedCarCrawl_bak.php
@@ -0,0 +1,681 @@
+ $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;
+ }
+}
diff --git a/app/common/utils/crawl/service/UsedCarCrawl_bak2.php b/app/common/utils/crawl/service/UsedCarCrawl_bak2.php
new file mode 100644
index 0000000..2612baa
--- /dev/null
+++ b/app/common/utils/crawl/service/UsedCarCrawl_bak2.php
@@ -0,0 +1,798 @@
+ '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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/common/validate/UsedcarCrawlResultsImagesValidate.php b/app/common/validate/UsedcarCrawlResultsImagesValidate.php
new file mode 100644
index 0000000..e6cbbd5
--- /dev/null
+++ b/app/common/validate/UsedcarCrawlResultsImagesValidate.php
@@ -0,0 +1,29 @@
+ [
+
+ ],
+
+ 'update' => [
+
+ ]
+ ];
+}
\ No newline at end of file
diff --git a/app/common/validate/UsedcarCrawlResultsValidate.php b/app/common/validate/UsedcarCrawlResultsValidate.php
new file mode 100644
index 0000000..f0766bd
--- /dev/null
+++ b/app/common/validate/UsedcarCrawlResultsValidate.php
@@ -0,0 +1,29 @@
+ [
+
+ ],
+
+ 'update' => [
+
+ ]
+ ];
+}
\ No newline at end of file
diff --git a/app/controller/IndexController.php b/app/controller/IndexController.php
index c3c75ae..4b4fb43 100644
--- a/app/controller/IndexController.php
+++ b/app/controller/IndexController.php
@@ -74,25 +74,25 @@ class IndexController
$options = [
'controller' => [
'module' => 'common',
- 'name' => 'finance_recon_dubai_files'
+ 'name' => 'usedcar_crawl_results_images'
],
'model' => [
'module' => 'common',
- 'name' => 'finance_recon_dubai_files'
+ 'name' => 'usedcar_crawl_results_images'
],
'service' => [
'module' => 'common',
- 'name' => 'finance_recon_dubai_files'
+ 'name' => 'usedcar_crawl_results_images'
],
'logic' => [
'module' => 'common',
- 'name' => 'finance_recon_dubai_files'
+ 'name' => 'usedcar_crawl_results_images'
],
'validate' => [
'module' => 'common',
- 'name' => 'finance_recon_dubai_files'
+ 'name' => 'usedcar_crawl_results_images'
],
- 'table' => 'finance_recon_dubai_files'
+ 'table' => 'usedcar_crawl_results_images'
];
$result = (new Generate($options))->doGenerate();
@@ -159,11 +159,16 @@ class IndexController
public function test(Request $request)
{
- $tmp = (new \app\common\service\OssService)->generatePrivateUrl('fdesk_test/20251104/20251104_财务.xlsx');;
-
- $service = new \app\common\service\FinanceRecoDubaiFilesService();
- $result = $service->import($tmp, true);
+ $service = new \app\common\utils\crawl\service\UsedCarCrawl();
- return json(['code' => 0, 'msg' => 'ok', 'data' => $result]);
+ $result = $service->pushUrl('https://www.dongchedi.com/usedcar/21631642');
+
+ // $result = $service->doData();
+
+ // $result = $service->decodeDongchediPrice('.');
+
+ // $result = $service->detectEnergyType('油电混合');
+
+ return json($result);
}
}
\ No newline at end of file
diff --git a/app/inquiry/utils/FbLeadHeaderMapper.php b/app/inquiry/utils/FbLeadHeaderMapper.php
new file mode 100644
index 0000000..ed048ed
--- /dev/null
+++ b/app/inquiry/utils/FbLeadHeaderMapper.php
@@ -0,0 +1,154 @@
+ DB 字段名
+ * 注意:key 用的是“原始表头字符串”(包括下划线)
+ */
+ protected array $headerMap = [
+ // ===== 基础公共字段 =====
+ 'id' => 'fb_lead_id',
+ 'created_time' => 'fb_created_time',
+ 'ad_id' => 'ad_id',
+ 'ad_name' => 'ad_name',
+ 'adset_id' => 'adset_id',
+ 'adset_name' => 'adset_name',
+ 'campaign_id' => 'campaign_id',
+ 'campaign_name' => 'campaign_name',
+ 'form_id' => 'form_id',
+ 'form_name' => 'form_name',
+ 'is_organic' => 'is_organic',
+ 'platform' => 'platform',
+
+ 'full_name' => 'full_name',
+ 'phone' => 'phone',
+ 'phone_number' => 'phone', // 兼容写法
+ 'email' => 'email',
+ 'lead_status' => 'lead_status',
+ 'inbox_url' => 'inbox_url',
+
+ // ===== 车型相关:全部归到 car_model =====
+ 'which_model_of_this_car_would_you_like_to_purchase?_' => 'car_model',
+ 'which_model_of_this_car_would_you_like_to_purchase?' => 'car_model',
+ 'which_car_model_would_you_like_to_purchase?' => 'car_model',
+ 'which_model_do_you_want_to_purchase?' => 'car_model',
+ 'which_model_would_you_like_to_purchase?' => 'car_model',
+ 'which_version_do_you_want_to_buy?' => 'car_model', // 这里也归到车型/版本字段
+
+ // ===== 数量相关:全部归到 car_quantity =====
+ 'how_many_cars_would_you_like_to_buy?_' => 'car_quantity',
+ 'how_many_cars_would_you_like_to_buy?' => 'car_quantity',
+ '_how_many_cars_would_you_like_to_buy?' => 'car_quantity',
+ 'how_many_cars_do_you_buy?' => 'car_quantity',
+ 'how_many_cars_do_you_want_to_purchase?' => 'car_quantity',
+ 'how_many_cars_do_you_want_to_buy?' => 'car_quantity',
+ 'what_quantity_do_you_want_to_buy?' => 'car_quantity',
+
+ // ===== 颜色相关:归到 favorite_color =====
+ 'what_color_do_you_like?' => 'favorite_color',
+ 'which_color_would_you_like_to_purchase?' => 'favorite_color',
+
+ // ===== 用途相关:归到 usage_type =====
+ 'are_you_using_it_for_business_or_personal_use?' => 'usage_type',
+ 'business_use_or_personal_use?' => 'usage_type',
+ 'personal_use_or_business_use?' => 'usage_type',
+
+ // ===== 座位数相关 =====
+ 'do_you_want_to_buy_six_seats_or_seven_seats?' => 'seat_preference',
+ ];
+
+ /**
+ * 返回所有支持的 Excel 表头
+ */
+ public function getSupportedHeaders(): array
+ {
+ return array_keys($this->headerMap);
+ }
+
+ /**
+ * 单个表头映射:原始表头 -> DB 字段名
+ */
+ public function mapHeader(string $rawHeader): ?string
+ {
+ $key = $this->normalizeHeader($rawHeader);
+
+ return $this->headerMap[$key] ?? null;
+ }
+
+ /**
+ * 校验一整行表头:
+ * - 有任何不认识的表头,就抛 BusinessException
+ */
+ public function validateHeaders(array $rawHeaders): void
+ {
+ $unsupported = [];
+
+ foreach ($rawHeaders as $header) {
+ $header = (string)$header;
+ $key = $this->normalizeHeader($header);
+
+ // 允许空表头(多余空列)
+ if ($key === '') {
+ continue;
+ }
+
+ if (!isset($this->headerMap[$key])) {
+ $unsupported[] = $header;
+ }
+ }
+
+ if (!empty($unsupported)) {
+ $msg = 'Excel 表头包含未兼容字段:' . implode('、', $unsupported)
+ . '。请检查这些字段是否需要加入映射配置。';
+ throw new BusinessException($msg);
+ }
+ }
+
+ /**
+ * 根据第一行表头,构建 “列索引 => DB 字段” 映射
+ */
+ public function buildColumnFieldMap(array $rawHeaders): array
+ {
+ // 先做一遍校验(发现问题直接抛异常)
+ $this->validateHeaders($rawHeaders);
+
+ $colToField = [];
+
+ foreach ($rawHeaders as $index => $header) {
+ $key = $this->normalizeHeader((string)$header);
+ if ($key === '') {
+ continue;
+ }
+
+ $field = $this->headerMap[$key] ?? null;
+ if ($field === null) {
+ continue; // 理论上不会到这儿,双保险
+ }
+
+ $colToField[$index] = $field;
+ }
+
+ return $colToField;
+ }
+
+ /**
+ * 表头标准化:
+ * 目前只去 BOM + trim,保持和 headerMap 中 key 完全一致
+ * 如果后面再出现“有空格/大小写”的版本,我们可以在这里做 strtolower / 替换空格 为 _
+ */
+ protected function normalizeHeader(string $header): string
+ {
+ // 去掉 UTF-8 BOM
+ $header = preg_replace('/^\xEF\xBB\xBF/', '', $header);
+
+ // 去掉前后空白
+ $header = trim($header);
+
+ return $header;
+ }
+}
diff --git a/app/inquiry/utils/PhoneCountryHelper.php b/app/inquiry/utils/PhoneCountryHelper.php
new file mode 100644
index 0000000..e6b0a48
--- /dev/null
+++ b/app/inquiry/utils/PhoneCountryHelper.php
@@ -0,0 +1,58 @@
+ +
+ if (str_starts_with($phone, '00')) {
+ $phone = '+' . substr($phone, 2);
+ } elseif ($phone[0] !== '+') {
+ // 没有 +,默认补一个(大多数线索都是国际号)
+ $phone = '+' . $phone;
+ }
+
+ $digits = ltrim($phone, '+');
+ if ($digits === '') {
+ return null;
+ }
+
+ // 3. 国家区号最长 3 位,从长到短匹配
+ for ($len = 3; $len >= 1; $len--) {
+ $code = substr($digits, 0, $len);
+ if ($code === '') {
+ continue;
+ }
+
+ $country = Db::table('country')
+ ->where('phone_code', $code)
+ ->first();
+
+ if ($country) {
+ return (int)$country->id;
+ }
+ }
+
+ // 没找到
+ return null;
+ }
+}
diff --git a/app/inquiry/utils/RoundRobinAssigner.php b/app/inquiry/utils/RoundRobinAssigner.php
new file mode 100644
index 0000000..dc0ac77
--- /dev/null
+++ b/app/inquiry/utils/RoundRobinAssigner.php
@@ -0,0 +1,46 @@
+ $v > 0));
+ if (empty($userIds)) {
+ throw new BusinessException('轮询分配业务员失败:业务员列表不能为空');
+ }
+
+ $count = count($userIds);
+
+ // Redis key:同一个 groupKey 共用一条轮询链
+ $key = self::REDIS_KEY_PREFIX . $groupKey;
+
+ // 使用 INCR 做自增计数,天然是原子操作,适合并发环境
+ // 第一次调用 INCR 返回 1,我们用 (value - 1) 来做索引,从 0 开始
+ $val = Redis::incr($key); // 如果不存在,Redis 会先置为 0 再加 1
+
+ $index = ($val - 1) % $count;
+
+ return $userIds[$index];
+ }
+}
diff --git a/app/process/CheckWeb.php b/app/process/CheckWeb.php
index 8b99d61..f3439ef 100644
--- a/app/process/CheckWeb.php
+++ b/app/process/CheckWeb.php
@@ -22,6 +22,12 @@ class CheckWeb
// });
// }
+ // 每秒执行:队列二手车数据处理
+ new Crontab('* * * * * *', function(){
+ $service = new \app\common\utils\crawl\service\UsedCarCrawl();
+ $service->execute();
+ });
+
new Crontab('*/50 * * * * *', function(){
$userIds = User::where('status', 1)->pluck('id')->toArray();
foreach ($userIds as $userId) {
diff --git a/app/process/Task.php b/app/process/Task.php
index 9675b28..47fe78f 100644
--- a/app/process/Task.php
+++ b/app/process/Task.php
@@ -15,7 +15,7 @@ class Task
{
// 每半小时执行一次,通知待跟进客户
new Crontab('0 */30 * * * *', function(){
- echo date('Y-m-d H:i:s') . "\n";
+ // echo date('Y-m-d H:i:s') . "\n";
Log::info('send follow start',[time()]);
$where = [
diff --git a/composer.json b/composer.json
index a696240..d502105 100644
--- a/composer.json
+++ b/composer.json
@@ -48,7 +48,8 @@
"webman/gateway-worker": "^1.0",
"mpdf/mpdf": "^8.2",
"jaeger/querylist": "^4.4",
- "zoujingli/ip2region": "^3.0"
+ "zoujingli/ip2region": "^3.0",
+ "webman/log": "^2.1"
},
"suggest": {
"ext-event": "For better performance. "
diff --git a/composer.lock b/composer.lock
index ba35bd5..ecd2fc5 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "ac2fa5a625069729975d072614509e1f",
+ "content-hash": "bf987c3cd5510fc0637d6b64d460dde1",
"packages": [
{
"name": "aliyuncs/oss-sdk-php",
@@ -18,13 +18,7 @@
"type": "zip",
"url": "https://api.github.com/repos/aliyun/aliyun-oss-php-sdk/zipball/483dd0b8bff5d47f0e4ffc99f6077a295c5ccbb5",
"reference": "483dd0b8bff5d47f0e4ffc99f6077a295c5ccbb5",
- "shasum": "",
- "mirrors": [
- {
- "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
- "preferred": true
- }
- ]
+ "shasum": ""
},
"require": {
"php": ">=5.3"
@@ -59,25 +53,25 @@
},
{
"name": "brick/math",
- "version": "0.13.1",
+ "version": "0.14.1",
"source": {
"type": "git",
"url": "https://github.com/brick/math.git",
- "reference": "fc7ed316430118cc7836bf45faff18d5dfc8de04"
+ "reference": "f05858549e5f9d7bb45875a75583240a38a281d0"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/brick/math/zipball/fc7ed316430118cc7836bf45faff18d5dfc8de04",
- "reference": "fc7ed316430118cc7836bf45faff18d5dfc8de04",
+ "url": "https://api.github.com/repos/brick/math/zipball/f05858549e5f9d7bb45875a75583240a38a281d0",
+ "reference": "f05858549e5f9d7bb45875a75583240a38a281d0",
"shasum": ""
},
"require": {
- "php": "^8.1"
+ "php": "^8.2"
},
"require-dev": {
"php-coveralls/php-coveralls": "^2.2",
- "phpunit/phpunit": "^10.1",
- "vimeo/psalm": "6.8.8"
+ "phpstan/phpstan": "2.1.22",
+ "phpunit/phpunit": "^11.5"
},
"type": "library",
"autoload": {
@@ -107,7 +101,7 @@
],
"support": {
"issues": "https://github.com/brick/math/issues",
- "source": "https://github.com/brick/math/tree/0.13.1"
+ "source": "https://github.com/brick/math/tree/0.14.1"
},
"funding": [
{
@@ -115,7 +109,7 @@
"type": "github"
}
],
- "time": "2025-03-29T13:50:30+00:00"
+ "time": "2025-11-24T14:40:29+00:00"
},
{
"name": "carbonphp/carbon-doctrine-types",
@@ -267,33 +261,32 @@
},
{
"name": "doctrine/inflector",
- "version": "2.0.10",
+ "version": "2.1.0",
"source": {
"type": "git",
"url": "https://github.com/doctrine/inflector.git",
- "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc"
+ "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/doctrine/inflector/zipball/5817d0659c5b50c9b950feb9af7b9668e2c436bc",
- "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc",
+ "url": "https://api.github.com/repos/doctrine/inflector/zipball/6d6c96277ea252fc1304627204c3d5e6e15faa3b",
+ "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0"
},
"require-dev": {
- "doctrine/coding-standard": "^11.0",
- "phpstan/phpstan": "^1.8",
- "phpstan/phpstan-phpunit": "^1.1",
- "phpstan/phpstan-strict-rules": "^1.3",
- "phpunit/phpunit": "^8.5 || ^9.5",
- "vimeo/psalm": "^4.25 || ^5.4"
+ "doctrine/coding-standard": "^12.0 || ^13.0",
+ "phpstan/phpstan": "^1.12 || ^2.0",
+ "phpstan/phpstan-phpunit": "^1.4 || ^2.0",
+ "phpstan/phpstan-strict-rules": "^1.6 || ^2.0",
+ "phpunit/phpunit": "^8.5 || ^12.2"
},
"type": "library",
"autoload": {
"psr-4": {
- "Doctrine\\Inflector\\": "lib/Doctrine/Inflector"
+ "Doctrine\\Inflector\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
@@ -338,7 +331,7 @@
],
"support": {
"issues": "https://github.com/doctrine/inflector/issues",
- "source": "https://github.com/doctrine/inflector/tree/2.0.10"
+ "source": "https://github.com/doctrine/inflector/tree/2.1.0"
},
"funding": [
{
@@ -354,7 +347,7 @@
"type": "tidelift"
}
],
- "time": "2024-02-18T20:23:39+00:00"
+ "time": "2025-08-10T19:31:58+00:00"
},
{
"name": "firebase/php-jwt",
@@ -421,31 +414,31 @@
},
{
"name": "fruitcake/php-cors",
- "version": "v1.3.0",
+ "version": "v1.4.0",
"source": {
"type": "git",
"url": "https://github.com/fruitcake/php-cors.git",
- "reference": "3d158f36e7875e2f040f37bc0573956240a5a38b"
+ "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/3d158f36e7875e2f040f37bc0573956240a5a38b",
- "reference": "3d158f36e7875e2f040f37bc0573956240a5a38b",
+ "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379",
+ "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379",
"shasum": ""
},
"require": {
- "php": "^7.4|^8.0",
- "symfony/http-foundation": "^4.4|^5.4|^6|^7"
+ "php": "^8.1",
+ "symfony/http-foundation": "^5.4|^6.4|^7.3|^8"
},
"require-dev": {
- "phpstan/phpstan": "^1.4",
+ "phpstan/phpstan": "^2",
"phpunit/phpunit": "^9",
- "squizlabs/php_codesniffer": "^3.5"
+ "squizlabs/php_codesniffer": "^4"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.2-dev"
+ "dev-master": "1.3-dev"
}
},
"autoload": {
@@ -476,7 +469,7 @@
],
"support": {
"issues": "https://github.com/fruitcake/php-cors/issues",
- "source": "https://github.com/fruitcake/php-cors/tree/v1.3.0"
+ "source": "https://github.com/fruitcake/php-cors/tree/v1.4.0"
},
"funding": [
{
@@ -488,7 +481,7 @@
"type": "github"
}
],
- "time": "2023-10-12T05:21:21+00:00"
+ "time": "2025-12-03T09:33:47+00:00"
},
{
"name": "graham-campbell/result-type",
@@ -554,22 +547,22 @@
},
{
"name": "guzzlehttp/guzzle",
- "version": "7.9.3",
+ "version": "7.10.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/guzzle.git",
- "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77"
+ "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/guzzle/guzzle/zipball/7b2f29fe81dc4da0ca0ea7d42107a0845946ea77",
- "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77",
+ "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b51ac707cfa420b7bfd4e4d5e510ba8008e822b4",
+ "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4",
"shasum": ""
},
"require": {
"ext-json": "*",
- "guzzlehttp/promises": "^1.5.3 || ^2.0.3",
- "guzzlehttp/psr7": "^2.7.0",
+ "guzzlehttp/promises": "^2.3",
+ "guzzlehttp/psr7": "^2.8",
"php": "^7.2.5 || ^8.0",
"psr/http-client": "^1.0",
"symfony/deprecation-contracts": "^2.2 || ^3.0"
@@ -660,7 +653,7 @@
],
"support": {
"issues": "https://github.com/guzzle/guzzle/issues",
- "source": "https://github.com/guzzle/guzzle/tree/7.9.3"
+ "source": "https://github.com/guzzle/guzzle/tree/7.10.0"
},
"funding": [
{
@@ -676,20 +669,20 @@
"type": "tidelift"
}
],
- "time": "2025-03-27T13:37:11+00:00"
+ "time": "2025-08-23T22:36:01+00:00"
},
{
"name": "guzzlehttp/promises",
- "version": "2.2.0",
+ "version": "2.3.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/promises.git",
- "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c"
+ "reference": "481557b130ef3790cf82b713667b43030dc9c957"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/guzzle/promises/zipball/7c69f28996b0a6920945dd20b3857e499d9ca96c",
- "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c",
+ "url": "https://api.github.com/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957",
+ "reference": "481557b130ef3790cf82b713667b43030dc9c957",
"shasum": ""
},
"require": {
@@ -697,7 +690,7 @@
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
- "phpunit/phpunit": "^8.5.39 || ^9.6.20"
+ "phpunit/phpunit": "^8.5.44 || ^9.6.25"
},
"type": "library",
"extra": {
@@ -743,7 +736,7 @@
],
"support": {
"issues": "https://github.com/guzzle/promises/issues",
- "source": "https://github.com/guzzle/promises/tree/2.2.0"
+ "source": "https://github.com/guzzle/promises/tree/2.3.0"
},
"funding": [
{
@@ -759,20 +752,20 @@
"type": "tidelift"
}
],
- "time": "2025-03-27T13:27:01+00:00"
+ "time": "2025-08-22T14:34:08+00:00"
},
{
"name": "guzzlehttp/psr7",
- "version": "2.7.1",
+ "version": "2.8.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/psr7.git",
- "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16"
+ "reference": "21dc724a0583619cd1652f673303492272778051"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/guzzle/psr7/zipball/c2270caaabe631b3b44c85f99e5a04bbb8060d16",
- "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16",
+ "url": "https://api.github.com/repos/guzzle/psr7/zipball/21dc724a0583619cd1652f673303492272778051",
+ "reference": "21dc724a0583619cd1652f673303492272778051",
"shasum": ""
},
"require": {
@@ -788,7 +781,7 @@
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
"http-interop/http-factory-tests": "0.9.0",
- "phpunit/phpunit": "^8.5.39 || ^9.6.20"
+ "phpunit/phpunit": "^8.5.44 || ^9.6.25"
},
"suggest": {
"laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
@@ -859,7 +852,7 @@
],
"support": {
"issues": "https://github.com/guzzle/psr7/issues",
- "source": "https://github.com/guzzle/psr7/tree/2.7.1"
+ "source": "https://github.com/guzzle/psr7/tree/2.8.0"
},
"funding": [
{
@@ -875,20 +868,20 @@
"type": "tidelift"
}
],
- "time": "2025-03-27T12:30:47+00:00"
+ "time": "2025-08-23T21:21:41+00:00"
},
{
"name": "guzzlehttp/uri-template",
- "version": "v1.0.4",
+ "version": "v1.0.5",
"source": {
"type": "git",
"url": "https://github.com/guzzle/uri-template.git",
- "reference": "30e286560c137526eccd4ce21b2de477ab0676d2"
+ "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/guzzle/uri-template/zipball/30e286560c137526eccd4ce21b2de477ab0676d2",
- "reference": "30e286560c137526eccd4ce21b2de477ab0676d2",
+ "url": "https://api.github.com/repos/guzzle/uri-template/zipball/4f4bbd4e7172148801e76e3decc1e559bdee34e1",
+ "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1",
"shasum": ""
},
"require": {
@@ -897,7 +890,7 @@
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
- "phpunit/phpunit": "^8.5.36 || ^9.6.15",
+ "phpunit/phpunit": "^8.5.44 || ^9.6.25",
"uri-template/tests": "1.0.0"
},
"type": "library",
@@ -945,7 +938,7 @@
],
"support": {
"issues": "https://github.com/guzzle/uri-template/issues",
- "source": "https://github.com/guzzle/uri-template/tree/v1.0.4"
+ "source": "https://github.com/guzzle/uri-template/tree/v1.0.5"
},
"funding": [
{
@@ -961,20 +954,20 @@
"type": "tidelift"
}
],
- "time": "2025-02-03T10:55:03+00:00"
+ "time": "2025-08-22T14:27:06+00:00"
},
{
"name": "illuminate/bus",
- "version": "v12.18.0",
+ "version": "v12.42.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/bus.git",
- "reference": "60da78ea881c539ce56c5b66321be73755c5918c"
+ "reference": "fdb568a20ad96461caa83b98ffc36d9e95894491"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/illuminate/bus/zipball/60da78ea881c539ce56c5b66321be73755c5918c",
- "reference": "60da78ea881c539ce56c5b66321be73755c5918c",
+ "url": "https://api.github.com/repos/illuminate/bus/zipball/fdb568a20ad96461caa83b98ffc36d9e95894491",
+ "reference": "fdb568a20ad96461caa83b98ffc36d9e95894491",
"shasum": ""
},
"require": {
@@ -1014,27 +1007,29 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
- "time": "2025-05-13T15:08:45+00:00"
+ "time": "2025-11-30T17:14:13+00:00"
},
{
"name": "illuminate/collections",
- "version": "v12.18.0",
+ "version": "v12.42.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/collections.git",
- "reference": "e5711846b7c68128bc8de72c13e017606043c996"
+ "reference": "16657effa6a5a4e728f9aeb3e38fb4fa9ba70e7d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/illuminate/collections/zipball/e5711846b7c68128bc8de72c13e017606043c996",
- "reference": "e5711846b7c68128bc8de72c13e017606043c996",
+ "url": "https://api.github.com/repos/illuminate/collections/zipball/16657effa6a5a4e728f9aeb3e38fb4fa9ba70e7d",
+ "reference": "16657effa6a5a4e728f9aeb3e38fb4fa9ba70e7d",
"shasum": ""
},
"require": {
"illuminate/conditionable": "^12.0",
"illuminate/contracts": "^12.0",
"illuminate/macroable": "^12.0",
- "php": "^8.2"
+ "php": "^8.2",
+ "symfony/polyfill-php84": "^1.33",
+ "symfony/polyfill-php85": "^1.33"
},
"suggest": {
"illuminate/http": "Required to convert collections to API resources (^12.0).",
@@ -1071,11 +1066,11 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
- "time": "2025-05-28T13:08:33+00:00"
+ "time": "2025-12-06T18:08:25+00:00"
},
{
"name": "illuminate/conditionable",
- "version": "v12.18.0",
+ "version": "v12.42.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/conditionable.git",
@@ -1121,26 +1116,37 @@
},
{
"name": "illuminate/container",
- "version": "v12.18.0",
+ "version": "v12.42.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/container.git",
- "reference": "ff9dde2c8dce16ea9ecf0418095749311240aff9"
+ "reference": "326667a4c813e3ad5a645969a7e3f5c10d159de2"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/illuminate/container/zipball/ff9dde2c8dce16ea9ecf0418095749311240aff9",
- "reference": "ff9dde2c8dce16ea9ecf0418095749311240aff9",
+ "url": "https://api.github.com/repos/illuminate/container/zipball/326667a4c813e3ad5a645969a7e3f5c10d159de2",
+ "reference": "326667a4c813e3ad5a645969a7e3f5c10d159de2",
"shasum": ""
},
"require": {
"illuminate/contracts": "^12.0",
+ "illuminate/reflection": "^12.0",
"php": "^8.2",
- "psr/container": "^1.1.1|^2.0.1"
+ "psr/container": "^1.1.1|^2.0.1",
+ "symfony/polyfill-php84": "^1.33",
+ "symfony/polyfill-php85": "^1.33"
},
"provide": {
"psr/container-implementation": "1.1|2.0"
},
+ "suggest": {
+ "illuminate/auth": "Required to use the Auth attribute",
+ "illuminate/cache": "Required to use the Cache attribute",
+ "illuminate/config": "Required to use the Config attribute",
+ "illuminate/database": "Required to use the DB attribute",
+ "illuminate/filesystem": "Required to use the Storage attribute",
+ "illuminate/log": "Required to use the Log or Context attributes"
+ },
"type": "library",
"extra": {
"branch-alias": {
@@ -1168,20 +1174,20 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
- "time": "2025-06-09T14:04:48+00:00"
+ "time": "2025-12-08T22:34:25+00:00"
},
{
"name": "illuminate/contracts",
- "version": "v12.18.0",
+ "version": "v12.42.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/contracts.git",
- "reference": "152313571553ef1be907a3c18b8ef2d635cb4339"
+ "reference": "19e8938edb73047017cfbd443b96844b86da4a59"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/illuminate/contracts/zipball/152313571553ef1be907a3c18b8ef2d635cb4339",
- "reference": "152313571553ef1be907a3c18b8ef2d635cb4339",
+ "url": "https://api.github.com/repos/illuminate/contracts/zipball/19e8938edb73047017cfbd443b96844b86da4a59",
+ "reference": "19e8938edb73047017cfbd443b96844b86da4a59",
"shasum": ""
},
"require": {
@@ -1216,24 +1222,24 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
- "time": "2025-06-09T18:23:03+00:00"
+ "time": "2025-11-26T21:36:01+00:00"
},
{
"name": "illuminate/database",
- "version": "v12.18.0",
+ "version": "v12.42.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/database.git",
- "reference": "da274285e4c09f6794945354b5b11157f4d9cab1"
+ "reference": "b44f021fee7b48289312e22fd6e4d580374a86ac"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/illuminate/database/zipball/da274285e4c09f6794945354b5b11157f4d9cab1",
- "reference": "da274285e4c09f6794945354b5b11157f4d9cab1",
+ "url": "https://api.github.com/repos/illuminate/database/zipball/b44f021fee7b48289312e22fd6e4d580374a86ac",
+ "reference": "b44f021fee7b48289312e22fd6e4d580374a86ac",
"shasum": ""
},
"require": {
- "brick/math": "^0.11|^0.12|^0.13",
+ "brick/math": "^0.11|^0.12|^0.13|^0.14",
"ext-pdo": "*",
"illuminate/collections": "^12.0",
"illuminate/container": "^12.0",
@@ -1241,7 +1247,8 @@
"illuminate/macroable": "^12.0",
"illuminate/support": "^12.0",
"laravel/serializable-closure": "^1.3|^2.0",
- "php": "^8.2"
+ "php": "^8.2",
+ "symfony/polyfill-php85": "^1.33"
},
"suggest": {
"ext-filter": "Required to use the Postgres database driver.",
@@ -1286,20 +1293,20 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
- "time": "2025-06-09T14:07:55+00:00"
+ "time": "2025-12-06T18:08:25+00:00"
},
{
"name": "illuminate/events",
- "version": "v12.18.0",
+ "version": "v12.42.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/events.git",
- "reference": "bf1f121ea51e077e893d32e2848e102513d4b1b5"
+ "reference": "5fbf9a127cb649699071c2fe98ac1d39e4991da3"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/illuminate/events/zipball/bf1f121ea51e077e893d32e2848e102513d4b1b5",
- "reference": "bf1f121ea51e077e893d32e2848e102513d4b1b5",
+ "url": "https://api.github.com/repos/illuminate/events/zipball/5fbf9a127cb649699071c2fe98ac1d39e4991da3",
+ "reference": "5fbf9a127cb649699071c2fe98ac1d39e4991da3",
"shasum": ""
},
"require": {
@@ -1341,20 +1348,20 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
- "time": "2025-05-13T15:08:45+00:00"
+ "time": "2025-12-01T15:02:02+00:00"
},
{
"name": "illuminate/filesystem",
- "version": "v12.18.0",
+ "version": "v12.42.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/filesystem.git",
- "reference": "80fe8605cfa360fdbc85f67c19801a9657615aab"
+ "reference": "b1fbb20010e868f838feac86aeac8ba439fca10d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/illuminate/filesystem/zipball/80fe8605cfa360fdbc85f67c19801a9657615aab",
- "reference": "80fe8605cfa360fdbc85f67c19801a9657615aab",
+ "url": "https://api.github.com/repos/illuminate/filesystem/zipball/b1fbb20010e868f838feac86aeac8ba439fca10d",
+ "reference": "b1fbb20010e868f838feac86aeac8ba439fca10d",
"shasum": ""
},
"require": {
@@ -1408,20 +1415,20 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
- "time": "2025-05-13T15:08:45+00:00"
+ "time": "2025-10-29T15:59:33+00:00"
},
{
"name": "illuminate/http",
- "version": "v12.18.0",
+ "version": "v12.42.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/http.git",
- "reference": "bbafb2b2e629530e516d52a1fa46dfeb09b5e184"
+ "reference": "b7997f2faffb3c31f2a0fa079428ff6bfcce8907"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/illuminate/http/zipball/bbafb2b2e629530e516d52a1fa46dfeb09b5e184",
- "reference": "bbafb2b2e629530e516d52a1fa46dfeb09b5e184",
+ "url": "https://api.github.com/repos/illuminate/http/zipball/b7997f2faffb3c31f2a0fa079428ff6bfcce8907",
+ "reference": "b7997f2faffb3c31f2a0fa079428ff6bfcce8907",
"shasum": ""
},
"require": {
@@ -1437,7 +1444,8 @@
"symfony/http-foundation": "^7.2.0",
"symfony/http-kernel": "^7.2.0",
"symfony/mime": "^7.2.0",
- "symfony/polyfill-php83": "^1.31"
+ "symfony/polyfill-php83": "^1.33",
+ "symfony/polyfill-php85": "^1.33"
},
"suggest": {
"ext-gd": "Required to use Illuminate\\Http\\Testing\\FileFactory::image()."
@@ -1469,11 +1477,11 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
- "time": "2025-06-06T18:14:49+00:00"
+ "time": "2025-12-09T15:25:50+00:00"
},
{
"name": "illuminate/macroable",
- "version": "v12.18.0",
+ "version": "v12.42.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/macroable.git",
@@ -1519,16 +1527,16 @@
},
{
"name": "illuminate/pagination",
- "version": "v12.18.0",
+ "version": "v12.42.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/pagination.git",
- "reference": "7f68669ad7d8a706883b8409f6b7254fdc437f67"
+ "reference": "87e7e3e7b02d6809b1bcd41782e1ca2c6d2a413b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/illuminate/pagination/zipball/7f68669ad7d8a706883b8409f6b7254fdc437f67",
- "reference": "7f68669ad7d8a706883b8409f6b7254fdc437f67",
+ "url": "https://api.github.com/repos/illuminate/pagination/zipball/87e7e3e7b02d6809b1bcd41782e1ca2c6d2a413b",
+ "reference": "87e7e3e7b02d6809b1bcd41782e1ca2c6d2a413b",
"shasum": ""
},
"require": {
@@ -1565,27 +1573,31 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
- "time": "2025-05-28T21:37:18+00:00"
+ "time": "2025-11-16T14:36:17+00:00"
},
{
"name": "illuminate/pipeline",
- "version": "v12.18.0",
+ "version": "v12.42.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/pipeline.git",
- "reference": "a1039dfe54854470cdda37782bab0901aa588dd4"
+ "reference": "b6a14c20d69a44bf0a6fba664a00d23ca71770ee"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/illuminate/pipeline/zipball/a1039dfe54854470cdda37782bab0901aa588dd4",
- "reference": "a1039dfe54854470cdda37782bab0901aa588dd4",
+ "url": "https://api.github.com/repos/illuminate/pipeline/zipball/b6a14c20d69a44bf0a6fba664a00d23ca71770ee",
+ "reference": "b6a14c20d69a44bf0a6fba664a00d23ca71770ee",
"shasum": ""
},
"require": {
"illuminate/contracts": "^12.0",
+ "illuminate/macroable": "^12.0",
"illuminate/support": "^12.0",
"php": "^8.2"
},
+ "suggest": {
+ "illuminate/database": "Required to use database transactions (^12.0)."
+ },
"type": "library",
"extra": {
"branch-alias": {
@@ -1613,20 +1625,20 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
- "time": "2025-05-13T15:08:45+00:00"
+ "time": "2025-08-20T13:36:50+00:00"
},
{
"name": "illuminate/redis",
- "version": "v12.18.0",
+ "version": "v12.42.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/redis.git",
- "reference": "b225dc28092b00ed0555df66ce3a3a448b4dd8ae"
+ "reference": "8dc6682638100db9feceaac3db206de752dd0f9a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/illuminate/redis/zipball/b225dc28092b00ed0555df66ce3a3a448b4dd8ae",
- "reference": "b225dc28092b00ed0555df66ce3a3a448b4dd8ae",
+ "url": "https://api.github.com/repos/illuminate/redis/zipball/8dc6682638100db9feceaac3db206de752dd0f9a",
+ "reference": "8dc6682638100db9feceaac3db206de752dd0f9a",
"shasum": ""
},
"require": {
@@ -1667,20 +1679,71 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
- "time": "2025-05-13T15:08:45+00:00"
+ "time": "2025-11-20T20:50:40+00:00"
},
{
- "name": "illuminate/session",
- "version": "v12.18.0",
+ "name": "illuminate/reflection",
+ "version": "v12.42.0",
"source": {
"type": "git",
- "url": "https://github.com/illuminate/session.git",
- "reference": "9d27155b34bca502fe3e1adc16035b46d5ef3ed8"
+ "url": "https://github.com/illuminate/reflection.git",
+ "reference": "7b86bc570d5b75e4a3ad79f8cca1491ba24b7c75"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/illuminate/session/zipball/9d27155b34bca502fe3e1adc16035b46d5ef3ed8",
- "reference": "9d27155b34bca502fe3e1adc16035b46d5ef3ed8",
+ "url": "https://api.github.com/repos/illuminate/reflection/zipball/7b86bc570d5b75e4a3ad79f8cca1491ba24b7c75",
+ "reference": "7b86bc570d5b75e4a3ad79f8cca1491ba24b7c75",
+ "shasum": ""
+ },
+ "require": {
+ "illuminate/collections": "^12.0",
+ "illuminate/contracts": "^12.0",
+ "php": "^8.2"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "12.x-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "helpers.php"
+ ],
+ "psr-4": {
+ "Illuminate\\Support\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Taylor Otwell",
+ "email": "taylor@laravel.com"
+ }
+ ],
+ "description": "The Illuminate Reflection package.",
+ "homepage": "https://laravel.com",
+ "support": {
+ "issues": "https://github.com/laravel/framework/issues",
+ "source": "https://github.com/laravel/framework"
+ },
+ "time": "2025-12-09T15:11:22+00:00"
+ },
+ {
+ "name": "illuminate/session",
+ "version": "v12.42.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/illuminate/session.git",
+ "reference": "c376e4a0dda79a88462e8a7d16cc924aa3eb6e15"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/illuminate/session/zipball/c376e4a0dda79a88462e8a7d16cc924aa3eb6e15",
+ "reference": "c376e4a0dda79a88462e8a7d16cc924aa3eb6e15",
"shasum": ""
},
"require": {
@@ -1724,20 +1787,20 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
- "time": "2025-05-13T15:08:45+00:00"
+ "time": "2025-10-26T17:11:19+00:00"
},
{
"name": "illuminate/support",
- "version": "v12.18.0",
+ "version": "v12.42.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/support.git",
- "reference": "3c3894ba09a86663b747efaa6822639f1c32ea01"
+ "reference": "d35411be5657e0b5560a5885f3c9140e4cbe0be5"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/illuminate/support/zipball/3c3894ba09a86663b747efaa6822639f1c32ea01",
- "reference": "3c3894ba09a86663b747efaa6822639f1c32ea01",
+ "url": "https://api.github.com/repos/illuminate/support/zipball/d35411be5657e0b5560a5885f3c9140e4cbe0be5",
+ "reference": "d35411be5657e0b5560a5885f3c9140e4cbe0be5",
"shasum": ""
},
"require": {
@@ -1749,8 +1812,11 @@
"illuminate/conditionable": "^12.0",
"illuminate/contracts": "^12.0",
"illuminate/macroable": "^12.0",
+ "illuminate/reflection": "^12.0",
"nesbot/carbon": "^3.8.4",
"php": "^8.2",
+ "symfony/polyfill-php83": "^1.33",
+ "symfony/polyfill-php85": "^1.33",
"voku/portable-ascii": "^2.0.2"
},
"conflict": {
@@ -1801,7 +1867,7 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
- "time": "2025-06-06T18:15:26+00:00"
+ "time": "2025-12-09T15:26:52+00:00"
},
{
"name": "jaeger/g-http",
@@ -1960,16 +2026,16 @@
},
{
"name": "laravel/serializable-closure",
- "version": "v2.0.4",
+ "version": "v2.0.7",
"source": {
"type": "git",
"url": "https://github.com/laravel/serializable-closure.git",
- "reference": "b352cf0534aa1ae6b4d825d1e762e35d43f8a841"
+ "reference": "cb291e4c998ac50637c7eeb58189c14f5de5b9dd"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/b352cf0534aa1ae6b4d825d1e762e35d43f8a841",
- "reference": "b352cf0534aa1ae6b4d825d1e762e35d43f8a841",
+ "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/cb291e4c998ac50637c7eeb58189c14f5de5b9dd",
+ "reference": "cb291e4c998ac50637c7eeb58189c14f5de5b9dd",
"shasum": ""
},
"require": {
@@ -1978,7 +2044,7 @@
"require-dev": {
"illuminate/support": "^10.0|^11.0|^12.0",
"nesbot/carbon": "^2.67|^3.0",
- "pestphp/pest": "^2.36|^3.0",
+ "pestphp/pest": "^2.36|^3.0|^4.0",
"phpstan/phpstan": "^2.0",
"symfony/var-dumper": "^6.2.0|^7.0.0"
},
@@ -2017,27 +2083,21 @@
"issues": "https://github.com/laravel/serializable-closure/issues",
"source": "https://github.com/laravel/serializable-closure"
},
- "time": "2025-03-19T13:51:03+00:00"
+ "time": "2025-11-21T20:52:36+00:00"
},
{
"name": "league/flysystem",
- "version": "3.29.0",
+ "version": "3.30.2",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/flysystem.git",
- "reference": "0adc0d9a51852e170e0028a60bd271726626d3f0"
+ "reference": "5966a8ba23e62bdb518dd9e0e665c2dbd4b5b277"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/0adc0d9a51852e170e0028a60bd271726626d3f0",
- "reference": "0adc0d9a51852e170e0028a60bd271726626d3f0",
- "shasum": "",
- "mirrors": [
- {
- "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
- "preferred": true
- }
- ]
+ "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/5966a8ba23e62bdb518dd9e0e665c2dbd4b5b277",
+ "reference": "5966a8ba23e62bdb518dd9e0e665c2dbd4b5b277",
+ "shasum": ""
},
"require": {
"league/flysystem-local": "^3.0.0",
@@ -2060,13 +2120,13 @@
"composer/semver": "^3.0",
"ext-fileinfo": "*",
"ext-ftp": "*",
- "ext-mongodb": "^1.3",
+ "ext-mongodb": "^1.3|^2",
"ext-zip": "*",
"friendsofphp/php-cs-fixer": "^3.5",
"google/cloud-storage": "^1.23",
"guzzlehttp/psr7": "^2.6",
"microsoft/azure-storage-blob": "^1.1",
- "mongodb/mongodb": "^1.2",
+ "mongodb/mongodb": "^1.2|^2",
"phpseclib/phpseclib": "^3.0.36",
"phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^9.5.11|^10.0",
@@ -2104,29 +2164,23 @@
],
"support": {
"issues": "https://github.com/thephpleague/flysystem/issues",
- "source": "https://github.com/thephpleague/flysystem/tree/3.29.0"
+ "source": "https://github.com/thephpleague/flysystem/tree/3.30.2"
},
- "time": "2024-09-29T11:59:11+00:00"
+ "time": "2025-11-10T17:13:11+00:00"
},
{
"name": "league/flysystem-local",
- "version": "3.29.0",
+ "version": "3.30.2",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/flysystem-local.git",
- "reference": "e0e8d52ce4b2ed154148453d321e97c8e931bd27"
+ "reference": "ab4f9d0d672f601b102936aa728801dd1a11968d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/e0e8d52ce4b2ed154148453d321e97c8e931bd27",
- "reference": "e0e8d52ce4b2ed154148453d321e97c8e931bd27",
- "shasum": "",
- "mirrors": [
- {
- "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
- "preferred": true
- }
- ]
+ "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/ab4f9d0d672f601b102936aa728801dd1a11968d",
+ "reference": "ab4f9d0d672f601b102936aa728801dd1a11968d",
+ "shasum": ""
},
"require": {
"ext-fileinfo": "*",
@@ -2159,29 +2213,23 @@
"local"
],
"support": {
- "source": "https://github.com/thephpleague/flysystem-local/tree/3.29.0"
+ "source": "https://github.com/thephpleague/flysystem-local/tree/3.30.2"
},
- "time": "2024-08-09T21:24:39+00:00"
+ "time": "2025-11-10T11:23:37+00:00"
},
{
"name": "league/mime-type-detection",
- "version": "1.15.0",
+ "version": "1.16.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/mime-type-detection.git",
- "reference": "ce0f4d1e8a6f4eb0ddff33f57c69c50fd09f4301"
+ "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/ce0f4d1e8a6f4eb0ddff33f57c69c50fd09f4301",
- "reference": "ce0f4d1e8a6f4eb0ddff33f57c69c50fd09f4301",
- "shasum": "",
- "mirrors": [
- {
- "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
- "preferred": true
- }
- ]
+ "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/2d6702ff215bf922936ccc1ad31007edc76451b9",
+ "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9",
+ "shasum": ""
},
"require": {
"ext-fileinfo": "*",
@@ -2211,7 +2259,7 @@
"description": "Mime-type detection for Flysystem",
"support": {
"issues": "https://github.com/thephpleague/mime-type-detection/issues",
- "source": "https://github.com/thephpleague/mime-type-detection/tree/1.15.0"
+ "source": "https://github.com/thephpleague/mime-type-detection/tree/1.16.0"
},
"funding": [
{
@@ -2223,26 +2271,26 @@
"type": "tidelift"
}
],
- "time": "2024-01-28T23:22:08+00:00"
+ "time": "2024-09-21T08:32:55+00:00"
},
{
"name": "maennchen/zipstream-php",
- "version": "3.1.2",
+ "version": "3.2.0",
"source": {
"type": "git",
"url": "https://github.com/maennchen/ZipStream-PHP.git",
- "reference": "aeadcf5c412332eb426c0f9b4485f6accba2a99f"
+ "reference": "9712d8fa4cdf9240380b01eb4be55ad8dcf71416"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/aeadcf5c412332eb426c0f9b4485f6accba2a99f",
- "reference": "aeadcf5c412332eb426c0f9b4485f6accba2a99f",
+ "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/9712d8fa4cdf9240380b01eb4be55ad8dcf71416",
+ "reference": "9712d8fa4cdf9240380b01eb4be55ad8dcf71416",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"ext-zlib": "*",
- "php-64bit": "^8.2"
+ "php-64bit": "^8.3"
},
"require-dev": {
"brianium/paratest": "^7.7",
@@ -2251,7 +2299,7 @@
"guzzlehttp/guzzle": "^7.5",
"mikey179/vfsstream": "^1.6",
"php-coveralls/php-coveralls": "^2.5",
- "phpunit/phpunit": "^11.0",
+ "phpunit/phpunit": "^12.0",
"vimeo/psalm": "^6.0"
},
"suggest": {
@@ -2293,7 +2341,7 @@
],
"support": {
"issues": "https://github.com/maennchen/ZipStream-PHP/issues",
- "source": "https://github.com/maennchen/ZipStream-PHP/tree/3.1.2"
+ "source": "https://github.com/maennchen/ZipStream-PHP/tree/3.2.0"
},
"funding": [
{
@@ -2301,7 +2349,7 @@
"type": "github"
}
],
- "time": "2025-01-27T12:07:53+00:00"
+ "time": "2025-07-17T11:15:13+00:00"
},
{
"name": "markbaker/complex",
@@ -2514,16 +2562,16 @@
},
{
"name": "mpdf/mpdf",
- "version": "v8.2.6",
+ "version": "v8.2.7",
"source": {
"type": "git",
"url": "https://github.com/mpdf/mpdf.git",
- "reference": "dd30e3b01061cf8dfe65e7041ab4cc46d8ebdd44"
+ "reference": "b59670a09498689c33ce639bac8f5ba26721dab3"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/mpdf/mpdf/zipball/dd30e3b01061cf8dfe65e7041ab4cc46d8ebdd44",
- "reference": "dd30e3b01061cf8dfe65e7041ab4cc46d8ebdd44",
+ "url": "https://api.github.com/repos/mpdf/mpdf/zipball/b59670a09498689c33ce639bac8f5ba26721dab3",
+ "reference": "b59670a09498689c33ce639bac8f5ba26721dab3",
"shasum": ""
},
"require": {
@@ -2591,7 +2639,7 @@
"type": "custom"
}
],
- "time": "2025-08-18T08:51:51+00:00"
+ "time": "2025-12-01T10:18:02+00:00"
},
{
"name": "mpdf/psr-http-message-shim",
@@ -2747,16 +2795,16 @@
},
{
"name": "nesbot/carbon",
- "version": "3.10.0",
+ "version": "3.11.0",
"source": {
"type": "git",
"url": "https://github.com/CarbonPHP/carbon.git",
- "reference": "c1397390dd0a7e0f11660f0ae20f753d88c1f3d9"
+ "reference": "bdb375400dcd162624531666db4799b36b64e4a1"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/c1397390dd0a7e0f11660f0ae20f753d88c1f3d9",
- "reference": "c1397390dd0a7e0f11660f0ae20f753d88c1f3d9",
+ "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/bdb375400dcd162624531666db4799b36b64e4a1",
+ "reference": "bdb375400dcd162624531666db4799b36b64e4a1",
"shasum": ""
},
"require": {
@@ -2764,9 +2812,9 @@
"ext-json": "*",
"php": "^8.1",
"psr/clock": "^1.0",
- "symfony/clock": "^6.3.12 || ^7.0",
+ "symfony/clock": "^6.3.12 || ^7.0 || ^8.0",
"symfony/polyfill-mbstring": "^1.0",
- "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0"
+ "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0 || ^8.0"
},
"provide": {
"psr/clock-implementation": "1.0"
@@ -2774,13 +2822,13 @@
"require-dev": {
"doctrine/dbal": "^3.6.3 || ^4.0",
"doctrine/orm": "^2.15.2 || ^3.0",
- "friendsofphp/php-cs-fixer": "^3.75.0",
+ "friendsofphp/php-cs-fixer": "^v3.87.1",
"kylekatarnls/multi-tester": "^2.5.3",
"phpmd/phpmd": "^2.15.0",
"phpstan/extension-installer": "^1.4.3",
- "phpstan/phpstan": "^2.1.17",
- "phpunit/phpunit": "^10.5.46",
- "squizlabs/php_codesniffer": "^3.13.0"
+ "phpstan/phpstan": "^2.1.22",
+ "phpunit/phpunit": "^10.5.53",
+ "squizlabs/php_codesniffer": "^3.13.4"
},
"bin": [
"bin/carbon"
@@ -2848,7 +2896,7 @@
"type": "tidelift"
}
],
- "time": "2025-06-12T10:24:28+00:00"
+ "time": "2025-12-02T21:04:28+00:00"
},
{
"name": "nikic/fast-route",
@@ -2952,16 +3000,16 @@
},
{
"name": "phpmailer/phpmailer",
- "version": "v6.10.0",
+ "version": "v6.12.0",
"source": {
"type": "git",
"url": "https://github.com/PHPMailer/PHPMailer.git",
- "reference": "bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144"
+ "reference": "d1ac35d784bf9f5e61b424901d5a014967f15b12"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144",
- "reference": "bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144",
+ "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/d1ac35d784bf9f5e61b424901d5a014967f15b12",
+ "reference": "d1ac35d784bf9f5e61b424901d5a014967f15b12",
"shasum": ""
},
"require": {
@@ -3021,7 +3069,7 @@
"description": "PHPMailer is a full-featured email creation and transfer class for PHP",
"support": {
"issues": "https://github.com/PHPMailer/PHPMailer/issues",
- "source": "https://github.com/PHPMailer/PHPMailer/tree/v6.10.0"
+ "source": "https://github.com/PHPMailer/PHPMailer/tree/v6.12.0"
},
"funding": [
{
@@ -3029,7 +3077,7 @@
"type": "github"
}
],
- "time": "2025-04-24T15:19:31+00:00"
+ "time": "2025-10-15T16:49:08+00:00"
},
{
"name": "phpoffice/phpspreadsheet",
@@ -3139,16 +3187,16 @@
},
{
"name": "phpoption/phpoption",
- "version": "1.9.3",
+ "version": "1.9.4",
"source": {
"type": "git",
"url": "https://github.com/schmittjoh/php-option.git",
- "reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54"
+ "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/e3fac8b24f56113f7cb96af14958c0dd16330f54",
- "reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54",
+ "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d",
+ "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d",
"shasum": ""
},
"require": {
@@ -3156,7 +3204,7 @@
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
- "phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28"
+ "phpunit/phpunit": "^8.5.44 || ^9.6.25 || ^10.5.53 || ^11.5.34"
},
"type": "library",
"extra": {
@@ -3198,7 +3246,7 @@
],
"support": {
"issues": "https://github.com/schmittjoh/php-option/issues",
- "source": "https://github.com/schmittjoh/php-option/tree/1.9.3"
+ "source": "https://github.com/schmittjoh/php-option/tree/1.9.4"
},
"funding": [
{
@@ -3210,7 +3258,7 @@
"type": "tidelift"
}
],
- "time": "2024-07-20T21:41:07+00:00"
+ "time": "2025-08-21T11:53:16+00:00"
},
{
"name": "psr/cache",
@@ -3801,13 +3849,7 @@
"type": "zip",
"url": "https://api.github.com/repos/shopwwi/filesystem-oss/zipball/85cb385d1a5d11ca3c22bc485a7b18c588fe7645",
"reference": "85cb385d1a5d11ca3c22bc485a7b18c588fe7645",
- "shasum": "",
- "mirrors": [
- {
- "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
- "preferred": true
- }
- ]
+ "shasum": ""
},
"require": {
"aliyuncs/oss-sdk-php": "^2.4",
@@ -3860,13 +3902,7 @@
"type": "zip",
"url": "https://api.github.com/repos/shopwwi/webman-filesystem/zipball/26ac82515d3787943573c50d1a6f0ca339f0d3d4",
"reference": "26ac82515d3787943573c50d1a6f0ca339f0d3d4",
- "shasum": "",
- "mirrors": [
- {
- "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
- "preferred": true
- }
- ]
+ "shasum": ""
},
"require": {
"league/flysystem": "^2.3|^3.0",
@@ -3909,16 +3945,16 @@
},
{
"name": "symfony/cache",
- "version": "v7.3.0",
+ "version": "v7.4.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/cache.git",
- "reference": "c4b217b578c11ec764867aa0c73e602c602965de"
+ "reference": "21e0755783bbbab58f2bb6a7a57896d21d27a366"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/cache/zipball/c4b217b578c11ec764867aa0c73e602c602965de",
- "reference": "c4b217b578c11ec764867aa0c73e602c602965de",
+ "url": "https://api.github.com/repos/symfony/cache/zipball/21e0755783bbbab58f2bb6a7a57896d21d27a366",
+ "reference": "21e0755783bbbab58f2bb6a7a57896d21d27a366",
"shasum": ""
},
"require": {
@@ -3926,12 +3962,14 @@
"psr/cache": "^2.0|^3.0",
"psr/log": "^1.1|^2|^3",
"symfony/cache-contracts": "^3.6",
- "symfony/deprecation-contracts": "^2.5|^3.0",
+ "symfony/deprecation-contracts": "^2.5|^3",
"symfony/service-contracts": "^2.5|^3",
- "symfony/var-exporter": "^6.4|^7.0"
+ "symfony/var-exporter": "^6.4|^7.0|^8.0"
},
"conflict": {
"doctrine/dbal": "<3.6",
+ "ext-redis": "<6.1",
+ "ext-relay": "<0.12.1",
"symfony/dependency-injection": "<6.4",
"symfony/http-kernel": "<6.4",
"symfony/var-dumper": "<6.4"
@@ -3946,13 +3984,13 @@
"doctrine/dbal": "^3.6|^4",
"predis/predis": "^1.1|^2.0",
"psr/simple-cache": "^1.0|^2.0|^3.0",
- "symfony/clock": "^6.4|^7.0",
- "symfony/config": "^6.4|^7.0",
- "symfony/dependency-injection": "^6.4|^7.0",
- "symfony/filesystem": "^6.4|^7.0",
- "symfony/http-kernel": "^6.4|^7.0",
- "symfony/messenger": "^6.4|^7.0",
- "symfony/var-dumper": "^6.4|^7.0"
+ "symfony/clock": "^6.4|^7.0|^8.0",
+ "symfony/config": "^6.4|^7.0|^8.0",
+ "symfony/dependency-injection": "^6.4|^7.0|^8.0",
+ "symfony/filesystem": "^6.4|^7.0|^8.0",
+ "symfony/http-kernel": "^6.4|^7.0|^8.0",
+ "symfony/messenger": "^6.4|^7.0|^8.0",
+ "symfony/var-dumper": "^6.4|^7.0|^8.0"
},
"type": "library",
"autoload": {
@@ -3987,7 +4025,7 @@
"psr6"
],
"support": {
- "source": "https://github.com/symfony/cache/tree/v7.3.0"
+ "source": "https://github.com/symfony/cache/tree/v7.4.1"
},
"funding": [
{
@@ -3998,12 +4036,16 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2025-05-06T19:00:13+00:00"
+ "time": "2025-12-04T18:11:45+00:00"
},
{
"name": "symfony/cache-contracts",
@@ -4083,16 +4125,16 @@
},
{
"name": "symfony/clock",
- "version": "v7.3.0",
+ "version": "v7.4.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/clock.git",
- "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24"
+ "reference": "9169f24776edde469914c1e7a1442a50f7a4e110"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/clock/zipball/b81435fbd6648ea425d1ee96a2d8e68f4ceacd24",
- "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24",
+ "url": "https://api.github.com/repos/symfony/clock/zipball/9169f24776edde469914c1e7a1442a50f7a4e110",
+ "reference": "9169f24776edde469914c1e7a1442a50f7a4e110",
"shasum": ""
},
"require": {
@@ -4137,7 +4179,7 @@
"time"
],
"support": {
- "source": "https://github.com/symfony/clock/tree/v7.3.0"
+ "source": "https://github.com/symfony/clock/tree/v7.4.0"
},
"funding": [
{
@@ -4148,25 +4190,29 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2024-09-25T14:21:43+00:00"
+ "time": "2025-11-12T15:39:26+00:00"
},
{
"name": "symfony/console",
- "version": "v7.3.0",
+ "version": "v7.4.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
- "reference": "66c1440edf6f339fd82ed6c7caa76cb006211b44"
+ "reference": "6d9f0fbf2ec2e9785880096e3abd0ca0c88b506e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/console/zipball/66c1440edf6f339fd82ed6c7caa76cb006211b44",
- "reference": "66c1440edf6f339fd82ed6c7caa76cb006211b44",
+ "url": "https://api.github.com/repos/symfony/console/zipball/6d9f0fbf2ec2e9785880096e3abd0ca0c88b506e",
+ "reference": "6d9f0fbf2ec2e9785880096e3abd0ca0c88b506e",
"shasum": ""
},
"require": {
@@ -4174,7 +4220,7 @@
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/polyfill-mbstring": "~1.0",
"symfony/service-contracts": "^2.5|^3",
- "symfony/string": "^7.2"
+ "symfony/string": "^7.2|^8.0"
},
"conflict": {
"symfony/dependency-injection": "<6.4",
@@ -4188,16 +4234,16 @@
},
"require-dev": {
"psr/log": "^1|^2|^3",
- "symfony/config": "^6.4|^7.0",
- "symfony/dependency-injection": "^6.4|^7.0",
- "symfony/event-dispatcher": "^6.4|^7.0",
- "symfony/http-foundation": "^6.4|^7.0",
- "symfony/http-kernel": "^6.4|^7.0",
- "symfony/lock": "^6.4|^7.0",
- "symfony/messenger": "^6.4|^7.0",
- "symfony/process": "^6.4|^7.0",
- "symfony/stopwatch": "^6.4|^7.0",
- "symfony/var-dumper": "^6.4|^7.0"
+ "symfony/config": "^6.4|^7.0|^8.0",
+ "symfony/dependency-injection": "^6.4|^7.0|^8.0",
+ "symfony/event-dispatcher": "^6.4|^7.0|^8.0",
+ "symfony/http-foundation": "^6.4|^7.0|^8.0",
+ "symfony/http-kernel": "^6.4|^7.0|^8.0",
+ "symfony/lock": "^6.4|^7.0|^8.0",
+ "symfony/messenger": "^6.4|^7.0|^8.0",
+ "symfony/process": "^6.4|^7.0|^8.0",
+ "symfony/stopwatch": "^6.4|^7.0|^8.0",
+ "symfony/var-dumper": "^6.4|^7.0|^8.0"
},
"type": "library",
"autoload": {
@@ -4231,7 +4277,7 @@
"terminal"
],
"support": {
- "source": "https://github.com/symfony/console/tree/v7.3.0"
+ "source": "https://github.com/symfony/console/tree/v7.4.1"
},
"funding": [
{
@@ -4242,12 +4288,16 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2025-05-24T10:34:04+00:00"
+ "time": "2025-12-05T15:23:39+00:00"
},
{
"name": "symfony/deprecation-contracts",
@@ -4318,32 +4368,33 @@
},
{
"name": "symfony/error-handler",
- "version": "v7.3.0",
+ "version": "v7.4.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/error-handler.git",
- "reference": "cf68d225bc43629de4ff54778029aee6dc191b83"
+ "reference": "48be2b0653594eea32dcef130cca1c811dcf25c2"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/error-handler/zipball/cf68d225bc43629de4ff54778029aee6dc191b83",
- "reference": "cf68d225bc43629de4ff54778029aee6dc191b83",
+ "url": "https://api.github.com/repos/symfony/error-handler/zipball/48be2b0653594eea32dcef130cca1c811dcf25c2",
+ "reference": "48be2b0653594eea32dcef130cca1c811dcf25c2",
"shasum": ""
},
"require": {
"php": ">=8.2",
"psr/log": "^1|^2|^3",
- "symfony/var-dumper": "^6.4|^7.0"
+ "symfony/polyfill-php85": "^1.32",
+ "symfony/var-dumper": "^6.4|^7.0|^8.0"
},
"conflict": {
"symfony/deprecation-contracts": "<2.5",
"symfony/http-kernel": "<6.4"
},
"require-dev": {
- "symfony/console": "^6.4|^7.0",
+ "symfony/console": "^6.4|^7.0|^8.0",
"symfony/deprecation-contracts": "^2.5|^3",
- "symfony/http-kernel": "^6.4|^7.0",
- "symfony/serializer": "^6.4|^7.0",
+ "symfony/http-kernel": "^6.4|^7.0|^8.0",
+ "symfony/serializer": "^6.4|^7.0|^8.0",
"symfony/webpack-encore-bundle": "^1.0|^2.0"
},
"bin": [
@@ -4375,7 +4426,7 @@
"description": "Provides tools to manage errors and ease debugging PHP code",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/error-handler/tree/v7.3.0"
+ "source": "https://github.com/symfony/error-handler/tree/v7.4.0"
},
"funding": [
{
@@ -4386,25 +4437,29 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2025-05-29T07:19:49+00:00"
+ "time": "2025-11-05T14:29:59+00:00"
},
{
"name": "symfony/event-dispatcher",
- "version": "v7.3.0",
+ "version": "v7.4.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/event-dispatcher.git",
- "reference": "497f73ac996a598c92409b44ac43b6690c4f666d"
+ "reference": "9dddcddff1ef974ad87b3708e4b442dc38b2261d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/497f73ac996a598c92409b44ac43b6690c4f666d",
- "reference": "497f73ac996a598c92409b44ac43b6690c4f666d",
+ "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/9dddcddff1ef974ad87b3708e4b442dc38b2261d",
+ "reference": "9dddcddff1ef974ad87b3708e4b442dc38b2261d",
"shasum": ""
},
"require": {
@@ -4421,13 +4476,14 @@
},
"require-dev": {
"psr/log": "^1|^2|^3",
- "symfony/config": "^6.4|^7.0",
- "symfony/dependency-injection": "^6.4|^7.0",
- "symfony/error-handler": "^6.4|^7.0",
- "symfony/expression-language": "^6.4|^7.0",
- "symfony/http-foundation": "^6.4|^7.0",
+ "symfony/config": "^6.4|^7.0|^8.0",
+ "symfony/dependency-injection": "^6.4|^7.0|^8.0",
+ "symfony/error-handler": "^6.4|^7.0|^8.0",
+ "symfony/expression-language": "^6.4|^7.0|^8.0",
+ "symfony/framework-bundle": "^6.4|^7.0|^8.0",
+ "symfony/http-foundation": "^6.4|^7.0|^8.0",
"symfony/service-contracts": "^2.5|^3",
- "symfony/stopwatch": "^6.4|^7.0"
+ "symfony/stopwatch": "^6.4|^7.0|^8.0"
},
"type": "library",
"autoload": {
@@ -4455,7 +4511,7 @@
"description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/event-dispatcher/tree/v7.3.0"
+ "source": "https://github.com/symfony/event-dispatcher/tree/v7.4.0"
},
"funding": [
{
@@ -4466,12 +4522,16 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2025-04-22T09:11:45+00:00"
+ "time": "2025-10-28T09:38:46+00:00"
},
{
"name": "symfony/event-dispatcher-contracts",
@@ -4551,16 +4611,16 @@
},
{
"name": "symfony/filesystem",
- "version": "v7.3.0",
+ "version": "v7.4.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/filesystem.git",
- "reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb"
+ "reference": "d551b38811096d0be9c4691d406991b47c0c630a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/filesystem/zipball/b8dce482de9d7c9fe2891155035a7248ab5c7fdb",
- "reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb",
+ "url": "https://api.github.com/repos/symfony/filesystem/zipball/d551b38811096d0be9c4691d406991b47c0c630a",
+ "reference": "d551b38811096d0be9c4691d406991b47c0c630a",
"shasum": ""
},
"require": {
@@ -4569,7 +4629,7 @@
"symfony/polyfill-mbstring": "~1.8"
},
"require-dev": {
- "symfony/process": "^6.4|^7.0"
+ "symfony/process": "^6.4|^7.0|^8.0"
},
"type": "library",
"autoload": {
@@ -4597,7 +4657,7 @@
"description": "Provides basic utilities for the filesystem",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/filesystem/tree/v7.3.0"
+ "source": "https://github.com/symfony/filesystem/tree/v7.4.0"
},
"funding": [
{
@@ -4608,32 +4668,36 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2024-10-25T15:15:23+00:00"
+ "time": "2025-11-27T13:27:24+00:00"
},
{
"name": "symfony/finder",
- "version": "v7.3.0",
+ "version": "v7.4.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
- "reference": "ec2344cf77a48253bbca6939aa3d2477773ea63d"
+ "reference": "340b9ed7320570f319028a2cbec46d40535e94bd"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/finder/zipball/ec2344cf77a48253bbca6939aa3d2477773ea63d",
- "reference": "ec2344cf77a48253bbca6939aa3d2477773ea63d",
+ "url": "https://api.github.com/repos/symfony/finder/zipball/340b9ed7320570f319028a2cbec46d40535e94bd",
+ "reference": "340b9ed7320570f319028a2cbec46d40535e94bd",
"shasum": ""
},
"require": {
"php": ">=8.2"
},
"require-dev": {
- "symfony/filesystem": "^6.4|^7.0"
+ "symfony/filesystem": "^6.4|^7.0|^8.0"
},
"type": "library",
"autoload": {
@@ -4661,7 +4725,7 @@
"description": "Finds files and directories via an intuitive fluent interface",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/finder/tree/v7.3.0"
+ "source": "https://github.com/symfony/finder/tree/v7.4.0"
},
"funding": [
{
@@ -4672,32 +4736,35 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2024-12-30T19:00:26+00:00"
+ "time": "2025-11-05T05:42:40+00:00"
},
{
"name": "symfony/http-foundation",
- "version": "v7.3.0",
+ "version": "v7.4.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-foundation.git",
- "reference": "4236baf01609667d53b20371486228231eb135fd"
+ "reference": "bd1af1e425811d6f077db240c3a588bdb405cd27"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/http-foundation/zipball/4236baf01609667d53b20371486228231eb135fd",
- "reference": "4236baf01609667d53b20371486228231eb135fd",
+ "url": "https://api.github.com/repos/symfony/http-foundation/zipball/bd1af1e425811d6f077db240c3a588bdb405cd27",
+ "reference": "bd1af1e425811d6f077db240c3a588bdb405cd27",
"shasum": ""
},
"require": {
"php": ">=8.2",
- "symfony/deprecation-contracts": "^2.5|^3.0",
- "symfony/polyfill-mbstring": "~1.1",
- "symfony/polyfill-php83": "^1.27"
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/polyfill-mbstring": "^1.1"
},
"conflict": {
"doctrine/dbal": "<3.6",
@@ -4706,13 +4773,13 @@
"require-dev": {
"doctrine/dbal": "^3.6|^4",
"predis/predis": "^1.1|^2.0",
- "symfony/cache": "^6.4.12|^7.1.5",
- "symfony/clock": "^6.4|^7.0",
- "symfony/dependency-injection": "^6.4|^7.0",
- "symfony/expression-language": "^6.4|^7.0",
- "symfony/http-kernel": "^6.4|^7.0",
- "symfony/mime": "^6.4|^7.0",
- "symfony/rate-limiter": "^6.4|^7.0"
+ "symfony/cache": "^6.4.12|^7.1.5|^8.0",
+ "symfony/clock": "^6.4|^7.0|^8.0",
+ "symfony/dependency-injection": "^6.4|^7.0|^8.0",
+ "symfony/expression-language": "^6.4|^7.0|^8.0",
+ "symfony/http-kernel": "^6.4|^7.0|^8.0",
+ "symfony/mime": "^6.4|^7.0|^8.0",
+ "symfony/rate-limiter": "^6.4|^7.0|^8.0"
},
"type": "library",
"autoload": {
@@ -4740,7 +4807,7 @@
"description": "Defines an object-oriented layer for the HTTP specification",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/http-foundation/tree/v7.3.0"
+ "source": "https://github.com/symfony/http-foundation/tree/v7.4.1"
},
"funding": [
{
@@ -4751,34 +4818,38 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2025-05-12T14:48:23+00:00"
+ "time": "2025-12-07T11:13:10+00:00"
},
{
"name": "symfony/http-kernel",
- "version": "v7.3.0",
+ "version": "v7.4.2",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-kernel.git",
- "reference": "ac7b8e163e8c83dce3abcc055a502d4486051a9f"
+ "reference": "f6e6f0a5fa8763f75a504b930163785fb6dd055f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/http-kernel/zipball/ac7b8e163e8c83dce3abcc055a502d4486051a9f",
- "reference": "ac7b8e163e8c83dce3abcc055a502d4486051a9f",
+ "url": "https://api.github.com/repos/symfony/http-kernel/zipball/f6e6f0a5fa8763f75a504b930163785fb6dd055f",
+ "reference": "f6e6f0a5fa8763f75a504b930163785fb6dd055f",
"shasum": ""
},
"require": {
"php": ">=8.2",
"psr/log": "^1|^2|^3",
"symfony/deprecation-contracts": "^2.5|^3",
- "symfony/error-handler": "^6.4|^7.0",
- "symfony/event-dispatcher": "^7.3",
- "symfony/http-foundation": "^7.3",
+ "symfony/error-handler": "^6.4|^7.0|^8.0",
+ "symfony/event-dispatcher": "^7.3|^8.0",
+ "symfony/http-foundation": "^7.4|^8.0",
"symfony/polyfill-ctype": "^1.8"
},
"conflict": {
@@ -4788,6 +4859,7 @@
"symfony/console": "<6.4",
"symfony/dependency-injection": "<6.4",
"symfony/doctrine-bridge": "<6.4",
+ "symfony/flex": "<2.10",
"symfony/form": "<6.4",
"symfony/http-client": "<6.4",
"symfony/http-client-contracts": "<2.5",
@@ -4805,27 +4877,27 @@
},
"require-dev": {
"psr/cache": "^1.0|^2.0|^3.0",
- "symfony/browser-kit": "^6.4|^7.0",
- "symfony/clock": "^6.4|^7.0",
- "symfony/config": "^6.4|^7.0",
- "symfony/console": "^6.4|^7.0",
- "symfony/css-selector": "^6.4|^7.0",
- "symfony/dependency-injection": "^6.4|^7.0",
- "symfony/dom-crawler": "^6.4|^7.0",
- "symfony/expression-language": "^6.4|^7.0",
- "symfony/finder": "^6.4|^7.0",
+ "symfony/browser-kit": "^6.4|^7.0|^8.0",
+ "symfony/clock": "^6.4|^7.0|^8.0",
+ "symfony/config": "^6.4|^7.0|^8.0",
+ "symfony/console": "^6.4|^7.0|^8.0",
+ "symfony/css-selector": "^6.4|^7.0|^8.0",
+ "symfony/dependency-injection": "^6.4|^7.0|^8.0",
+ "symfony/dom-crawler": "^6.4|^7.0|^8.0",
+ "symfony/expression-language": "^6.4|^7.0|^8.0",
+ "symfony/finder": "^6.4|^7.0|^8.0",
"symfony/http-client-contracts": "^2.5|^3",
- "symfony/process": "^6.4|^7.0",
- "symfony/property-access": "^7.1",
- "symfony/routing": "^6.4|^7.0",
- "symfony/serializer": "^7.1",
- "symfony/stopwatch": "^6.4|^7.0",
- "symfony/translation": "^6.4|^7.0",
+ "symfony/process": "^6.4|^7.0|^8.0",
+ "symfony/property-access": "^7.1|^8.0",
+ "symfony/routing": "^6.4|^7.0|^8.0",
+ "symfony/serializer": "^7.1|^8.0",
+ "symfony/stopwatch": "^6.4|^7.0|^8.0",
+ "symfony/translation": "^6.4|^7.0|^8.0",
"symfony/translation-contracts": "^2.5|^3",
- "symfony/uid": "^6.4|^7.0",
- "symfony/validator": "^6.4|^7.0",
- "symfony/var-dumper": "^6.4|^7.0",
- "symfony/var-exporter": "^6.4|^7.0",
+ "symfony/uid": "^6.4|^7.0|^8.0",
+ "symfony/validator": "^6.4|^7.0|^8.0",
+ "symfony/var-dumper": "^6.4|^7.0|^8.0",
+ "symfony/var-exporter": "^6.4|^7.0|^8.0",
"twig/twig": "^3.12"
},
"type": "library",
@@ -4854,7 +4926,7 @@
"description": "Provides a structured process for converting a Request into a Response",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/http-kernel/tree/v7.3.0"
+ "source": "https://github.com/symfony/http-kernel/tree/v7.4.2"
},
"funding": [
{
@@ -4865,29 +4937,34 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2025-05-29T07:47:32+00:00"
+ "time": "2025-12-08T07:43:37+00:00"
},
{
"name": "symfony/mime",
- "version": "v7.3.0",
+ "version": "v7.4.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/mime.git",
- "reference": "0e7b19b2f399c31df0cdbe5d8cbf53f02f6cfcd9"
+ "reference": "bdb02729471be5d047a3ac4a69068748f1a6be7a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/mime/zipball/0e7b19b2f399c31df0cdbe5d8cbf53f02f6cfcd9",
- "reference": "0e7b19b2f399c31df0cdbe5d8cbf53f02f6cfcd9",
+ "url": "https://api.github.com/repos/symfony/mime/zipball/bdb02729471be5d047a3ac4a69068748f1a6be7a",
+ "reference": "bdb02729471be5d047a3ac4a69068748f1a6be7a",
"shasum": ""
},
"require": {
"php": ">=8.2",
+ "symfony/deprecation-contracts": "^2.5|^3",
"symfony/polyfill-intl-idn": "^1.10",
"symfony/polyfill-mbstring": "^1.0"
},
@@ -4902,11 +4979,11 @@
"egulias/email-validator": "^2.1.10|^3.1|^4",
"league/html-to-markdown": "^5.0",
"phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0",
- "symfony/dependency-injection": "^6.4|^7.0",
- "symfony/process": "^6.4|^7.0",
- "symfony/property-access": "^6.4|^7.0",
- "symfony/property-info": "^6.4|^7.0",
- "symfony/serializer": "^6.4.3|^7.0.3"
+ "symfony/dependency-injection": "^6.4|^7.0|^8.0",
+ "symfony/process": "^6.4|^7.0|^8.0",
+ "symfony/property-access": "^6.4|^7.0|^8.0",
+ "symfony/property-info": "^6.4|^7.0|^8.0",
+ "symfony/serializer": "^6.4.3|^7.0.3|^8.0"
},
"type": "library",
"autoload": {
@@ -4938,7 +5015,7 @@
"mime-type"
],
"support": {
- "source": "https://github.com/symfony/mime/tree/v7.3.0"
+ "source": "https://github.com/symfony/mime/tree/v7.4.0"
},
"funding": [
{
@@ -4949,16 +5026,20 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2025-02-19T08:51:26+00:00"
+ "time": "2025-11-16T10:14:42+00:00"
},
{
"name": "symfony/polyfill-ctype",
- "version": "v1.32.0",
+ "version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
@@ -5017,7 +5098,7 @@
"portable"
],
"support": {
- "source": "https://github.com/symfony/polyfill-ctype/tree/v1.32.0"
+ "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0"
},
"funding": [
{
@@ -5028,6 +5109,10 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
@@ -5037,16 +5122,16 @@
},
{
"name": "symfony/polyfill-intl-grapheme",
- "version": "v1.32.0",
+ "version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-grapheme.git",
- "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe"
+ "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe",
- "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe",
+ "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70",
+ "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70",
"shasum": ""
},
"require": {
@@ -5095,7 +5180,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.32.0"
+ "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0"
},
"funding": [
{
@@ -5106,16 +5191,20 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2024-09-09T11:45:10+00:00"
+ "time": "2025-06-27T09:58:17+00:00"
},
{
"name": "symfony/polyfill-intl-idn",
- "version": "v1.32.0",
+ "version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-idn.git",
@@ -5178,7 +5267,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.32.0"
+ "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.33.0"
},
"funding": [
{
@@ -5189,6 +5278,10 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
@@ -5198,7 +5291,7 @@
},
{
"name": "symfony/polyfill-intl-normalizer",
- "version": "v1.32.0",
+ "version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
@@ -5259,7 +5352,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0"
+ "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0"
},
"funding": [
{
@@ -5270,6 +5363,10 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
@@ -5279,7 +5376,7 @@
},
{
"name": "symfony/polyfill-mbstring",
- "version": "v1.32.0",
+ "version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
@@ -5340,7 +5437,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0"
+ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0"
},
"funding": [
{
@@ -5351,6 +5448,10 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
@@ -5360,7 +5461,7 @@
},
{
"name": "symfony/polyfill-php80",
- "version": "v1.32.0",
+ "version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
@@ -5420,7 +5521,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-php80/tree/v1.32.0"
+ "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0"
},
"funding": [
{
@@ -5431,6 +5532,10 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
@@ -5440,16 +5545,16 @@
},
{
"name": "symfony/polyfill-php83",
- "version": "v1.32.0",
+ "version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php83.git",
- "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491"
+ "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/2fb86d65e2d424369ad2905e83b236a8805ba491",
- "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491",
+ "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5",
+ "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5",
"shasum": ""
},
"require": {
@@ -5496,7 +5601,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-php83/tree/v1.32.0"
+ "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0"
},
"funding": [
{
@@ -5507,25 +5612,189 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2024-09-09T11:45:10+00:00"
+ "time": "2025-07-08T02:45:35+00:00"
},
{
- "name": "symfony/service-contracts",
- "version": "v3.6.0",
+ "name": "symfony/polyfill-php84",
+ "version": "v1.33.0",
"source": {
"type": "git",
- "url": "https://github.com/symfony/service-contracts.git",
- "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4"
+ "url": "https://github.com/symfony/polyfill-php84.git",
+ "reference": "d8ced4d875142b6a7426000426b8abc631d6b191"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4",
- "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4",
+ "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191",
+ "reference": "d8ced4d875142b6a7426000426b8abc631d6b191",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Php84\\": ""
+ },
+ "classmap": [
+ "Resources/stubs"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-php84/tree/v1.33.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-06-24T13:30:11+00:00"
+ },
+ {
+ "name": "symfony/polyfill-php85",
+ "version": "v1.33.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-php85.git",
+ "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91",
+ "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Php85\\": ""
+ },
+ "classmap": [
+ "Resources/stubs"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-php85/tree/v1.33.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-06-23T16:12:55+00:00"
+ },
+ {
+ "name": "symfony/service-contracts",
+ "version": "v3.6.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/service-contracts.git",
+ "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43",
+ "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43",
"shasum": ""
},
"require": {
@@ -5579,7 +5848,7 @@
"standards"
],
"support": {
- "source": "https://github.com/symfony/service-contracts/tree/v3.6.0"
+ "source": "https://github.com/symfony/service-contracts/tree/v3.6.1"
},
"funding": [
{
@@ -5590,31 +5859,36 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2025-04-25T09:37:31+00:00"
+ "time": "2025-07-15T11:30:57+00:00"
},
{
"name": "symfony/string",
- "version": "v7.3.0",
+ "version": "v7.4.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
- "reference": "f3570b8c61ca887a9e2938e85cb6458515d2b125"
+ "reference": "d50e862cb0a0e0886f73ca1f31b865efbb795003"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/string/zipball/f3570b8c61ca887a9e2938e85cb6458515d2b125",
- "reference": "f3570b8c61ca887a9e2938e85cb6458515d2b125",
+ "url": "https://api.github.com/repos/symfony/string/zipball/d50e862cb0a0e0886f73ca1f31b865efbb795003",
+ "reference": "d50e862cb0a0e0886f73ca1f31b865efbb795003",
"shasum": ""
},
"require": {
"php": ">=8.2",
+ "symfony/deprecation-contracts": "^2.5|^3.0",
"symfony/polyfill-ctype": "~1.8",
- "symfony/polyfill-intl-grapheme": "~1.0",
+ "symfony/polyfill-intl-grapheme": "~1.33",
"symfony/polyfill-intl-normalizer": "~1.0",
"symfony/polyfill-mbstring": "~1.0"
},
@@ -5622,12 +5896,11 @@
"symfony/translation-contracts": "<2.5"
},
"require-dev": {
- "symfony/emoji": "^7.1",
- "symfony/error-handler": "^6.4|^7.0",
- "symfony/http-client": "^6.4|^7.0",
- "symfony/intl": "^6.4|^7.0",
+ "symfony/emoji": "^7.1|^8.0",
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/intl": "^6.4|^7.0|^8.0",
"symfony/translation-contracts": "^2.5|^3.0",
- "symfony/var-exporter": "^6.4|^7.0"
+ "symfony/var-exporter": "^6.4|^7.0|^8.0"
},
"type": "library",
"autoload": {
@@ -5666,7 +5939,7 @@
"utf8"
],
"support": {
- "source": "https://github.com/symfony/string/tree/v7.3.0"
+ "source": "https://github.com/symfony/string/tree/v7.4.0"
},
"funding": [
{
@@ -5677,38 +5950,36 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2025-04-20T20:19:01+00:00"
+ "time": "2025-11-27T13:27:24+00:00"
},
{
"name": "symfony/translation",
- "version": "v7.3.0",
+ "version": "v7.4.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/translation.git",
- "reference": "4aba29076a29a3aa667e09b791e5f868973a8667"
+ "reference": "2d01ca0da3f092f91eeedb46f24aa30d2fca8f68"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/translation/zipball/4aba29076a29a3aa667e09b791e5f868973a8667",
- "reference": "4aba29076a29a3aa667e09b791e5f868973a8667",
- "shasum": "",
- "mirrors": [
- {
- "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
- "preferred": true
- }
- ]
+ "url": "https://api.github.com/repos/symfony/translation/zipball/2d01ca0da3f092f91eeedb46f24aa30d2fca8f68",
+ "reference": "2d01ca0da3f092f91eeedb46f24aa30d2fca8f68",
+ "shasum": ""
},
"require": {
"php": ">=8.2",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/polyfill-mbstring": "~1.0",
- "symfony/translation-contracts": "^2.5|^3.0"
+ "symfony/translation-contracts": "^2.5.3|^3.3"
},
"conflict": {
"nikic/php-parser": "<5.0",
@@ -5727,17 +5998,17 @@
"require-dev": {
"nikic/php-parser": "^5.0",
"psr/log": "^1|^2|^3",
- "symfony/config": "^6.4|^7.0",
- "symfony/console": "^6.4|^7.0",
- "symfony/dependency-injection": "^6.4|^7.0",
- "symfony/finder": "^6.4|^7.0",
+ "symfony/config": "^6.4|^7.0|^8.0",
+ "symfony/console": "^6.4|^7.0|^8.0",
+ "symfony/dependency-injection": "^6.4|^7.0|^8.0",
+ "symfony/finder": "^6.4|^7.0|^8.0",
"symfony/http-client-contracts": "^2.5|^3.0",
- "symfony/http-kernel": "^6.4|^7.0",
- "symfony/intl": "^6.4|^7.0",
+ "symfony/http-kernel": "^6.4|^7.0|^8.0",
+ "symfony/intl": "^6.4|^7.0|^8.0",
"symfony/polyfill-intl-icu": "^1.21",
- "symfony/routing": "^6.4|^7.0",
+ "symfony/routing": "^6.4|^7.0|^8.0",
"symfony/service-contracts": "^2.5|^3",
- "symfony/yaml": "^6.4|^7.0"
+ "symfony/yaml": "^6.4|^7.0|^8.0"
},
"type": "library",
"autoload": {
@@ -5768,7 +6039,7 @@
"description": "Provides tools to internationalize your application",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/translation/tree/v7.3.0"
+ "source": "https://github.com/symfony/translation/tree/v7.4.0"
},
"funding": [
{
@@ -5779,25 +6050,29 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2025-05-29T07:19:49+00:00"
+ "time": "2025-11-27T13:27:24+00:00"
},
{
"name": "symfony/translation-contracts",
- "version": "v3.6.0",
+ "version": "v3.6.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/translation-contracts.git",
- "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d"
+ "reference": "65a8bc82080447fae78373aa10f8d13b38338977"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/df210c7a2573f1913b2d17cc95f90f53a73d8f7d",
- "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d",
+ "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/65a8bc82080447fae78373aa10f8d13b38338977",
+ "reference": "65a8bc82080447fae78373aa10f8d13b38338977",
"shasum": ""
},
"require": {
@@ -5846,7 +6121,7 @@
"standards"
],
"support": {
- "source": "https://github.com/symfony/translation-contracts/tree/v3.6.0"
+ "source": "https://github.com/symfony/translation-contracts/tree/v3.6.1"
},
"funding": [
{
@@ -5857,25 +6132,29 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2024-09-27T08:32:26+00:00"
+ "time": "2025-07-15T13:41:35+00:00"
},
{
"name": "symfony/var-dumper",
- "version": "v7.3.0",
+ "version": "v7.4.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-dumper.git",
- "reference": "548f6760c54197b1084e1e5c71f6d9d523f2f78e"
+ "reference": "41fd6c4ae28c38b294b42af6db61446594a0dece"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/var-dumper/zipball/548f6760c54197b1084e1e5c71f6d9d523f2f78e",
- "reference": "548f6760c54197b1084e1e5c71f6d9d523f2f78e",
+ "url": "https://api.github.com/repos/symfony/var-dumper/zipball/41fd6c4ae28c38b294b42af6db61446594a0dece",
+ "reference": "41fd6c4ae28c38b294b42af6db61446594a0dece",
"shasum": ""
},
"require": {
@@ -5887,11 +6166,10 @@
"symfony/console": "<6.4"
},
"require-dev": {
- "ext-iconv": "*",
- "symfony/console": "^6.4|^7.0",
- "symfony/http-kernel": "^6.4|^7.0",
- "symfony/process": "^6.4|^7.0",
- "symfony/uid": "^6.4|^7.0",
+ "symfony/console": "^6.4|^7.0|^8.0",
+ "symfony/http-kernel": "^6.4|^7.0|^8.0",
+ "symfony/process": "^6.4|^7.0|^8.0",
+ "symfony/uid": "^6.4|^7.0|^8.0",
"twig/twig": "^3.12"
},
"bin": [
@@ -5930,7 +6208,7 @@
"dump"
],
"support": {
- "source": "https://github.com/symfony/var-dumper/tree/v7.3.0"
+ "source": "https://github.com/symfony/var-dumper/tree/v7.4.0"
},
"funding": [
{
@@ -5941,25 +6219,29 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2025-04-27T18:39:23+00:00"
+ "time": "2025-10-27T20:36:44+00:00"
},
{
"name": "symfony/var-exporter",
- "version": "v7.3.0",
+ "version": "v7.4.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-exporter.git",
- "reference": "c9a1168891b5aaadfd6332ef44393330b3498c4c"
+ "reference": "03a60f169c79a28513a78c967316fbc8bf17816f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/var-exporter/zipball/c9a1168891b5aaadfd6332ef44393330b3498c4c",
- "reference": "c9a1168891b5aaadfd6332ef44393330b3498c4c",
+ "url": "https://api.github.com/repos/symfony/var-exporter/zipball/03a60f169c79a28513a78c967316fbc8bf17816f",
+ "reference": "03a60f169c79a28513a78c967316fbc8bf17816f",
"shasum": ""
},
"require": {
@@ -5967,9 +6249,9 @@
"symfony/deprecation-contracts": "^2.5|^3"
},
"require-dev": {
- "symfony/property-access": "^6.4|^7.0",
- "symfony/serializer": "^6.4|^7.0",
- "symfony/var-dumper": "^6.4|^7.0"
+ "symfony/property-access": "^6.4|^7.0|^8.0",
+ "symfony/serializer": "^6.4|^7.0|^8.0",
+ "symfony/var-dumper": "^6.4|^7.0|^8.0"
},
"type": "library",
"autoload": {
@@ -6007,7 +6289,7 @@
"serialize"
],
"support": {
- "source": "https://github.com/symfony/var-exporter/tree/v7.3.0"
+ "source": "https://github.com/symfony/var-exporter/tree/v7.4.0"
},
"funding": [
{
@@ -6018,12 +6300,16 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2025-05-15T09:04:05+00:00"
+ "time": "2025-09-11T10:15:23+00:00"
},
{
"name": "topthink/think-container",
@@ -6358,22 +6644,22 @@
},
{
"name": "webman/console",
- "version": "v2.1.8",
+ "version": "v2.1.10",
"source": {
"type": "git",
"url": "https://github.com/webman-php/console.git",
- "reference": "2d4ce527810f91e1b4f583ec90eae24a0334ab10"
+ "reference": "6b24bb77d04588c7c3b235087073e69ddfdaa773"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/webman-php/console/zipball/2d4ce527810f91e1b4f583ec90eae24a0334ab10",
- "reference": "2d4ce527810f91e1b4f583ec90eae24a0334ab10",
+ "url": "https://api.github.com/repos/webman-php/console/zipball/6b24bb77d04588c7c3b235087073e69ddfdaa773",
+ "reference": "6b24bb77d04588c7c3b235087073e69ddfdaa773",
"shasum": ""
},
"require": {
"doctrine/inflector": "^2.0",
"php": ">=8.1",
- "symfony/console": ">=6.0"
+ "symfony/console": "^6.0 || ^7.0"
},
"require-dev": {
"workerman/webman": "^2.1"
@@ -6408,20 +6694,20 @@
"source": "https://github.com/webman-php/console",
"wiki": "http://www.workerman.net/doc/webman"
},
- "time": "2025-06-02T00:42:16+00:00"
+ "time": "2025-12-08T08:23:12+00:00"
},
{
"name": "webman/database",
- "version": "v2.1.6",
+ "version": "v2.1.7",
"source": {
"type": "git",
"url": "https://github.com/webman-php/database.git",
- "reference": "943c5a0f92d8959d9160c2ee3925112b15c308c8"
+ "reference": "81a8a834cc8db5a5dbb46fe0073a9d5ac73848f6"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/webman-php/database/zipball/943c5a0f92d8959d9160c2ee3925112b15c308c8",
- "reference": "943c5a0f92d8959d9160c2ee3925112b15c308c8",
+ "url": "https://api.github.com/repos/webman-php/database/zipball/81a8a834cc8db5a5dbb46fe0073a9d5ac73848f6",
+ "reference": "81a8a834cc8db5a5dbb46fe0073a9d5ac73848f6",
"shasum": ""
},
"require": {
@@ -6444,9 +6730,9 @@
"description": "Webman database",
"support": {
"issues": "https://github.com/webman-php/database/issues",
- "source": "https://github.com/webman-php/database/tree/v2.1.6"
+ "source": "https://github.com/webman-php/database/tree/v2.1.7"
},
- "time": "2025-04-04T13:23:25+00:00"
+ "time": "2025-11-27T08:09:25+00:00"
},
{
"name": "webman/gateway-worker",
@@ -6481,6 +6767,40 @@
},
"time": "2024-04-30T01:05:42+00:00"
},
+ {
+ "name": "webman/log",
+ "version": "v2.1.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/webman-php/log.git",
+ "reference": "14e3f3e64e87783952417a68cf5205c1293aba69"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/webman-php/log/zipball/14e3f3e64e87783952417a68cf5205c1293aba69",
+ "reference": "14e3f3e64e87783952417a68cf5205c1293aba69",
+ "shasum": ""
+ },
+ "require": {
+ "workerman/webman-framework": "^2.1"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Webman\\Log\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Webman plugin webman/log",
+ "support": {
+ "issues": "https://github.com/webman-php/log/issues",
+ "source": "https://github.com/webman-php/log/tree/v2.1.3"
+ },
+ "time": "2025-02-26T11:43:36+00:00"
+ },
{
"name": "webman/redis",
"version": "v2.1.3",
@@ -6519,16 +6839,16 @@
},
{
"name": "webman/redis-queue",
- "version": "v2.1.0",
+ "version": "v2.1.1",
"source": {
"type": "git",
"url": "https://github.com/webman-php/redis-queue.git",
- "reference": "12e1c9915ec64c91c0c9c255606bd0c7de27334c"
+ "reference": "ff4791e21f3c324a47e21da7b6f2dae5a7311dcb"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/webman-php/redis-queue/zipball/12e1c9915ec64c91c0c9c255606bd0c7de27334c",
- "reference": "12e1c9915ec64c91c0c9c255606bd0c7de27334c",
+ "url": "https://api.github.com/repos/webman-php/redis-queue/zipball/ff4791e21f3c324a47e21da7b6f2dae5a7311dcb",
+ "reference": "ff4791e21f3c324a47e21da7b6f2dae5a7311dcb",
"shasum": ""
},
"require": {
@@ -6547,22 +6867,22 @@
"description": "Redis message queue plugin for webman.",
"support": {
"issues": "https://github.com/webman-php/redis-queue/issues",
- "source": "https://github.com/webman-php/redis-queue/tree/v2.1.0"
+ "source": "https://github.com/webman-php/redis-queue/tree/v2.1.1"
},
- "time": "2025-01-24T12:56:01+00:00"
+ "time": "2025-11-14T07:12:52+00:00"
},
{
"name": "workerman/coroutine",
- "version": "v1.1.3",
+ "version": "v1.1.4",
"source": {
"type": "git",
"url": "https://github.com/workerman-php/coroutine.git",
- "reference": "df8fc428967d512a74a8a7d80355c1d40228c9fa"
+ "reference": "b0bebfa9d41b992ad0a835ddf2ee8fa5d58eca44"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/workerman-php/coroutine/zipball/df8fc428967d512a74a8a7d80355c1d40228c9fa",
- "reference": "df8fc428967d512a74a8a7d80355c1d40228c9fa",
+ "url": "https://api.github.com/repos/workerman-php/coroutine/zipball/b0bebfa9d41b992ad0a835ddf2ee8fa5d58eca44",
+ "reference": "b0bebfa9d41b992ad0a835ddf2ee8fa5d58eca44",
"shasum": ""
},
"require": {
@@ -6586,9 +6906,9 @@
"description": "Workerman coroutine",
"support": {
"issues": "https://github.com/workerman-php/coroutine/issues",
- "source": "https://github.com/workerman-php/coroutine/tree/v1.1.3"
+ "source": "https://github.com/workerman-php/coroutine/tree/v1.1.4"
},
- "time": "2025-02-17T03:34:21+00:00"
+ "time": "2025-10-11T15:09:08+00:00"
},
{
"name": "workerman/crontab",
@@ -6696,23 +7016,17 @@
},
{
"name": "workerman/redis",
- "version": "v2.0.4",
+ "version": "v2.0.5",
"source": {
"type": "git",
"url": "https://github.com/walkor/redis.git",
- "reference": "82b398c5bdd5d683a3458aaf645b159b7b39b84e"
+ "reference": "49627c1809eff1ef7175eb8ee7549234a1d67ec5"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/walkor/redis/zipball/82b398c5bdd5d683a3458aaf645b159b7b39b84e",
- "reference": "82b398c5bdd5d683a3458aaf645b159b7b39b84e",
- "shasum": "",
- "mirrors": [
- {
- "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
- "preferred": true
- }
- ]
+ "url": "https://api.github.com/repos/walkor/redis/zipball/49627c1809eff1ef7175eb8ee7549234a1d67ec5",
+ "reference": "49627c1809eff1ef7175eb8ee7549234a1d67ec5",
+ "shasum": ""
},
"require": {
"php": ">=7",
@@ -6731,9 +7045,9 @@
"homepage": "http://www.workerman.net",
"support": {
"issues": "https://github.com/walkor/redis/issues",
- "source": "https://github.com/walkor/redis/tree/v2.0.4"
+ "source": "https://github.com/walkor/redis/tree/v2.0.5"
},
- "time": "2025-04-05T07:45:12+00:00"
+ "time": "2025-04-07T01:58:58+00:00"
},
{
"name": "workerman/redis-queue",
@@ -6747,13 +7061,7 @@
"type": "zip",
"url": "https://api.github.com/repos/walkor/redis-queue/zipball/75dbf7ed2ea228c45dc0df82c0fea35879b715d0",
"reference": "75dbf7ed2ea228c45dc0df82c0fea35879b715d0",
- "shasum": "",
- "mirrors": [
- {
- "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
- "preferred": true
- }
- ]
+ "shasum": ""
},
"require": {
"php": ">=7.0",
@@ -6780,16 +7088,16 @@
},
{
"name": "workerman/webman-framework",
- "version": "v2.1.2",
+ "version": "v2.1.4",
"source": {
"type": "git",
"url": "https://github.com/walkor/webman-framework.git",
- "reference": "f803bd867f07bb0929faef060b59a19a44186bfc"
+ "reference": "f19d1f9e47cc50210555f0b63db5ae1dd584f793"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/walkor/webman-framework/zipball/f803bd867f07bb0929faef060b59a19a44186bfc",
- "reference": "f803bd867f07bb0929faef060b59a19a44186bfc",
+ "url": "https://api.github.com/repos/walkor/webman-framework/zipball/f19d1f9e47cc50210555f0b63db5ae1dd584f793",
+ "reference": "f19d1f9e47cc50210555f0b63db5ae1dd584f793",
"shasum": ""
},
"require": {
@@ -6797,7 +7105,7 @@
"nikic/fast-route": "^1.3",
"php": ">=8.1",
"psr/container": ">=1.0",
- "psr/log": "^3.0",
+ "psr/log": "^2.0 || ^3.0",
"workerman/workerman": "^5.1 || dev-master"
},
"suggest": {
@@ -6842,20 +7150,20 @@
"source": "https://github.com/walkor/webman-framework",
"wiki": "https://doc.workerman.net/"
},
- "time": "2025-03-10T11:52:22+00:00"
+ "time": "2025-11-10T06:59:23+00:00"
},
{
"name": "workerman/workerman",
- "version": "v5.1.3",
+ "version": "v5.1.6",
"source": {
"type": "git",
"url": "https://github.com/walkor/workerman.git",
- "reference": "371f3a5decb28f1bd3464ae26d47ea1a4cf0a3c5"
+ "reference": "9b83bdf9fd0eaff9419a240b47da870447583112"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/walkor/workerman/zipball/371f3a5decb28f1bd3464ae26d47ea1a4cf0a3c5",
- "reference": "371f3a5decb28f1bd3464ae26d47ea1a4cf0a3c5",
+ "url": "https://api.github.com/repos/walkor/workerman/zipball/9b83bdf9fd0eaff9419a240b47da870447583112",
+ "reference": "9b83bdf9fd0eaff9419a240b47da870447583112",
"shasum": ""
},
"require": {
@@ -6868,9 +7176,9 @@
},
"require-dev": {
"guzzlehttp/guzzle": "^7.0",
- "mockery/mockery": "^1.6",
+ "mockery/mockery": "2.0.x-dev",
"pestphp/pest": "2.x-dev",
- "phpstan/phpstan": "1.11.x-dev"
+ "phpstan/phpstan": "2.1.x-dev"
},
"suggest": {
"ext-event": "For better performance. "
@@ -6918,28 +7226,31 @@
"type": "patreon"
}
],
- "time": "2025-06-12T13:34:04+00:00"
+ "time": "2025-11-12T01:36:20+00:00"
},
{
"name": "zoujingli/ip2region",
- "version": "v3.0.0",
+ "version": "v3.0.8",
"source": {
"type": "git",
"url": "https://github.com/zoujingli/ip2region.git",
- "reference": "fd31d7a023f28f4e2f3e63f46f1fd51f8b2672f6"
+ "reference": "4ab4283f4b4ea4222da6cb97fbf7f7167f1ebb53"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/zoujingli/ip2region/zipball/fd31d7a023f28f4e2f3e63f46f1fd51f8b2672f6",
- "reference": "fd31d7a023f28f4e2f3e63f46f1fd51f8b2672f6",
+ "url": "https://api.github.com/repos/zoujingli/ip2region/zipball/4ab4283f4b4ea4222da6cb97fbf7f7167f1ebb53",
+ "reference": "4ab4283f4b4ea4222da6cb97fbf7f7167f1ebb53",
"shasum": ""
},
"require": {
- "php": ">=5.4"
+ "php": ">=7.1.0"
},
"suggest": {
"ext-opcache": "启用 OPcache 以获得更好的性能"
},
+ "bin": [
+ "bin/ip2down"
+ ],
"type": "library",
"extra": {
"branch-alias": {
@@ -6948,10 +7259,13 @@
},
"autoload": {
"files": [
- "ip2region.php"
+ "src/common.php"
],
+ "psr-4": {
+ "ip2region\\": "src/ip2region/"
+ },
"classmap": [
- "src/"
+ "src/Ip2Region.php"
]
},
"notification-url": "https://packagist.org/downloads/",
@@ -6965,30 +7279,28 @@
"homepage": "https://thinkadmin.top"
}
],
- "description": "Ip2Region v3.0 for PHP - 企业级 IP 地理位置查询库,支持 IPv4/IPv6,分片文件管理,智能缓存,零依赖",
+ "description": "ip2region v3.0 for PHP - 企业级 IP 地理位置查询库,支持 IPv4 和 IPv6,多种缓存策略,零依赖,开箱即用",
"homepage": "https://github.com/zoujingli/Ip2Region",
"keywords": [
"IP",
- "IP定位",
"Ip2Region",
- "geoip",
+ "composer",
"geolocation",
"ipv4",
"ipv6",
"location",
+ "php",
"企业级",
- "分片数据库",
- "压缩",
- "地理位置",
- "地理位置查询",
- "缓存",
+ "智能加载",
+ "缓存策略",
+ "零依赖",
"高性能"
],
"support": {
"issues": "https://github.com/zoujingli/ip2region/issues",
- "source": "https://github.com/zoujingli/ip2region/tree/v3.0.0"
+ "source": "https://github.com/zoujingli/ip2region/tree/v3.0.8"
},
- "time": "2025-09-15T12:27:22+00:00"
+ "time": "2025-11-20T11:02:33+00:00"
}
],
"packages-dev": [],
diff --git a/config/plugin/webman/log/app.php b/config/plugin/webman/log/app.php
new file mode 100644
index 0000000..6217553
--- /dev/null
+++ b/config/plugin/webman/log/app.php
@@ -0,0 +1,19 @@
+ true,
+ 'exception' => [
+ // 是否记录异常到日志
+ 'enable' => true,
+ // 不会记录到日志的异常类
+ 'dontReport' => [
+ support\exception\BusinessException::class
+ ]
+ ],
+ 'dontReport' => [
+ 'app' => [],
+ 'controller' => [],
+ 'action' => [],
+ 'path' => []
+ ],
+ 'channel' => 'default' // 日志通道(在config/log.php里配置,默认是default)
+];
diff --git a/config/plugin/webman/log/middleware.php b/config/plugin/webman/log/middleware.php
new file mode 100644
index 0000000..7c7b0a9
--- /dev/null
+++ b/config/plugin/webman/log/middleware.php
@@ -0,0 +1,21 @@
+
+ * @copyright walkor
+ * @link http://www.workerman.net/
+ * @license http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+
+use Webman\Log\Middleware;
+
+return [
+ '@' => [
+ Middleware::class
+ ]
+];