news 2026/9/26 16:50:00

SSM航班订票系统开发全解析:数据库设计与并发控制实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SSM航班订票系统开发全解析:数据库设计与并发控制实战

每年到这个时间点,我的私信里就会出现同一个问题:“航班订票系统用SSM怎么写?”或者更直白一点:“能不能给我一套完整能跑的SSM航班订票系统?”说实话,这个题目确实是高校课设和毕设里的常青树,几乎人手一份。但大多数人只关心代码能不能跑通、页面长什么样,很少有人认真想过:为什么这个“看起来简单”的项目,真正做到一半才发现处处是坑。

SSM不是“Spring+SpringMVC+MyBatis”三个框架堆在一起就完事,航班订票系统也不只是“查航班、下订单”的增删改查。我在实际带人做完整个项目之后发现,这个题目真正难住人的地方集中在几个特定环节:框架配置的整合顺序、数据库表结构的设计取舍、订票与余票扣减时的事务和并发控制,以及项目部署时的一堆环境问题。这篇文章就把我实际完成这个项目的全过程拆给你看,从选型逻辑到表结构,从核心Service代码到压测翻车现场,都会讲到。适合正在做SSM相关课设、毕业设计的人阅读,也适合想用这个项目练手、准备面试框架原理问题的同学参考。

1. SSM还能稳坐高校项目榜首的真实原因

1.1 SSM到底是什么,它们各自管哪块

很多人一上来就背框架名,但真被问到“这三个框架分别解决什么问题”就卡住了。我用最朴素的话说清楚:SSM是一个典型的分层Java Web开发架构,三个框架各管一个层次,组合起来就是一套完整的业务系统骨架。

Spring是核心容器,负责管理系统里所有的Java对象。比如你的UserService、OrderService这些类,传统方式是自己new,但在SSM里头,对象的创建和依赖注入全部交给Spring容器,你需要用的时候直接声明@Autowired让它把对象“递”给你。Spring还有一个重要能力是AOP(面向切面编程),后面会讲到的事务管理、日志记录,利用AOP可以在不修改业务代码的情况下统一加上这些横切逻辑。

SpringMVC管的是Web层,也就是你打开浏览器输入网址、点按钮之后,请求是怎么被接收和响应的。它的核心机制是从请求进来开始,先经过DispatcherServlet这个总入口,再由HandlerMapping找到对应的Controller方法,方法执行完返回ModelAndView,最后由视图解析器渲染成JSP页面,返回给前端。简单说就是“请求分发和视图控制”。

MyBatis管的是持久层,也就是数据库访问。它比传统的JDBC方便太多,你用Mapper接口定义一个方法,然后在XML文件里写SQL,MyBatis会自动完成参数绑定和结果集映射,把数据库记录转成Java对象。它最核心的思路是“把SQL写在XML里”,所以SQL和Java代码分离,维护起来很清爽。

这三个框架连在一起的请求链路是这样走的:浏览器发起请求,SpringMVC接收并找到Controller,Controller调用Service完成业务逻辑,Service调用Mapper操作数据库,数据返回后再一级一级回传,最终由SpringMVC渲染页面。用餐厅点菜来类比:SpringMVC是服务员(接单传菜),Service是厨房(真正炒菜),MyBatis是仓库管理员(取食材),而Spring是这家店的总调度,管着服务员、厨师、仓管之间的协作关系。

1.2 既然Spring Boot都出来了,为什么还要选SSM

这是我被问得最多的问题。每次我都会反问一句:如果你的目标是“最快速度做出来一个能用的系统”,那选Spring Boot加MyBatis-Plus肯定更省事,因为Spring Boot帮你把大量配置都自动完成了,甚至内嵌了Tomcat,点击启动就能跑。但如果你想要的是“理解Java Web框架到底在做什么”,SSM反而是更好的学习材料。

原因是这个Spring Boot替你省掉的东西,恰恰是SSM里需要你手动写配置的部分。你在SSM项目里要自己配置数据源、自己扫描Mapper、自己写事务管理配置文件、自己配置视图解析器。这些配置每一行你都得搞明白是干什么用的,否则起不来。当你在Spring Boot里用一行注解就搞定事务时,你不会知道背后接的是什么、默认回滚规则是什么;但在SSM里你亲手配过事务管理器,这种理解是长在脑子里的,面试时讲IoC和AOP都有真实案例可以支撑。

