乘客下单

主要业务是从乘客选择起始地点和终止地点之后,也就是初步计算了估计价格然后点击呼叫司机之后的过程。等到附近的司机抢单成功,该业务执行完毕。

流程大致是:

  1. 在乘客点击下单之后,向数据库增加了一个order,前端每5s询问一次order的当前状态是否为接单,在这期间就没有客户端的事情了。
  2. 这个下单的动作会激活xxl-job的一个任务调度,给附近已经开启接单的司机的队列中加上这个order的id
  3. 等到司机端前端检查队列发现了这个订单,就会弹窗问是否需要接取,随后就是一个加锁然后抢票的逻辑了,需要有高并发的支持。
  4. 这个司机抢到了之后就会update这个订单,把司机的id变为自己的,状态改变。
  5. 等到乘客5s轮询发现改变了状态,此时接单成功。

其中,最重要的过程是xxl-job的这个任务如何找到周围的司机。平时使用xxl-job都是在web新建的,这里要求每一个订单自动给xxl-job,也就是要对xxl-job的接口做调整,使其能够继承springboot新建任务。
那么周围的司机如何找,每一个司机在接单的时候都会把自己的地理纬度上传到redis,用redis的数据结构geo快速完成计算。

新建订单

POST”/order/submitOrder”

由前端调用百度的api得到的初始位置和结束位置,以及乘客的id来在order数据库中新建一个订单。

对外接口

1
2
3
4
5
6
7
@Operation(summary = "乘客下单")
@GuiguLogin
@PostMapping("/submitOrder")
public Result<Long> submitOrder(@RequestBody SubmitOrderForm submitOrderForm) {
submitOrderForm.setCustomerId(AuthContextHolder.getUserId());
return Result.ok(orderService.submitOrder(submitOrderForm));
}

但是在数据库中这个表是需要有估计里程和估计价格的,也就是说还要再去调用一次map微服务和rule微服务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Autowired
private OrderInfoFeignClient orderInfoFeignClient;

@Override
public Long submitOrder(SubmitOrderForm submitOrderForm) {
//1.重新计算驾驶线路
CalculateDrivingLineForm calculateDrivingLineForm = new CalculateDrivingLineForm();
BeanUtils.copyProperties(submitOrderForm, calculateDrivingLineForm);
DrivingLineVo drivingLineVo = mapFeignClient.calculateDrivingLine(calculateDrivingLineForm).getData();

//2.重新计算订单费用
FeeRuleRequestForm calculateOrderFeeForm = new FeeRuleRequestForm();
calculateOrderFeeForm.setDistance(drivingLineVo.getDistance());
calculateOrderFeeForm.setStartTime(new Date());
calculateOrderFeeForm.setWaitMinute(0);
FeeRuleResponseVo feeRuleResponseVo = feeRuleFeignClient.calculateOrderFee(calculateOrderFeeForm).getData();

//3.封装订单信息对象
OrderInfoForm orderInfoForm = new OrderInfoForm();
//订单位置信息
BeanUtils.copyProperties(submitOrderForm, orderInfoForm);
//预估里程
orderInfoForm.setExpectDistance(drivingLineVo.getDistance());
orderInfoForm.setExpectAmount(feeRuleResponseVo.getTotalAmount());

//4.保存订单信息
Long orderId = orderInfoFeignClient.saveOrderInfo(orderInfoForm).getData();

//TODO启动任务调度

return orderId;
}

订单微服务

1
2
3
4
5
6
7
8
@Autowired
private OrderInfoService orderInfoService;

@Operation(summary = "保存订单信息")
@PostMapping("/saveOrderInfo")
public Result<Long> saveOrderInfo(@RequestBody OrderInfoForm orderInfoForm) {
return Result.ok(orderInfoService.saveOrderInfo(orderInfoForm));
}

设置订单的信息,状态为正在等待接单,给redis设置一个标识,说明正在接单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Autowired
private OrderInfoMapper orderInfoMapper;

@Autowired
private OrderStatusLogMapper orderStatusLogMapper;

@Autowired
private RedisTemplate redisTemplate;

