news 2026/10/10 9:33:10

SpringBoot+Vue+MyBatis+MySQL企业级图书大厦管理系统全栈实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot+Vue+MyBatis+MySQL企业级图书大厦管理系统全栈实战

做图书管理系统的源码很多,但大部分都是“能跑通的demo”:后端打个CRUD接口,前端画几个表格,录一本加一本,顶多再加个模糊搜索,然后就在简历上写“完成图书管理系统开发”。但真要放到图书大厦这种场景里,业务链条远不止“增删改查”四板斧——多楼层馆区、不同身份的读者、借还高峰期的并发扣库存、逾期罚金结算、营业员的操作审计,每一块都可能把系统撕开一个口子。

这篇博文要聊的就是一套完整的SpringBoot+Vue+MyBatis+MySQL企业级图书大厦图书管理系统。它不是某个模块的片段代码,而是从需求拆解、数据库建模、后端事务与并发控制、前端借阅工作台,到部署上线的全链路实现。文章会把关键设计决策的“为什么”讲清楚,也会把我在实际开发中踩过的坑直接摆出来,适合正在做毕业设计、接外包,或者想搞明白一套真实业务系统怎么从零落地的同学参考。

1. 为什么图书大厦的系统不能照着学校图书馆demo抄

1.1 图书大厦与普通图书馆的业务差异

很多人一听“图书管理系统”,第一反应就是学校图书馆期末作业那个量级:一个管理员账号,一个图书表,一个借阅表,完事。但图书大厦这类场所,业务模型要复杂得多。

首先是物理空间的维度。图书大厦通常按楼层和馆区分区,一层可能是社科畅销区,二层是儿童绘本区,三层是专业资料库。图书表里必须有对应的馆藏位置字段,不然读者查到书却不知道去哪层拿,营业员上架也找不到货架。

其次是角色和权限的维度。除了系统管理员,还有营业员(负责借还、上下架)、读者(查询、预约、查看自己的借阅记录)。如果系统没有独立的角色权限体系,任何一个人登进来都能删库存、改价格,这在真实营业场景里是灾难。

第三是运营和合规的维度。借出去的书记录要能追溯,谁经手的、什么时候借的、该什么时候还;逾期了还要能自动算罚金;每天的借还量、热门书目排行这些统计报表,运营团队要拿来做决策。学校demo里那两张表根本支撑不了这些查询。

这也是我在接到这个项目需求时,第一件事不是写代码,而是把“管理员、营业员、读者”三类角色的完整业务流程画出来的原因。很多做崩的系统,崩在最开始的需求边界没理顺。

1.2 技术选型逻辑:SpringBoot+Vue+MyBatis+MySQL的取舍

标题定了这套技术栈,我来复盘一下这套组合在企业级项目里的合理性。

后端用SpringBoot:这是目前Java后端最主流的快速开发框架,自带的自动配置和内嵌容器大大降低了部署成本,生态里不管是安全框架、ORM还是缓存都能无缝整合。图书管理系统属于典型的中小型业务系统,用SpringBoot单体应用足够,没必要上微服务——微服务的拆分、服务发现、分布式事务在这里只会徒增复杂度。

持久层用MyBatis:有人会问,为什么不用JPA?MyBatis对SQL有完全的控制力,复杂查询(比如图书检索里的多条件动态拼接、报表统计里的聚合SQL)写起来直接、直观,调优也方便。图书大厦系统的查询条件组合非常多——书名、ISBN、分类、楼层、库存状态——MyBatis的动态SQL做这类需求非常顺手。JPA在简单CRUD上确实省事,但一旦查询复杂起来,拼Specification或者写JPQL,远不如直接SQL来得痛快。

数据库用MySQL:企业级场景下MySQL 8.x的成熟度、稳定性、运维成本、社区资料都是经过大规模验证的。表结构合理设计、索引到位之后,支撑几万册图书和几十万条借阅记录的日常业务完全没有压力。不用PostgreSQL或者Oracle,不是它们不好,而是在这个体量下MySQL是性价比最高的选择,团队招人也最容易。

