免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

SpringBoot+Vue前后端分离:流浪宠物领养平台开发全流程详解

SpringBoot+Vue前后端分离:流浪宠物领养平台开发全流程详解 1. 项目到底要做什么需求拆解与方案选型1.1 流浪宠物领养平台的核心业务场景流浪宠物领养平台这个题目在高校毕业设计里算是非常经典的一类它的本质其实是一个典型的“信息撮合类”业务系统。你往大了看它可以类比成宠物界的“闲鱼”或“公益版58同城”往小了看就是一个宠物信息管理后台加一个用户浏览申请前台。但别因为它经典就觉得简单恰恰相反这类系统覆盖的业务链条相当完整。从用户侧来看普通访客可以浏览待领养宠物列表、查看宠物详情照片、年龄、健康状况、性格描述、提交领养申请还能查看个人申请进度从管理侧来看管理员需要维护宠物信息的上架和下架、审核用户提交的领养申请、管理公告通知、处理用户账号状态。有时还会加一些辅助功能比如宠物寄养登记、寻宠启事、爱心捐赠记录等。这些功能听起来不多但落地到代码上一套完整的用户端、管理端、数据库、接口文档、部署方案做下来量其实不小。也正因如此这个题目非常适合用来系统性地练一遍前后端分离开发的全流程——从需求分析、数据库建模、后端接口开发、前端页面联调到最后的打包部署每一环都能学到具体的东西。我在设计和实现这个项目时没有盲目堆功能而是先把核心痛点拎出来流浪宠物信息分散、领养流程不透明、审核机制缺失。所以最终确定的角色模型是普通用户和管理员核心业务模型是“宠物信息发布—用户提交申请—管理员审核—线下交接确认”这条主线其他功能都围绕这条主线做延伸。1.2 为什么选SpringBootVue这套前后端分离方案现在社区里做这类管理系统的技术方案大概有几种流派纯JSPServlet的老派做法、Spring Boot Thymeleaf服务端渲染的做法、Spring Boot Vue前后端分离的做法以及一些更轻量的方案比如Node.js React。这几种我都有接触过如果从学习价值和实际工作衔接度两个维度来看SpringBoot Vue这套组合是最稳妥的。首先说SpringBoot。它把Spring家族里大量繁琐的XML配置给干掉了通过自动装配机制让你用最少量的配置就能把一个Web项目跑起来。对于做毕设或者练手项目的人来说SpringBoot最大的价值在于“你不需要是一个Spring配置专家也能把项目启动”但你又不能完全不懂Spring的底层逻辑这种恰到好处的难度曲线非常合适。而且SpringBoot天然集成了内嵌Tomcat打包成jar直接就能跑部署成本极低。再说Vue。Vue在国内前端社区的使用率一直居高不下学习曲线比React平缓对新手极其友好。Vue的响应式数据绑定、组件化开发、Vue Router路由管理、Vuex/Pinia状态管理这些概念恰好覆盖了一个前端项目从零搭建到上线的大部分知识点。更重要的是Vue生态里的Element UI现在新版叫Element Plus组件库做管理后台简直是开箱即用表格、表单、弹窗、分页全都有现成组件能省掉大量重复的样式和交互代码。前后端分离本身也是一种更接近真实工业界的工作模式前端只管页面渲染和数据展示后端只管接口逻辑和数据处理两边通过约定的JSON格式进行通信各改各的互不干扰。这个项目的开发过程里我基本就是两个窗口来回切后端写一个接口前端立刻调一下测试联调效率比传统的服务端渲染高很多。1.3 技术栈清单与版本选择经验具体到我这个项目完整的技术栈清单如下每一层都可以单独拿出来写经验层次技术选型版本/说明后端框架Spring Boot2.7.18JDK8兼容性好稳定ORM层MyBatis-Plus3.5.x内置分页插件和代码生成器数据库MySQL8.0以上字符集utf8mb4安全认证JWT 拦截器jjwt 0.9.1无状态认证方案前端框架VueVue 2.x Element UI兼容性和资料最多前端构建Vue CLI / ViteVue CLI 4.x或5.xVite也可以HTTP请求Axios统一封装请求拦截器和响应拦截器文件存储本地存储 Nginx映射也可接MinIO看实际部署环境部署Docker Compose 或 传统jarnginx推荐Docker方式来演示版本选择这里我要多说一句。很多新手上来就装最新版Spring Boot 3.x、Vue 3.x、Element Plus一套组合拳打下来结果光是踩兼容性坑就能消耗掉一半的开发时间。我的经验是如果目标是快速做完一个可用系统而不是研究新技术那就老老实实选一套经过社区大量验证的稳定组合。Spring Boot 2.7.x Vue 2.x Element UI是目前中文社区资料最丰富、遇到问题最容易搜到解决方案的组合。至于Spring Boot 3.x和Vue 3.x等你把这套流程跑通之后再迁移过去也不迟核心逻辑都是相通的。2. 数据库设计把业务落到表结构上2.1 核心数据表与设计思路数据库设计是这类项目的命脉表结构建不好后面写SQL和业务逻辑的时候处处别扭。我设计的时候遵循了一个原则宁可多拆几张表也不要在一张表里塞一堆含义模糊的冗余字段。这个平台最终落了8张表核心的6张是这么设计的用户表user字段包含id、username、passwordBCrypt加密、nickname、phone、avatar、role0表示普通用户1表示管理员、status0禁用、1正常、create_time、update_time。这里有个细节值得注意用户名和手机号都加上唯一索引防止重复注册。宠物信息表pet字段包含id、name、category猫/狗/其他、breed品种、age、gender、health_status健康状态描述、vaccine_status是否已打疫苗、sterilization_status是否已绝育、description详细描述用TEXT类型、cover_image封面图URL、images多图用JSON字符串存、status0待审核、1已上架、2已下架、3已领养、publisher_id发布者ID、create_time、update_time。我把status字段做了细分特别是“已领养”这个状态它不是一个简单的删除逻辑而是业务闭环里的一个终点。宠物一旦被领养这条记录在列表里就不能再展示但管理后台仍然可以查看历史记录。领养申请表adoption_application字段包含id、pet_id、user_id、applicant_name、applicant_phone、address居住地址、home_condition家庭环境描述比如是否有封窗、是否有其他宠物、reason申请理由、status0待审核、1已通过、2已拒绝、3已完成、reject_reason拒绝原因审核不通过时填写、create_time、update_time。这个表是整个平台审核逻辑的核心载体设计得细一点管理员审核的时候才有依据。公告表notice字段包含id、title、content、cover_image可空、status0草稿、1已发布、create_time、update_time。其他还有收藏表favorite、管理员操作日志表operation_log这一类辅助表看个人需要决定加不加。我建议加上收藏表因为“收藏”这个功能虽然不起眼但它能明显提升用户的使用粘性而且实现起来就是用关联表查一下成本很低。2.2 表关系与关键索引设计这个项目里的表关系其实不算复杂但是有几处需要特别注意用户—宠物一对多。一个用户可以发布多条宠物信息所以pet表里的publisher_id指向user表的id。注意这里的逻辑不要和“管理员录入宠物”搞混实际项目中管理员也可以代发布所以可以加一个字段区分来源是“用户发布”还是“管理员录入”。用户—领养申请一对多宠物—领养申请一对多。一个用户可以对多只宠物发起申请但这里有一个需要前端和后端同时控制的逻辑同一个用户不能对同一只宠物重复提交领养申请。这个约束单靠前端按钮置灰是不够的必须在后端查询里做一次校验否则恶意用户直接调接口就能绕过。用户—收藏多对多。通过favorite表做关联favorite表里user_id和pet_id加联合索引。哪些字段要建索引我的经验是结合实际的查询需求来定。这个系统里高频查询有这几类宠物列表按状态和分类查询、领养申请按用户或按宠物查询、收藏列表按用户查询。对应的索引就是pet表的status category联合索引adoption_application表的user_id、pet_id单列索引favorite表的user_id pet_id联合索引。至于create_time这种字段分页排序的时候也会用到可以顺手加上但不用太纠结数据量小的时候索引收益没那么明显。表关系图我建议用Navicat或者DBeaver画出来写文档的时候贴上去会很加分。具体表结构的SQL脚本要注意用utf8mb4字符集不然存emoji表情或者生僻字的时候会报错这是一个很经典的低级坑。2.3 状态字段设计从“待审核”到“已完成”很多新手做这类系统的时候对状态字段的处理非常随意要么用String类型随便存个“待审核”“已通过”要么用int存裸数字不写注释导致后面接手的人完全看不懂。我的做法是用int类型存枚举值同时在代码里用常量类或枚举类统一定义。宠物状态我定义为0待审核、1已上架、2已下架、3已领养。领养申请状态定义为0待审核、1已通过、2已拒绝、3已完成。这里的“已完成”和“已通过”不一样管理员审核通过后状态是1此时用户看到的是“申请通过等待联系”后面双方线下完成交接管理员再把状态改成3宠物表里的状态同步更新为“已领养”。这个“二次状态确认”的设计思路是为了让整个领养流程有始有终。如果不加“已完成”这个状态管理员一旦审核通过流程就断了后续宠物是否真的被领养走了完全无据可查。加了之后管理后台可以形成一个完整的数据闭环也算是一个业务逻辑上的亮点写论文的时候可以重点描述。前端展示的时候状态值需要统一做映射处理。我的做法是在前端定义好常量映射比如const PET_STATUS {0: 待审核, 1: 已上架, 2: 已下架, 3: 已领养}然后通过计算属性或者过滤器来展示避免每个页面散落着一堆魔法数字。3. 后端核心模块实现要点3.1 项目初始化与基础配置后端项目的创建我推荐直接用IDEA的Spring Initializr也可以去start.spring.io网站生成后导入IDEA。最核心的几个配置项JDK版本选8或11都行我用的JDK8。Spring Boot版本选2.7.18这个版本是目前2.x系列的最后一个版本修复了大量已知问题稳定性最好。依赖方面需要引入Spring Web、MyBatis-Plus、MySQL驱动、Lombok、jjwt用于JWT生成与解析、Hutool工具类非必需但好用。application.yml的核心配置长这样server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/pet_adoption?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: 123456 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 servlet: multipart: max-file-size: 10MB max-request-size: 20MB mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 jwt: secret: your-secret-key expire: 604800注意几个容易踩坑的地方。第一url里的serverTimezone如果不配置高版本MySQL驱动会报时区错误。第二characterEncodingutf8和characterEncodingutf8mb4不一样必须用后者。第三MyBatis-Plus的逻辑删除配置很重要我这边是设置了deleted字段自动拼接WHERE deleted 0避免全表扫描时把已删除的数据查出来。Lombok的引入能省掉大量getter/setter代码但要注意IDEA必须安装Lombok插件否则编译直接报错找不到方法。这是一个环境问题很多新手卡在这里。3.2 JWT身份认证与拦截器设计登录认证方案我选了JWT而不是传统的Session。原因有三点前后端分离之后Session的跨域处理比较麻烦JWT是无状态的服务端不需要存储会话信息水平扩展的时候更轻松JWT本身包含用户身份信息和过期时间前端拿到之后存到localStorage里每次请求带上Authorization头即可。JWT工具类主要做三件事生成token、解析token、校验token是否过期。生成时把用户的id、角色和用户名放进去签名算法用HS256密钥放在配置里。拦截器配置这步是后端最关键的一个环节。SpringBoot里我implements HandlerInterceptor重写preHandle方法在方法里从请求头取Authorization如果token不存在或者解析失败直接返回401状态码然后设置Content-Type为application/json;charsetUTF-8返回一个统一的JSON错误体。前端拿到401之后自动跳转到登录页。同时还需要在WebMvcConfigurer里注册拦截器并设置排除路径。排除路径至少包括登录接口、注册接口、宠物列表查询接口、宠物详情接口、图片访问路径、以及前端的静态资源。这些做完了才算真正划清了哪些接口需要认证、哪些接口可以匿名访问。我实际做的时候还加了一个小细节管理员接口和普通用户接口的权限区分。管理员相关的接口地址统一以/admin/**开头拦截器里除了校验token有效性还额外校验token里携带的role字段是否为管理员不是则返回403。这样权限控制就集中在一个拦截器里完成了不会在业务代码里散落大量重复的权限判断。3.3 宠物管理接口与分页查询宠物模块是所有接口里最核心的部分主要包含发布宠物、宠物列表分页查询、宠物详情、修改宠物信息、上下架、删除这几个接口。分页查询这里我强烈推荐MyBatis-Plus的分页插件它用起来非常简单配置一个MybatisPlusInterceptorBean然后加入PaginationInnerInterceptor之后调用page()方法就能自动完成分页。Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; }宠物列表查询接口要做成一个多条件筛选的分页接口支持的查询参数包括petName关键字模糊查询、category分类筛选、status状态筛选、排序字段按时间倒序或按浏览量倒序。这里直接用MyBatis-Plus的LambdaQueryWrapper操作即可代码可读性很高。public PagePet getPetList(int pageNum, int pageSize, String keyword, Integer category, Integer status) { PagePet page new Page(pageNum, pageSize); LambdaQueryWrapperPet wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.isNotBlank(keyword), Pet::getName, keyword) .eq(category ! null, Pet::getCategory, category) .eq(status ! null, Pet::getStatus, status) .orderByDesc(Pet::getCreateTime); return petMapper.selectPage(page, wrapper); }宠物发布接口要处理封面图和多图上传的逻辑。图片我先存到服务器的一个固定目录比如/usr/local/upload/然后返回访问URL前端拿到URL后拼接到表单数据里一起提交。这里有个数据完整性的问题需要考虑如果用户上传了图片但没有最终提交表单这些图片会变成孤儿文件。我的处理方式是前端提交的时候先传图片拿到URL再提交表单数据如果表单提交失败前端主动调用删除图片的接口清理。3.4 领养审核流程的核心逻辑领养申请流程是这个项目里业务逻辑最复杂的部分。具体实现上分为两个角色视角用户端用户选中一只宠物进入详情页后点击“申请领养”弹出一个表单填写姓名、联系方式、居住地址、家庭环境、申请理由。提交后生成一条adoption_application记录status默认为0。同一个用户对同一只宠物只能有一条待审核状态的申请这个我上面提到了用后端查询做校验。管理端管理员在后台看到待审核列表点进详情查看用户的申请资料选择通过或拒绝。如果拒绝必须填写拒绝原因用户端会看到这条申请的状态变为“已拒绝”并展示原因。如果通过这条申请的status变为1用户端宠物详情页对当前用户显示“申请已通过请留意电话通知”管理员可以进一步把状态改为“已完成”完成整个闭环。这里有一处容易忽略的并发问题同一只宠物可能同时被多个用户申请而宠物只能被领养一次。所以管理员在将申请状态改为“已完成”时需要同时给出一个事务性的操作更新申请表状态的同时把pet表的status从1改为3已领养。这个操作必须要加Transactional注解保证两条SQL要么都成功要么都失败否则会出现申请状态显示已完成但宠物还在列表上出售的脏数据。Transactional public void completeAdoption(Long applicationId) { AdoptionApplication application applicationMapper.selectById(applicationId); Pet pet petMapper.selectById(application.getPetId()); application.setStatus(3); applicationMapper.updateById(application); pet.setStatus(3); petMapper.updateById(pet); }3.5 图片上传实际上手与路径处理图片上传看起来简单但牵扯到的路径问题相当容易出幺蛾子。我的方案是本地磁盘存储 后端映射虚拟路径 部署时用Nginx转发这个组合在当前绝大多数小型项目里都够用。首先在配置里定义一个自定义属性upload.path指向服务器的一个磁盘目录。然后配置一个WebMvc的静态资源映射把/files/**这个URL路径映射到该磁盘目录。Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/files/**) .addResourceLocations(file: uploadPath /); }这样前端访问http://localhost:8080/files/xxx.jpg就能直接看到图片。上传接口用MultipartFile接收文件生成文件名的时候我建议用UUID 原始文件后缀名避免文件名冲突也能防止中文路径问题。String originalFilename file.getOriginalFilename(); String ext StringUtils.substringAfterLast(originalFilename, .); String filename UUID.randomUUID().toString().replace(-, ) . ext;另外必须对上传文件做类型白名单校验。后端校验后缀名还不够图片被改后缀名为.jpg也能上传建议通过file.getContentType()判断MIME类型同时限制文件大小。我的经验是做毕设的系统虽然不用像企业系统那样把安全等级拉满但至少要在代码里体现出这个意识——热词里有一条“springboot项目全局过滤器处理上传pdf文件时xss攻击”说明很多人关心上传场景的安全校验这绝对是个加分项。如果本地没有部署Nginx的条件也可以直接把虚拟路径指到Nginx的静态资源目录。总之不要让前端通过相对路径拼接图片地址时出现404就算了图片能访问只是第一步目录的读写权限和路径规范也要一并处理好。4. 前端页面与交互实现4.1 Vue项目初始化与核心配置前端我用Vue CLI创建项目执行vue create pet-adoption-web然后选择Vue 2的默认配置项。这里插一句网上很多教程推荐用Vite来创建Vue项目Vite确实很快但Vite Vue 2的兼容性配置相对繁琐用Vue CLI图个省心等熟练之后想换Vite随时可以换。创建完成后的第一件事是安装Element UI和Axiosnpm install element-ui -S npm install axios -S然后在main.js里做全局注册import Vue from vue import ElementUI from element-ui import element-ui/lib/theme-chalk/index.css import App from ./App.vue import router from ./router Vue.use(ElementUI) Vue.config.productionTip false new Vue({ router, render: h h(App) }).$mount(#app)路由配置建议按模块拆分。核心路由我分了三个模块用户端首页和宠物列表游客可访问、用户中心需要登录、管理后台需要管理员权限。Vue Router的路由守卫beforeEach在这里派上用场router.beforeEach((to, from, next) { const token localStorage.getItem(token) const role localStorage.getItem(role) if (to.meta.requiresAuth !token) { next(/login) } else if (to.meta.requiresAdmin role ! 1) { next(/403) } else { next() } })4.2 Axios封装统一处理请求与响应Axios如果直接在组件里到处import axios然后写上完整URL开发到后面一定会变得很难维护。我的做法是在src/utils/request.js里做一次统一封装。封装的核心包含四个方面第一请求头统一携带token。请求拦截器里从localStorage取出token设置为Authorization头。第二响应统一处理。后端返回的数据格式约定为{ code: 200, message: success, data: ... }响应拦截器里判断code是否为200如果不是就组件上抛出错误提示。第三统一错误处理。401状态码表示token过期或未登录直接清除本地存储并跳转登录页其他状态码用Element UI的Message提示后端返回的message。第四基础URL统一下沉。开发环境用/api前缀配合Vue CLI的devServer代理转发到后端8080端口解决开发联调时的跨域问题。import axios from axios import { Message } from element-ui import router from /router const service axios.create({ baseURL: process.env.VUE_APP_BASE_API || /api, timeout: 10000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] token } return config }) service.interceptors.response.use( response { const res response.data if (res.code ! 200) { Message.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res }, error { if (error.response error.response.status 401) { localStorage.clear() router.push(/login) } Message.error(error.message || 网络错误) return Promise.reject(error) } ) export default service这样的封装用完一次就会上瘾所有页面的请求都变成request.get(/api/pet/list, { params })这样的形式页面代码非常干净。4.3 宠物首页与列表页开发首页是整个项目最先呈现在访客面前的页面视觉上一定要说得过去。我的首页布局是顶部导航栏 宠物卡片瀑布流 底部公告区。宠物卡片用Element UI的el-card组件图片区域用el-image做懒加载内容显示宠物名字、品种、年龄、健康状态等核心信息点击卡片跳转详情页。宠物列表页是核心页面功能上要支持顶部分类筛选全部/猫/狗/其他、关键字搜索、排序最新发布/人气最高底部用el-pagination组件做分页。这些交互本质上都是先把筛选条件同步到路由的query参数上然后调用后端接口刷新列表数据。这里有一个交互上的经验点分类筛选不要用新的路由页面而是通过更新query参数来切换比如/pet/list?categorycatpage1。这样的好处是用户刷新页面后筛选条件不会丢失而且可以直接通过URL分享当前的筛选结果体验感会好很多。宠物详情页要注意的信息比较多宠物基础信息、详细描述、多图预览、领养按钮、收藏按钮。我在详情页做了一个判断逻辑如果当前用户已经申请过这只宠物按钮显示为“已申请”点击无效如果宠物状态不是“已上架”按钮置灰显示对应状态。这些前置的用户体验设计减少了大量不合法的后端请求也让系统看起来更完整。4.4 管理后台的实现思路管理后台是Vue项目的另一个重要模块布局上用经典的左侧菜单 顶部栏 右侧内容区。我用嵌套路由来实现父路由是/admin子路由包括/admin/pet宠物管理、/admin/application领养审核、/admin/notice公告管理、/admin/user用户管理。宠物管理页面用el-table展示数据行数据包含宠物缩略图、名字、分类、状态、发布时间、操作按钮编辑/上下架/删除。缩略图直接渲染图片的URL如果图片挂了用error事件替换为默认占位图。领养审核页面这里是整个后台交互最密集的地方。我设计了两个Tab待处理和已处理。待处理Tab的每一行都有一个“审核”按钮点击弹出对话框展示完整的申请资料管理员在对话框里选择通过或填写拒绝原因后提交。已处理Tab展示的是历史的审核记录支持按审核结果筛选。用户管理页面比较简单列表展示注册用户的基本信息和注册时间操作上提供禁用/启用按钮。禁用用户时后端逻辑不仅要改用户表的状态还要处理该用户已提交但还在审核中的数据——具体怎么处理看业务需求我这边是保留数据但用户无法登录等解禁后继续走流程。5. 前后端联调、打包与部署5.1 开发环境跨域的两种常规解法前后端分离开发时前端页面跑在localhost:8081Vue Cli默认端口后端接口跑在localhost:8080两个端口不同浏览器会直接产生跨域问题。解决方式有两种第一种是后端加CORS全局配置。SpringBoot里可以用CrossOrigin注解加在Controller上也可以写一个全局的CorsFilter。我建议写全局配置一次性生效Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }第二种是前端做代理。在vue.config.js里配置devServer的proxy把/api开头的请求代理到后端地址module.exports { devServer: { port: 8081, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } }这两种方式我都用过的体会是开发阶段用前端代理更方便因为后端接口路径不需要做任何适配但如果后端有人直接访问Swagger测试接口会增加一些跨域限制的沟通成本。两者可以同时配置并不冲突。需要注意的一点是如果配置了CORSallowedOriginPatterns(*)和allowCredentials(true)同时用时不能写成allowedOrigins(*)否则浏览器会拒绝带cookies的请求。5.2 前端打包与Nginx配置实践开发完成之后就是构建部署阶段。前端先执行npm run build产物生成在dist目录。这个目录里就是一堆静态资源index.html、css/js文件、图片等。把这些文件放到Nginx的html根目录下再配一个反向代理把/api请求转发到后端jar包的8080端口就完成了整套部署。这里分享一份我实际用的Nginx配置里面加了两个容易被忽略的细节一个是SPA路由的history模式fallback另一个是gzip压缩server { listen 80; server_name localhost; gzip on; gzip_min_length 1k; gzip_types text/plain text/css application/javascript application/json image/svgxml; root /usr/share/nginx/html; 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; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }第一个细节很关键。Vue Router如果使用history模式前端跳转路由时URL看起来是/pet/detail/1这样的真实路径但刷新这个页面时Nginx会先去磁盘上找有没有对应该路径的文件找不到就会返回404。try_files $uri $uri/ /index.html;这句会让所有找不到文件路径的请求都回退到index.html由前端路由接管刷新就不会白屏了。第二个细节是gzip这个配了之后静态资源传输体积能压缩60%以上页面加载速度快一截。虽然说配置很简单但它属于那种“没配也不会出错、配了体验明显提升”的点有条件就顺手打开。5.3 后端打包与启动后端部署我用的是Maven打包成可执行jar包。打包前需要注意application.yml里配置的数据库地址、上传路径等要根据生产环境做调整。我建议通过Spring Boot的多环境配置机制来实现分别建application-dev.yml和application-prod.yml启动时用--spring.profiles.activeprod指定环境避免改代码和重新打包。打包命令很简单mvn clean package -DskipTests打完包在target目录下会生成一个xxx.jar文件放到服务器上执行nohup java -jar pet-adoption.jar --spring.profiles.activeprod app.log 21 启动日志输出到app.log文件里方便排查问题。如果服务器内存有限可以加-Xms256m -Xmx512m限制JVM内存占用这是我在2G小内存服务器上跑多个服务时学到的经验。6. 实际开发中遇到的高频问题与排查实录6.1 MyBatis-Plus分页插件不起作用这个坑我估计十个用MyBatis-Plus做毕设的人有八个会踩。明明配置了PaginationInnerInterceptor但调用selectPage返回的数据不带total字段或者分页SQL根本没有LIMIT语句。排查思路是这样的先确认PaginationInnerInterceptor的Bean是否真的被Spring容器加载了。一个很经典的错误是把MybatisPlusInterceptor类给弄错了注意在较新版本中它是在com.baomidou.mybatisplus.extension.plugins.inner包下面不要引成旧版的PaginationInterceptor旧版只兼容MyBatis-Plus 3.4.x以下。还有一种情况是配置了Bean但项目里同时存在多个MyBatis-Plus配置类导致拦截器链被覆盖。检查有没有多余的MybatisConfiguration或者MybatisSqlSessionFactoryBean配置有的话把分页插件也加到那个自定义配置里。6.2 图片上传后通过URL访问404上传接口返回成功但前端拿到URL后图片却显示404这个问题多半是静态资源映射没生效。排查步骤先试着手动拼接URL在浏览器直接访问如果报404说明addResourceHandlers没有生效。常见原因有两个。一个是addResourceHandlers方法所在的配置类没有被Spring扫描到导致注册没有生效。另一个是Linux服务器的磁盘路径权限问题上传目录如果不存在Spring也不会帮你自动创建必须在启动前手动mkdir -p创建目录或者用Java代码在启动时自动创建。还有一个容易被忽略的点Windows和Linux的路径分隔符不一样。上传路径如果写死了D:/upload/这种Windows风格路径部署到Linux上就会出问题。我建议用配置项外置同时代码里统一用Paths.get()来拼接这样兼容性最好。6.3 前端联调时CORS报错但CORS配置已经写了明明在SpringBoot里配置了CorsRegistry前端请求还是报跨域错误。原因可能是请求通过代理方式转发虽然前端配置了proxy但Nginx或Vue devServer转发时没有正确处理。也可能是拦截器直接拦了OPTIONS预检请求——浏览器在做跨域POST请求之前会先发一个OPTIONS请求探路如果你的SpringBoot拦截器把OPTIONS请求也当作正常请求拦截并要求带token预检请求返回401浏览器就会认为跨域失败。解决办法是在拦截器的排除路径里把OPTIONS请求放行或者直接判断if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; }这个坑排查的时候特别容易忽略因为后端CRUD接口的日志里完全没有OPTIONS请求的记录实际上它被拦截器在入口就处理掉了。6.4 Vue项目npm install各种报错这些年Vue项目装依赖的报错基本集中在两个方向网络问题和版本兼容。网络方面npm默认源在国外国内访问经常超时直接换成淘宝镜像npm config set registry https://registry.npmmirror.com版本兼容方面最常见的场景是Node.js版本太新导致一些旧包编译失败。某个版本的node-sass如果和当前Node版本不匹配就会报Module build failed这类错误。处理方式有两种换成对应的node-sass版本或者干脆换成sassdart-sasssass兼容性更好推荐直接放弃node-sass换用sass包。另外如果项目中配置的依赖版本和npm解析出来的版本有冲突导致node_modules目录出现各种奇怪问题最简单的处理方式是删掉锁文件和依赖目录重新装一遍rm -rf node_modules package-lock.json npm install --registryhttps://registry.npmmirror.com这个操作能解决绝大部分“莫名其妙”的依赖问题。6.5 常见问题与解决方案速查表问题现象可能原因解决方案后端启动时报Access denied for user数据库账号密码不对或权限不足检查application.yml中的账号密码和MySQL授权查询结果的时间字段差了8小时时区配置不一致JVM、MySQL、Jackson三层都要统一设置为Asia/Shanghai前端提交表单后请求报400后端实体类字段和前端字段名不一致核对JSON字段名开启map-underscore-to-camel-case映射修改密码后旧token还能访问JWT过期时间太长或无状态校验业务上处理用户修改密码后强制清除前端tokenVue页面加载后白屏路由或组件路径写错打开控制台查看报错检查import路径大小写部署到服务器后图片无法上传磁盘写入权限不足给上传目录设置chmod 777或单独创建账号授权这个表是开发过程中比较典型的几类问题整理出来放进论文的测试章节里也会比较好看明显是属于“真实开发过程中遇到的坑”而不是空话套话。个人实操经验总结这个项目做完之后我最大的一个体会是这类平台类项目的难点从来不是某个单独的技术点而是所有技术点串在一起之后形成的那条完整的链路。从数据库表结构开始每一张表的关系、每一个状态字段的流转、每一条接口的权限控制、每一个前端页面的联动交互都像是一条流水线上的某个环节任何一个环节没有处理好最后联调的时候就会暴露出来。比如领养审核这个功能如果你只在后端写了两个接口提交申请、审核申请没有仔细设计状态流转没有考虑同一宠物不能重复领养的约束没有处理管理员审核通过后宠物状态同步更新的事务问题那你做出来的东西只能叫“能跑”不能叫“能用”。真正好用的系统是需要站在角色的角度去思考的——普通用户在意的是流程透不透明管理员在意的是操不操作繁琐而你需要做的就是通过合理的数据结构和业务逻辑让两边都觉得爽。另外想提醒一个容易被忽略的环节文档。如果你这个项目是要用于毕业设计答辩或者作品展示那么数据库设计说明、接口文档、测试记录这些配套材料尽量在开发过程中顺手整理不要等到全部开发完再补。我当时就是一边写代码一边用Apifox维护接口文档用Markdown记录表结构和状态流转逻辑最后写论文的时候基本上是把这些材料整理重组一遍节省了非常多时间。还有一个小建议给准备做类似项目的人不要一开始就追求功能多先把核心业务链路跑通——用户注册登录、宠物发布、列表展示、提交申请、管理员审核、状态流转这条线稳定了再往上面加收藏、公告、统计之类的点缀功能。核心链路就像房子的承重墙承重墙立住了后面再怎么装修都安心。反过来功能堆了一大堆核心链路却三天两头出bug那整个项目给人的感觉就是花架子。如果你正打算用这个题目作为你的毕设或者练手项目这套方案完全可以作为起步参考。按我自己预估如果你每天能保证三到四个小时的编码时间从零基础到把核心链路开发完大概需要三到四周。中间遇到卡壳的时候先静下心把日志看明白再定位到具体模块去排查绝大多数问题都能自己解决。
返回列表