2026-08-18 14:40:53 +08:00
|
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
|
|
namespace app\command;
|
|
|
|
|
|
|
|
|
|
|
|
use app\common\model\CrmCustomer;
|
|
|
|
|
|
use app\common\utils\email\InquiryDigestMail;
|
|
|
|
|
|
use app\common\utils\email\MailSend;
|
|
|
|
|
|
use PhpOffice\PhpSpreadsheet\IOFactory;
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 合塑谷歌网站询盘导入
|
|
|
|
|
|
*
|
|
|
|
|
|
* Excel 新询盘(库外数据)-> 建 crm_customer 档案 -> 按行序轮询分配业务员 -> 每人一封汇总邮件。
|
|
|
|
|
|
* 固定口径:corp_id=1(合塑)、channel_id=4(合塑谷歌)、type=2(导入数据);
|
|
|
|
|
|
* 去重:电话或邮箱与现有未删除客户重复(含本文件内重复)则跳过。
|
|
|
|
|
|
*/
|
|
|
|
|
|
class HesuGoogleInquiryImportCommand extends Command
|
|
|
|
|
|
{
|
|
|
|
|
|
protected static $defaultName = 'inquiry:import-hesu-google';
|
|
|
|
|
|
protected static $defaultDescription = '合塑谷歌网站询盘导入:新询盘建档+轮询分配+汇总邮件';
|
|
|
|
|
|
|
|
|
|
|
|
private const CORP_ID = 1;
|
|
|
|
|
|
private const CHANNEL_ID = 4; // 合塑谷歌
|
|
|
|
|
|
private const CHANNEL_NAME = '合塑谷歌';
|
|
|
|
|
|
private const TYPE = 2; // 导入数据
|
|
|
|
|
|
private const GRADE = 0;
|
|
|
|
|
|
private const MAGNITUDE = 2;
|
|
|
|
|
|
|
|
|
|
|
|
protected function configure()
|
|
|
|
|
|
{
|
|
|
|
|
|
$this->addOption('file', null, InputOption::VALUE_REQUIRED, 'Excel 文件路径', 'docs/询盘明细_2026年6-8月_已清洗.xlsx')
|
|
|
|
|
|
->addOption('users', null, InputOption::VALUE_REQUIRED, '目标 user_id 列表,逗号分隔(必填)', '')
|
|
|
|
|
|
->addOption('dry-run', null, InputOption::VALUE_NONE, '只解析+去重预检+分配预演,不写库不发邮件')
|
|
|
|
|
|
->addOption('no-notify', null, InputOption::VALUE_NONE, '只建档分配,不发邮件(先核对库再 --notify-only 补发)')
|
|
|
|
|
|
->addOption('notify-only', null, InputOption::VALUE_NONE, '跳过写库,按 manifest 补发未成功的邮件')
|
|
|
|
|
|
->addOption('only-user', null, InputOption::VALUE_REQUIRED, '仅给指定 user_id 发邮件(试发)', '0');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 解析 Excel(按表头名定位列,顺序无关)。返回行数组。
|
|
|
|
|
|
*/
|
|
|
|
|
|
private function parseExcel(string $filePath): array
|
|
|
|
|
|
{
|
|
|
|
|
|
$sheet = IOFactory::load($filePath)->getActiveSheet();
|
|
|
|
|
|
$highestRow = $sheet->getHighestRow();
|
|
|
|
|
|
|
|
|
|
|
|
$colMap = [];
|
|
|
|
|
|
foreach (range('A', 'Z') as $col) {
|
|
|
|
|
|
$v = trim((string)($sheet->getCell($col . '1')->getValue() ?? ''));
|
|
|
|
|
|
if ($v !== '') {
|
|
|
|
|
|
$colMap[$v] = $col;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
$required = ['客户姓名', '电话/WhatsApp', '邮箱', '留言内容', '产品页链接'];
|
|
|
|
|
|
$missing = array_diff($required, array_keys($colMap));
|
|
|
|
|
|
if ($missing) {
|
|
|
|
|
|
throw new \RuntimeException('Excel 缺少必需列头: ' . implode(', ', $missing) . ';实际列头: ' . implode(',', array_keys($colMap)));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$get = function (string $header, int $r) use ($sheet, $colMap): string {
|
|
|
|
|
|
return trim((string)($sheet->getCell($colMap[$header] . $r)->getValue() ?? ''));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
$rows = [];
|
|
|
|
|
|
for ($r = 2; $r <= $highestRow; $r++) {
|
|
|
|
|
|
$row = [
|
|
|
|
|
|
'contacts' => $get('客户姓名', $r),
|
|
|
|
|
|
'phone' => $get('电话/WhatsApp', $r),
|
|
|
|
|
|
'email' => $get('邮箱', $r),
|
|
|
|
|
|
'message' => $get('留言内容', $r),
|
|
|
|
|
|
'url' => $get('产品页链接', $r),
|
|
|
|
|
|
'excel_row' => $r,
|
|
|
|
|
|
];
|
|
|
|
|
|
if ($row['contacts'] === '' && $row['phone'] === '' && $row['email'] === '') {
|
|
|
|
|
|
continue; // 整行为空
|
|
|
|
|
|
}
|
|
|
|
|
|
$rows[] = $row;
|
|
|
|
|
|
}
|
|
|
|
|
|
return $rows;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 从产品页链接提取车型 slug,如 /product/rox-01/ -> rox-01
|
|
|
|
|
|
*/
|
|
|
|
|
|
private function productSlug(string $url): string
|
|
|
|
|
|
{
|
|
|
|
|
|
$path = parse_url($url, PHP_URL_PATH) ?: '';
|
|
|
|
|
|
$segs = array_values(array_filter(explode('/', $path), fn($s) => $s !== ''));
|
|
|
|
|
|
if (empty($segs)) {
|
|
|
|
|
|
return '';
|
|
|
|
|
|
}
|
|
|
|
|
|
$i = array_search('product', $segs, true);
|
|
|
|
|
|
if ($i !== false && isset($segs[$i + 1])) {
|
|
|
|
|
|
return $segs[$i + 1];
|
|
|
|
|
|
}
|
|
|
|
|
|
return (string)end($segs);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 库内 + 文件内去重。返回 [待建档行, 跳过信息列表]
|
|
|
|
|
|
*/
|
|
|
|
|
|
private function dedup(array $rows, OutputInterface $output): array
|
|
|
|
|
|
{
|
|
|
|
|
|
$dbPhones = [];
|
|
|
|
|
|
foreach (array_chunk(array_filter(array_column($rows, 'phone')), 500) as $chunk) {
|
|
|
|
|
|
$dbPhones = array_merge($dbPhones, Db::table('crm_customer')
|
|
|
|
|
|
->whereNull('deleted_at')->whereIn('phone', $chunk)->pluck('phone')->map(fn($v) => trim((string)$v))->all());
|
|
|
|
|
|
}
|
|
|
|
|
|
$dbEmails = [];
|
|
|
|
|
|
foreach (array_chunk(array_filter(array_column($rows, 'email')), 500) as $chunk) {
|
|
|
|
|
|
$dbEmails = array_merge($dbEmails, Db::table('crm_customer')
|
|
|
|
|
|
->whereNull('deleted_at')->whereIn('email', $chunk)->pluck('email')->map(fn($v) => trim((string)$v))->all());
|
|
|
|
|
|
}
|
|
|
|
|
|
$dbPhones = array_flip($dbPhones);
|
|
|
|
|
|
$dbEmails = array_flip($dbEmails);
|
|
|
|
|
|
|
|
|
|
|
|
$keep = []; $skipped = []; $noContact = [];
|
|
|
|
|
|
$seenPhone = []; $seenEmail = [];
|
|
|
|
|
|
foreach ($rows as $row) {
|
|
|
|
|
|
if ($row['phone'] === '' && $row['email'] === '') {
|
|
|
|
|
|
$noContact[] = $row;
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
if ($row['phone'] !== '' && (isset($dbPhones[$row['phone']]) || isset($seenPhone[$row['phone']]))) {
|
|
|
|
|
|
$skipped[] = [$row['excel_row'], '电话重复: ' . $row['phone']];
|
|
|
|
|
|
$seenPhone[$row['phone']] = 1;
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
if ($row['email'] !== '' && (isset($dbEmails[$row['email']]) || isset($seenEmail[$row['email']]))) {
|
|
|
|
|
|
$skipped[] = [$row['excel_row'], '邮箱重复: ' . $row['email']];
|
|
|
|
|
|
$seenEmail[$row['email']] = 1;
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
if ($row['phone'] !== '') {
|
|
|
|
|
|
$seenPhone[$row['phone']] = 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
if ($row['email'] !== '') {
|
|
|
|
|
|
$seenEmail[$row['email']] = 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
$keep[] = $row;
|
|
|
|
|
|
}
|
|
|
|
|
|
return [$keep, $skipped, $noContact];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private function manifestPath(string $date): string
|
|
|
|
|
|
{
|
|
|
|
|
|
return runtime_path() . '/inquiry_hesu_google_import_' . $date . '.json';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
|
|
|
|
|
{
|
|
|
|
|
|
$file = $input->getOption('file');
|
|
|
|
|
|
$targetUserIds = array_values(array_filter(array_map('intval', explode(',', (string)$input->getOption('users')))));
|
|
|
|
|
|
$dryRun = (bool)$input->getOption('dry-run');
|
|
|
|
|
|
$notifyOnly = (bool)$input->getOption('notify-only');
|
|
|
|
|
|
$noNotify = (bool)$input->getOption('no-notify');
|
|
|
|
|
|
$onlyUser = (int)$input->getOption('only-user');
|
|
|
|
|
|
$date = date('Y-m-d');
|
|
|
|
|
|
|
|
|
|
|
|
if (!$targetUserIds) {
|
|
|
|
|
|
$output->writeln('<error>--users 不能为空(目标业务员 user_id 列表)</error>');
|
|
|
|
|
|
return self::FAILURE;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!is_file($file)) {
|
|
|
|
|
|
$output->writeln('<error>文件不存在: ' . $file . '</error>');
|
|
|
|
|
|
return self::FAILURE;
|
|
|
|
|
|
}
|
|
|
|
|
|
if ($notifyOnly && $noNotify) {
|
|
|
|
|
|
$output->writeln('<error>--notify-only 与 --no-notify 不能同时使用</error>');
|
|
|
|
|
|
return self::FAILURE;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$output->writeln('<info>解析 Excel: ' . $file . '</info>');
|
|
|
|
|
|
$rows = $this->parseExcel($file);
|
|
|
|
|
|
$output->writeln('<info>数据行: ' . count($rows) . '</info>');
|
|
|
|
|
|
|
|
|
|
|
|
[$keep, $skipped, $noContact] = $this->dedup($rows, $output);
|
|
|
|
|
|
$output->writeln('<info>去重后待建档: ' . count($keep) . ',重复跳过: ' . count($skipped) . ',无联系方式跳过: ' . count($noContact) . '</info>');
|
|
|
|
|
|
foreach (array_slice($skipped, 0, 10) as $s) {
|
|
|
|
|
|
$output->writeln('<comment> 跳过 行' . $s[0] . ' ' . $s[1] . '</comment>');
|
|
|
|
|
|
}
|
|
|
|
|
|
if (count($skipped) > 10) {
|
|
|
|
|
|
$output->writeln('<comment> ...另 ' . (count($skipped) - 10) . ' 条</comment>');
|
|
|
|
|
|
}
|
|
|
|
|
|
foreach ($noContact as $row) {
|
|
|
|
|
|
$output->writeln('<comment> 无联系方式 行' . $row['excel_row'] . ' ' . $row['contacts'] . '</comment>');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 轮询分配(按行序 i % n)
|
|
|
|
|
|
$n = count($targetUserIds);
|
|
|
|
|
|
foreach ($keep as $i => &$row) {
|
|
|
|
|
|
$row['new_user_id'] = $targetUserIds[$i % $n];
|
|
|
|
|
|
}
|
|
|
|
|
|
unset($row);
|
|
|
|
|
|
$groups = [];
|
|
|
|
|
|
foreach ($targetUserIds as $uid) {
|
|
|
|
|
|
$groups[$uid] = [];
|
|
|
|
|
|
}
|
|
|
|
|
|
foreach ($keep as $row) {
|
|
|
|
|
|
$groups[$row['new_user_id']][] = $row;
|
|
|
|
|
|
}
|
|
|
|
|
|
foreach ($targetUserIds as $uid) {
|
|
|
|
|
|
$output->writeln('<info>user ' . $uid . ' 分配: ' . count($groups[$uid]) . ' 条</info>');
|
|
|
|
|
|
}
|
|
|
|
|
|
foreach ($keep as $i => $row) {
|
|
|
|
|
|
if ($i < 3) {
|
|
|
|
|
|
$output->writeln('<comment>样例[行' . $row['excel_row'] . '] ' . $row['contacts'] . ' ' . $row['phone'] . ' '
|
|
|
|
|
|
. $this->productSlug($row['url']) . ' -> user ' . $row['new_user_id'] . '</comment>');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if ($dryRun) {
|
|
|
|
|
|
$output->writeln('<info>[dry-run] 不写库、不发邮件。确认后去掉 --dry-run 执行。</info>');
|
|
|
|
|
|
return self::SUCCESS;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!$notifyOnly) {
|
|
|
|
|
|
$created = [];
|
|
|
|
|
|
Db::connection()->transaction(function () use ($keep, $targetUserIds, $output, &$created) {
|
|
|
|
|
|
foreach ($keep as $row) {
|
|
|
|
|
|
$desc = $row['message'];
|
|
|
|
|
|
if ($row['url'] !== '') {
|
|
|
|
|
|
$desc .= ($desc !== '' ? "\n" : '') . '产品页:' . $row['url'];
|
|
|
|
|
|
}
|
|
|
|
|
|
$c = new CrmCustomer();
|
|
|
|
|
|
$c->corp_id = self::CORP_ID;
|
|
|
|
|
|
$c->user_id = $row['new_user_id'];
|
|
|
|
|
|
$c->type = self::TYPE;
|
|
|
|
|
|
$c->channel_id = self::CHANNEL_ID;
|
|
|
|
|
|
$c->grade = self::GRADE;
|
|
|
|
|
|
$c->magnitude = self::MAGNITUDE;
|
|
|
|
|
|
$c->email = $row['email'];
|
|
|
|
|
|
$c->phone = $row['phone'];
|
|
|
|
|
|
$c->WhatsApp = '';
|
|
|
|
|
|
$c->contacts = $row['contacts'];
|
|
|
|
|
|
$c->desc_collect = $desc;
|
|
|
|
|
|
$slug = $this->productSlug($row['url']);
|
|
|
|
|
|
$c->intended_vehicles = $slug !== '' ? $slug : null;
|
|
|
|
|
|
$c->obtain = time();
|
|
|
|
|
|
if (!$c->save()) {
|
|
|
|
|
|
throw new \RuntimeException('行' . $row['excel_row'] . ' 建档失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
$row['code'] = $c->code;
|
|
|
|
|
|
$created[] = $row;
|
|
|
|
|
|
}
|
|
|
|
|
|
$output->writeln('<info>已建档并分配: ' . count($created) . ' 条(code 由模型自动生成)</info>');
|
|
|
|
|
|
});
|
|
|
|
|
|
$keep = $created;
|
|
|
|
|
|
$groups = [];
|
|
|
|
|
|
foreach ($targetUserIds as $uid) {
|
|
|
|
|
|
$groups[$uid] = [];
|
|
|
|
|
|
}
|
|
|
|
|
|
foreach ($keep as $row) {
|
|
|
|
|
|
$groups[$row['new_user_id']][] = $row;
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
$output->writeln('<comment>[notify-only] 跳过写库。</comment>');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-19 10:14:40 +08:00
|
|
|
|
$manifestPath = $this->manifestPath($date);
|
|
|
|
|
|
|
|
|
|
|
|
if ($notifyOnly) {
|
|
|
|
|
|
// 建档后号码/邮箱已入库,重新解析会被去重清空——补发必须用 manifest 保存的分配结果
|
|
|
|
|
|
$manifest = is_file($manifestPath) ? (json_decode((string)@file_get_contents($manifestPath), true) ?: []) : [];
|
|
|
|
|
|
if (empty($manifest['groups'])) {
|
|
|
|
|
|
$output->writeln('<error>manifest 不存在或无分配数据: ' . $manifestPath . ';请先执行 --no-notify 或完整命令生成</error>');
|
|
|
|
|
|
return self::FAILURE;
|
|
|
|
|
|
}
|
|
|
|
|
|
$digestGroups = $manifest['groups'];
|
|
|
|
|
|
} else {
|
|
|
|
|
|
$totalRows = array_sum(array_map('count', $groups));
|
|
|
|
|
|
if ($totalRows > 0) {
|
|
|
|
|
|
$digestGroups = [];
|
|
|
|
|
|
foreach ($targetUserIds as $uid) {
|
|
|
|
|
|
$digestGroups[(string)$uid] = [];
|
|
|
|
|
|
}
|
|
|
|
|
|
foreach ($groups as $uid => $rows) {
|
|
|
|
|
|
foreach ($rows as $row) {
|
|
|
|
|
|
$digestGroups[(string)$uid][] = $this->digestRow($row);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
$this->saveManifest($manifestPath, $targetUserIds, $digestGroups);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
$manifest = is_file($manifestPath) ? (json_decode((string)@file_get_contents($manifestPath), true) ?: []) : [];
|
|
|
|
|
|
if (!empty($manifest['groups'])) {
|
|
|
|
|
|
$digestGroups = $manifest['groups'];
|
|
|
|
|
|
$output->writeln('<comment>本次无新建档(可能已导入过),沿用已有 manifest 的分配结果。</comment>');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
$output->writeln('<comment>没有可建档数据,结束。</comment>');
|
|
|
|
|
|
return self::SUCCESS;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if ($noNotify) {
|
|
|
|
|
|
$output->writeln('<comment>[no-notify] 已建档,跳过发邮件。核对库后用 --notify-only 补发。</comment>');
|
|
|
|
|
|
return self::SUCCESS;
|
|
|
|
|
|
}
|
2026-08-18 14:40:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-19 10:14:40 +08:00
|
|
|
|
$this->sendDigests($digestGroups, $targetUserIds, $manifestPath, $onlyUser, $output);
|
2026-08-18 14:40:53 +08:00
|
|
|
|
return self::SUCCESS;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-19 10:14:40 +08:00
|
|
|
|
private function saveManifest(string $path, array $targetUserIds, array $digestGroups): void
|
2026-08-18 14:40:53 +08:00
|
|
|
|
{
|
|
|
|
|
|
$perUser = []; $emailed = [];
|
|
|
|
|
|
foreach ($targetUserIds as $uid) {
|
2026-08-19 10:14:40 +08:00
|
|
|
|
$perUser[(string)$uid] = count($digestGroups[(string)$uid] ?? []);
|
2026-08-18 14:40:53 +08:00
|
|
|
|
$emailed[(string)$uid] = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
$data = [
|
|
|
|
|
|
'date' => date('Y-m-d'),
|
|
|
|
|
|
'imported_at' => time(),
|
|
|
|
|
|
'target_users' => $targetUserIds,
|
|
|
|
|
|
'per_user_counts' => $perUser,
|
2026-08-19 10:14:40 +08:00
|
|
|
|
'groups' => $digestGroups,
|
2026-08-18 14:40:53 +08:00
|
|
|
|
'emailed' => $emailed,
|
|
|
|
|
|
];
|
|
|
|
|
|
file_put_contents($path, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-19 10:14:40 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 建档行 -> 邮件汇总行
|
|
|
|
|
|
*/
|
|
|
|
|
|
private function digestRow(array $row): array
|
|
|
|
|
|
{
|
|
|
|
|
|
return [
|
|
|
|
|
|
'code' => $row['code'] ?? '',
|
|
|
|
|
|
'contact' => $row['contacts'],
|
|
|
|
|
|
'email' => $row['email'],
|
|
|
|
|
|
'phone' => $row['phone'],
|
|
|
|
|
|
'country_cn' => '',
|
|
|
|
|
|
'channel' => self::CHANNEL_NAME,
|
|
|
|
|
|
'remark' => $row['message'],
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private function sendDigests(array $digestGroups, array $targetUserIds, string $manifestPath, int $onlyUser, OutputInterface $output): void
|
2026-08-18 14:40:53 +08:00
|
|
|
|
{
|
|
|
|
|
|
$users = Db::table('user')->whereIn('id', $targetUserIds)->get(['id', 'name', 'email']);
|
|
|
|
|
|
$userMap = [];
|
|
|
|
|
|
foreach ($users as $u) {
|
|
|
|
|
|
$userMap[(int)$u->id] = $u;
|
|
|
|
|
|
}
|
|
|
|
|
|
$manifest = is_file($manifestPath) ? (json_decode((string)file_get_contents($manifestPath), true) ?: ['emailed' => []]) : ['emailed' => []];
|
|
|
|
|
|
$date = date('Y-m-d');
|
|
|
|
|
|
|
|
|
|
|
|
foreach ($targetUserIds as $uid) {
|
|
|
|
|
|
if ($onlyUser > 0 && $uid !== $onlyUser) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!empty($manifest['emailed'][(string)$uid])) {
|
|
|
|
|
|
$output->writeln('<comment>跳过已发送: user ' . $uid . '</comment>');
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
$u = $userMap[$uid] ?? null;
|
|
|
|
|
|
if (!$u || empty($u->email)) {
|
|
|
|
|
|
$output->writeln('<error>user ' . $uid . ' 无邮箱,跳过</error>');
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
2026-08-19 10:14:40 +08:00
|
|
|
|
$inquiries = array_values($digestGroups[(string)$uid] ?? []);
|
2026-08-18 14:40:53 +08:00
|
|
|
|
$html = InquiryDigestMail::build((string)$u->name, $inquiries, $date);
|
|
|
|
|
|
$subject = '新询盘分配汇总(共 ' . count($inquiries) . ' 条)- ' . $u->name . ' - ' . $date;
|
|
|
|
|
|
|
|
|
|
|
|
$ok = MailSend::send($u->email, $subject, $html);
|
|
|
|
|
|
if (!$ok) {
|
|
|
|
|
|
$ok = MailSend::send($u->email, $subject, $html);
|
|
|
|
|
|
}
|
|
|
|
|
|
$manifest['emailed'][(string)$uid] = (bool)$ok;
|
|
|
|
|
|
file_put_contents($manifestPath, json_encode($manifest, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
|
|
|
|
|
|
|
|
|
|
|
|
$tag = $ok ? 'info' : 'error';
|
|
|
|
|
|
$output->writeln('<' . $tag . '>user ' . $uid . ' (' . $u->email . '): '
|
|
|
|
|
|
. ($ok ? '发送成功' : '发送失败') . ',共 ' . count($inquiries) . ' 条</' . $tag . '>');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|