返回 AiToEarn
content-generation-task.repository.ts
根目录 / project / aitoearn-backend / libs / mongodb / src / repositories / content-generation-task.repository.ts
1 import { InjectModel } from '@nestjs/mongoose'
2 import { Pagination } from '@yikart/common'
3 import { FilterQuery, Model } from 'mongoose'
4 import { ContentGenerationTaskStatus } from '../enums'
5 import { ContentGenerationTask, TaskAnalysis } from '../schemas'
6 import { BaseRepository, LeanDoc } from './base.repository'
7
8 export interface ListContentGenerationTaskParams extends Pagination {
9 userId?: string
10 taskId?: string
11 status?: ContentGenerationTaskStatus
12 minRating?: number
13 maxRating?: number
14 hasRating?: boolean
15 }
16
17 export interface GetUserTasksParams extends Pagination {
18 keyword?: string
19 favoriteOnly?: boolean
20 }
21
22 export class ContentGenerationTaskRepository extends BaseRepository<ContentGenerationTask> {
23 constructor(
24 @InjectModel(ContentGenerationTask.name) contentGenerationTaskModel: Model<ContentGenerationTask>,
25 ) {
26 super(contentGenerationTaskModel)
27 }
28
29 override async create(data: Partial<ContentGenerationTask>) {
30 const doc = await this.model.create(data)
31 return doc.toObject() as LeanDoc<ContentGenerationTask>
32 }
33
34 async getUserTask(userId: string, taskId: string) {
35 return await this.findOne({ userId, _id: taskId, deletedAt: null })
36 }
37
38 async getByUserIdAndId(userId: string, taskId: string) {
39 return await this.getUserTask(userId, taskId)
40 }
41
42 async updateMessage(taskId: string, message: Record<string, unknown>) {
43 return await this.model.findByIdAndUpdate(
44 taskId,
45 { $push: { messages: message } },
46 { new: true },
47 ).lean({ virtuals: true }).exec()
48 }
49
50 async getMessages(taskId: string) {
51 const task = await this.getById(taskId)
52 return task?.messages || []
53 }
54
55 async getByMessageUuid(messageUuid: string) {
56 return await this.findOne({
57 messages: {
58 $elemMatch: {
59 uuid: messageUuid,
60 },
61 },
62 })
63 }
64
65 /**
66 * Find a task by its public share token.
67 * @param token share token
68 */
69 async findByPublicShareToken(token: string) {
70 return await this.findOne({ publicShareToken: token, deletedAt: null })
71 }
72
73 async getUserTasksWithPagination(userId: string, params: GetUserTasksParams) {
74 const { page, pageSize, keyword, favoriteOnly } = params
75 const filter: FilterQuery<ContentGenerationTask> = { userId, deletedAt: null }
76
77 if (keyword) {
78 const escapedKeyword = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
79 const regex = { $regex: escapedKeyword, $options: 'i' }
80 filter.$or = [
81 { title: regex },
82 { 'messages.content': regex },
83 { 'messages.content.text': regex },
84 { 'messages.message.content.text': regex },
85 ]
86 }
87
88 if (favoriteOnly === true) {
89 filter.favoritedAt = { $ne: null }
90 }
91
92 const [items, total] = await this.findWithPagination({
93 page,
94 pageSize,
95 filter,
96 options: { sort: { createdAt: -1 } },
97 })
98 return [items, total] as const
99 }
100
101 async updateFavoriteById(taskId: string, favoritedAt: Date | null) {
102 return await this.model.findByIdAndUpdate(
103 taskId,
104 { $set: { favoritedAt } },
105 { new: true },
106 ).lean({ virtuals: true }).exec()
107 }
108
109 async listWithPagination(params: ListContentGenerationTaskParams) {
110 const { page, pageSize, userId, taskId, status, minRating, maxRating, hasRating } = params
111
112 const filter: FilterQuery<ContentGenerationTask> = {
113 deletedAt: null,
114 }
115
116 if (userId) {
117 filter.userId = userId
118 }
119
120 if (taskId) {
121 filter._id = taskId
122 }
123
124 if (status) {
125 filter.status = status
126 }
127
128 // Rating 筛选逻辑
129 if (minRating !== undefined || maxRating !== undefined) {
130 filter.rating = {}
131 if (minRating !== undefined) {
132 filter.rating.$gte = minRating
133 }
134 if (maxRating !== undefined) {
135 filter.rating.$lte = maxRating
136 }
137 }
138 else if (hasRating !== undefined) {
139 if (hasRating) {
140 filter.rating = { $exists: true, $ne: null }
141 }
142 else {
143 filter.$or = [
144 { rating: { $exists: false } },
145 { rating: null },
146 ]
147 }
148 }
149
150 const [items, total] = await this.findWithPagination({
151 page,
152 pageSize,
153 filter,
154 projection: {
155 messages: false,
156 },
157 options: {
158 sort: { createdAt: -1 },
159 },
160 })
161 return [items, total] as const
162 }
163
164 async updateStatus(taskId: string, status: ContentGenerationTaskStatus) {
165 return await this.model.findByIdAndUpdate(
166 taskId,
167 { $set: { status } },
168 { new: true },
169 ).lean({ virtuals: true }).exec()
170 }
171
172 async softDeleteTask(userId: string, taskId: string) {
173 const res = await this.model.updateOne(
174 { _id: taskId, userId, deletedAt: null },
175 { $set: { deletedAt: new Date() } },
176 ).exec()
177 return res.modifiedCount > 0
178 }
179
180 async getActiveUserTotal(startDate: Date, endDate: Date) {
181 const result = await this.model.aggregate([
182 { $match: { createdAt: { $gte: startDate, $lte: endDate } } },
183 { $group: { _id: '$userId', total: { $sum: 1 } } },
184 ])
185 return result.length
186 }
187
188 /**
189 * 查询所有状态为 Running 且 updatedAt 超过指定时间的任务
190 * @param timeoutMs 超时时间(毫秒),默认 30 分钟
191 */
192 async listTimeoutRunningTasks(timeoutMs: number = 30 * 60 * 1000) {
193 const timeoutDate = new Date(Date.now() - timeoutMs)
194 return await this.model.find({
195 status: ContentGenerationTaskStatus.Running,
196 updatedAt: { $lt: timeoutDate },
197 deletedAt: null,
198 }).lean({ virtuals: true }).exec()
199 }
200
201 /**
202 * 批量更新任务状态
203 * @param taskIds 任务 ID 数组
204 * @param status 新状态
205 */
206 async batchUpdateStatus(taskIds: string[], status: ContentGenerationTaskStatus) {
207 if (taskIds.length === 0) {
208 return { modifiedCount: 0 }
209 }
210 return await this.model.updateMany(
211 { _id: { $in: taskIds } },
212 { $set: { status } },
213 ).exec()
214 }
215
216 /**
217 * 获取最新的任务(不含messages),按时间倒序,最多1000条
218 */
219 async getTasksByDateRange(startDate: Date, endDate: Date) {
220 return await this.model.find({
221 createdAt: { $gte: startDate, $lte: endDate },
222 // status: ContentGenerationTaskStatus.Error,
223 })
224 .sort({ createdAt: -1 })
225 .limit(1000)
226 .lean({ virtuals: true })
227 .exec()
228 }
229
230 /**
231 * 更新任务分析结果
232 */
233 async updateAnalysisById(taskId: string, analysis: TaskAnalysis) {
234 return await this.model.findByIdAndUpdate(
235 taskId,
236 { $set: { analysis } },
237 { new: true },
238 ).lean({ virtuals: true }).exec()
239 }
240
241 async listUnanalyzedByDateRange(startDate: Date, endDate: Date) {
242 return await this.model.find({
243 createdAt: { $gte: startDate, $lte: endDate },
244 deletedAt: null,
245 status: ContentGenerationTaskStatus.Completed,
246 $or: [
247 { analysis: { $exists: false } },
248 { analysis: null },
249 ],
250 }).select('_id userId').lean({ virtuals: true }).exec()
251 }
252
253 async aggregateIssuesByDateRange(startDate: Date, endDate: Date) {
254 return await this.model.aggregate([
255 {
256 $match: {
257 'createdAt': { $gte: startDate, $lte: endDate },
258 'deletedAt': null,
259 'analysis.optimizations': { $exists: true, $ne: [] },
260 },
261 },
262 { $unwind: '$analysis.optimizations' },
263 {
264 $project: {
265 issue: '$analysis.optimizations.issue',
266 priority: '$analysis.optimizations.priority',
267 },
268 },
269 ]).exec()
270 }
271
272 async countAnalyzedByDateRange(startDate: Date, endDate: Date): Promise<number> {
273 return await this.model.countDocuments({
274 createdAt: { $gte: startDate, $lte: endDate },
275 deletedAt: null,
276 analysis: { $ne: null },
277 }).exec()
278 }
279 }
280
280 lines TYPESCRIPT