前端用Vue:Vue的响应式数据绑定和组件化开发,做后台管理系统非常高效。配合Element Plus这类现成的组件库,表格、表单、弹窗、分页这些后台高频交互能快速落地,而且Vue的上手成本低,前后端联调时沟通效率也更高。

这套组合从开发效率、可控性和运维成本三个角度看,对一个“企业级图书大厦”项目来说,是务实的选择。

2. 数据模型设计:一张表把“大厦”拆成可落地的结构

2.1 核心表结构与字段说明

数据模型是整个系统最需要提前花时间的部分。表设计好了,后面所有业务代码都是顺着这个骨架走。我这里把核心几张表的要点列出来。

用户表 t_user

字段类型说明
idbigint主键,自增
usernamevarchar(50)登录名,唯一索引
passwordvarchar(100)BCrypt加密后的密文
real_namevarchar(50)真实姓名
rolevarchar(20)ADMIN / OPERATOR / READER
phonevarchar(20)手机号
statustinyint1启用,0禁用
create_timedatetime创建时间

图书表 t_book

字段类型说明
idbigint主键
isbnvarchar(20)ISBN编号,索引
book_namevarchar(200)书名
authorvarchar(100)作者
publishervarchar(100)出版社
category_idbigint分类ID,关联t_category
pricedecimal(10,2)定价
floor_novarchar(20)所在楼层
shelf_novarchar(50)货架编号
total_stockint总库存
available_stockint可借库存
statustinyint1上架,0下架
create_timedatetime录入时间

借阅记录表 t_borrow_record

字段类型说明
idbigint主键
user_idbigint借阅人ID
book_idbigint图书ID
borrow_timedatetime借出时间
due_timedatetime应还时间
return_timedatetime实际归还时间,可空
operator_idbigint经办营业员ID
statustinyint1借出,2已还,3逾期未还
fine_amountdecimal(10,2)罚金金额

分类表 t_category:id、name、parent_id,支持二级分类。比如“文学”下面还可以拆“小说”“散文”,图书大厦的图书分类比学校图书馆细很多,没有层级结构会很难维护。

操作日志表 t_operation_log:id、user_id、action、target_type、target_id、detail、create_time。审计追溯就靠它。

这里要强调一个细节:表的字段命名我全程用下划线风格,配合MyBatis的map-underscore-to-camel-case配置,实体里直接用驼峰字段,少写很多映射注解。

2.2 索引、唯一约束和软删除的设计

表建好只是第一步,索引设计直接决定系统在高数据量下的查询性能。

我在实际设计里做了这几件关键的事:

第一,借阅记录表建联合索引。日常查询基本都是围绕某个读者查“他借了什么书”,或者围绕某本书查“这本书被谁借走了”。所以(user_id, status)和(book_id, status)这两个联合索引是必须的。如果不建,几十万条记录里按照用户ID过滤就会走全表扫描,接口响应直接从毫秒级变成秒级。

第二,ISBN做唯一约束。同一本书的ISBN应该是唯一标识,如果允许重复录入,图书检索和统计就会出现脏数据。唯一约束在数据库层面兜底,比在业务代码里先查再插可靠得多,因为业务代码的判断在并发下可能有空隙。

第三,所有核心业务表保留status字段做软删除。图书大厦的管理人员误删一本在架图书,如果物理删除了,所有历史借阅记录的外键就悬空了。用status字段标记删除状态,业务查询默认过滤status = 1,既能防止误删,又能保留完整数据链条。

2.3 初始化数据与演示账号

源码里我准备了一份初始化SQL,除了建表语句,还内置了几组演示账号和一批示例图书数据。

演示账号设计成三种角色:

  • 系统管理员 admin / 123456
  • 营业员 operator01 / 123456
  • 读者 reader01 / 123456

