仿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更新?更新時間以及狀態

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/72725.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/72725.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/72725.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

第十二屆藍橋杯 異或數列

原題&#xff1a; https://www.acwing.com/problem/content/3424/ 題目大意&#xff1a; A、B兩人的數初始值均為0&#xff0c;他們輪流從X數組中取數&#xff0c;可以將該數與自己的數或對方的數進行異或操作&#xff0c;A先手&#xff0c;當X中的數被取完的時候誰的數大誰…

微服務的認識與拆分

微服務架構通過將應用分解為一組小的、獨立的服務來實現&#xff0c;每個服務圍繞特定業務功能構建&#xff0c;并能獨立部署與擴展。這種架構增強了開發靈活性、提高了系統的可維護性和擴展性&#xff0c;使得團隊可以更快地響應變化和市場需求。 目錄 認識微服務 單體架構 …

高效編程指南:PyCharm與DeepSeek的完美結合

DeepSeek接入Pycharm 前幾天DeepSeek的充值窗口又悄悄的開放了&#xff0c;這也就意味著我們又可以絲滑的使用DeepSeek的API進行各種輔助性工作了。本文我們來聊聊如何在代碼編輯器中使用DeepSeek自動生成代碼。 注&#xff1a;本文適用于所有的JetBrains開發工具&#xff0c…

項目中同時使用Redis(lettuce)和Redisson的報錯

溫馨提示&#xff1a;圖片有點小&#xff0c;可以放大頁面進行查看... 問題1&#xff1a;版本沖突 直接上圖&#xff0c;這個錯表示依賴版本不匹配問題&#xff0c;我本地SpringBoot用的是2.7&#xff0c;但是Redisson版本用的3.32.5。 我們通過點擊 artifactId跟進去 發現它…

Jackson 詳解

目錄 前言 Jackson 是 Java 生態中最流行的 JSON 處理庫之一&#xff0c;廣泛應用于 RESTful API、數據存儲和傳輸等場景。它提供了高效、靈活的 JSON 序列化和反序列化功能&#xff0c;支持注解、模塊化設計和多種數據格式&#xff08;如 XML、YAML&#xff09;。本文將詳細介…

H.264,H.265,H.266標準技術改進

關于H.264,H.265,H.266相關資料鏈接&#xff1a; 標準及中文資料鏈接 視頻編碼中的主要技術 視頻編碼的目標是在保證視頻質量的前提下&#xff0c;盡可能減少數據量。以下是視頻編碼中的核心技術&#xff1a; 塊劃分&#xff08;Block Partitioning&#xff09; 將視頻幀劃分…

clickhouse安裝路徑

《ClickHouse安裝路徑指南》 大家好&#xff0c;今天我們將一起學習如何在電腦上找到和理解ClickHouse的安裝路徑。這將幫助學生、科研人員以及任何對數據庫技術感興趣的人更好地管理他們的數據查詢工作。 ClickHouse是一款列式存儲數據庫管理系統&#xff08;DBMS&#xff09…

時序數據庫 InfluxDB 3.0 版本性能實測報告:寫入吞吐量提升效果驗證

亮點總結&#xff1a; TSBS 測試表明&#xff0c;對于少于 100 萬臺設備的數據集&#xff0c;InfluxDB OSS 3.0 的數據寫入速度實際上比 InfluxDB OSS 1.8 更慢。 對于 100 萬臺及以上設備的數據集&#xff0c;InfluxDB OSS 3.0 的數據寫入性能才開始超過 InfluxDB OSS 1.8。…

AS32X601雙核鎖步MCU技術優勢分析

AS32X601是國科安芯公司研制的一系列基于32位RISC-V指令集車規級MCU處理器芯片。主頻高達180MHz&#xff0c;支持雙核鎖步架構&#xff0c;基于軟錯誤防護技術加持&#xff0c;顯著提高芯片安全性能。產品具有高安全、低失效、多IO、低成本、抗輻照等特點。 一、功能安全與可靠…

基于 LeNet 網絡的 MNIST 數據集圖像分類

1.LeNet的原始實驗數據集MNIST 名稱&#xff1a;MNIST手寫數字數據集 數據類型&#xff1a;灰度圖 &#xff08;一通道&#xff09; 圖像大小&#xff1a;28*28 類別數&#xff1a;10類&#xff08;數字0-9&#xff09; 1.通過torchvision.datasets.MNIST下載并保存到本地…

