修改自动推广的相关任务

This commit is contained in:
wangjinlei
2026-04-29 15:07:56 +08:00
parent 085bf5365c
commit 02242b1f08
9 changed files with 738 additions and 244 deletions

View File

@@ -5,7 +5,7 @@ namespace app\common;
use think\Env;
/**
* 退订工具:生成/校验签名 token构造退订 URL。
* 退订工具:生成/校验签名 token构造退订 URL(支持 expert / user 两类受众)
*
* .env 配置([unsubscribe] 段):
* UNSUBSCRIBE_SECRET 用于签名的密钥(必须自行设置一个长随机字符串)
@@ -13,15 +13,22 @@ use think\Env;
* https://api.tmrjournals.com/api/Unsubscribe/index
*
* URL 格式:
* {UNSUBSCRIBE_BASE_URL}?id={expert_id}&t={sha256(expert_id|email_lower|secret)}
* {UNSUBSCRIBE_BASE_URL}?k={kind}&id={audience_id}&t={sha256(kind|id|email_lower|secret)}
*
* kind:
* expert → 外部专家库 t_expert.expert_id
* user → 系统内部用户 t_user.user_id编委 / 主编 / 青年编委等)
*
* 设计要点:
* - 不依赖 session纯 HMAC 风格签名,每个 expert 的 token 永久有效(直到密钥更换或邮箱变更)。
* - 收到请求后服务端按 expert_id 查邮箱重新计算签名比对hash_equals 防时序攻击。
* - 不依赖 session纯 HMAC 风格签名token 永久有效(直到密钥更换或邮箱变更)。
* - 收到请求后服务端按 kind+id 查邮箱重新计算签名比对hash_equals 防时序攻击。
* - 邮箱大小写敏感性:统一 strtolower 后再签名/校验。
*/
class UnsubscribeService
{
const KIND_EXPERT = 'expert';
const KIND_USER = 'user';
/**
* 读取签名密钥;未配置时使用一个固定默认值(仅供本地测试,生产必须覆盖)。
*/
@@ -29,7 +36,6 @@ class UnsubscribeService
{
$secret = trim((string)Env::get('unsubscribe.unsubscribe_secret', ''));
if ($secret === '') {
// 兜底密钥;强烈建议在 .env 里覆盖
$secret = 'tmrjournals-default-unsubscribe-secret-change-me';
}
return $secret;
@@ -43,28 +49,39 @@ class UnsubscribeService
return rtrim(trim((string)Env::get('unsubscribe.unsubscribe_base_url', '')), '/');
}
/**
* 规范化 kind未知值降级为 expert保持向后兼容
*/
public static function normalizeKind($kind)
{
$kind = strtolower(trim((string)$kind));
return $kind === self::KIND_USER ? self::KIND_USER : self::KIND_EXPERT;
}
/**
* 生成签名 token。
*/
public static function buildToken($expertId, $email)
public static function buildToken($kind, $audienceId, $email)
{
$expertId = intval($expertId);
$email = strtolower(trim((string)$email));
return hash('sha256', $expertId . '|' . $email . '|' . self::getSecret());
$kind = self::normalizeKind($kind);
$audienceId = intval($audienceId);
$email = strtolower(trim((string)$email));
return hash('sha256', $kind . '|' . $audienceId . '|' . $email . '|' . self::getSecret());
}
/**
* 校验签名 token恒定时间比较防时序攻击。
*/
public static function verifyToken($expertId, $email, $token)
public static function verifyToken($kind, $audienceId, $email, $token)
{
$expertId = intval($expertId);
$email = strtolower(trim((string)$email));
$token = strtolower(trim((string)$token));
if ($expertId <= 0 || $email === '' || $token === '') {
$kind = self::normalizeKind($kind);
$audienceId = intval($audienceId);
$email = strtolower(trim((string)$email));
$token = strtolower(trim((string)$token));
if ($audienceId <= 0 || $email === '' || $token === '') {
return false;
}
$expected = self::buildToken($expertId, $email);
$expected = self::buildToken($kind, $audienceId, $email);
return hash_equals($expected, $token);
}
@@ -72,13 +89,17 @@ class UnsubscribeService
* 构造完整退订 URL用于邮件模板变量 {{unsubscribe_url}})。
* BASE_URL 未配置或参数无效时返回空串。
*/
public static function buildUrl($expertId, $email)
public static function buildUrl($kind, $audienceId, $email)
{
$expertId = intval($expertId);
$email = trim((string)$email);
if ($expertId <= 0 || $email === '') return '';
$kind = self::normalizeKind($kind);
$audienceId = intval($audienceId);
$email = trim((string)$email);
if ($audienceId <= 0 || $email === '') return '';
$base = self::getBaseUrl();
if ($base === '') return '';
return $base . '?id=' . $expertId . '&t=' . self::buildToken($expertId, $email);
return $base
. '?k=' . $kind
. '&id=' . $audienceId
. '&t=' . self::buildToken($kind, $audienceId, $email);
}
}