Prechádzať zdrojové kódy

#565 小程序增加数据分析相关 API

* 微信开放平台:1. WxOpenInRedisConfigStorage 支持 JedisPool/JedisSentinelPool 等 Pool<Jedis> 的子类;2. WxOpenInRedisConfigStorage 增加 keyPrefix 以支持可配置的前缀;

* 微信开放平台:增加小程序代码模板库管理

* 小程序:增加代码管理相关 API

* 小程序:增加修改服务器地址、成员管理 API

* 小程序:增加数据分析相关 API
Charming 7 rokov pred
rodič
commit
3c2249eae8
19 zmenil súbory, kde vykonal 1048 pridanie a 0 odobranie
  1. 145 0
      weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/WxMaAnalysisService.java
  2. 7 0
      weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/WxMaService.java
  3. 126 0
      weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/WxMaAnalysisServiceImpl.java
  4. 7 0
      weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/WxMaServiceImpl.java
  5. 43 0
      weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/analysis/WxMaRetainInfo.java
  6. 37 0
      weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/analysis/WxMaSummaryTrend.java
  7. 68 0
      weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/analysis/WxMaUserPortrait.java
  8. 83 0
      weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/analysis/WxMaVisitDistribution.java
  9. 55 0
      weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/analysis/WxMaVisitPage.java
  10. 59 0
      weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/analysis/WxMaVisitTrend.java
  11. 7 0
      weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/analysis/package-info.java
  12. 6 0
      weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/util/json/WxMaGsonBuilder.java
  13. 52 0
      weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/util/json/WxMaRetainInfoGsonAdapter.java
  14. 67 0
      weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/util/json/WxMaUserPortraitGsonAdapter.java
  15. 67 0
      weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/util/json/WxMaVisitDistributionGsonAdapter.java
  16. 155 0
      weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/api/impl/WxMaAnalysisServiceImplTest.java
  17. 22 0
      weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/bean/analysis/WxMaRetainInfoTest.java
  18. 19 0
      weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/bean/analysis/WxMaUserPortraitTest.java
  19. 23 0
      weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/bean/analysis/WxMaVisitDistributionTest.java

+ 145 - 0
weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/WxMaAnalysisService.java

