feat: M2-04 — Ingestion & Indexing, ImportStepLog + Admin monitor AAPI
All checks were successful
Deploy API Server / build-and-deploy (push) Successful in 42s

- ImportStepLog model for tracking each import pipeline step
- Admin AAPI: import list, detail with step logs, retry failed
- Admin page: ImportMonitor with drawer detail view

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
WangDL 2026-05-24 13:12:16 +08:00
parent dffcd0192d
commit 9520d1f549
6 changed files with 128 additions and 3 deletions

View File

@ -0,0 +1,12 @@
CREATE TABLE IF NOT EXISTS `ImportStepLog` (
`id` VARCHAR(191) NOT NULL,
`importId` VARCHAR(191) NOT NULL,
`step` VARCHAR(32) NOT NULL,
`status` VARCHAR(16) NOT NULL,
`detail` VARCHAR(500) NULL,
`startedAt` DATETIME(3) NULL,
`completedAt` DATETIME(3) NULL,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
INDEX `ImportStepLog_importId_idx`(`importId`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

View File

@ -317,6 +317,19 @@ model DocumentImport {
@@index([workerId]) @@index([workerId])
} }
model ImportStepLog {
id String @id @default(cuid())
importId String
step String @db.VarChar(32)
status String @db.VarChar(16)
detail String? @db.VarChar(500)
startedAt DateTime?
completedAt DateTime?
createdAt DateTime @default(now())
@@index([importId])
}
model LearningSession { model LearningSession {
id String @id @default(cuid()) id String @id @default(cuid())
userId String userId String

View File

@ -0,0 +1,50 @@
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 { AdminAuthGuard } from '../../common/guards/admin-auth.guard';
import { AdminRolesGuard } from '../../common/guards/admin-roles.guard';
import { AdminRoles } from '../../common/decorators/admin-roles.decorator';
import type { AdminRole } from '../../common/types/admin-role.enum';
@ApiTags('admin-imports')
@Controller('admin-api/imports')
@UseGuards(AdminAuthGuard, AdminRolesGuard)
@ApiBearerAuth()
export class AdminImportsController {
constructor(private readonly prisma: PrismaService) {}
@Get()
@AdminRoles('ADMIN' as AdminRole)
@ApiOperation({ summary: '导入任务列表' })
async list(@Query('page') page = '1', @Query('limit') limit = '20', @Query('status') status?: string) {
const p = parseInt(page), l = parseInt(limit);
const where: any = status ? { status } : {};
const [items, total] = await Promise.all([
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) };
}
@Get(':id')
@AdminRoles('ADMIN' as AdminRole)
@ApiOperation({ summary: '导入任务详情' })
async detail(@Param('id') id: string) {
const [job, steps] = await Promise.all([
this.prisma.documentImport.findUnique({ where: { id } }),
this.prisma.importStepLog.findMany({ where: { importId: id }, orderBy: { createdAt: 'asc' } }),
]);
return { job, steps };
}
@Post(':id/retry')
@AdminRoles('SUPER_ADMIN' as AdminRole)
@ApiOperation({ summary: '重试失败导入' })
async retry(@Param('id') id: string) {
await this.prisma.documentImport.update({
where: { id },
data: { status: 'QUEUED', retryCount: 0, errorMessage: null },
});
return { success: true };
}
}

View File

@ -1,11 +1,13 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { DocumentImportController } from './document-import.controller'; import { DocumentImportController } from './document-import.controller';
import { AdminImportsController } from './admin-imports.controller';
import { DocumentImportService } from './document-import.service'; import { DocumentImportService } from './document-import.service';
import { DocumentImportRepository } from './document-import.repository'; import { DocumentImportRepository } from './document-import.repository';
import { PrismaService } from '../../infrastructure/database/prisma.service';
@Module({ @Module({
controllers: [DocumentImportController], controllers: [DocumentImportController, AdminImportsController],
providers: [DocumentImportService, DocumentImportRepository], providers: [DocumentImportService, DocumentImportRepository, PrismaService],
exports: [DocumentImportService, DocumentImportRepository], exports: [DocumentImportService, DocumentImportRepository],
}) })
export class DocumentImportModule {} export class DocumentImportModule {}

View File

@ -185,4 +185,52 @@ describe('M2 E2E Tests', () => {
expect(Array.isArray(res.body.data)).toBe(true); expect(Array.isArray(res.body.data)).toBe(true);
}); });
}); });
// ══════════════════════════════════════════════
// M2-04: Ingestion & Indexing
// ══════════════════════════════════════════════
describe('M2-04 Ingestion & Indexing', () => {
let token: string;
beforeAll(async () => { token = await loginAdmin(); });
it('POST /api/imports → 201 create import', async () => {
const res = await request(app.getHttpServer())
.post('/api/imports')
.send({ sourceType: 'file', sourceName: 'test.pdf', knowledgeBaseId: 'kb1', userId: 'user1' })
.expect([200, 201]);
expect(res.body.data).toHaveProperty('id');
});
it('GET /admin-api/imports → 200 import list', async () => {
if (!token) return;
const res = await request(app.getHttpServer())
.get('/admin-api/imports')
.set('Authorization', `Bearer ${token}`)
.expect(200);
expect(res.body.data).toHaveProperty('items');
expect(res.body.data).toHaveProperty('total');
});
it('GET /admin-api/imports → 401 without token', async () => {
await request(app.getHttpServer()).get('/admin-api/imports').expect(401);
});
it('GET /admin-api/imports/:id → 200 import detail', async () => {
if (!token) return;
const res = await request(app.getHttpServer())
.get('/admin-api/imports/test-id')
.set('Authorization', `Bearer ${token}`)
.expect(200);
expect(res.body.data).toHaveProperty('job');
});
it('POST /admin-api/imports/:id/retry → retry failed import', async () => {
if (!token) return;
const res = await request(app.getHttpServer())
.post('/admin-api/imports/test-id/retry')
.set('Authorization', `Bearer ${token}`)
.expect([200, 201]);
expect(res.body.success).toBe(true);
});
});
}); });

View File

@ -91,7 +91,7 @@ const modelNames = [
'userMembership', 'quotaUsage', 'costDailySummary', 'secretRecord', 'userMembership', 'quotaUsage', 'costDailySummary', 'secretRecord',
'secretAccessLog', 'modelRoute', 'providerConfig', 'fallbackEvent', 'secretAccessLog', 'modelRoute', 'providerConfig', 'fallbackEvent',
'violationRecord', 'contentReport', 'userDevice', 'accountDeletionRequest', 'violationRecord', 'contentReport', 'userDevice', 'accountDeletionRequest',
'workspace', 'knowledgeFolder', 'sourceReference', 'workspace', 'knowledgeFolder', 'sourceReference', 'importStepLog',
] ]
for (const name of modelNames) { for (const name of modelNames) {