提交 4f676fc3 编写于 作者: study夏羽's avatar study夏羽

uni-admin和uni-starter统一

上级 676c5f20
{
"name": "",
"appid": "",
"description": "云端一体应用快速开发基本项目模版",
"versionName": "",
"versionCode": "100",
"transformPx": false,
"app-plus": {
"usingComponents": true,
"nvueStyleCompiler": "uni-app",
"compilerVersion": 3,
"splashscreen": {
"alwaysShowBeforeRender": true,
"waiting": true,
"autoclose": true,
"delay": 0
"name" : "uni-starter",
"appid" : "__UNI__8E9C31E",
"description" : "云端一体应用快速开发基本项目模版",
"versionName" : "1.0.0",
"versionCode" : "100",
"transformPx" : false,
"app-plus" : {
"usingComponents" : true,
"nvueStyleCompiler" : "uni-app",
"compilerVersion" : 3,
"splashscreen" : {
"alwaysShowBeforeRender" : true,
"waiting" : true,
"autoclose" : true,
"delay" : 0
},
"modules": {
},
"distribute": {
"android": {
"permissions": [
"modules" : {},
"distribute" : {
"android" : {
"permissions" : [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
......@@ -37,35 +36,33 @@
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
]
},
"ios": {
},
"sdkConfigs": {
"push": {
"unipush": null
"ios" : {},
"sdkConfigs" : {
"push" : {
"unipush" : null
}
}
}
},
"quickapp": {
},
"mp-weixin": {
"appid": "",
"setting": {
"urlCheck": false
"quickapp" : {},
"mp-weixin" : {
"appid" : "",
"setting" : {
"urlCheck" : false
},
"usingComponents": true
"usingComponents" : true
},
"mp-alipay": {
"usingComponents": true
"mp-alipay" : {
"usingComponents" : true
},
"mp-baidu": {
"usingComponents": true
"mp-baidu" : {
"usingComponents" : true
},
"mp-toutiao": {
"usingComponents": true
"mp-toutiao" : {
"usingComponents" : true
},
"uniStatistics": {
"enable": false
"uniStatistics" : {
"enable" : false
},
"vueVersion": "2"
"vueVersion" : "2"
}
......@@ -27,7 +27,9 @@ describe('pages/grid/grid.vue', () => {
}
if (process.env.UNI_PLATFORM === "mp-weixin") {
const uniGrid = await page.$('uni-grid')
await page.waitFor(300)
await uniGrid.callMethod('change')
await page.waitFor(500)
}
})
});
\ No newline at end of file
......@@ -20,7 +20,7 @@
<!-- 通过body插槽定义作者信息内容 -->
<template v-slot:body>
<view class="header-content">
<view class="uni-title">{{data.user_id && data.user_id[0].username}}</view>
<view class="uni-title">{{data.user_id && data.user_id[0] && data.user_id[0].nickname || '未知'}}</view>
</view>
</template>
<template v-slot:footer>
......@@ -77,7 +77,7 @@
title: 'title',
// 数据表名
// 查询字段,多个字段用 , 分割
field: 'user_id.username,user_id._id,avatar,excerpt,last_modify_date,comment_count,like_count,title,content',
field: 'user_id.nickname,user_id._id,avatar,excerpt,last_modify_date,comment_count,like_count,title,content',
formData: {
noData: '<p style="text-align:center;color:#666">详情加载中...</p>'
}
......@@ -93,7 +93,7 @@
}
},
onLoad(event) {
console.log(event);
// console.log(event);
// event = {"id":"60783c5cb781700001375672","title":"阿里小程序IDE官方内嵌uni-app,为开发者提供多端开发服务","excerpt":"阿里小程序IDE官方内嵌uni-app,为开发者提供多端开发服务","avatar":"https://vkceyugu.cdn.bspapp.com/VKCEYUGU-aliyun-gacrhzeynhss7c6d04/249516a0-3941-11eb-899d-733ae62bed2f.jpg"}
//获取真实新闻id,通常 id 来自上一个页面
if (event.id) {
......
......@@ -11,9 +11,9 @@
<view class="cover-search-bar" @click="searchClick"></view>
</view>
<unicloud-db ref='udb' @load="loadData" v-slot:default="{data,pagination,hasMore, loading, error, options}" @error="onqueryerror"
:collection="colList" :page-size="10">
<!-- 基于 uni-list 的页面布局 field="user_id.username"-->
<unicloud-db ref='udb' v-slot:default="{data,pagination,hasMore, loading, error, options}" @error="onqueryerror"
:collection="colList" :page-size="10" @load="loadData">
<!-- 基于 uni-list 的页面布局 field="user_id.nickname"-->
<uni-list class="uni-list" :border="false" :style="{height:listHight}">
<!-- 作用于app端nvue页面的下拉加载 -->
......@@ -22,7 +22,8 @@
<!-- #endif -->
<!-- 列表渲染 -->
<uni-list-item :to="'/pages/list/detail?id='+item._id+'&title='+item.title" v-for="(item,index) in data" :key="index">
<uni-list-item :to="'/pages/list/detail?id='+item._id+'&title='+item.title" v-for="(item,index) in data"
:key="index">
<!-- 通过header插槽定义列表左侧图片 -->
<template v-slot:header>
<image class="avatar" :src="item.avatar" mode="aspectFill"></image>
......@@ -32,7 +33,7 @@
<view class="main">
<text class="title">{{item.title}}</text>
<view class="info">
<text class="author">{{item.user_id[0]?item.user_id[0].username:''}}</text>
<text class="author">{{item.user_id[0]?item.user_id[0].nickname:''}}</text>
<uni-dateformat class="last_modify_date" :date="item.last_modify_date"
format="yyyy-MM-dd" :threshold="[60000, 2592000000]" />
</view>
......@@ -53,6 +54,7 @@
<!-- #endif -->
</uni-list>
</unicloud-db>
</view>
</template>
......@@ -80,7 +82,7 @@
return [
db.collection('opendb-news-articles').where(this.where).field(
'avatar,title,last_modify_date,user_id').getTemp(),
db.collection('uni-id-users').field('_id,username').getTemp()
db.collection('uni-id-users').field('_id,nickname').getTemp()
]
}
},
......@@ -90,7 +92,7 @@
keyword: "",
showRefresh: false,
listHight: 0,
dataList:[]
dataList: []
}
},
watch: {
......@@ -169,7 +171,7 @@
cdbRef.loadMore()
},
onqueryerror(e) {
console.error("失败--",e);
console.error(e);
},
onpullingdown(e) {
console.log(e);
......
......@@ -9,7 +9,7 @@ describe('pages/list/list.vue', () => {
it('检测标题', async () => {
// expect.assertions(1);
const getData = await page.data('dataList')
// console.log("getData: ",getData);
console.log("getData: ",getData);
expect(getData.title).toBe('阿里小程序IDE官方内嵌uni-app,为开发者提供多端开发服务')
})
......
......@@ -78,7 +78,6 @@
},
created() {
this.about = this.uniStarterConfig.about
console.log("this.about: ",this.about);
uni.setNavigationBarTitle({
title: this.$t('about.about')+ " " + this.about.appName
})
......
......@@ -58,7 +58,6 @@
// #endif
data() {
return {
uniToken: '',
gridList: [{
"text": this.$t('mine.showText'),
"icon": "chat"
......@@ -140,7 +139,8 @@
"style": "solid", // 边框样式
"radius": "100%" // 边框圆角,支持百分比
}
}
},
uniToken: ''
}
},
onLoad() {
......@@ -154,11 +154,11 @@
})
//#endif
},
onShow() {},
onReady() {
this.uniToken = uni.getStorageSync('uni_id_token')
console.log("uniToken: ", this.uniToken);
},
onShow() {},
computed: {
userInfo() {
return store.userInfo
......
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)
}
}
{
"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"
}
}
\ No newline at end of file
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]
}
}
})
}
/**
* @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
}
}
})
}
}
module.exports = {
UniCloudError: require('./error'),
createApi: require('./create-api'),
... require('./utils')
}
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
}
/**
* @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
}
}
}
module.exports = {
DateTime: require('./date'),
UniCrypto: require('./uni-crypto')
}
\ No newline at end of file
/**
* @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)
}
}
/**
* @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
}
}
/**
* @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
}
}
/**
* @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
}
}
/**
* @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 查询参数 where:where条件,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: []
}
}
}
}
/**
* @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
}
}
/**
* @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)
}
}
/**
* @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
}
}
/**
* @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
}
}
/**
* @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
}
}
/**
* @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
}
}
/**
* @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
}
}
/**
* 基础对外模型
*/
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'),
}
/**
* 设备/用户忠诚度(粘性)统计模型
*/
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
}
}
/**
* @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
}
}
/**
* @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
}
}
/**
* @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
}
}
/**
* @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
}
}
/**
* @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
}
}
/**
* @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;
}
}
/**
* @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
}
}
/**
* 基础对外模型
*/
module.exports = {
PayResult: require('./payResult'),
}
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册