inquiry
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
namespace app\command;
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use support\Db;
|
||||
use support\Redis;
|
||||
use support\exception\BusinessException;
|
||||
use Webman\RedisQueue\Redis as RedisQueueRedis;
|
||||
|
||||
/**
|
||||
* 轮询分配 inquiry_log 里的询盘给业务员,并推送到 CRM 队列
|
||||
*/
|
||||
class AssignInquiryCommand extends Command
|
||||
{
|
||||
protected static $defaultName = 'assign-inquiry';
|
||||
protected static $defaultDescription = '轮询分配询盘给业务员,并推送到 CRM 队列';
|
||||
|
||||
/**
|
||||
* 参与轮询的业务员 ID 列表(请按你想要的顺序填写)
|
||||
* 比如:销售1=186,销售2=233...
|
||||
*/
|
||||
protected const SALES_USER_IDS = [
|
||||
1,
|
||||
74,
|
||||
78,
|
||||
];
|
||||
|
||||
/**
|
||||
* 轮询分配的分组 key(同一分组共用一个轮询指针)
|
||||
*/
|
||||
protected const ROUND_ROBIN_GROUP_KEY = 'fb_inquiry';
|
||||
|
||||
/**
|
||||
* 每次脚本最多分配多少条
|
||||
*/
|
||||
protected const BATCH_LIMIT = 100;
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
// 业务员列表检查
|
||||
$userIds = array_values(array_filter(
|
||||
array_map('intval', static::SALES_USER_IDS),
|
||||
fn($v) => $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('<comment>没有需要分配的询盘(status=0 且 recipient=0)</comment>');
|
||||
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("<comment>询盘 {$inquiryId} 未更新(可能已被处理)</comment>");
|
||||
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("<info>已分配询盘 {$inquiryId} 给业务员 {$recipientUserId},并推送到队列</info>");
|
||||
});
|
||||
}
|
||||
|
||||
$output->writeln("<info>本次共分配成功 {$count} 条询盘</info>");
|
||||
|
||||
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];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
namespace app\command;
|
||||
|
||||
use app\inquery\utils\PhoneCountryHelper;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use support\Db;
|
||||
|
||||
/**
|
||||
* 将 fb_leads 数据清洗后导入 inquiry_log
|
||||
*/
|
||||
class FbLeadToInquiryCommand extends Command
|
||||
{
|
||||
protected static $defaultName = 'fb-to-inquiry';
|
||||
protected static $defaultDescription = '将 fb_leads 数据清洗后导入 inquiry_log';
|
||||
|
||||
// 这些常量根据你实际业务改
|
||||
protected const DEFAULT_ENTER_USER_ID = 186;
|
||||
protected const DEFAULT_CHANNEL = 10;
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
// 只取未同步的记录
|
||||
$leads = Db::table('fb_leads')
|
||||
->where('sync_status', 0)
|
||||
->get();
|
||||
|
||||
if ($leads->isEmpty()) {
|
||||
$output->writeln('<comment>没有需要同步的 fb_leads(sync_status = 0)</comment>');
|
||||
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('<info>已导入 ' . count($insert) . ' 条数据到 inquiry_log,并更新 fb_leads.sync_status = 1</info>');
|
||||
}
|
||||
});
|
||||
|
||||
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[] = '<p><strong>意向车型:</strong>' . $e($lead['car_model']) . '</p>';
|
||||
}
|
||||
if (!empty($lead['car_quantity']) || !empty($lead['car_quantity_text'])) {
|
||||
$qty = $lead['car_quantity'] ?? $lead['car_quantity_text'] ?? '';
|
||||
$lines[] = '<p><strong>意向数量:</strong>' . $e($qty) . '</p>';
|
||||
}
|
||||
if (!empty($lead['favorite_color'])) {
|
||||
$lines[] = '<p><strong>意向颜色:</strong>' . $e($lead['favorite_color']) . '</p>';
|
||||
}
|
||||
if (!empty($lead['usage_type'])) {
|
||||
$lines[] = '<p><strong>用途:</strong>' . $e($lead['usage_type']) . '</p>';
|
||||
}
|
||||
if (!empty($lead['seat_preference'])) {
|
||||
$lines[] = '<p><strong>座位数偏好:</strong>' . $e($lead['seat_preference']) . '</p>';
|
||||
}
|
||||
|
||||
$lines[] = '<hr />';
|
||||
$lines[] = '<p><strong>广告与表单信息:</strong></p>';
|
||||
$lines[] = '<p>平台:' . $e($lead['platform'] ?? '') . '</p>';
|
||||
$lines[] = '<p>Campaign:' . $e($lead['campaign_name'] ?? '') . ' (#' . $e($lead['campaign_id'] ?? '') . ')</p>';
|
||||
$lines[] = '<p>Adset:' . $e($lead['adset_name'] ?? '') . ' (#' . $e($lead['adset_id'] ?? '') . ')</p>';
|
||||
$lines[] = '<p>Ad:' . $e($lead['ad_name'] ?? '') . ' (#' . $e($lead['ad_id'] ?? '') . ')</p>';
|
||||
$lines[] = '<p>Form:' . $e($lead['form_name'] ?? '') . ' (#' . $e($lead['form_id'] ?? '') . ')</p>';
|
||||
|
||||
$lines[] = '<hr />';
|
||||
$lines[] = '<p><strong>Meta 信息:</strong></p>';
|
||||
$lines[] = '<p>Lead ID:' . $e($lead['fb_lead_id'] ?? '') . '</p>';
|
||||
$lines[] = '<p>FB 创建时间原始值:' . $e($lead['fb_created_time'] ?? '') . '</p>';
|
||||
|
||||
if (!empty($lead['inbox_url'])) {
|
||||
$url = $e($lead['inbox_url']);
|
||||
$lines[] = '<p>Inbox 链接:<a href="' . $url . '" target="_blank">' . $url . '</a></p>';
|
||||
}
|
||||
|
||||
return implode("\n", $lines);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
<?php
|
||||
|
||||
namespace app\command;
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use support\Db;
|
||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||
use app\inquery\utils\FbLeadHeaderMapper;
|
||||
use support\exception\BusinessException;
|
||||
|
||||
class TbCommand extends Command
|
||||
{
|
||||
protected static $defaultName = 'tb-db';
|
||||
protected static $defaultDescription = '导入 Facebook Leads Excel 数据';
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->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("<info>开始导入单个文件:</info> {$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("<comment>目录 {$baseDir} 下没有找到 .xlsx 文件</comment>");
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$output->writeln("<info>开始批量导入目录:</info> {$baseDir}");
|
||||
foreach ($files as $filePath) {
|
||||
$output->writeln(" -> 文件:{$filePath}");
|
||||
try {
|
||||
$this->importOneFile($filePath, $output);
|
||||
$output->writeln(" <info>导入成功</info>");
|
||||
} catch (\Throwable $e) {
|
||||
// 某个文件失败,不影响后面的文件
|
||||
$output->writeln(" <error>导入失败:{$e->getMessage()}</error>");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(" <comment>文件 {$filePath} 没有有效数据行</comment>");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 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"
|
||||
|
||||
*/
|
||||
@@ -9,6 +9,11 @@ use support\Log;
|
||||
class IndexController
|
||||
{
|
||||
|
||||
public function index()
|
||||
{
|
||||
return 22;
|
||||
}
|
||||
|
||||
public function form(Request $request, int $id)
|
||||
{
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace app\controller;
|
||||
|
||||
use support\Request;
|
||||
use support\Log;
|
||||
use app\utils\Cf7InquiryClient;
|
||||
|
||||
class TransmitController
|
||||
{
|
||||
|
||||
public function form(Request $request)
|
||||
{
|
||||
try {
|
||||
|
||||
|
||||
// 表单信息 GET /wp-json/acf7db/v1/forms
|
||||
|
||||
$client = new Cf7InquiryClient(
|
||||
'https://hesupipe.com',
|
||||
'dev_server',
|
||||
'eakt wS0o TWVC G9Ys KDAh Djmx'
|
||||
);
|
||||
|
||||
// $result = $client->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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace app\inquery\controller;
|
||||
|
||||
use support\Request;
|
||||
use support\Db;
|
||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||
use app\inquery\utils\FbLeadHeaderMapper;
|
||||
use support\exception\BusinessException;
|
||||
|
||||
class InqueryController
|
||||
{
|
||||
|
||||
public function form(Request $request)
|
||||
{
|
||||
|
||||
$filePath = public_path('excel/byd song plus_Leads_2025-12-04_2025-12-09.xlsx');
|
||||
|
||||
$spreadsheet = IOFactory::load($filePath);
|
||||
$sheet = $spreadsheet->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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
|
||||
namespace app\inquery\utils;
|
||||
|
||||
use support\exception\BusinessException;
|
||||
|
||||
class FbLeadHeaderMapper
|
||||
{
|
||||
/**
|
||||
* Excel 原始表头 => 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace app\inquery\utils;
|
||||
|
||||
use support\Db;
|
||||
|
||||
class PhoneCountryHelper
|
||||
{
|
||||
/**
|
||||
* 根据电话/WhatsApp 号码识别国家 ID
|
||||
* 号码格式可以是:+996557660304、996557660304、00996557660304 等
|
||||
*/
|
||||
public static function detectCountryId(?string $rawPhone): ?int
|
||||
{
|
||||
if (!$rawPhone) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 1. 只保留数字和+
|
||||
$phone = preg_replace('/[^\d\+]/', '', $rawPhone);
|
||||
|
||||
if ($phone === '' || $phone === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 2. 统一前缀:00 开头 -> +
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace app\inquery\utils;
|
||||
|
||||
use support\Redis;
|
||||
use support\exception\BusinessException;
|
||||
|
||||
class RoundRobinAssigner
|
||||
{
|
||||
/**
|
||||
* Redis key 前缀
|
||||
* 你可以按业务分组:比如 "fb_inquiry", "website_inquiry" 等
|
||||
*/
|
||||
protected const REDIS_KEY_PREFIX = 'inquiry:assign:round_robin:';
|
||||
|
||||
/**
|
||||
* 从一组业务员 ID 中按轮询规则返回“下一个业务员 ID”
|
||||
*
|
||||
* @param int[] $userIds 业务员 ID 列表(有顺序)
|
||||
* @param string $groupKey 分配分组标识(同一个分组共享一个轮询指针)
|
||||
*
|
||||
* @return int 下一位业务员 ID
|
||||
* @throws BusinessException
|
||||
*/
|
||||
public static function nextUserId(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);
|
||||
|
||||
// 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];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace app\utils;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
|
||||
/**
|
||||
* 通用的 CF7 + Advanced CF7 DB 询盘客户端
|
||||
*
|
||||
* 支持多站点使用:
|
||||
* - 每个站点 new 一个实例(传入 baseUri + auth)
|
||||
* - 调用 fetchForms / fetchEntries 获取对应站点的数据
|
||||
*/
|
||||
class Cf7InquiryClient
|
||||
{
|
||||
protected Client $client;
|
||||
|
||||
/** 站点基础地址,例如:https://hesupipe.com */
|
||||
protected string $baseUri;
|
||||
|
||||
/** 构造函数:传入当前要访问的 WP 站点配置 */
|
||||
public function __construct(string $baseUri, string $username, string $password, array $options = [])
|
||||
{
|
||||
$this->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' => []];
|
||||
}
|
||||
}
|
||||
+8
-4
@@ -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. "
|
||||
|
||||
Generated
+1066
-35
File diff suppressed because it is too large
Load Diff
+6
-6
@@ -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' => [
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
return [
|
||||
'enable' => 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
|
||||
',
|
||||
];
|
||||
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
return [
|
||||
'enable' => true,
|
||||
];
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
use Webman\RedisQueue\Command\MakeConsumerCommand;
|
||||
|
||||
return [
|
||||
MakeConsumerCommand::class
|
||||
];
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @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],
|
||||
],
|
||||
]
|
||||
],
|
||||
]
|
||||
];
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
return [
|
||||
'consumer' => [
|
||||
'handler' => Webman\RedisQueue\Process\Consumer::class,
|
||||
'count' => 8, // 可以设置多进程同时消费
|
||||
'constructor' => [
|
||||
// 消费者类目录
|
||||
'consumer_dir' => app_path() . '/queue/redis'
|
||||
]
|
||||
]
|
||||
];
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
return [
|
||||
'default' => [
|
||||
// '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,
|
||||
]
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @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,
|
||||
],
|
||||
]
|
||||
];
|
||||
+2
-2
@@ -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']);
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
use Webman\Config;
|
||||
use Webman\Console\Command;
|
||||
use Webman\Console\Util;
|
||||
use support\Container;
|
||||
use Dotenv\Dotenv;
|
||||
|
||||
if (!Phar::running()) {
|
||||
chdir(__DIR__);
|
||||
}
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
if (!$appConfigFile = config_path('app.php')) {
|
||||
throw new RuntimeException('Config file not found: app.php');
|
||||
}
|
||||
|
||||
if (class_exists(Dotenv::class) && file_exists(run_path('.env'))) {
|
||||
if (method_exists(Dotenv::class, 'createUnsafeImmutable')) {
|
||||
Dotenv::createUnsafeImmutable(run_path())->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();
|
||||
Reference in New Issue
Block a user