
做JavaWeb方向的实战项目很多人会卡在“不知道做一个什么系统”上。既要业务逻辑清晰、方便演示又要把Spring、SpringMVC、MyBatis、Maven这些技术栈全部串起来同时还得让前端页面看得过去。我翻来覆去比较过出租车管理系统是个非常合适的选题——车辆、司机、订单、计费、统计每个模块拎出来都足够典型组合在一起又不会复杂到失控。这套项目完整技术栈是javaweb MySQL SSMSpring SpringMVC MyBatis Maven前端用JSP、HTML、JavaScript来渲染展示。整套项目跑起来之后可以说把JavaWeb开发的核心链路都走了一遍。这篇博文不打算只贴代码我会把项目从需求拆解、数据库设计、Maven环境配置、SSM三层架构落地再到运行调试踩过的坑完整地讲一遍。内容适合正在做毕业设计的学生、准备补项目经验的转行开发者以及想用完整案例验证SSM技术栈的Java初学者。读完你应该能自己动手搭出一套能跑、能演示、有业务深度的出租车管理系统。1. 项目整体拆解出租车管理系统到底在管什么1.1 从需求倒推功能模块很多新手拿到题目就急着写代码这是最容易翻车的习惯。正确的做法是先把“这个系统要给谁用、用在哪里、管哪些事”想清楚。出租车管理系统核心是把一辆车从空闲、接单、载客、行程结束到结算的完整生命周期管理起来。拆解下来系统至少需要四个角色维度系统管理员维护车辆档案、司机信息、乘客信息查看所有订单和营收统计。出租车司机登录后查看分配给自己的车辆、接收订单、更新行程状态、查看个人接单记录。乘客/用户注册登录、发起打车订单、查看行程状态、完成支付。系统本身需要处理订单流转、计费规则、数据统计等后台逻辑。功能模块表大致是这样的模块核心功能关键数据登录注册三个角色统一登录入口按角色跳转不同主页用户名、密码、角色ID车辆管理车辆信息的增删改查、车辆状态维护车牌号、车型、座位数、状态司机管理司机档案管理、司机与车辆绑定姓名、电话、驾驶证号、所属车辆订单管理创建订单、派单、行程开始、行程结束、订单取消订单号、乘客、司机、车辆、金额、状态计费与统计按里程/时长计费、每日/每月营收统计里程、单价、总金额、时间这个设计的好处是每个模块都有明确的业务边界后续用SSM分层时Controller、Service、Mapper天然对应不会出现“不知道代码该写在哪一层”的问题。1.2 为什么是SSM Maven MySQL这套组合现在SpringBoot很流行很多人会问为什么不直接用SpringBoot我的看法是如果你是为了快速交差SpringBoot确实省事但如果你是想理解JavaWeb开发的底层脉络SSM这套“老组合”反而更值得花时间。SSM的优势在于它的层级感特别清楚Spring负责对象管理和事务控制你能直观看到哪些Bean被容器托管事务注解加在哪里。SpringMVC负责请求路由和参数绑定从前端JSP页面发起一个请求经过DispatcherServlet分发到哪个Controller这个过程清晰可见。MyBatis负责数据库访问SQL写在哪里、参数怎么映射、结果集怎么封装全都可以手控。Maven负责依赖管理和项目构建jar包不用再手动往WEB-INF/lib里塞一个pom.xml全搞定。MySQL负责数据落盘所有业务最终都要落到表结构和SQL语句上。换句话说这套项目做完你对“一个请求从前端页面发出经过Controller、Service、Mapper最后查数据库、返回结果、渲染页面”的完整链路是真正有体感的。用SpringBoot写一遍很多东西被自动配置“藏”起来了反而不容易有这种收获。2. 数据库设计与订单状态流转2.1 核心表结构设计数据库是这套系统的基础表结构设计得好不好直接决定后面写Mapper和业务代码时是顺畅还是难受。我把常用字段直接写出来供参考。用户表t_user用于乘客和后台管理员CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(100) NOT NULL, real_name VARCHAR(50), phone VARCHAR(20), role INT DEFAULT 1 COMMENT 1-乘客 2-管理员, create_time DATETIME DEFAULT CURRENT_TIMESTAMP );司机表t_driver注意司机一般单独建表因为司机有自己特殊的资质字段CREATE TABLE t_driver ( id INT PRIMARY KEY AUTO_INCREMENT, driver_name VARCHAR(50) NOT NULL, license_no VARCHAR(20) UNIQUE NOT NULL, phone VARCHAR(20), car_id INT COMMENT 关联车辆ID, status TINYINT DEFAULT 0 COMMENT 0-休息 1-出车, create_time DATETIME DEFAULT CURRENT_TIMESTAMP );车辆表t_carCREATE TABLE t_car ( id INT PRIMARY KEY AUTO_INCREMENT, plate_no VARCHAR(20) UNIQUE NOT NULL, car_type VARCHAR(30), seat_count INT DEFAULT 4, status TINYINT DEFAULT 0 COMMENT 0-空闲 1-行驶中 2-维修, driver_id INT );订单表t_order这是整个系统最核心的表CREATE TABLE t_order ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) UNIQUE NOT NULL, user_id INT NOT NULL, driver_id INT, car_id INT, start_location VARCHAR(200), end_location VARCHAR(200), distance DECIMAL(10,2) DEFAULT 0 COMMENT 公里, amount DECIMAL(10,2) DEFAULT 0 COMMENT 金额, status TINYINT DEFAULT 0 COMMENT 0-待接单 1-已接单 2-行程中 3-已完成 4-已取消, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, finish_time DATETIME );这里有几个设计要点值得说状态字段用TINYINT存数字枚举而不是存字符串。查询效率更高也方便在Java代码里定义常量类统一管理。例如OrderStatus.WAITING 0。金额用DECIMAL(10,2)绝对不能用FLOAT或DOUBLE。计费金额涉及精度浮点运算会出问题这是很经典的坑。司机表和车辆表用car_id和driver_id互相关联体现“一个司机对应一辆车一辆车对应一个司机”的业务逻辑。实际项目里也可以只用逻辑关联不在数据库层面强制外键这样查询更灵活删除也不容易报外键约束错误。订单号用order_no因为自增ID不适合直接暴露给用户拼接时间戳加随机数更安全。2.2 订单状态机的流转逻辑订单状态是整个系统的“脊柱”所有业务模块基本都围着它转。我在代码实现时会单独定义一个常量类public class OrderStatus { public static final int WAITING 0; // 待接单 public static final int ACCEPTED 1; // 已接单 public static final int STARTED 2; // 行程中 public static final int FINISHED 3; // 已完成 public static final int CANCELED 4; // 已取消 }订单流转顺序是乘客下单WAITING→ 司机接单ACCEPTED→ 司机开始行程STARTED→ 到达目的地结束行程FINISHED。其中任意阶段乘客或司机都可以取消CANCELED但已完成的订单不能再取消。在MySQL层面状态字段配合事务和锁来保证一致性。比如司机点击“接单”时可能多个司机同时抢同一单这时候就会出现并发问题。MyBatis的update语句本身就带行锁我更习惯用“乐观锁”思路在更新时加条件判断UPDATE t_order SET driver_id #{driverId}, status 1 WHERE id #{orderId} AND status 0这条SQL的意思是只有当订单还是待接单状态时才能把司机ID写进去并改成已接单。如果执行后影响行数为0说明订单已经被别人抢走了Service层就可以抛出“接单失败订单已被抢”的提示。这种方式不需要显式加锁也不容易死锁线程安全又简单。3. Maven环境配置与IDEA运行JavaWeb项目3.1 Maven到底是干嘛的很多新手第一次用Maven时一脸懵两个最经典的问题就是Maven是干嘛的为什么我项目里一堆依赖报错用生活化的方式解释Maven就是一个“依赖管理和项目构建工具箱”。以前做JavaWeb项目要自己到网上下载各种jar包然后手动复制到WEB-INF/lib目录麻烦不说还经常遇到版本冲突。Maven出现之后你不用再手动管理jar包了只需要在pom.xml里声明依赖的“坐标”groupld artifactId versionMaven会自动从仓库下载并管理依赖。Maven的仓库分三级本地仓库、私服/中央仓库。默认配置下本地仓库没有的jar包会从中央仓库下载但中央仓库在国外下载速度很慢所以国内项目基本都会配置阿里云镜像这样下载速度会快很多。Maven的第三个作用是构建。mvn clean清理、mvn compile编译、mvn package打包、mvn install安装到本地仓库。这些命令在开发中很常用尤其是clean install很多时候项目运行不了先执行一遍mvn clean install就能解决一半问题。3.2 Maven安装与阿里云镜像配置下面是我在实际环境中验证过的配置流程。适用Windows系统IDEA版本不限制。第一步去Apache Maven官网下载二进制压缩包注意选择apache-maven-3.6.3-bin.tar.gz或者3.8.x版本不要下载太久远的版本。解压到本地目录比如D:\dev\apache-maven-3.6.3路径里不要有中文和空格。第二步配置环境变量。新建MAVEN_HOME值为解压路径编辑Path追加%MAVEN_HOME%\bin。配置完成后打开命令行输入mvn -v能显示版本信息说明配置成功。第三步修改settings.xml。这个文件在Maven的conf目录下主要改两处本地仓库路径和阿里云镜像。localRepositoryD:/dev/maven-repository/localRepositorymirror idaliyunmaven/id mirrorOfcentral/mirrorOf name阿里云公共仓库/name urlhttps://maven.aliyun.com/repository/public/url /mirror第四步IDEA里配置Maven。打开IDEA设置搜索Maven把Maven home directory指向你的解压目录User settings file指向刚才改的settings.xmlLocal repository会自动识别成D:/dev/maven-repository。这里特别提醒IDEA自己内置了一个Maven如果不用配置容易踩坑——内置Maven的settings.xml是IDEA默认的不会用你的阿里云镜像配置下载依赖会特别慢。所以一定要在IDEA里手动改成自己安装的Maven并且确定使用的是同一个settings.xml。热搜里还有一个很常见的问题“我有两个Maven本地仓库repository怎么合并”。这个问题我遇到过解决方式很简单两个仓库目录里存的是不同时期下载的jar包直接复制合并到一个目录会有风险可能同时存在不同版本。更好的做法是保留一个把另一个中的依赖用命令行重新安装到当前仓库或者干脆用新的仓库路径重新clean install让Maven自己去补全缺失的依赖。3.3 MySQL 5.7/8.0安装与初始化注意点MySQL安装版本我个人建议直接选8.0或者5.7.44注意选对版本对应的安装方式。如果下载的是ZIP免安装版比如mysql-8.0.x-winx64.zip步骤是这样的解压到D:\dev\mysql-8.0.44-winx64在根目录新建my.ini基础配置如下[mysqld] basedirD:/dev/mysql-8.0.44-winx64 datadirD:/dev/mysql-8.0.44-winx64/data port3306 character-set-serverutf8mb4 default-time-zone08:00然后以管理员身份打开命令行依次执行cd D:\dev\mysql-8.0.44-winx64\bin mysqld --initialize-insecure mysqld --install net start mysql初始化完成后默认root用户密码为空可以直接用mysql -u root -p登录。如果net start mysql提示服务启动失败多半是my.ini配置里的路径有问题或者data目录没有正确初始化删掉data目录重新执行mysqld --initialize-insecure即可。字符集方面我强烈建议在初始化时就把character-set-serverutf8mb4配上否则后面插入中文数据会出现乱码。排序规则可以用utf8mb4_general_ci日常项目完全够用。时区设置也很重要不设置的话Java连接MySQL可能会报The server time zone valueйʱis unrecognized在JDBC连接串上加serverTimezoneAsia/Shanghai也能解决但直接在MySQL配置里搞定更干净。数据库导入项目SQL文件命令行里执行mysql -u root -p source D:/taxi/src/main/resources/taxi.sql;导入完成后用show tables;验证一下总共应该有t_user、t_driver、t_car、t_order等表。4. 核心业务实现SSM三层架构和JSP页面落地4.1 Controller-Service-Mapper三层怎么分工SSM的分层很多人觉得是形式主义但实际上手写业务时分层的价值立马就能体会到。我以“乘客下单”这个最核心的操作为例演示三层是怎么协作的。前端JSP页面提交表单携带起点、终点、乘客IDform action${pageContext.request.contextPath}/order/create methodpost input typehidden nameuserId value${sessionScope.user.id} 起点input typetext namestartLocationbr 终点input typetext nameendLocationbr 里程input typetext namedistancebr button typesubmit呼叫出租车/button /formController层负责接参数、调服务、返回页面Controller RequestMapping(/order) public class OrderController { Autowired private OrderService orderService; PostMapping(/create) public String createOrder(Order order, Model model) { try { orderService.createOrder(order); model.addAttribute(msg, 下单成功等待司机接单); } catch (Exception e) { model.addAttribute(msg, 下单失败 e.getMessage()); } return order_result; } }Service层负责业务逻辑比如生成订单号、设置初始状态、验证乘客是否存在Service public class OrderServiceImpl implements OrderService { Autowired private OrderMapper orderMapper; Override Transactional public int createOrder(Order order) { String orderNo D System.currentTimeMillis() (int)(Math.random() * 900 100); order.setOrderNo(orderNo); order.setStatus(OrderStatus.WAITING); order.setCreateTime(new Date()); return orderMapper.insertOrder(order); } }Mapper层只做数据操作MyBatis的XML或者注解都行。这个流程走通之后你会理解一个核心理念Controller不写SQLMapper不写业务逻辑一切业务判断都放在Service。这样每个方法的职责单一出了问题只需要定位到对应的层不用在Servlet里挤一堆耦合代码。4.2 用PageHelper做分页展示出租车管理系统的订单列表、司机列表肯定要分页不可能一次把所有数据都查出来。分页实现方式有两种手写LIMIT参数或者用PageHelper插件。我更推荐PageHelper因为引入成本低而且代码侵入性很小。先在pom.xml添加PageHelper依赖然后MyBatis配置里加上插件plugins plugin interceptorcom.github.pagehelper.PageInterceptor property namehelperDialect valuemysql/ /plugin /plugins用法非常直接PageHelper.startPage(pageNum, pageSize); ListOrder orderList orderMapper.selectOrderList(); PageInfoOrder pageInfo new PageInfo(orderList);一行PageHelper.startPage之后紧接着的第一条SQL查询会被自动加上LIMIT。PageInfo里包含了总记录数、总页数、当前页码、当前页数据等所有分页信息直接放到Model里传给JSP页面渲染页码按钮。不过要注意PageHelper.startPage只对下一条查询语句生效。如果你在startPage和查询之间执行了其他操作分页就会失效或者错乱。所以我现在的习惯是startPage和查询语句紧挨着写中间不插入任何其他代码。4.3 JSP页面与图片坐标定位前端页面用JSP HTML JavaScript常见的需求有登录后按角色跳转不同主页JSP里用EL表达式${user.username}展示当前登录用户用JSTL标签c:forEach循环遍历订单列表个人信息展示页面显示头像、手机号、注册时间地图定位相关功能。上面第四点很多人会问“JSP图片如何对坐标定位”。这个问题我在项目里也处理过说一个简单可行的方案。出租车管理系统里如果有“车辆位置查看”功能最简单的实现不是接入地图API而是用JSP页面的img标签加载一张城市背景图PNG/JPG然后用CSS绝对定位把车辆图标渲染在对应的坐标点上。具体思路在数据库表里存车辆的longitude经度和latitude纬度知道背景图的宽高以及图片对应的地理范围左上角经纬度和右下角经纬度在JSP页面用JavaScript计算相对位置把经纬度换算成图片上的像素坐标。换算公式var x (longitude - minLon) / (maxLon - minLon) * imgWidth; var y (maxLat - latitude) / (maxLat - minLat) * imgHeight;然后把生成的车辆图标通过绝对定位放在背景图上position: absolute; left: x px; top: y px;如果项目要求更高精度的地图效果我会建议直接嵌入高德地图JS API。高德有专门为个人开发者提供的免费key支持标记点、轨迹绘制、周边检索代码也不复杂。但对SSM练习项目来说用背景图加坐标换算已经足够演示了还能顺便练一下JavaScript数据处理。5. 运行调试与常见问题速查5.1 IDEA中配置Tomcat并启动项目项目在IDEA里运行关键步骤是配置Tomcat。选择Run菜单里的Edit Configurations点击号选Tomcat Server Local。然后Application server选择你的Tomcat安装目录Deployment添加Artifact选择项目的war包或者war exploded模式Server标签页里设置HTTP端口一般用8080如果有冲突可以改成8081Application context改成/taxi或者留空。运行前提是Maven依赖已经刷新完毕pom.xml没有红色报错本地MySQL服务已启动数据库已经导入。点击运行后Tomcat日志出现Artifact is deployed successfully然后在浏览器输入http://localhost:8080/taxi访问首页。如果页面没出来先看IDEA控制台的报错信息。5.2 一套很实用的报错排查思路根据热搜词里的高频问题我整理一个更完整的速查表报错/现象可能原因解决办法IDEA里pom.xml红色报错Maven依赖下载失败检查Maven配置的是否是阿里云镜像执行mvn clean install刷新依赖404错误Tomcat启动成功但页面找不到Application context路径不对检查URL访问路径和项目context是否一致比如/taxi下的/index.jsp要访问http://localhost:8080/taxi/index.jsp500错误页面报ClassNotFoundException依赖没有被部署到Tomcat检查Artifact是否选择了war explodedIDE不会自动部署依赖jar包在Project Structure中确认lib目录有所有jar数据库连接失败Communications link failureMySQL服务没有启动或端口不对确认MySQL服务在运行检查JDBC URL的端口、数据库名、用户名密码中文乱码页面字符集和数据库字符集不一致JSP页面统一加UTF-8声明MySQL连接URL加useUnicodetruecharacterEncodingutf8表字段用utf8mb4端口被占用Tomcat启动失败多个Tomcat实例或其他程序占用8080换端口或者用netstat -anoservlet/api冲突问题本地jar包和Tomcat自带servlet-api重复pom.xml中把servlet-api的scope设置为provided除了这个表格还有两个特别值得注意的坑Maven依赖报错后不要盲目删除本地仓库。有些同学依赖一红就跑到本地仓库里一通删结果越删越乱。正确操作是先mvn clean再mvn compile看控制台具体报错的是哪个依赖再去处理。MySQL服务启动失败先看错误日志。在MySQL的data目录下有.err结尾的日志文件里面会写清楚为什么启动失败。如果是my.ini路径写错日志会提示如果是端口占用也会提示。看日志比瞎猜有效十倍。5.3 几条关于这个项目实战的真实经验我把这套出租车管理系统从头到尾实现过也帮别人排查过类似项目有几个经验分享个人项目最容易犯的毛病是“重功能轻结构”。最开始我写SSM项目Controller里塞了一堆业务代码Service形同虚设后来加了一个统计功能改动成本让我差点重构。所以现在做任何JavaWeb项目我都坚持流程先建数据库表再写Mapper然后Service最后Controller和页面。顺序一旦反过来后面一定乱。数据库脚本要在一开始就准备好。不要等代码写完了再手动往表里插数据。项目根目录放一个sql/taxi.sql包含了建库、建表、样例数据。这样无论换电脑还是给别人部署只要执行一遍脚本环境和数据全部就位。还有一点关于使用现成项目代码。很多同学习惯把别人的项目源码下载下来直接运行然后报错一堆不知从何下手。我的建议是就算用现成项目也要自己把代码从上到下读一遍尤其是pom.xml的依赖版本、applicationContext.xml的数据源配置、web.xml的DispatcherServlet配置。这三个地方基本决定了一个SSM项目能不能跑起来看懂它们之后遇到报错就不会手足无措。在我个人项目的实操体会里SSM出租车管理系统真正神奇的地方是它把JavaWeb的几乎所有重要知识点都揉在了一起表单提交、请求转发、数据库事务、分页查询、登录拦截、文件上传头像、状态流转、报表统计。完成一遍之后不会的东西会变得很少。如果你正在找项目练手或者需要完成一个能讲清楚原理的课程设计选这个方向是值得的。真正跑通的那一刻你会发现很多以前觉得模棱两可的概念一下子就通了。