using dy.net.model.dto; using dy.net.model.entity; using dy.net.model.response; using dy.net.service; using Quartz; namespace dy.net.job { [DisallowConcurrentExecution] // 禁止并发执行,确保同一时间只有一个实例在运行 public class DouyinFollowsAndCollnectsSyncJob : IJob { /// /// 抖音收藏夹服务 /// protected readonly DouyinCollectCateService douyinCollectCateService; /// /// 抖音Cookie服务,用于获取和管理用户Cookie /// protected readonly DouyinCookieService _dyCookieService; /// /// 抖音HTTP客户端服务,用于发送HTTP请求 /// protected readonly DouyinHttpClientService _douyinService; /// /// 抖音关注服务,用于管理关注数据 /// protected readonly DouyinFollowService _followService; protected readonly DouyinCommonService douyinCommonService; public DouyinFollowsAndCollnectsSyncJob(DouyinCookieService dyCookieService, DouyinHttpClientService douyinService, DouyinFollowService followService, DouyinCommonService douyinCommonService, DouyinCollectCateService douyinCollectCateService) { _dyCookieService = dyCookieService; _douyinService = douyinService; _followService = followService; this.douyinCommonService = douyinCommonService; this.douyinCollectCateService = douyinCollectCateService; } // 第一步:定义常量和枚举(建议放在单独的常量类中,此处为内联演示) private const string DEFAULT_FOLLOW_COUNT = "20"; private const int INVALID_COOKIE_STATUS_CODE = 8; private const string LOG_TAG_COLLECT = "收藏夹同步"; private const string LOG_TAG_MIX = "合集列表同步"; private const string LOG_TAG_SERIES = "短剧列表同步"; private const string LOG_TAG_FOLLOW = "关注列表同步"; /// /// 抖音数据同步任务执行方法(优化版) /// /// 任务执行上下文 public async Task Execute(IJobExecutionContext context) { // 1. 获取有效Cookie列表,提前做空值判断 var cookies = await _dyCookieService.GetOpendCookiesAsync(); if (cookies == null || !cookies.Any()) { Serilog.Log.Debug("当前无可用的抖音Cookie,同步任务跳过"); return; } // 2. 获取应用配置 AppConfig conf = douyinCommonService.GetConfig(); // 3. 遍历每个Cookie,执行各类数据同步 foreach (var ck in cookies) { Serilog.Log.Information($"开始处理Cookie用户:[{ck.UserName}](ID:{ck.Id})"); // 3.1 同步自定义收藏夹(UseCollectFolder=true时) if (ck.UseCollectFolder) { await SyncCollectDataAsync( cookie: ck, cateType: VideoTypeEnum.dy_custom_collect, dataFetchFunc: async (offset) => await _douyinService.SyncCollectFolderList(ck.Cookies, offset), entityConvertFunc: (collectItem) => new DouyinCollectCate { CookieId = ck.Id, CateType = VideoTypeEnum.dy_custom_collect, Name = collectItem.CollectsName, Sync = false, XId = collectItem.CollectsId }, hasMoreFunc: (data) => data?.HasMore ?? false, cursorFunc: (data) => data?.Cursor.ToString() ?? "0", dataListFunc: (data) => data?.CollectsList, logTag: LOG_TAG_COLLECT ); } // 3.2 同步收藏夹合集(DownMix=true时) if (ck.DownMix) { await SyncCollectDataAsync( cookie: ck, cateType: VideoTypeEnum.dy_mix, dataFetchFunc: async (offset) => await _douyinService.SyncMixList(ck.Cookies, offset), entityConvertFunc: (mixItem) => new DouyinCollectCate { CookieId = ck.Id, CateType = VideoTypeEnum.dy_mix, Name = mixItem.MixName, Sync = false, XId = mixItem.MixId, CoverUrl = mixItem.CoverUrl?.UrlList?.FirstOrDefault() // 空值防护:避免CoverUrl为null }, hasMoreFunc: (data) => (data?.HasMore ?? 0) == 1, cursorFunc: (data) => data?.Cursor.ToString() ?? "0", dataListFunc: (data) => data?.MixInfos, logTag: LOG_TAG_MIX ); } // 3.3 同步收藏夹短剧(DownSeries=true时) if (ck.DownSeries) { await SyncCollectDataAsync( cookie: ck, cateType: VideoTypeEnum.dy_series, dataFetchFunc: async (offset) => await _douyinService.SyncSeriesList(ck.Cookies, offset), entityConvertFunc: (seriesItem) => new DouyinCollectCate { CookieId = ck.Id, CateType = VideoTypeEnum.dy_series, Name = seriesItem.SeriesName, Sync = false, XId = seriesItem.SeriesId, CoverUrl = seriesItem.CoverImage?.ImageUrlList?.FirstOrDefault() // 空值防护:避免CoverImage为null }, hasMoreFunc: (data) => (data?.HasMore ?? 0) == 1, cursorFunc: (data) => data?.Cursor.ToString() ?? "0", dataListFunc: (data) => data?.SeriesList, logTag: LOG_TAG_SERIES ); } // 3.4 同步关注列表(单独处理,逻辑特殊不纳入通用方法) await SyncFollowListAsync(ck, conf); Serilog.Log.Debug($"完成Cookie用户:[{ck.UserName}](ID:{ck.Id})的所有收藏夹信息、合集信息、关注列表信息、短剧信息同步"); } // 4. 标记首次运行完成(仅执行一次) await douyinCommonService.SetConfigNotFirstRunning(); } #region 私有通用辅助方法 /// /// 通用收藏类数据同步方法(泛型封装,消除冗余) /// /// 抖音返回的分页数据类型 /// 抖音返回的列表项类型 /// 当前Cookie信息 /// 分类类型 /// 分页数据获取委托 /// 抖音项转换为DouyinCollectCate的委托 /// 判断是否还有更多数据的委托 /// 获取下一页游标值的委托 /// 从分页数据中提取列表的委托 /// 日志标签 private async Task SyncCollectDataAsync( DouyinCookie cookie, VideoTypeEnum cateType, Func> dataFetchFunc, Func entityConvertFunc, Func hasMoreFunc, Func cursorFunc, Func> dataListFunc, string logTag) { string offset = "0"; bool hasMore = true; List collectDataList = new List(); try { // 1. 分页获取抖音数据 while (hasMore) { var pageData = await dataFetchFunc(offset); if (pageData == null) { //Serilog.Log.Warning($"[{cookie.UserName}] - {logTag}:获取分页数据为空,停止分页"); break; } // 2. 更新分页状态 hasMore = hasMoreFunc(pageData); offset = cursorFunc(pageData); // 3. 提取当前页列表数据并转换 var currentPageItems = dataListFunc(pageData); if (currentPageItems != null && currentPageItems.Any()) { var convertItems = currentPageItems.Select(entityConvertFunc).ToList(); collectDataList.AddRange(convertItems); Serilog.Log.Debug($"[{cookie.UserName}] - {logTag}:获取到{currentPageItems.Count}条数据,累计{collectDataList.Count}条"); } } // 4. 调用Sync方法同步到数据库 if (collectDataList.Any()) { var (add, update, delete, succ) = await douyinCollectCateService.Sync( collectDataList, cookie.Id, cateType); Serilog.Log.Debug($"[{cookie.UserName}] - {logTag}:同步完成,新增{add}条,更新{update}条,删除{delete}条,是否成功:{succ}"); } else { Serilog.Log.Debug($"[{cookie.UserName}] - {logTag}:无有效数据需要同步"); } } catch (Exception ex) { Serilog.Log.Error(ex, $"[{cookie.UserName}] - {logTag}:分页同步失败,异常信息:{ex.Message}"); } } /// /// 关注列表单独同步方法(逻辑特殊,单独封装) /// /// 当前Cookie信息 /// 应用配置 private async Task SyncFollowListAsync(DouyinCookie cookie, AppConfig config) { // 1. 校验SecUserId是否有效 if (string.IsNullOrWhiteSpace(cookie.SecUserId)) { Serilog.Log.Debug($"[{cookie.UserName}] - {LOG_TAG_FOLLOW}:未设置SecUserId,跳过"); return; } string offset = "0"; bool hasMore = true; int total = 0; List followList = new List(); try { // 2. 分页获取关注列表数据 while (hasMore) { var (data, error) = await FetchFollowPageDataAsync(cookie, offset); // 3. 处理错误信息 if (error != null && error.StatusCode != 0) { Serilog.Log.Error($"[{cookie.UserName}] - {LOG_TAG_FOLLOW}:发生错误,状态码:{error.StatusCode},错误信息:{error.StatusMsg}"); // 无效Cookie(未登录)直接停止分页 if (error.StatusCode == INVALID_COOKIE_STATUS_CODE) { cookie.StatusMsg = "无效"; cookie.StatusCode = INVALID_COOKIE_STATUS_CODE; await _dyCookieService.UpdateAsync(cookie); break; } } // 4. 处理有效数据 if (data != null) { // 5. 更新MyUserId(若未设置) if (string.IsNullOrWhiteSpace(cookie.MyUserId)) { cookie.MyUserId = data.MySelfUserId; await _dyCookieService.UpdateAsync(cookie); } // 6. 更新分页状态和数据 total = data.Total; hasMore = data.HasMore; offset = data.Offset.ToString(); if (data.Followings != null && data.Followings.Any()) { followList.AddRange(data.Followings); Serilog.Log.Debug($"[{cookie.UserName}] - {LOG_TAG_FOLLOW}:获取到{data.Followings.Count}条关注数据,累计{followList.Count}条"); } // 7. 非首次运行时,仅同步第一页数据 if (!config.IsFirstRunning) { hasMore = false; //Serilog.Log.Debug($"[{cookie.UserName}] - {LOG_TAG_FOLLOW}:非首次运行,停止分页获取"); } } else { Serilog.Log.Warning($"[{cookie.UserName}] - {LOG_TAG_FOLLOW}:获取分页数据为空,停止分页"); break; } } // 8. 同步关注列表到数据库 if (followList.Any()) { var (add, update, succ) = await _followService.Sync(followList, cookie); Serilog.Log.Information($"[{cookie.UserName}] - {LOG_TAG_FOLLOW}:同步完成,新增{add}条,更新{update}条,是否成功:{succ},总关注数:{total}"); } else { Serilog.Log.Debug($"[{cookie.UserName}] - {LOG_TAG_FOLLOW}:无有效关注数据需要同步"); } } catch (Exception ex) { Serilog.Log.Error(ex, $"[{cookie.UserName}] - {LOG_TAG_FOLLOW}:同步失败,异常信息:{ex.Message}"); } } /// /// 分页获取关注列表数据(封装回调逻辑,简化代码) /// /// 当前Cookie信息 /// 分页偏移量 private async Task<(DouyinFollowInfoResponse Data, FollowErrorDto Error)> FetchFollowPageDataAsync(DouyinCookie cookie, string offset) { DouyinFollowInfoResponse resultData = null; FollowErrorDto resultError = null; resultData = await _douyinService.SyncMyFollows( DEFAULT_FOLLOW_COUNT, offset, cookie.SecUserId, cookie.Cookies, async (err) => { resultError = err; // 更新Cookie状态(异步不阻塞) cookie.StatusMsg = err.StatusCode == INVALID_COOKIE_STATUS_CODE ? "无效" : "正常"; cookie.StatusCode = err.StatusCode; await _dyCookieService.UpdateAsync(cookie); }); // 若原方法是回调式无返回值,需调整封装逻辑 return (resultData, resultError); } #endregion } }