图书数据我按照图书大厦常见的分区风格放了百来本,覆盖小说、儿童、经管、科技几个大类,分布在不同楼层货架。这样做的价值在于:前端页面联调时,登录进去就能看到有数据的界面,不会对着空表干瞪眼;运营人员验收系统时,也能直观地看到统计图表是有数据的。

提示:初始化SQL里的用户密码全部是BCrypt加密后的值,不能直接在数据库里改成明文。我后面会单独讲密码加密的处理方式。

3. 后端搭建:SpringBoot整合MyBatis的完整实践

3.1 项目分层与包结构

后端工程我按标准的分层架构组织,包结构如下:

com.library ├── config // 配置类:安全配置、跨域、异常处理 ├── controller // 接口层 ├── service // 业务层,接口和实现分离 ├── mapper // MyBatis的Mapper接口 ├── entity // 数据库实体 ├── dto // 请求/响应对象 ├── common // 公共类:统一返回结果、常量、异常 └── util // 工具类:JWT工具等

分层的好处不用多说,但我要特别强调一个容易被忽略的原则:Controller层只做参数接收和结果包装,Service层只做业务逻辑,Mapper层只做数据访问。网上很多demo把业务判断写在Controller里,一开始看着代码少,实际上业务一复杂就全乱套了,改一个借书规则要翻遍好几个接口。

实体和DTO我分开写。数据库实体是entity,前端请求参数和返回结果用dto。这样做看似多写几个类,但避免了把密码等敏感字段暴露给前端,也方便根据页面需求组合返回字段。

3.2 关键配置与依赖

pom.xml里核心依赖是这些:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.2</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>

MySQL 8.x的驱动类名和连接方式跟5.x有区别,配置的时候要特别注意:

spring: datasource: url: jdbc:mysql://localhost:3306/library_tower?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.library.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

这里面有两个我印象很深的坑,单独说一下。

第一个坑是serverTimezone参数。如果不加serverTimezone=Asia/Shanghai,MySQL 8连接时会直接报时区错误,或者返回的时间跟本地时间差8个小时。这个参数在本地开发时往往被忽略,部署到云服务器后一查数据全是乱的。

第二个坑是mapper-locations的路径。如果XML文件没放到resources/mapper/对应的位置,启动时会报Invalid bound statement(not found)。我见过很多同学把XML放在Java目录下,IDE里看着好好的,打包成jar之后就找不到文件了。XML和Mapper接口一定要按配置的路径放好。

3.3 动态SQL实现多条件图书检索

图书检索是读者端使用频率最高的功能,它的特点就是查询条件不固定:用户可能只输入书名,可能只选择分类,可能只按楼层过滤,也可能几个条件一起上。

MyBatis的动态SQL在这里是刚需:

