仿12306校招项目业务二(列车检索)

目录

验证数据

加载城市数据

查询列车站点信息

查询列车余票信息

构建列车返回数据


12306 项目中列车数据检索接口路径  TicketController的pageListTicketQuery。

@GetMapping("/api/ticket-service/ticket/query")
    public Result<TicketPageQueryRespDTO> pageListTicketQuery(TicketPageQueryReqDTO requestParam) {
        return Results.success(ticketService.pageListTicketQueryV1(requestParam));
    }

验证数据

查询列车数据 Service 实现层接口第一行代码,就是通过责任链模式验证数据是否必填以及城市

数据是否存在等执行逻辑。

ticketPageQueryAbstractChainContext.handler(TicketChainMarkEnum.TRAIN_QUERY_FILTER.name(), requestParam);

handler具体代码如下:

1. 检查相关数据是否为空或空的字符串,这个是最先被执行的:

/**
 * 查询列车车票流程过滤器之验证数据是否为空或空的字符串
 *
 * 
 */
@Component
public class TrainTicketQueryParamNotNullChainFilter implements TrainTicketQueryChainFilter<TicketPageQueryReqDTO> {

    @Override
    public void handler(TicketPageQueryReqDTO requestParam) {
        if (StrUtil.isBlank(requestParam.getFromStation())) {
            throw new ClientException("出发地不能为空");
        }
        if (StrUtil.isBlank(requestParam.getToStation())) {
            throw new ClientException("目的地不能为空");
        }
        if (requestParam.getDepartureDate() == null) {
            throw new ClientException("出发日期不能为空");
        }
    }

    @Override
    public int getOrder() {
        return 0;
    }
}

2. 检查数据是否正确

/**
 * 查询列车车票流程过滤器之验证数据是否正确
 *
 * 
 */
@Component
@RequiredArgsConstructor
public class TrainTicketQueryParamVerifyChainFilter implements TrainTicketQueryChainFilter<TicketPageQueryReqDTO> {

    private final RegionMapper regionMapper;
    private final StationMapper stationMapper;
    private final DistributedCache distributedCache;
    private final RedissonClient redissonClient;

    /**
     * 缓存数据为空并且已经加载过标识
     */
    private static boolean CACHE_DATA_ISNULL_AND_LOAD_FLAG = false;

    @Override
    public void handler(TicketPageQueryReqDTO requestParam) {
        StringRedisTemplate stringRedisTemplate = (StringRedisTemplate) distributedCache.getInstance();
        HashOperations<String, Object, Object> hashOperations = stringRedisTemplate.opsForHash();
        List<Object> actualExistList = hashOperations.multiGet(
                QUERY_ALL_REGION_LIST,
                ListUtil.toList(requestParam.getFromStation(), requestParam.getToStation())
        );
        long emptyCount = actualExistList.stream().filter(Objects::isNull).count();
        if (emptyCount == 0L) {
            return;
        }
        if (emptyCount == 1L || (emptyCount == 2L && CACHE_DATA_ISNULL_AND_LOAD_FLAG && distributedCache.hasKey(QUERY_ALL_REGION_LIST))) {
            throw new ClientException("出发地或目的地不存在");
        }
        RLock lock = redissonClient.getLock(LOCK_QUERY_ALL_REGION_LIST);
        lock.lock();
        try {
            if (distributedCache.hasKey(QUERY_ALL_REGION_LIST)) {
                actualExistList = hashOperations.multiGet(
                        QUERY_ALL_REGION_LIST,
                        ListUtil.toList(requestParam.getFromStation(), requestParam.getToStation())
                );
                emptyCount = actualExistList.stream().filter(Objects::nonNull).count();
                if (emptyCount != 2L) {
                    throw new ClientException("出发地或目的地不存在");
                }
                return;
            }
            List<RegionDO> regionDOList = regionMapper.selectList(Wrappers.emptyWrapper());
            List<StationDO> stationDOList = stationMapper.selectList(Wrappers.emptyWrapper());
            HashMap<Object, Object> regionValueMap = Maps.newHashMap();
            for (RegionDO each : regionDOList) {
                regionValueMap.put(each.getCode(), each.getName());
            }
            for (StationDO each : stationDOList) {
                regionValueMap.put(each.getCode(), each.getName());
            }
            hashOperations.putAll(QUERY_ALL_REGION_LIST, regionValueMap);
            CACHE_DATA_ISNULL_AND_LOAD_FLAG = true;
            emptyCount = regionValueMap.keySet().stream()
                    .filter(each -> StrUtil.equalsAny(each.toString(), requestParam.getFromStation(), requestParam.getToStation()))
                    .count();
            if (emptyCount != 2L) {
                throw new ClientException("出发地或目的地不存在");
            }
        } finally {
            lock.unlock();
        }
    }

