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

uni-admin和uni-starter统一

上级 676c5f20
{ {
"name": "", "name" : "uni-starter",
"appid": "", "appid" : "__UNI__8E9C31E",
"description": "云端一体应用快速开发基本项目模版", "description" : "云端一体应用快速开发基本项目模版",
"versionName": "", "versionName" : "1.0.0",
"versionCode": "100", "versionCode" : "100",
"transformPx": false, "transformPx" : false,
"app-plus": { "app-plus" : {
"usingComponents": true, "usingComponents" : true,
"nvueStyleCompiler": "uni-app", "nvueStyleCompiler" : "uni-app",
"compilerVersion": 3, "compilerVersion" : 3,
"splashscreen": { "splashscreen" : {
"alwaysShowBeforeRender": true, "alwaysShowBeforeRender" : true,
"waiting": true, "waiting" : true,
"autoclose": true, "autoclose" : true,
"delay": 0 "delay" : 0
}, },
"modules": { "modules" : {},
}, "distribute" : {
"distribute": { "android" : {
"android": { "permissions" : [
"permissions": [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>", "<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>", "<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>", "<uses-permission android:name=\"android.permission.VIBRATE\"/>",
...@@ -37,35 +36,33 @@ ...@@ -37,35 +36,33 @@
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>" "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
] ]
}, },
"ios": { "ios" : {},
}, "sdkConfigs" : {
"sdkConfigs": { "push" : {
"push": { "unipush" : null
"unipush": null
} }
} }
} }
}, },
"quickapp": { "quickapp" : {},
}, "mp-weixin" : {
"mp-weixin": { "appid" : "",
"appid": "", "setting" : {
"setting": { "urlCheck" : false
"urlCheck": false
}, },
"usingComponents": true "usingComponents" : true
}, },
"mp-alipay": { "mp-alipay" : {
"usingComponents": true "usingComponents" : true
}, },
"mp-baidu": { "mp-baidu" : {
"usingComponents": true "usingComponents" : true
}, },
"mp-toutiao": { "mp-toutiao" : {
"usingComponents": true "usingComponents" : true
}, },
"uniStatistics": { "uniStatistics" : {
"enable": false "enable" : false
}, },
"vueVersion": "2" "vueVersion" : "2"
} }
...@@ -27,7 +27,9 @@ describe('pages/grid/grid.vue', () => { ...@@ -27,7 +27,9 @@ describe('pages/grid/grid.vue', () => {
} }
if (process.env.UNI_PLATFORM === "mp-weixin") { if (process.env.UNI_PLATFORM === "mp-weixin") {
const uniGrid = await page.$('uni-grid') const uniGrid = await page.$('uni-grid')
await page.waitFor(300)
await uniGrid.callMethod('change') await uniGrid.callMethod('change')
await page.waitFor(500)
} }
}) })
}); });
\ No newline at end of file
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
<!-- 通过body插槽定义作者信息内容 --> <!-- 通过body插槽定义作者信息内容 -->
<template v-slot:body> <template v-slot:body>
<view class="header-content"> <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> </view>
</template> </template>
<template v-slot:footer> <template v-slot:footer>
...@@ -77,7 +77,7 @@ ...@@ -77,7 +77,7 @@
title: 'title', 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: { formData: {
noData: '<p style="text-align:center;color:#666">详情加载中...</p>' noData: '<p style="text-align:center;color:#666">详情加载中...</p>'
} }
...@@ -93,7 +93,7 @@ ...@@ -93,7 +93,7 @@
} }
}, },
onLoad(event) { 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"} // 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 来自上一个页面 //获取真实新闻id,通常 id 来自上一个页面
if (event.id) { if (event.id) {
......
...@@ -11,9 +11,9 @@ ...@@ -11,9 +11,9 @@
<view class="cover-search-bar" @click="searchClick"></view> <view class="cover-search-bar" @click="searchClick"></view>
</view> </view>
<unicloud-db ref='udb' @load="loadData" v-slot:default="{data,pagination,hasMore, loading, error, options}" @error="onqueryerror" <unicloud-db ref='udb' v-slot:default="{data,pagination,hasMore, loading, error, options}" @error="onqueryerror"
:collection="colList" :page-size="10"> :collection="colList" :page-size="10" @load="loadData">
<!-- 基于 uni-list 的页面布局 field="user_id.username"--> <!-- 基于 uni-list 的页面布局 field="user_id.nickname"-->
<uni-list class="uni-list" :border="false" :style="{height:listHight}"> <uni-list class="uni-list" :border="false" :style="{height:listHight}">
<!-- 作用于app端nvue页面的下拉加载 --> <!-- 作用于app端nvue页面的下拉加载 -->
...@@ -22,7 +22,8 @@ ...@@ -22,7 +22,8 @@
<!-- #endif --> <!-- #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插槽定义列表左侧图片 --> <!-- 通过header插槽定义列表左侧图片 -->
<template v-slot:header> <template v-slot:header>
<image class="avatar" :src="item.avatar" mode="aspectFill"></image> <image class="avatar" :src="item.avatar" mode="aspectFill"></image>
...@@ -32,7 +33,7 @@ ...@@ -32,7 +33,7 @@
<view class="main"> <view class="main">
<text class="title">{{item.title}}</text> <text class="title">{{item.title}}</text>
<view class="info"> <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" <uni-dateformat class="last_modify_date" :date="item.last_modify_date"
format="yyyy-MM-dd" :threshold="[60000, 2592000000]" /> format="yyyy-MM-dd" :threshold="[60000, 2592000000]" />
</view> </view>
...@@ -53,6 +54,7 @@ ...@@ -53,6 +54,7 @@
<!-- #endif --> <!-- #endif -->
</uni-list> </uni-list>
</unicloud-db> </unicloud-db>
</view> </view>
</template> </template>
...@@ -80,7 +82,7 @@ ...@@ -80,7 +82,7 @@
return [ return [
db.collection('opendb-news-articles').where(this.where).field( db.collection('opendb-news-articles').where(this.where).field(
'avatar,title,last_modify_date,user_id').getTemp(), '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 @@ ...@@ -90,7 +92,7 @@
keyword: "", keyword: "",
showRefresh: false, showRefresh: false,
listHight: 0, listHight: 0,
dataList:[] dataList: []
} }
}, },
watch: { watch: {
...@@ -169,7 +171,7 @@ ...@@ -169,7 +171,7 @@
cdbRef.loadMore() cdbRef.loadMore()
}, },
onqueryerror(e) { onqueryerror(e) {
console.error("失败--",e); console.error(e);
}, },
onpullingdown(e) { onpullingdown(e) {
console.log(e); console.log(e);
......
...@@ -9,7 +9,7 @@ describe('pages/list/list.vue', () => { ...@@ -9,7 +9,7 @@ describe('pages/list/list.vue', () => {
it('检测标题', async () => { it('检测标题', async () => {
// expect.assertions(1); // expect.assertions(1);
const getData = await page.data('dataList') const getData = await page.data('dataList')
// console.log("getData: ",getData); console.log("getData: ",getData);
expect(getData.title).toBe('阿里小程序IDE官方内嵌uni-app,为开发者提供多端开发服务') expect(getData.title).toBe('阿里小程序IDE官方内嵌uni-app,为开发者提供多端开发服务')
}) })
......
...@@ -78,7 +78,6 @@ ...@@ -78,7 +78,6 @@
}, },
created() { created() {
this.about = this.uniStarterConfig.about this.about = this.uniStarterConfig.about
console.log("this.about: ",this.about);
uni.setNavigationBarTitle({ uni.setNavigationBarTitle({
title: this.$t('about.about')+ " " + this.about.appName title: this.$t('about.about')+ " " + this.about.appName
}) })
......
...@@ -58,7 +58,6 @@ ...@@ -58,7 +58,6 @@
// #endif // #endif
data() { data() {
return { return {
uniToken: '',
gridList: [{ gridList: [{
"text": this.$t('mine.showText'), "text": this.$t('mine.showText'),
"icon": "chat" "icon": "chat"
...@@ -140,7 +139,8 @@ ...@@ -140,7 +139,8 @@
"style": "solid", // 边框样式 "style": "solid", // 边框样式
"radius": "100%" // 边框圆角,支持百分比 "radius": "100%" // 边框圆角,支持百分比
} }
} },
uniToken: ''
} }
}, },
onLoad() { onLoad() {
...@@ -154,11 +154,11 @@ ...@@ -154,11 +154,11 @@
}) })
//#endif //#endif
}, },
onShow() {},
onReady() { onReady() {
this.uniToken = uni.getStorageSync('uni_id_token') this.uniToken = uni.getStorageSync('uni_id_token')
console.log("uniToken: ", this.uniToken); console.log("uniToken: ", this.uniToken);
}, },
onShow() {},
computed: { computed: {
userInfo() { userInfo() {
return store.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 PageResult 页面结果统计模型
*/
const BaseMod = require('./base')
const Platform = require('./platform')
const Channel = require('./channel')
const Version = require('./version')
const SessionLog = require('./sessionLog')
const PageLog = require('./pageLog')
const ShareLog = require('./shareLog')
const {
DateTime
} = require('../lib')
module.exports = class PageResult extends BaseMod {
constructor() {
super()
this.tableName = 'page-result'
this.platforms = []
this.channels = []
this.versions = []
}
/**
* 数据统计
* @param {String} type 统计类型 hour:实时统计 day:按天统计,week:按周统计 month:按月统计
* @param {Date|Time} date 指定日期或时间戳
* @param {Boolean} reset 是否重置,为ture时会重置该批次数据
*/
async stat(type, date, reset) {
//允许的类型
const allowedType = ['day']
if (!allowedType.includes(type)) {
return {
code: 1002,
msg: 'This type is not allowed'
}
}
this.fillType = type
//获取当前统计的时间范围
const dateTime = new DateTime()
const dateDimension = dateTime.getTimeDimensionByType(type, -1, date)
this.startTime = dateDimension.startTime
this.endTime = dateDimension.endTime
if (this.debug) {
console.log('dimension time', this.startTime + '--' + this.endTime)
}
// 查看当前时间段日志是否已存在,防止重复执行
if (!reset) {
const checkRes = await this.getCollection(this.tableName).where({
start_time: this.startTime,
end_time: this.endTime
}).get()
if (checkRes.data.length > 0) {
console.error('This page stat log have exists')
return {
code: 1003,
msg: 'This page stat log have existed'
}
}
} else {
const delRes = await this.delete(this.tableName, {
start_time: this.startTime,
end_time: this.endTime
})
console.log('Delete old data result:', JSON.stringify(delRes))
}
// 数据获取
this.pageLog = new PageLog()
const statRes = await this.aggregate(this.pageLog.tableName, {
project: {
appid: 1,
version: 1,
platform: 1,
channel: 1,
page_id: 1,
create_time: 1
},
match: {
create_time: {
$gte: this.startTime,
$lte: this.endTime
}
},
group: {
_id: {
appid: '$appid',
version: '$version',
platform: '$platform',
channel: '$channel',
page_id: '$page_id'
},
visit_times: {
$sum: 1
}
},
sort: {
visit_times: 1
},
getAll: true
})
let res = {
code: 0,
msg: 'success'
}
if (this.debug) {
console.log('Page statRes', JSON.stringify(statRes))
}
if (statRes.data.length > 0) {
this.fillData = []
//获取填充数据
for (const i in statRes.data) {
await this.fill(statRes.data[i])
}
//数据批量入库
if (this.fillData.length > 0) {
res = await this.batchInsert(this.tableName, this.fillData)
}
}
return res
}
/**
* 页面统计数据填充
* @param {Object} data 统计数据
*/
async fill(data) {
// 平台信息
let platformInfo = null
if (this.platforms && this.platforms[data._id.platform]) {
//暂存下数据,减少读库
platformInfo = this.platforms[data._id.platform]
} else {
const platform = new Platform()
platformInfo = await platform.getPlatformAndCreate(data._id.platform, null)
if (!platformInfo || platformInfo.length === 0) {
platformInfo._id = ''
}
this.platforms[data._id.platform] = platformInfo
if (this.debug) {
console.log('platformInfo', JSON.stringify(platformInfo))
}
}
// 渠道信息
let channelInfo = null
const channelKey = data._id.appid + '_' + platformInfo._id + '_' + data._id.channel
if (this.channels && this.channels[channelKey]) {
channelInfo = this.channels[channelKey]
} else {
const channel = new Channel()
channelInfo = await channel.getChannelAndCreate(data._id.appid, platformInfo._id, data._id.channel)
if (!channelInfo || channelInfo.length === 0) {
channelInfo._id = ''
}
this.channels[channelKey] = channelInfo
if (this.debug) {
console.log('channelInfo', JSON.stringify(channelInfo))
}
}
// 版本信息
let versionInfo = null
const versionKey = data._id.appid + '_' + data._id.platform + '_' + data._id.version
if (this.versions && this.versions[versionKey]) {
versionInfo = this.versions[versionKey]
} else {
const version = new Version()
versionInfo = await version.getVersionAndCreate(data._id.appid, data._id.platform, data._id.version)
if (!versionInfo || versionInfo.length === 0) {
versionInfo._id = ''
}
this.versions[versionKey] = versionInfo
if (this.debug) {
console.log('versionInfo', JSON.stringify(versionInfo))
}
}
const matchCondition = data._id
Object.assign(matchCondition, {
create_time: {
$gte: this.startTime,
$lte: this.endTime
}
})
if (this.debug) {
console.log('matchCondition', JSON.stringify(matchCondition))
}
// 当前页面访问设备数
const statPageDeviceRes = await this.aggregate(this.pageLog.tableName, {
project: {
appid: 1,
version: 1,
platform: 1,
channel: 1,
device_id: 1,
page_id: 1,
create_time: 1
},
match: matchCondition,
group: [{
_id: {
device_id: '$device_id'
}
}, {
_id: {},
total_devices: {
$sum: 1
}
}]
})
let pageVisitDevices = 0
if (statPageDeviceRes.data.length > 0) {
pageVisitDevices = statPageDeviceRes.data[0].total_devices
}
// 当前页面访问人数
const statPageUserRes = await this.aggregate(this.pageLog.tableName, {
project: {
appid: 1,
version: 1,
platform: 1,
channel: 1,
uid: 1,
page_id: 1,
create_time: 1
},
match: {
...matchCondition,
uid: {
$ne: ''
}
},
group: [{
_id: {
uid: '$uid'
}
}, {
_id: {},
total_users: {
$sum: 1
}
}]
})
let pageVisitUsers = 0
if (statPageUserRes.data.length > 0) {
pageVisitUsers = statPageUserRes.data[0].total_users
}
// 退出次数
const sessionLog = new SessionLog()
let existTimes = 0
const existRes = await this.getCollection(sessionLog.tableName).where({
appid: data._id.appid,
version: data._id.version,
platform: data._id.platform,
channel: data._id.channel,
exit_page_id: data._id.page_id,
create_time: {
$gte: this.startTime,
$lte: this.endTime
}
}).count()
if (existRes && existRes.total > 0) {
existTimes = existRes.total
}
// 访问时长
const statPageDurationRes = await this.aggregate(this.pageLog.tableName, {
project: {
appid: 1,
version: 1,
platform: 1,
channel: 1,
previous_page_id: 1,
previous_page_duration: 1,
create_time: 1
},
match: {
appid: data._id.appid,
version: data._id.version,
platform: data._id.platform,
channel: data._id.channel,
previous_page_id: data._id.page_id,
create_time: {
$gte: this.startTime,
$lte: this.endTime
}
},
group: {
_id: {},
total_duration: {
$sum: '$previous_page_duration'
}
}
})
let totalDuration = 0
if (statPageDurationRes.data.length > 0) {
totalDuration = statPageDurationRes.data[0].total_duration
}
// 分享次数
const shareLog = new ShareLog()
const statShareRes = await this.aggregate(shareLog.tableName, {
project: {
appid: 1,
version: 1,
platform: 1,
channel: 1,
page_id: 1,
create_time: 1
},
match: {
appid: data._id.appid,
version: data._id.version,
platform: data._id.platform,
channel: data._id.channel,
page_id: data._id.page_id,
create_time: {
$gte: this.startTime,
$lte: this.endTime
}
},
group: {
_id: {},
share_count: {
$sum: 1
}
}
})
let shareCount = 0
if (statShareRes.data.length > 0) {
shareCount = statShareRes.data[0].share_count
}
// 作为入口页的总次数和总访问时长
const statPageEntryCountRes = await this.aggregate(this.pageLog.tableName, {
project: {
appid: 1,
version: 1,
platform: 1,
channel: 1,
previous_page_id: 1,
previous_page_duration: 1,
previous_page_is_entry: 1,
create_time: 1
},
match: {
appid: data._id.appid,
version: data._id.version,
platform: data._id.platform,
channel: data._id.channel,
previous_page_id: data._id.page_id,
previous_page_is_entry: 1,
create_time: {
$gte: this.startTime,
$lte: this.endTime
}
},
group: {
_id: {},
entry_count: {
$sum: 1
},
entry_duration: {
$sum: '$previous_page_duration'
}
}
})
let entryCount = 0
let entryDuration = 0
if (statPageEntryCountRes.data.length > 0) {
entryCount = statPageEntryCountRes.data[0].entry_count
entryDuration = statPageEntryCountRes.data[0].entry_duration
}
// 作为入口页的总设备数
const statPageEntryDevicesRes = await this.aggregate(this.pageLog.tableName, {
project: {
appid: 1,
version: 1,
platform: 1,
channel: 1,
device_id: 1,
previous_page_id: 1,
previous_page_is_entry: 1,
create_time: 1
},
match: {
appid: data._id.appid,
version: data._id.version,
platform: data._id.platform,
channel: data._id.channel,
previous_page_id: data._id.page_id,
previous_page_is_entry: 1,
create_time: {
$gte: this.startTime,
$lte: this.endTime
}
},
group: [{
_id: {
device_id: '$device_id'
}
}, {
_id: {},
entry_devices: {
$sum: 1
}
}]
})
let entryDevices = 0
if (statPageEntryDevicesRes.data.length > 0) {
entryDevices = statPageEntryDevicesRes.data[0].entry_devices
}
// 作为入口页的总人数
const statPageEntryUsersRes = await this.aggregate(this.pageLog.tableName, {
project: {
appid: 1,
version: 1,
platform: 1,
channel: 1,
uid: 1,
previous_page_id: 1,
previous_page_is_entry: 1,
create_time: 1
},
match: {
appid: data._id.appid,
version: data._id.version,
platform: data._id.platform,
channel: data._id.channel,
previous_page_id: data._id.page_id,
previous_page_is_entry: 1,
uid: {
$ne: ''
},
create_time: {
$gte: this.startTime,
$lte: this.endTime
}
},
group: [{
_id: {
uid: '$uid'
}
}, {
_id: {},
entry_users: {
$sum: 1
}
}]
})
let entryUsers = 0
if (statPageEntryUsersRes.data.length > 0) {
entryUsers = statPageEntryUsersRes.data[0].entry_users
}
// 跳出率
let bounceTimes = 0
const bounceRes = await this.getCollection(sessionLog.tableName).where({
appid: data._id.appid,
version: data._id.version,
platform: data._id.platform,
channel: data._id.channel,
entry_page_id: data._id.page_id,
page_count: 1,
create_time: {
$gte: this.startTime,
$lte: this.endTime
}
}).count()
if (bounceRes && bounceRes.total > 0) {
bounceTimes = bounceRes.total
}
let bounceRate = 0
if (bounceTimes > 0 && data.visit_times > 0) {
bounceRate = bounceTimes * 100 / data.visit_times
bounceRate = parseFloat(bounceRate.toFixed(2))
}
// 数据填充
const datetime = new DateTime()
const insertParams = {
appid: data._id.appid,
platform_id: platformInfo._id,
channel_id: channelInfo._id,
version_id: versionInfo._id,
page_id: data._id.page_id,
visit_times: data.visit_times,
visit_devices: pageVisitDevices,
visit_users: pageVisitUsers,
exit_times: existTimes,
duration: totalDuration > 0 ? totalDuration : 1,
share_count: shareCount,
entry_users: entryUsers,
entry_devices: entryDevices,
entry_count: entryCount,
entry_duration: entryDuration,
bounce_rate: bounceRate,
dimension: this.fillType,
stat_date: datetime.getDate('Ymd', this.startTime),
start_time: this.startTime,
end_time: this.endTime
}
this.fillData.push(insertParams)
return insertParams
}
}
/**
* @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 SessionLog 基础会话日志模型
*/
const BaseMod = require('./base')
const Page = require('./page')
const Platform = require('./platform')
const Channel = require('./channel')
const UserSessionLog = require('./userSessionLog')
const Device = require('./device')
const {
DateTime
} = require('../lib')
module.exports = class SessionLog extends BaseMod {
constructor() {
super()
this.tableName = 'session-logs'
}
/**
* 会话日志批量填充
* @param {Object} reportParams 上报参数
*/
async batchFill(reportParams) {
let params, pageInfo, nowTime, firstVistTime, lastVistTime;
const fillParams = []
const page = new Page()
const platform = new Platform()
const dateTime = new DateTime()
const channel = new Channel()
const device = new Device()
let res
for (const pk in reportParams) {
params = reportParams[pk]
res = await this.fill(params)
if (res.code) {
console.error(res.msg)
} else {
//添加设备信息
await device.setDevice(params)
}
}
return res
}
/**
* 会话日志填充
* @param {Object} params 上报参数
*/
async fill(params) {
// 应用信息
if (!params.ak) {
return {
code: 200,
msg: 'Parameter "ak" not found'
}
}
// 平台信息
if (!params.ut) {
return {
code: 200,
msg: 'Parameter "ut" not found'
}
}
// 设备信息
if (!params.did) {
return {
code: 200,
msg: 'Parameter "did" not found'
}
}
// 页面信息
const page = new Page()
const pageInfo = await page.getPageAndCreate(params.ak, params.url, params.ttpj)
if (!pageInfo || pageInfo.length === 0) {
return {
code: 300,
msg: 'Not found this entry page'
}
}
if (this.debug) {
console.log('pageInfo', JSON.stringify(pageInfo))
}
const platform = new Platform()
const dateTime = new DateTime()
const channel = new Channel()
const nowTime = dateTime.getTime()
const firstVistTime = params.fvts ? dateTime.strToTime(params.fvts) : nowTime
const lastVistTime = (params.lvts && params.lvts !== '0') ? dateTime.strToTime(params.lvts) : 0
const fillParams = {
appid: params.ak,
version: params.v ? params.v : '',
platform: platform.getPlatformCode(params.ut, params.p),
channel: channel.getChannelCode(params),
type: params.cst ? parseInt(params.cst) : 0,
// 访问设备
device_id: params.did,
//是否为首次访问,判断标准:最后一次访问时间为0
is_first_visit: (params.lt === '1' && !lastVistTime) ? 1 : 0,
first_visit_time: firstVistTime,
last_visit_time: nowTime,
visit_count: params.tvc ? parseInt(params.tvc) : 1,
// 用户相关
last_visit_user_id: params.uid ? params.uid : '',
// 页面相关
entry_page_id: pageInfo._id,
exit_page_id: pageInfo._id,
page_count: 0,
event_count: 0,
duration: 1,
// 版本
sdk_version: params.mpsdk ? params.mpsdk : '',
platform_version: params.mpv ? params.mpv : '',
// 设备相关
device_os_name: params.on ? params.on : platform.getOsName(params.p),
device_os_version: params.sv ? params.sv : '',
device_vendor: params.brand ? params.brand : '',
device_model: params.md ? params.md : '',
device_language: params.lang ? params.lang : '',
device_pixel_ratio: params.pr ? params.pr : '',
device_window_width: params.ww ? params.ww : '',
device_window_height: params.wh ? params.wh : '',
device_screen_width: params.sw ? params.sw : '',
device_screen_height: params.sh ? params.sh : '',
// 地区相关
location_ip: params.ip ? params.ip : '',
location_latitude: params.lat ? parseFloat(params.lat) : -1,
location_longitude: params.lng ? parseFloat(params.lng) : -1,
location_country: params.cn ? params.cn : '',
location_province: params.pn ? params.pn : '',
location_city: params.ct ? params.ct : '',
is_finish: 0,
create_time: nowTime
}
if(this.isHaveOldDeviceId(params)) {
fillParams.old_device_id = params.odid
}
const res = await this.insert(this.tableName, fillParams)
if (res && res.id) {
//填充用户的会话日志
if (params.uid) {
await new UserSessionLog().fill({
...params,
page_id: pageInfo._id,
sid: res.id
})
}
return {
code: 0,
msg: 'success',
data: {
pageId: pageInfo._id,
sessionLogId: res.id,
entryPageId: fillParams.entry_page_id,
eventCount: fillParams.event_count,
startTime: fillParams.first_visit_time,
createTime: fillParams.create_time,
pageCount: fillParams.page_count,
uid: fillParams.last_visit_user_id
}
}
} else {
return {
code: 500,
msg: 'Session log filled error'
}
}
}
/**
* 判断是否包含老版本sdk生成的device_id, 此功能用于处理前端sdk升级后 device_id 发生变化导致日活、留存数据不准确的问题
* @param {Object} params
*/
isHaveOldDeviceId(params) {
if(params.odid && params.odid !== params.did && params.odid !== 'YluY92BA6nJ6NfixI77sFQ%3D%3D&ie=1') {
console.log('params.odid', params.odid)
return true
}
return false
}
/**
* 获取会话
* @param {Object} params 上报参数
*/
async getSession(params) {
// 页面信息
const page = new Page()
const pageInfo = await page.getPageAndCreate(params.ak, params.url, params.ttpj)
if (!pageInfo || pageInfo.length === 0) {
return {
code: 300,
msg: 'Not found this entry page'
}
}
const platformObj = new Platform()
const platform = platformObj.getPlatformCode(params.ut, params.p)
// 查询日志
const sessionLogInfo = await this.getCollection(this.tableName).where({
appid: params.ak,
platform: platform,
device_id: params.did,
is_finish: 0
}).orderBy('create_time', 'desc').limit(1).get()
if (sessionLogInfo.data.length > 0) {
const userSessionLog = new UserSessionLog()
const sessionLogInfoData = sessionLogInfo.data[0]
// 最后一次访问时间距现在超过半小时算上次会话已结束并生成一次新的会话
let sessionExpireTime = this.getConfig('sessionExpireTime')
sessionExpireTime = sessionExpireTime ? sessionExpireTime : 1800
const sessionTime = new DateTime().getTime() - sessionLogInfoData.last_visit_time
if (sessionTime >= sessionExpireTime * 1000) {
if (this.debug) {
console.log('session log time expired', sessionTime)
}
await this.update(this.tableName, {
is_finish: 1
}, {
appid: params.ak,
platform: platform,
device_id: params.did,
is_finish: 0
})
//关闭用户会话
await userSessionLog.closeUserSession()
return await this.fill(params)
} else {
//如果当前会话切换了用户则生成新的用户会话
if (params.uid != sessionLogInfoData.last_visit_user_id) {
await userSessionLog.checkUserSession({
...params,
page_id: pageInfo._id,
sid: sessionLogInfoData._id,
last_visit_user_id: sessionLogInfoData.last_visit_user_id
})
await this.update(this.tableName, {
last_visit_user_id: params.uid ? params.uid : ''
}, {
_id: sessionLogInfoData._id
})
}
return {
code: 0,
msg: 'success',
data: {
pageId: pageInfo._id,
sessionLogId: sessionLogInfoData._id,
entryPageId: sessionLogInfoData.entry_page_id,
eventCount: sessionLogInfoData.event_count,
startTime: sessionLogInfoData.first_visit_time,
createTime: sessionLogInfoData.create_time,
pageCount: sessionLogInfoData.page_count,
uid: sessionLogInfoData.last_visit_user_id
}
}
}
} else {
return await this.fill(params)
}
}
/**
* 更新会话信息
* @param {String} sid 会话编号
* @param {Object} data 更新数据
*/
async updateSession(sid, data) {
const nowTime = new DateTime().getTime()
const accessTime = nowTime - data.createTime
const accessSenconds = accessTime > 1000 ? parseInt(accessTime / 1000) : 1
const updateData = {
last_visit_time: nowTime,
duration: accessSenconds,
}
//访问页面数量
if (data.addPageCount) {
updateData.page_count = data.pageCount
}
//最终访问的页面编号
if (data.pageId) {
updateData.exit_page_id = data.pageId
}
//产生事件次数
if (data.eventCount) {
updateData.event_count = data.eventCount
}
if (this.debug) {
console.log('update session log by sid-' + sid, updateData)
}
//更新会话
await this.update(this.tableName, updateData, {
_id: sid
})
//更新用户会话
if (data.uid) {
data.nowTime = nowTime
await new UserSessionLog().updateUserSession(sid, data)
}
return true
}
/**
* 清理日志数据
* @param {Number} days 保留天数, 留存统计需要计算30天后留存率,因此至少应保留31天的日志数据
*/
async clean(days) {
days = Math.max(parseInt(days), 1)
console.log('clean session logs - day:', days)
const dateTime = new DateTime()
const res = await this.delete(this.tableName, {
create_time: {
$lt: dateTime.getTimeBySetDays(0 - days)
}
})
if (!res.code) {
console.log('clean session log:', res)
}
return res
}
}
/**
* @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
}
}
/**
* @class StatResult 基础数据结果统计模型
*/
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 ErrorLog = require('./errorLog')
const ActiveDevices = require('./activeDevices')
const ActiveUsers = require('./activeUsers')
const UniIDUsers = require('./uniIDUsers')
const {
DateTime
} = require('../lib')
module.exports = class StatResult extends BaseMod {
constructor() {
super()
this.tableName = '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 = ['hour', 'day', 'week', 'month']
if (!allowedType.includes(type)) {
return {
code: 1002,
msg: 'This type is not allowed'
}
}
if (this.debug) {
console.log('result --type:' + type + ', date:' + date + ', reset:' + reset)
}
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({
dimension: this.fillType,
start_time: this.startTime,
end_time: this.endTime
}).get()
if (checkRes.data.length > 0) {
console.log('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))
}
// 周月数据单独统计
if (['week', 'month'].includes(this.fillType)) {
return await this.statWeekOrMonth()
}
// 数据获取
this.sessionLog = new SessionLog()
const statRes = await this.aggregate(this.sessionLog.tableName, {
project: {
appid: 1,
version: 1,
platform: 1,
channel: 1,
is_first_visit: 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'
},
new_device_count: {
$sum: '$is_first_visit'
},
page_view_count: {
$sum: '$page_count'
},
total_duration: {
$sum: '$duration'
},
session_times: {
$sum: 1
}
},
sort: {
new_device_count: 1,
page_view_count: 1,
session_times: 1
},
getAll: true
})
let res = {
code: 0,
msg: 'success'
}
if (this.debug) {
console.log('statRes', JSON.stringify(statRes))
}
this.fillData = []
this.composes = []
if (statRes.data.length > 0) {
for (const i in statRes.data) {
await this.fill(statRes.data[i])
}
}
//补充数据
await this.replenishStat()
if (this.fillData.length > 0) {
res = await this.batchInsert(this.tableName, this.fillData)
}
return res
}
/**
* 按周/月统计
*/
async statWeekOrMonth() {
const statRes = await this.aggregate(this.tableName, {
project: {
appid: 1,
version_id: 1,
platform_id: 1,
channel_id: 1,
new_device_count: 1,
new_user_count: 1,
page_visit_count: 1,
user_visit_times: 1,
app_launch_count: 1,
error_count: 1,
bounce_times: 1,
duration: 1,
user_duration: 1,
dimension: 1,
start_time: 1
},
match: {
dimension: 'day',
start_time: {
$gte: this.startTime,
$lte: this.endTime
}
},
group: {
_id: {
appid: '$appid',
version_id: '$version_id',
platform_id: '$platform_id',
channel_id: '$channel_id'
},
new_device_count: {
$sum: '$new_device_count'
},
new_user_count: {
$sum: '$new_user_count'
},
error_count: {
$sum: '$error_count'
},
page_count: {
$sum: '$page_visit_count'
},
total_duration: {
$sum: '$duration'
},
total_user_duration: {
$sum: '$user_duration'
},
total_user_session_times: {
$sum: '$user_session_times'
},
session_times: {
$sum: '$app_launch_count'
},
total_bounce_times: {
$sum: '$bounce_times'
}
},
sort: {
new_device_count: 1,
error_count: 1,
page_count: 1
},
getAll: true
})
let res = {
code: 0,
msg: 'success'
}
if (this.debug) {
console.log('statRes', JSON.stringify(statRes))
}
this.activeDevices = new ActiveDevices()
this.activeUsers = new ActiveUsers()
this.fillData = []
this.composes = []
if (statRes.data.length > 0) {
for (const i in statRes.data) {
await this.getWeekOrMonthData(statRes.data[i])
}
}
//补充数据
await this.replenishStat()
if (this.fillData.length > 0) {
res = await this.batchInsert(this.tableName, this.fillData)
}
return res
}
/**
* 获取周/月维度的填充数据
* @param {Object} data 统计数据
*/
async getWeekOrMonthData(data) {
const matchCondition = {
...data._id,
create_time: {
$gte: this.startTime,
$lte: this.endTime
}
}
// 查询活跃设备数
const statVisitDeviceRes = await this.getCollection(this.activeDevices.tableName).where({
...matchCondition,
dimension: this.fillType
}).count()
let activeDeviceCount = 0
if (statVisitDeviceRes && statVisitDeviceRes.total > 0) {
activeDeviceCount = statVisitDeviceRes.total
}
// 设备次均停留时长
let avgSessionTime = 0
if (data.total_duration > 0 && data.session_times > 0) {
avgSessionTime = Math.round(data.total_duration / data.session_times)
}
// 设均停留时长
let avgDeviceTime = 0
if (data.total_duration > 0 && activeDeviceCount > 0) {
avgDeviceTime = Math.round(data.total_duration / activeDeviceCount)
}
// 跳出率
let bounceRate = 0
if (data.total_bounce_times > 0 && data.session_times > 0) {
bounceRate = data.total_bounce_times * 100 / data.session_times
bounceRate = parseFloat(bounceRate.toFixed(2))
}
// 累计设备数
let totalDevices = data.new_device_count
const totalDeviceRes = await this.getCollection(this.tableName).where({
...matchCondition,
dimension: this.fillType,
start_time: {
$lt: this.startTime
}
}).orderBy('start_time', 'desc').limit(1).get()
if (totalDeviceRes && totalDeviceRes.data.length > 0) {
totalDevices += totalDeviceRes.data[0].total_devices
}
//活跃用户数
const statVisitUserRes = await this.getCollection(this.activeUsers.tableName).where({
...matchCondition,
dimension: this.fillType
}).count()
let activeUserCount = 0
if (statVisitUserRes && statVisitUserRes.total > 0) {
activeUserCount = statVisitUserRes.total
}
// 平台信息
let platformInfo = null
if (this.platforms && this.platforms[data._id.platform_id]) {
platformInfo = this.platforms[data._id.platform_id]
} else {
const platform = new Platform()
platformInfo = await this.getById(platform.tableName, data._id.platform_id)
if (!platformInfo || platformInfo.length === 0) {
platformInfo.code = ''
}
this.platforms[data._id.platform_id] = platformInfo
}
// 渠道信息
let channelInfo = null
if (this.channels && this.channels[data._id.channel_id]) {
channelInfo = this.channels[data._id.channel_id]
} else {
const channel = new Channel()
channelInfo = await this.getById(channel.tableName, data._id.channel_id)
if (!channelInfo || channelInfo.length === 0) {
channelInfo.channel_code = ''
}
this.channels[data._id.channel_id] = channelInfo
}
// 版本信息
let versionInfo = null
if (this.versions && this.versions[data._id.version_id]) {
versionInfo = this.versions[data._id.version_id]
} else {
const version = new Version()
versionInfo = await this.getById(version.tableName, data._id.version_id, false)
if (!versionInfo || versionInfo.length === 0) {
versionInfo.version = ''
}
this.versions[data._id.version_id] = versionInfo
}
//总用户数
const uniIDUsers = new UniIDUsers()
let totalUserCount = await uniIDUsers.getUserCount(data._id.appid, platformInfo.code, channelInfo.channel_code, versionInfo.version, {
$lte: this.endTime
})
//人均停留时长
let avgUserTime = 0
if (data.total_user_duration > 0 && activeUserCount > 0) {
avgUserTime = Math.round(data.total_user_duration / activeUserCount)
}
//用户次均访问时长
let avgUserSessionTime = 0
if (data.total_user_duration > 0 && data.total_user_session_times > 0) {
avgUserSessionTime = Math.round(data.total_user_duration / data.total_user_session_times)
}
//安卓平台活跃设备数需要减去sdk更新后device_id发生变更的设备数
if(platformInfo.code === 'android') {
try{
const statVisitOldDeviceRes = await this.getCollection(this.activeDevices.tableName).where({
...data._id,
create_time: {
$gte: this.startTime,
$lte: this.endTime
},
dimension: this.fillType + '-old'
}).count()
if (statVisitOldDeviceRes && statVisitOldDeviceRes.total > 0) {
// 活跃设备留存数
activeDeviceCount -= statVisitOldDeviceRes.total
//平均设备停留时长
avgDeviceTime = Math.round(data.total_duration / activeDeviceCount)
}
} catch (e) {
console.log('server error: ' + e)
}
}
const insertParam = {
appid: data._id.appid,
platform_id: data._id.platform_id,
channel_id: data._id.channel_id,
version_id: data._id.version_id,
//总设备数
total_devices: totalDevices,
//本时间段新增设备数
new_device_count: data.new_device_count,
//登录用户会话次数
user_session_times: data.total_user_session_times,
//活跃设备数
active_device_count: activeDeviceCount,
//总用户数
total_users: totalUserCount,
//新增用户数
new_user_count: data.new_user_count,
//活跃用户数
active_user_count: activeUserCount,
//应用启动次数 = 设备会话次数
app_launch_count: data.session_times,
//页面访问次数
page_visit_count: data.page_view_count,
//错误次数
error_count: data.error_count,
//会话总访问时长
duration: data.total_duration,
//用户会话总访问时长
user_duration: data.total_user_duration,
avg_device_session_time: avgSessionTime,
avg_user_session_time: avgUserSessionTime,
avg_device_time: avgDeviceTime,
avg_user_time: avgUserTime,
bounce_times: data.total_bounce_times,
bounce_rate: bounceRate,
retention: {},
dimension: this.fillType,
stat_date: new DateTime().getDate('Ymd', this.startTime),
start_time: this.startTime,
end_time: this.endTime
}
this.fillData.push(insertParam)
this.composes.push(data._id.appid + '_' + data._id.platform_id + '_' + data._id.channel_id + '_' + data
._id.version_id)
return insertParam
}
/**
* 基础填充数据-目前只有小时和天维度的数据
* @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
}
})
const statVisitDeviceRes = await this.aggregate(this.sessionLog.tableName, {
project: {
appid: 1,
version: 1,
platform: 1,
channel: 1,
device_id: 1,
create_time: 1
},
match: matchCondition,
group: [{
_id: {
device_id: '$device_id'
}
}, {
_id: {},
total_devices: {
$sum: 1
}
}]
})
let activeDeviceCount = 0
if (statVisitDeviceRes.data.length > 0) {
activeDeviceCount = statVisitDeviceRes.data[0].total_devices
}
//安卓平台活跃设备数需要减去sdk更新后device_id发生变更的设备数
if(platformInfo.code === 'android') {
const oldDeviceRes = await this.aggregate(this.sessionLog.tableName, {
project: {
appid: 1,
version: 1,
platform: 1,
channel: 1,
old_device_id: 1,
create_time: 1
},
match: matchCondition,
group: {
_id: {
device_id: '$old_device_id'
},
create_time: {
$min: '$create_time'
},
sessionCount: {
$sum: 1
}
},
sort: {
create_time: 1,
sessionCount: 1
},
getAll: true
})
if(oldDeviceRes.data.length) {
const thisOldDeviceIds = []
for (const tau in oldDeviceRes.data) {
if(oldDeviceRes.data[tau]._id.device_id) {
thisOldDeviceIds.push(oldDeviceRes.data[tau]._id.device_id)
}
}
const statVisitOldDeviceRes = await this.aggregate(this.sessionLog.tableName, {
project: {
appid: 1,
version: 1,
platform: 1,
channel: 1,
device_id: 1,
create_time: 1
},
match: {
...matchCondition,
device_id: {
$in: thisOldDeviceIds
}
},
group: [{
_id: {
device_id: '$device_id'
}
}, {
_id: {},
total_devices: {
$sum: 1
}
}]
})
if (this.debug) {
console.log('statVisitOldDeviceRes', JSON.stringify(statVisitOldDeviceRes))
}
if (statVisitOldDeviceRes && statVisitOldDeviceRes.data.length > 0) {
// 活跃设备留存数
activeDeviceCount -= statVisitOldDeviceRes.data[0].total_devices
}
}
}
// 错误数量统计
const errorLog = new ErrorLog()
const statErrorRes = await this.getCollection(errorLog.tableName).where(matchCondition).count()
let errorCount = 0
if (statErrorRes && statErrorRes.total > 0) {
errorCount = statErrorRes.total
}
// 设备的次均停留时长,设备访问总时长/设备会话次数
let avgSessionTime = 0
if (data.total_duration > 0 && data.session_times > 0) {
avgSessionTime = Math.round(data.total_duration / data.session_times)
}
// 设均停留时长
let avgDeviceTime = 0
if (data.total_duration > 0 && activeDeviceCount > 0) {
avgDeviceTime = Math.round(data.total_duration / activeDeviceCount)
}
// 跳出率
let bounceTimes = 0
const bounceRes = await this.getCollection(this.sessionLog.tableName).where({
...matchCondition,
page_count: 1
}).count()
if (bounceRes && bounceRes.total > 0) {
bounceTimes = bounceRes.total
}
let bounceRate = 0
if (bounceTimes > 0 && data.session_times > 0) {
bounceRate = bounceTimes * 100 / data.session_times
bounceRate = parseFloat(bounceRate.toFixed(2))
}
// 应用启动次数 = 会话次数
const launchCount = data.session_times
// 累计设备数
let totalDevices = data.new_device_count
const totalDeviceRes = await this.getCollection(this.tableName).where({
appid: data._id.appid,
platform_id: platformInfo._id,
channel_id: channelInfo._id,
version_id: versionInfo._id,
dimension: this.fillType,
start_time: {
$lt: this.startTime
}
}).orderBy('start_time', 'desc').limit(1).get()
if (totalDeviceRes && totalDeviceRes.data.length > 0) {
totalDevices += totalDeviceRes.data[0].total_devices
}
//活跃用户数
const userSessionLog = new UserSessionLog()
let activeUserCount = 0
const activeUserCountRes = await this.aggregate(userSessionLog.tableName, {
project: {
appid: 1,
version: 1,
platform: 1,
channel: 1,
uid: 1,
create_time: 1
},
match: matchCondition,
group: [{
_id: {
uid: '$uid'
}
}, {
_id: {},
total_users: {
$sum: 1
}
}]
})
if (activeUserCountRes && activeUserCountRes.data.length > 0) {
activeUserCount = activeUserCountRes.data[0].total_users
}
//新增用户数
const uniIDUsers = new UniIDUsers()
let newUserCount = await uniIDUsers.getUserCount(matchCondition.appid, matchCondition.platform,
matchCondition.channel, matchCondition.version, {
$gte: this.startTime,
$lte: this.endTime
})
//总用户数
let totalUserCount = await uniIDUsers.getUserCount(matchCondition.appid, matchCondition.platform,
matchCondition.channel, matchCondition.version, {
$lte: this.endTime
})
//用户停留总时长及总会话次数
let totalUserDuration = 0
let totalUserSessionTimes = 0
const totalUserDurationRes = await this.aggregate(userSessionLog.tableName, {
project: {
appid: 1,
version: 1,
platform: 1,
channel: 1,
duration: 1,
create_time: 1
},
match: matchCondition,
group: [{
_id: {},
total_duration: {
$sum: "$duration"
},
total_session_times: {
$sum: 1
}
}]
})
if (totalUserDurationRes && totalUserDurationRes.data.length > 0) {
totalUserDuration = totalUserDurationRes.data[0].total_duration
totalUserSessionTimes = totalUserDurationRes.data[0].total_session_times
}
//人均停留时长
let avgUserTime = 0
//用户次均访问时长
let avgUserSessionTime = 0
if (totalUserDuration > 0 && activeUserCount > 0) {
avgUserTime = Math.round(totalUserDuration / activeUserCount)
avgUserSessionTime = Math.round(totalUserDuration / totalUserSessionTimes)
}
//设置填充数据
const datetime = new DateTime()
const insertParam = {
appid: data._id.appid,
platform_id: platformInfo._id,
channel_id: channelInfo._id,
version_id: versionInfo._id,
total_devices: totalDevices,
new_device_count: data.new_device_count,
active_device_count: activeDeviceCount,
total_users: totalUserCount,
new_user_count: newUserCount,
active_user_count: activeUserCount,
user_session_times: totalUserSessionTimes,
app_launch_count: launchCount,
page_visit_count: data.page_view_count,
error_count: errorCount,
duration: data.total_duration,
user_duration: totalUserDuration,
avg_device_session_time: avgSessionTime,
avg_user_session_time: avgUserSessionTime,
avg_device_time: avgDeviceTime,
avg_user_time: avgUserTime,
bounce_times: bounceTimes,
bounce_rate: bounceRate,
retention: {},
dimension: this.fillType,
stat_date: datetime.getDate('Ymd', this.startTime),
start_time: this.startTime,
end_time: this.endTime
}
//数据填充
this.fillData.push(insertParam)
this.composes.push(data._id.appid + '_' + platformInfo._id + '_' + channelInfo._id + '_' + versionInfo
._id)
return insertParam
}
/**
* 基础统计数据补充,防止累计数据丢失
*/
async replenishStat() {
if (this.debug) {
console.log('composes data', this.composes)
}
const datetime = new DateTime()
// const {
// startTime
// } = datetime.getTimeDimensionByType(this.fillType, -1, this.startTime)
//上一次统计时间
let preStatRes = await this.getCollection(this.tableName).where({
start_time: {$lt: this.startTime},
dimension: this.fillType
}).orderBy('start_time', 'desc').limit(1).get()
let preStartTime = 0
if(preStatRes && preStatRes.data.length > 0) {
preStartTime = preStatRes.data[0].start_time
}
if (this.debug) {
console.log('replenishStat-preStartTime', preStartTime)
}
if(!preStartTime) {
return false
}
// 上一阶段数据
const preStatData = await this.selectAll(this.tableName, {
start_time: preStartTime,
dimension: this.fillType
}, {
appid: 1,
platform_id: 1,
channel_id: 1,
version_id: 1,
total_devices: 1,
total_users: 1
})
if (!preStatData || preStatData.data.length === 0) {
return false
}
if (this.debug) {
console.log('preStatData', JSON.stringify(preStatData))
}
let preKey
const preKeyArr = []
for (const pi in preStatData.data) {
preKey = preStatData.data[pi].appid + '_' + preStatData.data[pi].platform_id + '_' + preStatData
.data[pi].channel_id + '_' + preStatData.data[pi].version_id
if (!this.composes.includes(preKey) && !preKeyArr.includes(preKey)) {
preKeyArr.push(preKey)
if (this.debug) {
console.log('preKey -add', preKey)
}
this.fillData.push({
appid: preStatData.data[pi].appid,
platform_id: preStatData.data[pi].platform_id,
channel_id: preStatData.data[pi].channel_id,
version_id: preStatData.data[pi].version_id,
total_devices: preStatData.data[pi].total_devices,
new_device_count: 0,
active_device_count: 0,
total_users: preStatData.data[pi].total_users,
new_user_count: 0,
active_user_count: 0,
user_session_times: 0,
app_launch_count: 0,
page_visit_count: 0,
error_count: 0,
duration: 0,
user_duration: 0,
avg_device_session_time: 0,
avg_user_session_time: 0,
avg_device_time: 0,
avg_user_time: 0,
bounce_times: 0,
bounce_rate: 0,
retention: {},
dimension: this.fillType,
stat_date: datetime.getDate('Ymd', this.startTime),
start_time: this.startTime,
end_time: this.endTime
})
} else if (this.debug) {
console.log('preKey -have', preKey)
}
}
return true
}
/**
* 留存数据统计
* @param {String} type 统计类型 hour:实时统计 day:按天统计,week:按周统计 month:按月统计
* @param {Date|Time} date 指定日期或时间戳
* @param {String} mod 统计模块 device:设备,user:用户
*/
async retentionStat(type, date, mod = 'device') {
date = date ? date : new DateTime().getTimeBySetDays(-1, date)
const allowedType = ['day', 'week', 'month']
if (!allowedType.includes(type)) {
return {
code: 1002,
msg: 'This type is not allowed'
}
}
let days = []
switch (type) {
case 'week':
case 'month':
days = [1, 2, 3, 4, 5, 6, 7, 8, 9]
break
default:
days = [1, 2, 3, 4, 5, 6, 7, 14, 30]
break
}
let res = {
code: 0,
msg: 'success'
}
for (const day in days) {
//留存统计数据库查询较为复杂,因此这里拆分为多个维度
if (mod && mod === 'user') {
//用户留存统计
if (type === 'day') {
res = await this.userRetentionFillDayly(type, days[day], date)
} else {
res = await this.userRetentionFillWeekOrMonth(type, days[day], date)
}
} else {
//设备留存统计
if (type === 'day') {
res = await this.deviceRetentionFillDayly(type, days[day], date)
} else {
res = await this.deviceRetentionFillWeekOrMonth(type, days[day], date)
}
}
}
return res
}
/**
* 设备日留存统计数据填充
* @param {String} type 统计类型 hour:实时统计 day:按天统计,week:按周统计 month:按月统计
* @param {Date|Time} date 指定日期或时间戳
* @param {Boolean} reset 是否重置,为ture时会重置该批次数据
*/
async deviceRetentionFillDayly(type, day, date) {
if (type !== 'day') {
return {
code: 301,
msg: 'Type error:' + type
}
}
const dateTime = new DateTime()
const {
startTime,
endTime
} = dateTime.getTimeDimensionByType(type, 0 - day, date)
if (!startTime || !endTime) {
return {
code: 1001,
msg: 'The statistic time get failed'
}
}
// 截止时间范围
const lastTimeInfo = dateTime.getTimeDimensionByType(type, 0, date)
// 获取当时批次的统计日志
const resultLogRes = await this.selectAll(this.tableName, {
dimension: type,
start_time: startTime,
end_time: endTime
})
// const resultLogRes = await this.getCollection(this.tableName).where({
// dimension: type,
// start_time: startTime,
// end_time: endTime
// }).get()
if (this.debug) {
console.log('resultLogRes', JSON.stringify(resultLogRes))
}
if (!resultLogRes || resultLogRes.data.length === 0) {
if (this.debug) {
console.log('Not found this log --' + type + ':' + day + ', start:' + startTime + ',endTime:' +
endTime)
}
return {
code: 1000,
msg: 'Not found this log'
}
}
const sessionLog = new SessionLog()
const platform = new Platform()
const channel = new Channel()
const version = new Version()
let res = null
for (const resultIndex in resultLogRes.data) {
const resultLog = resultLogRes.data[resultIndex]
// 平台信息
let platformInfo = null
if (this.platforms && this.platforms[resultLog.platform_id]) {
platformInfo = this.platforms[resultLog.platform_id]
} else {
platformInfo = await this.getById(platform.tableName, resultLog.platform_id)
if (!platformInfo || platformInfo.length === 0) {
platformInfo.code = ''
}
this.platforms[resultLog.platform_id] = platformInfo
if (this.debug) {
console.log('platformInfo', JSON.stringify(platformInfo))
}
}
// 渠道信息
let channelInfo = null
if (this.channels && this.channels[resultLog.channel_id]) {
channelInfo = this.channels[resultLog.channel_id]
} else {
channelInfo = await this.getById(channel.tableName, resultLog.channel_id)
if (!channelInfo || channelInfo.length === 0) {
channelInfo.channel_code = ''
}
this.channels[resultLog.channel_id] = channelInfo
if (this.debug) {
console.log('channelInfo', JSON.stringify(channelInfo))
}
}
// 版本信息
let versionInfo = null
if (this.versions && this.versions[resultLog.version_id]) {
versionInfo = this.versions[resultLog.version_id]
} else {
versionInfo = await this.getById(version.tableName, resultLog.version_id, false)
if (!versionInfo || versionInfo.length === 0) {
versionInfo.version = ''
}
this.versions[resultLog.version_id] = versionInfo
if (this.debug) {
console.log('versionInfo', JSON.stringify(versionInfo))
}
}
// 获取该批次的活跃设备数
const activeDeviceRes = await this.aggregate(sessionLog.tableName, {
project: {
appid: 1,
version: 1,
platform: 1,
channel: 1,
device_id: 1,
create_time: 1
},
match: {
appid: resultLog.appid,
version: versionInfo.version,
platform: platformInfo.code,
channel: channelInfo.channel_code,
create_time: {
$gte: startTime,
$lte: endTime
}
},
group: {
_id: {
device_id: '$device_id'
},
create_time: {
$min: '$create_time'
},
sessionCount: {
$sum: 1
}
},
sort: {
create_time: 1,
sessionCount: 1
},
getAll: true
})
if (this.debug) {
console.log('activeDeviceRes', JSON.stringify(activeDeviceRes))
}
let activeDeviceRate = 0
let activeDevices = 0
if (activeDeviceRes && activeDeviceRes.data.length > 0) {
const thisDayActiveDevices = activeDeviceRes.data.length
const thisDayActiveDeviceIds = []
for (const tau in activeDeviceRes.data) {
thisDayActiveDeviceIds.push(activeDeviceRes.data[tau]._id.device_id)
}
if (this.debug) {
console.log('thisDayActiveDeviceIds', JSON.stringify(thisDayActiveDeviceIds))
}
// 留存活跃设备查询
const retentionActiveDeviceRes = await this.aggregate(sessionLog.tableName, {
project: {
appid: 1,
version: 1,
platform: 1,
channel: 1,
device_id: 1,
create_time: 1
},
match: {
appid: resultLog.appid,
version: versionInfo.version,
platform: platformInfo.code,
channel: channelInfo.channel_code,
device_id: {
$in: thisDayActiveDeviceIds
},
create_time: {
$gte: lastTimeInfo.startTime,
$lte: lastTimeInfo.endTime
}
},
group: [{
_id: {
device_id: '$device_id'
}
}, {
_id: {},
total_devices: {
$sum: 1
}
}]
})
if (this.debug) {
console.log('retentionActiveDeviceRes', JSON.stringify(retentionActiveDeviceRes))
}
if (retentionActiveDeviceRes && retentionActiveDeviceRes.data.length > 0) {
// 活跃设备留存数
activeDevices = retentionActiveDeviceRes.data[0].total_devices
// 活跃设备留存率
activeDeviceRate = parseFloat((activeDevices * 100 / thisDayActiveDevices).toFixed(2))
}
//安卓平台留存需要增加sdk更新后device_id发生变更的设备数
if(platformInfo.code === 'android') {
const retentionActiveOldDeviceRes = await this.aggregate(sessionLog.tableName, {
project: {
appid: 1,
version: 1,
platform: 1,
channel: 1,
old_device_id: 1,
create_time: 1
},
match: {
appid: resultLog.appid,
version: versionInfo.version,
platform: platformInfo.code,
channel: channelInfo.channel_code,
old_device_id: {
$in: thisDayActiveDeviceIds
},
create_time: {
$gte: lastTimeInfo.startTime,
$lte: lastTimeInfo.endTime
}
},
group: [{
_id: {
device_id: '$old_device_id'
}
}, {
_id: {},
total_devices: {
$sum: 1
}
}]
})
if (this.debug) {
console.log('retentionActiveOldDeviceRes', JSON.stringify(retentionActiveOldDeviceRes))
}
if (retentionActiveOldDeviceRes && retentionActiveOldDeviceRes.data.length > 0) {
// 活跃设备留存数
activeDevices += retentionActiveOldDeviceRes.data[0].total_devices
// 活跃设备留存率
activeDeviceRate = parseFloat((activeDevices * 100 / thisDayActiveDevices).toFixed(2))
}
}
}
// 获取该批次新增设备数
const newDeviceRes = await this.aggregate(sessionLog.tableName, {
project: {
appid: 1,
version: 1,
platform: 1,
channel: 1,
is_first_visit: 1,
device_id: 1,
create_time: 1
},
match: {
appid: resultLog.appid,
version: versionInfo.version,
platform: platformInfo.code,
channel: channelInfo.channel_code,
is_first_visit: 1,
create_time: {
$gte: startTime,
$lte: endTime
}
},
group: {
_id: {
device_id: '$device_id'
},
create_time: {
$min: '$create_time'
},
sessionCount: {
$sum: 1
}
},
sort: {
create_time: 1,
sessionCount: 1
},
getAll: true
})
let newDeviceRate = 0
let newDevices = 0
if (newDeviceRes && newDeviceRes.data.length > 0) {
const thisDayNewDevices = newDeviceRes.data.length
const thisDayNewDeviceIds = []
for (const tau in newDeviceRes.data) {
thisDayNewDeviceIds.push(newDeviceRes.data[tau]._id.device_id)
}
if (this.debug) {
console.log('thisDayNewDeviceIds', JSON.stringify(thisDayNewDeviceIds))
}
// 留存的设备查询
const retentionNewDeviceRes = await this.aggregate(sessionLog.tableName, {
project: {
appid: 1,
version: 1,
platform: 1,
channel: 1,
device_id: 1,
create_time: 1
},
match: {
appid: resultLog.appid,
version: versionInfo.version,
platform: platformInfo.code,
channel: channelInfo.channel_code,
device_id: {
$in: thisDayNewDeviceIds
},
create_time: {
$gte: lastTimeInfo.startTime,
$lte: lastTimeInfo.endTime
}
},
group: [{
_id: {
device_id: '$device_id'
}
}, {
_id: {},
total_devices: {
$sum: 1
}
}]
})
if (retentionNewDeviceRes && retentionNewDeviceRes.data.length > 0) {
// 新增设备留存数
newDevices = retentionNewDeviceRes.data[0].total_devices
// 新增设备留存率
newDeviceRate = parseFloat((newDevices * 100 / thisDayNewDevices).toFixed(2))
}
//安卓平台留存需要增加sdk更新后device_id发生变更的设备数
if(platformInfo.code === 'android') {
const retentionNewOldDeviceRes = await this.aggregate(sessionLog.tableName, {
project: {
appid: 1,
version: 1,
platform: 1,
channel: 1,
old_device_id: 1,
create_time: 1
},
match: {
appid: resultLog.appid,
version: versionInfo.version,
platform: platformInfo.code,
channel: channelInfo.channel_code,
old_device_id: {
$in: thisDayNewDeviceIds
},
create_time: {
$gte: lastTimeInfo.startTime,
$lte: lastTimeInfo.endTime
}
},
group: [{
_id: {
device_id: '$old_device_id'
}
}, {
_id: {},
total_devices: {
$sum: 1
}
}]
})
if (this.debug) {
console.log('retentionNewOldDeviceRes', JSON.stringify(retentionNewOldDeviceRes))
}
if (retentionNewOldDeviceRes && retentionNewOldDeviceRes.data.length > 0) {
// 新增设备留存数
newDevices += retentionNewOldDeviceRes.data[0].total_devices
// 新增设备留存率
newDeviceRate = parseFloat((newDevices * 100 / thisDayNewDevices).toFixed(2))
}
}
}
// 数据更新
const retentionData = resultLog.retention
const dataKey = type.substr(0, 1) + '_' + day
if (!retentionData.active_device) {
retentionData.active_device = {}
}
retentionData.active_device[dataKey] = {
device_count: activeDevices,
device_rate: activeDeviceRate
}
if (!retentionData.new_device) {
retentionData.new_device = {}
}
retentionData.new_device[dataKey] = {
device_count: newDevices,
device_rate: newDeviceRate
}
if (this.debug) {
console.log('retentionData', JSON.stringify(retentionData))
}
res = await this.update(this.tableName, {
retention: retentionData
}, {
_id: resultLog._id
})
}
if (res && res.updated) {
return {
code: 0,
msg: 'success'
}
} else {
return {
code: 500,
msg: 'retention data update failed'
}
}
}
/**
* 设备周/月留存数据填充
* @param {String} type 统计类型 hour:实时统计 day:按天统计,week:按周统计 month:按月统计
* @param {Date|Time} date 指定日期或时间戳
* @param {Boolean} reset 是否重置,为ture时会重置该批次数据
*/
async deviceRetentionFillWeekOrMonth(type, day, date) {
if (!['week', 'month'].includes(type)) {
return {
code: 301,
msg: 'Type error:' + type
}
}
const dateTime = new DateTime()
const {
startTime,
endTime
} = dateTime.getTimeDimensionByType(type, 0 - day, date)
if (!startTime || !endTime) {
return {
code: 1001,
msg: 'The statistic time get failed'
}
}
// 截止时间范围
const lastTimeInfo = dateTime.getTimeDimensionByType(type, 0, date)
// 获取当时批次的统计日志
const resultLogRes = await this.selectAll(this.tableName, {
dimension: type,
start_time: startTime,
end_time: endTime
})
if (this.debug) {
console.log('resultLogRes', JSON.stringify(resultLogRes))
}
if (!resultLogRes || resultLogRes.data.length === 0) {
if (this.debug) {
console.log('Not found this session log --' + type + ':' + day + ', start:' + startTime +
',endTime:' + endTime)
}
return {
code: 1000,
msg: 'Not found this session log'
}
}
const activeDevicesObj = new ActiveDevices()
let res = null;
let activeDeviceRes;
let activeDeviceRate;
let activeDevices;
let newDeviceRate;
let newDevices
for (const resultIndex in resultLogRes.data) {
const resultLog = resultLogRes.data[resultIndex]
// 获取该批次的活跃设备数
activeDeviceRes = await this.selectAll(activeDevicesObj.tableName, {
appid: resultLog.appid,
platform_id: resultLog.platform_id,
channel_id: resultLog.channel_id,
version_id: resultLog.version_id,
dimension: type,
create_time: {
$gte: startTime,
$lte: endTime
}
}, {
device_id: 1
})
if (this.debug) {
console.log('activeDeviceRes', JSON.stringify(activeDeviceRes))
}
activeDeviceRate = 0
activeDevices = 0
if (activeDeviceRes && activeDeviceRes.data.length > 0) {
const thisDayActiveDevices = activeDeviceRes.data.length
const thisDayActiveDeviceIds = []
for (const tau in activeDeviceRes.data) {
thisDayActiveDeviceIds.push(activeDeviceRes.data[tau].device_id)
}
if (this.debug) {
console.log('thisDayActiveDeviceIds', JSON.stringify(thisDayActiveDeviceIds))
}
// 留存活跃设备数
const retentionActiveDeviceRes = await this.getCollection(activeDevicesObj.tableName).where({
appid: resultLog.appid,
platform_id: resultLog.platform_id,
channel_id: resultLog.channel_id,
version_id: resultLog.version_id,
device_id: {
$in: thisDayActiveDeviceIds
},
dimension: type,
create_time: {
$gte: lastTimeInfo.startTime,
$lte: lastTimeInfo.endTime
}
}).count()
if (this.debug) {
console.log('retentionActiveDeviceRes', JSON.stringify(retentionActiveDeviceRes))
}
if (retentionActiveDeviceRes && retentionActiveDeviceRes.total > 0) {
// 活跃设备留存数
activeDevices = retentionActiveDeviceRes.total
// 活跃设备留存率
activeDeviceRate = parseFloat((activeDevices * 100 / thisDayActiveDevices).toFixed(2))
}
}
// 获取该批次的新增设备数
const newDeviceRes = await this.selectAll(activeDevicesObj.tableName, {
appid: resultLog.appid,
platform_id: resultLog.platform_id,
channel_id: resultLog.channel_id,
version_id: resultLog.version_id,
is_new: 1,
dimension: type,
create_time: {
$gte: startTime,
$lte: endTime
}
}, {
device_id: 1
})
newDeviceRate = 0
newDevices = 0
if (newDeviceRes && newDeviceRes.data.length > 0) {
const thisDayNewDevices = newDeviceRes.data.length
const thisDayNewDeviceIds = []
for (const tau in newDeviceRes.data) {
thisDayNewDeviceIds.push(newDeviceRes.data[tau].device_id)
}
// 新增设备留存数
const retentionNewDeviceRes = await this.getCollection(activeDevicesObj.tableName).where({
appid: resultLog.appid,
platform_id: resultLog.platform_id,
channel_id: resultLog.channel_id,
version_id: resultLog.version_id,
device_id: {
$in: thisDayNewDeviceIds
},
dimension: type,
create_time: {
$gte: lastTimeInfo.startTime,
$lte: lastTimeInfo.endTime
}
}).count()
if (retentionNewDeviceRes && retentionNewDeviceRes.total > 0) {
// 新增设备留存数
newDevices = retentionNewDeviceRes.total
// 新增设备留存率
newDeviceRate = parseFloat((newDevices * 100 / thisDayNewDevices).toFixed(2))
}
}
// 数据更新
const retentionData = resultLog.retention
const dataKey = type.substr(0, 1) + '_' + day
if (!retentionData.active_device) {
retentionData.active_device = {}
}
retentionData.active_device[dataKey] = {
device_count: activeDevices,
device_rate: activeDeviceRate
}
if (!retentionData.new_device) {
retentionData.new_device = {}
}
retentionData.new_device[dataKey] = {
device_count: newDevices,
device_rate: newDeviceRate
}
if (this.debug) {
console.log('retentionData', JSON.stringify(retentionData))
}
res = await this.update(this.tableName, {
retention: retentionData
}, {
_id: resultLog._id
})
}
if (res && res.updated) {
return {
code: 0,
msg: 'success'
}
} else {
return {
code: 500,
msg: 'retention data update failed'
}
}
}
/**
* 用户日留存数据填充
* @param {String} type 统计类型 hour:实时统计 day:按天统计,week:按周统计 month:按月统计
* @param {Date|Time} date 指定日期或时间戳
* @param {Boolean} reset 是否重置,为ture时会重置该批次数据
*/
async userRetentionFillDayly(type, day, date) {
if (type !== 'day') {
return {
code: 301,
msg: 'Type error:' + type
}
}
const dateTime = new DateTime()
const {
startTime,
endTime
} = dateTime.getTimeDimensionByType(type, 0 - day, date)
if (!startTime || !endTime) {
return {
code: 1001,
msg: 'The statistic time get failed'
}
}
// 截止时间范围
const lastTimeInfo = dateTime.getTimeDimensionByType(type, 0, date)
// 获取当时批次的统计日志
const resultLogRes = await this.selectAll(this.tableName, {
dimension: type,
start_time: startTime,
end_time: endTime
})
if (this.debug) {
console.log('resultLogRes', JSON.stringify(resultLogRes))
}
if (!resultLogRes || resultLogRes.data.length === 0) {
if (this.debug) {
console.log('Not found this log --' + type + ':' + day + ', start:' + startTime + ',endTime:' +
endTime)
}
return {
code: 1000,
msg: 'Not found this log'
}
}
const userSessionLog = new UserSessionLog()
const uniIDUsers = new UniIDUsers()
const platform = new Platform()
const channel = new Channel()
const version = new Version()
let res = null
for (const resultIndex in resultLogRes.data) {
const resultLog = resultLogRes.data[resultIndex]
// 平台信息
let platformInfo = null
if (this.platforms && this.platforms[resultLog.platform_id]) {
platformInfo = this.platforms[resultLog.platform_id]
} else {
platformInfo = await this.getById(platform.tableName, resultLog.platform_id)
if (!platformInfo || platformInfo.length === 0) {
platformInfo.code = ''
}
this.platforms[resultLog.platform_id] = platformInfo
if (this.debug) {
console.log('platformInfo', JSON.stringify(platformInfo))
}
}
// 渠道信息
let channelInfo = null
if (this.channels && this.channels[resultLog.channel_id]) {
channelInfo = this.channels[resultLog.channel_id]
} else {
channelInfo = await this.getById(channel.tableName, resultLog.channel_id)
if (!channelInfo || channelInfo.length === 0) {
channelInfo.channel_code = ''
}
this.channels[resultLog.channel_id] = channelInfo
if (this.debug) {
console.log('channelInfo', JSON.stringify(channelInfo))
}
}
// 版本信息
let versionInfo = null
if (this.versions && this.versions[resultLog.version_id]) {
versionInfo = this.versions[resultLog.version_id]
} else {
versionInfo = await this.getById(version.tableName, resultLog.version_id, false)
if (!versionInfo || versionInfo.length === 0) {
versionInfo.version = ''
}
this.versions[resultLog.version_id] = versionInfo
if (this.debug) {
console.log('versionInfo', JSON.stringify(versionInfo))
}
}
// 获取该时间段内的活跃用户
const activeUserRes = await this.aggregate(userSessionLog.tableName, {
project: {
appid: 1,
version: 1,
platform: 1,
channel: 1,
uid: 1,
create_time: 1
},
match: {
appid: resultLog.appid,
version: versionInfo.version,
platform: platformInfo.code,
channel: channelInfo.channel_code,
create_time: {
$gte: startTime,
$lte: endTime
}
},
group: {
_id: {
uid: '$uid'
},
create_time: {
$min: '$create_time'
},
sessionCount: {
$sum: 1
}
},
sort: {
create_time: 1,
sessionCount: 1
},
getAll: true
})
if (this.debug) {
console.log('activeUserRes', JSON.stringify(activeUserRes))
}
//活跃用户留存率
let activeUserRate = 0
//活跃用户留存数
let activeUsers = 0
if (activeUserRes && activeUserRes.data.length > 0) {
const thisDayActiveUsers = activeUserRes.data.length
//获取该时间段内的活跃用户编号,这里没用lookup联查是因为数据量较大时lookup效率很低
const thisDayActiveUids = []
for (let tau in activeUserRes.data) {
thisDayActiveUids.push(activeUserRes.data[tau]._id.uid)
}
if (this.debug) {
console.log('thisDayActiveUids', JSON.stringify(thisDayActiveUids))
}
// 留存活跃用户数
const retentionActiveUserRes = await this.aggregate(userSessionLog.tableName, {
project: {
appid: 1,
version: 1,
platform: 1,
channel: 1,
uid: 1,
create_time: 1
},
match: {
appid: resultLog.appid,
version: versionInfo.version,
platform: platformInfo.code,
channel: channelInfo.channel_code,
uid: {
$in: thisDayActiveUids
},
create_time: {
$gte: lastTimeInfo.startTime,
$lte: lastTimeInfo.endTime
}
},
group: [{
_id: {
uid: '$uid'
}
}, {
_id: {},
total_users: {
$sum: 1
}
}]
})
if (this.debug) {
console.log('retentionActiveUserRes', JSON.stringify(retentionActiveUserRes))
}
if (retentionActiveUserRes && retentionActiveUserRes.data.length > 0) {
// 活跃用户留存数
activeUsers = retentionActiveUserRes.data[0].total_users
// 活跃用户留存率
activeUserRate = parseFloat((activeUsers * 100 / thisDayActiveUsers).toFixed(2))
}
}
//新增用户编号
const thisDayNewUids = await uniIDUsers.getUserIds(resultLog.appid, platformInfo.code, channelInfo.channel_code, versionInfo.version, {
$gte: startTime,
$lte: endTime
})
//新增用户留存率
let newUserRate = 0
//新增用户留存数
let newUsers = 0
if (thisDayNewUids.length > 0) {
// 现在依然活跃的用户数
const retentionNewUserRes = await this.aggregate(userSessionLog.tableName, {
project: {
appid: 1,
version: 1,
platform: 1,
channel: 1,
uid: 1,
create_time: 1
},
match: {
appid: resultLog.appid,
version: versionInfo.version,
platform: platformInfo.code,
channel: channelInfo.channel_code,
uid: {
$in: thisDayNewUids
},
create_time: {
$gte: lastTimeInfo.startTime,
$lte: lastTimeInfo.endTime
}
},
group: [{
_id: {
uid: '$uid'
}
}, {
_id: {},
total_users: {
$sum: 1
}
}]
})
if (retentionNewUserRes && retentionNewUserRes.data.length > 0) {
// 新增用户留存数
newUsers = retentionNewUserRes.data[0].total_users
// 新增用户留存率
newUserRate = parseFloat((newUsers * 100 / thisDayNewUids.length).toFixed(2))
}
}
// 数据更新
const retentionData = resultLog.retention
const dataKey = type.substr(0, 1) + '_' + day
if (!retentionData.active_user) {
retentionData.active_user = {}
}
retentionData.active_user[dataKey] = {
user_count: activeUsers,
user_rate: activeUserRate
}
if (!retentionData.new_user) {
retentionData.new_user = {}
}
retentionData.new_user[dataKey] = {
user_count: newUsers,
user_rate: newUserRate
}
if (this.debug) {
console.log('retentionData', JSON.stringify(retentionData))
}
res = await this.update(this.tableName, {
retention: retentionData
}, {
_id: resultLog._id
})
}
if (res && res.updated) {
return {
code: 0,
msg: 'success'
}
} else {
return {
code: 500,
msg: 'retention data update failed'
}
}
}
/**
* 用户周/月留存数据填充
* @param {String} type 统计类型 hour:实时统计 day:按天统计,week:按周统计 month:按月统计
* @param {Date|Time} date 指定日期或时间戳
* @param {Boolean} reset 是否重置,为ture时会重置该批次数据
*/
async userRetentionFillWeekOrMonth(type, day, date) {
if (!['week', 'month'].includes(type)) {
return {
code: 301,
msg: 'Type error:' + type
}
}
const dateTime = new DateTime()
const {
startTime,
endTime
} = dateTime.getTimeDimensionByType(type, 0 - day, date)
if (!startTime || !endTime) {
return {
code: 1001,
msg: 'The statistic time get failed'
}
}
// 截止时间范围
const lastTimeInfo = dateTime.getTimeDimensionByType(type, 0, date)
// 获取当时批次的统计日志
const resultLogRes = await this.selectAll(this.tableName, {
dimension: type,
start_time: startTime,
end_time: endTime
})
if (this.debug) {
console.log('resultLogRes', JSON.stringify(resultLogRes))
}
if (!resultLogRes || resultLogRes.data.length === 0) {
if (this.debug) {
console.log('Not found this session log --' + type + ':' + day + ', start:' + startTime +
',endTime:' + endTime)
}
return {
code: 1000,
msg: 'Not found this session log'
}
}
const activeUserObj = new ActiveUsers()
const uniIDUsers = new UniIDUsers()
const platform = new Platform()
const channel = new Channel()
const version = new Version()
let res = null
//活跃用户留存率
let activeUserRate
//活跃用户留存数
let activeUsers
//新增用户留存率
let newUserRate
//新增用户留存数
let newUsers
for (const resultIndex in resultLogRes.data) {
const resultLog = resultLogRes.data[resultIndex]
// 平台信息
let platformInfo = null
if (this.platforms && this.platforms[resultLog.platform_id]) {
platformInfo = this.platforms[resultLog.platform_id]
} else {
platformInfo = await this.getById(platform.tableName, resultLog.platform_id)
if (!platformInfo || platformInfo.length === 0) {
platformInfo.code = ''
}
this.platforms[resultLog.platform_id] = platformInfo
if (this.debug) {
console.log('platformInfo', JSON.stringify(platformInfo))
}
}
// 渠道信息
let channelInfo = null
if (this.channels && this.channels[resultLog.channel_id]) {
channelInfo = this.channels[resultLog.channel_id]
} else {
channelInfo = await this.getById(channel.tableName, resultLog.channel_id)
if (!channelInfo || channelInfo.length === 0) {
channelInfo.channel_code = ''
}
this.channels[resultLog.channel_id] = channelInfo
if (this.debug) {
console.log('channelInfo', JSON.stringify(channelInfo))
}
}
// 版本信息
let versionInfo = null
if (this.versions && this.versions[resultLog.version_id]) {
versionInfo = this.versions[resultLog.version_id]
} else {
versionInfo = await this.getById(version.tableName, resultLog.version_id, false)
if (!versionInfo || versionInfo.length === 0) {
versionInfo.version = ''
}
this.versions[resultLog.version_id] = versionInfo
if (this.debug) {
console.log('versionInfo', JSON.stringify(versionInfo))
}
}
// 获取该批次的活跃用户数
const activeUserRes = await this.selectAll(activeUserObj.tableName, {
appid: resultLog.appid,
platform_id: resultLog.platform_id,
channel_id: resultLog.channel_id,
version_id: resultLog.version_id,
dimension: type,
create_time: {
$gte: startTime,
$lte: endTime
}
}, {
uid: 1
})
if (this.debug) {
console.log('activeUserRes', JSON.stringify(activeUserRes))
}
activeUserRate = 0
activeUsers = 0
if (activeUserRes && activeUserRes.data.length > 0) {
const thisDayactiveUsers = activeUserRes.data.length
const thisDayActiveDeviceIds = []
for (const tau in activeUserRes.data) {
thisDayActiveDeviceIds.push(activeUserRes.data[tau].uid)
}
if (this.debug) {
console.log('thisDayActiveDeviceIds', JSON.stringify(thisDayActiveDeviceIds))
}
// 留存活跃用户数
const retentionactiveUserRes = await this.getCollection(activeUserObj.tableName).where({
appid: resultLog.appid,
platform_id: resultLog.platform_id,
channel_id: resultLog.channel_id,
version_id: resultLog.version_id,
uid: {
$in: thisDayActiveDeviceIds
},
dimension: type,
create_time: {
$gte: lastTimeInfo.startTime,
$lte: lastTimeInfo.endTime
}
}).count()
if (this.debug) {
console.log('retentionactiveUserRes', JSON.stringify(retentionactiveUserRes))
}
if (retentionactiveUserRes && retentionactiveUserRes.total > 0) {
// 活跃用户留存数
activeUsers = retentionactiveUserRes.total
// 活跃用户留存率
activeUserRate = parseFloat((activeUsers * 100 / thisDayactiveUsers).toFixed(2))
}
}
//新增用户编号
const thisDayNewUids = await uniIDUsers.getUserIds(resultLog.appid, platformInfo.code, channelInfo.channel_code, versionInfo.version, {
$gte: startTime,
$lte: endTime
})
// 新增用户留存率
newUserRate = 0
// 新增用户留存数
newUsers = 0
if(thisDayNewUids && thisDayNewUids.length > 0) {
// 新增用户留存数
const retentionnewUserRes = await this.getCollection(activeUserObj.tableName).where({
appid: resultLog.appid,
platform_id: resultLog.platform_id,
channel_id: resultLog.channel_id,
version_id: resultLog.version_id,
uid: {
$in: thisDayNewUids
},
dimension: type,
create_time: {
$gte: lastTimeInfo.startTime,
$lte: lastTimeInfo.endTime
}
}).count()
if (retentionnewUserRes && retentionnewUserRes.total > 0) {
// 新增用户留存数
newUsers = retentionnewUserRes.total
// 新增用户留存率
newUserRate = parseFloat((newUsers * 100 / thisDayNewUids.length).toFixed(2))
}
}
// 数据更新
const retentionData = resultLog.retention
const dataKey = type.substr(0, 1) + '_' + day
if (!retentionData.active_user) {
retentionData.active_user = {}
}
retentionData.active_user[dataKey] = {
user_count: activeUsers,
user_rate: activeUserRate
}
if (!retentionData.new_user) {
retentionData.new_user = {}
}
retentionData.new_user[dataKey] = {
user_count: newUsers,
user_rate: newUserRate
}
if (this.debug) {
console.log('retentionData', JSON.stringify(retentionData))
}
res = await this.update(this.tableName, {
retention: retentionData
}, {
_id: resultLog._id
})
}
if (res && res.updated) {
return {
code: 0,
msg: 'success'
}
} else {
return {
code: 500,
msg: 'retention data update failed'
}
}
}
/**
* 清理实时统计的日志
* @param {Number} days 实时统计日志保留天数
*/
async cleanHourLog(days = 7) {
console.log('clean hour logs - day:', days)
const dateTime = new DateTime()
const res = await this.delete(this.tableName, {
dimension: 'hour',
start_time: {
$lt: dateTime.getTimeBySetDays(0 - days)
}
})
if (!res.code) {
console.log('clean hour logs - res:', res)
}
}
}
/**
* 表名
*/
const dbName = {
uniIdUsers: 'uni-id-users', // 支付订单明细表
uniPayOrders: 'uni-pay-orders', // 支付订单明细表
uniStatPayResult: 'uni-stat-pay-result', // 统计结果存储表
uniStatSessionLogs: 'uni-stat-session-logs', // 设备会话日志表(主要用于统计访问设备数)
uniStatUserSessionLogs: 'uni-stat-user-session-logs', // 用户会话日志表(主要用于统计访问人数)
};
module.exports = dbName;
/**
* 数据库操作
*/
module.exports = {
uniIdUsers: require('./uniIdUsers'),
uniPayOrders: require('./uniPayOrders'),
uniStatPayResult: require('./uniStatPayResult'),
uniStatSessionLogs: require('./uniStatSessionLogs'),
uniStatUserSessionLogs: require('./uniStatUserSessionLogs'),
}
/**
* 数据库操作
*/
const dbName = require("./config");
let db = uniCloud.database();
let _ = db.command;
let $ = _.aggregate;
class Dao {
async count(whereJson) {
let dbRes = await db.collection(dbName.uniIdUsers).where(whereJson).count()
return dbRes && dbRes.total ? dbRes.total : 0;
}
async countNewUserOrder(obj) {
let {
whereJson,
status
} = obj;
let dbRes = await db.collection(dbName.uniIdUsers).aggregate()
.match(whereJson)
.lookup({
from: dbName.uniPayOrders,
let: {
user_id: '$_id',
},
pipeline: $.pipeline()
.match(_.expr($.and([
$.eq(['$user_id', '$$user_id']),
$.in(['$status', status])
])))
.limit(1)
.done(),
as: 'order',
})
.unwind({
path: '$order',
})
.group({
_id: null,
count: {
$addToSet: '$_id'
},
})
.addFields({
count: {
$size: '$count'
}
})
.end()
try {
return dbRes.data[0].count;
} catch (err) {
return 0;
}
}
}
module.exports = new Dao();
/**
* 数据库操作
*/
const BaseMod = require('../../base');
const dbName = require("./config");
class Dao extends BaseMod {
constructor() {
super()
this.tablePrefix = false; // 不使用表前缀
}
async group(data) {
let {
start_time,
end_time,
status: status_str
} = data;
let status;
if (status_str === "已下单") {
} else if (status_str === "已付款") {
status = {
$gt: 0
}
} else if (status_str === "已退款") {
status = {
$in: [2, 3]
}
}
const dbRes = await this.aggregate(dbName.uniPayOrders, {
match: {
create_date: {
$gte: start_time,
$lte: end_time
},
status
},
group: {
_id: {
appid: '$appid',
version: '$stat_data.app_version',
platform: '$stat_data.platform',
channel: '$stat_data.channel',
},
status: {
$first: '$status'
},
// 支付金额
total_fee: {
$sum: '$total_fee'
},
// 退款金额
refund_fee: {
$sum: '$refund_fee'
},
// 支付笔数
order_count: {
$sum: 1
},
// 支付人数(去重复)
user_count: {
$addToSet: '$user_id'
},
// 支付设备数(去重复)
device_count: {
$addToSet: '$device_id'
},
create_date: {
$min: '$create_date'
}
},
addFields: {
user_count: {
$size: '$user_count'
},
device_count: {
$size: '$device_count'
}
},
// 按创建时间排序
sort: {
create_date: 1
},
getAll: true
});
let list = dbRes.data;
list.map((item) => {
item.status_str = status_str;
});
return list;
}
}
module.exports = new Dao();
/**
* 数据库操作
*/
const BaseMod = require('../../base');
const dbName = require("./config");
class Dao extends BaseMod {
constructor() {
super()
this.tablePrefix = false; // 不使用表前缀
}
async list(data) {
let {
whereJson,
} = data;
const dbRes = await this.getCollection(dbName.uniStatPayResult).where(whereJson).get();
return dbRes.data;
}
async del(data) {
let {
whereJson
} = data;
const dbRes = await this.delete(dbName.uniStatPayResult, whereJson);
return dbRes.deleted;
}
async adds(saveList) {
return await this.batchInsert(dbName.uniStatPayResult, saveList);
}
}
module.exports = new Dao();
/**
* 数据库操作
*/
const BaseMod = require('../../base');
const dbName = require("./config");
class Dao extends BaseMod {
constructor() {
super()
this.tablePrefix = false; // 不使用表前缀
}
async group(data) {
let {
whereJson,
} = data;
const dbRes = await this.aggregate(dbName.uniStatSessionLogs, {
match: whereJson,
group: {
_id: {
appid: '$appid',
version: '$version',
platform: '$platform',
channel: '$channel',
},
// 设备数(去重复)
device_count: {
$addToSet: '$device_id'
},
create_time: {
$min: '$create_time'
}
},
addFields: {
device_count: {
$size: '$device_count'
}
},
// 按创建时间排序
sort: {
create_time: 1
},
getAll: true
});
return dbRes.data;
}
async groupCount(whereJson) {
const dbRes = await this.aggregate(dbName.uniStatSessionLogs, {
match: whereJson,
group: {
_id: null,
// 设备数(去重复)
count: {
$addToSet: '$device_id'
},
},
addFields: {
count: {
$size: '$count'
}
},
getAll: true
});
try {
return dbRes.data[0].count;
} catch (err) {
return 0;
}
}
}
module.exports = new Dao();
/**
* 数据库操作
*/
const BaseMod = require('../../base');
const dbName = require("./config");
class Dao extends BaseMod {
constructor() {
super()
this.tablePrefix = false; // 不使用表前缀
}
async group(data) {
let {
whereJson
} = data;
const dbRes = await this.aggregate(dbName.uniStatUserSessionLogs, {
match: whereJson,
group: {
_id: {
appid: '$appid',
version: '$version',
platform: '$platform',
channel: '$channel',
},
// 用户数(去重复)
user_count: {
$addToSet: '$uid'
},
create_time: {
$min: '$create_time'
}
},
addFields: {
user_count: {
$size: '$user_count'
}
},
// 按创建时间排序
sort: {
create_time: 1
},
getAll: true
});
let list = dbRes.data;
return list;
}
async groupCount(whereJson) {
const dbRes = await this.aggregate(dbName.uniStatUserSessionLogs, {
match: whereJson,
group: {
_id: null,
// 设备数(去重复)
count: {
$addToSet: '$uid'
},
},
addFields: {
count: {
$size: '$count'
}
},
getAll: true
});
try {
return dbRes.data[0].count;
} catch (err) {
return 0;
}
}
}
module.exports = new Dao();
/**
* 基础对外模型
*/
module.exports = {
PayResult: require('./payResult'),
}
/**
* @class ActiveDevices 活跃设备模型 - 每日跑批合并,仅添加本周/本月首次访问的设备。
*/
const BaseMod = require('../base')
const Platform = require('../platform')
const Channel = require('../channel')
const Version = require('../version')
const {
DateTime,
UniCrypto
} = require('../../lib')
const dao = require('./dao')
let db = uniCloud.database();
let _ = db.command;
let $ = _.aggregate;
module.exports = class PayResult extends BaseMod {
constructor() {
super()
this.platforms = []
this.channels = []
this.versions = []
}
/**
支付金额:统计时间内,成功支付的订单金额之和(不剔除退款订单)。
支付笔数:统计时间内,成功支付的订单数,一个订单对应唯一一个订单号。(不剔除退款订单。)
支付人数:统计时间内,成功支付的人数(不剔除退款订单)。
支付设备数:统计时间内,成功支付的设备数(不剔除退款订单)。
下单金额:统计时间内,成功下单的订单金额(不剔除退款订单)。
下单笔数:统计时间内,成功下单的订单笔数(不剔除退款订单)。
下单人数:统计时间内,成功下单的客户数,一人多次下单记为一人(不剔除退款订单)。
下单设备数:统计时间内,成功下单的设备数,一台设备多次访问被计为一台(不剔除退款订单)。
访问人数:统计时间内,访问人数,一人多次访问被计为一人(只统计已登录的用户)。
访问设备数:统计时间内,访问设备数,一台设备多次访问被计为一台(包含未登录的用户)。
* @desc 支付统计(按日统计)
* @param {string} type 统计范围 hour:按小时统计,day:按天统计,week:按周统计,month:按月统计 quarter:按季度统计 year:按年统计
* @param {date|time} date
* @param {bool} reset
*/
async stat(type, date, reset, config = {}) {
if (!date) date = Date.now();
// 以下是测试代码-----------------------------------------------------------
//reset = true;
//date = 1667318400000;
// 以上是测试代码-----------------------------------------------------------
let res = await this.run(type, date, reset, config); // 每小时
if (type === "hour" && config.timely) {
/**
* 如果是小时纬度统计,则还需要再统计(今日实时数据)
* 2022-11-01 01:00:00 统计的是 2022-11-01 的天维度数据(即该天0点-1点数据)
* 2022-11-01 02:00:00 统计的是 2022-11-01 的天维度数据(即该天0点-2点数据)
* 2022-11-01 23:00:00 统计的是 2022-11-01 的天维度数据(即该天0点-23点数据)
* 2022-11-02 00:00:00 统计的是 2022-11-01 的天维度数据(即该天最终数据)
* 2022-11-02 01:00:00 统计的是 2022-11-02 的天维度数据(即该天0点-1点数据)
*/
date -= 1000 * 3600; // 需要减去1小时
let tasks = [];
tasks.push(this.run("day", date, true, 0)); // 今日
// 以下数据每6小时刷新一次
const dateTime = new DateTime();
const timeInfo = dateTime.getTimeInfo(date);
if ((timeInfo.nHour + 1) % 6 === 0) {
tasks.push(this.run("week", date, true, 0)); // 本周
tasks.push(this.run("month", date, true, 0)); // 本月
tasks.push(this.run("quarter", date, true, 0)); // 本季度
tasks.push(this.run("year", date, true, 0)); // 本年度
}
await Promise.all(tasks);
}
return res;
}
/**
* @desc 支付统计
* @param {string} type 统计范围 hour:按小时统计,day:按天统计,week:按周统计,month:按月统计 quarter:按季度统计 year:按年统计
* @param {date|time} date 哪个时间节点计算(默认已当前时间计算)
* @param {bool} reset 如果统计数据已存在,是否需要重新统计
*/
async run(type, date, reset, offset = -1) {
let dimension = type;
const dateTime = new DateTime();
// 获取统计的起始时间和截止时间
const dateDimension = dateTime.getTimeDimensionByType(dimension, offset, date);
let start_time = dateDimension.startTime;
let end_time = dateDimension.endTime;
let runStartTime = Date.now();
let debug = true;
if (debug) {
console.log(`-----------------支付统计开始(${dimension})-----------------`);
console.log('本次统计时间:', dateTime.getDate('Y-m-d H:i:s', start_time), "-", dateTime.getDate('Y-m-d H:i:s', end_time))
console.log('本次统计参数:', 'type:' + type, 'date:' + date, 'reset:' + reset)
}
this.startTime = start_time;
let pubWhere = {
start_time,
end_time
};
// 查看当前时间段数据是否已存在,防止重复生成
if (!reset) {
let list = await dao.uniStatPayResult.list({
whereJson: {
...pubWhere,
dimension
}
});
if (list.length > 0) {
console.log('data have exists')
if (debug) {
let runEndTime = Date.now();
console.log(`耗时:${((runEndTime - runStartTime ) / 1000).toFixed(3)} 秒`)
console.log(`-----------------支付统计结束(${dimension})-----------------`);
}
return {
code: 1003,
msg: 'Pay data in this time have already existed'
}
}
} else {
let delRes = await dao.uniStatPayResult.del({
whereJson: {
...pubWhere,
dimension
}
});
console.log('Delete old data result:', JSON.stringify(delRes))
}
// 支付订单分组(已下单)
let statPayOrdersList1 = await dao.uniPayOrders.group({
...pubWhere,
status: "已下单"
});
// 支付订单分组(且已付款,含退款)
let statPayOrdersList2 = await dao.uniPayOrders.group({
...pubWhere,
status: "已付款"
});
// 支付订单分组(已退款)
let statPayOrdersList3 = await dao.uniPayOrders.group({
...pubWhere,
status: "已退款"
});
let statPayOrdersList = statPayOrdersList1.concat(statPayOrdersList2).concat(statPayOrdersList3)
let res = {
code: 0,
msg: 'success'
}
// 将支付订单分组查询结果组装
let statDta = {};
if (statPayOrdersList.length > 0) {
for (let i = 0; i < statPayOrdersList.length; i++) {
let item = statPayOrdersList[i];
let {
appid,
version,
platform,
channel,
} = item._id;
let {
status_str
} = item;
let key = `${appid}-${version}-${platform}-${channel}`;
if (!statDta[key]) {
statDta[key] = {
appid,
version,
platform,
channel,
status: {}
};
}
let newItem = JSON.parse(JSON.stringify(item));
delete newItem._id;
statDta[key].status[status_str] = newItem;
}
}
if (this.debug) console.log('statDta: ', statDta)
let saveList = [];
for (let key in statDta) {
let item = statDta[key];
let {
appid,
version,
platform,
channel,
status: statusData,
} = item;
if (!channel) channel = item.scene;
let fieldData = {
pay_total_amount: 0,
pay_order_count: 0,
pay_user_count: 0,
pay_device_count: 0,
create_total_amount: 0,
create_order_count: 0,
create_user_count: 0,
create_device_count: 0,
refund_total_amount: 0,
refund_order_count: 0,
refund_user_count: 0,
refund_device_count: 0,
};
for (let status in statusData) {
let statusItem = statusData[status];
if (status === "已下单") {
// 已下单
fieldData.create_total_amount += statusItem.total_fee;
fieldData.create_order_count += statusItem.order_count;
fieldData.create_user_count += statusItem.user_count;
fieldData.create_device_count += statusItem.device_count;
} else if (status === "已付款") {
// 已付款
fieldData.pay_total_amount += statusItem.total_fee;
fieldData.pay_order_count += statusItem.order_count;
fieldData.pay_user_count += statusItem.user_count;
fieldData.pay_device_count += statusItem.device_count;
} else if (status === "已退款") {
// 已退款
fieldData.refund_total_amount += statusItem.total_fee;
fieldData.refund_order_count += statusItem.order_count;
fieldData.refund_user_count += statusItem.user_count;
fieldData.refund_device_count += statusItem.device_count;
}
}
// 平台信息
let platformInfo = null;
if (this.platforms && this.platforms[platform]) {
// 从缓存中读取数据
platformInfo = this.platforms[platform]
} else {
const platformObj = new Platform()
platformInfo = await platformObj.getPlatformAndCreate(platform, null)
if (!platformInfo || platformInfo.length === 0) {
platformInfo._id = ''
}
this.platforms[platform] = platformInfo;
}
// 渠道信息
let channelInfo = null
const channelKey = appid + '_' + platformInfo._id + '_' + channel;
if (this.channels && this.channels[channelKey]) {
channelInfo = this.channels[channelKey];
} else {
const channelObj = new Channel()
channelInfo = await channelObj.getChannelAndCreate(appid, platformInfo._id, channel)
if (!channelInfo || channelInfo.length === 0) {
channelInfo._id = ''
}
this.channels[channelKey] = channelInfo
}
// 版本信息
let versionInfo = null
const versionKey = appid + '_' + platform + '_' + version
if (this.versions && this.versions[versionKey]) {
versionInfo = this.versions[versionKey]
} else {
const versionObj = new Version()
versionInfo = await versionObj.getVersionAndCreate(appid, platform, version)
if (!versionInfo || versionInfo.length === 0) {
versionInfo._id = ''
}
this.versions[versionKey] = versionInfo
}
let countWhereJson = {
create_time: _.gte(start_time).lte(end_time),
appid,
version,
platform: _.in(getUniPlatform(platform)),
channel,
};
// 活跃设备数量
let activity_device_count = await dao.uniStatSessionLogs.groupCount(countWhereJson);
// 活跃用户数量
let activity_user_count = await dao.uniStatUserSessionLogs.groupCount(countWhereJson);
/*
// TODO 此处有问题,暂不使用
// 新设备数量
let new_device_count = await dao.uniStatSessionLogs.groupCount({
...countWhereJson,
is_first_visit: 1,
});
// 新注册用户数量
let new_user_count = await dao.uniIdUsers.count({
register_date: _.gte(start_time).lte(end_time),
register_env: {
appid,
app_version: version,
uni_platform: _.in(getUniPlatform(platform)),
channel,
}
});
// 新注册用户中下单的人数
let new_user_create_order_count = await dao.uniIdUsers.countNewUserOrder({
whereJson: {
register_date: _.gte(start_time).lte(end_time),
register_env: {
appid,
app_version: version,
uni_platform: _.in(getUniPlatform(platform)),
channel,
}
},
status: [-1, 0]
});
// 新注册用户中支付成功的人数
let new_user_pay_order_count = await dao.uniIdUsers.countNewUserOrder({
whereJson: {
register_date: _.gte(start_time).lte(end_time),
register_env: {
appid,
app_version: version,
uni_platform: _.in(getUniPlatform(platform)),
channel,
}
},
status: [1, 2, 3]
}); */
saveList.push({
appid,
platform_id: platformInfo._id,
channel_id: channelInfo._id,
version_id: versionInfo._id,
dimension,
create_date: Date.now(), // 记录下当前时间
start_time,
end_time,
stat_date: getNowDate(start_time, 8, dimension),
...fieldData,
activity_user_count,
activity_device_count,
// new_user_count,
// new_device_count,
// new_user_create_order_count,
// new_user_pay_order_count,
});
}
if (this.debug) console.log('saveList: ', saveList)
//return;
if (saveList.length > 0) {
res = await dao.uniStatPayResult.adds(saveList);
}
if (debug) {
let runEndTime = Date.now();
console.log(`耗时:${((runEndTime - runStartTime ) / 1000).toFixed(3)} 秒`)
console.log(`本次共添加:${saveList.length } 条记录`)
console.log(`-----------------支付统计结束(${dimension})-----------------`);
}
return res
}
}
function getUniPlatform(platform) {
let list = [];
if (["h5", "web"].indexOf(platform) > -1) {
list = ["h5", "web"];
} else if (["app-plus", "app"].indexOf(platform) > -1) {
list = ["app-plus", "app"];
} else {
list = [platform];
}
return list;
}
function getNowDate(date = new Date(), targetTimezone = 8, dimension) {
if (typeof date === "string" && !isNaN(date)) date = Number(date);
if (typeof date == "number") {
if (date.toString().length == 10) date *= 1000;
date = new Date(date);
}
const {
year,
month,
day,
hour,
minute,
second
} = getFullTime(date);
// 现在的时间
let date_str;
if (dimension === "month") {
date_str = timeFormat(date, "yyyy-MM", targetTimezone);
} else if (dimension === "quarter") {
date_str = timeFormat(date, "yyyy-MM", targetTimezone);
} else if (dimension === "year") {
date_str = timeFormat(date, "yyyy", targetTimezone);
} else {
date_str = timeFormat(date, "yyyy-MM-dd", targetTimezone);
}
return {
date_str,
year,
month,
day,
hour,
//minute,
//second,
};
}
function getFullTime(date = new Date(), targetTimezone = 8) {
if (!date) {
return "";
}
if (typeof date === "string" && !isNaN(date)) date = Number(date);
if (typeof date == "number") {
if (date.toString().length == 10) date *= 1000;
date = new Date(date);
}
const dif = date.getTimezoneOffset();
const timeDif = dif * 60 * 1000 + (targetTimezone * 60 * 60 * 1000);
const east8time = date.getTime() + timeDif;
date = new Date(east8time);
let YYYY = date.getFullYear() + '';
let MM = (date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1);
let DD = (date.getDate() < 10 ? '0' + (date.getDate()) : date.getDate());
let hh = (date.getHours() < 10 ? '0' + (date.getHours()) : date.getHours());
let mm = (date.getMinutes() < 10 ? '0' + (date.getMinutes()) : date.getMinutes());
let ss = (date.getSeconds() < 10 ? '0' + (date.getSeconds()) : date.getSeconds());
return {
YYYY: Number(YYYY),
MM: Number(MM),
DD: Number(DD),
hh: Number(hh),
mm: Number(mm),
ss: Number(ss),
year: Number(YYYY),
month: Number(MM),
day: Number(DD),
hour: Number(hh),
minute: Number(mm),
second: Number(ss),
};
};
/**
* 日期格式化
*/
function timeFormat(time, fmt = 'yyyy-MM-dd hh:mm:ss', targetTimezone = 8) {
try {
if (!time) {
return "";
}
if (typeof time === "string" && !isNaN(time)) time = Number(time);
// 其他更多是格式化有如下:
// yyyy-MM-dd hh:mm:ss|yyyy年MM月dd日 hh时MM分等,可自定义组合
let date;
if (typeof time === "number") {
if (time.toString().length == 10) time *= 1000;
date = new Date(time);
} else {
date = time;
}
const dif = date.getTimezoneOffset();
const timeDif = dif * 60 * 1000 + (targetTimezone * 60 * 60 * 1000);
const east8time = date.getTime() + timeDif;
date = new Date(east8time);
let opt = {
"M+": date.getMonth() + 1, //月份
"d+": date.getDate(), //日
"h+": date.getHours(), //小时
"m+": date.getMinutes(), //分
"s+": date.getSeconds(), //秒
"q+": Math.floor((date.getMonth() + 3) / 3), //季度
"S": date.getMilliseconds() //毫秒
};
if (/(y+)/.test(fmt)) {
fmt = fmt.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length));
}
for (let k in opt) {
if (new RegExp("(" + k + ")").test(fmt)) {
fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (opt[k]) : (("00" + opt[k]).substr(("" + opt[k]).length)));
}
}
return fmt;
} catch (err) {
// 若格式错误,则原值显示
return time;
}
};
/**
* @class UniIDUsers uni-id 用户模型
*/
const BaseMod = require('./base')
module.exports = class UniIDUsers extends BaseMod {
constructor() {
super()
this.tableName = 'uni-id-users'
this.tablePrefix = false
}
/**
* 获取用户数
* @param {String} appid DCloud-appid
* @param {String} platform 平台
* @param {String} channel 渠道
* @param {String} version 版本
* @param {Object} registerTime 注册时间范围 例:{$gte:开始日期时间戳, $lte:结束日期时间戳}
* @return {Number}
*/
async getUserCount(appid, platform, channel, version, registerTime) {
if(!appid || !platform) {
return false
}
const condition = this.getCondition(appid, platform, channel, version, registerTime)
let userCount = 0
const userCountRes = await this.getCollection(this.tableName).where(condition).count()
if(userCountRes && userCountRes.total > 0) {
userCount = userCountRes.total
}
return userCount
}
/**
* 获取用户编号列表
* @param {String} appid DCloud-appid
* @param {String} platform 平台
* @param {String} channel 渠道
* @param {String} version 版本
* @param {Object} registerTime 注册时间范围 例:{$gte:开始日期时间戳, $lte:结束日期时间戳}
* @return {Array}
*/
async getUserIds(appid, platform, channel, version, registerTime) {
if(!appid || !platform) {
return false
}
const condition = this.getCondition(appid, platform, channel, version, registerTime)
let uids = []
const uidsRes = await this.selectAll(this.tableName, condition, {
_id: 1
})
for (const u in uidsRes.data) {
uids.push(uidsRes.data[u]._id)
}
return uids
}
/**
* 获取查询条件
* @param {String} appid DCloud-appid
* @param {String} platform 平台
* @param {String} channel 渠道
* @param {String} version 版本
* @param {Object} registerTime 注册时间范围 例:{$gte:开始日期时间戳, $lte:结束日期时间戳}
*/
getCondition(appid, platform, channel, version, registerTime) {
let condition = {
'register_env.appid': appid,//DCloud appid
'register_env.uni_platform': platform,//平台
'register_env.channel': channel ? channel : '1001', //渠道或场景值
'register_env.app_version' : version //应用版本区分
}
//原生应用平台
if(['android', 'ios'].includes(platform)) {
condition['register_env.uni_platform'] = 'app'//systemInfo中uniPlatform字段android和ios都用app表示,所以此处查询需要用osName区分一下
condition['register_env.os_name'] = platform //系统
}
//兼容vue2
if(channel === '1001') {
condition['register_env.channel'] = {$in:['', '1001']}
}
//注册时间
if(registerTime) {
condition.register_date = registerTime
}
return condition
}
}
/**
* @class UserSessionLog 用户会话日志模型
*/
const BaseMod = require('./base')
const Platform = require('./platform')
const Channel = require('./channel')
const {
DateTime
} = require('../lib')
module.exports = class UserSessionLog extends BaseMod {
constructor() {
super()
this.tableName = 'user-session-logs'
}
/**
* 用户会话日志数据填充
* @param {Object} params 上报参数
*/
async fill(params) {
if (!params.sid) {
return {
code: 200,
msg: 'Not found session log'
}
}
if (!params.uid) {
return {
code: 200,
msg: 'Parameter "uid" not found'
}
}
const dateTime = new DateTime()
const platform = new Platform()
const channel = new Channel()
//获取当前页面信息
if (!params.page_id) {
const pageInfo = await page.getPageAndCreate(params.ak, params.url, params.ttpj)
if (!pageInfo || pageInfo.length === 0) {
return {
code: 300,
msg: 'Not found this entry page'
}
}
params.page_id = pageInfo._id
}
const nowTime = dateTime.getTime()
const fillParams = {
appid: params.ak,
version: params.v ? params.v : '',
platform: platform.getPlatformCode(params.ut, params.p),
channel: channel.getChannelCode(params),
session_id: params.sid,
uid: params.uid,
last_visit_time: nowTime,
entry_page_id: params.page_id,
exit_page_id: params.page_id,
page_count: 0,
event_count: 0,
duration: 1,
is_finish: 0,
create_time: nowTime,
}
const res = await this.insert(this.tableName, fillParams)
if (res && res.id) {
return {
code: 0,
msg: 'success'
}
} else {
return {
code: 500,
msg: 'User session log filled error'
}
}
}
/**
* 检测用户会话是否有变化,并更新
* @param {Object} params 校验参数 - sid:基础会话编号 uid:用户编号 last_visit_user_id:基础会话中最近一个访问用户的编号
*/
async checkUserSession(params) {
if (!params.sid) {
return {
code: 200,
msg: 'Not found session log'
}
}
if (!params.uid) {
//用户已退出会话
if (params.last_visit_user_id) {
if (this.debug) {
console.log('user "' + params.last_visit_user_id + '" is exit session :', params.sid)
}
await this.closeUserSession(params.sid)
}
} else {
//添加用户日志
if (!params.last_visit_user_id) {
await this.fill(params)
}
//用户已切换
else if (params.uid != params.last_visit_user_id) {
if (this.debug) {
console.log('user "' + params.last_visit_user_id + '" change to "' + params.uid +
'" in the session :', params.sid)
}
//关闭原会话生成新用户对话
await this.closeUserSession(params.sid)
await this.fill(params)
}
}
return {
code: 0,
msg: 'success'
}
}
/**
* 关闭用户会话
* @param {String} sid 基础会话编号
*/
async closeUserSession(sid) {
if (this.debug) {
console.log('close user session log by sid:', sid)
}
return await this.update(this.tableName, {
is_finish: 1
}, {
session_id: sid,
is_finish: 0
})
}
/**
* 更新会话信息
* @param {String} sid 基础会话编号
* @param {Object} data 更新数据
*/
async updateUserSession(sid, data) {
const userSession = await this.getCollection(this.tableName).where({
session_id: sid,
uid: data.uid,
is_finish: 0
}).orderBy('create_time', 'desc').limit(1).get()
if (userSession.data.length === 0) {
console.log('Not found the user session', {
session_id: sid,
uid: data.uid,
is_finish: 0
})
return {
code: 300,
msg: 'Not found the user session'
}
}
let nowTime = data.nowTime ? data.nowTime : new DateTime().getTime()
const accessTime = nowTime - userSession.data[0].createTime
const accessSenconds = accessTime > 1000 ? parseInt(accessTime / 1000) : 1
const updateData = {
last_visit_time: nowTime,
duration: accessSenconds,
}
//访问页面数量
if (data.addPageCount) {
updateData.page_count = userSession.data[0].page_count + data.addPageCount
}
//最终访问的页面编号
if (data.pageId) {
updateData.exit_page_id = data.pageId
}
//产生事件次数
if (data.eventCount) {
updateData.event_count = userSession.data[0].event_count + data.addEventCount
}
if (this.debug) {
console.log('update user session log by sid-' + sid, updateData)
}
await this.update(this.tableName, updateData, {
_id: userSession.data[0]._id
})
return {
code: 0,
msg: 'success'
}
}
/**
* 清理用户会话日志数据
* @param {Object} days 保留天数, 留存统计需要计算30天后留存率,因此至少应保留31天的日志数据
*/
async clean(days = 31) {
days = Math.max(parseInt(days), 1)
console.log('clean user session logs - day:', days)
const dateTime = new DateTime()
const res = await this.delete(this.tableName, {
create_time: {
$lt: dateTime.getTimeBySetDays(0 - days)
}
})
if (!res.code) {
console.log('clean user session log:', res)
}
return res
}
}
/**
* @class Version 应用版本模型
*/
const BaseMod = require('./base')
const {
DateTime
} = require('../lib')
module.exports = class Version extends BaseMod {
constructor() {
super()
this.tableName = 'opendb-app-versions'
this.tablePrefix = false
this.cacheKeyPre = 'uni-stat-app-version-'
}
/**
* 获取版本信息
* @param {String} appid DCloud-appid
* @param {String} platformId 平台编号
* @param {String} appVersion 平台版本号
*/
async getVersion(appid, platform, appVersion) {
const cacheKey = this.cacheKeyPre + appid + '-' + platform + '-' + appVersion
let versionData = await this.getCache(cacheKey)
if (!versionData) {
const versionInfo = await this.getCollection(this.tableName).where({
appid: appid,
uni_platform: platform,
type: 'native_app',
version: appVersion
}).limit(1).get()
versionData = []
if (versionInfo.data.length > 0) {
versionData = versionInfo.data[0]
await this.setCache(cacheKey, versionData)
}
}
return versionData
}
/**
* 获取版本信息没有则进行创建
* @param {String} appid DCloud-appid
* @param {String} platform 平台代码
* @param {String} appVersion 平台版本号
*/
async getVersionAndCreate(appid, platform, appVersion) {
const versionInfo = await this.getVersion(appid, platform, appVersion)
if (versionInfo.length === 0) {
if (appVersion.length > 0 && !appVersion.includes('}')) {
const thisTime = new DateTime().getTime()
const insertParam = {
appid: appid,
platform: [],
uni_platform: platform,
type: 'native_app',
version: appVersion,
stable_publish: false,
create_env: 'uni-stat',
create_date: thisTime
}
const res = await this.insert(this.tableName, insertParam)
if (res && res.id) {
return Object.assign(insertParam, {
_id: res.id
})
}
}
}
return versionInfo
}
}
/**
* @class UniStatReportDataReceiver uni统计上报数据接收器
* @function report 上报数据调度处理函数
*/
const {
parseUrlParams
} = require('../shared')
const SessionLog = require('./mod/sessionLog')
const PageLog = require('./mod/pageLog')
const EventLog = require('./mod/eventLog')
const ErrorLog = require('./mod/errorLog')
const Device = require('./mod/device')
class UniStatReportDataReceiver {
/**
* @description 上报数据调度处理函数
* @param {Object} params 基础上报参数
* @param {Object} context 请求附带的上下文信息
*/
async report(params, context) {
let res = {
code: 0,
msg: 'success'
}
if (!params || !params.requests) {
return {
code: 200,
msg: 'Invild params'
}
}
// JSON参数解析
const requestParam = JSON.parse(params.requests)
if (!requestParam || requestParam.length === 0) {
return {
code: 200,
msg: 'Invild params'
}
}
// 日志填充
const sessionParams = []
const pageParams = []
const eventParams = []
const errorParams = []
const device = new Device()
for (const ri in requestParam) {
//参数解析
const urlParams = parseUrlParams(requestParam[ri], context)
if (!urlParams.ak) {
return {
code: 201,
msg: 'Not found appid'
}
}
if (!urlParams.lt) {
return {
code: 202,
msg: 'Not found this log type'
}
}
switch (parseInt(urlParams.lt)) {
// 会话日志
case 1: {
sessionParams.push(urlParams)
break
}
// 页面日志
case 3:
case 11: {
pageParams.push(urlParams)
break
}
// 事件日志
case 21: {
eventParams.push(urlParams)
break
}
// 错误日志
case 31: {
errorParams.push(urlParams)
break
}
//unipush信息绑定
case 101: {
res = await device.bindPush(urlParams)
break
}
default: {
console.log('Invalid type by param "lt:' + urlParams.lt + '"')
break
}
}
}
//会话日志填充
if (sessionParams.length > 0) {
const sessionLog = new SessionLog()
res = await sessionLog.batchFill(sessionParams)
}
//页面日志填充
if (pageParams.length > 0) {
const pageLog = new PageLog()
res = await pageLog.fill(pageParams)
}
//事件日志填充
if (eventParams.length > 0) {
const eventLog = new EventLog()
res = await eventLog.fill(eventParams)
}
//错误日志填充
if (errorParams.length > 0) {
const errorLog = new ErrorLog()
res = await errorLog.fill(errorParams)
}
return res
}
}
module.exports = UniStatReportDataReceiver
/**
* @class UniStatDataStat uni统计-数据统计调度处理模块
* @function cron 数据统计定时任务处理函数
* @function stat 数据统计调度处理函数
* @function cleanLog 日志清理调度处理函数
*/
const {
DateTime
} = require('./lib')
const {
sleep
} = require('../shared')
const {
BaseMod,
SessionLog,
PageLog,
EventLog,
ShareLog,
ErrorLog,
StatResult,
ActiveDevices,
ActiveUsers,
PageResult,
EventResult,
ErrorResult,
Loyalty,
RunErrors,
UserSessionLog,
uniPay,
Setting
} = require('./mod')
class UniStatDataStat {
/**
* 数据统计定时任务处理函数
* @param {Object} context 服务器请求上下文参数
*/
async cron(context) {
const baseMod = new BaseMod()
const dateTime = new DateTime()
console.log('Cron start time: ', dateTime.getDate('Y-m-d H:i:s'))
// const setting = new Setting();
// let settingValue = await setting.getSetting()
// if (settingValue.mode === "close") {
// // 如果关闭了统计任务,则任务直接结束
// return {
// code: 0,
// msg: 'Task is close',
// }
// } else if (settingValue.mode === "auto") {
// // 如果开启了节能模式,则判断N天内是否有设备访问记录
// let runKey = await setting.checkAutoRun(settingValue);
// if (!runKey) {
// return {
// code: 0,
// msg: 'Task is auto close',
// }
// }
// }
//获取运行参数
const timeInfo = dateTime.getTimeInfo(null, false)
const cronConfig = baseMod.getConfig('cron')
const cronMin = baseMod.getConfig('cronMin')
const realtimeStat = baseMod.getConfig('realtimeStat')
// 数据跑批
let res = null
if (cronConfig && cronConfig.length > 0) {
for (var mi in cronConfig) {
const currCronConfig = cronConfig[mi]
const cronType = currCronConfig.type
const cronTime = currCronConfig.time.split(' ')
const cronDimension = currCronConfig.dimension
//未开启分钟级定时任务,则设置为小时级定时任务
if (cronTime.length === 4 && !cronMin) {
cronTime.splice(3, 1)
}
if (baseMod.debug) {
console.log('cronTime', cronTime)
}
//精度为分钟级的定时任务
if (cronTime.length === 4) {
if (cronTime[0] !== '*') {
//周统计任务
if (timeInfo.nWeek == cronTime[0] && timeInfo.nHour == cronTime[2] && timeInfo.nMinutes ==
cronTime[3]) {
let dimension = cronDimension || 'week';
console.log(cronType + `--${dimension} run`)
res = await this.stat({
type: cronType,
dimension: cronDimension,
config: currCronConfig
})
}
} else if (cronTime[1] !== '*') {
//月统计任务(包含季度统计任务和年统计任务)
if (timeInfo.nDay == cronTime[1] && timeInfo.nHour == cronTime[2] && timeInfo.nMinutes ==
cronTime[3]) {
let dimension = cronDimension || 'month';
console.log(cronType + `--${dimension} run`)
res = await this.stat({
type: cronType,
dimension: dimension,
config: currCronConfig
})
}
} else if (cronTime[2] !== '*') {
//日统计任务
if (timeInfo.nHour == cronTime[2] && timeInfo.nMinutes == cronTime[3]) {
let dimension = cronDimension || 'day';
console.log(cronType + `--${dimension} run`)
res = await this.stat({
type: cronType,
dimension: dimension,
config: currCronConfig
})
}
} else if (cronTime[3] !== '*') {
//实时统计任务
if (timeInfo.nMinutes == cronTime[3] && realtimeStat) {
let dimension = cronDimension || 'hour';
console.log(cronType + `--${dimension} run`)
res = await this.stat({
type: cronType,
dimension: dimension,
config: currCronConfig
})
}
}
}
//精度为小时级的定时任务
else if (cronTime.length === 3) {
if (cronTime[0] !== '*') {
//周统计任务
if (timeInfo.nWeek == cronTime[0] && timeInfo.nHour == cronTime[2]) {
let dimension = cronDimension || 'week';
console.log(cronType + `--${dimension} run`)
res = await this.stat({
type: cronType,
dimension: dimension,
config: currCronConfig
})
}
} else if (cronTime[1] !== '*') {
//月统计任务(包含季度统计任务和年统计任务)
if (timeInfo.nDay == cronTime[1] && timeInfo.nHour == cronTime[2]) {
let dimension = cronDimension || 'month';
console.log(cronType + `--${dimension} run`)
res = await this.stat({
type: cronType,
dimension: dimension,
config: currCronConfig
})
}
} else if (cronTime[2] !== '*') {
//日统计任务
if (timeInfo.nHour == cronTime[2]) {
let dimension = cronDimension || 'day';
console.log(cronType + `--${dimension} run`)
res = await this.stat({
type: cronType,
dimension: dimension,
config: currCronConfig
})
}
} else {
//实时统计任务
if (realtimeStat) {
let dimension = cronDimension || 'hour';
console.log(cronType + `--${dimension} run`)
res = await this.stat({
type: cronType,
dimension: dimension,
config: currCronConfig
})
}
}
} else {
console.error('Cron configuration error')
}
}
}
console.log('Cron end time: ', dateTime.getDate('Y-m-d H:i:s'))
return {
code: 0,
msg: 'Task have done',
lastCronResult: res
}
}
/**
* 数据统计调度处理函数
* @param {Object} params 统计参数
*/
async stat(params) {
const {
type,
dimension,
date,
reset,
config
} = params
let res = {
code: 0,
msg: 'success'
}
try {
switch (type) {
// 基础统计
case 'stat': {
const resultStat = new StatResult()
res = await resultStat.stat(dimension, date, reset)
break
}
// 活跃设备统计归集
case 'active-device': {
const activeDevices = new ActiveDevices()
res = await activeDevices.stat(date, reset)
break
}
// 活跃用户统计归集
case 'active-user': {
const activeUsers = new ActiveUsers()
res = await activeUsers.stat(date, reset)
break
}
// 设备留存统计
case 'retention-device': {
const retentionStat = new StatResult()
res = await retentionStat.retentionStat(dimension, date)
break
}
// 用户留存统计
case 'retention-user': {
const retentionStat = new StatResult()
res = await retentionStat.retentionStat(dimension, date, 'user')
break
}
// 页面统计
case 'page': {
const pageStat = new PageResult()
res = await pageStat.stat(dimension, date, reset)
break
}
// 事件统计
case 'event': {
const eventStat = new EventResult()
res = await eventStat.stat(dimension, date, reset)
break
}
// 错误统计
case 'error': {
const errorStat = new ErrorResult()
res = await errorStat.stat(dimension, date, reset)
break
}
// 设备忠诚度统计
case 'loyalty': {
const loyaltyStat = new Loyalty()
res = await loyaltyStat.stat(dimension, date, reset)
break
}
// 日志清理
case 'clean': {
res = await this.cleanLog()
}
// 支付统计
case 'pay-result': {
const paymentResult = new uniPay.PayResult()
res = await paymentResult.stat(dimension, date, reset, config)
break
}
}
} catch (e) {
const maxTryTimes = 2
if (!this.tryTimes) {
this.tryTimes = 1
} else {
this.tryTimes++
}
//报错则重新尝试2次, 解决部分云服务器偶现连接超时问题
if (this.tryTimes <= maxTryTimes) {
//休眠1秒后重新调用
await sleep(1000)
params.reset = true
res = await this.stat(params)
} else {
// 2次尝试失败后记录错误
console.error('server error: ' + e)
const runError = new RunErrors()
runError.create({
mod: 'stat',
params: params,
error: e,
create_time: new DateTime().getTime()
})
res = {
code: 500,
msg: 'server error' + e
}
}
}
return res
}
/**
* 日志清理调度处理函数
*/
async cleanLog() {
const baseMod = new BaseMod()
const cleanLog = baseMod.getConfig('cleanLog')
if (!cleanLog || !cleanLog.open) {
return {
code: 100,
msg: 'The log cleanup service has not been turned on'
}
}
const res = {
code: 0,
msg: 'success',
data: {}
}
// 会话日志
if (cleanLog.reserveDays.sessionLog > 0) {
const sessionLog = new SessionLog()
res.data.sessionLog = await sessionLog.clean(cleanLog.reserveDays.sessionLog)
}
// 用户会话日志
if (cleanLog.reserveDays.userSessionLog > 0) {
const userSessionLog = new UserSessionLog()
res.data.userSessionLog = await userSessionLog.clean(cleanLog.reserveDays.userSessionLog)
}
// 页面日志
if (cleanLog.reserveDays.pageLog > 0) {
const pageLog = new PageLog()
res.data.pageLog = await pageLog.clean(cleanLog.reserveDays.pageLog)
}
// 事件日志
if (cleanLog.reserveDays.eventLog > 0) {
const eventLog = new EventLog()
res.data.eventLog = await eventLog.clean(cleanLog.reserveDays.eventLog)
}
// 分享日志
if (cleanLog.reserveDays.shareLog > 0) {
const shareLog = new ShareLog()
res.data.shareLog = await shareLog.clean(cleanLog.reserveDays.shareLog)
}
// 错误日志
if (cleanLog.reserveDays.errorLog > 0) {
const errorLog = new ErrorLog()
res.data.errorLog = await errorLog.clean(cleanLog.reserveDays.errorLog)
}
// 活跃设备日志
const activeDevicesLog = new ActiveDevices()
res.data.activeDevicesLog = await activeDevicesLog.clean()
// 活跃用户日志
const activeUsersLog = new ActiveUsers()
res.data.activeUsersLog = await activeUsersLog.clean()
// 实时统计日志
const resultHourLog = new StatResult()
res.data.resultHourLog = await resultHourLog.cleanHourLog()
//原生应用崩溃日志
const appCrashLogs = new AppCrashLogs()
res.data.appCrashLogs = await appCrashLogs.clean()
return res
}
}
module.exports = UniStatDataStat
const fs = require('fs')
const path = require('path')
const TE = require('./lib/art-template.js');
// 标准语法的界定符规则
TE.defaults.openTag = '{@'
TE.defaults.closeTag = '@}'
const success = {
success: true
}
const fail = {
success: false
}
async function translateTCB(_fileList = []) {
if (!_fileList.length) return _fileList
// 腾讯云和阿里云下载链接不同,需要处理一下,阿里云会原样返回
const {
fileList
} = await uniCloud.getTempFileURL({
fileList: _fileList
});
return fileList.map((item, index) => item.tempFileURL ? item.tempFileURL : _fileList[index])
}
function hasValue(value) {
if (typeof value !== 'object') return !!value
if (value instanceof Array) return !!value.length
return !!(value && Object.keys(value).length)
}
module.exports = async function(id) {
if (!id) {
return {
...fail,
code: -1,
errMsg: 'id required'
};
}
// 根据sitemap配置加载页面模板,例如列表页,详情页
let templatePage = fs.readFileSync(path.resolve(__dirname, './template.html'), 'utf8');
if (!templatePage) {
return {
...fail,
code: -2,
errMsg: 'page template no found'
};
}
const db = uniCloud.database()
let dbPublishList
try {
dbPublishList = db.collection('opendb-app-list')
} catch (e) {}
if (!dbPublishList) return fail;
const record = await dbPublishList.where({
_id: id
}).get({
getOne: true
})
if (record && record.data && record.data.length) {
const appInfo = record.data[0]
const defaultOptions = {
hasApp: false,
hasMP: false,
hasH5: false,
hasQuickApp: false
}
defaultOptions.mpNames = {
'mp_weixin': '微信',
'mp_alipay': '支付宝',
'mp_baidu': '百度',
'mp_toutiao': '字节',
'mp_qq': 'QQ',
'mp_dingtalk': '钉钉',
'mp_kuaishou': '快手',
'mp_lark': '飞书',
'mp_jd': '京东'
}
const imageList = [];
['app_android'].forEach(key => {
if (!hasValue(appInfo[key])) return
imageList.push({
key,
urlKey: 'url',
url: appInfo[key].url
})
})
Object.keys(defaultOptions.mpNames).concat('quickapp').forEach(key => {
if (!hasValue(appInfo[key])) return
imageList.push({
key,
urlKey: 'qrcode_url',
url: appInfo[key].qrcode_url
})
});
['icon_url'].forEach(key => {
if (!hasValue(appInfo[key])) return
imageList.push({
key,
url: appInfo[key]
})
})
const filelist = await translateTCB(imageList.map(item => item.url))
imageList.forEach((item, index) => {
if (item.urlKey) {
appInfo[item.key][item.urlKey] = filelist[index]
} else {
appInfo[item.key] = filelist[index]
}
})
if (hasValue(appInfo.screenshot)) {
appInfo.screenshot = await translateTCB(appInfo.screenshot)
}
{
const appInfoKeys = Object.keys(appInfo)
if (appInfoKeys.some(key => {
return key.indexOf('app_') !== -1 && hasValue(appInfo[key])
})) {
defaultOptions.hasApp = true
}
if (appInfoKeys.some(key => {
return key.indexOf('mp') !== -1 && hasValue(appInfo[key])
})) {
defaultOptions.hasMP = true
}
if (appInfo.h5 && appInfo.h5.url) {
defaultOptions.hasH5 = true
}
if (appInfo.quickapp && appInfo.quickapp.qrcode_url) {
defaultOptions.hasQuickApp = true
}
// app
if (defaultOptions.hasApp && appInfo.app_android && appInfo.app_android.url) {
defaultOptions.android_url = appInfo.app_android.url
} else {
defaultOptions.android_url = ''
}
if (defaultOptions.hasApp && appInfo.app_ios && appInfo.app_ios.url) {
defaultOptions.ios_url = appInfo.app_ios.url
} else {
defaultOptions.ios_url = ''
}
// mp
defaultOptions.mpKeys = Object.keys(appInfo).filter(key => {
return key.indexOf('mp') !== -1 && hasValue(appInfo[key])
})
}
const html = TE.render(templatePage)(Object.assign({}, appInfo, defaultOptions));
if (!(defaultOptions.hasApp || defaultOptions.hasH5 || defaultOptions.hasMP || defaultOptions
.hasQuickApp)) {
return {
...fail,
code: -100,
errMsg: '缺少应用信息,App、小程序、H5、快应用请至少填写一项'
}
}
return {
...success,
mpserverlessComposedResponse: true, // 使用阿里云返回集成响应是需要此字段为true
statusCode: 200,
headers: {
'content-type': 'text/html'
},
body: html
};
}
return {
...fail,
code: -3,
errMsg: 'no record'
};
}
/*!art-template - Template Engine | http://aui.github.com/artTemplate/*/
!function(){function a(a){return a.replace(t,"").replace(u,",").replace(v,"").replace(w,"").replace(x,"").split(y)}function b(a){return"'"+a.replace(/('|\\)/g,"\\$1").replace(/\r/g,"\\r").replace(/\n/g,"\\n")+"'"}function c(c,d){function e(a){return m+=a.split(/\n/).length-1,k&&(a=a.replace(/\s+/g," ").replace(/<!--[\w\W]*?-->/g,"")),a&&(a=s[1]+b(a)+s[2]+"\n"),a}function f(b){var c=m;if(j?b=j(b,d):g&&(b=b.replace(/\n/g,function(){return m++,"$line="+m+";"})),0===b.indexOf("=")){var e=l&&!/^=[=#]/.test(b);if(b=b.replace(/^=[=#]?|[\s;]*$/g,""),e){var f=b.replace(/\s*\([^\)]+\)/,"");n[f]||/^(include|print)$/.test(f)||(b="$escape("+b+")")}else b="$string("+b+")";b=s[1]+b+s[2]}return g&&(b="$line="+c+";"+b),r(a(b),function(a){if(a&&!p[a]){var b;b="print"===a?u:"include"===a?v:n[a]?"$utils."+a:o[a]?"$helpers."+a:"$data."+a,w+=a+"="+b+",",p[a]=!0}}),b+"\n"}var g=d.debug,h=d.openTag,i=d.closeTag,j=d.parser,k=d.compress,l=d.escape,m=1,p={$data:1,$filename:1,$utils:1,$helpers:1,$out:1,$line:1},q="".trim,s=q?["$out='';","$out+=",";","$out"]:["$out=[];","$out.push(",");","$out.join('')"],t=q?"$out+=text;return $out;":"$out.push(text);",u="function(){var text=''.concat.apply('',arguments);"+t+"}",v="function(filename,data){data=data||$data;var text=$utils.$include(filename,data,$filename);"+t+"}",w="'use strict';var $utils=this,$helpers=$utils.$helpers,"+(g?"$line=0,":""),x=s[0],y="return new String("+s[3]+");";r(c.split(h),function(a){a=a.split(i);var b=a[0],c=a[1];1===a.length?x+=e(b):(x+=f(b),c&&(x+=e(c)))});var z=w+x+y;g&&(z="try{"+z+"}catch(e){throw {filename:$filename,name:'Render Error',message:e.message,line:$line,source:"+b(c)+".split(/\\n/)[$line-1].replace(/^\\s+/,'')};}");try{var A=new Function("$data","$filename",z);return A.prototype=n,A}catch(B){throw B.temp="function anonymous($data,$filename) {"+z+"}",B}}var d=function(a,b){return"string"==typeof b?q(b,{filename:a}):g(a,b)};d.version="3.0.0",d.config=function(a,b){e[a]=b};var e=d.defaults={openTag:"<%",closeTag:"%>",escape:!0,cache:!0,compress:!1,parser:null},f=d.cache={};d.render=function(a,b){return q(a,b)};var g=d.renderFile=function(a,b){var c=d.get(a)||p({filename:a,name:"Render Error",message:"Template not found"});return b?c(b):c};d.get=function(a){var b;if(f[a])b=f[a];else if("object"==typeof document){var c=document.getElementById(a);if(c){var d=(c.value||c.innerHTML).replace(/^\s*|\s*$/g,"");b=q(d,{filename:a})}}return b};var h=function(a,b){return"string"!=typeof a&&(b=typeof a,"number"===b?a+="":a="function"===b?h(a.call(a)):""),a},i={"<":"&#60;",">":"&#62;",'"':"&#34;","'":"&#39;","&":"&#38;"},j=function(a){return i[a]},k=function(a){return h(a).replace(/&(?![\w#]+;)|[<>"']/g,j)},l=Array.isArray||function(a){return"[object Array]"==={}.toString.call(a)},m=function(a,b){var c,d;if(l(a))for(c=0,d=a.length;d>c;c++)b.call(a,a[c],c,a);else for(c in a)b.call(a,a[c],c)},n=d.utils={$helpers:{},$include:g,$string:h,$escape:k,$each:m};d.helper=function(a,b){o[a]=b};var o=d.helpers=n.$helpers;d.onerror=function(a){var b="Template Error\n\n";for(var c in a)b+="<"+c+">\n"+a[c]+"\n\n";"object"==typeof console&&console.error(b)};var p=function(a){return d.onerror(a),function(){return"{Template Error}"}},q=d.compile=function(a,b){function d(c){try{return new i(c,h)+""}catch(d){return b.debug?p(d)():(b.debug=!0,q(a,b)(c))}}b=b||{};for(var g in e)void 0===b[g]&&(b[g]=e[g]);var h=b.filename;try{var i=c(a,b)}catch(j){return j.filename=h||"anonymous",j.name="Syntax Error",p(j)}return d.prototype=i.prototype,d.toString=function(){return i.toString()},h&&b.cache&&(f[h]=d),d},r=n.$each,s="break,case,catch,continue,debugger,default,delete,do,else,false,finally,for,function,if,in,instanceof,new,null,return,switch,this,throw,true,try,typeof,var,void,while,with,abstract,boolean,byte,char,class,const,double,enum,export,extends,final,float,goto,implements,import,int,interface,long,native,package,private,protected,public,short,static,super,synchronized,throws,transient,volatile,arguments,let,yield,undefined",t=/\/\*[\w\W]*?\*\/|\/\/[^\n]*\n|\/\/[^\n]*$|"(?:[^"\\]|\\[\w\W])*"|'(?:[^'\\]|\\[\w\W])*'|\s*\.\s*[$\w\.]+/g,u=/[^\w$]+/g,v=new RegExp(["\\b"+s.replace(/,/g,"\\b|\\b")+"\\b"].join("|"),"g"),w=/^\d[^,]*|,\d[^,]*/g,x=/^,+|,+$/g,y=/^$|,+/;e.openTag="{{",e.closeTag="}}";var z=function(a,b){var c=b.split(":"),d=c.shift(),e=c.join(":")||"";return e&&(e=", "+e),"$helpers."+d+"("+a+e+")"};e.parser=function(a){a=a.replace(/^\s/,"");var b=a.split(" "),c=b.shift(),e=b.join(" ");switch(c){case"if":a="if("+e+"){";break;case"else":b="if"===b.shift()?" if("+b.join(" ")+")":"",a="}else"+b+"{";break;case"/if":a="}";break;case"each":var f=b[0]||"$data",g=b[1]||"as",h=b[2]||"$value",i=b[3]||"$index",j=h+","+i;"as"!==g&&(f="[]"),a="$each("+f+",function("+j+"){";break;case"/each":a="});";break;case"echo":a="print("+e+");";break;case"print":case"include":a=c+"("+b.join(",")+");";break;default:if(/^\s*\|\s*[\w\$]/.test(e)){var k=!0;0===a.indexOf("#")&&(a=a.substr(1),k=!1);for(var l=0,m=a.split("|"),n=m.length,o=m[l++];n>l;l++)o=z(o,m[l]);a=(k?"=":"=#")+o}else a=d.helpers[c]?"=#"+c+"("+b.join(",")+");":"="+a}return a},"function"==typeof define?define(function(){return d}):"undefined"!=typeof exports?module.exports=d:this.template=d}();
\ No newline at end of file
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="pragma" content="no-cache">
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
<link rel="icon" sizes="any" mask="" href="{@icon_url@}">
<title>{@name@}</title>
<style type="text/css">*{box-sizing:border-box;-webkit-box-sizing:border-box}body,html{margin:0;padding:0;background-color:#fff;font-size:14px}a{text-decoration:none}.main{padding:0}.info,.main{height:100%}img[src=""]{opacity:0}.bottom-wrap .head-content *,.list-wrap ul li,.logo-wrap *{vertical-align:middle}.downloads-content .package,.pc-info .mask{visibility:hidden}.main{text-align:center;margin:0 auto;max-width:100%;width:700px}.info{position:relative}.downloads-wrap,.logo-wrap{width:100%}.downloads-wrap{bottom:0;position:absolute}.logo-wrap{padding-top:30px}.logo-wrap>img{border-radius:5px;width:79px;height:79px}.logo-wrap h2{font-size:18px;color:#2a2a2a;font-weight:700}.desc-content h2,.screenshots-content h2{font-weight:400;font-size:15px;color:#2a2a2a}.logo-wrap p{margin-top:21px;font-size:18px;font-weight:400;color:#2a2a2a;padding:0 20px}.logo-wrap #show_qrcode{display:block;font-size:10px;color:#007aff;text-decoration:none;margin-top:28px}.logo-wrap #show_qrcode>img{width:11px;height:11px}.logo-wrap #show_qrcode>span{display:inline-block;margin-left:10px}.command-content p{display:inline-block;font-size:12px;color:#2a2a2a}.command-content .stream-token{font-size:12px;color:#2a2a2a;background-color:#f8f8f8;border-radius:3px;padding:5px}.command-content a{display:inline-block;font-size:11px;color:#007aff;width:50px}.qrcode-content{display:none;margin-top:50px;margin-bottom:20px;font-size:20px}.qrcode-content img{width:120px;height:120px}.qrcode-content span{display:block}.qrcode-content .code{display:inline-block;color:#007aff}.qrcode-content p{margin:0;padding:0;display:inline-block}.btn-primary,.btn-secondary{background-color:#007aff;color:#fff;margin-top:10px}.btn{font-size:12px;width:288px;height:40px;line-height:40px;border-radius:5px;outline:0}.btn-primary{border:1px solid #007aff}.btn-secondary{border:1px solid #fff;margin-bottom:10px}.btn.stream{background-color:#fff;color:#007aff}.more img{height:10px;margin-bottom:5px}.more.down img{transform:rotateX(0)}.more.up img{transform:rotateX(180deg)}.screenshots-content{border-top:1px solid #bfbfbf;margin:0 15px;padding:0 24px;position:relative}.screenshots-content h2{margin-top:40px;margin-bottom:21px;text-align:left}.list-wrap{overflow-x:auto;white-space:nowrap;margin-bottom:36px;margin-left:39px}.list-wrap ul{display:block;list-style:none;white-space:nowrap;-webkit-padding-start:0;-moz-padding-start:0}.bottom-wrap .tips-content .tip-desc,.desc-content pre{white-space:pre-wrap;font-family:Helvetica Neue,Helvetica,Arial,sans-serif;word-break:break-all}.list-wrap ul li{display:inline-block;margin:0 9px}.list-wrap ul li:first-child{margin:0 9px 0 0}.list-wrap ul li img{width:176px;height:311px;border:1px solid #949494}.desc-content{border-top:1px solid #bfbfbf;margin:0 15px 36px;padding:0 24px;text-align:center}.desc-content h2{margin-top:40px;margin-bottom:21px;text-align:left}.desc-content pre{width:100%;font-size:14px;color:#2a2a2a;text-align:left}.footer{text-align:center;padding:20px;border-top:1px solid #dae2e3}.show{display:block}.hide{display:none!important}.overlay{width:100%;height:100%;left:0;top:0;background:rgba(0,0,0,.6);z-index:200;position:fixed}.bottom-wrap{position:absolute;bottom:0;z-index:201;width:100%;padding-bottom:18px;background-color:#fff}.bottom-wrap .head-content{padding:12px 0;text-align:left;margin-left:21px}.bottom-wrap .head-content img{display:inline-block;width:28px;height:28px}.bottom-wrap .head-content h2{display:inline-block;font-size:15px;font-weight:700;color:#313131;margin-left:10px}.bottom-wrap .tips-content{text-align:left;margin-left:21px;margin-right:21px;border-top:1px solid #bfbfbf}.bottom-wrap .tips-content .tip-desc{font-size:13px}.bottom-wrap .tips-content .tip-title{font-size:14px;color:#313131;margin:8px 0 0;font-weight:400}.bottom-wrap .tips-content .tip-token{font-size:13px;color:#313131;margin:8px 0 0;display:inline-block;padding:3px;background-color:#96d2fa}.bottom-wrap .tips-content .tip-guide{font-size:14px;margin:8px 0 0}.bottom-wrap .tips-content .guide-launch{margin:15px 0}.bottom-wrap .tips-content ul{-webkit-padding-start:21px}.bottom-wrap .tips-content ul li{margin:0;padding:0;-webkit-padding-start:0}.bottom-wrap button{width:327px}.qrcode-wrap{position:absolute;width:320px;height:350px;background-color:#fff;top:50%;left:50%;margin-top:-164px;margin-left:-160px;border-radius:5px}.qrcode-wrap div.close{display:block;width:30px;height:30px;position:absolute;right:15px;top:10px}.qrcode-wrap div.close img{width:14px;height:14px;margin-top:8px}.qrcode-wrap .kuaima-wrap{width:211px;height:211px;display:inline-block;margin-top:28px;border:1px solid #8eceab;background-color:#fbfffe;border-radius:5px}.qrcode-wrap .kuaima-wrap .qrcode{width:161px;height:161px;margin-top:25px}.qrcode-wrap .kuaima-wrap .app-name{display:block;font-size:12px}.qrcode-wrap .channel{width:211px}.qrcode-wrap .desc{color:#c5c5c5;font-size:12px}.pc-info{display:none;position:relative;text-align:left;margin-top:80px}.pc-name h2{font-size:24px;color:#2a2a2a;font-weight:700}.pc-name p{font-size:22px;color:#2a2a2a;font-weight:400;margin-top:30px;width:520px;word-wrap:break-word;word-break:break-all;overflow:hidden}.contact-list-item .contact-title,.h5-title,.mp-title-input{font-weight:700}.btn-download{width:260px;background-color:#fff;border:1px solid #007aff;color:#007aff}.btn-download .icon{margin-right:5px;line-height:14px}@font-face{font-family:publish_iconfont;src:url(data:font/ttf;base64,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AckKQ3JlYXRlZCBieSBpY29uZm9udApwdWJsaXNoX2ljb25mb250UmVndWxhcnB1Ymxpc2hfaWNvbmZvbnRwdWJsaXNoX2ljb25mb250VmVyc2lvbiAxLjBwdWJsaXNoX2ljb25mb250R2VuZXJhdGVkIGJ5IHN2ZzJ0dGYgZnJvbSBGb250ZWxsbyBwcm9qZWN0Lmh0dHA6Ly9mb250ZWxsby5jb20ACgBDAHIAZQBhAHQAZQBkACAAYgB5ACAAaQBjAG8AbgBmAG8AbgB0AAoAcAB1AGIAbABpAHMAaABfAGkAYwBvAG4AZgBvAG4AdABSAGUAZwB1AGwAYQByAHAAdQBiAGwAaQBzAGgAXwBpAGMAbwBuAGYAbwBuAHQAcAB1AGIAbABpAHMAaABfAGkAYwBvAG4AZgBvAG4AdABWAGUAcgBzAGkAbwBuACAAMQAuADAAcAB1AGIAbABpAHMAaABfAGkAYwBvAG4AZgBvAG4AdABHAGUAbgBlAHIAYQB0AGUAZAAgAGIAeQAgAHMAdgBnADIAdAB0AGYAIABmAHIAbwBtACAARgBvAG4AdABlAGwAbABvACAAcAByAG8AagBlAGMAdAAuAGgAdAB0AHAAOgAvAC8AZgBvAG4AdABlAGwAbABvAC4AYwBvAG0AAAAAAgAAAAAAAAAKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAQIBAwEEAQUAAXgHYW5kcm9pZANpb3MAAAA=) format("truetype")}.publish_iconfont{font-family:publish_iconfont!important;font-size:14px;font-style:normal;-webkit-font-smoothing:antialiased;-webkit-text-stroke-width:.2px;-moz-osx-font-smoothing:grayscale}#pc_download .btn-download,#pc_download_plus .btn-download{display:inline-block;text-decoration:none;text-align:center}.pc-download.plus-app{display:flex;position:relative;flex-direction:row;padding-top:50px;padding-bottom:50px}.pc-download.plus-app.center{justify-content:center}.pc-download.plus-app .qrcode-row{display:inline-block;width:200px;text-align:center}.pc-download.plus-app .qrcode-row .qrcode-img{width:200px;height:200px}.pc-download.plus-app .qrcode-row .app-tip{margin:0}.pc-download.plus-app .btn-row{display:inline-flex;flex-direction:column;margin-left:80px;justify-content:center}.pc-download.plus-app .btn-row .btn-download{margin:15px 0;width:240px}.pc-download.plus-app:after{position:absolute;right:0;bottom:0;left:0;height:1px;content:"";transform:scaleY(.5);background-color:#c8c7cc}@media only screen and (min-width:768px){.info.mobile{display:none}.pc-info{display:block}.desc-content{padding:0;margin:0 0 80px;border:none}.desc-content pre{color:#818181;font-size:14px}.desc-content h2{font-size:22px;margin-bottom:40px}.screenshots-content{border:none;margin:0;padding:0}.screenshots-content h2{font-size:22px;margin-bottom:40px}.list-wrap{margin-left:0}.tab-item{font-size:20px!important}}@media only screen and (max-width:320px){body{font-size:12px}}.command-content p{width:288px;text-align:left;position:relative}.command-content p .stream-token{width:230px;border:none}.command-content .copy-command{text-align:center;position:absolute;right:0;top:-5px;font-size:14px;line-height:14px;width:auto;background-color:#c5c5c5;color:#fff;padding:6px;border-radius:3px;text-decoration:none}.pc-logo{position:absolute;left:-126px}.pc-logo img{border-radius:5px;width:90px;height:90px}.pc-barcode{position:absolute;right:0;width:160px;height:160px;border:1px solid #8eceab;background-color:#fbfffe;border-radius:5px;text-align:center}.pc-barcode .kuaima-wrap .qrcode{width:120px;height:120px;margin-top:15px}.pc-barcode .kuaima-wrap .app-name{display:block;font-size:12px}.pc-barcode .tip{color:#296a43;margin-top:10px;font-size:14px}.bottom-wrap .head-content .loading-tip{display:inline-block;position:absolute;font-size:12px;right:21px;color:#c5c5c5}.loading-tip .spinner{margin-right:5px}.spinner{display:inline-block;width:16px;height:16px;transform-origin:50%;animation:a 1s step-end infinite}.spinner:after{display:block;width:100%;height:100%;content:"";background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 120 120' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cdefs%3E%3Cpath id='a' stroke='%236c6c6c' stroke-width='11' stroke-linecap='round' d='M60 7v20'/%3E%3C/defs%3E%3Cuse xlink:href='%23a' opacity='.27'/%3E%3Cuse xlink:href='%23a' opacity='.27' transform='rotate(30 60 60)'/%3E%3Cuse xlink:href='%23a' opacity='.27' transform='rotate(60 60 60)'/%3E%3Cuse xlink:href='%23a' opacity='.27' transform='rotate(90 60 60)'/%3E%3Cuse xlink:href='%23a' opacity='.27' transform='rotate(120 60 60)'/%3E%3Cuse xlink:href='%23a' opacity='.27' transform='rotate(150 60 60)'/%3E%3Cuse xlink:href='%23a' opacity='.37' transform='rotate(180 60 60)'/%3E%3Cuse xlink:href='%23a' opacity='.46' transform='rotate(210 60 60)'/%3E%3Cuse xlink:href='%23a' opacity='.56' transform='rotate(240 60 60)'/%3E%3Cuse xlink:href='%23a' opacity='.66' transform='rotate(270 60 60)'/%3E%3Cuse xlink:href='%23a' opacity='.75' transform='rotate(300 60 60)'/%3E%3Cuse xlink:href='%23a' opacity='.85' transform='rotate(330 60 60)'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:50%;background-size:100%}#download_list a.stream,.pc-info .mask a,.tab-item{display:inline-block;text-decoration:none}@keyframes a{0%{transform:rotate(0)}8.33333333%{transform:rotate(30deg)}16.66666667%{transform:rotate(60deg)}25%{transform:rotate(90deg)}33.33333333%{transform:rotate(120deg)}41.66666667%{transform:rotate(150deg)}50%{transform:rotate(180deg)}58.33333333%{transform:rotate(210deg)}66.66666667%{transform:rotate(240deg)}75%{transform:rotate(270deg)}83.33333333%{transform:rotate(300deg)}91.66666667%{transform:rotate(330deg)}to{transform:rotate(1turn)}}.tip-button-row{text-align:right;padding-right:21px}.pc-info .mask,.tab-content.pc .tab-content-item.app,.tab-content.pc .tab-content-item.mp{text-align:center}.tip-button-row .tip-btn{border:none;height:30px;border-radius:3px;color:#fff;margin:0;padding:0;font-size:12px}.app-tip,.mp-tip,.share-tip{font-size:14px}.tip-button-row .tip-btn.tip-btn-primary{width:70px;background-color:#007aff}.tip-button-row .tip-btn.tip-btn-cancel{background-color:transparent;color:#007aff;width:40px}.pc-info .mask{position:absolute;right:0;width:162px;height:162px;background-color:rgba(0,0,0,.6);border-radius:3px;cursor:pointer}.pc-info .mask a{width:100%;color:#fff;line-height:160px}#download_list .btn.stream.disabled{background-color:#ccc;border-color:#ccc;color:#fff}.tab{border-bottom:1px solid #f7f7f7}.tab-item{padding:10px 18px;margin:0 30px;color:#007aff;font-size:16px}.h5-link,.mp-clip,.mp-title-copy,.website-url{text-decoration:underline}.tab-item.active{border-bottom:2px solid #007aff;font-weight:700}.tab-content{border-bottom:1px solid #f7f7f7;padding:50px 0}.pc-download{margin:0}.tab-content-item{display:none}.pkg-download,.tab-content-item.active,.website-url{display:block}.tab-content.pc .tab-content-item{height:350px}.app-qrcode{border:1px solid #279a36;border-radius:5px;-webkit-border-radius:5px;-moz-border-radius:5px;-ms-border-radius:5px;-o-border-radius:5px}.mp-qrcode{width:135px;height:135px;margin-top:20px}.list-media{position:relative;padding:11px 15px}.list-media-object{float:left;width:42px;height:42px;margin-right:15px}.list-media-body{height:42px;position:relative}.pc-share,.toast{position:absolute}.list-media-desc{overflow:hidden;white-space:nowrap;text-overflow:ellipsis;font-size:14px;color:#8f8f94;margin:0}.contact-list{padding:30px 0 0 50px}.contact-list-item{padding:5px}.contact-list-item .contact-text{color:#007aff}.tab-wrap{bottom:10px;width:100%}#mb_tab .tab-item{margin:0}.pkg-download{margin:0 auto;width:300px;background-color:#007aff;border:1px solid #007aff;color:#fff;padding:8px 10px;border-radius:5px}#mb_tab_content{border:none;padding:15px 0}@media only screen and (min-height:650px){#mb_tab_content{padding:30px 0}}@media only screen and (min-height:750px){#mb_tab_content{padding:50px 0}}#mb_tab_content .app-qrcode,#mb_tab_content .mp-qrcode{width:150px;height:150px}#mb_tab_content .tab-content-item{height:260px}.app-tip{margin:5px 0 10px;height:20px;color:#007aff}.website-label,.website-url{margin-left:50px}.mp-clip{color:#007aff}.mp-tip{color:#777}#mb_tab_content .tab-content-item.contact,#mb_tab_content .tab-content-item.h5{text-align:left}#mb_tab_content .tab-content-item.h5{padding-top:50px;font-size:20px}.footer-wrap,.mp-title-input,.pc-share,.share-qrcode{text-align:center}.website-url{color:#007aff}.pkg-download.hide{display:none}.pkg-download.show{display:block}.pc-share{top:0;right:10px;color:#007aff;cursor:pointer;background-color:#fff;width:200px;z-index:9}.share-qrcode{width:200px;height:200px;box-shadow:0 0 10px #666;border-radius:5px;-webkit-border-radius:5px;-moz-border-radius:5px;-ms-border-radius:5px;-o-border-radius:5px;display:flex;justify-content:center;align-items:center}.share-qrcode.hide{display:none}.share-qrcode img{width:100%;height:100%;border:none}.share-tip{color:#007aff}.top-arrow{width:15px;height:15px;border-top:2px solid #ccc;border-right:2px solid #ccc;transform:rotate(-45deg)}.download-app{margin-bottom:0}@media only screen and (min-height:600px){.tab-wrap{bottom:50px}.download-app{margin-bottom:50px}}@media only screen and (min-height:700px){.tab-wrap{bottom:100px}.download-app{margin-bottom:100px}}.download-app .qrcode{width:150px;height:150px}.download-app .app-tip{margin:0}.download-tip{color:#949494}.footer-wrap{padding:5px}.footer-wrap .power-by{font-size:12px;color:#aeaeae}.footer-wrap .power-by a{color:#007aff}.mp-title{margin-bottom:10px;display:flex;flex-direction:column}.mp-title-input{border:none;font-size:16px}.mp-title-input.input-hidden{visibility:hidden;position:absolute;top:0;z-index:-99}.mp-title-copy{color:#007aff;font-size:16px;margin:0 auto;margin-top:10px}.mp-title-tip{font-size:12px;color:#aeaeae}.toast{left:0;right:0;bottom:50px;margin:auto;width:320px}.toast.hide{display:none}.toast.show{display:block}.toast-text{-webkit-margin-before:0;-webkit-margin-after:0;background-color:#000;color:#fff;border-radius:3px;padding:3px 0;font-size:14px;opacity:.5}.mb-no-pkg{line-height:240px}.pc-no-pkg{line-height:350px}.h5-title{padding:0 0 20px 50px}.h5-content-pc .h5-title{display:inline-block;margin-top:50px}.h5-link{padding:20px 50px 0;color:#007aff;display:inline-block;word-break:break-all;font-size:15px}.h5-content-m .h5-title{display:block}@media screen and (max-width:360px) and (max-height:500px){.logo-wrap{top:5px}#mb_tab_content .tab-content-item{height:220px}.btn-primary,.btn-secondary{margin-top:5px}#mb_tab_content .app-qrcode{width:120px;height:120px}}.mp-wrap{display:flex}.mp-side{width:95px;left:0;border-right:1px solid #eaeaea;height:260px;padding-left:10px}.mp-content{display:flex;flex:1;justify-content:center}.mp-content-item{display:none}.mp-content-item.active{display:block}.mp-side-item{color:#727272;opacity:.45;line-height:40px;text-align:left}.mp-side-item.active{color:#0f0f0f;opacity:1}.mp-side-item.active .inner-item{background-color:#f3f3f3}.mp-icon{width:25px;height:25px;vertical-align:middle;display:inline-block}.pc-mp-item{display:inline-block;margin:0 20px}.tab-content.pc .tab-content-item.mp.active{display:flex;flex-wrap:wrap;justify-content:center}.mp-icon.mp_alipay{background-size:cover;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAMAAAANIilAAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAABXFBMVEUAAAAYkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP8YkP////+e/RfuAAAAcnRSTlMAQsv+/crtuOtpB/uPELcnJdlJGJ+eF0rwcQV38/IGcpoVUNxBFl0su7ACXqgSlPxf5pJFTO4eoSPH9WsBRuMgxRHT0G/2+XYDi8iKPNsx1AyVBH7RMIX4dfp7ONgoCpB9NYNUvAjPkzpL3kS6K0hPnSLNskahAAAAAWJLR0RzQQk9zgAAAAd0SU1FB+UIEAYIFtK0fyQAAAH5SURBVEjH7ZbXU8JAEMYXAopBECygKGBD7NgFjBUVe+8Fu9jL/f8P7ibEcXBi7uRFZ9iH5L4Nv8txt/ddAIpRjP8RFqvEfgzJajFkbcw0bEa03ZxlzGoAfxtzSWlpybeRG8B5P3OUyQBymSMvzQM7y11a1lXuFIXdFZTxeOha4RaCvZWkq6olqcZHLb+XG66tC6CS6xtIBOtDKALhWj64sYlEc4s+b63qw0gjD9xGzWi7/qKOCMpOynWZw93Y6OmN6f+gTx1F/0AP3rpN4UGAoWH9te0jtFajVC9+gEFTOA6QyKHJMQWz4xOqSADETeFJfXhsSl3k6RlNpQAmeWBldo6x+fSClpUXbSIw1tRSKkrcstrDyqoQDAtUJmvrubFvbPLDfTKprW3n11njhVM7u6hCe+pi74cJBtcBL8wOj7ZQH5+gcZxSP9pzTpixJYIyZ+fUycVJh08IZnOzipYNXOLwHVdCMGPXN5SM3moqywN/lufqHS1Zej5Xq1zliRvjnixzM4MZz5S+Rfg2hrolsw+PeFPGkjk0luXbkqyLmjRZI5+G8PRMOQ4zQBuKqOpFd62WV5J8NoT2EabKDqWDvzBADK+ftO8NrbeKWgLWS+GmCoP3OF0FTZ8Vdtww0YOuoCOW63C3G8AFfVaAxW72QWM3ZItRjD8WH2JTtkZlpAWeAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDIxLTA4LTE2VDA2OjA4OjIyKzAwOjAwoKo0bQAAACV0RVh0ZGF0ZTptb2RpZnkAMjAyMS0wOC0xNlQwNjowODoyMiswMDowMNH3jNEAAAAASUVORK5CYII=)}.mp-icon.mp_baidu{background-size:cover;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAMAAAANIilAAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAABp1BMVEUAAAAwbP8wbP8wbP8wbP8wbP8wbP8wbP8wbP8xbf+Ztv/j6//O3P9YiP8zbv9ejf9Iff+Nrv/////u8/8/d/9BeP/W4v/8/f+Ep//x9f+Tsv/M2//9/v9XiP/E1f9umP+vxv9kkf/T4P+rw/9RhP+/0f+5zf+8z/96oP+Ytv9vmP8ybf/o7/+EqP+nwP92nv+pwv+zyf97ov/d5/83cf9jkP9ShP9ym//C1P9Dev+Hqv/p7//7/P+6zv89df99o/+KrP9fjf+Rsf/D1P9AeP+ivf+yyP+at//L2v86c/+vx/9sl//P3f+nwf98ov+euv9Vhv/k7P80b/++0f82cP/e5//09//q8P/a5f+wx/9Eev+txf/S3/9Qg/9zm/+Or/+Us//s8v/m7f/0+P9Uhf9Cef9Jfv9djP+rxP9Ngf8ybv+jvv84cv/F1v+cuf9Kfv/K2v/2+f9Wh/9xmv9+o/9mk/+Bpv9GfP+Mrf/f6P/B0/9lkf/Y4/82cf9Ogf/Z5P9vmf+7z/+Dp/9ij//4+v+hvP9bi/+Vs/93nv9HfP/f6f/y9v/I2P9TEXL0AAAACHRSTlMACW3D8jTf9mXThGQAAAABYktHRBJ7vGwAAAAAB3RJTUUH5QgQBggW0rR/JAAAAhZJREFUSMft1vdf00AUAHDaQnlh+4AyFARqWGXJssqSFZShsldBScECynCjCDjY8Edz91gBPmken/yin0/fD7nLy3176eVyl6ioSETifwmH0xUNJhHtcjrC2Rg3hA13TBgbCxYRa6odbisr+ja7c6e1BXCaYJexkRIXn5AoK0nJKca8ywRfGec7iJiaBpDuwYxM45ibYKNNyhIYs0G5K4p7iYYrDJwjLebCfSrzbofzCRWAl0rP7fADQioUUlnExkpxSSn4yiQqhwrClWxcJVo/VKrFsaYW6qRN8HFxPfVVDI/83sfi9ElDY1Nac8vTVhZuI9xuHIAO+R86OVi7Mb7QRal8Bs6jls9E7Xl5t7dHpnop1cfAL6jlS4BX/bJSOAAQT6lBBh4aFg1HUqB5lAhWAIxRZZzzqCYmA91TAEV4FtNnM+U1e4bBG8QZnVBwNrlRvmJzbJxNw3vadcPb0Lzat8Cd24vv0ICxf8Hws1b4/ZJ4VsslK4DaKmKv9mH0Ixv7hM2kGsInxM+g48gXLv6K6IfKoLztc4zBb0wcQFyD73gFYxcTZ8nFy3MN/2DiGbH+KOs/9RDgxqZn6xdhPxP/lnN7kQbsDyUkXmbiv/LZ6n5VVRED4qhui/NZC3yx6Lfhjdg5v2a26F9uN7s7mja/t19/MBdaGzw80o8vV0Cz7cbWRmdri7W1udv7rLD3QROJSPxTcQKP12yINhIHYAAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMS0wOC0xNlQwNjowODoyMiswMDowMKCqNG0AAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjEtMDgtMTZUMDY6MDg6MjIrMDA6MDDR94zRAAAAAElFTkSuQmCC)}.mp-icon.mp_kuaishou{width:20px;height:20px;margin:0 5px;background-size:cover;background-image:url(data:image/png;base64,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)}.mp-icon.mp_lark{width:18px;height:18px;margin-left:2px;margin-right:5px;background-size:cover;background-image:url(data:image/png;base64,AAABAAEAICAAAAEAIACoEAAAFgAAACgAAAAgAAAAQAAAAAEAIAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/iDIw/4gy7/+IMnAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/4gyMP+IMu//iDL//4gyvwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+IMjD/iDLv/4gy//+IMv//iDL//4gyEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/iDIw/4gy7/+IMv//iDL//4gy//+IMv//iDJQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/4gyMP+IMu//iDL//4gy//+IMv//iDL//4gy//+IMp8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+IMjD/iDLv/4gy//+IMv//iDL//4gy//+IMv//iDL//4gy7wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgXQAg/YUv7/+I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)}.mp-icon.mp_qq{background-size:cover;background-image:url(data:image/png;base64,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)}.mp-icon.mp_toutiao{background-size:cover;background-image:url(data:image/png;base64,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)}.mp-icon.mp_weixin{background-size:cover;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAMAAAANIilAAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAABO1BMVEUAAABgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7BgW7D///9XT6ZOAAAAZ3RSTlMAABRXlb7k7wJPtftI1BGsLuE/8TfwF+blAbpd7tgJvEEihGkja9GgHCjyPJh4bLMy+ekMpqV8/BnQh1LrWV7W/nW9YfhfB3NGmdwwWkv3IJYklGTOCxv2sQUNysQnO9XHiJcYLxIVjX/ORAAAAAFiS0dEaMts9CIAAAAHdElNRQflCBAGBSMxqcVKAAAB3UlEQVRIx+2WaVPCMBCGKfd9CFq5FTlE8UAFRBFR8Va88L5FyP//BybShhKStiPjjM70/dCd2eSZbDebTXQ6TZr+gzip9AajyWwBFrPJaND3jSjBVpvdAXpy2G1W1bDTBUi5nOpgtwfQ5HGrgL0+QJfPqwiP+AFL/hEFOMBmARgNyMJjPJDT+JgMHAwBeYWCbDhMmR+JSv8kzIRjg0HHJ+BakwnM8zEWPDXAJoLd7UlGRU+KBadJNoPLajoiuNIMOEuyM+hEzGZySWjmRGeWDs+T8AKEFvMALC3DwhOd83R4hYQLEC5CW1qFSROdK3S4TIPXoC1Cuy46y3S4Io14o7opwLUtlLFtcaRCh/MYzcNagIF24Rxid/BYnQ7X8YRdNH/P0YX34fcgrwTjsO2IXT4UVj465k56LCtsnLBTlKBKt0ZQwho1STYYCcNbdQbrETXAcwHuE2OrcJEsctxFCdpLGswoElyeKEVXDdBEZVkgYUZ54oNhvoZU7Aal7eyWYNOKR/LuXjhMD4/kwinlZvD0/M2+vJIsuxlwVTyp9JZ7/2iCAVWHaIBxmQbItXhZlm/9vOn75Zv+cNcNx30yL7pPFVdsm3HFtn/9ch/yWYHUkTxoOioeNJo0/TV9ARmVgnq6P05YAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDIxLTA4LTE2VDA2OjA1OjM1KzAwOjAwXFmBzQAAACV0RVh0ZGF0ZTptb2RpZnkAMjAyMS0wOC0xNlQwNjowNTozNSswMDowMC0EOXEAAAAASUVORK5CYII=)}</style></head>
<body>
<div class="main">
<!--移动端-->
<div class="info mobile" id="mobile_info">
<!--logo-->
<div class="logo-wrap">
<img src="{@icon_url@}" />
<h2>{@name@}</h2>
<p>{@introduction@}</p>
</div>
<!-- 选项卡 -->
{@if hasApp || hasMP || hasH5 || hasQuickApp@}
<div class="tab-wrap">
<div class="tab" id="mb_tab">
{@if hasApp@}
<a class="tab-item app" href="javascript:;" data-id="app">App</a>
{@/if@}
{@if hasMP@}
<a class="tab-item mp" href="javascript:;" data-id="mp">小程序</a>
{@/if@}
{@if hasH5@}
<a class="tab-item h5" href="javascript:;" data-id="h5">H5</a>
{@/if@}
{@if hasQuickApp@}
<a class="tab-item quickApp" href="javascript:;" data-id="quickApp">快应用</a>
{@/if@}
</div>
<div class="tab-content mb" id="mb_tab_content">
{@if hasApp@}
<div class="tab-content-item app">
<img class="app-qrcode" style="width:150px;height:150px;" src="">
<p class="app-tip">扫码获取</p>
<a class="pkg-download hide" href="javascript:;">下载安装
<span class="pkg-size"></span>
</a>
<p class="download-tip hide"><span class="os-name">Android</span>平台尚未发布,敬请期待~</p>
</div>
{@/if@}
{@if hasMP@}
<div class="tab-content-item mp">
<div class="mp-wrap" id="mp-wrap">
<div class="mp-side">
{@each mpKeys@}
<div class="mp-side-item" data-id="{@$value@}">
<div class="inner-item">
<div class="mp-icon {@$value@}"></div>
{@mpNames[$value]@}
</div>
</div>
{@/each@}
</div>
<div class="mp-content">
{@each mpKeys@}
<div class="mp-content-item {@$value@}">
<div class="mp-title" style="display: none;">
<span class="mp-title-input">{@$data[$value].name@}</span>
<a class="mp-title-copy" data-platform="{@mpNames[$value]@}" data-name="{@$data[$value].name@}" href="javascript:;">复制</a>
<p class="mp-title-tip">复制成功后可在{@mpNames[$value]@}中搜索小程序</p>
</div>
<img class="mp-qrcode" src="{@$data[$value].qrcode_url@}" />
<p class="mp-tip hide">长按图片识别小程序</p>
</div>
{@/each@}
</div>
</div>
</div>
{@/if@}
{@if hasH5@}
<div class="tab-content-item h5" style="padding-top: 50px;">
<div class="h5-content-m">
<label class="h5-title">链接地址</label>
<a class="h5-link" href="{@h5.url@}">{@h5.url@}</a>
</div>
</div>
{@/if@}
{@if hasQuickApp@}
<div class="tab-content-item quickApp">
<img class="mp-qrcode" style="width: 150px;height: 150px;" src="{@quickapp.qrcode_url@}">
<p class="tip">快应用</p>
<p class="mp-title-tip">扫描二维码或复制名称后可在手机应用市场中搜索快应用</p>
</div>
{@/if@}
</div>
</div>
{@/if@}
<!--应用下载-->
<div class="downloads-wrap">
<div class="more down" id="scroll_page">
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAASCAYAAAAzI3woAAAA3klEQVRIS73WsQ0CIRQG4EdorJzAbXSMV5A4gZM4gQkFY+g2TmBlQzDvAniedxwPOGggBHhfoPgR4JvW+khDpdQjzPXop3UFFTXG7K21FwDYOefuvVCEEUKcAOAtpbwi4msAedTBWnvuhZpgboj4JEcE9UQtYf5APVApzCxoS9QaZhG0BSoHkwS1ROViVkEtUBxMFqgGxcVkg0pQJRgWiIMqxbBBOagaTBEoharFFIPmUMNh36CM2cT9MfxkGXezMSYGst9LqV2MqbqhgB+hoBbTBBSej/rwheDe9Hj9BxnlByK3W3UrAAAAAElFTkSuQmCC" />
</div>
</div>
</div>
<!--pc端-->
<div class="pc-info">
<div class="pc-logo">
<img src="{@icon_url@}" />
</div>
<div class="pc-share">
<p class="share-tip">获取本页二维码</p>
<div class="share-qrcode hide">
<img class="app-qrcode" alt="二维码" src="">
</div>
</div>
<div class="pc-name">
<h2>{@name@}</h2>
<p>{@introduction@}</p>
</div>
<!-- 选项卡 -->
{@if hasApp || hasMP || hasH5 || hasQuickApp@}
<div class="pc-tab-wrap">
<div class="tab" id="pc_tab">
{@if hasApp@}
<a class="tab-item app" href="javascript:;" data-id="app">App</a>
{@/if@}
{@if hasMP@}
<a class="tab-item mp" href="javascript:;" data-id="mp">小程序</a>
{@/if@}
{@if hasH5@}
<a class="tab-item h5" href="javascript:;" data-id="h5">H5</a>
{@/if@}
{@if hasQuickApp@}
<a class="tab-item quickApp" href="javascript:;" data-id="quickApp">快应用</a>
{@/if@}
</div>
<div class="tab-content pc" id="pc_tab_content">
{@if hasApp@}
<div class="tab-content-item app">
<img class="app-qrcode" style="width:256px;height:256px;" src="">
<p class="app-tip">扫码获取</p>
<div class="pc-download" id="pc_download">
<a href="javascript:;" class="btn btn-download android" type="button">
<span class="icon icon-android publish_iconfont">&#xe616;</span>
<span>Android平台下载</span>
</a>
<a href="javascript:;" class="btn btn-download ios" type="button">
<span class="icon icon-ios publish_iconfont">&#xe61f;</span>
<span>iOS平台下载</span>
</a>
</div>
</div>
{@/if@}
{@if hasMP@}
<div class="tab-content-item mp">
{@each mpKeys@}
<div class="pc-mp-item">
<img class="mp-qrcode" src="{@$data[$value].qrcode_url@}" />
<div class="mp-tip">扫二维码识别小程序</div>
<div class="mp-platform">{@mpNames[$value]@}小程序</div>
</div>
{@/each@}
</div>
{@/if@}
{@if hasH5@}
<div class="tab-content-item h5">
<div class="h5-content-pc">
<label class="h5-title">链接地址</label><br>
<a class="h5-link" href="{@h5.url@}" target="_blank">{@h5.url@}</a>
</div>
</div>
{@/if@}
{@if hasQuickApp@}
<div class="tab-content-item quickApp" style="text-align: center">
<img class="mp_qrcode" style="width: 200px;height: 200px" src="{@quickapp.qrcode_url@}">
<p class="tip">快应用</p>
<p class="mp-title-tip">扫描二维码或复制名称后可在手机应用市场中搜索快应用</p>
</div>
{@/if@}
</div>
</div>
{@/if@}
</div>
<!--应用描述-->
{@if description && description.length@}
<div class="desc-content">
<h2>应用描述</h2>
<pre>{@description@}</pre>
</div>
{@/if@}
<!--应用截图-->
{@if screenshot && screenshot.length@}
<div class="screenshots-content">
<h2>应用截图</h2>
</div>
<div class="list-wrap">
<ul>
{@each screenshot@}
<li>
<img src="{@$value@}">
</li>
{@/each@}
</ul>
</div>
{@/if@}
<div class="toast hide">
<p class="toast-text">复制成功</p>
</div>
</div>
<script type="text/javascript">
window.$app = {
appid: '{@appid@}',
android_url: '{@android_url@}',
ios_url: '{@ios_url@}',
icon_url: '{@icon_url@}',
android_size: '',
ios_size: '',
mpPlatforms: {
weixin: 'mp_weixin',
alipay: 'mp_alipay',
toutiao: 'mp_toutiao',
lark: 'mp_lark',
kuaishou: 'mp_kuaishou',
qq: 'mp_qq',
baidu: 'mp_baidu'
},
mp: {@hasMP ? 1 : 0@},
app: {@hasApp ? 1 : 0@},
h5: {@hasH5 ? 1 : 0@},
quickApp: {@hasQuickApp ? 1 : 0@},
// mp_toutiao_url: 'http://t.zijieimg.com/YdVU8V/?a=b',
mp_toutiao_url: false
};
</script>
<script type="text/javascript">!function(t){var e={};function n(r){if(e[r])return e[r].exports;var i=e[r]={i:r,l:!1,exports:{}};return t[r].call(i.exports,i,i.exports,n),i.l=!0,i.exports}n.m=t,n.c=e,n.d=function(t,e,r){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:r})},n.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var i in t)n.d(r,i,function(e){return t[e]}.bind(null,i));return r},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="",n(n.s=30)}([function(t,e){var n,r=[0,26,44,70,100,134,172,196,242,292,346,404,466,532,581,655,733,815,901,991,1085,1156,1258,1364,1474,1588,1706,1828,1921,2051,2185,2323,2465,2611,2761,2876,3034,3196,3362,3532,3706];e.getSymbolSize=function(t){if(!t)throw new Error('"version" cannot be null or undefined');if(t<1||t>40)throw new Error('"version" should be in range from 1 to 40');return 4*t+17},e.getSymbolTotalCodewords=function(t){return r[t]},e.getBCHDigit=function(t){for(var e=0;0!==t;)e++,t>>>=1;return e},e.setToSJISFunction=function(t){if("function"!=typeof t)throw new Error('"toSJISFunc" is not a valid function.');n=t},e.isKanjiModeEnabled=function(){return void 0!==n},e.toSJIS=function(t){return n(t)}},function(t,e,n){var r=n(4),i=n(5);e.NUMERIC={id:"Numeric",bit:1,ccBits:[10,12,14]},e.ALPHANUMERIC={id:"Alphanumeric",bit:2,ccBits:[9,11,13]},e.BYTE={id:"Byte",bit:4,ccBits:[8,16,16]},e.KANJI={id:"Kanji",bit:8,ccBits:[8,10,12]},e.MIXED={bit:-1},e.getCharCountIndicator=function(t,e){if(!t.ccBits)throw new Error("Invalid mode: "+t);if(!r.isValid(e))throw new Error("Invalid version: "+e);return e>=1&&e<10?t.ccBits[0]:e<27?t.ccBits[1]:t.ccBits[2]},e.getBestModeForData=function(t){return i.testNumeric(t)?e.NUMERIC:i.testAlphanumeric(t)?e.ALPHANUMERIC:i.testKanji(t)?e.KANJI:e.BYTE},e.toString=function(t){if(t&&t.id)return t.id;throw new Error("Invalid mode")},e.isValid=function(t){return t&&t.bit&&t.ccBits},e.from=function(t,n){if(e.isValid(t))return t;try{return function(t){if("string"!=typeof t)throw new Error("Param is not a string");switch(t.toLowerCase()){case"numeric":return e.NUMERIC;case"alphanumeric":return e.ALPHANUMERIC;case"kanji":return e.KANJI;case"byte":return e.BYTE;default:throw new Error("Unknown mode: "+t)}}(t)}catch(t){return n}}},function(t,e){e.L={bit:1},e.M={bit:0},e.Q={bit:3},e.H={bit:2},e.isValid=function(t){return t&&void 0!==t.bit&&t.bit>=0&&t.bit<4},e.from=function(t,n){if(e.isValid(t))return t;try{return function(t){if("string"!=typeof t)throw new Error("Param is not a string");switch(t.toLowerCase()){case"l":case"low":return e.L;case"m":case"medium":return e.M;case"q":case"quartile":return e.Q;case"h":case"high":return e.H;default:throw new Error("Unknown EC Level: "+t)}}(t)}catch(t){return n}}},function(t,e,n){var r=n(2),i=[1,1,1,1,1,1,1,1,1,1,2,2,1,2,2,4,1,2,4,4,2,4,4,4,2,4,6,5,2,4,6,6,2,5,8,8,4,5,8,8,4,5,8,11,4,8,10,11,4,9,12,16,4,9,16,16,6,10,12,18,6,10,17,16,6,11,16,19,6,13,18,21,7,14,21,25,8,16,20,25,8,17,23,25,9,17,23,34,9,18,25,30,10,20,27,32,12,21,29,35,12,23,34,37,12,25,34,40,13,26,35,42,14,28,38,45,15,29,40,48,16,31,43,51,17,33,45,54,18,35,48,57,19,37,51,60,19,38,53,63,20,40,56,66,21,43,59,70,22,45,62,74,24,47,65,77,25,49,68,81],o=[7,10,13,17,10,16,22,28,15,26,36,44,20,36,52,64,26,48,72,88,36,64,96,112,40,72,108,130,48,88,132,156,60,110,160,192,72,130,192,224,80,150,224,264,96,176,260,308,104,198,288,352,120,216,320,384,132,240,360,432,144,280,408,480,168,308,448,532,180,338,504,588,196,364,546,650,224,416,600,700,224,442,644,750,252,476,690,816,270,504,750,900,300,560,810,960,312,588,870,1050,336,644,952,1110,360,700,1020,1200,390,728,1050,1260,420,784,1140,1350,450,812,1200,1440,480,868,1290,1530,510,924,1350,1620,540,980,1440,1710,570,1036,1530,1800,570,1064,1590,1890,600,1120,1680,1980,630,1204,1770,2100,660,1260,1860,2220,720,1316,1950,2310,750,1372,2040,2430];e.getBlocksCount=function(t,e){switch(e){case r.L:return i[4*(t-1)+0];case r.M:return i[4*(t-1)+1];case r.Q:return i[4*(t-1)+2];case r.H:return i[4*(t-1)+3];default:return}},e.getTotalCodewordsCount=function(t,e){switch(e){case r.L:return o[4*(t-1)+0];case r.M:return o[4*(t-1)+1];case r.Q:return o[4*(t-1)+2];case r.H:return o[4*(t-1)+3];default:return}}},function(t,e){e.isValid=function(t){return!isNaN(t)&&t>=1&&t<=40}},function(t,e){var n="(?:[u3000-u303F]|[u3040-u309F]|[u30A0-u30FF]|[uFF00-uFFEF]|[u4E00-u9FAF]|[u2605-u2606]|[u2190-u2195]|u203B|[u2010u2015u2018u2019u2025u2026u201Cu201Du2225u2260]|[u0391-u0451]|[u00A7u00A8u00B1u00B4u00D7u00F7])+",r="(?:(?![A-Z0-9 $%*+\\-./:]|"+(n=n.replace(/u/g,"\\u"))+")(?:.|[\r\n]))+";e.KANJI=new RegExp(n,"g"),e.BYTE_KANJI=new RegExp("[^A-Z0-9 $%*+\\-./:]+","g"),e.BYTE=new RegExp(r,"g"),e.NUMERIC=new RegExp("[0-9]+","g"),e.ALPHANUMERIC=new RegExp("[A-Z $%*+\\-./:]+","g");var i=new RegExp("^"+n+"$"),o=new RegExp("^[0-9]+$"),a=new RegExp("^[A-Z0-9 $%*+\\-./:]+$");e.testKanji=function(t){return i.test(t)},e.testNumeric=function(t){return o.test(t)},e.testAlphanumeric=function(t){return a.test(t)}},function(t,e){function n(t){if("number"==typeof t&&(t=t.toString()),"string"!=typeof t)throw new Error("Color should be defined as hex string");var e=t.slice().replace("#","").split("");if(e.length<3||5===e.length||e.length>8)throw new Error("Invalid hex color: "+t);3!==e.length&&4!==e.length||(e=Array.prototype.concat.apply([],e.map((function(t){return[t,t]})))),6===e.length&&e.push("F","F");var n=parseInt(e.join(""),16);return{r:n>>24&255,g:n>>16&255,b:n>>8&255,a:255&n,hex:"#"+e.slice(0,6).join("")}}e.getOptions=function(t){t||(t={}),t.color||(t.color={});var e=void 0===t.margin||null===t.margin||t.margin<0?4:t.margin,r=t.width&&t.width>=21?t.width:void 0,i=t.scale||4;return{width:r,scale:r?4:i,margin:e,color:{dark:n(t.color.dark||"#000000ff"),light:n(t.color.light||"#ffffffff")},type:t.type,rendererOpts:t.rendererOpts||{}}},e.getScale=function(t,e){return e.width&&e.width>=t+2*e.margin?e.width/(t+2*e.margin):e.scale},e.getImageWidth=function(t,n){var r=e.getScale(t,n);return Math.floor((t+2*n.margin)*r)},e.qrToImageData=function(t,n,r){for(var i=n.modules.size,o=n.modules.data,a=e.getScale(i,r),s=Math.floor((i+2*r.margin)*a),c=r.margin*a,u=[r.color.light,r.color.dark],l=0;l<s;l++)for(var h=0;h<s;h++){var d=4*(l*s+h),f=r.color.light;if(l>=c&&h>=c&&l<s-c&&h<s-c)f=u[o[Math.floor((l-c)/a)*i+Math.floor((h-c)/a)]?1:0];t[d++]=f.r,t[d++]=f.g,t[d++]=f.b,t[d]=f.a}}},function(t,e,n){var r=n(9),i=n(10),o=n(28),a=n(29);function s(t,e,n,o,a){var s=[].slice.call(arguments,1),c=s.length,u="function"==typeof s[c-1];if(!u&&!r())throw new Error("Callback required as last argument");if(!u){if(c<1)throw new Error("Too few arguments provided");return 1===c?(n=e,e=o=void 0):2!==c||e.getContext||(o=n,n=e,e=void 0),new Promise((function(r,a){try{var s=i.create(n,o);r(t(s,e,o))}catch(t){a(t)}}))}if(c<2)throw new Error("Too few arguments provided");2===c?(a=n,n=e,e=o=void 0):3===c&&(e.getContext&&void 0===a?(a=o,o=void 0):(a=o,o=n,n=e,e=void 0));try{var l=i.create(n,o);a(null,t(l,e,o))}catch(t){a(t)}}e.create=i.create,e.toCanvas=s.bind(null,o.render),e.toDataURL=s.bind(null,o.renderToDataURL),e.toString=s.bind(null,(function(t,e,n){return a.render(t,n)}))},function(t,e){function n(t){return(n="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}if(Array.prototype.find||Object.defineProperty(Array.prototype,"find",{value:function(t){if(null==this)throw new TypeError('"this" is null or not defined');var e=Object(this),n=e.length>>>0;if("function"!=typeof t)throw new TypeError("predicate must be a function");for(var r=arguments[1],i=0;i<n;){var o=e[i];if(t.call(r,o,i,e))return o;i++}}}),Array.prototype.includes||Object.defineProperty(Array.prototype,"includes",{value:function(t,e){if(null==this)throw new TypeError('"this" is null or not defined');var n=Object(this),r=n.length>>>0;if(0===r)return!1;var i,o,a=0|e,s=Math.max(a>=0?a:r-Math.abs(a),0);for(;s<r;){if((i=n[s])===(o=t)||"number"==typeof i&&"number"==typeof o&&isNaN(i)&&isNaN(o))return!0;s++}return!1}}),!window.Promise){var r=function(t){if("function"!=typeof t)throw Error("Promise resolver undefined is not a function");var e=this;this.status=i,this.onFulfilledCbs=[],this.onRejectedCbs=[],this.resolve=function(t){e.status===i&&(e.status=o,e.value=t,e.onFulfilledCbs.forEach((function(t){return t()})),e.onFulfilledCbs.length=0,e.onRejectedCbs.length=0)},this.reject=function(t){e.status===i&&(e.status=a,e.value=t,e.onRejectedCbs.forEach((function(t){return t()})),e.onFulfilledCbs.length=0,e.onRejectedCbs.length=0)};try{t(this.resolve,this.reject)}catch(t){this.reject(t)}},i="PENDING",o="RESOLVE",a="REJECT",s=function t(e,r,i,o){if(e===r)return o(new TypeError("Chaining cycle detected for promise #<Promise>"));var a;if("object"===n(r)&&null!==r||"function"==typeof r)try{var s=r.then;if("function"==typeof s)s.call(r,(function(n){a||(a=!0,t(e,n,i,o))}),(function(t){a||(a=!0,o(t))}));else{if(a)return;a=!0,i(r)}}catch(t){if(a)return;a=!0,o(t)}else i(r)};r.prototype.then=function(t,e){var n=this;t="function"==typeof t?t:function(t){return t},e="function"==typeof e?e:function(t){throw t};var c=new r((function(r,u){n.status===o&&setTimeout((function(){try{var e=t(n.value);s(c,e,r,u)}catch(t){u(t)}}),0),n.status===a&&setTimeout((function(){try{var t=e(n.value);s(c,t,r,u)}catch(t){u(t)}}),0),n.status===i&&(n.onFulfilledCbs.push((function(){setTimeout((function(){try{var e=t(n.value);s(c,e,r,u)}catch(t){u(t)}}),0)})),n.onRejectedCbs.push((function(){setTimeout((function(){try{var t=e(n.value);s(c,t,r,u)}catch(t){u(t)}}),0)})))}));return c},r.prototype.finally=function(t){return this.then((function(e){return r.resolve(t(e)).then((function(){return e}))}),(function(e){return r.resolve(t(data)).then((function(){throw e}))}))},r.resolve=function(t){if(function(t){return("object"===n(t)&&null!==t||"function"==typeof t)&&"function"==typeof t.then}(t)){var e=new r((function(n,r){setTimeout((function(){try{s(e,t,n,r)}catch(t){r(t)}}),0)}));return e}return new r((function(e,n){e(t)}))},r.defer=r.deferred=function(){var t={};return t.promise=new r((function(e,n){t.resolve=e,t.reject=n})),t},r.all=function(t){return new r((function(e,n){for(var i=[],o=0,a=function(n,r){i[n]=r,++o===t.length&&e(i)},s=0;s<t.length;s++)!function(e){var n=t[e];r.resolve(n).then((function(t){a(e,t)}))}(s)}))},window.Promise=r}},function(t,e){t.exports=function(){return"function"==typeof Promise&&Promise.prototype&&Promise.prototype.then}},function(t,e,n){var r=n(0),i=n(2),o=n(11),a=n(12),s=n(13),c=n(14),u=n(15),l=n(3),h=n(16),d=n(19),f=n(20),p=n(1),g=n(21);function m(t,e,n){var r,i,o=t.size,a=f.getEncodedBits(e,n);for(r=0;r<15;r++)i=1==(a>>r&1),r<6?t.set(r,8,i,!0):r<8?t.set(r+1,8,i,!0):t.set(o-15+r,8,i,!0),r<8?t.set(8,o-r-1,i,!0):r<9?t.set(8,15-r-1+1,i,!0):t.set(8,15-r-1,i,!0);t.set(o-8,8,1,!0)}function v(t,e,n){var i=new o;n.forEach((function(e){i.put(e.mode.bit,4),i.put(e.getLength(),p.getCharCountIndicator(e.mode,t)),e.write(i)}));var a=8*(r.getSymbolTotalCodewords(t)-l.getTotalCodewordsCount(t,e));for(i.getLengthInBits()+4<=a&&i.put(0,4);i.getLengthInBits()%8!=0;)i.putBit(0);for(var s=(a-i.getLengthInBits())/8,c=0;c<s;c++)i.put(c%2?17:236,8);return function(t,e,n){for(var i=r.getSymbolTotalCodewords(e),o=l.getTotalCodewordsCount(e,n),a=i-o,s=l.getBlocksCount(e,n),c=s-i%s,u=Math.floor(i/s),d=Math.floor(a/s),f=d+1,p=u-d,g=new h(p),m=0,v=new Array(s),A=new Array(s),w=0,y=new Uint8Array(t.buffer),E=0;E<s;E++){var I=E<c?d:f;v[E]=y.slice(m,m+I),A[E]=g.encode(v[E]),m+=I,w=Math.max(w,I)}var b,C,N=new Uint8Array(i),M=0;for(b=0;b<w;b++)for(C=0;C<s;C++)b<v[C].length&&(N[M++]=v[C][b]);for(b=0;b<p;b++)for(C=0;C<s;C++)N[M++]=A[C][b];return N}(i,t,e)}function A(t,e,n,i){var o;if(Array.isArray(t))o=g.fromArray(t);else{if("string"!=typeof t)throw new Error("Invalid data");var l=e;if(!l){var h=g.rawSplit(t);l=d.getBestVersionForData(h,n)}o=g.fromString(t,l||40)}var f=d.getBestVersionForData(o,n);if(!f)throw new Error("The amount of data is too big to be stored in a QR Code");if(e){if(e<f)throw new Error("\nThe chosen QR Code version cannot contain this amount of data.\nMinimum version required to store current data is: "+f+".\n")}else e=f;var p=v(e,n,o),A=r.getSymbolSize(e),w=new a(A);return function(t,e){for(var n=t.size,r=c.getPositions(e),i=0;i<r.length;i++)for(var o=r[i][0],a=r[i][1],s=-1;s<=7;s++)if(!(o+s<=-1||n<=o+s))for(var u=-1;u<=7;u++)a+u<=-1||n<=a+u||(s>=0&&s<=6&&(0===u||6===u)||u>=0&&u<=6&&(0===s||6===s)||s>=2&&s<=4&&u>=2&&u<=4?t.set(o+s,a+u,!0,!0):t.set(o+s,a+u,!1,!0))}(w,e),function(t){for(var e=t.size,n=8;n<e-8;n++){var r=n%2==0;t.set(n,6,r,!0),t.set(6,n,r,!0)}}(w),function(t,e){for(var n=s.getPositions(e),r=0;r<n.length;r++)for(var i=n[r][0],o=n[r][1],a=-2;a<=2;a++)for(var c=-2;c<=2;c++)-2===a||2===a||-2===c||2===c||0===a&&0===c?t.set(i+a,o+c,!0,!0):t.set(i+a,o+c,!1,!0)}(w,e),m(w,n,0),e>=7&&function(t,e){for(var n,r,i,o=t.size,a=d.getEncodedBits(e),s=0;s<18;s++)n=Math.floor(s/3),r=s%3+o-8-3,i=1==(a>>s&1),t.set(n,r,i,!0),t.set(r,n,i,!0)}(w,e),function(t,e){for(var n=t.size,r=-1,i=n-1,o=7,a=0,s=n-1;s>0;s-=2)for(6===s&&s--;;){for(var c=0;c<2;c++)if(!t.isReserved(i,s-c)){var u=!1;a<e.length&&(u=1==(e[a]>>>o&1)),t.set(i,s-c,u),-1===--o&&(a++,o=7)}if((i+=r)<0||n<=i){i-=r,r=-r;break}}}(w,p),isNaN(i)&&(i=u.getBestMask(w,m.bind(null,w,n))),u.applyMask(i,w),m(w,n,i),{modules:w,version:e,errorCorrectionLevel:n,maskPattern:i,segments:o}}e.create=function(t,e){if(void 0===t||""===t)throw new Error("No input text");var n,o,a=i.M;return void 0!==e&&(a=i.from(e.errorCorrectionLevel,i.M),n=d.from(e.version),o=u.from(e.maskPattern),e.toSJISFunc&&r.setToSJISFunction(e.toSJISFunc)),A(t,n,a,o)}},function(t,e){function n(){this.buffer=[],this.length=0}n.prototype={get:function(t){var e=Math.floor(t/8);return 1==(this.buffer[e]>>>7-t%8&1)},put:function(t,e){for(var n=0;n<e;n++)this.putBit(1==(t>>>e-n-1&1))},getLengthInBits:function(){return this.length},putBit:function(t){var e=Math.floor(this.length/8);this.buffer.length<=e&&this.buffer.push(0),t&&(this.buffer[e]|=128>>>this.length%8),this.length++}},t.exports=n},function(t,e){function n(t){if(!t||t<1)throw new Error("BitMatrix size must be defined and greater than 0");this.size=t,this.data=new Uint8Array(t*t),this.reservedBit=new Uint8Array(t*t)}n.prototype.set=function(t,e,n,r){var i=t*this.size+e;this.data[i]=n,r&&(this.reservedBit[i]=!0)},n.prototype.get=function(t,e){return this.data[t*this.size+e]},n.prototype.xor=function(t,e,n){this.data[t*this.size+e]^=n},n.prototype.isReserved=function(t,e){return this.reservedBit[t*this.size+e]},t.exports=n},function(t,e,n){var r=n(0).getSymbolSize;e.getRowColCoords=function(t){if(1===t)return[];for(var e=Math.floor(t/7)+2,n=r(t),i=145===n?26:2*Math.ceil((n-13)/(2*e-2)),o=[n-7],a=1;a<e-1;a++)o[a]=o[a-1]-i;return o.push(6),o.reverse()},e.getPositions=function(t){for(var n=[],r=e.getRowColCoords(t),i=r.length,o=0;o<i;o++)for(var a=0;a<i;a++)0===o&&0===a||0===o&&a===i-1||o===i-1&&0===a||n.push([r[o],r[a]]);return n}},function(t,e,n){var r=n(0).getSymbolSize;e.getPositions=function(t){var e=r(t);return[[0,0],[e-7,0],[0,e-7]]}},function(t,e){e.Patterns={PATTERN000:0,PATTERN001:1,PATTERN010:2,PATTERN011:3,PATTERN100:4,PATTERN101:5,PATTERN110:6,PATTERN111:7};var n=3,r=3,i=40,o=10;function a(t,n,r){switch(t){case e.Patterns.PATTERN000:return(n+r)%2==0;case e.Patterns.PATTERN001:return n%2==0;case e.Patterns.PATTERN010:return r%3==0;case e.Patterns.PATTERN011:return(n+r)%3==0;case e.Patterns.PATTERN100:return(Math.floor(n/2)+Math.floor(r/3))%2==0;case e.Patterns.PATTERN101:return n*r%2+n*r%3==0;case e.Patterns.PATTERN110:return(n*r%2+n*r%3)%2==0;case e.Patterns.PATTERN111:return(n*r%3+(n+r)%2)%2==0;default:throw new Error("bad maskPattern:"+t)}}e.isValid=function(t){return null!=t&&""!==t&&!isNaN(t)&&t>=0&&t<=7},e.from=function(t){return e.isValid(t)?parseInt(t,10):void 0},e.getPenaltyN1=function(t){for(var e=t.size,r=0,i=0,o=0,a=null,s=null,c=0;c<e;c++){i=o=0,a=s=null;for(var u=0;u<e;u++){var l=t.get(c,u);l===a?i++:(i>=5&&(r+=n+(i-5)),a=l,i=1),(l=t.get(u,c))===s?o++:(o>=5&&(r+=n+(o-5)),s=l,o=1)}i>=5&&(r+=n+(i-5)),o>=5&&(r+=n+(o-5))}return r},e.getPenaltyN2=function(t){for(var e=t.size,n=0,i=0;i<e-1;i++)for(var o=0;o<e-1;o++){var a=t.get(i,o)+t.get(i,o+1)+t.get(i+1,o)+t.get(i+1,o+1);4!==a&&0!==a||n++}return n*r},e.getPenaltyN3=function(t){for(var e=t.size,n=0,r=0,o=0,a=0;a<e;a++){r=o=0;for(var s=0;s<e;s++)r=r<<1&2047|t.get(a,s),s>=10&&(1488===r||93===r)&&n++,o=o<<1&2047|t.get(s,a),s>=10&&(1488===o||93===o)&&n++}return n*i},e.getPenaltyN4=function(t){for(var e=0,n=t.data.length,r=0;r<n;r++)e+=t.data[r];return Math.abs(Math.ceil(100*e/n/5)-10)*o},e.applyMask=function(t,e){for(var n=e.size,r=0;r<n;r++)for(var i=0;i<n;i++)e.isReserved(i,r)||e.xor(i,r,a(t,i,r))},e.getBestMask=function(t,n){for(var r=Object.keys(e.Patterns).length,i=0,o=1/0,a=0;a<r;a++){n(a),e.applyMask(a,t);var s=e.getPenaltyN1(t)+e.getPenaltyN2(t)+e.getPenaltyN3(t)+e.getPenaltyN4(t);e.applyMask(a,t),s<o&&(o=s,i=a)}return i}},function(t,e,n){var r=n(17);function i(t){this.genPoly=void 0,this.degree=t,this.degree&&this.initialize(this.degree)}i.prototype.initialize=function(t){this.degree=t,this.genPoly=r.generateECPolynomial(this.degree)},i.prototype.encode=function(t){if(!this.genPoly)throw new Error("Encoder not initialized");var e=new Uint8Array(t.length+this.degree);e.set(t);var n=r.mod(e,this.genPoly),i=this.degree-n.length;if(i>0){var o=new Uint8Array(this.degree);return o.set(n,i),o}return n},t.exports=i},function(t,e,n){var r=n(18);e.mul=function(t,e){for(var n=new Uint8Array(t.length+e.length-1),i=0;i<t.length;i++)for(var o=0;o<e.length;o++)n[i+o]^=r.mul(t[i],e[o]);return n},e.mod=function(t,e){for(var n=new Uint8Array(t);n.length-e.length>=0;){for(var i=n[0],o=0;o<e.length;o++)n[o]^=r.mul(e[o],i);for(var a=0;a<n.length&&0===n[a];)a++;n=n.slice(a)}return n},e.generateECPolynomial=function(t){for(var n=new Uint8Array([1]),i=0;i<t;i++)n=e.mul(n,new Uint8Array([1,r.exp(i)]));return n}},function(t,e){var n=new Uint8Array(512),r=new Uint8Array(256);!function(){for(var t=1,e=0;e<255;e++)n[e]=t,r[t]=e,256&(t<<=1)&&(t^=285);for(var i=255;i<512;i++)n[i]=n[i-255]}(),e.log=function(t){if(t<1)throw new Error("log("+t+")");return r[t]},e.exp=function(t){return n[t]},e.mul=function(t,e){return 0===t||0===e?0:n[r[t]+r[e]]}},function(t,e,n){var r=n(0),i=n(3),o=n(2),a=n(1),s=n(4),c=r.getBCHDigit(7973);function u(t,e){return a.getCharCountIndicator(t,e)+4}function l(t,e){var n=0;return t.forEach((function(t){var r=u(t.mode,e);n+=r+t.getBitsLength()})),n}e.from=function(t,e){return s.isValid(t)?parseInt(t,10):e},e.getCapacity=function(t,e,n){if(!s.isValid(t))throw new Error("Invalid QR Code version");void 0===n&&(n=a.BYTE);var o=8*(r.getSymbolTotalCodewords(t)-i.getTotalCodewordsCount(t,e));if(n===a.MIXED)return o;var c=o-u(n,t);switch(n){case a.NUMERIC:return Math.floor(c/10*3);case a.ALPHANUMERIC:return Math.floor(c/11*2);case a.KANJI:return Math.floor(c/13);case a.BYTE:default:return Math.floor(c/8)}},e.getBestVersionForData=function(t,n){var r,i=o.from(n,o.M);if(Array.isArray(t)){if(t.length>1)return function(t,n){for(var r=1;r<=40;r++){if(l(t,r)<=e.getCapacity(r,n,a.MIXED))return r}}(t,i);if(0===t.length)return 1;r=t[0]}else r=t;return function(t,n,r){for(var i=1;i<=40;i++)if(n<=e.getCapacity(i,r,t))return i}(r.mode,r.getLength(),i)},e.getEncodedBits=function(t){if(!s.isValid(t)||t<7)throw new Error("Invalid QR Code version");for(var e=t<<12;r.getBCHDigit(e)-c>=0;)e^=7973<<r.getBCHDigit(e)-c;return t<<12|e}},function(t,e,n){var r=n(0),i=r.getBCHDigit(1335);e.getEncodedBits=function(t,e){for(var n=t.bit<<3|e,o=n<<10;r.getBCHDigit(o)-i>=0;)o^=1335<<r.getBCHDigit(o)-i;return 21522^(n<<10|o)}},function(t,e,n){var r=n(1),i=n(22),o=n(23),a=n(24),s=n(26),c=n(5),u=n(0),l=n(27);function h(t){return unescape(encodeURIComponent(t)).length}function d(t,e,n){for(var r,i=[];null!==(r=t.exec(n));)i.push({data:r[0],index:r.index,mode:e,length:r[0].length});return i}function f(t){var e,n,i=d(c.NUMERIC,r.NUMERIC,t),o=d(c.ALPHANUMERIC,r.ALPHANUMERIC,t);return u.isKanjiModeEnabled()?(e=d(c.BYTE,r.BYTE,t),n=d(c.KANJI,r.KANJI,t)):(e=d(c.BYTE_KANJI,r.BYTE,t),n=[]),i.concat(o,e,n).sort((function(t,e){return t.index-e.index})).map((function(t){return{data:t.data,mode:t.mode,length:t.length}}))}function p(t,e){switch(e){case r.NUMERIC:return i.getBitsLength(t);case r.ALPHANUMERIC:return o.getBitsLength(t);case r.KANJI:return s.getBitsLength(t);case r.BYTE:return a.getBitsLength(t)}}function g(t,e){var n,c=r.getBestModeForData(t);if((n=r.from(e,c))!==r.BYTE&&n.bit<c.bit)throw new Error('"'+t+'" cannot be encoded with mode '+r.toString(n)+".\n Suggested mode is: "+r.toString(c));switch(n!==r.KANJI||u.isKanjiModeEnabled()||(n=r.BYTE),n){case r.NUMERIC:return new i(t);case r.ALPHANUMERIC:return new o(t);case r.KANJI:return new s(t);case r.BYTE:return new a(t)}}e.fromArray=function(t){return t.reduce((function(t,e){return"string"==typeof e?t.push(g(e,null)):e.data&&t.push(g(e.data,e.mode)),t}),[])},e.fromString=function(t,n){for(var i=function(t,e){for(var n={},i={start:{}},o=["start"],a=0;a<t.length;a++){for(var s=t[a],c=[],u=0;u<s.length;u++){var l=s[u],h=""+a+u;c.push(h),n[h]={node:l,lastCount:0},i[h]={};for(var d=0;d<o.length;d++){var f=o[d];n[f]&&n[f].node.mode===l.mode?(i[f][h]=p(n[f].lastCount+l.length,l.mode)-p(n[f].lastCount,l.mode),n[f].lastCount+=l.length):(n[f]&&(n[f].lastCount=l.length),i[f][h]=p(l.length,l.mode)+4+r.getCharCountIndicator(l.mode,e))}}o=c}for(var g=0;g<o.length;g++)i[o[g]].end=0;return{map:i,table:n}}(function(t){for(var e=[],n=0;n<t.length;n++){var i=t[n];switch(i.mode){case r.NUMERIC:e.push([i,{data:i.data,mode:r.ALPHANUMERIC,length:i.length},{data:i.data,mode:r.BYTE,length:i.length}]);break;case r.ALPHANUMERIC:e.push([i,{data:i.data,mode:r.BYTE,length:i.length}]);break;case r.KANJI:e.push([i,{data:i.data,mode:r.BYTE,length:h(i.data)}]);break;case r.BYTE:e.push([{data:i.data,mode:r.BYTE,length:h(i.data)}])}}return e}(f(t,u.isKanjiModeEnabled())),n),o=l.find_path(i.map,"start","end"),a=[],s=1;s<o.length-1;s++)a.push(i.table[o[s]].node);return e.fromArray(function(t){return t.reduce((function(t,e){var n=t.length-1>=0?t[t.length-1]:null;return n&&n.mode===e.mode?(t[t.length-1].data+=e.data,t):(t.push(e),t)}),[])}(a))},e.rawSplit=function(t){return e.fromArray(f(t,u.isKanjiModeEnabled()))}},function(t,e,n){var r=n(1);function i(t){this.mode=r.NUMERIC,this.data=t.toString()}i.getBitsLength=function(t){return 10*Math.floor(t/3)+(t%3?t%3*3+1:0)},i.prototype.getLength=function(){return this.data.length},i.prototype.getBitsLength=function(){return i.getBitsLength(this.data.length)},i.prototype.write=function(t){var e,n,r;for(e=0;e+3<=this.data.length;e+=3)n=this.data.substr(e,3),r=parseInt(n,10),t.put(r,10);var i=this.data.length-e;i>0&&(n=this.data.substr(e),r=parseInt(n,10),t.put(r,3*i+1))},t.exports=i},function(t,e,n){var r=n(1),i=["0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"," ","$","%","*","+","-",".","/",":"];function o(t){this.mode=r.ALPHANUMERIC,this.data=t}o.getBitsLength=function(t){return 11*Math.floor(t/2)+t%2*6},o.prototype.getLength=function(){return this.data.length},o.prototype.getBitsLength=function(){return o.getBitsLength(this.data.length)},o.prototype.write=function(t){var e;for(e=0;e+2<=this.data.length;e+=2){var n=45*i.indexOf(this.data[e]);n+=i.indexOf(this.data[e+1]),t.put(n,11)}this.data.length%2&&t.put(i.indexOf(this.data[e]),6)},t.exports=o},function(t,e,n){var r=n(25),i=n(1);function o(t){this.mode=i.BYTE,this.data=new Uint8Array(r(t))}o.getBitsLength=function(t){return 8*t},o.prototype.getLength=function(){return this.data.length},o.prototype.getBitsLength=function(){return o.getBitsLength(this.data.length)},o.prototype.write=function(t){for(var e=0,n=this.data.length;e<n;e++)t.put(this.data[e],8)},t.exports=o},function(t,e,n){"use strict";t.exports=function(t){for(var e=[],n=t.length,r=0;r<n;r++){var i=t.charCodeAt(r);if(i>=55296&&i<=56319&&n>r+1){var o=t.charCodeAt(r+1);o>=56320&&o<=57343&&(i=1024*(i-55296)+o-56320+65536,r+=1)}i<128?e.push(i):i<2048?(e.push(i>>6|192),e.push(63&i|128)):i<55296||i>=57344&&i<65536?(e.push(i>>12|224),e.push(i>>6&63|128),e.push(63&i|128)):i>=65536&&i<=1114111?(e.push(i>>18|240),e.push(i>>12&63|128),e.push(i>>6&63|128),e.push(63&i|128)):e.push(239,191,189)}return new Uint8Array(e).buffer}},function(t,e,n){var r=n(1),i=n(0);function o(t){this.mode=r.KANJI,this.data=t}o.getBitsLength=function(t){return 13*t},o.prototype.getLength=function(){return this.data.length},o.prototype.getBitsLength=function(){return o.getBitsLength(this.data.length)},o.prototype.write=function(t){var e;for(e=0;e<this.data.length;e++){var n=i.toSJIS(this.data[e]);if(n>=33088&&n<=40956)n-=33088;else{if(!(n>=57408&&n<=60351))throw new Error("Invalid SJIS character: "+this.data[e]+"\nMake sure your charset is UTF-8");n-=49472}n=192*(n>>>8&255)+(255&n),t.put(n,13)}},t.exports=o},function(t,e,n){"use strict";var r={single_source_shortest_paths:function(t,e,n){var i={},o={};o[e]=0;var a,s,c,u,l,h,d,f=r.PriorityQueue.make();for(f.push(e,0);!f.empty();)for(c in s=(a=f.pop()).value,u=a.cost,l=t[s]||{})l.hasOwnProperty(c)&&(h=u+l[c],d=o[c],(void 0===o[c]||d>h)&&(o[c]=h,f.push(c,h),i[c]=s));if(void 0!==n&&void 0===o[n]){var p=["Could not find a path from ",e," to ",n,"."].join("");throw new Error(p)}return i},extract_shortest_path_from_predecessor_list:function(t,e){for(var n=[],r=e;r;)n.push(r),t[r],r=t[r];return n.reverse(),n},find_path:function(t,e,n){var i=r.single_source_shortest_paths(t,e,n);return r.extract_shortest_path_from_predecessor_list(i,n)},PriorityQueue:{make:function(t){var e,n=r.PriorityQueue,i={};for(e in t=t||{},n)n.hasOwnProperty(e)&&(i[e]=n[e]);return i.queue=[],i.sorter=t.sorter||n.default_sorter,i},default_sorter:function(t,e){return t.cost-e.cost},push:function(t,e){var n={value:t,cost:e};this.queue.push(n),this.queue.sort(this.sorter)},pop:function(){return this.queue.shift()},empty:function(){return 0===this.queue.length}}};t.exports=r},function(t,e,n){var r=n(6);e.render=function(t,e,n){var i=n,o=e;void 0!==i||e&&e.getContext||(i=e,e=void 0),e||(o=function(){try{return document.createElement("canvas")}catch(t){throw new Error("You need to specify a canvas element")}}()),i=r.getOptions(i);var a=r.getImageWidth(t.modules.size,i),s=o.getContext("2d"),c=s.createImageData(a,a);return r.qrToImageData(c.data,t,i),function(t,e,n){t.clearRect(0,0,e.width,e.height),e.style||(e.style={}),e.height=n,e.width=n,e.style.height=n+"px",e.style.width=n+"px"}(s,o,a),s.putImageData(c,0,0),o},e.renderToDataURL=function(t,n,r){var i=r;void 0!==i||n&&n.getContext||(i=n,n=void 0),i||(i={});var o=e.render(t,n,i),a=i.type||"image/png",s=i.rendererOpts||{};return o.toDataURL(a,s.quality)}},function(t,e,n){var r=n(6);function i(t,e){var n=t.a/255,r=e+'="'+t.hex+'"';return n<1?r+" "+e+'-opacity="'+n.toFixed(2).slice(1)+'"':r}function o(t,e,n){var r=t+e;return void 0!==n&&(r+=" "+n),r}e.render=function(t,e,n){var a=r.getOptions(e),s=t.modules.size,c=t.modules.data,u=s+2*a.margin,l=a.color.light.a?"<path "+i(a.color.light,"fill")+' d="M0 0h'+u+"v"+u+'H0z"/>':"",h="<path "+i(a.color.dark,"stroke")+' d="'+function(t,e,n){for(var r="",i=0,a=!1,s=0,c=0;c<t.length;c++){var u=Math.floor(c%e),l=Math.floor(c/e);u||a||(a=!0),t[c]?(s++,c>0&&u>0&&t[c-1]||(r+=a?o("M",u+n,.5+l+n):o("m",i,0),i=0,a=!1),u+1<e&&t[c+1]||(r+=o("h",s),s=0)):i++}return r}(c,s,a.margin)+'"/>',d='viewBox="0 0 '+u+" "+u+'"',f='<svg xmlns="http://www.w3.org/2000/svg" '+(a.width?'width="'+a.width+'" height="'+a.width+'" ':"")+d+' shape-rendering="crispEdges">'+l+h+"</svg>\n";return"function"==typeof n&&n(null,f),f}},function(t,e,n){"use strict";n.r(e);n(8);var r={};!function(t,e,n){var r=n.createElement("style");r.innerText='.wx-share-guide-mask {\tposition: absolute;\tz-index: 900000;\twidth: 100%;\theight: 100%;\tleft: 0px;\ttop: 0px;\tbackground-color: rgba(0, 0, 0, 0.75);\tdisplay: none;\toverflow: hidden;\tbox-sizing: border-box;}.wx-share-guide-image {\tbackground-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAaMAAAEBCAYAAADVQcoRAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDY3IDc5LjE1Nzc0NywgMjAxNS8wMy8zMC0yMzo0MDo0MiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NUNDMEVBRjkyNDg3MTFFNTgxNzBEM0VFQzVCQUUyMzEiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NUNDMEVBRkEyNDg3MTFFNTgxNzBEM0VFQzVCQUUyMzEiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo1Q0MwRUFGNzI0ODcxMUU1ODE3MEQzRUVDNUJBRTIzMSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo1Q0MwRUFGODI0ODcxMUU1ODE3MEQzRUVDNUJBRTIzMSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pj+f/SIAABkPSURBVHja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");\tbackground-position: center top;\tbackground-repeat: no-repeat;\tbackground-size: 100%;\tposition: absolute;\theight: 300px;\twidth: 280px;\tright: 28px;\ttop: 5px;\tbox-sizing: border-box;\tz-index: 900001;}.wx-share-guide-text {\tposition: absolute;\tcolor: #eee;\tz-index: 900002;\twidth: 260px;\tright: 10px;\ttop: 180px;\tbox-sizing: border-box;\tfont-size:15px;}.wx-share-guide-mask.active {\tdisplay: block;}',(n.head||n.body).appendChild(r),t.show=function(e){t.mask||(t.mask=n.createElement("div"),t.mask.classList.add("wx-share-guide-mask"),n.body.appendChild(t.mask),t.image=n.createElement("div"),t.image.classList.add("wx-share-guide-image"),t.mask.appendChild(t.image),t.text=n.createElement("div"),t.text.classList.add("wx-share-guide-text"),t.mask.appendChild(t.text),t.mask.addEventListener("click",(function(){t.hide()}))),e&&(t.text.innerHTML=e),t.mask.classList.add("active")},t.hide=function(){t.mask&&t.mask.classList.remove("active")}}(r,window,document);var i=n(7);var o,a=(o=n.n(i).a.toCanvas,function(){var t=Array.prototype.slice.call(arguments);return new Promise((function(e,n){t.push((function(t,r){t?n(t):e(r)})),o.apply(null,t)}))}),s=function(t){var e=t.canvas,n=t.content,r=t.width,i=void 0===r?0:r,o=t.nodeQrCodeOptions,s=void 0===o?{}:o;return s.errorCorrectionLevel=s.errorCorrectionLevel||u(n),c(n,s).then((function(t){return s.scale=0===i?void 0:i/t*4,a(e,n,s)}))},c=function(t,e){var n=document.createElement("canvas");return a(n,t,e).then((function(){return n.width}))},u=function(t){return t.length>36?"M":t.length>16?"Q":"H"},l=function(t){var e=t.canvas,n=(t.content,t.logo);if(n){var r=e.width,i=n.logoSize,o=void 0===i?.15:i,a=n.borderColor,s=n.bgColor,c=void 0===s?a||"#ffffff":s,u=n.borderSize,l=void 0===u?.05:u,d=n.crossOrigin,f=n.borderRadius,p=void 0===f?8:f,g=n.logoRadius,m=void 0===g?0:g,v="string"==typeof n?n:n.src,A=r*o,w=r*(1-o)/2,y=r*(o+l),E=r*(1-o-l)/2,I=e.getContext("2d");h(I)(E,E,y,y,p),I.fillStyle=c,I.fill();var b=new Image;(d||m)&&b.setAttribute("crossOrigin",d||"anonymous"),b.src=v;return new Promise((function(t,e){b.onload=function(){m?function(t){var e=document.createElement("canvas");e.width=w+A,e.height=w+A,e.getContext("2d").drawImage(t,w,w,A,A),h(I)(w,w,A,A,m),I.fillStyle=I.createPattern(e,"no-repeat"),I.fill()}(b):function(t){I.drawImage(t,w,w,A,A)}(b),t()}}))}},h=function(t){return function(e,n,r,i,o){var a=Math.min(r,i);return o>a/2&&(o=a/2),t.beginPath(),t.moveTo(e+o,n),t.arcTo(e+r,n,e+r,n+i,o),t.arcTo(e+r,n+i,e,n+i,o),t.arcTo(e,n+i,e,n,o),t.arcTo(e,n,e+r,n,o),t.closePath(),t}},d=function(t){return s(t).then((function(){return t})).then(l)},f=function(t,e){var n=t.src,r=document.createElement("a");r.download=e,r.href=n,document.body.appendChild(r),r.click(),document.body.removeChild(r)},p={toCanvas:d,toImage:function(t){var e=document.createElement("canvas");return t.canvas=e,t.logo&&("string"==typeof t.logo&&(t.logo={src:t.logo}),t.logo.crossOrigin="Anonymous"),d(t).then((function(){var n=t.image,r=void 0===n?new Image:n,i=t.downloadName,o=void 0===i?"qr-code":i,a=t.download;if(r.src=e.toDataURL(),!0===a||function(t){return"function"==typeof t}(a)){(a=!0===a?function(t){return t()}:a)((function(){f(r,o)}))}}))}};!function(){var t=Array.prototype.find,e=Array.prototype.includes,n=function(t){var e=[];if(t)for(var n in t)t.hasOwnProperty(n)&&e.push(encodeURIComponent(n)+"="+encodeURIComponent(t[n]));return e.length?e.join("&"):""},i=function(t,e,r,i){var o=new window.XMLHttpRequest;o.onreadystatechange=function(){if(4==o.readyState)if(o.status>=200&&o.status<300||304===o.status||0===o.status&&o.responseText){if(i){var t=!1;try{t=JSON.parse(o.responseText)}catch(e){t=!1}}i&&i(t,o)}else i&&i(!1)};var a="string"==typeof r?r:n(r);return a&&"GET"===t&&(e=function(t,e){return"string"!=typeof e&&(e=n(e)),(t+"&"+e).replace(/[&?]{1,2}/,"?")}(e,a)),o.open(t,e),"POST"===t&&"string"!=typeof r&&o.setRequestHeader("Content-Type","application/x-www-form-urlencoded"),o.send("POST"===t&&a),o};function o(t){var e,n=t.data,r=t.success,i=t.fail,o=t.complete;if(document.execCommand){var a=document.getElementById("#clipboard");a&&a.remove();var s=document.createElement("textarea");s.id="#clipboard",s.style.position="absolute",s.style.top="-9999px",s.style.zIndex="-9999",document.body.appendChild(s),s.value=n,s.focus(),s.select(),e=document.execCommand("Copy",!1,null),s.blur()}e?r&&r():i&&i(),o&&o()}Date.now;var a=function(){return document.documentElement?document.documentElement.clientHeight:window.innerHeight},s={isVisible:function(t){return"string"==typeof t&&(t=this.querySelector(t)),"none"!==t.style.display},toggle:function(t){"string"==typeof t&&(t=this.querySelector(t));var e=t.className;~e.indexOf("show")?t.className=e.replace("show","hide"):~e.indexOf("hide")&&(t.className=e.replace("hide","show"))},querySelector:function(t){var e=null;if(document.querySelector)e=document.querySelector(t);else{for(var n=t.split(" "),r=document,i=0,o=n.length;i<o;i++)r=this.handleQuery(n[i],r);e=r}return e},handleQuery:function(t,e){var n=null;return/^#([\w-]+)$/.test(t)?n=e.getElementById(t.substring(1)):/^\.([\w-]+)$/.test(t)?n=e.getElementsByClassName(t.substring(1))[0]:/^[\w-]+$/.test(t)&&(n=e.getElementsByTagName(t)[0]),n},setFullScreen:function(t){"string"==typeof t&&(t=this.querySelector(t)),t.style.height=a()+"px"}},c={android:!1,ios:!1,mobile:!1,pc:!1,init:function(){var t=navigator.userAgent;t.match(/(Android);?[\s\/]+([\d+.]+)?/)?(this.android=!0,this.mobile=!0):t.match(/(iPhone\sOS)\s([\d_]+)/)?(this.ios=!0,this.mobile=!0):this.pc=!0}};c.init();var u={uc:!1,qihoo:!1,chrome:!1,safari:!1,weixin:!1,weibo:!1,qq:!1,meizu:!1,alipay:!1,baidu:!1,version:0,init:function(){var t=navigator.userAgent;if(c.android&&(t.indexOf("360")>0&&(this.qihoo=!0),t.indexOf("UCBrowser")>0&&(this.uc=!0),t.indexOf("MZ-")>0&&(this.meizu=!0),t.indexOf("Chrome")>0&&window.chrome)){this.chrome=!0;var e=t.match(/chrome\/(\d+)/i);e&&(this.version=e[1]-0)}~t.indexOf("MicroMessenger")&&(this.weixin=!0),~t.indexOf("Weibo")&&(this.weibo=!0),(c.ios&&/ QQ/i.test(navigator.userAgent)||c.android&&/MQQBrowser/i.test(navigator.userAgent)&&/QQ/i.test(navigator.userAgent.split("MQQBrowser")))&&(this.qq=!0),c.ios&&t.match(/safari\/[\d.]+/i)&&(this.safari=!0,this.version=t.match(/os\s+(\d+)/i)[1]-0),~t.indexOf("AlipayClient")&&(this.alipay=!0),~t.indexOf("baiduboxapp")&&(this.baidu=!0)}};u.init();var l,h={appid:"",scheme:"",tempLinkSelector:"",packageUrl:"",packageInfoService:"http://127.0.0.1:13137/?action=info",init:function(t){this.appid=t.appid,this.scheme=t.stream_scheme,this.tempLinkSelector=t.tempLinkSelector||"open_stream_temp",c.android?this.packageUrl=t.stream_android_url:c.ios&&(this.packageUrl=t.stream_ios_url)},open:function(){if(!this.scheme)return!1;c.android?this.openAndroid():c.ios&&this.openIos()},openAndroid:function(){navigator.userAgent;var t=s.querySelector(this.tempLinkSelector);if(t&&document.body.removeChild(t),u.qihoo||u.uc||u.meizu)window.location.href=this.scheme;else if(u.chrome)u.version&&u.version<42?this.openIntent(this.scheme):window.location.href=this.scheme;else{var e=document.createElement("iframe");e.id=this.tempLinkSelector,e.style.display="none",document.body.appendChild(e),e.src=this.scheme}},openIos:function(){window.location.href=this.scheme},openIntent:function(t){var e=t.split("://"),n="intent://{schemePath}#Intent;scheme={schemeName};end";n=n.replace("{schemePath}",e[1]).replace("{schemeName}",e[0]);var r=document.createElement("a");r.id=this.tempLinkSelector,r.style.width="2px",r.style.height="2px",r.href=n,document.body.appendChild(r),function(t,e){if(t.fireEvent)t.fireEvent("on"+e);else{var n=document.createEvent("Events");n.initEvent(e,!0,!1),t.dispatchEvent(n)}}(r,"click")},download:function(){window.location.href=this.packageUrl},getPackageInfo:function(t){i("GET",this.packageInfoService,{},(function(e){e?t.success&&t.success():t.error&&t.error()}))}};h.init($app),{url:"",size:0,download:function(){if(u.weixin&&!~this.url.indexOf("sj.qq.com")){var t=c.ios?"在Safari打开":"在浏览器打开";r.show("请点击右上角菜单,选择"+t)}else window.location.href=this.url},init:function(t){c.android?(this.url=t.android_url||"",this.size=t.android_size||0):c.ios&&(this.url=t.ios_url||"",this.size=t.ios_size||0)}}.init($app),l=function(){c.android&&$app.stream_scheme&&h.open(),{selector:"#mobile_info",element:null,eventType:"resize",handleEvent:function(){s.setFullScreen(this.element),this.removeEvent()},initEvent:function(){window.addEventListener(this.eventType,this.handleEvent.bind(this),!1)},removeEvent:function(){window.removeEventListener(this.eventType,this.handleEvent)},init:function(){this.element=s.querySelector(this.selector),s.setFullScreen(this.element),this.initEvent()}}.init(),["mp","app","h5","quickApp"].forEach((function(t){$app[t]||(l(document.querySelector("#mb_tab ."+t),"hide"),l(document.querySelector("#mb_tab_content ."+t),"hide"),l(document.querySelector("#pc_tab ."+t),"hide"),l(document.querySelector("#pc_tab_content ."+t),"hide"))})),{selector:"#scroll_page",element:null,eventType:"click",direction:"down",DOWN_SIGN:"down",UP_SIGN:"up",toggle:function(){var t=this.element.className;this.direction===this.DOWN_SIGN?(this.direction=this.UP_SIGN,this.element.className=t.replace(this.DOWN_SIGN,this.UP_SIGN)):(this.direction=this.DOWN_SIGN,this.element.className=t.replace(this.UP_SIGN,this.DOWN_SIGN))},initEvent:function(){this.element.addEventListener(this.eventType,this.handleEvent.bind(this),!1),window.addEventListener("scroll",this.onScroll.bind(this),!1)},handleEvent:function(){this.direction===this.DOWN_SIGN?this.down():this.direction===this.UP_SIGN&&this.up()},onScroll:function(){var t=a()-a()/2,e=window.scrollY;(e>t&&this.direction!==this.UP_SIGN||e<=t&&this.direction!==this.DOWN_SIGN)&&this.toggle()},down:function(){window.scrollTo(0,a()+1),this.toggle()},up:function(){window.scrollTo(0,0),this.toggle()},init:function(){this.element=s.querySelector(this.selector),this.initEvent()}}.init();var n=document.querySelector(".pc-share"),i=n.querySelector(".share-qrcode");function l(t,e){t&&(t.className+=" "+e)}function d(t,e){t&&(e instanceof Array?e:e.split(" ")).forEach((function(e){t.className=t.className.replace(e," ")}))}n.addEventListener("mouseover",(function(){~i.className.indexOf("hide")&&(i.className=i.className.replace("hide","show"))})),n.addEventListener("mouseleave",(function(){~i.className.indexOf("show")&&(i.className=i.className.replace("show","hide"))}));var f=function(t){this.options=t,this.container=t.container,this.itemSelector=t.itemSelector,this.activeClass="active",this.itemTagName=(t.itemTagName||"a").toUpperCase(),this.itemClassName=t.itemClassName,this.init()};f.prototype.init=function(){this.initElements()},f.prototype.initElements=function(){this.containerElem=document.querySelector(this.container),this.containerElem&&(this.activeItem=this.containerElem.querySelector(this.itemSelector+"."+this.activeClass),this.activeItem||(this.activeItem=t.call(this.containerElem.querySelectorAll(this.itemSelector),(function(t){return!e.call(t.classList,"hide")})),l(this.activeItem,"active")),this.initEvents())},f.prototype.initEvents=function(){this.containerElem.addEventListener("click",this.handleTab.bind(this),!1)},f.prototype.handleTab=function(t){for(var e=t.target;e!==document;e=e.parentNode)if(~e.className.indexOf(this.itemSelector.replace(".",""))){if(e.tagName===this.itemTagName){if(e===this.activeItem)return;this.switchTab(e)}break}},f.prototype.switchTab=function(t){this.activeItem.className=this.activeItem.className.replace(" "+this.activeClass,""),t.className=t.className+" "+this.activeClass,this.activeItem=t,this.options.callback&&this.options.callback(t.getAttribute("data-id"))};var g=function(t){this.options=t,this.container=t.container,this.itemSelector=t.itemSelector,this.activeClass="active",this.init()};g.prototype.init=function(){this.initElements()},g.prototype.initElements=function(){this.containerElem=document.querySelector(this.container),this.containerElem&&(this.activeItem=this.containerElem.querySelector(this.itemSelector+"."+this.activeClass),this.activeItem||(this.activeItem=t.call(this.containerElem.querySelectorAll(this.itemSelector),(function(t){return!e.call(t.classList,"hide")})),l(this.activeItem,"active")))},g.prototype.change=function(t){var e=this.containerElem.querySelector(this.itemSelector+"."+t);e!==this.activeItem&&(this.activeItem.className=this.activeItem.className.replace(" "+this.activeClass,""),e.className=e.className+" "+this.activeClass,this.activeItem=e)};var m=new g({container:"#pc_tab_content",itemSelector:".tab-content-item"});new f({container:"#pc_tab",itemSelector:".tab-item",callback:function(t){m.change(t)}});var v=new g({container:"#mb_tab_content",itemSelector:".tab-content-item"});new f({container:"#mb_tab",itemSelector:".tab-item",callback:function(t){v.change(t)}});var A=new g({container:".mp-content",itemSelector:".mp-content-item"});new f({container:".mp-side",itemSelector:".mp-side-item",itemTagName:"div",callback:function(t){A.change(t)}});var w=document.getElementById("mb_tab_content");if(w){var y=w.querySelector(".pkg-download"),E=w.querySelector(".download-tip");E&&(c.android&&($app.android_url?y.className=y.className.replace("hide","show"):E.querySelector(".os-name")&&(E.querySelector(".os-name").innerText="Android",E.className=E.className.replace("hide","show"))),c.ios&&($app.ios_url?y.className=y.className.replace("hide","show"):E.querySelector(".os-name")&&(E.querySelector(".os-name").innerText="iOS",E.className=E.className.replace("hide","show")))),y&&y.addEventListener("click",(function(){if(u.weixin){var t=c.ios?"在Safari打开":"在浏览器打开";r.show("请点击右上角菜单,选择"+t)}else c.android?window.location.href=$app.android_url:c.ios&&(window.location.href=$app.ios_url)}))}var I,b,C,N,M,T=document.getElementById("pc_tab_content");if(T){var S=T.querySelector(".pc-download");if(S){var B=S.querySelector("a.android");B&&($app.android_url?B.href=$app.android_url:l(B,"hide"));var R=S.querySelector("a.ios");R&&($app.ios_url?R.href=$app.ios_url:l(R,"hide"))}}$app.mp&&function(){var t=function(t,e){return t.getAttribute("data-"+e)};if(I=document.querySelectorAll(".mp-content .mp-content-item"))for(M=0;M<I.length;M++)!function(t){var e=!1;for(var n in $app.mpPlatforms)if(Object.hasOwnProperty.call($app.mpPlatforms,n)){var r=$app.mpPlatforms[n];u[n]&&(e=!0,~t.className.indexOf(r)?d(t.querySelector(".mp-tip"),"hide"):t.querySelector(".mp-title").style.display="flex")}e||(t.querySelector(".mp-title").style.display="flex")}(I[M]);if((b=document.getElementById("mp-toutiao-guide"))&&((u.chrome||u.safari)&&$app.mp_toutiao_url?b.innerHTML='<a href="'+$app.mp_toutiao_url+'">前往打开头条小程序</a>':b.innerHTML="通过Safari或Chrome打开头条体验"),C=document.querySelectorAll(".mp-title"),N=document.querySelector(".toast"),C)for(M=0;M<C.length;M++)!function(e){var n=e.querySelector(".mp-title-copy");n&&n.addEventListener("click",(function(){o({data:t(n,"name"),success:function(){N.querySelector(".toast-text").innerText="复制成功,请在%platform%搜索框里长按并粘贴搜索。".replace("%platform%",t(n,"platform")),d(N,"hide"),setTimeout((function(){l(N,"hide"),N.querySelector(".toast-text").innerText=""}),2e3)},fail:function(){N.querySelector(".toast-text").innerText="复制失败,请尝试长按小程序名称复制。",d(N,"hide"),setTimeout((function(){l(N,"hide"),N.querySelector(".toast-text").innerText=""}),2e3)},complete:function(){var t;!(t=document.activeElement)||"TEXTAREA"!==t.tagName&&"INPUT"!==t.tagName||t.blur()}})}))}(C[M])}();var U=document.createElement("img");U.style.position="absolute",U.style.left="100000px",U.style.width="0px",U.style.height="0px",document.body.appendChild(U);var q={image:U,content:location.href,width:256};p.toImage(q).then((function(){Array.prototype.forEach.call(document.querySelectorAll(".app-qrcode"),(function(t){t.src=U.src}))}))},/complete|loaded|interactive/.test(document.readyState)?l():document.addEventListener("DOMContentLoaded",l,!1)}()}]);</script></body>
</html>
\ No newline at end of file
'use strict';
const success = {
success: true
}
const fail = {
success: false
}
const createPublishHtml = require('./createPublishHtml')
exports.main = async (event, context) => {
//event为客户端上传的参数
console.log('event : ', event)
let res = {};
let params = event.data || event.params;
switch (event.action) {
case 'createPublishHtml':
res = createPublishHtml(params.id)
break;
}
//返回数据给客户端
return res
};
{
"name": "uni-portal",
"dependencies": {},
"extensions": {
"uni-cloud-jql": {}
}
}
\ No newline at end of file
module.exports = (templateData, user) => {
const data = {}
for (const template of templateData) {
const isDynamic = /\{.*?\}/.test(template.value)
// 仅支持uni-id-users
if (isDynamic) {
const [collection, field] = template.value.replace(/\{|\}/g, '').split('.')
data[template.field] = collection === 'uni-id-users' ? user[field] || template.value: template.value
} else {
data[template.field] = template.value
}
// switch (template.type) {
// case 'static':
// data[template.field] = template.value
// break
// case 'dynamic':
// data[template.field] = user[template.value] || ''
// break
// default:
// throw new Error(`template type [${template.type}] not supported`)
// }
}
return data
}
// 云对象教程: https://uniapp.dcloud.net.cn/uniCloud/cloud-obj
// jsdoc语法提示教程:https://ask.dcloud.net.cn/docs/#//ask.dcloud.net.cn/article/129
const createConfig = require('uni-config-center')
const buildTemplateData = require('./build-template-data')
const { parserDynamicField, checkIsStaticTemplate } = require('./utils')
const schemaNameAdapter = require('./schema-name-adapter')
const uniSmsCo = uniCloud.importObject('uni-sms-co')
const db = uniCloud.database()
const smsConfig = createConfig({
pluginId: 'uni-sms-co'
}).config()
function errCode(code) {
return 'uni-sms-co-' + code
}
const tableNames = {
template: 'opendb-sms-template',
task: 'opendb-sms-task',
log: 'opendb-sms-log'
}
module.exports = {
_before: async function () { // 通用预处理器
if (!smsConfig.smsKey || smsConfig.smsKey.length <= 20 || !smsConfig.smsSecret || smsConfig.smsSecret.length <= 20) {
throw new Error('请先配置smsKey和smsSecret')
}
this.tableNames = tableNames
/**
* 优化 schema 的命名规范,需要兼容 uni-admin@2.1.6 以下版本
* 如果是在uni-admin@2.1.6版本以上创建的项目可以将其注释
* */
await schemaNameAdapter.call(this)
},
_after: function (error, result) {
if (error) {
if (error instanceof Error) {
return {
errCode: 'error',
errMsg: error.message
}
}
if (error.errCode) {
return error
}
throw error
}
return result
},
/**
* 创建短信任务
* @param {Object} to
* @param {Boolean} to.all=false 全部用户发送
* @param {String} to.type=user to.all=true时用来区分发送类型
* @param {Array} to.receiver 用户ID's / 用户标签ID's
* @param {String} templateId 短信模板ID
* @param {Array} templateData 短信模板数据
* @param {String} options.taskName 任务名称
*/
async createSmsTask(to, templateId, templateData, options = {}) {
if (!templateId) {
return {
errCode: errCode('template-id-required'),
errMsg: '缺少templateId'
}
}
if (!to.all && (!to.receiver || to.receiver.length <= 0)) {
return {
errCode: errCode('send-users-is-null'),
errMsg: '请选择要发送的用户'
}
}
const clientInfo = this.getClientInfo()
const {data: templates} = await db.collection(this.tableNames.template).where({_id: templateId}).get()
if (templates.length <= 0) {
return {
errCode: errCode('template-not-found'),
errMsg: '短信模板不存在'
}
}
const [template] = templates
// 创建短信任务
const task = await db.collection(this.tableNames.task).add({
app_id: clientInfo.appId,
name: options.taskName,
template_id: templateId,
template_contnet: template.content,
vars: templateData,
to,
send_qty: 0,
success_qty: 0,
fail_qty: 0,
create_date: Date.now()
})
uniSmsCo.createUserSmsMessage(task.id)
return new Promise(resolve => setTimeout(() => resolve({
errCode: 0,
errMsg: '任务创建成功',
taskId: task.id
}), 300))
},
async createUserSmsMessage(taskId, execData = {}) {
const parallel = 100
let beforeId
const { data: tasks } = await db.collection(this.tableNames.task).where({ _id: taskId }).get()
if (tasks.length <= 0) {
return {
errCode: errCode('task-id-not-found'),
errMsg: '任务ID不存在'
}
}
const [task] = tasks
const query = {
mobile: db.command.exists(true)
}
// 指定用户发送
if (!task.to.all && task.to.type === 'user') {
let index = 0
if (execData.beforeId) {
const i = task.to.receiver.findIndex(id => id === execData.beforeId)
index = i !== -1 ? i + 1 : 0
}
const receiver = task.to.receiver.slice(index, index + parallel)
query._id = db.command.in(receiver)
beforeId = receiver[receiver.length - 1]
}
// 指定用户标签
if (task.to.type === 'userTags') {
query.tags = db.command.in(task.to.receiver)
}
// 全部用户
if (task.to.all && execData.beforeId) {
query._id = db.command.gt(execData.beforeId)
}
// 动态数据仅支持uni-id-users表字段
const dynamicField = parserDynamicField(task.vars)
const userFields = dynamicField['uni-id-users'] ? dynamicField['uni-id-users'].reduce((res, field) => {
res[field] = true
return res
}, {}): {}
const { data: users } = await db.collection('uni-id-users')
.where(query)
.field({
mobile: true,
...userFields
})
.limit(parallel)
.orderBy('_id', 'asc')
.get()
if (users.length <= 0) {
// 更新要发送的短信数量
const count = await db.collection(this.tableNames.log).where({ task_id: taskId }).count()
await db.collection(this.tableNames.task).where({ _id: taskId }).update({
send_qty: count.total
})
// 开始发送
uniSmsCo.sendSms(taskId)
return new Promise(resolve => setTimeout(() => resolve({
errCode: 0,
errMsg: '创建完成'
}), 500))
}
if (!beforeId) {
beforeId = users[users.length - 1]._id
}
let docs = []
for (const user of users) {
const varData = await buildTemplateData(task.vars, user)
docs.push({
uid: user._id,
task_id: taskId,
mobile: user.mobile,
var_data: varData,
status: 0,
create_date: Date.now()
})
}
await db.collection(this.tableNames.log).add(docs)
uniSmsCo.createUserSmsMessage(taskId, { beforeId })
return new Promise(resolve => setTimeout(() => resolve(), 500))
},
async sendSms(taskId) {
const { data: tasks } = await db.collection(this.tableNames.task).where({ _id: taskId }).get()
if (tasks.length <= 0) {
console.warn(`task [${taskId}] not found`)
return
}
const [task] = tasks
const isStaticTemplate = !task.vars.length
let sendData = {
appId: task.app_id,
smsKey: smsConfig.smsKey,
smsSecret: smsConfig.smsSecret,
templateId: task.template_id,
data: {}
}
const { data: records } = await db.collection(this.tableNames.log)
.where({ task_id: taskId, status: 0 })
.limit(isStaticTemplate ? 50 : 1)
.field({ mobile: true, var_data: true })
.get()
if (records.length <= 0) {
return {
errCode: 0,
errMsg: '发送完成'
}
}
if (isStaticTemplate) {
sendData.phoneList = records.reduce((res, user) => {
res.push(user.mobile)
return res
}, [])
} else {
const [record] = records
sendData.phone = record.mobile
sendData.data = record.var_data
}
try {
// await sendSms(sendData)
await uniCloud.sendSms(sendData)
// 修改发送状态为已发送
await db.collection(this.tableNames.log).where({
_id: db.command.in(records.map(record => record._id))
}).update({
status: 1,
send_date: Date.now()
})
// 更新任务的短信成功数
await db.collection(this.tableNames.task).where({ _id: taskId })
.update({
success_qty: db.command.inc(records.length)
})
} catch (e) {
console.error('[sendSms Fail]', e)
// 修改发送状态为发送失败
await db.collection(this.tableNames.log).where({
_id: db.command.in(records.map(record => record._id))
}).update({
status: 2,
reason: e.errMsg || '未知原因',
send_date: Date.now()
})
// 更新任务的短信失败数
await db.collection(this.tableNames.task).where({ _id: taskId })
.update({
fail_qty: db.command.inc(records.length)
})
}
uniSmsCo.sendSms(taskId)
return new Promise(resolve => setTimeout(() => resolve(), 500))
},
async template() {
const {data: templates = []} = await db.collection(this.tableNames.template).get()
return templates
},
async task (id) {
const {data: tasks} = await db.collection(this.tableNames.task).where({_id: id}).get()
if (tasks.length <= 0) {
return null
}
return tasks[0]
},
async updateTemplates (templates) {
if (templates.length <= 0) {
return {
errCode: errCode('template-is-null'),
errMsg: '缺少模板信息'
}
}
let group = []
for (const template of templates) {
group.push(
db.collection(this.tableNames.template).doc(String(template.templateId)).set({
name: template.templateName,
content: template.templateContent,
type: template.templateType,
sign: template.templateSign
})
)
}
await Promise.all(group)
return {
errCode: 0,
errMsg: '更新成功'
}
},
async preview (to, templateId, templateData) {
const count = 1
let query = {
mobile: db.command.exists(true)
}
// 指定用户发送
if (!to.all && to.type === 'user') {
const receiver = to.receiver.slice(0, 10)
query._id = db.command.in(receiver)
}
// 指定用户标签
if (to.type === 'userTags') {
query.tags = db.command.in(to.receiver)
}
const {data: users} = await db.collection('uni-id-users').where(query).limit(count).get()
console.log({users, query})
if (users.length <= 0) {
return {
errCode: errCode('users-is-null'),
errMsg: '请添加要发送的用户'
}
}
const {data: templates} = await db.collection(this.tableNames.template).where({_id: templateId}).get()
if (templates.length <= 0) {
return {
errCode: errCode('template-not-found'),
errMsg: '模板不存在'
}
}
const [template] = templates
let docs = []
for (const user of users) {
const varData = buildTemplateData(templateData, user)
const content = template.content.replace(/\$\{(.*?)\}/g, ($1, param) => varData[param] || $1)
docs.push(`【${template.sign}${content}`)
}
return {
errCode: 0,
errMsg: '',
list: docs
}
}
}
{
"name": "uni-sms-co",
"dependencies": {
"uni-config-center": "file:../../../uni_modules/uni-config-center/uniCloud/cloudfunctions/common/uni-config-center"
},
"extensions": {
"uni-cloud-sms": {},
"uni-cloud-jql": {}
}
}
const db = uniCloud.database()
module.exports = async function () {
try {
const count = await db.collection('batch-sms-template').count()
if (count.total > 0) {
this.tableNames = {
template: 'batch-sms-template',
task: 'batch-sms-task',
log: 'batch-sms-result'
}
}
} catch (e) {}
}
exports.chunk = function (arr, num) {
const list = []
let current = []
for (const item of arr) {
current.push(item);
if (current.length === num) {
list.push(current)
current = []
}
}
if (current.length) list.push(current)
return list
}
exports.checkIsStaticTemplate = function (data = []) {
let isStatic = data.length <= 0
for (const template of data) {
if (template.type === 'static') {
isStatic = true
break
}
}
return isStatic
}
exports.parserDynamicField = function (templateData) {
return templateData.reduce((res, template) => {
if (/\{.*?\}/.test(template.value)) {
const [collection, field] = template.value.replace(/\{|\}/g, '').split('.')
if (!res[collection]) {
res[collection] = []
}
res[collection].push(field)
}
return res
}, {})
}
\ No newline at end of file
'use strict';
const uniStat = require('uni-stat')
exports.main = async (event, context) => {
//数据跑批处理函数
return await uniStat.initStat().cron(context)
};
{
"name": "uni-stat-cron",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"uni-stat": "file:../common/uni-stat"
},
"cloudfunction-config": {
"concurrency": 1,
"memorySize": 512,
"timeout": 600,
"triggers": [
{
"name": "uni-stat-cron",
"type": "timer",
"config": "0 0 * * * * *"
}
]
},
"extensions": {}
}
\ No newline at end of file
const uniStat = require('uni-stat')
const uniID = require('uni-id-common')
module.exports = {
report: async function (params = {}) {
//客户端信息
const clientInfo = this.getClientInfo()
//云服务信息
const cloudInfo = this.getCloudInfo()
//token信息
const token = this.getUniIdToken()
//当前登录用户id
let uid
if(token) {
const tokenRes = await uniID.createInstance({
clientInfo
}).checkToken(token)
if(tokenRes.uid) {
uid = tokenRes.uid
}
}
//数据上报
return await uniStat.initReceiver().report(params, {
...clientInfo,
...cloudInfo,
uid
})
}
}
{
"name": "uni-stat-receiver",
"dependencies": {
"uni-stat": "file:../common/uni-stat",
"uni-id-common": "file:../../../uni_modules/uni-id-common/uniCloud/cloudfunctions/common/uni-id-common"
},
"cloudfunction-config": {
"concurrency": 1,
"memorySize": 128,
"timeout": 60,
"triggers": []
},
"extensions": {
"uni-cloud-jql": {}
}
}
'use strict';
module.exports = async (event, context) => {
/**
* 检测升级 使用说明
* 上传包:
* 1. 根据传参,先检测传参是否完整,appid appVersion wgtVersion 必传
* 2. 先从数据库取出所有该平台(从上下文读取平台信息,默认 Andriod)的所有线上发行更新
* 3. 再从所有线上发行更新中取出版本最大的一版。如果可以,尽量先检测wgt的线上发行版更新
* 4. 使用上步取出的版本包的版本号 和传参 appVersion、wgtVersion 来检测是否有更新,必须同时大于这两项,否则返回暂无更新
* 5. 如果库中 wgt包 版本大于传参 appVersion,但是不满足 min_uni_version < appVersion,则不会使用wgt更新,会接着判断库中 app包version 是否大于 appVersion
*/
let {
appid,
appVersion,
wgtVersion,
} = event;
const platform_Android = 'Android';
const platform_iOS = 'iOS';
const package_app = 'native_app';
const package_wgt = 'wgt';
const app_version_db_name = 'opendb-app-versions'
let platform = platform_Android;
// 云函数URL化请求
if (event.headers) {
let body;
try {
if (event.httpMethod.toLocaleLowerCase() === 'get') {
body = event.queryStringParameters;
} else {
body = JSON.parse(event.body);
}
} catch (e) {
return {
code: 500,
msg: '请求错误'
};
}
appid = body.appid;
appVersion = body.appVersion;
wgtVersion = body.wgtVersion;
platform = /iPhone|iPad/.test(event.headers) ? platform_iOS : platform_Android;
} else if (context.OS) {
platform = context.OS === 'android' ?
platform_Android :
context.OS === 'ios' ?
platform_iOS :
platform_Android;
}
if (appid && appVersion && wgtVersion && platform) {
const collection = uniCloud.database().collection(app_version_db_name);
const record = await collection.where({
appid,
platform,
stable_publish: true
})
.orderBy('create_date', 'desc')
.get();
if (record && record.data && record.data.length > 0) {
const appVersionInDb = record.data.find(item => item.type === package_app) || {};
const wgtVersionInDb = record.data.find(item => item.type === package_wgt) || {};
const hasAppPackage = !!Object.keys(appVersionInDb).length;
const hasWgtPackage = !!Object.keys(wgtVersionInDb).length;
// 取两个版本中版本号最大的包,版本一样,使用wgt包
let stablePublishDb = hasAppPackage ?
hasWgtPackage ?
compare(wgtVersionInDb.version, appVersionInDb.version) >= 0 ?
wgtVersionInDb :
appVersionInDb :
appVersionInDb :
wgtVersionInDb;
const {
version,
min_uni_version
} = stablePublishDb;
// 库中的version必须满足同时大于appVersion和wgtVersion才行,因为上次更新可能是wgt更新
const appUpdate = compare(version, appVersion) === 1; // app包可用更新
const wgtUpdate = compare(version, wgtVersion) === 1; // wgt包可用更新
if (Object.keys(stablePublishDb).length && appUpdate && wgtUpdate) {
// 判断是否可用wgt更新
if (min_uni_version && compare(min_uni_version, appVersion) < 1) {
return {
code: 101,
message: 'wgt更新',
...stablePublishDb
};
} else if (hasAppPackage && compare(appVersionInDb.version, appVersion) === 1) {
return {
code: 102,
message: '整包更新',
...appVersionInDb
};
}
}
return {
code: 0,
message: '当前版本已经是最新的,不需要更新'
};
}
return {
code: -101,
message: '暂无更新或检查appid是否填写正确'
};
}
return {
code: -102,
message: '请检查传参是否填写正确'
};
};
/**
* 对比版本号,如需要,请自行修改判断规则
* 支持比对 ("3.0.0.0.0.1.0.1", "3.0.0.0.0.1") ("3.0.0.1", "3.0") ("3.1.1", "3.1.1.1") 之类的
* @param {Object} v1
* @param {Object} v2
* v1 > v2 return 1
* v1 < v2 return -1
* v1 == v2 return 0
*/
function compare(v1 = '0', v2 = '0') {
v1 = String(v1).split('.')
v2 = String(v2).split('.')
const minVersionLens = Math.min(v1.length, v2.length);
let result = 0;
for (let i = 0; i < minVersionLens; i++) {
const curV1 = Number(v1[i])
const curV2 = Number(v2[i])
if (curV1 > curV2) {
result = 1
break;
} else if (curV1 < curV2) {
result = -1
break;
}
}
if (result === 0 && (v1.length !== v2.length)) {
const v1BiggerThenv2 = v1.length > v2.length;
const maxLensVersion = v1BiggerThenv2 ? v1 : v2;
for (let i = minVersionLens; i < maxLensVersion.length; i++) {
const curVersion = Number(maxLensVersion[i])
if (curVersion > 0) {
v1BiggerThenv2 ? result = 1 : result = -1
break;
}
}
}
return result;
}
'use strict';
const success = {
success: true
}
const fail = {
success: false
}
const checkVersion = require('./checkVersion')
exports.main = async (event, context) => {
//event为客户端上传的参数
const db = uniCloud.database()
const appListDBName = 'opendb-app-list'
const appVersionDBName = 'opendb-app-versions'
let res = {};
if (event.headers) {
try {
if (event.httpMethod.toLocaleLowerCase() === 'get') {
event = event.queryStringParameters;
} else {
event = JSON.parse(event.body);
}
} catch (e) {
return {
code: 500,
msg: '请求错误'
};
}
}
let params = event.data || event.params;
switch (event.action) {
case 'checkVersion':
res = await checkVersion(event, context)
break;
case 'deleteFile':
res = await uniCloud.deleteFile({
fileList: params.fileList
})
break;
case 'setNewAppData':
params.value.create_date = Date.now()
res = await db.collection(appListDBName).doc(params.id).set(params.value)
break;
case 'getAppInfo':
let dbAppList
try {
dbAppList = db.collection(appListDBName)
} catch (e) {}
if (!dbAppList) return fail;
const dbAppListRecord = await dbAppList.where({
appid: params.appid
}).get()
if (dbAppListRecord && dbAppListRecord.data.length)
return Object.assign({}, success, dbAppListRecord.data[0])
//返回数据给客户端
return fail
break;
case 'getAppVersionInfo':
let dbVersionList
try {
dbVersionList = db.collection(appVersionDBName)
} catch (e) {}
if (!dbVersionList) return fail;
const dbVersionListrecord = await dbVersionList.where({
appid: params.appid,
platform: params.platform,
type: "native_app",
stable_publish: true
})
.orderBy('create_date', 'desc')
.get();
if (dbVersionListrecord && dbVersionListrecord.data && dbVersionListrecord.data.length > 0)
return Object.assign({}, dbVersionListrecord.data[0], success)
return fail
break;
}
//返回数据给客户端
return res
};
{
"name": "uni-app-manager",
"dependencies": {},
"extensions": {
"uni-cloud-jql": {}
}
}
\ No newline at end of file
// 本文件中的json内容将在云函数【运行】时作为参数传给云函数。
// 配置教程参考:https://uniapp.dcloud.net.cn/uniCloud/quickstart?id=runparam
{
"action": "checkVersion",
"appid": "__UNI__HelloUNIApp",
"appVersion": "1.0.0",
"wgtVersion": "1.0.0"
}
// 在本文件中可配置云数据库初始化,数据格式见:https://uniapp.dcloud.io/uniCloud/cf-database?id=db_init // 在本文件中可配置云数据库初始化,数据格式见:https://uniapp.dcloud.io/uniCloud/hellodb?id=db-init
// 编写完毕后对本文件点右键,可按配置规则创建表和添加数据 // 编写完毕后对本文件点右键,可按配置规则创建表和添加数据
{ {
"uni-id-users": {
"data": [{
"_id": "_uni_starter_test_user_id",
"username": "uni-starter预置用户名",
"nickname": "测试用户昵称",
"avatar": "https://vkceyugu.cdn.bspapp.com/VKCEYUGU-dc-site/460d46d0-4fcc-11eb-8ff1-d5dcf8779628.png",
"mobile": "18888888888",
"mobile_confirmed": 1
}]
},
"opendb-banner": {
"data": [{
"status": true,
"bannerfile": {
"name": "094a9dc0-50c0-11eb-b680-7980c8a877b8.jpg",
"extname": "jpg",
"fileType": "image",
"url": "https://vkceyugu.cdn.bspapp.com/VKCEYUGU-76ce2c5e-31c7-4d81-8fcf-ed1541ecbc6e/b88a7e17-35f0-4d0d-bc32-93f8909baf03.jpg",
"size": 70880,
"image": {
"width": 500,
"height": 333,
"location": "blob:http://localhost:8081/a3bfaab4-7ee6-44d5-a171-dc8225d83598"
},
"path": "https://vkceyugu.cdn.bspapp.com/VKCEYUGU-76ce2c5e-31c7-4d81-8fcf-ed1541ecbc6e/b88a7e17-35f0-4d0d-bc32-93f8909baf03.jpg"
},
"open_url": "https://www.dcloud.io/",
"title": "测试",
"sort": 1,
"category_id": "",
"description": ""
},
{
"status": true,
"bannerfile": {
"name": "094a9dc0-50c0-11eb-b680-7980c8a877b8.jpg",
"extname": "jpg",
"fileType": "image",
"url": "https://vkceyugu.cdn.bspapp.com/VKCEYUGU-76ce2c5e-31c7-4d81-8fcf-ed1541ecbc6e/9db94cb4-a5e0-4ed9-b356-b42a392b3112.jpg",
"size": 70880,
"image": {
"width": 500,
"height": 333,
"location": "blob:http://localhost:8081/1a6f718a-4012-476a-9172-590fef2cc518"
},
"path": "https://vkceyugu.cdn.bspapp.com/VKCEYUGU-76ce2c5e-31c7-4d81-8fcf-ed1541ecbc6e/9db94cb4-a5e0-4ed9-b356-b42a392b3112.jpg"
},
"open_url": "https://www.dcloud.io/",
"title": "",
"category_id": "",
"description": ""
}
]
},
"opendb-news-articles": {
"data": [{
"title": "阿里小程序IDE官方内嵌uni-app,为开发者提供多端开发服务",
"excerpt": "阿里小程序IDE官方内嵌uni-app,为开发者提供多端开发服务",
"content": "<p>随着微信、阿里、百度、头条、QQ纷纷推出小程序,开发者的开发维护成本持续上升,负担过重。这点已经成为共识,现在连小程序平台厂商也充分意识到了。</p>\n<p>阿里小程序团队,为了减轻开发者的负担,在官方的小程序开发者工具中整合了多端框架。</p>\n<p>经过阿里团队仔细评估,uni-app 在产品完成度、跨平台支持度、开发者社区、可持续发展等多方面优势明显,最终选定 uni-app内置于阿里小程序开发工具中,为开发者提供多端开发解决方案。</p>\n<p>经过之前1个月的公测,10月10日,阿里小程序正式发布0.70版开发者工具,通过 uni-app 实现多端开发,成为本次版本更新的亮点功能!</p>\n<p>如下图,在阿里小程序工具左侧主导航选择 uni-app,创建项目,即可开发。</p>\n<div class=\"aw-comment-upload-img-list active\"><img class=\"img-polaroid\" width=\"100%\" src=\"https://vkceyugu.cdn.bspapp.com/VKCEYUGU-76ce2c5e-31c7-4d81-8fcf-ed1541ecbc6e/b698232a-e608-4b2d-8019-8dc6bbf343a8.png\" /></div>\n<p><br />阿里小程序开发工具更新说明详见:https://docs.alipay.com/mini/ide/0.70-stable</p>\n<p>&nbsp;</p>\n<p>集成uni-app,这对于阿里团队而言,并不是一个容易做出的决定。毕竟 uni-app 是一个三方产品,要经过复杂的评审流程。</p>\n<p>这一方面突显出阿里团队以开发者需求为本的优秀价值观,另一方面也证明 uni-app的产品确实过硬。</p>\n<p>很多开发者都有多端需求,但又没有足够精力去了解、评估 uni-app,而处于观望态度。现在大家可以更放心的使用 uni-app 了,它没有让阿里失望,也不会让你失望。</p>\n<p>自从uni-app推出以来,DCloud也取得了高速的发展,目前拥有370万开发者,框架运行在4.6亿手机用户设备上,月活达到1.35亿(仅包括部分接入DCloud统计平台的数据)。并且数据仍在高速增长中,在市场占有率上处于遥遥领先的位置。</p>\n<p>本次阿里小程序工具集成 uni-app,会让 uni-app 继续快速爆发,取得更大的成功。</p>\n<p>后续DCloud还将深化与阿里的合作,在serverless等领域给开发者提供更多优质服务。</p>\n<p>使用多端框架开发各端应用,是多赢的模式。开发者减轻了负担,获得了更多新流量。而小程序平台厂商,也能保证自己平台上的各种应用可以被及时的更新。</p>\n<p>DCloud欢迎更多小程序平台厂商,与我们一起合作,为开发者、平台、用户的多赢而努力。</p>\n<p>进一步了解uni-app,详见:https://uniapp.dcloud.io</p>\n<p>欢迎扫码关注DCloud公众号,转发消息到朋友圈。<br /><img src=\"https://vkceyugu.cdn.bspapp.com/VKCEYUGU-76ce2c5e-31c7-4d81-8fcf-ed1541ecbc6e/27302ea5-e369-4e6c-89b1-431408497b3d.jpg\" width=\"80%\" /></p>",
"avatar": "https://vkceyugu.cdn.bspapp.com/VKCEYUGU-aliyun-gacrhzeynhss7c6d04/249516a0-3941-11eb-899d-733ae62bed2f.jpg",
"type": 0,
"user_id": "_uni_starter_test_user_id",
"comment_count": 0,
"like_count": 0,
"comment_status": 0,
"article_status": 1,
"publish_date": 1616092287006,
"last_modify_date": 1616092303031,
"create_time": "2021-03-19T08:25:06.109Z"
}]
},
"opendb-app-versions": {
"data": [{
"is_silently": false,
"is_mandatory": false,
"appid": "__UNI__03B096E",
"name": "uni-starter",
"title": "新增升级中心",
"contents": "新增升级中心",
"platform": [
"Android"
],
"version": "1.0.1",
"url": "https://vkceyugu.cdn.bspapp.com/VKCEYUGU-3469aac7-a663-4c5d-8ee8-94275f8c09ab/3128d010-01c5-4121-a1d6-f3f919944a23.apk",
"stable_publish": false,
"type": "native_app",
"create_date": 1616771628150
}],
"index": [{
"IndexName": "appid",
"MgoKeySchema": {
"MgoIndexKeys": [{
"Name": "appid",
"Direction": "1"
}, {
"Name": "uni_platform",
"Direction": "1"
}, {
"Name": "create_env",
"Direction": "1"
}],
"MgoIsUnique": false
}
}, {
"IndexName": "查找上线发行应用",
"MgoKeySchema": {
"MgoIndexKeys": [{
"Name": "appid",
"Direction": "1"
}, {
"Name": "platform",
"Direction": "1"
}, {
"Name": "stable_publish",
"Direction": "1"
}, {
"Name": "uni_platform",
"Direction": "1"
}, {
"Name": "create_env",
"Direction": "1"
}],
"MgoIsUnique": false
}
}]
},
"opendb-verify-codes": { "opendb-verify-codes": {
"data": [] "data": []
}, },
"opendb-app-list": {
"data": [],
"index": [{
"IndexName": "appid",
"MgoKeySchema": {
"MgoIndexKeys": [{
"Name": "appid",
"Direction": "1"
}],
"MgoIsUnique": true
}
}, {
"IndexName": "name",
"MgoKeySchema": {
"MgoIndexKeys": [{
"Name": "name",
"Direction": "1"
}],
"MgoIsUnique": false
}
}]
},
"uni-id-roles": { "uni-id-roles": {
"data": [] "data": []
}, },
...@@ -15,9 +163,19 @@ ...@@ -15,9 +163,19 @@
}, },
"opendb-admin-menus": { "opendb-admin-menus": {
"data": [{ "data": [{
"menu_id": "index",
"name": "首页",
"icon": "uni-icons-home",
"url": "/",
"sort": 100,
"parent_id": "",
"permission": [],
"enable": true,
"create_date": 1602662469396
}, {
"menu_id": "system_management", "menu_id": "system_management",
"name": "系统管理", "name": "系统管理",
"icon": "uni-icons-gear", "icon": "admin-icons-fl-xitong",
"url": "", "url": "",
"sort": 1000, "sort": 1000,
"parent_id": "", "parent_id": "",
...@@ -27,7 +185,7 @@ ...@@ -27,7 +185,7 @@
}, { }, {
"menu_id": "system_user", "menu_id": "system_user",
"name": "用户管理", "name": "用户管理",
"icon": "uni-icons-person", "icon": "admin-icons-manager-user",
"url": "/pages/system/user/list", "url": "/pages/system/user/list",
"sort": 1010, "sort": 1010,
"parent_id": "system_management", "parent_id": "system_management",
...@@ -37,7 +195,7 @@ ...@@ -37,7 +195,7 @@
}, { }, {
"menu_id": "system_role", "menu_id": "system_role",
"name": "角色管理", "name": "角色管理",
"icon": "uni-icons-personadd", "icon": "admin-icons-manager-role",
"url": "/pages/system/role/list", "url": "/pages/system/role/list",
"sort": 1020, "sort": 1020,
"parent_id": "system_management", "parent_id": "system_management",
...@@ -47,7 +205,7 @@ ...@@ -47,7 +205,7 @@
}, { }, {
"menu_id": "system_permission", "menu_id": "system_permission",
"name": "权限管理", "name": "权限管理",
"icon": "uni-icons-locked", "icon": "admin-icons-manager-permission",
"url": "/pages/system/permission/list", "url": "/pages/system/permission/list",
"sort": 1030, "sort": 1030,
"parent_id": "system_management", "parent_id": "system_management",
...@@ -57,101 +215,409 @@ ...@@ -57,101 +215,409 @@
}, { }, {
"menu_id": "system_menu", "menu_id": "system_menu",
"name": "菜单管理", "name": "菜单管理",
"icon": "uni-icons-settings", "icon": "admin-icons-manager-menu",
"url": "/pages/system/menu/list", "url": "/pages/system/menu/list",
"sort": 1040, "sort": 1040,
"parent_id": "system_management", "parent_id": "system_management",
"permission": [], "permission": [],
"enable": true, "enable": true,
"create_date": 1602662469396 "create_date": 1602662469396
}] }, {
}, "menu_id": "system_app",
"opendb-news-articles": { "name": "应用管理",
"data": [{ "icon": "admin-icons-manager-app",
"title": "阿里小程序IDE官方内嵌uni-app,为开发者提供多端开发服务", "url": "/pages/system/app/list",
"excerpt": "阿里小程序IDE官方内嵌uni-app,为开发者提供多端开发服务", "sort": 1035,
"content": "<p>随着微信、阿里、百度、头条、QQ纷纷推出小程序,开发者的开发维护成本持续上升,负担过重。这点已经成为共识,现在连小程序平台厂商也充分意识到了。</p>\n<p>阿里小程序团队,为了减轻开发者的负担,在官方的小程序开发者工具中整合了多端框架。</p>\n<p>经过阿里团队仔细评估,uni-app 在产品完成度、跨平台支持度、开发者社区、可持续发展等多方面优势明显,最终选定 uni-app内置于阿里小程序开发工具中,为开发者提供多端开发解决方案。</p>\n<p>经过之前1个月的公测,10月10日,阿里小程序正式发布0.70版开发者工具,通过 uni-app 实现多端开发,成为本次版本更新的亮点功能!</p>\n<p>如下图,在阿里小程序工具左侧主导航选择 uni-app,创建项目,即可开发。</p>\n<div class=\"aw-comment-upload-img-list active\"><img class=\"img-polaroid\" width=\"100%\" src=\"https://vkceyugu.cdn.bspapp.com/VKCEYUGU-76ce2c5e-31c7-4d81-8fcf-ed1541ecbc6e/b698232a-e608-4b2d-8019-8dc6bbf343a8.png\" /></div>\n<p><br />阿里小程序开发工具更新说明详见:https://docs.alipay.com/mini/ide/0.70-stable</p>\n<p>&nbsp;</p>\n<p>集成uni-app,这对于阿里团队而言,并不是一个容易做出的决定。毕竟 uni-app 是一个三方产品,要经过复杂的评审流程。</p>\n<p>这一方面突显出阿里团队以开发者需求为本的优秀价值观,另一方面也证明 uni-app的产品确实过硬。</p>\n<p>很多开发者都有多端需求,但又没有足够精力去了解、评估 uni-app,而处于观望态度。现在大家可以更放心的使用 uni-app 了,它没有让阿里失望,也不会让你失望。</p>\n<p>自从uni-app推出以来,DCloud也取得了高速的发展,目前拥有370万开发者,框架运行在4.6亿手机用户设备上,月活达到1.35亿(仅包括部分接入DCloud统计平台的数据)。并且数据仍在高速增长中,在市场占有率上处于遥遥领先的位置。</p>\n<p>本次阿里小程序工具集成 uni-app,会让 uni-app 继续快速爆发,取得更大的成功。</p>\n<p>后续DCloud还将深化与阿里的合作,在serverless等领域给开发者提供更多优质服务。</p>\n<p>使用多端框架开发各端应用,是多赢的模式。开发者减轻了负担,获得了更多新流量。而小程序平台厂商,也能保证自己平台上的各种应用可以被及时的更新。</p>\n<p>DCloud欢迎更多小程序平台厂商,与我们一起合作,为开发者、平台、用户的多赢而努力。</p>\n<p>进一步了解uni-app,详见:https://uniapp.dcloud.io</p>\n<p>欢迎扫码关注DCloud公众号,转发消息到朋友圈。<br /><img src=\"https://vkceyugu.cdn.bspapp.com/VKCEYUGU-76ce2c5e-31c7-4d81-8fcf-ed1541ecbc6e/27302ea5-e369-4e6c-89b1-431408497b3d.jpg\" width=\"80%\" /></p>", "parent_id": "system_management",
"avatar": "https://vkceyugu.cdn.bspapp.com/VKCEYUGU-aliyun-gacrhzeynhss7c6d04/249516a0-3941-11eb-899d-733ae62bed2f.jpg", "permission": [],
"type": 0, "enable": true,
"user_id": "123", "create_date": 1602662469399
"comment_count": 0, }, {
"like_count": 0, "menu_id": "system_update",
"comment_status": 0, "name": "App升级中心",
"article_status": 1, "icon": "uni-icons-cloud-upload",
"publish_date": 1616092287006, "url": "/uni_modules/uni-upgrade-center/pages/version/list",
"last_modify_date": 1616092303031, "sort": 1036,
"create_time": "2021-03-19T08:25:06.109Z" "parent_id": "system_management",
}] "permission": [],
}, "enable": true,
"opendb-app-versions": { "create_date": 1656491532434
"data": [{ }, {
"is_silently": false, "menu_id": "system_tag",
"is_mandatory": false, "name": "标签管理",
"appid": "__UNI__03B096E", "icon": "admin-icons-manager-tag",
"name": "uni-starter", "url": "/pages/system/tag/list",
"title": "新增升级中心", "sort": 1037,
"contents": "新增升级中心", "parent_id": "system_management",
"platform": [ "permission": [],
"Android" "enable": true,
], "create_date": 1602662479389
"version": "1.0.1", }, {
"url": "https://vkceyugu.cdn.bspapp.com/VKCEYUGU-3469aac7-a663-4c5d-8ee8-94275f8c09ab/3128d010-01c5-4121-a1d6-f3f919944a23.apk", "permission": [],
"stable_publish": false, "enable": true,
"type": "native_app", "menu_id": "safety_statistics",
"create_date": 1616771628150 "name": "安全审计",
}] "icon": "admin-icons-safety",
}, "url": "",
"uni-id-users": { "sort": 3100,
"data": [{ "parent_id": "",
"_id": "123", "create_date": 1638356430871
"username": "预置用户", }, {
"nickname": "测试", "permission": [],
"avatar": "https://bjetxgzv.cdn.bspapp.com/VKCEYUGU-dc-site/d84c6de0-6080-11eb-bdc1-8bd33eb6adaa.png", "enable": true,
"mobile": "18888888888", "menu_id": "safety_statistics_user_log",
"mobile_confirmed": 1 "name": "用户日志",
}] "icon": "",
}, "url": "/pages/system/safety/list",
"opendb-banner": { "sort": 3101,
"data": [{ "parent_id": "safety_statistics",
"status": true, "create_date": 1638356430871
"bannerfile": { }, {
"name": "094a9dc0-50c0-11eb-b680-7980c8a877b8.jpg", "permission": [],
"extname": "jpg", "enable": true,
"fileType": "image", "menu_id": "uni-stat",
"url": "https://vkceyugu.cdn.bspapp.com/VKCEYUGU-76ce2c5e-31c7-4d81-8fcf-ed1541ecbc6e/b88a7e17-35f0-4d0d-bc32-93f8909baf03.jpg", "name": "uni 统计",
"size": 70880, "icon": "admin-icons-tongji",
"image": { "url": "",
"width": 500, "sort": 2100,
"height": 333, "parent_id": "",
"location": "blob:http://localhost:8081/a3bfaab4-7ee6-44d5-a171-dc8225d83598" "create_date": 1638356430871
}, }, {
"path": "https://vkceyugu.cdn.bspapp.com/VKCEYUGU-76ce2c5e-31c7-4d81-8fcf-ed1541ecbc6e/b88a7e17-35f0-4d0d-bc32-93f8909baf03.jpg" "parent_id": "uni-stat",
"permission": [],
"enable": true,
"menu_id": "uni-stat-device",
"name": "设备统计",
"icon": "admin-icons-shebeitongji",
"url": "",
"sort": 2120,
"create_date": 1638356902516
}, {
"parent_id": "uni-stat-device",
"permission": [],
"enable": true,
"menu_id": "uni-stat-device-overview",
"name": "概况",
"icon": "",
"url": "/pages/uni-stat/device/overview/overview",
"sort": 2121,
"create_date": 1638356902516
}, {
"parent_id": "uni-stat-device",
"permission": [],
"enable": true,
"menu_id": "uni-stat-device-activity",
"name": "活跃度",
"icon": "",
"url": "/pages/uni-stat/device/activity/activity",
"sort": 2122,
"create_date": 1638356902516
}, {
"parent_id": "uni-stat-device",
"permission": [],
"enable": true,
"menu_id": "uni-stat-device-trend",
"name": "趋势分析",
"icon": "",
"url": "/pages/uni-stat/device/trend/trend",
"sort": 2123,
"create_date": 1638356902516
}, {
"parent_id": "uni-stat-device",
"permission": [],
"enable": true,
"menu_id": "uni-stat-device-retention",
"name": "留存",
"icon": "",
"url": "/pages/uni-stat/device/retention/retention",
"sort": 2124,
"create_date": 1638356902516
}, {
"parent_id": "uni-stat-device",
"permission": [],
"enable": true,
"menu_id": "uni-stat-device-comparison",
"name": "平台对比",
"icon": "",
"url": "/pages/uni-stat/device/comparison/comparison",
"sort": 2125,
"create_date": 1638356902516
}, {
"parent_id": "uni-stat-device",
"permission": [],
"enable": true,
"menu_id": "uni-stat-device-stickiness",
"name": "粘性",
"icon": "",
"url": "/pages/uni-stat/device/stickiness/stickiness",
"sort": 2126,
"create_date": 1638356902516
}, {
"parent_id": "uni-stat",
"permission": [],
"enable": true,
"menu_id": "uni-stat-user",
"name": "注册用户统计",
"icon": "admin-icons-yonghutongji",
"url": "",
"sort": 2122,
"create_date": 1638356902516
}, {
"parent_id": "uni-stat-user",
"permission": [],
"enable": true,
"menu_id": "uni-stat-user-overview",
"name": "概况",
"icon": "",
"url": "/pages/uni-stat/user/overview/overview",
"sort": 2121,
"create_date": 1638356902516
}, {
"parent_id": "uni-stat-user",
"permission": [],
"enable": true,
"menu_id": "uni-stat-user-activity",
"name": "活跃度",
"icon": "",
"url": "/pages/uni-stat/user/activity/activity",
"sort": 2122,
"create_date": 1638356902516
}, {
"parent_id": "uni-stat-user",
"permission": [],
"enable": true,
"icon": "",
"menu_id": "uni-stat-user-trend",
"name": "趋势分析",
"url": "/pages/uni-stat/user/trend/trend",
"sort": 2123,
"create_date": 1638356902516
}, {
"parent_id": "uni-stat-user",
"permission": [],
"enable": true,
"menu_id": "uni-stat-user-retention",
"name": "留存",
"icon": "",
"url": "/pages/uni-stat/user/retention/retention",
"sort": 2124,
"create_date": 1638356902516
}, {
"parent_id": "uni-stat-user",
"permission": [],
"enable": true,
"menu_id": "uni-stat-user-comparison",
"name": "平台对比",
"icon": "",
"url": "/pages/uni-stat/user/comparison/comparison",
"sort": 2125,
"create_date": 1638356902516
}, {
"parent_id": "uni-stat-user",
"permission": [],
"enable": true,
"menu_id": "uni-stat-user-stickiness",
"name": "粘性",
"icon": "",
"url": "/pages/uni-stat/user/stickiness/stickiness",
"sort": 2126,
"create_date": 1638356902516
}, {
"parent_id": "uni-stat",
"permission": [],
"enable": true,
"menu_id": "uni-stat-page-analysis",
"name": "页面统计",
"icon": "admin-icons-page-ent",
"url": "",
"sort": 2123,
"create_date": 1638356902516
}, {
"parent_id": "uni-stat-page-analysis",
"permission": [],
"enable": true,
"menu_id": "uni-stat-page-res",
"name": "受访页",
"icon": "",
"url": "/pages/uni-stat/page-res/page-res",
"sort": 2131,
"create_date": 1638356902516
}, {
"parent_id": "uni-stat-page-analysis",
"permission": [],
"enable": true,
"menu_id": "uni-stat-page-ent",
"name": "入口页",
"icon": "",
"url": "/pages/uni-stat/page-ent/page-ent",
"sort": 2132,
"create_date": 1638356902516
}, {
"parent_id": "uni-stat",
"permission": [],
"enable": true,
"menu_id": "uni-stat-senceChannel",
"name": "渠道/场景值分析",
"icon": "admin-icons-qudaofenxi",
"url": "",
"sort": 2150,
"create_date": 1638356902516
}, {
"parent_id": "uni-stat-senceChannel",
"permission": [],
"enable": true,
"menu_id": "uni-stat-senceChannel-scene",
"name": "场景值(小程序)",
"icon": "",
"url": "/pages/uni-stat/scene/scene",
"sort": 2151,
"create_date": 1638356902516
}, {
"parent_id": "uni-stat-senceChannel",
"permission": [],
"enable": true,
"menu_id": "uni-stat-senceChannel-channel",
"name": "渠道(app)",
"icon": "",
"url": "/pages/uni-stat/channel/channel",
"sort": 2152,
"create_date": 1638356902516
}, {
"parent_id": "uni-stat",
"permission": [],
"enable": true,
"menu_id": "uni-stat-event-event",
"name": "自定义事件",
"icon": "admin-icons-shijianfenxi",
"url": "/pages/uni-stat/event/event",
"sort": 2160,
"create_date": 1638356902516
}, {
"parent_id": "uni-stat",
"permission": [],
"enable": true,
"menu_id": "uni-stat-error",
"name": "错误统计",
"icon": "admin-icons-cuowutongji",
"url": "",
"sort": 2170,
"create_date": 1638356902516
}, {
"parent_id": "uni-stat-error",
"permission": [],
"enable": true,
"menu_id": "uni-stat-error-js",
"name": "js报错",
"icon": "",
"url": "/pages/uni-stat/error/js/js",
"sort": 2171,
"create_date": 1638356902516
}, {
"parent_id": "uni-stat-error",
"permission": [],
"enable": true,
"menu_id": "uni-stat-error-app",
"name": "app崩溃",
"icon": "",
"url": "/pages/uni-stat/error/app/app",
"sort": 2172,
"create_date": 1638356902516
}, },
"open_url": "https://www.dcloud.io/", {
"title": "测试", "menu_id": "uni-stat-pay",
"sort": 1, "name": "支付统计",
"category_id": "", "icon": "uni-icons-circle",
"description": "" "url": "",
"sort": 2122,
"parent_id": "uni-stat",
"permission": [],
"enable": true,
"create_date": 1667386977981
}, {
"menu_id": "uni-stat-pay-overview",
"name": "概况",
"icon": "",
"url": "/pages/uni-stat/pay-order/overview/overview",
"sort": 21221,
"parent_id": "uni-stat-pay",
"permission": [],
"enable": true,
"create_date": 1667387038602
}, },
{ {
"status": true, "menu_id": "uni-stat-pay-funnel",
"bannerfile": { "name": "转换漏斗分析",
"name": "094a9dc0-50c0-11eb-b680-7980c8a877b8.jpg", "icon": "",
"extname": "jpg", "url": "/pages/uni-stat/pay-order/funnel/funnel",
"fileType": "image", "sort": 21222,
"url": "https://vkceyugu.cdn.bspapp.com/VKCEYUGU-76ce2c5e-31c7-4d81-8fcf-ed1541ecbc6e/9db94cb4-a5e0-4ed9-b356-b42a392b3112.jpg", "parent_id": "uni-stat-pay",
"size": 70880, "permission": [],
"image": { "enable": true,
"width": 500, "create_date": 1668430092890
"height": 333,
"location": "blob:http://localhost:8081/1a6f718a-4012-476a-9172-590fef2cc518"
}, },
"path": "https://vkceyugu.cdn.bspapp.com/VKCEYUGU-76ce2c5e-31c7-4d81-8fcf-ed1541ecbc6e/9db94cb4-a5e0-4ed9-b356-b42a392b3112.jpg" {
"menu_id": "uni-stat-pay-ranking",
"name": "价值用户排行",
"icon": "",
"url": "/pages/uni-stat/pay-order/ranking/ranking",
"sort": 21223,
"parent_id": "uni-stat-pay",
"permission": [],
"enable": true,
"create_date": 1668430128302
}, },
"open_url": "https://www.dcloud.io/", {
"title": "", "menu_id": "uni-stat-pay-order-list",
"category_id": "", "name": "订单明细",
"description": "" "icon": "",
}] "url": "/pages/uni-stat/pay-order/list/list",
"sort": 21224,
"parent_id": "uni-stat-pay",
"permission": [],
"enable": true,
"create_date": 1667387078947
} }
]
},
"uni-id-tag": {},
"uni-id-device": {},
"opendb-device": {},
"opendb-department": {},
"opendb-sms-task": {},
"opendb-sms-log": {},
"opendb-sms-template": {},
"opendb-open-data": {},
"uni-stat-app-versions": {},
"uni-stat-active-devices": {},
"uni-stat-active-users": {},
"uni-stat-app-channels": {},
"uni-stat-app-crash-logs": {},
"uni-stat-app-platforms": {},
"uni-stat-error-logs": {},
"uni-stat-error-result": {},
"uni-stat-event-logs": {},
"uni-stat-event-result": {},
"uni-stat-events": {},
"uni-stat-loyalty-result": {},
"uni-stat-mp-scenes": {},
"uni-stat-page-logs": {},
"uni-stat-page-result": {},
"uni-stat-pages": {},
"uni-stat-result": {},
"uni-stat-run-errors": {},
"uni-stat-session-logs": {},
"uni-stat-share-logs": {},
"uni-stat-user-session-logs": {},
"uni-pay-orders": {},
"uni-stat-pay-result": {},
"opendb-tempdata": {},
"opendb-feedback": {},
"opendb-news-categories": {},
"opendb-news-comments": {},
"opendb-news-favorite": {},
"opendb-search-hot": {},
"opendb-search-log": {},
"opendb-sign-in": {},
"read-news-log": {},
"uni-id-scores": {}
} }
...@@ -2,7 +2,10 @@ ...@@ -2,7 +2,10 @@
"bsonType": "object", "bsonType": "object",
"required": ["name", "menu_id"], "required": ["name", "menu_id"],
"permission": { "permission": {
"read": true "read": true,
"create": "'CREATE_OPENDB_ADMIN_MENUS' in auth.permission",
"update": "'UPDATE_OPENDB_ADMIN_MENUS' in auth.permission",
"delete": "'DELETE_OPENDB_ADMIN_MENUS' in auth.permission"
}, },
"properties": { "properties": {
"_id": { "_id": {
......
// 文档教程: https://uniapp.dcloud.net.cn/uniCloud/schema
{ {
"bsonType": "object", "bsonType": "object",
"required": ["appid", "platform", "version", "url", "contents", "type"], "required": [
"appid",
"uni_platform",
"version",
"type",
"create_env"
],
"permission": { "permission": {
"read": false, "read": "'READ_OPENDB_APP_VERSIONS' in auth.permission",
"create": false, "create": "'CREATE_OPENDB_APP_VERSIONS' in auth.permission",
"update": false, "update": "'UPDATE_OPENDB_APP_VERSIONS' in auth.permission",
"delete": false "delete": "'DELETE_OPENDB_APP_VERSIONS' in auth.permission"
}, },
"properties": { "properties": {
"_id": { "_id": {
...@@ -56,25 +63,31 @@ ...@@ -56,25 +63,31 @@
}, },
"platform": { "platform": {
"bsonType": "array", "bsonType": "array",
"enum": [{ "enum": [
{
"value": "Android", "value": "Android",
"text": "安卓" "text": "安卓"
}, { },
{
"value": "iOS", "value": "iOS",
"text": "苹果" "text": "苹果"
}], }
],
"description": "更新平台,Android || iOS || [Android, iOS]", "description": "更新平台,Android || iOS || [Android, iOS]",
"label": "平台" "label": "平台"
}, },
"type": { "type": {
"bsonType": "string", "bsonType": "string",
"enum": [{ "enum": [
{
"value": "native_app", "value": "native_app",
"text": "原生App安装包" "text": "原生App安装包"
}, { },
{
"value": "wgt", "value": "wgt",
"text": "Wgt资源包" "text": "Wgt资源包"
}], }
],
"description": "安装包类型,native_app || wgt", "description": "安装包类型,native_app || wgt",
"label": "安装包类型" "label": "安装包类型"
}, },
...@@ -90,8 +103,8 @@ ...@@ -90,8 +103,8 @@
}, },
"url": { "url": {
"bsonType": "string", "bsonType": "string",
"description": "可下载安装包地址", "description": "可下载或跳转的链接",
"label": "包地址" "label": "链接"
}, },
"stable_publish": { "stable_publish": {
"bsonType": "bool", "bsonType": "bool",
...@@ -119,6 +132,47 @@ ...@@ -119,6 +132,47 @@
"componentForEdit": { "componentForEdit": {
"name": "uni-dateformat" "name": "uni-dateformat"
} }
},
"uni_platform": {
"bsonType": "string",
"description": "uni平台信息,如:mp-weixin/web/ios/android",
"label": "uni 平台"
},
"create_env": {
"bsonType": "string",
"description": "创建来源,uni-stat:uni统计自动创建,upgrade-center:升级中心管理员创建"
},
"store_list": {
"bsonType": "array",
"description": "发布的应用市场",
"label": "应用市场",
"properties": {
"id": {
"bsonType": "string",
"description": "应用id,自动生成",
"label": "id"
},
"name": {
"bsonType": "string",
"description": "应用名称",
"label": "应用名称"
},
"scheme": {
"bsonType": "string",
"description": "应用 scheme",
"label": "应用 scheme"
},
"enable": {
"bsonType": "bool",
"description": "是否启用"
},
"priority": {
"bsonType": "int",
"description": "按照从大到小排序",
"label": "优先级"
} }
} }
}
},
"version": "0.0.1"
} }
\ No newline at end of file
{
"bsonType": "object",
"required": [],
"permission": {
"read": false,
"create": false,
"update": false,
"delete": false
},
"properties": {
"_id": {
"description": "ID,系统自动生成"
},
"task_id": {
"bsonType": "string",
"description": "任务ID",
"foreignKey": "batch-sms-task._id"
},
"uid": {
"bsonType": "string",
"description": "用户ID",
"foreignKey": "uni-id-users._id"
},
"mobile": {
"bsonType": "int",
"description": "手机号"
},
"var_data": {
"bsonType": "object",
"description": "变量数据"
},
"status": {
"bsonType": "int",
"description": "发送状态",
"defaultValue": 0,
"enum": [{
"text": "未发送",
"value": 0
}, {
"text": "已发送",
"value": 1
}, {
"text": "发送失败",
"value": 2
}]
},
"reason": {
"bsonType": "string",
"description": "发送失败原因"
},
"send_date": {
"bsonType": "timestamp",
"description": "发送时间"
},
"ccreate_date": {
"bsonType": "timestamp",
"description": "创建时间",
"forceDefaultValue": {
"$env": "now"
}
}
}
}
{
"bsonType": "object",
"required": [],
"permission": {
"read": false,
"create": false,
"update": false,
"delete": false
},
"properties": {
"_id": {
"description": "ID,系统自动生成"
},
"name": {
"bsonType": "string",
"description": "任务名称",
"trim": "both"
},
"app_id": {
"bsonType": "string",
"description": "App ID",
"trim": "both"
},
"template_id": {
"bsonType": "string",
"description": "短信模板ID",
"trim": "both"
},
"template_content": {
"bsonType": "string",
"description": "短信模板内容",
"trim": "both"
},
"vars": {
"bsonType": "array",
"description": "短信变量"
},
"to": {
"bsonType": "object",
"description": "短信接收者信息",
"properties": {
"all": {
"bsonType": "bool",
"description": "全部用户发送"
},
"type": {
"bsonType": "string",
"description": "to.all=true时用来区分发送类型, 可选值 user | userTags"
},
"receiver": {
"bsonType": "array",
"description": "用户ID's \/ 用户标签ID's"
}
}
},
"send_qty": {
"bsonType": "int",
"description": "发送总数"
},
"success_qty": {
"bsonType": "int",
"description": "成功总数"
},
"fail_qty": {
"bsonType": "int",
"description": "失败总数"
},
"create_date": {
"bsonType": "timestamp",
"description": "创建时间",
"forceDefaultValue": {
"$env": "now"
}
}
}
}
// 文档教程: https://uniapp.dcloud.net.cn/uniCloud/schema
{
"bsonType": "object",
"required": [],
"permission": {
"read": false,
"create": false,
"update": false,
"delete": false
},
"properties": {
"_id": {
"description": "模板ID"
},
"name": {
"bsonType": "string",
"description": "模板名称"
},
"content": {
"bsonType": "string",
"description": "模板内容"
},
"type": {
"bsonType": "int",
"description": "模板类型"
},
"sign": {
"bsonType": "string",
"description": "模板签名"
}
}
}
{
"bsonType": "object",
"required": ["value", "expired"],
"permission": {
"read": false,
"create": false,
"update": false,
"delete": false
},
"properties": {
"_id": {
"description": "ID,系统自动生成"
},
"value": {
"description": "值"
},
"expired": {
"description": "过期时间",
"bsonType": "timestamp"
}
}
}
...@@ -2,10 +2,10 @@ ...@@ -2,10 +2,10 @@
"bsonType": "object", "bsonType": "object",
"required": ["tagid", "name"], "required": ["tagid", "name"],
"permission": { "permission": {
"read": false, "read": "'READ_UNI_ID_TAG' in auth.permission",
"create": false, "create": "'CREATE_UNI_ID_TAG' in auth.permission",
"update": false, "update": "'UPDATE_UNI_ID_TAG' in auth.permission",
"delete": false "delete": "'DELETE_UNI_ID_TAG' in auth.permission"
}, },
"properties": { "properties": {
"_id": { "_id": {
......
{
"bsonType": "object",
"required": [],
"properties": {
"_id": {
"description": "ID,系统自动生成"
},
"provider": {
"title": "支付供应商",
"bsonType": "string",
"enum": [{
"text": "微信支付",
"value": "wxpay"
},
{
"text": "支付宝",
"value": "alipay"
},
{
"text": "苹果应用内支付",
"value": "appleiap"
}
],
"description": "支付供应商 如 wxpay alipay 参考 https://uniapp.dcloud.net.cn/api/plugins/provider.html#"
},
"provider_pay_type": {
"title": "支付方式",
"bsonType": "string",
"description": "支付供应商的支付类型(插件内部标记支付类型的标识,不需要用户传)",
"trim": "both"
},
"uni_platform": {
"title": "应用平台",
"bsonType": "string",
"description": "uni客户端平台,如:web、mp-weixin、mp-alipay、app等",
"trim": "both"
},
"status": {
"title": "订单状态",
"bsonType": "int",
"enum": [{
"text": "已关闭",
"value": -1
},
{
"text": "未支付",
"value": 0
},
{
"text": "已支付",
"value": 1
},
{
"text": "已部分退款",
"value": 2
},
{
"text": "已全额退款",
"value": 3
}
],
"description": "订单状态 -1 已关闭 0:未支付 1:已支付 2:已部分退款 3:已全额退款",
"defaultValue": 0
},
"type": {
"title": "订单类型",
"bsonType": "string",
"description": "订单类型 goods:订单付款 recharge:余额充值付款 vip:vip充值付款 等等,可自定义",
"trim": "both"
},
"order_no": {
"title": "业务系统订单号",
"bsonType": "string",
"minLength": 20,
"maxLength": 28,
"description": "业务系统订单号,控制在20-28位(不可以是24位,24位在阿里云空间可能会有问题,可重复,代表1个业务订单会有多次付款的情况)",
"trim": "both"
},
"out_trade_no": {
"title": "支付插件订单号",
"bsonType": "string",
"description": "支付插件订单号(需控制唯一,不传则由插件自动生成)",
"trim": "both"
},
"transaction_id": {
"title": "交易单号",
"bsonType": "string",
"description": "交易单号(支付平台订单号,由支付平台控制唯一)",
"trim": "both"
},
"user_id": {
"title": "用户ID",
"bsonType": "string",
"description": "用户id,参考uni-id-users表",
"foreignKey": "uni-id-users._id"
},
"nickname": {
"title": "用户昵称",
"bsonType": "string",
"description": "用户昵称冗余",
"trim": "both"
},
"device_id": {
"bsonType": "string",
"description": "客户端设备ID"
},
"client_ip": {
"title": "客户端IP",
"bsonType": "string",
"description": "创建支付的客户端ip",
"trim": "both"
},
"openid": {
"title": "openid",
"bsonType": "string",
"description": "发起支付的用户openid",
"trim": "both"
},
"description": {
"title": "支付描述",
"bsonType": "string",
"description": "支付描述,如:uniCloud个人版包月套餐",
"trim": "both"
},
"err_msg": {
"title": "支付失败原因",
"bsonType": "string",
"description": "支付失败原因",
"trim": "both"
},
"total_fee": {
"title": "订单总金额",
"bsonType": "int",
"description": "订单总金额,单位为分,100等于1元"
},
"refund_fee": {
"title": "订单总退款金额",
"bsonType": "int",
"description": "订单总退款金额,单位为分,100等于1元"
},
"refund_count": {
"title": "当前退款笔数",
"bsonType": "int",
"description": "当前退款笔数 (退款单号为 out_trade_no-refund_count)"
},
"refund_list": {
"title": "退款详情",
"bsonType": "array",
"description": "退款详情"
},
"provider_appid": {
"title": "开放平台appid",
"bsonType": "string",
"description": "公众号appid,小程序appid,app开放平台appid 等",
"trim": "both"
},
"appid": {
"title": "DCloud AppId",
"bsonType": "string",
"description": "dcloud_appid",
"trim": "both"
},
"user_order_success": {
"title": "回调状态",
"bsonType": "bool",
"description": "用户异步通知逻辑是否全部执行完成,且无异常(建议前端通过此参数是否为true来判断是否支付成功)"
},
"custom": {
"title": "自定义数据",
"bsonType": "object",
"description": "自定义数据(用户自定义数据)"
},
"original_data": {
"title": "异步通知原始数据",
"bsonType": "object",
"description": "异步回调通知返回的原始数据,微信v2是xml转json后的数据,微信v3和支付宝是原始json"
},
"create_date": {
"title": "创建时间",
"bsonType": "timestamp",
"description": "创建时间",
"forceDefaultValue": {
"$env": "now"
}
},
"pay_date": {
"title": "支付时间",
"bsonType": "timestamp",
"description": "支付时间"
},
"notify_date": {
"title": "异步通知时间",
"bsonType": "timestamp",
"description": "订单通知支付成功时间"
},
"cancel_date": {
"title": "取消时间",
"bsonType": "timestamp",
"description": "订单取消时间"
},
"stat_data": {
"title": "uni统计相关数据",
"bsonType": "object",
"description": "uni统计相关数据",
"properties": {
"platform": {
"bsonType": "string",
"description": "与uni_platform唯一区别是APP区分 android 和 ios"
},
"app_version": {
"bsonType": "string",
"description": "客户端版本号 (字符串形式)如1.0.0"
},
"app_version_code": {
"bsonType": "string",
"description": "客户端版本号(数字形式) 如100"
},
"app_wgt_version": {
"bsonType": "string",
"description": "客户端热更新版本号"
},
"os": {
"bsonType": "string",
"description": "设备的操作系统 如 android ios"
},
"ua": {
"bsonType": "string",
"description": "客户端userAgent"
},
"channel": {
"bsonType": "string",
"description": "客户端渠道"
},
"scene": {
"bsonType": "string",
"description": "小程序场景值"
}
}
}
}
}
// 活跃设备表
{
"bsonType": "object",
"description": "给周月维度的设备基础统计和留存统计提供数据,每日跑批合并,仅添加本周/本月首次访问的设备",
"required": [],
"permission": {
"read": false,
"create": false,
"update": false,
"delete": false
},
"properties": {
"_id": {
"description": "ID,系统自动生成"
},
"appid": {
"bsonType": "string",
"description": "应用ID"
},
"platform_id": {
"bsonType": "string",
"description": "应用平台ID,对应uni-stat-app-platforms._id",
"foreignKey": "uni-stat-app-platforms._id"
},
"channel_id": {
"bsonType": "string",
"description": "渠道\/场景值ID,对应uni-stat-app-channels._id",
"foreignKey": "uni-stat-app-channels._id"
},
"version_id": {
"bsonType": "string",
"description": "应用版本ID,对应opendb-app-versions._id",
"foreignKey": "opendb-app-versions._id"
},
"device_id": {
"bsonType": "string",
"description": "客户端携带的设备标识"
},
"is_new": {
"bsonType": "int",
"description": "是否为新设备",
"defaultValue": 0,
"enum": [{
"text": "否",
"value": 0
}, {
"text": "是",
"value": 1
}]
},
"dimension": {
"bsonType": "string",
"description": "时间范围 week:周,month:月",
"enum": [{
"text": "月",
"value": "month"
}, {
"text": "周",
"value": "week"
}]
},
"create_time": {
"bsonType": "timestamp",
"description": "创建时间"
}
}
}
\ No newline at end of file
// 活跃用户表
{
"bsonType": "object",
"description": "给周月维度的用户基础统计和留存统计提供数据,每日跑批合并,仅添加本周/本月首次访问的用户。",
"required": [],
"permission": {
"read": false,
"create": false,
"update": false,
"delete": false
},
"properties": {
"_id": {
"description": "ID,系统自动生成"
},
"appid": {
"bsonType": "string",
"description": "应用ID"
},
"platform_id": {
"bsonType": "string",
"description": "应用平台ID,对应uni-stat-app-platforms._id",
"foreignKey": "uni-stat-app-platforms._id"
},
"channel_id": {
"bsonType": "string",
"description": "渠道\/场景值ID,对应uni-stat-app-channels._id",
"foreignKey": "uni-stat-app-channels._id"
},
"version_id": {
"bsonType": "string",
"description": "应用版本ID,对应opendb-app-versions._id",
"foreignKey": "opendb-app-versions._id"
},
"uid": {
"bsonType": "string",
"description": "用户编号, 对应uni-id-users._id"
},
"dimension": {
"bsonType": "string",
"description": "时间范围 week:周,month:月",
"enum": [{
"text": "月",
"value": "month"
}, {
"text": "周",
"value": "week"
}]
},
"create_time": {
"bsonType": "timestamp",
"description": "创建时间"
}
}
}
\ No newline at end of file
// 应用渠道表
{
"bsonType": "object",
"description": "提供渠道和场景值数据",
"required": [],
"permission": {
"read": "'READ_UNI_STAT_APP_CHANNELS' in auth.permission",
"create": false,
"update": false,
"delete": false
},
"properties": {
"_id": {
"description": "ID,系统自动生成"
},
"appid": {
"bsonType": "string",
"description": "统计应用ID,对应opendb-app-list.appid",
"foreignKey": "opendb-app-list.appid"
},
"platform_id": {
"bsonType": "string",
"description": "应用平台,对应uni-stat-app-platforms._id",
"foreignKey": "uni-stat-app-platforms._id"
},
"channel_code": {
"bsonType": "int",
"description": "客户端上报的渠道代码"
},
"channel_name": {
"bsonType": "string",
"description": "渠道名称,用户可编辑"
},
"create_time": {
"bsonType": "timestamp",
"description": "创建时间"
},
"last_modify_time": {
"bsonType": "timestamp",
"description": "最后修改时间"
}
}
}
// 原生应用崩溃日志表
{
"bsonType": "object",
"description": "记录原生应用的崩溃日志",
"required": [],
"permission": {
"read": "'READ_UNI_STAT_APP_CRASH_LOGS' in auth.permission",
"create": false,
"update": false,
"delete": false
},
"properties": {
"_id": {
"description": "ID,系统自动生成"
},
"appid": {
"bsonType": "string",
"description": "用户端上报的应用ID"
},
"version": {
"bsonType": "string",
"description": "用户端上报的应用版本号。manifest.json中的version->name的值"
},
"platform": {
"bsonType": "string",
"description": "用户端上报的平台code"
},
"channel": {
"bsonType": "string",
"description": "用户端上报的渠道code\/场景值"
},
"sdk_version": {
"bsonType": "string",
"description": "基础库版本号"
},
"device_id": {
"bsonType": "string",
"description": "客户端携带的设备标识"
},
"device_net": {
"bsonType": "string",
"description": "设备网络型号wifi\/3G\/4G\/"
},
"device_os": {
"bsonType": "string",
"description": "系统版本:iOS平台为系统版本号,如15.1;Android平台为API等级,如30"
},
"device_os_version": {
"bsonType": "string",
"description": "系统版本名称:iOS平台与os字段一致;Android平台为版本名称,如5.1.1"
},
"device_vendor": {
"bsonType": "string",
"description": "设备供应商 "
},
"device_model": {
"bsonType": "string",
"description": "设备型号"
},
"device_is_root": {
"bsonType": "int",
"description": "是否root:1表示root;0表示未root"
},
"device_os_name": {
"bsonType": "string",
"description": "系统名称:用于区别Android和鸿蒙,仅Android支持"
},
"device_batt_level": {
"bsonType": "int",
"description": "设备电池电量:取值范围0-100,仅Android支持"
},
"device_batt_temp": {
"bsonType": "string",
"description": "电池温度,仅Android支持"
},
"device_memory_use_size": {
"bsonType": "int",
"description": "系统已使用内存,单位为Byte,仅Android支持"
},
"device_memory_total_size": {
"bsonType": "int",
"description": "系统总内存,单位为Byte,仅Android支持"
},
"device_disk_use_size": {
"bsonType": "int",
"description": "系统磁盘已使用大小,单位为Byte,仅Android支持"
},
"device_disk_total_size": {
"bsonType": "int",
"description": "系统磁盘总大小,单位为Byte,仅Android支持"
},
"device_abis": {
"bsonType": "string",
"description": "设备支持的CPU架构:多个使用,分割,如arm64-v8a,armeabi-v7a,armeabi,仅Android支持"
},
"app_count": {
"bsonType": "int",
"description": "运行的app个数:包括运行的uni小程序数目。独立App时值为1"
},
"app_use_memory_size": {
"bsonType": "int",
"description": "APP使用的内存量,单位为Byte"
},
"app_webview_count": {
"bsonType": "int",
"description": "打开Webview窗口的个数"
},
"app_use_duration": {
"bsonType": "int",
"description": "APP使用时长:单位为s"
},
"app_run_fore": {
"bsonType": "int",
"description": "是否前台运行:1表示前台运行,0表示后台运行"
},
"package_name": {
"bsonType": "string",
"description": "原生应用包名"
},
"package_version": {
"bsonType": "string",
"description": "Android的apk版本名称;iOS的ipa版本名称"
},
"page_url": {
"bsonType": "string",
"description": "页面url"
},
"error_msg": {
"bsonType": "string",
"description": "错误信息"
},
"create_time": {
"bsonType": "timestamp",
"description": "客户端记录到的崩溃时间"
}
}
}
\ No newline at end of file
// 应用平台表
{
"bsonType": "object",
"description": "提供应用的平台字典",
"required": [],
"permission": {
"read": "'READ_UNI_STAT_APP_PLATFORMS' in auth.permission",
"create": false,
"update": false,
"delete": false
},
"properties": {
"_id": {
"description": "ID,系统自动生成"
},
"code": {
"bsonType": "string",
"description": "平台代码,前端上报"
},
"name": {
"bsonType": "string",
"description": "平台名称,管理端显示"
},
"order": {
"bsonType": "int",
"description": "序号,前端页面排序使用",
"defaultValue": 0
},
"enable": {
"bsonType": "bool",
"description": "是否启动",
"defaultValue": true,
"enum": [{
"text": "否",
"value": false
}, {
"text": "是",
"value": true
}]
},
"create_time": {
"bsonType": "timestamp",
"description": "创建时间"
}
}
}
{
"bsonType": "object",
"description": "提供应用的版本号字典",
"required": [],
"permission": {
"read": false,
"create": false,
"update": false,
"delete": false
},
"properties": {
"_id": {
"description": "ID,系统自动生成"
},
"appid": {
"bsonType": "string",
"description": "统计应用ID,对应opendb-app-list.appid",
"foreignKey": "opendb-app-list.appid"
},
"platform_id": {
"bsonType": "string",
"description": "应用平台,对应uni-stat-app-platforms._id",
"foreignKey": "uni-stat-app-platforms._id"
},
"version": {
"bsonType": "string",
"description": "应用版本"
},
"create_time": {
"bsonType": "timestamp",
"description": "创建时间"
},
"last_modify_time": {
"bsonType": "timestamp",
"description": "最后修改时间"
}
}
}
// 应用错误日志表
{
"bsonType": "object",
"description": "记录上报的应用运行错误日志",
"required": [],
"permission": {
"read": "'READ_UNI_STAT_ERROR_LOGS' in auth.permission",
"create": false,
"update": false,
"delete": false
},
"properties": {
"_id": {
"description": "ID,系统自动生成"
},
"appid": {
"bsonType": "string",
"description": "用户端上报的应用ID"
},
"version": {
"bsonType": "string",
"description": "用户端上报的应用版本号"
},
"platform": {
"bsonType": "string",
"description": "用户端上报的平台code"
},
"channel": {
"bsonType": "string",
"description": "用户端上报的渠道code\/场景值"
},
"error_type": {
"bsonType": "int",
"description": "错误类型",
"defaultValue": 0,
"enum": [{
"text": "未知",
"value": 0
}, {
"text": "表示webview页面js异常(uni-app项目对应vue页面)",
"value": 2
}, {
"text": "表示uni框架js异常(仅uni-app项目)",
"value": 4
}, {
"text": "表示控制页js异常(仅uni-app项目)",
"value": 5
}, {
"text": "表示nvue页面js异常(仅uni-app项目)",
"value": 6
}]
},
"device_id": {
"bsonType": "string",
"description": "客户端携带的设备标识"
},
"uid": {
"bsonType": "string",
"description": "用户编号, 对应uni-id-users._id"
},
"os": {
"bsonType": "string",
"description": "客户端操作系统"
},
"ua": {
"bsonType": "string",
"description": "客户端user-agent信息"
},
"space_id": {
"bsonType": "string",
"description": "服务空间编号"
},
"space_provider": {
"bsonType": "string",
"description": "服务空间提供商"
},
"sdk_version": {
"bsonType": "string",
"description": "小程序基础库版本号"
},
"platform_version": {
"bsonType": "string",
"description": "微信、支付宝宿主App的版本号"
},
"error_msg": {
"bsonType": "string",
"description": "错误信息"
},
"error_hash": {
"bsonType": "string",
"description": "错误hash码"
},
"page_url": {
"bsonType": "string",
"description": "页面url"
},
"create_time": {
"bsonType": "timestamp",
"description": "创建时间"
}
}
}
// 错误数据统计结果表
{
"bsonType": "object",
"description": "存储汇总的错误日志的数据",
"required": [],
"permission": {
"read": "'READ_UNI_STAT_ERROR_RESULT' in auth.permission",
"create": false,
"update": false,
"delete": false
},
"properties": {
"_id": {
"description": "ID,系统自动生成"
},
"appid": {
"bsonType": "string",
"description": "应用ID"
},
"platform_id": {
"bsonType": "string",
"description": "应用平台ID,对应uni-stat-app-platforms._id",
"foreignKey": "uni-stat-app-platforms._id"
},
"channel_id": {
"bsonType": "string",
"description": "渠道\/场景值ID,对应uni-stat-app-channels._id",
"foreignKey": "uni-stat-app-channels._id"
},
"version_id": {
"bsonType": "string",
"description": "应用版本ID,对应opendb-app-versions._id",
"foreignKey": "opendb-app-versions._id"
},
"type": {
"bsonType": "string",
"description": "错误类型",
"enum": [{
"text": "前端js错误",
"value": "js"
}, {
"text": "原生应用崩溃错误",
"value": "crash"
}]
},
"hash": {
"bsonType": "string",
"description": "错误hash码"
},
"msg": {
"bsonType": "string",
"description": "错误信息"
},
"count": {
"bsonType":"int",
"description":"报错次数"
},
"app_launch_count": {
"bsonType": "int",
"description": "本时间段App启动或从后台切到前台的次数"
},
"last_time": {
"bsonType":"timestamp",
"description":"最近一次报错事件"
},
"dimension": {
"bsonType": "string",
"description": "统计范围 day:按天统计,hour:按小时统计",
"enum": [{
"text": "月",
"value": "month"
}, {
"text": "周",
"value": "week"
},{
"text": "天",
"value": "day"
}, {
"text": "小时",
"value": "hour"
}]
},
"stat_date":{
"bsonType":"int",
"description":"统计日期,格式yyyymmdd,例:20211201"
},
"start_time":{
"bsonType":"timestamp",
"description":"开始时间"
},
"end_time":{
"bsonType":"timestamp",
"description":"结束时间"
}
}
}
\ No newline at end of file
// 应用事件日志表
{
"bsonType": "object",
"description": "记录上报的事件日志",
"required": [],
"permission": {
"read": "'READ_UNI_STAT_EVENT_LOGS' in auth.permission",
"create": false,
"update": false,
"delete": false
},
"properties": {
"_id": {
"description": "ID,系统自动生成"
},
"appid": {
"bsonType": "string",
"description": "客户端上报的应用ID"
},
"version": {
"bsonType": "string",
"description": "客户端上报的应用版本号"
},
"platform": {
"bsonType": "string",
"foreignKey": "uni-stat-app-platforms.code",
"description": "客户端上报的平台code"
},
"channel": {
"bsonType": "string",
"description": "客户端上报的渠道code\/场景值"
},
"device_id": {
"bsonType": "string",
"description": "客户端携带的设备标识"
},
"uid": {
"bsonType": "string",
"description": "用户编号, 对应uni-id-users._id"
},
"session_id": {
"bsonType": "string",
"description": "访问会话日志ID,对应uni-stat-session-logs._id",
"foreignKey": "uni-stat-session-logs._id"
},
"page_id": {
"bsonType": "string",
"description": "页面ID,对应uni-stat-pages._id",
"foreignKey": "uni-stat-pages._id"
},
"event_key": {
"bsonType": "string",
"description": "客户端上报的key"
},
"param": {
"bsonType": "string",
"description": "事件参数"
},
"sdk_version": {
"bsonType": "string",
"description": "基础库版本号"
},
"platform_version": {
"bsonType": "string",
"description": "平台版本,如微信、支付宝宿主App版本号"
},
"device_os": {
"bsonType": "int",
"description": "设备系统编号,1:安卓,2:iOS,3:PC"
},
"device_os_version": {
"bsonType": "string",
"description": "设备系统版本"
},
"device_net": {
"bsonType": "string",
"description": "设备网络型号wifi\/3G\/4G\/"
},
"device_vendor": {
"bsonType": "string",
"description": "设备供应商 "
},
"device_model": {
"bsonType": "string",
"description": "设备型号"
},
"device_language": {
"bsonType": "string",
"description": "设备语言包"
},
"device_pixel_ratio": {
"bsonType": "string",
"description": "设备像素比 "
},
"device_window_width": {
"bsonType": "string",
"description": "设备窗口宽度 "
},
"device_window_height": {
"bsonType": "string",
"description": "设备窗口高度"
},
"device_screen_width": {
"bsonType": "string",
"description": "设备屏幕宽度"
},
"device_screen_height": {
"bsonType": "string",
"description": "设备屏幕高度"
},
"create_time": {
"bsonType": "timestamp",
"description": "创建时间"
}
}
}
// 事件统计结果表
{
"bsonType": "object",
"description": "存储汇总的事件日志的数据",
"required": [],
"permission": {
"read": "'READ_UNI_STAT_EVENT_RESULT' in auth.permission",
"create": false,
"update": false,
"delete": false
},
"properties": {
"_id": {
"description": "ID,系统自动生成"
},
"appid": {
"bsonType": "string",
"description": "应用ID"
},
"platform_id": {
"bsonType": "string",
"description": "应用平台ID,对应uni-stat-app-platforms._id",
"foreignKey": "uni-stat-app-platforms._id"
},
"channel_id": {
"bsonType": "string",
"description": "渠道\/场景值ID,对应uni-stat-app-channels._id",
"foreignKey": "uni-stat-app-channels._id"
},
"version_id": {
"bsonType": "string",
"description": "应用版本ID,对应opendb-app-versions._id",
"foreignKey": "opendb-app-versions._id"
},
"event_key": {
"bsonType": "string",
"description": "事件key,对应uni-stat-events.event_key",
"foreignKey": "uni-stat-events.event_key"
},
"event_count": {
"bsonType": "int",
"description": "触发次数"
},
"device_count": {
"bsonType": "int",
"description": "触发该事件的设备数"
},
"user_count": {
"bsonType": "int",
"description": "触发该事件的用户数"
},
"dimension": {
"bsonType": "string",
"description": "统计范围 day:按天统计,hour:按小时统计",
"enum": [{
"text": "月",
"value": "month"
}, {
"text": "周",
"value": "week"
}, {
"text": "天",
"value": "day"
}, {
"text": "小时",
"value": "hour"
}]
},
"stat_date": {
"bsonType": "int",
"description": "统计日期,格式yyyymmdd,例:20211201"
},
"start_time": {
"bsonType": "timestamp",
"description": "开始时间"
},
"end_time": {
"bsonType": "timestamp",
"description": "结束时间"
}
}
}
// 应用事件表
{
"bsonType": "object",
"description": "提供应用的事件字典",
"required": [],
"permission": {
"read": "'READ_UNI_STAT_EVENTS' in auth.permission",
"create": false,
"update": false,
"delete": false
},
"properties": {
"_id": {
"description": "ID,系统自动生成"
},
"appid": {
"bsonType": "string",
"description": "统计应用ID,对应opendb-app-list.appid",
"foreignKey": "opendb-app-list.appid"
},
"event_key": {
"bsonType": "string",
"description": "事件键值"
},
"event_name": {
"bsonType": "string",
"description": "事件名称"
},
"create_time": {
"bsonType": "timestamp",
"description": "创建时间"
},
"update_time": {
"bsonType": "timestamp",
"description": "last_modify_time"
}
}
}
// 用户忠诚度统计表
{
"bsonType": "object",
"description": "存储汇总的设备/用户的粘性数据",
"required": [],
"permission": {
"read": "'READ_UNI_STAT_LOYALTY_RESULT' in auth.permission",
"create": false,
"update": false,
"delete": false
},
"properties": {
"_id": {
"description": "ID,系统自动生成"
},
"appid": {
"bsonType": "string",
"description": "应用ID"
},
"platform_id": {
"bsonType": "string",
"description": "应用平台ID,对应uni-stat-app-platforms._id",
"foreignKey": "uni-stat-app-platforms._id"
},
"channel_id": {
"bsonType": "string",
"description": "渠道\/场景值ID,对应uni-stat-app-channels._id",
"foreignKey": "uni-stat-app-channels._id"
},
"version_id": {
"bsonType": "string",
"description": "应用版本ID,对应opendb-app-versions._id",
"foreignKey": "opendb-app-versions._id"
},
"visit_depth_data": {
"bsonType": "object",
"description": "访问深度数据",
"properties": {
"visit_users": {
"bsonType": "object",
"description": "访问用户数"
},
"visit_devices": {
"bsonType": "object",
"description": "访问设备数"
},
"visit_times": {
"bsonType": "object",
"description": "访问次数"
}
}
},
"duration_data": {
"bsonType": "object",
"description": "访问时长数据",
"properties": {
"visit_users": {
"bsonType": "object",
"description": "访问用户数"
},
"visit_devices": {
"bsonType": "object",
"description": "访问设备数"
},
"visit_times": {
"bsonType": "object",
"description": "访问次数"
}
}
},
"stat_date": {
"bsonType": "int",
"description": "统计日期,格式yyyymmdd,例:20211201"
},
"start_time": {
"bsonType": "timestamp",
"description": "开始时间"
},
"end_time": {
"bsonType": "timestamp",
"description": "结束时间"
}
}
}
// 小程序场景值对照表
{
"bsonType": "object",
"description": "提供应用渠道和小程序场景值的数据字典",
"required": [],
"permission": {
"read": false,
"create": false,
"update": false,
"delete": false
},
"properties": {
"_id": {
"description": "ID,系统自动生成"
},
"platform": {
"bsonType": "string",
"description": "应用平台,对应uni-stat-app-platforms.code",
"foreignKey": "uni-stat-app-platforms.code"
},
"scene_code": {
"bsonType": "string",
"description": "场景代码"
},
"scene_name": {
"bsonType": "string",
"description": "场景名称"
},
"create_time": {
"bsonType": "timestamp",
"description": "创建时间"
}
}
}
\ No newline at end of file
// 应用页面访问日志表
{
"bsonType": "object",
"description": "记录上报的页面访问日志",
"required": [],
"permission": {
"read": "'READ_UNI_STAT_PAGE_LOGS' in auth.permission",
"create": false,
"update": false,
"delete": false
},
"properties": {
"_id": {
"description": "ID,系统自动生成"
},
"appid": {
"bsonType": "string",
"description": "应用ID"
},
"version": {
"bsonType": "string",
"description": "用户端上报的应用版本号"
},
"platform": {
"bsonType": "string",
"description": "用户端上报的平台code"
},
"channel": {
"bsonType": "string",
"description": "用户端上报的渠道code\/场景值"
},
"device_id": {
"bsonType": "string",
"description": "客户端携带的设备标识"
},
"uid": {
"bsonType": "string",
"description": "用户编号, 对应uni-id-users._id"
},
"session_id": {
"bsonType": "string",
"description": "访问会话日志ID,对应uni-stat-session-logs._id",
"foreignKey": "uni-stat-session-logs._id"
},
"page_id": {
"bsonType": "string",
"description": "当前页面ID,对应uni-stat-pages._id",
"foreignKey": "uni-stat-pages._id"
},
"previous_page_id": {
"bsonType": "string",
"description": "上级页面ID,为空表示第一个页面, 对应uni-stat-pages._id"
},
"previous_page_duration": {
"bsonType": "int",
"description": "上级页面停留时间,单位秒,前端上报"
},
"previous_page_is_entry": {
"bsonType": "int",
"defaultValue": 0,
"description": " 上级页面是否为入口页, 0否 1是",
"enum": [{
"text": "否",
"value": 0
}, {
"text": "是",
"value": 1
}]
},
"query_string": {
"bsonType": "string",
"description": "页面参数"
},
"create_time": {
"bsonType": "timestamp",
"description": "创建时间"
}
}
}
// 页面统计结果表
{
"bsonType": "object",
"description": "存储汇总的页面访问日志的数据",
"required": [],
"permission": {
"read": "'READ_UNI_STAT_PAGE_RESULT' in auth.permission",
"create": false,
"update": false,
"delete": false
},
"properties": {
"_id": {
"description": "ID,系统自动生成"
},
"appid": {
"bsonType": "string",
"description": "应用ID"
},
"platform_id": {
"bsonType": "string",
"description": "应用平台ID,对应uni-stat-app-platforms._id",
"foreignKey": "uni-stat-app-platforms._id"
},
"channel_id": {
"bsonType": "string",
"description": "渠道\/场景值ID,对应uni-stat-app-channels._id",
"foreignKey": "uni-stat-app-channels._id"
},
"version_id": {
"bsonType": "string",
"description": "应用版本ID,对应opendb-app-versions._id",
"foreignKey": "opendb-app-versions._id"
},
"page_id": {
"bsonType": "string",
"description": "页面表ID,对应页面表ID,对应uni-stat-pages._id",
"foreignKey": "uni-stat-pages._id"
},
"visit_times": {
"bsonType": "int",
"description": "访问次数"
},
"visit_devices": {
"bsonType": "int",
"description": "访问设备数"
},
"exit_times": {
"bsonType": "int",
"description": "退出次数"
},
"duration": {
"bsonType": "int",
"description": "访问总时长,单位秒"
},
"share_count": {
"bsonType": "int",
"description": "分享次数"
},
"entry_devices": {
"bsonType": "int",
"description": "当前页作为入口页的设备数"
},
"entry_users": {
"bsonType": "int",
"description": "当前页作为入口页的用户数"
},
"entry_count": {
"bsonType": "int",
"description": "当前页作为入口页的总次数"
},
"entry_duration": {
"bsonType": "int",
"description": "当前页作为入口时,本页面的总访问时长,单位秒"
},
"bounce_times": {
"bsonType": "int",
"description": "跳出次数"
},
"bounce_rate": {
"bsonType": "double",
"description": "跳出率"
},
"dimension": {
"bsonType": "string",
"description": "统计范围 day:按天统计,hour:按小时统计",
"enum": [{
"text": "月",
"value": "month"
}, {
"text": "周",
"value": "week"
}, {
"text": "天",
"value": "day"
}, {
"text": "小时",
"value": "hour"
}]
},
"stat_date": {
"bsonType": "int",
"description": "统计日期,格式yyyymmdd,例:20211201"
},
"start_time": {
"bsonType": "timestamp",
"description": "开始时间"
},
"end_time": {
"bsonType": "timestamp",
"description": "结束时间"
}
}
}
// 应用页面表
{
"bsonType": "object",
"description": "提供应用的页面字典",
"required": [],
"permission": {
"read": "'READ_UNI_STAT_PAGES' in auth.permission",
"create": false,
"update": false,
"delete": false
},
"properties": {
"_id": {
"description": "ID,系统自动生成"
},
"appid": {
"bsonType": "string",
"description": "统计应用ID,对应opendb-app-list.appid",
"foreignKey": "opendb-app-list.appid"
},
"path": {
"bsonType": "string",
"description": "页面路径,如`\/pages\/index\/index`"
},
"title": {
"bsonType": "string",
"description": "页面标题"
},
"create_time": {
"bsonType": "timestamp",
"description": "创建时间"
}
}
}
{
"bsonType": "object",
"description": "存储统计汇总的支付数据",
"required": [],
"permission": {
"read": false,
"create": false,
"update": false,
"delete": false
},
"properties": {
"_id": {
"description": "ID,系统自动生成"
},
"appid": {
"bsonType": "string",
"description": "应用ID,对应opendb-app-list.appid",
"foreignKey": "opendb-app-list.appid"
},
"platform_id": {
"bsonType": "string",
"description": "应用平台ID,对应uni-stat-app-platforms._id",
"foreignKey": "uni-stat-app-platforms._id"
},
"channel_id": {
"bsonType": "string",
"description": "渠道/场景值ID,对应uni-stat-app-channels._id",
"foreignKey": "uni-stat-app-channels._id"
},
"version_id": {
"bsonType": "string",
"description": "应用版本ID,对应opendb-app-versions._id",
"foreignKey": "opendb-app-versions._id"
},
"pay_total_amount": {
"bsonType": "int",
"description": "支付金额:统计时间内,成功支付的订单金额之和(不剔除退款订单)。单位分。"
},
"pay_order_count": {
"bsonType": "int",
"description": "支付笔数:统计时间内,成功支付的订单数,一个订单对应唯一一个订单号。(不剔除退款订单。)"
},
"pay_user_count": {
"bsonType": "int",
"description": "支付人数:统计时间内,成功支付的人数(不剔除退款订单)。"
},
"pay_device_count": {
"bsonType": "int",
"description": "支付设备数:统计时间内,成功支付的设备数(不剔除退款订单)。"
},
"create_total_amount": {
"bsonType": "int",
"description": "下单金额:统计时间内,成功下单的订单金额(不剔除退款订单)。单位分。"
},
"create_order_count": {
"bsonType": "int",
"description": "下单笔数:统计时间内,成功下单的订单笔数(不剔除退款订单)。"
},
"create_user_count": {
"bsonType": "int",
"description": "下单人数:统计时间内,成功下单的客户数,一人多次下单记为一人(不剔除退款订单)。"
},
"create_device_count": {
"bsonType": "int",
"description": "下单设备数:统计时间内,成功下单的设备数,一台设备多次访问被计为一台(不剔除退款订单)。"
},
"refund_total_amount": {
"bsonType": "int",
"description": "成功退款金额:统计时间内,成功退款的金额。以成功退款时间点为准。单位分。"
},
"refund_order_count": {
"bsonType": "int",
"description": "成功退款订单数:统计时间内,成功退款的订单数。以成功退款时间点为准。"
},
"refund_user_count": {
"bsonType": "int",
"description": "成功退款人数:统计时间内,成功退款的人数(不剔除退款订单)。"
},
"refund_device_count": {
"bsonType": "int",
"description": "成功退款设备数:统计时间内,成功退款的设备数(不剔除退款订单)。"
},
"activity_user_count": {
"bsonType": "int",
"description": "访问人数:统计时间内,访问人数,一人多次访问被计为一人(只统计已登录的用户)。"
},
"activity_device_count": {
"bsonType": "int",
"description": "访问设备数:统计时间内,访问设备数,一台设备多次访问被计为一台(包含未登录的用户)。"
},
"new_user_count": {
"bsonType": "int",
"description": "新增注册人数:统计时间内,注册人数。"
},
"new_device_count": {
"bsonType": "int",
"description": "新增新设备数:统计时间内,新设备数。"
},
"new_user_create_order_count": {
"bsonType": "int",
"description": "新用户下单人数:统计时间内,新增注册人数中下单的人数。"
},
"new_user_pay_order_count": {
"bsonType": "int",
"description": "新用户支付人数:统计时间内,新增注册人数中下成功支付的人数。"
},
"dimension": {
"bsonType": "string",
"description": "统计范围 hour:按小时统计,day:按天统计,week:按周统计,month:按月统计 quarter:按季度统计 year:按年统计",
"enum": [{
"text": "年",
"value": "year"
}, {
"text": "季度",
"value": "quarter"
}, {
"text": "月",
"value": "month"
}, {
"text": "周",
"value": "week"
}, {
"text": "天",
"value": "day"
}, {
"text": "小时",
"value": "hour"
}]
},
"create_date": {
"bsonType": "timestamp",
"description": "创建时间"
},
"start_time": {
"bsonType": "timestamp",
"description": "统计开始时间"
},
"end_time": {
"bsonType": "timestamp",
"description": "统计结束时间"
},
"stat_date": {
"bsonType": "object",
"description": "统计日期参数",
"properties": {
"date_str": {
"bsonType": "string",
"description": "如:2021-07-27"
},
"year": {
"bsonType": "int",
"description": "年"
},
"month": {
"bsonType": "int",
"description": "月"
},
"day": {
"bsonType": "int",
"description": "日"
},
"hour": {
"bsonType": "int",
"description": "时"
}
}
}
}
}
// 应用统计结果表
{
"bsonType": "object",
"description": "存储统计汇总的会话数据包括不限于设备\/用户的数量、访问量、活跃度(日活、周活、月活)、留存率(日留存、周留存、月留存)、跳出率、访问时长等数据",
"required": [],
"permission": {
"read": "'READ_UNI_STAT_RESULT' in auth.permission",
"create": false,
"update": false,
"delete": false
},
"properties": {
"_id": {
"description": "ID,系统自动生成"
},
"appid": {
"bsonType": "string",
"description": "应用ID,对应opendb-app-list.appid",
"foreignKey": "opendb-app-list.appid"
},
"platform_id": {
"bsonType": "string",
"description": "应用平台ID,对应uni-stat-app-platforms._id",
"foreignKey": "uni-stat-app-platforms._id"
},
"channel_id": {
"bsonType": "string",
"description": "渠道\/场景值ID,对应uni-stat-app-channels._id",
"foreignKey": "uni-stat-app-channels._id"
},
"version_id": {
"bsonType": "string",
"description": "应用版本ID,对应opendb-app-versions._id",
"foreignKey": "opendb-app-versions._id"
},
"total_users": {
"bsonType": "int",
"description": "历史累计总用户数"
},
"new_user_count": {
"bsonType": "int",
"description": "本时间段新增用户数"
},
"active_user_count": {
"bsonType": "int",
"description": "本时间段活跃用户数"
},
"total_devices": {
"bsonType": "int",
"description": "历史累计总设备数"
},
"new_device_count": {
"bsonType": "int",
"description": "本时间段新增设备数"
},
"user_session_times": {
"bsonType": "int",
"description": "本时间段用户的会话次数"
},
"active_device_count": {
"bsonType": "int",
"description": "本时间段活跃设备数"
},
"app_launch_count": {
"bsonType": "int",
"description": "本时间段App启动或从后台切到前台的次数"
},
"error_count": {
"bsonType": "int",
"description": "本时间段报错次数"
},
"duration": {
"bsonType": "int",
"description": "时间段内,所有会话访问总时长,单位秒"
},
"user_duration": {
"bsonType": "int",
"description": "本次登录用户的会话总时长,单位为秒"
},
"avg_device_session_time": {
"bsonType": "int",
"description": "设备的次均停留时长,单位秒"
},
"avg_device_time": {
"bsonType": "int",
"defaultValue": "设均停留时长(平均每台设备的停留时长),单位秒"
},
"avg_user_session_time": {
"bsonType": "int",
"description": "用户的次均停留时长,单位秒"
},
"avg_user_time": {
"bsonType": "int",
"defaultValue": "人均停留时长(平均每个登录用户的停留时长),单位秒"
},
"bounce_times": {
"bsonType": "int",
"description": "跳出次数"
},
"bounce_rate": {
"bsonType": "double",
"description": "跳出率"
},
"retention": {
"bsonType": "object",
"description": "留存信息",
"properties": {
"active_user": {
"bsonType": "object",
"description": "活跃用户留存信息"
},
"new_user": {
"bsonType": "object",
"description": "新增用户留存信息"
},
"active_device": {
"bsonType": "object",
"description": "活跃设备留存信息"
},
"new_device": {
"bsonType": "object",
"description": "新增设备留存信息"
}
}
},
"dimension": {
"bsonType": "string",
"description": "统计范围 day:按天统计,hour:按小时统计",
"enum": [{
"text": "月",
"value": "month"
}, {
"text": "周",
"value": "week"
}, {
"text": "天",
"value": "day"
}, {
"text": "小时",
"value": "hour"
}]
},
"stat_date": {
"bsonType": "int",
"description": "统计日期,格式yyyymmdd,例:20211201"
},
"start_time": {
"bsonType": "timestamp",
"description": "开始时间"
},
"end_time": {
"bsonType": "timestamp",
"description": "结束时间"
}
}
}
// 运行错误日志表
{
"bsonType": "object",
"description": "记录数据统计时运行出错的日志",
"required": [],
"permission": {
"read": false,
"create": false,
"update": false,
"delete": false
},
"properties": {
"_id": {
"description": "ID,系统自动生成"
},
"mod": {
"bsonType": "string",
"description": "运行模块"
},
"params": {
"bsonType": "object",
"description": "运行参数"
},
"error": {
"bsonType": "string",
"description": "错误信息"
},
"create_time": {
"bsonType": "timestamp",
"description": "创建时间"
}
}
}
// 应用会话日志表
{
"bsonType": "object",
"description": "记录设备访问时产生的会话日志",
"required": [],
"permission": {
"read": false,
"create": false,
"update": false,
"delete": false
},
"properties": {
"_id": {
"description": "ID,系统自动生成"
},
"appid": {
"bsonType": "string",
"description": "客户端上报的应用ID"
},
"version": {
"bsonType": "string",
"description": "客户端上报的应用版本号"
},
"platform": {
"bsonType": "string",
"description": "客户端上报的平台code"
},
"channel": {
"bsonType": "string",
"description": "客户端上报的渠道code\/场景值"
},
"type": {
"bsonType": "string",
"description": "会话类型",
"defaultValue": 1,
"enum": [{
"text": "正常进入上报",
"value": 1
}, {
"text": "后台进前台超时上报",
"value": 2
}, {
"text": "页面停留超时上报",
"value": 3
}]
},
"device_id": {
"bsonType": "string",
"description": "客户端携带的设备标识"
},
"last_visit_user_id": {
"bsonType": "string",
"description": "本次会话最终访问用户的ID, uni-id-users._id,客户端上报"
},
"is_first_visit": {
"bsonType": "int",
"description": "是否为首次访问",
"defaultValue": 0,
"enum": [{
"text": "否",
"value": 0
}, {
"text": "是",
"value": 1
}]
},
"first_visit_time": {
"bsonType": "timestamp",
"description": "用户首次访问时间"
},
"last_visit_time": {
"bsonType": "timestamp",
"description": "用户最后一次访问时间"
},
"total_visit_count": {
"bsonType": "int",
"description": "用户累计访问次数,客户端上报"
},
"entry_page_id": {
"bsonType": "string",
"description": "本次会话入口页面ID, 同uni-stat-pagesd"
},
"exit_page_id": {
"bsonType": "string",
"description": "本次会话退出页面ID, 同uni-stat-pagesd"
},
"page_count": {
"bsonType": "int",
"description": "本次会话浏览的页面数"
},
"event_count": {
"bsonType": "int",
"description": "本次会话产生的事件数"
},
"duration": {
"bsonType": "int",
"description": "本次会话时长,单位为秒,服务端计算"
},
"sdk_version": {
"bsonType": "string",
"description": "基础库版本号"
},
"platform_version": {
"bsonType": "string",
"description": "平台版本,如微信、支付宝宿主App版本号"
},
"device_os": {
"bsonType": "int",
"description": "设备系统编号,1:安卓,2:iOS,3:PC"
},
"device_os_version": {
"bsonType": "string",
"description": "设备系统版本"
},
"device_net": {
"bsonType": "string",
"description": "设备网络型号wifi\/3G\/4G\/"
},
"device_vendor": {
"bsonType": "string",
"description": "设备供应商 "
},
"device_model": {
"bsonType": "string",
"description": "设备型号"
},
"device_language": {
"bsonType": "string",
"description": "设备语言包"
},
"device_pixel_ratio": {
"bsonType": "string",
"description": "设备像素比 "
},
"device_window_width": {
"bsonType": "string",
"description": "设备窗口宽度 "
},
"device_window_height": {
"bsonType": "string",
"description": "设备窗口高度"
},
"device_screen_width": {
"bsonType": "string",
"description": "设备屏幕宽度"
},
"device_screen_height": {
"bsonType": "string",
"description": "设备屏幕高度"
},
"location_ip": {
"bsonType": "string",
"description": "ip"
},
"location_latitude": {
"bsonType": "double",
"description": "纬度"
},
"location_longitude": {
"bsonType": "double",
"description": "经度"
},
"location_country": {
"bsonType": "string",
"description": "国家"
},
"location_province": {
"bsonType": "string",
"description": "省份"
},
"location_city": {
"bsonType": "string",
"description": "城市"
},
"is_finish": {
"bsonType": "int",
"defaultValue": 0,
"description": "本次会话是否结束,0:否,1是",
"enum": [{
"text": "否",
"value": 0
}, {
"text": "是",
"value": 1
}]
},
"create_time": {
"bsonType": "timestamp",
"description": "创建时间"
}
}
}
// 应用分享日志表
{
"bsonType": "object",
"description": "记录触发分享事件的日志",
"required": [],
"permission": {
"read": false,
"create": false,
"update": false,
"delete": false
},
"properties": {
"_id": {
"description": "ID,系统自动生成"
},
"appid": {
"bsonType": "string",
"description": "客户端上报的应用ID"
},
"version": {
"bsonType": "string",
"description": "客户端上报的应用版本号"
},
"platform": {
"bsonType": "string",
"description": "客户端上报的平台code"
},
"channel": {
"bsonType": "string",
"description": "客户端上报的渠道code\/场景值"
},
"device_id": {
"bsonType": "string",
"description": "客户端携带的设备标识"
},
"uid": {
"bsonType": "string",
"description": "用户编号, 对应uni-id-users._id"
},
"session_id": {
"bsonType": "string",
"description": "访问会话日志ID,对应uni-stat-session-logs._id",
"foreignKey": "uni-stat-session-logs._id"
},
"page_id": {
"bsonType": "string",
"description": "当前页面ID,对应uni-stat-pagesd",
"foreignKey": "uni-stat-pagesd"
},
"create_time": {
"bsonType": "timestamp",
"description": "创建时间"
}
}
}
\ No newline at end of file
// 用户会话日志表
{
"bsonType": "object",
"description": "记录登录用户的会话日志",
"required": [],
"permission": {
"read": false,
"create": false,
"update": false,
"delete": false
},
"properties": {
"_id": {
"description": "ID,系统自动生成"
},
"appid": {
"bsonType": "string",
"description": "客户端携带的应用ID"
},
"version": {
"bsonType": "string",
"description": "客户端上报的应用版本号"
},
"platform": {
"bsonType": "string",
"description": "客户端上报的平台code"
},
"channel": {
"bsonType": "string",
"description": "客户端上报的渠道code\/场景值"
},
"session_id": {
"bsonType": "string",
"description": "访问会话日志ID,对应uni-stat-session-logs._id",
"foreignKey": "uni-stat-session-logs._id"
},
"uid": {
"bsonType": "string",
"description": "本次会话最终访问用户的ID, uni-id-users._id"
},
"last_visit_time": {
"bsonType": "timestamp",
"description": "用户最后一次访问时间"
},
"entry_page_id": {
"bsonType": "string",
"description": "本次会话入口页面ID, 同uni-stat-pagesd"
},
"exit_page_id": {
"bsonType": "string",
"description": "本次会话退出页面ID, 同uni-stat-pagesd"
},
"page_count": {
"bsonType": "int",
"description": "本次会话浏览的页面数"
},
"event_count": {
"bsonType": "int",
"description": "本次会话产生的事件数"
},
"duration": {
"bsonType": "int",
"description": "本次会话时长,单位为秒,服务端计算"
},
"is_finish": {
"bsonType": "int",
"defaultValue": 0,
"description": "本次会话是否结束,0:否,1是",
"enum": [{
"text": "否",
"value": 0
}, {
"text": "是",
"value": 1
}]
},
"create_time": {
"bsonType": "timestamp",
"description": "创建时间"
}
}
}
\ No newline at end of file
{
"passwordSecret": "passwordSecret8899",
"tokenSecret": "token8899",
"tokenExpiresIn": 7200,
"tokenExpiresThreshold": 600,
"passwordErrorLimit": 6,
"bindTokenToDevice": false,
"passwordErrorRetryTime": 3600,
"autoSetInviteCode": false,
"forceInviteCode": false,
"app": {
"tokenExpiresIn": 2592000,
"oauth": {
"weixin": {
"appid": "填写来源微信开放平台https://open.weixin.qq.com/创建的应用的appid",
"appsecret": "填写来源微信开放平台https://open.weixin.qq.com/创建的应用的appsecret"
},
"apple": {
"bundleId": "苹果开发者后台获取的bundleId"
}
}
},
"mp-weixin": {
"oauth": {
"weixin": {
"appid": "微信小程序登录所用的appid、appsecret需要在对应的小程序管理控制台获取",
"appsecret": "微信小程序后台获取的appsecret"
}
}
},
"mp-alipay": {
"oauth": {
"alipay": {
"appid": "支付宝小程序登录用到的appid、privateKey请参考支付宝小程序的文档进行设置或者获取,https://opendocs.alipay.com/open/291/105971#LDsXr",
"privateKey": "支付宝小程序登录用到的appid、privateKey请参考支付宝小程序的文档进行设置或者获取,https://opendocs.alipay.com/open/291/105971#LDsXr"
}
}
},
"service": {
"sms": {
"name": "应用名称,对应短信模版的name",
"codeExpiresIn": 300,
"smsKey": "短信密钥key,开通短信服务处可以看到",
"smsSecret": "短信密钥secret,开通短信服务处可以看到",
"sence": {
"bind-mobile": {
"templateId": "",
"codeExpiresIn": 240
}
}
},
"univerify": {
"appid": "",
"apiKey": "",
"apiSecret": ""
}
}
}
console.log('----vue.config.js----') console.log('----vue.config.js----')
process.env.UNI_CLOUD_PROVIDER = JSON.stringify([{ process.env.UNI_CLOUD_PROVIDER = JSON.stringify([{
"provider": "aliyun", //阿里云 "provider": "aliyun", //阿里云
"clientSecret": "", "clientSecret": "SEPWNQ91E3Ymgn6ThQ6u8w==",
"spaceId": "", "spaceId": "52b18b34-3a3e-4861-89a0-c362c7634787",
/* "provider": "tencent", /* "provider": "tencent",
"spaceId": "" */ "spaceId": "" */
}]) }])
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册