仿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> {@Overridepublic 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("出发日期不能为空");}}@Overridepublic 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;@Overridepublic 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();}}@Overridepublic 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();

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

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

带使能控制的锂电池充放电解决方案

一、产品概述 TP4594R 是一款集成线性充电管理、同步升压转换、电池电量指示和多种保护功能的单芯片电源管理 SOC&#xff0c;为锂电池的充放电提供完整的单芯片电源解决方案。 TP4594R 内部集成了线性充电管理模块、同步升压放电管理模块、电量检测与 LED 指示模块、保护模块…

Rust升级慢,使用国内镜像进行加速

背景 rustup 是 Rust 官方的跨平台 Rust 安装工具&#xff0c;国内用户使用rustup update的时候&#xff0c;网速非常慢&#xff0c;可以使用国内的阿里云镜像源来进行加速 0x01 配置方法 1. Linux与Mac OS用户配置环境变量 修改~/.bash_profile文件添加如下内容&#xff1…

【服务发现--ingress】

1、ingress介绍 Ingress 提供从集群外部到集群内服务的 HTTP 和 HTTPS 路由。 流量路由由 Ingress 资源所定义的规则来控制。 Ingress 是对集群中服务的外部访问进行管理的 API 对象&#xff0c;典型的访问方式是 HTTP。 Ingress 可以提供负载均衡、SSL 终结和基于名称的虚拟…

if语句test

