@@ -86,7 +86,7 @@ export class DocumentsService {
8686
8787 async findAll ( spaceId : string ) {
8888 const docs = await this . prisma . document . findMany ( {
89- where : { spaceId } ,
89+ where : { spaceId, deletedAt : null } ,
9090 orderBy : { order : 'asc' } ,
9191 select : {
9292 id : true ,
@@ -109,7 +109,7 @@ export class DocumentsService {
109109
110110 /** Lightweight existence check — no side effects, no relations loaded */
111111 async exists ( id : string ) : Promise < boolean > {
112- const count = await this . prisma . document . count ( { where : { id } } ) ;
112+ const count = await this . prisma . document . count ( { where : { id, deletedAt : null } } ) ;
113113 return count > 0 ;
114114 }
115115
@@ -177,18 +177,33 @@ export class DocumentsService {
177177 return doc ;
178178 }
179179
180+ /** 软删除:将文档移至回收站 */
180181 async remove ( id : string , userId ?: string ) {
181182 const doc = await this . prisma . document . findUnique ( {
182183 where : { id } ,
183184 include : { space : { select : { name : true } } } ,
184185 } ) ;
185186
186- const result = await this . prisma . document . delete ( {
187- where : { id } ,
188- } ) ;
187+ if ( ! doc ) {
188+ throw new NotFoundException ( `Document with ID ${ id } not found` ) ;
189+ }
190+
191+ // 软删除:设置 deletedAt,同时将子文档一并软删除
192+ const now = new Date ( ) ;
193+ await this . prisma . $transaction ( [
194+ this . prisma . document . update ( {
195+ where : { id } ,
196+ data : { deletedAt : now } ,
197+ } ) ,
198+ // 子文档也一并移入回收站
199+ this . prisma . document . updateMany ( {
200+ where : { parentId : id , deletedAt : null } ,
201+ data : { deletedAt : now } ,
202+ } ) ,
203+ ] ) ;
189204
190205 // 记录删除活动
191- if ( userId && doc ) {
206+ if ( userId ) {
192207 this . activityService . log ( {
193208 userId,
194209 action : ActivityAction . DELETE ,
@@ -200,7 +215,158 @@ export class DocumentsService {
200215 } ) ;
201216 }
202217
203- return result ;
218+ return { success : true } ;
219+ }
220+
221+ /** 获取空间回收站列表 */
222+ async findTrash ( spaceId : string ) {
223+ return this . prisma . document . findMany ( {
224+ where : {
225+ spaceId,
226+ deletedAt : { not : null } ,
227+ } ,
228+ orderBy : { deletedAt : 'desc' } ,
229+ select : {
230+ id : true ,
231+ title : true ,
232+ parentId : true ,
233+ deletedAt : true ,
234+ createdAt : true ,
235+ updatedAt : true ,
236+ creator : {
237+ select : {
238+ id : true ,
239+ name : true ,
240+ avatarUrl : true ,
241+ } ,
242+ } ,
243+ } ,
244+ } ) ;
245+ }
246+
247+ /** 恢复文档(从回收站) */
248+ async restore ( id : string , userId ?: string ) {
249+ const doc = await this . prisma . document . findUnique ( {
250+ where : { id } ,
251+ include : { space : { select : { name : true } } } ,
252+ } ) ;
253+
254+ if ( ! doc ) {
255+ throw new NotFoundException ( `Document with ID ${ id } not found` ) ;
256+ }
257+ if ( ! doc . deletedAt ) {
258+ throw new BadRequestException ( 'Document is not in trash' ) ;
259+ }
260+
261+ // 恢复文档及其子文档
262+ await this . prisma . $transaction ( [
263+ this . prisma . document . update ( {
264+ where : { id } ,
265+ data : { deletedAt : null } ,
266+ } ) ,
267+ this . prisma . document . updateMany ( {
268+ where : { parentId : id , deletedAt : doc . deletedAt } ,
269+ data : { deletedAt : null } ,
270+ } ) ,
271+ ] ) ;
272+
273+ // 记录恢复活动
274+ if ( userId ) {
275+ this . activityService . log ( {
276+ userId,
277+ action : ActivityAction . RESTORE ,
278+ entityType : EntityType . DOCUMENT ,
279+ entityId : id ,
280+ entityName : doc . title ,
281+ spaceId : doc . spaceId ,
282+ spaceName : doc . space . name ,
283+ } ) ;
284+ }
285+
286+ return { success : true } ;
287+ }
288+
289+ /** 永久删除(从回收站彻底删除) */
290+ async permanentlyDelete ( id : string ) {
291+ const doc = await this . prisma . document . findUnique ( { where : { id } } ) ;
292+ if ( ! doc ) {
293+ throw new NotFoundException ( `Document with ID ${ id } not found` ) ;
294+ }
295+ if ( ! doc . deletedAt ) {
296+ throw new BadRequestException (
297+ 'Document must be in trash before permanent deletion' ,
298+ ) ;
299+ }
300+
301+ return this . prisma . document . delete ( { where : { id } } ) ;
302+ }
303+
304+ // ─── 收藏功能 ───────────────────────────────────────────
305+
306+ /** 收藏文档 */
307+ async favorite ( documentId : string , userId : string ) {
308+ // upsert 避免重复
309+ return this . prisma . documentFavorite . upsert ( {
310+ where : { userId_documentId : { userId, documentId } } ,
311+ create : { userId, documentId } ,
312+ update : { } ,
313+ } ) ;
314+ }
315+
316+ /** 取消收藏 */
317+ async unfavorite ( documentId : string , userId : string ) {
318+ return this . prisma . documentFavorite
319+ . delete ( {
320+ where : { userId_documentId : { userId, documentId } } ,
321+ } )
322+ . catch ( ( ) => {
323+ // 不存在也不报错
324+ return null ;
325+ } ) ;
326+ }
327+
328+ /** 获取收藏列表 */
329+ async getFavorites ( userId : string ) {
330+ const favorites = await this . prisma . documentFavorite . findMany ( {
331+ where : { userId } ,
332+ orderBy : { createdAt : 'desc' } ,
333+ include : {
334+ document : {
335+ select : {
336+ id : true ,
337+ title : true ,
338+ spaceId : true ,
339+ updatedAt : true ,
340+ deletedAt : true ,
341+ creator : {
342+ select : {
343+ id : true ,
344+ name : true ,
345+ avatarUrl : true ,
346+ } ,
347+ } ,
348+ } ,
349+ } ,
350+ } ,
351+ } ) ;
352+
353+ // 过滤掉已删除的文档
354+ return favorites
355+ . filter ( ( f ) => f . document . deletedAt === null )
356+ . map ( ( f ) => ( {
357+ id : f . id ,
358+ documentId : f . documentId ,
359+ createdAt : f . createdAt ,
360+ document : f . document ,
361+ } ) ) ;
362+ }
363+
364+ /** 检查是否已收藏 */
365+ async isFavorited ( documentId : string , userId : string ) : Promise < boolean > {
366+ const count = await this . prisma . documentFavorite . count ( {
367+ where : { userId, documentId } ,
368+ } ) ;
369+ return count > 0 ;
204370 }
205371
206372 /**
0 commit comments