一、业务需求
统计实体店下【上月、本月、前日、今日】的【已下单、已接单、已完成】的订单
二、 需求分析
实时同步流程:
mysql —>flinkEtl —>MongoDB
- 订单新增以及状态变化都会实时通知并落库到mongo中,订单流水日志表:orderStatusLog。
- 订单数据量庞大
- 需要通过门店坐标和有效距离,对数据进行筛选
- 订单可能被接单后取消,又被接单,所以一条订单同个状态可能会有多条数据,但是统计时同一订单同一状态只统计一条
- 需要根据实体店的坐标,和该实体店辐射的有效距离筛选订单数据(既哪些订单属于该实体店)
三、开发逻辑
- 数据是海量的,全国实体店店有3w+。所以不能从MongoDB中把需要的数据筛选出来再通过Java程序分组去重,所以需要在mongo中完成数据的统计
- 之前类似的统计接口都是除时间筛选【上月、本月、前日、今日】的传参不一样之外,其他条件都是一致的。这样的话,要分四次查询mongo,而且查询条件明显有所覆盖。有一丢丢代码洁癖的我决定只走一次查询
- 代码思路:筛选之后,通过【每天、状态】进行分类统计,在返回数据后根据该维度,再累加进行【上月、本月、前日、今日】的统计
mongoDB查询应该返回的数据:
【0528-已下单】:50000单
【0528-已接单】:20000单
【0529-已下单】:30000单
【0529-已接单】:10000单
- 操作MongoDB的需求
4.1. 根据日期区间、门店坐标和门店辐射有效距离进行订单筛选
4.2. 根据日期和订单类型进行分组,统计【每天,各状态】的订单数量
4.3. 注:订单可以接单后司机取消,再接单,所以一条订单同个状态可能会有多条数据,分组后要对order_id进行去重
四、MongoDB数据准备以及命令行操作
- 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]
}
}
- 数据插入语句
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]}});
- 查询语句:由于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}
}
}
])
- 查询结果输出
{ _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);
}
六、开发过程中遇到问题
- 坐标字段需要建立索引才能使用$geoNear进行查询
- 自己造数据的时候传入的经纬度超过正常值导致报错:【‘near’ field must be point】
- 传的数据maxDistance太小导致筛选不到数据,自测时由于造的数据地理距离差异较大,这个值可以传较大一些
- Java操作API进行日期比较时参数必须传入Date类型,否则会导致查询不到数据
- geoNear默认筛选100条数据,如果要返回更多,需要设置limit