零象废品回收小程序架构:从订单状态机到WebSocket实时通信
2026/9/16 1:46:40 网站建设 项目流程

简介:零象废品回收小程序提供一套基于微信小程序与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 '订单号,格式:yyyyMMddHHmmss+4位随机数', `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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='废品回收订单表';
字段说明设计要点
order_no业务订单号展示给用户和骑手,不用自增 id
status订单状态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, Map<String, 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?token=xxx格式。注意微信小程序对 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 NULL

Redis 锁承担第一层拦截,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 Result<Void> 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),但注意actualWeightunitPrice相乘之后精度可能超过两位,必须在 setScale 之后才入库。称重照片的 URL 存在order_item表的photo_url字段里,骑手可以传多张,审核页用图片列表展示。如果重量明显异常,比如纸箱订单填了 200kg,审核员在列表里能直接看到预估重量做对比。

4.3 结算单生成与微信支付商户号对接

审核通过之后,系统要把金额真正打给用户。v2.8.2 先用余额模式实现,管理员审核通过时自动往用户账户余额增加对应金额,用户可以在小程序里发起提现。提现动作走微信支付商家转账到零钱接口,需要商户号开通转账权限,并且用户的 openid 必须在该商户号下产生过交易。

结算单表settlement的核心字段包括user_idorder_idamountstatustransfer_nofail_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,不要改成订单号加随机数,否则重复通知时会产生两条结算单。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询