1. 项目概述
作为一名经历过毕业设计"洗礼"的过来人,我深知一个完整的旅游系统开发要经历多少坑。这次要分享的是基于SSM+Vue的家乡旅游文化推广平台,这个选题非常巧妙——既符合计算机专业的技能考察要求,又能解决实际社会问题。系统整合了景点导览、酒店预订、票务购买、路线规划和特产电商五大功能模块,相当于打造了一个县域版的"携程+淘宝"综合体。
从技术架构来看,项目采用经典的SSM(Spring+SpringMVC+MyBatis)作为后端框架,配合Vue.js前端实现响应式交互。这种组合在高校毕设中非常普遍,但真正能把各组件用好的案例并不多见。我在开发过程中特别注重三个核心问题的解决:多源数据整合、高并发保障和前后端协同优化。下面会具体拆解每个环节的实现细节。
2. 核心需求解析
2.1 解决旅游信息碎片化问题
传统县域旅游最大的痛点就是信息分散——景点介绍在公众号、酒店预订在美团、特产销售在拼多多。我们的调研数据显示,游客规划一次县域旅行平均需要切换5.6个平台。系统采用"数据中台"思想,通过三种方式整合数据:
- 标准化接入:对合作景区/酒店提供REST API规范,要求返回统一JSON格式
- 爬虫补全:对不愿开放接口的商家,使用WebMagic爬虫抓取公开数据
- 人工录入:当地文旅局可后台补充非遗文化等特色内容
java复制// 示例:景点数据聚合服务
@Service
public class AttractionAggregateService {
@Autowired
private RestTemplate restTemplate;
public AttractionVO getAggregatedInfo(Long id) {
// 优先从Redis查询
AttractionVO cached = redisTemplate.opsForValue().get("att:"+id);
if(cached != null) return cached;
// 多源数据聚合
AttractionDTO official = officialApiClient.getById(id);
List<CommentDTO> comments = commentService.listByAttraction(id);
RealTimeStats stats = monitoringService.getStats(id);
// 组装VO对象
AttractionVO vo = new AttractionVO();
BeanUtils.copyProperties(official, vo);
vo.setComments(comments);
vo.setCurrentVisitor(stats.getVisitorCount());
// 缓存30分钟
redisTemplate.opsForValue().set("att:"+id, vo, 30, TimeUnit.MINUTES);
return vo;
}
}
2.2 特产电商与旅游流量的结合
系统创新性地将特产销售嵌入旅游动线中,实现"游中种草-离前下单-到家收货"的闭环。关键技术点包括:
- 地理位置触发推荐:当用户靠近特产产区时,APP推送限时优惠
- 游客画像匹配:根据浏览行为预测可能感兴趣的特产类别
- 库存动态显示:对接农户库存系统,实时显示剩余可售数量
特别注意:特产模块需要严格处理库存并发问题。我们采用Redis的DECR命令配合Lua脚本实现原子扣减,核心逻辑是先扣减缓存再异步同步到数据库。
2.3 高并发场景保障
五一、国庆等节假日会出现访问高峰,我们通过以下措施确保系统稳定:
-
前端优化:
- Vue组件懒加载
- 图片使用WebP格式+CDN分发
- 请求合并与防抖处理
-
后端防护:
- Nginx负载均衡(加权轮询算法)
- Sentinel熔断规则(慢调用比例>50%时触发)
- MySQL读写分离(使用Sharding-JDBC中间件)
-
缓存策略:
- 热点数据多级缓存(Guava -> Redis -> DB)
- 秒杀商品预库存预热
3. 技术实现细节
3.1 后端架构设计
SSM框架的整合看似简单,但合理的分层设计直接影响后期维护成本。我的项目结构如下:
code复制src/main/java
├── config/ # Spring配置类
├── controller/ # 表现层
├── service/ # 业务逻辑层
│ ├── impl/ # 实现类
├── dao/ # 数据访问层
├── entity/ # 实体类
├── dto/ # 数据传输对象
├── vo/ # 视图对象
├── util/ # 工具类
└── exception/ # 异常处理
关键配置示例:
xml复制<!-- MyBatis动态SQL配置 -->
<sql id="hotelSearchCondition">
<where>
<if test="priceMin != null">AND price >= #{priceMin}</if>
<if test="priceMax != null">AND price <= #{priceMax}</if>
<if test="keywords != null">
AND (name LIKE CONCAT('%',#{keywords},'%')
OR address LIKE CONCAT('%',#{keywords},'%'))
</if>
</where>
</sql>
3.2 前端工程化实践
Vue项目采用最新的组合式API写法,通过Pinia管理全局状态。值得分享的几个实践:
- 路由懒加载:将不同功能模块拆分成独立chunk
javascript复制const routes = [
{
path: '/attractions',
component: () => import('./views/AttractionList.vue')
}
]
- 自适应布局:使用CSS Grid+媒体查询实现响应式
css复制/* 特产商品网格布局 */
.product-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px;
}
@media (max-width: 768px) {
.product-grid {
grid-template-columns: 1fr 1fr;
}
}
- 性能监控:集成Sentry捕获前端异常
javascript复制app.use(Sentry, {
dsn: 'your_dsn',
integrations: [
new BrowserTracing({
routingInstrumentation: Sentry.vueRouterInstrumentation(router)
})
],
tracesSampleRate: 0.2
})
3.3 数据库优化方案
MySQL表设计遵循以下原则:
- 景点/酒店等核心表采用InnoDB引擎
- 建立合适的复合索引(如(area_id, score))
- 大文本字段单独拆分(如景点详情)
sql复制-- 景点表结构示例
CREATE TABLE `attraction` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL,
`cover_url` varchar(255) DEFAULT NULL,
`area_id` int DEFAULT NULL,
`score` decimal(3,1) DEFAULT '0.0',
`open_time` varchar(50) DEFAULT NULL,
`intro` text,
PRIMARY KEY (`id`),
KEY `idx_area_score` (`area_id`,`score`),
FULLTEXT KEY `ft_name_intro` (`name`,`intro`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
4. 典型问题解决方案
4.1 微信支付回调处理
特产订单支付需要特别注意幂等性处理:
java复制@PostMapping("/pay/notify")
public String wxPayNotify(@RequestBody String xmlData) {
// 1. 验签
if(!WxPayUtil.isValidSign(xmlData, apiKey)) {
return "<xml><return_code>FAIL</return_code></xml>";
}
// 2. 解析订单号
String orderNo = XmlUtil.getValue(xmlData, "out_trade_no");
// 3. 查询本地订单状态
Order order = orderService.getByNo(orderNo);
if(order.getStatus() != OrderStatus.UNPAID) {
return "<xml><return_code>SUCCESS</return_code></xml>";
}
// 4. 更新订单(加分布式锁)
String lockKey = "pay_lock:" + orderNo;
try {
boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if(locked) {
orderService.handlePaidOrder(orderNo);
}
} finally {
redisTemplate.delete(lockKey);
}
return "<xml><return_code>SUCCESS</return_code></xml>";
}
4.2 并发库存超卖问题
采用Redis+Lua脚本实现原子扣减:
lua复制-- inventory.lua
local key = KEYS[1]
local num = tonumber(ARGV[1])
local remain = tonumber(redis.call('GET', key))
if remain >= num then
redis.call('DECRBY', key, num)
return 1 -- 成功
else
return 0 -- 库存不足
end
Java调用示例:
java复制public boolean deductInventory(String productCode, int num) {
String script = ResourceUtils.getScript("inventory.lua");
RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
Long result = redisTemplate.execute(
redisScript,
Collections.singletonList("inv:" + productCode),
String.valueOf(num));
return result == 1;
}
4.3 大文件上传优化
景点VR视频上传采用分片上传方案:
javascript复制// 前端分片处理
async function uploadFile(file) {
const chunkSize = 5 * 1024 * 1024; // 5MB
const chunks = Math.ceil(file.size / chunkSize);
for (let i = 0; i < chunks; i++) {
const start = i * chunkSize;
const end = Math.min(file.size, start + chunkSize);
const chunk = file.slice(start, end);
const formData = new FormData();
formData.append('file', chunk);
formData.append('chunkIndex', i);
formData.append('totalChunks', chunks);
formData.append('fileId', fileId);
await axios.post('/api/upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
});
}
// 合并请求
await axios.post('/api/merge', { fileId, fileName: file.name });
}
5. 部署与监控方案
5.1 Docker Compose部署
生产环境推荐使用容器化部署:
yaml复制version: '3'
services:
app:
image: openjdk:8-jdk-alpine
ports:
- "8080:8080"
volumes:
- ./logs:/app/logs
environment:
- SPRING_PROFILES_ACTIVE=prod
depends_on:
- redis
- mysql
mysql:
image: mysql:5.7
environment:
MYSQL_ROOT_PASSWORD: root123
MYSQL_DATABASE: tourism
volumes:
- ./mysql-data:/var/lib/mysql
redis:
image: redis:6-alpine
ports:
- "6379:6379"
volumes:
- ./redis-data:/data
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- ./dist:/usr/share/nginx/html
5.2 性能监控配置
使用Prometheus+Grafana监控关键指标:
yaml复制# application.yml监控配置
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
metrics:
tags:
application: ${spring.application.name}
建议监控的核心指标:
- 接口响应时间P99
- JVM内存使用率
- MySQL活跃连接数
- Redis缓存命中率
- 订单创建成功率
6. 项目总结与改进方向
经过三个月的开发迭代,系统最终实现了所有核心功能,并在测试环境达到:
- 500并发下平均响应时间672ms
- 库存超卖率为0
- 订单支付成功率99.3%
几个值得记录的教训:
- 分库分表时机:初期过度设计分库方案,实际县域旅游的订单量单库完全能支撑
- 缓存穿透防护:未设置空值缓存导致大量请求穿透到数据库
- 事务粒度控制:过大事务导致死锁频发,后拆分为小事务链
未来可扩展的方向:
- 接入智能客服系统
- 增加AR实景导航功能
- 开发微信小程序版本
这个项目让我深刻体会到,一个好的系统不仅需要技术深度,更要理解业务场景。特别是旅游这类线下属性强的领域,必须考虑网络条件差、用户操作中断等现实情况。