1. 项目概述前后端分离影院购票系统全栈实现这个基于SpringBootVueMyBatisMySQL的影院购票系统是我去年为本地连锁影院开发的线上票务平台。系统上线后日均订单量稳定在3000经受住了节假日流量高峰的考验。相比传统单体架构前后端分离设计让我们的迭代效率提升了60%前端团队和后端团队可以并行开发互不干扰。核心业务模块包括影院影片管理CRUD排期在线选座购票实时座位锁定订单支付处理第三方支付对接会员积分系统消费累计兑换数据统计看板ECharts可视化技术栈选型上SpringBoot 2.7提供了开箱即用的企业级特性Vue 3的组合式API让前端组件更易维护MyBatis-Plus 3.5的代码生成器节省了80%的重复CRUD代码MySQL 8.0的窗口函数极大简化了复杂统计查询。关键提示系统采用JWTRBAC实现权限控制购票环节使用Redis分布式锁防止超卖这些设计在后续章节会详细展开2. 技术架构深度解析2.1 后端SpringBoot设计要点采用经典的三层架构com.cinema ├── config # 安全/缓存/消息队列配置 ├── controller # 对外接口层 ├── service # 业务逻辑层 │ ├── impl # 实现类 ├── dao # 数据访问层 ├── entity # 实体类 └── util # 工具包重点配置类说明SecurityConfig配置JWT过滤器和权限拦截Configuration EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .addFilter(new JwtAuthenticationFilter(authenticationManager())) .authorizeRequests() .antMatchers(/api/public/**).permitAll() .anyRequest().authenticated(); } }RedisConfig定义缓存模板和序列化方式Bean public RedisTemplateString, Object redisTemplate(RedisConnectionFactory factory) { RedisTemplateString, Object template new RedisTemplate(); template.setConnectionFactory(factory); template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); return template; }2.2 前端Vue工程化实践使用Vue CLI 5创建的工程结构src/ ├── api/ # Axios请求封装 ├── assets/ # 静态资源 ├── components/ # 公共组件 │ ├── SeatMap.vue # 影院座位组件 ├── router/ # 路由配置 ├── store/ # Vuex状态管理 ├── utils/ # 工具函数 └── views/ # 页面组件关键技术点动态路由注册根据用户权限过滤路由// 在路由守卫中处理 router.beforeEach(async (to, from, next) { const hasToken localStorage.getItem(token) if (to.matched.some(record record.meta.requiresAuth)) { if (!hasToken) { next(/login) } else { if (!store.getters.roles.length) { await store.dispatch(user/getInfo) const accessRoutes await store.dispatch(permission/generateRoutes) router.addRoutes(accessRoutes) next({ ...to, replace: true }) } else { next() } } } else { next() } })选座组件实现使用Canvas绘制影院座位图template canvas refseatCanvas clickhandleSeatClick/canvas /template script export default { methods: { drawSeatMap() { const ctx this.$refs.seatCanvas.getContext(2d) // 绘制座位逻辑 seats.forEach(seat { ctx.beginPath() ctx.arc(seat.x, seat.y, 10, 0, Math.PI * 2) ctx.fillStyle seat.status available ? #4CAF50 : #F44336 ctx.fill() }) } } } /script3. 核心业务模块实现3.1 购票业务流程设计完整购票时序图文字描述版用户选择影片场次系统查询Redis获取座位状态用户选择座位并提交后端尝试获取Redis分布式锁锁获取成功后检查座位可用性生成预订单状态为待支付设置15分钟订单过期时间用户完成支付后更新订单状态为已支付扣减库存发送短信通知释放Redis锁关键代码片段public Result createOrder(OrderDTO dto) { String lockKey lock:schedule: dto.getScheduleId(); try { // 尝试获取分布式锁 boolean locked redisTemplate.opsForValue() .setIfAbsent(lockKey, 1, 30, TimeUnit.SECONDS); if (!locked) { return Result.fail(当前购票人数过多请稍后再试); } // 验证座位状态 ListSeat seats checkSeatAvailable(dto.getSeatIds()); // 创建订单 Order order new Order(); order.setStatus(OrderStatus.UNPAID); orderMapper.insert(order); // 设置订单过期时间 redisTemplate.opsForValue() .set(order:expire: order.getId(), 1, 15, TimeUnit.MINUTES); return Result.success(order.getId()); } finally { // 释放锁 redisTemplate.delete(lockKey); } }3.2 支付对接实现系统支持微信支付和支付宝支付双通道采用策略模式设计支付处理器public interface PaymentHandler { PaymentResult pay(PaymentRequest request); PaymentResult query(String orderNo); PaymentResult refund(RefundRequest request); } Service RequiredArgsConstructor public class PaymentService { private final MapString, PaymentHandler handlers; public PaymentResult pay(String channel, PaymentRequest request) { PaymentHandler handler handlers.get(channel PaymentHandler); if (handler null) { throw new BizException(不支持的支付渠道); } return handler.pay(request); } }微信支付回调处理示例PostMapping(/wxpay/callback) public String wxpayCallback(HttpServletRequest request) { try (InputStream is request.getInputStream()) { // 验证签名 WXPay wxPay new WXPay(config); MapString, String notifyData wxPay.processResponseXml(is); if (SUCCESS.equals(notifyData.get(return_code))) { String orderNo notifyData.get(out_trade_no); orderService.handlePaySuccess(orderNo); return xmlreturn_codeSUCCESS/return_code/xml; } } catch (Exception e) { log.error(微信支付回调处理失败, e); } return xmlreturn_codeFAIL/return_code/xml; }4. 系统部署实战指南4.1 生产环境部署方案推荐服务器配置前端服务器Nginx Vue打包产物2核4G后端服务器SpringBoot Jar包4核8GJDK17数据库MySQL 8.0主从配置8核16G缓存Redis哨兵集群3节点每节点2核4GNginx关键配置server { listen 80; server_name cinema.example.com; location / { root /var/www/cinema-web; try_files $uri $uri/ /index.html; } location /api { proxy_pass http://backend-server:8080; proxy_set_header X-Real-IP $remote_addr; proxy_connect_timeout 75s; } }SpringBoot应用启动参数java -Xms2048m -Xmx2048m \ -XX:UseG1GC \ -Dspring.profiles.activeprod \ -jar cinema-backend.jar4.2 持续集成部署使用Jenkins构建部署流水线代码检出阶段stage(Checkout) { git branch: main, url: gitgithub.com:username/cinema-system.git }后端构建阶段stage(Build Backend) { sh ./mvnw clean package -DskipTests archiveArtifacts artifacts: backend/target/*.jar, fingerprint: true }前端构建阶段stage(Build Frontend) { sh cd frontend npm install npm run build stash includes: frontend/dist/**, name: frontend-dist }部署阶段stage(Deploy) { sshPublisher( publishers: [ sshPublisherDesc( configName: production-server, transfers: [ sshTransfer( sourceFiles: backend/target/*.jar, removePrefix: backend/target, remoteDirectory: /opt/cinema, execCommand: sudo systemctl restart cinema.service ) ] ) ] ) }5. 性能优化实战记录5.1 数据库优化措施索引优化为高频查询字段添加组合索引ALTER TABLE t_order ADD INDEX idx_user_status (user_id, status), ADD INDEX idx_schedule_seat (schedule_id, seat_id);慢查询优化使用EXPLAIN分析执行计划-- 优化前全表扫描 SELECT * FROM t_seat WHERE schedule_id ? AND status 1; -- 优化后覆盖索引 SELECT seat_id, row_num, col_num FROM t_seat WHERE schedule_id ? AND status 1 ORDER BY row_num, col_num;分表策略按月份对订单表进行水平拆分TableName(t_order_#{#tableSuffix}) public class Order { // 动态表名处理器 public static String getTableSuffix(Date createTime) { return new SimpleDateFormat(yyyyMM).format(createTime); } }5.2 前端性能提升组件懒加载const SeatMap () import(./views/SeatMap.vue)接口请求合并使用GraphQL替代RESTfulquery { movie(id: 123) { title duration schedules { time hall { name } } } }Webpack分包策略configureWebpack: { optimization: { splitChunks: { chunks: all, maxSize: 244 * 1024 // 244KB } } }6. 踩坑实录与解决方案6.1 分布式环境下的定时任务问题现象集群环境下多个节点同时执行定时任务导致重复处理解决方案基于Redis实现分布式锁控制Scheduled(cron 0 0/5 * * * ?) public void checkExpiredOrders() { String lockKey job:checkExpiredOrders; try { Boolean locked redisTemplate.opsForValue() .setIfAbsent(lockKey, 1, 4, TimeUnit.MINUTES); if (Boolean.TRUE.equals(locked)) { // 执行业务逻辑 processExpiredOrders(); } } finally { redisTemplate.delete(lockKey); } }6.2 Vue响应式数据丢失问题场景编辑表单时直接赋值导致响应性丢失// 错误写法 this.form response.data // 正确写法保持响应性 this.form Object.assign({}, this.form, response.data)6.3 MyBatis批量插入优化原始方案循环单条插入性能差for (OrderItem item : items) { orderItemMapper.insert(item); }优化方案使用批量插入语法insert idbatchInsert parameterTypelist INSERT INTO t_order_item (order_id, seat_id, price) VALUES foreach collectionlist itemitem separator, (#{item.orderId}, #{item.seatId}, #{item.price}) /foreach /insert性能对比100条记录从1200ms → 80ms1000条记录从12s → 300ms7. 安全防护体系构建7.1 接口安全设计参数签名验证public boolean verifySign(HttpServletRequest request) { String requestSign request.getHeader(X-Sign); MapString, String params getSortedParams(request); String serverSign generateSign(params, SECRET_KEY); return serverSign.equals(requestSign); }SQL注入防护使用MyBatis预编译禁止拼接SQL语句定期使用SQLMap扫描7.2 敏感数据保护密码加密存储public String encryptPassword(String rawPassword) { return new BCryptPasswordEncoder().encode(rawPassword); }数据脱敏处理public static String maskMobile(String mobile) { if (StringUtils.isBlank(mobile)) return ; return mobile.replaceAll((\\d{3})\\d{4}(\\d{4}), $1****$2); }HTTPS强制启用Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { Override protected void configure(HttpSecurity http) throws Exception { http.requiresChannel() .requestMatchers(r - r.getHeader(X-Forwarded-Proto) ! null) .requiresSecure(); } }8. 监控与日志体系建设8.1 Prometheus监控配置SpringBoot集成示例Bean public MeterRegistryCustomizerPrometheusMeterRegistry metricsCommonTags() { return registry - registry.config().commonTags( application, cinema-backend, region, System.getenv(REGION) ); }关键监控指标系统指标CPU/Memory/DiskJVM指标GC次数/堆内存业务指标订单创建QPS/支付成功率8.2 ELK日志收集方案Logback配置示例appender nameLOGSTASH classnet.logstash.logback.appender.LogstashTcpSocketAppender destinationlogstash:5044/destination encoder classnet.logstash.logback.encoder.LogstashEncoder customFields{app:cinema-backend,env:${spring.profiles.active}}/customFields /encoder /appender日志查询技巧# 查找错误日志 level:ERROR AND app:cinema-backend # 追踪特定订单 message:orderId20230815123456 # 统计接口耗时 metrics.http.server.requests.duration:10009. 扩展功能开发思路9.1 小程序端适配方案技术选型原生小程序 Taro跨端框架接口复用现有后端APIWebSocket实现实时通知样式适配技巧/* 通用样式 */ .seat { width: 20px; height: 20px; /* 小程序需替换单位 */ width: 20rpx; height: 20rpx; }9.2 大数据分析扩展数据流向设计MySQL - Canal - Kafka - Flink - HBase - ElasticsearchFlink实时计算示例DataStreamOrderEvent orders env .addSource(new KafkaSource()) .keyBy(OrderEvent::getMovieId) .window(TumblingProcessingTimeWindows.of(Time.hours(1))) .aggregate(new OrderAggregator()); public static class OrderAggregator implements AggregateFunctionOrderEvent, MovieStats, MovieStats { Override public MovieStats add(OrderEvent value, MovieStats accumulator) { accumulator.setTotalSales(accumulator.getTotalSales() value.getAmount()); accumulator.setTicketCount(accumulator.getTicketCount() 1); return accumulator; } }10. 项目演进路线图10.1 技术债清理计划代码重构提取重复工具类到common模块统一异常处理规范接口文档自动化Swagger → OpenAPI 3.0架构升级服务拆分用户服务/订单服务/排片服务引入Spring Cloud Alibaba配置中心迁移到Nacos10.2 智能化改造方向推荐系统基于用户历史的协同过滤实时热度排行榜# 简易推荐算法示例 def recommend(user_id): viewed_movies get_view_history(user_id) similar_users find_similar_users(user_id) return [ movie for movie in get_top_movies(similar_users) if movie not in viewed_movies ]动态定价基于上座率的弹性定价节假日溢价算法public BigDecimal calculateDynamicPrice(Schedule schedule) { double basePrice schedule.getBasePrice(); double occupancyRate getOccupancyRate(schedule.getId()); // 基础算法上座率每增加10%价格上浮5% double rateFactor 1 (Math.floor(occupancyRate * 10) * 0.05); return BigDecimal.valueOf(basePrice * rateFactor) .setScale(2, RoundingMode.HALF_UP); }11. 项目源码解析要点11.1 核心类说明SeatService处理座位状态同步public void lockSeats(Long scheduleId, ListLong seatIds) { // 1. 数据库状态更新 seatMapper.updateStatus(scheduleId, seatIds, SeatStatus.LOCKED); // 2. Redis缓存更新 String key schedule:seats: scheduleId; seatIds.forEach(seatId - redisTemplate.opsForHash().put(key, seatId.toString(), SeatStatus.LOCKED) ); // 3. 发布领域事件 eventPublisher.publishEvent(new SeatLockedEvent(scheduleId, seatIds)); }PaymentCallbackHandler支付结果处理策略public interface PaymentCallbackHandler { boolean support(String paymentType); PaymentResult handle(MapString, String params); } Service public class AlipayCallbackHandler implements PaymentCallbackHandler { Override public boolean support(String paymentType) { return alipay.equals(paymentType); } Override public PaymentResult handle(MapString, String params) { // 验签逻辑 if (!AlipaySignature.rsaCheckV1(params, ALIPAY_PUBLIC_KEY)) { throw new BizException(支付宝验签失败); } // 业务处理 String orderNo params.get(out_trade_no); return orderService.handlePaySuccess(orderNo); } }11.2 设计模式应用策略模式在支付渠道选择中的应用startuml interface PaymentStrategy { pay(amount): PaymentResult refund(orderNo): RefundResult } class AlipayStrategy implements PaymentStrategy class WechatPayStrategy implements PaymentStrategy class UnionPayStrategy implements PaymentStrategy class PaymentContext { -strategy: PaymentStrategy executePay(amount) executeRefund(orderNo) } PaymentContext o-- PaymentStrategy enduml观察者模式在订单状态变更中的应用// 定义事件 public class OrderPaidEvent extends ApplicationEvent { private Long orderId; public OrderPaidEvent(Object source, Long orderId) { super(source); this.orderId orderId; } } // 事件监听器 Component public class OrderEventListener { EventListener public void handleOrderPaid(OrderPaidEvent event) { // 发送短信通知 smsService.sendPaymentSuccess(event.getOrderId()); // 更新会员积分 memberService.addPoints(event.getOrderId()); } }12. 项目部署常见问题排查12.1 前端部署问题静态资源404错误检查Nginx配置中的root路径确认Vue打包时的publicPath配置清理浏览器缓存测试接口跨域问题location /api { add_header Access-Control-Allow-Origin $http_origin; add_header Access-Control-Allow-Methods GET,POST,OPTIONS; add_header Access-Control-Allow-Headers Content-Type,Authorization; if ($request_method OPTIONS) { return 204; } proxy_pass http://backend:8080; }12.2 后端启动故障数据库连接失败检查application-prod.yml中的JDBC URL确认数据库白名单设置测试网络连通性telnet mysql_host 3306Redis连接超时spring: redis: timeout: 3000 # 适当增大超时时间 lettuce: pool: max-active: 50 max-wait: 100012.3 性能问题排查接口响应慢使用Arthas trace命令追踪方法耗时trace com.cinema.service.OrderService createOrder内存泄漏检测生成堆转储文件jmap -dump:live,formatb,fileheap.hprof pid使用MAT工具分析13. 项目二次开发建议13.1 功能扩展方向会员等级体系基于消费金额的自动升级差异化折扣策略CREATE TABLE t_member_level ( id BIGINT PRIMARY KEY, level_name VARCHAR(50), min_points INT, discount DECIMAL(3,2) );退票功能实现public Result refundOrder(Long orderId) { // 1. 校验订单状态 Order order checkRefundable(orderId); // 2. 执行退款 PaymentResult result paymentService.refund( order.getPaymentChannel(), order.getOrderNo(), order.getActualAmount() ); // 3. 更新订单状态 order.setStatus(OrderStatus.REFUNDED); orderMapper.updateById(order); // 4. 释放座位 seatService.unlockSeats(order.getScheduleId(), order.getSeatIds()); return Result.success(); }13.2 技术升级路径SpringBoot 3.0迁移JDK基线升级到17Jakarta EE 9命名空间变更废弃API替换清单Vue 3组合式API重构script setup import { ref, computed } from vue const seats ref([]) const selectedSeats computed(() seats.value.filter(s s.selected) ) async function loadSeats() { seats.value await api.getSeats() } /scriptMyBatis-Plus新特性动态表名处理器多租户SQL解析器自动填充优化14. 商业价值与运营思路14.1 盈利模式设计基础票务收入票价分成通常10-15%服务费固定金额/票增值服务优选座位加价退票保险零食套餐预售广告变现影片预告片贴片影院首页轮播图购票完成页推广14.2 数据驱动运营用户行为分析热力图分析选座偏好购票路径转化漏斗SELECT COUNT(DISTINCT session_id) AS view_count, COUNT(DISTINCT CASE WHEN page_type payment THEN session_id END) AS payment_count, COUNT(DISTINCT CASE WHEN status paid THEN order_id END) AS success_count FROM user_behavior WHERE create_time BETWEEN ? AND ?排片优化建议def optimize_schedule(movie_popularity, historical_data): # 基于机器学习的排片模型 model load_model(schedule_model.h5) return model.predict({ popularity: movie_popularity, history: historical_data })15. 项目完整部署checklist15.1 前置依赖检查服务器环境[ ] JDK 11 安装[ ] Node.js 16 安装[ ] MySQL 8.0 配置[ ] Redis 6 运行端口开放确认[ ] 80/443 (Nginx)[ ] 8080 (SpringBoot)[ ] 3306 (MySQL)[ ] 6379 (Redis)15.2 分步部署流程数据库初始化mysql -u root -p database/cinema_schema.sql mysql -u root -p database/initial_data.sql后端服务部署# 打包 mvn clean package -Pprod -DskipTests # 启动 nohup java -jar cinema-backend.jar backend.log 21 前端资源部署# 安装依赖 npm install --registryhttps://registry.npmmirror.com # 构建生产包 npm run build # 同步到Nginx目录 rsync -avz dist/* /usr/share/nginx/cinema/15.3 部署后验证基础功能测试[ ] 用户注册登录[ ] 影片查询筛选[ ] 选座购票流程[ ] 订单支付回调压力测试建议# 使用wrk模拟并发 wrk -t4 -c100 -d60s --latency http://localhost/api/movies监控指标确认[ ] Prometheus指标采集正常[ ] 应用日志输出完整[ ] 数据库慢查询监控16. 项目演进与团队协作建议16.1 代码质量管理Git分支策略main - 生产环境对应分支 release/* - 预发布分支 feature/* - 功能开发分支 hotfix/* - 紧急修复分支代码审查要点接口参数校验完整性事务边界合理性异常处理完备性SQL注入风险排查静态代码分析plugin groupIdorg.sonarsource.scanner.maven/groupId artifactIdsonar-maven-plugin/artifactId version3.9.1/version /plugin16.2 团队协作规范接口文档管理使用Swagger UI实时查看变更时同步更新文档添加必要的示例值前端协作要点定义统一的API响应格式{ code: 200, message: success, data: {} }每日构建制度定时触发Jenkins构建关键指标单元测试通过率 ≥95%代码覆盖率 ≥60%构建耗时 5分钟17. 项目后续优化方向17.1 架构演进路线服务化拆分原单体架构 → 用户服务 影片服务 订单服务 排期服务 支付服务容器化部署FROM openjdk:17-jdk COPY target/cinema-backend.jar /app.jar EXPOSE 8080 ENTRYPOINT [java,-jar,/app.jar]服务网格化引入Istio实现金丝雀发布流量镜像熔断控制17.2 技术深度优化JVM调优方向G1垃圾回收器参数优化堆外内存泄漏检测JIT编译阈值调整数据库进阶读写分离配置分库分表策略SQL审计系统集成前端性能极致优化WebAssembly应用Service Worker缓存关键渲染路径优化18. 项目完整源码解析18.1 工程结构说明完整项目目录树cinema-system/ ├── backend/ # SpringBoot工程 │ ├── src/ │ │ ├── main/ │ │ │ ├── java/com/cinema/ │ │ │ │ ├── config # 配置类 │ │ │ │ ├── controller # 接口层 │ │ │ │ ├── service # 业务逻辑 │ │ │ │ ├── dao # 数据访问 │ │ │ │ ├── entity # 数据实体 │ │ │ │ └── util # 工具类 │ │ │ └── resources/ │ │ │ ├── mapper/ # MyBatis映射文件 │ │ │ ├── static/ # 静态资源 │ │ │ ├── templates/ # 模板文件 │ │ │ ├── application.yml # 主配置 │ │ │ └── application-dev.yml # 开发配置 │ │ └── test/ # 测试代码 │ └── pom.xml # Maven配置 │ ├── frontend/ # Vue工程 │ ├── public/ # 静态资源 │ ├── src/ # 源码目录 │ │ ├── api/ # 接口定义 │ │ ├── assets/ # 资源文件 │ │ ├── components/ # 公共组件 │ │ ├── router/ # 路由配置 │ │ ├── store/ # 状态管理 │ │ ├── utils/ # 工具函数 │ │ └── views/ # 页面组件 │ ├── package.json # 依赖配置 │ └── vue.config.js # 构建配置 │ ├── database/ # 数据库脚本 │ ├── cinema_schema.sql # 表结构 │ └── initial_data.sql # 初始数据 │ ├── deployment/ # 部署配置 │ ├── nginx/ # Nginx配置 │ ├── docker/ # Dockerfile │ └── jenkins/ # Jenkinsfile │ └── docs/ # 项目文档 ├── api/ # 接口文档 ├── dev-guide/ # 开发指南 └── operations/ # 运维手册18.2 关键源码解析座位状态同步逻辑Transactional public void syncSeatStatus(Long scheduleId) { // 从数据库读取最新状态 ListSeat dbSeats seatMapper.selectBySchedule(scheduleId); // 更新Redis缓存 String redisKey schedule:seats: scheduleId; redisTemplate.delete(redisKey); MapString, String seatStatusMap dbSeats.stream() .collect(Collectors.toMap( s - s.getId().toString(), s - s.getStatus().name() )); if (!seatStatusMap.isEmpty()) { redisTemplate.opsForHash().putAll(redisKey, seatStatusMap); } // 设置过期时间24小时 redisTemplate.expire(redisKey, 1, TimeUnit.DAYS); }Vuex状态管理设计const store new Vuex.Store({ modules: { user: { state: () ({ token: localStorage.getItem(token) || , info: null }), mutations: { SET_TOKEN(state, token) { state.token token localStorage.setItem(token, token) }, SET_USER_INFO(state, info) { state.info info } }, actions: { async login({ commit }, { username, password }) { const { token } await api.login(username, password) commit(SET_TOKEN, token) const info await api.getUserInfo() commit(SET_USER_INFO, info) } } } } })19. 项目商业应用案例19.1 实际运营数据某连锁影院上线后关键指标线上售票占比从18%提升至65%平均出票时间从3分钟缩短至45秒退票纠纷率下降72%会员复购率提升至38%19.2 定制开发案例特殊场次需求包场业务模块团体票优惠配置学生认证折扣营销活动集成public OrderPrice calculateFinalPrice(Order order, ListPromotion promotions) { OrderPrice price new OrderPrice(); price.setOriginalAmount(order.getTotalAmount()); // 应用优惠券 promotions.stream() .filter(p - p.getType() PromotionType.COUPON) .findFirst() .ifPresent(coupon - { price.setDiscountAmount(coupon.getValue()); price.setCouponId(coupon.getId()); }); // 应用会员折扣 if (order.getMemberLevel() 0) { double discount memberService.getLevelDiscount(order.getMemberLevel()); price.setMemberDiscount(discount); price.setDiscountAmount( price.getDiscountAmount() price.getOriginalAmount() * (1 - discount) ); } price.setActualAmount( price.getOriginalAmount() - price.getDiscountAmount() ); return
SpringBoot+Vue影院购票系统全栈开发实战
1. 项目概述前后端分离影院购票系统全栈实现这个基于SpringBootVueMyBatisMySQL的影院购票系统是我去年为本地连锁影院开发的线上票务平台。系统上线后日均订单量稳定在3000经受住了节假日流量高峰的考验。相比传统单体架构前后端分离设计让我们的迭代效率提升了60%前端团队和后端团队可以并行开发互不干扰。核心业务模块包括影院影片管理CRUD排期在线选座购票实时座位锁定订单支付处理第三方支付对接会员积分系统消费累计兑换数据统计看板ECharts可视化技术栈选型上SpringBoot 2.7提供了开箱即用的企业级特性Vue 3的组合式API让前端组件更易维护MyBatis-Plus 3.5的代码生成器节省了80%的重复CRUD代码MySQL 8.0的窗口函数极大简化了复杂统计查询。关键提示系统采用JWTRBAC实现权限控制购票环节使用Redis分布式锁防止超卖这些设计在后续章节会详细展开2. 技术架构深度解析2.1 后端SpringBoot设计要点采用经典的三层架构com.cinema ├── config # 安全/缓存/消息队列配置 ├── controller # 对外接口层 ├── service # 业务逻辑层 │ ├── impl # 实现类 ├── dao # 数据访问层 ├── entity # 实体类 └── util # 工具包重点配置类说明SecurityConfig配置JWT过滤器和权限拦截Configuration EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .addFilter(new JwtAuthenticationFilter(authenticationManager())) .authorizeRequests() .antMatchers(/api/public/**).permitAll() .anyRequest().authenticated(); } }RedisConfig定义缓存模板和序列化方式Bean public RedisTemplateString, Object redisTemplate(RedisConnectionFactory factory) { RedisTemplateString, Object template new RedisTemplate(); template.setConnectionFactory(factory); template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); return template; }2.2 前端Vue工程化实践使用Vue CLI 5创建的工程结构src/ ├── api/ # Axios请求封装 ├── assets/ # 静态资源 ├── components/ # 公共组件 │ ├── SeatMap.vue # 影院座位组件 ├── router/ # 路由配置 ├── store/ # Vuex状态管理 ├── utils/ # 工具函数 └── views/ # 页面组件关键技术点动态路由注册根据用户权限过滤路由// 在路由守卫中处理 router.beforeEach(async (to, from, next) { const hasToken localStorage.getItem(token) if (to.matched.some(record record.meta.requiresAuth)) { if (!hasToken) { next(/login) } else { if (!store.getters.roles.length) { await store.dispatch(user/getInfo) const accessRoutes await store.dispatch(permission/generateRoutes) router.addRoutes(accessRoutes) next({ ...to, replace: true }) } else { next() } } } else { next() } })选座组件实现使用Canvas绘制影院座位图template canvas refseatCanvas clickhandleSeatClick/canvas /template script export default { methods: { drawSeatMap() { const ctx this.$refs.seatCanvas.getContext(2d) // 绘制座位逻辑 seats.forEach(seat { ctx.beginPath() ctx.arc(seat.x, seat.y, 10, 0, Math.PI * 2) ctx.fillStyle seat.status available ? #4CAF50 : #F44336 ctx.fill() }) } } } /script3. 核心业务模块实现3.1 购票业务流程设计完整购票时序图文字描述版用户选择影片场次系统查询Redis获取座位状态用户选择座位并提交后端尝试获取Redis分布式锁锁获取成功后检查座位可用性生成预订单状态为待支付设置15分钟订单过期时间用户完成支付后更新订单状态为已支付扣减库存发送短信通知释放Redis锁关键代码片段public Result createOrder(OrderDTO dto) { String lockKey lock:schedule: dto.getScheduleId(); try { // 尝试获取分布式锁 boolean locked redisTemplate.opsForValue() .setIfAbsent(lockKey, 1, 30, TimeUnit.SECONDS); if (!locked) { return Result.fail(当前购票人数过多请稍后再试); } // 验证座位状态 ListSeat seats checkSeatAvailable(dto.getSeatIds()); // 创建订单 Order order new Order(); order.setStatus(OrderStatus.UNPAID); orderMapper.insert(order); // 设置订单过期时间 redisTemplate.opsForValue() .set(order:expire: order.getId(), 1, 15, TimeUnit.MINUTES); return Result.success(order.getId()); } finally { // 释放锁 redisTemplate.delete(lockKey); } }3.2 支付对接实现系统支持微信支付和支付宝支付双通道采用策略模式设计支付处理器public interface PaymentHandler { PaymentResult pay(PaymentRequest request); PaymentResult query(String orderNo); PaymentResult refund(RefundRequest request); } Service RequiredArgsConstructor public class PaymentService { private final MapString, PaymentHandler handlers; public PaymentResult pay(String channel, PaymentRequest request) { PaymentHandler handler handlers.get(channel PaymentHandler); if (handler null) { throw new BizException(不支持的支付渠道); } return handler.pay(request); } }微信支付回调处理示例PostMapping(/wxpay/callback) public String wxpayCallback(HttpServletRequest request) { try (InputStream is request.getInputStream()) { // 验证签名 WXPay wxPay new WXPay(config); MapString, String notifyData wxPay.processResponseXml(is); if (SUCCESS.equals(notifyData.get(return_code))) { String orderNo notifyData.get(out_trade_no); orderService.handlePaySuccess(orderNo); return xmlreturn_codeSUCCESS/return_code/xml; } } catch (Exception e) { log.error(微信支付回调处理失败, e); } return xmlreturn_codeFAIL/return_code/xml; }4. 系统部署实战指南4.1 生产环境部署方案推荐服务器配置前端服务器Nginx Vue打包产物2核4G后端服务器SpringBoot Jar包4核8GJDK17数据库MySQL 8.0主从配置8核16G缓存Redis哨兵集群3节点每节点2核4GNginx关键配置server { listen 80; server_name cinema.example.com; location / { root /var/www/cinema-web; try_files $uri $uri/ /index.html; } location /api { proxy_pass http://backend-server:8080; proxy_set_header X-Real-IP $remote_addr; proxy_connect_timeout 75s; } }SpringBoot应用启动参数java -Xms2048m -Xmx2048m \ -XX:UseG1GC \ -Dspring.profiles.activeprod \ -jar cinema-backend.jar4.2 持续集成部署使用Jenkins构建部署流水线代码检出阶段stage(Checkout) { git branch: main, url: gitgithub.com:username/cinema-system.git }后端构建阶段stage(Build Backend) { sh ./mvnw clean package -DskipTests archiveArtifacts artifacts: backend/target/*.jar, fingerprint: true }前端构建阶段stage(Build Frontend) { sh cd frontend npm install npm run build stash includes: frontend/dist/**, name: frontend-dist }部署阶段stage(Deploy) { sshPublisher( publishers: [ sshPublisherDesc( configName: production-server, transfers: [ sshTransfer( sourceFiles: backend/target/*.jar, removePrefix: backend/target, remoteDirectory: /opt/cinema, execCommand: sudo systemctl restart cinema.service ) ] ) ] ) }5. 性能优化实战记录5.1 数据库优化措施索引优化为高频查询字段添加组合索引ALTER TABLE t_order ADD INDEX idx_user_status (user_id, status), ADD INDEX idx_schedule_seat (schedule_id, seat_id);慢查询优化使用EXPLAIN分析执行计划-- 优化前全表扫描 SELECT * FROM t_seat WHERE schedule_id ? AND status 1; -- 优化后覆盖索引 SELECT seat_id, row_num, col_num FROM t_seat WHERE schedule_id ? AND status 1 ORDER BY row_num, col_num;分表策略按月份对订单表进行水平拆分TableName(t_order_#{#tableSuffix}) public class Order { // 动态表名处理器 public static String getTableSuffix(Date createTime) { return new SimpleDateFormat(yyyyMM).format(createTime); } }5.2 前端性能提升组件懒加载const SeatMap () import(./views/SeatMap.vue)接口请求合并使用GraphQL替代RESTfulquery { movie(id: 123) { title duration schedules { time hall { name } } } }Webpack分包策略configureWebpack: { optimization: { splitChunks: { chunks: all, maxSize: 244 * 1024 // 244KB } } }6. 踩坑实录与解决方案6.1 分布式环境下的定时任务问题现象集群环境下多个节点同时执行定时任务导致重复处理解决方案基于Redis实现分布式锁控制Scheduled(cron 0 0/5 * * * ?) public void checkExpiredOrders() { String lockKey job:checkExpiredOrders; try { Boolean locked redisTemplate.opsForValue() .setIfAbsent(lockKey, 1, 4, TimeUnit.MINUTES); if (Boolean.TRUE.equals(locked)) { // 执行业务逻辑 processExpiredOrders(); } } finally { redisTemplate.delete(lockKey); } }6.2 Vue响应式数据丢失问题场景编辑表单时直接赋值导致响应性丢失// 错误写法 this.form response.data // 正确写法保持响应性 this.form Object.assign({}, this.form, response.data)6.3 MyBatis批量插入优化原始方案循环单条插入性能差for (OrderItem item : items) { orderItemMapper.insert(item); }优化方案使用批量插入语法insert idbatchInsert parameterTypelist INSERT INTO t_order_item (order_id, seat_id, price) VALUES foreach collectionlist itemitem separator, (#{item.orderId}, #{item.seatId}, #{item.price}) /foreach /insert性能对比100条记录从1200ms → 80ms1000条记录从12s → 300ms7. 安全防护体系构建7.1 接口安全设计参数签名验证public boolean verifySign(HttpServletRequest request) { String requestSign request.getHeader(X-Sign); MapString, String params getSortedParams(request); String serverSign generateSign(params, SECRET_KEY); return serverSign.equals(requestSign); }SQL注入防护使用MyBatis预编译禁止拼接SQL语句定期使用SQLMap扫描7.2 敏感数据保护密码加密存储public String encryptPassword(String rawPassword) { return new BCryptPasswordEncoder().encode(rawPassword); }数据脱敏处理public static String maskMobile(String mobile) { if (StringUtils.isBlank(mobile)) return ; return mobile.replaceAll((\\d{3})\\d{4}(\\d{4}), $1****$2); }HTTPS强制启用Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { Override protected void configure(HttpSecurity http) throws Exception { http.requiresChannel() .requestMatchers(r - r.getHeader(X-Forwarded-Proto) ! null) .requiresSecure(); } }8. 监控与日志体系建设8.1 Prometheus监控配置SpringBoot集成示例Bean public MeterRegistryCustomizerPrometheusMeterRegistry metricsCommonTags() { return registry - registry.config().commonTags( application, cinema-backend, region, System.getenv(REGION) ); }关键监控指标系统指标CPU/Memory/DiskJVM指标GC次数/堆内存业务指标订单创建QPS/支付成功率8.2 ELK日志收集方案Logback配置示例appender nameLOGSTASH classnet.logstash.logback.appender.LogstashTcpSocketAppender destinationlogstash:5044/destination encoder classnet.logstash.logback.encoder.LogstashEncoder customFields{app:cinema-backend,env:${spring.profiles.active}}/customFields /encoder /appender日志查询技巧# 查找错误日志 level:ERROR AND app:cinema-backend # 追踪特定订单 message:orderId20230815123456 # 统计接口耗时 metrics.http.server.requests.duration:10009. 扩展功能开发思路9.1 小程序端适配方案技术选型原生小程序 Taro跨端框架接口复用现有后端APIWebSocket实现实时通知样式适配技巧/* 通用样式 */ .seat { width: 20px; height: 20px; /* 小程序需替换单位 */ width: 20rpx; height: 20rpx; }9.2 大数据分析扩展数据流向设计MySQL - Canal - Kafka - Flink - HBase - ElasticsearchFlink实时计算示例DataStreamOrderEvent orders env .addSource(new KafkaSource()) .keyBy(OrderEvent::getMovieId) .window(TumblingProcessingTimeWindows.of(Time.hours(1))) .aggregate(new OrderAggregator()); public static class OrderAggregator implements AggregateFunctionOrderEvent, MovieStats, MovieStats { Override public MovieStats add(OrderEvent value, MovieStats accumulator) { accumulator.setTotalSales(accumulator.getTotalSales() value.getAmount()); accumulator.setTicketCount(accumulator.getTicketCount() 1); return accumulator; } }10. 项目演进路线图10.1 技术债清理计划代码重构提取重复工具类到common模块统一异常处理规范接口文档自动化Swagger → OpenAPI 3.0架构升级服务拆分用户服务/订单服务/排片服务引入Spring Cloud Alibaba配置中心迁移到Nacos10.2 智能化改造方向推荐系统基于用户历史的协同过滤实时热度排行榜# 简易推荐算法示例 def recommend(user_id): viewed_movies get_view_history(user_id) similar_users find_similar_users(user_id) return [ movie for movie in get_top_movies(similar_users) if movie not in viewed_movies ]动态定价基于上座率的弹性定价节假日溢价算法public BigDecimal calculateDynamicPrice(Schedule schedule) { double basePrice schedule.getBasePrice(); double occupancyRate getOccupancyRate(schedule.getId()); // 基础算法上座率每增加10%价格上浮5% double rateFactor 1 (Math.floor(occupancyRate * 10) * 0.05); return BigDecimal.valueOf(basePrice * rateFactor) .setScale(2, RoundingMode.HALF_UP); }11. 项目源码解析要点11.1 核心类说明SeatService处理座位状态同步public void lockSeats(Long scheduleId, ListLong seatIds) { // 1. 数据库状态更新 seatMapper.updateStatus(scheduleId, seatIds, SeatStatus.LOCKED); // 2. Redis缓存更新 String key schedule:seats: scheduleId; seatIds.forEach(seatId - redisTemplate.opsForHash().put(key, seatId.toString(), SeatStatus.LOCKED) ); // 3. 发布领域事件 eventPublisher.publishEvent(new SeatLockedEvent(scheduleId, seatIds)); }PaymentCallbackHandler支付结果处理策略public interface PaymentCallbackHandler { boolean support(String paymentType); PaymentResult handle(MapString, String params); } Service public class AlipayCallbackHandler implements PaymentCallbackHandler { Override public boolean support(String paymentType) { return alipay.equals(paymentType); } Override public PaymentResult handle(MapString, String params) { // 验签逻辑 if (!AlipaySignature.rsaCheckV1(params, ALIPAY_PUBLIC_KEY)) { throw new BizException(支付宝验签失败); } // 业务处理 String orderNo params.get(out_trade_no); return orderService.handlePaySuccess(orderNo); } }11.2 设计模式应用策略模式在支付渠道选择中的应用startuml interface PaymentStrategy { pay(amount): PaymentResult refund(orderNo): RefundResult } class AlipayStrategy implements PaymentStrategy class WechatPayStrategy implements PaymentStrategy class UnionPayStrategy implements PaymentStrategy class PaymentContext { -strategy: PaymentStrategy executePay(amount) executeRefund(orderNo) } PaymentContext o-- PaymentStrategy enduml观察者模式在订单状态变更中的应用// 定义事件 public class OrderPaidEvent extends ApplicationEvent { private Long orderId; public OrderPaidEvent(Object source, Long orderId) { super(source); this.orderId orderId; } } // 事件监听器 Component public class OrderEventListener { EventListener public void handleOrderPaid(OrderPaidEvent event) { // 发送短信通知 smsService.sendPaymentSuccess(event.getOrderId()); // 更新会员积分 memberService.addPoints(event.getOrderId()); } }12. 项目部署常见问题排查12.1 前端部署问题静态资源404错误检查Nginx配置中的root路径确认Vue打包时的publicPath配置清理浏览器缓存测试接口跨域问题location /api { add_header Access-Control-Allow-Origin $http_origin; add_header Access-Control-Allow-Methods GET,POST,OPTIONS; add_header Access-Control-Allow-Headers Content-Type,Authorization; if ($request_method OPTIONS) { return 204; } proxy_pass http://backend:8080; }12.2 后端启动故障数据库连接失败检查application-prod.yml中的JDBC URL确认数据库白名单设置测试网络连通性telnet mysql_host 3306Redis连接超时spring: redis: timeout: 3000 # 适当增大超时时间 lettuce: pool: max-active: 50 max-wait: 100012.3 性能问题排查接口响应慢使用Arthas trace命令追踪方法耗时trace com.cinema.service.OrderService createOrder内存泄漏检测生成堆转储文件jmap -dump:live,formatb,fileheap.hprof pid使用MAT工具分析13. 项目二次开发建议13.1 功能扩展方向会员等级体系基于消费金额的自动升级差异化折扣策略CREATE TABLE t_member_level ( id BIGINT PRIMARY KEY, level_name VARCHAR(50), min_points INT, discount DECIMAL(3,2) );退票功能实现public Result refundOrder(Long orderId) { // 1. 校验订单状态 Order order checkRefundable(orderId); // 2. 执行退款 PaymentResult result paymentService.refund( order.getPaymentChannel(), order.getOrderNo(), order.getActualAmount() ); // 3. 更新订单状态 order.setStatus(OrderStatus.REFUNDED); orderMapper.updateById(order); // 4. 释放座位 seatService.unlockSeats(order.getScheduleId(), order.getSeatIds()); return Result.success(); }13.2 技术升级路径SpringBoot 3.0迁移JDK基线升级到17Jakarta EE 9命名空间变更废弃API替换清单Vue 3组合式API重构script setup import { ref, computed } from vue const seats ref([]) const selectedSeats computed(() seats.value.filter(s s.selected) ) async function loadSeats() { seats.value await api.getSeats() } /scriptMyBatis-Plus新特性动态表名处理器多租户SQL解析器自动填充优化14. 商业价值与运营思路14.1 盈利模式设计基础票务收入票价分成通常10-15%服务费固定金额/票增值服务优选座位加价退票保险零食套餐预售广告变现影片预告片贴片影院首页轮播图购票完成页推广14.2 数据驱动运营用户行为分析热力图分析选座偏好购票路径转化漏斗SELECT COUNT(DISTINCT session_id) AS view_count, COUNT(DISTINCT CASE WHEN page_type payment THEN session_id END) AS payment_count, COUNT(DISTINCT CASE WHEN status paid THEN order_id END) AS success_count FROM user_behavior WHERE create_time BETWEEN ? AND ?排片优化建议def optimize_schedule(movie_popularity, historical_data): # 基于机器学习的排片模型 model load_model(schedule_model.h5) return model.predict({ popularity: movie_popularity, history: historical_data })15. 项目完整部署checklist15.1 前置依赖检查服务器环境[ ] JDK 11 安装[ ] Node.js 16 安装[ ] MySQL 8.0 配置[ ] Redis 6 运行端口开放确认[ ] 80/443 (Nginx)[ ] 8080 (SpringBoot)[ ] 3306 (MySQL)[ ] 6379 (Redis)15.2 分步部署流程数据库初始化mysql -u root -p database/cinema_schema.sql mysql -u root -p database/initial_data.sql后端服务部署# 打包 mvn clean package -Pprod -DskipTests # 启动 nohup java -jar cinema-backend.jar backend.log 21 前端资源部署# 安装依赖 npm install --registryhttps://registry.npmmirror.com # 构建生产包 npm run build # 同步到Nginx目录 rsync -avz dist/* /usr/share/nginx/cinema/15.3 部署后验证基础功能测试[ ] 用户注册登录[ ] 影片查询筛选[ ] 选座购票流程[ ] 订单支付回调压力测试建议# 使用wrk模拟并发 wrk -t4 -c100 -d60s --latency http://localhost/api/movies监控指标确认[ ] Prometheus指标采集正常[ ] 应用日志输出完整[ ] 数据库慢查询监控16. 项目演进与团队协作建议16.1 代码质量管理Git分支策略main - 生产环境对应分支 release/* - 预发布分支 feature/* - 功能开发分支 hotfix/* - 紧急修复分支代码审查要点接口参数校验完整性事务边界合理性异常处理完备性SQL注入风险排查静态代码分析plugin groupIdorg.sonarsource.scanner.maven/groupId artifactIdsonar-maven-plugin/artifactId version3.9.1/version /plugin16.2 团队协作规范接口文档管理使用Swagger UI实时查看变更时同步更新文档添加必要的示例值前端协作要点定义统一的API响应格式{ code: 200, message: success, data: {} }每日构建制度定时触发Jenkins构建关键指标单元测试通过率 ≥95%代码覆盖率 ≥60%构建耗时 5分钟17. 项目后续优化方向17.1 架构演进路线服务化拆分原单体架构 → 用户服务 影片服务 订单服务 排期服务 支付服务容器化部署FROM openjdk:17-jdk COPY target/cinema-backend.jar /app.jar EXPOSE 8080 ENTRYPOINT [java,-jar,/app.jar]服务网格化引入Istio实现金丝雀发布流量镜像熔断控制17.2 技术深度优化JVM调优方向G1垃圾回收器参数优化堆外内存泄漏检测JIT编译阈值调整数据库进阶读写分离配置分库分表策略SQL审计系统集成前端性能极致优化WebAssembly应用Service Worker缓存关键渲染路径优化18. 项目完整源码解析18.1 工程结构说明完整项目目录树cinema-system/ ├── backend/ # SpringBoot工程 │ ├── src/ │ │ ├── main/ │ │ │ ├── java/com/cinema/ │ │ │ │ ├── config # 配置类 │ │ │ │ ├── controller # 接口层 │ │ │ │ ├── service # 业务逻辑 │ │ │ │ ├── dao # 数据访问 │ │ │ │ ├── entity # 数据实体 │ │ │ │ └── util # 工具类 │ │ │ └── resources/ │ │ │ ├── mapper/ # MyBatis映射文件 │ │ │ ├── static/ # 静态资源 │ │ │ ├── templates/ # 模板文件 │ │ │ ├── application.yml # 主配置 │ │ │ └── application-dev.yml # 开发配置 │ │ └── test/ # 测试代码 │ └── pom.xml # Maven配置 │ ├── frontend/ # Vue工程 │ ├── public/ # 静态资源 │ ├── src/ # 源码目录 │ │ ├── api/ # 接口定义 │ │ ├── assets/ # 资源文件 │ │ ├── components/ # 公共组件 │ │ ├── router/ # 路由配置 │ │ ├── store/ # 状态管理 │ │ ├── utils/ # 工具函数 │ │ └── views/ # 页面组件 │ ├── package.json # 依赖配置 │ └── vue.config.js # 构建配置 │ ├── database/ # 数据库脚本 │ ├── cinema_schema.sql # 表结构 │ └── initial_data.sql # 初始数据 │ ├── deployment/ # 部署配置 │ ├── nginx/ # Nginx配置 │ ├── docker/ # Dockerfile │ └── jenkins/ # Jenkinsfile │ └── docs/ # 项目文档 ├── api/ # 接口文档 ├── dev-guide/ # 开发指南 └── operations/ # 运维手册18.2 关键源码解析座位状态同步逻辑Transactional public void syncSeatStatus(Long scheduleId) { // 从数据库读取最新状态 ListSeat dbSeats seatMapper.selectBySchedule(scheduleId); // 更新Redis缓存 String redisKey schedule:seats: scheduleId; redisTemplate.delete(redisKey); MapString, String seatStatusMap dbSeats.stream() .collect(Collectors.toMap( s - s.getId().toString(), s - s.getStatus().name() )); if (!seatStatusMap.isEmpty()) { redisTemplate.opsForHash().putAll(redisKey, seatStatusMap); } // 设置过期时间24小时 redisTemplate.expire(redisKey, 1, TimeUnit.DAYS); }Vuex状态管理设计const store new Vuex.Store({ modules: { user: { state: () ({ token: localStorage.getItem(token) || , info: null }), mutations: { SET_TOKEN(state, token) { state.token token localStorage.setItem(token, token) }, SET_USER_INFO(state, info) { state.info info } }, actions: { async login({ commit }, { username, password }) { const { token } await api.login(username, password) commit(SET_TOKEN, token) const info await api.getUserInfo() commit(SET_USER_INFO, info) } } } } })19. 项目商业应用案例19.1 实际运营数据某连锁影院上线后关键指标线上售票占比从18%提升至65%平均出票时间从3分钟缩短至45秒退票纠纷率下降72%会员复购率提升至38%19.2 定制开发案例特殊场次需求包场业务模块团体票优惠配置学生认证折扣营销活动集成public OrderPrice calculateFinalPrice(Order order, ListPromotion promotions) { OrderPrice price new OrderPrice(); price.setOriginalAmount(order.getTotalAmount()); // 应用优惠券 promotions.stream() .filter(p - p.getType() PromotionType.COUPON) .findFirst() .ifPresent(coupon - { price.setDiscountAmount(coupon.getValue()); price.setCouponId(coupon.getId()); }); // 应用会员折扣 if (order.getMemberLevel() 0) { double discount memberService.getLevelDiscount(order.getMemberLevel()); price.setMemberDiscount(discount); price.setDiscountAmount( price.getDiscountAmount() price.getOriginalAmount() * (1 - discount) ); } price.setActualAmount( price.getOriginalAmount() - price.getDiscountAmount() ); return