当前位置: 首页 > article >正文

仿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,这样子就可以得到车次所有的出发站和到达站的站站组合。对于余票信息来说,可以查询座位数这张表,查询车次以及座位类型就可以得到余票的信息。

        落实到具体的代码上,首先是删除要生成某日车次的余票信息,使其支持重复生成,之后是查询这个车次的所有车站信息,根据车站的信息进行嵌套循环,先生成一个余票对象,然后根据车站进行数据的填充,最后将实体保存到数据库

@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更新 更新时间以及状态


http://www.kler.cn/a/572504.html

相关文章:

  • 【入门Web安全之前端学习的侧重点和针对性的建议】
  • 掌握 findIndex、push 和 splice:打造微信小程序的灵活图片上传功能✨
  • CSS的列表属性
  • 网线水晶头接法
  • 牙齿缺陷分割数据集labelme格式2495张4类别
  • 05类加载机制篇(D7_类加载及执行子系统的案例与实战)
  • 20250304在飞凌OK3588-C的linux R4下提高温度控制阈值为95度
  • 阿里万相,正式开源
  • Rk3568驱动开发_自动创建设备节点_8
  • Qt 文件操作+多线程+网络
  • git的恢复命令
  • Linux命令常用的有哪些?
  • 计算机考研复试高频五十问(第一期)
  • pytorch高可用的设计策略和集成放大各自功能
  • 推荐几款优秀的PDF转电子画册的软件
  • Token相关设计
  • Oracle 数据库中的用户
  • SP导入智能材质球
  • 《OpenCV》—— dlib(换脸操作)
  • 洛谷 P2142 高精度减法(详解)c++