拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SpringBoot+Vue前后端分离图书进销存系统实战:从数据库设计到部署上线

SpringBoot+Vue前后端分离图书进销存系统实战:从数据库设计到部署上线

这篇博客是整理我实际把图书进销存系统从零搭起来、跑通、部署上线的详细过程,包含完整源码和部署教程。如果你正在用SpringBoot+Vue做毕业设计、课设,或者想找一个真正能落地的前后端分离项目练手,这篇内容能帮你省掉很多查资料的时间。

先说这是个什么东西。图书进销存系统,本质上就是给书店或者图书仓库做的一套管理工具,管三件事:进货、卖货、库存。但这套系统我把它做成了前后端分离架构:后端用SpringBoot提供JSON接口,前端用Vue3独立部署,中间通过RESTful API通信,静态资源、跨域、接口鉴权都按生产环境的标准做了处理。

标题里写了SpringBoot+Vue+MyBatis+MySQL,这就是整套系统的全部核心。我接下会从需求拆解、表结构设计、后端接口实现、前端页面开发、部署上线这五个维度展开,每个环节都会给出代码和配置,把自己实际踩过的坑直接标出来。我的目标是让你拿着这篇文章,跟着操作就能把项目跑起来,而不是只给你看一堆理论概念。

1. 内容整体设计与思路拆解

1.1 为什么选图书进销存这个业务,而不是通用进销存

我一开始也纠结过,要不要做一个通用的进销存系统,但后来决定聚焦图书这一个垂直品类。理由很简单:通用进销存系统字段太抽象,什么"货品编号""SKU属性"听起来很全能,实际做页面的时候你根本不知道怎么给用户解释。图书不一样,它有天然的实体属性:书名、作者、ISBN、出版社、定价、库存量。每个字段用户都看得懂,数据模型的设计逻辑也更接近真实业务。

另一个原因是图书进销存有清晰的业务闭环:采购入库、库存管理、销售出库、退货处理、库存预警。这五个环节可以拆成模块化开发,正好对应前后端分离项目的接口设计思路。图书本身又有"多册同版"的特点,不像衣服有尺码颜色这种多维度SKU,处理起来复杂度适中,适合用来展示CRUD、分页、统计、条件查询这些核心功能。

如果是第一次做前后端分离项目,我建议也选一个垂直化的小场景,不要一上来就做"通用系统"。通用系统意味着你要设计抽象的数据模型和复杂的权限体系,对新手不友好,对老手又体现不出架构优势。图书进销存这种业务,表结构能控制在六张左右,接口数量在三十个左右,页面在八到十个,恰好是"能体现架构能力但不失控"的规模。

1.2 技术栈选型的取舍:为什么是SpringBoot+Vue+MyBatis+MySQL

这套技术栈组合看起来有点老生常谈,但每个组件都是经过实际验证后才定下来的。

后端为什么不用SSH而用SpringBoot?因为SpringBoot简化了配置,内嵌了Tomcat,用java -jar就能启动,不用部署WAR包到外部容器,这对前后端分离部署模式特别重要。你想一下,前后端分离后后端只是一个API服务,没必要再搞一个Tomcat去管静态资源,SpringBoot内嵌容器直接省了这个麻烦。

ORM层我选了MyBatis而不是MyBatis-Plus或者JPA。原因是我希望把重点放在业务实现和SQL优化上,这个项目里需要写大量关联查询和统计SQL,比如台账汇总、日销售统计、库存报警列表。MyBatis的XML文件能让SQL完全可控,不会被框架语法限制住。如果你用MyBatis-Plus写lambdaQuery确实很快,但遇到复杂的联表统计就得退回去写SQL,反而多学一套API。MyBatis的核心能力就是把SQL和代码解耦,它比JPA好在可读性强,比MyBatis-Plus好在功能纯粹,而且面试的时候MyBatis动态SQL几乎是必考的,用这个项目练手等于把面试实战提前做了一遍。

MySQL就不多说了,互联网行业最通用的开源关系型数据库。库表设计、索引调优、事务隔离,这些技能在任何公司都用得上。前后端分离本身和数据库没有直接关系,但你的后端接口最终要落到数据上,MySQL在这个体系里负责把数据稳定存住。

1.3 功能模块划分:从业务到接口再到页面的映射关系

