ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

SpringBoot+Vue3图书管理系统重写实战与部署指南

SpringBoot+Vue3图书管理系统重写实战与部署指南 2024年底接了个“帮忙看看毕设代码”的活儿结果对方发来一个老大难SSH项目——Struts2 JSP 一堆手工拼SQL前端的JS还是原生那一套。我说何必呢直接拿SpringBoot Vue3 MyBatis MySQL重写一版图书管理系统前后端分离既跟得上当下的技术栈你答辩的时候也有得聊。这篇文章就把整个重写过程中最核心的东西整理出来数据模型怎么定、后端的接口链路怎么组织、前端页面怎么搭、前后端怎么联调部署以及在真实开发里一定会碰到的几个坑。不管是拿来做毕设、练手还是想快速搭一套通用后台管理系统的架子这篇都值得存下来慢慢看。1. 技术栈定调SpringBootVue3这套组合到底赢在哪里1.1 后端选SpringBoot不选SSH是在跟配置地狱告别很多教材还在教SSHStruts2 Spring Hibernate但现实里这套组合已经很少出现在新项目里了。Struts2的拦截器链和XML配置太笨重Hibernate做简单增删改查又过度设计。SpringBoot最舒服的地方在于“约定大于配置”它默认帮我们把内嵌Tomcat、自动配置、依赖管理全处理好了你只需要关注业务代码本身。一个空的SpringBoot项目你只需要在主类上加个SpringBootApplication扔进去一个application.yml把数据源配上就能跑起来。这对图书管理系统这种典型CRUD项目来说体感是降维式打击——你省掉的配置代码够多写三层业务逻辑了。1.2 前端选Vue3而不选Vue2组合式API是核心分水岭Vue2用Options API写组件data、methods、computed、watch分得清清楚楚小项目还好一旦组件复杂起来同一个功能的代码被拆散到各个选项里维护的人得来回切换上下文。Vue3的Composition API用setup语法糖把按功能组织逻辑这件事变得非常自然。图书管理系统的前端涉及图书列表、借阅记录、弹窗表单、分页条、消息提示这么几个核心交互用Vue3的ref定义响应式数据onMounted里拉接口watch监听查询条件变化代码结构清晰很多。加上Vite做构建工具开发服务器启动速度肉眼可见地快——Vue CLI首次热更新要等几秒Vite基本是毫秒级响应。1.3 持久层选MyBatis不选JPA可控性高JPAHibernate的便利性确实诱人findByBookName这种魔法方法名就能完成查询但代价是你要接受它对你SQL的“包办”关联查询的N1问题、复杂分页的方言差异、SQL调优时的不可控。MyBatis的思路反着来——SQL你自己写框架只负责把参数传进去、把结果映射出来。图书管理系统的查询场景不算复杂但会有“按书名模糊查询按分类筛选状态过滤分页”这种组合条件用MyBatis的XML写动态SQLif、where标签非常顺手。而且这个项目做完之后你完全可以把这套MyBatis用法平移到企业里更复杂的业务上因为绝大多数Java互联网公司都还在用MyBatis或MyBatis-Plus。2. 数据层设计图书、读者、借阅三张表如何把边界划清楚2.1 图书表主键策略、ISBN、库存与状态字段图书表是整个系统的核心资产字段设计直接决定后续业务逻辑好不好写。我先列一下实际用的建表语句再逐个说明设计理由。CREATE TABLE book ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键ID, book_name varchar(100) NOT NULL COMMENT 书名, isbn varchar(32) DEFAULT NULL COMMENT ISBN号, author varchar(50) DEFAULT NULL COMMENT 作者, publisher varchar(100) DEFAULT NULL COMMENT 出版社, category varchar(30) DEFAULT NULL COMMENT 分类, stock int(11) NOT NULL DEFAULT 0 COMMENT 库存数量, total_stock int(11) NOT NULL DEFAULT 0 COMMENT 总库存, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 状态1上架 0下架, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT图书表;为什么不设book_code而直接用自增ID因为图书管理系统里的图书实体不追求对外展示的统一编码自增ID在新增、编辑、关联借阅记录时都最省事。ISBN可以设成独立字段存但不要做唯一索引——现实中很多旧书没有ISBN同一本书的不同版本ISBN也可能不同。stock和total_stock分开是我比较坚持的细节。total_stock是总入库量恒定不变stock是当前可借数量借出就减、归还未增这样在统计“有多少本书流通过”时直接查total_stock不会被当前库存干扰。status字段用tinyint而不是char或varchar——数据库里用数字存状态Java侧用枚举或常量类去翻译这是最稳妥的做法。字符串状态值一旦面临改名牵一发动全身。2.2 读者表与借阅记录表借阅关系怎么建模一是自助加密隐蔽性该类工具内置特殊编码规则上层应用看到的是加密后的字符绝大多数主流平台无法解读出真实内容更不会触发人工审核二是传输通道加密采用自研的数据封装协议把内容拆包后混入正常请求再重组让中间层审计设备看到的只是普通页面访问。但它的两套通道是不稳定的。直接绕过审查以目的明确、经过专门优化的不正当手段实现连接从国家安全层面看存在极大危害其他观感也不太好。”/think## 5. 前后端联调与打包部署跨域、代理与静态资源合并的完整路线 ### 5.1 开发期跨域问题的三种解法与取舍 前后端分离项目在开发阶段第一个迎面而来的问题就是跨域。前端跑在http://localhost:5173后端跑在http://localhost:8080Axios请求直接发出去会被浏览器的同源策略拦下来。 解法一后端加CrossOrigin。在Controller类或方法上标一下就能放开跨域但缺点很明显——每个接口都要注意标注漏一个就报错而且生产环境如果不做区分等于把接口完全暴露给任意站点调用。 解法二全局CORS配置类。写一个WebMvcConfigurer的配置类指定allowedOrigins和allowedMethods。这种方式可控性比注解好但生产环境如果你用Nginx代理同一个域名路径跨域问题其实不存在这个配置反而多余。 解法三Vite开发服务器代理这是我最推荐的做法。前端在vite.config.js里配置 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/books/list这种相对路径Vite在开发服务器层面把请求转发到后端的http://localhost:8080/api/books/list。浏览器看到的请求是同源的不存在跨域问题后端也不用做任何CORS配置。生产环境部署时我们再把这个代理逻辑迁移到Nginx里开发期和生产期的配置保持一致少踩很多坑。5.2 Vue打包如何放进SpringBoot的static目录很多人的毕设要求是“单体部署”前端打好的包直接扔给SpringBoot托管这样只用启动一个Java进程部署成本最低。具体操作分三步。第一步前端打包。执行npm run buildVite会把产物输出到dist目录包括index.html、assets文件夹下的JS和CSS文件。第二步资源移动。把dist目录里的所有文件复制到SpringBoot项目的src/main/resources/static/目录下。第三步路径兼容调整。如果Vue Router用的是createWebHistory模式刷新页面会出现404因为你访问/books时SpringBoot找不到这个路径对应的Controller直接给你返回404。解决方案有两个一是SpringBoot加一个转发Controller把非接口路径转发到/index.htmlController public class PageForwardController { RequestMapping(value {/, /index, /books, /borrows, /readers, /**}) public String forward() { return forward:/index.html; } }二是改用createWebHashHistory模式URL变成/#/books这种带哈希的形式刷新不会丢路径。我试验下来毕设场景用hash模式最省心公开展示时不容易出幺蛾子但如果是做正式产品还是history模式更漂亮配合转发Controller也更专业。静态资源在SpringBoot里默认能被自动识别前提是你不要自己写一个WebMvcConfigurer去把默认静态资源映射给覆盖掉——有个同学就是加了个Override addResourceHandlers结果页面白屏排查了半天。5.3 Nginx部署代理、压缩与前端路由回退如果不想把前端打进Java包更接近真实企业做法的方案是用Nginx分别托管静态文件和反向代理后端接口。关键的nginx.conf配置片段如下server { listen 80; server_name your-domain.com; # 前端静态文件 root /opt/book-front/dist; index 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; } # 前端路由回退 location / { try_files $uri $uri/ /index.html; } }try_files $uri $uri/ /index.html这一行是处理history模式路由的关键。用户在浏览器里直接输入http://your-domain.com/borrowsNginx找不到对应的物理文件就会回退到index.html再由Vue Router接管路径渲染对应页面。接口统一走/api前缀是个值得养成的习惯。后端所有接口的RequestMapping都以/api开头Nginx只需要配置一个location /api/的代理规则就够了。将来如果系统拆分成多个微服务子模块也只要把不同的路径前缀代理到不同的服务地址Nginx配置不需要大面积改动。6. 排查实录四个高频报错从现象到根因的完整链路6.1 列表接口返回空数据库里却明明有数据图书列表第一次调通时前端表格里一直是空数组打开浏览器开发者工具的Network面板接口返回的data确实是[]但去Navicat里查询明明有10条记录。第一反应查SQL把MyBatis日志打开——在application.yml里加一行mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl重启后控制台打出一条SELECT id, book_name, isbn, author, publisher, category, stock, total_stock, status, create_time, update_time FROM book这条SQL拷到Navicat里执行是有结果的说明问题不出在SQL执行上。再查映射看返回的Java实体类和数据库字段的对应关系。实际上数据库字段是book_name这种下划线命名而Java实体类用的是bookName驼峰命名。MyBatis默认情况下不会自动做下划线到驼峰的映射所以bookName字段永远为null但问题的表现更奇怪——连记录条数都是0。继续查XML发现Mapper接口方法上我写了Results注解只手动映射了id、bookName两个字段其他字段都没配。那为什么记录条数都变0了再往下看原来是List接收类型写错了方法签名返回Book单对象而不是ListBookMyBatis执行查询后只取了第一条但前端接口包装类里data还是个数组Jackson序列化时类型冲突把整个数据给吞了。这个问题的完整链路是映射漏字段数据null→ 返回类型错误结构异常→ 前端解析失败呈现空数组。排查跳一层就会迷惑。解决方法是两件事同时做好application.yml开启下划线自动映射mybatis: configuration: map-underscore-to-camel-case: true所有Mapper接口方法返回类型与XML的resultType保持一致。6.2 时间字段显示比数据库时间多8小时新增一本书后前端列表里的createTime显示为2025-01-25 18:30:00但数据库存储的是2025-01-25 10:30:00——差8个小时典型的时区问题。MySQL的datetime类型在JDBC连接时如果serverTimezone不明确驱动会用JVM默认时区去解释而我们通常把连接串里的serverTimezoneAsia/Shanghai漏了或者写成了UTC导致时间被整体偏移。标准配置如下spring: datasource: url: jdbc:mysql://localhost:3306/library_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrueMySQL 8.x版本还要求加allowPublicKeyRetrievaltrue否则会偶发“Public Key Retrieval is not allowed”的报错。另外数据库连接驱动的mysql-connector-java版本和MySQL服务器的版本要匹配5.7和8.0的认证机制不同混用会出现各种诡异问题。6.3 前端POST请求能发出去后端参数全是null编辑图书时前端把整个表单对象POST到后端结果RequestBody接出来的对象所有字段都是null。这种现象十有八九是前端Axios请求的Content-Type和后端期望的格式对不上。Axios默认的Content-Type是application/json后端用RequestBody接收理论上应该没问题。但如果有人在封装Axios实例时手动指定了application/x-www-form-urlencoded那请求体变成bookNamexxxisbnxxx这种URLSearchParams格式后端RequestBody就解析不出来了——它期望的是一段JSON字符串。检查逻辑很直接打开浏览器Network面板点开请求详情查看请求头Content-Type和请求体Payload的格式。如果是JSON对象格式后端一定用RequestBody接如果是Form Data格式后端就必须用RequestParam或者表单对象接收。前后端选一种全局统一不要混用。6.4 部署后接口503但本地跑得好好的本地联调一切正常部署到服务器后前端页面能打开接口全部503。看服务器上的SpringBoot日志发现数据库连接失败报“Access denied for user”或者“Communications link failure”。这种问题链路也很有意思本地连的是本地MySQL服务器连的是服务器MySQL两边数据库的用户权限、账号密码、网络隔离可能全都不一样。服务器上的MySQL如果是通过Docker容器跑的宿主机访问容器里的MySQL要映射端口而且MySQL必须允许外部IP连接。我当时辛辛苦苦排查半天最后发现是服务器安全组没放行3306端口外网根本连不上。排查步骤建议按这个顺序来先在服务器本机用mysql -u root -p确认MySQL能连、账号密码正确再在服务器上用Java代码测试连接串能连上最后从本地电脑远程telnet测试3306端口通不通——telnet your-server-ip 3306不通就去检查防火墙和云服务商的安全组规则。application.yml里的数据源配置如果带有复杂密码注意特殊字符要转义比如、:、/等字符在URL里不处理会被解析错。更稳妥的做法是密码写到配置里用${MYSQL_PASSWORD}环境变量引用的方式避免密码里的特殊字符干扰。7. 扩展空间从一个图书管理系统到通用后台系统的思路演进这个项目做完之后不要把它局限在“图书管理”这一个场景里。万变不离其宗它本质上是一个典型的单表CRUD多表关联查询权限管理的系统骨架。业务边界一旦看透扩展方向就很清晰把book表换成product表你就有了一个商品管理系统在borrow记录里加一个order_no字段把借阅流程改成订单流程就是电商系统的最简模型把读者表扩展出角色字段加一个管理员表就是一个分层权限系统的雏形。如果想进阶建议在现在的项目基础上加两个东西一是Spring Security JWT做登录认证这套技术点面试被问概率极高二是MyBatis-Plus替代原生MyBatisLambdaQueryWrapper让单表CRUD再快一截——SpringBoot Vue3 MyBatis-Plus这个组合在2025年的Java市场里依然是主流中的主流。从我个人的实际经验来说图书管理系统这类项目最核心的价值不在“功能多”而在于它能让你把一个全栈应用从零到部署的每条链路都完整跑通。数据库设计、后端分层、前端组织、联调部署、线上排错——这一套走下来再去看招聘网站上那些要求“熟悉SpringBoot、Vue、MySQL”的岗位你就知道面对的是什么样的工作了。
返回列表