Files
douyin/service/DouyinUserLookupService.cs

138 lines
6.1 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections.Concurrent;
using System.Text.RegularExpressions;
using dy.net.model.dto;
using dy.net.model.entity;
using SqlSugar;
namespace dy.net.service
{
public sealed class DouyinUserLookupService
{
private static readonly TimeSpan CacheLifetime = TimeSpan.FromMinutes(5);
private static readonly SemaphoreSlim SearchGate = new(1, 1);
private static readonly ConcurrentDictionary<string, CacheEntry> Cache = new(StringComparer.Ordinal);
private static readonly Regex LabeledDouyinNo = new(
@"^(?:抖音号|douyin)\s*[:]\s*([^\s,;]+)",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex LabeledDouyinNoPrefix = new(
@"^(?:抖音号|douyin)\s*[:]",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
private readonly ISqlSugarClient _db;
private readonly DouyinUserSearchClient _searchClient;
public DouyinUserLookupService(ISqlSugarClient db, DouyinUserSearchClient searchClient)
{
_db = db;
_searchClient = searchClient;
}
public async Task<DouyinFollowLookupResult> ResolveAsync(
DouyinFollowLookupRequest request,
CancellationToken cancellationToken = default)
{
if (request == null) throw new InvalidOperationException("查询参数不能为空");
var query = NormalizeDouyinNo(request.DouyinNo);
if (string.IsNullOrWhiteSpace(request.CookieId)) throw new InvalidOperationException("请选择抖音授权账号");
var cookie = await _db.Queryable<DouyinCookie>().InSingleAsync(request.CookieId);
if (cookie == null) throw new InvalidOperationException("选择的抖音授权账号不存在");
if (cookie.Status != 1 || cookie.StatusCode != 0 || string.IsNullOrWhiteSpace(cookie.Cookies))
throw new InvalidOperationException("当前账号 Cookie 无效或未启用,请先更新抖音授权");
if (string.IsNullOrWhiteSpace(cookie.MyUserId))
throw new InvalidOperationException("当前账号缺少用户 UID,请先完善抖音授权配置");
var cacheKey = cookie.Id + ":" + query.ToUpperInvariant();
var candidates = TryGetCached(cacheKey);
if (candidates == null)
{
await SearchGate.WaitAsync(cancellationToken);
try
{
candidates = TryGetCached(cacheKey);
if (candidates == null)
{
candidates = await _searchClient.SearchAsync(query, cookie.Cookies, cancellationToken);
Cache[cacheKey] = new CacheEntry(DateTimeOffset.UtcNow.Add(CacheLifetime), Clone(candidates));
}
}
finally
{
SearchGate.Release();
}
}
if (candidates.Count == 0) throw new InvalidOperationException("未找到对应博主,请检查抖音号或使用手动添加");
var secUids = candidates.Select(x => x.SecUid).Distinct().ToList();
var existing = await _db.Queryable<DouyinFollowed>()
.Where(x => x.mySelfId == cookie.MyUserId && secUids.Contains(x.SecUid))
.Select(x => x.SecUid)
.ToListAsync();
var existingSet = existing.ToHashSet(StringComparer.Ordinal);
foreach (var candidate in candidates)
{
candidate.ExactMatch = string.Equals(candidate.UniqueId, query, StringComparison.OrdinalIgnoreCase)
|| string.Equals(candidate.ShortId, query, StringComparison.OrdinalIgnoreCase);
candidate.AlreadyExists = existingSet.Contains(candidate.SecUid);
}
return new DouyinFollowLookupResult
{
Query = query,
Candidates = candidates
.OrderByDescending(x => x.ExactMatch)
.ThenByDescending(x => x.FollowerCount)
.Take(10)
.ToList()
};
}
public static string NormalizeDouyinNo(string input)
{
var value = (input ?? string.Empty).Trim();
var match = LabeledDouyinNo.Match(value);
if (match.Success)
value = match.Groups[1].Value;
else if (LabeledDouyinNoPrefix.IsMatch(value))
value = string.Empty;
value = value.Trim().TrimStart('@').Trim();
if (string.IsNullOrWhiteSpace(value)) throw new InvalidOperationException("请输入抖音号");
if (value.Length > 64) throw new InvalidOperationException("抖音号长度不能超过 64 个字符");
if (value.Any(char.IsWhiteSpace) || value.Any(char.IsControl))
throw new InvalidOperationException("抖音号格式不正确");
return value;
}
private static List<DouyinFollowCandidate> TryGetCached(string key)
{
if (!Cache.TryGetValue(key, out var entry)) return null;
if (entry.ExpiresAt <= DateTimeOffset.UtcNow)
{
Cache.TryRemove(key, out _);
return null;
}
return Clone(entry.Candidates);
}
private static List<DouyinFollowCandidate> Clone(IEnumerable<DouyinFollowCandidate> source) =>
source.Select(x => new DouyinFollowCandidate
{
SecUid = x.SecUid,
UperId = x.UperId,
DouyinNo = x.DouyinNo,
UniqueId = x.UniqueId,
ShortId = x.ShortId,
UperName = x.UperName,
UperAvatar = x.UperAvatar,
Signature = x.Signature,
Enterprise = x.Enterprise,
FollowerCount = x.FollowerCount,
ExactMatch = x.ExactMatch,
AlreadyExists = x.AlreadyExists
}).ToList();
private sealed record CacheEntry(DateTimeOffset ExpiresAt, List<DouyinFollowCandidate> Candidates);
}
}