Files
tougao/application/api/controller/Unsubscribe.php
2026-04-29 15:07:56 +08:00

191 lines
7.8 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace app\api\controller;
use think\Controller;
use think\Db;
use think\Response;
use app\common\UnsubscribeService;
/**
* 推广 / 约稿邮件退订入口(公开访问,无需登录)
*
* 路由:
* GET /api/Unsubscribe/index?k=&id=&t= 展示确认页
* POST /api/Unsubscribe/confirm?k=&id=&t= 执行退订
*
* k = expert → 操作 t_expert.unsubscribed
* k = user → 操作 t_user.unsubscribed
*
* 安全:
* - URL 内带 sha256 签名 (kind, id, email, secret),防伪造
* - 必须确认页二次点击才执行(防爬虫预取链接误退订)
*/
class Unsubscribe extends Controller
{
/**
* 退订确认页GET
*/
public function index()
{
$kind = UnsubscribeService::normalizeKind($this->request->param('k', UnsubscribeService::KIND_EXPERT));
$id = intval($this->request->param('id', 0));
$token = trim((string)$this->request->param('t', ''));
$audience = $this->loadAudience($kind, $id);
if (!$audience) {
return $this->html($this->pageInvalid(), 404);
}
if (!UnsubscribeService::verifyToken($kind, $id, (string)$audience['email'], $token)) {
return $this->html($this->pageInvalid(), 403);
}
if (!empty($audience['unsubscribed'])) {
return $this->html($this->pageAlreadyDone((string)$audience['email']));
}
return $this->html($this->pageConfirm(
$kind,
$id,
$token,
(string)$audience['email'],
(string)$audience['name']
));
}
/**
* 真正执行退订POST 推荐GET 也允许)
*/
public function confirm()
{
$kind = UnsubscribeService::normalizeKind($this->request->param('k', UnsubscribeService::KIND_EXPERT));
$id = intval($this->request->param('id', 0));
$token = trim((string)$this->request->param('t', ''));
$audience = $this->loadAudience($kind, $id);
if (!$audience) {
return $this->html($this->pageInvalid(), 404);
}
if (!UnsubscribeService::verifyToken($kind, $id, (string)$audience['email'], $token)) {
return $this->html($this->pageInvalid(), 403);
}
if (empty($audience['unsubscribed'])) {
$table = $kind === UnsubscribeService::KIND_USER ? 'user' : 'expert';
$pk = $kind === UnsubscribeService::KIND_USER ? 'user_id' : 'expert_id';
Db::name($table)->where($pk, $id)->update([
'unsubscribed' => 1,
]);
}
return $this->html($this->pageSuccess((string)$audience['email']));
}
/**
* 按 kind 加载受众的最少必要信息id, email, name, unsubscribed
*/
private function loadAudience($kind, $id)
{
if ($id <= 0) return null;
if ($kind === UnsubscribeService::KIND_USER) {
$row = Db::name('user')
->where('user_id', $id)
->field('user_id, email, realname AS name, unsubscribed')
->find();
} else {
$row = Db::name('expert')
->where('expert_id', $id)
->field('expert_id, email, name, unsubscribed')
->find();
}
return $row ?: null;
}
// ==================== 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($kind, $id, $token, $email, $name)
{
$kindSafe = htmlspecialchars($kind, ENT_QUOTES, 'UTF-8');
$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="k" value="' . $kindSafe . '">'
. '<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);
}
}