我按照真实书店的作业流程来划分模块。一个门店经理每天干的活无非是看库存、进货、卖书、查账,对应到系统里就是四个核心业务模块和一个系统管理模块。

基础数据模块管图书信息和供应商信息,这是业务的地基。图书包括书名、ISBN、分类、作者、价格、库存上限下限,供应商包括名称、联系人、电话、地址。采购入库模块实现采购订单创建、入库单审核、自动更新库存。销售出库模块实现创建销售单、扣减库存、计算营业额。查询统计模块做库存报表、销售排行榜、过期图书预警。系统管理模块管用户登录、权限校验。

这些功能模块映射到后端接口就是一组RESTful API,映射到前端就是若干个路由页面。这里我可以明确说,前后端分离的核心就体现在这个映射关系上:页面组件通过axios调接口,接口返回JSON数据,页面再绑定渲染。数据格式统一用{code, message, data}这种包装结构,一个HTTP状态码加一个业务状态码,前端判断逻辑就非常统一,不会出现每个接口返回格式都不一样的情况。

1.4 前后端分离架构的关键设计点

前后端分离不只是一个说法,它在工程上意味着三件事。第一,后端不返回任何HTML页面,只返回JSON数据,所有ResponseBody都统一序列化。第二,前端所有路由跳转、页面渲染、组件交互都在浏览器端完成,后端不参与任何页面跳转。第三,静态资源部署和后端服务分开,可以通过Nginx做请求转发。

这套架构里有一个特别容易被忽略的点:跨域问题。前端跑在localhost:8081,后端跑在localhost:8080,端口不同就存在跨域。我在开发环境里采用的是后端配置CorsFilter的方式,在SpringBoot里注册一个全局CORS配置,允许前端地址访问。但到了生产环境,我让前端部署到Nginx,后端单独跑一个端口,然后通过Nginx的location配置把/api/开头的请求转发到后端服务,这样生产环境就没有跨域问题了。这个思路很关键,后面部署章节我会具体展开。

2. 数据库设计与核心表结构解析

2.1 数据表个数和设计原则

图书进销存系统一共设计了六张核心表:图书表、供应商表、采购入库单表、采购入库明细表、销售出库单表、销售出库明细表。另外加一张用户表用来存登录账号,一张操作日志表用来记录关键动作。

这个设计的核心原则是"主表加明细表"的模型。为什么不用一张大表把采购和销售的所有数据都塞进去?因为进销存场景里,一个采购单包含多个图书品种,每个品种有单独的单价和数量,如果都存一行,那主表就变成了无法维护的脏乱结构。这里必须拆成两层:主表存单号、供应商、日期、总金额、状态;明细表存每个图书的bookId、数量、单价、小计金额。这个结构的好处是,你想统计"今天进了多少货",直接查主表;想统计"某本书最近进了多少次",直接查明细表关联图书ID,不需要对字符串做任何拆分。

订单号我选择直接用时间戳加随机数生成,格式类似20250115143025001这种,时间戳精确到毫秒。你要注意,用户的真实业务单据编号通常有格式要求,但在这个项目里直接用日期加序列号就行,没必要搞一套复杂的编号规则,但必须保证唯一性和可排序性。

2.2 图书表的核心字段设计与索引策略

图书表是所有业务的中心节点,采购、销售、库存全部围绕它转。字段设计我做了这样的考量:book_id自增主键,book_name用varchar(100),isbn用varchar(20)并且建了唯一索引,author用varchar(50),publisher用varchar(100),category用varchar(50),price用decimal(10,2),stock用int,safety_stock用int。为什么不用float存价格?因为浮点数在计算总价时会存在精度误差,比如19.9乘以3可能得到59.69999999999999,用decimal(10,2)在MySQL层就保证了精度。

索引设计上,isbn建唯一索引,因为每一本正规出版的图书ISBN是唯一的的,这个索引可以加速查询并且防止重复录入。book_name建普通索引,因为图书查询的主要场景就是模糊搜书名。category不建索引,因为分类字段的区分度不够高,建了索引反而浪费空间。你要记住一个很简单的索引口诀:查询频率高、区分度高的字段才适合建索引,不要给每个字段都加索引。

库存字段stock和safety_stock用int类型。这里有一个重要细节:库存数量理论上范围不大,int完全够用,别用bigint。Bigint占8个字节,int占4个,对于百万级数据量差异可能还好,但mysql索引页存储的时候差别就出来了,索引占用空间更大,查询性能反而下降。

