ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

基于Python Django的汉服租赁可视化系统设计与实现

基于Python Django的汉服租赁可视化系统设计与实现

简介:一套基于Python+Django的汉服租赁可视化系统完整毕业设计项目,面向计算机相关专业正在准备毕设的学生,也适合需要项目实战练习的Java学习者,可直接用于课程设计、期末大作业。系统围绕汉服租赁业务,实现用户注册登录、汉服展示、租赁管理、订单处理、支付结算与用户反馈等核心功能,界面简洁、操作便捷,并借助可视化方式呈现业务流程。后端采用Python与Django框架,利用ORM简化数据库操作,前端配合Vue组件,整体结构清晰,易于二次开发。

资源包共725个文件,压缩包大小31.02MB,包含Python源码(py/pyc)、Vue前端组件、HTML/CSS/JS页面、SVG与PNG/JPG图片素材、SQL数据库脚本、bat启动与初始化脚本、doc/docx说明文档等,类型覆盖开发、部署与文档各环节。项目附带运行说明、数据库结构文档及源码,已调试可运行,学习者可直接启动查看效果,也可按需修改扩展。目前已有47人学习下载,适合需要完整毕设方案或Web全栈练习的读者。

1. 汉服租赁可视化系统的真实需求:先有数据纪律,再谈图表漂亮

汉服租赁可视化系统的重点不在“可视化”三个字。这个基于 Python+Django 的方案,核心是把衣服档案、顾客、订单和档期占用落成结构化数据,再让图表回答“未来两周哪些衣服租出去了、哪些在仓库积灰、哪些顾客月底会回来”。

它适合两类人。一类是已有线下租赁业务、还在用 Excel 排期表经营的店主,需要一套自己能维护的后台;另一类是刚学完 Django 基础、想找一个完整业务题练手的开发者。

如果只是想要一块会发光的 LED 大屏,那不必写代码;想真正管住租期、库存和押金,下面这套系统的落地路径才是起点。

2. 技术选型与数据模型:Python+Django 在租赁业务里的位置,以及四张核心表怎么拆

2.1 为什么选 Python+Django 而不是 Flask 或 Node:可视化系统里最可控的是 ORM 和 Admin

先说选型。汉服租赁这种项目,我一向不推荐一上来就搞前后端分离,更不推荐为一个小系统拆微服务。门店店员要录衣服、查档期,店主要看统计,这中间 70% 的工作是数据维护,不是交互体验。Django 的 Admin 站点在这一步能帮你省掉几乎全部表单页面:把车款模型注册进 Admin,店员就能在后台直接增删改款式、改日租金、下架破损衣服。Flask 当然也能做,但你需要再挑一个 Admin 组件、再配一套迁移工具,调通的时间往往比 Django 自带的一整套多出两三天;Node 的生态里合适的全栈框架也不少,但 Express 本身不给你 ORM、迁移和 Admin,还是得自己拼。

再往深看,这个系统的复杂度集中在外键关系上。一套汉服对应多条订单明细,一个顾客有多张订单,订单明细和订单之间又有状态联动。Django ORM 把外键、级联删除、过滤聚合收进同一个 QuerySet 接口,调试时可以直接看到底层 SQL,省掉很多排错时间。可视化部分也不需要单独起前端服务,Django 视图直接返回 JSON,模板做首屏,一台服务器就能跑完。日单量几十单的小店,这套架构冗余度已经足够。

2.2 核心模型:汉服档案、客户、租赁单、订单明细的字段取舍

我习惯先把对象拆清楚:衣服是资产,客户是人,租赁单是过程,明细是过程里每一件衣服的具体租期和金额。四个模型基本能装下小店的全部运营数据。