@@ -0,0 +1,145 @@
+package cn.binarywang.wx.miniapp.api;
+
+import cn.binarywang.wx.miniapp.bean.analysis.WxMaRetainInfo;
+import cn.binarywang.wx.miniapp.bean.analysis.WxMaSummaryTrend;
+import cn.binarywang.wx.miniapp.bean.analysis.WxMaUserPortrait;
+import cn.binarywang.wx.miniapp.bean.analysis.WxMaVisitDistribution;
+import cn.binarywang.wx.miniapp.bean.analysis.WxMaVisitPage;
+import cn.binarywang.wx.miniapp.bean.analysis.WxMaVisitTrend;
+import me.chanjar.weixin.common.exception.WxErrorException;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 小程序数据分析相关接口
+ * 文档:https://mp.weixin.qq.com/debug/wxadoc/dev/api/analysis.html
+ *
+ * @author <a href="https://github.com/charmingoh">Charming</a>
+ * @since 2018-04-28
+ */
+public interface WxMaAnalysisService {
+  String GET_DAILY_SUMMARY_TREND_URL = "https://api.weixin.qq.com/datacube/getweanalysisappiddailysummarytrend";
+  String GET_DAILY_VISIT_TREND_URL = "https://api.weixin.qq.com/datacube/getweanalysisappiddailyvisittrend";
+  String GET_WEEKLY_VISIT_TREND_URL = "https://api.weixin.qq.com/datacube/getweanalysisappidweeklyvisittrend";
+  String GET_MONTHLY_VISIT_TREND_URL = "https://api.weixin.qq.com/datacube/getweanalysisappidmonthlyvisittrend";
+  String GET_VISIT_DISTRIBUTION_URL = "https://api.weixin.qq.com/datacube/getweanalysisappidvisitdistribution";
+  String GET_DAILY_RETAIN_INFO_URL = "https://api.weixin.qq.com/datacube/getweanalysisappiddailyretaininfo";
+  String GET_WEEKLY_RETAIN_INFO_URL = "https://api.weixin.qq.com/datacube/getweanalysisappidweeklyretaininfo";
+  String GET_MONTHLY_RETAIN_INFO_URL = "https://api.weixin.qq.com/datacube/getweanalysisappidmonthlyretaininfo";
+  String GET_VISIT_PAGE_URL = "https://api.weixin.qq.com/datacube/getweanalysisappidvisitpage";
+  String GET_USER_PORTRAIT_URL = "https://api.weixin.qq.com/datacube/getweanalysisappiduserportrait";
+
+  /**
+   * 查询概况趋势
+   * 温馨提示:小程序接口目前只能查询一天的数据,即 beginDate 和 endDate 一样
+   *
+   * @param beginDate 开始日期
+   * @param endDate   结束日期,限定查询1天数据,end_date允许设置的最大值为昨日
+   * @return 概况趋势
+   * @throws WxErrorException 获取失败时抛出,具体错误码请看文档
+   */
+  List<WxMaSummaryTrend> getDailySummaryTrend(Date beginDate, Date endDate) throws WxErrorException;
+
+  /**
+   * 获取日访问趋势
+   * 温馨提示:小程序接口目前只能查询一天的数据,即 beginDate 和 endDate 一样
+   *
+   * @param beginDate 开始日期
+   * @param endDate   结束日期,限定查询1天数据,end_date允许设置的最大值为昨日
+   * @return 日访问趋势
+   * @throws WxErrorException 获取失败时抛出,具体错误码请看文档
+   */
+  List<WxMaVisitTrend> getDailyVisitTrend(Date beginDate, Date endDate) throws WxErrorException;
+
+  /**
+   * 获取周访问趋势
+   * 限定查询一个自然周的数据,时间必须按照自然周的方式输入: 如:20170306(周一), 20170312(周日)
+   *
+   * @param beginDate 开始日期,为周一日期
+   * @param endDate   结束日期,为周日日期,限定查询一周数据
+   * @return 周访问趋势(每项数据都是一个自然周汇总)
+   * @throws WxErrorException 获取失败时抛出,具体错误码请看文档
+   */
+  List<WxMaVisitTrend> getWeeklyVisitTrend(Date beginDate, Date endDate) throws WxErrorException;
+
+  /**
+   * 获取月访问趋势
+   * 限定查询一个自然月的数据,时间必须按照自然月的方式输入: 如:20170201(月初), 20170228(月末)
+   *
+   * @param beginDate 开始日期,为自然月第一天
+   * @param endDate   结束日期,为自然月最后一天,限定查询一个月数据
+   * @return 月访问趋势(每项数据都是一个自然月汇总)
+   * @throws WxErrorException 获取失败时抛出,具体错误码请看文档
+   */
+  List<WxMaVisitTrend> getMonthlyVisitTrend(Date beginDate, Date endDate) throws WxErrorException;
+
+  /**
+   * 获取访问分布
+   * (此接口目前只能查询一天的数据,即 beginDate 和 endDate 一样)
+   *
+   * @param beginDate 开始日期,为周一日期
+   * @param endDate   结束日期,限定查询1天数据,end_date允许设置的最大值为昨日
+   * @return 访问分布
+   * @throws WxErrorException 获取失败时抛出,具体错误码请看文档
+   */
+  WxMaVisitDistribution getVisitDistribution(Date beginDate, Date endDate) throws WxErrorException;
+
+  /**
+   * 日留存
+   * (此接口目前只能查询一天的数据,即 beginDate 和 endDate 一样)
+   *
+   * @param beginDate 开始日期,为周一日期
+   * @param endDate   结束日期,限定查询 1 天数据,endDate 允许设置的最大值为昨日
+   * @return 日留存
+   * @throws WxErrorException 获取失败时抛出,具体错误码请看文档
+   */
+  WxMaRetainInfo getDailyRetainInfo(Date beginDate, Date endDate) throws WxErrorException;
+
+  /**
+   * 周留存
+   * 限定查询一个自然周的数据,时间必须按照自然周的方式输入: 如:20170306(周一), 20170312(周日)
+   *
+   * @param beginDate 开始日期,为周一日期
+   * @param endDate   结束日期,为周日日期,限定查询一周数据
+   * @return 周留存
+   * @throws WxErrorException 获取失败时抛出,具体错误码请看文档
+   */
+  WxMaRetainInfo getWeeklyRetainInfo(Date beginDate, Date endDate) throws WxErrorException;
+
+  /**
+   * 月留存
+   * 限定查询一个自然月的数据,时间必须按照自然月的方式输入: 如:20170201(月初), 20170228(月末)
+   *
+   * @param beginDate 开始日期,为自然月第一天
+   * @param endDate   结束日期,为自然月最后一天,限定查询一个月数据
+   * @return 月留存
+   * @throws WxErrorException 获取失败时抛出,具体错误码请看文档
+   */
+  WxMaRetainInfo getMonthlyRetainInfo(Date beginDate, Date endDate) throws WxErrorException;
+
+  /**
+   * 获取访问页面数据
+   * 温馨提示:此接口目前只能查询一天的数据,即 beginDate 和 endDate 一样
+   *
+   * @param beginDate 开始日期
+   * @param endDate   结束日期,限定查询1天数据,end_date允许设置的最大值为昨日
+   * @return 访问页面数据
+   * @throws WxErrorException 获取失败时抛出,具体错误码请看文档
+   */
+  List<WxMaVisitPage> getVisitPage(Date beginDate, Date endDate) throws WxErrorException;
+
+  /**
+   * 获取小程序新增或活跃用户的画像分布数据
+   * 时间范围支持昨天、最近7天、最近30天。
+   * 其中,新增用户数为时间范围内首次访问小程序的去重用户数,
+   * 活跃用户数为时间范围内访问过小程序的去重用户数。
+   * 画像属性包括用户年龄、性别、省份、城市、终端类型、机型。
+   *
+   * @param beginDate 开始日期
+   * @param endDate   结束日期,开始日期与结束日期相差的天数限定为0/6/29,分别表示查询最近1/7/30天数据,end_date允许设置的最大值为昨日
+   * @return 小程序新增或活跃用户的画像分布数据
+   * @throws WxErrorException 获取失败时抛出,具体错误码请看文档
+   */
+  WxMaUserPortrait getUserPortrait(Date beginDate, Date endDate) throws WxErrorException;
+}

+ 7 - 0
weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/WxMaService.java