2.3 主表+明细表:采购和销售表如何做数据一致性

采购入库单表的设计核心字段包括purchase_id主键、purchase_no订单编号、supplier_id、total_amount总金额、status状态(0待审核、1已入库)、create_time。明细表字段包括purchase_detail_id、purchase_id、book_id、quantity、price、subtotal。

关键问题在于,采购单创建时怎么保证库存正确更新?我的方案是前端创建采购单时,同时提交主表信息和明细列表,后端在同一个事务里完成以下操作:插入主表数据,得到自增主键purchaseId,循环插入明细表数据,每插入一条明细就更新图书表库存和总金额。如果中途出现异常,整个事务回滚,主表和明细表都不会有残留数据。这件事必须在后端多表事务里做,千万不能在前端分两次请求调用两个接口。

销售出库逻辑完全对称:创建销售单,同时扣减库存。但是销售出库比入库多了一个校验步骤:下单时要检查库存是否充足,如果某本书的库存少于销售数量,整张销售单都要回滚,不允许部分成功。这个校验在MySQL里直接用UPDATE图书表SET stock = stock - ? WHERE book_id = ? AND stock >= ?来实现,这一条SQL天然具备原子性,比先查再更新安全得多。

2.4 初始化SQL脚本的准备和需要注意的坑

项目提供了初始化SQL脚本,包含建库、建表、插入初始数据三个部分。如果你在本地跑项目,直接在Navicat里执行脚本就行。但我必须提醒几个容易踩的坑。

第一坑,字符集问题。建库语句明确用DEFAULT CHARSET=utf8mb4,不要用utf8。utf8mb4是utf8的超集,能正常存储emoji和一些特殊字符,而且和MySQL 8.0默认字符集一致。第二坑,排序规则。如果MySQL 8.0环境,推荐utf8mb4_0900_ai_ci,如果是MySQL 5.7,就用utf8mb4_general_ci。写死一种排序规则容易导致后面导入数据时因为排序规则冲突报错。第三坑,外键约束。我在建表语句里没写物理外键,只建普通索引。为什么?物理外键会让数据插入时必须先查主表,在高并发写入场景会拖慢性能,而且删除数据时会被约束限制。项目里不用外键,靠事务和应用层逻辑保证数据一致性,这是实际项目里常见做法。

3. 后端接口实现与核心业务代码解析

3.1 SpringBoot项目结构搭建

后端项目用的是标准的Maven结构,包名我定义成com.bookstore。项目初始化时不用IDE的Spring Initializr也能搭,直接在pom.xml里引入依赖。核心依赖只有这么几个:spring-boot-starter-web提供Web能力,mybatis-spring-boot-starter整合MyBatis,mysql-connector-java驱动MySQL,lombok简化实体类代码。如果你不想用lombok,手动写getter setter也行,但lombok真的是节省大量样板代码的利器,建议直接上。

