中间有过改动的地方,想想还是留着之前的,记录一下这个思考、变化的过程
哇~终于到写博客的时候了
行为分析:分析什么(用户的行为)
前端是angular,借助其自带的指令收集用户行为;
后端借助ELK:存到elasticsearch中借助kibana强大的图形展示 以图表方式展示分析的结果;
思路:
1、分为二部分:整个系统为一个实体,具体网页 为一个实体,其实还有一个局部区域的实体,但是交接的时候三个对他们来说有点绕,那索性去一个好辣
2、进入时开始收集数据,离开区域时调用后端方法保存数据,单击时获取单击的相关信息;
如:进入系统界面时收集进入时数据、及浏览器的一些信息(给实体),离开或退出时(准备用组件销毁钩子)收集离开的相关信息;网页和区域同样的道理;
关于离开时调用后端,之前计划离开整个系统时一起提交到后端,但是跨指令我new实体,你知道,之前的数据就没有了,(⊙o⊙)…我可能陷里面了,目前没有找到更好的方法,欢迎大家多提宝贵意见;
前端:
大致思路如下
//定义一个指令:gatherComponentData;指令和组件是一样的道理
@Directive({
selector: '[gatherComponentData]'
})
//考虑到后端地址可能会变,所以需要@input给指令传递后端的url以方便调用,这样改的话成本比较小
@Input('gatherComponentData') behaviorActionUrl: string;
这些三个指令是一样的,下面多少有一些差异,但是都差不多:
//界面加载完成之后 给webContent实体赋值,并存储一部分数据到localstoage中,系统界面销毁时存储到后端
ngAfterViewInit() {
let webContent = {
webEnterTime: new Date(),browserLanguage: navigator.language || "en-*",browserColorDepth: screen.colorDepth.toString() + "像素/英寸",browserWidth: document.documentElement.scrollWidth.toString() || document.body.scrollWidth.toString() || screen.width.toString(),browserHeight: document.documentElement.scrollHeight.toString() || document.body.scrollHeight.toString() || screen.height.toString(),browserPlatform: navigator.platform || "",browserCookieEnabled: navigator.cookieEnabled.toString(),browserName: navigator.userAgent
}
this.localStorage.setObject('webContent',webContent);
};
/** * 监听 了点击事件,target是点击的目标:90%是监听对象 */
@HostListener('click',['$event.target'])
onMouseclick(btn: HTMLElement) {
//模块名、网页名 根据id获取
// iconfont icon-9 iconfont icon-jiantouxia iconfont icon-jiantouxia
if (btn.id == "webPageId") {
this.webPageContent.webpageName = btn.innerText.trim();//网页名
localStorage.setItem("pageName",btn.innerText.trim());
} else if (btn.id == "webModuleId") {
this.webPageContent.webmoduleName = btn.innerText.trim();//网页所属模块名 需要添加
localStorage.setItem("moduleName",btn.innerText.trim());
} else {
this.webPageContent.webmenuName = btn.innerHTML.trim();//网页一级菜单名
localStorage.setItem("menuName",btn.innerText.trim());
}
};
根据上面的信息,我有一个判断,前端html界面上如果没有这个id的话是需要添加的,或者换成其他的如class也可以,目前是根据id存储到localstorage里面,方便网页指令、区域指令获取自己的网页名等
再展示一个离开事件吧:为了防止用户只是一划而过而收集到无效信息,对时间添加了一个判断:
/** * 鼠标离开 整个组件的 事件 */
@HostListener('mouseleave')
onMouseLeave() {
if (this.childDiv) {
//日期格式问题:后台实体内的日期是date类型,但是我如果转成字符串格式是正确的,在转成日期格式就又自动格式化了;停用
// var startDate = new Date();
// let startString = startDate.getFullYear() + '-' + (startDate.getMonth() + 1) + '-' + startDate.getDate() + ' ' + startDate.getHours() + ':' + startDate.getMinutes() + ':' + startDate.getSeconds();
this.enterDivMsg.leaveTime = new Date();//离开组件的时间
let staySecond: number = (this.enterDivMsg.leaveTime.getTime() - this.enterDivMsg.enterTime.getTime()) / 1000
if (staySecond > 1) {//如果离开操作距离 进入操作 只隔 2s 则 此次离开事件不算
this.enterDivMsg.webpageName = localStorage.getItem("pageName") || " ";//网页名
this.enterDivMsg.webmoduleName = localStorage.getItem("moduleName") || " ";//网页所属模块名 需要添加
//传给后端网页数据
let body = JSON.stringify(this.enterDivMsg);
this.enterDivMsg = new WebPageContent();
let headers = new Headers({ 'Content-Type': 'application/json' });
let options = new RequestOptions({ headers: headers });//,options
let boolean: any = this.http.post(this.behaviorActionChildUrl,body)
.map(res => <any>res.json());
}
}
};
后端
后端用的是bboss连接的elasticsearch并存储:只是存储
bbossgroups是国内首款集AOP、MVC、持久化、JSP标签库、分布式RPC服务、分布式事件框架于一身的企业级JavaEE开发框架,在Apache License Version 2.0 许可协议下开源,后有介绍:
和solr差不多,es需要建立映射,映射很像给实体里的字段定义类型:基本类型
<property name="createUserActionIndice">
<![CDATA[{
"settings": {
"number_of_shards": 6,"index.refresh_interval": "5s"
},{
"mappings": {
"webContentComm": {
"properties": {
"userId": {//字段
"type": "text"//类型
},"userName": {
"type": "text"
},"webName": {
"type": "text"
},"userIp": {
"type": "ip"
},"browserWidth": {
"type": "text"
},"browserHeight": {
"type": "text"
},"browserDomain": {
"type": "text"
},"browserName": {
"type": "text"
},"browserColorDepth": {
"type": "text"
},"broserLanguage": {
"type": "text"
},"broserPlatform": {
"type": "keyword"
},"broserCookieEnabled": {
"type": "text"
},"webEnterTime": {
"type": "date"
},"webLeaveTime": {
"type": "date"
},"webStayTime": {
"type": "long"
},"webPageActionEntityList": {
"type": "nested","properties": {
"webmoduleName": {
"type": "text"
},"webpageName": {
"type": "text"
},"webpageUrl": {
"type": "text"
},"browserReferrer": {
"type": "text"
},"webpageId": {
"type": "text"
},"webpageEnterTime": {
"type": "date"
},"webpageLeaveTime": {
"type": "date"
},"webpageStayTime": {
"type": "long"
},"btnNameList": {
"type": "object"
},"selectOptionList": {
"type": "object"
},"searchContentList": {
"type": "object"
},"divActionEntityList": {
"type": "nested","properties": {
"userId": {
"type": "text"
},"enterTime": {
"type": "date"
},"leaveTime": {
"type": "date"
},"stayDivTime": {
"type": "long"
},"divTitle": {
"type": "text"
},"btnNameList": {
"type": "object"
}
}
}
}
}
}
}
}
}
}]]>
</property>
映射其实很简单吧只是有些长而已,需要说明的是:
1、object是一个对象,对应单个JSON对象
JSON文档本质上是层次化的:文档可能包含内部对象,而内部对象本身可能包含内部对象;不过其会扁平化处理:
拿官网来说:
//为my_index这个索引的_doc这个type下的1的文档创建映射
PUT my_index/_doc/1
{
"region": "US","manager": {
"age": 30,"name": {
"first": "John","last": "Smith"
}
}
}
被处理成:
{
"region": "US","manager.age": 30,"manager.name.first": "John",//**
"manager.name.last": "Smith"//**
}
2、Nested:嵌套用于JSON对象的数组,这样就立体了,嵌套的
嵌套字段也可以被单独查询了
3、映射其实还有父子关系,类似于一对多的数据库表,这个挺好研究的但是机会要留给后面的学习者嘛,以后再说吧,这个父子关系更加适合我的这三个实体
代码
刚才说了三个实体是依次传到后台的,所以也需要合起来传到es中,如果不和那岂不是很散,将来我们的系统做大了,那岂不是很乱;
先定义带有嵌套的实体,用于合数据提交到es中:
//region 定义list用来承接实体并提交给es
private List<DivActionEntity> enterDivMsgEntitieList = new ArrayList<>();//接受所有的div元素信息
private List<WebPageActionEntity> webPageActionEntityList = new ArrayList<>();//接受所有的网页信息,多个用户
private List<WebActionEntity> webActionEntityList = new ArrayList<>();//接受所有用户关于网站的信息
private String userIp;
//endregion
方法中有实体这个,这个实体中只有自己的东西,没有延伸没有扩展
@RequestMapping(value = "/gatherWebData",method = RequestMethod.POST)
@CrossOrigin
public void gatherWebData(@RequestBody OnlyWebModel webContent,@PathVariable String webName,HttpServletRequest request) throws IOException {
//region 如果ip为空 则获取ip 最后退出走这个方法,之前获取网页数据时应该是有ip了,为防万一重新获取
if (userIp.isEmpty()) {
userIp = this.getUserIp(request);
}
//endregion
//region 填充webContent网站实体 可能有多个用户,需要 传值给 list
WebActionEntity webContentComm = new WebActionEntity();
webContentComm.setUserId(webContent.getUserId());
……
webContentComm.setUserIp(this.userIp);
webActionEntityList.add(webContentComm);
//endregion
joinEntity();//合并实体
//submitData();//提交
}
最后的合并,筛选出一个用户的信息则提交:
public void joinEntity() {
webActionEntityList.forEach(webItem -> {
//region接受一个用户关于网页的信息
List<WebPageActionEntity> singleWebPageEntityList = new ArrayList<>();
webPageActionEntityList.forEach(pageItem -> {
if (webItem.getUserId() == pageItem.getUserId()) {
//region 接受一个用户的div信息
List<DivActionEntity> singleUserDivActionEntities = new ArrayList<>();
enterDivMsgEntitieList.forEach(divItem -> {
if (pageItem.getUserId() == divItem.getUserId()) {
//如果div的用户id和网页的用户id相同
singleUserDivActionEntities.add(divItem);
}
});
//将用户的div信息 以用户为单位 保存到 网页信息中
pageItem.setDivActionEntityList(singleUserDivActionEntities);
singleUserDivActionEntities.clear();
//endregion
singleWebPageEntityList.add(pageItem);
}
});
webItem.setWebPageActionEntityList(singleWebPageEntityList);
singleWebPageEntityList.clear();
//endregion
submitData(webItem);//提交
});
}
kibana目前真正研究中,进度有点慢的原因是借助x-pack给自己挖了不少的坑,现在关闭了x-pack的安全验证,这样是不太好、我是不想在这纠结了,好长时间,es也由原来的集群变成了现在的单机【说多了都是泪,以后不要一个人研究对象了,折寿】
修改:2018年5月1日14:43:24
上面的代码和前端的代码是的,new一下原来的东西就没有了,所以重构(xie)了一下,思路是将原来的索引一分为二,映射为:
#网站:
PUT /web-action
{
"mappings": {
"webContentComm": {
"properties": {
"userId": {
"type": "text"
},"webStayTime": {
"type": "long"
}
}
}
}
}
#网页:
PUT /webpage-action
{
"mappings": {
"webPageActionEntityList": {
"properties": {
"webmoduleName": {
"type": "text"
},"searchContentList": {
"type": "object"
}
}
}
}
}
#Div局部区域:这般不用,还是写上吧
PUT /webdiv-action
{
"mappings": {
"divActionEntityList": {
"properties": {
"userId": {
"type": "text"
},"enterTime": {
"type": "date"
},"leaveTime": {
"type": "date"
},"stayDivTime": {
"type": "long"
},"divTitle": {
"type": "text"
},"btnNameList": {
"type": "object"
}
}
}
}
}
后端存储也改成了分别存储:
package com.dmsdbj.bebavior.controller;
import ……
/** * 单个存储,映射已经建立, * 网页单独存储,通过userId 逻辑上建立联系,es中并没有真正建立联系 * Created by phoebeM on 2018/04/22. */
@Controller
@RequestMapping("/getSingelActionData")
public class userActionSingle {
private String userIp;
/** * 用户退出 时 获取网站信息 并提交到es中 * * @param webContent 只是包含整体信息,里面的子实体 是webContentComm * @return */
@RequestMapping(value = "/postWebData",method = RequestMethod.POST)
@CrossOrigin
public void gatherWebData(@RequestBody OnlyWebModel webContent,HttpServletRequest request) throws IOException {
List<OnlyWebModel> onlyWebModels = new ArrayList<>();
//region 如果ip为空 则获取ip 最后退出走这个方法,之前获取网页数据时应该是有ip了,为防万一重新获取
if (userIp.isEmpty()) {
userIp = this.getUserIp(request);
}
//endregion
//region 填充webContent网站实体 可能有多个用户,需要 传值给 list
//用户id作为标识 回覆盖 ,所以标识=用户id+时间
Date day = new Date();
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
webContent.setUserId(webContent.getUserId() + df.format(day));
//用户ip
webContent.setUserIp(this.userIp);
//停留时长 s为单位
webContent.setWebStayTime((webContent.getWebEnterTime().getTime() -
webContent.getWebLeaveTime().getTime()) / 1000);
onlyWebModels.add(webContent);
//endregion
//调用方法,提交到es
//创建创建/修改/获取文档的客户端对象,单实例多线程安全
ClientInterface clientUtil = ElasticSearchHelper.getRestClientUtil();
//添加或者修改文档,如果Id已经存在做修改操作,否则做添加文档操作,返回处理结果
String responseString = clientUtil.addDocuments("web-action",//索引表
"webContentComm",//索引类型
onlyWebModels);
onlyWebModels.clear();
}
/** * 用户离开网页时获取网页信息 * * @param * @return */
@RequestMapping(value = "/postWebPageData",method = RequestMethod.POST)
@CrossOrigin
public void gatherWebPageData(@RequestBody OnlyWebPageModel onlyWebPageModel,HttpServletRequest request,HttpServletResponse response) throws IOException {
List<OnlyWebPageModel> onlyWebPageModels = new ArrayList<>();
//获取用户ip
userIp = this.getUserIp(request);
//region 将参数中只包含网页级别信息的数据 需要传给 包含区域实体的网页实体
Date day = new Date();
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
onlyWebPageModel.setUserId(onlyWebPageModel.getUserId() + df.format(day));//加上日期,方便查询
onlyWebPageModel.setUserIp(userIp);
Long stayTime = (onlyWebPageModel.getWebpageEnterTime().getTime() - onlyWebPageModel.getWebpageLeaveTime().getTime()) / 1000;
onlyWebPageModel.setWebpageStayTime(stayTime);
onlyWebPageModels.add(onlyWebPageModel);
//endregion
//调用方法,提交到es
//创建创建/修改/获取/删除文档的客户端对象,单实例多线程安全
ClientInterface clientUtil = ElasticSearchHelper.getRestClientUtil();
//添加或者修改文档,如果Id已经存在做修改操作,否则做添加文档操作,返回处理结果
String responseString = clientUtil.addDocuments("webpage-action",//索引表
"webPageActionEntityList",//索引类型
onlyWebPageModels);
onlyWebPageModels.clear();
}
/** * 获取用户ip * * @param request * @return */
public String getUserIp(HttpServletRequest request) {
userIp = request.getHeader("x-forwarded-for");
if (userIp == null || userIp.length() == 0 || "unknown".equalsIgnoreCase(userIp)) {
userIp = request.getHeader("Proxy-Client-IP");
}
if (userIp == null || userIp.length() == 0 || "unknown".equalsIgnoreCase(userIp)) {
userIp = request.getHeader("WL-Proxy-Client-IP");
}
if (userIp == null || userIp.length() == 0 || "unknown".equalsIgnoreCase(userIp)) {
userIp = request.getRemoteAddr();
}
return userIp;
}
}
关于bboss
官网地址:http://www.bbossgroups.com/
官方博客:http://yin-bp.iteye.com/
作者:尹标平,2001年大学毕业,一直从事JavaEE企业应用开发和架构设计工作,做过开发员、架构师、项目经理之类的,喜欢搞点开源方面的东东,比较拿得出手的开源项目只有bbossgroups。 (#^.^#)这个自我介绍有点谦虚了,大佬也是一位平易近人、乐于助人的好大佬
伊标平大佬主页:https://my.oschina.net/bboss
更多信息请访问文档:
高性能elasticsearch ORM开发库使用介绍-http://www.jb51.cc/article/p-xpuveyrf-bqr.html 里面的视频有增删改查的例子
bboss elasticsearch技术交流群:166471282,大佬挺忙的,大家先百度Google
bboss elasticsearch微信公众号:bbossgroups
基础映射的网址:
https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-types.html
elk文档入口https://www.elastic.co/guide/index.html
中文权威指南:
https://www.elastic.co/guide/cn/elasticsearch/guide/current/index.html