前阵子接手了一个挺有意思的项目,内部代号叫 htgs84d4:基于微信小程序的旧衣回收商品系统。需求方是一家做本地环保回收的小团队,他们之前的回收业务全靠在微信群里手动登记,用户约时间、师傅上门称重、积分记录、兑换商品,全靠一张Excel表和群公告撑起来。一旦单量超过两位数,账就对不上,用户也经常在群里问"我的积分到账了吗""这个商品还能换吗",运营同学被这种问题磨得没脾气。
这个项目说白了就是把这套手工流程产品化:用户在小程序里提交旧衣回收预约,师傅上门取走衣物,系统按重量规格给积分,积分在商城兑换商品,后台还有一套管理界面处理订单、分类、商品上下架。技术栈选的是原生微信小程序加 Django REST Framework,前后端完全分离。如果你正好想找一个练手级的全栈项目,或者公司真有旧衣回收、再生资源回收这类业务,这篇文章应该能给你省不少弯路。
1. 项目全貌:这套系统到底解决了什么问题
1.1 旧衣回收里最常见的四个业务痛点
先聊聊业务侧。旧衣回收和普通电商完全不是一回事,乍一看都是"用户下单、平台发货/收货",但细节差距非常大。
第一个痛点是信息不透明。用户把旧衣交给回收员之后,心理预期是"至少知道我的衣服去哪了、能换多少积分"。但传统流程里,衣物被收走以后就进入了黑盒,用户只能被动等通知,甚至有些小回收点根本不通知,用户觉得自己被"白嫖"了。这种信任危机直接导致复投率极低。
第二个痛点是分类定价标准不统一。旧衣不是标准化商品,羽绒服和T恤、童装和成人装的回收价值差很多。如果全靠人眼判断,不同回收员给出的积分不一样,线下还好解释,线上用户一对比就会觉得平台"黑心"。
第三个痛点是积分和商品兑换的账目管理混乱。人工记账时期,积分错漏是家常便饭,用户投诉无门,运营核账又费时费力。
第四个痛点是订单状态没法追踪。用户想知道师傅几点来、衣物是否在质检、积分什么时候到账,在微信群模式下这些需求全部无法满足。
所以这套系统的核心并不只是"做一个电商小程序",而是要把回收履约链路线上化。系统设计上要同时满足用户端、回收师傅端、运营后台三方的诉求,我这边最终落地的方案是:用户小程序端负责预约和查询,后台用 Django Admin 承担订单审核和商品管理,回收员上门后通过后台(或接口)更新状态,全程数据闭环。
1.2 技术选型:为什么是微信小程序加 Django
技术选型这块,我和需求方做过好几轮沟通。
前端选微信小程序几乎是必然的。旧衣回收的目标用户覆盖全年龄段,其中相当一部分是中老年用户,让他们下载一个独立 App 去预约回收,门槛太高。微信小程序即用即走,还能通过微信支付、订阅消息做服务触达,对这类本地生活服务场景非常合适。
后端选 Django,核心原因是开发效率和内置后台。我是团队里唯一的后端开发,业务方还需要运营后台来管理订单和商品,Django 自带的 Admin 后台像作弊器一样,配置好 Model 之后,运营人员直接就能登录录入商品、审核订单,不用我再额外写一套管理前端。加上 Django ORM、迁移工具、DRF 的序列化器,整个 CRUD 开发速度非常快,大概一周就能把主体接口全部打通。
数据库用的 MySQL 8,虽然 SQLite 本地调试更方便,但生产环境的并发写入和事务支持还是 MySQL 更稳。整个部署链路是:微信小程序 -> Nginx -> uWSGI -> Django -> MySQL,图片和静态文件直接走对象存储,避免应用服务器扛流量。
这里多说一句,如果你是自己学习或者做毕设,完全可以不用服务器,直接把 Django 跑在本地,小程序开发者工具里勾选"不校验合法域名",后端地址填局域网 IP 就能联调,成本几乎为零。但真正上生产环境,HTTPS 证书、备案域名、小程序后台合法域名配置,每一项都得提前准备好,后面我会细讲。
2. 数据建模:表不在多,够用且不挖坑就行
2.1 核心数据表与模型代码
这套系统数据模型不算复杂,核心就五张表:用户表、衣物分类表、商品表、回收订单表、积分明细表。轮播图、地址薄这些属于锦上添花,第一版可以不做,后面再补。
用户表不搞复杂的注册逻辑,小程序的 openid 就是天然主键。
# models/user.py from django.db import models class User(models.Model): openid = models.CharField(max_length=64, unique=True, verbose_name="微信openid") nickname = models.CharField(max_length=32, blank=True, verbose_name="昵称") avatar = models.URLField(blank=True, verbose_name="头像") phone = models.CharField(max_length=11, blank=True, verbose_name="手机号") address = models.CharField(max_length=128, blank=True, verbose_name="默认上门地址") points = models.IntegerField(default=0, verbose_name="当前积分") created_at = models.DateTimeField(auto_now_add=True, verbose_name="注册时间") class Meta: db_table = "user"这里有个细节:积分字段直接冗余在用户表里。从范式角度可以拆成积分账户表,但实际业务中用户积分查询频率极高,而积分变动又只集中在回收完成这一个环节,冗余字段配合事务就能搞定,没必要为了范式增加复杂度。
衣物分类表是旧衣回收系统里最有行业特色的表。回收定价和积分计算都跟分类强相关:
class Category(models.Model): name = models.CharField(max_length=32, verbose_name="分类名称") unit_price = models.DecimalField(max_digits=6, decimal_places=2, default=0, verbose_name="每公斤积分单价") description = models.TextField(blank=True, verbose_name="分类说明") icon = models.URLField(blank=True, verbose_name="分类图标") is_active = models.BooleanField(default=True, verbose_name="是否启用")比如"羽绒服/棉服"单价 50 积分/公斤、"夏装T恤"单价 20 积分/公斤,这个单价就是用户端的计价依据。注意 unit_price 用 DecimalField 而不用 FloatField,浮点数计算会出现 0.1 + 0.2 不等于 0.3 的问题,涉及积分和钱必须用高精度类型。
商品表就是标准的商城商品模型,但加了一个来源字段,标识商品是"回收翻新"还是"积分兑换专用":
class Product(models.Model): title = models.CharField(max_length=64, verbose_name="商品标题") description = models.TextField(verbose_name="商品详情") price = models.DecimalField(max_digits=8, decimal_places=2, verbose_name="积分价格") images = models.TextField(verbose_name="图片URL列表,JSON格式") category = models.ForeignKey(Category, on_delete=models.CASCADE, verbose_name="所属分类", null=True, blank=True) source = models.SmallIntegerField(choices=[(0, "回收翻新"), (1, "积分兑换")], default=1, verbose_name="商品来源") stock = models.IntegerField(default=0, verbose_name="库存") status = models.SmallIntegerField(choices=[(0, "上架中"), (1, "已下架"), (2, "已售完")], default=0, verbose_name="状态") created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")回收订单表是最核心的一张表,它记录了整个履约链路:
class RecycleOrder(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="下单用户") category = models.ForeignKey(Category, on_delete=models.CASCADE, verbose_name="回收分类") estimated_weight = models.DecimalField(max_digits=5, decimal_places=2, default=0, verbose_name="预估重量(kg)") actual_weight = models.DecimalField(max_digits=5, decimal_places=2, null=True, blank=True, verbose_name="实收重量(kg)") pickup_time = models.DateTimeField(verbose_name="预约上门时间") address = models.CharField(max_length=128, verbose_name="上门地址") status = models.SmallIntegerField(choices=[ (0, "待上门"), (1, "已收取"), (2, "质检中"), (3, "已完成"), (4, "已取消") ], default=0, verbose_name="订单状态") points_awarded = models.IntegerField(default=0, verbose_name="实际发放积分") remark = models.CharField(max_length=255, blank=True, verbose_name="用户备注") created_at = models.DateTimeField(auto_now_add=True, verbose_name="下单时间")2.2 订单状态机:把"信任感"设计进流程里
旧衣回收订单的状态设计,我的体会是:一定要把"质检中"这个状态单独拎出来。
回收员上门取走衣物之后,衣物要先送到分拣中心做质检,确认没有混入不可回收物,再称重计算积分。这个质检环节是保护平台利益的关键,如果省略掉,用户随便塞一堆杂物也能拿积分,平台很快就赔穿。但质检需要时间,用户下单后如果长时间看到订单停在"已收取",又会觉得平台拖着不给积分。
所以我在订单状态流转上加了一条消息推送逻辑:每次状态变更都通过微信订阅消息通知用户,其中"质检中"转"已完成"的时候,在接口里同时发放积分并生成积分明细。
订单状态流转是系统的业务命脉,我通过 DRF 的 action 接口来控制流转,而不是让前端随意改 status 字段:
# serializers/recycle_order.py from rest_framework.decorators import action from rest_framework.response import Response @action(detail=True, methods=["post"]) def complete(self, request, pk=None): """回收员或管理员调用:完成质检并发放积分""" order = self.get_object() if order.status != 2: return Response({"detail": "当前状态不可完成"}, status=400) # 积分发放必须在事务里执行,防止并发重复发放 from django.db import transaction with transaction.atomic(): order.lock = True # select_for_update 加行锁 order.status = 3 order.actual_weight = request.data.get("actual_weight", order.estimated_weight) awarded = int(order.actual_weight * order.category.unit_price) order.points_awarded = awarded order.save(update_fields=["status", "actual_weight", "points_awarded"]) user = request.user user.points += awarded user.save(update_fields=["points"]) # 写入积分流水 from .models import PointsLog PointsLog.objects.create( user=user, order=order, points=awarded, change_type="recycle_income" ) return Response({"points_awarded": awarded})这个接口里我用了 transaction.atomic(),并且在查询订单时加行锁,目的就是防并发。实际运营中确实出现过重复点击导致积分发两次的情况,代码层面的保险是必须的。
3. 后端实现:Django REST Framework 从零到能跑
3.1 项目初始化与关键配置
后端项目我用的是 Django 4.x 加 Django REST Framework,建项目这些基础操作就不啰嗦了,重点说几个容易踩坑的配置。
第一个是跨域问题。小程序端请求后端接口属于跨域,必须在 Django 里配置 CORS。我用的是 django-cors-headers:
# settings.py INSTALLED_APPS = [ # ... "rest_framework", "corsheaders", ] MIDDLEWARE = [ "corsheaders.middleware.CorsMiddleware", # 注意 CorsMiddleware 要放在 CommonMiddleware 前面 ] CORS_ALLOWED_ORIGINS = [ "http://localhost:3000", # 本地调试 ] CORS_ALLOW_CREDENTIALS = True我在本地开发时为了方便直接用了CORS_ALLOW_ALL_ORIGINS = True,但上线前必须改回白名单模式,否则任何网站都能调用你的接口,安全隐患很大。
第二个是数据库连接。Django 默认连 SQLite,生产换 MySQL 只要改 DATABASES 配置,同时注意 MySQL 的字符集要设置成 utf8mb4:
DATABASES = { "default": { "ENGINE": "django.db.backends.mysql", "NAME": "recycle_db", "USER": "recycle_user", "PASSWORD": "********", "HOST": "127.0.0.1", "PORT": "3306", "OPTIONS": { "charset": "utf8mb4", }, } }utf8mb4 这个必须强调,用户微信昵称经常带 emoji 表情,你用普通 utf8 存进去直接报错,别问我怎么知道的。
第三个是 DRF 的分页配置。商品列表、订单列表都需要分页,我在 settings.py 里统一配置:
REST_FRAMEWORK = { "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", "PAGE_SIZE": 10, "DEFAULT_AUTHENTICATION_CLASSES": [ "apps.users.authentication.WxTokenAuthentication", ], }分页统一了,前端只需要传 page 参数就能翻页,不需要每个 viewset 单独写。
3.2 微信登录与自定义 token 鉴权
微信小程序登录流程是固定的:前端 wx.login 拿 code,后端拿 code 向微信接口换 openid,然后给前端签发一个登录态 token。
这里有个技术选择问题。Django 自带的 session 机制在前后端分离场景下不太好用,我选择了自己签发 token 字符串,存到数据库里,每次请求时校验。简单粗暴,胜在可控。
核心代码我封装成了一个独立的认证类:
# apps/users/authentication.py import hashlib import requests from rest_framework.authentication import BaseAuthentication from rest_framework.exceptions import AuthenticationFailed from django.conf import settings def wx_code_to_openid(code): """调用微信接口换取 openid""" url = "https://api.weixin.qq.com/sns/jscode2session" params = { "appid": settings.WX_APPID, "secret": settings.WX_SECRET, "js_code": code, "grant_type": "authorization_code", } resp = requests.get(url, params=params, timeout=5).json() if "openid" not in resp: raise AuthenticationFailed("微信登录失败: %s" % resp.get("errmsg")) return resp["openid"] def generate_token(openid): raw = f"{openid}-{settings.SECRET_KEY}" return hashlib.sha256(raw.encode()).hexdigest() class WxTokenAuthentication(BaseAuthentication): def authenticate(self, request): token = request.headers.get("Authorization", "") if token.startswith("Bearer "): token = token[7:] # 查 token 表,找不到抛异常 from .models import UserToken try: token_obj = UserToken.objects.select_related("user").get(token=token) except UserToken.DoesNotExist: raise AuthenticationFailed("无效的登录凭证") return (token_obj.user, token_obj)登录接口长这样:
# apps/users/views.py from rest_framework.views import APIView from rest_framework.response import Response from .models import User, UserToken class WxLoginView(APIView): authentication_classes = [] # 登录接口不鉴权 permission_classes = [] def post(self, request): code = request.data.get("code") openid = wx_code_to_openid(code) user, _ = User.objects.get_or_create(openid=openid) token = generate_token(openid) # token 存库,带过期时间 UserToken.objects.update_or_create( user=user, defaults={"token": token, "expired_at": ...} ) return Response({"token": token, "user_id": user.id})注意登录接口必须设置authentication_classes = [],否则 DRF 默认全局鉴权会把你这个接口也拦了。
3.3 回收预约和商品接口的落地细节
商品列表接口是一个标准的 ViewSet,但有几个地方要注意。旧衣回收商城里的商品,并不是所有都显示给所有用户,比如"回收翻新"类商品库存很不稳定,经常要下架。我用 status 过滤加上缓存,减少数据库压力:
# apps/products/views.py from rest_framework import viewsets from rest_framework.decorators import action from .models import Product from .serializers import ProductSerializer class ProductViewSet(viewsets.ReadOnlyModelViewSet): """商品只读接口:用户端只能看,不能改""" serializer_class = ProductSerializer permission_classes = [] # 公开商品信息 def get_queryset(self): qs = Product.objects.filter(status=0) cat = self.request.query_params.get("category") if cat: qs = qs.filter(category_id=cat) return qs回收预约接口是最核心的下单接口。前端提交的数据包含分类、预估重量、上门时间、地址,后端需要做几件事:校验时间不能是过去时间、校验地址非空、校验同一用户不能同时有多个"待上门"订单。
同用户不能有多个待上门订单,这个限制很多新手想不到。如果不加限制,用户一气儿下五个单,回收员跑一趟扑空四次,运营成本直接爆炸。我在下单接口里做了一行判断:
# apps/recycle/views.py class RecycleOrderCreateView(APIView): def post(self, request): data = request.data # 同一个用户只能有一个进行中的预约 pending = RecycleOrder.objects.filter( user=request.user, status__in=[0, 1, 2] ).exists() if pending: return Response({"detail": "您有正在进行中的预约"}, status=400) # 其他字段校验和创建...这个限制在实际运营中非常有用,避免了大量无效上门。
4. 小程序端:从登录到下单的完整链路
4.1 页面结构规划
小程序端我采用了经典的 TabBar 五页面结构:首页、回收预约、商城、订单、我的。微信小程序官方推荐 tabBar 是 2-5 个 tab,五个刚好占满。
app.json 配置:
{ "pages": [ "pages/index/index", "pages/recycle/recycle", "pages/mall/mall", "pages/order/order", "pages/profile/profile" ], "tabBar": { "list": [ { "pagePath": "pages/index/index", "text": "首页" }, { "pagePath": "pages/recycle/recycle", "text": "回收" }, { "pagePath": "pages/mall/mall", "text": "商城" }, { "pagePath": "pages/order/order", "text": "订单" }, { "pagePath": "pages/profile/profile", "text": "我的" } ] } }首页放公告、分类入口、绿色环保理念文案,让用户一眼看懂平台是做什么的。回收预约页是核心交互页,做成一整个表单,从选择衣物分类、预估重量,到选择上门时间段、填写地址。商城页面就是标准商品卡片流,支持分类筛选。订单页展示用户所有回收订单,每个订单卡片上显示状态和积分。我的页面展示积分余额、个人资料编辑、关于我们。
4.2 登录态处理与 request 封装
小程序端的登录态处理是整个前端的基建。我封装了一个 wob 接口,所有 wx.request 都走这个封装,自动带上 token:
// utils/request.js const BASE_URL = "https://api.yourdomain.com"; function request(path, method = "GET", data = {}) { return new Promise((resolve, reject) => { const token = wx.getStorageSync("token"); wx.request({ url: `${BASE_URL}${path}`, method, data, header: { "Content-Type": "application/json", "Authorization": token ? `Bearer ${token}` : "" }, success(res) { if (res.statusCode === 401) { // token 失效,重新静默登录 wx.removeStorageSync("token"); login().then(() => { // 重新请求 resolve(request(path, method, data)); }); return; } if (res.statusCode >= 200 && res.statusCode < 300) { resolve(res.data); } else { wx.showToast({ title: res.data.detail || "请求失败", icon: "none" }); reject(res.data); } }, fail(err) { wx.showToast({ title: "网络异常", icon: "none" }); reject(err); } }); }); } function login() { return new Promise((resolve) => { wx.login({ success(res) { wx.request({ url: `${BASE_URL}/api/user/login/`, method: "POST", data: { code: res.code }, success(loginRes) { wx.setStorageSync("token", loginRes.data.token); resolve(); } }); } }); }); } module.exports = { request, login };这里有个细节:login 之后重新请求原接口,能保证用户无感知续期。实际运营中确实会出现 token 过期,尤其是用户长期不打开小程序,重新打开时如果直接让用户重新登录,流失率会很高。
首页加载商品列表的代码:
// pages/index/index.js const { request } = require("../../utils/request"); Page({ data: { categories: [], products: [], currentCategory: null, page: 1, loading: false }, onLoad() { this.fetchCategories(); this.fetchProducts(); }, fetchCategories() { request("/api/categories/").then((res) => { this.setData({ categories: res.results }); }); }, fetchProducts() { const { page, currentCategory, loading, products } = this.data; if (loading) return; this.setData({ loading: true }); let url = `/api/products/?page=${page}`; if (currentCategory) url += `&category=${currentCategory}`; request(url).then((res) => { this.setData({ products: [...products, ...res.results], page: page + 1, loading: false }); }); } });4.3 回收预约表单:一次完整的提交流程
回收预约页提交表单时,前端要做一轮基础校验,减少无效请求。联系电话要正则校验,上门时间要选未来时间。用户填完表单点提交时,我先把数据拼装好,调用后端接口:
// pages/recycle/recycle.js submitOrder() { const { categoryId, weight, pickupTime, address, phone, remark } = this.data; if (!categoryId) return wx.showToast({ title: "请选择衣物分类", icon: "none" }); if (!weight || weight <= 0) return wx.showToast({ title: "请输入有效重量", icon: "none" }); if (!pickupTime) return wx.showToast({ title: "请选择上门时间", icon: "none" }); if (!/^1\d{10}$/.test(phone)) return wx.showToast({ title: "手机号格式错误", icon: "none" }); this.setData({ submitting: true }); request("/api/recycle/orders/", "POST", { category_id: categoryId, estimated_weight: weight, pickup_time: pickupTime, address, phone, remark }) .then((res) => { wx.showToast({ title: "预约成功", icon: "success" }); // 跳转到订单列表 wx.switchTab({ url: "/pages/order/order" }); }) .finally(() => this.setData({ submitting: false })); }这里我要讲一个经验:提交按钮一定要加 loading 状态,防重复点击。小程序端用户网络不好时容易双击表单提交按钮,后端虽然做了幂等控制,但前端层面也不能放任。
5. 常见问题与排查技巧实录
5.1 一张表解决大部分运行期问题
整个项目跑下来,最有价值的经验全在坑里。我整理一个速查表,都是真实踩过的:
| 问题现象 | 根本原因 | 解决方式 |
|---|---|---|
| 小程序请求失败,提示"url not in domain list" | 开发者工具里未勾选"不校验合法域名"或线上域名未配置 | 本地开发在"详情 -> 本地设置"勾选不校验;线上在微信公众平台配置 request 合法域名 |
| Django 后台密码重置无效 | 终端改了字符集但表还是旧字符集 | 确认表和库都是 utf8mb4,ALTER TABLE ... CONVERT TO CHARACTER SET utf8mb4 |
| 商品图片显示裂图 | 图片域名未加入 downloadFile 合法域名 | 公众平台添加图片域名白名单,开发时也需勾选"不校验" |
| 积分发放重复 | 前端双击提交,后端事务边界不对 | 后端加事务和行锁,接口做幂等处理 |
| 用户昵称带表情导致入库报错 | 数据库字符集不是 utf8mb4 | 建库时就指定 utf8mb4,连接配置再加 charset |
| 真机预览请求不到本地服务 | 手机和电脑不在同一局域网,或防火墙拦截 | 保证同一 Wi-Fi,电脑防火墙放行 8000 端口,用局域网 IP 而非 localhost |
| DEBUG=False 时静态文件丢失 | Django 生产环境不处理静态文件 | 用 Nginx 托管静态文件,或配置 whitenoise 中间件 |
5.2 实操经验:这些坑不踩一遍不会信
第一个坑是微信登录的 code 二次使用问题。wx.login 返回的 code 只能用一次,换过 openid 之后立即失效。我开始把 wx.login 写在 onLaunch 里,每次冷启动都调一次,没问题。但后来加了"token 过期自动重新 login"的逻辑,如果 login 和请求接口并发执行,就会出现 code 被后端重复使用的情况,第二个请求直接报"invalid code"。解决办法是把 login 封装成单例 Promise,保证同一时间只有一个登录请求在跑。
第二个坑是 Django 的 DateTimeField 时区问题。本地联调时录入的上门时间,部署到云服务器后差了 8 个小时。这是 USE_TZ 的典型表现。我建议这类业务系统直接USE_TZ = True,同时TIME_ZONE = "Asia/Shanghai",前端传时间字符串时带时区偏移,后端存 UTC,展示时转本地。如果嫌麻烦想用USE_TZ = False,必须确保服务器时区也是东八区,否则排期信息直接乱掉。
第三个坑是图片上传方案。旧衣回收的商品图片、分类图标都在后台录入,如果直接用 Django Admin 的默认 FileField,图片存在应用服务器上,后续扩展和备份都麻烦。我最终把图片上传改成了对象存储直传,前端拿到存储签名后直接传给对象存储,后端只保存 URL 字符串。这个改造虽然费了点功夫,但上线后效果非常好,服务器磁盘压力为零。
第四个坑是回收订单的取消逻辑。用户取消待上门订单时,如果回收员已经出发了,会造成体验事故。我加了一个时间判断:预约上门时间前 2 小时内不允许取消,只能联系客服人工关闭。这个规则看着简单,但能避免大部分纠纷。
第五个经验是关于业务扩展的。这套系统上线后,业务方很快提出了新需求:旧衣回收的同时想要附带旧书、旧手机回收。因为这个项目的数据模型把"回收品类"抽象成了 Category 表,新品类只是加一条记录的事,不需要改表结构。这个设计当初是无意中做对的,但也说明了一个通用原则:凡是存在扩展可能性的字段,尽量做成字典表而不是写死枚举。
6. 一些后续可做的优化方向
最后再分享一个我的个人判断。如果你准备在旧衣回收这个方向继续深入,有几个点值得优先考虑。
第一是接入微信订阅消息推送。现在用户需要主动打开小程序才能看订单状态,体验一般。微信订阅消息可以在状态变化时推送通知,这个对回收类业务的用户粘性提升非常明显,用户最关心的就是"师傅什么时候到""积分什么时候到账"。
第二是增加顺手回收品类。旧衣回收只是废弃资源回收的冰山一角,图书、家电、手机、塑料瓶都可以同构复用到这套预约上门回收的流程里。我刚说过 Category 表已经预留了扩展空间,后端几乎不用改。
第三是回收员端的小程序。目前回收员是通过后台账号操作订单状态,但回收员在外面跑,手机后台操作不方便。后续可以给回收员做一个独立小程序,只做接单、状态变更、称重记录三个功能,用户体验会好非常多。
这套系统从开发到上线,最大的感悟是:代码并不难,难的是把业务流程里的每个角色都服务好。用户要的是省心和信任,运营要的是效率和可控,回收员要的是简单和稳定。作为一个全栈开发者,时刻想着这三方需求,设计出来的系统才能真的可用。
如果你也准备做类似的项目,建议先花两天时间把业务方拉到一起,把回收流程的每一个环节画清楚,再动手写代码。磨刀不误砍柴工,这句话在这个项目里是实打实的真理。