    @Override
    public int getOrder() {
        return 20;
    }
}

加载城市数据

12306 站点查询实际功能中,比如你搜索了北京南到杭州东的搜索条件,它会帮你列出北京到杭州所有的列车车次。这个很好实现,直接通过站点关联到城市,通过城市查询列车即可。我们在缓存中,有一个 Hash 结构数据,专门负责保存列车站点 Code 值与城市之间的关联关系。

// 列车查询逻辑较为复杂,详细解析文章查看 https://nageoffer.com/12306/question
        // v1 版本存在严重的性能深渊问题,v2 版本完美的解决了该问题。通过 Jmeter 压测聚合报告得知,性能提升在 300% - 500%+
        List<Object> stationDetails = stringRedisTemplate.opsForHash()
                .multiGet(REGION_TRAIN_STATION_MAPPING, Lists.newArrayList(requestParam.getFromStation(), requestParam.getToStation()));
        long count = stationDetails.stream().filter(Objects::isNull).count();
        if (count > 0) {
            RLock lock = redissonClient.getLock(LOCK_REGION_TRAIN_STATION_MAPPING);
            lock.lock();
            try {
                stationDetails = stringRedisTemplate.opsForHash()
                        .multiGet(REGION_TRAIN_STATION_MAPPING, Lists.newArrayList(requestParam.getFromStation(), requestParam.getToStation()));
                count = stationDetails.stream().filter(Objects::isNull).count();
                if (count > 0) {
                    List<StationDO> stationDOList = stationMapper.selectList(Wrappers.emptyWrapper());
                    Map<String, String> regionTrainStationMap = new HashMap<>();
                    stationDOList.forEach(each -> regionTrainStationMap.put(each.getCode(), each.getRegionName()));
                    stringRedisTemplate.opsForHash().putAll(REGION_TRAIN_STATION_MAPPING, regionTrainStationMap);
                    stationDetails = new ArrayList<>();
                    stationDetails.add(regionTrainStationMap.get(requestParam.getFromStation()));
                    stationDetails.add(regionTrainStationMap.get(requestParam.getToStation()));
                }
            } finally {
                lock.unlock();
            }
        }

查询列车站点信息

采用Redis而不是Elasticsearch,因为搜索只允许选择一天的出发日期。

同时在12306网站上尝试,虽然页面上有很多查询条件,但大多数条件都是由前端进行筛选,实际上并没有触发后端的请求,发现只有在点击“查询”按钮时才会真正触发后端的请求,而点击页面上的其他筛选条件并不会向后端发出请求。

列车站点数据存入 Redis 中,结构如下:

具体查询代码:

