← Назад к вопросам

Как реализуешь отчет по нажатию кнопки в текущем проекте?

1.0 Junior🔥 161 комментариев
#REST API и микросервисы#Soft Skills и карьера

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Реализация отчета по нажатию кнопки в проекте

Это типичный требование, где нужно:

  1. Обработать событие нажатия кнопки
  2. Собрать данные
  3. Создать отчет
  4. Отправить клиенту (скачать или показать)

Вот как я бы реализовал это в современном Spring Boot приложении:

1. API контроллер

@RestController
@RequestMapping("/api/v1/reports")
@RequiredArgsConstructor
public class ReportController {
    
    private final ReportService reportService;
    
    // Кнопка на фронте отправляет POST запрос
    @PostMapping("/export")
    public ResponseEntity<ByteArrayResource> exportReport(
        @RequestBody ReportRequest request) throws IOException {
        
        // Валидируем входные данные
        if (request.getFromDate().isAfter(request.getToDate())) {
            throw new InvalidDateRangeException();
        }
        
        // Генерируем отчет
        byte[] reportData = reportService.generateReport(request);
        
        // Возвращаем как файл для скачивания
        ByteArrayResource resource = new ByteArrayResource(reportData);
        
        return ResponseEntity.ok()
            .header(HttpHeaders.CONTENT_DISPOSITION, 
                    "attachment; filename=\"report-" + UUID.randomUUID() + ".xlsx\"")
            .header(HttpHeaders.CONTENT_TYPE, 
                    "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
            .contentLength(reportData.length)
            .body(resource);
    }
}

@Data
@Builder
public class ReportRequest {
    private LocalDate fromDate;
    private LocalDate toDate;
    private List<String> filters; // опциональные фильтры
    private String reportType; // "summary", "detailed", etc
}

2. Service слой — бизнес-логика

@Service
@RequiredArgsConstructor
public class ReportService {
    
    private final ReportRepository reportRepository;
    private final ReportGeneratorFactory generatorFactory;
    private final ReportAuditService auditService;
    private static final Logger logger = LoggerFactory.getLogger(ReportService.class);
    
    @Transactional(readOnly = true)
    public byte[] generateReport(ReportRequest request) {
        try {
            // 1. Собираем данные из БД
            List<ReportData> data = reportRepository.findData(request);
            
            if (data.isEmpty()) {
                throw new NoDataException("No data found for the specified period");
            }
            
            // 2. Выбираем нужный генератор отчета
            ReportGenerator generator = generatorFactory
                .getGenerator(request.getReportType());
            
            // 3. Генерируем отчет
            byte[] reportBytes = generator.generate(data);
            
            // 4. Логируем для аудита
            auditService.logReportGeneration(
                request.getReportType(), 
                data.size()
            );
            
            return reportBytes;
            
        } catch (Exception e) {
            logger.error("Error generating report", e);
            throw new ReportGenerationException("Failed to generate report", e);
        }
    }
}

3. Repository слой — доступ к данным

@Repository
public class ReportRepository {
    
    private final EntityManager em;
    
    public List<ReportData> findData(ReportRequest request) {
        // Используем JPQL для эффективной загрузки данных
        return em.createQuery(
            "SELECT new com.example.domain.ReportData(" +
            "  o.id, o.number, o.amount, o.createdAt, o.status) " +
            "FROM Order o " +
            "WHERE o.createdAt BETWEEN :from AND :to " +
            "AND o.status IN :statuses " +
            "ORDER BY o.createdAt DESC",
            ReportData.class
        )
        .setParameter("from", request.getFromDate().atStartOfDay(ZoneId.of("UTC")))
        .setParameter("to", request.getToDate().plusDays(1).atStartOfDay(ZoneId.of("UTC")))
        .setParameter("statuses", request.getStatuses())
        .getResultList();
    }
}

@Data
@AllArgsConstructor
public class ReportData {
    private Long orderId;
    private String orderNumber;
    private BigDecimal amount;
    private LocalDateTime createdAt;
    private String status;
}

4. Report Generator — создание файла

Вариант 1: Отчет в XLSX (Excel)

public interface ReportGenerator {
    byte[] generate(List<ReportData> data);
}

@Component
public class ExcelReportGenerator implements ReportGenerator {
    
