仿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)
基本预定车票功能的开发 对于乘客购票来说,需要有每一个车次的余票信息,展示给乘客,供乘客选择,因此首个功能是余票的初始化,之后是余票查询,这两个都是控台端。对于会员端的购票,需要有余票查询…...
调研:如何实现智能分析助手(Agent)(AutoCoder、FastGPT、AutoGen、DataCopilot)
文章目录 调研:如何实现智能分析助手(Agent)(AutoCoder、FastGPT、AutoGen、DataCopilot)一、交互流程二、数据流程三、架构分类四、开源产品4.1 AutoCoder(知识库变体)4.2 FastGPT(…...
爬虫逆向:脱壳工具Youpk的使用详解
更多内容请见: 爬虫和逆向教程-专栏介绍和目录 文章目录 1. Youpk 简介1.1 Youpk介绍1.2 Youpk支持场景1.3 Youpk基本流程1.4 使用 Youpk 脱壳步骤1.5 常用的脱壳工具对比2. Youpk 的安装与使用2.1 安装 Youpk2.2 使用 Youpk 脱壳3. 脱壳后的 Dex 文件分析3.1 使用 JADX 反编译…...
Java 大视界 -- Java 大数据在智能政务公共服务资源优化配置中的应用(118)
💖亲爱的朋友们,热烈欢迎来到 青云交的博客!能与诸位在此相逢,我倍感荣幸。在这飞速更迭的时代,我们都渴望一方心灵净土,而 我的博客 正是这样温暖的所在。这里为你呈上趣味与实用兼具的知识,也…...
Java停车平台高并发抢锁技术方案设计 - 慧停宝开源停车管理平台
Java停车平台高并发抢锁技术方案设计 一、业务场景特征 瞬时流量峰值 早晚高峰时段(07:30-09:00, 17:30-19:00)请求量激增10倍热门商圈停车场每秒并发请求可达5000 QPS 资源竞争特性 单个车位被多人同时抢占(超卖风险)用户操作链…...
【论文笔记】Attentive Eraser
标题:Attentive Eraser: Unleashing Diffusion Model’s Object Removal Potential via Self-Attention Redirection Guidance Source:https://arxiv.org/pdf/2412.12974 收录:AAAI 25 作者单位:浙工商,字节&#…...
Android Flow操作符分类
Flow操作符分类...
Cursor + IDEA 双开极速交互
相信很多开发者朋友应该和我一样吧,都是Cursor和IDEA双开的开发模式:在Cursor中快速编写和生成代码,然后在IDEA中进行调试和优化 在这个双开模式的开发过程中,我就遇到一个说大不大说小不小的问题: 得在两个编辑器之间来回切换查…...
图像识别-手写数字识别项目
训练模型: 实现神经网络实例 准备数据 导入torchvision.transforms模块,它提供了许多常用的数据预处理操作,如裁剪、旋转、归一化等。 从torch.utils.data模块导入DataLoader类,用于加载数据集并提供批量处理功能。 导入tensorboa…...
推荐几款优秀的PDF转电子画册的软件
当然可以!以下是几款优秀的PDF转电子画册的软件推荐,内容简洁易懂,这些软件都具有易用性和互动性,适合不同需求的用户使用。 ❶ FLBOOK|在线创作平台 支持PDF直接导入生成仿真翻页电子书。提供15主题模板与字体库&a…...
bert模型笔记
1.各预训练模型说明 BERT模型在英文数据集上提供了两种大小的模型,Base和Large。Uncased是意味着输入的词都会转变成小写,cased是意味着输入的词会保存其大写(在命名实体识别等项目上需要)。Multilingual是支持多语言的࿰…...
利用 ArcGIS Pro 快速统计省域各市道路长度的实操指南
在地理信息分析与处理的工作中,ArcGIS Pro 是一款功能强大的 GIS 软件,它能够帮助我们高效地完成各种复杂的空间数据分析任务。 现在,就让我们一起深入学习如何借助 ArcGIS Pro 来统计省下面各市的道路长度,这一技能在城市规划、…...
数据库系统概论(一)详细介绍数据库与基本概念
数据库系统概论(一)介绍数据库与基本概念 前言一、什么数据库1.数据库的基本概念2.数据库的特点 二、数据库的基本概念1. 数据2. 数据库3.数据库管理系统4.数据库系统 三、数据管理技术的产生和发展四、数据库系统的特点1.数据结构化2.数据共享性3.数据冗…...
数字IC后端实现教程| Clock Gating相关clock tree案例解析
今天小编给大家分享几个跟时钟树综合,clock tree相关的典型问题。 数字IC后端设计实现之分段长clock tree经典案例 Q1:星主好,下面的图是通过duplicate icg来解setup违例的示意图。我没看懂这个 duplicate操作在cts阶段是怎么实现的,用什么…...
build gcc
1,下载源码 wget https://gcc.gnu.org/pub/gcc/infrastructure/mpfr-4.1.0.tar.bz2 wget https://gcc.gnu.org/pub/gcc/infrastructure/gmp-6.1.0.tar.bz2 wget https://gcc.gnu.org/pub/gcc/infrastructure/mpc-1.2.1.tar.gz git clone --mirror https://github…...
软考架构师笔记-计算机网络
1.9 计算机网络 OSI/RM 七层模型 物理层 二进制传输(中继器、集线器) (typedef) 数据链路层 传送以帧为单位的信息(网桥、交换机、网卡) 网络层 分组传输和路由选择(三层交换机、路由器)ARP/RARP/IGMP/ICMP/IP 传输层 端到端的连接(TCP/UDP)在前向纠错系统中,当接…...
ubuntu打包 qt 程序,不用每次都用linuxdeployqt打包
用linuxdeployqt打包太麻烦,每次程序编译都要用linuxdeployqt打包一次,而且每次都要很长时间,通过研究得出一个新的打包方法 1.用用linuxdeployqt得出依赖的库文件(只要没有增加新模块,只要用一次就可以) …...
Spark(6)vm与centos虚拟机
(一)克隆虚拟机 vm软件提供了克隆的功能,它可以允许我们从一台虚拟机上快速克隆出其他的一模一样的主机。 具体的操作步骤如下: 关闭hadoop100这台虚拟机。在它身上右键,并选择管理 → 克隆 命令 在随后的设置中&#…...
人工智能开发面经AI、大数据、算法
以下是一份AI算法开发岗位的面试面经,结合最新行业趋势和经典问题,涵盖技术解析与实战案例,供参考: 一、机器学习基础(占比约30%) 1. 过拟合与欠拟合的解决方案 问题:如何解决模型过拟合&…...
在 macOS 上使用 CLion 进行 Google Test 单元测试
介绍 Google Test(GTest)是 Google 开源的 C 单元测试框架,它提供了简单易用的断言、测试夹具(Fixtures)和测试运行机制,使 C 开发者能够编写高效的单元测试。 本博客将介绍如何在 macOS 上使用 CLion 配…...
NO2.C++语言基础|C++和Java|常量|重载重写重定义|构造函数|强制转换|指针和引用|野指针和悬空指针|const修饰指针|函数指针(C++)
6. C 和 Java 区别(语⾔特性,垃圾回收,应⽤场景等) 指针: Java 语⾔让程序员没法找到指针来直接访问内存,没有指针的概念,并有内存的⾃动管理功能,从⽽有效的防⽌了 C 语⾔中的指针…...
中原银行:从“小机+传统数据库”升级为“OceanBase+通用服务器”,30 +系统成功上线|OceanBase DB大咖说(十五)
OceanBase《DB 大咖说》第 15 期,我们邀请到了中原银行金融科技部数据团队负责人,吕春雷。本文为本期大咖说的精选。 吕春雷是一位资历深厚的数据库专家,从传统制造企业、IT企业、甲骨文公司到中原银行,他在数据库技术与运维管理…...
自注意力机制的演进-从Transformer架构到DeepSeek-R1模型的深度语义理解革新
2025年,我国发布了开创性且高性价比的大语言模型-DeepSeek-R1,推动了AI领域的重大变革。本章节回顾了LLM的发展历程,其起点可追溯至2017年Transformer架构的提出,该架构通过自注意力机制(Self-Attention)彻底革新了自然语言处理技…...
数据结构拓展:详解realloc(C++)
前言 在C中,realloc 是C标准库提供的一个内存管理函数,用于动态调整已分配内存块的大小。尽管C更推荐使用 new/delete 或智能指针,但在某些场景(如与C代码交互或底层内存操作)中仍可能用到 realloc。以下是详细分析&a…...
计算机网络数据传输探秘:包裹如何在数字世界旅行?
计算机网络数据传输探秘:包裹如何在数字世界旅行? 一、从快递网络看数据传输本质 想象你网购了一件商品: 打包:商家用纸箱包装,贴上地址标签(数据封装)运输:包裹经过网点→分拣中心→运输车(网络节点与链路)签收:快递员核对信息后交付(数据校验与接收)数据的网络…...
十二、OSG学习笔记-Control
上一章节: 十一、OSG学习笔记-操作系统接口-CSDN博客https://blog.csdn.net/weixin_36323170/article/details/145891502 本章节代码: OsgStudy/Controls CuiQingCheng/OsgStudy - 码云 - 开源中国https://gitee.com/cuiqingcheng/osg-study/tree/ma…...
DevOps全流程
DevOps全流程 面试速记 DevOps 是一种融合开与运维的软件研发实践方法,规划从需求调研到开发,测试,部署等全流程,通过Jenkins 和Kubernetes 等自动化工具集成部署实现从代码编写到部署的无缝衔接,加速软件交付周期。我…...
ARM Linux LCD上实时预览摄像头画面
文章目录 1、前言2、环境介绍3、步骤4、应用程序编写4.1、lcd初始化4.2、摄像头初始化4.3、jpeg解码4.4、开启摄像头4.5、完整的程序如下 5、测试5.1、编译应用程序5.2、运行应用程序 6、总结 1、前言 本次应用程序主要针对支持MJPEG格式输出的UVC摄像头。 2、环境介绍 rk35…...
NL2SQL-基于Dify+阿里通义千问大模型,实现自然语音自动生产SQL语句
本文基于Dify阿里通义千问大模型,实现自然语音自动生产SQL语句功能,话不多说直接上效果图 我们可以试着问他几个问题 查询每个部门的员工数量SELECT d.dept_name, COUNT(e.emp_no) AS employee_count FROM employees e JOIN dept_emp de ON e.emp_no d…...
centos8更换阿里云yum源
1.centos8更换为阿里云yum源 2.更换阿里云Yum-centos8源 mv /etc/yum.repos.d/CentOS-Stream-BaseOS.repo /etc/yum.repos.d/CentOS-Stream-BaseOS.repo.backupcurl -o /etc/yum.repos.d/CentOS-Stream-BaseOS.repo https://mirrors.aliyun.com/repo/Centos-8.repowget -O /et…...
在命令的轨迹中:创建、删除与查看普通用户,Linux用户管理的风华与深邃
文章目录 引言一、创建普通用户二、测试是否创建成功三、查看普通用户四、更改密码五、赋予普通用户root权限六、删除普通用户小结 引言 在浩瀚如海的Linux系统中,每一位用户都是一颗独立的星辰,散发着属于自己的光芒。在这片数字的星空中,用…...
年后寒假总结及计划安排
年后寒假总结 年后主要学习了微服务,nacos (服务注册中心),feign(远程调用),网关,双token(相较于之前更加规范,更加符合企业级),配置管理 ,mybati…...
IvorySQL v4 逻辑复制槽同步功能解析:高可用场景下的数据连续性保障
功能简介 IvorySQL v4 基于 PostgreSQL 17,引入了逻辑复制槽同步至热备份数据库的功能。这一改进有效解决了旧版本中主数据库与备份数据库切换后逻辑复制中断的问题。对于那些追求数据高可用性和业务连续性的数据库来说,这无疑是一个重大的利好消息。它…...
GitLab常用操作
git init //初始化一个git的本地仓库git rm -r --cached //清除缓存,比如修改了.gitignore文件,可以先清缓存再进行下边的操作git add README.md //增加一个文件 //git add 文件夹/ //增加文件夹内的所有文件 //git add --all //增加当前项目…...
【运维笔记】docker 中 MySQL从5.7版本升级到8.0版本 - 平滑升级
在Docker环境中,将MySQL从5.7版本升级到8.0版本时,数据确实需要迁移。虽然不能直接通过docker-compose命令简单地进行版本升级并保留所有数据,但可以通过一系列步骤来实现平滑升级和数据迁移。以下是详细的迁移步骤: 一、准备阶段…...
NUMA架构介绍
NUMA 架构详解 NUMA(Non-Uniform Memory Access,非统一内存访问) 是一种多处理器系统的内存设计架构,旨在解决多处理器系统中内存访问延迟不一致的问题。与传统的 UMA(Uniform Memory Access,统一内存访问…...
三、0-1搭建springboot+vue3前后端分离-idea新建springboot项目
一、ideal新建项目1 ideal新建项目2 至此父项目就创建好了,下面创建多模块: 填好之后点击create 不删了,直接改包名,看自己喜欢 修改包名和启动类名: 打开ServiceApplication启动类,修改如下: …...
【哇! C++】类和对象(三) - 构造函数和析构函数
目录 一、构造函数 1.1 构造函数的引入 1.2 构造函数的定义和语法 1.2.1 无参构造函数: 1.2.2 带参构造函数 1.3 构造函数的特性 1.4 默认构造函数 二、析构函数 2.1 析构函数的概念 2.2 特性 如果一个类中什么成员都没有,简称为空类。 空类中…...
单片机项目复刻需要的准备工作
一、前言 复刻单片机的项目的时候,有些模块是需要焊接的。很多同学对焊接没有概念。 这里说一下做项目的基本工具。 比如:像这种模块,都需要自己焊接了排针才可以链接的。 二、基本模块 2.1 单排排针 一些模块买回来是没有焊接的&#x…...
Nginx负载均衡配置详解:轻松实现高可用与高性能
在现代Web应用中,负载均衡是确保系统高可用性和高性能的关键技术之一。Nginx作为一款高性能的HTTP服务器和反向代理服务器,其负载均衡功能被广泛应用于各种场景。本文将详细介绍如何使用Nginx实现负载均衡配置,帮助开发者轻松应对高并发和大流…...
SQLAlchemy系列教程:集成Pydantic增强数据处理能力
本教程介绍如何将Pydantic用于数据验证,SQLAlchemy用于数据库操作,从而通过强大的数据处理能力增强Python应用程序。 介绍 在现代web开发中,确保数据的有效性和完整性至关重要。Pydantic和SQLAlchemy是两个功能强大的Python库,可…...
Linux下AWK命令使用方法
文章目录 **一、AWK 基本语法****二、核心概念****1. 内置变量****2. 分隔符****3. BEGIN 和 END 块** **三、常见用法示例****1. 基础打印****2. 条件过滤****3. 计算与统计****4. 字符串操作****5. 处理 CSV 文件** **四、高级用法****1. 使用数组统计****2. 多条件分支&…...
【AI】如何理解与应对AI中的敏感话题:详细分析与实用指南
引言 随着人工智能(AI)技术的不断发展,我们在与AI交互时,可能会遇到敏感话题的讨论限制。在许多情况下,AI系统为了避免触及社会、政治或文化敏感点,会对用户输入进行一定的筛选和过滤。那么,这…...
基于开源库编写MQTT通讯
目录 1. MQTT是什么?2. 开发交互UI3. 服务器核心代码4. 客户端核心代码5. 消息订阅与发布6. 通讯测试7. MQTT与PLC通讯最后. 核心总结 1. MQTT是什么? MQTT(Message Queuing Terlemetry Transport)消息队列遥测协议;是…...
linux指令学习--sudo apt-get install vim
1. 命令分解 部分含义sudo以管理员权限运行命令(需要输入用户密码)。apt-getUbuntu 的包管理工具,用于安装、更新、卸载软件包。installapt-get 的子命令,表示安装软件包。vim要安装的软件包名称(Vim 文本编辑器&…...
PHP之变量
在你有别的编程语言的基础下,你想学习PHP,可能要了解的一些关于变量的信息。 PHP中的变量不用指定数据类型,同时必须用$开头。 全局变量 可以在除函数外任意地方访问,如果需要在函数中访问要先获取 $x 111; function tt() {gl…...
代码规范和简化标准
代码规范和简化标准是编写高质量、可维护、可扩展和可读代码的基本原则。遵循这些标准不仅能提高团队协作效率,还能减少出错的概率和后期维护的成本。以下是一些常见的代码规范和简化标准: 1. 命名规范 变量命名: 使用具有描述性的名称&…...
DeepSeek安全:AI网络安全评估与防护策略
🍅 点击文末小卡片 ,免费获取网络安全全套资料,资料在手,涨薪更快 本文基于现有的公开资料,从企业资深网络安全专家的视角,系统梳理DeepSeek技术在网络安全领域的潜在贡献与核心风险,并结合中国…...
开发环境搭建-05.后端环境搭建-前后端联调-通过断点调试熟悉项目代码特点
一.后端环境搭建-前后端联调 首先进行编译,编译通过才能够正常运行,我们可以看到已成功编译通过。 SkyApplication启动成功 登录成功 二.登录操作 1.controller层 package com.sky.controller.admin;import com.sky.constant.JwtClaimsConstant; impo…...
python py文件转为可执行文件(.exe)与 exe文件转py文件
一、py文件转为可执行文件(.exe) #安装 PyInstaller库 pip install pyinstaller #将 Python 脚本打包成一个单一的可执行文件。 pyinstaller --onefile .\123.py 文件或 pyinstaller -F .\123.py 文件 执行这个命令后,PyInstaller 会将123…...