SpringBoot+Vue垃圾分类回收网站:全栈项目从设计到部署实战
发布时间:2026/10/2 8:42:53来源:尧图网络
小区里那几组四色垃圾桶表面贴着分类指南实际全靠保洁阿姨徒手二次分拣。我去年帮社区做了一套垃圾分类回收网站用的是SpringBootVue前后端分离架构把预约上门回收、积分奖励、兑换商品、后台管理这些环节全部串了起来。这里把项目的完整设计思路、关键技术选型和我在实际开发中踩过的坑一起记录下来。如果你正在做毕业设计或者想找个全栈项目练手、准备接点私活这篇内容应该能帮你少走不少弯路。整个项目从立项到正式上线大概花了三周后端SpringBoot负责业务接口和数据持久化前端Vue负责页面交互和状态管理中间用RESTful API通信完全前后端分离。另外还接入了MinIO做图片存储用Docker做环境编排整体不算复杂但涉及的知识点覆盖了当前企业级开发的主流技术栈。我把它拆成四个部分讲先聊整体设计思路再讲关键技术细节然后是核心功能的实操实现最后是问题排查和避坑经验。1. 项目整体思路与功能拆解1.1 为什么用 SpringBoot Vue 做前后端分离很多人首次接触前后端分离概念时容易纠结单体应用一个War包解决问题何必拆成两个项目我的体会是垃圾分类回收网站这种业务天然适合前后端分离。因为用户端、回收员端、管理后台三套界面的交互逻辑差异很大如果全部耦合在后端模板里改一个页面就要重新部署整个应用。拆开之后前端团队和后端团队可以并行开发Vue负责组件化渲染和路由切换SpringBoot只关心接口返回JSON职责清楚得多。从技术栈选择上SpringBoot在Java后端生态里几乎算标准答案。它简化了配置内嵌Tomcat自带Actuator健康检查配合MyBatis-Plus操作数据库非常顺手。Vue则胜在轻量灵活组件化开发让页面复用变得简单特别是管理后台这种典型CRUD场景写一个通用表格组件就能反复用。配合Vite构建工具本地开发时热更新速度快几乎不用等编译。1.2 核心功能模块梳理这套网站我按角色分了三大类模块实际开发时也是按这个边界拆解的。角色功能模块核心流程普通用户注册登录、预约回收、订单查询、来雨露积分、积分商城在线预约上门回收、回收完成后获得积分、积分兑换商品回收员今日任务、接单、回收登记、重量确认、结算抢单或派单、上门称重、录入回收记录、给用户发放积分管理员用户管理、回收员审核、分类管理、商品管理、数据统计维护基础数据、查看回收报表、处理异常订单用户端我重点做了垃圾分类百科接通了一个可回收物查询接口输入名称就能看到属于哪一类、如何投放。当初这个功能是为了提升网站活跃度实际去社区演示的时候大爷大妈们还真拿它查了不少东西。回收员端是移动端H5页面重点突出接单和登记效率界面只保留必要按钮尽量减少输入项。管理后台就是标准的数据表格加筛选项导出功能做了Excel下载方便运营人员做月度结算。1.3 业务流程的文字拆解核心流程是用户提交预约单填写地址、上门时间、可回收物种类系统生成订单号回收员在移动端看到可接订单确认后按约定时间上门回收员称重并拍照上传系统根据重量和品类按规则生成积分用户确认收货后积分入账积累的积分可以在商城兑换垃圾袋、纸巾等小商品。这里有几个设计细节需要注意。第一订单状态我用数字常量表示0待接单、1已接单、2已完成、3已取消字段类型是tinyint不要用字符串存中文状态否则查询统计会很痛苦。第二积分计算规则单独建表配置重量单价和积分倍率都可以调整这样运营改规则时不用发版。第三所有关键操作都加操作日志比如谁取消了订单、谁修改了重量出了问题回溯起来非常方便。2. 关键技术选型与实现细节2.1 SpringBoot 后端核心配置后端项目我用的Java 17 SpringBoot 2.7.x没有追新上SpringBoot 3.x原因很简单第三方兼容性。如果用的组件版本不够新升级到3.x后很多启动报错会让你排查半天。生产环境稳定性优先而不是追求版本号最新这是我个人的一个习惯。项目依赖上重点加了这几类spring-boot-starter-web处理接口请求mybatis-plus-boot-starter做ORMmysql-connector-j驱动数据库jjwt做登录令牌minio做文件存储spring-boot-starter-data-redis做缓存和限流。以下是application.yml的核心配置片段。server: port: 8080 servlet: context-path: /api spring: datasource: url: jdbc:mysql://localhost:3306/recycle?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver servlet: multipart: max-file-size: 20MB max-request-size: 50MB redis: host: localhost port: 6379 database: 0 mybatis-plus: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl minio: endpoint: http://localhost:9000 access-key: minioadmin secret-key: minioadmin bucket: recycle-images jwt: secret: your-secret-key-change-me expire-days: 7MyBatis-Plus的map-underscore-to-camel-case建议打开数据库字段的下划线命名自动映射成驼峰属性省去一大堆ResultMap配置。不过有一点要注意代码开log-impl: StdOutImpl是为了开发期看SQL方便上线前务必改成logback配置不然每一条SQL都往控制台刷日志量会让服务器很难受。2.2 Vue 前端环境与核心配置前端我用的是Vue3 Element Plus Vite相比Vue2组合式API在逻辑复用上顺手太多。安装依赖时有大坑一定先确认Node版本和npm源我一开始直接用默认npm源装依赖速度慢不说还经常报prisma的坑换成npmmirror镜像之后清净了。注意安装依赖时的网络问题这不是什么敏感话题路由配置是前端项目的骨架垃圾分类回收网站包含前端用户端和移动端两个入口我放在同一个项目里按路由懒加载拆分。以下是router/index.js的简化示例。import { createRouter, createWebHistory } from vue-router const routes [ { path: /, component: () import(/layout/HomeLayout.vue), children: [ { path: , name: Home, component: () import(/views/Home.vue) }, { path: appointment, name: Appointment, component: () import(/views/Appointment.vue) }, { path: orders, name: Orders, component: () import(/views/OrderList.vue) }, { path: mall, name: Mall, component: () import(/views/Mall.vue) } ] }, { path: /worker, component: () import(/layout/WorkerLayout.vue), children: [ { path: tasks, name: WorkerTasks, component: () import(/views/worker/TaskList.vue) }, { path: record, name: WorkerRecord, component: () import(/views/worker/RecordForm.vue) } ] } ] const router createRouter({ history: createWebHistory(), routes }) export default routerAxios请求封装我单独提出来说。直接在页面里写请求代码后期维护会想哭。我把请求实例统一封装在utils/request.js设置了baseURL、请求拦截器自动携带token、响应拦截器统一处理code和HTTP错误码。这样任何一个接口返回401时前端全局跳转到登录页不是每个页面重复写。2.3 文件与图片上传为什么我选了 MinIO分类回收业务里大量涉及图片上传例如回收员上门称重时必须拍下物品照片留作凭证后台审核时也需要查看不可回收物照片。最开始我想直接把图片存到服务器磁盘但考虑到后续横向扩展本地磁盘方案太局限。于是采用了MinIO它是开源的对象存储服务兼容AWS S3协议社区里有大量资料对本地化部署也很友好。MinIO整合进SpringBoot的步骤很直接引入minio依赖创建配置类读取endpoint和access-key封装一个MinioUtil工具类提供上传、下载、删除三个核心方法。关键代码示例Service public class MinioService { private final MinioClient minioClient; public MinioService(MinioProperties properties) { this.minioClient MinioClient.builder() .endpoint(properties.getEndpoint()) .credentials(properties.getAccessKey(), properties.getSecretKey()) .build(); } public String upload(MultipartFile file, String objectName) throws Exception { // 检查bucket是否存在不存在则created boolean found minioClient.bucketExists(BucketExistsArgs.builder() .bucket(properties.getBucket()).build()); if (!found) { minioClient.makeBucket(MakeBucketArgs.builder() .bucket(properties.getBucket()).build()); } // 上传文件 minioClient.putObject(PutObjectArgs.builder() .bucket(properties.getBucket()) .object(objectName) .stream(file.getInputStream(), file.getSize(), -1) .contentType(file.getContentType()) .build()); return properties.getEndpoint() / properties.getBucket() / objectName; } }这里需要注意bucket权限问题。默认bucket为私有通过链接直接访问会报AccessDenied。如果用于用户头像这些公开图片可以在MinIO控制台把bucket的Access Policy改为public但回收凭证图片有隐私属性建议保留私有后台通过预签名URL临时访问避免凭证图片被公开抓取。2.4 数据库设计要点垃圾分类回收网站的数据库表设计并不复杂但字段规划直接影响开发效率。我梳理了七张核心表下面只挑关键的几个字段说明。t_userid, openid, nickname, avatar, phone, points, statust_workerid, user_id, audit_status, work_status, online_timet_appointmentid, user_id, worker_id, address, appointment_time, goods_type, status, remarkt_recycle_recordid, appointment_id, worker_id, category_id, weight, points, photo_urlt_goods_categoryid, parent_id, name, icon, points_per_kgt_exchange_goodsid, name, image, points_required, stockt_exchange_recordid, user_id, goods_id, points_cost, exchange_time这里有些细节值得展开。appointment_time在数据库里我存的是datetime字符串用前端组件选择的日期时间生成后端始终按“用户提交的本地时间”处理。如果想做时区统一建议存UTC时间戳但也没必要在这个项目里卷。category表设置了parent_id做树形结构因为可回收物分类存在二级分类例如“纸类”下面有“旧报纸”“纸箱”等小类。重量字段weight用decimal(10,2)不要用float精度问题会直接影响积分计算。另外每一张业务表都加了create_time、update_time、logic_delete三个字段。逻辑删除特别重要预约单如果物理删除记录售后纠纷时查不到依据。MyBatis-Plus提供了TableLogic注解配置后自动在查询时过滤已删除记录。3. 实操过程与核心环节实现3.1 后端核心接口开发预约下单、接单、记录上传预约下单接口是用户端最重要的接口。我的设计是前端传appointment对象后端在Service层统一校验再插入订单并通过Redis记录该用户今日剩余可预约次数。防止恶意刷单的关键是幂等性设计预约单号用时间戳加随机数生成接口层加了重复提交令牌。Controller层一段示例RestController RequestMapping(/appointment) public class AppointmentController { Resource private AppointmentService appointmentService; PostMapping(/create) public R create(RequestBody Valid AppointmentCreateDTO dto) { // token用于防重复提交 appointmentService.checkRepeatToken(dto.getToken()); Appointment appointment appointmentService.createAppointment(dto); return R.ok(appointment); } PostMapping(/take) public R takeOrder(RequestBody TakeOrderDTO dto) { // 回收员接单 boolean success appointmentService.takeOrder(dto.getAppointmentId(), dto.getWorkerId()); return success ? R.ok() : R.fail(接单失败订单可能已被抢); } }小技巧接单接口一定要用乐观锁或者行锁不然两个回收员同时看到订单都点了接单最后订单就变成双接了。我用的方法是update appointment set worker_id?, status1 where id? and status0受影响行数为1才说明抢单成功这也是最朴素可靠的乐观锁写法。订单完成后的积分结算在事务里执行。步骤是先更新预约单状态再插入回收记录再更新用户积分最后记录积分流水。这四步必须处于同一事务任何一个失败都要整体回滚。业务上最严重的错误是状态变成了已完成但用户积分没加上用户端会直接炸锅。3.2 前端页面与接口联调实战前端联调阶段往往占了整个项目一半的开发时间。我以预约表单页举例用户在预约页选择上门时间、填写地址、勾选回收物类型。组件提交前需要二次校验地址字数、回收物类型不能为空这些规则我用Element Plus的FormRules表单验证实现贴合业务场景。核心代码片段template el-form refappointmentFormRef :modelform :rulesrules el-form-item label上门时间 propappointmentTime el-date-picker v-modelform.appointmentTime typedatetime placeholder选择上门时间 / /el-form-item el-form-item label地址 propaddress el-input v-modelform.address placeholder请输入详细地址 / /el-form-item el-form-item label可回收物类型 propgoodTypes el-checkbox-group v-modelform.goodTypes el-checkbox-button v-foritem in categoryList :keyitem.id :valueitem.id {{ item.name }} /el-checkbox-button /el-checkbox-group /el-form-item /el-form /template script setup import { reactive, ref } from vue import { request } from /utils/request const form reactive({ appointmentTime: , address: , goodTypes: [] }) const rules { address: [{ required: true, message: 请输入地址, trigger: blur }], goodTypes: [{ required: true, type: array, min: 1, message: 至少选择一种回收物, trigger: change }] } async function submit() { const res await request.post(/appointment/create, { ...form, token: generateToken() }) if (res.code 0) { // 跳转订单列表 } } /script联调时最烦的接口字段大小写不一致问题。后端规范是驼峰命名但一些历史接口返回了下划线字段前端拿到数据后总是存在res.user_id和res.userId弄混的情况。后来我订了规则Java实体统一驼峰前端接收字段宽进严出如果后端返回了下划线字段在axios响应拦截里做一层转换统一转成驼峰。3.3 用 Docker 把前后端一起部署到服务器开发环境一切正常部署上线时容易翻车。传统方式是后端打jar包、前端dist目录丢Nginx但换个服务器就要重新搭环境而且数据库、Redis、MinIO、Nginx互相依赖配置很容易乱。我用docker-compose把整套环境编排起来一次部署多处复用。后端Dockerfile示例FROM maven:3.8.6-openjdk-17 AS build WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn clean package -DskipTests FROM openjdk:17-jre-slim COPY --frombuild /app/target/recycle-backend.jar /app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, /app.jar]前端Dockerfile也可以用多阶段构建先用node镜像npm run build产出静态文件再拷贝到nginx镜像。但更重要的是docker-compose里网络和容器依赖怎么写。我贴一个精简版version: 3.8 services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: recycle ports: - 3306:3306 volumes: - mysql-data:/var/lib/mysql networks: - recycle-net minio: image: minio/minio command: server /data environment: MINIO_ROOT_USER: minioadmin MINIO_ROOT_PASSWORD: minioadmin ports: - 9000:9000 volumes: - minio-data:/data networks: - recycle-net backend: build: ./backend environment: SPRING_PROFILES_ACTIVE: docker depends_on: - mysql - minio ports: - 8080:8080 networks: - recycle-net frontend: build: ./frontend ports: - 80:80 depends_on: - backend networks: - recycle-net volumes: mysql-data: minio-data: networks: recycle-net: driver: bridge这里要特别强调depends_on只是控制启动顺序不代表数据库已经完全初始化完成。后端一旦启动发现连不上MySQL会立刻报错退出。稳妥做法是在backend启动脚本里加等待逻辑循环尝试连接数据库或者用healthcheck健康检查依赖。家庭小项目可以直接用restart: always让系统自动重启等MySQL起来了后端自然连上简单粗暴但很有效。3.4 关键参数配置说明开发过程中有好几个参数反复折磨人这里集中列一个表格。配置项推荐值原因后端端口8080默认内网访问Nginx监听端口80前端入口JWT过期时间7天用户端体验能续期上传文件大小限制20MB称重照片原图一般不超过5MBMinIO预签名URL有效期60分钟凭证照片不能永久公开Redis连接池maxTotal50足够支持小规模并发JWT密钥一定不要硬编码在配置文件里而且长度不低于32位否则容易被暴力破解。上传限制别只改前端或只改后端两边都要配置一致我最初只改了SpringBoot的multipart配置前端以为图片传上去了结果请求直接被Nginx拦截页面报413排查了半天才发现Nginx的client_max_body_size还要专门调。4. 常见问题排查与经验分享4.1 跨域问题你真的解决了吗前后端分离项目绕不开CORS。开发阶段最常见的是浏览器控制台报Access to XMLHttpRequest has been blocked by CORS policy。我的办法是后端写一个全局CorsFilter允许本地开发地址。生产环境交给Nginx反向代理让前端访问/api时后端转发这样浏览器看到的是同源请求不需要配置跨域。但这里有个坑如果使用了Chrome插件、跨域拦截工具等中间层请求方式被改OPTIONS预检请求后端没处理一样会出现跨域问题。我建议在后端Security配置里明确放行OPTIONS请求同时考虑老老实实配置Nginx代理把前端请求转发给后端。等两边都配好跨域问题才算彻底解决。4.2 图片上传超时和413错误上传照片时移动端网络差的情况下很容易出现超时。Axios默认配了超时时间我设为了10秒但大文件上传10秒完全不够。后来我把超时时间调成30秒但又不能太大否则服务器连接数容易被占满。更优解是分片上传社区项目不太需要这么复杂最简单是支持压缩图片后再上传前端用canvas把超过2MB的图片压到500KB以内上传速度会快很多。还有413 Request Entity Too Large这是Nginx的client_max_body_size限制。默认只有1MB直接无法上传。要么改Nginx配置为20m要么在后端做一个中间件校验文件大小提前返回错误提示不要让请求打到后端了才发现。我两种都做了前端还能拿到明确提示。4.3 前端路由刷新404问题Vue使用createWebHistory模式后刷新页面时容易遇到404。原因很简单history模式基于History APINginx没有一个默认的rewrite规则刷新时服务器找不到真实的文件路径。解决办法是在Nginx配置里加上location / { try_files $uri $uri/ /index.html; }这个配置让所有前端路由请求都回退到index.html前端Router再根据URL渲染对应组件。如果没有这个配置每次用户刷新订单详情页都会一头撞上404。如果你是部署在子路径下记得在Vite配置里设置base不然静态资源路径会全部错位。4.4 联调阶段的避坑清单最后把这些坑整理成一份速查清单都是我实际验证过的经验。后端接口字段改了前端缓存了旧的接口定义没刷新联调前先清理浏览器缓存。MyBatis-Plus分页插件没有注册分页查询不生效只会返回所有记录。多线程环境下数据库连接被占满排查连接池泄漏注意finally里关连接。定时任务没有加分布式锁多个后端实例部署时定时任务重复执行社区单实例部署没有大碍但要注意。日志不打印SQL上线后排查问题时才发现log-impl没改SQL看不到根本没法定位。小程序/H5使用localStorage存token刷新页面会丢失配合Cookie或者Pinia持久化更好。回收员接单后没有刷新订单列表前端需要调用接口重新拉取订单状态而不是用事件总线硬编码逻辑。排查问题时我习惯先看后端日志找到异常堆栈再看前端Network面板确认请求参数和响应状态码两者结合起来基本能判断是接口逻辑问题还是数据格式问题。垃圾分类回收网站这个项目本身不复杂但涉及的环节很多从搭建框架、设计数据库、开发接口到前端联调、部署上线每走一步都会遇到大大小小的问题。把这些经验记录下来下次再做类似的管理系统或商业项目处理速度会快很多。这个项目后续我还在考虑扩展数据可视化的功能把每天回收的重量、积分发放量、热门回收品类做成大屏展示配合ECharts实时刷新这样社区运营人员能更直观地看到垃圾减量和资源回收的效果。前端页面目前用的还是Element Plus默认风格后续打算引入TailwindCSS做一轮视觉升级配合暗色模式作为个人作品拿出来也更有说服力。如果你想上手这个项目建议先从预约下单主流程开始跑通再逐步完善积分商城和后台管理千万不要一上来就想把整个系统做完循序渐进反而更快。
网站建设高端定制企业官网