diff --git a/app/command/AssignInquiryCommand.php b/app/command/AssignInquiryCommand.php
new file mode 100644
index 0000000..b33b482
--- /dev/null
+++ b/app/command/AssignInquiryCommand.php
@@ -0,0 +1,145 @@
+ $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;
+
+ // 轮询取出一个业务员 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/FbLeadToInquiryCommand.php b/app/command/FbLeadToInquiryCommand.php
new file mode 100644
index 0000000..36ec869
--- /dev/null
+++ b/app/command/FbLeadToInquiryCommand.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/TbCommand.php b/app/command/TbCommand.php
new file mode 100644
index 0000000..9f505a4
--- /dev/null
+++ b/app/command/TbCommand.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/controller/IndexController.php b/app/controller/IndexController.php
index c6489a3..1a5d257 100644
--- a/app/controller/IndexController.php
+++ b/app/controller/IndexController.php
@@ -9,6 +9,11 @@ use support\Log;
class IndexController
{
+ public function index()
+ {
+ return 22;
+ }
+
public function form(Request $request, int $id)
{
try {
diff --git a/app/controller/TransmitController.php b/app/controller/TransmitController.php
new file mode 100644
index 0000000..e96e283
--- /dev/null
+++ b/app/controller/TransmitController.php
@@ -0,0 +1,35 @@
+fetchForms();
+
+ $result = $client->fetchEntries(263, 0, 10);
+
+ return json($result);
+ } catch (\Throwable $th) {
+ Log::info('Data:'.date('Y-m-d H:i:s').':'.$th->getMessage().'|'.$th->getLine().'|'.$th->getFile());
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/inquery/controller/InqueryController.php b/app/inquery/controller/InqueryController.php
new file mode 100644
index 0000000..0ee0b0e
--- /dev/null
+++ b/app/inquery/controller/InqueryController.php
@@ -0,0 +1,61 @@
+getActiveSheet();
+ $rows = $sheet->toArray(null, true, true, true);
+
+ if (empty($rows)) {
+ throw new BusinessException('Excel 内容为空');
+ }
+
+ // 取第一行表头
+ $headerRow = array_shift($rows);
+
+ // 实例化映射器
+ $mapper = new FbLeadHeaderMapper();
+
+ // ① 如果有未知表头,会在这里抛出异常并终止处理
+ $colToField = $mapper->buildColumnFieldMap($headerRow);
+
+ // ② 开始处理数据行
+ $insertData = [];
+ $now = time();
+
+ foreach ($rows as $row) {
+ $item = [];
+
+ foreach ($colToField as $colIndex => $field) {
+ $item[$field] = $row[$colIndex] ?? null;
+ }
+
+ // 补充你需要的其他字段
+ $item['source_file'] = basename($filePath);
+ $item['created_at'] = $now;
+ $item['updated_at'] = $now;
+
+ $insertData[] = $item;
+ }
+
+ // ③ 批量入库(根据你的实际需求做去重等逻辑)
+ if ($insertData) {
+ Db::table('fb_leads')->insert($insertData);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/inquery/utils/FbLeadHeaderMapper.php b/app/inquery/utils/FbLeadHeaderMapper.php
new file mode 100644
index 0000000..59b1cb5
--- /dev/null
+++ b/app/inquery/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/inquery/utils/PhoneCountryHelper.php b/app/inquery/utils/PhoneCountryHelper.php
new file mode 100644
index 0000000..78ec4ab
--- /dev/null
+++ b/app/inquery/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/inquery/utils/RoundRobinAssigner.php b/app/inquery/utils/RoundRobinAssigner.php
new file mode 100644
index 0000000..5063884
--- /dev/null
+++ b/app/inquery/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/utils/Cf7InquiryClient.php b/app/utils/Cf7InquiryClient.php
new file mode 100644
index 0000000..adf264b
--- /dev/null
+++ b/app/utils/Cf7InquiryClient.php
@@ -0,0 +1,93 @@
+baseUri = rtrim($baseUri, '/');
+
+ $config = [
+ 'base_uri' => $this->baseUri,
+ 'auth' => [$username, $password],
+ 'timeout' => $options['timeout'] ?? 10,
+ 'verify' => $options['verify'] ?? false, // 正式环境建议改成 true
+ ];
+
+ $this->client = new Client($config);
+ }
+
+ /**
+ * 获取当前站点的 CF7 表单列表(从 Advanced CF7 DB 推断)
+ * 对应插件里的 /acf7db/v1/forms
+ *
+ * @return array
+ * @throws GuzzleException
+ */
+ public function fetchForms(): array
+ {
+ $response = $this->client->get('/wp-json/acf7db/v1/forms');
+ $body = (string)$response->getBody();
+
+ return json_decode($body, true) ?: [];
+ }
+
+ /**
+ * 获取 Advanced CF7 DB 询盘列表(存量 + 增量通用)
+ *
+ * @param int $cf7Id 表单 ID(可选,为 0 时表示所有表单)
+ * @param int $lastId 上次同步到的最大 submit_id
+ * @param int $perPage 每次拉取数量
+ * @param string|null $from 起始时间(可选,Y-m-d H:i:s)
+ * @param string|null $to 结束时间(可选)
+ *
+ * @return array ['data' => [...], 'meta' => [...]]
+ * @throws GuzzleException
+ */
+ public function fetchEntries(
+ int $cf7Id = 0,
+ int $lastId = 0,
+ int $perPage = 200,
+ ?string $from = null,
+ ?string $to = null
+ ): array {
+ $query = [
+ 'last_id' => $lastId,
+ 'per_page' => $perPage,
+ ];
+
+ if ($cf7Id > 0) {
+ $query['cf7_id'] = $cf7Id;
+ }
+ if ($from) {
+ $query['from'] = $from;
+ }
+ if ($to) {
+ $query['to'] = $to;
+ }
+
+ $response = $this->client->get('/wp-json/acf7db/v1/entries', [
+ 'query' => $query,
+ ]);
+
+ $body = (string)$response->getBody();
+ return json_decode($body, true) ?: ['data' => [], 'meta' => []];
+ }
+}
diff --git a/composer.json b/composer.json
index 34464e3..b51dd8c 100644
--- a/composer.json
+++ b/composer.json
@@ -29,11 +29,15 @@
"monolog/monolog": "^2.0",
"vlucas/phpdotenv": "^5.6",
"webman/database": "^2.1",
- "illuminate/pagination": "^12.37",
- "illuminate/events": "^12.37",
- "symfony/var-dumper": "^7.3",
+ "illuminate/pagination": "^12.42",
+ "illuminate/events": "^12.42",
+ "symfony/var-dumper": "^7.4",
"phpmailer/phpmailer": "^7.0",
- "zoujingli/ip2region": "^3.0"
+ "zoujingli/ip2region": "^3.0",
+ "phpoffice/phpspreadsheet": "^5.3",
+ "webman/console": "^2.1",
+ "webman/redis": "^2.1",
+ "webman/redis-queue": "^2.1"
},
"suggest": {
"ext-event": "For better performance. "
diff --git a/composer.lock b/composer.lock
index d54d545..4d5f7c3 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": "48c77686c3f22f2ca6b7174d010a7656",
+ "content-hash": "bacff953706f64356bf4c93c5e0f7dad",
"packages": [
{
"name": "brick/math",
@@ -135,6 +135,85 @@
],
"time": "2024-02-09T16:56:22+00:00"
},
+ {
+ "name": "composer/pcre",
+ "version": "3.3.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/pcre.git",
+ "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
+ "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4 || ^8.0"
+ },
+ "conflict": {
+ "phpstan/phpstan": "<1.11.10"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^1.12 || ^2",
+ "phpstan/phpstan-strict-rules": "^1 || ^2",
+ "phpunit/phpunit": "^8 || ^9"
+ },
+ "type": "library",
+ "extra": {
+ "phpstan": {
+ "includes": [
+ "extension.neon"
+ ]
+ },
+ "branch-alias": {
+ "dev-main": "3.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Composer\\Pcre\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jordi Boggiano",
+ "email": "j.boggiano@seld.be",
+ "homepage": "http://seld.be"
+ }
+ ],
+ "description": "PCRE wrapping library that offers type-safe preg_* replacements.",
+ "keywords": [
+ "PCRE",
+ "preg",
+ "regex",
+ "regular expression"
+ ],
+ "support": {
+ "issues": "https://github.com/composer/pcre/issues",
+ "source": "https://github.com/composer/pcre/tree/3.3.2"
+ },
+ "funding": [
+ {
+ "url": "https://packagist.com",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/composer",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/composer/composer",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-11-12T16:29:46+00:00"
+ },
{
"name": "doctrine/inflector",
"version": "2.1.0",
@@ -771,7 +850,7 @@
},
{
"name": "illuminate/bus",
- "version": "v12.41.1",
+ "version": "v12.42.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/bus.git",
@@ -824,16 +903,16 @@
},
{
"name": "illuminate/collections",
- "version": "v12.41.1",
+ "version": "v12.42.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/collections.git",
- "reference": "1cf6115f711ff775fcce547dee82d59cac33fedb"
+ "reference": "16657effa6a5a4e728f9aeb3e38fb4fa9ba70e7d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/illuminate/collections/zipball/1cf6115f711ff775fcce547dee82d59cac33fedb",
- "reference": "1cf6115f711ff775fcce547dee82d59cac33fedb",
+ "url": "https://api.github.com/repos/illuminate/collections/zipball/16657effa6a5a4e728f9aeb3e38fb4fa9ba70e7d",
+ "reference": "16657effa6a5a4e728f9aeb3e38fb4fa9ba70e7d",
"shasum": ""
},
"require": {
@@ -879,11 +958,11 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
- "time": "2025-11-29T13:55:43+00:00"
+ "time": "2025-12-06T18:08:25+00:00"
},
{
"name": "illuminate/conditionable",
- "version": "v12.41.1",
+ "version": "v12.42.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/conditionable.git",
@@ -929,20 +1008,21 @@
},
{
"name": "illuminate/container",
- "version": "v12.41.1",
+ "version": "v12.42.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/container.git",
- "reference": "0489bc396de1537537750babec3a03ee7b753d1f"
+ "reference": "326667a4c813e3ad5a645969a7e3f5c10d159de2"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/illuminate/container/zipball/0489bc396de1537537750babec3a03ee7b753d1f",
- "reference": "0489bc396de1537537750babec3a03ee7b753d1f",
+ "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",
"symfony/polyfill-php84": "^1.33",
@@ -986,11 +1066,11 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
- "time": "2025-11-28T18:51:53+00:00"
+ "time": "2025-12-08T22:34:25+00:00"
},
{
"name": "illuminate/contracts",
- "version": "v12.41.1",
+ "version": "v12.42.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/contracts.git",
@@ -1038,16 +1118,16 @@
},
{
"name": "illuminate/database",
- "version": "v12.41.1",
+ "version": "v12.42.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/database.git",
- "reference": "199e9200a08c1841a31932e0a36a4094de84b72d"
+ "reference": "b44f021fee7b48289312e22fd6e4d580374a86ac"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/illuminate/database/zipball/199e9200a08c1841a31932e0a36a4094de84b72d",
- "reference": "199e9200a08c1841a31932e0a36a4094de84b72d",
+ "url": "https://api.github.com/repos/illuminate/database/zipball/b44f021fee7b48289312e22fd6e4d580374a86ac",
+ "reference": "b44f021fee7b48289312e22fd6e4d580374a86ac",
"shasum": ""
},
"require": {
@@ -1105,11 +1185,11 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
- "time": "2025-12-01T15:01:30+00:00"
+ "time": "2025-12-06T18:08:25+00:00"
},
{
"name": "illuminate/events",
- "version": "v12.41.1",
+ "version": "v12.42.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/events.git",
@@ -1164,7 +1244,7 @@
},
{
"name": "illuminate/filesystem",
- "version": "v12.41.1",
+ "version": "v12.42.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/filesystem.git",
@@ -1231,16 +1311,16 @@
},
{
"name": "illuminate/http",
- "version": "v12.41.1",
+ "version": "v12.42.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/http.git",
- "reference": "cdabd462fa50ab057da4807b980c861326c5a345"
+ "reference": "b7997f2faffb3c31f2a0fa079428ff6bfcce8907"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/illuminate/http/zipball/cdabd462fa50ab057da4807b980c861326c5a345",
- "reference": "cdabd462fa50ab057da4807b980c861326c5a345",
+ "url": "https://api.github.com/repos/illuminate/http/zipball/b7997f2faffb3c31f2a0fa079428ff6bfcce8907",
+ "reference": "b7997f2faffb3c31f2a0fa079428ff6bfcce8907",
"shasum": ""
},
"require": {
@@ -1289,11 +1369,11 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
- "time": "2025-11-28T18:00:52+00:00"
+ "time": "2025-12-09T15:25:50+00:00"
},
{
"name": "illuminate/macroable",
- "version": "v12.41.1",
+ "version": "v12.42.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/macroable.git",
@@ -1339,7 +1419,7 @@
},
{
"name": "illuminate/pagination",
- "version": "v12.41.1",
+ "version": "v12.42.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/pagination.git",
@@ -1389,7 +1469,7 @@
},
{
"name": "illuminate/pipeline",
- "version": "v12.41.1",
+ "version": "v12.42.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/pipeline.git",
@@ -1439,9 +1519,114 @@
},
"time": "2025-08-20T13:36:50+00:00"
},
+ {
+ "name": "illuminate/redis",
+ "version": "v12.42.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/illuminate/redis.git",
+ "reference": "8dc6682638100db9feceaac3db206de752dd0f9a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/illuminate/redis/zipball/8dc6682638100db9feceaac3db206de752dd0f9a",
+ "reference": "8dc6682638100db9feceaac3db206de752dd0f9a",
+ "shasum": ""
+ },
+ "require": {
+ "illuminate/collections": "^12.0",
+ "illuminate/contracts": "^12.0",
+ "illuminate/macroable": "^12.0",
+ "illuminate/support": "^12.0",
+ "php": "^8.2"
+ },
+ "suggest": {
+ "ext-redis": "Required to use the phpredis connector (^4.0|^5.0|^6.0).",
+ "predis/predis": "Required to use the predis connector (^2.3|^3.0)."
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "12.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Illuminate\\Redis\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Taylor Otwell",
+ "email": "taylor@laravel.com"
+ }
+ ],
+ "description": "The Illuminate Redis package.",
+ "homepage": "https://laravel.com",
+ "support": {
+ "issues": "https://github.com/laravel/framework/issues",
+ "source": "https://github.com/laravel/framework"
+ },
+ "time": "2025-11-20T20:50:40+00:00"
+ },
+ {
+ "name": "illuminate/reflection",
+ "version": "v12.42.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/illuminate/reflection.git",
+ "reference": "7b86bc570d5b75e4a3ad79f8cca1491ba24b7c75"
+ },
+ "dist": {
+ "type": "zip",
+ "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.41.1",
+ "version": "v12.42.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/session.git",
@@ -1498,16 +1683,16 @@
},
{
"name": "illuminate/support",
- "version": "v12.41.1",
+ "version": "v12.42.0",
"source": {
"type": "git",
"url": "https://github.com/illuminate/support.git",
- "reference": "c65715d8a41863ee3f2e22dc796b75c62143fca2"
+ "reference": "d35411be5657e0b5560a5885f3c9140e4cbe0be5"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/illuminate/support/zipball/c65715d8a41863ee3f2e22dc796b75c62143fca2",
- "reference": "c65715d8a41863ee3f2e22dc796b75c62143fca2",
+ "url": "https://api.github.com/repos/illuminate/support/zipball/d35411be5657e0b5560a5885f3c9140e4cbe0be5",
+ "reference": "d35411be5657e0b5560a5885f3c9140e4cbe0be5",
"shasum": ""
},
"require": {
@@ -1519,6 +1704,7 @@
"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",
@@ -1573,7 +1759,7 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
- "time": "2025-12-03T01:01:36+00:00"
+ "time": "2025-12-09T15:26:52+00:00"
},
{
"name": "laravel/serializable-closure",
@@ -1636,6 +1822,191 @@
},
"time": "2025-11-21T20:52:36+00:00"
},
+ {
+ "name": "maennchen/zipstream-php",
+ "version": "3.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/maennchen/ZipStream-PHP.git",
+ "reference": "9712d8fa4cdf9240380b01eb4be55ad8dcf71416"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/9712d8fa4cdf9240380b01eb4be55ad8dcf71416",
+ "reference": "9712d8fa4cdf9240380b01eb4be55ad8dcf71416",
+ "shasum": ""
+ },
+ "require": {
+ "ext-mbstring": "*",
+ "ext-zlib": "*",
+ "php-64bit": "^8.3"
+ },
+ "require-dev": {
+ "brianium/paratest": "^7.7",
+ "ext-zip": "*",
+ "friendsofphp/php-cs-fixer": "^3.16",
+ "guzzlehttp/guzzle": "^7.5",
+ "mikey179/vfsstream": "^1.6",
+ "php-coveralls/php-coveralls": "^2.5",
+ "phpunit/phpunit": "^12.0",
+ "vimeo/psalm": "^6.0"
+ },
+ "suggest": {
+ "guzzlehttp/psr7": "^2.4",
+ "psr/http-message": "^2.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "ZipStream\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Paul Duncan",
+ "email": "pabs@pablotron.org"
+ },
+ {
+ "name": "Jonatan Männchen",
+ "email": "jonatan@maennchen.ch"
+ },
+ {
+ "name": "Jesse Donat",
+ "email": "donatj@gmail.com"
+ },
+ {
+ "name": "András Kolesár",
+ "email": "kolesar@kolesar.hu"
+ }
+ ],
+ "description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.",
+ "keywords": [
+ "stream",
+ "zip"
+ ],
+ "support": {
+ "issues": "https://github.com/maennchen/ZipStream-PHP/issues",
+ "source": "https://github.com/maennchen/ZipStream-PHP/tree/3.2.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/maennchen",
+ "type": "github"
+ }
+ ],
+ "time": "2025-07-17T11:15:13+00:00"
+ },
+ {
+ "name": "markbaker/complex",
+ "version": "3.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/MarkBaker/PHPComplex.git",
+ "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/95c56caa1cf5c766ad6d65b6344b807c1e8405b9",
+ "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "require-dev": {
+ "dealerdirect/phpcodesniffer-composer-installer": "dev-master",
+ "phpcompatibility/php-compatibility": "^9.3",
+ "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
+ "squizlabs/php_codesniffer": "^3.7"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Complex\\": "classes/src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Mark Baker",
+ "email": "mark@lange.demon.co.uk"
+ }
+ ],
+ "description": "PHP Class for working with complex numbers",
+ "homepage": "https://github.com/MarkBaker/PHPComplex",
+ "keywords": [
+ "complex",
+ "mathematics"
+ ],
+ "support": {
+ "issues": "https://github.com/MarkBaker/PHPComplex/issues",
+ "source": "https://github.com/MarkBaker/PHPComplex/tree/3.0.2"
+ },
+ "time": "2022-12-06T16:21:08+00:00"
+ },
+ {
+ "name": "markbaker/matrix",
+ "version": "3.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/MarkBaker/PHPMatrix.git",
+ "reference": "728434227fe21be27ff6d86621a1b13107a2562c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/728434227fe21be27ff6d86621a1b13107a2562c",
+ "reference": "728434227fe21be27ff6d86621a1b13107a2562c",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1 || ^8.0"
+ },
+ "require-dev": {
+ "dealerdirect/phpcodesniffer-composer-installer": "dev-master",
+ "phpcompatibility/php-compatibility": "^9.3",
+ "phpdocumentor/phpdocumentor": "2.*",
+ "phploc/phploc": "^4.0",
+ "phpmd/phpmd": "2.*",
+ "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
+ "sebastian/phpcpd": "^4.0",
+ "squizlabs/php_codesniffer": "^3.7"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Matrix\\": "classes/src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Mark Baker",
+ "email": "mark@demon-angel.eu"
+ }
+ ],
+ "description": "PHP Class for working with matrices",
+ "homepage": "https://github.com/MarkBaker/PHPMatrix",
+ "keywords": [
+ "mathematics",
+ "matrix",
+ "vector"
+ ],
+ "support": {
+ "issues": "https://github.com/MarkBaker/PHPMatrix/issues",
+ "source": "https://github.com/MarkBaker/PHPMatrix/tree/3.0.1"
+ },
+ "time": "2022-12-02T22:17:43+00:00"
+ },
{
"name": "monolog/monolog",
"version": "2.10.0",
@@ -1975,6 +2346,112 @@
],
"time": "2025-11-25T07:18:09+00:00"
},
+ {
+ "name": "phpoffice/phpspreadsheet",
+ "version": "5.3.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
+ "reference": "4d597c1aacdde1805a33c525b9758113ea0d90df"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/4d597c1aacdde1805a33c525b9758113ea0d90df",
+ "reference": "4d597c1aacdde1805a33c525b9758113ea0d90df",
+ "shasum": ""
+ },
+ "require": {
+ "composer/pcre": "^1||^2||^3",
+ "ext-ctype": "*",
+ "ext-dom": "*",
+ "ext-fileinfo": "*",
+ "ext-gd": "*",
+ "ext-iconv": "*",
+ "ext-libxml": "*",
+ "ext-mbstring": "*",
+ "ext-simplexml": "*",
+ "ext-xml": "*",
+ "ext-xmlreader": "*",
+ "ext-xmlwriter": "*",
+ "ext-zip": "*",
+ "ext-zlib": "*",
+ "maennchen/zipstream-php": "^2.1 || ^3.0",
+ "markbaker/complex": "^3.0",
+ "markbaker/matrix": "^3.0",
+ "php": "^8.1",
+ "psr/http-client": "^1.0",
+ "psr/http-factory": "^1.0",
+ "psr/simple-cache": "^1.0 || ^2.0 || ^3.0"
+ },
+ "require-dev": {
+ "dealerdirect/phpcodesniffer-composer-installer": "dev-main",
+ "dompdf/dompdf": "^2.0 || ^3.0",
+ "friendsofphp/php-cs-fixer": "^3.2",
+ "mitoteam/jpgraph": "^10.5",
+ "mpdf/mpdf": "^8.1.1",
+ "phpcompatibility/php-compatibility": "^9.3",
+ "phpstan/phpstan": "^1.1 || ^2.0",
+ "phpstan/phpstan-deprecation-rules": "^1.0 || ^2.0",
+ "phpstan/phpstan-phpunit": "^1.0 || ^2.0",
+ "phpunit/phpunit": "^10.5",
+ "squizlabs/php_codesniffer": "^3.7",
+ "tecnickcom/tcpdf": "^6.5"
+ },
+ "suggest": {
+ "dompdf/dompdf": "Option for rendering PDF with PDF Writer",
+ "ext-intl": "PHP Internationalization Functions, required for NumberFormat Wizard",
+ "mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers",
+ "mpdf/mpdf": "Option for rendering PDF with PDF Writer",
+ "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Maarten Balliauw",
+ "homepage": "https://blog.maartenballiauw.be"
+ },
+ {
+ "name": "Mark Baker",
+ "homepage": "https://markbakeruk.net"
+ },
+ {
+ "name": "Franck Lefevre",
+ "homepage": "https://rootslabs.net"
+ },
+ {
+ "name": "Erik Tilt"
+ },
+ {
+ "name": "Adrien Crivelli"
+ }
+ ],
+ "description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine",
+ "homepage": "https://github.com/PHPOffice/PhpSpreadsheet",
+ "keywords": [
+ "OpenXML",
+ "excel",
+ "gnumeric",
+ "ods",
+ "php",
+ "spreadsheet",
+ "xls",
+ "xlsx"
+ ],
+ "support": {
+ "issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues",
+ "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/5.3.0"
+ },
+ "time": "2025-11-24T15:47:10+00:00"
+ },
{
"name": "phpoption/phpoption",
"version": "1.9.4",
@@ -2584,6 +3061,104 @@
],
"time": "2025-11-12T15:39:26+00:00"
},
+ {
+ "name": "symfony/console",
+ "version": "v7.4.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/console.git",
+ "reference": "6d9f0fbf2ec2e9785880096e3abd0ca0c88b506e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/console/zipball/6d9f0fbf2ec2e9785880096e3abd0ca0c88b506e",
+ "reference": "6d9f0fbf2ec2e9785880096e3abd0ca0c88b506e",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/polyfill-mbstring": "~1.0",
+ "symfony/service-contracts": "^2.5|^3",
+ "symfony/string": "^7.2|^8.0"
+ },
+ "conflict": {
+ "symfony/dependency-injection": "<6.4",
+ "symfony/dotenv": "<6.4",
+ "symfony/event-dispatcher": "<6.4",
+ "symfony/lock": "<6.4",
+ "symfony/process": "<6.4"
+ },
+ "provide": {
+ "psr/log-implementation": "1.0|2.0|3.0"
+ },
+ "require-dev": {
+ "psr/log": "^1|^2|^3",
+ "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": {
+ "psr-4": {
+ "Symfony\\Component\\Console\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Eases the creation of beautiful and testable command line interfaces",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "cli",
+ "command-line",
+ "console",
+ "terminal"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/console/tree/v7.4.1"
+ },
+ "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-12-05T15:23:39+00:00"
+ },
{
"name": "symfony/deprecation-contracts",
"version": "v3.6.0",
@@ -3335,6 +3910,88 @@
],
"time": "2024-09-09T11:45:10+00:00"
},
+ {
+ "name": "symfony/polyfill-intl-grapheme",
+ "version": "v1.33.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-intl-grapheme.git",
+ "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70",
+ "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "suggest": {
+ "ext-intl": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Intl\\Grapheme\\": ""
+ }
+ },
+ "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 for intl's grapheme_* functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "grapheme",
+ "intl",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-intl-grapheme/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-27T09:58:17+00:00"
+ },
{
"name": "symfony/polyfill-intl-idn",
"version": "v1.33.0",
@@ -3916,6 +4573,184 @@
],
"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": {
+ "php": ">=8.1",
+ "psr/container": "^1.1|^2.0",
+ "symfony/deprecation-contracts": "^2.5|^3"
+ },
+ "conflict": {
+ "ext-psr": "<1.1|>=2"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/contracts",
+ "name": "symfony/contracts"
+ },
+ "branch-alias": {
+ "dev-main": "3.6-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Contracts\\Service\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Test/"
+ ]
+ },
+ "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": "Generic abstractions related to writing services",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "abstractions",
+ "contracts",
+ "decoupling",
+ "interfaces",
+ "interoperability",
+ "standards"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/service-contracts/tree/v3.6.1"
+ },
+ "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-07-15T11:30:57+00:00"
+ },
+ {
+ "name": "symfony/string",
+ "version": "v7.4.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/string.git",
+ "reference": "d50e862cb0a0e0886f73ca1f31b865efbb795003"
+ },
+ "dist": {
+ "type": "zip",
+ "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.33",
+ "symfony/polyfill-intl-normalizer": "~1.0",
+ "symfony/polyfill-mbstring": "~1.0"
+ },
+ "conflict": {
+ "symfony/translation-contracts": "<2.5"
+ },
+ "require-dev": {
+ "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|^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "Resources/functions.php"
+ ],
+ "psr-4": {
+ "Symfony\\Component\\String\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "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": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "grapheme",
+ "i18n",
+ "string",
+ "unicode",
+ "utf-8",
+ "utf8"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/string/tree/v7.4.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-11-27T13:27:24+00:00"
+ },
{
"name": "symfony/translation",
"version": "v7.4.0",
@@ -4343,6 +5178,60 @@
],
"time": "2024-11-21T01:49:47+00:00"
},
+ {
+ "name": "webman/console",
+ "version": "v2.1.10",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/webman-php/console.git",
+ "reference": "6b24bb77d04588c7c3b235087073e69ddfdaa773"
+ },
+ "dist": {
+ "type": "zip",
+ "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 || ^7.0"
+ },
+ "require-dev": {
+ "workerman/webman": "^2.1"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Webman\\Console\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "walkor",
+ "email": "walkor@workerman.net",
+ "homepage": "http://www.workerman.net",
+ "role": "Developer"
+ }
+ ],
+ "description": "Webman console",
+ "homepage": "http://www.workerman.net",
+ "keywords": [
+ "webman console"
+ ],
+ "support": {
+ "email": "walkor@workerman.net",
+ "forum": "http://www.workerman.net/questions",
+ "issues": "https://github.com/webman-php/console/issues",
+ "source": "https://github.com/webman-php/console",
+ "wiki": "http://www.workerman.net/doc/webman"
+ },
+ "time": "2025-12-08T08:23:12+00:00"
+ },
{
"name": "webman/database",
"version": "v2.1.7",
@@ -4381,6 +5270,76 @@
},
"time": "2025-11-27T08:09:25+00:00"
},
+ {
+ "name": "webman/redis",
+ "version": "v2.1.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/webman-php/redis.git",
+ "reference": "559eb1692d39c6fef5cf526223fff728be6c0fb9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/webman-php/redis/zipball/559eb1692d39c6fef5cf526223fff728be6c0fb9",
+ "reference": "559eb1692d39c6fef5cf526223fff728be6c0fb9",
+ "shasum": ""
+ },
+ "require": {
+ "illuminate/redis": "^10.0 || ^11.0 || ^12.0",
+ "workerman/webman-framework": "^2.1 || dev-master"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "support\\": "src/support",
+ "Webman\\Redis\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Webman redis",
+ "support": {
+ "issues": "https://github.com/webman-php/redis/issues",
+ "source": "https://github.com/webman-php/redis/tree/v2.1.3"
+ },
+ "time": "2025-03-14T03:52:14+00:00"
+ },
+ {
+ "name": "webman/redis-queue",
+ "version": "v2.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/webman-php/redis-queue.git",
+ "reference": "ff4791e21f3c324a47e21da7b6f2dae5a7311dcb"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/webman-php/redis-queue/zipball/ff4791e21f3c324a47e21da7b6f2dae5a7311dcb",
+ "reference": "ff4791e21f3c324a47e21da7b6f2dae5a7311dcb",
+ "shasum": ""
+ },
+ "require": {
+ "ext-redis": "*",
+ "php": ">=8.1",
+ "workerman/redis-queue": "^1.2",
+ "workerman/webman-framework": "^2.1 || dev-master"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Webman\\RedisQueue\\": "./src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "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.1"
+ },
+ "time": "2025-11-14T07:12:52+00:00"
+ },
{
"name": "workerman/coroutine",
"version": "v1.1.4",
@@ -4420,6 +5379,78 @@
},
"time": "2025-10-11T15:09:08+00:00"
},
+ {
+ "name": "workerman/redis",
+ "version": "v2.0.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/walkor/redis.git",
+ "reference": "49627c1809eff1ef7175eb8ee7549234a1d67ec5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/walkor/redis/zipball/49627c1809eff1ef7175eb8ee7549234a1d67ec5",
+ "reference": "49627c1809eff1ef7175eb8ee7549234a1d67ec5",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7",
+ "workerman/workerman": "^4.1.0||^5.0.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Workerman\\Redis\\": "./src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "homepage": "http://www.workerman.net",
+ "support": {
+ "issues": "https://github.com/walkor/redis/issues",
+ "source": "https://github.com/walkor/redis/tree/v2.0.5"
+ },
+ "time": "2025-04-07T01:58:58+00:00"
+ },
+ {
+ "name": "workerman/redis-queue",
+ "version": "v1.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/walkor/redis-queue.git",
+ "reference": "75dbf7ed2ea228c45dc0df82c0fea35879b715d0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/walkor/redis-queue/zipball/75dbf7ed2ea228c45dc0df82c0fea35879b715d0",
+ "reference": "75dbf7ed2ea228c45dc0df82c0fea35879b715d0",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.0",
+ "workerman/redis": "^1.0||^2.0",
+ "workerman/workerman": ">=4.0.20"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Workerman\\RedisQueue\\": "./src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Message queue system written in PHP based on workerman and backed by Redis.",
+ "homepage": "http://www.workerman.net",
+ "support": {
+ "issues": "https://github.com/walkor/redis-queue/issues",
+ "source": "https://github.com/walkor/redis-queue/tree/v1.2.1"
+ },
+ "time": "2025-01-02T09:21:45+00:00"
+ },
{
"name": "workerman/webman-framework",
"version": "v2.1.4",
diff --git a/config/database.php b/config/database.php
index e436e95..93308ad 100644
--- a/config/database.php
+++ b/config/database.php
@@ -4,14 +4,14 @@ return [
'connections' => [
'mysql' => [
'driver' => 'mysql',
- 'host' => '127.0.0.1',
- 'port' => '3306',
- 'database' => 'your_database',
- 'username' => 'your_username',
- 'password' => 'your_password',
+ 'host' => getenv('DB_HOST'),
+ 'port' => getenv('DB_PORT'),
+ 'database' => getenv('DB_NAME'),
+ 'username' => getenv('DB_USER'),
+ 'password' => getenv('DB_PASSWORD'),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_general_ci',
- 'prefix' => '',
+ 'prefix' => getenv('DB_PREFIX'),
'strict' => true,
'engine' => null,
'options' => [
diff --git a/config/plugin/webman/console/app.php b/config/plugin/webman/console/app.php
new file mode 100644
index 0000000..074e986
--- /dev/null
+++ b/config/plugin/webman/console/app.php
@@ -0,0 +1,24 @@
+ true,
+
+ 'build_dir' => BASE_PATH . DIRECTORY_SEPARATOR . 'build',
+
+ 'phar_filename' => 'webman.phar',
+
+ 'bin_filename' => 'webman.bin',
+
+ 'signature_algorithm'=> Phar::SHA256, //set the signature algorithm for a phar and apply it. The signature algorithm must be one of Phar::MD5, Phar::SHA1, Phar::SHA256, Phar::SHA512, or Phar::OPENSSL.
+
+ 'private_key_file' => '', // The file path for certificate or OpenSSL private key file.
+
+ 'exclude_pattern' => '#^(?!.*(composer.json|/.github/|/.idea/|/.git/|/.setting/|/runtime/|/vendor-bin/|/build/|/vendor/webman/admin/))(.*)$#',
+
+ 'exclude_files' => [
+ '.env', 'LICENSE', 'composer.json', 'composer.lock', 'start.php', 'webman.phar', 'webman.bin'
+ ],
+
+ 'custom_ini' => '
+memory_limit = 256M
+ ',
+];
diff --git a/config/plugin/webman/redis-queue/app.php b/config/plugin/webman/redis-queue/app.php
new file mode 100644
index 0000000..8f9c426
--- /dev/null
+++ b/config/plugin/webman/redis-queue/app.php
@@ -0,0 +1,4 @@
+ true,
+];
\ No newline at end of file
diff --git a/config/plugin/webman/redis-queue/command.php b/config/plugin/webman/redis-queue/command.php
new file mode 100644
index 0000000..8bfe2a1
--- /dev/null
+++ b/config/plugin/webman/redis-queue/command.php
@@ -0,0 +1,7 @@
+
+ * @copyright walkor
+ * @link http://www.workerman.net/
+ * @license http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+
+return [
+ 'default' => [
+ 'handlers' => [
+ [
+ 'class' => Monolog\Handler\RotatingFileHandler::class,
+ 'constructor' => [
+ runtime_path() . '/logs/redis-queue/queue.log',
+ 7, //$maxFiles
+ Monolog\Logger::DEBUG,
+ ],
+ 'formatter' => [
+ 'class' => Monolog\Formatter\LineFormatter::class,
+ 'constructor' => [null, 'Y-m-d H:i:s', true],
+ ],
+ ]
+ ],
+ ]
+];
diff --git a/config/plugin/webman/redis-queue/process.php b/config/plugin/webman/redis-queue/process.php
new file mode 100644
index 0000000..c8d4da1
--- /dev/null
+++ b/config/plugin/webman/redis-queue/process.php
@@ -0,0 +1,11 @@
+ [
+ 'handler' => Webman\RedisQueue\Process\Consumer::class,
+ 'count' => 8, // 可以设置多进程同时消费
+ 'constructor' => [
+ // 消费者类目录
+ 'consumer_dir' => app_path() . '/queue/redis'
+ ]
+ ]
+];
\ No newline at end of file
diff --git a/config/plugin/webman/redis-queue/redis.php b/config/plugin/webman/redis-queue/redis.php
new file mode 100644
index 0000000..420914d
--- /dev/null
+++ b/config/plugin/webman/redis-queue/redis.php
@@ -0,0 +1,22 @@
+ [
+// 'host' => 'redis://127.0.0.1:6379',
+ 'host' => getenv('REDIS_QUEUE_HOST'),
+ 'options' => [
+ 'auth' => getenv('REDIS_QUEUE_AUTH'), // 密码,字符串类型,可选参数
+ 'db' => getenv('REDIS_QUEUE_DB'), // 数据库
+ 'prefix' => getenv('REDIS_QUEUE_PREFIX'), // key 前缀
+ 'max_attempts' => getenv('REDIS_QUEUE_MAX_ATTEMPTS'), // 消费失败后,重试次数
+ 'retry_seconds' => getenv('REDIS_QUEUE_RETRY_SECONDS'), // 重试间隔,单位秒
+ ],
+ // Connection pool, supports only Swoole or Swow drivers.
+ 'pool' => [
+ 'max_connections' => 5,
+ 'min_connections' => 1,
+ 'wait_timeout' => 3,
+ 'idle_timeout' => 60,
+ 'heartbeat_interval' => 50,
+ ]
+ ],
+];
diff --git a/config/redis.php b/config/redis.php
new file mode 100644
index 0000000..0508ef6
--- /dev/null
+++ b/config/redis.php
@@ -0,0 +1,29 @@
+
+ * @copyright walkor
+ * @link http://www.workerman.net/
+ * @license http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+
+return [
+ 'default' => [
+ 'password' => getenv('REDIS_PASSWORD'),
+ 'host' => getenv('REDIS_HOST'),
+ 'port' => getenv('REDIS_PORT'),
+ 'database' => getenv('REDIS_DATABASE'),
+ 'pool' => [
+ 'max_connections' => 5,
+ 'min_connections' => 1,
+ 'wait_timeout' => 3,
+ 'idle_timeout' => 60,
+ 'heartbeat_interval' => 50,
+ ],
+ ]
+];
diff --git a/config/route.php b/config/route.php
index cb45b8d..fd6c729 100644
--- a/config/route.php
+++ b/config/route.php
@@ -16,6 +16,6 @@ use Webman\Route;
-Route::any('/form/{id}', [app\controller\IndexController::class, 'form']);
-
+Route::any('/ts/form', [app\controller\TransmitController::class, 'form']);
+Route::any('/iq/form', [app\inquery\controller\InqueryController::class, 'form']);
diff --git a/public/excel/BYD tang_Leads_2025-12-04_2025-12-09.xlsx b/public/excel/BYD tang_Leads_2025-12-04_2025-12-09.xlsx
new file mode 100644
index 0000000..9c6b3b5
Binary files /dev/null and b/public/excel/BYD tang_Leads_2025-12-04_2025-12-09.xlsx differ
diff --git a/public/excel/Corolla Cross_Leads_2025-12-04_2025-12-09.xlsx b/public/excel/Corolla Cross_Leads_2025-12-04_2025-12-09.xlsx
new file mode 100644
index 0000000..313ce50
Binary files /dev/null and b/public/excel/Corolla Cross_Leads_2025-12-04_2025-12-09.xlsx differ
diff --git a/public/excel/ROX 01_Leads_2025-12-04_2025-12-09.xlsx b/public/excel/ROX 01_Leads_2025-12-04_2025-12-09.xlsx
new file mode 100644
index 0000000..bae4f24
Binary files /dev/null and b/public/excel/ROX 01_Leads_2025-12-04_2025-12-09.xlsx differ
diff --git a/public/excel/Toyota RAV4_Leads_2025-12-04_2025-12-09.xlsx b/public/excel/Toyota RAV4_Leads_2025-12-04_2025-12-09.xlsx
new file mode 100644
index 0000000..c5c718e
Binary files /dev/null and b/public/excel/Toyota RAV4_Leads_2025-12-04_2025-12-09.xlsx differ
diff --git a/public/excel/ZHENGZHOU NISSAN Z9_Leads_2025-12-04_2025-12-09.xlsx b/public/excel/ZHENGZHOU NISSAN Z9_Leads_2025-12-04_2025-12-09.xlsx
new file mode 100644
index 0000000..751faa2
Binary files /dev/null and b/public/excel/ZHENGZHOU NISSAN Z9_Leads_2025-12-04_2025-12-09.xlsx differ
diff --git a/public/excel/byd song plus_Leads_2025-12-04_2025-12-09.xlsx b/public/excel/byd song plus_Leads_2025-12-04_2025-12-09.xlsx
new file mode 100644
index 0000000..3c5d456
Binary files /dev/null and b/public/excel/byd song plus_Leads_2025-12-04_2025-12-09.xlsx differ
diff --git a/public/excel/jetour t2_Leads_2025-12-04_2025-12-09.xlsx b/public/excel/jetour t2_Leads_2025-12-04_2025-12-09.xlsx
new file mode 100644
index 0000000..ef512a1
Binary files /dev/null and b/public/excel/jetour t2_Leads_2025-12-04_2025-12-09.xlsx differ
diff --git a/public/excel/leopard 5_Leads_2025-12-04_2025-12-09.xlsx b/public/excel/leopard 5_Leads_2025-12-04_2025-12-09.xlsx
new file mode 100644
index 0000000..d31c5e9
Binary files /dev/null and b/public/excel/leopard 5_Leads_2025-12-04_2025-12-09.xlsx differ
diff --git a/webman b/webman
new file mode 100755
index 0000000..3f55062
--- /dev/null
+++ b/webman
@@ -0,0 +1,71 @@
+#!/usr/bin/env php
+load();
+ } else {
+ Dotenv::createMutable(run_path())->load();
+ }
+}
+
+$appConfig = require $appConfigFile;
+if ($timezone = $appConfig['default_timezone'] ?? '') {
+ date_default_timezone_set($timezone);
+}
+
+if ($errorReporting = $appConfig['error_reporting'] ?? '') {
+ error_reporting($errorReporting);
+}
+
+if (!in_array($argv[1] ?? '', ['start', 'restart', 'stop', 'status', 'reload', 'connections'])) {
+ require_once __DIR__ . '/support/bootstrap.php';
+} else {
+ if (class_exists('Support\App')) {
+ Support\App::loadAllConfig(['route']);
+ } else {
+ Config::reload(config_path(), ['route', 'container']);
+ }
+}
+
+$cli = new Command();
+$cli->setName('webman cli');
+$cli->installInternalCommands();
+if (is_dir($command_path = Util::guessPath(app_path(), '/command', true))) {
+ $cli->installCommands($command_path);
+}
+
+foreach (config('plugin', []) as $firm => $projects) {
+ if (isset($projects['app'])) {
+ foreach (['', '/app'] as $app) {
+ if ($command_str = Util::guessPath(base_path() . "/plugin/$firm{$app}", 'command')) {
+ $command_path = base_path() . "/plugin/$firm{$app}/$command_str";
+ $cli->installCommands($command_path, "plugin\\$firm" . str_replace('/', '\\', $app) . "\\$command_str");
+ }
+ }
+ }
+ foreach ($projects as $name => $project) {
+ if (!is_array($project)) {
+ continue;
+ }
+ $project['command'] ??= [];
+ array_walk($project['command'], [$cli, 'createCommandInstance']);
+ }
+}
+
+$cli->run();