1. 项目概述
这个基于SpringBoot2+Vue3的旅游网站系统,是我最近完成的一个全栈项目实战。采用当下主流的技术栈组合,实现了从景点展示、酒店预订到订单管理的完整业务流程。系统前后端完全分离,后端用SpringBoot2构建RESTful API,前端用Vue3开发响应式界面,数据库选用MySQL8.0,通过MyBatis-Plus简化数据操作。
在实际开发中,我发现这套技术组合有几个显著优势:SpringBoot的自动配置让后端服务快速启动;Vue3的Composition API使前端逻辑组织更清晰;MyBatis-Plus的Wrapper条件构造器大大简化了复杂查询的编写。系统还整合了JWT认证和Redis缓存,既保证了安全性又提升了性能。
2. 技术架构详解
2.1 后端技术栈
后端采用SpringBoot2.7作为基础框架,其自动配置特性让项目初始化变得非常简单。我在pom.xml中主要引入了这些依赖:
xml复制<dependencies>
<!-- Web支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MyBatis-Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3</version>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- JWT支持 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
</dependencies>
数据库连接配置在application.yml中:
yaml复制spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/travel_db?useSSL=false&serverTimezone=UTC
username: root
password: 123456
redis:
host: localhost
port: 6379
2.2 前端技术栈
前端选用Vue3+Element Plus组合,通过Vite构建工具获得极快的开发体验。项目初始化后,package.json中的关键依赖包括:
json复制"dependencies": {
"vue": "^3.2.47",
"element-plus": "^2.3.3",
"axios": "^1.3.4",
"vue-router": "^4.1.6",
"pinia": "^2.0.33"
}
前端工程结构采用模块化组织:
code复制src/
├── api/ # API请求封装
├── assets/ # 静态资源
├── components/ # 公共组件
├── router/ # 路由配置
├── stores/ # Pinia状态管理
├── utils/ # 工具函数
└── views/ # 页面组件
3. 核心功能实现
3.1 用户认证模块
采用JWT实现无状态认证,后端核心代码如下:
java复制@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private UserService userService;
@PostMapping("/login")
public Result login(@RequestBody LoginDTO dto) {
User user = userService.login(dto);
String token = JwtUtil.generateToken(user.getUserId());
return Result.success(token);
}
}
public class JwtUtil {
private static final String SECRET = "your-secret-key";
private static final long EXPIRATION = 86400L; // 24小时
public static String generateToken(Long userId) {
return Jwts.builder()
.setSubject(userId.toString())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION * 1000))
.signWith(SignatureAlgorithm.HS512, SECRET)
.compact();
}
}
前端在axios拦截器中添加token:
javascript复制// request拦截器
service.interceptors.request.use(
config => {
const token = localStorage.getItem('token')
if (token) {
config.headers['Authorization'] = 'Bearer ' + token
}
return config
},
error => {
return Promise.reject(error)
}
)
3.2 景点展示模块
后端采用MyBatis-Plus的Wrapper构建动态查询:
java复制@GetMapping("/list")
public Result listSpots(
@RequestParam(required = false) String keyword,
@RequestParam(required = false) Boolean isRecommended) {
LambdaQueryWrapper<ScenicSpot> wrapper = new LambdaQueryWrapper<>();
if (StringUtils.isNotBlank(keyword)) {
wrapper.like(ScenicSpot::getSpotName, keyword)
.or().like(ScenicSpot::getDescription, keyword);
}
if (isRecommended != null) {
wrapper.eq(ScenicSpot::getIsRecommended, isRecommended);
}
wrapper.orderByDesc(ScenicSpot::getCreateTime);
return Result.success(spotService.list(wrapper));
}
前端使用Element Plus的卡片组件展示景点:
vue复制<template>
<div class="spot-container">
<el-row :gutter="20">
<el-col
v-for="spot in spotList"
:key="spot.spotId"
:span="8">
<el-card :body-style="{ padding: '0px' }">
<img :src="spot.imageUrl" class="spot-image">
<div style="padding: 14px;">
<h3>{{ spot.spotName }}</h3>
<div class="spot-info">
<span>位置:{{ spot.location }}</span>
<span class="price">¥{{ spot.ticketPrice }}</span>
</div>
<el-button type="primary" @click="bookSpot(spot)">立即预订</el-button>
</div>
</el-card>
</el-col>
</el-row>
</div>
</template>
4. 数据库设计与优化
4.1 核心表结构
用户表(user_profile)添加了唯一索引:
sql复制ALTER TABLE user_profile
ADD UNIQUE INDEX idx_username (username),
ADD UNIQUE INDEX idx_email (email);
景点表(scenic_spot)建立了复合索引:
sql复制CREATE INDEX idx_location_recommend ON scenic_spot(location, is_recommended);
订单表(travel_order)设置了外键约束:
sql复制ALTER TABLE travel_order
ADD CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES user_profile(user_id),
ADD CONSTRAINT fk_spot FOREIGN KEY (spot_id) REFERENCES scenic_spot(spot_id);
4.2 缓存策略
使用Redis缓存热门景点数据:
java复制@Cacheable(value = "spots", key = "#spotId")
public ScenicSpot getSpotById(Long spotId) {
return baseMapper.selectById(spotId);
}
@CacheEvict(value = "spots", key = "#spot.spotId")
public void updateSpot(ScenicSpot spot) {
baseMapper.updateById(spot);
}
配置Redis缓存管理器:
java复制@Configuration
@EnableCaching
public class RedisConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(1))
.disableCachingNullValues();
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.transactionAware()
.build();
}
}
5. 部署与运维
5.1 后端部署
使用Docker打包SpringBoot应用:
dockerfile复制FROM openjdk:17-jdk-slim
VOLUME /tmp
COPY target/travel-system-0.0.1-SNAPSHOT.jar app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
构建并运行容器:
bash复制docker build -t travel-backend .
docker run -d -p 8080:8080 --name travel-backend-container travel-backend
5.2 前端部署
Nginx配置示例:
nginx复制server {
listen 80;
server_name yourdomain.com;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://backend:8080;
proxy_set_header Host $host;
}
}
6. 开发经验与技巧
6.1 前后端联调技巧
- Swagger文档集成:在后端添加springfox-swagger依赖,自动生成API文档
java复制@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.basePackage("com.example.travel"))
.paths(PathSelectors.any())
.build();
}
}
- Mock数据方案:前端开发阶段使用Mock.js模拟API响应
javascript复制import Mock from 'mockjs'
Mock.mock('/api/spots', 'get', {
'list|10': [{
'spotId|+1': 1,
'spotName': '@ctitle(5, 10)',
'location': '@county(true)',
'ticketPrice|50-500': 1,
'imageUrl': "@image('300x200', '#50B347', '#FFF', '景点')"
}]
})
6.2 性能优化实践
- 数据库连接池配置:
yaml复制spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 5
idle-timeout: 30000
max-lifetime: 1800000
connection-timeout: 30000
- 前端懒加载:Vue路由配置动态导入
javascript复制const routes = [
{
path: '/spots',
component: () => import('../views/SpotList.vue')
}
]
- 图片优化:使用WebP格式并实现懒加载
vue复制<img
v-lazy="convertToWebP(spot.imageUrl)"
alt="景点图片"
class="spot-image">
7. 常见问题解决方案
7.1 跨域问题处理
后端配置全局CORS:
java复制@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*")
.maxAge(3600);
}
}
7.2 接口幂等性保证
使用Redis实现token防重:
java复制@PostMapping("/order")
public Result createOrder(@RequestBody OrderDTO dto, HttpServletRequest request) {
String token = request.getHeader("Authorization").substring(7);
String key = "order:token:" + token;
if (redisTemplate.opsForValue().setIfAbsent(key, "1", 5, TimeUnit.MINUTES)) {
return orderService.createOrder(dto);
} else {
return Result.fail("请勿重复提交订单");
}
}
7.3 事务管理技巧
使用@Transactional注解时注意:
java复制@Service
public class OrderServiceImpl implements OrderService {
@Transactional(rollbackFor = Exception.class)
public void createOrder(OrderDTO dto) {
// 1. 扣减库存
spotService.reduceInventory(dto.getSpotId());
// 2. 创建订单
Order order = convertToEntity(dto);
orderMapper.insert(order);
// 3. 记录日志
logService.addOrderLog(order);
}
}
特别注意:@Transactional默认只对RuntimeException回滚,业务异常需要指定rollbackFor
8. 扩展功能建议
- 支付集成:接入支付宝/微信支付SDK
java复制public class PaymentService {
public String createAlipayOrder(Order order) {
AlipayClient alipayClient = new DefaultAlipayClient(
"https://openapi.alipay.com/gateway.do",
APP_ID,
APP_PRIVATE_KEY,
"json",
"UTF-8",
ALIPAY_PUBLIC_KEY,
"RSA2");
AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
request.setReturnUrl(returnUrl);
request.setNotifyUrl(notifyUrl);
JSONObject bizContent = new JSONObject();
bizContent.put("out_trade_no", order.getOrderNo());
bizContent.put("total_amount", order.getAmount());
bizContent.put("subject", "旅游景点门票");
request.setBizContent(bizContent.toString());
return alipayClient.pageExecute(request).getBody();
}
}
- 推荐算法:基于用户行为的简单推荐
java复制public List<ScenicSpot> recommendSpots(Long userId) {
// 1. 获取用户历史订单
List<Order> orders = orderService.getUserOrders(userId);
// 2. 提取常去地点
Set<String> locations = orders.stream()
.map(Order::getLocation)
.collect(Collectors.toSet());
// 3. 查询同地区推荐景点
return spotService.listByLocations(locations);
}
- Elasticsearch集成:实现景点全文搜索
java复制@Repository
public interface SpotSearchRepository extends ElasticsearchRepository<SpotEs, Long> {
List<SpotEs> findBySpotNameOrDescription(String spotName, String description);
}
@Service
public class SpotSearchService {
public List<SpotEs> search(String keyword) {
NativeSearchQuery query = new NativeSearchQueryBuilder()
.withQuery(QueryBuilders.multiMatchQuery(keyword, "spotName", "description"))
.build();
return elasticsearchTemplate.search(query, SpotEs.class).getContent();
}
}
这个旅游网站系统从技术选型到功能实现都采用了当前主流的技术方案,在实际开发中特别要注意前后端分离架构下的接口规范定义、JWT认证的安全实现以及Redis缓存的合理使用。我在开发过程中积累的这些经验,希望能帮助到正在开发类似项目的同行们。