Parcourir la source

feat(api): 招商门户功能

Signed-off-by: sunshihao <sunshihaoemail>
sunshihao il y a 4 jours
Parent
commit
7cc06ca703
23 fichiers modifiés avec 1245 ajouts et 31 suppressions
  1. 108 0
      xinkeaboard-server/b2b2c-core/src/main/java/com/slodon/b2b2c/core/util/HttpClientUtil.java
  2. 57 0
      xinkeaboard-server/b2b2c-investment/pom.xml
  3. 27 0
      xinkeaboard-server/b2b2c-investment/src/main/java/com/slodon/b2b2c/investment/bean/bo/CompetitorBO.java
  4. 23 0
      xinkeaboard-server/b2b2c-investment/src/main/java/com/slodon/b2b2c/investment/bean/bo/MonthlySearchesBO.java
  5. 96 0
      xinkeaboard-server/b2b2c-investment/src/main/java/com/slodon/b2b2c/investment/bean/bo/RankBO.java
  6. 29 0
      xinkeaboard-server/b2b2c-investment/src/main/java/com/slodon/b2b2c/investment/bean/bo/RecommendationBO.java
  7. 22 0
      xinkeaboard-server/b2b2c-investment/src/main/java/com/slodon/b2b2c/investment/bean/bo/RelatedInfoBO.java
  8. 22 0
      xinkeaboard-server/b2b2c-investment/src/main/java/com/slodon/b2b2c/investment/bean/bo/TrafficBO.java
  9. 24 0
      xinkeaboard-server/b2b2c-investment/src/main/java/com/slodon/b2b2c/investment/bean/dto/RivalDTO.java
  10. 22 0
      xinkeaboard-server/b2b2c-investment/src/main/java/com/slodon/b2b2c/investment/bean/dto/SearchVolumeDTO.java
  11. 29 0
      xinkeaboard-server/b2b2c-investment/src/main/java/com/slodon/b2b2c/investment/bean/vo/KeyWordPartVO.java
  12. 20 0
      xinkeaboard-server/b2b2c-investment/src/main/java/com/slodon/b2b2c/investment/bean/vo/RivalPartVO.java
  13. 20 0
      xinkeaboard-server/b2b2c-investment/src/main/java/com/slodon/b2b2c/investment/bean/vo/SuggestionVO.java
  14. 27 0
      xinkeaboard-server/b2b2c-investment/src/main/java/com/slodon/b2b2c/investment/config/AnalysisConfig.java
  15. 55 0
      xinkeaboard-server/b2b2c-investment/src/main/java/com/slodon/b2b2c/investment/constant/AnalysisConst.java
  16. 118 0
      xinkeaboard-server/b2b2c-investment/src/main/java/com/slodon/b2b2c/investment/controller/AnalysisController.java
  17. 463 0
      xinkeaboard-server/b2b2c-investment/src/main/java/com/slodon/b2b2c/investment/model/AnalysisModel.java
  18. 6 0
      xinkeaboard-server/b2b2c-web/pom.xml
  19. 61 30
      xinkeaboard-server/b2b2c-web/src/main/java/com/slodon/b2b2c/filter/ResponseI18NFilter.java
  20. 1 1
      xinkeaboard-server/b2b2c-web/src/main/java/com/slodon/b2b2c/interceptor/BearerTokenConfiguration.java
  21. 13 0
      xinkeaboard-server/b2b2c-web/src/main/resources/application-dev.yml
  22. 1 0
      xinkeaboard-server/b2b2c-web/src/main/resources/application.yml
  23. 1 0
      xinkeaboard-server/pom.xml

+ 108 - 0
xinkeaboard-server/b2b2c-core/src/main/java/com/slodon/b2b2c/core/util/HttpClientUtil.java

@@ -94,6 +94,114 @@ public class HttpClientUtil {
         return result;
     }
 
+
+    /**
+     * 发送HTTP POST请求,使用String类型作为请求体参数
+     * @param url 请求URL
+     * @param params 请求体参数(通常为JSON字符串)
+     * @param headers 请求头信息
+     * @return 响应结果字符串
+     * @throws Exception 可能抛出的异常
+     */
+    public static String httpPost(String url, String params, HashMap<String, String> headers) throws Exception {
+        String result = "";
+        CloseableHttpClient httpClient = HttpClients.createDefault();
+        HttpPost httpPost = new HttpPost(url);
+
+        // 设置最大请求和传输超时时间
+        RequestConfig requestConfig = RequestConfig.custom()
+                .setSocketTimeout(50000)
+                .setConnectTimeout(50000)
+                .build();
+        httpPost.setConfig(requestConfig);
+
+        // 设置请求头
+        if (headers != null && !headers.isEmpty()) {
+            for (String key : headers.keySet()) {
+                httpPost.addHeader(key, headers.get(key));
+            }
+        }
+
+        // 设置请求体参数
+        if (params != null && !params.isEmpty()) {
+            // 使用StringEntity直接设置字符串作为请求体
+            StringEntity stringEntity = new StringEntity(params, StandardCharsets.UTF_8);
+            httpPost.setEntity(stringEntity);
+        }
+
+        CloseableHttpResponse response = httpClient.execute(httpPost);
+
+        try {
+            if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
+                HttpEntity entity = response.getEntity();
+                result = EntityUtils.toString(entity, StandardCharsets.UTF_8);
+                EntityUtils.consume(entity);
+            } else {
+                throw new Exception(response.getStatusLine().toString());
+            }
+        } finally {
+            response.close();
+            httpClient.close();
+        }
+        return result;
+    }
+
+
+
+    /**
+     * http POST 请求,支持自定义请求头
+     *
+     * @param url     请求URL
+     * @param params  请求参数
+     * @param headers 请求头信息
+     * @return 响应结果字符串
+     * @throws Exception 异常信息
+     */
+    public static String httpPost(String url, HashMap<String, Object> params, HashMap<String, String> headers) throws Exception {
+        String result = "";
+        CloseableHttpClient httpClient = HttpClients.createDefault();
+        HttpPost httpPost = new HttpPost(url);
+
+        // 设置最大请求和传输超时时间
+        RequestConfig requestConfig = RequestConfig.custom()
+                .setSocketTimeout(5000)
+                .setConnectTimeout(5000)
+                .build();
+        httpPost.setConfig(requestConfig);
+
+        // 设置请求头
+        if (headers != null && !headers.isEmpty()) {
+            for (String key : headers.keySet()) {
+                httpPost.addHeader(key, headers.get(key));
+            }
+        }
+
+        // 拼接请求参数
+        java.util.List<NameValuePair> nvps = new ArrayList<>();
+        if (params != null && !params.isEmpty()) {
+            for (String key : params.keySet()) {
+                nvps.add(new BasicNameValuePair(key, params.get(key).toString()));
+            }
+        }
+        httpPost.setEntity(new UrlEncodedFormEntity(nvps, StandardCharsets.UTF_8));
+
+        CloseableHttpResponse response = httpClient.execute(httpPost);
+
+        try {
+            if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
+                HttpEntity entity = response.getEntity();
+                result = EntityUtils.toString(entity, StandardCharsets.UTF_8);
+                EntityUtils.consume(entity);
+            } else {
+                throw new Exception(response.getStatusLine().toString());
+            }
+        } finally {
+            response.close();
+            httpClient.close(); // 补充关闭httpClient,释放资源
+        }
+        return result;
+    }
+
     /**
      * http POST 请求
      *

+ 57 - 0
xinkeaboard-server/b2b2c-investment/pom.xml

@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>b2b2c</artifactId>
+        <groupId>com.slodon</groupId>
+        <version>3.0</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>b2b2c-investment</artifactId>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.slodon</groupId>
+            <artifactId>b2b2c-entity</artifactId>
+            <version>3.0</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.slodon</groupId>
+            <artifactId>b2b2c-core</artifactId>
+            <version>3.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-validation</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+            <version>3.12.0</version>
+        </dependency>
+
+        <!--   knife4j     -->
+        <dependency>
+            <groupId>com.github.xiaoymin</groupId>
+            <artifactId>knife4j-spring-boot-starter</artifactId>
+            <version>2.0.4</version>
+        </dependency>
+
+        <!-- Lombok 依赖 -->
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <version>1.18.2</version>
+            <optional>true</optional>
+        </dependency>
+
+    </dependencies>
+
+</project>

+ 27 - 0
xinkeaboard-server/b2b2c-investment/src/main/java/com/slodon/b2b2c/investment/bean/bo/CompetitorBO.java

@@ -0,0 +1,27 @@
+package com.slodon.b2b2c.investment.bean.bo;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * @author sunshihao
+ * @version 1.0
+ * @description: 竞品网站信息
+ * @date 2025/8/7 16:20
+ */
+@Data
+public class CompetitorBO {
+    @ApiModelProperty("竞品网站")
+    private String website;
+
+    @ApiModelProperty("折线图对象")
+    private List<TrafficBO> trafficBOList;
+
+    @ApiModelProperty("排名表格")
+    private List<RankBO> rankBOList;
+
+    @ApiModelProperty("推荐表格")
+    private List<RecommendationBO> recommendationBOList;
+}

