diff --git a/pom.xml b/pom.xml index a2b98a2..85726b4 100644 --- a/pom.xml +++ b/pom.xml @@ -161,14 +161,29 @@ 0.12.3 runtime + + + org.projectlombok + lombok + true + - - org.springframework.boot - spring-boot-maven-plugin - + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + diff --git a/src/main/java/kz/konturai/parser/dto/MarketingAnalysisRequest.java b/src/main/java/kz/konturai/parser/dto/MarketingAnalysisRequest.java index c5153d0..ee54279 100644 --- a/src/main/java/kz/konturai/parser/dto/MarketingAnalysisRequest.java +++ b/src/main/java/kz/konturai/parser/dto/MarketingAnalysisRequest.java @@ -10,10 +10,16 @@ import kz.konturai.parser.validator.ValidAnalysisType; import kz.konturai.parser.validator.ValidDetailLevel; import kz.konturai.parser.validator.ValidRegion; import kz.konturai.parser.validator.ValidTargetAudience; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; import java.util.List; import java.util.Map; +@Data +@AllArgsConstructor +@NoArgsConstructor public class MarketingAnalysisRequest { @NotBlank(message = "Поле 'businessNiche' обязательно для заполнения") @@ -52,98 +58,9 @@ public class MarketingAnalysisRequest { @ValidAnalysisType(message = "Поле 'analysisType' должно содержать один или несколько типов: РЫНОК, КОНКУРЕНТЫ, ЦА, КАНАЛЫ, SWOT") private List analysisType; - public MarketingAnalysisRequest() { - } - - public MarketingAnalysisRequest(String businessNiche, String product, Map targetAudience, List region, - String goal, String detailLevel, String strongSide, String weakSide, List analysisType) { - this.businessNiche = businessNiche; - this.product = product; - this.targetAudience = targetAudience; - this.region = region; - this.goal = goal; - this.detailLevel = detailLevel; - this.strongSide = strongSide; - this.weakSide = weakSide; - this.analysisType = analysisType; - } - - public String getBusinessNiche() { - return businessNiche; - } - - public void setBusinessNiche(String businessNiche) { - this.businessNiche = businessNiche; - } - - public String getProduct() { - return product; - } - - public void setProduct(String product) { - this.product = product; - } - - public Map getTargetAudience() { - return targetAudience; - } - - public void setTargetAudience(Map targetAudience) { - this.targetAudience = targetAudience; - } - - public List getRegion() { - return region; - } - - public void setRegion(List region) { - this.region = region; - } - - public String getGoal() { - return goal; - } - - public void setGoal(String goal) { - this.goal = goal; - } - - public String getDetailLevel() { - return detailLevel; - } - - public void setDetailLevel(String detailLevel) { - this.detailLevel = detailLevel; - } - public DetailLevel getDetailLevelEnum() { return DetailLevel.fromString(detailLevel); } - - public String getStrongSide() { - return strongSide; - } - - public void setStrongSide(String strongSide) { - this.strongSide = strongSide; - } - - public String getWeakSide() { - return weakSide; - } - - public void setWeakSide(String weakSide) { - this.weakSide = weakSide; - } - - public List getAnalysisType() { - return analysisType; - } - - public void setAnalysisType(List analysisType) { - this.analysisType = analysisType; - } - public List getAnalysisTypeEnums() { if (analysisType == null) { return List.of(); diff --git a/src/main/java/kz/konturai/parser/dto/MarketingAnalysisResponseV2.java b/src/main/java/kz/konturai/parser/dto/MarketingAnalysisResponseV2.java index 3b1c734..c513932 100644 --- a/src/main/java/kz/konturai/parser/dto/MarketingAnalysisResponseV2.java +++ b/src/main/java/kz/konturai/parser/dto/MarketingAnalysisResponseV2.java @@ -2,10 +2,14 @@ package kz.konturai.parser.dto; import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.Getter; +import lombok.Setter; import java.util.List; import java.util.Map; +@Setter +@Getter @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public class MarketingAnalysisResponseV2 { @@ -13,33 +17,8 @@ public class MarketingAnalysisResponseV2 { private Sections sections; private List sources; - public MarketingAnalysisResponseV2() { - } - - public MarketingAnalysisResponseV2(String reportTitle, Sections sections) { - this.reportTitle = reportTitle; - this.sections = sections; - } - - public String getReportTitle() { - return reportTitle; - } - - public void setReportTitle(String reportTitle) { - this.reportTitle = reportTitle; - } - - public Sections getSections() { - return sections; - } - - public void setSections(Sections sections) { - this.sections = sections; - } - - public void setSources(List sources) { this.sources = sources; } - public List getSources() { return sources; } - + @Setter + @Getter @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public static class Sections { private MarketOverview marketOverview; @@ -55,79 +34,10 @@ public class MarketingAnalysisResponseV2 { public Sections() { } - public MarketOverview getMarketOverview() { - return marketOverview; - } - - public void setMarketOverview(MarketOverview marketOverview) { - this.marketOverview = marketOverview; - } - - public TargetAudience getTargetAudience() { - return targetAudience; - } - - public void setTargetAudience(TargetAudience targetAudience) { - this.targetAudience = targetAudience; - } - - public CompetitiveEnvironment getCompetitiveEnvironment() { - return competitiveEnvironment; - } - - public void setCompetitiveEnvironment(CompetitiveEnvironment competitiveEnvironment) { - this.competitiveEnvironment = competitiveEnvironment; - } - - public DemandAndBehavior getDemandAndBehavior() { - return demandAndBehavior; - } - - public void setDemandAndBehavior(DemandAndBehavior demandAndBehavior) { - this.demandAndBehavior = demandAndBehavior; - } - - public CustomerJourney getCustomerJourney() { - return customerJourney; - } - - public void setCustomerJourney(CustomerJourney customerJourney) { - this.customerJourney = customerJourney; - } - - public AcquisitionChannels getAcquisitionChannels() { - return acquisitionChannels; - } - - public void setAcquisitionChannels(AcquisitionChannels acquisitionChannels) { - this.acquisitionChannels = acquisitionChannels; - } - - public MarketingEconomics getMarketingEconomics() { - return marketingEconomics; - } - - public void setMarketingEconomics(MarketingEconomics marketingEconomics) { - this.marketingEconomics = marketingEconomics; - } - - public Bottlenecks getBottlenecks() { - return bottlenecks; - } - - public void setBottlenecks(Bottlenecks bottlenecks) { - this.bottlenecks = bottlenecks; - } - - public Recommendations getRecommendations() { - return recommendations; - } - - public void setRecommendations(Recommendations recommendations) { - this.recommendations = recommendations; - } } + @Setter + @Getter @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public static class MarketOverview { private String title; @@ -139,47 +49,10 @@ public class MarketingAnalysisResponseV2 { public MarketOverview() { } - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public MarketOverviewMetrics getMetrics() { - return metrics; - } - - public void setMetrics(MarketOverviewMetrics metrics) { - this.metrics = metrics; - } - - public MonthlyOnlineIndex getMonthlyOnlineIndex() { - return monthlyOnlineIndex; - } - - public void setMonthlyOnlineIndex(MonthlyOnlineIndex monthlyOnlineIndex) { - this.monthlyOnlineIndex = monthlyOnlineIndex; - } - - public Map getGeography() { - return geography; - } - - public void setGeography(Map geography) { - this.geography = geography; - } - - public String getConclusion() { - return conclusion; - } - - public void setConclusion(String conclusion) { - this.conclusion = conclusion; - } } + @Setter + @Getter @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public static class MarketOverviewMetrics { private String onlineDemandIndex; @@ -191,47 +64,10 @@ public class MarketingAnalysisResponseV2 { public MarketOverviewMetrics() { } - public String getOnlineDemandIndex() { - return onlineDemandIndex; - } - - public void setOnlineDemandIndex(String onlineDemandIndex) { - this.onlineDemandIndex = onlineDemandIndex; - } - - public String getOnlinePotential() { - return onlinePotential; - } - - public void setOnlinePotential(String onlinePotential) { - this.onlinePotential = onlinePotential; - } - - public String getDemandGrowthRateYoy() { - return demandGrowthRateYoy; - } - - public void setDemandGrowthRateYoy(String demandGrowthRateYoy) { - this.demandGrowthRateYoy = demandGrowthRateYoy; - } - - public String getSeasonalityCoefficientSpring() { - return seasonalityCoefficientSpring; - } - - public void setSeasonalityCoefficientSpring(String seasonalityCoefficientSpring) { - this.seasonalityCoefficientSpring = seasonalityCoefficientSpring; - } - - public String getSeasonalityRange() { - return seasonalityRange; - } - - public void setSeasonalityRange(String seasonalityRange) { - this.seasonalityRange = seasonalityRange; - } } + @Setter + @Getter @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public static class MonthlyOnlineIndex { private Double january; @@ -250,103 +86,10 @@ public class MarketingAnalysisResponseV2 { public MonthlyOnlineIndex() { } - public Double getJanuary() { - return january; - } - - public void setJanuary(Double january) { - this.january = january; - } - - public Double getFebruary() { - return february; - } - - public void setFebruary(Double february) { - this.february = february; - } - - public Double getMarch() { - return march; - } - - public void setMarch(Double march) { - this.march = march; - } - - public Double getApril() { - return april; - } - - public void setApril(Double april) { - this.april = april; - } - - public Double getMay() { - return may; - } - - public void setMay(Double may) { - this.may = may; - } - - public Double getJune() { - return june; - } - - public void setJune(Double june) { - this.june = june; - } - - public Double getJuly() { - return july; - } - - public void setJuly(Double july) { - this.july = july; - } - - public Double getAugust() { - return august; - } - - public void setAugust(Double august) { - this.august = august; - } - - public Double getSeptember() { - return september; - } - - public void setSeptember(Double september) { - this.september = september; - } - - public Double getOctober() { - return october; - } - - public void setOctober(Double october) { - this.october = october; - } - - public Double getNovember() { - return november; - } - - public void setNovember(Double november) { - this.november = november; - } - - public Double getDecember() { - return december; - } - - public void setDecember(Double december) { - this.december = december; - } } + @Setter + @Getter @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public static class TargetAudience { private String title; @@ -360,50 +103,10 @@ public class MarketingAnalysisResponseV2 { public TargetAudience() { } - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public TargetAudienceMetrics getMetrics() { - return metrics; - } - - public void setMetrics(TargetAudienceMetrics metrics) { - this.metrics = metrics; - } - - public List getSegments() { - return segments; - } - - public void setSegments(List segments) { - this.segments = segments; - } - - public Map getAgeStructure() { - return ageStructure; - } - - public void setAgeStructure(Map ageStructure) { - this.ageStructure = ageStructure; - } - - public String getConclusion() { - return conclusion; - } - - public void setConclusion(String conclusion) { - this.conclusion = conclusion; - } - - public Map getIncomeByAge() { return incomeByAge; } - public void setIncomeByAge(Map incomeByAge) { this.incomeByAge = incomeByAge; } } + @Setter + @Getter @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public static class TargetAudienceMetrics { private String totalAudienceSize; @@ -413,31 +116,10 @@ public class MarketingAnalysisResponseV2 { public TargetAudienceMetrics() { } - public String getTotalAudienceSize() { - return totalAudienceSize; - } - - public void setTotalAudienceSize(String totalAudienceSize) { - this.totalAudienceSize = totalAudienceSize; - } - - public String getLeadingSegmentShare() { - return leadingSegmentShare; - } - - public void setLeadingSegmentShare(String leadingSegmentShare) { - this.leadingSegmentShare = leadingSegmentShare; - } - - public String getAboveAverageIncomeShare() { - return aboveAverageIncomeShare; - } - - public void setAboveAverageIncomeShare(String aboveAverageIncomeShare) { - this.aboveAverageIncomeShare = aboveAverageIncomeShare; - } } + @Setter + @Getter @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public static class AudienceSegment { private String name; @@ -451,23 +133,10 @@ public class MarketingAnalysisResponseV2 { this.share = share; } - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getShare() { - return share; - } - - public void setShare(String share) { - this.share = share; - } } + @Setter + @Getter @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public static class CompetitiveEnvironment { private String title; @@ -479,47 +148,10 @@ public class MarketingAnalysisResponseV2 { public CompetitiveEnvironment() { } - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public CompetitiveEnvironmentMetrics getMetrics() { - return metrics; - } - - public void setMetrics(CompetitiveEnvironmentMetrics metrics) { - this.metrics = metrics; - } - - public Map getDigitalVisibilityBenchmarks() { - return digitalVisibilityBenchmarks; - } - - public void setDigitalVisibilityBenchmarks(Map digitalVisibilityBenchmarks) { - this.digitalVisibilityBenchmarks = digitalVisibilityBenchmarks; - } - - public Map> getCompetitorStrengths() { - return competitorStrengths; - } - - public void setCompetitorStrengths(Map> competitorStrengths) { - this.competitorStrengths = competitorStrengths; - } - - public String getConclusion() { - return conclusion; - } - - public void setConclusion(String conclusion) { - this.conclusion = conclusion; - } } + @Setter + @Getter @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public static class CompetitiveEnvironmentMetrics { private String marketShareTraffic; @@ -530,39 +162,10 @@ public class MarketingAnalysisResponseV2 { public CompetitiveEnvironmentMetrics() { } - public String getMarketShareTraffic() { - return marketShareTraffic; - } - - public void setMarketShareTraffic(String marketShareTraffic) { - this.marketShareTraffic = marketShareTraffic; - } - - public String getVisibilityRank() { - return visibilityRank; - } - - public void setVisibilityRank(String visibilityRank) { - this.visibilityRank = visibilityRank; - } - - public String getCompetitorShareInContextAds() { - return competitorShareInContextAds; - } - - public void setCompetitorShareInContextAds(String competitorShareInContextAds) { - this.competitorShareInContextAds = competitorShareInContextAds; - } - - public String getBenchmarkAstana() { - return benchmarkAstana; - } - - public void setBenchmarkAstana(String benchmarkAstana) { - this.benchmarkAstana = benchmarkAstana; - } } + @Setter + @Getter @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public static class DemandAndBehavior { private String title; @@ -575,55 +178,10 @@ public class MarketingAnalysisResponseV2 { public DemandAndBehavior() { } - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public DemandAndBehaviorMetrics getMetrics() { - return metrics; - } - - public void setMetrics(DemandAndBehaviorMetrics metrics) { - this.metrics = metrics; - } - - public Map getSearchQueryTypes() { - return searchQueryTypes; - } - - public void setSearchQueryTypes(Map searchQueryTypes) { - this.searchQueryTypes = searchQueryTypes; - } - - public Map getDecisionFactors() { - return decisionFactors; - } - - public void setDecisionFactors(Map decisionFactors) { - this.decisionFactors = decisionFactors; - } - - public Map getReasonsForRefusal() { - return reasonsForRefusal; - } - - public void setReasonsForRefusal(Map reasonsForRefusal) { - this.reasonsForRefusal = reasonsForRefusal; - } - - public String getConclusion() { - return conclusion; - } - - public void setConclusion(String conclusion) { - this.conclusion = conclusion; - } } + @Setter + @Getter @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public static class DemandAndBehaviorMetrics { private String searchVolumeGrowth; @@ -633,31 +191,10 @@ public class MarketingAnalysisResponseV2 { public DemandAndBehaviorMetrics() { } - public String getSearchVolumeGrowth() { - return searchVolumeGrowth; - } - - public void setSearchVolumeGrowth(String searchVolumeGrowth) { - this.searchVolumeGrowth = searchVolumeGrowth; - } - - public String getInteractionsBeforePurchase() { - return interactionsBeforePurchase; - } - - public void setInteractionsBeforePurchase(String interactionsBeforePurchase) { - this.interactionsBeforePurchase = interactionsBeforePurchase; - } - - public String getRepeatRefusalProbability() { - return repeatRefusalProbability; - } - - public void setRepeatRefusalProbability(String repeatRefusalProbability) { - this.repeatRefusalProbability = repeatRefusalProbability; - } } + @Setter + @Getter @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public static class CustomerJourney { private String title; @@ -669,47 +206,10 @@ public class MarketingAnalysisResponseV2 { public CustomerJourney() { } - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public FunnelMetrics getFunnelMetrics() { - return funnelMetrics; - } - - public void setFunnelMetrics(FunnelMetrics funnelMetrics) { - this.funnelMetrics = funnelMetrics; - } - - public ConversionRates getConversionRates() { - return conversionRates; - } - - public void setConversionRates(ConversionRates conversionRates) { - this.conversionRates = conversionRates; - } - - public FunnelBottlenecks getFunnelBottlenecks() { - return funnelBottlenecks; - } - - public void setFunnelBottlenecks(FunnelBottlenecks funnelBottlenecks) { - this.funnelBottlenecks = funnelBottlenecks; - } - - public String getConclusion() { - return conclusion; - } - - public void setConclusion(String conclusion) { - this.conclusion = conclusion; - } } + @Setter + @Getter @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public static class FunnelMetrics { private String adTraffic; @@ -721,47 +221,10 @@ public class MarketingAnalysisResponseV2 { public FunnelMetrics() { } - public String getAdTraffic() { - return adTraffic; - } - - public void setAdTraffic(String adTraffic) { - this.adTraffic = adTraffic; - } - - public String getSiteVisitors() { - return siteVisitors; - } - - public void setSiteVisitors(String siteVisitors) { - this.siteVisitors = siteVisitors; - } - - public Integer getLeads() { - return leads; - } - - public void setLeads(Integer leads) { - this.leads = leads; - } - - public Integer getBookings() { - return bookings; - } - - public void setBookings(Integer bookings) { - this.bookings = bookings; - } - - public Integer getPayments() { - return payments; - } - - public void setPayments(Integer payments) { - this.payments = payments; - } } + @Setter + @Getter @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public static class ConversionRates { private String leadToBooking; @@ -771,31 +234,10 @@ public class MarketingAnalysisResponseV2 { public ConversionRates() { } - public String getLeadToBooking() { - return leadToBooking; - } - - public void setLeadToBooking(String leadToBooking) { - this.leadToBooking = leadToBooking; - } - - public String getBookingToPayment() { - return bookingToPayment; - } - - public void setBookingToPayment(String bookingToPayment) { - this.bookingToPayment = bookingToPayment; - } - - public String getLeadToPaymentTotal() { - return leadToPaymentTotal; - } - - public void setLeadToPaymentTotal(String leadToPaymentTotal) { - this.leadToPaymentTotal = leadToPaymentTotal; - } } + @Setter + @Getter @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public static class FunnelBottlenecks { private String leadToBooking; @@ -804,23 +246,10 @@ public class MarketingAnalysisResponseV2 { public FunnelBottlenecks() { } - public String getLeadToBooking() { - return leadToBooking; - } - - public void setLeadToBooking(String leadToBooking) { - this.leadToBooking = leadToBooking; - } - - public String getBookingToPayment() { - return bookingToPayment; - } - - public void setBookingToPayment(String bookingToPayment) { - this.bookingToPayment = bookingToPayment; - } } + @Setter + @Getter @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public static class AcquisitionChannels { private String title; @@ -832,47 +261,10 @@ public class MarketingAnalysisResponseV2 { public AcquisitionChannels() { } - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public AcquisitionChannelsMetrics getMetrics() { - return metrics; - } - - public void setMetrics(AcquisitionChannelsMetrics metrics) { - this.metrics = metrics; - } - - public Map getChannelShares() { - return channelShares; - } - - public void setChannelShares(Map channelShares) { - this.channelShares = channelShares; - } - - public Map getChannelEfficiency() { - return channelEfficiency; - } - - public void setChannelEfficiency(Map channelEfficiency) { - this.channelEfficiency = channelEfficiency; - } - - public String getConclusion() { - return conclusion; - } - - public void setConclusion(String conclusion) { - this.conclusion = conclusion; - } } + @Setter + @Getter @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public static class AcquisitionChannelsMetrics { private String searchTrafficShare; @@ -882,31 +274,10 @@ public class MarketingAnalysisResponseV2 { public AcquisitionChannelsMetrics() { } - public String getSearchTrafficShare() { - return searchTrafficShare; - } - - public void setSearchTrafficShare(String searchTrafficShare) { - this.searchTrafficShare = searchTrafficShare; - } - - public String getAverageLeadCost() { - return averageLeadCost; - } - - public void setAverageLeadCost(String averageLeadCost) { - this.averageLeadCost = averageLeadCost; - } - - public String getGrowthPotential() { - return growthPotential; - } - - public void setGrowthPotential(String growthPotential) { - this.growthPotential = growthPotential; - } } + @Setter + @Getter @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public static class ChannelEfficiency { private String cost; @@ -920,100 +291,29 @@ public class MarketingAnalysisResponseV2 { this.status = status; } - public String getCost() { - return cost; - } - - public void setCost(String cost) { - this.cost = cost; - } - - public String getStatus() { - return status; - } - - public void setStatus(String status) { - this.status = status; - } } + @Setter + @Getter @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public static class MarketingEconomics { private String title; private MarketingEconomicsMetrics metrics; private FinancialPerformance financialPerformance; private String conclusion; - - public MarketingEconomics() { - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public MarketingEconomicsMetrics getMetrics() { - return metrics; - } - - public void setMetrics(MarketingEconomicsMetrics metrics) { - this.metrics = metrics; - } - - public FinancialPerformance getFinancialPerformance() { - return financialPerformance; - } - - public void setFinancialPerformance(FinancialPerformance financialPerformance) { - this.financialPerformance = financialPerformance; - } - - public String getConclusion() { - return conclusion; - } - - public void setConclusion(String conclusion) { - this.conclusion = conclusion; - } } + @Setter + @Getter @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public static class MarketingEconomicsMetrics { private String customerAcquisitionCostCac; private String repeatSalesConversion; private String roi; - - public MarketingEconomicsMetrics() { - } - - public String getCustomerAcquisitionCostCac() { - return customerAcquisitionCostCac; - } - - public void setCustomerAcquisitionCostCac(String customerAcquisitionCostCac) { - this.customerAcquisitionCostCac = customerAcquisitionCostCac; - } - - public String getRepeatSalesConversion() { - return repeatSalesConversion; - } - - public void setRepeatSalesConversion(String repeatSalesConversion) { - this.repeatSalesConversion = repeatSalesConversion; - } - - public String getRoi() { - return roi; - } - - public void setRoi(String roi) { - this.roi = roi; - } } + @Setter + @Getter @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public static class FinancialPerformance { private MonthYearValue marketingExpenses; @@ -1022,218 +322,60 @@ public class MarketingAnalysisResponseV2 { private MonthYearValue revenueFromClient; private MonthYearValue profitFromClient; - public FinancialPerformance() { - } - - public MonthYearValue getMarketingExpenses() { - return marketingExpenses; - } - - public void setMarketingExpenses(MonthYearValue marketingExpenses) { - this.marketingExpenses = marketingExpenses; - } - - public MonthYearValue getClients() { - return clients; - } - - public void setClients(MonthYearValue clients) { - this.clients = clients; - } - - public MonthYearValue getCac() { - return cac; - } - - public void setCac(MonthYearValue cac) { - this.cac = cac; - } - - public MonthYearValue getRevenueFromClient() { - return revenueFromClient; - } - - public void setRevenueFromClient(MonthYearValue revenueFromClient) { - this.revenueFromClient = revenueFromClient; - } - - public MonthYearValue getProfitFromClient() { - return profitFromClient; - } - - public void setProfitFromClient(MonthYearValue profitFromClient) { - this.profitFromClient = profitFromClient; - } } + @Setter + @Getter @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public static class MonthYearValue { private String month; private String year; - public MonthYearValue() { - } - public MonthYearValue(String month, String year) { this.month = month; this.year = year; } - public String getMonth() { - return month; - } - - public void setMonth(String month) { - this.month = month; - } - - public String getYear() { - return year; - } - - public void setYear(String year) { - this.year = year; - } } + @Setter + @Getter @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public static class Bottlenecks { private String title; private Map statistics; private BottlenecksDetails details; private String conclusion; - - public Bottlenecks() { - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public Map getStatistics() { - return statistics; - } - - public void setStatistics(Map statistics) { - this.statistics = statistics; - } - - public BottlenecksDetails getDetails() { - return details; - } - - public void setDetails(BottlenecksDetails details) { - this.details = details; - } - - public String getConclusion() { - return conclusion; - } - - public void setConclusion(String conclusion) { - this.conclusion = conclusion; - } } + @Setter + @Getter @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public static class BottlenecksDetails { private List landing; private List leadManagement; private List retention; - - public BottlenecksDetails() { - } - - public List getLanding() { - return landing; - } - - public void setLanding(List landing) { - this.landing = landing; - } - - public List getLeadManagement() { - return leadManagement; - } - - public void setLeadManagement(List leadManagement) { - this.leadManagement = leadManagement; - } - - public List getRetention() { - return retention; - } - - public void setRetention(List retention) { - this.retention = retention; - } } + @Setter + @Getter @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public static class Recommendations { private String title; private List corePillars; private String conclusion; - - public Recommendations() { - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public List getCorePillars() { - return corePillars; - } - - public void setCorePillars(List corePillars) { - this.corePillars = corePillars; - } - - public String getConclusion() { - return conclusion; - } - - public void setConclusion(String conclusion) { - this.conclusion = conclusion; - } } + @Setter + @Getter @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public static class CorePillar { private String strategy; private String action; - - public CorePillar() { - } - public CorePillar(String strategy, String action) { this.strategy = strategy; this.action = action; } - public String getStrategy() { - return strategy; - } - - public void setStrategy(String strategy) { - this.strategy = strategy; - } - - public String getAction() { - return action; - } - - public void setAction(String action) { - this.action = action; - } } } diff --git a/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java b/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java index 65b540e..0d2afeb 100644 --- a/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java +++ b/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java @@ -12,6 +12,7 @@ import kz.konturai.parser.model.MarketingAnalysis; import kz.konturai.parser.model.MarketingAnalysisV2Document; import kz.konturai.parser.repository.MarketingAnalysisRepository; import kz.konturai.parser.repository.MarketingAnalysisV2Repository; +import lombok.RequiredArgsConstructor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; @@ -31,18 +32,20 @@ import jakarta.annotation.PostConstruct; import org.springframework.web.reactive.function.client.WebClientResponseException; @Service +@RequiredArgsConstructor public class MarketingAnalysisService { private static final String KZ_MARKET_CONTEXT = - "РОЛЬ: Ты — Партнер международной консалтинговой фирмы (Big 4: McKinsey/KPMG) с 50-летним опытом в маркетинге и стратегии.\n" + - "Твоя специализация: Рынки Центральной Азии (особенно Казахстан).\n" + - "СТИЛЬ: Уверенный, профессиональный, доказательный, без 'воды'. Используй термины (LTV, CAC, Retention, Unit-economics).\n" + - "КОНТЕКСТ РЫНКА:\n" + - "- Страна: Казахстан (Алматы, Астана, Шымкент - ключевые хабы).\n" + - "- Валюта: Тенге (KZT). Все цены и доходы — только в тенге.\n" + - "- Инфляция: Реальная потребительская инфляция ~10-12% (официальная ~8%).\n" + - "- Поведение: Высокая роль Kaspi (QR, рассрочка 0-0-12), Instagram (витрина), WhatsApp (коммуникация).\n" + - "- Менталитет: 'Уят' (репутация), семейные ценности, склонность к статусному потреблению, чувствительность к цене (акции).\n"; + "РОЛЬ: Ты — Партнер международной консалтинговой фирмы (Big 4: McKinsey/BCG/PwC) с офисом в Алматы.\n" + + "СПЕЦИАЛИЗАЦИЯ: Глубокий анализ рынка Казахстана. Ты мыслишь в Тенге (KZT).\n" + + "ТВОИ ЗНАНИЯ О РЫНКЕ KZ (СТРОГИЕ ПРАВИЛА):\n" + + "1. SuperApps: Kaspi.kz — это экосистема №1 (Магазин, Travel, Gov, Платежи). Это главный канал продаж для товарки и услуг.\n" + + "2. Карты: 2GIS в Казахстане популярнее Google Maps для поиска услуг офлайн. Отзывы в 2GIS критически важны.\n" + + "3. Фитнес-рынок: Лидеры — 1Fit (единый абонемент, НЕ переводи это название), Invictus, World Class, FitnessBlitz.\n" + + "4. Соцсети: Instagram (Direct/Reels) и TikTok — главные каналы продаж. ВКонтакте и Threads — мертвы для коммерции в РК, ЗАПРЕЩЕНО их рекомендовать.\n" + + "5. Экономика: Реальная инфляция ~12-15%. Потребитель чувствителен к цене, любит рассрочки (Kaspi Red/Kredit).\n" + + "ВАЛЮТА: Только KZT. Округляй цифры до сотен (например, 15 000 KZT, а не 14 321).\n"; + private static final String AI_MANDATORY_JSON_USAGE_WARNING = "ВНИМАНИЕ: Если в предоставленном JSON есть данные, ты ОБЯЗАН использовать их и вставить соответствующий тег [[CHART_...]]. " + "Никогда не пиши, что данных нет, если JSON не пустой.\n\n"; private static final String AI_ULTIMATE_JSON_PRESENCE_INSTRUCTION = "" @@ -423,21 +426,6 @@ public class MarketingAnalysisService { private Semaphore requestSemaphore; - public MarketingAnalysisService( - MarketingAnalysisRepository repository, - MarketingAnalysisV2Repository v2Repository, - OpenAIAnalyticsService openAIAnalyticsService, - MinIOService minIOService, - ResearchPdfService researchPdfService, - SerperSearchService serperSearchService) { - this.repository = repository; - this.v2Repository = v2Repository; - this.openAIAnalyticsService = openAIAnalyticsService; - this.minIOService = minIOService; - this.researchPdfService = researchPdfService; - this.serperSearchService = serperSearchService; - } - @PostConstruct public void initializeSemaphore() { this.requestSemaphore = new Semaphore(maxConcurrentRequests, true); @@ -473,17 +461,14 @@ public class MarketingAnalysisService { String topic = (niche != null && !niche.isBlank()) ? niche : (product != null ? product : "ниша"); - // Prefer the requested domains but keep query broad enough to return something. String preferredSites = "(site:stat.gov.kz OR site:kapital.kz OR site:kursiv.media OR site:forbes.kz OR site:pwc.kz OR site:kpmg.kz)"; - // Added for real estate/auto context if needed - String extraSites = "OR site:kolesa.kz OR site:krisha.kz OR site:market.kz OR site:hh.kz"; + String retailSites = "site:2gis.kz OR site:kaspi.kz OR site:market.kz OR site:kolesa.kz OR site:krisha.kz OR site:chocofood.kz"; List general = new ArrayList<>(); - String query1 = topic + " рынок " + regionStr + " 2024 2025 статистика объем рост " + preferredSites; + String query1 = topic + " рынок " + regionStr + " 2024 2025 статистика объем рост " + preferredSites + retailSites; try { general.add(serperSearchService.search(query1)); } catch (Exception e) { - // Handle DNS/network errors... (keeping existing error handling) String errorType = "Unexpected error"; if (e instanceof java.net.UnknownHostException || e instanceof java.net.ConnectException) errorType = "DNS/Network failure"; @@ -493,7 +478,7 @@ public class MarketingAnalysisService { general.add(new SerperSearchResult(query1, "ERROR", errorType + ": " + e.getMessage(), List.of())); } - String query2 = "инфляция Казахстан 2024 2025 курс тенге 2024 2025 логистика импорт " + preferredSites; + String query2 = "инфляция Казахстан 2024 2025 курс тенге 2024 2025 логистика импорт " + preferredSites + retailSites; try { general.add(serperSearchService.search(query2)); } catch (Exception e) { @@ -559,14 +544,19 @@ public class MarketingAnalysisService { followups.put("income", new SerperSearchResult(incomeQuery, "ERROR", e.getMessage(), List.of())); } - String demandQuery = topic + " сезонность спроса динамика популярности " + regionStr + " когда покупают google trends"; + String demandQuery = String.format("когда сезон %s в %s динамика спроса google trends", topic, regionStr); try { followups.put("demand", serperSearchService.search(demandQuery)); } catch (Exception e) { - logger.warn("Serper API error for demand query: {}", e.getMessage()); - followups.put("demand", new SerperSearchResult(demandQuery, "ERROR", e.getMessage(), List.of())); + logger.warn("Serper error demand: {}", e.getMessage()); } + String channelsQuery = String.format("%s реклама кейсы казахстан instagram tiktok kaspi маркетинг таргетолог", topic); + try { + followups.put("channels", serperSearchService.search(channelsQuery)); + } catch (Exception e) { + logger.warn("Serper error channels: {}", e.getMessage()); + } pack.put("general", general); pack.put("followups", followups); @@ -649,10 +639,6 @@ public class MarketingAnalysisService { return List.of(analysis); } - /** - * V2 endpoint payload for frontend integration testing. - * Generates a strict JSON response via OpenAI, grounded on Serper web evidence. - */ public MarketingAnalysisResponseV2 generateAnalysisV2(MarketingAnalysisRequest request) { logger.info("Starting Marketing Analysis V2 (Big 4 Standard) for request: {}", request); @@ -664,7 +650,6 @@ public class MarketingAnalysisService { MarketingAnalysisResponseV2 exampleDto = buildAnalysisV2ExampleForPrompt(request, competitorCompanies); exampleDto.setSources(List.of("https://stat.gov.kz/example", "https://forbes.kz/example")); String structureExampleJson = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(exampleDto); - // ------------------------------------------------------------------------- String systemPrompt = buildSystemPromptForKazakhstan(); String instruction = buildDetailedInstruction(request, competitorCompanies, structureExampleJson); @@ -674,28 +659,24 @@ public class MarketingAnalysisService { "evidence", researchPack )); - String lastCleanedResponse = null; for (int attempt = 1; attempt <= 2; attempt++) { - - String aiResponse = openAIAnalyticsService.generateWithInstructionWithModel( - contextJson, - instruction, - "ru", - textModelName, - systemPrompt, - 16000, - 180000L - ); - - String cleaned = cleanJsonFromMarkdown(aiResponse); - // ... (очистка \uFEFF и валидация плейсхолдеров) - try { + String aiResponse = openAIAnalyticsService.generateWithInstructionWithModel( + contextJson, + instruction, + "ru", + textModelName, + systemPrompt, + 16000, + 180000L + ); + + String cleaned = cleanJsonFromMarkdown(aiResponse); MarketingAnalysisResponseV2 parsed = objectMapper.readValue(cleaned, MarketingAnalysisResponseV2.class); replacePlaceholderCompanyKeysIfNeeded(parsed, competitorCompanies); return parsed; } catch (Exception e) { - logger.error("JSON Parsing failed on attempt {}: {}", attempt, e.getMessage()); + logger.error("JSON Parsing/Generation failed on attempt {}: {}", attempt, e.getMessage()); if (attempt == 2) throw e; } } @@ -706,79 +687,67 @@ public class MarketingAnalysisService { throw new RuntimeException("Не удалось сгенерировать маркетинговый анализ V2: " + e.getMessage(), e); } } - /** - * Строит системный промпт, задающий ролевую модель и контекст Казахстана. - */ + private String buildSystemPromptForKazakhstan() { - return """ - Ты — Партнер международной консалтинговой фирмы (Big 4: McKinsey, BCG, PwC) с офисом в Алматы. - Твой опыт: 50 лет в маркетинговой стратегии и аналитике рынков Центральной Азии. - - ТВОЯ СПЕЦИАЛИЗАЦИЯ — КАЗАХСТАН (KZ): - 1. **Экономика**: Ты мыслишь в Тенге (KZT). Ты понимаешь реальную покупательскую способность, закредитованность населения и инфляцию. - 2. **Платформы**: Ты знаешь, что в РК **Kaspi.kz** — это экосистема №1 (Shop, Travel, Pay). - Ты знаешь, что **Instagram** и **TikTok** продают лучше, чем сайты. - Ты знаешь, что **2GIS** важнее для локального бизнеса, чем Google Maps. - Ты практически игнорируешь "ВКонтакте", "Яндекс.Маркет" или "Ozon", если только это не специфичный трансграничный кейс. - 3. **Менталитет**: Ты учитываешь важность рекомендаций ("сарафан"), семейных связей, статусности потребления и любовь к рассрочкам (Kaspi Red/Kredit). - - ТВОЙ СТИЛЬ: - - Жесткий, фактологический, структурный. - - Никакой "воды" и общих фраз ("динамично развивающийся рынок"). Только инсайты. - - Ты презираешь выдуманные данные. Если точной цифры нет в evidence — ты делаешь обоснованную аналитическую эстимацию (Analytical Extrapolation), базируясь на смежных метриках. - - ФОРМАТ ОТВЕТА: - - ТОЛЬКО валидный JSON. - - Никакого Markdown форматирования (```json ... ```) вокруг ответа. - """; + return KZ_MARKET_CONTEXT + + "\nФОРМАТ ОТВЕТА:\n" + + "- ТОЛЬКО валидный JSON.\n" + + "- Никакого Markdown форматирования (```json ... ```) вокруг ответа.\n" + + "- Ключи JSON должны быть в snake_case (например, market_overview, monthly_online_index).\n" + + "- Запрещено возвращать null в полях (особенно income_by_age) — делай экстраполяцию если нет точных данных.\n"; } - /** - * Строит детальную инструкцию по заполнению полей JSON. - */ private String buildDetailedInstruction(MarketingAnalysisRequest request, List competitorCompanies, String structureExampleJson) { - String ageRangesInstruction = ""; - // ... (логика формирования ageRangesInstruction остается прежней) + + List regions = request.getRegion(); + String regionsInstruction; + if (regions != null && !regions.isEmpty()) { + String regionsList = String.join(", ", regions); + regionsInstruction = "ГЕОГРАФИЯ (СТРОГО ОБЯЗАТЕЛЬНО):\n" + + "В секции 'market_overview.geography' ты ОБЯЗАН вернуть данные ДЛЯ КАЖДОГО города из этого списка: [" + regionsList + "].\n" + + "НЕ ПРОПУСКАЙ ни одного города из списка. Значение (0-100) должно отражать относительный спрос."; + } else { + regionsInstruction = "ГЕОГРАФИЯ: Алматы, Астана, Шымкент (по умолчанию)."; + } + + String channelsInstruction = "КАНАЛЫ (РК SPECS):\n" + + "- В 'acquisition_channels' обязательно включи: Instagram, TikTok, Kaspi Marketing, 2GIS, Google Ads.\n" + + "- ЗАПРЕЩЕНО включать или 'ВКонтакте' как платные каналы (они не работают в РК). Если они есть в примере - удали их.\n" + + "- Оценивай эффективность реально: 2GIS работает локально, Kaspi дает горячих лидов."; + + String audienceInstruction = "АУДИТОРИЯ:\n" + + "- Следи, чтобы возрастные группы в 'age_structure' НЕ пересекались по логике, но если входные данные пересекаются - нормализуй их так, чтобы сумма % была равна 100%.\n" + + "- Поле 'income_by_age' ОБЯЗАТЕЛЬНО для заполнения (средний доход в KZT)."; return String.format(""" - Твоя задача: Провести глубокий маркетинговый анализ и вернуть его СТРОГО в формате JSON. + Твоя задача: Провести глубокий маркетинговый анализ для рынка Казахстана и вернуть JSON. Ниша: %s Продукт: %s - Регион: %s - - ПРАВИЛА ЗАПОЛНЕНИЯ: - 1. **СТРУКТУРА**: Ты обязан использовать структуру JSON, приведенную ниже. - - НЕ переименовывай ключи (например, используй `competitive_environment`, а не `competitors`). - - НЕ пропускай секции. - - Заполни все поля `EXAMPLE__` и `-9999` реальными данными и аналитическими оценками на основе `evidence`. - - 2. **ИСТОЧНИКИ (sources)**: - - В корневом поле `sources` перечисли ВСЕ реальные URL-адреса из `evidence`, которые ты использовал. - - Это обязательное требование для фронтенда. - - 3. **ЛОКАЛИЗАЦИЯ (Казахстан)**: - - Валюта: Тенге (KZT). - - Каналы: Instagram, Kaspi, 2GIS, TikTok. - - Данные: Опирайся на Бюро национальной статистики РК и локальные отчеты. - - 4. **ФОРМАТ**: - - Числа вместо -9999. - - Строки вместо EXAMPLE__. - - Только валидный JSON без Markdown. %s - ЭТАЛОННАЯ СТРУКТУРА ДАННЫХ ДЛЯ ЗАПОЛНЕНИЯ: + %s + + %s + + ПРАВИЛА ЦИФР И ЭКОНОМИКИ: + 1. Все цены в KZT. + 2. Округляй до красивых чисел (например, 15 000, а не 14 321). + 3. Прибыльность: Будь реалистом. Маржа в офлайн-бизнесе редко превышает 20-30%. Не рисуй сверхприбыли. + 4. В полях cost/value пиши строку с валютой (например "5 000 KZT"). + 5. В полях status/efficiency пиши понятные оценки: "High Impact", "Low Efficiency", "Moderate". + + ЭТАЛОННАЯ СТРУКТУРА JSON (Snake Case): %s """, request.getBusinessNiche(), request.getProduct(), - request.getRegion() != null ? request.getRegion() : "Казахстан", - ageRangesInstruction, + regionsInstruction, + channelsInstruction, + audienceInstruction, structureExampleJson ); } - private String buildJsonStructureExample() { return """ { @@ -796,56 +765,6 @@ public class MarketingAnalysisService { """; } - private boolean containsV2ExamplePlaceholders(String json) { - if (json == null || json.isBlank()) { - return true; - } - // Explicit placeholder tokens from buildAnalysisV2ExampleForPrompt() - if (json.contains("EXAMPLE__")) { - return true; - } - // Numeric placeholders from buildAnalysisV2ExampleForPrompt() - if (json.contains("-9999")) { - return true; - } - - // Check placeholder competitor keys specifically inside competitive_environment - // maps. - // This avoids false positives for normal Russian words like "лид/оплата" - // appearing in conclusions. - try { - JsonNode root = objectMapper.readTree(json); - JsonNode ce = root.at("/sections/competitive_environment"); - if (ce == null || ce.isMissingNode()) { - return false; - } - - JsonNode dvb = ce.get("digital_visibility_benchmarks"); - if (dvb != null && dvb.isObject()) { - Iterator it = dvb.fieldNames(); - while (it.hasNext()) { - if (isPlaceholderCompetitorKey(it.next())) { - return true; - } - } - } - - JsonNode strengths = ce.get("competitor_strengths"); - if (strengths != null && strengths.isObject()) { - Iterator it = strengths.fieldNames(); - while (it.hasNext()) { - if (isPlaceholderCompetitorKey(it.next())) { - return true; - } - } - } - return false; - } catch (Exception e) { - // If we can't parse JSON here, treat as unusable and retry. - return true; - } - } - private boolean isPlaceholderCompetitorKey(String key) { if (key == null) { return false; @@ -862,38 +781,61 @@ public class MarketingAnalysisService { } private List resolveCompetitorCompaniesForBenchmarks(MarketingAnalysisRequest request, - Map researchPack, - int limit) { - int max = Math.max(1, Math.min(limit, 10)); + Map researchPack, + int limit) { + int max = Math.max(3, Math.min(limit, 15)); LinkedHashSet names = new LinkedHashSet<>(); - SerperSearchResult fromPack = null; if (researchPack != null) { Object followupsObj = researchPack.get("followups"); if (followupsObj instanceof Map followups) { - Object compObj = followups.get("competitors"); - if (compObj instanceof SerperSearchResult sr) { - fromPack = sr; + if (followups.get("competitors") instanceof SerperSearchResult sr) { + names.addAll(extractCompanyNamesFromSerper(sr, max)); + } + if (names.size() < 3 && followups.get("market") instanceof SerperSearchResult sr) { + names.addAll(extractCompanyNamesFromSerper(sr, max)); + } + } + if (names.isEmpty() && researchPack.get("general") instanceof List list) { + for (Object item : list) { + if (item instanceof SerperSearchResult sr) { + names.addAll(extractCompanyNamesFromSerper(sr, max)); + } } } } - names.addAll(extractCompanyNamesFromSerper(fromPack, max)); - // If competitors query returned mostly directories/articles, do a second query - // to get more brand-like results. - if (names.size() < max) { - String topic = deriveTopic(request); - String regionStr = deriveRegionStr(request); - String q = topic + " " + regionStr + " официальный сайт"; + String topic = deriveTopic(request); + String regionStr = deriveRegionStr(request); + + if (names.size() < 4) { + String qRating = String.format("топ 10 лучших компаний %s %s рейтинг список", topic, regionStr); try { - SerperSearchResult extra = serperSearchService.search(q); - names.addAll(extractCompanyNamesFromSerper(extra, max - names.size())); - } catch (Exception ignored) { - } + SerperSearchResult ratingResult = serperSearchService.search(qRating); + names.addAll(extractCompanyNamesFromSerper(ratingResult, max - names.size())); + } catch (Exception ignored) {} } + if (names.size() < 4) { + String qMaps = String.format("%s магазины %s адреса 2gis", topic, regionStr); + try { + SerperSearchResult mapsResult = serperSearchService.search(qMaps); + names.addAll(extractCompanyNamesFromSerper(mapsResult, max - names.size())); + } catch (Exception ignored) {} + } + + + + names.removeIf(n -> { + String lower = n.toLowerCase().trim(); + if (lower.length() < 2 || lower.length() > 50) return true; + if (lower.contains("обзор") || lower.contains("отзывы") || lower.contains("как выбрать")) return true; + return false; + }); + List out = new ArrayList<>(names); - return out.size() > max ? out.subList(0, max) : out; + int resultLimit = Math.min(out.size(), Math.max(limit, 3)); + return out.subList(0, resultLimit); } private String deriveTopic(MarketingAnalysisRequest request) { @@ -948,7 +890,6 @@ public class MarketingAnalysisService { } h = h.startsWith("www.") ? h.substring(4) : h; - // Ignore obvious aggregators/social networks. String[] blocked = new String[] { "google.", "yandex.", "2gis.", "instagram.", "facebook.", "vk.", "tiktok.", "youtube.", "wikipedia." }; @@ -1074,13 +1015,7 @@ public class MarketingAnalysisService { } } - /** - * Генерирует анализ v2 и сохраняет его в MongoDB - * - * @param request Запрос на анализ - * @param userId ID пользователя - * @return ID сохраненного анализа - */ + public String generateAndSaveAnalysisV2(MarketingAnalysisRequest request, String userId) { try { // Генерируем анализ @@ -1110,32 +1045,14 @@ public class MarketingAnalysisService { } } - /** - * Получает сохраненный анализ v2 по ID - * - * @param id ID анализа - * @return Документ анализа или null, если не найден - */ public Optional getAnalysisV2ById(String id) { return v2Repository.findById(id); } - /** - * Обновляет анализ в базе данных - * - * @param analysis Анализ для обновления - * @return Обновленный анализ - */ public MarketingAnalysis updateAnalysis(MarketingAnalysis analysis) { return repository.save(analysis); } - /** - * Связывает v2 анализ с обычным анализом - * - * @param v2AnalysisId ID v2 анализа - * @param analysisId ID обычного анализа - */ public void linkV2AnalysisToAnalysis(String v2AnalysisId, String analysisId) { Optional optV2 = v2Repository.findById(v2AnalysisId); if (optV2.isPresent()) { @@ -1146,24 +1063,10 @@ public class MarketingAnalysisService { } } - /** - * Получает v2 анализ по ID обычного анализа - * - * @param analysisId ID обычного анализа - * @return Документ v2 анализа или null, если не найден - */ public Optional getV2AnalysisByAnalysisId(String analysisId) { return v2Repository.findByAnalysisId(analysisId); } - /** - * Example payload used ONLY for prompting the AI with the expected JSON - * schema/shape. - * This is not returned to the client. - * - * @param request Запрос на анализ для динамического формирования примера - * age_structure - */ private MarketingAnalysisResponseV2 buildAnalysisV2ExampleForPrompt(MarketingAnalysisRequest request, List competitorCompanies) { MarketingAnalysisResponseV2 response = new MarketingAnalysisResponseV2(); @@ -1171,7 +1074,6 @@ public class MarketingAnalysisService { MarketingAnalysisResponseV2.Sections sections = new MarketingAnalysisResponseV2.Sections(); - // market_overview MarketingAnalysisResponseV2.MarketOverview marketOverview = new MarketingAnalysisResponseV2.MarketOverview(); marketOverview.setTitle("EXAMPLE__MARKET_OVERVIEW_TITLE"); MarketingAnalysisResponseV2.MarketOverviewMetrics moMetrics = new MarketingAnalysisResponseV2.MarketOverviewMetrics(); @@ -1202,7 +1104,6 @@ public class MarketingAnalysisService { marketOverview.setConclusion("EXAMPLE__MARKET_OVERVIEW_CONCLUSION"); sections.setMarketOverview(marketOverview); - // target_audience MarketingAnalysisResponseV2.TargetAudience targetAudience = new MarketingAnalysisResponseV2.TargetAudience(); targetAudience.setTitle("EXAMPLE__TARGET_AUDIENCE_TITLE"); MarketingAnalysisResponseV2.TargetAudienceMetrics taMetrics = new MarketingAnalysisResponseV2.TargetAudienceMetrics(); @@ -1217,28 +1118,22 @@ public class MarketingAnalysisService { "EXAMPLE__AUDIENCE_SEGMENT_SHARE_2"), new MarketingAnalysisResponseV2.AudienceSegment("EXAMPLE__AUDIENCE_SEGMENT_3", "EXAMPLE__AUDIENCE_SEGMENT_SHARE_3"))); - // Динамически формируем age_structure на основе реальных данных из запроса Map ageStructure = new LinkedHashMap<>(); Map targetAudienceData = request.getTargetAudience(); if (targetAudienceData != null && targetAudienceData.containsKey("ageRanges")) { @SuppressWarnings("unchecked") List ageRanges = (List) targetAudienceData.get("ageRanges"); if (ageRanges != null && !ageRanges.isEmpty()) { - // Используем только те возрастные группы, которые указаны в запросе for (String ageRange : ageRanges) { - // Нормализуем формат ключа: "55+" -> "55_plus", остальные оставляем как есть String normalizedKey = ageRange.replace("+", "_plus"); - // Формируем плейсхолдер для примера String placeholderKey = normalizedKey.replace("-", "_").toUpperCase(); ageStructure.put(normalizedKey, "EXAMPLE__AGE_" + placeholderKey + "_SHARE"); } } else { - // Если возрастные группы не указаны, используем дефолтный пример ageStructure.put("25-34", "EXAMPLE__AGE_25_34_SHARE"); ageStructure.put("35-44", "EXAMPLE__AGE_35_44_SHARE"); } } else { - // Если targetAudience не указан, используем дефолтный пример ageStructure.put("25-34", "EXAMPLE__AGE_25_34_SHARE"); ageStructure.put("35-44", "EXAMPLE__AGE_35_44_SHARE"); } @@ -1246,7 +1141,6 @@ public class MarketingAnalysisService { targetAudience.setConclusion("EXAMPLE__TARGET_AUDIENCE_CONCLUSION"); sections.setTargetAudience(targetAudience); - // competitive_environment MarketingAnalysisResponseV2.CompetitiveEnvironment competitiveEnvironment = new MarketingAnalysisResponseV2.CompetitiveEnvironment(); competitiveEnvironment.setTitle("EXAMPLE__COMPETITIVE_ENVIRONMENT_TITLE"); MarketingAnalysisResponseV2.CompetitiveEnvironmentMetrics ceMetrics = new MarketingAnalysisResponseV2.CompetitiveEnvironmentMetrics(); @@ -1273,7 +1167,6 @@ public class MarketingAnalysisService { competitiveEnvironment.setConclusion("EXAMPLE__COMPETITIVE_ENVIRONMENT_CONCLUSION"); sections.setCompetitiveEnvironment(competitiveEnvironment); - // demand_and_behavior MarketingAnalysisResponseV2.DemandAndBehavior demandAndBehavior = new MarketingAnalysisResponseV2.DemandAndBehavior(); demandAndBehavior.setTitle("EXAMPLE__DEMAND_AND_BEHAVIOR_TITLE"); MarketingAnalysisResponseV2.DemandAndBehaviorMetrics dabMetrics = new MarketingAnalysisResponseV2.DemandAndBehaviorMetrics(); @@ -1303,7 +1196,6 @@ public class MarketingAnalysisService { demandAndBehavior.setConclusion("EXAMPLE__DEMAND_AND_BEHAVIOR_CONCLUSION"); sections.setDemandAndBehavior(demandAndBehavior); - // customer_journey MarketingAnalysisResponseV2.CustomerJourney customerJourney = new MarketingAnalysisResponseV2.CustomerJourney(); customerJourney.setTitle("EXAMPLE__CUSTOMER_JOURNEY_TITLE"); MarketingAnalysisResponseV2.FunnelMetrics funnelMetrics = new MarketingAnalysisResponseV2.FunnelMetrics(); @@ -1557,11 +1449,7 @@ public class MarketingAnalysisService { } } - /** - * Генерация структурированного JSON с данными для графиков - * Использует модульный подход: три отдельных запроса к OpenAI для повышения - * детализации - */ + private Map generateJsonAnalysis(MarketingAnalysisRequest request) { Map result = new HashMap<>(); @@ -1686,7 +1574,7 @@ public class MarketingAnalysisService { StringBuilder promptBuilder = new StringBuilder(); promptBuilder.append( - "Ты — профессиональный маркетинговый аналитик и психолог, работающий с малым и средним бизнесом в Казахстане.\n\n"); + "Ты — профессиональный маркетинговый аналитик и психолог с 50 летним опытом, работающий с малым и средним бизнесом в Казахстане.\n\n"); promptBuilder.append( "Твоя задача — на основе входных данных о бизнесе выполнить ГЛУБОКИЙ анализ целевой аудитории с психологическим профилированием сегментов.\n\n"); promptBuilder.append( @@ -1831,15 +1719,10 @@ public class MarketingAnalysisService { } } - /** - * Генерация анализа конкурентов - */ private Map getCompetitorAnalysis(MarketingAnalysisRequest request) { try { String context = buildContextString(request); - // Mandatory web search evidence (Serper.dev) to find REAL competitors in - // KZ/Almaty. String niche = request != null ? request.getBusinessNiche() : null; String product = request != null ? request.getProduct() : null; List regionsForSearch = request != null ? request.getRegion() : null; @@ -2238,10 +2121,7 @@ public class MarketingAnalysisService { return new HashMap<>(); } - /** - * Генерация анализа стратегии, воронки, каналов и SWOT - * Модульный метод для 4-го этапа генерации - */ + private Map getStrategyAndFunnel(MarketingAnalysisRequest request) { try { String context = buildContextString(request); @@ -2702,30 +2582,13 @@ public class MarketingAnalysisService { for (String p : noDataSentencePatterns) { cleaned = cleaned.replaceAll(p, "Экстраполяция: применяем консервативные допущения и методику расчета."); } - - // 2) Remove lingering short phrases (in case model wrote it without - // punctuation) cleaned = cleaned.replaceAll("(?i)\\b(нет\\s*данных|данные\\s*отсутствуют|нет\\s*информации)\\b", ""); - - // 3) Optionally remove any leftover placeholders if (removePlaceholders) { cleaned = cleaned.replaceAll("(?i)\\[\\[\\s*(CHART|TABLE)_[A-Za-z0-9_]+\\s*\\]\\]", ""); } - - // Normalize whitespace cleaned = cleaned.replaceAll("(\\r?\\n){3,}", "\n\n").trim(); return cleaned; } - - /** - * Удаляет «пустые» фразы-заглушки про графики, если рядом нет плейсхолдера - * [[CHART_...]] / [[TABLE_...]] или ```json: блока. - * - * Мотивация: модель может написать «Анализ данных на графике ниже - * показывает...», но забыть - * вставить плейсхолдер. В результате в тексте остаётся пустая ссылка на «график - * ниже». - */ private String removeDanglingChartNarration(String text) { if (text == null || text.trim().isEmpty()) { return text;