    @Override
    public byte[] generate(List<ReportData> data) throws IOException {
        // Используем Apache POI
        XSSFWorkbook workbook = new XSSFWorkbook();
        XSSFSheet sheet = workbook.createSheet("Orders");
        
        // Создаем заголовок
        XSSFRow headerRow = sheet.createRow(0);
        String[] headers = {"Order ID", "Order Number", "Amount", "Date", "Status"};
        for (int i = 0; i < headers.length; i++) {
            XSSFCell cell = headerRow.createCell(i);
            cell.setCellValue(headers[i]);
            cell.setCellStyle(getHeaderStyle(workbook));
        }
        
        // Заполняем данные
        int rowNum = 1;
        for (ReportData reportData : data) {
            XSSFRow row = sheet.createRow(rowNum++);
            row.createCell(0).setCellValue(reportData.getOrderId());
            row.createCell(1).setCellValue(reportData.getOrderNumber());
            row.createCell(2).setCellValue(reportData.getAmount().doubleValue());
            row.createCell(3).setCellValue(reportData.getCreatedAt().toString());
            row.createCell(4).setCellValue(reportData.getStatus());
        }
        
        // Авторазмер колонок
        for (int i = 0; i < headers.length; i++) {
            sheet.autoSizeColumn(i);
        }
        
        // Записываем в ByteArray
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        workbook.write(baos);
        workbook.close();
        
        return baos.toByteArray();
    }
    
    private CellStyle getHeaderStyle(XSSFWorkbook workbook) {
        XSSFCellStyle style = workbook.createCellStyle();
        XSSFFont font = workbook.createFont();
        font.setBold(true);
        style.setFont(font);
        return style;
    }
}

Вариант 2: Отчет в CSV

@Component
public class CsvReportGenerator implements ReportGenerator {
    
    @Override
    public byte[] generate(List<ReportData> data) throws IOException {
        StringWriter sw = new StringWriter();
        try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT
                .withHeader("Order ID", "Order Number", "Amount", "Date", "Status"))) {
            
            for (ReportData reportData : data) {
                printer.printRecord(
                    reportData.getOrderId(),
                    reportData.getOrderNumber(),
                    reportData.getAmount(),
                    reportData.getCreatedAt(),
                    reportData.getStatus()
                );
            }
        }
        
        return sw.toString().getBytes(StandardCharsets.UTF_8);
    }
}

5. Factory для выбора генератора

@Component
@RequiredArgsConstructor
public class ReportGeneratorFactory {
    
    private final ExcelReportGenerator excelGenerator;
    private final CsvReportGenerator csvGenerator;
    
    public ReportGenerator getGenerator(String reportType) {
        return switch (reportType.toLowerCase()) {
            case "excel" -> excelGenerator;
            case "csv" -> csvGenerator;
            default -> throw new IllegalArgumentException(
                "Unknown report type: " + reportType
            );
        };
    }
}

6. Аудит и мониторинг

@Service
@RequiredArgsConstructor
public class ReportAuditService {
    
    private final AuditRepository auditRepository;
    
    public void logReportGeneration(String reportType, int recordCount) {
        AuditLog log = AuditLog.builder()
            .action("REPORT_GENERATED")
            .reportType(reportType)
            .recordCount(recordCount)
            .generatedAt(LocalDateTime.now(ZoneId.of("UTC")))
            .userId(getCurrentUserId())
            .build();
        
        auditRepository.save(log);
    }
}

7. Frontend код (React/JavaScript)

// UI компонент
function ReportExporter() {
    const [loading, setLoading] = useState(false);
    const [error, setError] = useState<string | null>(null);
    
    const handleExportClick = async () => {
        setLoading(true);
        setError(null);
        
        try {
            const response = await fetch('/api/v1/reports/export', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    fromDate: '2024-01-01',
                    toDate: '2024-01-31',
                    reportType: 'excel'
                })
            });
            
            if (!response.ok) {
                throw new Error('Failed to generate report');
            }
            
            // Скачиваем файл
            const blob = await response.blob();
            const url = window.URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = `report-${Date.now()}.xlsx`;
            document.body.appendChild(a);
            a.click();
            window.URL.revokeObjectURL(url);
            
        } catch (err) {
            setError(err instanceof Error ? err.message : 'Unknown error');
        } finally {
            setLoading(false);
        }
    };
    
    return (
        <div>
            <button onClick={handleExportClick} disabled={loading}>
                {loading ? 'Generating...' : 'Export Report'}
            </button>
            {error && <div className="error">{error}</div>}
        </div>
    );
}

8. Обработка ошибок и исключения

@RestControllerAdvice
public class ReportExceptionHandler {
    
    @ExceptionHandler(InvalidDateRangeException.class)
    public ResponseEntity<ErrorResponse> handleInvalidDateRange() {
        return ResponseEntity.badRequest()
            .body(new ErrorResponse("Invalid date range"));
    }
    
    @ExceptionHandler(ReportGenerationException.class)
    public ResponseEntity<ErrorResponse> handleGenerationError() {
        return ResponseEntity.status(500)
            .body(new ErrorResponse("Failed to generate report"));
    }
}

Best Practices в этом решении

  1. Разделение ответственности: Controller → Service → Repository → Generator
  2. Factory pattern: для выбора подходящего генератора
  3. Асинхронность опциональна: для больших отчетов можно добавить async с очередью
  4. Аудит: логируем все генерации отчетов
  5. Обработка ошибок: специфичные исключения на каждом уровне
  6. Валидация: проверяем входные данные
  7. Производительность: читаем только нужные колонки из БД

Этот подход масштабируется и легко расширяется на новые типы отчетов.