
从Java毕设圈来看二手物品交易系统算是经典中的经典。光我见过的版本就有不下十几个但好用的、能一次跑通的却不多。原因无非就几个要么代码逻辑太乱要么数据库设计得稀烂要么前端环境配了半天还是白屏。今天我就来把这一套基于Spring Boot Vue的二手交易系统完整拆开从数据库设计到前后端联调从环境搭建到部署上线把里面的门道和坑都讲清楚。这套东西不光是拿来交作业自己学习全栈开发也是很好的练手素材。1. 项目整体设计与思路拆解1.1 业务场景定义二手交易到底在交易什么做系统之前先得搞清楚二手交易这个场景的真实业务流。表面看就是一个人发布闲置、另一个人买,但落地到系统里至少要拆成这么几条线用户线注册、登录、个人信息维护、信用记录商品线发布闲置、编辑下架、分类检索、条件筛选交易线下单购买、订单状态流转、买卖双方确认互动线商品留言、收藏关注、举报不良信息这里有个容易犯的错就是初学者喜欢一上来就做购物车、支付、物流。二手交易和电商平台有本质区别——二手交易更偏向C2C轻交易大部分场景下买家直接联系卖家线下看货重点应该放在信息撮合上。这套系统的设计就没做冗余的支付流程而是把重心放在商品发布和订单管理上思路是对的。1.2 技术选型为什么是Spring Boot Vue这个组合现在几乎是全栈开发的事实标准原因三条就能说清楚第一Spring Boot大幅降低后端搭建成本。它内置Tomcat、自动配置、Starter机制不需要像传统SSM那样写一堆XML配置。一个注解启动项目、一套依赖搞定数据源非常适合中小型系统。第二Vue的前后端分离开发体验好。组件化开发让页面复用性高虚拟DOM保证响应速度尤其适合二手交易这种列表多、筛选多、状态切换频繁的界面场景。第三市场认可度高。目前大量企业在用Spring Boot做微服务底座、Vue做前端SPA单页应用学这套组合找工作、做毕设都有实用价值。注意Spring Boot版本选择有讲究。如果不需要JDK 17建议选2.7.x系列稳定、兼容性好、网上资料多Spring Boot 3.x虽然新但很多老博客的代码直接用会报错调试成本高。后面会专门讲。1.3 功能模块规划前后台分离的权限模型这套系统的功能模块规划核心是前后台分离的权限模型前台用户端注册登录、商品首页展示、商品分类浏览、关键词搜索、商品详情、留言评论、收藏管理、个人中心、我的发布、我的订单后台管理端用户管理、商品审核与管理、分类管理、订单管理、留言管理后台为什么不和前台混在一起因为二手交易平台最怕信息虚假和交易纠纷。后台必须能下架违规商品、封禁不良用户、查看全部订单状态。前后台分开数据权限边界清晰——前台用户只能操作自己的数据管理员拥有全局权限这就避免了越权漏洞。权限模型的实现方式不复杂后端用拦截器Interceptor检查登录状态前端用路由守卫Vue Router的beforeEach控制页面访问。接口层面再用注解标记管理员接口就形成了完整的防线。2. 核心技术点与数据库设计详解2.1 数据库表结构设计六张核心表撑起整个业务数据库设计直接决定开发效率。这套系统的表结构是标准的用户-商品-订单-互动四组模型核心六张表如下表名核心字段作用userid, username, password, nickname, phone, avatar, status用户信息及状态管理categoryid, name, parent_id, sort商品分类支持多级goodsid, user_id, category_id, title, description, price, cover, images, status商品信息状态区分在售/下架/已售ordersid, order_no, goods_id, seller_id, buyer_id, price, status, create_time订单交易记录commentid, user_id, goods_id, content, create_time商品留言互动collectid, user_id, goods_id, create_time用户收藏记录重点说一下goods表的images字段。很多初学者喜欢建一张单独的图片表一张商品图对应一行记录看似专业实际徒增复杂度。这套系统直接在商品表里用VARCHAR存多张图片的路径用逗号分隔前端拿到后split一下就能遍历渲染简单高效。这类小而精的设计思路更适合真实项目落地。2.2 Spring Boot数据访问层MyBatis-Plus让增删改查变简单现在做Spring Boot项目数据访问层我强烈建议直接用MyBatis-Plus。它和原生MyBatis的区别用一个词就能说明省事。比如用户表分页查询原生MyBatis你得写PageHelper插件或者手写LIMIT #{offset}, #{size}再用PageInfo包裹返回。MyBatis-Plus自带IPageT接口写一行代码就能完成IPageUser page userMapper.selectPage(new Page(pageNum, pageSize), new LambdaQueryWrapperUser().like(StringUtils.isNotBlank(username), User::getUsername, username));连SQL都不用写。内置的LambdaQueryWrapper用起来不仅类型安全而且代码可读性比拼接SQL高得多。日常的selectById、updateById、deleteById也全部内置基础增删改查一行调用搞定。但这不代表SQL不用学。业务复杂到一定程度比如报表统计、多表关联查询还是得手写SQL和Select注解。2.3 前后端数据交互设计统一返回格式与跨域处理一套接口如果没有统一的返回格式前端拿到数据后光判断逻辑就要写三分支。这套系统的统一返回结构设计得很好{ code: 200, message: 操作成功, data: { ... } }code用200表示成功500表示系统异常401表示未登录403表示无权限。前端axios拦截器统一处理这些状态码不用每个页面单独写异常提示。跨域问题是前后端分离开发必踩的坑。前端跑在localhost:8080后端跑在localhost:9090浏览器默认禁止跨域请求。解决方案有两种这套系统用的是后端配置CORS代码很简单Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE) .allowCredentials(true) .maxAge(3600); } }也可以用前端代理方案在Vue的vite.config.js里配置server.proxy把/api开头的请求转发到后端地址。两种方案都行生产环境推荐Nginx反向代理解决部署到同一域名下就彻底没有跨域问题。3. 实操过程与环境搭建要点3.1 环境准备清单与版本匹配我实测下来这套系统的开发和部署推荐环境如下组件推荐版本说明JDKJDK 1.8或11与Spring Boot 2.7.x完美兼容Spring Boot2.7.16稳定文档多社区资源丰富Maven3.8.x依赖管理和项目构建Node.js16.x或18.x LTS过高版本可能与老依赖冲突MySQL5.7或8.0生产环境推荐8.0IDEIDEA 2023后端推荐IDEA前端也用它开发晚这里要专门提醒一句网上很多教程让你装Spring Boot 3.2但Spring Boot 3.x强制要求JDK 17如果你的机器装的是JDK 8连启动都做不到报错是UnsupportedClassVersionError。如果你的JDK是8或者11老老实实选Spring Boot 2.7系列省下的时间够你多写两个页面。3.2 后端项目搭建从Spring Initializr到分层架构后端项目创建有两条路一是IDEA内置的Spring Initializr二是在start.spring.io网站上在线生成。两条路效果一样关键在依赖选择。这套系统需要勾选这些依赖Spring Web提供MVC框架和RESTful接口支持MyBatis Framework数据访问层框架MySQL Driver数据库驱动Lombok简化实体类代码Validation参数校验项目生成后立即补上MyBatis-Plus依赖注意Spring Boot 2.x要用mybatis-plus-boot-starter的3.5.x版本dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency代码分层是这套系统的核心结构包结构如下com.example.usedtrade ├── controller // 接收前端请求返回数据 ├── service // 业务逻辑处理层 ├── mapper // 数据访问接口层 ├── entity // 数据库对应实体类 ├── config // 配置类CORS、拦截器、WebMvc等 └── common // 统一返回结果、异常处理等controller层只做三件事接收参数、调用service、返回统一结果。所有业务判断都放service层controller里不写if/else堆业务逻辑。这样分层的价值等到你写订单创建和商品状态流转这种复杂逻辑时就能体会到了。3.3 前端项目搭建Vite创建Vue3与核心依赖前端这里是Vue 3 Vite的组合。创建项目命令很简单npm create vitelatest frontend -- --template vue cd frontend npm install但实际经验是国内环境你要是直接npm install大概率会卡住或者报ETIMEDOUT。先设置淘宝镜像再装依赖效率完全不同npm config set registry https://registry.npmmirror.com装完基础依赖还要装Vue生态三件套npm install vue-router4 npm install axios npm install element-pluselement-plus是UI组件库做后台管理类界面用它效率极高。同时要把element-plus/icons-vue图标库也装上整个界面风格瞬间从毛坯房变精装修。Vue项目的目录结构这套系统的组织方式是frontend/src ├── api // 封装所有axios请求 ├── assets // 静态资源 ├── components // 公共组件 ├── router // 路由配置 ├── store // 状态管理Pinia ├── views // 页面组件 └── App.vue // 根组件3.4 核心功能实现商品发布与订单流转的后端逻辑商品发布是系统里含金量最高的功能因为它的逻辑一多就考验分层能力了。前端提交表单包含标题、描述、分类、价格、封面图和详情图。后端处理流程是PostMapping(/goods) public Result createGoods(RequestBody GoodsDTO dto) { Goods goods new Goods(); BeanUtils.copyProperties(dto, goods); goods.setUserId(getCurrentUserId()); goods.setStatus(1); // 1在售0下架2已售 goods.setCreateTime(new Date()); goodsMapper.insert(goods); return Result.success(); }这里要重点说一下图片上传。这套系统的方案是前端把图片传到后端后端存到本地磁盘的/upload目录然后把访问路径存进数据库。上传接口的核心代码PostMapping(/upload) public Result upload(RequestParam(file) MultipartFile file) { String originalFilename file.getOriginalFilename(); String ext originalFilename.substring(originalFilename.lastIndexOf(.)); String filename UUID.randomUUID().toString().replace(-, ) ext; String dateDir new SimpleDateFormat(yyyy-MM-dd).format(new Date()); File dir new File(uploadPath dateDir); if (!dir.exists()) dir.mkdirs(); file.transferTo(new File(dir.getAbsolutePath(), filename)); return Result.success(/upload/ dateDir / filename); }UUID随机命名是必须的。直接用原文件名一旦同名文件第二次上传就会覆盖而且中文文件名在URL访问时容易出乱码。这个坑很多新手都会踩。订单流转的逻辑相对清晰买家对在售商品发起购买后端创建订单同时把商品状态改为已下单买卖双方确认交易完成后订单状态改为已完成商品状态改为已售。核心逻辑Transactional public Integer createOrder(Integer goodsId, Integer buyerId) { Goods goods goodsMapper.selectById(goodsId); if (goods null || goods.getStatus() ! 1) { throw new RuntimeException(商品不存在或不可购买); } Orders order new Orders(); order.setOrderNo(TR System.currentTimeMillis()); order.setGoodsId(goodsId); order.setSellerId(goods.getUserId()); order.setBuyerId(buyerId); order.setPrice(goods.getPrice()); order.setStatus(0); // 0待确认1已完成2已取消 orderMapper.insert(order); goods.setStatus(2); goodsMapper.updateById(goods); return order.getId(); }Transactional注解一定要加在createOrder上。商品下单涉及插入订单修改商品状态两步操作如果第二步失败而第一步成功就出现了脏数据。给事务方法加上注解任何一个环节抛异常整体回滚保证数据一致性。这是面试官最喜欢问的考点也是生产环境必须有的保障。3.5 前端对接axios请求封装与路由守卫前端请求封装是工程化必备的一环。直接在页面里写axios.get(http://localhost:9090/api/goods)代码冗余不说后期改个接口前缀要全项目搜索替换。这套系统的做法是统一封装在api目录import axios from axios import { ElMessage } from element-plus import router from ../router const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization token } return config }) request.interceptors.response.use( response { const res response.data if (res.code 200) { return res } else if (res.code 401) { router.push(/login) return Promise.reject(new Error(未登录)) } else { ElMessage.error(res.message) return Promise.reject(new Error(res.message)) } }, error { ElMessage.error(网络请求异常) return Promise.reject(error) } ) export default requestrouter拦截器的逻辑也是一样每个需要权限的页面路由上配置meta: { requiresAuth: true }然后在全局前置守卫里做判断router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next(/login) } else { next() } })这样设计的好处是安全逻辑集中在两处后端拦截器负责接口校验前端路由守卫负责界面跳转。新增页面只需要在路由表里配置meta字段不需要额外写判断代码。4. 批量安装与配置时的环境优化与排查技巧4.1 启动异常速查表从项目启动到页面渲染的排查路径实操中最磨人的就是环境问题。我整理了这套系统的启动阶段高频异常速查表异常现象原因分析解决方案后端启动报Port 8080 was already in use端口被占用改application.yml中的端口或netstat -ano找出PID杀掉后端启动报Unknown database usedtrade数据库没建或者库名不匹配进MySQL执行CREATE DATABASE usedtrade DEFAULT CHARACTER SET utf8mb4后端启动报Access denied for user rootlocalhost数据库密码错误核对application.yml中spring.datasource.password前端npm install卡住不动npm源太慢用npm config set registry https://registry.npmmirror.com换源前端启动报vite: not recognized依赖没装完删掉node_modules和package-lock.json重新npm install前端npm run dev启动但页面白屏路由页面路径不对检查router配置文件里的component: () import(...)路径是否和views目录一致浏览器访问接口报CORS错误跨域未处理检查后端CorsConfig是否生效或前端配置代理4.2 数据库连接时区与编码问题最容易忽视的两处配置MySQL 8.0和Spring Boot连接时的时区问题可以说是最隐蔽的坑。你直接用jdbc:mysql://localhost:3306/usedtrade这个串去连启动时大概率报The server time zone value йʱ is unrecognized这是MySQL 8.0之后时区机制从USE_TZSYSTEM变成更严格的校验导致的。解决办法就是在JDBC连接串上显式加上时区url: jdbc:mysql://localhost:3306/usedtrade?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse同时数据库表字符集也建议在建库时指定utf8mb4而不是默认的latin1。否则你存中文时确实能存进去但查询或展示时会出现乱码问题。utf8mb4比utf8多支持四字节的emoji字符是真正的全编码方案。4.3 部署上线宝塔面板Docker部署Spring Boot实测项目写完要部署最省心的是宝塔面板Docker的组合。这里分享一套我实测过多次的流程。先在pom.xml里配置打包插件跳过测试类打jar包mvn clean package -DskipTests打出来的jar包在target目录下大概50MB左右。然后用Docker部署先写DockerfileFROM openjdk:8-jdk-alpine MAINTAINER yourname VOLUME /tmp COPY target/usedtrade.jar app.jar ENTRYPOINT [java,-jar,/app.jar]构建镜像并启动容器docker build -t usedtrade . docker run -d --name usedtrade -p 9090:9090 \ -e TZAsia/Shanghai \ -v /home/www/upload:/upload \ usedtrade这里有个关键的坑把本地的/home/www/upload目录挂载到容器里的/upload目录是为了让上传的图片持久化。如果不做目录挂载Docker容器一删所有上传的图片全丢。这是部署Docker应用最常见的数据消失事故原因。前端部署更简单npm run build生成dist静态文件然后用Nginx托管即可。Nginx配置里同时做两件事一是指向dist目录二是把/api开头的请求反向代理到后端容器地址server { listen 80; server_name www.example.com; root /www/wwwroot/usedtrade-front; index index.html; location /api/ { proxy_pass http://127.0.0.1:9090; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /upload/ { alias /home/www/upload/; } location / { try_files $uri $uri/ /index.html; } }try_files $uri $uri/ /index.html;这行是SPA应用部署的关键。Vue Router使用history模式时刷新/goods/1这类页面Nginx会把请求转发到后端找goods/1这个路径结果404。加上这行配置后刷新任意前端路由都会回退到index.html由前端路由接管渲染问题解决。4.4 项目扩展给二手交易系统加几个实用的升级点基础功能跑通之后有精力的话可以往这几个方向升级第一接入阿里云OSS做图片存储。本地磁盘存图片有个痛点是容量有限而且分布式部署时图片在不同服务器上Nginx挂载配置也麻烦。OSS按量付费一年几块钱就能扛住毕设流量。第二加入WebSocket做实时聊天。二手交易的核心是买卖双方沟通WebSocket用spring-boot-starter-websocket依赖很容易做。买家在商品页点击联系卖家就直接弹出一个聊天窗双方实时在线交流。第三引入Elasticsearch做全文检索。MySQL的LIKE %关键词%在数据量到十万条以后会明显变慢ES的分词检索能力能让搜索体验提升一个量级。毕设里讲出这一层需求演进答辩时很加分。第四做数据统计分析。后台管理端加一个统计面板用ECharts展示每天发布量、成交量、热门分类Top榜。这类可视化页面在做PPT演示时容易出彩而且SQL部分用GROUP BY和DATE_FORMAT就能搞定。5. 项目联动与日常运维的日常问题排查5.1 多模块脚手架的常见问题IDE导入与依赖冲突很多同学的电脑上不止一个Spring Boot项目。IDEA里同时打开多个Maven项目时经常出现莫名其妙的依赖报错。这个问题的根源通常是Maven仓库里的依赖缓存出了问题。解决办法很简单在IDEA右侧Maven面板先点刷新按钮Reload All Maven Projects不行的话执行cd ~/.m2/repository find . -name *.lastUpdated -delete mvn clean install -U-U参数强制更新SNAPSHOT依赖。如果还是不行检查项目的pom.xml里是否有重复依赖特别是多个模块spring-boot-starter-web、spring-boot-starter-thymeleaf同时存在时版本冲突很容易发生。5.2 数据库日常操作备份恢复与常用命令项目开发过程中数据库崩一次是常有的事。这里给出最常用的几类MySQL日常操作命令。备份数据库mysqldump -u root -p usedtrade usedtrade_backup.sql恢复数据库mysql -u root -p usedtrade usedtrade_backup.sql修改表结构加字段、改字段类型ALTER TABLE goods ADD COLUMN view_count INT DEFAULT 0 COMMENT 浏览量; ALTER TABLE goods MODIFY COLUMN price DECIMAL(10,2) NOT NULL COMMENT 价格;批量更新数据UPDATE goods SET status 1 WHERE status 0 AND user_id 1;重要提醒执行UPDATE和DELETE前务必先做一次SELECT确认条件是否准确。我吃过这个亏——原本只想更新某个商品的状态结果忘了加WHERE条件整个表全改了一遍。如果表数据量不大还能从备份恢复数据量大就只能哭。5.3 环境扩展思路给项目添加定时任务与缓存项目运行一段时间后可以加上两项实用能力。定时任务用Spring Boot内置的Scheduled就能实现。比如商品上架超过90天自动标记为已过期Component public class GoodsExpireTask { Autowired private GoodsMapper goodsMapper; Scheduled(cron 0 0 2 * * ?) // 每天凌晨2点执行 public void expireGoods() { Date threshold DateUtils.addDays(new Date(), -90); goodsMapper.updateStatusByTime(threshold); } }缓存用Spring Cache Redis实现。商品详情页是访问热点每次查询都打数据库压力不大但响应慢。给查询方法加Cacheable注解Cacheable(value goods, key #id) public Goods getGoodsById(Integer id) { return goodsMapper.selectById(id); }第二次请求同一个商品时直接走Redis缓存响应时间能压到原来的1/10。不过要注意商品状态变化时及时CacheEvict清缓存否则用户看到的是改了价格但页面还是旧价的商品交易纠纷就是这么来的。6. 写这套系统的真实心得如果让我用一句话总结这套Spring Boot Vue的二手交易系统我会说它不是那种花里胡哨的高大全项目但胜在结构规整、思路清晰、拿来就能跑通。从项目结构上看它把划分子系统的能力、权限管理的模型、事务处理的时机、跨域解决的方案、部署上线的路径全部过了一遍完整度足以对标20万级DAU的小型业务应用。你把这个项目吃透不论是面试聊项目还是工作中写业务需求都不会虚。我个人在实际操作中最大的体会是这类管理系统真正的难点从来不是某个功能有多难而是数据怎么流转。一个商品从发布到成交背后涉及用户权限、状态变更、事务一致性、文件上传、前后端数据交互——每一个环节单拎出来都不复杂但串起来了才是对一个开发者全局设计能力的真正考验。最后再分享一个小技巧拿到这套系统的源码后去看代码不要从上往下地毯式看先从数据库设计文档对照功能列表看搞清了表与表之间的关系之后再去看后端接口和前端页面逻辑会清晰非常多。磨刀不误砍柴工源码和文档配合着看这套项目的价值才能真正吃透。