1. 项目概述
这个影院购票管理系统是基于微信小程序平台开发的毕业设计项目。作为一名有多年开发经验的程序员,我深知一个优秀的票务系统需要兼顾用户体验和后台管理效率。微信小程序作为目前最流行的轻应用平台,具有无需安装、即用即走的优势,非常适合票务类应用场景。
系统采用前后端分离架构,前端使用微信小程序原生框架开发,后端采用Spring Boot构建RESTful API服务,数据库选用MySQL关系型数据库。整个系统包含用户端和管理端两大模块,实现了从电影展示、选座购票到订单管理的完整业务流程。
提示:在实际开发中,微信小程序的审核机制较为严格,特别是涉及支付功能时,需要提前准备好相关资质文件。
2. 系统架构设计
2.1 技术栈选型
前端技术栈:
- 微信小程序原生框架
- WXML/WXSS/JavaScript
- WeUI组件库
- ECharts图表库(用于数据可视化)
后端技术栈:
- Spring Boot 2.7.x
- Spring Security(权限控制)
- Redis(缓存和分布式锁)
- MyBatis-Plus(ORM框架)
- Swagger(API文档)
数据库:
- MySQL 8.0(主数据库)
- Redis 6.x(缓存数据库)
2.2 系统分层架构
系统采用经典的三层架构设计:
code复制表现层(微信小程序)
↓
业务逻辑层(Spring Boot)
↓
数据访问层(MySQL + Redis)
这种分层设计使得各层职责明确,便于后期维护和扩展。特别是在高并发场景下,可以通过增加Redis缓存层有效减轻数据库压力。
3. 核心功能实现
3.1 用户认证模块
用户认证采用微信官方提供的登录流程:
- 前端调用wx.login获取code
- 将code发送到后端服务器
- 后端使用code向微信服务器换取openid和session_key
- 生成自定义登录态token返回给前端
- 前端存储token用于后续接口鉴权
java复制// 示例:微信登录接口实现
@PostMapping("/wxLogin")
public Result wxLogin(@RequestParam String code) {
// 1. 使用code换取openid
String url = "https://api.weixin.qq.com/sns/jscode2session";
Map<String, String> params = new HashMap<>();
params.put("appid", appId);
params.put("secret", appSecret);
params.put("js_code", code);
params.put("grant_type", "authorization_code");
String response = restTemplate.getForObject(url + "?appid={appid}&secret={secret}&js_code={js_code}&grant_type={grant_type}",
String.class, params);
// 2. 解析响应获取openid和session_key
JSONObject json = JSON.parseObject(response);
String openid = json.getString("openid");
String sessionKey = json.getString("session_key");
// 3. 生成自定义登录态
String token = JwtUtil.generateToken(openid);
// 4. 返回token给前端
return Result.success(token);
}
3.2 电影场次管理
电影场次管理是系统的核心模块之一,需要考虑以下几个关键点:
- 场次排期冲突检测
- 座位库存管理
- 票价动态设置
我们设计了以下数据库表结构来支持这些功能:
sql复制CREATE TABLE `movie_schedule` (
`id` bigint NOT NULL AUTO_INCREMENT,
`movie_id` bigint NOT NULL COMMENT '电影ID',
`cinema_id` bigint NOT NULL COMMENT '影院ID',
`hall_id` bigint NOT NULL COMMENT '影厅ID',
`start_time` datetime NOT NULL COMMENT '开始时间',
`end_time` datetime NOT NULL COMMENT '结束时间',
`price` decimal(10,2) NOT NULL COMMENT '票价',
`status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:1-可售 0-不可售',
PRIMARY KEY (`id`),
KEY `idx_movie_id` (`movie_id`),
KEY `idx_cinema_id` (`cinema_id`),
KEY `idx_time` (`start_time`,`end_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='电影场次表';
CREATE TABLE `seat` (
`id` bigint NOT NULL AUTO_INCREMENT,
`schedule_id` bigint NOT NULL COMMENT '场次ID',
`seat_row` varchar(10) NOT NULL COMMENT '排号',
`seat_col` varchar(10) NOT NULL COMMENT '列号',
`status` tinyint NOT NULL DEFAULT '0' COMMENT '状态:0-可选 1-已售 2-锁定',
`lock_time` datetime DEFAULT NULL COMMENT '锁定时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_schedule_seat` (`schedule_id`,`seat_row`,`seat_col`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='座位表';
3.3 选座购票流程
选座购票是系统最复杂的业务流程,需要考虑高并发场景下的座位竞争问题。我们采用Redis分布式锁+数据库事务的方案来保证数据一致性。
购票流程时序图:
- 用户选择场次进入选座页面
- 前端定时轮询座位状态(每5秒一次)
- 用户选择座位后发起锁定请求
- 后端尝试获取Redis锁
- 锁定成功后更新座位状态为"锁定"
- 用户确认订单并支付
- 支付成功后更新座位状态为"已售"
- 释放Redis锁
关键代码实现:
java复制// 座位锁定服务
@Service
public class SeatLockService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private SeatMapper seatMapper;
private static final String LOCK_PREFIX = "seat_lock:";
private static final long LOCK_EXPIRE = 30; // 锁过期时间30秒
public boolean lockSeat(Long scheduleId, String seatRow, String seatCol, String userId) {
String lockKey = LOCK_PREFIX + scheduleId + ":" + seatRow + ":" + seatCol;
String lockValue = userId + ":" + System.currentTimeMillis();
// 尝试获取分布式锁
Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, LOCK_EXPIRE, TimeUnit.SECONDS);
if (locked != null && locked) {
try {
// 检查座位状态
Seat seat = seatMapper.selectByScheduleAndPosition(scheduleId, seatRow, seatCol);
if (seat == null || seat.getStatus() != 0) {
return false;
}
// 更新座位状态
seat.setStatus(2); // 2表示锁定状态
seat.setLockTime(new Date());
seatMapper.updateById(seat);
return true;
} catch (Exception e) {
// 发生异常释放锁
redisTemplate.delete(lockKey);
throw e;
}
}
return false;
}
public void unlockSeat(Long scheduleId, String seatRow, String seatCol) {
String lockKey = LOCK_PREFIX + scheduleId + ":" + seatRow + ":" + seatCol;
redisTemplate.delete(lockKey);
// 这里可以添加座位状态回滚逻辑
}
}
4. 支付系统集成
4.1 微信支付接入
系统集成了微信小程序支付功能,主要流程如下:
- 前端调用wx.requestPayment发起支付
- 后端生成预支付订单
- 用户确认支付
- 微信支付回调通知
- 更新订单状态
支付安全注意事项:
- 支付参数需要签名验证
- 支付结果以异步通知为准
- 需要处理重复通知的情况
- 支付超时需要取消订单
java复制// 微信支付服务实现
@Service
public class WxPayService {
@Value("${wxpay.appid}")
private String appid;
@Value("${wxpay.mchid}")
private String mchid;
@Value("${wxpay.key}")
private String key;
@Value("${wxpay.notifyUrl}")
private String notifyUrl;
public Map<String, String> createPayOrder(String openid, String orderNo, int amount, String body) {
Map<String, String> params = new HashMap<>();
params.put("appid", appid);
params.put("mch_id", mchid);
params.put("nonce_str", WxPayUtil.generateNonceStr());
params.put("body", body);
params.put("out_trade_no", orderNo);
params.put("total_fee", String.valueOf(amount));
params.put("spbill_create_ip", "127.0.0.1");
params.put("notify_url", notifyUrl);
params.put("trade_type", "JSAPI");
params.put("openid", openid);
// 生成签名
String sign = WxPayUtil.generateSignature(params, key);
params.put("sign", sign);
// 调用统一下单接口
String xmlResult = WxPayUtil.httpRequest("https://api.mch.weixin.qq.com/pay/unifiedorder", "POST", params);
Map<String, String> result = WxPayUtil.xmlToMap(xmlResult);
if ("SUCCESS".equals(result.get("return_code")) && "SUCCESS".equals(result.get("result_code"))) {
// 组装前端调起支付参数
Map<String, String> payParams = new HashMap<>();
payParams.put("appId", appid);
payParams.put("timeStamp", String.valueOf(System.currentTimeMillis() / 1000));
payParams.put("nonceStr", WxPayUtil.generateNonceStr());
payParams.put("package", "prepay_id=" + result.get("prepay_id"));
payParams.put("signType", "MD5");
String paySign = WxPayUtil.generateSignature(payParams, key);
payParams.put("paySign", paySign);
return payParams;
} else {
throw new RuntimeException("微信支付下单失败:" + result.get("return_msg"));
}
}
}
5. 性能优化实践
5.1 缓存策略
为了提高系统性能,我们采用了多级缓存策略:
- 静态数据缓存:电影信息、影院信息等不常变的数据使用Redis缓存
- 热点数据缓存:热门场次的座位信息使用本地缓存+Redis缓存
- 查询结果缓存:复杂查询结果缓存
缓存更新策略:
- 被动更新:数据变更时删除缓存
- 主动更新:定时任务刷新缓存
- 缓存穿透防护:布隆过滤器+空值缓存
java复制// 缓存服务示例
@Service
public class MovieCacheService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private MovieMapper movieMapper;
private static final String MOVIE_CACHE_PREFIX = "movie:";
private static final long CACHE_EXPIRE = 24 * 60 * 60; // 缓存过期时间24小时
public Movie getMovieById(Long movieId) {
String cacheKey = MOVIE_CACHE_PREFIX + movieId;
// 先从缓存获取
Movie movie = (Movie) redisTemplate.opsForValue().get(cacheKey);
if (movie != null) {
return movie;
}
// 缓存不存在,查询数据库
movie = movieMapper.selectById(movieId);
if (movie != null) {
// 存入缓存
redisTemplate.opsForValue().set(cacheKey, movie, CACHE_EXPIRE, TimeUnit.SECONDS);
} else {
// 防止缓存穿透,缓存空值
redisTemplate.opsForValue().set(cacheKey, new Movie(), 5, TimeUnit.MINUTES);
}
return movie;
}
public void evictMovieCache(Long movieId) {
String cacheKey = MOVIE_CACHE_PREFIX + movieId;
redisTemplate.delete(cacheKey);
}
}
5.2 数据库优化
- 索引优化:为常用查询字段添加合适索引
- 分表策略:订单表按月分表
- SQL优化:避免全表扫描,使用覆盖索引
- 连接池配置:合理设置连接池参数
sql复制-- 示例:优化后的订单查询SQL
EXPLAIN
SELECT o.order_no, o.create_time, o.total_amount, m.title, c.name AS cinema_name
FROM order_202307 o
JOIN movie_schedule ms ON o.schedule_id = ms.id
JOIN movie m ON ms.movie_id = m.id
JOIN cinema c ON ms.cinema_id = c.id
WHERE o.user_id = 12345
AND o.status = 1
ORDER BY o.create_time DESC
LIMIT 10;
6. 安全防护措施
6.1 常见安全风险防护
- XSS防护:前端输入过滤+后端转义处理
- CSRF防护:接口添加Token验证
- SQL注入:使用预编译语句
- 越权访问:接口权限校验
- 数据脱敏:敏感信息加密存储
java复制// 权限校验拦截器示例
@Component
public class AuthInterceptor implements HandlerInterceptor {
@Autowired
private JwtUtil jwtUtil;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 获取token
String token = request.getHeader("Authorization");
if (StringUtils.isBlank(token)) {
throw new BusinessException(ErrorCode.NOT_LOGIN);
}
// 验证token
Claims claims = jwtUtil.parseToken(token);
if (claims == null) {
throw new BusinessException(ErrorCode.TOKEN_INVALID);
}
// 检查用户状态
Long userId = claims.get("userId", Long.class);
User user = userService.getById(userId);
if (user == null || user.getStatus() != 1) {
throw new BusinessException(ErrorCode.ACCOUNT_DISABLED);
}
// 权限校验
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
RequireRole requireRole = handlerMethod.getMethodAnnotation(RequireRole.class);
if (requireRole != null) {
String[] roles = requireRole.value();
if (!ArrayUtils.contains(roles, user.getRole())) {
throw new BusinessException(ErrorCode.PERMISSION_DENIED);
}
}
}
// 将用户信息存入请求上下文
RequestContext.setCurrentUser(user);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 清除请求上下文
RequestContext.clear();
}
}
6.2 支付安全
- 支付参数签名验证
- 支付结果异步通知验证
- 支付状态机设计
- 支付超时处理
- 防重复支付机制
java复制// 支付状态机示例
public enum PayStatus {
UNPAID(0, "未支付"),
PAYING(1, "支付中"),
PAID(2, "已支付"),
FAILED(3, "支付失败"),
REFUNDING(4, "退款中"),
REFUNDED(5, "已退款");
private int code;
private String desc;
// 省略构造方法和getter
private static final Map<Integer, PayStatus> CODE_MAP = new HashMap<>();
static {
for (PayStatus status : values()) {
CODE_MAP.put(status.code, status);
}
}
public static PayStatus fromCode(int code) {
return CODE_MAP.get(code);
}
public boolean canTransitionTo(PayStatus target) {
switch (this) {
case UNPAID:
return target == PAYING || target == FAILED;
case PAYING:
return target == PAID || target == FAILED;
case PAID:
return target == REFUNDING;
case FAILED:
return target == PAYING;
case REFUNDING:
return target == REFUNDED;
default:
return false;
}
}
}
7. 部署与运维
7.1 服务器部署方案
我们采用Docker容器化部署方案,主要组件包括:
- Nginx:反向代理和负载均衡
- Spring Boot应用:运行业务逻辑
- MySQL:主从复制架构
- Redis:哨兵模式集群
- Elasticsearch:搜索和日志分析
yaml复制# docker-compose.yml示例
version: '3'
services:
nginx:
image: nginx:1.21
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d
- ./nginx/logs:/var/log/nginx
depends_on:
- app
app:
image: cinema-app:1.0
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
- DB_URL=jdbc:mysql://mysql:3306/cinema
- REDIS_HOST=redis
depends_on:
- mysql
- redis
mysql:
image: mysql:8.0
ports:
- "3306:3306"
environment:
- MYSQL_ROOT_PASSWORD=root123
- MYSQL_DATABASE=cinema
volumes:
- ./mysql/data:/var/lib/mysql
redis:
image: redis:6.2
ports:
- "6379:6379"
command: redis-server --appendonly yes
volumes:
- ./redis/data:/data
7.2 监控与告警
- Prometheus + Grafana监控系统指标
- ELK收集和分析日志
- 关键业务指标监控:
- 订单创建成功率
- 支付成功率
- 接口响应时间
- 异常告警:通过邮件/短信/企业微信通知
8. 项目总结与改进方向
这个影院购票管理系统作为毕业设计项目,完整实现了从需求分析、系统设计到编码实现的全过程。在实际开发过程中,我遇到了不少挑战,特别是在高并发场景下的数据一致性问题和微信支付集成方面积累了很多经验。
系统的主要亮点:
- 完整的微信小程序用户端体验
- 可靠的座位锁定机制
- 灵活的支付状态管理
- 完善的监控体系
未来改进方向:
- 引入微服务架构提高系统扩展性
- 增加大数据分析模块,提供更精准的推荐
- 实现多影院连锁管理功能
- 开发管理端APP提升运营效率
在实际部署这个系统时,有几个特别需要注意的地方:首先是微信支付资质的申请需要提前准备,通常需要1-2周时间;其次是座位锁定超时时间的设置需要根据实际业务场景调整,太短会影响用户体验,太长会影响座位周转率;最后是监控系统的搭建不能忽视,线上环境的稳定性至关重要。