很多做毕业设计和课程设计的朋友一听到“旅游网站”这种题目第一反应就是满大街的CRUD感觉没什么技术含量。但作为一个带过不少项目的开发者我想说这种题目恰恰是最能拉开差距的。一个看似平平无奇的旅游文化网站骨架是增删改查但血肉里填的是数据表设计是否合理、权限控制是否严密、前后端联调是否顺畅、以及后端接口能不能撑住前端的花式调用。这次拆解一个“SpringBootVue七彩云南文化旅游网站管理平台”的真实项目从数据库设计、后端接口建模、前端页面渲染到部署联调把那些实际开发中花了很长时间才绕过去的坑一次性说清楚。无论你是拿它做毕设、课设还是想练手学全栈这套源码的思路都值得完整过一遍。1. 项目整体定位与技术选型思路1.1 为什么用SpringBootVue这套组合先说结论SpringBootVue在目前的毕设和课设圈子里已经成了一个事实标准。不是因为它多先进而是因为它足够稳妥、资料够多、遇到问题搜得到答案。前端Vue负责页面渲染和交互后端SpringBoot提供接口服务MySQL存数据这三者组合起来分工明确一个学生团队完全吃得下。有人可能会问那用JSPServlet这种传统方式不行吗行但那是五年前的做法了。当下做项目前后端分离是基本盘。Vue的响应式数据绑定配合Element UI组件库做管理后台和前台展示页都很快。SpringBoot则把Spring那套复杂的配置简化到起步依赖里你只需要关心业务逻辑不用天天跟XML配置死磕。具体到这个七彩云南文化旅游平台它要承载的不只是景点列表和新闻公告像旅游线路规划、酒店预订、用户收藏、评论互动这些功能都是可以预期的业务场景。用SpringBootVue来做开发效率高后期扩展也不费劲。1.2 项目模块划分前台展示和后台管理必须分开这个项目按照常见的旅游网站产品形态拆成了两个端一个是普通用户访问的前台网站展示七彩云南的景区风采、文化资讯、美食推荐等另一个是管理员使用的后台管理系统负责内容发布、用户管理、订单审核等操作。这两端页面长什么样决定了你最终交付的完整度。很多学生项目只做了个展示页面后台随便弄几个列表就交差这其实是偷懒。真正的做法是前台页面走Visual Style展示云南的多民族文化元素后台管理走表格和表单逻辑强调信息录入和状态流转的效率。从开发节奏上讲先搭后台再完善前台是比较顺的思路。后台管理把数据录进去前台才能有内容渲染。不然你做完了前台页面发现没有数据源那只能靠写死的假数据撑场面看着不完整。1.3 源码里各目录之间的依赖关系和调用链路拿到源码后第一件事不是双击数据库脚本就跑而是先把整个项目的目录结构摸清。一般的标准结构是project-root ├── backend (SpringBoot工程) │ ├── src/main/java/... │ └── src/main/resources/... ├── frontend (Vue工程) │ ├── src/api/... │ ├── src/views/... │ └── src/router/... └── sql └── travel_platform.sql先讲讲调用链路。前端Vue页面里用户点击某个按钮触发一个API请求比如axios.get(/api/scenic/list)这个请求到达后端SpringBoot的Controller层。Controller层是入口但它不直接查数据库而是调Service层的业务方法Service再调Mapper层或者Repository最终通过MyBatis对MySQL执行SQL返回结果再逐层传递回前端。这个链路易犯的问题就是职责混乱。有的新手把SQL直接写在Controller里有的在Service里写一大堆非业务的逻辑。结果是代码看着别扭改起来非常费劲。规范的做法一定是Controller只做参数接收和响应封装Service只做业务逻辑处理Mapper只做数据访问。2. 数据库设计与核心表结构详解2.1 用户表、景点表、订单表旅游项目的三大基石带着源码跑起来之后你一定会好奇这么多数据是怎么组织的。其实旅游网站数据库设计的核心是三个主表用户表user、景点表scenic_spot和订单表tourism_order。这三个表如果设计得合理后续所有业务都能稳住。用户表是最基础的存储用户名、密码加密后、手机号、头像、角色管理员或普通用户、注册时间。密码必须用MD5加盐或BCrypt加密存储明文密码是绝对红线哪怕项目是交作业用的也不能偷懒。景点表是产品核心需要包含景点名称、所在地区云南有16个州市按州市分区管理、景点简介、详细内容、封面图、价格、开放时间、经度纬度用于地图展示。这里有一个容易被忽略的字段状态。删除功能不要物理删除用逻辑删除字段deleted0未删1已删去标记不然以后想做数据统计分析历史数据全被物理删没了就尴尬了。订单表则承担流程流转的作用关联用户ID和景点ID记录下单时间、出行日期、购买数量、订单金额、支付状态、备注信息。订单状态的枚举值建议用整数存0待支付、1已支付、2已取消、3已完成别直接用字符串存“待支付”这种中文后期扩展和维护都不方便。2.2 扩展表资讯、文化、美食与轮播图的存储设计一个文化旅游平台只靠景点撑不起来必须搭配文化资讯、节庆活动、美食攻略等内容既丰富站内信息也让SEO有东西可抓。资讯表news设计字段标题、封面、摘要、正文、分类比如民族节庆/非遗文化/旅游攻略、发布时间、点击量。这里的正文建议用Text类型不用Varchar因为一篇长文章动辄几千字Varchar默认255长度根本不够用。美食推荐表food类似记录菜品名称、所属地区、配图、介绍、推荐指数。云南美食很有说道过桥米线、野生菌火锅、鲜花饼各有故事介绍字段写好了前台渲染出来的页面才不显干瘪。轮播图表banner适用场景很大首页顶部的宣传大图这通常是运营位。表设计很简单图片地址、跳转链接、排序值、状态。重点是排序值它直接决定了轮播顺序前台查询时记得按排序值升序取前几条。2.3 MySQL建表时的字段规范与索引优化建议数据库表设计完了字段类型选择必须讲究。主键一律用BIGINT自增不用VARCHAR UUID为什么自增主键在InnoDB引擎下有聚簇索引优势插入性能好而UUID是无序字符插入时会造成页分裂性能差不少。时间字段用DATETIME别用TIMESTAMP。TIMESTAMP有2038年上限问题而且受时区影响DATETIME则没有这个问题存读取时都稳定。凡是经常查询的字段必须建索引。比如订单表里的用户ID、景点ID、下单时间这些字段在列表页和统计报表里会被频繁作为查询条件不加索引数据量一大就会全表扫描查询速度直线下降。以下是核心表结构的推荐示例以景点表为例CREATE TABLE scenic_spot ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, name VARCHAR(100) NOT NULL COMMENT 景点名称, region VARCHAR(50) DEFAULT NULL COMMENT 所属州市, summary VARCHAR(500) DEFAULT NULL COMMENT 简介, detail TEXT COMMENT 详细介绍, image VARCHAR(255) DEFAULT NULL COMMENT 封面图地址, price DECIMAL(10,2) DEFAULT 0.00 COMMENT 门票价格, open_time VARCHAR(50) DEFAULT NULL COMMENT 开放时间, longitude DECIMAL(10,6) DEFAULT NULL COMMENT 经度, latitude DECIMAL(10,6) DEFAULT NULL COMMENT 纬度, status TINYINT DEFAULT 1 COMMENT 1上架 0下架, deleted TINYINT DEFAULT 0 COMMENT 逻辑删除标记, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), KEY idx_region (region) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT景点信息表;utf8mb4这个编码必须强调一下。整个项目全程使用utf8mb4才能完整支持生僻字和emoji有些景点介绍文字里可能混有少数民族文字的特殊符号如果用utf8插入会报错或变成问号。3. 后端SpringBoot核心实现接口、权限与业务代码3.1 Controller层规范与统一响应体封装后端这边第一个要掌握的是统一响应体。一定不要每个接口返回一个裸的List或者Map那样前端无法统一处理错误状态。实际项目中封装一个Result类内部包含code、msg、data三个字段。public class ResultT { private Integer code; private String msg; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.code 200; result.msg 操作成功; result.data data; return result; } public static T ResultT error(Integer code, String msg) { ResultT result new Result(); result.code code; result.msg msg; return result; } // 省略getter/setter }接口的URL命名也要统一风格。比如/api/scenic/list获取景点列表/api/scenic/detail/{id}获取景点详情/api/order/create创建订单。接口路径语义清晰前端调用时就不会出现乱七八糟的拼接。Controller层要做的事尽量单纯一个接口方法就干一件事。参数校验可以用Spring的Validated注解配合NotNull、NotBlank这些约束注解参数不对就直接返回错误信息不用在业务代码里手动写一堆if判断。3.2 Service层业务逻辑与事务处理Service层是业务重镇也是一张考卷。以创建订单为例它的业务流程是接收用户ID和景点ID查询景点信息拿到单价计算总价扣减库存如果有数量限制生成订单记录。这一串操作有一条不成文的规矩——必须加事务。SpringBoot里用Transactional注解声明式事务就能搞定。不加这个注解会怎么样呢如果查景点信息成功了扣减库存也成功了但生成订单时数据库报错了那前面两步操作都会残留下来数据一致性彻底破坏。这在旅游高峰期订票场景下是重大事故级别的bug。Service public class OrderServiceImpl implements OrderService { Autowired private ScenicSpotMapper scenicSpotMapper; Autowired private OrderMapper orderMapper; Override Transactional public Long createOrder(OrderCreateRequest request) { ScenicSpot spot scenicSpotMapper.selectById(request.getScenicId()); if (spot null) { throw new BusinessException(景点不存在); } TourismOrder order new TourismOrder(); order.setUserId(request.getUserId()); order.setScenicId(spot.getId()); order.setOrderAmount(spot.getPrice().multiply(request.getNum())); order.setStatus(0); orderMapper.insert(order); return order.getId(); } }除了事务还有一类逻辑值得单独拎出来业务异常的抛出。永远不要返回一个默认的空值然后把错误吞掉。项目里可以定义一个BusinessException在Service里抛出去再由统一异常处理器RestControllerAdvice捕获并转成响应体。这样做的好处是前端收到的错误信息稳定可读不会出现框架默认的异常页面。3.3 登录认证与JWT Token签发登录模块是整个系统安全性的门面。常见的方案是JWT流程如下用户提交用户名密码后端校验成功后生成一个Token里面携带用户ID和角色返回给前端。前端存在localStorage里之后每次请求都在Header里带一个Authorization: Bearer token。后端需要一个拦截器Interceptor或者SpringSecurity配置对所有非公开接口做Token校验。解析Token拿到用户信息后放入ThreadLocal或Request作用域供后续业务方法使用。JWT有一个务必注意的点不要把敏感信息塞进Token里。Token是Base64编码中间那段Payload明文可读如果我把用户的手机号和银行卡号写了进去等于把敏感信息暴露给了任何人。Token里只放用户ID和角色其他信息需要时再去数据库查。// JWT工具类的核心生成逻辑 public String generateToken(Long userId, String role) { return Jwts.builder() .claim(userId, userId) .claim(role, role) .setExpiration(new Date(System.currentTimeMillis() EXPIRE_TIME)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); }拦截器配置里千万别忘了放行登录接口、注册接口和前台公开查询接口。很多新手把拦拦截器配置成拦所有路径结果前台的景点列表都打不开了又把樱拦截器整个取消后台管理又裸奔了。正确逻辑是白名单加黑名单双管齐下。我找了个示例这是配置登录拦截的核心部分直接复用即可public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (StringUtils.hasText(token) token.startsWith(Bearer )) { Claims claims JwtUtil.parseToken(token.replace(Bearer , )); if (claims ! null) { request.setAttribute(userId, claims.get(userId)); return true; } } response.setStatus(401); return false; } }配套的WebMvcConfig里单独维护一个放行列表Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns(/**) .excludePathPatterns(/api/auth/login, /api/auth/register, /api/scenic/list, /api/scenic/detail/**); }3.4 MyBatis-Plus的使用细节分页、条件构造器与代码生成这个项目中的数据访问层如果手写MyBatis XML工作量大得惊人。建议直接用MyBatis-Plus它的BaseMapper自带单表增删改查分页插件一行配置搞定。使用分页插件时注意要配置拦截器版本。SpringBoot 2.x对应MybatisPlusInterceptor设置分页参数必须传PageT对象前端传过来的pageNum和pageSize都要作为分页参数未经拦截器处理的分页查询实际上是全表查询然后内存截断数据量一大就崩。Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }MyBatis-Plus还有查询条件的构造器用起来非常顺手。比如景点列表筛选LambdaQueryWrapperScenicSpot wrapper new LambdaQueryWrapper(); wrapper.eq(ScenicSpot::getRegion, 大理) .eq(ScenicSpot::getStatus, 1) .orderByDesc(ScenicSpot::getCreateTime);注意条件构造器的使用场合动态条件拼装非常方便但如果SQL逻辑复杂、多表关联还是老老实实写XML里自定义SQL别硬用Wrapper凑。4. 前端Vue页面实现与前后端联调要点4.1 Vue工程初始化与axios请求封装前端这边Vue工程初始化可以通过Vue CLI或Vite搭建。Vue 2项目用Vue CLI还是主流选择Vue 3配合Vite则更轻快。毕设项目如果追求稳建议Vue 2 Element UI Vue CLI生态成熟文档多踩坑成本低。前端工程一创建第一件事就是封装axios。不封装的后果是页面里每个组件都写着this.$http.get(...)超时时间、错误处理、Token注入全部重复。按照项目规范做可以对axios实例设置baseURL、超时时间、请求拦截器和响应拦截器import axios from axios const service axios.create({ baseURL: process.env.VUE_APP_BASE_API, timeout: 10000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config }) service.interceptors.response.use( response response.data, error { if (error.response error.response.status 401) { localStorage.removeItem(token) window.location.href /login } return Promise.reject(error) } ) export default service这样的封装完成后每个接口函数只需写一行的请求调用代码清爽出问题也从拦截器统一排查。4.2 前端页面路由设计与守卫拦截Vue Router是管理页面跳转的核心。前台和后台的路由要分开。前台有首页、景点列表、景点详情、文化资讯、美食推荐、个人中心后台有数据看板、用户管理、景点管理、订单管理、资讯发布、轮播图配置。路由守卫是必加的一环。beforeEach里判断用户是否登录如果未登录访问任何需要权限的页面一律重定向到登录页。后台管理页面还要多判断一步非管理员不要放进去。router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next(/login) } else if (to.meta.isAdmin userInfo.role ! ADMIN) { next(/403) } else { next() } })路由懒加载是个容易被忽略的性能优化点。把所有组件都放在import后面直接引入意味着首屏要加载全部页面代码访问慢。改成component: () import(../views/ScenicList.vue)就能实现按需加载首页跑起来明显更快。4.3 前后端联调时的跨域问题与代理配置联调过程中前端跑在8080端口后端跑在8081端口直接访问必然跨域。跨域问题有两个解决办法前端配置Vue CLI的devServer代理或者后端配置CorsFilter。推荐前端代理方案开发环境下最简单module.exports { devServer: { port: 8080, proxy: { /api: { target: http://localhost:8081, changeOrigin: true, pathRewrite: { ^/api: } } } } }这个配置的意思是前端发到/api开头的请求自动转发到后端8081端口。这样前后端联调时接口就变成同源了不触发跨域。如果非要后端解决跨域注意一个坑CrossOrigin注解不要加在Controller类上那样全局生效范围不明确规范的做法是写一个全局的CORS配置类固定允许原始域。4.4 富文本上传、图片上传与文件存储方案很多页面内容比如文化资讯的正文需要富文本编辑器。推荐用vue-quill-editor这个组件它支持图片上传。这里特别容易踩坑的是富文本编辑器里的图片上传地址要指向后端接口而后端要把接收到的图片文件存到本地磁盘或者云存储。后端接收文件上传的写法PostMapping(/api/upload/image) public ResultString uploadImage(RequestParam(file) MultipartFile file) { String originalFilename file.getOriginalFilename(); String suffix originalFilename.substring(originalFilename.lastIndexOf(.)); String fileName UUID.randomUUID().toString() suffix; // 存储到本地或者OSS File dest new File(UPLOAD_DIR, fileName); file.transferTo(dest); return Result.success(/images/ fileName); }这里的文件路径必须思考清楚一个问题上传后的图片URL是前端口径还是后端口径。最稳妥的做法是后端将文件存到服务器本地目录然后对外暴露一个静态资源映射路径前端直接拼接这个URL就能访问。5. 前台页面设计七彩云南文化元素怎么展示5.1 首页模块轮播图、景点精选、文化资讯三区联动首页是整个平台的颜值担当。用户第一眼看到什么决定了他停不留在你网站上。具体到七彩云南这个主题首页大体分三个区块。顶部是轮播图。轮播图不要只放一张风景大图也要配上宣传语比如“七彩云南旅游天堂”、“大理三月好风光”这类有带入感的slogan图片配上文字叠加效果页面观感会有一个明显的提升。轮播图的数据来源是后台管理的banner表管理员可以在后台随时更换图片。中间是景点精选区。这里调用后端的/api/scenic/list?recommend1接口后台按推荐标记查询前6个景点前台以卡片网格展示封面图、景点名称、所属地区、门票价格四个信息卡片化展示。卡片要有hover效果鼠标移上去图片放大这是视觉交互的小细节很多学生的页面忽略了看起来就显得很“钝”。底部是文化资讯区展示最新的资讯文章标题列表。每篇资讯带一张小图和摘要点击跳到详情页。这个模块的存在是为了充实首页的内容密度也为用户提供“逛”的入口。5.2 景点详情页参数传递与富文本渲染景点详情页是用户决策的关键页面信息展示必须完整。页面通过this.$route.params.id拿到景点ID然后调用详情接口把后端返回的景点名称、详细描述、价格、开放时间、地图位置逐一渲染。这里有个技术细节务必掌握后端返回的详细描述是富文本HTMLVue默认会把HTML转义成纯文本需要渲染富文本时不能直接插值而要用v-html指令或者第三方组件字符串模板内就是插入HTML的权威方式。div classspot-detail v-htmlspotDetail.detail/div不过这有一个安全问题必须提醒如果富文本内容由后台录入且后台权限不安全v-html可能被XSS攻击。因此后台录入时要做内容过滤或者限制富文本只允许白名单标签。地图展示推荐使用高德地图JavaScript API在页面中引入库然后根据景点经纬度坐标初始化。这个功能加进去整个项目的完成度立马上了一个档次从“我用代码堆积了一个网站”变成“我做完了一个产品”。5.3 个人中心与订单管理用户的个人中心页面一般包含我的资料、我的订单、我的收藏。我的资料是修改个人昵称、头像、手机号。这是一个有趣的功能点修改资料时注册时用的密码字段怎么处理不能直接改要么加一个原密码校验要么只允许修改非密码字段。毕设项目里大多数只改非密码字段安全性够用即可。我的订单是一个列表页展示用户创建的订单记录。每单显示景点名称、下单时间、出行日期、订单金额、订单状态。已支付的订单前端提供“取消订单”按钮后端逻辑里校验订单状态只有待支付状态才允许取消。用户交互的基本规则就是状态流转前端传来的金额从不可信后端必须用数据库里的价格重新计算。个人中心是面向普通用户的入口这些路径都要在路由守卫的认证保护之下没有登录态直接重定向到登录页这一步不能省。6. 后台管理模块从数据看板到内容发布6.1 后台布局与菜单权限控制后台管理页面采用Element UI的Container布局左侧是菜单栏右侧是内容展示区。菜单项包括工作台、景点管理、资讯管理、美食管理、订单管理、用户管理、轮播图管理。点击菜单右侧router-view切换对应的管理页面。菜单项要根据角色动态渲染。管理员能看到所有菜单普通运营人员只能看到部分菜单。这个动态渲染的逻辑需要前端和后端配合登录接口返回用户角色前端存到Vuex或sessionStoragev-if判断角色显示菜单。后端接口层面同样要做权限校验不能只靠前端隐藏菜单来防未授权访问接口鉴权要在后端完成。6.2 景点管理分页列表、状态切换与图片回显景点管理是后台用得最多的模块。列表页要实现分页查询表格展示景点名称、所属地区、价格、状态、创建时间。表头右侧放“编辑”和“删除”按钮新增景点通过弹窗Dialog内嵌表单来完成。状态切换这里有一个高频操作景点下架。下架不是删除而是把状态字段改成0。前台查询景点列表时只查询status1的数据下架后用户就看不到该景点了。这种设计的好处是管理员随时调整内容上线状态而不影响历史数据。新增或编辑景点时表单里有一个封面图上传组件。上传成功后后端返回图片地址前端把它赋值给表单项里的image字段。图片回显是很多新手做不到位的点。编辑回显时把数据库里存好的图片地址赋给上传组件的fileList确保编辑页面打开时图片能正常显示而不是只看到一个空框。6.3 订单状态管理与用户数据报表订单管理后台不需要太复杂的交互核心就是三个功能查询列表、查看详情、修改状态。列表页按订单编号或下单用户筛选详情页展示订单的所有字段信息。修改状态一般就是后台客服为线下支付的订单手动标记为已支付。如果项目里还想加一点“数据味道”做一个简单的工作台数据看板。统计几个核心指标用户总数、景点总数、当月订单数、当月订单金额。再放一个ECharts折线图展示近七天的订单量趋势。这段代码不需要太复杂但加上它整个项目的视觉效果和答辩加分项都多了不少。6.4 文件上传目录权限与本地存储规划后台管理涉及大量的图片上传操作文件存储的位置要有统一规划。在SpringBoot配置文件中设置上传目录upload.dir/data/travel_platform/images同时配置静态资源映射让/images/**指向该目录。如果你用Windows本地跑就用file:D:/travel_platform/images/这种绝对路径但要注意目录必须提前创建好否则上传时会报找不到目录的错误。Linux服务器部署时上传目录千万别选/root目录下否则权限问题会一直困扰你。规范做法是在/usr/local或/data下建独立目录并将目录所有权分配给运行应用的用户。7. 部署联调与常见问题排查7.1 后端打包、前端构建与环境配置项目开发完成后面临的是部署上线环节。后端打包用Maven的package命令生成一个JAR包mvn clean package -DskipTests这个JAR包就是整个后端服务可以直接运行java -jar travel-platform.jar --spring.profiles.activeprod前端构建用npm命令npm run build构建完成后dist目录里是纯静态文件可以交给Nginx托管或者在SpringBoot里把dist目录映射为静态资源目录。更常见的做法是Nginx负责托管前端静态文件同时反向代理/api请求到后端服务。这是最标准的前后端分离部署方案。生产环境配置和开发环境一定要分开写配置文件用SpringBoot的profile机制切换。在application-prod.yml里配置生产数据库连接、上传目录等避免把本地数据库密码带到线上环境。这个细节在答辩时也能体现出工程意识。7.2 启动失败的几类常见原因与解决路径项目启动时报错是所有开发者的必修课。排查启动类问题首先要学会看控制台日志最后几行不要从头到尾刷过去直接定位ERROR。数据库连接失败是最常见的情况提示Access denied for user通常就是账号密码错了或者账号权限不够。另一个常见报错是Unknown database说明数据库名配错或是SQL脚本没导入成功。Communications link failure则是数据库地址或端口不对或者MySQL服务没有启动。前端npm安装依赖时经常遇到ELIFECYCLE错误大概率是node_modules缓存错乱优先使用rm -rf node_modules加npm cache clean --force再重装依赖。Vue CLI创建工程时node版本不匹配也会导致各种包解析失败建议先用nvm use 16切换到一个长期支持的Node版本再继续跑项目。7.3 接口404、跨域报错和Token失效的排查技巧接口404的核心原因很简单路径不一致。前端调用的URL和后端Controller的RequestMapping对不上。排查时先在浏览器开发者工具Network面板里看请求的URL确认请求打了哪个路径再去后端代码里找对应的接口路径逐个比对。跨域报错浏览器的提示非常明显Access to XMLHttpRequest at http://localhost:8081/api/... from origin http://localhost:8080 has been blocked by CORS policy。出现这个错误先看前端代理配了没再检查后端有没有全局配置允许跨域来源。我的经验是前端代理配置了但还是跨域多数是changeOrigin没写或者后端代码里的CORS配置顺序被拦截器覆盖了。Token失效的报错通常表现为接口返回401。遇到这个情况先确认登录接口正常生成Token再确认前端请求拦截器把Token正确放入了Header。调试时可以直接在后端拦截器里加日志打印接收到的Token。这个级别的问题排查多花点时间把日志打通比盲目猜测快得多。8. 踩坑记录与毕设答辩加分项8.1 我实际踩过的5个坑你尽量绕开第一个坑是数据库没有导入就直接启动后端。拿到源码第一件事永远是先把SQL脚本导入MySQL否则后端连着空数据库跑启动倒是成功了一查接口就报错“表不存在”。第二个坑是前端npm依赖版本冲突。旅游网站的结构里会用到很多插件特别容易出问题的是Element UI和Vue版本不匹配。Vue 2项目必须用Element UI的2.x版本如果误装了Element Plus就会白屏。报错信息只要搜一下就知道根因但仍有很多人在这上面卡一整天。第三个坑是跨域配置和接口代理重复设置。前端已经配了代理后端还全局加了CORS允许所有来源表面上没问题但这种双保险在部分浏览器上有时候反而出bug。规范打法就是二选一本地开发优先前端代理发布上线后靠Nginx反代解决同源问题。第四个坑是多文件上传。项目里如果涉及相册功能前端传FileList数组后端收到的是多部分表单数据必须用MultipartFile[]接收。新手最容易写错的是用RequestBody ListMultipartFile接收文件这是完全行不通的RequestBody是处理JSON的工具和文件上传是两个体系。第五个坑是token存错位置。登录后把token存到sessionStorage但是请求拦截器里莫名其妙去读localStorage两边不一样导致接口一直401。统一存储策略很关键项目从头到尾token就只放在localStorage拦截器也只从localStorage读。8.2 答辩时怎么讲项目才能体现真实开发能力如果你拿这个项目去答辩讲法很有讲究。不要一上来就对着PPT念技术名词而应该紧紧围绕“核心业务场景”来讲。比如讲到景点管理围绕“管理员如何上架一个新景点”的完整链路前端表单收集字段图片上传到后端保存到数据库再回显到前台页面。把一条链路讲透比堆五个技术名词都管用。答辩时一定要能回答清楚“为什么选择这些技术”这个问题。被问到“为什么不直接用JSP做”时回答思路应该是项目需要前后端分离前端变化频繁需要组件化和响应式能力后端要面向接口提供服务可能被多个端复用。这比单纯说“大家都用这个”高级得多。再准备一个扩展方向的回答比如“这个项目后续如何优化”。可以从性能层面讲增加Redis缓存热点景点数据减少数据库压力从体验层面讲接入地图API和天气接口让景区页面更智能从业务层面讲增加支付对接和短信通知。这些提法不需要实现但要能说出来体现你思考过产品的演进方向。8.3 项目代码的可维护性维护注释、命名与Git管理最后说一说代码质量。很多学生项目代码能从答辩老师那里拿高分核心就是三个字可维护。变量命名要见名知意别用list1、temp2这种。方法要短一个方法只干一件事。常量要抽出来像订单状态都定义在单独的常量类里不要散落在代码中。注释不是越多越好。好的代码是一读就懂注释负责说明业务背景和特殊情况。比如这样写注释就够“状态从待支付改为已支付同时锁定景点的库存”这种注释有价值而那些“// 返回列表”的注释纯属噪音。Git的管理可以从项目一开始就启用。提交规范建议用feat、fix、docs开头区分提交类型比如“feat: 新增景点列表筛选功能”“fix: 修复订单金额计算精度丢失”。答辩时把Git提交记录展示出来比任何口头描述都有说服力。9. 项目扩展方向与个人开发建议9.1 从毕设到工业级项目的三个可落地升级这个项目跑通之后如果你想更进一步有几个可落地的升级方向值得投入引入Redis缓存到热点查询上景点详情页属于读多写少的场景把热点景点的详情缓存起来扛并发能力立竿见影引入消息队列用于订单创建后的异步通知拆成微服务架构把用户中心、订单中心独立成独立的服务。不过给个中肯建议毕设阶段不要一股脑把微服务、容器化、分布式全堆上去技术栈越复杂答辩时被问倒的风险越大。把单体项目做深做透性能优化做到位展示效果已经非常好了。9.2 适合进一步练习的三个综合课题如果这套源码你已经吃透了还可以构思三个同类课题用来练手民宿预订管理平台订单模块的复杂程度更高涉及入住日期区间计算、文创商品商城涉及购物车、库存扣减、支付流程、旅游攻略社区涉及用户发帖、评论回复、内容审核。这三个课题在数据表设计和接口复杂度上都在这个旅游平台的基础上做了升级你之前积累的经验可以直接迁移。说到底做这类项目真正的收获不是“我跑通了一个网站”而是在一次次报错、排查、修复的过程中建立起对全栈开发完整链路的感知能力。今后无论做哪个方向这段经验都不会白费。最后分享一个我自己调试项目时稳赚不赔的土办法不管前后端哪一端报错先把完整日志复制下来看最后三行关键信息再往上翻回溯线索直接去搜索引擎搜报错的关键片段。别把时间浪费在反复重启服务上学会读日志你的开发效率至少翻倍。