admin.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. 'use strict';
  2. const { CrudService } = require('naf-framework-mongoose-free/lib/service');
  3. const { BusinessError, ErrorCode } = require('naf-core').Error;
  4. const _ = require('lodash');
  5. const assert = require('assert');
  6. const { ObjectId } = require('mongoose').Types;
  7. const moment = require('moment');
  8. // 中台统计
  9. class AdminService extends CrudService {
  10. constructor(ctx) {
  11. super(ctx, 'admin');
  12. this.goods = this.ctx.model.Shop.Goods;
  13. this.goodsSpec = this.ctx.model.Shop.GoodsSpec;
  14. this.orderModel = this.ctx.model.Trade.Order;
  15. this.orderDetailModel = this.ctx.model.Trade.OrderDetail;
  16. this.afterSaleModel = this.ctx.model.Trade.AfterSale;
  17. this.tf = 'YYYY-MM-DD';
  18. }
  19. // 待办事项
  20. async todo(query) {
  21. const notPay = await this.notPay(query);
  22. const notSend = await this.notSend(query);
  23. const notDealAfterSale = await this.notDealAfterSale(query);
  24. const stockWarning = await this.stockWarning(query);
  25. const sMarkOrder = await this.makeOrder(query);
  26. const sAfterSale = await this.makeAfterSale(query);
  27. return { notPay, notSend, notDealAfterSale, stockWarning, sMarkOrder, sAfterSale };
  28. }
  29. // 待办-库存警告
  30. async stockWarning(query) {
  31. // TODO,库存警告线需要设置
  32. const warningLine = 10;
  33. const { shop } = query;
  34. // 过滤警告线以外的数据
  35. const pipline = [{ $match: { status: '0', num: { $lte: warningLine } } }];
  36. // 因为不需要数据,节省内存,将没用的字段过滤掉
  37. pipline.push({ $project: { goods_id: { $toObjectId: '$goods' }, goodsSpec_id: { $toString: '$_id' }, num: '$num' } });
  38. // 表关联
  39. pipline.push({
  40. $lookup: {
  41. from: 'goods',
  42. localField: 'goods_id',
  43. foreignField: '_id',
  44. as: 'goods',
  45. },
  46. });
  47. // 平铺
  48. pipline.push({ $unwind: '$goods' });
  49. // 再组织数据
  50. pipline.push({ $project: { goods_id: '$goods_id', goodsSpec_id: '$goodsSpec_id', num: '$num', shop: { $toString: '$goods.shop' } } });
  51. if (ObjectId.isValid(shop)) {
  52. pipline.push({ $match: { shop } });
  53. }
  54. pipline.push(this.totalPip());
  55. const result = await this.goodsSpec.aggregate(pipline);
  56. return this.getTotal(result);
  57. }
  58. // 待办-未处理售后
  59. async notDealAfterSale(query) {
  60. const { shop } = query;
  61. const pipline = [{ $match: { status: '0' } }];
  62. if (ObjectId.isValid(shop)) {
  63. pipline.push({ $match: { shop } });
  64. }
  65. pipline.push(this.totalPip());
  66. const result = await this.afterSaleModel.aggregate(pipline);
  67. return this.getTotal(result);
  68. }
  69. // 待办-未发货
  70. async notSend(query) {
  71. const { shop } = query;
  72. const pipline = [{ $match: { status: '1' } }];
  73. if (ObjectId.isValid(shop)) {
  74. pipline.push({ $match: { shop } });
  75. }
  76. pipline.push(this.totalPip());
  77. const result = await this.orderDetailModel.aggregate(pipline);
  78. return this.getTotal(result);
  79. }
  80. // 待办-未付款订单数
  81. async notPay(query) {
  82. const { shop } = query;
  83. const pipline = [{ $match: { status: '0' } }];
  84. if (ObjectId.isValid(shop)) {
  85. pipline.push({ $match: { 'goods.shop': shop } });
  86. }
  87. pipline.push(this.totalPip());
  88. const result = await this.orderModel.aggregate(pipline);
  89. return this.getTotal(result);
  90. }
  91. /**
  92. * 统计-下单数(生成order,不管支不支付)
  93. * @param {Object} query 地址参数
  94. * @return {Array} 统计数据
  95. * @property {String} type 数据类型
  96. ** 默认为从 当天/本周/本月/本年
  97. ** custom:自定义,只查这个时间段的,每天的数
  98. ** day:单数据(一个数);
  99. ** week:指定(当前)周-每天的数; (默认)
  100. ** monthWeek:指定(当前)月-每周的数;
  101. ** monthDay:指定(当前)月-每天的数;
  102. ** yearMonth:指定(当前)年-每月的数;
  103. ** yearWeek:指定(当前)年-每周的数;
  104. ** yearDay:指定(当前)年-每天的数;
  105. * @property {Array} start 开始
  106. * @property {Array} end 结束
  107. * @property {Array} range 指定的时间范围;如果没传,则根据type自动生成
  108. * @property {String} status 订单状态(1为,支付单)
  109. */
  110. async makeOrder(query) {
  111. const pipline = [];
  112. const { type = 'week', start, end, shop, status } = query;
  113. let timeRange = [];
  114. if (type === 'custom') {
  115. const r = moment(start).isBefore(end);
  116. if (r) timeRange.push(start, end);
  117. else timeRange.push(end, start);
  118. } else timeRange = this.getDefaultRange(type);
  119. // 先用商店过滤下
  120. if (ObjectId.isValid(shop)) {
  121. pipline.push({ $match: { 'goods.shop': shop } });
  122. }
  123. if (status) {
  124. pipline.push({ $match: { status } });
  125. }
  126. // 时间格式化: create_date:日期,用来分组统计; create_time: 时间,用来过滤数据
  127. pipline.push({
  128. $addFields: {
  129. create_date: { $dateToString: { date: '$meta.createdAt', ...this.pipDateFormatDateObject() } },
  130. create_time: { $dateToString: { date: '$meta.createdAt', ...this.pipDateFormatTimeObject() } },
  131. },
  132. });
  133. pipline.push({ $match: { $and: [{ buy_time: { $gte: _.head(timeRange) } }, { buy_time: { $lte: _.last(timeRange) } }] } });
  134. pipline.push({
  135. $group: {
  136. _id: '$create_date',
  137. num: { $sum: 1 },
  138. },
  139. });
  140. const fs = await this.orderModel.aggregate(pipline);
  141. const list = this.getRangeDateList(timeRange);
  142. const result = [];
  143. for (const d of list) {
  144. const r = fs.find(f => f._id === d);
  145. const obj = { date: d, num: 0 };
  146. if (r) obj.num = r.num;
  147. result.push(obj);
  148. }
  149. return result;
  150. }
  151. /**
  152. * 售后数
  153. * @param {Object} query 查询条件
  154. */
  155. async makeAfterSale(query) {
  156. const pipline = [];
  157. const { type = 'week', start, end, shop, status } = query;
  158. let timeRange = [];
  159. if (type === 'custom') {
  160. const r = moment(start).isBefore(end);
  161. if (r) timeRange.push(start, end);
  162. else timeRange.push(end, start);
  163. } else timeRange = this.getDefaultRange(type);
  164. // 先用商店过滤下
  165. if (ObjectId.isValid(shop)) {
  166. pipline.push({ $match: { shop } });
  167. }
  168. if (status) {
  169. pipline.push({ $match: { status } });
  170. }
  171. // 时间格式化: create_date:日期,用来分组统计; create_time: 时间,用来过滤数据
  172. pipline.push({
  173. $project: {
  174. create_date: { $dateToString: { date: '$meta.createdAt', ...this.pipDateFormatDateObject() } },
  175. create_time: { $dateToString: { date: '$meta.createdAt', ...this.pipDateFormatTimeObject() } },
  176. },
  177. });
  178. pipline.push({ $match: { $and: [{ create_time: { $gte: _.head(timeRange) } }, { create_time: { $lte: _.last(timeRange) } }] } });
  179. pipline.push({
  180. $group: {
  181. _id: '$create_date',
  182. num: { $sum: 1 },
  183. },
  184. });
  185. const fs = await this.afterSaleModel.aggregate(pipline);
  186. const list = this.getRangeDateList(timeRange);
  187. const result = [];
  188. for (const d of list) {
  189. const r = fs.find(f => f._id === d);
  190. const obj = { date: d, num: 0 };
  191. if (r) obj.num = r.num;
  192. result.push(obj);
  193. }
  194. return result;
  195. }
  196. /**
  197. * 查询销售额
  198. * @param {Object} query 查询条件
  199. */
  200. async sellTotal(query) {
  201. const pipline = [];
  202. const { type = 'week', start, end, shop } = query;
  203. let timeRange = [];
  204. if (type === 'custom') {
  205. const r = moment(start).isBefore(end);
  206. if (r) timeRange.push(start, end);
  207. else timeRange.push(end, start);
  208. } else timeRange = this.getDefaultRange(type);
  209. // 先用商店过滤下
  210. if (ObjectId.isValid(shop)) {
  211. pipline.push({ $match: { 'goods.shop': shop } });
  212. }
  213. pipline.push({ $match: { status: '1' } });
  214. // 时间格式化: create_date:日期,用来分组统计; create_time: 时间,用来过滤数据
  215. pipline.push({
  216. $project: {
  217. create_date: { $dateToString: { date: '$meta.createdAt', ...this.pipDateFormatDateObject() } },
  218. create_time: { $dateToString: { date: '$meta.createdAt', ...this.pipDateFormatTimeObject() } },
  219. money: '$pay.pay_money',
  220. },
  221. });
  222. pipline.push({ $match: { $and: [{ create_time: { $gte: _.head(timeRange) } }, { create_time: { $lte: _.last(timeRange) } }] } });
  223. pipline.push({
  224. $group: {
  225. _id: '$create_date',
  226. money: { $sum: '$money' },
  227. },
  228. });
  229. const fs = await this.orderModel.aggregate(pipline);
  230. const list = this.getRangeDateList(timeRange);
  231. const result = [];
  232. for (const d of list) {
  233. const r = fs.find(f => f._id === d);
  234. const obj = { date: d, money: 0 };
  235. if (r) obj.money = this.ctx.toNumber(r.money);
  236. result.push(obj);
  237. if (type === 'day') break;
  238. }
  239. return result;
  240. }
  241. // 聚合总数管道
  242. totalPip() {
  243. return {
  244. $count: 'total',
  245. };
  246. }
  247. /**
  248. * 取出聚合查询的总数
  249. * @param {Array} data 聚合查询总数的结果($count)
  250. * @param {String} key 总数的字段
  251. * @return {Number} 返回总数
  252. */
  253. getTotal(data, key = 'total') {
  254. return _.get(_.head(data), key, 0);
  255. }
  256. /**
  257. * 管道格式化时间格式--至时分秒
  258. * @return {Object}
  259. */
  260. pipDateFormatTimeObject() {
  261. return { format: '%Y-%m-%d %H:%M:%S', timezone: '+08:00' };
  262. }
  263. /**
  264. * 管道格式化室间隔是-至日期
  265. * @return {Object}
  266. */
  267. pipDateFormatDateObject() {
  268. return { format: '%Y-%m-%d', timezone: '+08:00' };
  269. }
  270. /**
  271. * 获取时间范围内的每一天
  272. * @param {Array} range 时间范围
  273. * @return {Array} 时间范围数组
  274. */
  275. getRangeDateList(range) {
  276. const start = _.head(range);
  277. const end = _.last(range);
  278. const list = [ start ];
  279. let pd = 1;
  280. while (!moment(_.last(list)).isSame(end)) {
  281. const d = moment(start).add(pd, 'day').format(this.tf);
  282. pd++;
  283. list.push(d);
  284. }
  285. return list;
  286. }
  287. /**
  288. *
  289. * @param {String} type 类型
  290. ** week:指定(当前)周-每天的数; (默认)
  291. ** monthWeek:指定(当前)月-每周的数;
  292. ** monthDay:指定(当前)月-每天的数;
  293. ** yearMonth:指定(当前)年-每月的数;
  294. ** yearWeek:指定(当前)年-每周的数;
  295. ** yearDay:指定(当前)年-每天的数;
  296. */
  297. getDefaultRange(type) {
  298. let result;
  299. if (type === 'day') {
  300. const start = moment().startOf('day').format(`${this.tf}`);
  301. const end = moment(start).add(1, 'day').format(`${this.tf}`);
  302. result = [ start, end ];
  303. } else if (type === 'week') {
  304. const start = moment().startOf('week').format(this.tf);
  305. const end = moment(start).add(1, 'week').format(this.tf);
  306. result = [ start, end ];
  307. } else if (type.includes('month')) {
  308. const start = moment().startOf('month').format(this.tf);
  309. const end = moment(start).add(1, 'month').format(this.tf);
  310. result = [ start, end ];
  311. } else if (type.includes('year')) {
  312. const start = moment().startOf('year').format(this.tf);
  313. const end = moment(start).add(1, 'year').subtract(1, 'day')
  314. .format(this.tf);
  315. result = [ start, end ];
  316. }
  317. return result;
  318. }
  319. }
  320. module.exports = AdminService;