1. 项目概述
这个景区民宿预约系统采用当前主流的前后端分离架构,前端使用Vue3框架,后端基于SpringBoot+MyBatis技术栈,数据库选用MySQL。系统主要面向景区民宿经营者,提供房源管理、订单处理、用户评价等核心功能,同时为游客提供在线查询、预订、支付等完整服务流程。
在实际开发中,我们遇到了几个关键挑战:如何实现高并发的预订处理、如何设计灵活的房态管理机制、如何保证支付环节的安全性。通过合理的架构设计和代码优化,最终系统在测试环境下能够支持每秒200+的并发请求,订单处理延迟控制在300ms以内。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型解析
2.1 后端技术栈
SpringBoot 2.7.x作为基础框架,主要考虑其快速启动、自动配置和丰富的starter生态。我们特别使用了:
- Spring Security进行权限控制
- Spring Data Redis处理缓存
- Spring Task实现定时任务
MyBatis 3.5.x作为ORM框架,配合MyBatis-Plus 3.5.x增强功能。关键配置如下:
xml复制<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
2.2 前端技术栈
Vue3组合式API大幅提升了代码组织效率,主要技术组合:
- Vite作为构建工具
- Element Plus组件库
- Axios处理HTTP请求
- Vue Router管理路由
- Pinia状态管理
典型页面组件结构:
javascript复制<script setup>
import { ref } from 'vue'
import { useRoomStore } from '@/stores/room'
const roomStore = useRoomStore()
const loading = ref(false)
const fetchRooms = async () => {
loading.value = true
await roomStore.fetchAvailableRooms()
loading.value = false
}
</script>
2.3 数据库设计
MySQL 8.0采用InnoDB引擎,关键表结构设计:
- 用户表(users):
sql复制CREATE TABLE `users` (
`id` bigint NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL,
`password` varchar(100) NOT NULL,
`phone` varchar(20) NOT NULL,
`avatar` varchar(255) DEFAULT NULL,
`created_at` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_username` (`username`),
UNIQUE KEY `idx_phone` (`phone`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
- 民宿表(homestays):
sql复制CREATE TABLE `homestays` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL,
`location` point NOT NULL,
`address` varchar(255) NOT NULL,
`cover_image` varchar(255) NOT NULL,
`description` text,
`owner_id` bigint NOT NULL,
`status` tinyint NOT NULL DEFAULT '1',
PRIMARY KEY (`id`),
SPATIAL KEY `idx_location` (`location`),
KEY `idx_owner` (`owner_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
3. 核心功能实现
3.1 房源搜索与预订
采用Elasticsearch实现全文检索,核心搜索接口:
java复制@RestController
@RequestMapping("/api/search")
public class SearchController {
@Autowired
private ElasticsearchRestTemplate elasticsearchTemplate;
@GetMapping
public Page<HomestayDocument> search(
@RequestParam String keyword,
@RequestParam(required = false) Double lat,
@RequestParam(required = false) Double lng,
@RequestParam(required = false) Double distance,
@PageableDefault Pageable pageable) {
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder()
.withQuery(QueryBuilders.multiMatchQuery(keyword, "name", "description", "address"));
if (lat != null && lng != null && distance != null) {
queryBuilder.withFilter(QueryBuilders.geoDistanceQuery("location")
.point(lat, lng)
.distance(distance + "km"));
}
return elasticsearchTemplate.search(queryBuilder.build(), HomestayDocument.class)
.map(searchHit -> searchHit.getContent());
}
}
3.2 订单处理流程
订单状态机设计:
java复制public enum OrderStatus {
PENDING_PAYMENT, // 待支付
PAID, // 已支付
CONFIRMED, // 已确认
CHECKED_IN, // 已入住
COMPLETED, // 已完成
CANCELLED, // 已取消
REFUNDING, // 退款中
REFUNDED // 已退款
}
@Service
@Transactional
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private RedisTemplate<String, String> redisTemplate;
public Order createOrder(OrderCreateDTO dto) {
// 分布式锁防止重复下单
String lockKey = "order:lock:" + dto.getUserId() + ":" + dto.getRoomId();
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (!Boolean.TRUE.equals(locked)) {
throw new BusinessException("操作太频繁,请稍后再试");
}
try {
// 验证房源可用性
Room room = validateRoomAvailability(dto);
// 创建订单
Order order = new Order();
// 设置订单属性...
return orderRepository.save(order);
} finally {
redisTemplate.delete(lockKey);
}
}
}
3.3 支付集成
对接支付宝和微信支付双渠道:
java复制public interface PaymentService {
PaymentResponse createPayment(PaymentRequest request);
PaymentResult queryPayment(String orderNo);
boolean verifyNotification(Map<String, String> params);
}
@Service
public class AlipayServiceImpl implements PaymentService {
@Value("${alipay.app-id}")
private String appId;
@Value("${alipay.private-key}")
private String privateKey;
@Value("${alipay.public-key}")
private String publicKey;
@Override
public PaymentResponse createPayment(PaymentRequest request) {
AlipayClient alipayClient = new DefaultAlipayClient(
"https://openapi.alipay.com/gateway.do",
appId,
privateKey,
"json",
"UTF-8",
publicKey,
"RSA2");
AlipayTradePagePayRequest alipayRequest = new AlipayTradePagePayRequest();
alipayRequest.setReturnUrl(request.getReturnUrl());
alipayRequest.setNotifyUrl(request.getNotifyUrl());
JSONObject bizContent = new JSONObject();
bizContent.put("out_trade_no", request.getOrderNo());
bizContent.put("total_amount", request.getAmount());
bizContent.put("subject", request.getSubject());
bizContent.put("product_code", "FAST_INSTANT_TRADE_PAY");
alipayRequest.setBizContent(bizContent.toString());
try {
String form = alipayClient.pageExecute(alipayRequest).getBody();
return new PaymentResponse(form);
} catch (AlipayApiException e) {
throw new PaymentException("支付宝支付创建失败", e);
}
}
}
4. 性能优化实践
4.1 缓存策略
采用多级缓存架构:
- 本地Caffeine缓存:高频访问的基础数据
- Redis集群:共享缓存和分布式锁
- MySQL:持久化存储
缓存配置示例:
java复制@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
.initialCapacity(100)
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.recordStats());
return cacheManager;
}
@Bean
public RedisCacheManager redisCacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(1))
.disableCachingNullValues()
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.transactionAware()
.build();
}
}
4.2 数据库优化
- 索引优化:
sql复制-- 订单查询常用条件
ALTER TABLE orders ADD INDEX idx_user_status (user_id, status);
ALTER TABLE orders ADD INDEX idx_date_range (check_in_date, check_out_date);
-- 空间索引优化距离查询
ALTER TABLE homestays MODIFY COLUMN location POINT NOT NULL SRID 4326;
CREATE SPATIAL INDEX idx_location ON homestays(location);
- 分表策略:按月份对订单表进行水平分表,使用ShardingSphere实现:
yaml复制spring:
shardingsphere:
datasource:
names: ds0
ds0:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/homestay?serverTimezone=UTC
username: root
password: password
sharding:
tables:
orders:
actual-data-nodes: ds0.orders_$->{2023..2025}0$->{1..9},ds0.orders_$->{2023..2025}1$->{0..2}
table-strategy:
standard:
sharding-column: create_time
precise-algorithm-class-name: com.example.sharding.MonthShardingAlgorithm
5. 安全防护措施
5.1 接口安全
- JWT认证实现:
java复制@Component
public class JwtTokenProvider {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private long expiration;
public String generateToken(UserDetails userDetails) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + expiration);
return Jwts.builder()
.setSubject(userDetails.getUsername())
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
public String getUsernameFromToken(String token) {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody()
.getSubject();
}
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(secret).parseClaimsJws(token);
return true;
} catch (Exception ex) {
// 处理各种异常情况
return false;
}
}
}
5.2 数据安全
- 敏感数据加密:
java复制public class DataEncryptor {
private static final String AES_KEY = "your-256-bit-secret";
private static final String IV = "your-16-bit-IV";
public static String encrypt(String data) {
try {
IvParameterSpec iv = new IvParameterSpec(IV.getBytes(StandardCharsets.UTF_8));
SecretKeySpec keySpec = new SecretKeySpec(AES_KEY.getBytes(StandardCharsets.UTF_8), "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv);
byte[] encrypted = cipher.doFinal(data.getBytes());
return Base64.getEncoder().encodeToString(encrypted);
} catch (Exception e) {
throw new RuntimeException("加密失败", e);
}
}
public static String decrypt(String encryptedData) {
try {
IvParameterSpec iv = new IvParameterSpec(IV.getBytes(StandardCharsets.UTF_8));
SecretKeySpec keySpec = new SecretKeySpec(AES_KEY.getBytes(StandardCharsets.UTF_8), "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, keySpec, iv);
byte[] original = cipher.doFinal(Base64.getDecoder().decode(encryptedData));
return new String(original);
} catch (Exception e) {
throw new RuntimeException("解密失败", e);
}
}
}
6. 部署方案
6.1 容器化部署
Docker Compose编排文件示例:
yaml复制version: '3.8'
services:
mysql:
image: mysql:8.0
container_name: homestay-mysql
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MYSQL_DATABASE: homestay
MYSQL_USER: ${DB_USER}
MYSQL_PASSWORD: ${DB_PASSWORD}
volumes:
- mysql_data:/var/lib/mysql
ports:
- "3306:3306"
networks:
- homestay-net
redis:
image: redis:6.2
container_name: homestay-redis
ports:
- "6379:6379"
volumes:
- redis_data:/data
networks:
- homestay-net
backend:
build:
context: ./backend
dockerfile: Dockerfile
container_name: homestay-backend
depends_on:
- mysql
- redis
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
networks:
- homestay-net
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
container_name: homestay-frontend
ports:
- "80:80"
networks:
- homestay-net
volumes:
mysql_data:
redis_data:
networks:
homestay-net:
driver: bridge
6.2 CI/CD流程
GitLab CI配置示例:
yaml复制stages:
- build
- test
- deploy
variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_DRIVER: overlay2
build-backend:
stage: build
image: maven:3.8.4-jdk-11
script:
- mvn clean package -DskipTests
artifacts:
paths:
- backend/target/*.jar
test-backend:
stage: test
image: maven:3.8.4-jdk-11
script:
- mvn test
build-frontend:
stage: build
image: node:16
script:
- cd frontend
- npm install
- npm run build
artifacts:
paths:
- frontend/dist
deploy:
stage: deploy
image: docker:20.10.12
services:
- docker:20.10.12-dind
script:
- docker-compose down
- docker-compose up -d --build
only:
- master
7. 常见问题与解决方案
7.1 跨域问题处理
SpringBoot后端配置:
java复制@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.exposedHeaders("Authorization")
.maxAge(3600);
}
}
Vue3前端代理配置(vite.config.js):
javascript复制export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
})
7.2 MyBatis常见问题
- 动态SQL示例:
xml复制<select id="findAvailableRooms" resultType="Room">
SELECT * FROM rooms
<where>
<if test="homestayId != null">
AND homestay_id = #{homestayId}
</if>
<if test="checkIn != null and checkOut != null">
AND id NOT IN (
SELECT room_id FROM orders
WHERE status IN ('PAID', 'CONFIRMED', 'CHECKED_IN')
AND (
(check_in_date < #{checkOut} AND check_out_date > #{checkIn})
OR (check_in_date >= #{checkIn} AND check_in_date < #{checkOut})
OR (check_out_date > #{checkIn} AND check_out_date <= #{checkOut})
)
)
</if>
<if test="priceMin != null">
AND price >= #{priceMin}
</if>
<if test="priceMax != null">
AND price <= #{priceMax}
</if>
</where>
</select>
- 分页查询优化:
java复制@Mapper
public interface RoomMapper {
@Select("SELECT * FROM rooms WHERE status = 1 LIMIT #{limit} OFFSET #{offset}")
List<Room> findAvailableRooms(@Param("offset") int offset, @Param("limit") int limit);
@Select("SELECT COUNT(*) FROM rooms WHERE status = 1")
int countAvailableRooms();
}
// 使用示例
public Page<Room> getAvailableRooms(int page, int size) {
int total = roomMapper.countAvailableRooms();
int offset = (page - 1) * size;
List<Room> content = roomMapper.findAvailableRooms(offset, size);
return new PageImpl<>(content, PageRequest.of(page, size), total);
}
8. 项目扩展方向
8.1 微服务改造
考虑将单体应用拆分为:
- 用户服务
- 民宿服务
- 订单服务
- 支付服务
- 搜索服务
使用Spring Cloud Alibaba技术栈:
xml复制<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
8.2 小程序端开发
使用Uniapp开发微信小程序:
javascript复制// pages/detail/detail.vue
<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
const room = ref(null)
const loading = ref(true)
onLoad(async (options) => {
try {
const res = await uni.request({
url: '/api/rooms/' + options.id,
method: 'GET'
})
room.value = res.data
} catch (error) {
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
loading.value = false
}
})
</script>
8.3 数据分析模块
使用Apache Druid进行OLAP分析:
java复制@Repository
public class AnalysisRepository {
@Autowired
private JdbcTemplate jdbcTemplate;
public List<OrderAnalysis> analyzeOrders(LocalDate start, LocalDate end) {
String sql = "SELECT "
+ "DATE_FORMAT(create_time, '%Y-%m-%d') AS day, "
+ "COUNT(*) AS total_orders, "
+ "SUM(amount) AS total_amount, "
+ "AVG(amount) AS avg_amount "
+ "FROM orders "
+ "WHERE create_time BETWEEN ? AND ? "
+ "GROUP BY DATE_FORMAT(create_time, '%Y-%m-%d') "
+ "ORDER BY day";
return jdbcTemplate.query(sql,
(rs, rowNum) -> new OrderAnalysis(
rs.getString("day"),
rs.getInt("total_orders"),
rs.getBigDecimal("total_amount"),
rs.getBigDecimal("avg_amount")),
start, end.plusDays(1));
}
}