import com.sun.jdi.PathSearchingVirtualMachine;import java.sql.SQLOutput; import java.util.Scanner;public class Test5 {public static void main(String[] args) {//在电影院检查票据&#xff0c;票据在1-100之间才是真实有效的票据&#xff0c;且奇数做左边&#xff0…

2.25基础会计学

资本公积是指由股东投入、但不能构成“股本”或“实收资本”的资金部分。 盈余公积是指公司按照规定从净利润中提取的各种积累资金。 所以区别在于盈余公积来自净利润。 借贷其实就是钱从哪来和到哪去的问题&#xff0c;来源是贷&#xff0c;流向是借。比如购入9w原材料&…

Vue事件处理之v-on

1. 使用及定义 定义方法 function 方法名称(接受的event或是什么都不写) {//不管方法后括号内写与不写event,都可以接受到方法内表达式 }//定义一个接受参数的方法,此时也会在传入event function 方法名称(传入参数) {//可接受传入参数与event方法内表达式 } //定义一个接受参…

Cover和contain属性

一.背景的盒子 代码&#xff1a; <body><div class"box"></div><style>.box {width: 500px;height: 500px;border: 1px solid #ccc;background: url(./20191017095131790.png) no-repeat;}</style></body> 盒子的宽度和高度是…

软件游戏显示d3dx9_42.dll丢失的5种解决方法,快速解决dll问题

当计算机系统中d3dx9_42.dll文件丢失时&#xff0c;可能会引发一系列运行问题和功能异常&#xff0c;具体表现形式多样且影响范围较广。首先&#xff0c;对于依赖于DirectX 9.0c版本的各类应用程序&#xff0c;尤其是部分经典的老款游戏&#xff0c;由于d3dx9_42.dll是其中不可…

2024年危险化学品经营单位主要负责人证考试题库及危险化学品经营单位主要负责人试题解析

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 2024年危险化学品经营单位主要负责人证考试题库及危险化学品经营单位主要负责人试题解析是安全生产模拟考试一点通结合&#xff08;安监局&#xff09;特种作业人员操作证考试大纲和&#xff08;质检局&#xff09;特…

Sentinel 动态规则扩展

一、规则 Sentinel 的理念是开发者只需要关注资源的定义&#xff0c;当资源定义成功后可以动态增加各种流控降级规则。Sentinel 提供两种方式修改规则&#xff1a; 通过 API 直接修改 (loadRules)通过 DataSource 适配不同数据源修改 手动通过 API 修改比较直观&#xff0c;…

代码随想录算法训练营第三天

● 自己看到题目的第一想法 203.移除链表元素 方法一&#xff1a; 思路&#xff1a; 设置虚拟头节点 dummyhead 设置临时指针 cur 遍历 整个链表 循环&#xff1a; 如果 cur !nullptr &&cur->next !nullptr 则 遍历链表 否则结束遍历 如果 cur->next val 则…

CSS之包含块(contatining block)

可能了解包含块的人很少&#xff0c;但有这么个问题&#xff0c;大家可以看看。 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scal…

【架构】GPU架构总结

文章目录 GPU架构GPU渲染内存架构Streaming Multiprocessor(SM)CUDA CoreTensor CoreRT CoreCPU-GPU异构系统GPU资源管理模型 GPU架构演进G80 架构Fermi 架构Maxwell架构Tesla架构Pascal架构Volta 架构Turing架构Ampere 架构Hopper架构 参考文献 GPU架构 主要组成包括&#xf…

报错:org.springframework.jdbc.BadSqlGrammarException:

//报错 2024-02-24 19:44:10.814 ERROR 6184 --- [nio-9090-exec-5] c.e.exception.GlobalExceptionHandler : 异常信息&#xff1a; org.springframework.jdbc.BadSqlGrammarException: GPT&#xff1a; 根据异常信息&#xff0c;这是一个Spring框架抛出的BadSqlGrammar…

MATLAB:数组与矩阵

2.1 数组运算 数组运算时MATLAB计算的基础。由于MATLAB面向对象的特性&#xff0c;这种数值数组称为MATLAN最重要的一种内建数据类型&#xff0c;而数组运算就是定义这种数据结果的方法。 2.1.1 数组的创建和操作 在MATLAB中一般使用方括号“[]”、逗号“,”、空格和分号“;…

openssl3.2 - crypto-mdebug被弃用后, 内存泄漏检查的替代方法

文章目录 openssl3.2 - crypto-mdebug被弃用后, 内存泄漏检查的替代方法概述笔记查看特性列表openssl3.2编译脚本 - 加入enable-crypto-mdebug看看有没有替代内存诊断的方法?main.cppmy_openSSL_lib.hmy_openSSL_lib.c备注备注END openssl3.2 - crypto-mdebug被弃用后, 内存泄…

选座位 - 华为OD统一考试(C卷)

OD统一考试&#xff08;C卷&#xff09; 分值&#xff1a; 200分 题解&#xff1a; Java / Python / C 题目描述 疫情期间&#xff0c;需要大家保证一定的社交距离&#xff0c;公司组织开交流会议&#xff0c;座位有一排共N个座位&#xff0c;编号分别为[0…N-1]&#xff0c;要…

onnx 1.16 doc学习笔记一:ONNX概念

onnx作为一个通用格式&#xff0c;很少有中文教程&#xff0c;因此开一篇文章对onnx 1.16文档进行翻译与进一步解释&#xff0c; onnx 1.16官方文档&#xff1a;https://onnx.ai/onnx/intro/index.html](https://onnx.ai/onnx/intro/index.html)&#xff0c; 如果觉得有收获&am…

STL - 图

1、图的基本概念 图是由顶点集合及顶点间的关系组成的一种数据结构&#xff1a;G (V&#xff0c; E)&#xff0c;其中&#xff1a; 顶点集合 V {x|x属于某个数据对象集}是有穷非空集合&#xff1b; 边的集合 E {(x,y)|x,y属于V}或者E {<x&#xff0c;y>|x,y属于V …

牛客周赛 Round 33 解题报告 | 珂学家 | 思维场

前言 整体评价 感觉这场更偏思维&#xff0c;F题毫无思路&#xff0c;但是可以模拟骗点分, E题是dij最短路. A. 小红的单词整理 类型: 签到 w1,w2 input().split() print (w2) print (w1)B. 小红煮汤圆 思路: 模拟 可以从拆包的角度去构建模拟 注意拆一包&#xff0c;可以…