ready
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user