@@ -136,6 +136,13 @@ public interface WxMaService {
   WxMaTemplateService getTemplateService();
 
   /**
+   * 数据分析相关查询服务
+   *
+   * @return WxMaAnalysisService
+   */
+  WxMaAnalysisService getAnalysisService();
+
+  /**
    * 返回代码操作相关的 API
    *
    * @return WxMaCodeService

+ 126 - 0
weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/WxMaAnalysisServiceImpl.java

@@ -0,0 +1,126 @@
+package cn.binarywang.wx.miniapp.api.impl;
+
+import cn.binarywang.wx.miniapp.api.WxMaAnalysisService;
+import cn.binarywang.wx.miniapp.api.WxMaService;
+import cn.binarywang.wx.miniapp.bean.analysis.WxMaRetainInfo;
+import cn.binarywang.wx.miniapp.bean.analysis.WxMaSummaryTrend;
+import cn.binarywang.wx.miniapp.bean.analysis.WxMaUserPortrait;
+import cn.binarywang.wx.miniapp.bean.analysis.WxMaVisitDistribution;
+import cn.binarywang.wx.miniapp.bean.analysis.WxMaVisitPage;
+import cn.binarywang.wx.miniapp.bean.analysis.WxMaVisitTrend;
+import cn.binarywang.wx.miniapp.util.json.WxMaGsonBuilder;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import com.google.gson.reflect.TypeToken;
+import me.chanjar.weixin.common.exception.WxErrorException;
+import org.apache.commons.lang3.time.DateFormatUtils;
+
+import java.lang.reflect.Type;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * @author <a href="https://github.com/charmingoh">Charming</a>
+ * @since 2018-04-28
+ */
+public class WxMaAnalysisServiceImpl implements WxMaAnalysisService {
+  private static final JsonParser JSON_PARSER = new JsonParser();
+  private WxMaService wxMaService;
+
+  public WxMaAnalysisServiceImpl(WxMaService wxMaService) {
+    this.wxMaService = wxMaService;
+  }
+
+  @Override
+  public List<WxMaSummaryTrend> getDailySummaryTrend(Date beginDate, Date endDate) throws WxErrorException {
+    return getAnalysisResultAsList(GET_DAILY_SUMMARY_TREND_URL, beginDate, endDate,
+      new TypeToken<List<WxMaSummaryTrend>>() {
+      }.getType());
+  }
+
+  @Override
+  public List<WxMaVisitTrend> getDailyVisitTrend(Date beginDate, Date endDate) throws WxErrorException {
+    return getAnalysisResultAsList(GET_DAILY_VISIT_TREND_URL, beginDate, endDate,
+      new TypeToken<List<WxMaVisitTrend>>() {
+      }.getType());
+  }
+
+  @Override
+  public List<WxMaVisitTrend> getWeeklyVisitTrend(Date beginDate, Date endDate) throws WxErrorException {
+    return getAnalysisResultAsList(GET_WEEKLY_VISIT_TREND_URL, beginDate, endDate,
+      new TypeToken<List<WxMaVisitTrend>>() {
+      }.getType());
+  }
+
+  @Override
+  public List<WxMaVisitTrend> getMonthlyVisitTrend(Date beginDate, Date endDate) throws WxErrorException {
+    return getAnalysisResultAsList(GET_MONTHLY_VISIT_TREND_URL, beginDate, endDate,
+      new TypeToken<List<WxMaVisitTrend>>() {
+      }.getType());
+  }
+
+  @Override
+  public WxMaVisitDistribution getVisitDistribution(Date beginDate, Date endDate) throws WxErrorException {
+    String responseContent = this.wxMaService.post(GET_VISIT_DISTRIBUTION_URL, toJson(beginDate, endDate));
+    return WxMaVisitDistribution.fromJson(responseContent);
+  }
+
+  @Override
+  public WxMaRetainInfo getDailyRetainInfo(Date beginDate, Date endDate) throws WxErrorException {
+    return getRetainInfo(beginDate, endDate, GET_DAILY_RETAIN_INFO_URL);
+  }
+
+  @Override
+  public WxMaRetainInfo getWeeklyRetainInfo(Date beginDate, Date endDate) throws WxErrorException {
+    return getRetainInfo(beginDate, endDate, GET_WEEKLY_RETAIN_INFO_URL);
+  }
+
+  @Override
+  public WxMaRetainInfo getMonthlyRetainInfo(Date beginDate, Date endDate) throws WxErrorException {
+    return getRetainInfo(beginDate, endDate, GET_MONTHLY_RETAIN_INFO_URL);
+  }
+
+  @Override
+  public List<WxMaVisitPage> getVisitPage(Date beginDate, Date endDate) throws WxErrorException {
+    return getAnalysisResultAsList(GET_VISIT_PAGE_URL, beginDate, endDate,
+      new TypeToken<List<WxMaVisitPage>>() {
+      }.getType());
+  }
+
+  @Override
+  public WxMaUserPortrait getUserPortrait(Date beginDate, Date endDate) throws WxErrorException {
+    String responseContent = this.wxMaService.post(GET_USER_PORTRAIT_URL, toJson(beginDate, endDate));
+    return WxMaUserPortrait.fromJson(responseContent);
+  }
+
+  private WxMaRetainInfo getRetainInfo(Date beginDate, Date endDate, String url) throws WxErrorException {
+    String responseContent = this.wxMaService.post(url, toJson(beginDate, endDate));
+    return WxMaRetainInfo.fromJson(responseContent);
+  }
+
+  /**
+   * 获取数据分析结果并返回 List,returnType 类型
+   *
+   * @param url        链接
+   * @param returnType 返回的类型
+   * @param <T>        返回的类型
+   * @return List 类型的数据
+   */
+  private <T> List<T> getAnalysisResultAsList(String url, Date beginDate, Date endDate, Type returnType) throws WxErrorException {
+    String responseContent = this.wxMaService.post(url, toJson(beginDate, endDate));
+    JsonObject response = JSON_PARSER.parse(responseContent).getAsJsonObject();
+    boolean hasList = response.has("list");
+    if (hasList) {
+      return WxMaGsonBuilder.create().fromJson(response.getAsJsonArray("list"), returnType);
+    } else {
+      return null;
+    }
+  }
+
+  private static String toJson(Date beginDate, Date endDate) {
+    JsonObject param = new JsonObject();
+    param.addProperty("begin_date", DateFormatUtils.format(beginDate, "yyyyMMdd"));
+    param.addProperty("end_date", DateFormatUtils.format(endDate, "yyyyMMdd"));
+    return param.toString();
+  }
+}

+ 7 - 0
weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/WxMaServiceImpl.java

@@ -1,5 +1,6 @@
 package cn.binarywang.wx.miniapp.api.impl;
 
+import cn.binarywang.wx.miniapp.api.WxMaAnalysisService;
 import cn.binarywang.wx.miniapp.api.WxMaCodeService;
 import cn.binarywang.wx.miniapp.api.WxMaMediaService;
 import cn.binarywang.wx.miniapp.api.WxMaMsgService;
@@ -52,6 +53,7 @@ public class WxMaServiceImpl implements WxMaService, RequestHttp<CloseableHttpCl
   private WxMaUserService userService = new WxMaUserServiceImpl(this);
   private WxMaQrcodeService qrCodeService = new WxMaQrcodeServiceImpl(this);
   private WxMaTemplateService templateService = new WxMaTemplateServiceImpl(this);
+  private WxMaAnalysisService analysisService = new WxMaAnalysisServiceImpl(this);
   private WxMaCodeService codeService = new WxMaCodeServiceImpl(this);
   private WxMaSettingService settingService = new WxMaSettingServiceImpl(this);
 
@@ -295,6 +297,11 @@ public class WxMaServiceImpl implements WxMaService, RequestHttp<CloseableHttpCl
   }
 
   @Override
