仿12306项目(4)
基本预定车票功能的开发
对于乘客购票来说,需要有每一个车次的余票信息,展示给乘客,供乘客选择,因此首个功能是余票的初始化,之后是余票查询,这两个都是控台端。对于会员端的购票,需要有余票查询以及乘客的选择,不仅仅支持给自己买票,还可以给其他人买票,而且还可以选择座位类型,是一等座还是二等座,可以选择座位,最后是下单购票。
余票信息表
对于购票表来说,最为重要的字段是售卖字段,对于这个字段来说,将经过的车站用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,这样子就可以得到车次所有的出发站和到达站的站站组合。对于余票信息来说,可以查询座位数这张表,查询车次以及座位类型就可以得到余票的信息。
落实到具体的代码上,首先是删除要生成某日车次的余票信息,使其支持重复生成,之后是查询这个车次的所有车站信息,根据车站的信息进行嵌套循环,先生成一个余票对象,然后根据车站进行数据的填充,最后将实体保存到数据库
@Transactionalpublic 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 {@Resourceprivate 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);// 111String curSell = sellPart.replace('0', '1');// 0111curSell = StrUtil.fillBefore(curSell,'0', endIndex);curSell = StrUtil.fillAfter(curSell,'0',sell.length());// 当前区间售票信息curSell与库里的已售信息sell按位与,即可得到该座位卖出此票后的售票详情// 32int newSellInt = NumberUtil.binaryToInt(curSell) | NumberUtil.binaryToInt(sell);// 11111String 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以及售卖票的信息还要更新时间,不必要更新表中所有的字段。
@Transactionalpublic 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更新 更新时间以及状态
相关文章:

仿12306项目(4)
基本预定车票功能的开发 对于乘客购票来说,需要有每一个车次的余票信息,展示给乘客,供乘客选择,因此首个功能是余票的初始化,之后是余票查询,这两个都是控台端。对于会员端的购票,需要有余票查询…...

【GPT入门】第9课 思维树概念与原理
【GPT入门】第9课 思维树概念与原理 1.思维树概念与原理2. 算24游戏的方法 1.思维树概念与原理 思维树(Tree of Thought,ToT )是一种大模型推理框架,旨在解决更加复杂的多步骤推理任务,让大模型能够探索多种可能的解决…...