from django.db import models class Hanfu(models.Model): STYLE_CHOICES = [ ('quyao', '齐腰襦裙'), ('ming', '明制'), ('tang', '唐制'), ('song', '宋制'), ('other', '其他形制'), ] name = models.CharField('款式名称', max_length=64) style = models.CharField('形制', max_length=8, choices=STYLE_CHOICES, db_index=True) size = models.CharField('尺码', max_length=4, default='M') daily_rent = models.DecimalField('日租金', max_digits=7, decimal_places=2) deposit = models.DecimalField('押金', max_digits=7, decimal_places=2, default=0) is_active = models.BooleanField('是否上架', default=True) created_at = models.DateTimeField('创建时间', auto_now_add=True) class Meta: indexes = [ models.Index(fields=['style', 'is_active']), models.Index(fields=['size']), ] class Customer(models.Model): nickname = models.CharField('称呼', max_length=32) phone = models.CharField('手机号', max_length=20, unique=True, db_index=True) wechat = models.CharField('微信号', max_length=64, blank=True) created_at = models.DateTimeField('创建时间', auto_now_add=True)

字段里有三个细节我每次都会特意讲一下。第一,daily_rent和deposit都用DecimalField,不要用FloatField。租金按天累加,浮点运算会在几十单之后给你攒出一笔零头误差,看板营收会对不上账。第二,style加db_index=True,因为后面按形制统计出租率是高频查询;但索引不是越多越好,像remark这种备注文本千万不要加索引。第三,Customer.phone的unique=True能防止同一个顾客被店员录入两次,这个约束比在表单里做校验可靠得多。

这里还要补一个迁移动作:模型写完之后先makemigrations rental再migrate,别把这两个命令当成一回事。前者生成迁移文件,后者真正写库。本地开发用 SQLite 没问题,部署到 MySQL 或 PostgreSQL 时再看一遍生成的文件,重点看有没有意外把DecimalField变成double precision的记录。

2.3 给可视化查询铺路的三个字段习惯:索引、Decimal、时间

很多人把模型建完就开始写视图,到后端聚合统计发现数字不对,回来看表结构已经改不动。我的习惯是在建表阶段就把后面要踩的坑先埋掉。

第一,所有统计口径都用状态字段,不要靠推算。租赁单至少有四种状态,后面我会用 pending、rented、closed、cancelled 来表达;统计所谓“有效订单”应该直接过滤status,而不是用时间去猜。第二,时间字段分开用:订单创建时间用DateTimeField(auto_now_add=True)记录精确时刻,租期只用start_date和end_date两个DateField。可视化里“按天看量”和“算逾期天数”是两类不同口径,混用最容易出错。第三,删除保护要明确:Customer和Hanfu的外键都用on_delete=models.PROTECT,历史订单必须留着,别让一次误删 CASCADE 把可视化统计的根基给断掉。

这些做完之后还有一个附加好处:Django Admin 里直接注册这两个模型,店员就能用后台录数据。

from django.contrib import admin from .models import Hanfu, Customer @admin.register(Hanfu) class HanfuAdmin(admin.ModelAdmin): list_display = ('name', 'style', 'size', 'daily_rent', 'is_active') list_filter = ('style', 'size', 'is_active') search_fields = ('name',) @admin.register(Customer) class CustomerAdmin(admin.ModelAdmin): list_display = ('nickname', 'phone', 'created_at') search_fields = ('phone', 'nickname')

list_display控制后台列表显示哪些列,list_filter是右侧筛选器,search_fields是搜索框。店员不需要会 SQL,Admin 就是他们对系统最常见的入口。

3. 从零把订单闭环跑通:最小命令、租赁状态机与并发扣库存

3.1 创建项目和应用的最小命令

先把基本环境跑起来。下面这套命令适用于 Python 3.10 以上和 Django 4.2 LTS,是一个比较稳妥的组合。

python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install "django>=4.2,<5.0" django-admin startproject x050_hanfu . python manage.py startapp rental

这里说三个容易忽略的细节。第一,startproject最后面的点号很重要,它让项目配置文件生成在当前目录,避免后面所有模块导入都带着一层多余路径。第二,requirements.txt里不要写裸的 Django 版本,半年之后大版本更新可能让 ORM 行为变化;锁成一个区间,至少能保证你写文章时贴出来的命令在读者手里也可以复现。第三,项目名用x050_hanfu,符合 Python 包名规范,不会在导入时踩到关键字或者中划线问题。

项目创建后在settings.py的INSTALLED_APPS里加上rental,然后先跑一次迁移。这时候模型还没建,migrate只是把 Django 自带的用户、Session 表初始化出来,顺便验证数据库连接通不通。

