
简介零象废品回收小程序提供一套基于微信小程序与PHP后端的完整前后端源码包版本为V2.8.2适合有一定前端或后端基础的开发者直接部署或二次开发。资源围绕废品回收业务设计支持协议定期企业废品回收、垃圾分类小区物业定期回收等典型模式可帮助运营方快速搭建回收预约、订单跟踪、分类管理等核心功能。压缩包含606个文件总大小约3.96MB其中png与jpg提供界面与图标素材js、wxml、wxss构成小程序前端逻辑与样式json负责数据配置php处理后端业务逻辑另有html、css等辅助页面整体结构清晰便于按模块学习和修改。目前已有5304人学习下载适合正在研发同类型小程序或希望通过完整源码理解前后端协作原理的开发者。借助这套完整源码可获得可直接运行的全开源安装包与小程序前端覆盖从用户端操作到后台管理的完整链路有效缩短开发周期并降低试错成本。1. 零象废品回收小程序的架构全景从用户下单到骑手结算的业务闭环一部手机一套前后端分离的项目把收废品这件事做成订单流用户在微信小程序里预约上门骑手在骑手端接单管理员在后台审核称重数据结算金额自动进入用户账户。零象废品回收小程序v2.8.2的源码里就包含这三个端。这个项目解决的是废品回收行业里最常见的管理混乱问题微信群接龙式报单、称重金额靠手写、结算周期以周为单位。适合两类人看一类是打算快速搭建同城回收业务的团队另一类是正在做 springboot vue 前后端分离项目实战的开发者。前者关心业务流程怎么落地后者关心订单状态机、接口权限、WebSocket推送这些具体实现。下文按「建模—通信—审核—部署」的顺序把整套方案拆开讲。2. 零象废品回收小程序的用户下单与订单状态机建模2.1 订单主表字段设计与状态枚举定义废品回收的订单和普通电商订单不一样它多了称重环节实际金额在骑手上门之后才确定。因此订单表不能只存金额要把预估数据、实际称重数据、履约角色和时间点分开存。核心表recycle_order在 v2.8.2 里的定义大致如下CREATE TABLE recycle_order ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 订单号格式yyyyMMddHHmmss4位随机数, user_id bigint(20) NOT NULL COMMENT 下单用户ID, courier_id bigint(20) DEFAULT NULL COMMENT 接单骑手ID接单前为空, status tinyint(4) NOT NULL DEFAULT 0, category_code varchar(16) NOT NULL COMMENT 品类编码paper/plastic/metal/glass, estimated_weight decimal(8,2) DEFAULT NULL COMMENT 用户预估重量单位kg, actual_weight decimal(8,2) DEFAULT NULL COMMENT 骑手实际称重称重后填写, appointment_time datetime DEFAULT NULL COMMENT 预约上门时间, address varchar(255) NOT NULL COMMENT 上门地址, contact_name varchar(32) NOT NULL, contact_phone varchar(20) NOT NULL, remark varchar(500) DEFAULT NULL COMMENT 用户备注如大件物品, version int(11) NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_status (status), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT废品回收订单表;字段说明设计要点order_no业务订单号展示给用户和骑手不用自增 idstatus订单状态0待接单 1已接单 2已上门 3称重中 4待审核 5已结算 6已取消actual_weight实际称重默认 NULL不跟预估重量混在一个字段里version乐观锁抢单和称重更新时用于防止并发覆盖订单号用时间戳加随机数而不是自增主键是因为骑手在电话里报订单号时需要一串容易读的数字。status 字段预留了「待审核」这个状态属于废品回收特有的环节管理员要确认称重照片和重量真实一致才允许进入结算。2.2 前端 uniapp 下单页与动态标题设置用户端用 uniapp 开发一套代码同时编译到微信小程序和 H5。下单页提交时调用后端接口成功后用uni.setNavigationBarTitle动态修改顶部标题把订单号回显到导航栏方便用户后续在订单列表里对照。这是 v2.8.2 里一个小的交互改进但很适合做成模板// pages/order/create.vue async function submitOrder() { const res await uni.request({ url: /api/order/create, method: POST, header: { Authorization: Bearer ${uni.getStorageSync(token)} }, data: { categoryCode: form.categoryCode, estimatedWeight: form.estimatedWeight, address: form.address, contactName: form.contactName, contactPhone: form.contactPhone, appointmentTime: form.appointmentTime } }) if (res.data.code 0) { const orderNo res.data.data.orderNo uni.setNavigationBarTitle({ title: 回收单 ${orderNo} 派单中 }) uni.redirectTo({ url: /pages/order/detail?id${res.data.data.id} }) } else { uni.showToast({ title: res.data.msg, icon: none }) } }代码里res.data.code 0是后端统一返回结构中的成功标识。如果后端返回的 code 不是 0前端只做 toast 提示不跳转页面。uni.setNavigationBarTitle的页面标题默认从 pages.json 里读取动态设置之后返回上一页再进入时会重置所以每次进入详情页都需要重新设置。参数appointmentTime传给后端时建议转成 ISO 8601 格式字符串Java 侧用LocalDateTime直接解析。微信小程序顶部导航栏高度在不同机型上不一致页面样式不要硬编码导航栏以下的偏移量用uni.getSystemInfoSync()里的statusBarHeight动态计算。2.3 状态机流转约束谁在什么状态下能做什么状态枚举定好之后真正容易出问题的是流转控制。这套项目里把状态变更收敛到后端 Service 层用一张流转表约束每个动作的合法性前端只有展示逻辑不参与状态判断。当前状态允许动作目标状态操作角色待接单用户取消已取消用户待接单骑手接单已接单骑手已接单骑手上门已上门骑手已上门录入称重称重中骑手称重中提交审核待审核骑手待审核审核通过已结算管理员待审核审核驳回已取消管理员在OrderServiceImpl里每个状态变更方法第一行先校验当前状态再执行更新。比如接单方法只接受 status 0 的订单称重提交只接受 status 2 的订单。用update ... where id ? and status ?这种条件更新语句配合数据库影响行数判断能把状态冲突的概率降到最低。这套设计对新手工程师来说是一个完整的参考范本状态机不是抽象概念就是一张表加几个 if 判断。3. 骑手端任务流转与实时通信WebSocket 鉴权和抢单并发控制3.1 基于 token 的 WebSocket 握手拦截骑手端最核心的体验是新订单实时弹出。轮询接口也行但订单高峰期的请求量和延迟都很难看v2.8.2 采用的是 Spring Boot 内置 WebSocket 加 STOMP 协议。握手阶段需要校验用户身份借 token 传参的做法是把 token 放在连接 URL 的 query 参数里public class TokenChannelInterceptor implements HandshakeInterceptor { Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, MapString, Object attributes) { URI uri request.getURI(); String token UriComponentsBuilder.fromUri(uri) .build() .getQueryParams() .getFirst(token); Long userId JwtUtil.parseUserId(token); if (userId null) { return false; } attributes.put(userId, userId); return true; } }拦截器返回 false 就中断握手连接建立不起来这是第一道防线。attributes里存入的 userId 会在后续 WebSocket 会话中直接使用避免每次推送都重新解析 token。前端 uniapp 连接时的 URL 要拼成ws://域名/ws/order?tokenxxx格式。注意微信小程序对 WebSocket 的连接数有限制同一个时刻最多 5 个连接所以只需要骑手端用一个连接订阅新订单广播即可不要每个页面都开连接。3.2 抢单操作的并发控制Redis SETNX 与数据库版本号双保险新订单推送到骑手端之后多个骑手可能同时点击接单。最简单的接单接口是「先查 status再 update」但两个操作之间存在时间窗口并发下会重复接单。v2.8.2 的接单逻辑用了 Redis 锁加数据库条件更新两层控制public void acceptOrder(Long orderId, Long courierId) { String lockKey order:accept: orderId; Boolean locked redisTemplate.opsForValue() .setIfAbsent(lockKey, courierId.toString(), 8, TimeUnit.SECONDS); if (!locked) { throw new BizException(手慢了订单已被其他骑手接走); } try { int rows recycleOrderMapper.acceptOrder(orderId, courierId); if (rows 0) { throw new BizException(订单状态已变更请刷新列表); } } finally { redisTemplate.delete(lockKey); } }UPDATE recycle_order SET courier_id #{courierId}, status 1, updated_at NOW() WHERE id #{orderId} AND status 0 AND courier_id IS NULLRedis 锁承担第一层拦截setIfAbsent只有第一个请求能写入成功8 秒过期时间足够一个事务提交。真正兜底的是 SQL 里的status 0 AND courier_id IS NULL条件数据库行锁保证只有一个骑手能更新成功rows 0时再抛出业务异常。这两层配合下应用重启、Redis 宕机这类极端情况也不会出现一单多接。从实践角度看setIfAbsent 的 key 要带订单维度不能只用一个全局锁否则所有订单的接单操作都会串行化。3.3 接单后消息推送与离线补偿接单动作完成后骑手端要立即刷新自己的订单列表用户端也要收到「骑手已接单」的通知。方案里用 WebSocket 给用户端的连接主动推送一条 JSON 消息用户端收到后刷新订单详情。每个用户在建立 WebSocket 连接时服务端会把 session 和 userId 的映射存进ConcurrentHashMap推送时从 map 里取出 session 调用 sendMessage。推送场景消息类型目标角色新订单生成NEW_ORDER骑手端广播骑手接单ORDER_ACCEPTED用户端称重完成ORDER_WEIGHED用户端、管理员端审核通过ORDER_SETTLED用户端WebSocket 连接不是永久的用户切后台或者小程序被回收之后连接会断开。因此所有推送同时要写一张notify_message表用户下次打开小程序时先拉取未读消息再做页面跳转。给用户的微信服务通知走subscribeMessage.send接口模板里的订单号和结算金额从数据库取这个动作放到消息消费者里异步执行不在接单事务内操作。4. 管理后台的审核、称重与结算接口权限和金额计算细节4.1 审核回收单接口与角色权限控制后台管理端是典型的 vue 加 Spring Boot 前后端分离结构权限模型按角色区分 USER、COURIER、ADMIN 三档。审核称重单是一个只允许管理员执行的操作接口上直接用PreAuthorize注解声明权限框架层面先拦截非法请求RestController RequestMapping(/api/order) public class OrderAdminController { PostMapping(/review) PreAuthorize(hasRole(ADMIN)) public ResultVoid review(RequestBody Valid ReviewRequest req) { orderService.review(req.getOrderId(), req.getResult(), req.getRemark()); return Result.success(); } }PreAuthorize(hasRole(ADMIN))会在进入方法前解析当前用户的角色列表不是管理员直接返回 403不需要在业务代码里手写 if 判断。前端路由也要配合做访问控制vue-router 的 beforeEach 钩子里根据用户角色过滤路由表防止有人绕过菜单直接输入后台地址。前后端分离场景里这样做权限有两层含义接口权限保证数据安全路由权限保证界面整洁两者少一个都不完整。4.2 称重录入与按品类计价逻辑废品计价不是统一价格纸箱、塑料瓶、金属、玻璃的单价完全不同而且同一品类下还可能细分比如纸箱分黄板纸和花纸。计价逻辑放在后端独立的价格表里前端不存任何单价。骑手在用户家里称完重量后录入实际重量系统按品类编码取出单价计算顺序是先乘后舍入public BigDecimal calculateAmount(String categoryCode, BigDecimal actualWeight) { BigDecimal unitPrice priceService.getUnitPrice(categoryCode); BigDecimal amount actualWeight.multiply(unitPrice) .setScale(2, RoundingMode.HALF_UP); return amount; }金额保留两位小数用的是HALF_UP也就是四舍五入。实际重量按 kg 存单价按 元/kg 存两个字段都用decimal(8,2)但注意actualWeight跟unitPrice相乘之后精度可能超过两位必须在 setScale 之后才入库。称重照片的 URL 存在order_item表的photo_url字段里骑手可以传多张审核页用图片列表展示。如果重量明显异常比如纸箱订单填了 200kg审核员在列表里能直接看到预估重量做对比。4.3 结算单生成与微信支付商户号对接审核通过之后系统要把金额真正打给用户。v2.8.2 先用余额模式实现管理员审核通过时自动往用户账户余额增加对应金额用户可以在小程序里发起提现。提现动作走微信支付商家转账到零钱接口需要商户号开通转账权限并且用户的 openid 必须在该商户号下产生过交易。结算单表settlement的核心字段包括user_id、order_id、amount、status、transfer_no、fail_reason。生成结算单时做一次幂等判断同一个 order_id 只能生成一条结算记录通过唯一索引uk_order_id保证。转账接口的返回结果要同步更新status字段状态值含义后续处理0待转账定时任务扫描重试1转账成功更新用户余额流水2转账失败记录 fail_reason人工介入转账失败的原因常见为余额不足、用户未实名、openid 不匹配。定时任务重试时加上次数上限超过三次进入人工处理列表。金额计算和转账这两步在项目源码里是分开的两个模块计算模块只负责算数转账模块只负责调用微信接口中间用结算单表衔接这个拆分在排查问题时能省很多时间。5. 部署排错与版本升级Nginx 转发、uniapp 编译和 v2.8.2 的三处改动前后端分离项目部署时最常见的做法是前端构建产物放到 Nginx 静态目录接口请求转发到 Java 服务。uniapp 编译微信小程序时开发者工具里本地预览没问题但真机预览常常出现白屏或接口 404先检查 request 合法域名有没有在微信公众平台配置。管理后台的 H5 版本部署到 Nginx 时需要处理 history 路由刷新丢失问题server { listen 80; server_name recycle.example.com; location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { root /opt/recycle-web; index index.html; try_files $uri $uri/ /index.html; } }try_files $uri $uri/ /index.html是 vue history 路由模式的关键配置没有这一行用户刷新/order/detail/123页面时 Nginx 找不到对应文件直接返回 404。location /api/把接口请求转发给本机 8080 端口的 Spring Boot 服务改造业务域名上线时记得在微信公众平台小程序后台把 request 合法域名配置成完整的https://recycle.example.com。小程序备案时备注信息提交 ICP 备案号审核周期一般在几个工作日预留时间再发版。从 v2.8.1 升级到 v2.8.2 前检查三个地方数据库有没有执行新增的settlement.fail_reason字段脚本application.yml里 Redis 连接池参数是否更新前端config.js的接口地址是否从测试环境切换到生产环境。支付转账的幂等键用order_id不要改成订单号加随机数否则重复通知时会产生两条结算单。本文还有配套的精品资源点击获取