最近在做一个物流的项目,自己负责线路配置这个模块还有部分接口的开发。
线路配置里面有三个模块:专线列表、班次模板、班次列表。
专线列表主要是记录区域车次停靠点,班次模板主要是用于批量生产班次,班次列表是实际业务逻辑要用到的数据,也是呈现给用户看的数据。
业务流程:
后台方面:
在专线列表里面添加区域点,通过区域点,可以一键生成班次模板,通过班次模板可以一键生成班次列表;
譬如a,b, c, d, e: 如图 1
然而生成班次模板的算法也很简单,将专线生面的区域点,按照顺序排列,采用排列组合的方式生成班次模板,然后在班次模板里面完善发车时间,耗时,价格等数据。
五个点生成10个班次模板就是 :如图 2
通过班次模板生成连续多少天生成批量生成具体的班次,那班次的具体开始时间,结束时间,价格等都会生成。
接口方面:
1.也就是和携程里面一样,输入地点和起始时间,可以找到相应的班次信息。
2.减箱子操作,类似火车票卖出去一张票的操作(这个我花的心力比较多)
当然重点就是减箱子这个接口:
首先拿到需求的时候,专线只有单单一条(每个专线互不相关),并无子线这一说,所以我先以没有子线的时候说起:
假设现在专线上面有如图五个区域点为例子:
假设b - d 这一段卖出 ,那么受影响的班次就有a - d , a - e , b - d , b - e , c - d , c - e, 也就是说所有涵盖b - d这一段的班次和b - d里面的班次都要进行减箱子操作,那么怎么查找这些受影响的班次呢?正常直观的思维,找到所有起点是b的班次和所有终点是d的班次,还有起点终点是c的班次(如果中间何有c1,c2等,那么就得起点终点是,c,c1,c2的班次,依此类推),具体班次如图:3
通过图可以发现,出现了重复的班次,如果专线更长一点的话,那么就会造成资源的很多浪费,那么怎么解决呢。仔细看这些要减的班次,反观那些没有减的班次,会很容易发现,除了起点之前(包括起点)生成的班次和终点之后(包括终点)生成的班次不受影响,其他班次都要受影响,这样去思考问题就简单多了:如图
突然发现,自己的任务挺简单的,很容易就解决了;然而很快就发现高兴早了,需求有变动了,子线这个问题出来了,如图:4
所以以前生成班次模板的代码也要改,刚开始的事后想过用链表,后来发现其实没必要,只要变成下图的样子就行了,以前的代码也不用大改:如图 5
好了,现在回归正题,这个时候减箱子也会涉及到子线的问题了(这里说明一下,B1到D2其实是三辆车在跑,只不过是箱子运到B后换另一辆继续跑,有点像换乘,但是一个车一次就只能拖那么几个箱子,所以当b-d卖出一个箱子后,对应的b1 - d2也会减一个箱子),或许你会说直接像上述图5一样的延展,沿用原来的思想不就很好了吗,如果真这样做了,那么问题会很大,一方面会有减漏掉的bug,另外一方面这样做也会有很多不必要的查询,同时要遍历所有班次,这其实也是一件挺耗性能的事(虽然我一开始就是这样做的,后来重构代码的时候仔细考虑了下就改了),这么说你可能不服,先来谈谈有什么bug吧。同样我们以b - d 为列
1.第一个bug,我们可以很直观的发现b1 - b2 这趟班次是不用涉及到减箱子的操作了,应为这是另外一趟班次了。解决办法:设置一个判断语句,起点终点是同一子线上的点事就不进行减箱子操作,这样貌似好像解决,暂时不谈对性能的影响;
2.第二个bug,倘若b - d减了一个箱子,那么理所当然A -b1也要减一个箱子,其实并不用减。这次可就没法像上面那样好处里。
看到这里其实你大概应该有点想法,这样做其实不可行。我们可以直接沿用最开始的线路,不涉及子线按照原先的方法找到要减的线路,然后用原先线路的起点下面的子点和自己(如B,B1,B2),原先线路的终点下面的子点和自己(如D,D1,D2)进行笛卡尔乘积,这样就完美的避开了上述的两个bug;这里我就不细说,你可以自己去验证。
看到这里你可能觉得已经完了,事实没有,因为这样减箱子存在重复减的问题,如图:
倘若一个车一次只能装载6个箱子,那么初始的时候b-c, c-d, b-d,就都是6个箱子,倘若按照以前的算法,b-c卖掉6个箱子,这个时候b-d就和b-c一样只剩下0个箱子了,这个时候c-d再减一个箱子,那么就会如上图所示出现负值。出现重复减的状况!
针对这个状况,我是这样设计的。为了方便理解,我们将箱子当做有多少个座位,不考虑过程,只考虑发车前有多少个座位(最终重点就不计了),我们设置一个数组用来存放a-b, b-c, c-d, d-e的座位数(a,b,c,d用来存放座位数),倘若是b点(b-c)卖掉2个座位,c(c-d)卖掉1个座位,那么这个时候b-d就取卖掉座位多个剩余座位数,也就是去最小剩余座位数b.
下面是我的代码实现:
@Service("containeNumOperateService")
public class ContaineNumOperateService {
@Resource(name = "daoSupport")
private DaoSupport dao;
/**
* 通过主线的起始点,将子线(包括自己)进行箱子数量修改操作
* @param startCity
* @param endCity
* @param pd
* @param number 最小箱子数量
* @throws Exception
*/
private void changeClassesContaineNum(String startCity, String endCity,PageData pd,int number) throws Exception {
String specialLineName = pd.getString("SPECIAL_LINE_NAME");
//查询当前节点所有的子节点:如果当前节点是子节点,那么就查询兄弟节点(包括自己)
List<String> startChildSpecialLineList = getChildSpecialLineList(startCity,specialLineName);
List<String> endChildSpecialLineList = getChildSpecialLineList(endCity,specialLineName);
//int number = minContaineNum(majorList, specialLineArray, startCity, endCity);
//将已知起始点的子线(包括自己)进行箱子数量修改操作
changeContaineNum(startChildSpecialLineList,endChildSpecialLineList,number,pd);
}
/**
* 将已知起始点的子线(包括自己)进行箱子数量修改操作
* @param startChildSpecialLineList
* @param endChildSpecialLineList
* @param number 最小箱子数量
* @param pd
* @throws Exception
*/
private void changeContaineNum(List<String> startChildSpecialLineList,List<String> endChildSpecialLineList,int number,PageData pd) throws Exception{
for(int m=0;m<startChildSpecialLineList.size();m++){
for (int n=0;n<endChildSpecialLineList.size();n++){
//设置查询条件,
PageData pdQuery = new PageData();
pdQuery.put("START_CITY", startChildSpecialLineList.get(m));
pdQuery.put("END_CITY", endChildSpecialLineList.get(n));
pdQuery.put("SPECIAL_LINE_NAME", pd.getString("SPECIAL_LINE_NAME"));
pdQuery.put("TRAIN_NUMBER_ID", pd.getString("TRAIN_NUMBER_ID"));
PageData classesPd = (PageData) dao.findForObject("Classes_listMapper.findSingleClasses", pdQuery);
//比较当前线路箱子数与minNum是否相同,如果不同则将该班次箱子数量改为minNum
int pdContaineNum = (Integer) classesPd.get("CONTAINE_NUM");
if (number != pdContaineNum) {
classesPd.put("CONTAINE_NUM", number);
//classes_listService.updateContaineNum(classesList.get(0));
dao.update("Classes_listMapper.updateContaineNum", classesPd);
}
}
}
}
/**
* 查询当前节点所有的子节点:如果当前节点是子节点,那么就查询兄弟节点(包括自己)
* @param cityName
* @param specialLineName
* @return
* @throws Exception
*/
private List<String> getChildSpecialLineList(String cityName,String specialLineName) throws Exception{
PageData queryPd = new PageData();
queryPd.put("CITY_NAME",cityName);
queryPd.put("SPECIAL_LINE_NAME",specialLineName);
List<PageData> ChildSpecialLineList = (List<PageData>)dao.findForList("SpecialLineMapper.findBySuperiorSpecialLineAndParentCity", queryPd);
List<String> cityNameList = new ArrayList<>();
cityNameList.add(cityName);
for(PageData pd:ChildSpecialLineList){
cityNameList.add(pd.getString("CITY_NAME"));
}
return cityNameList;
}
/**
* 查看当前城市的父节点,如果不存在就是当前值
* @param majorCityList
* @param cityName
* @param specialLineName
* @return
*/
private String getParentCity(List<String> majorCityList,String cityName,String specialLineName) throws Exception{
if(!majorCityList.contains(cityName)){
PageData queryPd = new PageData();
queryPd.put("CITY_NAME",cityName);
queryPd.put("SUPERIOR_SPECIAL_LINE",specialLineName);
PageData spd = (PageData)dao.findForObject("SpecialLineMapper.findBySuperiorSpecialLineAndCityName", queryPd);
cityName = spd.getString("PARENT_CITY");
}
return cityName;
}
/**
* 修改起点到终点之间所有的值
*
* @param majorList
* @param specialLineArray
* @param startCity
* @param endCity
*/
private void changeArray(List<PageData> majorList, int[] specialLineArray, String startCity, String endCity, int num) {
int sIndex = getIndex(majorList, startCity);
int eIndex = getIndex(majorList, endCity);
if (majorList != null && specialLineArray != null) {
if (sIndex < eIndex) {
for (int i = sIndex; i < eIndex; i++) {
specialLineArray[i] += num;
}
}
}
}
/**
* 获取专线节点位置
*
* @param majorList
* @param cityName
* @return
*/
private int getIndex(List<PageData> majorList, String cityName) {
if (majorList != null) {
for (int i = 0; i < majorList.size(); i++) {
if (majorList.get(i).getString("CITY_NAME").equals(cityName)) {
return i;
}
}
}
return -1;
}
/**
* 获取节点之间最少箱子数量
*
* @param specialLineArray
* @param startCity
* @param endCity
* @return
*/
private int minContaineNum(List<PageData> majorList, int[] specialLineArray, String startCity, String endCity) {
int sIndex = getIndex(majorList, startCity);
int eIndex = getIndex(majorList, endCity);
int min = specialLineArray[sIndex];
if (sIndex < eIndex) {
for (int i = sIndex + 1; i < eIndex; i++) {
if (min > specialLineArray[i])
min = specialLineArray[i];
}
}
return min;
}
/**
* 减去囊括此班次相关班次的箱子数量
*
* @param num 倘若为负数就是减箱,正数为加箱
* @param classListId
* @throws Exception
*/
public void changeContaineNum(String classListId, int num) throws Exception {
//修改包含班次箱子的数量
PageData pd = new PageData();
//pd.put("CLASSES_LIST_ID","885d9c5644b547b2874b49cc9acdc4f3");
pd.put("CLASSES_LIST_ID", classListId);
//通过班次id获取该班次具体信息
//classes_listService.findById(pd);
pd = (PageData) dao.findForObject("Classes_listMapper.findById", pd);
//获取班次起点和终点
String startCity = pd.getString("START_CITY");
String endCity = pd.getString("END_CITY");
String specialLineName = pd.getString("SPECIAL_LINE_NAME");
String trainNumberId = pd.getString("TRAIN_NUMBER_ID");
//通过专线名获取该干线上的所有点
List<PageData> majorList = (List<PageData>) dao.findForList("SpecialLineMapper.findByName", pd);
//通过专线名和车次id获取这个车次上所有的班次
//List<PageData> allClassesList = (List<PageData>) dao.findForList("Classes_listMapper.findClassesByTrainNumber", pd);
//存储干线上所有的点
List<String> majorCityList = new ArrayList<>();
//存储干线上起点之前的点
List<String> preCityList = new ArrayList<>();
//存储干线上终点之后的点
List<String> nextCityList = new ArrayList<>();
//将主线上的点都存入majorCityList
for (int i = 0; i < majorList.size(); i++) {
String majorCity = majorList.get(i).getString("CITY_NAME");
majorCityList.add(majorCity);
}
int i = 0;
//获取该班次起点之前(包括起点)的所有地点
for (; i < majorCityList.size(); i++) {
String preCityName = majorCityList.get(i);
if (preCityName.equals(startCity)) {
preCityList.add(preCityName);
break;
}
}
for (; i < majorCityList.size(); i++) {
if (majorCityList.get(i).equals(endCity)) {
break;
}
}
//获取该班次终点以后(包括终点)的所有地点
for (; i < majorCityList.size(); i++) {
nextCityList.add(majorCityList.get(i));
}
//将所有专线放入数组中,并查出改点的箱子数
int[] specialLineArray = new int[majorCityList.size() - 1];
//查询各个最短节点的箱子的数量,
for (int k = 0; k < majorCityList.size() - 1; k++) {
String sCity = majorCityList.get(k);
String eCity = majorCityList.get(k + 1);
PageData queryPd = new PageData();
queryPd.put("START_CITY", sCity);
queryPd.put("END_CITY", eCity);
queryPd.put("SPECIAL_LINE_NAME", specialLineName);
queryPd.put("TRAIN_NUMBER_ID", trainNumberId);
//List<PageData> classesList = classes_listService.classesListByConditionMiddle(queryPd);
PageData classesList = (PageData) dao.findForObject("Classes_listMapper.findSingleClasses", queryPd);
if (!classesList.isEmpty()) {
int containeNum = (Integer) classesList.get("CONTAINE_NUM");
//map.put(sCity, containeNum);
specialLineArray[k] = containeNum;
} else {
//当这个班次停运,那么会出现缺省,用一个远大于车次能装载的箱子数代替,由于每次都是取最小的箱子数,所以不会影响结果
specialLineArray[k] = 10000;
}
}
//判断当前班次的起始点是否是干线上的点,如果不是就替换为主线上的点
startCity = getParentCity(majorCityList,startCity,specialLineName);
endCity = getParentCity(majorCityList,endCity,specialLineName);
//修改后台传递过来的班次的箱子数量
//修改当前班次影响的专线点的数量
changeArray(majorList, specialLineArray, startCity, endCity, num);
//修改与当前班次相关的班次的箱子的数量
for (int j = 0; j < majorCityList.size() - 1; j++) {
String startCityName = majorCityList.get(j);
for (int k = j + 1; k < majorCityList.size(); k++) {
String endCityName = majorCityList.get(k);
if ((preCityList.contains(startCityName)) && preCityList.contains(endCityName) || (nextCityList.contains(startCityName) && nextCityList.contains(endCityName))) {
continue;
}
int currntNumber = minContaineNum(majorList, specialLineArray, startCityName, endCityName);
changeClassesContaineNum(startCityName,endCityName,pd,currntNumber);
}
}
}
}
扩展情况
如过是这种又该怎么搞,采用图来做吗?
更近一步,还可能是这样的。
依次类推更加复杂的情况。
总结与思考
这个项目收获最大的就是这个减箱子接口的开发,通过这个接口的开发我发现自己刚开始写的代码很烂,同时逻辑经常考虑得不够周到,总是边测边改,很影响效率。思考了很久,发现还是得画流程图,通过图思考,能更好的发现逻辑漏洞!!
代码整体逻辑写完后,当然也不要忘记抽取重复的代码封装成方法,这点可以看看jdk的一些源码,慢慢的就会耳濡目染,知道该怎么封装方法了。当然最好是去看《重构-改善既有代码设计》进行系统的学习,这个书我还没有看,但是已经在计划中了!!