每日或每月间隔的交易总和

时间:2018-05-03 19:39:15

标签: node.js mongodb mongoose aggregation-framework

在我的收藏中,我记录了交易。

{
   "_id":{
      "$oid":"5ae77296c780351beadc5518"
   },
   "payment_amount":10000,
   "store_id":{
      "$oid":"5aa6babce7c97f0875556ae6"
   },
   "operator_id":{
      "$oid":"5aa6ba95e7c97f0875556ae3"
   },
   "cashier_id":{
      "$oid":"5acd4144c94ba7250da4af78"
   },
   "player_id":{
      "$oid":"5ae75fccc780351beadc5493"
   },
   "payment_type":"deposit",
   "payment_time":{
      "$date":"2018-04-30T19:46:30.055+0000"
   },
   "__v":0
}

现在我需要以某种方式聚合数据。

  1. 过去12个月我每个月需要12个结果。 在每个结果中应该有payment_amount所有payment_type deposit和所有payment_type == withdraw的总和

  2. 的总和
  3. 在过去31天内,我需要每天存款和取款的相同金额。

  4. 我正在使用mongoose,这是我的架构

    //schema definition
    var Sch = new Schema({
        store_id: Schema.Types.ObjectId,
        operator_id: Schema.Types.ObjectId, //users._id
        cashier_id: {type: Schema.Types.ObjectId, ref: 'usersMD'}, //users._id
        player_id: {type: Schema.Types.ObjectId, ref: 'playersMD'},
        payment_type: {type: String, enum: ['deposit', 'withdraw']},
        payment_amount: {type: Number, default: 0},
        payment_time: {type: Date, default: Date.now}
    }, opts);
    

    最好的方法是什么?

    我想在我的API中的log_transaction模块中创建两个方法。

    一个人会要求1.而一个人会要求2个。

1 个答案:

答案 0 :(得分:1)

我大部分都会在这里讨论“实现”细节,因为一旦你理解了它实际上是如何完成的,就能有一个合理的结论。

在理想情况下,您实际上会在存储时为“月”或“日”等间隔编写“预先累积”的数据。这通常是我们如何在高容量环境中通过编写累积总数来实现的。

如果没有它,您将使用现有数据的聚合,其中聚合框架就是您应该使用的。通常情况下,您至少会有一些代码用于报告您实际上并未实际“预先累积”的不同时间间隔。

您在这里寻找的基本要素是$cond运算符。这是一个“三元”或if/then/else条件,它允许为if表达一个条件,它将逻辑分支返回一个值,条件为真为then或为假为else

这是允许我们查看"payment_type"并在与$sum累积时确定值是否具有“正”或“负”数字表示的运算符。所以这里的基本陈述是:

 "$sum": {
   "$cond": {
     "if": { "$eq": [ "$payment_type", "deposit" ] },
     "then": "$payment_amount",
     "else": { "$subtract": [ 0, "$payment_amount" ] }
   }
 }

这适用于这里的基本数学,因此累积任务的其余部分都是关于收集“每个时间间隔”,并且有许多不同的方法可以做到这一点:

每月

使用MongoDB 3.6 $dateFromParts

Model.aggregate([
  { "$match": {
    "payment_time": { "$gte": start_date, "$lt": end_date }
  }},
  { "$group": {
    "_id": {
      "$dateFromParts": {
        "year": { "$year": "$payment_time" },
        "month": { "$month": "$payment_time" }
      }
    },
    "balance": {
      "$sum": {
        "$cond": {
          "if": { "$eq": [ "$payment_type", "deposit" ] },
          "then": "$payment_amount",
          "else": { "$subtract": [ 0, "$payment_amount" ] }
        }
      }
    }
  }}
])

使用日期数学

Model.aggregate([
  { "$match": {
    "payment_time": { "$gte": start_date, "$lt": end_date }
  }},
  { "$group": {
    "_id": {
      "$add": [
        { "$subtract": [
          { "$subtract": ["$payment_time", new Date(0)] },
          { "$mod": [
            { "$subtract": ["$payment_time", new Date(0)] },
            1000 * 60 * 60 * 24
          ]}
        ]},
        { "$multiply": [
          { "$subtract": [{ "$dayOfMonth": "$payment_time" }, 1] },
          -1000 * 60 * 60 * 24
        ]},
        new Date(0)
      ]
    },
    "balance": {
      "$sum": {
        "$cond": {
          "if": { "$eq": [ "$payment_type", "deposit" ] },
          "then": "$payment_amount",
          "else": { "$subtract": [ 0, "$payment_amount" ] }
        }
      }
    }
  }}
])

普通日期运算符

Model.aggregate([
  { "$match": {
    "payment_time": { "$gte": start_date, "$lt": end_date }
  }},
  { "$group": {
    "_id": {
      "year": { "$year": "$payment_time" },
      "month": { "$month": "$payment_time" }
    },
    "balance": {
      "$sum": {
        "$cond": {
          "if": { "$eq": [ "$payment_type", "deposit" ] },
          "then": "$payment_amount",
          "else": { "$subtract": [ 0, "$payment_amount" ] }
        }
      }
    }
  }}
])

