160 lines
6.6 KiB
PHP
160 lines
6.6 KiB
PHP
<?php
|
||
|
||
namespace app\api\controller;
|
||
|
||
use think\Controller;
|
||
use think\Db;
|
||
use think\Response;
|
||
use app\common\UnsubscribeService;
|
||
|
||
/**
|
||
* 推广邮件退订入口(公开访问,无需登录)
|
||
*
|
||
* 路由:
|
||
* GET /api/Unsubscribe/index?id=&t= 展示确认页
|
||
* POST /api/Unsubscribe/confirm?id=&t= 执行退订
|
||
*
|
||
* 安全:
|
||
* - URL 内带 sha256 签名 (id, email, secret),防伪造
|
||
* - 必须确认页二次点击才执行(防爬虫预取链接误退订)
|
||
*/
|
||
class Unsubscribe extends Controller
|
||
{
|
||
/**
|
||
* 退订确认页(GET)
|
||
*/
|
||
public function index()
|
||
{
|
||
$id = intval($this->request->param('id', 0));
|
||
$token = trim((string)$this->request->param('t', ''));
|
||
|
||
$expert = $id > 0 ? Db::name('expert')->where('expert_id', $id)->find() : null;
|
||
if (!$expert) {
|
||
return $this->html($this->pageInvalid(), 404);
|
||
}
|
||
if (!UnsubscribeService::verifyToken($id, (string)$expert['email'], $token)) {
|
||
return $this->html($this->pageInvalid(), 403);
|
||
}
|
||
if (!empty($expert['unsubscribed'])) {
|
||
return $this->html($this->pageAlreadyDone((string)$expert['email']));
|
||
}
|
||
|
||
return $this->html($this->pageConfirm(
|
||
$id,
|
||
$token,
|
||
(string)$expert['email'],
|
||
(string)($expert['name'] ?? '')
|
||
));
|
||
}
|
||
|
||
/**
|
||
* 真正执行退订(POST 推荐;GET 也允许,以兼容部分邮件客户端禁止表单提交)
|
||
*/
|
||
public function confirm()
|
||
{
|
||
$id = intval($this->request->param('id', 0));
|
||
$token = trim((string)$this->request->param('t', ''));
|
||
|
||
$expert = $id > 0 ? Db::name('expert')->where('expert_id', $id)->find() : null;
|
||
if (!$expert) {
|
||
return $this->html($this->pageInvalid(), 404);
|
||
}
|
||
if (!UnsubscribeService::verifyToken($id, (string)$expert['email'], $token)) {
|
||
return $this->html($this->pageInvalid(), 403);
|
||
}
|
||
|
||
if (empty($expert['unsubscribed'])) {
|
||
Db::name('expert')->where('expert_id', $id)->update([
|
||
'unsubscribed' => 1,
|
||
]);
|
||
}
|
||
|
||
return $this->html($this->pageSuccess((string)$expert['email']));
|
||
}
|
||
|
||
// ==================== HTML 页面 ====================
|
||
|
||
private function html($html, $status = 200)
|
||
{
|
||
$resp = Response::create($html, 'html', $status);
|
||
$resp->header('Content-Type', 'text/html; charset=utf-8');
|
||
return $resp;
|
||
}
|
||
|
||
private function pageShell($title, $bodyHtml)
|
||
{
|
||
$titleSafe = htmlspecialchars($title, ENT_QUOTES, 'UTF-8');
|
||
$css = "
|
||
body{margin:0;padding:0;font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;background:#f5f7fa;color:#1f2937;}
|
||
.wrap{max-width:520px;margin:64px auto;padding:32px;background:#fff;border-radius:12px;box-shadow:0 2px 16px rgba(0,0,0,.06);}
|
||
h1{font-size:22px;margin:0 0 16px;color:#111827;}
|
||
p{font-size:15px;line-height:1.6;margin:0 0 16px;color:#374151;}
|
||
.email{font-weight:600;color:#111827;word-break:break-all;}
|
||
.btn{display:inline-block;padding:10px 24px;border-radius:6px;border:0;cursor:pointer;font-size:15px;text-decoration:none;}
|
||
.btn-primary{background:#dc2626;color:#fff;}
|
||
.btn-primary:hover{background:#b91c1c;}
|
||
.btn-secondary{background:#e5e7eb;color:#374151;margin-left:8px;}
|
||
.muted{color:#6b7280;font-size:13px;margin-top:24px;}
|
||
.ok{color:#16a34a;font-weight:600;}
|
||
.warn{color:#d97706;font-weight:600;}
|
||
.err{color:#dc2626;font-weight:600;}
|
||
";
|
||
return '<!doctype html><html lang="en"><head><meta charset="utf-8">'
|
||
. '<meta name="viewport" content="width=device-width,initial-scale=1">'
|
||
. '<meta name="robots" content="noindex,nofollow">'
|
||
. '<title>' . $titleSafe . '</title>'
|
||
. '<style>' . $css . '</style></head>'
|
||
. '<body><div class="wrap">' . $bodyHtml . '</div></body></html>';
|
||
}
|
||
|
||
private function pageConfirm($id, $token, $email, $name)
|
||
{
|
||
$idSafe = intval($id);
|
||
$tokenSafe = htmlspecialchars($token, ENT_QUOTES, 'UTF-8');
|
||
$emailSafe = htmlspecialchars($email, ENT_QUOTES, 'UTF-8');
|
||
$nameSafe = htmlspecialchars($name !== '' ? $name : $email, ENT_QUOTES, 'UTF-8');
|
||
|
||
$body = '<h1>Confirm unsubscribe</h1>'
|
||
. '<p>Hi ' . $nameSafe . ',</p>'
|
||
. '<p>You are about to unsubscribe <span class="email">' . $emailSafe
|
||
. '</span> from all promotion and invitation emails sent by TMR Journals. '
|
||
. 'After unsubscribing you will no longer receive marketing emails from us.</p>'
|
||
. '<form method="post" action="confirm" style="margin-top:24px;">'
|
||
. '<input type="hidden" name="id" value="' . $idSafe . '">'
|
||
. '<input type="hidden" name="t" value="' . $tokenSafe . '">'
|
||
. '<button type="submit" class="btn btn-primary">Confirm unsubscribe</button>'
|
||
. '<a class="btn btn-secondary" href="javascript:window.close();">Cancel</a>'
|
||
. '</form>'
|
||
. '<p class="muted">If you didn\'t expect this email, you can safely close this page.</p>';
|
||
|
||
return $this->pageShell('Confirm unsubscribe - TMR Journals', $body);
|
||
}
|
||
|
||
private function pageSuccess($email)
|
||
{
|
||
$emailSafe = htmlspecialchars($email, ENT_QUOTES, 'UTF-8');
|
||
$body = '<h1>You have been unsubscribed</h1>'
|
||
. '<p class="ok">' . $emailSafe . ' will no longer receive promotion or invitation emails from TMR Journals.</p>'
|
||
. '<p>If this was a mistake, please contact us and we will be happy to resubscribe you.</p>'
|
||
. '<p class="muted">Thank you for your past interest in our journals.</p>';
|
||
return $this->pageShell('Unsubscribed - TMR Journals', $body);
|
||
}
|
||
|
||
private function pageAlreadyDone($email)
|
||
{
|
||
$emailSafe = htmlspecialchars($email, ENT_QUOTES, 'UTF-8');
|
||
$body = '<h1>Already unsubscribed</h1>'
|
||
. '<p class="warn">' . $emailSafe . ' is already unsubscribed from our promotion emails.</p>'
|
||
. '<p class="muted">No further action is needed.</p>';
|
||
return $this->pageShell('Already unsubscribed - TMR Journals', $body);
|
||
}
|
||
|
||
private function pageInvalid()
|
||
{
|
||
$body = '<h1>Invalid or expired link</h1>'
|
||
. '<p class="err">This unsubscribe link is invalid or has expired.</p>'
|
||
. '<p>Please use the most recent unsubscribe link in our emails, or contact us for help.</p>';
|
||
return $this->pageShell('Invalid link - TMR Journals', $body);
|
||
}
|
||
}
|