diff --git a/app/command/InquiryBulkAssignCommand.php b/app/command/InquiryBulkAssignCommand.php index 8c30789..f837fbf 100644 --- a/app/command/InquiryBulkAssignCommand.php +++ b/app/command/InquiryBulkAssignCommand.php @@ -3,6 +3,7 @@ namespace app\command; use app\common\utils\email\InquiryDigestMail; +use app\common\utils\email\MailSend; use app\crm\model\CustomerHis; use PhpOffice\PhpSpreadsheet\IOFactory; use Symfony\Component\Console\Command\Command; @@ -33,6 +34,7 @@ class InquiryBulkAssignCommand extends Command ->addOption('dry-run', null, InputOption::VALUE_NONE, '只解析+匹配+分配,打印报告,不写库不发邮件') ->addOption('only-user', null, InputOption::VALUE_REQUIRED, '仅给指定 user_id 发邮件(试发)', '0') ->addOption('notify-only', null, InputOption::VALUE_NONE, '跳过写库,按 manifest 补发未成功的邮件') + ->addOption('no-notify', null, InputOption::VALUE_NONE, '只写库不发邮件(先核对库再用 --notify-only 补发)') ->addOption('self-test', null, InputOption::VALUE_NONE, '运行纯逻辑自测'); } @@ -121,6 +123,108 @@ class InquiryBulkAssignCommand extends Command return ['matched' => $matched, 'unmatched' => $unmatched]; } + /** + * 单事务:更新 user_id + 写历史。用 Db::table() 不触发 model 事件。 + */ + private function applyAssignments(array $matched): void + { + Db::connection()->transaction(function () use ($matched) { + foreach ($matched as $row) { + Db::table('crm_customer') + ->where('id', $row['customer_id']) + ->update(['user_id' => $row['new_user_id']]); + CustomerHis::create([ + 'customer_id' => $row['customer_id'], + 'user_his' => $row['old_user_id'], + 'user_id' => $row['new_user_id'], + ]); + } + }); + } + + private function manifestPath(string $date): string + { + return runtime_path() . '/inquiry_bulk_assign_' . $date . '.json'; + } + + private function initManifest(string $path, array $targetUserIds, array $matched, array $groups): void + { + if (is_file($path)) { + return; // 已存在则保留(含 emailed 标记),供补发使用 + } + $perUser = []; + $emailed = []; + foreach ($targetUserIds as $uid) { + $perUser[(string)$uid] = count($groups[$uid] ?? []); + $emailed[(string)$uid] = false; + } + $data = [ + 'date' => date('Y-m-d'), + 'assigned_at' => time(), + 'target_users' => $targetUserIds, + 'total_matched' => count($matched), + 'per_user_counts' => $perUser, + 'emailed' => $emailed, + ]; + file_put_contents($path, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)); + } + + private function loadManifest(string $path): array + { + if (!is_file($path)) { + return ['emailed' => []]; + } + return json_decode((string)file_get_contents($path), true) ?: ['emailed' => []]; + } + + private function saveManifest(string $path, array $data): void + { + file_put_contents($path, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)); + } + + /** + * 按 targetUserIds 顺序发汇总邮件;跳过已标记成功与 --only-user 之外的人。 + */ + private function sendDigests(array $groups, array $targetUserIds, string $manifestPath, int $onlyUser, OutputInterface $output): void + { + $users = Db::table('user')->whereIn('id', $targetUserIds)->get(['id', 'name', 'email']); + $userMap = []; + foreach ($users as $u) { + $userMap[(int)$u->id] = $u; + } + $manifest = $this->loadManifest($manifestPath); + $date = date('Y-m-d'); + + foreach ($targetUserIds as $uid) { + if ($onlyUser > 0 && $uid !== $onlyUser) { + continue; + } + if (!empty($manifest['emailed'][(string)$uid])) { + $output->writeln('跳过已发送: user ' . $uid . ''); + continue; + } + $u = $userMap[$uid] ?? null; + if (!$u || empty($u->email)) { + $output->writeln('user ' . $uid . ' 无邮箱,跳过'); + continue; + } + $inquiries = $groups[$uid] ?? []; + $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); // 重试 1 次 + } + $manifest['emailed'][(string)$uid] = (bool)$ok; + $this->saveManifest($manifestPath, $manifest); + + $tag = $ok ? 'info' : 'error'; + $output->writeln('<' . $tag . '>user ' . $uid . ' (' . $u->email . '): ' + . ($ok ? '发送成功' : '发送失败') . ',共 ' . count($inquiries) . ' 条'); + } + } + protected function execute(InputInterface $input, OutputInterface $output): int { if ($input->getOption('self-test')) { @@ -131,6 +235,7 @@ class InquiryBulkAssignCommand extends Command $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'); @@ -142,6 +247,10 @@ class InquiryBulkAssignCommand extends Command $output->writeln('文件不存在: ' . $file . ''); return self::FAILURE; } + if ($notifyOnly && $noNotify) { + $output->writeln('--notify-only 与 --no-notify 不能同时使用'); + return self::FAILURE; + } $output->writeln('解析 Excel: ' . $file . ''); $rows = $this->parseExcel($file); @@ -172,7 +281,21 @@ class InquiryBulkAssignCommand extends Command return self::SUCCESS; } - $output->writeln('[未实现] 写库与发邮件见 Task 3/Task 4。'); + if (!$notifyOnly) { + $this->applyAssignments($matched); + $output->writeln('已写库(user_id 更新 + crm_customer_his),共 ' . count($matched) . ' 条。'); + } else { + $output->writeln('[notify-only] 跳过写库。'); + } + + if ($noNotify) { + $output->writeln('[no-notify] 已写库,跳过发邮件。核对库后用 --notify-only 补发。'); + return self::SUCCESS; + } + + $manifestPath = $this->manifestPath($date); + $this->initManifest($manifestPath, $targetUserIds, $matched, $groups); + $this->sendDigests($groups, $targetUserIds, $manifestPath, $onlyUser, $output); return self::SUCCESS; }