+ 23 - 0
xinkeaboard-server/b2b2c-investment/src/main/java/com/slodon/b2b2c/investment/bean/bo/MonthlySearchesBO.java

@@ -0,0 +1,23 @@
+package com.slodon.b2b2c.investment.bean.bo;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+/**
+ * @author sunshihao
+ * @version 1.0
+ * @description: TODO
+ * @date 2025/8/7 11:19
+ */
+@Data
+public class MonthlySearchesBO {
+    @ApiModelProperty("年份")
+    private  Integer year;
+
+    @ApiModelProperty("搜索量")
+    private  Long search_volume;
+
+    @ApiModelProperty("月份")
+    private  Integer month;
+
+}

+ 96 - 0
xinkeaboard-server/b2b2c-investment/src/main/java/com/slodon/b2b2c/investment/bean/bo/RankBO.java

@@ -0,0 +1,96 @@
+package com.slodon.b2b2c.investment.bean.bo;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+/**
+ * @author sunshihao
+ * @version 1.0
+ * @description: TODO
+ * @date 2025/8/6 15:34
+ */
+@Data
+public class RankBO {
+    @ApiModelProperty("关键词")
+    private  String keyword;
+
+    @ApiModelProperty("月平均搜索量")
+    private  Long searchVolume;
+
+    @ApiModelProperty("月度变化")
+    private  Long monthly;
+
+    @ApiModelProperty("季度变化")
+    private  Long quarterly;
+
+    @ApiModelProperty("年度变化")
+    private  Long yearly;
+
+    @ApiModelProperty("难度")
+    private  Integer dp;
+
+    @ApiModelProperty("每次点击费用")
+    private  Double cpc;
+
+    public static RankBOBuilder builder() {
+        return new RankBOBuilder();
+    }
+
+    public static class RankBOBuilder {
+        private String keyword;
+        private Long searchVolume;
+        private Long monthly;
+        private Long quarterly;
+        private Long yearly;
+        private Integer dp;
+        private Double cpc;
+
+        public RankBOBuilder keyword(String keyword) {
+            this.keyword = keyword;
+            return this;
+        }
+
+        public RankBOBuilder searchVolume(Long searchVolume) {
+            this.searchVolume = searchVolume;
+            return this;
+        }
+
+        public RankBOBuilder monthly(Long monthly) {
+            this.monthly = monthly;
+            return this;
+        }
+
+        public RankBOBuilder quarterly(Long quarterly) {
+            this.quarterly = quarterly;
+            return this;
+        }
+
+        public RankBOBuilder yearly(Long yearly) {
+            this.yearly = yearly;
+            return this;
+        }
+
+        public RankBOBuilder dp(Integer dp) {
+            this.dp = dp;
+            return this;
+        }
+
+        public RankBOBuilder cpc(Double cpc) {
+            this.cpc = cpc;
+            return this;
+        }
+
+        public RankBO build() {
+            RankBO rankBO = new RankBO();
+            rankBO.setKeyword(this.keyword);
+            rankBO.setSearchVolume(this.searchVolume);
+            rankBO.setMonthly(this.monthly);
+            rankBO.setQuarterly(this.quarterly);
+            rankBO.setYearly(this.yearly);
+            rankBO.setDp(this.dp);
+            rankBO.setCpc(this.cpc);
+            return rankBO;
+        }
+    }
+
+}

+ 29 - 0
xinkeaboard-server/b2b2c-investment/src/main/java/com/slodon/b2b2c/investment/bean/bo/RecommendationBO.java

@@ -0,0 +1,29 @@
+package com.slodon.b2b2c.investment.bean.bo;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+/**
+ * @author sunshihao
+ * @version 1.0
+ * @description: 推荐投放信息
+ * @date 2025/8/6 15:16
+ */
+@Data
+public class RecommendationBO {
+    /**
+     * 关键词
+     */
+    @ApiModelProperty("关键词")
+    private String keywords;
+    /**
+     * 搜索量
+     */
+    @ApiModelProperty("搜索量")
+    private Long searchVolume;
+    /**
+     * 价格
+     */
+    @ApiModelProperty("价格")
+    private Double price;
+}

+ 22 - 0
xinkeaboard-server/b2b2c-investment/src/main/java/com/slodon/b2b2c/investment/bean/bo/RelatedInfoBO.java

@@ -0,0 +1,22 @@
+package com.slodon.b2b2c.investment.bean.bo;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+/**
+ * @author sunshihao
+ * @version 1.0
+ * @description: 相关关键词信息
+ * @date 2025/8/6 15:26
+ */
+@Data
+public class RelatedInfoBO {
+    @ApiModelProperty("搜索量")
+    private Long searchVolume;
+
+    @ApiModelProperty("关键词难度")
+    private Integer competition;
+
+    @ApiModelProperty("关键词")
+    private String keyword;
+}

