ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Spring Boot与Vue构建动物园票务系统实践

Spring Boot与Vue构建动物园票务系统实践 1. 项目背景与需求分析动物园作为城市重要的休闲娱乐场所每年吸引大量游客参观。然而传统的人工售票窗口模式存在诸多痛点高峰期排队购票时间长、票务信息更新不及时、游客无法提前规划行程等。这些问题直接影响游客体验和园区运营效率。我在实地调研北京某中型动物园时发现周末购票队伍经常排到50米开外平均等待时间超过30分钟。而园区管理人员也反馈人工统计票务数据效率低下难以及时调整运营策略。这种低效的运营模式显然无法满足现代游客对便捷服务的需求。基于这些实际问题我们决定开发一套线上票务系统主要解决以下核心需求游客端需求实时查询票务信息和动物展览情况支持在线购票和电子票核验获取园区导览和活动资讯参与社区互动分享游园体验管理端需求高效管理票务库存和价格策略实时监控客流数据和销售情况便捷更新园区资讯和动物信息分析用户行为优化运营策略2. 技术选型与架构设计2.1 技术栈选型考量经过对多个技术方案的评估我们最终确定以下技术栈后端技术Spring Boot 2.7.3提供快速开发能力和丰富的starter组件Spring Security处理认证授权和安全防护MyBatis-Plus简化数据库操作提高开发效率Redis 6.x缓存热点数据提升系统响应速度前端技术Vue 3.x组件化开发良好的生态支持Element Plus提供丰富的UI组件Axios处理HTTP请求ECharts数据可视化展示数据库MySQL 8.0关系型数据库ACID特性保障数据一致性采用InnoDB引擎支持事务和行级锁基础设施Nginx反向代理和负载均衡Tomcat 9.x应用服务器Jenkins持续集成和部署选择这些技术主要基于以下考虑Spring Boot的自动配置和起步依赖能大幅减少配置工作Vue的响应式特性适合频繁交互的前端场景MySQL成熟稳定社区支持完善整套技术栈都有良好的中文文档和社区支持2.2 系统架构设计系统采用典型的分层架构分为表现层、应用层、服务层和数据层┌───────────────────────────────────────┐ │ 表现层 │ │ ┌───────────┐ ┌─────────────┐ │ │ │ Web前端 │ │ 移动端API │ │ │ └───────────┘ └─────────────┘ │ └───────────────────────────────────────┘ ┌───────────────────────────────────────┐ │ 应用层 │ │ ┌─────────────────────────────────┐ │ │ │ Spring Boot应用 │ │ │ └─────────────────────────────────┘ │ └───────────────────────────────────────┘ ┌───────────────────────────────────────┐ │ 服务层 │ │ ┌───────────┐ ┌─────────────┐ │ │ │ 业务逻辑 │ │ 缓存服务 │ │ │ └───────────┘ └─────────────┘ │ └───────────────────────────────────────┘ ┌───────────────────────────────────────┐ │ 数据层 │ │ ┌───────────┐ ┌─────────────┐ │ │ │ MySQL │ │ Redis │ │ │ └───────────┘ └─────────────┘ │ └───────────────────────────────────────┘这种架构的优势在于各层职责明确便于团队协作水平扩展能力强可根据负载单独扩展某一层技术栈替换成本低例如可以轻松更换前端框架3. 核心功能模块实现3.1 用户认证模块认证模块采用JWT(JSON Web Token)方案流程如下用户登录成功后服务端生成包含用户信息的JWT客户端存储JWT(通常放在localStorage)后续请求在Authorization头中携带JWT服务端验证JWT有效性并处理请求关键代码实现// JWT生成 public String generateToken(UserDetails userDetails) { MapString, Object claims new HashMap(); return Jwts.builder() .setClaims(claims) .setSubject(userDetails.getUsername()) .setIssuedAt(new Date(System.currentTimeMillis())) .setExpiration(new Date(System.currentTimeMillis() JWT_TOKEN_VALIDITY * 1000)) .signWith(SignatureAlgorithm.HS512, secret) .compact(); } // JWT验证过滤器 public class JwtRequestFilter extends OncePerRequestFilter { Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { final String requestTokenHeader request.getHeader(Authorization); if (requestTokenHeader ! null requestTokenHeader.startsWith(Bearer )) { String jwtToken requestTokenHeader.substring(7); try { String username jwtUtil.getUsernameFromToken(jwtToken); if (username ! null SecurityContextHolder.getContext().getAuthentication() null) { UserDetails userDetails this.userDetailsService.loadUserByUsername(username); if (jwtUtil.validateToken(jwtToken, userDetails)) { UsernamePasswordAuthenticationToken authenticationToken new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); authenticationToken.setDetails( new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authenticationToken); } } } catch (Exception e) { logger.error(Unable to get JWT Token); } } chain.doFilter(request, response); } }安全提示JWT应设置合理的过期时间(通常2-4小时)敏感操作应要求重新认证3.2 票务管理模块票务模块核心实体关系CREATE TABLE ticket ( id bigint NOT NULL AUTO_INCREMENT, park_id bigint NOT NULL COMMENT 园区ID, type varchar(20) NOT NULL COMMENT 票种类型, price decimal(10,2) NOT NULL COMMENT 票价, stock int NOT NULL COMMENT 库存, start_date date NOT NULL COMMENT 有效开始日期, end_date date NOT NULL COMMENT 有效结束日期, status tinyint NOT NULL DEFAULT 1 COMMENT 状态1-上架 0-下架, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_park (park_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT门票信息表; CREATE TABLE ticket_order ( id bigint NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 订单编号, user_id bigint NOT NULL COMMENT 用户ID, ticket_id bigint NOT NULL COMMENT 门票ID, quantity int NOT NULL COMMENT 购买数量, total_amount decimal(10,2) NOT NULL COMMENT 总金额, status tinyint NOT NULL DEFAULT 0 COMMENT 状态0-待支付 1-已支付 2-已取消, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user (user_id), KEY idx_ticket (ticket_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT门票订单表;购票业务流程实现要点库存控制采用乐观锁防止超卖Transactional public OrderResult createOrder(OrderRequest request) { // 查询票务信息 Ticket ticket ticketMapper.selectById(request.getTicketId()); if (ticket null || ticket.getStatus() ! 1) { throw new BusinessException(门票不存在或已下架); } // 校验库存 if (ticket.getStock() request.getQuantity()) { throw new BusinessException(门票库存不足); } // 扣减库存(乐观锁) int updateCount ticketMapper.reduceStock(ticket.getId(), request.getQuantity()); if (updateCount 0) { throw new BusinessException(门票库存不足请重新下单); } // 生成订单 TicketOrder order new TicketOrder(); order.setOrderNo(generateOrderNo()); order.setUserId(request.getUserId()); // 其他字段设置... orderMapper.insert(order); // 返回结果 return new OrderResult(order.getOrderNo(), order.getTotalAmount()); }订单状态管理使用状态机模式public enum OrderStatus { PENDING(0, 待支付) { Override public boolean canChangeTo(OrderStatus nextStatus) { return nextStatus PAID || nextStatus CANCELLED; } }, PAID(1, 已支付) { Override public boolean canChangeTo(OrderStatus nextStatus) { return nextStatus COMPLETED || nextStatus REFUNDED; } }, // 其他状态... private final int code; private final String desc; public abstract boolean canChangeTo(OrderStatus nextStatus); // getters... }支付集成对接微信支付和支付宝public interface PaymentService { PaymentResult createPayment(Order order); boolean verifyPayment(PaymentNotify notify); } Service RequiredArgsConstructor public class WechatPaymentService implements PaymentService { private final WechatPayConfig config; Override public PaymentResult createPayment(Order order) { // 构建微信支付请求参数 MapString, String params new HashMap(); params.put(appid, config.getAppId()); params.put(mch_id, config.getMchId()); params.put(nonce_str, generateNonceStr()); params.put(body, 动物园门票- order.getOrderNo()); params.put(out_trade_no, order.getOrderNo()); params.put(total_fee, order.getTotalAmount().multiply(new BigDecimal(100)).intValue()); params.put(spbill_create_ip, order.getClientIp()); params.put(notify_url, config.getNotifyUrl()); params.put(trade_type, JSAPI); params.put(openid, order.getOpenid()); // 签名并发送请求 params.put(sign, generateSign(params)); String response sendRequest(config.getUnifiedOrderUrl(), params); // 解析响应 return parseResponse(response); } // 其他方法... }3.3 动物信息管理模块动物信息采用富文本编辑和标签分类管理实体设计Entity Table(name animal) Data public class Animal { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; NotBlank Size(max 50) private String name; ManyToOne JoinColumn(name category_id) private AnimalCategory category; Lob private String description; ElementCollection CollectionTable(name animal_image, joinColumns JoinColumn(name animal_id)) Column(name url) private ListString images; Embedded private AnimalHabitat habitat; private Integer viewCount 0; CreationTimestamp private LocalDateTime createTime; UpdateTimestamp private LocalDateTime updateTime; } Embeddable Data public class AnimalHabitat { private String region; private String climate; private String diet; }信息展示优化使用Redis缓存热门动物信息实现增量更新浏览计数Cacheable(value animal, key #id) public Animal getAnimalDetail(Long id) { return animalRepository.findById(id) .orElseThrow(() - new ResourceNotFoundException(Animal not found)); } Scheduled(fixedRate 60000) public void syncViewCount() { MapLong, Long viewCounts redisTemplate.opsForHash().entries(animal:view); viewCounts.forEach((animalId, count) - { animalRepository.updateViewCount(animalId, count); }); redisTemplate.delete(animal:view); }4. 系统部署与性能优化4.1 生产环境部署方案推荐使用Docker Compose部署version: 3.8 services: mysql: image: mysql:8.0 container_name: zoo_mysql environment: MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD} MYSQL_DATABASE: zoo MYSQL_USER: zoo MYSQL_PASSWORD: ${DB_PASSWORD} volumes: - mysql_data:/var/lib/mysql ports: - 3306:3306 networks: - zoo_net redis: image: redis:6.2 container_name: zoo_redis ports: - 6379:6379 networks: - zoo_net backend: build: context: . dockerfile: Dockerfile.backend container_name: zoo_backend depends_on: - mysql - redis environment: SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/zoo SPRING_DATASOURCE_USERNAME: zoo SPRING_DATASOURCE_PASSWORD: ${DB_PASSWORD} SPRING_REDIS_HOST: redis ports: - 8080:8080 networks: - zoo_net frontend: build: context: . dockerfile: Dockerfile.frontend container_name: zoo_frontend ports: - 80:80 networks: - zoo_net nginx: image: nginx:1.21 container_name: zoo_nginx volumes: - ./nginx.conf:/etc/nginx/nginx.conf ports: - 443:443 - 8000:8000 depends_on: - backend - frontend networks: - zoo_net volumes: mysql_data: networks: zoo_net: driver: bridge4.2 性能优化实践数据库优化为常用查询字段添加索引大表进行分库分表设计使用读写分离减轻主库压力缓存策略多级缓存本地缓存 Redis热点数据预加载缓存雪崩防护随机过期时间接口优化合并细粒度接口实现分页查询使用DTO投影减少数据传输量public interface TicketProjection { Long getId(); String getType(); BigDecimal getPrice(); Value(#{target.park.name}) String getParkName(); } Repository public interface TicketRepository extends JpaRepositoryTicket, Long { Query(SELECT t.id as id, t.type as type, t.price as price, p.name as parkName FROM Ticket t JOIN t.park p WHERE t.status 1) PageTicketProjection findAvailableTickets(Pageable pageable); }5. 项目总结与经验分享在开发过程中我们遇到并解决了几个关键问题高并发售票场景最初使用悲观锁导致性能瓶颈最终采用Redis分布式锁数据库乐观锁的组合方案通过压力测试验证系统在1000并发下仍能保持稳定移动端适配发现部分安卓设备显示异常通过rem布局和flexible.js实现更好的响应式针对低端设备优化图片加载策略安全防护遭遇过几次简单的SQL注入尝试通过MyBatis参数绑定和输入过滤彻底防护添加接口限流防止恶意刷单对于想要开发类似系统的同学我的建议是前期充分调研业务需求特别是票务规则和退改政策设计阶段就考虑扩展性例如多园区支持重视监控系统建设及时发现问题编写全面的单元测试和集成测试这个项目让我深刻理解了如何将一个传统业务数字化转型。从最初的业务分析到最终上线每个环节都需要严谨的态度和扎实的技术能力。特别是在处理高并发场景时需要考虑各种边界条件和异常情况。
返回列表