From 0ea36a7073a2e881df6cd98441f0f45393f78319 Mon Sep 17 00:00:00 2001 From: wangdl Date: Fri, 19 Jun 2026 13:04:55 +0800 Subject: [PATCH] feat(API-ADMIN-010): add enrichWithNames to admin list controllers - New shared helper: src/common/helpers/name-resolver.ts - Applied to: reviews, users/members, files, knowledge-bases, imports - Learning service refactored to use shared helper Co-Authored-By: Claude Opus 4.7 --- src/common/helpers/name-resolver.ts | 39 +++++++++++++++ .../admin-knowledge.controller.ts | 3 +- .../admin-imports.controller.ts | 3 +- src/modules/files/admin-files.controller.ts | 3 +- .../admin-learning.service.ts | 48 +++++-------------- src/modules/review/admin-review.controller.ts | 4 +- .../users/admin-users-mgmt.controller.ts | 3 +- 7 files changed, 61 insertions(+), 42 deletions(-) create mode 100644 src/common/helpers/name-resolver.ts diff --git a/src/common/helpers/name-resolver.ts b/src/common/helpers/name-resolver.ts new file mode 100644 index 0000000..f7af309 --- /dev/null +++ b/src/common/helpers/name-resolver.ts @@ -0,0 +1,39 @@ +import { PrismaService } from '../../infrastructure/database/prisma.service'; + +/** + * Enrich result items with human-readable names resolved from IDs. + * Adds userName, kbName, materialName fields where applicable. + */ +export async function enrichWithNames>( + prisma: PrismaService, + items: T[], +): Promise<(T & { userName?: string; kbName?: string; materialName?: string })[]> { + if (items.length === 0) return items; + + const userIds = [...new Set(items.map(i => i.userId).filter(Boolean))]; + const kbIds = [...new Set(items.map(i => i.knowledgeBaseId).filter(Boolean))]; + const materialIds = [...new Set(items.map(i => i.materialId).filter(Boolean))]; + + const [users, kbs, materials] = await Promise.all([ + userIds.length > 0 + ? prisma.user.findMany({ where: { id: { in: userIds } }, select: { id: true, nickname: true } }) + : [], + kbIds.length > 0 + ? prisma.knowledgeBase.findMany({ where: { id: { in: kbIds } }, select: { id: true, title: true } }) + : [], + materialIds.length > 0 + ? prisma.knowledgeSource.findMany({ where: { id: { in: materialIds } }, select: { id: true, originalFilename: true, title: true } }) + : [], + ]); + + const userMap = new Map(users.map(u => [u.id, u.nickname || u.id] as [string, string])); + const kbMap = new Map(kbs.map(k => [k.id, k.title] as [string, string])); + const matMap = new Map(materials.map(m => [m.id, m.originalFilename || m.title || m.id] as [string, string])); + + return items.map(item => ({ + ...item, + userName: userMap.get(item.userId), + kbName: kbMap.get(item.knowledgeBaseId), + materialName: matMap.get(item.materialId), + })); +} diff --git a/src/modules/admin-knowledge/admin-knowledge.controller.ts b/src/modules/admin-knowledge/admin-knowledge.controller.ts index f9b7c21..28031c3 100644 --- a/src/modules/admin-knowledge/admin-knowledge.controller.ts +++ b/src/modules/admin-knowledge/admin-knowledge.controller.ts @@ -1,6 +1,7 @@ import { Controller, Get, Param, Delete, Query, UseGuards } from '@nestjs/common'; import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger'; import { PrismaService } from '../../infrastructure/database/prisma.service'; +import { enrichWithNames } from '../../common/helpers/name-resolver'; import { AdminAuthGuard } from '../../common/guards/admin-auth.guard'; import { AdminRolesGuard } from '../../common/guards/admin-roles.guard'; @@ -24,7 +25,7 @@ export class AdminKnowledgeController { }), this.prisma.knowledgeBase.count({ where: { deletedAt: null } }), ]); - return { items, total, page: p, limit: l, totalPages: Math.ceil(total / l) }; + const enriched = await enrichWithNames(this.prisma, items); return { items: enriched, total, page: p, limit: l, totalPages: Math.ceil(total / l) }; } @Get(':id') diff --git a/src/modules/document-import/admin-imports.controller.ts b/src/modules/document-import/admin-imports.controller.ts index db5defa..2bbc2b1 100644 --- a/src/modules/document-import/admin-imports.controller.ts +++ b/src/modules/document-import/admin-imports.controller.ts @@ -1,6 +1,7 @@ import { Controller, Get, Post, Param, Query, UseGuards } from '@nestjs/common'; import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger'; import { PrismaService } from '../../infrastructure/database/prisma.service'; +import { enrichWithNames } from '../../common/helpers/name-resolver'; import { AdminAuthGuard } from '../../common/guards/admin-auth.guard'; import { AdminRolesGuard } from '../../common/guards/admin-roles.guard'; import { AdminRoles } from '../../common/decorators/admin-roles.decorator'; @@ -23,7 +24,7 @@ export class AdminImportsController { this.prisma.documentImport.findMany({ where, orderBy: { createdAt: 'desc' }, skip: (p - 1) * l, take: l }), this.prisma.documentImport.count({ where }), ]); - return { items, total, page: p, limit: l, totalPages: Math.ceil(total / l) }; + const enriched = await enrichWithNames(this.prisma, items); return { items: enriched, total, page: p, limit: l, totalPages: Math.ceil(total / l) }; } @Get(':id') diff --git a/src/modules/files/admin-files.controller.ts b/src/modules/files/admin-files.controller.ts index 6e4da8e..03ca951 100644 --- a/src/modules/files/admin-files.controller.ts +++ b/src/modules/files/admin-files.controller.ts @@ -1,6 +1,7 @@ import { Controller, Get, Delete, Param, Query, UseGuards } from '@nestjs/common'; import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger'; import { PrismaService } from '../../infrastructure/database/prisma.service'; +import { enrichWithNames } from '../../common/helpers/name-resolver'; import { QueueService, QUEUE_FILE_CLEANUP } from '../../infrastructure/queue/queue.service'; import { AdminAuthGuard } from '../../common/guards/admin-auth.guard'; import { AdminRolesGuard } from '../../common/guards/admin-roles.guard'; @@ -28,7 +29,7 @@ export class AdminFilesController { }), this.prisma.uploadedFile.count({ where: {} }), ]); - return { items, total, page: p, limit: l, totalPages: Math.ceil(total / l) }; + const enriched = await enrichWithNames(this.prisma, items); return { items: enriched, total, page: p, limit: l, totalPages: Math.ceil(total / l) }; } @Delete(':id') diff --git a/src/modules/learning-session/admin-learning.service.ts b/src/modules/learning-session/admin-learning.service.ts index cb8b2bb..ce846af 100644 --- a/src/modules/learning-session/admin-learning.service.ts +++ b/src/modules/learning-session/admin-learning.service.ts @@ -1,6 +1,7 @@ import { Injectable, BadRequestException } from '@nestjs/common'; import { PrismaService } from '../../infrastructure/database/prisma.service'; import { SnapshotBuilderService } from '../ai-runtime/snapshot-builder.service'; +import { enrichWithNames } from '../../common/helpers/name-resolver'; @Injectable() export class AdminLearningService { @@ -11,35 +12,8 @@ export class AdminLearningService { // ══ Name resolution ══ - private async enrichWithNames>(items: T[]): Promise<(T & { userName?: string; kbName?: string; materialName?: string })[]> { - if (items.length === 0) return items; - - const userIds = [...new Set(items.map(i => i.userId).filter(Boolean))]; - const kbIds = [...new Set(items.map(i => i.knowledgeBaseId).filter(Boolean))]; - const materialIds = [...new Set(items.map(i => i.materialId).filter(Boolean))]; - - const [users, kbs, materials] = await Promise.all([ - userIds.length > 0 - ? this.prisma.user.findMany({ where: { id: { in: userIds } }, select: { id: true, nickname: true } }) - : [], - kbIds.length > 0 - ? this.prisma.knowledgeBase.findMany({ where: { id: { in: kbIds } }, select: { id: true, title: true } }) - : [], - materialIds.length > 0 - ? this.prisma.knowledgeSource.findMany({ where: { id: { in: materialIds } }, select: { id: true, originalFilename: true, title: true } }) - : [], - ]); - - const userMap = new Map(users.map(u => [u.id, u.nickname || u.id] as [string, string])); - const kbMap = new Map(kbs.map(k => [k.id, k.title] as [string, string])); - const matMap = new Map(materials.map(m => [m.id, m.originalFilename || m.title || m.id] as [string, string])); - - return items.map(item => ({ - ...item, - userName: userMap.get(item.userId), - kbName: kbMap.get(item.knowledgeBaseId), - materialName: matMap.get(item.materialId), - })); + private async enrich>(items: T[]) { + return enrichWithNames(this.prisma, items); } // ══ Knowledge Bases (for filter dropdown) ══ @@ -145,7 +119,7 @@ export class AdminLearningService { this.prisma.readingEvent.findMany({ where, orderBy, skip: (page - 1) * limit, take: limit }), this.prisma.readingEvent.count({ where }), ]); - const enriched = await this.enrichWithNames(items); return { items: enriched, total, page, limit }; + const enriched = await this.enrich(items); return { items: enriched, total, page, limit }; } async getReadingEvent(id: string) { @@ -171,7 +145,7 @@ export class AdminLearningService { this.prisma.learningSession.findMany({ where, orderBy: { startedAt: 'desc' }, skip: (page - 1) * limit, take: limit }), this.prisma.learningSession.count({ where }), ]); - const enriched = await this.enrichWithNames(items); return { items: enriched, total, page, limit }; + const enriched = await this.enrich(items); return { items: enriched, total, page, limit }; } async getSession(id: string) { @@ -194,7 +168,7 @@ export class AdminLearningService { this.prisma.materialReadingProgress.findMany({ where, orderBy: { lastReadAt: 'desc' }, skip: (page - 1) * limit, take: limit }), this.prisma.materialReadingProgress.count({ where }), ]); - const enriched = await this.enrichWithNames(items); return { items: enriched, total, page, limit }; + const enriched = await this.enrich(items); return { items: enriched, total, page, limit }; } async getProgressDetail(id: string) { @@ -217,7 +191,7 @@ export class AdminLearningService { this.prisma.dailyLearningActivity.findMany({ where, orderBy: { activityDate: 'desc' }, skip: (page - 1) * limit, take: limit }), this.prisma.dailyLearningActivity.count({ where }), ]); - const enriched = await this.enrichWithNames(items); return { items: enriched, total, page, limit }; + const enriched = await this.enrich(items); return { items: enriched, total, page, limit }; } // ══ Records ══ @@ -235,7 +209,7 @@ export class AdminLearningService { this.prisma.learningRecord.findMany({ where, orderBy: { occurredAt: 'desc' }, skip: (page - 1) * limit, take: limit }), this.prisma.learningRecord.count({ where }), ]); - const enriched = await this.enrichWithNames(items); return { items: enriched, total, page, limit }; + const enriched = await this.enrich(items); return { items: enriched, total, page, limit }; } async getRecord(id: string) { @@ -258,7 +232,7 @@ export class AdminLearningService { }), this.prisma.aiAnalysisResult.count({ where }), ]); - const enriched = await this.enrichWithNames(items); return { items: enriched, total, page, limit }; + const enriched = await this.enrich(items); return { items: enriched, total, page, limit }; } // ══ AI Usage ══ @@ -276,7 +250,7 @@ export class AdminLearningService { }), this.prisma.aiUsageLog.count({ where }), ]); - const enriched = await this.enrichWithNames(items); return { items: enriched, total, page, limit }; + const enriched = await this.enrich(items); return { items: enriched, total, page, limit }; } // ══ Timeline ══ @@ -343,7 +317,7 @@ export class AdminLearningService { this.prisma.temporaryReadingMaterial.findMany({ orderBy: { createdAt: 'desc' }, skip: (page - 1) * limit, take: limit }), this.prisma.temporaryReadingMaterial.count(), ]); - const enriched = await this.enrichWithNames(items); return { items: enriched, total, page, limit }; + const enriched = await this.enrich(items); return { items: enriched, total, page, limit }; } // ══ Operations ══ diff --git a/src/modules/review/admin-review.controller.ts b/src/modules/review/admin-review.controller.ts index ddbded8..f7a06a2 100644 --- a/src/modules/review/admin-review.controller.ts +++ b/src/modules/review/admin-review.controller.ts @@ -2,6 +2,7 @@ import { Controller, Get, Query, UseGuards } from '@nestjs/common'; import { ApiTags, ApiBearerAuth, ApiOperation, ApiQuery } from '@nestjs/swagger'; import { PrismaService } from '../../infrastructure/database/prisma.service'; import { AdminAuthGuard } from '../../common/guards/admin-auth.guard'; +import { enrichWithNames } from '../../common/helpers/name-resolver'; import { AdminRolesGuard } from '../../common/guards/admin-roles.guard'; @ApiTags('admin-review') @@ -44,6 +45,7 @@ export class AdminReviewController { this.prisma.reviewCard.count({ where }), ]); - return { items, total }; + const enriched = await enrichWithNames(this.prisma, items); + return { items: enriched, total }; } } diff --git a/src/modules/users/admin-users-mgmt.controller.ts b/src/modules/users/admin-users-mgmt.controller.ts index 3b4c906..71e5ee5 100644 --- a/src/modules/users/admin-users-mgmt.controller.ts +++ b/src/modules/users/admin-users-mgmt.controller.ts @@ -1,6 +1,7 @@ import { Controller, Get, Post, Delete, Body, Param, Query, UseGuards } from '@nestjs/common'; import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger'; import { PrismaService } from '../../infrastructure/database/prisma.service'; +import { enrichWithNames } from '../../common/helpers/name-resolver'; import { AdminAuthGuard } from '../../common/guards/admin-auth.guard'; import { AdminRolesGuard } from '../../common/guards/admin-roles.guard'; import { AdminRoles } from '../../common/decorators/admin-roles.decorator'; @@ -27,7 +28,7 @@ export class AdminUsersMgmtController { this.prisma.user.findMany({ where, orderBy: { createdAt: 'desc' }, take, skip, select: { id: true, email: true, nickname: true, role: true, status: true, lastLoginAt: true, createdAt: true } }), this.prisma.user.count({ where }), ]); - return { items, total }; + const enriched = await enrichWithNames(this.prisma, items); return { items: enriched, total }; } // ── Membership ──