作者 [wgf]

提交demo项目

  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  3 + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  4 + <modelVersion>4.0.0</modelVersion>
  5 + <parent>
  6 + <groupId>org.springframework.boot</groupId>
  7 + <artifactId>spring-boot-starter-parent</artifactId>
  8 + <version>2.2.7.RELEASE</version>
  9 + <relativePath/> <!-- lookup parent from repository -->
  10 + </parent>
  11 + <groupId>com.aukey.example</groupId>
  12 + <artifactId>dw-example</artifactId>
  13 + <version>0.0.1</version>
  14 + <name>dw-example</name>
  15 + <description>dw客户端demo项目</description>
  16 +
  17 + <properties>
  18 + <java.version>1.8</java.version>
  19 + </properties>
  20 +
  21 + <dependencies>
  22 + <dependency>
  23 + <groupId>org.springframework.boot</groupId>
  24 + <artifactId>spring-boot-starter-web</artifactId>
  25 + </dependency>
  26 +
  27 + <!-- 在线文档,实际使用不需要依赖 !-->
  28 + <dependency>
  29 + <groupId>io.springfox</groupId>
  30 + <artifactId>springfox-swagger2</artifactId>
  31 + <version>2.7.0</version>
  32 + </dependency>
  33 +
  34 + <dependency>
  35 + <groupId>io.springfox</groupId>
  36 + <artifactId>springfox-swagger-ui</artifactId>
  37 + <version>2.7.0</version>
  38 + </dependency>
  39 + <!-- 在线文档end !-->
  40 +
  41 + <dependency>
  42 + <groupId>com.alibaba</groupId>
  43 + <artifactId>fastjson</artifactId>
  44 + <version>1.2.68</version>
  45 + </dependency>
  46 +
  47 + <dependency>
  48 + <groupId>com.squareup.okhttp3</groupId>
  49 + <artifactId>okhttp</artifactId>
  50 + <version>3.14.1</version>
  51 + </dependency>
  52 +
  53 + <dependency>
  54 + <groupId>org.apache.commons</groupId>
  55 + <artifactId>commons-lang3</artifactId>
  56 + <version>3.4</version>
  57 + </dependency>
  58 +
  59 + <dependency>
  60 + <groupId>org.springframework.boot</groupId>
  61 + <artifactId>spring-boot-starter-test</artifactId>
  62 + <scope>test</scope>
  63 + </dependency>
  64 + </dependencies>
  65 +
  66 + <build>
  67 + <plugins>
  68 + <plugin>
  69 + <groupId>org.springframework.boot</groupId>
  70 + <artifactId>spring-boot-maven-plugin</artifactId>
  71 + </plugin>
  72 + </plugins>
  73 + </build>
  74 +
  75 +</project>
  1 +### dw example 使用手册
  2 +
  3 +修改配置文件
  4 +---
  5 +修改 application.yml 的 `dwAppId`、 `dwAppSecret` 配置
  6 +
  7 +- `dwAppId`:傲基数仓 APP 模块的 AppId。
  8 +
  9 +- `dwAppSecret`:傲基数仓 APP 模块的 应用秘钥。
  10 +
  11 +提供三种数据请求方案
  12 +---
  13 +
  14 +- 全量请求:一次http请求获取所有数据,有数量限制,最大1000条。适合小批量数据同步。
  15 +
  16 +- 分页请求:多次http请求获取所有数据,分页参数`pageNumber`, `pageSize(取值范围[1,3000])`. 适合中批量数据同步。
  17 +
  18 +- 流式请求:一次http请求获取所有数据,没有数量限制,要求网络稳定,最好是内网环境。适合大批量数据同步。
  19 +
  20 +token更新
  21 +---
  22 +
  23 +`TokenJob` 为tokne更新定时任务
  24 +
  25 +`TokenController` token更新回调地址
  26 +
  27 +定时任务定点调用dw服务刷新token api,然后dw将最新token作为参数调用数仓平台上对应的APP配置
  28 +的回调地址,将token传递给客户端。
  29 +
  30 +如何使用
  31 +---
  32 +1. 申请API
  33 +
  34 +2. 启动项目
  35 +
  36 +3. 访问 http://localhost:8099/swagger-ui.html
  37 +
  38 +4. 源码 demo 在 `TestApiController`
  1 +package com.aukey.example;
  2 +
  3 +import org.springframework.boot.SpringApplication;
  4 +import org.springframework.boot.autoconfigure.SpringBootApplication;
  5 +import org.springframework.context.annotation.Bean;
  6 +import org.springframework.scheduling.annotation.EnableScheduling;
  7 +import springfox.documentation.builders.ApiInfoBuilder;
  8 +import springfox.documentation.builders.PathSelectors;
  9 +import springfox.documentation.builders.RequestHandlerSelectors;
  10 +import springfox.documentation.service.ApiInfo;
  11 +import springfox.documentation.spi.DocumentationType;
  12 +import springfox.documentation.spring.web.plugins.Docket;
  13 +import springfox.documentation.swagger2.annotations.EnableSwagger2;
  14 +
  15 +@EnableSwagger2
  16 +@EnableScheduling
  17 +@SpringBootApplication
  18 +public class DwExampleApplication {
  19 +
  20 + public static void main(String[] args) {
  21 + SpringApplication.run(DwExampleApplication.class, args);
  22 + }
  23 +
  24 +
  25 + @Bean
  26 + public Docket createRestApi() {
  27 + Docket docket = new Docket(DocumentationType.SWAGGER_2)
  28 + .apiInfo(apiInfo())
  29 + .select()
  30 + //controller所在包
  31 + .apis(RequestHandlerSelectors.basePackage("com.aukey.example.web"))
  32 + .paths(PathSelectors.any())
  33 + .build();
  34 +
  35 + return docket;
  36 + }
  37 +
  38 + //构建 api文档的详细信息函数
  39 + private ApiInfo apiInfo() {
  40 + return new ApiInfoBuilder()
  41 + .title("dw example")
  42 + .description("")
  43 + .termsOfServiceUrl("")
  44 + .version("1.0.0")
  45 + .build();
  46 + }
  47 +}
  1 +package com.aukey.example.conf;
  2 +
  3 +import com.alibaba.fastjson.JSON;
  4 +import com.alibaba.fastjson.JSONException;
  5 +import com.alibaba.fastjson.JSONReader;
  6 +import okhttp3.OkHttpClient;
  7 +import okhttp3.Request;
  8 +import okhttp3.Response;
  9 +import org.slf4j.Logger;
  10 +import org.slf4j.LoggerFactory;
  11 +
  12 +import java.io.InputStream;
  13 +import java.io.InputStreamReader;
  14 +import java.io.Reader;
  15 +import java.net.URLEncoder;
  16 +import java.util.ArrayList;
  17 +import java.util.List;
  18 +import java.util.Map;
  19 +import java.util.Objects;
  20 +import java.util.concurrent.TimeUnit;
  21 +import java.util.function.Consumer;
  22 +import java.util.function.Supplier;
  23 +
  24 +/**
  25 + * 描述:流式读取帮助类
  26 + * 创建者: wgf
  27 + * 创建时间:2020年5月20日 10:03:15
  28 + **/
  29 +public class StreamReaderHandler {
  30 +
  31 + private static Logger log = LoggerFactory.getLogger(StreamReaderHandler.class);
  32 +
  33 + private StreamReaderHandler() {
  34 + }
  35 +
  36 + /**
  37 + * @param dwDataApi dw数据请求接口
  38 + * @param api 平台申请的api
  39 + * @param paramMap 参数
  40 + * @param callback 业务回调
  41 + * @param batchSize 业务回调数据量大小,参考值 [500,5000]
  42 + * @param constructor 实体的构造函数
  43 + * @throws Exception
  44 + */
  45 + public static <T> void reader(String dwDataApi, String api,
  46 + Map<String, Object> paramMap,
  47 + Consumer<List<T>> callback,
  48 + int batchSize,
  49 + Supplier<T> constructor) throws Exception {
  50 +
  51 + dwDataApi = dwDataApi + api;
  52 + Response response = null;
  53 + InputStream is = null;
  54 + Reader reader = null;
  55 + JSONReader jsonReader = null;
  56 +
  57 + try {
  58 + response = executeHttpPostRequest(dwDataApi, paramMap);
  59 +
  60 + if (response.code() == 200) {
  61 + List<T> container = new ArrayList<>();
  62 + int total = 0;
  63 +
  64 + // 从响应中获取流
  65 + is = response.body().byteStream();
  66 + reader = new InputStreamReader(is);
  67 + jsonReader = new JSONReader(reader);
  68 + // 开始读取
  69 + jsonReader.startArray();
  70 +
  71 + while (jsonReader.hasNext()) {
  72 + try {
  73 + T t = constructor.get();
  74 + jsonReader.readObject(t);
  75 + container.add(t);
  76 + } catch (JSONException ex) {
  77 + Object o = jsonReader.readObject();
  78 + log.info("json数据转换异常:{}", o.toString());
  79 + }
  80 +
  81 + if (container.size() == batchSize) {
  82 + callback.accept(container);
  83 + total += batchSize;
  84 + log.info("{} 流式读取已读条数:{}", dwDataApi, total);
  85 + container.clear();
  86 + }
  87 + }
  88 +
  89 + // 处理最后一批不足 batchSize 的数据
  90 + if (container.size() > 0) {
  91 + callback.accept(container);
  92 + total += container.size();
  93 + log.info("{} 流式读取已读条数:{}", dwDataApi, total);
  94 + container.clear();
  95 + }
  96 +
  97 + //结束读取
  98 + jsonReader.endArray();
  99 + } else {
  100 + log.info("流式读取异常 [ url:{} ] [ code:{} ]", dwDataApi, response.code());
  101 + }
  102 +
  103 + } finally {
  104 + if (Objects.nonNull(jsonReader)) {
  105 + jsonReader.close();
  106 + }
  107 +
  108 + if (Objects.nonNull(reader)) {
  109 + reader.close();
  110 + }
  111 +
  112 + if (Objects.nonNull(is)) {
  113 + is.close();
  114 + }
  115 +
  116 + if (Objects.nonNull(response)) {
  117 + response.close();
  118 + }
  119 + }
  120 + }
  121 +
  122 + protected static Response executeHttpPostRequest(String url, Map<String, Object> paramMap) throws Exception {
  123 + OkHttpClient client = new OkHttpClient.Builder()
  124 + .connectTimeout(60 * 30 * 2, TimeUnit.SECONDS)
  125 + .readTimeout(60 * 1, TimeUnit.SECONDS)
  126 + .build();
  127 +
  128 + StringBuilder sb = new StringBuilder(url);
  129 + if (paramMap != null && !paramMap.isEmpty()) {
  130 + boolean isFirst = true;
  131 +
  132 + for (Map.Entry<String, Object> entry : paramMap.entrySet()) {
  133 +
  134 + if (Objects.isNull(entry.getValue())) {
  135 + continue;
  136 + }
  137 +
  138 + if (isFirst) {
  139 + sb.append("?");
  140 + isFirst = false;
  141 + } else {
  142 + sb.append("&");
  143 + }
  144 +
  145 + // 兼容参数特殊符号
  146 + String value = String.valueOf(entry.getValue());
  147 + value = URLEncoder.encode(value, "UTF-8");
  148 + value = value.replaceAll("\\+", "%20");
  149 +
  150 + sb.append(entry.getKey())
  151 + .append("=")
  152 + .append(value);
  153 + }
  154 + url = sb.toString();
  155 + }
  156 +
  157 + Request request = new Request.Builder().url(url).build();
  158 + Response response = client.newCall(request).execute();
  159 + return response;
  160 + }
  161 +}
  1 +package com.aukey.example.conf;
  2 +
  3 +import org.springframework.context.annotation.Bean;
  4 +import org.springframework.context.annotation.Configuration;
  5 +import org.springframework.web.context.request.async.TimeoutCallableProcessingInterceptor;
  6 +import org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer;
  7 +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
  8 +
  9 +/**
  10 + * @author: wgf
  11 + * @create: 2020-05-19 12:56
  12 + * @description:
  13 + **/
  14 +//@Configuration
  15 +public class WebMvcConfig implements WebMvcConfigurer {
  16 + @Override
  17 + public void configureAsyncSupport(final AsyncSupportConfigurer configurer) {
  18 + configurer.setDefaultTimeout(30 * 60 * 1000);
  19 + configurer.registerCallableInterceptors(timeoutInterceptor());
  20 + }
  21 +
  22 + @Bean
  23 + public TimeoutCallableProcessingInterceptor timeoutInterceptor() {
  24 + return new TimeoutCallableProcessingInterceptor();
  25 + }
  26 +}
  1 +package com.aukey.example.entity;
  2 +
  3 +import java.math.BigDecimal;
  4 +import java.util.Date;
  5 +
  6 +/**
  7 + * @author: wgf
  8 + * @create: 2020-05-13 14:38
  9 + * @description: 汇率实体
  10 + **/
  11 +public class CurrencySet {
  12 + private Integer currencyId;
  13 + private String currencyCode;
  14 + private String currencyName;
  15 + private BigDecimal currencyRate;
  16 + private String baseCurrency;
  17 + private Integer createBy;
  18 + private Date createTime;
  19 + private String updateBy;
  20 + private Date updateTime;
  21 + private String dataStatus;
  22 + private Integer auditBy;
  23 + private String auditStatus;
  24 + private Date auditTime;
  25 + private String currencySymbol;
  26 +
  27 + public Integer getCurrencyId() {
  28 + return currencyId;
  29 + }
  30 +
  31 + public void setCurrencyId(Integer currencyId) {
  32 + this.currencyId = currencyId;
  33 + }
  34 +
  35 + public String getCurrencyCode() {
  36 + return currencyCode;
  37 + }
  38 +
  39 + public void setCurrencyCode(String currencyCode) {
  40 + this.currencyCode = currencyCode;
  41 + }
  42 +
  43 + public String getCurrencyName() {
  44 + return currencyName;
  45 + }
  46 +
  47 + public void setCurrencyName(String currencyName) {
  48 + this.currencyName = currencyName;
  49 + }
  50 +
  51 + public BigDecimal getCurrencyRate() {
  52 + return currencyRate;
  53 + }
  54 +
  55 + public void setCurrencyRate(BigDecimal currencyRate) {
  56 + this.currencyRate = currencyRate;
  57 + }
  58 +
  59 + public String getBaseCurrency() {
  60 + return baseCurrency;
  61 + }
  62 +
  63 + public void setBaseCurrency(String baseCurrency) {
  64 + this.baseCurrency = baseCurrency;
  65 + }
  66 +
  67 + public Integer getCreateBy() {
  68 + return createBy;
  69 + }
  70 +
  71 + public void setCreateBy(Integer createBy) {
  72 + this.createBy = createBy;
  73 + }
  74 +
  75 + public Date getCreateTime() {
  76 + return createTime;
  77 + }
  78 +
  79 + public void setCreateTime(Date createTime) {
  80 + this.createTime = createTime;
  81 + }
  82 +
  83 + public String getUpdateBy() {
  84 + return updateBy;
  85 + }
  86 +
  87 + public void setUpdateBy(String updateBy) {
  88 + this.updateBy = updateBy;
  89 + }
  90 +
  91 + public Date getUpdateTime() {
  92 + return updateTime;
  93 + }
  94 +
  95 + public void setUpdateTime(Date updateTime) {
  96 + this.updateTime = updateTime;
  97 + }
  98 +
  99 + public String getDataStatus() {
  100 + return dataStatus;
  101 + }
  102 +
  103 + public void setDataStatus(String dataStatus) {
  104 + this.dataStatus = dataStatus;
  105 + }
  106 +
  107 + public Integer getAuditBy() {
  108 + return auditBy;
  109 + }
  110 +
  111 + public void setAuditBy(Integer auditBy) {
  112 + this.auditBy = auditBy;
  113 + }
  114 +
  115 + public String getAuditStatus() {
  116 + return auditStatus;
  117 + }
  118 +
  119 + public void setAuditStatus(String auditStatus) {
  120 + this.auditStatus = auditStatus;
  121 + }
  122 +
  123 + public Date getAuditTime() {
  124 + return auditTime;
  125 + }
  126 +
  127 + public void setAuditTime(Date auditTime) {
  128 + this.auditTime = auditTime;
  129 + }
  130 +
  131 + public String getCurrencySymbol() {
  132 + return currencySymbol;
  133 + }
  134 +
  135 + public void setCurrencySymbol(String currencySymbol) {
  136 + this.currencySymbol = currencySymbol;
  137 + }
  138 +}
  1 +package com.aukey.example.entity;
  2 +
  3 +/**
  4 + * @author: wgf
  5 + * @create: 2020-05-19 17:18
  6 + * @description:
  7 + **/
  8 +public class FbaFulfillmentCurrentInventoryView {
  9 + private String corporationName;
  10 + private String groupCode;
  11 + private String groupName;
  12 + private String deptCode;
  13 + private String deptName;
  14 + private String companySku;
  15 + private String amazonSku;
  16 + private String fnsku;
  17 +
  18 + /**
  19 + * ......
  20 + * more field
  21 + */
  22 +
  23 + public String getCorporationName() {
  24 + return corporationName;
  25 + }
  26 +
  27 + public void setCorporationName(String corporationName) {
  28 + this.corporationName = corporationName;
  29 + }
  30 +
  31 + public String getGroupCode() {
  32 + return groupCode;
  33 + }
  34 +
  35 + public void setGroupCode(String groupCode) {
  36 + this.groupCode = groupCode;
  37 + }
  38 +
  39 + public String getGroupName() {
  40 + return groupName;
  41 + }
  42 +
  43 + public void setGroupName(String groupName) {
  44 + this.groupName = groupName;
  45 + }
  46 +
  47 + public String getDeptCode() {
  48 + return deptCode;
  49 + }
  50 +
  51 + public void setDeptCode(String deptCode) {
  52 + this.deptCode = deptCode;
  53 + }
  54 +
  55 + public String getDeptName() {
  56 + return deptName;
  57 + }
  58 +
  59 + public void setDeptName(String deptName) {
  60 + this.deptName = deptName;
  61 + }
  62 +
  63 + public String getCompanySku() {
  64 + return companySku;
  65 + }
  66 +
  67 + public void setCompanySku(String companySku) {
  68 + this.companySku = companySku;
  69 + }
  70 +
  71 + public String getAmazonSku() {
  72 + return amazonSku;
  73 + }
  74 +
  75 + public void setAmazonSku(String amazonSku) {
  76 + this.amazonSku = amazonSku;
  77 + }
  78 +
  79 + public String getFnsku() {
  80 + return fnsku;
  81 + }
  82 +
  83 + public void setFnsku(String fnsku) {
  84 + this.fnsku = fnsku;
  85 + }
  86 +}
  1 +package com.aukey.example.job;
  2 +
  3 +import com.aukey.example.util.DwUtil;
  4 +import org.slf4j.Logger;
  5 +import org.slf4j.LoggerFactory;
  6 +import org.springframework.beans.factory.annotation.Value;
  7 +import org.springframework.scheduling.annotation.Scheduled;
  8 +import org.springframework.stereotype.Component;
  9 +
  10 +import java.util.HashMap;
  11 +import java.util.Map;
  12 +
  13 +/**
  14 + * @author: wgf
  15 + * @create: 2020-05-13 10:54
  16 + * @description: 获取token定时任务
  17 + **/
  18 +@Component
  19 +public class TokenJob {
  20 + private Logger log = LoggerFactory.getLogger(TokenJob.class);
  21 +
  22 + @Value("${dwTokenApi:null}")
  23 + private String dwTokenApi;/* DW获取token的接口 */
  24 +
  25 + @Value("${dwAppId:null}")/* 数据仓库AppId */
  26 + private String appId;
  27 +
  28 + @Value("${dwAppSecret:null}")/* 数据仓库应用秘钥 */
  29 + private String appSecret;
  30 +
  31 + /**
  32 + * 3分钟刷新获取一次token
  33 + */
  34 + @Scheduled(fixedDelay = 180 * 1000l)
  35 + public synchronized void fetchToken() {
  36 + if ("null".equals(this.appSecret)) {
  37 + log.info("未配置appSecret,不访问远程DW服务!");
  38 + return;
  39 + }
  40 +
  41 + if ("null".equals(this.appId)) {
  42 + log.info("未配置appId,不访问远程DW服务!");
  43 + return;
  44 + }
  45 +
  46 + Map<String, Object> params = new HashMap<>();
  47 + params.put("appSecret", this.appSecret);
  48 + DwUtil.doGet(dwTokenApi, params);
  49 + }
  50 +}
  1 +package com.aukey.example.util;
  2 +
  3 +import org.springframework.util.StringUtils;
  4 +
  5 +import java.io.BufferedReader;
  6 +import java.io.IOException;
  7 +import java.io.InputStream;
  8 +import java.io.InputStreamReader;
  9 +import java.net.HttpURLConnection;
  10 +import java.net.URL;
  11 +import java.net.URLEncoder;
  12 +import java.nio.charset.StandardCharsets;
  13 +import java.util.Map;
  14 +import java.util.Objects;
  15 +
  16 +/**
  17 + * @author: wgf
  18 + * @create: 2020-05-13 11:21
  19 + * @description:
  20 + * 关于 java url 编码问题
  21 + * Java官方的URLEncoder.encode 实际上是为了post请求的content-type为x-www-form-urlencoded来设计的
  22 + * 在进行特殊参数转义的时候会将空格转为 +
  23 + * 但是在 RFC1738、RFC2396协议中规定,GET请求的空格转为为 %20
  24 + * 所以在发送GET请求的特殊参数中存在空格必须 先URLEncoder.encode 然后再用 %20替换掉所有+
  25 + **/
  26 +public class DwUtil {
  27 + private DwUtil() {
  28 + }
  29 +
  30 + /**
  31 + * GET请求
  32 + *
  33 + * @param urlStr 请求url
  34 + * @param params 请求参数
  35 + * @return
  36 + */
  37 + public static String doGet(String urlStr, Map<String, Object> params) {
  38 +
  39 + if (StringUtils.isEmpty(urlStr)) {
  40 + return null;
  41 + }
  42 +
  43 + HttpURLConnection connection = null;
  44 + InputStream is = null;
  45 + BufferedReader br = null;
  46 + String result = null;
  47 +
  48 + try {
  49 + StringBuilder sb = new StringBuilder(urlStr);
  50 + if (params != null && !params.isEmpty()) {
  51 + boolean isFirst = true;
  52 +
  53 + for (Map.Entry<String, Object> entry : params.entrySet()) {
  54 +
  55 + if (Objects.isNull(entry.getValue())) {
  56 + continue;
  57 + }
  58 +
  59 + if (isFirst) {
  60 + sb.append("?");
  61 + isFirst = false;
  62 + } else {
  63 + sb.append("&");
  64 + }
  65 +
  66 + // 兼容参数特殊符号
  67 + String value = String.valueOf(entry.getValue());
  68 + value = URLEncoder.encode(value, "UTF-8");
  69 + value = value.replaceAll("\\+", "%20");
  70 +
  71 + sb.append(entry.getKey())
  72 + .append("=")
  73 + .append(value);
  74 + }
  75 +
  76 + urlStr = sb.toString();
  77 + }
  78 +
  79 + URL url = new URL(urlStr);
  80 + connection = (HttpURLConnection) url.openConnection();
  81 + connection.setRequestMethod("GET");
  82 + connection.setConnectTimeout(15000);
  83 + connection.setReadTimeout(60000);
  84 + connection.connect();
  85 + int responseCode = connection.getResponseCode();
  86 + if (responseCode == 200) {
  87 + is = connection.getInputStream();
  88 + br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8));
  89 + StringBuilder sbf = new StringBuilder();
  90 + String temp;
  91 + while ((temp = br.readLine()) != null) {
  92 + sbf.append(temp);
  93 + sbf.append("\r\n");
  94 + }
  95 + result = sbf.toString();
  96 + }
  97 + } catch (Exception e) {
  98 + e.printStackTrace();
  99 + } finally {
  100 + if (null != br) {
  101 + try {
  102 + br.close();
  103 + } catch (IOException e) {
  104 + e.printStackTrace();
  105 + }
  106 + }
  107 +
  108 + if (null != is) {
  109 + try {
  110 + is.close();
  111 + } catch (IOException e) {
  112 + e.printStackTrace();
  113 + }
  114 + }
  115 +
  116 + if (connection != null) {
  117 + connection.disconnect();// 关闭远程连接
  118 + }
  119 + }
  120 +
  121 + return result;
  122 + }
  123 +
  124 +
  125 + /**
  126 + * @param dwDoMain 数据仓库域名
  127 + * @param api 数据仓库申请的API
  128 + * @param params 参数
  129 + * @return
  130 + */
  131 + public static String doGet(String dwDoMain, String api, Map<String, Object> params) {
  132 + return doGet(dwDoMain + api, params);
  133 + }
  134 +}
  1 +package com.aukey.example.vo;
  2 +
  3 +import org.springframework.cglib.beans.BeanMap;
  4 +
  5 +import java.util.Map;
  6 +
  7 +/**
  8 + * @author: wgf
  9 + * @create: 2020-05-13 12:01
  10 + * @description:
  11 + **/
  12 +public class DwParamVo {
  13 + public DwParamVo() {
  14 + }
  15 +
  16 + public DwParamVo(String appId, String token) {
  17 + this.appId = appId;
  18 + this.token = token;
  19 + }
  20 +
  21 + /**
  22 + * 应用id
  23 + */
  24 + private String appId;
  25 +
  26 + /**
  27 + * token
  28 + */
  29 + private String token;
  30 +
  31 + /**
  32 + * 查询条件(可不传)
  33 + */
  34 + private String queryCondition;
  35 +
  36 + /**
  37 + * 字段选择(可不传)
  38 + */
  39 + private String multiFields;
  40 +
  41 + /**
  42 + * 偏移量(可不传 如果传递offset则必传limit)
  43 + */
  44 + private Integer offset;
  45 +
  46 + /**
  47 + * 限制条数(可不传 如果传递limit则必传offset)
  48 + */
  49 + private Integer limit;
  50 +
  51 + /**
  52 + * 是否流式读写(Y/N)
  53 + */
  54 + private String stream;
  55 +
  56 + public String getAppId() {
  57 + return appId;
  58 + }
  59 +
  60 + public void setAppId(String appId) {
  61 + this.appId = appId;
  62 + }
  63 +
  64 + public String getToken() {
  65 + return token;
  66 + }
  67 +
  68 + public void setToken(String token) {
  69 + this.token = token;
  70 + }
  71 +
  72 + public String getQueryCondition() {
  73 + return queryCondition;
  74 + }
  75 +
  76 + public void setQueryCondition(String queryCondition) {
  77 + this.queryCondition = queryCondition;
  78 + }
  79 +
  80 + public String getMultiFields() {
  81 + return multiFields;
  82 + }
  83 +
  84 + public void setMultiFields(String multiFields) {
  85 + this.multiFields = multiFields;
  86 + }
  87 +
  88 + public Integer getOffset() {
  89 + return offset;
  90 + }
  91 +
  92 + public void setOffset(Integer offset) {
  93 + this.offset = offset;
  94 + }
  95 +
  96 + public Integer getLimit() {
  97 + return limit;
  98 + }
  99 +
  100 + public void setLimit(Integer limit) {
  101 + this.limit = limit;
  102 + }
  103 +
  104 + public String getStream() {
  105 + return stream;
  106 + }
  107 +
  108 + public void setStream(String stream) {
  109 + this.stream = stream;
  110 + }
  111 +
  112 + public Map<String, Object> toMap() {
  113 + return BeanMap.create(this);
  114 + }
  115 +}
  1 +package com.aukey.example.vo;
  2 +
  3 +import java.util.List;
  4 +
  5 +/**
  6 + * @author: wgf
  7 + * @create: 2020-05-13 14:22
  8 + * @description: 数据仓库结果集返回vo
  9 + **/
  10 +public class DwResultVo<T> {
  11 +
  12 + /**
  13 + * API是否调用成功
  14 + */
  15 + private boolean success;
  16 +
  17 + /**
  18 + * API返回的异常消息
  19 + */
  20 + private String message;
  21 +
  22 + /**
  23 + * 错误编码 200为正常
  24 + */
  25 + private Integer code;
  26 +
  27 + /**
  28 + * 返回数据
  29 + */
  30 + private List<T> data;
  31 +
  32 + public boolean isSuccess() {
  33 + return success;
  34 + }
  35 +
  36 + public void setSuccess(boolean success) {
  37 + this.success = success;
  38 + }
  39 +
  40 + public String getMessage() {
  41 + return message;
  42 + }
  43 +
  44 + public void setMessage(String message) {
  45 + this.message = message;
  46 + }
  47 +
  48 + public Integer getCode() {
  49 + return code;
  50 + }
  51 +
  52 + public void setCode(Integer code) {
  53 + this.code = code;
  54 + }
  55 +
  56 + public List<T> getData() {
  57 + return data;
  58 + }
  59 +
  60 + public void setData(List<T> data) {
  61 + this.data = data;
  62 + }
  63 +}
  1 +package com.aukey.example.web;
  2 +
  3 +import com.alibaba.fastjson.JSON;
  4 +import com.alibaba.fastjson.TypeReference;
  5 +import com.aukey.example.conf.StreamReaderHandler;
  6 +import com.aukey.example.entity.CurrencySet;
  7 +import com.aukey.example.entity.FbaFulfillmentCurrentInventoryView;
  8 +import com.aukey.example.util.DwUtil;
  9 +import com.aukey.example.vo.DwParamVo;
  10 +import com.aukey.example.vo.DwResultVo;
  11 +import io.swagger.annotations.Api;
  12 +import io.swagger.annotations.ApiOperation;
  13 +import org.slf4j.Logger;
  14 +import org.slf4j.LoggerFactory;
  15 +import org.springframework.beans.factory.annotation.Value;
  16 +import org.springframework.util.CollectionUtils;
  17 +import org.springframework.util.StringUtils;
  18 +import org.springframework.web.bind.annotation.GetMapping;
  19 +import org.springframework.web.bind.annotation.RestController;
  20 +
  21 +import java.util.List;
  22 +import java.util.function.Consumer;
  23 +import java.util.function.Supplier;
  24 +
  25 +/**
  26 + * @author: wgf
  27 + * @create: 2020-05-13 16:34
  28 + * @description: API调用测试
  29 + * <p>
  30 + * ****************************************************************************************
  31 + * * *
  32 + * * 这里的TEST代码写在controller是方便使用swagger2本地调试,正常业务应该是在项目的定时任务里 *
  33 + * * *
  34 + * ****************************************************************************************
  35 + * <p>
  36 + * Demo 提供三种数据拉取方式,
  37 + * 第一种:直接调用API适合[1, 100000]数据读取
  38 + * 第二种:分页适合[1, 1000000]数据读取,数据基数过大深层分页会影响查询性能,并且客户端需要不断发起请求
  39 + * 第三种:流式读取适合[1, 10000000]数据读取,不存在深层分页性能影响,并且客户端只需要发起一次请求
  40 + **/
  41 +@RestController
  42 +@Api(tags = "API调用DEMO")
  43 +public class TestApiController {
  44 + private Logger log = LoggerFactory.getLogger(TestApiController.class);
  45 +
  46 + // API需要在傲基数仓申请授权
  47 + public static final String CURRENCY_SET_API = "/base/currency_set";
  48 + public static final String FBA_FULFILLMENT_CURRENT_INVENTORY_VIEW_API = "/stock/fba_fulfillment_current_inventory_view";
  49 +
  50 + @Value("${dwDataApi:null}")
  51 + private String dwDataApi;
  52 +
  53 + @Value("${dwAppId:null}")
  54 + private String appId;
  55 +
  56 + @ApiOperation(value = "获取前100条汇率 DEMO")
  57 + @GetMapping("/query_all")
  58 + public void queryAll() {
  59 +
  60 + // 构造请求参数
  61 + DwParamVo paramVo = new DwParamVo(this.appId, TokenController.getCurrentToken());
  62 +
  63 + // 添加查询条件
  64 + paramVo.setQueryCondition("WHERE currency_code = 'USD'");
  65 +
  66 + // 指定获取条数
  67 + paramVo.setOffset(0);
  68 + paramVo.setLimit(100);
  69 +
  70 + // 调用API
  71 + String result = DwUtil.doGet(dwDataApi, CURRENCY_SET_API, paramVo.toMap());
  72 +
  73 + if (StringUtils.isEmpty(result)) {
  74 + throw new RuntimeException(String.format("API %s 调用失败", CURRENCY_SET_API));
  75 + }
  76 + // json解析为对象
  77 + DwResultVo<CurrencySet> resultVo = JSON.parseObject(result, new TypeReference<DwResultVo<CurrencySet>>() {
  78 + });
  79 +
  80 + log.info("");
  81 + log.info("↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓");
  82 + log.info("API请求状态:{}", resultVo.getMessage());
  83 + log.info("API TOP100 数据:{}", JSON.toJSONString(resultVo.getData()));
  84 + log.info("↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑");
  85 + log.info("");
  86 + }
  87 +
  88 +
  89 + @ApiOperation(value = "分页查询 DEMO")
  90 + @GetMapping("/query_page")
  91 + public void queryPage() {
  92 +
  93 + // 构造请求参数
  94 + DwParamVo paramVo = new DwParamVo(this.appId, TokenController.getCurrentToken());
  95 +
  96 + // 页数
  97 + int pageNum = 0;
  98 + // 每页数据大小
  99 + final int pageSize = 1000;
  100 + // 当前页数据
  101 + int currentPageSize;
  102 +
  103 + // 分页请求
  104 + do {
  105 + paramVo.setToken(TokenController.getCurrentToken());
  106 + paramVo.setOffset(pageNum * pageSize);
  107 + paramVo.setLimit(pageSize);
  108 + // TODO 自定义查询条件
  109 +
  110 + String result = DwUtil.doGet(dwDataApi, CURRENCY_SET_API, paramVo.toMap());
  111 + if (StringUtils.isEmpty(result)) {
  112 + throw new RuntimeException(String.format("API %s 调用失败", CURRENCY_SET_API));
  113 + }
  114 + // json解析为对象
  115 + DwResultVo<CurrencySet> resultVo = JSON.parseObject(result, new TypeReference<DwResultVo<CurrencySet>>() {
  116 + });
  117 +
  118 + currentPageSize = CollectionUtils.isEmpty(resultVo.getData()) ? 0 : resultVo.getData().size();
  119 + pageNum++;
  120 + log.info("========== 获取API:{} 第:{}页数据 数据条数:{} ==========", CURRENCY_SET_API, pageNum + 1, currentPageSize);
  121 + // TODO 自定义实现持久化逻辑
  122 +
  123 + } while (currentPageSize == pageSize);
  124 + }
  125 +
  126 +
  127 + /**
  128 + * 流式读取适合百/千万级别的内网数据同步,本机测试
  129 + * <p>
  130 + * 网络环境:局域网
  131 + * 数据源 :华为dws
  132 + * cpu : 4核3.2GHz
  133 + * 内存 :16GB DDR3
  134 + * 测试数据:1000W条
  135 + * 回调函数不做持久化操作
  136 + * <p>
  137 + * <p>
  138 + * 测试报表
  139 + * **************************************
  140 + * *
  141 + * * QPS :1800
  142 + * * 耗时 :100W / 5分钟
  143 + * * CPU毛刺 :1% - 5% 波动
  144 + * * 内存毛刺:50Mb - 150Mb 波动
  145 + * *
  146 + * **************************************
  147 + */
  148 + @ApiOperation(value = "流式读取 DEMO")
  149 + @GetMapping("/query_stream")
  150 + public void queryStream() {
  151 + long stratTime = System.currentTimeMillis();
  152 +
  153 + try {
  154 + // 构造请求参数
  155 + DwParamVo paramVo = new DwParamVo(this.appId, TokenController.getCurrentToken());
  156 + // 设置为流式读取
  157 + paramVo.setStream("Y");
  158 + // TODO 自定义查询条件
  159 + paramVo.setOffset(3000000);
  160 +
  161 + // 定义实体构造函数
  162 + Supplier<FbaFulfillmentCurrentInventoryView> constructor = FbaFulfillmentCurrentInventoryView::new;
  163 +
  164 + // 流式读取回调函数
  165 + Consumer<List<FbaFulfillmentCurrentInventoryView>> callBack = (List<FbaFulfillmentCurrentInventoryView> list) -> {
  166 + // TODO 自定义实现持久化逻辑
  167 + // ... more
  168 + // mapper.insertList(list)
  169 + };
  170 +
  171 + // 指定回调函数数据大小,取值[500, 5000].
  172 + // 取值越大,数据读取占用的堆内存越高
  173 + int batchSize = 5000;
  174 +
  175 + // 使用 StreamAPI
  176 + StreamReaderHandler.reader(
  177 + dwDataApi,
  178 + FBA_FULFILLMENT_CURRENT_INVENTORY_VIEW_API,
  179 + paramVo.toMap(),
  180 + callBack,
  181 + batchSize,
  182 + constructor);
  183 +
  184 + } catch (Exception e) {
  185 + log.info("流式读取异常", e);
  186 + } finally {
  187 + log.info("流式读取使用时间 {} 秒", (System.currentTimeMillis() - stratTime) / 1000.0f);
  188 + }
  189 + }
  190 +}
  1 +package com.aukey.example.web;
  2 +
  3 +import org.slf4j.Logger;
  4 +import org.slf4j.LoggerFactory;
  5 +import org.springframework.util.StringUtils;
  6 +import org.springframework.web.bind.annotation.PostMapping;
  7 +import org.springframework.web.bind.annotation.RequestMapping;
  8 +import org.springframework.web.bind.annotation.RestController;
  9 +import springfox.documentation.annotations.ApiIgnore;
  10 +
  11 +import java.util.concurrent.TimeUnit;
  12 +
  13 +/**
  14 + * @author: wgf
  15 + * @create: 2020-05-13 10:11
  16 + * @description: 获取token回调controller
  17 + * 当向数据仓库平台发起 http://dw.aukeyit.com/api/authorize?appSecret=应用秘钥 请求时,
  18 + * 数据仓库验证应用秘钥后会生成token,并通过调用APP回调地址将token作为参数返回到客户端
  19 + **/
  20 +
  21 +
  22 +@RestController
  23 +@RequestMapping("/token")
  24 +@ApiIgnore
  25 +public class TokenController {
  26 + private Logger log = LoggerFactory.getLogger(TokenController.class);
  27 +
  28 + /**
  29 + * 当前token
  30 + */
  31 + private static String currentToken = "";
  32 +
  33 +
  34 + /*********************************************************************
  35 + * 这里的controller path 需要和数据仓库中APP设置的回调地址保持一致。 *
  36 + * 例如APP里回调地址为 http://localhost:8099/token/receive *
  37 + * TokenJob定时任务定时请求数据仓库获取授权API后 *
  38 + * 数据仓库回调 http://localhost:8099/token/receive,并且传递token参数 *
  39 + * 获取token后,可以放在 mysql,redis等。这里DEMO取巧放在内存中。 *
  40 + ********************************************************************/
  41 + @PostMapping("/receive")
  42 + public void receive(String token) {
  43 + log.info("获取到远程DW服务回调请求,token:{}", token);
  44 + currentToken = token;
  45 + }
  46 +
  47 + public static String getCurrentToken() {
  48 + for (int i = 0; i < 5; i++) {
  49 + if (StringUtils.isEmpty(currentToken)) {
  50 + try {
  51 + TimeUnit.SECONDS.sleep(2);
  52 + } catch (InterruptedException e) {
  53 + e.printStackTrace();
  54 + }
  55 + } else {
  56 + return currentToken;
  57 + }
  58 + }
  59 +
  60 + return null;
  61 + }
  62 +}
  1 +server:
  2 + port: 8099
  3 +
  4 +# appid
  5 +dwAppId: 709f9bd417db4117a8665bb0f1a3346b
  6 +# 应用秘钥
  7 +dwAppSecret: d219db9b6c3c43a381c4a6312a1c3724
  8 +dwDoMain: http://dw.aukeyit.com
  9 +dwTokenApi: ${dwDoMain}/api/authorize
  10 +dwDataApi: ${dwDoMain}/api/data