ARTICLE DETAIL

资讯详情

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

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

SpringBoot+Vue前后端分离图书进销存系统实战:从数据库设计到部署上线 这篇博客是整理我实际把图书进销存系统从零搭起来、跑通、部署上线的详细过程包含完整源码和部署教程。如果你正在用SpringBootVue做毕业设计、课设或者想找一个真正能落地的前后端分离项目练手这篇内容能帮你省掉很多查资料的时间。先说这是个什么东西。图书进销存系统本质上就是给书店或者图书仓库做的一套管理工具管三件事进货、卖货、库存。但这套系统我把它做成了前后端分离架构后端用SpringBoot提供JSON接口前端用Vue3独立部署中间通过RESTful API通信静态资源、跨域、接口鉴权都按生产环境的标准做了处理。标题里写了SpringBootVueMyBatisMySQL这就是整套系统的全部核心。我接下会从需求拆解、表结构设计、后端接口实现、前端页面开发、部署上线这五个维度展开每个环节都会给出代码和配置把自己实际踩过的坑直接标出来。我的目标是让你拿着这篇文章跟着操作就能把项目跑起来而不是只给你看一堆理论概念。1. 内容整体设计与思路拆解1.1 为什么选图书进销存这个业务而不是通用进销存我一开始也纠结过要不要做一个通用的进销存系统但后来决定聚焦图书这一个垂直品类。理由很简单通用进销存系统字段太抽象什么货品编号SKU属性听起来很全能实际做页面的时候你根本不知道怎么给用户解释。图书不一样它有天然的实体属性书名、作者、ISBN、出版社、定价、库存量。每个字段用户都看得懂数据模型的设计逻辑也更接近真实业务。另一个原因是图书进销存有清晰的业务闭环采购入库、库存管理、销售出库、退货处理、库存预警。这五个环节可以拆成模块化开发正好对应前后端分离项目的接口设计思路。图书本身又有多册同版的特点不像衣服有尺码颜色这种多维度SKU处理起来复杂度适中适合用来展示CRUD、分页、统计、条件查询这些核心功能。如果是第一次做前后端分离项目我建议也选一个垂直化的小场景不要一上来就做通用系统。通用系统意味着你要设计抽象的数据模型和复杂的权限体系对新手不友好对老手又体现不出架构优势。图书进销存这种业务表结构能控制在六张左右接口数量在三十个左右页面在八到十个恰好是能体现架构能力但不失控的规模。1.2 技术栈选型的取舍为什么是SpringBootVueMyBatisMySQL这套技术栈组合看起来有点老生常谈但每个组件都是经过实际验证后才定下来的。后端为什么不用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用intsafety_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 CHARSETutf8mb4不要用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整合MyBatismysql-connector-java驱动MySQLlombok简化实体类代码。如果你不想用lombok手动写getter setter也行但lombok真的是节省大量样板代码的利器建议直接上。application.yml配置文件的重点是数据源和MyBatis配置。连接串一定要拼参数characterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai。不写serverTimezoneMySQL 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并指定时区GMT8。这个点看起来很小但实际项目里前后端因为时间格式扯皮的情况特别常见。3.2 RESTful接口设计规范接口路径我按资源名来定义统一使用名词复数形式比如/books、/suppliers、/purchases、/sales。操作语义由HTTP方法区分GET用来获取资源POST用来创建资源PUT用来更新资源DELETE用来删除资源。这个设计规范好在哪里它让接口名称具备自解释能力前端看到GET /books就知道是查图书列表看到POST /purchases就知道是创建采购单不需要维护一堆actionxxx风格的接口文档。图书管理接口包括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.port8080。这个参数可以不加代码地临时改变启动端口排错很实用。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.activeprod logs/bookstore.log 21 这个脚本解决了一个很关键的痛点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里加useSSLfalse就能解决。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加serverTimezoneAsia/Shanghai价格精度错误数据库字段用了float改用decimal(10,2)打包后页面白屏资源路径配置错误检查vite的base配置设置为./或绝对路径登录后刷新页面就退出Token没存localStorage登录成功后把Token保存到localStorage8080端口被占用其他进程占用端口使用netstat或lsof查找并结束进程数据库出现乱码连接串缺字符集参数JDBC URL加characterEncodingutf87. 源码解读与二次开发扩展方向7.1 项目源码的整体阅读指南源码里最核心的阅读顺序是先看数据库建表明白数据结构再看实体类和Mapper层了解对象与数据表的映射然后读Service层理解业务逻辑和事务边界最后看Controller层梳理对外接口。Service层是整套系统的核心所有业务规则都在这一层实现。你可以站在别人留的思路上去理解这套源码而我做这套项目时最好的体验也是来源于此每个方法都很短因为只处理单个业务动作。7.2 如何扩展成完整的图书商城系统当前是进销存后台管理系统如果你想往互联网商城方向扩展可以加一个前台门户模块。前台包括图书浏览、搜索、购物车、下单、支付等操作后台在原来的业务上增加订单状态管理。思路是新增数据表购物车表和订单表订单表和销售出库单表逻辑类似核心区别是销售出库单是门店现场开单一次性扣库存线上订单需要支持下单锁库存、付款减库存这种多阶段逻辑。如果想增加更完善的统计分析可以在现有销售数据上写定时任务把每天销售数据汇总到一张统计表里然后前端用ECharts展示销售趋势、分类占比、TOP10图书排行这些图表。这个过程涉及聚合查询和定时任务是学习数据分析的好素材。7.3 若依框架和本项目的对比与取舍很多使用者在看了热词里提到的若依框架之后也很好奇我这个项目和若依有什么区别。若依是一个成熟的开源前后端分离后台管理系统内置了用户权限、代码生成器、定时任务等模块非常适合快速搭建内部管理系统。这个项目的定位则是一个业务导向清晰的进销存系统代码结构更简洁没有若依那么多辅助功能更好理解。从学习角度我建议如果没接触过SpringBoot先用这个项目练手它的代码量适中但CRUD和事务都覆盖到了。等基础牢固以后再去研究若依这类框架后者代码规模更大动态路由、分布式缓存、权限标记这些概念需要基础支撑。等项目跑通过就能体会到学习路径越来越大难度逐步递增这样才对个人进步最有价值。我自己在带新手时通常这么安排学习顺序先读懂项目整体结构再照着源码把图书模块的增删改查完整实现一遍然后自己给供应商模块加一个删除前确认无关联采购单的校验逻辑最后独立完成一个新的退货管理模块。这套流程走下来前后端分离开发的基本功就扎实了。这个项目后续想往深里扩展可以做权限系统改造、对接Redis做缓存、集成MinIO做图书图片管理甚至用消息队列做采购单异步审核。每一种扩展方式都会逼你把对应技术栈的核心原理吃透比漫无目的地刷教程要有效得多。
返回列表