diff --git a/app/command/DomainCommand.php b/app/command/DomainCommand.php
index d23e755..7d09da5 100644
--- a/app/command/DomainCommand.php
+++ b/app/command/DomainCommand.php
@@ -11,6 +11,9 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use support\Db;
+/**
+ * 主要处理询单详情中的图片地址
+ */
class DomainCommand extends Command
{
protected static $defaultName = 'domain';
diff --git a/app/command/EmailDetailCommand.php b/app/command/EmailDetailCommand.php
new file mode 100644
index 0000000..78e5720
--- /dev/null
+++ b/app/command/EmailDetailCommand.php
@@ -0,0 +1,206 @@
+addOption(
+ 'per-page', 'p',
+ InputOption::VALUE_OPTIONAL,
+ '每批处理多少条详情',
+ config('imap.per_page', 50)
+ );
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ try {
+ $cfg = config('imap');
+ $mailbox = $cfg['mailbox'];
+ $username = $cfg['username'];
+ $password = $cfg['password'];
+ $batch = (int)$input->getOption('per-page');
+
+ // 打开 IMAP
+ $inbox = @imap_open($mailbox, $username, $password);
+ if (!$inbox) {
+ $output->writeln("IMAP 连接失败:". imap_last_error() ."");
+ return Command::FAILURE;
+ }
+
+ // 阶段二:逐批拉取详情并写入 emails 表
+ $output->writeln("依次拉取详情并写入 emails 表 …");
+ while (true) {
+ $pending = Db::table('email_uids')
+ ->where('status', 0)
+ // ->where('uid', '>', 13399)
+ ->orderBy('uid', 'desc')
+ ->limit(2)
+ ->get();
+
+ if (count($pending) === 0) {
+ $output->writeln("所有 UID 均已处理完毕。");
+ break;
+ }
+
+ $output->writeln("处理下一批 " . count($pending) . " 条 …");
+
+ foreach ($pending as $row) {
+ $uid = $row->uid;
+
+ // 包裹整行处理在 try/catch 中,任何异常都不会中断循环
+ try {
+ $output->writeln("---- 开始处理 UID={$uid} ----");
+
+ // 1. fetch overview
+ $ov = @imap_fetch_overview($inbox, $uid, FT_UID)[0] ?? null;
+ if (!$ov) {
+ $err = imap_last_error() ?: '未知错误';
+ $output->writeln("UID={$uid} overview 失败:{$err}");
+ $this->logError("UID={$uid} overview 失败:{$err}");
+ continue;
+ }
+ $subject = mb_convert_encoding($ov->subject ?? '', 'UTF-8', 'GBK,UTF-8');
+ $from = $ov->from ?? '';
+ $date = $ov->date ?? '';
+
+ $output->writeln("UID={$uid} overview 成功,subject={$subject}");
+
+ // 2. fetch body
+ $body = $this->getMailBody($inbox, $uid, true);
+ $output->writeln("UID={$uid} body 长度:" . strlen($body));
+
+ // 3. sanitize & insert
+ $body = $this->sanitizeUtf8($body);
+ $now = time();
+
+ try {
+ Db::table('emails')->insert([
+ 'uid' => $uid,
+ 'subject' => $subject,
+ 'from' => $from,
+ 'date' => $date,
+ 'body' => $body,
+ 'created_at' => $now,
+ 'updated_at' => $now,
+ ]);
+ $output->writeln("UID={$uid} 写入 emails 成功");
+ } catch (\Throwable $e) {
+ $output->writeln("UID={$uid} 写入失败:{$e->getMessage()}");
+ $this->logError("UID={$uid} 写入失败:{$e->getMessage()}");
+ continue;
+ }
+
+ // 4. 标记已处理
+ try {
+ Db::table('email_uids')
+ ->where('uid', $uid)
+ ->update([
+ 'status' => 1,
+ 'processed_at' => $now,
+ ]);
+ $output->writeln("UID={$uid} 标记为已处理");
+ } catch (\Throwable $e) {
+ $output->writeln("UID={$uid} 标记状态失败:{$e->getMessage()}");
+ $this->logError("UID={$uid} 标记状态失败:{$e->getMessage()}");
+ // 即使更新 status 失败,也继续下一条
+ }
+ } catch (\Throwable $e) {
+ // 捕获任何非预期错误
+ $output->writeln("UID={$uid} 处理异常:{$e->getMessage()}");
+ $this->logError("UID={$uid} 处理异常:{$e->getMessage()}");
+ continue;
+ }
+ }
+
+ // 批次结束后稍作休眠,防止服务器过载
+ sleep(1);
+ }
+
+ imap_close($inbox);
+ $output->writeln("\n✅ 全部邮件处理完成。");
+ return Command::SUCCESS;
+ } catch (\Throwable $th) {
+ dpr($th);
+ return Command::FAILURE;
+ }
+ }
+
+ /**
+ * 将任意字符串清洗为有效 UTF-8,移除或替换无效字节
+ */
+ protected function sanitizeUtf8(string $text): string
+ {
+ return iconv('UTF-8', 'UTF-8//IGNORE', $text);
+ }
+
+ /**
+ * 记录错误到日志
+ */
+ protected function logError(string $msg): void
+ {
+ $line = '[' . date('Y-m-d H:i:s') . '] ' . $msg . "\n";
+ file_put_contents(
+ runtime_path('logs/email_errors.log'),
+ $line,
+ FILE_APPEND | LOCK_EX
+ );
+ }
+
+ /**
+ * 按编码解码并保留 "\r\n"
+ */
+ protected function getMailBody($inbox, int $id, bool $isUid = false): string
+ {
+ $flags = $isUid ? FT_UID : 0;
+ $structure = imap_fetchstructure($inbox, $id, $flags);
+
+ if (!empty($structure->parts)) {
+ // HTML 部分
+ foreach ($structure->parts as $i => $part) {
+ if (isset($part->subtype) && strtolower($part->subtype) === 'html') {
+ $data = imap_fetchbody($inbox, $id, (string)($i+1), $flags);
+ return $this->decodeBody($data, $part->encoding);
+ }
+ }
+ // 文本部分
+ foreach ($structure->parts as $i => $part) {
+ if (isset($part->subtype) && strtolower($part->subtype) === 'plain') {
+ $data = imap_fetchbody($inbox, $id, (string)($i+1), $flags);
+ return $this->decodeBody($data, $part->encoding);
+ }
+ }
+ // 原始 body
+ return imap_body($inbox, $id, $flags);
+ }
+
+ return imap_body($inbox, $id, $flags);
+ }
+
+ /**
+ * 按编码解码
+ */
+ protected function decodeBody(string $body, int $encoding): string
+ {
+ switch ($encoding) {
+ case 3: return imap_base64($body);
+ case 4: return imap_qprint($body);
+ default: return $body;
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/command/EmailParseCommand.php b/app/command/EmailParseCommand.php
new file mode 100644
index 0000000..3bf21f7
--- /dev/null
+++ b/app/command/EmailParseCommand.php
@@ -0,0 +1,144 @@
+orderBy('id', 'asc')
+ ->limit($batchSize)
+ ->offset($page * $batchSize)
+ ->get();
+
+ if ($rows->isEmpty()) {
+ break;
+ }
+
+ $toInsert = [];
+ foreach ($rows as $row) {
+ $parsed = $this->parseHtml($row->body);
+ if ($parsed === null) {
+ $output->writeln(" ↳ [{$row->id}] skip: not a contact-form HTML");
+ continue;
+ }
+
+ // 严格按 $fields 顺序填充每条记录,缺失的字段补 null
+ $record = [];
+ foreach ($fields as $col) {
+ if ($col === 'email_id') {
+ $record[$col] = $row->id;
+ } else {
+ $record[$col] = $parsed[$col] ?? null;
+ }
+ }
+
+ $toInsert[] = $record;
+ }
+
+ if (!empty($toInsert)) {
+ Db::table('email_parsed_result')->insert($toInsert);
+ $output->writeln("Inserted batch #" . ($page + 1) . ", count: " . count($toInsert));
+ }
+
+ $page++;
+ }
+
+ $output->writeln("✔ All done.");
+ return Command::SUCCESS;
+ }
+
+ /**
+ * 解析 HTML,找不到
Preview
则返回 null,
+ * 否则提取所有 Key: Value 为小写蛇形 key=>value 数组
+ */
+ protected function parseHtml(string $html): ?array
+ {
+ $doc = new \DOMDocument();
+ libxml_use_internal_errors(true);
+ $doc->loadHTML('' . $html);
+ libxml_clear_errors();
+
+ $xpath = new \DOMXPath($doc);
+
+ // 类型检查
+ if ($xpath->query("//h2[text()='Preview']")->length === 0) {
+ return null;
+ }
+
+ $nodes = $xpath->query("//div[span]");
+ $data = [];
+
+ /** @var \DOMElement $node */
+ foreach ($nodes as $node) {
+ $span = $xpath->query("span", $node)->item(0);
+ if (! $span) {
+ continue;
+ }
+
+ // 原始键名
+ $key = trim(str_replace(':', '', $span->textContent));
+ // 转成小写蛇形
+ $col = strtolower(str_replace(' ', '_', $key));
+
+ // 获取值
+ $a = $xpath->query(".//a", $node)->item(0);
+ if ($a && $a->hasAttribute('href')) {
+ $value = trim($a->getAttribute('href'));
+ } else {
+ $clone = $node->cloneNode(true);
+ $spanClone = $xpath->query(".//span", $clone)->item(0);
+ if ($spanClone) {
+ $spanClone->parentNode->removeChild($spanClone);
+ }
+ $value = trim($clone->textContent);
+ }
+
+ $data[$col] = $value;
+ }
+
+ return $data;
+ }
+}
\ No newline at end of file
diff --git a/app/command/EmailUidCommand.php b/app/command/EmailUidCommand.php
new file mode 100644
index 0000000..bf0e744
--- /dev/null
+++ b/app/command/EmailUidCommand.php
@@ -0,0 +1,185 @@
+addOption(
+ // 'per-page', 'p',
+ // InputOption::VALUE_OPTIONAL,
+ // '每批处理多少条详情',
+ // config('imap.per_page', 50)
+ // );
+ // }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ try {
+ $cfg = config('imap');
+ $mailbox = $cfg['mailbox'];
+ $username = $cfg['username'];
+ $password = $cfg['password'];
+ // $batch = (int)$input->getOption('per-page');
+
+ $retryTimes = 3;
+ $retryDelay = 10; // 秒
+
+ $inbox = false;
+ $attempt = 0;
+
+ $maxUid = Db::table('email_uids')->max('uid');
+
+ while ($attempt < $retryTimes) {
+ $attempt++;
+ $output->writeln("尝试第 {$attempt} 次连接 IMAP...");
+ $inbox = @imap_open($mailbox, $username, $password);
+
+ if ($inbox) {
+ break;
+ }
+
+ $error = imap_last_error();
+ $output->writeln("第 {$attempt} 次失败:{$error}");
+
+ // 若出现 too many login failures 立即暂停一段时间
+ if (stripos($error, 'Too many login failures') !== false) {
+ $output->writeln("检测到登录失败次数过多,暂停 60 秒避免封禁");
+ sleep(60);
+ } else {
+ sleep($retryDelay);
+ }
+ }
+
+ if (!$inbox) {
+ $output->writeln("最终仍然连接失败:" . imap_last_error() . "");
+ return Command::FAILURE;
+ }
+
+ // 拉取 UID 并入库
+ $output->writeln("开始拉取 UID 并入库 …");
+ $uids = imap_search($inbox, 'ALL', SE_UID) ?: [];
+
+ $uids = array_filter($uids, function($uid) use ($maxUid) {
+ return $uid > $maxUid;
+ });
+
+ sort($uids);
+ $now = time();
+ $count = 0;
+
+ foreach ($uids as $uid) {
+ try {
+ Db::table('email_uids')->insert([
+ 'uid' => $uid,
+ 'fetched_at' => $now,
+ 'email' => $cfg['username']
+ ]);
+ $count++;
+
+ $output->writeln("UID:".$uid." 已写入数据库");
+ } catch (\Throwable $e) {
+ $output->writeln("{$e->getMessage()}");
+ }
+ }
+
+ imap_close($inbox);
+ $output->writeln(date('Y-m-d H:i:s').":共拉到 {$count} 个 UID 并写入 email_uids 表。");
+ return Command::SUCCESS;
+ } catch (\Throwable $th) {
+ dpr($th);
+ return Command::FAILURE;
+ }
+ }
+}
+
+/**
+ * 获取指定格式的日期字符串
+ *
+ * @param string|null $input_date 可选,指定的日期(可以是任何PHP可解析的日期格式)
+ * @return string 返回格式为 'd-M-Y' 的日期字符串(如 '28-Jul-2025')
+ */
+function getImapFormattedDate($input_date = null) {
+ // 如果有传入日期参数,尝试解析
+ if ($input_date !== null) {
+ $timestamp = strtotime($input_date);
+ if ($timestamp === false) {
+ // 如果日期解析失败,使用当前日期并记录警告
+ trigger_error("无法解析提供的日期: '$input_date',将使用当前日期", E_USER_WARNING);
+ $timestamp = time();
+ }
+ } else {
+ // 如果没有提供日期,使用当前日期
+ $timestamp = time();
+ }
+
+ // 格式化为 IMAP 需要的日期格式 (如 '28-Jul-2025')
+ return date('d-M-Y', $timestamp);
+}
+
+// // 使用示例
+// echo getImapFormattedDate(); // 输出当天的日期,如 '31-Jul-2025'
+// echo getImapFormattedDate('2025-07-28'); // 输出 '28-Jul-2025'
+// echo getImapFormattedDate('28/07/2025'); // 输出 '28-Jul-2025'
+// echo getImapFormattedDate('next Monday'); // 输出下个周一的日期
+
+/*
+
+// imap_search() 条件
+
+// 1. 基本日期搜索(按接收日期)
+// 搜索今天收到的邮件
+$today = date("d-M-Y");
+$uids = imap_search($inbox, 'SINCE "' . $today . '"', SE_UID);
+
+// 搜索特定日期之后的邮件
+$since_date = '15-Jul-2023';
+$uids = imap_search($inbox, 'SINCE "' . $since_date . '"', SE_UID);
+
+// 搜索特定日期之前的邮件
+$before_date = '20-Jul-2023';
+$uids = imap_search($inbox, 'BEFORE "' . $before_date . '"', SE_UID);
+
+// 搜索特定日期的邮件
+$on_date = '18-Jul-2023';
+$uids = imap_search($inbox, 'ON "' . $on_date . '"', SE_UID);
+
+// 2. 日期范围搜索
+// 搜索2023年7月15日到2023年7月20日之间的邮件
+$start_date = '15-Jul-2023';
+$end_date = '20-Jul-2023';
+$uids = imap_search($inbox, 'SINCE "' . $start_date . '" BEFORE "' . $end_date . '"', SE_UID);
+
+// 3. 结合日期和其他条件
+// 搜索2023年7月以后未读的邮件
+$date = '1-Jul-2023';
+$uids = imap_search($inbox, 'SINCE "' . $date . '" UNSEEN', SE_UID);
+
+// 搜索2023年7月来自某人的邮件
+$date = '1-Jul-2023';
+$from = 'example@domain.com';
+$uids = imap_search($inbox, 'SINCE "' . $date . '" FROM "' . $from . '"', SE_UID);
+
+// 4. 日期格式注意事项
+// - IMAP日期格式通常为 d-M-Y,例如 15-Jul-2023
+// - 月份使用英文缩写(Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec)
+// - 日期格式必须用双引号括起来
+
+所有ID
+imap_search($inbox, 'ALL', SE_UID)
+
+*/
\ No newline at end of file
diff --git a/app/command/FixInquiryLogOrderNo.php b/app/command/FixInquiryLogOrderNo.php
index ed959cd..91d3c04 100644
--- a/app/command/FixInquiryLogOrderNo.php
+++ b/app/command/FixInquiryLogOrderNo.php
@@ -7,6 +7,9 @@ use Symfony\Component\Console\Output\OutputInterface;
use support\Db;
use support\Log;
+/**
+ * 处理询单中的编号重复
+ */
class FixInquiryLogOrderNo extends Command
{
protected static $defaultName = 'sn';
diff --git a/app/command/TbCommand.php b/app/command/TbCommand.php
index d60482f..fa0a33a 100644
--- a/app/command/TbCommand.php
+++ b/app/command/TbCommand.php
@@ -9,7 +9,11 @@ use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use support\Db;
+use app\common\utils\email\Emails;
+/**
+ * 批量处理用户编号(也用于调试)
+ */
class TbCommand extends Command
{
protected static $defaultName = 'tb-db';
@@ -17,12 +21,16 @@ class TbCommand extends Command
protected function execute(InputInterface $input, OutputInterface $output): int
{
- $user = new User();
- $list = $user->all();
+ // $user = new User();
+ // $list = $user->all();
- foreach ($list as $item) {
- // UserSn::where('user_sn', $item->sn)->update(['user_id' => $item->id]);
- }
+ // foreach ($list as $item) {
+ // // UserSn::where('user_sn', $item->sn)->update(['user_id' => $item->id]);
+ // }
+
+ $m = new Emails();
+ $m->process();
+
return self::SUCCESS;
}
}
\ No newline at end of file
diff --git a/app/common/logic/AuthLogic.php b/app/common/logic/AuthLogic.php
index b2a74fc..59155dc 100644
--- a/app/common/logic/AuthLogic.php
+++ b/app/common/logic/AuthLogic.php
@@ -93,7 +93,7 @@ class AuthLogic
->orWhere('email', $email);
})
->first();
-
+ $user->isLogin = true;
if (!$user) {
// 获取编号
$userSn = $this->getAvailableUserSn();
diff --git a/app/common/model/InquiryLog.php b/app/common/model/InquiryLog.php
index ce01b4a..68926aa 100644
--- a/app/common/model/InquiryLog.php
+++ b/app/common/model/InquiryLog.php
@@ -60,7 +60,7 @@ class InquiryLog extends Model
return $this->hasOne(User::class, 'id', 'enter_user_id');
}
- public function recipient()
+ public function recipientInfo()
{
return $this->hasOne(User::class, 'id', 'recipient');
}
diff --git a/app/common/utils/email/Emails.php b/app/common/utils/email/Emails.php
new file mode 100644
index 0000000..4e87a4b
--- /dev/null
+++ b/app/common/utils/email/Emails.php
@@ -0,0 +1,410 @@
+mailbox = $options['mailbox'];
+ $this->username = $options['username'];
+ $this->password = $options['password'];
+ $this->limit = $options['limit'];
+ $this->per_page = $options['per_page'];
+ $this->batch_size = $options['batch_size'];
+ }
+ }
+
+ /**
+ * 将任意字符串清洗为有效 UTF-8,移除或替换无效字节
+ */
+ protected function sanitizeUtf8(string $text): string
+ {
+ return iconv('UTF-8', 'UTF-8//IGNORE', $text);
+ }
+
+ /**
+ * 记录错误到日志
+ */
+ protected function logError(string $msg): void
+ {
+ $line = '[' . date('Y-m-d H:i:s') . '] ' . $msg . "\n";
+ file_put_contents(
+ runtime_path('logs/email_errors.log'),
+ $line,
+ FILE_APPEND | LOCK_EX
+ );
+ }
+
+ /**
+ * 按编码解码并保留 "\r\n"
+ */
+ protected function getMailBody($inbox, int $id, bool $isUid = false): string
+ {
+ $flags = $isUid ? FT_UID : 0;
+ $structure = imap_fetchstructure($inbox, $id, $flags);
+
+ if (!empty($structure->parts)) {
+ // HTML 部分
+ foreach ($structure->parts as $i => $part) {
+ if (isset($part->subtype) && strtolower($part->subtype) === 'html') {
+ $data = imap_fetchbody($inbox, $id, (string)($i+1), $flags);
+ return $this->decodeBody($data, $part->encoding);
+ }
+ }
+ // 文本部分
+ foreach ($structure->parts as $i => $part) {
+ if (isset($part->subtype) && strtolower($part->subtype) === 'plain') {
+ $data = imap_fetchbody($inbox, $id, (string)($i+1), $flags);
+ return $this->decodeBody($data, $part->encoding);
+ }
+ }
+ // 原始 body
+ return imap_body($inbox, $id, $flags);
+ }
+
+ return imap_body($inbox, $id, $flags);
+ }
+
+ /**
+ * 按编码解码
+ */
+ protected function decodeBody(string $body, int $encoding): string
+ {
+ switch ($encoding) {
+ case 3: return imap_base64($body);
+ case 4: return imap_qprint($body);
+ default: return $body;
+ }
+ }
+
+ /**
+ * 解析 HTML,找不到 Preview
则返回 null,
+ * 否则提取所有 Key: Value 为小写蛇形 key=>value 数组
+ */
+ protected function parseHtml(string $html): ?array
+ {
+ $doc = new \DOMDocument();
+ libxml_use_internal_errors(true);
+ $doc->loadHTML('' . $html);
+ libxml_clear_errors();
+
+ $xpath = new \DOMXPath($doc);
+
+ // 类型检查
+ if ($xpath->query("//h2[text()='Preview']")->length === 0) {
+ return null;
+ }
+
+ $nodes = $xpath->query("//div[span]");
+ $data = [];
+
+ /** @var \DOMElement $node */
+ foreach ($nodes as $node) {
+ $span = $xpath->query("span", $node)->item(0);
+ if (! $span) {
+ continue;
+ }
+
+ // 原始键名
+ $key = trim(str_replace(':', '', $span->textContent));
+ // 转成小写蛇形
+ $col = strtolower(str_replace(' ', '_', $key));
+
+ // 获取值
+ $a = $xpath->query(".//a", $node)->item(0);
+ if ($a && $a->hasAttribute('href')) {
+ $value = trim($a->getAttribute('href'));
+ } else {
+ $clone = $node->cloneNode(true);
+ $spanClone = $xpath->query(".//span", $clone)->item(0);
+ if ($spanClone) {
+ $spanClone->parentNode->removeChild($spanClone);
+ }
+ $value = trim($clone->textContent);
+ }
+
+ $data[$col] = $value;
+ }
+
+ return $data;
+ }
+
+ /**
+ * 拉取邮件uid
+ */
+ public function uids()
+ {
+ try {
+ $retryTimes = 3;
+ $retryDelay = 10;
+ $inbox = false;
+ $attempt = 0;
+
+ $maxUid = Db::table('email_uids')->max('uid');
+
+ while ($attempt < $retryTimes) {
+ $attempt++;
+ flg("尝试第 {$attempt} 次连接 IMAP...", false);
+
+ $inbox = @imap_open($this->mailbox, $this->username, $this->password);
+
+ if ($inbox) {
+ break;
+ }
+
+ $error = imap_last_error();
+ flg("第 {$attempt} 次失败:{$error}");
+
+ // 若出现 too many login failures 立即暂停一段时间
+ if (stripos($error, 'Too many login failures') !== false) {
+ flg("检测到登录失败次数过多,暂停 60 秒避免封禁");
+ sleep(60);
+ } else {
+ sleep($retryDelay);
+ }
+ }
+
+ if (!$inbox) {
+ flg("最终仍然连接失败:" . imap_last_error() . "");
+ return false;
+ }
+
+ // 拉取 UID 并入库
+ flg("开始拉取 UID 并入库 …", false);
+ $uids = imap_search($inbox, 'ALL', SE_UID) ?: [];
+
+ $uids = array_filter($uids, function($uid) use ($maxUid) {
+ return $uid > $maxUid;
+ });
+
+ sort($uids);
+ $now = time();
+ $count = 0;
+
+ foreach ($uids as $uid) {
+ try {
+ Db::table('email_uids')->insert([
+ 'uid' => $uid,
+ 'fetched_at' => $now,
+ 'email' => $this->username
+ ]);
+ $count++;
+ flg("UID:".$uid." 已写入数据库", false);
+ } catch (\Throwable $e) {
+ flg("{$e->getMessage()}");
+ }
+ }
+
+ imap_close($inbox);
+ flg("✔ 获取到新询盘邮件 {$count} 条 ...", false);
+ return true;
+ } catch (\Throwable $th) {
+ flg(dpr($th));
+ return false;
+ }
+ }
+
+ /**
+ * 获取邮件详情
+ */
+ public function details()
+ {
+ try {
+ $inbox = @imap_open($this->mailbox, $this->username, $this->password);
+ if (!$inbox) {
+ flg("IMAP 连接失败:". imap_last_error() ."");
+ return false;
+ }
+
+ // 依次拉取详情并写入 emails 表
+ flg("依次拉取详情并写入 emails 表 …", false);
+ while (true) {
+ $pending = Db::table('email_uids')
+ ->where('status', 0)
+ ->orderBy('uid', 'desc')
+ ->limit(2)
+ ->get();
+
+ if (count($pending) === 0) {
+ flg("邮件UID的详情,已拉取完毕。", false);
+ break;
+ }
+
+ flg("处理下一批 " . count($pending) . " 条 …", false);
+
+ foreach ($pending as $row) {
+ $uid = $row->uid;
+
+ // 包裹整行处理在 try/catch 中,任何异常都不会中断循环
+ try {
+ flg("---- 开始处理 UID={$uid} ----", false);
+
+ // 1. fetch overview
+ $ov = @imap_fetch_overview($inbox, $uid, FT_UID)[0] ?? null;
+ if (!$ov) {
+ $err = imap_last_error() ?: '未知错误';
+ flg("UID={$uid} overview 失败:{$err}");
+ continue;
+ }
+ $subject = mb_convert_encoding($ov->subject ?? '', 'UTF-8', 'GBK,UTF-8');
+ $from = $ov->from ?? '';
+ $date = $ov->date ?? '';
+
+ flg("UID={$uid} overview 成功,subject={$subject}", false);
+
+ // 2. fetch body
+ $body = $this->getMailBody($inbox, $uid, true);
+ flg("UID={$uid} body 长度:" . strlen($body), false);
+
+ // 3. sanitize & insert
+ $body = $this->sanitizeUtf8($body);
+ $now = time();
+
+ try {
+ Db::table('emails')->insert([
+ 'uid' => $uid,
+ 'subject' => $subject,
+ 'from' => $from,
+ 'date' => $date,
+ 'body' => $body,
+ 'created_at' => $now,
+ 'updated_at' => $now,
+ ]);
+ flg("UID={$uid} 写入 emails 成功", false);
+ } catch (\Throwable $e) {
+ flg("UID={$uid} 写入失败:{$e->getMessage()}");
+ continue;
+ }
+
+ // 4. 标记已处理
+ try {
+ Db::table('email_uids')
+ ->where('uid', $uid)
+ ->update([
+ 'status' => 1,
+ 'processed_at' => $now,
+ ]);
+ flg("UID={$uid} 标记为已处理", false);
+ } catch (\Throwable $e) {
+ flg("UID={$uid} 标记状态失败:{$e->getMessage()}", false);
+ // 即使更新 status 失败,也继续下一条
+ }
+ } catch (\Throwable $e) {
+ // 捕获任何非预期错误
+ flg("UID={$uid} 处理异常:{$e->getMessage()}");
+ continue;
+ }
+ }
+
+ // 批次结束后稍作休眠,防止服务器过载
+ sleep(1);
+ }
+
+ imap_close($inbox);
+ flg("✔ 新邮件详情拉取完成 ...", false);
+ return true;
+ } catch (\Throwable $th) {
+ flg(dpr($th));
+ return false;
+ }
+ }
+
+ /**
+ * 邮件清洗处理
+ */
+ public function process()
+ {
+ try {
+ $page = 0;
+
+ // 明确定义 parsed_contacts 表里要插入的所有列(顺序无所谓,但要全部包含)
+ $fields = [
+ 'email_id',
+ 'date',
+ 'email',
+ 'name',
+ 'tel',
+ 'company',
+ 'country',
+ 'form_url',
+ 'message',
+ 'ip',
+ 'iplocation',
+ 'ilang',
+ 'mailto',
+ 'navigator_language',
+ 'navigator_platform',
+ 'screen_width',
+ 'screen_height',
+ 'ucountry',
+ ];
+
+ $maxEmailId = Db::table('email_parsed_result')->max('email_id');
+
+ while (true) {
+ $rows = Db::table('emails')
+ ->where('status', 0)
+ ->where('id', '>', $maxEmailId)
+ ->orderBy('id', 'asc')
+ ->limit($this->batch_size)
+ ->offset($page * $this->batch_size)
+ ->get();
+
+ if ($rows->isEmpty()) {
+ break;
+ }
+
+ $toInsert = [];
+ foreach ($rows as $row) {
+ $parsed = $this->parseHtml($row->body);
+ if ($parsed === null) {
+ flg(" ↳ [{$row->id}] skip: not a contact-form HTML", false);
+ continue;
+ }
+
+ // 严格按 $fields 顺序填充每条记录,缺失的字段补 null
+ $record = [];
+ foreach ($fields as $col) {
+ if ($col === 'email_id') {
+ $record[$col] = $row->id;
+ } else {
+ $record[$col] = $parsed[$col] ?? null;
+ }
+ }
+
+ $toInsert[] = $record;
+ }
+
+ if (!empty($toInsert)) {
+ Db::table('email_parsed_result')->insert($toInsert);
+
+ $emailId = array_column($toInsert, 'email_id');
+ Db::table('emails')->whereIn('id', $emailId)->update(['status' => 1]);
+
+ flg("Inserted batch #" . ($page + 1) . ", count: " . count($toInsert), false);
+ }
+
+ $page++;
+ }
+
+ flg("✔ 邮件清洗完毕,等待分配 ...", false);
+ return true;
+ } catch (\Throwable $th) {
+ flg(dpr($th));
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/common/utils/email/MailSend.php b/app/common/utils/email/MailSend.php
new file mode 100644
index 0000000..a5742d3
--- /dev/null
+++ b/app/common/utils/email/MailSend.php
@@ -0,0 +1,69 @@
+ '/path/file', 'name' => '文件名']]
+ * @return bool
+ */
+ public static function send($to, $subject, $body, $attachments = [])
+ {
+ $mail = new PHPMailer(true);
+
+ try {
+ // 服务器配置
+ $mail->isSMTP();
+ $mail->Host = config('imap.smtp_xyh.host'); // SMTP服务器
+ $mail->SMTPAuth = true;
+ $mail->Username = config('imap.smtp_xyh.username'); // 邮箱账号
+ $mail->Password = config('imap.smtp_xyh.password'); // 邮箱密码/授权码
+ $mail->SMTPSecure = config('imap.smtp_xyh.encryption'); // 加密方式 tls/ssl
+ $mail->Port = config('imap.smtp_xyh.port'); // 端口
+
+ // 发件人
+ $mail->setFrom(config('imap.smtp_xyh.from_address'), config('imap.smtp_xyh.from_name'));
+
+ // 收件人(支持单个或多个)
+ if (is_string($to)) {
+ $mail->addAddress($to);
+ } elseif (is_array($to)) {
+ foreach ($to as $email) {
+ $mail->addAddress($email);
+ }
+ }
+
+ // 附件
+ foreach ($attachments as $file) {
+ $mail->addAttachment($file['path'], $file['name'] ?? '');
+ }
+
+ // 内容
+ $mail->isHTML(true);
+ $mail->CharSet = 'UTF-8';
+ $mail->Encoding = 'base64';
+ $mail->Subject = mb_encode_mimeheader($subject, 'UTF-8', 'B', "\r\n");
+ $mail->Body = $body;
+ // $mail->AltBody = strip_tags($body); // 纯文本备用
+ // $mail->AltBody = $altbody;
+
+ $mail->send();
+ return true;
+ } catch (Exception $e) {
+ Log::error("邮件发送失败: " . $e->getMessage());
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/common/utils/scaffold/templates/Email.stub b/app/common/utils/scaffold/templates/Email.stub
new file mode 100644
index 0000000..16fc580
--- /dev/null
+++ b/app/common/utils/scaffold/templates/Email.stub
@@ -0,0 +1,365 @@
+
+
+
+
+
+ 客户信息 - {{order_no}}
+
+
+
+
+
+
+
+
+
+
+
+ 客户名称
+
+
{{contacts}}
+
+
+
+
+
+
+ 国家/地区
+
+
{{country}}
+
+
+
+
+
+
+
+
+ WhatsApp
+
+
{{whatsapp}}
+
+
+
+
+ 发布人
+
+
{{enter_user_id}}
+
+
+
+
+ 跟踪人
+
+
{{recipient}}
+
+
+
+
+
+
+ 录单时间
+
+
{{r_time}}
+
+
+
+
+ 商机等级
+
+
{{quality}}
+
+
+
+
+
+ 详情内容
+
+
+ {{content}}
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/controller/IndexController.php b/app/controller/IndexController.php
index b942887..d9ac96c 100644
--- a/app/controller/IndexController.php
+++ b/app/controller/IndexController.php
@@ -2,14 +2,17 @@
namespace app\controller;
+use app\common\model\InquiryLog;
use app\common\model\User;
use app\common\model\UserRoles;
use app\common\model\UserSystems;
+use app\common\utils\email\MailSend;
use app\common\utils\scaffold\Generate;
use support\Request;
use support\Db;
use support\Cache;
use app\inquiry\service\InquiryLogService;
+use Illuminate\Support\Facades\Mail;
class IndexController
{
@@ -77,7 +80,7 @@ class IndexController
// ],
'model' => [
'module' => 'common',
- 'name' => 'crm_customer'
+ 'name' => 'inquiry_channel'
],
// 'service' => [
// 'module' => 'inquiry',
@@ -91,7 +94,7 @@ class IndexController
// 'module' => 'inquiry',
// 'name' => 'country'
// ],
- 'table' => 'crm_customer'
+ 'table' => 'inquiry_channel'
];
$result = (new Generate($options))->doGenerate();
@@ -101,23 +104,10 @@ class IndexController
public function test()
{
- // $res = ['id'=>25714];
- // \Webman\RedisQueue\Redis::send('inquiry-to-crm', $res);
- // $res = [
- // 'id'=>25717,
- // 'category'=>'inquiry',
- // 'scene'=>'inquiry-person-allocation',
- // 'receive_type'=>'person'
- // ];
+ $data = MailSend::getContent(22);
+ MailSend::send('mzl@hnxinyouhe.com', '【XYH】新的客户线索', replaceTpl($data));
- // $res = [
- // 'id'=>1,
- // 'category'=>'crm',
- // 'scene'=>'crm-person-follow-up',
- // 'receive_type'=>'person'
- // ];
- // \Webman\RedisQueue\Redis::send('feishu-message', $res);
- return 'success';
+ return 22;
}
}
\ No newline at end of file
diff --git a/app/crm/controller/CustomerController.php b/app/crm/controller/CustomerController.php
index e1e3f79..4eb3a6b 100644
--- a/app/crm/controller/CustomerController.php
+++ b/app/crm/controller/CustomerController.php
@@ -162,6 +162,9 @@ class CustomerController extends BaseController
try {
$param = $request->all();
$param['user_id'] = $request->user->uid;
+ if($param['user_id']==78){
+ $param['user_id'] = 101;
+ }
$param['corp_abbr'] = $request->basic_data['corp_abbr'];
$result = (new CustomerService())->list($param);
diff --git a/app/functions.php b/app/functions.php
index 3fed6a2..45af92c 100644
--- a/app/functions.php
+++ b/app/functions.php
@@ -2,7 +2,9 @@
/**
* Here is your custom functions.
*/
-
+
+use Webman\RedisQueue\Redis as RedisQueueRedis;
+
/**
* 标准化账号参数
*
@@ -70,17 +72,126 @@ if (!function_exists('getConfig')) {
* 询单号生成
*/
if (!function_exists('orderNum')) {
- function orderNum() {
+ function orderNum()
+ {
return "SN".date('Ymd') . str_pad(mt_rand(1, 99999), 5, '0', STR_PAD_LEFT);
}
}
/*
- * 调试用
+ * 调试用(console)
*/
if (!function_exists('dpr')) {
function dpr(\Throwable $e)
{
- var_dump("=== === 报错打印 === ===", "文件:".$e->getFile(), "行数:".$e->getLine(), "详情:".$e->getMessage(), "=== === 报错打印 === ===");
+ $contents = "=== === 报错打印 === ===\n";
+ $contents .= "文件:".$e->getFile()."\n";
+ $contents .= "行数:".$e->getLine()."\n";
+ $contents .= "详情:".$e->getMessage()."\n";
+ $contents .= "=== === 报错打印 === ===\n";
+ var_dump($contents);
+ return $contents;
+ }
+}
+
+/**
+ * 调试用(file)
+ */
+if (!function_exists('flg')) {
+ function flg($eventData = '', $isFs = true, $fileName = 'flg', $fileCatalog = 'flg')
+ {
+ if ($eventData == '') {
+ return false;
+ }
+
+ if (is_array($eventData) || is_object($eventData)) {
+ $eventData = json_encode($eventData);
+ }
+
+ $fileCatalog = substr(__FILE__, 0, strpos(__FILE__, 'app/')) . "runtime/" . $fileCatalog;
+ if (!is_dir($fileCatalog)) {
+ mkdir($fileCatalog, 0777, true);
+ exec('chown -R nginx:nginx ' . $fileCatalog);
+ }
+
+ $fileName = $fileName . '-' . date('Ymd') . '.log';
+
+ if ($isFs) {
+ $data = [
+ 'content' => $eventData,
+ 'time' => date('Y-m-d H:i:s', time())
+ ];
+ RedisQueueRedis::send("server", $data);
+ }
+
+ return file_put_contents($fileCatalog . "/" . $fileName, date("Y-m-d H:i:s", time()) . "::" . $eventData . "\n", FILE_APPEND);
+ }
+}
+
+/***
+ * 发送飞书文本消息
+ */
+if (!function_exists('sendFeishuTextMessage')) {
+ function sendFeishuTextMessage($webhookUrl, $message) {
+ $data = [
+ 'msg_type' => 'text',
+ 'content' => [
+ 'text' => $message
+ ]
+ ];
+
+ $ch = curl_init();
+ curl_setopt($ch, CURLOPT_URL, $webhookUrl);
+ curl_setopt($ch, CURLOPT_POST, 1);
+ curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
+ curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+
+ $response = curl_exec($ch);
+ curl_close($ch);
+
+ return $response;
+ }
+}
+
+/**
+ * 替换模板
+ */
+if (!function_exists('replaceTpl')) {
+ function replaceTpl(array $data = [], string $templatePath = 'common/utils/scaffold/templates/Email.stub')
+ {
+ if (!$data || !$templatePath) return '';
+ $templatePath = app_path($templatePath);
+ $content = file_get_contents($templatePath);
+ foreach ($data as $key => $value) {
+ $content = str_replace('{{'.$key.'}}', $value, $content);
+ }
+ return $content;
+ }
+}
+
+/**
+ * 替换模板
+ */
+if (!function_exists('InquiryFormat')) {
+ function InquiryFormat(array $data = [])
+ {
+ if (!$data) return '';
+ $content = '';
+ // foreach ($data as $key => $value) {
+ // $content .= $key . ':' . $value . "\r\n";
+ // }
+ $content .= '编号:'.$data['order_no'] . "\r\n";
+ $content .= '询单质量:'.$data['quality'] . "\r\n";
+ $content .= '渠道:'.$data['channel'] . "\r\n";
+ $content .= '邮箱:'.$data['email'] . "\r\n";
+ $content .= '电话:'.$data['tel'] . "\r\n";
+ $content .= 'WhatsApp:'.$data['whatsapp'] . "\r\n";
+ $content .= '国家:'.$data['country'] . "\r\n";
+ $content .= '联系人:'.$data['contacts'] . "\r\n";
+ $content .= '录单人:'.$data['enter_user_id'] . "\r\n";
+ $content .= '录单时间:'.$data['r_time'] . "\r\n";
+ $content .= '内容:'.$data['content'];
+ return $content;
}
}
\ No newline at end of file
diff --git a/app/inquiry/controller/StatisticsController.php b/app/inquiry/controller/StatisticsController.php
new file mode 100644
index 0000000..0e37719
--- /dev/null
+++ b/app/inquiry/controller/StatisticsController.php
@@ -0,0 +1,199 @@
+service = new CountryService();
+ }
+
+ /**
+ * 列表页数据格式化
+ *
+ * @param array $data
+ * @return array
+ */
+ // protected function indexFormat($data)
+ // {
+ // if (isset($data['created_at']))
+ // $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']);
+ // if (isset($data['updated_at']))
+ // $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']);
+ // return $data;
+ // }
+
+ public function whole()
+ {
+ $totals = InquiryLog::count('id');
+ $channelCount = InquiryChannel::count('id');
+ $todayCount = InquiryLog::where('created_at', '>=', strtotime(date('Y-m-d')))->count('id');
+ $releaseCount = InquiryLog::groupBy('enter_user_id')->count('id');
+ $countryCount = Country::count('id');
+ $trackCount = InquiryLog::groupBy('recipient')->count('id');
+ $data = [
+ 'totals' => $totals,
+ 'channelCount' => $channelCount,
+ 'todayCount' => $todayCount,
+ 'releaseCount' => $releaseCount,
+ 'countryCount' => $countryCount,
+ 'trackCount' => $trackCount
+ ];
+ return $this->success($data);
+ }
+
+ // 各渠道计数
+ public function channelCount()
+ {
+ $inquiryChannelList = InquiryChannel::all(['id', 'name']);
+ $channels = [];
+ foreach ($inquiryChannelList as $key => $val) {
+ $channels[$val->id] = $val->name;
+ }
+
+ $list = InquiryLog::select('channel', Db::raw('COUNT(*) as totals'))
+ ->groupBy('channel')
+ ->get()->toArray();
+
+ $channelCounts = [];
+ foreach ($list as $vo) {
+ $channelCounts[] = [
+ "name" => $channels[$vo["channel"]],
+ "value" => $vo["totals"]
+ ];
+ }
+ return $this->success($channelCounts);
+ }
+
+ // 各国家计数
+ public function countryCount()
+ {
+ $list = InquiryLog::with(["countryInfo"])
+ ->select('country', Db::raw('COUNT(*) as totals'))
+ ->groupBy('country')
+ ->get();
+ $arr = [];
+ foreach ($list as $val) {
+ $arr[] = [
+ "value" => $val->totals,
+ "name" => $val->countryInfo->cn_addr
+ ];
+ }
+ return $this->success($arr);
+ }
+
+ // 跟单人计数
+ public function trackCount()
+ {
+ $list = InquiryLog::with(["recipientInfo"])
+ ->where("recipient", ">", 0)
+ ->whereNotIn('recipient', [1,5,10,11,13,15,16,17,19,74,75,78,141,147])
+ ->select('recipient', Db::raw('COUNT(*) as totals'))
+ ->groupBy('recipient')
+ ->get();
+ $optionType = [];
+ $counts = [];
+ foreach ($list as $val) {
+ if (isset($val->recipientInfo) && $val->recipientInfo) {
+ $optionType[] = $val->recipientInfo->name;
+ $counts[] = $val->totals;
+ }
+ }
+ $arr = [
+ "optionType" => $optionType,
+ "counts" => $counts
+ ];
+ return $this->success($arr);
+ }
+
+ // 每日录单数
+ public function dayCount()
+ {
+ $list = Db::table('inquiry_log')
+ ->where('created_at', '>=', Carbon::now()->subDay(7)->timestamp)
+ ->select(
+ Db::raw('DATE(FROM_UNIXTIME(created_at)) as date'),
+ Db::raw('COUNT(*) as order_count')
+ )
+ ->groupBy('date')
+ ->pluck('order_count', 'date')->toArray();
+ $optionType = [];
+ $counts = [];
+ foreach ($list as $key=>$val) {
+ $optionType[] = $key;
+ $counts[] = $val;
+ }
+ $arr = [
+ "optionType" => $optionType,
+ "counts" => $counts
+ ];
+ return $this->success($arr);
+ }
+
+ public function inquiryCount(Request $request)
+ {
+ $companyId = $request->get('company_id') ?? 0;
+
+ // 1. 计算时间范围
+ $now = Carbon::now();
+ $sixMonthsAgo = $now->copy()->subMonths(5)->startOfMonth(); // 包含6个月
+
+ // 2. 获取关联渠道ID
+ $channelIds = $this->getChannelIdsByCompany($companyId);
+
+ if (empty($channelIds)) {
+ return collect();
+ }
+
+ // 3. 执行查询
+ $list = InquiryLog::select([
+ DB::raw("DATE_FORMAT(FROM_UNIXTIME(r_time), '%Y-%m') AS month"),
+ DB::raw('COUNT(*) AS total_count')
+ ])
+ ->whereIn('channel', $channelIds)
+ ->where('r_time', '>', 0)
+ ->where('r_time', '>=', $sixMonthsAgo->timestamp) // 添加时间限制
+ ->groupBy('month')
+ ->orderBy('month')
+ ->get();
+
+ $optionType = [];
+ $counts = [];
+ foreach ($list as $item) {
+ $optionType[] = $item->month;
+ $counts[] = $item->total_count;
+ }
+ $arr = [
+ "optionType" => $optionType,
+ "counts" => $counts
+ ];
+ return $this->success($arr);
+ }
+
+ /**
+ * 获取公司与渠道的映射
+ */
+ private function getChannelIdsByCompany($companyId)
+ {
+ $data = [
+ 0 => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
+ 1 => [1, 2, 3, 4, 5, 6, 7, 8],
+ 3 => [9, 10, 11]
+ ];
+ return $data[$companyId] ?? [];
+ }
+}
\ No newline at end of file
diff --git a/app/inquiry/service/InquiryLogService.php b/app/inquiry/service/InquiryLogService.php
index c736ceb..667e41d 100644
--- a/app/inquiry/service/InquiryLogService.php
+++ b/app/inquiry/service/InquiryLogService.php
@@ -100,7 +100,7 @@ class InquiryLogService
$config['with']['countryInfo'][] = 'cn_addr';
$config['with']['countryInfo'][] = 'steering';
$config['with']['enter'][] = 'name';
- $config['with']['recipient'][] = 'name';
+ $config['with']['recipientInfo'][] = 'name';
$config['with']['channelInfo'][] = 'name';
break;
case 'show':
@@ -215,7 +215,7 @@ class InquiryLogService
$config['with']['countryInfo'][] = 'cn_addr';
$config['with']['countryInfo'][] = 'steering';
$config['with']['enter'][] = 'name';
- $config['with']['recipient'][] = 'name';
+ $config['with']['recipientInfo'][] = 'name';
$config['with']['channelInfo'][] = 'name';
$config['orWhere'][] = ['id', '=', $id];
$config['orWhere'][] = ['order_no', '=', $id];
@@ -239,4 +239,37 @@ class InquiryLogService
];
return $quality[$id];
}
+
+ /**
+ * 获取内容(发送邮箱用)
+ * @param int $id
+ * @return array
+ */
+ public function getContent(int $id)
+ {
+ $relation = ['enter', 'recipientInfo', 'channelInfo', 'countryInfo'];
+ $info = InquiryLog::with($relation)->find($id);
+
+ $enterName = $info->enter ? $info->enter->name : ' - ';
+ $recipientName = $info->recipientInfo ? $info->recipientInfo->name : ' - ';
+ $channelName = $info->channelInfo ? $info->channelInfo->name : ' - ';
+ $countryName = $info->countryInfo ? $info->countryInfo->cn_addr : ' - ';
+
+ $data = [
+ 'order_no' => $info->order_no && $info->order_no != '' ? $info->order_no : ' - ',
+ 'enter_user_id' => $enterName,
+ 'recipient' => $recipientName,
+ 'quality' => (new \app\inquiry\service\InquiryLogService())->qualityName($info->quality),
+ 'channel' => $channelName,
+ 'email' => $info->email && $info->email != '' ? $info->email : ' - ',
+ 'tel' => $info->tel && $info->tel != '' ? $info->tel : ' - ',
+ 'whatsapp' => $info->whatsapp && $info->whatsapp != '' ? $info->whatsapp : ' - ',
+ 'country' => $countryName,
+ 'contacts' => $info->contacts && $info->contacts != '' ? $info->contacts : ' - ',
+ 'content' => $info->content && $info->content != '' ? $info->content : ' - ',
+ 'r_time' => $info->r_time ? date('Y-m-d H:i:s', $info->r_time) : ' - ',
+ 't_time' => $info->t_time ? date('Y-m-d H:i:s', $info->t_time) : ' - '
+ ];
+ return $data;
+ }
}
\ No newline at end of file
diff --git a/app/process/Emails.php b/app/process/Emails.php
new file mode 100644
index 0000000..7886fff
--- /dev/null
+++ b/app/process/Emails.php
@@ -0,0 +1,34 @@
+uids();
+ });
+
+ // 邮件详情
+ new Crontab('0 */35 * * * *', function(){
+ $email = new EmailsUtils();
+ $email->details();
+ });
+
+ // 邮件处理
+ new Crontab('0 */40 * * * *', function(){
+ $email = new EmailsUtils();
+ $email->process();
+ });
+ }
+
+}
\ No newline at end of file
diff --git a/app/queue/redis/EmailMessage.php b/app/queue/redis/EmailMessage.php
new file mode 100644
index 0000000..8fbeb22
--- /dev/null
+++ b/app/queue/redis/EmailMessage.php
@@ -0,0 +1,51 @@
+getContent($data['id']);
+
+ MailSend::send($data['email'], '【XYH】新的客户线索', replaceTpl($data));
+ } catch (\Throwable $th) {
+ throw $th;
+ }
+ }
+
+ // 消费失败回调
+ /*
+ $package = [
+ 'id' => 1357277951, // 消息ID
+ 'time' => 1709170510, // 消息时间
+ 'delay' => 0, // 延迟时间
+ 'attempts' => 2, // 消费次数
+ 'queue' => 'send-mail', // 队列名
+ 'data' => ['to' => 'tom@gmail.com', 'content' => 'hello'], // 消息内容
+ 'max_attempts' => 5, // 最大重试次数
+ 'error' => '错误信息' // 错误信息
+ ]
+ */
+ public function onConsumeFailure(\Throwable $e, $package)
+ {
+ echo "consume failure\n";
+ echo $e->getMessage() . "\n";
+ // 无需反序列化
+ var_export($package);
+ }
+}
\ No newline at end of file
diff --git a/app/queue/redis/InquiryToCrm.php b/app/queue/redis/InquiryToCrm.php
index ac27813..966d4a3 100644
--- a/app/queue/redis/InquiryToCrm.php
+++ b/app/queue/redis/InquiryToCrm.php
@@ -29,19 +29,20 @@ class InquiryToCrm implements Consumer
// 获取用户信息
$recipient = 0;
$corpId = 0;
+ $uinfo = null;
if ($info->recipient) {
$recipient = $info->recipient;
$uinfo = User::where('id', $info->recipient)->first();
if ($uinfo) {
$corpId = $uinfo->corp_id;
}
- // 消息推送参数 分配
- $scene = 'inquiry-person-allocation';
- $receiveType = 'person';
+ // // 消息推送参数 分配
+ // $scene = 'inquiry-person-allocation';
+ // $receiveType = 'person';
} else {
- // 消息推送参数 发布
- $scene = 'inquiry-group-send';
- $receiveType = 'group';
+ // // 消息推送参数 发布
+ // $scene = 'inquiry-group-send';
+ // $receiveType = 'group';
}
// 同步客户数据
$crmCustomer = new CrmCustomer();
@@ -65,14 +66,17 @@ class InquiryToCrm implements Consumer
throw new BusinessException('同步失败~');
}
- // 发送飞书消息
- $options = [
- 'id' => $info->id,
- 'category' => 'inquiry',
- 'scene' => $scene,
- 'receive_type' => $receiveType
- ];
- \Webman\RedisQueue\Redis::send('feishu-message', $options);
+ // 发送邮件消息
+ if ($uinfo && $uinfo->email) {
+ $options = [
+ 'id' => $info->id,
+ 'email' => $uinfo->email,
+ // 'category' => 'inquiry',
+ // 'scene' => $scene,
+ // 'receive_type' => $receiveType
+ ];
+ \Webman\RedisQueue\Redis::send('email-message', $options);
+ }
}
} catch (\Throwable $th) {
throw $th;
diff --git a/app/queue/redis/Server.php b/app/queue/redis/Server.php
new file mode 100644
index 0000000..d425ea7
--- /dev/null
+++ b/app/queue/redis/Server.php
@@ -0,0 +1,55 @@
+getFile()."|".$th->getLine()."|".$th->getMessage());
+ throw new BusinessException($th->getFile()."|".$th->getLine()."|".$th->getMessage());
+ }
+ }
+
+ // 消费失败回调
+ /*
+ $package = [
+ 'id' => 1357277951, // 消息ID
+ 'time' => 1709170510, // 消息时间
+ 'delay' => 0, // 延迟时间
+ 'attempts' => 2, // 消费次数
+ 'queue' => 'send-mail', // 队列名
+ 'data' => ['to' => 'tom@gmail.com', 'content' => 'hello'], // 消息内容
+ 'max_attempts' => 5, // 最大重试次数
+ 'error' => '错误信息' // 错误信息
+ ]
+ */
+ public function onConsumeFailure(\Throwable $e, $package)
+ {
+ echo "consume failure\n";
+ echo $e->getMessage() . "\n";
+ // 无需反序列化
+ var_export($package);
+ }
+}
\ No newline at end of file
diff --git a/composer.json b/composer.json
index ec04c9f..98e3e54 100644
--- a/composer.json
+++ b/composer.json
@@ -44,7 +44,8 @@
"symfony/translation": "^7.3",
"symfony/filesystem": "^7.3",
"workerman/crontab": "^1.0",
- "phpoffice/phpspreadsheet": "^4.5"
+ "phpoffice/phpspreadsheet": "^4.5",
+ "phpmailer/phpmailer": "^6.10"
},
"suggest": {
"ext-event": "For better performance. "
diff --git a/composer.lock b/composer.lock
index d88df56..edf286f 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": "471a31011b58e1e514bdb9c8963055ad",
+ "content-hash": "505fb3d97c338ea53b787196fcc3daaa",
"packages": [
{
"name": "aliyuncs/oss-sdk-php",
@@ -3095,6 +3095,87 @@
},
"time": "2020-10-15T08:29:30+00:00"
},
+ {
+ "name": "phpmailer/phpmailer",
+ "version": "v6.10.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/PHPMailer/PHPMailer.git",
+ "reference": "bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144",
+ "reference": "bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144",
+ "shasum": ""
+ },
+ "require": {
+ "ext-ctype": "*",
+ "ext-filter": "*",
+ "ext-hash": "*",
+ "php": ">=5.5.0"
+ },
+ "require-dev": {
+ "dealerdirect/phpcodesniffer-composer-installer": "^1.0",
+ "doctrine/annotations": "^1.2.6 || ^1.13.3",
+ "php-parallel-lint/php-console-highlighter": "^1.0.0",
+ "php-parallel-lint/php-parallel-lint": "^1.3.2",
+ "phpcompatibility/php-compatibility": "^9.3.5",
+ "roave/security-advisories": "dev-latest",
+ "squizlabs/php_codesniffer": "^3.7.2",
+ "yoast/phpunit-polyfills": "^1.0.4"
+ },
+ "suggest": {
+ "decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication",
+ "ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses",
+ "ext-openssl": "Needed for secure SMTP sending and DKIM signing",
+ "greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication",
+ "hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication",
+ "league/oauth2-google": "Needed for Google XOAUTH2 authentication",
+ "psr/log": "For optional PSR-3 debug logging",
+ "symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)",
+ "thenetworg/oauth2-azure": "Needed for Microsoft XOAUTH2 authentication"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "PHPMailer\\PHPMailer\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "LGPL-2.1-only"
+ ],
+ "authors": [
+ {
+ "name": "Marcus Bointon",
+ "email": "phpmailer@synchromedia.co.uk"
+ },
+ {
+ "name": "Jim Jagielski",
+ "email": "jimjag@gmail.com"
+ },
+ {
+ "name": "Andy Prevost",
+ "email": "codeworxtech@users.sourceforge.net"
+ },
+ {
+ "name": "Brent R. Matzelle"
+ }
+ ],
+ "description": "PHPMailer is a full-featured email creation and transfer class for PHP",
+ "support": {
+ "issues": "https://github.com/PHPMailer/PHPMailer/issues",
+ "source": "https://github.com/PHPMailer/PHPMailer/tree/v6.10.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/Synchro",
+ "type": "github"
+ }
+ ],
+ "time": "2025-04-24T15:19:31+00:00"
+ },
{
"name": "phpoffice/phpspreadsheet",
"version": "4.5.0",
diff --git a/config/imap.php b/config/imap.php
new file mode 100644
index 0000000..de9d8f1
--- /dev/null
+++ b/config/imap.php
@@ -0,0 +1,52 @@
+ '{imap.feishu.cn:993/imap/ssl}INBOX',
+ 'username' => 'info@borderlesscar.com',
+ 'password' => 'ErkhUA629aocm7Bc',
+ // 最多拉取多少封邮件
+ 'limit' => 5,
+ 'per_page' => 20,
+ 'batch_size'=> 100, // 可选:CLI 不传 -p 时使用
+
+ // stmp
+ 'smtp' => [
+ 'host' => 'smtp.feishu.cn',
+ 'port' => 465,
+ 'username' => 'info@borderlesscar.com',
+ 'password' => 'ErkhUA629aocm7Bc',
+ 'encryption' => 'ssl',
+ 'from_address' => '',
+ 'from_name' => ''
+ ],
+
+ 'smtp_xyh' => [
+ 'host' => 'smtp.feishu.cn',
+ 'port' => 465,
+ 'username' => 'app@hnxinyouhe.com',
+ 'password' => 'akyIUFJ6ihGGjI5h',
+ 'encryption' => 'ssl',
+ 'from_address' => 'app@hnxinyouhe.com',
+ 'from_name' => '【XYH】'
+ ],
+];
+
+// docker run \
+// --name l2tp-vpn-server \
+// --env VPN_IPSEC_PSK='Xyh123456' \
+// --env VPN_USER='xinyouhe' \
+// --env VPN_PASSWORD='Xyh.2025' \
+// --restart=always \
+// --privileged \
+// -p 500:500/udp \
+// -p 4500:4500/udp \
+// -p 1701:1701/udp \
+// -d hwdsl2/ipsec-vpn-server
+
+
+// 拉取邮件的脚本顺序:
+// php webman emails:fetch:uid
+// php webman emails:fetch
+// php webman parse:emails
\ No newline at end of file
diff --git a/config/process.php b/config/process.php
index 05b250f..2de2c20 100644
--- a/config/process.php
+++ b/config/process.php
@@ -63,4 +63,7 @@ return [
'task' => [
'handler' => app\process\Task::class
],
+ 'email' => [
+ 'handler' => app\process\Emails::class
+ ],
];
diff --git a/config/route.php b/config/route.php
index 303c4aa..5023f1f 100644
--- a/config/route.php
+++ b/config/route.php
@@ -89,6 +89,15 @@ Route::group('/admin', function () {
Route::any('/destroy', [app\inquiry\controller\InquiryLogController::class, 'destroy']);
Route::any('/release', [app\inquiry\controller\InquiryLogController::class, 'release']);
Route::any('/allocation', [app\inquiry\controller\InquiryLogController::class, 'allocation']);
+ // 统计
+ Route::group('/statistics', function () {
+ Route::any('/whole', [app\inquiry\controller\StatisticsController::class, 'whole']);
+ Route::any('/channel/count', [app\inquiry\controller\StatisticsController::class, 'channelCount']);
+ Route::any('/country/count', [app\inquiry\controller\StatisticsController::class, 'countryCount']);
+ Route::any('/track/count', [app\inquiry\controller\StatisticsController::class, 'trackCount']);
+ Route::any('/day/count', [app\inquiry\controller\StatisticsController::class, 'dayCount']);
+ Route::any('/inquiry/count', [app\inquiry\controller\StatisticsController::class, 'inquiryCount']);
+ });
});
});