Compare commits

...

2 Commits

Author SHA1 Message Date
Vinjor
b1866a8cdd Merge branch 'master' of http://124.222.105.7:3000/dianliang/dl_site_system 2025-08-05 17:30:31 +08:00
Vinjor
d6a59d9f37 1 2025-08-05 17:30:20 +08:00
17 changed files with 720 additions and 131 deletions

View File

@ -0,0 +1,88 @@
package com.ruoyi.base.controller;
import java.util.*;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletResponse;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.base.vo.KeywordVO;
import com.ruoyi.busi.service.GoogleKeywordService;
import com.ruoyi.common.config.GoogleConfig;
import com.ruoyi.common.utils.StringUtils;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.base.domain.BaseSeo;
import com.ruoyi.base.service.IBaseSeoService;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.common.core.page.TableDataInfo;
import static com.ruoyi.constant.StrConstants.COUNTRY;
import static com.ruoyi.constant.StrConstants.LANGUAGE;
/**
* SEO用到的国家和语言Controller
*
* @author vinjor-m
* @date 2025-08-04
*/
@RestController
@RequestMapping("/base/seo")
public class BaseSeoController extends BaseController {
@Autowired
private GoogleConfig googleConfig;
@Autowired
private IBaseSeoService baseSeoService;
@Autowired
private GoogleKeywordService googleKeywordService;
/**
* 查询SEO用到的国家和语言列表
*/
@GetMapping("/list")
public AjaxResult list() {
LambdaQueryWrapper<BaseSeo> queryWrapper = new LambdaQueryWrapper<BaseSeo>().orderByAsc(BaseSeo::getSort);
List<BaseSeo> list = baseSeoService.list(queryWrapper);
List<BaseSeo> languageList = list.stream().filter(item -> LANGUAGE.equals(item.getDataType())).collect(Collectors.toList());
List<BaseSeo> countryList = list.stream().filter(item -> COUNTRY.equals(item.getDataType())).collect(Collectors.toList());
languageList.forEach(item->item.setTitle(item.getTitle()+"("+item.getCn()+")"));
countryList.forEach(item->item.setTitle(item.getTitle()+"("+item.getCn()+")"));
Map<String, Object> rtnMap = new HashMap<>();
rtnMap.put("language", languageList);
rtnMap.put("country", countryList);
return success(rtnMap);
}
/**
* 获取google关键词
*
* @param language 语言
* @param country 国家
* @param title 关键词
* @param url 产品预览网址
* @return com.ruoyi.common.core.domain.AjaxResult
* @author vinjor-M
* @date 17:51 2025/8/4
**/
@GetMapping("/getKeywords")
public AjaxResult getKeywords(String language, String country, String title, String url) {
Map<String,Object> rtnMap = new HashMap<>();
rtnMap.put("searchDown",googleConfig.getSearchDown());
rtnMap.put("dataList",googleKeywordService.getKeywords(language, country, title, url));
return success(rtnMap);
}
}

View File

@ -465,12 +465,6 @@ public class WebController extends BaseController {
return R.ok(); return R.ok();
} }
@GetMapping("/test")
public R<?> test() {
googleKeywordService.test();
return R.ok();
}
/** /**
* 导航栏接口--所有分类 * 导航栏接口--所有分类
* @author vinjor-M * @author vinjor-M

View File

@ -0,0 +1,59 @@
package com.ruoyi.base.domain;
import com.ruoyi.common.annotation.Excel;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import com.ruoyi.common.core.domain.DlBaseEntity;
/**
* SEO用到的国家和语言对象 dl_base_seo
*
* @author vinjor-m
* @date 2025-08-04
*/
@TableName("dl_base_seo")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class BaseSeo extends DlBaseEntity
{
private static final long serialVersionUID = 1L;
/** 主键 */
@TableId(type = IdType.ASSIGN_UUID)
private Long id;
/** 数据类型language-语言|country-国家) */
@Excel(name = "数据类型", readConverterExp = "l=anguage-语言|country-国家")
private String dataType;
/** 排序 */
@Excel(name = "排序")
private Long sort;
/** 标题 */
@Excel(name = "标题")
private String title;
/** 值 */
@Excel(name = "")
private String content;
/** 描述 */
@Excel(name = "描述")
private String description;
/** 图片 */
@Excel(name = "图片")
private String pic;
/** 中文 */
@Excel(name = "中文")
private String cn;
}

View File

@ -0,0 +1,21 @@
package com.ruoyi.base.mapper;
import java.util.List;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.base.domain.BaseSeo;
import org.apache.ibatis.annotations.Param;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* SEO用到的国家和语言Mapper接口
*
* @author vinjor-m
* @date 2025-08-04
*/
@Mapper
public interface BaseSeoMapper extends BaseMapper<BaseSeo>
{
IPage<BaseSeo> queryListPage(@Param("entity") BaseSeo entity, Page<BaseSeo> page);
}

View File

@ -0,0 +1,18 @@
package com.ruoyi.base.service;
import java.util.List;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.base.domain.BaseSeo;
/**
* SEO用到的国家和语言Service接口
*
* @author vinjor-m
* @date 2025-08-04
*/
public interface IBaseSeoService extends IService<BaseSeo>
{
IPage<BaseSeo> queryListPage(BaseSeo pageReqVO, Page<BaseSeo> page);
}

View File