前两个方法和最后一个方法之间的差异是那些初始方法实际返回BSON Date对象,它将在NodeJS下表示为标准JavaScript Date对象。使用$dateFromParts和“数学”方法基本上都会返回表示该月第一天的“舍入”日期。

像“银行对账单”这样的事情在“特定日期”实际发布是很常见的。这实际上只需要对所呈现的逻辑进行扩展以进行调整,只需简单地“修修”即可返回。

说每个月的“第15天”:

    "_id": {
      "$add": [
        { "$subtract": [
          { "$subtract": ["$payment_time", new Date(0)] },
          { "$mod": [
            { "$subtract": ["$payment_time", new Date(0)] },
            1000 * 60 * 60 * 24
          ]}
        ]},
        { "$multiply": [
          { "$subtract": [{ "$dayOfMonth": "$payment_time" }, 1] },
          -1000 * 60 * 60 * 24
        ]},
        1000 * 60 * 60 * 24 * (15-1),       // n-1 days adjusting
        new Date(0)
      ]
    },

这是我更喜欢“数学”方法的一个可靠原因,因为它比其他形式更灵活。您可以使用那些依赖于其他“日期运算符”的人来完成它,但他们最终会在一个月中应用“范围条件”,这需要采用更多逻辑来实现。但是,为数组添加一个额外的数字!还有什么比这更容易?

当然还有像$dateFromString$dateToString之类的东西作为使用Modern MongoDB执行此操作的其他方法,但是转换为“string”是一项昂贵的操作。你将基本上数字化的东西强制转换为字符串形式进行操作的操作最终会产生累积效应,这相当于对性能的巨大影响。在支付计算周期和数据传输的现代世界中,这等于$$ money $$。而且我是 cheapskate ,所以我喜欢有效率的事情。

每日

与以前大致相同,如何获得“每日”间隔

MongoDB 3.6 $dateFromParts

Model.aggregate([
  { "$match": {
    "payment_time": { "$gte": start_date, "$lt": end_date }
  }},
  { "$group": {
    "_id": {
      "$dateFromParts": {
        "year": { "$year": "$payment_time" },
        "month": { "$month": "$payment_time" },
        "day": { "$dayOfMonth": "$payment_time" }
      }
    },
    "balance": {
      "$sum": {
        "$cond": {
          "if": { "$eq": [ "$payment_type", "deposit" ] },
          "then": "$payment_amount",
          "else": { "$subtract": [ 0, "$payment_amount" ] }
        }
      }
    }
  }}
])

使用日期数学

Model.aggregate([
  { "$match": {
    "payment_time": { "$gte": start_date, "$lt": end_date }
  }},
  { "$group": {
    "_id": {
      "$add": [
        { "$subtract": [
          { "$subtract": ["$payment_time", new Date(0)] },
          { "$mod": [
            { "$subtract": ["$payment_time", new Date(0)] },
            1000 * 60 * 60 * 24
          ]}
        ]},
        new Date(0)
      ]
    },
    "balance": {
      "$sum": {
        "$cond": {
          "if": { "$eq": [ "$payment_type", "deposit" ] },
          "then": "$payment_amount",
          "else": { "$subtract": [ 0, "$payment_amount" ] }
        }
      }
    }
  }}
])

普通日期运算符

Model.aggregate([
  { "$match": {
    "payment_time": { "$gte": start_date, "$lt": end_date }
  }},
  { "$group": {
    "_id": {
      "year": { "$year": "$payment_time" },
      "month": { "$month": "$payment_time" },
      "day": { "$dayOfMonth": "$payment_time" }
    },
    "balance": {
      "$sum": {
        "$cond": {
          "if": { "$eq": [ "$payment_type", "deposit" ] },
          "then": "$payment_amount",
          "else": { "$subtract": [ 0, "$payment_amount" ] }
        }
      }
    }
  }}
])

值得注意的是,其他方法“添加”了一个$dayOfMonth运算符,而“数学”方法实际上删除了同一个运算符。造成这种情况的原因是因为“月”在数学运算中很难处理,因为每个月的天数都有所不同。因此,我们在“每月”四舍五入中基本上做的是考虑到今天为了找到月初。

这里的观点是,所呈现的相同“数学”实际上适用于任何时间间隔,无论是天还是数年,还是数小时或秒。您需要更改和调整的唯一时间是累积“月份”,其天数不一致。

另外请注意“重复”,因为一切都非常相似。最多你应该有两种方法,但实际上唯一需要改变的部分是月份和任何其他间隔之间的累加器,特别是如果你使用数学方法。老实说,一种识别“每月”和“其他所有”之间差异的方法就是你真正需要的,因为聚合管道无论如何都只是数据结构。

所以“操纵”他们。就像任何其他数据结构一样。

  

注意除了“日期”之外,该问题实际上没有提到哪个字段应该累积任何字段。如果您需要"player_id"之类的内容,那么您只需将其添加到"_id"阶段的"$group"作为复合键的一部分。上面的每个普通日期操作员部分都会演示“复合键”。