@Transactional(rollbackFor = {Exception.class})
@Override
public Long saveOrderInfo(OrderInfoForm orderInfoForm) {
OrderInfo orderInfo = new OrderInfo();
BeanUtils.copyProperties(orderInfoForm, orderInfo);
String orderNo = UUID.randomUUID().toString().replaceAll("-","");
orderInfo.setStatus(OrderStatus.WAITING_ACCEPT.getStatus());
orderInfo.setOrderNo(orderNo);
orderInfoMapper.insert(orderInfo);

//记录日志
this.log(orderInfo.getId(), orderInfo.getStatus());

//接单标识,标识不存在了说明不在等待接单状态了
redisTemplate.opsForValue().set(RedisConstant.ORDER_ACCEPT_MARK, "0", RedisConstant.ORDER_ACCEPT_MARK_EXPIRES_TIME, TimeUnit.MINUTES);
return orderInfo.getId();
}

public void log(Long orderId, Integer status) {
OrderStatusLog orderStatusLog = new OrderStatusLog();
orderStatusLog.setOrderId(orderId);
orderStatusLog.setOrderStatus(status);
orderStatusLog.setOperateTime(new Date());
orderStatusLogMapper.insert(orderStatusLog);
}

司机和乘客查询订单状态GET”/getOrderStatus/{orderId}”

乘客下完单后,订单状态为1,乘客端小程序会轮询订单状态,当订单状态为2时,说明已经有司机接单了,那么页面进行跳转,进行下一步操作

1
2
3
4
5
6
7
8
9
10
11
12
@Override
public Integer getOrderStatus(Long orderId) {
LambdaQueryWrapper<OrderInfo> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(OrderInfo::getId, orderId);
queryWrapper.select(OrderInfo::getStatus);
OrderInfo orderInfo = orderInfoMapper.selectOne(queryWrapper);
if(null == orderInfo) {
//返回null,feign解析会抛出异常,给默认值,后续会用
return OrderStatus.NULL_ORDER.getStatus();
}
return orderInfo.getStatus();
}

乘客端是一下单就一直询问这个接口有没有改变;司机端得是抢到单了然后才能有订单的id,再去用这个id请求方法

xxl-job和redis完成司机搜索调度

xxl-job定时任务查询附近司机,开启接单的司机会把地理坐标传到redis

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Autowired
private RedisTemplate redisTemplate;

@Override
public Boolean updateDriverLocation(UpdateDriverLocationForm updateDriverLocationForm) {
/**
* Redis GEO 主要用于存储地理位置信息,并对存储的信息进行相关操作,该功能在 Redis 3.2 版本新增。
* 后续用在,乘客下单后寻找5公里范围内开启接单服务的司机,通过Redis GEO进行计算
*/
Point point = new Point(updateDriverLocationForm.getLongitude().doubleValue(), updateDriverLocationForm.getLatitude().doubleValue());
redisTemplate.opsForGeo().add(RedisConstant.DRIVER_GEO_LOCATION, point, updateDriverLocationForm.getDriverId().toString());
return true;
}

@Override
public Boolean removeDriverLocation(Long driverId) {
redisTemplate.opsForGeo().remove(RedisConstant.DRIVER_GEO_LOCATION, driverId.toString());
return true;
}

POST”/searchNearByDriver”

司机端的小程序开启接单服务后,开始实时上传司机的定位信息到redis的GEO缓存,前面乘客已经下单,现在我们就要查找附近适合接单的司机,如果有对应的司机,那就给司机发送新订单消息。

首先是配置经纬度点和距离,作为中心和半径传入redis的arg,得到了一个升序排序的位置表,然后根据这些待选司机的一些配置(比如超过多少距离不予配送)筛选出符合的一队司机,作为结果返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
@Autowired
private DriverInfoFeignClient driverInfoFeignClient;