application.yml配置文件的重点是数据源和MyBatis配置。连接串一定要拼参数characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai。不写serverTimezone,MySQL 8.0驱动会默认UTC,你插入时间比实际时间少了八个小时,这种问题排查起来非常诡异。useSSL如果不关掉,本地开发时MySQL 8.0会经常报SSL连接错误,影响调试效率。mybatis.mapper-locations配置成classpath:mapper/*.xml,指定Mapper XML文件的位置。

这里还有一个容易被忽视的配置:spring.jackson.date-format和time-zone。后端返回的时间格式如果不对,前端显示会整个乱掉。我在配置里统一格式化日期为yyyy-MM-dd HH:mm:ss,并指定时区GMT+8。这个点看起来很小,但实际项目里前后端因为时间格式扯皮的情况特别常见。

3.2 RESTful接口设计规范

接口路径我按资源名来定义,统一使用名词复数形式,比如/books、/suppliers、/purchases、/sales。操作语义由HTTP方法区分,GET用来获取资源,POST用来创建资源,PUT用来更新资源,DELETE用来删除资源。这个设计规范好在哪里?它让接口名称具备自解释能力,前端看到GET /books就知道是查图书列表,看到POST /purchases就知道是创建采购单,不需要维护一堆action=xxx风格的接口文档。

图书管理接口包括:GET /books,分页查询图书列表,支持模糊搜索、按分类筛选;POST /books,新增图书;PUT /books/{id},更新图书信息;DELETE /books/{id},删除图书;GET /books/{id},获取详情。采购管理接口包括:POST /purchases,创建采购单,接收主表信息和明细列表;GET /purchases,分页查询采购单列表,支持按供应商查询;GET /purchases/{id},查询采购单详情,返回主表信息和明细列表;PUT /purchases/{id}/status,更新采购单状态。销售接口类似。

所有接口统一返回Result对象,这个对象有三个字段:code、message、data。code为200表示成功,500表示业务异常,401表示未登录。这样设计的好处让前端只用判断一个code是否等于200,不用处理各种Http状态码和业务状态码混在一起的情况。如果接口本身报404、405这类HTTP层错误,SpringBoot默认的异常处理会返回一个非200的Result,前端统一走error分支就行。

3.3 核心代码:采购入库的事务处理实现

采购入库是进销存里最能体现事务控制能力的模块。我写的Controller接收一个PurchaseDTO对象,这个DTO里既有Purchase主表数据,又有List 明细数据。Service层的实现需要加上@Transactional注解,确保一个事务开到底。

实际的代码逻辑是:先调用purchaseMapper.insertPurchase(purchase)获得自增主键,再把主键设置到每个明细对象的purchaseId里,然后循环调用purchaseMapper.insertPurchaseDetail(detail)。在循环内部,每次插入明细后就调用bookMapper.updateStock增加库存,并且累加totalAmount。等所有明细都插入完成,再以updatePurchaseTotalAmount方法把总金额写到主表。这整个流程中任何一个SQL执行失败,事务都会触发回滚,已经插入的明细数据和已经更新的库存都会被还原。

我实际测试过这样一种情况,如果前端传来的明细里有重复的bookId,传统写法会导致库存被加两次。所以Service层在循环前先做了校验,用Map对bookId做了去重,如果有重复的图书条目直接抛业务异常提示"同一本书不能重复出现在一个采购单中"。这个校验在真实业务场景中非常必要,因为前端表单用户完全可能把同一本书加了两行。

3.4 MyBatis动态SQL的实用写法

MyBatis的强大之处在于动态SQL,它可以根据传入的条件自动拼接查询语句。我分页查询图书时用了 标签和 标签组合,效果是:传入keyword就拼上keyword条件,传入category就拼上category条件,两个都没传就只执行"select * from book limit ? offset ?"的基础查询。

这是典型的动态SQL实现方式,和字符串拼接相比,它避免了SQL注入风险,因为参数都是通过PreparedStatement预编译绑定。这里我要特别强调一点:不要用${}做参数拼接,它会把参数直接拼进SQL语句,等于给SQL注入开了一扇门。""用#{}。这个细节面试时也经常被问到,但更重要的是实操中必须做对。

还有一个我常用到的小技巧,就是用标签处理更新语句,这样更新的字段可以动态配置,比如只改了价格就只更新价格字段,不会把其他字段全部覆盖为默认值,避免触发MySQL的"字段被更新为原值"这种无意义更新。

3.5 图书销售扣减库存的防超卖处理

销售模块最需要警惕的问题是超卖。所谓超卖,就是库存只剩两本,但系统同时卖出去三本。传统写法的第一步是select stock from book,第二步判断stock是否大于购买数量,第三步执行update。这种先查后更新的模式在并发情况下是有问题的:两个请求同时查到库存都是两本,都通过了判断,第三个请求就把库存扣到了负数。

正确的做法是把判断和更新合并成一条SQL,使用update book set stock = stock - #{quantity} where book_id = #{bookId} and stock >= #{quantity},我通过MyBatis的update方法返回int受影响行数来判断:如果返回值是1,说明扣减成功;如果返回值是0,说明库存不足或者条件不满足,直接抛异常。这个做法不需要显式加锁就可以避免超卖,而且性能开销最小。

当然,如果要求更强的数据一致性,可以使用SELECT ... FOR UPDATE给行加锁。但从实际场景看,书店管理系统并发量不高,使用条件更新的方式更简洁,性能也更好。我把这个决策作为一个补充说明写在代码注释里,方便学习的人理解不同粒度方案的选择依据。

4. 前端Vue实现与前后端联调

4.1 Vue项目初始化和依赖安装

前端用的是Vue3加上vue-router和axios。很多教程还在教Vue2的Options API写法,但我这个项目直接用Vue3的Composition API,因为它逻辑复用性更强,和TypeScript配合也好。组件库我选Element Plus,它提供的表格、表单、弹窗、分页组件刚好覆盖进销存后台管理系统的全部场景,不需要自己手写这些复杂的交互组件。

安装依赖时最容易出问题的是npm源。默认npm源在海外,下载速度慢得离谱,甚至直接卡死。我先执行npm config set registry https://registry.npmmirror.com切换成淘宝镜像源,再执行npm install下载依赖,速度会快上好几倍。如果你安装过程中遇到node-sass相关的报错,那是因为node-sass对Node版本要求严格,我建议在项目里不要使用node-sass,直接用sass或dart-sass替代,兼容性更好。

4.2 前端目录结构和页面路由设计

前端页面按模块组织,views目录下每个子目录对应一个业务模块。book目录放图书列表和图书编辑页面,purchase目录放采购入库单列表和采购单创建页面,sale目录放销售单相关页面,supplier目录放供应商管理页面,dashboard目录放置首页统计看板。

路由表设计的时候做了动态路由的拓展空间。先把登录路由和首页路由设置为公开路由,其余业务路由放在根路由的children数组里,每个路由配置component和meta信息,meta里存页面标题和所需角色。我预留了动态路由的钩子函数,后续如果需要根据用户角色动态注入路由表,可以直接在这个文件里扩展。

前端路由模式我选的是History模式而不是Hash模式。Hash模式路径里会有个#号,看起来不够专业,而且SEO层面不友好。History模式需要后端配置404回退到index.html,否则用户刷新页面时nginx会404,这个配置我放到部署章节具体讲。

4.3 axios封装和请求拦截器

前端所有HTTP请求统一走axios实例,我这里做了一个简单的封装。创建axios实例的时候设置baseURL为/api,在开发环境通过Vite的proxy将/api请求代理到http://localhost:8080,这样开发时就不会遇到跨域问题。为什么不在axios里直接写http://localhost:8080?因为到了生产环境,接口地址是Nginx端口而不是后端端口,配置不同域名,写死地址就完全无法切换。

请求拦截器负责在每次请求头加入Token。response拦截器做两件事:第一,如果响应状态码是401,说明Token失效,直接清空登录状态并跳转登录页;第二,如果业务code不是200,弹出错误提示。这样前端在业务代码里就不用重复处理错误分支了,每个页面调用接口时只需要在then里写成功逻辑,代码会清爽很多。

4.4 图书管理页面的增删改查实现

图书管理页面是这个系统最典型的CRUD页面,它的实现逻辑最能体现前端工程化的组织方式。页面进入时调用getBookList方法调用接口获取图书分页数据,然后绑定到tableData数组,el-table利用这个数组自动渲染。搜索区域放了一个书名输入框和分类选择框,点击搜索按钮时把两个条件拼成一个params对象传给后端接口,再重新加载第一页。

新增和编辑共用同一个对话框组件。区别在于,新增时先在data里resetForm,编辑时先调用getBookDetail接口回显数据,再弹出对话框。提交时判断当前对话框状态,如果是新增就调POST接口,如果是编辑就调PUT接口,这种复用方式可以大量减少模板代码。这个方案实现起来会有点绕,但可以在原来的基础上不断优化。

删除操作我加了确认删除弹窗提示,避免用户误点。同时后端做了保护性处理,如果图书已经有采购单或销售单关联,会拒绝删除并返回提示:"该图书存在历史单据,不能删除"。这个保护逻辑前端无法完全覆盖,必须后端兜住,这也是前后端分离项目里双端校验的一个典型场景。

4.5 采购入库页面里的嵌套表格交互

采购入库页面比较复杂,需要选择供应商、选择图书、填写数量单价,然后动态生成明细列表。前端设计思路是把页面分成上下两个区域:上半部分是主表信息表单,包含供应商选择、采购日期、备注;下半部分是一个明细表格,用户点击添加按钮就弹出图书选择对话框,选中图书后自动填入书名、定价等信息,用户只需要手工填数量和成交单价。

这个功能完全依赖Vue3的reactive数组管理明细行数据。当用户添加明细时push一条新的临时对象,当用户删除某一行时用splice移除,提交时把整个对象传给后端,由后端在同一个事务里处理主表明细和库存更新。我在提交按钮的loading状态里做了一个细微的处理:请求发出后按钮禁用并显示加载状态,防止用户重复点击造成重复提交。这个交互细节在实际项目中很容易暴露出来,但很多初学项目完全没有考虑,属于"看着小、实际影响大"的坑。

4.6 CORS跨域配置和联调注意事项

开发环境跨域问题我用Vite的proxy解决,具体配置是在vite.config.js里设置server.proxy,把/api开头的请求代理到http://localhost:8080。这样axios的baseURL就统一用/api作为前缀,无论开发还是生产,代码路径都不用改变。

但如果跳过Vite代理,直接在localhost:8081访问后端,就需要在后端配置CorsFilter了。我写了这样一个配置类:允许所有来源,允许所有请求头,允许GET、POST、PUT、DELETE、OPTIONS方法,设置预检请求的缓存时间。这里的细节是必须显式允许OPTIONS请求,因为浏览器跨域请求会先发一个预检请求,如果没有正确处理,浏览器会拦截真实请求。

联调阶段最容易碰到的接口问题是字段命名不一致。后端实体类用的是驼峰命名比如bookName,前端请求参数也用bookName,这个统一了就不会出问题。但MyBatis默认开启驼峰转换后,数据库字段book_name会自动映射到实体类的bookName属性,不需要额外配置。如果遇到了前端拿到的字段是undefined,先检查后端JSON序列化字段名和前端参数名是否一致,再检查MyBatis配置有没有开启mapUnderscoreToCamelCase。

5. 项目部署教程:从本地打包到服务器上线

5.1 后端JAR包打包流程

后端项目使用Maven打包。在项目根目录执行mvn clean package,会跳过单元测试直接打包成JAR文件。为什么需要跳过单元测试?我项目里没写Test类,但有些脏数据可能残留会导致测试失败,所以要跳过。如果需要跳过测试,命令是mvn clean package -DskipTests。

打包完成后,target目录下会生成bookstore.jar文件。这个JAR包包含了SpringBoot内嵌Tomcat和所有依赖,可以直接用java -jar bookstore.jar启动。我第一次部署的时候犯过一个错误,直接把JAR包放到Windows服务器上,用start javaw运行,结果端口冲突起不来。排查后发现是机器的8080端口被其他程序占用了,改成在启动命令里指定端口:java -jar bookstore.jar --server.port=8080。这个参数可以不加代码地临时改变启动端口,排错很实用。

5.2 前端Vue项目打包流程

前端项目打包命令是npm run build。执行完成后在项目根目录生成dist文件夹,这个文件夹里是纯静态资源,包含index.html、css、js等文件。前端构建产物不需要任何编译环境,一个Nginx甚至任意静态文件服务器就能跑起来。

打包前需要注意生产环境接口地址配置。我没把生产环境地址写死在代码里,而是用环境变量控制。在.env.production文件里设置VITE_API_BASE_URL为/api,然后让axios的baseURL读取这个变量。为什么生产环境依然用/api?因为我通过Nginx把所有/api请求转发到后端服务,前端代码本身不需要知道后端真实地址,只需要保持相对路径。这样以后后端换IP或者换端口,只需要改Nginx配置,不需要修改前端代码再重新构建。这种解耦方式是前后端分离项目部署的关键要点。

5.3 Nginx配置详解和部署要点

Nginx在前端部署中扮演两个角色:第一是静态服务器,提供前端文件的访问;第二是反向代理服务器,把接口请求转发给后端。你需要创建两个server块,或者用同一个server块的两种location来实现两个角色。

第一个location匹配 /,尽量把静态文件返回到前端页面。第二个location匹配 /api/,用proxy_pass把请求转发到后端服务,比如http://127.0.0.1:8080。这里有一个关键配置:proxy_pass的URL末尾不要加路径,直接把/api/替换成proxy_pass网址的路径。比如location /api/ { proxy_pass http://127.0.0.1:8080/; } 这样请求http://yourdomain/api/books会被转发到http://127.0.0.1:8080/books,后端SpringBoot的接口路径不用带/api前缀。

如果前端路由用了History模式,还要配置try_files $uri $uri/ /index.html;这样用户访问/login或/book/list时,Nginx先把请求匹配到静态文件,如果这个路径不是实际存在的文件,就回退到index.html,交由前端路由接管。这个配置不写的话,刷新页面就会404,这是History模式路由部署最常见的坑。

5.4 数据库部署与初始化

服务器上的MySQL数据库初始化用命令行就行。我把SQL脚本上传到服务器,然后执行mysql -uroot -p 回到mysql,用source命令加载SQL文件,一次性完成建库建表插数据。

生产环境MySQL需要注意两个参数:一是字符集,确认创建库时是utf8mb4;二是数据目录的磁盘容量和性能。如果用作小型图书门店,默认配置完全没问题。如果后续并发量上来了,可以调整innodb_buffer_pool_size和max_connections两个参数。但这个项目阶段不需要过度优化,先把服务跑通最要紧。

有一个部署时常见的坑是服务器防火墙没有放行端口。后端8080端口和前端80端口都需要在安全组或者防火墙规则中放行。有时候你确认tomcat或SpringBoot进程已经启动,浏览器访问时却一直卡住,百分之八十是防火墙产生的隔断,而不是服务本身起了问题。

5.5 一键启动脚本和进程管理

为了部署方便,我写了一个start.sh脚本来启动后端进程。脚本的核心逻辑是先找到旧进程,用kill命令结束旧进程,再重新启动JAR包,并通过nohup把日志写到日志文件,让服务在后台运行。nohup java -jar bookstore.jar --spring.profiles.active=prod > logs/bookstore.log 2>&1 &

这个脚本解决了一个很关键的痛点:ssh终端关闭后,服务进程也会被终止。如果不加nohup就把进程挂在终端上,你关闭ssh窗口服务也跟着停了。生产环境还要考虑进程守护,可以用systemd管理SpringBoot服务,也可以先用nohup简单跑着。中小项目用nohup足够稳定,但systemd更规范,即哪个服务崩溃了也会自动重新启动。如果你追求高稳定性,我建议写一个systemd的service单元文件,把启动命令和日志路径都配置好。

6. 常见问题与排查技巧实录

6.1 数据库连接失败与SSL错误排查

这个项目最常遇到的问题就是连不上数据库,具体表现有几种:Access denied for user、SSL连接错误、Connection refused和Unknown database。逐个说排查方案。

Access denied通常是用户名密码错误,或者账号权限不够,确认下root账号密码和授权情况。SSL连接报错,在MySQL 8.0的JDBC URL里加useSSL=false就能解决。Connection refused检查MySQL端口是否启动、防火墙是否放行3306。Unknown database说明数据库不存在,重新执行SQL脚本就行。

我还遇到过一种神秘情况,本地测试连不上,但navicat能连上,后来发现是JDBC URL写错了驱动名称或端口。要保持连接串和Navicat配置完全相同参数。

6.2 前端打包后接口404排查

前端打包后接口404是一个前后端对接的经典问题。我把排查步骤按顺序整理一下:打开浏览器开发者工具Network面板,查看接口请求的URL,看请求是发到了生产地址还是本地地址。如果URL是localhost:8080,说明前端打包时读取的是development环境变量而不是production环境变量,检查.env.production文件是否存在,构建命令是否正确识别的环境。

如果URL是api开头但请求进了Nginx,接下来就到后端日志看一下有没有对应的请求记录。如果后端日志没有记录,说明Nginx转发没生效,检查proxy_pass路径配置。如果后端日志有记录但仍然404,检查后端Controller的@RequestMapping路径是否带了/api前缀,SpringBoot应用端口和Nginx转发的端口是否一致。

这种排查路径本质上是个二分排查法:先定位是前端IP错误还是Nginx配置问题还是后端路由问题,逐步缩小范围。长久这样排查下来,效率最高。

6.3 SpringBoot启动失败或端口被占用

启动SpringBoot时如果报端口被占用,Windows上可以用netstat -ano | findstr 8080查看哪个进程占用了8080端口,然后taskkill /F /PID 进程号结束冲突进程。Linux上使用ss -lnp | grep 8080或lsof -i:8080查看进程,然后kill进程ID。

如果有一次你启动直接闪退,什么都不输出,可以用命令行方式运行java -jar来查看启动日志,而不是用后台方式运行。很多启动报错信息,比如端口冲突、数据库连接失败、Bean创建异常,控制台会明确打印,后台运行反而让日志丢失。排完错误后,再从正常空闲端口启动。

6.4 图书库存为负数或数据不一致的排查

如果发现库存变成负数,先看后端扣减库存的SQL是否加了stock >= #{quantity} 的限定条件。如果没有这个条件,并发场景就会超卖。再检查更新库存的代码是否使用了事务。假如更新图书表和插入销售单不在同一个事务里,可能存在部分成功引起的数据不一致,这时需要补充@Transactional。

为了避免这类问题反复出现,我在数据库层面做了一层兜底,用CHECK约束限制库存列不能为负数。这样即便代码逻辑有漏洞,数据库也会拒绝非法操作,并记录错误日志。该约束在MySQL 8.0的InnoDB引擎中会被强制执行。如果你是MySQL 5.7,需要确认是否启用。

6.5 常见问题速查表

问题现象可能原因排查与解决办法
接口返回401Token过期或未传Token检查请求头是否携带Authorization,重新登录获取新Token
前端连不上后端接口CORS跨域或代理配置错误开发环境检查vite proxy,生产环境检查Nginx转发
时间显示相差8小时JDBC连接串缺少serverTimezoneURL加serverTimezone=Asia/Shanghai
价格精度错误数据库字段用了float改用decimal(10,2)
打包后页面白屏资源路径配置错误检查vite的base配置,设置为./或绝对路径
登录后刷新页面就退出Token没存localStorage登录成功后把Token保存到localStorage
8080端口被占用其他进程占用端口使用netstat或lsof查找并结束进程
数据库出现乱码连接串缺字符集参数JDBC URL加characterEncoding=utf8

7. 源码解读与二次开发扩展方向

7.1 项目源码的整体阅读指南

源码里最核心的阅读顺序是:先看数据库建表,明白数据结构,再看实体类和Mapper层,了解对象与数据表的映射,然后读Service层,理解业务逻辑和事务边界,最后看Controller层,梳理对外接口。

Service层是整套系统的核心,所有业务规则都在这一层实现。你可以站在别人留的思路上去理解这套源码,而我做这套项目时最好的体验也是来源于此:每个方法都很短,因为只处理单个业务动作。

7.2 如何扩展成完整的图书商城系统

当前是进销存后台管理系统,如果你想往互联网商城方向扩展,可以加一个前台门户模块。前台包括图书浏览、搜索、购物车、下单、支付等操作,后台在原来的业务上增加订单状态管理。思路是新增数据表购物车表和订单表,订单表和销售出库单表逻辑类似,核心区别是销售出库单是门店现场开单一次性扣库存,线上订单需要支持"下单锁库存、付款减库存"这种多阶段逻辑。

如果想增加更完善的统计分析,可以在现有销售数据上写定时任务,把每天销售数据汇总到一张统计表里,然后前端用ECharts展示销售趋势、分类占比、TOP10图书排行这些图表。这个过程涉及聚合查询和定时任务,是学习数据分析的好素材。

7.3 若依框架和本项目的对比与取舍

很多使用者在看了热词里提到的若依框架之后,也很好奇我这个项目和若依有什么区别。若依是一个成熟的开源前后端分离后台管理系统,内置了用户权限、代码生成器、定时任务等模块,非常适合快速搭建内部管理系统。这个项目的定位则是一个业务导向清晰的进销存系统,代码结构更简洁,没有若依那么多辅助功能,更好理解。

从学习角度,我建议如果没接触过SpringBoot,先用这个项目练手,它的代码量适中,但CRUD和事务都覆盖到了。等基础牢固以后,再去研究若依这类框架,后者代码规模更大,动态路由、分布式缓存、权限标记这些概念需要基础支撑。等项目跑通过就能体会到,学习路径越来越大,难度逐步递增,这样才对个人进步最有价值。

我自己在带新手时通常这么安排学习顺序:先读懂项目整体结构,再照着源码把图书模块的增删改查完整实现一遍,然后自己给供应商模块加一个"删除前确认无关联采购单"的校验逻辑,最后独立完成一个新的"退货管理"模块。这套流程走下来,前后端分离开发的基本功就扎实了。

这个项目后续想往深里扩展,可以做权限系统改造、对接Redis做缓存、集成MinIO做图书图片管理,甚至用消息队列做采购单异步审核。每一种扩展方式都会逼你把对应技术栈的核心原理吃透,比漫无目的地刷教程要有效得多。

返回列表