package com.sf.service.impl; import cn.hutool.core.util.StrUtil; import com.alibaba.fastjson2.JSON; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.google.common.collect.Lists; import com.sf.dto.SeatClassDTO; import com.sf.dto.TicketListDTO; import com.sf.dto.domain.RouteDTO; import com.sf.dto.remote.TicketOrderCreateRemoteReqDTO; import com.sf.dto.remote.TicketOrderItemCreateRemoteReqDTO; import com.sf.dto.req.PurchaseTicketReqDTO; import com.sf.dto.req.TicketPageQueryReqDTO; import com.sf.dto.resp.TicketOrderDetailRespDTO; import com.sf.dto.resp.TicketPageQueryRespDTO; import com.sf.dto.resp.TicketPurchaseRespDTO; import com.sf.dto.resp.TrainPurchaseTicketRespDTO; import com.sf.entity.*; import com.sf.mapper.*; import com.sf.remote.TicketOrderRemoteService; import com.sf.service.TicketService; import com.sf.service.handler.TrainSeatTypeSelector; import com.sf.util.DateUtil; import com.sf.util.StationCalculateUtil; import com.sf.util.TimeStringComparator; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.opengoofy.index12306.framework.starter.cache.DistributedCache; import org.opengoofy.index12306.framework.starter.cache.toolkit.CacheUtil; import org.opengoofy.index12306.framework.starter.convention.exception.ServiceException; import org.opengoofy.index12306.framework.starter.convention.result.Result; import org.opengoofy.index12306.frameworks.starter.user.core.UserContext; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import static com.sf.constant.Index12306Constant.ADVANCE_TICKET_DAY; import static com.sf.constant.RedisKeyConstant.*; import static com.sf.util.DateUtil.convertDateToLocalTime; @Slf4j @RequiredArgsConstructor @Service public class TicketServiceImpl extends ServiceImpl implements TicketService { private final DistributedCache distributedCache; private final StationMapper stationMapper; private final TrainStationRelationMapper trainStationRelationMapper; private final TrainMapper trainMapper; private final TrainStationPriceMapper trainStationPriceMapper; private final TrainStationMapper trainStationMapper; private final SeatMapper seatMapper; private final TrainSeatTypeSelector trainSeatTypeSelector; private final TicketOrderRemoteService ticketOrderRemoteService; @Override public TicketPageQueryRespDTO pageListTicketQueryByMysql(TicketPageQueryReqDTO requestParam) { System.out.println("TicketPageQueryRespDTO pageListTicketQueryByMysql"); // 准备stationDetails 找到出发站和到达站的名字 // INSERT INTO `t_station` (`id`, `code`, `name`, `spell`, `region`, `region_name`, `create_time`, `update_time`, `del_flag`) // VALUES // (1, 'VNP', '北京南', 'beijingnan', 'BJP', '北京', '2023-06-01 20:54:00', '2023-06-01 20:54:00', 0); List stationDOList = stationMapper.selectList(Wrappers.emptyWrapper()); Map regionTrainStationMap = new HashMap<>(); stationDOList.forEach(each -> regionTrainStationMap.put(each.getCode(), each.getRegionName())); List stationDetails = new ArrayList<>(); stationDetails.add(regionTrainStationMap.get(requestParam.getFromStation())); stationDetails.add(regionTrainStationMap.get(requestParam.getToStation())); // LambdaQueryWrapper stationWrapper = new LambdaQueryWrapper<>(); // stationWrapper.in(StationDO::getCode, List.of(requestParam.getFromStation(), requestParam.getToStation())); // List stationDOS = stationMapper.selectList(stationWrapper); // 这里可以直接将code值传入数据库 就不需要再转化成map了 // 然后将两个名字作为参数 去 train_station_relation表中查询 // INSERT INTO `t_train_station_relation` (`id`, `train_id`, `departure`, `arrival`, `start_region`, `end_region`, `departure_flag`, `arrival_flag`, `departure_time`, `arrival_time`, `create_time`, `update_time`, `del_flag`) // VALUES // (1665025584123056128, 1, '北京南', '济南西', '北京', '济南', 1, 0, '2023-06-01 09:56:00', '2023-06-01 11:19:00', '2023-06-04 00:00:09', '2023-06-04 00:00:09', 0); // select * from t_train_station_relation where start_region = '北京' and end_region = '杭州' LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(TrainStationRelationDO.class) .eq(TrainStationRelationDO::getStartRegion, stationDetails.get(0)) .eq(TrainStationRelationDO::getEndRegion, stationDetails.get(1)); List trainStationRelationList = trainStationRelationMapper.selectList(queryWrapper); // 将TrainDO和TrainStationRelationDO中的数据整合在一起 List seatResults = new ArrayList<>(); // Map regionTrainStationAllMap = new HashMap<>(); // 遍历找到的车次 (4个) for (TrainStationRelationDO each : trainStationRelationList) { // INSERT INTO `t_train` (`id`, `train_number`, `train_type`, `train_tag`, `train_brand`, `start_station`, `end_station`, `start_region`, `end_region`, `sale_time`, `sale_status`, `departure_time`, `arrival_time`, `create_time`, `update_time`, `del_flag`) // VALUES // (1, 'G35', 0, '0,1,2', '0,6', '北京南', '宁波', '北京', '宁波', '2023-05-15 14:30:00', 0, '2023-06-01 09:56:00', '2023-06-01 15:14:00', '2023-06-01 20:45:00', '2023-06-01 20:45:00', 0); TrainDO trainDO = trainMapper.selectById(each.getTrainId()); TicketListDTO result = new TicketListDTO(); result.setTrainId(String.valueOf(trainDO.getId())); result.setTrainNumber(trainDO.getTrainNumber()); result.setDepartureTime(convertDateToLocalTime(each.getDepartureTime(), "HH:mm")); result.setArrivalTime(convertDateToLocalTime(each.getArrivalTime(), "HH:mm")); result.setDuration(DateUtil.calculateHourDifference(each.getDepartureTime(), each.getArrivalTime())); result.setDeparture(each.getDeparture()); result.setArrival(each.getArrival()); result.setDepartureFlag(each.getDepartureFlag()); result.setArrivalFlag(each.getArrivalFlag()); result.setTrainType(trainDO.getTrainType()); result.setTrainBrand(trainDO.getTrainBrand()); if (StrUtil.isNotBlank(trainDO.getTrainTag())) { result.setTrainTags(StrUtil.split(trainDO.getTrainTag(), ",")); } long betweenDay = cn.hutool.core.date.DateUtil.betweenDay(each.getDepartureTime(), each.getArrivalTime(), false); result.setDaysArrived((int) betweenDay); result.setSaleStatus(new Date().after(trainDO.getSaleTime()) ? 0 : 1); result.setSaleTime(convertDateToLocalTime(trainDO.getSaleTime(), "MM-dd HH:mm")); seatResults.add(result); // regionTrainStationAllMap.put( // // TrainId_Departure_Arrival // CacheUtil.buildKey(String.valueOf(each.getTrainId()), each.getDeparture(), each.getArrival()), // JSON.toJSONString(result)); } // 按照出发时间排序 seatResults = seatResults.stream().sorted(new TimeStringComparator()).toList(); for (TicketListDTO each : seatResults) { // INSERT INTO `t_train_station_relation` (`id`, `train_id`, `departure`, `arrival`, `start_region`, `end_region`, `departure_flag`, `arrival_flag`, `departure_time`, `arrival_time`, `create_time`, `update_time`, `del_flag`) // VALUES // (1665025584206942208, 1, '北京南', '杭州东', '北京', '杭州', 1, 0, '2023-06-01 09:56:00', '2023-06-01 14:26:00', '2023-06-04 00:00:09', '2023-06-04 00:00:09', 0); // select * from t_train_station_price where departure = '北京南' and arrival = '杭州东' and train_id = '1' LambdaQueryWrapper trainStationPriceQueryWrapper = Wrappers.lambdaQuery(TrainStationPriceDO.class) .eq(TrainStationPriceDO::getDeparture, each.getDeparture()) .eq(TrainStationPriceDO::getArrival, each.getArrival()) .eq(TrainStationPriceDO::getTrainId, each.getTrainId()); List trainStationPriceDOList = trainStationPriceMapper.selectList(trainStationPriceQueryWrapper); // 查询结果 // INSERT INTO `t_train_station_price` (`id`, `train_id`, `departure`, `arrival`, `seat_type`, `price`, `create_time`, `update_time`, `del_flag`) // VALUES // (1664877136516444160, 1, '北京南', '杭州东', 0, 231300, '2023-06-03 14:10:16', '2023-06-03 14:10:16', 0); // 这里是3条 对应商务座、一等座、二等座的价格 List seatClassList = new ArrayList<>(); trainStationPriceDOList.forEach(item -> { String trainId = each.getTrainId(); String departure = item.getDeparture(); String arrival = item.getArrival(); // Map seatMarginMap = seatMarginCacheLoader.load( // String.valueOf(each.getTrainId()), seatType, item.getDeparture(), item.getArrival()); Map> trainStationRemainingTicketMaps = new LinkedHashMap<>(); // INSERT INTO `t_train` (`id`, `train_number`, `train_type`, `train_tag`, `train_brand`, `start_station`, `end_station`, `start_region`, `end_region`, `sale_time`, `sale_status`, `departure_time`, `arrival_time`, `create_time`, `update_time`, `del_flag`) // VALUES // (1, 'G35', 0, '0,1,2', '0,6', '北京南', '宁波', '北京', '宁波', '2023-05-15 14:30:00', 0, '2023-06-01 09:56:00', '2023-06-01 15:14:00', '2023-06-01 20:45:00', '2023-06-01 20:45:00', 0); TrainDO trainDO = trainMapper.selectById(each.getTrainId()); // select departure from t_train_station where train_id = '1' LambdaQueryWrapper queryWrapperNew = Wrappers.lambdaQuery(TrainStationDO.class) .eq(TrainStationDO::getTrainId, trainId) .select(TrainStationDO::getDeparture); List trainStationDOList = trainStationMapper.selectList(queryWrapperNew); // 把查询到的出发站放在这个list中 List trainStationAllList = trainStationDOList.stream().map(TrainStationDO::getDeparture).collect(Collectors.toList()); // 计算出发站和终点站中间的站点 List routeDTOList = StationCalculateUtil.throughStation(trainStationAllList, departure, arrival); // 假设不为空 // G高铁是0 D动车1 Z直达是2 switch (trainDO.getTrainType()) { case 0 -> { for (RouteDTO routeDTO : routeDTOList) { Map trainStationRemainingTicket = new LinkedHashMap<>(); trainStationRemainingTicket.put("0", selectSeatMargin(trainId, 0, routeDTO.getStartStation(), routeDTO.getEndStation())); trainStationRemainingTicket.put("1", selectSeatMargin(trainId, 1, routeDTO.getStartStation(), routeDTO.getEndStation())); trainStationRemainingTicket.put("2", selectSeatMargin(trainId, 2, routeDTO.getStartStation(), routeDTO.getEndStation())); String actualKeySuffix = CacheUtil.buildKey(trainId, routeDTO.getStartStation(), routeDTO.getEndStation()); trainStationRemainingTicketMaps.put(TRAIN_STATION_REMAINING_TICKET + actualKeySuffix, trainStationRemainingTicket); } } case 1 -> { for (RouteDTO routeDTO : routeDTOList) { Map trainStationRemainingTicket = new LinkedHashMap<>(); trainStationRemainingTicket.put("3", selectSeatMargin(trainId, 3, routeDTO.getStartStation(), routeDTO.getEndStation())); trainStationRemainingTicket.put("4", selectSeatMargin(trainId, 4, routeDTO.getStartStation(), routeDTO.getEndStation())); trainStationRemainingTicket.put("5", selectSeatMargin(trainId, 5, routeDTO.getStartStation(), routeDTO.getEndStation())); trainStationRemainingTicket.put("13", selectSeatMargin(trainId, 13, routeDTO.getStartStation(), routeDTO.getEndStation())); String actualKeySuffix = CacheUtil.buildKey(trainId, routeDTO.getStartStation(), routeDTO.getEndStation()); trainStationRemainingTicketMaps.put(TRAIN_STATION_REMAINING_TICKET + actualKeySuffix, trainStationRemainingTicket); } } case 2 -> { for (RouteDTO routeDTO : routeDTOList) { Map trainStationRemainingTicket = new LinkedHashMap<>(); trainStationRemainingTicket.put("6", selectSeatMargin(trainId, 6, routeDTO.getStartStation(), routeDTO.getEndStation())); trainStationRemainingTicket.put("7", selectSeatMargin(trainId, 7, routeDTO.getStartStation(), routeDTO.getEndStation())); trainStationRemainingTicket.put("8", selectSeatMargin(trainId, 8, routeDTO.getStartStation(), routeDTO.getEndStation())); trainStationRemainingTicket.put("13", selectSeatMargin(trainId, 13, routeDTO.getStartStation(), routeDTO.getEndStation())); String actualKeySuffix = CacheUtil.buildKey(trainId, routeDTO.getStartStation(), routeDTO.getEndStation()); trainStationRemainingTicketMaps.put(TRAIN_STATION_REMAINING_TICKET + actualKeySuffix, trainStationRemainingTicket); } } } String keySuffix = CacheUtil.buildKey(trainId, departure, arrival); Map seatMarginMap = trainStationRemainingTicketMaps.get(TRAIN_STATION_REMAINING_TICKET + keySuffix); int quantity = Integer.parseInt(seatMarginMap.get(String.valueOf(item.getSeatType()))); seatClassList.add(new SeatClassDTO( item.getSeatType(), quantity, new BigDecimal(item.getPrice()).divide(new BigDecimal("100"), 1, RoundingMode.HALF_UP), false)); }); each.setSeatClassList(seatClassList); } return TicketPageQueryRespDTO.builder() .trainList(seatResults) .departureStationList(buildDepartureStationList(seatResults)) .arrivalStationList(buildArrivalStationList(seatResults)) .trainBrandList(buildTrainBrandList(seatResults)) .seatClassTypeList(buildSeatClassList(seatResults)) .build(); } private String selectSeatMargin(String trainId, Integer type, String departure, String arrival) { LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(SeatDO.class) .eq(SeatDO::getTrainId, trainId) .eq(SeatDO::getSeatType, type) .eq(SeatDO::getSeatStatus, 0) .eq(SeatDO::getStartStation, departure) .eq(SeatDO::getEndStation, arrival); return String.valueOf(seatMapper.selectCount(queryWrapper)); } // http://localhost:9000/api/ticket-service/ticket/query?fromStation=BJP&toStation=HZH&departureDate=2024-05-15 @Override public TicketPageQueryRespDTO pageListTicketQueryByRedis(TicketPageQueryReqDTO requestParam) { StringRedisTemplate stringRedisTemplate = (StringRedisTemplate) distributedCache.getInstance(); // 先获取 index12306-ticket-service:region_train_station_mapping 中的数据 // 得到出发站和目的站的名字 List stationDetails = stringRedisTemplate.opsForHash() .multiGet(REGION_TRAIN_STATION_MAPPING, Lists.newArrayList(requestParam.getFromStation(), requestParam.getToStation())); // index12306-ticket-service:region_train_station:%s_%s // 拼接出key index12306-ticket-service:region_train_station:北京_杭州 String buildRegionTrainStationHashKey = String.format(REGION_TRAIN_STATION, stationDetails.get(0), stationDetails.get(1)); // 从redis中 取出key存储的value值 对应一个map结果 Map regionTrainStationAllMap = stringRedisTemplate.opsForHash().entries(buildRegionTrainStationHashKey); // 遍历其中每个元素 转化为多个TicketListDTO对象组成的list List seatResults = regionTrainStationAllMap.values().stream().map( each -> JSON.parseObject(each.toString(), TicketListDTO.class)).toList(); //使用普通的方式来处理的逻辑 // List newSeatResults = new ArrayList<>(); // Collection values = regionTrainStationAllMap.values(); // values.forEach((Object each) -> { // TicketListDTO ticketListDTO = JSON.parseObject(each.toString(), TicketListDTO.class); // newSeatResults.add(ticketListDTO); // }); // 按照出发时间排序 seatResults = seatResults.stream().sorted(new TimeStringComparator()).toList(); for (TicketListDTO each : seatResults) { // index12306-ticket-service:train_station_price:%s_%s_%s // 拼接成key index12306-ticket-service:train_station_price:1_北京南_杭州东 String trainStationPriceStr = distributedCache.safeGet( String.format(TRAIN_STATION_PRICE, each.getTrainId(), each.getDeparture(), each.getArrival()), String.class, null, ADVANCE_TICKET_DAY, TimeUnit.DAYS ); // 返回结果是由 TrainStationPriceDO对象组成的list 所转化的json数据 List trainStationPriceDOList = JSON.parseArray(trainStationPriceStr, TrainStationPriceDO.class); List seatClassList = new ArrayList<>(); // 遍历每一个TrainStationPriceDO trainStationPriceDOList.forEach(item -> { String seatType = String.valueOf(item.getSeatType()); // 拼接成key index12306-ticket-service:train_station_remaining_ticket:1_北京南_杭州东 String keySuffix = StrUtil.join("_", each.getTrainId(), item.getDeparture(), item.getArrival()); // 获取每个席别对应的票数 Object quantityObj = stringRedisTemplate.opsForHash().get(TRAIN_STATION_REMAINING_TICKET + keySuffix, seatType); int quantity = Optional.ofNullable(quantityObj).map(Object::toString).map(Integer::parseInt).get(); seatClassList.add(new SeatClassDTO(item.getSeatType(), quantity, new BigDecimal(item.getPrice()).divide(new BigDecimal("100"), 1, RoundingMode.HALF_UP), false)); }); each.setSeatClassList(seatClassList); } // 组装数据 return TicketPageQueryRespDTO.builder() .trainList(seatResults) .departureStationList(buildDepartureStationList(seatResults)) .arrivalStationList(buildArrivalStationList(seatResults)) .trainBrandList(buildTrainBrandList(seatResults)) .seatClassTypeList(buildSeatClassList(seatResults)) .build(); } @Override public TicketPurchaseRespDTO purchaseTicketsV2(PurchaseTicketReqDTO requestParam) { return executePurchaseTickets(requestParam); } public TicketPurchaseRespDTO executePurchaseTickets(PurchaseTicketReqDTO requestParam) { List ticketOrderDetailResults = new ArrayList<>(); String trainId = requestParam.getTrainId(); // 节假日高并发购票Redis能扛得住么?详情查看:https://nageoffer.com/12306/question TrainDO trainDO = trainMapper.selectById(trainId); List trainPurchaseTicketResults = trainSeatTypeSelector.select(trainDO.getTrainType(), requestParam); List ticketDOList = trainPurchaseTicketResults.stream() .map(each -> TicketDO.builder() .username(UserContext.getUsername()) .trainId(Long.parseLong(requestParam.getTrainId())) .carriageNumber(each.getCarriageNumber()) .seatNumber(each.getSeatNumber()) .passengerId(each.getPassengerId()) .ticketStatus(0) //未支付状态 .build()) .toList(); saveBatch(ticketDOList); Result ticketOrderResult; try { List orderItemCreateRemoteReqDTOList = new ArrayList<>(); trainPurchaseTicketResults.forEach(each -> { TicketOrderItemCreateRemoteReqDTO orderItemCreateRemoteReqDTO = TicketOrderItemCreateRemoteReqDTO.builder() .amount(each.getAmount()) .carriageNumber(each.getCarriageNumber()) .seatNumber(each.getSeatNumber()) .idCard(each.getIdCard()) .idType(each.getIdType()) .phone(each.getPhone()) .seatType(each.getSeatType()) .ticketType(each.getUserType()) .realName(each.getRealName()) .build(); TicketOrderDetailRespDTO ticketOrderDetailRespDTO = TicketOrderDetailRespDTO.builder() .amount(each.getAmount()) .carriageNumber(each.getCarriageNumber()) .seatNumber(each.getSeatNumber()) .idCard(each.getIdCard()) .idType(each.getIdType()) .seatType(each.getSeatType()) .ticketType(each.getUserType()) .realName(each.getRealName()) .build(); orderItemCreateRemoteReqDTOList.add(orderItemCreateRemoteReqDTO); ticketOrderDetailResults.add(ticketOrderDetailRespDTO); }); LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(TrainStationRelationDO.class) .eq(TrainStationRelationDO::getTrainId, trainId) .eq(TrainStationRelationDO::getDeparture, requestParam.getDeparture()) .eq(TrainStationRelationDO::getArrival, requestParam.getArrival()); TrainStationRelationDO trainStationRelationDO = trainStationRelationMapper.selectOne(queryWrapper); TicketOrderCreateRemoteReqDTO orderCreateRemoteReqDTO = TicketOrderCreateRemoteReqDTO.builder() .departure(requestParam.getDeparture()) .arrival(requestParam.getArrival()) .orderTime(new Date()) .source(0) //购票方式 互联网购票是0 .trainNumber(trainDO.getTrainNumber()) .departureTime(trainStationRelationDO.getDepartureTime()) .arrivalTime(trainStationRelationDO.getArrivalTime()) .ridingDate(trainStationRelationDO.getDepartureTime()) .userId(UserContext.getUserId()) .username(UserContext.getUsername()) .trainId(Long.parseLong(requestParam.getTrainId())) .ticketOrderItems(orderItemCreateRemoteReqDTOList) .build(); ticketOrderResult = ticketOrderRemoteService.createTicketOrder(orderCreateRemoteReqDTO); if (!ticketOrderResult.isSuccess() || StrUtil.isBlank(ticketOrderResult.getData())) { log.error("订单服务调用失败,返回结果:{}", ticketOrderResult.getMessage()); throw new ServiceException("订单服务调用失败"); } } catch (Throwable ex) { log.error("远程调用订单服务创建错误,请求参数:{}", JSON.toJSONString(requestParam), ex); throw ex; } return new TicketPurchaseRespDTO(ticketOrderResult.getData(), ticketOrderDetailResults); } private List buildDepartureStationList(List seatResults) { return seatResults.stream().map(TicketListDTO::getDeparture).distinct().collect(Collectors.toList()); } private List buildArrivalStationList(List seatResults) { return seatResults.stream().map(TicketListDTO::getArrival).distinct().collect(Collectors.toList()); } private List buildSeatClassList(List seatResults) { Set resultSeatClassList = new HashSet<>(); for (TicketListDTO each : seatResults) { for (SeatClassDTO item : each.getSeatClassList()) { resultSeatClassList.add(item.getType()); } } return resultSeatClassList.stream().toList(); } private List buildTrainBrandList(List seatResults) { Set trainBrandSet = new HashSet<>(); for (TicketListDTO each : seatResults) { if (StrUtil.isNotBlank(each.getTrainBrand())) { trainBrandSet.addAll(StrUtil.split(each.getTrainBrand(), ",").stream().map(Integer::parseInt).toList()); } } return trainBrandSet.stream().toList(); } }