返回 AiToEarn
publish-record.service.ts
根目录 / project / aitoearn-backend / apps / aitoearn-server / src / core / publish-record / publish-record.service.ts
1 /*
2 * @Author: nevin
3 * @Date: 2024-06-17 19:19:15
4 * @LastEditTime: 2024-09-05 15:19:25
5 * @LastEditors: nevin
6 * @Description: PublishRecord
7 */
8 import type { EventEnvelope } from '@yikart/redis'
9 import { Injectable, Logger, OnModuleInit } from '@nestjs/common'
10 import { AccountType, TableDto, WorkStatus } from '@yikart/common'
11 import { PublishRecord, PublishRecordLinkStatus, PublishRecordRepository, PublishRecordSource, PublishStatus, PublishType } from '@yikart/mongodb'
12 import { EventStream, EventTopic, OnEventStream } from '@yikart/redis'
13 import { UpdateQuery } from 'mongoose'
14 import { MaterialService } from '../content/material.service'
15 import {
16 GetPublishRecordDetailDto,
17 PublishDayInfoListFiltersDto,
18 PublishRecordListFilterDto,
19 } from './publish-record.dto'
20
21 @Injectable()
22 export class PublishRecordService implements OnModuleInit {
23 private readonly logger = new Logger(PublishRecordService.name)
24 constructor(
25 private readonly publishRecordRepository: PublishRecordRepository,
26 private readonly materialService: MaterialService,
27 ) { }
28
29 async onModuleInit() {
30 // const data = {
31 // "id": "69aa26a88d4edb16c4642293",
32 // "userId": "69908f81a8660002fc02522b",
33 // "taskId": "69aa26778d4edb16c464227a",
34 // "type": PublishType.VIDEO,
35 // "title": "",
36 // "desc": "",
37 // "topics": [],
38 // "accountType": AccountType.TikTok,
39 // "imgUrlList": [],
40 // "publishTime": new Date("2026-03-06T00:58:16.583Z"),
41 // "status": 1,
42 // "inQueue": false,
43 // "queued": false,
44 // "dataId": "7610426383415774472",
45 // "uniqueId": "tiktok_7610426383415774472",
46 // "workLink": "https://www.tiktok.com/@arianalee0928/video/7610426383415774472",
47 // "createdAt": new Date("2026-03-06T00:58:16.587Z"),
48 // "updatedAt": new Date("2026-03-06T00:58:16.587Z")
49 // }
50 // await this.onPublishCompleted(data)
51 }
52
53 /**
54 * 更新素材使用次数
55 * @param materialId 素材ID
56 */
57 private async increaseMaterialUseCount(materialId?: string) {
58 if (!materialId)
59 return
60 try {
61 await this.materialService.addUseCount(materialId)
62 }
63 catch (error) {
64 this.logger.error(`Failed to increase material use count for ${materialId}: ${error}`)
65 }
66 }
67
68 private resolveSource(data: Partial<PublishRecord>) {
69 if (data.source) {
70 return data.source
71 }
72 return undefined
73 }
74
75 /**
76 * 创建发布记录,若状态为已发布则触发发布完成副作用
77 * @param data 发布记录数据
78 * @returns 创建后的发布记录
79 */
80 async createPublishRecord(data: Partial<PublishRecord>) {
81 const res = await this.publishRecordRepository.create({
82 ...data,
83 source: this.resolveSource(data),
84 })
85 return res
86 }
87
88 /**
89 * 发布完成后的副作用:任务处理、每日统计、Redis 事件
90 * @param data 已完成的发布记录
91 */
92 @OnEventStream(EventTopic.ChannelsPublishTaskPublished, {
93 streams: [EventStream.Channels],
94 })
95 private async handlePublishCompletedEvent(envelope: EventEnvelope<EventTopic.ChannelsPublishTaskPublished>) {
96 const data = envelope.payload
97 await this.onPublishCompleted({
98 id: data.publishRecordId ?? data.taskId,
99 userId: data.userId,
100 accountId: data.accountId,
101 accountType: data.accountType,
102 uid: data.uid,
103 materialId: data.materialId,
104 dataId: data.dataId ?? data.platformWorkId,
105 })
106 }
107
108 private async onPublishCompleted(data: Partial<PublishRecord>) {
109 const tasks = [
110 this.increaseMaterialUseCount(data.materialId),
111 ]
112 if (data.userId)
113 tasks.push(this.upDayPublishInfo({ userId: data.userId }))
114
115 await Promise.all(tasks)
116 }
117
118 /**
119 * 更新每日发布统计信息
120 * @param data 发布记录数据
121 */
122 private async upDayPublishInfo(data: Pick<PublishRecord, 'userId'>) {
123 try {
124 await this.publishRecordRepository.upDayPublishInfo(data)
125 }
126 catch (error) {
127 this.logger.error(error, `Failed to update publish day info for user ${data.userId}`)
128 }
129 }
130
131 /**
132 * 获取发布记录列表
133 * @param query 查询条件
134 * @returns 发布记录列表
135 */
136 async getPublishRecordList(query: PublishRecordListFilterDto): Promise<PublishRecord[]> {
137 const res = await this.publishRecordRepository.getPublishRecordList(query)
138 return res
139 }
140
141 /**
142 * 获取排队中的发布记录列表
143 * @param query 查询条件
144 * @returns 排队中的发布记录列表
145 */
146 async getQueuedPublishRecords(query: PublishRecordListFilterDto): Promise<PublishRecord[]> {
147 const res = await this.publishRecordRepository.getQueuedPublishRecords(query)
148 return res
149 }
150
151 /**
152 * 获取发布记录详细信息
153 * @param id 发布记录ID
154 * @returns 发布记录信息
155 */
156 async getPublishRecordInfo(id: string) {
157 return this.publishRecordRepository.getPublishRecordInfo(id)
158 }
159
160 /**
161 * 根据ID删除发布记录
162 * @param id 发布记录ID
163 * @returns 是否删除成功
164 */
165 async deletePublishRecordById(id: string): Promise<boolean> {
166 const res = await this.publishRecordRepository.deletePublishRecordById(id)
167 return res
168 }
169
170 /**
171 * 更新发布记录
172 * @param filter 查询过滤条件
173 * @param data 要更新的字段
174 * @returns 更新结果
175 */
176 async updatePublishRecord(
177 filter: any,
178 data: Partial<PublishRecord>,
179 ) {
180 const res = await this.publishRecordRepository.updatePublishRecord(filter, data)
181 return res
182 }
183
184 /**
185 * 获取每日发布信息列表(分页)
186 * @param inFilter 过滤条件
187 * @param pageInfo 分页参数
188 * @returns 每日发布信息列表
189 */
190 async getPublishDayInfoList(
191 inFilter: PublishDayInfoListFiltersDto,
192 pageInfo: TableDto,
193 ) {
194 return this.publishRecordRepository.getPublishDayInfoList(
195 inFilter,
196 pageInfo,
197 )
198 }
199
200 /**
201 * 获取用户的发布信息概览数据
202 * @param userId 用户ID
203 * @returns 发布信息数据
204 */
205 async getPublishInfoData(userId: string) {
206 const res = await this.publishRecordRepository.getPublishInfoData(userId)
207 return res
208 }
209
210 /**
211 * 根据平台类型和作品ID获取发布记录
212 * @param accountType 平台类型
213 * @param dataId 作品ID
214 * @returns 发布记录
215 */
216 async getPublishRecordByDataId(accountType: AccountType, dataId: string) {
217 const res = await this.publishRecordRepository.getPublishRecordByDataId(accountType, dataId)
218 return res
219 }
220
221 /**
222 * 获取发布记录详情(根据流水ID和用户ID)
223 * @param data 包含流水ID和用户ID的查询条件
224 * @returns 发布记录详情
225 */
226 async getPublishRecordDetail(data: GetPublishRecordDetailDto) {
227 const publishRecord = await this.publishRecordRepository.getPublishRecordDetail({
228 flowId: data.flowId,
229 userId: data.userId,
230 })
231 return publishRecord
232 }
233
234 /**
235 * 根据任务ID和用户ID获取发布记录
236 * @param taskId 任务ID
237 * @param userId 用户ID
238 * @returns 发布记录
239 */
240 async getPublishRecordByTaskId(taskId: string, userId: string) {
241 const res = await this.publishRecordRepository.getPublishRecordByTaskId(taskId, userId)
242 return res
243 }
244
245 async listPublishedByTaskId(
246 taskId: string,
247 query?: {
248 accountType?: AccountType
249 },
250 ) {
251 return this.publishRecordRepository.listPublishedByTaskId(taskId, query)
252 }
253
254 async getPublishedByTaskIdAndDataId(taskId: string, dataId: string) {
255 return this.publishRecordRepository.getPublishedByTaskIdAndDataId(taskId, dataId)
256 }
257
258 async listPublishedByTaskIdAndDataId(taskId: string, dataId: string) {
259 return this.publishRecordRepository.listPublishedByTaskIdAndDataId(taskId, dataId)
260 }
261
262 /**
263 * 根据用户UID和作品ID获取发布记录
264 * @param uid 用户UID
265 * @param dataId 作品ID
266 * @returns 发布记录
267 */
268 async getPublishRecordByDataIdAndUid(uid: string, dataId: string) {
269 const res = await this.publishRecordRepository.getPublishRecordByDataIdAndUid(uid, dataId)
270 return res
271 }
272
273 /**
274 * 完成发布记录:根据作品ID和UID更新发布状态为已完成,并触发任务处理
275 * @param filter 查询条件(作品ID和UID)
276 * @param data 附加数据(作品链接、扩展数据)
277 * @returns 是否完成成功
278 */
279 async donePublishRecord(
280 filter: { dataId: string, uid: string },
281 data: {
282 workLink?: string
283 dataOption?: unknown
284 },
285 ): Promise<boolean> {
286 const res = await this.publishRecordRepository.donePublishRecord(filter, data)
287 if (!res)
288 return false
289 return !!res
290 }
291
292 /**
293 * 发布平台回调失败:根据作品ID和UID更新发布状态为失败,并记录失败原因
294 * @param filter 查询条件(作品ID和UID)
295 * @param errorMsg 失败原因
296 * @returns 是否更新成功
297 */
298 async failPublishRecordByData(
299 filter: { dataId: string, uid: string },
300 errorMsg: string,
301 ): Promise<boolean> {
302 const res = await this.publishRecordRepository.failPublishRecordByData(filter, errorMsg)
303 return !!res
304 }
305
306 /**
307 * 根据草稿箱ID获取发布记录列表(分页)
308 * @param materialGroupId 草稿箱ID
309 * @param query 查询条件(状态、平台类型、分页)
310 * @returns 发布记录列表和总数
311 */
312 async getPublishRecordListByMaterialGroupId(
313 materialGroupId: string,
314 query?: {
315 status?: number
316 accountType?: AccountType
317 pageNo?: number
318 pageSize?: number
319 },
320 ) {
321 return this.publishRecordRepository.getPublishRecordListByMaterialGroupId(materialGroupId, query)
322 }
323
324 // ===== 以下为发布流程代理方法 =====
325 /**
326 * 根据ID获取发布记录
327 * @param id 发布记录ID
328 * @returns 发布记录
329 */
330 async getById(id: string) {
331 return this.publishRecordRepository.getById(id)
332 }
333
334 /**
335 * 根据ID更新发布记录
336 * @param id 发布记录ID
337 * @param update MongoDB更新查询
338 * @returns 更新结果
339 */
340 async updateById(id: string, update: UpdateQuery<PublishRecord>) {
341 return this.publishRecordRepository.updateById(id, update)
342 }
343
344 /**
345 * 根据发布记录ID更新作品链接及其派生字段
346 * @param id 发布记录ID
347 * @param data 作品链接更新数据
348 * @param data.workLink 作品链接
349 * @param data.dataId 作品ID
350 * @param data.uniqueId 作品唯一标识
351 * @param data.platformWorkId 平台作品ID
352 * @param data.linkStatus 作品链接状态
353 * @param data.linkError 作品链接获取错误
354 * @param data.linkMeta 作品链接扩展信息
355 * @param data.type 作品类型
356 * @returns 更新后的发布记录
357 * @param data.originalWorkLink Original submitted work link
358 * @param data.workStatus Work status
359 */
360 async updateWorkLinkById(
361 id: string,
362 data: {
363 workLink?: string
364 originalWorkLink?: string | null
365 dataId?: string
366 uniqueId?: string
367 platformWorkId?: string
368 workStatus?: WorkStatus | null
369 linkStatus: PublishRecordLinkStatus
370 linkError?: string
371 linkMeta?: Record<string, unknown>
372 type?: PublishType
373 },
374 ) {
375 const $set: Partial<PublishRecord> = {
376 linkStatus: data.linkStatus,
377 linkError: data.linkError || '',
378 ...(data.workLink && { workLink: data.workLink }),
379 ...(data.dataId && { dataId: data.dataId }),
380 ...(data.uniqueId && { uniqueId: data.uniqueId }),
381 ...(data.platformWorkId && { platformWorkId: data.platformWorkId }),
382 ...(data.linkMeta !== undefined && { linkMeta: data.linkMeta }),
383 ...(data.type && { type: data.type }),
384 }
385 const $unset: Record<string, 1> = {}
386
387 if ('originalWorkLink' in data) {
388 if (data.originalWorkLink) {
389 $set.originalWorkLink = data.originalWorkLink
390 }
391 else {
392 $unset['originalWorkLink'] = 1
393 }
394 }
395
396 if ('workStatus' in data) {
397 if (data.workStatus) {
398 $set.workStatus = data.workStatus
399 }
400 else {
401 $unset['workStatus'] = 1
402 }
403 }
404
405 return this.publishRecordRepository.updateById(id, {
406 $set,
407 ...(Object.keys($unset).length && { $unset }),
408 })
409 }
410
411 /**
412 * 根据流水ID获取发布记录
413 * @param flowId 流水ID
414 * @returns 发布记录
415 */
416 async getByFlowId(flowId: string) {
417 return this.publishRecordRepository.findOneByFlowId(flowId)
418 }
419
420 /**
421 * 根据ID获取单条发布记录
422 * @param id 发布记录ID
423 * @returns 发布记录
424 */
425 async getOneById(id: string) {
426 return this.publishRecordRepository.findOneById(id)
427 }
428
429 /**
430 /**
431 * 根据作品ID和账户类型获取单条发布记录
432 * @param dataId 作品ID
433 * @param accountType 账户类型
434 * @returns 发布记录
435 */
436 async getOneByDataId(dataId: string, accountType: AccountType) {
437 return this.publishRecordRepository.findOneByDataId(dataId, accountType)
438 }
439
440 /**
441 * 根据作品ID和UID获取单条发布记录
442 * @param dataId 作品ID
443 * @param uid 用户UID
444 * @returns 发布记录
445 */
446 async getOneByData(dataId: string, uid: string) {
447 return this.publishRecordRepository.findOneByData(dataId, uid)
448 }
449
450 /**
451 * 根据ID完成发布记录:更新状态为已发布
452 * @param id 发布记录ID
453 * @param dataId 平台返回的作品ID
454 * @param data 附加数据(作品链接、扩展数据)
455 * @returns 更新结果
456 */
457 async completeById(data: PublishRecord, dataId: string, newData?: { workLink: string, dataOption?: Record<string, any> }) {
458 await this.onPublishCompleted(data)
459 return this.publishRecordRepository.complete(data.id, dataId, newData)
460 }
461
462 /**
463 * 根据ID标记发布记录为失败状态
464 * @param id 发布记录ID
465 * @param errMsg 错误信息
466 * @returns 更新结果
467 */
468 async failById(id: string, errMsg: string) {
469 return this.publishRecordRepository.fail(id, errMsg)
470 }
471
472 /**
473 * 根据ID更新发布记录状态
474 * @param id 发布记录ID
475 * @param status 目标状态
476 * @param msg 附加消息(可选)
477 * @returns 更新结果
478 */
479 async updateStatusById(id: string, status: PublishStatus, msg?: string) {
480 return this.publishRecordRepository.updateStatus(id, status, msg)
481 }
482
483 /**
484 * 更新发布记录的队列ID
485 * @param id 发布记录ID
486 * @param queueId 队列ID
487 * @param queued 是否已入队(可选)
488 * @returns 更新结果
489 */
490 async updateQueueId(id: string, queueId: string, queued?: boolean) {
491 return this.publishRecordRepository.updateQueueId(id, queueId, queued)
492 }
493
494 /**
495 * 根据ID删除发布记录(发布流程专用)
496 * @param id 发布记录ID
497 * @returns 删除结果
498 */
499 async deleteById(id: string) {
500 return this.publishRecordRepository.delById(id)
501 }
502
503 /**
504 * 获取指定时间之前的发布任务列表
505 * @param end 截止时间
506 * @returns 发布任务列表
507 */
508 async listByTime(end: Date) {
509 return this.publishRecordRepository.getPublishTaskListByTime(end)
510 }
511
512 /**
513 * 获取长时间停留在发布中的任务列表
514 * @param cutoffTime 超时截止时间
515 * @param limit 单批处理数量
516 * @returns 发布中超时任务列表
517 */
518 async listStalePublishingTasks(cutoffTime: Date, limit: number) {
519 return this.publishRecordRepository.getStalePublishingTasks(cutoffTime, limit)
520 }
521
522 /**
523 * 获取发布任务列表(支持多条件查询)
524 * @param query 查询条件(用户ID、流水ID、账户ID、平台类型、状态、类型、时间范围、UID)
525 * @returns 发布任务列表
526 */
527 async listPublishTasks(query: {
528 userId: string
529 flowId?: string
530 accountId?: string
531 accountType?: AccountType
532 status?: PublishStatus
533 type?: PublishType
534 time?: [Date?, Date?, ...unknown[]]
535 uid?: string
536 }) {
537 return this.publishRecordRepository.listByFilter(query)
538 }
539
540 /**
541 * 获取排队中的发布任务列表
542 * @param query 查询条件(用户ID、账户ID、平台类型、时间范围)
543 * @returns 排队中的发布任务列表
544 */
545 async listQueuedPublishTasks(query: {
546 userId: string
547 accountId?: string
548 accountType?: AccountType
549 time?: [Date?, Date?, ...unknown[]]
550 }) {
551 return this.publishRecordRepository.listQueuedByFilter(query)
552 }
553
554 /**
555 * 获取已发布的发布任务列表
556 * @param query 查询条件(用户ID、账户ID、平台类型、时间范围)
557 * @returns 已发布的任务列表
558 */
559 async listPublishedPublishTasks(query: {
560 userId: string
561 accountId?: string
562 accountType?: AccountType
563 time?: [Date?, Date?, ...unknown[]]
564 }) {
565 return this.publishRecordRepository.listPublishedByFilter(query)
566 }
567
568 async listPublishedTaskLinkRecords(query: {
569 userId: string
570 accountId?: string
571 accountType?: AccountType
572 flowId?: string
573 time?: [Date?, Date?, ...unknown[]]
574 }) {
575 return this.publishRecordRepository.listPublishedTaskLinkRecords(query)
576 }
577
578 /**
579 * 根据流水ID获取发布任务列表
580 * @param flowId 流水ID
581 * @returns 发布任务列表
582 */
583 async listByFlowId(flowId: string) {
584 return this.publishRecordRepository.listByFlowId(flowId)
585 }
586
587 /**
588 * 更新发布任务状态(含错误信息、发布时间、队列状态等)
589 * @param id 任务ID
590 * @param newData 要更新的状态数据
591 * @returns 更新结果
592 */
593 async updateTaskStatus(
594 id: string,
595 newData: {
596 errorMsg?: string
597 errorData?: {
598 type: string
599 code: string
600 message: string
601 originalData?: Record<string, unknown>
602 }
603 status: PublishStatus
604 publishTime?: Date
605 queued?: boolean
606 inQueue?: boolean
607 },
608 ) {
609 return this.publishRecordRepository.updatePublishTaskStatus(id, newData)
610 }
611
612 /**
613 * 将发布记录更新为发布中状态
614 * @param id 发布记录ID
615 * @param dataId 平台返回的作品ID
616 * @param workLink 作品链接(可选)
617 * @returns 更新结果
618 */
619 async updateAsPublishing(id: string, dataId: string, workLink?: string) {
620 return this.publishRecordRepository.updateAsPublishing(id, dataId, workLink)
621 }
622
623 /**
624 * 根据流水ID和用户ID获取发布任务详情
625 * @param flowId 流水ID
626 * @param userId 用户ID
627 * @returns 发布任务详情
628 */
629 async getTaskInfoWithFlowId(flowId: string, userId: string) {
630 return this.publishRecordRepository.getByFlowIdAndUserId(flowId, userId)
631 }
632
633 /**
634 * 根据任务ID和用户ID获取发布任务详情
635 * @param id 任务ID
636 * @param userId 用户ID
637 * @returns 发布任务详情
638 */
639 async getTaskInfoWithUserId(id: string, userId: string) {
640 return this.publishRecordRepository.getByIdAndUserId(id, userId)
641 }
642
643 /**
644 * 根据草稿箱ID获取已发布的记录列表(分页)
645 * @param materialGroupId 草稿箱ID
646 * @param query 查询条件(平台类型、分页)
647 * @returns 已发布记录列表和总数
648 */
649 async listPublishedByMaterialGroupIdWithPagination(
650 materialGroupId: string,
651 query?: {
652 accountType?: AccountType
653 source?: PublishRecordSource
654 pageNo?: number
655 pageSize?: number
656 },
657 ) {
658 return this.publishRecordRepository.listPublishedByMaterialGroupIdWithPagination(materialGroupId, query)
659 }
660
661 async listPublishedByMaterialGroupId(
662 materialGroupId: string,
663 query?: {
664 accountType?: AccountType
665 source?: PublishRecordSource
666 },
667 ) {
668 return this.publishRecordRepository.listPublishedByMaterialGroupId(materialGroupId, query)
669 }
670 }
671
671 lines TYPESCRIPT