项目源码
乐尚代驾项目: 重做乐尚代驾项目 (gitee.com)
一 项目介绍
1 介绍
【**乐尚代驾**】代驾是一种新型的出行服务模式,主营业务:酒后代驾、商务代驾、长途代驾,其主要特点是通过线上平台为用户提供代驾服务,伴随中国家庭汽车保有量的飞速增长,互联网代驾行业驶进了快车道,当前项目就是以此为背景设计出来的。
2 核心技术
后端技术栈
- **SpringBoot**:简化Spring应用的初始搭建以及开发过程
- **SpringCloud**:基于Spring Boot实现的云原生应用开发工具,SpringCloud使用的技术:(Spring Cloud Gateway、Spring Cloud Task和Spring Cloud Feign等)
- **SpringBoot+SpringCloudAlibaba(Nacos,Sentinel) + OpenFeign + Gateway**
- MyBatis-Plus:持久层框架,也依赖mybatis
- Redis:内存做缓存
- Redisson:基于redis的Java驻内存数据网格 - 框架;操作redis的框架
- MongoDB: 分布式文件存储的数据库
- rocketMQ:消息中间件;大型分布式项目是标配;分布式事务最终一致性
- Seata:分布式事务
- Drools:规则引擎,计算预估费用、取消费用等等
- GEO:GPS分区定位计算
- ThreadPoolExecutor+CompletableFuture:异步编排,线程池来实现异步操作,提高效率
- XXL-JOB: 分布式定时任务调用中心
- Knife4J:Api接口文档工具
- MinIO(私有化对象存储集群):分布式文件存储 类似于OSS(公有)
- 微信支付:微信支付与微信分账
- MySQL:关系型数据库 {shardingSphere-jdbc 进行读写分离; 分库,分表}
- Lombok: 实体类的中get/set 生成的jar包
- Natapp:内网穿透
- Docker:容器化技术; 生产环境Redis(运维人员);快速搭建环境Docker run
- Git:代码管理工具;Git使用,拉代码、提交、推送、合并、冲突解决
前端技术栈(不学)
- UniApp
- Vue3全家桶
- TypeScript
- GraceUI
- UniUI
- uniapp-axios-adapter
3 使用的云服务
因为我们开发的是打车类的微信小程序项目,因此要使用了大量的云服务(腾讯云或者其它云都可以)与微信小程序插件,列举如下:
| 序号 | 云服务名称 | 具体用途 |
| :--- | :--------------------- | :------------------------------------------------------- |
| 1 | 对象存储服务(COS) | 存储司机实名认证的身份证和驾驶证照片等隐私图片 |
| 2 | 人脸识别(AiFace) | 每天司机接单前的身份核实,并且具备静态活体检测功能 |
| 3 | 人员库管理(face-lib) | 云端存储司机注册时候的人脸模型,用于身份比对使用 |
| 4 | 数据万象(ci) | 用于监控大家录音文本内容,判断是否包含暴力和色情 |
| 5 | OCR证件识别 | 用于OCR识别和扫描身份证和驾驶证的信息 |
| 6 | 微信同声传译插件 | 把文字合成语音,播报订单;把录音转换成文本,用于安全监控 |
| 7 | 路线规划插件 | 用于规划司机下班回家的线路,或者小程序订单显示的路线 |
| 8 | 地图选点插件 | 用于小程序上面地图选点操作 |
| 9 | 腾讯位置服务 | 路线规划、定位导航、里程和时间预估 |
4 技术架构图
5 业务流程图
5 项目模块
最终服务器端架构模块
daijia-parent:根目录,管理子模块:
common:公共类父模块
common-log:系统日志管理
common-util:核心工具类
rabbit-util:service模块工具类
service-util:service模块工具类
spring-security:spring-security业务模块
model:实体类模块
server-gateway:网关
service:service服务父模块
service-coupon:优惠券服务模块
service-customer:乘客服务模块
service-dispatch:调度服务模块
service-driver:司机服务模块
service-map:地图服务模块
service-mq:mq测试服务模块
service-order:订单服务模块
service-payment:支付服务模块
service-rules:规则服务模块
service-system:系统服务模块
service-client:远程调用服务父模块
service-coupon-client:优惠券服务远程接口
service-customer-client:客户服务远程接口
service-dispatch-client:调度服务远程接口
service-driver-client:司机服务远程接口
service-map-client:地图服务远程接口
service-order-client:订单服务远程接口
service-payment-client:支付服务远程接口
service-rules-client:规则服务远程接口
service-system-client:系统服务远程接口
web:前端系统父模块
web-customer:乘客端web系统
web-driver:司机端web系统
web-mgr:管理端web系统
二 第一部分乘客端编写
这个项目总体代码分为四个部分。web端,client端,service端和网关。我们主要在web端,client端,service端实现业务逻辑。
1 乘客端登录:微信小程序登录接口
1.1 流程:
微信小程序发送请求到后端后,我们web端接受到code的值,判断code的合法性,之后向service发送请求,带着code,获得用户的token,定时储存到redis内,并返回给前端。service接收到web传来的code,利用WxMaService解析code,获得openid。之后判断是否是第一次登陆,如果是就将个人信息插入到数据库,利用uuid生成并返回token。如果不是第一次登陆直接返回token
1.2 细节
WxMaService是微信小程序提供的一个实现类,在这里可以解析微信小程序生成的code获取openid
2 乘客端登录:获取登录用户信息接口
2.1 流程
我们要先判断是否登陆。登陆的接口中将登陆用的token存到redis。web拿到前端传来的token查询redis,看是否是第一次登陆。如果是,远程调用,根据用户id来向service要用户信息并返回给前端。service接收到用户的id,查询数据库获得并返回用户的信息。
2.2 springmvc读取请求头
web拿token用springmvc的实现类读取http请求头来拿
public Result<CustomerLoginVo>
getCustomerLoginInfo(@RequestHeader(value = "token") String token)
3 乘客端登录:登录校验
3.1 流程:
我们要自定义一个注解,然后使用aop技术。做一个注解,放在需要做登陆校验的方法上。用aop增强,通过判断token在redis的状态,来判断是否登陆。因为这个注解在每一个微服务中都要用到,所以我们编写在工具类commom中。
3.2 spring环境获取请求头
在没有springmvc环境,我们可以用spring提供的类来实现获取http请求头
//1 获取request对象
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes sra = (ServletRequestAttributes)attributes;
HttpServletRequest request = sra.getRequest();
三 第一部分司机端编写
司机登陆与认证总体概览
1司机端小程序登陆功能
1.1 流程
(没什么难度)
2 获取登录司机信息
流程:(没什么难度)
3 腾讯云对象存储
3.1 基本介绍
项目基于腾讯云对象存储服务COS,存储司机认证相关资料(身份证、驾驶证)
要使用腾讯云对象存储服务,首先进行开通,注册腾讯云之后,开通就可以了
使用对象存储服务,可以在控制台里面进行操作,也可以使用Java代码进行操作,这些操作,腾讯云官方提供详细文档说明,按照文档就方便进行操作
官方文档:
对象存储 快速入门-SDK 文档-文档中心-腾讯云 (tencent.com)
3.2 基本流程
3.3 腾讯云的相关操作
基于腾讯云的相关操作我整合到了一篇文章内,在这里不多赘述
3.4 @PostMapping中 consumes参数
这个属于springmvc的内容,用于表示接收到的参数是文件类型
详细教学:
PostMappering中consumes与produces属性的作用_postmapping produces-CSDN博客
@PostMapping(value = "/cos/upload",
consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
3.5 @RequestPart("file") MultipartFile file
用于获取文件格式的请求参数
权威分析@RequestParam和@RequestPart 的区别(官方文档)_requestparam和request-CSDN博客
4 获得回调的URL
在专门讲腾讯云的那里有
5 腾讯云身份证认证接口
调用腾讯接口的在专门讲腾讯云的那里有
6 腾讯云驾驶证识别接口
调用腾讯接口的在专门讲腾讯云的那里有
四 第二部分乘客端编写
1 预估订单数据-预估驾驶线路
这里的逻辑是:前端将开始与结束的代驾地点的经纬度传给后端,后端调用腾讯的服务来找到开始与结束的最短的路线。返回给前端。有专门的一个微服务做这个接口
我们将使用腾讯云中的预估路线这一个接口。在专门讲腾讯云那里有。
2 预估订单金额
2.1 封装计费规则
我们使用drools来编辑规则,不直接编码。详细教程看drools那章
步骤一 配置类
配置一个类,将drools注入到spring容器里面
步骤二 编写输入对象,接收参数
步骤三 编写输出对象,返回参数
2.2 预估订单金额
web-service调用预估线路接口,将返回数据用规则判断一下,调用预估订单金额接口计算,最后将金额返回给前端
3 乘客下单
3.1 保存订单信息
需求:当乘客点击呼叫代驾后,系统生成订单并保存到数据库中
思路:
web端接收到订单的信息,远程调用service,来重新计算订单的路程与金额。service将路程与金额计算后还需要将订单信息加入到数据库中,然后将订单的信息传到前端。
3.2 实时更新司机的位置信息
步骤分析:
* 司机开启接单服务之后,司机端小程序实时把司机当前位置信息上传到Redis里面GEO
* 更新司机位置信息
* 删除司机位置信息
redis geo 地理位置详细分析和使用(地图必备学习)_geo地理位置-CSDN博客
3.3 获取司机个性化设置消息
从前端传来司机的id去查询数据库返回给前端
3.4 搜索附近适合接单的司机
需求
* 司机端小程序开启接单之后,司机上传当前位置信息到Redis里面GEO。
* 我们搜索附近适合接单的司机,比如搜索附近3公里以内时候接单司机
也是用到了redis的geo功能
4 司机抢单
4.1 需求分析
4.2 创建并启动任务接口
这里我们通过代码的方式来实现创建并启动任务。
我们先在xxl的admin(任务调度中心)中定义了任务的新增任务,启动任务,删除任务等的api
然后在dispatch这个微服务,定义一个执行器。这个执行器可以通过代码的方式来远程调用admin里面的接口,实现新增任务,启动任务,删除任务等功能。
4.3 开发具体任务job方法
其实这么多铺垫就是要实现搜索合适的司机接单。我们执行器里面的方法(job)就是来实现这个功能的。
我们在执行器定义一个方法,调用service具体详细实现搜索司机的逻辑。
4.4 阶段总结
乘客点击搜索司机的时候,传来订单的数据给后端乘客下单接口,这个接口会调用搜索附近司机接口。根据数据和逻辑来查找附近合适的司机。查找运用了定时任务调度xxl-job技术。
司机接单
一 总体流程
* 乘客下单之后,新订单信息已经在司机临时队列
* 下面司机可以开始进行接单了
首先,司机登录,认证(身份证、驾驶证、创建人脸模型)
第二,司机进行人脸识别(每天司机接单之前都需要进行人脸识别)
第三,司机开始接单了,更新司机接单状态
第四,当司机开始接单之后,删除司机之前存储到Redis里面位置信息
第五,当司机开始接单之后,清空司机临时队列新订单信息
二 判断司机当日是否做过人脸识别
1 分析
首先,找到数据库表,存储司机当日认证信息数据
实现过程:根据司机id和当日日期,两个条件进行查询,根据查询结果判断
2 获取当天日期并以指定形式输出
https://blog.csdn.net/qq_36789243/article/details/119362598
java.util.Date
在 Java 中,获取当前日期最简单的方法之一就是直接实例化位于 Java 包 java.util 的 Date 类。
Date date = new Date();
// this object contains the current date value
上面获取到的日期也可以被format成我们需要的格式,例如:
SimpleDateFormat formatter = new SimpleDateFormat("dd-MM-yyyy HH:mm:ss");
System.out.println(formatter.format(date));
三 人脸识别接口
1 需求
* 进行人脸识别,基于腾讯云实现
* 之前创建人脸识别模型,基于之前创建人脸模型完成当前识别功能
* 找到腾讯云文档1:照片比对
https://console.cloud.tencent.com/api/explorer?Product=iai&Version=2020-03-03&Action=VerifyFace
* 找到腾讯云文档2:人脸静态活体检测
https://console.cloud.tencent.com/api/explorer?Product=iai&Version=2020-03-03&Action=DetectLiveFace
四 更新司机状态
主要是在验证完人脸识别之后要将司机状态调成正在接单,从而不在接受接单信息
五 开启和启动接单服务web接口
根据id来查询司机的个性化信息,如果是开启了接单就可以呼叫接单,如果没有就不可以,并且不更新司机位置信息。
六 这里有个错误 业务逻辑错误!!!
就是他点击开始接单会显示未认证,然后跳转去认证那里。我这里直接修改了数据库信息。
修改了driver-info里面的auth-status,就是通过了认证
并且修改了代码,他的code不知道为什么不是1,我把他强行改成1了。
然后才能正常呼叫接单
七 操作xxl错误,未选择执行器错误!!!
在xxl章节有讲。
司机抢单
一 需求
当前司机已经开启接单服务了,实时轮流司机服务器端临时队列,只要有合适的新订单产生,那么就会轮回获取新订单数据,进行语音播放,如果司机对这个订单感兴趣就可以抢单,大家注意,同一个新订单会放入满足条件的所有司机的临时队列,谁先抢到就是谁的。
二 司机抢单接口
在乘客下单并且司机点击开始接单之后,司机页面会显示可以抢单的提示。当司机抢了之后,订单状态和司机状态都会改成正在接单。一个订单可以多个司机抢,司机也可以抢多个订单。这里就有个问题。刚才抢单接口里面,并没有考虑并发的问题,所以,产生问题,类似于电商里面超卖问题。一个订单同时被多个司机抢。
并发问题有三种解决方案。
第一种 Serializable
设置数据库事务的隔离级别,设置为Serializable,
数据库事务的隔离级别是用来解决并发访问数据库时可能出现的问题,如脏读、不可重复读和幻读。隔离级别越高,数据库的性能可能会越低,因为需要更多的锁和事务管理开销。隔离级别通常由数据库管理系统(DBMS)提供,并且可以在会话级别或事务级别设置。
Serializable
是最高的隔离级别,它通过锁定整个范围的数据来防止脏读、不可重复读和幻读。在 Serializable
隔离级别下,事务会以顺序执行的方式进行,即一个事务完成后,下一个事务才能开始。这样可以确保事务的完全隔离,但可能会导致大量的锁争用和性能下降。
第二种 乐观锁
使用乐观锁解决,通过版本号进行控制
乐观锁(Optimistic Locking)是一种在数据库管理系统中用于处理并发控制的机制,它假设在大多数情况下,数据在读取和更新之间不会发生冲突。乐观锁通常用于减少锁的使用,从而提高数据库操作的性能,特别是在高并发的环境下。
乐观锁的核心思想是,每次读取数据时,系统都会假设没有其他事务会同时修改这些数据。因此,它不会立即锁定数据,而是在数据更新时进行检查。如果在此期间数据被其他事务修改,乐观锁会采取一定的策略来处理这种情况。
实现方式
-
版本号机制:这是实现乐观锁的一种常见方式。每个记录都有一个版本号字段。当读取数据时,会记录下这个版本号。在更新数据时,会检查版本号是否与之前读取的版本号相同。如果相同,说明在此期间没有其他事务修改过这条记录,然后更新数据并将版本号加一。如果不同,则表示数据已被其他事务修改,此时可以采取重试、回滚或报错等策略
-
时间戳机制:另一种实现方式是使用时间戳。每次读取数据时,都会记录下当前的时间戳。在更新数据时,会检查当前时间戳是否大于记录的时间戳。如果大于,说明数据已被其他事务更新,可以采取相应的处理策略。
在项目中:
我们使用订单状态作为版本号,因为我们约定的规则是,订单状态1是待接单,2是已接单。那么我们可以在搜索的时候搜索1状态的,在改变订单状态之后将状态改为2.抢单逻辑结束。如果别人在去抢单,那么得先去找状态值,此时状态值已经改变了,用的是mysql
第三种 分布式锁
加锁解决,学习过synchronized 及lock锁,他们是本地锁,目前采用的是微服务架构,分布式部署方式。所以我们使用分布式锁方式解决相关问题
1、基于 Redis 做分布式锁
基于 REDIS 的 SETNX()、EXPIRE() 方法做分布式锁(原生)
(1)、setnx(lockkey, 1) 如果返回 0,则说明占位失败;如果返回 1,则说明占位成功
(2)、expire() 命令对 lockkey 设置超时时间,为的是避免死锁问题。
2、基于 REDISSON 做分布式锁
redisson 是 redis 官方的分布式锁组件。
在分布式锁章节讲了。我们的项目就是基于分布式锁解决的。就是在他抢单更改订单数据的时候加上锁,防止并发。
订单执行(一)
1 司机和乘客加载当前订单
无论是司机端,还是乘客端,遇到页面切换,重新登录小程序等,只要回到首页面,查看当前是否有正在执行订单,如果有,跳转到当前订单执行页面。
通过乘客或者司机的id去查找他们的最新订单信息并返回给前段
2 司机和乘客端获取订单信息
根据前端传来的订单id去查找订单的信息并返回
3 司机乘客同显
司机抢单成功后要赶往上车点,我们要计算司机赶往上车点的最佳线路,司机端与乘客端都要显示司机乘同显,这样乘客就能实时看见司机的动向。
3.1 司机端司乘同显
需求:司机所在地址司乘同显开始位置,代驾地址就是司乘同显终点,并计算司机司乘同显最佳路线
3.2 更新订单位置到Redis缓存
司机赶往代驾点,会实时更新司机的经纬度位置到Redis缓存,这样乘客端才能看见司机的动向,司机端更新,乘客端获取。
3.3 获取司机基本信息
乘客端进入司乘同显页面,需要加载司机的基本信息,显示司机的姓名、头像及驾龄等信息
3.4 乘客端获取订单经纬度位置
之前将司机的位置信息存到redis里面了,现在需要拿出来就可以
3.5 乘客端司乘同显
乘客端获取订单数据和司机信息
4 司机到达代驾起始点
司机到达代驾起始点,司机手动触发“到达乘客起点”按钮,更新订单状态
* 司机到达代驾起始点之后,更新当前代驾订单数据
* 更新订单状态:司机到达
* 更新订单到达时间
5 司机更新代驾车辆信息
司机到达代驾起始点,联系了乘客,见到了代驾车辆,要拍照与录入车辆信息
订单执行(二)
司机录入车辆信息后,就开始代驾了。开始代驾后,司机端与乘客端同样要进入司乘同显页面,司乘同显的起始点与终点就是代驾订单的起始点与终点,这里我们不需要提供额外的接口,直接使用“计算最佳驾驶线路"接口即可。
订单在在代驾过程中,我们需要保存驾驶途中的GPS定位,将来我们计算代驾真实里程的时候,就需要用到这些坐标点。那么这些定位点保存在MySQL中可以吗?当然不行,MySQL单表记录超过千万行就开始变慢了。那么保存再哪里呢?保存到MongoDB中。
订单在代驾的过程中,司机端小程序要实时采集录音,把录音和对话文本上传到后端系统,将录音监控保存到Minio,对话文本保存到MongoDB中
前提技术学习MongoDB 在另外一篇有讲
1 开始代驾
代驾开始,我们要更新订单状态及相关信息
2 批量保存订单位置信息
司机开始代驾后,为了减少请求次数,司机端会实时收集变更的GPS定位信息,定时批量上传到后台服务器。
3 获取订单最后一个位置
* 司机开始代驾之后,乘客端获取司机最新动向,就必须获取到司机最后一个位置信息
* 从MongoDB获取
4 Minio上传接口MongoDB
司机代驾过程中,司机端小程序实时采集录音,把录音和对话文本上传到后台服务MongoDB,把录完监控保存Minio
司机在代驾的过程中要上传录音文件信息,我们可以保存到Minio里面。毕竟我们是拿Minio充当私有云来使用的,当前我们来封装Minio的上传接口
5 保存订单监控记录数据
只要开始代驾,司机端小程序就要录制司乘对话,直到代驾结束,才停止录音。
司机端小程序怎么录音呢?同声传译插件可以实现录音,并且把录音中的语音部分转换成文本。这是前端小程序实现的功能,我们只做了解。
我们将前端传来的语音存到了MongoDB里面
6 腾讯云COS图片审核
凡是上传到腾讯云COS私有桶的图片,我都需要审核一下。在我们上传图片后对图片做一个审核,如果图片违规就删除图片并抛出异常
7 封装文本审核接口
接口文档地址:https://cloud.tencent.com/document/product/460/61607
审核结果文档地址:https://cloud.tencent.com/document/product/460/56284
这里我们对语音转过来的文字,也就是文本进行审核
8 更新订单监控记录
前面我们保存了司乘之间的对话文字内容,接下来我们要对文字内容加以审核,看看对话的内容是否包含色情或者暴力的成分,我们要对聊天内容做一个安全评级。如果用人工去监视司机和乘客之间的对话,那成本可太高了,而且也雇不起那么多人,所以我们要用AI去监控对话的内容。
上面我们封装了腾讯云数据万象文本审核接口,数据万象可以对用户上传的文本进行内容安全识别,能够做到识别准确率高、召回率高,多维度覆盖对内容识别的要求,并实时更新识别服务的识别标准和能力。
9 订单监控统计
我们必须对司乘之间的对话内容做监控,保障乘客人身安全是我们首要的任务。腾讯的数据万象服务可以审核文本内容,咱们要把这个功能利用起来。我们调用腾讯万象的接口,审核司乘之间的对话内容,看看是否包含色情或者暴力的成分。审核的结果咱们要更新到order_monitor数据表中,每个订单一条记录。
我们现在开启订单的时候初始化订单的order_monitor数据表,然后在上传文本和图片的时候来录入订单的信息
订单执行(三)
1 需求
* 司机到达代驾终点,代驾结束了。
* 结束代驾之后,
-- 获取额外费用(高速费、停车费等) -- 计算订单实际里程
-- 计算代驾实际费用 -- 系统奖励
-- 分账信息 -- 生成最终账单
2 计算订单实际里程
在MongoDB保存代驾过程中司机位置信息,把MongoDB存储司机位置信息获取出来,以时间排序,连接成一条线,这条线是实际距离
3 计算系统奖励
平台会根据一定的规则,奖励司机。我们要根据订单的信息来奖励司机
4 根据时间段获取订单数
传入开始时间和结束时间,查找订单表来计算数量
5 计算分账信息
结束代驾之后,计算分账信息,平台按照一定规则抽成处理,将分账信息记录数据库表
6 结束代驾更新账单
* 更新订单数据:订单状态、订单实际距离、订单实际金额等
* 添加实际账单信息
* 添加分账信息
7 结束代驾
将上面的接口一一调用,实现上面的功能
8 智能判断司机刷单行为
判断司机刷单的办法也很简单,司机点击到达上车点按钮的时候,司机端小程序通过腾讯地图服务的API,计算当前定位到上车点的距离。如果超过1公里,那就不可以。司机必须距离上车点在1公里以内,点击到达上车点才有效。当司机想要结束代驾的时候,距离代驾终点必须在2公里以内才可以,否则就无法结束代驾。
只是上面的限制也是不够的,我们后面还可以加上预期的里程与实际的里程的一个距离差来进一步判断。