payResult.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539
  1. /**
  2. * @class ActiveDevices 活跃设备模型 - 每日跑批合并,仅添加本周/本月首次访问的设备。
  3. */
  4. // 导入BaseMod模块
  5. const BaseMod = require('../base')
  6. // 导入Platform模块
  7. const Platform = require('../platform')
  8. // 导入Channel模块
  9. const Channel = require('../channel')
  10. // 导入Version模块
  11. const Version = require('../version')
  12. const {
  13. DateTime,
  14. UniCrypto
  15. } = require('../../lib')
  16. // 导入dao模块
  17. const dao = require('./dao')
  18. // 获取uniCloud数据库实例
  19. let db = uniCloud.database();
  20. // 获取uniCloud数据库操作符command
  21. let _ = db.command;
  22. // 获取uniCloud数据库操作符aggregate
  23. let $ = _.aggregate;
  24. module.exports = class PayResult extends BaseMod {
  25. // 构造函数
  26. constructor() {
  27. // 调用父类的构造函数
  28. super()
  29. // 初始化平台数组
  30. this.platforms = []
  31. // 初始化渠道数组
  32. this.channels = []
  33. // 初始化版本数组
  34. this.versions = []
  35. }
  36. /**
  37. 支付金额:统计时间内,成功支付的订单金额之和(不剔除退款订单)。
  38. 支付笔数:统计时间内,成功支付的订单数,一个订单对应唯一一个订单号。(不剔除退款订单。)
  39. 支付人数:统计时间内,成功支付的人数(不剔除退款订单)。
  40. 支付设备数:统计时间内,成功支付的设备数(不剔除退款订单)。
  41. 下单金额:统计时间内,成功下单的订单金额(不剔除退款订单)。
  42. 下单笔数:统计时间内,成功下单的订单笔数(不剔除退款订单)。
  43. 下单人数:统计时间内,成功下单的客户数,一人多次下单记为一人(不剔除退款订单)。
  44. 下单设备数:统计时间内,成功下单的设备数,一台设备多次访问被计为一台(不剔除退款订单)。
  45. 访问人数:统计时间内,访问人数,一人多次访问被计为一人(只统计已登录的用户)。
  46. 访问设备数:统计时间内,访问设备数,一台设备多次访问被计为一台(包含未登录的用户)。
  47. * @desc 支付统计(按日统计)
  48. * @param {string} type 统计范围 hour:按小时统计,day:按天统计,week:按周统计,month:按月统计 quarter:按季度统计 year:按年统计
  49. * @param {date|time} date
  50. * @param {bool} reset
  51. */
  52. async stat(type, date, reset, config = {}) {
  53. if (!date) date = Date.now();
  54. // 以下是测试代码-----------------------------------------------------------
  55. //reset = true;
  56. //date = 1667318400000;
  57. // 以上是测试代码-----------------------------------------------------------
  58. let res = await this.run(type, date, reset, config); // 每小时
  59. if (type === "hour" && config.timely) {
  60. /**
  61. * 如果是小时纬度统计,则还需要再统计(今日实时数据)
  62. * 2022-11-01 01:00:00 统计的是 2022-11-01 的天维度数据(即该天0点-1点数据)
  63. * 2022-11-01 02:00:00 统计的是 2022-11-01 的天维度数据(即该天0点-2点数据)
  64. * 2022-11-01 23:00:00 统计的是 2022-11-01 的天维度数据(即该天0点-23点数据)
  65. * 2022-11-02 00:00:00 统计的是 2022-11-01 的天维度数据(即该天最终数据)
  66. * 2022-11-02 01:00:00 统计的是 2022-11-02 的天维度数据(即该天0点-1点数据)
  67. */
  68. date -= 1000 * 3600; // 需要减去1小时
  69. let tasks = [];
  70. tasks.push(this.run("day", date, true, 0)); // 今日
  71. // 以下数据每6小时刷新一次
  72. const dateTime = new DateTime();
  73. const timeInfo = dateTime.getTimeInfo(date);
  74. if ((timeInfo.nHour + 1) % 6 === 0) {
  75. tasks.push(this.run("week", date, true, 0)); // 本周
  76. tasks.push(this.run("month", date, true, 0)); // 本月
  77. tasks.push(this.run("quarter", date, true, 0)); // 本季度
  78. tasks.push(this.run("year", date, true, 0)); // 本年度
  79. }
  80. await Promise.all(tasks);
  81. }
  82. return res;
  83. }
  84. /**
  85. * @desc 支付统计
  86. * @param {string} type 统计范围 hour:按小时统计,day:按天统计,week:按周统计,month:按月统计 quarter:按季度统计 year:按年统计
  87. * @param {date|time} date 哪个时间节点计算(默认已当前时间计算)
  88. * @param {bool} reset 如果统计数据已存在,是否需要重新统计
  89. */
  90. async run(type, date, reset, offset = -1) {
  91. // 定义变量 dimension,赋值为 type
  92. let dimension = type;
  93. // 创建一个 DateTime 实例
  94. const dateTime = new DateTime();
  95. // 根据 dimension、offset 和 date 获取时间维度
  96. const dateDimension = dateTime.getTimeDimensionByType(dimension, offset, date);
  97. // 获取时间维度的起始时间,赋值给 start_time
  98. let start_time = dateDimension.startTime;
  99. // 获取时间维度的结束时间,赋值给 end_time
  100. let end_time = dateDimension.endTime;
  101. // 获取当前时间的时间戳,赋值给 runStartTime
  102. let runStartTime = Date.now();
  103. // 定义变量 debug,赋值为 true
  104. let debug = true;
  105. if (debug) {
  106. console.log(`-----------------支付统计开始(${dimension})-----------------`);
  107. console.log('本次统计时间:', dateTime.getDate('Y-m-d H:i:s', start_time), "-", dateTime.getDate('Y-m-d H:i:s', end_time))
  108. console.log('本次统计参数:', 'type:' + type, 'date:' + date, 'reset:' + reset)
  109. }
  110. this.startTime = start_time;
  111. let pubWhere = {
  112. start_time,
  113. end_time
  114. };
  115. // 查看当前时间段数据是否已存在,防止重复生成
  116. if (!reset) {
  117. // 如果 reset 为 false
  118. // 调用 dao.uniStatPayResult.list 方法获取列表
  119. let list = await dao.uniStatPayResult.list({
  120. whereJson: {
  121. ...pubWhere, // 使用 pubWhere 对象的属性作为查询条件
  122. dimension // 使用 dimension 变量作为查询条件
  123. }
  124. });
  125. // 如果列表长度大于0
  126. if (list.length > 0) {
  127. console.log('data have exists'); // 输出数据已存在的提示信息
  128. // 如果 debug 为 true
  129. if (debug) {
  130. let runEndTime = Date.now();
  131. console.log(`耗时:${((runEndTime - runStartTime ) / 1000).toFixed(3)} 秒`); // 输出耗时信息
  132. console.log(`-----------------支付统计结束(${dimension})-----------------`); // 输出支付统计结束信息
  133. }
  134. // 返回一个对象,包含 code 和 msg 属性
  135. return {
  136. code: 1003,
  137. msg: 'Pay data in this time have already existed'
  138. };
  139. }
  140. } else {
  141. // 如果 reset 为 true
  142. // 调用 dao.uniStatPayResult.del 方法删除数据
  143. let delRes = await dao.uniStatPayResult.del({
  144. whereJson: {
  145. ...pubWhere, // 使用 pubWhere 对象的属性作为删除条件
  146. dimension // 使用 dimension 变量作为删除条件
  147. }
  148. });
  149. console.log('Delete old data result:', JSON.stringify(delRes)); // 输出删除数据的结果
  150. }
  151. // 支付订单分组(已下单)
  152. let statPayOrdersList1 = await dao.uniPayOrders.group({
  153. ...pubWhere,
  154. status: "已下单"
  155. });
  156. // 支付订单分组(且已付款,含退款)
  157. let statPayOrdersList2 = await dao.uniPayOrders.group({
  158. ...pubWhere,
  159. status: "已付款"
  160. });
  161. // 支付订单分组(已退款)
  162. let statPayOrdersList3 = await dao.uniPayOrders.group({
  163. ...pubWhere,
  164. status: "已退款"
  165. });
  166. let statPayOrdersList = statPayOrdersList1.concat(statPayOrdersList2).concat(statPayOrdersList3);
  167. let res = {
  168. code: 0,
  169. msg: 'success'
  170. }
  171. // 将支付订单分组查询结果组装
  172. let statDta = {};
  173. if (statPayOrdersList.length > 0) {
  174. // 如果 statPayOrdersList 的长度大于0
  175. // 遍历 statPayOrdersList 列表
  176. for (let i = 0; i < statPayOrdersList.length; i++) {
  177. let item = statPayOrdersList[i]; // 获取当前遍历到的元素
  178. let {
  179. appid,
  180. version,
  181. platform,
  182. channel,
  183. } = item._id; // 从 _id 属性中解构出 appid、version、platform 和 channel 属性
  184. let {
  185. status_str
  186. } = item; // 解构出 status_str 属性
  187. let key = `${appid}-${version}-${platform}-${channel}`; // 拼接 key 字符串
  188. if (!statDta[key]) {
  189. // 如果 statDta 对应的 key 不存在
  190. statDta[key] = { // 创建一个新的对象,赋值给 statDta[key]
  191. appid,
  192. version,
  193. platform,
  194. channel,
  195. status: {} // 创建一个空的 status 对象
  196. };
  197. }
  198. let newItem = JSON.parse(JSON.stringify(item)); // 复制 item 对象,赋值给 newItem
  199. delete newItem._id; // 删除 newItem 中的 _id 属性
  200. statDta[key].status[status_str] = newItem; // 将 newItem 添加到 statDta[key].status 对象中
  201. }
  202. }
  203. if (this.debug) console.log('statDta: ', statDta)
  204. let saveList = [];
  205. for (let key in statDta) {
  206. let item = statDta[key];
  207. let {
  208. appid,
  209. version,
  210. platform,
  211. channel,
  212. status: statusData,
  213. } = item;
  214. if (!channel) channel = item.scene;
  215. let fieldData = {
  216. pay_total_amount: 0,
  217. pay_order_count: 0,
  218. pay_user_count: 0,
  219. pay_device_count: 0,
  220. create_total_amount: 0,
  221. create_order_count: 0,
  222. create_user_count: 0,
  223. create_device_count: 0,
  224. refund_total_amount: 0,
  225. refund_order_count: 0,
  226. refund_user_count: 0,
  227. refund_device_count: 0,
  228. };
  229. for (let status in statusData) {
  230. let statusItem = statusData[status];
  231. if (status === "已下单") {
  232. // 已下单
  233. fieldData.create_total_amount += statusItem.total_fee;
  234. fieldData.create_order_count += statusItem.order_count;
  235. fieldData.create_user_count += statusItem.user_count;
  236. fieldData.create_device_count += statusItem.device_count;
  237. } else if (status === "已付款") {
  238. // 已付款
  239. fieldData.pay_total_amount += statusItem.total_fee;
  240. fieldData.pay_order_count += statusItem.order_count;
  241. fieldData.pay_user_count += statusItem.user_count;
  242. fieldData.pay_device_count += statusItem.device_count;
  243. } else if (status === "已退款") {
  244. // 已退款
  245. fieldData.refund_total_amount += statusItem.total_fee;
  246. fieldData.refund_order_count += statusItem.order_count;
  247. fieldData.refund_user_count += statusItem.user_count;
  248. fieldData.refund_device_count += statusItem.device_count;
  249. }
  250. }
  251. // 平台信息
  252. let platformInfo = null;
  253. if (this.platforms && this.platforms[platform]) {
  254. // 从缓存中读取数据
  255. platformInfo = this.platforms[platform]
  256. } else {
  257. const platformObj = new Platform()
  258. platformInfo = await platformObj.getPlatformAndCreate(platform, null)
  259. if (!platformInfo || platformInfo.length === 0) {
  260. platformInfo._id = ''
  261. }
  262. this.platforms[platform] = platformInfo;
  263. }
  264. // 渠道信息
  265. let channelInfo = null
  266. const channelKey = appid + '_' + platformInfo._id + '_' + channel;
  267. if (this.channels && this.channels[channelKey]) {
  268. channelInfo = this.channels[channelKey];
  269. } else {
  270. const channelObj = new Channel()
  271. channelInfo = await channelObj.getChannelAndCreate(appid, platformInfo._id, channel)
  272. if (!channelInfo || channelInfo.length === 0) {
  273. channelInfo._id = ''
  274. }
  275. this.channels[channelKey] = channelInfo
  276. }
  277. // 版本信息
  278. let versionInfo = null
  279. const versionKey = appid + '_' + platform + '_' + version
  280. if (this.versions && this.versions[versionKey]) {
  281. versionInfo = this.versions[versionKey]
  282. } else {
  283. const versionObj = new Version()
  284. versionInfo = await versionObj.getVersionAndCreate(appid, platform, version)
  285. if (!versionInfo || versionInfo.length === 0) {
  286. versionInfo._id = ''
  287. }
  288. this.versions[versionKey] = versionInfo
  289. }
  290. let countWhereJson = {
  291. create_time: _.gte(start_time).lte(end_time),
  292. appid,
  293. version,
  294. platform: _.in(getUniPlatform(platform)),
  295. channel,
  296. };
  297. // 活跃设备数量
  298. let activity_device_count = await dao.uniStatSessionLogs.groupCount(countWhereJson);
  299. // 活跃用户数量
  300. let activity_user_count = await dao.uniStatUserSessionLogs.groupCount(countWhereJson);
  301. /*
  302. // TODO 此处有问题,暂不使用
  303. // 新设备数量
  304. let new_device_count = await dao.uniStatSessionLogs.groupCount({
  305. ...countWhereJson,
  306. is_first_visit: 1,
  307. });
  308. // 新注册用户数量
  309. let new_user_count = await dao.uniIdUsers.count({
  310. register_date: _.gte(start_time).lte(end_time),
  311. register_env: {
  312. appid,
  313. app_version: version,
  314. uni_platform: _.in(getUniPlatform(platform)),
  315. channel,
  316. }
  317. });
  318. // 新注册用户中下单的人数
  319. let new_user_create_order_count = await dao.uniIdUsers.countNewUserOrder({
  320. whereJson: {
  321. register_date: _.gte(start_time).lte(end_time),
  322. register_env: {
  323. appid,
  324. app_version: version,
  325. uni_platform: _.in(getUniPlatform(platform)),
  326. channel,
  327. }
  328. },
  329. status: [-1, 0]
  330. });
  331. // 新注册用户中支付成功的人数
  332. let new_user_pay_order_count = await dao.uniIdUsers.countNewUserOrder({
  333. whereJson: {
  334. register_date: _.gte(start_time).lte(end_time),
  335. register_env: {
  336. appid,
  337. app_version: version,
  338. uni_platform: _.in(getUniPlatform(platform)),
  339. channel,
  340. }
  341. },
  342. status: [1, 2, 3]
  343. }); */
  344. saveList.push({
  345. appid,
  346. platform_id: platformInfo._id,
  347. channel_id: channelInfo._id,
  348. version_id: versionInfo._id,
  349. dimension,
  350. create_date: Date.now(), // 记录下当前时间
  351. start_time,
  352. end_time,
  353. stat_date: getNowDate(start_time, 8, dimension),
  354. ...fieldData,
  355. activity_user_count,
  356. activity_device_count,
  357. // new_user_count,
  358. // new_device_count,
  359. // new_user_create_order_count,
  360. // new_user_pay_order_count,
  361. });
  362. }
  363. if (this.debug) console.log('saveList: ', saveList)
  364. //return;
  365. if (saveList.length > 0) {
  366. res = await dao.uniStatPayResult.adds(saveList);
  367. }
  368. if (debug) {
  369. let runEndTime = Date.now();
  370. console.log(`耗时:${((runEndTime - runStartTime ) / 1000).toFixed(3)} 秒`)
  371. console.log(`本次共添加:${saveList.length } 条记录`)
  372. console.log(`-----------------支付统计结束(${dimension})-----------------`);
  373. }
  374. return res
  375. }
  376. }
  377. // 获取平台
  378. function getUniPlatform(platform) {
  379. let list = [];
  380. if (["h5", "web"].indexOf(platform) > -1) {
  381. list = ["h5", "web"];
  382. } else if (["app-plus", "app"].indexOf(platform) > -1) {
  383. list = ["app-plus", "app"];
  384. } else {
  385. list = [platform];
  386. }
  387. return list;
  388. }
  389. // 获取当前时间
  390. function getNowDate(date = new Date(), targetTimezone = 8, dimension) {
  391. if (typeof date === "string" && !isNaN(date)) date = Number(date);
  392. if (typeof date == "number") {
  393. if (date.toString().length == 10) date *= 1000;
  394. date = new Date(date);
  395. }
  396. const {
  397. year,
  398. month,
  399. day,
  400. hour,
  401. minute,
  402. second
  403. } = getFullTime(date);
  404. // 现在的时间
  405. let date_str;
  406. if (dimension === "month") {
  407. date_str = timeFormat(date, "yyyy-MM", targetTimezone);
  408. } else if (dimension === "quarter") {
  409. date_str = timeFormat(date, "yyyy-MM", targetTimezone);
  410. } else if (dimension === "year") {
  411. date_str = timeFormat(date, "yyyy", targetTimezone);
  412. } else {
  413. date_str = timeFormat(date, "yyyy-MM-dd", targetTimezone);
  414. }
  415. return {
  416. date_str,
  417. year,
  418. month,
  419. day,
  420. hour,
  421. //minute,
  422. //second,
  423. };
  424. }
  425. // 获取完整的时间对象参数
  426. function getFullTime(date = new Date(), targetTimezone = 8) {
  427. if (!date) {
  428. return "";
  429. }
  430. if (typeof date === "string" && !isNaN(date)) date = Number(date);
  431. if (typeof date == "number") {
  432. if (date.toString().length == 10) date *= 1000;
  433. date = new Date(date);
  434. }
  435. const dif = date.getTimezoneOffset();
  436. const timeDif = dif * 60 * 1000 + (targetTimezone * 60 * 60 * 1000);
  437. const east8time = date.getTime() + timeDif;
  438. date = new Date(east8time);
  439. let YYYY = date.getFullYear() + '';
  440. let MM = (date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1);
  441. let DD = (date.getDate() < 10 ? '0' + (date.getDate()) : date.getDate());
  442. let hh = (date.getHours() < 10 ? '0' + (date.getHours()) : date.getHours());
  443. let mm = (date.getMinutes() < 10 ? '0' + (date.getMinutes()) : date.getMinutes());
  444. let ss = (date.getSeconds() < 10 ? '0' + (date.getSeconds()) : date.getSeconds());
  445. return {
  446. YYYY: Number(YYYY),
  447. MM: Number(MM),
  448. DD: Number(DD),
  449. hh: Number(hh),
  450. mm: Number(mm),
  451. ss: Number(ss),
  452. year: Number(YYYY),
  453. month: Number(MM),
  454. day: Number(DD),
  455. hour: Number(hh),
  456. minute: Number(mm),
  457. second: Number(ss),
  458. };
  459. };
  460. /**
  461. * 日期格式化
  462. */
  463. function timeFormat(time, fmt = 'yyyy-MM-dd hh:mm:ss', targetTimezone = 8) {
  464. try {
  465. if (!time) {
  466. return "";
  467. }
  468. if (typeof time === "string" && !isNaN(time)) time = Number(time);
  469. // 其他更多是格式化有如下:
  470. // yyyy-MM-dd hh:mm:ss|yyyy年MM月dd日 hh时MM分等,可自定义组合
  471. let date;
  472. if (typeof time === "number") {
  473. if (time.toString().length == 10) time *= 1000;
  474. date = new Date(time);
  475. } else {
  476. date = time;
  477. }
  478. const dif = date.getTimezoneOffset();
  479. const timeDif = dif * 60 * 1000 + (targetTimezone * 60 * 60 * 1000);
  480. const east8time = date.getTime() + timeDif;
  481. date = new Date(east8time);
  482. let opt = {
  483. "M+": date.getMonth() + 1, //月份
  484. "d+": date.getDate(), //日
  485. "h+": date.getHours(), //小时
  486. "m+": date.getMinutes(), //分
  487. "s+": date.getSeconds(), //秒
  488. "q+": Math.floor((date.getMonth() + 3) / 3), //季度
  489. "S": date.getMilliseconds() //毫秒
  490. };
  491. if (/(y+)/.test(fmt)) {
  492. fmt = fmt.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length));
  493. }
  494. for (let k in opt) {
  495. if (new RegExp("(" + k + ")").test(fmt)) {
  496. fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (opt[k]) : (("00" + opt[k]).substr(("" + opt[k]).length)));
  497. }
  498. }
  499. return fmt;
  500. } catch (err) {
  501. // 若格式错误,则原值显示
  502. return time;
  503. }
  504. };