This commit is contained in:
root
2025-10-05 02:56:17 +05:00
parent 18a363f772
commit 3caf1cce28
17 changed files with 1571 additions and 78 deletions
@@ -3,9 +3,14 @@ package kz.konturai.parser.controller;
import kz.konturai.parser.dto.ReportGenerateRequest;
import kz.konturai.parser.dto.ReportGenerationResponse;
import kz.konturai.parser.dto.ApiResponse;
import kz.konturai.parser.dto.ResearchReportRequest;
import kz.konturai.parser.dto.DeepResearchRequest;
import kz.konturai.parser.dto.DeepResearchResponse;
import kz.konturai.parser.model.ReportHistory;
import kz.konturai.parser.repository.ReportHistoryRepository;
import kz.konturai.parser.service.ReportGenerationService;
import kz.konturai.parser.service.DeepResearchService;
import kz.konturai.parser.service.ResearchPdfService;
import kz.konturai.parser.service.MinIOService;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
@@ -27,84 +32,171 @@ import kz.konturai.parser.dto.PageResponse;
@RequestMapping("/api/parser/report")
public class ReportController {
private final ReportGenerationService reportGenerationService;
private final ReportHistoryRepository reportHistoryRepository;
private final MinIOService minIOService;
private final ReportGenerationService reportGenerationService;
private final ReportHistoryRepository reportHistoryRepository;
private final MinIOService minIOService;
private final DeepResearchService deepResearchService;
private final ResearchPdfService researchPdfService;
public ReportController(ReportGenerationService reportGenerationService,
ReportHistoryRepository reportHistoryRepository,
MinIOService minIOService) {
this.reportGenerationService = reportGenerationService;
this.reportHistoryRepository = reportHistoryRepository;
this.minIOService = minIOService;
}
@PostMapping("/generate")
public ResponseEntity<ApiResponse<ReportGenerationResponse>> generate(
@Validated @RequestBody ReportGenerateRequest request) {
// Generate unique task ID
String taskId = UUID.randomUUID().toString();
// Start async report generation
reportGenerationService.generateAsync(request);
// Calculate estimated completion time (5-10 minutes for complex reports)
LocalDateTime estimatedCompletion = LocalDateTime.now().plusMinutes(8);
ReportGenerationResponse response = new ReportGenerationResponse(
taskId,
"Отчёт будет сгенерирован в фоновом режиме. Ожидаемое время завершения: "
+ estimatedCompletion.format(java.time.format.DateTimeFormatter.ofPattern("HH:mm")),
estimatedCompletion,
"PROCESSING");
return ResponseEntity.ok(ApiResponse.success("Отчёт поставлен в очередь на генерацию", response));
}
@GetMapping("/history")
public ResponseEntity<PageResponse<ReportHistory>> history(
@RequestParam(name = "page", defaultValue = "0") int page,
@RequestParam(name = "size", defaultValue = "20") int size,
@RequestParam(name = "sort", defaultValue = "createdAt,desc") String sort) {
String[] parts = sort.split(",");
String sortField = parts.length > 0 ? parts[0] : "createdAt";
Sort.Direction dir = parts.length > 1 && parts[1].equalsIgnoreCase("asc") ? Sort.Direction.ASC
: Sort.Direction.DESC;
Pageable pageable = PageRequest.of(page, size, Sort.by(dir, sortField));
Page<ReportHistory> res = reportHistoryRepository.findAll(pageable);
PageResponse<ReportHistory> dto = new PageResponse<>(
res.getContent(),
res.getNumber(),
res.getSize(),
res.getTotalElements(),
res.getTotalPages(),
sort);
return ResponseEntity.ok(dto);
}
@GetMapping("/history/{id}")
public ResponseEntity<byte[]> download(@PathVariable("id") String id) {
Optional<ReportHistory> opt = reportHistoryRepository.findById(id);
if (opt.isEmpty()) {
return ResponseEntity.notFound().build();
public ReportController(ReportGenerationService reportGenerationService,
ReportHistoryRepository reportHistoryRepository,
MinIOService minIOService,
DeepResearchService deepResearchService,
ResearchPdfService researchPdfService) {
this.reportGenerationService = reportGenerationService;
this.reportHistoryRepository = reportHistoryRepository;
this.minIOService = minIOService;
this.deepResearchService = deepResearchService;
this.researchPdfService = researchPdfService;
}
ReportHistory rh = opt.get();
try {
// Download from MinIO
java.io.InputStream inputStream = minIOService.downloadFile(rh.getFilePath());
byte[] bytes = inputStream.readAllBytes();
inputStream.close();
MediaType mt = rh.getFormat() != null && rh.getFormat().equalsIgnoreCase("DOCX")
? MediaType
.parseMediaType("application/vnd.openxmlformats-officedocument.wordprocessingml.document")
: MediaType.APPLICATION_PDF;
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + rh.getFilename() + "\"")
.contentType(mt)
.body(bytes);
} catch (Exception e) {
return ResponseEntity.internalServerError().build();
@PostMapping("/generate")
public ResponseEntity<ApiResponse<ReportGenerationResponse>> generate(
@Validated @RequestBody ReportGenerateRequest request) {
// Generate unique task ID
String taskId = UUID.randomUUID().toString();
// Start async report generation
reportGenerationService.generateAsync(request);
// Calculate estimated completion time (5-10 minutes for complex reports)
LocalDateTime estimatedCompletion = LocalDateTime.now().plusMinutes(8);
ReportGenerationResponse response = new ReportGenerationResponse(
taskId,
"Отчёт будет сгенерирован в фоновом режиме. Ожидаемое время завершения: "
+ estimatedCompletion.format(
java.time.format.DateTimeFormatter.ofPattern("HH:mm")),
estimatedCompletion,
"PROCESSING");
return ResponseEntity.ok(ApiResponse.success("Отчёт поставлен в очередь на генерацию", response));
}
@PostMapping
public ResponseEntity<byte[]> generateResearchReport(@Validated @RequestBody ResearchReportRequest request) {
try {
// Валидация входных данных
if (request.getQuery() == null || request.getQuery().trim().isEmpty()) {
return ResponseEntity.badRequest().build();
}
// Подготовка запроса для deep-research API
DeepResearchRequest deepRequest = new DeepResearchRequest(
request.getQuery(),
request.getLang(),
request.getDepth(),
request.getBreadth(),
request.getReportType());
// Вызов deep-research API
DeepResearchResponse researchResponse = deepResearchService.generateReport(deepRequest);
if (researchResponse == null || researchResponse.getMainContent().trim().isEmpty()) {
return ResponseEntity.internalServerError().build();
}
// Генерация PDF
byte[] pdfBytes = researchPdfService.generatePdfReport(request.getQuery(), researchResponse);
// Формирование имени файла
String filename = "research_report_" + System.currentTimeMillis() + ".pdf";
// Сохранение в историю отчётов
saveResearchReportToHistory(request, filename, pdfBytes, researchResponse);
// Возврат PDF файла
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + filename + "\"")
.contentType(MediaType.APPLICATION_PDF)
.body(pdfBytes);
} catch (Exception e) {
// Логирование ошибки
System.err.println("Error generating research report: " + e.getMessage());
e.printStackTrace();
// Возврат ошибки в зависимости от типа исключения
if (e.getMessage() != null && e.getMessage().contains("timeout")) {
return ResponseEntity.status(504).build(); // Gateway Timeout
} else {
return ResponseEntity.status(500).build(); // Internal Server Error
}
}
}
private void saveResearchReportToHistory(ResearchReportRequest request, String filename,
byte[] pdfBytes, DeepResearchResponse researchResponse) {
try {
// Загрузка в MinIO
minIOService.uploadFile(filename, pdfBytes, MediaType.APPLICATION_PDF.toString());
// Сохранение в базу данных
ReportHistory reportHistory = new ReportHistory();
reportHistory.setReportTitle(request.getQuery());
reportHistory.setAuthorName("AI Research Agent");
reportHistory.setCompanyName("Deep Research Service");
reportHistory.setStartDate(LocalDateTime.now());
reportHistory.setEndDate(LocalDateTime.now());
reportHistory.setFormat("PDF");
reportHistory.setFilename(filename);
reportHistory.setFilePath(filename);
reportHistory.setFileSize(pdfBytes.length);
reportHistory.setCreatedAt(LocalDateTime.now());
reportHistoryRepository.save(reportHistory);
} catch (Exception e) {
System.err.println("Error saving research report to history: " + e.getMessage());
// Не прерываем выполнение, если не удалось сохранить в историю
}
}
@GetMapping("/history")
public ResponseEntity<PageResponse<ReportHistory>> history(
@RequestParam(name = "page", defaultValue = "0") int page,
@RequestParam(name = "size", defaultValue = "20") int size,
@RequestParam(name = "sort", defaultValue = "createdAt,desc") String sort) {
String[] parts = sort.split(",");
String sortField = parts.length > 0 ? parts[0] : "createdAt";
Sort.Direction dir = parts.length > 1 && parts[1].equalsIgnoreCase("asc") ? Sort.Direction.ASC
: Sort.Direction.DESC;
Pageable pageable = PageRequest.of(page, size, Sort.by(dir, sortField));
Page<ReportHistory> res = reportHistoryRepository.findAll(pageable);
PageResponse<ReportHistory> dto = new PageResponse<>(
res.getContent(),
res.getNumber(),
res.getSize(),
res.getTotalElements(),
res.getTotalPages(),
sort);
return ResponseEntity.ok(dto);
}
@GetMapping("/history/{id}")
public ResponseEntity<byte[]> download(@PathVariable("id") String id) {
Optional<ReportHistory> opt = reportHistoryRepository.findById(id);
if (opt.isEmpty()) {
return ResponseEntity.notFound().build();
}
ReportHistory rh = opt.get();
try {
// Download from MinIO
java.io.InputStream inputStream = minIOService.downloadFile(rh.getFilePath());
byte[] bytes = inputStream.readAllBytes();
inputStream.close();
MediaType mt = rh.getFormat() != null && rh.getFormat().equalsIgnoreCase("DOCX")
? MediaType
.parseMediaType("application/vnd.openxmlformats-officedocument.wordprocessingml.document")
: MediaType.APPLICATION_PDF;
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + rh.getFilename() + "\"")
.contentType(mt)
.body(bytes);
} catch (Exception e) {
return ResponseEntity.internalServerError().build();
}
}
}
}
@@ -0,0 +1,72 @@
package kz.konturai.parser.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
public class DeepResearchRequest {
@JsonProperty("query")
private String query;
@JsonProperty("lang")
private String lang = "ru";
@JsonProperty("depth")
private Integer depth = 3;
@JsonProperty("breadth")
private Integer breadth = 5;
@JsonProperty("report_type")
private String reportType = "report";
public DeepResearchRequest() {
}
public DeepResearchRequest(String query, String lang, Integer depth, Integer breadth, String reportType) {
this.query = query;
this.lang = lang;
this.depth = depth;
this.breadth = breadth;
this.reportType = reportType;
}
public String getQuery() {
return query;
}
public void setQuery(String query) {
this.query = query;
}
public String getLang() {
return lang;
}
public void setLang(String lang) {
this.lang = lang;
}
public Integer getDepth() {
return depth;
}
public void setDepth(Integer depth) {
this.depth = depth;
}
public Integer getBreadth() {
return breadth;
}
public void setBreadth(Integer breadth) {
this.breadth = breadth;
}
public String getReportType() {
return reportType;
}
public void setReportType(String reportType) {
this.reportType = reportType;
}
}
@@ -0,0 +1,75 @@
package kz.konturai.parser.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
public class DeepResearchResponse {
@JsonProperty("report")
private String report;
@JsonProperty("answer")
private String answer;
@JsonProperty("visitedUrls")
private List<String> visitedUrls;
@JsonProperty("status")
private String status;
@JsonProperty("error")
private String error;
public DeepResearchResponse() {
}
public String getReport() {
return report;
}
public void setReport(String report) {
this.report = report;
}
public String getAnswer() {
return answer;
}
public void setAnswer(String answer) {
this.answer = answer;
}
public List<String> getVisitedUrls() {
return visitedUrls;
}
public void setVisitedUrls(List<String> visitedUrls) {
this.visitedUrls = visitedUrls;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getError() {
return error;
}
public void setError(String error) {
this.error = error;
}
/**
* Получить основной текст отчёта (report или answer)
*/
public String getMainContent() {
if (report != null && !report.trim().isEmpty()) {
return report;
}
return answer != null ? answer : "";
}
}
@@ -10,6 +10,7 @@ public class ReportGenerateRequest {
private LocalDateTime startDate;
private LocalDateTime endDate;
private String format; // PDF or DOCX
private String recipientEmail; // Email для отправки отчета
public String getReportTitle() {
return reportTitle;
@@ -58,4 +59,12 @@ public class ReportGenerateRequest {
public void setFormat(String format) {
this.format = format;
}
public String getRecipientEmail() {
return recipientEmail;
}
public void setRecipientEmail(String recipientEmail) {
this.recipientEmail = recipientEmail;
}
}
@@ -0,0 +1,74 @@
package kz.konturai.parser.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Max;
public class ResearchReportRequest {
@NotBlank(message = "Query is required")
private String query;
private String lang = "ru";
@Min(value = 1, message = "Depth must be between 1 and 5")
@Max(value = 5, message = "Depth must be between 1 and 5")
private Integer depth = 3;
@Min(value = 2, message = "Breadth must be between 2 and 10")
@Max(value = 10, message = "Breadth must be between 2 and 10")
private Integer breadth = 5;
private String reportType = "report";
public ResearchReportRequest() {
}
public ResearchReportRequest(String query, String lang, Integer depth, Integer breadth, String reportType) {
this.query = query;
this.lang = lang;
this.depth = depth;
this.breadth = breadth;
this.reportType = reportType;
}
public String getQuery() {
return query;
}
public void setQuery(String query) {
this.query = query;
}
public String getLang() {
return lang;
}
public void setLang(String lang) {
this.lang = lang;
}
public Integer getDepth() {
return depth;
}
public void setDepth(Integer depth) {
this.depth = depth;
}
public Integer getBreadth() {
return breadth;
}
public void setBreadth(Integer breadth) {
this.breadth = breadth;
}
public String getReportType() {
return reportType;
}
public void setReportType(String reportType) {
this.reportType = reportType;
}
}
@@ -0,0 +1,66 @@
package kz.konturai.parser.service;
import kz.konturai.parser.dto.DeepResearchRequest;
import kz.konturai.parser.dto.DeepResearchResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import reactor.core.publisher.Mono;
import java.time.Duration;
@Service
public class DeepResearchService {
private final WebClient webClient;
@Value("${deep-research.api.url:http://185.35.223.45:3051}")
private String apiUrl;
@Value("${deep-research.api.timeout:300000}") // 5 minutes default timeout
private long timeoutMs;
public DeepResearchService() {
this.webClient = WebClient.builder()
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
.build();
}
/**
* Вызывает deep-research API для генерации отчёта
*/
public DeepResearchResponse generateReport(DeepResearchRequest request) {
try {
return webClient.post()
.uri(apiUrl + "/api/research")
.bodyValue(request)
.retrieve()
.bodyToMono(DeepResearchResponse.class)
.timeout(Duration.ofMillis(timeoutMs))
.block();
} catch (WebClientResponseException e) {
throw new RuntimeException("Deep research API error: " + e.getResponseBodyAsString(), e);
} catch (Exception e) {
throw new RuntimeException("Failed to call deep research API", e);
}
}
/**
* Асинхронный вызов deep-research API
*/
public Mono<DeepResearchResponse> generateReportAsync(DeepResearchRequest request) {
return webClient.post()
.uri(apiUrl + "/api/research")
.bodyValue(request)
.retrieve()
.bodyToMono(DeepResearchResponse.class)
.timeout(Duration.ofMillis(timeoutMs))
.onErrorMap(WebClientResponseException.class,
e -> new RuntimeException("Deep research API error: " + e.getResponseBodyAsString(), e))
.onErrorMap(Exception.class, e -> new RuntimeException("Failed to call deep research API", e));
}
}
@@ -0,0 +1,119 @@
package kz.konturai.parser.service;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@Service
public class EmailService {
private final JavaMailSender mailSender;
public EmailService(JavaMailSender mailSender) {
this.mailSender = mailSender;
}
/**
* Отправляет PDF отчет на указанный email адрес
*
* @param recipientEmail email получателя
* @param reportTitle название отчета
* @param authorName имя автора
* @param companyName название компании
* @param pdfBytes содержимое PDF файла
* @param filename имя файла
*/
public void sendReportByEmail(String recipientEmail, String reportTitle,
String authorName, String companyName,
byte[] pdfBytes, String filename) {
try {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
// Настройка получателя и отправителя
helper.setTo(recipientEmail);
helper.setFrom("noreply@konturai.kz"); // Можно настроить в конфигурации
// Тема письма
String subject = String.format("Аналитический отчет: %s",
reportTitle != null ? reportTitle : "Отчет");
helper.setSubject(subject);
// Текст письма
String emailBody = buildEmailBody(reportTitle, authorName, companyName);
helper.setText(emailBody, true); // true означает HTML формат
// Прикрепляем PDF файл
ByteArrayResource pdfResource = new ByteArrayResource(pdfBytes) {
@Override
public String getFilename() {
return filename;
}
};
helper.addAttachment(filename, pdfResource, "application/pdf");
// Отправляем письмо
mailSender.send(message);
System.out.println("Email с отчетом успешно отправлен на: " + recipientEmail);
} catch (MessagingException e) {
System.err.println("Ошибка при отправке email: " + e.getMessage());
e.printStackTrace();
}
}
/**
* Создает HTML содержимое письма
*/
private String buildEmailBody(String reportTitle, String authorName, String companyName) {
String currentDate = LocalDateTime.now().format(DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm"));
return String.format(
"""
<html>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
<h2 style="color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 10px;">
Аналитический отчет готов
</h2>
<p>Здравствуйте!</p>
<p>Ваш аналитический отчет успешно сгенерирован и готов к просмотру.</p>
<div style="background-color: #f8f9fa; padding: 15px; border-left: 4px solid #3498db; margin: 20px 0;">
<h3 style="margin-top: 0; color: #2c3e50;">Детали отчета:</h3>
<p><strong>Название:</strong> %s</p>
<p><strong>Автор:</strong> %s</p>
<p><strong>Компания:</strong> %s</p>
<p><strong>Дата создания:</strong> %s</p>
</div>
<p>PDF файл с отчетом прикреплен к данному письму. Вы можете скачать его и сохранить для дальнейшего использования.</p>
<p>Если у вас возникли вопросы или нужна дополнительная информация, пожалуйста, свяжитесь с нами.</p>
<hr style="margin: 30px 0; border: none; border-top: 1px solid #eee;">
<p style="font-size: 12px; color: #666;">
С уважением,<br>
Команда Konturai Analytics<br>
<em>Это письмо отправлено автоматически, пожалуйста, не отвечайте на него.</em>
</p>
</div>
</body>
</html>
""",
reportTitle != null ? reportTitle : "Не указано",
authorName != null ? authorName : "Не указан",
companyName != null ? companyName : "Не указана",
currentDate);
}
}
@@ -85,15 +85,18 @@ public class ReportGenerationService {
private final OllamaAnalyticsService ollamaAnalyticsService;
private final ReportHistoryRepository reportHistoryRepository;
private final MinIOService minIOService;
private final EmailService emailService;
public ReportGenerationService(MarketItemRepository marketItemRepository,
OllamaAnalyticsService ollamaAnalyticsService,
ReportHistoryRepository reportHistoryRepository,
MinIOService minIOService) {
MinIOService minIOService,
EmailService emailService) {
this.marketItemRepository = marketItemRepository;
this.ollamaAnalyticsService = ollamaAnalyticsService;
this.reportHistoryRepository = reportHistoryRepository;
this.minIOService = minIOService;
this.emailService = emailService;
}
public ReportBinary simpleGenerate(ReportGenerateRequest req) {
@@ -135,7 +138,8 @@ public class ReportGenerationService {
public void generateAsync(ReportGenerateRequest req) {
try {
generate(req);
// Report is automatically saved to database and MinIO in the generate() method
// Report is automatically saved to database, MinIO and sent by email in the
// generate() method
} catch (Exception e) {
// Log error but don't throw to avoid async method issues
System.err.println("Error generating report asynchronously: " + e.getMessage());
@@ -165,6 +169,24 @@ public class ReportGenerationService {
rb = new ReportBinary(filename, MediaType.APPLICATION_PDF, pdf);
}
persistReport(req, rb);
// Отправляем PDF на email, если указан email и формат PDF
if (req.getRecipientEmail() != null && !req.getRecipientEmail().trim().isEmpty()
&& "PDF".equalsIgnoreCase(req.getFormat())) {
try {
emailService.sendReportByEmail(
req.getRecipientEmail(),
req.getReportTitle(),
req.getAuthorName(),
req.getCompanyName(),
rb.getBytes(),
rb.getFilename());
} catch (Exception e) {
System.err.println("Ошибка при отправке email: " + e.getMessage());
// Не прерываем выполнение, если email не отправился
}
}
return rb;
}
@@ -0,0 +1,203 @@
package kz.konturai.parser.service;
import com.lowagie.text.*;
import com.lowagie.text.pdf.PdfWriter;
import kz.konturai.parser.dto.DeepResearchResponse;
import org.springframework.stereotype.Service;
import java.io.ByteArrayOutputStream;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
@Service
public class ResearchPdfService {
private static final Font TITLE_FONT = new Font(Font.HELVETICA, 18, Font.BOLD);
private static final Font HEADING_FONT = new Font(Font.HELVETICA, 14, Font.BOLD);
private static final Font SUBHEADING_FONT = new Font(Font.HELVETICA, 12, Font.BOLD);
private static final Font NORMAL_FONT = new Font(Font.HELVETICA, 10, Font.NORMAL);
private static final Font SMALL_FONT = new Font(Font.HELVETICA, 8, Font.NORMAL);
/**
* Генерирует PDF отчёт на основе ответа от deep-research API
*/
public byte[] generatePdfReport(String query, DeepResearchResponse researchResponse) {
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
Document document = new Document(PageSize.A4);
PdfWriter.getInstance(document, outputStream);
document.open();
// Титульная страница
addTitlePage(document, query);
// Содержание
addTableOfContents(document);
// Основной отчёт
addMainReport(document, researchResponse);
// Список источников
addSources(document, researchResponse.getVisitedUrls());
document.close();
return outputStream.toByteArray();
} catch (Exception e) {
throw new RuntimeException("Failed to generate PDF report", e);
}
}
private void addTitlePage(Document document, String query) throws DocumentException {
// Заголовок
Paragraph title = new Paragraph(query, TITLE_FONT);
title.setAlignment(Element.ALIGN_CENTER);
title.setSpacingAfter(50);
document.add(title);
// Подзаголовок
Paragraph subtitle = new Paragraph("Исследовательский отчёт", HEADING_FONT);
subtitle.setAlignment(Element.ALIGN_CENTER);
subtitle.setSpacingAfter(30);
document.add(subtitle);
// Информация о дате
String currentDate = LocalDateTime.now().format(DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm"));
Paragraph date = new Paragraph("Дата создания: " + currentDate, NORMAL_FONT);
date.setAlignment(Element.ALIGN_CENTER);
date.setSpacingAfter(100);
document.add(date);
// Пустая строка для перехода на следующую страницу
document.add(new Paragraph(" "));
document.newPage();
}
private void addTableOfContents(Document document) throws DocumentException {
Paragraph tocTitle = new Paragraph("Содержание", HEADING_FONT);
tocTitle.setSpacingAfter(20);
document.add(tocTitle);
// Автоматически генерируемое содержание
String[] tocItems = {
"1. Введение",
"2. Основная часть",
"3. Заключение",
"4. Источники"
};
for (String item : tocItems) {
Paragraph tocItem = new Paragraph(item, NORMAL_FONT);
tocItem.setSpacingAfter(5);
document.add(tocItem);
}
document.add(new Paragraph(" "));
document.newPage();
}
private void addMainReport(Document document, DeepResearchResponse researchResponse) throws DocumentException {
String content = researchResponse.getMainContent();
if (content == null || content.trim().isEmpty()) {
content = "Отчёт не был сгенерирован.";
}
// Разбиваем контент на параграфы
String[] paragraphs = content.split("\n\n");
// Введение
Paragraph introTitle = new Paragraph("1. Введение", HEADING_FONT);
introTitle.setSpacingAfter(10);
document.add(introTitle);
Paragraph intro = new Paragraph(
"Данный отчёт представляет результаты исследования по теме: \"" +
researchResponse.getReport() != null ? "Полный отчёт"
: "Краткий ответ" + "\". " +
"Исследование проводилось с использованием автоматизированных инструментов анализа данных.",
NORMAL_FONT);
intro.setSpacingAfter(15);
document.add(intro);
// Основная часть
Paragraph mainTitle = new Paragraph("2. Основная часть", HEADING_FONT);
mainTitle.setSpacingAfter(10);
document.add(mainTitle);
// Добавляем основной контент
for (String paragraph : paragraphs) {
if (paragraph.trim().isEmpty())
continue;
// Проверяем, является ли строка заголовком (начинается с цифры или содержит
// ключевые слова)
if (isHeading(paragraph)) {
Paragraph heading = new Paragraph(paragraph, SUBHEADING_FONT);
heading.setSpacingAfter(5);
document.add(heading);
} else {
Paragraph para = new Paragraph(paragraph, NORMAL_FONT);
para.setSpacingAfter(8);
document.add(para);
}
}
// Заключение
Paragraph conclusionTitle = new Paragraph("3. Заключение", HEADING_FONT);
conclusionTitle.setSpacingAfter(10);
document.add(conclusionTitle);
Paragraph conclusion = new Paragraph(
"Исследование завершено. Полученные данные и выводы представлены в основной части отчёта. " +
"Для получения дополнительной информации рекомендуется обратиться к первоисточникам, " +
"указанным в разделе \"Источники\".",
NORMAL_FONT);
conclusion.setSpacingAfter(15);
document.add(conclusion);
}
private void addSources(Document document, List<String> visitedUrls) throws DocumentException {
Paragraph sourcesTitle = new Paragraph("4. Источники", HEADING_FONT);
sourcesTitle.setSpacingAfter(10);
document.add(sourcesTitle);
if (visitedUrls == null || visitedUrls.isEmpty()) {
Paragraph noSources = new Paragraph("Источники не указаны.", NORMAL_FONT);
document.add(noSources);
return;
}
for (int i = 0; i < visitedUrls.size(); i++) {
String url = visitedUrls.get(i);
Paragraph source = new Paragraph(
(i + 1) + ". " + url,
SMALL_FONT);
source.setSpacingAfter(3);
document.add(source);
}
}
private boolean isHeading(String text) {
if (text == null || text.trim().isEmpty())
return false;
String trimmed = text.trim();
// Проверяем, начинается ли с цифры и точки
if (trimmed.matches("^\\d+\\.\\s+.*"))
return true;
// Проверяем ключевые слова заголовков
String[] headingKeywords = {
"введение", "заключение", "выводы", "рекомендации", "анализ",
"результаты", "методология", "цель", "задачи", "основные", "ключевые"
};
String lowerText = trimmed.toLowerCase();
for (String keyword : headingKeywords) {
if (lowerText.startsWith(keyword))
return true;
}
return false;
}
}
+13
View File
@@ -50,3 +50,16 @@ logging.level.com.mongodb=WARN
ollama.host=http://185.35.223.45:11434
ollama.model=gemma3:1b
logging.level.kz.konturai.parser.service.OllamaAnalyticsService=INFO
# Email Configuration
spring.mail.host=smtp.gmail.com
spring.mail.port=587
spring.mail.username=your-email@gmail.com
spring.mail.password=your-app-password
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true
# Deep Research API Configuration
deep-research.api.url=http://185.35.223.45:3051
deep-research.api.timeout=300000
@@ -0,0 +1,91 @@
package kz.konturai.parser.service;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.mail.javamail.JavaMailSender;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class EmailServiceTest {
@Mock
private JavaMailSender mailSender;
@Mock
private MimeMessage mimeMessage;
private EmailService emailService;
@BeforeEach
void setUp() {
emailService = new EmailService(mailSender);
}
@Test
void testSendReportByEmail() throws MessagingException {
// Arrange
String recipientEmail = "test@example.com";
String reportTitle = "Test Report";
String authorName = "Test Author";
String companyName = "Test Company";
byte[] pdfBytes = "test pdf content".getBytes();
String filename = "test-report.pdf";
when(mailSender.createMimeMessage()).thenReturn(mimeMessage);
// Act
emailService.sendReportByEmail(recipientEmail, reportTitle, authorName, companyName, pdfBytes, filename);
// Assert
verify(mailSender, times(1)).createMimeMessage();
verify(mailSender, times(1)).send(any(MimeMessage.class));
}
@Test
void testSendReportByEmailWithNullValues() throws MessagingException {
// Arrange
String recipientEmail = "test@example.com";
String reportTitle = null;
String authorName = null;
String companyName = null;
byte[] pdfBytes = "test pdf content".getBytes();
String filename = "test-report.pdf";
when(mailSender.createMimeMessage()).thenReturn(mimeMessage);
// Act
emailService.sendReportByEmail(recipientEmail, reportTitle, authorName, companyName, pdfBytes, filename);
// Assert
verify(mailSender, times(1)).createMimeMessage();
verify(mailSender, times(1)).send(any(MimeMessage.class));
}
@Test
void testSendReportByEmailWithMessagingException() throws MessagingException {
// Arrange
String recipientEmail = "test@example.com";
String reportTitle = "Test Report";
String authorName = "Test Author";
String companyName = "Test Company";
byte[] pdfBytes = "test pdf content".getBytes();
String filename = "test-report.pdf";
when(mailSender.createMimeMessage()).thenThrow(new MessagingException("SMTP error"));
// Act
emailService.sendReportByEmail(recipientEmail, reportTitle, authorName, companyName, pdfBytes, filename);
// Assert
verify(mailSender, times(1)).createMimeMessage();
verify(mailSender, never()).send(any(MimeMessage.class));
}
}