另外一个现实因素:很多学校的课程体系还停留在以SSM为主线,官方要求你用SSM完成毕业设计。同时企业里也存在大量历史遗留的SSM项目,你入职后可能还要维护它们。所以我不建议一概而论说“别学SSM了”,关键看你的目标:为了求职突击,直接上Spring Boot;为了应付课设并想真正弄懂框架原理,SSM是非常扎实的选择。航班订票系统这种业务复杂度适中、模块边界清晰的项目,正好把SSM的各个能力都用上,所以才会被一届一届选中。

2. 航班订票的需求边界:功能拆解与版本规划

2.1 先定需求边界,别一上来就想着做全套

做这个项目的人里,十个有八个是一开始把功能想得特别满:要有会员等级、积分兑换、短信通知、航班动态推送、在线选座、退改签,甚至还要接入真实支付接口。然后写着写着就烂尾了,连最基础的功能都没跑通。

我的建议是分版本推进。第一版(MVP版本)只做核心闭环:用户注册登录、航班条件查询、下单订票、模拟支付、取消订单、我的订单列表、后台航班信息管理。这个版本跑通后,整个项目的主干流程就完整了。第二版再考虑锦上添花的功能,比如乘机人管理、历史订单统计、图表报表等。你拿着第一版去交课设,已经超过大多数人的完成度了。

做需求拆解时,可以按“用户角色 + 功能模块”来划分。系统分成三个视角:游客、登录用户、管理员。游客能看首页和航班列表;登录用户能下单、支付、取消订单、查看自己的订单;管理员能维护航班的基础信息和余票初始值。这个划分画清楚之后,页面跳转关系也就出来了:打开首页->登录/注册->进入航班查询列表->点击预订->生成订单->确认支付->在“我的订单”里看到状态并操作。

我见过很多同学在没想清楚之前就直接写Controller,结果页面URL混乱、参数对不上,后面调试得想哭。强烈建议先把功能清单列成表格,尤其是关键路径上的动作和页面,这样不管写代码还是写文档都清晰。

2.2 功能清单与页面流转

下面是我最终确定下来的MVP版本功能表,供你参考:

  • 用户模块:注册、登录、退出登录;密码加密保存;登录状态用Session维护
  • 航班模块:按出发城市、到达城市、出发日期查询航班;展示班次、起飞到达时间、舱位价格、余票
  • 订票模块:选择航班和舱位,生成订单;同一航班余票扣减必须原子操作
  • 支付模块:模拟支付,点击后把订单状态从待支付改成已支付,记录支付时间
  • 订单模块:展示当前用户订单列表;待支付状态可取消;取消后余票回补
  • 后台管理:管理员登录后维护航班表,新增航班、修改时间、调整价格

页面流转主线是:index.jsp(首页/登录入口) -> flightList.jsp(航班查询结果) -> bookConfirm.jsp(订票确认) -> orderList.jsp(我的订单)。后台单独分一套admin_login.jsp和admin_flight.jsp。

2.3 非功能性需求:并发、安全、幂等

除了功能需求,做这个项目时必须提前考虑三个非功能问题,否则后面会踩大坑。

并发:一个航班的余票是固定的,但多个用户可能同时下单。如果你不做并发控制,就会出现“两个人同时买到最后一个座位”的超卖结果。这个问题在第四章我会专门讲方案。

安全:用户密码不能明文存数据库,至少要用MD5加盐,最好用BCrypt这种不可逆算法。登录接口要考虑SQL注入,MyBatis里尽量用#{}而不是${}去拼接参数。

幂等:用户不小心连续点两次“下单”按钮,要避免生成两个同样的订单。一个简单方案是前端在点击后立刻禁用按钮,后端再做一层校验,比如很短时间内同一用户、同一航班的未支付订单只允许存在一张。

3. 数据库设计:表结构是一切功能的底座

3.1 核心表结构与SQL脚本

这个项目我最终使用了四张核心表:用户表user、航班表flight、订单表orders。有些同学会再加一张乘机人表,MVP阶段可以先不做,把乘机人信息直接冗余在订单里,比如存一个passenger_name字段,后面要扩展再拆表。

用户表设计如下:

CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(32) NOT NULL COMMENT '登录名', `password` varchar(64) NOT NULL COMMENT '加密后的密码', `real_name` varchar(32) DEFAULT NULL COMMENT '真实姓名', `id_card` varchar(32) DEFAULT NULL COMMENT '身份证号', `phone` varchar(20) DEFAULT NULL COMMENT '手机号', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

