最近刚完成了一个基于SpringBoot的旅游网站项目,从需求分析到最终部署上线,踩了不少坑也积累了不少经验。这个系统整合了景点信息、酒店预订、美食推荐、游记分享等核心功能模块,采用前后端分离架构,前端使用Vue.js+Element UI,后端基于SpringBoot+MyBatis实现。下面我就从技术选型、核心功能实现、数据库设计、部署优化等方面,详细分享这个项目的开发历程。
提示:这个项目特别适合需要快速开发旅游类系统的开发者参考,包含了从零搭建到生产部署的全流程经验。
选择SpringBoot作为后端框架主要基于以下几个实际考量:
快速开发:SpringBoot的自动配置和起步依赖特性,让我们在两周内就搭建起了基础框架。比如通过简单的spring-boot-starter-web依赖就自动配置好了Tomcat和Spring MVC。
微服务友好:考虑到后期可能扩展为微服务架构,SpringBoot天然支持Spring Cloud生态,迁移成本低。我们在配置中预留了服务注册发现的接口。
生产就绪:内置的健康检查、指标监控等功能,大大减少了运维工作量。通过/actuator端点可以实时查看应用状态。
前端选择Vue.js+Element UI组合,主要看中:
code复制[前端层]
├── 用户门户(Vue.js)
└── 管理后台(Element UI)
[网关层]
└── Spring Cloud Gateway(预留)
[服务层]
├── 用户服务
├── 景点服务
├── 订单服务
└── 内容服务
[数据层]
├── MySQL(主业务数据)
└── Redis(缓存+会话)
虽然当前是单体架构,但代码层面已经做了模块化拆分,为后续微服务化预留了扩展点。
景点模块是系统的核心,采用了经典的三层架构:
Controller层处理前端请求,关键代码示例:
java复制@RestController
@RequestMapping("/attraction")
public class AttractionController {
@Autowired
private AttractionService attractionService;
@GetMapping("/search")
public Result<List<AttractionVO>> search(
@RequestParam String keyword,
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer size) {
return attractionService.search(keyword, page, size);
}
}
Service层实现业务逻辑,包含:
DAO层使用MyBatis-Plus增强:
java复制public interface AttractionMapper extends BaseMapper<Attraction> {
@Select("SELECT * FROM attraction WHERE status = 1 ORDER BY heat DESC LIMIT #{limit}")
List<Attraction> selectHotList(@Param("limit") int limit);
}
典型的购票流程涉及分布式事务问题,我们采用最终一致性方案:
关键点在于使用Redis分布式锁防止超卖:
java复制public boolean createOrder(OrderDTO dto) {
String lockKey = "attraction:" + dto.getAttractionId();
try {
// 获取分布式锁
boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (!locked) {
throw new BusinessException("当前访问人数过多,请稍后再试");
}
// 检查库存
Attraction attraction = attractionMapper.selectById(dto.getAttractionId());
if (attraction.getStock() < dto.getQuantity()) {
throw new BusinessException("库存不足");
}
// 扣减库存
attractionMapper.updateStock(dto.getAttractionId(), dto.getQuantity());
// 创建订单
Order order = convertToOrder(dto);
orderMapper.insert(order);
return true;
} finally {
// 释放锁
redisTemplate.delete(lockKey);
}
}
景点表(attraction)
sql复制CREATE TABLE `attraction` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL COMMENT '景点名称',
`type_id` int NOT NULL COMMENT '类型ID',
`location` varchar(255) NOT NULL COMMENT '地理位置',
`description` text COMMENT '详细描述',
`price` decimal(10,2) NOT NULL COMMENT '基础价格',
`stock` int NOT NULL DEFAULT '0' COMMENT '可售票数',
`heat` int NOT NULL DEFAULT '0' COMMENT '热度',
`cover_image` varchar(255) DEFAULT NULL COMMENT '封面图',
`status` tinyint NOT NULL DEFAULT '1' COMMENT '状态(0下架1上架)',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_type` (`type_id`),
KEY `idx_heat` (`heat`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
订单表(order)
sql复制CREATE TABLE `order` (
`id` bigint NOT NULL AUTO_INCREMENT,
`order_no` varchar(32) NOT NULL COMMENT '订单编号',
`user_id` bigint NOT NULL COMMENT '用户ID',
`attraction_id` bigint NOT NULL COMMENT '景点ID',
`quantity` int NOT NULL COMMENT '购买数量',
`total_amount` decimal(10,2) NOT NULL COMMENT '总金额',
`status` tinyint NOT NULL DEFAULT '0' COMMENT '状态(0待支付1已支付2已取消3已退款)',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_order_no` (`order_no`),
KEY `idx_user` (`user_id`),
KEY `idx_attraction` (`attraction_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
采用Docker Compose部署方案:
yaml复制version: '3'
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
volumes:
- ./mysql/data:/var/lib/mysql
- ./mysql/conf:/etc/mysql/conf.d
ports:
- "3306:3306"
redis:
image: redis:6
ports:
- "6379:6379"
volumes:
- ./redis/data:/data
backend:
build: ./backend
ports:
- "8080:8080"
depends_on:
- mysql
- redis
environment:
SPRING_PROFILES_ACTIVE: prod
分布式Session问题:初期使用Tomcat Session导致多实例间数据不一致,最终改用Redis存储Session。
缓存穿透:针对不存在的景点ID大量查询,我们采用布隆过滤器+空值缓存的组合方案。
支付超时处理:对接第三方支付时,设置合理的超时时间(建议15-30秒),并实现自动查询补偿机制。
图片存储:初期直接存数据库导致性能低下,后来改为对象存储(MinIO)+CDN加速。
重要提示:在开发支付功能时,一定要做好对账系统,我们曾经因为网络问题导致支付状态不同步,造成了资金损失。
虽然不能直接贴图,但可以描述几个典型界面设计要点:
景点列表页:
订单详情页:
后台管理系统:
这个项目从零开始到上线历时3个月,期间遇到了各种技术挑战,但最终成功支撑了日均5万的访问量。最大的体会是:旅游系统的核心在于数据的准确性和服务的稳定性,特别是在节假日流量高峰时,完善的监控和弹性扩容机制至关重要。