恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Django建材销售平台实战:三端角色权限与SKU订单设计全解析
首页
资讯中心
/
Django建材销售平台实战:三端角色权限与SKU订单设计全解析
Django建材销售平台实战:三端角色权限与SKU订单设计全解析
发布时间:2026/10/10 20:51:23
做建材类电商项目的时候很多人第一反应是“这不就是个多用户商城吗”。但真上手之后你会发现建材这个行业和卖衣服卖数码完全不一样SKU维度极其复杂角色之间又有很强的数据隔离需求。我最近刚好完整做了一个基于Django的建材销售平台角色分了用户、商家、管理员三端整个项目从建模到落地踩了不少坑这篇文章就把我的完整设计思路、代码实现和排错记录都梳理出来给准备做同类Django项目实战的新手一个可以直接参考的模板。1. 项目定位与三大角色模型拆解1.1 建材销售平台的核心业务闭环为什么值得动手做建材销售平台本质上是一个B2B2C的电商系统。C端用户装修业主、包工头、小装修公司要浏览和购买建材B端商家要上架商品、管理库存、处理订单平台管理员则负责审核商家资质、维护类目、处理纠纷。这三者形成一个完整的业务闭环商家发布商品 → 用户浏览下单 → 商家发货履约 → 用户收货确认 → 管理员全程监管。选择Django来做这个项目我觉得非常合适。建材的品类数据模型比普通电商复杂得多比如瓷砖会有规格、色号、等级、表面工艺等多个维度水泥有袋装和散装的区别计价单位也不统一。Django的ORM抽象能力强模型类可以直接把这些复杂属性映射成数据库字段配合Django Admin做后台管理开发效率非常高。而且平台天然就有“用户、商家、管理员”三种角色的权限控制需求Django自带的认证系统加上自定义用户模型很轻松就能撑起这套权限体系。这个项目适合谁来学习参考呢如果你是正在做Django项目实战的新手想找一个不流于表面的完整案例或者你本身在做建材、五金、家居相关行业的管理系统需要一套可复用的角色权限设计这篇文章都能给你实打实的参考。1.2 三种角色的权限边界与核心业务场景先拆解一下三种角色到底要干什么因为权限设计的前提是把业务场景想清楚否则后面写代码全是补丁。用户端C端消费者注册登录、浏览商品、按类目和规格筛选、商品搜索、加入购物车、提交订单、在线支付、查看订单状态、申请售后。用户权限的边界很清晰——只能操作自己的购物车、自己的订单、自己的收藏夹绝对不能看到商家的库存和成本信息。商家端B端供货商商家入驻申请、商品上下架、库存管理、改价、订单处理发货、退款处理、查看自己店铺的销售数据。商家的核心诉求是管好自己的“一亩三分地”所以商家端所有功能都要围绕“当前登录商家ID”做数据隔离不能让商家A看到商家B的商品和订单。管理员端平台运营商用入驻审核、用户管理、商品类目管理、平台公告发布、全量订单查看、数据统计报表。管理员拥有最高权限但不能越权操作具体业务比如管理员不该直接替商家改价而是通过平台规则去约束。这里特别提醒一下权限控制不能只靠前端隐藏按钮后端必须每一层都校验。我见过太多项目页面上的按钮是藏了但接口没拦住别人直接构造URL或者抓包就能越权操作。Django里做权限控制核心思路是“装饰器中间件模型查询过滤”三层防线后面我会详细写实现方案。2. 技术选型为什么是Django 周边生态2.1 Django版本、Python环境与数据库的务实选择技术选型这件事我的原则是“不追新只求稳”。做项目实战和做技术研究不一样生产环境里稳定压倒一切。Python版本建议直接用3.10或3.11这两个版本目前生态兼容性最好。Django版本我推荐4.2 LTS这是长期支持版本官方维护周期到2026年4月坑都已经被人踩平了各种第三方库的兼容性也最好。不建议一上来就用Django 5.x虽然新功能不少但部分第三方库还没完全跟上。数据库方面开发环境用SQLite完全够但考虑到建材平台数据量大了以后查询会变慢生产环境建议直接上MySQL 8.0或PostgreSQL 14。创建项目的时候用django-admin startproject这个命令创建应用用python manage.py startapp。这里有个经验之谈一个项目不要把所有业务逻辑都塞进一个app里。我见过不少新手建一个app叫app然后把用户、商品、订单全写在一个models.py里几千行代码挤在一起后期维护欲哭无泪。我的做法是拆分多个app每个app只负责一个业务模块。django-admin startproject building_mall cd building_mall python manage.py startapp users python manage.py startapp products python manage.py startapp orders python manage.py startapp merchants python manage.py startapp platform2.2 数据模型设计一张User表还是扩展Profile这是整个项目最关键的决策之一直接影响后面所有代码的写法。两种方案第一种是直接自定义用户模型继承AbstractUser加一个user_type字段区分角色第二种是用Django默认的User再建一个Profile表通过OneToOne关联把角色信息放在Profile里。两种方案都可以但我的建议是如果你还在项目设计阶段直接自定义用户模型最好。因为Django官方文档明确说了最佳实践是“从一开始就设置自定义用户模型”否则中途想切换会非常痛苦。自定义用户模型的迁移文件一旦生成后面再改涉及大量数据迁移操作很可能把数据库搞乱。from django.contrib.auth.models import AbstractUser from django.db import models class User(AbstractUser): ROLE_CHOICES ( (user, 普通用户), (merchant, 商家), (admin, 管理员), ) role models.CharField(max_length20, choicesROLE_CHOICES, defaultuser, verbose_name角色) phone models.CharField(max_length11, blankTrue, verbose_name手机号) class Meta: verbose_name 用户 verbose_name_plural verbose_name同时在settings.py里配置AUTH_USER_MODEL这一点特别容易漏。AUTH_USER_MODEL users.User角色字段放在User上登录的时候一次查询就能拿到角色信息不用再额外关联Profile表查一次减少一次数据库查询性能更好。当然如果项目已经开始写了、模型已经迁移过了那就不建议强行改老老实实建Profile表关联会稳妥很多。2.3 后台管理现代化改造django-unfold值得一试Django自带的Admin后台功能强大但界面老气建材平台的运营人员经常要维护类目、审核商品天天对着原生Admin界面确实难受。这里我推荐一个第三方库django-unfold它能把Django Admin改造成现代化的UI界面侧边栏、主题色、卡片式布局都有非常像现在主流SaaS后端的风格。pip install django-unfoldINSTALLED_APPS [ unfold, # 一定要放在django.contrib.admin之前 django.contrib.admin, ... ]它兼容Django 4.2和5.0安装后基本不需要改代码继承Admin类的地方也都能正常用。这个小改动能让整个系统的专业感提升不少商家入驻审核页面和商品管理页面用起来舒服得多。我的体验是django-unfold最大的价值是让运营团队的好感度直线上升因为后台UI舒服了运营录入数据的效率和正确率也跟着上来了。3. 从零搭建项目初始化与核心功能落地3.1 用户模型扩展与登录注册的完整实现用户系统是整个平台的地基先从它开始写。自定义User模型建好之后要处理的就是注册和登录逻辑。注册的时候要区分角色普通用户和商家走不同的注册入口平台上商家要填写店铺名称和联系方式提交入驻申请后要等管理员审核通过才能变成有效商家。注册接口的核心逻辑很简单但要注意密码加密。Django默认的set_password方法会自动加盐并哈希千万不要自己存储明文密码。这里有一个踩过的坑新手容易图省事直接user.password request.POST[password]存明文这是致命操作一旦数据库泄露用户的密码就全裸奔了。from django.contrib.auth import login, authenticate from django.shortcuts import render, redirect from .forms import UserRegisterForm, MerchantRegisterForm def register_user(request): if request.method POST: form UserRegisterForm(request.POST) if form.is_valid(): user form.save(commitFalse) user.role user user.set_password(form.cleaned_data[password]) user.save() login(request, user) return redirect(home) else: form UserRegisterForm() return render(request, users/register_user.html, {form: form})登录的逻辑更简单authenticate传入用户名和密码验证通过后login写入会话。登录成功后注意按角色分发到不同的首页普通用户去商城首页商家去商家工作台管理员去平台后台。这里我用一个装饰器来封装角色判断后面每个视图都能复用。from django.core.exceptions import PermissionDenied def role_required(role): def decorator(view_func): def wrapper(request, *args, **kwargs): if not request.user.is_authenticated: return redirect(login) if request.user.role ! role: raise PermissionDenied return view_func(request, *args, **kwargs) return wrapper return decorator用法也很直观比如商家用户中心的视图就这样标注role_required(merchant) def merchant_dashboard(request): ...装饰器写好后每个业务视图只要加上一行就能控制访问角色比到处写if判断要清爽得多。但这里有个注意点因为User继承的是AbstractUserDjango默认的登录视图返回的user对象会自动带上自定义的role字段所以装饰器里直接取request.user.role是没问题的。3.2 商品与SKU建模建材行业特殊字段怎么设计建材商品的建模是整个项目的重头戏也是最容易暴露行业认知短板的地方。我一开始直接把所有字段堆在商品表里后来发现一个致命问题同一种瓷砖可能会有多个规格、多个色号每种规格和色号对应不同的库存和价格。如果把这些都塞在商品表里一条商品记录根本存不下实际情况。所以商品建模必须拆成两层商品表Product和SKU表ProductSKU。商品表存的是通用信息品牌、类目、标题、主图、描述SKU表存的是具体售卖规格每个SKU对应一个价格和一个库存数量。class Product(models.Model): name models.CharField(max_length200, verbose_name商品名称) category models.ForeignKey(products.Category, on_deletemodels.PROTECT, verbose_name类目) brand models.CharField(max_length100, verbose_name品牌) material models.CharField(max_length100, verbose_name材质, blankTrue) description models.TextField(verbose_name商品描述, blankTrue) cover_image models.ImageField(upload_toproducts/cover/, verbose_name主图) merchant models.ForeignKey(settings.AUTH_USER_MODEL, on_deletemodels.CASCADE, verbose_name所属商家) is_active models.BooleanField(defaultTrue, verbose_name是否上架) created_at models.DateTimeField(auto_now_addTrue) class ProductSKU(models.Model): product models.ForeignKey(Product, on_deletemodels.CASCADE, related_nameskus, verbose_name所属商品) spec_value models.CharField(max_length100, verbose_name规格值) # 比如 800x800mm 哑光灰 color models.CharField(max_length50, verbose_name色号, blankTrue) grade models.CharField(max_length20, verbose_name等级, choices((a,优等品),(b,一等品),(c,合格品)), blankTrue) unit models.CharField(max_length20, verbose_name单位, default平方米) price models.DecimalField(max_digits10, decimal_places2, verbose_name销售单价) stock models.IntegerField(default0, verbose_name库存数量)这里有几个建材行业的特殊点分享给大家单位不统一的问题。瓷砖按平方米、水泥按袋/吨、板材按张、电线按卷。所以unit字段不能省下单和展示时必须明确标注计价单位否则用户下单时看到的价格单位和自己预期的不一致售后纠纷能让你崩溃。等级的概念。瓷砖同一型号会有优等品和一等品的价格差异石膏板也分不同等级。这个在普通电商里不太会遇到但在建材里是常规操作。价格波动快。建材的原材料价格受市场行情影响明显商家需要经常调价。所以价格放在SKU表而不是商品表方便特定规格单独调价。同时建议加一个价格变动日志表记录每一次调价历史方便财务对账。3.3 订单流转与库存扣减一个事务解决并发问题订单模块是整个平台业务逻辑最密集的地方主要涉及订单状态管理和库存扣减。订单我拆成订单主表和订单明细表主表记录收货人信息、总金额、状态等明细表记录商品SKU、购买数量、单价。订单状态用一个choices字段维护可以定义如下状态序列待支付 → 已支付/备货中 → 已发货 → 已签收 → 已完成以及异常状态已取消、售后中、退款成功。状态流不能跳变比如待支付状态下不能直接变成已发货这些业务规则可以在模型中定义方法避免散落在各个视图里。库存扣减是电商平台最经典的问题——并发。两个用户同时买同一件商品库存只剩一件如果代码先读库存再更新库存就会发生超卖库存变成负数。Django里解决这个问题有两种常用方案。第一种方案是使用select_for_update在事务里对SKU记录加行锁。from django.db import transaction with transaction.atomic(): sku ProductSKU.objects.select_for_update().get(idsku_id) if sku.stock quantity: raise ValueError(库存不足) sku.stock - quantity sku.save() # 创建订单明细等操作select_for_update的作用是锁定该行记录其他事务要操作这行时就必须等待当前事务提交这样并发请求就能串行化不会出现超卖。第二种方案是用F表达式直接在数据库层面原子操作from django.db.models import F ProductSKU.objects.filter(idsku_id, stock__gtequantity).update(stockF(stock) - quantity)filter里的stock__gte保证库存足够才更新update返回受影响的行数如果返回值是0说明库存不足。这个方法更轻量性能也比加行锁好适合高并发的场景。但有些场景还是需要先查后写的比如下单时要同时校验商品是否上架在售所以我会把F表达式和事务结合使用既保证一致性又减少锁等待。还有一个业务细节需要决定库存扣减的时机。我采用的是“下单预占支付确认”的策略。用户提交订单后先把库存锁定扣减库存如果用户没有在15分钟内支付订单自动取消并释放库存。这样做的好处是用户下单后商品不会被别人买走体验更好。坏处是预占库存的时间内其他用户看到的库存是减少的有可能影响销售。权衡之下建材这种低频高客单价的商品预占策略更合适。3.4 ORM高频操作实战查询、更新、删除对象Django的ORM用好了能省很多事但用不好经常踩坑。这里我把项目中最高频的几种操作列一遍。查询是重头戏。建材商品的类目很多用户经常要用多个条件组合筛选按类目、按品牌、按材质、按价格区间。组合查询的核心是Q对象和filter的动态拼接。比如前端传多个筛选条件后端用字典动态构造filter参数from django.db.models import Q filters {is_active: True} if category_id: filters[category_id] category_id if brand: filters[brand] brand if min_price: filters[skus__price__gte] min_price if max_price: filters[skus__price__lte] max_price products Product.objects.filter(**filters).distinct()注意这里用了skus__price这种跨表查询因为价格在SKU表商品和SKU是一对多的关系所以要用双下划线跨关系过滤加distinct()是为了去重否则一个商品有多个SKU时会返回多条重复记录。聚合统计也经常用。比如商家工作台要看到“我的商品数量”“本月订单总金额”用annotate比遍历Python再统计效率高得多from django.db.models import Sum, Count merchant_orders Order.objects.filter(merchantrequest.user, created_at__month8) total_amount merchant_orders.aggregate(totalSum(total_amount)) total_count merchant_orders.aggregate(countCount(id))关于关联查询性能有一个必须提到的点select_related和prefetch_related。查询订单列表时如果订单关联了用户和SKU用select_related能把外键关联的表一次性join查出来避免在循环里逐条查询外键数据。这就是著名的N1问题列表有50条订单如果每个订单都要查一次用户表就会多出50条SQL用select_related之后只有1条SQL。至于删除对象Django的delete()方法有一个很容易被人忽略的特点它是级联的。删除一个商品它关联的所有SKU会被一并删除删除一个用户他的所有订单也会被删光。这有时候很危险。在销售平台里商品和订单都是重要的业务数据物理删除意味着数据彻底没了。我的处理思路是业务数据一律软删除用is_activeFalse代替delete()。# 下架商品软删除而不是物理删除 product.is_active False product.save()# 如果要物理删除先确认没有订单关联且做好级联评估 order_count OrderItem.objects.filter(sku__productproduct).count() if order_count 0: raise ValueError(该商品存在历史订单禁止删除) product.delete()Django的delete()还会返回一个元组第一个元素是删掉的对象总数第二个元素是每个模型删除数量的字典如果调试的时候想确认到底删了什么可以print出来看看我调级联删除问题时就靠这个定位到某个外键忘了加on_delete。3.5 文件上传与图片资源管理的配置建材商品离不开图片用户的购买决策很大程度依赖于商品实拍图。Django的文件上传配置不算复杂但很容易在部署阶段卡住。核心配置有三处MEDIA_URL、MEDIA_ROOT以及开发环境下的媒体路由。# settings.py MEDIA_URL /media/ MEDIA_ROOT BASE_DIR / media# urls.py开发环境专用 from django.conf import settings from django.conf.urls.static import static urlpatterns static(settings.MEDIA_URL, document_rootsettings.MEDIA_ROOT)开发环境里这行代码能让你直接通过/media/访问上传的图片非常方便调试。但要注意生产环境绝对不能这么配因为Django自带开发服务器不适合承载静态文件图片由Nginx直接处理配置会在server块里加一个location /media/指向MEDIA_ROOT目录。还有一个容易忽略的点用户上传的文件名可能包含中文或特殊字符在上传到服务器后容易引起编码问题。我习惯了在上传时统一重命名文件比如用时间戳加随机串来命名import os, uuid from django.utils import timezone def rename_upload(instance, filename): ext os.path.splitext(filename)[1].lower() new_name f{timezone.now().strftime(%Y%m%d%H%M%S)}_{uuid.uuid4().hex}{ext} return os.path.join(products, instance.product.merchant.username, new_name)这样处理之后文件名永远是纯ASCII的配合CDN做分发也不会有问题我建议所有上传字段都用这个逻辑。4. 常见问题与排查实录4.1 权限校验失效后台接口被未授权访问这是我遇到的第一个严重问题。项目初版做完后我用一个普通用户账号测试发现竟然能直接通过URL访问商家的订单管理页面。原因很简单我这个视图只在前端判断了按钮显隐后端接口探到URL就直接返回数据了根本没做角色校验。排查过程用浏览器开发者工具抓包伪造一个商家工作台的URL发现返回了200状态码和数据。修复方案就是上面说的role_required装饰器并且给所有需要权限的视图统一加上。这里给新手一个可复用的检查方法把所有URL路径整理成一张表逐个标注需要什么角色能访问然后用不同角色账号逐一实测。权限表用Excel或者Markdown记录都可以关键是覆盖所有视图别漏。4.2 图片上传后无法显示这个问题几乎人人都遇到过。开发环境下图片显示正常部署到服务器后图片链接404。原因可能有三个MEDIA_ROOT路径不对、MEDIA_URL没有匹配到静态文件服务、Nginx没有配置media目录映射。我的排查顺序是先检查服务器上文件是否真的上传成功去MEDIA_ROOT对应目录看文件在不在再检查Nginx配置看location /media是否指向了正确的目录最后看Django有没有在DEBUGFalse时配置静态文件处理。这里有个坑很多教程里的static函数只在DEBUGTrue时生效生产环境必须靠Nginx或S3这类独立存储服务这个区分得非常清楚不然部署就踩雷。4.3 N1查询导致接口慢得离谱项目经理反馈商品列表页打开要三秒多。我用Django Debug Toolbar看一下SQL查询次数商品列表30条记录总共执行了30多次额外SQL每个商品都查了一次所属商家的店铺名。这就是典型的N1查询问题。修复方案是对商品外键字段加上select_relatedproducts Product.objects.select_related(merchant).filter(is_activeTrue)对于商家和商品这类一对一/多对一关系select_related用SQL的JOIN一次性查出来对于多对多关系或者反向外键要使用prefetch_related它会先查主表再查关联表在Python内存里做关联避免JOIN造成结果集膨胀。这两个方法用对了查询效率可以有数量级的提升。4.4 delete()级联删除的惨痛教训有一次我在测试环境模拟下架滞销商品手滑用了product.delete()而不是软删除。当时还觉得测试环境无所谓结果发现这个商品关联的所有SKU、订单明细都被级联删除了而订单明细被删除后平台的销售报表数字全部不正常。最后只能从备份里恢复数据折腾了大半天。这次的教训我总结成两条铁律物理删除前必须检查所有外键关联。Django模型里每个ForeignKey的on_delete参数都要想清楚CASCADE虽然有但并不适合业务数据。订单、订单明细这类涉及交易的数据用PROTECT保护起来有引用就禁止删除。业务数据一定要做软删除。重要数据永远不物理删除用status字段的状态位控制可见性查询时统一过滤is_active。这样就算误操作也有后悔药吃。4.5 库存超卖问题并发场景下的数据一致性平台上线第三天就遇到问题某个瓷砖SKU只剩5件库存但后台显示下单成功了7单。这就是并发超卖。初版代码是“先查库存再扣减”的逻辑sku ProductSKU.objects.get(idsku_id) if sku.stock quantity: sku.stock - quantity sku.save()两个人同时请求都读到库存5都满足1的条件都执行了扣减最后库存变成4但事实上卖出了2件超卖了1件。修复方法前面已经写了要么用F表达式原子更新要么用select_for_update加行锁。我最终的方案是select_for_update配合事务因为库存扣减的同时还要创建订单明细、更新状态多个写操作需要放在同一个事务里保证原子性。5. 一些开发过程中的个人心得我前后做了两个版本的建材销售平台第一版完全按普通电商的思路来设计结果在商品建模和业务逻辑上被运营吐槽得体无完肤。第二版深入到建材行业的细节里从SKU拆分到计价单位再到等级划分每一条都是根据实际业务反推出来的。如果你准备用Django做这种多角色平台项目我强烈建议动手前先把业务角色画清楚。用户、商家、管理员三者在数据上的边界在项目开始时就通过模型的ForeignKey和查询条件定义好后期才不会越改越乱。另外如果打算用AI辅助编码来快速搭一个Django项目我的经验是AI生成代码速度快但ORM行为和权限校验逻辑不能全信特别是外键的on_delete参数和查询性能这部分关键代码还是要自己把逻辑捋顺了再放进去。最后再分享一个调试小妙招。Django项目调试时配置好Django Debug Toolbar每写一个列表页或者详情页都要看一眼SQL查询次数和页面响应时间。这条习惯能帮你提前发现绝大部分性能雷区比上线后再排查靠谱得多。这个项目做完之后我对Django的模型设计自信了不少下一次再遇到类似的B2B2C平台需求从需求到第一版上线节奏会快很多。