免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

SpringBoot+Vue前后端分离实战:火车票订票系统全栈开发与并发处理

SpringBoot+Vue前后端分离实战:火车票订票系统全栈开发与并发处理 简介这是一套面向计算机专业本科生毕业设计的SpringBootVue火车票订票系统完整实现聚焦Web全栈开发实战解决传统购票流程线上化、交互体验优化与前后端协同开发等核心问题。资源包共1197个文件涵盖85个Java后端业务类如CheciController、YonghuController、44个Vue前端组件、297个JS逻辑脚本、97个CSS样式文件及158个GIF动效资源配合SQL建表语句与配置文件yml/xml完整呈现模块化分层架构与RESTful接口对接细节压缩包大小38.24MB。已有45人学习下载。读者可直接获取可运行的前后端分离工程包含用户认证、车次查询、座位选择、微信/支付宝模拟支付、订单管理等全流程功能代码以及Dictionary字典服务、MPUtil工具类等实用封装便于理解企业级订票系统的设计逻辑、安全机制与数据库索引优化实践。 最近在整理做过的项目翻到之前写的那个火车票订票系统前后端分离的经典组合SpringBoot负责提供接口Vue负责页面交互。这个项目对刚入门前端分离开发的同学来说挺有参考价值的它不算复杂但把全栈开发里的常见问题都涉及了——接口鉴权、跨域、并发扣库存、异步任务、前端路由守卫这些踩一圈坑下来整个知识体系会清晰不少。这篇就把当时的设计思路、核心实现和排坑过程完整梳理一遍。1. 项目整体设计与技术选型思路1.1 业务需求与核心模块拆解先说业务。火车票订票系统说白了就是两个角色普通用户和后台管理员。用户能注册登录、查车次、看余票、下单、支付、退票管理员能维护车次信息、查看订单列表、统计基础数据。听起来不复杂但真正落地的时候有几个模块是绕不开的坎车次查询按出发城市、到达城市、出发日期三个条件组合搜索车次还要展示经停站、历时、座位等级和余票数量。用户与鉴权注册、登录、身份校验这个项目里我用了JWT做无状态鉴权不用session后面扩展成微服务也好迁移。订单与余票选车次、选座位、填乘客、生成订单、支付。这里最要命的是并发场景——同一个车次同一趟日期的票就那么多多人同时抢票不能超卖。后台管理车次CRUD、订单查询、简单统计图表。当时定下来的项目名就叫train-ticket-system整个代码库分成前端train-web和后端train-server两个子目录独立开发、独立部署前端跑在Nginx上后端打成jar包跑在服务器上中间纯走HTTP接口对接。1.2 为什么选SpringBootVue这套组合早几年大家的方案还是JSPServletjQuery那套页面和服务端混在一起改个样式都要重启服务器。前后端分离之后前端团队和后端团队可以并行开发后端专注接口和业务逻辑前端专注交互和体验效率高很多。技术选型上SpringBoot基本是当下Java后端的事实标准。它内置了Tomcat不用再打war包丢到外部容器里spring-boot-starter-web一行依赖就把Web能力带进来了。配合MyBatis-Plus操作数据库、Redis做缓存开发速度非常快。前端选了Vue原因很直接Vue的上手曲线比React平缓模板语法贴近HTML对从jQuery时代过来的开发者很友好。而且Element UI这套组件库已经把表格、表单、弹窗、分页全都封装好了做后台管理类型的界面几乎是开箱即用。如果是个人开发或者小团队交付Vue配Element UI比React配Ant Design效率更高。1.3 数据库表设计与核心字段数据表设计直接影响后面业务逻辑好不好写。我一开始图省事把车次和余票塞到一张表里结果写到余票扣减的时候就发现不对同一趟车不同日期、不同座位等级的余票数完全不一样于是老老实实拆表。-- 用户表 CREATE TABLE user ( id bigint NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL, password varchar(100) NOT NULL, real_name varchar(50) DEFAULT NULL, id_card varchar(30) DEFAULT NULL, phone varchar(20) DEFAULT NULL, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 车次表 CREATE TABLE train ( id bigint NOT NULL AUTO_INCREMENT, train_no varchar(20) NOT NULL COMMENT 车次号如G1024, train_type varchar(10) DEFAULT 高铁, start_station varchar(50) NOT NULL, end_station varchar(50) NOT NULL, start_time varchar(10) NOT NULL COMMENT 出发时间 HH:mm, end_time varchar(10) NOT NULL COMMENT 到达时间 HH:mm, duration int DEFAULT NULL COMMENT 历时分钟, mileage int DEFAULT NULL COMMENT 里程公里, PRIMARY KEY (id), UNIQUE KEY uk_train_no (train_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 余票表车次日期座位等级 对应一个余票数 CREATE TABLE train_stock ( id bigint NOT NULL AUTO_INCREMENT, train_id bigint NOT NULL, travel_date date NOT NULL, seat_type varchar(20) NOT NULL COMMENT 商务座/一等座/二等座/硬卧/硬座, stock int NOT NULL DEFAULT 0, price decimal(10,2) NOT NULL, PRIMARY KEY (id), UNIQUE KEY uk_train_date_seat (train_id, travel_date, seat_type) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单表 CREATE TABLE orders ( id bigint NOT NULL AUTO_INCREMENT, order_no varchar(40) NOT NULL, user_id bigint NOT NULL, train_id bigint NOT NULL, travel_date date NOT NULL, seat_type varchar(20) NOT NULL, passenger_name varchar(50) NOT NULL, passenger_id_card varchar(30) DEFAULT NULL, price decimal(10,2) NOT NULL, status tinyint NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2已出票 3已退票 4已取消, create_time datetime DEFAULT CURRENT_TIMESTAMP, pay_time datetime DEFAULT NULL, PRIMARY KEY (id), KEY idx_user_id (user_id), KEY idx_train_date (train_id, travel_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;余票单独拆一张表之后整个架构就清晰了查询走train表和train_stock连表下单扣减时只更新train_stock一行数据配合行锁或乐观锁就能解决并发问题。这个设计思路在后面第2.4节详细说。2. 后端SpringBoot核心实现2.1 项目初始化与依赖配置IDEA新建一个SpringBoot项目Group填com.exampleArtifact填train-serverJDK版本建议选JDK8或JDK11虽然SpringBoot 3.x已经出来了但国内大量企业的中间件和运维体系还是以JDK8为主2.7.x版本足够稳定坑也少。依赖这里直接引入这几个起步依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdcom.auth0/groupId artifactIdjava-jwt/artifactId version4.4.0/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency配置文件application.yml里几个容易踩坑的点说一下server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/train_ticket?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: root1234 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImplmap-underscore-to-camel-case: true这个配置必须开否则数据库字段create_time映射不到Java属性createTime上。MyBatis-Plus默认是开启的但如果你手写XML里的ResultMap还是要自己检查一下映射关系。2.2 用户登录鉴权JWT方案落地登录鉴权我没有用Session而是用JWT。对于前后端分离项目来说Session跨域麻烦服务端要存状态而JWT把用户信息加密放在令牌里服务端不保存任何会话天然的适合分布式扩展。实现逻辑是用户登录校验用户名密码。密码不能明文存数据库用BCrypt加密。校验通过后生成JWT把userId和username放进token的payload。前端拿到token之后每次请求放在Authorization请求头里。后端写一个拦截器HandlerInterceptor拦截需要登录的接口校验token有效性。生成token的工具类核心代码public class JwtUtils { private static final String SECRET your-secret-key-please-change; private static final long EXPIRE_TIME 24 * 60 * 60 * 1000L; public static String generateToken(Long userId, String username) { Algorithm algorithm Algorithm.HMAC256(SECRET); return JWT.create() .withClaim(userId, userId) .withClaim(username, username) .withExpiresAt(new Date(System.currentTimeMillis() EXPIRE_TIME)) .sign(algorithm); } public static Long getUserId(String token) { DecodedJWT jwt JWT.require(Algorithm.HMAC256(SECRET)).build().verify(token); return jwt.getClaim(userId).asLong(); } }拦截器校验token的写法我没用Spring Security那个体系太重了做一个简单的拦截器加上WebMvcConfigurer注册就够了。校验失败返回401状态码前端Axios拦截器统一捕获跳回登录页。注意JWT的密钥一定不要硬编码在代码里实际项目放到配置中心或者环境变量里。这里写成常量是为了演示方便项目真实上线的时候如果密钥泄露任何人都能伪造token。2.3 车次查询与余票信息组装车次查询是核心查询接口前端页面长这样输入出发城市、到达城市、出发日期点击查询列表展示符合条件的车次。SQL在MySQL里写好之后配合分页插件就能查了。Override public IPageTrainVO queryTrains(String startStation, String endStation, String date, int page, int size) { PageTrainVO pageParam new Page(page, size); return trainMapper.selectTrainWithStock(pageParam, startStation, endStation, date); }Mapper里用一个自定义SQLselect idselectTrainWithStock resultTypecom.example.train.vo.TrainVO SELECT t.id, t.train_no, t.train_type, t.start_station, t.end_station, t.start_time, t.end_time, t.duration, t.mileage, ts.seat_type, ts.stock, ts.price FROM train t JOIN train_stock ts ON t.id ts.train_id WHERE t.start_station #{startStation} AND t.end_station #{endStation} AND ts.travel_date #{date} ORDER BY t.start_time /select这里有个设计细节余票是按train_id travel_date seat_type维度拆开的所以JOIN之后一趟车会返回多行一行一种座位等级。前端拿到这个数据之后按train_id分组把不同座位等级和余票放在同一个车次节点下面这样渲染车次列表就很方便了。查询性能方面如果数据量大了train表的start_station和end_station建议建联合索引train_stock表的train_id travel_date本身就是唯一索引查询不会慢。2.4 下单与并发扣减库存从行锁到Redis预减下单是这个系统最容易出bug的地方。用户选好车次、日期、座位等级点击下单后端要做的事校验用户登录状态拦截器已做。生成订单号。校验余票是否足够。扣减余票。插入订单记录。如果直接把校验和扣减写成两步操作并发请求就会出问题。两个请求同时查到余票1都通过了校验都去扣减结果库存变成-1超卖了。最简单的解决方式是用数据库的原子更新来扣减库存Update(UPDATE train_stock SET stock stock - 1 WHERE train_id #{trainId} AND travel_date #{date} AND seat_type #{seatType} AND stock 0) int deductStock(Long trainId, LocalDate date, String seatType);关键点在AND stock 0——这条update语句自带行锁并且只有库存大于0时才会更新成功返回的影响行数int如果等于1说明扣减成功等于0说明库存不足直接返回“票已售罄”。这是最简单可靠的防超卖写法不需要显式加SELECT FOR UPDATE一条SQL同时完成了校验和扣减。如果压力再大一点就要引入Redis缓存余票数量做预扣减再用MQ异步通知数据库落减库存。但对于刚入门的项目来说数据库行锁方案完全够用而且逻辑清晰、不会被Redis和DB数据不一致问题烦扰。我当时的做法是先保证正确性后面压测发现瓶颈了再上Redis。下单之后还有一个场景用户下单了但一直不支付票就占着其他人买不到。常规做法是订单超时未支付自动取消。我用了最简单的定时任务方案写一个Scheduled方法每30秒扫描一次超过15分钟未支付的订单把订单状态改为取消同时把余票加回来。用Redis的expire事件做延迟队列方案更优雅但需要额外引入Redis监听配置复杂度和收益要权衡。3. 前端Vue核心实现3.1 项目搭建与环境配置用Vue CLI创建前端项目npm install -g vue/cli vue create train-web创建的时候选上Router和Vuex或者PiniaCSS预处理选SCSS。Vue CLI版本建议用4.x或5.xNode版本建议14以上。如果网络条件不好npm安装依赖超时优先配置淘宝镜像源npm config set registry https://registry.npmmirror.com然后再安装项目依赖npm install element-ui axiosElement UI按需引入的话还要配babel-plugin-component但为了省事我当时直接全量引入了——开发阶段体验最重要等体积出问题再按需优化也不迟。3.2 路由设计与登录守卫前端路由用Vue Router管理核心页面规划const routes [ { path: /, component: HomeView, meta: { title: 首页 } }, { path: /login, component: LoginView, meta: { title: 登录 } }, { path: /train/search, component: TrainSearchView, meta: { title: 车次查询, requiresAuth: true } }, { path: /order/list, component: OrderListView, meta: { title: 我的订单, requiresAuth: true } }, { path: /admin/train, component: AdminTrainView, meta: { title: 车次管理, requiresAuth: true, roles: [ADMIN] } } ];requiresAuth标记这个页面需要登录才能访问配合全局前置守卫router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.meta.requiresAuth !token) { next({ path: /login, query: { redirect: to.fullPath } }); } else { next(); } });这样用户未登录的时候访问/order/list会被自动踢到登录页登录完成之后带着redirect参数跳回原来的页面体验还行。管理员权限的角色校验也可以放在守卫里做控制哪些人能看到后台管理的菜单和路由。3.3 Axios封装与跨域问题Axios请求封装这块必须做好不然每个页面都要写重复的请求逻辑。我当时封装了一个request.jsimport axios from axios; import { Message } from element-ui; import router from /router; const request axios.create({ baseURL: /api, timeout: 10000 }); // 请求拦截器自动携带token request.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers[Authorization] Bearer token; } return config; }); // 响应拦截器统一处理错误 request.interceptors.response.use( response { const res response.data; if (res.code ! 200) { Message.error(res.message || 请求失败); return Promise.reject(new Error(res.message)); } return res.data; }, error { if (error.response error.response.status 401) { localStorage.removeItem(token); router.push(/login); } else { Message.error(error.message || 网络异常); } return Promise.reject(error); } ); export default request;这里配置的baseURL: /api是开发环境的代理入口解决跨域问题。跨域是整个前后端联调阶段最容易卡住新人的点。后端接口跑在8080前端开发服务器跑在8081浏览器直接访问必然跨域。当时我用的是Vue CLI自带的proxy代理在vue.config.js里配置module.exports { devServer: { port: 8081, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, pathRewrite: { ^/api: } } } } };这样前端请求/api/train/query开发服务器会把它代理到http://localhost:8080/train/query浏览器看到的始终是同源请求跨域问题就这么悄无声息地解决了。后端的CORS配置对于生产环境还有用因为生产环境可能是通过Nginx不同域名访问的。3.4 核心页面车次查询与下单流程车次查询页面我用了几行核心结构搜索表单出发地、到达地、日期、车次列表表格、选中车次后的下单弹窗。车次列表展示的是一个表格车次号、出发到达站、出发到达时间、历时、不同座位等级。座位等级那一列我用标签来展示不同票价并且根据余票数量控制是否可点击el-table :datatrainList stripe el-table-column proptrainNo label车次 width100 / el-table-column label出发/到达 width180 template slot-scopescope {{ scope.row.startStation }} → {{ scope.row.endStation }} /template /el-table-column el-table-column label时间 width200 template slot-scopescope {{ scope.row.startTime }} - {{ scope.row.endTime }} /template /el-table-column el-table-column label座位/票价 min-width220 template slot-scopescope el-tag v-forseat in scope.row.seatList :keyseat.seatType :typeseat.stock 0 ? success : info classseat-tag {{ seat.seatType }} {{ seat.price }}元 span v-ifseat.stock 0无票/span /el-tag /template /el-table-column el-table-column label操作 width100 template slot-scopescope el-button typeprimary sizesmall clickopenOrderDialog(scope.row)预订/el-button /template /el-table-column /el-table下单弹窗里会让用户选座位等级、填乘客姓名和身份证号提交后调下单接口。这里我在前端做了一次拦截如果该车次的某个座位等级余票为0这个座位等级的下单按钮置灰减少无效请求打到后端。整个流程走下来用户的体验链路是搜索结果 → 点预订 → 弹窗确认 → 支付跳转这里用了模拟支付页面 → 订单列表查看出票状态。麻雀虽小五脏俱全。4. 前后端联调与部署落地4.1 联调阶段的常见问题本地联调是前后端分离项目最痛苦的阶段我遇到的真实问题接口字段命名不一致后端返回trainNo前端写成了train_no列表全是空。后来统一规范后端VO层全部转成驼峰命名前端只认驼峰排查问题的成本就低多了。LocalDateTime序列化问题SQL查出来的是时间格式但SpringBoot返回给前端的是数组或者一串数字这就是没有配置jackson的date-format导致的。我在application.yml里加了两行配置就解决了spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai404和405傻傻分不清前端调接口返回404第一反应是路径写错了其实很可能是方法不对。GET请求写成了POSTSpringMVC直接拒绝。排查方法很简单看控制台的请求日志或者用Postman先测一遍后端接口确认后端没问题再查前端。联调阶段我一直坚持一个原则先用Postman把后端所有接口跑通再让前端对接。这样可以最大限度隔离前后端问题。后端接口如果自己都没用Postman测过前端对接的时候会遇到一堆低级错误两边还互相甩锅最后浪费时间。4.2 Docker部署后端后端部署我选了Docker好处是环境一致性强换服务器不用重新折腾JDK和Maven环境。Dockerfile写得很简单FROM openjdk:8-jre-alpine WORKDIR /app COPY target/train-server.jar app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, app.jar, --spring.profiles.activeprod]如果云服务器内存小1G以下的实例JVM参数需要调整一下ENTRYPOINT [java, -Xmx256m, -Xms128m, -jar, app.jar, --spring.profiles.activeprod]否则默认JVM会申请机器内存的四分之一小内存服务器很容易卡死。这个坑等服务器上的应用莫名其妙挂掉之后再发现就晚了所以提前配好。构建和启动命令mvn clean package -DskipTests docker build -t train-server:1.0 . docker run -d --name train-server -p 8080:8080 \ -e SPRING_DATASOURCE_URLjdbc:mysql://宿主机IP:3306/train_ticket?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai \ -e SPRING_DATASOURCE_USERNAMEroot \ -e SPRING_DATASOURCE_PASSWORDroot1234 \ train-server:1.0数据库连接串不要写死在镜像里用环境变量注入不然换环境就要重新build镜像。4.3 Nginx部署前端与反向代理前端打包之后是纯静态资源npm run build生成dist目录然后丢到服务器的/usr/share/nginx/html下。Nginx配置文件是重点既要处理静态资源又要反向代理API请求到后端服务还要解决前端路由的history模式刷新404问题server { listen 80; server_name your-domain.com; # 前端静态资源 location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } # API反向代理 location /api/ { proxy_pass http://localhost:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # 静态资源缓存 location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { root /usr/share/nginx/html; expires 7d; add_header Cache-Control public, max-age604800; } }try_files $uri $uri/ /index.html;这一行至关重要。Vue Router如果用的是history模式直接访问/order/list这个URL时Nginx在磁盘上找不到这个文件就会返回404。加上try_files之后所有找不到的文件都会回退到index.html让前端路由接管。proxy_pass http://localhost:8080/;末尾的斜杠也不能少少了会把/api原样带到后端路径里后端接口定义没有/api前缀的话就会匹配不上。这就是反向代理里最常见的路径重写坑。4.4 全流程验证与性能优化部署完成之后整个系统的请求链路是浏览器 → Nginx(80) → / 静态资源Vue打包产物 → /api/ → proxy_pass → SpringBoot(8080) → MySQL我建议做完部署之后用脚本做一个全链路冒烟测试注册 → 登录 → 查车次 → 下单 → 支付 → 查看订单这6个核心操作全部走一遍任何一个环节断了都能立刻发现。性能优化方面这个阶段我只做了两件事第一件是MyBatis-Plus的SQL日志关闭。开发环境开着StdOutImpl日志方便调试生产环境要把它关掉否则每次请求都把SQL打到日志里一天下来日志文件几个G磁盘直接被打满。第二件是车次查询加Redis缓存。热门线路北京到上海、广州到深圳这种的查询频率很高但车次和余票数据几分钟才变一次。把查询结果缓存到Rediskey设计成train:query:{start}:{end}:{date}缓存5分钟能挡住80%的重复请求。不过缓存更新时机要想清楚——下单成功之后要主动删掉相关key不然刚卖完票用户还能在缓存里看到有票。5. 常见问题与排查技巧实录5.1 高频问题速查表把这些年用SpringBootVue做项目经常遇到的问题整理一下写成一个速查表遇到类似问题直接对照排查问题现象可能原因解决方案前端请求接口404Nginx代理路径没配对/api前缀被带到后端检查proxy_pass末尾斜杠后端加ServerContextPath统一前缀前端请求接口跨域开发环境代理没开生产环境域名不一致开发环境配proxy生产环境用Nginx同域反代后端返回时间是数组/数字Jackson没有配置date-formatapplication.yml里配置spring.jackson.date-format数据库报字段不存在create_time下划线转驼峰没开开启map-underscore-to-camel-case: true并发下单超卖校验和扣减非原子操作改用UPDATE ... WHERE stock 0原子扣减报表页面刷新404Vue Router history模式没有配置try_filesNginx加try_files $uri $uri/ /index.html打包后前端资源路径不对baseURL写死了开发地址配置publicPath: ./或者用Nginx反向代理服务器OOM进程被杀JVM默认堆内存太大启动参数加-Xmx256m限制堆内存文件上传报错413Nginx默认请求体大小限制1MB配置client_max_body_size 10m接口返回慢但SQL快可能是循环查数据库产生N1问题用MyBatis-Plus连表查询或批量查询5.2 几个印象深刻的坑第一个坑是SpringBoot版本和JDK版本不匹配。当时Idea创建项目的时候默认选了SpringBoot 3.4.3然后一直报错启动不了。查了半天发现SpringBoot 3.x要求JDK17而我本机是JDK8。后来直接把pom里SpringBoot版本改成2.7.18一切恢复正常。所以新项目如果用的是JDK8不要一上来就选SpringBoot 3.x版本对应不上会让人怀疑人生。这里的判断标准很简单JDK8配SpringBoot 2.7.xJDK17以上才用SpringBoot 3.x。第二个坑是IDEA新建项目没有SpringBoot 3.4.3选项。这个其实是Spring Initializr服务端的版本列表在更新本地缓存的版本信息过期了。解决办法是在Settings里把Spring Initializr的URL刷新一下或者直接改成https://start.spring.io重新拉取。如果网络连不上就直接用Maven搭建项目手动往pom里加SpringBoot依赖也不是什么难事。第三个坑是前端npm安装依赖特别慢。国内环境下npm install能跑10分钟起步而且动不动报ERR_NETWORK。这个没什么好说的就是要用镜像源。踩过坑的人都知道一个registry配置就能把安装时间从10分钟降到30秒。第四个坑是Element UI的表格嵌套数据回显。订单列表里的乘客信息、车次信息都是通过接口一次返回的对象表格里拿scope.row对象再点属性第一次写总是忘了一层嵌套关系控制台报undefined错误。后来我在VO里把嵌套对象都拍平了比如passengerName、trainNo这些字段直接平铺在订单VO里简单粗暴省得前端各种判断。最后再分享一个实操心得整个项目从零做下来最大的收获不是学会了某个框架的API而是理解了系统设计里“拆”和“合”的平衡。数据库表拆到多细、接口拆到多小、页面组件拆到什么粒度这些决策直接影响项目的维护成本。比如余票表单独拆出来订单逻辑和车次管理逻辑就能互不干扰但如果你把所有功能都塞到一张大表和一个大Controller里面后面改需求就是一场灾难。另外我想强调一个经常被忽略的点日志和错误信息设计。后端接口的返回值一定要规范我当时统一的格式是{code: 200, message: 操作成功, data: {...}}前端Axios拦截器统一处理。这样不管是开发还是运维看到返回码就能快速定位问题比在代码里一遍遍debug要高效得多。这个项目后续如果要扩展可以考虑的方向接入真实支付网关支付宝/微信、增加改签和候补购票功能、把定时任务改造成消息队列驱动、前端引入ECharts做更丰富的报表统计。技术栈本身没有瓶颈重点是业务逻辑的复杂度会跟着涨到时候再根据实际需求做取舍就好。本文还有配套的精品资源点击获取
返回列表