<select id="searchBooks" resultType="com.library.entity.Book"> SELECT * FROM t_book <where> <if test="bookName != null and bookName != ''"> AND book_name LIKE CONCAT('%', #{bookName}, '%') </if> <if test="isbn != null and isbn != ''"> AND isbn = #{isbn} </if> <if test="categoryId != null"> AND category_id = #{categoryId} </if> <if test="floorNo != null and floorNo != ''"> AND floor_no = #{floorNo} </if> <if test="status != null"> AND status = #{status} </if> </where> ORDER BY create_time DESC LIMIT #{offset}, #{pageSize} </select>

<where>标签会自动去掉第一个条件前面的AND,这样就不用手动拼“WHERE 1=1”这种丑代码。查询参数用#{param}而不是${param},是防止SQL注入的关键,这是MyBatis的预编译机制,任何时候都不能图省事改成${}。

分页我把offset和pageSize直接作为参数传入,简单可控。也可以引入PageHelper插件,不过这个项目体量下,手写LIMIT分页已经足够清晰。统计总条数的时候单独写一个countBooks查询,配合前端表格的分页展示。

4. 借还书这个核心链路:事务、并发与罚金计算

4.1 借书流程的接口设计与事务边界

图书管理系统里最核心的业务是借书和还书,这个链路的正确性直接决定系统能不能用。借书接口的关键不是“插入一条记录”这么简单,它要保证一系列动作的原子性。

我梳理的借书流程是:

  1. 校验读者身份和账号状态(禁用状态不能借书)
  2. 校验图书状态(下架的不能借)
  3. 扣减可借库存
  4. 生成借阅记录,设置应还时间(系统默认30天,可按会员等级调整)
  5. 记录操作日志

这五个步骤任何一步失败,前面的操作都要回滚。比如库存扣了但借阅记录没生成成功,书就“凭空消失”了。所以整个流程在一个事务方法里处理:

@Transactional(rollbackFor = Exception.class) public BorrowResult borrowBook(Long userId, Long bookId, Long operatorId) { User user = userMapper.selectById(userId); if (user == null || user.getStatus() != 1) { throw new BusinessException("读者不存在或账号已锁定"); } Book book = bookMapper.selectByIdForUpdate(bookId); if (book == null || book.getStatus() != 1) { throw new BusinessException("图书不存在或已下架"); } if (book.getAvailableStock() <= 0) { throw new BusinessException("库存不足"); } int updated = bookMapper.decreaseStock(bookId); if (updated == 0) { throw new BusinessException("库存不足"); } BorrowRecord record = new BorrowRecord(); record.setUserId(userId); record.setBookId(bookId); record.setBorrowTime(new Date()); record.setDueTime(DateUtil.addDays(new Date(), 30)); record.setOperatorId(operatorId); record.setStatus(BorrowStatus.BORROWED.getValue()); borrowRecordMapper.insert(record); return new BorrowResult(record); }

事务注解一定要加rollbackFor = Exception.class。这是因为Spring默认只对RuntimeException回滚,如果业务方法抛出了受检异常,不加这个参数事务不会回滚,数据就会出现部分更新。

4.2 库存扣减的并发控制

借书场景最典型的并发问题是:两个读者同时借同一本只剩最后库存的书,如果两个请求同时读到available_stock等于1,都判断“库存足够”,然后都去减库存,最终就会出现超借。

解决并发扣减我采用的是数据库行锁的方案:

<select id="selectByIdForUpdate" resultType="com.library.entity.Book"> SELECT * FROM t_book WHERE id = #{id} FOR UPDATE </select>

SELECT ... FOR UPDATE会对命中的行加排他锁,第二个事务在第一个事务提交之前只能等待。这样“查库存-判断-扣减”就变成了串行操作,从根本上避免了超借问题。

这个方案用起来有两个注意点。

第一,必须在事务里使用。行锁在事务提交时才会释放,如果查询方法外面没有事务包裹,锁会立即释放,加了等于白加。

第二,锁的范围要小而准。这里锁的是一本具体图书的行,不是整张表,并发时其他图书的借阅完全不受影响,性能损耗是很低的。相比之下,用synchronized或者分布式锁虽然也能解决,但在单机事务场景下杀鸡用牛刀,而且分布式锁还要考虑锁超时和释放问题。

还有一种做法是乐观锁:update时带WHERE available_stock > 0条件,通过受影响行数判断是否抢到库存。这个方案也能用,但行锁在并发量不极端的情况下更直观、更好理解,我就选了它。

4.3 还书、逾期与罚金逻辑

还书流程和借书相反,核心动作是更新借阅记录状态、回补库存,同时判断是否逾期并计算罚金。

@Transactional(rollbackFor = Exception.class) public ReturnResult returnBook(Long recordId, Long operatorId) { BorrowRecord record = borrowRecordMapper.selectByIdForUpdate(recordId); if (record == null || record.getStatus() != BorrowStatus.BORROWED.getValue()) { throw new BusinessException("借阅记录不存在或已归还"); } Date now = new Date(); record.setReturnTime(now); record.setStatus(BorrowStatus.RETURNED.getValue()); record.setOperatorId(operatorId); if (now.after(record.getDueTime())) { long overdueDays = DateUtil.betweenDay(record.getDueTime(), now, true); BigDecimal fine = BigDecimal.valueOf(overdueDays) .multiply(Constants.FINE_PER_DAY); record.setFineAmount(fine); } else { record.setFineAmount(BigDecimal.ZERO); } borrowRecordMapper.updateById(record); bookMapper.increaseStock(record.getBookId()); return new ReturnResult(record); }

罚金规则我定义为每本每天0.5元,这个费率在配置常量里管理,方便运营调整。逾期天数按Math.ceil向上取整,比如超期1小时也算逾期1天,这是行业里常见的做法,避免因为精确到小时产生争议。

这里有个容易忽略的细节:还书回补库存之前,也要先锁定借阅记录。原因是防止两个营业员同时使用同一个借阅单号操作,或者同一本书两次归还导致库存多回补。锁住记录行之后,第二次操作就能通过状态校验拦截掉。

5. Vue前端:面向营业员的借阅工作台怎么做

5.1 前端工程结构与登录流程

前端我采用的是Vue 3 + Element Plus + Vue Router + Pinia + Axios的组合。工程结构按照后台管理系统最常见的模式组织:

src ├── api // 接口请求封装 ├── assets // 静态资源 ├── components // 公共组件 ├── router // 路由配置 ├── store // Pinia状态 ├── utils // 工具:axios实例、token存取 └── views // 页面 ├── login // 登录页 ├── dashboard // 首页仪表盘 ├── book // 图书管理相关 ├── borrow // 借还管理 ├── reader // 读者管理 └── system // 系统管理

登录流程是前后端交互的第一个关键点。用户在登录页输入账号密码,后端校验成功后返回JWT令牌。前端拿到token后存储在localStorage里,同时把用户基本信息存到Pinia里,后续所有请求自动带上token头部,路由守卫检查token存在才能进入业务页面。

在登录这一块,我建议后端接口返回的数据要区分token和userInfo两部分:token用来做身份凭证,userInfo用来控制前端页面显示哪些菜单和按钮。如果把角色信息只存在token里,前端每次都要解析JWT,很别扭。

5.2 axios封装、Token持久化与跨域处理

axios的封装是整个前端工程质量的分水岭。我习惯建一个统一实例,把公共逻辑全收拢进去:

import axios from 'axios' import { ElMessage } from 'element-plus' import router from '@/router' const service = axios.create({ baseURL: '/api', timeout: 15000 }) // 请求拦截器:自动携带token service.interceptors.request.use( config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = 'Bearer ' + token } return config }, error => Promise.reject(error) ) // 响应拦截器:统一处理业务错误和登录过期 service.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { ElMessage.error(res.message || '请求失败') return Promise.reject(new Error(res.message)) } return res }, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token') router.push('/login') } ElMessage.error(error.message || '网络异常') return Promise.reject(error) } ) export default service

有一个前后端分离项目的经典问题:跨域。我的做法是让前端通过baseURL: '/api'发起请求,然后在开发环境通过Vite代理转发到后端,生产环境通过nginx把/api前缀的请求反向代理到后端服务。这样浏览器看到的始终是同源请求,不需要后端开CORS,也避免了很多跨域安全的坑。

Vite的开发代理配置:

server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, rewrite: path => path.replace(/^\/api/, '') } } }

