analytics
This commit is contained in:
@@ -2,8 +2,10 @@ package kz.konturai;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableScheduling
|
||||
public class KonturaiApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package kz.konturai.configuration;
|
||||
|
||||
import java.util.NoSuchElementException;
|
||||
import kz.konturai.exception.SocialApiException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
@@ -55,6 +56,11 @@ public class GlobalExceptionHandler {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(new ErrorResponse("Forbidden"));
|
||||
}
|
||||
|
||||
@ExceptionHandler(SocialApiException.class)
|
||||
public ResponseEntity<ErrorResponse> handleSocialApi(SocialApiException ex) {
|
||||
return ResponseEntity.status(HttpStatus.BAD_GATEWAY).body(new ErrorResponse(ex.getMessage()));
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<ErrorResponse> handleGeneric(Exception ex) {
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new ErrorResponse("Internal server error"));
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package kz.konturai.controller;
|
||||
|
||||
import java.util.Map;
|
||||
import kz.konturai.service.spec.AnalyticsService;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
// Optional admin/debug controller
|
||||
@RestController
|
||||
@RequestMapping("/api/smm/analytics")
|
||||
public class AnalyticsController {
|
||||
|
||||
private final AnalyticsService analyticsService;
|
||||
|
||||
public AnalyticsController(AnalyticsService analyticsService) {
|
||||
this.analyticsService = analyticsService;
|
||||
}
|
||||
|
||||
@PostMapping("/collect-now")
|
||||
public Map<String, Object> collectNow() {
|
||||
analyticsService.collectAndSaveAnalytics();
|
||||
return Map.of(
|
||||
"success", true,
|
||||
"message", "Сбор аналитики запущен.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package kz.konturai.controller;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import kz.konturai.dto.CampaignDto;
|
||||
import kz.konturai.dto.CreateCampaignRequest;
|
||||
import kz.konturai.service.spec.CampaignService;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/smm/campaigns")
|
||||
public class CampaignController {
|
||||
|
||||
private final CampaignService campaignService;
|
||||
|
||||
public CampaignController(CampaignService campaignService) {
|
||||
this.campaignService = campaignService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public List<CampaignDto> getAll() {
|
||||
return campaignService.getAllCampaigns();
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public CampaignDto getById(@PathVariable UUID id) {
|
||||
return campaignService.getCampaignById(id);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
public CampaignDto create(@Valid @RequestBody CreateCampaignRequest request) {
|
||||
return campaignService.createCampaign(request);
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public CampaignDto update(@PathVariable UUID id, @Valid @RequestBody CreateCampaignRequest request) {
|
||||
return campaignService.updateCampaign(id, request);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
public void delete(@PathVariable UUID id) {
|
||||
campaignService.deleteCampaign(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package kz.konturai.controller;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import kz.konturai.dto.ChannelDto;
|
||||
import kz.konturai.dto.CreateChannelRequest;
|
||||
import kz.konturai.service.spec.ChannelService;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/smm/channels")
|
||||
public class ChannelController {
|
||||
|
||||
private final ChannelService channelService;
|
||||
|
||||
public ChannelController(ChannelService channelService) {
|
||||
this.channelService = channelService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public List<ChannelDto> getAll() {
|
||||
return channelService.getAllChannels();
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ChannelDto getById(@PathVariable UUID id) {
|
||||
return channelService.getChannelById(id);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
public ChannelDto create(@Valid @RequestBody CreateChannelRequest request) {
|
||||
return channelService.createChannel(request);
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public ChannelDto update(@PathVariable UUID id, @Valid @RequestBody CreateChannelRequest request) {
|
||||
return channelService.updateChannel(id, request);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
public void delete(@PathVariable UUID id) {
|
||||
channelService.deleteChannel(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package kz.konturai.controller;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import kz.konturai.dto.ContentQueueDto;
|
||||
import kz.konturai.dto.CreateContentRequest;
|
||||
import kz.konturai.dto.MessageDto;
|
||||
import kz.konturai.service.spec.ContentService;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/smm/content")
|
||||
public class ContentController {
|
||||
|
||||
private final ContentService contentService;
|
||||
|
||||
public ContentController(ContentService contentService) {
|
||||
this.contentService = contentService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public List<ContentQueueDto> list() {
|
||||
return contentService.list();
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ContentQueueDto get(@PathVariable UUID id) {
|
||||
return contentService.get(id);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
public ContentQueueDto create(@Valid @RequestBody CreateContentRequest request) {
|
||||
return contentService.create(request);
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public ContentQueueDto update(@PathVariable UUID id, @Valid @RequestBody CreateContentRequest request) {
|
||||
return contentService.update(id, request);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
public void delete(@PathVariable UUID id) {
|
||||
contentService.delete(id);
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/approve")
|
||||
public ContentQueueDto approve(@PathVariable UUID id) {
|
||||
return contentService.approve(id);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/messages")
|
||||
public List<MessageDto> listMessages(@PathVariable UUID id) {
|
||||
return contentService.listMessages(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package kz.konturai.controller;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import kz.konturai.domain.Message;
|
||||
import kz.konturai.service.spec.PublishingService;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/smm/publishing")
|
||||
public class PublishingController {
|
||||
|
||||
private final PublishingService publishingService;
|
||||
|
||||
public PublishingController(PublishingService publishingService) {
|
||||
this.publishingService = publishingService;
|
||||
}
|
||||
|
||||
@PostMapping("/post/{contentId}")
|
||||
public Map<String, Object> publish(@PathVariable UUID contentId) {
|
||||
Message m = publishingService.publishContentById(contentId);
|
||||
return Map.of(
|
||||
"success", true,
|
||||
"message", "Пост успешно опубликован",
|
||||
"data", Map.of(
|
||||
"messageId", m.getId(),
|
||||
"externalUrl", m.getUrl()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package kz.konturai.domain;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EnumType;
|
||||
import jakarta.persistence.Enumerated;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.UUID;
|
||||
import kz.konturai.domain.enums.CampaignStatus;
|
||||
|
||||
@Entity
|
||||
@Table(name = "campaigns")
|
||||
public class Campaign {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
private UUID id;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String name;
|
||||
|
||||
@Column
|
||||
private String goal;
|
||||
|
||||
@Column
|
||||
private BigDecimal budget;
|
||||
|
||||
@Column
|
||||
private ZonedDateTime startAt;
|
||||
|
||||
@Column
|
||||
private ZonedDateTime endAt;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false)
|
||||
private CampaignStatus status;
|
||||
|
||||
public UUID getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(UUID id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getGoal() {
|
||||
return goal;
|
||||
}
|
||||
|
||||
public void setGoal(String goal) {
|
||||
this.goal = goal;
|
||||
}
|
||||
|
||||
public BigDecimal getBudget() {
|
||||
return budget;
|
||||
}
|
||||
|
||||
public void setBudget(BigDecimal budget) {
|
||||
this.budget = budget;
|
||||
}
|
||||
|
||||
public ZonedDateTime getStartAt() {
|
||||
return startAt;
|
||||
}
|
||||
|
||||
public void setStartAt(ZonedDateTime startAt) {
|
||||
this.startAt = startAt;
|
||||
}
|
||||
|
||||
public ZonedDateTime getEndAt() {
|
||||
return endAt;
|
||||
}
|
||||
|
||||
public void setEndAt(ZonedDateTime endAt) {
|
||||
this.endAt = endAt;
|
||||
}
|
||||
|
||||
public CampaignStatus getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(CampaignStatus status) {
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package kz.konturai.domain;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EnumType;
|
||||
import jakarta.persistence.Enumerated;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import java.util.UUID;
|
||||
import kz.konturai.domain.enums.ChannelType;
|
||||
|
||||
@Entity
|
||||
@Table(name = "channels")
|
||||
public class Channel {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
private UUID id;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String name;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false)
|
||||
private ChannelType type;
|
||||
|
||||
@Column
|
||||
private String apiKeyRef;
|
||||
|
||||
@Column(nullable = false)
|
||||
private boolean isActive;
|
||||
|
||||
public UUID getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(UUID id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public ChannelType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(ChannelType type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public String getApiKeyRef() {
|
||||
return apiKeyRef;
|
||||
}
|
||||
|
||||
public void setApiKeyRef(String apiKeyRef) {
|
||||
this.apiKeyRef = apiKeyRef;
|
||||
}
|
||||
|
||||
public boolean isActive() {
|
||||
return isActive;
|
||||
}
|
||||
|
||||
public void setActive(boolean active) {
|
||||
isActive = active;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package kz.konturai.domain;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EnumType;
|
||||
import jakarta.persistence.Enumerated;
|
||||
import jakarta.persistence.FetchType;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.UUID;
|
||||
import kz.konturai.domain.enums.ContentStatus;
|
||||
import org.hibernate.annotations.JdbcTypeCode;
|
||||
import org.hibernate.type.SqlTypes;
|
||||
|
||||
@Entity
|
||||
@Table(name = "content_queue")
|
||||
public class ContentQueue {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
private UUID id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "campaign_id")
|
||||
private Campaign campaign;
|
||||
|
||||
@Column
|
||||
private String locale;
|
||||
|
||||
@Column
|
||||
private String topic;
|
||||
|
||||
@JdbcTypeCode(SqlTypes.JSON)
|
||||
@Column(columnDefinition = "jsonb")
|
||||
private String postDraft;
|
||||
|
||||
@JdbcTypeCode(SqlTypes.JSON)
|
||||
@Column(columnDefinition = "jsonb")
|
||||
private String assetsRefs;
|
||||
|
||||
@Column
|
||||
private ZonedDateTime scheduledAt;
|
||||
|
||||
@Column
|
||||
private int priority;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false)
|
||||
private ContentStatus status;
|
||||
|
||||
public UUID getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(UUID id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Campaign getCampaign() {
|
||||
return campaign;
|
||||
}
|
||||
|
||||
public void setCampaign(Campaign campaign) {
|
||||
this.campaign = campaign;
|
||||
}
|
||||
|
||||
public String getLocale() {
|
||||
return locale;
|
||||
}
|
||||
|
||||
public void setLocale(String locale) {
|
||||
this.locale = locale;
|
||||
}
|
||||
|
||||
public String getTopic() {
|
||||
return topic;
|
||||
}
|
||||
|
||||
public void setTopic(String topic) {
|
||||
this.topic = topic;
|
||||
}
|
||||
|
||||
public String getPostDraft() {
|
||||
return postDraft;
|
||||
}
|
||||
|
||||
public void setPostDraft(String postDraft) {
|
||||
this.postDraft = postDraft;
|
||||
}
|
||||
|
||||
public String getAssetsRefs() {
|
||||
return assetsRefs;
|
||||
}
|
||||
|
||||
public void setAssetsRefs(String assetsRefs) {
|
||||
this.assetsRefs = assetsRefs;
|
||||
}
|
||||
|
||||
public ZonedDateTime getScheduledAt() {
|
||||
return scheduledAt;
|
||||
}
|
||||
|
||||
public void setScheduledAt(ZonedDateTime scheduledAt) {
|
||||
this.scheduledAt = scheduledAt;
|
||||
}
|
||||
|
||||
public int getPriority() {
|
||||
return priority;
|
||||
}
|
||||
|
||||
public void setPriority(int priority) {
|
||||
this.priority = priority;
|
||||
}
|
||||
|
||||
public ContentStatus getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(ContentStatus status) {
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package kz.konturai.domain;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.FetchType;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Table(name = "kpi_snapshots")
|
||||
public class KpiSnapshot {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
private UUID id;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDate date;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "channel_id")
|
||||
private Channel channel;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String metric;
|
||||
|
||||
@Column(nullable = false)
|
||||
private BigDecimal value;
|
||||
|
||||
public UUID getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(UUID id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public LocalDate getDate() {
|
||||
return date;
|
||||
}
|
||||
|
||||
public void setDate(LocalDate date) {
|
||||
this.date = date;
|
||||
}
|
||||
|
||||
public Channel getChannel() {
|
||||
return channel;
|
||||
}
|
||||
|
||||
public void setChannel(Channel channel) {
|
||||
this.channel = channel;
|
||||
}
|
||||
|
||||
public String getMetric() {
|
||||
return metric;
|
||||
}
|
||||
|
||||
public void setMetric(String metric) {
|
||||
this.metric = metric;
|
||||
}
|
||||
|
||||
public BigDecimal getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public void setValue(BigDecimal value) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package kz.konturai.domain;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.FetchType;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.OneToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Table(name = "messages")
|
||||
public class Message {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
private UUID id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "channel_id")
|
||||
private Channel channel;
|
||||
|
||||
@OneToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "content_id")
|
||||
private ContentQueue content;
|
||||
|
||||
@Column
|
||||
private String externalId;
|
||||
|
||||
@Column
|
||||
private String url;
|
||||
|
||||
@Column
|
||||
private ZonedDateTime postedAt;
|
||||
|
||||
public UUID getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(UUID id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Channel getChannel() {
|
||||
return channel;
|
||||
}
|
||||
|
||||
public void setChannel(Channel channel) {
|
||||
this.channel = channel;
|
||||
}
|
||||
|
||||
public ContentQueue getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public void setContent(ContentQueue content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public String getExternalId() {
|
||||
return externalId;
|
||||
}
|
||||
|
||||
public void setExternalId(String externalId) {
|
||||
this.externalId = externalId;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public ZonedDateTime getPostedAt() {
|
||||
return postedAt;
|
||||
}
|
||||
|
||||
public void setPostedAt(ZonedDateTime postedAt) {
|
||||
this.postedAt = postedAt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package kz.konturai.domain.enums;
|
||||
|
||||
public enum CampaignStatus {
|
||||
PLANNED,
|
||||
ACTIVE,
|
||||
COMPLETED
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package kz.konturai.domain.enums;
|
||||
|
||||
public enum ChannelType {
|
||||
TELEGRAM,
|
||||
VK,
|
||||
INSTAGRAM
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package kz.konturai.domain.enums;
|
||||
|
||||
public enum ContentStatus {
|
||||
DRAFT,
|
||||
PENDING_APPROVAL,
|
||||
APPROVED,
|
||||
PUBLISHED,
|
||||
FAILED
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package kz.konturai.dto;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.UUID;
|
||||
import kz.konturai.domain.enums.CampaignStatus;
|
||||
|
||||
public class CampaignDto {
|
||||
|
||||
public UUID id;
|
||||
public String name;
|
||||
public String goal;
|
||||
public BigDecimal budget;
|
||||
public ZonedDateTime startAt;
|
||||
public ZonedDateTime endAt;
|
||||
public CampaignStatus status;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package kz.konturai.dto;
|
||||
|
||||
import java.util.UUID;
|
||||
import kz.konturai.domain.enums.ChannelType;
|
||||
|
||||
public class ChannelDto {
|
||||
|
||||
public UUID id;
|
||||
public String name;
|
||||
public ChannelType type;
|
||||
public boolean isActive;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package kz.konturai.dto;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.UUID;
|
||||
import kz.konturai.domain.enums.ContentStatus;
|
||||
|
||||
public class ContentQueueDto {
|
||||
public UUID id;
|
||||
public UUID campaignId;
|
||||
public String locale;
|
||||
public String topic;
|
||||
public String postDraft;
|
||||
public String assetsRefs;
|
||||
public ZonedDateTime scheduledAt;
|
||||
public int priority;
|
||||
public ContentStatus status;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package kz.konturai.dto;
|
||||
|
||||
import jakarta.validation.constraints.Future;
|
||||
import jakarta.validation.constraints.FutureOrPresent;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.PositiveOrZero;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.ZonedDateTime;
|
||||
import kz.konturai.domain.enums.CampaignStatus;
|
||||
|
||||
public class CreateCampaignRequest {
|
||||
|
||||
@NotBlank
|
||||
public String name;
|
||||
public String goal;
|
||||
@PositiveOrZero
|
||||
public BigDecimal budget;
|
||||
@FutureOrPresent
|
||||
public ZonedDateTime startAt;
|
||||
@Future
|
||||
public ZonedDateTime endAt;
|
||||
public CampaignStatus status = CampaignStatus.PLANNED;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package kz.konturai.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import kz.konturai.domain.enums.ChannelType;
|
||||
|
||||
public class CreateChannelRequest {
|
||||
|
||||
@NotBlank
|
||||
public String name;
|
||||
@NotNull
|
||||
public ChannelType type;
|
||||
@NotBlank
|
||||
public String apiKeyRef;
|
||||
public boolean isActive = true;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package kz.konturai.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
public class CreateContentRequest {
|
||||
@NotNull
|
||||
public UUID campaignId;
|
||||
@NotBlank
|
||||
public String locale;
|
||||
@NotBlank
|
||||
public String topic;
|
||||
public String postDraft;
|
||||
public String assetsRefs;
|
||||
public ZonedDateTime scheduledAt;
|
||||
public Integer priority;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package kz.konturai.dto;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
public class MessageDto {
|
||||
public UUID id;
|
||||
public UUID channelId;
|
||||
public UUID contentId;
|
||||
public String externalId;
|
||||
public String url;
|
||||
public ZonedDateTime postedAt;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package kz.konturai.dto;
|
||||
|
||||
public class PostStatsDto {
|
||||
public long views;
|
||||
public long reactions;
|
||||
public long shares;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package kz.konturai.exception;
|
||||
|
||||
public class SocialApiException extends RuntimeException {
|
||||
public SocialApiException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public SocialApiException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package kz.konturai.repository;
|
||||
|
||||
import java.util.UUID;
|
||||
import kz.konturai.domain.Campaign;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface CampaignRepository extends JpaRepository<Campaign, UUID> {
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package kz.konturai.repository;
|
||||
|
||||
import java.util.UUID;
|
||||
import kz.konturai.domain.Channel;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface ChannelRepository extends JpaRepository<Channel, UUID> {
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package kz.konturai.repository;
|
||||
|
||||
import java.util.UUID;
|
||||
import kz.konturai.domain.ContentQueue;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface ContentQueueRepository extends JpaRepository<ContentQueue, UUID> {
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package kz.konturai.repository;
|
||||
|
||||
import java.util.UUID;
|
||||
import kz.konturai.domain.KpiSnapshot;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface KpiSnapshotRepository extends JpaRepository<KpiSnapshot, UUID> {
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package kz.konturai.repository;
|
||||
|
||||
import java.util.UUID;
|
||||
import kz.konturai.domain.Message;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface MessageRepository extends JpaRepository<Message, UUID> {
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package kz.konturai.service.impl;
|
||||
|
||||
import kz.konturai.dto.PostStatsDto;
|
||||
import kz.konturai.exception.SocialApiException;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.RestClientException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
@Component
|
||||
public class TelegramApiClient {
|
||||
|
||||
public record TelegramPostResponse(String externalId, String url) {
|
||||
}
|
||||
|
||||
private final RestTemplate restTemplate = new RestTemplate();
|
||||
|
||||
public TelegramPostResponse postMessage(String apiToken, String chatId, String text, String imagePath) {
|
||||
try {
|
||||
String url = "https://api.telegram.org/bot" + apiToken + "/sendMessage";
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
String body = "{" +
|
||||
"\"chat_id\":\"" + chatId + "\"," +
|
||||
"\"text\":\"" + text.replace("\"", "\\\"") + "\"" +
|
||||
"}";
|
||||
HttpEntity<String> request = new HttpEntity<>(body, headers);
|
||||
var response = restTemplate.postForEntity(url, request, java.util.Map.class);
|
||||
Object msg = ((java.util.Map<?, ?>) response.getBody()).get("result");
|
||||
String messageId = String.valueOf(((java.util.Map<?, ?>) msg).get("message_id"));
|
||||
String externalUrl = "https://t.me/c/" + chatId + "/" + messageId;
|
||||
return new TelegramPostResponse(messageId, externalUrl);
|
||||
} catch (RestClientException e) {
|
||||
throw new SocialApiException("Telegram API error: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public PostStatsDto getPostStatistics(String apiToken, String chatId, String externalId) {
|
||||
try {
|
||||
// Placeholder implementation; Telegram views require specific APIs and bot
|
||||
// settings.
|
||||
// Here we just call a harmless endpoint to validate token and return zeros.
|
||||
String url = "https://api.telegram.org/bot" + apiToken + "/getChat";
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
String body = "{" +
|
||||
"\"chat_id\":\"" + chatId + "\"" +
|
||||
"}";
|
||||
HttpEntity<String> request = new HttpEntity<>(body, headers);
|
||||
restTemplate.postForEntity(url, request, java.util.Map.class);
|
||||
PostStatsDto stats = new PostStatsDto();
|
||||
stats.views = 0;
|
||||
stats.reactions = 0;
|
||||
stats.shares = 0;
|
||||
return stats;
|
||||
} catch (RestClientException e) {
|
||||
throw new SocialApiException("Telegram API error: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package kz.konturai.service.spec;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import kz.konturai.domain.Channel;
|
||||
import kz.konturai.domain.Message;
|
||||
import kz.konturai.domain.KpiSnapshot;
|
||||
import kz.konturai.dto.PostStatsDto;
|
||||
import kz.konturai.repository.KpiSnapshotRepository;
|
||||
import kz.konturai.repository.MessageRepository;
|
||||
import kz.konturai.service.impl.TelegramApiClient;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
public class AnalyticsService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(AnalyticsService.class);
|
||||
|
||||
private final MessageRepository messageRepository;
|
||||
private final KpiSnapshotRepository kpiSnapshotRepository;
|
||||
private final TelegramApiClient telegramApiClient;
|
||||
|
||||
public AnalyticsService(MessageRepository messageRepository, KpiSnapshotRepository kpiSnapshotRepository,
|
||||
TelegramApiClient telegramApiClient) {
|
||||
this.messageRepository = messageRepository;
|
||||
this.kpiSnapshotRepository = kpiSnapshotRepository;
|
||||
this.telegramApiClient = telegramApiClient;
|
||||
}
|
||||
|
||||
@Scheduled(cron = "0 0 * * * *")
|
||||
@Transactional
|
||||
public void collectAndSaveAnalytics() {
|
||||
List<Message> messages = messageRepository.findAll();
|
||||
for (Message m : messages) {
|
||||
try {
|
||||
Channel ch = m.getChannel();
|
||||
// For demo we assume Telegram for all
|
||||
PostStatsDto stats = telegramApiClient.getPostStatistics(ch.getApiKeyRef(), "" + 0, m.getExternalId());
|
||||
saveSnapshot(m, ch, "VIEWS", BigDecimal.valueOf(stats.views));
|
||||
saveSnapshot(m, ch, "REACTIONS", BigDecimal.valueOf(stats.reactions));
|
||||
saveSnapshot(m, ch, "SHARES", BigDecimal.valueOf(stats.shares));
|
||||
// Example ER calc: (reactions + shares) / max(views,1)
|
||||
long denom = Math.max(stats.views, 1);
|
||||
BigDecimal er = BigDecimal.valueOf((double) (stats.reactions + stats.shares) / denom);
|
||||
saveSnapshot(m, ch, "ENGAGEMENT_RATE", er);
|
||||
} catch (Exception ex) {
|
||||
log.warn("Analytics collection failed for message {}: {}", m.getId(), ex.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void saveSnapshot(Message m, Channel ch, String metric, BigDecimal value) {
|
||||
KpiSnapshot snap = new KpiSnapshot();
|
||||
snap.setDate(LocalDate.now());
|
||||
snap.setChannel(ch);
|
||||
snap.setMetric(metric);
|
||||
snap.setValue(value);
|
||||
kpiSnapshotRepository.save(snap);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package kz.konturai.service.spec;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.UUID;
|
||||
import kz.konturai.domain.Campaign;
|
||||
import kz.konturai.dto.CampaignDto;
|
||||
import kz.konturai.dto.CreateCampaignRequest;
|
||||
import kz.konturai.repository.CampaignRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
public class CampaignService {
|
||||
|
||||
private final CampaignRepository campaignRepository;
|
||||
|
||||
public CampaignService(CampaignRepository campaignRepository) {
|
||||
this.campaignRepository = campaignRepository;
|
||||
}
|
||||
|
||||
public List<CampaignDto> getAllCampaigns() {
|
||||
return campaignRepository.findAll().stream().map(CampaignService::toDto).toList();
|
||||
}
|
||||
|
||||
public CampaignDto getCampaignById(UUID id) {
|
||||
Campaign c = campaignRepository.findById(id)
|
||||
.orElseThrow(() -> new NoSuchElementException("Campaign not found"));
|
||||
return toDto(c);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public CampaignDto createCampaign(CreateCampaignRequest request) {
|
||||
Campaign c = new Campaign();
|
||||
c.setName(request.name);
|
||||
c.setGoal(request.goal);
|
||||
c.setBudget(request.budget);
|
||||
c.setStartAt(request.startAt);
|
||||
c.setEndAt(request.endAt);
|
||||
c.setStatus(request.status);
|
||||
Campaign saved = campaignRepository.save(c);
|
||||
return toDto(saved);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public CampaignDto updateCampaign(UUID id, CreateCampaignRequest request) {
|
||||
Campaign c = campaignRepository.findById(id)
|
||||
.orElseThrow(() -> new NoSuchElementException("Campaign not found"));
|
||||
c.setName(request.name);
|
||||
c.setGoal(request.goal);
|
||||
c.setBudget(request.budget);
|
||||
c.setStartAt(request.startAt);
|
||||
c.setEndAt(request.endAt);
|
||||
c.setStatus(request.status);
|
||||
Campaign saved = campaignRepository.save(c);
|
||||
return toDto(saved);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deleteCampaign(UUID id) {
|
||||
if (!campaignRepository.existsById(id)) {
|
||||
throw new NoSuchElementException("Campaign not found");
|
||||
}
|
||||
campaignRepository.deleteById(id);
|
||||
}
|
||||
|
||||
private static CampaignDto toDto(Campaign c) {
|
||||
CampaignDto dto = new CampaignDto();
|
||||
dto.id = c.getId();
|
||||
dto.name = c.getName();
|
||||
dto.goal = c.getGoal();
|
||||
dto.budget = c.getBudget();
|
||||
dto.startAt = c.getStartAt();
|
||||
dto.endAt = c.getEndAt();
|
||||
dto.status = c.getStatus();
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package kz.konturai.service.spec;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.UUID;
|
||||
import kz.konturai.domain.Channel;
|
||||
import kz.konturai.dto.ChannelDto;
|
||||
import kz.konturai.dto.CreateChannelRequest;
|
||||
import kz.konturai.repository.ChannelRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
public class ChannelService {
|
||||
|
||||
private final ChannelRepository channelRepository;
|
||||
|
||||
public ChannelService(ChannelRepository channelRepository) {
|
||||
this.channelRepository = channelRepository;
|
||||
}
|
||||
|
||||
public List<ChannelDto> getAllChannels() {
|
||||
return channelRepository.findAll().stream().map(ChannelService::toDto).toList();
|
||||
}
|
||||
|
||||
public ChannelDto getChannelById(UUID id) {
|
||||
Channel ch = channelRepository.findById(id).orElseThrow(() -> new NoSuchElementException("Channel not found"));
|
||||
return toDto(ch);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ChannelDto createChannel(CreateChannelRequest request) {
|
||||
Channel ch = new Channel();
|
||||
ch.setName(request.name);
|
||||
ch.setType(request.type);
|
||||
ch.setApiKeyRef(request.apiKeyRef);
|
||||
ch.setActive(request.isActive);
|
||||
Channel saved = channelRepository.save(ch);
|
||||
return toDto(saved);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ChannelDto updateChannel(UUID id, CreateChannelRequest request) {
|
||||
Channel ch = channelRepository.findById(id).orElseThrow(() -> new NoSuchElementException("Channel not found"));
|
||||
ch.setName(request.name);
|
||||
ch.setType(request.type);
|
||||
ch.setApiKeyRef(request.apiKeyRef);
|
||||
ch.setActive(request.isActive);
|
||||
Channel saved = channelRepository.save(ch);
|
||||
return toDto(saved);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deleteChannel(UUID id) {
|
||||
if (!channelRepository.existsById(id)) {
|
||||
throw new NoSuchElementException("Channel not found");
|
||||
}
|
||||
channelRepository.deleteById(id);
|
||||
}
|
||||
|
||||
private static ChannelDto toDto(Channel ch) {
|
||||
ChannelDto dto = new ChannelDto();
|
||||
dto.id = ch.getId();
|
||||
dto.name = ch.getName();
|
||||
dto.type = ch.getType();
|
||||
dto.isActive = ch.isActive();
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package kz.konturai.service.spec;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.UUID;
|
||||
import kz.konturai.domain.Campaign;
|
||||
import kz.konturai.domain.ContentQueue;
|
||||
import kz.konturai.domain.Message;
|
||||
import kz.konturai.domain.enums.ContentStatus;
|
||||
import kz.konturai.dto.ContentQueueDto;
|
||||
import kz.konturai.dto.CreateContentRequest;
|
||||
import kz.konturai.dto.MessageDto;
|
||||
import kz.konturai.repository.CampaignRepository;
|
||||
import kz.konturai.repository.ContentQueueRepository;
|
||||
import kz.konturai.repository.MessageRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
public class ContentService {
|
||||
|
||||
private final ContentQueueRepository contentQueueRepository;
|
||||
private final CampaignRepository campaignRepository;
|
||||
private final MessageRepository messageRepository;
|
||||
|
||||
public ContentService(ContentQueueRepository contentQueueRepository, CampaignRepository campaignRepository,
|
||||
MessageRepository messageRepository) {
|
||||
this.contentQueueRepository = contentQueueRepository;
|
||||
this.campaignRepository = campaignRepository;
|
||||
this.messageRepository = messageRepository;
|
||||
}
|
||||
|
||||
public List<ContentQueueDto> list() {
|
||||
return contentQueueRepository.findAll().stream().map(ContentService::toDto).toList();
|
||||
}
|
||||
|
||||
public ContentQueueDto get(UUID id) {
|
||||
return contentQueueRepository.findById(id).map(ContentService::toDto)
|
||||
.orElseThrow(() -> new NoSuchElementException("Content not found"));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ContentQueueDto create(CreateContentRequest r) {
|
||||
Campaign campaign = campaignRepository.findById(r.campaignId)
|
||||
.orElseThrow(() -> new NoSuchElementException("Campaign not found"));
|
||||
ContentQueue c = new ContentQueue();
|
||||
c.setCampaign(campaign);
|
||||
c.setLocale(r.locale);
|
||||
c.setTopic(r.topic);
|
||||
c.setPostDraft(r.postDraft);
|
||||
c.setAssetsRefs(r.assetsRefs);
|
||||
c.setScheduledAt(r.scheduledAt);
|
||||
c.setPriority(r.priority != null ? r.priority : 0);
|
||||
c.setStatus(ContentStatus.DRAFT);
|
||||
return toDto(contentQueueRepository.save(c));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ContentQueueDto update(UUID id, CreateContentRequest r) {
|
||||
ContentQueue c = contentQueueRepository.findById(id)
|
||||
.orElseThrow(() -> new NoSuchElementException("Content not found"));
|
||||
if (r.campaignId != null) {
|
||||
Campaign campaign = campaignRepository.findById(r.campaignId)
|
||||
.orElseThrow(() -> new NoSuchElementException("Campaign not found"));
|
||||
c.setCampaign(campaign);
|
||||
}
|
||||
if (r.locale != null)
|
||||
c.setLocale(r.locale);
|
||||
if (r.topic != null)
|
||||
c.setTopic(r.topic);
|
||||
if (r.postDraft != null)
|
||||
c.setPostDraft(r.postDraft);
|
||||
if (r.assetsRefs != null)
|
||||
c.setAssetsRefs(r.assetsRefs);
|
||||
if (r.scheduledAt != null)
|
||||
c.setScheduledAt(r.scheduledAt);
|
||||
if (r.priority != null)
|
||||
c.setPriority(r.priority);
|
||||
return toDto(contentQueueRepository.save(c));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void delete(UUID id) {
|
||||
if (!contentQueueRepository.existsById(id)) {
|
||||
throw new NoSuchElementException("Content not found");
|
||||
}
|
||||
contentQueueRepository.deleteById(id);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ContentQueueDto approve(UUID id) {
|
||||
ContentQueue c = contentQueueRepository.findById(id)
|
||||
.orElseThrow(() -> new NoSuchElementException("Content not found"));
|
||||
c.setStatus(ContentStatus.APPROVED);
|
||||
return toDto(contentQueueRepository.save(c));
|
||||
}
|
||||
|
||||
public List<MessageDto> listMessages(UUID contentId) {
|
||||
ContentQueue c = contentQueueRepository.findById(contentId)
|
||||
.orElseThrow(() -> new NoSuchElementException("Content not found"));
|
||||
return messageRepository.findAll().stream()
|
||||
.filter(m -> m.getContent() != null && m.getContent().getId().equals(c.getId()))
|
||||
.map(ContentService::toDto)
|
||||
.toList();
|
||||
}
|
||||
|
||||
private static ContentQueueDto toDto(ContentQueue c) {
|
||||
ContentQueueDto dto = new ContentQueueDto();
|
||||
dto.id = c.getId();
|
||||
dto.campaignId = c.getCampaign() != null ? c.getCampaign().getId() : null;
|
||||
dto.locale = c.getLocale();
|
||||
dto.topic = c.getTopic();
|
||||
dto.postDraft = c.getPostDraft();
|
||||
dto.assetsRefs = c.getAssetsRefs();
|
||||
dto.scheduledAt = c.getScheduledAt();
|
||||
dto.priority = c.getPriority();
|
||||
dto.status = c.getStatus();
|
||||
return dto;
|
||||
}
|
||||
|
||||
private static MessageDto toDto(Message m) {
|
||||
MessageDto dto = new MessageDto();
|
||||
dto.id = m.getId();
|
||||
dto.channelId = m.getChannel() != null ? m.getChannel().getId() : null;
|
||||
dto.contentId = m.getContent() != null ? m.getContent().getId() : null;
|
||||
dto.externalId = m.getExternalId();
|
||||
dto.url = m.getUrl();
|
||||
dto.postedAt = m.getPostedAt();
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package kz.konturai.service.spec;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.UUID;
|
||||
import kz.konturai.domain.Channel;
|
||||
import kz.konturai.domain.ContentQueue;
|
||||
import kz.konturai.domain.Message;
|
||||
import kz.konturai.domain.enums.ContentStatus;
|
||||
import kz.konturai.repository.ChannelRepository;
|
||||
import kz.konturai.repository.ContentQueueRepository;
|
||||
import kz.konturai.repository.MessageRepository;
|
||||
import kz.konturai.service.impl.TelegramApiClient;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
public class PublishingService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(PublishingService.class);
|
||||
|
||||
private final ContentQueueRepository contentQueueRepository;
|
||||
private final MessageRepository messageRepository;
|
||||
private final ChannelRepository channelRepository;
|
||||
private final TelegramApiClient telegramApiClient;
|
||||
|
||||
public PublishingService(ContentQueueRepository contentQueueRepository, MessageRepository messageRepository,
|
||||
ChannelRepository channelRepository, TelegramApiClient telegramApiClient) {
|
||||
this.contentQueueRepository = contentQueueRepository;
|
||||
this.messageRepository = messageRepository;
|
||||
this.channelRepository = channelRepository;
|
||||
this.telegramApiClient = telegramApiClient;
|
||||
}
|
||||
|
||||
@Scheduled(cron = "0 * * * * *")
|
||||
@Transactional
|
||||
public void findAndPublishScheduledContent() {
|
||||
// naive scan (should use a query in repo for production)
|
||||
contentQueueRepository.findAll().stream()
|
||||
.filter(c -> c.getStatus() == ContentStatus.APPROVED)
|
||||
.filter(c -> c.getScheduledAt() != null && !c.getScheduledAt().isAfter(ZonedDateTime.now()))
|
||||
.forEach(c -> {
|
||||
try {
|
||||
publishSingle(c);
|
||||
} catch (Exception ex) {
|
||||
log.error("Failed to publish content {}: {}", c.getId(), ex.getMessage());
|
||||
c.setStatus(ContentStatus.FAILED);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Message publishContentById(UUID contentId) {
|
||||
ContentQueue c = contentQueueRepository.findById(contentId)
|
||||
.orElseThrow(() -> new NoSuchElementException("Content not found"));
|
||||
return publishSingle(c);
|
||||
}
|
||||
|
||||
private Message publishSingle(ContentQueue c) {
|
||||
// For demo, assume Telegram channel and use campaign id as channel id
|
||||
// placeholder
|
||||
// In real design, ContentQueue should reference a Channel. Here we pick by
|
||||
// priority mod.
|
||||
Channel channel = channelRepository.findAll().stream().findFirst()
|
||||
.orElseThrow(() -> new NoSuchElementException("No channels configured"));
|
||||
String text = c.getPostDraft() != null ? c.getPostDraft() : "";
|
||||
TelegramApiClient.TelegramPostResponse resp = telegramApiClient.postMessage(channel.getApiKeyRef(), "" + 0,
|
||||
text,
|
||||
null);
|
||||
|
||||
Message m = new Message();
|
||||
m.setChannel(channel);
|
||||
m.setContent(c);
|
||||
m.setExternalId(resp.externalId());
|
||||
m.setUrl(resp.url());
|
||||
m.setPostedAt(ZonedDateTime.now());
|
||||
Message saved = messageRepository.save(m);
|
||||
|
||||
c.setStatus(ContentStatus.PUBLISHED);
|
||||
return saved;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
-- Create SMM core tables
|
||||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||
|
||||
-- channels
|
||||
CREATE TABLE IF NOT EXISTS channels (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(255) NOT NULL,
|
||||
type VARCHAR(32) NOT NULL,
|
||||
api_key_ref VARCHAR(512),
|
||||
is_active BOOLEAN NOT NULL
|
||||
);
|
||||
|
||||
-- campaigns
|
||||
CREATE TABLE IF NOT EXISTS campaigns (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(255) NOT NULL,
|
||||
goal TEXT,
|
||||
budget NUMERIC(19,2),
|
||||
start_at TIMESTAMPTZ,
|
||||
end_at TIMESTAMPTZ,
|
||||
status VARCHAR(32) NOT NULL
|
||||
);
|
||||
|
||||
-- content_queue
|
||||
CREATE TABLE IF NOT EXISTS content_queue (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
campaign_id UUID REFERENCES campaigns(id) ON DELETE SET NULL,
|
||||
locale VARCHAR(16),
|
||||
topic TEXT,
|
||||
post_draft JSONB,
|
||||
assets_refs JSONB,
|
||||
scheduled_at TIMESTAMPTZ,
|
||||
priority INT,
|
||||
status VARCHAR(32) NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_content_queue_campaign ON content_queue(campaign_id);
|
||||
|
||||
-- messages
|
||||
CREATE TABLE IF NOT EXISTS messages (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
channel_id UUID REFERENCES channels(id) ON DELETE SET NULL,
|
||||
content_id UUID REFERENCES content_queue(id) ON DELETE SET NULL,
|
||||
external_id VARCHAR(255),
|
||||
url TEXT,
|
||||
posted_at TIMESTAMPTZ
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_channel ON messages(channel_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_content ON messages(content_id);
|
||||
|
||||
-- kpi_snapshots
|
||||
CREATE TABLE IF NOT EXISTS kpi_snapshots (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
date DATE NOT NULL,
|
||||
channel_id UUID REFERENCES channels(id) ON DELETE SET NULL,
|
||||
metric VARCHAR(64) NOT NULL,
|
||||
value NUMERIC(19,4) NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_kpi_snapshots_channel_date ON kpi_snapshots(channel_id, date);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user