| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777 |
- using System.Net.Http.Headers;
- using System.Text.Json;
- using System.Globalization;
- using Microsoft.Extensions.Options;
- namespace OASystem.API.OAMethodLib.SnovioAPI;
- /// <summary>
- /// Snov.io API 服务实现。
- /// </summary>
- public sealed class SnovioService : ISnovioService
- {
- private const string AccessTokenPath = "v1/oauth/access_token";
- private const string CompanySearchStartPath = "v2/database-search/companies/start";
- private const string CompanySearchResultPath = "v2/database-search/companies/result";
- private const string DomainProspectsSearchStartPath = "v2/domain-search/prospects/start";
- private const string DomainProspectsSearchResultPath = "v2/domain-search/prospects/result";
- private const string ProspectEmailSearchStartPath = "v2/domain-search/prospects/search-emails/start";
- private const string ProspectEmailSearchResultPath = "v2/domain-search/prospects/search-emails/result";
- private static readonly JsonSerializerOptions JsonOptions = new()
- {
- PropertyNameCaseInsensitive = true
- };
- private readonly HttpClient _httpClient;
- private readonly SnovioOptions _options;
- private readonly ILogger<SnovioService> _logger;
- public SnovioService(
- HttpClient httpClient,
- IOptions<SnovioOptions> options,
- ILogger<SnovioService> logger)
- {
- _httpClient = httpClient;
- _options = options.Value;
- _logger = logger;
- if (!Uri.TryCreate(NormalizeBaseUrl(_options.BaseUrl), UriKind.Absolute, out var baseAddress))
- throw new InvalidOperationException("Snovio:BaseUrl 配置不是有效的绝对地址。");
- _httpClient.BaseAddress = baseAddress;
- _httpClient.Timeout = TimeSpan.FromSeconds(
- _options.TimeoutSeconds > 0 ? _options.TimeoutSeconds : 30);
- _httpClient.DefaultRequestHeaders.Accept.Add(
- new MediaTypeWithQualityHeaderValue("application/json"));
- }
- /// <inheritdoc />
- public async Task<SnovioAccessTokenResponse> GetAccessTokenAsync(
- CancellationToken cancellationToken = default)
- {
- ValidateCredentials();
- var request = new SnovioAccessTokenRequest
- {
- GrantType = "client_credentials",
- ClientId = _options.ClientId.Trim(),
- ClientSecret = _options.ClientSecret.Trim()
- };
- using var content = new FormUrlEncodedContent(new Dictionary<string, string>
- {
- ["grant_type"] = request.GrantType,
- ["client_id"] = request.ClientId,
- ["client_secret"] = request.ClientSecret
- });
- using var response = await _httpClient.PostAsync(
- AccessTokenPath,
- content,
- cancellationToken).ConfigureAwait(false);
- var responseBody = await response.Content
- .ReadAsStringAsync(cancellationToken)
- .ConfigureAwait(false);
- if (!response.IsSuccessStatusCode)
- {
- _logger.LogError(
- "Snovio OAuth 获取 token 失败,状态码:{StatusCode},响应:{ResponseBody}",
- (int)response.StatusCode,
- responseBody);
- throw new HttpRequestException(
- $"Snovio OAuth 获取 token 失败,HTTP 状态码:{(int)response.StatusCode}。",
- null,
- response.StatusCode);
- }
- var tokenResponse = System.Text.Json.JsonSerializer.Deserialize<SnovioAccessTokenResponse>(
- responseBody,
- JsonOptions);
- if (tokenResponse == null || string.IsNullOrWhiteSpace(tokenResponse.AccessToken))
- {
- _logger.LogError("Snovio OAuth 响应中未返回有效 access_token。");
- throw new InvalidOperationException("Snovio OAuth 响应中未返回有效 access_token。");
- }
- return tokenResponse;
- }
- /// <inheritdoc />
- public async Task<SnovioCompanySearchStartResponse> StartCompanySearchAsync(
- SnovioCompanySearchRequest request,
- CancellationToken cancellationToken = default)
- {
- if (request == null)
- throw new ArgumentNullException(nameof(request));
- if (request.Page < 1)
- throw new ArgumentOutOfRangeException(nameof(request.Page), "页码必须大于等于 1。");
- var tokenResponse = await GetAccessTokenAsync(cancellationToken).ConfigureAwait(false);
- var formFields = BuildCompanySearchForm(request, tokenResponse.AccessToken);
- // DatabaseSearch 文档示例将 access_token 放在请求参数中;同时保留 Bearer 请求头,兼容统一认证方式。
- var requestUri = $"{CompanySearchStartPath}?access_token={Uri.EscapeDataString(tokenResponse.AccessToken)}";
- using var content = new FormUrlEncodedContent(formFields);
- using var httpRequest = new HttpRequestMessage(HttpMethod.Post, requestUri)
- {
- Content = content
- };
- httpRequest.Headers.Authorization = new AuthenticationHeaderValue(
- string.IsNullOrWhiteSpace(tokenResponse.TokenType) ? "Bearer" : tokenResponse.TokenType,
- tokenResponse.AccessToken);
- using var response = await _httpClient
- .SendAsync(httpRequest, cancellationToken)
- .ConfigureAwait(false);
- var responseBody = await response.Content
- .ReadAsStringAsync(cancellationToken)
- .ConfigureAwait(false);
- if (!response.IsSuccessStatusCode)
- {
- _logger.LogError(
- "Snovio 公司搜索任务创建失败,状态码:{StatusCode},响应:{ResponseBody}",
- (int)response.StatusCode,
- responseBody);
- throw new HttpRequestException(
- $"Snovio 公司搜索任务创建失败,HTTP 状态码:{(int)response.StatusCode}。",
- null,
- response.StatusCode);
- }
- var searchResponse = System.Text.Json.JsonSerializer.Deserialize<SnovioCompanySearchStartResponse>(
- responseBody,
- JsonOptions);
- if (searchResponse == null || string.IsNullOrWhiteSpace(searchResponse.Meta?.TaskHash))
- {
- _logger.LogError("Snovio 公司搜索响应中未返回有效 task_hash。");
- throw new InvalidOperationException("Snovio 公司搜索响应中未返回有效 task_hash。");
- }
- return searchResponse;
- }
- /// <inheritdoc />
- public async Task<SnovioCompanySearchResultResponse> GetCompanySearchResultAsync(
- string taskHash,
- CancellationToken cancellationToken = default)
- {
- var normalizedTaskHash = ValidateTaskHash(taskHash);
- var tokenResponse = await GetAccessTokenAsync(cancellationToken).ConfigureAwait(false);
- return await GetCompanySearchResultAsync(
- normalizedTaskHash,
- tokenResponse,
- cancellationToken).ConfigureAwait(false);
- }
- /// <inheritdoc />
- public async Task<SnovioCompanySearchResultResponse> WaitForCompanySearchResultAsync(
- string taskHash,
- TimeSpan? timeout = null,
- TimeSpan? pollInterval = null,
- CancellationToken cancellationToken = default)
- {
- var normalizedTaskHash = ValidateTaskHash(taskHash);
- var tokenResponse = await GetAccessTokenAsync(cancellationToken).ConfigureAwait(false);
- return await WaitForResultAsync(
- () => GetCompanySearchResultAsync(
- normalizedTaskHash,
- tokenResponse,
- cancellationToken),
- result => result.Status,
- "公司搜索",
- timeout,
- pollInterval,
- cancellationToken).ConfigureAwait(false);
- }
- /// <inheritdoc />
- public async Task<SnovioCompanySearchResultResponse> SearchCompaniesAsync(
- SnovioCompanySearchRequest request,
- TimeSpan? timeout = null,
- TimeSpan? pollInterval = null,
- CancellationToken cancellationToken = default)
- {
- var startResponse = await StartCompanySearchAsync(
- request,
- cancellationToken).ConfigureAwait(false);
- return await WaitForCompanySearchResultAsync(
- startResponse.Meta.TaskHash!,
- timeout,
- pollInterval,
- cancellationToken).ConfigureAwait(false);
- }
- /// <inheritdoc />
- public async Task<SnovioDomainProspectsSearchStartResponse> StartDomainProspectsSearchAsync(
- SnovioDomainProspectsSearchRequest request,
- CancellationToken cancellationToken = default)
- {
- if (request == null)
- throw new ArgumentNullException(nameof(request));
- if (string.IsNullOrWhiteSpace(request.Domain))
- throw new ArgumentException("域名不能为空。", nameof(request.Domain));
- if (request.Page < 1)
- throw new ArgumentOutOfRangeException(nameof(request.Page), "页码必须大于等于 1。");
- var positions = request.Positions?
- .Where(x => !string.IsNullOrWhiteSpace(x))
- .Select(x => x.Trim())
- .ToList() ?? new List<string>();
- if (positions.Count > 10)
- throw new ArgumentException("每次搜索最多只能指定 10 个职位。", nameof(request.Positions));
- var tokenResponse = await GetAccessTokenAsync(cancellationToken).ConfigureAwait(false);
- var formFields = BuildDomainProspectsSearchForm(request, positions, tokenResponse.AccessToken);
- // DatabaseSearch 文档示例将 access_token 放在请求参数中;同时保留 Bearer 请求头,兼容统一认证方式。
- var requestUri = $"{DomainProspectsSearchStartPath}?access_token={Uri.EscapeDataString(tokenResponse.AccessToken)}";
- using var content = new FormUrlEncodedContent(formFields);
- using var httpRequest = new HttpRequestMessage(HttpMethod.Post, requestUri)
- {
- Content = content
- };
- httpRequest.Headers.Authorization = new AuthenticationHeaderValue(
- string.IsNullOrWhiteSpace(tokenResponse.TokenType) ? "Bearer" : tokenResponse.TokenType,
- tokenResponse.AccessToken);
- using var response = await _httpClient
- .SendAsync(httpRequest, cancellationToken)
- .ConfigureAwait(false);
- var responseBody = await response.Content
- .ReadAsStringAsync(cancellationToken)
- .ConfigureAwait(false);
- if (!response.IsSuccessStatusCode)
- {
- _logger.LogError(
- "Snovio 域名潜在客户搜索任务创建失败,状态码:{StatusCode},响应:{ResponseBody}",
- (int)response.StatusCode,
- responseBody);
- throw new HttpRequestException(
- $"Snovio 域名潜在客户搜索任务创建失败,HTTP 状态码:{(int)response.StatusCode}。",
- null,
- response.StatusCode);
- }
- var searchResponse = System.Text.Json.JsonSerializer.Deserialize<SnovioDomainProspectsSearchStartResponse>(
- responseBody,
- JsonOptions);
- if (searchResponse == null || string.IsNullOrWhiteSpace(searchResponse.Meta?.TaskHash))
- {
- _logger.LogError("Snovio 域名潜在客户搜索响应中未返回有效 task_hash。");
- throw new InvalidOperationException("Snovio 域名潜在客户搜索响应中未返回有效 task_hash。");
- }
- return searchResponse;
- }
- /// <inheritdoc />
- public async Task<SnovioDomainProspectsSearchResultResponse> GetDomainProspectsSearchResultAsync(
- string taskHash,
- CancellationToken cancellationToken = default)
- {
- var normalizedTaskHash = ValidateTaskHash(taskHash);
- var tokenResponse = await GetAccessTokenAsync(cancellationToken).ConfigureAwait(false);
- return await GetDomainProspectsSearchResultAsync(
- normalizedTaskHash,
- tokenResponse,
- cancellationToken).ConfigureAwait(false);
- }
- /// <inheritdoc />
- public async Task<SnovioDomainProspectsSearchResultResponse> WaitForDomainProspectsSearchResultAsync(
- string taskHash,
- TimeSpan? timeout = null,
- TimeSpan? pollInterval = null,
- CancellationToken cancellationToken = default)
- {
- var normalizedTaskHash = ValidateTaskHash(taskHash);
- var tokenResponse = await GetAccessTokenAsync(cancellationToken).ConfigureAwait(false);
- return await WaitForResultAsync(
- () => GetDomainProspectsSearchResultAsync(
- normalizedTaskHash,
- tokenResponse,
- cancellationToken),
- result => result.Status,
- "域名潜在客户搜索",
- timeout,
- pollInterval,
- cancellationToken).ConfigureAwait(false);
- }
- /// <inheritdoc />
- public async Task<SnovioDomainProspectsSearchResultResponse> SearchDomainProspectsAsync(
- SnovioDomainProspectsSearchRequest request,
- TimeSpan? timeout = null,
- TimeSpan? pollInterval = null,
- CancellationToken cancellationToken = default)
- {
- var startResponse = await StartDomainProspectsSearchAsync(
- request,
- cancellationToken).ConfigureAwait(false);
- return await WaitForDomainProspectsSearchResultAsync(
- startResponse.Meta.TaskHash!,
- timeout,
- pollInterval,
- cancellationToken).ConfigureAwait(false);
- }
- /// <inheritdoc />
- public async Task<SnovioProspectEmailSearchStartResponse> StartProspectEmailSearchAsync(
- string prospectHash,
- SnovioProspectEmailSearchRequest? request = null,
- CancellationToken cancellationToken = default)
- {
- var normalizedProspectHash = ValidateProspectHash(prospectHash);
- var tokenResponse = await GetAccessTokenAsync(cancellationToken).ConfigureAwait(false);
- var formFields = BuildProspectEmailSearchForm(request);
- using var content = new FormUrlEncodedContent(formFields);
- using var httpRequest = new HttpRequestMessage(
- HttpMethod.Post,
- $"{ProspectEmailSearchStartPath}/{Uri.EscapeDataString(normalizedProspectHash)}")
- {
- Content = content
- };
- httpRequest.Headers.Authorization = new AuthenticationHeaderValue(
- string.IsNullOrWhiteSpace(tokenResponse.TokenType) ? "Bearer" : tokenResponse.TokenType,
- tokenResponse.AccessToken);
- using var response = await _httpClient
- .SendAsync(httpRequest, cancellationToken)
- .ConfigureAwait(false);
- var responseBody = await response.Content
- .ReadAsStringAsync(cancellationToken)
- .ConfigureAwait(false);
- if (!response.IsSuccessStatusCode)
- {
- _logger.LogError(
- "Snovio 潜客邮箱检索任务创建失败,状态码:{StatusCode},响应:{ResponseBody}",
- (int)response.StatusCode,
- responseBody);
- throw new HttpRequestException(
- $"Snovio 潜客邮箱检索任务创建失败,HTTP 状态码:{(int)response.StatusCode}。",
- null,
- response.StatusCode);
- }
- var searchResponse = System.Text.Json.JsonSerializer.Deserialize<SnovioProspectEmailSearchStartResponse>(
- responseBody,
- JsonOptions);
- if (searchResponse == null || string.IsNullOrWhiteSpace(searchResponse.Meta?.TaskHash))
- {
- _logger.LogError("Snovio 潜客邮箱检索响应中未返回有效 task_hash。");
- throw new InvalidOperationException("Snovio 潜客邮箱检索响应中未返回有效 task_hash。");
- }
- return searchResponse;
- }
- /// <inheritdoc />
- public async Task<SnovioProspectEmailSearchResultResponse> GetProspectEmailSearchResultAsync(
- string taskHash,
- CancellationToken cancellationToken = default)
- {
- var normalizedTaskHash = ValidateTaskHash(taskHash);
- var tokenResponse = await GetAccessTokenAsync(cancellationToken).ConfigureAwait(false);
- return await GetProspectEmailSearchResultAsync(
- normalizedTaskHash,
- tokenResponse,
- cancellationToken).ConfigureAwait(false);
- }
- /// <inheritdoc />
- public async Task<SnovioProspectEmailSearchResultResponse> WaitForProspectEmailSearchResultAsync(
- string taskHash,
- TimeSpan? timeout = null,
- TimeSpan? pollInterval = null,
- CancellationToken cancellationToken = default)
- {
- var normalizedTaskHash = ValidateTaskHash(taskHash);
- var tokenResponse = await GetAccessTokenAsync(cancellationToken).ConfigureAwait(false);
- return await WaitForResultAsync(
- () => GetProspectEmailSearchResultAsync(
- normalizedTaskHash,
- tokenResponse,
- cancellationToken),
- result => result.Status,
- "潜客邮箱检索",
- timeout,
- pollInterval,
- cancellationToken).ConfigureAwait(false);
- }
- /// <inheritdoc />
- public async Task<SnovioProspectEmailSearchResultResponse> SearchProspectEmailAsync(
- string prospectHash,
- SnovioProspectEmailSearchRequest? request = null,
- TimeSpan? timeout = null,
- TimeSpan? pollInterval = null,
- CancellationToken cancellationToken = default)
- {
- var startResponse = await StartProspectEmailSearchAsync(
- prospectHash,
- request,
- cancellationToken).ConfigureAwait(false);
- return await WaitForProspectEmailSearchResultAsync(
- startResponse.Meta.TaskHash!,
- timeout,
- pollInterval,
- cancellationToken).ConfigureAwait(false);
- }
- private async Task<SnovioCompanySearchResultResponse> GetCompanySearchResultAsync(
- string taskHash,
- SnovioAccessTokenResponse tokenResponse,
- CancellationToken cancellationToken)
- {
- using var httpRequest = CreateAuthorizedGetRequest(
- $"{CompanySearchResultPath}/{Uri.EscapeDataString(taskHash)}",
- tokenResponse);
- using var response = await _httpClient
- .SendAsync(httpRequest, cancellationToken)
- .ConfigureAwait(false);
- var responseBody = await response.Content
- .ReadAsStringAsync(cancellationToken)
- .ConfigureAwait(false);
- if (!response.IsSuccessStatusCode)
- {
- _logger.LogError(
- "Snovio 公司搜索结果获取失败,状态码:{StatusCode},响应:{ResponseBody}",
- (int)response.StatusCode,
- responseBody);
- throw new HttpRequestException(
- $"Snovio 公司搜索结果获取失败,HTTP 状态码:{(int)response.StatusCode}。",
- null,
- response.StatusCode);
- }
- var searchResponse = System.Text.Json.JsonSerializer.Deserialize<SnovioCompanySearchResultResponse>(
- responseBody,
- JsonOptions);
- if (searchResponse == null)
- throw new InvalidOperationException("Snovio 公司搜索结果响应为空或格式无效。");
- return searchResponse;
- }
- private async Task<SnovioDomainProspectsSearchResultResponse> GetDomainProspectsSearchResultAsync(
- string taskHash,
- SnovioAccessTokenResponse tokenResponse,
- CancellationToken cancellationToken)
- {
- using var httpRequest = CreateAuthorizedGetRequest(
- $"{DomainProspectsSearchResultPath}/{Uri.EscapeDataString(taskHash)}",
- tokenResponse);
- using var response = await _httpClient
- .SendAsync(httpRequest, cancellationToken)
- .ConfigureAwait(false);
- var responseBody = await response.Content
- .ReadAsStringAsync(cancellationToken)
- .ConfigureAwait(false);
- if (!response.IsSuccessStatusCode)
- {
- _logger.LogError(
- "Snovio 域名潜在客户搜索结果获取失败,状态码:{StatusCode},响应:{ResponseBody}",
- (int)response.StatusCode,
- responseBody);
- throw new HttpRequestException(
- $"Snovio 域名潜在客户搜索结果获取失败,HTTP 状态码:{(int)response.StatusCode}。",
- null,
- response.StatusCode);
- }
- var searchResponse = System.Text.Json.JsonSerializer.Deserialize<SnovioDomainProspectsSearchResultResponse>(
- responseBody,
- JsonOptions);
- if (searchResponse == null)
- throw new InvalidOperationException("Snovio 域名潜在客户搜索结果响应为空或格式无效。");
- return searchResponse;
- }
- private async Task<SnovioProspectEmailSearchResultResponse> GetProspectEmailSearchResultAsync(
- string taskHash,
- SnovioAccessTokenResponse tokenResponse,
- CancellationToken cancellationToken)
- {
- using var httpRequest = CreateAuthorizedGetRequest(
- $"{ProspectEmailSearchResultPath}/{Uri.EscapeDataString(taskHash)}",
- tokenResponse);
- using var response = await _httpClient
- .SendAsync(httpRequest, cancellationToken)
- .ConfigureAwait(false);
- var responseBody = await response.Content
- .ReadAsStringAsync(cancellationToken)
- .ConfigureAwait(false);
- if (!response.IsSuccessStatusCode)
- {
- _logger.LogError(
- "Snovio 潜客邮箱检索结果获取失败,状态码:{StatusCode},响应:{ResponseBody}",
- (int)response.StatusCode,
- responseBody);
- throw new HttpRequestException(
- $"Snovio 潜客邮箱检索结果获取失败,HTTP 状态码:{(int)response.StatusCode}。",
- null,
- response.StatusCode);
- }
- var searchResponse = System.Text.Json.JsonSerializer.Deserialize<SnovioProspectEmailSearchResultResponse>(
- responseBody,
- JsonOptions);
- if (searchResponse == null)
- throw new InvalidOperationException("Snovio 潜客邮箱检索结果响应为空或格式无效。");
- return searchResponse;
- }
- private async Task<TResponse> WaitForResultAsync<TResponse>(
- Func<Task<TResponse>> getResultAsync,
- Func<TResponse, string?> getStatus,
- string taskDescription,
- TimeSpan? timeout,
- TimeSpan? pollInterval,
- CancellationToken cancellationToken)
- {
- var maxWait = timeout ?? TimeSpan.FromMinutes(2);
- var interval = pollInterval ?? TimeSpan.FromSeconds(3);
- if (maxWait <= TimeSpan.Zero)
- throw new ArgumentOutOfRangeException(nameof(timeout), "等待超时时间必须大于 0。");
- if (interval <= TimeSpan.Zero)
- throw new ArgumentOutOfRangeException(nameof(pollInterval), "轮询间隔必须大于 0。");
- var deadline = DateTimeOffset.UtcNow.Add(maxWait);
- while (true)
- {
- var result = await getResultAsync().ConfigureAwait(false);
- var status = getStatus(result);
- if (string.Equals(status, "completed", StringComparison.OrdinalIgnoreCase))
- return result;
- if (IsFailedStatus(status))
- throw new InvalidOperationException(
- $"Snovio {taskDescription}任务失败,状态:{status}。");
- _logger.LogDebug(
- "Snovio {TaskDescription}任务仍在处理中,状态:{Status}",
- taskDescription,
- status ?? "unknown");
- var remaining = deadline - DateTimeOffset.UtcNow;
- if (remaining <= TimeSpan.Zero)
- throw new TimeoutException(
- $"Snovio {taskDescription}任务在 {maxWait.TotalSeconds:0} 秒内未完成。");
- await Task.Delay(
- remaining < interval ? remaining : interval,
- cancellationToken).ConfigureAwait(false);
- }
- }
- private static bool IsFailedStatus(string? status)
- {
- return status is not null &&
- (status.Equals("failed", StringComparison.OrdinalIgnoreCase) ||
- status.Equals("failure", StringComparison.OrdinalIgnoreCase) ||
- status.Equals("error", StringComparison.OrdinalIgnoreCase) ||
- status.Equals("cancelled", StringComparison.OrdinalIgnoreCase) ||
- status.Equals("canceled", StringComparison.OrdinalIgnoreCase));
- }
- private static string ValidateTaskHash(string taskHash)
- {
- if (string.IsNullOrWhiteSpace(taskHash))
- throw new ArgumentException("task_hash 不能为空。", nameof(taskHash));
- return taskHash.Trim();
- }
- private static string ValidateProspectHash(string prospectHash)
- {
- if (string.IsNullOrWhiteSpace(prospectHash))
- throw new ArgumentException("prospect_hash 不能为空。", nameof(prospectHash));
- return prospectHash.Trim();
- }
- private static HttpRequestMessage CreateAuthorizedGetRequest(
- string path,
- SnovioAccessTokenResponse tokenResponse)
- {
- var requestUri = $"{path}?access_token={Uri.EscapeDataString(tokenResponse.AccessToken)}";
- var httpRequest = new HttpRequestMessage(HttpMethod.Get, requestUri);
- httpRequest.Headers.Authorization = new AuthenticationHeaderValue(
- string.IsNullOrWhiteSpace(tokenResponse.TokenType) ? "Bearer" : tokenResponse.TokenType,
- tokenResponse.AccessToken);
- return httpRequest;
- }
- private static Dictionary<string, string> BuildCompanySearchForm(
- SnovioCompanySearchRequest request,
- string accessToken)
- {
- var formFields = new Dictionary<string, string>
- {
- ["access_token"] = accessToken,
- ["page"] = request.Page.ToString(CultureInfo.InvariantCulture)
- };
- if (!string.IsNullOrWhiteSpace(request.WebhookUrl))
- formFields["webhook_url"] = request.WebhookUrl.Trim();
- var filters = request.Filters ?? new SnovioCompanySearchFilters();
- var company = filters.Company ?? new SnovioCompanyFilter();
- AddStringList(formFields, "filters[company][name][include]", company.Name?.Include);
- AddStringList(formFields, "filters[company][name][exclude]", company.Name?.Exclude);
- AddStringList(formFields, "filters[company][industries][include]", company.Industries?.Include);
- AddStringList(formFields, "filters[company][industries][exclude]", company.Industries?.Exclude);
- AddStringList(formFields, "filters[company][specialities]", company.Specialities);
- if (!string.IsNullOrWhiteSpace(company.Size))
- formFields["filters[company][size]"] = company.Size.Trim();
- if (company.Revenue?.Min is { } revenueMin)
- formFields["filters[company][revenue][min]"] = revenueMin.ToString(CultureInfo.InvariantCulture);
- if (company.Revenue?.Max is { } revenueMax)
- formFields["filters[company][revenue][max]"] = revenueMax.ToString(CultureInfo.InvariantCulture);
- if (company.Founded?.From is { } foundedFrom)
- formFields["filters[company][founded][from]"] = foundedFrom.ToString(CultureInfo.InvariantCulture);
- if (company.Founded?.Till is { } foundedTill)
- formFields["filters[company][founded][till]"] = foundedTill.ToString(CultureInfo.InvariantCulture);
- AddLocation(formFields, "include", filters.Locations?.Include);
- AddLocation(formFields, "exclude", filters.Locations?.Exclude);
- return formFields;
- }
- private static Dictionary<string, string> BuildDomainProspectsSearchForm(
- SnovioDomainProspectsSearchRequest request,
- IReadOnlyList<string> positions,
- string accessToken)
- {
- var formFields = new Dictionary<string, string>
- {
- ["access_token"] = accessToken,
- ["domain"] = request.Domain.Trim(),
- ["page"] = request.Page.ToString(CultureInfo.InvariantCulture)
- };
- if (!string.IsNullOrWhiteSpace(request.WebhookUrl))
- formFields["webhook_url"] = request.WebhookUrl.Trim();
- for (var index = 0; index < positions.Count; index++)
- {
- formFields[$"positions[{index}]"] = positions[index];
- }
- return formFields;
- }
- private static Dictionary<string, string> BuildProspectEmailSearchForm(
- SnovioProspectEmailSearchRequest? request)
- {
- var formFields = new Dictionary<string, string>();
- if (!string.IsNullOrWhiteSpace(request?.WebhookUrl))
- formFields["webhook_url"] = request.WebhookUrl.Trim();
- return formFields;
- }
- private static void AddStringList(
- IDictionary<string, string> formFields,
- string keyPrefix,
- IEnumerable<string>? values)
- {
- if (values == null)
- return;
- var index = 0;
- foreach (var value in values.Where(x => !string.IsNullOrWhiteSpace(x)))
- {
- formFields[$"{keyPrefix}[{index++}]"] = value.Trim();
- }
- }
- private static void AddLocation(
- IDictionary<string, string> formFields,
- string locationMode,
- SnovioLocationFilterItem? location)
- {
- if (location == null)
- return;
- if (!string.IsNullOrWhiteSpace(location.Locality))
- formFields[$"filters[locations][{locationMode}][locality]"] = location.Locality.Trim();
- if (!string.IsNullOrWhiteSpace(location.LocationType))
- formFields[$"filters[locations][{locationMode}][location_type]"] = location.LocationType.Trim();
- }
- private void ValidateCredentials()
- {
- if (string.IsNullOrWhiteSpace(_options.ClientId))
- throw new InvalidOperationException("Snovio:ClientId 未配置。");
- if (string.IsNullOrWhiteSpace(_options.ClientSecret))
- throw new InvalidOperationException("Snovio:ClientSecret 未配置。");
- }
- private static string NormalizeBaseUrl(string baseUrl)
- {
- if (string.IsNullOrWhiteSpace(baseUrl))
- return "https://api.snov.io/";
- return baseUrl.TrimEnd('/') + "/";
- }
- }
|