+ 22 - 0
xinkeaboard-server/b2b2c-investment/src/main/java/com/slodon/b2b2c/investment/bean/bo/TrafficBO.java

@@ -0,0 +1,22 @@
+package com.slodon.b2b2c.investment.bean.bo;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+/**
+ * @author sunshihao
+ * @version 1.0
+ * @description: TODO
+ * @date 2025/8/6 15:31
+ */
+@Data
+public class TrafficBO {
+    @ApiModelProperty("x轴")
+    private int x_axis;
+
+    @ApiModelProperty("自然流量")
+    private Long organic;
+
+    @ApiModelProperty("付费流量")
+    private Long paid;
+}

+ 24 - 0
xinkeaboard-server/b2b2c-investment/src/main/java/com/slodon/b2b2c/investment/bean/dto/RivalDTO.java

@@ -0,0 +1,24 @@
+package com.slodon.b2b2c.investment.bean.dto;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+
+/**
+ * @author sunshihao
+ * @version 1.0
+ * @description: 入参
+ * @date 2025/8/5 13:22
+ */
+@Data
+public class RivalDTO {
+    @ApiModelProperty("目标区域")
+    @NotBlank(message = "目标区域不能为空")
+    private String locationName;
+
+    @ApiModelProperty("竞品网站(多个逗号隔开)")
+    @NotBlank(message = "竞品网站不能为空")
+    private String competitorWebsite;
+
+}

+ 22 - 0
xinkeaboard-server/b2b2c-investment/src/main/java/com/slodon/b2b2c/investment/bean/dto/SearchVolumeDTO.java

@@ -0,0 +1,22 @@
+package com.slodon.b2b2c.investment.bean.dto;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+
+/**
+ * @author sunshihao
+ * @version 1.0
+ * @description: TODO
+ * @date 2025/8/7 11:46
+ */
+@Data
+public class SearchVolumeDTO {
+    @ApiModelProperty("产品名称")
+    @NotBlank(message = "关键词不能为空")
+    private String productName;
+    @ApiModelProperty("目标区域")
+    @NotBlank(message = "目标区域不能为空")
+    private String locationName;
+}

+ 29 - 0
xinkeaboard-server/b2b2c-investment/src/main/java/com/slodon/b2b2c/investment/bean/vo/KeyWordPartVO.java

@@ -0,0 +1,29 @@
+package com.slodon.b2b2c.investment.bean.vo;
+
+import com.slodon.b2b2c.investment.bean.bo.MonthlySearchesBO;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * @author sunshihao
+ * @version 1.0
+ * @description: 关键词部分
+ * @date 2025/8/6 15:21
+ */
+@Data
+public class KeyWordPartVO {
+    @ApiModelProperty("互联网关键词")
+    private List<String> keywords;
+
+    @ApiModelProperty("最近月搜索量")
+    private MonthlySearchesBO monthlySearchesBO;
+
+    @ApiModelProperty("关键词难度")
+    private Integer competition;
+
+    @ApiModelProperty("英文表达")
+    private String keywordEn;
+
+}

+ 20 - 0
xinkeaboard-server/b2b2c-investment/src/main/java/com/slodon/b2b2c/investment/bean/vo/RivalPartVO.java

@@ -0,0 +1,20 @@
+package com.slodon.b2b2c.investment.bean.vo;
+
+import com.slodon.b2b2c.investment.bean.bo.CompetitorBO;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * @author sunshihao
+ * @version 1.0
+ * @description: 竞品部分
+ * @date 2025/8/6 15:29
+ */
+@Data
+public class RivalPartVO {
+    @ApiModelProperty("竞品网站信息")
+    private List<CompetitorBO> competitorBOS;
+
+}

+ 20 - 0
xinkeaboard-server/b2b2c-investment/src/main/java/com/slodon/b2b2c/investment/bean/vo/SuggestionVO.java

@@ -0,0 +1,20 @@
+package com.slodon.b2b2c.investment.bean.vo;
+
+import com.slodon.b2b2c.investment.bean.bo.RelatedInfoBO;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * @author sunshihao
+ * @version 1.0
+ * @description: 相关关键词搜索量及难度
+ * @date 2025/8/7 14:44
+ */
+@Data
+public class SuggestionVO {
+    @ApiModelProperty("相关关键词搜索量及难度")
+    private List<RelatedInfoBO> relatedInfoBOList;
+
+}

+ 27 - 0
xinkeaboard-server/b2b2c-investment/src/main/java/com/slodon/b2b2c/investment/config/AnalysisConfig.java

@@ -0,0 +1,27 @@
+package com.slodon.b2b2c.investment.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+/**
+ * @author sunshihao
+ * @version 1.0
+ * @description: 配置类
+ * @date 2025/8/5 14:20
+ */
+@Component
+@ConfigurationProperties(prefix = "analysis")
+@Data
+public class AnalysisConfig {
+    private String translateAddress;
+    private String loginUser;
+    private String password;
+    private String searchVolume;
+    private String keywordsForSite;
+    private String KeywordForSuggestions;
+    private String rankedKeywords;
+    private String historicalTraffic;
+    private String modelName;
+
+}

+ 55 - 0
xinkeaboard-server/b2b2c-investment/src/main/java/com/slodon/b2b2c/investment/constant/AnalysisConst.java

@@ -0,0 +1,55 @@
+package com.slodon.b2b2c.investment.constant;
+
+/**
+ * @author sunshihao
+ * @version 1.0
+ * @description: TODO
+ * @date 2025/8/5 15:33
+ */
+public class AnalysisConst {
+    /**
+     * 默认固定线程数
+     */
+    public static final int DEFAULT_RUN_THREAD_NUM = 2;
+
+    public static final int NUM_ZERO = 0;
+    public static final int NUM_ONE = 1;
+    public static final int NUM_TWO = 2;
+    public static final int NUM_THREE = 3;
+    public static final int NUM_FOUR = 4;
+    public static final long NUM_TEN = 10;
+    public static final int NUM_FIFTY = 50;
+    public static final int NUM_ONE_HUNDRED = 100;
+    public static final int NEGATIVE_ONE = -1;
+    /**
+     * 线程池存活时间
+     */
+    public static final long KEEP_ALIVE_TIME = 5000L;
+    /**
+     * 等待队列大小
+     */
+    public static final int WORK_QUEUE = 2;
+    public static final Double ONE_HUNDRED_DOUBLE = 100.0;
+    public static final String CONTENT_TYPE = "Content-Type";
+    public static final String APPLICATION_JSON = "application/json";
+    public static final String TEXT = "text";
+    public static final String MODEL_NAME = "model_name";
+    public static final String AUTHORIZATION = "Authorization";
+    public static final String KEYWORDS = "keywords";
+    public static final String LANGUAGE_NAME = "language_name";
+    public static final String LOCATION_NAME = "location_name";
+    public static final String ENGLISH = "English";
+    public static final String LIMIT = "limit";
+    public static final String TARGET = "target";
+    public static final String HTTPS = "https://";
+    public static final String HTTP = "http://";
+    public static final String WWW = "www.";
+    public static final String COM = ".com";
+    public static final String TARGETS = "targets";
+    public static final String KEYWORD = "keyword";
+    public static final String CHINESE = "chinese";
+    public static final String LAST_PART_MARKER = "```\n" +
+            "\n" +
+            "## 参考资料";
+    public static final String FIRST_MARKER = "## 4. 引用";
+}

