主页与其它页面跳转,与基本数据渲染

master
吴治霖 1 year ago
parent d70e9ade2a
commit c1d37358dd

@ -0,0 +1,66 @@
<template>
<view class="dishes-item">
<!-- 菜品左侧图片区域 -->
<view class="dishes-item-left">
<image :src="dishes.dish_src" class="dishes-pic"></image>
</view>
<!-- 菜品右侧信息区域 -->
<view class="dishes-item-right">
<!-- 菜品标题 -->
<view class="dishes-name">{{dishes.dish_name}}</view>
<view class="dishes-info-box">
</view>
</view>
</view>
</template>
<script>
export default {
props: {
dishes: {
type: Object,
default:{},
}
},
data: {
}
}
</script>
<style lang="scss">
.dishes-item {
display: flex;
padding: 10px 5px;
border-bottom: 1px solid #f0f0f0;
.dishes-item-left {
margin-right: 5px;
.dishes-pic {
width: 100px;
height: 100px;
display: block;
}
}
.dishes-item-right {
display: flex;
flex-direction: column;
justify-content: space-between;
.dishes-name {
font-size: 13px;
}
.dishes-price {
font-size: 16px;
color: #c00000;
}
}
}
</style>

@ -0,0 +1,96 @@
<template>
<view class="login-container">
<uni-icons type="contact-filled" size="100" color="#afafaf"></uni-icons>
<button type="primary" class="btn-login" open-type="getUserInfo" @getuserinfo="GETuserinfo"></button>
<text class="login-text">登陆后尽享更多权益</text>
</view>
</template>
<script>
import {
mapMutations
} from 'vuex'
export default {
name: "my-login",
data() {
return {};
},
methods: {
...mapMutations('m_user', ['updateUserInfo', 'updatetoken']),
//
GETuserinfo(e) {
//console.log(e)
if (e.detail.errMsg === 'getUserInfo:fail auth deny')
return uni.$showMsg('您取消了登录授权')
//console.log(e.detail.userInfo)
this.updateUserInfo(e.detail.userInfo)
this.getToken(e.detail)
},
async getToken(info) {
//code
const [err, res] = await wx.login().catch(err => err)
//const [err,res] = await uni.login(). catch(err => err)
if (err || res.errMsg !== 'login:ok')
return uni.$showMsg('登陆失败 ')
//
const query = {
code: res.code,
encryptedData: info.encryptedData,
iv: info.iv,
rawData: info.rawData,
signature: info.signature
}
const {data: loginResult} = await this.codeRequest(res.code)
// const {data: loginResult} = await uni.$http.post('https://www.uinav.com/api/public/v1/users/wxlogin' , query)
console.log(loginResult)
},
codeRequest(cod) {
let that = this
uniCloud.callFunction({
name: 'wxlogin',
data: {
code:cod
},
success:function(res){
return res
}
})
}
}
}
</script>
<style lang="scss">
.login-container {
height: 750rpx;
background-color: #f8f8f8;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
&::after {
content: '';
display: block;
width: 100%;
height: 40px;
background-color: #f8f8f8;
position: absolute;
bottom: 0;
left: 0;
border-radius: 100%;
transform: translateY(50%);
}
.btn-login {
width: 90%;
border-radius: 100px;
margin: 15px 0;
background-color: #C00000;
}
}
</style>

@ -0,0 +1,64 @@
<template>
<view class="my-search-container" @click="searchBoxHandler">
<!-- 使用 view 组件模拟 input 输入框的样式 -->
<view class="my-search-box">
<uni-icons type="search" size="17"></uni-icons>
<text class="placeholder">搜索</text>
</view>
</view>
</template>
<script>
export default {
name:"my-search",
props: {
//
bgcolor: {
type: String,
default: '#C00000'
},
//
radius: {
type: Number,
// px
default: 18
}
},
data() {
return {
};
},
methods: {
searchBoxHandler(){
// @click click
this.$emit('click')
}
}
}
</script>
<style lang="scss">
.my-search-container {
background-color: #c00000;
height: 50px;
padding: 0 10px;
display: flex;
align-items: center;
}
.my-search-box {
height: 36px;
background-color: #ffffff;
border-radius: 15px;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
.placeholder {
font-size: 15px;
margin-left: 5px;
}
}
</style>

@ -1,18 +1,20 @@
{
"pages": [ //pageshttps://uniapp.dcloud.io/collocation/pages
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "uni-app"
"path" : "pages/home/home",
"style" :
{
"navigationBarTitleText": "菜品圈",
"enablePullDownRefresh": false
}
}
,{
"path" : "pages/home/home",
"style" :
{
"navigationBarTitleText": "",
"enablePullDownRefresh": false
}
},{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "uni-app"
}
}
,{
@ -42,12 +44,52 @@
}
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "uni-app",
"navigationBarTitleText": "菜品圈",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8"
},
"uniIdRouter": {}
"subpackages": [{
"root":"subpkg",
"pages": [{
"path" : "search/search",
"style" :{}
}, {
"path" : "dishDetail/dishDetail",
"style" :{}
}]
}],
"tabBar": {
"color": "#a9a9a9",
"selectedColor": "#000000",
"list": [{
"pagePath": "pages/home/home",
"text": "首页",
"iconPath": "static/static/images/home/home1.png",
"selectedIconPath": "static/static/images/home/home2.png"
},
{
"pagePath": "pages/search/search",
"text": "搜索",
"iconPath": "",
"selectedIconPath": ""
},
{
"pagePath": "pages/ranking/ranking",
"text": "排行榜",
"iconPath": "",
"selectedIconPath": ""
},
{
"pagePath": "pages/my/my",
"text": "个人中心",
"iconPath": "static/static/images/home/my.png",
"selectedIconPath": ""
}
]
}
}

@ -1,19 +1,145 @@
<template>
<view>
<view class="search-box">
<my-search @click="gotoSearch"></my-search>
</view>
<swiper :class="swiper" :indicator-dots="true " :autoplay="true" :interval="3000" :duration="1000"
:circular="true">
<swiper-item v-for="(item,i) in swiperList" :key="i">
<image class="image" :src="item.image_src"></image>
</swiper-item>
</swiper>
<view class="dishes-list">
<view v-for="(dishes, i) in floorList" :key="i" @click="gotoDetail(dishes)">
<view class=" dishes-item">
<!-- 菜品左侧图片区域 -->
<view class="dishes-item-left">
<image :src="dishes.dish_src" class="dishes-pic"></image>
</view>
<!-- 菜品右侧信息区域 -->
<view class="dishes-item-right">
<!-- 菜品标题 -->
<view class="dishes-name">{{dishes.dish_name}}</view>
<view class="dishes-info-box">
{{dishes.location}}
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import {
mapState,
mapActions
} from "vuex";
export default {
name: "swiper-index",
data() {
return {
swiperList: [],
floorList: []
};
},
onLoad() {
this.getSwiperList()
this.getFloorList()
const sysInfo = uni.getSystemInfoSync()
// = - navigationBar - tabBar - search
this.wh = sysInfo.windowHeight - 50
},
methods: {
gotoSearch() {
uni.navigateTo({
url: '/subpkg/search/search'
})
},
gotoDetail(dishes) {
uni.navigateTo({
url:'/subpkg/dishDetail/dishDetail?_id='+encodeURIComponent(JSON.stringify(dishes._id))
})
},
getSwiperList() {
let that = this
uniCloud.callFunction({
name: "getSwiperImage",
data: "",
success: function(res) {
that.swiperList = res.result
console.log(res)
}
})
},
getFloorList() {
let that = this
uniCloud.callFunction({
name: "getDishes",
data: {
api: "getFloorList"
},
success: function(res) {
that.floorList = res.result.data
console.log(res)
}
})
},
}
}
</script>
<style lang="scss">
.swiper {
height: 50rpx;
z-index: -1;
}
.image {
width: 100%;
height: 100%;
}
.dishes-item {
display: flex;
padding: 10px 5px;
border-bottom: 1px solid #f0f0f0;
}
.dishes-item-left {
margin-right: 5px;
}
.dishes-pic {
width: 100px;
height: 100px;
display: block;
}
.dishes-item-right {
display: flex;
flex-direction: column;
justify-content: space-between;
}
.dishes-name {
font-size: 13px;
}
.dishes-price {
font-size: 16px;
color: #c00000;
}
.search-box {
//
position: sticky;
//
top: 0;
//
z-index: 999;
}
</style>

@ -1,19 +1,28 @@
<template>
<view>
<view class="my-container">
<my-login v-if="!token"></my-login>
</view>
<my-userinfo v-else></my-userinfo>
</view>
</template>
<script>
export default {
data() {
return {
import {mapState} from "vuex"
export default {
data() {
return {
};
}
}
};
},
computed: {
...mapState('m_user',['token'])
}
}
</script>
<style lang="scss">
page,
.my-container {
height: 100%;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 765 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 759 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 715 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 672 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 765 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 435 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 596 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 589 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

@ -0,0 +1,122 @@
<template>
<view>
<view class="image_view">
<image class='dish_image' :src="dish_image_src"></image>
</view>
<view>
<text>{{"菜品名称:" + dish_name + '\n'}}</text>
<text>{{"菜品位置:" + dish_location + dish_window_name + '\n'}}</text>
</view>
<view class="evaluete">
<hb-comment ref="hbComment" @add="add" @del="del" @like="like" :deleteTip="'确认删除?'"
:cmData="commentData" v-if="commentData"></hb-comment>
</view>
<view>
<text>{{"评分:\n"}}</text>
<uni-rate v-model="value" @change="onChange(e.value)" allow-half="true" size="75" />
</view>
</view>
</template>
<script>
export default {
data() {
return {
dish_image_src: '',
dish_name: '',
dish_label: [],
dish_location: '',
dish_window_name: '',
value: 4,
comentData:[],
dish_id:''
};
},
onLoad(option) {
//this.getLabels()
this.dish_id = JSON.parse(decodeURIComponent(option._id))
this.getImage()
console.log(this.dish_id )
},
methods: {
getImage() {
let that = this
uniCloud.callFunction({
name: 'getDishes',
data: {
api: 'getByID',
id: this.dish_id
},
success: function(res) {
that.dish_image_src = res.result.data[0].dish_src
that.dish_name = res.result.data[0].dish_name
that.dish_location =res.result.data[0].location
that.dish_window_name =res.result.data[0].window_name
console.log(res)
}
})
},
getLabels() {
let that = this
uniCloud.callFunction({
name: 'getLabels',
data: {
id: '650e8610a09a9bd68ba734ed'
},
success: function(res) {
that.dish_label = res.result.data[0].labelList
console.log(res)
}
})
},
onChange() {
this.value = e.value
},
updateScore() {
uniCloud.callFunction({
name: 'scoreUpdate',
data: {
//
}
})
},
add(commentReq){
console.log(commentReq)
},
del(commentid){
console.log(commentid)
},
like(commentid){
console.log(commentid)
}
}
}
</script>
<style lang="scss">
.image_view {
height: 400rpx;
display: flex;
justify-content: center;
padding-left: 10rpx;
padding-right: 10rpx;
}
.dish_image {
display: flex;
height: 400rpx;
width: 600rpx;
}
.evaluete{
}
</style>

@ -0,0 +1,69 @@
<template>
<view>
<d-search-log @onSearchNameApi="onSearchNameApi"></d-search-log>
<view>
<view class="serach-list" v-for="(data,i) in dishList" :key="i">
<view class="dish-item">
<image class="dish-image" :src="data[i].dish_src"></image>
</view>
</view>
</view>
</view>
</template>
<script>
import Vue from 'vue'
export default {
data() {
return {
dishList: [],
};
},
onLoad() {
},
methods: {
async onSearchNameApi(e) {
let that = this
let data = []
uniCloud.callFunction({
name: 'getDishes',
data: {
api: 'getByName',
dish_name: e
},
success: function(res) {
/*that.dishList.splice(0)
data = [res.result.data]
for (let i = 0; i < data.length; i++) {
Vue.set(that.dishList, i, data[i])
}*/
console.log(res)
}
})
}
}
}
</script>
<style lang="scss">
.searchbox {
width: 500;
display: flex;
}
.search-list {
display: flex;
}
.dish_image {
width: 100rpx;
height: 70rpx;
}
</style>

@ -0,0 +1,23 @@
const {
createApi
} = require('./shared/index')
let reportDataReceiver, dataStatCron
module.exports = {
//uni统计数据上报数据接收器初始化
initReceiver: (options = {}) => {
if(!reportDataReceiver) {
reportDataReceiver = require('./stat/receiver')
}
options.clientType = options.clientType || __ctx__.PLATFORM
return createApi(reportDataReceiver, options)
},
//uni统计数据统计模块初始化
initStat: (options = {}) => {
if(!dataStatCron) {
dataStatCron = require('./stat/stat')
}
options.clientType = options.clientType || __ctx__.PLATFORM
return createApi(dataStatCron, options)
}
}

@ -0,0 +1,5 @@
{
"name": "uni-stat",
"version": "1.0.0",
"lockfileVersion": 1
}

@ -0,0 +1,15 @@
{
"name": "uni-stat",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"uni-config-center": "file:../../../../uni_modules/uni-config-center/uniCloud/cloudfunctions/common/uni-config-center"
}
}

@ -0,0 +1,82 @@
const {
isFn,
isPlainObject
} = require('./utils')
/**
* 实例参数处理注意不进行递归处理
* @param {Object} params 初始参数
* @param {Object} rule 规则集
* @returns {Object} 处理后的参数
*/
function parseParams (params = {}, rule) {
if (!rule || !params) {
return params
}
const internalKeys = ['_pre', '_purify', '_post']
// 转换之前的处理
if (rule._pre) {
params = rule._pre(params)
}
// 净化参数
let purify = { shouldDelete: new Set([]) }
if (rule._purify) {
const _purify = rule._purify
for (const purifyKey in _purify) {
_purify[purifyKey] = new Set(_purify[purifyKey])
}
purify = Object.assign(purify, _purify)
}
if (isPlainObject(rule)) {
for (const key in rule) {
const parser = rule[key]
if (isFn(parser) && internalKeys.indexOf(key) === -1) {
params[key] = parser(params)
} else if (typeof parser === 'string' && internalKeys.indexOf(key) === -1) {
// 直接转换属性名称的删除旧属性名
params[key] = params[parser]
purify.shouldDelete.add(parser)
}
}
} else if (isFn(rule)) {
params = rule(params)
}
if (purify.shouldDelete) {
for (const item of purify.shouldDelete) {
delete params[item]
}
}
// 转换之后的处理
if (rule._post) {
params = rule._post(params)
}
return params
}
/**
* 返回一个提供应用上下文的应用实例应用实例挂载的整个组件树共享同一个上下文
* @param {class} ApiClass 实例类
* @param {Object} options 参数
* @returns {Object} 实例类对象
*/
module.exports = function createApi (ApiClass, options) {
const apiInstance = new ApiClass(options)
return new Proxy(apiInstance, {
get: function (obj, prop) {
if (typeof obj[prop] === 'function' && prop.indexOf('_') !== 0 && obj._protocols && obj._protocols[prop]) {
const protocol = obj._protocols[prop]
return async function (params) {
params = parseParams(params, protocol.args)
let result = await obj[prop](params)
result = parseParams(result, protocol.returnValue)
return result
}
} else {
return obj[prop]
}
}
})
}

@ -0,0 +1,19 @@
/**
* @class UniCloudError 错误处理模块
*/
module.exports = class UniCloudError extends Error {
constructor (options) {
super(options.message)
this.errMsg = options.message || ''
Object.defineProperties(this, {
message: {
get () {
return `errCode: ${options.code || ''} | errMsg: ` + this.errMsg
},
set (msg) {
this.errMsg = msg
}
}
})
}
}

@ -0,0 +1,6 @@
module.exports = {
UniCloudError: require('./error'),
createApi: require('./create-api'),
... require('./utils')
}

@ -0,0 +1,197 @@
const _toString = Object.prototype.toString
const hasOwnProperty = Object.prototype.hasOwnProperty
/**
* 检查对象是否包含某个属性
* @param {Object} obj 对象
* @param {String} key 属性键值
*/
function hasOwn(obj, key) {
return hasOwnProperty.call(obj, key)
}
/**
* 参数是否为JavaScript的简单对象
* @param {Object} obj
* @returns {Boolean} true|false
*/
function isPlainObject(obj) {
return _toString.call(obj) === '[object Object]'
}
/**
* 是否为函数
* @param {String} fn 函数名
*/
function isFn(fn) {
return typeof fn === 'function'
}
/**
* 深度克隆对象
* @param {Object} obj
*/
function deepClone(obj) {
return JSON.parse(JSON.stringify(obj))
}
/**
* 解析客户端上报的参数
* @param {String} primitiveParams 原始参数
* @param {Object} context 附带的上下文
*/
function parseUrlParams(primitiveParams, context) {
if (!primitiveParams) {
return primitiveParams
}
let params = {}
if(typeof primitiveParams === 'string') {
params = primitiveParams.split('&').reduce((res, cur) => {
const arr = cur.split('=')
return Object.assign({
[arr[0]]: arr[1]
}, res)
}, {})
} else {
//转换参数类型--兼容性
for(let key in primitiveParams) {
if(typeof primitiveParams[key] === 'number') {
params[key] = primitiveParams[key] + ''
} else {
params[key] = primitiveParams[key]
}
}
}
//原以下数据要从客户端上报现调整为如果以下参数客户端未上报则通过请求附带的context参数中获取
const convertParams = {
//appid
ak: 'appId',
//当前登录用户编号
uid: 'uid',
//设备编号
did: 'deviceId',
//uni-app 运行平台,与条件编译平台相同。
up: 'uniPlatform',
//操作系统名称
p: 'osName',
//因为p参数可能会被前端覆盖掉所以这里单独拿出来一个osName
on: 'osName',
//客户端ip
ip: 'clientIP',
//客户端的UA
ua: 'userAgent',
//当前服务空间编号
spid: 'spaceId',
//当前服务空间提供商
sppd: 'provider',
//应用版本号
v: 'appVersion',
//rom 名称
rn: 'romName',
//rom 版本
rv: 'romVersion',
//操作系统版本
sv: 'osVersion',
//操作系统语言
lang: 'osLanguage',
//操作系统主题
ot: 'osTheme',
//设备类型
dtp: 'deviceType',
//设备品牌
brand: 'deviceBrand',
//设备型号
md: 'deviceModel',
//设备像素比
pr: 'devicePixelRatio',
//可使用窗口宽度
ww: 'windowWidth',
//可使用窗口高度
wh: 'windowHeight',
//屏幕宽度
sw: 'screenWidth',
//屏幕高度
sh: 'screenHeight',
}
context = context ? context : {}
for (let key in convertParams) {
if (!params[key] && context[convertParams[key]]) {
params[key] = context[convertParams[key]]
}
}
return params
}
/**
* 解析url
* @param {String} url
*/
function parseUrl(url) {
if (typeof url !== "string" || !url) {
return false
}
const urlInfo = url.split('?')
baseurl = urlInfo[0]
if (baseurl !== '/' && baseurl.indexOf('/') === 0) {
baseurl = baseurl.substr(1)
}
return {
path: baseurl,
query: urlInfo[1] ? decodeURI(urlInfo[1]) : ''
}
}
//加载配置中心-uni-config-center
let createConfig
try {
createConfig = require('uni-config-center')
} catch (e) {}
/**
* 获取配置文件信息
* @param {String} file 配置文件名称
* @param {String} key 配置参数键值
*/
function getConfig(file, key) {
if (!file) {
return false
}
const uniConfig = createConfig && createConfig({
pluginId: 'uni-stat'
})
if (!uniConfig || !uniConfig.hasFile(file + '.json')) {
console.error('Not found the config file')
return false
}
const config = uniConfig.requireFile(file)
return key ? config[key] : config
}
/**
* 休眠
* @param {Object} ms 休眠时间毫秒
*/
function sleep(ms) {
return new Promise(resolve => setTimeout(() => resolve(), ms))
}
module.exports = {
hasOwn,
isPlainObject,
isFn,
deepClone,
parseUrlParams,
parseUrl,
getConfig,
sleep
}