@ -0,0 +1,30 @@
package com.ruoyi.base.service.impl;
import java.util.List;
import com.ruoyi.common.utils.DateUtils;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.base.mapper.BaseSeoMapper;
import com.ruoyi.base.domain.BaseSeo;
import com.ruoyi.base.service.IBaseSeoService;
/**
* SEO用到的国家和语言Service业务层处理
*
* @author vinjor-m
* @date 2025-08-04
*/
@Service
public class BaseSeoServiceImpl extends ServiceImpl<BaseSeoMapper,BaseSeo> implements IBaseSeoService
{
@Autowired
private BaseSeoMapper baseSeoMapper;
@Override
public IPage<BaseSeo> queryListPage(BaseSeo pageReqVO, Page<BaseSeo> page) {
return baseSeoMapper.queryListPage(pageReqVO, page);
}
}

View File

@ -0,0 +1,27 @@
package com.ruoyi.base.vo;
import com.ruoyi.base.domain.BasePics;
import lombok.Data;
import java.util.List;
/**
* 谷歌关键词规划师VO
* @author vinjor-M
* @date 17:53 2025/8/4
**/
@Data
public class KeywordVO {
/**
* 关键词
**/
private String title;
/**
* 月搜索量
**/
private Long searchMonth;
/**
* 竞争程度
**/
private String competition;
}

View File

