基于Python Django的汉服租赁可视化系统设计与实现 简介一套基于PythonDjango的汉服租赁可视化系统完整毕业设计项目面向计算机相关专业正在准备毕设的学生也适合需要项目实战练习的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. 汉服租赁可视化系统的真实需求先有数据纪律再谈图表漂亮汉服租赁可视化系统的重点不在“可视化”三个字。这个基于 PythonDjango 的方案核心是把衣服档案、顾客、订单和档期占用落成结构化数据再让图表回答“未来两周哪些衣服租出去了、哪些在仓库积灰、哪些顾客月底会回来”。它适合两类人。一类是已有线下租赁业务、还在用 Excel 排期表经营的店主需要一套自己能维护的后台另一类是刚学完 Django 基础、想找一个完整业务题练手的开发者。如果只是想要一块会发光的 LED 大屏那不必写代码想真正管住租期、库存和押金下面这套系统的落地路径才是起点。2. 技术选型与数据模型PythonDjango 在租赁业务里的位置以及四张核心表怎么拆2.1 为什么选 PythonDjango 而不是 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_length64) style models.CharField(形制, max_length8, choicesSTYLE_CHOICES, db_indexTrue) size models.CharField(尺码, max_length4, defaultM) daily_rent models.DecimalField(日租金, max_digits7, decimal_places2) deposit models.DecimalField(押金, max_digits7, decimal_places2, default0) is_active models.BooleanField(是否上架, defaultTrue) created_at models.DateTimeField(创建时间, auto_now_addTrue) class Meta: indexes [ models.Index(fields[style, is_active]), models.Index(fields[size]), ] class Customer(models.Model): nickname models.CharField(称呼, max_length32) phone models.CharField(手机号, max_length20, uniqueTrue, db_indexTrue) wechat models.CharField(微信号, max_length64, blankTrue) created_at models.DateTimeField(创建时间, auto_now_addTrue)字段里有三个细节我每次都会特意讲一下。第一daily_rent和deposit都用DecimalField不要用FloatField。租金按天累加浮点运算会在几十单之后给你攒出一笔零头误差看板营收会对不上账。第二style加db_indexTrue因为后面按形制统计出租率是高频查询但索引不是越多越好像remark这种备注文本千万不要加索引。第三Customer.phone的uniqueTrue能防止同一个顾客被店员录入两次这个约束比在表单里做校验可靠得多。这里还要补一个迁移动作模型写完之后先makemigrations rental再migrate别把这两个命令当成一回事。前者生成迁移文件后者真正写库。本地开发用 SQLite 没问题部署到 MySQL 或 PostgreSQL 时再看一遍生成的文件重点看有没有意外把DecimalField变成double precision的记录。2.3 给可视化查询铺路的三个字段习惯索引、Decimal、时间很多人把模型建完就开始写视图到后端聚合统计发现数字不对回来看表结构已经改不动。我的习惯是在建表阶段就把后面要踩的坑先埋掉。第一所有统计口径都用状态字段不要靠推算。租赁单至少有四种状态后面我会用 pending、rented、closed、cancelled 来表达统计所谓“有效订单”应该直接过滤status而不是用时间去猜。第二时间字段分开用订单创建时间用DateTimeField(auto_now_addTrue)记录精确时刻租期只用start_date和end_date两个DateField。可视化里“按天看量”和“算逾期天数”是两类不同口径混用最容易出错。第三删除保护要明确Customer和Hanfu的外键都用on_deletemodels.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是搜索框。店员不需要会 SQLAdmin 就是他们对系统最常见的入口。3. 从零把订单闭环跑通最小命令、租赁状态机与并发扣库存3.1 创建项目和应用的最小命令先把基本环境跑起来。下面这套命令适用于 Python 3.10 以上和 Django 4.2 LTS是一个比较稳妥的组合。python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install django4.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 createsuperusercreatesuperuser创建的是 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_length32, uniqueTrue) customer models.ForeignKey(Customer, on_deletemodels.PROTECT, related_nameorders) start_date models.DateField(开始日期) end_date models.DateField(结束日期) status models.CharField(状态, max_length10, choicesSTATUS_CHOICES, defaultSTATUS_PENDING, db_indexTrue) total_amount models.DecimalField(租金合计, max_digits8, decimal_places2) deposit models.DecimalField(押金, max_digits8, decimal_places2, default0) actual_return_date models.DateField(实际归还日期, nullTrue, blankTrue) late_fee models.DecimalField(逾期费用, max_digits8, decimal_places2, nullTrue, blankTrue) created_at models.DateTimeField(创建时间, auto_now_addTrue) updated_at models.DateTimeField(更新时间, auto_nowTrue) class OrderItem(models.Model): order models.ForeignKey(LeaseOrder, on_deletemodels.CASCADE, related_nameitems) hanfu models.ForeignKey(Hanfu, on_deletemodels.PROTECT, related_nameorder_items) rent_days models.PositiveIntegerField(租期天数) amount models.DecimalField(小计金额, max_digits7, decimal_places2)这里最容易被带偏的是不要用布尔字段表达状态。租赁订单的生命周期至少有两个维度——是否完成、是否取消一个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, pkform.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__inhanfu_ids, is_activeTrue)) if len(items) ! len(hanfu_ids): messages.error(request, 有衣服已下架请刷新后重选) return redirect(order_create) conflict OrderItem.objects.filter( hanfu__initems, order__status__in[pending, rented], order__start_date__lteend, order__end_date__gtestart, ).exists() if conflict: messages.error(request, 选择的时段里存在档期冲突) return redirect(order_create) days (end - start).days order LeaseOrder.objects.create( order_nodatetime.now().strftime(%Y%m%d%H%M%S) secrets.token_hex(2).upper(), customercustomer, start_datestart, end_dateend, statusLeaseOrder.STATUS_PENDING, total_amountsum(item.daily_rent for item in items) * days, depositsum(item.deposit for item in items), ) OrderItem.objects.bulk_create([ OrderItem(orderorder, hanfuitem, rent_daysdays, amountitem.daily_rent * days) for item in items ]) return redirect(order_detail, pkorder.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(pkorder_id) if order.status ! LeaseOrder.STATUS_RENTED: return JsonResponse({error: 订单未处于租赁中状态}, status400) 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 出月营收 JSONDjango 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(statusLeaseOrder.STATUS_CLOSED) .annotate(monthTruncMonth(created_at)) .values(month) .annotate( revenueSum(total_amount), order_countCount(id), ) .order_by(month) ) return JsonResponse({rows: list(rows)}, encoderDjangoJSONEncoder)这里面三个参数最值得讲。第一filter(statusLeaseOrder.STATUS_CLOSED)只统计已归还订单和第 4.1 节的口径完全一致。第二annotate(monthTruncMonth(created_at))把每一条订单创建时间截断到当月一号后续分组自然落在月份维度要按周看就换TruncWeek。第三DjangoJSONEncoder解决日期和 Decimal 的序列化问题不然视图里还得自己手动转字符串。注意TruncMonth是数据库函数MySQL 和 PostgreSQL 都能用SQLite 本地开发时受日期格式影响部署到 MySQL 前最好先对比一次首尾月数据。4.3 前端渲染模板里放 json_scriptECharts 只负责画图表选 ECharts 是常见做法。模板里我用json_script把后端数据直接塞进一个 script 标签前端不用发第二次请求。!-- dashboard.html -- div idmonthly-chart styleheight: 320px;/div {{ chart_data|json_script:monthly-data }} script srchttps://cdn.jsdelivr.net/npm/echarts5/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_TZTrue数据库里存的是 UTC 时间。如果settings.py没写TIME_ZONE后面拿timezone.now().date()去和end_date比较北京时间八点以前取到的 UTC 日期还是前一天逾期天数就差了 1。解决在settings.py里写TIME_ZONE Asia/ShanghaiUSE_TZ保持True。业务上凡是要判断“今天”的地方统一从django.utils.timezone.localtime取日期不要直接调用datetime.date.today()。这个坑排查起来最容易伪装成“代码写错”其实是时区配置的玄学。5.3 订单列表页打开要好几秒N1 查询现象管理员后台点开订单列表页面转圈好几秒订单量到几百单之后一次页面加载要执行几百条 SQL。原因模板里显示order.customer.phone和order.items.all如果列表视图没有做预取Django ORM 默认懒加载每行订单都额外执行查询这就是典型的 N1 问题。解决在列表视图的 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。原因DEBUGFalse之后 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(days1) closed_orders LeaseOrder.objects.filter( statusLeaseOrder.STATUS_CLOSED, actual_return_dateyesterday, ) revenue sum(order.total_amount for order in closed_orders) overdue LeaseOrder.objects.filter( status__in[LeaseOrder.STATUS_PENDING, LeaseOrder.STATUS_RENTED], end_date__ltyesterday, ).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 和日志它比实时大屏更能守住数据可信度。数据可信了可视化看板才能从装饰变成决策工具。我自己的习惯是每天先看一遍对账命令的输出再打开看板看趋势这两步顺序从不反过来。希望帮到你。本文还有配套的精品资源点击获取