python manage.py makemigrations rental python manage.py migrate python manage.py createsuperuser

createsuperuser创建的是 Admin 管理员。本地实际跑起来之后,我建议你立刻在后台把两套汉服、一个顾客录进去,后面写订单功能时手动测试会方便很多。

3.2 订单状态机设计:pending、rented、closed、cancelled 四态

租赁订单比普通商品订单多一个时间坐标:衣服在某天被取走,在某天被归还,中间是占用状态。状态机设计成四个状态,是我试过几版之后觉得最省心的模型。

class LeaseOrder(models.Model): STATUS_PENDING = 'pending' STATUS_RENTED = 'rented' STATUS_CLOSED = 'closed' STATUS_CANCELLED = 'cancelled' STATUS_CHOICES = [ (STATUS_PENDING, '待取衣'), (STATUS_RENTED, '租赁中'), (STATUS_CLOSED, '已归还'), (STATUS_CANCELLED, '已取消'), ] order_no = models.CharField('订单号', max_length=32, unique=True) customer = models.ForeignKey(Customer, on_delete=models.PROTECT, related_name='orders') start_date = models.DateField('开始日期') end_date = models.DateField('结束日期') status = models.CharField('状态', max_length=10, choices=STATUS_CHOICES, default=STATUS_PENDING, db_index=True) total_amount = models.DecimalField('租金合计', max_digits=8, decimal_places=2) deposit = models.DecimalField('押金', max_digits=8, decimal_places=2, default=0) actual_return_date = models.DateField('实际归还日期', null=True, blank=True) late_fee = models.DecimalField('逾期费用', max_digits=8, decimal_places=2, null=True, blank=True) created_at = models.DateTimeField('创建时间', auto_now_add=True) updated_at = models.DateTimeField('更新时间', auto_now=True) class OrderItem(models.Model): order = models.ForeignKey(LeaseOrder, on_delete=models.CASCADE, related_name='items') hanfu = models.ForeignKey(Hanfu, on_delete=models.PROTECT, related_name='order_items') rent_days = models.PositiveIntegerField('租期天数') amount = models.DecimalField('小计金额', max_digits=7, decimal_places=2)

这里最容易被带偏的是:不要用布尔字段表达状态。租赁订单的生命周期至少有两个维度——是否完成、是否取消,一个is_returned表达不了中间态。把状态文本直接写进字段的好处是可读、可索引、聚合查询里一眼就能写出过滤条件;坏处是状态数量必须克制,别让它长成七八个值,否则每个视图都会变成状态判断的金字塔。四态在这个场景刚好。

另外两个字段要解释清楚。rent_days是冗余字段,理论上可以从start_date和end_date推算,但租赁业务里经常有“续租一天”这种操作,直接改日期会让历史金额不可追溯,所以我把它冗余进明细作为计费依据。actual_return_date和late_fee先置空,归还时再写入。

3.3 下单视图:事务、锁和档期冲突检测

这套系统能不能防住“同一套汉服同一个周末被两个人同时订”,是成败分水岭。最危险的做法是:先查有没有冲突,再插入。单用户手动操作没问题,但店里前台和线上渠道同时来单,两个请求都查到“无冲突”,然后各自插入,库存就超了。

import secrets from datetime import datetime from django.db import transaction from django.shortcuts import redirect, render, get_object_or_404 from django.contrib import messages def create_order(request): form = OrderForm(request.POST or None) if request.method == 'POST' and form.is_valid(): customer = get_object_or_404(Customer, pk=form.cleaned_data['customer_id']) hanfu_ids = form.cleaned_data['hanfu_ids'] start = form.cleaned_data['start_date'] end = form.cleaned_data['end_date'] if start >= end: messages.error(request, '归还日期必须晚于取衣日期') return redirect('order_create') with transaction.atomic(): items = list(Hanfu.objects.select_for_update().filter(id__in=hanfu_ids, is_active=True)) if len(items) != len(hanfu_ids): messages.error(request, '有衣服已下架,请刷新后重选') return redirect('order_create') conflict = OrderItem.objects.filter( hanfu__in=items, order__status__in=['pending', 'rented'], order__start_date__lte=end, order__end_date__gte=start, ).exists() if conflict: messages.error(request, '选择的时段里存在档期冲突') return redirect('order_create') days = (end - start).days order = LeaseOrder.objects.create( order_no=datetime.now().strftime('%Y%m%d%H%M%S') + secrets.token_hex(2).upper(), customer=customer, start_date=start, end_date=end, status=LeaseOrder.STATUS_PENDING, total_amount=sum(item.daily_rent for item in items) * days, deposit=sum(item.deposit for item in items), ) OrderItem.objects.bulk_create([ OrderItem(order=order, hanfu=item, rent_days=days, amount=item.daily_rent * days) for item in items ]) return redirect('order_detail', pk=order.pk) return render(request, 'rental/order_form.html', {'form': form})

