这个企业级旅游网站管理系统采用当前主流的SpringBoot+Vue+MyBatis技术栈,是一套完整的全栈解决方案。我在实际开发中发现,旅游行业管理系统相比通用CMS有着显著差异:需要处理复杂的行程组合、实时库存管理、多供应商对接等业务场景。这套源码的价值在于它已经实现了旅游行业80%的核心功能模块,开发者可以基于此快速构建定制化系统。
系统采用前后端分离架构,后端基于SpringBoot 2.7提供RESTful API,前端使用Vue 3组合式API开发管理后台,数据库选用MySQL 8.0。特别值得一提的是,项目对旅游行业特有的"动态打包"功能(如酒店+机票+景点组合销售)有完整实现,这通常是同类开源项目最欠缺的部分。
SpringBoot的选择主要基于其快速启动特性。我在配置中特别优化了:
yaml复制spring:
datasource:
hikari:
maximum-pool-size: 20 # 根据压测结果调整的连接池大小
connection-timeout: 30000
jackson:
serialization:
indent-output: true # 开发阶段开启格式化
MyBatis-Plus 3.5.2的引入大幅简化了DAO层开发。对于旅游产品这类复杂对象,我采用结果映射处理嵌套查询:
java复制@Select("SELECT p.*, d.departure_date FROM products p LEFT JOIN departures d ON p.id=d.product_id")
@Results({
@Result(property = "id", column = "id"),
@Result(property = "departures", column = "id",
many = @Many(select = "com.travel.mapper.DepartureMapper.selectByProductId"))
})
List<Product> selectProductsWithDepartures();
Vue 3的组合式API更适合管理后台这类复杂交互场景。项目中使用Pinia替代Vuex进行状态管理,典型store结构如下:
javascript复制// stores/product.js
export const useProductStore = defineStore('product', () => {
const variants = ref([])
async function loadVariants(productId) {
variants.value = await api.get(`/products/${productId}/variants`)
}
return { variants, loadVariants }
})
Element Plus组件库经过二次封装,形成了统一的表单验证规则:
javascript复制// utils/validators.js
export const tourRules = {
title: [{ required: true, trigger: 'blur' }],
days: [{ type: 'number', min: 1, message: '至少1天' }],
price: [{ validator: (v) => v > 0, message: '价格必须大于0' }]
}
产品模型采用组合模式设计,数据库schema包含:
sql复制CREATE TABLE products (
id BIGINT PRIMARY KEY,
type ENUM('TOUR','HOTEL','TICKET') NOT NULL,
title VARCHAR(100) NOT NULL,
-- 其他公共字段...
);
CREATE TABLE tour_variants (
id BIGINT PRIMARY KEY,
product_id BIGINT NOT NULL,
departure_date DATE NOT NULL,
max_persons INT DEFAULT 10,
FOREIGN KEY (product_id) REFERENCES products(id)
);
后台实现的关键在于动态定价策略:
java复制public BigDecimal calculatePrice(Product product, LocalDate date, int persons) {
// 1. 获取基础价格
BigDecimal basePrice = product.getBasePrice();
// 2. 应用季节性系数
Season season = seasonService.getSeason(date);
basePrice = basePrice.multiply(season.getRate());
// 3. 早鸟折扣逻辑
if (ChronoUnit.DAYS.between(LocalDate.now(), date) > 30) {
basePrice = basePrice.multiply(BigDecimal.valueOf(0.9));
}
return basePrice;
}
订单状态机设计是核心难点,我们采用状态模式实现:
java复制public interface OrderState {
void pay(Order order);
void cancel(Order order);
void complete(Order order);
}
@Component
@Scope("prototype")
public class PendingState implements OrderState {
@Override
public void pay(Order order) {
order.setState(OrderStatus.PAID);
inventoryService.lock(order.getItems());
}
}
库存扣减需要特别注意并发控制:
sql复制UPDATE tour_variants
SET remaining = remaining - 1
WHERE id = ? AND remaining >= 1
我们采用Redis+Lua实现分布式锁:
lua复制-- reserve.lua
local key = KEYS[1]
local quantity = tonumber(ARGV[1])
local current = tonumber(redis.call('GET', key) or "0")
if current >= quantity then
redis.call('DECRBY', key, quantity)
return 1
else
return 0
end
Spring中通过自定义注解简化调用:
java复制@RedisLock(key = "#productId", waitTime = 500)
public boolean reserveInventory(Long productId, int quantity) {
// 业务逻辑
}
使用Spring Integration实现供应商通道:
java复制@Bean
public IntegrationFlow supplierFlow() {
return IntegrationFlows
.from("supplierChannel")
.handle(Http.outboundGateway("{baseUrl}/api")
.uriVariable("baseUrl", "supplier.url")
.httpMethod(HttpMethod.POST))
.transform(new Jackson2JsonObjectMapper())
.get();
}
建议为每个供应商创建独立的线程池:
java复制@Bean(name = "expediaThreadPool")
public ThreadPoolTaskExecutor expediaExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(50);
executor.setThreadNamePrefix("Expedia-");
return executor;
}
Docker Compose编排示例:
yaml复制version: '3'
services:
app:
image: travel-app:${TAG:-latest}
environment:
- SPRING_PROFILES_ACTIVE=prod
deploy:
resources:
limits:
cpus: '2'
memory: 2G
mysql:
image: mysql:8.0
command: --innodb-buffer-pool-size=1G
Nginx配置要点:
nginx复制location /api {
proxy_pass http://backend;
proxy_set_header X-Real-IP $remote_addr;
proxy_next_upstream error timeout http_500;
# 长连接优化
proxy_http_version 1.1;
proxy_set_header Connection "";
}
采用多级缓存架构:
Spring Cache配置示例:
java复制@CacheConfig(cacheNames = "products")
@Repository
public class ProductRepository {
@Cacheable(key = "#id", unless = "#result == null")
public Product findById(Long id) { ... }
@CacheEvict(allEntries = true)
public void clearCache() { ... }
}
支付宝沙箱配置要点:
properties复制# application-payment.properties
alipay.app-id=20210001234
alipay.gateway=https://openapi.alipaydev.com/gateway.do
alipay.notify-url=/api/payment/alipay/notify
支付状态回调处理:
java复制@PostMapping("/notify")
public String handleNotify(@RequestBody String body) {
if (alipaySignature.verify(body)) {
paymentService.processNotify(parseNotify(body));
return "success";
}
return "failure";
}
使用Elasticsearch实现搜索分析:
java复制@Repository
public interface ProductSearchRepository extends ElasticsearchRepository<Product, Long> {
@Query("{\"bool\":{\"must\":[{\"match\":{\"title\":\"?0\"}}]}}")
Page<Product> searchByTitle(String title, Pageable pageable);
}
建议的Kibana看板指标:
这套系统在实际部署时,我发现最大的挑战是库存同步的实时性要求。最终采用的解决方案是通过Debezium监听MySQL binlog变更,将库存变动实时推送到WebSocket前端。具体实现时需要注意MySQL的binlog_format必须设置为ROW模式,否则无法捕获准确的数据变更。