List<TicketListDTO> seatResults = new ArrayList<>();
        String buildRegionTrainStationHashKey = String.format(REGION_TRAIN_STATION, stationDetails.get(0), stationDetails.get(1));
        Map<Object, Object> regionTrainStationAllMap = stringRedisTemplate.opsForHash().entries(buildRegionTrainStationHashKey);
        if (MapUtil.isEmpty(regionTrainStationAllMap)) {
            RLock lock = redissonClient.getLock(LOCK_REGION_TRAIN_STATION);
            lock.lock();
            try {
                regionTrainStationAllMap = stringRedisTemplate.opsForHash().entries(buildRegionTrainStationHashKey);
                if (MapUtil.isEmpty(regionTrainStationAllMap)) {
                    LambdaQueryWrapper<TrainStationRelationDO> queryWrapper = Wrappers.lambdaQuery(TrainStationRelationDO.class)
                            .eq(TrainStationRelationDO::getStartRegion, stationDetails.get(0))
                            .eq(TrainStationRelationDO::getEndRegion, stationDetails.get(1));
                    List<TrainStationRelationDO> trainStationRelationList = trainStationRelationMapper.selectList(queryWrapper);
                    for (TrainStationRelationDO each : trainStationRelationList) {
                        TrainDO trainDO = distributedCache.safeGet(
                                TRAIN_INFO + each.getTrainId(),
                                TrainDO.class,
                                () -> trainMapper.selectById(each.getTrainId()),
                                ADVANCE_TICKET_DAY,
                                TimeUnit.DAYS);
                        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(CacheUtil.buildKey(String.valueOf(each.getTrainId()), each.getDeparture(), each.getArrival()), JSON.toJSONString(result));
                    }
                    stringRedisTemplate.opsForHash().putAll(buildRegionTrainStationHashKey, regionTrainStationAllMap);
                }
            } finally {
                lock.unlock();
            }
        }

查询出来列车基本信息后,开始对列车按照出发时间进行排序。

seatResults = CollUtil.isEmpty(seatResults)
                ? regionTrainStationAllMap.values().stream().map(each -> JSON.parseObject(each.toString(), TicketListDTO.class)).toList()
                : seatResults;
seatResults = seatResults.stream().sorted(new TimeStringComparator()).toList();

查询列车余票信息

列车基本信息已经全部填充完毕了,接下来就是查询列车余票信息并填充到基本信息中。

列车余票数据是实时变更的,如果在存储到基本信息中,就没办法变更了,所以单独存储。

for (TicketListDTO each : seatResults) {
            String trainStationPriceStr = distributedCache.safeGet(
                    String.format(TRAIN_STATION_PRICE, each.getTrainId(), each.getDeparture(), each.getArrival()),
                    String.class,
                    () -> {
                        LambdaQueryWrapper<TrainStationPriceDO> trainStationPriceQueryWrapper = Wrappers.lambdaQuery(TrainStationPriceDO.class)
                                .eq(TrainStationPriceDO::getDeparture, each.getDeparture())
                                .eq(TrainStationPriceDO::getArrival, each.getArrival())
                                .eq(TrainStationPriceDO::getTrainId, each.getTrainId());
                        return JSON.toJSONString(trainStationPriceMapper.selectList(trainStationPriceQueryWrapper));
                    },
                    ADVANCE_TICKET_DAY,
                    TimeUnit.DAYS
            );
            List<TrainStationPriceDO> trainStationPriceDOList = JSON.parseArray(trainStationPriceStr, TrainStationPriceDO.class);
            List<SeatClassDTO> seatClassList = new ArrayList<>();
            trainStationPriceDOList.forEach(item -> {
                String seatType = String.valueOf(item.getSeatType());
                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)
                        .orElseGet(() -> {
                            Map<String, String> seatMarginMap = seatMarginCacheLoader.load(String.valueOf(each.getTrainId()), seatType, item.getDeparture(), item.getArrival());
                            return Optional.ofNullable(seatMarginMap.get(String.valueOf(item.getSeatType()))).map(Integer::parseInt).orElse(0);
                        });
                seatClassList.add(new SeatClassDTO(item.getSeatType(), quantity, new BigDecimal(item.getPrice()).divide(new BigDecimal("100"), 1, RoundingMode.HALF_UP), false));
            });
            each.setSeatClassList(seatClassList);
        }

构建列车返回数据

查看 12306 列车查询页可知,会存在不同的查询条件,这些查询条件都是通过本次查询所有列车数据构建出来的。不同地区的不同查询列车数据,车次类型、出发车站、到达车站以及车次席别都不同。

接下来就是通过构建者模式构建列车查询返回数据。

return TicketPageQueryRespDTO.builder()
                .trainList(seatResults)
                .departureStationList(buildDepartureStationList(seatResults))
                .arrivalStationList(buildArrivalStationList(seatResults))
                .trainBrandList(buildTrainBrandList(seatResults))
                .seatClassTypeList(buildSeatClassList(seatResults))
                .build();

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/408934.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

电商评价分析:NLP信息抽取技术在用户评论中的应用与挖掘

一、引言 在2019年&#xff0c;电子商务的蓬勃发展不仅推动了消费市场的增长&#xff0c;也带来了海量的用户评价数据。这些数据&#xff0c;作为消费者对商品和服务直接反馈的载体&#xff0c;蕴含着巨大的价值。然而&#xff0c;由于其非结构化的特性&#xff0c;这些文本信息…

蓝桥云课-第6场小白赛

第一题&#xff1a;输出字符串即可 第二题2.猜灯谜【算法赛】 - 蓝桥云课 (lanqiao.cn)&#xff1a; 猜灯谜 也就是输入自己前后值的总和&#xff0c;除去第一个和最后一个&#xff08;单独输出&#xff09; c代码如下&#xff1a; #include <iostream> using namesp…

DBeaver一段时间不使用,就会自动断开连接,需要刷新数据库或者断开重连解决方案 DB2

DBeaver一段时间不使用&#xff0c;就会自动断开连接&#xff0c;需要刷新数据库或者断开重连解决方案 DB2

【程序员必备技能】Git入门

目录 &#x1f308;前言&#x1f308; &#x1f4c1; Git的概念 &#x1f4c2; 版本控制 &#x1f4c2; 集中式 和 分布式 ​ &#x1f4c1; 创建和配置本地仓库 &#x1f4c1; 理解工作区&#xff0c;暂存区&#xff0c;版本库 &#x1f4c1; Git的基本操作 &#x1f4c2;…

人工智能绘画的时代下到底是谁在主导,是人类的想象力,还是AI的创造力?

#ai作画 目录 一.AI绘画的概念 1. 数据集准备&#xff1a; 2. 模型训练&#xff1a; 3. 生成绘画&#xff1a; 二.AI绘画的应用领域 三.AI绘画的发展 四.AI绘画背后的技术剖析 1.AI绘画的底层原理 2.主流模型的发展趋势 2.1VAE — 伊始之门 2.2GAN 2.2.1GAN相较于…

MES管理系统生产过程控制的核心要素

MES&#xff08;制造执行系统&#xff09;是为优化制造业生产过程和管理而设计的软件系统&#xff0c;其核心要素包括&#xff1a; 工单管理&#xff1a;工单管理是MES系统最基本的功能之一&#xff0c;它可以跟踪和管理各种类型的工单&#xff0c;如生产工单、维修工单和质量…

快速排序C语言实现程序

快速排序 快速排序算法一种最常见的排序算法&#xff0c;其核心思想就是 分治 &#xff0c;具体的&#xff1a;1&#xff09; 选定一个基准数&#xff1b;2&#xff09; 分区&#xff0c;将所有大于基准数的数据分为一区&#xff0c;将所有小于等于基准数的数据分为一区&#x…

自定义神经网络四之编写自定义神经网络

文章目录 前言神经网络组件代码整体的项目结构Tensor张量Layers层NeuralNet神经网络Loss损失函数Optim优化器data数据处理train训练 神经网络解决实际问题实际问题训练和推理代码 总结 前言 自定义神经网络一之Tensor和神经网络 自定义神经网络二之模型训练推理 自定义神经网络…

osg qt5.15 osg3.6.3 osgEarth3.1 编译爬山

Demo演示&#xff1a;Qt5.15.2OSG3.6.3OsgEarth3.1的QtCreator下的msvc2019x64版本 osgQt编译 步骤一&#xff1a;下载解压 步骤二&#xff1a;CMake配置 步骤三&#xff1a;CMake配置添加osg环境 步骤四&#xff1a;CMake配置添加Qt环境 步骤五&#xff1a;CMake修改CMakeLis…

微信小程序uniapp劳务咨询系统知识百科考试系统java+python+nodejs+php均支持

使用劳务咨询服务平台小程序的分别管理员和用户二个权限子模块。 管理员所能使用的功能主要有&#xff1a;首页、个人中心、用户管理、百科分类管理、知识百科管理、地区信息管理、劳务需求管理、试卷管理、试题管理、论坛交流、系统管理、考试管理等。 用户用户端可以实现首页…

Pytorch 复习总结 3

Pytorch 复习总结&#xff0c;仅供笔者使用&#xff0c;参考教材&#xff1a; 《动手学深度学习》Stanford University: Practical Machine Learning 本文主要内容为&#xff1a;Pytorch 多层感知机。 本文先介绍了多层感知机的用法&#xff0c;再就训练过程中经常出现的过拟…

2024.2.23 模拟实现 RabbitMQ —— 实现消费消息逻辑

目录 引言 函数式接口 消费者订阅消息 实现思路 关于消息确认 引言 函数式接口 Lambda 表达式的本质是匿名函数Java 函数无法脱离类而存在&#xff0c;所以 Java 通过引入函数式接口以支持 Lambda 表达式 特性&#xff1a; 函数式接口为一个 interface 类该类中有且仅有一个…

【Python笔记-设计模式】代理模式

一、说明 代理模式是一种结构型设计模式&#xff0c;提供对象的替代品或其占位符。代理控制着对于原对象的访问&#xff0c;并允许在将请求提交给对象前后进行一些处理。 (一) 解决问题 控制对对象的访问&#xff0c;或在访问对象前增加额外的功能或控制访问 (二) 使用场景…

统信UOS系统窗口特效设置

原文链接&#xff1a;统信UOS系统设置窗口特效 在今天的技术分享中&#xff0c;我们将探讨如何在统信UOS系统上充分利用窗口特效来美化和提升用户界面的交互体验。统信UOS作为一款注重视觉体验和用户友好性的操作系统&#xff0c;提供了丰富的窗口特效设置&#xff0c;让用户可…

R语言入门笔记2.6

描述统计 分类数据与顺序数据的图表展示 为了下面代码便于看出颜色参数所对应的值&#xff0c;在这里先集中介绍&#xff0c; col1是黑色&#xff0c;2是粉红&#xff0c;3是绿色&#xff0c;4是天蓝&#xff0c;5是浅蓝&#xff0c;6是紫红&#xff0c;7是黄色&#xff0c;…

Go 利用上下文进行并发计算

关注公众号【爱发白日梦的后端】分享技术干货、读书笔记、开源项目、实战经验、高效开发工具等&#xff0c;您的关注将是我的更新动力&#xff01; 在Go编程中&#xff0c;上下文&#xff08;context&#xff09;是一个非常重要的概念&#xff0c;它包含了与请求相关的信息&…

Bluejay电调固件修改自检音乐、自定义启动音乐旋律

Bluejay电调固件修改自检音乐、自定义启动音乐旋律 Bluejay电调固件基本介绍Bluejay电调固件特点修改自检音乐、启动音乐旋律准备材料修改过程 Bluejay固件旋律音乐格式开头部分音符部分 收集到的音乐代码 Bluejay电调固件基本介绍 Bluejay是一种数字电调固件&#xff0c;用于控…

Stable Diffusion 3 发布及其重大改进

1. 引言 就在 OpenAI 发布可以生成令人瞠目的视频的 Sora 和谷歌披露支持多达 150 万个Token上下文的 Gemini 1.5 的几天后&#xff0c;Stability AI 最近展示了 Stable Diffusion 3 的预览版。 闲话少说&#xff0c;我们快来看看吧&#xff01; 2. 什么是Stable Diffusion…

运维SRE-08 网络基础与进阶

今日内容 - **定时备份案例进阶.** - **定时巡检(检查系统基础指标),写入到文件中.** - 网络(抽象) 掌握与吸收时间: 直到课程结束.(第2阶段结束) - 网络基础: 网络概述,网络结构,网络设备. - 网络核心: OSI7层模型 ※※※※※※TCP/IP 3次握手 ※※※※※※TCP/IP 4…

Django入门指南:从环境搭建到模型管理系统的完整教程

环境安装&#xff1a; ​ 由于我的C的Anaconda 是安装在C盘的&#xff0c;但是没内存了&#xff0c;所有我将环境转在e盘&#xff0c;下面的命令是创建环境到指定目录中. conda create --prefixE:\envs\dj42 python3.9进入环境中&#xff1a; conda activate E:\envs\dj42…