ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

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

SpringBoot+Vue+MyBatis+MySQL企业级图书大厦管理系统全栈实战 做图书管理系统的源码很多但大部分都是“能跑通的demo”后端打个CRUD接口前端画几个表格录一本加一本顶多再加个模糊搜索然后就在简历上写“完成图书管理系统开发”。但真要放到图书大厦这种场景里业务链条远不止“增删改查”四板斧——多楼层馆区、不同身份的读者、借还高峰期的并发扣库存、逾期罚金结算、营业员的操作审计每一块都可能把系统撕开一个口子。这篇博文要聊的就是一套完整的SpringBootVueMyBatisMySQL企业级图书大厦图书管理系统。它不是某个模块的片段代码而是从需求拆解、数据库建模、后端事务与并发控制、前端借阅工作台到部署上线的全链路实现。文章会把关键设计决策的“为什么”讲清楚也会把我在实际开发中踩过的坑直接摆出来适合正在做毕业设计、接外包或者想搞明白一套真实业务系统怎么从零落地的同学参考。1. 为什么图书大厦的系统不能照着学校图书馆demo抄1.1 图书大厦与普通图书馆的业务差异很多人一听“图书管理系统”第一反应就是学校图书馆期末作业那个量级一个管理员账号一个图书表一个借阅表完事。但图书大厦这类场所业务模型要复杂得多。首先是物理空间的维度。图书大厦通常按楼层和馆区分区一层可能是社科畅销区二层是儿童绘本区三层是专业资料库。图书表里必须有对应的馆藏位置字段不然读者查到书却不知道去哪层拿营业员上架也找不到货架。其次是角色和权限的维度。除了系统管理员还有营业员负责借还、上下架、读者查询、预约、查看自己的借阅记录。如果系统没有独立的角色权限体系任何一个人登进来都能删库存、改价格这在真实营业场景里是灾难。第三是运营和合规的维度。借出去的书记录要能追溯谁经手的、什么时候借的、该什么时候还逾期了还要能自动算罚金每天的借还量、热门书目排行这些统计报表运营团队要拿来做决策。学校demo里那两张表根本支撑不了这些查询。这也是我在接到这个项目需求时第一件事不是写代码而是把“管理员、营业员、读者”三类角色的完整业务流程画出来的原因。很多做崩的系统崩在最开始的需求边界没理顺。1.2 技术选型逻辑SpringBootVueMyBatisMySQL的取舍标题定了这套技术栈我来复盘一下这套组合在企业级项目里的合理性。后端用SpringBoot这是目前Java后端最主流的快速开发框架自带的自动配置和内嵌容器大大降低了部署成本生态里不管是安全框架、ORM还是缓存都能无缝整合。图书管理系统属于典型的中小型业务系统用SpringBoot单体应用足够没必要上微服务——微服务的拆分、服务发现、分布式事务在这里只会徒增复杂度。持久层用MyBatis有人会问为什么不用JPAMyBatis对SQL有完全的控制力复杂查询比如图书检索里的多条件动态拼接、报表统计里的聚合SQL写起来直接、直观调优也方便。图书大厦系统的查询条件组合非常多——书名、ISBN、分类、楼层、库存状态——MyBatis的动态SQL做这类需求非常顺手。JPA在简单CRUD上确实省事但一旦查询复杂起来拼Specification或者写JPQL远不如直接SQL来得痛快。数据库用MySQL企业级场景下MySQL 8.x的成熟度、稳定性、运维成本、社区资料都是经过大规模验证的。表结构合理设计、索引到位之后支撑几万册图书和几十万条借阅记录的日常业务完全没有压力。不用PostgreSQL或者Oracle不是它们不好而是在这个体量下MySQL是性价比最高的选择团队招人也最容易。前端用VueVue的响应式数据绑定和组件化开发做后台管理系统非常高效。配合Element Plus这类现成的组件库表格、表单、弹窗、分页这些后台高频交互能快速落地而且Vue的上手成本低前后端联调时沟通效率也更高。这套组合从开发效率、可控性和运维成本三个角度看对一个“企业级图书大厦”项目来说是务实的选择。2. 数据模型设计一张表把“大厦”拆成可落地的结构2.1 核心表结构与字段说明数据模型是整个系统最需要提前花时间的部分。表设计好了后面所有业务代码都是顺着这个骨架走。我这里把核心几张表的要点列出来。用户表 t_user字段类型说明idbigint主键自增usernamevarchar(50)登录名唯一索引passwordvarchar(100)BCrypt加密后的密文real_namevarchar(50)真实姓名rolevarchar(20)ADMIN / OPERATOR / READERphonevarchar(20)手机号statustinyint1启用0禁用create_timedatetime创建时间图书表 t_book字段类型说明idbigint主键isbnvarchar(20)ISBN编号索引book_namevarchar(200)书名authorvarchar(100)作者publishervarchar(100)出版社category_idbigint分类ID关联t_categorypricedecimal(10,2)定价floor_novarchar(20)所在楼层shelf_novarchar(50)货架编号total_stockint总库存available_stockint可借库存statustinyint1上架0下架create_timedatetime录入时间借阅记录表 t_borrow_record字段类型说明idbigint主键user_idbigint借阅人IDbook_idbigint图书IDborrow_timedatetime借出时间due_timedatetime应还时间return_timedatetime实际归还时间可空operator_idbigint经办营业员IDstatustinyint1借出2已还3逾期未还fine_amountdecimal(10,2)罚金金额分类表 t_categoryid、name、parent_id支持二级分类。比如“文学”下面还可以拆“小说”“散文”图书大厦的图书分类比学校图书馆细很多没有层级结构会很难维护。操作日志表 t_operation_logid、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 groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependencyMySQL 8.x的驱动类名和连接方式跟5.x有区别配置的时候要特别注意spring: datasource: url: jdbc:mysql://localhost:3306/library_tower?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/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参数。如果不加serverTimezoneAsia/ShanghaiMySQL 8连接时会直接报时区错误或者返回的时间跟本地时间差8个小时。这个参数在本地开发时往往被忽略部署到云服务器后一查数据全是乱的。第二个坑是mapper-locations的路径。如果XML文件没放到resources/mapper/对应的位置启动时会报Invalid bound statementnot found。我见过很多同学把XML放在Java目录下IDE里看着好好的打包成jar之后就找不到文件了。XML和Mapper接口一定要按配置的路径放好。3.3 动态SQL实现多条件图书检索图书检索是读者端使用频率最高的功能它的特点就是查询条件不固定用户可能只输入书名可能只选择分类可能只按楼层过滤也可能几个条件一起上。MyBatis的动态SQL在这里是刚需select idsearchBooks resultTypecom.library.entity.Book SELECT * FROM t_book where if testbookName ! null and bookName ! AND book_name LIKE CONCAT(%, #{bookName}, %) /if if testisbn ! null and isbn ! AND isbn #{isbn} /if if testcategoryId ! null AND category_id #{categoryId} /if if testfloorNo ! null and floorNo ! AND floor_no #{floorNo} /if if teststatus ! null AND status #{status} /if /where ORDER BY create_time DESC LIMIT #{offset}, #{pageSize} /selectwhere标签会自动去掉第一个条件前面的AND这样就不用手动拼“WHERE 11”这种丑代码。查询参数用#{param}而不是${param}是防止SQL注入的关键这是MyBatis的预编译机制任何时候都不能图省事改成${}。分页我把offset和pageSize直接作为参数传入简单可控。也可以引入PageHelper插件不过这个项目体量下手写LIMIT分页已经足够清晰。统计总条数的时候单独写一个countBooks查询配合前端表格的分页展示。4. 借还书这个核心链路事务、并发与罚金计算4.1 借书流程的接口设计与事务边界图书管理系统里最核心的业务是借书和还书这个链路的正确性直接决定系统能不能用。借书接口的关键不是“插入一条记录”这么简单它要保证一系列动作的原子性。我梳理的借书流程是校验读者身份和账号状态禁用状态不能借书校验图书状态下架的不能借扣减可借库存生成借阅记录设置应还时间系统默认30天可按会员等级调整记录操作日志这五个步骤任何一步失败前面的操作都要回滚。比如库存扣了但借阅记录没生成成功书就“凭空消失”了。所以整个流程在一个事务方法里处理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 idselectByIdForUpdate resultTypecom.library.entity.Book SELECT * FROM t_book WHERE id #{id} FOR UPDATE /selectSELECT ... 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 本地跑通全流程拿到源码第一件事是让系统在本地完整跑起来。我按从零开始的环境准备顺序列一份完整操作清单。安装MySQL 8.x执行sql/init.sql初始化数据库包含建库、建表、初始数据。修改application.yml中的数据库账号密码确保能连上本地MySQL。启动后端mvn spring-boot:run看到“Started Application”表示启动成功。安装Node.js 16以上版本进入前端目录执行npm install安装依赖。修改前端.env.development中的接口地址配置执行npm run dev启动开发服务器。浏览器访问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 21 前端构建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里注入调用或者自行注入代理对象。这套图书大厦管理系统做到最后我最深的体会是技术上没有哪个模块是“高不可攀”的真正的难点全在业务边界、并发细节和数据一致性这些看不见的地方。如果你正在照着源码学习改造建议不要急着通读所有代码先跑起来然后挑一个你最关心的业务场景比如借还书那个事务走一遍完整链路再对照这篇文章看设计意图收获会大得多。源码里我保留了完整的注释和初始化数据遇到跑不起来的问题先查数据库连接和前端代理这两处大部分情况都是环境配置的锅。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进