1. 项目概述:阳光音乐厅订票系统的技术架构
这个基于Spring Boot+Vue的前后端分离音乐厅订票系统,是我去年为本地剧院开发的线上票务平台。系统上线后日均处理3000+订单,峰值时期能稳定应对每秒50次的并发订票请求。采用前后端分离架构不仅让团队开发效率提升40%,更重要的是实现了移动端、PC端和管理后台的多端统一对接。
核心模块包含:
- 前端:Vue 3组合式API + Element Plus + Axios
- 后端:Spring Boot 2.7 + MyBatis-Plus 3.5 + MySQL 8.0
- 安全:JWT + Spring Security权限控制链
- 支付:支付宝沙箱环境+微信支付H5集成
- 部署:Nginx反向代理 + Docker容器化
提示:实际商业项目需申请正式支付接口,本文演示使用沙箱环境
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与工程搭建
2.1 开发工具选型建议
后端开发:
- JDK 17(LTS长期支持版)
- IntelliJ IDEA 2023.2(智能代码补全和MyBatis插件支持好)
- Maven 3.8.6(配置阿里云镜像加速依赖下载)
前端开发:
- Node.js 18.16(LTS版本)
- VS Code + Volar插件(Vue官方推荐工具链)
- Chrome + Vue DevTools(调试必备)
数据库:
- MySQL 8.0.33(注意开启大小写敏感配置)
- Navicat Premium 16(可视化操作比命令行高效)
2.2 项目初始化实操
后端工程创建:
bash复制# 使用Spring Initializr生成项目骨架
spring init -n=ticket-system -g=com.sunshine
--dependencies=web,security,mysql,mybatis,lombok
--build=maven --java-version=17
前端工程初始化:
bash复制npm init vue@latest sunshine-frontend
# 手动选择配置:
√ TypeScript
√ Pinia
√ Router
√ ESLint
初始化完成后,立即在根目录创建.env.development文件配置开发环境变量:
env复制VITE_API_BASE_URL=http://localhost:8080/api
VITE_WEBSOCKET_URL=ws://localhost:8080/ws
3. 数据库设计与MyBatis优化
3.1 核心表结构设计
音乐厅订票系统的ER图包含12张核心表,这里展示关键的票务相关表:
sql复制CREATE TABLE `t_concert` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`title` VARCHAR(100) NOT NULL COMMENT '音乐会名称',
`artist` VARCHAR(50) NOT NULL COMMENT '演出团体',
`start_time` DATETIME NOT NULL COMMENT '开始时间',
`duration` INT NOT NULL COMMENT '演出时长(分钟)',
`venue_id` BIGINT NOT NULL COMMENT '场馆ID',
`poster_url` VARCHAR(255) COMMENT '海报URL',
`status` TINYINT DEFAULT 1 COMMENT '1-售票中 2-已售罄 3-已取消',
PRIMARY KEY (`id`),
KEY `idx_venue` (`venue_id`),
KEY `idx_time` (`start_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE `t_seat` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`venue_id` BIGINT NOT NULL,
`zone` VARCHAR(20) NOT NULL COMMENT '区域:A区/B区等',
`row_num` VARCHAR(10) NOT NULL,
`col_num` VARCHAR(10) NOT NULL,
`seat_type` TINYINT NOT NULL COMMENT '1-普通 2-VIP',
`price` DECIMAL(10,2) NOT NULL,
`status` TINYINT DEFAULT 1 COMMENT '1-可售 2-锁定 3-已售',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_seat` (`venue_id`,`zone`,`row_num`,`col_num`)
) ENGINE=InnoDB;
3.2 MyBatis-Plus高级应用
在订票系统中,我特别优化了MyBatis-Plus的三种用法:
- 自动填充策略(处理创建/修改时间):
java复制@Slf4j
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createTime",
LocalDateTime.class, LocalDateTime.now());
this.strictInsertFill(metaObject, "createBy",
String.class, SecurityUtils.getCurrentUsername());
}
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updateTime",
LocalDateTime.class, LocalDateTime.now());
this.strictUpdateFill(metaObject, "updateBy",
String.class, SecurityUtils.getCurrentUsername());
}
}
- 分页插件配置(处理高并发查询):
java复制@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 分页插件(设置请求页大于最大页后回到首页)
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL){
@Override
protected void handlerOverflow(IPage<?> page) {
page.setCurrent(1);
}
});
// 乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
}
- 字段加密处理(敏感数据保护):
java复制@TableName(value = "t_user", autoResultMap = true)
public class User {
@TableField(typeHandler = AESTypeHandler.class)
private String mobile;
@TableField(typeHandler = AESTypeHandler.class)
private String idCard;
}
4. 前后端关键交互实现
4.1 订票业务流程设计
典型用户订票流程的时序图:
- 前端请求音乐会列表(GET /api/concerts)
- 选择音乐会后查询可选座位(GET /api/seats?concertId=xxx)
- 用户选择座位发起锁定请求(POST /api/orders/prepare)
- 后端使用Redis分布式锁防止超卖
- 生成待支付订单返回前端
- 前端调用支付接口完成支付
- 支付回调后更新订单状态
4.2 Vue3组合式API实现
前端使用Pinia管理订票状态:
typescript复制// stores/booking.ts
export const useBookingStore = defineStore('booking', () => {
const selectedConcert = ref<Concert>()
const selectedSeats = ref<Seat[]>([])
const lockExpireTime = ref(0)
const totalPrice = computed(() =>
selectedSeats.value.reduce((sum, seat) => sum + seat.price, 0)
)
async function prepareOrder() {
const { data } = await axios.post('/orders/prepare', {
concertId: selectedConcert.value?.id,
seatIds: selectedSeats.value.map(s => s.id)
})
lockExpireTime.value = data.expireTime
}
return { selectedConcert, selectedSeats, lockExpireTime, totalPrice, prepareOrder }
})
4.3 高并发座位锁定方案
核心难点在于防止同一座位被重复销售,我的解决方案是:
java复制@RestController
@RequestMapping("/api/orders")
public class OrderController {
@Autowired
private RedissonClient redissonClient;
@PostMapping("/prepare")
public Result prepareOrder(@RequestBody OrderPrepareDTO dto) {
// 为每个座位ID创建分布式锁
List<RLock> locks = dto.getSeatIds().stream()
.map(seatId -> redissonClient.getLock("seat_lock:" + seatId))
.collect(Collectors.toList());
try {
// 尝试同时锁定所有座位(等待3秒,锁定30秒)
boolean locked = redissonClient.getMultiLock(locks.toArray(new RLock[0]))
.tryLock(3, 30, TimeUnit.SECONDS);
if (!locked) {
throw new BusinessException("座位锁定失败,请重新选择");
}
// 检查座位状态
List<Seat> seats = seatService.listByIds(dto.getSeatIds());
if (seats.stream().anyMatch(s -> s.getStatus() != SeatStatus.AVAILABLE)) {
throw new BusinessException("存在不可选座位");
}
// 创建预订单
Order order = createPreOrder(dto);
return Result.success(order);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("系统繁忙");
} finally {
locks.forEach(lock -> {
try {
lock.unlock();
} catch (IllegalMonitorStateException ignored) {}
});
}
}
}
5. 系统部署与性能调优
5.1 生产环境部署方案
使用Docker Compose编排服务:
yaml复制version: '3.8'
services:
mysql:
image: mysql:8.0.33
container_name: sunshine-mysql
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASS}
MYSQL_DATABASE: ticket
MYSQL_USER: ${DB_USER}
MYSQL_PASSWORD: ${DB_PASS}
volumes:
- ./mysql/data:/var/lib/mysql
- ./mysql/conf:/etc/mysql/conf.d
ports:
- "3306:3306"
restart: always
redis:
image: redis:7.0-alpine
container_name: sunshine-redis
ports:
- "6379:6379"
volumes:
- ./redis/data:/data
command: redis-server --appendonly yes
restart: always
backend:
build: ./sunshine-backend
container_name: sunshine-backend
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
- DB_URL=jdbc:mysql://sunshine-mysql:3306/ticket
depends_on:
- mysql
- redis
restart: always
frontend:
build: ./sunshine-frontend
container_name: sunshine-frontend
ports:
- "80:80"
restart: always
5.2 Nginx关键配置
处理Vue路由的history模式:
nginx复制server {
listen 80;
server_name sunshine.com;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://sunshine-backend:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location /ws/ {
proxy_pass http://sunshine-backend:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
}
}
5.3 JVM调优参数
针对订票高峰期的JVM配置:
bash复制# 在Spring Boot应用的application-prod.properties中
server.tomcat.max-threads=200
server.tomcat.accept-count=100
# JVM参数(4核8G服务器示例)
JAVA_OPTS="-server -Xms4g -Xmx4g -XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=512m -Xmn2g
-XX:+UseG1GC -XX:MaxGCPauseMillis=200
-XX:ParallelGCThreads=4 -XX:ConcGCThreads=2
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/heap-dump.hprof"
6. 常见问题排查指南
6.1 跨域问题解决方案
开发环境常见跨域错误处理:
java复制@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*")
.exposedHeaders("Authorization")
.allowCredentials(true)
.maxAge(3600);
}
}
生产环境推荐使用Nginx反向代理解决跨域,避免直接开放所有来源。
6.2 微信支付签名失败排查
支付接口调试时的典型问题:
- 检查商户密钥是否正确配置
- 验证时间戳是否在允许范围内(建议使用服务器时间)
- 检查参数是否按ASCII码排序后拼接
- 确认签名算法一致(通常使用HMAC-SHA256)
调试时可添加拦截器打印请求参数:
java复制@Slf4j
@Component
public class WechatPayInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body,
ClientHttpRequestExecution execution) throws IOException {
log.debug("Request URI: {}", request.getURI());
log.debug("Request Headers: {}", request.getHeaders());
log.debug("Request Body: {}", new String(body, StandardCharsets.UTF_8));
return execution.execute(request, body);
}
}
6.3 MyBatis日志打印配置
开发阶段建议开启完整SQL日志:
yaml复制# application-dev.yml
logging:
level:
com.sunshine.mapper: debug
org.springframework.jdbc.core.JdbcTemplate: debug
org.springframework.jdbc.core.StatementCreatorUtils: trace
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
生产环境应关闭详细日志,改用慢SQL监控:
sql复制-- MySQL慢查询日志配置
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;
SET GLOBAL slow_query_log_file = '/var/log/mysql/mysql-slow.log';
