import Realm from 'realm' import { RealmBaseService } from '../base/realmBase' import { cloneDeep, isEmpty } from 'lodash' import { Book } from '@/define/model/book/book' import { BookBackTaskStatus, BookBackTaskType, TaskExecuteType } from '@/define/enum/bookEnum' import { OtherData } from '@/define/enum/softwareEnum' import { TaskModal } from '@/define/model/task' export class TaskListService extends RealmBaseService { static instance: TaskListService | null = null declare realm: Realm private constructor() { super() } /** * 获取当前实例对象,为空则创建一个新的 * @returns */ public static async getInstance() { if (TaskListService.instance === null) { TaskListService.instance = new TaskListService() await super.getInstance() } await TaskListService.instance.open() return TaskListService.instance } /** * 获取任务集合 * * 该方法根据提供的查询条件从数据库中检索任务列表,支持多种筛选条件和分页功能。 * 可通过小说名称、任务名称、状态等多个维度进行筛选,并为每个任务补充关联的小说 * 和任务信息。结果按创建时间降序排列,并根据分页参数返回指定范围的数据。 * * 支持的筛选条件包括: * - 小说名称(模糊匹配) * - 小说任务名称(模糊匹配) * - 小说任务详情名称(模糊匹配) * - 任务名称(模糊匹配) * - 任务类型(精确匹配) * - 任务状态(精确匹配) * - 任务错误信息(模糊匹配) * * @param {TaskModal.QueryTaskCondition} queryTaskCondition - 查询条件对象,包含筛选和分页参数 * @returns {TaskModal.TaskCollection} 包含分页信息和任务数据的集合对象 * * @example * // 查询与"小说项目"相关的所有失败任务,第1页,每页10条 * const condition = { * bookName: "小说项目", * taskStatus: BookBackTaskStatus.FAIL, * page: 1, * pageSize: 10 * }; * const taskCollection = taskListService.GetTaskCollection(condition); * console.log(`共找到${taskCollection.count}条记录,当前显示${taskCollection.data.length}条`); */ GetTaskCollection(queryTaskCondition: TaskModal.QueryTaskCondition): TaskModal.TaskCollection { let tasks = this.realm.objects('TaskList') if (!isEmpty(queryTaskCondition.bookName)) { let book = this.realm .objects('Book') .filtered('name CONTAINS[c] $0', queryTaskCondition.bookName) let ids = [] as string[] if (book.length > 0) { ids = book.map((item) => { return item.id as string }) } tasks = tasks.filtered('bookId in $0', ids) } if (!isEmpty(queryTaskCondition.bookTaskName)) { let bookTask = this.realm .objects('BookTask') .filtered('name CONTAINS[c] $0', queryTaskCondition.bookTaskName) let ids = [] as string[] if (bookTask.length > 0) { ids = bookTask.map((item) => { return item.id as string }) } tasks = tasks.filtered('bookTaskId in $0', ids) } if (!isEmpty(queryTaskCondition.bookTaskDetailName)) { let bookTaskDetail = this.realm .objects('BookTaskDetail') .filtered('name CONTAINS[c] $0', queryTaskCondition.bookTaskDetailName) let ids = [] as string[] if (bookTaskDetail.length > 0) { ids = bookTaskDetail.map((item) => { return item.id as string }) } tasks = tasks.filtered('bookTaskDetailId in $0', ids) } if (!isEmpty(queryTaskCondition.taskName)) { tasks = tasks.filtered('name CONTAINS[c] $0', queryTaskCondition.taskName) } if (!isEmpty(queryTaskCondition.taskType)) { tasks = tasks.filtered('type == $0', queryTaskCondition.taskType) } if (!isEmpty(queryTaskCondition.taskStatus)) { tasks = tasks.filtered('status == $0', queryTaskCondition.taskStatus) } if (!isEmpty(queryTaskCondition.taskErrorMessage)) { tasks = tasks.filtered('errorMessage CONTAINS[c] $0', queryTaskCondition.taskErrorMessage) } let count = tasks.length tasks = tasks.sorted('createTime', true) let task = tasks.slice( (queryTaskCondition.page - 1) * queryTaskCondition.pageSize, queryTaskCondition.page * queryTaskCondition.pageSize ) as TaskModal.BackTaskCollection[] let taskList = Array.from(task).map((item) => { let resObj = { ...item } as TaskModal.BackTaskCollection return cloneDeep(resObj) }) for (let i = 0; i < taskList.length; i++) { const element = taskList[i] let book = this.realm.objectForPrimaryKey('Book', element.bookId) if (book) { element.bookName = book.name as string } let bookTask = this.realm.objectForPrimaryKey('BookTask', element.bookTaskId) if (bookTask) { element.bookTaskName = bookTask.name as string } let bookTaskDetail = this.realm.objectForPrimaryKey( 'BookTaskDetail', element.bookTaskDetailId ) if (bookTaskDetail) { element.bookTaskDetailName = bookTaskDetail.name as string } } return { page: queryTaskCondition.page, pageSize: queryTaskCondition.pageSize, count: count, data: taskList } } /** * 获取等待状态的任务和返回指定数量的数据 * @param executeType 任务的执行类型 * @param count 返回数据的数量 */ GetWaitTaskAndSlice(executeType: TaskExecuteType, count: number) { try { let tasks = this.realm .objects('TaskList') .filtered( '(status == $0 || status == $1) && executeType == $2', BookBackTaskStatus.WAIT, BookBackTaskStatus.RECONNECT, executeType ? executeType : TaskExecuteType.AUTO ) .sorted('createTime', false) let tasksArray = Array.from(tasks) if (count != null) { tasksArray = tasksArray.slice(0, count) } let res = tasksArray.map((item) => { let resObj = { ...item } return resObj }) return res } catch (error) { throw error } } /** * 获取指定状态的任务数量 * * 该方法查询数据库中符合指定状态集合的任务总数,用于统计分析不同状态的任务分布。 * 使用Realm的IN操作符高效查询匹配多个状态条件的任务。 * * @param {BookBackTaskStatus[]} status - 要统计的任务状态数组,如[WAIT, RUNNING] * @returns {number} 符合指定状态的任务总数 * * @example * // 获取所有等待中的任务数量 * const waitingCount = taskListService.GetAssignStatusTaskCount([BookBackTaskStatus.WAIT]); * * // 获取所有正在运行或重连的任务数量 * const activeCount = taskListService.GetAssignStatusTaskCount([ * BookBackTaskStatus.RUNNING, * BookBackTaskStatus.RECONNECT * ]); */ GetAssignStatusTaskCount(status: BookBackTaskStatus[]): number { let taskLength = this.realm .objects('TaskList') .filtered('status IN $0', status).length return taskLength } /** * 新增一个小说相关的后台任务队列 * @param bookBackTask 要添加的小说数据 */ AddOneTask( bookId: string, taskType: BookBackTaskType, executeType: TaskExecuteType = TaskExecuteType.AUTO, bookTaskId: string | undefined = undefined, bookTaskDetailId: string | undefined = undefined, responseMessageName?: string ): TaskModal.Task { try { // 通过bookid获取book信息 let book = this.realm.objectForPrimaryKey('Book', bookId) if (book == null) { throw new Error('新增后台队列任务到数据库失败,没有找到对应的小说') } let bookTask if (bookTaskId) { bookTask = this.realm.objectForPrimaryKey('BookTask', bookTaskId) if (bookTask == null) { throw new Error('新增后台队列任务到数据库失败,没有找到对应的小说批次任务') } } let bookTaskDetail if (bookTaskDetailId) { bookTaskDetail = this.realm.objectForPrimaryKey('BookTaskDetail', bookTaskDetailId) if (bookTaskDetail == null) { throw new Error( '新增后台队列任务到数据库失败,没有找到对应的小说批次任务详情(分镜数据)' ) } } // 开始往数据库中写数据 let name = `${book.name}-${bookTask ? bookTask.name : 'default'}-${ bookTaskDetail ? bookTaskDetail.name : 'default' }-${taskType}` let bookBackTask = { id: crypto.randomUUID(), bookId: bookId, bookTaskId: bookTaskId ? bookTaskId : OtherData.DEFAULT, bookTaskDetailId: bookTaskDetailId ? bookTaskDetailId : OtherData.DEFAULT, name: name, type: taskType, executeType: executeType, status: BookBackTaskStatus.WAIT, createTime: new Date(), updateTime: new Date(), messageName: responseMessageName, startTime: 0, endTime: 0 } as TaskModal.Task this.realm.write(() => { this.realm.create('TaskList', bookBackTask) }) return bookBackTask } catch (error) { throw error } } /** * 修改一个小说相关的后台任务队列中的详细信息 * (对于后台的队列任务只能修改状态)和错误信息 * @param bookBackTask 修改的数据 */ UpdateTaskStatus(bookBackTask: Book.UpdateBookTaskListStatus): Book.UpdateBookTaskListStatus { try { // 判断数据是不是存在 if (isEmpty(bookBackTask.id) || isEmpty(bookBackTask.status)) { throw new Error('修改后台队列任务失败,数据不完整,缺少必要字段') } // 开始修改 this.transaction(() => { // 获取指定ID的队列任务 let _bookBackTask = this.realm.objectForPrimaryKey( 'TaskList', bookBackTask.id ) as TaskModal.Task // 判断数据是不是存在 if (_bookBackTask == null) { throw new Error('修改后台队列任务失败,数据不存在') } // 修改数据 _bookBackTask.status = bookBackTask.status if (bookBackTask.errorMessage) { _bookBackTask.errorMessage = bookBackTask.errorMessage } // 根据状态修改结束和完成时间 if (bookBackTask.status == BookBackTaskStatus.RUNNING) { _bookBackTask.startTime = new Date().getTime() } else if ( bookBackTask.status == BookBackTaskStatus.DONE || bookBackTask.status == BookBackTaskStatus.FAIL || bookBackTask.status == BookBackTaskStatus.PAUSE ) { _bookBackTask.endTime = new Date().getTime() } else if (bookBackTask.status == BookBackTaskStatus.RECONNECT) { _bookBackTask.startTime = 0 _bookBackTask.endTime = 0 } }) return bookBackTask } catch (error) { throw error } } /** * 删除满足条件的数据,包含 id、bookId、bookTaskId * 上面的条件,至少要有一个 * @param bookBackTask 删除的数据 */ async DeleteBookBackTask(bookBackTask) { try { if ( !bookBackTask.hasOwnProperty('id') && !bookBackTask.hasOwnProperty('bookId') && !bookBackTask.hasOwnProperty('bookTaskId') ) { throw new Error('删除后台队列任务失败,缺少必要的删除条件') } this.transaction(() => { // 构建查询条件 const tasksToDelete = this.realm.objects('TaskList').filtered('id == $0', bookBackTask.id) if (bookBackTask.bookId) { tasksToDelete.filtered('bookId == $0', bookBackTask.bookId) } if (bookBackTask.bookTaskId) { tasksToDelete.filtered('bookTaskId == $0', bookBackTask.bookTaskId) } this.realm.delete(tasksToDelete) return bookBackTask }) } catch (error) { throw error } } /** * 设置指定消息名称的任务为失败,并设置错误消息 * @param messageName * @param errorMessage */ async SetMessageNameTaskToFail(messageName: string, errorMessage: string) { let tasks = this.realm.objects('TaskList').filtered('messageName == $0 ', messageName) tasks = tasks.filtered('status != $0', BookBackTaskStatus.DONE) tasks = tasks.filtered('status != $0', BookBackTaskStatus.FAIL) let ids = tasks.map((item) => { return item.id }) this.transaction(() => { for (let i = 0; i < ids.length; i++) { let task = this.realm.objectForPrimaryKey('TaskList', ids[i]) if (task == null) { throw new Error('没有找到对应的任务') } task.status = BookBackTaskStatus.FAIL task.errorMessage = errorMessage } }) } /** * 丢弃所有未开始的后台任务 * * 该方法将所有处于等待(WAIT)或重新连接(RECONNECT)状态的任务标记为失败(FAIL), * 并设置错误信息为"任务被丢弃"。通常在需要清理任务队列、重启系统或手动干预任务处理 * 流程时使用。 * * 丢弃操作会在一个数据库事务中执行,确保所有状态更改的原子性,避免部分更新导致的 * 数据不一致问题。 * * @returns {Promise} 无返回值的Promise * @throws {Error} 当任务ID存在但无法找到对应任务对象时抛出错误 * * @example * // 重启任务队列前丢弃所有未开始的任务 * await taskListService.GiveUpNotStartBackTask(); * await taskManager.restart(); */ GiveUpNotStartBackTask(): void { let task = this.realm .objects('TaskList') .filtered( 'status == $0 || status == $1', BookBackTaskStatus.WAIT, BookBackTaskStatus.RECONNECT ) let ids = task.map((item) => { return item.id }) // 讲所有的人物状态改为放弃,然后errmessage改为 此任务已丢弃 this.transaction(() => { for (let i = 0; i < ids.length; i++) { const element = this.realm.objectForPrimaryKey('TaskList', ids[i]) if (element == null) { throw new Error('没有找到对应的任务') } element.status = BookBackTaskStatus.FAIL element.errorMessage = '任务被丢弃' } }) } }