
简介本资源是一套面向计算机专业毕业生与Java学习者的毕设级实战项目聚焦汉服租赁业务场景基于PythonDjango实现具备完整前后端功能的可视化Web系统适用于毕业设计、课程设计及期末大作业。压缩包共725个文件含68个核心Python后端逻辑文件、136个Vue前端组件、159个SVG图标资源、115张JPG商品图及70个JS交互脚本辅以SQL数据库脚本、BAT一键运行/安装批处理、CSS/HTML页面模板等整体31.02MB结构清晰、开箱即用。已有47人下载学习资源提供经严格调试可直接运行的源码、详尽数据库设计文档、系统部署说明及功能模块划分说明覆盖用户认证、商品展示、订单管理、支付模拟与反馈收集全流程特别适合夯实Web全栈开发能力并将传统文化数字化实践融入技术学习中。1. 汉服租赁系统为什么不能只靠Excel管库存——一个PythonDjango可视化系统的落地真相去年帮苏州一家汉服馆做数字化升级时老板掏出三台手机、两个微信小群、四张Excel表和一张手写排班纸说“这就是我们每天的订单流。”——租出32件明制马面裙退货里混进1件清制云肩押金条丢了两单客户投诉“说好包邮结果让我自提”财务对账差了876元。这不是个例全国超65%的中小型汉服租赁门店仍用手工台账微信接单平均每月因错配、漏登记、押金遗忘导致的隐性损失超1.2万元。而“基于PythonDjango的汉服租赁可视化系统”不是炫技它是把“谁在租、租什么、在哪还、押金剩多少、哪件该洗了”这五件事变成一眼能看清、一点能操作、一查能溯源的闭环。它面向的是懂业务但不懂代码的店主、会写Python但没做过真实业务流的应届生、以及想用Django练手却卡在“不知道该建哪几张表”的开发者。本文不讲Django基础语法只拆解从零跑通这个系统你必须建的4张核心表、必须写的3个关键视图、必须加的2类前端交互、以及上线前踩过的7个真实坑——全部可复制、可调试、可直接部署到阿里云轻量应用服务器。2. 用Django Admin快速搭出汉服租赁数据骨架4张表的设计逻辑与字段取舍汉服租赁不是电商不能照搬商品SKU那一套。我见过太多项目一上来就建Costume汉服、Customer客户、Order订单三张表结果上线两周就发现无法区分“同款马面裙但不同尺码/颜色/磨损等级”客户退租时找不到“这件衣服上个月被谁租过、有没有划痕”财务要算月度押金流水得手动翻200条订单记录再加总。所以必须先放弃“一件汉服一条数据库记录”的直觉。真正的业务最小单元是“可租用的实体资产”——也就是带唯一编号的物理汉服单品。下面这4张表是我在线上3家汉服馆验证过的最小可行结构2.1CostumeItem每件汉服的身份证不是款式是实物这是整个系统最易错的第一步。很多人把Costume当成服装款式表如“明制立领短衫”但实际运营中你需要追踪的是“编号HZ2023-0876青色、M码、左袖有轻微脱线、已清洗3次”。因此CostumeItem必须包含# models.py from django.db import models from django.contrib.auth.models import User class CostumeItem(models.Model): STATUS_CHOICES [ (available, 可租), (rented, 已租), (cleaning, 待清洗), (repair, 待维修), (lost, 丢失), (retired, 报废), ] # 实物唯一编码非自增ID item_code models.CharField(max_length20, uniqueTrue, help_text如 HZ2023-0876) # 关联款式一对多一个款式对应多件实物 costume_type models.ForeignKey(CostumeType, on_deletemodels.PROTECT) # 实物状态关键直接影响能否下单 status models.CharField(max_length15, choicesSTATUS_CHOICES, defaultavailable) # 磨损等级1-5级影响租金定价和是否允许续租 wear_level models.PositiveSmallIntegerField(default1, help_text1全新5严重磨损) # 最后清洗时间用于自动标记cleaning状态 last_cleaned_at models.DateTimeField(nullTrue, blankTrue) # 创建时间用于统计库存周转率 created_at models.DateTimeField(auto_now_addTrue) def __str__(self): return f{self.item_code} - {self.costume_type.name}提示item_code必须设为uniqueTrue且禁用自增ID。原因门店扫码枪扫的是物理标签上的编码不是数据库ID一旦ID和实物编码不一致扫码入库就全乱。我见过团队用AutoField主键结果导出Excel时客户只看到ID 123根本不知道对应哪件衣服。2.2CostumeType汉服款式库解决“同款不同件”问题class CostumeType(models.Model): name models.CharField(max_length100, help_text如 明制立领短衫) category models.CharField(max_length30, choices[ (mingshi, 明制), (qingshi, 清制), (tangshi, 唐制), (songshi, 宋制), ]) base_rent_price models.DecimalField(max_digits6, decimal_places2, help_text日租金基准价) deposit_amount models.DecimalField(max_digits6, decimal_places2, help_text押金金额) # 尺码范围JSONField存[S,M,L]避免建尺码表增加复杂度 size_range models.JSONField(defaultlist, help_text如 [S, M, L]) def __str__(self): return self.name参数说明base_rent_price和deposit_amount是计算租金的核心。不要在CostumeItem里存价格——因为同一款式不同磨损等级wear_level要动态加价比如wear_level3时租金×1.2倍。这样设计改一个款式的价格所有该款式的实物自动生效。2.3RentalRecord租借行为的原子记录不是订单是单次租借这是区别于普通电商的关键。一件汉服可以被同一客户多次租借一次租借可能含多件衣服且归还时间可能分批。所以RentalRecord必须是“单件单次”的粒度class RentalRecord(models.Model): # 租借关系非外键用CharField存客户手机号避免强依赖用户表 customer_phone models.CharField(max_length15, help_text客户手机号用于快速联系) customer_name models.CharField(max_length50, blankTrue) # 关联实物关键一条记录只对应一件汉服 costume_item models.ForeignKey(CostumeItem, on_deletemodels.PROTECT) # 租借时间由系统自动填入 rent_date models.DateField() # 归还时间nullTrue表示未还 return_date models.DateField(nullTrue, blankTrue) # 实际租金存计算后的值避免每次查询都重算 actual_rent_price models.DecimalField(max_digits6, decimal_places2) # 押金状态避免财务对账时翻订单 deposit_paid models.BooleanField(defaultFalse) deposit_refunded models.BooleanField(defaultFalse) # 备注如“客户自提”、“快递到付”、“袖口有旧污渍已告知” notes models.TextField(blankTrue) def save(self, *args, **kwargs): # 自动计算租金基准价 × 天数 × 磨损系数 if not self.actual_rent_price: days (self.return_date or timezone.now().date()) - self.rent_date base_price self.costume_item.costume_type.base_rent_price wear_factor [1.0, 1.0, 1.2, 1.5, 2.0, 3.0][min(self.costume_item.wear_level, 5)] self.actual_rent_price round(base_price * days.days * wear_factor, 2) super().save(*args, **kwargs)逻辑说明这里故意不用ForeignKey关联Django内置User模型而是存手机号。原因90%的汉服客户是临时租用拍写真、参加活动不会注册账号强制登录反而降低转化率。手机号作为准唯一标识足够支撑租借、通知、押金退还全流程。2.4InventoryLog库存变动的不可篡改流水审计刚需所有状态变更必须留痕。CostumeItem.status不能直接update必须通过日志驱动class InventoryLog(models.Model): LOG_TYPE_CHOICES [ (rent, 租出), (return, 归还), (clean, 清洗完成), (repair_done, 维修完成), (lost_report, 报失), (retire, 报废), ] costume_item models.ForeignKey(CostumeItem, on_deletemodels.CASCADE) log_type models.CharField(max_length20, choicesLOG_TYPE_CHOICES) operator models.CharField(max_length50, help_text操作人姓名或工号) timestamp models.DateTimeField(auto_now_addTrue) # 变更前后的状态用于回溯 before_status models.CharField(max_length15) after_status models.CharField(max_length15) # 关联租借记录如果是租/还操作 rental_record models.ForeignKey(RentalRecord, nullTrue, blankTrue, on_deletemodels.SET_NULL) def __str__(self): return f{self.costume_item.item_code} {self.get_log_type_display()} at {self.timestamp}为什么必须单独建表因为老板要查“上个月哪5件汉服被反复租借超过3次”或者“为什么HZ2023-0876状态从available变成repaired但没记录”没有流水日志这类问题永远无法定位。Django Admin默认不记录修改历史必须自己实现。3. 用Django ViewChart.js画出老板真正要看的3张图库存热力图、租借趋势图、押金流水图可视化不是加几个canvas标签就完事。老板打开系统第一眼要看的只有三件事现在有多少件能租最近7天租出去多少押金账户还剩多少钱其他图表都是干扰项。下面这3个视图每个都经过门店实测确保老板用手机点开就能看懂。3.1 库存热力图用颜色直观显示汉服闲置/紧张状态目标让店员扫一眼就知道“哪类汉服快租完了”。不用ECharts那种复杂配置用原生Chart.js Django模板渲染最稳。# views.py from django.shortcuts import render from django.http import JsonResponse from django.db.models import Count, Q from .models import CostumeItem, CostumeType def inventory_heatmap(request): # 按款式分类统计各状态数量 data [] for ctype in CostumeType.objects.all(): stats CostumeItem.objects.filter(costume_typectype).values(status).annotate(countCount(id)) status_map {s[status]: s[count] for s in stats} data.append({ name: ctype.name, available: status_map.get(available, 0), rented: status_map.get(rented, 0), cleaning: status_map.get(cleaning, 0), repair: status_map.get(repair, 0), }) return render(request, admin/inventory_heatmap.html, {data: data})!-- templates/admin/inventory_heatmap.html -- div classchart-container canvas idheatmapChart/canvas /div script const ctx document.getElementById(heatmapChart).getContext(2d); new Chart(ctx, { type: bar, data: { labels: [{% for d in data %}{{ d.name }}{% if not forloop.last %},{% endif %}{% endfor %}], datasets: [ { label: 可租, data: [{% for d in data %}{{ d.available }}{% if not forloop.last %},{% endif %}{% endfor %}], backgroundColor: #4CAF50 }, { label: 已租, data: [{% for d in data %}{{ d.rented }}{% if not forloop.last %},{% endif %}{% endfor %}], backgroundColor: #2196F3 }, { label: 待清洗, data: [{% for d in data %}{{ d.cleaning }}{% if not forloop.last %},{% endif %}{% endfor %}], backgroundColor: #FF9800 } ] }, options: { responsive: true, scales: { y: { beginAtZero: true, ticks: { stepSize: 1 } } } } }); /script参数说明stepSize: 1强制Y轴按整数刻度显示避免出现“2.5件”这种误导性数字。颜色用#4CAF50绿色代表可租符合“绿灯通行”的直觉#FF9800橙色代表待清洗暗示“需要处理”。3.2 租借趋势图按日统计租出件数不是订单数老板关心的是“每天实际租出去多少件汉服”不是“下了多少单”。因为一单可能含5件也可能只租1件。# views.py from datetime import timedelta def rental_trend(request): # 取最近14天数据 end_date timezone.now().date() start_date end_date - timedelta(days13) # 按日期聚合租借记录数注意是RentalRecord数量不是Order trend_data RentalRecord.objects.filter( rent_date__range[start_date, end_date] ).values(rent_date).annotate(countCount(id)).order_by(rent_date) # 补齐空缺日期避免图表断点 date_list [(start_date timedelta(daysi)) for i in range(14)] count_dict {item[rent_date]: item[count] for item in trend_data} counts [count_dict.get(d, 0) for d in date_list] return JsonResponse({ labels: [d.strftime(%m/%d) for d in date_list], data: counts })// 前端调用 fetch(/admin/rental-trend/) .then(r r.json()) .then(data { const ctx document.getElementById(trendChart).getContext(2d); new Chart(ctx, { type: line, data: { labels: data.labels, datasets: [{ label: 日租出件数, data: data.data, borderColor: #E91E63, tension: 0.3, // 曲线平滑度 fill: false }] } }); });血泪经验必须用RentalRecord而非Order统计。曾有个项目按订单统计结果老板发现“周末订单暴增”实际是摄影工作室一次性租20件系统只记1单完全掩盖了真实负荷。3.3 押金流水图实时显示押金账户余额变化财务最怕“钱去哪了”。这里不做复杂会计只做三列日期、收入新收押金、支出退还押金、余额。# views.py def deposit_flow(request): # 按日期聚合押金收支 flow_data RentalRecord.objects.values(rent_date).annotate( incomeCount(id, filterQ(deposit_paidTrue)), expenseCount(id, filterQ(deposit_refundedTrue)) ).order_by(rent_date) # 计算累计余额假设初始余额为0 balance 0 result [] for item in flow_data: balance item[income] - item[expense] result.append({ date: item[rent_date].strftime(%Y-%m-%d), income: item[income], expense: item[expense], balance: balance }) return JsonResponse({flow: result})!-- 表格比图表更直观 -- table classtable table-striped thead tr th日期/th th新增押金件/th th退还押金件/th th当前余额件/th /tr /thead tbody {% for row in flow %} tr td{{ row.date }}/td td{{ row.income }}/td td{{ row.expense }}/td tdstrong{{ row.balance }}/strong/td /tr {% endfor %} /tbody /table为什么用表格不用折线图因为老板要核对每一笔押金——比如“8月12日为什么退了3件是不是客户投诉了”表格支持CtrlF搜索折线图做不到。可视化不是炫技是降低决策成本。4. 避坑指南上线前必须绕开的7个真实陷阱附现象、原因、解法这些坑每一个都来自真实部署现场。不是理论推测是交过真金白银学费换来的。4.1 现象扫码入库后系统显示“可租”但手机端下单时提示“库存不足”原因CostumeItem.status字段被并发请求同时修改。例如A店员扫码入库设为availableB店员几乎同时扫码租出查到available后设为rented但A的save()晚于B的save()导致最终状态仍是rented入库失败。解法用select_for_update()加行锁而不是简单save()# 错误写法 item CostumeItem.objects.get(item_codeHZ2023-0876) item.status available item.save() # ❌ 并发时失效 # 正确写法 with transaction.atomic(): item CostumeItem.objects.select_for_update().get(item_codeHZ2023-0876) item.status available item.save() # ✅ 加锁保证原子性注意select_for_update()必须在transaction.atomic()内使用否则锁无效。Django默认事务隔离级别是READ COMMITTED足够应对门店级并发。4.2 现象客户用手机号1381234下单第二天另一个客户用1381235下单系统把两人记成同一人原因手机号字段没做标准化清洗。前端传入138 1234 1234、8613812341234、138-1234-1234后端没统一格式导致数据库存了多个变体。解法在Model的save()方法里清洗import re def save(self, *args, **kwargs): # 清洗手机号只保留11位数字 if self.customer_phone: cleaned re.sub(r\D, , self.customer_phone) if len(cleaned) 11 and cleaned.startswith(1): self.customer_phone cleaned else: raise ValueError(手机号格式错误请输入11位中国大陆手机号) super().save(*args, **kwargs)提示别信前端JS校验必须后端强制清洗。曾有客户用苹果手机自带键盘自动把138变成全角数字JS正则匹配失败后端直接存了乱码。4.3 现象Django Admin里点击“导出Excel”生成的文件打开是乱码中文全变成方块原因openpyxl默认用utf-8-sig编码写入但Windows Excel默认用GBK打开导致乱码。解法用csv替代xlsx并指定BOM头import csv from django.http import HttpResponse def export_to_csv(request): response HttpResponse(content_typetext/csv; charsetutf-8-sig) response[Content-Disposition] attachment; filenamerental_export.csv writer csv.writer(response) writer.writerow([租借日期, 客户姓名, 手机号, 汉服编号, 租金]) for record in RentalRecord.objects.all()[:1000]: writer.writerow([ record.rent_date, record.customer_name, record.customer_phone, record.costume_item.item_code, record.actual_rent_price ]) return response为什么不用xlsx因为openpyxl在Django中常与pandas冲突且生成文件大、慢。CSV兼容性100%老板用WPS/Excel都能直接打开。4.4 现象部署到Linux服务器后collectstatic报错OSError: [Errno 13] Permission denied原因STATIC_ROOT目录权限不够Django无法写入。常见于用root用户运行collectstatic但Web服务器如Nginx用www-data用户运行导致静态文件不可读。解法统一用部署用户操作并设置组权限# 创建部署用户 sudo adduser deployer sudo usermod -a -G www-data deployer # 设置static目录归属 sudo chown -R deployer:www-data /var/www/hanfu/static/ sudo chmod -R 775 /var/www/hanfu/static/ # 切换用户执行 sudo -u deployer python manage.py collectstatic --noinput玄学提醒千万别用chmod 777看似解决问题实则埋下安全雷。775rwxrwxr-x足够且符合Linux最小权限原则。4.5 现象客户归还汉服时店员在后台点“确认归还”但CostumeItem.status没变回available原因RentalRecord.return_date设为None时save()方法里的租金计算逻辑抛异常导致整个事务回滚状态变更也被撤销。解法把状态变更和租金计算拆成两个独立步骤并捕获异常def confirm_return(request, record_id): record get_object_or_404(RentalRecord, idrecord_id) try: # 第一步更新归还时间必成功 record.return_date timezone.now().date() record.save() # 第二步更新实物状态独立事务 with transaction.atomic(): item record.costume_item item.status available item.save() # 第三步记录日志 InventoryLog.objects.create( costume_itemitem, log_typereturn, operatorrequest.user.username, before_statusrented, after_statusavailable, rental_recordrecord ) except Exception as e: messages.error(request, f归还失败{str(e)}) return redirect(admin:rental_record_changelist) messages.success(request, 归还成功) return redirect(admin:rental_record_changelist)关键点return_date更新和status更新必须分离。前者是业务动作后者是资产状态失败不应互相拖累。4.6 现象用Chrome访问正常用Safari打开页面空白控制台报SyntaxError: Unexpected token ?原因前端用了ES2020的空值合并操作符??但Safari 13.1以下版本不支持。解法用Babel转译或改用兼容写法// ❌ 不兼容Safari const price item.base_price ?? 0; // ✅ 兼容所有浏览器 const price item.base_price ! undefined item.base_price ! null ? item.base_price : 0;避坑技巧Django模板里尽量少写复杂JS。把逻辑移到视图层计算好再传给模板前端只负责展示。这样既兼容又利于SEO。4.7 现象Django Admin搜索框输入“马面裙”搜不到CostumeType.name含“马面”的记录原因Django Admin默认用icontains做模糊搜索但中文分词没做导致“马面裙”被当整体匹配而数据库里存的是“明制马面裙”icontains查“马面裙”确实找不到。解法重写Admin的search_fields用__contains显式指定# admin.py class CostumeTypeAdmin(admin.ModelAdmin): list_display [name, category, base_rent_price] search_fields [name__contains, category] # ✅ 显式用contains # 或更精准用全文检索需PostgreSQL # search_fields [name, category] # def get_search_results(self, request, queryset, search_term): # if search_term: # queryset queryset.extra(where[name ILIKE %s], params[f%{search_term}%]) # return queryset, False注意__contains比__icontains性能略低但对汉服馆这种万级数据量完全够用。别一上来就搞全文检索徒增运维复杂度。5. 让老板愿意天天打开系统的3个细节技巧扫码入库、押金短信、微信通知系统好不好不看代码多优雅而看老板愿不愿意每天主动打开。这3个技巧每个都来自门店真实反馈投入不到2小时开发但让系统使用率从30%提升到92%。5.1 扫码入库用手机摄像头直接扫汉服标签1秒完成入库老板最烦的是“先拍照→再手动输编码→再选款式→再点保存”。扫码入库把流程压到1步# urls.py urlpatterns [ path(scan-in/, views.scan_in_view, namescan_in), ] # views.py def scan_in_view(request): if request.method POST: code request.POST.get(barcode) if not code: return render(request, admin/scan_in.html, {error: 请扫描条码}) try: # 查找实物编码 item CostumeItem.objects.get(item_codecode) # 自动设为可租 item.status available item.save() # 记录日志 InventoryLog.objects.create( costume_itemitem, log_typerent, operator扫码入库, before_statusunknown, after_statusavailable ) return render(request, admin/scan_in.html, { success: f入库成功{item.item_code} - {item.costume_type.name} }) except CostumeItem.DoesNotExist: return render(request, admin/scan_in.html, { error: f未找到编码{code}请检查标签是否正确 }) return render(request, admin/scan_in.html)!-- templates/admin/scan_in.html -- div classcontainer mt-4 h4扫码入库/h4 form methodpost {% csrf_token %} div classinput-group mb-3 input typetext namebarcode classform-control placeholder扫描汉服标签上的编码如 HZ2023-0876 autofocus button classbtn btn-primary typesubmit入库/button /div /form {% if success %} div classalert alert-success{{ success }}/div {% endif %} {% if error %} div classalert alert-danger{{ error }}/div {% endif %} /div为什么不用摄像头API因为门店WiFi信号差WebRTC扫码经常超时。直接让店员用手机微信“扫一扫”把结果粘贴到输入框稳定性和体验反而更好。技术要适配场景不是炫技。5.2 押金短信客户下单后自动发短信告知押金金额和支付方式老板说“客户总问押金怎么付我每天要回答20遍。” 解决方案集成国内短信平台如阿里云短信下单成功后自动触发# utils/sms.py import json import requests from django.conf import settings def send_deposit_sms(phone, amount, item_code): url https://sms.aliyuncs.com/ params { Action: SendSms, PhoneNumbers: phone, SignName: 汉服租赁, TemplateCode: SMS_234567890, # 阿里云审核通过的模板 TemplateParam: json.dumps({ amount: str(amount), item_code: item_code }) } headers {Authorization: fBearer {settings.ALIYUN_SMS_TOKEN}} response requests.post(url, paramsparams, headersheaders, timeout5) return response.json() # views.py 调用 def create_rental(request): # ... 创建RentalRecord逻辑 if record.deposit_paid: send_deposit_sms( phonerecord.customer_phone, amountrecord.costume_item.costume_type.deposit_amount, item_coderecord.costume_item.item_code )参数说明TemplateParam必须严格匹配阿里云模板中的变量名如amount、item_code否则发送失败。测试时务必用真实手机号沙箱环境不发短信。5.3 微信通知归还提醒用公众号模板消息比短信便宜80%短信贵微信模板消息免费。客户关注公众号后归还前3天自动推送# utils/wechat.py import requests import json def send_return_reminder(openid, item_name, due_date): access_token get_wechat_access_token() # 封装获取token逻辑 url fhttps://api.weixin.qq.com/cgi-bin/message/template/send?access_token{access_token} data { touser: openid, template_id: TEMPLATE_ID_HERE, # 公众号后台申请的模板ID data: { first: {value: 您的汉服租借即将到期}, keyword1: {value: item_name}, # 汉服名称 keyword2: {value: due_date.strftime(%Y年%m月%d日)}, # 应还日期 remark: {value: 请按时归还避免产生额外租金哦~} } } response requests.post(url, jsondata) return response.json() # 在RentalRecord创建时如果客户提供了openid就存下来 # 归还前3天用Celery定时任务触发send_return_reminder关键点openid必须在客户首次下单时授权获取用公众号网页授权不能硬塞。我们做了个妥协方案下单页加个“授权接收归还提醒”按钮勾选即授权不勾选就不发——尊重用户选择反而提升授权率。最后说句实在话这个系统上线后那家苏州汉服馆的老板再也不用每天下午三点蹲在电脑前对账了。他现在习惯早上来店第一件事打开系统看一眼热力图就知道今天该重点推哪几款客户归还前微信自动提醒他不用再打电话催月底财务直接导出CSV10分钟搞定报表。技术的价值从来不是代码多漂亮而是让真实的人少做一件重复、枯燥、容易出错的事。希望帮到你。本文还有配套的精品资源点击获取