@Override
public List<NearByDriverVo> searchNearByDriver(SearchNearByDriverForm searchNearByDriverForm) {
// 搜索经纬度位置5公里以内的司机
//定义经纬度点
Point point = new Point(searchNearByDriverForm.getLongitude().doubleValue(), searchNearByDriverForm.getLatitude().doubleValue());
//定义距离:5公里(系统配置)
Distance distance = new Distance(SystemConstant.NEARBY_DRIVER_RADIUS, RedisGeoCommands.DistanceUnit.KILOMETERS);
//定义以point点为中心,distance为距离这么一个范围
Circle circle = new Circle(point, distance);

//定义GEO参数
RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs()
.includeDistance() //包含距离
.includeCoordinates() //包含坐标
.sortAscending(); //排序:升序

// 1.GEORADIUS获取附近范围内的信息
GeoResults<RedisGeoCommands.GeoLocation<String>> result = this.redisTemplate.opsForGeo().radius(RedisConstant.DRIVER_GEO_LOCATION, circle, args);

//2.收集信息,存入list
List<GeoResult<RedisGeoCommands.GeoLocation<String>>> content = result.getContent();

//3.返回计算后的信息
List<NearByDriverVo> list = new ArrayList();
if(!CollectionUtils.isEmpty(content)) {
Iterator<GeoResult<RedisGeoCommands.GeoLocation<String>>> iterator = content.iterator();
while (iterator.hasNext()) {
GeoResult<RedisGeoCommands.GeoLocation<String>> item = iterator.next();

//司机id
Long driverId = Long.parseLong(item.getContent().getName());
//当前距离
BigDecimal currentDistance = new BigDecimal(item.getDistance().getValue()).setScale(2, RoundingMode.HALF_UP);
log.info("司机:{},距离:{}",driverId, item.getDistance().getValue());

//获取司机接单设置参数
DriverSet driverSet = driverInfoFeignClient.getDriverSet(driverId).getData();
//接单里程判断,acceptDistance==0:不限制,
if(driverSet.getAcceptDistance().doubleValue() != 0 && driverSet.getAcceptDistance().subtract(currentDistance).doubleValue() < 0) {
continue;
}
//订单里程判断,orderDistance==0:不限制
if(driverSet.getOrderDistance().doubleValue() != 0 && driverSet.getOrderDistance().subtract(searchNearByDriverForm.getMileageDistance()).doubleValue() < 0) {
continue;
}

//满足条件的附近司机信息
NearByDriverVo nearByDriverVo = new NearByDriverVo();
nearByDriverVo.setDriverId(driverId);
nearByDriverVo.setDistance(currentDistance);
list.add(nearByDriverVo);
}
}
return list;
}

xxl-job这里配置就不做介绍了,主要梳理业务

准备一个添加任务调度的业务,一分钟执行一次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Transactional(rollbackFor = Exception.class)
@Override
public Long addAndStartTask(NewOrderTaskVo newOrderTaskVo) {
OrderJob orderJob = orderJobMapper.selectOne(new LambdaQueryWrapper<OrderJob>().eq(OrderJob::getOrderId, newOrderTaskVo.getOrderId()));
if(null == orderJob) {
//新增一个名为newOrderTaskHandler的任务调度,一分钟执行一次
Long jobId = xxlJobClient.addAndStart("newOrderTaskHandler", "", "0 0/1 * * * ?", "新订单任务,订单id:"+newOrderTaskVo.getOrderId());

//记录订单与任务的关联信息
orderJob = new OrderJob();
orderJob.setOrderId(newOrderTaskVo.getOrderId());
orderJob.setJobId(jobId);
orderJob.setParameter(JSONObject.toJSONString(newOrderTaskVo));
orderJobMapper.insert(orderJob);
}
return orderJob.getJobId();
}

newOrderTaskHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
@XxlJob("newOrderTaskHandler")
public void newOrderTaskHandler() {
log.info("新订单调度任务:{}", XxlJobHelper.getJobId());

//记录定时任务相关的日志信息
//封装日志对象
XxlJobLog xxlJobLog = new XxlJobLog();
xxlJobLog.setJobId(XxlJobHelper.getJobId());
long startTime = System.currentTimeMillis();
try {
//执行任务
newOrderService.executeTask(XxlJobHelper.getJobId());

xxlJobLog.setStatus(1);//成功
} catch (Exception e) {
xxlJobLog.setStatus(0);//失败
xxlJobLog.setError(ExceptionUtil.getAllExceptionMsg(e));
log.error("定时任务执行失败,任务id为:{}", XxlJobHelper.getJobId());
e.printStackTrace();
} finally {
//耗时
int times = (int) (System.currentTimeMillis() - startTime);
xxlJobLog.setTimes(times);
xxlJobLogMapper.insert(xxlJobLog);
}
}