+  public WxMaAnalysisService getAnalysisService() {
+    return this.analysisService;
+  }
+
+  @Override
   public WxMaCodeService getCodeService() {
     return this.codeService;
   }

+ 43 - 0
weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/analysis/WxMaRetainInfo.java

@@ -0,0 +1,43 @@
+package cn.binarywang.wx.miniapp.bean.analysis;
+
+import cn.binarywang.wx.miniapp.util.json.WxMaGsonBuilder;
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Map;
+
+/**
+ * 访问留存
+ *
+ * @author <a href="https://github.com/charmingoh">Charming</a>
+ * @since 2018-04-28 14:41
+ */
+@Data
+public class WxMaRetainInfo implements Serializable {
+  private static final long serialVersionUID = 8986403173656848413L;
+  /**
+   * 日留存:日期,yyyyMMdd 格式,如 20170313
+   * 周留存:时间,如"20170306-20170312"
+   * 月留存:时间,如"201702"
+   */
+  @SerializedName(value = "refDate", alternate = "ref_date")
+  private String refDate;
+  /**
+   * 新增用户留存
+   * - key:
+   *  - 日留存:标识,0开始,0表示当天,1表示1天后,依此类推,key取值分别是:0,1,2,3,4,5,6,7,14,30
+   *  - 周留存:标识,0开始,0表示当周,1表示1周后,依此类推,key 取值分别是:0,1,2,3,4
+   *  - 月留存:标识,0开始,0表示当月,1表示1月后,key取值分别是:0,1
+   * - value: key对应日期的新增用户数/活跃用户数(key=0时)或留存用户数(k>0时)
+   */
+  private Map<Integer, Integer> visitUvNew;
+  /**
+   * 活跃用户留存
+   */
+  private Map<Integer, Integer> visitUv;
+
+  public static WxMaRetainInfo fromJson(String json) {
+    return WxMaGsonBuilder.create().fromJson(json, WxMaRetainInfo.class);
+  }
+}

+ 37 - 0
weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/analysis/WxMaSummaryTrend.java

@@ -0,0 +1,37 @@
+package cn.binarywang.wx.miniapp.bean.analysis;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 小程序概况趋势
+ *
+ * @author <a href="https://github.com/charmingoh">Charming</a>
+ * @since 2018-04-28
+ */
+@Data
+public class WxMaSummaryTrend implements Serializable {
+  private static final long serialVersionUID = 1379688517709317935L;
+  /**
+   * 日期,yyyyMMdd 格式,如 20170313
+   */
+  @SerializedName(value = "refDate", alternate = "ref_date")
+  private String refDate;
+  /**
+   * 累计用户数
+   */
+  @SerializedName(value = "visitTotal", alternate = "visit_total")
+  private Long visitTotal;
+  /**
+   * 转发次数
+   */
+  @SerializedName(value = "sharePv", alternate = "share_pv")
+  private Long sharePv;
+  /**
+   * 转发人数
+   */
+  @SerializedName(value = "shareUv", alternate = "share_uv")
+  private Long shareUv;
+}

+ 68 - 0
weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/analysis/WxMaUserPortrait.java

@@ -0,0 +1,68 @@
+package cn.binarywang.wx.miniapp.bean.analysis;
+
+import cn.binarywang.wx.miniapp.util.json.WxMaGsonBuilder;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Map;
+
+/**
+ * 用户画像
+ *
+ * @author <a href="https://github.com/charmingoh">Charming</a>
+ * @since 2018-04-28
+ */
+@Data
+public class WxMaUserPortrait implements Serializable {
+  private static final long serialVersionUID = 5653571047669243178L;
+  /**
+   * 时间范围,如: "20170611-20170617"
+   */
+  private String refDate;
+  /**
+   * 新用户
+   */
+  private Item visitUvNew;
+  /**
+   * 活跃用户
+   */
+  private Item visitUv;
+
+  public static WxMaUserPortrait fromJson(String json) {
+    return WxMaGsonBuilder.create().fromJson(json, WxMaUserPortrait.class);
+  }
+
+  @Data
+  public static class Item {
+    /**
+     * key: 省份,如北京、广东等
+     * value: 活跃用户数或新用户数
+     */
+    private Map<String, Long> province;
+    /**
+     * key: 城市,如北京、广州等
+     * value: 活跃用户数或新用户数
+     */
+    private Map<String, Long> city;
+    /**
+     * key: 性别,包括男、女、未知
+     * value: 活跃用户数或新用户数
+     */
+    private Map<String, Long> genders;
+    /**
+     * key: 终端类型,包括iPhone, android,其他
+     * value: 活跃用户数或新用户数
+     */
+    private Map<String, Long> platforms;
+    /**
+     * key: 机型,如苹果iPhone6, OPPO R9等
+     * value: 活跃用户数或新用户数
+     */
+    private Map<String, Long> devices;
+    /**
+     * key: 年龄,包括17岁以下、18-24岁等区间
+     * value: 活跃用户数或新用户数
+     */
+    private Map<String, Long> ages;
+  }
+}

+ 83 - 0
weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/analysis/WxMaVisitDistribution.java