@ -0,0 +1,371 @@
/**
* @class DateTime
* @description 日期处理模块
*/
module.exports = class DateTime {
constructor() {
//默认日期展示格式
this.defaultDateFormat = 'Y-m-d H:i:s'
//默认时区
this.defaultTimezone = 8
this.setTimeZone(this.defaultTimezone)
}
/**
* 设置时区
* @param {Number} timezone 时区
*/
setTimeZone(timezone) {
if (timezone) {
this.timezone = parseInt(timezone)
}
return this
}
/**
* 获取 Date对象
* @param {Date|Time} time
*/
getDateObj(time) {
return time ? new Date(time) : new Date()
}
/**
* 获取毫秒/秒级时间戳
* @param {DateTime} datetime 日期 '2022-04-21 00:00:00'
* @param {Boolean} showSenconds 是否显示为秒级时间戳
*/
getTime(datetime, showSenconds) {
let time = this.getDateObj(datetime).getTime()
if (showSenconds) {
time = Math.trunc(time / 1000)
}
return time
}
/**
* 获取日期
* @param {String} dateFormat 日期格式
* @param {Time} time 时间戳
*/
getDate(dateFormat, time) {
return this.dateFormat(dateFormat, time)
}
/**
* 获取日期在不同时区下的时间戳
* @param {Date|Time}} time 日期或时间戳
* @param {Object} timezone 时区
*/
getTimeByTimeZone(time, timezone) {
this.setTimeZone(timezone)
const thisDate = time ? new Date(time) : new Date()
const localTime = thisDate.getTime()
const offset = thisDate.getTimezoneOffset()
const utc = offset * 60000 + localTime
return utc + (3600000 * this.timezone)
}
/**
* 获取时间信息
* @param {Time} time 时间戳
* @param {Boolean} full 是否完整展示, 为true时小于10的位会自动补0
*/
getTimeInfo(time, full = true) {
time = this.getTimeByTimeZone(time)
const date = this.getDateObj(time)
const retData = {
nYear: date.getFullYear(),
nMonth: date.getMonth() + 1,
nWeek: date.getDay() || 7,
nDay: date.getDate(),
nHour: date.getHours(),
nMinutes: date.getMinutes(),
nSeconds: date.getSeconds()
}
if (full) {
for (const k in retData) {
if (retData[k] < 10) {
retData[k] = '0' + retData[k]
}
}
}
return retData
}
/**
* 时间格式转换
* @param {String} format 展示格式如:Y-m-d H:i:s
* @param {Time} time 时间戳
*/
dateFormat(format, time) {
const timeInfo = this.getTimeInfo(time)
format = format || this.defaultDateFormat
let date = format
if (format.indexOf('Y') > -1) {
date = date.replace(/Y/, timeInfo.nYear)
}
if (format.indexOf('m') > -1) {
date = date.replace(/m/, timeInfo.nMonth)
}
if (format.indexOf('d') > -1) {
date = date.replace(/d/, timeInfo.nDay)
}
if (format.indexOf('H') > -1) {
date = date.replace(/H/, timeInfo.nHour)
}
if (format.indexOf('i') > -1) {
date = date.replace(/i/, timeInfo.nMinutes)
}
if (format.indexOf('s') > -1) {
date = date.replace(/s/, timeInfo.nSeconds)
}
return date
}
/**
* 获取utc格式时间
* @param {Date|Time} datetime 日期或时间戳
*/
getUTC(datetime) {
return this.getDateObj(datetime).toUTCString()
}
/**
* 获取ISO 格式时间
* @param {Date|Time} datetime 日期或时间戳
*/
getISO(datetime) {
return this.getDateObj(datetime).toISOString()
}
/**
* 获取两时间相差天数
* @param {Time} time1 时间戳
* @param {Time} time2 时间戳
*/
getDiffDays(time1, time2) {
if (!time1) {
return false
}
time2 = time2 ? time2 : this.getTime()
let diffTime = time2 - time1
if (diffTime < 0) {
diffTime = Math.abs(diffTime)
}
return Math.ceil(diffTime / 86400000)
}
/**
* 字符串转时间戳
* @param {Object} str 字符串类型的时间戳
*/
strToTime(str) {
if (Array.from(str).length === 10) {
str += '000'
}
return this.getTime(parseInt(str))
}
/**
* 根据设置的天数获取指定日期N天后的时间戳
* @param {Number} days 天数
* @param {Date|Time} time 指定的日期或时间戳
* @param {Boolean} getAll 是否获取完整时间戳 false 时返回指定日期初始时间戳当天00:00:00的时间戳
*/
getTimeBySetDays(days, time, getAll = false) {
const date = this.getDateObj(time)
date.setDate(date.getDate() + days)
let startTime = date.getTime()
if (!getAll) {
const realdate = this.getDate('Y-m-d 00:00:00', startTime)
startTime = this.getTimeByDateAndTimezone(realdate)
}
return startTime
}
/**
* 根据设置的小时数获取指定日期N小时后的时间戳
* @param {Number} hours 小时数
* @param {Date|Time} time 指定的日期或时间戳
* @param {Boolean} getAll 是否获取完整时间戳 false 时返回指定时间初始时间戳该小时00:00的时间戳
*/
getTimeBySetHours(hours, time, getAll = false) {
const date = this.getDateObj(time)
date.setHours(date.getHours() + hours)
let startTime = date.getTime()
if (!getAll) {
const realdate = this.getDate('Y-m-d H:00:00', startTime)
startTime = this.getTimeByDateAndTimezone(realdate)
}
return startTime
}
/**
* 根据设置的周数获取指定日期N周后的时间戳
* @param {Number} weeks 周数
* @param {Date|Time} time 指定的日期或时间戳
* @param {Boolean} getAll 是否获取完整时间戳 false 时返回指定日期初始时间戳当天00:00:00的时间戳
*/
getTimeBySetWeek(weeks, time, getAll = false) {
const date = this.getDateObj(time)
const dateInfo = this.getTimeInfo(time)
const day = dateInfo.nWeek
const offsetDays = 1 - day
weeks = weeks * 7 + offsetDays
date.setDate(date.getDate() + weeks)
let startTime = date.getTime()
if (!getAll) {
const realdate = this.getDate('Y-m-d 00:00:00', startTime)
startTime = this.getTimeByDateAndTimezone(realdate)
}
return startTime
}
/**
* 根据设置的月数获取指定日期N月后的时间戳
* @param {Number} monthes 月数
* @param {Date|Time} time 指定的日期或时间戳
* @param {Boolean} getAll 是否获取完整时间戳 false 时返回指定日期初始时间戳当天00:00:00的时间戳
*/
getTimeBySetMonth(monthes, time, getAll = false) {
const date = this.getDateObj(time)
date.setMonth(date.getMonth() + monthes)
let startTime = date.getTime()
if (!getAll) {
const realdate = this.getDate('Y-m-01 00:00:00', startTime)
startTime = this.getTimeByDateAndTimezone(realdate)
}
return startTime
}
/**
* 根据设置的季度数获取指定日期N个季度后的时间戳
* @param {Number} quarter 季度
* @param {Date|Time} time 指定的日期或时间戳
* @param {Boolean} getAll 是否获取完整时间戳 false 时返回指定日期初始时间戳当天00:00:00的时间戳
*/
getTimeBySetQuarter(quarter, time, getAll = false) {
const date = this.getDateObj(time)
const dateInfo = this.getTimeInfo(time)
date.setMonth(date.getMonth() + quarter * 3)
const month = date.getMonth() + 1;
let quarterN;
let mm;
if ([1,2,3].indexOf(month) > -1) {
// 第1季度
mm = "01";
} else if ([4,5,6].indexOf(month) > -1) {
// 第2季度
mm = "04";
} else if ([7,8,9].indexOf(month) > -1) {
// 第3季度
mm = "07";
} else if ([10,11,12].indexOf(month) > -1) {
// 第4季度
mm = "10";
}
let yyyy = date.getFullYear();
let startTime = date.getTime()
if (!getAll) {
const realdate = this.getDate(`${yyyy}-${mm}-01 00:00:00`, startTime)
startTime = this.getTimeByDateAndTimezone(realdate)
}
return startTime
}
/**
* 根据设置的年数获取指定日期N年后的时间戳
* @param {Number} year 月数
* @param {Date|Time} time 指定的日期或时间戳
* @param {Boolean} getAll 是否获取完整时间戳 false 时返回指定日期初始时间戳当天00:00:00的时间戳
*/
getTimeBySetYear(year, time, getAll = false) {
const date = this.getDateObj(time)
date.setFullYear(date.getFullYear() + year)
let startTime = date.getTime()
if (!getAll) {
const realdate = this.getDate('Y-01-01 00:00:00', startTime)
startTime = this.getTimeByDateAndTimezone(realdate)
}
return startTime
}
/**
* 根据时区获取指定时间的偏移时间
* @param {Date|Time} 指定的日期或时间戳
* @param {Number} timezone 时区
*/
getTimeByDateAndTimezone(date, timezone) {
if (!timezone) {
timezone = this.timezone
}
const thisDate = this.getDateObj(date)
const thisTime = thisDate.getTime()
const offset = thisDate.getTimezoneOffset()
const offsetTime = offset * 60000 + timezone * 3600000
return thisTime - offsetTime
}
/**
* 根据指定的时间类型获取时间范围
* @param {String} type 时间类型 hour:小时 day: week: month
* @param {Number} offset 时间的偏移量
* @param {Date|Time} thistime 指定的日期或时间戳
* @param {Boolean} getAll 是否获取完整时间戳 false 时返回指定日期初始时间戳当天00:00:00的时间戳
*/
getTimeDimensionByType(type, offset = 0, thistime, getAll = false) {
let startTime = 0
let endTime = 0
switch (type) {
case 'hour': {
startTime = this.getTimeBySetHours(offset, thistime, getAll)
endTime = getAll ? startTime : startTime + 3599999
break
}
case 'day': {
startTime = this.getTimeBySetDays(offset, thistime, getAll)
endTime = getAll ? startTime : startTime + 86399999
break
}
case 'week': {
startTime = this.getTimeBySetWeek(offset, thistime, getAll)
endTime = getAll ? startTime + 86400000 * 6 : startTime + 86400000 * 6 + 86399999
break
}
case 'month': {
startTime = this.getTimeBySetMonth(offset, thistime, getAll)
const date = this.getDateObj(this.getDate('Y-m-d H:i:s', startTime))
const nextMonthFirstDayTime = new Date(date.getFullYear(), date.getMonth() + 1, 1).getTime()
endTime = getAll ? nextMonthFirstDayTime - 86400000 : this.getTimeByDateAndTimezone(
nextMonthFirstDayTime) - 1
break
}
case 'quarter': {
startTime = this.getTimeBySetQuarter(offset, thistime, getAll)
const date = this.getDateObj(this.getDate('Y-m-d H:i:s', startTime))
const nextMonthFirstDayTime = new Date(date.getFullYear(), date.getMonth() + 3, 1).getTime()
endTime = getAll ? nextMonthFirstDayTime - 86400000 : this.getTimeByDateAndTimezone(
nextMonthFirstDayTime) - 1
break
}
case 'year': {
startTime = this.getTimeBySetYear(offset, thistime, getAll)
const date = this.getDateObj(this.getDate('Y-m-d H:i:s', startTime))
const nextFirstDayTime = new Date(date.getFullYear() + 1, 0, 1).getTime()
endTime = getAll ? nextFirstDayTime - 86400000 : this.getTimeByDateAndTimezone(
nextFirstDayTime) - 1
break
}
}
return {
startTime,
endTime
}
}
}

@ -0,0 +1,4 @@
module.exports = {
DateTime: require('./date'),
UniCrypto: require('./uni-crypto')
}

@ -0,0 +1,98 @@
/**
* @class UniCrypto 数据加密服务
* @function init 初始化函数
* @function showConfig 返回配置信息函数
* @function getCrypto 返回原始crypto对象函数
* @function aesEncode AES加密函数
* @function aesDecode AES解密函数
* @function md5 MD5加密函数
*/
const crypto = require('crypto')
module.exports = class UniCrypto {
constructor(config) {
this.init(config)
}
/**
* 配置初始化函数
* @param {Object} config
*/
init(config) {
this.config = {
//AES加密默认参数
AES: {
mod: 'aes-128-cbc',
pasword: 'UniStat!010',
iv: 'UniStativ',
charset: 'utf8',
encodeReturnType: 'base64'
},
//MD5加密默认参数
MD5: {
encodeReturnType: 'hex'
},
...config || {}
}
return this
}
/**
* 返回配置信息函数
*/
showConfig() {
return this.config
}
/**
* 返回原始crypto对象函数
*/
getCrypto() {
return crypto
}
/**
* AES加密函数
* @param {String} data 加密数据明文
* @param {String} encodeReturnType 返回加密数据类型base64
* @param {String} key 密钥
* @param {String} iv 偏移量
* @param {String} mod 模式
* @param {String} charset 编码
*/
aesEncode(data, encodeReturnType, key, iv, mod, charset) {
const cipher = crypto.createCipheriv(mod || this.config.AES.mod, key || this.config.AES.pasword, iv ||
this.config.AES.iv)
let crypted = cipher.update(data, charset || this.config.AES.charset, 'binary')
crypted += cipher.final('binary')
crypted = Buffer.from(crypted, 'binary').toString(encodeReturnType || this.config.AES.encodeReturnType)
return crypted
}
/**
* AES解密函数
* @param {Object} crypted 加密数据密文
* @param {Object} encodeReturnType 返回加密数据类型base64
* @param {Object} key 密钥
* @param {Object} iv 偏移量
* @param {Object} mod 模式
* @param {Object} charset 编码
*/
aesDecode(crypted, encodeReturnType, key, iv, mod, charset) {
crypted = Buffer.from(crypted, encodeReturnType || this.config.AES.encodeReturnType).toString('binary')
const decipher = crypto.createDecipheriv(mod || this.config.AES.mod, key || this.config.AES.pasword,
iv || this.config.AES.iv)
let decoded = decipher.update(crypted, 'binary', charset || this.config.AES.charset)
decoded += decipher.final(charset || this.config.AES.charset)
return decoded
}
/**
* @param {Object} str 加密字符串
* @param {Object} encodeReturnType encodeReturnType 返回加密数据类型hex(转为16进制)
*/
md5(str, encodeReturnType) {
const md5Mod = crypto.createHash('md5')
md5Mod.update(str)
return md5Mod.digest(encodeReturnType || this.config.MD5.encodeReturnType)
}
}

@ -0,0 +1,528 @@
/**
* @class ActiveDevices 活跃设备模型 - 每日跑批合并仅添加本周/本月首次访问的设备
*/
const BaseMod = require('./base')
const Platform = require('./platform')
const Channel = require('./channel')
const Version = require('./version')
const SessionLog = require('./sessionLog')
const {
DateTime,
UniCrypto
} = require('../lib')
module.exports = class ActiveDevices extends BaseMod {
constructor() {
super()
this.tableName = 'active-devices'
this.platforms = []
this.channels = []
this.versions = []
}
/**
* @desc 活跃设备统计 - 为周统计/月统计提供周活/月活数据
* @param {date|time} date
* @param {bool} reset
*/
async stat(date, reset) {
const dateTime = new DateTime()
const dateDimension = dateTime.getTimeDimensionByType('day', -1, date)
this.startTime = dateDimension.startTime
// 查看当前时间段数据是否已存在,防止重复生成
if (!reset) {
const checkRes = await this.getCollection(this.tableName).where({
create_time: {
$gte: dateDimension.startTime,
$lte: dateDimension.endTime
}
}).get()
if (checkRes.data.length > 0) {
console.log('data have exists')
return {
code: 1003,
msg: 'Devices data in this time have already existed'
}
}
} else {
const delRes = await this.delete(this.tableName, {
create_time: {
$gte: dateDimension.startTime,
$lte: dateDimension.endTime
}
})
console.log('Delete old data result:', JSON.stringify(delRes))
}
const sessionLog = new SessionLog()
const statRes = await this.aggregate(sessionLog.tableName, {
project: {
appid: 1,
version: 1,
platform: 1,
channel: 1,
is_first_visit: 1,
create_time: 1,
device_id: 1
},
match: {
create_time: {
$gte: dateDimension.startTime,
$lte: dateDimension.endTime
}
},
group: {
_id: {
appid: '$appid',
version: '$version',
platform: '$platform',
channel: '$channel',
device_id: '$device_id'
},
is_new: {
$max: '$is_first_visit'
},
create_time: {
$min: '$create_time'
}
},
sort: {
create_time: 1
},
getAll: true
})
let res = {
code: 0,
msg: 'success'
}
// if (this.debug) {
// console.log('statRes', JSON.stringify(statRes))
// }
if (statRes.data.length > 0) {
const uniCrypto = new UniCrypto()
// 同应用、平台、渠道、版本的数据合并
const statData = [];
let statKey;
let data
const statOldRes = await this.aggregate(sessionLog.tableName, {
project: {
appid: 1,
version: 1,
platform: 1,
channel: 1,
is_first_visit: 1,
create_time: 1,
old_device_id: 1
},
match: {
create_time: {
$gte: dateDimension.startTime,
$lte: dateDimension.endTime
},
old_device_id: {$exists: true}
},
group: {
_id: {
appid: '$appid',
version: '$version',
platform: '$platform',
channel: '$channel',
old_device_id: '$old_device_id'
},
create_time: {
$min: '$create_time'
}
},
sort: {
create_time: 1
},
getAll: true
})
if (this.debug) {
console.log('statOldRes', JSON.stringify(statOldRes))
}
for (const sti in statRes.data) {
data = statRes.data[sti]
statKey = uniCrypto.md5(data._id.appid + data._id.platform + data._id.version + data._id
.channel)
if (!statData[statKey]) {
statData[statKey] = {
appid: data._id.appid,
platform: data._id.platform,
version: data._id.version,
channel: data._id.channel,
device_ids: [],
old_device_ids: [],
info: [],
old_info: []
}
statData[statKey].device_ids.push(data._id.device_id)
statData[statKey].info[data._id.device_id] = {
is_new: data.is_new,
create_time: data.create_time
}
} else {
statData[statKey].device_ids.push(data._id.device_id)
statData[statKey].info[data._id.device_id] = {
is_new: data.is_new,
create_time: data.create_time
}
}
}
if(statOldRes.data.length) {
const oldDeviceIds = []
for(const osti in statOldRes.data) {
if(!statOldRes.data[osti]._id.old_device_id) {
continue
}
oldDeviceIds.push(statOldRes.data[osti]._id.old_device_id)
}
if(oldDeviceIds.length) {
const statOldDidRes = await this.aggregate(sessionLog.tableName, {
project: {
appid: 1,
version: 1,
platform: 1,
channel: 1,
is_first_visit: 1,
create_time: 1,
device_id: 1
},
match: {
create_time: {
$gte: dateDimension.startTime,
$lte: dateDimension.endTime
},
device_id: {$in: oldDeviceIds}
},
group: {
_id: {
appid: '$appid',
version: '$version',
platform: '$platform',
channel: '$channel',
old_device_id: '$device_id'
},
create_time: {
$min: '$create_time'
}
},
sort: {
create_time: 1
},
getAll: true
})
if(statOldDidRes.data.length){
for(const osti in statOldDidRes.data) {
data = statOldDidRes.data[osti]
statKey = uniCrypto.md5(data._id.appid + data._id.platform + data._id.version + data._id
.channel)
if(!data._id.old_device_id) {
continue
}
if (!statData[statKey]) {
statData[statKey] = {
appid: data._id.appid,
platform: data._id.platform,
version: data._id.version,
channel: data._id.channel,
device_ids: [],
old_device_ids: [],
old_info: []
}
statData[statKey].old_device_ids.push(data._id.old_device_id)
} else {
statData[statKey].old_device_ids.push(data._id.old_device_id)
}
if(!statData[statKey].old_info[data._id.old_device_id]) {
statData[statKey].old_info[data._id.old_device_id] = {
create_time: data.create_time
}
}
}
}
}
}
this.fillData = []
for (const sk in statData) {
await this.getFillData(statData[sk])
}
if (this.fillData.length > 0) {
res = await this.batchInsert(this.tableName, this.fillData)
}
}
return res
}
/**
* 获取填充数据
* @param {Object} data
*/
async getFillData(data) {
// 平台信息
let platformInfo = null
if (this.platforms && this.platforms[data.platform]) {
platformInfo = this.platforms[data.platform]
} else {
const platform = new Platform()
platformInfo = await platform.getPlatformAndCreate(data.platform, null)
if (!platformInfo || platformInfo.length === 0) {
platformInfo._id = ''
}
this.platforms[data.platform] = platformInfo
if (this.debug) {
console.log('platformInfo', JSON.stringify(platformInfo))
}
}
// 渠道信息
let channelInfo = null
const channelKey = data.appid + '_' + platformInfo._id + '_' + data.channel
if (this.channels && this.channels[channelKey]) {
channelInfo = this.channels[channelKey]
} else {
const channel = new Channel()
channelInfo = await channel.getChannelAndCreate(data.appid, platformInfo._id, data.channel)
if (!channelInfo || channelInfo.length === 0) {
channelInfo._id = ''
}
this.channels[channelKey] = channelInfo
if (this.debug) {
console.log('channelInfo', JSON.stringify(channelInfo))
}
}
// 版本信息
let versionInfo = null
const versionKey = data.appid + '_' + data.platform + '_' + data.version
if (this.versions && this.versions[versionKey]) {
versionInfo = this.versions[versionKey]
} else {
const version = new Version()
versionInfo = await version.getVersionAndCreate(data.appid, data.platform, data.version)
if (!versionInfo || versionInfo.length === 0) {
versionInfo._id = ''
}
this.versions[versionKey] = versionInfo
if (this.debug) {
console.log('versionInfo', JSON.stringify(versionInfo))
}
}
const datetime = new DateTime()
const dateDimension = datetime.getTimeDimensionByType('week', 0, this.startTime)
const dateMonthDimension = datetime.getTimeDimensionByType('month', 0, this.startTime)
if(data.device_ids) {
// 取出本周已经存储的device_id
const weekHaveDeviceList = []
const haveWeekList = await this.selectAll(this.tableName, {
appid: data.appid,
version_id: versionInfo._id,
platform_id: platformInfo._id,
channel_id: channelInfo._id,
device_id: {
$in: data.device_ids
},
dimension: 'week',
create_time: {
$gte: dateDimension.startTime,
$lte: dateDimension.endTime
}
}, {
device_id: 1
})
if (haveWeekList.data.length > 0) {
for (const hui in haveWeekList.data) {
weekHaveDeviceList.push(haveWeekList.data[hui].device_id)
}
}
if (this.debug) {
console.log('weekHaveDeviceList', JSON.stringify(weekHaveDeviceList))
}
// 取出本月已经存储的device_id
const monthHaveDeviceList = []
const haveMonthList = await this.selectAll(this.tableName, {
appid: data.appid,
version_id: versionInfo._id,
platform_id: platformInfo._id,
channel_id: channelInfo._id,
device_id: {
$in: data.device_ids
},
dimension: 'month',
create_time: {
$gte: dateMonthDimension.startTime,
$lte: dateMonthDimension.endTime
}
}, {
device_id: 1
})
if (haveMonthList.data.length > 0) {
for (const hui in haveMonthList.data) {
monthHaveDeviceList.push(haveMonthList.data[hui].device_id)
}
}
if (this.debug) {
console.log('monthHaveDeviceList', JSON.stringify(monthHaveDeviceList))
}
//数据填充
for (const ui in data.device_ids) {
//周活跃数据填充
if (!weekHaveDeviceList.includes(data.device_ids[ui])) {
this.fillData.push({
appid: data.appid,
platform_id: platformInfo._id,
channel_id: channelInfo._id,
version_id: versionInfo._id,
is_new: data.info[data.device_ids[ui]].is_new,
device_id: data.device_ids[ui],
dimension: 'week',
create_time: data.info[data.device_ids[ui]].create_time
})
}
//月活跃数据填充
if (!monthHaveDeviceList.includes(data.device_ids[ui])) {
this.fillData.push({
appid: data.appid,
platform_id: platformInfo._id,
channel_id: channelInfo._id,
version_id: versionInfo._id,
is_new: data.info[data.device_ids[ui]].is_new,
device_id: data.device_ids[ui],
dimension: 'month',
create_time: data.info[data.device_ids[ui]].create_time
})
}
}
}
if(data.old_device_ids) {
// 取出本周已经存储的old_device_id
const weekHaveOldDeviceList = []
const haveOldWeekList = await this.selectAll(this.tableName, {
appid: data.appid,
version_id: versionInfo._id,
platform_id: platformInfo._id,
channel_id: channelInfo._id,
device_id: {
$in: data.old_device_ids
},
dimension: 'week-old',
create_time: {
$gte: dateDimension.startTime,
$lte: dateDimension.endTime
}
}, {
device_id: 1
})
if (haveOldWeekList.data.length > 0) {
for (const hui in haveOldWeekList.data) {
weekHaveOldDeviceList.push(haveOldWeekList.data[hui].device_id)
}
}
if (this.debug) {
console.log('weekHaveOldDeviceList', JSON.stringify(weekHaveOldDeviceList))
}
// 取出本月已经存储的old_device_id
const monthHaveOldDeviceList = []
const haveOldMonthList = await this.selectAll(this.tableName, {
appid: data.appid,
version_id: versionInfo._id,
platform_id: platformInfo._id,
channel_id: channelInfo._id,
device_id: {
$in: data.old_device_ids
},
dimension: 'month-old',
create_time: {
$gte: dateMonthDimension.startTime,
$lte: dateMonthDimension.endTime
}
}, {
device_id: 1
})
if (haveOldMonthList.data.length > 0) {
for (const hui in haveOldMonthList.data) {
monthHaveOldDeviceList.push(haveOldMonthList.data[hui].device_id)
}
}
if (this.debug) {
console.log('monthHaveOldDeviceList', JSON.stringify(monthHaveOldDeviceList))
}
//数据填充
for (const ui in data.old_device_ids) {
//周活跃数据填充
if (!weekHaveOldDeviceList.includes(data.old_device_ids[ui])) {
this.fillData.push({
appid: data.appid,
platform_id: platformInfo._id,
channel_id: channelInfo._id,
version_id: versionInfo._id,
is_new: 0,
device_id: data.old_device_ids[ui],
dimension: 'week-old',
create_time: data.old_info[data.old_device_ids[ui]].create_time
})
}
//月活跃数据填充
if (!monthHaveOldDeviceList.includes(data.old_device_ids[ui])) {
this.fillData.push({
appid: data.appid,
platform_id: platformInfo._id,
channel_id: channelInfo._id,
version_id: versionInfo._id,
is_new: 0,
device_id: data.old_device_ids[ui],
dimension: 'month-old',
create_time: data.old_info[data.old_device_ids[ui]].create_time
})
}
}
}
return true
}
/**
* 日志清理此处日志为临时数据并不需要自定义清理默认为固定值即可
*/
async clean() {
// 清除周数据周留存统计最高需要10周数据多余的为无用数据
const weeks = 10
console.log('Clean device\'s weekly logs - week:', weeks)
const dateTime = new DateTime()
const res = await this.delete(this.tableName, {
dimension: 'week',
create_time: {
$lt: dateTime.getTimeBySetWeek(0 - weeks)
}
})
if (!res.code) {
console.log('Clean device\'s weekly logs - res:', res)
}
// 清除月数据月留存统计最高需要10个月数据多余的为无用数据
const monthes = 10
console.log('Clean device\'s monthly logs - month:', monthes)
const monthRes = await this.delete(this.tableName, {
dimension: 'month',
create_time: {
$lt: dateTime.getTimeBySetMonth(0 - monthes)
}
})
if (!monthRes.code) {
console.log('Clean device\'s monthly logs - res:', res)
}
return monthRes
}
}