uniapp登录用户名在其他页面都能响应
使用全局变量 1、在APP.vue中定义一个全局变量,然后在需要的地方引用它; <script>export default {onLaunch: function() {console.log(App Launch)this.globalData { userInfo: {} };},onShow: function() {console.log(App Show)},onHide: fu…...

一周热点-OpenAI 推出了 GPT-4.5,这可能是其最后一个非推理模型
在人工智能领域,大型语言模型一直是研究的热点。OpenAI 的 GPT 系列模型在自然语言处理方面取得了显著成就。GPT-4.5 是 OpenAI 在这一领域的又一力作,它在多个方面进行了升级和优化。 1 新模型的出现 GPT-4.5 目前作为研究预览版发布。与 OpenAI 最近的 o1 和 o3 模型不同,…...

Linux基础--用户管理
目录 查看用户 使用命令: id 创建用户 使用命令: useradd 编辑 为用户设置密码 使用命令: passwd 编辑 删除用户 使用命令: userdel 创建用户组 使用命令: groupadd 删除用户组 使用命令: groupdel 用户设置 使用命令: usermod 将用户从组中去除 使用…...

软件测试的基础入门(二)
文章目录 一、软件(开发)的生命周期什么是生命周期软件(开发)的生命周期需求分析计划设计编码测试运行维护 二、常见的开发模型瀑布模型流程优点缺点适应的场景 螺旋模型流程优点缺点适应的场景 增量模型和迭代模型流程适应的场景…...

【SpringMVC】深入解析@ RequestMapping 注解的概念及使用和 MVC 介绍
Spring Web MVC入门 1. Spring Web MVC 什么是 Spring Web MVC? MVC官方文档介绍 Spring Web MVC是Spring框架中的一个用来做网站开发的部分,它是基于Servlet技术的。 虽然它的正式名字叫“Spring Web MVC”,但大家一般都简称它“SpringMVC”…...

YOLOv8 自定义目标检测
一、引言 YOLOv8 不仅支持预训练模型的推理,还允许用户将其应用于自定义对象检测。本文将详细介绍如何使用 YOLOv8 训练一个新的模型,并在自定义数据集上进行对象检测。 二、数据集准备 1. 数据集格式 YOLOv8 支持多种数据集格式,包括 CO…...

抓包分析工具介绍
什么是抓包分析工具? 抓包分析工具,也称为网络数据包嗅探器或协议分析器,用于捕获和检查网络上传输的数据包。这些数据包包含了网络通信的详细信息,例如请求的资源、服务器的响应、HTTP 头信息、传输的数据内容等等。通过分析这些…...

计算机毕业设计SpringBoot+Vue.js爱心捐赠项目推荐系统 慈善大数据(源码+文档+PPT+讲解)
温馨提示:文末有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:文末有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:文末有 CSDN 平台官方提供的学长联系方式的名片! 作者简介:Java领…...

Python----数据分析(Matplotlib四:Figure的用法,创建Figure对象,常用的Figure对象的方法)
一、Figure的用法 在 Matplotlib 中, Figure对象是整个绘图的顶级容器,它是所有绘图元素的基础, 提供了一个用于绘制图形的画布空间。 在 Matplotlib 中, Axes对象是进行数据绘制和设置坐标轴等操作的核心区域,它与 Fi…...

CameraX学习2-关于录像、慢动作录像
CameraX实现录像 首先在起预览时就要配置录像usecase,对于cameraX来说就是绑定录像usecase到lifecycle。如下。 1,创建Recorder对象,是与 VideoCapture 紧密耦合的 VideoOutput 实现。Recorder可以用来配置录像的分辨率、比例等,还…...

Java链接redis
一、准备工作就像谈恋爱 首先咱们得来点仪式感是不是?打开你的Maven(Gradle玩家别打我),把这两个宝贝依赖给我焊死在pom.xml里: <!-- 经典永不过时的Jedis --> <dependency> <groupId>redis.cli…...

2025最新群智能优化算法:基于RRT的优化器(RRT-based Optimizer,RRTO)求解23个经典函数测试集,MATLAB
一、基于RRT的优化器 基于RRT的优化器(RRT-based Optimizer,RRTO)是2025年提出的一种新型元启发式算法。其受常用于机器人路径规划的快速探索随机树(RRT)算法的搜索机制启发,首次将RRT算法的概念与元启发式…...

VBA 数据库同一表的当前行与其他行的主键重复判断实现方案
目的,判断是否主键重复,不重复则登录新数据,重复则不登录。 定义类型: DataRecord tableName 表名 rowNumber 行号 columnName 列名 data 数据 想要实现的代码逻辑如下: 模拟数据库的登录过程。假设…...

DeepSeek开启AI办公新模式,WPS/Office集成DeepSeek-R1本地大模型!
从央视到地方媒体,已有多家媒体机构推出AI主播,最近杭州文化广播电视集团的《杭州新闻联播》节目,使用AI主持人进行新闻播报,且做到了0失误率,可见AI正在逐渐取代部分行业和一些重复性的工作,这一现象引发很…...

android为第三方提供部分系统接口
文章目录 Settings - 亮灭屏Settings - 恢复出厂设置Settings - 数字锁屏/解锁Settings - 设置系统时间PackageInstaller - 安装/卸载第三方应用摘要:本文对系统模块进行改造,提供广播等形式的接口对外提供无法直接调用的系统级别接口,实现部分功能的集合。如果是广播形式,…...

Android 自定义View 加 lifecycle 简单使用
前言 本文是自定义view中最简单的使用方法,分别进行 ‘onMeasure’、‘onDraw’、‘自定义样式’、‘lifecycle’的简单使用,了解自定义view的使用。 通过lifecycle来控制 动画的状态 一、onMeasure做了什么? 在onMeasure中获取view 的宽和…...

在K8S中,svc底层是如何实现的?
在Kubernetes中,Service是集群内部的一个抽象层,用于定义一组Pod的逻辑分组,并提供统一的访问入口点,同时还可以对这些Pod提供负载均衡和网络代理功能。Service底层的实现主要包括以下几个关键组件和技术: 标签选择器…...

Python pyqt小技巧:默认打开某文件(即自动加载某文件)
文章目录 前言 前言 有的时候需要界面自动加载某文件。不需要人为在打开选择。 import os #自带 import sys # 获取该程序当前文件目录dir_name os.path.dirname(os.path.realpath(sys.argv[0])) f1 os.path.join(dir_name, 题目调度规程.xls) # 拼接路径 文件必须和程序在…...

vue2实现组件库的自动按需引入,unplugin-auto-import,unplugin-vue-components
1.使用ant-design-vue或者element-ui时,如何每个组件都去import导入组件,大大降低了开发效率,如果全局一次性注册会增加项目体积,那么如何实现既不局部引入,也不全局注册? 2.在element-plus官网看到有说明…...

C++第十节:map和set的介绍与使用
【本节要点】 1.关联式容器2.键值对3.map介绍与使用4.set介绍与使用5.multimap与multisedd的介绍与使用 一、关联式容器:数据管理的核心利器 关联式容器是STL中用于高效存储和检索键值对(key-value pair)的数据结构,其底层基于红黑…...

线性代数笔记28--奇异值分解(SVD)
1. 奇异值分解 假设矩阵 A A A有 m m m行 n n n列 奇异值分解就是在 A A A的行向量上选取若干对标准正交基,对它作 A A A矩阵变化并投射到了 A A A的列空间上的正交基的若干倍数。 A v → u → σ u → ∈ R m v → ∈ R n A\overrightarrow{v}\overrightarrow{u…...

【从零开始学习计算机科学】硬件设计与FPGA原理
硬件设计 硬件设计流程 在设计硬件电路之前,首先要把大的框架和架构要搞清楚,这要求我们搞清楚要实现什么功能,然后找找有否能实现同样或相似功能的参考电路板(要懂得尽量利用他人的成果,越是有经验的工程师越会懂得借鉴他人的成果)。如果你找到了的参考设计,最好还是…...

项目中同时使用Redis(lettuce)和Redisson的报错
温馨提示:图片有点小,可以放大页面进行查看... 问题1:版本冲突 直接上图,这个错表示依赖版本不匹配问题,我本地SpringBoot用的是2.7,但是Redisson版本用的3.32.5。 我们通过点击 artifactId跟进去 发现它…...

leetcode-数组
26. 删除有序数组中的重复项 已解答 简单 相关标签 相关企业 提示 给你一个 非严格递增排列 的数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。然后返回 n…...

人工智能里的深度学习指的是什么?
深度学习(Deep Learning, 简称DL)是机器学习领域的一个重要分支,它通过构建和训练深层神经网络模型,从大量数据中自动学习和提取特征,以实现复杂任务的自动化处理和决策。以下是关于深度学习的详细介绍: 一…...

docker本地部署ollama
启动ollama容器 1.使用该命令启动CPU版运行本地AI模型 docker run -d -v ollama:/root/.ollama -p 11434:11434 --name ollama ollama/ollama 2.此命令用于启动GPU版本运行AI模型 前提是笔记本已配置NVIDIA的GPU驱动,可在shell中输入nvidia-smi查看详细情况…...

LangChain构建语言模型驱动应用的强大框架
LangChain 核心功能与组件链(Chains)记忆(Memory)提示模板(Prompts)代理(Agents)数据检索(Indexes) 应用场景文档问答自动化工作流知识管理系统 发展历程总结…...

2025-03-08 学习记录--C/C++-PTA 习题10-2 递归求阶乘和
合抱之木,生于毫末;九层之台,起于累土;千里之行,始于足下。💪🏻 一、题目描述 ⭐️ 二、代码(C语言)⭐️ #include <stdio.h>double fact( int n ); double facts…...