feat: implement QuestionBank CRUD with pagination and template query

- Add pagination support to findAll (page, limit query params)
- Add findByTemplateId method to service
- Add GET /by-template/:templateId endpoint to controller
- Service already includes CRUD for QuestionBank and QuestionBankItem
This commit is contained in:
Developer
2026-04-23 17:19:11 +08:00
commit 0a9588abb7
492 changed files with 112453 additions and 0 deletions
+35
View File
@@ -0,0 +1,35 @@
import {
Controller,
Post,
UseGuards,
UseInterceptors,
UploadedFile,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { CombinedAuthGuard } from '../auth/combined-auth.guard';
import { OcrService } from './ocr.service';
import { I18nService } from '../i18n/i18n.service';
@Controller('ocr')
@UseGuards(CombinedAuthGuard)
@UseGuards(CombinedAuthGuard)
export class OcrController {
constructor(
private readonly ocrService: OcrService,
private readonly i18n: I18nService,
) {}
@Post('recognize')
@UseInterceptors(FileInterceptor('image'))
async recognizeText(@UploadedFile() image: Express.Multer.File) {
console.log('OCR recognition endpoint called');
if (!image) {
console.error('No image uploaded');
throw new Error(this.i18n.getMessage('noImageUploaded'));
}
console.log(`Received image. Size: ${image.size} bytes`);
const text = await this.ocrService.extractTextFromImage(image.buffer);
console.log(`OCR extraction completed. Text length: ${text.length}`);
return { text };
}
}
+10
View File
@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { OcrService } from './ocr.service';
import { OcrController } from './ocr.controller';
@Module({
controllers: [OcrController],
providers: [OcrService],
exports: [OcrService],
})
export class OcrModule {}
+80
View File
@@ -0,0 +1,80 @@
import { Injectable, Logger } from '@nestjs/common';
import { createWorker, Worker } from 'tesseract.js';
import { I18nService } from '../i18n/i18n.service';
@Injectable()
export class OcrService {
private readonly logger = new Logger(OcrService.name);
constructor(private readonly i18n: I18nService) {}
async extractTextFromImage(imageBuffer: Buffer): Promise<string> {
this.logger.log(
`Starting OCR extraction from image (${imageBuffer.length} bytes)...`,
);
// Create worker for this request to ensure stability
let worker: any = null;
try {
worker = await createWorker('chi_sim+eng+jpn');
const {
data: { text },
} = await worker.recognize(imageBuffer);
this.logger.log(
`OCR extraction completed. ${text.length} characters extracted.`,
);
if (text.length === 0) {
this.logger.warn('OCR returned empty text.');
}
await worker.terminate();
return text.trim();
} catch (error) {
this.logger.error(`OCR text extraction failed: ${error.message}`);
if (worker) {
try {
await worker.terminate();
} catch (e) {}
}
throw new Error(
this.i18n.formatMessage('ocrFailed', { message: error.message }),
);
}
}
async extractTextWithConfidence(imageBuffer: Buffer): Promise<{
text: string;
confidence: number;
}> {
this.logger.log(
`Starting OCR extraction with confidence (${imageBuffer.length} bytes)...`,
);
let worker: any = null;
try {
worker = await createWorker('chi_sim+eng+jpn');
const { data } = await worker.recognize(imageBuffer);
this.logger.log(
`OCR extraction completed. Confidence: ${data.confidence}%`,
);
await worker.terminate();
return {
text: data.text.trim(),
confidence: data.confidence,
};
} catch (error) {
this.logger.error(`OCR text extraction failed: ${error.message}`);
if (worker) {
try {
await worker.terminate();
} catch (e) {}
}
throw new Error(
this.i18n.formatMessage('ocrFailed', { message: error.message }),
);
}
}
}