这段代码的关键在于transaction.atomic和select_for_update的配合。transaction.atomic把冲突检查和插入绑成同一个事务,select_for_update锁定选中的汉服行,让针对同一批衣服的并发请求变成串行:后来的请求必须先等前一个事务提交,提交后它再做冲突检查,这时已经能看到刚创建的订单。天数计算用(end - start).days,金额全部走 Decimal,不能用 int 去拼。

注意:select_for_update不能替代数据库级的唯一约束。真要彻底防超租,需要再设计一张档期占用表,用数据库唯一索引约束“同一件衣服同一天只能一条占用记录”。上面这个方案能防住并发窗口期,日单量几十单的小店足够,等量大了再加落库约束。

3.4 归还视图:逾期费用计算和状态流转

取衣之后把状态从pending改成rented,归还时关单。逾期费用只需要拿今天和end_date做差。

from datetime import date from django.db import transaction from django.http import JsonResponse from django.views.decorators.http import require_POST LATE_FEE_PER_DAY = 20 @require_POST def return_order(request, order_id): with transaction.atomic(): order = LeaseOrder.objects.select_for_update().get(pk=order_id) if order.status != LeaseOrder.STATUS_RENTED: return JsonResponse({'error': '订单未处于租赁中状态'}, status=400) today = date.today() overdue_days = (today - order.end_date).days late_fee = max(0, overdue_days) * LATE_FEE_PER_DAY order.status = LeaseOrder.STATUS_CLOSED order.actual_return_date = today order.late_fee = late_fee order.save(update_fields=['status', 'actual_return_date', 'late_fee']) return JsonResponse({'ok': True, 'late_fee': str(late_fee)})

两点说明。第一,LATE_FEE_PER_DAY先写成模块常量,真实项目里应该改成一张计费配置表,店主经常要调整逾期费率。第二,逾期费按订单收还是按明细收,要在设计阶段定清楚。按天整租的店订单都是整批取走整批归还,按订单收最省;如果做成一件一价、允许部分归还,逾期费就要下沉到 OrderItem 上,判断逻辑得提前拆出来。

4. 可视化层实现:后端聚合查询输出 JSON,前端 ECharts 渲染租赁看板

4.1 先定义统计口径:哪些数字算营收,哪些算逾期

可视化系统的口碑往往不是死在图表上,而是死在“系统数字和店里 Excel 对不上”。所以写任何聚合查询之前,先把口径固定下来。这是我做汉服租赁项目的三行铁律:

  • 营收 = 已归还订单的租金总和,不含押金,不含 pending 和 cancelled;
  • 出租率 = 某时间段内订单占用天数 / 该时间段可租天数;
  • 逾期率 = 已归还订单里有逾期记录的订单数 / 已归还订单总数。

押金不是营收,这是新手最容易犯的错误。押金在模型里单列字段,看板里凡是涉及利润的地方都不会带它。等客户归还后再做退款操作,流程上和可视化才能对应得上。

4.2 后端聚合查询:用 annotate 和 TruncMonth 出月营收 JSON

Django ORM 的TruncMonth函数可以把订单创建时间直接截断到月份,再配合values和annotate做分组聚合,完全不需要写原生 SQL。

