
简介基于Spring Boot与Vue的校园二手商城系统完整源码包面向JavaWeb学习者、毕业设计与课程设计人群提供了一个典型的前后端分离校园交易平台实现方案。项目采用Spring Boot、Vue.js与MySQL构建涵盖二手商品发布、浏览、交易管理等核心场景可帮助理解RESTful接口设计、Vue组件化开发、Maven项目组织以及Lombok简化实体类等实用技巧。资源共609个文件压缩包约7.9MB包含Java后端源码、Vue前端页面、SQL数据库脚本、Maven及yml配置其中87个Java文件承担业务逻辑130余个JavaScript配合CSS、HTML、SCSS/LESS等构建前端交互与页面样式图片和gif演示素材便于核对界面效果与操作流程另含layui、font-awesome等常见样式库资源。目前已有5345人学习下载。通过该包可完成本地环境部署对照源码梳理用户登录、商品上架、订单管理等模块的编码思路借助SQL脚本快速初始化数据库整体目录结构清晰适合需要完整校园二手交易课题方案的读者直接参考与二次开发。1. 从校园二手商城到 SpringbootVue 的前后端分离落地把校园二手商城拆开看最容易被低估的是订单状态和用户身份谁在卖、谁在买、交易走到哪一步这些比页面样式重要得多。用 SpringbootVue 来落地本质上是让后端只提供 REST API前端只负责渲染和交互两边通过 JSON 通信。这个组合非常适合课程设计、毕业设计也适合想在正式做小程序之前先跑通 Web 原型的校内项目。后端要管的是登录令牌、商品生命周期、订单确认前端要管的是路由、表单、商品列表。两条线可以并行开发最后合并部署这就是最常见的 Spring Boot Vue 前后端分离项目形态。2. 先把骨架立起来Spring Boot 后端分层与 Vue 前端工程2.1 为什么选前后端分离而不是模板渲染传统做法是用 Thymeleaf 或 JSP 在后端直接渲染 HTML。对于只有几个页面的管理后台这种方式写起来很快但校园二手商城有比较强的交互性用户要刷新商品列表、发布商品后立即看到新状态、在订单列表里切换标签页。这些操作一旦全部改成 Ajax后端模板就会慢慢变成一堆th:each加th:if的混合体接口和页面耦合在一起改动成本会变高。前后端分离后Spring Boot 只暴露/api/**接口Vue 通过 Axios 拿到数据再渲染组件。前端可以单独用 mock 数据调试后端用 Postman 或 Swagger 调试接口两个任务的边界很清楚。代价是多了跨域、登录态透传、打包部署等步骤这也是后文要解决的几个关键点。2.2 后端工程的最小目录结构创建 Spring Boot 项目时版本选择要留意。如果 JDK 是 8 或 11建议直接选 Spring Boot 2.7.x如果是 Seata、Sa-Token 这类依赖对 3.x 还不完全熟悉的场景也先别急着升版本。工程内按下面的方式分层基本够一个校园二手商城使用src/main/java/com/campus/market ├── MarketApplication.java ├── config │ └── WebConfig.java # 跨域配置、拦截器注册 ├── controller │ ├── AuthController.java │ ├── ProductController.java │ └── OrderController.java ├── entity │ ├── User.java │ ├── Product.java │ └── Order.java ├── mapper │ ├── UserMapper.java │ ├── ProductMapper.java │ └── OrderMapper.java ├── service │ ├── AuthService.java │ ├── ProductService.java │ └── OrderService.java └── common ├── Result.java # 统一返回体 └── JwtUtil.java # 登录令牌生成与校验这个结构里entity对应数据库表mapper只做数据访问service写业务规则controller只做参数接收和路由分发。最容易犯的错误是让 Controller 直接操作 Mapper等后面加了库存、订单状态流转时相同的代码要在多个接口里复制。按层拆好后改业务逻辑时只需要定位到对应的 service 方法排查问题也能少花一半时间。2.3 Vue 前端工程初始化与依赖配置Vue 侧我习惯用 Vite 创建 Vue 3 项目。先跑初始化命令npm create vitelatest campus-market -- --template vue cd campus-market npm install npm install vue-router4 axios pinia npm run dev第一条命令会生成一个带 Vite 配置的 Vue 3 工程npm install安装基础骨架的依赖vue-router负责路由axios负责发 HTTP 请求pinia负责跨组件共享登录态和用户信息。npm run dev启动后前端默认跑在 5173 端口Spring Boot 默认跑在 8080 端口联调之前必须先把第 5 章要讲的跨域转发配置写对。校园二手商城的技术选型可以归纳成下面这张表模块技术选型说明后端框架Spring Boot 2.7提供 REST API内嵌 TomcatORMMyBatis-Plus简化单表 CRUD分页插件成熟数据库MySQL 8.0 / H2开发阶段可用 H2部署用 MySQL前端框架Vue 3 Vite组合式 API开发启动快路由Vue Router 4管理页面跳转与下单参数传递状态管理Pinia保存 token、用户信息等全局状态HTTP 客户端Axios统一处理请求头与异常码3. Spring Boot 后端核心接口与数据模型从表结构到可复现的业务逻辑3.1 二手商城最少需要哪几张表一个没有在线支付、没有购物车结算的校内二手商城最少需要三张表用户表、商品表、订单表。商品图片可以放在商品表的image_url字段里先不要单独建图片表如果需要多图再加关联表。下面是推荐的表结构CREATE TABLE user ( id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, nickname VARCHAR(50), phone VARCHAR(20), avatar VARCHAR(255), role TINYINT DEFAULT 0, -- 0 普通用户1 管理员 created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE product ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL, title VARCHAR(100) NOT NULL, description TEXT, price DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 0, -- 0 在售1 已售出2 下架 image_url VARCHAR(500), created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE biz_order ( id BIGINT AUTO_INCREMENT PRIMARY KEY, product_id BIGINT NOT NULL, buyer_id BIGINT NOT NULL, seller_id BIGINT NOT NULL, price DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 0, -- 0 待确认1 交易完成2 已取消 created_at DATETIME DEFAULT CURRENT_TIMESTAMP );biz_order取这个名字是为了避免和 SQL 保留字order冲突。订单表里的price字段在创建订单时从商品表拷贝一份而不是下单后实时去商品表读取。这样即使卖家在交易期间修改了价格订单里保存的仍然是买家下单时看到的金额。状态字段用TINYINT并加注释比直接存中文状态更好扩展。3.2 用 MyBatis-Plus 写仓储层实体类加上TableName注解后Mapper 接口继承BaseMapper就能获得基础的增删改查。商品实体类是这样Data TableName(product) public class Product { private Long id; private Long userId; private String title; private String description; private BigDecimal price; private Integer status; private String imageUrl; private LocalDateTime createdAt; }Mapper 接口定义一个带关键词和状态筛选的分页方法Mapper public interface ProductMapper extends BaseMapperProduct { IPageProduct selectPageByKeyword(IPageProduct page, Param(keyword) String keyword, Param(status) Integer status); }对应的 XML 文件里写动态 SQLselect idselectPageByKeyword resultTypecom.campus.market.entity.Product SELECT * FROM product where if testkeyword ! null and keyword ! AND (title LIKE CONCAT(%, #{keyword}, %) OR description LIKE CONCAT(%, #{keyword}, %)) /if if teststatus ! null AND status #{status} /if /where ORDER BY created_at DESC /select这段 SQL 的关键点在于where标签会自动处理条件前的AND。如果只传status不传keyword生成的语句是SELECT * FROM product WHERE status 0 ORDER BY created_at DESC。分页插件会拦截带有IPage参数的方法自动追加LIMIT所以方法里不需要手写LIMIT。3.3 商品发布、浏览与订单状态机的接口设计Controller 层保持薄的状态只做三件事接收参数、调用 service、返回统一结果。统一返回体Result的结构是{ code, message, data }。商品相关的接口如下RestController RequestMapping(/api/product) public class ProductController { Autowired private ProductService productService; PostMapping public ResultLong createProduct(RequestBody Valid ProductVO vo) { Long productId productService.create(vo); return Result.success(productId); } GetMapping(/{id}) public ResultProductVO detail(PathVariable Long id) { return Result.success(productService.getDetail(id)); } PostMapping(/{id}/off) public ResultVoid offShelf(PathVariable Long id) { productService.offShelf(id); return Result.success(); } }ProductVO是视图对象与实体类分开避免把userId、createdAt这类字段直接暴露给前端。前端提交商品时只需要传title、description、price、imageUrl后端从登录态里取当前用户 id 并填入实体这是一定要在 service 里完成的。订单状态流转是整个系统最容易失控的地方。常见做法是让订单状态只允许特定方向变化待确认 0 → 交易完成 1或 0 → 已取消 2。用 switch 把逻辑收拢在一个方法里public void updateStatus(Long orderId, Integer targetStatus) { Order order orderMapper.selectById(orderId); if (order null) { throw new BusinessException(订单不存在); } switch (order.getStatus()) { case 0: if (targetStatus 1 || targetStatus 2) { order.setStatus(targetStatus); orderMapper.updateById(order); } break; case 1: case 2: throw new BusinessException(订单已结束不能变更); } }这样可以保证前端不能随便传一个status值就把订单改成任意状态。商品上架、下架、售出之间的联动也要放在同一个 service 方法里。比如买家下单成功后商品状态要立刻从在售改成已售出否则同一件商品会被两个人同时下单这一步建议用数据库事务包住。4. Vue 页面与 API 联调登录态、商品列表和下单环境4.1 用 Axios 统一封装请求把 Spring Boot 接口接进来前端所有对后端的请求最好都走同一个 Axios 实例这样可以在拦截器里统一加 token、统一处理登录失效。新建src/api/request.jsimport axios from axios import { useUserStore } from /stores/user import router from /router const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const userStore useUserStore() if (userStore.token) { config.headers.Authorization Bearer ${userStore.token} } return config }) request.interceptors.response.use( res { const body res.data if (body.code 401) { router.push(/login) } return body }, err { return Promise.reject(err) } ) export default requestbaseURL设为/api开发时通过 Vite 的server.proxy配置把/api开头的请求转发到 8080 端口部署时由 Nginx 做同样的路径映射。请求拦截器从 Pinia 里读 token放到Authorization请求头响应拦截器统一处理code。这里要特别注意不要在每次请求前手动写headers: { Authorization: ... }一旦在某个页面漏掉登录态就会静默失效。4.2 商品列表与详情页的前端状态处理商品列表页用 Vue 3 的组合式 API 写核心逻辑是页面加载时请求第一页滚动到底部时追加下一页。下面的代码放在script setup中script setup import { ref, onMounted } from vue import { getProductPage } from /api/product const list ref([]) const page ref(1) const pages ref(1) const loading ref(false) async function loadProducts() { if (loading.value) return loading.value true const res await getProductPage({ page: page.value, size: 10 }) list.value.push(...res.data.records) pages.value res.data.pages page.value 1 loading.value false } onMounted(() { loadProducts() }) /script合并列表时用push(...res.data.records)而不是list.value res.data.records这样才能在翻页场景下保留之前已经渲染的商品。pages字段表示总页数页面底部可以判断page pages时显示没有更多了用来阻止无意义的重复请求。每个商品卡片上用router.push跳到详情页路由参数就带上idrouter.push({ path: /product/${item.id} })在详情页里用route.params.id接收再请求/api/product/{id}接口。4.3 下单与我的订单跨组件数据流校园二手商城的下单不需要购物车。商品详情页里一个联系购买按钮点击后带着productId和商品价格创建订单。这个流程里productId通过路由参数传递当前用户信息和 token 存在 Pinia 中。页面刷新后要从localStorage恢复 Pinia 里的用户信息否则刷新页面登录态会丢。订单列表页面要区分买家视角和卖家视角后端可以分别提供两个接口页面入口后端接口说明买家订单GET /api/order/buyer当前用户作为 buyer_id 的订单卖家订单GET /api/order/seller当前用户作为 seller_id 的订单前端把状态码翻译成文字0是等待卖家确认1是交易完成2是已取消。卖家端对0显示确认交易按钮买家端对1显示确认完成按钮。不要在前端直接维护订单状态逻辑状态机必须放在后端前端只做展示和动作触发。5. Spring Boot Vue 的部署验证与 3 个容易踩的坑5.1 Vite 开发服务器的跨域转发配置开发环境不需要 CORS 注解更省事的是让 Vite 把/api开头的请求转发到后端。在vite.config.js中写入export default defineConfig({ server: { host: 0.0.0.0, port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })配置完成后重启npm run dev前端代码里请求/api/product/1时开发服务器会转发到http://localhost:8080/api/product/1。浏览器控制台不再报跨域错误也不需要在 Spring Boot 里额外放开所有跨域来源。5.2 完整验证一条交易链路后端启动MarketApplication前端启动 Vite 后按下面顺序验证注册一个用户 A再注册用户 B用户 A 发布一件商品设置价格和图片用户 A 退出登录用户 B在列表页搜索商品标题并打开详情用户 B 下单订单状态变成待确认重新登录用户 A进入卖家订单列表完成交易查看数据库中product表的状态是否已经从 0 变成 1。如果第 4 步商品状态没有被同步改成已售出说明下单事务没有包住商品更新要在OrderService的createOrder方法上加Transactional。5.3 三个让新手卡住的点第一个是 Spring Boot 3.x 的包名变化。Spring Boot 3 把javax.servlet换成了jakarta.servlet很多老教程里的代码直接复制会编译失败。解决方案是使用 2.7.x 起步或者把所有javax的 import 改成jakarta。第二个是 Vue Router 的 history 模式刷新 404。使用createWebHistory后前端路由就不再是真实的文件路径线上部署时如果只配置静态资源映射刷新/product/3会返回 404。Nginx 里需要加try_files $uri $uri/ /index.html;让前端路由回退到首页。第三个是 MyBatis-Plus 分页不生效。只引入mybatis-plus-boot-starter还不够必须注册分页拦截器Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }没有这个配置时selectPage返回的分页对象里records是全部数据pages计算也错误。加了拦截器之后SQL 才会自动带上LIMIT同时这个配置对 Spring Boot 2.7 和 3.x 都适用。本文还有配套的精品资源点击获取