+ 118 - 0
xinkeaboard-server/b2b2c-investment/src/main/java/com/slodon/b2b2c/investment/controller/AnalysisController.java

@@ -0,0 +1,118 @@
+package com.slodon.b2b2c.investment.controller;
+
+import com.slodon.b2b2c.core.response.JsonResult;
+import com.slodon.b2b2c.core.response.SldResponse;
+
+import com.slodon.b2b2c.investment.bean.dto.RivalDTO;
+import com.slodon.b2b2c.investment.bean.dto.SearchVolumeDTO;
+import com.slodon.b2b2c.investment.bean.vo.KeyWordPartVO;
+import com.slodon.b2b2c.investment.bean.vo.RivalPartVO;
+import com.slodon.b2b2c.investment.bean.vo.SuggestionVO;
+import com.slodon.b2b2c.investment.model.AnalysisModel;
+import io.swagger.annotations.Api;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.HashMap;
+import java.util.List;
+
+
+/**
+ * @author sunshihao
+ * @version 1.0
+ * @description: 定量分析控制层
+ * @date 2025/8/5 10:56
+ */
+
+@Api(tags = "招商分析控制")
+@RestController
+@RequestMapping("/analysis")
+@Slf4j
+public class AnalysisController {
+    @Resource
+    private AnalysisModel analysisModel;
+
+    /***
+     * @description: 接口1 返回页面左上 关键词搜索量 关键词难度
+     * @param:
+     * @param dto
+     * @author sunshihao
+     * @date: 2025/8/7 13:46
+     */
+    @PostMapping("/keyword")
+    public JsonResult<KeyWordPartVO> searchVolume(@RequestBody SearchVolumeDTO dto) {
+        HashMap<String,String> headers = analysisModel.getCredential();
+        KeyWordPartVO result;
+        try {
+        List<String> keyword = analysisModel.translateProduct(dto.getProductName());
+        result = analysisModel.searchVolume(headers, keyword.get(0), dto.getLocationName());
+        result.setKeywordEn(keyword.get(0));
+        result.setKeywords(keyword);
+        }catch (Exception e) {
+                log.error(e.getMessage(),e);
+            return SldResponse.fail("Analysis failed: " + e.getMessage());
+        }
+        return SldResponse.success(result);
+    }
+
+    @PostMapping("/suggestions")
+    public JsonResult<SuggestionVO> suggestions(@RequestBody SearchVolumeDTO dto) {
+        HashMap<String,String> headers = analysisModel.getCredential();
+        SuggestionVO result;
+        try {
+            List<String> keyword = analysisModel.translateProduct(dto.getProductName());
+            result = analysisModel.KeywordForSuggestions(headers, keyword.get(0), dto.getLocationName());
+        }catch (Exception e) {
+            log.error(e.getMessage(),e);
+            return SldResponse.fail("Analysis failed: " + e.getMessage());
+        }
+        return SldResponse.success(result);
+    }
+
+
+
+    /***
+     * @description: 竞品网站信息
+     * @param dto
+     * @return: com.slodon.b2b2c.core.response.JsonResult<com.slodon.b2b2c.investment.bean.vo.RivalPartVO>
+     * @author sunshihao
+     * @date: 2025/8/8 13:23
+     */
+    @PostMapping("/rival")
+    public JsonResult<RivalPartVO> rival(@RequestBody RivalDTO dto) {
+        HashMap<String,String> headers = analysisModel.getCredential();
+        RivalPartVO result;
+        try {
+            result = analysisModel.rival(headers, dto);
+        }catch (Exception e) {
+            log.error(e.getMessage(),e);
+            return SldResponse.fail("Analysis failed: " + e.getMessage());
+        }
+        return SldResponse.success(result);
+    }
+
+
+
+    /***
+     * @description: 定性分析
+     * @param:
+     * @param keyword  关键词
+     * @return: com.slodon.b2b2c.core.response.JsonResult<java.lang.String>
+     * @author sunshihao
+     * @date: 2025/8/8 13:30
+     */
+    @GetMapping("/qualitative")
+    public JsonResult<String> qualitative(@RequestParam String keyword) {
+        String result;
+        try {
+            result = analysisModel.qualitative(keyword);
+        }catch (Exception e) {
+            log.error(e.getMessage(),e);
+            return SldResponse.fail("Analysis failed: " + e.getMessage());
+        }
+        return SldResponse.success(result);
+    }
+
+
+}

+ 463 - 0
xinkeaboard-server/b2b2c-investment/src/main/java/com/slodon/b2b2c/investment/model/AnalysisModel.java

