← Назад к вопросам
Как реализуешь отчет по нажатию кнопки в текущем проекте?
1.0 Junior🔥 161 комментариев
#REST API и микросервисы#Soft Skills и карьера
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Реализация отчета по нажатию кнопки в проекте
Это типичный требование, где нужно:
- Обработать событие нажатия кнопки
- Собрать данные
- Создать отчет
- Отправить клиенту (скачать или показать)
Вот как я бы реализовал это в современном 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 в этом решении
- Разделение ответственности: Controller → Service → Repository → Generator
- Factory pattern: для выбора подходящего генератора
- Асинхронность опциональна: для больших отчетов можно добавить async с очередью
- Аудит: логируем все генерации отчетов
- Обработка ошибок: специфичные исключения на каждом уровне
- Валидация: проверяем входные данные
- Производительность: читаем только нужные колонки из БД
Этот подход масштабируется и легко расширяется на новые типы отчетов.