返回 AiToEarn
tiktok.controller.ts
根目录 / project / aitoearn-electron / server / src / modules / plat / tiktok / tiktok.controller.ts
1 import { Controller, Get, Post, Body, Query, Res, Param, Delete, UseInterceptors, BadRequestException,
2 UploadedFile, HttpException, HttpStatus, UseGuards } from '@nestjs/common';
3 import { FileInterceptor } from '@nestjs/platform-express';
4 import { ApiTags, ApiOperation, ApiBody, ApiConsumes, ApiQuery, ApiParam } from '@nestjs/swagger';
5 import { Express } from 'express';
6 import { TikTokService } from './tiktok.service';
7 import { TikTokAuthService } from './tiktok.auth.service';
8 import { GetToken, Public, AuthGuard } from 'src/auth/auth.guard';
9 import { TokenInfo } from 'src/auth/interfaces/auth.interfaces';
10 import { CreateVideoDto, TikTokCommentDto, GetVideosQueryDto, TikTokVideoFilterDto, CombinedVideoUploadDto } from './dto/tiktok.dto';
11 import { Response } from 'express';
12
13 @ApiTags('plat/tiktok - TikTok 平台')
14 @Controller('plat/tiktok')
15 // @UseGuards(AuthGuard)
16 export class TikTokController {
17 constructor(
18 private readonly tikTokService: TikTokService,
19 private readonly tikTokAuthService: TikTokAuthService,
20 ) {}
21
22 /**
23 * 获取TikTok授权URL
24 */
25 @Get('auth/url')
26 @ApiOperation({ summary: '获取TikTok授权URL' })
27 @ApiQuery({ name: 'mail', required: false, description: '用户邮箱' })
28 async getAuthUrl(
29 @GetToken() systemToken: TokenInfo,
30 @Query('mail') mail: string,
31 ) {
32 if (!systemToken.id || !mail) {
33 throw new BadRequestException('token和mail是必须的');
34 }
35 return this.tikTokAuthService.getAuthorizationUrl(systemToken.id, mail);
36 }
37
38 /**
39 * TikTok OAuth2回调处理
40 */
41
42 @ApiOperation({ summary: 'TikTok OAuth2回调处理' })
43 @Public()
44 @ApiQuery({ name: 'code', type: String, description: 'OAuth2授权码' })
45 @ApiQuery({ name: 'state', type: String, description: '状态码' })
46 @Get('auth/callback')
47 async handleAuthCallback(
48 @Query('code') code: string,
49 @Query('state') state: string,
50 @Res() res: Response,
51 ) {
52 if (!code || !state) {
53 throw new BadRequestException('缺少必要的参数');
54 }
55 console.log(code, state);
56
57 try {
58 // 处理授权回调
59 const results = await this.tikTokAuthService.handleAuthorizationCallback(code, state);
60 const render_msg = {
61 message: "授权成功! 这里是添加账号成功后的前端页面," ,
62 datas: results
63 };
64
65 return res.render('google/index', render_msg);
66 } catch (error) {
67 console.error('处理TikTok授权回调失败:', error.response?.data || error.message);
68 throw new BadRequestException(`处理授权回调失败: ${error.response?.data?.error?.message || error.message}`);
69 }
70 }
71
72 /**
73 * 检查用户是否已授权TikTok
74 */
75 @Get('auth/check')
76 @ApiOperation({ summary: '检查用户是否已授权TikTok' })
77 @ApiQuery({ name: 'accountId', type: String, description: 'TikTok账号ID' })
78 async checkAuth(@Query('accountId') accountId: string) {
79 if (!accountId) {
80 throw new BadRequestException('accountId是必须的');
81 }
82
83 const isAuthorized = await this.tikTokAuthService.isAuthorized(accountId);
84 return { authorized: isAuthorized };
85 }
86
87 /**
88 * 撤销TikTok授权
89 */
90 @Post('auth/revoke')
91 @ApiOperation({ summary: '撤销TikTok授权' })
92 @ApiBody({
93 schema: {
94 type: 'object',
95 properties: {
96 accountId: { type: 'string', description: 'TikTok账号ID' }
97 },
98 required: ['accountId']
99 }
100 })
101 async revokeAuth(@GetToken() systemToken: TokenInfo, @Body('accountId') accountId: string) {
102 if (!accountId) {
103 throw new BadRequestException('accountId是必须的');
104 }
105
106 const result = await this.tikTokAuthService.revokeAuthorization(accountId);
107 return { success: result };
108 }
109
110 /**
111 * 获取用户视频列表
112 */
113 @Get('videos/list')
114 @ApiOperation({ summary: '获取用户视频列表' })
115 async getUserVideos(
116 @GetToken() systemToken: TokenInfo,
117 @Query() queryDto: GetVideosQueryDto
118 ) {
119 const userId = systemToken.id;
120 if (!queryDto.accountId) {
121 throw new BadRequestException('accountId是必须的');
122 }
123
124 const accessToken = await this.tikTokAuthService.getUserAccessToken(queryDto.accountId);
125 return this.tikTokService.getUserVideos(
126 accessToken,
127 userId,
128 queryDto.accountId,
129 queryDto.limit,
130 queryDto.cursor
131 );
132 }
133
134 /**
135 * 获取视频详情
136 */
137 @Get('videos/detail')
138 @ApiOperation({ summary: '获取视频详情' })
139 @ApiQuery({ name: 'videoId', description: '视频ID' })
140 @ApiQuery({ name: 'accountId', description: 'TikTok账号ID' })
141 async getVideoDetail(
142 @Query('videoId') videoId: string,
143 @Query('accountId') accountId: string
144 ) {
145 if (!videoId || !accountId) {
146 throw new BadRequestException('videoId和accountId是必须的');
147 }
148
149 const accessToken = await this.tikTokAuthService.getUserAccessToken(accountId);
150 return this.tikTokService.getVideoDetail(accessToken, videoId);
151 }
152
153 /**
154 * 上传视频
155 */
156 @Post('videos/upload')
157 @ApiOperation({ summary: '上传视频文件' })
158 @ApiConsumes('multipart/form-data')
159 @ApiBody({
160 schema: {
161 type: 'object',
162 properties: {
163 accountId: { type: 'string' },
164 file: {
165 type: 'string',
166 format: 'binary',
167 description: '视频文件'
168 }
169 },
170 required: ['accountId', 'file']
171 }
172 })
173 @UseInterceptors(FileInterceptor('file'))
174 async uploadVideo(
175 @GetToken() systemToken: TokenInfo,
176 @Body('accountId') accountId: string,
177 @UploadedFile() file: Express.Multer.File
178 ) {
179 if (!accountId || !file) {
180 throw new BadRequestException('accountId和视频文件是必须的');
181 }
182
183 const accessToken = await this.tikTokAuthService.getUserAccessToken(accountId);
184 return this.tikTokService.uploadVideo(accessToken, file.buffer);
185 }
186
187 /**
188 * 发布视频
189 */
190 @Post('videos/publish')
191 @ApiOperation({ summary: '发布视频' })
192 @ApiBody({
193 schema: {
194 type: 'object',
195 properties: {
196 accountId: { type: 'string', description: 'TikTok账号ID' },
197 description: { type: 'string', description: '视频描述' },
198 videoId: { type: 'string', description: '上传后的视频ID' },
199 private: { type: 'boolean', description: '是否为私有视频' },
200 hashtags: {
201 type: 'array',
202 items: { type: 'string' },
203 description: '话题标签列表'
204 }
205 },
206 required: ['accountId', 'description', 'videoId']
207 }
208 })
209 async publishVideo(
210 @GetToken() systemToken: TokenInfo,
211 @Body() createVideoDto: CreateVideoDto,
212 @Body('videoId') videoId: string
213 ) {
214 const userId = systemToken.id;
215 if (!createVideoDto.accountId || !videoId) {
216 throw new BadRequestException('accountId和videoId是必须的');
217 }
218
219 const accessToken = await this.tikTokAuthService.getUserAccessToken(createVideoDto.accountId);
220 return this.tikTokService.publishVideo(
221 accessToken,
222 userId,
223 createVideoDto.accountId,
224 createVideoDto,
225 videoId
226 );
227 }
228
229 /**
230 * 删除视频
231 */
232 @Post('videos/delete')
233 @ApiOperation({ summary: '删除视频' })
234 @ApiBody({
235 schema: {
236 type: 'object',
237 properties: {
238 accountId: { type: 'string', description: 'TikTok账号ID' },
239 videoId: { type: 'string', description: '视频ID' }
240 },
241 required: ['accountId', 'videoId']
242 }
243 })
244 async deleteVideo(
245 @GetToken() systemToken: TokenInfo,
246 @Body('videoId') videoId: string,
247 @Body('accountId') accountId: string
248 ) {
249 if (!videoId || !accountId) {
250 throw new BadRequestException('videoId和accountId是必须的');
251 }
252
253 const accessToken = await this.tikTokAuthService.getUserAccessToken(accountId);
254 return this.tikTokService.deleteVideo(accessToken, videoId);
255 }
256
257 /**
258 * 获取视频评论
259 */
260 @Get('videos/:videoId/comments')
261 @ApiOperation({ summary: '获取视频评论' })
262 @ApiParam({ name: 'videoId', description: '视频ID' })
263 @ApiQuery({ name: 'accountId', description: 'TikTok账号ID' })
264 @ApiQuery({ name: 'limit', description: '每页结果数', required: false })
265 @ApiQuery({ name: 'cursor', description: '分页游标', required: false })
266 async getVideoComments(
267 @Param('videoId') videoId: string,
268 @Query('accountId') accountId: string,
269 @Query('limit') limit?: number,
270 @Query('cursor') cursor?: string
271 ) {
272 if (!videoId || !accountId) {
273 throw new BadRequestException('videoId和accountId是必须的');
274 }
275
276 const accessToken = await this.tikTokAuthService.getUserAccessToken(accountId);
277 return this.tikTokService.getVideoComments(accessToken, videoId, limit, cursor);
278 }
279
280 /**
281 * 发表评论
282 */
283 @Post('videos/comment')
284 @ApiOperation({ summary: '发表评论' })
285 @ApiBody({
286 schema: {
287 type: 'object',
288 properties: {
289 accountId: { type: 'string', description: 'TikTok账号ID' },
290 videoId: { type: 'string', description: '视频ID' },
291 text: { type: 'string', description: '评论内容' }
292 },
293 required: ['accountId', 'videoId', 'text']
294 }
295 })
296 async postComment(
297 @GetToken() systemToken: TokenInfo,
298 @Body() commentDto: TikTokCommentDto
299 ) {
300 if (!commentDto.accountId || !commentDto.videoId || !commentDto.text) {
301 throw new BadRequestException('accountId, videoId和text是必须的');
302 }
303
304 const accessToken = await this.tikTokAuthService.getUserAccessToken(commentDto.accountId);
305 return this.tikTokService.postComment(accessToken, commentDto);
306 }
307
308 /**
309 * 删除评论
310 */
311 @Post('videos/comment/delete')
312 @ApiOperation({ summary: '删除评论' })
313 @ApiBody({
314 schema: {
315 type: 'object',
316 properties: {
317 accountId: { type: 'string', description: 'TikTok账号ID' },
318 videoId: { type: 'string', description: '视频ID' },
319 commentId: { type: 'string', description: '评论ID' }
320 },
321 required: ['accountId', 'videoId', 'commentId']
322 }
323 })
324 async deleteComment(
325 @GetToken() systemToken: TokenInfo,
326 @Body('accountId') accountId: string,
327 @Body('videoId') videoId: string,
328 @Body('commentId') commentId: string
329 ) {
330 if (!accountId || !videoId || !commentId) {
331 throw new BadRequestException('accountId, videoId和commentId是必须的');
332 }
333
334 const accessToken = await this.tikTokAuthService.getUserAccessToken(accountId);
335 return this.tikTokService.deleteComment(accessToken, videoId, commentId);
336 }
337
338 /**
339 * 视频点赞或取消点赞
340 */
341 @Post('videos/rate')
342 @ApiOperation({ summary: '视频点赞或取消点赞' })
343 @ApiBody({
344 schema: {
345 type: 'object',
346 properties: {
347 accountId: { type: 'string', description: 'TikTok账号ID' },
348 videoId: { type: 'string', description: '视频ID' },
349 rating: { type: 'string', description: '操作类型: like 或 unlike' }
350 },
351 required: ['accountId', 'videoId', 'rating']
352 }
353 })
354 async rateVideo(
355 @GetToken() systemToken: TokenInfo,
356 @Body('videoId') videoId: string,
357 @Body('accountId') accountId: string,
358 @Body('rating') rating: string
359 ) {
360 const userId = systemToken.id;
361 if (!videoId || !userId || !accountId) {
362 throw new BadRequestException('videoId, userId和accountId是必须的');
363 }
364
365 const accessToken = await this.tikTokAuthService.getUserAccessToken(accountId);
366
367 // 在方法开始处验证
368 if (!["like", "unlike"].includes(rating)) {
369 throw new BadRequestException('参数错误: rating必须为like或unlike');
370 }
371
372 // 后续处理
373 if (rating === "like") {
374 return this.tikTokService.likeVideo(accessToken, userId, accountId, videoId);
375 } else {
376 return this.tikTokService.unlikeVideo(accessToken, userId, accountId, videoId);
377 }
378 }
379
380 /**
381 * 搜索TikTok视频
382 */
383 @Get('search')
384 @ApiOperation({ summary: '搜索TikTok视频' })
385 async searchVideos(
386 @GetToken() systemToken: TokenInfo,
387 @Query() filterDto: TikTokVideoFilterDto
388 ) {
389 if (!filterDto.accountId) {
390 throw new BadRequestException('accountId是必须的');
391 }
392
393 const accessToken = await this.tikTokAuthService.getUserAccessToken(filterDto.accountId);
394 return this.tikTokService.searchVideos(accessToken, filterDto);
395 }
396
397 /**
398 * 初始化视频发布(新版API)
399 */
400 @Post('videos/init-publish')
401 @ApiOperation({ summary: '初始化视频发布(新版API)' })
402 @ApiBody({
403 schema: {
404 type: 'object',
405 properties: {
406 accountId: { type: 'string', description: 'TikTok账号ID' },
407 title: { type: 'string', description: '视频标题' },
408 description: { type: 'string', description: '视频描述' },
409 privacyLevel: { type: 'string', description: '隐私级别', enum: ['PUBLIC', 'PRIVATE', 'FRIENDS'], default: 'PUBLIC' },
410 disableComment: { type: 'boolean', description: '是否禁用评论', default: false },
411 disableDuet: { type: 'boolean', description: '是否禁用二重奏', default: false },
412 disableStitch: { type: 'boolean', description: '是否禁用Stitch', default: false },
413 videoCoverTimestampMs: { type: 'number', description: '视频封面时间点(毫秒)' },
414 hashtags: { type: 'array', items: { type: 'string' }, description: '话题标签列表' },
415 videoSize: { type: 'number', description: '视频大小(字节)' }
416 },
417 required: ['accountId', 'videoSize']
418 }
419 })
420 async initVideoPublish(
421 @GetToken() systemToken: TokenInfo,
422 @Body('accountId') accountId: string,
423 @Body('videoSize') videoSize: number,
424 @Body() videoInfo: {
425 title?: string;
426 description?: string;
427 privacyLevel?: string;
428 disableComment?: boolean;
429 disableDuet?: boolean;
430 disableStitch?: boolean;
431 videoCoverTimestampMs?: number;
432 hashtags?: string[];
433 }
434 ) {
435 if (!accountId || !videoSize) {
436 throw new BadRequestException('accountId和视频大小是必须的');
437 }
438
439 const accessToken = await this.tikTokAuthService.getUserAccessToken(accountId);
440 return this.tikTokService.initVideoPublish(accessToken, videoSize, videoInfo);
441 }
442
443 /**
444 * 检查视频发布状态(新版API)
445 */
446 @Post('videos/publish/status')
447 @ApiOperation({ summary: '检查视频发布状态' })
448 @ApiBody({
449 schema: {
450 type: 'object',
451 properties: {
452 accountId: { type: 'string', description: 'TikTok账号ID' },
453 publishId: { type: 'string', description: '发布ID' }
454 },
455 required: ['accountId', 'publishId']
456 }
457 })
458 async checkPublishStatus(
459 @GetToken() systemToken: TokenInfo,
460 @Body('accountId') accountId: string,
461 @Body('publishId') publishId: string
462 ) {
463 if (!accountId || !publishId) {
464 throw new BadRequestException('accountId和publishId是必须的');
465 }
466
467 const accessToken = await this.tikTokAuthService.getUserAccessToken(accountId);
468 return this.tikTokService.checkPublishStatus(accessToken, publishId);
469 }
470
471 /**
472 * 一键上传并发布视频(新版API三步法)
473 */
474 @Post('videos/publish')
475 @ApiOperation({ summary: '一键上传并发布视频(新版API)' })
476 @ApiConsumes('multipart/form-data')
477 @ApiBody({
478 schema: {
479 type: 'object',
480 properties: {
481 accountId: { type: 'string', description: 'TikTok账号ID' },
482 title: { type: 'string', description: '视频标题' },
483 description: { type: 'string', description: '视频描述' },
484 privacyStatus: { type: 'string', description: '隐私级别', enum: ['PUBLIC', 'PRIVATE', 'FRIENDS'], default: 'PUBLIC' },
485 disableComment: { type: 'boolean', description: '是否禁用评论', default: false },
486 disableDuet: { type: 'boolean', description: '是否禁用二重奏', default: false },
487 disableStitch: { type: 'boolean', description: '是否禁用Stitch', default: false },
488 videoCoverTimestampMs: { type: 'number', description: '视频封面时间点(毫秒)' },
489 hashtags: { type: 'array', items: { type: 'string' }, description: '话题标签列表' },
490 pollInterval: { type: 'number', description: '轮询间隔(毫秒)', default: 2000 },
491 maxRetries: { type: 'number', description: '最大重试次数', default: 30 },
492 file: { type: 'string', format: 'binary', description: '视频文件' }
493 },
494 required: ['accountId', 'file']
495 }
496 })
497 @UseInterceptors(FileInterceptor('file'))
498 async uploadAndPublishVideo(
499 @GetToken() systemToken: TokenInfo,
500 @Body() uploadDto: CombinedVideoUploadDto,
501 @UploadedFile() file: Express.Multer.File
502 ) {
503 const userId = systemToken.id;
504 if (!uploadDto.accountId || !file) {
505 throw new BadRequestException('accountId和视频文件是必须的');
506 }
507
508 const accessToken = await this.tikTokAuthService.getUserAccessToken(uploadDto.accountId);
509 return this.tikTokService.uploadAndPublishVideo(
510 accessToken,
511 userId,
512 uploadDto.accountId,
513 file.buffer,
514 {
515 title: uploadDto.title,
516 description: uploadDto.description,
517 privacyStatus: uploadDto.privacyStatus || 'PUBLIC',
518 disableComment: uploadDto.disableComment || false,
519 disableDuet: uploadDto.disableDuet || false,
520 disableStitch: uploadDto.disableStitch || false,
521 videoCoverTimestampMs: uploadDto.videoCoverTimestampMs,
522 tags: uploadDto.tags
523 },
524 uploadDto.pollInterval,
525 uploadDto.maxRetries
526 );
527 }
528
529 /**
530 * 获取TikTok账号信息
531 */
532 @Get('user/profile')
533 @ApiOperation({ summary: '获取TikTok账号信息' })
534 @ApiQuery({ name: 'accountId', description: 'TikTok账号ID' })
535 async getUserProfile(
536 @GetToken() systemToken: TokenInfo,
537 @Query('accountId') accountId: string
538 ) {
539 if (!accountId) {
540 throw new BadRequestException('accountId是必须的');
541 }
542
543 const accessToken = await this.tikTokAuthService.getUserAccessToken(accountId);
544 return this.tikTokService.getUserProfile(accessToken, accountId);
545 }
546
547 }
547 lines TYPESCRIPT