ARTICLE DETAIL

资讯详情

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

Spring Boot + Vue实现旧物回收系统:从数据模型到前后端部署

Spring Boot + Vue实现旧物回收系统:从数据模型到前后端部署 简介面向计算机专业毕业设计及课程设计场景这份基于SpringbootVue的旧物回收管理系统源码包涵盖了前后端完整实现与数据库设计适合正在准备毕设或需要项目实战练手的Java学习者。项目采用JDK1.8、MySQL5.7与Maven3.3环境已通过严格调试并配有导师指导可直接作为毕设基础进行二次开发。压缩包共361个文件以130个Java源码文件、80个JavaScript文件、55个HTML页面为核心另有XML配置、SQL脚本、设计文档、部署与代码讲解视频等内容整体大小7.35MB。包内附开发说明文档、数据库脚本及全套软件能够帮助使用者快速搭建运行环境并理解系统模块逻辑。当前已有54人学习使用适合用来快速获取一套可运行、可讲解、可扩展的实战项目。1. 为什么“旧物回收管理系统”是Spring Boot Vue的典型考题旧物回收管理系统听起来像后台里常见的增删改查真正动手时才发现它至少要跑通这样一条链路用户发布旧物管理员审核回收员接单上门称重后给出报价用户确认后订单完成。整条链路里存在多个角色、多次状态变更还涉及图片上传、地址维护和分页列表难度恰好卡在一个Java初学者“看得懂但又需要认真设计”的位置。用Spring Boot Vue来做这套系统是Java方向里资料最密、面试也最常被追问的组合。Spring Boot负责REST接口、事务和文件存储Vue负责页面交互、路由守卫和请求封装前后端分离后能在简历里写出的技术点比单页项目多出一大截。这篇内容面向准备毕业设计或者刚拿到一个旧物回收系统的rar包、想把前后端完整跑通并讲清楚的人。下面按我常用的分工来讲先定数据模型再写后端接口和上传再写Vue调用最后把本地能跑通要补的配置位补齐。2. 先定表再写码旧物回收系统的数据模型与订单状态机2.1 角色怎么设计用字段还是独立表多数旧物回收系统只需要三种角色普通用户、回收员、管理员。回收员和管理员都共用同一张登录账号表通过role字段区分不需要一上来就建user_role、role_menu这类多对多关联表。这样做的直接好处是登录后把角色写进JWT前端路由守卫看一眼角色就能放行缺点是以后要看“权限点”级别的控制时还得回去补权限表。对于毕业设计角色字段足够。如果想把体系做得完整一点可以再加一张t_menu和t_role_menu但业务上没有动态菜单需求时那属于过度设计。真正应该花精力的是订单表和旧物表的关系以及订单状态由谁在什么时间点修改。2.2 五张核心表与关键字段直接照抄我一般会建五张表t_user账号表、t_category旧物分类表、t_goods旧物信息表、t_address用户地址表、t_recycle_order回收订单表。其中t_goods与t_user是多对一t_recycle_order与t_goods、t_user、t_address都是多对一表和表之间不需要强行建物理外键业务层通过userId、goodsId去查反而更好迁移。关键字段可以按下面这张表来定表名关键字段说明t_userid, username, password, phone, rolerole取值范围USER/RECYCLER/ADMINt_categoryid, name, sort分类不乱删删除前判断是否有旧物t_goodsid, user_id, category_id, title, images, quality_level, statusstatus: 0待审核/1展示中/2已回收t_addressid, user_id, receiver, phone, region, detail, is_default下单时快照到订单表t_recycle_orderid, order_no, goods_id, user_id, recycler_id, appointment_time, status订单状态机单独维护订单表是这张ER图的核心创建语句可以直接照抄一部分CREATE TABLE t_recycle_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 业务单号, goods_id BIGINT NOT NULL, user_id BIGINT NOT NULL, recycler_id BIGINT DEFAULT NULL COMMENT 接单回收员, address_id BIGINT NOT NULL, appointment_time BIGINT COMMENT 预约上门时间戳(ms), estimate_price DECIMAL(10,2) COMMENT 估价, final_price DECIMAL(10,2) COMMENT 最终成交价, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待接单 1待上门 2待确认 3已完成 4已取消, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_user_status (user_id, status), KEY idx_recycler_status (recycler_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT旧物回收订单;这里把预约时间设计成BIGINT毫秒时间戳而不是DATETIME。原因很实际Vue端日期选择器拿到的值转时间戳非常直接传回后端也不用担心Jackson把2026-08-01 10:30解析成乱七八糟的Date格式。数据库里看着不直观但查询时用FROM_UNIXTIME转一下就行。order_no不建议用自增id直接拼因为有业务可读性要求。简单做法是yyyyMMddHHmmss 三位随机数并发量不大时足够也不会被人从订单号猜到当天单量。2.3 订单状态机谁有权限把这单从0改到3回收订单不是每个字段都能随便改。给一个最小可用的状态集合0待接单、1待上门、2待确认、3已完成、4已取消。状态转换规则固定为下面这张表当前状态可执行动作下一状态操作人0用户取消4用户0回收员接单1回收员1回收员上门并报价2回收员2用户确认金额3用户2用户拒绝报价4用户这张表要画进系统设计文档里因为代码里到处都要判断if(status 2)。能绕开状态条件的地方尽量绕开后端统一拦截会更可靠。防重复提交是毕设里最容易被问到的点。用户双击“确认完成”两个请求同时到达Service层如果先查询再更新第二次查询拿到的还是旧状态最后可能重复执行两次完成逻辑。解决办法是加条件更新Update(UPDATE t_recycle_order SET status3, final_price#{finalPrice}, update_timeNOW() WHERE id#{id} AND user_id#{userId} AND status2) int confirmOrder(Param(id) Long id, Param(userId) Long userId, Param(finalPrice) BigDecimal finalPrice);返回值受影响行数是0说明状态已经被别人改过直接抛“订单状态已变化请刷新页面”。这种乐观锁写法比先select后update更可靠也方便写进简历的“防重复提交”回答里。2.4 两个索引位别忘记加第一个是t_recycle_order的(user_id, status)因为用户端列表几乎都是“查我的未完成订单”。第二个是t_goods的(category_id, status)首页旧物列表要按分类过滤并且只展示status1的数据。再往下就是创建时间的倒排序索引数据量上来了再考虑毕业设计阶段这两个联合索引已经能过。3. Spring Boot后端旧物回收系统接口层怎么搭才不像玩具3.1 用IDEA创建Spring Boot项目依赖选中这几个“springboot版本太高”是最近很常见的坑。Spring Boot 3.x要求JDK 17而且包名从javax换成jakarta网上很多两年前的MyBatis-Plus配置直接失效。做旧物回收系统不用追新Spring Boot 2.7.18搭配JDK 8/11就够用。在IDEA的Spring Initializr里创建项目时依赖选Web、MySQL Driver、Lombok、Validation。MyBatis-Plus需要手动加版本用3.5.3.2能兼容Boot 2.x。核心依赖长这样dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.2/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency如果JDK已经装了17那就老老实实用Boot 3.2.x并且把MyBatis-Plus升到3.5.5以上。不要因为教程旧就硬降JDKIDEA里改Project Structure的SDK比改代码简单但很多报错会误导人。3.2 统一结果体Result和全局异常接口返回不再乱前端不可能记住每个接口的返回格式所以后端第一件事是规定一个Result对象。它的结构只有三个字段code、message、data。code200代表成功500代表业务失败401代表未登录或token过期。Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT r new Result(); r.setCode(200); r.setMessage(ok); r.setData(data); return r; } public static T ResultT fail(String message) { ResultT r new Result(); r.setCode(500); r.setMessage(message); return r; } }有了这个基础Controller层每个方法返回Result就行。再配一个RestControllerAdvice把业务异常统一转成fail结果前端拦截器收到非200就直接弹message不需要每个页面自己catch一遍。3.3 旧物图片上传本地存储URL返回不碰数据库BLOB用户发布旧物图片是刚需。图片处理的常见做法有三种存数据库BLOB、存本地磁盘、存OSS/MinIO。数据库BLOB对MySQL不友好OSS需要额外账号和依赖毕业设计最实用的是本地磁盘存文件、数据库存路径。上传接口核心逻辑可以这样写PostMapping(/file/upload) public ResultString upload(RequestParam(file) MultipartFile file) throws IOException { String dirPath uploadDir; File dirFile new File(dirPath); if (!dirFile.exists()) { dirFile.mkdirs(); } String original file.getOriginalFilename(); String suffix original ! null ? original.substring(original.lastIndexOf(.)) : .jpg; String filename UUID.randomUUID().toString().replace(-, ) suffix; file.transferTo(new File(dirPath, filename)); return Result.success(/upload/ filename); }uploadDir从application.yml里读取比如upload.dirD:/recycle-upload。然后要把这个目录暴露成静态资源spring: servlet: multipart: max-file-size: 10MB max-request-size: 30MB web: resources: static-locations: file:${upload.dir}注意static-locations配置里必须带file:前缀否则Spring Boot会把它当成classpath路径。图片路径不会返回http://localhost:8080这种完整地址前端拿/upload/xxx.jpg拼接自己的VITE_BASE_URL端口不同环境都能跑。3.4 旧物列表查询MyBatis-Plus分页和条件过滤首页旧物列表和后台管理列表都需要分页。MyBatis-Plus的分页插件要先配一个Configuration类Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }查询旧物的Service方法就可以直接写PageGoods page new Page(pageNum, pageSize); LambdaQueryWrapperGoods wrapper new LambdaQueryWrapper(); wrapper.eq(StringUtils.hasText(categoryId), Goods::getCategoryId, categoryId) .eq(Goods::getStatus, 1) .orderByDesc(Goods::getCreateTime); goodsMapper.selectPage(page, wrapper);wrapper.eq的第一个条件传入StringUtils.hasText(categoryId)意思是categoryId没传时不拼这个条件传了才拼。不要手动拼接SQL字符串避免注入。pageSize要给一个上限比如超过50就强制设为50防止有人用pageSize9999把整个表拉走。3.5 订单状态变更和Transactional订单状态变更通常要同时改两张表改回订单表状态改旧物表状态。比如用户拒绝报价后订单状态变成4旧物状态要从2“已锁定”恢复成1“展示中”。这两步必须在一个事务里。Transactional(rollbackFor Exception.class) public void rejectOrder(Long orderId, Long userId) { int rows recycleOrderMapper.updateStatus(4, orderId, userId, 2); if (rows 0) { throw new BizException(订单状态已变化请刷新页面); } recycleOrderMapper.releaseGoodsByOrder(orderId); }rollbackFor Exception.class指的是任何异常都回滚不加的话Spring默认只在RuntimeException时回滚。这个区别是面试常问点写代码时最好显式声明。4. Vue前端用路由守卫、axios拦截器把页面和后端粘起来4.1 Vite Vue 3初始化依赖装一次过Vue前端我习惯用Vite创建比Vue CLI快很多。命令行执行npm create vitelatest old-recycle-web -- --template vue cd old-recycle-web npm install npm install vue-router4 pinia axios element-plus element-plus/icons-vuevue安装依赖时如果卡住或者出现ERESOLVE错误最常见的解决路径是删除node_modules和package-lock.json再重新安装很多版本冲突都是旧锁文件造成的假象。Node版本最好用18以上Vite 5对Node 16的兼容已经不在维护范围内。4.2 封装axios处统一处理token和业务码每个页面单独写axios会立刻乱套token注入、401跳转都难统一。先创建一个src/api/request.jsimport 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 Bearer ${token} } return config }) request.interceptors.response.use(res { const data res.data if (data.code 200) { return data } ElMessage.error(data.message) return Promise.reject(data) }, err { if (err.response err.response.status 401) { localStorage.removeItem(token) router.push(/login) } return Promise.reject(err) }) export default request请求拦截器里从localStorage拿token放到Authorization头响应拦截器里只认后端Result中的codeHTTP 401则统一清token跳登录。这样写的好处是后续所有API文件都只需要关心业务数据不用管鉴权。4.3 路由meta和全局守卫控制旧物回收页面访问vite创建的项目默认只有一个路由旧物回收系统至少要有登录页、首页、旧物详情页、发布页、订单页。给需要登录的页面加meta.auth标记{ path: /publish, name: Publish, component: () import(/views/Publish.vue), meta: { auth: true, roles: [USER] } }全局前置守卫检查metarouter.beforeEach((to, from, next) { const token localStorage.getItem(token) const role localStorage.getItem(role) if (to.meta.auth !token) { next(/login) } else if (to.meta.roles !to.meta.roles.includes(role)) { next(/) } else { next() } })用户没登录点发布页会跳登录页回收员角色访问用户页面会被踢回首页。这种路由守卫写法比每个页面onMounted里判断登录状态干净得多。4.4 旧物详情页通过路由参数发请求从列表页跳详情页Vue Router推荐用params传idconst router useRouter() router.push({ path: /goods/ id })详情页里取参数并加载数据import { useRoute } from vue-router const route useRoute() const goodsId route.params.id const detail ref(null) const loadDetail async () { const res await request.get(/goods/ goodsId) detail.value res.data }vue路由参数的常见误区是用query传对象然后刷新后丢失或者变成字符串。用params加path的方式参数会体现在URL上刷新不丢且后端也好做RESTful路由。4.5 旧物发布页图片上传不能用actionElement Plus的el-upload有一个action属性直接指向后端上传接口。但这种做法会把文件上传交给组件内部XMLHttpRequest不经过封装好的axios实例token根本不会带上去。正确做法是覆盖默认上传行为el-upload v-model:file-listfileList :http-requestcustomUpload list-typepicture-card :limit6 el-iconPlus //el-icon /el-uploadconst customUpload async ({ file, onSuccess, onError }) { const formData new FormData() formData.append(file, file) try { const res await request.post(/file/upload, formData) imageUrls.value.push(res.data) onSuccess(res) } catch (e) { onError(e) } }http-request接收一个对象里面包括file、onSuccess、onError。上传完成之前Element Plus会一直显示loading态必须在onSuccess之后才切换成图片。图片URL数组最后随旧物表单一起提交给后端后端存进t_goods的images字段。4.6 预约时间用时间戳省掉时区扯皮回收订单的预约时间要传给后端最省事的方式是前端先用getTime转成毫秒const payload { goodsId: detail.value.id, addressId: selectedAddress.value.id, appointmentTime: new Date(selectedTime.value).getTime() }后端appointmentTime字段定义成Long接收毫秒值直接落库。不用JsonFormat不配置Jackson时区也不会有UTC和CST相差8小时的问题。展示上门时间时前端再用formatTime(timestamp)转成YYYY-MM-DD HH:mm。5. 前后端都写完本地跑通旧物回收系统的三个关键配置5.1 devServer把/api转发到8080前端不再有跨域开发阶段Vite用自己的5173端口后端是8080直接请求会跨域。不用后端加CORS配置直接在vite.config.js里配置devServer的proxy转发import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })axios的baseURL是/api浏览器请求/api/goods/list时Vite开发服务器会原样转发到http://localhost:8080/api/goods/list。后端接口路径必须带上/api前缀或者用转发规则把/api去掉。我一般建议后端Controller统一写RequestMapping(/api/goods)这样前后端路径完全对应排查时少一层改写。5.2 upload目录和数据库时间配置404和连接错大多是这里上传图片后访问404先看application.yml里有没有file:前缀。数据库连不上先看连接串时区spring: datasource: url: jdbc:mysql://localhost:3306/recycle?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 servlet: multipart: max-file-size: 10MB max-request-size: 30MB改完配置后启动后端用一条命令验证连通性curl http://localhost:8080/api/goods/list?pageNum1pageSize10返回的JSON里code是200说明数据库、MyBatis-Plus、接口三层都通了。再浏览器访问http://localhost:5173看前端页面能否加载出这条列表数据。5.3 刷新404和打包后布局异常一个base一个hashVue项目打包后放到Spring Boot的static目录常见两处问题。第一处是刷新子路由404这是vue-router的history模式在静态服务器上找不到对应路径导致的。毕设最省事的方案是用hash模式const router createRouter({ history: createWebHashHistory(), routes })URL里会多一个#但部署到Tomcat、nginx、Spring Boot都能直接刷新。第二处是“vue打包后布局异常”CSS和图片路径全是绝对路径把Vite的base改成相对路径即可export default defineConfig({ base: ./, plugins: [vue()] })重新build后dist目录里的index.html引用js/css都变成相对路径放到src/main/resources/static下面启动Spring Boot访问http://localhost:8080/index.html整套旧物回收系统就在一个端口上完整跑起来了。本文还有配套的精品资源点击获取
返回列表