5.3 图书借还页面与扫码交互设计

营业员日常工作最密集的场景就是借书和还书,这个页面我花了很多心思。

借书操作的设计逻辑是:营业员输入或扫入读者编号,系统回显读者信息;再输入或扫入图书ISBN,系统查询这本书并显示库存和馆藏位置;确认无误后点击借出按钮。

这里做了一个交互优化:输入框支持连扫和连续输入。营业员操作时经常是一手拿着扫码枪,一手操作键盘,所以读者编号和图书ISBN的输入框要支持扫码枪的快速输入模式,扫完一个立即清空并自动聚焦到下一个输入框。Vue里用ref控制焦点就能轻松实现。

还书页面则简单直接:扫描图书条码后,自动匹配当前借出状态的借阅记录,展示读者信息和借阅天数、是否逾期、罚金金额,营业员确认后点击归还。

这个页面还有一个细节值得提:馆藏位置提示。图书大厦楼层多,营业员根据提示能快速定位书架,所以页面在显示图书信息时,把floor_no和shelf_no放在显眼位置,甚至可以做成卡片样式,方便肉眼快速扫到。

6. 企业级细节落地:权限、日志、性能和安全性

6.1 RBAC权限模型在系统里的实现方式

权限控制我采用的是经典RBAC模型。虽然系统只有三种角色,但我不建议直接把角色写死在业务判断里,而是通过角色+接口的映射做统一控制。

