基本预定车票功能的开发
对于乘客购票来说,需要有每一个车次的余票信息,展示给乘客,供乘客选择,因此首个功能是余票的初始化,之后是余票查询,这两个都是控台端。对于会员端的购票,需要有余票查询以及乘客的选择,不仅仅支持给自己买票,还可以给其他人买票,而且还可以选择座位类型,是一等座还是二等座,可以选择座位,最后是下单购票。
余票信息表
对于购票表来说,最为重要的字段是售卖字段,对于这个字段来说,将经过的车站用0和1拼接,如果是0表示可卖,1表示不可卖。例如有ABCD四个站,那么000表示这四个站都可以买,最终是可以通过售卖信息来计算出余票的信息。余票查询会显示还有多少张票,票数如果通过实时计算,会影响性能,所以应该另外做张表(余票信息表),直接存储余票数,这张表通过购票表的售卖字段定时的更新此表的信息。这张表是火车的一个子表,可以看作用余票的角度观察火车,因此需要包含id,日期,车次以及出发站和到达站的信息。对于出发站和到达站来说,重要的是这个站在整个车次是第几站,以及每一个站站记录它的余票信息。唯一键是日期,车次,出发站和终点站。
为什么时日期,车次,出发站和终点站呢?首先是日期,同一个车次每天会运行一次,余票需要按天来划分,车次是列车的唯一标识,不同的车次余票应该进行隔离,对于出发站和终点站,举个例子,现在有100张票,有5个区间A->B->C->D->E,现在小刚买了A->C的票,他影响了A->C,A->B,B->C这三个区间,进行库存扣减,但是对于D->E并不影响,还是100张,这中间有座位复用的问题,因此需要加上出发站和终点站座位唯一键。
构建余票表完成后,有两个问题,这张表应该如何初始化?初始化的数据从何而来?首先第一个问题,什么时候初始化?当一辆火车准备开始卖票时,就可以初始化了。对于车站数据来说,是一个嵌套循环,例如ABCD四个车站,用户可以查AB,AC,AD,BC,BD,CD,这样子就可以得到车次所有的出发站和到达站的站站组合。对于余票信息来说,可以查询座位数这张表,查询车次以及座位类型就可以得到余票的信息。
落实到具体的代码上,首先是删除要生成某日车次的余票信息,使其支持重复生成,之后是查询这个车次的所有车站信息,根据车站的信息进行嵌套循环,先生成一个余票对象,然后根据车站进行数据的填充,最后将实体保存到数据库
@Transactional
public void genDaily(Date date, String trainCode) {
LOG.info("生成日期【{}】车次【{}】的余票信息开始", DateUtil.formatDate(date), trainCode);
// 删除某日某车次的余票信息
DailyTrainTicketExample dailyTrainTicketExample = new DailyTrainTicketExample();
dailyTrainTicketExample.createCriteria()
.andDateEqualTo(date)
.andTrainCodeEqualTo(trainCode);
dailyTrainTicketMapper.deleteByExample(dailyTrainTicketExample);
// 查出某车次的所有车站信息
List<TrainStation> stationList = trainStationService.selectByTrainCode(trainCode);
if(CollUtil.isEmpty(stationList)) {
LOG.info("该车次信息没有车站基础数据,生成该车次的余票信息失败");
return;
}
DateTime now = DateTime.now();
for (int i = 0; i < stationList.size(); i++) {
// 得到出发站
TrainStation trainStationStart = stationList.get(i);
for (int j = i + 1; j < stationList.size(); j++) {
TrainStation trainStationEnd = stationList.get(j);
DailyTrainTicket dailyTrainTicket = new DailyTrainTicket();
dailyTrainTicket.setId(SnowUtil.getSnowflakeNextId());
dailyTrainTicket.setDate(date);
dailyTrainTicket.setTrainCode(trainCode);
dailyTrainTicket.setStart(trainStationStart.getName());
dailyTrainTicket.setStartPinyin(trainStationStart.getNamePinyin());
dailyTrainTicket.setStartTime(trainStationStart.getOutTime());
dailyTrainTicket.setStartIndex(trainStationStart.getIndex());
dailyTrainTicket.setEnd(trainStationEnd.getName());
dailyTrainTicket.setEndPinyin(trainStationEnd.getNamePinyin());
dailyTrainTicket.setEndTime(trainStationEnd.getInTime());
dailyTrainTicket.setEndIndex(trainStationEnd.getIndex());
dailyTrainTicket.setYdz(0);
dailyTrainTicket.setYdzPrice(BigDecimal.ZERO);
dailyTrainTicket.setEdz(0);
dailyTrainTicket.setEdzPrice(BigDecimal.ZERO);
dailyTrainTicket.setRw(0);
dailyTrainTicket.setRwPrice(BigDecimal.ZERO);
dailyTrainTicket.setYw(0);
dailyTrainTicket.setYwPrice(BigDecimal.ZERO);
dailyTrainTicket.setCreateTime(now);
dailyTrainTicket.setUpdateTime(now);
dailyTrainTicketMapper.insert(dailyTrainTicket);
}
}
LOG.info("生成日期【{}】车次【{}】的余票信息结束", DateUtil.formatDate(date), trainCode);
}
现在发现,对于座位类型的个数和票价还是未知,因此接下来解决这个方面的信息。求某个车次的某类型的票数,需要知道的是座位数。因此应该在每日座位表中查某个日期,某个车次,某个座位类型的票数。由于在每日座位表已经有了这些信息,因此只需要填写好信息去查询就可以得到初始化的余票信息,对于无票的时候,如果设置为0,用户可以以为卖光了,其实想要表达的是改车次没有这种类型的票,因此可以设置为-1.
public int countSeat(Date date, String trainCode,String seatType){
DailyTrainSeatExample example = new DailyTrainSeatExample();
example.createCriteria()
.andDateEqualTo(date)
.andTrainCodeEqualTo(trainCode)
.andSeatTypeEqualTo(seatType);
long l = dailyTrainSeatMapper.countByExample(example);
if(l == 0L) {
return -1;
}
return (int)l;
}
接下来是计算票价,票价和火车类型以及座位类型有关:票价=里程之和*座位类型的票价*车次类型系数。计算里程时从初始站加上每一次到达站的距离,即
sumKM = sumKM.add(trainStationEnd.getKm());
最终的总计算公式如下:
// 票价=里程之和*座位类型的票价*车次类型系数 String trainType = dailyTrain.getType(); // 计算票价系数:TrainTypeEnum.priceRate BigDecimal priceRate = EnumUtil.getFieldBy(TrainTypeEnum::getPriceRate,TrainTypeEnum::getCode,trainType);
余票查询
对于余票的查询,设置查询条件为日期,火车车次,起始站和终点站。对于会员端的车票界面来说,它不支持增加和修改和删除,只是支持查询,因此后端对于会员端增加车票查询的接口。
@RestController
@RequestMapping("/daily-train-ticket")
public class DailyTrainTicketController {
@Resource
private DailyTrainTicketService dailyTrainTicketService;
@GetMapping("/query-list")
public CommonResp<PageResp<DailyTrainTicketQueryResp>> queryList(@Valid DailyTrainTicketQueryReq req) {
PageResp<DailyTrainTicketQueryResp> list = dailyTrainTicketService.queryList(req);
return new CommonResp<>(list);
}
}
由于进行查询时需要选择车站以及火车车次,而且为了进行分离,之前在控制台界面统一增加了admin,为了使会员端实现相同的查询,因此需要重新写controller层的车次和车票查询,它和控台的功能相同,只是url不同。至此,会员端和控制台界面开发完毕。
选座功能
首先是预定的按钮,在点击按钮之后需要跳转到order界面,因此需要在router增加一个路由,那用什么取传递参数呢?session就是一个很好的选择,在order界面打开时执行setup(),定义一个参数dailyTrainTicket从缓存中获取dailyTrainTicket,如果没有,给一个空对象,避免空指针异常,之后进行返回,在html部分进行显示出来。在点击时,利用自定义的toOrder,首先是把record放入Session中,之后进行路由跳转。现在是每次查询之后返回,选择框不会保存之前选择的值,为了增强用户体验,可以为余票查询页面缓存查询参数,方便用户使用,将session key写成常量,方便统一维护,可以避免多个功能使用同一个key。当用户选择之后,将用户的选择缓存到一个session key中,然后在公共区添加不同的session key,避免混用。之后修改onMounted(),它表示页面打开,先进性缓存的获取,之后不为空是进行查询。
// order.vue
<template>
<div>{{dailyTrainTicket}}</div>
</template>
<script>
import {defineComponent} from 'vue';
export default defineComponent({
name: "order-view",
setup() {
const dailyTrainTicket = SessionStorage.get("dailyTrainTicket") || {};
console.log("下单的车次信息", dailyTrainTicket);
return {
dailyTrainTicket,
};
},
});
</script>
// ticket.vue
// 保存查询参数
SessionStorage.set(SESSION_TICKET_PARAMS, params.value);
onMounted(() => {
params.value = SessionStorage.get(SESSION_TICKET_PARAMS) || {};
if(Tool.isNotEmpty(params.value)) {
handleQuery({
page: 1,
pageSize: pagination.value.pageSize
});
}
});
最后的是在order界面的展示优化,得到从出发站到终点站以及座位类型和价格以及票数的展示。
接下来是真正的选择座位功能,首先是后端去查找我的所有乘客接口,在order界面调用接口,在搜索的service层,需要获取当前登录者的id,然后根据登录者的id去库中搜索出为那些乘客购票,如果乘客太多,可以增加一个功能,当乘客数量大于50时就不拿增加乘客了,controller直接增加接口查询即可。对于前端,增加一个响应式变量passenger,增加一个handleQueryPassen-ger,方法,这个方法调用后端接口,得到后给响应式变量赋值,即初始化时直接查询。
对于选择乘客,在js部分增加了const passengerOptions = ref([]); const passengerChecks = ref([]);表示选项和选择,由于乘客带有的属性过多,因此可以在handleQueryPassenger方法中增加lable和value,分别表示看到的值以及实际操作的值。勾选完乘客后,需要为乘客构造购票数据。由于一次不仅仅勾选一个乘客,因此可以引入watch,实时监控勾选的变化,用来显示购票的界面。
// 购票列表,用于界面展示,并传递到后端接口,用来描述:哪个乘客购买什么座位的票
const tickets = ref([]);
// 勾选或去掉某个乘客时,在购票列表中加上或去掉一张表
watch(() => passengerChecks.value, (newVal, oldVal)=>{
console.log("勾选乘客发生变化", newVal, oldVal)
// 每次有变化时,把购票列表清空,重新构造列表
tickets.value = [];
passengerChecks.value.forEach((item) => tickets.value.push({
passengerId: item.id,
passengerType: item.type,
seatTypeCode: seatTypes[0].code,
passengerName: item.name,
passengerIdCard: item.idCard
}))
}, {immediate: true});
最后选择是勾选乘客后提交,显示购票列表确认框进行最后的核对。此时,购票的选座展示效果完毕。
选座规则
- 只有全部是一等座或全部是二等座才支持选座
- 余票小于20张时,不允许选座
选座效果
显示两排,一等座每排4个,二等座每排5个,为什么是两排,只是一个自定义的规则,可以3排进行显示,由自己规定。每排的座位是由枚举座位类型得到的,对于1,2等座的划分,根据枚举中的type值即可进行得到。之后构造两个响应式变量chooseSeatType和chooseSeatObj,其中chooseSeatType是表示是否支持选座以及选择的类型,chooseSeatObj表示用户选择的座位是那些,默认为false,选择之后为true,通过读这个对象就知道用户选择了什么座位。经过选座,就可以得到tickets,其中有乘客id,乘客类型,座位类型,乘客姓名,身份证,实际座位。当没有选座时,实际座位为空,由系统来分配,从一号开始找,未被购买,就选座。选座,以购买两张一等座AC为例:遍历一等座车厢,每个车厢从1号座位开始找A列座位,未被购买的,就预选中它;再挑它旁边的C,如果也未被购买,则最终选中这两个座位,如果B已被购买,则回到第一步,继续找未被购买的A座。再挑它旁边的C,这个应该怎么写?可以从第二个座位开始,需要计算和第一个座位的偏移值,不需要再从1位置开始找,提高选座效率。
前端的选座效果
首先是要考虑这个车票能不能选,例如现在还有5张票,共有7个人来买票,这肯定是不行的,因此可以在前端增加一层校验,来检验余票是否足够,可以减小后端的压力。这步是预扣减库存,只是用来校验,所有拷贝出临时变量来扣减,即点击提交是预扣减库存,实际提交才是真正扣减库存
// 校验余票是否充足,购票列表中的每个座位类型,都去车次座位余票信息中,看余票是否充足
// 前端校验不一定准,但前端校验可以减轻后端很多压力
// 注意:这段只是校验,必须copy出seatTypesTemp变量来扣减,用原始的seatTypes去扣减,会影响真实的库存
let seatTypesTemp = Tool.copy(seatTypes);
for (let i = 0; i < tickets.value.length; i++) {
let ticket = tickets.value[i];
for (let j = 0; j < seatTypesTemp.length; j++) {
let seatType = seatTypesTemp[j];
// 同类型座位余票-1,这里扣减的是临时copy出来的库存,不是真正的库存,只是为了校验
if (ticket.seatTypeCode === seatType.code) {
seatType.count--;
if (seatType.count < 0) {
notification.error({description: seatType.desc + '余票不足'});
return;
}
}
}
}
console.log("前端余票校验通过");
开始选座
响应式变量chooseSeatType首先是0,表示不支持选座,然后根据座位类型选择对应的列,赋值给SEAT_COL_ARRAY,之后对两排的座位进行初始化,赋值为false;由于规定不能同时选择1和2等座,所有开始选座之前先进行去重,如果多于1中返回选座不成功,否则的话根据类型进行选座。最后进行界面优化,增加选座的按钮,这里注意,如果是选择一个人进行购票,这里采用只显示一排按钮。回到选择的函数中来,增加一个约定,余票小于20张时,不允许选座。最后提交时,计算出每个用户的座位选择,代码如下:
const handleOk = () => {
console.log("选好的座位:", chooseSeatObj.value);
// 设置每张票的座位
// 先清空购票列表的座位,有可能之前选了并设置座位了,但选座数不对被拦截了,又重新选一遍
for (let i = 0; i < tickets.value.length; i++) {
tickets.value[i].seat = null;
}
let i = -1;
// 要么不选座位,要么所选座位应该等于购票数,即i === (tickets.value.length - 1)
for (let key in chooseSeatObj.value) {
if (chooseSeatObj.value[key]) {
i++;
if (i > tickets.value.length - 1) {
notification.error({description: '所选座位数大于购票数'});
return;
}
// 实际的赋值
tickets.value[i].seat = key;
}
}
if (i > -1 && i < (tickets.value.length - 1)) {
notification.error({description: '所选座位数小于购票数'});
return;
}
console.log("最终购票:", tickets.value);
}
后端选座接口
前面的余票信息表是根据购票表来进行得到的,但是,购买完成票之后还需要进行落表,无论是否成功。需要哪一个人购的票,表示当前访问这个接口的是哪一个会员,还需要日期,车次,出发站和到达站以及这些基础信息,这些信息就可以唯一定位到余票信息这张表,就可以判断它的一等座,二等座等的余票信息了。余票id字段也可以和上面的余票表进行关联。由于订单状态不一定成功,因此需要订单的状态,车票可以做成json,也可以做成子表。
开发接口的话,那么传入的参数是什么,可以参考设计的表,接口进来之后,就应该数据落表,那么表中的数据如何来?其中member_id可以从线程本地变量获取,日期,车次,出发站,到达站和车票都可以从前端获取,其中车票可以把json映射成Java类,这样子操作更加的方便。因为需要车票进行选座,用json操作不太方便。订单状态是由程序根据不同的步骤进行落库,因此不用管。首先要做的是添加车票类(ConfrimOrderTicketReq),即购买车票的信息,接受前端传来的对象。之后要构造一个订单类,方便入库。之后在controller层增加doConfirm接口,最后在service层增加相关保存购票信息的方法。最后前端调用后端接口。
重点的话是会员模块的service层的保存订单方法doConfirm如何实现。
- 保存确认订单表,状态初始,对于id,直接使用雪花算法,时间使用当前的时间,memberid使用登录人的id,像traincode,date,start和end一次ticket都是从前端获取的,之前保存到了ConfirmOrderReq,从这里直接获取即可,注意,对于ticket来说,需要将json字符串转化为车票类
- 查出余票记录,得到真实的库存。由于唯一键是日期,车次,起始和终点站,这样子构造号条件,进行查询。
public DailyTrainTicket selectByUnique(Date date, String trainCode,String start,String end) { DailyTrainTicketExample dailyTrainTicketExample = new DailyTrainTicketExample(); dailyTrainTicketExample.createCriteria() .andDateEqualTo(date) .andTrainCodeEqualTo(trainCode) .andStartEqualTo(start) .andEndEqualTo(end); List<DailyTrainTicket> list = dailyTrainTicketMapper.selectByExample(dailyTrainTicketExample); if(CollUtil.isNotEmpty(list)) { return list.get(0); }else { return null; } }
- 进行票数的预扣减,由于前端的是实时显示到界面上,因此需要一个变量,而这里只要不更新到数据库,怎么扣减都可以,因此可以之间操作查出的库存记录。一张票一张票的循环进行扣减,由于选择的可能是不同的座位类型(不同的人),因此不能按照列表进行扣票,应该按照不同的座位类型进行扣票。
private static void reduceTickets(ConfirmOrderDoReq req, DailyTrainTicket dailyTrainTicket) { for (ConfirmOrderTicketReq ticketReq : req.getTickets()) { String seatTypeCode = ticketReq.getSeatTypeCode(); SeatTypeEnum seatTypeEnum = EnumUtil.getBy(SeatTypeEnum::getCode, seatTypeCode); switch (seatTypeEnum) { case YDZ -> { int countLeft = dailyTrainTicket.getYdz() - 1; if(countLeft < 0) { throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_TICKET_COUNT_ERROR); } dailyTrainTicket.setYdz(countLeft); } case EDZ -> { int countLeft = dailyTrainTicket.getEdz() - 1; if(countLeft < 0) { throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_TICKET_COUNT_ERROR); } dailyTrainTicket.setEdz(countLeft); } case RW -> { int countLeft = dailyTrainTicket.getRw() - 1; if(countLeft < 0) { throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_TICKET_COUNT_ERROR); } dailyTrainTicket.setRw(countLeft); } case YW -> { int countLeft = dailyTrainTicket.getYw() - 1; if(countLeft < 0) { throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_TICKET_COUNT_ERROR); } dailyTrainTicket.setYw(countLeft); } } } }
- 偏移值的计算,这里的偏移值指的是相对于第一个选座位置的偏移值,这样子可以一次循环找到所有位置的值(假设一个车厢50个座位,A,B,C,D,E各10个,现在A全部被买了,如果现在选座A,C那么不会选座成功)。对于第一个购票来说,要么是成功,要么不成功,因此就可以根据第一个张票成功与否的情况判断后面的票是否能选,即本次购票是否有选座。对于有选座的功能,还需要偏移值进行计算,得到的结果有空位算是选座成功。由于一等座和二等座每排位置不同,还需要知道座位的类型,之后组成和前端两排选座一样的列表,用于作参照的座位列表,需要两次循环,因为有两排,这一步就是座位的初始化。之后计算绝对的偏移值,再根据第一个位置计算相对的偏移值
-
挑车厢:对于购票没有选座的来说,比较简单,只要这个座位是可选的即可,需要一张票一张票的去挑选。在买票之前,首先要知道购买票的类型在哪一个车厢中,因此需要是要写一个寻找车厢的方法,根据日期,车次,和车票类型,然后把上面的当作传入的参数就可以得到符合条件的车厢了,之后在得到的车厢列表中一个个的寻找。
-
根据车厢挑座位:根据获取车厢的方法获得符合条件的车厢之后,现在得到的车厢都是一种票型了,先获取起始车厢,以及进入车厢前的座位列表,座位列表可以根据日期,车次以及车厢位置来获取,因为一个车厢的座位类型是相同的,然后就一个车厢一个车厢的寻找。下面的选座就是调用了本段写的getSeat方法,在有选座的情况下,需要知道第一个选座的实际列,以及得到的偏移值,根据这两个参数进行选座。在没有选座的情况,并没有特定的列号,也没有偏移值,那就传null,只是一个座位一个座位的选座。对于座位还应该进行排序,根据座位索引进行排序。现如今插入车厢的座位后,第一次需要一个座位一个座位的挑选,写个循环,看每个座位是否可卖,可卖的话之间返回,否则跳过。
-
判断是否可卖:1表示在这个区间买过票了,就不能够售票了。0表示在这个给区间没有卖票。例如:sell=10001,本次购买的区间是1~4,则区间应该售000,这里是10001中间的三个0,000要变成111,那么之后可以根据或运算变成11111,即10001|01110==11111,如果sell=00001,那么按位或得到01111,之后转为数字是15,但再转成二进制是1111,不是01111,因此需要补0。
private boolean calSell(DailyTrainSeat dailyTrainSeat,Integer startIndex, Integer endIndex) { String sell = dailyTrainSeat.getSell(); String sellPart = sell.substring(startIndex, endIndex); if(Integer.parseInt(sellPart) > 0) { LOG.info("座位{}在本车站区间{}--{}已售过票,不可选中该座位",dailyTrainSeat.getCarriageSeatIndex(), startIndex,endIndex); return false; }else { LOG.info("座位{}在本车站区间{}--{}未售过票,可选中该座位",dailyTrainSeat.getCarriageSeatIndex(), startIndex,endIndex); // 111 String curSell = sellPart.replace('0', '1'); // 0111 curSell = StrUtil.fillBefore(curSell,'0', endIndex); curSell = StrUtil.fillAfter(curSell,'0',sell.length()); // 当前区间售票信息curSell与库里的已售信息sell按位与,即可得到该座位卖出此票后的售票详情 // 32 int newSellInt = NumberUtil.binaryToInt(curSell) | NumberUtil.binaryToInt(sell); // 11111 String newSell = NumberUtil.getBinaryStr(newSellInt); newSell = StrUtil.fillBefore(newSell,'0',sell.length()); LOG.info("座位{}在本车站区间{}--{}卖出该票后,最终售票详情:{}",dailyTrainSeat.getCarriageSeatIndex(), startIndex,endIndex,newSell); dailyTrainSeat.setSell(newSell); return true; }
-
优化getSeat方法:首先判断传入的column和拿到的col,进行比较,如果无值,表示无选座,有值需要判断是否可以匹配,不匹配的话跳过。现在选完第一个座位后,接下来根据偏移值选则后面几人的座位,偏移值可能有多个,从索引1开始(0是以及选完的第一个座位)进行循环,下一个位置是索引+偏移量,然后根据是否超卖判断是否可选。还要注意,选座是在同一个车厢,根据下一个索引是否大于车厢座位数
-
保存最终的选座结果,但不是更新到数据库中。此时用一个临时变量保存选择的座位,当下一个选座时,应该看这个选座的结果以及最终的选座结果(这个最终结果是之前的结果,如果此次选座成功,那么就更新最终选座),否则就出现挑选同一个座位的情况。例如系统分配座位的时候,先分配座位3,由于同一个人为不同人买多张票,在进行第二张票购买前,没有更新到数据库中,导致下一个分配的还是3号座位,这显然是不合理的,因此需要对每一次选择的座位进行保存,为了让下一次正确的挑选座位。对于选座的情况,由于第一次选座成功的情况下,可能之后根据偏移值选择的其他座位不成功,如果直接保存的最终结果,可能导致结果和符合的不一致,因此也是需要保存到临时变量中,注意没有成功选座时清空临时变量,当两种情况都成功选座后,才真正的保存到最终选座的变量中。
-
根据售卖信息更新座位售卖情况,就是更新数据库的售卖信息,例如从000更新到101.就是选好座位后,将座位信息更新到日常座位表中。由于后面还需要涉及到修改余票,为会员增加购票记录,更新订单状态,因此可以把他们称为选座后的事务处理。可以在最后修改数据库的时候增加事务,这样子占用事务的时间较少,否则的话占用大量的数据库资源。由于本垒方法间的调用,事务不生效,因此增加一个类,专门进行处理。根据最终选票结果来处理。此时需要更新的是id以及售卖票的信息还要更新时间,不必要更新表中所有的字段。
@Transactional public void afterDoConfirm(List<DailyTrainSeat> finalSeatList) { for (DailyTrainSeat dailyTrainSeat: finalSeatList){ DailyTrainSeat seatForUpdate = new DailyTrainSeat(); seatForUpdate.setId(dailyTrainSeat.getId()); seatForUpdate.setSell(dailyTrainSeat.getSell()); seatForUpdate.setUpdateTime(new Date()); dailyTrainSeatMapper.updateByPrimaryKeySelective(seatForUpdate); } }
-
扣减库存(很重要):售卖一张票影响的是多个区间的库存,就是本次选座之前没卖过票的,并且本次购买的区间有交集的区间,怎么理解,如下图:
由于座位区间2没有卖过票,而且AD和AE与购买的CD区间有交集,因此减库存。
由于 座位区间1已经卖过票了,因此不需要减库存。
因此先计算区间,然后根据区间来进行扣减库存。需要计算出最大最小开始结束的影响区间(4个参数)。
这里买了4(startIndex)到7(endIndex)站的票,购买区间下标7不更新是因为在第七站下车,不会影响第七站的购票,首先看最小开始影响的下标,这里是3,因此minStartIndex = startIndex - 往前碰0到的最后一个0,由于这里购买7往后不会前面的影响库存,因此maxStratIndex = endIndex - 1,表示在[minStartIndex,maxStartIndex]这个区间开始的都会影响其他库存。如果是在3下标结束不会影响其他库存,但是4下标开始有影响,即minEndIndex= startIndex+1,同理maxEndIndex=endIndex+ 往后碰0到的最后一个0,表示在[minEndIndex,maxEndIndex]这个区间结束都会影响库存。
Integer startIndex = dailyTrainTicket.getStartIndex();
Integer endIndex = dailyTrainTicket.getEndIndex();
char[] chars = seatForUpdate.getSell().toCharArray();
Integer maxStartIndex = endIndex - 1;
Integer minEndIndex = startIndex + 1;
Integer minStartIndex = 0;
for (int i = startIndex - 1; i >= 0; i--) {
char aChar = chars[i];
if(aChar == '1') {
minStartIndex = i + 1;
break;
}
}
LOG.info("影响出发站区间:"+minStartIndex+"-"+maxStartIndex);
Integer maxEndIndex = seatForUpdate.getSell().length();
for (int i = endIndex; i < seatForUpdate.getSell().length(); i++) {
char aChar = chars[i];
if(aChar == '1') {
maxEndIndex = i;
break;
}
}
LOG.info("影响结束站区间:"+minEndIndex+"-"+maxEndIndex);
dailyTrainTicketMapperCust.updateCountBySell(
dailyTrainSeat.getDate(),
dailyTrainSeat.getTrainCode(),
dailyTrainSeat.getSeatType(),
minStartIndex,
maxStartIndex,
minEndIndex,
maxEndIndex);
之后根据这四个值进行库存的更新。
- 对乘客增加车票表,由于之前的购票表订单状态由可能为失败,而且信息不容易搜索,因此新建一张表,用来保存乘客购买车票成功的信息,由于每个会员经常查自己购买的车票,因此可以把会员id当作索引。由于生成的表在member模块,购票业务在business模块,当business模块购票成功后调用member模块的接口,把数据传入,这里使用的了feign。对于会员车票参数来说,在business模块需要调用它进行车票的构造,number模块需要调用它进行入库,因此可以放到common模块。
- 要启用feign,需要在调用方buiness增加依赖,之后在启动类配置路径,表示哪个包是属于feign的,之后在相关的路径增加一个接口,路径配置是调用的哪个包下的接口。
之后在business模块就可以调用member的接口,为会员(乘客)增加一张票。之后可以为会员段增加我的车票,方便查看车票。但这里需要根据会员的id查看,只能够查看自己买的车票。
最后更新订单状态为成功,根据id更新 更新时间以及状态