from django.db.models import Sum, Count from django.db.models.functions import TruncMonth from django.http import JsonResponse from django.core.serializers.json import DjangoJSONEncoder from django.views import View from .models import LeaseOrder class MonthlyRevenueView(View): def get(self, request): rows = ( LeaseOrder.objects .filter(status=LeaseOrder.STATUS_CLOSED) .annotate(month=TruncMonth('created_at')) .values('month') .annotate( revenue=Sum('total_amount'), order_count=Count('id'), ) .order_by('month') ) return JsonResponse({'rows': list(rows)}, encoder=DjangoJSONEncoder)

这里面三个参数最值得讲。第一,filter(status=LeaseOrder.STATUS_CLOSED)只统计已归还订单,和第 4.1 节的口径完全一致。第二,annotate(month=TruncMonth('created_at'))把每一条订单创建时间截断到当月一号,后续分组自然落在月份维度;要按周看就换TruncWeek。第三,DjangoJSONEncoder解决日期和 Decimal 的序列化问题,不然视图里还得自己手动转字符串。

注意:TruncMonth是数据库函数,MySQL 和 PostgreSQL 都能用;SQLite 本地开发时受日期格式影响,部署到 MySQL 前最好先对比一次首尾月数据。

4.3 前端渲染:模板里放 json_script,ECharts 只负责画

图表选 ECharts 是常见做法。模板里我用json_script把后端数据直接塞进一个 script 标签,前端不用发第二次请求。

<!-- dashboard.html --> <div id="monthly-chart" style="height: 320px;"></div> {{ chart_data|json_script:"monthly-data" }} <script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script> <script> const rows = JSON.parse(document.getElementById('monthly-data').textContent); const chart = echarts.init(document.getElementById('monthly-chart')); chart.setOption({ xAxis: { type: 'category', data: rows.map(r => r.month.slice(0, 7)) }, yAxis: { type: 'value' }, series: [{ type: 'bar', data: rows.map(r => Number(r.revenue)) }] }); </script>

对应的视图代码要把聚合结果放进 render 的 context,命名为chart_data。这样做的两个好处:一是首屏在服务端完成,不需要前端工程化构建链,小团队拿到代码就能改;二是json_script自带 HTML 转义,数据里的字符串不会成为 XSS 入口。图表多的时候,把 chart 初始化部分的逻辑拆成函数,重复调用就好。

4.4 把图表映射回运营动作:出租率、Top 5、档期日历

图表不是装饰品。我在店里实际会用三个看板模块:第一个是“未来 14 天档期占用表”,本质是一个日历,每天格子显示当天已预订件数和可租件数;第二个是“形制出租率”,马上能看出齐腰襦裙和明制哪个才是现金流;第三个是“客户复购列表”,统计每个顾客在最近 90 天内是否第二次下单,这比每天盯流水更能提前发现趋势。

这三个模块在后端都不是复杂的 SQL,难点全在口径统一。档期占用表基于订单日期做区间过滤,出租率基于明细天数求和,复购基于客户分组计数。不同指标查询不同的表和字段很正常,关键是每个指标都要能从代码注释一路追溯到运营动作,否则画完图没人看,就白白浪费了可视化的价值。

5. 汉服租赁 Django 系统最常见的 5 个坑:现象、原因和解决办法

5.1 并发下单导致同一天同一件汉服被订两次

现象:周末某个爆款汉服在系统里显示可租,两个顾客同时提交订单,前后间隔不到一秒,两边都提示下单成功,店面实际只有一件,线下盘点时才发现重复占用。

原因:订单创建逻辑如果写成“先查有没有冲突再插入”,两个并发请求在各自事务里都看不到对方未提交的插入,于是双双通过冲突检查。没有事务、没有锁,也没有数据库约束,就是最容易翻车的写法。

解决:像第 3.3 节那样用transaction.atomic把冲突检测和订单创建包进同一事务,并用select_for_update锁住选中汉服行,让同一批衣服的请求变成串行。想做到数据库铁约束,再加一张Occupancy表,用唯一索引约束同一服装同一天只能一条记录;基础方案先上锁,量大了再补。

5.2 归还日期总是差一天:时区设置翻车

现象:顾客 14 号晚上归还,店员在系统里点归还,逾期计算判断成 13 号,多收一天费用,顾客投诉。

原因:Django 默认USE_TZ=True,数据库里存的是 UTC 时间。如果settings.py没写TIME_ZONE,后面拿timezone.now().date()去和end_date比较,北京时间八点以前取到的 UTC 日期还是前一天,逾期天数就差了 1。

