作为一名参与过多个城市轨道交通信息化项目的开发者,我深知传统地铁售票系统面临的痛点。去年参与某二线城市地铁票务系统升级时,亲眼目睹早高峰时段售票窗口排起的长龙——平均每位乘客需要花费3-5分钟完成购票,而使用移动支付的乘客仅需30秒。这种效率差异直接促使我们团队开始探索基于Spring Boot的全栈解决方案。
本系统采用经典的B/S架构,前端使用Thymeleaf模板引擎实现服务端渲染,后端基于Spring Boot 2.7整合了Spring Security认证体系。数据库选用MySQL 8.0,利用其JSON字段类型存储动态线路信息。特别值得一提的是,我们在票务计算模块创新性地应用了图论中的Dijkstra算法,实现跨线路最短路径规划和票价自动计算。
选择Spring Boot而非传统SSM框架主要基于三点考量:
数据库对比方案:
票务购买流程采用状态机模式设计:
code复制用户查询 → 选择车次 → 余额校验 → 生成订单 → 扣款 → 出票
关键状态转换逻辑:
java复制public enum OrderStatus {
INITIALIZED,
PAYING,
PAID,
CANCELLED,
COMPLETED;
public static boolean canTransfer(OrderStatus from, OrderStatus to) {
// 状态转换规则校验逻辑
}
}
sql复制CREATE TABLE `account` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`username` VARCHAR(32) UNIQUE NOT NULL,
`password` VARCHAR(64) NOT NULL COMMENT 'BCrypt加密存储',
`balance` DECIMAL(10,2) DEFAULT 0.00,
`avatar` VARCHAR(255) COMMENT 'OSS存储路径',
`phone` VARCHAR(20) COMMENT '用于短信通知',
`status` TINYINT DEFAULT 1 COMMENT '0-禁用 1-正常'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
sql复制CREATE TABLE `metro_line` (
`id` INT PRIMARY KEY,
`line_name` VARCHAR(50) NOT NULL,
`stations` JSON NOT NULL COMMENT '站点数组及坐标信息',
`first_train` TIME NOT NULL,
`last_train` TIME NOT NULL,
`interval_minutes` INT DEFAULT 5
);
针对高频查询场景建立复合索引:
sql复制-- 车次查询索引
ALTER TABLE `metro_schedule` ADD INDEX `idx_line_time` (`line_id`, `departure_time`);
-- 订单查询索引
ALTER TABLE `ticket_order` ADD INDEX `idx_user_status` (`user_id`, `status`);
基于邻接表构建地铁网络图:
java复制public class MetroGraph {
private Map<String, List<Edge>> adjacencyList = new HashMap<>();
class Edge {
String toStation;
int weight; // 站点间行驶时间(分钟)
}
public List<String> shortestPath(String from, String to) {
// Dijkstra算法实现
}
}
使用Spring声明式事务保证数据一致性:
java复制@Transactional
public TicketOrder purchaseTicket(Long userId, Long scheduleId) {
// 1. 校验余票
int remaining = ticketMapper.selectRemaining(scheduleId);
if(remaining <= 0) {
throw new BusinessException("票已售罄");
}
// 2. 扣减余额
BigDecimal price = scheduleMapper.selectPriceById(scheduleId);
accountMapper.deductBalance(userId, price);
// 3. 生成订单
TicketOrder order = new TicketOrder();
// ...订单构建逻辑
orderMapper.insert(order);
// 4. 余票扣减
ticketMapper.decreaseRemaining(scheduleId);
return order;
}
java复制public class XssFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
// 使用Jsoup清理HTML标签
String safeInput = Jsoup.clean(rawInput, Whitelist.basic());
}
}
sql复制CREATE TABLE `balance_log` (
`id` BIGINT PRIMARY KEY,
`user_id` BIGINT NOT NULL,
`amount` DECIMAL(10,2) NOT NULL,
`type` ENUM('RECHARGE','CONSUME','REFUND') NOT NULL,
`order_id` VARCHAR(32) COMMENT '关联订单号',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP
);
java复制public void confirmWithSms(Long userId, String operation) {
String code = smsService.sendVerifyCode(user.getPhone());
redisTemplate.opsForValue().set(
"confirm:" + userId + ":" + operation,
code, 5, TimeUnit.MINUTES);
}
使用Redis缓存热点数据:
java复制@Cacheable(value = "lineInfo", key = "#lineId")
public MetroLine getLineDetail(Integer lineId) {
return lineMapper.selectById(lineId);
}
@CacheEvict(value = "lineInfo", key = "#lineId")
public void updateLine(MetroLine line) {
lineMapper.updateById(line);
}
在application.yml中优化HikariCP参数:
yaml复制spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
Dockerfile示例:
dockerfile复制FROM openjdk:11-jre
COPY target/metro-ticket-1.0.0.jar /app.jar
EXPOSE 8080
ENTRYPOINT ["java","-jar","/app.jar"]
Spring Boot Actuator配置:
yaml复制management:
endpoints:
web:
exposure:
include: health,info,metrics
endpoint:
health:
show-details: always
java复制// 错误示范
double total = 0.1 + 0.2; // 结果将是0.30000000000000004
// 正确做法
BigDecimal total = new BigDecimal("0.1").add(new BigDecimal("0.2"));
java复制@Transactional
public void handleConcurrentOrder(Long orderId) {
// 使用SELECT FOR UPDATE加锁
Order order = orderMapper.selectForUpdate(orderId);
if(order.getStatus() != INITIALIZED) {
throw new BusinessException("订单状态异常");
}
// 后续处理...
}
java复制private static final Logger logger = LoggerFactory.getLogger(TicketService.class);
public void someMethod() {
logger.info("购票请求参数:{}",
LogUtil.maskSensitive(param));
}
这个项目让我深刻体会到,一个健壮的票务系统不仅需要完善的功能设计,更要考虑异常处理、数据一致性和系统扩展性。特别是在高并发场景下,如何平衡系统性能与数据准确性是需要持续优化的课题。