@ -0,0 +1,314 @@
/**
* @class ActiveUsers 活跃用户模型 - 每日跑批合并仅添加本周/本月首次访问的用户
*/
const BaseMod = require('./base')
const Platform = require('./platform')
const Channel = require('./channel')
const Version = require('./version')
const UserSessionLog = require('./userSessionLog')
const {
DateTime,
UniCrypto
} = require('../lib')
module.exports = class ActiveUsers extends BaseMod {
constructor() {
super()
this.tableName = 'active-users'
this.platforms = []
this.channels = []
this.versions = []
}
async stat(date, reset) {
const dateTime = new DateTime()
const dateDimension = dateTime.getTimeDimensionByType('day', -1, date)
this.startTime = dateDimension.startTime
// 查看当前时间段数据是否已存在,防止重复生成
if (!reset) {
const checkRes = await this.getCollection(this.tableName).where({
create_time: {
$gte: dateDimension.startTime,
$lte: dateDimension.endTime
}
}).get()
if (checkRes.data.length > 0) {
console.log('data have exists')
return {
code: 1003,
msg: 'Users data in this time have already existed'
}
}
} else {
const delRes = await this.delete(this.tableName, {
create_time: {
$gte: dateDimension.startTime,
$lte: dateDimension.endTime
}
})
console.log('Delete old data result:', JSON.stringify(delRes))
}
const userSessionLog = new UserSessionLog()
const statRes = await this.aggregate(userSessionLog.tableName, {
project: {
appid: 1,
version: 1,
platform: 1,
channel: 1,
create_time: 1,
uid: 1
},
match: {
create_time: {
$gte: dateDimension.startTime,
$lte: dateDimension.endTime
}
},
group: {
_id: {
appid: '$appid',
version: '$version',
platform: '$platform',
channel: '$channel',
uid: '$uid'
},
create_time: {
$min: '$create_time'
}
},
sort: {
create_time: 1
},
getAll: true
})
let res = {
code: 0,
msg: 'success'
}
// if (this.debug) {
// console.log('statRes', JSON.stringify(statRes))
// }
if (statRes.data.length > 0) {
const uniCrypto = new UniCrypto()
// 同应用、平台、渠道、版本的数据合并
const statData = [];
let statKey;
let data
for (const sti in statRes.data) {
data = statRes.data[sti]
statKey = uniCrypto.md5(data._id.appid + data._id.platform + data._id.version + data._id
.channel)
if (!statData[statKey]) {
statData[statKey] = {
appid: data._id.appid,
platform: data._id.platform,
version: data._id.version,
channel: data._id.channel,
uids: [],
info: []
}
statData[statKey].uids.push(data._id.uid)
statData[statKey].info[data._id.uid] = {
create_time: data.create_time
}
} else {
statData[statKey].uids.push(data._id.uid)
statData[statKey].info[data._id.uid] = {
create_time: data.create_time
}
}
}
this.fillData = []
for (const sk in statData) {
await this.getFillData(statData[sk])
}
if (this.fillData.length > 0) {
res = await this.batchInsert(this.tableName, this.fillData)
}
}
return res
}
async getFillData(data) {
// 平台信息
let platformInfo = null
if (this.platforms && this.platforms[data.platform]) {
platformInfo = this.platforms[data.platform]
} else {
const platform = new Platform()
platformInfo = await platform.getPlatformAndCreate(data.platform, null)
if (!platformInfo || platformInfo.length === 0) {
platformInfo._id = ''
}
this.platforms[data.platform] = platformInfo
if (this.debug) {
console.log('platformInfo', JSON.stringify(platformInfo))
}
}
// 渠道信息
let channelInfo = null
const channelKey = data.appid + '_' + platformInfo._id + '_' + data.channel
if (this.channels && this.channels[channelKey]) {
channelInfo = this.channels[channelKey]
} else {
const channel = new Channel()
channelInfo = await channel.getChannelAndCreate(data.appid, platformInfo._id, data.channel)
if (!channelInfo || channelInfo.length === 0) {
channelInfo._id = ''
}
this.channels[channelKey] = channelInfo
if (this.debug) {
console.log('channelInfo', JSON.stringify(channelInfo))
}
}
// 版本信息
let versionInfo = null
const versionKey = data.appid + '_' + data.platform + '_' + data.version
if (this.versions && this.versions[versionKey]) {
versionInfo = this.versions[versionKey]
} else {
const version = new Version()
versionInfo = await version.getVersionAndCreate(data.appid, data.platform, data.version)
if (!versionInfo || versionInfo.length === 0) {
versionInfo._id = ''
}
this.versions[versionKey] = versionInfo
if (this.debug) {
console.log('versionInfo', JSON.stringify(versionInfo))
}
}
// 是否在本周内已存在
const datetime = new DateTime()
const dateDimension = datetime.getTimeDimensionByType('week', 0, this.startTime)
// 取出本周已经存储的uid
const weekHaveUserList = []
const haveWeekList = await this.selectAll(this.tableName, {
appid: data.appid,
version_id: versionInfo._id,
platform_id: platformInfo._id,
channel_id: channelInfo._id,
uid: {
$in: data.uids
},
dimension: 'week',
create_time: {
$gte: dateDimension.startTime,
$lte: dateDimension.endTime
}
}, {
uid: 1
})
if (this.debug) {
console.log('haveWeekList', JSON.stringify(haveWeekList))
}
if (haveWeekList.data.length > 0) {
for (const hui in haveWeekList.data) {
weekHaveUserList.push(haveWeekList.data[hui].uid)
}
}
// 取出本月已经存储的uid
const dateMonthDimension = datetime.getTimeDimensionByType('month', 0, this.startTime)
const monthHaveUserList = []
const haveMonthList = await this.selectAll(this.tableName, {
appid: data.appid,
version_id: versionInfo._id,
platform_id: platformInfo._id,
channel_id: channelInfo._id,
uid: {
$in: data.uids
},
dimension: 'month',
create_time: {
$gte: dateMonthDimension.startTime,
$lte: dateMonthDimension.endTime
}
}, {
uid: 1
})
if (this.debug) {
console.log('haveMonthList', JSON.stringify(haveMonthList))
}
if (haveMonthList.data.length > 0) {
for (const hui in haveMonthList.data) {
monthHaveUserList.push(haveMonthList.data[hui].uid)
}
}
for (const ui in data.uids) {
if (!weekHaveUserList.includes(data.uids[ui])) {
this.fillData.push({
appid: data.appid,
platform_id: platformInfo._id,
channel_id: channelInfo._id,
version_id: versionInfo._id,
uid: data.uids[ui],
dimension: 'week',
create_time: data.info[data.uids[ui]].create_time
})
}
if (!monthHaveUserList.includes(data.uids[ui])) {
this.fillData.push({
appid: data.appid,
platform_id: platformInfo._id,
channel_id: channelInfo._id,
version_id: versionInfo._id,
uid: data.uids[ui],
dimension: 'month',
create_time: data.info[data.uids[ui]].create_time
})
}
}
return true
}
/**
* 日志清理此处日志为临时数据并不需要自定义清理默认为固定值即可
*/
async clean() {
// 清除周数据周留存统计最高需要10周数据多余的为无用数据
const weeks = 10
console.log('Clean user\'s weekly logs - week:', weeks)
const dateTime = new DateTime()
const res = await this.delete(this.tableName, {
dimension: 'week',
create_time: {
$lt: dateTime.getTimeBySetWeek(0 - weeks)
}
})
if (!res.code) {
console.log('Clean user\'s weekly logs - res:', res)
}
// 清除月数据月留存统计最高需要10个月数据多余的为无用数据
const monthes = 10
console.log('Clean user\'s monthly logs - month:', monthes)
const monthRes = await this.delete(this.tableName, {
dimension: 'month',
create_time: {
$lt: dateTime.getTimeBySetMonth(0 - monthes)
}
})
if (!monthRes.code) {
console.log('Clean user\'s monthly logs - res:', res)
}
return monthRes
}
}

@ -0,0 +1,37 @@
/**
* @class AppCrashLogs 原生应用崩溃日志模型
* @function clean 原生应用崩溃日志清理函数
*/
const BaseMod = require('./base')
const {
DateTime,
UniCrypto
} = require('../lib')
module.exports = class AppCrashLogs extends BaseMod {
constructor() {
super()
this.tableName = 'app-crash-logs'
}
/**
* 原生应用崩溃日志清理函数
* @param {Number} days 保留天数
*/
async clean(days = 7) {
days = Math.max(parseInt(days), 1)
console.log('clean app crash logs - day:', days)
const dateTime = new DateTime()
const res = await this.delete(this.tableName, {
create_time: {
$lt: dateTime.getTimeBySetDays(0 - days)
}
})
if (!res.code) {
console.log('clean app crash log:', res)
}
return res
}
}

@ -0,0 +1,485 @@
/**
* @class BaseMod 数据模型基类提供基础服务支持
*/
const {
getConfig
} = require('../../shared')
//基类
module.exports = class BaseMod {
constructor() {
//配置信息
this.config = getConfig('config')
//开启/关闭debug
this.debug = this.config.debug
//主键
this.primaryKey = '_id'
//单次查询最多返回 500 条数据(阿里云500腾讯云1000这里取最小值)
this.selectMaxLimit = 500
//数据表前缀
this.tablePrefix = 'uni-stat'
//数据表连接符
this.tableConnectors = '-'
//数据表名
this.tableName = ''
//参数
this.params = {}
//数据库连接
this._dbConnection()
//redis连接
this._redisConnection()
}
/**
* 建立uniCloud数据库连接
*/
_dbConnection() {
if (!this.db) {
try {
this.db = uniCloud.database()
this.dbCmd = this.db.command
this.dbAggregate = this.dbCmd.aggregate
} catch (e) {
console.error('database connection failed: ' + e)
throw new Error('database connection failed: ' + e)
}
}
}
/**
* 建立uniCloud redis连接
*/
_redisConnection() {
if (this.config.redis && !this.redis) {
try {
this.redis = uniCloud.redis()
} catch (e) {
console.log('redis server connection failed: ' + e)
}
}
}
/**
* 获取uni统计配置项
* @param {String} key
*/
getConfig(key) {
return this.config[key]
}
/**
* 获取带前缀的数据表名称
* @param {String} tab 表名
* @param {Boolean} useDBPre 是否使用数据表前缀
*/
getTableName(tab, useDBPre = true) {
tab = tab || this.tableName
const table = (useDBPre && this.tablePrefix && tab.indexOf(this.tablePrefix) !== 0) ? this.tablePrefix + this
.tableConnectors + tab : tab
return table
}
/**
* 获取数据集
* @param {String} tab表名
* @param {Boolean} useDBPre 是否使用数据表前缀
*/
getCollection(tab, useDBPre = true) {
return this.db.collection(this.getTableName(tab, useDBPre))
}
/**
* 获取reids缓存
* @param {String} key reids缓存键值
*/
async getCache(key) {
if (!this.redis || !key) {
return false
}
let cacheResult = await this.redis.get(key)
if (this.debug) {
console.log('get cache result by key:' + key, cacheResult)
}
if (cacheResult) {
try {
cacheResult = JSON.parse(cacheResult)
} catch (e) {
if (this.debug) {
console.log('json parse error: ' + e)
}
}
}
return cacheResult
}
/**
* 设置redis缓存
* @param {String} key 键值
* @param {String} val
* @param {Number} expireTime 过期时间
*/
async setCache(key, val, expireTime) {
if (!this.redis || !key) {
return false
}
if (val instanceof Object) {
val = JSON.stringify(val)
}
if (this.debug) {
console.log('set cache result by key:' + key, val)
}
return await this.redis.set(key, val, 'EX', expireTime || this.config.cachetime)
}
/**
* 清除redis缓存
* @param {String} key 键值
*/
async clearCache(key) {
if (!this.redis || !key) {
return false
}
if (this.debug) {
console.log('delete cache by key:' + key)
}
return await this.redis.del(key)
}
/**
* 通过数据表主键_id获取数据
* @param {String} tab 表名
* @param {String} id 主键值
* @param {Boolean} useDBPre 是否使用数据表前缀
*/
async getById(tab, id, useDBPre = true) {
const condition = {}
condition[this.primaryKey] = id
const info = await this.getCollection(tab, useDBPre).where(condition).get()
return (info && info.data.length > 0) ? info.data[0] : []
}
/**
* 插入数据到数据表
* @param {String} tab 表名
* @param {Object} params 字段参数
* @param {Boolean} useDBPre 是否使用数据表前缀
*/
async insert(tab, params, useDBPre = true) {
params = params || this.params
return await this.getCollection(tab, useDBPre).add(params)
}
/**
* 修改数据表数据
* @param {String} tab 表名
* @param {Object} params 字段参数
* @param {Object} condition 条件
* @param {Boolean} useDBPre 是否使用数据表前缀
*/
async update(tab, params, condition, useDBPre = true) {
params = params || this.params
return await this.getCollection(tab).where(condition).update(params)
}
/**
* 删除数据表数据
* @param {String} tab 表名
* @param {Object} condition 条件
* @param {Boolean} useDBPre 是否使用数据表前缀
*/
async delete(tab, condition, useDBPre = true) {
if (!condition) {
return false
}
return await this.getCollection(tab, useDBPre).where(condition).remove()
}
/**
* 批量插入 - 云服务空间对单条mongo语句执行时间有限制所以批量插入需限制每次执行条数
* @param {String} tab 表名
* @param {Object} data 数据集合
* @param {Boolean} useDBPre 是否使用数据表前缀
*/
async batchInsert(tab, data, useDBPre = true) {
let batchInsertNum = this.getConfig('batchInsertNum') || 3000
batchInsertNum = Math.min(batchInsertNum, 5000)
const insertNum = Math.ceil(data.length / batchInsertNum)
let start;
let end;
let fillData;
let insertRes;
const res = {
code: 0,
msg: 'success',
data: {
inserted: 0
}
}
for (let p = 0; p < insertNum; p++) {
start = p * batchInsertNum
end = Math.min(start + batchInsertNum, data.length)
fillData = []
for (let i = start; i < end; i++) {
fillData.push(data[i])
}
if (fillData.length > 0) {
insertRes = await this.insert(tab, fillData, useDBPre)
if (insertRes && insertRes.inserted) {
res.data.inserted += insertRes.inserted
}
}
}
return res
}
/**
* 批量删除 - 云服务空间对单条mongo语句执行时间有限制所以批量删除需限制每次执行条数
* @param {String} tab 表名
* @param {Object} condition 条件
* @param {Boolean} useDBPre 是否使用数据表前缀
*/
async batchDelete(tab, condition, useDBPre = true) {
const batchDeletetNum = 5000;
let deleteIds;
let delRes;
let thisCondition
const res = {
code: 0,
msg: 'success',
data: {
deleted: 0
}
}
let run = true
while (run) {
const dataRes = await this.getCollection(tab).where(condition).limit(batchDeletetNum).get()
if (dataRes && dataRes.data.length > 0) {
deleteIds = []
for (let i = 0; i < dataRes.data.length; i++) {
deleteIds.push(dataRes.data[i][this.primaryKey])
}
if (deleteIds.length > 0) {
thisCondition = {}
thisCondition[this.primaryKey] = {
$in: deleteIds
}
delRes = await this.delete(tab, thisCondition, useDBPre)
if (delRes && delRes.deleted) {
res.data.deleted += delRes.deleted
}
}
} else {
run = false
}
}
return res
}
/**
* 基础查询
* @param {String} tab 表名
* @param {Object} params 查询参数 wherewhere条件field返回字段skip跳过的文档数limit返回的记录数orderBy排序count返回查询结果的数量
* @param {Boolean} useDBPre 是否使用数据表前缀
*/
async select(tab, params, useDBPre = true) {
const {
where,
field,
skip,
limit,
orderBy,
count
} = params
const query = this.getCollection(tab, useDBPre)
//拼接where条件
if (where) {
if (where.length > 0) {
where.forEach(key => {
query.where(where[key])
})
} else {
query.where(where)
}
}
//排序
if (orderBy) {
Object.keys(orderBy).forEach(key => {
query.orderBy(key, orderBy[key])
})
}
//指定跳过的文档数
if (skip) {
query.skip(skip)
}
//指定返回的记录数
if (limit) {
query.limit(limit)
}
//指定返回字段
if (field) {
query.field(field)
}
//指定返回查询结果数量
if (count) {
return await query.count()
}
//返回查询结果数据
return await query.get()
}
/**
* 查询并返回全部数据
* @param {String} tab 表名
* @param {Object} condition 条件
* @param {Object} field 指定查询返回字段
* @param {Boolean} useDBPre 是否使用数据表前缀
*/
async selectAll(tab, condition, field = {}, useDBPre = true) {
const countRes = await this.getCollection(tab, useDBPre).where(condition).count()
if (countRes && countRes.total > 0) {
const pageCount = Math.ceil(countRes.total / this.selectMaxLimit)
let res, returnData
for (let p = 0; p < pageCount; p++) {
res = await this.getCollection(tab, useDBPre).where(condition).orderBy(this.primaryKey, 'asc').skip(p *
this.selectMaxLimit).limit(this.selectMaxLimit).field(field).get()
if (!returnData) {
returnData = res
} else {
returnData.affectedDocs += res.affectedDocs
for (const i in res.data) {
returnData.data.push(res.data[i])
}
}
}
return returnData
}
return {
affectedDocs: 0,
data: []
}
}
/**
* 聚合查询
* @param {String} tab 表名
* @param {Object} params 聚合参数
*/
async aggregate(tab, params) {
let {
project,
match,
lookup,
group,
skip,
limit,
sort,
getAll,
useDBPre,
addFields
} = params
//useDBPre 是否使用数据表前缀
useDBPre = (useDBPre !== null && useDBPre !== undefined) ? useDBPre : true
const query = this.getCollection(tab, useDBPre).aggregate()
//设置返回字段
if (project) {
query.project(project)
}
//设置匹配条件
if (match) {
query.match(match)
}
//数据表关联
if (lookup) {
query.lookup(lookup)
}
//分组
if (group) {
if (group.length > 0) {
for (const gi in group) {
query.group(group[gi])
}
} else {
query.group(group)
}
}
//添加字段
if (addFields) {
query.addFields(addFields)
}
//排序
if (sort) {
query.sort(sort)
}
//分页
if (skip) {
query.skip(skip)
}
if (limit) {
query.limit(limit)
} else if (!getAll) {
query.limit(this.selectMaxLimit)
}
//如果未指定全部返回则直接返回查询结果
if (!getAll) {
return await query.end()
}
//若指定了全部返回则分页查询全部结果后再返回
const resCount = await query.group({
_id: {},
aggregate_count: {
$sum: 1
}
}).end()
if (resCount && resCount.data.length > 0 && resCount.data[0].aggregate_count > 0) {
//分页查询
const total = resCount.data[0].aggregate_count
const pageCount = Math.ceil(total / this.selectMaxLimit)
let res, returnData
params.limit = this.selectMaxLimit
params.getAll = false
//结果合并
for (let p = 0; p < pageCount; p++) {
params.skip = p * params.limit
res = await this.aggregate(tab, params)
if (!returnData) {
returnData = res
} else {
returnData.affectedDocs += res.affectedDocs
for (const i in res.data) {
returnData.data.push(res.data[i])
}
}
}
return returnData
} else {
return {
affectedDocs: 0,
data: []
}
}
}
}

@ -0,0 +1,107 @@
/**
* @class Channel 渠道模型
*/
const BaseMod = require('./base')
const Scenes = require('./scenes')
const {
DateTime
} = require('../lib')
module.exports = class Channel extends BaseMod {
constructor() {
super()
this.tableName = 'app-channels'
this.scenes = new Scenes()
}
/**
* 获取渠道信息
* @param {String} appid
* @param {String} platformId 平台编号
* @param {String} channel 渠道代码
*/
async getChannel(appid, platformId, channel) {
const cacheKey = 'uni-stat-channel-' + appid + '-' + platformId + '-' + channel
let channelData = await this.getCache(cacheKey)
if (!channelData) {
const channelInfo = await this.getCollection(this.tableName).where({
appid: appid,
platform_id: platformId,
channel_code: channel
}).limit(1).get()
channelData = []
if (channelInfo.data.length > 0) {
channelData = channelInfo.data[0]
if (channelData.channel_name === '') {
const scenesName = await this.scenes.getScenesNameByPlatformId(platformId, channel)
if (scenesName) {
await this.update(this.tableName, {
channel_name: scenesName,
update_time: new DateTime().getTime()
}, {
_id: channelData._id
})
}
}
await this.setCache(cacheKey, channelData)
}
}
return channelData
}
/**
* 获取渠道信息没有则进行创建
* @param {String} appid
* @param {String} platformId
* @param {String} channel
*/
async getChannelAndCreate(appid, platformId, channel) {
if (!appid || !platformId) {
return []
}
const channelInfo = await this.getChannel(appid, platformId, channel)
if (channelInfo.length === 0) {
const thisTime = new DateTime().getTime()
const insertParam = {
appid: appid,
platform_id: platformId,
channel_code: channel,
channel_name: await this.scenes.getScenesNameByPlatformId(platformId, channel),
create_time: thisTime,
update_time: thisTime
}
const res = await this.insert(this.tableName, insertParam)
if (res && res.id) {
return Object.assign(insertParam, {
_id: res.id
})
}
}
return channelInfo
}
/**
* 获取渠道_id
* @param {String} appid
* @param {String} platformId
* @param {String} channel
*/
async getChannelId(appid, platformId, channel) {
const channelInfo = await this.getChannel(appid, platformId, channel)
return channelInfo.length > 0 ? channelInfo._id : ''
}
/**
* 获取渠道码或者场景值
* @param {Object} params 上报参数
*/
getChannelCode(params) {
//小程序未上报渠道则使用场景值
if (params.ch) {
return params.ch
} else if (params.sc && params.ut.indexOf('mp-') === 0) {
return params.sc
}
return this.scenes.defualtCode
}
}

@ -0,0 +1,184 @@
/**
* @class Device 设备模型
*/
const BaseMod = require('./base')
const Platform = require('./platform')
const {
DateTime
} = require('../lib')
module.exports = class Device extends BaseMod {
constructor() {
super()
this.tableName = 'opendb-device'
this.tablePrefix = false
this.cacheKeyPre = 'uni-stat-device-'
}
/**
* 通过设备编号获取设备信息
* @param {Object} deviceId 设备编号
*/
async getDeviceById(deviceId) {
const cacheKey = this.cacheKeyPre + deviceId
let deviceData = await this.getCache(cacheKey)
if (!deviceData) {
const deviceRes = await this.getCollection().where({
device_id: deviceId
}).get()
deviceData = []
if (deviceRes.data.length > 0) {
deviceData = deviceRes.data[0]
await this.setCache(cacheKey, deviceData)
}
}
return deviceData
}
/**
* 设置设备信息
* @param {Object} params 上报参数
*/
async setDevice(params) {
// 设备信息
if (!params.did) {
return {
code: 200,
msg: 'Parameter "did" not found'
}
}
const deviceData = await this.getDeviceById(params.did)
//不存在则添加
if(deviceData.length === 0) {
return await this.addDevice(params)
} else {
return await this.updateDevice(params, deviceData)
}
}
/**
* 添加设备信息
* @param {Object} params 上报参数
*/
async addDevice(params) {
const dateTime = new DateTime()
const platform = new Platform()
const fillParams = {
device_id: params.did,
appid: params.ak,
vendor: params.brand ? params.brand : '',
push_clientid: params.cid ? params.cid : '',
imei: params.imei ? params.imei : '',
oaid: params.oaid ? params.oaid : '',
idfa: params.idfa ? params.idfa : '',
imsi: params.imsi ? params.imsi : '',
model: params.md ? params.md : '',
uni_platform: params.up ? params.up : '',
os_name: params.on ? params.on : platform.getOsName(params.p),
os_version: params.sv ? params.sv : '',
os_language: params.lang ? params.lang : '',
os_theme: params.ot ? params.ot : '',
pixel_ratio: params.pr ? params.pr : '',
network_model: params.net ? params.net : '',
window_width: params.ww ? params.ww : '',
window_height: params.wh ? params.wh : '',
screen_width: params.sw ? params.sw : '',
screen_height: params.sh ? params.sh : '',
rom_name: params.rn ? params.rn : '',
rom_version: params.rv ? params.rv : '',
location_ip: params.ip ? params.ip : '',
location_latitude: params.lat ? parseFloat(params.lat) : 0,
location_longitude: params.lng ? parseFloat(params.lng) : 0,
location_country: params.cn ? params.cn : '',
location_province: params.pn ? params.pn : '',
location_city: params.ct ? params.ct : '',
create_date: dateTime.getTime(),
last_update_date: dateTime.getTime()
}
const res = await this.insert(this.tableName, fillParams)
if (res && res.id) {
return {
code: 0,
msg: 'success',
}
} else {
return {
code: 500,
msg: 'Device data filled error'
}
}
}
/**
* 修改设备信息
* @param {Object} params
* @param {Object} deviceData
*/
async updateDevice(params, deviceData) {
//最新的参数
const dateTime = new DateTime()
const platform = new Platform()
console.log('device params', params)
const newDeviceParams = {
appid: params.ak,
push_clientid: params.cid ? params.cid : '',
imei: params.imei ? params.imei : '',
oaid: params.oaid ? params.oaid : '',
idfa: params.idfa ? params.idfa : '',
imsi: params.imsi ? params.imsi : '',
uni_platform: params.up ? params.up : '',
os_name: params.on ? params.on : platform.getOsName(params.p),
os_version: params.sv ? params.sv : '',
os_language: params.lang ? params.lang : '',
pixel_ratio: params.pr ? params.pr : '',
network_model: params.net ? params.net : '',
window_width: params.ww ? params.ww : '',
window_height: params.wh ? params.wh : '',
screen_width: params.sw ? params.sw : '',
screen_height: params.sh ? params.sh : '',
rom_name: params.rn ? params.rn : '',
rom_version: params.rv ? params.rv : '',
location_ip: params.ip ? params.ip : '',
location_latitude: params.lat ? parseFloat(params.lat) : '',
location_longitude: params.lng ? parseFloat(params.lng) : '',
location_country: params.cn ? params.cn : '',
location_province: params.pn ? params.pn : '',
location_city: params.ct ? params.ct : '',
}
//检查是否有需要更新的数据
const updateData = {}
for(let key in newDeviceParams) {
if(newDeviceParams[key] && newDeviceParams[key] !== deviceData[key]) {
updateData[key] = newDeviceParams[key]
}
}
if(Object.keys(updateData).length) {
if(this.debug) {
console.log('Device need to update', updateData)
}
//数据更新
updateData.last_update_date = dateTime.getTime()
await this.update(this.tableName, updateData, {device_id: params.did})
} else {
if(this.debug) {
console.log('Device not need update', newDeviceParams)
}
}
return {
code: 0,
msg: 'success'
}
}
async bindPush(params) {
if (!params.cid) {
return {
code: 200,
msg: 'Parameter "cid" not found'
}
}
return await this.setDevice(params)
}
}