解决:在settings.py里写TIME_ZONE = 'Asia/Shanghai',USE_TZ保持True。业务上凡是要判断“今天”的地方,统一从django.utils.timezone.localtime取日期,不要直接调用datetime.date.today()。这个坑排查起来最容易伪装成“代码写错”,其实是时区配置的玄学。

5.3 订单列表页打开要好几秒:N+1 查询

现象:管理员后台点开订单列表,页面转圈好几秒;订单量到几百单之后,一次页面加载要执行几百条 SQL。

原因:模板里显示order.customer.phone和order.items.all,如果列表视图没有做预取,Django ORM 默认懒加载,每行订单都额外执行查询,这就是典型的 N+1 问题。

解决:在列表视图的 QuerySet 上加上select_related('customer'),订单明细用prefetch_related('items__hanfu')预取。一条 SQL 顶掉原来的几百条。之后只要出现联表显示,都要回头检查一次查询计数。

5.4 看板数字和店里 Excel 对不上:统计口径不一致

现象:老板用 Excel 自己统计的月营收比系统看板多出两千多,一张张核对发现系统把两笔取消订单的押金也算进了费用。

原因:押金字段和租金字段在同一个模型里,聚合营收时直接对total_amount + deposit做了求和,或者 filter 里漏掉了 cancelled 状态。Excel 只算现金流水,系统把押金并进来,自然对不上。

解决:把“押金不计入营收”“cancelled 不算有效订单”这类口径凝固成公共查询函数。我一般会在service.py里封装一个closed_orders()方法,视图统一调它,不重复写 filter。同时把口径写进注释,比店长口头传达可靠得多。

5.5 部署到服务器后 CSS 全丢:静态文件没处理

现象:本地开发一切正常,部署到 Nginx 之后页面布局全乱,Admin 后台也没样式,控制台几百个 404。

原因:DEBUG=False之后 Django 不再接管静态文件,STATIC_ROOT没设置,也没执行过collectstatic。本地开发依赖 Django 的静态文件服务,上生产这条链路断了。

解决:在settings.py里配好STATIC_URL和STATIC_ROOT,部署流程中加一步python manage.py collectstatic --noinput,让 Nginx 直接服务STATIC_ROOT目录。如果丢的只是 Admin 样式,说明django.contrib.staticfiles的默认静态目录没被收集完整。

6. 让可视化数据可信的小习惯:写一个每日对账的管理命令

看板做得再漂亮,一旦报表数据和实际单据对不上,之前积累的信任就全没了。我的做法是给 Django 写一个每日对账的管理命令,定时执行,把系统关键数字和“昨天应该发生的事”做一次硬核对。

from datetime import date, timedelta from django.core.management.base import BaseCommand from rental.models import LeaseOrder class Command(BaseCommand): help = '每日对账:核对昨日订单、营收和逾期未还' def handle(self, *args, **options): yesterday = date.today() - timedelta(days=1) closed_orders = LeaseOrder.objects.filter( status=LeaseOrder.STATUS_CLOSED, actual_return_date=yesterday, ) revenue = sum(order.total_amount for order in closed_orders) overdue = LeaseOrder.objects.filter( status__in=[LeaseOrder.STATUS_PENDING, LeaseOrder.STATUS_RENTED], end_date__lt=yesterday, ).count() self.stdout.write(f'昨日归还订单数: {closed_orders.count()}') self.stdout.write(f'昨日确认营收(不含押金): {revenue}') if overdue: self.stdout.write(self.style.WARNING(f'超期未归还订单数: {overdue}')) else: self.stdout.write('超期未归还订单数: 0')

这段命令有两个技巧。第一,查询统一收口到管理命令,每天固定时间执行一次,输出存进日志,复盘时能查;第二,判断“超期未归还”直接用end_date < yesterday,和订单状态无关,能自动把逾期订单揪出来。配合 crontab 和日志,它比实时大屏更能守住数据可信度。

数据可信了,可视化看板才能从装饰变成决策工具。我自己的习惯是每天先看一遍对账命令的输出,再打开看板看趋势,这两步顺序从不反过来。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表