using dy.net.model.dto; using dy.net.model.entity; using dy.net.service; using dy.net.utils; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Serilog; namespace dy.net.Controllers { [Route("api/[controller]")] [ApiController] [Authorize] public class FollowController : ControllerBase { private readonly DouyinFollowService _douyinFollowService; private readonly DouyinQuartzJobService _douyinQuartzJobService; private readonly DouyinUserLookupService _douyinUserLookupService; private readonly DouyinLiveStatusService _douyinLiveStatusService; public FollowController( DouyinFollowService douyinFollowService, DouyinQuartzJobService douyinQuartzJobService, DouyinUserLookupService douyinUserLookupService, DouyinLiveStatusService douyinLiveStatusService) { this._douyinFollowService = douyinFollowService; _douyinQuartzJobService = douyinQuartzJobService; _douyinUserLookupService = douyinUserLookupService; _douyinLiveStatusService = douyinLiveStatusService; } /// /// 分页查询 /// /// 分页结果 [HttpPost("paged")] public async Task GetPagedAsync( FollowRequestDto dto) { if (string.IsNullOrWhiteSpace(dto.MySelfId)) { return ApiResult.Success(new { }); } var (list, totalCount) = await _douyinFollowService.GetPagedAsync(dto); return ApiResult.Success(new { data = list, total = totalCount, pageIndex = dto.PageIndex, pageSize = dto.PageSize }); } /// /// 重新同步-单次 /// /// [HttpGet("sync")] public async Task SyncFollowList() { try { var started = await _douyinQuartzJobService.StartFollowJobOnceAsync(); return started ? ApiResult.Success(new { message = "关注列表更新任务已启动" }) : ApiResult.Fail("关注列表更新任务启动失败,请查看错误日志。"); } catch (Exception ex) { Log.Error(ex, "手动启动关注列表更新任务失败"); return ApiResult.Fail("关注列表更新任务启动失败:" + ex.GetBaseException().Message); } } [HttpPost("add")] public async Task AddFollow(DouyinFollowed followed) { if (followed == null) { return ApiResult.Fail("新增信息不能为空"); } if (string.IsNullOrWhiteSpace(followed.mySelfId)) { return ApiResult.Fail("请先配置抖音授权信息,抖音授权配置里面填写你的uid"); } if (string.IsNullOrWhiteSpace(followed.SecUid)) { return ApiResult.Fail("博主 SecUid 不能为空"); } if (string.IsNullOrWhiteSpace(followed.UperId)) { return ApiResult.Fail("博主 UID 不能为空"); } if (string.IsNullOrWhiteSpace(followed.UperName)) { return ApiResult.Fail("博主姓名不能为空"); } followed.mySelfId = followed.mySelfId.Trim(); followed.SecUid = followed.SecUid.Trim(); followed.UperId = followed.UperId.Trim(); followed.UperName = followed.UperName.Trim(); followed.DouyinNo = followed.DouyinNo?.Trim(); followed.SavePath = followed.SavePath?.Trim(); followed.Signature = Limit(followed.Signature, 500); followed.UperAvatar = Limit(followed.UperAvatar, 1000); followed.Enterprise = Limit(followed.Enterprise, 200); followed.FullSync = followed.OpenSync && followed.FullSync; if (followed.mySelfId.Length > 200 || followed.SecUid.Length > 500 || followed.UperId.Length > 100 || followed.UperName.Length > 200) { return ApiResult.Fail("新增信息长度超出限制,请检查 UID、SecUid 和博主姓名"); } if (!string.IsNullOrWhiteSpace(followed.DouyinNo) && followed.DouyinNo.Length > 100) { return ApiResult.Fail("抖音号长度不能超过 100 个字符"); } if (!string.IsNullOrWhiteSpace(followed.SavePath)) { if (followed.SavePath.Length > 20) { return ApiResult.Fail("保存文件夹名称最长 20 个字符"); } if (!DouyinFileNameHelper.IsValidWithoutSpecialChars(followed.SavePath)) { return ApiResult.Fail("保存文件夹名称只能包含字母、数字或中文"); } } var result = await _douyinFollowService.TryAddAsync(followed); return result switch { AddFollowResult.Added => ApiResult.Success("新增非关注博主成功"), AddFollowResult.AlreadyExists => ApiResult.Fail("该博主已存在,无需重复添加"), _ => ApiResult.Fail("添加失败,请稍后重试") }; } /// /// 使用展示在抖音资料页上的抖音号查找博主。结果需要用户确认后再调用 add。 /// [HttpPost("resolve-by-douyin-no")] public async Task ResolveByDouyinNo( DouyinFollowLookupRequest request, CancellationToken cancellationToken) { try { var result = await _douyinUserLookupService.ResolveAsync(request, cancellationToken); return ApiResult.Success(result); } catch (InvalidOperationException ex) { return ApiResult.Fail(ex.Message); } } private static string Limit(string value, int maxLength) { value = value?.Trim(); return value != null && value.Length > maxLength ? value[..maxLength] : value; } /// /// 修改关注同步状态 /// /// /// [HttpPost("openOrCloseSync")] public async Task OpenOrCloseSync(FollowUpdateDto dto) { if (dto.OpenSync) { if (!string.IsNullOrWhiteSpace(dto.SavePath)) { if (!DouyinFileNameHelper.IsValidWithoutSpecialChars(dto.SavePath)) { return ApiResult.Fail("请输入有效文件夹名称(字母数字中文简体)"); } if (dto.SavePath.Length > 20) { return ApiResult.Fail("请输入有效文件夹名称(最长20)"); } } } var result = await _douyinFollowService.OpenOrCloseSync(dto); return ApiResult.SuccOrFail(result, result); } /// /// 修改关注全量同步状态 /// /// /// [HttpPost("openOrCloseFullSync")] public async Task OpenOrCloseFullSync(FollowUpdateDto dto) { return await OpenOrCloseSync(dto); } [HttpPost("live-monitor")] public async Task UpdateLiveMonitor( FollowLiveMonitorUpdateDto dto, CancellationToken cancellationToken) { try { return ApiResult.Success(await _douyinLiveStatusService.SetMonitorAsync(dto, cancellationToken)); } catch (Exception ex) when (ex is InvalidOperationException or KeyNotFoundException) { return ApiResult.Fail(ex.Message); } } [HttpPost("live-status/refresh")] public async Task RefreshLiveStatus( FollowLiveStatusRefreshDto dto, CancellationToken cancellationToken) { try { if (dto == null || string.IsNullOrWhiteSpace(dto.Id)) return ApiResult.Fail("博主记录不能为空"); return ApiResult.Success(await _douyinLiveStatusService.RefreshAsync(dto.Id, cancellationToken)); } catch (Exception ex) when (ex is InvalidOperationException or KeyNotFoundException) { return ApiResult.Fail(ex.Message); } } [HttpPost("live-status/query")] public async Task QueryLiveStatus(FollowLiveStatusQueryDto dto) { if (dto?.Ids == null || dto.Ids.Count == 0) return ApiResult.Success(Array.Empty()); return ApiResult.Success(await _douyinLiveStatusService.QueryAsync(dto.Ids)); } [HttpPost("live-email")] public async Task UpdateLiveEmail(FollowLiveEmailUpdateDto dto) { try { return ApiResult.Success(await _douyinLiveStatusService.SetEmailNotificationAsync(dto)); } catch (Exception ex) when (ex is InvalidOperationException or KeyNotFoundException) { return ApiResult.Fail(ex.Message); } } /// /// 删除关注对象 /// /// /// [HttpPost("delete")] public async Task DeleteFollow(FollowUpdateDto dto) { var result = await _douyinFollowService.DeleteFollow(dto); return ApiResult.SuccOrFail(result, result); } } }