“南航顺风车”

第一周(2018/7/16 - 2018/7/23) 

明确接口定义,和前端沟通好了URL格式(比如:http://localhost:8080/login?name= ) ,请求体(position、seats、id、name之类的)和返回类型(比如我的登录接口返回的是String)。然后开始各自开发,前后端的分离。

作为一枚后台小白,java以及框架都成为我要学习的模块。导师列出了一系列的技术难点,比如springboot、springMVC、restful API、JSON语言等等。由于对接口是什么都不清楚,我前期花了大量时间去了解springboot和接口。包括,一个完整的springboot项目目录应该如下:

com
  +- example
    +- myproject
      +- Application.java
      |
      +- domain
      |  +- Customer.java
      |  +- CustomerRepository.java
      |
      +- service
      |  +- CustomerService.java
      |
      +- controller
      |  +- CustomerController.java
      |

1、Application.java 建议放到跟目录下面,主要用于做一些框架配置

2、domain目录主要用于实体(Entity)与数据访问层(Repository)

3、service 层主要是业务类代码

4、controller 负责页面访问控制

此外,Spring Boot的基础结构共三个文件:

l. src/main/java  程序开发以及主程序入口

ll. src/main/resources 配置文件

lll. src/test/java  测试程序

第一周之后,我才认识到:“URL”格式和请求体都是后端定义的,前端按照这个格式传参数即可。如(http://localhost:8080/login?name=)前端可以在后面写“张三”那么就把对应的字符串POST到了后端。而URL的格式在后端controller是通过注解@RequestMapping(value = "/login")来自主定义的。

 

第二周(2018/7/24 - 2018/7/31)

经过第一周的配置环境和软件下载 ,并且在看了一大堆案例的情况下,对springboot框架有了较深的理解。接着我下了一个sqlyog图形界面方便建表和查看表内字段值,下载了postman作http操作(Get\Post\Update\Delete)。对了顺便列一下software:

开发语言:JAVA 、 Spring Data JPA、少量原生SQL语句

轻量级框架:springboot

编辑器:ecplise(STS)插件 -->Spring Starrer Project        

PS:下次要试下主流的Intellj IDEA,xml文件还是比application.properties要便捷很多,可以合并某些字段头

HTTP操作模拟器:Postman

附一下项目的配置(application.properties):

spring.datasource.url = jdbc:mysql://127.0.0.1:3306/csu?useUnicode=true&characterEncoding=UTF8
spring.datasource.username = root
spring.datasource.password = 123456
spring.datasource.driverClassName = com.mysql.jdbc.Driver
 
 
# \u914D\u7F6E\u6570\u636E\u5E93
spring.jpa.database = MYSQL
# \u67E5\u8BE2\u65F6\u662F\u5426\u663E\u793A\u65E5\u5FD7
spring.jpa.show-sql = true
# Hibernate ddl auto (create, create-drop, update)
spring.jpa.hibernate.ddl-auto = update
# Naming strategy
spring.jpa.hibernate.naming-strategy = org.hibernate.cfg.ImprovedNamingStrategy
# stripped before adding them to the entity manager)
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5Dialect

第二周遇到的比较困难的点在于JPA,项目前半段三张表“司机表--用户表--乘客表”。JPA语言虽然不需要写任何一句原生sql语句,并且通过一些注解可以实现自增和外键约束。但是JPA的关联查询及其复杂。难点如下:

1.要访问某个表,JPA需要三个类:(1)实体类 (2)数据接口repository (一般要继承JPARepository或者CURDRepository) (3)Controller类。里面的方法与一般的java方法区别仅在于顶部有路由路径,并且是由@requestmapping注解声明的。还有一个很致命的问题在此提一下:不同的Controller类的方法不可以相互调用!对于普通的java类,可以new一个对象来继承别的类就可以使用它的成员及其方法。但在springboot中你是不可以的,那怎么解决呢?

比如A controller类的B方法要调用 C controller类的D方法。你只能在A类下声明C类controller对应的数据接口对象c1,然后把D方法写在A类中(此时B、D方法都在一个controller里,但同时D方法可以在A类里使用到数据接口c1)。示例如下:

       public class DistanceGetController {

	  	@Autowired
	    private  RestTemplate restTemplate;
	  	@Autowired
		private PassengerInfoRepository passengerRepositoy;
	  	@Autowired
		private DriverInfoRepository driverRepositoy;

在整个项目完成过程中,给我很大困扰的,是JPA外键的约束问题。先来解答几个问题:

1.什么是外键,外键有什么要求,多对一情况下外键设置在哪?

外键是不同表之间建立关联的重要手段,外键字段可以不是主键但一定要建立索引,但被外键引用的字段一定要是该表的主键。多对一、一对多情况下外键都是设置在“多”的一方。

2.JPA外键怎么设置?

    @Id
	@GeneratedValue(strategy = GenerationType.AUTO)
	private int id;
	@NotNull
	private String name;
	@OneToMany(cascade={CascadeType.ALL},fetch = FetchType.LAZY,mappedBy = "user")
	private List<DriverInfo> driver;
    @JsonManagedReference
	public List<DriverInfo> getDriver() {
		return driver;
	}

	public void setDriver(List<DriverInfo> driver) {
        this.driver = driver;
    }
public class DriverInfo {

	@Id
	@GeneratedValue(strategy = GenerationType.AUTO)
	private int id;
	@NotNull
	private String startPlace;
    @ManyToOne
	@JoinColumn(name = "user_id")

	private LoginInfo user;

	@JsonBackReference
	public LoginInfo getUser() {
		return user;
	}

	public void setUser(LoginInfo user) {
		this.user = user;
	}

由一些注解组成,一对多、多对一的时候,@OneToMany 和@ManyToOne 不要混淆,此外一定要在自己的实体类里写上对方的

字段。在ORM模型是通过JPA的外键字段映射到另一张表的,这里特别神奇。比如有司机表实体类driverInfo。先new一个对象:driverInfo aa = new driverInfo; 然后通过aa.getUser().getName() 可以从司机表映射查询到另外一种表用户表的name字段。但是

JPA的关联查询 存在很大的问题 :json序列化及反序列化。在“顺风车”项目中,司机表和乘客表都双向关联着用户表。

A<---> B <---> C 意味着:在查询A表的时候会带出B表,同时B表还可以带出A表(双向)这时候便产生了死循环。花了差不多一周的时间 都没能找到支持双向关联查询的解决办法。最后为了防止序列化,只能加上上图的注解,抑制双向关联,不再死锁。

第三周(2018/8/1 - 2018/8/8  )

经过两周的时间 ,把司机、乘客发布行程的接口写好了。在和导师沟通过后,决定要做两件事:(1)统一异常处理 (2)登录信息存放在session 里面。在session这一部分,也遇到了一些困难。session的存储其实很简单,寥寥几行代码,用到httpsession的reponse和request即可,这里也可以记录下:

        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = requestAttributes.getRequest();
		HttpSession session = (HttpSession) request.getSession();
        //下面是存进session
        session.setAttribute("userId", user.getId());//前面是键,后面是值

        //从session取
        int userId = (int) session.getAttribute("userId");//取出session值
        system.out.println(userId);//输出来看看~

 

一般作登录接口的时候,都会用到session或者cookie。我在项目中用session是想把用户表对应id映射到外键字段里。十分的方便。

开始 着手写匹配度接口,经过2天的讨论,我们把算法确定了一下:先调用高德开发平台/路径规划/驾驶路径API,算出 顺风车司机行程的距离distance,接着将乘客起点终点设置为途经点,计算出新距离distance 1。然后自定义匹配度规范:多走的距离distance-distance1 每 0.5km 减掉1%的匹配度,并且只对前端返回附近的匹配度 >50% 的司机/乘客。

记录下这部分算法的难点吧:

(1)第一次接触到后台调用第三方API。原本一直以为前端js才可以调用api,后来了解到当后台需要第三方数据时,后台可以当做前端,给第三方发请求,这是一样的道理。这里使用的是导师介绍的Resttemplate:

//主要代码就两句
//lat 、lng是经纬度变量 
String url="https://restapi.amap.com/v3/direction/driving?origin="+lat+","+lng+"&destination="+lat1+","+lng1+"&extensions=all&output=json&key=924c44ade82fbc30ab8e19ffba3c67e6";//调用第三方API的url
JSONObject json = restTemplate.getForEntity(url, JSONObject.class).getBody();//接收返回的JSON数据

(2)JSON解析

拿到第三方返回的JSON数据时,第一反应是发了条朋友圈,但是冷静下来后发现解析真的繁琐。我们知道,JSON数据流是由JSONArray以及JSONObject两种形成,对应表现形式是{ }和[ ] 。那么对应的解析方法可以详见https://www.cnblogs.com/xudong-bupt/archive/2013/05/06/3060745.html(转载“旭东的博客“)。简而言之,需要从上往下一层层对数组及object对象进行解析。提一下object解析吧,由于是由“键-值“组成,所以只能通过键来提取出值。

(3)当前司机,遍历乘客表乘客,计算对应的匹配度,并返回>50%的 乘客

首先,解决下当前司机的问题。在这里我用的是全局变量 。在司机提交行程接口取出司机的经纬度,保存在全局变量m1、n1、m2、n2

其次,是遍历的问题,如雨后春笋搬冒出许多问题:

  • 由于设置表关联,单表的id不是连续的。如A表插入id=110的数据,B表插入的就会是id=111,下次再往A表新增数据时,插入的是id=112,因此仅仅写一个for循环是不够的,因为id++不成立(id不连续)

       解决办法:找到第一个元素输出其对象 ,用getId()获取乘客表第一行的id;用count方法获取总行数。

  • 虽然解决了for循环的起始问题,但是便利的时候还是无法确定id。经过一天的时候(那天有点笨maybe...)决定用findALL 和get(i)方法分别获取i次的id,再进行取经纬度 操作。
        Long b1 = driverRepositoy.count();//获取总行数
		String[] dis1 = new String[(int) (b1+1-1)];
		
		for(int i=0; i < b1 ;i++){
			
			List<DriverInfo> abc = driverRepositoy.findAll();
			DriverInfo def= abc.get(i);//获取乘客表第i个元素
			int a1 = def.getId(); //id
			
		DriverInfo driverEntity = driverRepositoy.findDriverInfoById(a1);
		double lat2 = driverEntity.getStartPosition2();
		double lng2 = driverEntity.getStartPosition1();
		double lat3 = driverEntity.getEndPosition2();
		double lng3 = driverEntity.getEndPosition1();
  • 一开始打算构建一张新表,关于“乘客信息以及匹配度”,再写一个接口让前端GET。但是当一切都写好了之后,包括我把匹配度从单个数组取出,将乘客信息取出,设置断点发现都能存进实体类。但是最后的最后无法执行save操作,即将实体类数据保存到数据库。查看save官方文档发现,save(entity)的实体只能是前端接收的形参组成的。也就是说该方法参数形式必须是(@requestbody 数据类型 形参)。而前端传过来的数据是json数据,我尝试将数据用json封装,但是依旧无法save。

        解决办法:改为自行封装json,并直接返回前端。使用JPA的save方法优点在于:把数据保存在数据库后,再用JPA语句返回前端是会自行封装好的。而手动封装容易出错,而且繁琐。我最后封装的格式是:[{object1} , {object2},{object3} ]

  • 对于匹配度50%以上的筛选,是用一个if else 语句及continue来完成的。如果大于50%则执行jsonobject.put()操作,否则continue使id++
  • 优化算法的时候,发现全局变量的设置对于用户量大的情况,可能会出现因大数据产生订单覆盖的问题。最后总结出三种改进思路:(1)消息队列  (2)线程池  (3)预先设置约束条件,筛减符合的乘客数量,减少遍历次数

        解决办法:最后我并没有选择以上几种,我还是选择最熟悉的Session机制。Sesion优点在于用户唯一性,客户端(服务器)会对每一个访问的用户分配一个新的session对象,这个对象有着唯一的session_id。因此,我把对应司机/乘客的订单信息(算法只用到经纬度信息)存储在Session里面再对应取出来。这样就不会在A司机提交行程的时候,出现被B司机行程覆盖的问题。

第四周(2018/8/9 - 2018/8/16)

继续往下走,完善智能匹配路线模块。这部分想让前端响应用户操作,跳转新界面。

实现功能如下:前端点击选中乘客,则把该司机和该乘客的经纬度POST后台,后台再按照前端需要的格式封装返回给前端。前端调用高德路线显示API,展示接单后路线。这部分要注意把乘客起点终点当成途经点。

ps:忍不住想吐槽一下前端partner,我感觉应该小数据流的保存是没有问题的。发后台转前端实在增加时间复杂度。

暂时写到这啦,第一篇技术博客。后台小白,冲鸭!                 

猜你喜欢

转载自blog.csdn.net/qq_38622452/article/details/81388904