简介:一套面向JavaWeb课程设计与毕业设计的浙江绍兴展示网站全栈源码包。项目采用Spring Boot 3 + Vue + Element Plus前后端分离架构,涵盖美食展示、文化展示、景点展示与用户认证模块,适合正在学习前后端分离开发、需要完整项目参考的高校学生。资源包共129个文件,包含40个Java后端类、23个Vue前端组件、11个XML配置、5个JS脚本、4个JSON及SQL、YAML等,另附项目文档与图片素材,文件组织清晰,导入后即可看到前后端完整代码结构。压缩包19.08MB,已有97人浏览学习。内容包含完整可运行源代码、数据库脚本及项目说明文档,可直接作为课程设计或毕业设计基础,也可帮助读者理解Spring Boot 3与Vue3的整合方式、用户认证流程以及模块化开发思路。
1. 这个 JavaWeb 大作业,到底在做什么
把“浙江绍兴展示网站”做成 Spring Boot 3 + Vue 3 + Element Plus 的前后端分离项目,是近几年 JavaWeb 课程设计里最常见也最稳妥的选题方向。它覆盖了一个完整 Web 项目从数据库设计、后端接口、前端页面到用户认证的全部环节,答辩时每一层都能拿出东西讲。这篇文章就是顺着这个标题,把整个项目从零搭建的路径拆开:先讲为何选这套技术栈,再给后端和前端的关键实现,最后落在部署和答辩时容易踩的细节上。适合正在做类似“基于 javaweb 的旅游管理系统”课程设计、或者想用 Spring Boot 3 + Vue 3 重写旧项目的开发者。文中所有命令和代码都按可直接复制的粒度给出,照着改就能跑。
2. Spring Boot 3 + Vue 3 + Element Plus 的选型与前后端架构拆分
2.1 为什么课程设计要选 Spring Boot 3 而不是 Spring Boot 2
Spring Boot 3 在 2022 年底正式发布,底层依赖 Spring Framework 6,强制要求 JDK 17 及以上。相比 Spring Boot 2.x,最直观的区别是javax.*命名空间整体迁移到jakarta.*,这导致很多老教程里的 import 语句直接报错。另一个关键变化是 Spring Security 6 的配置方式从 WebSecurityConfigurerAdapter 改为基于 SecurityFilterChain 的 Bean 声明,课程设计里写用户认证模块时,网上搜到的老代码基本都是废弃写法,需要格外注意。
选择 Spring Boot 3 的理由不只是“新版更时髦”。JDK 17 带来的record类型、switch表达式、增强的var支持,让实体类和配置代码明显更简洁;Spring Boot 3 的 GraalVM 原生镜像支持虽然课程设计用不上,但能体现你对新技术栈的跟进。更重要的是,新项目直接上 Spring Boot 3 可以避免“做完就过时”的问题,将来想往微服务方向扩展,Spring Cloud 2023 系列已经全面兼容 Boot 3。
2.2 前后端分离下的四层架构与目录规范
这个项目不是单体 JSP 项目,而是前端静态资源完全独立的前后端分离结构。后端只提供 RESTful API,前端通过 Axios 请求 JSON 数据渲染页面,部署时可以由 Nginx 托管 Vue 构建产物并把/api路径反向代理到后端服务。
后端目录遵循 Spring Boot 四层架构:Controller(接口层)、Service(业务层)、Mapper/Repository(数据访问层)、Entity(实体层)。课程设计里很多同学把业务逻辑全堆在 Controller 里,答辩时被问“分层的作用”就答不上来。正确做法是 Controller 只做参数接收和响应封装,业务判断放在 Service 接口的实现类中,数据访问通过 Mapper 接口完成。建议的包结构如下:
com.shaoxing.demo ├── config # 安全配置、跨域配置、WebMvc 配置 ├── controller # 前端接口入口 ├── entity # 数据库实体类 ├── mapper # MyBatis/Mapper 接口 ├── service # 业务接口 │ └── impl # 业务实现 ├── common # 统一响应对象、异常处理、工具类 └── filter # JWT 认证过滤器这种按技术角色分包的方式,对于单体应用仍然是最经典的规范。答辩时你只需要说清楚“请求先到 Controller,Controller 调 Service 接口,Service 实现里调用 Mapper,Mapper 对应 SQL 操作”,整条链路就非常清晰。
前端目录则按功能模块划分,Vue 3 项目根目录下的src/views存放页面组件,src/router管理路由,src/api封装 Axios 请求,src/store存放 Pinia 状态。和黑马 javaweb 笔记里传统的 HTML + Ajax 写法相比,Vue 的组件化让页面复用度高很多,例如美食、文化、景点三个展示页结构高度相似,抽成公共组件后只需维护一份。
2.3 数据库设计:五张表的字段、类型与关联关系
数据库是这个题目的核心之一,也是答辩老师必问的部分。很多同学一上来就建表,结果字段冗余、关联混乱。我一般先按业务模块拆:用户认证是一块,美食/文化/景点展示是一块。展示模块又需要分类信息,所以至少需要以下表。
| 表名 | 用途 | 关键字段 |
|---|---|---|
user | 登录用户 | id, username, password, email, avatar, role, create_time |
category | 内容分类(美食/文化/景点) | id, name, description, sort_order |
food | 绍兴美食条目 | id, category_id, name, image, description, address, price, recommend_count |
culture | 绍兴文化条目 | id, category_id, title, content, image, source, create_time |
scenic | 绍兴景点条目 | id, category_id, name, opening_time, ticket_price, location, cover_image, introduction |
外键关系上,food、culture、scenic都通过category_id关联category表,这种设计支持后续扩展更多展示类型而不需要改表结构。用户表不与其他表产生外键,因为浏览行为(收藏、评论)不在本版本范围内,如果后续要加“我的收藏”,再建favorites表关联user_id和target_id即可。
建表时需要注意两点:password字段长度至少 60 或 64,因为 BCrypt 加密后的字符串长度在 60 左右,varchar(20) 一定存不下;所有表都建议设置create_time和update_time的默认值,MyBatis 的自动填充功能可以在插入和更新时自动维护这两个字段。数据库课程设计里如果能画出一张正规的 ER 图写进项目文档,会是明显的加分项。
3. 后端落地:从依赖、实体到 JWT 认证的完整实现
3.1 pom.xml 关键依赖与 application.yml 参数配置
创建 Spring Boot 3 项目时,我推荐直接在 Spring Initializr 选择 Java 17 和 Spring Boot 3.x 版本生成骨架,然后手动加入需要的依赖。核心依赖包括 Web、MyBatis、MySQL 驱动、Lombok、JWT 工具库和 Spring Security(如果自己写 JWT 生成与校验,可以不引入 Spring Security,用拦截器实现,课程设计完全够用,也更好解释原理)。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>3.0.3</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.12.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.12.5</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.12.5</version> <scope>runtime</scope> </dependency>提示:Spring Boot 3 中 MyBatis 官方 Starter 的坐标是
mybatis-spring-boot-starter,不要在旧资料里找mybatis-spring-boot-starter的 2.x 版本,也不要在 Spring Boot 3 里引入spring-boot-starter-jdbc再手动配 MyBatis,那样会产生冲突。
application.yml里需要配置数据源、MyBatis 的 mapper 位置和 JWT 的密钥。JWT 密钥建议设置为至少 256 位(32 字节)的随机字符串,jjwt 0.12.x 对密钥长度有强校验,太短会直接抛异常。
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/shaoxing_web?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.shaoxing.demo.entity configuration: map-underscore-to-camel-case: true jwt: secret: "A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q7R8S9T0" expire-hours: 24map-underscore-to-camel-case非常关键,它让数据库的snake_case字段自动映射到实体的camelCase属性,create_time直接对应createTime,省去大量 ResultMap 手写映射。连接 MySQL 8 时必须在 URL 里带上serverTimezone,否则本地时间正常但部署到服务器后会出现时区偏差 8 小时的怪异问题。
3.2 实体类与 Mapper 层的接口定义
实体类使用 Lombok 的@Data注解简化 getter/setter,用@TableName(MyBatis-Plus)或 XML 里手动指定表名均可。如果不想引入 MyBatis-Plus 那套代码生成器,纯 MyBatis + XML 写法在课程设计中反而更被老师认可,因为你能讲清楚 SQL 是手写的。
@Data public class Food { private Integer id; private Integer categoryId; private String name; private String image; private String description; private String address; private BigDecimal price; private Integer recommendCount; private LocalDateTime createTime; }Mapper 接口定义与 XML 分离,接口中只写方法签名,SQL 全部集中在resources/mapper/FoodMapper.xml中。一个常见的面试问题是“Mapper 接口和 XML 如何关联”,答案就是接口全限定名与 XML 的namespace一致、方法名与 XML 中<select>的id一致。
public interface FoodMapper { List<Food> selectList(@Param("keyword") String keyword, @Param("categoryId") Integer categoryId, @Param("offset") Integer offset, @Param("limit") Integer limit); int count(@Param("keyword") String keyword, @Param("categoryId") Integer categoryId); Food selectById(@Param("id") Integer id); }与之对应的 XML 片段:
<select id="selectList" resultType="com.shaoxing.demo.entity.Food"> SELECT id, category_id, name, image, description, address, price, recommend_count, create_time FROM food <where> <if test="keyword != null and keyword != ''"> AND name LIKE CONCAT('%', #{keyword}, '%') </if> <if test="categoryId != null"> AND category_id = #{categoryId} </if> </where> ORDER BY recommend_count DESC LIMIT #{offset}, #{limit} </select><where>标签会自动去掉不必要的 AND,防止 keyword 为空时生成WHERE AND xxx的语法错误。LIMIT 分页是手写的,课程设计阶段不引入 PageHelper 完全没问题,答辩时反而能体现你对分页原理的理解。注意recommend_count这种字段用int接收时,数据库里不要让它为 NULL,否则拆箱会空指针。
3.3 Service 与 Controller 的请求链路和统一响应封装
Service 层建议先写接口再写实现,这不仅是规范,也是 Spring 依赖注入的惯用方式。Controller 调 Service 接口,Service 实现里做参数校验和业务拼接,数据访问只通过 Mapper。为了前台页面拿到统一结构的数据,定义一个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> r = new Result<>(); r.setCode(200); r.setMessage("操作成功"); r.setData(data); return r; } public static <T> Result<T> error(String message) { Result<T> r = new Result<>(); r.setCode(500); r.setMessage(message); return r; } }Controller 中按 RESTful 风格暴露接口。展示模块是只读接口,用户认证模块包含登录和注册。一个典型的列表接口写法:
@RestController @RequestMapping("/api/food") public class FoodController { @Resource private FoodService foodService; @GetMapping("/list") public Result<Map<String, Object>> list(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "9") Integer pageSize, @RequestParam(required = false) String keyword) { int offset = (page - 1) * pageSize; List<Food> records = foodService.getPage(keyword, offset, pageSize); int total = foodService.count(keyword); Map<String, Object> data = new HashMap<>(); data.put("records", records); data.put("total", total); return Result.success(data); } }@RequestParam(defaultValue = "1")把页码默认值交给后端处理,前端不用每次判断参数为空的边界。这里返回的是Map而不是自定义分页对象,原因是 Vue 前端拿到 records 和 total 后,配合 Element Plus 的el-pagination组件刚好一一对应。记住一个原则:Controller 层不要出现任何业务逻辑判断和 SQL 拼接,这样测试和排错都容易定位。
3.4 用户认证模块的 JWT 实现方式与安全过滤链
用户认证是本项目最有分量的功能点。常见做法是用 Spring Security + JWT 的组合,但完整引入 Spring Security 后配置繁琐且容易被忽略细节,而题目只是“用户认证模块”,因此更轻量的方案是手写 JWT 工具类 + 拦截器。答辩时你可以说:该方案在课程设计规模下足够安全,且逻辑完全可控。
JWT 工具类负责生成和解析 Token,使用 jjwt 0.12.x 的 API。注意这个版本和 0.9.x 的方法签名变化很大,老代码在 Spring Boot 3 里编译不过。
@Component public class JwtUtil { @Value("${jwt.secret}") private String secret; @Value("${jwt.expire-hours}") private int expireHours; public String generateToken(Integer userId, String username) { return Jwts.builder() .subject(username) .claim("userId", userId) .issuedAt(new Date()) .expiration(new Date(System.currentTimeMillis() + expireHours * 3600_000L)) .signWith(Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8))) .compact(); } public Claims parseToken(String token) { return Jwts.parser() .verifyWith(Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8))) .build() .parseSignedClaims(token) .getPayload(); } }siginWith方法接收 Key 对象,不能直接传字符串;verifyWith必须和生成时使用同一个 Key,将密钥字符串通过getBytes转为字节数组是标准做法。claim("userId", userId)是把用户 ID 塞进 Token 的 payload 部分,后续需要知道当前登录用户是谁时从 Claims 里取。
登录接口的实现逻辑是:根据用户名查出用户,用 BCryptPasswordEncoder 比对密码,成功后生成 Token 返回给前端。前端把 Token 存到 localStorage 或 Pinia,并在每次请求的 Authorization 头里带上,后端通过拦截器统一校验。
public class JwtInterceptor implements HandlerInterceptor { @Resource private JwtUtil jwtUtil; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod)) { return true; } String token = request.getHeader("Authorization"); if (token != null && token.startsWith("Bearer ")) { token = token.substring(7); try { Claims claims = jwtUtil.parseToken(token); request.setAttribute("userId", claims.get("userId")); return true; } catch (Exception e) { response.setStatus(401); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":401,\"message\":\"登录已过期\"}"); return false; } } response.setStatus(401); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":401,\"message\":\"未登录\"}"); return false; } }注意preHandle里的判断顺序:只有请求路径匹配时才拦截,放行接口比如/api/auth/login和/api/auth/register。request.setAttribute("userId", ...)把用户 ID 传递给 Controller,避免了每次都要重新解析 Token。如果项目还引入了 Spring Boot Actuator 监控,务必把/actuator/**排除在认证过滤之外或直接关闭敏感端点,这正是“spring boot actuator 未授权访问”这类安全演练里暴露出来的问题,课程设计阶段就应该养成习惯。
4. 前端实现:Vue 3 项目搭建与 Element Plus 页面组装
4.1 Vite 创建 Vue 3 项目并配置 Element Plus 中文语言包
前端环境搭建是很多人卡住的第一关,核心是 Node.js 版本和 npm 源。建议 Node.js 使用 18 及以上 LTS 版本,Vite 5 对 Node 版本有硬性要求,低于 16 会直接启动失败。
npm create vite@latest shaoxing-frontend -- --template vue cd shaoxing-frontend npm install npm install element-plus @element-plus/icons-vue axios vue-router pinia提示:运行
npm create vite时如果响应卡住,多半是 npm 镜像源问题,先执行npm config set registry https://registry.npmmirror.com再创建项目。
Element Plus 默认组件是英文语言,直接使用日期选择器、分页等组件时会看到英文文本。在main.js中配置 ConfigProvider 包裹根组件即可切换为中文。这里处理不好就会出现“vue3 使用 element plus 的时候组件显示的是英文”这类频繁搜索的问题。
import { createApp } from 'vue' import { createPinia } from 'pinia' import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' import zhCn from 'element-plus/es/locale/lang/zh-cn' import App from './App.vue' import router from './router' import './style.css' const app = createApp(App) app.use(createPinia()) app.use(router) app.use(ElementPlus, { locale: zhCn }) app.mount('#app')ElementPlus是全局注册组件的方式,好处是模板里直接使用<el-button>等组件无需逐个 import,对课程设计这种体量的项目更省时间。全量引入的代价是打包体积变大,但构建产物在 Nginx 上做 gzip 压缩后影响不大。如果追求性能可以改用自动按需导入,但需要额外配置 unplugin-vue-components 和 unplugin-auto-import,复杂度提升不少。
4.2 路由配置与 Axios 封装的请求拦截、响应拦截
Vue Router 4 在 Vue 3 项目中的配置方式很直观。针对游客可访问的展示页和需要登录的会员中心/后台页面,通过路由守卫控制跳转。美食、文化、景点三个页面都是列表加详情结构,路由定义如下:
import { createRouter, createWebHistory } from 'vue-router' const routes = [ { path: '/', redirect: '/scenic' }, { path: '/food', name: 'Food', component: () => import('../views/FoodList.vue') }, { path: '/culture', name: 'Culture', component: () => import('../views/CultureList.vue') }, { path: '/scenic', name: 'Scenic', component: () => import('../views/ScenicList.vue') }, { path: '/detail/:type/:id', name: 'Detail', component: () => import('../views/DetailPage.vue') }, { path: '/login', name: 'Login', component: () => import('../views/Login.vue') }, { path: '/admin', name: 'Admin', component: () => import('../views/admin/AdminLayout.vue'), meta: { requiresAuth: true } } ] router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.meta.requiresAuth && !token) { next('/login') } else { next() } })详情页:type/:id这种动态路由参数写法很通用,:type区分 food/culture/scenic,组件内部根据 type 调用不同的接口。这种方法比写三个详情页面合理得多,也是 vue 路由参数最常见的应用场景。
Axios 封装是每个 Vue 项目的基建,核心是请求拦截器自动带上 token、响应拦截器统一处理 401。封装好了以后每个页面只需要调用api.getFoodList(),不需要重复处理错误弹窗。
import axios from 'axios' import { ElMessage } from 'element-plus' import router from '../router' const request = axios.create({ baseURL: '/api', timeout: 10000 }) request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = 'Bearer ' + token } return config }) request.interceptors.response.use( response => { const res = response.data if (res.code === 200) { return res.data } ElMessage.error(res.message || '请求失败') return Promise.reject(new Error(res.message)) }, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token') router.push('/login') } ElMessage.error('网络异常,请稍后重试') return Promise.reject(error) } )baseURL: '/api'是开发路径,配合 Vite 的 proxy 把请求转发到http://localhost:8080。生产环境下 Nginx 同样把/api反向代理到后端服务,前端代码完全不用改。响应拦截器里res.data的返回值直接是业务数据,页面调用时不用再res.data.records层层取值。这种“统一入口、统一出口”的思路,答辩时也值得作为亮点讲。
4.3 美食展示页面与 Element Plus 组件的组合写法
美食列表页是前端的样板页面,涉及轮播图、卡片布局、分页和搜索。Element Plus 的el-carousel、el-card、el-pagination、el-input四个组件组合起来,就构成了完整的展示页。
<template> <div class="food-page"> <el-carousel height="300px" class="banner"> <el-carousel-item v-for="item in banners" :key="item.id"> <img :src="item.image" :alt="item.name" /> </el-carousel-item> </el-carousel> <div class="search-bar"> <el-input v-model="keyword" placeholder="搜索绍兴美食" clearable @keyup.enter="loadFood(1)" /> <el-button type="primary" @click="loadFood(1)">搜索</el-button> </div> <div class="food-grid"> <el-card v-for="food in foodList" :key="food.id" class="food-card" @click="$router.push(`/detail/food/${food.id}`)"> <img :src="food.image" class="food-image" /> <div class="food-name">{{ food.name }}</div> <div class="food-price">¥{{ food.price }}</div> </el-card> </div> <el-pagination v-model:current-page="page" v-model:page-size="pageSize" :total="total" :page-sizes="[9, 18, 30]" layout="total, sizes, prev, pager, next" @current-change="loadFood(page)" /> </div> </template>v-model:current-page是 Vue 3 中组件双向绑定的语法,Element Plus 的分页组件通过这种方式把当前页码同步到page变量。@keyup.enter让用户在输入框按回车即可搜索,这是个经常被忽略的交互细节。卡片使用@click做整卡跳转,比单独放一个“查看详情”按钮更符合展示类页面的浏览习惯。列表页的样式重点在于flex布局和固定卡片宽度,这部分直接写在<style scoped>里。
文化展示页面与景点展示页面的结构高度类似,唯一区别是字段渲染不同,例如景点要展示开放时间(opening_time)、门票价格(ticket_price)、位置(location)。实际开发时可以直接复制美食页改造,这也正是组件化开发带来的效率提升。
4.4 登录页与管理后台的联调要点
登录页的表单校验使用 Element Plus 的el-form加rules规则,校验通过后调用登录接口,把返回的 token 存入 localStorage,随后跳转回用户之前想访问的页面。这里的逻辑很多初学者写错:跳转目标应该是route.query.redirect,而不是固定跳首页。
后台页面使用侧边栏加顶栏的经典布局,左侧菜单通过el-menu的router模式绑定路由,右侧内容是<router-view>。后台里仅需要两个页面:美食管理(列表、新增、编辑、删除)和用户管理(查看列表、禁用用户)。新增页面使用el-dialog弹窗内嵌el-form,保存时调用新增接口后关闭弹窗并刷新表格,不需要离开当前页面。
后台表格如果需要多选删除功能,会遇到 Element Plus 的@selection-change事件和分页刷新后勾选丢失的问题。原因是切换页码后表格数据重新加载,选中的行对象已经变成了新的对象,没有勾起。这时需要给el-table加上row-key和reserve-selection,选中状态才会通过唯一主键保留。
<el-table ref="tableRef" :data="tableData" row-key="id" @selection-change="handleSelectionChange"> <el-table-column type="selection" reserve-selection width="55" /> </el-table>reserve-selection实现勾选保留后,@selection-change事件的参数rows数组里始终包含所有已选行(即使它们不在当前页面数据中),提交批量删除时拿到的是一个完整列表。这个细节在“element plus 复选框怎么保留勾选”相关问题里出现频率很高,提前做进项目里能省很多麻烦。
5. 部署、排错与答辩前的自检清单
5.1 前后端分开部署的具体命令和 Nginx 反向代理
后端打包时,Spring Boot 3 的 Maven 插件默认打的包内置 Tomcat,直接把 jar 上传服务器即可运行。打包前确认pom.xml里没有spring-boot-maven-plugin版本冲突,然后执行:
mvn clean package -DskipTests java -jar target/shaoxing-demo-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod前端构建命令是npm run build,产物在dist目录。把dist下的文件上传到服务器的/usr/share/nginx/html,然后 Nginx 配置一个 server 块:
server { listen 80; server_name your-domain.com; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://localhost:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }try_files指令是 Vue Router 的 history 模式部署核心,不加它刷新/food/1页面会返回 404。location /api/把接口请求反向代理给后端 jar,前端代码里baseURL写/api就是为了让开发环境和生产环境共用一套代码。如果服务器上有多个站点,注意root路径和server_name的对应。
5.2 答辩演示前 15 分钟的 5 项自检
演示前最容易翻车的环节往往不是功能,而是环境和数据。建议按下面的清单快速过一遍。
| 检查项 | 操作 | 失败影响 |
|---|---|---|
| 后端端口占用 | lsof -i:8080确认未被占用 | 接口全部连接失败 |
| 前端跨域 | 打开浏览器控制台看请求是否 200 | 页面白屏 |
| 数据库编码 | 查询美食名是否乱码 | 展示效果大打折扣 |
| Token 过期 | 检查系统时间是否与服务器一致 | 登录后立即跳回登录页 |
| 图片资源路径 | 确认图片是绝对路径还是相对路径 | 列表页图片裂开 |
有一个容易被忽略的细节是 Element Plus 中 el-image 的引用路径。如果图片字段存的是相对路径如uploads/food1.jpg,而前端部署在/下,那么访问/uploads/food1.jpg时需要 Nginx 额外配置静态资源映射:
location /uploads/ { alias /var/www/shaoxing/uploads/; }5.3 一个提升演示体验的进阶技巧:Mock 数据开关
课程设计项目经常要面对“数据库里数据不完整”的尴尬。建议在 Service 层加一个@Value("${app.mock-enabled}")配置项,当值为 true 时直接返回内置 Mock 数据,不需要连接数据库。这种做法在演示环境没有 MySQL 时能救急,也展示了配置外置的开发习惯。
@Service public class FoodServiceImpl implements FoodService { @Value("${app.mock-enabled}") private boolean mockEnabled; @Override public List<Food> getPage(String keyword, int offset, int limit) { if (mockEnabled) { return MockData.foodList(keyword, offset, limit); } return foodMapper.selectList(keyword, null, offset, limit); } }@Value注解把配置文件里的值注入到字段,切换环境时只改配置文件不重新编译代码。MockData 类可以采用静态方法在内存里生成 20 条假数据,字段和真实实体类一一对应。这个技巧实际价值不大但演示效果很好,因为即使数据库连接断了,页面依然可以完整跑通展示。
本文还有配套的精品资源,点击获取