去年夏天,我接手了一个自助游网站的开发项目。客户希望打造一个能够整合旅游资源、提供个性化行程规划的在线平台。作为一个有着多年Java开发经验的工程师,我决定采用Spring Boot作为基础框架,结合Vue.js前端技术栈,构建一个高性能、易扩展的旅游服务平台。
这个项目最吸引我的地方在于,它不仅仅是一个简单的信息展示网站,而是需要处理复杂的业务逻辑:实时库存管理、动态路线规划、多维度搜索过滤等。在开发过程中,我遇到了不少挑战,也积累了许多宝贵的经验。今天,我就把这个项目的完整开发过程分享给大家,希望能为正在开发类似系统的同行提供参考。
选择Java作为后端开发语言主要基于以下几个考虑:
具体技术组件如下:
提示:在选择Spring Boot版本时,建议使用长期支持(LTS)版本,避免使用最新的非LTS版本,以减少潜在的兼容性问题。
前端采用Vue 3 + Element Plus的组合,主要考虑因素包括:
系统采用前后端分离的微服务架构,主要分为以下几个服务模块:
code复制旅游服务系统架构
├── 用户服务 (User Service)
├── 产品服务 (Product Service)
├── 订单服务 (Order Service)
├── 支付服务 (Payment Service)
├── 搜索服务 (Search Service)
└── 推荐服务 (Recommendation Service)
每个服务都独立部署,通过Spring Cloud Gateway进行统一API路由,使用Nacos作为服务注册与配置中心。这种架构设计使得系统具备良好的扩展性和可维护性。
用户系统采用RBAC(基于角色的访问控制)模型,主要包含三种角色:
认证流程实现如下:
java复制// JWT Token生成示例
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put("roles", userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList()));
return Jwts.builder()
.setClaims(claims)
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + JWT_TOKEN_VALIDITY * 1000))
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
注意:在实际项目中,务必将JWT密钥存储在安全的地方,如配置中心或密钥管理服务,切勿硬编码在代码中。
产品服务负责管理所有旅游相关的产品信息,包括:
数据库设计采用MySQL,主要表结构如下:
| 表名 | 主要字段 | 说明 |
|---|---|---|
| product | id, name, type, price, inventory | 产品基础信息 |
| product_detail | id, description, images, features | 产品详情 |
| product_schedule | id, product_id, date, available | 产品日程库存 |
java复制// MyBatis-Plus 分页查询实现
public Page<ProductVO> searchProducts(ProductQuery query) {
Page<Product> page = new Page<>(query.getPageNum(), query.getPageSize());
LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<>();
if (StringUtils.isNotBlank(query.getKeyword())) {
wrapper.like(Product::getName, query.getKeyword());
}
if (query.getType() != null) {
wrapper.eq(Product::getType, query.getType());
}
if (query.getMinPrice() != null && query.getMaxPrice() != null) {
wrapper.between(Product::getPrice, query.getMinPrice(), query.getMaxPrice());
}
return productMapper.selectPage(page, wrapper)
.convert(this::convertToVO);
}
订单系统是整个平台最复杂的模块之一,需要处理:
我们采用乐观锁解决库存并发问题:
sql复制UPDATE product_schedule
SET available = available - 1
WHERE id = #{scheduleId} AND available >= #{quantity}
订单状态机设计如下:
code复制订单状态流转图
[待支付] --支付成功--> [已支付]
[待支付] --超时未支付--> [已取消]
[已支付] --商家确认--> [已确认]
[已确认] --完成服务--> [已完成]
[已支付] --用户退款--> [退款中]
[退款中] --商家同意--> [已退款]
为提高系统响应速度,我们实施了多级缓存:
java复制// 缓存注解使用示例
@Cacheable(value = "products", key = "#id", unless = "#result == null")
public Product getProductById(Long id) {
return productMapper.selectById(id);
}
@CacheEvict(value = "products", key = "#product.id")
public void updateProduct(Product product) {
productMapper.updateById(product);
}
旅游产品搜索具有以下特点:
我们使用Elasticsearch构建搜索服务,索引设计如下:
json复制{
"mappings": {
"properties": {
"name": {"type": "text", "analyzer": "ik_max_word"},
"description": {"type": "text", "analyzer": "ik_max_word"},
"city": {"type": "keyword"},
"price": {"type": "double"},
"rating": {"type": "double"},
"tags": {"type": "keyword"},
"location": {"type": "geo_point"}
}
}
}
针对旅游旺季可能出现的流量高峰,我们采取了以下措施:
系统采用Docker + Kubernetes的部署方案,主要优势:
yaml复制# deployment.yaml示例
apiVersion: apps/v1
kind: Deployment
metadata:
name: user-service
spec:
replicas: 3
selector:
matchLabels:
app: user-service
template:
metadata:
labels:
app: user-service
spec:
containers:
- name: user-service
image: registry.example.com/tour/user-service:1.0.0
ports:
- containerPort: 8080
resources:
limits:
cpu: "1"
memory: 1Gi
requests:
cpu: "0.5"
memory: 512Mi
为确保系统稳定运行,我们建立了完整的监控体系:
关键监控指标包括:
在初期部署时,我们发现订单创建时间比实际时间慢了8小时。这是因为Docker容器默认使用UTC时区,而我们的服务器位于东八区。解决方案:
dockerfile复制# Dockerfile中添加时区配置
RUN apk add --no-cache tzdata
ENV TZ=Asia/Shanghai
在使用@Transactional注解时,发现事务没有生效。排查后发现是因为在同一个类中方法调用导致的代理失效。解决方法:
java复制// 方法1:拆分到不同类
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderHelper orderHelper;
public void createOrder() {
// ...
orderHelper.deductInventory();
}
}
@Service
class OrderHelper {
@Transactional
public void deductInventory() {
// ...
}
}
// 方法2:使用AopContext
((OrderService) AopContext.currentProxy()).deductInventory();
当产品信息更新时,可能会出现缓存与数据库不一致的情况。我们采用"先更新数据库,再删除缓存"的策略,并结合消息队列确保最终一致性。
java复制public void updateProduct(Product product) {
// 1. 更新数据库
productMapper.updateById(product);
// 2. 删除缓存
redisTemplate.delete("product:" + product.getId());
// 3. 发送消息通知其他服务
rabbitTemplate.convertAndSend("product.update", product.getId());
}
经过三个月的开发和优化,这个自助游网站项目最终成功上线。系统目前支持日均10万UV,核心接口响应时间在200ms以内,订单处理能力达到1000TPS,完全满足了客户的业务需求。
在实际开发中,我深刻体会到几个关键点:
未来可以考虑的优化方向: