简介:这是一份基于 Spring Boot 与 Vue.js 的废品回收系统小程序源码,面向希望学习前后端分离开发或进行回收业务二次开发的开发者。压缩包共 37 个文件,大小 2.87MB,包含 Java 后端业务逻辑、HTML/CSS/JS 前端页面、yml/xml 配置、图片字体资源,以及 docx 必读说明和 pdf 配置说明,目录清晰,便于快速定位。项目覆盖用户认证、废品价格查询、回收订单处理、废品分类与回收流程管理等典型模块,展示了 Spring Boot 快速构建接口与 Vue 数据驱动视图的协作方式,对理解系统设计和前后端联调很有帮助;源码中的 Java 算法文件和配置文件,还能帮助读者掌握回收定价或订单分配相关的实现思路。当前已有 20 人学习,适合开发者从源码入手,结合文档分析模块实现,理解核心功能的完整链路,并在此基础上升级功能或替换业务流程。
1. 废品回收系统小程序到底在做什么:一个被低估的全栈样本
废品回收系统小程序,本质上是一套「用户端下单预约回收 + 回收员接单上门称重 + 管理员后台定价审核」的三端闭环项目,技术栈是微信小程序原生 + Vue 管理后台 + Spring Boot 后端。我见过太多人把这个项目当成简单的 CRUD 课设,实际动手才发现:运费模板计算、订单状态机、微信手机号授权、小程序真机调试这几块才是真正卡人的地方。不论你是想把它当作毕设、接单练手,还是给社区做个再生资源回收的 demo,这套源码能让你在一天内摸清前后端分离项目的骨架和坑。下面我按自己做过的落地路径,把选型理由、接口设计、踩坑记录一次讲清楚。
2. 先把业务边界画清楚:三个端各管什么事,表怎么设计
2.1 角色拆解与权限模型:别一上来就写代码
拿到「废品回收系统」这类项目标题,第一反应不该是建工程,而是先问:谁在用这个系统?我一般梳理成三类角色。C 端用户通过微信小程序提交回收预约,选择地址、回收品类、预计重量和上门时间;回收员在小程序端(或专用的小程序窗口)接单、上门、称重、结算;管理员在 Vue 后台维护回收品类、运费模板、审核回收员资质、查看订单统计。
权限模型不一定要引入 Spring Security 那一套重东西。常见做法是:用户表里加一个role字段,取值USER、COLLECTOR、ADMIN,配合 JWT 拦截器做接口级鉴权。小项目这么做完全够用,等到真的需要细粒度权限(比如区域回收员只能看自己片区的订单)再上 Sa-Token 或 Spring Security 也不迟。
2.2 核心表设计:订单表是心脏,运费模板表是良心
我把表结构画出来再动手写代码,一般至少需要六张表:user(用户)、recycle_category(回收品类)、order(回收订单)、order_item(订单明细,记录每类废品的重量和金额)、freight_template(运费模板)、collector_apply(回收员入驻申请)。
订单表字段里,除了常见的外键和状态,有两个字段建议从一开始就留好。一个是order_no(订单编号),别用自增 ID 直接暴露给用户,用时间戳 + 随机数生成个业务号;另一个是coupon_amount(优惠金额),哪怕第一期不做优惠券,也先把字段埋上。返工改表最烦,尤其是订单这种核心表。品类表和运费模板表的关联方式是:每个品类挂一个freight_template_id,管理员调整价格时只改模板不动订单。
2.3 订单状态机:从预约到结算一共七个节点
废品回收和普通电商最大的区别是有一个「上门称重」的线下环节,所以订单状态不能只靠支付驱动。我设计的状态流是这个顺序:待接单 → 已接单 → 待上门 → 已称重 → 待结算 → 已完成,外加一个终态已取消。每一笔订单在状态流转时,后端必须记录状态变更日志,否则用户投诉「我的订单怎么从待接单变成已取消了」时,你没证据可查。
状态机的实现不建议用一堆if/else在 Controller 里硬写。我习惯在每个状态节点上写清楚「允许从哪些状态进入、由谁触发、需要哪些前置条件」。比如「已接单」只能由待接单状态进入,触发人必须是回收员角色,前置条件是该订单没有被其他回收员抢先接走。这种约束写在后端 Service 里,比放前端控制可靠得多。后面第 3 章我会给出具体的代码模板。
3. Spring Boot 后端落地:订单状态机、运费计算与权限拦截
3.1 工程结构与依赖选型:Java 8 + Spring Boot 2.7.x 最稳
废品回收系统这类中小型项目,我一般用 Java 8 配 Spring Boot 2.7.x,别追新。原因很现实:很多教程、开源组件、云服务器上的 JDK 环境还在 Java 8,Spring Boot 3.x 强制要求 JDK 17 且javax.servlet包名改为jakarta.servlet,一旦选型失误,你会发现网上搜到的老代码全跑不起来——这个坑在第 5 章我会展开讲。
基础依赖就四个:spring-boot-starter-web、mybatis-plus(数据库操作)、jjwt(生成和解析 JWT)、lombok(省略 getter/setter)。MyBatis-Plus 在这个项目里比 JPA 顺手,因为它自带分页插件和条件构造器,写订单查询这类多条件过滤时能少写很多 XML。
工程结构按功能分包,不按技术层次分包。也就是说,不要建controller、service、mapper三个平级包然后互相引用,而是每个业务模块一个包:order、user、recycle、freight。这样后期维护时,改一个订单功能只需要进order包,不会牵连别的模块。
3.2 订单状态机的代码实现:状态流转写在 Service 里
接单接口的核心代码,我一般这么写:
// OrderService.java @Transactional(rollbackFor = Exception.class) public boolean acceptOrder(Long orderId, Long collectorId) { // 1. 查询订单并加锁,防止两个回收员同时接同一单 Order order = orderMapper.selectByIdForUpdate(orderId); if (order == null) { throw new BizException("订单不存在"); } // 2. 状态机校验:只有待接单状态才能被接单 if (!OrderStatus.PENDING_ACCEPT.getCode().equals(order.getStatus())) { throw new BizException("该订单已被接走或已取消"); } // 3. 业务校验:回收员必须已通过资质审核 User collector = userMapper.selectById(collectorId); if (collector == null || !"APPROVED".equals(collector.getCollectorStatus())) { throw new BizException("回收员资质未通过审核"); } // 4. 更新订单状态和回收员信息 order.setStatus(OrderStatus.ACCEPTED.getCode()); order.setCollectorId(collectorId); order.setAcceptTime(LocalDateTime.now()); orderMapper.updateById(order); // 5. 写状态变更日志 orderLogMapper.insert(OrderLog.build(order.getId(), OrderStatus.PENDING_ACCEPT.getCode(), OrderStatus.ACCEPTED.getCode(), collectorId)); return true; }逻辑说明:第一步用了selectByIdForUpdate行级锁,这是避免并发接单的关键。两个回收员同时点接单时,数据库层面会串行化,后执行的那个事务读到的是加了锁之后的最新状态,状态机校验就会失败,接口直接抛出「该订单已被接走」。第二步到第四步是状态机的核心约束:状态必须按预设方向流转,不允许跳状态或回退。第五步写日志属于锦上添花,但生产环境排查问题时就靠它。
参数说明:OrderStatus是枚举类,PENDING_ACCEPT 表示「待接单」,ACCEPTED 表示「已接单」;BizException是自定义业务异常,由全局异常处理器统一捕获并返回{ "code": 400, "msg": "..." }格式给前端。事务注解@Transactional保证了状态更新和日志写入在同一次事务里,要么都成功,要么都回滚,不会出现订单状态改了但日志丢了的情况。
3.3 运费模板计算:重量阶梯计价,金额一律用分存储
废品回收的计价规则,我遇到过三类:按品类单价乘重量、按重量区间给不同单价、上门服务费按距离算。最省事的方案是「重量阶梯计价」,比如纸壳 10 公斤以内每公斤 0.8 元,超过 10 公斤部分每公斤 0.6 元。实现方式放在FreightTemplateService里:
// FreightTemplateService.java public BigDecimal calcAmount(Long categoryId, BigDecimal weightKg) { FreightTemplate template = freightTemplateMapper.selectByCategoryId(categoryId); if (template == null) { throw new BizException("该品类暂未开通回收服务"); } // 阶梯计价:第一阶梯 thresholdKg 内按 priceFirst,超出部分按 priceSecond if (weightKg.compareTo(template.getThresholdKg()) <= 0) { return weightKg.multiply(template.getPriceFirst()) .setScale(0, RoundingMode.HALF_UP); // 金额转分为整数 } BigDecimal within = template.getThresholdKg().multiply(template.getPriceFirst()); BigDecimal excess = weightKg.subtract(template.getThresholdKg()) .multiply(template.getPriceSecond()); return within.add(excess).setScale(0, RoundingMode.HALF_UP); }逻辑说明:金额存储一律用「分」这个整数单位,避免浮点数运算带来的 0.1 + 0.2 不等于 0.3 的问题。数据库字段类型用BIGINT,实体里用Long或BigDecimal都行,但计算时必须用BigDecimal而不是double。最后setScale(0, RoundingMode.HALF_UP)是把计算结果四舍五入到整数分。
参数说明:thresholdKg是阶梯分界重量,priceFirst是第一阶梯单价(分/公斤),priceSecond是超出部分的单价。这两个字段由管理员在 Vue 后台维护,修改后立即生效。这里有坑:如果前端展示的单价是元,后端接收时必须先转换成分为整数再入库,我见过太多人因为单位不统一导致结算金额差一百倍——第 5 章避坑部分还要再提一次。
3.4 权限拦截:JWT 拦截器与注解鉴权
接口鉴权我用 JWT + HandlerInterceptor 实现。登录成功后签发 token,后续请求在 Header 里带Authorization: Bearer <token>,拦截器解析 token 并提取用户 ID 和角色:
// JwtInterceptor.java @Component public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (token != null && token.startsWith("Bearer ")) { token = token.substring(7); try { Claims claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody(); request.setAttribute("userId", claims.get("userId")); request.setAttribute("role", claims.get("role")); return true; } catch (Exception e) { // token 过期或非法,统一返回 401 } } response.setStatus(401); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":401,\"msg\":\"登录已过期,请重新登录\"}"); return false; } }逻辑说明:preHandle 里先取 Header 中的 token,解析成功后把用户信息塞进 request attribute,Controller 里就能通过@RequestAttribute直接拿。解析失败统一返回 401 JSON,不让请求继续往下走。这个拦截器只做身份认证,角色鉴权放 Controller 层用@RequireRole("COLLECTOR")这类自定义注解做会更清晰。
参数说明:secretKey是签名密钥,至少 32 位字符串,生产环境必须放在配置中心或环境变量里,不能写死在代码里。token 有效期一般设 2 小时,小程序端每次启动时静默调用刷新接口续期。注意:Forbidden 不等于 Unauthorized,角色不匹配返回 403,登录过期返回 401,很多新手把这两个状态码混用,前端拿到后不好判断该跳登录页还是弹「无权限」提示。
4. 微信小程序端实现:手机号登录、接单推送与地图选点
4.1 微信小程序登录获取手机号:不是调一个接口那么简单
小程序端最重要的登录链路是「微信登录获取手机号」。很多新手以为前端拿到wx.login的 code 传给后端就能直接换手机号——这是错的。正确流程分三步:wx.login拿 code 换 openid;用户点击授权弹窗触发e.detail.code拿到手机号动态令牌;后端拿这个动态令牌调微信接口换真实手机号。
第一步wx.login的代码长这样:
// pages/login/login.js wx.login({ success: async (res) => { if (res.code) { // 将 code 传给后端,后端用 code + appid + secret 换 openid 和 session_key const loginRes = await wx.request({ url: 'https://your-server.com/api/auth/wx-login', method: 'POST', data: { code: res.code } }); // 后端返回的自定义登录态 token,后续所有请求都带上 wx.setStorageSync('token', loginRes.data.data.token); } } });逻辑说明:wx.login获取的 code 只能使用一次,有效期五分钟。后端拿这个 code 调用微信的jscode2session接口,拿到 openid 和 session_key。openid 是用户在当前小程序下的唯一标识,拿它查数据库,有记录就直接登录,没记录就自动注册一个账号。这里不建议直接拿 openid 当登录凭证返给前端,而是后端自己签发 JWT token,这样 token 过期时间、角色信息都能自定义控制。
参数说明:wx.request的 URL 不能是 IP 或 localhost,必须是小程序后台配置的合法域名,且必须是 HTTPS。开发阶段可以在开发者工具里勾选「不校验合法域名」,但真机预览时这个开关不生效——这是新手最常翻车的地方,第 5 章细说。
第二步手机号授权,代码是:
// pages/profile/profile.js async handleGetPhoneNumber(e) { if (e.detail.code) { // code 是动态令牌,5 分钟内有效,且只能用一次 const res = await wx.request({ url: 'https://your-server.com/api/auth/phone', method: 'POST', data: { code: e.detail.code, token: wx.getStorageSync('token') } }); // 后端拿 code 调微信接口换手机号 if (res.data.code === 0) { wx.showToast({ title: '手机号绑定成功' }); } } else { // 用户拒绝了授权 wx.showToast({ title: '不授权无法下单', icon: 'none' }); } }逻辑说明:从基础库 2.21.2 开始,open-type="getPhoneNumber"的按钮返回的是e.detail.code,不是手机号明文。后端要用这个 code 调微信的接口换手机号。这个 code 是一次性的,有效期为五分钟,所以拿到后要立刻传给后端。用户拒绝授权时,e.detail.code是空的——此时不能弹个 toast 就了事,业务上要引导用户手动填写手机号作为兜底方案。
4.2 接单消息的实时性:WebSocket 还是轮询
废品回收订单的时效性很强,用户预约了明天上午九点上门,回收员得尽快接单。消息推送有两条路:WebSocket 长连接,或者小程序端定时轮询。我建议小项目先用轮询——微信小程序的 WebSocket 在切后台时有可能会被系统断开,重新建立连接的逻辑复杂度远超你的预期。
我用轮询的方案是:回收员端小程序每隔 15 秒请求一次/api/order/pending-list?status=PENDING_ACCEPT,有新订单就震动 + 弹提示。接口里用updatedAt做增量查询,只返回最近更新的订单,减少流量浪费。等日活上千、回收员数量超过五十人再换 WebSocket 不迟。
这里有一句话要说:接单页面的「立即接单」按钮一定要做防重复点击。用户手快了连点两次,后端有状态机拦着不会出现两张单,但前端要提示「接单成功」,否则回收员以为没点上又点一次,后端返回订单已接走,用户就会觉得系统有 bug。
4.3 地图选点与地址管理:腾讯位置服务的小程序适配
用户预约回收时得选上门地址,地图选点我推荐用腾讯位置服务微信小程序 SDK,因为微信内置了wx.chooseLocation,不需要额外申请地图 SDK key,拿到的经纬度和地址名可以直接存库:
// pages/order/create.js async chooseAddress() { const res = await wx.chooseLocation({ success: (res) => { this.setData({ address: res.address, latitude: res.latitude, longitude: res.longitude }); } }); }逻辑说明:wx.chooseLocation需要在app.json里声明permission和requiredPrivateInfos,否则真机调用会直接失败。声明方式如下:
{ "permission": { "scope.userLocation": { "desc": "你的位置信息将用于选择回收上门地址" } }, "requiredPrivateInfos": ["getLocation", "chooseLocation"] }参数说明:如果不加requiredPrivateInfos,开发工具里可能正常,但真机预览时接口返回fail: no permission——这是 2022 年微信新增的隐私接口管控,属于高频翻车点。另外,经纬度坐标一定要用小程序返回的原始值,不要前端自己转换坐标系,后端对接第三方地图(比如高德)时再统一转换 GCJ-02 与 WGS-84,否则位置会偏几百米。
5. 部署与联调避坑:springboot 版本、小程序抓包、合法域名这些我踩过的坑
5.1 Spring Boot 版本太高,javax 改 jakarta 导致老代码全废
现象:用 Spring Boot 3.x 创建工程,引入网上找的 MyBatis-Plus 或其他第三方依赖,启动报ClassNotFoundException: javax.servlet.Filter,或编译错误cannot find symbol javax.servlet.http.HttpServletRequest。
原因:Spring Boot 3.0 从 Java EE 迁移到 Jakarta EE,所有javax.*包名变成jakarta.*。老教程、老代码、相当一部分第三方 starter 都还停留在 javax。如果你照着一个 2.x 写的教程搭 3.x 的工程,十有八九会对不上。
解决:废品回收这种项目直接用 Java 8 + Spring Boot 2.7.x。两个版本的功能差异对你这个项目没有任何影响,但网上 90% 的教程和问答都能直接照抄。如果你已经建了 3.x 工程,要么全局替换javax.为jakarta.(只影响少量 import),要么把 spring-boot-starter-parent 版本降到 2.7.18。别在这上面较劲,没有意义。
5.2 微信小程序请求接口报「不在以下 request 合法域名列表中」
现象:开发者工具里勾选了「不校验合法域名」,一切正常;一真机预览,所有wx.request全部失败,报url not in domain list。
原因:小程序真机环境强制校验合法域名,开发者工具的开关只在本地调试时生效。你把后端跑在http://localhost:8080或局域网 IP 上,真机当然访问不了。
解决:两个办法。第一,部署阶段花钱或找一台云服务器,把 Spring Boot 服务通过 Nginx 代理成 HTTPS 域名,域名在小程序后台配到合法域名列表里。第二,开发阶段用微信开发者工具的「真机调试」功能,它允许你填入本机局域网 IP——注意手机和电脑必须在同一 Wi-Fi 下,且电脑防火墙要放行 8080 端口。我见过最离谱的情况是后端服务正常、手机也能 ping 通电脑,但就是连不上,最后发现是 Windows 防火墙默认拦截了 Java 进程。
5.3 小程序抓包:Charles 配好也抓不到 HTTPS 流量
现象:用 Charles 抓小程序请求,看到一堆CONNECT请求但没有具体接口内容,或直接显示SSL Handshake Failed。
原因:小程序对 HTTPS 证书有完整性校验,Charles 的 CA 证书默认不被信任。你以为装了 Charles 根证书就行,实际上微信对本地证书有额外的校验逻辑。
解决:我常用的方案有三个,按省事程度排序。第一,用微信开发者工具自带的 Network 面板——它内置抓包,不用任何代理配置,这是最推荐的。第二,在装有 Charles 的电脑上启动 Android 模拟器,模拟器里安装 Charles 证书并把代理指向电脑 IP,大部分调试场景够用。第三,如果你需要看加密后的请求体,那就别抓包了,直接在后端接口打日志,打印请求参数和响应结果,反而最快。抓包的目的是确认数据对不对,后端日志同样能达到这个目的。
5.4 Vue 打包放进 Spring Boot 的静态目录,路由刷新 404
现象:Vue 管理后台执行npm run build后把 dist 文件复制到 Spring Boot 的src/main/resources/static目录,打开首页正常,但点开「订单管理」页后刷新浏览器,出现 404。
原因:Vue Router 默认用 HTML5 History 模式,路由路径是/order/list这种伪路径,不存在的物理文件。Spring Boot 的静态资源处理找不到对应路径,就返回 404。
解决:两种办法。第一种,Vue Router 改用 hash 模式,URL 变成/index.html#/order/list,刷新不会 404,但 URL 不够好看。第二种,Spring Boot 里加一个路由转发配置,把所有非 API 请求转发到index.html。我建议用第二种,配置如下:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/{path:[^\\.]*}") .setViewName("forward:/index.html"); } }逻辑说明:这段配置把所有不带文件扩展名的路径都转发到/index.html,由 Vue Router 接管渲染。注意[^\\.]*正则排除了带点号的路径,避免静态资源(.js、.css)也被转发。另外这个方案只在后端部署静态资源时用,开发阶段请保持前后端分离:Vue 起npm run dev在 5173 端口,后端接口在 8080 端口,通过 Vite 的代理转发接口请求。这样前端改动即时生效,后端也能正常断点调试。
5.5 zip 解压后中文路径乱码,项目启动直接失败
现象:从网上下载的「废品回收系统小程序源码.zip」解压后,目录名是锟斤拷或液体这类乱码,IDE 打开代码文件也乱码,Maven 编译报符号错误。
原因:压缩包创建时用的是非 UTF-8 编码的压缩软件(常见于 Windows 默认的 GBK 编码),而你的解压环境默认 UTF-8。VSCode 和 IntelliJ IDEA 默认读取 UTF-8,两者不一致就出乱码。
解决:Windows 上装 7-Zip,用它打开 zip 后手动把文件名复制走,或者在解压时设置编码为 GBK。macOS 上可以用命令ditto -V -x -k 源码.zip 目标目录处理。代码文件如果已经乱码,用 VSCode 打开文件后点右下角编码,选「通过编码重新打开」再选 GBK 或 GB18030 恢复。总之后端代码统一 UTF-8,前端小程序也是 UTF-8,只有压缩包本身可能不是,先排查这一点。
6. 从源码到能演示的完整验证链路:四个端一次跑通
拿到源码后别急着改代码,先按顺序验证四件事,确保这套东西能「跑起来给你看」。后端先启动:用 IDEA 打开 Spring Boot 工程(确认 JDK 8 和 Maven 配置对),等依赖下载完后直接运行Application.java——这期间你会等 3 到 10 分钟不等,期间最可能挂的是依赖下载超时,把 Maven 源换成阿里云镜像就快了。看到Started Application in xx seconds后,用浏览器访问http://localhost:8080/api/health检查服务是否存活。
后端起来后再动小程序端:用微信开发者工具打开小程序目录,本地调试时勾选「不校验合法域名」,把utils/request.js里的基础 URL 改成http://localhost:8080。正常启动后,你会看到登录页、首页的回收品类列表能拉到数据,这一步通了说明前后端联调已经走通一个问题链路。最后打开 Vue 管理后台的工程,npm install安装依赖后npm run dev,登录管理员账号进后台,试着改一个回收品类价格并创建一个运费模板,再回到小程序端刷新看价格是否更新——到这里,三个端的数据流动闭环就跑通了。
我每次接到这类项目的第一天都强迫自己走完这条链路再睡。因为源码本身能不能编译、数据库脚本能不能执行、接口能不能通,这三件事任何一个出问题都会让你后面的定制无从下手。记得把数据库连接配置(application.yml里的url、username、password)和自己的 MySQL 环境对齐,用 Navicat 或命令导入项目里附带的sql脚本。最怕的是你项目启动成功但查不到数据——大概率是连了别的库或者初始化脚本没跑,别问我怎么知道的,都是血泪经验。
废品回收系统小程序这个方向,技术上最大的价值不是功能新颖,而是把「线下服务 + 微信生态 + 管理后台」这一整套闭环走完整了。你把它跑通一次,以后再做同城跑腿、上门维修、家政预约,都只是在订单状态机上加节点的问题。希望帮到你——按上面链路走一遍,遇到卡住的地方,优先查日志而不是盲改代码。
本文还有配套的精品资源,点击获取