
简介在Java后端开发中SSM框架是一套经典的技术组合它将Spring的IoC与事务管理、SpringMVC的请求分发和MyBatis的动态SQL能力整合在一起为中小型企业级项目提供了清晰的分层架构。理解这套组合的原理有助于开发者从底层掌控框架的协作机制。实战中通过构建一个网约车用户服务平台可以完整覆盖从用户、司机、管理后台的多角色设计到订单状态机、并发抢单、费用计算等业务场景。文章以该项目为例详细拆解了架构选型、MySQL表结构设计、SSM整合配置、核心功能实现以及部署排错过程帮助读者快速掌握SSM项目开发的完整链路。 不做系统全栈的人可能体会不到一个看起来只是做个系统的毕设或者练手项目真正折磨人的地方往往不在技术多高深而是在架构取舍和细节落地之间反复折腾。SSM加MySQL这套组合在国内Java方向的项目里出现频率极高尤其是网约车这种业务链条完整的选题既有用户端、司机端、管理后台的角色区分又涉及订单状态流转、费用计算、数据统计这些偏业务逻辑的模块练一遍基本能把框架整合和数据库设计都吃透。下面我以自己做过的一个内部练习项目为例把这个网约车用户服务平台从架构选型、表结构设计、SSM整合到核心模块实现、部署排错整条链路拆开讲清楚。如果你正在做一个SSM方向的项目或者想从零搭一个完整系统练手这篇内容可以省掉不少查资料和踩坑的时间。1. 项目定位与整体架构设计思路1.1 为什么选SSM这个组合而不是一上来用Spring Boot先回答一个很多人纠结的问题现在新项目不都用Spring Boot吗为什么还要做SSM从技术演进的角度看Spring Boot确实让Java后端开发效率高了不少但SSM作为Spring生态的经典组合拆开来看每一层都更显式。Spring容器负责Bean管理和AOP事务SpringMVC负责Web层的请求分发和参数绑定MyBatis负责数据库访问和SQL控制。三个框架的边界非常清晰配置都是独立文件你写的时候能明确知道一个请求从Tomcat进来以后经过了哪个DispatcherServlet、哪一层拦截器、哪一段事务代理再到SQL语句执行。对学习和面试来说这份显式的清晰反而是优势。你能肉眼看到IoC容器是如何把Service注入到Controller的也能在XML里直观地配置事务切面理解MyBatis的Mapper代理是怎么生成出来的。Spring Boot把这些都自动装配了方便是真方便但对原理的理解深度要弱一些。从项目交付的角度看这套平台包含用户端、司机端、管理后台三条业务线用SSM做分层开发前后端分离或者传统的JSP渲染都能支撑。毕设评审或者项目汇报时你可以把Spring的IoC容器如何管理Service、SpringMVC的完整请求链路、MyBatis的动态SQL如何应对复杂查询条件这些点都讲清楚内容量完全够。1.2 系统功能模块与角色权限拆解网约车平台的核心角色我拆成了三类乘客、司机、管理员。这也是订单业务闭环最基本的三个参与方。乘客端的功能主要围绕叫车这个动作展开注册登录、个人信息维护、余额充值、发起叫车请求、选择出发地和目的地、查看司机接单状态、行程结束后支付费用、对司机进行评价。这里有一个容易被忽略的细节乘客端发起叫车请求时前端需要调用高德或百度地图的Web服务API把起终点的文字描述转换为经纬度坐标后端拿到的是结构化坐标数据才能做距离计算和后续的订单匹配。司机端的核心是接单和履行订单司机注册并提交资质材料姓名、身份证号、驾驶证号、车牌号、车型、车辆颜色管理员审核通过后才能成为正式司机。司机具备查询可抢订单、抢单、开始行程、结束行程、查看收入流水等功能。状态机是整个司机端业务的关键后面我会专门讲订单状态流转的设计。管理后台面向平台运营人员司机资质审核、乘客账号管理、订单管理包含异常订单处理、基础数据统计每日订单量、GMV、司机完单量、系统公告配置。这部分在实现上偏CRUD但统计报表会涉及时间范围查询、分组聚合等稍微复杂的SQL。权限控制这块我没有引入Shiro或Spring Security这类重量级安全框架而是通过拦截器加Session判断来实现。系统按角色划分了乘客、司机、管理员三种会话登录成功后把用户ID和角色存入Session在拦截器里校验用户是否能访问当前URL。这种做法在中小型学习项目中完全够用而且实现起来直观面试时能说清楚拦截逻辑即可。1.3 技术选型明细与版本搭配这套系统我选用的具体技术版本组合如下表基本是2018年到2020年间最稳定的一套搭配现在用也不会有兼容性问题技术组件选型方案说明JDK1.8SSM项目在JDK 8下兼容性最好lambda表达式和Stream也能用数据库MySQL 5.75.7性能稳定事务和索引都够用8.0也可以但驱动和时区配置有差异后端框架Spring 5.1.x SpringMVC MyBatis 3.4.x经典SSM组合构建工具Maven 3.6统一依赖管理多模块或单模块都方便Web容器Tomcat 8.5支持Servlet 3.0规范IDEA内置的Tomcat就是8.5.x前端技术JSP JSTL jQuery Bootstrap传统的服务端渲染方式和SSM风格匹配数据库连接池Druid 1.1.x自带监控页面开发调试很方便JSON处理Jackson 2.9.x前后端数据交互统一使用JSON格式分页插件PageHelper 5.x基于MyBatis拦截器实现简化分页查询代码这个组合看起来朴素但每个组件在项目里都有明确职责不容易出现依赖一堆却说不清干什么用的情况。Druid的监控页面对调优SQL很有帮助PageHelper则把分页逻辑从手写LIMIT中解放出来都是实用选择。2. 数据库设计与核心表结构2.1 核心实体关系梳理在设计数据库之前先把业务实体之间的关联梳理清楚。这个网约车平台最核心的关系就是乘客发起订单司机接单并完成服务。主要的实体包括用户乘客、司机、订单、评价、充值记录/支付流水、管理员、系统公告。其中用户和司机是包含关系因为司机本身也是平台用户我先设计了user表作为账号主体再建driver_profile表存放司机特有的资质和车辆信息通过user_id字段关联。这样的好处是账号体系统一登录验证逻辑只需要走一张表。订单表是全局的核心它同时关联乘客ID和司机ID记录了一次完整出行服务的所有数据。评价表挂在订单之下一笔订单最多只有一条评价评价后再更新司机的平均评分。充值/支付流水表则记录余额变动的每一笔明细方便对账。2.2 乘客、司机、订单三张关键表的设计细节user表用户表CREATE TABLE user ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键ID, phone varchar(11) NOT NULL COMMENT 手机号登录账号, password varchar(64) NOT NULL COMMENT 密码MD5加密存储, nickname varchar(32) DEFAULT NULL COMMENT 昵称, avatar varchar(255) DEFAULT NULL COMMENT 头像地址, balance decimal(10,2) DEFAULT 0.00 COMMENT 账户余额, status tinyint(4) DEFAULT 1 COMMENT 状态0禁用 1正常, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 注册时间, PRIMARY KEY (id), UNIQUE KEY uk_phone (phone) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4 COMMENT乘客用户表;这里有两个设计细节值得注意。第一手机号作为登录账号必须加唯一索引这是账号体系最基本的约束。第二余额字段使用decimal(10,2)在Java实体类中对应BigDecimal避免用double造成精度丢失。支付场景下金额计算的精度问题项目里是严格按分为单位在代码里计算的展示时才转为元。driver表司机表CREATE TABLE driver ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键ID, user_id bigint(20) NOT NULL COMMENT 关联用户表ID, real_name varchar(32) NOT NULL COMMENT 真实姓名, id_card varchar(18) NOT NULL COMMENT 身份证号, driver_license varchar(64) NOT NULL COMMENT 驾驶证号, plate_number varchar(10) NOT NULL COMMENT 车牌号, car_brand varchar(32) DEFAULT NULL COMMENT 车辆品牌, car_color varchar(16) DEFAULT NULL COMMENT 车辆颜色, status tinyint(4) DEFAULT 0 COMMENT 审核状态0待审核 1正常 2禁用, rating decimal(2,1) DEFAULT 5.0 COMMENT 综合评分, total_orders int(11) DEFAULT 0 COMMENT 完单量, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 申请时间, PRIMARY KEY (id), UNIQUE KEY uk_user_id (user_id), UNIQUE KEY uk_plate_number (plate_number) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4 COMMENT司机信息表;司机表需要单独拆分是因为司机的属性和乘客差异太大了。一个用户既能当乘客又能申请成为司机这种扩展表而不是加字段的方式在数据库设计里属于非常合理的规范化处理。注意身份证号、车牌号都加了唯一索引防止重复提交。order表订单表订单表需要特别说明一个坑order这个词虽然可以创建成功但它不是SQL保留字实际使用中绝大多数情况没问题不过为了完全避免某些JDBC驱动或框架解析时的歧义建议建表名时使用t_order或者bus_order。我自己用的是t_order。CREATE TABLE t_order ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 订单ID, order_no varchar(32) NOT NULL COMMENT 订单编号, user_id bigint(20) NOT NULL COMMENT 乘客ID, driver_id bigint(20) DEFAULT NULL COMMENT 司机ID, start_position varchar(255) NOT NULL COMMENT 上车点, end_position varchar(255) NOT NULL COMMENT 下车点, start_lng decimal(10,6) DEFAULT NULL COMMENT 起点经度, start_lat decimal(10,6) DEFAULT NULL COMMENT 起点纬度, end_lng decimal(10,6) DEFAULT NULL COMMENT 终点经度, end_lat decimal(10,6) DEFAULT NULL COMMENT 终点纬度, distance decimal(10,2) DEFAULT 0.00 COMMENT 预估距离(公里), estimated_price decimal(10,2) DEFAULT 0.00 COMMENT 预估价格, real_price decimal(10,2) DEFAULT 0.00 COMMENT 实际价格, status tinyint(4) DEFAULT 0 COMMENT 状态0待接单 1已接单 2行程中 3已完成 4已取消, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 下单时间, accept_time datetime DEFAULT NULL COMMENT 接单时间, finish_time datetime DEFAULT NULL COMMENT 完成时间, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_id (user_id), KEY idx_driver_id (driver_id), KEY idx_status (status) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4 COMMENT订单表;订单表是查询压力最大的表所以索引设计很关键。我分别对user_id、driver_id、status建立了普通索引覆盖查看我的历史订单和筛选待接单订单这两个高频查询场景。订单编号order_no用唯一索引它由时间戳加随机数生成作为分布式场景下对外暴露的订单标识。2.3 订单状态机设计与流转逻辑订单状态是整个系统业务逻辑最复杂的部分我设计了一个明确的五态状态机每个状态变化都对应一个明确的业务操作状态值状态含义触发操作前置状态0待接单乘客提交叫车请求-1已接单司机抢单成功02行程中司机开始行程13已完成司机结束行程乘客支付完成24已取消乘客取消待接单状态下或超时未接自动取消0这个状态机在实现时依赖两件事一是Service层方法里做状态判定二是更新SQL里带状态条件。也就是说执行司机接单这个操作时SQL语句是UPDATE t_order SET driver_id ?, status 1, accept_time NOW() WHERE id ? AND status 0。这样即使两个司机同时抢同一单也只有一个能更新成功天然解决了并发抢单的竞态问题。乘客取消订单只允许在待接单状态进行司机接单后乘客不能随便取消如果确实需要取消那就要走管理后台的客服介入流程这属于异常订单处理的部分。通过状态机限制可以防止数据被乱改这是这类业务设计的精髓。3. SSM框架整合与核心配置细节3.1 Maven项目结构与依赖管理项目采用标准Maven Web工程结构我直接给出目录组织方式方便对照ride-hailing-platform ├── pom.xml └── src ├── main │ ├── java │ │ └── com/example/ride │ │ ├── controller // Controller层处理请求和响应 │ │ ├── service // Service接口层 │ │ ├── service/impl // Service实现层事务边界 │ │ ├── mapper // MyBatis Mapper接口 │ │ ├── entity // 实体类对应数据库表 │ │ ├── common // 通用工具类、常量、统一返回结果 │ │ └── interceptor // 登录拦截器、角色校验拦截器 │ ├── resources │ │ ├── jdbc.properties // 数据库连接配置 │ │ ├── spring-context.xml // Spring核心配置 │ │ ├── spring-mvc.xml // SpringMVC配置 │ │ └── mapper // MyBatis映射XML文件 │ └── webapp │ ├── WEB-INF/web.xml // Web部署描述符 │ ├── static // CSS、JS、图片资源 │ └── jsp // JSP页面 └── test └── java // 单元测试pom.xml里的依赖需要注意版本协调。Spring相关依赖我用的是统一的5.1.8.RELEASE版本MyBatis选3.4.6mybatis-spring是2.0.3。这里有个常见的坑mybatis-spring版本如果低于2.0和Spring 5配合时可能因为接口改动报异常。用Maven的话建议spring-webmvc、spring-jdbc、spring-tx都是同一个版本号不然Spring内部组件版本不一致很容易出现奇怪的问题。3.2 Spring核心配置与事务管理spring-context.xml负责所有除Controller外Bean的装配。核心配置包含组件扫描配置排除Controller、数据源配置、SqlSessionFactory配置、Mapper扫描器配置、事务管理器配置和事务切面配置。!-- 组件扫描排除ControllerController交给SpringMVC管理 -- context:component-scan base-packagecom.example.ride context:exclude-filter typeannotation expressionorg.springframework.stereotype.Controller/ /context:component-scan !-- 数据库连接池 -- bean iddataSource classcom.alibaba.druid.pool.DruidDataSource init-methodinit destroy-methodclose property namedriverClassName value${jdbc.driver}/ property nameurl value${jdbc.url}/ property nameusername value${jdbc.username}/ property namepassword value${jdbc.password}/ /bean !-- SqlSessionFactory -- bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath:mapper/*.xml/ property nametypeAliasesPackage valuecom.example.ride.entity/ /bean !-- Mapper接口扫描 -- bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.example.ride.mapper/ /bean !-- 事务管理 -- bean idtransactionManager classorg.springframework.jdbc.datasource.DataSourceTransactionManager property namedataSource refdataSource/ /bean !-- 事务切面 -- tx:advice idtxAdvice transaction-managertransactionManager tx:attributes tx:method nameadd* propagationREQUIRED rollback-forException/ tx:method nameupdate* propagationREQUIRED rollback-forException/ tx:method nameinsert* propagationREQUIRED rollback-forException/ tx:method namedelete* propagationREQUIRED rollback-forException/ tx:method name* read-onlytrue/ /tx:attributes /tx:advice事务配置这块我用的tx:advice加aop:config的方式事务切面作用于service包下所有方法。为什么要单独配事务而不直接用Transactional注解因为注解方式需要对每个方法或每个类手动标注很容易漏标而XML声明式事务可以按方法名前缀统一切入比如query方法强制只读写操作强制REQUIRED传播行为这在团队协作时是一个不错的规范约束。关于事务有一个实际发生过的问题需要提醒如果配置了事务却不起作用先检查Spring管理的Service实现类是否被AOP代理有没有实现接口、CGLIB代理是否生效再检查方法调用是不是通过this引用进行的。事务只对通过代理对象的外部调用生效类内部方法互调是不经过代理的这就解释了为什么很多初学者发现我在同一个类里调用addXxx事务好像没有生效。3.3 SpringMVC配置与请求流转spring-mvc.xml配置DispatcherServlet的上下文核心是组件扫描Controller、注解驱动、视图解析器、静态资源放行。!-- Controller扫描 -- context:component-scan base-packagecom.example.ride.controller use-default-filtersfalse context:include-filter typeannotation expressionorg.springframework.stereotype.Controller/ /context:component-scan !-- 注解驱动 -- mvc:annotation-driven mvc:message-converters bean classorg.springframework.http.converter.json.MappingJackson2HttpMessageConverter property nameobjectMapper bean classcom.fasterxml.jackson.databind.ObjectMapper property namedateFormat bean classjava.text.SimpleDateFormat constructor-arg valueyyyy-MM-dd HH:mm:ss/ /bean /property /bean /property /bean /mvc:message-converters /mvc:annotation-driven !-- 视图解析器 -- bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/jsp// property namesuffix value.jsp/ /bean !-- 静态资源放行 -- mvc:resources location/static/ mapping/static/**/ /bean一个请求到达Tomcat后的完整链路是DispatcherServlet拦截符合url-pattern的请求通过HandlerMapping找到对应的Controller方法执行方法前经过自定义拦截器做登录校验方法内部接收参数并调用Service层处理业务返回字符串时视图解析器拼接前缀后缀定位到JSP页面返回对象并用ResponseBody标注时则通过Jackson消息转换器输出JSON。这里基于Jackson做的全局日期格式配置非常关键否则后端返回的日期字段默认会是时间戳格式前端拿到后还要手动转换。3.4 MyBatis映射与动态SQL实践MyBatis是SSM框架中写SQL最灵活的一层。我列举两个高频场景的动态SQL写法。第一个是订单多条件查询用于管理后台和乘客历史订单列表select idselectOrderList resultTypecom.example.ride.entity.Order SELECT * FROM t_order where if testuserId ! null AND user_id #{userId} /if if testdriverId ! null AND driver_id #{driverId} /if if teststatus ! null AND status #{status} /if if teststartTime ! null AND create_time gt; #{startTime} /if if testendTime ! null AND create_time lt; #{endTime} /if /where ORDER BY create_time DESC /select第二个是批量更新司机状态比如管理员批量审核update idbatchUpdateDriverStatus UPDATE driver SET status #{status} WHERE id IN foreach collectionids itemid open( separator, close) #{id} /foreach /update关于where标签和if标签的配合很多人第一次写会踩坑如果所有条件都不满足where标签会自动去掉WHERE关键字不会生成WHERE 11这种有性能隐患的SQL。如果你手写SQL用WHERE 11在数据量大时就会放弃索引优化动态SQL标签就是为解决这类问题而生的。另外在XML里写大于号小于号时建议使用gt;和lt;转义避免解析冲突。4. 核心功能模块实现与关键代码4.1 用户端注册登录与信息维护注册登录模块相对常规但有两块设计值得说说。密码存储用的是MD5加盐的方式。由于项目是学习性质没有引入BCrypt这种更安全的算法但直接MD5裸存肯定是不行的我加了一个固定盐值比如用户手机号后四位拼上业务方随机字符串拼好后再做MD5这样至少能防住彩虹表碰撞。如果你打算把这个项目作为毕业设计去答辩在安全模块讲清楚为什么要加盐并且能顺手对比一下BCrypt和MD5的差异是一个很不错的加分点。注册接口的核心代码逻辑如下PostMapping(/register) ResponseBody public Result register(RequestBody User user) { // 1. 校验手机号格式 if (!RegexUtils.isPhone(user.getPhone())) { return Result.error(手机号格式不正确); } // 2. 校验手机号是否已注册 if (userService.getByPhone(user.getPhone()) ! null) { return Result.error(该手机号已注册); } // 3. 密码加盐MD5 String salt user.getPhone().substring(7); user.setPassword(Md5Util.md5(user.getPassword() salt)); user.setBalance(new BigDecimal(0.00)); user.setStatus(1); userService.register(user); return Result.success(); }密码加盐这里要特别说明substring(7)这个操作的含义手机号是11位从第8位开始取后4位作为盐的一部分然后拼接固定盐串。这样同一个密码在不同手机上注册时最终存储在数据库里的密文是不同的。登录后需要把用户信息放进Session后续拦截器通过Session里是否存在用户对象来判断是否已登录。角色判断也要依托Session我在登录成功时塞了一个userRole字段防止乘客直接访问司机端的管理接口。4.2 司机端抢单与订单状态流转司机端的核心操作是接单前面已经提到并发控制靠状态条件更新来实现。这里把完整的Service层代码逻辑补齐Override Transactional public boolean acceptOrder(Long orderId, Long driverId) { // 1. 查询司机是否处于正常状态 Driver driver driverMapper.selectByUserId(driverId); if (driver null || driver.getStatus() ! 1) { return false; } // 2. 尝试将订单状态从0更新为1带上条件状态 int rows orderMapper.acceptOrder(orderId, driverId); return rows 0; }对应的Mapper SQL是update idacceptOrder UPDATE t_order SET driver_id #{driverId}, status 1, accept_time NOW() WHERE id #{orderId} AND status 0 /update这个方法返回rows 0表示更新影响了一行数据说明抢单成功如果影响行数为0说明订单已经被别人抢走或订单状态已经不是待接单了。这个方法加了Transactional因为后续如果还有司机接单数1、通知乘客等操作需要一起提交或一起回滚。行程结束时的费用计算我在项目里做了一个简化版的计费规则起步价10元含3公里超出部分每公里2.5元。这个规则写死在OrderService的一个常量类里实际商业产品肯定要支持动态计价规则配置但作为学习项目清晰展示计价逻辑就够了public BigDecimal calculateRealPrice(BigDecimal distance) { BigDecimal basePrice new BigDecimal(10.00); BigDecimal baseDistance new BigDecimal(3); BigDecimal unitPrice new BigDecimal(2.5); if (distance.compareTo(baseDistance) 0) { return basePrice; } BigDecimal extraDistance distance.subtract(baseDistance); return basePrice.add(extraDistance.multiply(unitPrice)).setScale(2, RoundingMode.HALF_UP); }注意BigDecimal在金额计算中的使用规范不能直接用double做乘除法否则会产出0.30000000000000004这类精度问题。计算完要设置舍入模式为HALF_UP保留两位小数这就是金融领域金额计算的基本要求。司机的行程管理和收入统计也比较直白通过driver_id从t_order表查已完成订单汇总real_price字段。这一步可以顺带练一下MyBatis的聚合查询SQL例如按天统计收入select idselectDailyIncome resultTypemap SELECT DATE_FORMAT(finish_time, %Y-%m-%d) as day, SUM(real_price) as income FROM t_order WHERE driver_id #{driverId} AND status 3 GROUP BY DATE_FORMAT(finish_time, %Y-%m-%d) ORDER BY day DESC /select4.3 管理后台司机审核与数据统计管理后台用的是独立的admin表管理员账号在初始化SQL里手动预置。审核司机这个功能核心是更新driver表的status字段把待审核(0)改为正常(1)或禁用(2)。数据统计模块我做了三个核心指标每日订单量、每日平台GMV订单总额、司机完单排行。统计实现有两种路径实时SQL查询和定时汇总。考虑到项目数据量不会很大就直接用SQL实时聚合查询了。SQL里用到了DATE_FORMAT、GROUP BY、SUM、COUNT、ORDER BY这些基础但常用的聚合语法写完这部分的Mapper基本能把SQL中级查询练熟。管理后台的权限控制比用户端严格一层所有的/admin/**请求都要经过AdminInterceptor先判断是否登录再判断Session里的角色是否是管理员。如果直接用乘客身份访问管理接口拦截器直接返回403或者重定向到登录页。4.4 订单匹配的简化实现真实网约车平台的订单调度是一个很复杂的问题涉及地理位置搜索、并发抢单、派单策略、风控等多个维度。这个项目我采用的是乘客发单 候选司机抢单的简化方案。具体做法是订单创建时状态为待接单司机端提供一个可抢订单列表接口查询条件是订单状态为0、创建时间在最近10分钟内、且当前司机不在该订单的拒绝列表中。司机看到列表后点击抢单进入前面说的乐观锁更新流程。为什么不做自动派单因为自动派单需要维护司机实时地理位置、计算距离排序、解决拒绝派单的惩罚机制这些在SSM阶段会让项目复杂度指数上升。做毕设或者练手先把抢单这条链路跑通后续有精力再用WebSocket或者定时任务去扩展自动派单逻辑是比较切合实际的规划。这里也可以扩展讲一下候选司机列表SQL的实现距离计算可以用经纬度球面距离公式Haversine公式在SQL里通过MySQL的数学函数直接计算例如SELECT *, 6371 * ACOS( SIN(RADIANS(#{lat})) * SIN(RADIANS(start_lat)) COS(RADIANS(#{lat})) * COS(RADIANS(start_lat)) * COS(RADIANS(#{lng}) - RADIANS(start_lng)) ) AS distance_km FROM t_order WHERE status 0 AND create_time (NOW() - INTERVAL 10 MINUTE) AND driver_id IS NULL ORDER BY distance_km LIMIT 20不过这个计算在MySQL里执行时无法走索引数据量大了以后性能会下降学习项目里用没问题实际项目会用GeoHash或空间索引来优化。这块能讲清楚面试官会觉得你是真的理解性能瓶颈的。5. 环境部署与常见问题排查实录5.1 本地部署完整步骤如果要把这套项目从Git仓库拉下来在自己电脑上跑通我按步骤整理了一份操作流程环境是Windows 10 IDEA 2021安装JDK 1.8并配置JAVA_HOME环境变量命令行执行java -version能输出版本号则成功。安装Maven并配置本地仓库路径和阿里云镜像源。settings.xml里配置镜像避免依赖下载卡顿这个步骤能省下大量等待时间。安装MySQL 5.7设置root密码。然后执行项目提供的ride_hailing.sql初始化脚本创建数据库和所有表结构同时插入管理员账号和若干测试数据。修改jdbc.properties里的数据库连接信息重点检查url、用户名、密码是否正确。如果你的MySQL是8.0版本驱动类名和连接url里需要加上时区参数否则会报错我下面会在问题清单里写详细说明。IDEA打开项目等待Maven下载依赖。这一步很关键不要直接点击运行先执行mvn clean compile确认编译通过后再继续。配置Tomcat 8.5。IDEA里点击Run Configuration选择Tomcat Server - Local部署war包或者直接选择exploded模式设置Application context为/然后启动。浏览器访问http://localhost:8080/如果能跳到登录页面说明整个安装部署流程已经打通。5.2 高频报错与排查速查表我在调试项目时踩过不少坑把最有代表性的几个列出来这些在SSM项目里出现频率极高看一眼基本能定位问题报错现象根因分析解决方案启动Tomcat时报ClassNotFoundException: DispatcherServletTomcat用的是JDK自带的JRE没找到Spring的jar包检查Artifacts里是否将Spring依赖以lib形式打进了WEB-INF/libIDEA中要右键Artifacts选择Put into Output RootMapper接口方法找不到报Invalid bound statement (not found)Mapper接口和XML文件的namespace或id不匹配或mapperLocations没扫到XML检查XML文件是否在resources/mapper目录下namespace必须等于Mapper接口全限定名数据库连接报Public Key Retrieval is not allowedMySQL 8.0默认使用caching_sha2_password认证jdbc url加参数allowPublicKeyRetrievaltrue和useSSLfalse查询结果中文乱码数据库连接url没指定characterEncoding或JSP页面编码设置不一致url加characterEncodingutf8JSP页面meta和pageEncoding统一用UTF-8事务不生效数据异常未回滚Service方法被同类内部调用或者没走Spring代理类内部调用改为注入自身代理或者把需要事务的逻辑拆分到不同Service中页面报404但Controller路径看起来没错没有加ResponseBody且返回字符串被当成视图名确认是返回JSON还是页面返回JSON的方法必须加ResponseBodyDruid监控页面看不到SQL统计Druid的WebStatFilter没配置web.xml里配置DruidWebStatFilter并配置StatViewServlet使用Invalid bound statement这个问题的时候还有一个很隐蔽的坑Maven默认只把resources目录下的文件打进classes如果你的Mapper XML放在java源码目录下编译后不会进入target/classes从而报找不到绑定语句。解决方式是把XML放resources目录或者在pom.xml里配置buildresources把xml文件也包含进去。5.3 从项目实战中总结的避坑心得做完这个项目后我最大的感受是SSM系统的难点不在某一个单独框架使用上而在三个框架配合时对细节的把控。这里分享几条我实际踩完坑后的心得。第一写代码前一定要先把数据库表设计到位。这个网约车项目我前前后后改了三次表结构最痛苦的一次是订单状态字段一开始用字符串存中文结果后续统计、筛选、状态流转全都不方便改回数字枚举时又得连带改前端逻辑。表设计定下来以后Service层的接口设计基本就是水到渠成的事。第二Controller层别堆业务逻辑。业务逻辑全部下沉到Service层Controller只做参数接收、校验、调用Service、返回结果这几件事。这样后面写单元测试、改功能都不需要动Controller。这在SSM项目评审时也是被问得比较多的设计问题提前做好分层能省去重构的痛苦。第三学会使用统一的返回结果对象。我定义了一个Result类包含code、message、data三个字段。所有Controller方法的返回类型都是Result成功返回Result.success(data)失败返回Result.error(message)。这个习惯让项目统一性大幅提升前端处理也简单这也是从模拟真实企业项目实践中总结的经验。第四勤用日志而不是System.out。因为SSM项目里多个模块相互调用如果每个关键节点都不打日志出了问题定位会非常痛苦。我在Service层的关键操作都加了log.info输出包括下单、接单、行程结束、支付成功等排查问题时看日志能直接还原操作链路。第五测试数据要设计得贴近真实场景。项目初始化SQL里我造了不同状态的订单有待接单的、行程中的、已完成的、已取消的这样前端页面一打开就能看到各种状态下的实际效果。建议你也按业务状态把测试数据铺满别只插几条状态一样的假数据不然页面调试时很多逻辑分支根本走不到。6. 后续可以怎么扩展这个平台等SSM版本的网约车平台完全跑通后如果你想继续在这个方向上深入有几个扩展方向值得考虑。第一引入WebSocket做实时订单推送。现在是司机手动刷新可抢订单列表体验比较初级。可以加上WebSocket在有新订单时服务端主动推送消息给在线司机。SSM项目里集成WebSocket不复杂Spring 5对WebSocket的原生支持也够用这算一个功能上很直观的升级。第二接入第三方地图API实现路径规划和费用预估。现在起终点经纬度是手动录入的如果接上高德地图的Web服务API可以根据起终点坐标真实计算驾车距离计价也更接近实际。同时前端可以接入地图JS API展示路线页面效果会好很多。第三引入Spring Security或Shiro统一做认证授权。项目目前用拦截器加Session做权限控制在只需要简单区分角色时完全够用。但如果你想学习更标准的安全框架集成可以在当前项目基础上替换权限控制模块实现基于角色的访问控制这也是面试中常被追问的知识点。第四把数据统计模块升级为ECharts可视化图表。管理后台目前是纯表格展示统计结果接入ECharts后可以按天展示订单量趋势图、司机完单排名柱状图、订单状态分布饼图等。用SSM做后端接口返回JSON数据前端用Ajax请求ECharts渲染这一套组合在写简历和答辩展示时都很加分。我在自己动手做这个项目的过程中折腾最深的就是订单并发抢单那一段因为要同时想清楚数据库锁机制、事务隔离级别和业务流程三者之间的配合。后来理清了状态条件更新这种乐观锁思路再回头看整个系统发现每个模块其实都是围绕数据状态在转。如果你也正在做同类项目建议先把订单表的状态机和并发更新SQL想明白再去写别的功能这样整体推进会顺畅很多。最后再分享一个小技巧初始化数据时在订单表里多插几条处于不同状态的数据调前端页面的时候会省下大量来回操作的时间。本文还有配套的精品资源点击获取