@ -0,0 +1,141 @@
/**
* @class ErrorLog 错误日志模型
*/
const BaseMod = require('./base')
const Platform = require('./platform')
const Channel = require('./channel')
const {
DateTime,
UniCrypto
} = require('../lib')
module.exports = class ErrorLog extends BaseMod {
constructor() {
super()
this.tableName = 'error-logs'
}
/**
* 错误日志数据填充
* @param {Object} reportParams 上报参数
*/
async fill(reportParams) {
let params, errorHash, errorCount, cacheKey;
const fillParams = []
const platform = new Platform()
const dateTime = new DateTime()
const uniCrypto = new UniCrypto()
const channel = new Channel()
const {
needCheck,
checkTime
} = this.getConfig('errorCheck')
const errorCheckTime = Math.max(checkTime, 1)
let spaceId
let spaceProvider
for (const rk in reportParams) {
params = reportParams[rk]
errorHash = uniCrypto.md5(params.em)
cacheKey = 'error-count-' + errorHash
// 校验在指定时间段内是否已存在相同的错误项
if (needCheck) {
errorCount = await this.getCache(cacheKey)
if (!errorCount) {
errorCount = await this.getCollection(this.tableName).where({
error_hash: errorHash,
create_time: {
$gte: dateTime.getTime() - errorCheckTime * 60000
}
}).count()
if (errorCount && errorCount.total > 0) {
await this.setCache(cacheKey, errorCount, errorCheckTime * 60)
}
}
if (errorCount && errorCount.total > 0) {
if (this.debug) {
console.log('This error have already existsed: ' + params.em)
}
continue
}
}
//获取云端信息
spaceId = null
spaceProvider = null
if (params.spi) {
//云函数调用参数
spaceId = params.spi.spaceId
spaceProvider = params.spi.provider
} else {
//云对象调用参数
if (params.spid) {
spaceId = params.spid
}
if (params.sppd) {
spaceProvider = params.sppd
}
}
// 填充数据
fillParams.push({
appid: params.ak,
version: params.v ? params.v : '',
platform: platform.getPlatformCode(params.ut, params.p),
channel: channel.getChannelCode(params),
device_id: params.did,
uid: params.uid ? params.uid : '',
os: params.on ? params.on : platform.getOsName(params.p),
ua: params.ua ? params.ua : '',
page_url: params.url ? params.url : '',
space_id: spaceId ? spaceId : '',
space_provider: spaceProvider ? spaceProvider : '',
platform_version: params.mpv ? params.mpv : '',
error_msg: params.em ? params.em : '',
error_hash: errorHash,
create_time: dateTime.getTime()
})
}
if (fillParams.length === 0) {
return {
code: 200,
msg: 'Invild param'
}
}
const res = await this.insert(this.tableName, fillParams)
if (res && res.inserted) {
return {
code: 0,
msg: 'success'
}
} else {
return {
code: 500,
msg: 'Filled error'
}
}
}
/**
* 错误日志清理
* @param {Number} days 日志保留天数
*/
async clean(days) {
days = Math.max(parseInt(days), 1)
console.log('clean error logs - day:', days)
const dateTime = new DateTime()
const res = await this.delete(this.tableName, {
create_time: {
$lt: dateTime.getTimeBySetDays(0 - days)
}
})
if (!res.code) {
console.log('clean error log:', res)
}
return res
}
}

@ -0,0 +1,459 @@
/**
* @class ErrorResult 错误结果统计模型
*/
const BaseMod = require('./base')
const Platform = require('./platform')
const Channel = require('./channel')
const Version = require('./version')
const ErrorLog = require('./errorLog')
const AppCrashLogs = require('./appCrashLogs')
const SessionLog = require('./sessionLog')
const {
DateTime
} = require('../lib')
module.exports = class ErrorResult extends BaseMod {
constructor() {
super()
this.tableName = 'error-result'
this.platforms = []
this.channels = []
this.versions = []
this.errors = []
}
/**
* 错误结果统计
* @param {String} type 统计类型 hour实时统计 day按天统计week按周统计 month按月统计
* @param {Date|Time} date 指定日期或时间戳
* @param {Boolean} reset 是否重置为ture时会重置该批次数据
*/
async stat(type, date, reset) {
//前端js错误统计
const resJs = await this.statJs(type, date, reset)
//原生应用崩溃错误统计
const resCrash = await this.statCrash(type, date, reset)
return {
code: 0,
msg: 'success',
data: {
resJs,
resCrash
}
}
}
/**
* 前端js错误结果统计
* @param {String} type 统计类型 hour实时统计 day按天统计week按周统计 month按月统计
* @param {Date|Time} date 指定日期或时间戳
* @param {Boolean} reset 是否重置为ture时会重置该批次数据
*/
async statJs(type, date, reset) {
const allowedType = ['day']
if (!allowedType.includes(type)) {
return {
code: 1002,
msg: 'This type is not allowed'
}
}
this.fillType = type
const dateTime = new DateTime()
const dateDimension = dateTime.getTimeDimensionByType(type, -1, date)
this.startTime = dateDimension.startTime
this.endTime = dateDimension.endTime
if (this.debug) {
console.log('dimension time', this.startTime + '--' + this.endTime)
}
// 查看当前时间段日志是否已存在,防止重复生成
if (!reset) {
const checkRes = await this.getCollection(this.tableName).where({
type: 'js',
start_time: this.startTime,
end_time: this.endTime
}).get()
if (checkRes.data.length > 0) {
console.log('error log have existed')
return {
code: 1003,
msg: 'This log have existed'
}
}
} else {
const delRes = await this.delete(this.tableName, {
type: 'js',
start_time: this.startTime,
end_time: this.endTime
})
console.log('delete old data result:', JSON.stringify(delRes))
}
// 数据获取
this.errorLog = new ErrorLog()
const statRes = await this.aggregate(this.errorLog.tableName, {
project: {
appid: 1,
version: 1,
platform: 1,
channel: 1,
error_hash: 1,
create_time: 1
},
match: {
create_time: {
$gte: this.startTime,
$lte: this.endTime
}
},
group: {
_id: {
appid: '$appid',
version: '$version',
platform: '$platform',
channel: '$channel',
error_hash: '$error_hash'
},
error_count: {
$sum: 1
}
},
sort: {
error_count: 1
},
getAll: true
})
let res = {
code: 0,
msg: 'success'
}
if (this.debug) {
console.log('statRes', JSON.stringify(statRes))
}
if (statRes.data.length > 0) {
this.fillData = []
for (const i in statRes.data) {
await this.fillJs(statRes.data[i])
}
if (this.fillData.length > 0) {
res = await this.batchInsert(this.tableName, this.fillData)
}
}
return res
}
/**
* 前端js错误统计结果数据填充
* @param {Object} data 数据集合
*/
async fillJs(data) {
// 平台信息
let platformInfo = null
if (this.platforms && this.platforms[data._id.platform]) {
//暂存下数据,减少读库
platformInfo = this.platforms[data._id.platform]
} else {
const platform = new Platform()
platformInfo = await platform.getPlatformAndCreate(data._id.platform, null)
if (!platformInfo || platformInfo.length === 0) {
platformInfo._id = ''
}
this.platforms[data._id.platform] = platformInfo
if (this.debug) {
console.log('platformInfo', JSON.stringify(platformInfo))
}
}
// 渠道信息
let channelInfo = null
const channelKey = data._id.appid + '_' + platformInfo._id + '_' + data._id.channel
if (this.channels && this.channels[channelKey]) {
channelInfo = this.channels[channelKey]
} else {
const channel = new Channel()
channelInfo = await channel.getChannelAndCreate(data._id.appid, platformInfo._id, data._id.channel)
if (!channelInfo || channelInfo.length === 0) {
channelInfo._id = ''
}
this.channels[channelKey] = channelInfo
if (this.debug) {
console.log('channelInfo', JSON.stringify(channelInfo))
}
}
// 版本信息
let versionInfo = null
const versionKey = data._id.appid + '_' + data._id.platform + '_' + data._id.version
if (this.versions && this.versions[versionKey]) {
versionInfo = this.versions[versionKey]
} else {
const version = new Version()
versionInfo = await version.getVersionAndCreate(data._id.appid, data._id.platform, data._id.version)
if (!versionInfo || versionInfo.length === 0) {
versionInfo._id = ''
}
this.versions[versionKey] = versionInfo
if (this.debug) {
console.log('versionInfo', JSON.stringify(versionInfo))
}
}
// 错误信息
let errorInfo = null
if (this.errors && this.errors[data._id.error_hash]) {
errorInfo = this.errors[data._id.error_hash]
} else {
const cacheKey = 'uni-stat-errors-' + data._id.error_hash
errorInfo = await this.getCache(cacheKey)
if (!errorInfo) {
errorInfo = await this.getCollection(this.errorLog.tableName).where({
error_hash: data._id.error_hash
}).limit(1).get()
if (!errorInfo || errorInfo.data.length === 0) {
errorInfo.error_msg = ''
} else {
errorInfo = errorInfo.data[0]
await this.setCache(cacheKey, errorInfo)
}
}
this.errors[data._id.error_hash] = errorInfo
}
// 最近一次报错时间
const matchCondition = data._id
Object.assign(matchCondition, {
create_time: {
$gte: this.startTime,
$lte: this.endTime
}
})
const lastErrorLog = await this.getCollection(this.errorLog.tableName).where(matchCondition).orderBy(
'create_time', 'desc').limit(1).get()
let lastErrorTime = ''
if (lastErrorLog && lastErrorLog.data.length > 0) {
lastErrorTime = lastErrorLog.data[0].create_time
}
//数据填充
const datetime = new DateTime()
const insertParams = {
appid: data._id.appid,
platform_id: platformInfo._id,
channel_id: channelInfo._id,
version_id: versionInfo._id,
type: 'js',
hash: data._id.error_hash,
msg: errorInfo.error_msg,
count: data.error_count,
last_time: lastErrorTime,
dimension: this.fillType,
stat_date: datetime.getDate('Ymd', this.startTime),
start_time: this.startTime,
end_time: this.endTime
}
this.fillData.push(insertParams)
return insertParams
}
/**
* 原生应用错误结果统计
* @param {String} type 统计类型 hour实时统计 day按天统计week按周统计 month按月统计
* @param {Date|Time} date 指定日期或时间戳
* @param {Boolean} reset 是否重置为ture时会重置该批次数据
*/
async statCrash(type, date, reset) {
const allowedType = ['day']
if (!allowedType.includes(type)) {
return {
code: 1002,
msg: 'This type is not allowed'
}
}
this.fillType = type
const dateTime = new DateTime()
const dateDimension = dateTime.getTimeDimensionByType(type, -1, date)
this.startTime = dateDimension.startTime
this.endTime = dateDimension.endTime
if (this.debug) {
console.log('dimension time', this.startTime + '--' + this.endTime)
}
// 查看当前时间段日志是否已存在,防止重复生成
if (!reset) {
const checkRes = await this.getCollection(this.tableName).where({
type: 'crash',
start_time: this.startTime,
end_time: this.endTime
}).get()
if (checkRes.data.length > 0) {
console.log('error log have existed')
return {
code: 1003,
msg: 'This log have existed'
}
}
} else {
const delRes = await this.delete(this.tableName, {
type: 'crash',
start_time: this.startTime,
end_time: this.endTime
})
console.log('delete old data result:', JSON.stringify(delRes))
}
// 数据获取
this.crashLogs = new AppCrashLogs()
const statRes = await this.aggregate(this.crashLogs.tableName, {
project: {
appid: 1,
version: 1,
platform: 1,
channel: 1,
create_time: 1
},
match: {
create_time: {
$gte: this.startTime,
$lte: this.endTime
}
},
group: {
_id: {
appid: '$appid',
version: '$version',
platform: '$platform',
channel: '$channel'
},
error_count: {
$sum: 1
}
},
sort: {
error_count: 1
},
getAll: true
})
let res = {
code: 0,
msg: 'success'
}
if (this.debug) {
console.log('statRes', JSON.stringify(statRes))
}
if (statRes.data.length > 0) {
this.fillData = []
for (const i in statRes.data) {
await this.fillCrash(statRes.data[i])
}
if (this.fillData.length > 0) {
res = await this.batchInsert(this.tableName, this.fillData)
}
}
return res
}
async fillCrash(data) {
// 平台信息
let platformInfo = null
if (this.platforms && this.platforms[data._id.platform]) {
//暂存下数据,减少读库
platformInfo = this.platforms[data._id.platform]
} else {
const platform = new Platform()
platformInfo = await platform.getPlatformAndCreate(data._id.platform, null)
if (!platformInfo || platformInfo.length === 0) {
platformInfo._id = ''
}
this.platforms[data._id.platform] = platformInfo
if (this.debug) {
console.log('platformInfo', JSON.stringify(platformInfo))
}
}
// 渠道信息
let channelInfo = null
data._id.channel = data._id.channel ? data._id.channel : '1001'
const channelKey = data._id.appid + '_' + platformInfo._id + '_' + data._id.channel
if (this.channels && this.channels[channelKey]) {
channelInfo = this.channels[channelKey]
} else {
const channel = new Channel()
channelInfo = await channel.getChannelAndCreate(data._id.appid, platformInfo._id, data._id.channel)
if (!channelInfo || channelInfo.length === 0) {
channelInfo._id = ''
}
this.channels[channelKey] = channelInfo
if (this.debug) {
console.log('channelInfo', JSON.stringify(channelInfo))
}
}
// 版本信息
let versionInfo = null
const versionKey = data._id.appid + '_' + data._id.platform + '_' + data._id.version
if (this.versions && this.versions[versionKey]) {
versionInfo = this.versions[versionKey]
} else {
const version = new Version()
versionInfo = await version.getVersionAndCreate(data._id.appid, data._id.platform, data._id.version)
if (!versionInfo || versionInfo.length === 0) {
versionInfo._id = ''
}
this.versions[versionKey] = versionInfo
if (this.debug) {
console.log('versionInfo', JSON.stringify(versionInfo))
}
}
//app启动次数
const sessionLog = new SessionLog()
const sessionTimesRes = await this.getCollection(sessionLog.tableName).where({
appid: data._id.appid,
version: data._id.version,
platform: data._id.platform,
channel: data._id.channel,
create_time: {
$gte: this.startTime,
$lte: this.endTime
}
}).count()
let sessionTimes = 0
if(sessionTimesRes && sessionTimesRes.total > 0) {
sessionTimes = sessionTimesRes.total
} else {
console.log('Not found session logs')
return false
}
//数据填充
const datetime = new DateTime()
const insertParams = {
appid: data._id.appid,
platform_id: platformInfo._id,
channel_id: channelInfo._id,
version_id: versionInfo._id,
type: 'crash',
count: data.error_count,
app_launch_count: sessionTimes,
dimension: this.fillType,
stat_date: datetime.getDate('Ymd', this.startTime),
start_time: this.startTime,
end_time: this.endTime
}
this.fillData.push(insertParams)
return insertParams
}
}

@ -0,0 +1,72 @@
/**
* @class StatEvent 事件统计模型
*/
const BaseMod = require('./base')
const {
DateTime
} = require('../lib')
module.exports = class StatEvent extends BaseMod {
constructor() {
super()
this.tableName = 'events'
this.defaultEvent = this.getConfig('event') || {
login: '登录',
register: '注册',
click: '点击',
share: '分享',
pay_success: '支付成功',
pay_fail: '支付失败'
}
}
/**
* 获取事件信息
* @param {String} appid: DCloud appid
* @param {String} eventKey 事件键值
*/
async getEvent(appid, eventKey) {
const cacheKey = 'uni-stat-event-' + appid + '-' + eventKey
let eventData = await this.getCache(cacheKey)
if (!eventData) {
const eventInfo = await this.getCollection(this.tableName).where({
appid: appid,
event_key: eventKey
}).get()
eventData = []
if (eventInfo.data.length > 0) {
eventData = eventInfo.data[0]
await this.setCache(cacheKey, eventData)
}
}
return eventData
}
/**
* 获取事件信息不存在则创建
* @param {String} appid: DCloud appid
* @param {String} eventKey 事件键值
*/
async getEventAndCreate(appid, eventKey) {
const eventInfo = await this.getEvent(appid, eventKey)
if (eventInfo.length === 0) {
const thisTime = new DateTime().getTime()
const insertParam = {
appid: appid,
event_key: eventKey,
event_name: this.defaultEvent[eventKey] ? this.defaultEvent[eventKey] : '',
create_time: thisTime,
update_time: thisTime
}
const res = await this.insert(this.tableName, insertParam)
if (res && res.id) {
return Object.assign(insertParam, {
_id: res.id
})
}
}
return eventInfo
}
}

@ -0,0 +1,156 @@
/**
* @class EventLog 事件日志模型
*/
const BaseMod = require('./base')
const Platform = require('./platform')
const Channel = require('./channel')
const StatEvent = require('./event')
const SessionLog = require('./sessionLog')
const ShareLog = require('./shareLog')
const {
DateTime
} = require('../lib')
module.exports = class EventLog extends BaseMod {
constructor() {
super()
this.tableName = 'event-logs'
this.sessionLogInfo = []
}
/**
* 事件日志填充
* @param {Object} reportParams 上报参数
*/
async fill(reportParams) {
let params;
let sessionKey, sessionLogKey;
let sessionLogInfo;
const sessionData = []
const fillParams = []
const shareParams = []
const sessionLog = new SessionLog()
const event = new StatEvent()
const platform = new Platform()
const dateTime = new DateTime()
const channel = new Channel()
for (const rk in reportParams) {
params = reportParams[rk]
//暂存下会话数据,减少读库
sessionKey = params.ak + params.did + params.p
if (!this.sessionLogInfo[sessionKey]) {
// 会话日志
sessionLogInfo = await sessionLog.getSession(params)
if (sessionLogInfo.code) {
return sessionLogInfo
}
if (this.debug) {
console.log('sessionLogInfo', JSON.stringify(sessionLogInfo))
}
this.sessionLogInfo[sessionKey] = sessionLogInfo
} else {
sessionLogInfo = this.sessionLogInfo[sessionKey]
}
// 会话数据
sessionLogKey = sessionLogInfo.data.sessionLogId.toString()
if (!sessionData[sessionLogKey]) {
sessionData[sessionLogKey] = {
eventCount: sessionLogInfo.data.eventCount + 1,
addEventCount: 1,
uid: sessionLogInfo.data.uid,
createTime: sessionLogInfo.data.createTime
}
} else {
sessionData[sessionLogKey].eventCount++
sessionData[sessionLogKey].addEventCount++
}
// 事件
const eventInfo = await event.getEventAndCreate(params.ak, params.e_n)
// 填充数据
fillParams.push({
appid: params.ak,
version: params.v ? params.v : '',
platform: platform.getPlatformCode(params.ut, params.p),
channel: channel.getChannelCode(params),
device_id: params.did,
uid: params.uid ? params.uid : '',
session_id: sessionLogInfo.data.sessionLogId,
page_id: sessionLogInfo.data.pageId,
event_key: eventInfo.event_key,
param: params.e_v ? params.e_v : '',
// 版本
sdk_version: params.mpsdk ? params.mpsdk : '',
platform_version: params.mpv ? params.mpv : '',
// 设备相关
device_os_name: params.on ? params.on : platform.getOsName(params.p),
device_os_version: params.sv ? params.sv : '',
device_vendor: params.brand ? params.brand : '',
device_model: params.md ? params.md : '',
device_language: params.lang ? params.lang : '',
device_pixel_ratio: params.pr ? params.pr : '',
device_window_width: params.ww ? params.ww : '',
device_window_height: params.wh ? params.wh : '',
device_screen_width: params.sw ? params.sw : '',
device_screen_height: params.sh ? params.sh : '',
create_time: dateTime.getTime()
})
// 分享数据
if (eventInfo.event_key === 'share') {
shareParams.push(params)
}
}
if (fillParams.length === 0) {
return {
code: 200,
msg: 'Invild param'
}
}
if (shareParams.length > 0) {
const shareLog = new ShareLog()
await shareLog.fill(shareParams, this.sessionLogInfo)
}
const res = await this.insert(this.tableName, fillParams)
if (res && res.inserted) {
for (const sid in sessionData) {
await sessionLog.updateSession(sid, sessionData[sid])
}
return {
code: 0,
msg: 'success'
}
} else {
return {
code: 500,
msg: 'Filled error'
}
}
}
/**
* 事件日志清理
* @param {Number} days 保留天数
*/
async clean(days) {
days = Math.max(parseInt(days), 1)
console.log('clean event logs - day:', days)
const dateTime = new DateTime()
//删除过期数据
const res = await this.delete(this.tableName, {
create_time: {
$lt: dateTime.getTimeBySetDays(0 - days)
}
})
if (!res.code) {
console.log('clean event log:', res)
}
return res
}
}