后端通过Spring Security实现:

@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeHttpRequests() .requestMatchers("/api/auth/login", "/api/book/search").permitAll() .requestMatchers("/api/admin/**").hasRole("ADMIN") .requestMatchers("/api/operator/**").hasAnyRole("ADMIN", "OPERATOR") .requestMatchers("/api/reader/**").authenticated() .anyRequest().authenticated() .and() .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); }

规则是:登录接口和图书检索接口所有人可访问;管理员相关的接口只有ADMIN能访问;营业员操作的接口ADMIN和OPERATOR都可以;读者的个人接口只要登录即可。

前端路由也做了一层配合,根据角色动态注册可访问的路由和菜单。这样双端控制虽然会增加一点工作量,但能避免“接口改了、前端还能进”这种权限漏洞。

6.2 操作审计日志与借阅历史

企业级系统必须有审计能力。营业员在系统里做的每一次敏感操作——上架图书、下架图书、办理借书、办理还书、调整罚金——都要记录谁在什么时间做了什么操作。

我用Spring AOP实现了一个统一的操作日志切面,在需要审计的方法上加自定义注解,切面自动拦截并记录:

@Aspect @Component public class OperationLogAspect { @Around("@annotation(operationLog)") public Object record(ProceedingJoinPoint point, OperationLog operationLog) throws Throwable { Object result = point.proceed(); // 在正常执行后记录日志 // 获取当前登录用户、方法名、参数、耗时 return result; } }

这里我刻意选择在方法正常返回后记录日志,而不是在方法执行前记录。因为操作失败时的日志价值没那么高,而且一个真正成功的操作才需要审计。

借阅历史的逻辑则要区分两个视角:读者端看到的是“我借了哪些书、什么时候还”;运营端看到的是“某本书的流通记录、某读者的借阅轨迹”。两张报表本质上都查t_borrow_record,只是过滤维度和展示字段不同,可以共用一套查询接口,前端传不同参数区分。

6.3 性能优化:分页、缓存和连接池

业务量起来之后,性能瓶颈通常出现在几个地方,我提前做了布局。

列表查询全分页。无论是图书列表、借阅记录还是操作日志,都采用分页查询。图书数据可能上万条,一次性全查出来用户也看不过来,还白白占用网络和内存。

热门数据用缓存。图书分类这种变更频率极低、读取频率极高的数据,用@Cacheable做了一层本地缓存,避免每次查询分类列表都打数据库。图书检索结果不做缓存,因为库存数据实时性要求高,缓存的收益太低。

数据库连接池重视配置。默认的连接池参数在并发稍高时会成为瓶颈。我这里给了HikariCP一个合理的初始连接数和最大连接数:

spring: datasource: hikari: minimum-idle: 5 maximum-pool-size: 20 connection-timeout: 30000

最大连接数20对于图书大厦这种规模的系统足够,但又不会因为连接数过多拖垮数据库。这个参数生产环境一定要监控实际连接数再调整,不能拍脑袋。

6.4 安全注意点:密码加密、SQL注入防护、长整型精度

安全相关的几个点,很多项目会忽视,但出了事都是大问题。

密码必须加盐加密。登录密码存储用的是BCrypt,它能自动生成随机盐,即使两个用户的密码相同,加密后的密文也不同。登录校验时用matches方法比对。我不会把密码明文登在数据库,也不会用MD5这种已不安全的算法。

SQL注入的防线在预编译。MyBatis的#{}就是预编译占位符,能避免拼接SQL导致的注入风险。我明确要求项目里所有查询都用#{},杜绝${}拼表名或列名。

Long类型主键的JSON精度问题。Java的Long类型如果超过JavaScript的Number安全整数范围(2的53次方减1),前端拿到的ID精度会丢失。数据库自增主键在数据量大时很容易超过这个范围。解决办法是在返回前端的DTO里给ID字段加上ToString序列化注解:

@JsonSerialize(using = ToStringSerializer.class) private Long id;

这样前端拿到的就是字符串,主键精度不会丢,后面做更新、删除操作时传给后端也能准确匹配。

7. 从源码到上线:部署步骤与我踩过的坑

7.1 本地跑通全流程

拿到源码第一件事,是让系统在本地完整跑起来。我按从零开始的环境准备顺序,列一份完整操作清单。

