修改自动推广的相关任务 退订相关
This commit is contained in:
@@ -74,6 +74,18 @@ class PromotionService
|
||||
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);
|
||||
if (!$account) {
|
||||
$this->enqueueNextEmail($taskId, 600);
|
||||
@@ -285,6 +297,15 @@ class PromotionService
|
||||
$this->tryFinalizeTask($task['task_id']);
|
||||
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')
|
||||
->where('expert_id', $expert['expert_id'])
|
||||
@@ -792,6 +813,15 @@ class PromotionService
|
||||
{
|
||||
$llm = $expert['llm_description'] ?? '';
|
||||
$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 [
|
||||
'expert_title' => "Ph.D",
|
||||
'expert_name' => $expert['name'] ?? '',
|
||||
@@ -803,6 +833,7 @@ class PromotionService
|
||||
'ai_content_analysis' => $llm,
|
||||
'ai_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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user