, * range_department_ids:list, * skip_verify:bool, * priority_option:array, * snapshot:array * } */ public static function normaliseRemoteResponse(array $response, string $fallbackId = ''): array { $link = isset($response['link']) && is_array($response['link']) ? $response['link'] : $response; $range = isset($response['range']) && is_array($response['range']) ? $response['range'] : (isset($link['range']) && is_array($link['range']) ? $link['range'] : []); $priorityOption = isset($response['priority_option']) && is_array($response['priority_option']) ? $response['priority_option'] : (isset($link['priority_option']) && is_array($link['priority_option']) ? $link['priority_option'] : []); $linkId = trim((string) ($link['link_id'] ?? $response['link_id'] ?? $fallbackId)); $url = trim((string) ($link['url'] ?? $link['link_url'] ?? $response['url'] ?? '')); if ($linkId === '') { throw new RuntimeException('企业微信获客链接详情缺少 link_id'); } if (!self::isAllowed($url)) { throw new RuntimeException('企业微信获客链接详情未返回有效的 https://work.weixin.qq.com/ca/... 地址'); } return [ 'link_id' => $linkId, 'link_name' => trim((string) ($link['link_name'] ?? $link['name'] ?? $linkId)), 'url' => $url, 'create_time' => max(0, (int) ($link['create_time'] ?? 0)), 'range_userids' => self::normaliseScalarList($range['user_list'] ?? []), 'range_department_ids' => self::normaliseScalarList($range['department_list'] ?? []), 'skip_verify' => !empty($link['skip_verify']), 'priority_option' => $priorityOption, 'snapshot' => $response, ]; } /** 追加或替换企业微信获客助手的自定义渠道标识。 */ public static function withCustomerChannel(string $url, string $channel): string { $url = trim($url); $channel = trim($channel); if (!self::isAllowed($url) || $channel === '' || strlen($channel) > 64) { return ''; } // 冒号在查询参数中是合法字符,保留“命名空间:编号”的可读结构;其余字符仍编码。 $parameter = 'customer_channel=' . str_replace('%3A', ':', rawurlencode($channel)); if (preg_match('/([?&])customer_channel=[^&#]*/i', $url) === 1) { return (string) preg_replace_callback( '/([?&])customer_channel=[^&#]*/i', static fn (array $matches): string => $matches[1] . $parameter, $url, 1 ); } $fragment = ''; $fragmentPosition = strpos($url, '#'); if ($fragmentPosition !== false) { $fragment = substr($url, $fragmentPosition); $url = substr($url, 0, $fragmentPosition); } return $url . (str_contains($url, '?') ? '&' : '?') . $parameter . $fragment; } /** @return list */ private static function normaliseScalarList(mixed $values): array { $result = []; $seen = []; foreach ((array) $values as $value) { $normalised = trim((string) $value); $key = 'value:' . $normalised; if ($normalised !== '' && !isset($seen[$key])) { $seen[$key] = true; $result[] = $normalised; } } return $result; } }