当前位置: 首页 > 工具软件 > Morphia > 使用案例 >

morphia操作MongoDB记录:$geoNear、分组、去重等聚合操作

阎功
2023-12-01

一、业务需求

统计实体店下【上月、本月、前日、今日】的【已下单、已接单、已完成】的订单

二、 需求分析

实时同步流程:
mysql —>flinkEtl —>MongoDB

  1. 订单新增以及状态变化都会实时通知并落库到mongo中,订单流水日志表:orderStatusLog。
  2. 订单数据量庞大
  3. 需要通过门店坐标和有效距离,对数据进行筛选
  4. 订单可能被接单后取消,又被接单,所以一条订单同个状态可能会有多条数据,但是统计时同一订单同一状态只统计一条
  5. 需要根据实体店的坐标,和该实体店辐射的有效距离筛选订单数据(既哪些订单属于该实体店)

三、开发逻辑

  1. 数据是海量的,全国实体店店有3w+。所以不能从MongoDB中把需要的数据筛选出来再通过Java程序分组去重,所以需要在mongo中完成数据的统计
  2. 之前类似的统计接口都是除时间筛选【上月、本月、前日、今日】的传参不一样之外,其他条件都是一致的。这样的话,要分四次查询mongo,而且查询条件明显有所覆盖。有一丢丢代码洁癖的我决定只走一次查询
  3. 代码思路:筛选之后,通过【每天、状态】进行分类统计,在返回数据后根据该维度,再累加进行【上月、本月、前日、今日】的统计

mongoDB查询应该返回的数据:
【0528-已下单】:50000单
【0528-已接单】:20000单
【0529-已下单】:30000单
【0529-已接单】:10000单

  1. 操作MongoDB的需求
    4.1. 根据日期区间、门店坐标和门店辐射有效距离进行订单筛选
    4.2. 根据日期和订单类型进行分组,统计【每天,各状态】的订单数量
    4.3. 注:订单可以接单后司机取消,再接单,所以一条订单同个状态可能会有多条数据,分组后要对order_id进行去重

四、MongoDB数据准备以及命令行操作

  1. MongoDB表结构展示,这里"start_point"为加了空间索引的字段,"date_day"冗余字段用于根据日期进行分组
{
    "_id": {
        "$oid": "60aeee3a63936082b9abc647"
    },
    "service_type": 40,
    "status": 0,
    "order_id": 52152552,
    "date_day": "2021-05-24",
    "order_create_time": {
        "$date": "2021-05-24T00:00:00.000Z"
    },
    "start_point": {
        "type": "Point",
        "coordinates": [116.4913, 40.002]
    }
}
  1. 数据插入语句
db.orderStatusLog.insert({status:0,order_id:55552,date_day:"2021-05-26",order_update_time:new ISODate("2021-05-26"),start_point:{type:"Point",coordinates:[40.55,40.002]}});
db.orderStatusLog.insert({status:10,order_id:55552,date_day:"2021-05-26",order_update_time:new ISODate("2021-05-26"),start_point:{type:"Point",coordinates:[40.55,40.002]}});
db.orderStatusLog.insert({status:40,order_id:24214,date_day:"2021-04-26",order_update_time:new ISODate("2021-04-26"),start_point:{type:"Point",coordinates:[40.55,40.002]}});
db.orderStatusLog.insert({status:0,order_id:512125,date_day:"2021-04-26",order_update_time:new ISODate("2021-04-26"),start_point:{type:"Point",coordinates:[40.55,40.002]}});
db.orderStatusLog.insert({status:10,order_id:125152,date_day:"2021-05-25",order_update_time:new ISODate("2021-05-25"),start_point:{type:"Point",coordinates:[40.55,40.002]}});
db.orderStatusLog.insert({status:10,order_id:125152,date_day:"2021-05-25",order_update_time:new ISODate("2021-05-25"),start_point:{type:"Point",coordinates:[40.55,40.002]}});
db.orderStatusLog.insert({status:40,order_id:24214,date_day:"2021-04-26",order_update_time:new ISODate("2021-04-26"),start_point:{type:"Point",coordinates:[40.55,40.002]}});
db.orderStatusLog.insert({status:0,order_id:25125,date_day:"2021-04-26",order_update_time:new ISODate("2021-04-26"),start_point:{type:"Point",coordinates:[40.55,40.002]}});
db.orderStatusLog.insert({status:40,order_id:12512,date_day:"2021-04-26",order_update_time:new ISODate("2021-04-26"),start_point:{type:"Point",coordinates:[40.55,40.002]}});

  1. 查询语句:由于order_id需要进行去重,所以先根据【日期、状态、订单id】进行去重后再统计订单数量
