/
viktorphp
/
back-socket
Обзор
Документация
Войти
/
viktorphp
/
back-socket
Код
Запросы
0
Задачи
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
src/controllers/diary/diary.controller.ts
320 строк
10 KB
Иванов Виктор Евгеньевич
[feature-diary]: добавить сохранение отредактированного OCR
12 апр 2026, 20:08
12 апр 2026, 20:08
1c5d24c
Код
Авторство
О чём код?
import { Controller, Get, Post, Put, Patch, Delete, Param, Body, Query, UploadedFiles, UseInterceptors, UseGuards, Request, HttpCode, HttpStatus, BadRequestException, } from '@nestjs/common'; import { FilesInterceptor, FileInterceptor } from '@nestjs/platform-express'; import { UploadedFile } from '@nestjs/common'; import { memoryStorage } from 'multer'; import { DiaryService } from 'src/services/diary/diary.service'; import { CreateDiaryEntryDto } from 'src/dto/diary/create-diary-entry.dto'; import { UpdateDiaryEntryDto } from 'src/dto/diary/update-diary-entry.dto'; import { SessionGuard } from 'src/auth/session.guard'; /** * DiaryController — HTTP-контроллер для работы с дневником. * * Все эндпоинты защищены SessionGuard — доступны только авторизованным пользователям. * ID пользователя извлекается из req.user (заполняется SessionGuard через passport). * * Базовый путь: /diary */ @Controller('diary') @UseGuards(SessionGuard) // применяем guard ко всем эндпоинтам контроллера export class DiaryController { constructor(private readonly diaryService: DiaryService) {} // ───────────────────────────────────────────── // Записи дневника // ───────────────────────────────────────────── /** * POST /diary/entries * Создаёт новую запись дневника. * * Принимает multipart/form-data: * - date: string (YYYY-MM-DD) — обязательно * - text: string — опционально * - files: File[] — опционально, любые файлы * * FilesInterceptor('files') — multer middleware, который читает файлы из поля 'files'. * memoryStorage() — хранит файлы в памяти (Buffer), а не на диске контейнера. */ @Post('entries') @HttpCode(HttpStatus.CREATED) @UseInterceptors( FilesInterceptor('files', undefined, { storage: memoryStorage() }), ) async createEntry( @Request() req: any, @Body() dto: CreateDiaryEntryDto, @UploadedFiles() files?: Express.Multer.File[], ) { const userId = req.user.id; const entry = await this.diaryService.createEntry( userId, dto.date, dto.text, files, ); return { status: HttpStatus.CREATED, message: 'Запись создана', entry, }; } /** * GET /diary/entries * Возвращает список записей пользователя. * * Query-параметры (все опциональны): * - date: YYYY-MM-DD — конкретная дата (приоритет над from/to) * - from: YYYY-MM-DD — начало диапазона * - to: YYYY-MM-DD — конец диапазона * * Примеры: * GET /diary/entries → все записи * GET /diary/entries?date=2025-04-11 → записи за 11 апреля * GET /diary/entries?from=2025-04-01&to=2025-04-30 → записи за апрель */ @Get('entries') async getEntries( @Request() req: any, @Query('date') date?: string, @Query('from') from?: string, @Query('to') to?: string, ) { const userId = req.user.id; const entries = await this.diaryService.getEntries(userId, from, to, date); return { status: HttpStatus.OK, entries, }; } /** * GET /diary/entries/:id * Возвращает одну запись со всеми прикреплёнными файлами. */ @Get('entries/:id') async getEntry(@Request() req: any, @Param('id') id: string) { const entry = await this.diaryService.getEntry(id, req.user.id); return { status: HttpStatus.OK, entry, }; } /** * PUT /diary/entries/:id * Обновляет текст существующей записи. * Принимает JSON: { "text": "новый текст" } */ @Put('entries/:id') async updateEntry( @Request() req: any, @Param('id') id: string, @Body() dto: UpdateDiaryEntryDto, ) { const entry = await this.diaryService.updateEntry( id, req.user.id, dto.text, ); return { status: HttpStatus.OK, message: 'Запись обновлена', entry, }; } /** * DELETE /diary/entries/:id * Удаляет запись и все прикреплённые файлы (из БД и Object Storage). */ @Delete('entries/:id') @HttpCode(HttpStatus.OK) async deleteEntry(@Request() req: any, @Param('id') id: string) { await this.diaryService.deleteEntry(id, req.user.id); return { status: HttpStatus.OK, message: 'Запись удалена', }; } // ───────────────────────────────────────────── // Файлы // ───────────────────────────────────────────── /** * POST /diary/entries/:id/files * Добавляет файлы к существующей записи. * Принимает multipart/form-data с полем 'files'. */ @Post('entries/:id/files') @HttpCode(HttpStatus.CREATED) @UseInterceptors( FilesInterceptor('files', undefined, { storage: memoryStorage() }), ) async addFiles( @Request() req: any, @Param('id') entryId: string, @UploadedFiles() files: Express.Multer.File[], ) { if (!files || files.length === 0) { throw new BadRequestException('Файлы не переданы'); } // Проверяем, что запись существует и принадлежит пользователю await this.diaryService.getEntry(entryId, req.user.id); const savedFiles = await this.diaryService.uploadFiles( req.user.id, entryId, files, ); return { status: HttpStatus.CREATED, message: 'Файлы загружены', files: savedFiles, }; } /** * PUT /diary/files/:fileId * Заменяет файл в записи дневника. * * Атомарная операция — за один запрос: * 1. Удаляет старый файл из Object Storage * 2. Загружает новый файл * 3. Обновляет метаданные в БД (имя, тип, ключ хранилища) * 4. Сбрасывает OCR-статус в 'none' (файл изменился — старый результат неактуален) * * Принимает multipart/form-data с одним файлом в поле 'file'. */ @Put('files/:fileId') @UseInterceptors(FileInterceptor('file', { storage: memoryStorage() })) async replaceFile( @Request() req: any, @Param('fileId') fileId: string, @UploadedFile() file: Express.Multer.File, ) { if (!file) { throw new BadRequestException('Файл не передан'); } const updatedFile = await this.diaryService.replaceFile( fileId, req.user.id, file, ); return { status: HttpStatus.OK, message: 'Файл заменён', file: updatedFile, }; } /** * PATCH /diary/files/:fileId/ocr * Обновляет OCR-текст файла вручную (после редактирования пользователем). * Принимает JSON: { "ocrText": "текст" } */ @Patch('files/:fileId/ocr') async updateOcrText( @Request() req: any, @Param('fileId') fileId: string, @Body('ocrText') ocrText: string, ) { const updatedFile = await this.diaryService.updateOcrText( fileId, req.user.id, ocrText, ); return { status: HttpStatus.OK, message: 'OCR-текст обновлён', file: updatedFile, }; } /** * DELETE /diary/files/:fileId * Удаляет один файл из записи (из БД и Object Storage). */ @Delete('files/:fileId') @HttpCode(HttpStatus.OK) async deleteFile(@Request() req: any, @Param('fileId') fileId: string) { await this.diaryService.deleteFile(fileId, req.user.id); return { status: HttpStatus.OK, message: 'Файл удалён', }; } /** * GET /diary/files/:fileId/url * Возвращает временную подписанную ссылку на скачивание файла. * Ссылка действительна 1 час. * * Фронт должен использовать эту ссылку для отображения изображений * или скачивания файлов — файлы хранятся в приватном бакете. */ @Get('files/:fileId/url') async getFileUrl(@Request() req: any, @Param('fileId') fileId: string) { const url = await this.diaryService.getFileUrl(fileId, req.user.id); return { status: HttpStatus.OK, url, }; } // ───────────────────────────────────────────── // OCR // ───────────────────────────────────────────── /** * POST /diary/files/:fileId/ocr * Запускает OCR-распознавание для файла. * * Метод немедленно возвращает { status: 'pending' } — распознавание идёт в фоне. * Результат придёт через WebSocket событие 'ocrComplete' (namespace /diary). * * Поддерживаемые типы: изображения (jpeg, png, gif, bmp, tiff, webp) и PDF. */ @Post('files/:fileId/ocr') @HttpCode(HttpStatus.ACCEPTED) async startOcr(@Request() req: any, @Param('fileId') fileId: string) { await this.diaryService.startOcr(fileId, req.user.id); return { status: HttpStatus.ACCEPTED, message: 'Распознавание запущено. Результат придёт через WebSocket (событие ocrComplete)', }; } }