Files
links/application/common/service/SplitRedirectService.php
T

135 lines
4.6 KiB
PHP
Raw Normal View History

2026-06-05 04:22:29 +08:00
<?php
declare(strict_types=1);
namespace app\common\service;
use app\admin\model\split\Link;
use app\admin\model\split\Number;
use think\Collection;
/**
2026-07-01 01:26:27 +08:00
* 分流链接落地页:查链、跨工单合并选号、拼接跳转 URL、访问计数
*
* 号码池为同一 split_link_id 下全部 status=normal 的号码;
* 优先选用 ratio_deferred=0 的号码,全部触线降权时回退至降权池仍可轮转。
2026-07-01 01:26:27 +08:00
* random_shuffle 关闭时严格轮转,开启时每次访问随机选号。
2026-06-05 04:22:29 +08:00
*/
class SplitRedirectService
{
private SplitRoundRobinStore $roundRobinStore;
private SplitNumberVisitRuleService $visitRuleService;
public function __construct(
?SplitRoundRobinStore $roundRobinStore = null,
?SplitNumberVisitRuleService $visitRuleService = null
) {
2026-06-05 04:22:29 +08:00
$this->roundRobinStore = $roundRobinStore ?? new SplitRoundRobinStore();
$this->visitRuleService = $visitRuleService ?? new SplitNumberVisitRuleService();
2026-06-05 04:22:29 +08:00
}
/**
* 根据链接码解析本次应跳转的加好友 URL;无效时返回 null
*
* @param string $linkCode 9 位链接码
* @param string $clientIp 访客 IPIP 防护开启时用于国家校验)
*/
public function resolveRedirectUrl(string $linkCode, string $clientIp = ''): ?string
{
$linkCode = SplitLinkCodeService::normalize($linkCode);
if (!SplitLinkCodeService::isValidFormat($linkCode)) {
return null;
}
$link = Link::where('link_code', $linkCode)
->where('status', 'normal')
->find();
if (!$link) {
return null;
}
$ipProtect = (int) $link->getAttr('ip_protect');
$countries = (string) $link->getAttr('countries');
if (!SplitIpProtectService::isAllowed($ipProtect, $countries, $clientIp)) {
return null;
}
/** @var Collection<int, Number>|array<int, Number> $numbers */
$numbers = Number::where('split_link_id', (int) $link['id'])
->where('status', 'normal')
->order('id', 'asc')
->field('id,number,number_type,number_type_custom,ratio_deferred')
2026-06-05 04:22:29 +08:00
->select();
$list = $numbers instanceof Collection ? $numbers->all() : (array) $numbers;
if ($list === []) {
2026-06-05 04:22:29 +08:00
return null;
}
$linkId = (int) $link->getAttr('id');
$picked = $this->pickNumberRow($linkId, $list);
2026-06-05 04:22:29 +08:00
if ($picked === null) {
return null;
}
$numberType = is_array($picked) ? (string) ($picked['number_type'] ?? '') : (string) $picked->getAttr('number_type');
$numberValue = is_array($picked) ? (string) ($picked['number'] ?? '') : (string) $picked->getAttr('number');
$numberCustom = is_array($picked)
? (string) ($picked['number_type_custom'] ?? '')
: (string) $picked->getAttr('number_type_custom');
2026-06-20 04:47:34 +08:00
$replyText = '';
if (in_array($numberType, ['whatsapp', 'telegram'], true)) {
$replyText = SplitAutoReplyService::pickRandomLine((string) $link->getAttr('auto_reply'));
}
$redirectUrl = SplitFriendUrlBuilder::build(
$numberType,
$numberValue,
$numberCustom,
2026-06-20 04:47:34 +08:00
$replyText
);
2026-06-05 04:22:29 +08:00
if ($redirectUrl === '') {
return null;
}
$numberId = is_array($picked) ? (int) ($picked['id'] ?? 0) : (int) $picked->getAttr('id');
if ($numberId > 0) {
Number::where('id', $numberId)->setInc('visit_count');
$this->visitRuleService->recordVisitAfterRedirect($numberId);
2026-06-05 04:22:29 +08:00
}
return $redirectUrl;
}
/**
* 双层选号:优先 ratio_deferred=0,否则回退至降权池
*
* @param array<int, array<string, mixed>|Number> $rows
* @return array<string, mixed>|Number|null
*/
private function pickNumberRow(int $linkId, array $rows)
{
$preferred = [];
$deferred = [];
foreach ($rows as $row) {
$deferredFlag = is_array($row)
? (int) ($row['ratio_deferred'] ?? 0)
: (int) $row->getAttr('ratio_deferred');
if ($deferredFlag === 1) {
$deferred[] = $row;
} else {
$preferred[] = $row;
}
}
$picked = SplitNumberWeighService::pickRowFromList($linkId, $preferred, $this->roundRobinStore, 'preferred');
if ($picked !== null) {
return $picked;
}
return SplitNumberWeighService::pickRowFromList($linkId, $deferred, $this->roundRobinStore, 'deferred');
}
2026-06-05 04:22:29 +08:00
}