@ -0,0 +1,268 @@
/**
* @class EventResult 事件结果统计
*/
const BaseMod = require('./base')
const Platform = require('./platform')
const Channel = require('./channel')
const Version = require('./version')
const EventLog = require('./eventLog')
const {
DateTime
} = require('../lib')
module.exports = class EventResult extends BaseMod {
constructor() {
super()
this.tableName = 'event-result'
this.platforms = []
this.channels = []
this.versions = []
}
/**
* 事件数据统计
* @param {String} type 统计类型 hour实时统计 day按天统计week按周统计 month按月统计
* @param {Date|Time} date 指定日期或时间戳
* @param {Boolean} reset 是否重置为ture时会重置该批次数据
*/
async stat(type, date, reset) {
const allowedType = ['day']
if (!allowedType.includes(type)) {
return {
code: 1002,
msg: 'This type is not allowed'
}
}
this.fillType = type
const dateTime = new DateTime()
const dateDimension = dateTime.getTimeDimensionByType(type, -1, date)
this.startTime = dateDimension.startTime
this.endTime = dateDimension.endTime
if (this.debug) {
console.log('dimension time', this.startTime + '--' + this.endTime)
}
// 查看当前时间段日志是否已存在,防止重复生成
if (!reset) {
const checkRes = await this.getCollection(this.tableName).where({
start_time: this.startTime,
end_time: this.endTime
}).get()
if (checkRes.data.length > 0) {
console.log('event log have existed')
return {
code: 1003,
msg: 'This log have existed'
}
}
} else {
const delRes = await this.delete(this.tableName, {
start_time: this.startTime,
end_time: this.endTime
})
console.log('delete old data result:', JSON.stringify(delRes))
}
// 数据获取
this.eventLog = new EventLog()
const statRes = await this.aggregate(this.eventLog.tableName, {
project: {
appid: 1,
version: 1,
platform: 1,
channel: 1,
event_key: 1,
device_id: 1,
create_time: 1
},
match: {
create_time: {
$gte: this.startTime,
$lte: this.endTime
}
},
group: {
_id: {
appid: '$appid',
version: '$version',
platform: '$platform',
channel: '$channel',
event_key: '$event_key'
},
event_count: {
$sum: 1
}
},
sort: {
event_count: 1
},
getAll: true
})
let res = {
code: 0,
msg: 'success'
}
if (this.debug) {
console.log('statRes', JSON.stringify(statRes))
}
if (statRes.data.length > 0) {
this.fillData = []
for (const i in statRes.data) {
await this.fill(statRes.data[i])
}
if (this.fillData.length > 0) {
res = await this.batchInsert(this.tableName, this.fillData)
}
}
return res
}
/**
* 事件统计数据填充
* @param {Object} data 数据集合
*/
async fill(data) {
// 平台信息
let platformInfo = null
if (this.platforms && this.platforms[data._id.platform]) {
//暂存下数据,减少读库
platformInfo = this.platforms[data._id.platform]
} else {
const platform = new Platform()
platformInfo = await platform.getPlatformAndCreate(data._id.platform, null)
if (!platformInfo || platformInfo.length === 0) {
platformInfo._id = ''
}
this.platforms[data._id.platform] = platformInfo
if (this.debug) {
console.log('platformInfo', JSON.stringify(platformInfo))
}
}
// 渠道信息
let channelInfo = null
const channelKey = data._id.appid + '_' + platformInfo._id + '_' + data._id.channel
if (this.channels && this.channels[channelKey]) {
channelInfo = this.channels[channelKey]
} else {
const channel = new Channel()
channelInfo = await channel.getChannelAndCreate(data._id.appid, platformInfo._id, data._id.channel)
if (!channelInfo || channelInfo.length === 0) {
channelInfo._id = ''
}
this.channels[channelKey] = channelInfo
if (this.debug) {
console.log('channelInfo', JSON.stringify(channelInfo))
}
}
// 版本信息
let versionInfo = null
const versionKey = data._id.appid + '_' + data._id.platform + '_' + data._id.version
if (this.versions && this.versions[versionKey]) {
versionInfo = this.versions[versionKey]
} else {
const version = new Version()
versionInfo = await version.getVersionAndCreate(data._id.appid, data._id.platform, data._id.version)
if (!versionInfo || versionInfo.length === 0) {
versionInfo._id = ''
}
this.versions[versionKey] = versionInfo
if (this.debug) {
console.log('versionInfo', JSON.stringify(versionInfo))
}
}
const matchCondition = data._id
Object.assign(matchCondition, {
create_time: {
$gte: this.startTime,
$lte: this.endTime
}
})
if (this.debug) {
console.log('matchCondition', JSON.stringify(matchCondition))
}
// 触发事件设备数统计
const statEventDeviceRes = await this.aggregate(this.eventLog.tableName, {
project: {
appid: 1,
version: 1,
platform: 1,
channel: 1,
event_key: 1,
device_id: 1,
create_time: 1
},
match: matchCondition,
group: [{
_id: {
device_id: '$device_id'
}
}, {
_id: {},
total_devices: {
$sum: 1
}
}]
})
let eventDeviceCount = 0
if (statEventDeviceRes.data.length > 0) {
eventDeviceCount = statEventDeviceRes.data[0].total_devices
}
// 触发事件用户数统计
const statEventUserRes = await this.aggregate(this.eventLog.tableName, {
project: {
appid: 1,
version: 1,
platform: 1,
channel: 1,
event_key: 1,
uid: 1,
create_time: 1
},
match: {
...matchCondition,
uid: {
$ne: ''
}
},
group: [{
_id: {
uid: '$uid'
}
}, {
_id: {},
total_users: {
$sum: 1
}
}]
})
let eventUserCount = 0
if (statEventUserRes.data.length > 0) {
eventUserCount = statEventUserRes.data[0].total_users
}
const datetime = new DateTime()
const insertParams = {
appid: data._id.appid,
platform_id: platformInfo._id,
channel_id: channelInfo._id,
version_id: versionInfo._id,
event_key: data._id.event_key,
event_count: data.event_count,
device_count: eventDeviceCount,
user_count: eventUserCount,
dimension: this.fillType,
stat_date: datetime.getDate('Ymd', this.startTime),
start_time: this.startTime,
end_time: this.endTime
}
this.fillData.push(insertParams)
return insertParams
}
}

@ -0,0 +1,23 @@
/**
* 基础对外模型
*/
module.exports = {
BaseMod: require('./base'),
SessionLog: require('./sessionLog'),
UserSessionLog: require('./userSessionLog'),
PageLog: require('./pageLog'),
EventLog: require('./eventLog'),
ShareLog: require('./shareLog'),
ErrorLog: require('./errorLog'),
AppCrashLogs: require('./appCrashLogs'),
StatResult: require('./statResult'),
ActiveUsers: require('./activeUsers'),
ActiveDevices: require('./activeDevices'),
PageResult: require('./pageResult'),
EventResult: require('./eventResult'),
ErrorResult: require('./errorResult'),
Loyalty: require('./loyalty'),
RunErrors: require('./runErrors'),
uniPay: require('./uni-pay'),
Setting: require('./setting'),
}

@ -0,0 +1,491 @@
/**
* 设备/用户忠诚度粘性统计模型
*/
const BaseMod = require('./base')
const Platform = require('./platform')
const Channel = require('./channel')
const Version = require('./version')
const SessionLog = require('./sessionLog')
const UserSessionLog = require('./userSessionLog')
const {
DateTime
} = require('../lib')
module.exports = class Loyalty extends BaseMod {
constructor() {
super()
this.tableName = 'loyalty-result'
this.platforms = []
this.channels = []
this.versions = []
}
/**
* 设备/用户忠诚度粘性统计
* @param {String} type 统计类型 hour实时统计 day按天统计week按周统计 month按月统计
* @param {Date|Time} date 指定日期或时间戳
* @param {Boolean} reset 是否重置为ture时会重置该批次数据
*/
async stat(type, date, reset) {
const allowedType = ['day']
if (!allowedType.includes(type)) {
return {
code: 1002,
msg: 'This type is not allowed'
}
}
this.fillType = type
const dateTime = new DateTime()
const dateDimension = dateTime.getTimeDimensionByType(type, -1, date)
this.startTime = dateDimension.startTime
this.endTime = dateDimension.endTime
if (this.debug) {
console.log('this time', dateTime.getTime())
console.log('dimension time', this.startTime + '--' + this.endTime)
}
// 查看当前时间段日志是否已存在,防止重复生成
if (!reset) {
const checkRes = await this.getCollection(this.tableName).where({
start_time: this.startTime,
end_time: this.endTime
}).get()
if (checkRes.data.length > 0) {
console.log('loyalty log have existed')
return {
code: 1003,
msg: 'This log have existed'
}
}
} else {
const delRes = await this.delete(this.tableName, {
start_time: this.startTime,
end_time: this.endTime
})
console.log('delete old data result:', JSON.stringify(delRes))
}
// 数据获取
this.sessionLog = new SessionLog()
const statRes = await this.aggregate(this.sessionLog.tableName, {
project: {
appid: 1,
version: 1,
platform: 1,
channel: 1,
page_count: 1,
duration: 1,
create_time: 1
},
match: {
create_time: {
$gte: this.startTime,
$lte: this.endTime
}
},
group: {
_id: {
appid: '$appid',
version: '$version',
platform: '$platform',
channel: '$channel'
},
page_count_sum: {
$sum: '$page_count'
},
duration_sum: {
$sum: '$duration'
}
},
sort: {
page_count_sum: 1,
duration_sum: 1
},
getAll: true
})
let res = {
code: 0,
msg: 'success'
}
if (this.debug) {
console.log('statRes', JSON.stringify(statRes))
}
if (statRes.data.length > 0) {
this.fillData = []
for (const i in statRes.data) {
await this.fill(statRes.data[i])
}
if (this.fillData.length > 0) {
res = await this.batchInsert(this.tableName, this.fillData)
}
}
return res
}
/**
* 设备/用户忠诚度粘性数据填充
* @param {Object} data 数据集合
*/
async fill(data) {
// 平台信息
let platformInfo = null
if (this.platforms && this.platforms[data._id.platform]) {
platformInfo = this.platforms[data._id.platform]
} else {
const platform = new Platform()
platformInfo = await platform.getPlatformAndCreate(data._id.platform, null)
if (!platformInfo || platformInfo.length === 0) {
platformInfo._id = ''
}
this.platforms[data._id.platform] = platformInfo
if (this.debug) {
console.log('platformInfo', JSON.stringify(platformInfo))
}
}
// 渠道信息
let channelInfo = null
const channelKey = data._id.appid + '_' + platformInfo._id + '_' + data._id.channel
if (this.channels && this.channels[channelKey]) {
channelInfo = this.channels[channelKey]
} else {
const channel = new Channel()
channelInfo = await channel.getChannelAndCreate(data._id.appid, platformInfo._id, data._id.channel)
if (!channelInfo || channelInfo.length === 0) {
channelInfo._id = ''
}
this.channels[channelKey] = channelInfo
if (this.debug) {
console.log('channelInfo', JSON.stringify(channelInfo))
}
}
// 版本信息
let versionInfo = null
const versionKey = data._id.appid + '_' + data._id.platform + '_' + data._id.version
if (this.versions && this.versions[versionKey]) {
versionInfo = this.versions[versionKey]
} else {
const version = new Version()
versionInfo = await version.getVersionAndCreate(data._id.appid, data._id.platform, data._id.version)
if (!versionInfo || versionInfo.length === 0) {
versionInfo._id = ''
}
this.versions[versionKey] = versionInfo
if (this.debug) {
console.log('versionInfo', JSON.stringify(versionInfo))
}
}
// 访问深度-用户数统计和访问次数
const pageMark = [1, 2, 3, 4, [5, 10], [10]]
const matchCondition = Object.assign(data._id, {
create_time: {
$gte: this.startTime,
$lte: this.endTime
}
})
const visitDepthData = {
visit_devices: {},
visit_users: {},
visit_times: {}
}
const userSessionLog = new UserSessionLog()
//根据各访问页面数区间统计
for (const pi in pageMark) {
let pageMarkCondition = {
page_count: pageMark[pi]
}
if (Array.isArray(pageMark[pi])) {
if (pageMark[pi].length === 2) {
pageMarkCondition = {
page_count: {
$gte: pageMark[pi][0],
$lte: pageMark[pi][1]
}
}
} else {
pageMarkCondition = {
page_count: {
$gt: pageMark[pi][0]
}
}
}
}
// 访问次数(会话次数)统计
const searchCondition = {
...matchCondition,
...pageMarkCondition
}
const vistRes = await this.aggregate(this.sessionLog.tableName, {
project: {
appid: 1,
version: 1,
platform: 1,
channel: 1,
page_count: 1,
create_time: 1
},
match: searchCondition,
group: {
_id: {},
total_visits: {
$sum: 1
}
}
})
if (this.debug) {
console.log('vistResCondtion', JSON.stringify(searchCondition))
console.log('vistRes', JSON.stringify(vistRes))
}
let vistCount = 0
if (vistRes.data.length > 0) {
vistCount = vistRes.data[0].total_visits
}
// 设备数统计
const deviceRes = await this.aggregate(this.sessionLog.tableName, {
project: {
appid: 1,
version: 1,
platform: 1,
channel: 1,
page_count: 1,
create_time: 1,
device_id: 1
},
match: searchCondition,
group: [{
_id: {
device_id: '$device_id'
}
}, {
_id: {},
total_devices: {
$sum: 1
}
}]
})
if (this.debug) {
console.log('searchCondition', JSON.stringify(searchCondition))
console.log('deviceRes', JSON.stringify(deviceRes))
}
let deviceCount = 0
if (deviceRes.data.length > 0) {
deviceCount = deviceRes.data[0].total_devices
}
// 用户数统计
const userRes = await this.aggregate(userSessionLog.tableName, {
project: {
appid: 1,
version: 1,
platform: 1,
channel: 1,
page_count: 1,
create_time: 1,
uid: 1
},
match: searchCondition,
group: [{
_id: {
uid: '$uid'
}
}, {
_id: {},
total_users: {
$sum: 1
}
}]
})
if (this.debug) {
console.log('userResCondtion', JSON.stringify(searchCondition))
console.log('userRes', JSON.stringify(userRes))
}
let userCount = 0
if (userRes.data.length > 0) {
userCount = userRes.data[0].total_users
}
const pageKey = 'p_' + (Array.isArray(pageMark[pi]) ? pageMark[pi][0] : pageMark[pi])
visitDepthData.visit_devices[pageKey] = deviceCount
visitDepthData.visit_users[pageKey] = userCount
visitDepthData.visit_times[pageKey] = vistCount
}
// 访问时长-用户数统计和访问次数
const durationMark = [
[0, 2],
[3, 5],
[6, 10],
[11, 20],
[21, 30],
[31, 50],
[51, 100],
[100]
]
const durationData = {
visit_devices: {},
visit_users: {},
visit_times: {}
}
//根据各访问时长区间统计
for (const di in durationMark) {
let durationMarkCondition = {
duration: durationMark[di]
}
if (Array.isArray(durationMark[di])) {
if (durationMark[di].length === 2) {
durationMarkCondition = {
duration: {
$gte: durationMark[di][0],
$lte: durationMark[di][1]
}
}
} else {
durationMarkCondition = {
duration: {
$gt: durationMark[di][0]
}
}
}
}
// 访问次数(会话次数)统计
const searchCondition = {
...matchCondition,
...durationMarkCondition
}
if (this.debug) {
console.log('searchCondition', JSON.stringify(searchCondition))
}
const vistRes = await this.aggregate(this.sessionLog.tableName, {
project: {
appid: 1,
version: 1,
platform: 1,
channel: 1,
duration: 1,
create_time: 1
},
match: searchCondition,
group: {
_id: {},
total_visits: {
$sum: 1
}
}
})
if (this.debug) {
console.log('vistRes', JSON.stringify(vistRes))
}
let vistCount = 0
if (vistRes.data.length > 0) {
vistCount = vistRes.data[0].total_visits
}
// 设备数统计
const deviceRes = await this.aggregate(this.sessionLog.tableName, {
project: {
appid: 1,
version: 1,
platform: 1,
channel: 1,
device_id: 1,
duration: 1,
create_time: 1
},
match: searchCondition,
group: [{
_id: {
device_id: '$device_id'
}
}, {
_id: {},
total_devices: {
$sum: 1
}
}]
})
if (this.debug) {
console.log('userRes', JSON.stringify(deviceRes))
}
let deviceCount = 0
if (deviceRes.data.length > 0) {
deviceCount = deviceRes.data[0].total_devices
}
// 用户数统计
const userRes = await this.aggregate(userSessionLog.tableName, {
project: {
appid: 1,
version: 1,
platform: 1,
channel: 1,
uid: 1,
duration: 1,
create_time: 1
},
match: searchCondition,
group: [{
_id: {
uid: '$uid'
}
}, {
_id: {},
total_users: {
$sum: 1
}
}]
})
if (this.debug) {
console.log('userRes', JSON.stringify(userRes))
}
let userCount = 0
if (userRes.data.length > 0) {
userCount = userRes.data[0].total_users
}
const pageKey = 's_' + (Array.isArray(durationMark[di]) ? durationMark[di][0] : durationMark[di])
durationData.visit_devices[pageKey] = deviceCount
durationData.visit_users[pageKey] = userCount
durationData.visit_times[pageKey] = vistCount
}
// 数据填充
const datetime = new DateTime()
const insertParams = {
appid: data._id.appid,
platform_id: platformInfo._id,
channel_id: channelInfo._id,
version_id: versionInfo._id,
visit_depth_data: visitDepthData,
duration_data: durationData,
stat_date: datetime.getDate('Ymd', this.startTime),
start_time: this.startTime,
end_time: this.endTime
}
this.fillData.push(insertParams)
return insertParams
}
}

@ -0,0 +1,83 @@
/**
* @class Page 页面模型
*/
const BaseMod = require('./base')
const {
parseUrl
} = require('../../shared')
const {
DateTime
} = require('../lib')
module.exports = class Page extends BaseMod {
constructor() {
super()
this.tableName = 'pages'
}
/**
* 获取页面信息
* @param {String} appid
* @param {String} url 页面地址
*/
async getPage(appid, url) {
const cacheKey = 'uni-stat-page-' + appid + '-' + url
let pageData = await this.getCache(cacheKey)
if (!pageData) {
const pageInfo = await this.getCollection(this.tableName).where({
appid: appid,
path: url
}).limit(1).get()
pageData = []
if (pageInfo.data.length > 0) {
pageData = pageInfo.data[0]
await this.setCache(cacheKey, pageData)
}
}
return pageData
}
/**
* 获取页面信息不存在则创建
* @param {String} appid
* @param {String} url 页面地址
* @param {Object} title 页面标题
*/
async getPageAndCreate(appid, url, title) {
//获取url信息
const urlInfo = parseUrl(url)
if (!urlInfo) {
return false
}
const baseurl = urlInfo.path
const pageInfo = await this.getPage(appid, baseurl)
//页面不存在则创建
if (pageInfo.length === 0) {
const thisTime = new DateTime().getTime()
const insertParam = {
appid: appid,
path: baseurl,
title: title,
page_params: [],
create_time: thisTime,
update_time: thisTime
}
const res = await this.insert(this.tableName, insertParam)
if (res && res.id) {
return Object.assign(insertParam, {
_id: res.id
})
}
} else if (!pageInfo.title && title) {
const cacheKey = 'uni-stat-page-' + appid + '-' + baseurl
await this.clearCache(cacheKey)
await this.update(this.tableName, {
title: title
}, {
_id: pageInfo._id
})
}
return pageInfo
}
}

@ -0,0 +1,186 @@
/**
* @class PageLog 页面日志模型
*/
const BaseMod = require('./base')
const Page = require('./page')
const Platform = require('./platform')
const Channel = require('./channel')
const SessionLog = require('./sessionLog')
const {
DateTime
} = require('../lib')
const {
parseUrl
} = require('../../shared')
module.exports = class PageLog extends BaseMod {
constructor() {
super()
this.tableName = 'page-logs'
this.sessionLogInfo = []
}
/**
* 页面日志数据填充
* @param {Object} reportParams 上报参数
*/
async fill(reportParams) {
let params;
let sessionKey
let sessionLogKey
let sessionLogInfo
let pageKey
let pageInfo
let referPageInfo
const sessionData = []
const pageData = []
const fillParams = []
const sessionLog = new SessionLog()
const page = new Page()
const platform = new Platform()
const dateTime = new DateTime()
const channel = new Channel()
for (const pk in reportParams) {
params = reportParams[pk]
if (['3', '4'].includes(params.lt) && !params.url && params.urlref) {
params.url = params.urlref
}
// 页面信息
pageKey = params.ak + params.url
if (pageData[pageKey]) {
pageInfo = pageData[pageKey]
} else {
pageInfo = await page.getPageAndCreate(params.ak, params.url, params.ttpj)
if (!pageInfo || pageInfo.length === 0) {
console.log('Not found this page by param:', JSON.stringify(params))
continue
}
pageData[pageKey] = pageInfo
}
// 会话日志,暂存下会话数据,减少读库
sessionKey = params.ak + params.did + params.p
if (!this.sessionLogInfo[sessionKey]) {
sessionLogInfo = await sessionLog.getSession(params)
if (sessionLogInfo.code) {
return sessionLogInfo
}
if (this.debug) {
console.log('sessionLogInfo', JSON.stringify(sessionLogInfo))
}
this.sessionLogInfo[sessionKey] = sessionLogInfo
} else {
sessionLogInfo = this.sessionLogInfo[sessionKey]
}
// 会话数据
sessionLogKey = sessionLogInfo.data.sessionLogId.toString()
if (!sessionData[sessionLogKey]) {
//临时存储减少查询次数
sessionData[sessionLogKey] = {
pageCount: sessionLogInfo.data.pageCount + 1,
addPageCount: 1,
createTime: sessionLogInfo.data.createTime,
pageId: pageInfo._id,
uid: sessionLogInfo.data.uid
}
if (this.debug) {
console.log('add sessionData - ' + sessionLogKey, sessionData)
}
} else {
sessionData[sessionLogKey].pageCount += 1
sessionData[sessionLogKey].addPageCount += 1
sessionData[sessionLogKey].pageId = pageInfo._id
if (this.debug) {
console.log('update sessionData - ' + sessionLogKey, sessionData)
}
}
// 上级页面信息
pageKey = params.ak + params.urlref
if (pageData[pageKey]) {
referPageInfo = pageData[pageKey]
} else {
referPageInfo = await page.getPageAndCreate(params.ak, params.urlref, params.ttpj)
if (!referPageInfo || referPageInfo.length === 0) {
referPageInfo = {_id:''}
}
pageData[pageKey] = referPageInfo
}
//当前页面url信息
const urlInfo = parseUrl(params.url)
// 填充数据
fillParams.push({
appid: params.ak,
version: params.v ? params.v : '',
platform: platform.getPlatformCode(params.ut, params.p),
channel: channel.getChannelCode(params),
device_id: params.did,
uid: params.uid ? params.uid : '',
session_id: sessionLogInfo.data.sessionLogId,
page_id: pageInfo._id,
query_string: urlInfo.query,
//上级页面相关
previous_page_id: referPageInfo._id,
previous_page_duration: params.urlref_ts ? parseInt(params.urlref_ts) : 0,
previous_page_is_entry: referPageInfo._id === sessionLogInfo.data.entryPageId ? 1 : 0,
create_time: dateTime.getTime()
})
}
if (fillParams.length === 0) {
console.log('No page params')
return {
code: 200,
msg: 'Invild param'
}
}
//日志数据入库
const res = await this.insert(this.tableName, fillParams)
if (res && res.inserted) {
// 更新会话数据
const nowTime = dateTime.getTime()
for (const sid in sessionData) {
await sessionLog.updateSession(sid, sessionData[sid])
}
return {
code: 0,
msg: 'success'
}
} else {
return {
code: 500,
msg: 'Filled error'
}
}
}
/**
* 页面日志清理
* @param {Number} days 页面日志保留天数
*/
async clean(days) {
days = Math.max(parseInt(days), 1)
console.log('clean page logs - day:', days)
const dateTime = new DateTime()
const res = await this.delete(this.tableName, {
create_time: {
$lt: dateTime.getTimeBySetDays(0 - days)
}
})
if (!res.code) {
console.log('clean page log:', res)
}
return res
}
}