@Override
public Boolean executeTask(Long jobId) {
//获取任务参数
OrderJob orderJob = orderJobMapper.selectOne(new LambdaQueryWrapper<OrderJob>().eq(OrderJob::getJobId, jobId));
if(null == orderJob) {
return true;
}
NewOrderTaskVo newOrderTaskVo = JSONObject.parseObject(orderJob.getParameter(), NewOrderTaskVo.class);

//查询订单状态,如果该订单还在接单状态,继续执行;如果不在接单状态,则停止定时调度
Integer orderStatus = orderInfoFeignClient.getOrderStatus(newOrderTaskVo.getOrderId()).getData();
if(orderStatus.intValue() != OrderStatus.WAITING_ACCEPT.getStatus().intValue()) {
xxlJobClient.stopJob(jobId);
log.info("停止任务调度: {}", JSON.toJSONString(newOrderTaskVo));
return true;
}

//搜索附近满足条件的司机
SearchNearByDriverForm searchNearByDriverForm = new SearchNearByDriverForm();
searchNearByDriverForm.setLongitude(newOrderTaskVo.getStartPointLongitude());
searchNearByDriverForm.setLatitude(newOrderTaskVo.getStartPointLatitude());
searchNearByDriverForm.setMileageDistance(newOrderTaskVo.getExpectDistance());
List<NearByDriverVo> nearByDriverVoList = locationFeignClient.searchNearByDriver(searchNearByDriverForm).getData();
//给司机派发订单信息
nearByDriverVoList.forEach(driver -> {
//记录司机id,防止重复推送订单信息
String repeatKey = RedisConstant.DRIVER_ORDER_REPEAT_LIST+newOrderTaskVo.getOrderId();
boolean isMember = redisTemplate.opsForSet().isMember(repeatKey, driver.getDriverId());
if(!isMember) {
//记录该订单已放入司机临时容器
redisTemplate.opsForSet().add(repeatKey, driver.getDriverId());
//过期时间:15分钟,新订单15分钟没人接单自动取消
redisTemplate.expire(repeatKey, RedisConstant.DRIVER_ORDER_REPEAT_LIST_EXPIRES_TIME, TimeUnit.MINUTES);

NewOrderDataVo newOrderDataVo = new NewOrderDataVo();
newOrderDataVo.setOrderId(newOrderTaskVo.getOrderId());
newOrderDataVo.setStartLocation(newOrderTaskVo.getStartLocation());
newOrderDataVo.setEndLocation(newOrderTaskVo.getEndLocation());
newOrderDataVo.setExpectAmount(newOrderTaskVo.getExpectAmount());
newOrderDataVo.setExpectDistance(newOrderTaskVo.getExpectDistance());
newOrderDataVo.setExpectTime(newOrderTaskVo.getExpectTime());
newOrderDataVo.setFavourFee(newOrderTaskVo.getFavourFee());
newOrderDataVo.setDistance(driver.getDistance());
newOrderDataVo.setCreateTime(newOrderTaskVo.getCreateTime());

//将消息保存到司机的临时队列里面,司机接单了会定时轮询到他的临时队列获取订单消息
String key = RedisConstant.DRIVER_ORDER_TEMP_LIST+driver.getDriverId();
redisTemplate.opsForList().leftPush(key, JSONObject.toJSONString(newOrderDataVo));
//过期时间:1分钟,1分钟未消费,自动过期
//注:司机端开启接单,前端每5秒(远小于1分钟)拉取1次“司机临时队列”里面的新订单消息
redisTemplate.expire(key, RedisConstant.DRIVER_ORDER_TEMP_LIST_EXPIRES_TIME, TimeUnit.MINUTES);
log.info("该新订单信息已放入司机临时队列: {}", JSON.toJSONString(newOrderDataVo));
}
});
return true;
}

最后再在下单的那个任务中执行添加xxl-job任务调度。