原创

Java架构师方案——打车平台派单系统的DDD领域分析与代码设计(附完整项目代码)

1. 派单系统领域建模

问题域

打车派单系统要解决什么问题?

派单系统要成为乘客和司机之间的实时连接桥梁。从用户发出一笔行程单,到司机接下这笔行程单的整个过程,都是打车派单系统做的工作。

打车服务是的主体对象是乘客和司机,但是司机和乘客两个业务实体在地图上是随机离散的,而且具有实时属性:从乘客发出行程单,到司机接到订单,整个过程的时间不能太长,否则乘客就会选择其他交通方式。

如何让附近的司机接到附近乘客发出的行程单?

我们都知道乘客的订单具有实时性,你不可能让离乘客几十分钟路程以外的司机接下这笔订单,如果真的发生这种情况,不仅司机不愿意接,乘客也不会花几十分钟的时间等司机,而是会选择其他交通方式。因此我们需要建立一个通用的模型来聚合附近乘客的行程单,并让附近的司机接到附近乘客的这些单。在本文我把它成为线上公交站

线上公交站:在一个城市的每个地方,派单系统都建立一个线上的虚拟公交站点,所有附近乘客发出的行程单都会来到这个公交站排队等候司机来这个公交站点接行程单。

线上公交站是如何分布在城市地图上的呢?

假如我们对整个北京建立一个地图网格模型,如下图所示:网格中所有的节点就是线上公交站点。

alt

北京市界的地理坐标为:北纬39°28’至41°03’,东经115°25’至117°35’。因此将坐标数据换算成秒单位,那么北京的市界地理坐标区间是:维度 142080 -- 147780 , 经度 415500 -- 423300。范围值是纬度5700'',经度7800''。

如果我设置公交站点之间的间隔是40‘‘。那么网格在水平方向上就有142.5个长度网格,垂直方向上有195个宽度网格。

为什么要设置站点间距是40''呢? 我们可以这么计算下,赤道长40000km,在赤道上东西、南北方向每秒角度对应于30.86m ,计算公式如4*10^7m/(360*60*60)=30.86m/sec。 那么我设置网格间距是40秒,那么网格间距的地理距离就是1200m,这个距离正好是公交站点距离(可能实际情况间距更大,看你如何设置)。如果北京城市是个规矩的长方形,那么线上的虚拟公交站点就有143*195=27885个。

行程单该去往哪个线上公交站点?

我们从网格模型图可以看到,Q1负责的区域是周围的红色区域,在红色区域发出的所有行程单,都会进入到站线上站点Q1,开始排队等待司机来接单。

司机又该去哪个站点接单呢?

司机也属于领域中的一个实体对象,但是他比乘客多出一个属性:他是行驶中的(位置会时刻发生变化),因此在设计司机接单业务活动的时候,你不能想乘客发出行程单那样的规则,应该考虑司机行驶中GPS位置的变化。如果去线上站点接单的话,应该是去行驶方向上的几个站点遍历接单。

领域模型如何保证乘客在派单系统上排队等待司机呢?

在前文我们提到的场景三,乘客多的情况下,需要排队等待司机接单,因为此时司机是非常稀缺的资源。实现这个场景的模型就是使用我们熟悉的队列数据结构,这种数据结构能保证先到先得的排队模型。

核心领域概念

线上站点:用户的行程单会放到站点排队,等待司机接单。
地图网格模型:城市地图使用网格建模,单个网格间距1200m,每个网格就是一个虚拟的线上站点。
站点范围:站点Q1所管辖的位置区域如上图1、2、3、4。站点与其他周围的站点均分网格区域。
行程单:包含乘客的GPS经纬度位置信息,以及用户的出发地和目的地信息,当然还有用户名。
乘客:派单系统的核心领域实体对象,行程单发起人。
司机:派单系统的核心领域实体对象,接下行程单。

核心业务规则

  1. 一个行程单只能被一个司机接到。
  2. 一个医生一次只能接到一个行程单。
  3. 一个行程单只能放到一个站点进行排队。
  4. 行程单需要排队,遵守先到先得的规则,优先进入队列的行程单优先被接单。

业务场景

场景一:乘客发出行程单,排队等待附近司机接单。
场景二:司机侧不断扫描附近线上站点,随时准备接收乘客行程单。
场景三:乘客查看到目前线上站点的排队情况。
场景四:司机能查看到附近大范围内的站点行程单情况,往更容易的区域行驶,增大接单成功概率。

业务流程

用户发出行程单: 行程单带着gps的经纬度数据进入附近公交站点排队,类似生活场景中的公交站,在公交站,如果乘客比较多的话,大家会自动排成一排,公交车到站,排队的乘客会一个接一个刷卡上车。如果公交车满了,其他排队的乘客就继续等待下一趟公交车。