@ -0,0 +1,522 @@
/**
* @class PageResult 页面结果统计模型
*/
const BaseMod = require('./base')
const Platform = require('./platform')
const Channel = require('./channel')
const Version = require('./version')
const SessionLog = require('./sessionLog')
const PageLog = require('./pageLog')
const ShareLog = require('./shareLog')
const {
DateTime
} = require('../lib')
module.exports = class PageResult extends BaseMod {
constructor() {
super()
this.tableName = 'page-result'
this.platforms = []
this.channels = []
this.versions = []
}
/**
* 数据统计
* @param {String} type 统计类型 hour实时统计 day按天统计week按周统计 month按月统计
* @param {Date|Time} date 指定日期或时间戳
* @param {Boolean} reset 是否重置为ture时会重置该批次数据
*/
async stat(type, date, reset) {
//允许的类型
const allowedType = ['day']
if (!allowedType.includes(type)) {
return {
code: 1002,
msg: 'This type is not allowed'
}
}
this.fillType = type
//获取当前统计的时间范围
const dateTime = new DateTime()
const dateDimension = dateTime.getTimeDimensionByType(type, -1, date)
this.startTime = dateDimension.startTime
this.endTime = dateDimension.endTime
if (this.debug) {
console.log('dimension time', this.startTime + '--' + this.endTime)
}
// 查看当前时间段日志是否已存在,防止重复执行
if (!reset) {
const checkRes = await this.getCollection(this.tableName).where({
start_time: this.startTime,
end_time: this.endTime
}).get()
if (checkRes.data.length > 0) {
console.error('This page stat log have exists')
return {
code: 1003,
msg: 'This page stat log have existed'
}
}
} else {
const delRes = await this.delete(this.tableName, {
start_time: this.startTime,
end_time: this.endTime
})
console.log('Delete old data result:', JSON.stringify(delRes))
}
// 数据获取
this.pageLog = new PageLog()
const statRes = await this.aggregate(this.pageLog.tableName, {
project: {
appid: 1,
version: 1,
platform: 1,
channel: 1,
page_id: 1,
create_time: 1
},
match: {
create_time: {
$gte: this.startTime,
$lte: this.endTime
}
},
group: {
_id: {
appid: '$appid',
version: '$version',
platform: '$platform',
channel: '$channel',
page_id: '$page_id'
},
visit_times: {
$sum: 1
}
},
sort: {
visit_times: 1
},
getAll: true
})
let res = {
code: 0,
msg: 'success'
}
if (this.debug) {
console.log('Page statRes', JSON.stringify(statRes))
}
if (statRes.data.length > 0) {
this.fillData = []
//获取填充数据
for (const i in statRes.data) {
await this.fill(statRes.data[i])
}
//数据批量入库
if (this.fillData.length > 0) {
res = await this.batchInsert(this.tableName, this.fillData)
}
}
return res
}
/**
* 页面统计数据填充
* @param {Object} data 统计数据
*/
async fill(data) {
// 平台信息
let platformInfo = null
if (this.platforms && this.platforms[data._id.platform]) {
//暂存下数据,减少读库
platformInfo = this.platforms[data._id.platform]
} else {
const platform = new Platform()
platformInfo = await platform.getPlatformAndCreate(data._id.platform, null)
if (!platformInfo || platformInfo.length === 0) {
platformInfo._id = ''
}
this.platforms[data._id.platform] = platformInfo
if (this.debug) {
console.log('platformInfo', JSON.stringify(platformInfo))
}
}
// 渠道信息
let channelInfo = null
const channelKey = data._id.appid + '_' + platformInfo._id + '_' + data._id.channel
if (this.channels && this.channels[channelKey]) {
channelInfo = this.channels[channelKey]
} else {
const channel = new Channel()
channelInfo = await channel.getChannelAndCreate(data._id.appid, platformInfo._id, data._id.channel)
if (!channelInfo || channelInfo.length === 0) {
channelInfo._id = ''
}
this.channels[channelKey] = channelInfo
if (this.debug) {
console.log('channelInfo', JSON.stringify(channelInfo))
}
}
// 版本信息
let versionInfo = null
const versionKey = data._id.appid + '_' + data._id.platform + '_' + data._id.version
if (this.versions && this.versions[versionKey]) {
versionInfo = this.versions[versionKey]
} else {
const version = new Version()
versionInfo = await version.getVersionAndCreate(data._id.appid, data._id.platform, data._id.version)
if (!versionInfo || versionInfo.length === 0) {
versionInfo._id = ''
}
this.versions[versionKey] = versionInfo
if (this.debug) {
console.log('versionInfo', JSON.stringify(versionInfo))
}
}
const matchCondition = data._id
Object.assign(matchCondition, {
create_time: {
$gte: this.startTime,
$lte: this.endTime
}
})
if (this.debug) {
console.log('matchCondition', JSON.stringify(matchCondition))
}
// 当前页面访问设备数
const statPageDeviceRes = await this.aggregate(this.pageLog.tableName, {
project: {
appid: 1,
version: 1,
platform: 1,
channel: 1,
device_id: 1,
page_id: 1,
create_time: 1
},
match: matchCondition,
group: [{
_id: {
device_id: '$device_id'
}
}, {
_id: {},
total_devices: {
$sum: 1
}
}]
})
let pageVisitDevices = 0
if (statPageDeviceRes.data.length > 0) {
pageVisitDevices = statPageDeviceRes.data[0].total_devices
}
// 当前页面访问人数
const statPageUserRes = await this.aggregate(this.pageLog.tableName, {
project: {
appid: 1,
version: 1,
platform: 1,
channel: 1,
uid: 1,
page_id: 1,
create_time: 1
},
match: {
...matchCondition,
uid: {
$ne: ''
}
},
group: [{
_id: {
uid: '$uid'
}
}, {
_id: {},
total_users: {
$sum: 1
}
}]
})
let pageVisitUsers = 0
if (statPageUserRes.data.length > 0) {
pageVisitUsers = statPageUserRes.data[0].total_users
}
// 退出次数
const sessionLog = new SessionLog()
let existTimes = 0
const existRes = await this.getCollection(sessionLog.tableName).where({
appid: data._id.appid,
version: data._id.version,
platform: data._id.platform,
channel: data._id.channel,
exit_page_id: data._id.page_id,
create_time: {
$gte: this.startTime,
$lte: this.endTime
}
}).count()
if (existRes && existRes.total > 0) {
existTimes = existRes.total
}
// 访问时长
const statPageDurationRes = await this.aggregate(this.pageLog.tableName, {
project: {
appid: 1,
version: 1,
platform: 1,
channel: 1,
previous_page_id: 1,
previous_page_duration: 1,
create_time: 1
},
match: {
appid: data._id.appid,
version: data._id.version,
platform: data._id.platform,
channel: data._id.channel,
previous_page_id: data._id.page_id,
create_time: {
$gte: this.startTime,
$lte: this.endTime
}
},
group: {
_id: {},
total_duration: {
$sum: '$previous_page_duration'
}
}
})
let totalDuration = 0
if (statPageDurationRes.data.length > 0) {
totalDuration = statPageDurationRes.data[0].total_duration
}
// 分享次数
const shareLog = new ShareLog()
const statShareRes = await this.aggregate(shareLog.tableName, {
project: {
appid: 1,
version: 1,
platform: 1,
channel: 1,
page_id: 1,
create_time: 1
},
match: {
appid: data._id.appid,
version: data._id.version,
platform: data._id.platform,
channel: data._id.channel,
page_id: data._id.page_id,
create_time: {
$gte: this.startTime,
$lte: this.endTime
}
},
group: {
_id: {},
share_count: {
$sum: 1
}
}
})
let shareCount = 0
if (statShareRes.data.length > 0) {
shareCount = statShareRes.data[0].share_count
}
// 作为入口页的总次数和总访问时长
const statPageEntryCountRes = await this.aggregate(this.pageLog.tableName, {
project: {
appid: 1,
version: 1,
platform: 1,
channel: 1,
previous_page_id: 1,
previous_page_duration: 1,
previous_page_is_entry: 1,
create_time: 1
},
match: {
appid: data._id.appid,
version: data._id.version,
platform: data._id.platform,
channel: data._id.channel,
previous_page_id: data._id.page_id,
previous_page_is_entry: 1,
create_time: {
$gte: this.startTime,
$lte: this.endTime
}
},
group: {
_id: {},
entry_count: {
$sum: 1
},
entry_duration: {
$sum: '$previous_page_duration'
}
}
})
let entryCount = 0
let entryDuration = 0
if (statPageEntryCountRes.data.length > 0) {
entryCount = statPageEntryCountRes.data[0].entry_count
entryDuration = statPageEntryCountRes.data[0].entry_duration
}
// 作为入口页的总设备数
const statPageEntryDevicesRes = await this.aggregate(this.pageLog.tableName, {
project: {
appid: 1,
version: 1,
platform: 1,
channel: 1,
device_id: 1,
previous_page_id: 1,
previous_page_is_entry: 1,
create_time: 1
},
match: {
appid: data._id.appid,
version: data._id.version,
platform: data._id.platform,
channel: data._id.channel,
previous_page_id: data._id.page_id,
previous_page_is_entry: 1,
create_time: {
$gte: this.startTime,
$lte: this.endTime
}
},
group: [{
_id: {
device_id: '$device_id'
}
}, {
_id: {},
entry_devices: {
$sum: 1
}
}]
})
let entryDevices = 0
if (statPageEntryDevicesRes.data.length > 0) {
entryDevices = statPageEntryDevicesRes.data[0].entry_devices
}
// 作为入口页的总人数
const statPageEntryUsersRes = await this.aggregate(this.pageLog.tableName, {
project: {
appid: 1,
version: 1,
platform: 1,
channel: 1,
uid: 1,
previous_page_id: 1,
previous_page_is_entry: 1,
create_time: 1
},
match: {
appid: data._id.appid,
version: data._id.version,
platform: data._id.platform,
channel: data._id.channel,
previous_page_id: data._id.page_id,
previous_page_is_entry: 1,
uid: {
$ne: ''
},
create_time: {
$gte: this.startTime,
$lte: this.endTime
}
},
group: [{
_id: {
uid: '$uid'
}
}, {
_id: {},
entry_users: {
$sum: 1
}
}]
})
let entryUsers = 0
if (statPageEntryUsersRes.data.length > 0) {
entryUsers = statPageEntryUsersRes.data[0].entry_users
}
// 跳出率
let bounceTimes = 0
const bounceRes = await this.getCollection(sessionLog.tableName).where({
appid: data._id.appid,
version: data._id.version,
platform: data._id.platform,
channel: data._id.channel,
entry_page_id: data._id.page_id,
page_count: 1,
create_time: {
$gte: this.startTime,
$lte: this.endTime
}
}).count()
if (bounceRes && bounceRes.total > 0) {
bounceTimes = bounceRes.total
}
let bounceRate = 0
if (bounceTimes > 0 && data.visit_times > 0) {
bounceRate = bounceTimes * 100 / data.visit_times
bounceRate = parseFloat(bounceRate.toFixed(2))
}
// 数据填充
const datetime = new DateTime()
const insertParams = {
appid: data._id.appid,
platform_id: platformInfo._id,
channel_id: channelInfo._id,
version_id: versionInfo._id,
page_id: data._id.page_id,
visit_times: data.visit_times,
visit_devices: pageVisitDevices,
visit_users: pageVisitUsers,
exit_times: existTimes,
duration: totalDuration > 0 ? totalDuration : 1,
share_count: shareCount,
entry_users: entryUsers,
entry_devices: entryDevices,
entry_count: entryCount,
entry_duration: entryDuration,
bounce_rate: bounceRate,
dimension: this.fillType,
stat_date: datetime.getDate('Ymd', this.startTime),
start_time: this.startTime,
end_time: this.endTime
}
this.fillData.push(insertParams)
return insertParams
}
}

@ -0,0 +1,160 @@
/**
* @class Platform 应用平台模型
*/
const BaseMod = require('./base')
const {
DateTime
} = require('../lib')
module.exports = class Platform extends BaseMod {
constructor() {
super()
this.tableName = 'app-platforms'
}
/**
* 获取平台信息
* @param {String} platform 平台代码
* @param {String} os 系统
*/
async getPlatform(platform, os) {
const cacheKey = 'uni-stat-platform-' + platform + '-' + os
let platformData = await this.getCache(cacheKey)
if (!platformData) {
const platformCode = this.getPlatformCode(platform, os)
const platformInfo = await this.getCollection(this.tableName).where({
code: platformCode
}).limit(1).get()
platformData = []
if (platformInfo.data.length > 0) {
platformData = platformInfo.data[0]
await this.setCache(cacheKey, platformData)
}
}
return platformData
}
/**
* 获取平台信息没有则创建
* @param {String} platform 平台代码
* @param {String} os 系统
*/
async getPlatformAndCreate(platform, os) {
if (!platform) {
return false
}
const platformInfo = await this.getPlatform(platform, os)
if (platformInfo.length === 0) {
const platformCode = this.getPlatformCode(platform, os)
const insertParam = {
code: platformCode,
name: platformCode,
create_time: new DateTime().getTime()
}
const res = await this.insert(this.tableName, insertParam)
if (res && res.id) {
return Object.assign(insertParam, {
_id: res.id
})
}
}
return platformInfo
}
/**
* 获取平台代码
* @param {String} platform 平台代码
* @param {String} os 系统
*/
getPlatformCode(platform, os) {
let platformCode = platform
//兼容客户端上报参数
switch(platform) {
//h5|web
case 'h5':
platformCode = 'web'
break
//微信小程序
case 'wx':
platformCode = 'mp-weixin'
break
//百度小程序
case 'bd':
platformCode = 'mp-baidu'
break
//支付宝小程序
case 'ali':
platformCode = 'mp-alipay'
break
//字节跳动小程序
case 'tt':
platformCode = 'mp-toutiao'
break
//qq小程序
case 'qq':
platformCode = 'mp-qq'
break
//快应用联盟
case 'qn':
platformCode = 'quickapp-webview-union'
break
//快应用(webview)
case 'qw':
platformCode = 'quickapp-webview'
break
//快应用华为
case 'qi':
platformCode = 'quickapp-webview-huawei'
break
//360小程序
case '360':
platformCode = 'mp-360'
break
//京东小程序
case 'jd':
platformCode = 'mp-jd'
break
//钉钉小程序
case 'dt':
platformCode = 'mp-dingtalk'
break
//快手小程序
case 'ks':
platformCode = 'mp-kuaishou'
break
//飞书小程序
case 'lark':
platformCode = 'mp-lark'
break
//原生应用
case 'n':
case 'app-plus':
case 'app':
os = this.getOsName(os)
if (os === 'ios') {
platformCode = 'ios'
} else {
platformCode = 'android'
}
break
}
return platformCode
}
/**
* 获取系统名称
* @param {Object} os系统标识
*/
getOsName(os) {
if(!os) {
return ''
}
//兼容老版上报参数
const osSetting = {
i: 'ios',
a: 'android'
}
return osSetting[os] ? osSetting[os] : os
}
}

@ -0,0 +1,20 @@
/**
* @class RunErrors 运行错误日志
*/
const BaseMod = require('./base')
module.exports = class RunErrors extends BaseMod {
constructor() {
super()
this.tableName = 'run-errors'
}
/**
* 创建日志
* @param {Object} params 参数
*/
async create(params) {
if (!params) return
const res = await this.insert(this.tableName, params)
return res
}
}

@ -0,0 +1,80 @@
/**
* @class Scenes 场景值模型
*/
const BaseMod = require('./base')
const Platform = require('./platform')
module.exports = class Scenes extends BaseMod {
constructor() {
super()
this.tableName = 'mp-scenes'
this.defualtCode = '1001'
}
/**
* 获取场景值
* @param {String} platform 平台代码
* @param {String} code 场景值代码
*/
async getScenes(platform, code) {
const cacheKey = 'uni-stat-scenes-' + platform + '-' + code
let scenesData = await this.getCache(cacheKey)
if (!scenesData) {
const scenesInfo = await this.getCollection(this.tableName).where({
platform: platform,
scene_code: code
}).limit(1).get()
scenesData = []
if (scenesInfo.data.length > 0) {
scenesData = scenesInfo.data[0]
await this.setCache(cacheKey, scenesData)
}
}
return scenesData
}
/**
* 通过平台编号获取场景值
* @param {String} platformId 平台编号
* @param {String} code 场景值代码
*/
async getScenesByPlatformId(platformId, code) {
const platform = new Platform()
let platformInfo = await this.getCollection(platform.tableName).where({
_id: platformId
}).limit(1).get()
let scenesData
if (platformInfo.data.length > 0) {
platformInfo = platformInfo.data[0]
scenesData = await this.getScenes(platformInfo.code, code)
} else {
scenesData = []
}
return scenesData
}
/**
* 获取场景值名称
* @param {String} platform 平台代码
* @param {String} code 场景值代码
*/
async getScenesName(platform, code) {
const scenesData = await this.getScenes(platform, code)
if (scenesData.length === 0) {
return ''
}
return scenesData.scene_name
}
/**
* 通过平台编号获取场景值名称
* @param {String} platformId 平台编号
* @param {String} code 场景值代码
*/
async getScenesNameByPlatformId(platformId, code) {
const scenesData = await this.getScenesByPlatformId(platformId, code)
if (scenesData.length === 0) {
return code === this.defualtCode ? '默认' : ''
}
return scenesData.scene_name
}
}

@ -0,0 +1,343 @@
/**
* @class SessionLog 基础会话日志模型
*/
const BaseMod = require('./base')
const Page = require('./page')
const Platform = require('./platform')
const Channel = require('./channel')
const UserSessionLog = require('./userSessionLog')
const Device = require('./device')
const {
DateTime
} = require('../lib')
module.exports = class SessionLog extends BaseMod {
constructor() {
super()
this.tableName = 'session-logs'
}
/**
* 会话日志批量填充
* @param {Object} reportParams 上报参数
*/
async batchFill(reportParams) {
let params, pageInfo, nowTime, firstVistTime, lastVistTime;
const fillParams = []
const page = new Page()
const platform = new Platform()
const dateTime = new DateTime()
const channel = new Channel()
const device = new Device()
let res
for (const pk in reportParams) {
params = reportParams[pk]
res = await this.fill(params)
if (res.code) {
console.error(res.msg)
} else {
//添加设备信息
await device.setDevice(params)
}
}
return res
}
/**
* 会话日志填充
* @param {Object} params 上报参数
*/
async fill(params) {
// 应用信息
if (!params.ak) {
return {
code: 200,
msg: 'Parameter "ak" not found'
}
}
// 平台信息
if (!params.ut) {
return {
code: 200,
msg: 'Parameter "ut" not found'
}
}
// 设备信息
if (!params.did) {
return {
code: 200,
msg: 'Parameter "did" not found'
}
}
// 页面信息
const page = new Page()
const pageInfo = await page.getPageAndCreate(params.ak, params.url, params.ttpj)
if (!pageInfo || pageInfo.length === 0) {
return {
code: 300,
msg: 'Not found this entry page'
}
}
if (this.debug) {
console.log('pageInfo', JSON.stringify(pageInfo))
}
const platform = new Platform()
const dateTime = new DateTime()
const channel = new Channel()
const nowTime = dateTime.getTime()
const firstVistTime = params.fvts ? dateTime.strToTime(params.fvts) : nowTime
const lastVistTime = (params.lvts && params.lvts !== '0') ? dateTime.strToTime(params.lvts) : 0
const fillParams = {
appid: params.ak,
version: params.v ? params.v : '',
platform: platform.getPlatformCode(params.ut, params.p),
channel: channel.getChannelCode(params),
type: params.cst ? parseInt(params.cst) : 0,
// 访问设备
device_id: params.did,
//是否为首次访问判断标准最后一次访问时间为0
is_first_visit: (params.lt === '1' && !lastVistTime) ? 1 : 0,
first_visit_time: firstVistTime,
last_visit_time: nowTime,
visit_count: params.tvc ? parseInt(params.tvc) : 1,
// 用户相关
last_visit_user_id: params.uid ? params.uid : '',
// 页面相关
entry_page_id: pageInfo._id,
exit_page_id: pageInfo._id,
page_count: 0,
event_count: 0,
duration: 1,
// 版本
sdk_version: params.mpsdk ? params.mpsdk : '',
platform_version: params.mpv ? params.mpv : '',
// 设备相关
device_os_name: params.on ? params.on : platform.getOsName(params.p),
device_os_version: params.sv ? params.sv : '',
device_vendor: params.brand ? params.brand : '',
device_model: params.md ? params.md : '',
device_language: params.lang ? params.lang : '',
device_pixel_ratio: params.pr ? params.pr : '',
device_window_width: params.ww ? params.ww : '',
device_window_height: params.wh ? params.wh : '',
device_screen_width: params.sw ? params.sw : '',
device_screen_height: params.sh ? params.sh : '',
// 地区相关
location_ip: params.ip ? params.ip : '',
location_latitude: params.lat ? parseFloat(params.lat) : -1,
location_longitude: params.lng ? parseFloat(params.lng) : -1,
location_country: params.cn ? params.cn : '',
location_province: params.pn ? params.pn : '',
location_city: params.ct ? params.ct : '',
is_finish: 0,
create_time: nowTime
}
if(this.isHaveOldDeviceId(params)) {
fillParams.old_device_id = params.odid
}
const res = await this.insert(this.tableName, fillParams)
if (res && res.id) {
//填充用户的会话日志
if (params.uid) {
await new UserSessionLog().fill({
...params,
page_id: pageInfo._id,
sid: res.id
})
}
return {
code: 0,
msg: 'success',
data: {
pageId: pageInfo._id,
sessionLogId: res.id,
entryPageId: fillParams.entry_page_id,
eventCount: fillParams.event_count,
startTime: fillParams.first_visit_time,
createTime: fillParams.create_time,
pageCount: fillParams.page_count,
uid: fillParams.last_visit_user_id
}
}
} else {
return {
code: 500,
msg: 'Session log filled error'
}
}
}
/**
* 判断是否包含老版本sdk生成的device_id, 此功能用于处理前端sdk升级后 device_id 发生变化导致日活留存数据不准确的问题
* @param {Object} params
*/
isHaveOldDeviceId(params) {
if(params.odid && params.odid !== params.did && params.odid !== 'YluY92BA6nJ6NfixI77sFQ%3D%3D&ie=1') {
console.log('params.odid', params.odid)
return true
}
return false
}
/**
* 获取会话
* @param {Object} params 上报参数
*/
async getSession(params) {
// 页面信息
const page = new Page()
const pageInfo = await page.getPageAndCreate(params.ak, params.url, params.ttpj)
if (!pageInfo || pageInfo.length === 0) {
return {
code: 300,
msg: 'Not found this entry page'
}
}
const platformObj = new Platform()
const platform = platformObj.getPlatformCode(params.ut, params.p)
// 查询日志
const sessionLogInfo = await this.getCollection(this.tableName).where({
appid: params.ak,
platform: platform,
device_id: params.did,
is_finish: 0
}).orderBy('create_time', 'desc').limit(1).get()
if (sessionLogInfo.data.length > 0) {
const userSessionLog = new UserSessionLog()
const sessionLogInfoData = sessionLogInfo.data[0]
// 最后一次访问时间距现在超过半小时算上次会话已结束并生成一次新的会话
let sessionExpireTime = this.getConfig('sessionExpireTime')
sessionExpireTime = sessionExpireTime ? sessionExpireTime : 1800
const sessionTime = new DateTime().getTime() - sessionLogInfoData.last_visit_time
if (sessionTime >= sessionExpireTime * 1000) {
if (this.debug) {
console.log('session log time expired', sessionTime)
}
await this.update(this.tableName, {
is_finish: 1
}, {
appid: params.ak,
platform: platform,
device_id: params.did,
is_finish: 0
})
//关闭用户会话
await userSessionLog.closeUserSession()
return await this.fill(params)
} else {
//如果当前会话切换了用户则生成新的用户会话
if (params.uid != sessionLogInfoData.last_visit_user_id) {
await userSessionLog.checkUserSession({
...params,
page_id: pageInfo._id,
sid: sessionLogInfoData._id,
last_visit_user_id: sessionLogInfoData.last_visit_user_id
})
await this.update(this.tableName, {
last_visit_user_id: params.uid ? params.uid : ''
}, {
_id: sessionLogInfoData._id
})
}
return {
code: 0,
msg: 'success',
data: {
pageId: pageInfo._id,
sessionLogId: sessionLogInfoData._id,
entryPageId: sessionLogInfoData.entry_page_id,
eventCount: sessionLogInfoData.event_count,
startTime: sessionLogInfoData.first_visit_time,
createTime: sessionLogInfoData.create_time,
pageCount: sessionLogInfoData.page_count,
uid: sessionLogInfoData.last_visit_user_id
}
}
}
} else {
return await this.fill(params)
}
}
/**
* 更新会话信息
* @param {String} sid 会话编号
* @param {Object} data 更新数据
*/
async updateSession(sid, data) {
const nowTime = new DateTime().getTime()
const accessTime = nowTime - data.createTime
const accessSenconds = accessTime > 1000 ? parseInt(accessTime / 1000) : 1
const updateData = {
last_visit_time: nowTime,
duration: accessSenconds,
}
//访问页面数量
if (data.addPageCount) {
updateData.page_count = data.pageCount
}
//最终访问的页面编号
if (data.pageId) {
updateData.exit_page_id = data.pageId
}
//产生事件次数
if (data.eventCount) {
updateData.event_count = data.eventCount
}
if (this.debug) {
console.log('update session log by sid-' + sid, updateData)
}
//更新会话
await this.update(this.tableName, updateData, {
_id: sid
})
//更新用户会话
if (data.uid) {
data.nowTime = nowTime
await new UserSessionLog().updateUserSession(sid, data)
}
return true
}
/**
* 清理日志数据
* @param {Number} days 保留天数, 留存统计需要计算30天后留存率因此至少应保留31天的日志数据
*/
async clean(days) {
days = Math.max(parseInt(days), 1)
console.log('clean session logs - day:', days)
const dateTime = new DateTime()
const res = await this.delete(this.tableName, {
create_time: {
$lt: dateTime.getTimeBySetDays(0 - days)
}
})
if (!res.code) {
console.log('clean session log:', res)
}
return res
}
}