@@ -0,0 +1,463 @@
+package com.slodon.b2b2c.investment.model;
+
+import cn.hutool.json.JSONArray;
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import com.slodon.b2b2c.core.util.HttpClientUtil;
+import com.slodon.b2b2c.investment.bean.bo.*;
+import com.slodon.b2b2c.investment.bean.dto.RivalDTO;
+import com.slodon.b2b2c.investment.bean.vo.KeyWordPartVO;
+import com.slodon.b2b2c.investment.bean.vo.RivalPartVO;
+import com.slodon.b2b2c.investment.bean.vo.SuggestionVO;
+import com.slodon.b2b2c.investment.config.AnalysisConfig;
+import com.slodon.b2b2c.investment.constant.AnalysisConst;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+import java.nio.charset.StandardCharsets;
+import javax.annotation.Resource;
+import java.util.concurrent.*;
+
+
+/**
+ * @author sunshihao
+ * @version 1.0
+ * @description: 定量分析模型
+ * @date 2025/8/5 11:24
+ */
+@Component
+@Slf4j
+public class AnalysisModel {
+
+
+    @Resource
+    private AnalysisConfig analysisConfig;
+
+
+
+/***
+ * @description:翻译产品名称
+ * @param originalText 各语言名称
+ * @return: java.lang.String
+ * @author sunshihao
+ * @date: 2025/8/5 13:52
+ */
+public List<String> translateProduct(String originalText) throws Exception {
+    HashMap<String, String> headers = new HashMap<>();
+    headers.put(AnalysisConst.CONTENT_TYPE, AnalysisConst.APPLICATION_JSON);
+    JSONObject param = new JSONObject();
+    param.set(AnalysisConst.TEXT, originalText);
+    param.set(AnalysisConst.MODEL_NAME, analysisConfig.getModelName());
+    String result;
+    try {
+        result = HttpClientUtil.httpPost(analysisConfig.getTranslateAddress() + "/api/translate", param.toString(), headers);
+    } catch (Exception e) {
+        log.error(e.getMessage(), "产品翻译接口出错");
+        throw new Exception("产品翻译接口出错", e);
+    }
+    JSONObject jsonObject = JSONUtil.parseObj(result);
+    String text = jsonObject.getByPath("translated_text", String.class);
+    if (StringUtils.isEmpty(text)) {
+        throw new Exception("翻译结果为空");
+    }
+    int start = text.indexOf("[");
+    int end = text.lastIndexOf("]") + AnalysisConst.NUM_ONE;
+    String jsonString = "";
+    if (start != AnalysisConst.NEGATIVE_ONE && end != AnalysisConst.NEGATIVE_ONE && start < end) {
+        jsonString = text.substring(start, end);
+    } else {
+        throw new Exception("未找到翻译结果");
+    }
+    return JSONUtil.parseArray(jsonString).toList(String.class);
+}
+
+
+public HashMap<String,String> getCredential(){
+    String credentials = analysisConfig.getLoginUser() + ":" + analysisConfig.getPassword();
+    // 进行Base64编码
+    String base64Creds = Base64.getEncoder()
+            .encodeToString(credentials.getBytes(StandardCharsets.UTF_8));
+    // 创建请求头Map
+    HashMap<String, String> headers = new HashMap<>();
+    headers.put(AnalysisConst.CONTENT_TYPE, AnalysisConst.APPLICATION_JSON);
+    headers.put(AnalysisConst.AUTHORIZATION, "Basic " + base64Creds);
+    return headers;
+}
+
+
+private ThreadPoolExecutor createThreadPool() {
+    return new ThreadPoolExecutor(
+            AnalysisConst.DEFAULT_RUN_THREAD_NUM,
+            AnalysisConst.DEFAULT_RUN_THREAD_NUM,
+            AnalysisConst.KEEP_ALIVE_TIME,
+            TimeUnit.MILLISECONDS,
+            new LinkedBlockingQueue<>(AnalysisConst.WORK_QUEUE),
+            new ThreadFactory() {
+                private int count = AnalysisConst.NUM_ZERO;
+                @Override
+                public Thread newThread(Runnable r) {
+                    Thread thread = new Thread(r);
+                    thread.setName("dataForSeo-api-call-thread-" + count++);
+                    thread.setDaemon(true);
+                    return thread;
+                }
+            },
+            new ThreadPoolExecutor.CallerRunsPolicy()
+    );
+}
+    private void shutdownThreadPool(ThreadPoolExecutor executor) {
+        executor.shutdown();
+        try {
+            if (!executor.awaitTermination(AnalysisConst.NUM_TEN, TimeUnit.SECONDS)) {
+                executor.shutdownNow();
+            }
+        } catch (InterruptedException e) {
+            executor.shutdownNow();
+        }
+    }
+
+
+
+/***
+ * @description: 接口1
+ * @param header 请求头
+ * @param keywords 关键词
+ * @param locationName 目标市场
+ * @return: com.slodon.b2b2c.investment.bean.vo.KeyWordPartVO
+ * @author sunshihao
+ * @date: 2025/8/7 11:54
+ */
+public KeyWordPartVO searchVolume(HashMap<String,String> header,String keywords,String locationName) throws Exception {
+    KeyWordPartVO keyWordPartVO = new KeyWordPartVO();
+    JSONArray payload = new JSONArray();
+    JSONObject payloadItem = new JSONObject();
+    JSONArray keywordsArray = new JSONArray();
+    keywordsArray.add(keywords);
+    payloadItem.set(AnalysisConst.KEYWORDS, keywordsArray);
+    payloadItem.set(AnalysisConst.LANGUAGE_NAME, AnalysisConst.ENGLISH);
+    payloadItem.set(AnalysisConst.LOCATION_NAME, locationName);
+    payload.put(payloadItem);
+    String post = HttpClientUtil.httpPost(analysisConfig.getSearchVolume(), payload.toString(),header);
+    JSONObject postObject = JSONUtil.parseObj(post);
+    Integer tasksError = postObject.getByPath("tasks_error", Integer.class);
+    if(tasksError==null||tasksError> AnalysisConst.NUM_ZERO){
+        throw new RuntimeException("接口1查询关键词 按月返回搜索量/CPC search_volume 任务失败");
+    }
+    MonthlySearchesBO bo = postObject.getByPath("tasks[0].result[0].monthly_searches[0]", MonthlySearchesBO.class);
+    Double competition = postObject.getByPath("tasks[0].result[0].competition",Double.class);
+    Integer cp = competition==null?null:(int)Math.round(competition * AnalysisConst.NUM_ONE_HUNDRED);
+    keyWordPartVO.setMonthlySearchesBO(bo);
+    keyWordPartVO.setCompetition(cp);
+    return keyWordPartVO;
+}
+
+
+/***
+ * @description: 上方右侧推荐数据
+ * @param:
+ * @param header 请求头
+ * @param keyword 关键词
+ * @param locationName 地区
+ * @return: com.slodon.b2b2c.investment.bean.vo.SuggestionVO
+ * @author sunshihao
+ * @date: 2025/8/7 15:49
+ */
+public SuggestionVO KeywordForSuggestions(HashMap<String,String> header, String keyword, String locationName) throws Exception {
+    SuggestionVO suggestionVO = new SuggestionVO();
+    JSONArray payload = new JSONArray();
+    JSONObject payloadItem = new JSONObject();
+    payloadItem.set(AnalysisConst.KEYWORD, keyword);
+    payloadItem.set(AnalysisConst.LANGUAGE_NAME, AnalysisConst.ENGLISH);
+    payloadItem.set(AnalysisConst.LOCATION_NAME, locationName);
+    payloadItem.set("include_serp_info", true);
+    payloadItem.set("include_seed_keyword", true);
+    payloadItem.set("include_seed_keyword", true);
+    payloadItem.set(AnalysisConst.LIMIT, AnalysisConst.NUM_TEN);
+    payload.put(payloadItem);
+    String post = HttpClientUtil.httpPost(analysisConfig.getKeywordForSuggestions(), payload.toString(),header);
+    JSONObject postObject = JSONUtil.parseObj(post);
+    Integer tasksError = postObject.getByPath("tasks_error", Integer.class);
+    if(tasksError==null||tasksError> AnalysisConst.NUM_ZERO){
+        throw new RuntimeException("接口5查询推荐数据 任务失败");
+    }
+    JSONArray jsonArray = postObject.getByPath("tasks[0].result[0].items", JSONArray.class);
+    if(jsonArray==null|| jsonArray.isEmpty()){
+        return suggestionVO;
+    }
+    List<RelatedInfoBO> relatedInfoBOList = new ArrayList<>();
+    for (Object o : jsonArray) {
+        RelatedInfoBO bo = new RelatedInfoBO();
+        JSONObject obj = (JSONObject) o;
+        bo.setKeyword(obj.getByPath(AnalysisConst.KEYWORD, String.class));
+        bo.setSearchVolume(obj.getByPath("keyword_info.search_volume", Long.class));
+        Double competition = obj.getByPath("keyword_info.competition", Double.class);
+        bo.setCompetition(competition==null?null:(int)Math.round(competition * AnalysisConst.NUM_ONE_HUNDRED));
+        relatedInfoBOList.add(bo);
+    }
+    suggestionVO.setRelatedInfoBOList(relatedInfoBOList);
+    return suggestionVO;
+}
+
+
+
+
+    public RivalPartVO rival(HashMap<String, String> header, RivalDTO dto) throws Exception {
+        String websites = dto.getCompetitorWebsite();
+        String[] competitorWebsites = websites.split(",");
+        RivalPartVO vo = new RivalPartVO();
+        if(competitorWebsites.length== AnalysisConst.NUM_ZERO){
+            throw new Exception("竞品网站地址不正确");
+        }
+        List<CompetitorBO> competitorBOS = new ArrayList<>();
+        ThreadPoolExecutor executor = createThreadPool();
+        try {
+            for (String competitorWebsite : competitorWebsites) {
+                CompetitorBO bo = new CompetitorBO();
+                bo.setWebsite(competitorWebsite);
+                // 推荐投放关键词表格(接口3)
+                CompletableFuture<List<RecommendationBO>> recommendationBOListFuture = CompletableFuture.supplyAsync(
+                        () -> {
+                            try {
+                                return keywordsForSite(header, competitorWebsite, dto.getLocationName());
+                            } catch (Exception e) {
+                                throw new RuntimeException(e);
+                            }
+                        },
+                        executor
+                );
+                // 折线图表格(接口8)
+                CompletableFuture<List<TrafficBO>> trafficBOListFuture = CompletableFuture.supplyAsync(
+                        () -> {
+                            try {
+                                return historicalTraffic(header, competitorWebsite, dto.getLocationName());
+                            } catch (Exception e) {
+                                throw new RuntimeException(e);
+                            }
+                        },
+                        executor
+                );
+                // 排名表格(接口7)
+                CompletableFuture<List<RankBO>> rankBOListFuture = CompletableFuture.supplyAsync(
+                        () -> {
+                            try {
+                                return rankedKeywords(header, competitorWebsite, dto.getLocationName());
+                            } catch (Exception e) {
+                                throw new RuntimeException(e);
+                            }
+                        },
+                        executor
+                );
+                CompletableFuture.allOf(recommendationBOListFuture, trafficBOListFuture,rankBOListFuture).join();
+                bo.setRecommendationBOList(recommendationBOListFuture.get());
+                bo.setTrafficBOList(trafficBOListFuture.join());
+                bo.setRankBOList(rankBOListFuture.get());
+                competitorBOS.add(bo);
+            }
+        }finally {
+            shutdownThreadPool(executor);
+        }
+        vo.setCompetitorBOS(competitorBOS);
+        return vo;
+    }
+
+
+    // 接口7
+    public List<RankBO> rankedKeywords(HashMap<String,String> header,String website,String locationName) throws Exception {
+        JSONArray payload = new JSONArray();
+        JSONObject payloadItem = new JSONObject();
+        payloadItem.set(AnalysisConst.TARGET, website);
+        payloadItem.set(AnalysisConst.LANGUAGE_NAME, AnalysisConst.ENGLISH);
+        payloadItem.set(AnalysisConst.LOCATION_NAME, locationName);
+        payload.put(payloadItem);
+        String post = HttpClientUtil.httpPost(analysisConfig.getRankedKeywords(), payload.toString(),header);
+        JSONObject postObject = JSONUtil.parseObj(post);
+        Integer tasksError = postObject.getByPath("tasks_error", Integer.class);
+        if(tasksError==null||tasksError> AnalysisConst.NUM_ZERO){
+            throw new RuntimeException("接口7查询 关键词排名 任务失败");
+        }
+        JSONArray items = postObject.getByPath("tasks[0].result[0].items", JSONArray.class);
+        List<RankBO> list = new ArrayList<>();
+        if(items==null|| items.isEmpty()){
+            return list;
+        }
+        for (Object item : items) {
+            JSONObject obj = (JSONObject) item;
+            String keyword = obj.getByPath("keyword_data.keyword", String.class);
+            Long searchVolume = obj.getByPath("keyword_data.keyword_info.search_volume", Long.class);
+            Double competition = obj.getByPath("keyword_data.keyword_info.competition", Double.class);
+            Integer dp = competition==null?null:(int)Math.round(competition * AnalysisConst.NUM_ONE_HUNDRED);
+            Double cpc = obj.getByPath("keyword_data.keyword_info.cpc", Double.class);
+            Double cpcFormat=cpc==null?null:Math.round(cpc * AnalysisConst.NUM_ONE_HUNDRED) / AnalysisConst.ONE_HUNDRED_DOUBLE;
+            Long monthly = obj.getByPath("keyword_data.keyword_info.search_volume_trend.monthly", Long.class);
+            Long quarterly = obj.getByPath("keyword_data.keyword_info.search_volume_trend.quarterly", Long.class);
+            Long yearly = obj.getByPath("keyword_data.keyword_info.search_volume_trend.yearly", Long.class);
+            RankBO bo = RankBO.builder()
+                    .keyword(keyword)
+                    .searchVolume(searchVolume)
+                    .monthly(monthly)
+                    .quarterly(quarterly)
+                    .yearly(yearly)
+                    .dp(dp)
+                    .cpc(cpcFormat)
+                    .build();
+            list.add(bo);
+        }
+        return list;
+    }
+
+    // 接口8
+    public List<TrafficBO> historicalTraffic(HashMap<String,String> header,String website,String locationName) throws Exception {
+        JSONArray payload = new JSONArray();
+        JSONObject payloadItem = new JSONObject();
+        JSONArray targets = new JSONArray();
+        String keyword = StringUtils.removeStart(
+            StringUtils.removeStart(
+                StringUtils.removeStart(website, AnalysisConst.HTTPS),
+                    AnalysisConst.HTTP),
+                AnalysisConst.WWW);
+        int comIndex = keyword.indexOf(AnalysisConst.COM);
+        if (comIndex != -AnalysisConst.NEGATIVE_ONE) {
+            keyword =  keyword.substring(AnalysisConst.NUM_ZERO, comIndex+ AnalysisConst.NUM_FOUR);
+        }
+        targets.add(keyword);
+        payloadItem.set(AnalysisConst.TARGETS,targets);
+        payloadItem.set(AnalysisConst.LANGUAGE_NAME, AnalysisConst.ENGLISH);
+        payloadItem.set(AnalysisConst.LOCATION_NAME, locationName);
+        payload.put(payloadItem);
+        LocalDate currentDate = LocalDate.now();
+        LocalDate currentMonthFirstDay = currentDate.withDayOfMonth(AnalysisConst.NUM_ONE);
+        LocalDate lastYearSameMonthFirstDay = currentDate.minusYears(AnalysisConst.NUM_ONE).plusMonths(AnalysisConst.NUM_ONE).withDayOfMonth(AnalysisConst.NUM_ONE);
+        DateTimeFormatter formatter = DateTimeFormatter.ISO_DATE;
+        payloadItem.set("date_from", lastYearSameMonthFirstDay.format(formatter));
+        payloadItem.set("date_to", currentMonthFirstDay.format(formatter));
+        JSONArray itemTypes = new JSONArray();
+        itemTypes.put("organic");
+        itemTypes.put("paid");
+        payloadItem.set("item_types", itemTypes);
+        JSONObject postData = new JSONObject();
+        postData.set("0", payloadItem);
+
+        String post = HttpClientUtil.httpPost(analysisConfig.getHistoricalTraffic(), postData.toString(),header);
+        JSONObject postObject = JSONUtil.parseObj(post);
+        Integer tasksError = postObject.getByPath("tasks_error", Integer.class);
+        if(tasksError==null||tasksError> AnalysisConst.NUM_ZERO){
+            throw new RuntimeException("接口8查询 历史流量 任务失败");
+        }
+        List<TrafficBO> list = new ArrayList<>();
+        JSONArray organics = postObject.getByPath("tasks[0].result[0].items[0].metrics.organic", JSONArray.class);
+        JSONArray paids = postObject.getByPath("tasks[0].result[0].items[0].metrics.paid", JSONArray.class);
+        Map<Integer, Long> temp = new HashMap<>();
+        if(organics!=null&&!organics.isEmpty()){
+            for (Object organic : organics) {
+                JSONObject obj = (JSONObject) organic;
+                int month = obj.getByPath("month", Integer.class);
+                long organicCount = obj.getByPath("count", Long.class);
+                temp.put(month,organicCount);
+            }
+        }
+        if(paids!=null&&!paids.isEmpty()){
+            for (Object paid : paids) {
+                TrafficBO bo = new TrafficBO();
+                JSONObject obj = (JSONObject) paid;
+                int month = obj.getByPath("month", Integer.class);
+                long paidCount = obj.getByPath("count", Long.class);
+                long organicCount = temp.get(month);
+                bo.setX_axis(month);
+                bo.setPaid(paidCount);
+                bo.setOrganic(organicCount);
+                list.add(bo);
+            }
+        }
+        Collections.reverse(list);
+        return list;
+    }
+
+    // 接口3
+    public List<RecommendationBO> keywordsForSite(HashMap<String, String> header, String website, String locationName) throws Exception {
+        JSONArray payload = new JSONArray();
+        JSONObject payloadItem = new JSONObject();
+        payloadItem.set(AnalysisConst.TARGET, website);
+        payloadItem.set(AnalysisConst.LANGUAGE_NAME, AnalysisConst.ENGLISH);
+        payloadItem.set(AnalysisConst.LOCATION_NAME, locationName);
+        payloadItem.set(AnalysisConst.LIMIT, AnalysisConst.NUM_FIFTY);
+        payload.put(payloadItem);
+        String post = HttpClientUtil.httpPost(analysisConfig.getKeywordsForSite(), payload.toString(),header);
+        JSONObject postObject = JSONUtil.parseObj(post);
+        Integer tasksError = postObject.getByPath("tasks_error", Integer.class);
+        if(tasksError==null||tasksError> AnalysisConst.NUM_ZERO){
+            throw new RuntimeException("接口3查询 keywordsForSite 任务失败");
+        }
+        List<RecommendationBO> list = new ArrayList<>();
+        JSONArray results = postObject.getByPath("tasks[0].result", JSONArray.class);
+        if(results==null){
+            return list;
+        }
+        int total = Math.min(results.size(), AnalysisConst.NUM_FIFTY);
+        for(int i = AnalysisConst.NUM_ZERO; i<total; i++){
+            JSONObject result = (JSONObject) results.get(i);
+            RecommendationBO bo = new RecommendationBO();
+            bo.setKeywords(result.getByPath(AnalysisConst.KEYWORD,String.class));
+            bo.setSearchVolume(result.getByPath("search_volume",Long.class));
+            Double cpc = result.getByPath("cpc",Double.class);
+            Double cpcFormat=cpc==null?null:Math.round(cpc * AnalysisConst.NUM_ONE_HUNDRED) / AnalysisConst.ONE_HUNDRED_DOUBLE;
+            bo.setPrice(cpcFormat);
+            list.add(bo);
+        }
+        return list;
+    }
+
+    public String qualitative(String keyword) throws Exception {
+        HashMap<String, String> headers = new HashMap<>();
+        headers.put(AnalysisConst.CONTENT_TYPE, AnalysisConst.APPLICATION_JSON);
+        JSONObject param = new JSONObject();
+        param.set("question", keyword);
+        param.set("initial_search_query_count", AnalysisConst.NUM_THREE);
+        param.set("max_research_loops", AnalysisConst.NUM_THREE);
+        param.set("reasoning_model", analysisConfig.getModelName());
+        String result;
+        try {
+            result = HttpClientUtil.httpPost(analysisConfig.getTranslateAddress() + "/api/research", param.toString(), headers);
+            JSONObject jsonObject = JSONUtil.parseObj(result);
+            String answer = jsonObject.getByPath("answer",String.class);
+            if(StringUtils.isEmpty(answer)){
+                return "";
+            }
+        return dealAnswer(answer);
+        } catch (Exception e) {
+            log.error(e.getMessage(), "定性分析接口出错");
+            throw new Exception("定性分析接口出错", e);
+        }
+    }
+
+    private String dealAnswer(String answer) {
+        answer = answer.replace(AnalysisConst.CHINESE, "");
+        String lastPart = getLastPart(answer);
+        String firstPart = getFirstPart(answer);
+        return firstPart + lastPart;
+    }
+
+    private String getLastPart(String answer) {
+        int startIndex = answer.indexOf (AnalysisConst.LAST_PART_MARKER);
+        if (startIndex != AnalysisConst.NEGATIVE_ONE) {
+            return answer.substring (startIndex);
+        } else {
+            return "";
+        }
+    }
+
+    public String getFirstPart (String input) {
+        int markerIndex = input.indexOf (AnalysisConst.FIRST_MARKER);
+        if (markerIndex != AnalysisConst.NEGATIVE_ONE) {
+            return input.substring (AnalysisConst.NUM_ZERO, markerIndex);
+        } else {
+            return input;
+        }
+    }
+}
+
+
+

