基于Django的插画鲜花商城系统:从商品建模到并发防超卖的实现解析

发布时间:2026/10/14 22:24:09
基于Django的插画鲜花商城系统:从商品建模到并发防超卖的实现解析 每年到了毕业设计季总有人跑来问我Django 到底选什么题目才能既不做烂大街的图书管理、新闻发布又能在答辩现场把话说清楚我的答案里插画鲜花商城系统出现频率很高。这个题目听起来文艺内核却是一条完整的电商交易链路商品管理、购物车、订单、支付、库存、后台运营一个都跑不掉而且插画和鲜花这两种品类天然逼着你去处理图片展示、规格组合、虚拟商品与实物商品并存的逻辑。今天这篇就把整套设计与实现过程拆开讲一遍从选题思路到数据建模从状态机设计到并发防超卖最后再说说远程调试和交付讲解那些没人写进文档的坑。准备用 Django 做商城类项目的同学不管是毕设还是个人练手都可以直接参考。1. 为什么插画鲜花商城是有得讲的毕设选题需求背后的真实业务逻辑1.1 插画和鲜花的组合逼着你解决电商系统的两类典型难点很多人选电商系统做毕设都是照着XX商城抄一个商品表、订单表、购物车表堆在一起做的其实是 CRUD 的排列组合。这种项目答辩时最容易被问倒因为老师一眼就能看出你没有思考过业务本身。插画鲜花商城的聪明之处在于它把两种完全不同的商品形态放在同一个平台里。插画是数字内容商品可以卖原图文件、卖印刷画芯、卖商业授权鲜花是典型的易损耗实物商品涉及到花材搭配、配送时效、温区保存。一个平台要同时处理这两种形态系统设计上就不得不考虑下面这些问题同一套商品数据结构怎样既能描述一张插画的授权方式又能描述一束花的规格组合数字商品不需要物流实物商品需要物流订单履约流程怎么做区分插画的展示极度依赖大图、细节图鲜花的展示又依赖场景图、包装图图片资源怎么统一管理这些问题恰恰是真实电商系统每天都在处理的。你只要在文档里把这些问题写清楚答辩时就不是背代码而是讲业务决策。老师听到的是你理解这个系统在解决什么问题而不是你会用 Django 建表。1.2 功能清单的取舍从应付答辩到闭环可运营的四个层次我跟准备做毕设的同学沟通时习惯把电商系统的功能分成四个层次。这个分层方式同样适用于你评估自己的项目范围层次功能范围工作量建议基础层用户注册登录、商品展示、详情页、个人中心小必须做少了不像商城交易层购物车、订单确认、地址管理、库存扣减、支付对接中核心答辩主要围绕这里展开营销层优惠券、满减活动、积分、收藏、商品评价中选 1-2 个做增加系统丰满度运营层后台管理、数据看板、权限控制、订单处理中后台必须做看板可选对这个题目我的建议是基础层和交易层做完整营销层挑优惠券或者收藏来做运营层重点做后台订单处理和权限控制。别想着把优惠券、积分、秒杀、直播全部塞进去功能越多意味着状态流转越复杂文档越难写答辩时暴露的问题也越多。插画鲜花商城这个组合天然具备展示优势。商品模块要把插画的色彩细节、鲜花的精美包装展示得足够好看这样项目截图放进论文里很占优势交易模块又因为虚拟商品和实物商品并存状态流转比普通商城更复杂讲解时可以展示的设计点非常多。这也是我推荐这个题目的核心原因既有视觉内容可以撑门面又有业务逻辑可以做深度。2. 商品体系建模用一套模型装下数字插画和实物鲜花2.1 SPU 与 SKU 的关系梳理电商系统里最基础的建模概念就是 SPU 和 SKU。SPU 是一个抽象商品比如向日葵主题插画装饰画或者粉色系混合花束SKU 是具体可下单的规格项比如向日葵主题插画 A3 尺寸 实木装裱版或者粉色系混合花束 中号 含贺卡。系统里用户最终买的一定是 SKU而不是 SPU。很多同学的第一个错误就是只建一张商品表把规格直接写成字符串存进去。这样做订单模块会非常痛苦因为订单明细根本不知道该关联哪个库存、哪个价格。正确的做法是拆成 SPU 表和 SKU 表一对多关联。class Spu(models.Model): CATEGORY_CHOICES [ (illustration, 插画), (flower, 鲜花), ] name models.CharField(商品名称, max_length128) category models.CharField(商品分类, max_length32, choicesCATEGORY_CHOICES) main_image models.ImageField(主图, upload_tospu/main/%Y/%m/) detail_html models.TextField(详情内容, blankTrue) status models.CharField(上下架状态, max_length16, defaulton) sales_count models.IntegerField(销量, default0) created_at models.DateTimeField(auto_now_addTrue) class Sku(models.Model): spu models.ForeignKey(Spu, on_deletemodels.CASCADE, related_nameskus, verbose_name所属商品) name models.CharField(SKU名称, max_length128) price models.DecimalField(售价, max_digits10, decimal_places2) stock models.IntegerField(库存, default0) sku_code models.CharField(SKU编码, max_length64, uniqueTrue) image models.ImageField(规格图, upload_tosku/%Y/%m/, blankTrue)这里有几个细节值得注意。第一SKU 编码一定要独立生成不要直接用自增主键因为你后续要用它做订单快照、对账、出库编码里最好包含商品分类和规格序号第二库存字段放在 SKU 上因为真正被购买的是具体规格你不能说花束商品库存 100 个而是中号花束库存 50、大号花束库存 50第三价格用 DecimalField 而不是 FloatFieldfloat 的精度问题在涉及金额计算时会被无限放大别给自己挖坑。2.2 插画商品的虚拟属性与鲜花商品的时效属性SPU 和 SKU 只解决了基础结构但插画和鲜花各自有专属字段需求。插画需要区分授权类型、提供下载链接、标注源文件格式鲜花需要标注保鲜期、配送温区、是否支持当日送达。这些字段差异直接平铺在同一张表里会变得很乱。我的处理方案是在 SKU 表上增加一个商品类型扩展字段用 Django 的 JSONField 存储非结构化扩展信息。这样既避免建一堆一对一扩展表又能保持模型足够灵活class Sku(models.Model): # ... 基础字段省略 extra_info models.JSONField(扩展属性, defaultdict, blankTrue)插画 SKU 的扩展信息长这样{ author: 某插画师, resolution: 300dpi, download_url: /media/files/illustration-20240601.zip, license_type: 个人非商用, format: JPGPNG }鲜花 SKU 的扩展信息则可能长这样{ storage_temp: 2-8℃, shelf_life: 3天, delivery_type: 冷链配送, need_card: true, flower_material: [向日葵, 尤加利叶, 雏菊] }用 JSONField 不是偷懒而是这类垂直属性本身不具备全局查询价值只有展示和详情页会用。如果你将来某个属性真的需要参与筛选再把它提升成独立字段就好了。这个建模思路可以写进文档的数据库设计章节作为你对复杂业务场景的抽象方案答辩时很加分。2.3 图片处理Pillow 裁剪、多尺寸缩略图与懒加载插画鲜花商城的图片量不会小。每一件插画作品都需要主图、细节图、场景图每一款花束也需要不同角度和尺寸的展示图。如果你直接把上传的原图全部塞给前端页面加载会慢得让人崩溃。我在项目里常用的做法是上传原图后服务端自动生成三套规格的缩略图分别用于列表页、详情页轮播和购物车小图。Django 的 ImageField 本身只负责存储路径真正的裁剪可以用 Pillow 库完成。我没有用第三方 app而是写了一个小工具函数在上传时统一调用from PIL import Image from django.conf import settings import os THUMB_SIZES { list: (400, 400), detail: (800, 800), cart: (120, 120), } def generate_thumbs(image_field, base_name): base_path image_field.path base_dir os.path.dirname(base_path) base_url image_field.url for tag, size in THUMB_SIZES.items(): try: im Image.open(base_path) im im.resize(size, Image.LANCZOS) thumb_path os.path.join(base_dir, f{base_name}_{tag}.jpg) im.convert(RGB).save(thumb_path, JPEG, quality85) except Exception: continue return { f{tag}_url: f{base_url}_{tag}.jpg for tag in THUMB_SIZES }前端页面里列表页使用list_url详情页使用detail_url同时用loadinglazy属性开启图片懒加载。提示不要忽略图片体积。同一张图在列表页显示 200 像素宽和在详情页显示 800 像素宽加载成本完全不同。多尺寸缩略图是体验问题也是答辩时可以展开讲的性能优化点。3. 购物车与订单状态机从加入购物车到支付成功的状态流转3.1 购物车落库还是落 Session毕设场景的最佳取舍Django 里实现购物车主要有三种方案各有优劣我直接整理成表格供你对比方案优点缺点适用场景数据库购物车数据持久化跨设备同步后台可查每次增删改都写库压力稍大几乎所有正式商城推荐Session 购物车实现简单无需建表读写在客户端换设备丢失后台不可查数据量大时 Cookie 膨胀匿名用户临时购物Redis 购物车读写快支持过期时间需要额外维护 Redis讲解成本高高并发场景我给绝大多数做毕设的同学推荐数据库购物车原因很实在实现逻辑直观每个表和每一步操作都能在答辩 PPT 里讲清楚后台管理可以直接看到用户的购物车数据方便演示而且不依赖 Redis 这类额外组件部署到新机器时少一个故障点。数据库购物车表建议这样设计class CartItem(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE, related_namecart_items) sku models.ForeignKey(Sku, on_deletemodels.CASCADE, related_namecart_items) quantity models.PositiveIntegerField(数量, default1) checked models.BooleanField(是否选中, defaultTrue) created_at models.DateTimeField(auto_now_addTrue) updated_at models.DateTimeField(auto_nowTrue) class Meta: unique_together (user, sku)注意unique_together这个约束同一个用户往购物车放同一个 SKU 时不要生成多条记录而是数量叠加。这也是很多人容易忽略的业务细节——重复加购会导致订单明细出现两行一模一样的数据对库存扣减和结算展示都不友好。3.2 订单状态机设计状态枚举与合法流转订单是电商系统里最核心的实体而设计订单的关键不是建表而是状态流转。我见过太多项目只有一个status字段代码里到处散落着if status 1这样的魔法数字改需求的时候无从下手。更好的做法是先定义状态枚举再定义合法流转路径from enum import Enum class OrderStatus(Enum): PENDING_PAYMENT pending_payment # 待付款 PAID paid # 已付款待处理 PACKAGING packaging # 备货中 SHIPPED shipped # 已发货 COMPLETED completed # 已完成 CANCELLED cancelled # 已取消 REFUNDING refunding # 退款中 REFUNDED refunded # 已退款 ALLOWED_TRANSITIONS { OrderStatus.PENDING_PAYMENT: {OrderStatus.PAID, OrderStatus.CANCELLED}, OrderStatus.PAID: {OrderStatus.PACKAGING, OrderStatus.REFUNDING}, OrderStatus.PACKAGING: {OrderStatus.SHIPPED, OrderStatus.REFUNDING}, OrderStatus.SHIPPED: {OrderStatus.COMPLETED, OrderStatus.REFUNDING}, OrderStatus.COMPLETED: set(), OrderStatus.CANCELLED: set(), OrderStatus.REFUNDING: {OrderStatus.REFUNDED}, OrderStatus.REFUNDED: set(), }每次变更状态时做一个校验def change_order_status(order, new_status): if new_status not in ALLOWED_TRANSITIONS.get(order.status, set()): raise ValueError(f非法状态流转: {order.status} - {new_status}) order.status new_status order.save(update_fields[status, updated_at])这样做最大的好处是非法流转在代码层面就被拦截而不是等脏数据出现了再去修补。你写论文时也可以画一张状态转换图把待付款→已付款→备货中→已发货→已完成这条主线以及取消、退款分支展示出来评审老师看你的思路会非常清晰。3.3 防重复提交订单号幂等与支付状态校验电商项目里有一个常见的逻辑漏洞用户连续点击提交订单按钮导致系统生成多笔相同内容的订单。解决这个问题的思路叫幂等也就是同一次请求无论执行多少次结果都一样。我在项目里做两道锁。第一道是前端按钮防重复提交提交后立即置灰同时显示订单创建中第二道是后端生成一个幂等键用户在订单确认页提交时携带后端下发的checkout_token创建订单前先校验这个 token 是否已经被使用过。class CheckoutToken(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE) token models.UUIDField(defaultuuid.uuid4, uniqueTrue) used models.BooleanField(defaultFalse) created_at models.DateTimeField(auto_now_addTrue) expires_at models.DateTimeField()用户点击提交后开启一个事务先查询token.used如果已经是True就直接返回上一次创建的订单否则把used置为True再继续创建订单。注意这里要把查询和更新放在同一个事务里并配合数据库行锁否则高并发下还是可能重复使用同一个 token。支付状态的校验也容易被忽略。用户在支付页和支付回调之间偶尔会因为网络原因回调滞后而重复发起支付请求。处理办法是订单状态只有PENDING_PAYMENT时才允许发起支付一旦订单状态变成PAID或其他终态直接拒绝新的支付请求。这笔逻辑虽然简单却是保证订单和资金一致性的底线。4. 支付对接与库存并发最容易翻车的两个环节4.1 支付方案的选择沙箱、模拟支付还是直接跳过很多商城类毕设最容易卡壳的地方就是支付。真实商户号需要营业执照个人根本申请不到而网上随便找的第三方支付接口又可能涉及安全风险。针对毕设场景我的建议顺序是如果学校提供的演示环境允许优先对接支付平台的沙箱环境这样支付流程最真实回调验签也走的是真链路如果沙箱申请下来比较麻烦就自己实现一个模拟支付接口在前端拉起一个模拟收银台输入任意信息后调用后端接口完成支付同时模拟异步回调绝对不要在项目里硬编码一个点击按钮直接把订单改成已支付的裸逻辑没有任何回调、没有任何校验这个在答辩时属于致命伤。模拟支付的核心是模拟出异步回调这个过程。我当时的做法是用户点击模拟支付后后端先创建一个支付单然后启动一个子线程延迟两秒后自动回调本地支付接口把订单状态改成已付款。这样既还原了真实支付先跳转、再回调的时序又不需要外部依赖。4.2 支付回调的验签与幂等处理无论选哪种支付方案回调处理逻辑是一致的。支付回调是一个典型的高要求接口它要求你做到验签、幂等、事务三件事同时成立。验签的逻辑不复杂支付平台异步通知你的服务器时会带上一组签名参数。你需要用约定的密钥把所有参数拼接后做签名验证比对一致才继续处理否则直接忽略。签名的目的就是防止有人伪造支付成功通知来刷数据。def verify_sign(params: dict, secret: str) - bool: sorted_params sorted(params.items()) raw .join(f{k}{v} for k, v in sorted_params if v and k ! sign) sign hashlib.md5((raw secret).encode(utf-8)).hexdigest() return sign params.get(sign)幂等处理则是回调进来后先根据支付单编号查数据库如果这个支付单已经处理过就直接返回成功。这一步非常关键因为支付平台的回调网络不稳定时会重复发送如果你的回调接口不是幂等的就可能出现订单金额被重复入账的情况。最后是事务。整个回调处理流程——修改支付单状态、修改订单状态、扣减库存、增加销量——应该放在同一个数据库事务里任何一个环节失败全部回滚避免出现钱付了但订单状态没更新这种极难排查的脏数据问题。4.3 库存扣减的并发控制与超卖处理库存超卖是所有电商项目都会遇到的问题。用户在秒杀或者促销场景下两个人同时下单而库存只剩最后 1 件如果不加控制两个订单都会扣减成功出现超卖。最简单的有效方案是用一条带条件的 SQL 语句完成扣减from django.db import transaction transaction.atomic def deduct_stock(sku_id, quantity): sku Sku.objects.select_for_update().get(idsku_id) if sku.stock quantity: raise ValueError(库存不足) sku.stock - quantity sku.save(update_fields[stock])select_for_update()会对这一行记录加锁直到事务结束才释放。这样即使两个请求同时到达第二个请求也必须等第一个事务提交后才能读库存这时库存已经变少超卖自然不会发生。我用一个简单的并发脚本实测过初始库存 100200 个并发请求同时下单最终成功创建的订单数就是 100 单剩余库存为 0全程没有出现负数库存或重复扣减。注意select_for_update()必须在事务中才有意义。很多同学在视图里直接调用这个 QuerySet但外层没有包裹transaction.atomic()导致锁在事务结束前就被释放问题依旧存在。5. 管理后台的二次开发把 Django Admin 变成运营后台5.1 Admin 定制列表页、编辑页、行内表单Django Admin 是所有项目里投入产出比最高的模块。默认情况下你把模型注册进去就有了一个能用的后台。但如果不做任何定制后台界面会比较原始和商城管理系统的定位差距偏大。我建议至少做以下三类定制。第一列表页的list_display要展示最有信息量的字段比如商品名称、分类、状态、价格、库存、销量而不是默认的 ID第二给状态字段加list_filter让运营人员可以一键筛选已上架/已下架或者待付款/已发货订单第三给 SKU 使用TabularInline让管理员在编辑 SPU 时直接在同一页面维护多个规格体验会提升很大。class SkuInline(admin.TabularInline): model Sku extra 0 fields [name, price, stock, sku_code, status] admin.register(Spu) class SpuAdmin(admin.ModelAdmin): list_display (name, category, status, sales_count, created_at) list_filter (category, status) search_fields (name,) inlines [SkuInline]5.2 权限与角色运营、客服、管理员的分层后台有了权限设计要跟上。Django 自带的 Permission 机制已经足够用关键是把它用好。我建议在后台字段级做控制普通运营人员只能管理商品的上架下架和价格修改客服人员可以查看订单、处理退款但不能改商品超级管理员才拥有全部权限包括用户管理、权限分配和数据导出。实现方式不复杂创建三个用户组分别分配不同权限然后给每个后台视图加上admin.site.admin_view的访问控制。对于自定义的操作比如批量发货或者审核退款需要在方法里显式校验权限class OrderAdmin(admin.ModelAdmin): actions [mark_as_shipped, approve_refund] def approve_refund(self, request, queryset): if not request.user.has_perm(orders.can_approve_refund): self.message_user(request, 没有退款审核权限, levelerror) return # 后续业务处理...权限这一块写到论文里可以很好地体现你对数据安全和职责分离的理解是很多同学会忽略的加分点。5.3 数据看板销售概况、热销商品、低库存预警运营后台如果只有增删改查终究差了点管理系统的味道。我建议给后台首页加一个简单的数据看板不需要复杂的图表库直接用 Django ORM 聚合就能实现。展示四个核心指标今日销售额、今日订单量、总会员数、热销商品 Top5。from django.db.models import Sum, Count today timezone.now().date() daily_sales Order.objects.filter( status__in[paid, packaging, shipped, completed], created_at__datetoday ).aggregate(totalSum(total_amount))[total] or 0 hot_items OrderItem.objects.filter( order__status__in[paid, packaging, shipped, completed] ).values(sku__name).annotate(totalSum(quantity)).order_by(-total)[:5]低库存预警也可以做筛选所有stock threshold的 SKU在后台页面顶部展示一条红色提示条。这个功能逻辑简单但演示效果很好能够直观说明你的系统具备自动化运营提醒能力。6. 远程调试、文档编写与讲解演示交付阶段的实战经验6.1 环境一致性虚拟环境、依赖锁定与数据库迁移项目写得再好交付时跑不起来等于零。我接手过不少远程调试的需求十次里有七八次都是卡在环境问题上。最常见的三个坑Python 版本不一致导致依赖包冲突、依赖没有锁定版本导致升级后行为差异、数据库没有做迁移导致表结构缺失。正确做法是在项目根目录下维护一个requirements.txt并且所有依赖都锁定精确版本。pip freeze requirements.txt如果你用的是 virtualenv 或 venv还可以把 Python 版本写入项目文档注明本项目使用 Python 3.10 开发请勿使用 3.12 以下版本运行。数据库方面不要用python manage.py migrate --run-syncdb碰运气而是老老实实准备初始迁移文件python manage.py makemigrations python manage.py migrate如果你提供的是演示环境还可以准备一份seed_data.json用loaddata命令导入模板数据确保对方一打开就能看到漂亮的商品示例而不是空荡荡的页面。6.2 远程调试的常见问题与处理流程远程调试最怕的是对方报错然后你全靠猜。我的习惯是让对方先提供三样东西完整报错截图、Python 和 Django 版本、运行到报错前的操作步骤。拿到这三样绝大多数问题都能定位。启动阶段最常见的错误是ModuleNotFoundError一般是依赖没装全。处理方法是让对方执行pip install -r requirements.txt如果仍然报模块找不到检查一下是不是本机存在多个 Python 版本pip对应的 Python 和python命令对应的 Python 不一致导致。第二个高频问题是数据库连接失败。本地开发我一直用 SQLite方便快捷零配置如果对方需要远程演示而必须切换 MySQL 或 PostgreSQL建议在配置文件中区分settings_dev.py和settings_prod.py不要让开发环境直接连生产数据库数据库结构改变时容易出错。第三个高频问题是静态文件加载不出来。Debug 模式下 Django 会自动托管静态文件但一旦DEBUGFalse就需要手动执行collectstatic并配置静态文件服务。如果是用 Windows 系统做演示建议保留DEBUGTrue避免额外配置静态文件服务器。6.3 讲解演示的节奏与重点项目交付不只是把代码发过去讲解也是毕设辅导的重要组成部分。我看了很多同学的答辩演示发现一个普遍问题讲得太平从头到尾顺着页面点一遍没有重点。我建议的演示节奏是这样的。开场先花一分钟讲选题背景直接说插画鲜花商城的核心难点在于两种不同品类商品的管理与交易把整个项目的定位立住。然后进入系统演示按业务主线走商品浏览 → 商品详情 → 加入购物车 → 结算下单 → 支付成功 → 后台订单处理 → 库存扣减验证。这条路径走完你的核心功能就已经展示完了。随后选一个最有技术含量的点深入讲。比如库存并发控制现场打开数据库把库存改成 5开两个浏览器窗口同时下单展示最终只有一个订单成功用实事说明你做了超卖防护。这个效果比任何 PPT 描述都更有说服力。最后讲一个你在项目里踩过的坑以及你是怎么定位和解决的。真实的失败经历比完美的演示更能证明你的能力。比如某个状态流转你最初没有考虑取消订单后来发现用户需要取消待付款订单于是补上了状态校验逻辑。这种细节会让老师觉得项目是真正被做完、被思考过的而不是照着教程敲出来的。最后再说一点我在实际交付中经常提醒别人的事情代码里一定要写注释。不是每行都写而是在关键的模型设计、状态流转、事务边界这些地方写清楚设计原因。半年之后你再回看自己的代码会感谢当时留下的注释对方接手代码时想要理解你的设计思路注释就是最直接的语言。项目在你们手上能跑明白在你的文档和讲解里能说明白这比多做一个功能重要得多。