@ -0,0 +1,44 @@
/**
* @class Version 应用版本模型
*/
const BaseMod = require('./base')
const {
DateTime
} = require('../lib')
module.exports = class Setting extends BaseMod {
constructor() {
super()
this.tableName = 'opendb-tempdata'
this.tablePrefix = false
this.settingKey = "uni-stat-setting"
}
/**
* 获取统计云端配置
*/
async getSetting() {
const res = await this.getCollection(this.tableName).doc(this.settingKey).get();
if (res.data && res.data[0] && res.data[0].value) {
return res.data[0].value;
} else {
return {
mode: "open",
day: 7
};
}
}
/**
* 检测N天内是否有设备访问记录如果有则返回true否则返回false
*/
async checkAutoRun(obj = {}) {
let {
day = 7
} = obj;
const _ = this.dbCmd;
let nowTime = Date.now();
const res = await this.getCollection("uni-stat-session-logs").where({
create_time: _.gte(nowTime - 1000 * 3600 * 24 * day)
}).count();
return res.total > 0 ? true : false;
}
}

@ -0,0 +1,104 @@
/**
* @class ShareLog 分享日志模型
*/
const BaseMod = require('./base')
const Platform = require('./platform')
const Channel = require('./channel')
const SessionLog = require('./sessionLog')
const {
DateTime
} = require('../lib')
module.exports = class ShareLog extends BaseMod {
constructor() {
super()
this.tableName = 'share-logs'
}
/**
* 分析日志填充
* @param {Object} reportParams 上报参数
* @param {Object} sessionLogData 会话日志数据此参数传递可减少数据库查询
*/
async fill(reportParams, sessionLogData) {
let params, sessionLogInfo, sessionKey;
const fillParams = []
const sessionLog = new SessionLog()
const platform = new Platform()
const dateTime = new DateTime()
const channel = new Channel()
for (const rk in reportParams) {
params = reportParams[rk]
//暂存下会话数据,减少读库
sessionKey = params.ak + params.did + params.p
if (!sessionLogData[sessionKey]) {
// 会话日志
sessionLogInfo = await sessionLog.getSession(params)
if (sessionLogInfo.code) {
return sessionLogInfo
}
if (this.debug) {
console.log('sessionLogInfo', JSON.stringify(sessionLogInfo))
}
sessionLogData[sessionKey] = sessionLogInfo
} else {
sessionLogInfo = sessionLogData[sessionKey]
}
// 填充数据
fillParams.push({
appid: params.ak,
version: params.v ? params.v : '',
platform: platform.getPlatformCode(params.ut, params.p),
channel: channel.getChannelCode(params),
device_id: params.did,
uid: params.uid ? params.uid : '',
session_id: sessionLogInfo.data.sessionLogId,
page_id: sessionLogInfo.data.pageId,
create_time: dateTime.getTime()
})
}
if (fillParams.length === 0) {
return {
code: 200,
msg: 'Invild param'
}
}
const res = await this.insert(this.tableName, fillParams)
if (res && res.inserted) {
return {
code: 0,
msg: 'success'
}
} else {
return {
code: 500,
msg: 'Filled error'
}
}
}
/**
* 分享日志清理
* @param {Number} days 保留天数
*/
async clean(days) {
days = Math.max(parseInt(days), 1)
console.log('clean share logs - day:', days)
const dateTime = new DateTime()
const res = await this.delete(this.tableName, {
create_time: {
$lt: dateTime.getTimeBySetDays(0 - days)
}
})
if (!res.code) {
console.log('clean share log:', res)
}
return res
}
}

@ -0,0 +1,12 @@
/**
* 表名
*/
const dbName = {
uniIdUsers: 'uni-id-users', // 支付订单明细表
uniPayOrders: 'uni-pay-orders', // 支付订单明细表
uniStatPayResult: 'uni-stat-pay-result', // 统计结果存储表
uniStatSessionLogs: 'uni-stat-session-logs', // 设备会话日志表(主要用于统计访问设备数)
uniStatUserSessionLogs: 'uni-stat-user-session-logs', // 用户会话日志表(主要用于统计访问人数)
};
module.exports = dbName;

@ -0,0 +1,10 @@
/**
* 数据库操作
*/
module.exports = {
uniIdUsers: require('./uniIdUsers'),
uniPayOrders: require('./uniPayOrders'),
uniStatPayResult: require('./uniStatPayResult'),
uniStatSessionLogs: require('./uniStatSessionLogs'),
uniStatUserSessionLogs: require('./uniStatUserSessionLogs'),
}

@ -0,0 +1,62 @@
/**
* 数据库操作
*/
const dbName = require("./config");
let db = uniCloud.database();
let _ = db.command;
let $ = _.aggregate;
class Dao {
async count(whereJson) {
let dbRes = await db.collection(dbName.uniIdUsers).where(whereJson).count()
return dbRes && dbRes.total ? dbRes.total : 0;
}
async countNewUserOrder(obj) {
let {
whereJson,
status
} = obj;
let dbRes = await db.collection(dbName.uniIdUsers).aggregate()
.match(whereJson)
.lookup({
from: dbName.uniPayOrders,
let: {
user_id: '$_id',
},
pipeline: $.pipeline()
.match(_.expr($.and([
$.eq(['$user_id', '$$user_id']),
$.in(['$status', status])
])))
.limit(1)
.done(),
as: 'order',
})
.unwind({
path: '$order',
})
.group({
_id: null,
count: {
$addToSet: '$_id'
},
})
.addFields({
count: {
$size: '$count'
}
})
.end()
try {
return dbRes.data[0].count;
} catch (err) {
return 0;
}
}
}
module.exports = new Dao();

@ -0,0 +1,99 @@
/**
* 数据库操作
*/
const BaseMod = require('../../base');
const dbName = require("./config");
class Dao extends BaseMod {
constructor() {
super()
this.tablePrefix = false; // 不使用表前缀
}
async group(data) {
let {
start_time,
end_time,
status: status_str
} = data;
let status;
if (status_str === "已下单") {
} else if (status_str === "已付款") {
status = {
$gt: 0
}
} else if (status_str === "已退款") {
status = {
$in: [2, 3]
}
}
const dbRes = await this.aggregate(dbName.uniPayOrders, {
match: {
create_date: {
$gte: start_time,
$lte: end_time
},
status
},
group: {
_id: {
appid: '$appid',
version: '$stat_data.app_version',
platform: '$stat_data.platform',
channel: '$stat_data.channel',
},
status: {
$first: '$status'
},
// 支付金额
total_fee: {
$sum: '$total_fee'
},
// 退款金额
refund_fee: {
$sum: '$refund_fee'
},
// 支付笔数
order_count: {
$sum: 1
},
// 支付人数(去重复)
user_count: {
$addToSet: '$user_id'
},
// 支付设备数(去重复)
device_count: {
$addToSet: '$device_id'
},
create_date: {
$min: '$create_date'
}
},
addFields: {
user_count: {
$size: '$user_count'
},
device_count: {
$size: '$device_count'
}
},
// 按创建时间排序
sort: {
create_date: 1
},
getAll: true
});
let list = dbRes.data;
list.map((item) => {
item.status_str = status_str;
});
return list;
}
}
module.exports = new Dao();

@ -0,0 +1,36 @@
/**
* 数据库操作
*/
const BaseMod = require('../../base');
const dbName = require("./config");
class Dao extends BaseMod {
constructor() {
super()
this.tablePrefix = false; // 不使用表前缀
}
async list(data) {
let {
whereJson,
} = data;
const dbRes = await this.getCollection(dbName.uniStatPayResult).where(whereJson).get();
return dbRes.data;
}
async del(data) {
let {
whereJson
} = data;
const dbRes = await this.delete(dbName.uniStatPayResult, whereJson);
return dbRes.deleted;
}
async adds(saveList) {
return await this.batchInsert(dbName.uniStatPayResult, saveList);
}
}
module.exports = new Dao();

@ -0,0 +1,78 @@
/**
* 数据库操作
*/
const BaseMod = require('../../base');
const dbName = require("./config");
class Dao extends BaseMod {
constructor() {
super()
this.tablePrefix = false; // 不使用表前缀
}
async group(data) {
let {
whereJson,
} = data;
const dbRes = await this.aggregate(dbName.uniStatSessionLogs, {
match: whereJson,
group: {
_id: {
appid: '$appid',
version: '$version',
platform: '$platform',
channel: '$channel',
},
// 设备数(去重复)
device_count: {
$addToSet: '$device_id'
},
create_time: {
$min: '$create_time'
}
},
addFields: {
device_count: {
$size: '$device_count'
}
},
// 按创建时间排序
sort: {
create_time: 1
},
getAll: true
});
return dbRes.data;
}
async groupCount(whereJson) {
const dbRes = await this.aggregate(dbName.uniStatSessionLogs, {
match: whereJson,
group: {
_id: null,
// 设备数(去重复)
count: {
$addToSet: '$device_id'
},
},
addFields: {
count: {
$size: '$count'
}
},
getAll: true
});
try {
return dbRes.data[0].count;
} catch (err) {
return 0;
}
}
}
module.exports = new Dao();

@ -0,0 +1,78 @@
/**
* 数据库操作
*/
const BaseMod = require('../../base');
const dbName = require("./config");
class Dao extends BaseMod {
constructor() {
super()
this.tablePrefix = false; // 不使用表前缀
}
async group(data) {
let {
whereJson
} = data;
const dbRes = await this.aggregate(dbName.uniStatUserSessionLogs, {
match: whereJson,
group: {
_id: {
appid: '$appid',
version: '$version',
platform: '$platform',
channel: '$channel',
},
// 用户数(去重复)
user_count: {
$addToSet: '$uid'
},
create_time: {
$min: '$create_time'
}
},
addFields: {
user_count: {
$size: '$user_count'
}
},
// 按创建时间排序
sort: {
create_time: 1
},
getAll: true
});
let list = dbRes.data;
return list;
}
async groupCount(whereJson) {
const dbRes = await this.aggregate(dbName.uniStatUserSessionLogs, {
match: whereJson,
group: {
_id: null,
// 设备数(去重复)
count: {
$addToSet: '$uid'
},
},
addFields: {
count: {
$size: '$count'
}
},
getAll: true
});
try {
return dbRes.data[0].count;
} catch (err) {
return 0;
}
}
}
module.exports = new Dao();

@ -0,0 +1,6 @@
/**
* 基础对外模型
*/
module.exports = {
PayResult: require('./payResult'),
}

@ -0,0 +1,500 @@
/**
* @class ActiveDevices 活跃设备模型 - 每日跑批合并仅添加本周/本月首次访问的设备
*/
const BaseMod = require('../base')
const Platform = require('../platform')
const Channel = require('../channel')
const Version = require('../version')
const {
DateTime,
UniCrypto
} = require('../../lib')
const dao = require('./dao')
let db = uniCloud.database();
let _ = db.command;
let $ = _.aggregate;
module.exports = class PayResult extends BaseMod {
constructor() {
super()
this.platforms = []
this.channels = []
this.versions = []
}
/**
支付金额统计时间内成功支付的订单金额之和不剔除退款订单
支付笔数统计时间内成功支付的订单数一个订单对应唯一一个订单号不剔除退款订单
支付人数统计时间内成功支付的人数不剔除退款订单
支付设备数统计时间内成功支付的设备数不剔除退款订单
下单金额统计时间内成功下单的订单金额不剔除退款订单
下单笔数统计时间内成功下单的订单笔数不剔除退款订单
下单人数统计时间内成功下单的客户数一人多次下单记为一人不剔除退款订单
下单设备数统计时间内成功下单的设备数一台设备多次访问被计为一台不剔除退款订单
访问人数统计时间内访问人数一人多次访问被计为一人只统计已登录的用户
访问设备数统计时间内访问设备数一台设备多次访问被计为一台包含未登录的用户
* @desc 支付统计按日统计
* @param {string} type 统计范围 hour按小时统计day按天统计week按周统计month按月统计 quarter按季度统计 year按年统计
* @param {date|time} date
* @param {bool} reset
*/
async stat(type, date, reset, config = {}) {
if (!date) date = Date.now();
// 以下是测试代码-----------------------------------------------------------
//reset = true;
//date = 1667318400000;
// 以上是测试代码-----------------------------------------------------------
let res = await this.run(type, date, reset, config); // 每小时
if (type === "hour" && config.timely) {
/**
* 如果是小时纬度统计则还需要再统计今日实时数据
* 2022-11-01 01:00:00 统计的是 2022-11-01 的天维度数据即该天0点-1点数据
* 2022-11-01 02:00:00 统计的是 2022-11-01 的天维度数据即该天0点-2点数据
* 2022-11-01 23:00:00 统计的是 2022-11-01 的天维度数据即该天0点-23点数据
* 2022-11-02 00:00:00 统计的是 2022-11-01 的天维度数据即该天最终数据
* 2022-11-02 01:00:00 统计的是 2022-11-02 的天维度数据即该天0点-1点数据
*/
date -= 1000 * 3600; // 需要减去1小时
let tasks = [];
tasks.push(this.run("day", date, true, 0)); // 今日
// 以下数据每6小时刷新一次
const dateTime = new DateTime();
const timeInfo = dateTime.getTimeInfo(date);
if ((timeInfo.nHour + 1) % 6 === 0) {
tasks.push(this.run("week", date, true, 0)); // 本周
tasks.push(this.run("month", date, true, 0)); // 本月
tasks.push(this.run("quarter", date, true, 0)); // 本季度
tasks.push(this.run("year", date, true, 0)); // 本年度
}
await Promise.all(tasks);
}
return res;
}
/**
* @desc 支付统计
* @param {string} type 统计范围 hour按小时统计day按天统计week按周统计month按月统计 quarter按季度统计 year按年统计
* @param {date|time} date 哪个时间节点计算默认已当前时间计算
* @param {bool} reset 如果统计数据已存在是否需要重新统计
*/
async run(type, date, reset, offset = -1) {
let dimension = type;
const dateTime = new DateTime();
// 获取统计的起始时间和截止时间
const dateDimension = dateTime.getTimeDimensionByType(dimension, offset, date);
let start_time = dateDimension.startTime;
let end_time = dateDimension.endTime;
let runStartTime = Date.now();
let debug = true;
if (debug) {
console.log(`-----------------支付统计开始(${dimension}-----------------`);
console.log('本次统计时间:', dateTime.getDate('Y-m-d H:i:s', start_time), "-", dateTime.getDate('Y-m-d H:i:s', end_time))
console.log('本次统计参数:', 'type:' + type, 'date:' + date, 'reset:' + reset)
}
this.startTime = start_time;
let pubWhere = {
start_time,
end_time
};
// 查看当前时间段数据是否已存在,防止重复生成
if (!reset) {
let list = await dao.uniStatPayResult.list({
whereJson: {
...pubWhere,
dimension
}
});
if (list.length > 0) {
console.log('data have exists')
if (debug) {
let runEndTime = Date.now();
console.log(`耗时:${((runEndTime - runStartTime ) / 1000).toFixed(3)}`)
console.log(`-----------------支付统计结束(${dimension}-----------------`);
}
return {
code: 1003,
msg: 'Pay data in this time have already existed'
}
}
} else {
let delRes = await dao.uniStatPayResult.del({
whereJson: {
...pubWhere,
dimension
}
});
console.log('Delete old data result:', JSON.stringify(delRes))
}
// 支付订单分组(已下单)
let statPayOrdersList1 = await dao.uniPayOrders.group({
...pubWhere,
status: "已下单"
});
// 支付订单分组(且已付款,含退款)
let statPayOrdersList2 = await dao.uniPayOrders.group({
...pubWhere,
status: "已付款"
});
// 支付订单分组(已退款)
let statPayOrdersList3 = await dao.uniPayOrders.group({
...pubWhere,
status: "已退款"
});
let statPayOrdersList = statPayOrdersList1.concat(statPayOrdersList2).concat(statPayOrdersList3)
let res = {
code: 0,
msg: 'success'
}
// 将支付订单分组查询结果组装
let statDta = {};
if (statPayOrdersList.length > 0) {
for (let i = 0; i < statPayOrdersList.length; i++) {
let item = statPayOrdersList[i];
let {
appid,
version,
platform,
channel,
} = item._id;
let {
status_str
} = item;
let key = `${appid}-${version}-${platform}-${channel}`;
if (!statDta[key]) {
statDta[key] = {
appid,
version,
platform,
channel,
status: {}
};
}
let newItem = JSON.parse(JSON.stringify(item));
delete newItem._id;
statDta[key].status[status_str] = newItem;
}
}
if (this.debug) console.log('statDta: ', statDta)
let saveList = [];
for (let key in statDta) {
let item = statDta[key];
let {
appid,
version,
platform,
channel,
status: statusData,
} = item;
if (!channel) channel = item.scene;
let fieldData = {
pay_total_amount: 0,
pay_order_count: 0,
pay_user_count: 0,
pay_device_count: 0,
create_total_amount: 0,
create_order_count: 0,
create_user_count: 0,
create_device_count: 0,
refund_total_amount: 0,
refund_order_count: 0,
refund_user_count: 0,
refund_device_count: 0,
};
for (let status in statusData) {
let statusItem = statusData[status];
if (status === "已下单") {
// 已下单
fieldData.create_total_amount += statusItem.total_fee;
fieldData.create_order_count += statusItem.order_count;
fieldData.create_user_count += statusItem.user_count;
fieldData.create_device_count += statusItem.device_count;
} else if (status === "已付款") {
// 已付款
fieldData.pay_total_amount += statusItem.total_fee;
fieldData.pay_order_count += statusItem.order_count;
fieldData.pay_user_count += statusItem.user_count;
fieldData.pay_device_count += statusItem.device_count;
} else if (status === "已退款") {
// 已退款
fieldData.refund_total_amount += statusItem.total_fee;
fieldData.refund_order_count += statusItem.order_count;
fieldData.refund_user_count += statusItem.user_count;
fieldData.refund_device_count += statusItem.device_count;
}
}
// 平台信息
let platformInfo = null;
if (this.platforms && this.platforms[platform]) {
// 从缓存中读取数据
platformInfo = this.platforms[platform]
} else {
const platformObj = new Platform()
platformInfo = await platformObj.getPlatformAndCreate(platform, null)
if (!platformInfo || platformInfo.length === 0) {
platformInfo._id = ''
}
this.platforms[platform] = platformInfo;
}
// 渠道信息
let channelInfo = null
const channelKey = appid + '_' + platformInfo._id + '_' + channel;
if (this.channels && this.channels[channelKey]) {
channelInfo = this.channels[channelKey];
} else {
const channelObj = new Channel()
channelInfo = await channelObj.getChannelAndCreate(appid, platformInfo._id, channel)
if (!channelInfo || channelInfo.length === 0) {
channelInfo._id = ''
}
this.channels[channelKey] = channelInfo
}
// 版本信息
let versionInfo = null
const versionKey = appid + '_' + platform + '_' + version
if (this.versions && this.versions[versionKey]) {
versionInfo = this.versions[versionKey]
} else {
const versionObj = new Version()
versionInfo = await versionObj.getVersionAndCreate(appid, platform, version)
if (!versionInfo || versionInfo.length === 0) {
versionInfo._id = ''
}
this.versions[versionKey] = versionInfo
}
let countWhereJson = {
create_time: _.gte(start_time).lte(end_time),
appid,
version,
platform: _.in(getUniPlatform(platform)),
channel,
};
// 活跃设备数量
let activity_device_count = await dao.uniStatSessionLogs.groupCount(countWhereJson);
// 活跃用户数量
let activity_user_count = await dao.uniStatUserSessionLogs.groupCount(countWhereJson);
/*
// TODO 此处有问题,暂不使用
// 新设备数量
let new_device_count = await dao.uniStatSessionLogs.groupCount({
...countWhereJson,
is_first_visit: 1,
});
// 新注册用户数量
let new_user_count = await dao.uniIdUsers.count({
register_date: _.gte(start_time).lte(end_time),
register_env: {
appid,
app_version: version,
uni_platform: _.in(getUniPlatform(platform)),
channel,
}
});
// 新注册用户中下单的人数
let new_user_create_order_count = await dao.uniIdUsers.countNewUserOrder({
whereJson: {
register_date: _.gte(start_time).lte(end_time),
register_env: {
appid,
app_version: version,
uni_platform: _.in(getUniPlatform(platform)),
channel,
}
},
status: [-1, 0]
});
// 新注册用户中支付成功的人数
let new_user_pay_order_count = await dao.uniIdUsers.countNewUserOrder({
whereJson: {
register_date: _.gte(start_time).lte(end_time),
register_env: {
appid,
app_version: version,
uni_platform: _.in(getUniPlatform(platform)),
channel,
}
},
status: [1, 2, 3]
}); */
saveList.push({
appid,
platform_id: platformInfo._id,
channel_id: channelInfo._id,
version_id: versionInfo._id,
dimension,
create_date: Date.now(), // 记录下当前时间
start_time,
end_time,
stat_date: getNowDate(start_time, 8, dimension),
...fieldData,
activity_user_count,
activity_device_count,
// new_user_count,
// new_device_count,
// new_user_create_order_count,
// new_user_pay_order_count,
});
}
if (this.debug) console.log('saveList: ', saveList)
//return;
if (saveList.length > 0) {
res = await dao.uniStatPayResult.adds(saveList);
}
if (debug) {
let runEndTime = Date.now();
console.log(`耗时:${((runEndTime - runStartTime ) / 1000).toFixed(3)}`)
console.log(`本次共添加:${saveList.length } 条记录`)
console.log(`-----------------支付统计结束(${dimension}-----------------`);
}
return res
}
}
function getUniPlatform(platform) {
let list = [];
if (["h5", "web"].indexOf(platform) > -1) {
list = ["h5", "web"];
} else if (["app-plus", "app"].indexOf(platform) > -1) {
list = ["app-plus", "app"];
} else {
list = [platform];
}
return list;
}
function getNowDate(date = new Date(), targetTimezone = 8, dimension) {
if (typeof date === "string" && !isNaN(date)) date = Number(date);
if (typeof date == "number") {
if (date.toString().length == 10) date *= 1000;
date = new Date(date);
}
const {
year,
month,
day,
hour,
minute,
second
} = getFullTime(date);
// 现在的时间
let date_str;
if (dimension === "month") {
date_str = timeFormat(date, "yyyy-MM", targetTimezone);
} else if (dimension === "quarter") {
date_str = timeFormat(date, "yyyy-MM", targetTimezone);
} else if (dimension === "year") {
date_str = timeFormat(date, "yyyy", targetTimezone);
} else {
date_str = timeFormat(date, "yyyy-MM-dd", targetTimezone);
}
return {
date_str,
year,
month,
day,
hour,
//minute,
//second,
};
}
function getFullTime(date = new Date(), targetTimezone = 8) {
if (!date) {
return "";
}
if (typeof date === "string" && !isNaN(date)) date = Number(date);
if (typeof date == "number") {
if (date.toString().length == 10) date *= 1000;
date = new Date(date);
}
const dif = date.getTimezoneOffset();
const timeDif = dif * 60 * 1000 + (targetTimezone * 60 * 60 * 1000);
const east8time = date.getTime() + timeDif;
date = new Date(east8time);
let YYYY = date.getFullYear() + '';
let MM = (date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1);
let DD = (date.getDate() < 10 ? '0' + (date.getDate()) : date.getDate());
let hh = (date.getHours() < 10 ? '0' + (date.getHours()) : date.getHours());
let mm = (date.getMinutes() < 10 ? '0' + (date.getMinutes()) : date.getMinutes());
let ss = (date.getSeconds() < 10 ? '0' + (date.getSeconds()) : date.getSeconds());
return {
YYYY: Number(YYYY),
MM: Number(MM),
DD: Number(DD),
hh: Number(hh),
mm: Number(mm),
ss: Number(ss),
year: Number(YYYY),
month: Number(MM),
day: Number(DD),
hour: Number(hh),
minute: Number(mm),
second: Number(ss),
};
};
/**
* 日期格式化
*/
function timeFormat(time, fmt = 'yyyy-MM-dd hh:mm:ss', targetTimezone = 8) {
try {
if (!time) {
return "";
}
if (typeof time === "string" && !isNaN(time)) time = Number(time);
// 其他更多是格式化有如下:
// yyyy-MM-dd hh:mm:ss|yyyy年MM月dd日 hh时MM分等,可自定义组合
let date;
if (typeof time === "number") {
if (time.toString().length == 10) time *= 1000;
date = new Date(time);
} else {
date = time;
}
const dif = date.getTimezoneOffset();
const timeDif = dif * 60 * 1000 + (targetTimezone * 60 * 60 * 1000);
const east8time = date.getTime() + timeDif;
date = new Date(east8time);
let opt = {
"M+": date.getMonth() + 1, //月份
"d+": date.getDate(), //日
"h+": date.getHours(), //小时
"m+": date.getMinutes(), //分
"s+": date.getSeconds(), //秒
"q+": Math.floor((date.getMonth() + 3) / 3), //季度
"S": date.getMilliseconds() //毫秒
};
if (/(y+)/.test(fmt)) {
fmt = fmt.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length));
}
for (let k in opt) {
if (new RegExp("(" + k + ")").test(fmt)) {
fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (opt[k]) : (("00" + opt[k]).substr(("" + opt[k]).length)));
}
}
return fmt;
} catch (err) {
// 若格式错误,则原值显示
return time;
}
};