@ -1,18 +1,14 @@
package com.ruoyi.busi.service; package com.ruoyi.busi.service;
import cn.hutool.core.util.StrUtil;
import com.google.ads.googleads.lib.GoogleAdsClient; import com.google.ads.googleads.lib.GoogleAdsClient;
import com.google.ads.googleads.v20.common.Keyword;
import com.google.ads.googleads.v20.common.KeywordAnnotations;
import com.google.ads.googleads.v20.common.KeywordConcept;
import com.google.ads.googleads.v20.enums.KeywordPlanKeywordAnnotationEnum;
import com.google.ads.googleads.v20.enums.KeywordPlanNetworkEnum; import com.google.ads.googleads.v20.enums.KeywordPlanNetworkEnum;
import com.google.ads.googleads.v20.enums.KeywordPlanKeywordAnnotationEnum.KeywordPlanKeywordAnnotation;
import com.google.ads.googleads.v20.services.*; import com.google.ads.googleads.v20.services.*;
import com.google.auth.oauth2.GoogleCredentials; import com.google.auth.oauth2.GoogleCredentials;
import com.google.auth.oauth2.ServiceAccountCredentials; import com.google.auth.oauth2.ServiceAccountCredentials;
import com.ruoyi.base.vo.KeywordVO;
import com.ruoyi.common.config.GoogleConfig; import com.ruoyi.common.config.GoogleConfig;
import com.ruoyi.common.config.OssConfig; import com.ruoyi.constant.CompetitionEnum;
import lombok.var;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -21,8 +17,6 @@ import org.springframework.util.ResourceUtils;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*; import java.util.*;
@Service @Service
@ -30,7 +24,19 @@ public class GoogleKeywordService {
@Autowired @Autowired
private GoogleConfig googleConfig; private GoogleConfig googleConfig;
public void test() { /**
* 获取谷歌关键词规划师规划的关键词
*
* @param language 语言编码
* @param country 国家编码多个英文逗号隔开
* @param title 关键词,多个英文逗号分隔
* @param url url
* @return java.util.List<com.ruoyi.base.vo.KeywordVO>
* @author vinjor-M
* @date 17:55 2025/8/4
**/
public List<KeywordVO> getKeywords(String language, String country, String title, String url) {
List<KeywordVO> keywordList = new ArrayList<>();
try { try {
// 1. 检查文件是否存在 // 1. 检查文件是否存在
File file = ResourceUtils.getFile("file:" + googleConfig.getJsonPath()); File file = ResourceUtils.getFile("file:" + googleConfig.getJsonPath());
@ -49,31 +55,42 @@ public class GoogleKeywordService {
.build(); .build();
// 2. 设置参数 // 2. 设置参数
List<String> seedKeywords = Arrays.asList("TRUCK"); List<String> seedKeywords = Arrays.asList(title.split(StrUtil.COMMA));
// 英语 // 语言
String language = "languageConstants/1000"; language = "languageConstants/"+language;
// 美国 // 国家/地区
String geoTarget = "geoTargetConstants/2840"; List<String> geoTargetList = new ArrayList<>();
for (String item:country.split(StrUtil.COMMA)){
geoTargetList.add("geoTargetConstants/"+item);
}
// 3. 获取关键词提示 // 3. 获取关键词提示
List<GenerateKeywordIdeaResult> keywordIdeas = generateKeywordIdeas( List<GenerateKeywordIdeaResult> keywordIdeas = generateKeywordIdeas(
googleAdsClient, googleConfig.getCustomerId(), seedKeywords, language, geoTarget); googleAdsClient, googleConfig.getCustomerId(), seedKeywords, language, geoTargetList,url);
// 4. 输出结果 // 4. 输出结果
for (GenerateKeywordIdeaResult idea : keywordIdeas) { for (GenerateKeywordIdeaResult idea : keywordIdeas) {
// 获取关键词文本 // 获取关键词文本
String text = idea.getText(); String text = idea.getText();
KeywordVO keywordVO = new KeywordVO();
keywordVO.setTitle(text);
// 获取关键词指标可选 // 获取关键词指标可选
if (idea.hasKeywordIdeaMetrics()) { if (idea.hasKeywordIdeaMetrics()) {
long avgMonthlySearches = idea.getKeywordIdeaMetrics().getAvgMonthlySearches(); long avgMonthlySearches = idea.getKeywordIdeaMetrics().getAvgMonthlySearches();
String competition = String.valueOf(idea.getKeywordIdeaMetrics().getCompetition()); String competition = String.valueOf(idea.getKeywordIdeaMetrics().getCompetition());
System.out.println("找到的关键词文本:" + text+";月搜索量:" + avgMonthlySearches+";竞争程度:" + competition); if(avgMonthlySearches>=googleConfig.getSearchDown()){
//大于要求的最低月搜索量返回
keywordVO.setSearchMonth(avgMonthlySearches);
keywordVO.setCompetition(CompetitionEnum.getTitleByCode(competition).getTitle());
keywordList.add(keywordVO);
}
} }
} }
} catch (IOException e) { } catch (IOException e) {
System.err.printf("初始化 Google Ads 客户端失败: %s%n", e.getMessage()); System.err.printf("初始化 Google Ads 客户端失败: %s%n", e.getMessage());
e.printStackTrace();
} }
// 倒序排列
keywordList.sort((o1, o2) -> Long.compare(o2.getSearchMonth(), o1.getSearchMonth()));
return keywordList;
} }
/** /**
@ -83,7 +100,8 @@ public class GoogleKeywordService {
* @param customerId 客户ID * @param customerId 客户ID
* @param seedKeywords 种子关键词列表 * @param seedKeywords 种子关键词列表
* @param language 语言常量 ( "languageConstants/1000" 表示英语) * @param language 语言常量 ( "languageConstants/1000" 表示英语)
* @param geoTarget 地理位置目标常量 ( "geoTargetConstants/2840" 表示美国) * @param geoTargets 地理位置目标常量 ( "geoTargetConstants/2840" 表示美国)
* @param url 网址
* @return 关键词提示列表 * @return 关键词提示列表
* @throws IOException 如果API调用失败 * @throws IOException 如果API调用失败
*/ */
@ -92,24 +110,36 @@ public class GoogleKeywordService {
long customerId, long customerId,
List<String> seedKeywords, List<String> seedKeywords,
String language, String language,
String geoTarget) throws IOException { List<String> geoTargets,String url) throws IOException {
List<GenerateKeywordIdeaResult> allKeywordIdeas = new ArrayList<>(); List<GenerateKeywordIdeaResult> allKeywordIdeas = new ArrayList<>();
//分页token
String nextPageToken = null; String nextPageToken = null;
//组装关键词请求体
KeywordAndUrlSeed seed = KeywordAndUrlSeed.newBuilder() KeywordAndUrlSeed urlSeed = null;
.addAllKeywords(seedKeywords) KeywordSeed seed = null;
.setUrl("https://www.cdtruck.com/truck/dump-truck/china-sinotruk-howo-19m3-6x4-cheap-336hp-10.html") // 提供相关 URL if(StringUtils.isNotEmpty(url)){
.build(); urlSeed = KeywordAndUrlSeed.newBuilder()
.addAllKeywords(seedKeywords)
.setUrl(url)
.build();
}else{
seed = KeywordSeed.newBuilder()
.addAllKeywords(seedKeywords)
.build();
}
do { do {
try (KeywordPlanIdeaServiceClient client = try (KeywordPlanIdeaServiceClient client =
googleAdsClient.getLatestVersion().createKeywordPlanIdeaServiceClient()) { googleAdsClient.getLatestVersion().createKeywordPlanIdeaServiceClient()) {
GenerateKeywordIdeasRequest.Builder requestBuilder = GenerateKeywordIdeasRequest.newBuilder() GenerateKeywordIdeasRequest.Builder requestBuilder = GenerateKeywordIdeasRequest.newBuilder()
.setCustomerId(Long.toString(customerId)) .setCustomerId(Long.toString(customerId))
.setLanguage(String.valueOf(1000)) .setLanguage(language)
.addAllGeoTargetConstants(Collections.singletonList(geoTarget)) .addAllGeoTargetConstants(geoTargets)
.setKeywordPlanNetwork(KeywordPlanNetworkEnum.KeywordPlanNetwork.GOOGLE_SEARCH_AND_PARTNERS) .setKeywordPlanNetwork(KeywordPlanNetworkEnum.KeywordPlanNetwork.GOOGLE_SEARCH_AND_PARTNERS);
.setKeywordAndUrlSeed(seed); if(StringUtils.isNotEmpty(url)){
requestBuilder.setKeywordAndUrlSeed(urlSeed);
}else{
requestBuilder.setKeywordSeed(seed);
}
// 仅在非首次请求时设置 pageToken // 仅在非首次请求时设置 pageToken
if (nextPageToken != null) { if (nextPageToken != null) {
requestBuilder.setPageToken(nextPageToken); requestBuilder.setPageToken(nextPageToken);
@ -118,10 +148,9 @@ public class GoogleKeywordService {
GenerateKeywordIdeaResponse response = client.generateKeywordIdeasCallable().call(request); GenerateKeywordIdeaResponse response = client.generateKeywordIdeasCallable().call(request);
allKeywordIdeas.addAll(response.getResultsList()); allKeywordIdeas.addAll(response.getResultsList());
nextPageToken = response.getNextPageToken(); nextPageToken = response.getNextPageToken();
System.out.println("分页token"+nextPageToken);
} }
} while (StringUtils.isNotEmpty(nextPageToken)); } while (StringUtils.isNotEmpty(nextPageToken));
System.out.println("关键词总数:"+allKeywordIdeas.size()); System.out.println("关键词总数:" + allKeywordIdeas.size());
return allKeywordIdeas; return allKeywordIdeas;
} }

View File

@ -0,0 +1,58 @@
package com.ruoyi.constant;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 竞争程度枚举值
*
* @author vinjor-m
*/
@AllArgsConstructor
@Getter
public enum CompetitionEnum {
/**
*UNSPECIFIED-未知
*/
UNSPECIFIED("UNSPECIFIED","未知"),
/**
*UNKNOWN-未知
*/
UNKNOWN("UNKNOWN","未知"),
/**
*LOW-
*/
LOW("LOW",""),
/**
*MEDIUM-
*/
MEDIUM("MEDIUM",""),
/**
*HIGH-
*/
HIGH("HIGH","");
/**
* code
*/
private String code;
/**
* 标题
*/
private String title;
/**
* 根据code返回对应的枚举
* @author vinjor-M
* @date 14:23 2024/10/16
* @param code code
**/
public static CompetitionEnum getTitleByCode(String code) {
for (CompetitionEnum thisEnum : CompetitionEnum.values()) {
if (thisEnum.getCode().equalsIgnoreCase(code)) {
// 找到对应的枚举
return thisEnum;
}
}
throw new IllegalArgumentException("无效的code" + code);
}
}

View File

@ -27,4 +27,13 @@ public class StrConstants {
* 取权重最高的前N个词 * 取权重最高的前N个词
*/ */
public static final int TOP_N = 3; public static final int TOP_N = 3;
/**
* 语言
*/
public static final String LANGUAGE = "language";
/**
* 国家
*/
public static final String COUNTRY = "country";
} }

View File

@ -153,7 +153,12 @@ aliyun:
#google ads配置 #google ads配置
google: google:
ads: ads:
customer-id: 4577179829 #经理账号id
developer-token: qwFA3dGfOGqEhSEHlCMorA customer-id: 3052342727
json-path: /www/wwwroot/nuxt/test.json #开发者令牌
developer-token: 7e_-FWCPRjWc16zr8uaUDg
#服务账号秘钥文件路径
json-path: /www/wwwroot/nuxt/chengda.json
#月搜索量下限
search-down: 500

View File

@ -1,13 +1,13 @@
{ {
"type": "service_account", "type": "service_account",
"project_id": "chengda-466307", "project_id": "chengda-467707",
"private_key_id": "c4f38952fb0922ff625a85d364543f415f93d03d", "private_key_id": "a9a538af8b2e1e3a41f8bc6265200a432450dd00",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCRIuvHsOaOnXv5\nlLl3R0fLrWjd8fk6tDv1bIJFpC2xfFem2rYaG1IFJVfnGgCAL7P4j6tVn0j684dL\n2wb4BRJjyK4EDYBLkugc3BWNnpt6z80f8+U39/QtDwRBJqQGvRD4LGhm5dDrAWhf\ngeiCKVC+3TGDGDwkt4JjuZQ6/GSrvX9aSaLyT5PlpYSvxH2tetb5rY6LdeAR9wS1\n0dtFD0jIxYHm96vWpe1LLhtw+28thxYeR7VFOZf6HzJLDDOt4hYbHoo+DAKaPIyC\n6+CepAcE13WWn7ooPGjh8JAGPqs2JpqR6ujFrvYXAto3UvpXSV0W6h1gRv9AD3SW\n33Te4KYzAgMBAAECggEAEu74zkCsGx+9TBZUSZSxb4csPoPyDO/1QHOK3RXpZhWA\nA8LVbbtxrD0uZfYU6aQPeNYZNl7EsQQy+rjUhGd4+i50URAH1BQSlq05XJO72b4h\nFtGE7hO5NWWXmv40+LISdCWq6v2BDx9MY+U5FT3ZjESj0GPJeMq9xk+v4DAL3AF2\nqsXsL/GhSLTyIWufIaxwUPFfumVVqjH3r9B1YqY+0Z//E/2I9W3NOSQ83JzI1osw\nQ5ybQdQ9zQptlr/kh9sb+vtgJ4+7h6RQ6Xcyy6OsW4Kg6Ho7eRTBDIytv9o7Ef+h\nVYwV6OjonAcWcbfCY2TGEO2DDJJbkZZwRby2WE5jkQKBgQDMQt/XfWCI2EVPNwpn\nl6gFY7vWcXSO+dkEloFgIpbxiJTmZTRmWPnpqqEaxUNo2KCxyuUoOCC7B+D6AdRI\nBZfxaFtQe+f71oocnWbjNtzLvW8cnZouHI1pT+HXWO84aq7fh4lkYhXsEZkhu2Ce\nV1nGjSVboe/OJ+Z9oCNVQxtlsQKBgQC15idlkFZbCR3zBDmc9LRDC1iZXyhBlSva\nT30e1M7sOO8lN2Uw825XaP4CKE70jOxL5eLkSvXALWOdeVZ79iclAoz+iE8FsLPf\nbUOWAEuUtsgyyrmkcKU/ddvtvEwsBFqZ0CxxqOCDXj+uXoGifyFLigkl3w5wvY0s\njpe1wipvIwKBgQCUvvpc5XugC8ZlSlK0X5dG3XsTTamw2Lc2BRgP1wCOwYSVRwvi\noFbV16DcatyNBHv5HSTFpiIHsVQfG6foDtK4ROOCd8jW90O6VNFxEym04K2CbC6z\n96zvDPIMrUH/lojkVMIzrM4EDEi0bMyOYlQJKA4VbZbBTQMnZq90Tpsr8QKBgQCr\nd/k9aZGuIWsFEb+JsLdY2BI+ChC1ufvrwLDO5obk8UqmR5DxUxh597QyrnK3XzzE\n00FOOUduUJst8BrRohoGbmAg9LehQpBdFu/2L/MPjjosfyP+2l079EtM0QrxF22c\nvzuWLT7vN2JKajZDyxnEzquO8rT3HAg/r29d3FoKBwKBgQCM4BNOTBT10gCPKQBx\nApULMInlweLr393rc17cJHSSLmL0z26oIfpJYVf+AbV4sLKE9HaCmwxTiR/frt9f\nEhAaOsWfprl/CFC3kcF/Vndey2v9Nlwdzl7//8c0O8ITlqt+OpK3giOUquMWuG8i\nFU9AwpseRhEp5uHZ5e1wKgsEsA==\n-----END PRIVATE KEY-----\n", "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCaoBGTyINWYh5o\nFSmaGJ33q8wRIevL+GC6oqywzdyvk7in5MKpkGtKnx4yy8Cy0TSHp5yAsoXtblWV\n1TrrV7hia8IizB6QxkZQbcwwPz12shH7HN65m4lOdhsEjsSPZogYIwvDDtkXsAyl\nB+ALfYb+DZpX2aTST+1fvtTc+3PdrfxXBmBqmwJeTaQOVBk10BW+vDL9Y8TxuvHS\nYWbix/PJDQEpJFcQI/1shgiyeNkvBQJAxUHgb58EncukAD3F1W7wLLEJtE+5cnGU\nCH1KNHt+ymmZ/TJ1hdf/BvSjtHTgTYSM/ViEQZ7aTj1RZ1xueflSt2Pzqq84GMP3\nOJaCgcObAgMBAAECggEAC6v3h7NEDro7V1n5mPMwwWhLcCOiW/Q0wQHUjUgM14gM\nEUhYVVpcbvaR3f25XwYhs/e3/ZL8Pw7sEPJWjztA5NUDg4Lq77y3xAx1tFKhvhr0\n9oKYYWRXOf/6TB7GwZhYv5GtqpytUHdujOqKQ/PGw9idBPKd2W5FGcwa309v0JTa\ndhiaUAxi19QJNwqxDHn665EBfAHqZE76h0TR2fXiQ8XfH0T7EQuTuSI4sAD1MYaS\nU/0YnEi9eulCVJNpD5CDVFjL1C5Oa/LkMTo/SubaAd7pTY9Uo3LOrykikRBozrMc\nisObanEZDyi+y435ZMfH4ikywJq7cN4Xnlis06nBcQKBgQDTv0JaX0ze8v6fiwaZ\nrbHvEehAyRvGGaY0/mxDYM9Ioq80aIAG5/Ogh3WGDyrynz4zCu1mqjRqterzpY+H\nSP6fZzhTi7lUHGVa4QUb4QlSkDWV0BED8we4AzXnBOHmto+aMwm1bi0O3qWcbWDk\nOacLIoAOhNKOn4IAzaHWAkgH6QKBgQC68LfXb9EEGafeHIjz2o08EhqHSoMs1kAp\nvu39vh3qdMIlSXWrmGXvQS4EJyBMyq24DLufUbEMRaf2yvbCpl0NRzo3dUeXXQWh\nLBT/9erKF10IjEHd8t4PUoE4JbtuLUPUAKBCx5pbtCcAMi1wWm+kcx9uLdKw/sPC\nKBKuRJTA4wKBgA4yYpaAkTzZeezke1rOZIGs56+ATFZp3dTrwgJ6eStbjPeskfjk\nkFcQ/fYxCiYeOyNc9jHN85m7/La9QPRHFptFZmdjlNdo1/rR32/RFLjuZklXG+zx\n3HCf9ns4vpSjZNln1pzNeEPo9Z2ZT8e9fc+nToKqsRtS4np/Tvx0RrQRAoGAB/Pd\nnkifd7Evtv83xEfz4i5S+pZs5idnK193+GdyFltJNxTc8KPXEkqPjpWrhhjJMx83\njBuJkKAV+SXBQZ6aLsps65cNqJ7IcobRjeIETvyXdJmi47JFSBmWbbENPC6oyAgO\nyUh8hJIxZoy76h+uyCwtlh5U7DqqUUQrJ0a7nUkCgYBoRunRKHNRycFlMFW2rLf+\n+ayC7fIPvLiKzYg2xAHIG/ykTGmtxrqKiVQ909FyVN7k9Rbyg9ZVZdkCqG/Br7iS\nVOF7xD1SCma3pdupRP4GIQSxVZVvUo48xzXNUFWXg4bjzMEgIErG12zL286blVPi\nxMz4TKijJq2YR67kdAEzDw==\n-----END PRIVATE KEY-----\n",
"client_email": "chengdatest@chengda-466307.iam.gserviceaccount.com", "client_email": "shandongchengda@chengda-467707.iam.gserviceaccount.com",
"client_id": "108513526311014104962", "client_id": "107299671168418329002",
"auth_uri": "https://accounts.google.com/o/oauth2/auth", "auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token", "token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/chengdatest%40chengda-466307.iam.gserviceaccount.com", "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/shandongchengda%40chengda-467707.iam.gserviceaccount.com",
"universe_domain": "googleapis.com" "universe_domain": "googleapis.com"
} }

View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.base.mapper.BaseSeoMapper">
<resultMap type="BaseSeo" id="BaseSeoResult">
<result property="id" column="id" />
<result property="dataType" column="data_type" />
<result property="sort" column="sort" />
<result property="title" column="title" />
<result property="content" column="content" />
<result property="description" column="description" />
<result property="pic" column="pic" />
<result property="cn" column="cn" />
<result property="creator" column="creator" />
<result property="createTime" column="create_time" />
<result property="updater" column="updater" />
<result property="updateTime" column="update_time" />
<result property="delFlag" column="del_flag" />
</resultMap>
<sql id="selectBaseSeoVo">
select id, data_type, sort, title, content, description, pic, cn, creator, create_time, updater, update_time, del_flag from dl_base_seo
</sql>
<select id="queryListPage" parameterType="BaseSeo" resultMap="BaseSeoResult">
<include refid="selectBaseSeoVo"/>
<where>
<if test="entity.dataType != null and entity.dataType != ''"> and data_type = #{entity.dataType}</if>
<if test="entity.sort != null "> and sort = #{entity.sort}</if>
<if test="entity.title != null and entity.title != ''"> and title = #{entity.title}</if>
<if test="entity.content != null and entity.content != ''"> and content = #{entity.content}</if>
<if test="entity.description != null and entity.description != ''"> and description = #{entity.description}</if>
<if test="entity.pic != null and entity.pic != ''"> and pic = #{entity.pic}</if>
<if test="entity.cn != null and entity.cn != ''"> and cn = #{entity.cn}</if>
</where>
</select>
</mapper>

View File

@ -15,6 +15,7 @@ public class GoogleConfig {
private Long customerId; private Long customerId;
private String developerToken; private String developerToken;
private String jsonPath; private String jsonPath;
private Long searchDown;
} }

View File

@ -5,7 +5,7 @@ VUE_APP_TITLE = 成事达管理平台
ENV = 'development' ENV = 'development'
# 成事达管理平台/开发环境 # 成事达管理平台/开发环境
VUE_APP_BASE_API = '/dev-api' VUE_APP_BASE_API = 'http://127.0.0.1:8099'
# 路由懒加载 # 路由懒加载
VUE_CLI_BABEL_TRANSPILE_MODULES = true VUE_CLI_BABEL_TRANSPILE_MODULES = true

View File

@ -0,0 +1,19 @@
import request from '@/utils/request'
// 查询SEO用到的国家和语言列表
export function listSeo(query) {
return request({
url: '/base/seo/list',
method: 'get',
params: query
})
}
// 查询关键词规划
export function getKeywords(query) {
return request({
url: '/base/seo/getKeywords',
method: 'get',
params: query
})
}

View File

@ -6,99 +6,182 @@
<el-button type="success" @click="saveTmp"> </el-button> <el-button type="success" @click="saveTmp"> </el-button>
<el-button type="primary" @click="submitForm"> </el-button> <el-button type="primary" @click="submitForm"> </el-button>
<el-button type="warning" @click="checkContent">相似度检测</el-button> <el-button type="warning" @click="checkContent">相似度检测</el-button>
<el-button @click="toggleSideBar">{{ showKeywords ? '关闭' : '打开' }}关键词推荐</el-button>
</el-col> </el-col>
</el-row> </el-row>
<el-divider></el-divider> <el-divider></el-divider>
<el-form ref="form" :model="form" :rules="rules" label-width="150px"> <!-- 表单区域-->
<el-row> <el-row>
<el-col :span="8"> <el-col v-if="showKeywords" :span="8">
<el-form-item label="产品名称" prop="title"> <el-form :model="formSeo" ref="queryForm" :rules="rulesSeo" size="small" :inline="true" label-width="110px">
<div class="dl-flex-column"> <el-form-item label="基准词" prop="title">
<el-input v-model="form.title" placeholder="请输入产品名称"/>
<div class="dl-add-catg" style="width: 180px" @click="goCatgView">Google关键词获取</div>
</div>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="所属分类" prop="catgId">
<div class="dl-flex-column">
<treeselect style="width: 200px" v-model="form.catgId" :options="catgOptions" :normalizer="normalizer"
:noResultsText="'暂无数据'" placeholder="请选择产品分类"
/>
<div class="dl-add-catg" @click="goCatgView">添加产品分类</div>
</div>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="排序" prop="sort">
<el-input v-model="form.sort" type="number" placeholder="请输入排序"/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="8">
<el-form-item label="页面title" prop="prodTitle">
<el-input v-model="form.prodTitle" type="textarea" placeholder="请输入页面title"/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="页面keyword" prop="prodKeyword">
<el-input v-model="form.prodKeyword" type="textarea" placeholder="请输入页面keyword"/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="页面description" prop="prodDescription">
<el-input v-model="form.prodDescription" type="textarea" placeholder="请输入页面description"/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="8">
<el-form-item label="产品主图" prop="mainPic">
<el-tag v-if="!form.mainPic" style="cursor: pointer" @click="choosePic('mainPic',1)">图片库选择</el-tag>
<image-upload @uploadedImg="uploadedImg" v-model="form.mainPic" :limit="1"/>
</el-form-item>
</el-col>
<el-col :span="16">
<el-form-item label="产品图库" prop="pics">
<el-tag v-if="canPicsNum>0" style="cursor: pointer" @click="choosePic('pics',canPicsNum)">图片库选择</el-tag>
<image-upload @uploadedImg="uploadedImg" @input="listenerPicNum" v-model="form.pics" :limit="picsNum"/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="18">
<el-form-item label="产品简介" prop="description">
<div class="dl-flex-column">
<el-input style="width: 80%" ref="descriptionInput" v-model="form.description" type="textarea"
placeholder="请输入内容" @blur="handleBlur"
/>
<el-tag style="cursor: pointer;margin-left: 10px" @click="insertBr">插入换行符</el-tag>
</div>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="首页显示" prop="ifReco">
<template v-slot:label> <template v-slot:label>
<span>首页显示</span> <span>基准词</span>
<el-tooltip class="item" effect="dark" content="开启后将在首页优先显示" <el-tooltip class="item" effect="dark" content="请输入您的产品名称,尽量不要过于具体或泛化" placement="bottom">
placement="bottom" <i class="el-icon-question"></i>
</el-tooltip>
</template>
<div class="dl-flex-column">
<el-input style="width: 150px" v-model="formSeo.title" placeholder="请输入基准词"/>
<div class="dl-add-catg" @click="useProdName">使用产品名称</div>
</div>
</el-form-item>
<el-form-item label="语言" prop="language">
<el-select style="width: 150px" v-model="formSeo.language" filterable placeholder="请选择">
<el-option
v-for="item in languageArray"
:key="item.content"
:label="item.title"
:value="item.content"
> >
</el-option>
</el-select>
</el-form-item>
<el-form-item label="结合产品内容" prop="needUrl">
<template v-slot:label>
<span>结合产品内容</span>
<el-tooltip class="item" effect="dark" content="请确保已经录入产品内容并保存!" placement="bottom">
<i class="el-icon-question"></i> <i class="el-icon-question"></i>
</el-tooltip> </el-tooltip>
</template> </template>
<el-switch <el-switch
v-model="form.ifReco" v-model="formSeo.needUrl"
> >
</el-switch> </el-switch>
</el-form-item> </el-form-item>
</el-col> <el-form-item label="国家/地区" prop="country">
</el-row> <el-select v-model="formSeo.country" multiple filterable placeholder="请选择">
<el-form-item label="产品内容"> <el-option
<editor v-model="form.content" :min-height="192"/> v-for="item in countryArray"
</el-form-item> :key="item.content"
</el-form> :label="item.title"
:value="item.content"
>
</el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" :loading="loading" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-tag type="warning" v-if="keywordsList.length>0">共抓取到{{keywordsList.length}}个关键词月搜索量低于{{searchDown}}的关键词已为您自动忽略</el-tag>
<el-table v-loading="loading" :data="keywordsList" max-height="500">
<el-table-column type="index" width="60" label="序号" align="center"></el-table-column>
<el-table-column label="关键词" align="center" prop="title"/>
<el-table-column label="月搜索量" align="center" prop="searchMonth" width="100">
<template slot-scope="scope">
<countTo :startVal="0" :endVal="scope.row.searchMonth" :duration="1000"/>
</template>
</el-table-column>
<el-table-column label="竞争程度" align="center" prop="competition" width="80"/>
<el-table-column width="60" label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-dropdown size="mini" @command="(command) => handleCommand(command, scope.row)">
<el-button size="mini" type="text">使用</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="handleTitle">添加至title</el-dropdown-item>
<el-dropdown-item command="handleKeyword">添加至keyword</el-dropdown-item>
<el-dropdown-item command="handleDescription">添加至description</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
</el-table-column>
</el-table>
</el-col>
<el-col :span="showKeywords?16:24">
<el-form ref="form" :model="form" :rules="rules" label-width="150px">
<el-row>
<el-col :span="8">
<el-form-item label="产品名称" prop="title">
<div class="dl-flex-column">
<el-input v-model="form.title" placeholder="请输入产品名称"/>
<div class="dl-add-catg" style="width: 180px" @click="goCatgView">Google关键词获取</div>
</div>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="所属分类" prop="catgId">
<div class="dl-flex-column">
<treeselect style="width: 200px" v-model="form.catgId" :options="catgOptions" :normalizer="normalizer"
:noResultsText="'暂无数据'" placeholder="请选择产品分类"
/>
<div class="dl-add-catg" @click="goCatgView">添加产品分类</div>
</div>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="排序" prop="sort">
<el-input v-model="form.sort" type="number" placeholder="请输入排序"/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="8">
<el-form-item label="页面title" prop="prodTitle">
<el-input v-model="form.prodTitle" type="textarea" placeholder="请输入页面title"/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="页面keyword" prop="prodKeyword">
<el-input v-model="form.prodKeyword" type="textarea" placeholder="请输入页面keyword"/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="页面description" prop="prodDescription">
<el-input v-model="form.prodDescription" type="textarea" placeholder="请输入页面description"/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="8">
<el-form-item label="产品主图" prop="mainPic">
<el-tag v-if="!form.mainPic" style="cursor: pointer" @click="choosePic('mainPic',1)">图片库选择</el-tag>
<image-upload @uploadedImg="uploadedImg" v-model="form.mainPic" :limit="1"/>
</el-form-item>
</el-col>
<el-col :span="16">
<el-form-item label="产品图库" prop="pics">
<el-tag v-if="canPicsNum>0" style="cursor: pointer" @click="choosePic('pics',canPicsNum)">图片库选择</el-tag>
<image-upload @uploadedImg="uploadedImg" @input="listenerPicNum" v-model="form.pics" :limit="picsNum"/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="18">
<el-form-item label="产品简介" prop="description">
<div class="dl-flex-column">
<el-input style="width: 80%" ref="descriptionInput" v-model="form.description" type="textarea"
placeholder="请输入内容" @blur="handleBlur"
/>
<el-tag style="cursor: pointer;margin-left: 10px" @click="insertBr">插入换行符</el-tag>
</div>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="首页显示" prop="ifReco">
<template v-slot:label>
<span>首页显示</span>
<el-tooltip class="item" effect="dark" content="开启后将在首页优先显示"
placement="bottom"
>
<i class="el-icon-question"></i>
</el-tooltip>
</template>
<el-switch
v-model="form.ifReco"
>
</el-switch>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="产品内容">
<editor v-model="form.content" :min-height="192"/>
</el-form-item>
</el-form>
</el-col>
</el-row>
<el-divider></el-divider> <el-divider></el-divider>
<el-row> <el-row>
<el-col :span="24"> <el-col :span="24">
@ -115,7 +198,10 @@
</template> </template>
<script> <script>
import countTo from 'vue-count-to';
import { listCategory } from '@/api/busi/category' import { listCategory } from '@/api/busi/category'
import { listSeo ,getKeywords} from '@/api/base/seo'
import Hamburger from '@/components/Hamburger'
import { import {
listProdNew, listProdNew,
getProdNew, getProdNew,
@ -133,7 +219,7 @@ import SimilarityProduct from './similarityProduct'
export default { export default {
name: 'prodForm', name: 'prodForm',
components: { SimilarityProduct, Treeselect, selectPic }, components: { SimilarityProduct, Treeselect, selectPic, Hamburger , countTo},
data() { data() {
return { return {
//-- //--
@ -171,6 +257,22 @@ export default {
// //
fileList: [] fileList: []
}, },
formSeo: {
title: '',
needUrl: true,
language: '',
country:"",
},
languageArray: [],
countryArray: [],
//
showKeywords: true,
//
loading: false,
//
keywordsList: [],
//
searchDown:null,
// //
similarityList: [], similarityList: [],
// //
@ -190,11 +292,23 @@ export default {
description: [ description: [
{ required: true, message: '请输入产品简介', trigger: 'blur' } { required: true, message: '请输入产品简介', trigger: 'blur' }
] ]
},
rulesSeo:{
title: [
{ required: true, message: '请输入基准词', trigger: 'blur' }
],
language: [
{ required: true, message: '请选择语言', trigger: 'blur' }
],
country: [
{ required: true, message: '请选择国家', trigger: 'blur' }
],
} }
} }
}, },
mounted() { mounted() {
this.initCatg() this.initCatg()
this.initSeo()
if (this.$route.query.id) { if (this.$route.query.id) {
if (this.$route.query.type) { if (this.$route.query.type) {
// //
@ -208,6 +322,64 @@ export default {
} }
}, },
methods: { methods: {
useProdName(){
this.formSeo.title = this.form.title
},
/** 搜索按钮操作 */
handleQuery() {
this.$refs['queryForm'].validate(valid => {
if (valid) {
this.loading=true
let query = JSON.parse(JSON.stringify(this.formSeo))
let countryStr = query.country.join()
query.country = countryStr
if(query.needUrl){
query.url="https://www.cdtruck.com/truck/dump-truck/china-sinotruk-howo-19m3-6x4-cheap-336hp-10.html"
}
getKeywords(query).then(response => {
this.keywordsList = response.data.dataList
this.searchDown = response.data.searchDown
this.loading=false
})
}
})
},
/** 重置按钮操作 */
resetQuery() {
this.resetForm("queryForm");
this.handleQuery();
},
toggleSideBar() {
this.showKeywords = !this.showKeywords
},
//
handleCommand(command, row) {
switch (command) {
case 'handleTitle':
if(this.form.prodTitle){
this.form.prodTitle+=","+row.title
}else{
this.form.prodTitle=row.title
}
break
case 'handleKeyword':
if(this.form.prodKeyword){
this.form.prodKeyword+=","+row.title
}else{
this.form.prodKeyword=row.title
}
break
case 'handleDescription':
if(this.form.prodDescription){
this.form.prodDescription+=","+row.title
}else{
this.form.prodDescription=row.title
}
break
default:
break
}
},
goCatgView() { goCatgView() {
this.$router.push({ path: '/busi/category' }) this.$router.push({ path: '/busi/category' })
}, },
@ -220,6 +392,12 @@ export default {
this.catgOptions = response.data this.catgOptions = response.data
}) })
}, },
initSeo(){
listSeo({ }).then(response => {
this.languageArray = response.data.language
this.countryArray = response.data.country
})
},
/** 转换部门数据结构 */ /** 转换部门数据结构 */
normalizer(node) { normalizer(node) {
if (node.children && !node.children.length) { if (node.children && !node.children.length) {
@ -363,10 +541,24 @@ export default {
width: 110px; width: 110px;
text-align: center; text-align: center;
color: #1890ff; color: #1890ff;
line-height: normal;
} }
.dl-tooltip-text { .dl-tooltip-text {
color: #495060; color: #495060;
font-size: 12px; font-size: 12px;
} }
.hamburger-container {
line-height: 46px;
height: 100%;
float: left;
cursor: pointer;
transition: background .3s;
-webkit-tap-highlight-color: transparent;
}
.hamburger-container:hover {
background: rgba(0, 0, 0, .025)
}
</style> </style>