做家电电商的项目,我从接手第一个需求开始就发现,真正的坑往往不在页面有多炫,而在售后流程带来的各种异常状态。用户今天下单买一台洗衣机,明天可能就发起维修申请,从生成订单到上门服务,中间涉及客服、仓库、维修工、服务网点多个角色,任何一个环节断了,后面都是扯皮。这个“Python基于flask-django家用电器家电销售商城售后服务管理系统”的项目,就是围绕这条主线展开的:商城卖货是入口,售后才是留住用户的关键。
这个项目本质上是把两套业务装进一个系统:一套面向消费者的家电销售商城,负责商品展示、购物车、下单支付和库存管理;另一套面向运营和维修团队,负责售后服务工单、安装派单、维修记录、回访评价。技术上选择了 Python 生态,用 Flask 和 Django 配合完成。如果只是做毕业设计或者个人练手,这套组合足够体现完整度;如果是放在真实业务里当原型,也能直接拿下来改一改就上线。
我会把整个项目从框架选型、数据库设计,到关键模块的落地细节、部署排障一条条拆开讲。内容包括实际可复用的代码片段、状态机设计思路,以及一些文档里根本不会写的高频坑。无论你是刚学完 Python 想找实战项目的萌新,还是需要一个可二次开发模板的技术负责人,这篇内容应该都能给你省几天时间。
1. 项目选题与整体设计思路
1.1 为什么家电销售必须单独做售后管理
卖家电和卖服装是两种完全不同的电商模式。服装退货率高,但退完基本就结束了;家电牵扯安装、上门维修、零件更换、保修期计算,甚至还有以旧换新、延保服务。订单完成只是服务的开始,而不是终点。
这个项目的第一个设计前提,就是要把“订单闭环”和“售后闭环”打通。用户在前台下单购买冰箱,售后端能看到这个订单对应的保修状态;用户提交维修申请,客服后台能直接关联原始订单、商品型号和购买日期;维修工上门处理完,系统自动更新工单状态并触发用户评价。这些动作之间不能靠人工抄 Excel,必须靠数据模型和状态机串起来。
我见过不少同类项目把售后做成一个单独的表,和订单没有任何关联,结果客服每天拿着手机翻订单号,效率极低。这个项目的核心思路恰恰相反:订单订单项是售后工单的外键,所有售后服务都挂在具体订单明细上。这样既能校验保修期,也能追溯商品批次,后续做故障率统计也有了数据基础。
另一个容易忽略的点是流程角色。家电售后的参与方多:用户、前台客服、售后审核员、维修师傅、网点管理者。系统必须区分这些人的权限和操作范围,而不是把所有按钮都堆在一个后台里。Django 自带的用户权限体系在这里帮了大忙,后面我会细讲。
1.2 Flask 和 Django 到底怎么选:别被框架之争带偏
标题里同时出现 Flask 和 Django,很多人的第一反应是“这两个不是对立面吗,为什么要放一起”。实际上,这个标题可以有两种理解:一是让你二选一,二是在同一个系统里让它们各司其职。做项目时我建议优先采用“Django 主管核心业务,Flask 管轻量服务和实时通道”的组合。
两个框架的真实差异,我用一个表格说清楚:
| 对比维度 | Django | Flask |
|---|---|---|
| 自带组件 | Admin 后台、ORM、迁移、认证 | 只有核心路由和 WSGI,其余靠扩展 |
| 适合场景 | 业务复杂、表关系多、需要后台管理 | 接口轻量、逻辑简单、实时推送 |
| 学习曲线 | 偏陡,适合一口气搭完大项目 | 上手快,灵活性高但约束少 |
| 部署方式 | wsgi.py + gunicorn | 普通服务或 SocketIO 服务 |
| 生态风格 | 全家桶,规范统一 | 像组装乐高,自由但容易乱 |
选择 Django 做商城和售后主系统,看重的是它的 ORM、Amin 后台和 Form/DRF 生态。商城订单、售后工单这类数据密集型业务,最怕的就是模型和数据库脱节,Django 的 migration 机制让表结构变更可控,这一点在开发后期尤其值钱。
Flask 在这个项目里的角色,我定位成两个独立微服务:一个是售后消息推送,用 Flask-SocketIO 提供 WebSocket 实时通道;另一个是文本智能匹配服务,处理用户故障描述与服务类型的相似度计算。这两个服务逻辑独立、升级频繁,单独拆出来用 Flask 会比塞进 Django 的请求/响应循环里更清爽,也更符合实际部署时“哪个服务挂了不拖垮主站”的原则。
1.3 整体架构和数据流:从下单到售后
整个系统我按三层划分:前端展示层负责商城页面、用户中心的订单和售后入口;业务服务层由 Django 处理核心交易、工单、用户权限,Flask 处理实时推送和文本匹配;数据存储层用 MySQL 保存业务数据,Redis 承担缓存、消息队列和实时发布订阅。
用户真实的操作流是这样的:浏览商品并加入购物车,提交订单后通过模拟支付或接入第三方支付;支付回调到达 Django,订单从待付款变为待发货,系统异步扣减库存;仓库发货后订单进入待收货状态;用户确认收货后,如果发现质量问题,在订单详情页发起售后申请,选择问题类型并填写故障描述。
售后申请提交后,Django 生成一张售后工单并调用 Flask 的匹配服务,对故障描述做关键词提取和相似度计算,推荐出一个维修类别和合适的处理方式;审核员在后台确认工单,系统自动派单到对应网点;维修师傅接单后上门处理,在移动端或后台填写处理结果;用户收到消息推送并完成评价。全流程的状态变化都会通过 Redis 发布订阅推到前端,让页面实时刷新。
这套数据流的价值在于:任何一个环节出了异常,都能在数据库里找到明确记录,而不是靠微信群接龙。后面几节我会按实际开发顺序,把每个模块的实现要点和代码展开。
2. 核心模块拆解与数据库设计
2.1 数据模型设计:从用户到订单再到工单
先讲用户表。家电系统里的用户不仅是买家,还有维修师傅、客服和网点管理员。Django 的 AbstractUser 扩展一套自定义用户模型,加手机号和收货地址就够了,业务角色通过 Group 和权限控制,不要拆一堆用户表,否则后面关联会越来越乱。
from django.contrib.auth.models import AbstractUser from django.db import models class User(AbstractUser): phone = models.CharField("手机号", max_length=20, blank=True) address = models.CharField("默认收货地址", max_length=255, blank=True) is_service_staff = models.BooleanField("是否售后人员", default=False) class Meta: db_table = "sys_user"商品表这块,家电有个特点:同一个 SKU 会有参数差异,比如颜色、容量、能效等级。所以商品表不是简单一个表,而是一张产品表加一张商品规格表。产品表存品牌、型号、主图、描述;规格表存具体 SKU、价格和库存。
class Category(models.Model): name = models.CharField("分类名称", max_length=50) class Brand(models.Model): name = models.CharField("品牌", max_length=50) class Product(models.Model): category = models.ForeignKey(Category, on_delete=models.PROTECT) brand = models.ForeignKey(Brand, on_delete=models.PROTECT) name = models.CharField("商品名称", max_length=200) detail = models.TextField("详情") status = models.SmallIntegerField("上下架状态", default=1) class ProductSku(models.Model): product = models.ForeignKey(Product, related_name="skus", on_delete=models.CASCADE) spec = models.CharField("规格说明", max_length=100) price = models.DecimalField("售价", max_digits=10, decimal_places=2) stock = models.PositiveIntegerField("库存", default=0)订单表我强烈建议独立设计一个订单号字段 order_sn,不要直接用自增主键暴露给用户。自增主键一旦被遍历,订单量就直接泄露了。订单和订单明细拆成两张表,是为了支持一单多商品以及部分退款。
class Order(models.Model): STATUS_CHOICES = [ (10, "待付款"), (20, "待发货"), (30, "待收货"), (40, "已完成"), (50, "售后中"), (60, "已关闭"), ] order_sn = models.CharField("订单编号", max_length=64, unique=True) user = models.ForeignKey(User, on_delete=models.PROTECT) total_amount = models.DecimalField("订单总额", max_digits=10, decimal_places=2) status = models.SmallIntegerField("状态", choices=STATUS_CHOICES, default=10) created_at = models.DateTimeField(auto_now_add=True) class OrderItem(models.Model): order = models.ForeignKey(Order, related_name="items", on_delete=models.CASCADE) sku = models.ForeignKey(ProductSku, on_delete=models.PROTECT) product_name = models.CharField("快照商品名", max_length=200) price = models.DecimalField("成交价", max_digits=10, decimal_places=2) quantity = models.PositiveIntegerField("数量", default=1)注意 product_name 和 price 这里我写了“快照”。实际订单生成后,商品名称和价格必须复制到订单明细里,不能通过外键实时查询。否则运营改了商品价格,历史订单金额也会跟着变,财务对账就乱了。
售后工单表是整个售后系统的核心,它挂在 OrderItem 上而不是 Order 上,因为同一个订单里可能有一台冰箱要维修、一台电视要退货,这是两种完全独立的售后处理。
class AfterSaleTicket(models.Model): TICKET_TYPE = [ ("repair", "维修"), ("refund", "退货"), ("exchange", "换货"), ("install", "安装"), ] STATUS_CHOICES = [ (10, "待审核"), (20, "待派单"), (30, "已派单"), (40, "服务中"), (50, "待确认"), (60, "已完成"), (70, "已驳回"), ] ticket_no = models.CharField("工单号", max_length=32, unique=True) order_item = models.ForeignKey(OrderItem, on_delete=models.PROTECT) user = models.ForeignKey(User, on_delete=models.PROTECT) type = models.CharField("售后类型", max_length=20, choices=TICKET_TYPE) desc = models.TextField("问题描述") images = models.JSONField("现场图片", default=list) status = models.SmallIntegerField("状态", choices=STATUS_CHOICES, default=10) result = models.TextField("处理结果", blank=True) created_at = models.DateTimeField(auto_now_add=True)images 字段用 JSONField 存图片 URL 列表,比再建一张附表简单,而且这次售后一般就三五张图,不需要复杂关联。Django 的 JSONField 在 MySQL 底层走 JSON 类型,查询也能满足需求。
2.2 订单状态机:从下单到完成的关键约束
订单状态不能靠随便 update,否则会出现“货还没发,订单却被标成已完成”这种低级错误。项目里我把状态迁移收敛到 OrderService 里,每个方法只允许特定状态往后走。
- 创建订单:生成 order_sn,状态 10,同时锁定库存
- 支付成功:状态 10 → 20,扣减真实库存
- 仓库发货:状态 20 → 30,写入物流单号
- 用户确认:状态 30 → 40,完成交易
- 发起售后:状态 40 → 50,售后流程接管
为什么支付回调要做幂等?第三方支付平台可能会因为网络重试,同一个支付通知发多次,如果后端不校验,订单金额会被重复处理。做法很简单:在支付回调里先查 order_sn 对应订单当前状态,如果已经是支付完成状态,直接返回成功,不再执行任何业务逻辑。
库存扣减也有讲究。提交订单时先“锁定库存”,防止超卖;超过 30 分钟未支付则释放库存;支付成功后真正扣减。如果要做得更严谨,可以在 Redis 里做库存缓存并用 Lua 脚本扣减,但作为项目原型,用数据库事务配合 select_for_update 锁住 SKU 行也够用。
2.3 售后工单全生命周期设计
工单状态看起来多,但实际上就是一条主线:用户申请 → 客服审核 → 派单 → 师傅上门 → 确认结果 → 评价关闭。
我特别想强调“派单”这个环节。家电售后派单不能只是随机扔给一个维修师傅,要考虑品牌授权网点、用户所在城市、维修类型匹配度。项目里我先在后台维护了服务网点表,网点绑定品牌和维修品类,派单时先过滤出符合条件的网点,再按当前待处理工单数最少的原则分配。
“服务中”状态要记录每次操作时间。师傅上门后点击开始服务,系统记录服务开始时间;服务结束后填写配件消耗和故障原因,上传图片;用户端看到状态变化,可以确认结果或发起申诉。这个设计能有效减少“师傅到底来没来”的纠纷,因为每一步都有时间戳和责任人。
工单还应该支持“多次服务”。一次维修可能不到位,用户需要二次上门。所以在工单表里我不建议只放一个 result 字段,而是另外建一张工单处理记录表,每次操作都追加一条记录。工单主表保存当前状态,明细表留下所有操作轨迹,后面分析“哪些故障重复率高”也靠这张表。
3. 关键技术项落地:API、推送、匹配与管理后台
3.1 基于 Django REST Framework 构建商城 API
前端如果打算做前后端分离,就需要把后端能力暴露成 API。这里使用 DRF 而不是手写 JsonResponse,好处在于序列化、鉴权和分页都是现成的,代码量能省不少。
# api/views.py from rest_framework.viewsets import ReadOnlyModelViewSet from rest_framework.permissions import IsAuthenticatedOrReadOnly from .serializers import ProductSerializer, OrderSerializer class ProductViewSet(ReadOnlyModelViewSet): queryset = Product.objects.filter(status=1) serializer_class = ProductSerializer permission_classes = [IsAuthenticatedOrReadOnly] class OrderViewSet(viewsets.ModelViewSet): serializer_class = OrderSerializer permission_classes = [IsAuthenticated] def get_queryset(self): return Order.objects.filter(user=self.request.user)登录认证这块,我一开始用的 Django Session 认证,结果前端 WebSocket 鉴权时挺别扭,最终还是切到了 JWT。用户登录返回 access_token 和 refresh_token,前端请求头带上 Bearer token。Django 端配置 djangorestframework-simplejwt 之后,只改 DEFAULT_AUTHENTICATION_CLASSES 就能接入。
权限控制不要只在视图层做,也要在序列化层做。比如订单详情里的售后按钮,只有在订单状态为“已完成”时才显示;用户只能看到自己的订单。DRF 的 get_serializer_context 可以把当前 request 传进去,序列化器里根据 request.user 动态产生字段。
3.2 用 Flask + WebSocket 实现售后消息实时推送
这个模块是这个项目里 Flask 发挥价值的地方。售后工单状态一变,用户前端希望立刻看到“审核通过”“师傅已上门”这些变化,而不是反复刷新页面。用传统 AJAX 轮询也能实现,但体验差而且浪费服务器资源。WebSocket 长连接更合适,但 Django 3 之前原生不原生支持,即使装了 channels 也带着异步和 Daphne 一堆配置,对项目初期来说太重了。
我的做法是把 Flask-SocketIO 独立成一个推送服务,跑在 5000 端口。它的任务很单一:订阅 Redis 的 ticket_events 频道,收到消息就向所有连接中的用户广播,或者按 user_id 定向推送。
# flask_push/app.py import json import redis import threading from flask import Flask, request from flask_socketio import SocketIO, emit, join_room app = Flask(__name__) app.config["SECRET_KEY"] = "please-change-me" socketio = SocketIO(app, cors_allowed_origins="*", async_mode="threading") cache = redis.Redis(host="127.0.0.1", port=6379, db=5) def event_listener(): pubsub = cache.pubsub() pubsub.subscribe("ticket_events") for message in pubsub.listen(): if message["type"] != "message": continue socketio.emit("ticket_update", message["data"], to=None) @socketio.on("connect") def handle_connect(): user_id = request.args.get("user_id") if user_id: join_room(str(user_id)) @socketio.on("ticket_follow") def handle_follow(data): user_id = data.get("user_id") if user_id: join_room(str(user_id)) if __name__ == "__main__": threading.Thread(target=event_listener, daemon=True).start() socketio.run(app, host="0.0.0.0", port=5000, debug=False)Django 端在工单状态变更的地方,发布消息到同一个 Redis:
# apps/aftersale/services.py import redis import json from django.conf import settings r = redis.Redis(host=settings.REDIS_HOST, port=settings.REDIS_PORT, db=5) def publish_ticket_event(ticket): r.publish("ticket_events", json.dumps({ "ticket_no": ticket.ticket_no, "status": ticket.status, "type": ticket.type, "update_time": ticket.updated_at.strftime("%Y-%m-%d %H:%M:%S"), }, ensure_ascii=False))前端用 Socket.IO 客户端连接:
const socket = io('http://localhost:5000', { transports: ['websocket'], query: { user_id: currentUserId } }); socket.on('ticket_update', (data) => { refreshTicketList(data.ticket_no); });这里要注意几个细节。第一,发布订阅的 Redis 数据库编号要一致,建议用 db=5 专门跑业务消息,别和缓存数据混一个库。第二,Flask-SocketIO 生产不能用socketio.run裸跑,后面部署会讲到。第三,连接鉴权不能只靠 query 里的 user_id,正式项目应该用 token 换房间号,这个在项目管理和安全设计里再细化。
3.3 故障描述智能匹配:用类似度算法减少人工判断
回到售后工单的审核环节。用户填写的故障描述五花八门,“冰箱不制冷了”“空调异响”“开机没反应”,客服要逐个归类,效率低。我在 Flask 里实现了一个轻量匹配服务,先用 jieba 做中文分词,再去和故障库里的标准问题做相似度计算,返回最可能的售后类型和建议处理方案。
这里的底层算法我选了difflib.SequenceMatcher,配合关键词权重。复杂场景可以换成 TF-IDF 或向量模型,但家电故障描述比较短,关键词直接命中已经能覆盖大部分需求,先用轻量方案跑起来再说。
import jieba import difflib def build_keywords(text): return [w for w in jieba.lcut(text) if len(w) > 1] def match_rule(desc): text = desc.strip() if len(text) < 4: return {"code": "noise", "message": "描述过短"} if len(set(text)) <= 2: return {"code": "noise", "message": "疑似无效描述"} rules = [ ("refund", ["退货", "退款", "不想要"]), ("exchange", ["换货", "换一台", "换新"]), ("install", ["安装", "装一下", "上门装"]), ("repair", ["维修", "修一下", "坏了", "不制冷", "异响", "故障"]), ] results = [] for code, keywords in rules: score = 0.0 for kw in keywords: if kw in desc: score += 1.0 results.append((code, score)) best = max(results, key=lambda x: x[1]) if best[1] <= 0: return {"code": "unknown", "suggestion": "请客服人工判断"} return {"code": "ok", "suggestion": best[0]}我当时做这个模块踩了一个坑:用户描述里经常有错别字,比如“不制冷”写成“不制泠”,直接关键词匹配就失效。后来我在故障库维护了一个“常见错法”映射表,在分词前先做一轮替换,召回率高了不少。别小看这几个错别字,真实用户输入质量远比教程里的数据差。
“无效信息过滤”也是这个模块附带的价值。不少工单的自我描述只有“坏了”两个字,或者全是重复字符。系统在创建工单前先做一次质量检查,质量过低的直接弹提示让用户补充图片或选择故障类型,而不是硬着头皮派单。
3.4 Django Admin 定制:运营后台的快速落地
使用 Django 自带的 Admin,是将后台快速落地最可靠的方式。虽然很多人觉得 Admin 丑,但它对内部运营系统来说,产出速度远高于从零写一套 Vue 后台。通过继承 admin.ModelAdmin 自定义列表页和操作按钮,能把售后的主要操作集中到一处。
# apps/aftersale/admin.py from django.contrib import admin from .models import AfterSaleTicket @admin.register(AfterSaleTicket) class AfterSaleTicketAdmin(admin.ModelAdmin): list_display = ("ticket_no", "user", "order_item", "type", "status", "created_at") list_filter = ("type", "status") search_fields = ("ticket_no", "user__username", "desc") list_select_related = ("user", "order_item__order") actions = ["mark_applying"] def mark_applying(self, request, queryset): queryset.update(status=20) mark_applying.short_description = "批量审核通过并进入派单"Admin 的 list 页面如果表关联过多,记得用list_select_related减少 SQL 查询数量,否则后台列表数据一多就会慢得离谱。另外不要把全部字段都放进编辑页,售后工单的 ticket_no、user 这类字段应该设为只读,防止误改。
运营数据看板我建议单独做一个页面,从订单表、工单表里聚合出今日销售额、售后率、维修类型 Top,用简单的 /api/dashboard 返回 JSON,前端用 ECharts 展示。数据量大以后再改成定时任务把统计结果写入汇总表,目前原型阶段实时聚合查询就行。
4. 部署上线与常见问题排查实操
4.1 环境初始化与依赖清单
项目是 Python 应用,建议直接用 Python 3.10 或 3.11,不要用太老的版本。先建虚拟环境,再装依赖,这是最基础但最重要的习惯。
Django==4.2.7 djangorestframework==3.14.0 djangorestframework-simplejwt==5.3.0 django-filter==23.5 mysqlclient==2.2.0 redis==5.0.1 celery==5.3.4 gunicorn==21.2.0 whitenoise==6.5.0 flask==3.0.0 flask-socketio==5.3.4 eventlet==0.33.3 jieba==0.42.1本地开发时数据库我直接用了 SQLite,零配置能跑;上线前切换到 MySQL。切换的关键是注意 MySQL 的字符集,建库时一定要CREATE DATABASE shop DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;,否则中文写入会乱码。
还有一个容易踩的坑:Windows 上开发,Linux 上部署,requirements 里如果写死mysqlclient,在 Linux 编译时还需要系统依赖 libmysqlclient-dev。为了省事,有些人用 PyMySQL 替代,但要记得在 Django 的__init__.py里加一行:
import pymysql pymysql.install_as_MySQLdb()这种方法适合快速部署,但性能上还是建议生产直接用 mysqlclient。
4.2 Nginx + Gunicorn + Supervisor 实战配置
生产环境我推荐一套很稳的组合:Nginx 处理静态文件并反代请求,Gunicorn 跑 Django,Flask-SocketIO 跑独立端口,Supervisor 守护所有进程。上配置之前先把 Django 的静态文件收集一下:
python manage.py collectstatic --noinputGunicorn 配置我一般单独写一个文件,方便环境差异调整:
# deploy/gunicorn.conf.py bind = "127.0.0.1:8000" workers = 3 max_requests = 1000 timeout = 30Django 的 WSGI 启动命令:
cd /opt/shop source venv/bin/activate export DJANGO_SETTINGS_MODULE=shop.settings.production gunicorn shop.wsgi:application -c deploy/gunicorn.conf.pyFlask-SocketIO 不能直接交给普通 Gunicorn worker,它需要兼容异步的 worker。用 eventlet 就是最简单的方式:
cd /opt/shop/flask_push source ../venv/bin/activate gunicorn -k eventlet -w 1 app:app -b 127.0.0.1:5000Nginx 配置核心在于路径分发:
server { listen 80; server_name shop.example.com; location /static/ { alias /opt/shop/static/; } location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location /socket.io/ { proxy_pass http://127.0.0.1:5000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_read_timeout 60s; } }WebSocket 反向代理最容易出问题的就是Connection: upgrade头和proxy_read_timeout。少了升级头,Socket.IO 会反复回退到 polling 模式;超时太短,长连接会被 Nginx 掐断。这里我把挂起超时设成了 60 秒,线上如果心跳频率正常,基本不会有断连问题。
4.3 高频踩坑与解决实录
这个项目开发过程中我整理了一张高频问题清单,直接列出来,碰到类似问题不用从头猜:
| 问题现象 | 起因 | 解决办法 |
|---|---|---|
| 接口返回 403 CSRF Failed | AJAX 请求没带 CSRF token | 用 JWT 认证,关闭 Session 认证的 CSRF 校验 |
| 上传图片后刷新就 404 | FileField 存了绝对路径或目录冲突 | 只存相对路径,用ImageField(upload_to="ticket/"),按日期分目录 |
| MySQL 中文乱码 | 数据库默认字符集不是 utf8mb4 | 建库时指定 utf8mb4,并检查 Django DATABASES 设置 |
| WebSocket 一直断线 | Nginx 没升级协议或代理超时太短 | 参考上文 Nginx 配置,socket.io 注意开启心跳 |
| Celery 定时任务不执行 | 只启动了 worker 没启动 beat | 两个进程都要跑,celery -A shop worker -B |
| 迁移时外键删除报错 | 删除或修改外键字段时业务数据存在 | 先备份数据,用makemigrations --empty手写依赖规则,或重命名保留旧字段 |
| 后台列表页巨慢 | 关联查询 N+1 | 加list_select_related,并在查询集上 select_related/prefetch_related |
| 支付回调重复入账 | 支付平台重试机制 | 回调接口幂等,检测订单状态已经支付就直接返回成功 |
| Redis 连接数被打满 | 每次操作新建连接不回收 | 使用连接池或统一 Redis 客户端实例 |
这里挑一个展开讲:图片上传路径问题。开发时常见的写法是把图片保存到 MEDIA_ROOT 下的绝对路径,结果一部署到服务器,路径变了,图片全 404。正确做法是项目中只用相对路径,比如ticket/2024/06/example.jpg,访问由 Nginx 或 Django 的 MEDIA_URL 拼出来。不要把 Windows 的C:\...路径存进数据库,这是新手最容易踩的坑。
时区问题也很隐蔽。Django 的USE_TZ=True后,auto_now_add存的 UTC 时间,前端直接显示会比本地慢 8 小时。解决办法是接口返回前统一转成本地时区,或者前端拿到时间戳自己格式化。不要为了省事关掉 USE_TZ,否则夏令时和跨时区用户数据会乱。
5. 复盘心得与后续扩展建议
这个项目我前后迭代了两个版本,第一版所有功能都堆在 Django 里,后来拆分出 Flask 服务,整体结构才算顺了。复盘下来,最能提高开发效率的三个经验:第一,状态机一定要收敛在 service 层,禁止在视图里随手改状态字段;第二,售后核心表挂订单明细而不是订单主表,否则多商品订单的售后逻辑会重写一半;第三,Redis 在项目里不只是缓存,它还是异步消息和发布订阅的连接器,这比手动加队列简单一个数量级。
整个系统目前已经能支撑从用户下单到维修完成的全链路业务。如果后续继续迭代,我会优先加三个方向:一是小程序端,让用户通过微信提交售后工单和查看进度,核心 API 已经现成,只要加一套鉴权和对应前端即可;二是消息通知渠道,工单状态变化时除了 WebSocket 推送,还要叠加短信或微信模板消息,尤其要通知维修师傅接单;三是在智能匹配模块里把规则引擎换成向量检索,接一个大模型做售后问答,用户描述故障更口语化时也能准确分类。
最后一个实际体会是,项目做得再大,也不要离业务现场太远。我后来去模拟了几次工单流转,发现客服真正需要的是“少点几下”、师傅真正需要的是“少填几张表”。功能设计的取舍,往往就藏在这些真实操作细节里,这也是这个系统能不能从“能跑”变成“好用”的分水岭。
如果你正在做类似的家电商城或售后管理项目,建议先把我的数据模型和状态机抄下来跑一遍,再按自己的业务场景改字段。遇到问题时,对照上面的问题清单逐项排查,大多数卡点都能在半天内解决。