roundRobinStore = $roundRobinStore ?? new SplitRoundRobinStore(); $this->visitRuleService = $visitRuleService ?? new SplitNumberVisitRuleService(); } /** * 根据链接码解析本次应跳转的加好友 URL;无效时返回 null * * @param string $linkCode 9 位链接码 * @param string $clientIp 访客 IP(IP 防护开启时用于国家校验) */ 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; } $linkId = (int) $link->getAttr('id'); $list = $this->loadEligibleNumbers($linkId); if ($list === []) { return null; } if (SplitNumberWeighService::isRandomShuffleEnabled($linkId)) { $picked = $this->pickNumberRowRandomByTicketWeight($linkId, $list); } else { $picked = $this->pickNumberRow($linkId, $list); } 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'); $replyText = ''; if (in_array($numberType, ['whatsapp', 'telegram'], true)) { $replyText = SplitAutoReplyService::pickRandomLine((string) $link->getAttr('auto_reply')); } $redirectUrl = SplitFriendUrlBuilder::build( $numberType, $numberValue, $numberCustom, $replyText ); 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); } return $redirectUrl; } /** * 可参与选号的号码:status=normal,且 ticket_name 不属于已关闭工单 * * @return list> */ private function loadEligibleNumbers(int $linkId): array { if ($linkId <= 0) { return []; } /** @var list $hiddenTicketNames */ $hiddenTicketNames = Ticket::where('split_link_id', $linkId) ->where('status', 'hidden') ->column('ticket_name'); $hiddenTicketNames = array_values(array_unique(array_filter(array_map( static function ($name): string { return trim((string) $name); }, $hiddenTicketNames )))); $query = Number::where('split_link_id', $linkId) ->where('status', 'normal') ->order('id', 'asc') ->field('id,number,number_type,number_type_custom,ratio_deferred,ticket_name'); if ($hiddenTicketNames !== []) { $query->where('ticket_name', 'not in', $hiddenTicketNames); } $numbers = $query->select(); return $numbers instanceof Collection ? $numbers->all() : (array) $numbers; } /** * random_shuffle=1:先按工单开启号码数加权随机选工单,再在该工单内选号 * * @param list> $rows * @return array|Number|null */ private function pickNumberRowRandomByTicketWeight(int $linkId, array $rows) { $groups = $this->groupNumbersByTicket($rows); if ($groups === []) { return null; } $ticketKey = $this->pickTicketGroupKeyWeighted($groups); if ($ticketKey === null || !isset($groups[$ticketKey])) { return null; } return $this->pickNumberRow($linkId, $groups[$ticketKey]); } /** * @param list> $rows * @return array>> */ private function groupNumbersByTicket(array $rows): array { /** @var array>> $groups */ $groups = []; foreach ($rows as $row) { $name = is_array($row) ? trim((string) ($row['ticket_name'] ?? '')) : trim((string) $row->getAttr('ticket_name')); $key = $name !== '' ? $name : '__manual__'; $groups[$key][] = $row; } return $groups; } /** * 按各分组号码数量加权随机选一个工单(分组 key) * * @param array>> $groups */ private function pickTicketGroupKeyWeighted(array $groups): ?string { $total = 0; foreach ($groups as $rows) { $total += count($rows); } if ($total <= 0) { return null; } $keys = array_keys($groups); if (count($keys) === 1) { // PHP 会把纯数字字符串键转成 int,需强转以匹配 ?string 返回类型 return (string) $keys[0]; } $r = random_int(1, $total); $acc = 0; foreach ($groups as $key => $rows) { $acc += count($rows); if ($r <= $acc) { return (string) $key; } } return (string) $keys[0]; } /** * 双层选号:优先 ratio_deferred=0,否则回退至降权池 * * @param list> $rows * @return array|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'); } }