航班表是核心,包含班次信息、出发到达城市与时间、三档舱位价格,以及关键的余票数字段:

CREATE TABLE `flight` ( `id` int(11) NOT NULL AUTO_INCREMENT, `flight_no` varchar(10) NOT NULL COMMENT '航班号', `departure_city` varchar(50) NOT NULL, `arrival_city` varchar(50) NOT NULL, `departure_time` datetime NOT NULL, `arrival_time` datetime NOT NULL, `seat_count` int(11) NOT NULL DEFAULT 180 COMMENT '总座位数', `remaining_seats` int(11) NOT NULL DEFAULT 180 COMMENT '剩余座位数', `price_ec` decimal(10,2) NOT NULL COMMENT '经济舱价格', `price_bc` decimal(10,2) NOT NULL COMMENT '商务舱价格', `price_fc` decimal(10,2) NOT NULL COMMENT '头等舱价格', `airline` varchar(50) DEFAULT NULL COMMENT '航空公司', `version` int(11) NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', PRIMARY KEY (`id`), KEY `idx_departure_city` (`departure_city`), KEY `idx_arrival_city` (`arrival_city`), KEY `idx_departure_time` (`departure_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

订单表承载业务流转,状态字段用int表示,不需要复杂的枚举表:

CREATE TABLE `orders` ( `id` int(11) NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '订单号', `user_id` int(11) NOT NULL, `flight_id` int(11) NOT NULL, `passenger_name` varchar(32) DEFAULT NULL COMMENT '乘机人姓名', `seat_class` varchar(10) NOT NULL COMMENT '舱位:EC经济舱 BC商务舱 FC头等舱', `price` decimal(10,2) NOT NULL COMMENT '成交价格', `status` tinyint(4) NOT NULL DEFAULT 0 COMMENT '0待支付 1已支付 2已取消', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `pay_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_user_id` (`user_id`), KEY `idx_flight_id` (`flight_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

3.2 为什么要把余票数冗余在航班表里

有些同学看到这里会问:“余票不是应该实时去订单表里统计已支付订单数、然后用总数减掉么?”理论上可以,但实际性能很差。每次用户查询航班,如果都要去COUNT一遍订单表里该航班已支付的订单数量,查询压力会非常大;而航班列表页面用户往往要一次看几十个航班,这个统计量就爆炸了。

所以我在航班表里直接维护一个remaining_seats字段,下单时扣减,取消时加回。这是一种以空间换时间、以冗余换性能的常见做法。但注意,冗余带来的副作用就是一致性问题:订单插入和余票扣减必须是一个原子操作,不能出现“订单插进去了但余票没减”这种脏数据。这就引出了后面事务和并发控制的内容。

3.3 订单号生成策略

订单表的主键用的是自增id,但对外暴露时应该使用业务订单号order_no。不能直接把自增id给用户看,因为很容易被遍历。我采取的方案是时间戳加用户信息加随机数:

public String generateOrderNo(Integer userId) { String time = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")); String userPart = String.format("%04d", userId % 10000); String randomPart = String.format("%04d", new Random().nextInt(10000)); return time + userPart + randomPart; }

这样生成的订单号是28位以内的字符串,足够唯一,也包含了生成时间信息,排查问题时一眼能看出是哪天下单的。

4. 核心业务实现:查询、预订、订单处理的完整链路

4.1 航班查询:MyBatis动态SQL的典型应用

查询功能最常用的场景是:用户选择出发城市、到达城市、出发日期,要能组合筛选。因为有多种条件组合的可能,这里就用到了MyBatis的动态SQL标签。

<select id="queryFlights" resultType="com.ssm.flight.entity.Flight"> SELECT * FROM flight <where> <if test="departureCity != null and departureCity != ''"> AND departure_city = #{departureCity} </if> <if test="arrivalCity != null and arrivalCity != ''"> AND arrival_city = #{arrivalCity} </if> <if test="departureDate != null"> AND DATE(departure_time) = #{departureDate} </if> </where> AND remaining_seats > 0 ORDER BY departure_time </select>

标签很智能,它会自动把第一个多余的AND去掉。如果你只填了到达城市,那么实际SQL就变成WHERE arrival_city = ?,不会出现语法错误。另外注意我加了AND remaining_seats > 0这个硬条件,这一行可以让前端在查询时直接过滤掉无票航班,用户体验好很多。

4.2 订票核心Service:并发扣票问题的两种解法

订票是整个系统的心脏,也是并发问题最集中的地方。先看一个最朴素、也是最危险的写法:

// 错误示例:并发下绝对会超卖 public Order createOrder(Integer userId, Integer flightId, String seatClass) { Flight flight = flightMapper.selectById(flightId); if (flight.getRemainingSeats() > 0) { flightMapper.decreaseRemainingSeats(flightId); orderMapper.insert(order); } }

这个写法在两个用户同时下单时,可能两个人都在执行if (remainingSeats > 0)的瞬间看到余票为1,然后各自都执行了扣减,结果一张票卖了两次。解决方式有两条路:悲观锁和乐观锁。

悲观锁的思路是:我在判断余票之前,先把这一行数据锁住,其他事务只能等待我提交后才能读取最新数据。SQL长这样:

SELECT * FROM flight WHERE id = #{flightId} FOR UPDATE;

在Java代码里,对应这种写法:

@Transactional(rollbackFor = Exception.class) public Order createOrderByPessimistic(Integer userId, Integer flightId, String seatClass) { Flight flight = flightMapper.selectFlightForUpdate(flightId); if (flight == null || flight.getRemainingSeats() <= 0) { throw new BizException("航班不存在或余票不足"); } Order order = buildOrder(userId, flight, seatClass); orderMapper.insert(order); flightMapper.decreaseRemainingSeats(flightId); return order; }

注意,FOR UPDATE锁必须在事务里才有意义,事务提交后锁才会释放,所以方法上必须有@Transactional注解。悲观锁的优点是逻辑直观、写起来简单;缺点是并发大时,同一航班的订票请求会串行排队,吞吐量会降低。

乐观锁的思路是不加锁,但在更新时带上版本条件,谁先更新成功谁赢。核心在SQL上:

<update id="decreaseRemainingSeatsByVersion"> UPDATE flight SET remaining_seats = remaining_seats - 1, version = version + 1 WHERE id = #{id} AND version = #{version} AND remaining_seats > 0 </update>

Java方法里在扣减后判断受影响行数,如果返回值是0说明版本已经被别人改过了,你就丢了这次竞争,需要提示用户“余票被别人抢了,请重试”:

@Transactional(rollbackFor = Exception.class) public Order createOrderByOptimistic(Integer userId, Integer flightId, String seatClass) { Flight flight = flightMapper.selectById(flightId); if (flight == null || flight.getRemainingSeats() <= 0) { throw new BizException("航班不存在或余票不足"); } Order order = buildOrder(userId, flight, seatClass); orderMapper.insert(order); int rows = flightMapper.decreaseRemainingSeatsByVersion(flightId, flight.getVersion()); if (rows == 0) { throw new BizException("余票已被抢占,请重新选择航班"); } return order; }

在我实测中,这个项目用乐观锁就够了,因为航班的订票并发量通常不会特别夸张,乐观锁不会阻塞其他事务,逻辑也更简洁。

4.3 订单状态机:状态流转不能随便改

订单状态我设计了三个值:0待支付,1已支付,2已取消。状态流转规则必须写清楚,不然会出现“已支付的订单还能重复取消、取消后还能支付”的混乱场景。

合法的状态流转路径就三条:创建订单时状态为0;待支付状态下,用户支付成功变成1;待支付状态下,用户取消变成2。已支付订单不能直接取消,因为涉及退款,MVP阶段可以不做;但要注意,很多同学把按钮“取消订单”无条件暴露在页面上,用户点已支付订单的取消,结果代码直接把状态改成已取消了,这是错的。

取消订单的Service里,必须判断当前订单状态是否为0:

@Transactional(rollbackFor = Exception.class) public void cancelOrder(Long orderId, Integer userId) { Order order = orderMapper.selectById(orderId); if (order == null || !order.getUserId().equals(userId)) { throw new BizException("订单不存在"); } if (order.getStatus() != 0) { throw new BizException("当前订单状态不允许取消"); } orderMapper.updateStatus(orderId, 2); flightMapper.increaseRemainingSeats(order.getFlightId()); }

同时把余票加回去:

<update id="increaseRemainingSeats"> UPDATE flight SET remaining_seats = remaining_seats + 1 WHERE id = #{id} </update>

注意这里我只在订单状态为0时可取消,所以回补余票时不需要再判断航班状态。事务把“改订单状态”和“回补余票”绑在一起了,所以即使中途出错,两个操作都会回滚,不会出现“订单取消了但余票没回来”。

5. 最容易翻车的三个地方:并发、事务、隐藏的坑

5.1 实测压测暴露的超卖问题:完整排查链路

我第一次写完这个项目,用JMeter模拟100个用户并发抢同一个航班的180个座位,结果数据库里出现了182条待支付订单。当时第一反应是SQL写错了,于是把SQL单独拿去数据库执行,每次扣减都正常,怎么程序里就出错?

排查过程我建议按这个顺序走:

第一,检查@Transactional注解有没有真正生效。当时我的一个Service方法用了@Transactional,但同一个类里另一个方法调用了它,形成了自调用,Spring的代理根本没拦截,事务等于没有。Spring事务是基于AOP代理的,只有在代理对象上调用方法,才会触发事务拦截器;你在类内部通过this.method()调用,是不经过代理的。解决方式是拆成两个独立的Service,或者用注入自身的方式调用另一个代理方法。

第二,检查事务配置里有没有开启事务管理器。SSM中你定义了DataSource和事务管理器还不够,必须显式配置注解驱动扫描,如果少了这一步,所有@Transactional注解都会静默失效,这是最坑的一处。

第三,检查数据库表和引擎。MySQL的MyISAM引擎不支持事务,即使Java层配置全对,底层也回滚不了。确保表是InnoDB引擎。

这三步查完,超卖问题大概率就能解决。

5.2 事务失效的其他经典场景

刚才说的自调用是事务失效最常见的场景,但还有几种情况同样会让你抓狂。

第一种:方法不是public。Spring的@Transactional默认使用JDK动态代理或CGLIB代理,但要求目标方法必须可被代理,private方法无法被外部代理拦截,事务自然无效。

第二种:rollbackFor设置不正确。Spring默认只对RuntimeException(运行时异常)回滚,如果业务代码里抛出的是自定义的受检异常(比如数据库约束异常、自定义BizException继承自Exception),默认情况下事务不会回滚,数据就会停留在半成品状态。所以统一做法是写@Transactional(rollbackFor = Exception.class),让所有异常都触发回滚。

第三种:数据库连接没走事务管理器。如果你的数据源配置了多个或者MyBatis的SqlSessionFactory没接到同一个事务管理器上,也会出现“看起来配了事务但实际没生效”的诡异问题。SSM项目里要确保sqlSessionFactory的dataSource属性和transactionManager的dataSource是同一个对象。

5.3 两个容易忽略的安全与线程问题

第一个是MyBatis里的#{}和${}。我见过很多新手在动态排序时图方便直接写ORDER BY ${sortField},这个写法是直接把字符串拼接进SQL,如果sortField来自前端用户输入,就是经典的SQL注入漏洞。正确做法是排序字段在Java代码里做一个白名单映射,比如传入的key对应到具体的实体字段名,再以固定值拼接进SQL。所有参数的值传递,一律用#{}。

第二个是SimpleDateFormat的线程安全问题。很多老项目习惯在工具类里写一个全局的SimpleDateFormat变量,然后到处调用。但SimpleDateFormat是线程不安全的,多线程并发解析日期时会出现错误结果,甚至抛异常。我在这个项目里改用Java 8的LocalDateTime加DateTimeFormatter,后者是线程安全的,用法更清爽:

private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); String formatTime(LocalDateTime time) { return time.format(FORMATTER); }

另外,数据库连接URL里一定要加上characterEncoding=UTF-8,否则插入中文姓名时会出现乱码。

6. 部署与验证:让系统真正跑起来

6.1 环境准备与部署步骤

这个项目我使用的技术栈是:JDK 1.8、Maven 3.6(可选)、Tomcat 8.5、MySQL 5.7。SSM对版本的包容度比较高,只要这几个版本不要过于极端都没问题。

部署步骤我给你理一遍,照着操作基本不会卡壳:

  1. 在MySQL中建库,执行上述的表结构和样例数据SQL脚本。
  2. 修改jdbc.properties,把数据库地址、用户名、密码改成你自己的环境。
  3. 在IDEA里导入项目,等待Maven把依赖下载完。这里容易遇到的坑是拉不到依赖或版本冲突,建议Spring相关依赖统一版本(比如4.3.30.RELEASE),MyBatis用好3.4.x,别混着latest乱用。
  4. 配置Tomcat,Document Base指向项目,启动后浏览器访问http://localhost:8080/项目名。
  5. 启动时如果报404,先看控制台有没有完整输出“Mapped to xxxController#yyy()”这样的映射日志;如果没有,就是包扫描路径配置不对,Spring容器根本没扫描到Controller。

6.2 验收测试清单

写完之后别急着交付,至少跑一遍下面的测试清单:

  • 注册新用户,密码保存到数据库是否加密(不应该是明文)。
  • 用错误密码登录,是否被正确拦截并提示。
  • 输入出发城市、到达城市、日期查询航班,组合条件查询是否正确。
  • 对同一个航班发起两个并发订票请求,在余票只有1张时,是否只有一个成功。
  • 创建订单后不支付,执行取消,再查航班余票是否回补。
  • 支付成功后,该订单是否还能被取消。
  • 管理员新增航班后,用户端能否立刻查到。

这些测试千万别靠肉眼在浏览器里手动点几下就算完,至少要用JMeter跑一次并发场景。很多同学的项目单机操作一切正常,一到并发测试就露馅,原因就是没提前做过验证。

6.3 演示数据准备

为了让流程能完整跑起来,我在库里放了几条演示数据。注意时间字段如果用的是datetime,可以用未来的日期方便测试。

INSERT INTO flight (flight_no, departure_city, arrival_city, departure_time, arrival_time, seat_count, remaining_seats, price_ec, price_bc, price_fc, airline) VALUES ('CA1234', '北京', '上海', '2025-06-10 08:00:00', '2025-06-10 10:30:00', 180, 180, 800.00, 1500.00, 2200.00, '中国国航'), ('MU5678', '上海', '广州', '2025-06-11 14:00:00', '2025-06-11 16:20:00', 200, 200, 950.00, 1800.00, 2600.00, '东方航空'), ('CZ9012', '广州', '成都', '2025-06-12 09:30:00', '2025-06-12 11:50:00', 150, 150, 700.00, 1300.00, 2000.00, '南方航空');

这些数据的出发和到达城市、时间、价格都有差异,登录后随便搜一个城市组合就能看到测试效果。

这个项目做完之后我自己有个很深的感受:SSM航班的代码量其实不多,但真正花时间的全在配置细节和并发一致性上。如果你也是第一次写这类系统,建议把上面的测试清单从头到尾执行一遍,特别是并发场景最值得跑。把这一套吃透,你以后再切换到Spring Boot的项目,会发现很多概念都是相通的,只不过框架帮你做了刚才那些繁琐的配置。到时候再回头想想为什么当初要选SSM来练这个题目,答案已经在你的动手过程里了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 16:49:59

AI浏览器代理工具实战:从自然语言指令到自动化操作

朋友公司每天要处理几百条来自不同电商平台的订单&#xff0c;人工在各个卖家后台来回切换、复制、粘贴再汇总成表格。他问我能不能写个脚本自动搞定&#xff0c;换作以前&#xff0c;我大概率会给他一套 Selenium Python&#xff0c;然后说“先学会 CSS 选择器再说”。但现在…

作者头像 李华
网站建设 2026/9/26 16:48:01

性能测试内存分析实战:free、vmstat、sar三工具排查链路

做了这么多年性能测试&#xff0c;我越来越觉得内存分析是三大件里最容易被“差不多”糊弄过去的一环。CPU 飙起来一眼就能看见&#xff0c;磁盘 IO 慢下来监控曲线也骗不了人&#xff0c;唯独内存&#xff0c;很多人压测时就是敲一下 free&#xff0c;看到 used 不高就放行了&…

作者头像 李华
网站建设 2026/9/26 16:44:14

Zero Autonomous Thinking 配置指南:为 OpenClaw 接入 TaoToken 统一 API 通道

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/26 16:44:09

Linux网卡配置指南:从NetworkManager到ip命令的永久与临时配置

刚接触服务器运维那阵子&#xff0c;我最怕的就是改网卡配置。明明照着网上教程敲完了命令&#xff0c;重启之后网络又回到原点&#xff1b;或者照着 A 发行版的文章改了配置文件&#xff0c;放到 B 发行版上却完全不生效。后来时间长了才明白&#xff0c;Linux 网卡配置无非三…

作者头像 李华
网站建设 2026/9/26 16:44:05

DeepSeek 使用技巧:VLM-R1 图生文测评与 TaoToken 配置实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华