古城景区作为文化遗产的重要载体,每年吸引大量游客前来参观。传统的人工管理模式已难以应对日益增长的游客量和服务需求。我们团队基于实际调研发现,古城景区管理普遍存在以下痛点:
针对这些问题,我们设计开发了这套基于SpringBoot+Vue的智慧景区管理系统。系统上线后,某5A级古城景区的运营数据显示:
系统采用前后端分离架构,充分发挥各技术栈优势:
code复制[客户端层] Vue.js + ElementUI + Axios
↑↓ HTTP/HTTPS
[应用层] SpringBoot + Spring Security + MyBatis-Plus
↑↓ JDBC
[数据层] MySQL 8.0 + Redis 6.2
前端使用Vue 3.2组合式API开发,配合ElementPlus组件库实现响应式布局。后端基于SpringBoot 2.7自动配置特性快速搭建微服务架构,数据访问层采用MyBatis-Plus 3.5简化CRUD操作。
系统主要包含六大功能模块:
票务管理模块
游客服务模块
商户管理模块
数据分析模块
系统管理模块
移动端接口
景区节假日高峰期瞬时并发可达5000+,我们采用多级缓存策略保障系统稳定:
java复制// 票务库存缓存设计
@Cacheable(value = "ticketStock", key = "#scenicId")
public TicketStock getStock(Long scenicId) {
// 一级缓存:Redis集群
String redisKey = "ticket:stock:" + scenicId;
String stock = redisTemplate.opsForValue().get(redisKey);
if(stock != null) {
return JSON.parseObject(stock, TicketStock.class);
}
// 二级缓存:本地Caffeine
TicketStock localCache = localCacheManager.get(scenicId);
if(localCache != null) {
return localCache;
}
// 数据库查询
TicketStock dbData = ticketMapper.selectStock(scenicId);
redisTemplate.opsForValue().set(redisKey,
JSON.toJSONString(dbData), 5, TimeUnit.MINUTES);
localCacheManager.put(scenicId, dbData);
return dbData;
}
配合分布式锁防止超卖:
java复制public boolean purchaseTicket(Long userId, Long ticketId, int quantity) {
String lockKey = "ticket:lock:" + ticketId;
// 尝试获取分布式锁
boolean locked = redisLock.tryLock(lockKey, 10, TimeUnit.SECONDS);
try {
if(locked) {
// 检查库存
int stock = getRealTimeStock(ticketId);
if(stock < quantity) {
throw new BusinessException("库存不足");
}
// 扣减库存
int rows = ticketMapper.reduceStock(ticketId, quantity);
if(rows == 0) {
throw new ConcurrentUpdateException("并发修改冲突");
}
// 创建订单
createOrder(userId, ticketId, quantity);
return true;
}
} finally {
if(locked) {
redisLock.unlock(lockKey);
}
}
return false;
}
基于A*算法实现最优游览路线推荐:
java复制public List<ScenicSpot> recommendRoute(Long startId, List<Long> preferIds) {
// 获取所有景点数据
List<ScenicSpot> allSpots = spotMapper.selectAll();
Map<Long, ScenicSpot> spotMap = allSpots.stream()
.collect(Collectors.toMap(ScenicSpot::getId, Function.identity()));
// 构建图结构
Graph graph = new Graph();
for (ScenicSpot spot : allSpots) {
for (Route route : spot.getRoutes()) {
graph.addEdge(spot.getId(), route.getToSpotId(),
route.getDistance(), route.getCrowdFactor());
}
}
// A*算法实现
AStarAlgorithm astar = new AStarAlgorithm(graph);
return astar.findPath(startId, preferIds)
.stream()
.map(spotMap::get)
.collect(Collectors.toList());
}
算法考虑以下因素:
使用WebSocket实现客流数据实时推送:
vue复制<template>
<div class="dashboard">
<real-time-chart :data="chartData" />
<heat-map :points="heatPoints" />
</div>
</template>
<script setup>
import { onMounted, ref } from 'vue'
import { useWebSocket } from '@/composables/websocket'
const chartData = ref([])
const heatPoints = ref([])
const { initSocket, closeSocket } = useWebSocket(
'wss://api.scenic.com/ws/flow',
{
onMessage: (msg) => {
const data = JSON.parse(msg.data)
chartData.value = processChartData(data)
heatPoints.value = processHeatData(data)
},
onError: (err) => {
console.error('WebSocket error:', err)
}
}
)
onMounted(() => {
initSocket()
return () => closeSocket()
})
</script>
后端使用Netty处理高并发WS连接:
java复制@ChannelHandler.Sharable
public class FlowDataHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) {
String sceneId = parseSceneId(msg.text());
// 加入对应景区的频道
FlowDataChannelManager.joinChannel(sceneId, ctx.channel());
}
@Override
public void channelInactive(ChannelHandlerContext ctx) {
// 断开连接时移除频道
FlowDataChannelManager.leaveAllChannels(ctx.channel());
}
}
景点表(scenic_spot)
sql复制CREATE TABLE `scenic_spot` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL COMMENT '景点名称',
`location` point NOT NULL COMMENT '地理坐标',
`description` text COMMENT '景点介绍',
`max_capacity` int DEFAULT 0 COMMENT '最大承载量',
`current_flow` int DEFAULT 0 COMMENT '实时人流量',
`status` tinyint DEFAULT 1 COMMENT '状态:1开放 0关闭',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
SPATIAL KEY `idx_location` (`location`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
票务表(ticket)
sql复制CREATE TABLE `ticket` (
`id` bigint NOT NULL AUTO_INCREMENT,
`spot_id` bigint NOT NULL,
`type` tinyint NOT NULL COMMENT '1成人票 2儿童票 3优惠票',
`price` decimal(10,2) NOT NULL,
`total_stock` int NOT NULL COMMENT '总库存',
`available_stock` int NOT NULL COMMENT '可用库存',
`valid_date` date NOT NULL,
`start_time` time DEFAULT NULL,
`end_time` time DEFAULT NULL,
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_spot_type_date` (`spot_id`,`type`,`valid_date`),
KEY `idx_valid_date` (`valid_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
sql复制-- 查找1公里范围内的景点
SELECT id, name,
ST_Distance_Sphere(location, POINT(116.404, 39.915)) as distance
FROM scenic_spot
WHERE ST_Distance_Sphere(location, POINT(116.404, 39.915)) <= 1000
ORDER BY distance;
java复制@Aspect
@Component
@Slf4j
public class DaoMonitorAspect {
@Around("execution(* com..mapper.*.*(..))")
public Object monitorSqlPerformance(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
Object result = pjp.proceed();
long elapsed = System.currentTimeMillis() - start;
if(elapsed > 500) { // 超过500ms记录警告
log.warn("Slow SQL detected: {} - {}ms",
pjp.getSignature().toShortString(), elapsed);
}
return result;
}
}
java复制@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/api/public/**").permitAll()
.antMatchers("/api/user/**").hasRole("USER")
.antMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.addFilter(new JwtAuthenticationFilter(authenticationManager()))
.addFilter(new JwtAuthorizationFilter(authenticationManager()))
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
java复制@RateLimiter(value = 5, key = "#userId")
public Order createOrder(Long userId, TicketRequest request) {
// 订单创建逻辑
}
vue复制<template>
<div>
<slider-captcha
v-if="showCaptcha"
@success="onVerifySuccess"
@close="showCaptcha = false"
/>
</div>
</template>
<script>
export default {
methods: {
async submitOrder() {
const { data } = await checkRisk(this.userId);
if(data.riskLevel > 2) {
this.showCaptcha = true;
return;
}
// 正常下单流程
}
}
}
</script>
Docker Compose编排示例:
yaml复制version: '3.8'
services:
app:
image: scenic-app:${TAG:-latest}
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
- REDIS_HOST=redis
depends_on:
- redis
- mysql
redis:
image: redis:6.2-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
mysql:
image: mysql:8.0
ports:
- "3306:3306"
environment:
- MYSQL_ROOT_PASSWORD=${DB_ROOT_PASS}
- MYSQL_DATABASE=scenic
volumes:
- mysql_data:/var/lib/mysql
volumes:
redis_data:
mysql_data:
yaml复制management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
metrics:
export:
prometheus:
enabled: true
tags:
application: scenic-system
在项目开发过程中,我们深刻体会到良好的模块划分和接口设计对后期扩展的重要性。特别是在景区管理系统这类业务复杂的项目中,建议采用领域驱动设计(DDD)方法,先明确核心子域和限界上下文,再逐步迭代开发。对于准备开发类似系统的同学,一定要重视压力测试环节,我们曾在模拟10万并发请求时发现了数据库连接池配置不当导致的性能瓶颈,这个经验教训值得分享。