|
|
/**
|
|
|
* @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;
|
|
|
}
|
|
|
};
|