|
@@ -0,0 +1,277 @@
|
|
|
+'use strict';
|
|
|
+const { CrudService } = require('naf-framework-mongoose-free/lib/service');
|
|
|
+const { BusinessError, ErrorCode } = require('naf-core').Error;
|
|
|
+const _ = require('lodash');
|
|
|
+const assert = require('assert');
|
|
|
+const { ObjectId } = require('mongoose').Types;
|
|
|
+const moment = require('moment');
|
|
|
+
|
|
|
+// 中台统计
|
|
|
+class AdminService extends CrudService {
|
|
|
+ constructor(ctx) {
|
|
|
+ super(ctx, 'admin');
|
|
|
+ this.goods = this.ctx.model.Shop.Goods;
|
|
|
+ this.goodsSpec = this.ctx.model.Shop.GoodsSpec;
|
|
|
+ this.orderModel = this.ctx.model.Trade.Order;
|
|
|
+ this.orderDetailModel = this.ctx.model.Trade.OrderDetail;
|
|
|
+ this.afterSaleModel = this.ctx.model.Trade.AfterSale;
|
|
|
+ this.tf = 'YYYY-MM-DD';
|
|
|
+ }
|
|
|
+ // 待办事项
|
|
|
+ async todo(query) {
|
|
|
+ const notPay = await this.notPay(query);
|
|
|
+ const notSend = await this.notSend(query);
|
|
|
+ const notDealAfterSale = await this.notDealAfterSale(query);
|
|
|
+ const stockWarning = await this.stockWarning(query);
|
|
|
+ const sMarkOrder = await this.makeOrder(query);
|
|
|
+ const sAfterSale = await this.makeAfterSale(query);
|
|
|
+ return { notPay, notSend, notDealAfterSale, stockWarning, sMarkOrder, sAfterSale };
|
|
|
+ }
|
|
|
+
|
|
|
+ // 待办-库存警告
|
|
|
+ async stockWarning(query) {
|
|
|
+ // TODO,库存警告线需要设置
|
|
|
+ const warningLine = 10;
|
|
|
+ const { shop } = query;
|
|
|
+ // 过滤警告线以外的数据
|
|
|
+ const pipline = [{ $match: { status: '0', num: { $lte: warningLine } } }];
|
|
|
+ // 因为不需要数据,节省内存,将没用的字段过滤掉
|
|
|
+ pipline.push({ $project: { goods_id: { $toObjectId: '$goods' }, goodsSpec_id: { $toString: '$_id' }, num: '$num' } });
|
|
|
+ // 表关联
|
|
|
+ pipline.push({
|
|
|
+ $lookup: {
|
|
|
+ from: 'goods',
|
|
|
+ localField: 'goods_id',
|
|
|
+ foreignField: '_id',
|
|
|
+ as: 'goods',
|
|
|
+ },
|
|
|
+ });
|
|
|
+ // 平铺
|
|
|
+ pipline.push({ $unwind: '$goods' });
|
|
|
+ // 再组织数据
|
|
|
+ pipline.push({ $project: { goods_id: '$goods_id', goodsSpec_id: '$goodsSpec_id', num: '$num', shop: { $toString: '$goods.shop' } } });
|
|
|
+ if (ObjectId.isValid(shop)) {
|
|
|
+ pipline.push({ $match: { shop } });
|
|
|
+ }
|
|
|
+ pipline.push(this.totalPip());
|
|
|
+ const result = await this.goodsSpec.aggregate(pipline);
|
|
|
+ return this.getTotal(result);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 待办-未处理售后
|
|
|
+ async notDealAfterSale(query) {
|
|
|
+ const { shop } = query;
|
|
|
+ const pipline = [{ $match: { status: '0' } }];
|
|
|
+ if (ObjectId.isValid(shop)) {
|
|
|
+ pipline.push({ $match: { shop } });
|
|
|
+ }
|
|
|
+ pipline.push(this.totalPip());
|
|
|
+ const result = await this.afterSaleModel.aggregate(pipline);
|
|
|
+ return this.getTotal(result);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 待办-未发货
|
|
|
+ async notSend(query) {
|
|
|
+ const { shop } = query;
|
|
|
+ const pipline = [{ $match: { status: '1' } }];
|
|
|
+ if (ObjectId.isValid(shop)) {
|
|
|
+ pipline.push({ $match: { shop } });
|
|
|
+ }
|
|
|
+ pipline.push(this.totalPip());
|
|
|
+ const result = await this.orderDetailModel.aggregate(pipline);
|
|
|
+ return this.getTotal(result);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 待办-未付款订单数
|
|
|
+ async notPay(query) {
|
|
|
+ const { shop } = query;
|
|
|
+ const pipline = [{ $match: { status: '0' } }];
|
|
|
+ if (ObjectId.isValid(shop)) {
|
|
|
+ pipline.push({ $match: { 'goods.shop': shop } });
|
|
|
+ }
|
|
|
+ pipline.push(this.totalPip());
|
|
|
+ const result = await this.orderModel.aggregate(pipline);
|
|
|
+ return this.getTotal(result);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 统计-下单数(生成order,不管支不支付)
|
|
|
+ * @param {Object} query 地址参数
|
|
|
+ * @return {Array} 统计数据
|
|
|
+ * @property {String} type 数据类型
|
|
|
+ ** 默认为从 当天/本周/本月/本年
|
|
|
+ ** custom:自定义,只查这个时间段的,每天的数
|
|
|
+ ** day:单数据(一个数);
|
|
|
+ ** week:指定(当前)周-每天的数; (默认)
|
|
|
+ ** monthWeek:指定(当前)月-每周的数;
|
|
|
+ ** monthDay:指定(当前)月-每天的数;
|
|
|
+ ** yearMonth:指定(当前)年-每月的数;
|
|
|
+ ** yearWeek:指定(当前)年-每周的数;
|
|
|
+ ** yearDay:指定(当前)年-每天的数;
|
|
|
+ * @property {Array} start 开始
|
|
|
+ * @property {Array} end 结束
|
|
|
+ * @property {Array} range 指定的时间范围;如果没传,则根据type自动生成
|
|
|
+ */
|
|
|
+ async makeOrder(query) {
|
|
|
+ const pipline = [];
|
|
|
+ const { type = 'week', start, end, shop } = query;
|
|
|
+ let timeRange = [];
|
|
|
+ if (type === 'custom') {
|
|
|
+ const r = moment(start).isBefore(end);
|
|
|
+ if (r) timeRange.push(start, end);
|
|
|
+ else timeRange.push(end, start);
|
|
|
+ } else timeRange = this.getDefaultRange(type);
|
|
|
+ // 先用商店过滤下
|
|
|
+ if (ObjectId.isValid(shop)) {
|
|
|
+ pipline.push({ $match: { shop } });
|
|
|
+ }
|
|
|
+ // 时间格式化: create_date:日期,用来分组统计; create_time: 时间,用来过滤数据
|
|
|
+ pipline.push({
|
|
|
+ $project: {
|
|
|
+ create_date: { $dateToString: { date: '$meta.createdAt', ...this.pipDateFormatDateObject() } },
|
|
|
+ create_time: { $dateToString: { date: '$meta.createdAt', ...this.pipDateFormatTimeObject() } },
|
|
|
+ },
|
|
|
+ });
|
|
|
+ pipline.push({ $match: { $and: [{ create_time: { $gte: _.head(timeRange) } }, { create_time: { $lte: _.last(timeRange) } }] } });
|
|
|
+ pipline.push({
|
|
|
+ $group: {
|
|
|
+ _id: '$create_date',
|
|
|
+ num: { $sum: 1 },
|
|
|
+ },
|
|
|
+ });
|
|
|
+ const fs = await this.orderModel.aggregate(pipline);
|
|
|
+ const list = this.getRangeDateList(timeRange);
|
|
|
+ const result = [];
|
|
|
+ for (const d of list) {
|
|
|
+ const r = fs.find(f => f._id === d);
|
|
|
+ const obj = { date: d, num: 0 };
|
|
|
+ if (r) obj.num = r.num;
|
|
|
+ result.push(obj);
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 售后数
|
|
|
+ * @param {Object} query 查询条件
|
|
|
+ */
|
|
|
+ async makeAfterSale(query) {
|
|
|
+ const pipline = [];
|
|
|
+ const { type = 'week', start, end, shop } = query;
|
|
|
+ let timeRange = [];
|
|
|
+ if (type === 'custom') {
|
|
|
+ const r = moment(start).isBefore(end);
|
|
|
+ if (r) timeRange.push(start, end);
|
|
|
+ else timeRange.push(end, start);
|
|
|
+ } else timeRange = this.getDefaultRange(type);
|
|
|
+ // 先用商店过滤下
|
|
|
+ if (ObjectId.isValid(shop)) {
|
|
|
+ pipline.push({ $match: { shop } });
|
|
|
+ }
|
|
|
+ // 时间格式化: create_date:日期,用来分组统计; create_time: 时间,用来过滤数据
|
|
|
+ pipline.push({
|
|
|
+ $project: {
|
|
|
+ create_date: { $dateToString: { date: '$meta.createdAt', ...this.pipDateFormatDateObject() } },
|
|
|
+ create_time: { $dateToString: { date: '$meta.createdAt', ...this.pipDateFormatTimeObject() } },
|
|
|
+ },
|
|
|
+ });
|
|
|
+ pipline.push({ $match: { $and: [{ create_time: { $gte: _.head(timeRange) } }, { create_time: { $lte: _.last(timeRange) } }] } });
|
|
|
+ pipline.push({
|
|
|
+ $group: {
|
|
|
+ _id: '$create_date',
|
|
|
+ num: { $sum: 1 },
|
|
|
+ },
|
|
|
+ });
|
|
|
+ const fs = await this.afterSaleModel.aggregate(pipline);
|
|
|
+ const list = this.getRangeDateList(timeRange);
|
|
|
+ const result = [];
|
|
|
+ for (const d of list) {
|
|
|
+ const r = fs.find(f => f._id === d);
|
|
|
+ const obj = { date: d, num: 0 };
|
|
|
+ if (r) obj.num = r.num;
|
|
|
+ result.push(obj);
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 聚合总数管道
|
|
|
+ totalPip() {
|
|
|
+ return {
|
|
|
+ $count: 'total',
|
|
|
+ };
|
|
|
+ }
|
|
|
+ /**
|
|
|
+ * 取出聚合查询的总数
|
|
|
+ * @param {Array} data 聚合查询总数的结果($count)
|
|
|
+ * @param {String} key 总数的字段
|
|
|
+ * @return {Number} 返回总数
|
|
|
+ */
|
|
|
+ getTotal(data, key = 'total') {
|
|
|
+ return _.get(_.head(data), key, 0);
|
|
|
+ }
|
|
|
+ /**
|
|
|
+ * 管道格式化时间格式--至时分秒
|
|
|
+ * @return {Object}
|
|
|
+ */
|
|
|
+ pipDateFormatTimeObject() {
|
|
|
+ return { format: '%Y-%m-%d %H:%M:%S', timezone: '+08:00' };
|
|
|
+ }
|
|
|
+ /**
|
|
|
+ * 管道格式化室间隔是-至日期
|
|
|
+ * @return {Object}
|
|
|
+ */
|
|
|
+ pipDateFormatDateObject() {
|
|
|
+ return { format: '%Y-%m-%d', timezone: '+08:00' };
|
|
|
+ }
|
|
|
+ /**
|
|
|
+ * 获取时间范围内的每一天
|
|
|
+ * @param {Array} range 时间范围
|
|
|
+ * @return {Array} 时间范围数组
|
|
|
+ */
|
|
|
+ getRangeDateList(range) {
|
|
|
+ const start = _.head(range);
|
|
|
+ const end = _.last(range);
|
|
|
+ const list = [ start ];
|
|
|
+ let pd = 1;
|
|
|
+ while (!moment(_.last(list)).isSame(end)) {
|
|
|
+ const d = moment(start).add(pd, 'day').format(this.tf);
|
|
|
+ pd++;
|
|
|
+ list.push(d);
|
|
|
+ }
|
|
|
+
|
|
|
+ return list;
|
|
|
+ }
|
|
|
+ /**
|
|
|
+ *
|
|
|
+ * @param {String} type 类型
|
|
|
+ ** week:指定(当前)周-每天的数; (默认)
|
|
|
+ ** monthWeek:指定(当前)月-每周的数;
|
|
|
+ ** monthDay:指定(当前)月-每天的数;
|
|
|
+ ** yearMonth:指定(当前)年-每月的数;
|
|
|
+ ** yearWeek:指定(当前)年-每周的数;
|
|
|
+ ** yearDay:指定(当前)年-每天的数;
|
|
|
+ */
|
|
|
+ getDefaultRange(type) {
|
|
|
+ let result;
|
|
|
+ if (type === 'day') {
|
|
|
+ const start = moment().startOf('day').format(this.tf);
|
|
|
+ const end = moment(start).add(1, 'day').format(this.tf);
|
|
|
+ result = [ start, end ];
|
|
|
+ } else if (type === 'week') {
|
|
|
+ const start = moment().startOf('week').format(this.tf);
|
|
|
+ const end = moment(start).add(1, 'week').format(this.tf);
|
|
|
+ result = [ start, end ];
|
|
|
+ } else if (type.includes('month')) {
|
|
|
+ const start = moment().startOf('month').format(this.tf);
|
|
|
+ const end = moment(start).add(1, 'month').format(this.tf);
|
|
|
+ result = [ start, end ];
|
|
|
+ } else if (type.includes('year')) {
|
|
|
+ const start = moment().startOf('year').format(this.tf);
|
|
|
+ const end = moment(start).add(1, 'year').format(this.tf);
|
|
|
+ result = [ start, end ];
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+module.exports = AdminService;
|