db.orderStatusLog.aggregate([
    { 
        "$geoNear" : { 
        "near" : { "type" : "Point" , "coordinates" : [ 39.11 , 40.51]} ,
        "query" : { "order_create_time" : { "$gte" : new ISODate("2021-04-01") }} ,
        "distanceField" : "dist.calculated" , 
        "maxDistance" : 100000000.0 ,
        "spherical" : true
        }},
    {
        $group:{
            _id:{dateDay:"$date_day",status:"$status",orderId:"$order_id"}
            }
    },
    {
        $group:{
            _id:{dateDay:"$_id.dateDay",status:"$_id.status"},
            count:{$sum:1}
            }
    }
])
  1. 查询结果输出
{ _id: { dateDay: '2021-04-25', status: 10 }, count: 1 }
{ _id: { dateDay: '2021-04-25', status: 0 }, count: 2 }
{ _id: { dateDay: '2021-05-24', status: 0 }, count: 1 }
{ _id: { dateDay: '2021-05-24', status: 10 }, count: 1 }
{ _id: { dateDay: '2021-04-25', status: 40 }, count: 1 }
{ _id: { dateDay: '2021-05-25', status: 40 }, count: 1 }
{ _id: { dateDay: '2021-04-26', status: 40 }, count: 1 }
{ _id: { dateDay: '2021-05-26', status: 40 }, count: 2 }
{ _id: { dateDay: '2021-05-24', status: 40 }, count: 2 }

五、Java代码DAO操作

// 日期筛选条件,【2021-04-01 00:00:00】——【2021-05-31 23:59:59】
Query<OrderStatusLog> query = datastore.createQuery(getEntityClass())
    .field("order_update_time").greaterThanOrEq(bo.getDayStart())
    .field("order_update_time").lessThan(bo.getDayEnd());

// 设置筛选条件、实体店坐标、有效距离,"calcDist"是计算订单开始地点到实体店的距离,这边我们只统计数量,没有用到
GeoNear geoNear = GeoNear.builder("calcDist")
        .setNear(GeoJson.point(bo.getLat(), bo.getLng()))
        .setMaxDistance(Double.parseDouble(String.valueOf(bo.getVisibleDistance())))
        .setSpherical(true)
        .setQuery(query)
        .build();

Iterator<RestaurantOrderMongoVo> resultIterator = datastore.createAggregation(getEntityClass())
        // 设置过滤文档条件
        .geoNear(geoNear)
        // 先分组去重
        .group(
                Group.id(Group.grouping("dateDay", "date_day")
                        , Group.grouping("orderStatus", "status")
                        , Group.grouping("orderId", "order_id"))
        )
        // 分组统计订单数据
        .group(
                Group.id(Group.grouping("dateDay", "_id.dateDay"), Group.grouping("orderStatus", "_id.orderStatus"))
                , Group.grouping("orderSum", new Accumulator("$sum", 1))
        )
        .aggregate(RestaurantOrderMongoVo.class);
        
// 遍历返回结果(返回结果中"_id"存储着分组的字段,需要解析出来)
List<RestaurantOrderSumVo> list = new ArrayList<>();
while (resultIterator.hasNext()) {
    RestaurantOrderMongoVo mongoVo = resultIterator.next();
    RestaurantOrderMongoVo.Result resultId = JSONObject.parseObject(mongoVo.getResultId(), RestaurantOrderMongoVo.Result.class);
    RestaurantOrderSumVo vo = new RestaurantOrderSumVo();
    vo.setOrderSum(mongoVo.getOrderSum());
    vo.setDateDay(resultId.getDateDay());
    vo.setOrderStatus(String.valueOf(resultId.getOrderStatus()));
    list.add(vo);
}

六、开发过程中遇到问题

  1. 坐标字段需要建立索引才能使用$geoNear进行查询
  2. 自己造数据的时候传入的经纬度超过正常值导致报错:【‘near’ field must be point】
  3. 传的数据maxDistance太小导致筛选不到数据,自测时由于造的数据地理距离差异较大,这个值可以传较大一些
  4. Java操作API进行日期比较时参数必须传入Date类型,否则会导致查询不到数据
  5. geoNear默认筛选100条数据,如果要返回更多,需要设置limit
 类似资料: