校园二手交易系统实战开发全记录(vue+SpringBoot+MySQL)

校园二手交易系统实战开发全记录(SpringBoot+Vue+MySQL)

github地址:点击此处进入项目

去年,我做了一个校园二手交易系统,今天给大家,分享一下项目细节

文章目录

一、项目概述与技术栈选型

1. 项目核心需求

本项目实现极简版二手交易系统,聚焦用户注册、商品发布、订单创建三大核心模块,完成从前端页面交互到后端数据持久化的全链路闭环,具体需求如下:

  • 用户模块:支持用户注册,实现密码加密存储,保障用户信息安全;
  • 商品模块:支持商品发布,维护商品"在售/已售出"状态,关联卖家信息;
  • 订单模块:支持订单创建,自动关联商品、买家、卖家信息,创建订单时同步更新商品状态。

2. 技术栈选型

技术栈选取主流、易用、面试高频的组合,兼顾开发效率与学习价值:

技术分层 技术选型 选型理由
后端框架 SpringBoot + MyBatis-Plus SpringBoot快速搭建后端项目,简化配置;MyBatis-Plus高效实现CRUD操作,减少手写SQL工作量
前端框架 Vue2 + ElementUI Vue2生态成熟、上手成本低;ElementUI提供丰富组件,快速搭建美观实用的前端页面
数据库 MySQL 关系型数据库,完美适配用户、商品、订单的关联数据存储场景
其他工具 Axios 前端发送HTTP请求,实现前后端数据交互;Lombok简化后端实体类代码

二、核心模块开发与实战踩坑

本部分聚焦开发过程中遇到的典型问题、原因分析、解决方案及核心代码,直击新手开发痛点。

(一)后端核心开发与坑点解决

坑1:@RequestBody参数解析失败(400 Bad Request)
  • 现象:前端调用用户注册接口,后端返回400错误,控制台提示"Required request body is missing"。

  • 原因 :前端请求未设置Content-Type: application/json,导致后端无法识别JSON格式的请求体;或前端参数传递方式错误(如用URL拼接而非JSON请求体)。

  • 解决方案

    1. 前端配置Axios请求头,指定数据格式为JSON;
    2. 后端接口使用@RequestBody注解接收参数,确保前后端参数名一致。
  • 核心代码
    后端用户注册接口(UserController.java):

    java 复制代码
    @RestController
    @RequestMapping("/api/user")
    @CrossOrigin  // 解决跨域问题
    public class UserController {
    
        @Autowired
        private UserService userService;
    
        @PostMapping("/register")
        public Result<?> register(@RequestBody User user) {
            boolean success = userService.register(user);
            return success ? Result.success("注册成功") : Result.error("注册失败");
        }
    }

    前端Axios全局配置(main.js):

    javascript 复制代码
    import axios from 'axios';
    Vue.prototype.$axios = axios.create({
        baseURL: 'http://localhost:8080/api',
        timeout: 5000,
        headers: {
            'Content-Type': 'application/json;charset=utf-8'
        }
    });
坑2:数据库字段映射错误(Unknown column 'xxx')
  • 现象:发布商品接口调用时,后端报500错误,提示"Unknown column 'image_url' in 'field list'"。

  • 原因 :后端实体类定义了imageUrl字段(驼峰命名),但数据库goods表未创建对应的image_url字段,MyBatis-Plus默认开启驼峰命名转下划线,导致字段映射失败。

  • 解决方案 :建表时补全所有实体类对应的字段,确保实体类驼峰字段 ↔ 数据库下划线字段一一对应。

  • 核心代码goods表创建SQL

    sql 复制代码
    CREATE TABLE `goods` (
      `id` BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '商品ID',
      `title` VARCHAR(255) NOT NULL COMMENT '商品标题',
      `price` DECIMAL(10,2) NOT NULL COMMENT '商品价格',
      `description` TEXT COMMENT '商品描述',
      `image_url` VARCHAR(255) COMMENT '商品图片URL',
      `user_id` BIGINT NOT NULL COMMENT '卖家ID',
      `status` TINYINT DEFAULT 0 COMMENT '0-在售 1-已售出',
      `create_time` DATETIME DEFAULT NOW() COMMENT '创建时间'
    ) COMMENT '商品表';
坑3:数据库表名冲突(MySQL关键字)
  • 现象:创建订单接口报错,SQL执行提示"near 'order ( goods_id, user_id...' at line 1"。

  • 原因 :订单表名定义为order,而order是MySQL关键字(用于排序操作),直接使用会导致SQL语法错误。

  • 解决方案 :表名添加前缀(行业通用规范),将order表改为tb_order,同时修改实体类@TableName注解指定表名。

  • 核心代码 :订单实体类(Order.java)

    java 复制代码
    import com.baomidou.mybatisplus.annotation.TableName;
    import lombok.Data;
    
    @Data
    @TableName("tb_order")  // 指定数据库表名
    public class Order {
        @TableId(type = IdType.AUTO)
        private Long id;
        private Long goodsId;   // 关联商品ID
        private Long userId;    // 买家ID
        private Long sellerId;  // 卖家ID
        private BigDecimal price; // 订单价格
        private Integer status; // 0-待付款
    }
坑4:用户名重复注册(唯一索引冲突)
  • 现象:重复注册同一用户名,后端报500错误,提示"Duplicate entry 'xxx' for key 'uk_username'"。

  • 原因 :数据库user表的username字段设置了唯一索引,保证用户名唯一性,但后端代码未提前校验,直接插入导致触发数据库约束报错。

  • 解决方案:后端注册逻辑中,先查询用户名是否存在,存在则直接返回失败,避免触发数据库异常。

  • 核心代码 :用户注册业务逻辑(UserServiceImpl.java)

    java 复制代码
    @Service
    public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
    
        @Override
        public boolean register(User user) {
            // 1. 用户名非空校验
            String username = user.getUsername().trim();
            if (username.isEmpty()) {
                return false;
            }
            // 2. 校验用户名是否已存在
            QueryWrapper<User> queryWrapper = new QueryWrapper<>();
            queryWrapper.eq("username", username);
            if (this.count(queryWrapper) > 0) {
                return false;
            }
            // 3. 密码MD5加密
            user.setPassword(PasswordUtil.encrypt(user.getPassword().trim()));
            // 4. 保存用户
            return this.save(user);
        }
    }

(二)前端核心开发与坑点解决

坑1:按钮点击无响应(submit事件失效)
  • 现象:点击"发布商品""创建订单"按钮,前端无任何反应,浏览器Network面板无请求记录。

  • 原因 :使用ElementUI的el-formel-button,直接绑定submit事件时,el-form是封装组件,并非原生form标签,导致事件触发失效。

  • 解决方案 :将按钮触发方式改为@click,直接调用对应方法,避开组件事件兼容问题。

  • 核心代码 :前端发布商品按钮(App.vue)

    html 复制代码
    <el-tab-pane label="发布商品" name="publishGoods">
      <el-form :model="goodsForm" label-width="80px">
        <el-form-item label="商品标题">
          <el-input v-model="goodsForm.title"></el-input>
        </el-form-item>
        <el-form-item label="商品价格">
          <el-input v-model="goodsForm.price" type="number"></el-input>
        </el-form-item>
        <el-form-item label="卖家ID">
          <el-input v-model="goodsForm.userId" type="number"></el-input>
        </el-form-item>
        <!-- 关键:用@click触发方法 -->
        <el-form-item>
          <el-button type="primary" @click="handlePublishGoods">发布商品</el-button>
        </el-form-item>
      </el-form>
    </el-tab-pane>
坑2:提示文案矛盾("发布失败:发布成功")
  • 现象:操作成功后,前端弹出错误提示"发布失败:发布成功",逻辑与提示完全矛盾。

  • 原因 :前端响应判断逻辑错误,将后端返回的code=200(成功状态)误判为失败,同时错误拼接了成功的响应信息。

  • 解决方案 :统一响应判断标准------code=200时弹出成功提示,否则弹出失败提示,明确区分msg(通用信息)和data(业务信息)。

  • 核心代码 :前端发布商品方法(App.vue)

    javascript 复制代码
    async handlePublishGoods() {
        // 非空校验
        if (!this.goodsForm.title || this.goodsForm.price <= 0 || this.goodsForm.userId <= 0) {
            this.$message.error("标题、价格、卖家ID不能为空!");
            return;
        }
        try {
            const res = await this.$axios.post("/goods/publish", this.goodsForm);
            // 正确判断逻辑
            if (res.data.code === 200) {
                this.$message.success("发布商品成功!");
                this.goodsForm = { title: "", price: 0, description: "", userId: 0 };
            } else {
                this.$message.error("发布失败:" + res.data.msg);
            }
        } catch (err) {
            this.$message.error("发布出错:" + (err.response?.data?.msg || err.message));
        }
    }
坑3:手机号长度超限(Data truncation错误)
  • 现象:注册时输入过长手机号,后端报500错误,提示"Data too long for column 'phone'"。

  • 原因 :数据库phone字段长度设置为VARCHAR(11),但前端未做长度校验,输入超过11位的手机号导致数据插入失败。

  • 解决方案:前端添加手机号格式校验,限制输入为11位纯数字,从源头拦截非法数据。

  • 核心代码 :前端手机号校验逻辑

    javascript 复制代码
    async handleRegister() {
        // 手机号格式校验
        if (this.registerForm.phone && !/^1[3-9]\d{9}$/.test(this.registerForm.phone)) {
            this.$message.error("请输入11位有效手机号!");
            return;
        }
        // 后续注册逻辑...
    }

三、全链路功能验证

1. 验证步骤

  1. 用户注册 :前端填写用户名、密码、手机号,点击注册 → 后端加密密码并保存 → 数据库user表新增记录;
  2. 商品发布 :填写商品标题、价格、卖家ID,点击发布 → 数据库goods表新增记录,状态为0(在售)
  3. 订单创建 :填写商品ID、买家ID,点击创建 → 数据库tb_order表新增记录,同时goods表对应商品状态更新为1(已售出)

