feat: 询盘批量分配接入事务写库与汇总邮件发送
- applyAssignments: 单事务更新 crm_customer.user_id + 写 crm_customer_his - sendDigests: 按 user 表邮箱每人发一封汇总邮件,失败重试1次 - manifest: runtime 下 JSON 记录 emailed 状态,防重复发送 - 新增 --no-notify(只写库不发邮件),与 --notify-only 配合分步执行 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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('<comment>跳过已发送: user ' . $uid . '</comment>');
|
||||
continue;
|
||||
}
|
||||
$u = $userMap[$uid] ?? null;
|
||||
if (!$u || empty($u->email)) {
|
||||
$output->writeln('<error>user ' . $uid . ' 无邮箱,跳过</error>');
|
||||
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) . ' 条</' . $tag . '>');
|
||||
}
|
||||
}
|
||||
|
||||
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('<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);
|
||||
@@ -172,7 +281,21 @@ class InquiryBulkAssignCommand extends Command
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$output->writeln('<error>[未实现] 写库与发邮件见 Task 3/Task 4。</error>');
|
||||
if (!$notifyOnly) {
|
||||
$this->applyAssignments($matched);
|
||||
$output->writeln('<info>已写库(user_id 更新 + crm_customer_his),共 ' . count($matched) . ' 条。</info>');
|
||||
} else {
|
||||
$output->writeln('<comment>[notify-only] 跳过写库。</comment>');
|
||||
}
|
||||
|
||||
if ($noNotify) {
|
||||
$output->writeln('<comment>[no-notify] 已写库,跳过发邮件。核对库后用 --notify-only 补发。</comment>');
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$manifestPath = $this->manifestPath($date);
|
||||
$this->initManifest($manifestPath, $targetUserIds, $matched, $groups);
|
||||
$this->sendDigests($groups, $targetUserIds, $manifestPath, $onlyUser, $output);
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user