在航空旅游行业数字化浪潮中,票务管理的效率与准确性直接关系到企业的运营成本与客户满意度。传统依赖纸质单据、电话沟通和人工核对的模式已难以应对日益增长的业务量和高时效性要求。针对这一行业痛点,一套基于SSH(Struts2 + Spring + Hibernate)整合框架的航空票务全流程管理系统应运而生,我们可将其命名为“翼途通”智慧票务平台。该系统通过将航班管理、在线预订、订单处理、财务对账等核心业务模块进行数字化整合,构建了一个集前台销售与后台管理于一体的自动化运营支撑体系。
技术架构选型与设计理念
系统采用经典的三层架构设计,实现了表现层、业务逻辑层与数据持久层的清晰分离,确保了系统的高内聚、低耦合特性。
表现层:基于Struts2框架构建,负责处理用户界面交互与请求路由。Struts2的拦截器机制有效处理了通用预处理逻辑,如用户身份验证、请求参数过滤与字符编码转换。其强大的标签库与模板技术简化了前端页面的开发,同时支持基于配置的页面流管理,使视图跳转逻辑清晰可控。
业务逻辑层:由Spring框架的IoC(控制反转)容器统一管理所有Service组件。通过依赖注入(DI)机制,业务组件间的依赖关系由容器动态注入,极大提升了代码的可测试性与可维护性。Spring的声明式事务管理(@Transactional)被应用于所有核心业务操作,如订单创建、票务状态更新等,确保了业务数据的一致性。
持久层:采用Hibernate作为ORM(对象关系映射)框架,将面向对象的领域模型与关系型数据库表结构进行映射。通过注解配置实体类与数据库表的对应关系,开发者可以以面向对象的方式操作数据,Hibernate自动生成高效的SQL语句,简化了数据库访问代码,并提供了缓存机制以提升查询性能。
整个技术栈的整合通过Spring进行统筹,Spring负责Struts2 Action对象与Hibernate SessionFactory的生命周期管理,以及事务的切面配置,形成了一个稳定、高效的企业级应用基础。
核心数据模型设计与业务逻辑体现
系统的数据模型设计紧密围绕票务业务的核心实体展开,共计6张核心表支撑着整个平台的运转。以下重点分析flight航班信息表与ticket_order机票订单表的设计。
1. 航班信息表(flight) 该表是系统的基石,存储了所有可售航班的核心信息。其DDL定义如下:
CREATE TABLE `flight` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`flight_number` varchar(50) NOT NULL COMMENT '航班号',
`departure_city` varchar(100) NOT NULL COMMENT '出发城市',
`arrival_city` varchar(100) NOT NULL COMMENT '到达城市',
`departure_time` datetime NOT NULL COMMENT '计划起飞时间',
`arrival_time` datetime NOT NULL COMMENT '计划到达时间',
`aircraft_type` varchar(50) DEFAULT NULL COMMENT '机型',
`total_seats` int(11) NOT NULL DEFAULT '0' COMMENT '总座位数',
`available_seats` int(11) NOT NULL DEFAULT '0' COMMENT '可售座位数',
`ticket_price` decimal(10,2) NOT NULL COMMENT '票面价格',
`status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '航班状态:1-正常,0-取消',
PRIMARY KEY (`id`),
KEY `idx_departure_arrival` (`departure_city`,`arrival_city`),
KEY `idx_departure_time` (`departure_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='航班信息表';
设计亮点分析:
- 业务完整性约束:字段设计涵盖了航班运营所需的全部关键信息,从基础的航班号、起降城市与时间,到运营相关的机型、座位数和票价。
status字段提供了航班生命周期管理的能力。 - 数据一致性保障:
total_seats与available_seats的分离设计是库存管理的核心。通过应用程序逻辑确保available_seats始终小于等于total_seats,并在用户下单时进行原子性更新,防止超售。 - 查询性能优化:针对最频繁的查询场景——按起降城市和起飞时间搜索,建立了复合索引
idx_departure_arrival和单列索引idx_departure_time,能够快速定位符合条件的航班,提升用户查询体验。
2. 机票订单表(ticket_order) 该表记录了所有的交易记录,是系统财务与客户服务的核心。
CREATE TABLE `ticket_order` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`order_number` varchar(64) NOT NULL COMMENT '订单号',
`flight_id` int(11) NOT NULL COMMENT '关联航班ID',
`passenger_name` varchar(100) NOT NULL COMMENT '乘客姓名',
`passenger_id_card` varchar(18) NOT NULL COMMENT '乘客身份证号',
`contact_phone` varchar(20) NOT NULL COMMENT '联系手机',
`seat_number` varchar(10) DEFAULT NULL COMMENT '座位号',
`order_amount` decimal(10,2) NOT NULL COMMENT '订单金额',
`order_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '订单状态:0-待支付,1-已支付,2-已出票,3-已取消,4-已退款',
`create_time` datetime NOT NULL COMMENT '订单创建时间',
`pay_time` datetime DEFAULT NULL COMMENT '支付时间',
`update_time` datetime DEFAULT NULL COMMENT '最后更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_order_number` (`order_number`),
KEY `idx_flight_id` (`flight_id`),
KEY `idx_create_time` (`create_time`),
CONSTRAINT `fk_order_flight` FOREIGN KEY (`flight_id`) REFERENCES `flight` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='机票订单表';
设计亮点分析:
- 订单状态机设计:
order_status字段清晰地定义了订单的完整生命周期(待支付 -> 已支付 -> 已出票 -> ...)。这种设计便于跟踪订单流向,并为后续的退改签业务逻辑提供了状态判断依据。 - 唯一性约束与关联完整性:
uk_order_number唯一索引确保了每个订单号的唯一性,防止重复订单产生。外键约束fk_order_flight保证了每张订单都必须对应一个真实存在的航班,维护了数据的参照完整性。 - 审计字段:
create_time,pay_time,update_time等字段记录了订单的关键时间节点,不仅用于业务逻辑(如判断是否超时未支付),也为后续的数据统计分析提供了时间维度。
核心功能模块实现深度解析
1. 智能航班查询与余票展示
前端用户输入出发地、目的地和日期后,系统通过FlightAction接收请求,并调用FlightService进行查询。其核心查询逻辑封装在Service层,利用Hibernate的HQL进行数据库交互。
// FlightService.java 中的核心查询方法
@Service
@Transactional
public class FlightService {
@Autowired
private FlightDao flightDao;
public List<Flight> searchFlights(String departureCity, String arrivalCity, Date departureDate) {
// 构造查询条件,精确到日期
Calendar calendar = Calendar.getInstance();
calendar.setTime(departureDate);
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
Date startDate = calendar.getTime();
calendar.add(Calendar.DAY_OF_MONTH, 1);
Date endDate = calendar.getTime();
String hql = "FROM Flight f WHERE f.departureCity = :depCity " +
"AND f.arrivalCity = :arrCity " +
"AND f.departureTime BETWEEN :start AND :end " +
"AND f.status = 1 AND f.availableSeats > 0 " +
"ORDER BY f.departureTime ASC";
Map<String, Object> params = new HashMap<>();
params.put("depCity", departureCity);
params.put("arrCity", arrivalCity);
params.put("start", startDate);
params.put("end", endDate);
return flightDao.find(hql, params);
}
}
该方法首先将前端传入的日期处理为当天的起始和结束时间点,然后使用HQL查询在此时间范围内、状态正常且仍有空余座位的航班,并按起飞时间排序。Hibernate会将此HQL转换为带参数绑定的SQL,有效防止SQL注入,并利用之前建立的索引进行高效查询。
上图展示了航班查询结果列表页面,清晰呈现了航班号、起降时间、机型、票价及最重要的余票信息,用户可直观地进行选择。
2. 事务安全的机票预订流程
机票预订是系统的核心交易环节,涉及航班余票更新和订单创建,必须在一个事务中完成,以确保数据一致性。该功能由OrderAction和OrderService协同完成。
// OrderAction.java 中处理预订请求
public class OrderAction extends ActionSupport {
private Integer flightId;
private String passengerName;
private String passengerIdCard;
// ... 其他订单参数
private OrderService orderService;
public String submitOrder() {
try {
TicketOrder newOrder = orderService.createOrder(flightId, passengerName, passengerIdCard, ...);
// 将订单信息放入值栈,供结果页面显示
// ...
return SUCCESS;
} catch (NoSeatAvailableException e) {
addActionError("抱歉,所选航班已无余票!");
return ERROR;
} catch (Exception e) {
addActionError("系统繁忙,请稍后再试!");
return ERROR;
}
}
// getter and setter ...
}
// OrderService.java 中的创建订单方法,展示了事务控制
@Service
public class OrderService {
@Autowired
private FlightDao flightDao;
@Autowired
private OrderDao orderDao;
@Transactional(rollbackFor = Exception.class) // 声明式事务,遇到任何异常都回滚
public TicketOrder createOrder(Integer flightId, String passengerName, ...) throws NoSeatAvailableException {
// 1. 查询并锁定航班记录(使用悲观锁或版本号乐观锁)
Flight flight = flightDao.getWithLock(flightId);
if (flight == null || flight.getAvailableSeats() <= 0) {
throw new NoSeatAvailableException("航班不存在或已无余票");
}
// 2. 生成唯一订单号
String orderNumber = generateOrderNumber();
// 3. 创建订单对象
TicketOrder order = new TicketOrder();
order.setOrderNumber(orderNumber);
order.setFlight(flight);
order.setPassengerName(passengerName);
// ... 设置其他订单属性
order.setOrderStatus(OrderStatus.PENDING_PAYMENT);
order.setCreateTime(new Date());
// 4. 减少航班余票
flight.setAvailableSeats(flight.getAvailableSeats() - 1);
flightDao.update(flight); // 更新航班信息
// 5. 保存订单
orderDao.save(order);
return order;
}
private String generateOrderNumber() {
// 生成规则:日期时间 + 随机数,确保唯一性
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
String timeStr = sdf.format(new Date());
int random = (int) ((Math.random() * 9 + 1) * 1000); // 4位随机数
return "ORD" + timeStr + random;
}
}
此段代码是系统高并发处理能力的核心。@Transactional注解确保了整个createOrder方法在一个数据库事务中执行。如果任何一个步骤失败(例如,在保存订单前系统异常),整个操作将会回滚,航班余票不会减少,避免了数据不一致。flightDao.getWithLock(flightId)的实现(例如使用SELECT ... FOR UPDATE)防止了在高并发下的超售问题。
用户在确认订单信息后,被引导至支付页面。页面清晰列出航班详情、乘客信息及应付总额,等待用户完成支付操作。
3. 后台订单管理与状态跟踪
后台管理人员需要对所有订单进行跟踪、审核和处理(如出票、退改签)。系统通过一个集中的OrderManageAction提供管理功能,并通过Hibernate的延迟加载机制高效关联查询订单与航班信息。
// OrderManageAction.java 中的订单列表查询
public class OrderManageAction extends ActionSupport {
private List<TicketOrder> orderList;
private OrderService orderService;
public String listAllOrders() {
orderList = orderService.findAllOrdersWithFlight();
return SUCCESS;
}
// 处理出票操作
public String issueTicket() {
Integer orderId = ...; // 从请求参数获取
try {
orderService.issueTicket(orderId);
addActionMessage("出票成功!");
} catch (BusinessException e) {
addActionError("出票失败:" + e.getMessage());
}
return listAllOrders(); // 重新查询列表
}
// ... getter and setter
}
// OrderService.java 中的出票业务逻辑
@Service
public class OrderService {
// ... 其他方法
@Transactional
public void issueTicket(Integer orderId) throws BusinessException {
TicketOrder order = orderDao.get(orderId);
if (order == null) {
throw new BusinessException("订单不存在");
}
if (order.getOrderStatus() != OrderStatus.PAID) {
throw new BusinessException("订单状态非已支付,无法出票");
}
// 分配座位号(这里简化处理,实际可能更复杂)
String seatNum = assignSeatNumber(order.getFlight());
order.setSeatNumber(seatNum);
order.setOrderStatus(OrderStatus.ISSUED);
order.setUpdateTime(new Date());
orderDao.update(order);
// 可能触发短信或邮件通知乘客
// notificationService.sendIssuedNotification(order);
}
// 查询所有订单及其关联的航班信息(解决N+1查询问题)
public List<TicketOrder> findAllOrdersWithFlight() {
String hql = "SELECT o FROM TicketOrder o LEFT JOIN FETCH o.flight ORDER BY o.createTime DESC";
return orderDao.find(hql, null);
}
}
在findAllOrdersWithFlight方法中,HQL使用了LEFT JOIN FETCH来急切加载订单关联的航班对象。这避免了在遍历订单列表时,为每个订单单独发送SQL查询其航班信息的“N+1查询问题”,显著提升了列表页面的加载性能。
后台订单管理界面以表格形式集中展示所有订单,管理员可清晰看到订单状态、金额、创建时间等,并可直接进行出票、取消等操作。
4. 数据统计与报表生成
为管理人员提供数据决策支持是系统的重要价值。销售统计模块通过复杂的HQL查询聚合数据,生成如航线营收、客座率等报表。
// StatService.java 中的销售统计方法
@Service
@Transactional(readOnly = true) // 统计查询设置为只读事务
public class StatService {
@Autowired
private OrderDao orderDao;
/**
* 按航线统计指定时间段的销售额和订单数
*/
public List<RouteStatDTO> getSalesStatByRoute(Date startDate, Date endDate) {
String hql = "SELECT new com.airticket.dto.RouteStatDTO(" +
"f.departureCity, f.arrivalCity, " +
"COUNT(o.id), SUM(o.orderAmount)) " +
"FROM TicketOrder o JOIN o.flight f " +
"WHERE o.createTime BETWEEN :start AND :end " +
"AND o.orderStatus IN (:validStatuses) " +
"GROUP BY f.departureCity, f.arrivalCity " +
"ORDER BY SUM(o.orderAmount) DESC";
Map<String, Object> params = new HashMap<>();
params.put("start", startDate);
params.put("end", endDate);
params.put("validStatuses", Arrays.asList(OrderStatus.PAID, OrderStatus.ISSUED)); // 只统计有效订单
return orderDao.find(hql, params);
}
/**
* 计算某航班的客座率
*/
public Double calculateOccupancyRate(Integer flightId) {
String hql = "SELECT f.totalSeats, COUNT(o.id) " +
"FROM Flight f LEFT JOIN TicketOrder o ON o.flight.id = f.id AND o.orderStatus IN (:validStatuses) " +
"WHERE f.id = :flightId " +
"GROUP BY f.id, f.totalSeats";
Map<String, Object> params = new HashMap<>();
params.put("flightId", flightId);
params.put("validStatuses", Arrays.asList(OrderStatus.PAID, OrderStatus.ISSUED));
List<Object[]> result = orderDao.find(hql, params);
if (result != null && !result.isEmpty()) {
Object[] row = result.get(0);
Integer totalSeats = (Integer) row[0];
Long soldSeats = (Long) row[1];
if (totalSeats > 0) {
return (soldSeats.doubleValue() / totalSeats) * 100;
}
}
return 0.0;
}
}
这里的HQL使用了构造函数表达式(new com...RouteStatDTO(...))直接将查询结果映射到自定义的数据传输对象(DTO)中,避免了将整个实体对象加载到内存,提升了统计查询的效率。@Transactional(readOnly = true)向数据库提示此为只读操作,某些数据库优化器可能会据此进行性能优化。
![销售