2. 数据库验证结果

  • user表(部分数据)

    sql 复制代码
    +----+----------+----------------------------------+-------------+
    | id | username | password                         | phone       |
    +----+----------+----------------------------------+-------------+
    |  5 | test9999 | e10adc3949ba59abbe56e057f20f883e | 13800138000 |
    +----+----------+----------------------------------+-------------+
  • goods表(部分数据)

    sql 复制代码
    +----+-------+---------+-------------+---------+--------+
    | id | title | price   | description | user_id | status |
    +----+-------+---------+-------------+---------+--------+
    |  2 | myObj | 20.00   | test desc   | 1001    | 1      |
    +----+-------+---------+-------------+---------+--------+
  • tb_order表(部分数据)

    sql 复制代码
    +----+----------+---------+-----------+-------+--------+
    | id | goods_id | user_id | seller_id | price | status |
    +----+----------+---------+-----------+-------+--------+
    |  2 |        2 |       1 |      1001 | 20.00 | 0      |
    +----+----------+---------+-----------+-------+--------+

四、项目优化方向(面试加分项)

1. 基础优化(快速落地)

  • 接口参数校验 :引入JSR303注解(@NotBlank@NotNull@Pattern),在实体类中添加校验规则,替代手动if判断;
  • 全局异常处理 :自定义GlobalExceptionHandler,统一捕获SQL异常、参数异常等,返回标准化错误信息;
  • 分页功能实现 :基于MyBatis-Plus的IPage实现商品列表分页查询,提升前端展示体验。

2. 功能扩展(提升项目价值)

  • 商品图片上传:对接阿里云OSS,实现图片上传、存储、回显功能,完善商品信息;
  • 订单状态管理:新增"付款""取消订单"接口,支持订单状态流转(待付款→已付款→已完成);
  • 个人中心模块:开发"我的发布""我的订单"查询接口,前端实现对应页面,完善用户操作闭环。

五、面试高频问题与解答

  1. :项目中如何保证数据一致性?比如创建订单时商品状态不会被重复修改?
    :当前通过业务逻辑校验 保证------创建订单前先查询商品状态,仅当状态为"在售"时才执行创建逻辑并更新状态;若后续面临高并发场景,可引入数据库乐观锁 (添加version字段)或分布式锁,避免并发修改导致的数据不一致。

  2. :为什么选择MyBatis-Plus?相比原生MyBatis有什么优势?
    :MyBatis-Plus是MyBatis的增强工具,核心优势有三点:一是简化CRUD操作 ,内置大量通用方法,无需手动编写XML映射文件;二是支持条件构造器 ,通过QueryWrapper实现复杂条件查询,代码可读性更高;三是内置分页插件,一行代码即可实现分页功能,开发效率大幅提升。

  3. :前后端对接时遇到的最大问题是什么?如何解决?
    :最大问题是参数传递和响应判断的一致性问题 ,比如400参数解析失败、提示文案矛盾。解决思路是:首先通过浏览器Network面板和后端日志定位问题(前端参数格式/后端注解配置);其次制定统一的接口规范(请求头、响应体格式);最后在前端封装统一的请求方法,后端封装统一的响应工具类Result,从根本上避免类似问题。

六、总结

本项目通过SpringBoot+Vue+MySQL技术栈,实现了二手交易系统的核心功能闭环。开发过程中遇到的字段映射、关键字冲突、前后端交互等问题,均是新手入门前后端分离开发的典型痛点。解决这些问题的过程,不仅是代码能力的提升,更是问题分析和排查思路的积累。

该项目虽小,但覆盖了后端接口开发、数据库设计、前端页面交互、前后端联调等核心环节,是夯实基础、备战面试的优质实战案例。

github地址:点击此处进入项目

相关推荐
web打印社区22 分钟前
web-print-pdf:突破浏览器限制,实现专业级Web静默打印
前端·javascript·vue.js·electron·html
m0_7400437323 分钟前
【无标题】
java·spring boot·spring·spring cloud·微服务
重整旗鼓~38 分钟前
1.外卖项目介绍
spring boot
Dxy12393102161 小时前
MySQL INSERT ... ON DUPLICATE KEY UPDATE 与非主键唯一字段
数据库·mysql
Amumu121381 小时前
Vuex介绍
前端·javascript·vue.js
css趣多多2 小时前
Vue过滤器
前端·javascript·vue.js
一点技术2 小时前
基于SpringBoot的选课调查系统
java·spring boot·后端·选课调查系统
shuair2 小时前
redis实现布隆过滤器
spring boot·redis·bootstrap
RANCE_atttackkk3 小时前
Springboot+langchain4j的RAG检索增强生成
java·开发语言·spring boot·后端·spring·ai·ai编程
这是个栗子4 小时前
【Vue代码分析】前端动态路由传参与可选参数标记:实现“添加/查看”模式的灵活路由配置
前端·javascript·vue.js