从个人开发经验来说,二手电子产品回收系统这种项目,最难的不是某个功能写不出来,而是整条业务链路怎么串起来、状态怎么管、估价逻辑怎么设计才能让系统真正能落地。市面上很多教程都在讲单个功能点的代码,真正把“用户提交回收申请→系统估价→回收员接单→检测打款”这条完整闭环跑通的项目总结非常少。这篇博文我就拿自己实际做过的一个项目来拆解,系统基于 Python 双框架实现,Django 负责管理后台与核心业务,Flask 负责轻量估价服务和部分对外接口,把整个设计与实现过程、踩过的坑、关键代码全部整理出来,希望能给准备做类似系统的人一些参考。
老实说,刚开始我也纠结过要不要统一用 Django 一个框架,毕竟一个框架搞定所有功能,部署也简单。但实际做下来发现,二手回收业务有个特点:估价逻辑变化非常频繁,今天这个型号行情涨了,明天那个机型回收价调整,如果每次改估价规则都要重启主应用,线上用户正在操作就会受影响。后来我把估价拆成独立的 Flask 服务,Django 通过 HTTP 调用它拿估价结果,这样改估价规则只需要重启 Flask 服务,主站完全不受影响。这个架构为整个系统省了非常多麻烦,也是我觉得最值得分享的一个设计决策。
项目定位说明:这个系统面向的是一般回收商或校园创业团队,不是那种大型平台级回收系统,但核心业务链路完整,可以平滑扩展到小程序端和线下门店使用。
1. 项目整体设计与需求拆解
1.1 核心需求到底有哪些
做这类系统之前,一定要先想清楚一个事:二手电子产品回收不是单纯的“二手交易”,它比 C2C 交易多了一个“检测与定价”环节。用户卖手机给你,你不能直接按用户报价打款,必须等设备收到、检测完、确认没问题,才算完成交易。所以核心需求整理下来其实就这几块。
第一块是用户端需求。用户注册登录后,可以提交回收申请,选择电子产品类型(手机、平板、笔记本、耳机、手表等),填写品牌型号、购买年份、存储容量、成色描述,上传设备外观照片,然后系统根据这些信息给出预估价。用户认可估价后,选择回收方式(上门取件或邮寄),填写地址,生成回收订单,后续可以在订单列表里查看进度状态。
第二块是回收员/检测员需求。回收员端需要看到待接单的回收订单,接单后上门取件;检测员收到设备后,需要登记检测结果,包括设备实际成色、功能是否正常、有无维修痕迹,然后录入实际回收价。如果检测结果和用户申报不一致,需要能修改价格并让用户确认。
第三块是管理员需求。管理员要能管理用户、管理商品型号库、管理估价规则、查看所有订单、处理纠纷、做数据统计,还要能发布回收活动(比如旧机补贴、以旧换新)。这些需求没有 Django Admin 也能做,但用了 Django Admin 可以节省非常多的开发时间。
第四块是估价服务。这个看起来不起眼,实际上最烧脑。估价不能只写死在代码里,要有规则可配置:同样的 iPhone 13 128G,外观完美无修和屏幕有划痕,回收价差了非常多。估价规则本质上是一个多条件评分逻辑,需要把设备属性映射到价格区间。
1.2 双框架架构的选型理由
我最终选择 Django + Flask 的混合架构,不是炫技,而是被项目需求逼出来的。
Django 的定位是主后端服务,负责用户、订单、回收流程、后台管理、权限体系这些重业务。Django 自带 ORM、Admin、Form 校验、中间件,这些东西对业务系统来说太成熟了,尤其是 Django Admin,我直接把回收订单、用户、型号库注册进去,管理后台就完成了大半。
Flask 的定位是估价服务和轻量接口。估价规则变化快,而且后续打算接行情数据源,用 Flask 这种轻框架做微服务拆分,独立部署、独立升级、独立扩展,非常合适。主服务和估价服务之间通过 HTTP + JSON 通信,Django 不直接操作估价的数据库表。
选型时还考虑过 Flask 全栈、Django 全栈、FastAPI 全栈。Flask 全栈的问题是用户体系、Admin、权限都要自己搭,工作量巨大;FastAPI 虽然后起之秀,性能好,但生态在管理后台方面不如 Django 成熟。Django + Flask 的组合,算是在这个特定业务场景里“既要又要”的一个折中方案。
注意:双框架不等于两套系统,两者的边界要清晰。我的划分标准是:凡是要写业务逻辑、操作数据库的,放 Django;凡是纯计算、纯逻辑、要高频改动的,放 Flask。这样代码仓库可以放同一个工程目录下,但进程和数据库逻辑上是分离的。
1.3 功能模块清单与整体架构
系统按功能模块划分,主要包括:
| 模块 | 核心功能 | 归属框架 |
|---|---|---|
| 用户模块 | 注册登录、个人信息、地址管理 | Django |
| 回收申请模块 | 设备信息填写、照片上传、预估价获取 | Django + Flask |
| 估价服务模块 | 估价规则配置、价格计算、行情调整 | Flask |
| 订单模块 | 订单生成、状态流转、物流信息 | Django |
| 检测模块 | 检测结果录入、价格调整、图片上传 | Django |
| 支付结算模块 | 打款记录、提现、结算单 | Django |
| 管理后台 | 用户管理、型号管理、订单管理、数据统计 | Django Admin |
| 接口服务 | 小程序/APP 对接 API | Django + Flask |
整体请求链路举个例子:用户在页面提交回收申请,前端先把表单数据传到 Django,Django 保存申请草稿后,把设备参数通过 HTTP 请求发给 Flask 估价服务,Flask 根据规则计算出预估价返回,Django 把预估价写入草稿记录并展示给用户。用户确认后,正式生成回收订单。这个流程保证用户感知的“估价很快”与“估价规则灵活调整”两者兼得。
2. 数据库设计与核心模块实现
2.1 核心数据表设计
数据库设计是这类系统最关键的部分,一旦后期要改表结构,牵一发动全身。我用了 MySQL 8.0,字符集 utf8mb4。核心表包括用户表、回收订单表、估价规则表、检测记录表、物流记录表、结算记录表。下面挑几张贴出来讲。
用户表基本复用 Django 自带的 auth.User,我额外扩展了一个 Profile 表存手机号、头像、积分这些字段,而不是直接改 auth.User 表,因为 Django 的 auth.User 在迁移和升级时容易出问题,扩展表更安全。
回收订单表是最核心的表,字段设计必须考虑状态流转的灵活性。我当时建的表大致是这样:
id, order_no, user_id, device_type, brand, model_name, storage, color, purchase_year, condition_desc, condition_images, estimated_price, confirmed_price, final_price, status, address_id, express_no, create_time, update_time其中 status 字段是灵魂,后面会专门讲状态机。order_no 一定要做唯一索引,用户端展示、物流查询、内部沟通都用它。
估价规则表我放在了 Flask 服务的数据库里,但 Django 侧通过接口读取,不直接连表。表结构大致是:设备类型、品牌、型号、基础价、内存系数、成色系数、配件扣减、市场浮动价、更新时间。核心思想是“基础价 + 条件系数修正”,而不是给每个型号写死一个价格。
检测记录表单独拆出来,因为一个订单可能被检测两次(第一次检测不合格,用户复议后复检),记录表存检测人、检测结果、检测备注、价格调整记录、复检关联 ID,确保每次检测都有迹可循。
2.2 订单状态机的设计思路
订单状态是整个系统里最容易写烂的部分。很多人用几个 if/else 硬写,最后状态越来越多,逻辑越来越乱。我在这个项目里用状态机来管理,把状态和流转条件集中定义出来。
核心状态分为:待估价、待确认、待取件/待邮寄、检测中、待结算、已完成、已取消、已拒绝。每个状态允许流转到哪些目标状态,要画一张表:
| 当前状态 | 可流转状态 | 触发条件 |
|---|---|---|
| 待估价 | 待确认、已取消 | 用户确认预估价 / 用户取消 |
| 待确认 | 待取件、待邮寄、待估价、已取消 | 用户选择回收方式 / 用户改价重新估价 |
| 待取件/待邮寄 | 检测中、已取消 | 回收员接单取件/用户邮寄并填写单号 |
| 检测中 | 待结算、待确认、已拒绝 | 检测通过自动确认 / 检测价变更需用户确认 / 检测不合格退回 |
| 待结算 | 已完成 | 打款完成 |
| 已拒绝 | 已取消 | 用户放弃回收 |
这个状态机在代码里怎么实现呢?我定义了一个字典,每个状态的合法目标状态是个集合,流转时先校验,不合法直接抛异常,这样就不会出现用户还在“检测中”结果订单就显示“已完成”这种低级错误。
状态变更必须做记录。我加了一张 order_log 表,每次状态改变都写一条日志,包含操作者、旧状态、新状态、操作时间、备注。刚开始我觉得这个表多余,后来排查线上问题时才发现,状态日志几乎是唯一能还原现场的依据。
2.3 用户认证与权限控制
用户认证我用了 Django 自带的 session 认证,没有引入 JWT,因为主业务是服务端渲染页面 + Admin 后台,session 简单可靠。接口部分为了后续小程序接入,单独实现了一个简单的 Token 鉴权:用户在 App 登录后拿到 token,后续请求在 Header 带 token,Django 中间件统一校验。
权限控制分三层:普通用户只能看自己的订单,回收员能看分配给自己的待取件订单,管理员能看全部订单和后台管理页面。Django 自带的 Group 和 Permission 机制基本够用,我用 Group 创建了“回收员”组,然后在视图函数里用装饰器判断。需要说明的是,Django 的权限控制默认作用在模型层级,要控制到“回收员只能看到指定物流公司分配的单子”这种行级权限,需要自己在 queryset 里加过滤条件。
经验之谈:行级权限不要硬编码,最好做成可配置的规则,比如“回收员只能看到自己所属区域内的订单”,区域 ID 存在回收员 Profile 里,查询时动态过滤,后续加城市回收站的时候不用改代码。
3. 用户端与管理端的实操流程实现
3.1 回收申请与估价核心流程
用户提交回收申请,前端表单收集的数据包括品牌、型号、存储、购买年份、外观成色、功能问题、期望价格。外观成色我做成单选加图片上传,功能问题做成多选框,比如“屏幕有划痕”“电池不耐用”“摄像头有灰尘”“无法开机”等,这些选项直接影响估价系数。
表单提交到 Django 后,Django 先生成一条状态为“待估价”的草稿记录,然后调用 Flask 估价服务。调用代码如下:
import requests import json def get_estimate_price(device_data): url = "http://127.0.0.1:5001/api/estimate" payload = { "device_type": device_data.get("device_type"), "brand": device_data.get("brand"), "model": device_data.get("model_name"), "storage": device_data.get("storage"), "condition": device_data.get("condition_desc"), "issues": device_data.get("issues") } try: resp = requests.post(url, json=payload, timeout=3) if resp.status_code == 200: return resp.json().get("estimated_price") except requests.exceptions.RequestException: # 估价服务异常时返回兜底价格,避免主流程直接失败 return get_default_price(device_data)这段代码里我加了 3 秒超时和异常兜底。真实踩过的坑是:Flask 估价服务重启或者数据库连接池超时的时候,前端会一直转圈,用户以为卡死了。后来我的处理方式是 Django 侧 catch 所有网络异常,直接返回一个默认估价,同时后台记录告警日志,保证用户流程不被一块“估价”卡死。这个兜底看似简单,实际对用户体验的提升非常明显。
Flask 端的估价逻辑简化如下:
from flask import Flask, request, jsonify app = Flask(__name__) ESTIMATE_BASE = { "iphone_13_128": 2800, "iphone_13_256": 3200, "xiaomi_12_128": 1600, } def calc_condition_coefficient(condition): mapping = { "全新": 1.0, "95新": 0.95, "9成新": 0.88, "8成新": 0.78, "有维修": 0.6, } return mapping.get(condition, 0.7) @app.route("/api/estimate", methods=["POST"]) def estimate(): data = request.get_json() base_price = ESTIMATE_BASE.get(data["model"], 0) condition_coef = calc_condition_coefficient(data["condition"]) storage_coef = 1.0 + (data.get("storage", 128) - 128) / 1000.0 estimated_price = round(base_price * condition_coef * storage_coef, 2) return jsonify({"estimated_price": estimated_price})真实项目的估价规则比这个复杂得多,比如要按购买年份折旧、按功能问题扣减、按市场行情浮动,但核心逻辑都是“基础价×成色系数×配置系数-问题扣减+行情浮动”,把规则做成 JSON 配置存数据库,运营人员可以直接在后台改,不用发版。
3.2 回收订单流转与物流信息更新
用户确认估价后,订单状态从“待确认”变为“待取件”或“待邮寄”。选择上门取件的,系统会把订单放入回收员任务池,回收员在后台“接单大厅”里看到待取件订单,接单后更新订单状态并锁定订单,防止多个回收员同时抢单。
针对同时抢单并发问题,我在订单表加了一个 lock_by 字段,回收员点击接单时,用 UPDATE 语句带条件更新实现原子操作。伪代码是:
updated = Order.objects.filter(id=order_id, status='待取件', lock_by__isnull=True).update( lock_by=recycler_id, status='待取件', update_time=now() ) if updated == 0: return error("订单已被其他人接单")这个写法能避免先 SELECT 再 UPDATE 导致的超卖问题。我最初用“先查后更”的逻辑,测试的时候开两个账号同时点接单,两个都提示成功,后来改成这种“条件更新”的方式才彻底解决。
用户选择邮寄回收的,需要在订单里填写快递公司和运单号,系统会有一个简单的物流信息录入入口。真正对接快递鸟这种物流查询接口,我放在了二期,第一期只是人工查看物流轨迹,够用。
3.3 管理后台:检测录入与价格调整
检测员收到设备后,在后台打开检测页面,录入实际检测结果。检测页面的核心是“检测报告 + 最终报价”的组合:有成色复评、功能检测列表(屏幕、摄像头、电池、充电口、主板、按键)、维修史检测,每一项都能勾选“正常/异常”,异常项填写描述。
检测完成后,系统计算最终回收价。这里有个容易忽略的点:检测价格和预估价很可能不一致,如果检测价格低于预估价的 10% 以上,系统不能直接打款,而是要生成一条“价格变更确认”消息推送给用户,用户同意后订单才能进入“待结算”,用户不同意则订单进入“待退回”状态,设备原路退回。这个机制我一开始没做,结果用户收到打款金额和预估差很多,直接投诉,后来老老实实补上了。
后端检测提交的核心逻辑伪代码如下:
def submit_inspection(order_id, inspection_data): order = Order.objects.select_for_update().get(id=order_id) if order.status not in ["检测中"]: raise BusinessError("订单状态不允许检测提交") inspection = Inspection.objects.create( order=order, final_price=inspection_data["final_price"], items_result=json.dumps(inspection_data["items"]), operator=request.user ) if inspection_data["final_price"] < order.confirmed_price * 0.9: order.status = "待确认" order.price_change_status = "pending" else: order.status = "待结算" order.final_price = inspection_data["final_price"] order.save() add_order_log(order, "检测完成", ...)select_for_update 是为了防止两个检测员同时打开同一订单分别提交,数据库行锁能保证同一时间只有一个人能修改。注意,select_for_update 必须放在事务里才生效,我一开始没写 with transaction.atomic(),行锁失效,排查半天才意识到。
3.4 管理员后台的数据统计与分析
Django Admin 默认只能做简单的 CRUD,数据统计还得自己写视图。我的做法是做一个独立的数据看板页面,展示几个核心指标:每日新增回收申请数、订单完成率、平均回收价、各品牌回收占比、待处理订单数量。统计 SQL 不难,但要注意时间范围和时区问题。我遇到过统计结果和实际订单数据对不上,是因为服务端时区是 UTC,而业务统计用的是北京时间,两个差了 8 小时,晚 8 点后的订单被统计到第二天去了。后来统一在数据库查询时用本地时区,前端展示也一致,才彻底解决。
4. 常见问题排查与部署实战经验
4.1 开发期踩过的印象最深的坑
第一个坑是 Flask 和 Django 同属一个工程目录时,根目录的 requirements.txt 和启动命令容易混。我当时的工程结构是:
project_root/ ├── django_project/ │ ├── manage.py │ ├── config/ │ └── apps/ ├── flask_service/ │ ├── app.py │ └── estimate_rules.py ├── requirements.txt └── deploy/两个框架共用一套虚拟环境,但启动方式完全不同,Django 用 manage.py runserver,Flask 用 python app.py。我建议在 README 里把两套启动命令写清楚,否则过两周你自己都不记得哪个端口对应哪个服务。
第二个坑是图片上传大小限制。用户上传设备照片,我最初没有限制文件大小,结果有人上传一张 15MB 的图片,Django 直接内存溢出,页面 500。后来在配置里加了 MAX_UPLOAD_SIZE,前端也做了压缩,后端再用 Pillow 统一压缩成 800px 宽度的 JPG 存储,问题解决。
第三个坑是 Flask 估价服务的并发性能。Flask 自带开发服务器是单线程的,并发一高就阻塞。我本地测试没问题,部署后生产环境一压测,接口响应时间从 50ms 涨到 3 秒。解决方案是生产环境用 gunicorn 启动 Flask,配置多 worker,同时在 Django 侧对估价接口做缓存,相同型号、相同成色的估价请求直接命中缓存,不重复计算。
4.2 下单与接单的并发处理
并发问题除了前面说的接单场景,还出现在下单环节。用户重复点击“提交订单”按钮,会生成多条相同内容的订单。前端的处理是按钮置灰加 loading,后端的处理是幂等校验:根据用户 ID + 设备唯一标识 + 当日时间生成一个幂等键,数据库加唯一索引,重复请求直接返回已有订单。
这个幂等键设计一开始没做,测试时用脚本模拟并发请求 10 次,数据库里出现 10 条一模一样的订单,用户被扣了 10 次预估价额度,非常尴尬。加了幂等键后,相同的请求只允许创建一次,从根源上避免脏数据。
4.3 生产环境部署步骤详解
部署我采用的是同机部署双服务,用 Nginx 做统一入口,按路径转发:/api/estimate/* 转发到 Flask 的 5001 端口,其他所有请求转发到 Django 的 8000 端口,静态文件和上传文件由 Nginx 直接处理。
部署步骤简单整理如下:
- 服务器上创建虚拟环境,安装 requirements.txt。
- 安装并配置 MySQL,创建数据库,导入初始数据。
- 用 collectstatic 收集 Django 静态文件。
- 编写两个 systemd 服务文件,分别管理 Django(gunicorn + 8 worker)和 Flask(gunicorn + 4 worker)。
- 配置 Nginx,反向代理到两个服务端口,配置上传文件目录别名。
- 部署完成后,用 curl 分别测试 Django 首页和 Flask 估价接口是否正常。
gunicorn 配置我推荐一行命令搞定:
gunicorn config.wsgi:application -w 8 -b 127.0.0.1:8000 --timeout 60Flask 服务同理:
gunicorn app:app -w 4 -b 127.0.0.1:5001 --timeout 30worker 数量不是越多越好,要根据服务器 CPU 核心数来,一般 2 核服务器 Django 配 4 个 worker 足够,太多反而增加内存消耗,导致 OOM。
上线前一定要检查 Django 的 DEBUG 是否设置为 False,ALLOWED_HOSTS 是否配置了你的域名。我犯过一次上线几个小时用户访问全部 500 的错,罪魁祸首就是 DEBUG=True,代码异常时直接把敏感信息暴露在页面上。
4.4 常见问题速查表
| 问题现象 | 可能原因 | 排查思路 |
|---|---|---|
| 页面 500,日志无报错 | ALLOWED_HOSTS 未配置 | 检查 settings.py,添加域名到白名单 |
| 用户图片上传失败 | 上传大小超限 | 查看上传目录权限和配置文件大小限制 |
| 估价接口偶发超时 | Flask worker 数不足 | 检查 gunicorn 日志,按并发调整 worker 数 |
| 订单状态混乱 | 状态机校验缺失 | 检查状态流转代码,确认所有入口都走统一更新方法 |
| 接单后订单丢失 | 事务未正确提交 | 检查是否使用 atomic 包裹订单更新逻辑 |
| 统计数据少 8 小时 | 时区不一致 | 统一数据库连接时区和业务展示时区 |
| 后台地址栏输入订单号显示别人的订单 | 行级权限未做 | 增加 queryset 过滤,校验订单归属 |
我给自己的排查流程是:先看后端日志,再看 Nginx 日志,然后确认是 Django 问题还是 Flask 问题,最后定位到具体接口。日志一定要加上请求 ID 和用户 ID,否则排查用户反馈时根本不知道对应哪一条记录。
5. 一些个人经验分享与后续扩展方向
这个项目从设计到上线,前前后后花了大约两个月,核心开发时间其实只占一半,另一半都花在状态流转逻辑、并发处理和部署安全这些“看不见的地方”。我最大的体会是:业务系统最怕的不是功能少,而是逻辑上的漏洞。比如状态机设计完整了,订单就不会乱;条件更新写对了,并发就不会超;兜底逻辑做足了,外部服务挂了你也不会崩。
对于后续扩展,我计划往这三个方向推进。
第一个方向是接入更智能的估价模型。前面说的估价规则还是基于人工配置的系数,下一步想引入历史成交数据,用简单回归模型预测回收价,让估价更贴近市场真实行情。其实做起来不复杂,把历史订单的“最终回收价”作为标签,设备属性作为特征,训练一个轻量模型,Flask 服务里直接加载模型文件,实时预测。
第二个方向是对接物流查询接口和微信小程序。小程序可以复用 Django 的接口,把核心回收流程搬到微信端,用户拍照上传更方便;物流查询对接快递鸟开放平台后,用户随时能看到设备走到哪一步,非常能提升信任感。
第三个方向是运营侧的数据分析。现在后台统计只做了基础指标,后续想把用户流失分析做起来:用户在哪个环节放弃最多,估价和最终价格的差距对成交率影响有多大,这些都能指导运营优化回收流程。
写到最后说一句实在的:如果你也想做类似系统,先把需求边界想清楚,再搭骨架,然后一个模块一个模块填肉。别一上来就追求完美的架构,更别一开始就 CTD。先把最核心链路跑通,你会在迭代过程中自然地发现问题、调整设计。这也是我个人做这个项目收获最大的一点。