旧物回收系统怎么开发?从品类建模到上门回收调度的工程实践
旧物回收类平台的技术难点,从来不在“做一个表单提交页面”,而在于三件事:物品怎么被准确地描述、价值怎么被合理地估算、人怎么被高效地调度上门。围绕这三个问题,一套可用的旧物回收系统通常由品类中心、估值引擎、回收单状态机、调度派单池和多端应用层组成。本文按开发顺序拆解每一层的设计要点与落地经验,代码示例基于 Java + Spring Boot + MySQL + UniApp 这一常见组合,思路可平移到其他技术栈。
一、领域建模:品类树、成色与回收单状态机
回收业务的个坑是把“物品”当成一个扁平的商品表。实际上同一件旧家电,按品牌、型号、年份、功能完好度、外观磨损度会拆出成百上千种组合。合理的做法是品类树 + 属性模板 + 成色枚举三层结构。
品类树负责归类,属性模板负责差异化字段(手机看内存和电池健康度,空调看匹数和是否缺氟),成色枚举负责统一话语体系,避免用户各写各的。
-- 品类表:支持无限级,materialized path 便于整棵子树查询CREATETABLEcategory(idBIGINTPRIMARYKEYAUTO_INCREMENT,parent_idBIGINTNOTNULLDEFAULT0,nameVARCHAR(64)NOTNULL,pathVARCHAR(255)NOTNULLCOMMENT'如 /1/12/135/',attr_schema JSONNULLCOMMENT'该品类下的动态属性定义',statusTINYINTNOTNULLDEFAULT1,KEYidx_path(path));-- 回收单:状态机是整条链路的主线CREATETABLErecycle_order(idBIGINTPRIMARYKEYAUTO_INCREMENT,order_noVARCHAR(32)NOTNULL,user_idBIGINTNOTNULL,category_idBIGINTNOTNULL,snapshotJSONNOTNULLCOMMENT'下单时的品类/成色快照,防止配置变更导致历史单失真',estimateDECIMAL(10,2)NULLCOMMENT'系统估价结果,仅作业务字段',statusVARCHAR(24)NOTNULL,address_idBIGINTNOTNULL,worker_idBIGINTNULL,created_atDATETIMENOTNULLDEFAULTCURRENT_TIMESTAMP,UNIQUEKEYuk_order_no(order_no),KEYidx_status_created(status,created_at));状态机建议显式建模,不要用零散的 if-else 判断:
publicenumOrderStatus{DRAFT,// 用户填写中SUBMITTED,// 已提交,待系统估价VALUATED,// 已出估价,待用户确认CONFIRMED,// 用户确认,进入派单池ASSIGNED,// 已指派回收员ON_SITE,// 已上门,现场核验SETTLED,// 成交完成CANCELED,// 用户取消REJECTED;// 现场核验不符,单方终止publicbooleancanTransferTo(OrderStatusnext){returnswitch(this){caseSUBMITTED->next==VALUATED||next==CANCELED;caseVALUATED->next==CONFIRMED||next==CANCELED;caseCONFIRMED->next==ASSIGNED||next==CANCELED;caseASSIGNED->next==ON_SITE||next==CONFIRMED;// 回收员拒单可退回caseON_SITE->next==SETTLED||next==REJECTED;default->false;};}}经验点:snapshot字段必须存。品类属性和估值规则会随运营调整,如果不做快照,三个月后回查历史订单会得到完全不同的解释,对账时非常痛苦。状态流转要落库审计表,记录操作人、时间、前后状态,这是后期排查纠纷的依据。
二、估值引擎:把规则从代码里赶出去
估值是回收业务敏感的一层。硬编码在 Service 里的判断逻辑,改一次要发一次版,运营根本等不起。可行的做法是规则引擎 + 行情基准表:规则描述“怎么算”,行情表描述“按什么基准算”。
规则用 JSON 配置,支持条件命中、系数加权、上下限截断:
{"categoryId":135,"version":7,"base":{"source":"market_quote","key":"air_conditioner_1_5p"},"factors":[{"field":"ageYears","op":"between","range":[0,3],"weight":1.0},{"field":"ageYears","op":"between","range":[4,8],"weight":0.82},{"field":"condition","op":"eq","value":"GOOD","weight":1.0},{"field":"condition","op":"eq","value":"DAMAGED","weight":0.55},{"field":"hasInvoice","op":"eq","value":true,"weight":1.05}],"clamp":{"minRatio":0.5,"maxRatio":1.2}}执行时把用户提交的属性与规则逐条匹配,命中项权重相乘,再乘行情基准,后做截断。这样运营改规则只需新增一条 version 记录,历史订单继续用旧版本重算,可复现。
几个必须做的约束:估价结果只作为“参考区间”而非承诺值,前端展示时要给出区间与二次核验提示;行情基准表要保留时间序列,方便回溯任意一天的基准;规则命中数过少或过多都要打日志告警,这通常意味着属性模板设计出了偏差。
三、上门回收调度:抢单池、地理围栏与幂等派单
回收员的上门成本远高于纯线上业务,调度做不好,回收员的接单意愿会直接崩塌。调度层建议做两级:系统预筛 + 抢单池。
系统预筛负责把明显不合适的单子挡掉——距离超出服务半径、品类不在回收员技能标签内、当日负载已超阈值。剩下的进入抢单池,由回收员自主选择。
地理筛选不要用ST_Distance直接算全表,先粗筛再精算:
-- 粗筛:GeoHash 前缀匹配,命中面积约为目标半径的 1~4 倍SELECTo.id,o.order_no,o.category_idFROMrecycle_order oWHEREo.status='CONFIRMED'ANDo.geohashLIKE'4g%'-- 按中心点计算出的前缀ANDo.created_at>NOW()-INTERVAL24HOURLIMIT200;拿到候选集后,在应用层用 Haversine 精算距离,再叠加回收员技能标签与当前负载做排序。
派单必须幂等。回收员手速快、网络抖动、客户端重复提交,都会造成同一订单被多人抢到。方案是乐观锁 + 约束双保险:
UPDATErecycle_orderSETstatus='ASSIGNED',worker_id=#{workerId}, version = version + 1WHEREid=#{orderId} AND status = 'CONFIRMED' AND version = #{version};-- 影响行数为 0 即代表抢单失败,直接返回友好提示如果业务上允许多个回收员参与同一订单(例如大件需要两人上门),就不要在订单表上抢,改为建order_worker关联表并加UNIQUE KEY (order_id, worker_id)。
负载均衡建议:给回收员维护一个“当日已接单数 / 日承载上限”的滑动统计,排序时作为惩罚项。否则热门区域的单子会被少数活跃账号全部吃掉,新回收员接不到单,很快流失。
四、多端架构与配置一致性
旧物回收的用户触点很分散:小程序适合快速下单,APP 适合回收员长时间在线接单,H5 常被用作分享和轻量入口。多端并行的代价不是写页面,而是同一套业务规则在四端各写一遍。
几个收敛点:
- 品类树与属性模板由服务端下发,客户端只负责渲染 JSON Schema,禁止在各端硬编码品类判断。
- 状态机的可用操作由服务端计算,接口返回
allowedActions,避免客户端自己推断“这个状态下该显示哪个按钮”。 - 估价接口做版本化(如
/api/v1/valuation),客户端版本落后时仍能调用兼容逻辑。 - 列表接口统一分页与排序字段,小程序和 APP 复用同一套 DTO,减少字段漂移。
后台管理侧通常需要覆盖多角色:平台管理、区域服务商、回收员、企业客户。权限模型建议用 RBAC + 数据范围两层,角色决定能看哪些菜单,数据范围决定能看哪些订单(本人 / 本团队 / 本区域 / 全部)。这一层如果不提前设计,后期加一个角色就要动一次核心查询。
五、上线前容易忽略的几件事
- 订单快照与版本:估值规则、品类配置、地址数据都要在订单上留档。
- 地址解析:用户手填地址的脏数据比例很高,接入地图 POI 检索并强制选择,能大幅降低派单失败率。
- 图片存储:现场核验照片是核心凭证,走对象存储 + 私有读 CDN,不要直接存数据库。
- 审计日志:状态流转、估值结果、派单结果三类日志必须可追溯。
- 压测目标:抢单接口是并发尖峰,重点压这一条链路,而不是首页。
FAQ
Q:旧物回收系统的估值结果和现场核验结果差得多怎么办?
A:这是常态而非异常。设计上把估价定位为“参考区间”,在状态机里保留ON_SITE → SETTLED / REJECTED两条出口,并允许现场录入实际核验参数重新计算。差异超过阈值的订单单独打标,用于回看规则命中是否合理。
Q:品类树要不要一开始就做得很细?
A:不要。建议先做两层,属性模板做成可动态扩展的 JSON Schema。品类过细会导致冷启动阶段每个叶子节点下的样本都很少,估值规则没有数据支撑,反而算不准。等订单量积累起来再逐步下钻。
Q:抢单和派单应该选哪个?
A:两者不互斥。常见做法是系统预筛后进入抢单池,超时未接单则转为自动指派。纯抢单在低活跃时段会积压订单,纯指派在高峰时段又容易造成回收员负载不均。
Q:多端并行开发,怎么保证接口不各自为政?
A:接口契约先于客户端开发,用统一的状态枚举、统一的品类下发接口、统一的分页规范。客户端只做渲染和交互,任何业务判断都回归服务端。这样新增一个端时,工作量基本只剩 UI 层。