+ 6 - 0
xinkeaboard-server/b2b2c-web/pom.xml

@@ -24,6 +24,12 @@
             <version>3.0</version>
         </dependency>
 
+        <dependency>
+            <groupId>com.slodon</groupId>
+            <artifactId>b2b2c-investment</artifactId>
+            <version>3.0</version>
+        </dependency>
+
         <!--    bbc common    -->
         <dependency>
             <groupId>com.slodon</groupId>

+ 61 - 30
xinkeaboard-server/b2b2c-web/src/main/java/com/slodon/b2b2c/filter/ResponseI18NFilter.java

@@ -36,47 +36,70 @@ public class ResponseI18NFilter implements Filter, Ordered {
         HttpServletRequest request = (HttpServletRequest) servletRequest;
         HttpServletResponse response = (HttpServletResponse) servletResponse;
         ResponseWrapper responseWrapper = new ResponseWrapper(response);
-        filterChain.doFilter(request, responseWrapper);
 
-        if (request.getRequestURI().contains("api-docs")) {
-            response.getOutputStream().write(responseWrapper.getContent());
-            return;
-        }
+        try {
+            filterChain.doFilter(request, responseWrapper);
 
-        if (request.getRequestURI().contains("v3/oss/ueditor/upload")) {
-            return;
+            // 检查是否是需要跳过的路径
+            if (request.getRequestURI().contains("api-docs") ||
+                    request.getRequestURI().contains("v3/oss/ueditor/upload")) {
+                writeResponse(response, responseWrapper);
+                return;
+            }
+
+            // 检查响应类型
+            if (!isJsonResponse(responseWrapper)) {
+                writeResponse(response, responseWrapper);
+                return;
+            }
+
+            // 处理JSON响应
+            processJsonResponse(request, response, responseWrapper);
+        } finally {
+            // 确保资源被正确释放
+            try {
+                responseWrapper.close();
+            } catch (Exception e) {
+                log.warn("Error closing response wrapper", e);
+            }
         }
+    }
 
-        if (!responseWrapper.getContentType().equalsIgnoreCase("application/json;charset=utf-8")) {
-            //响应类型不是json,不处理
+    private void writeResponse(HttpServletResponse response, ResponseWrapper responseWrapper) throws IOException {
+        if (!response.isCommitted()) {
             response.getOutputStream().write(responseWrapper.getContent());
-            return;
         }
+    }
 
-        //响应二进制流
-        byte[] content = responseWrapper.getContent();
-        String s = new String(content, StandardCharsets.UTF_8);
-        JsonResult jsonResult;
+    private boolean isJsonResponse(ResponseWrapper responseWrapper) {
+        String contentType = responseWrapper.getContentType();
+        return contentType != null && contentType.toLowerCase().contains("application/json");
+    }
+
+    private void processJsonResponse(HttpServletRequest request, HttpServletResponse response, ResponseWrapper responseWrapper) throws IOException {
         try {
-            jsonResult = JSON.parseObject(s, JsonResult.class);
-        } catch (Exception e) {
-            //转换异常,直接跳过
-            response.getOutputStream().write(content);
-            return;
-        }
+            byte[] content = responseWrapper.getContent();
+            String s = new String(content, StandardCharsets.UTF_8);
+            JsonResult jsonResult = JSON.parseObject(s, JsonResult.class);
+
+            if (jsonResult != null && !StringUtils.isEmpty(jsonResult.getMsg())) {
+                String languageType = request.getHeader("Language");
+                if (StringUtils.isEmpty(languageType)) {
+                    languageType = Language.DEFAULT_LANGUAGE_TYPE;
+                }
+                jsonResult.setMsg(Language.translate(jsonResult.getMsg(), languageType));
+            }
 
-        if (!StringUtils.isEmpty(jsonResult.getMsg())) {
-            //从请求头中获取语言类型
-            String languageType = request.getHeader("Language");
-            if (StringUtils.isEmpty(languageType)) {
-                languageType = Language.DEFAULT_LANGUAGE_TYPE;
+            if (!response.isCommitted()) {
+                response.getOutputStream().write(JSON.toJSONString(jsonResult, SerializerFeature.WriteMapNullValue).getBytes());
+            }
+        } catch (Exception e) {
+            log.error("Error processing JSON response for translation", e);
+            // 出现异常时返回原始内容
+            if (!response.isCommitted()) {
+                response.getOutputStream().write(responseWrapper.getContent());
             }
-            //翻译
-            //对响应中的msg字段进行翻译
-            jsonResult.setMsg(Language.translate(jsonResult.getMsg(), languageType));
         }
-        //重新写入响应
-        response.getOutputStream().write(JSON.toJSONString(jsonResult, SerializerFeature.WriteMapNullValue).getBytes());
     }
 
     @Override
@@ -93,6 +116,14 @@ public class ResponseI18NFilter implements Filter, Ordered {
      * 响应包装类
      */
     public class ResponseWrapper extends HttpServletResponseWrapper {
+        public void close() throws IOException {
+            if (out != null) {
+                out.close();
+            }
+            if (buffer != null) {
+                buffer.close();
+            }
+        }
 
         private ByteArrayOutputStream buffer;
 

+ 1 - 1
xinkeaboard-server/b2b2c-web/src/main/java/com/slodon/b2b2c/interceptor/BearerTokenConfiguration.java

@@ -13,7 +13,7 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
  */
 @Configuration
 public class BearerTokenConfiguration implements WebMvcConfigurer {
-    public static String[] urlList = new String[]{"/openapi/**"};
+    public static String[] urlList = new String[]{"/openapi/**","/analysis/**"};
 
     @Bean
     public BearerTokenInterceptor tokenConfiguration(){

+ 13 - 0
xinkeaboard-server/b2b2c-web/src/main/resources/application-dev.yml

@@ -53,3 +53,16 @@ geoip:
   static:
     city:
       mmdb: /data/GeoLite2/GeoLite2-City.mmdb
+
+
+#外部接口
+analysis:
+  translateAddress: http://54.46.9.88:8007
+  loginUser: metaljacket@meshinfo.cn
+  password: d088821c1d6863ec
+  searchVolume: https://api.dataforseo.com/v3/keywords_data/google/search_volume/live
+  keywordsForSite: https://api.dataforseo.com/v3/keywords_data/google_ads/keywords_for_site/live
+  KeywordForSuggestions: https://api.dataforseo.com/v3/dataforseo_labs/google/keyword_suggestions/live
+  rankedKeywords: https://api.dataforseo.com/v3/dataforseo_labs/google/ranked_keywords/live
+  historicalTraffic: https://api.dataforseo.com/v3/dataforseo_labs/google/historical_bulk_traffic_estimation/live
+  modelName: gemini-2.5-flash

+ 1 - 0
xinkeaboard-server/b2b2c-web/src/main/resources/application.yml

@@ -44,6 +44,7 @@ secure:
       - "/v3/open/api/sso/login/**"
       - "/v3/system/seller/setting/getSiteSettingList"
       - "/openapi/**"
+      - "/analysis/**"
     #      - "/v3/seller/seller/enquiry/sendMsgStr"
     login-urls: #登录接口
       - "/v3/frontLogin/**"

+ 1 - 0
xinkeaboard-server/pom.xml

@@ -29,6 +29,7 @@
         <module>b2b2c-config-starter</module>
         <module>smartid</module>
         <module>b2b2c-captcha</module>
+        <module>b2b2c-investment</module>
     </modules>
 
 </project>