ARTICLE DETAIL

资讯详情

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

SpringBoot+Vue校园二手图书交易平台:从数据库设计到订单状态机实战解析

SpringBoot+Vue校园二手图书交易平台:从数据库设计到订单状态机实战解析 简介基于Springboot与Vue技术栈打造的校园二手图书交易平台是一套完整可运行的毕业设计源码项目面向计算机专业学生可用于毕业设计、课程设计及期末大作业。系统前端采用Vue框架搭配Element UI等界面组件后端由Springboot提供接口服务配以MySQL数据库脚本功能覆盖图书发布、浏览检索、在线交易等核心模块管理后台与用户端设计齐全界面美观、操作简洁。压缩包共751个文件大小约25.71MB主要包含121个Java后端源码、46个Vue页面组件、155个JavaScript脚本、48个CSS样式文件以及162个SVG图标和丰富的GIF/JPG/PNG图片素材另有SQL数据库脚本与1-install.bat、2-run.bat、3-build.bat部署辅助脚本配套代码注释与使用文档便于快速导入运行。项目经过严格调试简单部署即可使用已有387人学习下载是高分毕设与实战练手的优质参考。1. 二手书的流转难题这套 SpringBoot Vue 校园二手图书交易平台到底能解决什么每年毕业季宿舍楼下总堆着一箱箱卖不掉的旧教材和新考研书开学季新生又在为动辄上百一本的教材肉疼。学校不缺存量书缺的是一个能挂出来、能被搜到、能约线下交接的线上货架。这个基于 SpringBoot Vue 的校园二手图书交易平台本质就是给校园场景做一套垂直的 C2C 二手书交易系统学生注册登录后发布图书、浏览检索、下单购买管理员处理图书审核和用户管理。它不碰物流、不碰在线支付把交易闭环停在“线下自提平台确认完成”这正好卡在毕设的工作量适中、业务逻辑完整、前后端分离技术栈全用上的位置。适合两类人一类是正在选毕设题目、想用主流技术栈稳稳拿高分的学生另一类是刚学完 SpringBoot 和 Vue、想完整走一遍全栈项目的初级开发。这套方案的落地核心不在代码量而在状态机的严谨性和前后端联调的细节下面按一条可复现的路径逐步拆开。2. 功能拆解与数据库建模从角色反推表结构能省一半返工做任何管理系统先定角色再定模块最后反推表结构这是一条不容易返工的路线。校园二手图书平台的用户侧只有两类普通学生用户和管理员。学生能注册登录、发布图书、浏览检索、下单购买、管理自己的订单和图书管理员除了用户管理还要做图书审核和分类维护。别急着写代码第一步把这些动作翻译成数据表表设计的质量直接决定后面 Service 层要写多少补丁。2.1 功能模块与角色权限先画清楚谁能干什么我一般会把需求拆成三张图角色权限矩阵、页面清单、状态流转图。这个项目里角色简单权限矩阵不必引入 Spring Security 那套重武器用拦截器校验登录态、用角色字段区分管理员即可。页面清单大致是这样游客可访问首页、图书列表、图书详情、登录注册页登录用户可以额外访问发布图书、我的在售、我的下单、我的接单、个人信息管理员多一个后台管理页里面包含用户列表、图书审核、分类列表。这套划分贴在 Vue Router 上就是一层路由守卫贴在 SpringBoot 上就是一个拦截器成本很低。表结构上需要注意订单表不要叫 order它是 MySQL 的保留字建表时要么加反引号要么直接叫 orders。很多人在这一步图省事用了 order后面每次写 SQL 都要带着反引号属于给自己挖坑。分类表我建议单独建一张 category虽然图书表里直接存分类名也能跑但管理员要改个分类名时就得批量 UPDATE犯不上。2.2 核心表结构用户表、图书表与订单表的字段怎么落后端采用 SpringBoot MyBatis-Plus MySQL 8数据库初始化脚本里至少要建四张表用户表、分类表、图书表、订单表。图书表是核心状态字段撑起整个业务流转。CREATE TABLE user ( id bigint NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登录账号, password varchar(100) NOT NULL COMMENT BCrypt加密后的密码, nickname varchar(50) DEFAULT NULL COMMENT 昵称, avatar varchar(255) DEFAULT NULL COMMENT 头像URL, phone varchar(20) DEFAULT NULL COMMENT 联系电话用于线下交接, role tinyint NOT NULL DEFAULT 0 COMMENT 0-学生 1-管理员, status tinyint NOT NULL DEFAULT 0 COMMENT 0-正常 1-禁用, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; CREATE TABLE category ( id bigint NOT NULL AUTO_INCREMENT, name varchar(50) NOT NULL COMMENT 分类名, sort int DEFAULT 0 COMMENT 排序权重, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT图书分类表; CREATE TABLE book ( id bigint NOT NULL AUTO_INCREMENT, title varchar(200) NOT NULL COMMENT 书名, author varchar(100) DEFAULT NULL COMMENT 作者, publisher varchar(100) DEFAULT NULL COMMENT 出版社, isbn varchar(20) DEFAULT NULL COMMENT ISBN, category_id bigint DEFAULT NULL COMMENT 分类ID, original_price decimal(10,2) DEFAULT NULL COMMENT 原价, price decimal(10,2) NOT NULL COMMENT 售价, quality tinyint NOT NULL DEFAULT 0 COMMENT 成色0-全新 1-九成 2-七成 3-有笔记, book_desc text COMMENT 图书描述, cover_url varchar(255) DEFAULT NULL COMMENT 封面图URL, status tinyint NOT NULL DEFAULT 0 COMMENT 0-在售 1-交易中 2-已售出 3-已下架, seller_id bigint NOT NULL COMMENT 卖家ID, view_count int DEFAULT 0 COMMENT 浏览次数, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_seller_id (seller_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT图书表; CREATE TABLE orders ( id bigint NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 订单号雪花算法生成, book_id bigint NOT NULL COMMENT 图书ID, buyer_id bigint NOT NULL COMMENT 买家ID, seller_id bigint NOT NULL COMMENT 卖家ID, deal_price decimal(10,2) NOT NULL COMMENT 成交价, status tinyint NOT NULL DEFAULT 0 COMMENT 0-待付款 1-待线下交接 2-已完成 3-已取消, contact_place varchar(255) DEFAULT NULL COMMENT 约见的交接地点, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, pay_time datetime DEFAULT NULL COMMENT 模拟支付时间, finish_time datetime DEFAULT NULL COMMENT 完成时间, cancel_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_buyer_id (buyer_id), KEY idx_seller_id (seller_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单表;这段 SQL 里有几个选型细节值得说明。价格字段用 decimal(10,2) 而不是 double浮点类型在金额计算上会有精度误差答辩时被问到“为什么用 decimal”是个明显的加分点。user 表的 username 加了唯一索引是防止注册接口并发时插入重复账号的兜底手段。图书表故意没建物理外键只保留 seller_id 的普通索引理由有两个物理外键在删除用户时会带来一系列级联约束毕设阶段的业务代码用逻辑外键配合 Service 层校验更灵活而且 MyBatis-Plus 对物理外键没有任何天然支持。2.3 状态字段与数据字典用枚举和常量接口管理图书与订单状态很多人在 Service 层直接写 if (book.getStatus() 0)这个写法本身没问题但项目里一旦有三处以上用到同一个状态值就很容易把 0 和 1 的含义记混。更好的做法是定义常量接口或者枚举把状态的可读性做出来。图书状态我用四个值0 在售、1 交易中有人下单但还没完成交接、2 已售出、3 已下架。订单状态用四个值0 待付款、1 待线下交接、2 已完成、3 已取消。订单状态和图书状态是两套独立的流转逻辑绝不能混在一个字段里。设计时有一条铁律图书状态“交易中”对应的订单必须是“待线下交接”这一步一致性要靠下单接口里的同步更新来保证。public interface BookStatus { int ON_SALE 0; int TRADING 1; int SOLD 2; int OFF_SHELF 3; } public interface OrderStatus { int UNPAID 0; int WAIT_PICKUP 1; int FINISHED 2; int CANCELED 3; }把魔法值提成常量后代码里不会再出现裸奔的 0 和 1而且前端下拉框展示、后端状态过滤、管理员后台统计都引用同一份定义。数据库增删改查写起来也更清晰比如管理员后台要筛选“交易中的图书”SQL 条件就是 book.status 1 而不是靠记忆写 0。如果你时间充裕还可以把这套常量同步维护到前端的 constant.js 里前后端各留一份并在接口返回时直接带回 statusText 文本前端列表页就不需要自己再翻译一遍了。3. SpringBoot 后端实现从 JWT 登录到订单状态机后端部分最核心的三个链路是登录鉴权、图书发布、订单流转。项目骨架推荐直接用 Spring Initializr 生成依赖只加 Web、MySQL 驱动、MyBatis-Plus、Lombok、JWT 工具不去引入 Spring Security。原因很现实Security 的过滤器链配置在小型项目中占比太重调试成本高答辩时也不好三言两语讲清楚而 JWT 拦截器的方式每个接口的鉴权逻辑都是显式的讲解时更有抓手。3.1 项目骨架与依赖一个干净的 SpringBoot 2.7 工程SpringBoot 版本我建议锁在 2.7.x不要一上来就选 3.x。SpringBoot 3 的 javax 包名全部迁移到了 jakarta网上大量现成代码和教程还在用 javax.servlet复制过来直接编译报错。2.7 是 SpringBoot 2 的最后一个大版本稳定性和资料丰富度都最好作为毕设足够。pom.xml 里加上 MyBatis-Plus 和 JWT 依赖注意 MyBatis-Plus 要选择适配 SpringBoot2 的版本dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependencyapplication.yml 里配置数据源和 MyBatis-Plus 的驼峰映射这里有个高频坑MySQL 8 之前的驱动参数和 8.0 之后不一样url 上必须拼接 allowPublicKeyRetrievaltrue否则首次连接会报 Public Key Retrieval is not allowedspring: datasource: url: jdbc:mysql://localhost:3306/campus_book?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltruecharacterEncodingutf8mb4 username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImplserverTimezone 必须配成 Asia/Shanghai不配的话数据库连接的默认时区可能不是东八区后面查询时间数据会差 8 小时。log-impl 配置成 StdOutImpl 会在控制台打印每一条 SQL联调阶段开着排查问题时能直观看到 MyBatis-Plus 生成的语句部署前再关掉。3.2 登录鉴权链路JWT 签发与拦截器放行规则登录接口接收用户名和密码密码用 BCrypt 加密存储登录成功后签发 JWT前端后续请求在请求头里带 Authorization。JWT 工具类封装两个方法generateToken 和 parseToken。Component public class JwtUtil { Value(${jwt.secret}) private String secret; Value(${jwt.expire}) private Long expire; public String generateToken(User user) { return Jwts.builder() .setSubject(user.getUsername()) .claim(userId, user.getId()) .claim(role, user.getRole()) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() expire)) .signWith(SignatureAlgorithm.HS256, secret) .compact(); } public Claims parseToken(String token) { return Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); } }拦截器里做两件事从请求头取 token解析成功就把 userId 和 role 塞进 request 的 attribute 里解析失败或过期直接返回 401。需要放行的路径集中在登录注册和图书公开浏览这两个模块。public class AuthInterceptor extends HandlerInterceptorAdapter { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (OPTIONS.equals(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (token null || !token.startsWith(Bearer )) { response.setStatus(401); return false; } try { Claims claims jwtUtil.parseToken(token.replace(Bearer , )); request.setAttribute(userId, claims.get(userId)); request.setAttribute(role, claims.get(role)); return true; } catch (Exception e) { response.setStatus(401); return false; } } }这段代码里最容易忽略的是 OPTIONS 请求的放行。浏览器跨域场景下前端发 POST 请求时会先发一个 OPTIONS 预检请求这个预检请求不带 Authorization 头如果不放行前端看到的就是 401 而不是真正的业务错误。另一个细节是 token 前缀 Bearer这是一种约定俗成的规范写法前端封装 Axios 时也会加同样的前缀两边的字符串拼接保持一致即可。3.3 图书发布与订单流转状态机至少要管住三个动作图书发布接口相对简单前端表单提交后端插入 book 表status 默认 0。真正考验设计的是下单动作它要同时改订单表和图书表的两条记录必须放在一个事务里。我用状态机的方式管理订单流转核心方法是 createOrder、cancelOrder、completeOrder。Transactional(rollbackFor Exception.class) public Long createOrder(Long bookId, Long buyerId, String contactPlace) { Book book bookMapper.selectById(bookId); if (book null || book.getStatus() ! BookStatus.ON_SALE) { throw new BizException(图书不存在或已被买走); } if (book.getSellerId().equals(buyerId)) { throw new BizException(不能购买自己发布的图书); } Orders order new Orders(); order.setOrderNo(generateOrderNo()); order.setBookId(bookId); order.setBuyerId(buyerId); order.setSellerId(book.getSellerId()); order.setDealPrice(book.getPrice()); order.setStatus(OrderStatus.UNPAID); order.setContactPlace(contactPlace); orderMapper.insert(order); book.setStatus(BookStatus.TRADING); bookMapper.updateById(book); return order.getId(); }下单接口的校验有三个边界图书必须处于在售状态、不能买自己的书、库存由状态字段代替。图书状态从在售改成交易中是一个临界操作并发场景下两个买家同时下单同一本书理论上都应该校验通过但实际会出现超卖。要彻底解决得用乐观锁或者 SELECT FOR UPDATE我在 book 表里加了一个 version 字段做乐观锁update 时带上 version 条件但毕设阶段如果讲解压力不大这一步可以作为加分项写在论文里代码里先保证事务一致性即可。订单支付接口我做成模拟支付不接任何真实支付渠道接口逻辑是把订单状态从待付款改成待线下交接。取消订单和完成订单同理cancelOrder 只允许卖家或买家操作completeOrder 则把订单状态改成已完成的同时把对应图书状态改成已售出。3.4 分页查询与模糊检索的参数设计图书列表是前端访问量最大的接口支持按书名模糊搜索、按分类筛选、按价格区间筛选、按成色筛选还要分页。MyBatis-Plus 的 Page 对象配合 LambdaQueryWrapper 能省掉大量样板代码。public PageBook pageBooks(int pageNum, int pageSize, String keyword, Long categoryId, Integer minPrice, Integer maxPrice, Integer quality) { PageBook page new Page(pageNum, pageSize); LambdaQueryWrapperBook wrapper new LambdaQueryWrapper(); wrapper.eq(Book::getStatus, BookStatus.ON_SALE) .like(StringUtils.hasText(keyword), Book::getTitle, keyword) .eq(categoryId ! null, Book::getCategoryId, categoryId) .ge(minPrice ! null, Book::getPrice, minPrice) .le(maxPrice ! null, Book::getPrice, maxPrice) .eq(quality ! null, Book::getQuality, quality) .orderByDesc(Book::getCreateTime); return bookMapper.selectPage(page, wrapper); }条件构造器的每个 eq 和 like 都带一个前置布尔参数这个参数是 MyBatis-Plus 的特色当前置条件为 false 时该条件自动不拼接。前端不用在 Controller 里写一堆 if else 判断参数是否为空直接传参即可代码干净很多。返回的分页对象里包含总数、当前页数据、总页数前端表格组件直接消费。4. Vue 前端与接口联调路由、Axios 与图片上传前端用 Vue 3 Vite Element Plus 是当前主流组合Vue 3 的组合式 API 让业务逻辑的组织比 Vue 2 清晰不少而且 Element Plus 组件库对表单、表格、弹窗的覆盖很完整能省下大量样式调试时间。整体页面结构分成三块游客可见区、用户中心区、管理后台区。4.1 页面结构与路由设计登录后动态挂载用户中心路由表用静态路由加动态路由的组合。静态路由包含首页、图书列表、图书详情、登录注册页动态路由在登录成功后根据角色字段追加用户中心、发布图书、订单管理、后台管理这些页面。之所以不在静态路由表里全写死是为了让未登录用户直接访问 /order 时能被路由守卫拦下来。const router createRouter({ history: createWebHashHistory(), routes: [ { path: /, component: Home, name: home }, { path: /books, component: BookList, name: bookList }, { path: /book/:id, component: BookDetail, name: bookDetail }, { path: /login, component: Login, name: login } ] }); const userRoutes [ { path: /publish, component: PublishBook, name: publishBook, meta: { requiresAuth: true } }, { path: /my-orders, component: MyOrders, name: myOrders, meta: { requiresAuth: true } }, { path: /my-books, component: MyBooks, name: myBooks, meta: { requiresAuth: true } } ]; router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.meta.requiresAuth !token) { next(/login); } else { next(); } });路由模式这里有两个选择createWebHashHistory 和 createWebHistory。前者 URL 里带 #后者是 history 模式URL 干净。但 history 模式有个大坑后端不做路由回退时刷新页面会 404。为了避免部署时的额外配置我的建议是毕设阶段直接用 hash 模式省下的时间去调业务逻辑更划算如果坚持用 history 模式后端必须配一个转发到 index.html 的接口这个坑放到下一章细说。4.2 Axios 封装与 Token 携带拦截器里完成优雅登录Axios 实例的封装是前端联动后端的第一步。统一配置 baseURL、请求超时时间请求拦截器里把 token 塞进请求头响应拦截器里统一处理 401 和业务错误码。每次接口调用都手动写一遍 token 处理既容易漏又不好维护。const request axios.create({ baseURL: /api, timeout: 10000 }); request.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers.Authorization Bearer ${token}; } return config; }); request.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.data; }, error { if (error.response error.response.status 401) { localStorage.removeItem(token); localStorage.removeItem(userInfo); router.push(/login); } ElMessage.error(网络请求失败请稍后重试); return Promise.reject(error); } );baseURL 配成 /api 而不是完整地址是为了配合 Vite 的 proxy 配置。开发环境下所有请求都会先打到前端开发服务器的 /api 路径再由 Vite 代理转发到后端的 8080 端口生产环境下前端打包产物由 SpringBoot 托管/api 直接命中后端天然同源。这种设计让开发和生产环境的前端代码零改动。后端 Controller 的 RequestMapping 也需要统一加 /api 前缀前后端对路径的约定保持一致。4.3 图片上传与跨域代理本地与打包后的差异图书封面上传用 Element Plus 的 el-upload 组件接口指向后端的 /api/file/upload。后端接收 MultipartFile把文件写到本地磁盘的上传目录数据库里只存访问的相对路径。这里不建议把图片转成 base64 存数据库文件越来越大时数据库会变成灾难。el-upload action/api/file/upload :headersuploadHeaders :on-successhandleUploadSuccess :show-file-listfalse acceptimage/jpeg,image/png,image/webp el-button上传封面/el-button /el-uploadel-upload 的 action 是上传地址headers 必须带上 Authorization否则上传请求会被后端的 JWT 拦截器拦下来。这个细节常被忽略很多人前端调试时发现列表接口正常、上传接口 401就是忘了传 token 头。后端 FileController 里保存文件时要注意两点存储目录用配置项而不是硬编码路径文件名用 UUID 重命名防止中文文件名乱码和路径穿越。开发环境的跨域代理配置在 vite.config.js 里export default defineConfig({ plugins: [vue()], server: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } });用了 proxy 之后前端开发环境不会产生真正的跨域请求浏览器的 Network 面板里请求地址是 localhost:3000/api/xxx由 Vite 内部转发到 8080。这种情况下后端不需要额外配置 CORS如果后端配了 CORS 又同时开 proxy部分浏览器会出现 OPTIONS 预检请求被重复处理的怪问题建议二选一。4.4 Vue 环境配置与依赖安装的常见细节Vue 项目初始化用 npm create vite 命令选择 Vue 模板后进入目录执行 npm install 安装依赖。国内网络环境下建议先给 npm 配好镜像源否则安装 Element Plus 和 Axios 时会卡在下载阶段。安装完依赖后有几个常见的启动问题一是 Node 版本过低导致 Vite 启动报错Vite 5 需要 Node 18 以上二是 element-plus 按需导入配置没做全组件显示异常三是 .env.development 文件里没有配环境变量。基础依赖安装命令npm install element-plus axios vue-router4 pinia按需导入 Element Plus 需要装 unplugin-auto-import 和 unplugin-vue-components 两个插件并在 vite.config.js 里注册。嫌麻烦的话可以全量引入import ElementPlus from element-plus加上app.use(ElementPlus)毕设项目打包体积多几百 KB 无所谓全量引入配置最稳。路由和状态管理库的版本要选对Vue 3 对应的是 vue-router 4.x 和 pinia装成 vue-router 3 会在启动时报各种奇怪错误。5. 高分毕设避坑排查数据库、打包与 SpringBoot 版本相关的 5 个坑这一章说几个我和学生做项目过程中真实踩过、且网上提问频率最高的坑。每一条都按现象、原因、解决的顺序写方便对应排查。5.1 坑位一MySQL 8 连接时报 Public Key Retrieval is not allowed现象是启动 SpringBoot 项目后第一次请求数据库相关接口直接 500控制台报错Public Key Retrieval is not allowed。这个报错常见于 MySQL 8 的 caching_sha2_password 认证插件客户端与服务器首次握手时需要用 RSA 公钥加密密码默认情况下驱动不允许自动获取公钥。解决办法很简单在数据库连接 URL 上加 allowPublicKeyRetrievaltrue 参数。我见过有人为了解决这个报错把 MySQL 的认证方式改回 mysql_native_password这属于绕路而且在新版本 MySQL 里越来越不推荐。顺带把 useSSLfalse 也加上本地开发环境没有 SSL 证书再加 serverTimezoneAsia/Shanghai这三个参数一次配全后面少很多事。5.2 坑位二SpringBoot 版本太高导致 javax 包全部报红现象是网上找的登录拦截器、文件上传代码复制到自己项目里import javax.servlet 这一行直接提示找不到包。原因是 SpringBoot 3.x 把 Jakarta EE 规范里的 javax.* 包全部迁移到了 jakarta.*Servlet API、注解、认证相关接口全是如此。如果你非要用最新版 SpringBoot 3把所有的 javax.servlet 改成 jakarta.servlet注意不只是 import 语句有些内部调用的静态方法也要跟着换。我更建议直接选 SpringBoot 2.7.x这个版本技术成熟、教程存量最大、MyBatis-Plus 和一些老依赖的兼容性最好等毕设做完有余力再研究 3.x 的迁移也不迟。springboot 版本太高这个坑每年答辩前都会有一批人栽在上面。5.3 坑位三Vue 打包放进 SpringBoot 后刷新页面 404现象是前端本地开发一切正常npm run build后把 dist 目录拷进 SpringBoot 的 resources/static 下访问首页正常但进入 /book/1 详情页后按 F5 刷新浏览器直接白屏控制台显示 404。原因是前端用了 history 模式路由刷新时浏览器向 SpringBoot 请求 /book/1 这个路径后端没有这个地址的资源返回 404。三种解法任选一种前端改成 hash 模式路由URL 里带个 # 号刷新时不会触发后端路由或者后端加一个路由回退 Controller把非 /api 开头的路径全部转发到 index.html再或者用 Nginx 托管前端静态资源并配置 try_files。毕设项目我推荐第一种改动最小一行代码。5.4 坑位四日期字段查询出来比实际时间差 8 小时现象是后端数据库里 create_time 存的是北京时间但前端页面显示的时间慢 8 个小时。原因有两层数据库连接 URL 里没配 serverTimezone驱动用默认时区连接或者后端 Jackson 序列化时用了 UTC 时区。解决方法是两端都锁定东八区数据库 URL 里加 serverTimezoneAsia/ShanghaiSpringBoot 配置文件里加spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8这里要提醒date-format 控制的是格式time-zone 控制的是时区两个都要配。只配格式不配时区展示的仍然是 UTC 时间。排查这个问题的技巧是先把 MyBatis 的控制台 SQL 打印打开看 SQL 里的时间参数是否正常再判断是数据库层的问题还是序列化层的问题不要一上来就改前端。5.5 坑位五文件上传路径在 Windows 能跑部署到 Linux 就失败现象是本地 Windows 开发上传图片一切正常打包部署到 Linux 服务器后上传报错或者上传成功但图片访问不到。原因很直接代码里把保存路径写成了D:/upload/这种硬编码或者用字符串拼接了File.separator换系统后路径不存在也创建不了目录。解决方法是把上传路径做成配置项在 application.yml 里定义file.upload-dir启动时检查目录是否存在不存在就自动创建。Component public class FileStorageConfig implements ApplicationRunner { Value(${file.upload-dir}) private String uploadDir; Override public void run(ApplicationArguments args) { File dir new File(uploadDir); if (!dir.exists()) { dir.mkdirs(); } } }同时注意 Linux 下 SpringBoot 进程对目录要有写权限部署时建议把上传目录放在 /home/ 或者 /data/ 这类非 root 目录下不要直接丢在 /root 里否则权限问题够折腾一阵子。图片访问映射也得配好SpringBoot 需要把 /upload/** 路径映射到本地磁盘目录不然前端拿到相对路径也加载不出图片。6. 让方案能跑给老师看全链路验收与演示数据设计系统做完后花半小时准备一套能撑住全场演示的数据和操作路径远比临场随便点来得稳。我习惯用两个账号、五本图书、三笔订单把整个核心链路串起来。演示时先打开首页展示图书列表和搜索筛选接着用买家账号登录搜“高数”找到一本在售的书点进详情下单此时订单状态变成待付款然后切换卖家账号确认收到付款提示把订单推到待线下交接再回买家账号确认收货订单完成回到图书列表能看到这本书状态已经变成已售出。这套路径把用户注册、图书发布、搜索、下单、支付模拟、订单流转、图书状态同步全部覆盖而且每一步都有界面变化可以展开讲。验收时我习惯过一遍关键检查点订单号是否为 20 位左右的不重复编号同一个买家反复点击下单按钮是否只会生成一笔订单卖家能不能下单自己的图书被禁用用户是否能正常登录图书列表页的分页参数是否在地址栏同步。这些细节是答辩时最容易被人钻空子的地方。我自己的教训是遥控器测试永远发现不了问题真拿两台设备同时操作一下或者打开两个浏览器窗口对同一本书轮流下单才能看到状态机写得到底对不对。打包部署时记得后端先改数据库账号密码和上传路径前端确认接口代理路径然后npm run build把 dist 里的文件复制到后端 resources/static 目录再mvn clean package打成一个 jar。运维只认识一个 jar 包这本身就是这套架构最直观的优势。希望这篇拆解能帮你把每一步走实少踩几个我已经替你踩过的坑。本文还有配套的精品资源点击获取
返回列表