電池綜合測試儀:科技賦能,精準守護能源安全

在當今這個科技日新月異的時代&#xff0c;電池作為眾多電子設備的心臟&#xff0c;其性能的穩定與高效直接關系到設備的運行質量與使用安全。隨著電動汽車、可穿戴設備、儲能系統等領域的快速發展&#xff0c;對電池性能的檢測與評估提出了更高要求。在此背景下&#xff0c;電…

【Linux 22.4 ubuntu 安裝cuda12.1 完整方案】

下載cuda12.1 官網網址 wget https://developer.download.nvidia.com/compute/cuda/12.1.1/local_installers/cuda_12.1.1_530.30.02_linux.run sudo sh cuda_12.1.1_530.30.02_linux.run!import! 如果已經安裝驅動&#xff0c;則不要選擇dirver那項 添加環境變量 vim ~/.b…

實戰案例分享:Android WLAN Hal層移植(MTK+QCA6696)

本文將詳細介紹基于MTK平臺&#xff0c;適配高通&#xff08;Qualcomm&#xff09;QCA6696芯片的Android WLAN HAL層的移植過程&#xff0c;包括HIDL接口定義、Wi-Fi驅動移植以及wpa_supplicant適配過程&#xff0c;涵蓋STA與AP模式的常見問題與解決方法。 1. HIDL接口簡介 HID…

Greenplum6.19集群搭建

一&#xff0c;安裝說明 1.1環境說明 1、首先確定部署的環境&#xff0c;確定下服務器的端口&#xff0c;一般默認是22的端口&#xff1b; 2、當前這份文檔是服務器處于10022端口下部署的&#xff08;現場生產環境要求&#xff0c;22端口在生產環境存在安全隱患&#xff09;&…

電商項目-秒殺系統(四)秒殺異步下單防止重復秒殺

一、 防止惡意刷單解決 在生產場景下&#xff0c;可能會有一些人會惡意訪問當前網站&#xff0c;來進行惡意的刷單。這樣會造成當前系統出現一些業務上的業務混亂&#xff0c;出現臟數據&#xff0c;或者造成后端訪問壓力大等問題。 一般要解決這個問題的話&#xff0c;前端可…

原生android 打包.aar到uniapp使用

1.原生安卓里面引入uniapp官方提供的包文件&#xff1a; uniapp-v8-release.aar 2.提供uniapp調用的接口&#xff0c;新建類文件繼承UniModule&#xff0c; package com.dermandar.panoramal;import com.scjt.lib.certlib;import io.dcloud.feature.uniapp.annotation.UniJSM…

Android 多用戶相關

Android 多用戶相關 本文主要記錄下android 多用戶相關的adb 命令操作. 1: 獲取用戶列表 命令: adb shell pm list users 輸出如下: Users:UserInfo{0:機主:c13} running默認只有一個用戶, id為0 &#xff0c;用戶狀態為運行 2: 創建新用戶 命令&#xff1a; adb shell …

基于Spring Boot的高校就業招聘系統的設計與實現(LW+源碼+講解)

專注于大學生項目實戰開發,講解,畢業答疑輔導&#xff0c;歡迎高校老師/同行前輩交流合作?。 技術范圍&#xff1a;SpringBoot、Vue、SSM、HLMT、小程序、Jsp、PHP、Nodejs、Python、爬蟲、數據可視化、安卓app、大數據、物聯網、機器學習等設計與開發。 主要內容&#xff1a;…

前端安全面試題匯總及參考答案

目錄 簡述 XSS 攻擊的原理及三種常見類型(存儲型、反射型、DOM 型) 如何在前端防御 XSS 攻擊?列舉編碼、過濾、CSP 策略的具體實現方式 富文本編輯器場景下如何安全處理用戶輸入的 HTML 內容? 如何通過 HttpOnly 屬性增強 Cookie 安全性?它與 XSS 防御的關系是什么? …

Linux驅動開發(1.基礎創建)

序言&#xff1a;從高層邏輯到底層硬件的回歸 在當今的軟件開發中&#xff0c;我們習慣于用高級語言構建抽象層——通過框架、庫和云服務快速實現功能。這種“軟邏輯”的便利性讓開發效率倍增&#xff0c;卻也逐漸模糊了我們對計算機本質的認知&#xff1a;一切代碼終將落地為…