lrf %!s(int64=2) %!d(string=hai) anos
pai
achega
d3b2478858

+ 0 - 3
app/controller/home.js

@@ -5,9 +5,6 @@ class HomeController extends Controller {
   async index() {
     const { ctx } = this;
     ctx.body = 'hi, egg';
-    // 模拟优惠券发行
-    // const data = await this.makeCoupons();
-    // this.ctx.ok({ data, total: data.length });
   }
 
   async makeCoupons() {

+ 16 - 0
app/controller/statistics/admin.js

@@ -0,0 +1,16 @@
+'use strict';
+const Controller = require('egg').Controller;
+const { CrudController } = require('naf-framework-mongoose-free/lib/controller');
+
+//
+class AdminController extends Controller {
+  constructor(ctx) {
+    super(ctx);
+    this.service = this.ctx.service.statistics.admin;
+  }
+  async todo() {
+    const res = await this.service.todo(this.ctx.query);
+    this.ctx.ok({ data: res });
+  }
+}
+module.exports = CrudController(AdminController, {});

+ 1 - 1
app/public/defaultData/goodsTags.js

@@ -661,7 +661,7 @@ module.exports = [
             code: 'xj',
           },
           {
-            label: '',
+            label: '',
             code: 'l',
           },
           {

+ 1 - 0
app/router.js

@@ -16,4 +16,5 @@ module.exports = app => {
   require('./z_router/view/index')(app); // 视图部分
   require('./z_router/util')(app); // 工具接口
   require('./z_router/trade/pay')(app); // 支付接口
+  require('./z_router/statistics/index')(app); // 统计部分
 };

+ 277 - 0
app/service/statistics/admin.js

@@ -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;

+ 13 - 0
app/z_router/statistics/admin.js

@@ -0,0 +1,13 @@
+'use strict';
+// 路由配置
+const path = require('path');
+const regPath = path.resolve('app', 'public', 'routerRegister');
+const routerRegister = require(regPath);
+const rkey = 'statistics/admin';
+const ckey = 'statistics.admin';
+const keyZh = '中台统计';
+const routes = [{ method: 'get', path: `${rkey}/todo`, controller: `${ckey}.todo`, name: `${ckey}Todo`, zh: `${keyZh}待办查询` }];
+
+module.exports = app => {
+  routerRegister(app, routes, keyZh, rkey, ckey);
+};

+ 6 - 0
app/z_router/statistics/index.js

@@ -0,0 +1,6 @@
+/**
+ * @param {Egg.Application} app - egg application
+ */
+module.exports = app => {
+  require('./admin')(app); // 中台统计
+};