  1. 安装MySQL 8.x,执行sql/init.sql初始化数据库,包含建库、建表、初始数据。
  2. 修改application.yml中的数据库账号密码,确保能连上本地MySQL。
  3. 启动后端:mvn spring-boot:run,看到“Started Application”表示启动成功。
  4. 安装Node.js 16以上版本,进入前端目录执行npm install安装依赖。
  5. 修改前端.env.development中的接口地址配置,执行npm run dev启动开发服务器。
  6. 浏览器访问http://localhost:5173,用演示账号登录。

我在源码里把前后端默认端口都固定好了:后端8080,前端Vite默认5173。如果本地端口被占用,改配置即可,但要注意同步修改前端的代理转发目标和后端的允许来源。

7.2 服务器部署与nginx反向代理

生产环境部署,我的经典组合是后端jar包 + nginx托管前端静态文件。

后端打包:

mvn clean package -DskipTests java -jar library-system-1.0.0.jar

后端用nohup后台运行并输出日志到指定文件,方便排查问题:

nohup java -jar library-system-1.0.0.jar > /app/logs/system.log 2>&1 &

前端构建:

npm run build # 将生成的dist目录上传到服务器

nginx配置核心部分:

server { listen 80; server_name your-domain.com; root /usr/share/nginx/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这行是Vue Router的History模式必须的:前端路由的路径在nginx里找不到对应文件时,回退到index.html由前端路由接管,否则刷新页面就会404。

7.3 几个高频问题的排查记录

最后分享几个我在开发和运维这套系统过程中实际遇到的问题,每个都花过不少时间定位。

问题一:修改用户密码后无法登录

原因通常是数据库里的密文和登录时传入的明文密码不匹配。排查思路:先确认数据库里存的密码字段是不是BCrypt格式,调试时直接在登录接口打日志,看用户查出来的密文和传入的明文能否通过matches校验。如果期间有人在数据库里手工UPDATE过密码为明文,BCrypt校验必然失败。

问题二:前端页面总是白屏

Vue项目构建后部署,出现白屏先看浏览器控制台报错。最常见的是静态资源路径问题。如果部署在域名根路径下,Vite的base配置是根路径;如果部署在子目录,就要配置base: '/subpath/',否则CSS和JS文件引用路径不对。

问题三:借阅高峰期接口响应变慢

现象是下班前借还高峰期,借书接口从几十毫秒变成几秒。我先看数据库慢查询日志,发现借阅记录表的数据量已经比较大,而部分查询没有走索引。后来补上了(user_id, status)和(book_id, status)两个联合索引,问题直接解决。这提醒我:上线前的表设计要预留索引,上线后要监控慢查询。

问题四:事务不生效导致库存异常

这是典型的Spring事务陷阱。我在同一个Service里写了A方法调用B方法,B方法上标注了@Transactional,但通过this调用时注解失效,因为事务是通过代理对象生效的,直接调用内部方法绕过了代理。解决方式是把被调用方法放到另一个Service里注入调用,或者自行注入代理对象。

这套图书大厦管理系统做到最后,我最深的体会是:技术上没有哪个模块是“高不可攀”的,真正的难点全在业务边界、并发细节和数据一致性这些看不见的地方。如果你正在照着源码学习改造,建议不要急着通读所有代码,先跑起来,然后挑一个你最关心的业务场景(比如借还书那个事务)走一遍完整链路,再对照这篇文章看设计意图,收获会大得多。源码里我保留了完整的注释和初始化数据,遇到跑不起来的问题,先查数据库连接和前端代理这两处,大部分情况都是环境配置的锅。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/10 9:33:03

MATLAB SVM柴油机故障识别:从特征提取到参数寻优的完整流程

简介&#xff1a;这份资源面向机器学习入门者、故障诊断方向工程师及自动化专业学生&#xff0c;提供一套基于MATLAB的支持向量机柴油机故障识别完整实现方案&#xff0c;帮助读者理解SVM分类原理并落地到工业设备健康管理场景。压缩包共2个文件&#xff0c;包含1个xlsx数据表与…

作者头像 李华
网站建设 2026/10/10 9:32:49

Docker容器操作与私有仓库部署实战笔记

1. 实验背景与整体设计思路最近整理了一份Docker容器常用操作与私有仓库部署的实验笔记&#xff0c;起因是某测试环境需要一套完全内网可控的镜像交付链路&#xff1a;开发机打好的镜像既能随手跑起来验证&#xff0c;又要能推到一台统一管理的私有仓库里&#xff0c;供其他节点…

作者头像 李华
网站建设 2026/10/10 9:32:37

智能家居数据管道实战:Kafka + Spark 流批一体处理

简介&#xff1a;这是一套面向物联网与大数据方向学习者的智能家居数据分析系统源码&#xff0c;适合具备一定Spark、Kafka基础、希望动手实践流式数据处理的中高级开发者。项目以MQTT协议采集智能家居设备传感器数据&#xff0c;经Kafka消息队列实现实时传输&#xff0c;再由S…

作者头像 李华
网站建设 2026/10/10 9:32:03

基于双教师自适应特权蒸馏的强化学习自蒸馏方法DualOPSD

这次我们来看一个强化学习方向的自蒸馏方法&#xff1a;DualOPSD&#xff0c;全称是 Adaptive Privileged Teachers for On-Policy Self-Distillation。核心思路并不复杂&#xff1a;训练一个学生策略时&#xff0c;同时维护两个具备特权信息的教师模型&#xff0c;并根据当前状…

作者头像 李华
网站建设 2026/10/10 9:32:03

Nanointerpret部署实战:轻量级LLM可解释性分析平台

这次我们来看一个在 Hacker News 上以 Show HN 形式出现的开源项目&#xff1a;Nanointerpret。从命名和展示形态来看&#xff0c;这是一个轻量级的 LLM 可解释性实验平台&#xff0c;目标是把大模型内部的注意力分布、激活值、层间输出等抽象信号&#xff0c;用可视化界面的方…

作者头像 李华
网站建设 2026/10/10 9:31:02

校园反诈骗微信小程序:SSM全栈模板从零搭建实战

简介&#xff1a;一套面向计算机相关专业毕业设计的校园反诈骗微信小程序完整资料包&#xff0c;涵盖微信小程序端与基于SSM框架的管理后台&#xff0c;可帮助从选题、功能设计、代码实现到论文撰写完成毕业设计&#xff0c;也适用于校园安全知识推广类课程实践。包内含小程序前…

作者头像 李华