@@ -188,6 +188,258 @@ export class ActivityService {
188188 } ;
189189 }
190190
191+ // ==================== Stats / Analytics ====================
192+
193+ /**
194+ * 空间级统计数据
195+ */
196+ async getSpaceStats ( spaceId : string ) {
197+ const now = new Date ( ) ;
198+ const weekStart = new Date ( now ) ;
199+ weekStart . setDate ( now . getDate ( ) - now . getDay ( ) ) ;
200+ weekStart . setHours ( 0 , 0 , 0 , 0 ) ;
201+
202+ const thirtyDaysAgo = new Date ( now ) ;
203+ thirtyDaysAgo . setDate ( now . getDate ( ) - 30 ) ;
204+
205+ const [
206+ docCount ,
207+ memberCount ,
208+ totalVisitsAgg ,
209+ weeklyActions ,
210+ actionDistribution ,
211+ topDocuments ,
212+ topMembers ,
213+ ] = await Promise . all ( [
214+ // 文档总数
215+ this . prisma . document . count ( {
216+ where : { spaceId, deletedAt : null } ,
217+ } ) ,
218+ // 成员数
219+ this . prisma . spacePermission . count ( {
220+ where : { spaceId } ,
221+ } ) ,
222+ // 总阅读量
223+ this . prisma . documentVisit . aggregate ( {
224+ where : { spaceId } ,
225+ _sum : { visitCount : true } ,
226+ } ) ,
227+ // 本周活跃操作数
228+ this . prisma . activityLog . count ( {
229+ where : { spaceId, createdAt : { gte : weekStart } } ,
230+ } ) ,
231+ // 操作类型分布
232+ this . prisma . activityLog . groupBy ( {
233+ by : [ 'action' ] ,
234+ where : { spaceId } ,
235+ _count : true ,
236+ } ) ,
237+ // 热门文档 Top 10
238+ this . prisma . documentVisit . groupBy ( {
239+ by : [ 'documentId' ] ,
240+ where : { spaceId } ,
241+ _sum : { visitCount : true } ,
242+ orderBy : { _sum : { visitCount : 'desc' } } ,
243+ take : 10 ,
244+ } ) ,
245+ // 活跃成员 Top 10(近 30 天)
246+ this . prisma . activityLog . groupBy ( {
247+ by : [ 'userId' ] ,
248+ where : { spaceId, createdAt : { gte : thirtyDaysAgo } } ,
249+ _count : true ,
250+ orderBy : { _count : { userId : 'desc' } } ,
251+ take : 10 ,
252+ } ) ,
253+ ] ) ;
254+
255+ // 30 天文档增长趋势
256+ const docGrowth = await this . prisma . document . groupBy ( {
257+ by : [ 'createdAt' ] ,
258+ where : {
259+ spaceId,
260+ deletedAt : null ,
261+ createdAt : { gte : thirtyDaysAgo } ,
262+ } ,
263+ _count : true ,
264+ orderBy : { createdAt : 'asc' } ,
265+ } ) ;
266+
267+ // 按日聚合文档增长
268+ const growthByDay : Record < string , number > = { } ;
269+ for ( const item of docGrowth ) {
270+ const day = new Date ( item . createdAt ) . toISOString ( ) . slice ( 0 , 10 ) ;
271+ growthByDay [ day ] = ( growthByDay [ day ] || 0 ) + item . _count ;
272+ }
273+ const docGrowthTrend = [ ] ;
274+ for ( let i = 29 ; i >= 0 ; i -- ) {
275+ const d = new Date ( now ) ;
276+ d . setDate ( now . getDate ( ) - i ) ;
277+ const key = d . toISOString ( ) . slice ( 0 , 10 ) ;
278+ docGrowthTrend . push ( { date : key , count : growthByDay [ key ] || 0 } ) ;
279+ }
280+
281+ // 填充热门文档标题
282+ const docIds = topDocuments . map ( ( d ) => d . documentId ) ;
283+ const docs = docIds . length
284+ ? await this . prisma . document . findMany ( {
285+ where : { id : { in : docIds } } ,
286+ select : { id : true , title : true } ,
287+ } )
288+ : [ ] ;
289+ const docMap = new Map ( docs . map ( ( d ) => [ d . id , d . title ] ) ) ;
290+
291+ // 填充活跃成员名称
292+ const userIds = topMembers . map ( ( m ) => m . userId ) ;
293+ const users = userIds . length
294+ ? await this . prisma . user . findMany ( {
295+ where : { id : { in : userIds } } ,
296+ select : { id : true , name : true , avatarUrl : true } ,
297+ } )
298+ : [ ] ;
299+ const userMap = new Map ( users . map ( ( u ) => [ u . id , u ] ) ) ;
300+
301+ return {
302+ overview : {
303+ docCount,
304+ memberCount,
305+ totalViews : totalVisitsAgg . _sum . visitCount || 0 ,
306+ weeklyActions,
307+ } ,
308+ docGrowthTrend,
309+ topDocuments : topDocuments . map ( ( d ) => ( {
310+ documentId : d . documentId ,
311+ title : docMap . get ( d . documentId ) || '未知文档' ,
312+ views : d . _sum . visitCount || 0 ,
313+ } ) ) ,
314+ topMembers : topMembers . map ( ( m ) => ( {
315+ userId : m . userId ,
316+ name : userMap . get ( m . userId ) ?. name || '未知用户' ,
317+ avatarUrl : userMap . get ( m . userId ) ?. avatarUrl || null ,
318+ actions : m . _count ,
319+ } ) ) ,
320+ actionDistribution : actionDistribution . map ( ( a ) => ( {
321+ action : a . action ,
322+ count : a . _count ,
323+ } ) ) ,
324+ } ;
325+ }
326+
327+ /**
328+ * 文档级阅读统计
329+ */
330+ async getDocumentStats ( documentId : string ) {
331+ const now = new Date ( ) ;
332+ const sevenDaysAgo = new Date ( now ) ;
333+ sevenDaysAgo . setDate ( now . getDate ( ) - 7 ) ;
334+
335+ const [ uvCount , pvAgg , recentVisits ] = await Promise . all ( [
336+ // UV:独立访客数
337+ this . prisma . documentVisit . count ( {
338+ where : { documentId } ,
339+ } ) ,
340+ // PV:总访问次数
341+ this . prisma . documentVisit . aggregate ( {
342+ where : { documentId } ,
343+ _sum : { visitCount : true } ,
344+ } ) ,
345+ // 近 7 天访问记录(按最近访问时间过滤)
346+ this . prisma . documentVisit . findMany ( {
347+ where : {
348+ documentId,
349+ lastVisitAt : { gte : sevenDaysAgo } ,
350+ } ,
351+ select : { lastVisitAt : true , visitCount : true } ,
352+ } ) ,
353+ ] ) ;
354+
355+ // 按日聚合近 7 天访问
356+ const visitsByDay : Record < string , number > = { } ;
357+ for ( const v of recentVisits ) {
358+ const day = new Date ( v . lastVisitAt ) . toISOString ( ) . slice ( 0 , 10 ) ;
359+ visitsByDay [ day ] = ( visitsByDay [ day ] || 0 ) + 1 ;
360+ }
361+ const dailyTrend = [ ] ;
362+ for ( let i = 6 ; i >= 0 ; i -- ) {
363+ const d = new Date ( now ) ;
364+ d . setDate ( now . getDate ( ) - i ) ;
365+ const key = d . toISOString ( ) . slice ( 0 , 10 ) ;
366+ dailyTrend . push ( { date : key , count : visitsByDay [ key ] || 0 } ) ;
367+ }
368+
369+ return {
370+ uv : uvCount ,
371+ pv : pvAgg . _sum . visitCount || 0 ,
372+ dailyTrend,
373+ } ;
374+ }
375+
376+ /**
377+ * 个人生产力统计
378+ */
379+ async getUserProductivityStats ( userId : string ) {
380+ const now = new Date ( ) ;
381+
382+ const thisWeekStart = new Date ( now ) ;
383+ thisWeekStart . setDate ( now . getDate ( ) - now . getDay ( ) ) ;
384+ thisWeekStart . setHours ( 0 , 0 , 0 , 0 ) ;
385+
386+ const lastWeekStart = new Date ( thisWeekStart ) ;
387+ lastWeekStart . setDate ( lastWeekStart . getDate ( ) - 7 ) ;
388+
389+ const [
390+ thisWeekCreated ,
391+ lastWeekCreated ,
392+ thisWeekEdited ,
393+ lastWeekEdited ,
394+ totalReadsAgg ,
395+ ] = await Promise . all ( [
396+ this . prisma . activityLog . count ( {
397+ where : {
398+ userId,
399+ action : 'CREATE' ,
400+ entityType : 'DOCUMENT' ,
401+ createdAt : { gte : thisWeekStart } ,
402+ } ,
403+ } ) ,
404+ this . prisma . activityLog . count ( {
405+ where : {
406+ userId,
407+ action : 'CREATE' ,
408+ entityType : 'DOCUMENT' ,
409+ createdAt : { gte : lastWeekStart , lt : thisWeekStart } ,
410+ } ,
411+ } ) ,
412+ this . prisma . activityLog . count ( {
413+ where : {
414+ userId,
415+ action : 'UPDATE' ,
416+ entityType : 'DOCUMENT' ,
417+ createdAt : { gte : thisWeekStart } ,
418+ } ,
419+ } ) ,
420+ this . prisma . activityLog . count ( {
421+ where : {
422+ userId,
423+ action : 'UPDATE' ,
424+ entityType : 'DOCUMENT' ,
425+ createdAt : { gte : lastWeekStart , lt : thisWeekStart } ,
426+ } ,
427+ } ) ,
428+ this . prisma . documentVisit . aggregate ( {
429+ where : { document : { createdBy : userId } } ,
430+ _sum : { visitCount : true } ,
431+ } ) ,
432+ ] ) ;
433+
434+ return {
435+ thisWeekCreated,
436+ lastWeekCreated,
437+ thisWeekEdited,
438+ lastWeekEdited,
439+ totalReads : totalReadsAgg . _sum . visitCount || 0 ,
440+ } ;
441+ }
442+
191443 /**
192444 * 清理超过 90 天的旧日志(可由定时任务调用)
193445 */
0 commit comments