request->url(true); return function_exists('cms_lang_url') ? cms_lang_url($url) : $url; } protected function buildUnsubscribeToken(string $email, string $lang): string { return md5($email . '|' . $lang . '|' . uniqid('', true) . '|' . mt_rand(100000, 999999)); } protected function buildUnsubscribeUrl(EmailSubscribeModel $subscribe): string { return $this->frontendUrl('index/emailsubscribe/unsubscribe', ['token' => (string)$subscribe->unsubscribe_token], true, true); } protected function normalizeLang(string $lang): string { $lang = strtolower(trim($lang)); if ($lang !== '') { return $lang; } if (function_exists('cms_current_region_lang')) { $resolved = strtolower(trim((string)cms_current_region_lang('cn'))); if ($resolved !== '') { return $resolved; } } return 'cn'; } protected function sendSubscribeEmail(string $email, string $subject, string $content): array { try { $config = get_addon_config('phpmailer'); if (!$config) { return [false, 'NO_CONFIG', '未找到 phpmailer 插件配置']; } $mailer = new \addons\phpmailer\library\Mailer($config); $result = $mailer->to($email)->subject($subject)->message($content, true)->send(); if ($result) { return [true, 'SUCCESS', '邮件发送成功']; } return [false, 'FAIL', $mailer->getError() ?: '邮件发送失败']; } catch (\Throwable $e) { return [false, 'EXCEPTION', $e->getMessage()]; } } protected function buildMailContext(array $data): array { $context = []; foreach ($data as $key => $value) { $context[$key] = is_scalar($value) || $value === null ? (string)$value : json_encode($value, JSON_UNESCAPED_UNICODE); } return $context; } protected function renderTemplateContent(string $content, array $context): string { $replace = []; foreach ($context as $key => $value) { $replace['{{' . $key . '}}'] = (string)$value; $replace['{{ ' . $key . ' }}'] = (string)$value; } return strtr($content, $replace); } protected function getTemplateByType(string $type, string $lang): ?EmailTemplateModel { $lang = $this->normalizeLang($lang); $template = EmailTemplateModel::where(['type' => $type, 'lang' => $lang, 'status' => 1]) ->order('listorder', 'asc') ->order('id', 'asc') ->find(); if ($template) { return $template; } if ($lang !== 'cn') { return EmailTemplateModel::where(['type' => $type, 'lang' => 'cn', 'status' => 1]) ->order('listorder', 'asc') ->order('id', 'asc') ->find(); } return null; } protected function buildMailByType(string $type, string $lang, array $context = []): array { $template = $this->getTemplateByType($type, $lang); if ($template) { return [ 'subject' => $this->renderTemplateContent((string)$template->subject, $context), 'content' => $this->renderTemplateContent((string)$template->content, $context), 'template_id' => (int)$template->id, 'template_code' => (string)$template->code, ]; } $fallback = $this->buildAutoMail($lang, $type); return [ 'subject' => $this->renderTemplateContent((string)$fallback['subject'], $context), 'content' => $this->renderTemplateContent((string)$fallback['content'], $context), 'template_id' => 0, 'template_code' => '', ]; } protected function buildAutoMail(string $lang, string $type = 'subscribe_auto'): array { $isCn = in_array($lang, ['cn', 'zh', 'zh-cn'], true); if ($isCn) { return [ 'subject' => '感谢订阅 XTU 最新动态', 'content' => '您已成功订阅 XTU 的新品发布与品牌动态邮件通知,后续如有更新我们会第一时间通过邮件发送给您。', ]; } return [ 'subject' => 'Thanks for subscribing to XTU updates', 'content' => 'You have successfully subscribed to XTU news, new arrivals, and brand updates. We will email you when new updates are available.', ]; } public function submit() { if (!$this->request->isPost()) { $this->error('请求方式错误'); } $email = trim((string)$this->request->post('email/s', '')); $lang = $this->normalizeLang((string)$this->request->post('lang/s', '')); $sourcePage = trim((string)$this->request->post('source_page/s', $this->frontendCurrentUrl())); $sourceKey = trim((string)$this->request->post('source_key/s', 'footer')); $remark = trim((string)$this->request->post('remark/s', '')); $privacyAgree = (int)$this->request->post('privacy_agree/d', 0); $now = time(); $ip = (string)$this->request->ip(); $userAgent = substr((string)$this->request->server('HTTP_USER_AGENT'), 0, 255); if ($email === '' || !Validate::is($email, 'email')) { $this->error('请输入正确的邮箱地址'); } if ($privacyAgree !== 1) { $this->error('请先勾选并同意隐私政策'); } $ipLimitWindowSeconds = 60; $ipLimitWindowMax = max(0, (int) config('site.sms_rpm', 5)); $ipLimitDailyMax = max(0, (int) config('site.sms_drl', 10)); $dayKey = date('Ymd', $now); $ipWindowKey = 'email_subscribe_ip_window_' . md5($ip); $ipDailyKey = 'email_subscribe_ip_daily_' . md5($ip . '_' . $dayKey); $ipWindowHits = (int) Cache::get($ipWindowKey, 0); if ($ipLimitWindowMax > 0 && $ipWindowHits >= $ipLimitWindowMax) { $this->error('提交过于频繁,请稍后再试'); } $ipDailyHits = (int) Cache::get($ipDailyKey, 0); if ($ipLimitDailyMax > 0 && $ipDailyHits >= $ipLimitDailyMax) { $this->error('今日提交次数已达上限,请明天再试'); } if ($ipLimitWindowMax > 0) { Cache::set($ipWindowKey, $ipWindowHits + 1, $ipLimitWindowSeconds); } if ($ipLimitDailyMax > 0) { Cache::set($ipDailyKey, $ipDailyHits + 1, 86400); } $existing = EmailSubscribeModel::where(['email' => $email, 'lang' => $lang])->find(); if ($existing && (int)$existing->status === 1) { $this->success('该邮箱已订阅,请勿重复提交', null, [ 'subscribe_id' => $existing->id, 'duplicate' => 1, ]); } $sendResult = false; $responseCode = ''; $responseMsg = ''; $subscribe = $existing ?: new EmailSubscribeModel(); Db::startTrans(); try { $unsubscribeToken = (string)($existing->unsubscribe_token ?? ''); if ($unsubscribeToken === '') { $unsubscribeToken = $this->buildUnsubscribeToken($email, $lang); } $subscribe->save([ 'email' => $email, 'lang' => $lang, 'source_page' => $sourcePage, 'source_key' => $sourceKey !== '' ? $sourceKey : 'footer', 'status' => 1, 'confirm_status' => 0, 'last_send_status' => 0, 'last_send_time' => 0, 'ip' => $ip, 'user_agent' => $userAgent, 'remark' => $remark, 'privacy_agree' => 1, 'privacy_agree_time' => $now, 'privacy_agree_ip' => $ip, 'unsubscribe_token' => $unsubscribeToken, 'unsubscribe_time' => 0, 'createtime' => $existing ? (int)$existing->createtime : $now, 'updatetime' => $now, ]); $mailContext = $this->buildMailContext([ 'email' => $email, 'lang' => $lang, 'source_page' => $sourcePage, 'source_key' => $sourceKey !== '' ? $sourceKey : 'footer', 'remark' => $remark, 'ip' => $ip, 'user_agent' => $userAgent, 'unsubscribe_url' => $this->buildUnsubscribeUrl($subscribe), ]); $mail = $this->buildMailByType('subscribe_auto', $lang, $mailContext); $autoSendWelcome = (int) config('site.email_subscribe_send_welcome', 1) === 1; $log = new EmailSubscribeLogModel(); $log->save([ 'subscribe_id' => $subscribe->id, 'email' => $email, 'mail_type' => 'subscribe_auto', 'subject' => $mail['subject'], 'content' => $mail['content'], 'status' => 0, 'response_code' => '', 'response_msg' => $autoSendWelcome ? '待自动发送' : '后台开关已关闭,跳过发送', 'retry_count' => 0, 'send_time' => 0, 'createtime' => $now, 'updatetime' => $now, ]); if ($autoSendWelcome) { [$sendResult, $responseCode, $responseMsg] = $this->sendSubscribeEmail($email, $mail['subject'], $mail['content']); $log->status = $sendResult ? 1 : 2; $log->response_code = $responseCode; $log->response_msg = $responseMsg ?: ($sendResult ? '订阅自动邮件发送成功' : '订阅自动邮件发送失败'); $log->send_time = $sendResult ? $now : 0; $subscribe->last_send_status = $sendResult ? 1 : 2; $subscribe->last_send_time = $sendResult ? $now : 0; } else { $sendResult = true; $responseCode = 'DISABLED'; $responseMsg = '订阅欢迎邮件发送开关已关闭'; $log->status = 0; $log->response_code = $responseCode; $log->response_msg = $responseMsg; $log->send_time = 0; $subscribe->last_send_status = 0; $subscribe->last_send_time = 0; } $log->updatetime = $now; $log->save(); $subscribe->updatetime = $now; $subscribe->save(); Db::commit(); } catch (\Throwable $e) { Db::rollback(); $this->error('邮件订阅提交失败:' . $e->getMessage()); } $msg = $sendResult ? '订阅成功,请留意邮箱通知' : '订阅成功,但确认邮件暂未发送成功'; if (!in_array($lang, ['cn', 'zh', 'zh-cn'], true)) { $msg = $sendResult ? 'Subscription successful. Please check your email inbox.' : 'Subscription saved, but the confirmation email was not sent successfully.'; } $this->success($msg, null, [ 'subscribe_id' => $subscribe->id, 'last_send_status' => $subscribe->last_send_status, 'lang' => $lang, ]); } public function unsubscribe() { $token = trim((string)$this->request->param('token/s', '')); if ($token === '') { $this->error('缺少退订令牌'); } $row = EmailSubscribeModel::where('unsubscribe_token', $token)->find(); if (!$row) { $this->error('退订链接无效或已失效'); } $now = time(); if ((int)$row->status !== 9) { $row->status = 9; $row->unsubscribe_time = $now; $row->updatetime = $now; $row->save(); EmailSubscribeLogModel::create([ 'subscribe_id' => (int)$row->id, 'email' => (string)$row->email, 'mail_type' => 'unsubscribe', 'subject' => '', 'content' => '用户通过退订链接取消订阅', 'status' => 1, 'response_code' => 'UNSUBSCRIBED', 'response_msg' => '用户主动退订', 'retry_count' => 0, 'send_time' => $now, 'createtime' => $now, 'updatetime' => $now, ]); } $lang = $this->normalizeLang((string)$row->lang); $title = in_array($lang, ['cn', 'zh', 'zh-cn'], true) ? '您已成功退订' : 'You have unsubscribed successfully'; $message = in_array($lang, ['cn', 'zh', 'zh-cn'], true) ? '您的邮箱已停止接收后续订阅邮件。如需重新订阅,可再次在网站底部提交邮箱。' : 'Your email has been unsubscribed successfully. You can subscribe again anytime from the website footer.'; $this->assign('pageTitle', $title); $this->assign('message', $message); $this->assign('email', (string)$row->email); return $this->fetch(); } }