如果你正在找 Java 课程设计、毕业设计选题,或者想做一个“带真实业务逻辑、不只有基础 CRUD”的 SpringBoot3 + Vue3 全栈项目,那这个“足球观赛售卖系统”可以直接拿来做参考。
这次我们来看一个典型的 Java 全栈管理系统:后端用 SpringBoot3,前端用 Vue.js3 + Element Plus,数据库用 MySQL。业务范围覆盖用户登录、赛事场次管理、看台座位选择、球票/纪念品售卖、订单支付流程、优惠券、数据统计看板等功能模块。它比单表的增删改查更有价值,能覆盖权限、事务、定时任务、图表报表、微信支付回调(或测试沙箱支付)等真实业务场景。
本文会以“从零搭建 + 跑通流程 + 验证效果”的顺序展开:先是核心能力速览和环境准备,再给项目目录结构与数据库表设计,接着是后端接口编写、前端页面联调,最后是启动测试、接口验证、常见问题排查和部署上线注意点。
如果你现在做毕业设计、实训项目,或者想提高 SpringBoot3 + Vue3 的工程化能力,这篇文章可以直接收藏。
1. 核心能力速览
| 能力项 | 说明 |
|---|---|
| 项目类型 | 前后端分离的 Java Web 管理系统 |
| 后端技术栈 | Java + SpringBoot3 + Spring MVC + MyBatis-Plus + MySQL |
| 前端技术栈 | Vue.js3 + Vite + Element Plus + Pinia + Vue Router + Axios |
| 数据库 | MySQL 8.x |
| 用户角色 | 普通用户 / 系统管理员(建议用 Spring Security + JWT 做权限控制) |
| 核心业务 | 赛事场次、看台与座位、球票售卖、纪念品售卖、购物车、订单、支付回调、优惠券、数据统计 |
| 启动方式 | 后端 Maven 启动 + 前端 npm 启动 |
| 是否支持接口 API | 支持,RESTful 风格,可提供给小程序或 APP 复用 |
| 是否支持批量任务 | 支持,可封装批量导入赛事场次、批量释放过期订单座位 |
| 适合场景 | Java 课程设计、毕业设计、小型体育赛事售货商城系统原型 |
从技术角度看,这个项目的核心不是复杂算法,而是把“商品(球票和纪念品)→ 购物车 → 订单 → 支付 → 库存扣减 → 订单统计”这条链路完整打通。前端能用 Vue3 组件化方式展示看台、赛事列表和订单中心,后端能用 SpringBoot3 统一处理业务异常、事务回滚和权限校验。
2. 适用场景与业务边界
2.1 适合什么场景
- Java 课程设计、毕业设计:功能复杂度足够,前后端技术栈新,演示效果好。
- 小型体育赛事线上售卖原型:可以快速扩展成真实赛事售票系统。
- SpringBoot3 + Vue3 全栈学习项目:能覆盖主流开发流程、接口联调、权限管理。
- 前后端分离架构实践:后端只提供 API,前端独立构建。
2.2 能解决什么问题
系统要解决的核心问题包括:赛事和场次信息杂乱、球票售卖状态不透明、座位选择困难、订单与库存不同步、销售数据统计费时。
最终交付的成果应该是:管理员可以维护赛事、场次、看台、票价档位、纪念品库存;用户在页面上能看赛事列表、选场次、选看台座位、加购物车、下单支付;支付成功回写订单状态并锁定座位,未支付订单超时自动释放;后台用图表展示销售额、出票量、商品销量。
2.3 不适合什么场景
- 高并发抢票:该系统适合中小规模售票。真正的大规模抢票需要消息队列、分布式锁、限流、分库分表,已经超出课程设计边界。
- 真实在线支付:如果接入真实微信支付/支付宝,需要企业资质和正式商户号。个人学习建议使用支付沙箱或模拟支付。
- 超大型前端后台:如果要做多级组织架构、复杂的供应链管理,选型上应该考虑更完整的企业级脚手架。
2.4 版权、隐私与安全边界
无论做什么售卖系统,都要注意几个点:赛事信息、球队 Logo、球星照片涉及版权,演示环境不能直接使用未经授权的商业素材;用户手机号、收货地址属于个人敏感信息,数据库不能明文存储,前端不能过度采集;涉及线上支付时,必须走官方支付渠道或测试沙箱,不能私自伪造支付回调;系统上线前要做基础安全加固,比如密码加密、SQL 注入防护、接口鉴权。
3. 环境准备与前置条件
3.1 版本清单
建议用以下版本组合,兼容性较好:
| 组件 | 版本建议 | 说明 |
|---|---|---|
| JDK | JDK 17 或 JDK 21 | SpringBoot3 要求 JDK 17 起步 |
| Maven | 3.8+ | 项目构建和依赖管理 |
| Node.js | 18.x 或 20.x LTS | Vue3 + Vite 开发环境 |
| npm/pnpm | npm 9+ / pnpm 8+ | 前端依赖安装 |
| MySQL | MySQL 8.x | 建议用 8.0 以上版本 |
| IDE | IntelliJ IDEA / VS Code | 后端推荐 IDEA,前端用 VS Code 也可以 |
3.2 JDK17 与 SpringBoot3 的选择原因
SpringBoot3 默认基于 Spring Framework 6,最低要求 JDK17。相比旧版,SpringBoot3 在依赖管理、GraalVM 原生镜像、Jakarta EE 命名空间等方面有明显变化。如果网上看到javax.servlet等旧包名,注意 SpringBoot3 中已经改成jakarta.servlet。
3.3 MySQL 准备
需要提前创建好数据库:
CREATE DATABASE IF NOT EXISTS football_match_sale DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;MySQL 8.x 默认字符集更好,支持中文和 emoji。在application.yml中连接数据库时,建议加上serverTimezone=Asia/Shanghai和useUnicode=true&characterEncoding=utf8。
3.4 端口规划
需要保证以下端口不被占用:
| 服务 | 默认端口 | 说明 |
|---|---|---|
| 后端 SpringBoot | 8080 | 可通过server.port修改 |
| 前端 Vite 开发服务器 | 5173 | 可通过vite.config.js修改 |
| MySQL | 3306 | 本地数据库端口 |
4. 项目目录结构与数据库设计
4.1 后端项目结构
football-sale-backend ├── pom.xml ├── src/main/java/com/example/footballsale │ ├── FootballSaleApplication.java │ ├── common │ │ ├── Result.java │ │ ├── ResultCode.java │ │ └── GlobalExceptionHandler.java │ ├── config │ │ ├── MybatisPlusConfig.java │ │ ├── SecurityConfig.java │ │ └── WebMvcConfig.java │ ├── controller │ │ ├── AuthController.java │ │ ├── MatchController.java │ │ ├── SessionController.java │ │ ├── SeatController.java │ │ ├── TicketController.java │ │ ├── GoodsController.java │ │ ├── CartController.java │ │ ├── OrderController.java │ │ ├── PaymentController.java │ │ ├── CouponController.java │ │ └── DashboardController.java │ ├── entity │ │ ├── User.java │ │ ├── Match.java │ │ ├── MatchSession.java │ │ ├── Stand.java │ │ ├── Seat.java │ │ ├── TicketStock.java │ │ ├── Goods.java │ │ ├── CartItem.java │ │ ├── Order.java │ │ ├── OrderItem.java │ │ ├── Coupon.java │ │ └── UserCoupon.java │ ├── mapper │ ├── service │ └── dto │ ├── LoginDTO.java │ ├── OrderCreateDTO.java │ ├── SeatLockDTO.java │ └── PaymentNotifyDTO.java └── src/main/resources ├── application.yml ├── mapper/*.xml └── sql/init.sql4.2 前端项目结构
football-sale-frontend ├── index.html ├── package.json ├── vite.config.js ├── src │ ├── main.js │ ├── App.vue │ ├── api │ │ ├── request.js │ │ ├── auth.js │ │ ├── match.js │ │ ├── order.js │ │ └── dashboard.js │ ├── router │ │ └── index.js │ ├── stores │ │ ├── user.js │ │ └── cart.js │ ├── views │ │ ├── Home.vue │ │ ├── MatchList.vue │ │ ├── MatchDetail.vue │ │ ├── SeatSelect.vue │ │ ├── GoodsList.vue │ │ ├── Cart.vue │ │ ├── Checkout.vue │ │ ├── OrderList.vue │ │ ├── Login.vue │ │ └── admin │ │ ├── AdminLayout.vue │ │ ├── Dashboard.vue │ │ ├── MatchManage.vue │ │ ├── SessionManage.vue │ │ ├── SeatManage.vue │ │ ├── GoodsManage.vue │ │ ├── OrderManage.vue │ │ └── CouponManage.vue │ └── components │ ├── SeatMap.vue │ ├── MatchCard.vue │ ├── OrderStatusTag.vue │ └── ChartCard.vue4.3 数据库核心表设计
核心表建议分成三类:基础数据表、交易相关表、营销相关表。
| 表名 | 说明 | 关键字段 |
|---|---|---|
user | 用户表 | id, username, password, phone, real_name, role, status |
match | 赛事表 | id, match_name, home_team, away_team, match_time, stadium, cover_image |
match_session | 场次表 | id, match_id, session_time, sale_start_time, sale_end_time, status |
stand | 看台表 | id, session_id, stand_name, stand_price, total_seats |
seat | 座位表 | id, stand_id, seat_row, seat_col, status(0空闲/1锁定/2已售) |
ticket_stock | 球票库存表 | id, session_id, stand_id, total_count, sold_count, locked_count |
goods | 纪念品表 | id, goods_name, goods_desc, price, stock, image |
cart_item | 购物车表 | id, user_id, goods_id, ticket_stand_id, quantity, checked |
orders | 订单表 | id, order_no, user_id, total_amount, pay_amount, discount_amount, status, create_time, pay_time |
order_item | 订单明细表 | id, order_id, item_type(0球票/1商品), item_id, item_name, price, quantity |
coupon | 优惠券表 | id, coupon_name, coupon_type, discount_amount, threshold_amount, stock, start_time, end_time |
user_coupon | 用户优惠券表 | id, user_id, coupon_id, status(0未使用/1已使用), get_time, use_time |
4.4 表关系说明
一个赛事match下有多个场次match_session,一个场次下有多个看台stand,一个看台下有多个座位seat。用户购买球票时,选择的实际是“场次 + 看台 + 座位”。订单主表orders保存一次订单的总金额和状态,订单明细表order_item保存本次订单包含的球票或商品。这样设计的好处是:一个订单可以同时包含球票和纪念品,计价逻辑和库存扣减逻辑都放在同一事务里处理。
5. 后端核心接口设计与功能实现
5.1 统一返回结构
所有接口统一返回Result<T>,避免前端每次解析多种数据结构。
@Data public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("success"); result.setData(data); return result; } public static <T> Result<T> error(Integer code, String message) { Result<T> result = new Result<>(); result.setCode(code); result.setMessage(message); return result; } }5.2 登录鉴权接口
建议使用 Spring Security + JWT 实现登录和角色鉴权。用户登录后,后端返回 token,前端把 token 存到 Pinia 和 localStorage,每次请求由 Axios 拦截器自动携带Authorization: Bearer <token>。
@RestController @RequestMapping("/api/auth") public class AuthController { @PostMapping("/login") public Result<LoginResponse> login(@RequestBody LoginDTO dto) { // 1. 验证用户名密码 // 2. 生成 JWT token // 3. 返回用户基本信息和 token return Result.success(response); } @PostMapping("/register") public Result<Void> register(@RequestBody RegisterDTO dto) { // 校验用户名唯一性 // 密码加密存储(BCrypt) return Result.success(null); } }密码一定不能明文存储。推荐使用BCryptPasswordEncoder:
PasswordEncoder encoder = new BCryptPasswordEncoder(); String encodedPassword = encoder.encode("123456"); // 校验时 boolean matches = encoder.matches(rawPassword, encodedPassword);5.3 场次与座位接口
选座流程建议按照“场次 → 看台 → 座位 → 锁定”的顺序实现:
@GetMapping("/api/session/{sessionId}/seats") public Result<List<SeatVO>> getSeatList(@PathVariable Long sessionId) { // 查询当前场次下所有看台的座位状态 // 返回 seatId、standId、seatRow、seatCol、status }前端拿到座位数据后,用类似影院选座的交互方式渲染SeatMap.vue。空闲座位可点击选择,锁定座位置灰,已售座位显示为不同颜色。
5.4 下单与库存事务
下单接口是整个系统最核心的部分,必须加事务。业务逻辑如下:
- 校验用户是否登录。
- 校验订单中的球票和商品库存。
- 校验优惠券是否可用。
- 计算订单总金额、优惠金额、实付金额。
- 生成订单号和订单明细。
- 扣减商品库存,更新座位状态和球票库存。
- 清空购物车对应条目。
@Transactional(rollbackFor = Exception.class) @Override public Order createOrder(OrderCreateDTO dto) { // 1. 校验 // 2. 计算价格 // 3. 保存订单主表 // 4. 保存订单明细 // 5. 扣库存 // 6. 更新座位状态 // 7. 返回订单 }注意:商品库存扣减必须使用乐观锁或悲观锁,避免超卖。MyBatis-Plus 中可以用版本号实现乐观锁:
@Version private Integer version;更新库存 SQL 建议写成:
UPDATE goods SET stock = stock - #{quantity}, version = version + 1 WHERE id = #{goodsId} AND stock >= #{quantity} AND version = #{version}5.5 支付接口
如果只是演示,建议实现“模拟支付”:订单创建后,用户点击“模拟支付”,后端直接修改订单状态为已支付,并触发支付成功后的后续逻辑。
@PostMapping("/api/payment/mockPay") public Result<Void> mockPay(@RequestBody MockPayDTO dto) { // 1. 校验订单归属 // 2. 判断订单状态是否为待支付 // 3. 订单状态改为已支付 // 4. 扣减商品库存(如果在取票/发货时扣减则不需要) // 5. 锁定座位改为已售 // 6. 发送站内通知 }如果接入真实支付,回调地址必须是外网可访问地址,并且需要验签。个人学习阶段不建议直接搞真实支付。
5.6 批量任务实现
系统里可以加一个定时任务,处理“超时未支付订单自动取消”和“释放锁定座位”。用 Spring 自带的@Scheduled即可:
@Component public class OrderTimeoutTask { @Scheduled(fixedDelay = 60000) public void cancelTimeoutOrders() { // 查询超过15分钟未支付的订单 // 取消订单 // 释放座位状态 // 恢复商品库存 } }在启动类上添加@EnableScheduling:
@SpringBootApplication @EnableScheduling public class FootballSaleApplication { public static void main(String[] args) { SpringApplication.run(FootballSaleApplication.class, args); } }6. 前端页面与接口联调
6.1 前端开发服务器代理配置
前后端分离开发时,前端请求会跨域。最方便的方式是在 Vite 中配置代理,而不是在后端开启全局跨域。
在项目根目录vite.config.js中配置:
import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], server: { host: '0.0.0.0', port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })这样设置后,前端请求/api/match/list时,Vite 开发服务器会自动转发到http://localhost:8080/api/match/list,避免开发环境的跨域问题。
6.2 Axios 请求封装
在src/api/request.js中统一封装请求:
import axios from 'axios' import { ElMessage } from 'element-plus' import router from '../router' import { useUserStore } from '../stores/user' const request = axios.create({ baseURL: '/api', timeout: 15000 }) // 请求拦截器:自动携带 token request.interceptors.request.use(config => { const userStore = useUserStore() if (userStore.token) { config.headers.Authorization = `Bearer ${userStore.token}` } return config }) // 响应拦截器:统一处理错误 request.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { ElMessage.error(res.message || '请求失败') return Promise.reject(new Error(res.message)) } return res }, error => { if (error.response && error.response.status === 401) { ElMessage.error('登录状态已过期,请重新登录') router.push('/login') } else { ElMessage.error(error.message || '网络错误') } return Promise.reject(error) } ) export default request6.3 赛事列表页面
赛事列表页是用户进入系统后的第一个核心页面。建议用卡片式布局展示赛事,包含主队、客队、比赛时间、场馆、封面图等信息。
// src/api/match.js import request from './request' export function getMatchList(params) { return request.get('/match/list', { params }) } export function getMatchDetail(id) { return request.get(`/match/${id}`) }页面核心逻辑:
- 页面加载时调用
getMatchList获取赛事列表。 - 用户点击“查看场次”后,跳转到场次选择页。
- 场次选择页展示某个赛事下的所有场次,场次状态区分未开始售卖、售卖中、已结束。
6.4 选座页面
选座页是整个前端交互最复杂的部分。要把后端返回的座位数据渲染成看台矩阵。建议用 CSS Grid 布局,行和列对应seat_row和seat_col。
<template> <div class="seat-map"> <div v-for="seat in seatList" :key="seat.id" class="seat" :class="seatClass(seat)" @click="selectSeat(seat)" > {{ seat.row }}排{{ seat.col }}座 </div> </div> </template> <script setup> import { ref } from 'vue' import { getSeatList } from '../api/session' const props = defineProps({ sessionId: { type: Number, required: true } }) const seatList = ref([]) const selectedSeats = ref([]) function loadSeats() { getSeatList(props.sessionId).then(res => { seatList.value = res.data }) } function seatClass(seat) { if (seat.status === 2) return 'seat-sold' if (seat.status === 1) return 'seat-locked' if (selectedSeats.value.includes(seat.id)) return 'seat-selected' return 'seat-available' } function selectSeat(seat) { if (seat.status !== 0) return // 已选中的取消选中,未选中的加入 const index = selectedSeats.value.indexOf(seat.id) if (index > -1) { selectedSeats.value.splice(index, 1) } else { selectedSeats.value.push(seat.id) } } </script>6.5 购票与下单流程
用户选完座位后,进入确认订单页。页面需要展示:
- 场次信息。
- 座位列表。
- 球票单价。
- 优惠券选择。
- 是否加购纪念品。
- 实付金额。
提交订单前,前端需要先检查用户是否登录:
import { useUserStore } from '../stores/user' import { createOrder } from '../api/order' const userStore = useUserStore() function submitOrder() { if (!userStore.token) { ElMessage.warning('请先登录') router.push('/login') return } createOrder(orderData).then(res => { ElMessage.success('订单创建成功') router.push('/checkout/' + res.data.orderNo) }) }7. 接口 API 调用与批量任务验证
7.1 后端接口风格
所有接口统一使用/api前缀:
| 模块 | 方法 | 路径 | 说明 |
|---|---|---|---|
| 认证 | POST | /api/auth/login | 用户登录 |
| 赛事 | GET | /api/match/list | 赛事列表 |
| 场次 | GET | /api/match/{matchId}/sessions | 某赛事下的场次 |
| 座位 | GET | /api/session/{sessionId}/seats | 某场次下的所有座位 |
| 购物车 | GET | /api/cart/list | 购物车列表 |
| 订单 | POST | /api/order/create | 创建订单 |
| 支付 | POST | /api/payment/mockPay | 模拟支付 |
| 管理端 | GET | /api/admin/dashboard | 数据统计看板 |
7.2 后端接口验证示例
以“创建订单”为例,先登录获取 token,再调用订单接口:
# 1. 登录 curl -X POST http://localhost:8080/api/auth/login \ -H "Content-Type: application/json" \ -d '{ "username": "testuser", "password": "123456" }'返回结果中包含 token。携带 token 创建订单:
# 2. 创建订单 curl -X POST http://localhost:8080/api/order/create \ -H "Content-Type: application/json" \ -H "Authorization: Bearer <token>" \ -d '{ "userId": 1, "ticketItems": [ { "sessionId": 1, "standId": 2, "seatIds": [10, 11, 12] } ], "goodsItems": [ { "goodsId": 3, "quantity": 2 } ], "userCouponId": 5 }'7.3 批量导入赛事场次
管理员端可以做一个“批量导入场次”的功能,前端上传 CSV 或 Excel,后端解析后批量写入数据库。
@PostMapping("/api/admin/session/batchImport") public Result<Void> batchImport(@RequestBody List<SessionImportDTO> list) { // 逐条校验数据 // 批量插入场次和看台 // 生成默认座位 }批量导入的业务意义在于:真实业务中一个赛季可能有几十场比赛、上百个场次,手工录入效率太低。批量导入功能可以作为系统亮点写入课程设计报告。
7.4 定时任务验证
配合定时任务,可以在数据库中手动造一张“待支付订单”,把create_time改成 20 分钟前,观察任务是否自动取消订单并释放座位。验证步骤如下:
- 在 MySQL 中手动修改一条待支付订单的
create_time。 - 等待定时任务执行(如 1 分钟一次)。
- 检查订单状态是否变为“已取消”。
- 检查对应座位状态是否从“锁定”变为“空闲”。
- 检查商品库存是否恢复。
UPDATE orders SET status = 0, create_time = DATE_SUB(NOW(), INTERVAL 20 MINUTE) WHERE order_no = 'TEST202501010001';8. 资源占用与性能观察
由于这是 SpringBoot3 + Vue3 + MySQL 的管理系统,不需要 GPU 推理,性能观察重点在 CPU、内存、数据库连接池和接口响应时间。
8.1 本地运行资源占用
本地开发环境运行后,可以观察到:
| 项目 | 参考占用 | 说明 |
|---|---|---|
| 后端 Java 进程 | 200MB ~ 500MB 左右 | 受 Maven 依赖和实际代码量影响 |
| 前端 Node 进程 | 300MB ~ 600MB 左右 | Vite 开发服务器和依赖预构建 |
| MySQL | 200MB ~ 500MB | 看配置和连接量 |
| IDEA/VS Code | 1GB+ | IDE 自身的消耗不在业务范围内 |
实际占用以本机环境为准。如果用 Maven 打包后运行java -jar,后端内存复用会比开发模式更稳定。
8.2 接口性能观察方式
建议给项目加上 Spring Boot Actuator,用来观察应用健康状态:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>访问http://localhost:8080/actuator/health可以看到服务是否健康。生产环境还要加入 Micrometer 监控指标。
数据库慢查询日志也要打开:
SET GLOBAL slow_query_log = 'ON'; SET GLOBAL long_query_time = 1;当某个接口响应变慢时,优先看是不是 SQL 没有走索引。比如按order_no查询订单,就要给order_no建唯一索引:
ALTER TABLE orders ADD UNIQUE INDEX uk_order_no (order_no);8.3 常见性能优化点
- MySQL 连接池默认配置不一定适合高并发,可以用 HikariCP 调整
maximum-pool-size。 - 列表页查询不要用
SELECT *,只查需要的字段。 - 分页查询必须用
LIMIT,推荐 MyBatis-Plus 的分页插件。 - 订单表数据量变大后,按时间做冷热数据分离。
- 座位状态更新要及时,不能让多个请求同时操作同一个座位。
9. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| SpringBoot3 项目启动即失败 | JDK 版本过低 | 查看启动日志中的 JDK 报错 | 安装 JDK 17+,IDEA 中切换 Project SDK |
| 接口返回 401 | token 缺失或过期 | 打开浏览器 Network 面板,看请求头 | 重新登录获取 token,检查 Axios 拦截器 |
| 前端调用接口跨域 | 前后端端口不同 | 查看浏览器 Console 报错信息 | 使用 Vite proxy 或后端配置 CORS |
| 数据库连接失败 | MySQL 未启动或密码错误 | 使用 Navicat/命令行连接测试 | 确认 MySQL 服务状态和配置文件密码 |
| 商品库存出现负数 | 缺少乐观锁或库存校验不严 | 检查更新库存 SQL | 使用乐观锁 +stock >= quantity条件 |
| 座位被重复售卖 | 座位状态更新和下单不在同一事务 | 检查@Transactional是否生效 | 确保座位更新、订单创建、库存扣减在同一个方法事务内 |
| 定时任务不执行 | 没有添加@EnableScheduling | 检查启动类注解 | 在启动类添加@EnableScheduling |
| 中文乱码 | 数据库字符集不是 utf8mb4 | 查看表结构和连接参数 | 建库用 utf8mb4,URL 加 characterEncoding |
| 前端路由刷新 404 | history 模式部署未配置 | 先看本地开发环境是否正常 | 部署 Nginx 时配置 try_files 或改用 hash 模式 |
| 打包后静态资源找不到 | 前端未打包或路径配置错误 | 检查dist目录是否存在 | 执行npm run build,Nginx 指向 dist 目录 |
9.1 Maven 依赖下载慢或失败
如果 Maven 下载依赖卡住,修改settings.xml,使用国内镜像:
<mirror> <id>aliyunmaven</id> <mirrorOf>central</mirrorOf> <name>Aliyun Maven Mirror</name> <url>https://maven.aliyun.com/repository/public</url> </mirror>9.2 npm 依赖安装失败
npm 也推荐使用国内镜像:
npm config set registry https://registry.npmmirror.com安装依赖时如果报错,优先删除node_modules和package-lock.json后重试:
rm -rf node_modules package-lock.json npm install9.3 端口被占用
启动后端时如果 8080 端口被占用,可以先查占用进程:
# Windows netstat -ano | findstr 8080 # macOS / Linux lsof -i :8080临时修改后端端口:
server: port: 808110. 最佳实践与工程化建议
10.1 项目规范
- 后端 Controller 只做参数接收和结果返回,业务逻辑全部写在 Service 层。
- 数据库字段用下划线命名,Java 实体用驼峰命名,MyBatis-Plus 会自动做驼峰映射。
- 所有返回给前端的时间格式统一,建议使用
yyyy-MM-dd HH:mm:ss。 - 金额相关字段用
BigDecimal,不要使用double或float。 - 订单号不要使用数据库自增 id,建议用时间戳 + 随机数生成,例如
20250101102030加 6 位随机串。
10.2 第一次先小功能跑通
不要一上来就写完整项目。建议按这个顺序从零搭起:
- 搭建 SpringBoot3 骨架,写一个
hello接口,跑起来。 - 接入 MySQL 和 MyBatis-Plus,完成用户表查询。
- 完成登录注册和 JWT 鉴权。
- 写赛事、场次、座位三个基础模块。
- 写购物车和订单模块。
- 接入模拟支付。
- 写管理端统计数据。
- 写前端页面联调。
每一步都能看到结果,排查问题范围就会小很多。
10.3 安全与合规提醒
- 密码必须加密存储,不能明文入库。
- 身份证、手机号等敏感信息要脱敏展示。
- 接口不能裸奔,除登录和注册外,其他接口都要做登录校验。
- 管理员接口要单独校验角色,普通用户不能调用管理员接口。
- 涉及足球赛事素材、球队标志、球员照片时,演示环境使用自己制作的占位图或免费可商用素材。
- 在线支付功能必须使用官方沙箱或商户平台提供的测试环境,不能在生产环境伪造支付结果。
10.4 部署上线建议
本地跑通后,可以按以下方式部署:
后端打包:
mvn clean package -DskipTests java -jar target/football-sale-backend-0.0.1-SNAPSHOT.jar前端打包:
npm run build打包后dist目录由 Nginx 托管,API 请求反向代理到后端:
server { listen 80; server_name your-domain.com; root /usr/share/nginx/football-sale/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }11. 总结与下一步
这个足球观赛售卖系统,最值得学习的地方不是某一个页面效果,而是从赛事管理、座位选择、购物车、下单、库存扣减、模拟支付、定时取消订单到后台统计的完整业务闭环。整个链路覆盖了 SpringBoot3 事务、MyBatis-Plus 操作、JWT 鉴权、定时任务、Vue3 组件通信、Axios 请求封装、Element Plus 表格和图表等高频技术点,简历项目经验、课程设计报告和答辩演示都有内容可写。
最先跑通的功能建议选“赛事列表 + 场次详情 + 选座下单”,这三个功能关联最紧密,能最快看到前后端联调效果。最容易踩坑的地方是座位状态并发更新和商品库存校验,写代码时把@Transactional用好、把乐观锁加上,后续测试会顺畅很多。如果时间充裕,还可以继续扩展:
- 增加订单超时取消的延迟队列。
- 增加 Redis 缓存赛事热门场次数据。
- 增加后台销售统计图表。
- 增加 Excel 导出订单报表。
- 增加微信小程序端,复用现有后端 API。
把这个项目完整开发一遍,你对 Java 全栈项目从设计到上线的理解会明显提升。建议收藏备用,开发过程中遇到具体报错,可以从上面的排查清单里找思路。