司机接单流程:司机接单请求会去他附近站点和行驶方向的站点去接单,如果扫描的站点有行程单的话,直接接单成功,如果没有的话,就继续去行驶方向上的其他站点尝试接单。一般情况都会将司机远距离的站点行程单情况显示给司机。引导司机去繁忙的区域接单。

2. 代码设计

行程单数据结构

public class TripOrder {

    private String userName;//用户名
    private String departure;//出发地点
    private String destination ;//目的地
    private int latitude;//纬度
    private int longtude;//经度

}

用户发出的行程单数据包括:用户名、出发地、目的地、维度数据、经度数据。

计算行程单的线上公交站点key值

每个用户发出的行程单,都会根据行程单的经纬度值来计算线上站点信息,这个静态方法的逻辑能保证所有的行程单数据都能放入到附近唯一的线上公交站点数据中去。

    public final static int STEP_LENGTH = 40;//网格模型的网格步长,单位 秒。
    public final static String NET_POINT_PREFIX = "net_point";//站点key的前缀
    public final static String KEY_SPLIT = ":";//redis key的间隔符

    //latitude:维度  , longitude:经度
    //根据传入的经纬度数值,得到它对应的网格节点队列
    public static String getNetPointKey(int latitude , int longitude) {

        Long lat =(long) Math.ceil(latitude*1.0/STEP_LENGTH) ;

        Long lon = (long) Math.ceil(longitude*1.0/STEP_LENGTH);

        return NET_POINT_PREFIX+KEY_SPLIT+lon+KEY_SPLIT+lat;

    }

计算司机应该去哪个站点接单?

司机接单会在app端根据汽车速度跟方向,会实时更新每次接单请求中的经纬度值,这个可以根据c端的策略选择,司机附近的行程单都没有的话,就去行驶一段距离后的区域中扫描站点,进行接单。派单服务侧实时为司机提供行程单。

3. 打车场景测试

我选择了以(1166060,406060)为中心,距离为200''的正方形区域作为测试区。总共有36个线上站点。在这些区域,随机选取位置发出行程单。同样在这个区域内随机出现新的司机(司机在这个区域做完上一笔订单,开始下一笔订单)。下面两个是具体的代码.

生成行程单

    while(true) {
            //随机获取乘客位置, 在(lati,lontu)点的100''区间内随机生成一个用户行程单
            latitude = DaCheUtil.getLatitu_lontude(DaCheUtil.lati-100, DaCheUtil.lati+100);
            longtude = DaCheUtil.getLatitu_lontude(DaCheUtil.lontu-100, DaCheUtil.lontu+100);

            tripOrder = new TripOrder();
            tripOrder.setLatitude(latitude);
            tripOrder.setLongtude(longtude);
            tripOrder.setUserName(userid+"");
            tripOrder.setDeparture("海淀黄庄");
            tripOrder.setDestination("西直门");
            passangerService.pushTripOrder(tripOrder);
            userid++;
            System.out.println("发出的订单:"+tripOrder.toString());

            TimeUnit.MILLISECONDS.sleep(50*1);
        }

随机出现司机


        while(true) {//循环随机在固定的这些区域,出现特定的司机

            Driver dirver = new Driver(id++, driverService);
            excutor.submit(dirver);
            TimeUnit.MILLISECONDS.sleep(100);

        }

redis中的站点信息

总共有36个站点。每个队列中存放的是行程单数据。

127.0.0.1:6379> keys *net_point*
 1) "net_point:10443:3599"
 2) "net_point:10442:3603"
 3) "net_point:10439:3601"
 4) "net_point:10439:3599"
 5) "net_point:10439:3600"
 6) "net_point:10438:3601"
 7) "net_point:10440:3601"
 8) "net_point:10439:3603"
 9) "net_point:10442:3601"
10) "net_point:10438:3598"
11) "net_point:10443:3598"
12) "net_point:10443:3602"
13) "net_point:10442:3599"
14) "net_point:10440:3600"
15) "net_point:10438:3602"
16) "net_point:10443:3603"
17) "net_point:10438:3599"
18) "net_point:10438:3603"
19) "net_point:10441:3598"
20) "net_point:10440:3599"
21) "net_point:10441:3602"
22) "net_point:10442:3602"
23) "net_point:10441:3603"
24) "net_point:10438:3600"
25) "net_point:10443:3600"
26) "net_point:10442:3598"
27) "net_point:10439:3602"
28) "net_point:10443:3601"
29) "net_point:10440:3598"
30) "net_point:10441:3600"
31) "net_point:10440:3602"
32) "net_point:10439:3598"
33) "net_point:10442:3600"
34) "net_point:10441:3599"
35) "net_point:10440:3603"
36) "net_point:10441:3601"

完整的demo项目,请关注公众号“前沿科技bot“并发送"打车派单"获取。

alt

正文到此结束
本文目录