1. 项目概述与背景
出租车管理系统是城市公共交通信息化建设的重要组成部分。作为一名长期从事Java Web开发的工程师,我最近完成了一个基于SSM框架的出租车管理系统开发项目。这个系统采用Maven进行项目管理,整合了Spring、SpringMVC和MyBatis三大框架,前端使用HTML+CSS+JavaScript+JSP技术栈,数据库选用MySQL 5.7。
在实际开发过程中,我发现很多类似的教程只关注基础功能的实现,而忽略了实际业务场景中的复杂需求。本文将详细介绍这个系统的完整开发过程,包括技术选型、架构设计、核心功能实现以及开发中遇到的典型问题解决方案。
2. 技术栈选型与项目搭建
2.1 技术栈组成解析
选择合适的技术栈是项目成功的基础。本系统采用以下技术组合:
- 后端框架:SSM(Spring+SpringMVC+MyBatis)
- Spring 5.x:提供IoC和AOP支持,管理Bean生命周期
- SpringMVC:处理Web请求和响应
- MyBatis 3.x:ORM框架,简化数据库操作
- 前端技术:
- JSP:动态页面渲染
- jQuery:简化DOM操作和AJAX请求
- Bootstrap:响应式页面布局
- 构建工具:Maven 3.6+,管理项目依赖
- 数据库:MySQL 5.7,关系型数据库
- 应用服务器:Tomcat 8.5+
提示:选择SSM框架而非Spring Boot的原因是教学场景下需要更清晰地展示各层配置,而Spring Boot的自动配置会隐藏很多细节。
2.2 开发环境准备
确保开发环境正确配置是项目顺利开展的前提:
-
JDK安装:
bash复制# 检查Java版本 java -version # 应为1.8.x -
Maven配置:
在settings.xml中配置阿里云镜像加速依赖下载:xml复制<mirror> <id>aliyunmaven</id> <mirrorOf>*</mirrorOf> <name>阿里云公共仓库</name> <url>https://maven.aliyun.com/repository/public</url> </mirror> -
数据库准备:
sql复制CREATE DATABASE taxi_management DEFAULT CHARACTER SET utf8mb4;
2.3 项目结构设计
合理的项目结构能显著提高开发效率。以下是核心目录结构:
code复制src/
├── main/
│ ├── java/
│ │ └── com/
│ │ └── taxi/
│ │ ├── controller/ # 控制器层
│ │ ├── service/ # 业务逻辑层
│ │ ├── dao/ # 数据访问层
│ │ ├── entity/ # 实体类
│ │ └── util/ # 工具类
│ ├── resources/
│ │ ├── spring/ # Spring配置
│ │ ├── mybatis/ # MyBatis映射文件
│ │ └── application.yml # 应用配置
│ └── webapp/
│ ├── WEB-INF/
│ │ └── views/ # JSP页面
│ └── static/ # 静态资源
└── test/ # 测试代码
3. 数据库设计与实现
3.1 核心表结构设计
根据出租车管理业务需求,设计了以下主要表结构:
3.1.1 车辆信息表(cars)
sql复制CREATE TABLE `cars` (
`car_id` int(11) NOT NULL AUTO_INCREMENT,
`car_number` varchar(20) NOT NULL COMMENT '车牌号',
`brand` varchar(50) DEFAULT NULL COMMENT '品牌',
`model` varchar(50) DEFAULT NULL COMMENT '型号',
`purchase_date` date DEFAULT NULL COMMENT '购买日期',
`engine_number` varchar(50) DEFAULT NULL COMMENT '发动机号',
`status` tinyint(4) DEFAULT '1' COMMENT '状态:1-运营中 2-维修中 3-已报废',
`driver_id` int(11) DEFAULT NULL COMMENT '当前司机ID',
`photo` varchar(255) DEFAULT NULL COMMENT '车辆照片',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`car_id`),
UNIQUE KEY `idx_car_number` (`car_number`),
KEY `idx_driver_id` (`driver_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
3.1.2 司机信息表(drivers)
sql复制CREATE TABLE `drivers` (
`driver_id` int(11) NOT NULL AUTO_INCREMENT,
`driver_name` varchar(50) NOT NULL COMMENT '司机姓名',
`id_card` varchar(18) NOT NULL COMMENT '身份证号',
`phone` varchar(20) DEFAULT NULL COMMENT '联系电话',
`license_type` varchar(10) DEFAULT 'C1' COMMENT '驾照类型',
`license_expire` date DEFAULT NULL COMMENT '驾照到期日',
`status` tinyint(4) DEFAULT '1' COMMENT '状态:1-在职 2-休假 3-离职',
`photo` varchar(255) DEFAULT NULL COMMENT '司机照片',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`driver_id`),
UNIQUE KEY `idx_id_card` (`id_card`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
3.1.3 维修保养记录表(cars_repair)
sql复制CREATE TABLE `cars_repair` (
`cars_repair_id` int(11) NOT NULL AUTO_INCREMENT,
`car_id` int(11) NOT NULL COMMENT '车辆ID',
`cars_repair_type` tinyint(4) DEFAULT NULL COMMENT '类型:1-日常保养 2-故障维修',
`cars_repair_text` text COMMENT '维修内容',
`cost` decimal(10,2) DEFAULT '0.00' COMMENT '费用',
`repair_date` date DEFAULT NULL COMMENT '维修日期',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`cars_repair_id`),
KEY `idx_car_id` (`car_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
3.2 数据库连接配置
在application.yml中配置数据库连接:
yaml复制spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/taxi_management?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8
username: root
password: yourpassword
mybatis:
mapper-locations: classpath:mybatis/*.xml
type-aliases-package: com.taxi.entity
4. 核心功能实现
4.1 车辆管理模块
4.1.1 车辆信息CRUD实现
控制器层代码示例:
java复制@Controller
@RequestMapping("/cars")
public class CarsController {
@Autowired
private CarsService carsService;
/**
* 分页查询车辆信息
*/
@RequestMapping(value="/queryAllCars", produces="text/html;charset=UTF-8")
@ResponseBody
public Pager<Car> queryAllCars(
@RequestParam(defaultValue="1") Integer page,
@RequestParam(defaultValue="10") Integer rows) {
PageInfo<Car> pageInfo = carsService.queryPageList(page, rows);
return new Pager<>(pageInfo.getTotal(), pageInfo.getList());
}
/**
* 保存或更新车辆信息
*/
@RequestMapping(value="/saveUpdateCars", produces="text/html;charset=UTF-8")
@ResponseBody
public String saveUpdateCars(
Car car,
@RequestParam MultipartFile carPhoto,
HttpServletRequest request) {
try {
// 处理上传的车辆照片
if(!carPhoto.isEmpty()) {
String photoPath = FileUploadUtil.uploadFile(carPhoto, request);
car.setPhoto(photoPath);
}
if(car.getCarId() == null) {
carsService.save(car);
} else {
carsService.update(car);
}
return "true";
} catch (Exception e) {
logger.error("保存车辆信息失败", e);
return "false";
}
}
}
4.1.2 文件上传工具类
java复制public class FileUploadUtil {
public static String uploadFile(MultipartFile file, HttpServletRequest request)
throws IOException {
if(file.isEmpty()) {
return null;
}
// 获取上传目录
String uploadDir = request.getServletContext().getRealPath("/uploads");
File dir = new File(uploadDir);
if(!dir.exists()) {
dir.mkdirs();
}
// 生成唯一文件名
String originalName = file.getOriginalFilename();
String fileExt = originalName.substring(originalName.lastIndexOf("."));
String newFileName = UUID.randomUUID().toString() + fileExt;
// 保存文件
File dest = new File(dir, newFileName);
file.transferTo(dest);
return "/uploads/" + newFileName;
}
}
4.2 司机管理模块
4.2.1 司机信息关联查询
java复制@Controller
@RequestMapping("/drivers")
public class DriversController {
@Autowired
private DriversService driversService;
/**
* 分页查询司机信息(带关联车辆信息)
*/
@RequestMapping(value="/queryDriversWithCars", produces="text/html;charset=UTF-8")
@ResponseBody
public Pager<DriverVO> queryDriversWithCars(
@RequestParam(defaultValue="1") Integer page,
@RequestParam(defaultValue="10") Integer rows) {
PageInfo<Driver> pageInfo = driversService.queryPageList(page, rows);
// 转换为VO对象,包含关联的车辆信息
List<DriverVO> voList = pageInfo.getList().stream().map(driver -> {
DriverVO vo = new DriverVO();
BeanUtils.copyProperties(driver, vo);
// 查询关联的车辆信息
Car car = carsService.queryByDriverId(driver.getDriverId());
if(car != null) {
vo.setCarNumber(car.getCarNumber());
vo.setCarBrand(car.getBrand());
}
return vo;
}).collect(Collectors.toList());
return new Pager<>(pageInfo.getTotal(), voList);
}
}
4.2.2 司机-车辆关联关系维护
java复制@Service
public class DriversServiceImpl implements DriversService {
@Autowired
private CarsDao carsDao;
@Override
@Transactional
public void assignCarToDriver(Integer driverId, Integer carId) {
// 1. 检查司机和车辆是否存在
Driver driver = driversDao.selectById(driverId);
if(driver == null) {
throw new RuntimeException("司机不存在");
}
Car car = carsDao.selectById(carId);
if(car == null) {
throw new RuntimeException("车辆不存在");
}
// 2. 解除车辆原有分配
if(car.getDriverId() != null) {
Car oldDriverCar = new Car();
oldDriverCar.setCarId(carId);
oldDriverCar.setDriverId(null);
carsDao.update(oldDriverCar);
}
// 3. 建立新关联
Car updateCar = new Car();
updateCar.setCarId(carId);
updateCar.setDriverId(driverId);
carsDao.update(updateCar);
}
}
4.3 维修保养管理模块
4.3.1 维修记录关联查询
java复制public class CarsRepairShow {
private Integer carsRepairId;
private Integer carsRepairType;
private String carsRepairText;
private Integer carId;
private Date createTime;
private String carNumber; // 车辆牌号
private String driverName; // 司机姓名
// 构造方法、getter/setter省略
}
@Controller
@RequestMapping("/carsRepair")
public class CarsRepairController {
@Autowired
private CarsRepairService carsRepairService;
@RequestMapping(value="/queryAll", produces="text/html;charset=UTF-8")
@ResponseBody
public Pager<CarsRepairShow> queryAll(
@RequestParam(defaultValue="1") Integer page,
@RequestParam(defaultValue="10") Integer rows) {
PageInfo<CarsRepair> pageInfo = carsRepairService.queryPageList(page, rows);
List<CarsRepairShow> showList = pageInfo.getList().stream()
.map(this::convertToShow)
.collect(Collectors.toList());
return new Pager<>(pageInfo.getTotal(), showList);
}
private CarsRepairShow convertToShow(CarsRepair repair) {
CarsRepairShow show = new CarsRepairShow();
BeanUtils.copyProperties(repair, show);
// 查询关联的车辆信息
Car car = carsService.queryById(repair.getCarId());
if(car != null) {
show.setCarNumber(car.getCarNumber());
// 查询关联的司机信息
if(car.getDriverId() != null) {
Driver driver = driversService.queryById(car.getDriverId());
if(driver != null) {
show.setDriverName(driver.getDriverName());
}
}
}
return show;
}
}
5. 系统安全与权限控制
5.1 用户认证与授权
5.1.1 登录认证实现
java复制@Controller
@RequestMapping("/auth")
public class AuthController {
@Autowired
private UsersService usersService;
@PostMapping("/login")
@ResponseBody
public ResponseEntity<Map<String, Object>> login(
@RequestParam String username,
@RequestParam String password,
HttpSession session) {
Map<String, Object> result = new HashMap<>();
Users user = usersService.authenticate(username, password);
if(user == null) {
result.put("success", false);
result.put("message", "用户名或密码错误");
return ResponseEntity.ok(result);
}
// 设置会话信息
session.setAttribute("currentUser", user);
session.setAttribute("userRole", user.getRole());
result.put("success", true);
result.put("redirectUrl", determineRedirectUrl(user.getRole()));
return ResponseEntity.ok(result);
}
private String determineRedirectUrl(String role) {
switch(role) {
case "ADMIN":
return "/admin/dashboard";
case "MANAGER":
return "/manager/dashboard";
default:
return "/driver/dashboard";
}
}
}
5.1.2 权限拦截器实现
java复制public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
// 获取请求路径
String uri = request.getRequestURI();
// 允许公开访问的路径
if(uri.startsWith("/static/") || uri.startsWith("/auth/")) {
return true;
}
// 检查会话
HttpSession session = request.getSession(false);
if(session == null || session.getAttribute("currentUser") == null) {
response.sendRedirect(request.getContextPath() + "/auth/login");
return false;
}
// 检查角色权限
String role = (String) session.getAttribute("userRole");
if(uri.startsWith("/admin/") && !"ADMIN".equals(role)) {
response.sendError(HttpServletResponse.SC_FORBIDDEN);
return false;
}
return true;
}
}
5.2 数据安全措施
5.2.1 SQL注入防护
使用MyBatis的参数化查询防止SQL注入:
xml复制<!-- 正确的参数化查询示例 -->
<select id="queryByCondition" resultType="Car">
SELECT * FROM cars
<where>
<if test="brand != null">
AND brand = #{brand}
</if>
<if test="status != null">
AND status = #{status}
</if>
</where>
ORDER BY car_id DESC
</select>
5.2.2 XSS防护
使用Spring的HtmlUtils进行HTML转义:
java复制public class XssFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
XssRequestWrapper wrappedRequest = new XssRequestWrapper(request);
filterChain.doFilter(wrappedRequest, response);
}
}
public class XssRequestWrapper extends HttpServletRequestWrapper {
public XssRequestWrapper(HttpServletRequest request) {
super(request);
}
@Override
public String getParameter(String name) {
String value = super.getParameter(name);
return HtmlUtils.htmlEscape(value);
}
@Override
public String[] getParameterValues(String name) {
String[] values = super.getParameterValues(name);
if(values == null) return null;
return Arrays.stream(values)
.map(HtmlUtils::htmlEscape)
.toArray(String[]::new);
}
}
6. 系统部署与优化
6.1 生产环境部署
6.1.1 Tomcat优化配置
在conf/server.xml中配置连接器:
xml复制<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443"
maxThreads="200"
minSpareThreads="10"
enableLookups="false"
acceptCount="100"
disableUploadTimeout="true"
compression="on"
compressionMinSize="2048"
compressableMimeType="text/html,text/xml,text/css,application/javascript"/>
6.1.2 数据库连接池配置
使用Druid连接池替代默认连接池:
yaml复制spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
druid:
initial-size: 5
min-idle: 5
max-active: 20
max-wait: 60000
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
validation-query: SELECT 1
test-while-idle: true
test-on-borrow: false
test-on-return: false
filters: stat,wall
6.2 性能优化实践
6.2.1 MyBatis二级缓存配置
xml复制<!-- 在mybatis-config.xml中 -->
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
<!-- 在Mapper XML中 -->
<cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>
6.2.2 静态资源缓存
配置Spring MVC静态资源缓存:
java复制@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**")
.addResourceLocations("classpath:/static/")
.setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS));
}
}
7. 常见问题与解决方案
7.1 开发阶段常见问题
7.1.1 中文乱码问题
问题现象:前端提交的中文数据在数据库中显示为乱码。
解决方案:
- 确保数据库、表和字段使用utf8mb4字符集
- 在JDBC连接URL中添加参数:
code复制jdbc:mysql://localhost:3306/db?useUnicode=true&characterEncoding=utf8 - 在web.xml中添加字符编码过滤器:
xml复制<filter> <filter-name>encodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> <init-param> <param-name>forceEncoding</param-name> <param-value>true</param-value> </init-param> </filter>
7.1.2 文件上传大小限制
问题现象:上传大文件时报错。
解决方案:
在application.yml中配置:
yaml复制spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 20MB
7.2 生产环境常见问题
7.2.1 数据库连接泄露
问题现象:系统运行一段时间后出现连接池耗尽。
解决方案:
- 使用Druid的监控功能检测泄漏:
java复制@Bean public ServletRegistrationBean<StatViewServlet> druidServlet() { ServletRegistrationBean<StatViewServlet> servlet = new ServletRegistrationBean<>(new StatViewServlet(), "/druid/*"); servlet.addInitParameter("loginUsername", "admin"); servlet.addInitParameter("loginPassword", "admin"); return servlet; } - 确保所有数据库操作都在try-with-resources或finally块中关闭连接
7.2.2 并发修改冲突
问题现象:多人同时修改同一条记录导致数据不一致。
解决方案:
使用乐观锁机制:
sql复制ALTER TABLE cars ADD COLUMN version INT DEFAULT 0;
在Mapper中:
xml复制<update id="updateCar">
UPDATE cars
SET
car_number = #{carNumber},
brand = #{brand},
version = version + 1
WHERE car_id = #{carId} AND version = #{version}
</update>
在Java代码中检查更新影响行数,如果为0表示并发冲突。
8. 项目扩展与改进方向
8.1 功能扩展建议
-
预约叫车功能:
- 添加乘客端微信小程序
- 实现实时位置共享
- 集成在线支付
-
车辆轨迹记录:
- 接入GPS设备数据
- 使用Redis存储实时位置
- 基于Elasticsearch实现轨迹查询
-
大数据分析:
- 使用Spark分析运营数据
- 生成热力图指导车辆调度
- 预测高峰时段和区域
8.2 技术架构升级
-
微服务化改造:
- 拆分为车辆服务、司机服务、订单服务等
- 使用Spring Cloud Alibaba套件
- 引入Nacos作为注册中心
-
前后端分离:
- 前端使用Vue.js或React
- 后端提供RESTful API
- 使用Swagger生成API文档
-
容器化部署:
- 使用Docker打包应用
- Kubernetes集群管理
- CI/CD自动化流水线
9. 开发经验与心得
在实际开发这个出租车管理系统的过程中,我积累了一些宝贵的经验:
-
数据库设计先行:良好的数据库设计是系统稳定性的基础。在开始编码前,我花了大量时间分析业务实体和关系,这大大减少了后期的结构调整。
-
合理的异常处理:在DAO层、Service层和Controller层采用不同的异常处理策略。例如,DAO层抛出原始异常,Service层转换为业务异常,Controller层统一处理并返回友好错误信息。
-
日志记录要全面:除了使用SLF4J记录常规日志外,对于关键业务操作(如车辆分配、维修记录变更)额外记录操作日志,便于后续审计。
-
接口设计原则:遵循RESTful风格设计API,使用HTTP状态码正确反映操作结果,响应体格式统一(如{code:200, data:{}, message:""})。
-
性能考量:在开发初期就考虑性能问题,如列表查询必须支持分页,关联查询注意N+1问题,频繁访问的数据考虑缓存等。
这个项目从技术角度来说不算复杂,但完整实现了一个可实际应用的出租车管理系统。对于初学者而言,通过这个项目可以掌握SSM框架的整合使用、前后端交互、数据库设计等核心技能。对于有经验的开发者,则可以在此基础上扩展更复杂的业务功能和技术组件。