修改自动推广的相关任务 退订相关
This commit is contained in:
5
.env
5
.env
@@ -26,6 +26,11 @@ PROMOTION_LLM_TIMEOUT=30
|
|||||||
PROMOTION_LLM_FALLBACK="We would like to cordially invite you to consider submitting a manuscript to {{journal_name}}."
|
PROMOTION_LLM_FALLBACK="We would like to cordially invite you to consider submitting a manuscript to {{journal_name}}."
|
||||||
PROMOTION_LLM_ADVISED_FALLBACK=""
|
PROMOTION_LLM_ADVISED_FALLBACK=""
|
||||||
|
|
||||||
|
[unsubscribe]
|
||||||
|
UNSUBSCRIBE_SECRET="TMR Unsubscribe Secret create on 20260427"
|
||||||
|
UNSUBSCRIBE_BASE_URL=https://submission.tmrjournals.com/api/Unsubscribe/index
|
||||||
|
|
||||||
|
|
||||||
[journal]
|
[journal]
|
||||||
;官网服务器地址
|
;官网服务器地址
|
||||||
base_url = http://journalapi.tmrjournals.com/public/index.php
|
base_url = http://journalapi.tmrjournals.com/public/index.php
|
||||||
|
|||||||
@@ -411,10 +411,14 @@ class EmailClient extends Base
|
|||||||
}
|
}
|
||||||
|
|
||||||
$ids = array_map('intval', explode(',', $expertIds));
|
$ids = array_map('intval', explode(',', $expertIds));
|
||||||
$experts = Db::name('expert')->where('expert_id', 'in', $ids)->where('state', 0)->select();
|
$experts = Db::name('expert')
|
||||||
|
->where('expert_id', 'in', $ids)
|
||||||
|
->where('state', 0)
|
||||||
|
->where('unsubscribed', 0)
|
||||||
|
->select();
|
||||||
|
|
||||||
if (empty($experts)) {
|
if (empty($experts)) {
|
||||||
return jsonError('No valid experts found (state must be 0)');
|
return jsonError('No valid experts found (state must be 0 and not unsubscribed)');
|
||||||
}
|
}
|
||||||
|
|
||||||
$sent = 0;
|
$sent = 0;
|
||||||
@@ -2475,6 +2479,7 @@ class EmailClient extends Base
|
|||||||
$query = Db::name('expert')->alias('e')
|
$query = Db::name('expert')->alias('e')
|
||||||
->join('t_expert_field ef', 'e.expert_id = ef.expert_id', 'inner')
|
->join('t_expert_field ef', 'e.expert_id = ef.expert_id', 'inner')
|
||||||
->where('e.state', 0)
|
->where('e.state', 0)
|
||||||
|
->where('e.unsubscribed', 0)
|
||||||
->where('ef.state', 0);
|
->where('ef.state', 0);
|
||||||
|
|
||||||
$query->where(function ($q) use ($fields) {
|
$query->where(function ($q) use ($fields) {
|
||||||
@@ -2631,11 +2636,20 @@ class EmailClient extends Base
|
|||||||
|
|
||||||
private function buildExpertVars($expert)
|
private function buildExpertVars($expert)
|
||||||
{
|
{
|
||||||
|
$unsubUrl = '';
|
||||||
|
if (!empty($expert['expert_id']) && !empty($expert['email'])) {
|
||||||
|
$unsubUrl = \app\common\UnsubscribeService::buildUrl(
|
||||||
|
intval($expert['expert_id']),
|
||||||
|
(string)$expert['email']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'name' => $expert['name'] ?? '',
|
'name' => $expert['name'] ?? '',
|
||||||
'email' => $expert['email'] ?? '',
|
'email' => $expert['email'] ?? '',
|
||||||
'affiliation' => $expert['affiliation'] ?? '',
|
'affiliation' => $expert['affiliation'] ?? '',
|
||||||
'field' => $expert['field'] ?? '',
|
'field' => $expert['field'] ?? '',
|
||||||
|
'unsubscribe_url' => $unsubUrl,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -653,4 +653,58 @@ class ExpertManage extends Base
|
|||||||
|
|
||||||
return jsonSuccess([]);
|
return jsonSuccess([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后台手工将专家标记为退订
|
||||||
|
* 参数: expert_id(必填) 或 expert_ids(逗号分隔批量)
|
||||||
|
*/
|
||||||
|
public function unsubscribe()
|
||||||
|
{
|
||||||
|
$data = $this->request->post();
|
||||||
|
$idsRaw = trim((string)(isset($data['expert_ids']) ? $data['expert_ids'] : (isset($data['expert_id']) ? $data['expert_id'] : '')));
|
||||||
|
if ($idsRaw === '') {
|
||||||
|
return jsonError('expert_id 或 expert_ids 必填');
|
||||||
|
}
|
||||||
|
$ids = array_filter(array_map('intval', explode(',', $idsRaw)));
|
||||||
|
if (empty($ids)) {
|
||||||
|
return jsonError('expert_id 无效');
|
||||||
|
}
|
||||||
|
|
||||||
|
$affected = Db::name('expert')
|
||||||
|
->where('expert_id', 'in', $ids)
|
||||||
|
->where('unsubscribed', 0)
|
||||||
|
->update(['unsubscribed' => 1]);
|
||||||
|
|
||||||
|
return jsonSuccess([
|
||||||
|
'requested' => count($ids),
|
||||||
|
'affected' => intval($affected),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后台手工恢复专家订阅状态
|
||||||
|
* 参数: expert_id(必填) 或 expert_ids(逗号分隔批量)
|
||||||
|
*/
|
||||||
|
public function resubscribe()
|
||||||
|
{
|
||||||
|
$data = $this->request->post();
|
||||||
|
$idsRaw = trim((string)(isset($data['expert_ids']) ? $data['expert_ids'] : (isset($data['expert_id']) ? $data['expert_id'] : '')));
|
||||||
|
if ($idsRaw === '') {
|
||||||
|
return jsonError('expert_id 或 expert_ids 必填');
|
||||||
|
}
|
||||||
|
$ids = array_filter(array_map('intval', explode(',', $idsRaw)));
|
||||||
|
if (empty($ids)) {
|
||||||
|
return jsonError('expert_id 无效');
|
||||||
|
}
|
||||||
|
|
||||||
|
$affected = Db::name('expert')
|
||||||
|
->where('expert_id', 'in', $ids)
|
||||||
|
->where('unsubscribed', 1)
|
||||||
|
->update(['unsubscribed' => 0]);
|
||||||
|
|
||||||
|
return jsonSuccess([
|
||||||
|
'requested' => count($ids),
|
||||||
|
'affected' => intval($affected),
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
159
application/api/controller/Unsubscribe.php
Normal file
159
application/api/controller/Unsubscribe.php
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
<?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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -74,6 +74,18 @@ class PromotionService
|
|||||||
return ['done' => false, 'skipped' => $logEntry['email_to'], 'reason' => 'expert_invalid'];
|
return ['done' => false, 'skipped' => $logEntry['email_to'], 'reason' => 'expert_invalid'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 退订过滤(防止准备 → 发送之间窗口期内退订的人被误发)
|
||||||
|
if (!empty($expert['unsubscribed'])) {
|
||||||
|
Db::name('promotion_email_log')->where('log_id', $logEntry['log_id'])->update([
|
||||||
|
'state' => 2,
|
||||||
|
'error_msg' => 'Expert unsubscribed',
|
||||||
|
'send_time' => time(),
|
||||||
|
]);
|
||||||
|
Db::name('promotion_task')->where('task_id', $taskId)->update(['utime' => time()]);
|
||||||
|
$this->enqueueNextEmail($taskId, 2);
|
||||||
|
return ['done' => false, 'skipped' => $logEntry['email_to'], 'reason' => 'expert_unsubscribed'];
|
||||||
|
}
|
||||||
|
|
||||||
$account = $this->pickSmtpAccountForTask($task);
|
$account = $this->pickSmtpAccountForTask($task);
|
||||||
if (!$account) {
|
if (!$account) {
|
||||||
$this->enqueueNextEmail($taskId, 600);
|
$this->enqueueNextEmail($taskId, 600);
|
||||||
@@ -285,6 +297,15 @@ class PromotionService
|
|||||||
$this->tryFinalizeTask($task['task_id']);
|
$this->tryFinalizeTask($task['task_id']);
|
||||||
return ['code' => 1, 'msg' => 'expert_not_found', 'llm_status' => 0];
|
return ['code' => 1, 'msg' => 'expert_not_found', 'llm_status' => 0];
|
||||||
}
|
}
|
||||||
|
if (!empty($expert['unsubscribed'])) {
|
||||||
|
Db::name('promotion_email_log')->where('log_id', $logId)->update([
|
||||||
|
'state' => 2,
|
||||||
|
'error_msg' => 'Expert unsubscribed',
|
||||||
|
'send_time' => time(),
|
||||||
|
]);
|
||||||
|
$this->tryFinalizeTask($task['task_id']);
|
||||||
|
return ['code' => 1, 'msg' => 'expert_unsubscribed', 'llm_status' => 0];
|
||||||
|
}
|
||||||
|
|
||||||
$expert_fields = Db::name('expert_field')
|
$expert_fields = Db::name('expert_field')
|
||||||
->where('expert_id', $expert['expert_id'])
|
->where('expert_id', $expert['expert_id'])
|
||||||
@@ -792,6 +813,15 @@ class PromotionService
|
|||||||
{
|
{
|
||||||
$llm = $expert['llm_description'] ?? '';
|
$llm = $expert['llm_description'] ?? '';
|
||||||
$advised = $expert['ai_advised_topics'] ?? '';
|
$advised = $expert['ai_advised_topics'] ?? '';
|
||||||
|
|
||||||
|
$unsubUrl = '';
|
||||||
|
if (!empty($expert['expert_id']) && !empty($expert['email'])) {
|
||||||
|
$unsubUrl = \app\common\UnsubscribeService::buildUrl(
|
||||||
|
intval($expert['expert_id']),
|
||||||
|
(string)$expert['email']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'expert_title' => "Ph.D",
|
'expert_title' => "Ph.D",
|
||||||
'expert_name' => $expert['name'] ?? '',
|
'expert_name' => $expert['name'] ?? '',
|
||||||
@@ -803,6 +833,7 @@ class PromotionService
|
|||||||
'ai_content_analysis' => $llm,
|
'ai_content_analysis' => $llm,
|
||||||
'ai_advised_topics' => $advised,
|
'ai_advised_topics' => $advised,
|
||||||
'llm_advised_topics' => $advised,
|
'llm_advised_topics' => $advised,
|
||||||
|
'unsubscribe_url' => $unsubUrl,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
84
application/common/UnsubscribeService.php
Normal file
84
application/common/UnsubscribeService.php
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\common;
|
||||||
|
|
||||||
|
use think\Env;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退订工具:生成/校验签名 token,构造退订 URL。
|
||||||
|
*
|
||||||
|
* .env 配置([unsubscribe] 段):
|
||||||
|
* UNSUBSCRIBE_SECRET 用于签名的密钥(必须自行设置一个长随机字符串)
|
||||||
|
* UNSUBSCRIBE_BASE_URL 退订入口 URL,指向 Unsubscribe 控制器的 index 方法,例如:
|
||||||
|
* https://api.tmrjournals.com/api/Unsubscribe/index
|
||||||
|
*
|
||||||
|
* URL 格式:
|
||||||
|
* {UNSUBSCRIBE_BASE_URL}?id={expert_id}&t={sha256(expert_id|email_lower|secret)}
|
||||||
|
*
|
||||||
|
* 设计要点:
|
||||||
|
* - 不依赖 session:纯 HMAC 风格签名,每个 expert 的 token 永久有效(直到密钥更换或邮箱变更)。
|
||||||
|
* - 收到请求后服务端按 expert_id 查邮箱,重新计算签名比对,hash_equals 防时序攻击。
|
||||||
|
* - 邮箱大小写敏感性:统一 strtolower 后再签名/校验。
|
||||||
|
*/
|
||||||
|
class UnsubscribeService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 读取签名密钥;未配置时使用一个固定默认值(仅供本地测试,生产必须覆盖)。
|
||||||
|
*/
|
||||||
|
public static function getSecret()
|
||||||
|
{
|
||||||
|
$secret = trim((string)Env::get('unsubscribe.unsubscribe_secret', ''));
|
||||||
|
if ($secret === '') {
|
||||||
|
// 兜底密钥;强烈建议在 .env 里覆盖
|
||||||
|
$secret = 'tmrjournals-default-unsubscribe-secret-change-me';
|
||||||
|
}
|
||||||
|
return $secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取退订入口 URL(指向 Unsubscribe/index);未配置返回空串。
|
||||||
|
*/
|
||||||
|
public static function getBaseUrl()
|
||||||
|
{
|
||||||
|
return rtrim(trim((string)Env::get('unsubscribe.unsubscribe_base_url', '')), '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成签名 token。
|
||||||
|
*/
|
||||||
|
public static function buildToken($expertId, $email)
|
||||||
|
{
|
||||||
|
$expertId = intval($expertId);
|
||||||
|
$email = strtolower(trim((string)$email));
|
||||||
|
return hash('sha256', $expertId . '|' . $email . '|' . self::getSecret());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验签名 token;恒定时间比较,防时序攻击。
|
||||||
|
*/
|
||||||
|
public static function verifyToken($expertId, $email, $token)
|
||||||
|
{
|
||||||
|
$expertId = intval($expertId);
|
||||||
|
$email = strtolower(trim((string)$email));
|
||||||
|
$token = strtolower(trim((string)$token));
|
||||||
|
if ($expertId <= 0 || $email === '' || $token === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$expected = self::buildToken($expertId, $email);
|
||||||
|
return hash_equals($expected, $token);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造完整退订 URL(用于邮件模板变量 {{unsubscribe_url}})。
|
||||||
|
* BASE_URL 未配置或参数无效时返回空串。
|
||||||
|
*/
|
||||||
|
public static function buildUrl($expertId, $email)
|
||||||
|
{
|
||||||
|
$expertId = intval($expertId);
|
||||||
|
$email = trim((string)$email);
|
||||||
|
if ($expertId <= 0 || $email === '') return '';
|
||||||
|
$base = self::getBaseUrl();
|
||||||
|
if ($base === '') return '';
|
||||||
|
return $base . '?id=' . $expertId . '&t=' . self::buildToken($expertId, $email);
|
||||||
|
}
|
||||||
|
}
|
||||||
9
sql/add_unsubscribed_to_expert.sql
Normal file
9
sql/add_unsubscribed_to_expert.sql
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
-- 退订功能:t_expert 增加退订标记
|
||||||
|
-- 用户已手动新增 unsubscribed 字段,以下为完整建议建表语句(含索引)
|
||||||
|
-- 0=正常 1=已退订;选人时统一过滤 unsubscribed=0
|
||||||
|
|
||||||
|
ALTER TABLE `t_expert`
|
||||||
|
ADD COLUMN `unsubscribed` TINYINT NOT NULL DEFAULT 0 COMMENT '0正常 1已退订' AFTER `state`;
|
||||||
|
|
||||||
|
ALTER TABLE `t_expert`
|
||||||
|
ADD INDEX `idx_unsubscribed` (`unsubscribed`);
|
||||||
Reference in New Issue
Block a user