@@ -0,0 +1,83 @@
+package cn.binarywang.wx.miniapp.bean.analysis;
+
+import cn.binarywang.wx.miniapp.util.json.WxMaGsonBuilder;
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Map;
+
+/**
+ * 访问分布
+ * 访问来源:(index="access_source_session_cnt")
+ * 1:小程序历史列表
+ * 2:搜索
+ * 3:会话
+ * 4:二维码
+ * 5:公众号主页
+ * 6:聊天顶部
+ * 7:系统桌面
+ * 8:小程序主页
+ * 9:附近的小程序
+ * 10:其他
+ * 11:模板消息
+ * 12:客服消息
+ * 13: 公众号菜单
+ * 14: APP分享
+ * 15: 支付完成页
+ * 16: 长按识别二维码
+ * 17: 相册选取二维码
+ * 18: 公众号文章
+ * 19:钱包
+ * 20:卡包
+ * 21:小程序内卡券
+ * 22:其他小程序
+ * 23:其他小程序返回
+ * 24:卡券适用门店列表
+ * 25:搜索框快捷入口
+ * 26:小程序客服消息
+ * 27:公众号下发
+ * 访问时长:(index="access_staytime_info")
+ * 1: 0-2s
+ * 2: 3-5s
+ * 3: 6-10s
+ * 4: 11-20s
+ * 5: 20-30s
+ * 6: 30-50s
+ * 7: 50-100s
+ * 8: > 100s
+ * 平均访问深度:(index="access_depth_info")
+ * 1: 1页
+ * 2: 2页
+ * 3: 3页
+ * 4: 4页
+ * 5: 5页
+ * 6: 6-10页
+ * 7: >10页
+ *
+ * @author <a href="https://github.com/charmingoh">Charming</a>
+ * @since 2018-04-28
+ */
+@Data
+public class WxMaVisitDistribution implements Serializable {
+  private static final long serialVersionUID = 5404250039495926632L;
+  /**
+   * 日期,yyyyMMdd 格式,如 20170313
+   */
+  @SerializedName(value = "refDate", alternate = "ref_date")
+  private String refDate;
+  /**
+   * key: 分布类型
+   * - access_source_session_cnt 访问来源分布
+   * - access_staytime_info 访问时长分布
+   * - access_depth_info 访问深度的分布
+   * value: 场景 ID 下的值
+   * - key: 场景 ID
+   * - value: 场景下的值
+   */
+  private Map<String, Map<Integer, Integer>> list;
+
+  public static WxMaVisitDistribution fromJson(String json) {
+    return WxMaGsonBuilder.create().fromJson(json, WxMaVisitDistribution.class);
+  }
+}

+ 55 - 0
weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/analysis/WxMaVisitPage.java

@@ -0,0 +1,55 @@
+package cn.binarywang.wx.miniapp.bean.analysis;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * @author <a href="https://github.com/charmingoh">Charming</a>
+ * @since 2018-04-28
+ */
+@Data
+public class WxMaVisitPage implements Serializable {
+  private static final long serialVersionUID = -7006334774516877372L;
+  /**
+   * 页面路径
+   */
+  @SerializedName(value = "pagePath", alternate = "page_path")
+  private String pagePath;
+  /**
+   * 访问次数
+   */
+  @SerializedName(value = "pageVisitPv", alternate = "page_visit_pv")
+  private Long pageVisitPv;
+  /**
+   * 访问人数
+   */
+  @SerializedName(value = "pageVisitUv", alternate = "page_visit_uv")
+  private Long pageVisitUv;
+  /**
+   * 次均停留时长
+   */
+  @SerializedName(value = "pageStayTimePv", alternate = "page_staytime_pv")
+  private Float pageStayTimePv;
+  /**
+   * 进入页次数
+   */
+  @SerializedName(value = "entryPagePv", alternate = "entrypage_pv")
+  private Long entryPagePv;
+  /**
+   * 退出页次数
+   */
+  @SerializedName(value = "exitPagePv", alternate = "exitpage_pv")
+  private Long exitPagePv;
+  /**
+   * 转发次数
+   */
+  @SerializedName(value = "pageSharePv", alternate = "page_share_pv")
+  private Long pageSharePv;
+  /**
+   * 转发人数
+   */
+  @SerializedName(value = "pageShareUv", alternate = "page_share_uv")
+  private Long pageShareUv;
+}

+ 59 - 0
weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/analysis/WxMaVisitTrend.java

@@ -0,0 +1,59 @@
+package cn.binarywang.wx.miniapp.bean.analysis;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 访问趋势
+ *
+ * @author <a href="https://github.com/charmingoh">Charming</a>
+ * @since 2018-04-28
+ */
+@Data
+public class WxMaVisitTrend implements Serializable {
+  private static final long serialVersionUID = 1379688517709317935L;
+  /**
+   * 日留存:日期,yyyyMMdd 格式,如 20170313
+   * 周留存:时间,如"20170306-20170312"
+   * 月留存:时间,如"201702"
+   */
+  @SerializedName(value = "refDate", alternate = "ref_date")
+  private String refDate;
+  /**
+   * 打开次数
+   */
+  @SerializedName(value = "sessionCnt", alternate = "session_cnt")
+  private Long sessionCnt;
+  /**
+   * 访问次数
+   */
+  @SerializedName(value = "visitPv", alternate = "visit_pv")
+  private Long visitPv;
+  /**
+   * 访问人数
+   */
+  @SerializedName(value = "visitUv", alternate = "visit_uv")
+  private Long visitUv;
+  /**
+   * 新用户数
+   */
+  @SerializedName(value = "visitUvNew", alternate = "visit_uv_new")
+  private Long visitUvNew;
+  /**
+   * 人均停留时长 (浮点型,单位:秒)
+   */
+  @SerializedName(value = "stayTimeUv", alternate = "stay_time_uv")
+  private Float stayTimeUv;
+  /**
+   * 人均停留时长 (浮点型,单位:秒)
+   */
+  @SerializedName(value = "stayTimeSession", alternate = "stay_time_session")
+  private Float stayTimeSession;
+  /**
+   * 人均停留时长 (浮点型,单位:秒)
+   */
+  @SerializedName(value = "visitDepth", alternate = "visit_depth")
+  private Float visitDepth;
+}

