From a629c82fdf41d8d7066098ccc443747ce4651223 Mon Sep 17 00:00:00 2001 From: arys Date: Fri, 3 Apr 2026 17:28:59 +0500 Subject: [PATCH] target fix --- .../controller/PublicAssetController.java | 78 +++++++++++++++++++ .../TargetingCampaignController.java | 15 ++-- .../service/TargetingCampaignService.java | 10 ++- 3 files changed, 95 insertions(+), 8 deletions(-) create mode 100644 src/main/java/kz/konturai/parser/controller/PublicAssetController.java diff --git a/src/main/java/kz/konturai/parser/controller/PublicAssetController.java b/src/main/java/kz/konturai/parser/controller/PublicAssetController.java new file mode 100644 index 0000000..ce0af39 --- /dev/null +++ b/src/main/java/kz/konturai/parser/controller/PublicAssetController.java @@ -0,0 +1,78 @@ +package kz.konturai.parser.controller; + +import kz.konturai.parser.service.MinIOService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.io.InputStreamResource; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; + +import java.io.InputStream; + +/** + * Controller for public access to marketing images and videos at the root level. + * This handles requests to https://api.konturai.kz/image_*.png or video_*.mp4. + */ +@RestController +@RequiredArgsConstructor +@Slf4j +public class PublicAssetController { + + private final MinIOService minIOService; + + /** + * Serves images that start with "image_" and end with ".png" from the root. + * Example: https://api.konturai.kz/image_1772616225367_1567876836.png + */ + @GetMapping("/{filename:image_.+\\.png}") + public ResponseEntity getPublicImage(@PathVariable String filename) { + log.info("[PublicAsset] Access request for image: {}", filename); + try { + if (!minIOService.fileExists(filename)) { + return ResponseEntity.notFound().build(); + } + + try (InputStream is = minIOService.downloadFile(filename)) { + byte[] bytes = is.readAllBytes(); + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_PNG_VALUE) + .header(HttpHeaders.CACHE_CONTROL, "public, max-age=3600") + .body(bytes); + } + } catch (Exception e) { + log.error("[PublicAsset] Failed to serve public image {}: {}", filename, e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } + + /** + * Serves videos that start with "video_" and end with ".mp4" from the root. + * Example: https://api.konturai.kz/video_1772616225367_1567876836.mp4 + */ + @GetMapping("/{filename:video_.+\\.mp4}") + public ResponseEntity getPublicVideo(@PathVariable String filename) { + log.info("[PublicAsset] Access request for video: {}", filename); + try { + if (!minIOService.fileExists(filename)) { + return ResponseEntity.notFound().build(); + } + + InputStream is = minIOService.downloadFile(filename); + InputStreamResource resource = new InputStreamResource(is); + + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_TYPE, "video/mp4") + .header(HttpHeaders.CACHE_CONTROL, "public, max-age=3600") + .body(resource); + } catch (Exception e) { + log.error("[PublicAsset] Failed to serve public video {}: {}", filename, e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } +} diff --git a/src/main/java/kz/konturai/parser/controller/TargetingCampaignController.java b/src/main/java/kz/konturai/parser/controller/TargetingCampaignController.java index b56a500..bf4ecd5 100644 --- a/src/main/java/kz/konturai/parser/controller/TargetingCampaignController.java +++ b/src/main/java/kz/konturai/parser/controller/TargetingCampaignController.java @@ -190,12 +190,15 @@ public class TargetingCampaignController { @PathVariable String id) { String userId = resolveUserId(userIdHeader, authHeader); TargetingCampaign campaign = campaignService.getCampaignById(id, userId); - return ResponseEntity.ok(Map.of( - "id", campaign.getId(), - "status", campaign.getStatus(), - "completedAt", campaign.getCompletedAt() != null ? campaign.getCompletedAt().toString() : "", - "statusHistory", campaign.getStatusHistory() != null ? campaign.getStatusHistory() : List.of() - )); + String status = campaign.getStatus(); + boolean isTerminal = "active".equals(status) || "failed".equals(status) || "paused".equals(status); + java.util.Map response = new java.util.LinkedHashMap<>(); + response.put("id", campaign.getId()); + response.put("status", status); + response.put("isTerminal", isTerminal); + response.put("completedAt", campaign.getCompletedAt() != null ? campaign.getCompletedAt().toString() : null); + response.put("statusHistory", campaign.getStatusHistory() != null ? campaign.getStatusHistory() : List.of()); + return ResponseEntity.ok(response); } // ── AI Предсказания ─────────────────────────────────────────────────────── diff --git a/src/main/java/kz/konturai/parser/service/TargetingCampaignService.java b/src/main/java/kz/konturai/parser/service/TargetingCampaignService.java index 511247e..e9e909f 100644 --- a/src/main/java/kz/konturai/parser/service/TargetingCampaignService.java +++ b/src/main/java/kz/konturai/parser/service/TargetingCampaignService.java @@ -18,6 +18,8 @@ import kz.konturai.parser.enums.CampaignObjective; import kz.konturai.parser.util.RangeParser; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; @@ -49,6 +51,10 @@ public class TargetingCampaignService { private final MarketingStrategyRepository strategyRepository; private final CampaignPredictionService predictionService; + // Self-reference through Spring proxy so @Async works (avoids self-invocation bypass) + @Autowired + private ApplicationContext applicationContext; + public TargetingCampaign createCampaign(TargetingCampaignRequest req, String userId) { if (req.getStrategyId() == null || req.getStrategyId().isBlank()) { throw new IllegalArgumentException("strategyId is required"); @@ -157,8 +163,8 @@ public class TargetingCampaignService { addStatusHistory(campaign, "CREATED", CampaignStatusMessage.CREATED.getMessage()); TargetingCampaign saved = repository.save(campaign); - // Process Async - processAsync(saved.getId(), req, userId); + // Process Async — вызываем через Spring proxy чтобы @Async работал корректно + applicationContext.getBean(TargetingCampaignService.class).processAsync(saved.getId(), req, userId); return saved; }