@ -0,0 +1,97 @@
/**
* @class UniIDUsers uni-id 用户模型
*/
const BaseMod = require('./base')
module.exports = class UniIDUsers extends BaseMod {
constructor() {
super()
this.tableName = 'uni-id-users'
this.tablePrefix = false
}
/**
* 获取用户数
* @param {String} appid DCloud-appid
* @param {String} platform 平台
* @param {String} channel 渠道
* @param {String} version 版本
* @param {Object} registerTime 注册时间范围 {$gte:开始日期时间戳, $lte:结束日期时间戳}
* @return {Number}
*/
async getUserCount(appid, platform, channel, version, registerTime) {
if(!appid || !platform) {
return false
}
const condition = this.getCondition(appid, platform, channel, version, registerTime)
let userCount = 0
const userCountRes = await this.getCollection(this.tableName).where(condition).count()
if(userCountRes && userCountRes.total > 0) {
userCount = userCountRes.total
}
return userCount
}
/**
* 获取用户编号列表
* @param {String} appid DCloud-appid
* @param {String} platform 平台
* @param {String} channel 渠道
* @param {String} version 版本
* @param {Object} registerTime 注册时间范围 {$gte:开始日期时间戳, $lte:结束日期时间戳}
* @return {Array}
*/
async getUserIds(appid, platform, channel, version, registerTime) {
if(!appid || !platform) {
return false
}
const condition = this.getCondition(appid, platform, channel, version, registerTime)
let uids = []
const uidsRes = await this.selectAll(this.tableName, condition, {
_id: 1
})
for (const u in uidsRes.data) {
uids.push(uidsRes.data[u]._id)
}
return uids
}
/**
* 获取查询条件
* @param {String} appid DCloud-appid
* @param {String} platform 平台
* @param {String} channel 渠道
* @param {String} version 版本
* @param {Object} registerTime 注册时间范围 {$gte:开始日期时间戳, $lte:结束日期时间戳}
*/
getCondition(appid, platform, channel, version, registerTime) {
let condition = {
'register_env.appid': appid,//DCloud appid
'register_env.uni_platform': platform,//平台
'register_env.channel': channel ? channel : '1001', //渠道或场景值
'register_env.app_version' : version //应用版本区分
}
//原生应用平台
if(['android', 'ios'].includes(platform)) {
condition['register_env.uni_platform'] = 'app'//systemInfo中uniPlatform字段android和ios都用app表示所以此处查询需要用osName区分一下
condition['register_env.os_name'] = platform //系统
}
//兼容vue2
if(channel === '1001') {
condition['register_env.channel'] = {$in:['', '1001']}
}
//注册时间
if(registerTime) {
condition.register_date = registerTime
}
return condition
}
}

@ -0,0 +1,229 @@
/**
* @class UserSessionLog 用户会话日志模型
*/
const BaseMod = require('./base')
const Platform = require('./platform')
const Channel = require('./channel')
const {
DateTime
} = require('../lib')
module.exports = class UserSessionLog extends BaseMod {
constructor() {
super()
this.tableName = 'user-session-logs'
}
/**
* 用户会话日志数据填充
* @param {Object} params 上报参数
*/
async fill(params) {
if (!params.sid) {
return {
code: 200,
msg: 'Not found session log'
}
}
if (!params.uid) {
return {
code: 200,
msg: 'Parameter "uid" not found'
}
}
const dateTime = new DateTime()
const platform = new Platform()
const channel = new Channel()
//获取当前页面信息
if (!params.page_id) {
const pageInfo = await page.getPageAndCreate(params.ak, params.url, params.ttpj)
if (!pageInfo || pageInfo.length === 0) {
return {
code: 300,
msg: 'Not found this entry page'
}
}
params.page_id = pageInfo._id
}
const nowTime = dateTime.getTime()
const fillParams = {
appid: params.ak,
version: params.v ? params.v : '',
platform: platform.getPlatformCode(params.ut, params.p),
channel: channel.getChannelCode(params),
session_id: params.sid,
uid: params.uid,
last_visit_time: nowTime,
entry_page_id: params.page_id,
exit_page_id: params.page_id,
page_count: 0,
event_count: 0,
duration: 1,
is_finish: 0,
create_time: nowTime,
}
const res = await this.insert(this.tableName, fillParams)
if (res && res.id) {
return {
code: 0,
msg: 'success'
}
} else {
return {
code: 500,
msg: 'User session log filled error'
}
}
}
/**
* 检测用户会话是否有变化并更新
* @param {Object} params 校验参数 - sid:基础会话编号 uid:用户编号 last_visit_user_id:基础会话中最近一个访问用户的编号
*/
async checkUserSession(params) {
if (!params.sid) {
return {
code: 200,
msg: 'Not found session log'
}
}
if (!params.uid) {
//用户已退出会话
if (params.last_visit_user_id) {
if (this.debug) {
console.log('user "' + params.last_visit_user_id + '" is exit session :', params.sid)
}
await this.closeUserSession(params.sid)
}
} else {
//添加用户日志
if (!params.last_visit_user_id) {
await this.fill(params)
}
//用户已切换
else if (params.uid != params.last_visit_user_id) {
if (this.debug) {
console.log('user "' + params.last_visit_user_id + '" change to "' + params.uid +
'" in the session :', params.sid)
}
//关闭原会话生成新用户对话
await this.closeUserSession(params.sid)
await this.fill(params)
}
}
return {
code: 0,
msg: 'success'
}
}
/**
* 关闭用户会话
* @param {String} sid 基础会话编号
*/
async closeUserSession(sid) {
if (this.debug) {
console.log('close user session log by sid:', sid)
}
return await this.update(this.tableName, {
is_finish: 1
}, {
session_id: sid,
is_finish: 0
})
}
/**
* 更新会话信息
* @param {String} sid 基础会话编号
* @param {Object} data 更新数据
*/
async updateUserSession(sid, data) {
const userSession = await this.getCollection(this.tableName).where({
session_id: sid,
uid: data.uid,
is_finish: 0
}).orderBy('create_time', 'desc').limit(1).get()
if (userSession.data.length === 0) {
console.log('Not found the user session', {
session_id: sid,
uid: data.uid,
is_finish: 0
})
return {
code: 300,
msg: 'Not found the user session'
}
}
let nowTime = data.nowTime ? data.nowTime : new DateTime().getTime()
const accessTime = nowTime - userSession.data[0].createTime
const accessSenconds = accessTime > 1000 ? parseInt(accessTime / 1000) : 1
const updateData = {
last_visit_time: nowTime,
duration: accessSenconds,
}
//访问页面数量
if (data.addPageCount) {
updateData.page_count = userSession.data[0].page_count + data.addPageCount
}
//最终访问的页面编号
if (data.pageId) {
updateData.exit_page_id = data.pageId
}
//产生事件次数
if (data.eventCount) {
updateData.event_count = userSession.data[0].event_count + data.addEventCount
}
if (this.debug) {
console.log('update user session log by sid-' + sid, updateData)
}
await this.update(this.tableName, updateData, {
_id: userSession.data[0]._id
})
return {
code: 0,
msg: 'success'
}
}
/**
* 清理用户会话日志数据
* @param {Object} days 保留天数, 留存统计需要计算30天后留存率因此至少应保留31天的日志数据
*/
async clean(days = 31) {
days = Math.max(parseInt(days), 1)
console.log('clean user session logs - day:', days)
const dateTime = new DateTime()
const res = await this.delete(this.tableName, {
create_time: {
$lt: dateTime.getTimeBySetDays(0 - days)
}
})
if (!res.code) {
console.log('clean user session log:', res)
}
return res
}
}

@ -0,0 +1,73 @@
/**
* @class Version 应用版本模型
*/
const BaseMod = require('./base')
const {
DateTime
} = require('../lib')
module.exports = class Version extends BaseMod {
constructor() {
super()
this.tableName = 'opendb-app-versions'
this.tablePrefix = false
this.cacheKeyPre = 'uni-stat-app-version-'
}
/**
* 获取版本信息
* @param {String} appid DCloud-appid
* @param {String} platformId 平台编号
* @param {String} appVersion 平台版本号
*/
async getVersion(appid, platform, appVersion) {
const cacheKey = this.cacheKeyPre + appid + '-' + platform + '-' + appVersion
let versionData = await this.getCache(cacheKey)
if (!versionData) {
const versionInfo = await this.getCollection(this.tableName).where({
appid: appid,
uni_platform: platform,
type: 'native_app',
version: appVersion
}).limit(1).get()
versionData = []
if (versionInfo.data.length > 0) {
versionData = versionInfo.data[0]
await this.setCache(cacheKey, versionData)
}
}
return versionData
}
/**
* 获取版本信息没有则进行创建
* @param {String} appid DCloud-appid
* @param {String} platform 平台代码
* @param {String} appVersion 平台版本号
*/
async getVersionAndCreate(appid, platform, appVersion) {
const versionInfo = await this.getVersion(appid, platform, appVersion)
if (versionInfo.length === 0) {
if (appVersion.length > 0 && !appVersion.includes('}')) {
const thisTime = new DateTime().getTime()
const insertParam = {
appid: appid,
platform: [],
uni_platform: platform,
type: 'native_app',
version: appVersion,
stable_publish: false,
create_env: 'uni-stat',
create_date: thisTime
}
const res = await this.insert(this.tableName, insertParam)
if (res && res.id) {
return Object.assign(insertParam, {
_id: res.id
})
}
}
}
return versionInfo
}
}

@ -0,0 +1,126 @@
/**
* @class UniStatReportDataReceiver uni统计上报数据接收器
* @function report 上报数据调度处理函数
*/
const {
parseUrlParams
} = require('../shared')
const SessionLog = require('./mod/sessionLog')
const PageLog = require('./mod/pageLog')
const EventLog = require('./mod/eventLog')
const ErrorLog = require('./mod/errorLog')
const Device = require('./mod/device')
class UniStatReportDataReceiver {
/**
* @description 上报数据调度处理函数
* @param {Object} params 基础上报参数
* @param {Object} context 请求附带的上下文信息
*/
async report(params, context) {
let res = {
code: 0,
msg: 'success'
}
if (!params || !params.requests) {
return {
code: 200,
msg: 'Invild params'
}
}
// JSON参数解析
const requestParam = JSON.parse(params.requests)
if (!requestParam || requestParam.length === 0) {
return {
code: 200,
msg: 'Invild params'
}
}
// 日志填充
const sessionParams = []
const pageParams = []
const eventParams = []
const errorParams = []
const device = new Device()
for (const ri in requestParam) {
//参数解析
const urlParams = parseUrlParams(requestParam[ri], context)
if (!urlParams.ak) {
return {
code: 201,
msg: 'Not found appid'
}
}
if (!urlParams.lt) {
return {
code: 202,
msg: 'Not found this log type'
}
}
switch (parseInt(urlParams.lt)) {
// 会话日志
case 1: {
sessionParams.push(urlParams)
break
}
// 页面日志
case 3:
case 11: {
pageParams.push(urlParams)
break
}
// 事件日志
case 21: {
eventParams.push(urlParams)
break
}
// 错误日志
case 31: {
errorParams.push(urlParams)
break
}
//unipush信息绑定
case 101: {
res = await device.bindPush(urlParams)
break
}
default: {
console.log('Invalid type by param "lt:' + urlParams.lt + '"')
break
}
}
}
//会话日志填充
if (sessionParams.length > 0) {
const sessionLog = new SessionLog()
res = await sessionLog.batchFill(sessionParams)
}
//页面日志填充
if (pageParams.length > 0) {
const pageLog = new PageLog()
res = await pageLog.fill(pageParams)
}
//事件日志填充
if (eventParams.length > 0) {
const eventLog = new EventLog()
res = await eventLog.fill(eventParams)
}
//错误日志填充
if (errorParams.length > 0) {
const errorLog = new ErrorLog()
res = await errorLog.fill(errorParams)
}
return res
}
}
module.exports = UniStatReportDataReceiver

@ -0,0 +1,388 @@
/**
* @class UniStatDataStat uni统计-数据统计调度处理模块
* @function cron 数据统计定时任务处理函数
* @function stat 数据统计调度处理函数
* @function cleanLog 日志清理调度处理函数
*/
const {
DateTime
} = require('./lib')
const {
sleep
} = require('../shared')
const {
BaseMod,
SessionLog,
PageLog,
EventLog,
ShareLog,
ErrorLog,
StatResult,
ActiveDevices,
ActiveUsers,
PageResult,
EventResult,
ErrorResult,
Loyalty,
RunErrors,
UserSessionLog,
uniPay,
Setting
} = require('./mod')
class UniStatDataStat {
/**
* 数据统计定时任务处理函数
* @param {Object} context 服务器请求上下文参数
*/
async cron(context) {
const baseMod = new BaseMod()
const dateTime = new DateTime()
console.log('Cron start time: ', dateTime.getDate('Y-m-d H:i:s'))
// const setting = new Setting();
// let settingValue = await setting.getSetting()
// if (settingValue.mode === "close") {
// // 如果关闭了统计任务,则任务直接结束
// return {
// code: 0,
// msg: 'Task is close',
// }
// } else if (settingValue.mode === "auto") {
// // 如果开启了节能模式则判断N天内是否有设备访问记录
// let runKey = await setting.checkAutoRun(settingValue);
// if (!runKey) {
// return {
// code: 0,
// msg: 'Task is auto close',
// }
// }
// }
//获取运行参数
const timeInfo = dateTime.getTimeInfo(null, false)
const cronConfig = baseMod.getConfig('cron')
const cronMin = baseMod.getConfig('cronMin')
const realtimeStat = baseMod.getConfig('realtimeStat')
// 数据跑批
let res = null
if (cronConfig && cronConfig.length > 0) {
for (var mi in cronConfig) {
const currCronConfig = cronConfig[mi]
const cronType = currCronConfig.type
const cronTime = currCronConfig.time.split(' ')
const cronDimension = currCronConfig.dimension
//未开启分钟级定时任务,则设置为小时级定时任务
if (cronTime.length === 4 && !cronMin) {
cronTime.splice(3, 1)
}
if (baseMod.debug) {
console.log('cronTime', cronTime)
}
//精度为分钟级的定时任务
if (cronTime.length === 4) {
if (cronTime[0] !== '*') {
//周统计任务
if (timeInfo.nWeek == cronTime[0] && timeInfo.nHour == cronTime[2] && timeInfo.nMinutes ==
cronTime[3]) {
let dimension = cronDimension || 'week';
console.log(cronType + `--${dimension} run`)
res = await this.stat({
type: cronType,
dimension: cronDimension,
config: currCronConfig
})
}
} else if (cronTime[1] !== '*') {
//月统计任务(包含季度统计任务和年统计任务)
if (timeInfo.nDay == cronTime[1] && timeInfo.nHour == cronTime[2] && timeInfo.nMinutes ==
cronTime[3]) {
let dimension = cronDimension || 'month';
console.log(cronType + `--${dimension} run`)
res = await this.stat({
type: cronType,
dimension: dimension,
config: currCronConfig
})
}
} else if (cronTime[2] !== '*') {
//日统计任务
if (timeInfo.nHour == cronTime[2] && timeInfo.nMinutes == cronTime[3]) {
let dimension = cronDimension || 'day';
console.log(cronType + `--${dimension} run`)
res = await this.stat({
type: cronType,
dimension: dimension,
config: currCronConfig
})
}
} else if (cronTime[3] !== '*') {
//实时统计任务
if (timeInfo.nMinutes == cronTime[3] && realtimeStat) {
let dimension = cronDimension || 'hour';
console.log(cronType + `--${dimension} run`)
res = await this.stat({
type: cronType,
dimension: dimension,
config: currCronConfig
})
}
}
}
//精度为小时级的定时任务
else if (cronTime.length === 3) {
if (cronTime[0] !== '*') {
//周统计任务
if (timeInfo.nWeek == cronTime[0] && timeInfo.nHour == cronTime[2]) {
let dimension = cronDimension || 'week';
console.log(cronType + `--${dimension} run`)
res = await this.stat({
type: cronType,
dimension: dimension,
config: currCronConfig
})
}
} else if (cronTime[1] !== '*') {
//月统计任务(包含季度统计任务和年统计任务)
if (timeInfo.nDay == cronTime[1] && timeInfo.nHour == cronTime[2]) {
let dimension = cronDimension || 'month';
console.log(cronType + `--${dimension} run`)
res = await this.stat({
type: cronType,
dimension: dimension,
config: currCronConfig
})
}
} else if (cronTime[2] !== '*') {
//日统计任务
if (timeInfo.nHour == cronTime[2]) {
let dimension = cronDimension || 'day';
console.log(cronType + `--${dimension} run`)
res = await this.stat({
type: cronType,
dimension: dimension,
config: currCronConfig
})
}
} else {
//实时统计任务
if (realtimeStat) {
let dimension = cronDimension || 'hour';
console.log(cronType + `--${dimension} run`)
res = await this.stat({
type: cronType,
dimension: dimension,
config: currCronConfig
})
}
}
} else {
console.error('Cron configuration error')
}
}
}
console.log('Cron end time: ', dateTime.getDate('Y-m-d H:i:s'))
return {
code: 0,
msg: 'Task have done',
lastCronResult: res
}
}
/**
* 数据统计调度处理函数
* @param {Object} params 统计参数
*/
async stat(params) {
const {
type,
dimension,
date,
reset,
config
} = params
let res = {
code: 0,
msg: 'success'
}
try {
switch (type) {
// 基础统计
case 'stat': {
const resultStat = new StatResult()
res = await resultStat.stat(dimension, date, reset)
break
}
// 活跃设备统计归集
case 'active-device': {
const activeDevices = new ActiveDevices()
res = await activeDevices.stat(date, reset)
break
}
// 活跃用户统计归集
case 'active-user': {
const activeUsers = new ActiveUsers()
res = await activeUsers.stat(date, reset)
break
}
// 设备留存统计
case 'retention-device': {
const retentionStat = new StatResult()
res = await retentionStat.retentionStat(dimension, date)
break
}
// 用户留存统计
case 'retention-user': {
const retentionStat = new StatResult()
res = await retentionStat.retentionStat(dimension, date, 'user')
break
}
// 页面统计
case 'page': {
const pageStat = new PageResult()
res = await pageStat.stat(dimension, date, reset)
break
}
// 事件统计
case 'event': {
const eventStat = new EventResult()
res = await eventStat.stat(dimension, date, reset)
break
}
// 错误统计
case 'error': {
const errorStat = new ErrorResult()
res = await errorStat.stat(dimension, date, reset)
break
}
// 设备忠诚度统计
case 'loyalty': {
const loyaltyStat = new Loyalty()
res = await loyaltyStat.stat(dimension, date, reset)
break
}
// 日志清理
case 'clean': {
res = await this.cleanLog()
}
// 支付统计
case 'pay-result': {
const paymentResult = new uniPay.PayResult()
res = await paymentResult.stat(dimension, date, reset, config)
break
}
}
} catch (e) {
const maxTryTimes = 2
if (!this.tryTimes) {
this.tryTimes = 1
} else {
this.tryTimes++
}
//报错则重新尝试2次, 解决部分云服务器偶现连接超时问题
if (this.tryTimes <= maxTryTimes) {
//休眠1秒后重新调用
await sleep(1000)
params.reset = true
res = await this.stat(params)
} else {
// 2次尝试失败后记录错误
console.error('server error: ' + e)
const runError = new RunErrors()
runError.create({
mod: 'stat',
params: params,
error: e,
create_time: new DateTime().getTime()
})
res = {
code: 500,
msg: 'server error' + e
}
}
}
return res
}
/**
* 日志清理调度处理函数
*/
async cleanLog() {
const baseMod = new BaseMod()
const cleanLog = baseMod.getConfig('cleanLog')
if (!cleanLog || !cleanLog.open) {
return {
code: 100,
msg: 'The log cleanup service has not been turned on'
}
}
const res = {
code: 0,
msg: 'success',
data: {}
}
// 会话日志
if (cleanLog.reserveDays.sessionLog > 0) {
const sessionLog = new SessionLog()
res.data.sessionLog = await sessionLog.clean(cleanLog.reserveDays.sessionLog)
}
// 用户会话日志
if (cleanLog.reserveDays.userSessionLog > 0) {
const userSessionLog = new UserSessionLog()
res.data.userSessionLog = await userSessionLog.clean(cleanLog.reserveDays.userSessionLog)
}
// 页面日志
if (cleanLog.reserveDays.pageLog > 0) {
const pageLog = new PageLog()
res.data.pageLog = await pageLog.clean(cleanLog.reserveDays.pageLog)
}
// 事件日志
if (cleanLog.reserveDays.eventLog > 0) {
const eventLog = new EventLog()
res.data.eventLog = await eventLog.clean(cleanLog.reserveDays.eventLog)
}
// 分享日志
if (cleanLog.reserveDays.shareLog > 0) {
const shareLog = new ShareLog()
res.data.shareLog = await shareLog.clean(cleanLog.reserveDays.shareLog)
}
// 错误日志
if (cleanLog.reserveDays.errorLog > 0) {
const errorLog = new ErrorLog()
res.data.errorLog = await errorLog.clean(cleanLog.reserveDays.errorLog)
}
// 活跃设备日志
const activeDevicesLog = new ActiveDevices()
res.data.activeDevicesLog = await activeDevicesLog.clean()
// 活跃用户日志
const activeUsersLog = new ActiveUsers()
res.data.activeUsersLog = await activeUsersLog.clean()
// 实时统计日志
const resultHourLog = new StatResult()
res.data.resultHourLog = await resultHourLog.cleanHourLog()
//原生应用崩溃日志
const appCrashLogs = new AppCrashLogs()
res.data.appCrashLogs = await appCrashLogs.clean()
return res
}
}
module.exports = UniStatDataStat

@ -0,0 +1,23 @@
'use strict';
exports.main = async (event, context) => {
const db = uniCloud.database()
if (event.api === 'getByID') {
const res = await db.collection('dishes').where({
_id: event.id
}).get()
return res
}
if (event.api === 'getFloorList') {
const res = await db.collection('dishes').skip(4).limit(15).get()
return res
}
if (event.api === 'getByName') {
const res = await db.collection('dishes').where({
dish_name: new RegExp(event.dish_name, 'g')
}).get()
return res
}
//返回数据给客户端
return "请求错误"
};

@ -0,0 +1,7 @@
{
"name": "getDishes",
"dependencies": {},
"extensions": {
"uni-cloud-jql": {}
}
}

@ -0,0 +1,13 @@
'use strict';
exports.main = async (event, context) => {
const db = uniCloud.database()
let labelList = []
const res = db.collection('dish-label').where({
dish_id:event.id
})
for(int i = 0;i < res.data.length;i++) {
labelList.push(res.data[i].label)
}
//返回数据给客户端
return labelList
};

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save