import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Observable, throwError } from 'rxjs'; import { catchError, retry } from 'rxjs/operators'; import { environment } from '../../environments/environment'; // 抖音API接口定义 interface DouyinSearchParams { keyword: string; cursor?: number | string; // 支持数字或字符串 sort_type?: string; publish_time?: string; filter_duration?: string; content_type?: string; search_id?: string; } interface DouyinVideoDetailParams { aweme_id: string; } interface DouyinCommentsParams { aweme_id: string; cursor?: number | string; count?: number | string; } @Injectable({ providedIn: 'root' }) export class DouyinApiService { private readonly BASE_URL = environment.douyinApi.baseUrl; private readonly TIMEOUT = environment.douyinApi.timeout; private readonly MAX_RETRIES = environment.douyinApi.maxRetries; constructor(private http: HttpClient) {} /** * 抖音综合搜索 * @param params 搜索参数 * @returns 搜索结果 */ searchVideos(params: DouyinSearchParams): Observable { const url = `${this.BASE_URL}/api/v1/douyin/search/fetch_challenge_search_v2`; const defaultParams = { cursor: 0, sort_type: '0', publish_time: '0', filter_duration: '0', content_type: '0', search_id: '' }; const requestParams = { ...defaultParams, ...params, cursor: params.cursor || 0 // 保持cursor为数字,根据API文档 }; return this.http.post(url, requestParams, { headers: this.getHeaders(), timeout: this.TIMEOUT }).pipe( retry(this.MAX_RETRIES), catchError(this.handleError) ); } /** * 获取单个视频详情 * @param aweme_id 视频ID * @returns 视频详情 */ getVideoDetail(aweme_id: string): Observable { const url = `${this.BASE_URL}/api/v1/douyin/app/v3/fetch_one_video_v3`; return this.http.get(url, { params: { aweme_id }, headers: this.getHeaders(), timeout: this.TIMEOUT }).pipe( retry(this.MAX_RETRIES), catchError(this.handleError) ); } /** * 获取视频评论 * @param params 评论参数 * @returns 评论列表 */ getVideoComments(params: DouyinCommentsParams): Observable { const url = `${this.BASE_URL}/app/v3/fetch_video_comments`; const defaultParams = { cursor: 0, count: 20 }; const requestParams = { ...defaultParams, ...params, cursor: params.cursor || 0, count: params.count || 20 }; return this.http.get(url, { params: requestParams, headers: this.getHeaders(), timeout: this.TIMEOUT }).pipe( retry(this.MAX_RETRIES), catchError(this.handleError) ); } /** * 获取评论回复 * @param item_id 视频ID * @param comment_id 评论ID * @param cursor 游标 * @returns 回复列表 */ getCommentReplies(item_id: string, comment_id: string, cursor: number = 0): Observable { const url = `${this.BASE_URL}/comment/reply/list`; return this.http.post(url, { item_id, comment_id, cursor, count: 20 }, { headers: this.getHeaders(), timeout: this.TIMEOUT }).pipe( retry(this.MAX_RETRIES), catchError(this.handleError) ); } /** * 获取用户信息 * @param sec_uid 用户sec_uid * @returns 用户信息 */ getUserProfile(sec_uid: string): Observable { const url = `${this.BASE_URL}/app/v3/handler_user_profile`; return this.http.get(url, { params: { sec_user_id: sec_uid }, headers: this.getHeaders(), timeout: this.TIMEOUT }).pipe( retry(this.MAX_RETRIES), catchError(this.handleError) ); } /** * 获取请求头(使用环境配置) */ private getHeaders(): HttpHeaders { return new HttpHeaders({ 'Content-Type': 'application/json', 'Accept': 'application/json', 'Authorization': `Bearer ${environment.douyinApi.token}` }); } /** * 错误处理 */ private handleError(error: any): Observable { let errorMessage = ''; if (error.error instanceof ErrorEvent) { // 客户端错误 errorMessage = `网络错误: ${error.error.message}`; } else { // 服务端错误 switch (error.status) { case 401: errorMessage = '认证失败 (401): Token无效或已过期'; break; case 403: errorMessage = '访问被拒绝 (403): 权限不足'; break; case 429: errorMessage = '请求过于频繁 (429): 请稍后重试'; break; case 500: errorMessage = '服务器内部错误 (500): 请稍后重试'; break; case 400: errorMessage = '请求参数错误 (400): 请检查请求参数格式'; break; case 0: errorMessage = '网络连接失败: 请检查网络设置'; break; default: errorMessage = `请求失败 (${error.status}): ${error.message || '未知错误'}`; } // 检查API返回的具体错误信息 if (error.error && error.error.msg) { errorMessage += ` | API错误: ${error.error.msg}`; } } console.error('Douyin API Error:', errorMessage, error); return throwError(() => new Error(errorMessage)); } }