+ 7 - 0
weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/analysis/package-info.java

@@ -0,0 +1,7 @@
+/**
+ * 数据分析
+ *
+ * @author <a href="https://github.com/charmingoh">Charming</a>
+ * @since 2018-04-28
+ */
+package cn.binarywang.wx.miniapp.bean.analysis;

+ 6 - 0
weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/util/json/WxMaGsonBuilder.java

@@ -1,6 +1,9 @@
 package cn.binarywang.wx.miniapp.util.json;
 
 import cn.binarywang.wx.miniapp.bean.WxMaTemplateMessage;
+import cn.binarywang.wx.miniapp.bean.analysis.WxMaRetainInfo;
+import cn.binarywang.wx.miniapp.bean.analysis.WxMaUserPortrait;
+import cn.binarywang.wx.miniapp.bean.analysis.WxMaVisitDistribution;
 import cn.binarywang.wx.miniapp.bean.code.WxMaCodeCommitRequest;
 import cn.binarywang.wx.miniapp.bean.code.WxMaCodeVersionDistribution;
 import com.google.gson.Gson;
@@ -17,6 +20,9 @@ public class WxMaGsonBuilder {
     INSTANCE.registerTypeAdapter(WxMaTemplateMessage.class, new WxMaTemplateMessageGsonAdapter());
     INSTANCE.registerTypeAdapter(WxMaCodeCommitRequest.class, new WxMaCodeCommitRequestGsonAdapter());
     INSTANCE.registerTypeAdapter(WxMaCodeVersionDistribution.class, new WxMaCodeVersionDistributionGsonAdapter());
+    INSTANCE.registerTypeAdapter(WxMaVisitDistribution.class, new WxMaVisitDistributionGsonAdapter());
+    INSTANCE.registerTypeAdapter(WxMaRetainInfo.class, new WxMaRetainInfoGsonAdapter());
+    INSTANCE.registerTypeAdapter(WxMaUserPortrait.class, new WxMaUserPortraitGsonAdapter());
   }
 
   public static Gson create() {

+ 52 - 0
weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/util/json/WxMaRetainInfoGsonAdapter.java

@@ -0,0 +1,52 @@
+package cn.binarywang.wx.miniapp.util.json;
+
+import cn.binarywang.wx.miniapp.bean.analysis.WxMaRetainInfo;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+import me.chanjar.weixin.common.util.json.GsonHelper;
+
+import java.lang.reflect.Type;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * @author <a href="https://github.com/charmingoh">Charming</a>
+ * @since 2018-04-28
+ */
+public class WxMaRetainInfoGsonAdapter implements JsonDeserializer<WxMaRetainInfo> {
+  @Override
+  public WxMaRetainInfo deserialize(JsonElement json, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
+    if (json == null) {
+      return null;
+    }
+
+    WxMaRetainInfo retainInfo = new WxMaRetainInfo();
+    JsonObject object = json.getAsJsonObject();
+    String refDate = GsonHelper.getString(object, "ref_date");
+    retainInfo.setRefDate(refDate);
+    retainInfo.setVisitUvNew(getAsMap(object, "visit_uv_new"));
+    retainInfo.setVisitUv(getAsMap(object, "visit_uv"));
+    return retainInfo;
+  }
+
+  private Map<Integer, Integer> getAsMap(JsonObject object, String memberName) {
+    JsonArray array = object.getAsJsonArray(memberName);
+    if (array != null && array.size() > 0) {
+      Map<Integer, Integer> map = new LinkedHashMap<>(array.size());
+      for (JsonElement element : array) {
+        JsonObject elementObject = element.getAsJsonObject();
+        Integer key = GsonHelper.getInteger(elementObject, "key");
+        if (key != null) {
+          Integer value = GsonHelper.getInteger(elementObject, "value");
+          map.put(key, value);
+        }
+      }
+      return map;
+    }
+    return null;
+  }
+}

+ 67 - 0
weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/util/json/WxMaUserPortraitGsonAdapter.java

@@ -0,0 +1,67 @@
+package cn.binarywang.wx.miniapp.util.json;
+
+import cn.binarywang.wx.miniapp.bean.analysis.WxMaUserPortrait;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+import me.chanjar.weixin.common.util.json.GsonHelper;
+import org.apache.commons.lang3.StringUtils;
+
+import java.lang.reflect.Type;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * @author <a href="https://github.com/charmingoh">Charming</a>
+ * @since 2018-04-28
+ */
+public class WxMaUserPortraitGsonAdapter implements JsonDeserializer<WxMaUserPortrait> {
+  @Override
+  public WxMaUserPortrait deserialize(JsonElement json, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
+    if (json == null) {
+      return null;
+    }
+
+    WxMaUserPortrait portrait = new WxMaUserPortrait();
+    JsonObject object = json.getAsJsonObject();
+    String refDate = GsonHelper.getString(object, "ref_date");
+    portrait.setRefDate(refDate);
+    portrait.setVisitUvNew(getPortraitItem(object.getAsJsonObject("visit_uv_new")));
+    portrait.setVisitUv(getPortraitItem(object.getAsJsonObject("visit_uv")));
+    return portrait;
+  }
+
+  private WxMaUserPortrait.Item getPortraitItem(JsonObject object) {
+    if (object == null) {
+      return null;
+    }
+    WxMaUserPortrait.Item item = new WxMaUserPortrait.Item();
+    item.setProvince(getAsMap(object, "province"));
+    item.setCity(getAsMap(object, "city"));
+    item.setGenders(getAsMap(object, "genders"));
+    item.setPlatforms(getAsMap(object, "platforms"));
+    item.setDevices(getAsMap(object, "devices"));
+    item.setAges(getAsMap(object, "ages"));
+    return item;
+  }
+
+  private Map<String, Long> getAsMap(JsonObject object, String memberName) {
+    JsonArray array = object.getAsJsonArray(memberName);
+    if (array != null && array.size() > 0) {
+      Map<String, Long> map = new LinkedHashMap<>(array.size());
+      for (JsonElement element : array) {
+        JsonObject elementObject = element.getAsJsonObject();
+        String name = GsonHelper.getString(elementObject, "name");
+        if (StringUtils.isNotBlank(name)) {
+          Long value = GsonHelper.getLong(elementObject, "value");
+          map.put(name, value);
+        }
+      }
+      return map;
+    }
+    return null;
+  }
+}

+ 67 - 0
weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/util/json/WxMaVisitDistributionGsonAdapter.java

@@ -0,0 +1,67 @@
+package cn.binarywang.wx.miniapp.util.json;
+
+import cn.binarywang.wx.miniapp.bean.analysis.WxMaVisitDistribution;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+import me.chanjar.weixin.common.util.json.GsonHelper;
+
+import java.lang.reflect.Type;
+import java.util.Hashtable;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * @author <a href="https://github.com/charmingoh">Charming</a>
+ * @since 2018-04-28
+ */
+public class WxMaVisitDistributionGsonAdapter implements JsonDeserializer<WxMaVisitDistribution> {
+  @Override
+  public WxMaVisitDistribution deserialize(JsonElement json, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
+    if (json == null) {
+      return null;
+    }
+
+    WxMaVisitDistribution distribution = new WxMaVisitDistribution();
+    JsonObject object = json.getAsJsonObject();
+    String refDate = GsonHelper.getString(object, "ref_date");
+    distribution.setRefDate(refDate);
+
+    boolean hasList = object.has("list");
+    if (!hasList) {
+      return distribution;
+    }
+
+    JsonArray listArray = object.getAsJsonArray("list");
+    Map<String, Map<Integer, Integer>> list = new Hashtable<>(listArray.size());
+    for (JsonElement indexElement : listArray) {
+      JsonObject indexObject = indexElement.getAsJsonObject();
+      String index = GsonHelper.getString(indexObject, "index");
+      if (index == null) {
+        continue;
+      }
+
+      Map<Integer, Integer> itemList = new LinkedHashMap<>();
+      JsonArray itemArray = indexObject.getAsJsonArray("item_list");
+      if (itemArray == null || itemArray.size() <= 0) {
+        list.put(index, itemList);
+        continue;
+      }
+
+      for (JsonElement itemElement : itemArray) {
+        JsonObject itemObject = itemElement.getAsJsonObject();
+        Integer key = GsonHelper.getInteger(itemObject, "key");
+        Integer value = GsonHelper.getInteger(itemObject, "value");
+        if (key != null) {
+          itemList.put(key, value);
+        }
+      }
+      list.put(index, itemList);
+    }
+    distribution.setList(list);
+    return distribution;
+  }
+}

+ 155 - 0
weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/api/impl/WxMaAnalysisServiceImplTest.java

@@ -0,0 +1,155 @@
+package cn.binarywang.wx.miniapp.api.impl;
+
+import cn.binarywang.wx.miniapp.api.WxMaAnalysisService;
+import cn.binarywang.wx.miniapp.api.WxMaService;
+import cn.binarywang.wx.miniapp.bean.analysis.WxMaRetainInfo;
+import cn.binarywang.wx.miniapp.bean.analysis.WxMaSummaryTrend;
+import cn.binarywang.wx.miniapp.bean.analysis.WxMaUserPortrait;
+import cn.binarywang.wx.miniapp.bean.analysis.WxMaVisitDistribution;
+import cn.binarywang.wx.miniapp.bean.analysis.WxMaVisitPage;
+import cn.binarywang.wx.miniapp.bean.analysis.WxMaVisitTrend;
+import cn.binarywang.wx.miniapp.test.ApiTestModule;
+import com.google.inject.Inject;
+import org.apache.commons.lang3.time.DateFormatUtils;
+import org.apache.commons.lang3.time.DateUtils;
+import org.testng.annotations.Guice;
+import org.testng.annotations.Test;
+
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+
+import static org.testng.Assert.*;
+
+/**
+ * @author <a href="https://github.com/charmingoh">Charming</a>
+ * @since 2018-04-28
+ */
+@Guice(modules = ApiTestModule.class)
+public class WxMaAnalysisServiceImplTest {
+  @Inject
+  private WxMaService wxMaService;
+
+  @Test
+  public void testGetDailySummaryTrend() throws Exception {
+    final WxMaAnalysisService service = wxMaService.getAnalysisService();
+    Date twoDaysAgo = DateUtils.addDays(new Date(), -2);
+    List<WxMaSummaryTrend> trends = service.getDailySummaryTrend(twoDaysAgo, twoDaysAgo);
+    assertEquals(1, trends.size());
+    System.out.println(trends);
+  }
+
+  @Test
+  public void testGetDailyVisitTrend() throws Exception {
+    final WxMaAnalysisService service = wxMaService.getAnalysisService();
+    Date twoDaysAgo = DateUtils.addDays(new Date(), -2);
+    List<WxMaVisitTrend> trends = service.getDailyVisitTrend(twoDaysAgo, twoDaysAgo);
+    assertEquals(1, trends.size());
+    System.out.println(trends);
+  }
+
+  @Test
+  public void testGetWeeklyVisitTrend() throws Exception {
+    Calendar calendar = Calendar.getInstance();
+    calendar.set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY);
+    Date now = new Date();
+    Date lastSunday = calendar.getTime();
+    if (DateUtils.isSameDay(lastSunday, now)) {
+      lastSunday = DateUtils.addDays(lastSunday, -7);
+    }
+    Date lastMonday = DateUtils.addDays(lastSunday, -6);
+
+    final WxMaAnalysisService service = wxMaService.getAnalysisService();
+    List<WxMaVisitTrend> trends = service.getWeeklyVisitTrend(lastMonday, lastSunday);
+    assertEquals(1, trends.size());
+    System.out.println(trends);
+  }
+
+  @Test
+  public void testGetMonthlyVisitTrend() throws Exception {
+    Date now = new Date();
+    Date firstDayOfThisMonth = DateUtils.setDays(now, 1);
+    Date lastDayOfLastMonth = DateUtils.addDays(firstDayOfThisMonth, -1);
+    Date firstDayOfLastMonth = DateUtils.addMonths(firstDayOfThisMonth, -1);
+
+    final WxMaAnalysisService service = wxMaService.getAnalysisService();
+    List<WxMaVisitTrend> trends = service.getMonthlyVisitTrend(firstDayOfLastMonth, lastDayOfLastMonth);
+    assertEquals(1, trends.size());
+    System.out.println(trends);
+  }
+
+  @Test
+  public void testGetVisitDistribution() throws Exception {
+    final WxMaAnalysisService service = wxMaService.getAnalysisService();
+    Date twoDaysAgo = DateUtils.addDays(new Date(), -2);
+    WxMaVisitDistribution distribution = service.getVisitDistribution(twoDaysAgo, twoDaysAgo);
+    assertNotNull(distribution);
+    String date = DateFormatUtils.format(twoDaysAgo, "yyyyMMdd");
+    assertEquals(date, distribution.getRefDate());
+    assertTrue(distribution.getList().containsKey("access_source_session_cnt"));
+    assertTrue(distribution.getList().containsKey("access_staytime_info"));
+    assertTrue(distribution.getList().containsKey("access_depth_info"));
+    System.out.println(distribution);
+  }
+
+  @Test
+  public void testGetDailyRetainInfo() throws Exception {
+    final WxMaAnalysisService service = wxMaService.getAnalysisService();
+    Date twoDaysAgo = DateUtils.addDays(new Date(), -2);
+    WxMaRetainInfo retainInfo = service.getDailyRetainInfo(twoDaysAgo, twoDaysAgo);
+    assertNotNull(retainInfo);
+    System.out.println(retainInfo);
+  }
+
+  @Test
+  public void testGetWeeklyRetainInfo() throws Exception {
+    Calendar calendar = Calendar.getInstance();
+    calendar.set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY);
+    Date now = new Date();
+    Date lastSunday = calendar.getTime();
+    if (DateUtils.isSameDay(lastSunday, now)) {
+      lastSunday = DateUtils.addDays(lastSunday, -7);
+    }
+    Date lastMonday = DateUtils.addDays(lastSunday, -6);
+
+    final WxMaAnalysisService service = wxMaService.getAnalysisService();
+    WxMaRetainInfo retainInfo = service.getWeeklyRetainInfo(lastMonday, lastSunday);
+    assertNotNull(retainInfo);
+    System.out.println(retainInfo);
+  }
+
+  @Test
+  public void testGetMonthlyRetainInfo() throws Exception {
+    Date now = new Date();
+    Date firstDayOfThisMonth = DateUtils.setDays(now, 1);
+    Date lastDayOfLastMonth = DateUtils.addDays(firstDayOfThisMonth, -1);
+    Date firstDayOfLastMonth = DateUtils.addMonths(firstDayOfThisMonth, -1);
+
+    final WxMaAnalysisService service = wxMaService.getAnalysisService();
+    WxMaRetainInfo retainInfo = service.getMonthlyRetainInfo(firstDayOfLastMonth, lastDayOfLastMonth);
+    assertNotNull(retainInfo);
+    System.out.println(retainInfo);
+  }
+
+  @Test
+  public void testGetVisitPage() throws Exception {
+    final WxMaAnalysisService service = wxMaService.getAnalysisService();
+    Date twoDaysAgo = DateUtils.addDays(new Date(), -2);
+    List<WxMaVisitPage> visitPages = service.getVisitPage(twoDaysAgo, twoDaysAgo);
+    assertNotNull(visitPages);
+    System.out.println(visitPages);
+    System.out.println(visitPages.get(0).getPagePath());
+    System.out.println(visitPages.get(0).getPageVisitPv());
+  }
+
+  @Test
+  public void testGetUserPortrait() throws Exception {
+    Date twoDaysAgo = DateUtils.addDays(new Date(), -2);
+    Date eightDaysAgo = DateUtils.addDays(new Date(), -8);
+
+    final WxMaAnalysisService service = wxMaService.getAnalysisService();
+    WxMaUserPortrait portrait = service.getUserPortrait(eightDaysAgo, twoDaysAgo);
+    assertNotNull(portrait);
+    System.out.println(portrait);
+  }
+}

+ 22 - 0
weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/bean/analysis/WxMaRetainInfoTest.java

@@ -0,0 +1,22 @@
+package cn.binarywang.wx.miniapp.bean.analysis;
+
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.*;
+
+/**
+ * @author <a href="https://github.com/charmingoh">Charming</a>
+ * @since 2018-04-28
+ */
+public class WxMaRetainInfoTest {
+  @Test
+  public void testFromJson() throws Exception {
+    String json = "{\"ref_date\":\"20170313\",\"visit_uv_new\":[{\"key\":0,\"value\":5464}],\"visit_uv\":[{\"key\":0,\"value\":55500}]}\n";
+    WxMaRetainInfo retainInfo = WxMaRetainInfo.fromJson(json);
+    assertNotNull(retainInfo);
+    assertEquals("20170313", retainInfo.getRefDate());
+    assertTrue(retainInfo.getVisitUv().containsKey(0));
+    assertTrue(retainInfo.getVisitUvNew().containsKey(0));
+    System.out.println(retainInfo);
+  }
+}

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 19 - 0
weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/bean/analysis/WxMaUserPortraitTest.java


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 23 - 0
weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/bean/analysis/WxMaVisitDistributionTest.java