前阵子带人做项目选型,二手物品交易系统又成了热门选项。这个题目的好处在于:需求场景清晰、功能边界明确、前后端都有足够空间施展,而且做出来的东西确实能跑、能演示、能答辩。最近我把一套基于SpringBoot+Vue的二手物品交易管理系统完整跑通了一遍,后端工程代号叫bootpf,顺手把整个设计和实现过程梳理成了这篇文章。我不是来发代码包的,而是要把从需求拆解、数据库建模到前后端联调这一路的真实思路和踩坑记录写清楚。技术栈就是标题里那套:Java+MySQL+MyBatis做后端,Vue做前端。如果你正在纠结毕设选题,或者想找一个完整的全栈项目练手,这篇文章应该能帮你少走不少弯路。
1. 这个二手交易系统的需求到底长什么样
很多人拿到这类题目第一反应是"不就是个商品增删改查吗",真做起来才发现,二手交易和普通商城在需求上有本质区别。先把这个理清楚,后面写代码才不会返工。
1.1 角色与权限:谁在使用这个平台
一套完整的二手交易系统,至少要有两类使用者:普通用户和管理员。这里的普通用户既是买家也是卖家——这是二手平台和B2C商城的核心差异,任何一个注册用户都应该能在"买东西"和"卖东西"两个身份之间自由切换。
普通用户端的需求拆开看大概是这些:
| 功能模块 | 具体说明 |
|---|---|
| 注册登录 | 用户名、密码、手机号、昵称等基础信息,密码加密存储 |
| 商品浏览 | 首页商品流、分类筛选、关键字搜索、商品详情查看 |
| 商品发布 | 填写标题、描述、价格、成色、分类,上传图片 |
| 我的商品 | 查看自己发布的商品,支持编辑、上下架操作 |
| 收藏管理 | 收藏感兴趣的商品,在我的收藏里快速找到 |
| 留言咨询 | 对商品发起提问,卖家查看并回复 |
| 订单管理 | 下单、发货、确认收货、取消订单 |
| 评价 | 交易完成后互评,影响买卖双方信用 |
管理端则是另一套逻辑:
| 功能模块 | 具体说明 |
|---|---|
| 用户管理 | 用户列表,禁用/解禁违规账号 |
| 商品管理 | 商品列表,下架违规商品,推荐/取消推荐 |
| 分类管理 | 维护商品分类树,增删改分类 |
| 订单管理 | 查看所有订单,处理纠纷/异常订单 |
| 数据概览 | 用户数、商品数、交易量等基础统计 |
权限控制这块,实际做的时候不需要引入Spring Security那么重的东西。在user表里加一个role字段,用0和1区分普通用户和管理员,后端写一个简单的拦截器判断接口权限,前端再根据角色渲染不同菜单,完全够用。过度设计在毕设和练手项目里是大忌。
1.2 核心业务流程:从发布到成交的完整闭环
业务流程是这套系统的灵魂。我在设计阶段画过完整的状态流转图,这里用文字把关键链路说清楚。
商品发布链路:用户登录后填写商品信息,提交后进入"待上架"状态,管理员在后台审核通过后商品变为"上架"状态,出现在前台商品列表中。对于练手项目,如果不想做审核环节,也可以省掉审核状态,发布即上架,但答辩时老师一定会问"怎么处理违规商品",所以留一个下架接口管理端能操作即可。
二手商品和一手商品在发布时的字段差异也要注意:成色、入手渠道、入手价格、转手原因,这些是二手场景特有的信息。尤其是成色,直接决定买家心理预期,是二手交易里最重要的一个卖点。
交易链路:买家看中商品后可以对卖家发起留言咨询,确认没问题后下单。下单后订单状态变成"待付款",系统这里用模拟支付按钮替代真实支付,支付后变成"待发货",卖家那边看到订单后操作发货变成"待收货",买家确认收货后变成"已完成"。完成之后买卖双方可以互相评价,整个交易闭环才算真正走完。
这里有个设计细节值得说一下,就是商品和订单是解耦的。订单表里除了商品ID,还要冗余一份商品快照(标题、图片、价格),因为订单是交易凭证,如果卖家把商品改了或删了,订单里的信息不能跟着变。这个细节做数据库设计的时候非常能体现思考深度。
2. 为什么是SpringBoot+Vue+MyBatis这套组合
技术选型不是越新越好,而是越适合越好。这套组合放在二手交易系统上,属于典型的"不折腾且能讲清楚道理"的方案。
2.1 后端选型:SpringBoot省下的配置功夫
如果是早几年用SpringMVC,光是配置文件就够写半天的:web.xml、spring.xml、spring-mvc.xml、数据源配置、事务配置,每一处都是坑。SpringBoot把这些统一收敛成自动配置和starter依赖,一个application.yml就搞定大部分事情。内嵌的Tomcat让部署也简单了,后端打包成jar直接java -jar就能跑。
SpringBoot在二手交易系统里帮我们处理了大量基础设施问题:
- spring-boot-starter-web帮我们搭好了MVC和JSON序列化
- spring-boot-starter-jdbc和数据源配合,数据库连接配置控制在配置文件里
- 事务支持通过@Transactional注解就能实现,对订单这类强一致性的业务非常关键
- 内置的拦截器、过滤器机制方便做登录校验
这个项目的artifactId我当时起的就是bootpf,就是Boot Platform的意思,纯粹内部工程代号,没有其他含义。后面前端工程对应叫bootpf-web,方便一眼看出两个工程的归属。
2.2 前端选型:Vue组件化在二手交易场景的契合点
Vue这套前端框架之所以适合这个场景,核心在于组件化和响应式数据。二手交易系统的前台有大量重复出现的东西:商品卡片、分类导航、分页器、搜索框,每个页面都在复用同样的UI和逻辑。Vue把每个部分封装成组件之后,商品列表可以复用商品卡片组件,我的收藏页面也可以复用同一个组件,代码量肉眼可见地减少。
响应式数据绑定对搜索筛选也格外好使。带上价格区间、选择分类、输入关键词,这些筛选条件用data属性绑着,数据一变页面自动更新,不需要像JQuery时代那样手动操作DOM。联调的时候前后端分工也清晰:后端只返回JSON数据,前端负责渲染。
项目里我用的Vue版本是Vue2配ElementUI,这个组合文档多、坑少、资料遍地都是,对新手最友好。如果你对Vue3和Element Plus更熟,直接用Vue3也没问题,骨架思路完全一致。
2.3 MyBatis+MySQL:轻量ORM和动态SQL的默契配合
后端选型时很多人会纠结MyBatis和JPA(Hibernate)选哪个。我的建议很直接:查询条件复杂的业务系统,无脑选MyBatis。
二手交易系统的商品列表查询就是个典型的多条件动态查询。用户可能按分类查,可能按价格区间查,可能按成色查,也可能同时输入好几个条件,还可能要按时间排序。这种"查询条件个数不确定"的场景,恰恰是MyBatis动态SQL最擅长的地方。在XML里用if标签拼条件,有就拼接,没有就跳过,一条SQL可以应对无数种查询组合。换成JPA的话,要么用规格类那套抽象,要么一个条件写一个方法,写起来绕且不直观。
MySQL作为底层数据库没什么悬念,开源免费、主流稳定、资料丰富,最重要的是事务和锁机制足够支撑订单这种核心业务。MySQL8的JSON字段类型还可以用来存商品图片列表,不过这个需求用普通字符串处理更简单,没那么玄乎。
3. 数据库设计与表结构拆解
数据库设计决定了这套系统的天花板。表设计好,后面写代码如鱼得水;表设计烂,后面每个接口都在填坑。我把这个项目的主要表按模块拆开讲。
3.1 用户与分类:基础表的细节不能省
用户表是系统的基础,字段设计要兼顾登录、展示和权限:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | BIGINT | 主键,自增 |
| username | VARCHAR(50) | 登录用户名,唯一索引 |
| password | VARCHAR(100) | 密码,MD5加盐或BCrypt加密后存储 |
| nickname | VARCHAR(50) | 昵称,前端展示用 |
| avatar | VARCHAR(200) | 头像图片路径 |
| phone | VARCHAR(20) | 手机号 |
| role | TINYINT | 0=普通用户,1=管理员 |
| status | TINYINT | 0=正常,1=禁用 |
| create_time | DATETIME | 注册时间 |
密码加密这件事必须重视。明文存密码是答辩现场最容易被致命追问的硬伤,也是实际开发中的最低级错误。我习惯用MD5加盐的思路演示,加盐的意思是在原始密码后面拼一段随机字符串再做哈希,这样即使两个用户密码相同,存出来的哈希值也不一样。加密后数据库里就算被盗也不会直接泄漏原始密码。
分类表用自关联的形式支持多级分类。一级分类像"数码产品""服饰日用""图书教材",二级分类是具体品类如"手机""电脑""女装"。用parent_id字段指向父分类ID,顶级分类的parent_id为0,这样一个表就能撑起三级甚至更多级的分类树。
3.2 商品表:二手商品的字段比你想的多
商品表是整个系统的核心表。我在设计时往里面塞了很多"二手专属"字段,这也是和普通商城商品表最明显的区别:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | BIGINT | 主键 |
| seller_id | BIGINT | 卖家ID,外键关联用户表 |
| category_id | BIGINT | 分类ID |
| title | VARCHAR(100) | 商品标题 |
| description | TEXT | 商品详细描述 |
| original_price | DECIMAL(10,2) | 入手原价 |
| current_price | DECIMAL(10,2) | 当前转让价 |
| condition_level | TINYINT | 成色,1=全新 2=九成新 3=八成新 4=七成新及以下 |
| images | VARCHAR(1000) | 图片路径集合,用逗号分隔 |
| status | TINYINT | 0=草稿 1=上架 2=下架 3=已售出 |
| views_count | INT | 浏览量 |
| create_time | DATETIME | 发布时间 |
| update_time | DATETIME | 更新时间 |
这里说几个设计时的关键判断。
第一,原价和现价必须分开。二手商品的价值感来源之一就是"比原价便宜多少",商品详情页要把原价用删除线划掉,现价加粗展示,这种视觉对比能明显增强购买欲望。只放一个价格字段,这个卖点就丢了。
第二,成色等级不能做成自由文本。让用户自己输入"很新""几乎没怎么用过"这种描述,前端筛选就完全没法处理。用数字枚举锁定成色等级,既方便前端做筛选下拉框,也能让列表页按成色排序。
第三,图片字段用逗号分隔路径存,还是单独建一张图片表?项目规模不大时,逗号分隔足够用。Java后端接收上传后返回路径列表,前端用split方法拆开渲染成轮播图即可。一个字段解决的事情,没必要再建一张表增加JOIN复杂度。
第四,status字段是商品状态的核心。设计的精髓在于它和订单状态联动——商品一旦被人下单进入"已售出"状态,就不能被搜到、不能被重复下单。什么时候恢复成上架?订单取消的时候。这个联动的逻辑后面会细讲。
3.3 订单与评价:交易闭环的两张核心表
订单表的字段设计直奔"交易凭证"这个定位:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | BIGINT | 主键 |
| order_no | VARCHAR(32) | 订单号,业务唯一 |
| product_id | BIGINT | 商品ID |
| product_title | VARCHAR(100) | 商品标题快照 |
| product_image | VARCHAR(200) | 商品图片快照 |
| product_price | DECIMAL(10,2) | 成交价快照 |
| buyer_id | BIGINT | 买家ID |
| seller_id | BIGINT | 卖家ID |
| status | TINYINT | 0=待付款 1=待发货 2=待收货 3=已完成 4=已取消 |
| create_time | DATETIME | 下单时间 |
| pay_time | DATETIME | 付款时间 |
| deliver_time | DATETIME | 发货时间 |
| finish_time | DATETIME | 完成时间 |
订单号order_no的生成策略我推荐用"时间戳+随机数"或"年月日时分秒+用户ID截取"的方式,核心诉求只有一个:不重复。纯自增ID不适合直接暴露给用户当订单号,一是太长,二是容易被猜到单量。
金额字段用DECIMAL(10,2)是原则性问题。float和double在计算机里的二进制存储天生有精度问题,0.1加0.2可能等于0.30000000000000004,涉及钱的运算出现了这种结果,整单逻辑都别想正确。DECIMAL是定点数,在数据库层面就保证了精度。
订单状态字段看起来很常规,实际上就是一张状态机的表:
- 待付款:买家下单成功,此时商品锁定为已售出
- 待发货:买家模拟支付完成,需要卖家发货
- 待收货:卖家已发货,等待买家确认
- 已完成:买家确认收货,订单完结,触发评价入口
- 已取消:超时未付款或任意一方取消,商品恢复上架
评价表我坚持要求每个订单最多一条评价。实现方式是在评价表里给order_id加唯一约束,数据库层面的约束比代码层面的if判断可靠得多。评价的rating字段用TINYINT存1到5星,content存文字评价,mutual表示这份评价是买家给卖家的还是卖家给买家的。
4. 后端接口与核心业务逻辑实现
表设计完了,后端接口的开发就顺理成章,分层套路固定:Controller接收请求、Service处理业务、Mapper负责数据库操作。但有几个核心逻辑值得单独拉出来讲清楚,因为这些地方最容易出问题,也最能体现一个开发者靠不靠谱。
4.1 登录鉴权:Session还是JWT
登录鉴权方案的选择,直接影响后面所有接口的写法。我见过太多人一上来就选JWT,结果token刷新、过期处理、服务端主动失效这些额外问题一堆,把整个方案的复杂度推高了一大截。对于一个单体架构、无前后端分离部署压力的教务/毕设级别项目,Session方案其实更实用。
| 对比维度 | Session + Cookie | JWT |
|---|---|---|
| 服务端存储 | 需要,存session | 不需要 |
| 实现复杂度 | 低 | 中 |
| 服务端主动失效 | 方便(销毁session) | 困难(需维护黑名单) |
| 前后端分离 | 可行,需配代理 | 天然友好 |
| 适合场景 | 单体应用 | 分布式/微服务 |
我当时实际选择的是Session方案:登录成功后把用户ID和role放进Session,写一个拦截器,把需要登录的接口拦下来校验Session里有没有用户,没有就返回401。管理员接口额外检查role是不是1。整个鉴权逻辑加起来不到100行代码。
如果是前后端完全分离、部署在不同域名下的场景,Session方案确实会遇到跨域Cookie问题,这时候转JWT更顺。我在文章后半段会讲前端的配合处理。如果你老师要求用JWT,就去用Token加拦截器的方式,设计思路上殊途同归。
4.2 商品模块接口:分页、搜索与上下架
商品模块的接口设计走RESPful思路,路径和语义保持一致:
- GET /api/product/page —— 分页+多条件查询商品
- GET /api/product/{id} —— 商品详情
- POST /api/product —— 发布商品
- PUT /api/product/{id} —— 编辑商品
- PUT /api/product/{id}/status —— 上下架商品
- DELETE /api/product/{id} —— 删除商品(逻辑删除)
分页查询是商品列表的核心接口。我习惯手写分页而不是用PageHelper插件,原因很简单:手写LIMIT语句逻辑清清楚楚,PageHelper偶尔会遇到版本不兼容导致的分页失效问题,排查起来反而浪费时间。分页参数就两个,pageNum和pageSize,后端计算出offset传给SQL。
多条件动态查询这一段,是MyBatis动态SQL最能展示价值的地方:
<select id="selectProductPage" resultType="com.bootpf.entity.Product"> SELECT * FROM product <where> status = 1 <if test="keyword != null and keyword != ''"> AND title LIKE CONCAT('%', #{keyword}, '%') </if> <if test="categoryId != null"> AND category_id = #{categoryId} </if> <if test="minPrice != null"> AND current_price >= #{minPrice} </if> <if test="maxPrice != null"> AND current_price <= #{maxPrice} </if> <if test="conditionLevel != null"> AND condition_level = #{conditionLevel} </if> </where> ORDER BY create_time DESC LIMIT #{offset}, #{pageSize} </select>这个写法的妙处在于:用户传了几个条件,SQL就只有几个条件,不传的条件自动忽略。所有筛选条件都在同一个接口里处理,前端不需要按条件拆多个接口。keyword的模糊搜索用LIKE加上百分号拼接,二手交易系统的搜索不需要全文搜索引擎,这个量级完全够用。
文件上传用Spring Boot自带的MultipartFile就能实现。我写完上传逻辑后,图片存在本地上传目录,数据库只存相对路径,前端通过静态资源映射访问。配置里记得调大文件大小限制,不然传几张稍大的图就报错:
spring: servlet: multipart: max-file-size: 10MB max-request-size: 50MB mvc: static-path-pattern: /upload/** resources: static-locations: file:D:/bootpf/upload/静态资源路径配置这里容易踩坑,如果不加static-locations配置,前端访问图片路径时会得到404。这个报错几乎每个做文件上传功能的人都会遇到一次,属于必修课。
4.3 订单创建的事务与并发:最容易翻车的地方
创建订单是整个系统业务逻辑最密集的接口,也是最容易翻车的地方。很多人第一版写出来的逻辑是:插入订单记录、修改商品状态、扣减库存,三个步骤三个方法,但没有加事务。结果订单插入成功、商品状态更新失败,买家以为自己下单了,卖家那边商品却还是上架状态,最终出现一单两卖的情况。
创建订单的逻辑必须用@Transactional包住,要么全部成功,要么全部回滚:
@Transactional(rollbackFor = Exception.class) public Order createOrder(Long productId, Long buyerId) { Product product = productMapper.selectById(productId); if (product == null || product.getStatus() != 1) { throw new BizException("商品不存在或已下架"); } // 乐观锁更新:只有status=1时才能更新为已售出 int rows = productMapper.updateStatus(productId, 3, 1); if (rows == 0) { throw new BizException("手慢了,商品已被购买"); } Order order = new Order(); order.setOrderNo(generateOrderNo()); order.setProductId(productId); order.setProductTitle(product.getTitle()); order.setProductImage(product.getImages()); order.setProductPrice(product.getCurrentPrice()); order.setBuyerId(buyerId); order.setSellerId(product.getSellerId()); order.setStatus(0); order.setCreateTime(new Date()); orderMapper.insert(order); return order; }这里的核心是那一行WHERE status = 1的UPDATE。它叫做乐观锁思路:更新的时候加上条件判断,数据库的锁机制会保证同一时刻只有一个事务能更新成功,第二个事务来更新时发现status已经不是1了,受影响行数为0,于是抛出"手慢了,商品已被购买"。这种方式既防止了超卖,又不需要手写synchronized或Redis分布式锁,在这个并发量级的系统中足够可靠。
订单取消的逻辑同样要注意事务:订单状态改成已取消的同时,商品状态要改回上架。只改一边不动另一边,就会造成商品明明没卖出却无法被搜索到的尴尬场景。
5. 前端Vue页面组织与前后端联调的实战细节
后端接口写完后,前端的工作量大头其实不在写页面,而在怎么把页面组织和数据请求理清楚。很多项目死在了联调阶段,尤其是跨域和请求拦截这两个点。
5.1 页面路由与组件拆分
前端工程的结构我分成views和components两个主要目录。views放页面级组件,每个路由对应一个;components放可复用的通用组件。
页面路由表大概是这样的:
| 路径 | 页面 | 说明 |
|---|---|---|
| /home | 首页 | 商品流、分类导航、搜索栏 |
| /product/detail/:id | 商品详情 | 图片轮播、信息展示、下单入口 |
| /publish | 发布商品 | 表单页,含图片上传 |
| /user/products | 我的商品 | 管理自己发布的商品 |
| /user/orders | 我的订单 | 按角色区分买家/卖家维度 |
| /user/favorites | 我的收藏 | 收藏列表 |
| /user/profile | 个人中心 | 基本信息修改 |
| /admin | 管理后台 | 用户/商品/分类/订单管理 |
| /login /register | 登录注册 | 独立页面 |
商品卡片是复用度最高的组件,列表页、收藏页、我的商品页全都在用它。组件内部接收一个product对象,渲染封面图、标题、价格、成色角标,点击跳转详情页。这种"一个组件多个页面复用"的组织方式,是Vue组件化思想最落地的地方。
详情页的下单按钮要区分情况:商品是自己发布的不能买,成色和原价展示在前面提到的二手专属字段上。这些逻辑放在前端判断只是体验层,最终的正确性由后端接口保证,前端判断只能作为辅助。
5.2 axios封装与统一错误处理
项目里的所有请求都走封装好的request.js,不要让每个页面都直接去axios.get。封装axios的核心目的是统一处理baseURL、token注入、错误码响应和登录失效跳转:
import axios from 'axios' const request = axios.create({ baseURL: '/api', timeout: 10000 }) // 请求拦截器:把登录用户的会话信息带上 request.interceptors.request.use(config => { const user = sessionStorage.getItem('user') if (user) { config.headers['Authorization'] = 'Bearer ' + user } return config }) // 响应拦截器:统一处理后端返回的业务码 request.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { this.$Message.error(res.msg) if (res.code === 401) { router.push('/login') } return Promise.reject(new Error(res.msg)) } return res.data }, error => { this.$Message.error('网络异常,请稍后重试') return Promise.reject(error) } ) export default request这样封装完之后,每个页面的请求代码就只剩下一句话:request.get('/product/page', { params })。统一拦截的好处是全局逻辑只写一次。比如登录失效的情况下,前端自动跳回登录页,而不是每个接口都写一遍重复的判断。
5.3 跨域与静态资源的处理方案
前后端联调时最经典的报错就是跨域。后端跑在8080端口,前端开发服务器跑在8081端口,前端请求后端接口如果不处理,浏览器会直接拦截。这里有几种标准方案,按场景选:
| 方案 | 适用场景 | 配置复杂度 |
|---|---|---|
| 后端CorsFilter | 任意场景 | 低 |
| 前端devServer代理 | 开发环境 | 低 |
| Nginx反向代理 | 生产环境 | 中 |
我在开发时用vue.config.js的代理方式,把前端请求的/api前缀转发到后端8080端口,浏览器视角里所有请求都是同源,跨域问题直接消失:
module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } }这里有一个细节陷阱:后端的Controller如果没加统一前缀,代理转发后路径很容易对不上。优雅的做法是后端Controller统一带/api前缀,和前端代理路径完全对齐,避免手忙脚乱地改路径。
后端如果选择Session方案,前端用axios请求时会自动携带Cookie吗?代理模式下同源,Cookie自动带上,Session机制正常工作。如果你选了JWT方案,请求头里手动带token即可,不存在Cookie跨域问题。两个方案和前端代理的配合都验证过,是稳定的。
6. 源码导读:从零跑通项目与二次扩展建议
拿到一套完整源码后,很多人的第一个困惑是"从哪看起"。我按自己的阅读习惯和调试经验,把一套SpringBoot+Vue项目的阅读路径和运行细节整理出来,至少能在拿到源码的第一小时做到不慌。
6.1 后端目录结构:拿到源码先看哪里
一个规范的SpringBoot后端工程,目录结构是这样的:
bootpf ├── pom.xml ├── src/main/java/com/bootpf │ ├── BootpfApplication.java // 启动类 │ ├── controller/ // 接口层 │ │ ├── UserController.java │ │ ├── ProductController.java │ │ ├── OrderController.java │ │ └── AdminController.java │ ├── service/ // 业务逻辑层 │ │ ├── UserService.java │ │ ├── ProductService.java │ │ └── OrderService.java │ ├── mapper/ // 数据访问层 │ │ ├── UserMapper.java │ │ ├── ProductMapper.java │ │ └── OrderMapper.java │ ├── entity/ // 实体类,对应数据库表 │ │ ├── User.java │ │ ├── Product.java │ │ └── Order.java │ ├── config/ // 配置类 │ │ └── WebConfig.java │ └── common/ // 通用工具 │ ├── Result.java // 统一返回封装 │ ├── BizException.java // 自定义业务异常 │ └── GlobalExceptionHandler.java阅读源码的顺序我建议:先看entity里有哪些实体,对应数据库哪几张表;再看controller,了解系统暴露了哪些接口;然后看service里的核心业务方法,特别是带@Transactional的;最后再看mapper里的XML文件怎么写的SQL。控制器+服务+Mapper的调用链是SpringBoot后端的主线,顺着这条线读下来,整个系统的骨架就清楚了。
统一返回封装Result是我每套系统都坚持做的。所有接口都返回{code, msg, data}这个结构,前端axios响应拦截器才能统一处理错误。如果不做这层封装,会出现每个接口返回格式都不一样,前端越改越乱的局面。
6.2 本地运行配置与常见坑
拿到源码第一件事是把项目跑起来。后端运行步骤:
- 创建数据库,执行项目里的bootpf.sql脚本,初始化表和测试数据
- 修改application.yml里的数据库账号密码
- IDEA导入maven工程,等待依赖下载完成
- 运行BootpfApplication.java,后端默认8080端口
- 打开swagger或直接访问接口文档地址验证
前端运行步骤:
- 进入bootpf-web目录
- npm install安装依赖
- npm run serve启动开发服务器,默认8081端口
- 浏览器访问前端地址
跑起来的过程中有四个高频报错,几乎每个人都会遇到:
| 报错信息 | 原因 | 解决方案 |
|---|---|---|
| Access denied for user | 数据库账号密码错误 | 检查application.yml |
| Unknown time zone | 数据库时区未配置 | 连接串加serverTimezone=Asia/Shanghai |
| Port 8080 already in use | 端口被占用 | 改后端端口或kill占用进程 |
| Failed to read candidate component class | 依赖没下载 | mvn clean + 重新导入 |
| 前端图片404 | 静态资源映射没配 | 检查yaml里的static-locations配置 |
这些坑全部是基于我实际操作经验的总结,每个都真实翻过车。尤其是时区问题,MySQL8默认使用UTC时区,和本地时间差了8个小时,不配置时区后端会直接报错,这个问题几乎成了MySQL8新手的固定拦路虎。
6.3 从毕设到实际应用的扩展思路
一个拿得出手的系统,能跑通主流程只是及格线。如果你还想让这个项目在答辩或者简历上更有竞争力,这里有四条切实可行的进阶方向。
第一,接入Redis做缓存。商品详情页是热点数据,用户每次刷新都要查数据库,量大了扛不住。把浏览量高的商品缓存到Redis,设置过期时间,能显著降低数据库压力。这属于"成本低、故事好讲"的加分项。
第二,用WebSocket做站内消息通知。买家下单后通过WebSocket实时通知卖家"你有新订单",卖家发货后买家收到通知。这个功能能让系统的实时体验上一个台阶,实现复杂度又不算太高。
第三,给商品加一个"猜你喜欢"的推荐逻辑。基于用户收藏和浏览记录,查询同分类其他商品推荐展示。不需要搞算法,简单的"看过的人都看过"SQL查询就能实现,讲起来也有说服力。
第四,引入RabbitMQ做订单超时自动关闭。模拟支付的订单如果30分钟未付款,通过延时队列自动取消并恢复库存。这个方案可以把订单状态机的"已取消"分支补完整,也展示了消息队列的实际应用场景。
这些扩展方向做任何一个都能给项目增加差异化亮点,而且我建议只挑一个做透做精,比堆砌三个半成品要好。
最后分享一点个人体会:这套SpringBoot+Vue二手交易系统,我前后带项目跑了不止五遍,每一遍都会发现新的优化点。如果你正在拿这套源码做自己的项目,我的建议是先老老实实把主流程跑通,从数据库脚本执行到下单、发货、收货完整走一遍,之后再考虑锦上添花的功能。项目里的代码注释和调试记录都很完整,对着代码动手改一改,琢磨一下每个接口为什么要这么设计,收获比拿着浏览器刷十篇技术文章都大。